From 2d7fcf8457e9c0d38a1c6b703f5fa3b91e8f6899 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Wed, 18 Mar 2026 09:11:48 -0400 Subject: [PATCH] chore: align pyproject.toml, docs, and source with project standards - Remove stale [tool.setuptools.dynamic] section (build backend is hatchling) - Remove q debug dependency and add Python 3.13 classifier to match tox matrix - Fix Documentation URL to point to docs/ directory instead of repo root - Enforce B904 and T201 globally; suppress T201 per-file in logging.py where print is intentional - Add [tool.pytest.ini_options] and [tool.coverage] configuration sections - Fix all raise-without-from violations in heuristics.py, http.py, and jsonutils.py - Remove unused asyncio import and add missing type annotation in help.py - Rewrite README.md to be concise and accurate; update AGENTS.md to reflect all services/resources --- AGENTS.md | 46 ++++++---- README.md | 134 ++++++++--------------------- pyproject.toml | 32 ++++--- src/asyncplatform/heuristics.py | 2 +- src/asyncplatform/http.py | 2 +- src/asyncplatform/jsonutils.py | 8 +- src/asyncplatform/services/help.py | 3 +- uv.lock | 11 --- 8 files changed, 87 insertions(+), 151 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ca97eb1..b1a3b01 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Technology Stack -- **Language**: Python 3.10+ (supports 3.10, 3.11, 3.12) +- **Language**: Python 3.10+ (supports 3.10, 3.11, 3.12, 3.13) - **Build System**: Hatchling with uv-dynamic-versioning - **Dependency Management**: uv (see uv.lock) - **Core Dependencies**: ipsdk @@ -52,6 +52,17 @@ uv run python -m build uv sync --group dev ``` +### Make Shortcuts +```bash +make test # Run test suite (verbose) +make coverage # Run tests with HTML coverage report +make lint # Lint src/ and tests/ +make security # Run bandit security analysis +make premerge # Run full premerge checks locally +make tox # Test across Python 3.10–3.13 +make clean # Remove build artifacts +``` + ## Architecture ### Core Components @@ -72,24 +83,17 @@ uv sync --group dev ### Current Services -- **automation_studio** (`src/asyncplatform/services/automation_studio.py`): Manages Automation Studio projects and workflows - - `get_projects()`: Retrieve all projects with automatic pagination - - `describe_project()`: Get detailed project information - - `find_projects()`: Search projects by name - - `import_project()`: Import a project - - `delete_project()`: Delete a project by ID - - `patch_project()`: Update project fields - - `describe_workflow()`: Get workflow details - -- **authorization** (`src/asyncplatform/services/authorization.py`): Manages authorization groups and accounts - - `get_groups()`: Retrieve all authorization groups with pagination - - `get_accounts()`: Retrieve all user accounts with pagination +- **automation_studio**: Manage projects and workflows (import, delete, patch, describe) +- **authorization**: Manage authorization groups and user accounts +- **configuration_manager**: Manage platform configuration +- **lifecycle_manager**: Manage platform lifecycle operations +- **operations_manager**: Manage platform operations +- **help**: Access platform help and documentation ### Current Resources -- **projects** (`src/asyncplatform/resources/projects.py`): High-level project management - - `importer()`: Import project with member assignments - - `delete()`: Delete project by name +- **projects**: Import projects with member assignments, delete by name +- **automations**: High-level automation management ### Service Base Class @@ -136,11 +140,19 @@ async with asyncplatform.client(**cfg) as client: - `src/asyncplatform/exceptions.py`: Custom exception classes - `src/asyncplatform/jsonutils.py`: JSON utilities for API responses - `src/asyncplatform/http.py`: HTTP enumerations and response wrapper +- `src/asyncplatform/heuristics.py`: Heuristic utilities +- `src/asyncplatform/metadata.py`: Package metadata - `src/asyncplatform/models/`: Data models (e.g., ProjectMember) - `examples/import_project.py`: Example usage patterns ## Development Notes +### Test Structure + +- Tests live in `tests/unit/` (flat layout, not mirroring `src/`) +- Test files named `test_.py` (e.g., `test_services_authorization.py`) +- `tox.ini` configures multi-version testing across Python 3.10–3.13 + ### Code Style and Type Annotations - **Python 3.10+ Required**: All code uses modern Python 3.10+ features @@ -209,5 +221,3 @@ async with asyncplatform.client(**cfg) as client: - Use pathlib for file operations - Prefer `pathlib.glob()` over manual iteration for file discovery - Use dictionary comprehensions to filter None values from kwargs - - diff --git a/README.md b/README.md index 350f6c3..5203fcc 100644 --- a/README.md +++ b/README.md @@ -1,169 +1,107 @@ # AsyncPlatform -An async Python client library for the Itential Platform REST API. +> Async Python client library for the Itential Platform REST API. -AsyncPlatform provides a high-level, asynchronous interface for interacting with Itential Automation Platform services. Built on top of [ipsdk](https://github.com/itential/ipsdk), it offers a plugin-based architecture with automatic service discovery and resource management. - -## Features - -- **Async/Await Support**: Built for modern Python with full asyncio support -- **High-Level Resources**: Complex operations simplified through resource abstractions -- **Type Hints**: Fully typed for better IDE support and type checking -- **Caching**: Built-in async-safe caching with TTL support -- **Context Management**: Automatic connection lifecycle management +AsyncPlatform provides a high-level, asynchronous interface for the Itential Automation Platform. It wraps [ipsdk](https://github.com/itential/ipsdk) with automatic service discovery, resource abstractions, and connection lifecycle management. ## Requirements -- Python 3.10 or higher -- Itential Platform 2023.1 or higher +- Python 3.10+ +- Itential Platform 2023.1+ +- [uv](https://github.com/astral-sh/uv) (recommended) or pip ## Installation ```bash +# with uv (recommended) +uv add asyncplatform + +# with pip pip install asyncplatform ``` ## Quick Start -### Basic Usage - ```python +import asyncio import asyncplatform async def main(): cfg = { "host": "platform.example.com", "user": "admin@domain", - "password": "your-password" + "password": "your-password", } async with asyncplatform.client(**cfg) as client: - # Access services directly projects = await client.automation_studio.get_projects() print(f"Found {len(projects)} projects") -if __name__ == "__main__": - import asyncio - asyncio.run(main()) +asyncio.run(main()) ``` -### Using Services +## Usage -Services provide access to specific Itential Platform APIs: +### Services + +Services map directly to Itential Platform APIs and are available as attributes on the client: ```python async with asyncplatform.client(**cfg) as client: - # Get all projects + # Automation Studio projects = await client.automation_studio.get_projects() - - # Get specific project details project = await client.automation_studio.describe_project("project-id") - # Get authorization groups + # Authorization groups = await client.authorization.get_groups() - - # Get user accounts accounts = await client.authorization.get_accounts() ``` -### Using Resources +Available services: `automation_studio`, `authorization`, `configuration_manager`, +`lifecycle_manager`, `operations_manager`, `help`. + +### Resources -Resources provide high-level abstractions for complex operations: +Resources combine multiple service calls into single high-level operations: ```python from asyncplatform.models.projects import ProjectMember async with asyncplatform.client(**cfg) as client: - # Get the projects resource projects = client.resource("projects") - # Import a project with member assignments - project_data = { - "name": "My Project", - "description": "Project description" - } - members = [ ProjectMember(name="admin_group", type="group", role="owner"), - ProjectMember(name="user@example.com", type="account", role="editor") + ProjectMember(name="user@example.com", type="account", role="editor"), ] result = await projects.importer(project_data, members=members) - print(f"Imported project: {result['name']}") - - # Delete a project by name await projects.delete("My Project") ``` +Available resources: `projects`, `automations`. + ## Development -### Setup +**Prerequisites**: Python 3.10+, [uv](https://github.com/astral-sh/uv) ```bash -# Clone the repository git clone https://github.com/itential/asyncplatform.git cd asyncplatform - -# Install dependencies -uv sync +uv sync --group dev ``` -### Running Tests - ```bash -# Run all tests -uv run pytest - -# Run with coverage -uv run pytest --cov=src/asyncplatform --cov-report=term - -# Run specific test file -uv run pytest tests/unit/test_loader.py +make test # run tests +make lint # lint src/ and tests/ +make coverage # test with HTML coverage report +make security # bandit security scan +make premerge # full premerge checks (lint + test + security) +make tox # test across Python 3.10–3.13 ``` -### Code Quality - -```bash -# Linting -uv run ruff check src/asyncplatform tests - -# Type checking -uv run mypy src/asyncplatform - -# Formatting -uv run ruff format src/asyncplatform tests -``` - -## Contributing - -Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct, development workflow, and the process for submitting pull requests. - -## Documentation - -- [AGENTS.md](AGENTS.md) - Project overview and architecture for AI assistants -- [CONTRIBUTING.md](CONTRIBUTING.md) - Development guide and contribution guidelines -- [Itential Platform Documentation](https://docs.itential.com/) - -## Support - -- Report bugs and feature requests via [GitHub Issues](https://github.com/itential/asyncplatform/issues) -- For questions about the Itential Platform, visit [Itential Documentation](https://docs.itential.com/) +See [CONTRIBUTING.md](CONTRIBUTING.md) and the [`docs/`](docs/) directory for architecture, testing patterns, and contribution guidelines. ## License -Copyright (c) 2025 Itential, Inc - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -See [LICENSE](LICENSE) for the full license text and [LICENSES.md](LICENSES.md) for third-party license information. +Copyright (c) 2025 Itential, Inc. GPL-3.0-or-later — see [LICENSE](LICENSE) and [NOTICE](NOTICE). diff --git a/pyproject.toml b/pyproject.toml index 4e6ec0b..74ac8b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Typing :: Typed", ] @@ -41,7 +42,7 @@ email = "opensource@itential.com" [project.urls] Homepage = "https://itential.com" Repository = "https://github.com/itential/asyncplatform" -Documentation = "https://github.com/itential/asyncplatform" +Documentation = "https://github.com/itential/asyncplatform/tree/main/docs" [dependency-groups] @@ -49,7 +50,6 @@ dev = [ "pytest", "pytest-cov", "pytest-asyncio", - "q", "ruff", "mypy", "coverage", @@ -64,9 +64,6 @@ dev = [ requires = ["hatchling", "uv-dynamic-versioning>=0.7.0"] build-backend = "hatchling.build" -[tool.setuptools.dynamic] -version = { attr = "asyncplatform.metadata.version" } - [tool.hatch.version] source = "uv-dynamic-versioning" @@ -199,8 +196,6 @@ ignore = [ "S105", "S106", "S107", # Ignore complexity "C901", "PLR0911", "PLR0912", "PLR0913", "PLR0915", - # Allow print statements (useful for debugging) - "T201", # Allow assert statements (used for internal validation in SDK) "S101", # Allow subprocess without shell=False (we control the input) @@ -209,20 +204,10 @@ ignore = [ "S607", # Allow use of `typing.Any` "ANN401", - # Allow missing docstrings for magic methods - "D105", - # Allow missing docstrings in __init__ - "D107", # Allow relative imports for local packages "TID252", # Allow catching broad exceptions (necessary for HTTP client) "BLE001", - # Allow raise without from inside except - "B904", - # Allow too many return statements - "PLR0911", - # Allow too many arguments - "PLR0913", # Allow implicit string concatenation (useful for long strings) "ISC001", # Allow passing strings directly to exceptions @@ -288,6 +273,7 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" "G004", # Logging statement uses f-string (acceptable for informational logging) "E501", # Line too long (docstrings can be longer) "PLW0603", # Global statement (used for module-level state) + "T201", # print used intentionally in fatal() to write to stderr before sys.exit ] "src/asyncplatform/heuristics.py" = [ @@ -385,3 +371,15 @@ disable_error_code = ["attr-defined", "arg-type"] module = "asyncplatform.resources.*" # Ignore attribute errors for dynamically loaded service attributes disable_error_code = ["attr-defined"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +asyncio_mode = "strict" + +[tool.coverage.run] +source = ["src/asyncplatform"] +omit = ["tests/*"] + +[tool.coverage.report] +show_missing = true +skip_covered = false diff --git a/src/asyncplatform/heuristics.py b/src/asyncplatform/heuristics.py index 5190678..8d34470 100644 --- a/src/asyncplatform/heuristics.py +++ b/src/asyncplatform/heuristics.py @@ -177,7 +177,7 @@ def add_pattern( ) except re.error as e: msg = f"Invalid regex pattern for '{name}': {e}" - raise re.error(msg) + raise re.error(msg) from e def remove_pattern(self, name: str) -> bool: """Remove a pattern from the scanner. diff --git a/src/asyncplatform/http.py b/src/asyncplatform/http.py index c792c51..8f41884 100644 --- a/src/asyncplatform/http.py +++ b/src/asyncplatform/http.py @@ -143,7 +143,7 @@ def json(self) -> dict[str, Any]: return self._response.json() except Exception as exc: msg = f"Failed to parse response as JSON: {exc!s}" - raise ValueError(msg) + raise ValueError(msg) from exc def raise_for_status(self) -> None: """ diff --git a/src/asyncplatform/jsonutils.py b/src/asyncplatform/jsonutils.py index e321c5a..d1a38cc 100644 --- a/src/asyncplatform/jsonutils.py +++ b/src/asyncplatform/jsonutils.py @@ -34,12 +34,12 @@ def loads(s: str) -> dict | list: except json.JSONDecodeError as exc: logging.exception(exc) msg = f"Failed to parse JSON: {exc!s}" - raise exceptions.SerializationError(msg, exc=exc) + raise exceptions.SerializationError(msg, exc=exc) from exc except Exception as exc: logging.exception(exc) msg = f"Unexpected error parsing JSON: {exc!s}" - raise exceptions.SerializationError(msg, exc=exc) + raise exceptions.SerializationError(msg, exc=exc) from exc def dumps(o: dict | list) -> str: @@ -60,9 +60,9 @@ def dumps(o: dict | list) -> str: except (TypeError, ValueError) as exc: logging.exception(exc) msg = f"Failed to serialize object to JSON: {exc!s}" - raise exceptions.SerializationError(msg, exc=exc) + raise exceptions.SerializationError(msg, exc=exc) from exc except Exception as exc: logging.exception(exc) msg = f"Unexpected error serializing JSON: {exc!s}" - raise exceptions.SerializationError(msg, exc=exc) + raise exceptions.SerializationError(msg, exc=exc) from exc diff --git a/src/asyncplatform/services/help.py b/src/asyncplatform/services/help.py index 293109d..2abdd86 100644 --- a/src/asyncplatform/services/help.py +++ b/src/asyncplatform/services/help.py @@ -15,6 +15,7 @@ class Service(ServiceBase): @logging.trace async def get_openapi(self, url: str | None = None) -> dict[str, Any]: - """ """ + """ + """ res = await self.get("/help/openapi", params={"url": (url or "/")}) return res.json() diff --git a/uv.lock b/uv.lock index cc2cf40..3c06adc 100644 --- a/uv.lock +++ b/uv.lock @@ -33,7 +33,6 @@ dev = [ { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, - { name = "q" }, { name = "ruff" }, { name = "tox" }, { name = "tox-uv" }, @@ -52,7 +51,6 @@ dev = [ { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, - { name = "q" }, { name = "ruff" }, { name = "tox" }, { name = "tox-uv" }, @@ -721,15 +719,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] -[[package]] -name = "q" -version = "2.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/15/90/2649ecc3b4b335e62de4a0c3762c7cd7b2f77a023c5c00649f549cebb56c/q-2.7.tar.gz", hash = "sha256:8e0b792f6658ab9e1133b5ea17af1b530530e60124cf9743bc0fa051b8c64f4e", size = 7946, upload-time = "2022-07-23T20:07:39.429Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/75/f0/ae942c0530d02092702211fd36d9a465e203f732789c84d0b96fbebe3039/q-2.7-py2.py3-none-any.whl", hash = "sha256:8388a3ef7e79b3b6224189e44ddba8dc1a6e9ed3212ce96f83f6056fa532459c", size = 10390, upload-time = "2022-07-23T20:07:37.727Z" }, -] - [[package]] name = "rich" version = "14.2.0"