From 1dc0064ce9743fcbd731e06ce006a31c27ad676a Mon Sep 17 00:00:00 2001 From: "damian.roth" Date: Fri, 21 Nov 2025 11:02:28 +0100 Subject: [PATCH 1/4] rebuild --- .github/COPILOT_INSTRUCTIONS.md | 696 ++++----- .github/workflows/ci.yml | 82 +- .gitignore | 418 +++--- .vscode/launch.json | 78 +- LICENSE | 1322 ++++++++--------- README.md | 72 +- data/nc-examples/O0004 | 60 +- data/nc-examples/O6POINT | 62 +- dev-requirements.txt | 8 +- docs/CGI_API.md | 328 ++-- docs/CODING_GUIDELINES.md | 240 +-- pyproject.toml | 52 +- scripts/run_example_O0004.py | 316 ++-- scripts/run_example_O6POINT.py | 538 +++---- src/ncplot7py/__init__.py | 14 +- src/ncplot7py/application/nc_execution.py | 442 +++--- src/ncplot7py/cli/main.py | 78 +- src/ncplot7py/domain/cnc_state.py | 390 ++--- src/ncplot7py/domain/exceptions.py | 278 ++-- src/ncplot7py/domain/exec_chain.py | 64 +- src/ncplot7py/domain/handlers/control_flow.py | 636 ++++---- .../gcode_group0_coordinate_set.py | 176 +-- .../fanuc_turn_cnc/gcode_group16_plane.py | 166 +-- .../fanuc_turn_cnc/gcode_group21_polar_co.py | 284 ++-- .../fanuc_turn_cnc/gcode_group2_speed_mode.py | 144 +- .../fanuc_turn_cnc/gcode_group5_feed_mode.py | 140 +- .../handlers/fanuc_turn_cnc/gcode_precheck.py | 70 +- src/ncplot7py/domain/handlers/modal.py | 84 +- src/ncplot7py/domain/handlers/motion.py | 688 ++++----- .../star_machine/star_turn_handler.py | 172 +-- src/ncplot7py/domain/handlers/variable.py | 454 +++--- src/ncplot7py/domain/i18n.py | 230 +-- src/ncplot7py/domain/models.py | 40 +- .../machines/star_canal_syncro.py | 472 +++--- .../machines/stateful_iso_turn_control.py | 506 +++---- .../machines/stateful_star_turn_control.py | 548 +++---- .../parsers/nc_command_parser.py | 266 ++-- .../interfaces/BaseLinkedListAsList.py | 92 +- src/ncplot7py/interfaces/BaseNCCanal.py | 362 ++--- src/ncplot7py/interfaces/BaseNCCommandNode.py | 116 +- .../interfaces/BaseNCCommandParser.py | 64 +- src/ncplot7py/interfaces/BaseNCControl.py | 106 +- src/ncplot7py/locales/__init__.py | 16 +- src/ncplot7py/locales/de.xml | 70 +- src/ncplot7py/locales/en.xml | 70 +- src/ncplot7py/shared/__init__.py | 66 +- src/ncplot7py/shared/file_adapter.py | 216 +-- src/ncplot7py/shared/linked_list_list.py | 154 +- src/ncplot7py/shared/logging_facade.py | 396 ++--- src/ncplot7py/shared/nc_nodes.py | 220 +-- src/ncplot7py/shared/point.py | 42 +- src/ncplot7py/shared/registry.py | 48 +- src/ncplot7py/shared/state_protocol.py | 66 +- tests/integration/test_example_O0004.py | 176 +-- tests/integration/test_hexagon_O6POINT.py | 202 +-- tests/integration/test_lathe_diameter_mode.py | 100 +- tests/integration/test_o6point_while_loop.py | 158 +- tests/integration/test_polar_plane_restore.py | 90 +- .../test_star_canal_syncro_integration.py | 182 +-- .../test_stateful_iso_turn_control.py | 70 +- tests/integration/test_while_do_end.py | 134 +- tests/unit/test_axis_units.py | 58 +- tests/unit/test_control_flow_more.py | 134 +- tests/unit/test_control_flow_variable.py | 102 +- tests/unit/test_error_helper.py | 66 +- tests/unit/test_exceptions.py | 64 +- tests/unit/test_fanuc_modals.py | 116 +- tests/unit/test_file_adapter.py | 96 +- tests/unit/test_gcode_group21.py | 108 +- tests/unit/test_gcode_precheck.py | 74 +- tests/unit/test_i18n_exceptions.py | 62 +- tests/unit/test_linked_list_list.py | 102 +- tests/unit/test_logging_facade.py | 152 +- tests/unit/test_nc_command_node.py | 66 +- tests/unit/test_nc_command_parser.py | 82 +- tests/unit/test_nc_execution.py | 174 +-- tests/unit/test_registry.py | 56 +- tests/unit/test_star_canal_syncro.py | 174 +-- tests/unit/test_star_turn_handler.py | 158 +- tests/unit/test_variable_nested.py | 74 +- 80 files changed, 7874 insertions(+), 7874 deletions(-) diff --git a/.github/COPILOT_INSTRUCTIONS.md b/.github/COPILOT_INSTRUCTIONS.md index cfbaf3a..86980a4 100644 --- a/.github/COPILOT_INSTRUCTIONS.md +++ b/.github/COPILOT_INSTRUCTIONS.md @@ -1,348 +1,348 @@ -# Copilot Instructions: Clean Architecture guidance for ncplot7py - -Purpose -------- -This document tells Copilot (or other AI assistants) how to produce code aligned with a clean, maintainable architecture for a large Python project. It also includes notes for TypeScript when the repo grows into multi-language or monorepo setups. - -High-level contract for suggestions ----------------------------------- -- Inputs: existing code context and a concrete change request (file snippet, issue, or feature description). -- Outputs: small, focused, well-typed code changes; updated tests; brief rationale in PR/commit message when behavior changes. -- Error modes: when a suggestion breaks API, include a migration note and tests; prefer additive changes. - -Core architecture principles ---------------------------- -- Layering: keep Domain (core rules), Application (use-cases), Interfaces/Adapters, and Infrastructure separate. -- Dependency rule: code dependencies point inward (outer layers depend on inner layers, inner layers know nothing about outer layers). -- Dependency inversion: use abstractions (Protocols/Interfaces) so application code depends on contracts, not concrete implementations. -- Side-effects at the edges: I/O (DB, network, filesystem, plotting libraries) belong only in infrastructure/adapters. - -Recommended repository layout (scalable) ----------------------------------------- -Use a src-style package layout. Example for a single large package: - -src/ - ncplot7py/ - domain/ - models.py # dataclasses, value objects, domain errors - services.py # pure domain logic - application/ - usecases.py # orchestrators that use domain services & ports - adapters/ - api.py # controllers/entrypoints (FastAPI, CLI) - serializers.py # mapping external <-> domain types - infrastructure/ - netcdf_reader.py # concrete I/O implementations - persistence.py # DB adapters - plotting_impl.py # plotting wrapper implementations - shared/ - config.py - types.py - __init__.py -tests/ - unit/ - integration/ -pyproject.toml -README.md - -For monorepos or multi-package setups, apply the same layer boundaries inside each package. - -Typing & interfaces -------------------- -- Python: use dataclasses for domain models, typing.Protocol for ports, and mypy/pyright in CI. -- Keep Protocols near the consumer (application/usecase) — not buried in infrastructure. - -How to structure a new feature (recommended steps) ------------------------------------------------ -1. Design domain types (dataclasses) and pure validations in `domain/`. -2. Implement a use-case in `application/` that depends on a small Protocol (port) for I/O. -3. Provide concrete implementations in `infrastructure/`. -4. Expose adapters in `adapters/` (HTTP/CLI) that translate external requests into domain inputs and call use-cases. -5. Add unit tests for domain and application; add integration tests for infrastructure with small test assets. - -Small illustrative pattern (reader example) ------------------------------------------ -# domain/models.py -from dataclasses import dataclass - -@dataclass -class Metadata: - variable_names: list[str] - attributes: dict - -# application/usecases.py -from typing import Protocol -from ..domain.models import Metadata - -class MetadataReader(Protocol): - def read(self, path: str) -> Metadata: ... - -def extract_metadata(path: str, reader: MetadataReader) -> Metadata: - md = reader.read(path) - # domain validation or transformations - return md - -# infrastructure/netcdf_reader.py -from ..domain.models import Metadata - -class NetCDFReader: - def read(self, path: str) -> Metadata: - # use xarray/netCDF4 to read and return domain.Metadata - ... - -Testing strategy ----------------- -- Unit tests: target `domain/` and `application/` only; mock ports. Fast and deterministic. -- Integration tests: test `infrastructure/` + `adapters/` with small real assets (temporary files, fixtures). -- Test pyramid: many unit tests, fewer integration tests, minimal end-to-end tests. - -Tooling & CI recommendations ---------------------------- -- Formatting: Black + isort. Linting: ruff/flake8. Type checks: mypy/pyright. -- Testing: prefer the standard library (`unittest`, `doctest`, `unittest.mock`). Use third-party test runners only with explicit justification and PR rationale. -- Packaging: `pyproject.toml` (PEP 621) with Poetry or pip-tools; keep `dependencies` minimal or empty unless runtime packages are justified. -- CI: run format, lint, typecheck, and the stdlib unit tests on PRs; run integration tests on main branch or scheduled runs. - -CI / cocompiler test commands (PowerShell) ------------------------------------------ -When running tests in a Windows PowerShell environment (or in CI that emulates it), set the `PYTHONPATH` to the repository `src` directory so imports resolve correctly. The project uses the standard library `unittest` discovery for both unit and integration tests. Example commands to include in your CI job or cocompiler step: - -```powershell -$env:PYTHONPATH = 'src'; python -m unittest discover -s tests/unit -p "test_*.py" -v -$env:PYTHONPATH = 'src'; python -m unittest discover -s tests/integration -p "test_*.py" -v -``` - -Place these commands in the CI job that runs tests. If your CI runner uses bash or another shell, adapt the equivalent `PYTHONPATH=src python -m unittest ...` syntax for that shell. - -Dependencies policy -------------------- -- Default: prefer Python standard library for runtime features. Do not add new third-party runtime dependencies unless there is a strong, documented justification. -- If a third-party package is absolutely necessary, prefer small, well-maintained packages available on PyPI and add them explicitly to `pyproject.toml` or `requirements.txt` with a short rationale in the related PR. -- Developer tooling (formatters, linters, type-checkers, test runners) are allowed as dev-dependencies, but should not be shipped as runtime requirements. -- Avoid bringing in heavy, opaque dependencies for simple functionality that can be implemented with the standard library. - -Coding guidelines ------------------ -Follow these practical rules for readable, maintainable code across the codebase. - -- Style and formatting - - Use Black formatting and an 88-character line width. Run `isort` for import ordering. - - Prefer expressive, explicit code over clever one-liners. - - Use f-strings for string interpolation. - -- Naming - - Modules and packages: short, lowercase, underscore-separated if needed (snake_case). - - Functions and variables: snake_case. - - Classes: PascalCase. - - Constants: UPPER_SNAKE_CASE. - -- Types & typing - - Add type hints for public functions and methods (PEP 484). Prefer `typing` generics over `Any`. - - Use `dataclasses` for domain models and `typing.Protocol` for interfaces/ports. - - Run static type checks in CI (mypy or pyright). Keep strictness where practical. - -- Docstrings & documentation - - Document public functions, classes and modules with Google or NumPy style docstrings. Include Args, Returns, and Raises sections for public APIs. - - Document public functions, classes and modules with Google or NumPy style docstrings. Include Args, Returns, and Raises sections for public APIs. - - Add short examples when behavior is non-obvious. - - Doctest guidelines - ------------------ - Follow these rules when adding doctest examples in docstrings so they run reliably with `python -m doctest` in CI. - - - Keep examples terse and deterministic - - Examples must run without interactive user input and produce the same output on every run. - - Avoid printing memory addresses, timestamps, UUIDs, or platform-dependent output. If randomness is required, set a fixed seed inside the example. - - - Importing in examples - - Use full imports that will work when the module is executed from the project root. Example: - >>> from ncplot7py.domain.models import Metadata - - - Use exact, stable output - - Show canonical string or repr output that is stable across Python versions when possible. - - For floating-point examples, either round the result in the example or use doctest directives such as `# doctest: +ELLIPSIS` to match approximate output. Example: - >>> round(0.1 + 0.2, 6) - 0.3 - >>> 0.1 + 0.2 # doctest: +ELLIPSIS - 0.3000000... - - - Use doctest option flags when needed - - Use `# doctest: +ELLIPSIS` to allow partial matching, or `# doctest: +NORMALIZE_WHITESPACE` if whitespace differs. - - Avoid overusing flags; prefer making examples precise. - - - Setup and long examples - - Keep long examples out of function docstrings; put them in `examples/` or `README.md` and test them separately if needed. - - If an example requires setup (creating temp files, sample NetCDF), show only the minimal reproducible snippet in the docstring and add a full integration example in `tests/integration/`. - - - Avoid side effects - - Do not perform destructive actions (deleting files, network calls) in doctest examples. If demonstrating I/O, use temporary files within the example or put the full scenario in integration tests. - - - Running doctests in CI - - CI should run: `python -m doctest -v src/ncplot7py/*.py` (or a narrower file list). The `pyproject.toml` includes a `doctest-command` showing the canonical command. - - - Example of a good doctest - def add(a, b): - """ - Return the sum of two numbers. - - >>> add(1, 2) - 3 - >>> round(add(0.1, 0.2), 6) - 0.3 - """ - return a + b - - - Notes for contributors - - Keep doctest examples focused on documenting observable behavior, not internal implementation. - - If you add or change doctest examples, run the doctest command locally before opening a PR. - -- Imports - - Use absolute imports within the package (from ncplot7py.xxx import Y). - - Group imports: stdlib, third-party, local (use isort to enforce). - -- Exceptions & error handling - - Prefer specific exceptions (ValueError, TypeError, etc.). Avoid bare except: clauses. - - Define domain-specific exception types in `domain/` when callers need to distinguish error classes. - - Fail fast and validate inputs early. Keep error messages clear and actionable. - -- Logging - - Use the standard `logging` module. Modules should get a logger via `logger = logging.getLogger(__name__)`. - - Do not use `print()` for production/logging; reserve `print()` only for quick debugging (remove before commit). - -- Resource management - - Use context managers for files, network connections and other resources (`with` statement). - - Avoid mutable default arguments. Use `None` and set defaults inside the function. - -- Concurrency - - Prefer simple solutions (threading, multiprocessing, asyncio) only when necessary. Encapsulate concurrency in infrastructure layer adapters and keep domain/use-cases synchronous unless the whole stack is async. - - - Tests - - Follow Arrange-Act-Assert. Keep unit tests fast and deterministic. - - Use the standard library `unittest` for unit tests (TestCase classes, setUp/tearDown, subTest) and `unittest.mock` for mocking. - - For table-driven tests, prefer `subTest` or explicit loops inside a TestCase rather than adding test framework dependencies. - - Use `doctest` for executable examples embedded in docstrings when helpful. - - For integration tests, use temporary directories (`tempfile.TemporaryDirectory`) and small sample assets. Clean up after tests. - -- Commits & PRs - - Commit message: short title (<=72 chars), optional body with rationale and any migration notes. - - PR description: explain what changed, why, tests added/updated, and any migration steps for consumers. - - Keep PRs focused: each PR should implement a single logical change or narrowly related set of changes. - -- Code review checklist - - Does the change respect layer boundaries (domain vs adapters vs infra)? - - Are side-effects localized to infrastructure/adapters? - - Are types and docstrings present for public APIs? - - Are there unit tests for domain/application logic and integration tests for adapters where needed? - - Is the change small and well-commented (or does it include a rationale in the PR)? - -Current project structure -------------------------- -The repository already follows the recommended clean-architecture layout. Use these locations when adding or editing files so Copilot suggestions target the correct layer. - -Top-level files -- `LICENSE`, `README.md`, `pyproject.toml`, `dev-requirements.txt` -- `.github/COPILOT_INSTRUCTIONS.md` (this file) - -Package layout (src-style) -- `src/ncplot7py/` — main package root - - `cli/` — CLI entrypoint and command wiring - - `main.py` — CLI implementation (simulate, plot subcommands) - - `domain/` — pure domain dataclasses and exceptions - - `models.py` — NCNode, ToolpathPoint, MachineSpec, SimulationResult - - `exceptions.py` — domain-specific exception types - - `application/` — use-cases / orchestrators - - `simulate.py` — parse -> simulate orchestration utilities - - `interfaces/` — Protocols / interface definitions - - `parser.py` — `Parser` Protocol - - `machine.py` — `MachineDriver` Protocol - - `plotter.py` — `Plotter` Protocol - - (intended) `nc_control.py` — NC control Protocol / base class (interface for NC controllers) - - `infrastructure/` — concrete adapters and implementations (I/O, drivers, plotters) - - `parsers/gcode_parser.py` — minimal G-code parser (registers as `gcode`) - - `machines/generic_machine.py` — simple generic machine driver (registers as `generic`) - - `plotters/matplotlib_plotter.py` — optional matplotlib adapter (registers `matplotlib` plotter) - - `persistence/` — storage adapters (file-based persistence stubs) - - `shared/` — small shared helpers and registry - - `registry.py` — runtime registry resolving parsers/machines/plotters - -Tests and examples -- `tests/unit/` — unit tests (e.g. `test_simulate_flow.py`) -- `tests/integration/` — integration tests (parsing+simulate+plot if optional deps installed) -- `examples/` and `data/nc-examples/` — sample NC files and fixtures for integration tests and demos - -Notes -- Optional dependencies (plotting) are declared under `pyproject.toml` extras: `plotting = ["matplotlib>=3.0,<4.0"]`. -- Concrete implementations register with the `shared.registry` so CLI and application code can resolve components by name (e.g. parser `gcode`, machine `generic`, plotter `matplotlib`). -- When Copilot generates code that crosses layers, update or create files in the matching package path above. - - -How Copilot should behave when editing code (updated) ----------------------------------------------------- -- When implementing features, prefer stdlib solutions first (os, pathlib, csv, json, tempfile, subprocess, builtins, typing, dataclasses). -- If a suggestion uses a third-party library, add a short justification comment and note that adding a dependency requires an explicit PR description and approval. -- For packages that must be added, include an update to `pyproject.toml` or `requirements.txt` and a brief migration note in the PR description. - -How Copilot should behave when editing code ------------------------------------------- -- Make minimal, well-scoped edits. If a feature crosses layers, create the smallest set of files required and update tests. -- Prefer adding a Protocol and refactoring callers to depend on it rather than editing many concrete implementations. -- Always include or update unit tests for any domain or application logic changes. - -Do / Don't (architecture-focused) ---------------------------------- -- Do: keep domain pure, add types and docstrings, add focused unit tests, and include migration notes for breaking changes. -- Don't: put business logic in HTTP handlers, scatter I/O into domain modules, or introduce large runtime deps without justification. - -Security --------- -- Never add secrets to the repo. Use environment variables or secrets managers and document required env vars in `.env.example`. - -Customization & next steps --------------------------- -This file is a living guideline. To change rules: -- Edit this file and commit. -- Optionally add CI checks to enforce specific rules (typecheck, linter, format). - -I can also scaffold starter artifacts if you want: -- `pyproject.toml` + minimal `src/` layout -- Example `MetadataReader` Protocol + `NetCDFReader` implementation + unit/integration tests -- A GitHub Actions workflow that runs Black, ruff/flake8, mypy, and the stdlib tests on PRs - - - - - -## Error handling (use the structured system) -- Use `ExceptionNode` and `ExceptionTyps` from `ncplot7py.domain.exceptions`. -- Prefer the helper `raise_nc_error(...)` to automatically populate trace and caret when possible. - -Example: -```python -from ncplot7py.domain.exceptions import ExceptionTyps, raise_nc_error - -if token not in allowed: - raise_nc_error( - ExceptionTyps.NCCodeErrors, - 1001, # see locales XML id "1:1001" - value=token, - file=filename, - line=lineno, - source_line=line_text, - ) -``` - --## Localization of messages -- Message templates are in `src/ncplot7py/locales/{lang}.xml`. -- Keys are `{typ_value}:{code}`. Use placeholders `{value}`, `{line}`, `{code}`, `{typ}`. -- To format for output: -```python -from ncplot7py.domain.i18n import MessageCatalog -text = MessageCatalog().format_exception(exc, lang="en") -``` - -## Tracing -- Provide `file`, `line`, `source_line` (and `value`) when raising errors to get a caret under the offending token. -- If you know the exact `column`, pass it; otherwise `raise_nc_error` will try to infer it by searching `value` in `source_line`. - - --- End of architecture-focused Copilot instructions -- +# Copilot Instructions: Clean Architecture guidance for ncplot7py + +Purpose +------- +This document tells Copilot (or other AI assistants) how to produce code aligned with a clean, maintainable architecture for a large Python project. It also includes notes for TypeScript when the repo grows into multi-language or monorepo setups. + +High-level contract for suggestions +---------------------------------- +- Inputs: existing code context and a concrete change request (file snippet, issue, or feature description). +- Outputs: small, focused, well-typed code changes; updated tests; brief rationale in PR/commit message when behavior changes. +- Error modes: when a suggestion breaks API, include a migration note and tests; prefer additive changes. + +Core architecture principles +--------------------------- +- Layering: keep Domain (core rules), Application (use-cases), Interfaces/Adapters, and Infrastructure separate. +- Dependency rule: code dependencies point inward (outer layers depend on inner layers, inner layers know nothing about outer layers). +- Dependency inversion: use abstractions (Protocols/Interfaces) so application code depends on contracts, not concrete implementations. +- Side-effects at the edges: I/O (DB, network, filesystem, plotting libraries) belong only in infrastructure/adapters. + +Recommended repository layout (scalable) +---------------------------------------- +Use a src-style package layout. Example for a single large package: + +src/ + ncplot7py/ + domain/ + models.py # dataclasses, value objects, domain errors + services.py # pure domain logic + application/ + usecases.py # orchestrators that use domain services & ports + adapters/ + api.py # controllers/entrypoints (FastAPI, CLI) + serializers.py # mapping external <-> domain types + infrastructure/ + netcdf_reader.py # concrete I/O implementations + persistence.py # DB adapters + plotting_impl.py # plotting wrapper implementations + shared/ + config.py + types.py + __init__.py +tests/ + unit/ + integration/ +pyproject.toml +README.md + +For monorepos or multi-package setups, apply the same layer boundaries inside each package. + +Typing & interfaces +------------------- +- Python: use dataclasses for domain models, typing.Protocol for ports, and mypy/pyright in CI. +- Keep Protocols near the consumer (application/usecase) — not buried in infrastructure. + +How to structure a new feature (recommended steps) +----------------------------------------------- +1. Design domain types (dataclasses) and pure validations in `domain/`. +2. Implement a use-case in `application/` that depends on a small Protocol (port) for I/O. +3. Provide concrete implementations in `infrastructure/`. +4. Expose adapters in `adapters/` (HTTP/CLI) that translate external requests into domain inputs and call use-cases. +5. Add unit tests for domain and application; add integration tests for infrastructure with small test assets. + +Small illustrative pattern (reader example) +----------------------------------------- +# domain/models.py +from dataclasses import dataclass + +@dataclass +class Metadata: + variable_names: list[str] + attributes: dict + +# application/usecases.py +from typing import Protocol +from ..domain.models import Metadata + +class MetadataReader(Protocol): + def read(self, path: str) -> Metadata: ... + +def extract_metadata(path: str, reader: MetadataReader) -> Metadata: + md = reader.read(path) + # domain validation or transformations + return md + +# infrastructure/netcdf_reader.py +from ..domain.models import Metadata + +class NetCDFReader: + def read(self, path: str) -> Metadata: + # use xarray/netCDF4 to read and return domain.Metadata + ... + +Testing strategy +---------------- +- Unit tests: target `domain/` and `application/` only; mock ports. Fast and deterministic. +- Integration tests: test `infrastructure/` + `adapters/` with small real assets (temporary files, fixtures). +- Test pyramid: many unit tests, fewer integration tests, minimal end-to-end tests. + +Tooling & CI recommendations +--------------------------- +- Formatting: Black + isort. Linting: ruff/flake8. Type checks: mypy/pyright. +- Testing: prefer the standard library (`unittest`, `doctest`, `unittest.mock`). Use third-party test runners only with explicit justification and PR rationale. +- Packaging: `pyproject.toml` (PEP 621) with Poetry or pip-tools; keep `dependencies` minimal or empty unless runtime packages are justified. +- CI: run format, lint, typecheck, and the stdlib unit tests on PRs; run integration tests on main branch or scheduled runs. + +CI / cocompiler test commands (PowerShell) +----------------------------------------- +When running tests in a Windows PowerShell environment (or in CI that emulates it), set the `PYTHONPATH` to the repository `src` directory so imports resolve correctly. The project uses the standard library `unittest` discovery for both unit and integration tests. Example commands to include in your CI job or cocompiler step: + +```powershell +$env:PYTHONPATH = 'src'; python -m unittest discover -s tests/unit -p "test_*.py" -v +$env:PYTHONPATH = 'src'; python -m unittest discover -s tests/integration -p "test_*.py" -v +``` + +Place these commands in the CI job that runs tests. If your CI runner uses bash or another shell, adapt the equivalent `PYTHONPATH=src python -m unittest ...` syntax for that shell. + +Dependencies policy +------------------- +- Default: prefer Python standard library for runtime features. Do not add new third-party runtime dependencies unless there is a strong, documented justification. +- If a third-party package is absolutely necessary, prefer small, well-maintained packages available on PyPI and add them explicitly to `pyproject.toml` or `requirements.txt` with a short rationale in the related PR. +- Developer tooling (formatters, linters, type-checkers, test runners) are allowed as dev-dependencies, but should not be shipped as runtime requirements. +- Avoid bringing in heavy, opaque dependencies for simple functionality that can be implemented with the standard library. + +Coding guidelines +----------------- +Follow these practical rules for readable, maintainable code across the codebase. + +- Style and formatting + - Use Black formatting and an 88-character line width. Run `isort` for import ordering. + - Prefer expressive, explicit code over clever one-liners. + - Use f-strings for string interpolation. + +- Naming + - Modules and packages: short, lowercase, underscore-separated if needed (snake_case). + - Functions and variables: snake_case. + - Classes: PascalCase. + - Constants: UPPER_SNAKE_CASE. + +- Types & typing + - Add type hints for public functions and methods (PEP 484). Prefer `typing` generics over `Any`. + - Use `dataclasses` for domain models and `typing.Protocol` for interfaces/ports. + - Run static type checks in CI (mypy or pyright). Keep strictness where practical. + +- Docstrings & documentation + - Document public functions, classes and modules with Google or NumPy style docstrings. Include Args, Returns, and Raises sections for public APIs. + - Document public functions, classes and modules with Google or NumPy style docstrings. Include Args, Returns, and Raises sections for public APIs. + - Add short examples when behavior is non-obvious. + + Doctest guidelines + ------------------ + Follow these rules when adding doctest examples in docstrings so they run reliably with `python -m doctest` in CI. + + - Keep examples terse and deterministic + - Examples must run without interactive user input and produce the same output on every run. + - Avoid printing memory addresses, timestamps, UUIDs, or platform-dependent output. If randomness is required, set a fixed seed inside the example. + + - Importing in examples + - Use full imports that will work when the module is executed from the project root. Example: + >>> from ncplot7py.domain.models import Metadata + + - Use exact, stable output + - Show canonical string or repr output that is stable across Python versions when possible. + - For floating-point examples, either round the result in the example or use doctest directives such as `# doctest: +ELLIPSIS` to match approximate output. Example: + >>> round(0.1 + 0.2, 6) + 0.3 + >>> 0.1 + 0.2 # doctest: +ELLIPSIS + 0.3000000... + + - Use doctest option flags when needed + - Use `# doctest: +ELLIPSIS` to allow partial matching, or `# doctest: +NORMALIZE_WHITESPACE` if whitespace differs. + - Avoid overusing flags; prefer making examples precise. + + - Setup and long examples + - Keep long examples out of function docstrings; put them in `examples/` or `README.md` and test them separately if needed. + - If an example requires setup (creating temp files, sample NetCDF), show only the minimal reproducible snippet in the docstring and add a full integration example in `tests/integration/`. + + - Avoid side effects + - Do not perform destructive actions (deleting files, network calls) in doctest examples. If demonstrating I/O, use temporary files within the example or put the full scenario in integration tests. + + - Running doctests in CI + - CI should run: `python -m doctest -v src/ncplot7py/*.py` (or a narrower file list). The `pyproject.toml` includes a `doctest-command` showing the canonical command. + + - Example of a good doctest + def add(a, b): + """ + Return the sum of two numbers. + + >>> add(1, 2) + 3 + >>> round(add(0.1, 0.2), 6) + 0.3 + """ + return a + b + + - Notes for contributors + - Keep doctest examples focused on documenting observable behavior, not internal implementation. + - If you add or change doctest examples, run the doctest command locally before opening a PR. + +- Imports + - Use absolute imports within the package (from ncplot7py.xxx import Y). + - Group imports: stdlib, third-party, local (use isort to enforce). + +- Exceptions & error handling + - Prefer specific exceptions (ValueError, TypeError, etc.). Avoid bare except: clauses. + - Define domain-specific exception types in `domain/` when callers need to distinguish error classes. + - Fail fast and validate inputs early. Keep error messages clear and actionable. + +- Logging + - Use the standard `logging` module. Modules should get a logger via `logger = logging.getLogger(__name__)`. + - Do not use `print()` for production/logging; reserve `print()` only for quick debugging (remove before commit). + +- Resource management + - Use context managers for files, network connections and other resources (`with` statement). + - Avoid mutable default arguments. Use `None` and set defaults inside the function. + +- Concurrency + - Prefer simple solutions (threading, multiprocessing, asyncio) only when necessary. Encapsulate concurrency in infrastructure layer adapters and keep domain/use-cases synchronous unless the whole stack is async. + + - Tests + - Follow Arrange-Act-Assert. Keep unit tests fast and deterministic. + - Use the standard library `unittest` for unit tests (TestCase classes, setUp/tearDown, subTest) and `unittest.mock` for mocking. + - For table-driven tests, prefer `subTest` or explicit loops inside a TestCase rather than adding test framework dependencies. + - Use `doctest` for executable examples embedded in docstrings when helpful. + - For integration tests, use temporary directories (`tempfile.TemporaryDirectory`) and small sample assets. Clean up after tests. + +- Commits & PRs + - Commit message: short title (<=72 chars), optional body with rationale and any migration notes. + - PR description: explain what changed, why, tests added/updated, and any migration steps for consumers. + - Keep PRs focused: each PR should implement a single logical change or narrowly related set of changes. + +- Code review checklist + - Does the change respect layer boundaries (domain vs adapters vs infra)? + - Are side-effects localized to infrastructure/adapters? + - Are types and docstrings present for public APIs? + - Are there unit tests for domain/application logic and integration tests for adapters where needed? + - Is the change small and well-commented (or does it include a rationale in the PR)? + +Current project structure +------------------------- +The repository already follows the recommended clean-architecture layout. Use these locations when adding or editing files so Copilot suggestions target the correct layer. + +Top-level files +- `LICENSE`, `README.md`, `pyproject.toml`, `dev-requirements.txt` +- `.github/COPILOT_INSTRUCTIONS.md` (this file) + +Package layout (src-style) +- `src/ncplot7py/` — main package root + - `cli/` — CLI entrypoint and command wiring + - `main.py` — CLI implementation (simulate, plot subcommands) + - `domain/` — pure domain dataclasses and exceptions + - `models.py` — NCNode, ToolpathPoint, MachineSpec, SimulationResult + - `exceptions.py` — domain-specific exception types + - `application/` — use-cases / orchestrators + - `simulate.py` — parse -> simulate orchestration utilities + - `interfaces/` — Protocols / interface definitions + - `parser.py` — `Parser` Protocol + - `machine.py` — `MachineDriver` Protocol + - `plotter.py` — `Plotter` Protocol + - (intended) `nc_control.py` — NC control Protocol / base class (interface for NC controllers) + - `infrastructure/` — concrete adapters and implementations (I/O, drivers, plotters) + - `parsers/gcode_parser.py` — minimal G-code parser (registers as `gcode`) + - `machines/generic_machine.py` — simple generic machine driver (registers as `generic`) + - `plotters/matplotlib_plotter.py` — optional matplotlib adapter (registers `matplotlib` plotter) + - `persistence/` — storage adapters (file-based persistence stubs) + - `shared/` — small shared helpers and registry + - `registry.py` — runtime registry resolving parsers/machines/plotters + +Tests and examples +- `tests/unit/` — unit tests (e.g. `test_simulate_flow.py`) +- `tests/integration/` — integration tests (parsing+simulate+plot if optional deps installed) +- `examples/` and `data/nc-examples/` — sample NC files and fixtures for integration tests and demos + +Notes +- Optional dependencies (plotting) are declared under `pyproject.toml` extras: `plotting = ["matplotlib>=3.0,<4.0"]`. +- Concrete implementations register with the `shared.registry` so CLI and application code can resolve components by name (e.g. parser `gcode`, machine `generic`, plotter `matplotlib`). +- When Copilot generates code that crosses layers, update or create files in the matching package path above. + + +How Copilot should behave when editing code (updated) +---------------------------------------------------- +- When implementing features, prefer stdlib solutions first (os, pathlib, csv, json, tempfile, subprocess, builtins, typing, dataclasses). +- If a suggestion uses a third-party library, add a short justification comment and note that adding a dependency requires an explicit PR description and approval. +- For packages that must be added, include an update to `pyproject.toml` or `requirements.txt` and a brief migration note in the PR description. + +How Copilot should behave when editing code +------------------------------------------ +- Make minimal, well-scoped edits. If a feature crosses layers, create the smallest set of files required and update tests. +- Prefer adding a Protocol and refactoring callers to depend on it rather than editing many concrete implementations. +- Always include or update unit tests for any domain or application logic changes. + +Do / Don't (architecture-focused) +--------------------------------- +- Do: keep domain pure, add types and docstrings, add focused unit tests, and include migration notes for breaking changes. +- Don't: put business logic in HTTP handlers, scatter I/O into domain modules, or introduce large runtime deps without justification. + +Security +-------- +- Never add secrets to the repo. Use environment variables or secrets managers and document required env vars in `.env.example`. + +Customization & next steps +-------------------------- +This file is a living guideline. To change rules: +- Edit this file and commit. +- Optionally add CI checks to enforce specific rules (typecheck, linter, format). + +I can also scaffold starter artifacts if you want: +- `pyproject.toml` + minimal `src/` layout +- Example `MetadataReader` Protocol + `NetCDFReader` implementation + unit/integration tests +- A GitHub Actions workflow that runs Black, ruff/flake8, mypy, and the stdlib tests on PRs + + + + + +## Error handling (use the structured system) +- Use `ExceptionNode` and `ExceptionTyps` from `ncplot7py.domain.exceptions`. +- Prefer the helper `raise_nc_error(...)` to automatically populate trace and caret when possible. + +Example: +```python +from ncplot7py.domain.exceptions import ExceptionTyps, raise_nc_error + +if token not in allowed: + raise_nc_error( + ExceptionTyps.NCCodeErrors, + 1001, # see locales XML id "1:1001" + value=token, + file=filename, + line=lineno, + source_line=line_text, + ) +``` + +-## Localization of messages +- Message templates are in `src/ncplot7py/locales/{lang}.xml`. +- Keys are `{typ_value}:{code}`. Use placeholders `{value}`, `{line}`, `{code}`, `{typ}`. +- To format for output: +```python +from ncplot7py.domain.i18n import MessageCatalog +text = MessageCatalog().format_exception(exc, lang="en") +``` + +## Tracing +- Provide `file`, `line`, `source_line` (and `value`) when raising errors to get a caret under the offending token. +- If you know the exact `column`, pass it; otherwise `raise_nc_error` will try to infer it by searching `value` in `source_line`. + + +-- End of architecture-focused Copilot instructions -- diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b098a35..7aedcf1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,41 +1,41 @@ -name: CI - -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - -jobs: - test: - name: Test on ${{ matrix.os }} / Python ${{ matrix.python-version }} - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, windows-latest] - python-version: [3.10, 3.11] - env: - # Ensure imports can resolve package under src/ - PYTHONPATH: src - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - - name: Upgrade pip - run: python -m pip install --upgrade pip - - - name: Install dev dependencies - run: | - if [ -f dev-requirements.txt ]; then pip install -r dev-requirements.txt; fi - - - name: Run unit tests - run: python -m unittest discover -s tests -p "test_*.py" - - - name: Run doctests - run: python -m doctest -v src/ncplot7py/*.py +name: CI + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + test: + name: Test on ${{ matrix.os }} / Python ${{ matrix.python-version }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + python-version: [3.10, 3.11] + env: + # Ensure imports can resolve package under src/ + PYTHONPATH: src + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Upgrade pip + run: python -m pip install --upgrade pip + + - name: Install dev dependencies + run: | + if [ -f dev-requirements.txt ]; then pip install -r dev-requirements.txt; fi + + - name: Run unit tests + run: python -m unittest discover -s tests -p "test_*.py" + + - name: Run doctests + run: python -m doctest -v src/ncplot7py/*.py diff --git a/.gitignore b/.gitignore index 82ee2a8..0f9cbd0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,209 +1,209 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[codz] -*$py.class -*cgiserver.cgi - - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py.cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# UV -# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -#uv.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock -#poetry.toml - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. -# https://pdm-project.org/en/latest/usage/project/#working-with-version-control -#pdm.lock -#pdm.toml -.pdm-python -.pdm-build/ - -# pixi -# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. -#pixi.lock -# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one -# in the .venv directory. It is recommended not to include this directory in version control. -.pixi - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.envrc -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ - -# Abstra -# Abstra is an AI-powered process automation framework. -# Ignore directories containing user credentials, local state, and settings. -# Learn more at https://abstra.io/docs -.abstra/ - -# Visual Studio Code -# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore -# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore -# and can be added to the global gitignore or merged into this file. However, if you prefer, -# you could uncomment the following to ignore the entire vscode folder -# .vscode/ - -# Ruff stuff: -.ruff_cache/ - -# PyPI configuration file -.pypirc - -# Cursor -# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to -# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data -# refer to https://docs.cursor.com/context/ignore-files -.cursorignore -.cursorindexingignore - -# Marimo -marimo/_static/ -marimo/_lsp/ -__marimo__/ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[codz] +*$py.class +*cgiserver.cgi + + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py.cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock +#poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +#pdm.lock +#pdm.toml +.pdm-python +.pdm-build/ + +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +#pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.envrc +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Cursor +# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to +# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data +# refer to https://docs.cursor.com/context/ignore-files +.cursorignore +.cursorindexingignore + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ diff --git a/.vscode/launch.json b/.vscode/launch.json index 53694b9..e953c35 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,39 +1,39 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "name": "Python: Current File (with PYTHONPATH)", - "type": "python", - "request": "launch", - "program": "${file}", - "console": "integratedTerminal", - "env": { - "PYTHONPATH": "${workspaceFolder}/src" - }, - "envFile": "${workspaceFolder}/.env" - }, - { - "name": "Python: Run example_O0004.py (with PYTHONPATH)", - "type": "python", - "request": "launch", - "program": "${workspaceFolder}/scripts/run_example_O0004.py", - "console": "integratedTerminal", - "env": { - "PYTHONPATH": "${workspaceFolder}/src" - }, - "envFile": "${workspaceFolder}/.env" - }, - { - "name": "Python: Unittest (discover, with PYTHONPATH)", - "type": "python", - "request": "launch", - "module": "unittest", - "args": ["discover", "-s", "tests", "-p", "test_*.py"], - "console": "integratedTerminal", - "env": { - "PYTHONPATH": "${workspaceFolder}/src" - }, - "envFile": "${workspaceFolder}/.env" - } - ] -} +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Current File (with PYTHONPATH)", + "type": "python", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal", + "env": { + "PYTHONPATH": "${workspaceFolder}/src" + }, + "envFile": "${workspaceFolder}/.env" + }, + { + "name": "Python: Run example_O0004.py (with PYTHONPATH)", + "type": "python", + "request": "launch", + "program": "${workspaceFolder}/scripts/run_example_O0004.py", + "console": "integratedTerminal", + "env": { + "PYTHONPATH": "${workspaceFolder}/src" + }, + "envFile": "${workspaceFolder}/.env" + }, + { + "name": "Python: Unittest (discover, with PYTHONPATH)", + "type": "python", + "request": "launch", + "module": "unittest", + "args": ["discover", "-s", "tests", "-p", "test_*.py"], + "console": "integratedTerminal", + "env": { + "PYTHONPATH": "${workspaceFolder}/src" + }, + "envFile": "${workspaceFolder}/.env" + } + ] +} diff --git a/LICENSE b/LICENSE index 0ad25db..ada1a81 100644 --- a/LICENSE +++ b/LICENSE @@ -1,661 +1,661 @@ - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU Affero General Public License is a free, copyleft license for -software and other kinds of works, specifically designed to ensure -cooperation with the community in the case of network server software. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -our General Public Licenses are intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - Developers that use our General Public Licenses protect your rights -with two steps: (1) assert copyright on the software, and (2) offer -you this License which gives you legal permission to copy, distribute -and/or modify the software. - - A secondary benefit of defending all users' freedom is that -improvements made in alternate versions of the program, if they -receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and -encouraged by the resulting cooperation. However, in the case of -software used on network servers, this result may fail to come about. -The GNU General Public License permits making a modified version and -letting the public access it on a server without ever releasing its -source code to the public. - - The GNU Affero General Public License is designed specifically to -ensure that, in such cases, the modified source code becomes available -to the community. It requires the operator of a network server to -provide the source code of the modified version running there to the -users of that server. Therefore, public use of a modified version, on -a publicly accessible server, gives the public access to the source -code of the modified version. - - An older license, called the Affero General Public License and -published by Affero, was designed to accomplish similar goals. This is -a different license, not a version of the Affero GPL, but Affero has -released a new version of the Affero GPL which permits relicensing under -this license. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU Affero General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Remote Network Interaction; Use with the GNU General Public License. - - Notwithstanding any other provision of this License, if you modify the -Program, your modified version must prominently offer all users -interacting with it remotely through a computer network (if your version -supports such interaction) an opportunity to receive the Corresponding -Source of your version by providing access to the Corresponding Source -from a network server at no charge, through some standard or customary -means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 -of the GNU General Public License that is incorporated pursuant to the -following paragraph. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the work with which it is combined will remain governed by version -3 of the GNU General Public License. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU Affero General Public License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU Affero General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU Affero General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU Affero General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If your software can interact with users remotely through a computer -network, you should also make sure that it provides a way for users to -get its source. For example, if your program is a web application, its -interface could display a "Source" link that leads users to an archive -of the code. There are many ways you could offer source, and different -solutions will be better for different programs; see section 13 for the -specific requirements. - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU AGPL, see -. + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/README.md b/README.md index 3c53573..d0499dc 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,37 @@ -# ncplot7py -NC Code Plot for CNC Code - - - -# run Tests -& C:/Users/Damian/Project/ncplot7py/ncplot7py/.venv/Scripts/Activate.ps1 - - $env:PYTHONPATH = 'src'; python -m unittest discover -s tests/unit -p "test_*.py" -v - - $env:PYTHONPATH='src'; python -m unittest discover -s tests/integration -p "test_*.py" -v - -## Error handling and i18n - -This project provides structured, localized errors for CNC/NC parsing and runtime. - -- Use `ExceptionNode` and `ExceptionTyps` from `ncplot7py.domain.exceptions`. -- Prefer `raise_nc_error(...)` to attach file/line/column/context and infer the caret position. --- Localize via XML catalogs under `src/ncplot7py/locales/{lang}.xml` using keys `{typ_value}:{code}`. - -Quick example: - -```python -from ncplot7py.domain.exceptions import ExceptionTyps, raise_nc_error -from ncplot7py.domain.i18n import MessageCatalog - -try: - raise_nc_error( - ExceptionTyps.NCCodeErrors, 1001, - value="M30", file="program.nc", line=12, - source_line="N12 G1 X10 Y10 M30", - ) -except Exception as exc: - print(MessageCatalog().format_exception(exc, lang="en")) -``` - +# ncplot7py +NC Code Plot for CNC Code + + + +# run Tests +& C:/Users/Damian/Project/ncplot7py/ncplot7py/.venv/Scripts/Activate.ps1 + + $env:PYTHONPATH = 'src'; python -m unittest discover -s tests/unit -p "test_*.py" -v + + $env:PYTHONPATH='src'; python -m unittest discover -s tests/integration -p "test_*.py" -v + +## Error handling and i18n + +This project provides structured, localized errors for CNC/NC parsing and runtime. + +- Use `ExceptionNode` and `ExceptionTyps` from `ncplot7py.domain.exceptions`. +- Prefer `raise_nc_error(...)` to attach file/line/column/context and infer the caret position. +-- Localize via XML catalogs under `src/ncplot7py/locales/{lang}.xml` using keys `{typ_value}:{code}`. + +Quick example: + +```python +from ncplot7py.domain.exceptions import ExceptionTyps, raise_nc_error +from ncplot7py.domain.i18n import MessageCatalog + +try: + raise_nc_error( + ExceptionTyps.NCCodeErrors, 1001, + value="M30", file="program.nc", line=12, + source_line="N12 G1 X10 Y10 M30", + ) +except Exception as exc: + print(MessageCatalog().format_exception(exc, lang="en")) +``` + See `docs/CODING_GUIDELINES.md` and `docs/COPILOT.md` for details. \ No newline at end of file diff --git a/data/nc-examples/O0004 b/data/nc-examples/O0004 index d0734f0..8f4b95b 100644 --- a/data/nc-examples/O0004 +++ b/data/nc-examples/O0004 @@ -1,31 +1,31 @@ -% -O8976(EXAMPLE) -M81 -GOTO789 -M99 -X5 -#500 = 1 -#600 =[#500 / 3] -#50 = -4 -U#600 - -#500 = 1 -#600 = #500 - 3 -#50 = -4 -T#600 -G#50 / 4 -#70 = -4 + 4 * 2 -X#70 / 4 - -% -O8976(EXAMPLE) -M200 -M20 -G98 G1 Z20.0 F100 -G2 X10 R40.0 -G1 C10.0 -G1 X0 -G3 Z10 R1 -% -G266A6.W13.S2000.X15.Z15.F0.02B2.0T100. +% +O8976(EXAMPLE) +M81 +GOTO789 +M99 +X5 +#500 = 1 +#600 =[#500 / 3] +#50 = -4 +U#600 + +#500 = 1 +#600 = #500 - 3 +#50 = -4 +T#600 +G#50 / 4 +#70 = -4 + 4 * 2 +X#70 / 4 + +% +O8976(EXAMPLE) +M200 +M20 +G98 G1 Z20.0 F100 +G2 X10 R40.0 +G1 C10.0 +G1 X0 +G3 Z10 R1 +% +G266A6.W13.S2000.X15.Z15.F0.02B2.0T100. % \ No newline at end of file diff --git a/data/nc-examples/O6POINT b/data/nc-examples/O6POINT index 34e5ff8..fba1918 100644 --- a/data/nc-examples/O6POINT +++ b/data/nc-examples/O6POINT @@ -1,31 +1,31 @@ -O0001(SB12 RG FRONT) -T1400 -M36S3000 -G99 -F200 G98 - -G112 -(TEST) - -#100 =0 -#101 = 2 -WHILE[#100 LT #101]DO1 -#100 = #100 + 1 -G1 X0 C0 -G1 X-8.0 C0 -G1 X-8.0 C-1.73 -G3 X-7.0 C-2.66 R1.0 -G1 X-1.0 C-4.33 -G3 X1.0 C-4.33 R1.0 -G1 X7.0 C-2.66 -G3 X8.0 C-1.73 R1.0 -G1 X8.0 C1.73 -G3 X7.0 C2.66 R1.0 -G1 X1.0 C4.33 -G3 X-1.0 C4.33 R1.0 -G1 X-7.0 C2.66 -G3 X-8.0 C1.73 R1.0 -G1 X-8.0 C0 -G1W1.0 -END1 -G113 +O0001(SB12 RG FRONT) +T1400 +M36S3000 +G99 +F200 G98 + +G112 +(TEST) + +#100 =0 +#101 = 2 +WHILE[#100 LT #101]DO1 +#100 = #100 + 1 +G1 X0 C0 +G1 X-8.0 C0 +G1 X-8.0 C-1.73 +G3 X-7.0 C-2.66 R1.0 +G1 X-1.0 C-4.33 +G3 X1.0 C-4.33 R1.0 +G1 X7.0 C-2.66 +G3 X8.0 C-1.73 R1.0 +G1 X8.0 C1.73 +G3 X7.0 C2.66 R1.0 +G1 X1.0 C4.33 +G3 X-1.0 C4.33 R1.0 +G1 X-7.0 C2.66 +G3 X-8.0 C1.73 R1.0 +G1 X-8.0 C0 +G1W1.0 +END1 +G113 diff --git a/dev-requirements.txt b/dev-requirements.txt index 43bf9cf..0112def 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,4 +1,4 @@ -# Development requirements (install with `pip install -r dev-requirements.txt`) -# These are dev-only tools; runtime code should prefer the Python stdlib and keep -# `dependencies` in `pyproject.toml` empty unless an explicit justification exists. - +# Development requirements (install with `pip install -r dev-requirements.txt`) +# These are dev-only tools; runtime code should prefer the Python stdlib and keep +# `dependencies` in `pyproject.toml` empty unless an explicit justification exists. + diff --git a/docs/CGI_API.md b/docs/CGI_API.md index 01ba18f..f943420 100644 --- a/docs/CGI_API.md +++ b/docs/CGI_API.md @@ -1,165 +1,165 @@ -# CGI API for `scripts/cgiserver.cgi` - -This document describes the CGI interface implemented by `scripts/cgiserver.cgi`. - -## Overview -- The CGI script accepts a JSON POST and returns a JSON response. -- The script logs the request into a MariaDB table (if DB credentials are configured). -- The core processing runs the project's NC execution engine and returns syncro plot data. - -## Request payload shapes -Two accepted shapes: - -1) Object with `machinedata` list: - -{ - "machinedata": [ - { "program": "", "machineName": "", "canalNr": }, - ... - ] -} - -2) Direct list of machine-data objects: - -[ - { "program": "", "machineName": "", "canalNr": "" }, - ... -] - -Each machine-data entry must include `program`, `machineName`, and `canalNr`. - -## Allowed machine names -- SB12RG_F -- FANUC_T -- SR20JII_F -- SB12RG_B -- SR20JII_B -- ISO_MILL - -## Validation rules -- Top-level must be an object containing `machinedata` or a list. -- Each entry must have `program`, `machineName`, and `canalNr`. -- `machineName` must be one of the allowed names above. -- `program` must NOT contain any of these characters: `(`, `)`, `{`, `}` — payloads containing them are rejected. - -## Server-side preprocessing -- The script removes substrings matching `\(.*\)` from the program (comments in parentheses). -- Newlines are converted to semicolons: `\n` -> `;`. -- Spaces are removed from the program string. - -## Side effects / logging -- The script attempts to insert a row into MariaDB `log.logNCR` with columns `IP` and `POST`. -- IP is taken from `REMOTE_ADDR` environment variable (trimmed to 19 characters) or "NAN" if missing. -- POST body is truncated when logged (approx 1000-1500 characters). -- MariaDB credentials must be provided in the running environment or script. - -## Processing flow -- Builds initial `CNCState` instances per machine name. -- Sets X axis unit to `diameter` for lathe-style machines (SB12 and SR20, FANUC_T). -- Creates a `StatefulIsoTurnNCControl` with `count_of_canals`, `canal_names`, and initial `CNCState` list. -- Instantiates `NCExecutionEngine(control)` and calls `engine.get_Syncro_plot(programs, True)`. - -## Response format -On success, a JSON object similar to: - -{ - "canal": , - "message": -} - -- `canal` contains the syncro plot data returned by the execution engine. -- `message` is the project's message stack (diagnostics/info accumulated during processing). - -On error, the script returns a JSON-like error message such as: - -{"message_TEST": "", "program": [ ... ]} - -or - -{"message_T": "", "program": []} - -(Exact shape may vary depending on where the exception was raised.) - -## Example request (JSON body) - -Single program (object with `machinedata`): - -{ - "machinedata": [ - { - "program": "N10 G00 X0 Y0\nN20 G01 X10 Y0", - "machineName": "SB12RG_F", - "canalNr": 1 - } - ] -} - -Equivalent as a plain list: - -[ - { - "program": "N10 G00 X0 Y0\nN20 G01 X10 Y0", - "machineName": "SB12RG_F", - "canalNr": "canal1" - } -] - -## Example PowerShell POST (replace URL) - -```powershell -$json = @' -{ - "machinedata": [ - { "program": "N10 G00 X0 Y0\nN20 G01 X10 Y0", "machineName": "SB12RG_F", "canalNr": 1 } - ] -} -'@ - -Invoke-RestMethod -Uri 'https://your-server/cgi-bin/cgiserver.cgi' -Method Post -Body $json -ContentType 'application/json' -``` - -## Example response (success) - -```json -{ - "canal": [ /* engine-specific syncro plot structure */ ], - "message": [ /* diagnostic messages */ ] -} -``` - -## Notes / caveats -- The script has two `request_precheck` blocks; the later one is authoritative for validation. -- Ensure MariaDB credentials and connectivity are configured in the environment where the CGI runs. -- Clients need not pre-normalize newlines or spaces; the server will remove spaces and convert newlines to semicolons, but clients must avoid forbidden characters. - -## Listing available machines (new) - -The CGI supports a lightweight request to retrieve the available machine names and a simple control type description. - -Request JSON body: - -{ - "action": "list_machines" -} - -or - -{ - "action": "get_machines" -} - -Response JSON body: - -{ - "machines": [ - { "machineName": "SB12RG_F", "controlType": "StatefulIsoTurnNCControl" }, - { "machineName": "SB12RG_B", "controlType": "StatefulIsoTurnNCControl" }, - ... - ] -} - -This is useful for clients to discover supported machine names before sending processing requests. - ---- - +# CGI API for `scripts/cgiserver.cgi` + +This document describes the CGI interface implemented by `scripts/cgiserver.cgi`. + +## Overview +- The CGI script accepts a JSON POST and returns a JSON response. +- The script logs the request into a MariaDB table (if DB credentials are configured). +- The core processing runs the project's NC execution engine and returns syncro plot data. + +## Request payload shapes +Two accepted shapes: + +1) Object with `machinedata` list: + +{ + "machinedata": [ + { "program": "", "machineName": "", "canalNr": }, + ... + ] +} + +2) Direct list of machine-data objects: + +[ + { "program": "", "machineName": "", "canalNr": "" }, + ... +] + +Each machine-data entry must include `program`, `machineName`, and `canalNr`. + +## Allowed machine names +- SB12RG_F +- FANUC_T +- SR20JII_F +- SB12RG_B +- SR20JII_B +- ISO_MILL + +## Validation rules +- Top-level must be an object containing `machinedata` or a list. +- Each entry must have `program`, `machineName`, and `canalNr`. +- `machineName` must be one of the allowed names above. +- `program` must NOT contain any of these characters: `(`, `)`, `{`, `}` — payloads containing them are rejected. + +## Server-side preprocessing +- The script removes substrings matching `\(.*\)` from the program (comments in parentheses). +- Newlines are converted to semicolons: `\n` -> `;`. +- Spaces are removed from the program string. + +## Side effects / logging +- The script attempts to insert a row into MariaDB `log.logNCR` with columns `IP` and `POST`. +- IP is taken from `REMOTE_ADDR` environment variable (trimmed to 19 characters) or "NAN" if missing. +- POST body is truncated when logged (approx 1000-1500 characters). +- MariaDB credentials must be provided in the running environment or script. + +## Processing flow +- Builds initial `CNCState` instances per machine name. +- Sets X axis unit to `diameter` for lathe-style machines (SB12 and SR20, FANUC_T). +- Creates a `StatefulIsoTurnNCControl` with `count_of_canals`, `canal_names`, and initial `CNCState` list. +- Instantiates `NCExecutionEngine(control)` and calls `engine.get_Syncro_plot(programs, True)`. + +## Response format +On success, a JSON object similar to: + +{ + "canal": , + "message": +} + +- `canal` contains the syncro plot data returned by the execution engine. +- `message` is the project's message stack (diagnostics/info accumulated during processing). + +On error, the script returns a JSON-like error message such as: + +{"message_TEST": "", "program": [ ... ]} + +or + +{"message_T": "", "program": []} + +(Exact shape may vary depending on where the exception was raised.) + +## Example request (JSON body) + +Single program (object with `machinedata`): + +{ + "machinedata": [ + { + "program": "N10 G00 X0 Y0\nN20 G01 X10 Y0", + "machineName": "SB12RG_F", + "canalNr": 1 + } + ] +} + +Equivalent as a plain list: + +[ + { + "program": "N10 G00 X0 Y0\nN20 G01 X10 Y0", + "machineName": "SB12RG_F", + "canalNr": "canal1" + } +] + +## Example PowerShell POST (replace URL) + +```powershell +$json = @' +{ + "machinedata": [ + { "program": "N10 G00 X0 Y0\nN20 G01 X10 Y0", "machineName": "SB12RG_F", "canalNr": 1 } + ] +} +'@ + +Invoke-RestMethod -Uri 'https://your-server/cgi-bin/cgiserver.cgi' -Method Post -Body $json -ContentType 'application/json' +``` + +## Example response (success) + +```json +{ + "canal": [ /* engine-specific syncro plot structure */ ], + "message": [ /* diagnostic messages */ ] +} +``` + +## Notes / caveats +- The script has two `request_precheck` blocks; the later one is authoritative for validation. +- Ensure MariaDB credentials and connectivity are configured in the environment where the CGI runs. +- Clients need not pre-normalize newlines or spaces; the server will remove spaces and convert newlines to semicolons, but clients must avoid forbidden characters. + +## Listing available machines (new) + +The CGI supports a lightweight request to retrieve the available machine names and a simple control type description. + +Request JSON body: + +{ + "action": "list_machines" +} + +or + +{ + "action": "get_machines" +} + +Response JSON body: + +{ + "machines": [ + { "machineName": "SB12RG_F", "controlType": "StatefulIsoTurnNCControl" }, + { "machineName": "SB12RG_B", "controlType": "StatefulIsoTurnNCControl" }, + ... + ] +} + +This is useful for clients to discover supported machine names before sending processing requests. + +--- + Generated from `scripts/cgiserver.cgi` in the repository. If you'd like, I can also add an automated test or a small example script under `scripts/` to POST a sample request and save the response. \ No newline at end of file diff --git a/docs/CODING_GUIDELINES.md b/docs/CODING_GUIDELINES.md index d89948c..0cb89f2 100644 --- a/docs/CODING_GUIDELINES.md +++ b/docs/CODING_GUIDELINES.md @@ -1,120 +1,120 @@ -# Coding Guidelines - -This project favors clear, small, stdlib-first Python with helpful errors and tests. The key conventions are below. - -"""Contributor codeline examples. - -This module contains a minimal, stdlib-only example showing a write-only helper -function. It includes documentation and doctest examples so CI can run -`python -m doctest` against it. - -Guidelines followed: -- Use only Python standard library. -- Keep examples deterministic and self-cleaning (remove temporary files). -- Provide a small doctest demonstrating usage. - -""" -from __future__ import annotations - -from pathlib import Path -from typing import Union - - -def write_text(path: Union[str, Path], text: str, append: bool = False) -> None: - """Write text to a file using only standard library functions. - - Args: - path: File path to write to. Can be a string or Path. - text: Text content to write. - append: If True, append to the file; otherwise overwrite. - - Returns: - None - - Doctest example (runs deterministically): - - >>> # create a small file, read it back, then remove it - >>> write_text('tmp_test.txt', 'hello') - >>> open('tmp_test.txt', 'r', encoding='utf-8').read() - 'hello' - >>> # clean up - >>> import os - >>> os.remove('tmp_test.txt') - - """ - mode = 'a' if append else 'w' - p = Path(path) - # Ensure parent directory exists when a Path with directories is provided - if p.parent and not p.parent.exists(): - p.parent.mkdir(parents=True, exist_ok=True) - - # Use explicit encoding for reproducible behavior across platforms - with p.open(mode, encoding='utf-8') as fh: - fh.write(text) - - -__all__ = ["write_text"] - - -## Error handling and i18n - -Use the structured exception `ExceptionNode` with localized messages. - -- Exception types: `ExceptionTyps` (IntEnum) - - NCCodeErrors = 1 - - NCCanalStarErrors = 2 - - CNCError = 3 -- Raise errors via helper `raise_nc_error` to include trace and infer column: - - ```python - from ncplot7py.domain.exceptions import ExceptionTyps, raise_nc_error - - raise_nc_error( - ExceptionTyps.NCCodeErrors, - 1001, # maps to XML key "1:1001" - value="M30", # offending token (used in message formatting) - file="program.nc", - line=12, - source_line="N12 G1 X10 Y10 M30", # for caret positioning and context - ) - ``` - -- Exception fields available on `ExceptionNode`: - - `typ`, `code`, `line`, `message`, `value` - - Trace: `file`, `column`, `context` - - `localized(lang)` method returns a localized string with trace. - --### Message catalog (XML) -- Messages live under `src/ncplot7py/locales/{lang}.xml`. -- Each entry has id `{typ_value}:{code}`. Example (`en.xml`): - - ```xml - - Invalid NC code '{value}' at line {line} - - ``` - -- Avoid over-coupling to English: use placeholders `{value}`, `{line}`, `{code}`, `{typ}`. -- Provide translations in other languages (e.g. `de.xml`). - -### Choosing codes -- Codes are domain-specific. Reserve ranges per area if helpful: - - 1000–1999: NC code syntax/semantics - - 2000–2999: canal/synchronization - - 3000–3999: generic CNC runtime - -Document new codes by adding entries to XML and referencing them in code. - -### Displaying errors -- For UI/CLI, prefer `MessageCatalog().format_exception(e, lang=...)`. -- This appends trace info automatically (file, line, column) and prints context with a caret. - -## Tests -- Add unit tests for new error conditions and for new message keys. -- Keep tests deterministic and small; use the standard library `unittest`. - -## Style & structure -- Prefer small modules and functions. -- Standard library first; add dependencies only with justification. -- Add type hints and keep public APIs stable. - +# Coding Guidelines + +This project favors clear, small, stdlib-first Python with helpful errors and tests. The key conventions are below. + +"""Contributor codeline examples. + +This module contains a minimal, stdlib-only example showing a write-only helper +function. It includes documentation and doctest examples so CI can run +`python -m doctest` against it. + +Guidelines followed: +- Use only Python standard library. +- Keep examples deterministic and self-cleaning (remove temporary files). +- Provide a small doctest demonstrating usage. + +""" +from __future__ import annotations + +from pathlib import Path +from typing import Union + + +def write_text(path: Union[str, Path], text: str, append: bool = False) -> None: + """Write text to a file using only standard library functions. + + Args: + path: File path to write to. Can be a string or Path. + text: Text content to write. + append: If True, append to the file; otherwise overwrite. + + Returns: + None + + Doctest example (runs deterministically): + + >>> # create a small file, read it back, then remove it + >>> write_text('tmp_test.txt', 'hello') + >>> open('tmp_test.txt', 'r', encoding='utf-8').read() + 'hello' + >>> # clean up + >>> import os + >>> os.remove('tmp_test.txt') + + """ + mode = 'a' if append else 'w' + p = Path(path) + # Ensure parent directory exists when a Path with directories is provided + if p.parent and not p.parent.exists(): + p.parent.mkdir(parents=True, exist_ok=True) + + # Use explicit encoding for reproducible behavior across platforms + with p.open(mode, encoding='utf-8') as fh: + fh.write(text) + + +__all__ = ["write_text"] + + +## Error handling and i18n + +Use the structured exception `ExceptionNode` with localized messages. + +- Exception types: `ExceptionTyps` (IntEnum) + - NCCodeErrors = 1 + - NCCanalStarErrors = 2 + - CNCError = 3 +- Raise errors via helper `raise_nc_error` to include trace and infer column: + + ```python + from ncplot7py.domain.exceptions import ExceptionTyps, raise_nc_error + + raise_nc_error( + ExceptionTyps.NCCodeErrors, + 1001, # maps to XML key "1:1001" + value="M30", # offending token (used in message formatting) + file="program.nc", + line=12, + source_line="N12 G1 X10 Y10 M30", # for caret positioning and context + ) + ``` + +- Exception fields available on `ExceptionNode`: + - `typ`, `code`, `line`, `message`, `value` + - Trace: `file`, `column`, `context` + - `localized(lang)` method returns a localized string with trace. + +-### Message catalog (XML) +- Messages live under `src/ncplot7py/locales/{lang}.xml`. +- Each entry has id `{typ_value}:{code}`. Example (`en.xml`): + + ```xml + + Invalid NC code '{value}' at line {line} + + ``` + +- Avoid over-coupling to English: use placeholders `{value}`, `{line}`, `{code}`, `{typ}`. +- Provide translations in other languages (e.g. `de.xml`). + +### Choosing codes +- Codes are domain-specific. Reserve ranges per area if helpful: + - 1000–1999: NC code syntax/semantics + - 2000–2999: canal/synchronization + - 3000–3999: generic CNC runtime + +Document new codes by adding entries to XML and referencing them in code. + +### Displaying errors +- For UI/CLI, prefer `MessageCatalog().format_exception(e, lang=...)`. +- This appends trace info automatically (file, line, column) and prints context with a caret. + +## Tests +- Add unit tests for new error conditions and for new message keys. +- Keep tests deterministic and small; use the standard library `unittest`. + +## Style & structure +- Prefer small modules and functions. +- Standard library first; add dependencies only with justification. +- Add type hints and keep public APIs stable. + diff --git a/pyproject.toml b/pyproject.toml index 1184fff..a730f64 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,26 +1,26 @@ -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[project] -name = "ncplot7py" -version = "0.0.1" -description = "ncplot7py — plotting and utilities for NetCDF data" -readme = "README.md" -license = { text = "See LICENSE" } -authors = [ { name = "d-creations" } ] -requires-python = ">=3.8" -dependencies = [] - - -[tool.testing] -# Test policy: prefer the Python standard library test tools (unittest, doctest, unittest.mock). -# Use third-party test runners only with explicit justification and PR rationale. -test-command = "python -m unittest discover -s tests -p 'test_*.py'" -doctest-command = "python -m doctest -v src/ncplot7py/*.py" -# CI-run command (PowerShell friendly): -# powershell: python -m unittest discover -s tests -p 'test_*.py' - -[project.optional-dependencies] -plotting = ["matplotlib>=3.0,<4.0"] - +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "ncplot7py" +version = "0.0.1" +description = "ncplot7py — plotting and utilities for NetCDF data" +readme = "README.md" +license = { text = "See LICENSE" } +authors = [ { name = "d-creations" } ] +requires-python = ">=3.8" +dependencies = [] + + +[tool.testing] +# Test policy: prefer the Python standard library test tools (unittest, doctest, unittest.mock). +# Use third-party test runners only with explicit justification and PR rationale. +test-command = "python -m unittest discover -s tests -p 'test_*.py'" +doctest-command = "python -m doctest -v src/ncplot7py/*.py" +# CI-run command (PowerShell friendly): +# powershell: python -m unittest discover -s tests -p 'test_*.py' + +[project.optional-dependencies] +plotting = ["matplotlib>=3.0,<4.0"] + diff --git a/scripts/run_example_O0004.py b/scripts/run_example_O0004.py index 21f2756..318eb6d 100644 --- a/scripts/run_example_O0004.py +++ b/scripts/run_example_O0004.py @@ -1,158 +1,158 @@ -""" -TEST Function example how to use the NC Analyzer - -Copyright (C) <2024> . -""" - -from pathlib import Path -import sys -from typing import List, Tuple, Any - -# Use package imports from the project -from ncplot7py.application.nc_execution import NCExecutionEngine -from ncplot7py.shared import configure_logging, get_message_stack, configure_i18n -from ncplot7py.shared.nc_nodes import NCCommandNode -from ncplot7py.shared.file_adapter import get_program - - -class _Point: - def __init__(self, x: float, y: float, z: float): - self.x = x - self.y = y - self.z = z - - -class FileBasedFakeControl: - """A minimal fake control that converts parsed NC nodes into simple toolpath points. - - It doesn't perform kinematics — it simply reads X/Y/Z parameters (if present) - and creates 1-point lines with a fixed time per command so the engine can - produce a plot structure for demonstration and testing. - """ - - def __init__(self): - self._canal_nodes = {} - - def get_canal_count(self) -> int: - return 1 - - def run_nc_code_list(self, node_list: List[NCCommandNode], canal: int) -> None: - # store the nodes for later retrieval - self._canal_nodes[canal] = list(node_list) - - def get_tool_path(self, canal: int): - nodes = self._canal_nodes.get(canal, []) - path = [] - for n in nodes: - params = getattr(n, "command_parameter", {}) - try: - x = float(params.get("X", 0.0)) - except Exception: - x = 0.0 - try: - y = float(params.get("Y", 0.0)) - except Exception: - y = 0.0 - try: - z = float(params.get("Z", 0.0)) - except Exception: - z = 0.0 - # one-point line segment, time 0.1s - path.append(([_Point(x, y, z)], 0.1)) - return path - - def get_exected_nodes(self, canal: int): - return self._canal_nodes.get(canal, []) - - def get_canal_name(self, idx: int) -> str: - return f"C{idx}" - - def synchro_points(self, tool_paths, nodes): - # no-op for fake control - return None - - -def read_program_from_file(path: Path) -> list: - """Read a file and return one or more program strings using the file adapter. - - Returns a list of program strings (each string contains commands separated - by ';') suitable for passing directly to NCExecutionEngine.get_Syncro_plot. - """ - # Delegate to the shared file_adapter which handles parentheses removal and - # splitting into multiple programs if blank lines are used as separators. - return get_program(path, split_on_blank_line=True) - - -def main(plot: bool = False) -> int: - # Configure logging to capture messages in the in-memory web buffer so - # we can print them after the run. - configure_logging(console=True, web_buffer=True) - configure_i18n() - - # Resolve path to data file (repo root / data / nc-examples / O0004) - repo_root = Path(__file__).resolve().parent.parent - data_file = repo_root / "data" / "nc-examples" / "O0004" - - if not data_file.exists(): - print(f"Data file not found: {data_file}") - return 2 - - programs = read_program_from_file(data_file) - - control = FileBasedFakeControl() - engine = NCExecutionEngine(control) - - # get_Syncro_plot expects a list of program strings; our adapter returns - # that directly, so pass it through. - plot_result = engine.get_Syncro_plot(programs, synch=False) - - print("Returned structure type:", type(plot_result)) - print("Number of canals:", len(plot_result) if isinstance(plot_result, list) else "n/a") - print("Calculated runtime (engine):", engine.get_cacluated_runtime()) - - print("Captured messages:") - print(get_message_stack()) - - # Optionally plot if matplotlib is available and a result was produced - if plot and isinstance(plot_result, list) and len(plot_result) > 0: - try: - import matplotlib.pyplot as plt - from mpl_toolkits.mplot3d import Axes3D # noqa: F401 - - all_x, all_y, all_z = [], [], [] - for canal in plot_result: - for line in canal.get("plot", []): - all_x.extend(line.get("x", [])) - all_y.extend(line.get("y", [])) - all_z.extend(line.get("z", [])) - - if all_x and all_y and all_z: - fig = plt.figure(1) - ax = fig.add_subplot(111, projection='3d') - ax.plot(all_z, all_x, all_y, label='parametric curve') - ax.set_xlabel('z') - ax.set_ylabel('x') - ax.set_zlabel('y') - plt.title('NC example O0004') - plt.show() - except Exception as e: - print(f"Matplotlib plot failed: {e}") - - return 0 - - -if __name__ == '__main__': - sys.exit(main(plot=True)) +""" +TEST Function example how to use the NC Analyzer + +Copyright (C) <2024> . +""" + +from pathlib import Path +import sys +from typing import List, Tuple, Any + +# Use package imports from the project +from ncplot7py.application.nc_execution import NCExecutionEngine +from ncplot7py.shared import configure_logging, get_message_stack, configure_i18n +from ncplot7py.shared.nc_nodes import NCCommandNode +from ncplot7py.shared.file_adapter import get_program + + +class _Point: + def __init__(self, x: float, y: float, z: float): + self.x = x + self.y = y + self.z = z + + +class FileBasedFakeControl: + """A minimal fake control that converts parsed NC nodes into simple toolpath points. + + It doesn't perform kinematics — it simply reads X/Y/Z parameters (if present) + and creates 1-point lines with a fixed time per command so the engine can + produce a plot structure for demonstration and testing. + """ + + def __init__(self): + self._canal_nodes = {} + + def get_canal_count(self) -> int: + return 1 + + def run_nc_code_list(self, node_list: List[NCCommandNode], canal: int) -> None: + # store the nodes for later retrieval + self._canal_nodes[canal] = list(node_list) + + def get_tool_path(self, canal: int): + nodes = self._canal_nodes.get(canal, []) + path = [] + for n in nodes: + params = getattr(n, "command_parameter", {}) + try: + x = float(params.get("X", 0.0)) + except Exception: + x = 0.0 + try: + y = float(params.get("Y", 0.0)) + except Exception: + y = 0.0 + try: + z = float(params.get("Z", 0.0)) + except Exception: + z = 0.0 + # one-point line segment, time 0.1s + path.append(([_Point(x, y, z)], 0.1)) + return path + + def get_exected_nodes(self, canal: int): + return self._canal_nodes.get(canal, []) + + def get_canal_name(self, idx: int) -> str: + return f"C{idx}" + + def synchro_points(self, tool_paths, nodes): + # no-op for fake control + return None + + +def read_program_from_file(path: Path) -> list: + """Read a file and return one or more program strings using the file adapter. + + Returns a list of program strings (each string contains commands separated + by ';') suitable for passing directly to NCExecutionEngine.get_Syncro_plot. + """ + # Delegate to the shared file_adapter which handles parentheses removal and + # splitting into multiple programs if blank lines are used as separators. + return get_program(path, split_on_blank_line=True) + + +def main(plot: bool = False) -> int: + # Configure logging to capture messages in the in-memory web buffer so + # we can print them after the run. + configure_logging(console=True, web_buffer=True) + configure_i18n() + + # Resolve path to data file (repo root / data / nc-examples / O0004) + repo_root = Path(__file__).resolve().parent.parent + data_file = repo_root / "data" / "nc-examples" / "O0004" + + if not data_file.exists(): + print(f"Data file not found: {data_file}") + return 2 + + programs = read_program_from_file(data_file) + + control = FileBasedFakeControl() + engine = NCExecutionEngine(control) + + # get_Syncro_plot expects a list of program strings; our adapter returns + # that directly, so pass it through. + plot_result = engine.get_Syncro_plot(programs, synch=False) + + print("Returned structure type:", type(plot_result)) + print("Number of canals:", len(plot_result) if isinstance(plot_result, list) else "n/a") + print("Calculated runtime (engine):", engine.get_cacluated_runtime()) + + print("Captured messages:") + print(get_message_stack()) + + # Optionally plot if matplotlib is available and a result was produced + if plot and isinstance(plot_result, list) and len(plot_result) > 0: + try: + import matplotlib.pyplot as plt + from mpl_toolkits.mplot3d import Axes3D # noqa: F401 + + all_x, all_y, all_z = [], [], [] + for canal in plot_result: + for line in canal.get("plot", []): + all_x.extend(line.get("x", [])) + all_y.extend(line.get("y", [])) + all_z.extend(line.get("z", [])) + + if all_x and all_y and all_z: + fig = plt.figure(1) + ax = fig.add_subplot(111, projection='3d') + ax.plot(all_z, all_x, all_y, label='parametric curve') + ax.set_xlabel('z') + ax.set_ylabel('x') + ax.set_zlabel('y') + plt.title('NC example O0004') + plt.show() + except Exception as e: + print(f"Matplotlib plot failed: {e}") + + return 0 + + +if __name__ == '__main__': + sys.exit(main(plot=True)) diff --git a/scripts/run_example_O6POINT.py b/scripts/run_example_O6POINT.py index d8975a8..b062fa7 100644 --- a/scripts/run_example_O6POINT.py +++ b/scripts/run_example_O6POINT.py @@ -1,269 +1,269 @@ -""" -TEST Function example how to use the NC Analyzer - -Copyright (C) <2024> . -""" - -from pathlib import Path -import sys -import os - -# Configuration: set to True to save the produced plot to a PNG file instead -# of (or in addition to) showing it interactively. Adjust the path as needed. -SAVE_PLOT_TO_PNG = False -# Example: 'plots/O6POINT.png' (relative to repo root). Parent directories -# will be created automatically when saving. -PLOT_OUTPUT_PATH = Path("plots") / "O6POINT.png" -from typing import List, Tuple, Any - -# Use package imports from the project -from ncplot7py.application.nc_execution import NCExecutionEngine -from ncplot7py.shared import configure_logging, get_message_stack, configure_i18n -from ncplot7py.shared.nc_nodes import NCCommandNode -from ncplot7py.shared.file_adapter import get_program -from ncplot7py.infrastructure.machines.stateful_star_turn_control import StatefulIsoTurnNCControl - - -class _Point: - def __init__(self, x: float, y: float, z: float): - self.x = x - self.y = y - self.z = z - - -class FileBasedFakeControl: - """A minimal fake control that converts parsed NC nodes into simple toolpath points. - - It doesn't perform kinematics — it simply reads X/Y/Z parameters (if present) - and creates 1-point lines with a fixed time per command so the engine can - produce a plot structure for demonstration and testing. - """ - - def __init__(self): - self._canal_nodes = {} - - def get_canal_count(self) -> int: - return 1 - - def run_nc_code_list(self, node_list: List[NCCommandNode], canal: int) -> None: - # store the nodes for later retrieval - self._canal_nodes[canal] = list(node_list) - - def get_tool_path(self, canal: int): - nodes = self._canal_nodes.get(canal, []) - path = [] - for n in nodes: - params = getattr(n, "command_parameter", {}) - try: - x = float(params.get("X", 0.0)) - except Exception: - x = 0.0 - try: - y = float(params.get("Y", 0.0)) - except Exception: - y = 0.0 - try: - z = float(params.get("Z", 0.0)) - except Exception: - z = 0.0 - # one-point line segment, time 0.1s - path.append(([_Point(x, y, z)], 0.1)) - return path - - def get_exected_nodes(self, canal: int): - return self._canal_nodes.get(canal, []) - - def get_canal_name(self, idx: int) -> str: - return f"C{idx}" - - def synchro_points(self, tool_paths, nodes): - # no-op for fake control - return None - - -def read_program_from_file(path: Path) -> list: - """Read a file and return one or more program strings using the file adapter. - - Returns a list of program strings (each string contains commands separated - by ';') suitable for passing directly to NCExecutionEngine.get_Syncro_plot. - """ - # Delegate to the shared file_adapter which handles parentheses removal and - # splitting into multiple programs if blank lines are used as separators. - return get_program(path, split_on_blank_line=False) - - -def main(plot: bool = False) -> int: - # Configure logging to capture messages in the in-memory web buffer so - # we can print them after the run. - configure_logging(console=True, web_buffer=True) - configure_i18n() - - # Resolve path to data file (repo root / data / nc-examples / O0004) - repo_root = Path(__file__).resolve().parent.parent - data_file = repo_root / "data" / "nc-examples" / "O6POINT" - - if not data_file.exists(): - print(f"Data file not found: {data_file}") - return 2 - - programs = read_program_from_file(data_file) - - # Use the stateful ISO-turn control implementation for execution. - # If the input file contains multiple programs (split on blank lines), - # create a control with the matching number of canals to avoid - # "Canal X not configured" errors when each program is intended for a - # separate canal. - control = StatefulIsoTurnNCControl(count_of_canals=max(1, len(programs))) - engine = NCExecutionEngine(control) - - # get_Syncro_plot expects a list of program strings; our adapter returns - # that directly, so pass it through. - plot_result = engine.get_Syncro_plot(programs, synch=False) - - print("Returned structure type:", type(plot_result)) - print("Number of canals:", len(plot_result) if isinstance(plot_result, list) else "n/a") - print("Calculated runtime (engine):", engine.get_cacluated_runtime()) - - print("Captured messages:") - print(get_message_stack()) - - # Optionally plot if matplotlib is available and a result was produced - if plot and isinstance(plot_result, list) and len(plot_result) > 0: - try: - import matplotlib.pyplot as plt - from mpl_toolkits.mplot3d import Axes3D # noqa: F401 - - all_x, all_y, all_z = [], [], [] - for canal in plot_result: - # canal may be a dict with a 'plot' key (new API) or a plain list - # (legacy/alternate implementations). Handle both safely. - if isinstance(canal, dict): - plot_lines = canal.get("plot", []) - elif isinstance(canal, (list, tuple)): - plot_lines = canal - else: - # unknown format, skip - continue - - for line in plot_lines: - if isinstance(line, dict): - # expected shape from NCExecutionEngine - all_x.extend(line.get("x", [])) - all_y.extend(line.get("y", [])) - all_z.extend(line.get("z", [])) - else: - # fallback: line may be a (points_list, time) tuple - try: - pts = line[0] - for p in pts: - all_x.append(getattr(p, "x", None)) - all_y.append(getattr(p, "y", None)) - all_z.append(getattr(p, "z", None)) - except Exception: - # give up on this line - continue - - if all_x and all_y: - # decide whether to show 3D or 2D: if Z is essentially flat - # (all zeros or None) prefer a 2D X vs Y plot similar to the - # provided reference image. - z_vals = [v for v in all_z if v is not None] if all_z else [] - z_range = (max(z_vals) - min(z_vals)) if z_vals else 0.0 - - if z_range > 1e-6: - # 3D plot - fig = plt.figure(1) - ax = fig.add_subplot(111, projection='3d') - ax.plot(all_x, all_y, all_z, label='parametric curve') - ax.set_xlabel('x') - ax.set_ylabel('y') - ax.set_zlabel('z') - plt.title('NC example O6POINT (3D)') - # save if requested, otherwise show - if SAVE_PLOT_TO_PNG: - PLOT_OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True) - fig.savefig(str(PLOT_OUTPUT_PATH), bbox_inches='tight') - print(f"Saved plot to {PLOT_OUTPUT_PATH}") - else: - plt.show() - else: - # 2D plot (X horizontal, Y vertical) with red axis lines - fig, ax = plt.subplots(figsize=(5, 6)) - - # Prepare XY lists, filter out None values - xs = [v for v in all_x if v is not None] - ys = [v for v in all_y if v is not None] - if not xs or not ys: - # nothing to plot - if SAVE_PLOT_TO_PNG: - PLOT_OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True) - fig.savefig(str(PLOT_OUTPUT_PATH), bbox_inches='tight') - print(f"Saved plot to {PLOT_OUTPUT_PATH}") - else: - plt.show() - else: - # Close the polygon path if not already closed - if xs[0] != xs[-1] or ys[0] != ys[-1]: - xs = xs + [xs[0]] - ys = ys + [ys[0]] - - # Draw polygon with rounded joins to resemble the reference - ax.plot(xs, ys, color='black', linewidth=2, solid_capstyle='round', solid_joinstyle='round') - - ax.set_xlabel('X') - ax.set_ylabel('Y') - ax.set_title('NC example O6POINT') - ax.set_aspect('equal', adjustable='datalim') - - # Compute centroid and draw red axes through it (like reference) - try: - cx = sum(xs[:-1]) / (len(xs) - 1) - cy = sum(ys[:-1]) / (len(ys) - 1) - except Exception: - cx, cy = 0.0, 0.0 - - # draw red axes lines through centroid - ax.axhline(cy, color='red', linewidth=1) - ax.axvline(cx, color='red', linewidth=1) - - # annotate axis labels similar to reference image - xlim = ax.get_xlim() - ylim = ax.get_ylim() - # place X label to the right, Y label on top - ax.text(xlim[1], cy, 'X', color='red', verticalalignment='bottom', horizontalalignment='right') - ax.text(cx, ylim[1], 'Y', color='red', verticalalignment='top', horizontalalignment='left') - - # tighten limits with small padding - pad_x = (max(xs) - min(xs)) * 0.12 if (max(xs) - min(xs)) > 0 else 1.0 - pad_y = (max(ys) - min(ys)) * 0.12 if (max(ys) - min(ys)) > 0 else 1.0 - ax.set_xlim(min(xs) - pad_x, max(xs) + pad_x) - ax.set_ylim(min(ys) - pad_y, max(ys) + pad_y) - - # save if requested, otherwise show - if SAVE_PLOT_TO_PNG: - PLOT_OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True) - fig.savefig(str(PLOT_OUTPUT_PATH), bbox_inches='tight') - print(f"Saved plot to {PLOT_OUTPUT_PATH}") - else: - plt.show() - except Exception as e: - print(f"Matplotlib plot failed: {e}") - - return 0 - - -if __name__ == '__main__': - sys.exit(main(plot=True)) +""" +TEST Function example how to use the NC Analyzer + +Copyright (C) <2024> . +""" + +from pathlib import Path +import sys +import os + +# Configuration: set to True to save the produced plot to a PNG file instead +# of (or in addition to) showing it interactively. Adjust the path as needed. +SAVE_PLOT_TO_PNG = False +# Example: 'plots/O6POINT.png' (relative to repo root). Parent directories +# will be created automatically when saving. +PLOT_OUTPUT_PATH = Path("plots") / "O6POINT.png" +from typing import List, Tuple, Any + +# Use package imports from the project +from ncplot7py.application.nc_execution import NCExecutionEngine +from ncplot7py.shared import configure_logging, get_message_stack, configure_i18n +from ncplot7py.shared.nc_nodes import NCCommandNode +from ncplot7py.shared.file_adapter import get_program +from ncplot7py.infrastructure.machines.stateful_star_turn_control import StatefulIsoTurnNCControl + + +class _Point: + def __init__(self, x: float, y: float, z: float): + self.x = x + self.y = y + self.z = z + + +class FileBasedFakeControl: + """A minimal fake control that converts parsed NC nodes into simple toolpath points. + + It doesn't perform kinematics — it simply reads X/Y/Z parameters (if present) + and creates 1-point lines with a fixed time per command so the engine can + produce a plot structure for demonstration and testing. + """ + + def __init__(self): + self._canal_nodes = {} + + def get_canal_count(self) -> int: + return 1 + + def run_nc_code_list(self, node_list: List[NCCommandNode], canal: int) -> None: + # store the nodes for later retrieval + self._canal_nodes[canal] = list(node_list) + + def get_tool_path(self, canal: int): + nodes = self._canal_nodes.get(canal, []) + path = [] + for n in nodes: + params = getattr(n, "command_parameter", {}) + try: + x = float(params.get("X", 0.0)) + except Exception: + x = 0.0 + try: + y = float(params.get("Y", 0.0)) + except Exception: + y = 0.0 + try: + z = float(params.get("Z", 0.0)) + except Exception: + z = 0.0 + # one-point line segment, time 0.1s + path.append(([_Point(x, y, z)], 0.1)) + return path + + def get_exected_nodes(self, canal: int): + return self._canal_nodes.get(canal, []) + + def get_canal_name(self, idx: int) -> str: + return f"C{idx}" + + def synchro_points(self, tool_paths, nodes): + # no-op for fake control + return None + + +def read_program_from_file(path: Path) -> list: + """Read a file and return one or more program strings using the file adapter. + + Returns a list of program strings (each string contains commands separated + by ';') suitable for passing directly to NCExecutionEngine.get_Syncro_plot. + """ + # Delegate to the shared file_adapter which handles parentheses removal and + # splitting into multiple programs if blank lines are used as separators. + return get_program(path, split_on_blank_line=False) + + +def main(plot: bool = False) -> int: + # Configure logging to capture messages in the in-memory web buffer so + # we can print them after the run. + configure_logging(console=True, web_buffer=True) + configure_i18n() + + # Resolve path to data file (repo root / data / nc-examples / O0004) + repo_root = Path(__file__).resolve().parent.parent + data_file = repo_root / "data" / "nc-examples" / "O6POINT" + + if not data_file.exists(): + print(f"Data file not found: {data_file}") + return 2 + + programs = read_program_from_file(data_file) + + # Use the stateful ISO-turn control implementation for execution. + # If the input file contains multiple programs (split on blank lines), + # create a control with the matching number of canals to avoid + # "Canal X not configured" errors when each program is intended for a + # separate canal. + control = StatefulIsoTurnNCControl(count_of_canals=max(1, len(programs))) + engine = NCExecutionEngine(control) + + # get_Syncro_plot expects a list of program strings; our adapter returns + # that directly, so pass it through. + plot_result = engine.get_Syncro_plot(programs, synch=False) + + print("Returned structure type:", type(plot_result)) + print("Number of canals:", len(plot_result) if isinstance(plot_result, list) else "n/a") + print("Calculated runtime (engine):", engine.get_cacluated_runtime()) + + print("Captured messages:") + print(get_message_stack()) + + # Optionally plot if matplotlib is available and a result was produced + if plot and isinstance(plot_result, list) and len(plot_result) > 0: + try: + import matplotlib.pyplot as plt + from mpl_toolkits.mplot3d import Axes3D # noqa: F401 + + all_x, all_y, all_z = [], [], [] + for canal in plot_result: + # canal may be a dict with a 'plot' key (new API) or a plain list + # (legacy/alternate implementations). Handle both safely. + if isinstance(canal, dict): + plot_lines = canal.get("plot", []) + elif isinstance(canal, (list, tuple)): + plot_lines = canal + else: + # unknown format, skip + continue + + for line in plot_lines: + if isinstance(line, dict): + # expected shape from NCExecutionEngine + all_x.extend(line.get("x", [])) + all_y.extend(line.get("y", [])) + all_z.extend(line.get("z", [])) + else: + # fallback: line may be a (points_list, time) tuple + try: + pts = line[0] + for p in pts: + all_x.append(getattr(p, "x", None)) + all_y.append(getattr(p, "y", None)) + all_z.append(getattr(p, "z", None)) + except Exception: + # give up on this line + continue + + if all_x and all_y: + # decide whether to show 3D or 2D: if Z is essentially flat + # (all zeros or None) prefer a 2D X vs Y plot similar to the + # provided reference image. + z_vals = [v for v in all_z if v is not None] if all_z else [] + z_range = (max(z_vals) - min(z_vals)) if z_vals else 0.0 + + if z_range > 1e-6: + # 3D plot + fig = plt.figure(1) + ax = fig.add_subplot(111, projection='3d') + ax.plot(all_x, all_y, all_z, label='parametric curve') + ax.set_xlabel('x') + ax.set_ylabel('y') + ax.set_zlabel('z') + plt.title('NC example O6POINT (3D)') + # save if requested, otherwise show + if SAVE_PLOT_TO_PNG: + PLOT_OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True) + fig.savefig(str(PLOT_OUTPUT_PATH), bbox_inches='tight') + print(f"Saved plot to {PLOT_OUTPUT_PATH}") + else: + plt.show() + else: + # 2D plot (X horizontal, Y vertical) with red axis lines + fig, ax = plt.subplots(figsize=(5, 6)) + + # Prepare XY lists, filter out None values + xs = [v for v in all_x if v is not None] + ys = [v for v in all_y if v is not None] + if not xs or not ys: + # nothing to plot + if SAVE_PLOT_TO_PNG: + PLOT_OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True) + fig.savefig(str(PLOT_OUTPUT_PATH), bbox_inches='tight') + print(f"Saved plot to {PLOT_OUTPUT_PATH}") + else: + plt.show() + else: + # Close the polygon path if not already closed + if xs[0] != xs[-1] or ys[0] != ys[-1]: + xs = xs + [xs[0]] + ys = ys + [ys[0]] + + # Draw polygon with rounded joins to resemble the reference + ax.plot(xs, ys, color='black', linewidth=2, solid_capstyle='round', solid_joinstyle='round') + + ax.set_xlabel('X') + ax.set_ylabel('Y') + ax.set_title('NC example O6POINT') + ax.set_aspect('equal', adjustable='datalim') + + # Compute centroid and draw red axes through it (like reference) + try: + cx = sum(xs[:-1]) / (len(xs) - 1) + cy = sum(ys[:-1]) / (len(ys) - 1) + except Exception: + cx, cy = 0.0, 0.0 + + # draw red axes lines through centroid + ax.axhline(cy, color='red', linewidth=1) + ax.axvline(cx, color='red', linewidth=1) + + # annotate axis labels similar to reference image + xlim = ax.get_xlim() + ylim = ax.get_ylim() + # place X label to the right, Y label on top + ax.text(xlim[1], cy, 'X', color='red', verticalalignment='bottom', horizontalalignment='right') + ax.text(cx, ylim[1], 'Y', color='red', verticalalignment='top', horizontalalignment='left') + + # tighten limits with small padding + pad_x = (max(xs) - min(xs)) * 0.12 if (max(xs) - min(xs)) > 0 else 1.0 + pad_y = (max(ys) - min(ys)) * 0.12 if (max(ys) - min(ys)) > 0 else 1.0 + ax.set_xlim(min(xs) - pad_x, max(xs) + pad_x) + ax.set_ylim(min(ys) - pad_y, max(ys) + pad_y) + + # save if requested, otherwise show + if SAVE_PLOT_TO_PNG: + PLOT_OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True) + fig.savefig(str(PLOT_OUTPUT_PATH), bbox_inches='tight') + print(f"Saved plot to {PLOT_OUTPUT_PATH}") + else: + plt.show() + except Exception as e: + print(f"Matplotlib plot failed: {e}") + + return 0 + + +if __name__ == '__main__': + sys.exit(main(plot=True)) diff --git a/src/ncplot7py/__init__.py b/src/ncplot7py/__init__.py index 5bf848f..52643d1 100644 --- a/src/ncplot7py/__init__.py +++ b/src/ncplot7py/__init__.py @@ -1,7 +1,7 @@ -"""ncplot7py package. - -Lightweight package initialiser. The package follows a clean architecture -layout: domain, application, interfaces, infrastructure, shared, and cli. -""" - -__all__ = ["cli", "domain", "application", "interfaces", "infrastructure", "shared"] +"""ncplot7py package. + +Lightweight package initialiser. The package follows a clean architecture +layout: domain, application, interfaces, infrastructure, shared, and cli. +""" + +__all__ = ["cli", "domain", "application", "interfaces", "infrastructure", "shared"] diff --git a/src/ncplot7py/application/nc_execution.py b/src/ncplot7py/application/nc_execution.py index 81fd32e..baac224 100644 --- a/src/ncplot7py/application/nc_execution.py +++ b/src/ncplot7py/application/nc_execution.py @@ -1,221 +1,221 @@ -"""NC execution engine (renamed from `nc_analyzer`). - -Provides `NCExecutionEngine` which is the new name for the orchestrator that -parses NC programs, delegates execution to an NC control implementation and -produces toolpath plot data. This keeps the public return values compatible -with the previous implementation. -""" -from __future__ import annotations - -import time -from typing import List, Dict, Optional, Any - -from ncplot7py.shared import ( - configure_logging, - print_error, - print_message, - print_translated_error, - get_message_stack, - configure_i18n, -) -from ncplot7py.shared.registry import registry - - -class NCExecutionEngine: - """NC Execution Engine. - - This class preserves the original `get_Syncro_plot` return structure - (a list of canal dictionaries, or `[[],[]]` on error) to avoid changing - the public API while improving internal structure and naming. - """ - - def __init__( - self, - cnc_control: Any, - ) -> None: - """Initialize the engine. - - Parameters - ---------- - cnc_control: - An object implementing the NC control interface (run_nc_code_list, - get_tool_path, get_exected_nodes, get_canal_name, get_canal_count, - synchro_points). - """ - self.cnc_control = cnc_control - self.caclulatet_runtime: float = -1.0 - # If control offers canal count, use it, otherwise default to 1 - try: - self.count_of_canals = int(self.cnc_control.get_canal_count()) - except Exception: - self.count_of_canals = 1 - - # Ensure logging and i18n are configured (caller can reconfigure) - configure_logging(console=True, web_buffer=False) - configure_i18n() - - def get_cacluated_runtime(self) -> float: - return self.caclulatet_runtime - - def _ensure_parser(self): - # Ensure a parser is registered (same strategy as cli.bootstrap) - if registry.get("parser", "nc_command") is None: - try: - from ncplot7py.infrastructure.parsers.nc_command_parser import register as _reg_p - - _reg_p(registry) - except Exception: - # If parser registration fails, let parse attempts raise - pass - - def _get_parser(self): - self._ensure_parser() - parser_cls = registry.get("parser", "nc_command") - if parser_cls is None: - # try by interface name - parser_cls = registry.get("parser", "BaseNCCommandParser") - if parser_cls is None: - raise RuntimeError("No NC command parser registered") - return parser_cls() - - def get_Syncro_plot(self, programs: List[str], synch: bool) -> List[Dict]: - """Create the plot for the given NC `programs`. - - Parameters - ---------- - programs: list[str] - Each program is a string containing NC commands separated by ';'. - synch: bool - Whether to attempt synchronization across canals. - - Returns - ------- - list[dict] - A list of canal dictionaries in the same shape as the original - implementation. On error returns `[[],[]]` to match previous API. - """ - parser = None - try: - parser = self._get_parser() - except Exception as e: - print_error(f"Parser setup failed: {e}") - return [[], []] - - canal_number = 0 - tool_paths: List[Any] = [] - nodes: List[Any] = [] - error = False - - for program in programs: - # Parse program into a list of command nodes - node_list = [] - i = 0 - try: - for raw_line in [p for p in program.split(";") if p.strip()]: - node = parser.parse(raw_line, i) - node_list.append(node) - i += 1 - - # Delegate execution to control; many controls accept an iterable - # of nodes. Keep canal numbering consistent with callers (+1). - self.cnc_control.run_nc_code_list(node_list, canal_number + 1) - except Exception as exc: - # Try to handle known control exception style (has log_route) - log_route = getattr(exc, "log_route", None) - if log_route: - # Try to format exception nodes using MessageCatalog when available - try: - from ncplot7py.domain.i18n import MessageCatalog - - catalog = MessageCatalog() - for node in log_route: - msg = catalog.format_exception(node) - print_error(msg) - except Exception: - print_error(f"NC PARSER/CONTROL ERROR AT LINE {i+1}: {exc}") - else: - # generic parser/control error - print_error(f"NC PARSER/CONTROL ERROR AT LINE {i+1}: {exc}") - error = True - break - - # Try to collect results for this canal - try: - tool_paths.append(self.cnc_control.get_tool_path(canal_number + 1)) - nodes.append(self.cnc_control.get_exected_nodes(canal_number + 1)) - except Exception as exc: - print_error(f"Error creating tool path: {exc}") - error = True - break - - canal_number += 1 - - # Synchronize across canals if requested - try: - if not error and self.count_of_canals > 1 and synch: - self.cnc_control.synchro_points(tool_paths, nodes) - except Exception as e: - log_route = getattr(e, "log_route", None) - if log_route: - try: - from ncplot7py.domain.i18n import MessageCatalog - - catalog = MessageCatalog() - for node in log_route: - msg = catalog.format_exception(node) - print_error(msg) - except Exception: - print_error(f"Synchronization error: {e}") - else: - print_error(f"Synchronization error: {e}") - error = True - - # Build plots if no error - if not error: - lines_list: List[Dict] = [] - runtime = 0.0 - canal_index = 0 - for tool_path in tool_paths: - lines: List[Dict] = [] - linesExec: List[int] = [] - for line in tool_path: - # each line expected to be (points_list, t) - try: - l, t = line - except Exception: - # unknown format, skip - continue - x = [] - y = [] - z = [] - for point in l: - if point is not None: - x.append(getattr(point, "x", None)) - y.append(getattr(point, "y", None)) - z.append(getattr(point, "z", None)) - lines.append({"x": x, "y": y, "z": z, "t": t}) - try: - runtime += float(t) - except Exception: - pass - - self.caclulatet_runtime = runtime - - if len(tool_paths) == len(nodes): - for node in nodes[canal_index]: - linesExec.append(getattr(node, "nc_code_line_nr", None)) - - canal = { - "plot": lines, - "canalNr": self.cnc_control.get_canal_name(canal_index), - "programExec": linesExec, - } - canal_index += 1 - lines_list.append(canal) - - return lines_list - - # On error, preserve prior API return value - print_message("Error in NC Code") - print_message(get_message_stack()) - return [[], []] +"""NC execution engine (renamed from `nc_analyzer`). + +Provides `NCExecutionEngine` which is the new name for the orchestrator that +parses NC programs, delegates execution to an NC control implementation and +produces toolpath plot data. This keeps the public return values compatible +with the previous implementation. +""" +from __future__ import annotations + +import time +from typing import List, Dict, Optional, Any + +from ncplot7py.shared import ( + configure_logging, + print_error, + print_message, + print_translated_error, + get_message_stack, + configure_i18n, +) +from ncplot7py.shared.registry import registry + + +class NCExecutionEngine: + """NC Execution Engine. + + This class preserves the original `get_Syncro_plot` return structure + (a list of canal dictionaries, or `[[],[]]` on error) to avoid changing + the public API while improving internal structure and naming. + """ + + def __init__( + self, + cnc_control: Any, + ) -> None: + """Initialize the engine. + + Parameters + ---------- + cnc_control: + An object implementing the NC control interface (run_nc_code_list, + get_tool_path, get_exected_nodes, get_canal_name, get_canal_count, + synchro_points). + """ + self.cnc_control = cnc_control + self.caclulatet_runtime: float = -1.0 + # If control offers canal count, use it, otherwise default to 1 + try: + self.count_of_canals = int(self.cnc_control.get_canal_count()) + except Exception: + self.count_of_canals = 1 + + # Ensure logging and i18n are configured (caller can reconfigure) + configure_logging(console=True, web_buffer=False) + configure_i18n() + + def get_cacluated_runtime(self) -> float: + return self.caclulatet_runtime + + def _ensure_parser(self): + # Ensure a parser is registered (same strategy as cli.bootstrap) + if registry.get("parser", "nc_command") is None: + try: + from ncplot7py.infrastructure.parsers.nc_command_parser import register as _reg_p + + _reg_p(registry) + except Exception: + # If parser registration fails, let parse attempts raise + pass + + def _get_parser(self): + self._ensure_parser() + parser_cls = registry.get("parser", "nc_command") + if parser_cls is None: + # try by interface name + parser_cls = registry.get("parser", "BaseNCCommandParser") + if parser_cls is None: + raise RuntimeError("No NC command parser registered") + return parser_cls() + + def get_Syncro_plot(self, programs: List[str], synch: bool) -> List[Dict]: + """Create the plot for the given NC `programs`. + + Parameters + ---------- + programs: list[str] + Each program is a string containing NC commands separated by ';'. + synch: bool + Whether to attempt synchronization across canals. + + Returns + ------- + list[dict] + A list of canal dictionaries in the same shape as the original + implementation. On error returns `[[],[]]` to match previous API. + """ + parser = None + try: + parser = self._get_parser() + except Exception as e: + print_error(f"Parser setup failed: {e}") + return [[], []] + + canal_number = 0 + tool_paths: List[Any] = [] + nodes: List[Any] = [] + error = False + + for program in programs: + # Parse program into a list of command nodes + node_list = [] + i = 0 + try: + for raw_line in [p for p in program.split(";") if p.strip()]: + node = parser.parse(raw_line, i) + node_list.append(node) + i += 1 + + # Delegate execution to control; many controls accept an iterable + # of nodes. Keep canal numbering consistent with callers (+1). + self.cnc_control.run_nc_code_list(node_list, canal_number + 1) + except Exception as exc: + # Try to handle known control exception style (has log_route) + log_route = getattr(exc, "log_route", None) + if log_route: + # Try to format exception nodes using MessageCatalog when available + try: + from ncplot7py.domain.i18n import MessageCatalog + + catalog = MessageCatalog() + for node in log_route: + msg = catalog.format_exception(node) + print_error(msg) + except Exception: + print_error(f"NC PARSER/CONTROL ERROR AT LINE {i+1}: {exc}") + else: + # generic parser/control error + print_error(f"NC PARSER/CONTROL ERROR AT LINE {i+1}: {exc}") + error = True + break + + # Try to collect results for this canal + try: + tool_paths.append(self.cnc_control.get_tool_path(canal_number + 1)) + nodes.append(self.cnc_control.get_exected_nodes(canal_number + 1)) + except Exception as exc: + print_error(f"Error creating tool path: {exc}") + error = True + break + + canal_number += 1 + + # Synchronize across canals if requested + try: + if not error and self.count_of_canals > 1 and synch: + self.cnc_control.synchro_points(tool_paths, nodes) + except Exception as e: + log_route = getattr(e, "log_route", None) + if log_route: + try: + from ncplot7py.domain.i18n import MessageCatalog + + catalog = MessageCatalog() + for node in log_route: + msg = catalog.format_exception(node) + print_error(msg) + except Exception: + print_error(f"Synchronization error: {e}") + else: + print_error(f"Synchronization error: {e}") + error = True + + # Build plots if no error + if not error: + lines_list: List[Dict] = [] + runtime = 0.0 + canal_index = 0 + for tool_path in tool_paths: + lines: List[Dict] = [] + linesExec: List[int] = [] + for line in tool_path: + # each line expected to be (points_list, t) + try: + l, t = line + except Exception: + # unknown format, skip + continue + x = [] + y = [] + z = [] + for point in l: + if point is not None: + x.append(getattr(point, "x", None)) + y.append(getattr(point, "y", None)) + z.append(getattr(point, "z", None)) + lines.append({"x": x, "y": y, "z": z, "t": t}) + try: + runtime += float(t) + except Exception: + pass + + self.caclulatet_runtime = runtime + + if len(tool_paths) == len(nodes): + for node in nodes[canal_index]: + linesExec.append(getattr(node, "nc_code_line_nr", None)) + + canal = { + "plot": lines, + "canalNr": self.cnc_control.get_canal_name(canal_index), + "programExec": linesExec, + } + canal_index += 1 + lines_list.append(canal) + + return lines_list + + # On error, preserve prior API return value + print_message("Error in NC Code") + print_message(get_message_stack()) + return [[], []] diff --git a/src/ncplot7py/cli/main.py b/src/ncplot7py/cli/main.py index e14bb45..aa62abe 100644 --- a/src/ncplot7py/cli/main.py +++ b/src/ncplot7py/cli/main.py @@ -1,39 +1,39 @@ -"""CLI bootstrap helpers. - -Provide a small `bootstrap()` function that registers builtin adapters -into the shared `registry`. This keeps import-time side-effects out of -module import and makes tests deterministic. -""" -from __future__ import annotations - -from typing import Optional - -from ncplot7py.shared.registry import registry - - -def bootstrap(reg: Optional[object] = None) -> None: - """Register built-in adapters (idempotent). - - Parameters: - reg: Optional registry object; if omitted the shared `registry` is used. - """ - reg = reg or registry - - # Avoid repeated registration - if reg.get("parser", "nc_command") is None: - try: - from ncplot7py.infrastructure.parsers.nc_command_parser import register as _reg_p - - _reg_p(reg) - except Exception: - # Keep bootstrap safe even if optional adapters fail - pass - - -def main() -> None: # pragma: no cover - CLI entrypoint - bootstrap() - - -if __name__ == "__main__": - main() - +"""CLI bootstrap helpers. + +Provide a small `bootstrap()` function that registers builtin adapters +into the shared `registry`. This keeps import-time side-effects out of +module import and makes tests deterministic. +""" +from __future__ import annotations + +from typing import Optional + +from ncplot7py.shared.registry import registry + + +def bootstrap(reg: Optional[object] = None) -> None: + """Register built-in adapters (idempotent). + + Parameters: + reg: Optional registry object; if omitted the shared `registry` is used. + """ + reg = reg or registry + + # Avoid repeated registration + if reg.get("parser", "nc_command") is None: + try: + from ncplot7py.infrastructure.parsers.nc_command_parser import register as _reg_p + + _reg_p(reg) + except Exception: + # Keep bootstrap safe even if optional adapters fail + pass + + +def main() -> None: # pragma: no cover - CLI entrypoint + bootstrap() + + +if __name__ == "__main__": + main() + diff --git a/src/ncplot7py/domain/cnc_state.py b/src/ncplot7py/domain/cnc_state.py index 9a83f9b..cbe112a 100644 --- a/src/ncplot7py/domain/cnc_state.py +++ b/src/ncplot7py/domain/cnc_state.py @@ -1,195 +1,195 @@ -"""CNCState model moved into domain package. - -Same content as the previous `shared.cnc_state` but placed under domain to -reflect that this is core domain model logic. -""" -from __future__ import annotations - -from dataclasses import dataclass, field, asdict -from copy import deepcopy -from typing import Dict, List, Optional, Tuple - - -AxisName = str -Numeric = float - - -@dataclass -class CNCState: - """Represents the machine state during NC interpretation. - - Key pieces of data: - - modal_groups: mapping of modal group name -> active code (e.g. 'G0', 'G1') - - axes: mapping axis name -> current coordinate (e.g. 'X': 0.0) - - offsets: tool/cs offsets applied to axes (same shape as `axes`) - - feed_rate: current feed value (units depend on `units` modal) - - spindle_speed: current spindle speed - - tool_radius: numeric tool radius (for compensation) - - parameters: generic user/parameter variables (#-style) stored as dict - - extra: place to store vendor-specific flags (e.g., polar mode axis name) - - Methods on the state are intentionally small and side-effecting; callers - should control transactional behaviour (clone/restore) when needed. - """ - - # Modal groups — keys are group identifiers (string), values the active code - modal_groups: Dict[str, Optional[str]] = field(default_factory=dict) - - # Axis positions — use dict to support arbitrary axis sets (X,Y,Z,A,B,C...) - axes: Dict[AxisName, Numeric] = field(default_factory=lambda: {"X": 0.0, "Y": 0.0, "Z": 0.0}) - - # Offset axes (tool length, coordinate system offsets, etc.) - offsets: Dict[AxisName, Numeric] = field(default_factory=dict) - - # Multipliers and override feeds per axis (e.g., for scaling or axis-specific feed) - axis_multipliers: Dict[AxisName, Numeric] = field(default_factory=dict) - axis_override_feeds: Dict[AxisName, Numeric] = field(default_factory=dict) - - # Per-axis unit interpretation: 'radius' or 'diameter'. Internally we - # prefer to work in radius (true distances). When an axis is marked as - # 'diameter' incoming values on that axis should be divided by 2. - axis_units: Dict[AxisName, str] = field(default_factory=lambda: {"X": "radius", "Y": "radius", "Z": "radius"}) - - # Motion & tooling fields - feed_rate: Optional[Numeric] = None - spindle_speed: Optional[Numeric] = None - tool_radius: Optional[Numeric] = None - tool_quadrant: Optional[int] = None - - # Program parameters / variables (#500 style) and DDDP table if present - parameters: Dict[str, Numeric] = field(default_factory=dict) - dddp_set: Dict[str, Numeric] = field(default_factory=dict) - - # Misc - line_number: int = 0 - loop_command: List[str] = field(default_factory=list) - extra: Dict[str, object] = field(default_factory=dict) - - def clone(self) -> "CNCState": - """Return a deep copy of the state for transactional updates.""" - return deepcopy(self) - - def as_dict(self) -> Dict: - """Serialize to a plain dict (helpful for debugging/tests).""" - return asdict(self) - - # --- axis helpers ------------------------------------------------- - def get_axis(self, name: AxisName) -> Numeric: - return self.axes.get(name, 0.0) - - def set_axis(self, name: AxisName, value: Numeric) -> None: - self.axes[name] = float(value) - - def update_axes(self, updates: Dict[AxisName, Numeric]) -> None: - for k, v in updates.items(): - self.set_axis(k, float(v)) - - # --- axis unit helpers ------------------------------------------ - def get_axis_unit(self, name: AxisName) -> str: - """Return the unit interpretation for an axis: 'radius' or 'diameter'.""" - return str(self.axis_units.get(name, "radius")) - - def set_axis_unit(self, name: AxisName, unit: str) -> None: - """Set the unit interpretation for an axis. Accepts 'radius' or 'diameter'.""" - self.axis_units[name] = str(unit) - - def is_axis_diameter(self, name: AxisName) -> bool: - return self.get_axis_unit(name).lower() == "diameter" - - def normalize_axis_value(self, name: AxisName, value: Numeric) -> Numeric: - """Normalize a numeric value for the given axis to internal units (radius). - - If the axis is marked as 'diameter' this divides the value by 2. Preserves - sign and numeric type. - """ - try: - v = float(value) - except Exception: - return 0.0 - return v / 2.0 if self.is_axis_diameter(name) else v - - def normalize_target_spec(self, target_spec: Dict[AxisName, Numeric]) -> Dict[AxisName, Numeric]: - """Return a new target spec with per-axis normalization applied. - - This does not mutate the original dict. - """ - return {ax: self.normalize_axis_value(ax, val) for ax, val in target_spec.items()} - - def normalize_arc_params(self, params: Dict[str, Numeric]) -> Dict[str, Numeric]: - """Normalize common arc parameters (I,J,K,R) into internal units. - - - I -> X offset, J -> Y offset, K -> Z offset - - R is treated as a radial distance: if either X or Y is set to - 'diameter' we assume R was provided in diameter units and divide by 2. - This is a reasonable heuristic for XY-plane arcs on lathes. - """ - p = dict(params) - if "I" in p: - p["I"] = float(self.normalize_axis_value("X", p.get("I", 0.0))) - if "J" in p: - p["J"] = float(self.normalize_axis_value("Y", p.get("J", 0.0))) - if "K" in p: - p["K"] = float(self.normalize_axis_value("Z", p.get("K", 0.0))) - if "R" in p: - # Treat R as a true radial distance. Do not alter R based on - # per-axis 'diameter' interpretation — R is a geometric radius - # and should match the units of the provided coordinates. - p["R"] = float(p.get("R", 0.0)) - return p - - def apply_offsets(self) -> Dict[AxisName, Numeric]: - """Return axes with offsets applied (doesn't mutate state).""" - result = {} - for ax, pos in self.axes.items(): - off = self.offsets.get(ax, 0.0) - result[ax] = pos + off - return result - - # --- modal helpers ----------------------------------------------- - def set_modal(self, group: str, code: Optional[str]) -> None: - """Set the active code for a modal group.""" - self.modal_groups[group] = code - - def get_modal(self, group: str) -> Optional[str]: - return self.modal_groups.get(group) - - # --- parameter helpers ------------------------------------------- - def set_parameter(self, name: str, value: Numeric) -> None: - self.parameters[name] = float(value) - - def get_parameter(self, name: str, default: Optional[Numeric] = None) -> Optional[Numeric]: - return self.parameters.get(name, default) - - # --- coordinate resolution --------------------------------------- - def resolve_target(self, target_spec: Dict[AxisName, Numeric], absolute: bool = True) -> Dict[AxisName, Numeric]: - """Given a target spec (possibly partial), return resolved absolute coords. - - If `absolute` is False, the values in `target_spec` are treated as deltas - and applied to the current axis positions. - """ - resolved: Dict[AxisName, Numeric] = {} - if absolute: - # start with current axes, then update with provided values - for ax in set(list(self.axes.keys()) + list(target_spec.keys())): - if ax in target_spec: - resolved[ax] = float(target_spec[ax]) - else: - resolved[ax] = self.get_axis(ax) - else: - for ax in set(list(self.axes.keys()) + list(target_spec.keys())): - delta = float(target_spec.get(ax, 0.0)) - resolved[ax] = self.get_axis(ax) + delta - return resolved - - # --- small utility ------------------------------------------------ - def compute_distance(self, a: Dict[AxisName, Numeric], b: Dict[AxisName, Numeric], axes: Optional[List[AxisName]] = None) -> float: - """Euclidean distance between two axis positions using given axes list.""" - if axes is None: - axes = list(set(list(a.keys()) + list(b.keys()))) - s = 0.0 - for ax in axes: - s += (a.get(ax, 0.0) - b.get(ax, 0.0)) ** 2 - return float(s ** 0.5) - - -__all__ = ["CNCState"] +"""CNCState model moved into domain package. + +Same content as the previous `shared.cnc_state` but placed under domain to +reflect that this is core domain model logic. +""" +from __future__ import annotations + +from dataclasses import dataclass, field, asdict +from copy import deepcopy +from typing import Dict, List, Optional, Tuple + + +AxisName = str +Numeric = float + + +@dataclass +class CNCState: + """Represents the machine state during NC interpretation. + + Key pieces of data: + - modal_groups: mapping of modal group name -> active code (e.g. 'G0', 'G1') + - axes: mapping axis name -> current coordinate (e.g. 'X': 0.0) + - offsets: tool/cs offsets applied to axes (same shape as `axes`) + - feed_rate: current feed value (units depend on `units` modal) + - spindle_speed: current spindle speed + - tool_radius: numeric tool radius (for compensation) + - parameters: generic user/parameter variables (#-style) stored as dict + - extra: place to store vendor-specific flags (e.g., polar mode axis name) + + Methods on the state are intentionally small and side-effecting; callers + should control transactional behaviour (clone/restore) when needed. + """ + + # Modal groups — keys are group identifiers (string), values the active code + modal_groups: Dict[str, Optional[str]] = field(default_factory=dict) + + # Axis positions — use dict to support arbitrary axis sets (X,Y,Z,A,B,C...) + axes: Dict[AxisName, Numeric] = field(default_factory=lambda: {"X": 0.0, "Y": 0.0, "Z": 0.0}) + + # Offset axes (tool length, coordinate system offsets, etc.) + offsets: Dict[AxisName, Numeric] = field(default_factory=dict) + + # Multipliers and override feeds per axis (e.g., for scaling or axis-specific feed) + axis_multipliers: Dict[AxisName, Numeric] = field(default_factory=dict) + axis_override_feeds: Dict[AxisName, Numeric] = field(default_factory=dict) + + # Per-axis unit interpretation: 'radius' or 'diameter'. Internally we + # prefer to work in radius (true distances). When an axis is marked as + # 'diameter' incoming values on that axis should be divided by 2. + axis_units: Dict[AxisName, str] = field(default_factory=lambda: {"X": "radius", "Y": "radius", "Z": "radius"}) + + # Motion & tooling fields + feed_rate: Optional[Numeric] = None + spindle_speed: Optional[Numeric] = None + tool_radius: Optional[Numeric] = None + tool_quadrant: Optional[int] = None + + # Program parameters / variables (#500 style) and DDDP table if present + parameters: Dict[str, Numeric] = field(default_factory=dict) + dddp_set: Dict[str, Numeric] = field(default_factory=dict) + + # Misc + line_number: int = 0 + loop_command: List[str] = field(default_factory=list) + extra: Dict[str, object] = field(default_factory=dict) + + def clone(self) -> "CNCState": + """Return a deep copy of the state for transactional updates.""" + return deepcopy(self) + + def as_dict(self) -> Dict: + """Serialize to a plain dict (helpful for debugging/tests).""" + return asdict(self) + + # --- axis helpers ------------------------------------------------- + def get_axis(self, name: AxisName) -> Numeric: + return self.axes.get(name, 0.0) + + def set_axis(self, name: AxisName, value: Numeric) -> None: + self.axes[name] = float(value) + + def update_axes(self, updates: Dict[AxisName, Numeric]) -> None: + for k, v in updates.items(): + self.set_axis(k, float(v)) + + # --- axis unit helpers ------------------------------------------ + def get_axis_unit(self, name: AxisName) -> str: + """Return the unit interpretation for an axis: 'radius' or 'diameter'.""" + return str(self.axis_units.get(name, "radius")) + + def set_axis_unit(self, name: AxisName, unit: str) -> None: + """Set the unit interpretation for an axis. Accepts 'radius' or 'diameter'.""" + self.axis_units[name] = str(unit) + + def is_axis_diameter(self, name: AxisName) -> bool: + return self.get_axis_unit(name).lower() == "diameter" + + def normalize_axis_value(self, name: AxisName, value: Numeric) -> Numeric: + """Normalize a numeric value for the given axis to internal units (radius). + + If the axis is marked as 'diameter' this divides the value by 2. Preserves + sign and numeric type. + """ + try: + v = float(value) + except Exception: + return 0.0 + return v / 2.0 if self.is_axis_diameter(name) else v + + def normalize_target_spec(self, target_spec: Dict[AxisName, Numeric]) -> Dict[AxisName, Numeric]: + """Return a new target spec with per-axis normalization applied. + + This does not mutate the original dict. + """ + return {ax: self.normalize_axis_value(ax, val) for ax, val in target_spec.items()} + + def normalize_arc_params(self, params: Dict[str, Numeric]) -> Dict[str, Numeric]: + """Normalize common arc parameters (I,J,K,R) into internal units. + + - I -> X offset, J -> Y offset, K -> Z offset + - R is treated as a radial distance: if either X or Y is set to + 'diameter' we assume R was provided in diameter units and divide by 2. + This is a reasonable heuristic for XY-plane arcs on lathes. + """ + p = dict(params) + if "I" in p: + p["I"] = float(self.normalize_axis_value("X", p.get("I", 0.0))) + if "J" in p: + p["J"] = float(self.normalize_axis_value("Y", p.get("J", 0.0))) + if "K" in p: + p["K"] = float(self.normalize_axis_value("Z", p.get("K", 0.0))) + if "R" in p: + # Treat R as a true radial distance. Do not alter R based on + # per-axis 'diameter' interpretation — R is a geometric radius + # and should match the units of the provided coordinates. + p["R"] = float(p.get("R", 0.0)) + return p + + def apply_offsets(self) -> Dict[AxisName, Numeric]: + """Return axes with offsets applied (doesn't mutate state).""" + result = {} + for ax, pos in self.axes.items(): + off = self.offsets.get(ax, 0.0) + result[ax] = pos + off + return result + + # --- modal helpers ----------------------------------------------- + def set_modal(self, group: str, code: Optional[str]) -> None: + """Set the active code for a modal group.""" + self.modal_groups[group] = code + + def get_modal(self, group: str) -> Optional[str]: + return self.modal_groups.get(group) + + # --- parameter helpers ------------------------------------------- + def set_parameter(self, name: str, value: Numeric) -> None: + self.parameters[name] = float(value) + + def get_parameter(self, name: str, default: Optional[Numeric] = None) -> Optional[Numeric]: + return self.parameters.get(name, default) + + # --- coordinate resolution --------------------------------------- + def resolve_target(self, target_spec: Dict[AxisName, Numeric], absolute: bool = True) -> Dict[AxisName, Numeric]: + """Given a target spec (possibly partial), return resolved absolute coords. + + If `absolute` is False, the values in `target_spec` are treated as deltas + and applied to the current axis positions. + """ + resolved: Dict[AxisName, Numeric] = {} + if absolute: + # start with current axes, then update with provided values + for ax in set(list(self.axes.keys()) + list(target_spec.keys())): + if ax in target_spec: + resolved[ax] = float(target_spec[ax]) + else: + resolved[ax] = self.get_axis(ax) + else: + for ax in set(list(self.axes.keys()) + list(target_spec.keys())): + delta = float(target_spec.get(ax, 0.0)) + resolved[ax] = self.get_axis(ax) + delta + return resolved + + # --- small utility ------------------------------------------------ + def compute_distance(self, a: Dict[AxisName, Numeric], b: Dict[AxisName, Numeric], axes: Optional[List[AxisName]] = None) -> float: + """Euclidean distance between two axis positions using given axes list.""" + if axes is None: + axes = list(set(list(a.keys()) + list(b.keys()))) + s = 0.0 + for ax in axes: + s += (a.get(ax, 0.0) - b.get(ax, 0.0)) ** 2 + return float(s ** 0.5) + + +__all__ = ["CNCState"] diff --git a/src/ncplot7py/domain/exceptions.py b/src/ncplot7py/domain/exceptions.py index 3fa7fd3..288910c 100644 --- a/src/ncplot7py/domain/exceptions.py +++ b/src/ncplot7py/domain/exceptions.py @@ -1,139 +1,139 @@ -"""Domain-specific exceptions. - -This module provides a small, typed exception class suitable for tracing -errors originating from CNC/NC code parsing and handling. The class is -lightweight, serializable and has a pleasant string representation for -consumers (CLI/IDE/web) so we can present clean error messages to users. -""" - -from __future__ import annotations - -from enum import IntEnum -from dataclasses import dataclass, asdict -from typing import Any - - -class ExceptionTyps(IntEnum): - """Types of domain exceptions. - - Values are small ints to make serialization compact where necessary. - """ - - NCCodeErrors = 1 - NCCanalStarErrors = 2 - CNCError = 3 - - -@dataclass -class ExceptionNode(Exception): - """A structured exception carrying CNC error information. - - Fields: - typ: Which category of error (ExceptionTyps). - code: Numeric error code (domain-specific). - line: Source line number where the error occurred (1-based), 0 if unknown. - message: Human readable message describing the error. - value: Optional value/context (e.g. offending token or text). - - The class inherits from Exception so it can be raised. It also provides - `to_dict()` and a friendly `__str__()` for user-facing output. - """ - - typ: ExceptionTyps - code: int = 0 - line: int = 0 - message: str = "" - value: Any = "" - # Traceback-ish metadata for better diagnostics - file: str = "" - column: int = 0 - context: str = "" - - def __post_init__(self) -> None: - # Ensure base Exception gets the message so built-in tooling sees it. - Exception.__init__(self, self.message) - - def to_dict(self) -> dict: - """Return a plain-serializable representation of the exception.""" - data = asdict(self) - # Represent enum as its name (and maybe value) for consumers. - data["typ"] = {"name": self.typ.name, "value": int(self.typ)} - return data - - def __str__(self) -> str: # pragma: no cover - simple formatting - parts = [f"{self.typ.name} (code={self.code})"] - if self.line: - parts.append(f"line={self.line}") - if self.message: - parts.append(f"message={self.message}") - if self.value not in ("", None): - parts.append(f"value={self.value}") - if self.file: - parts.append(f"file={self.file}") - if self.column: - parts.append(f"col={self.column}") - return "; ".join(parts) - - def localized(self, lang: str = "en") -> str: - """Return a localized message if a catalog is available. - - This avoids importing i18n at module import time to keep domain slim. - """ - try: - from .i18n import MessageCatalog - - catalog = MessageCatalog() - return catalog.format_exception(self, lang=lang, include_trace=True) - except Exception: - return str(self) - - -def raise_nc_error( - typ: ExceptionTyps, - code: int, - *, - message: str = "", - value: Any = "", - file: str = "", - line: int = 0, - column: int = 0, - source_line: str = "", -) -> None: - """Raise an ExceptionNode with best-effort column/context extraction. - - Parameters: - typ: Exception type (ExceptionTyps) - code: Numeric error code - message: Human readable fallback message if i18n template not found - value: Offending value/token - file: Source file name/path - line: 1-based line number - column: 1-based column number; if 0 and source_line+value are provided, - column will be inferred from the first match of value within - source_line. - source_line: The full line text to include as context (optional) - """ - - inferred_col = column - ctx = source_line - if (not inferred_col) and value and source_line: - try: - idx = source_line.find(str(value)) - if idx >= 0: - inferred_col = idx + 1 # 1-based - except Exception: - inferred_col = column or 0 - - exc = ExceptionNode( - typ=typ, - code=code, - line=line, - message=message, - value=value, - file=file, - column=inferred_col or column, - context=ctx, - ) - raise exc - - +"""Domain-specific exceptions. + +This module provides a small, typed exception class suitable for tracing +errors originating from CNC/NC code parsing and handling. The class is +lightweight, serializable and has a pleasant string representation for +consumers (CLI/IDE/web) so we can present clean error messages to users. +""" + +from __future__ import annotations + +from enum import IntEnum +from dataclasses import dataclass, asdict +from typing import Any + + +class ExceptionTyps(IntEnum): + """Types of domain exceptions. + + Values are small ints to make serialization compact where necessary. + """ + + NCCodeErrors = 1 + NCCanalStarErrors = 2 + CNCError = 3 + + +@dataclass +class ExceptionNode(Exception): + """A structured exception carrying CNC error information. + + Fields: + typ: Which category of error (ExceptionTyps). + code: Numeric error code (domain-specific). + line: Source line number where the error occurred (1-based), 0 if unknown. + message: Human readable message describing the error. + value: Optional value/context (e.g. offending token or text). + + The class inherits from Exception so it can be raised. It also provides + `to_dict()` and a friendly `__str__()` for user-facing output. + """ + + typ: ExceptionTyps + code: int = 0 + line: int = 0 + message: str = "" + value: Any = "" + # Traceback-ish metadata for better diagnostics + file: str = "" + column: int = 0 + context: str = "" + + def __post_init__(self) -> None: + # Ensure base Exception gets the message so built-in tooling sees it. + Exception.__init__(self, self.message) + + def to_dict(self) -> dict: + """Return a plain-serializable representation of the exception.""" + data = asdict(self) + # Represent enum as its name (and maybe value) for consumers. + data["typ"] = {"name": self.typ.name, "value": int(self.typ)} + return data + + def __str__(self) -> str: # pragma: no cover - simple formatting + parts = [f"{self.typ.name} (code={self.code})"] + if self.line: + parts.append(f"line={self.line}") + if self.message: + parts.append(f"message={self.message}") + if self.value not in ("", None): + parts.append(f"value={self.value}") + if self.file: + parts.append(f"file={self.file}") + if self.column: + parts.append(f"col={self.column}") + return "; ".join(parts) + + def localized(self, lang: str = "en") -> str: + """Return a localized message if a catalog is available. + + This avoids importing i18n at module import time to keep domain slim. + """ + try: + from .i18n import MessageCatalog + + catalog = MessageCatalog() + return catalog.format_exception(self, lang=lang, include_trace=True) + except Exception: + return str(self) + + +def raise_nc_error( + typ: ExceptionTyps, + code: int, + *, + message: str = "", + value: Any = "", + file: str = "", + line: int = 0, + column: int = 0, + source_line: str = "", +) -> None: + """Raise an ExceptionNode with best-effort column/context extraction. + + Parameters: + typ: Exception type (ExceptionTyps) + code: Numeric error code + message: Human readable fallback message if i18n template not found + value: Offending value/token + file: Source file name/path + line: 1-based line number + column: 1-based column number; if 0 and source_line+value are provided, + column will be inferred from the first match of value within + source_line. + source_line: The full line text to include as context (optional) + """ + + inferred_col = column + ctx = source_line + if (not inferred_col) and value and source_line: + try: + idx = source_line.find(str(value)) + if idx >= 0: + inferred_col = idx + 1 # 1-based + except Exception: + inferred_col = column or 0 + + exc = ExceptionNode( + typ=typ, + code=code, + line=line, + message=message, + value=value, + file=file, + column=inferred_col or column, + context=ctx, + ) + raise exc + + diff --git a/src/ncplot7py/domain/exec_chain.py b/src/ncplot7py/domain/exec_chain.py index 547b9ce..38b4e57 100644 --- a/src/ncplot7py/domain/exec_chain.py +++ b/src/ncplot7py/domain/exec_chain.py @@ -1,32 +1,32 @@ -"""Chain-of-Responsibility base Handler moved to domain. - -This module contains the Handler primitive; placing it in `domain` makes it -the canonical location for execution primitives that operate on domain state. -""" -from __future__ import annotations - -from typing import Optional, Tuple, List, TYPE_CHECKING, Any - -from ncplot7py.shared.nc_nodes import NCCommandNode -from ncplot7py.shared.state_protocol import StateProtocol - -if TYPE_CHECKING: - CNCStateType = StateProtocol -else: - CNCStateType = Any - - -class Handler: - """Base handler with optional next pointer.""" - - def __init__(self, next_handler: Optional["Handler"] = None): - self.next_handler = next_handler - - def handle(self, node: NCCommandNode, state: CNCStateType) -> Tuple[Optional[List], Optional[float]]: - """Handle a node; return (points_list, duration) or delegate.""" - if self.next_handler is not None: - return self.next_handler.handle(node, state) - return None, None - - -__all__ = ["Handler"] +"""Chain-of-Responsibility base Handler moved to domain. + +This module contains the Handler primitive; placing it in `domain` makes it +the canonical location for execution primitives that operate on domain state. +""" +from __future__ import annotations + +from typing import Optional, Tuple, List, TYPE_CHECKING, Any + +from ncplot7py.shared.nc_nodes import NCCommandNode +from ncplot7py.shared.state_protocol import StateProtocol + +if TYPE_CHECKING: + CNCStateType = StateProtocol +else: + CNCStateType = Any + + +class Handler: + """Base handler with optional next pointer.""" + + def __init__(self, next_handler: Optional["Handler"] = None): + self.next_handler = next_handler + + def handle(self, node: NCCommandNode, state: CNCStateType) -> Tuple[Optional[List], Optional[float]]: + """Handle a node; return (points_list, duration) or delegate.""" + if self.next_handler is not None: + return self.next_handler.handle(node, state) + return None, None + + +__all__ = ["Handler"] diff --git a/src/ncplot7py/domain/handlers/control_flow.py b/src/ncplot7py/domain/handlers/control_flow.py index d2ef2b3..173ee1d 100644 --- a/src/ncplot7py/domain/handlers/control_flow.py +++ b/src/ncplot7py/domain/handlers/control_flow.py @@ -1,318 +1,318 @@ -"""Control-flow handler for simple loop constructs in NC programs. - -Supports a small subset of constructs found in some controllers: -- GOTO -- IF ... GOTO -- WHILE DO / END (basic skip-if-false behaviour) - -This handler manipulates node linked pointers (`_next_ncCode`) so the -canal execution loop can jump to the desired node. It expects nodes to be -linked via `_next_ncCode` and `_before_ncCode` beforehand. -""" -from __future__ import annotations - -import re -from typing import Optional, Tuple -import os -import logging - -from ncplot7py.domain.exec_chain import Handler -from ncplot7py.shared.nc_nodes import NCCommandNode -from ncplot7py.domain.cnc_state import CNCState -from ncplot7py.domain.handlers.variable import VariableHandler - - -class ControlFlowHandler(Handler): - TOKEN_RE = re.compile(r"(GOTO|THEN|IF|END|DO|WHILE)") - - def __init__(self, next_handler: Optional[Handler] = None): - super().__init__(next_handler=next_handler) - # use variable handler utilities for expression evaluation - self._eval_helper = VariableHandler() - # runtime maps (populated by canal before execution) - self._n_map = None - self._do_map = None - self._end_map = None - self._nodes = None - self._loop_counters = {} - - def _find_node_with_N(self, start: NCCommandNode, pos: str) -> Optional[NCCommandNode]: - # Prefer lookup via precomputed map when available - try: - target = float(pos) - except Exception: - return None - - if getattr(self, "_n_map", None) is not None: - return self._n_map.get(target) - - # fallback: search forward then backward - node = start - # search forwards - while node is not None: - try: - nval = node.command_parameter.get("N") - except Exception: - nval = None - if nval is not None: - try: - if float(nval) == target: - return node - except Exception: - pass - node = getattr(node, "_next_ncCode", None) - - # search backwards - node = start - while node is not None: - try: - nval = node.command_parameter.get("N") - except Exception: - nval = None - if nval is not None: - try: - if float(nval) == target: - return node - except Exception: - pass - node = getattr(node, "_before_ncCode", None) - - return None - - def _find_end_for_do(self, start: NCCommandNode, pos: str) -> Optional[NCCommandNode]: - # Prefer map lookup when available - if getattr(self, "_end_map", None) is not None: - lst = self._end_map.get(pos) - if lst: - # return first end node after start - if self._nodes is not None: - try: - start_idx = self._nodes.index(start) - except ValueError: - start_idx = -1 - for n in lst: - try: - idx = self._nodes.index(n) - if idx > start_idx: - return n - except ValueError: - continue - return lst[0] - - node = start - while node is not None: - lc = node.loop_command - if lc and f"END{pos}" in lc: - return node - node = getattr(node, "_next_ncCode", None) - return None - - def _is_true(self, cond_text: str, state: CNCState) -> bool: - logger = logging.getLogger(__name__) - # strip surrounding brackets which may come from parsed tokens - try: - cond_text = cond_text.strip() - if cond_text.startswith("[") and cond_text.endswith("]"): - cond_text = cond_text[1:-1] - except Exception: - pass - # detect operators GT, LT, GE, LE, EQ - for op in ("GE", "LE", "GT", "LT", "EQ"): - if op in cond_text: - left, right = cond_text.split(op, 1) - try: - lv = self._eval_helper._eval_expression(left, state) - rv = self._eval_helper._eval_expression(right, state) - except Exception: - logger.debug("_is_true failed eval for cond=%s left=%s right=%s", cond_text, left, right, exc_info=True) - try: - logger.debug("_is_true state.parameters snapshot: %s", getattr(state, 'parameters', None)) - except Exception: - pass - return False - if op == "GT": - result = float(lv) > float(rv) - logger.debug("_is_true: %s GT %s -> %s", lv, rv, result) - return result - if op == "LT": - result = float(lv) < float(rv) - logger.debug("_is_true: %s LT %s -> %s", lv, rv, result) - return result - if op == "GE": - result = float(lv) >= float(rv) - logger.debug("_is_true: %s GE %s -> %s", lv, rv, result) - return result - if op == "LE": - result = float(lv) <= float(rv) - logger.debug("_is_true: %s LE %s -> %s", lv, rv, result) - return result - if op == "EQ": - result = float(lv) == float(rv) - logger.debug("_is_true: %s EQ %s -> %s", lv, rv, result) - return result - return False - - def handle(self, node: NCCommandNode, state: CNCState) -> Tuple[Optional[list], Optional[float]]: - lc = node.loop_command - if not lc: - return super().handle(node, state) - - # Insert spaces before tokens to help splitting (parser removed spaces) - command = re.sub(self.TOKEN_RE, r" \1", lc) - tokens = [t for t in command.split(" ") if t] - - # simple scan for IF...GOTO or plain GOTO - for token in tokens: - if token.startswith("IF"): - cond = token[2:] - if self._is_true(cond, state): - # find GOTO token in same command - for t2 in tokens: - if t2.startswith("GOTO"): - pos = t2.split("GOTO", 1)[1] - target = self._find_node_with_N(node, pos) - if target is None: - # nothing we can do; fallthrough - break - node._next_ncCode = target - break - # whether true or false, stop processing IF - break - elif token.startswith("GOTO"): - pos = token.split("GOTO", 1)[1] - # GOTO may reference a DO-label or an N label - target = None - try: - key = float(pos) - except Exception: - key = None - if key is not None and getattr(self, "_n_map", None) is not None: - target = self._n_map.get(key) - if target is None and getattr(self, "_do_map", None) is not None: - # do_map entries are lists; pick first - lst = self._do_map.get(pos) - if lst: - target = lst[0] - if target is None: - target = self._find_node_with_N(node, pos) - if target is not None: - node._next_ncCode = target - break - elif token.startswith("WHILE"): - cond = token[5:] - # if condition is false, skip forward to matching END - if not self._is_true(cond, state): - # find DO token to get pos - for t2 in tokens: - if t2.startswith("DO"): - pos = t2.split("DO", 1)[1] - end_node = self._find_end_for_do(node, pos) - if end_node is not None: - node._next_ncCode = getattr(end_node, "_next_ncCode", None) - break - # otherwise, continue into loop (no change) - break - elif token.startswith("DO"): - # DO may be a label or start of a counted loop. If the DO node - # provides parameter 'L' we treat it as a loop counter. - label = token[2:] - # initialize loop counter if present on this node - try: - lval = node.command_parameter.get("L") - except Exception: - lval = None - if lval is not None: - try: - cnt = int(float(self._eval_helper._eval_expression(str(lval), state))) - except Exception: - try: - cnt = int(float(lval)) - except Exception: - cnt = None - if cnt is not None: - # store counter by label so END can find it - self._loop_counters[label] = cnt - break - elif token.startswith("END"): - label = token[3:] - # find matching DO node (prefer map) - do_node = None - if getattr(self, "_do_map", None) is not None and label in self._do_map: - # pick the nearest DO before this END - candidates = self._do_map[label] - if self._nodes is not None: - try: - end_idx = self._nodes.index(node) - except ValueError: - end_idx = -1 - best = None - best_idx = -1 - for n in candidates: - try: - idx = self._nodes.index(n) - if idx < end_idx and idx > best_idx: - best = n - best_idx = idx - except ValueError: - continue - do_node = best or (candidates[0] if candidates else None) - else: - do_node = candidates[0] if candidates else None - else: - # fallback: search backward for a DO with same label - cur = getattr(node, "_before_ncCode", None) - while cur is not None: - lc = cur.loop_command - if lc and f"DO{label}" in lc: - do_node = cur - break - cur = getattr(cur, "_before_ncCode", None) - - if do_node is not None: - # if there's a loop counter for this label, decrement and jump - cnt = self._loop_counters.get(label) - if cnt is not None: - if cnt > 1: - self._loop_counters[label] = cnt - 1 - node._next_ncCode = getattr(do_node, "_next_ncCode", do_node) - else: - # completed loop - del self._loop_counters[label] - else: - # If this DO was actually a WHILE (e.g. "WHILE[...]DO