diff --git a/.gitignore b/.gitignore index 28b18d0..ef3447e 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ venv/ ENV/ env.bak/ venv.bak/ -.pytest_cache/ \ No newline at end of file +.pytest_cache/ +/site/ \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9b16053..b3d2025 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,33 +1,127 @@ # Contributing to ImportSpy -🎉 Thank you for considering a contribution to **ImportSpy**! We value your efforts and welcome **issues, features, and documentation improvements**. +🎉 Thank you for considering a contribution to **ImportSpy** — a project designed to bring structure, security, and clarity to Python imports. + +We welcome all kinds of contributions: new features, bug fixes, tests, documentation, and even thoughtful ideas. Your involvement makes the project better. + +--- ## 📢 How to Contribute -### 🔍 1. Issue Reporting -If you find a **bug** or have a **feature request**, please open an issue: -🔗 [GitHub Issues](https://github.com/atellaluca/ImportSpy/issues) - -### 🔄 2. Fork & Branching Strategy -- **Create a fork** and work on a separate branch before submitting a pull request. -- Use the following **branch naming conventions**: - - **For features:** `feature/nome-feature-in-breve` - - **For bug fixes:** `fix/bugfix-description` - - **For documentation:** `docs/update-section` - -### ✅ 3. Code Style & Commit Rules -- **Follow Conventional Commits**: - - `feat:` → For new features - - `fix:` → For bug fixes - - `docs:` → For documentation changes - - `test:` → For tests - - `refactor:` → Code improvements without changing functionality - -Example: +### 1. Open an Issue +If you encounter a bug, have a feature suggestion, or want to propose a change, please [open an issue](https://github.com/atellaluca/ImportSpy/issues). Use clear and descriptive titles. + +You can use labels such as: +- `bug`: for broken functionality +- `enhancement`: for feature requests +- `question`: for clarification or design discussions + +### 2. Fork and Branch Strategy + +- Create a **fork** of the repository. +- Work in a dedicated **feature branch** off `main`. +- Use consistent branch naming: + +| Purpose | Branch Name Format | +|----------------|-------------------------------| +| Feature | `feature/short-description` | +| Bug fix | `fix/issue-description` | +| Documentation | `docs/section-description` | +| Tests | `test/feature-or-bug-name` | + +> 💡 Keep pull requests focused and small. This helps reviewers and speeds up merging. + +--- + +## ✅ Code Standards + +### Python Style Guide +ImportSpy follows: +- [PEP8](https://peps.python.org/pep-0008/) +- Type hints throughout the codebase +- `black` + `ruff` for formatting and linting +- `pydantic` for data models + +### Linting & Tests + +To run tests and lint checks: + ```bash -git commit -m "feat: add validation for OS compatibility" +poetry install +pytest +ruff check . +black --check . ``` -💡 **We Appreciate Every Contribution!** +--- + +## 📄 Commit Conventions + +We use **[Conventional Commits](https://www.conventionalcommits.org/)** for readable history and automatic changelog generation. + +| Type | Use For | +|----------|-------------------------------| +| `feat:` | New feature | +| `fix:` | Bug fix | +| `docs:` | Documentation only | +| `refactor:` | Code change w/o new feature or fix | +| `test:` | Adding or updating tests | +| `chore:` | Internal tooling or CI | + +**Example:** +```bash +git commit -m "feat: support multiple Python interpreters in contract" +``` + +--- + +## 🧪 Test Philosophy + +Tests live in `tests/validators/` and should: +- Cover core logic (validators, spymodel, contract violations) +- Include both positive and negative cases +- Be fast, deterministic, and isolated + +--- + +## ✍️ Docs Contributions + +We use **MkDocs + Material** for documentation. Docs live under: + +``` +docs/ +``` + +To preview locally: + +```bash +poetry install +mkdocs serve +``` + +New pages should be added to `mkdocs.yml` under the right section. + +--- + +## 🙌 Join the Community + +While we don’t yet have a Discord or forum, we encourage: +- Sharing feedback via GitHub Issues +- Discussing architecture via PRs and comments +- Connecting with the maintainer via LinkedIn or GitHub + +--- + +## 💬 Need Help? + +Open an issue with the `question` label or ping @atellaluca in your PR. + +--- + +## 📜 License + +By contributing, you agree your work will be released under the MIT License. + +--- -ImportSpy is an open-source project, and every contribution—big or small—helps make it better. 🚀 \ No newline at end of file +**Let your modules enforce their own rules — and thank you for helping ImportSpy grow!** diff --git a/README.md b/README.md new file mode 100644 index 0000000..c394218 --- /dev/null +++ b/README.md @@ -0,0 +1,178 @@ +# ImportSpy + +![License](https://img.shields.io/github/license/atellaluca/importspy) +[![PyPI Version](https://img.shields.io/pypi/v/importspy)](https://pypi.org/project/importspy/) +![Python Versions](https://img.shields.io/pypi/pyversions/importspy) +[![Build Status](https://img.shields.io/github/actions/workflow/status/atellaluca/ImportSpy/python-package.yml?branch=main)](https://github.com/atellaluca/ImportSpy/actions/workflows/python-package.yml) +[![Docs](https://img.shields.io/readthedocs/importspy)](https://importspy.readthedocs.io/) + +![ImportSpy banner](https://github.com/atellaluca/ImportSpy/docs/assets/importspy-banner.png) + +**Runtime contract validation for Python imports.** +_Enforce structure. Block invalid usage. Stay safe at runtime._ + +--- + +## 🔍 What is ImportSpy? + +**ImportSpy** lets your Python modules declare structured **import contracts** (via `.yml` files) to define: + +- What environment they expect (OS, Python version, interpreter) +- What structure they must follow (classes, methods, variables) +- Who is allowed to import them + +If the contract is not met, **ImportSpy blocks the import** — ensuring safe and predictable runtime behavior. + +--- + +## ✨ Key Features + +- ✅ Validate imports dynamically at runtime or via CLI +- ✅ Block incompatible usage of internal or critical modules +- ✅ Enforce module structure, arguments, annotations +- ✅ Context-aware: Python version, OS, architecture, interpreter +- ✅ Human-readable YAML contracts +- ✅ Clear, CI-friendly violation messages + +--- + +## 📦 Installation + +```bash +pip install importspy +``` + +> Requires Python 3.10+ + +--- + +## 📐 Architecture + +![SpyModel UML](https://github.com/atellaluca/ImportSpy/docs/assets/importspy-spy-model-architecture.png) + +ImportSpy is powered by a layered introspection model (`SpyModel`), which captures: + +- `Runtime`: CPU architecture +- `System`: OS and environment +- `Python`: interpreter and version +- `Module`: classes, functions, variables, annotations + +Each layer is validated against the corresponding section of your `.yml` contract. + +--- + +## 📜 Example Contract + +```yaml +filename: plugin.py +variables: + - name: mode + value: production + annotation: str +classes: + - name: Plugin + methods: + - name: run + arguments: + - name: self + - name: data + annotation: dict + return_annotation: None +``` + +--- + +## 🔧 Modes of Use + +### Embedded Mode – protect your own module + +```python +from importspy import Spy + +caller = Spy().importspy(filepath="spymodel.yml") +caller.Plugin().run() +``` + +![Embedded mode](https://github.com/atellaluca/ImportSpy/docs/assets/importspy-embedded-mode.png) + +--- + +### CLI Mode – external validation in CI + +```bash +importspy -s spymodel.yml -l DEBUG path/to/module.py +``` + +![CLI mode](https://github.com/atellaluca/ImportSpy/docs/assets/importspy-works.png) + +--- + +## 🧠 How It Works + +1. You define an import contract in `.yml` +2. At runtime or via CLI, ImportSpy inspects: + - Who is importing the module + - What the system/environment looks like + - What the module structure provides +3. If validation fails → the import is blocked +4. If valid → the module runs safely + +--- + +## ✅ Tech Stack + +- [Pydantic 2.x](https://docs.pydantic.dev) – schema validation +- [Typer](https://typer.tiangolo.com) – CLI +- [ruamel.yaml](https://yaml.readthedocs.io/) – YAML support +- `inspect` + `sys` – runtime introspection +- [Poetry](https://python-poetry.org) – dependency management +- [Sphinx](https://www.sphinx-doc.org) + ReadTheDocs – documentation + +--- + +## 📘 Documentation + +- **Full docs** → [importspy.readthedocs.io](https://importspy.readthedocs.io) +- [Quickstart](https://importspy.readthedocs.io/en/latest/intro/quickstart.html) +- [Contract syntax](https://importspy.readthedocs.io/en/latest/contracts/syntax.html) +- [Violation system](https://importspy.readthedocs.io/en/latest/advanced/violations.html) +- [API Reference](https://importspy.readthedocs.io/en/latest/api-reference.html) + +--- + +## 🚀 Ideal Use Cases + +- Plugin-based frameworks (e.g., CMS, CLI, IDE) +- CI/CD pipelines with strict integration +- Security-regulated environments (IoT, medical, fintech) +- Package maintainers enforcing internal boundaries + +--- + +## 💡 Why It Matters + +Python’s flexibility comes at a cost: + +- Silent runtime mismatches +- Missing methods or classes +- Platform-dependent failures +- No enforcement over module consumers + +**ImportSpy brings governance** +to how, when, and where modules are imported. + +--- + +## ❤️ Contribute & Support + +- ⭐ [Star on GitHub](https://github.com/atellaluca/ImportSpy) +- 🐛 [File issues or feature requests](https://github.com/atellaluca/ImportSpy/issues) +- 🤝 [Contribute](https://github.com/atellaluca/ImportSpy/blob/main/CONTRIBUTING.md) +- 💖 [Sponsor on GitHub](https://github.com/sponsors/atellaluca) + +--- + +## 📜 License + +MIT © 2024 – Luca Atella +![ImportSpy logo](https://github.com/atellaluca/ImportSpy/docs/assets/importspy-logo.png) diff --git a/README.rst b/README.rst deleted file mode 100644 index 2b554f8..0000000 --- a/README.rst +++ /dev/null @@ -1,190 +0,0 @@ -.. image:: https://img.shields.io/github/license/atellaluca/importspy - :alt: License - -.. image:: https://img.shields.io/pypi/v/importspy - :target: https://pypi.org/project/importspy/ - :alt: PyPI Version - -.. image:: https://img.shields.io/pypi/pyversions/importspy - :alt: Supported Python Versions - -.. image:: https://img.shields.io/github/actions/workflow/status/atellaluca/ImportSpy/python-package.yml - :target: https://github.com/atellaluca/ImportSpy/actions/workflows/python-package.yml - :alt: Build Status - -.. image:: https://img.shields.io/readthedocs/importspy - :target: https://importspy.readthedocs.io/ - :alt: Documentation Status - -.. image:: https://github.com/atellaluca/ImportSpy/blob/main/assets/importspy-banner.png - :alt: ImportSpy – Runtime Contract Validation for Python - :width: 500px - -ImportSpy -========= - -Contract-based import validation for Python modules. - -*Runtime-safe, structure-aware, declarative.* - -ImportSpy allows your Python modules to define explicit **import contracts**: -rules about where, how, and by whom they can be safely imported — and blocks any import that doesn’t comply. - -🔍 Key Benefits ---------------- - -- ✅ Prevent import from unsupported environments -- ✅ Enforce structural expectations (classes, attributes, arguments) -- ✅ Control who can use your module and how -- ✅ Reduce runtime surprises across CI, staging, and production -- ✅ Define everything in readable `.yml` contracts - -💡 Why ImportSpy? ------------------ - -Python is flexible, but uncontrolled imports can lead to: - -- 🔥 Silent runtime failures -- 🔍 Structural mismatches (wrong or missing methods/classes) -- 🌍 Inconsistent behavior across platforms -- 🚫 Unauthorized usage of internal code - -ImportSpy offers you **runtime import governance** — clearly defined, enforced in real-time. - -📐 Architecture Highlight -------------------------- - -.. image:: https://raw.githubusercontent.com/atellaluca/ImportSpy/refs/heads/main/assets/importspy-spy-model-architecture.png - :alt: ImportSpy, SpyModel Architecture - :width: 830 - -ImportSpy uses a layered model (`SpyModel`) that mirrors your execution context and module structure: - -- `Runtime` → defines architecture and system -- `System` → declares OS and environment variables -- `Python` → specifies interpreter, version, and modules -- `Module` → lists classes, functions, variables (each represented as objects, not dicts) - -Each element is introspected and validated dynamically, at runtime or via CLI. - -📜 Contract Example -------------------- - -.. code-block:: yaml - - filename: plugin.py - variables: - - name: mode - value: production - annotation: str - classes: - - name: Plugin - methods: - - name: run - arguments: - - name: self - - name: data - annotation: dict - return_annotation: None - -📦 Installation ---------------- - -.. code-block:: bash - - pip install importspy - -✅ Requires Python 3.10+ - -🔒 Usage Modes --------------- - -**Embedded Mode** – the module protects itself: - -.. image:: https://raw.githubusercontent.com/atellaluca/ImportSpy/refs/heads/main/assets/importspy-embedded-mode.png - :alt: How ImportSpy Embedded Mode Works - :width: 830 - -.. code-block:: python - - from importspy import Spy - importer = Spy().importspy(filepath="spymodel.yml") - importer.Plugin().run() - -**CLI Mode** – validate externally in CI/CD: - -.. image:: https://raw.githubusercontent.com/atellaluca/ImportSpy/refs/heads/main/assets/importspy-works.png - :alt: How ImportSpy CLI Mode Works - :width: 830 - -.. code-block:: bash - - importspy -s spymodel.yml -l DEBUG path/to/module.py - -📚 Features Overview --------------------- - -- ✅ Runtime validation based on import contracts -- ✅ YAML-based, declarative format -- ✅ Fine-grained introspection of classes, functions, arguments -- ✅ OS, architecture, interpreter matching -- ✅ Full error messages, CI-friendly output -- ✅ Supports embedded or external enforcement -- ✅ Strong internal model (`SpyModel`) powered by `pydantic` - -🚀 Ideal Use Cases ------------------- - -- 🛡️ Security-sensitive systems (finance, IoT, medical) -- 🧩 Plugin-based architectures (CMS, CLI, extensions) -- 🧪 CI/CD pipelines with strict integration rules -- 🧱 Frameworks with third-party extension points -- 📦 Package maintainers enforcing integration rules - -🧠 How It Works ---------------- - -1. Define your contract in `.yml` or Python. -2. ImportSpy loads your module and introspects its importer. -3. Runtime environment + structure are matched against the contract. -4. If mismatch → import blocked. - If valid → import continues safely. - -🎯 Tech Stack -------------- - -- ✅ Pydantic 2.x – contract validation engine -- ✅ Typer – CLI interface -- ✅ ruamel.yaml – YAML parsing -- ✅ inspect + sys – runtime context introspection -- ✅ Poetry – package + dependency management -- ✅ Sphinx + ReadTheDocs – full docs and architecture reference - -📘 Documentation ----------------- - -- 🔗 Full Docs → https://importspy.readthedocs.io/ -- 🧱 Model Overview → https://importspy.readthedocs.io/en/latest/advanced/architecture_index.html -- 🧪 Use Cases → https://importspy.readthedocs.io/en/latest/overview/use_cases_index.html - -🌟 Contribute & Support ------------------------ - -- ⭐ Star → https://github.com/atellaluca/ImportSpy -- 🛠 Contribute via issues or PRs -- 💖 Sponsor → https://github.com/sponsors/atellaluca - -🔥 **Let your modules enforce their own rules.** -Start importing with structure. - -📜 License ----------- - -MIT © 2024 – Luca Atella - -.. image:: ./assets/importspy-logo.png - :alt: ImportSpy Logo - :width: 100px - :align: center - -**ImportSpy** is an open-source project maintained with ❤️ by `Luca Atella `_. diff --git a/assets/importspy-spy-model-architecture.png b/assets/importspy-spy-model-architecture.png deleted file mode 100644 index f40bb25..0000000 Binary files a/assets/importspy-spy-model-architecture.png and /dev/null differ diff --git a/diagrams/importspy-spy-model-architecture.drawio b/diagrams/importspy-spy-model-architecture.drawio new file mode 100644 index 0000000..e33f784 --- /dev/null +++ b/diagrams/importspy-spy-model-architecture.drawio @@ -0,0 +1,428 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index d0c3cbf..0000000 --- a/docs/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line, and also -# from the environment for the first two. -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = source -BUILDDIR = build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/advanced/spymodel.md b/docs/advanced/spymodel.md new file mode 100644 index 0000000..b6ecd13 --- /dev/null +++ b/docs/advanced/spymodel.md @@ -0,0 +1,85 @@ +# SpyModel Architecture + +The **SpyModel** is the central object in ImportSpy's validation model. +It represents both the structural definition of a Python module and the contextual constraints under which the module can be imported and executed. + +This hybrid role makes it a bridge between the static world of code structure and the dynamic world of runtime validation. + +--- + +## Overview + +ImportSpy introduces the concept of **import contracts**: structured `.yml` files that describe how a Python module is expected to behave, what it provides, and where it is valid. + +The SpyModel is the object-oriented representation of these contracts. It is composed of two main aspects: + +- **Structural metadata**: variables, functions, classes, attributes, and type annotations +- **Deployment constraints**: supported architectures, operating systems, interpreters, Python versions, environment variables, and secrets + +This combination allows precise control over when and where a module can be imported. + +--- + +## Structural Layer + +The structural part of a SpyModel describes the internal shape of the Python module: + +- **filename**: the name of the `.py` file +- **variables**: global variables defined at module level, including optional type and value +- **functions**: standalone functions, with argument names, types, and return annotations +- **classes**: including attributes (class-level and instance-level), methods, and inheritance + +Each of these elements is validated against what the contract expects. +This ensures that a module importing another can rely on a well-defined structure. + +--- + +## Deployment Layer + +The second part of the model defines where and under which conditions the module is considered valid. + +This is handled by the `deployments` field, a list of accepted runtime configurations. +Each deployment includes: + +- **arch**: CPU architecture (e.g., `x86_64`, `ARM`) +- **systems**: operating systems supported (`linux`, `windows`, `macos`) + - Each system includes: + - **environment**: + - **variables**: required environment variables and their expected values + - **secrets**: variable names that must exist, without checking their value + - **pythons**: accepted Python interpreters and versions, each with: + - a list of expected **modules**, including their own structure + +This allows defining highly specific constraints such as: + +- “This plugin can only be used on Linux x86_64 with Python 3.12.9” +- “This module requires `MY_SECRET_KEY` to be present in the environment” + +--- + +## Schema Overview + +The following UML diagram summarizes the structure of the SpyModel and its relationships: + +![SpyModel UML](../assets/importspy-spy-model-architecture.png) + +Each node represents a data structure used during validation. +The model is hierarchical: from deployments down to classes and attributes, every element is traceable and verifiable. + +--- + +## Design Rationale + +The SpyModel is designed to be: + +- **Declarative**: the contract is expressed in data, not code logic +- **Versionable**: stored as YAML, the contract can be committed to Git +- **Composable**: it supports multiple deployment targets and alternative environments +- **Predictable**: ensures that structural mismatches are detected early + +By separating structure from logic, ImportSpy enables a contract-driven development workflow. +This is particularly useful in plugin frameworks, controlled environments, or distributed systems where consistency across modules and contexts is critical. + +--- + +ImportSpy treats contracts as **first-class citizens**. The SpyModel is the embodiment of this philosophy: a transparent, enforceable, and structured declaration of what a module requires and provides. diff --git a/docs/advanced/violations.md b/docs/advanced/violations.md new file mode 100644 index 0000000..badc68a --- /dev/null +++ b/docs/advanced/violations.md @@ -0,0 +1,165 @@ +# Violation System + +The Violation System in **ImportSpy** is responsible for surfacing clear, contextual, and actionable errors when a Python module fails to comply with its declared **import contract**. + +Rather than raising generic Python exceptions, this subsystem transforms validation failures into precise, domain-specific diagnostics that are structured, explainable, and safe to expose in both development and production contexts. + +--- + +## Purpose + +In modular and plugin-based systems, structural mismatches and runtime incompatibilities can lead to subtle bugs, hard crashes, or silent failures. + +The Violation System serves as a **contract enforcement layer** that: + +- Captures detailed context about the failure (module, scope, variable, system) +- Distinguishes between **missing**, **mismatched**, and **invalid** values +- Produces **human-readable** error messages tailored to developers and CI pipelines +- Structures error reporting consistently across validation layers (module, runtime, environment, etc.) + +--- + +## Core Concepts + +### 1. `ContractViolation` Interface + +An abstract base that defines the required interface for all contract violations. It ensures consistency across the different scopes (variables, functions, systems, etc.). + +```python +class ContractViolation(ABC): + @property + @abstractmethod + def context(self) -> str + + @abstractmethod + def label(self, spec: str) -> str + + @abstractmethod + def missing_error_handler(self, spec: str) -> str + + @abstractmethod + def mismatch_error_handler(self, spec: str) -> str + + @abstractmethod + def invalid_error_handler(self, spec: str) -> str +``` + +### 2. `BaseContractViolation` + +A reusable abstract class that implements common logic for generating violation messages (e.g. missing values, mismatches, invalid values) across all scopes. +It uses templates from `Errors` to construct consistent, high-fidelity diagnostics. + +```text +Example output: +[Module Validation Error]: Variable 'API_KEY' is missing. - Declare it in the expected module. +``` + +--- + +## Specific Violation Classes + +Each domain (Variable, Function, Runtime, etc.) has its own implementation: + +| Class | Purpose | +|--------------------------|----------------------------------------------------------| +| `VariableContractViolation` | Handles variable mismatches or missing values | +| `FunctionContractViolation` | Detects function mismatches and signature issues | +| `ModuleContractViolation` | Validates filename and version consistency | +| `RuntimeContractViolation` | Validates system architectures (`x86_64`, `arm64`, etc.)| +| `SystemContractViolation` | Checks operating system and environment variable match | +| `PythonContractViolation` | Validates interpreter and Python version compatibility | + +Each one specializes the `.label()` method to return error-specific context (e.g., "Environment variable `MY_SECRET` is missing in production runtime"). + +--- + +## Dynamic Payload Injection via `Bundle` + +Each violation operates with a shared mutable dictionary-like object called a **`Bundle`**: + +```python +@dataclass +class Bundle(MutableMapping): + state: Optional[dict[str, Any]] = field(default_factory=dict) +``` + +It serves two purposes: + +- Collect **contextual information** at validation time (e.g., variable name, expected type) +- Dynamically populate **templated error messages** using the `Errors` constant map + +This design allows error messages to adapt to the specific failure, with no hardcoding. + +--- + +## Error Categorization + +ImportSpy distinguishes between three primary error types: + +| Category | Description | +|--------------|--------------------------------------------------------------------------| +| `MISSING` | A required entity (class, method, variable) was not found | +| `MISMATCH` | An entity exists but its value, annotation, or structure differs | +| `INVALID` | A value exists but does not belong to the allowed set (e.g., OS types) | + +The `Errors` constant defines templates for all categories, per context: + +```yaml +"MISSING": + "module": + "template": "Expected module '{label}' is missing" + "solution": "Add the module to your import path" +``` + +--- + +## Engineering Highlights + +- **Encapsulation**: All formatting, message construction, and error typing is abstracted out of validators. +- **Separation of Concerns**: Validators focus only on logic, while violations handle messaging. +- **Templated Errors**: All violations draw from `Errors`, ensuring uniformity and easier localization or branding. +- **Composable Context**: The `Bundle` allows rich diagnostics without tight coupling between layers. + +--- + +## Example Usage + +```python +from importspy.violation_systems import VariableContractViolation + +bundle = Bundle() +bundle["expected_variable"] = "API_KEY" + +raise ValueError( + VariableContractViolation( + scope="module", + context="MODULE_CONTEXT", + bundle=bundle + ).missing_error_handler("entity") +) +``` + +Produces: + +```text +[Module Validation]: Variable 'API_KEY' is missing - Declare it in the target module. +``` + +--- + +## Future Extensions + +The Violation System is designed to be: + +- Extensible with new scopes (`Decorators`, `ReturnTypes`, `Dependencies`) +- Pluggable with i18n/l10n systems +- Renderable in **structured JSON** for machine processing in DevOps pipelines + +--- + +## Summary + +The Violation System is not just error handling — it is ImportSpy’s **engine of clarity**. +It converts abstract contract mismatches into structured, interpretable diagnostics that empower developers to catch errors before runtime. + +By modeling validation feedback as first-class entities, ImportSpy enables precise governance across modular codebases. diff --git a/docs/api-reference.md b/docs/api-reference.md new file mode 100644 index 0000000..c0ccbf1 --- /dev/null +++ b/docs/api-reference.md @@ -0,0 +1,119 @@ +# API Reference + +This section documents the public Python API exposed by ImportSpy, organized by module. +All components listed here are available when you install the package. + +## `importspy.s` + +::: importspy.s + handler: python + options: + show_source: false + +--- + +## `importspy.models` + +::: importspy.models + handler: python + options: + show_source: false + +--- + +## `importspy.validators` + +::: importspy.validators + handler: python + options: + show_source: false + +--- + +## `importspy.violation_systems` + +::: importspy.violation_systems + handler: python + options: + show_source: false + +--- + +## `importspy.persistences` + +::: importspy.persistences + handler: python + options: + show_source: false + +--- + +## `importspy.cli` + +::: importspy.cli + handler: python + options: + show_source: false + +--- + +## `importspy.constants` + +::: importspy.constants + handler: python + options: + show_source: false + +--- + +## `importspy.config` + +::: importspy.config + handler: python + options: + show_source: false + +--- + +## `importspy.log_manager` + +::: importspy.log_manager + handler: python + options: + show_source: false + +--- + +## `importspy.utilities.module_util` + +::: importspy.utilities.module_util + handler: python + options: + show_source: false + +--- + +## `importspy.utilities.runtime_util` + +::: importspy.utilities.runtime_util + handler: python + options: + show_source: false + +--- + +## `importspy.utilities.system_util` + +::: importspy.utilities.system_util + handler: python + options: + show_source: false + +--- + +## `importspy.utilities.python_util` + +::: importspy.utilities.python_util + handler: python + options: + show_source: false diff --git a/docs/assets/apple-touch-icon.png b/docs/assets/apple-touch-icon.png new file mode 100644 index 0000000..98a6468 Binary files /dev/null and b/docs/assets/apple-touch-icon.png differ diff --git a/docs/assets/favicon.ico b/docs/assets/favicon.ico new file mode 100644 index 0000000..f09007e Binary files /dev/null and b/docs/assets/favicon.ico differ diff --git a/assets/importspy-banner.png b/docs/assets/importspy-banner.png similarity index 100% rename from assets/importspy-banner.png rename to docs/assets/importspy-banner.png diff --git a/assets/importspy-embedded-mode.png b/docs/assets/importspy-embedded-mode.png similarity index 100% rename from assets/importspy-embedded-mode.png rename to docs/assets/importspy-embedded-mode.png diff --git a/assets/importspy-logo.png b/docs/assets/importspy-logo.png similarity index 100% rename from assets/importspy-logo.png rename to docs/assets/importspy-logo.png diff --git a/docs/assets/importspy-spy-model-architecture.png b/docs/assets/importspy-spy-model-architecture.png new file mode 100644 index 0000000..35b8a46 Binary files /dev/null and b/docs/assets/importspy-spy-model-architecture.png differ diff --git a/assets/importspy-works.png b/docs/assets/importspy-works.png similarity index 100% rename from assets/importspy-works.png rename to docs/assets/importspy-works.png diff --git a/docs/contracts/examples.md b/docs/contracts/examples.md new file mode 100644 index 0000000..393dede --- /dev/null +++ b/docs/contracts/examples.md @@ -0,0 +1,168 @@ +# Contract Examples + +This page provides complete examples of import contracts (`.yml` files) supported by ImportSpy. + +Each example demonstrates how to declare structural expectations and runtime constraints for a Python module using the SpyModel format. + +--- + +## Basic Module Contract + +This example defines a simple module called `plugin.py` with one variable, one class, and one method. + +```yaml +filename: plugin.py +variables: + - name: mode + value: production + annotation: str +classes: + - name: Plugin + methods: + - name: run + arguments: + - name: self + return_annotation: None +deployments: + - arch: x86_64 + systems: + - os: linux + pythons: + - version: 3.11 + interpreter: CPython +``` + +--- + +## Function With Typed Arguments + +This contract describes a module where the `analyze` function requires two arguments with specific types. + +```yaml +filename: analyzer.py +functions: + - name: analyze + arguments: + - name: self + - name: data + annotation: list[str] + - name: verbose + annotation: bool + return_annotation: dict +deployments: + - arch: x86_64 + systems: + - os: linux + pythons: + - version: 3.12 + interpreter: CPython +``` + +--- + +## Class With Attributes and Methods + +This contract defines a module that exposes a `TaskManager` class with attributes and a method. + +```yaml +filename: manager.py +classes: + - name: TaskManager + attributes: + - name: tasks + annotation: list[str] + - name: state + annotation: str + methods: + - name: reset + arguments: + - name: self + return_annotation: None +deployments: + - arch: arm64 + systems: + - os: darwin + environment: + variables: + - name: MODE + value: development + annotation: str + pythons: + - version: 3.11 + interpreter: CPython +``` + +--- + +## Runtime-Only Validation + +This contract enforces only environmental and interpreter constraints — no structural validation. + +```yaml +deployments: + - arch: x86_64 + systems: + - os: linux + pythons: + - version: 3.10 + interpreter: CPython +``` + +--- + +## Multiple Deployments + +If your module supports multiple platforms, you can define multiple `deployments`. + +```yaml +filename: plugin.py +classes: + - name: Plugin + methods: + - name: run + arguments: + - name: self +deployments: + - arch: x86_64 + systems: + - os: linux + pythons: + - version: 3.11 + interpreter: CPython + - arch: arm64 + systems: + - os: darwin + pythons: + - version: 3.12 + interpreter: CPython +``` + +--- + +## Using Environment Variables + +This example enforces that an environment variable is set and has a given type. + +```yaml +filename: checker.py +deployments: + - arch: x86_64 + systems: + - os: linux + environment: + variables: + - name: DEBUG + value: "true" + annotation: str + pythons: + - version: 3.10 + interpreter: CPython +``` + +--- + +## Related Topics + +- [Contract Syntax](syntax.md) +- [Violation System](../advanced/violations.md) +- [SpyModel Architecture](../advanced/spymodel.md) diff --git a/docs/contracts/syntax.md b/docs/contracts/syntax.md new file mode 100644 index 0000000..ce65c89 --- /dev/null +++ b/docs/contracts/syntax.md @@ -0,0 +1,214 @@ +# Contract Syntax + +ImportSpy contracts are written in YAML and describe the **structure and runtime expectations** of a Python module. These contracts can be embedded or validated externally (e.g., in CI/CD), and act as **import-time filters** for enforcing compatibility and intent. + +A contract defines: +- What variables, classes, and functions a module must expose +- What runtime conditions are required (OS, architecture, Python version, etc.) +- How strict or flexible the structure must be + +This document explains the full syntax supported in `.yml` contract files. + +--- + +## Top-Level Structure + +Every `.yml` contract is structured as follows: + +```yaml +filename: plugin.py +version: "1.2.3" + +variables: + - name: MODE + value: production + annotation: str + +functions: + - name: initialize + arguments: + - name: self + - name: config + annotation: dict + return_annotation: None + +classes: + - name: Plugin + attributes: + - name: settings + value: default + annotation: dict + type: instance + methods: + - name: run + arguments: + - name: self + return_annotation: None + superclasses: + - BasePlugin + +deployments: + - arch: x86_64 + systems: + - os: linux + environment: + variables: + - name: IMPORTSPY_ENABLED + value: true + annotation: bool + pythons: + - version: "3.11" + interpreter: CPython + modules: + - filename: plugin.py + version: "1.2.3" +``` + +--- + +## Fields Explained + +### `filename` +The name of the target Python file or module this contract applies to. + +### `version` +Optional string that defines the expected version of the module. Can be used to pin specific builds or releases. + +--- + +## `variables` + +Declares **global or module-level variables** the importer must provide. + +```yaml +variables: + - name: DEBUG + value: true + annotation: bool +``` + +Each variable entry supports: +- `name` (**required**): Variable name +- `value` (optional): Expected value +- `annotation` (optional): Expected type annotation as string (e.g., `"str"`, `"dict"`) + +--- + +## `functions` + +Specifies required functions in the importing module. + +```yaml +functions: + - name: load_config + arguments: + - name: path + annotation: str + return_annotation: dict +``` + +Each function supports: +- `name`: The function's name +- `arguments`: List of required arguments (each with optional `annotation`) +- `return_annotation`: Expected return type (as string) + +--- + +## `classes` + +Defines required class structures. + +```yaml +classes: + - name: Plugin + attributes: + - name: config + annotation: dict + value: {} + type: instance + methods: + - name: execute + arguments: + - name: self + superclasses: + - BasePlugin +``` + +A class may include: +- `name`: Class name +- `attributes`: A list of attributes exposed by the class + - `type`: Can be `"instance"` or `"class"` to indicate attribute level +- `methods`: Required method declarations (same format as top-level `functions`) +- `superclasses`: Optional list of superclass names expected + +> Attributes are matched on name, annotation, and (if provided) value. + +--- + +## `deployments` + +This section defines **runtime constraints** in which the import is valid. It allows validation based on: + +- Architecture +- Operating system +- Environment variables +- Python version and interpreter +- Declared modules + +```yaml +deployments: + - arch: x86_64 + systems: + - os: linux + environment: + variables: + - name: MODE + value: prod + annotation: str + pythons: + - version: "3.10" + interpreter: CPython + modules: + - filename: plugin.py +``` + +### Deployment fields: + +| Field | Description | +|--------------|---------------------------------------------| +| `arch` | CPU architecture (e.g. `x86_64`, `arm64`) | +| `systems.os` | Operating system (e.g. `linux`, `windows`) | +| `environment.variables` | Required runtime env variables | +| `pythons.version` | Required Python version (string) | +| `pythons.interpreter` | Interpreter (e.g., `CPython`) | +| `modules` | Specific modules to check (with filename/version) | + +> Note: all conditions are **AND-combined** within a deployment block. + +--- + +## Matching Rules + +ImportSpy uses a strict structural validator. Here are some notes: + +- Variables, functions, and methods are matched **by name**. +- Annotations are matched **as plain strings** – no semantic typing or runtime evaluation. +- If an annotation is omitted, it is **not enforced**. +- Superclasses are checked only **by name**, not by inheritance tree resolution. + +--- + +## Best Practices + +- Use consistent annotations: `"str"`, `"dict"`, `"list"`, etc. +- Prefer matching exact function signatures for critical plugins +- Define environment constraints only when needed (e.g., `IMPORTSPY_MODE=prod`) +- Use `modules.filename` to enforce versioning in multi-plugin systems + +--- + +## Related Sections + +- [Contract Examples](examples.md) +- [SpyModel Architecture](../advanced/spymodel.md) +- [Contract Violations](../errors/contract-violations.md) diff --git a/docs/errors/contract-violations.md b/docs/errors/contract-violations.md new file mode 100644 index 0000000..3d6c308 --- /dev/null +++ b/docs/errors/contract-violations.md @@ -0,0 +1,85 @@ +# Contract Violations + +When an import contract is not satisfied, **ImportSpy** blocks the import and raises a detailed error message. +These violations are central to the library's purpose: enforcing predictable, secure, and valid module usage across Python runtimes. + +## How Violations Work + +Every time a module is imported using ImportSpy (either in **embedded** or **CLI** mode), the system performs deep introspection and validation checks. + +If something does not match the declared contract (`.yml`), ImportSpy will: + +1. **Capture the context** (e.g., `MODULE`, `CLASS`, `RUNTIME`, etc.) +2. **Identify the type** of error: + - `missing`: required element is absent + - `mismatch`: expected vs actual values differ + - `invalid`: unexpected or disallowed value found +3. **Generate a structured error message** including: + - a human-readable message + - exact label of the failing entity + - possible solutions or corrective actions + +These violations are raised as `ValueError`, but contain detailed introspection metadata under the hood. + +--- + +## Error Categories + +ImportSpy organizes violations into **logical layers**, based on what is being validated: + +| Layer | Validator Class | Violation Raised | +|-------------------|--------------------------|------------------------------------------| +| Architecture/OS | `RuntimeValidator` | `RuntimeContractViolation` | +| OS / Environment | `SystemValidator` | `SystemContractViolation` | +| Python Interpreter| `PythonValidator` | `PythonContractViolation` | +| Module File | `ModuleValidator` | `ModuleContractViolation` | +| Class Structure | `ClassValidator` | `ModuleContractViolation (CLASS_CONTEXT)` | +| Functions | `FunctionValidator` | `FunctionContractViolation` | +| Variables / Args | `VariableValidator` | `VariableContractViolation` | + +Each of these violations inherits from `BaseContractViolation`, which provides: +- A consistent interface for labeling (`.label()`) +- Templated messages for each category +- A `Bundle` object used to inject dynamic context into the error + +--- + +## Error Message Anatomy + +A full ImportSpy violation message looks like this: + +``` +[MODULE] Expected variable `timeout: int` not found in `my_module.py` +→ Please add the variable or update your contract. +``` + +Each message consists of: +- `[CONTEXT]`: tells where the error occurred +- **Label**: dynamically generated from the contract structure +- **Expected/Actual**: shown for mismatch/invalid errors +- **Solution**: human-readable advice from the YAML spec + +--- + +## Debugging Tips + +- Use `-l DEBUG` when invoking ImportSpy via CLI to see exact comparison steps. +- Violations are deterministic and reproducible. If one fails in CI, it will fail locally too. +- You can inspect the violation context by capturing the `ValueError` and logging its message. + +--- + +## 📋 Contract Violation Table + +Below is a comprehensive list of all possible error messages emitted by ImportSpy: + +--8<-- "errors/error_table.md" + +--- + +## Related Topics + +- [Contract Syntax](../contracts/syntax.md) +- [Embedded Mode](../modes/embedded.md) +- [CLI Mode](../modes/cli.md) +- [SpyModel Architecture](../advanced/spymodel.md) diff --git a/docs/errors/error-table.md b/docs/errors/error-table.md new file mode 100644 index 0000000..0760d81 --- /dev/null +++ b/docs/errors/error-table.md @@ -0,0 +1,10 @@ +| Category | Context | Error Message | +|------------|---------------|---------------| +| `missing` | `runtime` | The runtime `CPython 3.12` is declared but missing. Ensure it is properly defined and implemented. | +| | `environment` | The environment variable `DEBUG` is declared but missing. Ensure it is properly defined and implemented. | +| | `module` | The variable `plugin_name` in module `extension.py` is declared but missing. Ensure it is properly defined and implemented. | +| | `class` | The method `run` in class `Plugin` is declared but missing. Ensure it is properly defined and implemented. | +| `mismatch` | `runtime` | The runtime `CPython 3.12` does not match the expected value. Expected: `CPython 3.11`, Found: `CPython 3.12`. Check the value and update the contract or implementation accordingly. | +| | `environment` | The environment variable `LOG_LEVEL` does not match the expected value. Expected: `'INFO'`, Found: `'DEBUG'`. Check the value and update the contract or implementation accordingly. | +| | `class` | The class attribute `engine` in class `Extension` does not match the expected value. Expected: `'docker'`, Found: `'podman'`. Check the value and update the contract or implementation accordingly. | +| `invalid` | `class` | The argument `msg` of method `send` has an invalid value. Allowed values: `[str, None]`, Found: `42`. Update the value to one of the allowed options. | diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..44540e4 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,139 @@ +# ImportSpy + +**Context-aware import validation for Python modules** + +ImportSpy is an open-source Python library that introduces a robust mechanism to control and validate how and when modules are imported. At its core, it relies on versioned, declarative **import contracts** — written in YAML — which describe what a module expects from its execution context and its importer. + +It brings **modularity**, **predictability**, and **security** to Python ecosystems. + +--- + +## What is an Import Contract? + +An import contract is a `.yml` file that defines: + +- The expected **structure** of a module: functions, classes, arguments, annotations, variables +- The allowed **environments**: OS, architecture, Python version, interpreter +- Optional conditions on runtime **environment variables** or **superclasses** + +If these conditions are not met, ImportSpy can stop the import and raise a detailed, structured error — before any runtime failure can occur. + +--- + +## Key Features + +- YAML-based **import contracts** +- Embedded and CLI-based **validation** +- Structural validation of **variables**, **functions**, **classes** +- Runtime checks for **OS**, **architecture**, **Python version**, **interpreter** +- Contract-driven plugin validation for **secure extensibility** +- Clear, explainable **error reporting** on mismatch, missing, or invalid usage +- Fully integrable in **CI/CD pipelines** + +--- + +## Use Cases + +ImportSpy is built for teams that need: + +- **Plugin-based architectures** with strict interface enforcement +- **Runtime protection** against incompatible environments +- Early validation in **DevSecOps** or **regulatory** pipelines +- Defensive boundaries between **internal components** +- **Automated structure verification** during deployment + +--- + +## Example: Embedded Mode + +```python +from importspy import Spy + +caller = Spy().importspy(filepath="contracts/spymodel.yml") +caller.MyPlugin().run() +``` + +--- + +## Example: CLI Mode + +```bash +importspy src/mymodule.py -s contracts/spymodel.yml --log-level DEBUG +``` + +--- + +## Project Structure + +ImportSpy is built around 3 key components: + +- `SpyModel`: represents the structural and runtime definition of a module +- `Spy`: the validation engine that compares real vs expected modules +- `Violation System`: formal system for raising errors with human-readable messages + +--- + +## Documentation Overview + +### 👣 Get Started + +- [Quickstart](intro/quickstart.md) +- [Install](intro/install.md) +- [Overview](intro/overview.md) + +### ⚙️ Modes of Operation + +- [Embedded Mode](modes/embedded.md) +- [CLI Mode](modes/cli.md) + +### 📄 Import Contracts + +- [Contract Syntax](contracts/syntax.md) +- [SpyModel Specification](advanced/spymodel.md) + +### 🧠 Validation Engine + +- [Violation System](advanced/violations.md) +- [Contract Violations](errors/contract-violations.md) +- [Error Table](errors/error-table.md) + +### 📦 Use Cases + +- [Plugin-based Architectures](use_cases/index.md) + +### 📘 API Reference + +- [API Docs](api-reference.md) + +--- + +## Architecture Diagram + +![SpyModel UML](assets/importspy-spy-model-architecture.png) + +--- + +## Why ImportSpy? + +Python’s import system is powerful, but not context-aware. ImportSpy solves this by adding a **layer of structural governance** and **runtime filtering**. + +This makes it ideal for: + +- Plugin systems +- Isolated runtimes +- Package compliance +- Security-aware applications +- CI enforcement of expected module interfaces + +--- + +## Sponsorship & Community + +If ImportSpy is useful in your infrastructure, help us grow by: + +- [Starring the project on GitHub](https://github.com/your-org/importspy) +- [Becoming a GitHub Sponsor](https://github.com/sponsors/your-org) + +--- + +> ImportSpy is more than a validator — it's a contract of trust between Python modules. diff --git a/docs/intro/install.md b/docs/intro/install.md new file mode 100644 index 0000000..0b9e24a --- /dev/null +++ b/docs/intro/install.md @@ -0,0 +1,46 @@ +# Installation + +You can install ImportSpy directly from [PyPI](https://pypi.org/project/importspy/) using `pip`. + +--- + +## Basic installation + +```bash +pip install importspy +``` + +This installs both the core runtime and the command-line interface (CLI), allowing you to use ImportSpy in both **Embedded Mode** and **CLI Mode**. + +--- + +## Minimum requirements + +- **Python 3.8 or higher** +- Supported operating systems: + - Linux + - macOS + - Windows +- Compatible with CPython and alternative interpreters (e.g. IronPython), if declared in the contract + +--- + +## Updating ImportSpy + +To upgrade to the latest version: + +```bash +pip install --upgrade importspy +``` + +--- + +## Verify installation + +To confirm that ImportSpy is correctly installed and the CLI is available: + +```bash +importspy --help +``` + +You should see the full list of command-line options and usage instructions. diff --git a/docs/intro/overview.md b/docs/intro/overview.md new file mode 100644 index 0000000..a9f8892 --- /dev/null +++ b/docs/intro/overview.md @@ -0,0 +1,74 @@ +# What is ImportSpy? + +**ImportSpy** is a Python library that brings context awareness to the most fragile point in the lifecycle of a module: its import. + +It lets developers declare explicit import contracts — versionable `.yml` files that describe under which conditions a module can be imported. These contracts are validated at runtime, ensuring that the importing environment (and optionally the importing module) matches the declared requirements. + +If the conditions are not satisfied, the import fails immediately, with a detailed and structured error message. + +--- + +## Why use ImportSpy? + +Python offers no built-in mechanism to control how and where a module can be imported. In modular systems, plugin frameworks, and regulated environments, this leads to: + +- Unexpected runtime errors +- Hard-to-diagnose misconfigurations +- Fragile CI/CD workflows + +ImportSpy solves this by introducing **import-time validation** based on: + +- Runtime environment (OS, CPU, Python version, interpreter) +- Required environment variables and secret presence +- Structural expectations of the importing module (classes, methods, types…) + +This results in safer, more predictable imports — whether you're building a plugin system, enforcing architectural rules, or protecting critical components. + +--- + +## How does it work? + +ImportSpy uses a **declarative import contract** — a `.yml` file written by the developer — to define expected conditions. + +At runtime, this contract is parsed into a structured Python object called a **SpyModel**, which is used internally for validation. + +Depending on the operation mode (Embedded or CLI), ImportSpy: + +- Validates the runtime and system environment +- Optionally inspects the module that is importing the protected one +- Enforces declared constraints on structure, type annotations, variable values, and more + +If all constraints are respected, the import succeeds. +If not, a descriptive error is raised (e.g. `ValueError`, `ImportSpyViolation`). + +--- + +## Key features + +- ✅ Declarative, versionable `.yml` import contracts +- 🧠 Runtime validation of OS, CPU architecture, Python version/interpreter +- 🔍 Structural checks on the importing module (classes, methods, attributes, types) +- 🔐 Validation of required environment variables and secrets (presence only) +- 🚀 Dual operation modes: Embedded Mode and CLI Mode +- 🧪 Full integration with CI/CD pipelines +- 📋 Structured, human-readable error reports with actionable messages + +--- + +## Who is it for? + +ImportSpy is designed for: + +- Plugin frameworks that enforce structural compliance +- Systems with strict version, runtime or security constraints +- Teams building modular Python applications +- Projects that need to validate import-time compatibility +- Open-source maintainers looking to define and enforce import boundaries + +--- + +## What’s next? + +- [→ Install ImportSpy](install.md) +- [→ Try a minimal working example](quickstart.md) +- [→ Learn about the operation modes](../modes/embedded.md) diff --git a/docs/intro/quickstart.md b/docs/intro/quickstart.md new file mode 100644 index 0000000..93c984f --- /dev/null +++ b/docs/intro/quickstart.md @@ -0,0 +1,98 @@ +# Quickstart + +This quickstart shows how to use **ImportSpy in Embedded Mode** to protect a Python module from being imported in an invalid context. + +--- + +## Step 1 — Install ImportSpy + +If you haven’t already: + +```bash +pip install importspy +``` + +--- + +## Step 2 — Create a contract (`spymodel.yml`) + +This file defines the conditions under which your module can be imported. +For example, it can require specific Python versions, operating systems, or structure in the calling module. + +```yaml +filename: plugin.py +classes: + - name: Plugin + methods: + - name: run + arguments: + - name: self + return_annotation: +deployments: + - arch: x86_64 + systems: + - os: linux + pythons: + - version: 3.12 + interpreter: CPython +``` + +Save this file as `spymodel.yml`. + +--- + +## Step 3 — Protect your module + +Here’s how to use ImportSpy inside the module you want to protect (e.g. `plugin.py`): + +```python +# plugin.py +from importspy import Spy + +caller = Spy().importspy(filepath="spymodel.yml") + +# Call something from the importer (for example) +caller.MyPlugin().run() +``` + +This checks the current environment and the module that is importing `plugin.py`. +If it doesn’t match the contract, ImportSpy raises an error and blocks the import. + +--- + +## Step 4 — Create an importer + +Write a simple module that tries to import `plugin.py`. + +```python +# main.py +class MyPlugin: + def run(self): + print("Plugin running") + +import plugin +``` + +--- + +## Step 5 — Run it + +If the environment and structure of `main.py` match the contract, the import will succeed: + +```bash +python main.py +``` + +Otherwise, you'll get a clear and structured error like: + +``` +[Structure Violation] Missing required class 'Plugin' in caller module. +``` + +--- + +## Next steps + +- Learn more about [Embedded Mode](../modes/embedded.md) +- Explore [CLI Mode](../modes/cli.md) for validating modules from the outside +- Dive into [contract syntax](../contracts/syntax.md) to write more advanced rules diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index 747ffb7..0000000 --- a/docs/make.bat +++ /dev/null @@ -1,35 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=source -set BUILDDIR=build - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.https://www.sphinx-doc.org/ - exit /b 1 -) - -if "%1" == "" goto help - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% - -:end -popd diff --git a/docs/modes/cli.md b/docs/modes/cli.md new file mode 100644 index 0000000..25515e7 --- /dev/null +++ b/docs/modes/cli.md @@ -0,0 +1,276 @@ +# CLI Mode + +ImportSpy can also be used **outside of runtime** to validate a Python module against a contract from the command line. + +This is useful in **CI/CD pipelines**, **pre-commit hooks**, or manual validations — whenever you want to enforce import contracts without modifying the target module. + +--- + +## How it works + +In CLI Mode, you invoke the `importspy` command and provide: + +- The path to the **module** to validate +- The path to the **YAML contract** +- (Optional) a log level for output verbosity + +ImportSpy loads the module dynamically, builds its SpyModel, and compares it against the `.yml` contract. + +If the module is non-compliant, the command will: + +- Exit with a non-zero status +- Print a structured error explaining the violation + +--- + +## Basic usage + +```bash +importspy extensions.py -s spymodel.yml -l WARNING +``` + +### CLI options + ++-----------------------------------------------------------------------------+ +| Flag | Description | +|--------------------|--------------------------------------------------------| +| `-s, --spymodel` | Path to the import contract `.yml` file | +| `-l, --log-level` | Logging verbosity: `DEBUG`, `INFO`, `WARNING`, `ERROR` | +| `-v, --version` | Show ImportSpy version | +| `--help` | Show help message and exit | ++-----------------------------------------------------------------------------+ +--- + +## Example project + +Let’s look at a full CLI-mode validation example. + +### Project structure + +``` +pipeline_validation/ +├── extensions.py +└── spymodel.yml +``` + +### 📄 Source files + +=== "extensions.py" + +```python +--8<-- "examples/plugin_based_architecture/pipeline_validation/extensions.py" +``` + +=== "spymodel.yml" + +```yaml +--8<-- "examples/plugin_based_architecture/pipeline_validation/spymodel.yml" +``` + +### 🔍 Run validation + +```bash +cd examples/plugin_based_architecture/pipeline_validation +importspy extensions.py -s spymodel.yml -l WARNING +``` + +If the module matches the contract, the command exits silently with `0`. +If it doesn't, you’ll see a structured error like: + +``` +[Structure Violation] Missing required method 'get_bar' in class 'Foo'. +``` + +--- + +## When to use CLI Mode + +!!! tip "Use CLI Mode for automation" + CLI Mode is ideal when you want to: + + - Validate modules **without changing their code** + - Integrate checks in **CI/CD pipelines** + - Enforce contracts in **external packages** + - Run **batch validations** over multiple files + +--- + +# Import Contract Syntax + +An ImportSpy contract is a YAML file that describes: + +- The **structure** expected in the calling module (classes, methods, variables…) +- The **runtime and system environment** where the module is allowed to run +- The required **environment variables** and optional secrets + +This contract is parsed into a `SpyModel`, which is then compared against the actual runtime and importing module. + +--- + +## ✅ Overview + +Here’s a minimal but complete contract: + +```yaml +filename: extension.py +variables: + - name: engine + value: docker +classes: + - name: Plugin + methods: + - name: run + arguments: + - name: self +deployments: + - arch: x86_64 + systems: + - os: linux + pythons: + - version: 3.12 + interpreter: CPython +``` + +--- + +## 📄 filename + +```yaml +filename: extension.py +``` + +- Optional. +- Declares the filename of the module being validated. +- Used for reference and filtering in multi-module declarations. + +--- + +## 🔣 variables + +```yaml +variables: + - name: engine + value: docker +``` + +- Declares top-level variables that must be present in the importing module. +- Supports optional `annotation` (type hint). + +```yaml + - name: debug + annotation: bool + value: true +``` + +--- + +## 🧠 functions + +```yaml +functions: + - name: run + arguments: + - name: self + - name: config + annotation: dict + return_annotation: bool +``` + +- Declares standalone functions expected in the importing module. +- Use `arguments` and `return_annotation` for stricter typing. + +--- + +## 🧱 classes + +```yaml +classes: + - name: Plugin + attributes: + - type: class + name: plugin_name + value: my_plugin + methods: + - name: run + arguments: + - name: self + superclasses: + - name: BasePlugin +``` + +Each class can declare: + +- `attributes`: divided by `type` (`class` or `instance`) +- `methods`: each with `arguments` and optional `return_annotation` +- `superclasses`: flat list of required superclass names + +--- + +## 🧭 deployments + +This section defines where the module is allowed to run. + +```yaml +deployments: + - arch: x86_64 + systems: + - os: linux + pythons: + - version: 3.12.9 + interpreter: CPython + modules: + - filename: extension.py + version: 1.0.0 + variables: + - name: author + value: Luca Atella +``` + +### ✳️ Fields + +| Field | Type | Description | +|--------------|----------|--------------------------------------------| +| `arch` | Enum | e.g. `x86_64`, `arm64` | +| `os` | Enum | `linux`, `windows`, `darwin` | +| `version` | str | Python version string (`3.12.4`) | +| `interpreter`| Enum | `CPython`, `PyPy`, `IronPython`, etc. | +| `modules` | list | Repeats the structure declaration per module | + +This structure allows fine-grained targeting of supported environments. + +--- + +## 🌱 environment + +Environment variables and secrets expected on the system. + +```yaml +environment: + variables: + - name: LOG_LEVEL + value: INFO + - name: DEBUG + annotation: bool + value: true + secrets: + - MY_SECRET_KEY + - DATABASE_PASSWORD +``` + +- `variables`: can define name, value, and annotation +- `secrets`: only their presence is verified — values are never exposed + +--- + +## Notes + +- All fields are optional — contracts can be partial +- Field order does not matter +- Unknown fields are ignored with a warning (not an error) + +--- + +## Learn more + +- [Contract syntax](../contracts/syntax.md) +- [Contract violations](../errors/contract-violations.md) diff --git a/docs/modes/embedded.md b/docs/modes/embedded.md new file mode 100644 index 0000000..c5a2d88 --- /dev/null +++ b/docs/modes/embedded.md @@ -0,0 +1,74 @@ +# Embedded Mode + +In Embedded Mode, ImportSpy is embedded directly into the module you want to protect. +When that module is imported, it inspects the runtime environment and the importing module. +If the context doesn't match the declared contract, the import fails with a structured error. + +--- + +## How it works + +By using `Spy().importspy(...)`, a protected module can validate: + +- The **runtime** (OS, Python version, architecture…) +- The **caller module’s structure** (classes, methods, variables, annotations…) + +If validation passes, the module returns a reference to the caller. +If not, the import is blocked and an exception is raised (e.g. `ValueError` or custom error class). + +--- + +## Real-world example: plugin-based architecture + +Let’s walk through a complete example. +This simulates a plugin framework that wants to validate the structure of external plugins at import time. + +### Project structure + +``` +external_module_compliance/ +├── extensions.py # The plugin (caller) +├── package.py # The protected framework +├── plugin_interface.py # Base interface for plugins +└── spymodel.yml # The import contract +``` + +--- + +### 🧩 Source files + +=== "package.py" + +```python +--8<-- "examples/plugin_based_architecture/external_module_compliance/package.py" +``` + +=== "extensions.py" + +```python +--8<-- "examples/plugin_based_architecture/external_module_compliance/extensions.py" +``` + +=== "spymodel.yml" + +```yaml +--8<-- "examples/plugin_based_architecture/external_module_compliance/spymodel.yml" +``` + +--- + +## When to use Embedded Mode + +Use this mode when: + +- You want to **protect a module** from being imported incorrectly +- You’re building a **plugin system** and expect structural consistency from plugins +- You want to **fail fast** in invalid environments +- You need to enforce custom logic during `import` without modifying the caller + +--- + +## Learn more + +- [Contract syntax](../contracts/syntax.md) +- [Contract violations](../errors/contract-violations.md) diff --git a/docs/overrides/extra.html b/docs/overrides/extra.html new file mode 100644 index 0000000..d9dfd47 --- /dev/null +++ b/docs/overrides/extra.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index 1777ce4..0000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,10 +0,0 @@ -sphinx>=8.1.3 -sphinx-tabs>=3.4.7 -furo>=2024.8.6 -sphinx-basic-ng>=1.0.0b2 -sphinxcontrib-applehelp>=2.0.0 -sphinxcontrib-devhelp>=2.0.0 -sphinxcontrib-htmlhelp>=2.1.0 -sphinxcontrib-jsmath>=1.0.1 -sphinxcontrib-qthelp>=2.0.0 -sphinxcontrib-serializinghtml>=2.0.0 diff --git a/docs/source/_static/custom.css b/docs/source/_static/custom.css deleted file mode 100644 index 616a96f..0000000 --- a/docs/source/_static/custom.css +++ /dev/null @@ -1,10 +0,0 @@ -.wy-nav-content { - max-width: 100% !important; - width: 100% !important; -} -/* - -.wy-side-nav-search { - max-width: 20% !important; -} -*/ \ No newline at end of file diff --git a/docs/source/advanced/advanced_index.rst b/docs/source/advanced/advanced_index.rst deleted file mode 100644 index 8eb3642..0000000 --- a/docs/source/advanced/advanced_index.rst +++ /dev/null @@ -1,43 +0,0 @@ -Advanced Topics & Internals of ImportSpy -======================================== - -Welcome to the advanced section of ImportSpy’s documentation — built for developers, integrators, and contributors who want to **go beyond usage** and dive into **how ImportSpy works under the hood**. - -Whether you're building runtime enforcement pipelines, customizing structural validators, or embedding ImportSpy into multi-tenant plugin architectures, this section provides the **deep technical foundation** to unlock ImportSpy's full capabilities. - -🧠 Who This Is For -------------------- - -- Engineers building **custom validation flows** -- Contributors exploring the **internal mechanics** of ImportSpy -- Teams integrating ImportSpy into **CI/CD, containers, and plugin frameworks** -- Architects enforcing **organization-wide import policies** - -🔍 What You’ll Explore ------------------------ - -This section is structured into two complementary areas: - -🏗️ **Architectural Internals** - - A deep technical exploration of ImportSpy’s runtime model, validation stack, and modular design. - - Learn how ImportSpy inspects environments, builds validation contexts, and enforces contracts in both embedded and CLI modes. - -🛠️ **Extension & Integration Points** - - Discover how to write custom validators, extend `SpyModel`, inject runtime policies, or build tooling on top of ImportSpy’s API. - - Ideal for integrating with internal frameworks, policy engines, or advanced CI/CD pipelines. - -📚 **API Reference** - - Browse a fully documented catalog of internal components: - - `SpyModel`, `Function`, `Attribute`, `Deployment`, `Validator`, etc. - - Includes type annotations, usage patterns, and extension strategies. - -This section balances **low-level documentation** with **real-world extensibility guidance**. - -.. toctree:: - :maxdepth: 2 - :caption: Explore the Internals - - architecture_index - api_reference_index - -🚀 Whether you're enforcing security boundaries or writing custom validators, this section is your blueprint for building with — and on top of — ImportSpy. diff --git a/docs/source/advanced/api_reference/api_core.rst b/docs/source/advanced/api_reference/api_core.rst deleted file mode 100644 index afab174..0000000 --- a/docs/source/advanced/api_reference/api_core.rst +++ /dev/null @@ -1,104 +0,0 @@ -Core Engine: Classes, Controllers, and Contracts -================================================ - -This section documents the **core subsystems of ImportSpy** — the internal machinery that powers its runtime validation engine, CLI interface, and contract execution model. - -These APIs are essential for: - -- 🧠 Understanding how validation requests are orchestrated -- 🔄 Hooking into the enforcement lifecycle -- 🛠 Extending ImportSpy for custom validation, logging, or policy enforcement - -Each class below plays a **central role in ImportSpy’s internal flow**, and is fully documented for integration and contribution use cases. - -Spy Class 🕵️‍♂️ -^^^^^^^^^^^^^^^^^ - -The `Spy` class is the **central controller** of ImportSpy’s validation pipeline. - -It handles: - -- Contract parsing and validation -- Import-time introspection of the calling environment -- Runtime orchestration for embedded validation -- Entry point for dynamic execution enforcement - -.. autoclass:: importspy.s.Spy - :members: - :undoc-members: - :show-inheritance: - -Contract Parsers 💾 -^^^^^^^^^^^^^^^^^^^^ - -Import contracts are defined externally as `.yml` files and parsed into structured models. - -- `Parser` is the abstract interface that defines contract loading behavior. -- `YamlParser` is the default parser implementation supporting YAML-based contracts. -- `handle_persistence_error` is a decorator for consistent exception wrapping and traceability. - -.. autoclass:: importspy.persistences.Parser - :members: - :undoc-members: - :show-inheritance: - -.. autoclass:: importspy.persistences.YamlParser - :members: - :undoc-members: - :show-inheritance: - -.. autofunction:: importspy.persistences.handle_persistence_error - -.. autoclass:: importspy.persistences.PersistenceError - :members: - :undoc-members: - :show-inheritance: - -Log Manager 📝 -^^^^^^^^^^^^^^^ - -The `LogManager` provides **structured logging** across both CLI and embedded modes. -It supports log-level control (`DEBUG`, `INFO`, `ERROR`, etc.) and unified message formatting. - -.. autoclass:: importspy.log_manager.LogManager - :members: - :undoc-members: - :show-inheritance: - -Error Messaging ⚠️ -^^^^^^^^^^^^^^^^^^^^ - -The `Errors` class contains standardized error templates used across ImportSpy. -It defines **consistent, user-facing messages** for contract violations, misconfigurations, and environment mismatches. - -.. autoclass:: importspy.errors.Errors - :members: - :undoc-members: - :show-inheritance: - -Constants 📌 -^^^^^^^^^^^^ - -All shared constants, labels, and tags used by the validation engine, parsers, and CLI. -These are used to maintain **naming consistency** across internal modules. - -.. autoclass:: importspy.constants.Constants - :members: - :undoc-members: - :show-inheritance: - -Configuration ⚙️ -^^^^^^^^^^^^^^^^ - -The `Config` class defines **runtime settings and execution context** for ImportSpy. - -It allows developers to: - -- Customize behavior based on validation mode -- Register external contract paths -- Modify enforcement flags dynamically - -.. autoclass:: importspy.config.Config - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/advanced/api_reference/api_models.rst b/docs/source/advanced/api_reference/api_models.rst deleted file mode 100644 index 00334a8..0000000 --- a/docs/source/advanced/api_reference/api_models.rst +++ /dev/null @@ -1,106 +0,0 @@ -Model Layer: SpyModel & Contract Validation System -================================================== - -At the heart of ImportSpy’s compliance framework lies the `SpyModel`: -a fully structured representation of how a Python module **should behave** across different runtime environments. - -ImportSpy does not merely analyze code — it validates whether a module conforms to a **contractual definition** -that includes its **structure**, **runtime expectations**, and **execution constraints**. - -🔍 Whether you’re operating in **embedded mode** or running validations via the **CLI**, -`SpyModel` is the foundation on which all validation logic is built. - -Validation Modes Supported 🧭 ------------------------------ - -Import contracts defined via `.yml` files (or SpyModel objects in code) are evaluated in: - -- **Embedded Mode** 🔌 - Modules protect themselves by invoking `Spy().importspy()` and enforcing contracts on their importers. - -- **External (CLI) Mode** 🛠️ - Used in pipelines or audits to validate a target module before execution or integration. - -Both workflows rely on **SpyModel-based comparison** between expected and actual module states. - -SpyModel Class 🏗️ -------------------- - -The `SpyModel` is a high-level, Pydantic-based model that transforms an import contract into a validated runtime object. - -It defines: - -- 🧱 Structural rules → Expected classes, attributes, methods, return types -- 🧪 Runtime rules → Supported OS, CPU architectures, Python interpreters -- 🔐 Environment rules → Required environment variables and submodule dependencies - -.. autoclass:: importspy.models.SpyModel - :members: - :undoc-members: - :show-inheritance: - -Model Subcomponents 📦 -^^^^^^^^^^^^^^^^^^^^^^^^ - -`SpyModel` is composed of granular submodels that represent the contract’s declarative schema: - -- `Function` → Represents function name, arguments, and return annotations -- `Attribute` → Captures class or global variables (with value, type, and scope) -- `Class` → Groups attributes and methods, along with expected inheritance -- `Module` → Represents nested modules inside deployments -- `Python`, `System`, `Deployment` → Define runtime matrix for cross-platform validation - -.. autoclass:: importspy.models.Function - :members: - :undoc-members: - -.. autoclass:: importspy.models.Attribute - :members: - :undoc-members: - -.. autoclass:: importspy.models.Argument - :members: - :undoc-members: - -.. autoclass:: importspy.models.Class - :members: - :undoc-members: - -.. autoclass:: importspy.models.Module - :members: - :undoc-members: - -.. autoclass:: importspy.models.Python - :members: - :undoc-members: - -.. autoclass:: importspy.models.System - :members: - :undoc-members: - -.. autoclass:: importspy.models.Runtime - :members: - :undoc-members: - -Validator Interface ✅ ----------------------- - -ImportSpy includes a pluggable validator system that compares: - -- The `SpyModel` contract (expected) -- The actual runtime snapshot of the importing environment - -Validators are executed as part of a pipeline that checks: - -- ✔️ Function and method presence -- ✔️ Signature alignment and argument types -- ✔️ Class structure and attribute correctness -- ✔️ Deployment compatibility and environment config -- ✔️ Interpreter and version compliance - -To explore how validators are defined and chained: - -.. toctree:: - :maxdepth: 1 - - api_validators diff --git a/docs/source/advanced/api_reference/api_utilities.rst b/docs/source/advanced/api_reference/api_utilities.rst deleted file mode 100644 index 2002420..0000000 --- a/docs/source/advanced/api_reference/api_utilities.rst +++ /dev/null @@ -1,114 +0,0 @@ -Utilities & Mixins: Internal Tools for Reflection and Runtime Enforcement -========================================================================= - -ImportSpy’s validation engine is powered by a suite of **utility classes** and **mixins** -that enable deep introspection of modules, environments, and Python runtimes. -These components provide the **mechanical backbone** for runtime analysis, structural extraction, -and platform compatibility checks across both **embedded** and **external** validation modes. - -This layer is not typically exposed to end-users—but is invaluable for contributors, -integrators, and advanced developers extending ImportSpy’s logic or building custom tooling. - -Utility Modules ⚙️ ------------------- - -Each utility module encapsulates a specific aspect of **runtime introspection**, enabling: - -- 🔍 Extraction of class/function signatures, annotations, and globals -- 🧠 Detection of system identity (OS, architecture, interpreter, etc.) -- 🔐 Compliance checks for cross-platform and interpreter-specific constraints -- ⚙️ Lightweight, cached evaluation of runtime conditions - -These utilities are **orchestrated automatically** by the validation pipeline but can also -be used independently to write tools or perform standalone validations. - -ModuleUtil – Structural Reflection 🧱 -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -This utility performs deep reflection on a Python module, extracting: - -- Public/private classes -- Methods and their argument signatures -- Attribute values and types -- Class hierarchies and superclasses -- Function return annotations - -.. autoclass:: importspy.utilities.module_util.ModuleUtil - :members: - :undoc-members: - :show-inheritance: - -SystemUtil – OS & Environment Detection 🌐 -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Gathers platform metadata such as: - -- OS name (`linux`, `windows`, etc.) -- Hostname, architecture -- Environment variable inspection and resolution - -.. autoclass:: importspy.utilities.system_util.SystemUtil - :members: - :undoc-members: - :show-inheritance: - -PythonUtil – Interpreter Validation 🐍 -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Identifies the active interpreter and Python version with semantic normalization. -Also verifies whether the environment satisfies version-based or interpreter-based constraints -declared in the import contract. - -.. autoclass:: importspy.utilities.python_util.PythonUtil - :members: - :undoc-members: - :show-inheritance: - -RuntimeUtil – Hardware & Architecture Awareness 🧬 -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Provides a detailed overview of: - -- CPU architecture (`x86_64`, `arm64`, etc.) -- Runtime compatibility with deployment targets -- Cross-architecture filtering logic for contract enforcement - -.. autoclass:: importspy.utilities.runtime_util.RuntimeUtil - :members: - :undoc-members: - :show-inheritance: - -Mixin Components 🔁 -------------------- - -ImportSpy also uses **mixins** to modularize logic that applies across validators and inspectors -without duplicating functionality or creating deep class hierarchies. - -These reusable components inject specialized logic into validation classes where needed. - -AnnotationValidatorMixin 🏷️ -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Ensures that function signatures and variable annotations match the declared expectations. -It supports: - -- Basic type matching (`str`, `int`, etc.) -- Optional and generic annotations -- Graceful fallback for missing or untyped values - -.. autoclass:: importspy.mixins.annotations_validator_mixin.AnnotationValidatorMixin - :members: - :undoc-members: - :show-inheritance: - -📌 Tip: -------- - -While these components are internal, they can be extended or overridden when customizing -ImportSpy’s validation strategy for highly specific use cases (e.g., custom deployment platforms, -corporate runtime wrappers, or secure import enforcement). - -For more on customizing validation behavior, see: - -- :doc:`../architecture/architecture_design_decisions` -- :doc:`api_validators` diff --git a/docs/source/advanced/api_reference/api_validators.rst b/docs/source/advanced/api_reference/api_validators.rst deleted file mode 100644 index 39384f6..0000000 --- a/docs/source/advanced/api_reference/api_validators.rst +++ /dev/null @@ -1,154 +0,0 @@ -The Validation Engine -===================== - -ImportSpy’s validation system is a modular, extensible framework designed to enforce the integrity -and runtime compatibility of any Python module protected by an **Import Contract**. -Whether used in **embedded validation** (inside the module) or in **external CLI mode**, -this engine guarantees that no module is loaded in an unauthorized or structurally inconsistent environment. - -Core Validator Pipeline ⚙️ --------------------------- - -At the center of the engine is the `SpyModelValidator`, a coordinator that orchestrates multiple specialized validators. -Each validator is responsible for comparing part of the actual runtime context or module structure against its declared expectations. - -This pipeline enforces: - -- ✅ Structural integrity -- ✅ Environment compatibility -- ✅ Runtime reproducibility -- ✅ Compliance with declared variables and system settings - -.. note:: - All validators are executed **at runtime**, just before the module is made accessible to the importing code. - -Validation Modes Supported ---------------------------- - -The validation engine operates seamlessly across both execution modes: - -- **🧬 Embedded Mode** — The module validates its own importer at the moment it is loaded. -- **🛠️ External Mode (CLI)** — The module is validated *before* execution begins, often in CI/CD or static validation workflows. - -SpyModelValidator 🧠 ---------------------- - -This is the **entry point** to the validation pipeline. It receives both: - -- The expected structure (parsed from a `.yml` contract or inline SpyModel), and -- The actual runtime data (extracted via introspection) - -It dispatches these to domain-specific validators, collects results, and raises structured errors on failure. - -.. autoclass:: importspy.validators.spymodel_validator.SpyModelValidator - :members: - :undoc-members: - :show-inheritance: - -Structural Validators 🔎 ------------------------- - -These validators inspect the internal structure of the target module: - -AttributeValidator 🔤 -^^^^^^^^^^^^^^^^^^^^^ - -- Ensures global/module/class attributes exist -- Validates `type`, `value`, and `scope` (class vs instance) -- Supports default values and optional annotations - -.. autoclass:: importspy.validators.attribute_validator.AttributeValidator - :members: - :undoc-members: - :show-inheritance: - -FunctionValidator 🛠️ -^^^^^^^^^^^^^^^^^^^^^ - -- Verifies function/method presence -- Checks signatures and return annotations -- Detects function mismatches across versions or overrides - -.. autoclass:: importspy.validators.function_validator.FunctionValidator - :members: - :undoc-members: - :show-inheritance: - -ArgumentValidator 🎛️ -^^^^^^^^^^^^^^^^^^^^^^ - -- Validates function arguments against contract declarations -- Supports type annotations and default value checks -- Ensures complete function interface compliance - -.. autoclass:: importspy.validators.argument_validator.ArgumentValidator - :members: - :undoc-members: - :show-inheritance: - -Environment & Runtime Validators 🌍 ------------------------------------ - -These components validate that the system attempting to import the module is authorized: - -ModuleValidator 📦 -^^^^^^^^^^^^^^^^^^ - -- Validates module metadata (`filename`, `version`, global `variables`) -- Applies naming constraints defined in contracts - -.. autoclass:: importspy.validators.module_validator.ModuleValidator - :members: - :undoc-members: - :show-inheritance: - -SystemValidator 🖥️ -^^^^^^^^^^^^^^^^^^^ - -- Verifies OS name and version compatibility -- Enforces required `env` variables (e.g., `API_KEY`, `DEPLOY_REGION`) -- Useful for containerized and multi-host setups - -.. autoclass:: importspy.validators.system_validator.SystemValidator - :members: - :undoc-members: - :show-inheritance: - -PythonValidator 🐍 -^^^^^^^^^^^^^^^^^^ - -- Checks that the interpreter matches contract constraints -- Supports semantic Python version matching -- Verifies implementation type (`CPython`, `PyPy`, `IronPython`) - -.. autoclass:: importspy.validators.python_validator.PythonValidator - :members: - :undoc-members: - :show-inheritance: - -RuntimeValidator 🚀 -^^^^^^^^^^^^^^^^^^^^ - -- Validates CPU architecture (`x86_64`, `ARM64`, etc.) -- Filters deployments that must only run on specific hardware targets -- Useful for embedded devices, edge computing, and platform-specific plugins - -.. autoclass:: importspy.validators.runtime_validator.RuntimeValidator - :members: - :undoc-members: - :show-inheritance: - -Extending the Engine 🧩 ------------------------ - -Want to add your own validator? - -- Subclass any validator listed here -- Implement the `.validate()` interface -- Register it manually via `SpyModelValidator` or contract-driven hooks - -This makes ImportSpy ideal for **internal compliance layers**, **custom rule sets**, or **secure enterprise environments**. - -See also: - -- :doc:`api_models` for understanding SpyModel structure diff --git a/docs/source/advanced/api_reference_index.rst b/docs/source/advanced/api_reference_index.rst deleted file mode 100644 index 8ca660e..0000000 --- a/docs/source/advanced/api_reference_index.rst +++ /dev/null @@ -1,45 +0,0 @@ -api_reference_index -=================== - -API Reference: Internals & Extensibility ----------------------------------------- - -This section provides a **complete reference guide** to ImportSpy's internal API — designed for developers and contributors who want to: - -- 🔍 Understand how ImportSpy operates under the hood -- 🛠️ Extend its validation logic with custom models and validators -- ⚙️ Integrate runtime enforcement into existing architectures - -Whether you're writing plugins, debugging structural mismatches, or integrating ImportSpy into a CI/CD pipeline, this reference exposes all the essential **building blocks** behind the framework. - -🧩 What You'll Find Inside ---------------------------- - -🔹 **Core API** - The runtime logic powering ImportSpy’s contract enforcement. - Includes import interceptors, validation orchestration, and execution gating. - -🔹 **Model Layer** - Formal Pydantic-based representations of everything from modules and attributes - to Python interpreters, environments, and deployment matrices. - -🔹 **Utility Layer** - Introspection helpers for analyzing imports, resolving dependencies, - reading metadata, or reflecting on runtime state. - -📚 Each module in this section is fully documented with: -- Class definitions -- Method signatures -- Expected behavior -- Extension guidance -- Real-world usage examples - -.. toctree:: - :maxdepth: 2 - :caption: API Modules - - api_reference/api_core - api_reference/api_models - api_reference/api_utilities - -🧠 Use this reference to go beyond configuration — and shape ImportSpy around your architecture, policies, and execution model. diff --git a/docs/source/advanced/architecture/architecture_design_decisions.rst b/docs/source/advanced/architecture/architecture_design_decisions.rst deleted file mode 100644 index f79d5e7..0000000 --- a/docs/source/advanced/architecture/architecture_design_decisions.rst +++ /dev/null @@ -1,150 +0,0 @@ -Design Principles Behind ImportSpy -================================== - -ImportSpy was built to answer a fundamental question in dynamic Python environments: - -🔐 *"How can we guarantee that modules are imported only under the conditions they were designed for?"* - -Unlike traditional linters or static analyzers, ImportSpy enforces **live structural contracts** at the moment of import. This section details the architectural decisions that enable ImportSpy to operate securely, predictably, and scalably across both **runtime validation** and **automated pipelines**. - -Why Runtime Validation? 🧠 --------------------------- - -Python is dynamic. That’s its strength—and its risk. - -Most tools operate **before execution** (e.g. `mypy`, `flake8`), but these tools can’t: - -- Detect runtime-only configurations (e.g. `os.environ`, `importlib`). -- Block a plugin from loading in an unauthorized host. -- Enforce interpreter or architecture constraints **at runtime**. - -✨ **ImportSpy validates code *as it’s being imported***—right where behavior matters. -It defers enforcement to the **moment of execution**, where guarantees can be *proven*. - -Why Validate the Importing Environment? 🔄 ------------------------------------------- - -ImportSpy inverts the typical validation direction. - -Instead of saying: - -> “This plugin must look like X.” - -It asks: - -> “Is the context trying to use this plugin *safe enough* to do so?” - -Modules don’t just exist—they **run somewhere**. -By inspecting the **caller**, ImportSpy ensures: - -- Plugins are loaded only in verified systems. -- The host respects the structure, env vars, interpreter, etc. -- No unauthorized module can silently consume sensitive logic. - -📌 **This is fundamental to plugin security, cross-runtime compliance, and containerized deployments.** - -Why Declarative Import Contracts? 📜 ------------------------------------- - -Validation logic shouldn’t live inside Python files. - -Instead, ImportSpy uses **external YAML contracts** that describe: - -- Expected runtime constraints (e.g. `os: linux`, `python: 3.12`) -- Structural contracts (functions, classes, attributes) -- Deployment variation (e.g. different rules for Windows vs Linux) - -These contracts are parsed into a runtime `SpyModel`—an internal abstraction built with Pydantic. - -Benefits: - -- ✅ Easy to version -- ✅ Works across both embedded and CLI validation -- ✅ Enables testing, linting, and reuse -- ✅ No invasive logic in the codebase - -Why Python Introspection? 🔍 ----------------------------- - -ImportSpy is powered by Python’s built-in runtime introspection features: - -- `inspect.stack()` to find the caller -- `getmembers()`, `isfunction()`, `isclass()` to rebuild module structure -- `sys`, `platform`, and `os` to gather system metadata - -This allows ImportSpy to: - -- Mirror the structure of any module -- Dynamically analyze real-time execution state -- Validate *what’s really there*—not what’s assumed - -By using native reflection instead of static assumptions, ImportSpy gains **flexibility and truthfulness**. - -Why Two Validation Modes? ⚙️ ------------------------------ - -ImportSpy supports two usage models: - -### 1. Embedded Mode (Validation from inside the module) - -- Great for plugins and reusable components -- Self-protecting modules: reject imports from unverified hosts -- Guarantees runtime safety at every import - -### 2. External Mode (Validation from CI/CD or CLI) - -- Ideal for pre-deployment validation -- Works well in test automation, pipelines, and secure releases -- Ensures modules are structurally valid before execution - -Both use the same contract schema. -Both use the same engine. -Both improve trust. - -Why Block on Failure? 🚫 ------------------------- - -ImportSpy adopts a **zero-trust default**: - -- ❌ Invalid environment? → Raise `ValidationError` -- ❌ Mismatched structure? → Abort import -- ✅ Fully compliant? → Proceed as normal - -This prevents: - -- Unexpected side effects -- Code being “partially valid” -- Runtime surprises in production - -Errors are detailed, categorized, and explain *what failed, where, and why*. - -Performance Tradeoffs & Optimizations 🧮 ----------------------------------------- - -Runtime validation adds overhead. But ImportSpy minimizes it through: - -- 🧠 **Caching** of introspection results -- 🔍 **Selective analysis** (skip unused layers) -- 🧱 **Lazy evaluation** of module components -- 📉 **Short-circuiting** at first contract breach - -Result: validation is fast enough for real-time enforcement—even inside plugins. - -Core Design Principles 🧭 --------------------------- - -These ideas guide ImportSpy’s architecture: - -- **Declarative-first** – Let contracts define validation, not Python logic. -- **Zero-trust imports** – Always verify before executing. -- **Context-aware validation** – Enforce structure *and* environment. -- **Cross-environment readiness** – Design for CI, containers, local, and cloud. -- **Developer ergonomics** – Errors are clear. Contracts are readable. Setup is fast. - -Next Steps 🔬 -------------- - -To see these principles in action: - -- Dive into :doc:`architecture_runtime_analysis` → How runtime environments are captured -- Or explore :doc:`architecture_validation_engine` → How actual validation decisions are made diff --git a/docs/source/advanced/architecture/architecture_overview.rst b/docs/source/advanced/architecture/architecture_overview.rst deleted file mode 100644 index 4e7cfdf..0000000 --- a/docs/source/advanced/architecture/architecture_overview.rst +++ /dev/null @@ -1,114 +0,0 @@ -Architecture Overview -===================== - -ImportSpy is a structural validation engine for Python that operates across two distinct execution models: **embedded mode** and **external (CLI) mode**. -Its architecture is designed to adapt seamlessly to both, providing a **runtime validation system** that enforces **import contracts**—declarative YAML specifications defining how and where a module is allowed to run. - -This section introduces the architectural layers, flows, and principles behind ImportSpy’s execution model. - -Architectural Objectives ------------------------- - -ImportSpy is built upon four core pillars: - -1. **Contract-Driven Validation** - Modules define import contracts that describe the expected runtime and structural context. - -2. **Zero-Trust Execution Model** - Code is never executed unless the importing or imported module complies with declared constraints. - -3. **Dynamic Runtime Enforcement** - System context is reconstructed at runtime using reflection and introspection. - -4. **Composable Validation Layers** - Validation is performed in discrete phases (structure, environment, runtime, interpreter), making the architecture modular and extensible. - -Supported Execution Modes --------------------------- - -ImportSpy is dual-mode by design: - -🔹 **Embedded Mode** (for modules that protect themselves) - -- Validation is triggered **inside** the protected module. -- The module inspects **who is importing it** and verifies the caller’s structure and runtime context. -- Typical use case: plugins that must ensure their importing host complies with an expected contract. - -🔹 **External Mode** (for CI/CD or static compliance pipelines) - -- Validation is triggered via CLI before execution. -- The target module is validated **from the outside**, ensuring it conforms to its declared contract. -- Typical use case: pipeline validation of Python modules before deployment. - -Both modes share the same validation engine and contract semantics but differ in the **direction** of the inspection (who validates whom). - -Architectural Layers --------------------- - -The architecture of ImportSpy can be decomposed into the following logical layers: - -🏗️ **Context Reconstruction Layer** - - Gathers system information from the current runtime. - - Captures OS, Python version, architecture, interpreter, and environment variables. - -🔁 **SpyModel Builder** - - Builds a structured representation of the runtime or module to validate. - - Converts contracts and runtime state into Pydantic models. - -📦 **Import Contract Loader** - - Parses the YAML `.yml` contract into a typed validation model. - - Supports nested structures, deployment variations, and type annotations. - -🔍 **Validation Pipeline** - - Compares the reconstructed runtime or module state against the contract. - - Handles structure (functions, classes), environment (variables), and system (interpreter, OS, arch). - -🔐 **Enforcement & Error Handling** - - Raises structured exceptions on failure (with detailed error classification). - - Blocks execution in embedded mode; returns exit codes in CLI mode. - -Execution Flow --------------- - -📌 Embedded Mode: - -1. Module executes `Spy().importspy(...)` at the top of its source. -2. The call stack is inspected to identify the **caller module**. -3. A `SpyModel` of the caller is reconstructed. -4. The module’s own contract is loaded. -5. If the caller matches the contract, execution continues. -6. If not, a `ValidationError` is raised and execution is blocked. - -📌 External Mode: - -1. CLI is invoked with `importspy -s contract.yml my_module.py`. -2. `my_module.py` is dynamically loaded and introspected. -3. Its structure is extracted: classes, functions, attributes, variables. -4. The YAML contract is parsed into a validation model. -5. Structural and runtime validation is performed. -6. Success → status code 0. Failure → detailed error message and exit code 1. - -Illustration: - -.. image:: https://raw.githubusercontent.com/atellaluca/ImportSpy/main/assets/importspy-architecture.png - :align: center - :alt: ImportSpy Architecture Overview - -Why This Architecture Matters ------------------------------ - -This architecture provides: - -- ✅ Full control over **execution guarantees** of Python modules -- ✅ Runtime enforcement of **environmental and structural policies** -- ✅ Dual-mode support for **plugin protection and CI/CD validation** -- ✅ A uniform validation model across **local, container, and distributed runtimes** - -What’s Next? ------------- - -Continue with: - -- :doc:`architecture_runtime_analysis` → How ImportSpy reconstructs runtime environments -- :doc:`architecture_validation_engine` → The core validation logic and error system -- :doc:`architecture_design_decisions` → Design trade-offs, limitations, and rationale diff --git a/docs/source/advanced/architecture/architecture_runtime_analysis.rst b/docs/source/advanced/architecture/architecture_runtime_analysis.rst deleted file mode 100644 index f4527e0..0000000 --- a/docs/source/advanced/architecture/architecture_runtime_analysis.rst +++ /dev/null @@ -1,125 +0,0 @@ -architecture_runtime_analysis -============================= - -Understanding Runtime Analysis in ImportSpy -------------------------------------------- - -ImportSpy doesn’t guess — it **knows exactly who’s importing your code, from where, and how**. - -Its runtime analysis engine reconstructs the **real-time execution context** surrounding an import and evaluates whether it complies with the expectations declared in an **Import Contract**. - -This section explains how ImportSpy leverages Python's introspection system to **enforce validation dynamically**, across both embedded and CLI modes. - -🧠 What Makes ImportSpy Runtime-Aware? ---------------------------------------- - -At the heart of ImportSpy is a fundamental insight: - -> A Python module isn’t just *defined* — it’s *executed in context.* - -ImportSpy inspects that context to answer questions like: - -- Who is importing this code? -- Is the environment approved (OS, Python version, interpreter)? -- Are expected environment variables and metadata in place? -- Does the runtime architecture match the contract? - -Instead of assuming compliance, ImportSpy **validates the reality of execution** — and blocks code that violates it. - -🧱 Key Layers of Runtime Analysis ----------------------------------- - -Here’s how ImportSpy turns Python’s dynamic nature into a validation pipeline: - -1️⃣ **Call Stack Introspection** - - Uses `inspect.stack()` to trace back to the module attempting the import. - - Identifies the **caller module**, not just the callee. - -2️⃣ **Context Extraction** - - Gathers system metadata, including: - - OS (Linux, macOS, Windows) - - CPU architecture (e.g. `x86_64`, `arm64`) - - Python version and interpreter (CPython, PyPy, IronPython) - - Environment variables (e.g., `API_KEY`, `STAGE`) - - Installed module structure (classes, functions, globals) - -3️⃣ **SpyModel Construction** - - Dynamically builds an internal `SpyModel` from the importing context. - - Matches structure and environment against the import contract. - -4️⃣ **Validation Decision** - - Compares expected constraints from YAML or Python object. - - Raises `ValidationError` if mismatches are found — or returns control if all checks pass. - -🔍 Core Python Tools Behind the Magic --------------------------------------- - -ImportSpy uses only built-in Python modules — no black magic, just introspection: - -- `inspect.stack()` – call stack tracing -- `inspect.getmodule()` – resolve module context -- `platform.system()`, `platform.machine()` – OS and architecture -- `sys.version_info`, `platform.python_implementation()` – Python version and interpreter -- `os.environ` – environment variable resolution -- `getmembers()` – dynamic class/function structure extraction - -These are the building blocks behind ImportSpy’s runtime truth-checking engine. - -⚙️ Embedded vs External Mode: Runtime Differences --------------------------------------------------- - -| Mode | Validated Context | Typical Use Case | -|-----------------|----------------------------|---------------------------------------------------| -| **Embedded** | The **importer** of a module | Plugin architectures, sandbox validation | -| **External** | The **module itself** | CI pipelines, security audits, pre-release checks | - -Both modes rely on the **same runtime model**, but invert the direction of validation. - -✅ Embedded Mode Example: -- `my_plugin.py` calls `import core_module.py` -- Inside `core_module`, validation ensures `my_plugin` is allowed to import it. - -✅ CLI Mode Example: -- You run `importspy -s contract.yml my_plugin.py` -- ImportSpy checks if `my_plugin` complies with its declared structure and runtime constraints. - -🚀 Why Runtime Analysis Changes the Game ------------------------------------------ - -ImportSpy’s runtime model enables features few tools can offer: - -- **Import-time contract enforcement** — with precise control over OS, interpreter, architecture -- **Real context validation** — no assumptions, just introspection -- **Full plugin safety** — modules can reject untrusted importers -- **CI/CD guarantees** — validate module deployment conditions at build time - -Python’s flexibility is often seen as a liability — ImportSpy turns it into an *auditable gate*. - -⚡ Performance Considerations ------------------------------ - -Runtime analysis has a cost — but ImportSpy minimizes it through: - -- **Lazy evaluation** — modules are only analyzed when loaded -- **Context caching** — previously computed SpyModels are reused -- **Selective enforcement** — system modules are skipped unless explicitly targeted - -Validation takes milliseconds, not seconds — even in dynamic plugin workflows. - -🔐 Final Takeaway ------------------- - -ImportSpy’s runtime analysis engine turns **introspection into validation**. - -By deeply understanding **who is importing what, from where, and under which conditions**, ImportSpy enforces: - -✅ Structural correctness -✅ Environmental compliance -✅ Runtime safety — across interpreters, containers, and pipelines - -Whether you’re building a plugin system, securing a package, or hardening your CI, -ImportSpy gives you the tools to **intercept, introspect, and enforce — right at import time.** - -Next: -- :doc:`architecture_validation_engine` → See how the validator pipeline executes -- :doc:`architecture_design_decisions` → Understand the rationale behind ImportSpy’s runtime-first approach diff --git a/docs/source/advanced/architecture/architecture_validation_engine.rst b/docs/source/advanced/architecture/architecture_validation_engine.rst deleted file mode 100644 index 681bbe5..0000000 --- a/docs/source/advanced/architecture/architecture_validation_engine.rst +++ /dev/null @@ -1,117 +0,0 @@ -The Validation Engine: Import-Time Assurance for Python -======================================================= - -At the center of ImportSpy lies its **validation engine** — a layered, runtime-first mechanism designed to make sure that: - -✅ Code only runs in verified environments -✅ Structure and behavior match declared expectations -✅ Unauthorized imports are blocked at the boundary - -Unlike static linters or test suites, ImportSpy runs at **import time**, ensuring that modules are **never executed unless compliant** — a zero-trust posture for the Python ecosystem. - -🎯 What the Validation Engine Actually Does -------------------------------------------- - -The validation engine intercepts import events and answers: - -- Is the importing environment trusted? -- Is the runtime (OS, architecture, interpreter) allowed? -- Does the structure of the module match what was promised? -- Are declared environment variables, dependencies, and APIs present? - -It acts like a **runtime compliance firewall** — catching issues before a single line of code is executed. - -📦 Core Pipeline Stages ------------------------- - -Whether in **embedded** mode, ImportSpy uses the same five-stage validation pipeline: - -1️⃣ **Import Interception** - - Uses stack inspection (`inspect.stack`) to trace the importing module. - - Determines the precise origin of the import. - -2️⃣ **Context Modeling (SpyModel Construction)** - - Builds a full runtime profile: - - OS, CPU architecture - - Python version and interpreter - - Environment variables - - Nested module dependencies - -3️⃣ **Structural Validation** - - Analyzes the module’s actual structure (via `inspect`, `ast`, `getmembers`) - - Compares it against the declared contract: - - Classes and superclasses - - Function names, signatures, return types - - Global variables, attributes, and annotations - -4️⃣ **Contract Evaluation** - - Evaluates the runtime `SpyModel` against the declared import contract (in YAML or Python). - - Uses typed validators to match expected values — with support for optional vs required fields. - -5️⃣ **Enforcement & Feedback** - - ✅ If all checks pass, control is returned to the caller. - - ❌ If validation fails: - - Raise `ValidationError` with structured diagnostics - - Provide exact mismatch detail (missing method, wrong version, etc.) - - Halt execution unless soft mode is enabled - -🔍 Modular Validation Subsystems ---------------------------------- - -ImportSpy’s engine is composed of distinct layers, each with its own responsibility: - -🔹 **Import Interceptor** - Detects runtime context at the moment of import. Gathers caller identity and call stack. - -🔹 **SpyModel Generator** - Constructs a normalized model from dynamic runtime inputs. Represents the environment as data. - -🔹 **Validator Stack** - Runs a pipeline of validators, including: - - Structural validators (classes, functions, attributes) - - Environmental validators (OS, Python, architecture) - - Context validators (importer identity, variables, contract location) - -🔹 **Report Engine** - Formats failure messages and traces: - - Uses centralized error codes - - Offers developer-facing hints and CI-friendly logs - -🔹 **Resolution Manager** (Planned) - In future releases, this will support: - - Auto-suggestions for mismatches - - Soft warnings for dry runs - - Contract diffing and explainability tools - -⚙️ Optimizing for Runtime Performance --------------------------------------- - -Validation must be precise — but also fast. ImportSpy uses: - -- **Lazy Evaluation** – modules are only analyzed when accessed. -- **Context Caching** – avoids recomputing runtime metadata. -- **Selective Enforcement** – skips system libraries and only enforces contracts for targeted modules. -- **Failure Short-Circuiting** – stops on the first critical violation unless configured otherwise. - -In most use cases, validation completes in under 50ms — fast enough for production use, even inside plugin systems. - -🔐 Why This Matters --------------------- - -Python offers no guardrails by default. Anyone can import anything, in any context. - -ImportSpy's validation engine creates those guardrails by: - -✅ Binding module behavior to structural truth -✅ Locking execution to trusted environments -✅ Giving developers and systems **predictable, explainable outcomes** - -It’s the difference between _hoping your module runs correctly_ and _knowing that it only ever runs under the right conditions._ - -📘 Next Steps -------------- - -Continue exploring the architecture: - -- :doc:`architecture_runtime_analysis` → See how execution context is captured -- :doc:`architecture_design_decisions` → Understand the philosophy behind runtime validation diff --git a/docs/source/advanced/architecture_index.rst b/docs/source/advanced/architecture_index.rst deleted file mode 100644 index edeb632..0000000 --- a/docs/source/advanced/architecture_index.rst +++ /dev/null @@ -1,64 +0,0 @@ -ImportSpy Architecture -====================== - -The Internal Blueprint of Runtime Validation 🧠 ------------------------------------------------ - -ImportSpy is more than a module linter — it is an **import-time enforcement layer** -that introduces structural awareness and compliance validation directly into the Python runtime. - -This section explores **how ImportSpy works under the hood**, breaking down its core architecture -into modular layers that combine **dynamic reflection**, **declarative contracts**, and **runtime interception**. - -Why Architecture Matters -------------------------- - -In a Python ecosystem where: - -- Modules are shared across microservices and containers, -- Plugins are authored by third parties, -- Deployments span heterogeneous systems, - -...you need more than just "tests". You need a **validation engine** that adapts at runtime. - -ImportSpy was designed to: - -- 🛡️ **Enforce predictable structure** in external modules -- 🧩 **Capture and interpret runtime conditions** dynamically -- 🔒 **Prevent misaligned or unauthorized integrations** - -It introduces formal boundaries where Python has none. - -What You'll Learn in This Section 📚 ------------------------------------- - -This section explains how ImportSpy brings **declarative rigor to dynamic Python environments**. - -You’ll explore: - -- ✅ The **layered architecture** that enables flexible yet strict validation -- ✅ The **rationale behind each design decision** — from using YAML contracts to stack inspection -- ✅ The **engine that drives compliance enforcement**, based on Pydantic and reflection -- ✅ The **runtime analyzer** that reconstructs execution environments -- ✅ The **performance patterns** that make ImportSpy usable even at scale - -.. toctree:: - :maxdepth: 2 - - architecture/architecture_overview - architecture/architecture_design_decisions - architecture/architecture_validation_engine - architecture/architecture_runtime_analysis - -Who Is This For? ----------------- - -Whether you're: - -- a **developer** embedding ImportSpy in a plugin framework, -- a **security engineer** hardening Python execution boundaries, -- or a **contributor** improving contract modeling, - -this section will give you the architectural grounding to wield ImportSpy **confidently and effectively**. - -Ready to look inside the engine? Let’s go. 🚀 diff --git a/docs/source/beginner/beginner_index.rst b/docs/source/beginner/beginner_index.rst deleted file mode 100644 index c072445..0000000 --- a/docs/source/beginner/beginner_index.rst +++ /dev/null @@ -1,64 +0,0 @@ -Beginner Guide to ImportSpy -=========================== - -👋 Welcome to the **Beginner Guide** for ImportSpy! - -This section is built for developers who are **new to ImportSpy** and want to build a **deep and practical understanding** -of how it works—from its internal architecture to the powerful Python concepts it builds upon. - -ImportSpy is more than just a validation tool—it’s a teaching opportunity. -By understanding its foundations, you’ll not only use it better, but you’ll also sharpen your overall Python skills. - -What You’ll Learn in This Guide 📚 ----------------------------------- - -This guide is designed to help you understand **how ImportSpy works under the hood**, -by introducing you to the core technologies and design principles it leverages. - -🧠 Topics include: - -- **🔧 Managing ImportSpy with Poetry** - Understand how ImportSpy is structured, installed, and maintained using [**Poetry**](https://python-poetry.org/), - a modern dependency manager and project builder for Python. - -- **🔍 Python Reflection & Introspection** - Learn how ImportSpy uses Python’s dynamic features like `inspect`, `importlib`, and stack frames - to reconstruct runtime context and validate imports on the fly. - -- **📐 Pydantic and Data Modeling** - Explore how ImportSpy uses **Pydantic** to define and validate import contracts, - turning YAML declarations into structured, type-safe models that enforce correctness. - -Who Should Read This Guide? 🎯 ------------------------------- - -You’ll benefit most from this guide if: - -✅ You’re **new to ImportSpy** and want to understand how it really works -✅ You’re interested in Python's **runtime system, reflection, and data validation** -✅ You want to learn how **modern tooling like Poetry and Pydantic** can help you write better software - -How to Use This Guide 🛠️ -------------------------- - -Each section in this guide is designed to be self-contained, but together they provide a **progressive learning path**. -We recommend reading them in order, especially if you're new to: - -- Poetry and dependency management in Python -- Reflection and runtime validation -- Declarative contracts and schema-driven design - -Each topic includes: - -- ✅ Clear explanations -- 💡 Real-world examples -- 🧠 Best practices you can apply to your own projects - -.. toctree:: - :maxdepth: 2 - - poetry_basics - python_reflection - pydantic_in_importspy - -🎉 Let’s get started—build your knowledge and unlock the full power of ImportSpy! diff --git a/docs/source/beginner/poetry_basics.rst b/docs/source/beginner/poetry_basics.rst deleted file mode 100644 index 24268b9..0000000 --- a/docs/source/beginner/poetry_basics.rst +++ /dev/null @@ -1,146 +0,0 @@ -Using Poetry with ImportSpy -=========================== - -Poetry is the **official packaging and dependency management tool** used in ImportSpy. -It ensures reproducibility, streamlines development workflows, and enables better collaboration. -This guide will help you understand how to use Poetry within ImportSpy’s ecosystem and learn why it’s essential. - -Why Poetry? ------------ - -Poetry offers a modern alternative to legacy tools like `pip`, `setup.py`, and `requirements.txt`. -It provides: - -- ✅ **Isolated virtual environments** with automatic activation -- ✅ **Declarative dependency management** via `pyproject.toml` -- ✅ **Lockfile consistency** with `poetry.lock` -- ✅ **Integrated build and publishing workflow** -- ✅ **Support for multiple dependency groups** (dev, docs, ci, etc.) - -Installing Poetry ------------------ - -You can install Poetry with the official script: - -.. code-block:: bash - - curl -sSL https://install.python-poetry.org | python3 - - -Verify installation: - -.. code-block:: bash - - poetry --version - -Setting Up ImportSpy --------------------- - -1. Clone the repository: - - .. code-block:: bash - - git clone https://github.com/atellaluca/importspy.git - cd importspy - -2. Install all project dependencies: - - .. code-block:: bash - - poetry install - -3. Activate the virtual environment (optional): - - .. code-block:: bash - - poetry shell - -Dependency Management ---------------------- - -Add dependencies: - -.. code-block:: bash - - poetry add pydantic - poetry add --group dev pytest - -Remove dependencies: - -.. code-block:: bash - - poetry remove pydantic - -Update dependencies: - -.. code-block:: bash - - poetry update # Update all - poetry update pydantic # Update a specific one - -Best practice: -✅ Always commit `poetry.lock` to your VCS to ensure reproducibility. - -Understanding the `pyproject.toml` ----------------------------------- - -.. code-block:: toml - - [tool.poetry] - name = "importspy" - version = "0.2.0" - description = "A validation and compliance framework for Python modules." - authors = ["Luca Atella "] - - [tool.poetry.dependencies] - python = "^3.10" - pydantic = "^2.9.2" - - [tool.poetry.group.dev.dependencies] - pytest = "^8.3.3" - - [tool.poetry.group.docs.dependencies] - sphinx = "^7.2" - furo = "^2024.8.6" - - [tool.poetry.scripts] - importspy = "importspy.cli:validate" - -To run CLI commands defined in the `pyproject.toml`: - -.. code-block:: bash - - poetry run importspy --help - -Versioning and Releases ------------------------ - -ImportSpy follows Semantic Versioning (SemVer). -You can bump versions like this: - -.. code-block:: bash - - poetry version patch | minor | major - -Build and publish (requires authentication): - -.. code-block:: bash - - poetry build - poetry publish - -Exporting Requirements ----------------------- - -If you need a `requirements.txt` (e.g., for Docker or legacy tooling): - -.. code-block:: bash - - poetry export -f requirements.txt --output requirements.txt - -Next Steps ----------- - -Now that you’ve configured Poetry, continue learning about ImportSpy’s internals: - -- :doc:`python_reflection` -- :doc:`pydantic_in_importspy` diff --git a/docs/source/beginner/pydantic_in_importspy.rst b/docs/source/beginner/pydantic_in_importspy.rst deleted file mode 100644 index aca6355..0000000 --- a/docs/source/beginner/pydantic_in_importspy.rst +++ /dev/null @@ -1,126 +0,0 @@ -Pydantic in ImportSpy -====================== - -Why Pydantic Matters for ImportSpy 🧠 -------------------------------------- - -ImportSpy uses **Pydantic** as the foundation for its validation engine, enabling it to model and enforce strict structural and environmental expectations. - -In a dynamic language like Python, where anything can change at runtime, Pydantic provides **deterministic enforcement** of expected module attributes, function signatures, return types, and environment variables. - -By wrapping all validation logic in **Pydantic-based models**, ImportSpy transforms flexible contracts into **strict runtime guards**. - -Core Advantages: - -- ✅ Declarative schemas that model module structure and runtime constraints. -- ✅ Precise, readable errors that help developers fix violations quickly. -- ✅ Built-in support for complex types, enums, environment parsing, and more. - -How Pydantic Is Used in ImportSpy 🔍 ------------------------------------- - -All import contracts (`.yml`) are parsed and converted into nested **Pydantic models** during runtime or CLI validation. These models serve as the "expected shape" against which a module or runtime is validated. - -Each layer of the import contract is mapped to a Pydantic model: - -- A class like `Extension` in a plugin? → `ClassModel`. -- A function like `add_extension(msg: str) -> str`? → `FunctionModel`. -- An interpreter requirement? → `InterpreterModel`. -- OS/environment constraints? → `SystemModel`. - -.. code-block:: python - - from pydantic import BaseModel - from typing import List, Optional - - class MethodModel(BaseModel): - name: str - arguments: List[str] - return_annotation: Optional[str] = None - - class ClassModel(BaseModel): - name: str - methods: List[MethodModel] - -Validation Example 🧪 ----------------------- - -Here’s a simplified validation use case. - -.. code-block:: python - - class PluginContract(BaseModel): - filename: str - classes: List[ClassModel] - - contract = PluginContract( - filename="extension.py", - classes=[ - ClassModel( - name="Extension", - methods=[ - MethodModel(name="add_extension", arguments=["self", "msg"], return_annotation="str") - ] - ) - ] - ) - -Now at runtime, if a module lacks that method or returns the wrong type, ImportSpy fails **before** execution. - -Runtime Failures Are Structured ⚠️ ----------------------------------- - -Pydantic errors are deeply integrated with ImportSpy’s logging and debugging layers: - -.. code-block:: json - - [ - { - "loc": ["classes", 0, "methods", 0, "return_annotation"], - "msg": "str type expected", - "type": "type_error.str" - } - ] - -This means: no silent failures, no vague logs. -You know *exactly* what’s missing, and where. - -Benefits Beyond Type Checking ✅ --------------------------------- - -- 🧩 **Cross-layer schema validation**: classes within modules, methods within classes, etc. -- 🛡️ **Zero-Trust enforcement**: if something’s missing, execution is blocked. -- 🔄 **Reusable contract definitions**: models are consistent across embedded and CLI mode. -- 📖 **Documentation as code**: import contracts double as machine- and human-readable specs. - -Advanced Use: Dynamic Constraints ---------------------------------- - -Want to block execution in certain Python versions? Or only allow certain interpreters? - -Pydantic makes it easy to write declarative rules: - -.. code-block:: python - - from pydantic import BaseModel, validator - - class PythonRuntime(BaseModel): - version: str - - @validator("version") - def must_be_310_or_higher(cls, v): - if v < "3.10": - raise ValueError("Python version must be >= 3.10") - return v - -Conclusion 🎯 -------------- - -Pydantic is not just a convenience in ImportSpy — it’s the **core engine** behind runtime validation. - -It provides a robust layer to define, enforce, and debug structural rules with confidence. - -Next steps: - -- :doc:`python_reflection` — Learn how ImportSpy introspects code dynamically. -- https://docs.pydantic.dev/ — Go deeper into advanced Pydantic use cases. diff --git a/docs/source/beginner/python_reflection.rst b/docs/source/beginner/python_reflection.rst deleted file mode 100644 index 72e4c84..0000000 --- a/docs/source/beginner/python_reflection.rst +++ /dev/null @@ -1,121 +0,0 @@ -Understanding Python Reflection in ImportSpy -============================================ - -Why Reflection Matters 🪞 --------------------------- - -Python's reflection capabilities allow code to inspect, analyze, and interact with itself at runtime. -This is central to how **ImportSpy** validates modules dynamically — it doesn't just look at source code, -it actively examines **what exists and how it behaves** at the moment of import. - -In a system where plugins or modules are loosely coupled, this allows ImportSpy to: - -- Validate structural expectations (`classes`, `functions`, `attributes`). -- Detect runtime constraints (`interpreter`, `version`, `environment`). -- Prevent unexpected or unauthorized imports. - -Core Python Reflection Tools 🔍 -------------------------------- - -ImportSpy uses several key components of Python’s reflection toolbox: - -**1. `inspect`** — Runtime introspection - -.. code-block:: python - - import inspect - - def foo(): pass - - print(inspect.isfunction(foo)) # True - print(inspect.getmembers(foo)) # List all members of the function object - -**2. `getattr` / `hasattr` / `setattr`** — Attribute access and mutation - -.. code-block:: python - - class User: name = "Alice" - - u = User() - print(getattr(u, "name")) # "Alice" - print(hasattr(u, "email")) # False - setattr(u, "email", "a@example.com") # Dynamically add attribute - -**3. `importlib`** — Dynamic module loading - -.. code-block:: python - - import importlib - - mod = importlib.import_module("math") - print(mod.sqrt(16)) # 4.0 - -These techniques allow ImportSpy to analyze **any arbitrary Python module** during validation. - -How ImportSpy Uses Reflection 🧠 --------------------------------- - -ImportSpy doesn’t hardcode validation rules into your code. -Instead, it reads a YAML contract, parses it into a structured `SpyModel`, and: - -1. **Intercepts the importing context** - → via `inspect.stack()` to determine *who* is importing the validated module. - -2. **Loads the target module** - → via `importlib` or by extracting from `sys.modules`. - -3. **Validates its structure** - → using `inspect.getmembers()` to check for methods, annotations, and base classes. - -4. **Checks runtime environment** - → including Python version, interpreter type, and required variables. - -This **dynamic, contract-driven validation** is only possible thanks to Python's reflective architecture. - -Reflection in Embedded Mode vs CLI Mode 🔁 ------------------------------------------- - -In **Embedded Mode**, reflection is used by the validated module itself: - -- It calls `Spy().importspy(...)` -- Uses `inspect.stack()` to identify the **caller** -- Then validates that external environment using reflection - -In **CLI Mode**, reflection is applied directly to the target file: - -- `importspy -s contract.yml module.py` -- ImportSpy dynamically loads and introspects the module -- Checks all runtime constraints before it can be deployed - -Best Practices & Pitfalls ⚠️ ----------------------------- - -Reflection is powerful — but should be used wisely: - -✅ **Cache inspection results** to avoid repeat analysis -❌ Avoid calling unknown or unsafe methods with `getattr()` blindly -✅ Combine with type checks (`callable`, `isinstance`) before execution -❌ Don’t mutate live objects unless you're in full control - -Example: safe method invocation - -.. code-block:: python - - if hasattr(module, "run") and callable(module.run): - module.run() - -Takeaway 🧠 ------------ - -Reflection is what makes ImportSpy possible. - -By using `inspect`, `importlib`, and Python’s runtime model, ImportSpy can: - -- Enforce validation without altering your code -- Dynamically adapt to different environments -- Offer a robust, runtime-safe contract enforcement system - -Explore more: - -- :doc:`pydantic_in_importspy` -- `https://docs.python.org/3/library/inspect.html` diff --git a/docs/source/conf.py b/docs/source/conf.py deleted file mode 100644 index bd90af1..0000000 --- a/docs/source/conf.py +++ /dev/null @@ -1,66 +0,0 @@ -# Configuration file for the Sphinx documentation builder. -# -# For the full list of built-in configuration values, see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html - -# -- Project information ----------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information - -project = 'ImportSpy' -copyright = '2024, Luca Atella' -author = 'Luca Atella' -release = '0.3.3' - -# -- General configuration --------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration - -extensions = [ - 'sphinx.ext.napoleon', - 'sphinx.ext.autodoc', - 'sphinx.ext.viewcode', - 'sphinx_tabs.tabs', -] - -templates_path = ['_templates'] -exclude_patterns = [] - - - -# -- Options for HTML output ------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output - -html_theme = "furo" -html_static_path = ['_static'] -html_css_files = ['custom.css'] - -html_theme_options = { - -} - -html_theme_options["footer_icons"] = [ - { - "name": "GitHub", - "url": "https://github.com/atellaluca/ImportSpy", - "html": """ - - - - - """, - "class": "", - }, - { - "name": "PyPI", - "url": "https://pypi.org/project/ImportSpy/", - "html": """ - - - - """, - "class": "", - } -] diff --git a/docs/source/get_started.rst b/docs/source/get_started.rst deleted file mode 100644 index 76e1b5a..0000000 --- a/docs/source/get_started.rst +++ /dev/null @@ -1,69 +0,0 @@ -Get Started with ImportSpy 🚀 -============================= - -Welcome to the official **Get Started** guide for ImportSpy! - -This section is your hands-on introduction to ImportSpy’s capabilities and philosophy. -If you're building systems with **plugins**, **microservices**, **modular designs**, or simply want **more control over how your code is imported**, you're in the right place. - -ImportSpy brings structure and validation to Python's dynamic import system — allowing you to **enforce strict module contracts**, detect **runtime mismatches**, and ensure that every imported module behaves exactly as expected. - -What You’ll Learn 📘 ---------------------- - -This guide walks you through everything you need to **set up, understand, and use ImportSpy effectively**, including: - -- 🧰 **Installation** - Learn how to install ImportSpy using your preferred package manager and verify your setup. - -- 🔍 **Example Overview** - Explore a real-world use case built around a **plugin-based architecture**, and understand how ImportSpy adds reliability to it. - -- 📜 **Import Contracts in YAML** - Understand how ImportSpy uses `.yml` contracts to define what an external module must look like: classes, methods, attributes, environments, and more. - -- ⚙️ **Code Walkthrough** - Dive into a fully working example and see how ImportSpy enforces contracts dynamically at runtime. - -- 🧪 **Validation in Action** - Run the example locally and observe how ImportSpy validates external modules and reports violations clearly and precisely. - -Is This Guide for You? ------------------------ - -Absolutely — if any of these apply to you: - -- ✅ You’ve never used ImportSpy before and want a guided path -- ✅ You work with Python plugins, extensions, or modular projects -- ✅ You want to ensure that external code is predictable and secure -- ✅ You're interested in enforcing structure and compliance at runtime - -What You’ll Need 🛠️ --------------------- - -- Basic familiarity with Python and importing modules -- **Python 3.10 or later** installed on your machine -- A terminal or shell to execute ImportSpy from the command line (optional for CLI mode) -- A willingness to explore and experiment! - -Let’s Build Your First Validated Project ✅ -------------------------------------------- - -In the next steps, you’ll learn how to: - -1. Install ImportSpy in seconds -2. Define your first import contract -3. Validate an external module in both embedded and CLI mode -4. Understand how ImportSpy reacts to structural mismatches -5. Integrate it into a real project - -By the end of this guide, you’ll have a **fully functional ImportSpy environment**, understand the power of import contracts, and feel confident applying them in your own architecture. - -Let’s get started! - -.. toctree:: - :maxdepth: 2 - - get_started/installation - get_started/example_overview - get_started/examples/plugin_based_architecture/index diff --git a/docs/source/get_started/example_overview.rst b/docs/source/get_started/example_overview.rst deleted file mode 100644 index f9c815c..0000000 --- a/docs/source/get_started/example_overview.rst +++ /dev/null @@ -1,65 +0,0 @@ -ImportSpy Examples: Real-World Scenarios in Action 🚀 -===================================================== - -Welcome to the **Examples** section of ImportSpy — where theory meets practice. - -In this space, you'll explore **runnable, real-world demonstrations** showing how ImportSpy helps enforce **structural validation**, **interface consistency**, and **runtime compliance** in modular Python systems. - -Whether you're working with plugins, pipelines, APIs, or layered architectures, these examples provide **blueprints you can adapt** to your own projects. - -Why Examples Matter 🧩 ------------------------ - -Modern Python applications are highly dynamic. -But that flexibility comes with risks: unexpected behaviors, silent failures, and integration mismatches. - -ImportSpy gives you a way to bring **formal guarantees** into the dynamic world of imports. -Here, you'll see exactly how it works — with practical, minimal, and extensible examples. - -How to Use These Examples ⚙️ ------------------------------- - -Each example in this section is: - -- ✅ Self-contained and ready to run -- ✅ Designed around real architectural patterns -- ✅ Focused on one validation principle at a time -- ✅ Ideal for experimentation and adaptation - -To try them: - -1. Ensure you have **ImportSpy installed** - .. code-block:: bash - - pip install importspy - -2. Choose an example that matches your context -3. Run it locally -4. Modify the contract or the code and observe the validation outcomes -5. Learn how ImportSpy blocks invalid imports and reinforces structural safety - -Available Example 💡 ---------------------- - -The first complete walkthrough is: - -📦 :doc:`examples/plugin_based_architecture/index` - -This scenario demonstrates: - -- Defining import contracts -- Handling plugin structure enforcement -- Using both **embedded** and **CLI** validation modes -- Running validation in a pipeline context - -Coming Soon ✨ --------------- - -We're actively expanding this section with more examples, including: - -- API structure validation (FastAPI, Flask) -- Cross-service contract enforcement in microservices -- Schema enforcement in data pipelines -- Security policies for runtime imports - -Want to contribute your own? Reach out or open a PR on GitHub! diff --git a/docs/source/get_started/examples/plugin_based_architecture/external_module_compilance.rst b/docs/source/get_started/examples/plugin_based_architecture/external_module_compilance.rst deleted file mode 100644 index 6753fc9..0000000 --- a/docs/source/get_started/examples/plugin_based_architecture/external_module_compilance.rst +++ /dev/null @@ -1,114 +0,0 @@ -External Module Compliance (Embedded Mode Example) -================================================== - -This example demonstrates one of ImportSpy’s most powerful features: -**embedded validation**, where a module being imported can validate **who is importing it**. - -Unlike traditional tools that validate their own structure, ImportSpy allows a module to **control its consumers** — -ensuring that only fully compliant modules can interact with it. - -Why This Matters 🔐 --------------------- - -In plugin architectures, dynamic systems, or modular platforms, core components are often imported by untrusted or external code. -Without structural guarantees, this opens the door to: - -- Runtime crashes from missing methods -- Silent logic errors due to incompatible extensions -- Unpredictable behaviors across environments - -ImportSpy solves this by allowing the core module to define a **YAML-based contract**, and reject importers that don’t match. - -Use Cases ✅ -~~~~~~~~~~~~ - -- Plugin systems with strict APIs -- Modular backends with third-party integration -- Secure extensions and validation gateways -- Projects needing **controlled extensibility** from external modules - -Project Structure 📁 ---------------------- - -.. code-block:: - - external_module_compliance/ - ├── extension.py # External module trying to import the core - ├── package.py # Core module protected by ImportSpy - ├── plugin_interface.py # Shared interface definition - └── spymodel.yml # Structural contract for external validation - -How It Works 🧠 ----------------- - -1. `extension.py` tries to import `package.py` -2. Inside `package.py`, ImportSpy runs in **embedded mode**: - .. code-block:: python - - caller_module = Spy().importspy(filepath="spymodel.yml") - -3. The contract in `spymodel.yml` defines what `extension.py` must contain (e.g., classes, methods, variables) -4. If the contract is satisfied: - - `caller_module` is assigned to `extension.py` - - The validated importer can be used directly, like: - `caller_module.Foo().get_bar()` -5. If not, ImportSpy raises an error and **prevents usage of the module**. - -Run the Example ▶️ --------------------- - -From the root of the project, run: - -.. code-block:: bash - - cd examples/plugin_based_architecture/external_module_compliance - python extension.py - -Expected Output: - -.. code-block:: text - - Foobar - -This means: - -- The importer (`extension.py`) passed validation -- The core module (`package.py`) verified its importer before doing anything -- You now have **runtime-level confidence** in how the system integrates - -Simulating a Failure ❌ ------------------------- - -To see ImportSpy in action, try this: - -1. Open `spymodel.yml` -2. Modify a method name (e.g., `add_extension` → `add_extension_WRONG`) -3. Run the example again: - -.. code-block:: bash - - python extension.py - -Expected output: - -.. code-block:: text - - ValueError: Missing method in class Extension: 'add_extension_WRONG'. Ensure it is defined. - -🛑 This is **real-time structural enforcement**. -The module is immediately blocked for violating the import contract. - -Key Takeaways 🧩 ------------------ - -- ImportSpy’s **embedded mode** empowers a module to **control who is allowed to import it** -- It guarantees that plugins, extensions, or third-party modules conform to the contract before any code runs -- The returned `caller_module` gives you full access to the validated importer — just like any other module object -- This pattern is ideal when **predictability, structure, and security** are non-negotiable - -Next Steps 🔄 -------------- - -- Try editing the contract and module to explore different validations -- Combine this with :doc:`pipeline_validation` to enforce contracts in CI/CD pipelines -- Read more about embedded mode in :doc:`../../../overview/understanding_importspy/embedded_mode` diff --git a/docs/source/get_started/examples/plugin_based_architecture/index.rst b/docs/source/get_started/examples/plugin_based_architecture/index.rst deleted file mode 100644 index ef1c92a..0000000 --- a/docs/source/get_started/examples/plugin_based_architecture/index.rst +++ /dev/null @@ -1,79 +0,0 @@ -Plugin-Based Architecture: Example Suite Overview -================================================= - -Welcome to the **Plugin-Based Architecture** examples for ImportSpy. -This section showcases how ImportSpy can be integrated into real-world modular systems to ensure **structural integrity**, **runtime compatibility**, and **import-time compliance**. - -Why Plugins Need Validation 🧩 ------------------------------- - -Modern applications are often built around **plugin systems**, **modular services**, or **runtime extensions** — -components that are loaded dynamically and sometimes authored externally. - -Without strict validation, these integrations can lead to: - -- ❌ Unexpected runtime errors -- ❌ Silent logic bugs due to mismatched interfaces -- ❌ Security vulnerabilities in dynamic loading scenarios - -ImportSpy solves this by enforcing **formal contracts** — ensuring that every module that is imported or interacted with follows a precise structure and runtime context. - -What You'll Learn Here 🎯 --------------------------- - -In this section, you’ll explore two complementary validation modes: - -.. list-table:: - :widths: 25 75 - :header-rows: 1 - - * - Validation Mode - - Description - * - Embedded Mode - - Validation is performed **inside the core module** (e.g. `package.py`) that is being imported. - When an external module (like `extension.py`) imports it, the core validates the importer. - Ideal for secure plugin frameworks, APIs, or modular applications. - * - CLI Mode - - Validation is performed **externally via the command line**, using `importspy -s contract.yml module.py`. - Perfect for CI/CD, static enforcement, or pre-deployment checks. - -How to Run the Examples 🛠️ ---------------------------- - -Make sure ImportSpy is installed: - -.. code-block:: bash - - pip install importspy - -Then: - -- 🧪 **Embedded Validation** - .. code-block:: bash - - cd examples/plugin_based_architecture - python extension.py - -- 🧪 **CLI Validation** - .. code-block:: bash - - cd examples/plugin_based_architecture - importspy -s spymodel.yml extension.py - -Try editing the modules or the contract and rerun the validations — -you’ll see how ImportSpy detects mismatches immediately. - -Ready to Dive In? 🚀 --------------------- - -These examples provide a practical foundation for using ImportSpy in your own architecture. -They demonstrate not just how validation works, but **where it fits** in modern Python workflows. - -Navigate to a specific mode to explore: - -.. toctree:: - :maxdepth: 1 - :caption: Validation Modes - - external_module_compilance - pipeline_validation diff --git a/docs/source/get_started/examples/plugin_based_architecture/pipeline_validation.rst b/docs/source/get_started/examples/plugin_based_architecture/pipeline_validation.rst deleted file mode 100644 index 6c0099d..0000000 --- a/docs/source/get_started/examples/plugin_based_architecture/pipeline_validation.rst +++ /dev/null @@ -1,93 +0,0 @@ -Pipeline Validation (CLI Mode Example) -====================================== - -This example demonstrates how to use **ImportSpy** in **CLI mode** to validate a Python module against a declared import contract. - -In this scenario, validation is **external and decoupled** — the module being validated has no awareness of ImportSpy. -This makes it ideal for **CI/CD pipelines**, **automated pre-deployment checks**, or **manual compliance validation** during code review. - -Why This Mode Is Powerful 🎯 ----------------------------- - -Unlike embedded mode (where the validated module uses ImportSpy internally), -CLI mode allows you to **treat validation as an independent, enforceable policy**. - -This is especially useful when: - -- You’re validating **third-party plugins or contributors' code** -- You want **full separation of concerns** between business logic and validation -- You’re integrating ImportSpy into **automated pipelines** - -Project Structure 📁 ---------------------- - -.. code-block:: - - pipeline_validation/ - ├── extension.py # The module to validate - ├── plugin_interface.py # Shared base class expected by the contract - └── spymodel.yml # Contract declaring expected structure and runtime - -How It Works ⚙️ ----------------- - -1. The contract in `spymodel.yml` defines the structure, environment, and runtime context expected from `extension.py` -2. ImportSpy is invoked from the command line to **validate `extension.py` against the contract** -3. If validation passes ✅, the pipeline continues - If it fails ❌, the pipeline halts with an explicit error - -Running the Example ▶️ ------------------------ - -First, make sure ImportSpy is installed: - -.. code-block:: bash - - pip install importspy - -Then run: - -.. code-block:: bash - - cd examples/plugin_based_architecture/pipeline_validation - importspy -s spymodel.yml extension.py - -If the module matches the contract, you’ll see something like: - -.. code-block:: text - - ✅ Validation passed: extension.py complies with contract. - -If it fails, you’ll get a detailed, actionable error: - -.. code-block:: text - - ❌ Validation failed - - Reason: - Missing attribute 'instance' in class 'Extension': extension_instance_name_WRONG - -Try Breaking It 🔧 -------------------- - -To see the validator in action: - -1. Open `spymodel.yml` -2. Change an attribute, method, or variable name (e.g., `add_extension` → `add_extension_WRONG`) -3. Run the command again -4. ImportSpy will immediately detect the structural mismatch and explain why - -Key Takeaways 💡 ------------------ - -- **CLI mode** is perfect for validating modules *before execution* -- You can enforce architectural contracts without modifying the validated code -- Works seamlessly in CI/CD pipelines, GitHub Actions, or any build process -- Makes **structural integrity** a core part of your development workflow - -What’s Next? -------------- - -- Try integrating this step into your CI/CD pipeline (e.g., GitHub Actions or GitLab CI) -- Explore :doc:`external_module_compilance` to learn how embedded mode complements this approach -- Read more about CLI mode in :doc:`../../../overview/understanding_importspy/external_mode` diff --git a/docs/source/get_started/installation.rst b/docs/source/get_started/installation.rst deleted file mode 100644 index 9e21d28..0000000 --- a/docs/source/get_started/installation.rst +++ /dev/null @@ -1,87 +0,0 @@ -Installation Guide -================== - -Welcome to the **Installation Guide** for ImportSpy. -This section will walk you through setting up ImportSpy in your environment — quickly, cleanly, and with confidence. - -ImportSpy is designed to be lightweight and easy to integrate into any Python project that values **runtime validation**, **structural compliance**, and **predictable imports**. - -System Requirements 📌 ------------------------ - -Before you begin, make sure your development environment meets the following requirements: - -- **Python 3.10 or later** - ImportSpy relies on modern Python features and guarantees compatibility only from version 3.10 onward. - -- **pip (latest version)** - To ensure smooth installation and dependency resolution. - -- **Virtual Environment (Recommended)** - While optional, using a virtual environment is best practice for avoiding dependency conflicts and ensuring isolation. - -Installing ImportSpy ⚙️ ------------------------- - -1. **Create and Activate a Virtual Environment** - - While not mandatory, we strongly recommend installing ImportSpy in a virtual environment: - - .. tabs:: - - .. tab:: macOS / Linux - - .. code-block:: bash - - python3 -m venv venv - source venv/bin/activate - - .. tab:: Windows - - .. code-block:: bash - - python -m venv venv - .\venv\Scripts\activate - - Once activated, your terminal should indicate that the environment is active. - -2. **Install ImportSpy with pip** - - Now install ImportSpy directly from PyPI: - - .. code-block:: bash - - pip install importspy - - This command will install the latest stable version of ImportSpy and all required dependencies. - -Verifying the Installation ✅ ------------------------------- - -To confirm that ImportSpy is correctly installed and ready to use, run: - -.. code-block:: bash - - importspy --version - -If everything is set up correctly, the terminal will display the current version of ImportSpy. - -Troubleshooting Tips 🧯 ------------------------- - -If something goes wrong: - -- Ensure you're using **Python 3.10+** -- Activate your virtual environment before running `pip install` -- If needed, upgrade pip: - .. code-block:: bash - python -m pip install --upgrade pip - -You're Ready to Go 🎉 ----------------------- - -That’s it! You’re now ready to start using ImportSpy to enforce module validation in your projects. - -Continue to the next section to explore a working example and see ImportSpy in action: - -📎 :doc:`example_overview` diff --git a/docs/source/index.rst b/docs/source/index.rst deleted file mode 100644 index 1f15e31..0000000 --- a/docs/source/index.rst +++ /dev/null @@ -1,74 +0,0 @@ -Welcome to ImportSpy 🔎 -======================== - -**ImportSpy** is a contract-based runtime validation framework that transforms how Python modules interact—making those interactions **predictable, secure, and verifiable**. -It empowers developers to define, enforce, and validate **import contracts** that describe exactly how a module should behave when it is imported, under specific runtime conditions. - -Whether you're working with **plugin-based systems**, **microservices**, or **cross-platform applications**, ImportSpy gives you **full control over integration boundaries**. -It ensures that the modules importing your code—or the ones you're importing—adhere to **explicit structural and environmental rules**, avoiding silent failures, runtime crashes, or unpredictable behavior. - -🔐 ImportSpy is not just about validation—it’s about **bringing discipline and clarity to the most dynamic part of Python: the import system**. - -Why ImportSpy? 🚀 ------------------- - -- **🧩 Bring Structure to Dynamic Systems** - Enforce well-defined contracts on imported modules: classes, methods, variables, versions, OS, interpreters, and more. - -- **🔍 Runtime-Aware Validation** - Validate modules **based on actual runtime context**—OS, CPU architecture, Python interpreter, and version. - -- **🔌 Built for Plugin Ecosystems** - Protect core logic from integration errors in environments where dynamic loading is common. - -- **🧪 Two Powerful Modes** - In **embedded mode**, validate external modules *that import your code*, enforcing structure and context dynamically. - In **CLI mode**, validate any Python module against a contract—ideal for CI/CD pipelines and automated checks. - -- **📜 Self-Documenting Contracts** - The `.yml` contract files double as **live documentation**, formalizing how modules are expected to behave. - -What You'll Learn From This Documentation 📖 --------------------------------------------- - -This guide is designed to help you: - -- Understand how ImportSpy works and **why it exists** -- Learn how to **define and apply import contracts** -- Explore **real-world use cases** across validation, compliance, CI/CD, security, and IoT integration -- Navigate through **beginner-friendly training material** that introduces reflection, Pydantic, Poetry, and more -- Dive into the **internals** of ImportSpy with detailed API references and architectural insights -- Discover how to **support or sponsor the project** to help it grow - -How to Navigate This Documentation 🧭 -------------------------------------- - -- **👋 New to ImportSpy?** → Start with **Get Started** to see how it works, step by step. -- **📚 Want to understand the bigger picture?** → Visit the **Overview** section to explore the vision, story, and use cases. -- **🧠 Curious about internals?** → Explore **Advanced Documentation** for architecture, runtime analysis, and API design. -- **🎓 Need a learning space?** → Head to the **Beginner Section** to explore tools and practices relevant to ImportSpy. -- **💼 Interested in supporting ImportSpy?** → Visit the **Sponsorship** section to learn how to get involved. - -Let’s build Python software that’s not just flexible, but also **reliable, validated, and future-proof**. -**Welcome to the new standard for structural integration in Python.** - -.. toctree:: - :maxdepth: 2 - :caption: 📌 Core Documentation - - vision - overview - get_started - sponsorship - -.. toctree:: - :maxdepth: 2 - :caption: 🎓 Beginner Resources - - beginner/beginner_index - -.. toctree:: - :maxdepth: 2 - :caption: 🧠 Advanced Topics - - advanced/advanced_index diff --git a/docs/source/overview.rst b/docs/source/overview.rst deleted file mode 100644 index be78402..0000000 --- a/docs/source/overview.rst +++ /dev/null @@ -1,72 +0,0 @@ -Overview of ImportSpy -====================== - -Welcome to the **ImportSpy Overview** — a complete starting point for understanding the *why*, *how*, and *where* of this project. - -ImportSpy was born from a clear need: -> How can we bring **predictability**, **security**, and **structural clarity** to Python’s dynamic import system? - -This section explores not only how ImportSpy works, but also **why it exists**, the **real-world problems it solves**, and **what principles it’s built upon**. Whether you’re a developer, architect, or security engineer, this is where your journey begins. - -What You’ll Find in This Section 📖 ------------------------------------ - -This overview is structured into three key parts, each with a distinct purpose: - -The Story Behind ImportSpy -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -At its core, ImportSpy is more than just a tool — it’s the result of a **personal journey**. -Created by Luca Atella as a response to burnout and routine, ImportSpy emerged from a need to **reclaim joy and meaning in development**. -This section tells that story — not for sentiment, but to show that **structure and purpose can coexist in software**. -*It reminds us that even small tools, built from a place of passion, can change the way we work.* - -📄 :doc:`overview/story` - -Use Cases in the Real World -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -ImportSpy is used wherever **modular boundaries need to be enforced** — from plugin ecosystems to CI/CD pipelines. -This section presents **detailed, practical examples** that show how ImportSpy prevents: - -- Misaligned structures in dynamically loaded components -- Security flaws from unvalidated external modules -- Runtime instability across architectures or Python environments - -Use cases include: - -- ✅ **IoT and platform-specific integration** -- ✅ **Validation & structural integrity in plugin systems** -- ✅ **Security enforcement through runtime checks** -- ✅ **Regulatory compliance for mission-critical modules** - -📄 :doc:`overview/use_cases_index` - -Understanding ImportSpy’s Core -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -This is your **deep dive** into the internal logic and architecture of ImportSpy. - -You’ll learn: - -- What an **import contract** really is, and how to write one -- How the **Spy validation flow** works from import interception to enforcement -- The difference between **embedded mode** and **CLI mode** -- How runtime context (OS, Python version, architecture) plays a role -- How errors are reported and how to fix them -- Best practices for **CI/CD pipelines and integration at scale** - -*This section turns concepts into confidence, and makes ImportSpy a natural extension of your development process.* - -📄 :doc:`overview/understanding_importspy_index` - -Let’s get started by exploring the motivation, capabilities, and inner workings of ImportSpy — -and discover how it can help you build Python software that’s **modular, compliant, and future-ready**. - -.. toctree:: - :maxdepth: 2 - :caption: 🔍 Overview - - overview/story - overview/use_cases_index - overview/understanding_importspy_index diff --git a/docs/source/overview/story.rst b/docs/source/overview/story.rst deleted file mode 100644 index 6e1e141..0000000 --- a/docs/source/overview/story.rst +++ /dev/null @@ -1,91 +0,0 @@ -From Burnout to Reinvention: The Birth of ImportSpy -=================================================== - -In the relentless world of software development, the passion that once sparked creativity -can slowly be consumed by **repetition, stagnation, and burnout**. -What begins as a thrilling craft may turn into an **unfulfilling routine**, -where problem-solving is replaced by **rote tasks and organizational complexity**. - -**ImportSpy was born from this very struggle.** -It is more than just a validation framework—it’s the product of a **deeply personal journey**, -a project that transformed frustration into clarity and reignited a **genuine love for meaningful software**. - -Losing Passion in a World of Repetition ---------------------------------------- - -For **Luca Atella**, creator of ImportSpy, programming was never just a job. -Like many developers, he started young—captivated by the magic of **turning logic into solutions**, -and the joy of **creating something from nothing**. - -But over time, that joy began to fade. -The once-exciting world of coding gave way to **monotony and constraint**. -Innovation was replaced by **repetitive cycles, boilerplate maintenance, and uninspiring work**. - -At just **24 years old**, Luca made a bold move: he walked away from a stable but soulless job. -A leap into uncertainty, driven by the realization that working without passion was no longer sustainable. -But quitting wasn’t the solution—it was just the start of something deeper: -**a period of rediscovery, doubt, and the difficult question—how do you fall in love with your craft again?** - -Rebuilding Passion, One Line at a Time --------------------------------------- - -The answer wasn’t to abandon programming, but to **reclaim it with intention**. -Luca didn’t need to leave software behind—he needed to return to it **on his own terms**. - -Instead of chasing trends or reacting to deadlines, he built something that truly mattered. -The idea behind ImportSpy was simple, yet radical: - -**What if validation wasn’t just a chore? -What if it could be elegant, structural, and empowering?** - -That idea became the foundation of ImportSpy. -More than just a tool, it became a **manifesto**—a new way to bring structure to modular software -while rebuilding the joy of craftsmanship. - -More Than Code: A Community-Driven Journey ------------------------------------------- - -What began as a **personal experiment** quickly evolved into something greater. -ImportSpy isn’t just a validation framework—it’s a **testament to the resilience of developers** -who refuse to let their creativity be buried under routine. - -Luca’s path mirrors that of many developers trapped in roles that stifle innovation— -questioning their purpose in an industry that often rewards **velocity over clarity, -deadlines over design, and output over impact**. - -But ImportSpy offers another way. - -It’s a **reminder** that: - -- **Code should empower, not frustrate.** -- **Learning should be embraced—even when messy.** -- **Passion projects can reshape careers, communities, and industries.** - -ImportSpy represents a **new perspective**—one that values **structure, clarity, and the joy of building software that just works**. - -Why ImportSpy Matters ---------------------- - -ImportSpy isn’t just another dev tool—it’s a **statement**. - -- A statement that **modular software deserves structural validation and compliance by design**. -- A statement that **developers need tools that empower their workflows, not complicate them**. -- A statement that **side projects born from burnout can become catalysts for innovation**. - -Technically, ImportSpy ensures **stability, predictability, and architectural rigor** across your modules. -But its real power lies in its **philosophy**: -**build with purpose, validate with precision, and code with renewed passion**. - -Join the Movement ------------------ - -If you’ve ever felt **burned out, stuck, or disconnected** from your code, -know that you’re **not alone**. - -ImportSpy is more than a framework—it’s a **community-driven project** built on the belief that -software should be **precise, predictable, and a joy to create**. - -This project is a **tribute to all developers** reclaiming their craft and striving to build **better, smarter software**. -Because even the smallest ideas, when built with clarity, can have a **massive impact**. - -**🔹 Reclaim your passion. Build with confidence. Join the movement.** diff --git a/docs/source/overview/understanding_importspy/ci_cd_integration.rst b/docs/source/overview/understanding_importspy/ci_cd_integration.rst deleted file mode 100644 index 14fd2cd..0000000 --- a/docs/source/overview/understanding_importspy/ci_cd_integration.rst +++ /dev/null @@ -1,135 +0,0 @@ -CI/CD Integration -================= - -Modern CI/CD pipelines are powerful — but also fragile. - -Between dynamic environments, dependency drift, and plugin chaos, -it’s easy for code to pass local tests and **still fail at runtime**. - -ImportSpy brings a layer of **predictability, structural assurance**, and **contract enforcement** -to your automated workflows — making sure that every module behaves the way it should, -in the environment where it’s going to run. - -Why CI/CD Needs Structural Validation -------------------------------------- - -Functional tests catch **what your code does**. -ImportSpy ensures that it’s **running in the right place, with the right structure**. - -Without structural validation, pipelines are vulnerable to: - -- ❌ Hidden mismatches in **Python versions**, **interpreters**, or **platforms** -- ❌ Missing or malformed **environment variables** -- ❌ Untracked changes in shared modules or plugins -- ❌ Non-compliant third-party code with unexpected APIs - -These failures often appear **late**, when debugging is slow and costly. -ImportSpy helps you catch them **early**, at build time — not post-deploy. - -Where to Use ImportSpy in CI/CD -------------------------------- - -**1. During the Build Phase 🧱** - -Validate module structure before packaging: - -.. code-block:: bash - - importspy -s spymodel.yml mymodule.py - -This prevents broken contracts from ever making it into an artifact. - -**2. During Testing 🔬** - -Add ImportSpy validation before or alongside your unit tests: - -.. code-block:: bash - - importspy -s spymodel.yml -l ERROR path/to/module.py - -Catch unexpected mutations, missing methods, or API drift as part of CI. - -**3. Before Deployment 🚀** - -Use ImportSpy to verify: - -- Environment constraints (OS, Python, interpreter) -- Runtime assumptions (env vars, module-level variables) -- Plugin compliance across distributed services - -✅ If everything matches the contract, continue. -❌ If anything is wrong, block the deploy. - -Supported CI/CD Platforms --------------------------- - -ImportSpy is CI-native and works anywhere: - -- **GitHub Actions** - Add a step before your test matrix or deployment job. - -- **GitLab CI** - Use it in before_script or as a job stage. - -- **CircleCI / Jenkins** - Run via shell or Python-based jobs. - -- **Docker / Kubernetes** - Validate plugins or runtime images before deployment. - -- **Legacy or VM pipelines** - Enforce stability even in less dynamic stacks. - -Minimal Example for GitHub Actions ----------------------------------- - -.. code-block:: yaml - - - name: Validate Plugin - run: | - pip install importspy - importspy -s spymodel.yml extension.py - -Any contract violations will raise a `ValueError` and halt the build. - -Security Benefits ------------------- - -ImportSpy also strengthens your **software supply chain**: - -- Blocks unexpected or tampered code -- Prevents unauthorized plugin registration -- Confirms that runtime conditions are exactly what you expect -- Complements tools like `pip-audit`, `bandit`, or SAST engines - -Think of it as **import-time policy enforcement**, directly in your build. - -Best Practices for Integration ------------------------------- - -- 🔐 Treat ImportSpy as a **quality gate**, not just a linter -- 💥 Use `-l ERROR` log level to fail fast and get clear diagnostics -- 🔁 Keep contracts under version control with your code -- 🧪 Validate early, not just at release -- 🧭 Use strict contracts in production, relaxed ones in dev/test - -Related Topics --------------- - -- :doc:`contract_structure` – How to write import contracts -- :doc:`external_mode` – Running validation externally -- :doc:`spy_execution_flow` – See what happens during validation - -Summary -------- - -ImportSpy turns fragile CI pipelines into **predictable safety systems**. - -It guarantees that: - -- ✅ Every module is structurally sound -- ✅ Every environment matches your expectations -- ✅ Every build is trustworthy - -No more surprises. No more silent regressions. -Just clean, validated, future-proof Python — every time you deploy. diff --git a/docs/source/overview/understanding_importspy/contract_structure.rst b/docs/source/overview/understanding_importspy/contract_structure.rst deleted file mode 100644 index 3b47b8f..0000000 --- a/docs/source/overview/understanding_importspy/contract_structure.rst +++ /dev/null @@ -1,176 +0,0 @@ -Import Contract Structure -========================== - -Import contracts are the foundation of how ImportSpy performs validation. - -They are **YAML-based configuration files** that describe both the **structure** of a Python module and the **execution environments** in which it is valid. - -This page provides a deep dive into the schema, semantics, and flexibility of import contracts — and how they serve as **executable specifications** for modular systems. - -Overview --------- - -Each contract is made up of two primary blocks: - -- **Module definition**: describes what the module must contain (e.g., classes, functions, metadata) -- **Deployments**: lists the environments (OS, Python, interpreter) in which the module is allowed to run - -Contracts can define either: - -- **Global constraints**: structural requirements that apply to all deployments -- **Deployment-specific overrides**: context-sensitive rules based on platform or interpreter - -Top-Level Schema ----------------- - -Here is a reference of the main fields in a contract: - -Top-Level Fields -~~~~~~~~~~~~~~~~ - -- ``filename`` *(str)*: The name of the module to validate -- ``version`` *(str, optional)*: Expected module version (e.g., `__version__`) -- ``variables`` *(dict)*: Top-level variables and their expected values -- ``functions`` *(list)*: List of required functions -- ``classes`` *(list)*: List of required classes and their details -- ``deployments`` *(list)*: Permitted environments in which the module can be loaded - -Function Schema -~~~~~~~~~~~~~~~ - -Each function can define: - -- ``name``: Function name -- ``arguments``: List of parameter names and optional annotations -- ``return_annotation``: Optional return type annotation - -Class Schema -~~~~~~~~~~~~ - -Each class may include: - -- ``name``: Class name -- ``attributes``: - - ``type``: `"instance"` or `"class"` - - ``name``: Attribute name - - ``value``: Expected value - - ``annotation``: Optional type annotation -- ``methods``: Follows the same format as functions -- ``superclasses``: List of required base classes - -Deployments Block ------------------- - -The ``deployments`` section defines runtime compatibility requirements: - -- ``arch``: CPU architecture (e.g., `x86_64`, `arm64`) -- ``systems``: list of operating systems and environment constraints - - ``os``: `linux`, `windows`, or `macos` - - ``envs``: Required environment variables (`KEY: value`) - - ``pythons``: Supported Python versions and interpreters - - ``version``: Python version (e.g., `3.12.8`) - - ``interpreter``: `CPython`, `PyPy`, etc. - - ``modules``: Nested module definitions required in that context - -Global Module Definition (Baseline) ------------------------------------ - -If you define a module structure **outside the `deployments:` section**, -it acts as a **global baseline** — a structural requirement that applies to **all environments**. - -This section can include: - -- ``filename``, ``variables``, ``functions``, ``classes`` -- Shared interface contracts across platforms -- The minimum valid structure for the module to ever be imported - -.. note:: - This is semantically treated as a **lower bound**: - each deployment must satisfy the global structure plus any deployment-specific overrides. - -Full Example ------------- - -Here is a complete import contract: - -.. code-block:: yaml - - filename: extension.py - variables: - engine: docker - plugin_name: plugin name - plugin_description: plugin description - classes: - - name: Extension - attributes: - - type: instance - name: extension_instance_name - value: extension_instance_value - - type: class - name: extension_name - value: extension_value - methods: - - name: __init__ - arguments: - - name: self - - name: add_extension - arguments: - - name: self - - name: msg - annotation: str - return_annotation: str - - name: remove_extension - arguments: - - name: self - - name: http_get_request - arguments: - - name: self - superclasses: - - Plugin - - name: Foo - methods: - - name: get_bar - arguments: - - name: self - deployments: - - arch: x86_64 - systems: - - os: windows - pythons: - - version: 3.12.8 - interpreter: CPython - modules: - - filename: extension.py - variables: - author: Luca Atella - - version: 3.12.4 - modules: - - filename: addons.py - - interpreter: IronPython - modules: - - filename: addons.py - - os: linux - pythons: - - version: 3.12.8 - interpreter: CPython - modules: - - filename: extension.py - variables: - author: Luca Atella - -Validation Behavior --------------------- - -- All fields are **optional**, but the **hierarchy must be respected** -- Missing fields are simply skipped during validation -- Order of items in lists (methods, attributes) is **not enforced** -- Contracts are parsed into `SpyModel` objects during validation -- Validation is consistent across both embedded and CLI modes - -Related Topics --------------- - -- :doc:`defining_import_contracts` -- :doc:`spy_execution_flow` -- :doc:`embedded_mode` -- :doc:`external_mode` diff --git a/docs/source/overview/understanding_importspy/defining_import_contracts.rst b/docs/source/overview/understanding_importspy/defining_import_contracts.rst deleted file mode 100644 index 3c858e8..0000000 --- a/docs/source/overview/understanding_importspy/defining_import_contracts.rst +++ /dev/null @@ -1,156 +0,0 @@ -Defining Import Contracts -========================== - -Import contracts are at the core of how ImportSpy enforces structural and runtime compliance. - -They are **YAML-based declarations** that describe exactly how a Python module is expected to behave — not in terms of logic, but in terms of **structure, compatibility, and execution context**. - -This page explains how to define contracts, what they contain, and how ImportSpy uses them to validate modules at import time. - -What Are Import Contracts? --------------------------- - -An import contract tells ImportSpy: - -- 🔧 What a module **must contain** (functions, classes, attributes, variables) -- 🧠 Where it **is allowed to run** (Python version, OS, architecture, interpreter) -- 🔐 What **runtime conditions** must be met (environment variables, deployment setups) - -ImportSpy uses this contract to decide: -> “Should I allow this module to be used — or block it immediately?” - -Contracts are used in both validation modes: - -- **Embedded mode**: the validated module validates the importer at runtime -- **CLI mode**: a module is validated externally via `importspy -s contract.yml module.py` - -When to Use Them ----------------- - -Use import contracts when: - -- You need to **block incompatible modules** from being loaded -- You want to define **clear structure and interface expectations** -- Your code must **run only on certain platforms or interpreters** -- You’re building a system with **extensible plugins or dynamic modules** -- You need **environment-aware validation** during CI/CD or deployment - -Import Contract Anatomy ------------------------- - -Import contracts follow a hierarchical schema, with two main areas: - -Structure Requirements -~~~~~~~~~~~~~~~~~~~~~~ - -This defines what the module must expose: - -- `filename`: expected module file name -- `variables`: global-level constants or metadata -- `functions`: expected functions (with arguments and annotations) -- `classes`: expected classes, with methods, attributes, and superclasses - -Deployment Constraints -~~~~~~~~~~~~~~~~~~~~~~ - -This defines **where and under what conditions** the module is valid: - -- `arch`: expected CPU architecture (e.g., `x86_64`, `arm64`) -- `systems`: list of supported OS/platform combinations - - `os`: operating system (e.g., `linux`, `windows`) - - `envs`: required environment variables - - `pythons`: Python runtime environments - - `version`, `interpreter` (e.g., `CPython`, `PyPy`) - - `modules`: nested modules required in that Python env - -Contracts can declare **multiple deployments**, supporting flexibility while enforcing strict constraints. - -Baseline Constraints (Global Scope) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If you define module-level fields **outside the `deployments:` section**, -they act as **baseline constraints** that apply to **all runtime environments**. - -This is useful when: - -- You want to enforce a shared structure across multiple deployments -- The module must always expose certain classes, functions, or variables -- You need a "lowest common denominator" for all environments - -.. note:: - These top-level rules define a **lower bound** for compliance — - each `deployment` can extend or specialize these expectations, - but never violate or ignore the top-level contract. - -Minimal Example ----------------- - -Here’s a basic contract example: - -.. code-block:: yaml - - filename: extension.py - variables: - plugin_name: core - classes: - - name: Extension - attributes: - - type: class - name: extension_name - value: core - methods: - - name: __init__ - arguments: - - name: self - - name: run - arguments: - - name: self - - name: data - annotation: str - return_annotation: str - deployments: - - arch: x86_64 - systems: - - os: linux - envs: - ENV: production - pythons: - - version: 3.12.8 - interpreter: CPython - modules: - - filename: extension.py - variables: - author: Luca Atella - -This contract: - -- Requires a class `Extension` with specific methods and attributes -- Enforces Linux OS, CPython 3.12.8, and a production environment -- Declares `extension.py` as the expected module file -- Requires a top-level variable `plugin_name` - -Design Principles ------------------- - -- Contracts are **declarative**: no logic, only structure and expectations -- All fields are optional — but if declared, they **must be met** -- Lists like `classes`, `methods`, and `attributes` are **order-independent** -- Contracts are parsed once per session for performance -- Contracts can express **baseline rules** (outside deployments) or per-environment logic - -Best Practices --------------- - -- Start minimal: validate structure first, then layer on environment constraints -- Version your contracts alongside your code — they are **enforceable documentation** -- Use deployments to support different runtime contexts while keeping control -- Always define `filename` — it's the root entry point for validation - -What’s Next? -------------- - -Now that you understand how to define contracts: - -- See how ImportSpy executes validation in :doc:`spy_execution_flow` -- Explore common validation patterns in the :doc:`validation_and_compliance` section -- Learn how contracts behave in :doc:`embedded_mode` and :doc:`external_mode` diff --git a/docs/source/overview/understanding_importspy/embedded_mode.rst b/docs/source/overview/understanding_importspy/embedded_mode.rst deleted file mode 100644 index a6c6801..0000000 --- a/docs/source/overview/understanding_importspy/embedded_mode.rst +++ /dev/null @@ -1,127 +0,0 @@ -Embedded Mode -============= - -Embedded mode allows a Python module to **protect itself** at runtime. - -Unlike external validation, where checks are triggered from outside, embedded mode runs ImportSpy **from within the module**, -verifying whether the environment that imported it complies with a declared contract. - -It’s a powerful mechanism to ensure that **your module only runs in safe, predictable, and validated contexts**. - -What Is Embedded Validation? ------------------------------ - -In embedded mode, the validated module: - -- ✅ Includes ImportSpy directly in its own code -- ✅ Loads a local `.yml` import contract (e.g., `spymodel.yml`) -- ✅ Introspects the caller (who is importing it) -- ✅ Validates the **importing environment**, not itself -- ❌ Refuses to execute if validation fails - -This is ideal for: - -- Plugins in plugin-based architectures -- Shared packages used across teams or platforms -- Sensitive modules with **runtime assumptions** (OS, interpreter, env vars) -- Security-hardened components - -How It Works ------------- - -Here’s the execution flow: - -1. 🧠 The module runs `Spy().importspy(...)` when imported -2. 📁 It parses its import contract (`spymodel.yml`) -3. 👀 It introspects the **importing module** (via stack trace) -4. 🔍 The importing context is matched against the contract: - - OS, CPU, Python version, interpreter - - Required env vars - - Module structure and metadata -5. ❌ If validation fails, a `ValueError` is raised and execution is blocked -6. ✅ If validation passes, the importing module is returned and can be used programmatically - -🔒 This creates a **Zero-Trust contract gate** — your module is only usable when the importing context is compliant. - -Example Usage --------------- - -Inside your protected module (e.g., `package.py`): - -.. code-block:: python - - from importspy import Spy - import logging - - caller_module = Spy().importspy(filepath="spymodel.yml", log_level=logging.DEBUG) - - # You now have access to the validated importer - caller_module.Foo().get_bar() - -Minimal Contract Example -------------------------- - -Here’s a simplified import contract for embedded validation: - -.. code-block:: yaml - - filename: extension.py - variables: - plugin_name: my_plugin - classes: - - name: Extension - methods: - - name: run - arguments: - - name: self - deployments: - - arch: x86_64 - systems: - - os: linux - pythons: - - version: 3.12.8 - interpreter: CPython - -This contract says: - -- Only modules named `extension.py` -- With a class `Extension` containing a `run(self)` method -- Are allowed to import this module -- Only on Linux + x86_64 + Python 3.12.8 + CPython - -If even one condition is not satisfied, execution is halted immediately. - -Why Use Embedded Mode? ------------------------ - -- ✅ The module validates **who is importing it** -- ✅ Ensures runtime safety without relying on external checks -- ✅ Makes plugins and extensions **self-defensive** -- ✅ Protects against unverified execution contexts in dynamic systems -- ✅ Integrates smoothly into plugin registries or dynamic loaders - -Best Practices --------------- - -- Always run embedded validation **at the top** of your module -- Version control both the module and its contract together -- Use detailed contracts in production, relaxed ones in dev/test -- Log validation steps using `log_level=logging.DEBUG` for traceability - -Comparison to External Mode ----------------------------- - -Use embedded mode when: - -- You want **tight control over where your module is used** -- You are building a **plugin** or **shared extension** -- You need to **validate the importing environment**, not just structure - -Use :doc:`external_mode` when you want to validate a module from the outside (e.g., in CI/CD). - -Related Topics --------------- - -- :doc:`contract_structure` – Learn how to define rich, nested import contracts -- :doc:`spy_execution_flow` – Understand how validation works under the hood -- :doc:`external_mode` – External validation for static and pipeline use cases diff --git a/docs/source/overview/understanding_importspy/error_handling.rst b/docs/source/overview/understanding_importspy/error_handling.rst deleted file mode 100644 index f06649b..0000000 --- a/docs/source/overview/understanding_importspy/error_handling.rst +++ /dev/null @@ -1,153 +0,0 @@ -Error Handling in ImportSpy -============================ - -Validation errors are not failures — they are **enforced expectations**. - -ImportSpy treats every contract violation as a **signal**, not just a disruption. -Its error system is designed to be **precise, informative, and traceable**, -helping developers identify and resolve problems early, consistently, and with confidence. - -Why Structured Errors Matter ----------------------------- - -In complex Python systems, especially those using plugins, microservices, or dynamic loading, -errors can be vague and hard to reproduce. - -ImportSpy solves this by generating: - -- 🧠 **Human-readable messages** with contextual hints -- 🧩 **Categorized errors**, sorted by validation layer -- 🛠️ **Diagnostic templates** that identify the cause and expected structure -- 🔎 **Traceable exceptions**, usable in CLI, IDE, or CI pipelines - -Whether you’re debugging a failing import or enforcing a strict policy in production, -ImportSpy makes validation feedback **clear, consistent, and useful**. - -Error Categories ------------------ - -ImportSpy groups errors into well-defined categories to simplify resolution: - -Missing Elements -~~~~~~~~~~~~~~~~ - -Raised when a required function, class, attribute, or variable is **not present**. - -Example: -`Missing method in class Extension: 'run'` - -Type or Value Mismatch -~~~~~~~~~~~~~~~~~~~~~~~ - -Triggered when types, annotations, or literal values do not match. - -Example: -`Return type mismatch: expected 'str', found 'None'` - -Environmental Misconfiguration -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Raised when runtime assumptions are unmet, such as: - -- Missing environment variables -- Incompatible OS or interpreter -- Python version mismatch - -Example: -`Missing required environment variable: API_KEY` - -Unsupported Runtime -~~~~~~~~~~~~~~~~~~~ - -Validation fails if the runtime environment does not match any declared `deployment`. - -Example: -`Unsupported Python version: 3.7. Expected: 3.12.8` - -The goal of these categories is to **pinpoint root causes** and prevent regression over time. - -Reference Error Table ----------------------- - -All known validation errors are defined in a centralized table: - -.. include:: error_table.rst - -Each entry includes: - -- A symbolic error constant (e.g., `Errors.CLASS_ATTRIBUTE_MISSING`) -- A dynamic message template -- A short description and suggested resolution - -These errors are **reused consistently across embedded and CLI validation**. - -Enforcement Strategies ------------------------ - -ImportSpy enforces contracts in strict mode by default: - -Strict Mode (Default) -~~~~~~~~~~~~~~~~~~~~~~ - -- ❌ Any error raises a `ValueError` -- ⛔ Execution halts immediately -- 🔐 Recommended for CI/CD, production, and regulated systems - -Soft Mode (Future Feature) -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -- ⚠️ Errors are downgraded to warnings -- 🔁 Execution proceeds -- 🧪 Ideal for development, onboarding, or exploratory validation - -Traceability and Debugging ---------------------------- - -Every raised exception includes: - -- The failing **element** (function, class, attribute, etc.) -- The **context** of the violation (e.g., deployment block or module scope) -- The **expected vs actual** values/types - -Thanks to integrated logging (`LogManager`) and specific exception classes (`ValidationError`, `PersistenceError`), -ImportSpy ensures traceability across: - -- Local debugging -- Containerized runtimes -- CI pipelines -- Logging dashboards - -Developer-Focused Feedback ---------------------------- - -Validation errors are formatted to be helpful across: - -- Terminal sessions and shell scripts -- IDE consoles with embedded validation -- CI output logs for quality gates or metrics - -If you're using ImportSpy in CLI mode, errors are printed with full detail — -ready to be parsed, logged, or even turned into automated reports. - -Best Practices --------------- - -- ✅ Run with `--log-level DEBUG` to get full trace on failure -- ✅ Keep `spymodel.yml` in version control and in sync with module changes -- ✅ Use error messages as checklists during onboarding or code review -- ✅ Integrate the error table into your internal docs or linting rules - -Errors Are Enforced Contracts ------------------------------- - -ImportSpy’s validation model is **contract-first** — if a rule is declared, it’s enforced. - -That means errors are not just problems — they’re confirmations that the system is working. -By treating every validation failure as a source of insight, ImportSpy helps your team: - -- Identify problems early -- Understand context clearly -- Move toward stronger modularity and runtime safety - -📌 Errors are not interruptions. -They are the **boundary where safety begins**. diff --git a/docs/source/overview/understanding_importspy/error_table.rst b/docs/source/overview/understanding_importspy/error_table.rst deleted file mode 100644 index 12f11e1..0000000 --- a/docs/source/overview/understanding_importspy/error_table.rst +++ /dev/null @@ -1,42 +0,0 @@ -.. list-table:: ImportSpy Validation Errors - :widths: 30 70 - :header-rows: 1 - - * - **Error Type** - - **Description** - * - `Missing Elements` - - A required **function**, **class**, **method**, or **attribute** is not found in the module or structure defined in the import contract. - * - `Type Mismatch` - - A return annotation, argument type, or class attribute type does **not match** the one declared in the contract. - * - `Value Mismatch` - - A variable or attribute exists but has a **different value** than expected (e.g., metadata mismatch). - * - `Function Argument Mismatch` - - A function's arguments do **not match in name, annotation, or default values**. - * - `Function Return Type Mismatch` - - The return type annotation of a function differs from the contract. - * - `Class Missing` - - A required class is **absent** from the module. - * - `Class Attribute Missing` - - One or more declared **class or instance attributes** are missing. - * - `Class Attribute Type Mismatch` - - A class attribute exists, but its **type or annotation** differs from what is expected. - * - `Superclass Mismatch` - - A class does not inherit from one or more required **superclasses** as declared. - * - `Variable Missing` - - A required **top-level variable** (e.g., `plugin_name`) is not defined in the module. - * - `Variable Value Mismatch` - - A variable exists but its value does not match the one declared in the contract. - * - `Filename Mismatch` - - The actual filename of the module differs from the one declared in `filename`. - * - `Version Mismatch` - - The module’s `__version__` (if defined) differs from the expected version. - * - `Unsupported Operating System` - - The current OS is **not included** in the allowed platforms (e.g., Linux, Windows, macOS). - * - `Missing Required Runtime` - - A required **architecture, OS, or interpreter version** is not satisfied. - * - `Unsupported Python Interpreter` - - The current interpreter (e.g., CPython, PyPy, IronPython) is not supported by the contract. - * - `Missing Environment Variable` - - A declared environment variable is **not present** in the current context. - * - `Invalid Environment Variable` - - An environment variable exists but contains an **unexpected value**. diff --git a/docs/source/overview/understanding_importspy/external_mode.rst b/docs/source/overview/understanding_importspy/external_mode.rst deleted file mode 100644 index 8aee7b5..0000000 --- a/docs/source/overview/understanding_importspy/external_mode.rst +++ /dev/null @@ -1,119 +0,0 @@ -External Mode -============= - -External mode allows you to use ImportSpy as a **standalone validator**, without embedding any logic in the module being validated. - -It’s ideal for teams who want to enforce structure and runtime compliance from the outside — -during **CI checks**, **code review gates**, or **manual inspections** of dynamic modules, plugins, or extensions. - -What Is External Validation? ----------------------------- - -In this mode, ImportSpy runs from the command line and: - -- Loads the target module dynamically -- Parses a separate **YAML import contract** -- Validates the module’s **structure**, **metadata**, and **runtime compatibility** -- Blocks execution if any contract rule is violated - -It’s perfect for use cases where **you don’t own the module**, or want to **validate before running anything at all**. - -Typical Use Cases ------------------- - -- ✅ Pre-deployment contract checks in CI/CD pipelines -- ✅ Validating plugins before registering them in a host application -- ✅ Enforcing runtime assumptions for sandboxed or remote code -- ✅ Auditing third-party extensions for structural and environmental compliance - -How to Use It -------------- - -1. Write your import contract (usually `spymodel.yml`): - -.. code-block:: yaml - - filename: extension.py - classes: - - name: Extension - methods: - - name: run - arguments: - - name: self - -2. Run the validation using the ImportSpy CLI: - -.. code-block:: bash - - importspy -s spymodel.yml -l DEBUG extension.py - -This will: - -- Load `extension.py` -- Parse `spymodel.yml` -- Validate all structure, types, env vars, OS, interpreter, and Python version -- Print any errors or mismatches to the terminal -- Exit with an error if validation fails - -Full CLI Reference -------------------- - -.. code-block:: text - - Usage: importspy [OPTIONS] [MODULEPATH] - - CLI command to validate a Python module against a YAML-defined import contract. - - Arguments: - modulepath Path to the Python module to load and validate. - - Options: - --version, -v Show ImportSpy version and exit. - --spymodel, -s TEXT Path to the import contract file (.yml). [default: spymodel.yml] - --log-level, -l [DEBUG|INFO|WARNING|ERROR] - Log level for output verbosity. - --install-completion Install completion for the current shell. - --show-completion Output shell snippet for autocompletion. - --help Show this message and exit. - -How External Validation Works 🔍 --------------------------------- - -Here’s what happens under the hood: - -1. 📥 **Contract is loaded** → Parsed from YAML into an internal `SpyModel` -2. 🧠 **Module is dynamically loaded** → No execution is triggered, just inspection -3. 🏗️ **Structure is reconstructed** → Classes, methods, attributes, annotations, etc. -4. 🌐 **Runtime context is gathered** → OS, architecture, interpreter, Python version, env vars -5. ⚖️ **Contract is evaluated** → Actual vs expected values are compared deeply -6. ❌ **Violations are raised** → A detailed `ValueError` is thrown with full diagnostics - -All of this happens **before any code is executed**, ensuring a safe, validated runtime context. - -Best Practices 🧪 ------------------ - -- Keep `.yml` contracts **under version control** -- Integrate into **CI/CD** to block broken modules from reaching production -- Use `--log-level DEBUG` to get full trace information when testing -- Validate all external plugins **before dynamic loading** -- Combine with :doc:`contract_structure` for clean, declarative specs - -Comparison to Embedded Mode ----------------------------- - -External mode: - -- ✅ Validates modules **without modifying them** -- ✅ Decouples validation logic from business logic -- ✅ Ideal for **automated pipelines** and **security reviews** - -If you want the **imported module to enforce rules about its importer**, -see :doc:`embedded_mode`. - -Related Topics --------------- - -- :doc:`contract_structure` – Full breakdown of contract syntax and nesting -- :doc:`spy_execution_flow` – Internals of validation lifecycle -- :doc:`embedded_mode` – For runtime protection from inside the validated module diff --git a/docs/source/overview/understanding_importspy/integration_best_practices.rst b/docs/source/overview/understanding_importspy/integration_best_practices.rst deleted file mode 100644 index ee975e9..0000000 --- a/docs/source/overview/understanding_importspy/integration_best_practices.rst +++ /dev/null @@ -1,144 +0,0 @@ -Integration Best Practices -=========================== - -ImportSpy is most powerful when it’s seamlessly integrated into your development lifecycle. -It acts as a **structural firewall** — ensuring that Python modules are only executed in validated environments, -with predictable interfaces and runtime guarantees. - -To get the most out of ImportSpy, it’s essential to follow practices that promote **clarity, maintainability**, -and long-term compliance. - -Contract Design Principles ---------------------------- - -A good import contract is: - -- 🧠 **Readable** → Easy to understand by developers and reviewers -- 🔁 **Reusable** → Avoids repetition by isolating shared environments -- 🔧 **Maintainable** → Easy to update as your system evolves -- 🎯 **Targeted** → Matches how your code is actually deployed, not just idealized setups - -Design your contracts with these goals in mind — and treat them as part of your project’s architecture. - -Modularize Your Contracts 🧱 ----------------------------- - -Avoid monolithic `.yml` files with everything mixed together. - -Instead: - -- Create **separate `deployments:` blocks** for each OS, architecture, or runtime -- Group constraints based on **real deployment contexts** (e.g., CI, Docker, staging) -- Keep **top-level structure global**, and specialize deeper in deployment-specific modules -- Use **baseline declarations** to enforce a minimum structure across all environments - -This makes your contracts scalable, and keeps them aligned with your actual execution model. - -Match Contracts to Real Environments ⚙️ ----------------------------------------- - -Don't write constraints that don't reflect reality. - -- ✅ In production: lock down OS, interpreter, variables, and structure -- ⚠️ In development: relax constraints slightly for flexibility -- 🧪 In CI: validate structure early, fail fast, and log everything - -ImportSpy’s power comes from accuracy — so your contract should describe **how your code really runs**, not how you wish it did. - -Reduce Duplication 🔁 ----------------------- - -Avoid repeating validation rules between modules. - -Strategies: - -- Define shared `deployments:` blocks and reuse them across multiple contracts -- Use generation tools to inject common blocks -- Extract reusable parts (e.g., shared classes or envs) into templated components - -Keeping contracts DRY improves maintainability and reduces the chance of silent mismatches. - -Structure Contracts Clearly 🏗️ -------------------------------- - -A recommended hierarchy: - -1. `filename`, `version`, and top-level `variables` -2. `functions` (optional, with argument specs) -3. `classes`, with: - - `attributes` (instance/class, with optional types and values) - - `methods`, defined like functions - - `superclasses` -4. `deployments`, each with: - - `arch`, `os`, and optional `envs` - - `pythons`, with version/interpreter/modules - - `modules`, repeating structure at the environment level if needed - -This structure allows deep validation of runtime-specific behavior. - -Validate Where It Matters 🌍 ----------------------------- - -ImportSpy is perfect for verifying: - -- Plugins or dynamic modules running in cloud or hybrid environments -- Modules that depend on env-specific config (e.g., secrets, endpoints) -- Microservices where drift between containers or hosts can break integrations - -Explicitly declare: - -- OS and architecture -- Required environment variables -- Supported Python versions and interpreters - -Consistency across environments starts with precise expectations. - -Embed ImportSpy in Your Pipeline 🧪 ------------------------------------ - -Use the CLI tool during CI builds and before deployments: - -.. code-block:: bash - - importspy -s spymodel.yml -l DEBUG path/to/module.py - -Fail the build if the contract isn't satisfied. -This catches integration issues **before** they reach staging or production. - -Consider combining ImportSpy with: - -- Linting (e.g., `ruff`, `flake8`) -- Typing (e.g., `mypy`) -- Unit and integration tests -- Security scanners - -ImportSpy adds **structural guarantees** on top of these tools. - -Choose Enforcement Mode Strategically 👥 ----------------------------------------- - -ImportSpy supports strict and soft enforcement: - -- **Strict Mode** → Violations raise `ValueError`. Use in CI and production. -- **Debug Logging** → Add `--log-level DEBUG` to trace without halting execution. -- **Soft Mode** (planned) → Logs validation failures as warnings. Ideal for onboarding or dry runs. - -Adapt validation levels to your team's tolerance for risk and your deployment maturity. - -Final Advice 🎯 ---------------- - -ImportSpy is not a replacement for testing — it complements it. - -It ensures your modules are used **only where and how they’re meant to be**, -preventing drift, mismatches, and unexpected runtime behavior. - -To integrate ImportSpy effectively: - -- 📁 Keep contracts clean and modular -- 🔄 Update them alongside the code they protect -- ⚙️ Match them to real-world runtimes -- 🚦 Automate validation in CI/CD -- 🔐 Use strict enforcement in trusted production pipelines - -ImportSpy helps you build **modular, secure, and future-proof Python systems** — one contract at a time. diff --git a/docs/source/overview/understanding_importspy/introduction.rst b/docs/source/overview/understanding_importspy/introduction.rst deleted file mode 100644 index 1b5dfab..0000000 --- a/docs/source/overview/understanding_importspy/introduction.rst +++ /dev/null @@ -1,113 +0,0 @@ -Introduction to ImportSpy -========================== - -Welcome to the core introduction to **ImportSpy** — -a validation and compliance framework that transforms Python's dynamic import system into a structured, predictable process. - -ImportSpy enables developers to define **executable contracts** that external modules must follow. -These contracts describe not only a module's expected structure, but also its **execution environment**, including: - -- Python version -- Interpreter type (e.g., CPython, PyPy, IronPython) -- Operating system -- Required classes, functions, variables -- Environment variables and metadata - -If the contract is not respected — the module doesn’t load. -It’s as simple and powerful as that. - -What Problem Does ImportSpy Solve? 🚧 -------------------------------------- - -Python is known for flexibility — but that comes at a cost: - -- Modules can silently drift from expected interfaces -- Plugin systems can misbehave if assumptions aren’t validated -- Deployment environments may differ in subtle, breaking ways -- Runtime errors often appear too late, and debugging them is slow and painful - -ImportSpy introduces **import-time validation**, enforcing that: - -✅ A module’s structure is as expected -✅ Its runtime context matches predefined constraints -✅ Violations are caught **before execution** begins - -The result? Safer systems, clearer boundaries, and predictable integrations. - -What Are Import Contracts? 📜 ------------------------------- - -Import contracts are YAML-based documents that define the rules a module must follow to be considered valid. - -They declare: - -- Required classes, methods, and attributes -- Module-level variables and metadata -- Expected Python interpreter, version, OS, and CPU architecture -- Environmental assumptions (e.g., required env vars) - -At runtime, ImportSpy parses these contracts and validates them **against the actual module and environment**. -These contracts serve as: - -- 🔍 Executable specifications -- 📖 Documentation for expected interfaces -- 🛡️ Runtime validation logic - -The result is a **formal, testable boundary** between modules — especially in dynamic systems like plugin frameworks. - -Validation Modes Supported 🔁 ------------------------------- - -ImportSpy supports two complementary modes of validation: - -.. list-table:: - :widths: 25 75 - :header-rows: 1 - - * - Mode - - Description - * - Embedded Mode - - The core module validates the structure of the **importer**. - Useful in plugin architectures where the base module ensures it is being imported in a safe, compliant context. - * - CLI Mode - - Validation is performed externally via the command line. - Ideal for pipelines, static checks, and CI/CD integration. - -This flexibility allows ImportSpy to adapt to **runtime and pre-deployment validation scenarios** with equal precision. - -What You’ll Learn in This Documentation 📘 ------------------------------------------- - -This documentation will guide you through: - -- 🚀 How ImportSpy works and why it matters -- 🛠️ How to define and apply import contracts -- 🔄 How validation is triggered in both modes -- 🧪 Real-world examples with plugins and pipelines -- ⚙️ Best practices for integration in production systems -- 🔐 Security benefits and enforcement patterns -- 💼 How to use ImportSpy in automated CI/CD workflows - -Installing ImportSpy ----------------------- - -To get started, install ImportSpy using pip: - -.. code-block:: bash - - pip install importspy - -Then visit: - -- :doc:`../../get_started/installation` to set up your environment -- :doc:`../../get_started/example_overview` to run your first validated example - -Let’s Get Started 🚀 ---------------------- - -ImportSpy turns Python’s imports into a **secure contract** — not just a hope for compatibility. - -By shifting validation **before execution**, it empowers developers to build modular, extensible, and production-safe Python systems. - -Ready to unlock the next level of confidence in your code? -Start by defining your first import contract and let ImportSpy take care of the rest. diff --git a/docs/source/overview/understanding_importspy/spy_execution_flow.rst b/docs/source/overview/understanding_importspy/spy_execution_flow.rst deleted file mode 100644 index 2e6cc94..0000000 --- a/docs/source/overview/understanding_importspy/spy_execution_flow.rst +++ /dev/null @@ -1,115 +0,0 @@ -Spy Execution Flow -=================== - -At the heart of ImportSpy lies a powerful validation engine that activates the moment an import occurs. - -Whether you're using **embedded mode** or **CLI validation**, ImportSpy reconstructs a full picture of the runtime environment, compares it to your declared import contract, and enforces compliance **before execution begins**. - -This page explains, step by step, how ImportSpy processes a validation request — from **introspection to enforcement**. - -Overview --------- - -ImportSpy enforces a simple but strict rule: - -> ❌ If the importing environment does not match the contract, the module is blocked. -> ✅ If the environment is compliant, execution proceeds normally. - -This model brings **predictability and control** to Python's otherwise dynamic import system. - -Execution Modes Supported --------------------------- - -ImportSpy works in two execution modes: - -- :doc:`Embedded Mode ` → The validated module runs `importspy()` internally to inspect its importer -- :doc:`External Mode ` → Validation is triggered via CLI, often in CI/CD or static validation steps - -Execution Pipeline ------------------- - -Here’s how ImportSpy validates imports, broken down into phases: - -1. Detect the Importer -~~~~~~~~~~~~~~~~~~~~~~ - -ImportSpy uses **stack introspection** to determine which module is importing the validated one. -This lets it establish a **validation boundary**, isolating the exact caller and its context. - -2. Capture Runtime Context -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Once the importer is found, ImportSpy collects runtime data: - -- Current OS and CPU architecture -- Python version and interpreter type -- Available environment variables -- Installed Python modules and paths - -This snapshot is encoded into an internal `SpyModel` object, representing the actual runtime conditions. - -3. Parse the Contract -~~~~~~~~~~~~~~~~~~~~~ - -Next, ImportSpy loads and parses the YAML-based import contract (typically `spymodel.yml`). - -This creates a second `SpyModel` representing the **expected structure and execution environment**. - -4. Compare & Validate -~~~~~~~~~~~~~~~~~~~~~ - -ImportSpy performs a deep comparison between the **actual SpyModel** and the **expected SpyModel**. - -Validation includes: - -- Matching CPU architecture and operating system -- Checking Python version and interpreter type -- Verifying required environment variables -- Matching declared functions, classes, methods, and attributes -- Validating module variables and custom metadata -- Checking nested modules and deployment-specific rules - -5. Enforce or Reject -~~~~~~~~~~~~~~~~~~~~~ - -- If the validation **fails**: - - ImportSpy raises a `ValueError` - - Detailed diagnostics are included in the exception - - Execution is halted immediately - -- If the validation **passes**: - - The validated importer is returned (in embedded mode) - - Execution proceeds safely - -6. Optimize for Runtime -~~~~~~~~~~~~~~~~~~~~~~~~ - -To avoid repeated validation during long-running processes or multi-import scenarios, ImportSpy uses **caching** to store validated environments. - -This provides fast re-entry for already-validated modules without compromising security. - -Security Philosophy -------------------- - -ImportSpy follows a **fail-fast and zero-trust model**: - -- 🚫 No module runs unless it satisfies all declared constraints -- ✅ All failures are explicit and traceable -- 🔐 This prevents silent regressions, broken interfaces, and unpredictable imports - -There is **no fallback behavior** — if a violation is detected, it is blocked **by design**. - -Best Practices --------------- - -- Use :doc:`embedded_mode` to validate who is importing your module -- Use :doc:`external_mode` to validate a module before deployment -- Always define structural + runtime constraints in your contract for full coverage - -Related Topics --------------- - -- :doc:`defining_import_contracts` -- :doc:`contract_structure` -- :doc:`error_handling` -- :doc:`validation_and_compliance` diff --git a/docs/source/overview/understanding_importspy/validation_and_compliance.rst b/docs/source/overview/understanding_importspy/validation_and_compliance.rst deleted file mode 100644 index 74a8f56..0000000 --- a/docs/source/overview/understanding_importspy/validation_and_compliance.rst +++ /dev/null @@ -1,129 +0,0 @@ -Validation and Compliance in ImportSpy -======================================= - -ImportSpy is more than a validator — it's a **compliance enforcement layer** -that ensures every import happens under the right conditions. - -It prevents fragile integrations, runtime surprises, and architectural drift by validating -**both the structure of modules** and **the environment they run in** — before they’re allowed to execute. - -Why Compliance Matters ------------------------ - -In modern Python systems, you often deal with: - -- Multiple operating systems and architectures -- Third-party plugins and dynamic module loading -- Varying Python runtimes across dev/staging/prod -- Environment variables that silently shape behavior - -Without strict validation, these differences introduce risk. - -ImportSpy guarantees that external modules only execute if their importing environment **matches the declared constraints** in their import contract. - -Multilayer Validation ----------------------- - -ImportSpy checks compliance at multiple levels: - -Execution Context -~~~~~~~~~~~~~~~~~ - -Before a module runs, ImportSpy validates: - -- Which module is trying to import it (via introspection) -- The **OS**, **architecture**, **Python version**, and **interpreter** -- Any required **environment variables** -- Whether the current runtime matches one of the allowed **deployment blocks** - -If anything is off, validation fails — no execution occurs. - -Example: - -- ✅ Declared: CPython 3.12.8 on Linux - ✅ Actual: CPython 3.12.8 on Linux → Allowed -- ❌ Declared: Windows-only - ❌ Actual: Linux → Rejected -- ❌ Declared: Requires `PLUGIN_KEY` - ❌ Missing from environment → Blocked - -Module Structure -~~~~~~~~~~~~~~~~ - -Structural validation includes: - -- Required **functions**, with specific argument names and annotations -- Required **classes**, including attributes, methods, and superclasses -- Top-level **variables** and their expected values -- Submodule definitions (when declared inside a `deployment`) - -If a module fails to meet these expectations, it's rejected at import time. - -System-Level Constraints -~~~~~~~~~~~~~~~~~~~~~~~~ - -ImportSpy allows contracts to specify: - -- Required environment variables -- Specific OS-level deployments -- Architecture-specific compatibility (e.g., only ARM64) - -This prevents issues like: - -- Silent misconfiguration -- Undeclared system assumptions -- Crashes due to missing runtime data - -Diagnostics and Failure Behavior --------------------------------- - -ImportSpy is strict: when validation fails, execution halts. - -But it also provides **high-quality error feedback**, including: - -- ❌ What went wrong (e.g., `"Missing method 'run'"`) -- ❌ Where the mismatch occurred (e.g., `"In class Extension"`) -- ❌ Why it’s invalid (e.g., `"Expected str, found int"`) - -Errors are raised as `ValueError` with clear stack traces and contract violation summaries. - -This fail-fast approach prevents issues from reaching production — or worse, going unnoticed. - -Maintaining Long-Term Compliance ---------------------------------- - -ImportSpy helps teams enforce long-term consistency via: - -- **Versioned contracts** that evolve with your code -- **Validation in CI/CD pipelines** to catch regressions early -- **Runtime checks** in production or sandbox environments - -This ensures: - -- No configuration drift -- No mismatches between staging and production -- Structural integrity across deployments and updates - -Compliance Is Not Optional ---------------------------- - -ImportSpy adopts a **Zero-Trust philosophy**: - -> ❌ No module is trusted without validation -> ✅ No import occurs unless the environment is approved - -This guarantees: - -- Secure plugin systems -- Stable microservice communication -- Predictable behavior across machines and versions - -If you care about runtime integrity, ImportSpy turns your import logic into **an enforceable contract** — and blocks anything that breaks it. - -Related Topics --------------- - -- :doc:`spy_execution_flow` -- :doc:`contract_structure` -- :doc:`error_handling` -- :doc:`integration_best_practices` diff --git a/docs/source/overview/understanding_importspy_index.rst b/docs/source/overview/understanding_importspy_index.rst deleted file mode 100644 index cdd1f9d..0000000 --- a/docs/source/overview/understanding_importspy_index.rst +++ /dev/null @@ -1,89 +0,0 @@ -Understanding ImportSpy 🔍 -========================== - -Welcome to the **technical heart** of ImportSpy. - -This section provides a full breakdown of how ImportSpy works, -why it matters in modern modular architectures, and how you can harness its full potential. - -ImportSpy isn’t just a utility — it’s a **runtime contract enforcement framework**. -It brings validation to the import system by ensuring that external modules conform to **predefined structural rules and runtime constraints**. -Whether used in embedded or CLI mode, ImportSpy guarantees **predictability, security, and compliance** before code ever runs. - -Why This Section Matters ⚠️ ----------------------------- - -Modern systems are complex. -You rely on dynamically loaded modules, plugins, APIs, microservices — often across multiple environments. - -But with this flexibility comes risk: - -- Missing or incompatible methods, attributes, or classes -- Subtle mismatches in Python versions, OS, or interpreters -- Unexpected behavior caused by configuration drift or structural divergence -- Modules that silently break contracts and cause late-stage failures - -ImportSpy intercepts these issues **before execution**, making validation **a first-class citizen** of your architecture. - -What You’ll Learn Here 📘 --------------------------- - -This section guides you through ImportSpy’s internals, starting from high-level concepts to runtime execution flow: - -- :doc:`understanding_importspy/introduction` - A clear introduction to ImportSpy’s core mission and use cases. - -- :doc:`understanding_importspy/defining_import_contracts` - Learn how to describe structural and environmental expectations through YAML contracts. - -- :doc:`understanding_importspy/contract_structure` - Understand the schema and semantics of a well-formed import contract. - -- :doc:`understanding_importspy/spy_execution_flow` - Discover how ImportSpy intercepts imports and validates modules in real time. - -- :doc:`understanding_importspy/embedded_mode` - Explore how modules can protect themselves by validating their importers at runtime. - -- :doc:`understanding_importspy/external_mode` - Understand CLI-based validation and its role in automation, CI/CD, and review pipelines. - -- :doc:`understanding_importspy/validation_and_compliance` - Dive into the validation engine and what it checks (types, names, annotations, OS, versions, etc). - -- :doc:`understanding_importspy/error_handling` - See how ImportSpy produces actionable, clear error messages when something doesn’t match. - -- :doc:`understanding_importspy/integration_best_practices` - Apply ImportSpy cleanly in real projects — plugins, microservices, libraries, or secure APIs. - -- :doc:`understanding_importspy/ci_cd_integration` - Integrate ImportSpy into your continuous delivery pipeline for automated contract enforcement. - -Who This Is For 👨‍💻👩‍💻 ---------------------------- - -This section is written for: - -- **Developers** using ImportSpy in plugin-based or multi-module Python apps -- **Architects** designing extensible, contract-driven software systems -- **DevOps and Security Engineers** aiming to validate runtime boundaries and block unknowns -- **Open Source Maintainers** who want to ensure compatibility across environments - -Ready to see how ImportSpy works under the hood? -Let’s explore the architecture that makes dynamic imports deterministic. - -.. toctree:: - :maxdepth: 2 - :caption: Understanding ImportSpy: - - understanding_importspy/introduction - understanding_importspy/defining_import_contracts - understanding_importspy/contract_structure - understanding_importspy/spy_execution_flow - understanding_importspy/embedded_mode - understanding_importspy/external_mode - understanding_importspy/validation_and_compliance - understanding_importspy/error_handling - understanding_importspy/integration_best_practices - understanding_importspy/ci_cd_integration diff --git a/docs/source/overview/use_cases/use_case_compilance.rst b/docs/source/overview/use_cases/use_case_compilance.rst deleted file mode 100644 index 05b77e9..0000000 --- a/docs/source/overview/use_cases/use_case_compilance.rst +++ /dev/null @@ -1,99 +0,0 @@ -Ensuring Compliance with Industry Standards -=========================================== - -📑 Enforcing Structural and Regulatory Conformance at the Module Level - -In industries governed by **strict regulations** — such as **finance**, **healthcare**, and **public sector platforms** — -software must not only function correctly, but also **prove compliance** with internal standards and external laws. - -Uncontrolled imports and unverified module interactions introduce risks that go far beyond bugs: - -- ⚠️ **Legal exposure**, from non-compliant code paths -- 🔓 **Security vulnerabilities**, exposing sensitive data -- 🐞 **Operational inconsistencies**, undermining traceability and auditability - -Modern teams need to **enforce validation before execution**, ensuring every module behaves exactly as expected -— across environments, platforms, and regulatory requirements. - -The Compliance Challenge -------------------------- - -A leading **healthcare SaaS provider** needed to secure their plugin system against non-compliant third-party modules. - -Their platform was required to uphold **HIPAA** and **GDPR** standards while supporting dynamic integration -with third-party services that could access **sensitive medical data**. - -Their core concerns: - -- 🔍 **Unstructured interactions** with plugin modules -- ❌ **No enforcement of structural expectations** -- 🕵️ **Poor audit visibility** over how and when validation occurred - -What they needed was **automated enforcement** at import time — a guardrail to **block non-compliant code -before it could execute**, with **evidence trails** for regulators and internal audits. - -How ImportSpy Solved It ------------------------- - -The team adopted **ImportSpy in CLI validation mode**, using **YAML-based import contracts** to: - -✅ **Define Compliance Constraints Declaratively** - -- Listed allowed **module names**, **functions**, **attributes**, and **expected annotations** -- Enforced execution limits: **Python version**, **OS**, **interpreter**, and **environment variables** -- Integrated contracts into the source repository as part of **version-controlled policy enforcement** - -✅ **Block Violations Before Runtime** - -- ImportSpy loaded the target module and validated it **against its declared contract** -- On mismatch, the module was **rejected immediately**, with detailed error feedback -- Violations raised `ValueError` exceptions, stopping non-compliant code from running - -✅ **Generate Audit Logs Automatically** - -- Each validation produced logs containing: - - Validation time and result - - Name of contract and validated module - - Structural mismatches or missing components -- Logs were parsed into **compliance dashboards** and shared with auditors - -Results: Measurable, Auditable Compliance ------------------------------------------ - -Before ImportSpy: - -- Compliance relied on **manual reviews and inconsistent scripts** -- Risk exposure was high due to **third-party code with unchecked access** -- Audits were painful, requiring **manual tracing of module usage across services** - -After ImportSpy: - -✅ All imported modules were validated automatically -✅ Violations were blocked before deployment or execution -✅ Audit trails were generated for every contract match/failure -✅ Compliance with **HIPAA**, **GDPR**, and internal policies was built into the lifecycle - -Why It Matters --------------- - -ImportSpy bridges the gap between **modular extensibility** and **regulatory control**. -By moving compliance checks to the import boundary, it ensures that **only verified, policy-compliant code** can run. - -Whether you're building regulated cloud platforms or securing internal APIs, ImportSpy gives your team: - -- ✅ **Automated, declarative validation** -- ✅ **Runtime protection against policy violations** -- ✅ **Structured logging for audits and security reviews** - -Try It Yourself ---------------- - -To validate a module before runtime, run: - -.. code-block:: bash - - importspy -s spymodel.yml your_module.py - -ImportSpy will block any import that doesn’t match the compliance contract — ensuring policy adherence before execution. - -📌 Import contracts are the new compliance policy — and ImportSpy is how you enforce them. diff --git a/docs/source/overview/use_cases/use_case_iot_integration.rst b/docs/source/overview/use_cases/use_case_iot_integration.rst deleted file mode 100644 index a16ed86..0000000 --- a/docs/source/overview/use_cases/use_case_iot_integration.rst +++ /dev/null @@ -1,112 +0,0 @@ -Ensuring Compliance in IoT Smart Home Integration -================================================= - -🔌 Real-World Enforcement Across Heterogeneous Devices - -In the evolving world of the **Internet of Things (IoT)**, ensuring **predictable behavior** across a wide variety of devices is no small feat. -Vendors, hardware platforms, Python runtimes, and execution environments all vary — making consistency difficult to guarantee. - -A leading IoT company, building a **smart home automation platform**, needed to support **third-party plugins** while maintaining **strict compliance**. -They required enforcement of **interface contracts**, **environmental conditions**, and **runtime compatibility** across a fragmented deployment landscape. - -📐 System Architecture ----------------------- - -The platform was designed around a **plugin-based architecture** that allowed modular integration of: - -- Smart thermostats -- Lighting controllers -- Security sensors -- Voice assistants -- External automation services - -Key architectural traits: - -- Device drivers implemented as Python plugins. -- Plugins communicate via a **RESTful API** layer. -- Deployed across edge devices like **Raspberry Pi**, as well as **Kubernetes-based smart hubs**. -- Environment setup includes: - - **ARM/x86_64** CPUs - - **Multiple Python versions** (3.10, 3.12) and **interpreters** (CPython, PyPy) - - **Dockerized plugins** with injected secrets via environment variables - -🧩 The Compliance Problem --------------------------- - -Without enforcement, plugins were deployed with: - -- Missing functions or improperly annotated interfaces -- Incorrect assumptions about Python version or CPU architecture -- Misconfigured or absent environment variables (`DEVICE_TOKEN`, `API_KEY`, etc.) -- Unvalidated structure that only failed **after** deployment - -These mismatches led to: - -- ❌ Runtime crashes in smart home hubs -- ❌ Inconsistent API behavior -- ❌ Security concerns due to environment misconfigurations -- ❌ Long debugging cycles and delayed releases - -🛡️ ImportSpy in Action: Embedded Validation Mode -------------------------------------------------- - -To regain control, the team embedded **ImportSpy** directly into each plugin’s entry point: - -.. code-block:: python - - from importspy import Spy - - caller_module = Spy().importspy(filepath="spymodel.yml") - -Plugins were paired with YAML-based **import contracts** that defined strict structural and runtime constraints. - -Contract enforcement ensured: - -✅ Structural Compliance - - Validated presence of all **required methods, attributes, and return types** - - Prevented silent schema drift between plugin and control layer - -✅ Runtime Compatibility - - Verified execution on the correct **CPU architecture** and **Python interpreter** - - Blocked execution on unsupported hardware setups - -✅ Environmental Validation - - Checked for required **env vars** (e.g., `DEVICE_TOKEN`, `PLATFORM_ENV`) - - Rejected execution if secrets were missing or malformed - -✅ Deployment Readiness - - CLI mode (`importspy -s spymodel.yml plugin.py`) used in **CI/CD pipelines** - - Pre-deployment validation embedded into Docker build stages - - Only validated containers promoted to **production clusters** - -🚀 Results in Production -------------------------- - -After adopting ImportSpy: - -- 🔒 **Plugin integrity was guaranteed pre-execution** -- 🐛 **Edge-device errors were eliminated before rollout** -- ⚙️ **CI pipelines caught contract violations early** -- 🔁 **New plugins were integrated 3× faster**, with fewer regressions - -Before ImportSpy: - -- Incompatible drivers were deployed to production -- Manual tests were required per device and platform -- Configuration bugs were discovered too late - -After ImportSpy: - -✅ Structural drift was eliminated -✅ Plugin execution was **bounded by contract** -✅ IoT integration became scalable and predictable - -Conclusion ----------- - -This real-world case shows how ImportSpy enables **modular safety** in highly distributed systems. -By turning contracts into enforcement mechanisms, it transforms each plugin into a **self-validating unit** — -capable of **refusing to run in invalid contexts**, and ensuring that integration is both safe and scalable. - -📦 ImportSpy is more than validation. -It’s **runtime insurance** for systems that must adapt — without compromising structure or control. diff --git a/docs/source/overview/use_cases/use_case_security.rst b/docs/source/overview/use_cases/use_case_security.rst deleted file mode 100644 index 76a2653..0000000 --- a/docs/source/overview/use_cases/use_case_security.rst +++ /dev/null @@ -1,120 +0,0 @@ -Strengthening Software Security with ImportSpy 🔐 -================================================= - -🔍 Enforcing Controlled Interactions with External Modules - -In security-critical software, **unregulated imports** are a gateway to vulnerabilities. -From misconfigured plugins to dynamic imports of malicious code, **Python’s flexibility becomes a liability** without structural enforcement. - -Organizations operating in fields like **cybersecurity**, **finance**, and **enterprise platforms** need more than just static analysis — -they need **runtime enforcement** that validates what is imported, how it behaves, and under which context it executes. - -🧨 The Problem: Invisible Risks in External Dependencies ---------------------------------------------------------- - -A cybersecurity firm specializing in **real-time threat detection** uncovered serious risks in its plugin framework: - -- Internal APIs were accessible via loosely defined module boundaries. -- External components bypassed authentication checks using dynamic imports. -- Function contracts were silently broken after dependency upgrades. -- No system-wide trace existed of who imported what — and under which conditions. - -These issues weren’t caused by malicious intent, but by the **absence of strict validation**. - -Without safeguards: - -- ⚠️ Plugins introduced **execution drift**. -- ⚠️ Imports became **non-deterministic** across environments. -- ⚠️ Attackers could **abuse loosely validated integrations**. - -🛡️ The Solution: ImportSpy Embedded + CLI Validation ------------------------------------------------------ - -The team introduced **ImportSpy** using both: - -- **Embedded Mode** for real-time validation at module import time. -- **CLI Mode** for enforcement in automated build pipelines. - -Each plugin and internal service was paired with a YAML-based **import contract** (`spymodel.yml`), defining strict: - -- Allowed functions and methods (including arguments and annotations) -- Required attributes and class hierarchies -- Valid operating systems, Python versions, and interpreters -- Mandatory environment variables for secrets or context - -📦 Example snippet from a contract: - -.. code-block:: yaml - - filename: secure_plugin.py - functions: - - name: verify_signature - arguments: - - name: data - annotation: bytes - return_annotation: bool - deployments: - - arch: x86_64 - systems: - - os: linux - envs: - SECURITY_TOKEN: required - pythons: - - version: 3.12.8 - interpreter: CPython - -⚙️ Security Mechanisms Enabled by ImportSpy --------------------------------------------- - -🔐 **Structural Boundary Enforcement (Embedded Mode)** - - ImportSpy executed *inside* secure modules to inspect who was importing them. - - If the importer didn’t match declared contracts, the execution was blocked. - - Validations were performed **every time the module was used**, ensuring active defense. - -🧪 **CI/CD Enforcement (CLI Mode)** - - ImportSpy was used in pipelines to validate plugins **before deployment**. - - Prevented misconfigured or unauthorized code from entering production. - - Ideal for automated checks on third-party or external codebases. - -🚫 **Blocking Unauthorized Imports** - - Attempted imports from unknown modules were rejected. - - Reflection-based imports (e.g. `importlib`, `__import__`) were intercepted if they bypassed structure. - -📈 **Audit-Ready Validation Logs** - - Each validation generated: - - Who imported the module and from where. - - Whether all structural, runtime, and environmental constraints were satisfied. - - A traceable record for security and compliance audits. - -🚀 Real Impact --------------- - -After adopting ImportSpy: - -✅ Only **pre-approved, contract-compliant modules** were allowed to interface with secure APIs -✅ All imports were **traceable and auditable**, including dynamic execution paths -✅ Teams could **prevent misuse of sensitive interfaces at runtime**, not just in reviews -✅ Security incidents related to uncontrolled plugin behavior dropped to zero - -Before ImportSpy: - -- Access to internal components was based on trust, not enforcement. -- Developers could unknowingly introduce insecure behaviors through third-party dependencies. -- Detection of misuses happened **after the fact**, during production failures or audits. - -After ImportSpy: - -✅ Security was enforced as **a contract**, not a convention -✅ Modules became **self-defensive**, refusing to run under unsafe conditions -✅ Compliance teams gained **real-time insight** into software integrity - -Conclusion ----------- - -ImportSpy transforms Python’s import mechanism into a **structural firewall**, -enforcing the principle of **Zero Trust by default**. - -Whether embedded in secure modules or integrated into CI/CD workflows, -it ensures that only **authorized, structurally sound, and contextually valid** code is ever executed. - -🔐 With ImportSpy, your code doesn’t just run — it runs **safely, predictably, and by the rules**. diff --git a/docs/source/overview/use_cases/use_case_validation.rst b/docs/source/overview/use_cases/use_case_validation.rst deleted file mode 100644 index bead272..0000000 --- a/docs/source/overview/use_cases/use_case_validation.rst +++ /dev/null @@ -1,99 +0,0 @@ -Validating Imports in Large-Scale Architectures -=============================================== - -🔍 Enforcing Predictable Module Integration Across Microservices - -In modern software platforms, especially those built around **microservices and shared components**, -imports can quickly become a **source of instability** if not explicitly controlled. - -ImportSpy addresses this challenge by enabling teams to define and enforce **import contracts**, -bringing structure, validation, and security to large-scale Python ecosystems. - -The Challenge: Structural Drift at Scale ----------------------------------------- - -A global fintech company operating a **real-time trading platform** faced a growing problem: - -- Over 200 services exchanged shared modules, but **no validation existed** on what those modules should look like. -- Developers introduced **untracked changes** to shared libraries — often without awareness of the ripple effect. -- Bugs emerged **during runtime**, causing unpredictable behavior in APIs, logs, and financial transactions. -- Regulatory audits revealed **unauthorized dependencies**, triggering compliance concerns. - -Without validation, **even a renamed method or removed class attribute** had the potential to break entire workflows -— often in systems critical to financial accuracy and regulatory visibility. - -How ImportSpy Resolved the Problem ----------------------------------- - -The team adopted ImportSpy to introduce **contract-based validation** between services. - -Each service defined a **`spymodel.yml`** contract that: - -- ✅ Declared which modules could be imported -- ✅ Specified required functions, classes, and their expected structure -- ✅ Described the allowed Python version, interpreter, and OS for each deployment context -- ✅ Enforced environmental assumptions like `env` variables and module metadata - -Validation was performed in two ways: - -- **Externally in CI/CD pipelines**, using the CLI tool -- **Dynamically at runtime**, via embedded validation inside critical modules - -Core Benefits for Large-Scale Systems --------------------------------------- - -🔒 **Structural Enforcement, Not Just Testing** - -Every import was validated against the contract: - -- Missing functions? ❌ Blocked -- Changed signatures? ❌ Blocked -- Incorrect return types? ❌ Blocked -- Drift in module metadata? ❌ Blocked - -🧩 **Modular Contracts per Microservice** - -Each team owned their own import contract, versioned alongside their codebase. -Contracts were reviewed in pull requests, giving visibility into integration assumptions. - -🛑 **Fail Fast, Fail Loud** - -When violations occurred, ImportSpy halted execution and raised detailed errors -before the application could misbehave. - -📋 **Compliance and Audit Alignment** - -Contracts became part of compliance reviews. -ImportSpy ensured that: - -- Only approved dependencies were used -- Environments matched what was declared -- Drift was caught before deployment - -🚀 Real-World Impact ---------------------- - -**Before ImportSpy**: - -- Services broke silently due to changing APIs -- Debugging required tracing through dozens of unrelated modules -- Compliance reports had no traceability on module-level expectations - -**After ImportSpy**: - -✅ Every shared module was paired with a structural contract -✅ Integration bugs were detected early in CI -✅ Teams had clear ownership and boundaries -✅ Compliance teams had visible, testable enforcement logic - -Conclusion ----------- - -ImportSpy enabled the company to treat imports as **governed integration points**, -not dynamic and unpredictable behaviors. - -It transformed their microservice architecture into a **contract-bound system**, -where validation was continuous, clear, and automated — at runtime and in pipelines. - -📌 For teams operating at scale, ImportSpy brings **structure, clarity, and runtime discipline** -to one of the most overlooked areas of Python: the import statement itself. diff --git a/docs/source/overview/use_cases_index.rst b/docs/source/overview/use_cases_index.rst deleted file mode 100644 index 37d4d91..0000000 --- a/docs/source/overview/use_cases_index.rst +++ /dev/null @@ -1,61 +0,0 @@ -Use Cases -========= - -🔍 Real-World Applications of ImportSpy - -ImportSpy is built for **real-world modular ecosystems** — where external components, plugins, and dynamic imports -must interact with precision, safety, and structural integrity. - -This section presents practical scenarios where ImportSpy ensures: - -- ✅ **Runtime validation** of external modules -- ✅ **Predictable integration** across complex environments -- ✅ **Compliance enforcement** in regulated and distributed systems - -Why Use Cases Matter --------------------- - -As modern applications adopt **microservices**, **plugin-based extensions**, and **cloud-native deployments**, -the complexity of imports grows — and so does the risk of: - -- ❌ Uncontrolled module behavior -- ❌ Silent contract violations -- ❌ Runtime incompatibilities across environments - -ImportSpy brings **order, visibility, and validation** to these architectures — blocking what doesn’t belong, and allowing only compliant modules to run. - -What You’ll Learn Here ------------------------ - -These case studies walk through: - -- **Modular IoT Environments** 🌐 - How ImportSpy validates plugins and services across **multi-device deployments** and **cross-platform runtimes**. - -- **Structural Validation in Dynamic Systems** 🧱 - Ensuring that plugins, modules, and APIs always match the expected **structure, behavior, and metadata**. - -- **Security and Runtime Threat Prevention** 🔒 - Protecting your application from **malicious imports**, **tampering**, and **unauthorized runtime mutations**. - -- **Regulatory Compliance and Policy Enforcement** 📋 - Using ImportSpy to meet **industry standards** by validating runtime environments, interpreters, and module structure. - -Use Case Preview ------------------ - -Each case is based on **realistic implementations**, designed to help teams: - -- Integrate ImportSpy into **CI/CD pipelines** -- Harden **plugin-based architectures** -- Secure **cloud, edge, and hybrid environments** -- Automate validation as part of **development workflows** - -.. toctree:: - :maxdepth: 2 - :caption: Use Cases - - use_cases/use_case_iot_integration - use_cases/use_case_validation - use_cases/use_case_security - use_cases/use_case_compilance diff --git a/docs/source/sponsorship.rst b/docs/source/sponsorship.rst deleted file mode 100644 index 00f97f6..0000000 --- a/docs/source/sponsorship.rst +++ /dev/null @@ -1,68 +0,0 @@ -Support the ImportSpy Mission 💡 -================================= - -ImportSpy is more than a validation tool — it’s a call for structure, precision, and integrity in the Python ecosystem. -By supporting ImportSpy, you’re backing a vision: one where Python modules behave as expected, where integrations are safe by design, and where developers can build **modular, reliable systems with confidence**. - -Whether you're an individual developer, an engineering team, or a tech leader, your support helps shape a future where **every Python import is secure, predictable, and compliant**. - -Why Your Support Matters 🚀 ----------------------------- - -ImportSpy is built on open-source values: **transparency, accessibility, and community-driven evolution**. -Your support enables us to: - -- **Accelerate Feature Development** 🛠️ - More funding means more focus. We can deliver powerful features, implement community requests, and evolve ImportSpy faster and more reliably. - -- **Expand Learning Resources** 📚 - Sponsor contributions help us invest in tutorials, video walkthroughs, onboarding guides, and examples that make ImportSpy accessible for everyone — from beginner to expert. - -- **Maintain Compatibility** 🔄 - Keeping up with the ever-changing Python ecosystem requires time and effort. Your support ensures ImportSpy stays compatible with new versions, platforms, and interpreters. - -- **Fuel Community Growth** 🌍 - From live workshops to online Q&As and collaborative initiatives — sponsors make it possible for us to build a **global, active, and inclusive developer community**. - -How You Can Help 💖 --------------------- - -**⭐ Star ImportSpy on GitHub** -Visibility matters. A single star tells the world this project matters. -`Star the project `_ - -**💝 Become a GitHub Sponsor** -Help us focus full-time on building and maintaining ImportSpy. -Your sponsorship directly funds the project's future. -`Sponsor ImportSpy `_ - -**📢 Spread the Word** -Tell a friend. Mention it in your team. Share it in your community. -Every conversation strengthens the ecosystem. - -**🔧 Contribute Code, Ideas, or Feedback** -Open issues, suggest features, or submit pull requests — your voice and your code matter. -We grow better with you involved. - -Thank You to Our Sponsors 💙 ------------------------------ - -Every sponsor — whether individual or organization — plays a vital role in ImportSpy’s growth. -We are deeply grateful for your belief in our mission and your commitment to building a more structured Python ecosystem. - -Your sponsorship supports: - -- Open development -- Better tools for the Python community -- A culture of care, rigor, and transparency - -Let’s Shape the Future Together 🔭 ------------------------------------ - -As a sponsor, you’re not just funding development — you’re helping define the roadmap. -We welcome your input on features, priorities, and directions for the future. - -ImportSpy exists because developers believed Python could be better. -With your help, we can **set a new standard for what “safe imports” look like.** - -**🔹 Support structure. Support clarity. Support ImportSpy.** diff --git a/docs/source/vision.rst b/docs/source/vision.rst deleted file mode 100644 index 21c2b8d..0000000 --- a/docs/source/vision.rst +++ /dev/null @@ -1,104 +0,0 @@ -The Vision Behind ImportSpy -============================ - -ImportSpy exists to solve a simple but powerful problem: -> How can we make dynamic Python imports **secure**, **predictable**, and **compliant**—without sacrificing flexibility? - -Modern Python development thrives on **modularity**, with architectures powered by **plugins, microservices, and third-party integrations**. -But the more dynamic our systems become, the harder it is to guarantee **structural consistency**, **environmental compatibility**, and **execution safety**. - -ImportSpy redefines how we think about `import` in Python. -It brings the **rigor of contracts** to the most permissive part of the language—validating structure, runtime context, and compliance **before code is allowed to execute**. - -What Problem Does ImportSpy Solve? ------------------------------------ - -Too often, developers rely on: - -- ✅ Best practices -- ✅ Static linters -- ✅ Runtime trial-and-error -- ✅ Outdated documentation - -…to ensure that external modules conform to expectations. But when things go wrong: - -- APIs silently drift -- Plugins break in production -- Modules fail across environments -- Security boundaries are crossed - -ImportSpy addresses these gaps **by enforcing executable contracts** at runtime—**automatically** and **contextually**. - -The Mission ------------ - -ImportSpy is designed to be the **compliance layer** for dynamic Python systems. - -Its mission is to: - -- 🧩 **Protect modular systems** from unpredictable imports -- 🔒 **Prevent integration errors** before they happen -- 🚦 **Validate structure, versioning, and runtime environment** -- 📜 **Promote living contracts** between modules and their runtime expectations - -And in doing so, it helps developers build systems that are: - -- Easier to maintain -- Safer to extend -- Ready for scale -- Aligned with compliance standards in regulated environments - -A Runtime Contract Philosophy ------------------------------- - -The vision behind ImportSpy is rooted in a new philosophy: - -> *“If a module must behave a certain way, let’s not hope it does — let’s validate it.”* - -By introducing **import contracts**, ImportSpy formalizes the structure of Python modules in YAML. -These contracts define what’s expected: -classes, methods, attributes, variables, Python versions, interpreters, OS targets, and more. - -At runtime, ImportSpy checks if those expectations are met — and if not, it blocks execution with **clear, actionable feedback**. - -Why This Matters ----------------- - -Today’s software is **distributed**, **heterogeneous**, and **highly modular**. -Whether you’re building for: - -- Embedded devices and IoT -- Plugin ecosystems -- Regulated sectors -- Containerized architectures -- Cloud-based platforms - -…you need to know that **the code running in production is exactly what you intended to deploy**. - -ImportSpy gives you that guarantee. - -It becomes a contract enforcer for: - -- **Security**: detect tampering and unauthorized changes -- **Compliance**: validate structural and environmental constraints -- **Stability**: prevent “it worked on my machine” failures -- **Clarity**: reduce guesswork and accelerate debugging - -Looking Ahead -------------- - -This is only the beginning. - -Future goals include: - -- ✨ Auto-generating contracts from Python modules -- 🔁 Bi-directional validation between contracts and code -- 🔍 Fine-grained integration with dependency graphs -- 🧠 Enhanced static-to-runtime consistency tooling -- 💼 First-class CI/CD and DevSecOps integrations - -With ImportSpy, we believe Python can be **both dynamic and dependable**. - -Join the movement toward **validated modularity**, and help shape a future where every import is safe, consistent, and predictable. - -**🔹 Structure with clarity. Import with confidence. Trust your code.** diff --git a/docs/use_cases/index.md b/docs/use_cases/index.md new file mode 100644 index 0000000..f72cf82 --- /dev/null +++ b/docs/use_cases/index.md @@ -0,0 +1,120 @@ +# Use Cases + +ImportSpy brings contract-based validation to dynamic Python environments, enabling control, predictability, and safety. + +Below are common scenarios where ImportSpy proves useful. + +--- + +## Embedded Mode in Plugin Architectures + +In plugin-based systems, a core module often exposes an interface or expected structure that plugins must follow. + +With ImportSpy embedded in the core, plugins are validated **at import time** to ensure they define the required classes, methods, variables, and environment conditions. + +This prevents silent failures or misconfigurations by enforcing structural and runtime constraints early. + +!!! example "Example: Plugin Enforced at Import" + ```python + --8<-- "examples/plugin_based_architecture/package.py" + ``` + +See also [Embedded Mode](../modes/embedded.md) and [Contract Syntax](../contracts/syntax.md) for YAML details. + +--- + +## CLI Validation in CI/CD Pipelines + +In DevOps workflows or during pre-release validation, ImportSpy can be executed from the command line to ensure a Python module conforms to its declared import contract. + +Typical use cases: +- Automated deployment verification +- Open-source plugin contributions +- Validating extension points in modular codebases + +!!! example "Example: Validate via CLI" + ```bash + importspy extensions.py -s spymodel.yml -l INFO + ``` + +See [CLI Mode](../modes/cli.md) for full usage. + +--- + +## Restricting Import Access by Runtime Context + +A module can refuse to be imported unless specific runtime conditions are met — such as CPU architecture, OS, Python version, or interpreter. + +This enables: +- Targeted deployments (e.g., Linux-only, CPython-only) +- Restriction to known-safe execution environments +- Fail-fast behavior in unsupported contexts + +Contracts can define system constraints like: + +```yaml +deployments: + - arch: x86_64 + systems: + - os: linux + pythons: + - version: 3.12 + interpreter: CPython +``` + +If the runtime doesn't match, import fails with a clear error message. + +--- + +## Contract-as-Code: Executable Documentation + +ImportSpy contracts double as living specifications for module structure and environment assumptions. + +Rather than maintaining separate interface docs, a `.yml` contract acts as: + +- ✅ Interface specification +- ✅ Compatibility schema +- ✅ Runtime validator + +This approach improves communication in: +- Plugin-based systems +- Collaborative teams sharing Python APIs +- Educational or onboarding contexts + +!!! example "Example Contract Snippet" + ```yaml + classes: + - name: Plugin + methods: + - name: run + arguments: + - name: self + ``` + +Any contributor can run `importspy` or trigger the embedded validation to ensure conformance. + +--- + +## Supporting Multiple Deployment Targets + +Modules may need to support more than one runtime environment (e.g., Linux and Windows, or Python 3.10+). + +ImportSpy contracts support listing **multiple valid deployments**, each with its own OS, interpreter, and version constraints. + +```yaml +deployments: + - arch: x86_64 + systems: + - os: linux + pythons: + - version: 3.10 + - os: windows + pythons: + - version: 3.11 +``` + +This enables the same module to be validated across CI matrices or downstream consumers with differing setups. + +--- + +Together, these use cases show how ImportSpy bridges **runtime context and modular structure** through declarative contracts — empowering safer, more predictable Python architectures. diff --git a/examples/plugin_based_architecture/external_module_compilance/package.py b/examples/plugin_based_architecture/external_module_compilance/package.py index b1a6a70..14474df 100644 --- a/examples/plugin_based_architecture/external_module_compilance/package.py +++ b/examples/plugin_based_architecture/external_module_compilance/package.py @@ -6,5 +6,5 @@ __version__ = None -caller_module = Spy().importspy(filepath="./spymodel.yml", log_level=logging.DEBUG) +caller_module = Spy().importspy(filepath="./spymodel.yml", log_level=logging.WARN) caller_module.Foo().get_bar() \ No newline at end of file diff --git a/examples/plugin_based_architecture/external_module_compilance/spymodel.yml b/examples/plugin_based_architecture/external_module_compilance/spymodel.yml index a3cb127..cd78b33 100644 --- a/examples/plugin_based_architecture/external_module_compilance/spymodel.yml +++ b/examples/plugin_based_architecture/external_module_compilance/spymodel.yml @@ -34,7 +34,7 @@ classes: arguments: - name: self superclasses: - - Plugin + - name: Plugin - name: Foo attributes: methods: @@ -44,9 +44,9 @@ classes: deployments: - arch: x86_64 systems: - - os: windows + - os: linux pythons: - - version: 3.12.8 + - version: 3.12.9 interpreter: CPython modules: - filename: extension.py @@ -63,9 +63,9 @@ deployments: - interpreter: IronPython modules: - filename: addons.py - - os: linux + - os: windows pythons: - - version: 3.12.8 + - version: 3.12.9 interpreter: CPython modules: - filename: extension.py diff --git a/examples/plugin_based_architecture/pipeline_validation/plugin_interface.py b/examples/plugin_based_architecture/pipeline_validation/plugin_interface.py deleted file mode 100644 index cd3c151..0000000 --- a/examples/plugin_based_architecture/pipeline_validation/plugin_interface.py +++ /dev/null @@ -1,2 +0,0 @@ -class Plugin: - pass \ No newline at end of file diff --git a/examples/plugin_based_architecture/pipeline_validation/spymodel.yml b/examples/plugin_based_architecture/pipeline_validation/spymodel.yml index a3cb127..cd78b33 100644 --- a/examples/plugin_based_architecture/pipeline_validation/spymodel.yml +++ b/examples/plugin_based_architecture/pipeline_validation/spymodel.yml @@ -34,7 +34,7 @@ classes: arguments: - name: self superclasses: - - Plugin + - name: Plugin - name: Foo attributes: methods: @@ -44,9 +44,9 @@ classes: deployments: - arch: x86_64 systems: - - os: windows + - os: linux pythons: - - version: 3.12.8 + - version: 3.12.9 interpreter: CPython modules: - filename: extension.py @@ -63,9 +63,9 @@ deployments: - interpreter: IronPython modules: - filename: addons.py - - os: linux + - os: windows pythons: - - version: 3.12.8 + - version: 3.12.9 interpreter: CPython modules: - filename: extension.py diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..6fab34e --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,52 @@ +site_name: ImportSpy +site_url: https://importspy.github.io/importspy/ +repo_url: https://github.com/importspy/importspy +repo_name: importspy/importspy + +theme: + name: material + custom_dir: docs/overrides + favicon: assets/favicon.ico + logo: assets/importspy-logo.png + palette: + - scheme: default + primary: purple + accent: light blue + features: + - navigation.tabs + - navigation.instant + +nav: + - Home: index.md + - Introduction: + - What is ImportSpy: intro/overview.md + - Installation: intro/install.md + - Quickstart: intro/quickstart.md + - Operating Modes: + - Embedded Mode: modes/embedded.md + - CLI Mode: modes/cli.md + - YAML Contracts: + - Syntax: contracts/syntax.md + - Examples: contracts/examples.md + - Advanced Usage: +# - CI/CD Integration: advanced/cicd.md + - SpyModel Architecture: advanced/spymodel.md + - Violation System: advanced/violations.md + - Validation & Errors: errors/contract-violations.md + - Use Cases: use_cases/index.md + - API Reference: api-reference.md + +plugins: + - search + - mkdocstrings: + handlers: + python: + options: + show_source: false + +markdown_extensions: + - pymdownx.snippets + - admonition + - pymdownx.tabbed: + alternate_style: true + diff --git a/poetry.lock b/poetry.lock index b74e3f8..92785f1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,15 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. - -[[package]] -name = "alabaster" -version = "1.0.0" -description = "A light, configurable Sphinx theme" -optional = false -python-versions = ">=3.10" -files = [ - {file = "alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b"}, - {file = "alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e"}, -] +# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. [[package]] name = "annotated-types" @@ -17,165 +6,19 @@ version = "0.7.0" description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, ] -[[package]] -name = "babel" -version = "2.17.0" -description = "Internationalization utilities" -optional = false -python-versions = ">=3.8" -files = [ - {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, - {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, -] - -[package.extras] -dev = ["backports.zoneinfo", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata"] - -[[package]] -name = "beautifulsoup4" -version = "4.13.4" -description = "Screen-scraping library" -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b"}, - {file = "beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195"}, -] - -[package.dependencies] -soupsieve = ">1.2" -typing-extensions = ">=4.0.0" - -[package.extras] -cchardet = ["cchardet"] -chardet = ["chardet"] -charset-normalizer = ["charset-normalizer"] -html5lib = ["html5lib"] -lxml = ["lxml"] - -[[package]] -name = "certifi" -version = "2025.4.26" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.6" -files = [ - {file = "certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3"}, - {file = "certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6"}, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.2" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7" -files = [ - {file = "charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-win32.whl", hash = "sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-win32.whl", hash = "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e"}, - {file = "charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0"}, - {file = "charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63"}, -] - [[package]] name = "click" version = "8.1.8" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" +groups = ["main", "dev"] files = [ {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, @@ -190,84 +33,72 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "dev"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] - -[[package]] -name = "docutils" -version = "0.21.2" -description = "Docutils -- Python Documentation Utilities" -optional = false -python-versions = ">=3.9" -files = [ - {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, - {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, -] +markers = {main = "platform_system == \"Windows\""} [[package]] name = "exceptiongroup" -version = "1.2.2" +version = "1.3.0" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version == \"3.10\"" files = [ - {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, - {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, + {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, + {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, ] +[package.dependencies] +typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} + [package.extras] test = ["pytest (>=6)"] [[package]] -name = "furo" -version = "2024.8.6" -description = "A clean customisable Sphinx documentation theme." +name = "ghp-import" +version = "2.1.0" +description = "Copy your docs directly to the gh-pages branch." optional = false -python-versions = ">=3.8" +python-versions = "*" +groups = ["dev"] files = [ - {file = "furo-2024.8.6-py3-none-any.whl", hash = "sha256:6cd97c58b47813d3619e63e9081169880fbe331f0ca883c871ff1f3f11814f5c"}, - {file = "furo-2024.8.6.tar.gz", hash = "sha256:b63e4cee8abfc3136d3bc03a3d45a76a850bada4d6374d24c1716b0e01394a01"}, + {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, + {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, ] [package.dependencies] -beautifulsoup4 = "*" -pygments = ">=2.7" -sphinx = ">=6.0,<9.0" -sphinx-basic-ng = ">=1.0.0.beta2" - -[[package]] -name = "idna" -version = "3.10" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.6" -files = [ - {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, - {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, -] +python-dateutil = ">=2.8.1" [package.extras] -all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] +dev = ["flake8", "markdown", "twine", "wheel"] [[package]] -name = "imagesize" -version = "1.4.1" -description = "Getting image size from png/jpeg/jpeg2000/gif file" +name = "griffe" +version = "1.10.0" +description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.9" +groups = ["dev"] files = [ - {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, - {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, + {file = "griffe-1.10.0-py3-none-any.whl", hash = "sha256:a5eec6d5431cc49eb636b8a078d2409844453c1b0e556e4ba26f8c923047cd11"}, + {file = "griffe-1.10.0.tar.gz", hash = "sha256:7fe89ebfb5140e0589748888b99680968e5b9ef7e2dcb2b01caf87ec552b66be"}, ] +[package.dependencies] +colorama = ">=0.4" + [[package]] name = "iniconfig" version = "2.1.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, @@ -279,6 +110,7 @@ version = "3.1.6" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, @@ -290,12 +122,29 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] +[[package]] +name = "markdown" +version = "3.8.2" +description = "Python implementation of John Gruber's Markdown." +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "markdown-3.8.2-py3-none-any.whl", hash = "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24"}, + {file = "markdown-3.8.2.tar.gz", hash = "sha256:247b9a70dd12e27f67431ce62523e675b866d254f900c4fe75ce3dda62237c45"}, +] + +[package.extras] +docs = ["mdx_gh_links (>=0.2)", "mkdocs (>=1.6)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] +testing = ["coverage", "pyyaml"] + [[package]] name = "markdown-it-py" version = "3.0.0" description = "Python port of markdown-it. Markdown parsing, done right!" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, @@ -320,6 +169,7 @@ version = "3.0.2" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, @@ -390,46 +240,200 @@ version = "0.1.2" description = "Markdown URL utilities" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] +[[package]] +name = "mergedeep" +version = "1.3.4" +description = "A deep merge function for 🐍." +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, + {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +description = "Project documentation with Markdown." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e"}, + {file = "mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2"}, +] + +[package.dependencies] +click = ">=7.0" +colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} +ghp-import = ">=1.0" +jinja2 = ">=2.11.1" +markdown = ">=3.3.6" +markupsafe = ">=2.0.1" +mergedeep = ">=1.3.4" +mkdocs-get-deps = ">=0.2.0" +packaging = ">=20.5" +pathspec = ">=0.11.1" +pyyaml = ">=5.1" +pyyaml-env-tag = ">=0.1" +watchdog = ">=2.0" + +[package.extras] +i18n = ["babel (>=2.9.0)"] +min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4) ; platform_system == \"Windows\"", "ghp-import (==1.0)", "importlib-metadata (==4.4) ; python_version < \"3.10\"", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"] + +[[package]] +name = "mkdocs-autorefs" +version = "1.4.2" +description = "Automatically link across pages in MkDocs." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "mkdocs_autorefs-1.4.2-py3-none-any.whl", hash = "sha256:83d6d777b66ec3c372a1aad4ae0cf77c243ba5bcda5bf0c6b8a2c5e7a3d89f13"}, + {file = "mkdocs_autorefs-1.4.2.tar.gz", hash = "sha256:e2ebe1abd2b67d597ed19378c0fff84d73d1dbce411fce7a7cc6f161888b6749"}, +] + +[package.dependencies] +Markdown = ">=3.3" +markupsafe = ">=2.0.1" +mkdocs = ">=1.1" + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +description = "MkDocs extension that lists all dependencies according to a mkdocs.yml file" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"}, + {file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"}, +] + +[package.dependencies] +mergedeep = ">=1.3.4" +platformdirs = ">=2.2.0" +pyyaml = ">=5.1" + +[[package]] +name = "mkdocstrings" +version = "0.30.0" +description = "Automatic documentation from sources, for MkDocs." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "mkdocstrings-0.30.0-py3-none-any.whl", hash = "sha256:ae9e4a0d8c1789697ac776f2e034e2ddd71054ae1cf2c2bb1433ccfd07c226f2"}, + {file = "mkdocstrings-0.30.0.tar.gz", hash = "sha256:5d8019b9c31ddacd780b6784ffcdd6f21c408f34c0bd1103b5351d609d5b4444"}, +] + +[package.dependencies] +Jinja2 = ">=2.11.1" +Markdown = ">=3.6" +MarkupSafe = ">=1.1" +mkdocs = ">=1.6" +mkdocs-autorefs = ">=1.4" +mkdocstrings-python = {version = ">=1.16.2", optional = true, markers = "extra == \"python\""} +pymdown-extensions = ">=6.3" + +[package.extras] +crystal = ["mkdocstrings-crystal (>=0.3.4)"] +python = ["mkdocstrings-python (>=1.16.2)"] +python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] + +[[package]] +name = "mkdocstrings-python" +version = "1.16.12" +description = "A Python handler for mkdocstrings." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "mkdocstrings_python-1.16.12-py3-none-any.whl", hash = "sha256:22ded3a63b3d823d57457a70ff9860d5a4de9e8b1e482876fc9baabaf6f5f374"}, + {file = "mkdocstrings_python-1.16.12.tar.gz", hash = "sha256:9b9eaa066e0024342d433e332a41095c4e429937024945fea511afe58f63175d"}, +] + +[package.dependencies] +griffe = ">=1.6.2" +mkdocs-autorefs = ">=1.4" +mkdocstrings = ">=0.28.3" +typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} + [[package]] name = "packaging" version = "25.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, ] +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "platformdirs" +version = "4.3.8" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, + {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.14.1)"] + [[package]] name = "pluggy" -version = "1.5.0" +version = "1.6.0" description = "plugin and hook calling mechanisms for python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["dev"] files = [ - {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, - {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, ] [package.extras] dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] +testing = ["coverage", "pytest", "pytest-benchmark"] [[package]] name = "pydantic" -version = "2.11.4" +version = "2.11.7" description = "Data validation using Python type hints" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "pydantic-2.11.4-py3-none-any.whl", hash = "sha256:d9615eaa9ac5a063471da949c8fc16376a84afb5024688b3ff885693506764eb"}, - {file = "pydantic-2.11.4.tar.gz", hash = "sha256:32738d19d63a226a52eed76645a98ee07c1f410ee41d93b4afbfa85ed8111c2d"}, + {file = "pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b"}, + {file = "pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db"}, ] [package.dependencies] @@ -440,7 +444,7 @@ typing-inspection = ">=0.4.0" [package.extras] email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] [[package]] name = "pydantic-core" @@ -448,6 +452,7 @@ version = "2.33.2" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"}, {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"}, @@ -555,93 +560,188 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pygments" -version = "2.19.1" +version = "2.19.2" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ - {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, - {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, ] [package.extras] windows-terminal = ["colorama (>=0.4.6)"] +[[package]] +name = "pymdown-extensions" +version = "10.16.1" +description = "Extension pack for Python Markdown." +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "pymdown_extensions-10.16.1-py3-none-any.whl", hash = "sha256:d6ba157a6c03146a7fb122b2b9a121300056384eafeec9c9f9e584adfdb2a32d"}, + {file = "pymdown_extensions-10.16.1.tar.gz", hash = "sha256:aace82bcccba3efc03e25d584e6a22d27a8e17caa3f4dd9f207e49b787aa9a91"}, +] + +[package.dependencies] +markdown = ">=3.6" +pyyaml = "*" + +[package.extras] +extra = ["pygments (>=2.19.1)"] + [[package]] name = "pytest" -version = "8.3.5" +version = "8.4.1" description = "pytest: simple powerful testing with Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["dev"] files = [ - {file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"}, - {file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"}, + {file = "pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7"}, + {file = "pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c"}, ] [package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -iniconfig = "*" -packaging = "*" +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} +iniconfig = ">=1" +packaging = ">=20" pluggy = ">=1.5,<2" +pygments = ">=2.7.2" tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] [[package]] -name = "requests" -version = "2.32.3" -description = "Python HTTP for Humans." +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" optional = false -python-versions = ">=3.8" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["dev"] files = [ - {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, - {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, ] [package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" +six = ">=1.5" -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +description = "A custom YAML tag for referencing environment variables in YAML files." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04"}, + {file = "pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff"}, +] + +[package.dependencies] +pyyaml = "*" [[package]] name = "rich" -version = "14.0.0" +version = "14.1.0" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.8.0" +groups = ["main"] files = [ - {file = "rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0"}, - {file = "rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725"}, + {file = "rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f"}, + {file = "rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8"}, ] [package.dependencies] markdown-it-py = ">=2.2.0" pygments = ">=2.13.0,<3.0.0" -typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruamel-yaml" -version = "0.18.10" +version = "0.18.14" description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +groups = ["main"] files = [ - {file = "ruamel.yaml-0.18.10-py3-none-any.whl", hash = "sha256:30f22513ab2301b3d2b577adc121c6471f28734d3d9728581245f1e76468b4f1"}, - {file = "ruamel.yaml-0.18.10.tar.gz", hash = "sha256:20c86ab29ac2153f80a428e1254a8adf686d3383df04490514ca3b79a362db58"}, + {file = "ruamel.yaml-0.18.14-py3-none-any.whl", hash = "sha256:710ff198bb53da66718c7db27eec4fbcc9aa6ca7204e4c1df2f282b6fe5eb6b2"}, + {file = "ruamel.yaml-0.18.14.tar.gz", hash = "sha256:7227b76aaec364df15936730efbf7d72b30c0b79b1d578bbb8e3dcb2d81f52b7"}, ] [package.dependencies] -"ruamel.yaml.clib" = {version = ">=0.2.7", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.13\""} +"ruamel.yaml.clib" = {version = ">=0.2.7", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.14\""} [package.extras] docs = ["mercurial (>5.7)", "ryd"] @@ -653,6 +753,8 @@ version = "0.2.12" description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" optional = false python-versions = ">=3.9" +groups = ["main"] +markers = "platform_python_implementation == \"CPython\" and python_version < \"3.14\"" files = [ {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:11f891336688faf5156a36293a9c362bdc7c88f03a8a027c2c1d8e0bcde998e5"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:a606ef75a60ecf3d924613892cc603b154178ee25abb3055db5062da811fd969"}, @@ -708,205 +810,32 @@ version = "1.5.4" description = "Tool to Detect Surrounding Shell" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, ] [[package]] -name = "snowballstemmer" -version = "2.2.0" -description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." -optional = false -python-versions = "*" -files = [ - {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, - {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, -] - -[[package]] -name = "soupsieve" -version = "2.7" -description = "A modern CSS selector implementation for Beautiful Soup." -optional = false -python-versions = ">=3.8" -files = [ - {file = "soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4"}, - {file = "soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a"}, -] - -[[package]] -name = "sphinx" -version = "8.1.3" -description = "Python documentation generator" -optional = false -python-versions = ">=3.10" -files = [ - {file = "sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2"}, - {file = "sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927"}, -] - -[package.dependencies] -alabaster = ">=0.7.14" -babel = ">=2.13" -colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} -docutils = ">=0.20,<0.22" -imagesize = ">=1.3" -Jinja2 = ">=3.1" -packaging = ">=23.0" -Pygments = ">=2.17" -requests = ">=2.30.0" -snowballstemmer = ">=2.2" -sphinxcontrib-applehelp = ">=1.0.7" -sphinxcontrib-devhelp = ">=1.0.6" -sphinxcontrib-htmlhelp = ">=2.0.6" -sphinxcontrib-jsmath = ">=1.0.1" -sphinxcontrib-qthelp = ">=1.0.6" -sphinxcontrib-serializinghtml = ">=1.1.9" -tomli = {version = ">=2", markers = "python_version < \"3.11\""} - -[package.extras] -docs = ["sphinxcontrib-websupport"] -lint = ["flake8 (>=6.0)", "mypy (==1.11.1)", "pyright (==1.1.384)", "pytest (>=6.0)", "ruff (==0.6.9)", "sphinx-lint (>=0.9)", "tomli (>=2)", "types-Pillow (==10.2.0.20240822)", "types-Pygments (==2.18.0.20240506)", "types-colorama (==0.4.15.20240311)", "types-defusedxml (==0.7.0.20240218)", "types-docutils (==0.21.0.20241005)", "types-requests (==2.32.0.20240914)", "types-urllib3 (==1.26.25.14)"] -test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "setuptools (>=70.0)", "typing_extensions (>=4.9)"] - -[[package]] -name = "sphinx-basic-ng" -version = "1.0.0b2" -description = "A modern skeleton for Sphinx themes." -optional = false -python-versions = ">=3.7" -files = [ - {file = "sphinx_basic_ng-1.0.0b2-py3-none-any.whl", hash = "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b"}, - {file = "sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9"}, -] - -[package.dependencies] -sphinx = ">=4.0" - -[package.extras] -docs = ["furo", "ipython", "myst-parser", "sphinx-copybutton", "sphinx-inline-tabs"] - -[[package]] -name = "sphinx-tabs" -version = "3.4.7" -description = "Tabbed views for Sphinx" -optional = false -python-versions = ">=3.7" -files = [ - {file = "sphinx-tabs-3.4.7.tar.gz", hash = "sha256:991ad4a424ff54119799ba1491701aa8130dd43509474aef45a81c42d889784d"}, - {file = "sphinx_tabs-3.4.7-py3-none-any.whl", hash = "sha256:c12d7a36fd413b369e9e9967a0a4015781b71a9c393575419834f19204bd1915"}, -] - -[package.dependencies] -docutils = "*" -pygments = "*" -sphinx = ">=1.8" - -[package.extras] -code-style = ["pre-commit (==2.13.0)"] -testing = ["bs4", "coverage", "pygments", "pytest (>=7.1,<8)", "pytest-cov", "pytest-regressions", "rinohtype"] - -[[package]] -name = "sphinxcontrib-applehelp" -version = "2.0.0" -description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" -optional = false -python-versions = ">=3.9" -files = [ - {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}, - {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}, -] - -[package.extras] -lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] -standalone = ["Sphinx (>=5)"] -test = ["pytest"] - -[[package]] -name = "sphinxcontrib-devhelp" -version = "2.0.0" -description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" -optional = false -python-versions = ">=3.9" -files = [ - {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}, - {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}, -] - -[package.extras] -lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] -standalone = ["Sphinx (>=5)"] -test = ["pytest"] - -[[package]] -name = "sphinxcontrib-htmlhelp" -version = "2.1.0" -description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" -optional = false -python-versions = ">=3.9" -files = [ - {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, - {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, -] - -[package.extras] -lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] -standalone = ["Sphinx (>=5)"] -test = ["html5lib", "pytest"] - -[[package]] -name = "sphinxcontrib-jsmath" -version = "1.0.1" -description = "A sphinx extension which renders display math in HTML via JavaScript" -optional = false -python-versions = ">=3.5" -files = [ - {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, - {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, -] - -[package.extras] -test = ["flake8", "mypy", "pytest"] - -[[package]] -name = "sphinxcontrib-qthelp" -version = "2.0.0" -description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" +name = "six" +version = "1.17.0" +description = "Python 2 and 3 compatibility utilities" optional = false -python-versions = ">=3.9" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["dev"] files = [ - {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}, - {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}, + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] -[package.extras] -lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] -standalone = ["Sphinx (>=5)"] -test = ["defusedxml (>=0.7.1)", "pytest"] - -[[package]] -name = "sphinxcontrib-serializinghtml" -version = "2.0.0" -description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" -optional = false -python-versions = ">=3.9" -files = [ - {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, - {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, -] - -[package.extras] -lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] -standalone = ["Sphinx (>=5)"] -test = ["pytest"] - [[package]] name = "tomli" version = "2.2.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version == \"3.10\"" files = [ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, @@ -944,64 +873,94 @@ files = [ [[package]] name = "typer" -version = "0.15.3" +version = "0.15.4" description = "Typer, build great CLIs. Easy to code. Based on Python type hints." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ - {file = "typer-0.15.3-py3-none-any.whl", hash = "sha256:c86a65ad77ca531f03de08d1b9cb67cd09ad02ddddf4b34745b5008f43b239bd"}, - {file = "typer-0.15.3.tar.gz", hash = "sha256:818873625d0569653438316567861899f7e9972f2e6e0c16dab608345ced713c"}, + {file = "typer-0.15.4-py3-none-any.whl", hash = "sha256:eb0651654dcdea706780c466cf06d8f174405a659ffff8f163cfbfee98c0e173"}, + {file = "typer-0.15.4.tar.gz", hash = "sha256:89507b104f9b6a0730354f27c39fae5b63ccd0c95b1ce1f1a6ba0cfd329997c3"}, ] [package.dependencies] -click = ">=8.0.0" +click = ">=8.0.0,<8.2" rich = ">=10.11.0" shellingham = ">=1.3.0" typing-extensions = ">=3.7.4.3" [[package]] name = "typing-extensions" -version = "4.13.2" -description = "Backported and Experimental Type Hints for Python 3.8+" +version = "4.14.1" +description = "Backported and Experimental Type Hints for Python 3.9+" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main", "dev"] files = [ - {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, - {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, + {file = "typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76"}, + {file = "typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36"}, ] +markers = {dev = "python_version == \"3.10\""} [[package]] name = "typing-inspection" -version = "0.4.0" +version = "0.4.1" description = "Runtime typing introspection tools" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f"}, - {file = "typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122"}, + {file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"}, + {file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"}, ] [package.dependencies] typing-extensions = ">=4.12.0" [[package]] -name = "urllib3" -version = "2.4.0" -description = "HTTP library with thread-safe connection pooling, file post, and more." +name = "watchdog" +version = "6.0.0" +description = "Filesystem events monitoring" optional = false python-versions = ">=3.9" -files = [ - {file = "urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813"}, - {file = "urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466"}, +groups = ["dev"] +files = [ + {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c"}, + {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881"}, + {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11"}, + {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa"}, + {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2"}, + {file = "watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a"}, + {file = "watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680"}, + {file = "watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f"}, + {file = "watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -h2 = ["h2 (>=4,<5)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] +watchmedo = ["PyYAML (>=3.10)"] [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "^3.10" -content-hash = "4821238bf1d24a0a5c5afd1d7852fd0862b7e2dfaf564a897df87c8e1310107b" +content-hash = "4518c79cecbdff96f897154506a0a0183050a76ac4dda54278518f4629690f58" diff --git a/pyproject.toml b/pyproject.toml index 8b8e26a..c18b327 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,10 @@ [tool.poetry] name = "importspy" -version = "0.3.3" +version = "0.4.0" description = "ImportSpy ensures structural integrity, runtime compliance, and security for external modules, preventing inconsistencies and enforcing controlled execution." license = "MIT" authors = ["Luca Atella "] -readme = "README.rst" +readme = "README.md" packages = [{include = "importspy", from = "src"}] [tool.poetry.dependencies] @@ -12,13 +12,13 @@ python = "^3.10" pydantic = "^2.9.2" ruamel-yaml = "^0.18.10" typer = "^0.15.2" +pymdown-extensions = "^10.16.1" [tool.poetry.group.dev.dependencies] -furo = "^2024.8.6" -sphinx = ">=5,<9" pytest = "^8.3.3" -sphinx-tabs = "^3.4.7" +mkdocs = "^1.6.1" +mkdocstrings = {version = "^0.30.0", extras = ["python"]} [tool.poetry.urls] Repository = "https://github.com/atellaluca/importspy" diff --git a/src/importspy/cli.py b/src/importspy/cli.py index 837fb5a..db25877 100644 --- a/src/importspy/cli.py +++ b/src/importspy/cli.py @@ -1,57 +1,27 @@ """ -Command-line interface for ImportSpy import contract validation. +Command-line interface (CLI) for validating Python modules against ImportSpy contracts. -This module defines the CLI entry point `importspy`, which enables developers and CI/CD pipelines -to validate Python modules against a declared import contract written in YAML format. +This module defines the `importspy` CLI command, enabling local and automated validation +of a Python file against a YAML-based SpyModel contract. It is designed for use in +CI/CD pipelines, plugin systems, or developer workflows. -Overview: ---------- -The CLI allows structural and runtime compliance checks through: -- Dynamic import of a Python module from file. -- Parsing of the import contract from a `.yml` file. -- Validation of the module’s structure and metadata. -- Clear CLI feedback with styled messages. -- Optional log verbosity control for debugging purposes. +Features: +- Loads and executes the specified Python module. +- Parses the YAML contract file describing expected structure and runtime conditions. +- Validates that the module complies with the declared interface and environment. +- Provides user-friendly CLI feedback, including optional logging. -Main Command: -------------- -- `importspy`: Validates a Python module against an import contract definition. +Use cases: +- Enforcing structure of external plugins before loading. +- Automating validation in GitHub Actions or other CI tools. +- Assuring consistency in modular libraries or educational tools. -Decorators: ------------ -- `handle_validation_error`: Intercepts and formats validation errors - to improve user experience from the terminal. +Example: + importspy ./examples/my_plugin.py -s ./contracts/expected.yml --log-level DEBUG -Usage Examples: ---------------- -Basic validation: - -.. code-block:: bash - - importspy ./examples/my_module.py - -With contract and log level: - -.. code-block:: bash - - importspy ./my_module.py --spymodel contracts/example.yml --log-level DEBUG - -Options: --------- ---spymodel / -s : str - Path to the YAML file containing the import contract. Default: `spymodel.yml`. - ---log-level / -l : str - Log verbosity. Accepts: DEBUG, INFO, WARNING, ERROR. - ---version / -v - Show ImportSpy’s current version. - -Notes: ------- -- Validation is handled by the `Spy` core class. -- This command is ideal for local development, CI enforcement, or release pipelines. -- Validation issues are surfaced through color-coded output, not raw exceptions. +Note: + Validation is powered by the core `Spy` class. + Validation errors are caught and displayed with enhanced CLI formatting. """ import typer @@ -70,17 +40,20 @@ def handle_validation_error(func): """ - Intercepts validation errors and formats them for CLI output. + Decorator that formats validation errors for CLI output. - Provides color-coded feedback based on validation result. + Intercepts `ValueError` raised by the `Spy.importspy()` call and presents + the error reason in a readable, styled terminal message. + + Used to wrap the main `importspy()` CLI command. """ @functools.wraps(func) def wrapper(*args, **kwargs): try: func(*args, **kwargs) - typer.echo(typer.style("✅ Module is compliant with the import contract!", fg=typer.colors.GREEN, bold=True)) + typer.echo(typer.style("Module is compliant with the import contract.", fg=typer.colors.GREEN, bold=True)) except ValueError as ve: - typer.echo(typer.style("❌ Module is NOT compliant with the import contract.", fg=typer.colors.RED, bold=True)) + typer.echo(typer.style("Module is NOT compliant with the import contract.", fg=typer.colors.RED, bold=True)) typer.echo() typer.secho("Reason:", fg="magenta", bold=True) typer.echo(f" {typer.style(str(ve), fg='yellow')}") @@ -97,24 +70,45 @@ class LogLevel(str, Enum): @app.command() @handle_validation_error def importspy( - version: Optional[bool] = typer.Option( - None, - "--version", - "-v", - callback=lambda value: show_version(value), - is_eager=True, - help="Show the version and exit." - ), - modulepath: Optional[str] = typer.Argument(str, help="Path to the Python module to load and validate."), - spymodel_path: Optional[str] = typer.Option( - "spymodel.yml", "--spymodel", "-s", help="Path to the import contract file (.yml)." - ), - log_level: Optional[LogLevel] = typer.Option( - None, "--log-level", "-l", help="Log level for output verbosity." - ) + version: Optional[bool] = typer.Option( + None, + "--version", + "-v", + callback=lambda value: show_version(value), + is_eager=True, + help="Show the version and exit." + ), + modulepath: Optional[str] = typer.Argument( + str, + help="Path to the Python module to load and validate." + ), + spymodel_path: Optional[str] = typer.Option( + "spymodel.yml", + "--spymodel", + "-s", + help="Path to the import contract file (.yml)." + ), + log_level: Optional[LogLevel] = typer.Option( + None, + "--log-level", + "-l", + help="Log level for output verbosity." + ) ) -> ModuleType: """ - CLI command to validate a Python module against a YAML-defined import contract. + Validates a Python module against a YAML-defined SpyModel contract. + + Args: + version (bool, optional): Show ImportSpy version and exit. + modulepath (str): Path to the Python module to validate. + spymodel_path (str, optional): Path to the YAML contract file. Defaults to `spymodel.yml`. + log_level (LogLevel, optional): Set logging verbosity (DEBUG, INFO, WARNING, ERROR). + + Returns: + ModuleType: The validated Python module (if compliant). + + Raises: + ValueError: If the module does not conform to the contract. """ module_path = Path(modulepath).resolve() module_name = module_path.stem @@ -131,7 +125,10 @@ def importspy( def show_version(value: bool): """ - Displays the current ImportSpy version and exits. + Displays the current version of ImportSpy and exits the process. + + Args: + value (bool): If True, prints the version and exits immediately. """ if value: typer.secho(f"ImportSpy v{__version__}", fg="cyan", bold=True) @@ -139,6 +136,10 @@ def show_version(value: bool): def main(): """ - Entry point for CLI execution. + CLI entry point. + + Executes the `importspy` Typer app, allowing CLI usage like: + + $ importspy my_module.py -s my_contract.yml """ app() diff --git a/src/importspy/config.py b/src/importspy/config.py index 663a1bb..1f875b3 100644 --- a/src/importspy/config.py +++ b/src/importspy/config.py @@ -1,63 +1,32 @@ class Config: + """ - Static configuration container for ImportSpy. + Static configuration for ImportSpy. - This class centralizes all constant values used for runtime validation, - defining supported architectures, operating systems, Python versions, - interpreter implementations, attribute classifications, and annotation types. - These constants ensure consistency and safety during contract enforcement - across diverse execution environments. + This class defines the baseline constants used during runtime and structural + validation of Python modules. Values declared here represent the supported + options for platforms, interpreters, Python versions, class attribute types, + and type annotations within a SpyModel contract. - Attributes: - ARCH_x86_64 (str): Identifier for the x86_64 CPU architecture. - ARCH_AARCH64 (str): Identifier for the AArch64 architecture. - ARCH_ARM (str): Identifier for the ARM architecture. - ARCH_ARM64 (str): Identifier for the ARM64 architecture. - ARCH_I386 (str): Identifier for the i386 (32-bit Intel) architecture. - ARCH_PPC64 (str): Identifier for the PowerPC 64-bit architecture. - ARCH_PPC64LE (str): Identifier for PowerPC 64-bit Little Endian architecture. - ARCH_S390X (str): Identifier for IBM s390x architecture. + These constants are used internally to validate compatibility and enforce + declared constraints across diverse environments. - OS_WINDOWS (str): Identifier for Windows operating systems. - OS_LINUX (str): Identifier for Linux operating systems. - OS_MACOS (str): Identifier for macOS (Darwin-based) operating systems. + Categories: + ---------- + • Architectures: CPU instruction sets (e.g. x86_64, arm64). + • Operating Systems: Target OS identifiers (e.g. linux, windows). + • Python Versions: Compatible interpreter versions. + • Interpreters: Supported Python implementations. + • Attribute Types: Class vs. instance variables. + • Type Annotations: Accepted runtime-compatible types. - PYTHON_VERSION_3_13 (str): Supported Python version 3.13. - PYTHON_VERSION_3_12 (str): Supported Python version 3.12. - PYTHON_VERSION_3_11 (str): Supported Python version 3.11. - PYTHON_VERSION_3_10 (str): Supported Python version 3.10. - PYTHON_VERSION_3_9 (str): Supported Python version 3.9. - - INTERPRETER_CPYTHON (str): Identifier for CPython interpreter. - INTERPRETER_PYPY (str): Identifier for PyPy interpreter. - INTERPRETER_JYTHON (str): Identifier for Jython interpreter. - INTERPRETER_IRON_PYTHON (str): Identifier for IronPython interpreter. - INTERPRETER_STACKLESS (str): Identifier for Stackless Python. - INTERPRETER_MICROPYTHON (str): Identifier for MicroPython interpreter. - INTERPRETER_BRYTHON (str): Identifier for Brython interpreter. - INTERPRETER_PYSTON (str): Identifier for Pyston interpreter. - INTERPRETER_GRAALPYTHON (str): Identifier for GraalPython interpreter. - INTERPRETER_RUSTPYTHON (str): Identifier for RustPython interpreter. - INTERPRETER_NUITKA (str): Identifier for Nuitka interpreter. - INTERPRETER_TRANSCRYPT (str): Identifier for Transcrypt interpreter. - - CLASS_TYPE (str): Label for class-level attributes in contract definitions. - INSTANCE_TYPE (str): Label for instance-level attributes in contract definitions. - - ANNOTATION_INT (str): Annotation identifier for integers. - ANNOTATION_FLOAT (str): Annotation identifier for floats. - ANNOTATION_STR (str): Annotation identifier for strings. - ANNOTATION_BOOL (str): Annotation identifier for booleans. - ANNOTATION_LIST (str): Annotation identifier for generic lists. - ANNOTATION_DICT (str): Annotation identifier for generic dictionaries. - ANNOTATION_TUPLE (str): Annotation identifier for generic tuples. - ANNOTATION_SET (str): Annotation identifier for sets. - ANNOTATION_OPTIONAL (str): Annotation identifier for optional values. - ANNOTATION_UNION (str): Annotation identifier for union types. - ANNOTATION_ANY (str): Annotation identifier for untyped (any) values. - ANNOTATION_CALLABLE (str): Annotation identifier for callable objects. + Examples: + --------- + - A contract may require `arch: x86_64` and `interpreter: CPython`. + - A method argument may be annotated with `Optional[str]`. """ + # Supported Architectures ARCH_x86_64 = "x86_64" ARCH_AARCH64 = "aarch64" @@ -111,6 +80,6 @@ class Config: ANNOTATION_UNION = "Union" ANNOTATION_ANY = "Any" ANNOTATION_CALLABLE = "Callable" - ANNOTATION_LIST = "List" - ANNOTATION_DICT = "Dict" - ANNOTATION_TUPLE = "Tuple" + ANNOTATION_LIST_TYPING = "List" + ANNOTATION_DICT_TYPING = "Dict" + ANNOTATION_TUPLE_TYPING = "Tuple" diff --git a/src/importspy/constants.py b/src/importspy/constants.py index 6bf93ee..28442e8 100644 --- a/src/importspy/constants.py +++ b/src/importspy/constants.py @@ -1,131 +1,283 @@ from .config import Config +from enum import Enum class Constants: """ - Constants used internally by ImportSpy's runtime validation engine. + Canonical constants used by ImportSpy's runtime validation engine. - This class defines the canonical reference values used during import contract - validation, including supported architectures, operating systems, Python - interpreters, annotation types, and structural metadata keys. + This class acts as a reference for valid architectures, operating systems, + Python interpreters, structural types, and annotation labels. All values + defined here represent the allowed forms of metadata used to verify + import contracts. - Unlike `Config`, which defines values dynamically from the runtime or user - environment, `Constants` serves as the fixed baseline for what ImportSpy - considers valid and contract-compliant. + Unlike `Config`, which reflects the current runtime, `Constants` provides + the fixed set of values used for validation logic. + """ - Attributes: - KNOWN_ARCHITECTURES (List[str]): - List of CPU architectures supported in runtime validation, - including 'x86_64', 'arm64', 'i386', and others. + class SupportedArchitectures(str, Enum): + """Valid CPU architectures accepted in contracts.""" + ARCH_x86_64 = Config.ARCH_x86_64 + ARCH_AARCH64 = Config.ARCH_AARCH64 + ARCH_ARM = Config.ARCH_ARM + ARCH_ARM64 = Config.ARCH_ARM64 + ARCH_I386 = Config.ARCH_I386 + ARCH_PPC64 = Config.ARCH_PPC64 + ARCH_PPC64LE = Config.ARCH_PPC64LE + ARCH_S390X = Config.ARCH_S390X - SUPPORTED_OS (List[str]): - List of supported operating systems: 'linux', 'windows', and 'darwin'. + class SupportedOS(str, Enum): + """Valid operating systems accepted in contracts.""" + OS_WINDOWS = Config.OS_WINDOWS + OS_LINUX = Config.OS_LINUX + OS_MACOS = Config.OS_MACOS - SUPPORTED_PYTHON_VERSION (List[str]): - List of supported Python versions, e.g. '3.9', '3.10', '3.11', etc. + class SupportedPythonImplementations(str, Enum): + """Valid Python interpreter implementations.""" + INTERPRETER_CPYTHON = Config.INTERPRETER_CPYTHON + INTERPRETER_PYPY = Config.INTERPRETER_PYPY + INTERPRETER_JYTHON = Config.INTERPRETER_JYTHON + INTERPRETER_IRON_PYTHON = Config.INTERPRETER_IRON_PYTHON + INTERPRETER_MICROPYTHON = Config.INTERPRETER_MICROPYTHON + INTERPRETER_BRYTHON = Config.INTERPRETER_BRYTHON + INTERPRETER_PYSTON = Config.INTERPRETER_PYSTON + INTERPRETER_GRAALPYTHON = Config.INTERPRETER_GRAALPYTHON + INTERPRETER_RUSTPYTHON = Config.INTERPRETER_RUSTPYTHON + INTERPRETER_NUITKA = Config.INTERPRETER_NUITKA + INTERPRETER_TRANSCRYPT = Config.INTERPRETER_TRANSCRYPT - SUPPORTED_PYTHON_IMPLEMENTATION (List[str]): - Python interpreter implementations recognized by ImportSpy, - such as 'CPython', 'PyPy', 'IronPython', and others. + class SupportedClassAttributeTypes(str, Enum): + """Type of attribute in a class-level contract.""" + CLASS = Config.CLASS_TYPE + INSTANCE = Config.INSTANCE_TYPE - SUPPORTED_CLASS_ATTRIBUTE_TYPES (List[str]): - Allowed attribute type classifications: 'class' and 'instance'. + NAME = "Name" + VALUE = "Value" + ANNOTATION = "Annotation" - SUPPORTED_ANNOTATIONS (List[str]): - Allowed annotation types used for validating variables, - arguments, and return values. Includes types such as - 'int', 'str', 'Optional', 'Union', 'Callable', etc. + CLASS_TYPE = Config.CLASS_TYPE + INSTANCE_TYPE = Config.INSTANCE_TYPE - NAME (str): - Metadata key used for referencing object names in the model. + class SupportedAnnotations(str, Enum): + """Supported type annotations for validation purposes.""" + INT = Config.ANNOTATION_INT + FLOAT = Config.ANNOTATION_FLOAT + STR = Config.ANNOTATION_STR + BOOL = Config.ANNOTATION_BOOL + LIST = Config.ANNOTATION_LIST + DICT = Config.ANNOTATION_DICT + TUPLE = Config.ANNOTATION_TUPLE + SET = Config.ANNOTATION_SET + OPTIONAL = Config.ANNOTATION_OPTIONAL + UNION = Config.ANNOTATION_UNION + ANY = Config.ANNOTATION_ANY + CALLABLE = Config.ANNOTATION_CALLABLE + LIST_TYPING = Config.ANNOTATION_LIST_TYPING + DICT_TYPING = Config.ANNOTATION_DICT_TYPING + TUPLE_TYPING = Config.ANNOTATION_TUPLE_TYPING - VALUE (str): - Metadata key used to represent literal values in contracts. + LOG_MESSAGE_TEMPLATE = ( + "[Operation: {operation}] [Status: {status}] [Details: {details}]" + ) - ANNOTATION (str): - Metadata key used to refer to a declared annotation in contracts. - CLASS_TYPE (str): - String literal used to label a class-level attribute type. +class Contexts(str, Enum): + """ + Context types used for contract validation. + + These labels identify which layer of the system the error or constraint applies to. + """ + RUNTIME_CONTEXT = "runtime" + ENVIRONMENT_CONTEXT = "environment" + MODULE_CONTEXT = "module" + CLASS_CONTEXT = "class" - INSTANCE_TYPE (str): - String literal used to label an instance-level attribute type. - LOG_MESSAGE_TEMPLATE (str): - Template string for standardized log message formatting - during contract evaluation and model parsing. +class Errors: """ + Reusable error string templates and labels. - KNOWN_ARCHITECTURES = [ - Config.ARCH_x86_64, - Config.ARCH_AARCH64, - Config.ARCH_ARM, - Config.ARCH_ARM64, - Config.ARCH_I386, - Config.ARCH_PPC64, - Config.ARCH_PPC64LE, - Config.ARCH_S390X - ] - - SUPPORTED_OS = [ - Config.OS_WINDOWS, - Config.OS_LINUX, - Config.OS_MACOS - ] - - SUPPORTED_PYTHON_VERSION=[ - Config.PYTHON_VERSION_3_13, - Config.PYTHON_VERSION_3_12, - Config.PYTHON_VERSION_3_11, - Config.PYTHON_VERSION_3_10, - Config.PYTHON_VERSION_3_9 - ] - - SUPPORTED_PYTHON_IMPLEMENTATION = [ - Config.INTERPRETER_CPYTHON, - Config.INTERPRETER_PYPY, - Config.INTERPRETER_JYTHON, - Config.INTERPRETER_IRON_PYTHON, - Config.INTERPRETER_MICROPYTHON, - Config.INTERPRETER_BRYTHON, - Config.INTERPRETER_PYSTON, - Config.INTERPRETER_GRAALPYTHON, - Config.INTERPRETER_RUSTPYTHON, - Config.INTERPRETER_NUITKA, - Config.INTERPRETER_TRANSCRYPT - ] - - SUPPORTED_CLASS_ATTRIBUTE_TYPES = [ - Config.CLASS_TYPE, - Config.INSTANCE_TYPE - ] + This utility provides consistent formatting for all error messages + generated by ImportSpy. It supports singular and plural forms, as well as + different validation categories: missing, mismatch, and invalid. + """ - NAME = "Name" - VALUE = "Value" - ANNOTATION = "Annotation" + TEMPLATE_KEY = "template" + SOLUTION_KEY = "solution" + SCOPE_VARIABLE = "variable" + SCOPE_ARGUMENT = "argument" - CLASS_TYPE = Config.CLASS_TYPE - INSTANCE_TYPE = Config.INSTANCE_TYPE + ENTITY_MESSAGES = "entity" + COLLECTIONS_MESSAGES = "collections" - SUPPORTED_ANNOTATIONS = [ - Config.ANNOTATION_INT, - Config.ANNOTATION_FLOAT, - Config.ANNOTATION_STR, - Config.ANNOTATION_BOOL, - Config.ANNOTATION_LIST, - Config.ANNOTATION_DICT, - Config.ANNOTATION_TUPLE, - Config.ANNOTATION_SET, - Config.ANNOTATION_OPTIONAL, - Config.ANNOTATION_UNION, - Config.ANNOTATION_ANY, - Config.ANNOTATION_CALLABLE, - Config.ANNOTATION_LIST, - Config.ANNOTATION_DICT, - Config.ANNOTATION_TUPLE - ] + CONTEXT_INTRO = { + Contexts.RUNTIME_CONTEXT: "Runtime constraint violation", + Contexts.ENVIRONMENT_CONTEXT: "Environment validation failure", + Contexts.MODULE_CONTEXT: "Module structural inconsistency", + Contexts.CLASS_CONTEXT: "Class contract violation" + } - LOG_MESSAGE_TEMPLATE = ( - "[Operation: {operation}] [Status: {status}] " - "[Details: {details}]" - ) + class Category(str, Enum): + """Validation error types.""" + MISSING = "missing" + MISMATCH = "mismatch" + INVALID = "invalid" + + # Label formatting templates for each contract context + RUNTIME_LABEL_TEMPLATE = { + ENTITY_MESSAGES: 'The runtime "{runtime_1}"', + COLLECTIONS_MESSAGES: 'The runtimes "{runtimes_1}"' + } + + SYSTEM_LABEL_TEMPLATE = { + ENTITY_MESSAGES: 'The system "{system_1}"', + COLLECTIONS_MESSAGES: 'systems "{systems_1}"' + } + + PYTHON_LABEL_TEMPLATE = { + ENTITY_MESSAGES: 'The python "{python_1}"', + COLLECTIONS_MESSAGES: 'The pythons "{pythons_1}"' + } + + VARIABLES_LABEL_TEMPLATE = { + SCOPE_VARIABLE: { + ENTITY_MESSAGES: { + Contexts.ENVIRONMENT_CONTEXT: 'The environment variable "{environment_variable_name}"', + Contexts.MODULE_CONTEXT: 'The variable "{variable_name}" in module "{module_name}"', + Contexts.CLASS_CONTEXT: 'The {attribute_type} attribute "{attribute_name}" in class "{class_name}"' + }, + COLLECTIONS_MESSAGES: { + Contexts.ENVIRONMENT_CONTEXT: 'The environment "{environment_1}"', + Contexts.MODULE_CONTEXT: 'The variables "{variables_1}"', + Contexts.CLASS_CONTEXT: 'The attributes "{attributes_1}"' + } + }, + SCOPE_ARGUMENT: { + ENTITY_MESSAGES: { + Contexts.MODULE_CONTEXT: 'The argument "{argument_name}" of function "{function_name}"', + Contexts.CLASS_CONTEXT: 'The argument "{argument_name}" of method "{method_name}" in class "{class_name}"' + }, + COLLECTIONS_MESSAGES: { + Contexts.MODULE_CONTEXT: 'The arguments "{arguments_1}" of function "{function_name}"', + Contexts.CLASS_CONTEXT: 'The arguments "{arguments_1}" of method "{method_name}", in class "{class_name}"' + } + } + } + + FUNCTIONS_LABEL_TEMPLATE = { + ENTITY_MESSAGES: { + Contexts.MODULE_CONTEXT: 'The function "{function_name}" in module "{filename}"', + Contexts.CLASS_CONTEXT: 'The method "{method_name}" in class "{class_name}"' + }, + COLLECTIONS_MESSAGES: { + Contexts.MODULE_CONTEXT: 'The functions "{functions_1}" in module "{filename}"', + Contexts.CLASS_CONTEXT: 'The methods "{methods_1}" in class "{class_name}"' + } + } + + MODULE_LABEL_TEMPLATE = { + ENTITY_MESSAGES: { + Contexts.CLASS_CONTEXT: 'The class "{class_name}"', + Contexts.RUNTIME_CONTEXT: 'The module "{filename}"', + Contexts.ENVIRONMENT_CONTEXT: 'The version "{version}" of module "{filename}"' + }, + COLLECTIONS_MESSAGES: { + Contexts.CLASS_CONTEXT: 'The classes "{classes_1}" in module "{filename}"' + } + } + + # Dynamic variable keys used in label formatting + KEY_RUNTIMES_1 = "runtimes_1" + KEY_SYSTEMS_1 = "systems_1" + KEY_PYTHONS_1 = "pythons_1" + KEY_PYTHON_1 = "python_1" + KEY_ENVIRONMENT_1 = "environment_1" + KEY_ENVIRONMENT_VARIABLE_NAME = "environment_variable_name" + KEY_MODULES_1 = "modules_1" + KEY_VARIABLES_1 = "variables_1" + KEY_ATTRIBUTES_1 = "attributes_1" + KEY_ARGUMENTS_1 = "arguments_1" + KEY_FUNCTIONS_1 = "functions_1" + KEY_CLASSES_1 = "classes_1" + KEY_METHODS_1 = "methods_1" + + KEY_VARIABLE_NAME = "variable_name" + KEY_ARGUMENT_NAME = "argument_name" + KEY_FUNCTION_NAME = "function_name" + KEY_METHOD_NAME = "method_name" + KEY_MODULE_NAME = "module_name" + KEY_ATTRIBUTE_TYPE = "attribute_type" + KEY_ATTRIBUTE_NAME = "attribute_name" + KEY_CLASS_NAME = "class_name" + KEY_MODULE_VERSION = "version" + KEY_FILE_NAME = "filename" + + # Dynamic template values used to build error labels + VARIABLES_DINAMIC_PAYLOAD = { + SCOPE_VARIABLE: { + ENTITY_MESSAGES: { + Contexts.ENVIRONMENT_CONTEXT: KEY_ENVIRONMENT_VARIABLE_NAME, + Contexts.MODULE_CONTEXT: KEY_VARIABLE_NAME, + Contexts.CLASS_CONTEXT: KEY_ATTRIBUTE_NAME + }, + COLLECTIONS_MESSAGES: { + Contexts.ENVIRONMENT_CONTEXT: KEY_ENVIRONMENT_1, + Contexts.MODULE_CONTEXT: KEY_VARIABLES_1, + Contexts.CLASS_CONTEXT: KEY_ATTRIBUTES_1 + } + }, + SCOPE_ARGUMENT: { + ENTITY_MESSAGES: { + Contexts.MODULE_CONTEXT: KEY_ARGUMENT_NAME, + Contexts.CLASS_CONTEXT: KEY_ARGUMENT_NAME + }, + COLLECTIONS_MESSAGES: { + Contexts.MODULE_CONTEXT: KEY_ARGUMENTS_1, + Contexts.CLASS_CONTEXT: KEY_ARGUMENTS_1 + } + } + } + + FUNCTIONS_DINAMIC_PAYLOAD = { + ENTITY_MESSAGES: { + Contexts.MODULE_CONTEXT: KEY_FUNCTION_NAME, + Contexts.CLASS_CONTEXT: KEY_METHOD_NAME + }, + COLLECTIONS_MESSAGES: { + Contexts.MODULE_CONTEXT: KEY_FUNCTIONS_1, + Contexts.CLASS_CONTEXT: KEY_METHODS_1 + } + } + + ERROR_MESSAGE_TEMPLATES = { + Category.MISSING: { + ENTITY_MESSAGES: { + TEMPLATE_KEY: '{label} is declared but missing.', + SOLUTION_KEY: 'Ensure it is properly defined and implemented.' + }, + COLLECTIONS_MESSAGES: { + TEMPLATE_KEY: '{label} are declared but missing.', + SOLUTION_KEY: 'Ensure all of them are properly defined and implemented.' + } + }, + Category.MISMATCH: { + ENTITY_MESSAGES: { + TEMPLATE_KEY: '{label} does not match the expected value. Expected: {expected!r}, Found: {actual!r}.', + SOLUTION_KEY: 'Check the value and update the contract or implementation accordingly.' + }, + COLLECTIONS_MESSAGES: { + TEMPLATE_KEY: '{label} do not match the expected values. Expected: {expected!r}, Found: {actual!r}.', + SOLUTION_KEY: 'Review the values and update the contract or implementation as needed.' + } + }, + Category.INVALID: { + ENTITY_MESSAGES: { + TEMPLATE_KEY: '{label} has an invalid value. Allowed values: {allowed}. Found: {found!r}.', + SOLUTION_KEY: 'Update the value to one of the allowed options.' + }, + COLLECTIONS_MESSAGES: { + TEMPLATE_KEY: '{label} have invalid values. Allowed values: {allowed}. Found: {found!r}.', + SOLUTION_KEY: 'Update the values to be within the allowed options.' + } + } + } diff --git a/src/importspy/errors.py b/src/importspy/errors.py deleted file mode 100644 index e5b0562..0000000 --- a/src/importspy/errors.py +++ /dev/null @@ -1,139 +0,0 @@ -class Errors: - - """ - Central repository for error messages used in ImportSpy’s validation engine. - - This class contains formatted string constants for every type of structural, - semantic, and runtime validation error that can be raised during contract - evaluation. These error messages provide actionable feedback and are used - throughout ImportSpy's exception handling system. - - The format strings typically include placeholders for contextual details, - such as expected and actual values, function names, class names, or - annotation types. Grouped by category, these constants help keep the - validation engine consistent and maintainable. - - Attributes: - ANALYSIS_RECURSION_WARNING (str): - General warning when the validation process detects recursive self-analysis. - - FILENAME_MISMATCH (str): - Raised when the module filename does not match the expected contract. - - VERSION_MISMATCH (str): - Triggered when the module version deviates from the one declared in the contract. - - ENV_VAR_MISSING (str): - Raised when a required environment variable is not found in the system. - - ENV_VAR_MISMATCH (str): - Indicates a mismatch between the expected and actual values of an environment variable. - - VAR_MISSING (str): - Raised when a required variable is not present in the importing module. - - VAR_MISMATCH (str): - Raised when a variable is present but its value does not match what the contract expects. - - FUNCTIONS_MISSING (str): - Used when one or more expected functions are missing from the module. - - FUNCTION_RETURN_ANNOTATION_MISMATCH (str): - Indicates a mismatch in the return type annotation of a function. - - VARIABLE_MISMATCH (str): - Raised when a declared variable's value does not match the expected value. - - VARIABLE_MISSING (str): - Raised when a declared variable is not found. - - ARGUMENT_MISMATCH (str): - Raised when a function argument has an unexpected name or annotation. - - ARGUMENT_MISSING (str): - Raised when a required argument is missing in the function signature. - - CLASS_MISSING (str): - Triggered when a required class is not defined in the importing module. - - CLASS_ATTRIBUTE_MISSING (str): - Raised when an expected attribute is not found in a class definition. - - CLASS_ATTRIBUTE_MISMATCH (str): - Raised when an attribute exists but its value does not match what the contract expects. - - CLASS_SUPERCLASS_MISSING (str): - Triggered when a required superclass is missing from a class declaration. - - INVALID_ATTRIBUTE_TYPE (str): - Raised when an attribute has an unsupported type. - - INVALID_ARCHITECTURE (str): - Triggered when the system architecture does not match any of the allowed values. - - INVALID_OS (str): - Triggered when the operating system is not among those supported. - - INVALID_PYTHON_VERSION (str): - Raised when the current Python version is not one of the accepted versions. - - INVALID_PYTHON_INTERPRETER (str): - Raised when the Python interpreter is not among the supported ones. - - INVALID_ANNOTATION (str): - Raised when a variable, argument, or return annotation is unsupported. - - ELEMENT_MISSING (str): - Generic error for any expected element missing from the system or module context. - """ - - # General Warnings - ANALYSIS_RECURSION_WARNING = ( - "Warning: Analysis recursion detected. Avoid analyzing code that itself handles analysis, " - "to prevent stack overflow or performance issues." - ) - - # Module Validation Errors - FILENAME_MISMATCH = "Filename mismatch: expected '{0}', found '{1}'." - VERSION_MISMATCH = "Version mismatch: expected '{0}', found '{1}'." - ENV_VAR_MISSING = "Missing environment variable: '{0}'. Ensure it is defined in the system." - ENV_VAR_MISMATCH = "Environment variable value mismatch: expected '{0}', found '{1}'." - VAR_MISSING = "Missing variable: '{0}'. Ensure it is defined." - VAR_MISMATCH = "Variable value mismatch: expected '{0}', found '{1}'." - FUNCTIONS_MISSING = "Missing {0}: '{1}'. Ensure it is defined." - - # Function and Class Validation Errors - FUNCTION_RETURN_ANNOTATION_MISMATCH = ( - "Return annotation mismatch for {0} '{1}': expected '{2}', found '{3}'." - ) - VARIABLE_MISMATCH = "Variable mismatch'{1}': expected '{2}', found '{3}'." - VARIABLE_MISSING = "Missing variable '{0}'" - ARGUMENT_MISMATCH = "Argument mismatch for {0} '{1}': expected '{2}', found '{3}'." - ARGUMENT_MISSING = "Missing argument '{0}' in {1}." - - CLASS_MISSING = "Missing class: '{0}'. Ensure it is defined." - CLASS_ATTRIBUTE_MISSING = "Missing attribute '{0}' in class '{1}'." - CLASS_ATTRIBUTE_MISMATCH = ( - "Attribute value mismatch for '{0}' in class '{1}': expected '{2}', found '{3}'." - ) - CLASS_SUPERCLASS_MISSING = ( - "Missing superclass '{0}' in class '{1}'. Ensure that '{1}' extends '{0}'." - ) - INVALID_ATTRIBUTE_TYPE = "Invalid attribute type: '{0}'. Supported types are: {1}." - - # Runtime Validation Errors - INVALID_ARCHITECTURE = "Invalid architecture: expected '{0}', found '{1}'." - INVALID_OS = "Invalid Operating System: expected one of {0}, but found '{1}'." - - # Python Valitation Errors - INVALID_PYTHON_VERSION = "Invalid python version: expected one of '{0}', but found '{1}'." - INVALID_PYTHON_INTERPRETER = "Invalid python interpreter: expected one of '{0}', but found '{1}'." - - # Annotation Validation - INVALID_ANNOTATION = "Invalid annotation: expected one of {0}, but found '{1}'." - - # Generic Element Missing - ELEMENT_MISSING = ( - "{0} is declared but missing in the system. " - "Ensure it is properly defined and implemented." - ) \ No newline at end of file diff --git a/src/importspy/log_manager.py b/src/importspy/log_manager.py index c0ca8fb..754592a 100644 --- a/src/importspy/log_manager.py +++ b/src/importspy/log_manager.py @@ -22,19 +22,15 @@ class CustomFormatter(logging.Formatter): This formatter extends the default logging format by appending the exact filename, line number, and function name where each log was triggered. - This is especially useful in distributed architectures, plugin-based systems, - or debugging deeply nested calls during module inspection. - Format: ------- - ``[timestamp] [LEVEL] [logger name] [caller: file, line, function] message`` + [timestamp] [LEVEL] [logger name] + [caller: file, line, function] message Example: -------- - .. code-block:: text - - 2024-02-24 14:30:12 [INFO] [my_logger] - [caller: example.py, line: 42, function: my_function] This is a log message. + 2024-02-24 14:30:12 [INFO] [my_logger] + [caller: example.py, line: 42, function: my_function] This is a log message. """ LOG_FORMAT = ( @@ -51,22 +47,17 @@ def __init__(self): def format(self, record): """ - Adds caller details to the log record. - - Enhances logs with: - - Filename where the log was triggered - - Line number - - Function name + Enriches the log record with caller information. Parameters: ----------- record : logging.LogRecord - The original log event. + The original log record to be formatted. Returns: -------- str - The enriched, formatted log message. + A fully formatted log message including file, line, and function context. """ record.caller_file = record.pathname.split("/")[-1] record.caller_line = record.lineno @@ -78,37 +69,28 @@ class LogManager: """ Centralized manager for all logging within ImportSpy. - This class ensures that: - - All loggers use the same format (`CustomFormatter`) - - Logging is only configured once to avoid duplication - - Each component of the framework can retrieve its own scoped logger - - Whether ImportSpy runs embedded inside another module or as a CLI tool, - the `LogManager` ensures that log output is clean, traceable, and standardized. + This class ensures: + - Uniform formatting across all loggers + - Avoidance of duplicate configuration + - Consistent output in both CLI and embedded contexts Attributes: ----------- default_level : int - The system's current log level at the time of instantiation. + The current log level derived from the root logger. default_handler : logging.StreamHandler - Default output handler using `CustomFormatter`. + Default handler for logging output, using the `CustomFormatter`. configured : bool - Indicates whether global logging has already been configured. - - Methods: - -------- - - `configure(level, handlers)`: Applies global settings to the root logger. - - `get_logger(name)`: Retrieves a logger with consistent formatting and context. + Whether the logging system has already been configured. """ def __init__(self): """ - Sets up default logging options. + Sets up the default logging handler and format. - The default handler uses ImportSpy’s `CustomFormatter` and logs to `stdout`. - Logging is deferred until explicitly configured. + Uses `CustomFormatter` and logs to standard output by default. """ self.default_level = logging.getLogger().getEffectiveLevel() self.default_handler = logging.StreamHandler() @@ -117,30 +99,22 @@ def __init__(self): def configure(self, level: int = None, handlers: list = None): """ - Configures the global logging system. + Applies logging configuration globally. - This method attaches handlers to the root logger and sets the global level. - It must be called only once to avoid duplicate logs or handler conflicts. + Prevents duplicate setup. This method should be called once per application. Parameters: ----------- level : int, optional - Desired log level (e.g., `logging.DEBUG` or `logging.INFO`). - Defaults to the system’s current level. + Log level (e.g., logging.DEBUG). Defaults to current system level. handlers : list of logging.Handler, optional - List of custom handlers to attach. If omitted, uses the default stream handler. + Custom handlers to use. Falls back to `default_handler` if none provided. Raises: ------- RuntimeError - If logging has already been configured elsewhere in the application. - - Example: - -------- - .. code-block:: python - - LogManager().configure(level=logging.DEBUG) + If logging is already configured. """ if self.configured: raise RuntimeError("LogManager has already been configured.") @@ -158,27 +132,19 @@ def configure(self, level: int = None, handlers: list = None): def get_logger(self, name: str) -> logging.Logger: """ - Retrieves a scoped logger configured with ImportSpy’s formatting. + Returns a named logger with ImportSpy's formatting applied. - This logger is safe to use across modules and plugins. - It ensures no duplicate handlers and maintains the current log level. + Ensures the logger is properly configured and ready for use. Parameters: ----------- name : str - Name of the logger (typically `__name__` or class name). + The name of the logger (e.g., a module name). Returns: -------- logging.Logger - A configured logger ready for use. - - Example: - -------- - .. code-block:: python - - logger = LogManager().get_logger("my_module") - logger.info("Validation complete.") + The initialized logger instance. """ logger = logging.getLogger(name) if not logger.handlers: diff --git a/src/importspy/models.py b/src/importspy/models.py index e6c3651..15b0b73 100644 --- a/src/importspy/models.py +++ b/src/importspy/models.py @@ -1,15 +1,18 @@ """ -importspy.models ----------------- +models.py +========== -This module defines the core data models used by ImportSpy for contract-based -runtime validation of Python modules. It includes structural representations -of variables, functions, classes, and full modules, as well as runtime and -system-level metadata required to enforce import contracts across execution contexts. +Defines the structural and contextual data models used across ImportSpy. +These models represent modules, variables, functions, classes, runtimes, +systems, and environments involved in contract-based validation. + +This module powers both embedded validation and CLI checks, enabling ImportSpy +to introspect, serialize, and enforce compatibility rules at multiple levels: +from source code structure to runtime platform details. """ -from pydantic import BaseModel, field_validator, Field -from typing import Optional, List, Union +from pydantic import BaseModel +from typing import Optional, Union, List from types import ModuleType from .utilities.module_util import ( @@ -19,8 +22,8 @@ from .utilities.runtime_util import RuntimeUtil from .utilities.system_util import SystemUtil from .utilities.python_util import PythonUtil -from .constants import Constants -from .errors import Errors +from .constants import Constants, Contexts, Errors +from .config import Config import logging logger = logging.getLogger("/".join(__file__.split('/')[-2:])) @@ -29,128 +32,118 @@ class Python(BaseModel): """ - Represents a specific Python runtime configuration. + Represents a Python runtime environment. - Includes the Python version, interpreter type, and the list of loaded modules. - Used to validate compatibility between caller and callee environments. + Includes: + - Python version + - Interpreter type (e.g., CPython, PyPy) + - List of loaded modules + Used in validating runtime compatibility. """ version: Optional[str] = None - interpreter: Optional[str] = None - modules: List['Module'] + interpreter: Optional[Constants.SupportedPythonImplementations] = None + modules: list['Module'] - @field_validator('version') - def validate_version(cls, value: str): - """ - Validate that the Python version is within supported versions. - """ - if ".".join(value.split(".")[:2]) not in Constants.SUPPORTED_PYTHON_VERSION: - raise ValueError(Errors.INVALID_PYTHON_VERSION.format(Constants.SUPPORTED_PYTHON_VERSION, value)) - return value + def __str__(self): + return f"{self.interpreter} v{self.version}" + + def __repr__(self): + return str(self) - @field_validator('interpreter') - def validate_interpreter(cls, value: str): - """ - Validate that the interpreter is among the supported Python implementations. - """ - if value not in Constants.SUPPORTED_PYTHON_IMPLEMENTATION: - raise ValueError(Errors.INVALID_PYTHON_INTERPRETER.format(Constants.SUPPORTED_PYTHON_IMPLEMENTATION, value)) - return value + +class Environment(BaseModel): + """ + Represents runtime environment variables and secrets. + Used for validating runtime configuration. + """ + variables: Optional[list['Variable']] = None + secrets: Optional[list[str]] = None + + def __str__(self): + return f"variables: {self.variables} | secrets: {self.secrets}" + + def __repr__(self): + return str(self) class System(BaseModel): """ - Represents the system environment, including OS, environment variables, - and Python runtimes configured within the system. + Represents a full OS environment within a deployment system. + + Includes: + - OS type + - Environment variables + - Python runtimes + Used to validate cross-platform compatibility. """ - os: str - envs: Optional[dict] = Field(default=None, repr=False) - pythons: List[Python] + os: Constants.SupportedOS + environment: Optional[Environment] = None + pythons: list[Python] - @field_validator('os') - def validate_os(cls, value: str): - """ - Validate that the provided OS is among the supported platforms. - """ - if value not in Constants.SUPPORTED_OS: - raise ValueError(Errors.INVALID_OS.format(Constants.SUPPORTED_OS, value)) - return value + def __str__(self): + return f"{self.os.value}" + + def __repr__(self): + return str(self) class Runtime(BaseModel): """ - Represents the deployment runtime, identified by CPU architecture and - the list of supported systems associated with that architecture. + Represents a runtime deployment context. + + Defined by CPU architecture and associated systems. """ - arch: str - systems: List[System] + arch: Constants.SupportedArchitectures + systems: list[System] - @field_validator('arch') - def validate_arch(cls, value: str): - """ - Validate that the CPU architecture is known and supported. - """ - if value not in Constants.KNOWN_ARCHITECTURES: - raise ValueError(Errors.INVALID_ARCHITECTURE.format(value, Constants.KNOWN_ARCHITECTURES)) - return value + def __str__(self): + return f"{self.arch}" + + def __repr__(self): + return str(self) class Variable(BaseModel): """ - Represents a declared variable within a Python module, including optional type - annotation and value. Used for structural validation of the importing module. + Represents a top-level variable in a Python module. + + Includes: + - Name + - Optional annotation + - Optional static value + Used to enforce structural consistency. """ name: str - annotation: Optional[str] = None + annotation: Optional[Constants.SupportedAnnotations] = None value: Optional[Union[int, str, float, bool, None]] = None - @field_validator("annotation") - def validate_annotation(cls, value): - """ - Validate that the annotation is supported by the current contract. - """ - if not value: - return None - base = value.split("[")[0] - if base not in Constants.SUPPORTED_ANNOTATIONS: - raise ValueError( - Errors.INVALID_ANNOTATION.format(value, Constants.SUPPORTED_ANNOTATIONS) - ) - return value - @classmethod - def from_variable_info(cls, variables_info: List[VariableInfo]): - """ - Convert a list of extracted VariableInfo into Variable instances. - """ - return [Variable( + def from_variable_info(cls, variables_info: list[VariableInfo]): + return [cls( name=var_info.name, value=var_info.value, annotation=var_info.annotation ) for var_info in variables_info] + def __str__(self): + type_part = f": {self.annotation}" if self.annotation else "" + return f"{self.name}{type_part} = {self.value}" + + def __repr__(self): + return str(self) + class Attribute(Variable): """ - Represents a class attribute, extending Variable with a 'type' indicator - (e.g., 'class', 'instance'). Used in class-level contract validation. - """ - type: str + Represents a class-level attribute. - @field_validator('type') - def validate_type(cls, value: str): - """ - Validate that the attribute type is among supported class attribute types. - """ - if value not in Constants.SUPPORTED_CLASS_ATTRIBUTE_TYPES: - raise ValueError(Errors.INVALID_ATTRIBUTE_TYPE.format(value, Constants.SUPPORTED_CLASS_ATTRIBUTE_TYPES)) - return value + Extends Variable with attribute type (e.g., class or instance). + """ + type: Constants.SupportedClassAttributeTypes @classmethod - def from_attributes_info(cls, attributes_info: List[AttributeInfo]): - """ - Convert a list of AttributeInfo objects into Attribute instances. - """ - return [Attribute( + def from_attributes_info(cls, attributes_info: list[AttributeInfo]): + return [cls( type=attr_info.type, name=attr_info.name, value=attr_info.value, @@ -160,15 +153,17 @@ def from_attributes_info(cls, attributes_info: List[AttributeInfo]): class Argument(Variable, BaseModel): """ - Represents a function argument, including its name, type annotation, and default value. - Used to validate callable structures and type consistency. + Represents a function/method argument. + + Includes: + - Name + - Optional type annotation + - Optional default value + Used to check call signatures. """ @classmethod - def from_arguments_info(cls, arguments_info: List[ArgumentInfo]): - """ - Convert a list of ArgumentInfo into Argument instances. - """ - return [Argument( + def from_arguments_info(cls, arguments_info: list[ArgumentInfo]): + return [cls( name=arg_info.name, annotation=arg_info.annotation, value=arg_info.value @@ -177,81 +172,102 @@ def from_arguments_info(cls, arguments_info: List[ArgumentInfo]): class Function(BaseModel): """ - Represents a callable function, including its name, argument signature, - and return type annotation. + Represents a callable entity. + + Includes: + - Name + - List of arguments + - Optional return annotation """ name: str - arguments: Optional[List[Argument]] = None - return_annotation: Optional[str] = None - - @field_validator("return_annotation") - def validate_annotation(cls, value): - """ - Validate that the return annotation is supported. - """ - return CommonValidator.validate_annotation(value) + arguments: Optional[list[Argument]] = None + return_annotation: Optional[Constants.SupportedAnnotations] = None @classmethod - def from_functions_info(cls, functions_info: List[FunctionInfo]): - """ - Convert a list of FunctionInfo into Function instances. - """ - return [Function( + def from_functions_info(cls, functions_info: list[FunctionInfo]): + return [cls( name=func_info.name, arguments=Argument.from_arguments_info(func_info.arguments), return_annotation=func_info.return_annotation ) for func_info in functions_info] + def __str__(self): + args = ", ".join(str(arg) for arg in self.arguments) if self.arguments else "" + return f"{self.name}({args}) -> {self.return_annotation}" + + def __repr__(self): + return str(self) + class Class(BaseModel): """ - Represents a Python class, including its attributes, methods, and declared superclasses. - Used to enforce object-level validation rules in contracts. + Represents a Python class declaration. + + Includes: + - Name + - Attributes (class/instance) + - Methods + - Superclasses (recursive) """ name: str - attributes: Optional[List[Attribute]] = None - methods: Optional[List[Function]] = None - superclasses: Optional[List[str]] = None + attributes: Optional[list[Attribute]] = None + methods: Optional[list[Function]] = None + superclasses: Optional[list['Class']] = None @classmethod - def from_class_info(cls, extracted_classes: List[ClassInfo]): - """ - Convert a list of extracted class definitions into Class instances. - """ - return [Class( + def from_class_info(cls, extracted_classes: list[ClassInfo]): + return [cls( name=name, attributes=Attribute.from_attributes_info(attributes), methods=Function.from_functions_info(methods), - superclasses=superclasses + superclasses=cls.from_class_info(superclasses) ) for name, attributes, methods, superclasses in extracted_classes] + def get_class_attributes(self) -> List[Attribute]: + if self.attributes: + return [attr for attr in self.attributes if attr.type == Config.CLASS_TYPE] + + def get_instance_attributes(self) -> List[Attribute]: + if self.attributes: + return [attr for attr in self.attributes if attr.type == Config.INSTANCE_TYPE] + class Module(BaseModel): """ - Represents a full Python module, including its filename, version, - and all its internal components (variables, functions, classes). + Represents a Python module. + + Includes: + - Filename + - Version (if extractable) + - Top-level variables, functions, and classes """ filename: Optional[str] = None version: Optional[str] = None - variables: Optional[List[Variable]] = None - functions: Optional[List[Function]] = None - classes: Optional[List[Class]] = None + variables: Optional[list[Variable]] = None + functions: Optional[list[Function]] = None + classes: Optional[list[Class]] = None + + def __str__(self): + return f"Module: {self.filename or 'unknown'} (v{self.version or '-'})" + + def __repr__(self): + return str(self) class SpyModel(Module): """ - Extends the base Module structure with additional deployment metadata. + High-level model used by ImportSpy for validation. - SpyModel is the top-level object representing a module's structure and its - runtime/environment constraints. This is the core of ImportSpy's contract model. + Extends the module representation with runtime metadata and + platform-specific deployment constraints (architecture, OS, interpreter, etc). """ - deployments: Optional[List[Runtime]] = None + deployments: Optional[list[Runtime]] = None @classmethod def from_module(cls, info_module: ModuleType): """ - Create a SpyModel from a loaded Python module, extracting its metadata - and attaching runtime/system context. + Build a SpyModel instance by extracting structure and metadata + from an actual Python module object. """ module_utils = ModuleUtil() runtime_utils = RuntimeUtil() @@ -271,7 +287,7 @@ def from_module(cls, info_module: ModuleType): os = system_utils.extract_os() python_version = python_utils.extract_python_version() interpreter = python_utils.extract_python_implementation() - envs = system_utils.extract_envs() + envs = Variable.from_variable_info(system_utils.extract_envs()) module_utils.unload_module(info_module) logger.debug("Unload module") @@ -284,7 +300,9 @@ def from_module(cls, info_module: ModuleType): systems=[ System( os=os, - envs=envs, + environment=Environment( + variables=envs + ), pythons=[ Python( version=python_version, @@ -307,21 +325,15 @@ def from_module(cls, info_module: ModuleType): ) -class CommonValidator: - """ - Provides shared validation utilities for type annotations and other structural elements. +class Error(BaseModel): """ + Describes a structured validation error. - @classmethod - def validate_annotation(cls, value): - """ - Validate a type annotation against the supported base annotations. - """ - if not value: - return None - base = value.split("[")[0] - if base not in Constants.SUPPORTED_ANNOTATIONS: - raise ValueError( - Errors.INVALID_ANNOTATION.format(value, Constants.SUPPORTED_ANNOTATIONS) - ) - return value + Includes the context, error type, message, and resolution steps. + Used to serialize feedback during contract enforcement. + """ + context: Contexts + title: str + category: Errors.Category + description: str + solution: str diff --git a/src/importspy/persistences.py b/src/importspy/persistences.py index ebd9a43..099f85d 100644 --- a/src/importspy/persistences.py +++ b/src/importspy/persistences.py @@ -1,16 +1,12 @@ """ -importspy.persistences -======================= - -This module defines the interfaces and implementations for handling **import contracts** — +Defines interfaces and implementations for handling **import contracts** — external YAML files used by ImportSpy to validate the structure and runtime expectations of dynamically loaded Python modules. -Currently, YAML is the only supported contract format, but the architecture is fully -extensible via the `Parser` interface. +Currently, only YAML is supported, but the architecture is extensible via the `Parser` interface. -All file access operations are wrapped in safe error handling using `handle_persistence_error`, -which raises human-readable exceptions when contract files are missing, corrupted, or unreadable. +All file I/O operations are wrapped in `handle_persistence_error`, ensuring clear error +messages in case of missing, malformed, or inaccessible contract files. """ from abc import ABC, abstractmethod @@ -20,86 +16,84 @@ class Parser(ABC): """ - Abstract interface for import contract parsers. + Abstract base class for import contract parsers. - A contract parser is responsible for loading and saving `.yml` files - that describe the expected structure of a Python module. This abstraction - allows ImportSpy to support multiple formats (e.g., YAML, JSON, TOML) in the future. + Parsers are responsible for loading and saving `.yml` contract files that define + a module’s structural and runtime expectations. This abstraction enables future + support for additional formats (e.g., JSON, TOML). - Subclasses must implement both `save()` and `load()` methods. + Subclasses must implement `save()` and `load()`. """ @abstractmethod def save(self, data: dict, filepath: str): """ - Serializes the given import contract (as a Python dictionary) and writes it to a file. + Serializes the contract (as a dictionary) and writes it to disk. Parameters: ----------- data : dict - A dictionary representation of the import contract. + Dictionary containing the contract structure. filepath : str - The path where the contract should be saved (usually with `.yml` extension). + Target path for saving the contract (typically `.yml`). """ pass @abstractmethod def load(self, filepath: str) -> dict: """ - Loads and parses an import contract from a file into a Python dictionary. + Parses a contract file and returns it as a dictionary. Parameters: ----------- filepath : str - Path to the `.yml` contract file. + Path to the contract file on disk. Returns: -------- dict - The parsed contract as a dictionary. + Parsed contract data. """ pass class PersistenceError(Exception): """ - Custom exception raised when there is a problem reading or writing import contracts. + Raised when contract loading or saving fails due to I/O or syntax issues. - This error wraps low-level I/O or parsing issues and presents them in a way - that is meaningful to end users. + This exception wraps low-level errors and provides human-readable feedback. """ def __init__(self, msg: str): """ - Initializes the `PersistenceError`. + Initialize the error with a descriptive message. Parameters: ----------- msg : str - A human-readable error message describing the failure. + Explanation of the failure. """ super().__init__(msg) def handle_persistence_error(func): """ - Decorator that wraps file I/O operations in safe error handling. - - If the decorated function raises any exception (e.g., file not found, malformed YAML), - a `PersistenceError` is raised with a descriptive message instead. + Decorator for wrapping parser I/O methods with user-friendly error handling. - This helps ensure that ImportSpy fails gracefully during contract handling. + Catches all exceptions and raises a `PersistenceError` with a generic message. + This ensures ImportSpy fails gracefully if a contract file is missing, + malformed, or inaccessible. Parameters: ----------- func : Callable - The function to decorate. + The I/O method to wrap. Returns: -------- Callable - The wrapped function. + A wrapped version that raises `PersistenceError` on failure. """ @functools.wraps(func) def wrapper(*args, **kwargs): @@ -115,26 +109,26 @@ def wrapper(*args, **kwargs): class YamlParser(Parser): """ - YAML-based implementation of the `Parser` interface. + YAML-based contract parser implementation. - This parser reads and writes import contracts from `.yml` files using the `ruamel.yaml` library. - It preserves indentation, flow style, and quotes to ensure consistent structure across validations. + Uses `ruamel.yaml` to read and write `.yml` files that define import contracts. + Preserves formatting, indentation, and quotes for consistent serialization. """ def __init__(self): """ - Initializes the YAML parser and applies default formatting rules for readability. + Initializes the YAML parser and configures output formatting. """ self.yaml = YAML() self._yml_configuration() def _yml_configuration(self): """ - Applies consistent formatting to YAML output: + Applies formatting rules to YAML output: - - Disables flow style for better readability - - Sets indentation rules for mappings and sequences - - Preserves quotes for exact string representation + - Disables flow style + - Sets consistent indentation + - Preserves quotes in strings """ self.yaml.default_flow_style = False self.yaml.indent(mapping=2, sequence=4, offset=2) @@ -143,15 +137,15 @@ def _yml_configuration(self): @handle_persistence_error def save(self, data: dict, filepath: str): """ - Saves an import contract to a `.yml` file. + Saves a contract dictionary to a `.yml` file. Parameters: ----------- data : dict - The contract content as a dictionary. + Contract structure. filepath : str - The output path where the YAML file will be saved. + Destination file path. """ with open(filepath, "w") as file: self.yaml.dump(data, file) @@ -159,17 +153,17 @@ def save(self, data: dict, filepath: str): @handle_persistence_error def load(self, filepath: str) -> dict: """ - Loads an import contract from a `.yml` file and parses it into a dictionary. + Loads and parses a `.yml` contract into a Python dictionary. Parameters: ----------- filepath : str - Path to the YAML file. + Path to the contract file. Returns: -------- dict - A Python dictionary containing the contract structure. + Parsed contract structure. """ with open(filepath) as file: data = self.yaml.load(file) diff --git a/src/importspy/s.py b/src/importspy/s.py index b421685..8d38433 100644 --- a/src/importspy/s.py +++ b/src/importspy/s.py @@ -1,7 +1,4 @@ """ -importspy.s -=========== - Core validation logic for ImportSpy. This module defines the `Spy` class, the central component responsible for dynamically @@ -20,57 +17,64 @@ """ from types import ModuleType -from .models import SpyModel +from .models import ( + SpyModel, + Runtime, + Python, + Module +) from .utilities.module_util import ModuleUtil -from .validators.spymodel_validator import SpyModelValidator +from .validators import ( + RuntimeValidator, + SystemValidator, + PythonValidator, + ModuleValidator +) from .log_manager import LogManager from .persistences import Parser, YamlParser -from typing import Optional +from typing import ( + Optional, + List +) import logging +from .violation_systems import ( + Bundle, + ModuleContractViolation, + RuntimeContractViolation, + SystemContractViolation, + PythonContractViolation +) +from .constants import Contexts class Spy: """ - The `Spy` class is the core engine of ImportSpy — it handles validation, introspection, - and enforcement of structural contracts for Python modules. - - This class is designed to support both: + Core validation engine for ImportSpy. - - **Embedded validation**, where it is imported and executed inside the module under control. - - **CLI-based or pipeline validation**, where an external tool invokes Spy programmatically. + The `Spy` class is responsible for loading a target module, extracting its structure, + and validating it against a YAML-based import contract. This ensures that the importing + module satisfies all declared structural and runtime constraints. - ImportSpy uses declarative **import contracts**, written as human-readable YAML files, - to describe what a valid module should contain. These contracts define expected classes, - attributes, methods, and even environmental constraints (like Python version or OS). - - The `Spy` class dynamically loads the target module, extracts its metadata, and checks - for compliance against the contract. If validation fails, descriptive errors are raised - before the module can be used improperly. + It supports two modes: + - **Embedded mode**: validates the caller of the current module + - **External/CLI mode**: validates an explicitly provided module Attributes: ----------- logger : logging.Logger - Logger instance used to track validation steps and internal processing. + Structured logger for validation diagnostics. parser : Parser - Parser responsible for loading the import contract from disk (currently supports YAML). - - Methods: - -------- - - `__init__()` → Initializes logger and default parser. - - `importspy(filepath, log_level, info_module)` → Validates a specified or inferred module. - - `_configure_logging(log_level)` → Sets logging level based on user/system config. - - `_validate_module(contract, info_module)` → Compares a module to the contract definition. - - `_inspect_module()` → Introspects the call stack to locate the calling module. + Parser used to load import contracts (defaults to YAML). """ def __init__(self): """ - Initializes the `Spy` instance. + Initialize the Spy instance. - This method sets up: - - the logging system for capturing all validation and introspection steps - - the default parser (`YamlParser`) for loading `.yml` import contracts + Sets up: + - a dedicated logger + - the default YAML parser """ self.logger = LogManager().get_logger(self.__class__.__name__) self.parser: Parser = YamlParser() @@ -80,35 +84,34 @@ def importspy(self, log_level: Optional[int] = None, info_module: Optional[ModuleType] = None) -> ModuleType: """ - Loads and validates a Python module based on an import contract. + Main entry point for validation. - This is the primary method used to validate a module, whether in embedded mode - (by inspecting the importer), or in external mode (via CLI or script). + Loads and validates a Python module against the contract defined in the given YAML file. + If no module is explicitly provided, introspects the call stack to infer the caller. Parameters: ----------- filepath : Optional[str] - Path to the `.yml` contract file defining the expected structure. + Path to the `.yml` import contract. log_level : Optional[int] - Logging level for output verbosity. Uses system default if not provided. + Log verbosity level (e.g., `logging.DEBUG`). info_module : Optional[ModuleType] - Optional reference to the module to validate. If not provided, - the calling module is inferred via stack inspection. + The module to validate. If `None`, uses the importer via stack inspection. Returns: -------- ModuleType - The module that was validated. + The validated module. Raises: ------- RuntimeError - If logging is misconfigured or reconfigured unexpectedly. + If logging setup fails. ValueError - If a recursion pattern is detected (e.g., a module validating itself). + If recursion is detected (e.g., a module is validating itself). """ self._configure_logging(log_level) spymodel: SpyModel = SpyModel(**self.parser.load(filepath=filepath)) @@ -118,15 +121,15 @@ def importspy(self, def _configure_logging(self, log_level: Optional[int] = None): """ - Configures ImportSpy's logging system for runtime use. + Set up logging for validation. - If a log level is provided, it overrides the system's default. This method ensures - the logger is only configured once, preventing duplicate log handlers. + If not already configured, applies the provided or default log level + using ImportSpy’s centralized logging system. Parameters: ----------- log_level : Optional[int] - The desired log level (e.g., logging.INFO, logging.DEBUG). + Logging level to use (e.g., `logging.INFO`, `logging.DEBUG`). """ log_manager = LogManager() if not log_manager.configured: @@ -135,62 +138,69 @@ def _configure_logging(self, log_level: Optional[int] = None): def _validate_module(self, spymodel: SpyModel, info_module: ModuleType) -> ModuleType: """ - Compares a module's structure against the loaded import contract. + Perform all validation steps against the loaded module. - This includes checking for: - - required classes and methods - - expected variable names and values - - inheritance and method signatures + This includes contract-level, runtime, system, and Python environment checks. + All contract violations are collected in a `Bundle`. Parameters: ----------- spymodel : SpyModel - Parsed import contract used as the validation baseline. + The expected contract loaded from file. info_module : ModuleType - The actual module being validated. + The actual module to inspect and validate. Returns: -------- ModuleType - The validated module. - - Raises: - ------- - ValueError - If the module does not conform to the expected structure. + The validated module, reloaded after introspection. """ self.logger.debug(f"info_module: {info_module}") if spymodel: + bundle = Bundle() + module_validator = ModuleValidator() self.logger.debug(f"Import contract detected: {spymodel}") spy_module = SpyModel.from_module(info_module) self.logger.debug(f"Extracted module structure: {spy_module}") - SpyModelValidator().validate(spymodel, spy_module) + + module_contract = ModuleContractViolation(Contexts.MODULE_CONTEXT, bundle) + module_validator.validate([spymodel], spy_module.deployments[0].systems[0].pythons[0].modules[0], module_contract) + + runtime_contract = RuntimeContractViolation(Contexts.RUNTIME_CONTEXT, bundle) + runtime = RuntimeValidator().validate(spymodel.deployments, spy_module.deployments, runtime_contract) + + system_contract = SystemContractViolation(Contexts.RUNTIME_CONTEXT, bundle) + pythons = SystemValidator().validate(runtime.systems, spy_module.deployments[0].systems, system_contract) + + python_contract = PythonContractViolation(Contexts.RUNTIME_CONTEXT, bundle) + modules = PythonValidator().validate(pythons, spy_module.deployments[0].systems[0].pythons, python_contract) + + module_validator.validate(modules, spy_module.deployments[0].systems[0].pythons[0].modules[0], module_contract) + return ModuleUtil().load_module(info_module) def _inspect_module(self) -> ModuleType: """ - Introspects the call stack to determine which module called `importspy()`. + Infer the module that invoked validation (embedded mode). - This is used primarily in embedded mode to locate the external plugin - or module that triggered the validation. It prevents the system from - analyzing itself (recursive inspection). + This prevents a module from validating itself and ensures that + ImportSpy targets the correct caller in the stack. Returns: -------- ModuleType - The module that imported or triggered validation. + The inferred external module. Raises: ------- ValueError - If recursion is detected (i.e., the same module is inspecting itself). + If a module attempts to validate itself. """ module_util = ModuleUtil() current_frame, caller_frame = module_util.inspect_module() if current_frame.filename == caller_frame.filename: raise ValueError("Recursion detected during module analysis.") - info_module = module_util.get_info_module(caller_frame) self.logger.debug(f"Inferred caller module: {info_module}") return info_module diff --git a/src/importspy/utilities/__init__.py b/src/importspy/utilities/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/importspy/utilities/module_util.py b/src/importspy/utilities/module_util.py index c81b419..ea52a7c 100644 --- a/src/importspy/utilities/module_util.py +++ b/src/importspy/utilities/module_util.py @@ -1,25 +1,26 @@ """ -Module: Module Utilities +Module utilities for runtime introspection and structure extraction. -This module provides a comprehensive set of utility functions for dynamic module inspection, -loading, unloading, and metadata extraction. It is designed to support ImportSpy's runtime -validation processes by enabling detailed analysis of Python modules. +This module provides utility functions for analyzing Python modules dynamically, +primarily to support ImportSpy's runtime validation mechanisms. It enables inspection +of modules, their metadata, and internal structure at runtime. -Key Features: -------------- -- Inspect the calling stack and retrieve information about modules. -- Dynamically load and unload modules for runtime modifications. -- Extract metadata such as classes, functions, variables, and inheritance hierarchies from modules. - -Example Usage: --------------- -.. code-block:: python +Features: +- Inspect the call stack and determine caller modules. +- Dynamically load and unload Python modules. +- Extract version information via metadata or attributes. +- Retrieve global variables, top-level functions, and class definitions. +- Analyze methods, attributes (class-level and instance-level), and superclasses. +Example: + ```python from importspy.utilities.module_util import ModuleUtil + import inspect module_util = ModuleUtil() - module_info = module_util.get_info_module(inspect.stack()[0]) - print(f"Module Name: {module_info.__name__}") + info = module_util.get_info_module(inspect.stack()[0]) + print(info.__name__) + ``` """ import inspect @@ -43,23 +44,11 @@ class ModuleUtil: """ - Utility class for dynamic module inspection and metadata extraction. - - The `ModuleUtil` class provides methods to inspect, load, unload, and analyze Python - modules at runtime. These utilities are essential for enabling ImportSpy's dynamic - validation of runtime conditions. - - Methods: - -------- - - `inspect_module()`: Retrieve current and caller frames. - - `get_info_module()`: Extract module from a caller frame. - - `load_module()`: Dynamically reload a module. - - `unload_module()`: Remove module from memory. - - `extract_version()`: Retrieve version of a module. - - `extract_variables()`: Extract global variables. - - `extract_functions()`: Extract top-level functions. - - `extract_classes()`: Extract class definitions. - - `extract_superclasses()`: Collect all used base classes. + Provides methods to inspect and extract structural metadata from Python modules. + + This class enables runtime inspection of loaded modules for metadata such as + functions, classes, variables, inheritance hierarchies, and version information. + It is a core component used by ImportSpy to validate structural contracts. """ def inspect_module(self) -> tuple: @@ -67,9 +56,7 @@ def inspect_module(self) -> tuple: Retrieve the current and caller frames from the stack. Returns: - -------- - tuple - A tuple containing the current and caller frame. + tuple: A tuple with the current and the outermost caller frame. """ stack = inspect.stack() current_frame = stack[1] @@ -78,33 +65,25 @@ def inspect_module(self) -> tuple: def get_info_module(self, caller_frame: inspect.FrameInfo) -> ModuleType | None: """ - Retrieves the module object from a caller frame. + Resolve a module object from a given caller frame. - Parameters: - ----------- - caller_frame : inspect.FrameInfo - The frame to analyze. + Args: + caller_frame (inspect.FrameInfo): The caller frame to analyze. Returns: - -------- - ModuleType | None - The resolved module or None if not found. + ModuleType | None: The resolved module or None if not found. """ return inspect.getmodule(caller_frame.frame) def load_module(self, info_module: ModuleType) -> ModuleType | None: """ - Dynamically reload a module. + Reload a module dynamically from its file location. - Parameters: - ----------- - info_module : ModuleType - The module reference. + Args: + info_module (ModuleType): The module to reload. Returns: - -------- - ModuleType | None - The reloaded module. + ModuleType | None: The reloaded module or None if loading fails. """ spec = importlib.util.spec_from_file_location(info_module.__name__, info_module.__file__) if spec and spec.loader: @@ -116,12 +95,10 @@ def load_module(self, info_module: ModuleType) -> ModuleType | None: def unload_module(self, module: ModuleType): """ - Removes a module from memory. + Unload a module from sys.modules and globals. - Parameters: - ----------- - module : ModuleType - The module to unload. + Args: + module (ModuleType): The module to unload. """ module_name = module.__name__ if module_name in sys.modules: @@ -130,16 +107,13 @@ def unload_module(self, module: ModuleType): def extract_version(self, info_module: ModuleType) -> str | None: """ - Retrieves version metadata for the module. + Attempt to retrieve the version string from a module. - Parameters: - ----------- - info_module : ModuleType + Args: + info_module (ModuleType): The target module. Returns: - -------- - str | None - The version string if found. + str | None: Version string if found, otherwise None. """ if hasattr(info_module, '__version__'): return info_module.__version__ @@ -150,7 +124,13 @@ def extract_version(self, info_module: ModuleType) -> str | None: def extract_annotation(self, annotation) -> Optional[str]: """ - Converts annotations to string format for validation. + Convert a type annotation object into a string representation. + + Args: + annotation: The annotation object to convert. + + Returns: + Optional[str]: The extracted annotation string or None. """ if annotation == inspect._empty or not annotation: return None @@ -158,8 +138,17 @@ def extract_annotation(self, annotation) -> Optional[str]: return annotation.__name__ return str(annotation) - def extract_variables(self, info_module: ModuleType) -> dict: - variables_info:List[VariableInfo] = [] + def extract_variables(self, info_module: ModuleType) -> List[VariableInfo]: + """ + Extract top-level variable definitions from a module. + + Args: + info_module (ModuleType): The module to analyze. + + Returns: + List[VariableInfo]: List of variable metadata. + """ + variables_info: List[VariableInfo] = [] for name, value in inspect.getmembers(info_module): if not name.startswith('__') and not inspect.ismodule(value) and not inspect.isfunction(value) and not inspect.isclass(value): annotation = self.extract_annotation(type(value)) @@ -168,11 +157,13 @@ def extract_variables(self, info_module: ModuleType) -> dict: def extract_functions(self, info_module: ModuleType) -> List[FunctionInfo]: """ - Extracts function definitions from the module. + Extract all functions defined at the top level of the module. + + Args: + info_module (ModuleType): The target module. Returns: - -------- - List[FunctionInfo] + List[FunctionInfo]: Function metadata extracted from the module. """ functions_info: List[FunctionInfo] = [] for name, obj in inspect.getmembers(info_module, inspect.isfunction): @@ -182,7 +173,14 @@ def extract_functions(self, info_module: ModuleType) -> List[FunctionInfo]: def _extract_function(self, name: str, obj: FunctionType) -> FunctionInfo: """ - Builds metadata for a function. + Build structured metadata for a function. + + Args: + name (str): Function name. + obj (FunctionType): Function object. + + Returns: + FunctionInfo: Extracted function metadata. """ return FunctionInfo( name, @@ -192,7 +190,13 @@ def _extract_function(self, name: str, obj: FunctionType) -> FunctionInfo: def _extract_arguments(self, obj: FunctionType) -> List[ArgumentInfo]: """ - Retrieves argument names and annotations. + Extract arguments from a function's signature. + + Args: + obj (FunctionType): Function object. + + Returns: + List[ArgumentInfo]: List of function argument metadata. """ args = [] for name, param in inspect.signature(obj).parameters.items(): @@ -202,7 +206,13 @@ def _extract_arguments(self, obj: FunctionType) -> List[ArgumentInfo]: def extract_methods(self, cls_obj) -> List[FunctionInfo]: """ - Extracts all method definitions from a class. + Extract method definitions from a class object. + + Args: + cls_obj: The class to inspect. + + Returns: + List[FunctionInfo]: Extracted method metadata. """ methods: List[FunctionInfo] = [] for name, obj in inspect.getmembers(cls_obj, inspect.isfunction): @@ -212,7 +222,14 @@ def extract_methods(self, cls_obj) -> List[FunctionInfo]: def extract_attributes(self, cls_obj, info_module: ModuleType) -> List[AttributeInfo]: """ - Extracts class-level and instance-level attributes. + Extract both class-level and instance-level attributes. + + Args: + cls_obj: The class to analyze. + info_module (ModuleType): The module containing the class. + + Returns: + List[AttributeInfo]: List of extracted attributes. """ attributes: List[AttributeInfo] = [] annotations = getattr(cls_obj, '__annotations__', {}) @@ -243,31 +260,43 @@ def extract_attributes(self, cls_obj, info_module: ModuleType) -> List[Attribute def extract_classes(self, info_module: ModuleType) -> List[ClassInfo]: """ - Extracts class definitions from the module. + Extract all class definitions from a module. + + Args: + info_module (ModuleType): The module to inspect. Returns: - -------- - List[ClassInfo] + List[ClassInfo]: Metadata about the module’s classes. """ classes = [] for name, cls in inspect.getmembers(info_module, inspect.isclass): attributes = self.extract_attributes(cls, info_module) methods = self.extract_methods(cls) - superclasses = [base.__name__ for base in cls.__bases__ if base.__name__ != "object"] + superclasses = self.extract_superclasses(cls) classes.append(ClassInfo(name, attributes, methods, superclasses)) return classes - def extract_superclasses(self, module: ModuleType) -> List[str]: + def extract_superclasses(self, cls) -> List[ClassInfo]: """ - Extracts unique superclass names from all classes. + Extract base classes for a given class, recursively. + + Args: + cls: The class whose base classes are being extracted. Returns: - -------- - List[str] + List[ClassInfo]: Metadata for each superclass. """ - superclasses = set() - for name, cls in inspect.getmembers(module, inspect.isclass): - if cls.__module__ == module.__name__: - for base in cls.__bases__: - superclasses.add(base.__name__) - return list(superclasses) + superclasses = [] + for base in cls.__bases__: + if base.__name__ == "object": + continue + module = sys.modules.get(base.__module__) + if not module: + continue + superclasses.append(ClassInfo( + base.__name__, + self.extract_attributes(base, module), + self.extract_methods(base), + [] + )) + return superclasses diff --git a/src/importspy/utilities/python_util.py b/src/importspy/utilities/python_util.py index eca7867..0e37d53 100644 --- a/src/importspy/utilities/python_util.py +++ b/src/importspy/utilities/python_util.py @@ -1,22 +1,20 @@ -""" -Python Runtime Utilities -======================== +"""Python Runtime Utilities -Provides helpers to inspect the active Python environment, -such as the interpreter implementation and version. +Provides utility methods to inspect the active Python runtime environment, +such as the version number and interpreter implementation. -Useful in ImportSpy to validate compatibility constraints across Python versions -and runtime variants (e.g., CPython, PyPy, IronPython). +These utilities are useful within ImportSpy to evaluate whether the current +runtime context satisfies declared compatibility constraints in import contracts. +This includes checks for specific Python versions and interpreter families +(CPython, PyPy, IronPython, etc.). Example: --------- -.. code-block:: python - - from importspy.utilities.python_util import PythonUtil - - util = PythonUtil() - print(util.extract_python_version()) - print(util.extract_python_implementation()) + >>> from importspy.utilities.python_util import PythonUtil + >>> util = PythonUtil() + >>> util.extract_python_version() + '3.12.0' + >>> util.extract_python_implementation() + 'CPython' """ import logging @@ -25,47 +23,42 @@ logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) -class PythonUtil: - """ - Utility class for querying Python runtime details. - Methods - ------- - extract_python_version() -> str - Returns the current Python version (e.g., "3.11.2"). +class PythonUtil: + """Utility class for inspecting Python runtime characteristics. - extract_python_implementation() -> str - Returns the Python interpreter name (e.g., "CPython", "PyPy"). + Used internally by ImportSpy to validate runtime-specific conditions declared + in `.yml` import contracts. This includes checking Python version and interpreter + type during structural introspection and contract validation. """ def extract_python_version(self) -> str: - """ - Return the active Python version. + """Return the currently active Python version as a string. - Returns - ------- - str - Python version string (e.g., '3.11.2'). + This method queries the runtime using `platform.python_version()` and is + typically used to match version constraints defined in an import contract. - Example - ------- - >>> PythonUtil().extract_python_version() - '3.11.2' + Returns: + str: The Python version string (e.g., "3.11.4"). + + Example: + >>> PythonUtil().extract_python_version() + '3.11.4' """ return platform.python_version() def extract_python_implementation(self) -> str: - """ - Return the Python implementation type. - - Returns - ------- - str - Python interpreter name (e.g., 'CPython', 'PyPy'). - - Example - ------- - >>> PythonUtil().extract_python_implementation() - 'CPython' + """Return the implementation name of the running Python interpreter. + + Common values include "CPython", "PyPy", or "IronPython". This is + essential in contexts where the implementation affects runtime behavior + or compatibility with native extensions. + + Returns: + str: The interpreter implementation (e.g., "CPython"). + + Example: + >>> PythonUtil().extract_python_implementation() + 'CPython' """ return platform.python_implementation() diff --git a/src/importspy/utilities/runtime_util.py b/src/importspy/utilities/runtime_util.py index f19ebfe..60ec06e 100644 --- a/src/importspy/utilities/runtime_util.py +++ b/src/importspy/utilities/runtime_util.py @@ -1,20 +1,16 @@ -""" -Runtime Environment Utilities -============================= - -Provides a lightweight interface to query the system's hardware architecture. - -This module supports ImportSpy in enforcing architecture-specific constraints -declared in import contracts. +"""Runtime Environment Utilities -Example -------- -.. code-block:: python +Provides a lightweight utility for querying the system's hardware architecture. - from importspy.utilities.runtime_util import RuntimeUtil +This module is used by ImportSpy to enforce architecture-specific constraints +defined in import contracts (e.g., allowing a plugin only on x86_64 or arm64). +It ensures that module imports are aligned with the intended deployment environment. - runtime = RuntimeUtil() - print(runtime.extract_arch()) +Example: + >>> from importspy.utilities.runtime_util import RuntimeUtil + >>> runtime = RuntimeUtil() + >>> runtime.extract_arch() + 'x86_64' """ import logging @@ -23,30 +19,28 @@ logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) -class RuntimeUtil: - """ - Utility class to retrieve system architecture details. - Methods - ------- - extract_arch() -> str - Returns the machine’s hardware architecture. +class RuntimeUtil: + """Utility class to inspect system architecture. - Example - ------- - >>> RuntimeUtil().extract_arch() - 'x86_64' + This class provides methods to retrieve runtime hardware architecture details, + which are essential when validating platform-specific import constraints + in ImportSpy's embedded or CLI modes. """ def extract_arch(self) -> str: - """ - Return the system architecture (e.g., 'x86_64', 'arm64'). - - Uses the `platform.machine()` method to query the current hardware. - - Returns - ------- - str - Architecture name (e.g., 'x86_64', 'arm64'). + """Return the name of the machine's hardware architecture. + + Uses `platform.machine()` to retrieve the architecture string, which may vary + depending on the underlying system (e.g., "x86_64", "arm64", "aarch64"). + This is typically used during contract validation to ensure that the importing + environment matches expected deployment conditions. + + Returns: + str: The system's hardware architecture. + + Example: + >>> RuntimeUtil().extract_arch() + 'arm64' """ return platform.machine() diff --git a/src/importspy/utilities/system_util.py b/src/importspy/utilities/system_util.py index a886158..5fb2709 100644 --- a/src/importspy/utilities/system_util.py +++ b/src/importspy/utilities/system_util.py @@ -1,80 +1,79 @@ -""" -System Utilities for ImportSpy -============================== +"""System Utilities for ImportSpy -Provides tools to interact with the host system and environment variables. +Provides tools to inspect the host operating system and environment variables. -This utility module helps ImportSpy detect and normalize runtime conditions, such as -the operating system or environment setup, ensuring compatibility checks work reliably. +This module supports ImportSpy by normalizing system-level information that may +affect import contract validation. It helps ensure that environmental conditions +are consistent and inspectable across different operating systems and deployment contexts. Features: ---------- -- Identifies the current operating system in a standardized lowercase format. -- Retrieves environment variables as a key-value dictionary. + - Detects the current operating system in a normalized, lowercase format. + - Retrieves all environment variables as a list of structured objects. Example: --------- -.. code-block:: python - - from importspy.utilities.system_util import SystemUtil - - util = SystemUtil() - os_name = util.extract_os() - env = util.extract_envs() + >>> from importspy.utilities.system_util import SystemUtil + >>> util = SystemUtil() + >>> util.extract_os() + 'linux' + >>> envs = util.extract_envs() + >>> envs[0] + VariableInfo(name='PATH', annotation=None, value='/usr/bin') """ import os import logging import platform +from collections import namedtuple +from typing import List logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) -class SystemUtil: - """ - System-level utility class for environment inspection. +VariableInfo = namedtuple('VariableInfo', ["name", "annotation", "value"]) - Offers support for OS detection and retrieval of environment variables. - Methods - ------- - extract_os() -> str - Returns the lowercase name of the operating system (e.g., 'windows', 'linux'). +class SystemUtil: + """Utility class for inspecting system-level properties. + + Used by ImportSpy to collect information about the current operating system + and active environment variables. These details are typically validated + against constraints defined in `.yml` import contracts. - extract_envs() -> dict - Returns a dictionary of all active environment variables. + Methods: + extract_os(): Return the normalized name of the current operating system. + extract_envs(): Return all active environment variables as structured entries. """ def extract_os(self) -> str: - """ - Return the operating system name in lowercase. + """Return the name of the operating system in lowercase format. - Uses `platform.system()` for OS detection. + This method uses `platform.system()` and normalizes the result + to lowercase. It simplifies comparisons with import contract conditions + that expect a canonical form such as "linux", "darwin", or "windows". - Returns - ------- - str - 'windows', 'linux', or 'darwin', depending on the system. + Returns: + str: The normalized operating system name (e.g., "linux", "windows"). - Example - ------- - >>> SystemUtil().extract_os() - 'linux' + Example: + >>> SystemUtil().extract_os() + 'darwin' """ return platform.system().lower() - def extract_envs(self) -> dict: - """ - Retrieve all current environment variables. + def extract_envs(self) -> List[VariableInfo]: + """Return all environment variables as a list of structured objects. + + Collects all key-value pairs from `os.environ` and wraps them in + `VariableInfo` namedtuples. The `annotation` field is reserved for + optional type annotation metadata (currently set to `None`). - Returns - ------- - dict - Dictionary of key-value environment variables. + Returns: + List[VariableInfo]: A list of environment variables available + in the current process environment. - Example - ------- - >>> SystemUtil().extract_envs() - {'PATH': '/usr/bin:/bin', 'HOME': '/home/user', ...} + Example: + >>> envs = SystemUtil().extract_envs() + >>> envs[0] + VariableInfo(name='PATH', annotation=None, value='/usr/bin') """ - return dict(os.environ) + return [VariableInfo(name, None, value) for name, value in os.environ.items()] diff --git a/src/importspy/validators.py b/src/importspy/validators.py new file mode 100644 index 0000000..1833b4c --- /dev/null +++ b/src/importspy/validators.py @@ -0,0 +1,501 @@ +from typing import List +from .models import ( + Runtime, + System, + Environment, + Python, + Module, + Variable, + Function, + Class +) + +from .violation_systems import ( + RuntimeContractViolation, + SystemContractViolation, + VariableContractViolation, + PythonContractViolation, + ModuleContractViolation, + BaseContractViolation, + FunctionContractViolation, + Bundle +) + +from .constants import ( + Constants, + Contexts, + Errors +) + +from .config import Config + +from .log_manager import LogManager + +class RuntimeValidator: + + def validate( + self, + runtimes_1: List[Runtime], + runtimes_2: List[Runtime], + contract_violation: RuntimeContractViolation + + ): + if not runtimes_1: + return + + bundle: Bundle = contract_violation.bundle + bundle[Errors.KEY_RUNTIMES_1] = runtimes_1 + + if not runtimes_2: + raise ValueError( + RuntimeContractViolation( + Contexts.RUNTIME_CONTEXT, + bundle + ).missing_error_handler(Errors.COLLECTIONS_MESSAGES)) + + runtime_2 = runtimes_2[0] + + for runtime_1 in runtimes_1: + if runtime_1.arch == runtime_2.arch: + return runtime_1 + raise ValueError(RuntimeContractViolation( + Contexts.RUNTIME_CONTEXT, + bundle + ).missing_error_handler(Errors.COLLECTIONS_MESSAGES)) + + +class SystemValidator: + + + def __init__(self): + + self._environment_validator = SystemValidator.EnvironmentValidator() + + def validate( + self, + systems_1: List[System], + systems_2: List[System], + contract_violation: SystemContractViolation + ): + + if not systems_1: + return + + bundle: Bundle = contract_violation.bundle + bundle[Errors.KEY_SYSTEMS_1] = systems_1 + + if not systems_2: + raise ValueError( + SystemContractViolation( + Contexts.RUNTIME_CONTEXT, + bundle + ).missing_error_handler(Errors.COLLECTIONS_MESSAGES)) + + system_2 = systems_2[0] + + for system_1 in systems_1: + if system_1.os == system_2.os: + if system_1.environment: + self._environment_validator.validate(system_1.environment, system_2.environment, bundle) + return system_1.pythons + raise ValueError( + SystemContractViolation( + Contexts.RUNTIME_CONTEXT, + bundle + ).missing_error_handler(Errors.COLLECTIONS_MESSAGES)) + + class EnvironmentValidator: + + def validate(self, + environment_1: Environment, + environment_2: Environment, + bundle: Bundle + ): + + if not environment_1: + return + + bundle[Errors.KEY_ENVIRONMENT_1] = environment_1 + + if not environment_2: + raise ValueError( + VariableContractViolation( + Errors.SCOPE_VARIABLE, + Contexts.ENVIRONMENT_CONTEXT, + bundle + ).missing_error_handler(Errors.COLLECTIONS_MESSAGES)) + + variables_2 = environment_2.variables + + if environment_1.variables: + variables_1 = environment_1.variables + VariableValidator().validate( + variables_1, + variables_2, + VariableContractViolation( + Errors.SCOPE_VARIABLE, + Contexts.ENVIRONMENT_CONTEXT, + bundle + ) + ) + +class PythonValidator: + + def validate( + self, + pythons_1: List[Python], + pythons_2: List[Python], + contract_violation: PythonContractViolation + ): + if not pythons_1: + return + + bundle: Bundle = contract_violation.bundle + bundle[Errors.KEY_PYTHONS_1] = pythons_1 + + if not pythons_2: + raise ValueError( + PythonContractViolation( + Contexts.RUNTIME_CONTEXT, + bundle + ).missing_error_handler(Errors.COLLECTIONS_MESSAGES)) + + python_2 = pythons_2[0] + for python_1 in pythons_1: + + if self._is_python_match(python_1, python_2, contract_violation): + return python_1.modules + + raise ValueError( + PythonContractViolation( + Contexts.RUNTIME_CONTEXT, + bundle + ).missing_error_handler(Errors.COLLECTIONS_MESSAGES)) + + def _is_python_match( + self, + python_1: Python, + python_2: Python, + contract_violation: PythonContractViolation + ) -> bool: + bundle: Bundle = contract_violation.bundle + bundle[Errors.KEY_PYTHON_1] = python_1 + if python_1.version and python_1.interpreter: + return ( + python_1.version == python_2.version and + python_1.interpreter == python_2.interpreter + ) + + if python_1.version: + return python_1.version == python_2.version + + if python_1.interpreter: + return python_1.interpreter == python_2.interpreter + +class ModuleValidator: + + def __init__(self): + self.variable_validator:VariableValidator = VariableValidator() + self.function_validator:FunctionValidator = FunctionValidator() + self.class_validator:ClassValidator = ClassValidator() + + def validate( + self, + modules_1: List[Module], + module_2: Module, + contract_violation: ModuleContractViolation + + ): + bundle: Bundle = contract_violation.bundle + if not modules_1: + return + + bundle[Errors.KEY_MODULES_1] = modules_1 + + if not module_2: + raise ValueError( + ModuleContractViolation( + Contexts.RUNTIME_CONTEXT, + bundle + ).missing_error_handler(Errors.COLLECTIONS_MESSAGES)) + + for module_1 in modules_1: + + bundle[Errors.KEY_MODULE_NAME] = module_1.filename + bundle[Errors.KEY_MODULE_VERSION] = module_1.version + + if module_1.filename and module_1.filename != module_2.filename: + raise ValueError( + ModuleContractViolation( + Contexts.RUNTIME_CONTEXT, + bundle + ).mismatch_error_handler(module_1.filename, module_2.filename, Errors.ENTITY_MESSAGES)) + + if module_1.version and module_1.version != module_2.version: + raise ValueError( + ModuleContractViolation( + Contexts.RUNTIME_CONTEXT, + bundle + ).mismatch_error_handler(module_1.version, module_2.version, Errors.ENTITY_MESSAGES)) + + self.variable_validator.validate( + module_1.variables, + module_2.variables, + VariableContractViolation( + Errors.SCOPE_VARIABLE, + Contexts.MODULE_CONTEXT, + bundle + ) + ) + + self.function_validator.validate( + module_1.functions, + module_2.functions, + FunctionContractViolation( + Contexts.MODULE_CONTEXT, + bundle + ) + ) + + self.class_validator.validate( + module_1.classes, + module_2.classes, + ModuleContractViolation( + Contexts.CLASS_CONTEXT, + bundle + ) + ) + +class ClassValidator: + + def __init__(self): + + self.variable_validator:VariableValidator = VariableValidator() + self.function_validator:FunctionValidator = FunctionValidator() + + def validate( + self, + classes_1: List[Class], + classes_2: List[Class], + contract_violation: BaseContractViolation + ): + if not classes_1: + return + + bundle: Bundle = contract_violation.bundle + bundle[Errors.KEY_CLASSES_1] = classes_1 + + if not classes_2: + raise ValueError( + ModuleContractViolation( + Contexts.CLASS_CONTEXT, + bundle + ).missing_error_handler(Errors.COLLECTIONS_MESSAGES)) + + for class_1 in classes_1: + class_2 = next((cls for cls in classes_2 if cls.name == class_1.name), None) + + bundle[Errors.KEY_CLASS_NAME] = class_1.name + + if not class_2: + raise ValueError( + ModuleContractViolation( + Contexts.CLASS_CONTEXT, + bundle + ).missing_error_handler(Errors.ENTITY_MESSAGES) + ) + + bundle[Errors.KEY_ATTRIBUTE_TYPE] = Config.CLASS_TYPE + + self.variable_validator.validate( + class_1.get_class_attributes(), + class_2.get_class_attributes(), + VariableContractViolation(Errors.SCOPE_ARGUMENT, Contexts.CLASS_CONTEXT, bundle) + ) + + bundle[Errors.KEY_ATTRIBUTE_TYPE] = Config.INSTANCE_TYPE + + self.variable_validator.validate( + class_1.get_instance_attributes(), + class_2.get_instance_attributes(), + VariableContractViolation(Errors.SCOPE_ARGUMENT, Contexts.CLASS_CONTEXT, bundle) + ) + + self.function_validator.validate( + class_1.methods, + class_2.methods, + FunctionContractViolation( + Contexts.CLASS_CONTEXT, + bundle + ) + ) + + self.validate(class_1.superclasses, + class_2.superclasses, + ModuleContractViolation( + Contexts.CLASS_CONTEXT, + bundle + ) + ) + +class VariableValidator: + + def __init__(self): + + self.logger = LogManager().get_logger(self.__class__.__name__) + + def validate( + self, + variables_1: List[Variable], + variables_2: List[Variable], + contract_violation: VariableContractViolation + ): + bundle: Bundle = contract_violation.bundle + self.logger.debug(f"Type of variables_1: {type(variables_1)}") + self.logger.debug( + Constants.LOG_MESSAGE_TEMPLATE.format( + operation="Variable validating", + status="Starting", + details=f"Expected Variables: {variables_1} ; Actual Variables: {variables_2}" + ) + ) + + if not variables_1: + self.logger.debug( + Constants.LOG_MESSAGE_TEMPLATE.format( + operation="Check if variables_1 is not none", + status="Finished", + details="No expected Variables to validate" + ) + ) + return + + bundle[Errors.VARIABLES_DINAMIC_PAYLOAD[contract_violation.scope][Errors.COLLECTIONS_MESSAGES][contract_violation.context]] = variables_1 + + if not variables_2: + self.logger.debug( + Constants.LOG_MESSAGE_TEMPLATE.format( + operation="Checking variables_2 when variables_1 is missing", + status="Finished", + details="No actual Variables found for validation" + ) + ) + raise ValueError(contract_violation.missing_error_handler(Errors.COLLECTIONS_MESSAGES)) + + for var_1 in variables_1: + self.logger.debug( + Constants.LOG_MESSAGE_TEMPLATE.format( + operation="Variable validating", + status="Progress", + details=f"Current var_1: {var_1}" + ) + ) + bundle[Errors.VARIABLES_DINAMIC_PAYLOAD[contract_violation.scope][Errors.ENTITY_MESSAGES][contract_violation.context]] = var_1.name + self.logger.debug(bundle) + if var_1.name not in {var.name for var in variables_2}: + raise ValueError(contract_violation.missing_error_handler(Errors.ENTITY_MESSAGES)) + + for var_1 in variables_1: + var_2 = next((var for var in variables_2 if var.name == var_1.name), None) + if not var_2: + raise ValueError(contract_violation.missing_error_handler(Errors.ENTITY_MESSAGES)) + + if var_1.annotation and var_1.annotation != var_2.annotation: + raise ValueError(contract_violation.mismatch_error_handler(var_1.annotation, var_2.annotation, Errors.ENTITY_MESSAGES)) + + if var_1.value != var_2.value: + raise ValueError(contract_violation.mismatch_error_handler(var_1.value, var_2.value, Errors.ENTITY_MESSAGES)) + +class FunctionValidator: + + def __init__(self): + + self.argument_validator:VariableValidator = VariableValidator() + self.logger = LogManager().get_logger(self.__class__.__name__) + + def validate( + self, + functions_1: List[Function], + functions_2: List[Function], + contract_violation: BaseContractViolation + ): + + bundle: Bundle = contract_violation.bundle + self.logger.debug( + Constants.LOG_MESSAGE_TEMPLATE.format( + operation="Function validating", + status="Starting", + details=f"Expected functions: {functions_1} ; Current functions: {functions_2}" + ) + ) + + if not functions_1: + self.logger.debug( + Constants.LOG_MESSAGE_TEMPLATE.format( + operation="Check if functions_1 is not none", + status="Finished", + details="No functions to validate" + ) + ) + return + + bundle[Errors.FUNCTIONS_DINAMIC_PAYLOAD[Errors.COLLECTIONS_MESSAGES][contract_violation.context]] = functions_1 + + if not functions_2: + self.logger.debug( + Constants.LOG_MESSAGE_TEMPLATE.format( + operation="Checking functions_2 when functions_1 is missing", + status="Finished", + details="No actual functions found" + ) + ) + raise ValueError(contract_violation.missing_error_handler(Errors.COLLECTIONS_MESSAGES)) + + for function_1 in functions_1: + bundle[Errors.FUNCTIONS_DINAMIC_PAYLOAD[Errors.ENTITY_MESSAGES][contract_violation.context]] = function_1.name + self.logger.debug( + Constants.LOG_MESSAGE_TEMPLATE.format( + operation="Function validating", + status="Progress", + details=f"Current function: {function_1}" + ) + ) + if function_1.name not in {f.name for f in functions_2}: + self.logger.debug( + Constants.LOG_MESSAGE_TEMPLATE.format( + operation="Checking if function_1 is in functions_2", + status="Finished for function missing", + details=f"function_1: {function_1}; functions_2: {functions_2}" + ) + ) + raise ValueError(contract_violation.missing_error_handler(Errors.ENTITY_MESSAGES)) + + for function_1 in functions_1: + function_2 = next((f for f in functions_2 if f.name == function_1.name), None) + if not function_2: + raise ValueError(contract_violation.missing_error_handler(Errors.ENTITY_MESSAGES)) + + self.argument_validator.validate( + function_1.arguments, + function_2.arguments, + VariableContractViolation( + Errors.SCOPE_ARGUMENT, + Contexts.MODULE_CONTEXT, + bundle) + ) + + if function_1.return_annotation and function_1.return_annotation != function_2.return_annotation: + raise ValueError(contract_violation.mismatch_error_handler( + function_1.return_annotation, + function_2.return_annotation, + Errors.ENTITY_MESSAGES + ) + ) + + self.logger.debug( + Constants.LOG_MESSAGE_TEMPLATE.format( + operation="Function validating", + status="Completed", + details="Validation successful." + ) + ) diff --git a/src/importspy/validators/argument_validator.py b/src/importspy/validators/argument_validator.py deleted file mode 100644 index a8eb027..0000000 --- a/src/importspy/validators/argument_validator.py +++ /dev/null @@ -1,162 +0,0 @@ -""" -importspy.validators.argument_validator -======================================= - -This module provides validation for function or method arguments -within Python modules being inspected by ImportSpy. - -The `ArgumentValidator` compares declared arguments from the import contract -against the actual arguments found in the target module, ensuring: -- Name consistency -- Type annotation compliance -- Default value consistency - -This validator is typically called from FunctionValidator or ClassValidator -as part of a full SpyModel validation. -""" - -from ..models import Argument -from ..errors import Errors -from ..constants import Constants -from typing import Optional, List -from importspy.log_manager import LogManager - - -class ArgumentValidator: - """ - Validates argument definitions within functions or methods. - - This class ensures that each expected argument matches its actual counterpart - in terms of name, type annotation, and default value. - - Attributes - ---------- - logger : logging.Logger - Internal logger used for debug output. - - Methods - ------- - validate(arguments_1, arguments_2, function_name, class_name="") - Compare two sets of arguments and raise errors for mismatches. - """ - - def __init__(self): - """ - Initialize the ArgumentValidator with a scoped logger. - """ - self.logger = LogManager().get_logger(self.__class__.__name__) - - def validate( - self, - arguments_1: List[Argument], - arguments_2: List[Argument], - function_name: str, - class_name: Optional[str] = "" - ): - """ - Validate function or method arguments for name, type, and value compliance. - - Parameters - ---------- - arguments_1 : List[Argument] - List of expected arguments defined in the import contract. - - arguments_2 : List[Argument] - List of actual arguments found in the inspected module. - - function_name : str - The name of the function or method being validated. - - class_name : Optional[str], default="" - The name of the class containing the method (if any), used for error context. - - Returns - ------- - bool - True if validation passes without raising an exception. - - Raises - ------ - ValueError - - If expected arguments are missing. - - If type annotations mismatch. - - If default values differ. - - Example - ------- - >>> validator = ArgumentValidator() - >>> validator.validate( - ... arguments_1=[Argument(name="x", annotation="int")], - ... arguments_2=[Argument(name="x", annotation="int")], - ... function_name="my_function" - ... ) - True - """ - context_name = f"method {function_name}" if class_name else f"function {function_name}" - - self.logger.debug( - Constants.LOG_MESSAGE_TEMPLATE.format( - operation="Attribute validating", - status="Starting", - details=f"Expected attributes: {arguments_1} ; Current attributes: {arguments_2}" - ) - ) - - if not arguments_1: - self.logger.debug( - Constants.LOG_MESSAGE_TEMPLATE.format( - operation="Check if arguments_1 is not none", - status="Finished", - details=f"No declared arguments to validate; arguments_1: {arguments_1}" - ) - ) - return - - if not arguments_2: - self.logger.debug( - Constants.LOG_MESSAGE_TEMPLATE.format( - operation="Checking arguments_2 when arguments_1 is missing", - status="Finished", - details=f"No actual arguments found; arguments_2: {arguments_2}" - ) - ) - raise ValueError(Errors.ELEMENT_MISSING.format(arguments_1)) - - for argument_1 in arguments_1: - self.logger.debug( - Constants.LOG_MESSAGE_TEMPLATE.format( - operation="Argument validating", - status="Progress", - details=f"Current argument_1: {argument_1}" - ) - ) - if argument_1.name not in set(arg.name for arg in arguments_2): - raise ValueError(Errors.ARGUMENT_MISSING.format(argument_1.name, context_name)) - - for argument_1 in arguments_1: - argument_2 = next((arg for arg in arguments_2 if arg.name == argument_1.name), None) - - if not argument_2: - raise ValueError(Errors.ELEMENT_MISSING.format(argument_1)) - - if argument_1.annotation and argument_1.annotation != argument_2.annotation: - raise ValueError( - Errors.ARGUMENT_MISMATCH.format( - Constants.ANNOTATION, - argument_1.name, - argument_1.annotation, - argument_2.annotation - ) - ) - - if argument_1.value != argument_2.value: - raise ValueError( - Errors.ARGUMENT_MISMATCH.format( - Constants.VALUE, - argument_1.name, - argument_1.value, - argument_2.value - ) - ) - - return True diff --git a/src/importspy/validators/attribute_validator.py b/src/importspy/validators/attribute_validator.py deleted file mode 100644 index 9dcc3b8..0000000 --- a/src/importspy/validators/attribute_validator.py +++ /dev/null @@ -1,172 +0,0 @@ -""" -importspy.validators.attribute_validator -======================================== - -This module implements the validation logic for class and instance attributes -within modules inspected by ImportSpy. - -The `AttributeValidator` ensures that attributes declared in the import contract -match the ones actually defined in the module under inspection in terms of: -- Existence -- Type annotation -- Default value -""" - -from ..models import Attribute -from ..errors import Errors -from ..constants import Constants -from typing import List -from importspy.log_manager import LogManager - - -class AttributeValidator: - """ - Validator for class and instance attributes. - - Compares expected attributes (from the import contract) with those - extracted from the inspected module. Ensures attribute names, - annotations, and values are consistent. - - Attributes - ---------- - logger : logging.Logger - Internal logger used for debug tracing during validation. - - Methods - ------- - validate(attrs_1, attrs_2, classname) - Performs full validation of attributes for a given class. - """ - - def __init__(self): - """ - Initializes the AttributeValidator with scoped logging. - """ - self.logger = LogManager().get_logger(self.__class__.__name__) - - def validate( - self, - attrs_1: List[Attribute], - attrs_2: List[Attribute], - classname: str - ): - """ - Validates expected vs actual attributes in a class definition. - - Parameters - ---------- - attrs_1 : List[Attribute] - List of attributes defined in the import contract. - - attrs_2 : List[Attribute] - List of attributes found in the actual module. - - classname : str - The name of the class whose attributes are being validated. - - Returns - ------- - bool - True if all attributes match expectations. - - Raises - ------ - ValueError - - If required attributes are missing. - - If type annotations differ. - - If attribute values differ. - - Example - ------- - >>> validator = AttributeValidator() - >>> validator.validate( - ... attrs_1=[Attribute(name="path", value="/tmp", annotation="str", type="class")], - ... attrs_2=[Attribute(name="path", value="/tmp", annotation="str", type="class")], - ... classname="Config" - ... ) - True - """ - self.logger.debug(f"Type of attrs_1: {type(attrs_1)}") - self.logger.debug( - Constants.LOG_MESSAGE_TEMPLATE.format( - operation="Attribute validating", - status="Starting", - details=f"Expected attributes: {attrs_1} ; Current attributes: {attrs_2}" - ) - ) - - if not attrs_1: - self.logger.debug( - Constants.LOG_MESSAGE_TEMPLATE.format( - operation="Check if attrs_1 is not none", - status="Finished", - details=f"No expected attributes; attrs_1: {attrs_1}" - ) - ) - return - - if not attrs_2: - self.logger.debug( - Constants.LOG_MESSAGE_TEMPLATE.format( - operation="Checking attrs_2 when attrs_1 is missing", - status="Finished", - details=f"No actual attributes found; attrs_2: {attrs_2}" - ) - ) - raise ValueError(Errors.ELEMENT_MISSING.format(attrs_1)) - - for attr_1 in attrs_1: - self.logger.debug(f"Type of attr_1: {type(attr_1)}") - self.logger.debug( - Constants.LOG_MESSAGE_TEMPLATE.format( - operation="Attribute validating", - status="Progress", - details=f"Current attr_1: {attr_1}" - ) - ) - if attr_1.name not in {attr.name for attr in attrs_2}: - self.logger.debug(Constants.LOG_MESSAGE_TEMPLATE.format( - operation="Checking if attr_1 is in attrs_2", - status="Finished", - details=Errors.CLASS_ATTRIBUTE_MISSING.format( - attr_1.type, - f"{attr_1.name}={attr_1.value}", - classname - ) - )) - raise ValueError( - Errors.CLASS_ATTRIBUTE_MISSING.format( - attr_1.type, - f"{attr_1.name}={attr_1.value}", - classname - ) - ) - - for attr_1 in attrs_1: - attr_2 = next((attr for attr in attrs_2 if attr.name == attr_1.name), None) - if not attr_2: - raise ValueError(Errors.ELEMENT_MISSING.format(attrs_1)) - - if attr_1.annotation and attr_1.annotation != attr_2.annotation: - raise ValueError( - Errors.CLASS_ATTRIBUTE_MISMATCH.format( - Constants.ANNOTATION, - attr_1.type, - attr_1.name, - attr_1.annotation, - attr_2.annotation - ) - ) - - if attr_1.value != attr_2.value: - raise ValueError( - Errors.CLASS_ATTRIBUTE_MISMATCH.format( - Constants.VALUE, - attr_1.type, - attr_1.name, - attr_1.value, - attr_2.value - ) - ) - - return True diff --git a/src/importspy/validators/common_validator.py b/src/importspy/validators/common_validator.py deleted file mode 100644 index 27227af..0000000 --- a/src/importspy/validators/common_validator.py +++ /dev/null @@ -1,161 +0,0 @@ -""" -importspy.validators.common_validator -===================================== - -Reusable validation logic for dictionary and list structures. - -This module defines the `CommonValidator` class, which provides utility methods to validate -data structures commonly used in contract inspection: -- General list containment validation -- Key/value consistency between dictionaries - -It is used across structural validators like: -- SystemValidator -- ModuleValidator -- RuntimeValidator -""" -from typing import List, Dict -from ..errors import Errors -from ..constants import Constants -from ..log_manager import LogManager - - -class CommonValidator: - """ - Common validation utilities for iterable structures. - - This helper class enables reusable checks across all ImportSpy validators. - It ensures list and dict structures match between expected (from `.yml`) - and actual (live modules) data. - - Validation Modes: - ----------------- - - list_validate(...) : All elements in list1 must exist in list2. - - dict_validate(...) : All key/value pairs in dict1 must match dict2. - - Attributes - ---------- - logger : logging.Logger - A scoped logger for structured output and debugging. - """ - - def __init__(self): - """ - Initializes the CommonValidator and its logger. - """ - self.logger = LogManager().get_logger(self.__class__.__name__) - - def list_validate( - self, - list1: List, - list2: List, - missing_error: str, - *args - ) -> None: - """ - Validates that all elements in `list1` exist in `list2`. - - Parameters - ---------- - list1 : List - The expected list of items. - list2 : List - The actual list to be validated. - missing_error : str - Error message format if an element is missing (e.g., "Missing: {0}"). - *args : tuple - Optional dynamic context passed to `missing_error.format(...)`. - - Raises - ------ - ValueError - If any element from `list1` is not present in `list2`. - - Returns - ------- - None - - Example - ------- - >>> CommonValidator().list_validate(["A", "B"], ["A"], "Missing item: {0}") - """ - self.logger.debug( - Constants.LOG_MESSAGE_TEMPLATE.format( - operation="List validating", - status="Starting", - details=f"Expected list: {list1} ; Current list: {list2}" - ) - ) - - if not list1: - return - if list1 and not list2: - return - if not list2: - raise ValueError(Errors.ELEMENT_MISSING.format(list1)) - - for expected_element in list1: - if expected_element not in list2: - raise ValueError(missing_error.format(expected_element, *args)) - - def dict_validate( - self, - dict1: Dict, - dict2: Dict, - missing_error: str, - mismatch_error: str - ) -> bool: - """ - Validates keys and values between two dictionaries. - - Parameters - ---------- - dict1 : Dict - The expected key-value mapping. - dict2 : Dict - The actual dictionary to check. - missing_error : str - Format string for a missing key (e.g., "Key missing: {0}"). - mismatch_error : str - Format string for value mismatch (e.g., "Mismatch for {0}: {1} != {2}"). - - Returns - ------- - bool - True if validation passes. - - Raises - ------ - ValueError - If a key is missing or a value does not match. - - Example - ------- - >>> CommonValidator().dict_validate( - ... {"x": 1}, {"x": 2}, - ... "Missing key: {0}", - ... "Mismatch for {0}: expected {1}, got {2}" - ... ) - """ - self.logger.debug( - Constants.LOG_MESSAGE_TEMPLATE.format( - operation="Dict validating", - status="Starting", - details=f"Expected dict: {dict1} ; Current dict: {dict2}" - ) - ) - - if not dict1: - return True - if not dict2: - raise ValueError(missing_error.format(dict1)) - - for expected_key, expected_value in dict1.items(): - if expected_key in dict2: - actual_value = dict2[expected_key] - if expected_value != actual_value: - raise ValueError(mismatch_error.format(expected_key, expected_value, actual_value)) - else: - raise ValueError(missing_error.format(expected_key)) - - return True diff --git a/src/importspy/validators/function_validator.py b/src/importspy/validators/function_validator.py deleted file mode 100644 index 72ef6b7..0000000 --- a/src/importspy/validators/function_validator.py +++ /dev/null @@ -1,160 +0,0 @@ -""" -importspy.validators.function_validator -======================================= - -Validator for function declarations and signatures. - -This module defines the `FunctionValidator`, responsible for verifying that functions -defined in a Python module (or class) match those specified in an import contract. -It ensures that: -- Each expected function is present. -- Return annotations are correct. -- Function arguments match in name, annotation, and value. - -This validator uses `ArgumentValidator` to validate function arguments. -""" - -from ..models import Function -from ..errors import Errors -from ..constants import Constants -from typing import List, Optional -from ..log_manager import LogManager -from .argument_validator import ArgumentValidator - - -class FunctionValidator: - """ - Validator for function declarations and signatures. - - Attributes - ---------- - logger : logging.Logger - Logger used for debug tracing. - _argument_validator : ArgumentValidator - Helper validator to handle argument validation. - """ - - def __init__(self): - """ - Initialize the function validator and argument checker. - """ - self.logger = LogManager().get_logger(self.__class__.__name__) - self._argument_validator = ArgumentValidator() - - def validate( - self, - functions_1: List[Function], - functions_2: List[Function], - classname: Optional[str] = "" - ) -> Optional[bool]: - """ - Validate a list of expected functions against actual module functions. - - Parameters - ---------- - functions_1 : List[Function] - The list of expected functions (from import contract). - functions_2 : List[Function] - The actual functions extracted from the module. - classname : Optional[str], default="" - If validating class methods, the class name (for error context). - - Returns - ------- - Optional[bool] - True if validation passes, None if nothing to validate. - - Raises - ------ - ValueError - If: - - A function is missing. - - Return annotations differ. - - Argument validation fails. - - Example - ------- - >>> validator = FunctionValidator() - >>> validator.validate(expected_functions, actual_functions, classname="MyService") - True - """ - context_name = f"method in class {classname}" if classname else "function" - - self.logger.debug( - Constants.LOG_MESSAGE_TEMPLATE.format( - operation="Function validating", - status="Starting", - details=f"Expected functions: {functions_1} ; Current functions: {functions_2}" - ) - ) - - if not functions_1: - self.logger.debug( - Constants.LOG_MESSAGE_TEMPLATE.format( - operation="Check if functions_1 is not none", - status="Finished", - details="No functions to validate" - ) - ) - return None - - if not functions_2: - self.logger.debug( - Constants.LOG_MESSAGE_TEMPLATE.format( - operation="Checking functions_2 when functions_1 is missing", - status="Finished", - details="No actual functions found" - ) - ) - raise ValueError(Errors.ELEMENT_MISSING.format(functions_1)) - - for function_1 in functions_1: - self.logger.debug( - Constants.LOG_MESSAGE_TEMPLATE.format( - operation="Function validating", - status="Progress", - details=f"Current function: {function_1}" - ) - ) - if function_1.name not in {f.name for f in functions_2}: - self.logger.debug( - Constants.LOG_MESSAGE_TEMPLATE.format( - operation="Checking if function_1 is in functions_2", - status="Finished for function missing", - details=f"function_1: {function_1}; functions_2: {functions_2}" - ) - ) - raise ValueError( - Errors.FUNCTIONS_MISSING.format(context_name, function_1.name) - ) - - for function_1 in functions_1: - function_2 = next((f for f in functions_2 if f.name == function_1.name), None) - if not function_2: - raise ValueError(Errors.ELEMENT_MISSING.format(function_1)) - - self._argument_validator.validate( - function_1.arguments, - function_2.arguments, - function_1.name, - classname - ) - - if function_1.return_annotation and function_1.return_annotation != function_2.return_annotation: - raise ValueError( - Errors.FUNCTION_RETURN_ANNOTATION_MISMATCH.format( - context_name, - function_1.name, - function_1.return_annotation, - function_2.return_annotation - ) - ) - - self.logger.debug( - Constants.LOG_MESSAGE_TEMPLATE.format( - operation="Function validating", - status="Completed", - details="Validation successful." - ) - ) - return True diff --git a/src/importspy/validators/module_validator.py b/src/importspy/validators/module_validator.py deleted file mode 100644 index d8b6b3f..0000000 --- a/src/importspy/validators/module_validator.py +++ /dev/null @@ -1,141 +0,0 @@ -""" -importspy.validators.module_validator -===================================== - -Validator for Python modules and their internal structures. - -This module provides the `ModuleValidator` class, responsible for checking -that a Python module conforms to the expectations defined in an import contract. - -It validates: -- Module filename and version -- Declared global variables -- Top-level functions -- Declared classes, including their attributes, methods, and superclasses - -Delegates detailed checks to: -- `AttributeValidator` -- `FunctionValidator` -- `CommonValidator` -""" - -from ..models import Module -from ..errors import Errors -from .variable_validator import VariableValidator -from .attribute_validator import AttributeValidator -from .function_validator import FunctionValidator -from .common_validator import CommonValidator -from typing import List, Optional - - -class ModuleValidator: - """ - Validator for full Python module metadata and structure. - - Attributes - ---------- - _attribute_validator : AttributeValidator - Responsible for validating class attributes. - _function_validator : FunctionValidator - Validates top-level and class methods. - """ - - def __init__(self): - """ - Initialize the validator with attribute and function checkers. - """ - self._variable_validator = VariableValidator() - self._attribute_validator = AttributeValidator() - self._function_validator = FunctionValidator() - - def validate( - self, - modules_1: List[Module], - module_2: Optional[Module] - ) -> Optional[None]: - """ - Validate one or more expected modules against the actual loaded module. - - Parameters - ---------- - modules_1 : List[Module] - List of expected module definitions from the import contract. - module_2 : Optional[Module] - The actual module extracted from the system for validation. - - Returns - ------- - None - Returns None when: - - No modules to validate (`modules_1` is empty). - - Validation is successful. - - Raises - ------ - ValueError - Raised if: - - `module_2` is missing. - - Filename or version mismatches. - - Variables differ in name or value. - - Missing or invalid functions, classes, attributes, or superclasses. - - Example - ------- - >>> validator = ModuleValidator() - >>> validator.validate([expected_module], actual_module) - """ - if not modules_1: - return - - if not module_2: - raise ValueError(Errors.ELEMENT_MISSING.format(modules_1)) - - for module_1 in modules_1: - # Check filename - if module_1.filename and module_1.filename != module_2.filename: - raise ValueError(Errors.FILENAME_MISMATCH.format(module_1.filename, module_2.filename)) - - # Check version - if module_1.version and module_1.version != module_2.version: - raise ValueError(Errors.VERSION_MISMATCH.format(module_1.version, module_2.version)) - - self._variable_validator.validate( - module_1.variables, - module_2.variables) - - # Validate top-level functions - self._function_validator.validate( - module_1.functions, - module_2.functions - ) - - # Validate classes and class contents - if module_1.classes: - for class_1 in module_1.classes: - class_2 = next((cls for cls in module_2.classes if cls.name == class_1.name), None) - if not class_2: - raise ValueError(Errors.CLASS_MISSING.format(class_1.name)) - - # Class attribute check - self._attribute_validator.validate( - class_1.attributes, - class_2.attributes, - class_1.name - ) - - # Method (function) check - self._function_validator.validate( - class_1.methods, - class_2.methods, - classname=class_1.name - ) - - # Superclass check - CommonValidator().list_validate( - class_1.superclasses, - class_2.superclasses, - Errors.CLASS_SUPERCLASS_MISSING, - class_2.name - ) - - return diff --git a/src/importspy/validators/python_validator.py b/src/importspy/validators/python_validator.py deleted file mode 100644 index b6cc131..0000000 --- a/src/importspy/validators/python_validator.py +++ /dev/null @@ -1,135 +0,0 @@ -""" -importspy.validators.python_validator -===================================== - -Validator for Python runtime configurations. - -This module defines the `PythonValidator` class, responsible for validating -the Python version, interpreter, and associated modules declared in an -import contract against the actual Python runtime context. - -Delegates module-level validation to `ModuleValidator`. -""" - -from ..models import Python -from ..errors import Errors -from .module_validator import ModuleValidator -from ..log_manager import LogManager -from ..constants import Constants -from typing import List, Optional - - -class PythonValidator: - """ - Validates Python runtime configuration and associated modules. - - Attributes - ---------- - logger : logging.Logger - Logger instance for debugging and tracing. - _module_validator : ModuleValidator - Validator for modules within the Python configuration. - """ - - def __init__(self): - """ - Initialize the validator and internal module validator. - """ - self.logger = LogManager().get_logger(self.__class__.__name__) - self._module_validator = ModuleValidator() - - def validate( - self, - pythons_1: List[Python], - pythons_2: Optional[List[Python]] - ) -> Optional[None]: - """ - Validate a list of expected Python environments against actual ones. - - Parameters - ---------- - pythons_1 : List[Python] - Expected Python configurations from the contract. - pythons_2 : Optional[List[Python]] - Actual Python runtime details extracted from the system. - - Returns - ------- - None - Returned when: - - `pythons_1` is empty (no validation needed). - - Validation succeeds. - - Raises - ------ - ValueError - If `pythons_2` is missing or does not match - the declared expectations in `pythons_1`. - - Example - ------- - >>> validator = PythonValidator() - >>> validator.validate([expected_python], [actual_python]) - """ - if not pythons_1: - return - - if not pythons_2: - raise ValueError(Errors.ELEMENT_MISSING.format(pythons_1)) - - python_2 = pythons_2[0] - for python_1 in pythons_1: - self.logger.debug( - Constants.LOG_MESSAGE_TEMPLATE.format( - operation="Python validating", - status="Progress", - details=f"Expected python: {python_1} ; Current python: {python_2}" - ) - ) - - if self._is_python_match(python_1, python_2): - if python_2.modules: - self._module_validator.validate(python_1.modules, python_2.modules[0]) - return - - def _is_python_match( - self, - python_1: Python, - python_2: Python - ) -> bool: - """ - Determine whether two Python configurations match. - - Parameters - ---------- - python_1 : Python - Expected configuration. - python_2 : Python - Actual system configuration. - - Returns - ------- - bool - `True` if the two configurations match according to the declared criteria, - otherwise `False`. - - Matching Criteria - ----------------- - - If both version and interpreter are defined: match both. - - If only version is defined: match version. - - If only interpreter is defined: match interpreter. - - If none are defined: match anything (default `True`). - """ - if python_1.version and python_1.interpreter: - return ( - python_1.version == python_2.version and - python_1.interpreter == python_2.interpreter - ) - - if python_1.version: - return python_1.version == python_2.version - - if python_1.interpreter: - return python_1.interpreter == python_2.interpreter - - return True diff --git a/src/importspy/validators/runtime_validator.py b/src/importspy/validators/runtime_validator.py deleted file mode 100644 index 87e0ff3..0000000 --- a/src/importspy/validators/runtime_validator.py +++ /dev/null @@ -1,82 +0,0 @@ -""" -importspy.validators.runtime_validator -====================================== - -Validator for runtime configurations. - -This module defines the `RuntimeValidator` class, which ensures that the -runtime architecture and system-level environment of a Python module -conform to what is declared in its import contract. - -Delegates system validation to `SystemValidator`. -""" - -from ..models import Runtime -from ..errors import Errors -from .system_validator import SystemValidator -from typing import List - - -class RuntimeValidator: - """ - Validates runtime architecture and system configurations. - - Attributes - ---------- - _system_validator : SystemValidator - Handles validation of OS and platform-specific system expectations. - """ - - def __init__(self): - """ - Initialize the runtime validator and prepare the system validator. - """ - self._system_validator = SystemValidator() - - def validate( - self, - runtimes_1: List[Runtime], - runtimes_2: List[Runtime] - ) -> None: - """ - Validate expected runtime declarations against actual runtime data. - - Parameters - ---------- - runtimes_1 : List[Runtime] - The expected runtime environments declared in the contract. - runtimes_2 : List[Runtime] - The actual detected runtime environments from the host system. - - Returns - ------- - None - Returned when: - - `runtimes_1` is empty (no validation required). - - Validation completes successfully. - - Raises - ------ - ValueError - - If `runtimes_2` is missing but expectations are defined. - - If the architectures do not match. - - If any contained system-level configuration mismatches are detected. - - Example - ------- - >>> validator = RuntimeValidator() - >>> validator.validate([expected_runtime], [actual_runtime]) - """ - if not runtimes_1: - return - - if not runtimes_2: - raise ValueError(Errors.ELEMENT_MISSING.format(runtimes_1)) - - runtime_2 = runtimes_2[0] - - for runtime_1 in runtimes_1: - if runtime_1.arch == runtime_2.arch: - if runtime_1.systems: - self._system_validator.validate(runtime_1.systems, runtime_2.systems) - return diff --git a/src/importspy/validators/spymodel_validator.py b/src/importspy/validators/spymodel_validator.py deleted file mode 100644 index a7d7327..0000000 --- a/src/importspy/validators/spymodel_validator.py +++ /dev/null @@ -1,95 +0,0 @@ -""" -importspy.validators.spymodel_validator -======================================== - -Validator for top-level SpyModel objects. - -This module defines the `SpyModelValidator` class, which orchestrates full-model validation, -including both runtime environments and declared modules. It is typically invoked as the -final step during the ImportSpy validation pipeline. -""" - -from ..models import SpyModel -from .runtime_validator import RuntimeValidator -from .module_validator import ModuleValidator - - -class SpyModelValidator: - """ - Validates the full structure of an ImportSpy model contract. - - The `SpyModelValidator` ensures that: - - Declared runtime deployments (architecture + system + interpreter) match. - - Declared module definitions (files, classes, functions) match. - - Delegates: - ---------- - - Runtime inspection to `RuntimeValidator` - - Module structure comparison to `ModuleValidator` - - Validation Scope: - ----------------- - ✓ Architecture and OS validation - ✓ Interpreter and Python version match - ✓ Module filename, version, structure, functions, classes - - This validator serves as the **entry point** for verifying SpyModel objects, - typically loaded from YAML contracts and dynamically matched against live modules. - - Attributes - ---------- - _runtime_validator : RuntimeValidator - Validates runtime architecture and interpreter. - _module_validator : ModuleValidator - Validates classes, functions, and variables inside modules. - """ - - def __init__(self): - """ - Initializes the SpyModelValidator with supporting sub-validators. - """ - self._runtime_validator = RuntimeValidator() - self._module_validator = ModuleValidator() - - def validate( - self, - spy_model_1: SpyModel, - spy_model_2: SpyModel - ) -> None: - """ - Validates a declared SpyModel (from contract) against the active runtime SpyModel. - - Parameters - ---------- - spy_model_1 : SpyModel - The expected SpyModel structure (loaded from contract). - spy_model_2 : SpyModel - The actual SpyModel structure (derived from live inspection). - - Returns - ------- - None - If validation passes or `spy_model_1` has no runtime deployments to validate. - - Raises - ------ - ValueError - If architecture, interpreter, modules, or structural expectations are not met. - - Example - ------- - >>> validator = SpyModelValidator() - >>> validator.validate(spy_model_contract, spy_model_live) - """ - # Validate runtime deployments - self._runtime_validator.validate(spy_model_1.deployments, spy_model_2.deployments) - - # Navigate through the resolved runtime > system > python > module - self._module_validator.validate( - [spy_model_1], - spy_model_2 - .deployments[0] - .systems[0] - .pythons[0] - .modules[0] - ) diff --git a/src/importspy/validators/system_validator.py b/src/importspy/validators/system_validator.py deleted file mode 100644 index 4bbb42c..0000000 --- a/src/importspy/validators/system_validator.py +++ /dev/null @@ -1,99 +0,0 @@ -""" -importspy.validators.system_validator -====================================== - -Validator for system-level configurations. - -This module defines the `SystemValidator` class, responsible for validating -operating systems, environment variables, and Python interpreter settings -within a runtime context. - -Delegates: -- Environment and key-value matching to `CommonValidator` -- Python version/interpreter matching to `PythonValidator` -""" - -from typing import List -from ..models import System -from ..errors import Errors -from .common_validator import CommonValidator -from .python_validator import PythonValidator - - -class SystemValidator: - """ - Validates system-level execution environments. - - This includes: - - Operating system matching - - Environment variable validation - - Python configuration checks (via `PythonValidator`) - - Attributes - ---------- - _python_validator : PythonValidator - Handles validation of nested Python interpreter configurations. - """ - - def __init__(self): - """ - Initialize the system validator and prepare supporting validators. - """ - self._python_validator = PythonValidator() - - def validate( - self, - systems_1: List[System], - systems_2: List[System] - ) -> None: - """ - Validate declared system expectations against actual system properties. - - Parameters - ---------- - systems_1 : List[System] - Expected system configurations as declared in the import contract. - systems_2 : List[System] - Actual detected system environment. - - Returns - ------- - None - Returned when: - - `systems_1` is empty (no validation required). - - Validation completes successfully without mismatches. - - Raises - ------ - ValueError - - If `systems_2` is missing but expected. - - If operating systems do not match. - - If environment variables mismatch or are missing. - - If any Python interpreter configuration fails validation. - - Example - ------- - >>> validator = SystemValidator() - >>> validator.validate([expected_system], [actual_system]) - """ - if not systems_1: - return - - if not systems_2: - raise ValueError(Errors.ELEMENT_MISSING.format(systems_1)) - - cv = CommonValidator() - system_2 = systems_2[0] - - for system_1 in systems_1: - if system_1.os == system_2.os: - if system_1.envs: - cv.dict_validate( - system_1.envs, - system_2.envs, - Errors.ENV_VAR_MISSING, - Errors.ENV_VAR_MISMATCH - ) - if system_1.pythons: - self._python_validator.validate(system_1.pythons, system_2.pythons) - return diff --git a/src/importspy/validators/variable_validator.py b/src/importspy/validators/variable_validator.py deleted file mode 100644 index a3fdcd5..0000000 --- a/src/importspy/validators/variable_validator.py +++ /dev/null @@ -1,93 +0,0 @@ -from ..log_manager import LogManager -from ..models import Variable -from ..constants import Constants -from ..errors import Errors -from typing import List - -class VariableValidator: - - def __init__(self): - self.logger = LogManager().get_logger(self.__class__.__name__) - - def validate( - self, - variables_1: List[Variable], - variables_2: List[Variable], - ): - self.logger.debug(f"Type of variables_1: {type(variables_1)}") - self.logger.debug( - Constants.LOG_MESSAGE_TEMPLATE.format( - operation="Variable validating", - status="Starting", - details=f"Expected Variables: {variables_1} ; Current Variables: {variables_2}" - ) - ) - - if not variables_1: - self.logger.debug( - Constants.LOG_MESSAGE_TEMPLATE.format( - operation="Check if variables_1 is not none", - status="Finished", - details=f"No expected Variables; variables_1: {variables_1}" - ) - ) - return - - if not variables_2: - self.logger.debug( - Constants.LOG_MESSAGE_TEMPLATE.format( - operation="Checking variables_2 when variables_1 is missing", - status="Finished", - details=f"No actual Variables found; variables_2: {variables_2}" - ) - ) - raise ValueError(Errors.ELEMENT_MISSING.format(variables_1)) - - for vars_1 in variables_1: - self.logger.debug( - Constants.LOG_MESSAGE_TEMPLATE.format( - operation="Variable validating", - status="Progress", - details=f"Current vars_1: {vars_1}" - ) - ) - if vars_1.name not in {var.name for var in variables_2}: - self.logger.debug(Constants.LOG_MESSAGE_TEMPLATE.format( - operation="Checking if vars_1 is in variables_2", - status="Finished", - details=Errors.VARIABLE_MISSING.format( - f"{vars_1.name}={vars_1.value}" - ) - )) - raise ValueError( - Errors.VARIABLE_MISSING.format( - f"{vars_1.name}={vars_1.value}" - ) - ) - - for vars_1 in variables_1: - vars_2 = next((var for var in variables_2 if var.name == vars_1.name), None) - if not vars_2: - raise ValueError(Errors.ELEMENT_MISSING.format(variables_1)) - - if vars_1.annotation and vars_1.annotation != vars_2.annotation: - raise ValueError( - Errors.VARIABLE_MISMATCH.format( - Constants.ANNOTATION, - vars_1.name, - vars_1.annotation, - vars_2.annotation - ) - ) - - if vars_1.value != vars_2.value: - raise ValueError( - Errors.VARIABLE_MISMATCH.format( - Constants.VALUE, - vars_1.name, - vars_1.value, - vars_2.value - ) - ) - - return True \ No newline at end of file diff --git a/src/importspy/violation_systems.py b/src/importspy/violation_systems.py new file mode 100644 index 0000000..460e010 --- /dev/null +++ b/src/importspy/violation_systems.py @@ -0,0 +1,200 @@ +""" +This module defines the hierarchy of contract violation classes used by ImportSpy. + +Each violation type corresponds to a validation context (e.g., environment, runtime, module structure), +and provides structured, human-readable error messages when the importing module does not meet the +contract’s requirements. + +The base interface `ContractViolation` defines the common error interface, while specialized classes +like `VariableContractViolation` or `RuntimeContractViolation` define formatting logic for each scope. + +Violations carry a dynamic `Bundle` object, which collects contextual metadata needed for +formatting error messages and debugging failed imports. +""" + +from abc import ABC, abstractmethod +from collections.abc import MutableMapping +from dataclasses import dataclass, field +from typing import Optional, Any, Iterator +from .constants import Errors + + +class ContractViolation(ABC): + """ + Abstract base interface for all contract violations. + + Defines the core methods for rendering structured error messages, + including context resolution and label generation. + + Properties: + ----------- + - `context`: Validation context (e.g., environment, class, runtime) + - `label(spec)`: Retrieves the field name or reference used in error text. + - `missing_error_handler(spec)`: Formats error when required entity is missing. + - `mismatch_error_handler(expected, actual, spec)`: Formats error when values differ. + - `invalid_error_handler(allowed, found, spec)`: Formats error when a value is invalid. + """ + + @property + @abstractmethod + def context(self) -> str: + pass + + @abstractmethod + def label(self, spec: str) -> str: + pass + + @abstractmethod + def missing_error_handler(self, spec: str) -> str: + pass + + @abstractmethod + def mismatch_error_handler(self, expected: Any, actual: Any, spec: str) -> str: + pass + + @abstractmethod + def invalid_error_handler(self, allowed: Any, found: Any, spec: str) -> str: + pass + + +class BaseContractViolation(ContractViolation): + """ + Base implementation of a contract violation. + + Includes default implementations of error formatting methods. + """ + + def __init__(self, context: str, bundle: 'Bundle'): + self._context = context + self.bundle = bundle + super().__init__() + + @property + def context(self) -> str: + return self._context + + def missing_error_handler(self, spec: str) -> str: + return ( + f"{Errors.CONTEXT_INTRO[self.context]}: " + f"{Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.MISSING][spec][Errors.TEMPLATE_KEY].format(label=self.label(spec))} - " + f"{Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.MISSING][spec][Errors.SOLUTION_KEY].capitalize()}" + ) + + def mismatch_error_handler(self, expected: Any, actual: Any, spec: str) -> str: + return ( + f"{Errors.CONTEXT_INTRO[self.context]}: " + f"{Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.MISMATCH][spec][Errors.TEMPLATE_KEY].format(label=self.label(spec), expected=expected, actual=actual)} - " + f"{Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.MISMATCH][spec][Errors.SOLUTION_KEY].capitalize()}" + ) + + def invalid_error_handler(self, allowed: Any, found: Any, spec: str) -> str: + return ( + f"{Errors.CONTEXT_INTRO[self.context]}: " + f"{Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.INVALID][spec][Errors.TEMPLATE_KEY].format(label=self.label(spec), expected=allowed, actual=found)} - " + f"{Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.INVALID][spec][Errors.SOLUTION_KEY].capitalize()}" + ) + + +class VariableContractViolation(BaseContractViolation): + """ + Contract violation handler for variables (module, class, environment, etc.). + + Includes scope information to distinguish between types of variables. + """ + + def __init__(self, scope: str, context: str, bundle: 'Bundle'): + super().__init__(context, bundle) + self.scope = scope + + def label(self, spec: str) -> str: + return Errors.VARIABLES_LABEL_TEMPLATE[self.scope][spec][self.context].format(**self.bundle) + + +class FunctionContractViolation(BaseContractViolation): + """ + Contract violation handler for function signature mismatches. + """ + + def __init__(self, context: str, bundle: 'Bundle'): + super().__init__(context, bundle) + + def label(self, spec: str) -> str: + return Errors.FUNCTIONS_LABEL_TEMPLATE[spec][self.context].format(**self.bundle) + + +class RuntimeContractViolation(BaseContractViolation): + """ + Contract violation handler for runtime architecture mismatches. + """ + + def __init__(self, context: str, bundle: 'Bundle'): + super().__init__(context, bundle) + + def label(self, spec: str) -> str: + return Errors.RUNTIME_LABEL_TEMPLATE[spec].format(**self.bundle) + + +class SystemContractViolation(BaseContractViolation): + """ + Contract violation handler for system-level mismatches (OS, environment variables). + """ + + def __init__(self, context: str, bundle: 'Bundle'): + super().__init__(context, bundle) + + def label(self, spec: str) -> str: + return Errors.SYSTEM_LABEL_TEMPLATE[spec].format(**self.bundle) + + +class PythonContractViolation(BaseContractViolation): + """ + Contract violation handler for Python version and interpreter mismatches. + """ + + def __init__(self, context: str, bundle: 'Bundle'): + super().__init__(context, bundle) + + def label(self, spec: str) -> str: + return Errors.PYTHON_LABEL_TEMPLATE[spec].format(**self.bundle) + + +class ModuleContractViolation(BaseContractViolation): + """ + Contract violation handler for module-level mismatches (filename, version, structure). + """ + + def __init__(self, context: str, bundle: 'Bundle'): + super().__init__(context, bundle) + + def label(self, spec: str) -> str: + return Errors.MODULE_LABEL_TEMPLATE[spec][self.context].format(**self.bundle) + + +@dataclass +class Bundle(MutableMapping): + """ + Shared mutable state passed to all violation handlers. + + The bundle is a dynamic container used to inject contextual values + (like module name, attribute name, or class name) into error templates. + """ + + state: Optional[dict[str, Any]] = field(default_factory=dict) + + def __getitem__(self, key): + return self.state[key] + + def __setitem__(self, key, value): + self.state[key] = value + + def __delitem__(self, key): + del self.state[key] + + def __iter__(self) -> Iterator: + return iter(self.state) + + def __len__(self) -> int: + return len(self.state) + + def __repr__(self): + return repr(self.state) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..1030f84 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,30 @@ +import pytest + +from importspy.violation_systems import Bundle + +from importspy.constants import ( + Errors, + Contexts +) + +@pytest.fixture +def modulebundle() -> Bundle: + bundle = Bundle() + bundle[Errors.KEY_FILE_NAME] = "testmodule.py" + bundle[Errors.KEY_MODULE_VERSION] = "0.1.0" + return bundle + +@pytest.fixture +def classbundle(modulebundle) -> Bundle: + modulebundle[Errors.KEY_CLASS_NAME] = "TestClass" + return modulebundle + +@pytest.fixture +def functionbundle(modulebundle) -> Bundle: + modulebundle[Errors.FUNCTIONS_DINAMIC_PAYLOAD[Errors.ENTITY_MESSAGES][Contexts.MODULE_CONTEXT]] = "test_function" + return modulebundle + +@pytest.fixture +def methodbundle(classbundle) -> Bundle: + classbundle[Errors.FUNCTIONS_DINAMIC_PAYLOAD[Errors.ENTITY_MESSAGES][Contexts.CLASS_CONTEXT]] = "test_method" + return classbundle \ No newline at end of file diff --git a/tests/validators/test_argument_validator.py b/tests/validators/test_argument_validator.py index 37e0681..759396a 100644 --- a/tests/validators/test_argument_validator.py +++ b/tests/validators/test_argument_validator.py @@ -3,17 +3,25 @@ Argument ) from typing import List -from importspy.validators.argument_validator import ArgumentValidator -from importspy.errors import Errors +from importspy.validators import VariableValidator import re -from importspy.constants import Constants + +from importspy.constants import ( + Errors, + Contexts +) + +from importspy.violation_systems import ( + VariableContractViolation, + Bundle +) class TestArgumentValidator: - validator = ArgumentValidator() + validator = VariableValidator() @pytest.fixture - def data_1(self): + def data_1(self) -> Argument: return [Argument( name="arg1", annotation="int", @@ -21,7 +29,7 @@ def data_1(self): )] @pytest.fixture - def data_2(self): + def data_2(self) -> Argument: return [Argument( name="arg2", annotation="str", @@ -29,7 +37,7 @@ def data_2(self): )] @pytest.fixture - def data_3(self): + def data_3(self) -> Argument: return [Argument( name="arg1", annotation="int", @@ -37,13 +45,17 @@ def data_3(self): )] @pytest.fixture - def data_4(self): + def data_4(self) -> Argument: return [Argument( name="arg1", annotation="int", value=10 )] + @pytest.fixture + def contract_violation(self, functionbundle:Bundle) -> VariableContractViolation: + return VariableContractViolation(Errors.SCOPE_ARGUMENT, Contexts.MODULE_CONTEXT, functionbundle) + @pytest.fixture def argument_value_setter(self, data_3: List[Argument]): data_3[0].value = 10 @@ -52,60 +64,83 @@ def argument_value_setter(self, data_3: List[Argument]): def argument_annotation_setter(self, data_3: List[Argument]): data_3[0].annotation = "str" - def test_argument_match(self, data_1: List[Argument], data_4: List[Argument]): - assert self.validator.validate(data_1, data_4, "function_name") + def test_argument_match(self, data_1: List[Argument], data_4: List[Argument], contract_violation): + assert data_1 + assert data_4 + assert self.validator.validate(data_1, data_4, contract_violation) is None @pytest.mark.usefixtures("argument_value_setter") - def test_argument_match_1(self, data_1: List[Argument], data_3: List[Argument]): - assert self.validator.validate(data_1, data_3, "function_name") - - def test_argument_mismatch(self, data_2: List[Argument]): - assert self.validator.validate(None, data_2, "function_name") is None - - def test_argument_mismatch_1(self, data_3: List[Argument], data_4: List[Argument]): + def test_argument_match_1(self, data_1: List[Argument], data_3: List[Argument], contract_violation): + assert data_1 + assert data_3 + assert self.validator.validate(data_1, data_3, contract_violation) is None + + def test_argument_mismatch_no_data(self, data_2: List[Argument], contract_violation): + assert self.validator.validate(None, data_2, contract_violation) is None + + def test_argument_mismatch_1(self, data_3: List[Argument], data_4: List[Argument], contract_violation: VariableContractViolation): + mock_bundle = Bundle() + mock_bundle[Errors.KEY_ARGUMENT_NAME] = "arg1" + mock_bundle[Errors.KEY_FUNCTION_NAME] = "test_function" + mock_contract_violation = VariableContractViolation(Errors.SCOPE_ARGUMENT, Contexts.MODULE_CONTEXT, mock_bundle) with pytest.raises(ValueError, match=re.escape( - Errors.ARGUMENT_MISMATCH.format( - Constants.VALUE, - data_4[0].name, + mock_contract_violation.mismatch_error_handler( data_4[0].value, - data_3[0].value + data_3[0].value, + Errors.ENTITY_MESSAGES, ) )): - self.validator.validate(data_4, data_3, "function_name") + self.validator.validate(data_4, data_3, contract_violation) - def test_argument_mismatch_2(self): - assert self.validator.validate(None, None, "function_name") is None + def test_argument_mismatch_2(self, contract_violation): + assert self.validator.validate(None, None, contract_violation) is None - def test_argument_mismatch_3(self, data_2: List[Argument]): - assert self.validator.validate(None, data_2, "function_name") is None + def test_argument_mismatch_3(self, data_2: List[Argument], contract_violation): + assert self.validator.validate(None, data_2, contract_violation) is None @pytest.mark.usefixtures("argument_value_setter") @pytest.mark.usefixtures("argument_annotation_setter") - def test_argument_mismatch_5(self, data_1: List[Argument], data_3: List[Argument]): - arg_1 = data_3[0] - arg_2 = data_1[0] - with pytest.raises( - ValueError, - match=re.escape(Errors.ARGUMENT_MISMATCH.format(Constants.ANNOTATION, arg_1.name, arg_1.annotation, arg_2.annotation)) - ): - self.validator.validate(data_3, data_1, "function_name") - - def test_argument_mismatch_6(self, data_1: List[Argument], data_3: List[Argument]): - arg_1 = data_3[0] - arg_2 = data_1[0] - with pytest.raises( - ValueError, - match=re.escape(Errors.ARGUMENT_MISMATCH.format(Constants.VALUE, arg_1.name, arg_1.value, arg_2.value)) - ): - self.validator.validate(data_3, data_1, "function_name") + def test_argument_mismatch_5(self, data_1: List[Argument], data_3: List[Argument], contract_violation): + mock_bundle = Bundle() + mock_bundle[Errors.KEY_ARGUMENT_NAME] = "arg1" + mock_bundle[Errors.KEY_FUNCTION_NAME] = "test_function" + mock_contract_violation = VariableContractViolation(Errors.SCOPE_ARGUMENT, Contexts.MODULE_CONTEXT, mock_bundle) + with pytest.raises(ValueError, + match=re.escape( + mock_contract_violation.mismatch_error_handler( + data_3[0].annotation, + data_1[0].annotation, + Errors.ENTITY_MESSAGES, + ) + )): + self.validator.validate(data_3, data_1, contract_violation) + + def test_argument_mismatch_6(self, data_1: List[Argument], data_3: List[Argument], contract_violation): + mock_bundle = Bundle() + mock_bundle[Errors.KEY_ARGUMENT_NAME] = "arg1" + mock_bundle[Errors.KEY_FUNCTION_NAME] = "test_function" + mock_contract_violation = VariableContractViolation(Errors.SCOPE_ARGUMENT, Contexts.MODULE_CONTEXT, mock_bundle) + with pytest.raises(ValueError, + match=re.escape( + mock_contract_violation.mismatch_error_handler( + data_3[0].value, + data_1[0].value, + Errors.ENTITY_MESSAGES, + ) + )): + self.validator.validate(data_3, data_1, contract_violation) - def test_argument_mismatch_7(self, data_1: List[Argument]): - with pytest.raises( - ValueError, - match=re.escape( - Errors.ELEMENT_MISSING.format(data_1) - ) - ): - self.validator.validate(data_1, None, "function_name") + def test_argument_mismatch_7(self, data_1: List[Argument], contract_violation): + mock_bundle = Bundle() + mock_bundle[Errors.KEY_ARGUMENTS_1] = data_1 + mock_bundle[Errors.KEY_FUNCTION_NAME] = "test_function" + mock_contract_violation = VariableContractViolation(Errors.SCOPE_ARGUMENT, Contexts.MODULE_CONTEXT, mock_bundle) + with pytest.raises(ValueError, + match=re.escape( + mock_contract_violation.missing_error_handler( + Errors.COLLECTIONS_MESSAGES, + ) + )): + self.validator.validate(data_1, None, contract_violation) diff --git a/tests/validators/test_attribute_validator.py b/tests/validators/test_attribute_validator.py index e8b5770..fa72a94 100644 --- a/tests/validators/test_attribute_validator.py +++ b/tests/validators/test_attribute_validator.py @@ -2,16 +2,29 @@ from importspy.models import ( Attribute ) -from importspy.config import Config + +from importspy.validators import VariableValidator + from typing import List -from importspy.validators.attribute_validator import AttributeValidator -from importspy.errors import Errors + import re -from importspy.constants import Constants + +from importspy.constants import ( + Errors, + Contexts +) + +from importspy.violation_systems import ( + VariableContractViolation, + Bundle +) + +from importspy.config import Config + class TestAttributeValidator: - validator = AttributeValidator() + validator = VariableValidator() @pytest.fixture def data_1(self): @@ -45,36 +58,57 @@ def data_4(self): value=4 )] + @pytest.fixture + def class_type_bundle(self, classbundle) -> Bundle: + classbundle[Errors.KEY_ATTRIBUTE_TYPE] = Config.CLASS_TYPE + return classbundle + + @pytest.fixture + def instance_type_bundle(self, classbundle) -> Bundle: + classbundle[Errors.KEY_ATTRIBUTE_TYPE] = Config.INSTANCE_TYPE + return classbundle + + @pytest.fixture + def class_type_contract(self, class_type_bundle): + return VariableContractViolation(Errors.SCOPE_VARIABLE, Contexts.CLASS_CONTEXT, class_type_bundle) + + @pytest.fixture + def instance_type_contract(self, instance_type_bundle): + return VariableContractViolation(Errors.SCOPE_VARIABLE, Contexts.CLASS_CONTEXT, instance_type_bundle) + + @pytest.fixture def attribute_value_setter(self, data_3:Attribute): data_3[0].value = "value" - def test_attribute_match(self, data_1:List[Attribute], data_4:List[Attribute]): - assert self.validator.validate(data_1, data_4, "classname") + def test_attribute_match(self, data_1:List[Attribute], data_4:List[Attribute], class_type_contract): + assert self.validator.validate(data_1, data_4, class_type_contract) is None @pytest.mark.usefixtures("attribute_value_setter") - def test_attribute_match_1(self, data_2:List[Attribute], data_3:List[Attribute]): - assert self.validator.validate(data_2, data_3, "classname") + def test_attribute_match_1(self, data_2:List[Attribute], data_3:List[Attribute], instance_type_contract): + assert self.validator.validate(data_2, data_3, instance_type_contract) is None - def test_attribute_mismatch(self, data_2:List[Attribute]): - assert self.validator.validate(None, data_2, "classname") is None + def test_attribute_mismatch(self, data_2:List[Attribute], class_type_contract): + assert self.validator.validate(None, data_2, class_type_contract) is None - def test_attribute_mismatch_1(self, data_3, data_4): + def test_attribute_mismatch_1(self, data_3, data_4, class_type_contract): + mock_bundle = Bundle() + mock_bundle[Errors.KEY_ATTRIBUTE_NAME] = "class_attribute" + mock_bundle[Errors.KEY_ATTRIBUTE_TYPE] = "class" + mock_bundle[Errors.KEY_CLASS_NAME] = "TestClass" + mock_contract_violation = VariableContractViolation(Errors.SCOPE_VARIABLE, Contexts.CLASS_CONTEXT, mock_bundle) with pytest.raises(ValueError, match=re.escape( - Errors.CLASS_ATTRIBUTE_MISSING.format( - Config.CLASS_TYPE, - f"{data_4[0].name}={data_4[0].value}", - "classname" - ) - )): - self.validator.validate(data_4, data_3, "classname") + mock_contract_violation.missing_error_handler( + Errors.ENTITY_MESSAGES + ))): + self.validator.validate(data_4, data_3, class_type_contract) - def test_attribute_mismatch_2(self): - assert self.validator.validate(None, None, "classname") is None + def test_attribute_mismatch_2(self, instance_type_contract): + assert self.validator.validate(None, None, instance_type_contract) is None - def test_attribute_mismatch_3(self, data_2:List[Attribute]): - assert self.validator.validate(None, data_2, "classname") is None + def test_attribute_mismatch_3(self, data_2:List[Attribute], instance_type_contract): + assert self.validator.validate(None, data_2, instance_type_contract) is None @pytest.fixture def attribute_annotation_setter(self, data_3:Attribute): @@ -82,23 +116,34 @@ def attribute_annotation_setter(self, data_3:Attribute): @pytest.mark.usefixtures("attribute_value_setter") @pytest.mark.usefixtures("attribute_annotation_setter") - def test_attribute_match_3(self, data_2:List[Attribute], data_3:List[Attribute]): - assert self.validator.validate(data_2, data_3, "classname") + def test_attribute_match_3(self, data_2:List[Attribute], data_3:List[Attribute], instance_type_contract): + assert self.validator.validate(data_2, data_3, instance_type_contract) is None @pytest.mark.usefixtures("attribute_value_setter") @pytest.mark.usefixtures("attribute_annotation_setter") - def test_attribute_mismatch_5(self, data_2:List[Attribute], data_3:List[Attribute]): - attr_1 = data_3[0] - attr_2 = data_2[0] + def test_attribute_mismatch_5(self, data_2:List[Attribute], data_3:List[Attribute], instance_type_contract): + mock_bundle = Bundle() + mock_bundle[Errors.KEY_ATTRIBUTE_NAME] = "instance_attribute" + mock_bundle[Errors.KEY_ATTRIBUTE_TYPE] = "instance" + mock_bundle[Errors.KEY_CLASS_NAME] = "TestClass" + mock_contract_violation = VariableContractViolation(Errors.SCOPE_VARIABLE, Contexts.CLASS_CONTEXT, mock_bundle) with pytest.raises( ValueError, - match=re.escape(Errors.CLASS_ATTRIBUTE_MISMATCH.format(Constants.ANNOTATION, attr_1.type, attr_1.name, attr_1.annotation, attr_2.annotation)) + match=re.escape( + mock_contract_violation.mismatch_error_handler(data_3[0].annotation, data_2[0].annotation, Errors.ENTITY_MESSAGES) + ) ): - self.validator.validate(data_3, data_2, "classname") + self.validator.validate(data_3, data_2, instance_type_contract) - def test_attribute_mismatch_6(self, data_3:List[Attribute]): + def test_attribute_mismatch_6(self, data_3:List[Attribute], instance_type_contract): + mock_bundle = Bundle() + mock_bundle[Errors.KEY_ATTRIBUTES_1] = data_3 + mock_bundle[Errors.KEY_CLASS_NAME] = "TestClass" + mock_contract_violation = VariableContractViolation(Errors.SCOPE_VARIABLE, Contexts.CLASS_CONTEXT, mock_bundle) with pytest.raises( ValueError, - match=re.escape(Errors.ELEMENT_MISSING.format(data_3)) + match=re.escape( + mock_contract_violation.missing_error_handler(Errors.COLLECTIONS_MESSAGES) + ) ): - self.validator.validate(data_3, None, "classname") \ No newline at end of file + self.validator.validate(data_3, None, instance_type_contract) \ No newline at end of file diff --git a/tests/validators/test_function_validator.py b/tests/validators/test_function_validator.py index 77f7180..d8efe5a 100644 --- a/tests/validators/test_function_validator.py +++ b/tests/validators/test_function_validator.py @@ -1,10 +1,18 @@ import pytest -from importspy.models import ( - Function -) +from importspy.models import Function from typing import List -from importspy.validators.function_validator import FunctionValidator -from importspy.errors import Errors +from importspy.validators import FunctionValidator + +from importspy.violation_systems import ( + FunctionContractViolation, + Bundle +) + +from importspy.constants import ( + Errors, + Contexts +) + import re class TestFunctionValidator: @@ -38,49 +46,56 @@ def data_4(self): return_annotation="str" )] + @pytest.fixture + def function_contract(self, functionbundle:Bundle): + return FunctionContractViolation(Contexts.MODULE_CONTEXT, functionbundle) + @pytest.fixture def function_return_annotation_setter(self, data_3:Function): data_3[0].return_annotation = "str" - def test_function_match(self, data_1:List[Function], data_4:List[Function]): - assert self.validator.validate(data_1, data_4, "classname") + def test_function_match(self, data_1:List[Function], data_4:List[Function], function_contract: FunctionContractViolation): + assert self.validator.validate(data_1, data_4, function_contract) is None - def test_function_mismatch(self, data_2:List[Function]): - assert self.validator.validate(None, data_2, "classname") is None + def test_function_mismatch(self, data_2:List[Function], function_contract): + assert self.validator.validate(None, data_2, function_contract) is None @pytest.mark.usefixtures("function_return_annotation_setter") - def test_function_mismatch_1(self, data_2:List[Function], data_3:List[Function]): + def test_function_mismatch_1(self, data_2:List[Function], data_3:List[Function], function_contract: FunctionContractViolation): with pytest.raises( ValueError, match=re.escape( - Errors.FUNCTIONS_MISSING.format("function", data_2[0].name) + Errors.FUNCTIONS_MISSING.format("function", data_2[0].name, function_contract) ) ): self.validator.validate(data_2, data_3) - def test_function_mismatch_1(self, data_3:List[Function], data_4:List[Function]): + def test_function_mismatch_1(self, data_3:List[Function], data_4:List[Function], function_contract:FunctionContractViolation): + mock_bundle = Bundle() + mock_bundle[Errors.KEY_FILE_NAME] = "testmodule.py" + mock_bundle[Errors.KEY_FUNCTION_NAME] = "function" + mock_contract_violation = FunctionContractViolation(Contexts.MODULE_CONTEXT, mock_bundle) with pytest.raises( ValueError, match=re.escape( - Errors.FUNCTION_RETURN_ANNOTATION_MISMATCH.format( - "method in class classname", - data_4[0].name, - data_4[0].return_annotation, - data_3[0].return_annotation + mock_contract_violation.mismatch_error_handler(data_4[0].return_annotation, data_3[0].return_annotation, Errors.ENTITY_MESSAGES) ) - ) - ): - self.validator.validate(data_4, data_3, "classname") + ): + self.validator.validate(data_4, data_3, function_contract) - def test_function_mismatch_2(self): - assert self.validator.validate(None, None, "classname") is None + def test_function_mismatch_2(self, function_contract:FunctionContractViolation): + assert self.validator.validate(None, None, function_contract) is None - def test_function_mismatch_3(self, data_2:List[Function]): - assert self.validator.validate(None, data_2, "classname") is None + def test_function_mismatch_3(self, data_2:List[Function], function_contract:FunctionContractViolation): + assert self.validator.validate(None, data_2, function_contract) is None - def test_function_mismatch_5(self, data_3:List[Function]): + def test_function_mismatch_5(self, data_3:List[Function], function_contract:FunctionContractViolation): + mock_bundle = Bundle() + mock_bundle[Errors.KEY_FILE_NAME] = "testmodule.py" + mock_bundle[Errors.KEY_FUNCTIONS_1] = data_3 + mock_contract_violation = FunctionContractViolation(Contexts.MODULE_CONTEXT, mock_bundle) with pytest.raises( ValueError, - match=re.escape(Errors.ELEMENT_MISSING.format(data_3)) + match=re.escape(mock_contract_violation.missing_error_handler(Errors.COLLECTIONS_MESSAGES)) ): - self.validator.validate(data_3, None, "classname") \ No newline at end of file + self.validator.validate(data_3, None, function_contract) \ No newline at end of file diff --git a/tests/validators/test_method_validator.py b/tests/validators/test_method_validator.py new file mode 100644 index 0000000..e2b484d --- /dev/null +++ b/tests/validators/test_method_validator.py @@ -0,0 +1,103 @@ +import pytest +from importspy.models import Function +from typing import List +from importspy.validators import FunctionValidator + +from importspy.violation_systems import ( + FunctionContractViolation, + Bundle +) + +from importspy.constants import ( + Errors, + Contexts +) + +import re + +class TestFunctionValidator: + + validator = FunctionValidator() + + @pytest.fixture + def data_1(self): + return [Function( + name="method", + )] + + @pytest.fixture + def data_2(self): + return [Function( + name="getname", + return_annotation="str" + )] + + @pytest.fixture + def data_3(self): + return [Function( + name="method", + return_annotation="int" + )] + + @pytest.fixture + def data_4(self): + return [Function( + name="method", + return_annotation="str" + )] + + @pytest.fixture + def function_contract(self, methodbundle:Bundle): + return FunctionContractViolation(Contexts.CLASS_CONTEXT, methodbundle) + + @pytest.fixture + def function_return_annotation_setter(self, data_3:Function): + data_3[0].return_annotation = "str" + + def test_function_match(self, data_1:List[Function], data_4:List[Function], function_contract: FunctionContractViolation): + assert self.validator.validate(data_1, data_4, function_contract) is None + + def test_function_mismatch(self, data_2:List[Function], function_contract): + assert self.validator.validate(None, data_2, function_contract) is None + + @pytest.mark.usefixtures("function_return_annotation_setter") + def test_function_mismatch_1(self, data_2:List[Function], data_3:List[Function], function_contract: FunctionContractViolation): + with pytest.raises( + ValueError, + match=re.escape( + Errors.FUNCTIONS_MISSING.format("function", data_2[0].name, function_contract) + ) + ): + self.validator.validate(data_2, data_3) + + def test_function_mismatch_1(self, data_3:List[Function], data_4:List[Function], function_contract:FunctionContractViolation): + mock_bundle = Bundle() + mock_bundle[Errors.KEY_FILE_NAME] = "testmodule.py" + mock_bundle[Errors.KEY_CLASS_NAME] = "TestClass" + mock_bundle[Errors.KEY_METHOD_NAME] = "method" + mock_contract_violation = FunctionContractViolation(Contexts.CLASS_CONTEXT, mock_bundle) + with pytest.raises( + ValueError, + match=re.escape( + mock_contract_violation.mismatch_error_handler(data_4[0].return_annotation, data_3[0].return_annotation, Errors.ENTITY_MESSAGES) + ) + ): + self.validator.validate(data_4, data_3, function_contract) + + def test_function_mismatch_2(self, function_contract:FunctionContractViolation): + assert self.validator.validate(None, None, function_contract) is None + + def test_function_mismatch_3(self, data_2:List[Function], function_contract:FunctionContractViolation): + assert self.validator.validate(None, data_2, function_contract) is None + + def test_function_mismatch_5(self, data_3:List[Function], function_contract:FunctionContractViolation): + mock_bundle = Bundle() + mock_bundle[Errors.KEY_FILE_NAME] = "testmodule.py" + mock_bundle[Errors.KEY_CLASS_NAME] = "TestClass" + mock_bundle[Errors.KEY_METHODS_1] = data_3 + mock_contract_violation = FunctionContractViolation(Contexts.CLASS_CONTEXT, mock_bundle) + with pytest.raises( + ValueError, + match=re.escape(mock_contract_violation.missing_error_handler(Errors.COLLECTIONS_MESSAGES)) + ): + self.validator.validate(data_3, None, function_contract) \ No newline at end of file diff --git a/tests/validators/test_module_validator.py b/tests/validators/test_module_validator.py index b35ed04..8db13dc 100644 --- a/tests/validators/test_module_validator.py +++ b/tests/validators/test_module_validator.py @@ -3,10 +3,15 @@ Module, Variable ) -from importspy.validators.module_validator import ModuleValidator -from importspy.errors import Errors +from importspy.validators import ModuleValidator +from importspy.violation_systems import ( + Bundle, + ModuleContractViolation +) +from importspy.constants import Errors import re from typing import List +from importspy.constants import Contexts class TestModuleValidator: @@ -24,6 +29,17 @@ def data_2(self): filename="package.py", version="0.1.0" )] + + @pytest.fixture + def data_3(self): + return [Module( + filename="package.py", + version="0.2.0" + )] + + @pytest.fixture + def module_contract(self, methodbundle:Bundle) -> ModuleContractViolation: + return ModuleContractViolation(Contexts.RUNTIME_CONTEXT, methodbundle) @pytest.fixture def filename_setter(self, data_1:List[Module]): @@ -66,43 +82,35 @@ def variables_msg_setter(self, data_3:List[Module]): @pytest.mark.usefixtures("filename_setter") @pytest.mark.usefixtures("version_unsetter") - def test_module_match(self, data_1:List[Module], data_2:List[Module]): - assert self.validator.validate(data_1, data_2[0]) is None + def test_module_match(self, data_1:List[Module], data_2:List[Module], module_contract:ModuleContractViolation): + assert self.validator.validate(data_1, data_2[0], module_contract) is None @pytest.mark.usefixtures("filename_setter") @pytest.mark.usefixtures("variables_setter") @pytest.mark.usefixtures("variables_msg_setter") - def test_module_match_1(self, data_2:List[Module], data_3:List[Module]): - assert self.validator.validate(data_2, data_3[0]) is None + def test_module_match_1(self, data_2:List[Module], data_3:List[Module], module_contract:ModuleContractViolation): + assert self.validator.validate(data_2, data_3[0], module_contract) is None - def test_module_match_2(self, data_2:List[Module], data_3:List[Module]): - assert self.validator.validate(data_2, data_3[0]) is None + def test_module_match_2(self, data_2:List[Module], data_3:List[Module], module_contract:ModuleContractViolation): + assert self.validator.validate(data_2, data_3[0], module_contract) is None - def test_module_mismatch(self, data_2:List[Module]): - assert self.validator.validate(None, data_2[0]) is None - - def test_module_mismatch_2(self, data_2:List[Module], data_3:List[Module]): - with pytest.raises( - ValueError, - match=re.escape(Errors.ELEMENT_MISSING.format(data_3[0].variables)) - ): - self.validator.validate(data_3, data_2[0]) + def test_module_mismatch(self, data_2:List[Module], module_contract:ModuleContractViolation): + assert self.validator.validate(None, data_2[0], module_contract) is None @pytest.mark.usefixtures("version_unsetter") - def test_module_mismatch_3(self, data_1:List[Module], data_2:List[Module]): + def test_module_mismatch_1(self, data_1:List[Module], data_2:List[Module], module_contract:ModuleContractViolation): with pytest.raises(ValueError, match=re.escape( - Errors.FILENAME_MISMATCH.format(data_1[0].filename, - data_2[0].filename) - ) + module_contract.mismatch_error_handler(data_1[0].filename, data_2[0].filename, Errors.ENTITY_MESSAGES) + ) ): - self.validator.validate(data_1, data_2[0]) - - def test_module_mismatch_4(self, data_1:List[Module], data_2:List[Module]): + self.validator.validate(data_1, data_2[0], module_contract) + + @pytest.mark.usefixtures("version_unsetter") + def test_module_mismatch_1(self, data_3:List[Module], data_2:List[Module], module_contract:ModuleContractViolation): with pytest.raises(ValueError, match=re.escape( - Errors.FILENAME_MISMATCH.format(data_1[0].filename, - data_2[0].filename) - ) + module_contract.mismatch_error_handler(data_3[0].version, data_2[0].version, Errors.ENTITY_MESSAGES) + ) ): - self.validator.validate(data_1, data_2[0]) \ No newline at end of file + self.validator.validate(data_3, data_2[0], module_contract) \ No newline at end of file diff --git a/tests/validators/test_python_validator.py b/tests/validators/test_python_validator.py index 410f788..8184f1c 100644 --- a/tests/validators/test_python_validator.py +++ b/tests/validators/test_python_validator.py @@ -3,11 +3,20 @@ Python ) from importspy.config import Config -from importspy.errors import Errors -from importspy.validators.python_validator import PythonValidator +from importspy.constants import ( + Errors, + Contexts +) + +from importspy.validators import PythonValidator from typing import List import re +from importspy.violation_systems import ( + PythonContractViolation, + Bundle +) + class TestPythonValidator: @@ -40,6 +49,20 @@ def data_4(self): modules=[] )] + @pytest.fixture + def pythonbundle(self) -> Bundle: + bundle = Bundle() + bundle[Errors.KEY_PYTHON_1] = Python( + version=Config.PYTHON_VERSION_3_13, + interpreter=Config.INTERPRETER_IRON_PYTHON, + modules=[] + ) + return bundle + + @pytest.fixture + def python_contract(self, pythonbundle: Bundle) -> PythonContractViolation: + return PythonContractViolation(context=Contexts.RUNTIME_CONTEXT, bundle=pythonbundle) + @pytest.fixture def python_version_setter(self, data_2:List[Python]): data_2[0].version = "12.0.1" @@ -48,34 +71,61 @@ def python_version_setter(self, data_2:List[Python]): def python_interpreter_setter(self, data_3:List[Python]): data_3[0].interpreter = Config.INTERPRETER_GRAALPYTHON - @pytest.mark.usefixtures("python_version_setter") - def test_python_match(self, data_1:List[Python], data_2:List[Python]): - assert self.validator.validate(data_1, data_2) is None - - def test_python_match_1(self, data_3:List[Python], data_4:List[Python]): - assert self.validator.validate(data_3, data_4) is None + def test_python_match(self, data_3:List[Python], data_4:List[Python], python_contract): + assert self.validator.validate(data_3, data_4, python_contract) == data_3[0].modules - def test_python_mismatch(self, data_2:List[Python]): - assert self.validator.validate(None, data_2) is None + def test_python_mismatch(self, data_2:List[Python], python_contract): + assert self.validator.validate(None, data_2, python_contract) is None @pytest.mark.usefixtures("python_interpreter_setter") - def test_python_mismatch_1(self, data_3, data_4): - assert self.validator.validate(data_4, data_3) is None + def test_python_mismatch_1(self, data_3, data_4, python_contract): + mock_contract = PythonContractViolation( + Contexts.RUNTIME_CONTEXT, + Bundle( + state= { Errors.KEY_PYTHONS_1 : data_4 } + ) + ) + with pytest.raises(ValueError, + match=re.escape( + mock_contract.missing_error_handler(Errors.COLLECTIONS_MESSAGES) + ) + ): + assert self.validator.validate(data_4, data_3, python_contract) + + def test_python_mismatch_2(self, python_contract): + assert self.validator.validate(None, None, python_contract) is None - def test_python_mismatch_2(self): - assert self.validator.validate(None, None) is None + def test_python_mismatch_3(self, python_contract): + assert self.validator.validate(None, None, python_contract) is None - def test_python_mismatch_3(self): - assert self.validator.validate(None, None) is None + def test_python_mismatch_4(self, data_2:List[Python], python_contract): + assert self.validator.validate(None, data_2, python_contract) is None - def test_python_mismatch_4(self, data_2:List[Python]): - assert self.validator.validate(None, data_2) is None + def test_python_mismatch_5(self, data_3:List[Python], python_contract): + mock_contract = PythonContractViolation( + Contexts.RUNTIME_CONTEXT, + Bundle( + state= { Errors.KEY_PYTHONS_1 : data_3 } + ) + ) + with pytest.raises(ValueError, + match=re.escape( + mock_contract.missing_error_handler(Errors.COLLECTIONS_MESSAGES) + ) + ): + self.validator.validate(data_3, None, python_contract) - def test_python_mismatch_5(self, data_3:List[Python]): - with pytest.raises( - ValueError, - match=re.escape( - Errors.ELEMENT_MISSING.format(data_3) - ) + @pytest.mark.usefixtures("python_version_setter") + def test_python_mismatch_6(self, data_1:List[Python], data_2:List[Python], python_contract:PythonContractViolation): + mock_contract = PythonContractViolation( + Contexts.RUNTIME_CONTEXT, + Bundle( + state= { Errors.KEY_PYTHONS_1 : data_1 } + ) + ) + with pytest.raises(ValueError, + match=re.escape( + mock_contract.missing_error_handler(Errors.COLLECTIONS_MESSAGES) + ) ): - self.validator.validate(data_3, None) \ No newline at end of file + self.validator.validate(data_1, data_2, python_contract) \ No newline at end of file diff --git a/tests/validators/test_runtime_validator.py b/tests/validators/test_runtime_validator.py index 76a1df1..ab647c9 100644 --- a/tests/validators/test_runtime_validator.py +++ b/tests/validators/test_runtime_validator.py @@ -3,11 +3,20 @@ Runtime ) from importspy.config import Config -from importspy.constants import Constants -from importspy.validators.runtime_validator import RuntimeValidator -from importspy.errors import Errors + +from importspy.validators import RuntimeValidator +from importspy.constants import ( + Errors, + Constants, + Contexts +) + import re from typing import List +from importspy.violation_systems import ( + Bundle, + RuntimeContractViolation +) class TestRuntimeValidator: @@ -27,21 +36,33 @@ def data_2(self): systems=[] )] + @pytest.fixture + def runtimebundle(self) -> Bundle: + bundle = Bundle() + return bundle + + @pytest.fixture + def runtime_contract(self, runtimebundle: Bundle) -> RuntimeContractViolation: + return RuntimeContractViolation(context=Contexts.RUNTIME_CONTEXT, bundle=runtimebundle) + @pytest.fixture def arch_arm_setter(self, data_1:List[Runtime]): data_1[0].arch = Config.ARCH_ARM - def test_runtime_arch_match(self, data_1:List[Runtime], data_2:List[Runtime]): - assert self.validator.validate(data_1, data_2) is None - - def test_runtime_arch_invalid(self): - with pytest.raises(ValueError, - match=re.escape(Errors.INVALID_ARCHITECTURE.format("A invalid value", Constants.KNOWN_ARCHITECTURES))): - Runtime( - arch="A invalid value", - systems=[] - ) + def test_runtime_arch_match(self, data_1:List[Runtime], data_2:List[Runtime], runtime_contract): + assert self.validator.validate(data_1, data_2, runtime_contract) == data_1[0] @pytest.mark.usefixtures("arch_arm_setter") - def test_runtime_arch_mismatch(self, data_1:List[Runtime], data_2:List[Runtime]): - assert self.validator.validate(data_1, data_2) is None \ No newline at end of file + def test_runtime_arch_mismatch(self, data_1:List[Runtime], data_2:List[Runtime], runtime_contract): + mock_contract = RuntimeContractViolation( + Contexts.RUNTIME_CONTEXT, + Bundle( + state= { Errors.KEY_RUNTIMES_1 : data_1 } + ) + ) + with pytest.raises(ValueError, + match=re.escape( + mock_contract.missing_error_handler(Errors.COLLECTIONS_MESSAGES) + ) + ): + assert self.validator.validate(data_1, data_2, runtime_contract) is None \ No newline at end of file diff --git a/tests/validators/test_system_validator.py b/tests/validators/test_system_validator.py index feb79f3..fa8effa 100644 --- a/tests/validators/test_system_validator.py +++ b/tests/validators/test_system_validator.py @@ -1,14 +1,27 @@ import pytest from importspy.models import ( - System + System, + Environment, + Variable ) from importspy.config import Config -from importspy.constants import Constants -from importspy.validators.system_validator import SystemValidator -from importspy.errors import Errors + +from importspy.constants import ( + Contexts, + Constants +) + +from importspy.validators import SystemValidator +from importspy.constants import Errors import re from typing import List +from importspy.violation_systems import ( + Bundle, + SystemContractViolation, + VariableContractViolation +) + class TestSystemValidator: validator = SystemValidator() @@ -16,60 +29,97 @@ class TestSystemValidator: @pytest.fixture def data_1(self): return [System( - os=Config.OS_LINUX, - envs={"CI": "true"}, + os=Constants.SupportedOS.OS_LINUX, + environment=Environment( + variables=[Variable( + name="CI", + value="true" + )] + ), pythons=[] )] @pytest.fixture def data_2(self): return [System( - os=Config.OS_LINUX, + os=Constants.SupportedOS.OS_LINUX, pythons=[] )] + @pytest.fixture + def systembundle(self) -> Bundle: + bundle = Bundle() + return bundle + + @pytest.fixture + def variable_contract(self, systembundle) -> VariableContractViolation: + return VariableContractViolation(Errors.SCOPE_VARIABLE, Contexts.ENVIRONMENT_CONTEXT, systembundle) + + @pytest.fixture + def system_contract(self, systembundle) -> SystemContractViolation: + return SystemContractViolation(Contexts.RUNTIME_CONTEXT, systembundle) + @pytest.fixture def os_windows_setter(self, data_1:List[System]): - data_1[0].os = Config.OS_WINDOWS + data_1[0].os = Constants.SupportedOS.OS_WINDOWS @pytest.fixture def envs_setter(self, data_2): - data_2[0].envs = {"CI": "true"} + data_2[0].environment = Environment(variables=[(Variable(name="CI", value="true"))]) @pytest.mark.usefixtures("envs_setter") - def test_system_os_match(self, data_1:List[System], data_2:List[System]): - assert self.validator.validate(data_1, data_2) is None + def test_system_os_match(self, data_1:List[System], data_2:List[System], system_contract: SystemContractViolation): + assert self.validator.validate(data_1, data_2, system_contract) == data_1[0].pythons - def test_system_os_match_2(self, data_1:List[System], data_2:List[System]): - assert self.validator.validate(data_2, data_1) is None - - def test_system_os_invalid(self): - with pytest.raises(ValueError, - match=re.escape(Errors.INVALID_OS.format(Constants.SUPPORTED_OS, "A invalid value"))): - System( - os="A invalid value", - pythons=[] - ) + def test_system_os_match_2(self, data_1:List[System], data_2:List[System], system_contract: SystemContractViolation): + assert self.validator.validate(data_2, data_1, system_contract) == data_2[0].pythons - def test_system_os_mismatch(self, data_1:List[System], data_2:List[System]): - with pytest.raises( - ValueError, - match=re.escape( - Errors.ENV_VAR_MISSING.format(data_1[0].envs) + def test_system_os_mismatch(self, data_1:List[System], data_2:List[System], system_contract: SystemContractViolation): + mock_contract = VariableContractViolation( + Errors.SCOPE_VARIABLE, + Contexts.ENVIRONMENT_CONTEXT, + Bundle( + state= { + Errors.KEY_ENVIRONMENT_1 : data_1[0].environment, + Errors.KEY_ENVIRONMENT_VARIABLE_NAME: "CI" + } ) + ) + with pytest.raises(ValueError, + match=re.escape( + mock_contract.missing_error_handler(Errors.COLLECTIONS_MESSAGES) + ) ): - self.validator.validate(data_1, data_2) + self.validator.validate(data_1, data_2, system_contract) @pytest.mark.usefixtures("os_windows_setter") - def test_system_os_mismatch_1(self, data_1:List[System], data_2:List[System]): - assert self.validator.validate(data_1, data_2) is None + def test_system_os_mismatch_1(self, data_1:List[System], data_2:List[System], system_contract: SystemContractViolation): + bundle: Bundle = Bundle() + bundle[Errors.KEY_SYSTEMS_1] = data_1 + mock_contract = SystemContractViolation( + Contexts.RUNTIME_CONTEXT, + bundle + ) + with pytest.raises(ValueError, + match=re.escape( + mock_contract.missing_error_handler(Errors.COLLECTIONS_MESSAGES) + ) + ): + self.validator.validate(data_1, data_2, system_contract) - def test_system_mismatch(self, data_2:List[System]): - assert self.validator.validate(None, data_2) is None + def test_system_mismatch(self, data_2:List[System], system_contract: SystemContractViolation): + assert self.validator.validate(None, data_2, system_contract) is None - def test_system_mismatch_1(self, data_2:List[System]): - with pytest.raises( - ValueError, - match=re.escape(Errors.ELEMENT_MISSING.format(data_2)) + def test_system_mismatch_1(self, data_2:List[System], system_contract: SystemContractViolation): + bundle: Bundle = Bundle() + bundle[Errors.KEY_SYSTEMS_1] = data_2 + mock_contract = SystemContractViolation( + Contexts.RUNTIME_CONTEXT, + bundle + ) + with pytest.raises(ValueError, + match=re.escape( + mock_contract.missing_error_handler(Errors.COLLECTIONS_MESSAGES) + ) ): - self.validator.validate(data_2, None) \ No newline at end of file + self.validator.validate(data_2, None, system_contract) \ No newline at end of file