diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 0a84f76..f3f3f92 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.8.3 +current_version = 1.9.0 commit = False tag = False parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+)(?P\d+))? diff --git a/.github/workflows/test-package.yaml b/.github/workflows/test-package.yaml index f9ec0f0..3e85581 100644 --- a/.github/workflows/test-package.yaml +++ b/.github/workflows/test-package.yaml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.11", "3.12"] + python-version: ["3.11", "3.12", "3.13"] fail-fast: false steps: @@ -27,12 +27,13 @@ jobs: python -m pip install --upgrade pip pip install flit flit install --deps develop --symlink - - name: Ruff - run: ruff check . - - name: Black - run: black . --check - name: Mypy run: mypy . + - name: License headers + run: | + apache-license-check --copyright "2019-`date +%Y` SURF" nwastdlib + - name: Run pre-commit hooks + uses: pre-commit/action@v3.0.1 - name: Test with pytest run: pytest --cov-branch --cov=nwastdlib --cov-report=xml --ignore=tests --ignore=benchmarks - name: "Upload coverage to Codecov" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b83e1cd..5876068 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,24 +1,23 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v3.2.2 + rev: v3.19.1 hooks: - id: pyupgrade exclude: tests args: - - --py39-plus + - --py311-plus - --keep-runtime-typing - repo: https://github.com/psf/black - rev: 23.10.1 + rev: 24.10.0 hooks: - id: black - language_version: python3.9 - repo: https://github.com/asottile/blacken-docs - rev: 1.16.0 + rev: 1.19.1 hooks: - id: blacken-docs - additional_dependencies: [black==22.1.0] + additional_dependencies: [black==24.10.0] - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v5.0.0 hooks: - id: trailing-whitespace exclude: .bumpversion.cfg @@ -30,6 +29,12 @@ repos: - id: debug-statements - id: requirements-txt-fixer - id: detect-private-key + - repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.8.4 + hooks: + - id: ruff + args: [ --fix, --exit-non-zero-on-fix, --show-fixes ] - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.10.0 hooks: @@ -38,6 +43,6 @@ repos: - id: python-check-mock-methods - id: rst-backticks - repo: https://github.com/shellcheck-py/shellcheck-py - rev: v0.9.0.6 + rev: v0.10.0.1 hooks: - id: shellcheck diff --git a/README.md b/README.md index aab4ca0..56daf07 100644 --- a/README.md +++ b/README.md @@ -4,35 +4,80 @@ [![Supported python versions](https://img.shields.io/pypi/pyversions/nwa-stdlib.svg?color=%2334D058)](https://pypi.org/project/nwa-stdlib) [![codecov](https://codecov.io/github/workfloworchestrator/nwa-stdlib/graph/badge.svg?token=9XWVHKKF06)](https://codecov.io/github/workfloworchestrator/nwa-stdlib) -This library contains the functions and utilities that are shared by most -Network Automation projects built at SURF. +This library contains the functions and utilities that are shared by most Network Automation projects built at SURF. -## Getting started +## Installation -If you want to use a virtual environment first create the environment: +To install the package from PyPI: ```bash -pip install flit +pip install nwa-stdlib ``` -And then run the following commands: -If you want to enhance or develop bug fixes for `nwastdlib` it's easiest to run the following commands: +## Development + +### Virtual Environment + +Steps to setup a virtual environment. + +#### Step 1: + +Create and activate a python3 virtualenv. + +#### Step 2: + +Install flit to enable you to develop on this repository: + ```bash -flit install --deps develop --symlink +pip install flit ``` -## Development -Depending on the feature type, run bumpversion (patch|minor|major) to increment the version you are working on. For -example to update the increment the patch version use +#### Step 3: + +To install all development dependencies: + ```bash -bumpversion patch +flit install --deps develop ``` -## To run tests +All steps combined into 1 command: + +```bash +python -m venv .venv && source .venv/bin/activate && pip install -U pip && pip install flit && flit install --deps develop ``` + +### Unit tests + +Activate the virtualenv and run the unit tests with: + +```bash pytest ``` +### Pre-commit + +This project uses [pre-commit](https://pre-commit.com/) to automatically run a number of checks before making a git commit. +The same checks will be performed in the CI pipeline so this can save you some time. + +First ensure you have pre-commit installed. +It is recommended to install it outside the virtualenv. +On Linux and Mac, pre-commit is available in most package managers. Alternatively you can install it globally with [pipx](https://github.com/pypa/pipx). + +Once pre-commit is installed, go into the project root and enable it: +```bash +pre-commit install +``` + +This should output `pre-commit installed at .git/hooks/pre-commit`. The next time you run `git commit` the pre-commit hooks will validate your changes. + +### Bump version + +Depending on the feature type, run bumpversion (patch|minor|major) to increment the version you are working on. For +example to update the increment the patch version use +```bash +bumpversion patch +``` + ## Supported Python versions nwa-stdlib must support the same python versions as [orchestrator-core](https://github.com/workfloworchestrator/orchestrator-core). diff --git a/nwastdlib/__init__.py b/nwastdlib/__init__.py index 314b9e1..87dcaf9 100644 --- a/nwastdlib/__init__.py +++ b/nwastdlib/__init__.py @@ -1,19 +1,19 @@ -# Copyright 2019 SURF. -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at +# Copyright 2019-2025 SURF. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. # """The NWA-stdlib module.""" -__version__ = "1.8.3" +__version__ = "1.9.0" from nwastdlib.f import const, identity diff --git a/nwastdlib/asyncio.py b/nwastdlib/asyncio.py index 9269ea8..7cda1fd 100644 --- a/nwastdlib/asyncio.py +++ b/nwastdlib/asyncio.py @@ -1,4 +1,4 @@ -# Copyright 2019-2024 SURF. +# Copyright 2019-2025 SURF. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -13,7 +13,7 @@ import asyncio import warnings from collections.abc import Awaitable, Callable, Iterable -from typing import TypeVar, Union +from typing import TypeVar A = TypeVar("A") R = TypeVar("R") @@ -29,7 +29,7 @@ async def gather_nice_sync( args: Iterable[A], limit: int = 5, return_exceptions: bool = False, - ) -> list[R]: + ) -> list[R | BaseException]: """Run function in thread for each args, using asyncio.gather() with limited concurrency. Example: @@ -45,7 +45,7 @@ def my_function_2(arg1, arg2): """ - def make_args(func_args: Union[Iterable, object]) -> Iterable: + def make_args(func_args: Iterable | object) -> Iterable: # When one argument is passed, wrap it in a list so run_sync can unpack it again if not isinstance(func_args, (tuple, list, set)): return [func_args] @@ -60,7 +60,7 @@ async def gather_nice( coros: Iterable[Awaitable[R]], limit: int = 5, return_exceptions: bool = False, -) -> Iterable[R]: +) -> Iterable[R | BaseException]: """Run coroutines in asyncio.gather() with limited concurrency. Example: diff --git a/nwastdlib/asyncio_cache.py b/nwastdlib/asyncio_cache.py index 29d377a..6ac7338 100644 --- a/nwastdlib/asyncio_cache.py +++ b/nwastdlib/asyncio_cache.py @@ -1,22 +1,23 @@ -# Copyright 2019-2024 SURF. -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at +# Copyright 2019-2025 SURF. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. # import hashlib import hmac -import pickle # noqa S403 +import pickle # noqa: S403 import sys +from collections.abc import Callable from functools import wraps -from typing import Any, Callable, Protocol, Union, runtime_checkable +from typing import Any, Protocol, runtime_checkable import structlog from redis.asyncio import Redis as AIORedis @@ -40,11 +41,11 @@ class DefaultSerializer: @staticmethod def deserialize(data: Any) -> Any: - return pickle.loads(data) # noqa S403 + return pickle.loads(data) # noqa: S301 @staticmethod - def serialize(data: Union[bytes, bytearray, str]) -> Any: - return pickle.dumps(data) # noqa S403 + def serialize(data: bytes | bytearray | str) -> Any: + return pickle.dumps(data) def _deserialize(data: Any, serializer: SerializerProtocol) -> Any: @@ -58,7 +59,7 @@ def _deserialize(data: Any, serializer: SerializerProtocol) -> Any: return data -def get_hmac_checksum(secret: str, message: Union[bytes, bytearray, str]) -> str: +def get_hmac_checksum(secret: str, message: bytes | bytearray | str) -> str: if isinstance(message, str): message = message.encode() h = hmac.new(secret.encode(), message, hashlib.sha512) @@ -113,8 +114,8 @@ async def get_signed_cache_value(pool: AIORedis, secret: str, cache_key: str, se def cached_result( pool: AIORedis, prefix: str, - secret: Union[str, None], - key_name: Union[str, None] = None, + secret: str | None, + key_name: str | None = None, expiry_seconds: int = 120, serializer: SerializerProtocol = DefaultSerializer, ) -> Callable: diff --git a/nwastdlib/debugging.py b/nwastdlib/debugging.py index 90ba661..48ed6cf 100644 --- a/nwastdlib/debugging.py +++ b/nwastdlib/debugging.py @@ -1,4 +1,4 @@ -# Copyright 2019-2024 SURF. +# Copyright 2019-2025 SURF. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at diff --git a/nwastdlib/ex.py b/nwastdlib/ex.py index c1e5736..f449f5d 100644 --- a/nwastdlib/ex.py +++ b/nwastdlib/ex.py @@ -1,25 +1,24 @@ -# Copyright 2019-2024 SURF. -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at +# Copyright 2019-2025 SURF. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. # """Module containing functions to deal with `Exception`s.""" import traceback -from typing import Union -def show_ex(ex: Exception, stacklimit: Union[int, None] = None) -> str: +def show_ex(ex: Exception, stacklimit: int | None = None) -> str: """Show an exception, including its class name, message and (limited) stacktrace. >>> try: diff --git a/nwastdlib/f.py b/nwastdlib/f.py index a53e67e..cb49528 100644 --- a/nwastdlib/f.py +++ b/nwastdlib/f.py @@ -1,18 +1,19 @@ -# Copyright 2019-2024 SURF. -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at +# Copyright 2019-2025 SURF. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. # -from typing import Any, Callable, TypeVar +from collections.abc import Callable +from typing import Any, TypeVar α = TypeVar("α") β = TypeVar("β") diff --git a/nwastdlib/graphql/extensions/deprecation_checker_extension.py b/nwastdlib/graphql/extensions/deprecation_checker_extension.py index ddcf134..da78277 100644 --- a/nwastdlib/graphql/extensions/deprecation_checker_extension.py +++ b/nwastdlib/graphql/extensions/deprecation_checker_extension.py @@ -1,4 +1,4 @@ -# Copyright 2022-2024 SURF. +# Copyright 2019-2025 SURF. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at diff --git a/nwastdlib/graphql/extensions/error_handler_extension.py b/nwastdlib/graphql/extensions/error_handler_extension.py index 27f502f..be98a5d 100644 --- a/nwastdlib/graphql/extensions/error_handler_extension.py +++ b/nwastdlib/graphql/extensions/error_handler_extension.py @@ -1,4 +1,4 @@ -# Copyright 2019-2024 SURF. +# Copyright 2019-2025 SURF. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -10,11 +10,11 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from collections.abc import Collection, Generator, Iterable +from collections.abc import Callable, Collection, Generator, Iterable from contextvars import ContextVar from enum import StrEnum, auto from http import HTTPStatus -from typing import Any, Callable +from typing import Any import structlog from graphql import GraphQLError diff --git a/nwastdlib/logging.py b/nwastdlib/logging.py index 659ec56..caaba54 100644 --- a/nwastdlib/logging.py +++ b/nwastdlib/logging.py @@ -1,19 +1,19 @@ -# Copyright 2019-2024 SURF. -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at +# Copyright 2019-2025 SURF. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import logging.config import os -from typing import Any, Union +from typing import Any import structlog @@ -65,7 +65,7 @@ } -def initialise_logging(additional_loggers: Union[dict[str, dict[str, Any]], None] = None) -> None: +def initialise_logging(additional_loggers: dict[str, dict[str, Any]] | None = None) -> None: """Initialise the StructLog logging setup. An example of the additional_loggers format: diff --git a/nwastdlib/settings.py b/nwastdlib/settings.py index f7698e5..b1aed8f 100644 --- a/nwastdlib/settings.py +++ b/nwastdlib/settings.py @@ -1,4 +1,4 @@ -# Copyright 2019-2024 SURF. +# Copyright 2019-2025 SURF. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at diff --git a/nwastdlib/url.py b/nwastdlib/url.py index 0019399..5d9af41 100644 --- a/nwastdlib/url.py +++ b/nwastdlib/url.py @@ -1,15 +1,15 @@ -# Copyright 2019-2024 SURF. -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at +# Copyright 2019-2025 SURF. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. # from __future__ import annotations diff --git a/nwastdlib/version.py b/nwastdlib/version.py index 1c87dd6..444e1c3 100644 --- a/nwastdlib/version.py +++ b/nwastdlib/version.py @@ -1,4 +1,4 @@ -# Copyright 2019-2024 SURF. +# Copyright 2019-2025 SURF. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -12,14 +12,13 @@ # limitations under the License. from subprocess import check_output # noqa: S404 -from typing import Union import structlog logger = structlog.getLogger(__name__) -def __getattr__(name: str) -> Union[str, None]: +def __getattr__(name: str) -> str | None: """Return the GIT_COMMIT_HASH. Usage:: diff --git a/nwastdlib/vlans.py b/nwastdlib/vlans.py index 6d87d96..3481f75 100644 --- a/nwastdlib/vlans.py +++ b/nwastdlib/vlans.py @@ -1,4 +1,4 @@ -# Copyright 2019-2024 SURF. +# Copyright 2019-2025 SURF. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -19,7 +19,7 @@ from collections import abc from collections.abc import Iterable, Iterator, Sequence from functools import reduce, total_ordering -from typing import AbstractSet, Any, ClassVar, Optional, Union, cast +from typing import AbstractSet, Any, ClassVar, cast from pydantic import GetCoreSchemaHandler, GetJsonSchemaHandler from pydantic.json_schema import JsonSchemaValue @@ -121,13 +121,11 @@ class VlanRanges(abc.Set): """ - __pydantic_serializer__: ClassVar[Optional[SchemaSerializer]] # workaround for a bug, see usage below + __pydantic_serializer__: ClassVar[SchemaSerializer | None] # workaround for a bug, see usage below _vlan_ranges: tuple[range, ...] - def __init__( # noqa: C901 - self, val: Optional[Union[str, int, Iterable[int], Sequence[Sequence[int]]]] = None - ) -> None: + def __init__(self, val: str | int | Iterable[int] | Sequence[Sequence[int]] | None = None) -> None: # noqa: C901 # The idea is to bring all acceptable values to one canonical intermediate format: the `Sequence[Sequence[ # int]]`. Where the inner sequence is either a one or two element sequence. The one element sequence # represents a single VLAN, the two element sequence represents a VLAN range. @@ -226,7 +224,7 @@ def __eq__(self, o: object) -> bool: def __hash__(self) -> int: return hash(self._vlan_ranges) - def __sub__(self, other: Union[int, AbstractSet[Any]]) -> VlanRanges: + def __sub__(self, other: int | AbstractSet[Any]) -> VlanRanges: if isinstance(other, int): new_set = set(self) new_set.remove(other) @@ -285,7 +283,7 @@ def __get_pydantic_json_schema__(cls, core_schema_: CoreSchema, handler: GetJson return json_schema_resolved | schema_override @staticmethod - def _validate(input_value: Union[str, VlanRanges]) -> VlanRanges: + def _validate(input_value: str | VlanRanges) -> VlanRanges: if isinstance(input_value, VlanRanges): return input_value return VlanRanges(input_value) diff --git a/pyproject.toml b/pyproject.toml index ae5ef40..e01bf5c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ classifiers = [ "Intended Audience :: Telecommunications Industry", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.11", ] @@ -38,7 +39,7 @@ requires = [ "structlog>=22.1.0", ] description-file = "README.md" -requires-python = ">=3.11" +requires-python = ">=3.11,<3.14" [tool.flit.metadata.urls] Documentation = "https://workfloworchestrator.org/" @@ -46,11 +47,12 @@ Documentation = "https://workfloworchestrator.org/" [tool.flit.metadata.requires-extra] test = [ "anyio", + "apache-license-check", "black", "fakeredis", "fastapi", "httpx", - "mypy==1.6.1", + "mypy", "mypy_extensions", "pytest", "pytest-cov", @@ -67,7 +69,7 @@ dev = [ [tool.black] line-length = 120 -target-version = ["py39", "py310"] +target-version = ["py311"] exclude = ''' ( /( @@ -123,7 +125,7 @@ lint.ignore = [ ] line-length = 120 lint.select = ["B", "C", "D", "E", "F", "I", "N", "RET", "S", "T", "W"] -target-version = "py39" +target-version = "py311" [tool.ruff.lint.flake8-tidy-imports] ban-relative-imports = "all"