diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..8f92cba --- /dev/null +++ b/.coveragerc @@ -0,0 +1,2 @@ +[run] +omit = pyds8k/test/* \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..a8a0b55 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,44 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python + +name: Lint, Test + +on: + push: + branches: [ "main", "develop" ] + pull_request: + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + - name: Run Ruff Lint + uses: astral-sh/ruff-action@v3 + with: + src: "./pyds8k" + - name: Run Ruff Format + uses: astral-sh/ruff-action@v3 + with: + args: "format --check --diff" + src: "./pyds8k" + + unit_test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + python: ["3.9", "3.10", "3.11", "3.12", "3.13"] + + steps: + - uses: actions/checkout@v4 + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + - name: Install tox and any other packages + run: | + pip install tox + - name: Run tox + run: tox -e py diff --git a/.gitignore b/.gitignore index 9c27cce..82c8f6e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,24 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv + +# Coverage .coverage +cover + +# Docs +docs/html + +# Tests .tox -*.pyc + .project .idea .pydevproject @@ -10,9 +28,6 @@ *.help .pylintrc *.log -__pycache__ .DS_Store -nosetests.xml -*.egg-info -docs/html -cover + + diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..bd28b9c --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.9 diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..7179c9c --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,25 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the version of Python and other tools you might need +build: + os: ubuntu-20.04 + tools: + python: "3.9" + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: docs/conf.py + +# If using Sphinx, optionally build your docs in additional formats such as PDF +formats: + - pdf + +# Optionally declare the Python requirements required to build your docs +python: + install: + - requirements: docs/requirements.txt diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 303ab35..0000000 --- a/.travis.yml +++ /dev/null @@ -1,20 +0,0 @@ -# ref: https://docs.travis-ci.com/user/languages/python -language: python -dist: xenial - -matrix: - include: - - python: 3.6 - env: TOXENV=py36 - - python: 3.7 - env: TOXENV=py37 - - python: 3.7 - env: TOXENV=cover,docs,flake8 - - python: 3.8 - env: TOXENV=py38 - -install: - - pip install tox - -script: - - tox diff --git a/Notices.txt b/Notices.txt index e4b034a..e83c064 100644 --- a/Notices.txt +++ b/Notices.txt @@ -256,43 +256,6 @@ END OF APACHE 2.0 NOTICES AND INFORMATION =========================================================================== - -@@@@@@@@@@@@ -=========================================================================== -HTTPretty version 0.9.6: The Program includes HTTPretty version 0.9.6 -software. IBM obtained the HTTPretty version 0.9.6 software under the -terms and conditions of the following license(s): ---------------------------------------------------------------------------- - -Copyright (C) <2011-2018> Gabriel Falcão - -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation -files (the "Software"), to deal in the Software without -restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - - -=========================================================================== -END OF HTTPretty version 0.9.6 NOTICES AND INFORMATION -=========================================================================== - - =========================================================================== END OF NOTICES AND INFORMATION FOR IBM DS8000 Python Client Version 1.1.0 Third Party Licenses and Notices diff --git a/README.md b/README.md index 7790340..217ec75 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,32 @@ # DS8000 Python Client -[![Build Status](https://travis-ci.com/IBM/pyds8k.svg?branch=develop)](https://travis-ci.com/IBM/pyds8k) +[![Build Status](https://github.com/IBM/pyds8k/actions/workflows/main.yml/badge.svg?branch=develop)](https://github.com/IBM/pyds8k/actions/workflows/main.yml) +[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/5884/badge)](https://bestpractices.coreinfrastructure.org/projects/5884) +[![Documentation Status](https://readthedocs.org/projects/pyds8k/badge/?version=latest)](https://pyds8k.readthedocs.io/en/latest/?badge=latest) This repository contains the IBM RESTful API Python client, which establishes terminal connection with IBM DS8000 storage systems. The Python client protocol enables full management and monitoring of these storage arrays by issuing dedicated RESTful APIs. ## Python Compatibility -The content in this collection supports Python 3.6 and newer. +The content in this collection supports Python 3.9 and newer. ## Getting started Clone the repository, and then add it to your PYTHONPATH directory. The Python client is then ready for import and use. +The library is also available to install using pip. See [the pypi pyds8k project](https://pypi.org/project/pyds8k/) + +To install via pip run the following command: + +```shell +pip install pyds8k +``` + +## Documentation + +Documentation for the pyds8k library can be generated using sphinx. +The documentation for the latest release is also available via [pyds8k.readthedocs.io](https://pyds8k.readthedocs.io/en/latest) + +NOTE: To view older versions of the doc, click on the link at the bottom right corner in the readthedocs link and select the desired version. ## Usage examples @@ -22,29 +38,39 @@ Each storage system of DS8000 and major software version has its own set of REST To display the full RESTful API Reference Guide of a specific storage system and a specific software version: -1. Navigate to a storage system welcome page on KC: - +1. Navigate to a storage system welcome page on KC: 2. On the welcome page, select a storage system software version. For example, select **Version 8.5.3**. -![Software version](https://github.com/IBM/pyds8k/blob/master/images/1.jpg) + ![Software version](images/1.jpg) -The welcome page of the selected software version is displayed. + The welcome page of the selected software version is displayed. 3. If needed, select the **Table of contents** tab. -![Table of contents](https://github.com/IBM/pyds8k/blob/master/images/2.jpg) + ![Table of contents](images/2.jpg) 4. On the table of contents, click **RESTful API**. -![CLI interface](https://github.com/IBM/pyds8k/blob/master/images/3.jpg) + ![CLI interface](images/3.jpg) -5. Refer to **Host commands** and to all subsequent chapters. +5. Refer to **Host commands** and to all subsequent chapters. ## Contributing + We do not accept any contributions at the moment. This may change in the future, so you can fork, clone, and suggest a pull request. ## Running tests -Use nosetests command to run a test. - nosetests -v +Use tox to run the test suite + +```shell +tox +``` + +Use pytest to run tests or a specific test. + +```shell +pytest --cov-config=.coveragerc --cov pyds8k --disable-warnings -v +pytest --cov-config=.coveragerc --cov pyds8k --disable-warnings -v pyds8k/test/test_resources/test_ds8k/test_host.py +``` diff --git a/docs/api/pyds8k.resources.ds8k.v1.hmc.certificate.rst b/docs/api/pyds8k.resources.ds8k.v1.hmc.certificate.rst new file mode 100644 index 0000000..74a80e5 --- /dev/null +++ b/docs/api/pyds8k.resources.ds8k.v1.hmc.certificate.rst @@ -0,0 +1,37 @@ +pyds8k.resources.ds8k.v1.hmc.certificate package +================================================ + +Submodules +---------- + +pyds8k.resources.ds8k.v1.hmc.certificate.certificate +---------------------------------------------------- + +.. automodule:: pyds8k.resources.ds8k.v1.hmc.certificate.certificate + :members: + :undoc-members: + :show-inheritance: + +pyds8k.resources.ds8k.v1.hmc.certificate.csr module +--------------------------------------------------- + +.. automodule:: pyds8k.resources.ds8k.v1.hmc.certificate.csr + :members: + :undoc-members: + :show-inheritance: + +pyds8k.resources.ds8k.v1.hmc.certificate.selfsigned +--------------------------------------------------- + +.. automodule:: pyds8k.resources.ds8k.v1.hmc.certificate.selfsigned + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: pyds8k.resources.ds8k.v1.hmc.certificate + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/api/pyds8k.resources.ds8k.v1.hmc.rst b/docs/api/pyds8k.resources.ds8k.v1.hmc.rst new file mode 100644 index 0000000..81a49d9 --- /dev/null +++ b/docs/api/pyds8k.resources.ds8k.v1.hmc.rst @@ -0,0 +1,29 @@ +pyds8k.resources.ds8k.v1.hmc package +==================================== + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + pyds8k.resources.ds8k.v1.hmc.certificate + +Submodules +---------- + +pyds8k.resources.ds8k.v1.hmc.restart module +------------------------------------------- + +.. automodule:: pyds8k.resources.ds8k.v1.hmc.restart + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: pyds8k.resources.ds8k.v1.hmc + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/api/pyds8k.resources.ds8k.v1.rst b/docs/api/pyds8k.resources.ds8k.v1.rst index 4f3a71b..87bd1ec 100644 --- a/docs/api/pyds8k.resources.ds8k.v1.rst +++ b/docs/api/pyds8k.resources.ds8k.v1.rst @@ -9,6 +9,7 @@ Subpackages pyds8k.resources.ds8k.v1.common pyds8k.resources.ds8k.v1.cs + pyds8k.resources.ds8k.v1.hmc Submodules ---------- diff --git a/docs/conf.py b/docs/conf.py index 5b24962..1b651d2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -7,8 +7,13 @@ import os import sys -sys.path.insert(0, os.path.realpath(os.path.abspath('../'))) -sys.path.append(os.path.abspath('../..')) +sys.path.insert( + 0, + os.path.realpath( + os.path.abspath(f"..{os.sep}") + ) +) +sys.path.append(os.path.abspath(f"..{os.sep}..")) import pyds8k @@ -22,15 +27,28 @@ # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' + +on_rtd = os.environ.get('READTHEDOCS', None) == 'True' + # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.viewcode', - 'sphinx.ext.coverage', - 'sphinx.ext.todo', - 'sphinx.ext.napoleon' -] +if on_rtd: + extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.viewcode', + 'sphinx.ext.coverage', + 'sphinx.ext.todo', + 'sphinx.ext.napoleon', + 'sphinx_rtd_theme' + ] +else: + extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.viewcode', + 'sphinx.ext.coverage', + 'sphinx.ext.todo', + 'sphinx.ext.napoleon' + ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -46,7 +64,7 @@ # General information about the project. project = 'IBM DS8000 pyds8k' -copyright = '2012-2022 IBM' +copyright = '2012-2023 IBM' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -92,11 +110,15 @@ #modindex_common_prefix = [] -# -- Options for HTML output --------------------------------------------------- +# -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'alabaster' + +if on_rtd: # only import and set the theme if we're building docs locally + html_theme = 'sphinx_rtd_theme' +else: + html_theme = 'alabaster' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -256,7 +278,7 @@ epub_title = 'pyds8k' epub_author = 'IBM' epub_publisher = 'IBM' -epub_copyright = '2022, IBM' +epub_copyright = '2023, IBM' # The language of the text. It defaults to the language option # or en if the language is not set. diff --git a/docs/environment.yaml b/docs/environment.yaml new file mode 100644 index 0000000..8fba4a2 --- /dev/null +++ b/docs/environment.yaml @@ -0,0 +1,11 @@ +# File: docs/environment.yaml + +name: docs +channels: + - conda-forge + - defaults +dependencies: + - sphinx==4.2.0 + - nbsphinx==0.8.1 + - pip: + - sphinx_rtd_theme==1.0.0 diff --git a/docs/index.rst b/docs/index.rst index 3e49947..8459ead 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -43,6 +43,7 @@ following command from the root directory of the **pyds8k** source. installation changelog + api/pyds8k Indices and tables diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..8e905ce --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,5 @@ +# File: docs/requirements.txt + +# Defining the exact version will make sure things don't break +sphinx==5.0.0 +sphinx_rtd_theme==1.0.0 \ No newline at end of file diff --git a/pyds8k/__init__.py b/pyds8k/__init__.py index 9dd7718..3238b99 100644 --- a/pyds8k/__init__.py +++ b/pyds8k/__init__.py @@ -20,9 +20,10 @@ :module: pyds8k """ + PYDS8K_DEFAULT_LOGGER = "pyds8k" -version_tuple = (1, 4, 0) +version_tuple = (1, 6, 0) def get_version_string(): diff --git a/pyds8k/auth/authenticate.py b/pyds8k/auth/authenticate.py index 58f98b5..e56f413 100644 --- a/pyds8k/auth/authenticate.py +++ b/pyds8k/auth/authenticate.py @@ -18,7 +18,5 @@ def get_authenticate(service_type, service_version): - auth_module = import_module( - '{0}.{1}.{2}.auth'.format(__package__, service_type, service_version) - ) + auth_module = import_module(f'{__package__}.{service_type}.{service_version}.auth') return auth_module.Auth diff --git a/pyds8k/auth/ds8k/base.py b/pyds8k/auth/ds8k/base.py index 3e404a7..6616b65 100644 --- a/pyds8k/auth/ds8k/base.py +++ b/pyds8k/auth/ds8k/base.py @@ -20,8 +20,7 @@ DEFAULT_BASE_URL = '' -class Auth(object): - +class Auth: base_url = DEFAULT_BASE_URL auth_url = AUTH_URL @@ -29,7 +28,7 @@ def __init__(self): pass @classmethod - def authenticate(self, client): + def authenticate(cls, client): """ The main authenticate method. Mandatory """ @@ -40,21 +39,19 @@ def authenticate(self, client): if client.hostname: params['hmc1'] = client.hostname req_p = RequestParser(params) - _, body = client.post(self.get_auth_url(), - body=req_p.get_request_data() - ) + _, body = client.post(cls.get_auth_url(), body=req_p.get_request_data()) token = _get_data(body).get('token', '') if token: client.set_defaultHeaders('X-Auth-Token', token) # client.set_defaultQuerystrings('token', token) @classmethod - def get_auth_url(self): + def get_auth_url(cls): """ Return the auth url. Mandatory """ - return self.base_url + self.auth_url + return cls.base_url + cls.auth_url def _get_data(response_body): diff --git a/pyds8k/auth/ds8k/v1/auth.py b/pyds8k/auth/ds8k/v1/auth.py old mode 100755 new mode 100644 index 25b1f1c..96310c2 --- a/pyds8k/auth/ds8k/v1/auth.py +++ b/pyds8k/auth/ds8k/v1/auth.py @@ -22,5 +22,4 @@ class Auth(AuthBase): - base_url = Base.base_url diff --git a/pyds8k/base.py b/pyds8k/base.py old mode 100755 new mode 100644 index bf80d61..483479b --- a/pyds8k/base.py +++ b/pyds8k/base.py @@ -14,20 +14,30 @@ # limitations under the License. ############################################################################## +import contextlib import json -import os import sys -from . import messages -from pyds8k.utils import get_response_parser_class, \ - get_request_parser_class -from pyds8k.utils import is_absolute_url -from pyds8k.utils import HTTP200, HTTP204, POSTA, POST -from pyds8k.exceptions import URLNotSpecifiedError, \ - FieldReadOnly, \ - URLParseError, \ - ResponseBodyMissingError -from pyds8k import PYDS8K_DEFAULT_LOGGER +from http import HTTPStatus from logging import getLogger +from pathlib import Path + +from pyds8k import PYDS8K_DEFAULT_LOGGER +from pyds8k.exceptions import ( + FieldReadOnly, + InvalidMethodForCreate, + ResponseBodyMissingError, + URLNotSpecifiedError, + URLParseError, +) +from pyds8k.utils import ( + POST, + POSTA, + get_request_parser_class, + get_response_parser_class, + is_absolute_url, +) + +from . import messages logger = getLogger(PYDS8K_DEFAULT_LOGGER) @@ -35,7 +45,7 @@ MANAGERS = {} -class URLBuilderMixin(object): +class URLBuilderMixin: def one(self, route, resource_id): pass @@ -43,7 +53,7 @@ def all(self, route): pass -class UtilsMixin(object): +class UtilsMixin: def _update_list_field(self, field_name, value_list, operator='+'): if not isinstance(value_list, list): value_list = [value_list] @@ -51,45 +61,35 @@ def _update_list_field(self, field_name, value_list, operator='+'): if operator == '+': for item in value_list: if item in field: - raise KeyError( - messages.ITEM_IN_LIST.format(field_name, item) - ) + raise KeyError(messages.ITEM_IN_LIST.format(field_name, item)) field.append(item) if operator == '-': for item in value_list: if item not in field: - raise KeyError( - messages.ITEM_NOT_IN_LIST.format(field_name, item) - ) + raise KeyError(messages.ITEM_NOT_IN_LIST.format(field_name, item)) field.pop(field.index(item)) return field def _get_id(self): return self.id if hasattr(self, 'id') else id(self) - def remove_None_fields_from_dict(self, input_dict): - new_dict = {} - for (key, value) in input_dict.items(): - if value is not None: - new_dict[key] = value - return new_dict + def remove_None_fields_from_dict(self, input_dict): # noqa: N802 + return {key: value for key, value in input_dict.items() if value is not None} + + def remove_empty_fields_from_dict(self, input_dict): + return {key: value for key, value in input_dict.items() if value} # all the resources are under folder "resources", # the route prefix of a resource resources/a/b/c.py is a.b def get_resource_route_prefix_by_class(cls): - path = os.path.abspath( - os.path.dirname(sys.modules[cls.__module__].__file__) - ) - route = path.replace("/", ".") - return route.rsplit(".resources.", 2)[1] + path = Path(sys.modules[cls.__module__].__file__).parent.resolve() + return '.'.join(path.parts[path.parts.index("resources") + 1 :]) class ResourceMeta(type): - def __new__(mcs, name, bases, dct): - - new_class = super(ResourceMeta, mcs).__new__(mcs, name, bases, dct) + new_class = super().__new__(mcs, name, bases, dct) if "resource_type" in dct: prefix = get_resource_route_prefix_by_class(new_class) route = "{}.{}".format(prefix, dct["resource_type"]) @@ -98,10 +98,8 @@ def __new__(mcs, name, bases, dct): class ManagerMeta(type): - def __new__(mcs, name, bases, dct): - - new_class = super(ManagerMeta, mcs).__new__(mcs, name, bases, dct) + new_class = super().__new__(mcs, name, bases, dct) if "resource_type" in dct: prefix = get_resource_route_prefix_by_class(new_class) route = "{}.{}".format(prefix, dct["resource_type"]) @@ -113,9 +111,7 @@ def get_resource_class_by_route(route): try: return RESOURCES[route] except KeyError: - logger.debug('Failed to get resource by name: {}, ' - 'return default one.'.format(route) - ) + logger.debug(f'Failed to get resource by name: {route}, return default one.') return Resource @@ -123,19 +119,15 @@ def get_manager_class_by_route(route): try: return MANAGERS[route] except KeyError: - logger.debug('Failed to get manager by name: {}, ' - 'return default one.'.format(route) - ) + logger.debug(f'Failed to get manager by name: {route}, return default one.') return DefaultManager def get_resource_and_manager_class_by_route(route): - return get_resource_class_by_route(route), \ - get_manager_class_by_route(route) + return get_resource_class_by_route(route), get_manager_class_by_route(route) -class BaseResource(object): - +class BaseResource: pass @@ -164,8 +156,16 @@ class Resource(UtilsMixin, BaseResource): related_resource = {} alias = {} - def __init__(self, client, manager=None, url='', info={}, - resource_id=None, parent=None, loaded=False): + def __init__( + self, + client, + manager=None, + url='', + info=None, + resource_id=None, + parent=None, + loaded=False, + ): self.set_loaded(loaded) self._start_init() self._init_updating() @@ -181,27 +181,28 @@ def __init__(self, client, manager=None, url='', info={}, self.parent = parent self._custom_url = '' self._set_modified_info_dict() + if info is None: + info = {} if info: self._add_details(info) self._finish_init() def one(self, route, resource_id, rebuild_url=False): url = self._set_url(route, resource_id, rebuild_url=rebuild_url) - return self._get_resource_by_route( - route, self.client, - url, self, resource_id - ) + return self._get_resource_by_route(route, self.client, url, self, resource_id) def all(self, route, rebuild_url=False): url = self._set_url(route, rebuild_url=rebuild_url) return self._get_resource_by_route(route, self.client, url, self) - def toUrl(self, method, body={}): + def toUrl(self, method, body=None): # noqa: N802 """ To send non-standard rest request, like /attach """ self._set_custom_url(method) + if body is None: + body = {} if body: resp, res_body = self.post(body=body) else: @@ -223,46 +224,46 @@ def custom_method(self, para1, para2): return result def create(self, **kwargs): - custom_info = {} - # for (k, v) in six.iteritems(info): - for (k, v) in kwargs.items(): - if k in list(self._template.keys()): - custom_info[k] = v + custom_info = { + k: v for k, v in kwargs.items() if k in list(self._template.keys()) + } return self.create_from_template(custom_info) - def create_from_template(self, custom_info={}): + def create_from_template(self, custom_info=None): + if custom_info is None: + custom_info = {} _url = self._rm_id_in_url() _info = self._template.copy() if self.id_field in _info: del _info[self.id_field] _info.update(custom_info) - data = self.remove_None_fields_from_dict(_info) - - res = self.__class__(client=self.client, - manager=self.manager.__class__(self.client), - url=_url, - info=data, - parent=self.parent, - # Set loaded=True to avoid lazy-loading - loaded=True) + data = self.remove_empty_fields_from_dict(_info) + + res = self.__class__( + client=self.client, + manager=self.manager.__class__(self.client), + url=_url, + info=data, + parent=self.parent, + # Set loaded=True to avoid lazy-loading + loaded=True, + ) for key, value in data.items(): if value: res._set_modified_info_dict(key, value) res._is_new = True return res - def _get_resource_by_route(self, route, client, url, - parent=None, resource_id=None): - prefix = '{}.{}'.format(client.service_type, client.service_version) - r, m = get_resource_and_manager_class_by_route( - "{}.{}".format(prefix, str(route).lower()) + def _get_resource_by_route(self, route, client, url, parent=None, resource_id=None): + prefix = f'{client.service_type}.{client.service_version}' + r, m = get_resource_and_manager_class_by_route(f"{prefix}.{str(route).lower()}") + return r( + client=client, + manager=m(client=client), + url=url, + parent=parent, + resource_id=resource_id, ) - return r(client=client, - manager=m(client=client), - url=url, - parent=parent, - resource_id=resource_id, - ) def _update_alias(self, res): for key, alias in self.alias.items(): @@ -270,27 +271,27 @@ def _update_alias(self, res): res[alias] = res.pop(key) return res - def _set_url(self, route, resource_id='', rebuild_url=False): + def _set_url(self, route, resource_id='', rebuild_url=False): url = self.url if not rebuild_url else '' # when route contains prefix, like cs.pprcs # cs.pprcs => cs/pprcs route = route.replace('.', '/') if resource_id: - url += '/{}/{}'.format(route, resource_id) + url += f'/{route}/{resource_id}' else: - url += '/{}'.format(route) + url += f'/{route}' return url def _add_id_to_url(self, resource_id): - if not self.url.endswith('/{}'.format(resource_id)): - self.url += '/{}'.format(resource_id) + if not self.url.endswith(f'/{resource_id}'): + self.url += f'/{resource_id}' def _rm_id_in_url(self, resource_id=''): if not hasattr(self, 'id'): return self.url res_id = resource_id or self.id - if self.url.endswith('/{}'.format(res_id)): - return self.url[:len(self.url) - len(self.id) - 1] + if self.url.endswith(f'/{res_id}'): + return self.url[: len(self.url) - len(self.id) - 1] return self.url def _add_base_to_url(self, url): @@ -311,20 +312,16 @@ def _reverse_custom_url(self): def _add_details(self, info, force=False): self._start_updating() info = self._update_alias(info) - try: - # set id field first. + with contextlib.suppress(KeyError): self._id = info[self.id_field] - except KeyError: - pass self_url = self.ResponseParser.get_link_from_representation(info) if self_url: self.url = self_url - # for (k, v) in six.iteritems(info): - for (k, v) in info.items(): + for k, v in info.items(): if not force and k in list(self._modified_info_dict.keys()): continue - if not k == self.id_field: + if k != self.id_field: setattr(self, k, v) self.representation[k] = v @@ -344,19 +341,24 @@ def _set_related_resource(self, res_key): res_id = res_info[res_class.id_field] self.representation[res_key] = res_id setattr(self, res_key, res_id) - setattr(self, - '_' + res_key, - res_class(self.client, - manager=res_manager(self.client), - resource_id=res_id, - info=res_info, - loaded=False, - ) - ) - except Exception: - logger.debug( - messages.SET_RELATED_RESOURCE_FAILED.format(res_key, self) - ) + setattr( + self, + '_' + res_key, + res_class( + self.client, + manager=res_manager(self.client), + resource_id=res_id, + info=res_info, + loaded=False, + ), + ) + except (KeyError, TypeError, ValueError): + logger.debug(messages.SET_RELATED_RESOURCE_FAILED.format(res_key, self)) + self.representation[res_key] = res_info + setattr(self, res_key, res_info) + setattr(self, '_' + res_key, None) + except Exception: # noqa: BLE001 # ???: Unsure if other exceptions can occur + logger.debug(messages.SET_RELATED_RESOURCE_FAILED.format(res_key, self)) self.representation[res_key] = res_info setattr(self, res_key, res_info) setattr(self, '_' + res_key, None) @@ -364,23 +366,22 @@ def _set_related_resource(self, res_key): def _get_url(self, urls): if isinstance(urls, str): return urls - elif isinstance(urls, dict): + if isinstance(urls, dict): urls = [urls] elif isinstance(urls, list): pass else: - raise URLParseError() + raise URLParseError for url in urls: if url.get('rel') == 'self': return url.get('href', '') return '' def __getattr__(self, k): - if k == 'id' or k == self.id_field: + if k in ('id', self.id_field): if '_id' not in self.__dict__: raise AttributeError(k) - else: - return self._id + return self._id # If we can get the attr from a resource collection # we don't need to get the resource details. # So we don't load the details until an attr which is @@ -391,41 +392,43 @@ def __getattr__(self, k): return getattr(self, k) raise AttributeError(k) - else: - return self.__dict__[k] + return self.__dict__[k] def __setattr__(self, key, value): if key == '_id': self._add_id_to_url(value) if key.startswith('_'): - super(Resource, self).__setattr__(key, value) - return + super().__setattr__(key, value) + return None if self._is_init(): - return super(Resource, self).__setattr__(key, value) - if key == 'id' or key == self.id_field: + return super().__setattr__(key, value) + if key in ('id', self.id_field): raise FieldReadOnly(key) - if not self.is_updating() and (key in self._template or key in self.representation): # noqa + if not self.is_updating() and ( + key in self._template or key in self.representation + ): self.representation[key] = value self._set_modified_info_dict(key, value) - super(Resource, self).__setattr__(key, value) + super().__setattr__(key, value) + + return None def __repr__(self): - reprkeys = \ - sorted(k for k in self.__dict__ if not str(k).startswith('_') and - k not in ('manager', 'client') - ) - info = ", ".join("{0}={1}".format(k, getattr(self, k)) - for k in reprkeys) - return "<{0} {1}>".format(self.__class__.__name__, info) + reprkeys = sorted( + k + for k in self.__dict__ + if not str(k).startswith('_') and k not in ('manager', 'client') + ) + info = ", ".join(f"{k}={getattr(self, k)}" for k in reprkeys) + return f"<{self.__class__.__name__} {info}>" def get(self, resource_id='', force=False, **kwargs): self.set_loaded(True) if resource_id: return self.manager.get(resource_id, **kwargs) - else: - _, info = self.manager.get(**kwargs) - self._add_details(info, force) - return self + _, info = self.manager.get(**kwargs) + self._add_details(info, force) + return self def get_response(self): return self.manager.get() @@ -457,15 +460,16 @@ def delete(self): resp, data = self.manager.delete() return resp, data - def update(self, info={}): + def update(self, info=None): + if info is None: + info = {} resp = None data = None if info: resp, data = self.manager.patch(body=info) self._del_modified_info_dict_keys(info) else: - resp, data = self.manager.patch(body=self._get_modified_info_dict() - ) + resp, data = self.manager.patch(body=self._get_modified_info_dict()) self._set_modified_info_dict() return resp, data @@ -478,20 +482,19 @@ def save(self): resp, data = self.put() else: if self.create_method.lower() not in ('posta', 'put'): - raise Exception( - "You should use POSTA or PUT method to create new resources" # noqa - ) + raise InvalidMethodForCreate(self.create_method.lower()) resp, data = getattr(self, self.create_method.lower())() - if self.create_method.lower() == 'posta': - if isinstance(data[0], Resource): - # re-init the res object according to the returned data - self.__init__( - client=self.client, - manager=self.manager, - url=data[0].url, - resource_id=data[0].id, - info=data[0].representation - ) + if self.create_method.lower() == 'posta' and isinstance( + data[0], Resource + ): + # re-init the res object according to the returned data + self.__init__( + client=self.client, + manager=self.manager, + url=data[0].url, + resource_id=data[0].id, + info=data[0].representation, + ) self._is_new = False else: resp, data = self.posta() @@ -502,7 +505,7 @@ def save(self): manager=self.manager, url=data[0].url, resource_id=data[0].id, - info=data[0].representation + info=data[0].representation, ) # self.set_loaded(False) # Set to false in order to use lazy loading. @@ -535,9 +538,12 @@ def __eq__(self, other): return self.id == other.id try: return self._info == other._info and self.url == other.url - except Exception: + except Exception: # noqa: BLE001 # ???: Unsure what exceptions can occur return False + def __hash__(self): + return hash(self.name) + def is_loaded(self): return self._loaded @@ -572,8 +578,7 @@ def set_id_field(cls, _id_field): @classmethod def set_base_url(cls, base): url = base - if url.endswith('/'): - url = url[:-1] + url = url.removesuffix('/') if not url.startswith('/'): url = '/' + url cls.base_url = url @@ -586,8 +591,7 @@ def get_template_from_server(self): pass -class BaseManager(object): - +class BaseManager: pass @@ -600,6 +604,7 @@ class Manager(UtilsMixin, BaseManager): :param managed_object: The related resource object :param url: A resource or a resource collection's url """ + resource_class = Resource response_key = 'data' resource_type = '' @@ -614,42 +619,48 @@ def __init__(self, client, managed_object=None, url=''): def _get_data(self, response_body, method='', response=None): if not method: # get or list if not response_body: - raise ResponseBodyMissingError() + raise ResponseBodyMissingError res_p = self.ResponseParser(response_body, self.resource_type) return res_p.get_representations() - elif method == POSTA: + if method == POSTA: if not response_body: - raise ResponseBodyMissingError() + raise ResponseBodyMissingError res_p = self.ResponseParser(response_body, self.resource_type) return res_p.get_posta_response_data() + if response_body: + try: + data = self._get_status_body(response_body) + except AttributeError: + logger.debug( + messages.CAN_NOT_GET_STATUS_BODY.format( + method, self.resource_class.__name__ + ) + ) + data = response_body + except Exception: # noqa: BLE001 # ???: Unsure if other exceptions can occur + logger.debug( + messages.CAN_NOT_GET_STATUS_BODY.format( + method, self.resource_class.__name__ + ) + ) + data = response_body + + elif response.status_code in (HTTPStatus.OK, HTTPStatus.NO_CONTENT): + data = messages.DEFAULT_SUCCESS_BODY_DICT else: - if response_body: - try: - data = self._get_status_body(response_body) - except Exception: - logger.debug( - messages.CAN_NOT_GET_STATUS_BODY.format( - method, - self.resource_class.__name__ - ) - ) - data = response_body - elif response.status_code in (HTTP200, HTTP204): - data = messages.DEFAULT_SUCCESS_BODY_DICT - else: + res_id = '' + try: + res_id = self.managed_object.id + except Exception: # noqa: BLE001 # ???: Unsure what exceptions can occur res_id = '' - try: - res_id = self.managed_object.id - except Exception: - res_id = '' - data = json.loads( - messages.DEFAULT_FAIL_BODY_JSON.format( - action=method, - res_class=self.resource_class.__name__, - res_id=res_id - ) - ) - return data + data = json.loads( + messages.DEFAULT_FAIL_BODY_JSON.format( + action=method, + res_class=self.resource_class.__name__, + res_id=res_id, + ) + ) + return data def _get_status_body(self, response_body): res_p = self.ResponseParser(response_body, self.resource_type) @@ -665,7 +676,7 @@ def _return_new_resource_by_response_data(self, resp, data, url): resource_id = self.ResponseParser.get_resource_id_from_url( url=resource_uri, resource_type=self.resource_type, - ) + ) else: resource_id = None return self.resource_class( @@ -673,7 +684,7 @@ def _return_new_resource_by_response_data(self, resp, data, url): manager=self.__class__(self.client), url=resource_uri, resource_id=resource_id, - info=data + info=data, ) def _get(self, resource_id='', url='', obj_class=None, **kwargs): @@ -687,7 +698,7 @@ def _get(self, resource_id='', url='', obj_class=None, **kwargs): self.url += '/' + resource_id new = True else: - raise URLNotSpecifiedError() + raise URLNotSpecifiedError else: self.url = url new = True @@ -696,15 +707,16 @@ def _get(self, resource_id='', url='', obj_class=None, **kwargs): if not new: return resp, data - else: - if obj_class is None: - obj_class = self.resource_class - return obj_class(client=self.client, - manager=self.__class__(self.client), - url=self.url, - info=data, - parent=parent, - loaded=True) + if obj_class is None: + obj_class = self.resource_class + return obj_class( + client=self.client, + manager=self.__class__(self.client), + url=self.url, + info=data, + parent=parent, + loaded=True, + ) # if url and obj_class is not none, list the sub collection # of current resource. @@ -715,7 +727,7 @@ def _list(self, url='', obj_class=None, body=None, **kwargs): self.url = self.managed_object.url parent = self.managed_object.parent else: - raise URLNotSpecifiedError() + raise URLNotSpecifiedError else: self.url = url if body: @@ -726,21 +738,25 @@ def _list(self, url='', obj_class=None, body=None, **kwargs): obj_class = self.resource_class data = self._get_data(body) - return [obj_class(client=self.client, - manager=self.__class__(self.client), - url=self.url, - parent=parent, - info=res) for res in data if res] + return [ + obj_class( + client=self.client, + manager=self.__class__(self.client), + url=self.url, + parent=parent, + info=res, + ) + for res in data + if res + ] def _post(self, body, url=None): if not url: if self.managed_object is not None: url = self.managed_object.url else: - raise URLNotSpecifiedError() - resp, res_body = self.client.post(url, - body=self._get_request_data(body) - ) + raise URLNotSpecifiedError + resp, res_body = self.client.post(url, body=self._get_request_data(body)) data = self._get_data(res_body, method=POST, response=resp) return resp, data @@ -752,17 +768,16 @@ def _posta(self, url='', body=None): self.url = self.managed_object.url post_body = body or self.managed_object.representation else: - raise URLNotSpecifiedError() + raise URLNotSpecifiedError else: self.url = url post_body = body or self.managed_object.representation - post_body = self.remove_None_fields_from_dict(post_body) - resp, body = self.client.post(self.url, - body=self._get_request_data(post_body) - ) + post_body = self.remove_empty_fields_from_dict(post_body) + resp, body = self.client.post(self.url, body=self._get_request_data(post_body)) data = self._get_data(body, method=POSTA, response=resp) if not isinstance(data, list): - raise Exception("The parsed posta response data should be a list.") + msg = "The parsed posta response data should be a list." + raise TypeError(msg) res_list = [] for s_data in data: res_data, res_url = s_data @@ -770,9 +785,7 @@ def _posta(self, url='', body=None): res = res_data.get(self.ResponseParser.error_status_key) else: res = self._return_new_resource_by_response_data( - resp, - res_data.get(self.ResponseParser.resource_data_key), - res_url + resp, res_data.get(self.ResponseParser.resource_data_key), res_url ) res_list.append(res) return resp, res_list @@ -784,13 +797,11 @@ def _put(self, url='', body=None): self.url = self.managed_object.url put_body = body or self.managed_object.representation else: - raise URLNotSpecifiedError() + raise URLNotSpecifiedError else: self.url = url put_body = body - resp, body = self.client.put(self.url, - body=self._get_request_data(put_body) - ) + resp, body = self.client.put(self.url, body=self._get_request_data(put_body)) data = self._get_data(body, method='PUT', response=resp) return resp, data @@ -799,16 +810,17 @@ def _patch(self, url='', body=None): if not url: if self.managed_object is not None: self.url = self.managed_object.url - patch_body = body if body else \ - self.managed_object._get_modified_info_dict() + patch_body = ( + body if body else self.managed_object._get_modified_info_dict() + ) else: - raise URLNotSpecifiedError() + raise URLNotSpecifiedError else: self.url = url patch_body = body - resp, body = self.client.patch(self.url, - body=self._get_request_data(patch_body) - ) + resp, body = self.client.patch( + self.url, body=self._get_request_data(patch_body) + ) data = self._get_data(body, method='PATCH', response=resp) return resp, data @@ -817,7 +829,7 @@ def _delete(self, url=''): if self.managed_object is not None: self.url = self.managed_object.url else: - raise URLNotSpecifiedError() + raise URLNotSpecifiedError else: self.url = url resp, body = self.client.delete(self.url) @@ -829,12 +841,14 @@ class DefaultManager(Manager): """ Default resource manager. """ + resource_class = Resource resource_type = 'default' def get(self, resource_id='', url='', obj_class=None, **kwargs): - return self._get(resource_id=resource_id, url=url, - obj_class=obj_class, **kwargs) + return self._get( + resource_id=resource_id, url=url, obj_class=obj_class, **kwargs + ) def list(self, url='', obj_class=None, body=None, **kwargs): return self._list(url=url, obj_class=obj_class, body=body, **kwargs) diff --git a/pyds8k/client/ds8k/v1/client.py b/pyds8k/client/ds8k/v1/client.py old mode 100755 new mode 100644 index b92ca4d..f645eb1 --- a/pyds8k/client/ds8k/v1/client.py +++ b/pyds8k/client/ds8k/v1/client.py @@ -14,17 +14,17 @@ # limitations under the License. ############################################################################## from logging import getLogger + from pyds8k import PYDS8K_DEFAULT_LOGGER +from pyds8k.base import DefaultManager, Resource from pyds8k.httpclient import HTTPClient -from pyds8k.base import Resource, DefaultManager -from pyds8k.resources.ds8k.v1.systems import System, \ - SystemManager +from pyds8k.resources.ds8k.v1.systems import System, SystemManager logger = getLogger(PYDS8K_DEFAULT_LOGGER) DEFAULT_PORT = 8452 -class Client(object): +class Client: """ Top-level object to access all the DS8K resources. @@ -59,37 +59,48 @@ class Client(object): object: DS8000 REST-API Client """ - def __init__(self, service_address, user, password, - port=DEFAULT_PORT, - hostname='', - service_type='ds8k', - service_version='v1', - timeout=None, - verify=True - ): + def __init__( + self, + service_address, + user, + password, + port=DEFAULT_PORT, + hostname='', + service_type='ds8k', + service_version='v1', + timeout=None, + verify=True, + ): logger.info('================== logger is enabled ==================') - client = HTTPClient(service_address, user, password, - port=port, - hostname=hostname, - service_type=service_type, - service_version=service_version, - timeout=timeout, - verify=verify - ) + client = HTTPClient( + service_address, + user, + password, + port=port, + hostname=hostname, + service_type=service_type, + service_version=service_version, + timeout=timeout, + verify=verify, + ) self.client = client self.resource = Resource(self.client, DefaultManager(self.client)) self.system = System(self.client, SystemManager(self.client)) + def _callable(self, k): + method = getattr(self.system, k) + + if not callable(method): + raise TypeError(k) + + return method + def __getattr__(self, k): try: # if not self.system.is_loaded(): # self.system = self.system.get_system() - method = getattr(self.system, k) - if not callable(method): - raise AttributeError(k) - else: - return method - except Exception: - raise AttributeError(k) + return self._callable(k) + except Exception as exc: + raise AttributeError(k) from exc diff --git a/pyds8k/client/ds8k/v1/sc_client.py b/pyds8k/client/ds8k/v1/sc_client.py old mode 100755 new mode 100644 index 980a741..4d16991 --- a/pyds8k/client/ds8k/v1/sc_client.py +++ b/pyds8k/client/ds8k/v1/sc_client.py @@ -14,30 +14,39 @@ # limitations under the License. ############################################################################## -from .client import Client -from pyds8k.utils import dictionarize -from pyds8k.exceptions import NotFound from logging import getLogger + from pyds8k import PYDS8K_DEFAULT_LOGGER +from pyds8k.exceptions import NotFound +from pyds8k.utils import dictionarize + +from .client import Client logger = getLogger(PYDS8K_DEFAULT_LOGGER) -class SCClient(object): +class SCClient: """ SC side client. Used to interaction with current side client. !--important: the id field of all resources are case-insensitive--! """ - def __init__(self, service_address, user, password, - port=None, - hostname='', - ): - self.client = Client(service_address, user, password, - port=port, - hostname=hostname, - ) + def __init__( + self, + service_address, + user, + password, + port=None, + hostname='', + ): + self.client = Client( + service_address, + user, + password, + port=port, + hostname=hostname, + ) @dictionarize def get_system(self): @@ -140,29 +149,25 @@ def get_used_lun_numbers_by_host(self, host_name): mappings = self.client.get_mappings_by_host(host_name) return [mapping.id for mapping in mappings] - def create_volumes(self, pool_id, capacity_in_GiB, sam, - volume_names_list): - return self.client.create_volumes(name_col=volume_names_list, - cap=capacity_in_GiB, - pool=pool_id, - tp=sam) + def create_volumes(self, pool_id, capacity_in_GiB, sam, volume_names_list): # noqa: N803 + return self.client.create_volumes( + name_col=volume_names_list, cap=capacity_in_GiB, pool=pool_id, tp=sam + ) def rename_volume(self, volume_id, new_name): - return self.client.update_volume_rename(volume_id=volume_id, - new_name=new_name) + return self.client.update_volume_rename(volume_id=volume_id, new_name=new_name) - def extend_volume(self, volume_id, new_size_in_GiB): - return self.client.update_volume_extend(volume_id=volume_id, - new_size=new_size_in_GiB, - captype='gib') + def extend_volume(self, volume_id, new_size_in_GiB): # noqa: N803 + return self.client.update_volume_extend( + volume_id=volume_id, new_size=new_size_in_GiB, captype='gib' + ) def delete_volume(self, volume_id): # remember to unmap all hosts before delete. return self.client.delete_volume(volume_id=volume_id) def relocate_volume(self, volume_id, new_pool_id): - return self.client.update_volume_move(volume_id=volume_id, - new_pool=new_pool_id) + return self.client.update_volume_move(volume_id=volume_id, new_pool=new_pool_id) def create_extentpool_virtualpool(self): pass @@ -171,9 +176,7 @@ def remove_extentpool_virtualpool(self): pass def crate_host(self, host_name, wwpn, host_type='VMware'): - hosts = self.client.create_host(host_name=host_name, - hosttype=host_type - ) + hosts = self.client.create_host(host_name=host_name, hosttype=host_type) self.attach_hostport_to_host(host_name=hosts[0].id, wwpn=wwpn) return hosts[0].id @@ -182,38 +185,28 @@ def delete_host(self, host_name): return self.client.delete_host(host_name=host_name) def attach_hostport_to_host(self, host_name, wwpn): - return self._get_attach_or_create_host_port(host_name=host_name, - wwpn=wwpn - ) + return self._get_attach_or_create_host_port(host_name=host_name, wwpn=wwpn) def detach_hostport_from_host(self, wwpn): return self.client.delete_host_port(port_id=wwpn) def map_volume_to_host(self, host_name, volume_id, lunid): - return self.client.map_volume_to_host(host_name=host_name, - volume_id=volume_id, - lunid=lunid - ) + return self.client.map_volume_to_host( + host_name=host_name, volume_id=volume_id, lunid=lunid + ) def unmap_volume_from_host(self, host_name, lunid): - return self.client.unmap_volume_from_host(host_name=host_name, - lunid=lunid - ) + return self.client.unmap_volume_from_host(host_name=host_name, lunid=lunid) def _get_attach_or_create_host_port(self, host_name, wwpn): try: host_port = self.client.get_host_port(port_id=wwpn) return self.client.update_host_port_change_host( - port_id=host_port.id, - host_name=host_name + port_id=host_port.id, host_name=host_name ) except NotFound: - logger.debug( - 'host port {} is not found, creating new...'.format(wwpn) - ) - return self.client.create_host_port(port_id=wwpn, - host_name=host_name - ) + logger.debug(f'host port {wwpn} is not found, creating new...') + return self.client.create_host_port(port_id=wwpn, host_name=host_name) # APIs below are deprecated. diff --git a/pyds8k/client/exceptions.py b/pyds8k/client/exceptions.py old mode 100755 new mode 100644 index 08ef2b1..9e6213e --- a/pyds8k/client/exceptions.py +++ b/pyds8k/client/exceptions.py @@ -18,9 +18,10 @@ Exception definitions. """ -from pyds8k.utils import get_subclasses, \ - get_response_parser_class +from http import HTTPStatus + from pyds8k import messages +from pyds8k.utils import get_response_parser_class, get_subclasses class BaseRestError(Exception): @@ -32,23 +33,29 @@ def __init__(self, reason): self.reason = reason def __str__(self): - return messages.INVALID_ARGUMENT.format( - self.reason - ) + return messages.INVALID_ARGUMENT.format(self.reason) + + +class InvalidMethodForCreate(Exception): + def __init__(self, method): + self.method = method + + def __str__(self): + return messages.INVALID_ARGUMINVALID_METHOD_FOR_CREATE.format(self.method) class OperationNotAllowed(Exception): """ The operation performed on the resource is not allowed. """ + def __init__(self, operation_name, resource_name=''): self.operation_name = operation_name self.resource_name = resource_name def __str__(self): return messages.OPERATION_NOT_ALLOWED.format( - self.operation_name, - self.resource_name + self.operation_name, self.resource_name ) @@ -56,6 +63,7 @@ class URLNotSpecifiedError(Exception): """ The URL is not specified. """ + def __str__(self): return messages.URL_NOT_SPECIFIED @@ -64,6 +72,7 @@ class URLMissingError(Exception): """ The URL is missing. """ + def __str__(self): return messages.URL_MISSING @@ -72,6 +81,7 @@ class IDMissingError(Exception): """ The id field is missing or None. """ + def __str__(self): return messages.ID_MISSING @@ -80,6 +90,7 @@ class ResponseBodyMissingError(Exception): """ The response body is missing. """ + def __str__(self): return messages.RESPONSE_BODY_MISSING @@ -88,14 +99,25 @@ class URLParseError(Exception): """ Can not get the URL """ + def __str__(self): return messages.CAN_NOT_GET_URL +class RepresentationNotFoundError(Exception): + """ + Can not find the representation + """ + + def __str__(self): + return messages.REPRESENTATION_NOT_FOUND + + class RepresentationParseError(Exception): """ Can not get the representation """ + def __str__(self): return messages.CAN_NOT_GET_REPRESENTATION @@ -112,11 +134,10 @@ def __str__(self): return messages.FIELD_READONLY.format(self.field_name) -class ConnectionError(Exception): +class ConnectionError(Exception): # noqa: A001 """ Could not open a connection to the API service. """ - pass class Timeout(Exception): @@ -135,30 +156,28 @@ class ClientException(Exception): """ The base exception class for all HTTP client or server errors. """ + def __init__(self, code, message=None, detail='', origin_data=None): self.code = code self.message = message self.detail = detail self.error_data = origin_data if self.message and self.detail: - self.details = '[{}] {}'.format(self.message, self.detail) + self.details = f'[{self.message}] {self.detail}' elif self.message or self.detail: self.details = self.message or self.detail else: self.details = '' def __str__(self): - return "HTTP {0} {1}. {2}".format( - self.code, - self.reason_phrase, - self.details - ) + return f"HTTP {self.code} {self.reason_phrase}. {self.details}" class ClientError(ClientException): """ HTTP 4xx - Client Error """ + status_code = '4xx' reason_phrase = "Client Error" @@ -167,6 +186,7 @@ class ServerError(ClientException): """ HTTP 5xx - Server Error """ + status_code = '5xx' reason_phrase = "Server Error" @@ -175,16 +195,18 @@ class BadRequest(ClientError): """ HTTP 400 - Bad request: you sent some malformed data. """ - status_code = '400' - reason_phrase = "Bad Request" + + status_code = str(HTTPStatus.BAD_REQUEST.value) + reason_phrase = HTTPStatus.BAD_REQUEST.phrase class Unauthorized(ClientError): """ HTTP 401 - Unauthorized: bad credentials. """ - status_code = '401' - reason_phrase = "Unauthorized" + + status_code = str(HTTPStatus.UNAUTHORIZED.value) + reason_phrase = HTTPStatus.UNAUTHORIZED.phrase class Forbidden(ClientError): @@ -192,40 +214,45 @@ class Forbidden(ClientError): HTTP 403 - Forbidden: your credentials don't give you access to this resource. """ - status_code = '403' - reason_phrase = "Forbidden" + + status_code = str(HTTPStatus.FORBIDDEN.value) + reason_phrase = HTTPStatus.FORBIDDEN.phrase class NotFound(ClientError): """ HTTP 404 - Not found """ - status_code = '404' - reason_phrase = "Not Found" + + status_code = str(HTTPStatus.NOT_FOUND.value) + reason_phrase = HTTPStatus.NOT_FOUND.phrase class MethodNotAllowed(ClientError): """ HTTP 405 - Method Not Allowed """ - status_code = '405' - reason_phrase = "Method Not Allowed" + + status_code = str(HTTPStatus.METHOD_NOT_ALLOWED.value) + reason_phrase = HTTPStatus.METHOD_NOT_ALLOWED.phrase class Conflict(ClientError): """ HTTP 409 - Conflict """ - status_code = '409' - reason_phrase = "Conflict" + + status_code = str(HTTPStatus.CONFLICT.value) + reason_phrase = HTTPStatus.CONFLICT.phrase class UnsupportedMediaType(ClientError): """ HTTP 415 - Unsupported Media Type """ - status_code = '415' - reason_phrase = "Unsupported Media Type" + + status_code = str(HTTPStatus.UNSUPPORTED_MEDIA_TYPE.value) + reason_phrase = HTTPStatus.UNSUPPORTED_MEDIA_TYPE.phrase class InternalServerError(ServerError): @@ -233,27 +260,30 @@ class InternalServerError(ServerError): HTTP 500 - Internal Server Error: The server encountered an unexpected condition which prevented it from fulfilling the request. """ - status_code = '500' - reason_phrase = "Internal Server Error" + + status_code = str(HTTPStatus.INTERNAL_SERVER_ERROR.value) + reason_phrase = HTTPStatus.INTERNAL_SERVER_ERROR.phrase class ServiceUnavailable(ServerError): """ HTTP 503 - Service Unavailable """ - status_code = '503' - reason_phrase = "Service Unavailable" + + status_code = str(HTTPStatus.SERVICE_UNAVAILABLE.value) + reason_phrase = HTTPStatus.SERVICE_UNAVAILABLE.phrase class GatewayTimeout(ServerError): """ HTTP 504 - Gateway Timeout """ - status_code = '504' - reason_phrase = "Gateway Timeout" + status_code = str(HTTPStatus.GATEWAY_TIMEOUT.value) + reason_phrase = HTTPStatus.GATEWAY_TIMEOUT.phrase -_error_dict = dict((c.status_code, c) for c in get_subclasses(ClientException)) + +_error_dict = {c.status_code: c for c in get_subclasses(ClientException)} def raise_error(response, body, service_type=''): @@ -261,20 +291,14 @@ def raise_error(response, body, service_type=''): Return an instance of an ClientException or subclass based on an requests response. """ - ResponseParser = get_response_parser_class(service_type) + ResponseParser = get_response_parser_class(service_type) # noqa: N806 cls = _error_dict.get(str(response.status_code), ClientException) if body: res_p = ResponseParser(body) message = res_p.get_error_code() details = res_p.get_error_msg() data = res_p.get_status_body() - return cls(code=response.status_code, - message=message, - detail=details, - origin_data=data - ) - else: - return cls(code=response.status_code, - message=response.reason, - origin_data=body - ) + return cls( + code=response.status_code, message=message, detail=details, origin_data=data + ) + return cls(code=response.status_code, message=response.reason, origin_data=body) diff --git a/pyds8k/dataParser/base.py b/pyds8k/dataParser/base.py old mode 100755 new mode 100644 index f593023..3d36f8b --- a/pyds8k/dataParser/base.py +++ b/pyds8k/dataParser/base.py @@ -16,11 +16,8 @@ from abc import ABCMeta, abstractmethod -import six - -@six.add_metaclass(ABCMeta) -class BaseRequestParser(object): +class BaseRequestParser(metaclass=ABCMeta): # Parse the data user wants to send to server, # in the right format that server defined. @@ -33,12 +30,14 @@ def get_request_data(self): pass -@six.add_metaclass(ABCMeta) -class BaseResponseParser(object): +class BaseResponseParser(metaclass=ABCMeta): # Parser response data, to get resource link, representation, etc. response_key = '' - success_status = ('ok', 'successful', ) + success_status = ( + 'ok', + 'successful', + ) resource_data_key = 'data' error_status_key = 'status' diff --git a/pyds8k/dataParser/ds8k.py b/pyds8k/dataParser/ds8k.py old mode 100755 new mode 100644 index 5e7aa72..e8a0da0 --- a/pyds8k/dataParser/ds8k.py +++ b/pyds8k/dataParser/ds8k.py @@ -15,17 +15,22 @@ ############################################################################## from logging import getLogger -from pyds8k import PYDS8K_DEFAULT_LOGGER -from pyds8k.dataParser.base import BaseRequestParser, \ - BaseResponseParser -from pyds8k import messages -from pyds8k.exceptions import URLParseError, \ - RepresentationParseError, \ - IDMissingError + +from pyds8k import PYDS8K_DEFAULT_LOGGER, messages +from pyds8k.dataParser.base import BaseRequestParser, BaseResponseParser +from pyds8k.exceptions import ( + IDMissingError, + RepresentationNotFoundError, + RepresentationParseError, + URLParseError, +) logger = getLogger(PYDS8K_DEFAULT_LOGGER) -success_status = ('ok', 'successful', ) +success_status = ( + 'ok', + 'successful', +) class RequestParser(BaseRequestParser): @@ -37,7 +42,7 @@ class RequestParser(BaseRequestParser): param_key = 'params' def __init__(self, raw_data, resource_key=''): - if not (isinstance(raw_data, list) or isinstance(raw_data, dict)): + if not (isinstance(raw_data, (list, dict))): raise TypeError(messages.NEED_A_DICT_OR_DICT_LIST.format(raw_data)) self.raw_data = raw_data self.request_data = None @@ -82,7 +87,7 @@ def get_link(self, representation=None): elif self.representation: rep = self.representation else: - raise Exception(messages.REPRESENTATION_NOT_FOUND) + raise RepresentationNotFoundError return self.__class__.get_link_from_representation(rep) def get_representations(self): @@ -92,10 +97,12 @@ def get_representations(self): return parsed def get_posta_response_data(self): - MULTIFLAG = "responses" - data = self.raw_data.get(MULTIFLAG, self.raw_data) + multi_flag = "responses" + data = self.raw_data.get(multi_flag, self.raw_data) if not isinstance(data, list): - data = [data, ] + data = [ + data, + ] res = [] for s_data in data: res_status_body = s_data.get(self.status_key) @@ -108,9 +115,10 @@ def get_posta_response_data(self): res.append(({self.resource_data_key: None}, res_url)) else: res.append( - ({self.resource_data_key: self._parse_data(res_data)[0]}, # noqa - res_url - ) + ( + {self.resource_data_key: self._parse_data(res_data)[0]}, + res_url, + ) ) # return status part if something failed. else: @@ -144,29 +152,27 @@ def get_link_from_representation(cls, representation): url_objects = representation.get(cls.url_field) if not url_objects: return "" - url = cls._get_url(url_objects) - return url + return cls._get_url(url_objects) @classmethod - def get_resource_id_from_url(self, url, resource_type): - if url.endswith('/'): - url = url[:-1] + def get_resource_id_from_url(cls, url, resource_type): + url = url.removesuffix('/') url_frag = url.split('/') if len(url_frag) > 1 and url_frag[-2] == resource_type: return url_frag[-1] - logger.debug("Failed to get resource id from url {}".format(url)) - raise IDMissingError() + logger.debug(f"Failed to get resource id from url {url}") + raise IDMissingError @classmethod def _get_url(cls, urls): if isinstance(urls, str): return urls - elif isinstance(urls, dict): + if isinstance(urls, dict): urls = [urls] elif isinstance(urls, list): pass else: - raise URLParseError() + raise URLParseError for url in urls: if url.get('rel') == 'self': return url.get('href', '') @@ -179,11 +185,11 @@ def _parse_data(self, data): parsed = data.get(self.resource_key) if parsed is None: logger.debug( - "Failed to parse resource from data, return raw data: {}".format(data) # noqa + f"Failed to parse resource from data, return raw data: {data}" ) parsed = data else: - raise RepresentationParseError() + raise RepresentationParseError if not isinstance(parsed, list): parsed = [parsed] return parsed diff --git a/pyds8k/dateutil.py b/pyds8k/dateutil.py index ba942dc..dcb7afc 100644 --- a/pyds8k/dateutil.py +++ b/pyds8k/dateutil.py @@ -14,32 +14,26 @@ # limitations under the License. ############################################################################## -from datetime import tzinfo, timedelta, time import time as _time +from datetime import time, timedelta, tzinfo ZERO = timedelta(0) STDOFFSET = timedelta(seconds=-_time.timezone) -if _time.daylight: - DSTOFFSET = timedelta(seconds=-_time.altzone) -else: - DSTOFFSET = STDOFFSET +DSTOFFSET = timedelta(seconds=-_time.altzone) if _time.daylight else STDOFFSET DSTDIFF = DSTOFFSET - STDOFFSET FORMAT = '%Y-%m-%dT%H:%M:%S%Z' class LocalTimezone(tzinfo): - def utcoffset(self, dt): if self._isdst(dt): return DSTOFFSET - else: - return STDOFFSET + return STDOFFSET def dst(self, dt): if self._isdst(dt): return DSTDIFF - else: - return ZERO + return ZERO def tzname(self, dt): local_time_zone = int(self.utcoffset(dt).total_seconds()) / 60 @@ -53,9 +47,17 @@ def tzname(self, dt): return prefix + local_time.strftime('%H%M') def _isdst(self, dt): - tt = (dt.year, dt.month, dt.day, - dt.hour, dt.minute, dt.second, - dt.weekday(), 0, 0) + tt = ( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.weekday(), + 0, + 0, + ) stamp = _time.mktime(tt) tt = _time.localtime(stamp) return tt.tm_isdst > 0 diff --git a/pyds8k/example.py b/pyds8k/example.py index f1a2eb7..c07534e 100644 --- a/pyds8k/example.py +++ b/pyds8k/example.py @@ -4,36 +4,62 @@ from pyds8k.client.ds8k.v1.client import Client -# Connect storage array through SSH -restclient = Client(service_address='ip_address', user='username', - password='password') +# Connect to storage array through HTTPS +# Option verify is passed to requests. +# From requests documentation: +# Note that when verify is set to False, requests will accept any TLS +# certificate presented by the server, and will ignore hostname mismatches +# and/or expired certificates, which will make your application vulnerable +# to man-in-the-middle (MitM) attacks. Setting verify to False may be +# useful during local development or testing. +# verify=False can be used when the DS8K certificate is self-signed. +restclient = Client('ip_address or fqdn', 'username', 'password', verify=False) + +# Available functions are located in pyds8k/resources/ds8k/v1/common/mixins.py # Create volumes -restclient.create_volumes(name_col=["volume_name"], - cap="capacity_in_GiB", - pool="pool_id", - tp="none") +vol = restclient.create_volumes( + name_col=['volume_name'], cap='capacity_in_GiB', pool='pool_id', tp='none' +) + +# Delete volume +restclient.delete_volume(vol[0].id) -# Get Volume from storage +# Get volume vol = restclient.get_volumes('vol_id') -print(vol['name']) +print(vol.name) +print(vol.cap) -# Get all the volume from storage by lss +# Get volumes by lss lss_vols = restclient.get_volumes_by_lss('lss_number') for vol in lss_vols: - print(vol['name']) + print(vol.name) + print(vol.id) -# Get all the volumes from storage -vols = restclient.get_volumes() -for vol in vols: - print(vol['name']) +# Calculate allocated capacity by lss +virtual_total = 0 +allocated_total = 0 +for vol in lss_vols: + allocated_total += int(vol.real_cap) + virtual_total += int(vol.virtual_cap) + +print(f'{virtual_total=}') +print(f'{allocated_total=}') -# Get Pool from storage +# Get pool pool = restclient.get_pools('pool_id') -print(pool['name']) +print(vol.name) print(pool.eserep[0]) -# Getting all of the pools from storage +# Get all of the pools pools = restclient.get_pools() for p in pools: - print(p['name']) + print(p.name) + +# Get all the volumes +vols = [] +for p in pools: + vols.extend(restclient.get_volumes_by_pool(p.id)) + +for vol in vols: + print(vol.name) diff --git a/pyds8k/exceptions.py b/pyds8k/exceptions.py old mode 100755 new mode 100644 index 08ef2b1..9e6213e --- a/pyds8k/exceptions.py +++ b/pyds8k/exceptions.py @@ -18,9 +18,10 @@ Exception definitions. """ -from pyds8k.utils import get_subclasses, \ - get_response_parser_class +from http import HTTPStatus + from pyds8k import messages +from pyds8k.utils import get_response_parser_class, get_subclasses class BaseRestError(Exception): @@ -32,23 +33,29 @@ def __init__(self, reason): self.reason = reason def __str__(self): - return messages.INVALID_ARGUMENT.format( - self.reason - ) + return messages.INVALID_ARGUMENT.format(self.reason) + + +class InvalidMethodForCreate(Exception): + def __init__(self, method): + self.method = method + + def __str__(self): + return messages.INVALID_ARGUMINVALID_METHOD_FOR_CREATE.format(self.method) class OperationNotAllowed(Exception): """ The operation performed on the resource is not allowed. """ + def __init__(self, operation_name, resource_name=''): self.operation_name = operation_name self.resource_name = resource_name def __str__(self): return messages.OPERATION_NOT_ALLOWED.format( - self.operation_name, - self.resource_name + self.operation_name, self.resource_name ) @@ -56,6 +63,7 @@ class URLNotSpecifiedError(Exception): """ The URL is not specified. """ + def __str__(self): return messages.URL_NOT_SPECIFIED @@ -64,6 +72,7 @@ class URLMissingError(Exception): """ The URL is missing. """ + def __str__(self): return messages.URL_MISSING @@ -72,6 +81,7 @@ class IDMissingError(Exception): """ The id field is missing or None. """ + def __str__(self): return messages.ID_MISSING @@ -80,6 +90,7 @@ class ResponseBodyMissingError(Exception): """ The response body is missing. """ + def __str__(self): return messages.RESPONSE_BODY_MISSING @@ -88,14 +99,25 @@ class URLParseError(Exception): """ Can not get the URL """ + def __str__(self): return messages.CAN_NOT_GET_URL +class RepresentationNotFoundError(Exception): + """ + Can not find the representation + """ + + def __str__(self): + return messages.REPRESENTATION_NOT_FOUND + + class RepresentationParseError(Exception): """ Can not get the representation """ + def __str__(self): return messages.CAN_NOT_GET_REPRESENTATION @@ -112,11 +134,10 @@ def __str__(self): return messages.FIELD_READONLY.format(self.field_name) -class ConnectionError(Exception): +class ConnectionError(Exception): # noqa: A001 """ Could not open a connection to the API service. """ - pass class Timeout(Exception): @@ -135,30 +156,28 @@ class ClientException(Exception): """ The base exception class for all HTTP client or server errors. """ + def __init__(self, code, message=None, detail='', origin_data=None): self.code = code self.message = message self.detail = detail self.error_data = origin_data if self.message and self.detail: - self.details = '[{}] {}'.format(self.message, self.detail) + self.details = f'[{self.message}] {self.detail}' elif self.message or self.detail: self.details = self.message or self.detail else: self.details = '' def __str__(self): - return "HTTP {0} {1}. {2}".format( - self.code, - self.reason_phrase, - self.details - ) + return f"HTTP {self.code} {self.reason_phrase}. {self.details}" class ClientError(ClientException): """ HTTP 4xx - Client Error """ + status_code = '4xx' reason_phrase = "Client Error" @@ -167,6 +186,7 @@ class ServerError(ClientException): """ HTTP 5xx - Server Error """ + status_code = '5xx' reason_phrase = "Server Error" @@ -175,16 +195,18 @@ class BadRequest(ClientError): """ HTTP 400 - Bad request: you sent some malformed data. """ - status_code = '400' - reason_phrase = "Bad Request" + + status_code = str(HTTPStatus.BAD_REQUEST.value) + reason_phrase = HTTPStatus.BAD_REQUEST.phrase class Unauthorized(ClientError): """ HTTP 401 - Unauthorized: bad credentials. """ - status_code = '401' - reason_phrase = "Unauthorized" + + status_code = str(HTTPStatus.UNAUTHORIZED.value) + reason_phrase = HTTPStatus.UNAUTHORIZED.phrase class Forbidden(ClientError): @@ -192,40 +214,45 @@ class Forbidden(ClientError): HTTP 403 - Forbidden: your credentials don't give you access to this resource. """ - status_code = '403' - reason_phrase = "Forbidden" + + status_code = str(HTTPStatus.FORBIDDEN.value) + reason_phrase = HTTPStatus.FORBIDDEN.phrase class NotFound(ClientError): """ HTTP 404 - Not found """ - status_code = '404' - reason_phrase = "Not Found" + + status_code = str(HTTPStatus.NOT_FOUND.value) + reason_phrase = HTTPStatus.NOT_FOUND.phrase class MethodNotAllowed(ClientError): """ HTTP 405 - Method Not Allowed """ - status_code = '405' - reason_phrase = "Method Not Allowed" + + status_code = str(HTTPStatus.METHOD_NOT_ALLOWED.value) + reason_phrase = HTTPStatus.METHOD_NOT_ALLOWED.phrase class Conflict(ClientError): """ HTTP 409 - Conflict """ - status_code = '409' - reason_phrase = "Conflict" + + status_code = str(HTTPStatus.CONFLICT.value) + reason_phrase = HTTPStatus.CONFLICT.phrase class UnsupportedMediaType(ClientError): """ HTTP 415 - Unsupported Media Type """ - status_code = '415' - reason_phrase = "Unsupported Media Type" + + status_code = str(HTTPStatus.UNSUPPORTED_MEDIA_TYPE.value) + reason_phrase = HTTPStatus.UNSUPPORTED_MEDIA_TYPE.phrase class InternalServerError(ServerError): @@ -233,27 +260,30 @@ class InternalServerError(ServerError): HTTP 500 - Internal Server Error: The server encountered an unexpected condition which prevented it from fulfilling the request. """ - status_code = '500' - reason_phrase = "Internal Server Error" + + status_code = str(HTTPStatus.INTERNAL_SERVER_ERROR.value) + reason_phrase = HTTPStatus.INTERNAL_SERVER_ERROR.phrase class ServiceUnavailable(ServerError): """ HTTP 503 - Service Unavailable """ - status_code = '503' - reason_phrase = "Service Unavailable" + + status_code = str(HTTPStatus.SERVICE_UNAVAILABLE.value) + reason_phrase = HTTPStatus.SERVICE_UNAVAILABLE.phrase class GatewayTimeout(ServerError): """ HTTP 504 - Gateway Timeout """ - status_code = '504' - reason_phrase = "Gateway Timeout" + status_code = str(HTTPStatus.GATEWAY_TIMEOUT.value) + reason_phrase = HTTPStatus.GATEWAY_TIMEOUT.phrase -_error_dict = dict((c.status_code, c) for c in get_subclasses(ClientException)) + +_error_dict = {c.status_code: c for c in get_subclasses(ClientException)} def raise_error(response, body, service_type=''): @@ -261,20 +291,14 @@ def raise_error(response, body, service_type=''): Return an instance of an ClientException or subclass based on an requests response. """ - ResponseParser = get_response_parser_class(service_type) + ResponseParser = get_response_parser_class(service_type) # noqa: N806 cls = _error_dict.get(str(response.status_code), ClientException) if body: res_p = ResponseParser(body) message = res_p.get_error_code() details = res_p.get_error_msg() data = res_p.get_status_body() - return cls(code=response.status_code, - message=message, - detail=details, - origin_data=data - ) - else: - return cls(code=response.status_code, - message=response.reason, - origin_data=body - ) + return cls( + code=response.status_code, message=message, detail=details, origin_data=data + ) + return cls(code=response.status_code, message=response.reason, origin_data=body) diff --git a/pyds8k/httpclient.py b/pyds8k/httpclient.py old mode 100755 new mode 100644 index d7d8a49..d7c87e7 --- a/pyds8k/httpclient.py +++ b/pyds8k/httpclient.py @@ -14,18 +14,18 @@ # limitations under the License. ############################################################################## +from http import HTTPStatus from logging import getLogger -from pyds8k import PYDS8K_DEFAULT_LOGGER -from pyds8k import exceptions + import requests -from requests.packages.urllib3.exceptions import InsecureRequestWarning -from requests.packages.urllib3 import disable_warnings from requests.exceptions import Timeout +from requests.packages.urllib3 import disable_warnings +from requests.packages.urllib3.exceptions import InsecureRequestWarning + +from pyds8k import PYDS8K_DEFAULT_LOGGER, exceptions from pyds8k.auth.authenticate import get_authenticate +from pyds8k.messages import CONNECTION_ERROR, REAUTH_SERVER, REDIRECTING from pyds8k.utils import is_absolute_url -from pyds8k.messages import CONNECTION_ERROR, \ - REAUTH_SERVER, \ - REDIRECTING try: import json @@ -40,7 +40,7 @@ disable_warnings(InsecureRequestWarning) -class HTTPClient(object): +class HTTPClient: """ An HTTP client interacting with RESTAPI web service. @@ -78,21 +78,26 @@ class HTTPClient(object): """ USER_AGENT = 'python-restclient' - DefaultHeaders = {'User-Agent': USER_AGENT, - 'Accept': 'application/json', - } - - def __init__(self, service_address, user, password, - service_type, - service_version=DEFAULT_SERVICE_VERSION, - port=None, - hostname=None, - secure=True, - timeout=DEFAULT_TIMEOUT_SEC, - default_headers=None, - cert=None, - verify=True - ): + DefaultHeaders = { + 'User-Agent': USER_AGENT, + 'Accept': 'application/json', + } + + def __init__( + self, + service_address, + user, + password, + service_type, + service_version=DEFAULT_SERVICE_VERSION, + port=None, + hostname=None, + secure=True, + timeout=DEFAULT_TIMEOUT_SEC, + default_headers=None, + cert=None, + verify=True, + ): self.user = user self.password = password self.service_type = service_type @@ -111,17 +116,17 @@ def __init__(self, service_address, user, password, list_uri = list_uri[1:] # if no schema provide, default secure as True set schema to https - self.schema = self.schema or secure and "https" or "http" + self.schema = self.schema or (secure and "https") or "http" prefix_http = f"{self.schema}://" - list_uri[0] = list_uri[0].lstrip("//") - if len(list_uri) > 1 and "/" != list_uri[1][0]: + list_uri[0] = list_uri[0].removeprefix("//") + if len(list_uri) > 1 and list_uri[1][0] != "/": # found embedded port self.port = int(list_uri[1].split('/')[0]) url_service_point = ":".join(list_uri) elif len(list_uri) == 1: # no port in service address, add port if defined port is not 80 - if "80" != self.port: + if self.port != "80": list_seg_sp = list_uri[0].split('/') list_seg_sp[0] = f"{list_seg_sp[0]}:{self.port}" url_service_point = "/".join(list_seg_sp) @@ -133,17 +138,16 @@ def __init__(self, service_address, user, password, self.service_address = f"{prefix_http}{'/'.join(list_uri)}" self.domain = f"{prefix_http}{list_uri[0]}" self.base_url = f"/{'/'.join(list_uri[1:])}" - self.verify = verify if "https" == self.schema else False + self.verify = verify if self.schema == "https" else False self.cert = cert self.timeout = timeout self.defaultHeaders = self.DefaultHeaders.copy() - self.defaultHeaders = dict() + self.defaultHeaders = {} if default_headers is not None and isinstance(default_headers, dict): self.defaultHeaders.update(default_headers) self.defaultQuerystrings = {} self.authenticate = get_authenticate( - service_type=self.service_type, - service_version=self.service_version + service_type=self.service_type, service_version=self.service_version ) self.session = requests.session() @@ -152,15 +156,12 @@ def log_req(cls, args, kwargs): string_parts = ['curl -i'] for element in args: if element in ('GET', 'POST', 'DELETE', 'PUT', 'PATCH'): - string_parts.append(' -X {}'.format(element)) + string_parts.append(f' -X {element}') else: - string_parts.append(' {}'.format(element)) + string_parts.append(f' {element}') for element in kwargs['headers']: - header = ' -H "{0}: {1}"'.format( - element, - kwargs['headers'][element] - ) + header = ' -H "{}: {}"'.format(element, kwargs['headers'][element]) string_parts.append(header) if 'data' in kwargs: @@ -170,16 +171,11 @@ def log_req(cls, args, kwargs): @classmethod def log_resp(cls, resp): logger.debug( - "\nRESP: [{0}] {1}\nRESP BODY: {2}\n".format( - resp.status_code, - resp.headers, - resp.text - ) + f"\nRESP: [{resp.status_code}] {resp.headers}\nRESP BODY: {resp.text}\n" ) - def request(self, url, method, **kwargs): + def request(self, url, method, **kwargs): # noqa: C901 log_required = True - url = url headers = kwargs.get('headers', {}).copy() with_http_headers = kwargs.get('with_http_headers', {}) @@ -203,20 +199,13 @@ def request(self, url, method, **kwargs): if self.authenticate.get_auth_url() in url: attempts += 1 log_required = False - absolute_url = url if is_absolute_url(url) \ - else self.service_address + url + absolute_url = url if is_absolute_url(url) else self.service_address + url if log_required: - self.log_req( - (absolute_url, method,), - kwargs - ) + self.log_req((absolute_url, method), kwargs) try: resp = self.session.request( - method, - absolute_url, - verify=self.verify, - cert=self.cert, - **kwargs) + method, absolute_url, verify=self.verify, cert=self.cert, **kwargs + ) self.log_resp(resp) if resp.text: try: @@ -227,32 +216,33 @@ def request(self, url, method, **kwargs): body = None # Requests will deal with redirect automatically, code here is # not needed. You can set allow_redirects=False to disable it. - if resp.status_code == 301: + if resp.status_code == HTTPStatus.MOVED_PERMANENTLY: old_url = url link = self._get_uri_from_location(resp) url = self._parse_url(link) logger.info(REDIRECTING.format(old_url, url)) continue - if resp.status_code >= 400: + if resp.status_code >= HTTPStatus.BAD_REQUEST: raise exceptions.raise_error(resp, body, self.service_type) - return resp, body - except exceptions.Unauthorized as e: + except exceptions.Unauthorized: if attempts > 0: - raise e + raise logger.debug(REAUTH_SERVER) attempts += 1 self.authenticate.authenticate(self) continue - except exceptions.BadRequest as e: - raise e - except requests.exceptions.ConnectionError as e: - logger.error(f"Error When Requesting Url: {absolute_url}") - raise exceptions.ConnectionError(CONNECTION_ERROR.format(e)) - except Timeout as e: - logger.debug(e) - raise exceptions.Timeout(absolute_url) + except exceptions.BadRequest: + raise + except requests.exceptions.ConnectionError as exc: + logger.exception(f"Error When Requesting Url: {absolute_url}") + raise exceptions.ConnectionError(CONNECTION_ERROR.format(exc)) from exc + except Timeout as exc: + logger.debug(exc) + raise exceptions.Timeout(absolute_url) from exc + else: + return resp, body def get(self, url, **kwargs): # logger.info('getting {}'.format(url)) @@ -270,23 +260,23 @@ def patch(self, url, **kwargs): def delete(self, url, **kwargs): return self.request(url, 'DELETE', **kwargs) - def set_defaultQuerystrings(self, key, value): + def set_defaultQuerystrings(self, key, value): # noqa: N802 self.defaultQuerystrings[key] = value - def set_defaultHeaders(self, key, value): + def set_defaultHeaders(self, key, value): # noqa: N802 self.defaultHeaders[key] = value - def set_defaultHttpFields(self): + def set_defaultHttpFields(self): # noqa: N802 pass def _get_uri_from_location(self, resp): link = resp.headers.get('Location') if not link: - raise exceptions.URLParseError() + raise exceptions.URLParseError return link def _parse_url(self, url): - schma = '{}:'.format(self.schema) + schma = f'{self.schema}:' if '//' in url: schma, url1 = url.split('//') else: @@ -294,7 +284,6 @@ def _parse_url(self, url): domain, url2 = url1.split('/', 1) if not domain: return url - elif schma + '//' + domain == self.domain: + if schma + '//' + domain == self.domain: return '/' + url2 - else: - raise exceptions.URLParseError() + raise exceptions.URLParseError diff --git a/pyds8k/messages.py b/pyds8k/messages.py old mode 100755 new mode 100644 index 858ee49..d0a3a11 --- a/pyds8k/messages.py +++ b/pyds8k/messages.py @@ -17,18 +17,21 @@ # ============================================================================== # restclient.exceptions # ============================================================================== -OPERATION_NOT_ALLOWED = 'OperationNotAllowed: the {} operation you performed \ -on resource {} is not allowed.' +OPERATION_NOT_ALLOWED = ( + 'OperationNotAllowed: the {} operation you performed on resource {} is not allowed.' +) URL_NOT_SPECIFIED = 'The URL is missing, you must specify a valid URL here.' URL_MISSING = 'Can not get URL, the URL here is missing.' ID_MISSING = 'The id field of current resource is missing or not specified.' FIELD_READONLY = 'The field {} is read only.' CAN_NOT_GET_URL = 'Can not get URL.' RESPONSE_BODY_MISSING = 'The response has no content.' -CAN_NOT_GET_REPRESENTATION = 'Can not get the requested resource from \ -returned data.' +CAN_NOT_GET_REPRESENTATION = 'Can not get the requested resource from returned data.' REQUEST_TIMED_OUT = 'Connection to {} timed out.' INVALID_ARGUMENT = 'InvalidArgument: {}' +INVALID_METHOD_FOR_CREATE = ( + "{} method cannot be used to create a new resource. Use POSTA or PUT." +) # ============================================================================== # restclient.utils @@ -51,10 +54,7 @@ # ============================================================================== CAN_NOT_GET_STATUS_BODY = 'Can not get the status body in {} {} response, \ will return the original response body.' -DEFAULT_SUCCESS_BODY_DICT = { - 'status': 'ok', - 'message': 'Operation done successfully.' -} +DEFAULT_SUCCESS_BODY_DICT = {'status': 'ok', 'message': 'Operation done successfully.'} DEFAULT_FAIL_BODY_JSON = '{{"status": "failed", "message": \ "Can not {action} {res_class} {res_id}"}}' SET_RELATED_RESOURCE_FAILED = 'Can not set {} during loading {}' @@ -73,9 +73,7 @@ # ============================================================================== INVALID_TYPE = 'Invalid type you specified, expected one of: {}.' INVALID_LSS_TYPE = 'Invalid lss type. Expected one of: {}.' -INVALID_NAME = \ - r'Invalid volume name. The right format is: {nameprefix}_{volume_id}' -INVALID_POOL_NAME = \ - r'Invalid pool name. The right format is: {pool_name}_{pool_id}' +INVALID_NAME = r'Invalid volume name. The right format is: {nameprefix}_{volume_id}' +INVALID_POOL_NAME = r'Invalid pool name. The right format is: {pool_name}_{pool_id}' ITEM_IN_LIST = '{} [{}] are already existed.' ITEM_NOT_IN_LIST = '{0} [{1}] are not in the current {0}.' diff --git a/pyds8k/resources/ds8k/v1/__init__.py b/pyds8k/resources/ds8k/v1/__init__.py old mode 100755 new mode 100644 index 6ef8f0b..ee70685 --- a/pyds8k/resources/ds8k/v1/__init__.py +++ b/pyds8k/resources/ds8k/v1/__init__.py @@ -14,30 +14,56 @@ # limitations under the License. ############################################################################## -from . import ioports, flashcopy, events, mappings, pprc, eserep, \ - users, systems, nodes, marrays, encryption_groups, io_enclosures, \ - pools, tserep, lss, volumes, host_ports, hosts -from .cs import pprcs, flashcopies +from . import ( + encryption_groups, + eserep, + events, + flashcopy, + host_ports, + hosts, + io_enclosures, + ioports, + lss, + mappings, + marrays, + nodes, + pools, + pprc, + resource_groups, + systems, + tserep, + users, + volumes, +) +from .cs import flashcopies, pprcs +from .hmc import hmc, restart +from .hmc.certificate import certificate, csr, selfsigned __all__ = ( - 'ioports', - 'flashcopy', - 'flashcopies', + 'certificate', + 'csr', + 'encryption_groups', + 'eserep', 'events', + 'flashcopies', + 'flashcopy', + 'hmc', + 'host_ports', + 'hosts', + 'io_enclosures', + 'ioports', + 'lss', 'mappings', + 'marrays', + 'nodes', + 'pools', 'pprc', - 'eserep', 'pprcs', - 'users', + 'resource_groups', + 'restart', + 'selfsigned', 'systems', - 'nodes', - 'marrays', - 'encryption_groups', - 'io_enclosures', - 'pools', 'tserep', - 'lss', + 'users', 'volumes', - 'host_ports', - 'hosts' ) diff --git a/pyds8k/resources/ds8k/v1/common/attr_names.py b/pyds8k/resources/ds8k/v1/common/attr_names.py old mode 100755 new mode 100644 index ac1ed2c..33923f5 --- a/pyds8k/resources/ds8k/v1/common/attr_names.py +++ b/pyds8k/resources/ds8k/v1/common/attr_names.py @@ -15,7 +15,7 @@ ############################################################################## # system -SYSTEM_ID = 'id' # 2107-{sn} +SYSTEM_ID = 'id' # 2107-{sn} SYSTEM_CODE_LEVEL = 'bundle' SYSTEM_NAME = 'name' SYSTEM_MODEL = 'MTM' diff --git a/pyds8k/resources/ds8k/v1/common/base.py b/pyds8k/resources/ds8k/v1/common/base.py old mode 100755 new mode 100644 index e04c414..8771cbf --- a/pyds8k/resources/ds8k/v1/common/base.py +++ b/pyds8k/resources/ds8k/v1/common/base.py @@ -17,15 +17,16 @@ """ DS8K resources base interface. """ + +import contextlib from logging import getLogger -from pyds8k.messages import INVALID_TYPE + from pyds8k import PYDS8K_DEFAULT_LOGGER -from pyds8k.base import Resource, Manager -from pyds8k.base import get_resource_and_manager_class_by_route +from pyds8k.base import Manager, Resource, get_resource_and_manager_class_by_route +from pyds8k.exceptions import FieldReadOnly, OperationNotAllowed, URLNotSpecifiedError +from pyds8k.messages import INVALID_TYPE + from .mixins import RootResourceMixin -from pyds8k.exceptions import OperationNotAllowed, \ - URLNotSpecifiedError, \ - FieldReadOnly logger = getLogger(PYDS8K_DEFAULT_LOGGER) @@ -45,7 +46,7 @@ class Base(RootResourceMixin, Resource): related_resources_collection = () def _add_details(self, info, force=False): - super(Base, self)._add_details(info, force=force) + super()._add_details(info, force=force) self._start_updating() self._set_related_resources_collection() self._stop_updating() @@ -57,23 +58,21 @@ def _set_related_resources_collection(self): # will empty them and wait for lazy-loading. if not isinstance(res, list): self.representation[key] = '' - try: + with contextlib.suppress(AttributeError): delattr(self, key) - except AttributeError: - pass # If the related resources(should be a list) are in info, set it. else: re_class, re_manager = self._get_resource_class_by_name(key) - res_list = [re_class(self.client, - manager=re_manager(self.client), - info=r) - for r in res] + res_list = [ + re_class(self.client, manager=re_manager(self.client), info=r) + for r in res + ] setattr(self, key, res_list) def __setattr__(self, key, value): if key in self.readonly_fileds and not self.is_updating(): raise FieldReadOnly(key) - super(Base, self).__setattr__(key, value) + super().__setattr__(key, value) try: if key in self.related_resources_collection: ids = [getattr(item, item.id_field) for item in value] @@ -86,32 +85,22 @@ def __setattr__(self, key, value): def __getattr__(self, key): if key in self.related_resources_collection: try: - return getattr(self, 'get_{}'.format(key))() - except Exception as e: - logger.debug( - "Can not get {} from {}, reason is: {}".format( - key, self, type(e) - ) - ) - raise AttributeError(key) - return super(Base, self).__getattr__(key) + return getattr(self, f'get_{key}')() + except Exception as exc: + logger.debug(f"Can not get {key} from {self}, reason is: {type(exc)}") + raise AttributeError(key) from exc + return super().__getattr__(key) def __repr__(self): - return "<{0}: {1}>".format(self.__class__.__name__, self._get_id()) + return f"<{self.__class__.__name__}: {self._get_id()}>" def _get_resource_class_by_name(self, resource_type): - prefix = '{}.{}'.format(self.client.service_type, - self.client.service_version - ) - return get_resource_and_manager_class_by_route( - "{}.{}".format(prefix, resource_type) - ) + prefix = f'{self.client.service_type}.{self.client.service_version}' + return get_resource_and_manager_class_by_route(f"{prefix}.{resource_type}") def _verify_type(self, new_type, valid_type_list): - if new_type and not (new_type in valid_type_list): - raise ValueError( - INVALID_TYPE.format(', '.join(valid_type_list)) - ) + if new_type and new_type not in valid_type_list: + raise ValueError(INVALID_TYPE.format(', '.join(valid_type_list))) class SingletonBase(Base): @@ -135,10 +124,10 @@ def _post(self, url='', body=None): # if key not in self.managed_object.readonly_fileds # } else: - raise URLNotSpecifiedError() + raise URLNotSpecifiedError else: post_body = body - return super(BaseManager, self)._post(url=url, body=post_body) + return super()._post(url=url, body=post_body) # DS8K will use PUT in PATCH way, and don't use PATCH. def _put(self, url='', body=None): @@ -147,16 +136,15 @@ def _put(self, url='', body=None): if self.managed_object is not None: self.url = self.managed_object.url # use modified info here - put_body = body if body else \ - self.managed_object._get_modified_info_dict() + put_body = ( + body if body else self.managed_object._get_modified_info_dict() + ) else: - raise URLNotSpecifiedError() + raise URLNotSpecifiedError else: self.url = url put_body = body - resp, body = self.client.put(self.url, - body=self._get_request_data(put_body) - ) + resp, body = self.client.put(self.url, body=self._get_request_data(put_body)) data = self._get_data(body, method='PUT', response=resp) return resp, data @@ -164,39 +152,45 @@ def _patch(self, url='', body=None): return self._put(url=url, body=body) def get(self, resource_id='', url='', obj_class=None, **kwargs): - raise OperationNotAllowed('get', self.resource_class.__name__) + msg = 'get' + raise OperationNotAllowed(msg, self.resource_class.__name__) def list(self, url='', obj_class=None, body=None, **kwargs): - raise OperationNotAllowed('list', self.resource_class.__name__) + msg = 'list' + raise OperationNotAllowed(msg, self.resource_class.__name__) def post(self, url='', body=None): - raise OperationNotAllowed('post', self.resource_class.__name__) + msg = 'post' + raise OperationNotAllowed(msg, self.resource_class.__name__) def posta(self, url='', body=None): - raise OperationNotAllowed('posta', self.resource_class.__name__) + msg = 'posta' + raise OperationNotAllowed(msg, self.resource_class.__name__) def put(self, url='', body=None): - raise OperationNotAllowed('put', self.resource_class.__name__) + msg = 'put' + raise OperationNotAllowed(msg, self.resource_class.__name__) def patch(self, url='', body=None): - raise OperationNotAllowed('patch', self.resource_class.__name__) + msg = 'patch' + raise OperationNotAllowed(msg, self.resource_class.__name__) def delete(self, url=''): - raise OperationNotAllowed('delete', self.resource_class.__name__) + msg = 'delete' + raise OperationNotAllowed(msg, self.resource_class.__name__) class ReadOnlyManager(BaseManager): - def get(self, resource_id='', url='', obj_class=None, **kwargs): - return self._get(resource_id=resource_id, - url=url, obj_class=obj_class, **kwargs) + return self._get( + resource_id=resource_id, url=url, obj_class=obj_class, **kwargs + ) def list(self, url='', obj_class=None, body=None, **kwargs): return self._list(url=url, obj_class=obj_class, body=body, **kwargs) class SingletonBaseManager(BaseManager): - def get(self, url='', obj_class=None, **kwargs): return self._get(url=url, obj_class=obj_class, **kwargs) diff --git a/pyds8k/resources/ds8k/v1/common/mixins.py b/pyds8k/resources/ds8k/v1/common/mixins.py old mode 100755 new mode 100644 index 5bf1e56..b6e7054 --- a/pyds8k/resources/ds8k/v1/common/mixins.py +++ b/pyds8k/resources/ds8k/v1/common/mixins.py @@ -15,16 +15,17 @@ ############################################################################## from datetime import datetime -from . import types -from pyds8k.messages import INVALID_TYPE -from pyds8k.exceptions import IDMissingError, \ - InvalidArgumentError + from pyds8k.dateutil import LocalTimezone +from pyds8k.exceptions import IDMissingError, InvalidArgumentError +from pyds8k.messages import INVALID_TYPE + +from . import types FORMAT = '%Y-%m-%dT%H:%M:%S%Z' -class RootBaseMixin(object): +class RootBaseMixin: pass @@ -36,7 +37,7 @@ class RootBaseMixin(object): # lss and pprc do not have singular form. -class RootSystemMixin(object): +class RootSystemMixin: def get_systems(self): """ Get DS8000 System Object @@ -49,7 +50,7 @@ def get_systems(self): return self.all(types.DS8K_SYSTEM, rebuild_url=True).list() -class RootNodeMixin(object): +class RootNodeMixin: def get_nodes(self, node_id=None): """ Get nodes @@ -79,7 +80,7 @@ def get_node(self, node_id): return self.one(types.DS8K_NODE, node_id, rebuild_url=True).get() -class RootMarrayMixin(object): +class RootMarrayMixin: def get_marrays(self, marray_id=None): """ Get managed arrays @@ -110,7 +111,7 @@ def get_marray(self, marray_id): return self.one(types.DS8K_MARRAY, marray_id, rebuild_url=True).get() -class RootUserMixin(object): +class RootUserMixin: def get_users(self, user_name=None): """ Get users. @@ -140,7 +141,7 @@ def get_user(self, user_name): return self.one(types.DS8K_USER, user_name, rebuild_url=True).get() -class RootIOEnclosureMixin(object): +class RootIOEnclosureMixin: def get_io_enclosures(self, enclosure_id=None): """ Get IO Enclosures. @@ -169,12 +170,10 @@ def get_io_enclosure(self, enclosure_id): :py:class:`pyds8k.resources.ds8k.v1.io_enclosures.IOEnclosure`. """ - return self.one(types.DS8K_IOENCLOSURE, - enclosure_id, - rebuild_url=True).get() + return self.one(types.DS8K_IOENCLOSURE, enclosure_id, rebuild_url=True).get() -class RootEncryptionGroupMixin(object): +class RootEncryptionGroupMixin: def get_encryption_groups(self, group_id=None): """ Get Encryption Groups. @@ -203,12 +202,10 @@ def get_encryption_group(self, group_id): :py:class:`pyds8k.resources.ds8k.v1.encryption_groups.EncryptionGroup`. """ - return self.one(types.DS8K_ENCRYPTION_GROUP, - group_id, - rebuild_url=True).get() + return self.one(types.DS8K_ENCRYPTION_GROUP, group_id, rebuild_url=True).get() -class RootPoolMixin(object): +class RootPoolMixin: def get_pools(self, pool_id=None): """ Get Extent Pools @@ -238,65 +235,65 @@ def get_pool(self, pool_id): return self.one(types.DS8K_POOL, pool_id, rebuild_url=True).get() def get_tserep_by_pool(self, pool_id): - return self.one(types.DS8K_POOL, - pool_id, - rebuild_url=True - ).all(types.DS8K_TSEREP).get() + return ( + self.one(types.DS8K_POOL, pool_id, rebuild_url=True) + .all(types.DS8K_TSEREP) + .get() + ) def get_eserep_by_pool(self, pool_id): - return self.one(types.DS8K_POOL, - pool_id, - rebuild_url=True - ).all(types.DS8K_ESEREP).get() + return ( + self.one(types.DS8K_POOL, pool_id, rebuild_url=True) + .all(types.DS8K_ESEREP) + .get() + ) def delete_tserep_by_pool(self, pool_id): - _, res = self.one(types.DS8K_POOL, - pool_id, - rebuild_url=True - ).all(types.DS8K_TSEREP).delete() + _, res = ( + self.one(types.DS8K_POOL, pool_id, rebuild_url=True) + .all(types.DS8K_TSEREP) + .delete() + ) return res def delete_eserep_by_pool(self, pool_id): - _, res = self.one(types.DS8K_POOL, - pool_id, - rebuild_url=True - ).all(types.DS8K_ESEREP).delete() + _, res = ( + self.one(types.DS8K_POOL, pool_id, rebuild_url=True) + .all(types.DS8K_ESEREP) + .delete() + ) return res def update_tserep_cap_by_pool(self, pool_id, cap, captype=''): - _, res = self.one(types.DS8K_POOL, - pool_id, - rebuild_url=True - ).all( - types.DS8K_TSEREP - ).update({'cap': cap, 'captype': captype}) + _, res = ( + self.one(types.DS8K_POOL, pool_id, rebuild_url=True) + .all(types.DS8K_TSEREP) + .update({'cap': cap, 'captype': captype}) + ) return res def update_eserep_cap_by_pool(self, pool_id, cap, captype=''): - _, res = self.one(types.DS8K_POOL, - pool_id, - rebuild_url=True - ).all( - types.DS8K_ESEREP - ).update({'cap': cap, 'captype': captype}) + _, res = ( + self.one(types.DS8K_POOL, pool_id, rebuild_url=True) + .all(types.DS8K_ESEREP) + .update({'cap': cap, 'captype': captype}) + ) return res def update_tserep_threshold_by_pool(self, pool_id, threshold): - _, res = self.one(types.DS8K_POOL, - pool_id, - rebuild_url=True - ).all( - types.DS8K_TSEREP - ).update({'threshold': threshold}) + _, res = ( + self.one(types.DS8K_POOL, pool_id, rebuild_url=True) + .all(types.DS8K_TSEREP) + .update({'threshold': threshold}) + ) return res def update_eserep_threshold_by_pool(self, pool_id, threshold): - _, res = self.one(types.DS8K_POOL, - pool_id, - rebuild_url=True - ).all( - types.DS8K_ESEREP - ).update({'threshold': threshold}) + _, res = ( + self.one(types.DS8K_POOL, pool_id, rebuild_url=True) + .all(types.DS8K_ESEREP) + .update({'threshold': threshold}) + ) return res def get_volumes_by_pool(self, pool_id): @@ -311,13 +308,139 @@ def get_volumes_by_pool(self, pool_id): :py:class:`pyds8k.resources.ds8k.v1.volumes.Volume`. """ - return self.one(types.DS8K_POOL, - pool_id, - rebuild_url=True - ).all(types.DS8K_VOLUME).list() + return ( + self.one(types.DS8K_POOL, pool_id, rebuild_url=True) + .all(types.DS8K_VOLUME) + .list() + ) + +class RootResourceGroupMixin: + def get_resource_groups(self, resource_group_id=None): + """ + Get Resource Groups -class RootVolumeMixin(object): + Args: + resource_group_id (str): id of the target resource group. + Get all if none. + + Returns: + list: A list of + :py:class:`pyds8k.resources.ds8k.v1.resource_groups.ResourceGroup`. + + """ + if resource_group_id: + return self.get_resource_group(resource_group_id) + return self.all(types.DS8K_RESOURCE_GROUP, rebuild_url=True).list() + + def get_resource_group(self, resource_group_id): + """ + Get a Resource Group + + Args: + resource_group_id (str): Required. id of the target resource group. + + Returns: + object: + :py:class:`pyds8k.resources.ds8k.v1.resource_groups.ResourceGroup`. + + """ + return self.one( + types.DS8K_RESOURCE_GROUP, resource_group_id, rebuild_url=True + ).get() + + def delete_resource_group(self, resource_group_id): + """ + Delete a Resource Group + + Args: + resource_group_id (str): Required. id of the target resource group. + + Returns: + tuple: tuple of DS8000 Server Response. + + """ + return self.one( + types.DS8K_RESOURCE_GROUP, resource_group_id, rebuild_url=True + ).delete() + + def create_resource_group( + self, + label, + name='', + resource_group_id='', + ): + """ + Create one Resource Group + + Args: + label (str): Required. + The label for the resource group to be created. + name (str): The name for the resource group to be created. + resource_group_id (str): The resource group id to be created. + + Returns: + object: + :py:class:`pyds8k.resources.ds8k.v1.resource_groups.ResourceGroup`. + + """ + _, res = self.all(types.DS8K_RESOURCE_GROUP, rebuild_url=True).posta( + { + 'label': label, + 'id': resource_group_id, + 'name': name, + } + ) + return res + + def update_resource_group( + self, + resource_group_id, + label='', + name='', + cs_global='', + pass_global='', + gm_masters='', + gm_sessions='', + ): + """ + Update one Resource Group + + Args: + resource_group_id (str): Required. + The resource group id to be updated. + label (str): The label to assign to the resource group. + name (str): The name to assign to the resource group. + cs_global (str): The resource group label to associate with the + Copy Services Global Resource Scope. + pass_global (str): The resource group label to associate with the + Pass-thru Global Copy Services Resource Scope. + gm_masters (list): An list of Global Mirror session IDs allowed + to be used as a master session for volumes + in this resource. + gm_sessions (list): A list of Global Mirror session IDs allowed + to be used for the volumes in this resource. + + Returns: + tuple: tuple of DS8000 Server Response. + + """ + _, res = self.one( + types.DS8K_RESOURCE_GROUP, resource_group_id, rebuild_url=True + ).update( + { + 'label': label, + 'name': name, + 'cs_global': cs_global, + 'pass_global': pass_global, + 'gm_masters': gm_masters, + 'gm_sessions': gm_sessions, + } + ) + return res + + +class RootVolumeMixin: def get_volumes(self, volume_id=None): """ Get Volumes @@ -358,20 +481,20 @@ def delete_volume(self, volume_id): tuple: tuple of DS8000 Server Response. """ - _, res = self.one(types.DS8K_VOLUME, - volume_id, - rebuild_url=True).delete() + _, res = self.one(types.DS8K_VOLUME, volume_id, rebuild_url=True).delete() return res def create_volume( - self, - name, - cap, - pool, - stgtype=types.DS8K_VOLUME_TYPE_FB, - captype=types.DS8K_CAPTYPE_GIB, - lss='', - tp=''): + self, + name, + cap, + pool, + stgtype=types.DS8K_VOLUME_TYPE_FB, + captype=types.DS8K_CAPTYPE_GIB, + lss='', + tp='', + id='', # noqa: A002 + ): """ Create One Volume @@ -386,6 +509,7 @@ def create_volume( lss (str): logical subsystem id tp (str): storage allocation method, valid options include `none`, `ese`, `tse` + id (str): volume id to be created Returns: object: :py:class:`pyds8k.resources.ds8k.v1.volumes.Volume`. @@ -394,30 +518,33 @@ def create_volume( self._verify_type(captype, types.DS8K_CAPTYPES) self._verify_type(stgtype, types.DS8K_VOLUME_TYPES) self._verify_type(tp, types.DS8K_TPS) - _, res = self.all(types.DS8K_VOLUME, - rebuild_url=True - ).posta({'name': name, - 'cap': cap, - 'captype': captype, - 'stgtype': stgtype, - 'pool': pool, - 'lss': lss, - 'tp': tp, - } - ) + _, res = self.all(types.DS8K_VOLUME, rebuild_url=True).posta( + { + 'name': name, + 'cap': cap, + 'captype': captype, + 'stgtype': stgtype, + 'pool': pool, + 'lss': lss, + 'tp': tp, + 'id': id, + } + ) return res def create_volumes( - self, - name_col, - cap, - pool, - name='', - quantity='', - stgtype=types.DS8K_VOLUME_TYPE_FB, - captype=types.DS8K_CAPTYPE_GIB, - lss='', - tp=''): + self, + name_col, + cap, + pool, + name='', + quantity='', + stgtype=types.DS8K_VOLUME_TYPE_FB, + captype=types.DS8K_CAPTYPE_GIB, + lss='', + tp='', + ids=None, + ): """ Create a group of volumes with different names @@ -435,6 +562,7 @@ def create_volumes( lss (str): logical subsystem id tp (str): storage allocation method, in ``none``, ``ese``, ``tse`` + ids (list): list of volume ids to be created. Returns: list: A list of @@ -444,24 +572,59 @@ def create_volumes( self._verify_type(captype, types.DS8K_CAPTYPES) self._verify_type(stgtype, types.DS8K_VOLUME_TYPES) self._verify_type(tp, types.DS8K_TPS) - _, res = self.all(types.DS8K_VOLUME, - rebuild_url=True - ).posta({'name': name, - 'namecol': name_col if name_col else None, - 'quantity': quantity, - 'cap': cap, - 'captype': captype, - 'stgtype': stgtype, - 'pool': pool, - 'lss': lss, - 'tp': tp, - } - ) + _, res = self.all(types.DS8K_VOLUME, rebuild_url=True).posta( + { + 'name': name, + 'namecol': name_col if name_col else None, + 'quantity': quantity, + 'cap': cap, + 'captype': captype, + 'stgtype': stgtype, + 'pool': pool, + 'lss': lss, + 'tp': tp, + 'ids': ids, + } + ) + return res + + def create_alias_volumes( + self, + id, # noqa: A002 + ckd_base_ids, + quantity='', + alias_create_order='decrement', + ): + """ + Create ckd alias volumes for a list of base ckd volumes + + Args: + id (str): the starting volume id for where aliases + should be created + ckd_base_ids (list): list of ckd base ids aliases + will be created for + quantity (str): number of aliases per ckd base id to + create, number in str + alias_create_order (str): whether to ``increment`` + or ``decrement`` from starting id + default ``decrement`` + Returns: + list: A list of + :py:class:`pyds8k.resources.ds8k.v1.volumes.Volume`. + + """ + _, res = self.all(types.DS8K_VOLUME, rebuild_url=True).posta( + { + 'id': id, + 'quantity': quantity, + 'alias': 'true', + 'alias_create_order': alias_create_order, + 'ckd_base_ids': ckd_base_ids, + } + ) return res - def create_volume_ckd(self, name, cap, pool, - captype='', lss='', tp='', - ): + def create_volume_ckd(self, name, cap, pool, captype='', lss='', tp='', id=''): # noqa: A002 """ Create One CKD Volume @@ -474,6 +637,7 @@ def create_volume_ckd(self, name, cap, pool, lss (str): logical subsystem id tp (str): storage allocation method, valid options include `none`, `ese`, `tse` + id (str): volume id to be created Returns: list: A list of @@ -481,16 +645,17 @@ def create_volume_ckd(self, name, cap, pool, """ return self.create_volume( - name, cap, pool, + name, + cap, + pool, stgtype=types.DS8K_VOLUME_TYPE_CKD, captype=captype, lss=lss, - tp=tp + tp=tp, + id=id, ) - def create_volume_fb(self, name, cap, pool, - captype='', lss='', tp='', - ): + def create_volume_fb(self, name, cap, pool, captype='', lss='', tp='', id=''): # noqa: A002 """ Create One FB Volume @@ -504,6 +669,7 @@ def create_volume_fb(self, name, cap, pool, lss (str): logical subsystem id tp (str): storage allocation method, valid options include `none`, `ese`, `tse` + id (str): volume id to be created Returns: list: A list of @@ -511,23 +677,28 @@ def create_volume_fb(self, name, cap, pool, """ return self.create_volume( - name, cap, pool, + name, + cap, + pool, stgtype=types.DS8K_VOLUME_TYPE_FB, captype=captype, lss=lss, - tp=tp + tp=tp, + id=id, ) def create_volumes_with_same_prefix( - self, - name, - cap, - pool, - quantity='', - stgtype=types.DS8K_VOLUME_TYPE_FB, - captype=types.DS8K_CAPTYPE_GIB, - lss='', - tp=''): + self, + name, + cap, + pool, + quantity='', + stgtype=types.DS8K_VOLUME_TYPE_FB, + captype=types.DS8K_CAPTYPE_GIB, + lss='', + tp='', + ids=None, + ): """ Create a volume with a name or a group of volumes with the same prefix @@ -545,6 +716,7 @@ def create_volumes_with_same_prefix( default to ``'gib'`` lss (str): logical subsystem id tp (str): storage allocation method, in none, ese, tse + ids (list): list of volume ids to be created Returns: list: A list of @@ -552,24 +724,29 @@ def create_volumes_with_same_prefix( """ return self.create_volumes( - None, cap, pool, + None, + cap, + pool, name=name, quantity=quantity, stgtype=stgtype, captype=captype, lss=lss, - tp=tp + tp=tp, + ids=ids, ) def create_volumes_without_same_prefix( - self, - name_col, - cap, - pool, - stgtype=types.DS8K_VOLUME_TYPE_FB, - captype=types.DS8K_CAPTYPE_GIB, - lss='', - tp=''): + self, + name_col, + cap, + pool, + stgtype=types.DS8K_VOLUME_TYPE_FB, + captype=types.DS8K_CAPTYPE_GIB, + lss='', + tp='', + ids=None, + ): """ Create a group of volumes with specified names @@ -583,32 +760,36 @@ def create_volumes_without_same_prefix( default to ``'gib'`` lss (str): logical subsystem id tp (str): storage allocation method, in none, ese, tse + ids (list): list of volume ids to be created Returns: list: A list of :py:class:`pyds8k.resources.ds8k.v1.volumes.Volume`. """ if not isinstance(name_col, list): - raise ValueError( - INVALID_TYPE.format('list') - ) + raise TypeError(INVALID_TYPE.format('list')) return self.create_volumes( - name_col, cap, pool, + name_col, + cap, + pool, stgtype=stgtype, captype=captype, lss=lss, - tp=tp + tp=tp, + ids=ids, ) def create_volumes_with_names( - self, - names, - cap, - pool, - stgtype=types.DS8K_VOLUME_TYPE_FB, - captype=types.DS8K_CAPTYPE_GIB, - lss='', - tp=''): + self, + names, + cap, + pool, + stgtype=types.DS8K_VOLUME_TYPE_FB, + captype=types.DS8K_CAPTYPE_GIB, + lss='', + tp='', + ids=None, + ): """ Create a group of volumes with specified names @@ -622,21 +803,16 @@ def create_volumes_with_names( default to ``'gib'`` lss (str): logical subsystem id tp (str): storage allocation method, in none, ese, tse + ids (list): list of volume ids to be created Returns: list: A list of :py:class:`pyds8k.resources.ds8k.v1.volumes.Volume`. """ if not isinstance(names, list): - raise ValueError( - INVALID_TYPE.format('list') - ) + raise TypeError(INVALID_TYPE.format('list')) return self.create_volumes( - names, cap, pool, - stgtype=stgtype, - captype=captype, - lss=lss, - tp=tp + names, cap, pool, stgtype=stgtype, captype=captype, lss=lss, tp=tp, ids=ids ) def update_volume_rename(self, volume_id, new_name): @@ -651,9 +827,9 @@ def update_volume_rename(self, volume_id, new_name): object: :py:class:`pyds8k.resources.ds8k.v1.volumes.Volume`. """ - _, res = self.one(types.DS8K_VOLUME, - volume_id, - rebuild_url=True).update({'name': new_name}) + _, res = self.one(types.DS8K_VOLUME, volume_id, rebuild_url=True).update( + {'name': new_name} + ) return res def update_volume_extend(self, volume_id, new_size, captype=''): @@ -668,9 +844,7 @@ def update_volume_extend(self, volume_id, new_size, captype=''): object: :py:class:`pyds8k.resources.ds8k.v1.volumes.Volume`. """ - _, res = self.one(types.DS8K_VOLUME, - volume_id, - rebuild_url=True).update( + _, res = self.one(types.DS8K_VOLUME, volume_id, rebuild_url=True).update( {'cap': new_size, 'captype': captype} ) return res @@ -686,9 +860,9 @@ def update_volume_move(self, volume_id, new_pool): object: :py:class:`pyds8k.resources.ds8k.v1.volumes.Volume`. """ - _, res = self.one(types.DS8K_VOLUME, - volume_id, - rebuild_url=True).update({'pool': new_pool}) + _, res = self.one(types.DS8K_VOLUME, volume_id, rebuild_url=True).update( + {'pool': new_pool} + ) return res # def update_volume_map(self, volume_id, host): @@ -698,7 +872,7 @@ def update_volume_move(self, volume_id, new_pool): # return res -class RootIOPortMixin(object): +class RootIOPortMixin: def get_ioports(self, port_id=None): """ Get IO Ports @@ -729,7 +903,7 @@ def get_ioport(self, port_id): return self.one(types.DS8K_IOPORT, port_id, rebuild_url=True).get() -class RootHostPortMixin(object): +class RootHostPortMixin: def get_host_ports(self, port_id=None): """ Get Host Ports. @@ -769,10 +943,7 @@ def delete_host_port(self, port_id): tuple: A tuple of HTTP Response and DS8000 server message. """ - _, res = self.one(types.DS8K_HOST_PORT, - port_id, - rebuild_url=True - ).delete() + _, res = self.one(types.DS8K_HOST_PORT, port_id, rebuild_url=True).delete() return res def create_host_port(self, port_id, host_name): @@ -788,9 +959,9 @@ def create_host_port(self, port_id, host_name): """ # .create().save() is not a good way for DS8K. - _, res = self.all(types.DS8K_HOST_PORT, - rebuild_url=True - ).posta({'wwpn': port_id, 'host': host_name}) + _, res = self.all(types.DS8K_HOST_PORT, rebuild_url=True).posta( + {'wwpn': port_id, 'host': host_name} + ) return res def update_host_port_change_host(self, port_id, host_name): @@ -805,14 +976,13 @@ def update_host_port_change_host(self, port_id, host_name): object: :py:class:`pyds8k.resources.ds8k.v1.host_ports.HostPort`. """ - _, res = self.one(types.DS8K_HOST_PORT, - port_id, - rebuild_url=True - ).update({'host': host_name}) + _, res = self.one(types.DS8K_HOST_PORT, port_id, rebuild_url=True).update( + {'host': host_name} + ) return res -class RootHostMixin(object): +class RootHostMixin: def get_hosts(self, host_name=None): """ Get Hosts. @@ -852,10 +1022,29 @@ def get_ioports_by_host(self, host_name): :py:class:`pyds8k.resources.ds8k.v1.ioports.IOPort`. """ - return self.one(types.DS8K_HOST, - host_name, - rebuild_url=True - ).all(types.DS8K_IOPORT).list() + return ( + self.one(types.DS8K_HOST, host_name, rebuild_url=True) + .all(types.DS8K_IOPORT) + .list() + ) + + def get_host_ports_by_host(self, host_name): + """ + Get Host Ports by the name of the Host. + + Args: + host_name (str): Required. name of the host. + + Returns: + list: A list of + :py:class:`pyds8k.resources.ds8k.v1.host_ports.HostPort`. + + """ + return ( + self.one(types.DS8K_HOST, host_name, rebuild_url=True) + .all(types.DS8K_HOST_PORT) + .list() + ) def get_mappings_by_host(self, host_name): """ @@ -869,10 +1058,11 @@ def get_mappings_by_host(self, host_name): :py:class:`pyds8k.resources.ds8k.v1.mappings.Volmap`. """ - return self.one(types.DS8K_HOST, - host_name, - rebuild_url=True - ).all(types.DS8K_VOLMAP).list() + return ( + self.one(types.DS8K_HOST, host_name, rebuild_url=True) + .all(types.DS8K_VOLMAP) + .list() + ) def get_mapping_by_host(self, host_name, lunid): """ @@ -887,10 +1077,11 @@ def get_mapping_by_host(self, host_name, lunid): object: :py:class:`pyds8k.resources.ds8k.v1.mappings.Volmap`. """ - return self.one(types.DS8K_HOST, - host_name, - rebuild_url=True - ).one(types.DS8K_VOLMAP, lunid).get() + return ( + self.one(types.DS8K_HOST, host_name, rebuild_url=True) + .one(types.DS8K_VOLMAP, lunid) + .get() + ) def get_volumes_by_host(self, host_name): """ @@ -921,10 +1112,7 @@ def delete_host(self, host_name): tuple: tuple of DS8000 Server Response. """ - _, res = self.one(types.DS8K_HOST, - host_name, - rebuild_url=True - ).delete() + _, res = self.one(types.DS8K_HOST, host_name, rebuild_url=True).delete() return res def create_host(self, host_name, hosttype): @@ -940,45 +1128,50 @@ def create_host(self, host_name, hosttype): """ # .create().save() is not a good way for DS8K. - _, res = self.all(types.DS8K_HOST, - rebuild_url=True - ).posta({'name': host_name, 'hosttype': hosttype}) + _, res = self.all(types.DS8K_HOST, rebuild_url=True).posta( + {'name': host_name, 'hosttype': hosttype} + ) return res def update_host_add_ioports_all(self, host_name): - _, res = self.one(types.DS8K_HOST, - host_name, - rebuild_url=True - ).update({'ioports': 'all'}) + _, res = self.one(types.DS8K_HOST, host_name, rebuild_url=True).update( + {'ioports': 'all'} + ) return res def update_host_rm_ioports_all(self, host_name): - _, res = self.one(types.DS8K_HOST, - host_name, - rebuild_url=True - ).update({'ioports': []}) + _, res = self.one(types.DS8K_HOST, host_name, rebuild_url=True).update( + {'ioports': []} + ) return res def map_volume_to_host(self, host_name, volume_id, lunid=''): - post_data = {'mappings': [{lunid: volume_id}, ]} \ - if lunid else {'volumes': [volume_id]} - _, res = self.one(types.DS8K_HOST, - host_name, - rebuild_url=True - ).all(types.DS8K_VOLMAP).posta( - post_data + post_data = ( + { + 'mappings': [ + {lunid: volume_id}, + ] + } + if lunid + else {'volumes': [volume_id]} + ) + _, res = ( + self.one(types.DS8K_HOST, host_name, rebuild_url=True) + .all(types.DS8K_VOLMAP) + .posta(post_data) ) return res def unmap_volume_from_host(self, host_name, lunid): - _, res = self.one(types.DS8K_HOST, - host_name, - rebuild_url=True - ).one(types.DS8K_VOLMAP, lunid).delete() + _, res = ( + self.one(types.DS8K_HOST, host_name, rebuild_url=True) + .one(types.DS8K_VOLMAP, lunid) + .delete() + ) return res -class RootLSSMixin(object): +class RootLSSMixin: def get_lss(self, lss_id=None, lss_type=''): """ Get LSS @@ -995,12 +1188,8 @@ def get_lss(self, lss_id=None, lss_type=''): return self.get_lss_by_id(lss_id) if not lss_type: return self.all(types.DS8K_LSS, rebuild_url=True).list() - elif str(lss_type) not in types.DS8K_VOLUME_TYPES: - raise ValueError( - INVALID_TYPE.format( - ', '.join(types.DS8K_VOLUME_TYPES) - ) - ) + if str(lss_type) not in types.DS8K_VOLUME_TYPES: + raise ValueError(INVALID_TYPE.format(', '.join(types.DS8K_VOLUME_TYPES))) return self.all(types.DS8K_LSS, rebuild_url=True).list( params={'type': lss_type} ) @@ -1030,17 +1219,19 @@ def get_volumes_by_lss(self, lss_id): :py:class:`pyds8k.resources.ds8k.v1.volumes.Volume`. """ - return self.one(types.DS8K_LSS, - lss_id, - rebuild_url=True - ).all(types.DS8K_VOLUME).list() + return ( + self.one(types.DS8K_LSS, lss_id, rebuild_url=True) + .all(types.DS8K_VOLUME) + .list() + ) def create_lss_ckd( - self, - lss_id=None, - lss_type=types.DS8K_VOLUME_TYPE_CKD, - lcu_type=types.DS8K_LCU_TYPE_3990_6, - ss_id=None): + self, + lss_id=None, + lss_type=types.DS8K_VOLUME_TYPE_CKD, + lcu_type=types.DS8K_LCU_TYPE_3990_6, + ss_id=None, + ): """ Create CKD LSS @@ -1056,15 +1247,12 @@ def create_lss_ckd( """ self._verify_type(lss_type, types.DS8K_LSS_TYPES) self._verify_type(lcu_type, types.DS8K_LCU_TYPES) - _, res = self.all( - types.DS8K_LSS, - rebuild_url=True - ).posta( + _, res = self.all(types.DS8K_LSS, rebuild_url=True).posta( { 'id': lss_id, 'type': lss_type, 'sub_system_identifier': ss_id, - 'ckd_base_cu_type': lcu_type + 'ckd_base_cu_type': lcu_type, } ) return res @@ -1083,40 +1271,44 @@ def delete_lss_by_id(self, lss_id): return self.one(types.DS8K_LSS, lss_id, rebuild_url=True).delete() -class RootFlashCopyMixin(object): - def get_flashcopies(self, fcid=None): +class RootFlashCopyMixin: + def get_flashcopies(self, volume_id=None): """ - Get Flash Copies. + Get Flash Copies. Deprecated after R8. Args: - fcid (str): id of the flash copy. Get all if None. + volume_id (str): id of the associating volume. Get all if None. Returns: list: A list of :py:class:`pyds8k.resources.ds8k.v1.flashcopy.FlashCopy`. """ - if fcid: - return self.one(types.DS8K_FLASHCOPY, fcid, rebuild_url=True).get() + if not volume_id and self.resource_type == "volumes": + volume_id = self.id + + if volume_id: + return self.get_flashcopies_by_volume(volume_id) + return self.all(types.DS8K_FLASHCOPY, rebuild_url=True).list() - def get_flashcopy(self, fcid=None): + def get_flashcopy(self, volume_id=None): """ - Get A Flash Copy. + Get A Flash Copy. Deprecated after R8. Args: - fcid (str): Required. id of the flash copy. + volume_id (str): Required. id of the associating volume. Returns: object: :py:class:`pyds8k.resources.ds8k.v1.flashcopy.FlashCopy`. """ - return self.get_flashcopies(fcid) + return self.get_flashcopies(volume_id) def get_flashcopies_by_volume(self, volume_id): """ - Get Flash Copies by volume id. + Get Flash Copies by volume id. Deprecated after R8. Args: volume_id (str): Required. id of the target volume. @@ -1126,9 +1318,11 @@ def get_flashcopies_by_volume(self, volume_id): :py:class:`pyds8k.resources.ds8k.v1.flashcopy.FlashCopy`. """ - return self.one(types.DS8K_VOLUME, - volume_id, - rebuild_url=True).all(types.DS8K_FLASHCOPY).list() + return ( + self.one(types.DS8K_VOLUME, volume_id, rebuild_url=True) + .all(types.DS8K_FLASHCOPY) + .list() + ) def get_cs_flashcopies(self, fcid=None): """ @@ -1142,12 +1336,15 @@ def get_cs_flashcopies(self, fcid=None): """ if fcid: - return self.one('{}.{}'.format( - types.DS8K_COPY_SERVICE_PREFIX, - types.DS8K_CS_FLASHCOPY), fcid, rebuild_url=True).get() - return self.all('{}.{}'.format( - types.DS8K_COPY_SERVICE_PREFIX, - types.DS8K_CS_FLASHCOPY), rebuild_url=True).list() + return self.one( + f'{types.DS8K_COPY_SERVICE_PREFIX}.{types.DS8K_CS_FLASHCOPY}', + fcid, + rebuild_url=True, + ).get() + return self.all( + f'{types.DS8K_COPY_SERVICE_PREFIX}.{types.DS8K_CS_FLASHCOPY}', + rebuild_url=True, + ).list() def get_cs_flashcopy(self, fcid=None): """ @@ -1162,28 +1359,28 @@ def get_cs_flashcopy(self, fcid=None): """ return self.get_cs_flashcopies(fcid) - def create_cs_flashcopy(self, volume_pairs, options=[]): + def create_cs_flashcopy(self, volume_pairs, options=None): """ Create Copy Service FlashCopy Args: volume_pairs (list): [{"source_volume": 0000,"target_volume": 1100},..] - options (list): Options. + options (list): Options. Default is []. Returns: object: :py:class:`pyds8k.resources.ds8k.v1.cs.flashcopies.FlashCopy`. """ + if options is None: + options = [] for option in options: self._verify_type(option, types.DS8K_FC_OPTIONS) - _, res = self.all('{}.{}'.format( - types.DS8K_COPY_SERVICE_PREFIX, - types.DS8K_CS_FLASHCOPY), - rebuild_url=True).posta({"volume_pairs": volume_pairs, - "options": options - }) + _, res = self.all( + f'{types.DS8K_COPY_SERVICE_PREFIX}.{types.DS8K_CS_FLASHCOPY}', + rebuild_url=True, + ).posta({"volume_pairs": volume_pairs, "options": options}) return res def delete_cs_flashcopy(self, flashcopy_id): @@ -1197,15 +1394,15 @@ def delete_cs_flashcopy(self, flashcopy_id): tuple: A tuple of DS8000 RESTAPI server response. """ - _, res = self.one('{}.{}'.format( - types.DS8K_COPY_SERVICE_PREFIX, - types.DS8K_CS_FLASHCOPY), + _, res = self.one( + f'{types.DS8K_COPY_SERVICE_PREFIX}.{types.DS8K_CS_FLASHCOPY}', flashcopy_id, - rebuild_url=True).delete() + rebuild_url=True, + ).delete() return res -class RootPPRCMixin(object): +class RootPPRCMixin: def get_pprc(self, pprc_id=None): """ Get PPRC. @@ -1232,9 +1429,11 @@ def get_pprc_by_volume(self, volume_id): list: A list of :py:class:`pyds8k.resources.ds8k.v1.pprc.PPRC`. """ - return self.one(types.DS8K_VOLUME, - volume_id, - rebuild_url=True).all(types.DS8K_PPRC).list() + return ( + self.one(types.DS8K_VOLUME, volume_id, rebuild_url=True) + .all(types.DS8K_PPRC) + .list() + ) def get_cs_pprcs(self, pprc_id=None): """ @@ -1249,9 +1448,10 @@ def get_cs_pprcs(self, pprc_id=None): """ if pprc_id: return self.get_cs_pprc(pprc_id) - return self.all('{}.{}'.format( - types.DS8K_COPY_SERVICE_PREFIX, - types.DS8K_CS_PPRC), rebuild_url=True).list() + return self.all( + f'{types.DS8K_COPY_SERVICE_PREFIX}.{types.DS8K_CS_PPRC}', + rebuild_url=True, + ).list() def get_cs_pprc(self, pprc_id): """ @@ -1264,29 +1464,173 @@ def get_cs_pprc(self, pprc_id): object: :py:class:`pyds8k.resources.ds8k.v1.cs.pprcs.PPRC`. """ - return self.one('{}.{}'.format( - types.DS8K_COPY_SERVICE_PREFIX, - types.DS8K_CS_PPRC), pprc_id, rebuild_url=True).get() + return self.one( + f'{types.DS8K_COPY_SERVICE_PREFIX}.{types.DS8K_CS_PPRC}', + pprc_id, + rebuild_url=True, + ).get() + + +class RootHMCMixin: + def restart_hmc(self): + """ + Restart the Hardware Management Console. + + Returns: + tuple: tuple of HTTP Response and DS8000 server message. + """ + return self.all( + f'{types.DS8K_HMC}.{types.DS8K_HMC_RESTART}', rebuild_url=True + ).post(body=None) + + def create_hmc_csr( + self, + O=None, # noqa: E741, N803 + OU=None, # noqa: N803 + C=None, # noqa: N803 + ST=None, # noqa: N803 + L=None, # noqa: N803 + email=None, + force=True, + ): + """ + Create a Certificate Signing Request + for the Hardware Management Console + + Args: + O (str): Optional. The name of your organization or company. + Defaults to None. + OU (str): Optional. A department name within your organization. + Defaults to None. + C (str): Optional. The two-letter ISO code for the country + where your organization is located. Defaults to None. + ST (str): Optional. The state or province where your organization + is located. Do not abbreviate this value. Defaults to None. + L (str): Optional. The city or town where your organization is + located. Defaults to None. + email (str): Optional. An email contact address within your + organization. Defaults to None. + force (bool): Optional. Force the creation of a new CSR if one is + already in progress. Set to False if you want the CSR creation to + fail if a CSR currently exists. Defaults to True. + + Returns: + str: The PEM formatted CSR. + """ + + res, _ = self.all( + f'{types.DS8K_HMC}.{types.DS8K_HMC_CERTIFICATE}.{types.DS8K_HMC_CERTIFICATE_CSR}', + rebuild_url=True, + ).post( + { + "O": O, + "OU": OU, + "C": C, + "ST": ST, + "L": L, + "email": email, + "force": str(force), + } + ) + + # CAVEAT: The certificate is directly returned in the response body. + # Code expects all responses to be json, thus None is returned as the + # body result. Get it directly from the response. + return res.text + + def create_hmc_selfsigned_certificate( + self, + O=None, # noqa: E741, N803 + OU=None, # noqa: N803 + C=None, # noqa: N803 + ST=None, # noqa: N803 + L=None, # noqa: N803 + days=365, + email=None, + restart=False, + ): + """ + Create a Self-signed Certificate for the Hardware Management Console + + Args: + O (str): Optional. The name of your organization or company. + Defaults to None. + OU (str): Optional. A department name within your organization. + Defaults to None. + C (str): Optional. The two-letter ISO code for the country + where your organization is located. Defaults to None. + ST (str): Optional. The state or province where your organization + is located. Do not abbreviate this value. Defaults to None. + L (str): Optional. The city or town where your organization is + located. Defaults to None. + days (int): Optional. The number of days the certificate is valid. + Defaults to 365. + email (str): Optional. An email contact address within your + organization. Defaults to None. + restart (bool): Optional. Restart the HMC to activate the + certificate. Defaults to True. + + Returns: + tuple: tuple of HTTP Response and DS8000 server message. + """ + + return self.all( + f'{types.DS8K_HMC}.{types.DS8K_HMC_CERTIFICATE}.{types.DS8K_HMC_CERTIFICATE_SELFSIGNED}', + rebuild_url=True, + ).post( + { + "O": O, + "OU": OU, + "C": C, + "ST": ST, + "L": L, + "days": days, + "email": email, + "restart": str(restart), + } + ) + + def upload_hmc_signed_certificate(self, certificate): + """ + Upload a Signed Certificate to the Hardware Management Console + Args: + certificate (str): The opened signed certificate file or text + string. -class RootEventMixin(object): - def get_events(self, evt_id=None, evt_filter={}): + Returns: + tuple: tuple of HTTP Response and DS8000 server message. + """ + + # CAVEAT: Packages and modules can't have the same name, so the + # resource is ds8k.v1.hmc.certificate.certificate + # not ds8k.v1.hmc.certificate because the design uses meta classes to + # build the urls. It didn't expect calls to object in /object/objects? + # Force to match object.object. + return self.all( + f'{types.DS8K_HMC}.{types.DS8K_HMC_CERTIFICATE}.{types.DS8K_HMC_CERTIFICATE}', + rebuild_url=True, + ).post(body=certificate) + + +class RootEventMixin: + def get_events(self, evt_id=None, evt_filter=None): """ Get Events. Args: evt_id (str): id of the event. Get all if None. - evt_filter (dict): predefined filters. + evt_filter (dict): predefined filters. Default is {}. Returns: list: A list of :py:class:`pyds8k.resources.ds8k.v1.events.Event`. """ + if evt_filter is None: + evt_filter = {} if evt_id: return self.get_event(evt_id) - return self.all(types.DS8K_EVENT, - rebuild_url=True - ).list(params=evt_filter) + return self.all(types.DS8K_EVENT, rebuild_url=True).list(params=evt_filter) def get_event(self, evt_id): """ @@ -1301,13 +1645,14 @@ def get_event(self, evt_id): """ return self.one(types.DS8K_EVENT, evt_id, rebuild_url=True).get() - def get_events_by_filter(self, - warning=None, - error=None, - info=None, - before=None, - after=None, - ): + def get_events_by_filter( + self, + warning=None, + error=None, + info=None, + before=None, + after=None, + ): """ Get Events by filters. @@ -1323,57 +1668,62 @@ def get_events_by_filter(self, """ severity = [] - for k, v in {'warning': warning, - 'error': error, - 'info': info, - }.items(): + for k, v in { + 'warning': warning, + 'error': error, + 'info': info, + }.items(): if v: severity.append(k) evt_filter = {} if severity: evt_filter['severity'] = ','.join(severity) - for k, v in {'before': before, - 'after': after, - }.items(): + for k, v in { + 'before': before, + 'after': after, + }.items(): if v: if not isinstance(v, datetime): - raise InvalidArgumentError( - 'before/after must be an datetime instance.' - ) - dttz = datetime(year=v.year, - month=v.month, - day=v.day, - hour=v.hour, - minute=v.minute, - second=v.second, - tzinfo=LocalTimezone(), - ) + msg = 'before/after must be an datetime instance.' + raise InvalidArgumentError(msg) + dttz = datetime( + year=v.year, + month=v.month, + day=v.day, + hour=v.hour, + minute=v.minute, + second=v.second, + tzinfo=LocalTimezone(), + ) evt_filter[k] = dttz.strftime(FORMAT) return self.get_events(evt_filter=evt_filter) -class RootResourceMixin(RootSystemMixin, - RootFlashCopyMixin, - RootPPRCMixin, - RootNodeMixin, - RootMarrayMixin, - RootUserMixin, - RootIOEnclosureMixin, - RootEncryptionGroupMixin, - RootEventMixin, - RootPoolMixin, - RootVolumeMixin, - RootLSSMixin, - RootIOPortMixin, - RootHostPortMixin, - RootHostMixin, - RootBaseMixin - ): +class RootResourceMixin( + RootSystemMixin, + RootHMCMixin, + RootFlashCopyMixin, + RootPPRCMixin, + RootNodeMixin, + RootMarrayMixin, + RootUserMixin, + RootIOEnclosureMixin, + RootEncryptionGroupMixin, + RootEventMixin, + RootPoolMixin, + RootResourceGroupMixin, + RootVolumeMixin, + RootLSSMixin, + RootIOPortMixin, + RootHostPortMixin, + RootHostMixin, + RootBaseMixin, +): pass -class VolumeMixin(object): +class VolumeMixin: def get_volumes(self, volume_id=None): """ Get Volumes for the Caller Object. @@ -1389,7 +1739,7 @@ def get_volumes(self, volume_id=None): if volume_id: return self.get_volume(volume_id) if not self.id: - raise IDMissingError() + raise IDMissingError volumes = self.all(types.DS8K_VOLUME).list() self._start_updating() setattr(self, types.DS8K_VOLUME, volumes) @@ -1409,11 +1759,11 @@ def get_volume(self, volume_id): """ if not self.id: - raise IDMissingError() + raise IDMissingError return self.one(types.DS8K_VOLUME, volume_id).get() -class FCPortMixin(object): +class FCPortMixin: def get_ioports(self, port_id=None): """ Get IO Ports for the Caller Object. @@ -1429,7 +1779,7 @@ def get_ioports(self, port_id=None): if port_id: return self.get_ioport(port_id) if not self.id: - raise IDMissingError() + raise IDMissingError ioports = self.all(types.DS8K_IOPORT).list() self._start_updating() setattr(self, types.DS8K_IOPORT, ioports) @@ -1448,11 +1798,11 @@ def get_ioport(self, port_id): """ if not self.id: - raise IDMissingError() + raise IDMissingError return self.one(types.DS8K_IOPORT, port_id).get() -class HostPortMixin(object): +class HostPortMixin: def get_host_ports(self, port_id=None): """ Get Host Ports for the Caller Object. @@ -1468,7 +1818,7 @@ def get_host_ports(self, port_id=None): if port_id: return self.get_host_port(port_id) if not self.id: - raise IDMissingError() + raise IDMissingError host_ports = self.all(types.DS8K_HOST_PORT).list() self._start_updating() setattr(self, types.DS8K_HOST_PORT, host_ports) @@ -1487,11 +1837,11 @@ def get_host_port(self, port_id): """ if not self.id: - raise IDMissingError() + raise IDMissingError return self.one(types.DS8K_HOST_PORT, port_id).get() -class FlashCopyMixin(object): +class FlashCopyMixin: def get_flashcopies(self, fcid=None): """ Get Flash Copies for the Caller Object. @@ -1505,7 +1855,7 @@ def get_flashcopies(self, fcid=None): """ if not self.id: - raise IDMissingError() + raise IDMissingError if fcid: return self.one(types.DS8K_FLASHCOPY, fcid).get() flashcopies = self.all(types.DS8K_FLASHCOPY).list() @@ -1540,14 +1890,15 @@ def get_cs_flashcopies(self, fcid=None): """ if not self.id: - raise IDMissingError() + raise IDMissingError if fcid: - return self.one('{}.{}'.format( - types.DS8K_COPY_SERVICE_PREFIX, - types.DS8K_CS_FLASHCOPY), fcid).get() - flashcopies = self.all('{}.{}'.format( - types.DS8K_COPY_SERVICE_PREFIX, - types.DS8K_CS_FLASHCOPY)).list() + return self.one( + f'{types.DS8K_COPY_SERVICE_PREFIX}.{types.DS8K_CS_FLASHCOPY}', + fcid, + ).get() + flashcopies = self.all( + f'{types.DS8K_COPY_SERVICE_PREFIX}.{types.DS8K_CS_FLASHCOPY}' + ).list() self._start_updating() setattr(self, types.DS8K_CS_FLASHCOPY, flashcopies) self._stop_updating() @@ -1568,7 +1919,7 @@ def get_cs_flashcopy(self, fcid): return self.get_cs_flashcopies(fcid) -class PPRCMixin(object): +class PPRCMixin: def get_pprc(self, pprc_id=None): """ Get PPRC for the Caller Object. @@ -1581,7 +1932,7 @@ def get_pprc(self, pprc_id=None): """ if not self.id: - raise IDMissingError() + raise IDMissingError if pprc_id: return self.one(types.DS8K_PPRC, pprc_id).get() pprc = self.all(types.DS8K_PPRC).list() @@ -1605,13 +1956,15 @@ def get_cs_pprcs(self, pprc_id=None): raise IDMissingError if pprc_id: return self.get_cs_pprc(pprc_id) - pprcs = self.all('{}.{}'.format( - types.DS8K_COPY_SERVICE_PREFIX, - types.DS8K_CS_PPRC)).list() + pprcs = self.all( + f'{types.DS8K_COPY_SERVICE_PREFIX}.{types.DS8K_CS_PPRC}' + ).list() self._start_updating() - setattr(self, '{}.{}'.format( - types.DS8K_COPY_SERVICE_PREFIX, - types.DS8K_CS_PPRC), pprcs) + setattr( + self, + f'{types.DS8K_COPY_SERVICE_PREFIX}.{types.DS8K_CS_PPRC}', + pprcs, + ) self._stop_updating() return pprcs @@ -1628,12 +1981,12 @@ def get_cs_pprc(self, pprc_id): """ if not self.id: raise IDMissingError - return self.one('{}.{}'.format( - types.DS8K_COPY_SERVICE_PREFIX, - types.DS8K_CS_PPRC), pprc_id).get() + return self.one( + f'{types.DS8K_COPY_SERVICE_PREFIX}.{types.DS8K_CS_PPRC}', pprc_id + ).get() -class VolmapMixin(object): +class VolmapMixin: def get_mappings(self, lunid=None): """ Get Mappings of the Volume by Volume id for the Caller Object. @@ -1649,7 +2002,7 @@ def get_mappings(self, lunid=None): if lunid: return self.get_mapping(lunid) if not self.id: - raise IDMissingError() + raise IDMissingError mappings = self.all(types.DS8K_VOLMAP).list() self._start_updating() setattr(self, types.DS8K_VOLMAP, mappings) @@ -1661,14 +2014,14 @@ def get_mapping(self, lunid): Get the Mapping of the Volume by Volume id for the Caller Object. Args: - lunid (str): Require. id of the volume. + lunid (str): Required. id of the volume. Returns: object: :py:class:`pyds8k.resources.ds8k.v1.mappings.Volmap`. """ if not self.id: - raise IDMissingError() + raise IDMissingError return self.one(types.DS8K_VOLMAP, lunid).get() def delete_mapping(self, lunid): @@ -1676,24 +2029,24 @@ def delete_mapping(self, lunid): Delete the Mapping of the Volume by Volume id for the Caller Object. Args: - lunid (str): Require. id of the volume. + lunid (str): Required. id of the volume. Returns: tuple: tuple of DS8000 RESTAPI Server Response. """ if not self.id: - raise IDMissingError() + raise IDMissingError _, res = self.one(types.DS8K_VOLMAP, lunid).delete() return res - def create_mappings(self, volumes=[], mappings=[]): + def create_mappings(self, volumes=None, mappings=None): """ Create the Mapping of the Volume by Volume id for the Caller Object. Args: - volumes (list): Require. volume ids. - mappings (list): Required. mappings. + volumes (list): Required. volume ids. Default is []. + mappings (list): Required. mappings. Default is []. Returns: list: A list of @@ -1701,7 +2054,11 @@ def create_mappings(self, volumes=[], mappings=[]): """ if not self.id: - raise IDMissingError() + raise IDMissingError + if volumes is None: + volumes = [] + if mappings is None: + mappings = [] if volumes: _, res = self.all(types.DS8K_VOLMAP).posta({'volumes': volumes}) else: diff --git a/pyds8k/resources/ds8k/v1/common/types.py b/pyds8k/resources/ds8k/v1/common/types.py old mode 100755 new mode 100644 index 52befe4..57ecb2b --- a/pyds8k/resources/ds8k/v1/common/types.py +++ b/pyds8k/resources/ds8k/v1/common/types.py @@ -16,12 +16,18 @@ DS8K_TOKEN = 'tokens' DS8K_SYSTEM = 'systems' +DS8K_HMC = 'hmc' +DS8K_HMC_CERTIFICATE = 'certificate' +DS8K_HMC_CERTIFICATE_CSR = 'csr' +DS8K_HMC_CERTIFICATE_SELFSIGNED = 'selfsigned' +DS8K_HMC_RESTART = 'restart' DS8K_NODE = 'nodes' DS8K_MARRAY = 'marrays' DS8K_USER = 'users' DS8K_ENCRYPTION_GROUP = 'encryption_groups' DS8K_IOENCLOSURE = 'io_enclosures' DS8K_POOL = 'pools' +DS8K_RESOURCE_GROUP = 'resource_groups' DS8K_TSEREP = 'tserep' DS8K_ESEREP = 'eserep' DS8K_LSS = 'lss' @@ -33,7 +39,7 @@ DS8K_LCU_TYPE_3990_3, DS8K_LCU_TYPE_3990_TPF, DS8K_LCU_TYPE_3990_6, - DS8K_LCU_TYPE_bs2000 + DS8K_LCU_TYPE_bs2000, ) DS8K_VOLUME = 'volumes' DS8K_SE = 'SE' @@ -46,18 +52,19 @@ DS8K_VOLUME_TYPE_FB = 'fb' DS8K_VOLUME_TYPE_CKD = 'ckd' DS8K_VOLUME_TYPES = (DS8K_VOLUME_TYPE_FB, DS8K_VOLUME_TYPE_CKD) -DS8K_LSS_TYPES = (DS8K_VOLUME_TYPE_CKD, ) +DS8K_LSS_TYPES = (DS8K_VOLUME_TYPE_CKD,) DS8K_FLASHCOPY = 'flashcopy' DS8K_PPRC = 'pprc' DS8K_CAPTYPE_GIB = 'gib' DS8K_CAPTYPE_BYTE = 'bytes' DS8K_CAPTYPE_CYL = 'cyl' DS8K_CAPTYPE_MOD1 = 'mod1' -DS8K_CAPTYPES = (DS8K_CAPTYPE_GIB, - DS8K_CAPTYPE_BYTE, - DS8K_CAPTYPE_CYL, - DS8K_CAPTYPE_MOD1 - ) +DS8K_CAPTYPES = ( + DS8K_CAPTYPE_GIB, + DS8K_CAPTYPE_BYTE, + DS8K_CAPTYPE_CYL, + DS8K_CAPTYPE_MOD1, +) DS8K_TP_NONE = 'none' DS8K_TP_ESE = 'ese' DS8K_TP_TSE = 'tse' @@ -78,13 +85,15 @@ DS8K_OPTION_PSET = "permit_space_efficient_target" DS8K_OPTION_FSETOOS = "fail_space_efficient_target_out_of_space" -DS8K_FC_OPTIONS = (DS8K_OPTION_FRCO, - DS8K_OPTION_ITW, - DS8K_OPTION_RECH, - DS8K_OPTION_NBC, - DS8K_OPTION_PER, - DS8K_OPTION_APTP, - DS8K_OPTION_RERE, - DS8K_OPTION_FRR, - DS8K_OPTION_PSET, - DS8K_OPTION_FSETOOS) +DS8K_FC_OPTIONS = ( + DS8K_OPTION_FRCO, + DS8K_OPTION_ITW, + DS8K_OPTION_RECH, + DS8K_OPTION_NBC, + DS8K_OPTION_PER, + DS8K_OPTION_APTP, + DS8K_OPTION_RERE, + DS8K_OPTION_FRR, + DS8K_OPTION_PSET, + DS8K_OPTION_FSETOOS, +) diff --git a/pyds8k/resources/ds8k/v1/cs/flashcopies.py b/pyds8k/resources/ds8k/v1/cs/flashcopies.py index 51c1424..163f2f8 100644 --- a/pyds8k/resources/ds8k/v1/cs/flashcopies.py +++ b/pyds8k/resources/ds8k/v1/cs/flashcopies.py @@ -17,51 +17,55 @@ """ advanced FlashCopies interface. """ -import six + from pyds8k.base import ManagerMeta, ResourceMeta -from ..common.base import Base, BaseManager -from ..common.types import DS8K_CS_FLASHCOPY, DS8K_FLASHCOPY -from ..volumes import Volume, VolumeManager +from pyds8k.resources.ds8k.v1.common.base import Base, BaseManager +from pyds8k.resources.ds8k.v1.common.types import DS8K_CS_FLASHCOPY, DS8K_FLASHCOPY +from pyds8k.resources.ds8k.v1.volumes import Volume, VolumeManager -@six.add_metaclass(ResourceMeta) -class FlashCopy(Base): +class FlashCopy(Base, metaclass=ResourceMeta): resource_type = DS8K_CS_FLASHCOPY - _template = {'id': None, - 'persistent': None, - 'recording': None, - 'backgroundcopy': None, - 'state': None, - 'options': [], - 'volume_pairs': [] - } + _template = { + 'id': None, + 'persistent': None, + 'recording': None, + 'backgroundcopy': None, + 'state': None, + 'options': [], + 'volume_pairs': [], + } - related_resource = {'_volume_pairs': [{ - 'source_volume': (Volume, VolumeManager), - 'target_volume': (Volume, VolumeManager) - }] + related_resource = { + '_volume_pairs': [ + { + 'source_volume': (Volume, VolumeManager), + 'target_volume': (Volume, VolumeManager), + } + ] } def __repr__(self): - return "".format(self._get_id()) + return f"" def _add_details(self, info, force=False): - super(FlashCopy, self)._add_details(info, force=force) + super()._add_details(info, force=force) if DS8K_FLASHCOPY in info: self._id = info[DS8K_FLASHCOPY][0]['id'] -@six.add_metaclass(ManagerMeta) -class FlashCopyManager(BaseManager): +class FlashCopyManager(BaseManager, metaclass=ManagerMeta): """ Manage advanced FlashCopies resources. """ + resource_class = FlashCopy resource_type = DS8K_CS_FLASHCOPY def get(self, resource_id='', url='', obj_class=None, **kwargs): - return self._get(resource_id=resource_id, url=url, - obj_class=obj_class, **kwargs) + return self._get( + resource_id=resource_id, url=url, obj_class=obj_class, **kwargs + ) def list(self, url='', obj_class=None, body=None, **kwargs): return self._list(url=url, obj_class=obj_class, body=body, **kwargs) diff --git a/pyds8k/resources/ds8k/v1/cs/pprcs.py b/pyds8k/resources/ds8k/v1/cs/pprcs.py index 5b03e20..f34f134 100644 --- a/pyds8k/resources/ds8k/v1/cs/pprcs.py +++ b/pyds8k/resources/ds8k/v1/cs/pprcs.py @@ -17,32 +17,33 @@ """ advanced PPRC interface. """ -import six + from pyds8k.base import ManagerMeta, ResourceMeta -from ..common.base import Base, ReadOnlyManager -from ..common.types import DS8K_CS_PPRC -from ..volumes import Volume, VolumeManager -from ..systems import System, SystemManager +from pyds8k.resources.ds8k.v1.common.base import Base, ReadOnlyManager +from pyds8k.resources.ds8k.v1.common.types import DS8K_CS_PPRC +from pyds8k.resources.ds8k.v1.systems import System, SystemManager +from pyds8k.resources.ds8k.v1.volumes import Volume, VolumeManager -@six.add_metaclass(ResourceMeta) -class PPRC(Base): +class PPRC(Base, metaclass=ResourceMeta): resource_type = DS8K_CS_PPRC - _template = {'id': '', - 'type': '', - 'state': '', - 'source_system': '', - 'target_system': '', - 'source_volume': '', - 'target_volume': '', - } + _template = { + 'id': '', + 'type': '', + 'state': '', + 'source_system': '', + 'target_system': '', + 'source_volume': '', + 'target_volume': '', + } - related_resource = {'_source_volume': (Volume, VolumeManager), - '_source_system': (System, SystemManager), - '_target_volume': (Volume, VolumeManager), - '_target_system': (System, SystemManager), - } + related_resource = { + '_source_volume': (Volume, VolumeManager), + '_source_system': (System, SystemManager), + '_target_volume': (Volume, VolumeManager), + '_target_system': (System, SystemManager), + } def _update_volume_info(self, info): # Handle for bug in DS8000 RESTful API /api/v1/cs/pprcs: @@ -57,13 +58,13 @@ def _add_details(self, info, force=False): self._start_updating() self._update_volume_info(info) self._stop_updating() - super(PPRC, self)._add_details(info, force=force) + super()._add_details(info, force=force) -@six.add_metaclass(ManagerMeta) -class PPRCManager(ReadOnlyManager): +class PPRCManager(ReadOnlyManager, metaclass=ManagerMeta): """ Manage advanced PPRC resources. """ + resource_class = PPRC resource_type = DS8K_CS_PPRC diff --git a/pyds8k/resources/ds8k/v1/encryption_groups.py b/pyds8k/resources/ds8k/v1/encryption_groups.py old mode 100755 new mode 100644 index e7f4dd1..3b10b5a --- a/pyds8k/resources/ds8k/v1/encryption_groups.py +++ b/pyds8k/resources/ds8k/v1/encryption_groups.py @@ -17,26 +17,26 @@ """ Encryption Group interface. """ -import six + from pyds8k.base import ManagerMeta, ResourceMeta from .common.base import Base, ReadOnlyManager from .common.types import DS8K_ENCRYPTION_GROUP -@six.add_metaclass(ResourceMeta) -class EncryptionGroup(Base): +class EncryptionGroup(Base, metaclass=ResourceMeta): resource_type = DS8K_ENCRYPTION_GROUP # id_field = 'id' - _template = {'id': '', - 'state': '', - } + _template = { + 'id': '', + 'state': '', + } -@six.add_metaclass(ManagerMeta) -class EncryptionGroupManager(ReadOnlyManager): +class EncryptionGroupManager(ReadOnlyManager, metaclass=ManagerMeta): """ Manage Encryption Group resources. """ + resource_class = EncryptionGroup resource_type = DS8K_ENCRYPTION_GROUP diff --git a/pyds8k/resources/ds8k/v1/eserep.py b/pyds8k/resources/ds8k/v1/eserep.py old mode 100755 new mode 100644 index 731b213..d58f377 --- a/pyds8k/resources/ds8k/v1/eserep.py +++ b/pyds8k/resources/ds8k/v1/eserep.py @@ -17,39 +17,41 @@ """ ESE Rep interface. """ -import six + from pyds8k.base import ManagerMeta, ResourceMeta -from .common.types import DS8K_ESEREP + from .common.base import SingletonBase, SingletonBaseManager +from .common.types import DS8K_ESEREP from .pools import Pool, PoolManager -@six.add_metaclass(ResourceMeta) -class ESERep(SingletonBase): +class ESERep(SingletonBase, metaclass=ResourceMeta): resource_type = DS8K_ESEREP # id_field = 'id' - _template = {'cap': '', - 'capalloc': '', - 'capavail': '', - 'overprovisioned': '', - 'threshold': '', - 'pool': '', - } + _template = { + 'cap': '', + 'capalloc': '', + 'capavail': '', + 'overprovisioned': '', + 'threshold': '', + 'pool': '', + } readonly_fileds = ('capalloc', 'capavail', 'overprovisioned', 'pool') - related_resource = {'_pool': (Pool, PoolManager), - } + related_resource = { + '_pool': (Pool, PoolManager), + } def __getattr__(self, key): - if key == 'id' or key == self.id_field: - return 'eserep_in_pool_{}'.format(self.pool) - return super(ESERep, self).__getattr__(key) + if key in ('id', self.id_field): + return f'eserep_in_pool_{self.pool}' + return super().__getattr__(key) -@six.add_metaclass(ManagerMeta) -class ESERepManager(SingletonBaseManager): +class ESERepManager(SingletonBaseManager, metaclass=ManagerMeta): """ Manage ESE Rep resources. """ + resource_class = ESERep resource_type = DS8K_ESEREP diff --git a/pyds8k/resources/ds8k/v1/events.py b/pyds8k/resources/ds8k/v1/events.py old mode 100755 new mode 100644 index e3ff02d..2a3f3cd --- a/pyds8k/resources/ds8k/v1/events.py +++ b/pyds8k/resources/ds8k/v1/events.py @@ -17,30 +17,31 @@ """ Event interface. """ -import six + from pyds8k.base import ManagerMeta, ResourceMeta -from .common.types import DS8K_EVENT + from .common.base import Base, ReadOnlyManager +from .common.types import DS8K_EVENT -@six.add_metaclass(ResourceMeta) -class Event(Base): +class Event(Base, metaclass=ResourceMeta): resource_type = DS8K_EVENT # id_field = 'id' - _template = {'id': '', - 'type': '', - 'severity': '', - 'time': '', - 'resource_id': '', - 'formatted_parameter': '', - 'description': '', - } - - -@six.add_metaclass(ManagerMeta) -class EventManager(ReadOnlyManager): + _template = { + 'id': '', + 'type': '', + 'severity': '', + 'time': '', + 'resource_id': '', + 'formatted_parameter': '', + 'description': '', + } + + +class EventManager(ReadOnlyManager, metaclass=ManagerMeta): """ Manage Event resources. """ + resource_class = Event resource_type = DS8K_EVENT diff --git a/pyds8k/resources/ds8k/v1/flashcopy.py b/pyds8k/resources/ds8k/v1/flashcopy.py old mode 100755 new mode 100644 index 5b41e85..e20ea0d --- a/pyds8k/resources/ds8k/v1/flashcopy.py +++ b/pyds8k/resources/ds8k/v1/flashcopy.py @@ -17,47 +17,48 @@ """ FlashCopy interface. """ -import six + from pyds8k.base import ManagerMeta, ResourceMeta + from .common.base import Base, ReadOnlyManager from .common.types import DS8K_FLASHCOPY from .volumes import Volume, VolumeManager -@six.add_metaclass(ResourceMeta) -class FlashCopy(Base): +class FlashCopy(Base, metaclass=ResourceMeta): resource_type = DS8K_FLASHCOPY # id_field = 'id' - _template = {'id': '', - 'persistent': '', - 'recording': '', - 'backgroundcopy': '', - 'state': '', - 'sourcevolume': '', - 'targetvolume': '', - } - - related_resource = {'_sourcevolume': (Volume, VolumeManager), - '_targetvolume': (Volume, VolumeManager) - } + _template = { + 'id': '', + 'persistent': '', + 'recording': '', + 'backgroundcopy': '', + 'state': '', + 'sourcevolume': '', + 'targetvolume': '', + } + + related_resource = { + '_sourcevolume': (Volume, VolumeManager), + '_targetvolume': (Volume, VolumeManager), + } def _add_details(self, info, force=False): - super(FlashCopy, self)._add_details(info, force=force) + super()._add_details(info, force=force) # Temporarily, remove this line when flashcopy resource has id field. self._id = self.representation['id'] = '{}:{}'.format( - info['sourcevolume']['id'], - info['targetvolume']['id'] + info['sourcevolume']['id'], info['targetvolume']['id'] ) # def __repr__(self): # return "".format(self.id) -@six.add_metaclass(ManagerMeta) -class FlashCopyManager(ReadOnlyManager): +class FlashCopyManager(ReadOnlyManager, metaclass=ManagerMeta): """ Manage FlashCopy resources. """ + resource_class = FlashCopy resource_type = DS8K_FLASHCOPY diff --git a/pyds8k/resources/ds8k/v1/hmc/__init__.py b/pyds8k/resources/ds8k/v1/hmc/__init__.py new file mode 100644 index 0000000..c1240a6 --- /dev/null +++ b/pyds8k/resources/ds8k/v1/hmc/__init__.py @@ -0,0 +1,20 @@ +############################################################################## +# Copyright 2023 IBM Corp. +# +# 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 +# +# 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 . import certificate, restart +from .certificate import csr, selfsigned + +__all__ = ('certificate', 'csr', 'restart', 'selfsigned') diff --git a/pyds8k/resources/ds8k/v1/hmc/certificate/__init__.py b/pyds8k/resources/ds8k/v1/hmc/certificate/__init__.py new file mode 100644 index 0000000..e860322 --- /dev/null +++ b/pyds8k/resources/ds8k/v1/hmc/certificate/__init__.py @@ -0,0 +1,19 @@ +############################################################################## +# Copyright 2023 IBM Corp. +# +# 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 +# +# 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 . import csr, selfsigned + +__all__ = ('csr', 'selfsigned') diff --git a/pyds8k/resources/ds8k/v1/hmc/certificate/certificate.py b/pyds8k/resources/ds8k/v1/hmc/certificate/certificate.py new file mode 100644 index 0000000..90d0fe6 --- /dev/null +++ b/pyds8k/resources/ds8k/v1/hmc/certificate/certificate.py @@ -0,0 +1,58 @@ +############################################################################## +# Copyright 2023 IBM Corp. +# +# 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 +# +# 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. +############################################################################## + +""" +Hardware Management Console Certificate interface. +""" + +from pyds8k.base import ManagerMeta, ResourceMeta +from pyds8k.resources.ds8k.v1.common.base import Base, BaseManager +from pyds8k.resources.ds8k.v1.common.types import DS8K_HMC_CERTIFICATE + + +class HmcCertificate(Base, metaclass=ResourceMeta): + resource_type = DS8K_HMC_CERTIFICATE + + # id_field = '' + + # _template = {} + + # readonly_fields = {''} + + # related_resources_collection = () + + # def __repr__(self): + # return "".format(self.id) + + +class HmcCertificateManager(BaseManager, metaclass=ManagerMeta): + """ + Manage Hardware Management Console Certificate resources. + """ + + resource_class = HmcCertificate + resource_type = DS8K_HMC_CERTIFICATE + + # CAVEAT: Meta classes are built using the dir structure and the resulting + # url is /hmc/certificate/certificate. + # Force the default to be the correct url. + def post(self, url='/hmc/certificate', body=None): + files = {'file': body} + # CAVEAT: self._post() underlying code from _post in + # BaseManager -> Manager expects the body to be json. + # Call the client directly, which calls requests. + # Side effect is the REQ not being logged correctly. + return self.client.post(url=url, files=files) diff --git a/pyds8k/resources/ds8k/v1/hmc/certificate/csr.py b/pyds8k/resources/ds8k/v1/hmc/certificate/csr.py new file mode 100644 index 0000000..9ea69ca --- /dev/null +++ b/pyds8k/resources/ds8k/v1/hmc/certificate/csr.py @@ -0,0 +1,50 @@ +############################################################################## +# Copyright 2023 IBM Corp. +# +# 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 +# +# 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. +############################################################################## + +""" +Hardware Management Console Certificate Signing Request interface. +""" + +from pyds8k.base import ManagerMeta, ResourceMeta +from pyds8k.resources.ds8k.v1.common.base import Base, BaseManager +from pyds8k.resources.ds8k.v1.common.types import DS8K_HMC_CERTIFICATE_CSR + + +class HmcCertificateCsr(Base, metaclass=ResourceMeta): + resource_type = DS8K_HMC_CERTIFICATE_CSR + + # id_field = '' + + # _template = {} + + # readonly_fields = {''} + + # related_resources_collection = () + + # def __repr__(self): + # return "".format(self.id) + + +class HmcCertificateCsrManager(BaseManager, metaclass=ManagerMeta): + """ + Manage Hardware Management Console Certificate Signing Request resources. + """ + + resource_class = HmcCertificateCsr + resource_type = DS8K_HMC_CERTIFICATE_CSR + + def post(self, url='', body=None): + return self._post(url=url, body=body) diff --git a/pyds8k/resources/ds8k/v1/hmc/certificate/selfsigned.py b/pyds8k/resources/ds8k/v1/hmc/certificate/selfsigned.py new file mode 100644 index 0000000..10d741d --- /dev/null +++ b/pyds8k/resources/ds8k/v1/hmc/certificate/selfsigned.py @@ -0,0 +1,39 @@ +############################################################################## +# Copyright 2023 IBM Corp. +# +# 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 +# +# 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. +############################################################################## + +""" +Hardware Management Console Self-signed Certificate interface. +""" + +from pyds8k.base import ManagerMeta, ResourceMeta +from pyds8k.resources.ds8k.v1.common.base import Base, BaseManager +from pyds8k.resources.ds8k.v1.common.types import DS8K_HMC_CERTIFICATE_SELFSIGNED + + +class HmcCertificateSelfSigned(Base, metaclass=ResourceMeta): + resource_type = DS8K_HMC_CERTIFICATE_SELFSIGNED + + +class HmcCertificateSelfSignedManager(BaseManager, metaclass=ManagerMeta): + """ + Manage Hardware Management Console Self-signed Certificate resources. + """ + + resource_class = HmcCertificateSelfSigned + resource_type = DS8K_HMC_CERTIFICATE_SELFSIGNED + + def post(self, url='', body=None): + return self._post(url=url, body=body) diff --git a/pyds8k/resources/ds8k/v1/hmc/hmc.py b/pyds8k/resources/ds8k/v1/hmc/hmc.py new file mode 100644 index 0000000..2af0e29 --- /dev/null +++ b/pyds8k/resources/ds8k/v1/hmc/hmc.py @@ -0,0 +1,47 @@ +############################################################################## +# Copyright 2023 IBM Corp. +# +# 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 +# +# 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. +############################################################################## + +""" +Hardware Management Console interface. +""" + +from pyds8k.base import ManagerMeta, ResourceMeta +from pyds8k.resources.ds8k.v1.common.base import Base, BaseManager +from pyds8k.resources.ds8k.v1.common.types import DS8K_HMC + + +class HMC(Base, metaclass=ResourceMeta): + resource_type = DS8K_HMC + + # id_field = '' + + # _template = {} + + # readonly_fields = {''} + + # related_resources_collection = () + + # def __repr__(self): + # return "".format(self.id) + + +class HMCManager(BaseManager, metaclass=ManagerMeta): + """ + Manage Hardware Management Console resources. + """ + + resource_class = HMC + resource_type = DS8K_HMC diff --git a/pyds8k/resources/ds8k/v1/hmc/restart.py b/pyds8k/resources/ds8k/v1/hmc/restart.py new file mode 100644 index 0000000..1d56557 --- /dev/null +++ b/pyds8k/resources/ds8k/v1/hmc/restart.py @@ -0,0 +1,39 @@ +############################################################################## +# Copyright 2023 IBM Corp. +# +# 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 +# +# 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. +############################################################################## + +""" +Hardware Management Console Restart interface. +""" + +from pyds8k.base import ManagerMeta, ResourceMeta +from pyds8k.resources.ds8k.v1.common.base import Base, BaseManager +from pyds8k.resources.ds8k.v1.common.types import DS8K_HMC_RESTART + + +class HMCRestart(Base, metaclass=ResourceMeta): + resource_type = DS8K_HMC_RESTART + + +class HMCRestartManager(BaseManager, metaclass=ManagerMeta): + """ + Manage Hardware Management Console Restart. + """ + + resource_class = HMCRestart + resource_type = DS8K_HMC_RESTART + + def post(self, url='', body=None): + return self._post(url=url, body=body) diff --git a/pyds8k/resources/ds8k/v1/host_ports.py b/pyds8k/resources/ds8k/v1/host_ports.py old mode 100755 new mode 100644 index 18c1d21..1fbf3ea --- a/pyds8k/resources/ds8k/v1/host_ports.py +++ b/pyds8k/resources/ds8k/v1/host_ports.py @@ -17,68 +17,77 @@ """ Host Ports interface. """ -import six + from pyds8k.base import ManagerMeta, ResourceMeta -from .common.types import DS8K_HOST_PORT + from .common.base import Base, BaseManager +from .common.types import DS8K_HOST_PORT from .hosts import Host, HostManager from .ioports import IOPort, IOPortManager -@six.add_metaclass(ResourceMeta) -class HostPort(Base): +class HostPort(Base, metaclass=ResourceMeta): resource_type = DS8K_HOST_PORT id_field = 'wwpn' - _template = {'wwpn': '', - 'state': None, - 'hosttype': None, - 'addrdiscovery': None, - 'lbs': None, - 'host': '', - 'login_type': None, - 'logical_path_established': None, - 'wwnn': None, - 'login_ports': None, - } - related_resource = {'_host': (Host, HostManager), - } - readonly_fileds = ('state', 'hosttype', 'addrdiscovery', 'lbs',) + _template = { + 'wwpn': '', + 'state': None, + 'hosttype': None, + 'addrdiscovery': None, + 'lbs': None, + 'host': '', + 'login_type': None, + 'logical_path_established': None, + 'wwnn': None, + 'login_ports': None, + } + related_resource = { + '_host': (Host, HostManager), + } + readonly_fileds = ( + 'state', + 'hosttype', + 'addrdiscovery', + 'lbs', + ) def _add_details(self, info, force=False): - super(HostPort, self)._add_details(info, force=force) + super()._add_details(info, force=force) self._start_updating() self._set_ioports() self._stop_updating() def _set_ioports(self): - OCCUPIED_IOPORTS = 'login_ports' - port_list = self.representation.get(OCCUPIED_IOPORTS, []) + occupied_ioports = 'login_ports' + port_list = self.representation.get(occupied_ioports, []) port_obj_list = [] for port in port_list: - port_obj = IOPort(self.client, - manager=IOPortManager(self.client), - info=port, - loaded=False, - ) + port_obj = IOPort( + self.client, + manager=IOPortManager(self.client), + info=port, + loaded=False, + ) port_obj_list.append(port_obj) - self.representation[OCCUPIED_IOPORTS] = [p.id for p in port_obj_list] - setattr(self, OCCUPIED_IOPORTS, port_obj_list) + self.representation[occupied_ioports] = [p.id for p in port_obj_list] + setattr(self, occupied_ioports, port_obj_list) # def __repr__(self): # return "".format(self.id) -@six.add_metaclass(ManagerMeta) -class HostPortManager(BaseManager): +class HostPortManager(BaseManager, metaclass=ManagerMeta): """ Manage Host Ports resources. """ + resource_class = HostPort resource_type = DS8K_HOST_PORT def get(self, resource_id='', url='', obj_class=None, **kwargs): - return self._get(resource_id=resource_id, - url=url, obj_class=obj_class, **kwargs) + return self._get( + resource_id=resource_id, url=url, obj_class=obj_class, **kwargs + ) def list(self, url='', obj_class=None, body=None, **kwargs): return self._list(url=url, obj_class=obj_class, body=body, **kwargs) diff --git a/pyds8k/resources/ds8k/v1/hosts.py b/pyds8k/resources/ds8k/v1/hosts.py old mode 100755 new mode 100644 index ea74b6b..faf7429 --- a/pyds8k/resources/ds8k/v1/hosts.py +++ b/pyds8k/resources/ds8k/v1/hosts.py @@ -17,65 +17,77 @@ """ Host interface. """ -import six + from pyds8k.base import ManagerMeta, ResourceMeta -from .common.base import Base, BaseManager -from .common.mixins import FCPortMixin, HostPortMixin, VolumeMixin, VolmapMixin + from .common import types +from .common.base import Base, BaseManager +from .common.mixins import FCPortMixin, HostPortMixin, VolmapMixin, VolumeMixin -@six.add_metaclass(ResourceMeta) -class Host(FCPortMixin, HostPortMixin, VolumeMixin, VolmapMixin, Base): +class Host( + FCPortMixin, HostPortMixin, VolumeMixin, VolmapMixin, Base, metaclass=ResourceMeta +): resource_type = types.DS8K_HOST id_field = 'name' alias = {'id': 'host_id'} - _template = {'name': '', - 'state': '', - 'hosttype': '', - 'addrmode': '', - 'addrdiscovery': '', - 'lbs': '', - types.DS8K_VOLUME: '', - types.DS8K_IOPORT: '', - types.DS8K_HOST_PORT: '', - } - readonly_fileds = ('state', 'addrmode', 'addrdiscovery', 'lbs', - types.DS8K_HOST_PORT, - ) - related_resources_collection = (types.DS8K_VOLUME, - types.DS8K_IOPORT, - types.DS8K_HOST_PORT, - types.DS8K_VOLMAP, - ) + _template = { + 'name': '', + 'state': '', + 'hosttype': '', + 'addrmode': '', + 'addrdiscovery': '', + 'lbs': '', + types.DS8K_VOLUME: '', + types.DS8K_IOPORT: '', + types.DS8K_HOST_PORT: '', + } + readonly_fileds = ( + 'state', + 'addrmode', + 'addrdiscovery', + 'lbs', + types.DS8K_HOST_PORT, + ) + related_resources_collection = ( + types.DS8K_VOLUME, + types.DS8K_IOPORT, + types.DS8K_HOST_PORT, + types.DS8K_VOLMAP, + ) # def __repr__(self): # return "".format(self.id) - def update_host_add_ioports(self, port_ids=[]): + def update_host_add_ioports(self, port_ids=None): + if port_ids is None: + port_ids = [] if not port_ids: return self.update_host_add_ioports_all(self.id) updated_port_ids = self._update_ioports_and_return_ids(port_ids) - _, res = self.one(types.DS8K_HOST, - self.id, - rebuild_url=True - ).update({'ioports': updated_port_ids}) + _, res = self.one(types.DS8K_HOST, self.id, rebuild_url=True).update( + {'ioports': updated_port_ids} + ) return res - def update_host_rm_ioports(self, port_ids=[]): + def update_host_rm_ioports(self, port_ids=None): + if port_ids is None: + port_ids = [] if not port_ids: return self.update_host_rm_ioports_all(self.id) updated_port_ids = self._update_ioports_and_return_ids(port_ids, '-') - _, res = self.one(types.DS8K_HOST, - self.id, - rebuild_url=True - ).update({'ioports': updated_port_ids}) + _, res = self.one(types.DS8K_HOST, self.id, rebuild_url=True).update( + {'ioports': updated_port_ids} + ) return res def _update_ioports_and_return_ids(self, port_ids, operator='+'): ports = [] if not isinstance(port_ids, list): - port_ids = [port_ids, ] + port_ids = [ + port_ids, + ] for port_id in port_ids: port = port_id if not isinstance(port_id, Base): @@ -85,17 +97,18 @@ def _update_ioports_and_return_ids(self, port_ids, operator='+'): return [port.id for port in updated] -@six.add_metaclass(ManagerMeta) -class HostManager(BaseManager): +class HostManager(BaseManager, metaclass=ManagerMeta): """ Manage Host resources. """ + resource_class = Host resource_type = types.DS8K_HOST def get(self, resource_id='', url='', obj_class=None, **kwargs): - return self._get(resource_id=resource_id, - url=url, obj_class=obj_class, **kwargs) + return self._get( + resource_id=resource_id, url=url, obj_class=obj_class, **kwargs + ) def list(self, url='', obj_class=None, body=None, **kwargs): return self._list(url=url, obj_class=obj_class, body=body, **kwargs) diff --git a/pyds8k/resources/ds8k/v1/io_enclosures.py b/pyds8k/resources/ds8k/v1/io_enclosures.py old mode 100755 new mode 100644 index 3ab46dc..cd0aea8 --- a/pyds8k/resources/ds8k/v1/io_enclosures.py +++ b/pyds8k/resources/ds8k/v1/io_enclosures.py @@ -17,26 +17,27 @@ """ IO Enclosure interface. """ -import six + from pyds8k.base import ManagerMeta, ResourceMeta -from .common.types import DS8K_IOENCLOSURE + from .common.base import Base, ReadOnlyManager +from .common.types import DS8K_IOENCLOSURE -@six.add_metaclass(ResourceMeta) -class IOEnclosure(Base): +class IOEnclosure(Base, metaclass=ResourceMeta): resource_type = DS8K_IOENCLOSURE # id_field = 'id' - _template = {'id': '', - 'name': '', - 'state': '', - } + _template = { + 'id': '', + 'name': '', + 'state': '', + } -@six.add_metaclass(ManagerMeta) -class IOEnclosureManager(ReadOnlyManager): +class IOEnclosureManager(ReadOnlyManager, metaclass=ManagerMeta): """ Manage IO Enclosure resources. """ + resource_class = IOEnclosure resource_type = DS8K_IOENCLOSURE diff --git a/pyds8k/resources/ds8k/v1/ioports.py b/pyds8k/resources/ds8k/v1/ioports.py old mode 100755 new mode 100644 index 6c644d9..c950277 --- a/pyds8k/resources/ds8k/v1/ioports.py +++ b/pyds8k/resources/ds8k/v1/ioports.py @@ -17,38 +17,40 @@ """ IO Ports interface. """ -import six + from pyds8k.base import ManagerMeta, ResourceMeta -from .common.types import DS8K_IOPORT + from .common.base import Base, ReadOnlyManager +from .common.types import DS8K_IOPORT from .io_enclosures import IOEnclosure, IOEnclosureManager -@six.add_metaclass(ResourceMeta) -class IOPort(Base): +class IOPort(Base, metaclass=ResourceMeta): resource_type = DS8K_IOPORT # id_field = 'id' - _template = {'id': '', - 'state': '', - 'protocol': '', - 'wwpn': '', - 'type': '', - 'speed': '', - 'loc': '', - 'io_enclosure': '', - } - - related_resource = {'_io_enclosure': (IOEnclosure, IOEnclosureManager), - } + _template = { + 'id': '', + 'state': '', + 'protocol': '', + 'wwpn': '', + 'type': '', + 'speed': '', + 'loc': '', + 'io_enclosure': '', + } + + related_resource = { + '_io_enclosure': (IOEnclosure, IOEnclosureManager), + } # def __repr__(self): # return "".format(self.id) -@six.add_metaclass(ManagerMeta) -class IOPortManager(ReadOnlyManager): +class IOPortManager(ReadOnlyManager, metaclass=ManagerMeta): """ Manage IO Ports resources. """ + resource_class = IOPort resource_type = DS8K_IOPORT diff --git a/pyds8k/resources/ds8k/v1/lss.py b/pyds8k/resources/ds8k/v1/lss.py old mode 100755 new mode 100644 index 30f4c59..56e2793 --- a/pyds8k/resources/ds8k/v1/lss.py +++ b/pyds8k/resources/ds8k/v1/lss.py @@ -17,19 +17,22 @@ """ LSS interface. """ -import six + from pyds8k.base import ManagerMeta, ResourceMeta -from .common.types import DS8K_LSS, DS8K_VOLUME, \ - DS8K_LCU_TYPES, \ - DS8K_LSS_TYPES, \ - DS8K_LCU_TYPE_3990_6, \ - DS8K_VOLUME_TYPE_CKD + from .common.base import Base, BaseManager from .common.mixins import VolumeMixin +from .common.types import ( + DS8K_LCU_TYPE_3990_6, + DS8K_LCU_TYPES, + DS8K_LSS, + DS8K_LSS_TYPES, + DS8K_VOLUME, + DS8K_VOLUME_TYPE_CKD, +) -@six.add_metaclass(ResourceMeta) -class LSS(VolumeMixin, Base): +class LSS(VolumeMixin, Base, metaclass=ResourceMeta): resource_type = DS8K_LSS # id_field = 'id' _template = { @@ -48,37 +51,35 @@ class LSS(VolumeMixin, Base): { 'ckd_base_cu_type': DS8K_LCU_TYPE_3990_6, # lss type shared with volume type - 'type': DS8K_VOLUME_TYPE_CKD + 'type': DS8K_VOLUME_TYPE_CKD, } ) - template_dict = { - DS8K_VOLUME_TYPE_CKD: ckd_template - } + template_dict = {DS8K_VOLUME_TYPE_CKD: ckd_template} readonly_fileds = () related_resource = {} - related_resources_collection = (DS8K_VOLUME, ) + related_resources_collection = (DS8K_VOLUME,) def __init__( - self, - client, - manager=None, - url='', - info=None, - resource_id=None, - parent=None, - loaded=False, - lss_type=DS8K_VOLUME_TYPE_CKD, - lcu_type=DS8K_LCU_TYPE_3990_6 + self, + client, + manager=None, + url='', + info=None, + resource_id=None, + parent=None, + loaded=False, + lss_type=DS8K_VOLUME_TYPE_CKD, + lcu_type=DS8K_LCU_TYPE_3990_6, ): - super(LSS, self).__init__( + super().__init__( client, manager=manager, url=url, info=info, resource_id=resource_id, parent=parent, - loaded=loaded + loaded=loaded, ) self._lss_type = lss_type self._lcu_type = lcu_type @@ -88,32 +89,24 @@ def __init__( self._template = self.template_dict[self._lss_type] def __repr__(self): - return "".format(self._get_id()) + return f"" -@six.add_metaclass(ManagerMeta) -class LSSManager(BaseManager): +class LSSManager(BaseManager, metaclass=ManagerMeta): """ Manage LSS resources. """ + resource_class = LSS resource_type = DS8K_LSS def get(self, resource_id='', url='', obj_class=None, **kwargs): return self._get( - resource_id=resource_id, - url=url, - obj_class=obj_class, - **kwargs + resource_id=resource_id, url=url, obj_class=obj_class, **kwargs ) def list(self, url='', obj_class=None, body=None, **kwargs): - return self._list( - url=url, - obj_class=obj_class, - body=body, - **kwargs - ) + return self._list(url=url, obj_class=obj_class, body=body, **kwargs) def posta(self, url='', body=None): return self._posta(url=url, body=body) diff --git a/pyds8k/resources/ds8k/v1/mappings.py b/pyds8k/resources/ds8k/v1/mappings.py old mode 100755 new mode 100644 index ffa2aff..d9801ac --- a/pyds8k/resources/ds8k/v1/mappings.py +++ b/pyds8k/resources/ds8k/v1/mappings.py @@ -17,38 +17,41 @@ """ Host Volume Mapping interface. """ -import six + from pyds8k.base import ManagerMeta, ResourceMeta + from .common.base import Base, BaseManager from .common.types import DS8K_VOLMAP from .volumes import Volume, VolumeManager -@six.add_metaclass(ResourceMeta) -class Volmap(Base): +class Volmap(Base, metaclass=ResourceMeta): resource_type = DS8K_VOLMAP id_field = 'lunid' - _template = {'lunid': '', - 'volume': '', - } - related_resource = {'_volume': (Volume, VolumeManager), - } + _template = { + 'lunid': '', + 'volume': '', + } + related_resource = { + '_volume': (Volume, VolumeManager), + } def __repr__(self): - return "".format(self._get_id()) + return f"" -@six.add_metaclass(ManagerMeta) -class VolmapManager(BaseManager): +class VolmapManager(BaseManager, metaclass=ManagerMeta): """ Manage Host Volume Mapping resources. """ + resource_class = Volmap resource_type = DS8K_VOLMAP def get(self, resource_id='', url='', obj_class=None, **kwargs): - return self._get(resource_id=resource_id, url=url, - obj_class=obj_class, **kwargs) + return self._get( + resource_id=resource_id, url=url, obj_class=obj_class, **kwargs + ) def list(self, url='', obj_class=None, body=None, **kwargs): return self._list(url=url, obj_class=obj_class, body=body, **kwargs) diff --git a/pyds8k/resources/ds8k/v1/marrays.py b/pyds8k/resources/ds8k/v1/marrays.py old mode 100755 new mode 100644 index f66e30e..9cdad29 --- a/pyds8k/resources/ds8k/v1/marrays.py +++ b/pyds8k/resources/ds8k/v1/marrays.py @@ -17,30 +17,32 @@ """ Marray interface. """ -import six + from pyds8k.base import ManagerMeta, ResourceMeta -from .common.types import DS8K_MARRAY + from .common.base import Base, ReadOnlyManager +from .common.types import DS8K_MARRAY from .pools import Pool, PoolManager -@six.add_metaclass(ResourceMeta) -class Marray(Base): +class Marray(Base, metaclass=ResourceMeta): resource_type = DS8K_MARRAY # id_field = 'id' - _template = {'id': '', - 'disk_class': '', - 'pool': '', - } + _template = { + 'id': '', + 'disk_class': '', + 'pool': '', + } - related_resource = {'_pool': (Pool, PoolManager), - } + related_resource = { + '_pool': (Pool, PoolManager), + } -@six.add_metaclass(ManagerMeta) -class MarrayManager(ReadOnlyManager): +class MarrayManager(ReadOnlyManager, metaclass=ManagerMeta): """ Manage Marray resources. """ + resource_class = Marray resource_type = DS8K_MARRAY diff --git a/pyds8k/resources/ds8k/v1/nodes.py b/pyds8k/resources/ds8k/v1/nodes.py old mode 100755 new mode 100644 index 0338741..921f2fd --- a/pyds8k/resources/ds8k/v1/nodes.py +++ b/pyds8k/resources/ds8k/v1/nodes.py @@ -17,25 +17,26 @@ """ Node interface. """ -import six + from pyds8k.base import ManagerMeta, ResourceMeta -from .common.types import DS8K_NODE + from .common.base import Base, ReadOnlyManager +from .common.types import DS8K_NODE -@six.add_metaclass(ResourceMeta) -class Node(Base): +class Node(Base, metaclass=ResourceMeta): resource_type = DS8K_NODE # id_field = 'id' - _template = {'id': '', - 'state': '', - } + _template = { + 'id': '', + 'state': '', + } -@six.add_metaclass(ManagerMeta) -class NodeManager(ReadOnlyManager): +class NodeManager(ReadOnlyManager, metaclass=ManagerMeta): """ Manage LSS resources. """ + resource_class = Node resource_type = DS8K_NODE diff --git a/pyds8k/resources/ds8k/v1/pools.py b/pyds8k/resources/ds8k/v1/pools.py old mode 100755 new mode 100644 index 516a272..4751430 --- a/pyds8k/resources/ds8k/v1/pools.py +++ b/pyds8k/resources/ds8k/v1/pools.py @@ -17,106 +17,102 @@ """ Extent pool interface. """ -import six + from pyds8k.base import ManagerMeta, ResourceMeta -from .common.types import DS8K_POOL, DS8K_VOLUME, \ - DS8K_TSEREP, DS8K_ESEREP +from pyds8k.exceptions import IDMissingError + from .common.base import Base, ReadOnlyManager from .common.mixins import VolumeMixin -from pyds8k.exceptions import IDMissingError +from .common.types import DS8K_ESEREP, DS8K_POOL, DS8K_TSEREP, DS8K_VOLUME # Note: VolumeMixin will override the methods with # same name in RootResourceMixin. -@six.add_metaclass(ResourceMeta) -class Pool(VolumeMixin, Base): +class Pool(VolumeMixin, Base, metaclass=ResourceMeta): resource_type = DS8K_POOL # id_field = 'id' - _template = {'id': '', - 'name': '', - 'node': '', - 'stgtype': '', - 'cap': '', - 'capalloc': '', - 'capavail': '', - 'overprovisioned': '', - 'easytier': '', - 'tieralloc': [], - 'threshold': '', - 'real_capacity_allocated_on_ese': None, - 'virtual_capacity_allocated_on_ese': None, - DS8K_VOLUME: '', - DS8K_TSEREP: '', - DS8K_ESEREP: '', - } + _template = { + 'id': '', + 'name': '', + 'node': '', + 'stgtype': '', + 'cap': '', + 'capalloc': '', + 'capavail': '', + 'overprovisioned': '', + 'easytier': '', + 'tieralloc': [], + 'threshold': '', + 'real_capacity_allocated_on_ese': None, + 'virtual_capacity_allocated_on_ese': None, + DS8K_VOLUME: '', + DS8K_TSEREP: '', + DS8K_ESEREP: '', + } related_resources_collection = (DS8K_VOLUME, DS8K_TSEREP, DS8K_ESEREP) def __repr__(self): - return "".format(self._get_id()) + return f"" def get_tserep(self): if not self.id: - raise IDMissingError() + raise IDMissingError tserep = self.all(DS8K_TSEREP).list() self._start_updating() setattr(self, DS8K_TSEREP, tserep) self._stop_updating() return tserep - def get_TSE_rep(self): + def get_TSE_rep(self): # noqa: N802 return self.get_tserep()[0] def get_eserep(self): if not self.id: - raise IDMissingError() + raise IDMissingError eserep = self.all(DS8K_ESEREP).list() self._start_updating() setattr(self, DS8K_ESEREP, eserep) self._stop_updating() return eserep - def get_ESE_rep(self): + def get_ESE_rep(self): # noqa: N802 return self.get_eserep()[0] def delete_tserep(self): if not self.id: - raise IDMissingError() + raise IDMissingError return self.all(DS8K_TSEREP).delete() def delete_eserep(self): if not self.id: - raise IDMissingError() + raise IDMissingError return self.all(DS8K_ESEREP).delete() def update_tserep_cap(self, cap, captype=''): if not self.id: - raise IDMissingError() - return self.all(DS8K_TSEREP - ).update({'cap': cap, 'captype': captype}) + raise IDMissingError + return self.all(DS8K_TSEREP).update({'cap': cap, 'captype': captype}) def update_eserep_cap(self, cap, captype=''): if not self.id: - raise IDMissingError() - return self.all(DS8K_ESEREP - ).update({'cap': cap, 'captype': captype}) + raise IDMissingError + return self.all(DS8K_ESEREP).update({'cap': cap, 'captype': captype}) def update_tserep_threshold(self, threshold): if not self.id: - raise IDMissingError() - return self.all(DS8K_TSEREP - ).update({'threshold': threshold}) + raise IDMissingError + return self.all(DS8K_TSEREP).update({'threshold': threshold}) def update_eserep_threshold(self, threshold): if not self.id: - raise IDMissingError() - return self.all(DS8K_ESEREP - ).update({'threshold': threshold}) + raise IDMissingError + return self.all(DS8K_ESEREP).update({'threshold': threshold}) -@six.add_metaclass(ManagerMeta) -class PoolManager(ReadOnlyManager): +class PoolManager(ReadOnlyManager, metaclass=ManagerMeta): """ Manage Extent Pool resources. """ + resource_class = Pool resource_type = DS8K_POOL diff --git a/pyds8k/resources/ds8k/v1/pprc.py b/pyds8k/resources/ds8k/v1/pprc.py old mode 100755 new mode 100644 index cb84daa..94e147b --- a/pyds8k/resources/ds8k/v1/pprc.py +++ b/pyds8k/resources/ds8k/v1/pprc.py @@ -17,48 +17,49 @@ """ PPRC interface. """ -import six + from pyds8k.base import ManagerMeta, ResourceMeta + from .common.base import Base, ReadOnlyManager from .common.types import DS8K_PPRC -from .volumes import Volume, VolumeManager from .systems import System, SystemManager +from .volumes import Volume, VolumeManager -@six.add_metaclass(ResourceMeta) -class PPRC(Base): +class PPRC(Base, metaclass=ResourceMeta): resource_type = DS8K_PPRC # id_field = 'id' - _template = {'id': '', - 'type': '', - 'state': '', - 'targetsystem': '', - 'sourcevolume': '', - 'targetvolume': '', - } + _template = { + 'id': '', + 'type': '', + 'state': '', + 'targetsystem': '', + 'sourcevolume': '', + 'targetvolume': '', + } - related_resource = {'_sourcevolume': (Volume, VolumeManager), - '_targetvolume': (Volume, VolumeManager), - '_targetsystem': (System, SystemManager), - } + related_resource = { + '_sourcevolume': (Volume, VolumeManager), + '_targetvolume': (Volume, VolumeManager), + '_targetsystem': (System, SystemManager), + } def _add_details(self, info, force=False): - super(PPRC, self)._add_details(info, force=force) + super()._add_details(info, force=force) # Temporarily, remove this line when flashcopy resource has id field. self._id = self.representation['id'] = '{}:{}'.format( - info['sourcevolume']['id'], - info['targetvolume']['id'] + info['sourcevolume']['id'], info['targetvolume']['id'] ) # def __repr__(self): # return "".format(self.id) -@six.add_metaclass(ManagerMeta) -class PPRCManager(ReadOnlyManager): +class PPRCManager(ReadOnlyManager, metaclass=ManagerMeta): """ Manage PPRC resources. """ + resource_class = PPRC resource_type = DS8K_PPRC diff --git a/pyds8k/resources/ds8k/v1/resource_groups.py b/pyds8k/resources/ds8k/v1/resource_groups.py new file mode 100644 index 0000000..234a203 --- /dev/null +++ b/pyds8k/resources/ds8k/v1/resource_groups.py @@ -0,0 +1,69 @@ +############################################################################## +# Copyright 2022 IBM Corp. +# +# 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 +# +# 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. +############################################################################## + +""" +Resource Group interface. +""" + +from pyds8k.base import ManagerMeta, ResourceMeta + +from .common.base import Base, BaseManager +from .common.types import DS8K_RESOURCE_GROUP + + +class ResourceGroup(Base, metaclass=ResourceMeta): + resource_type = DS8K_RESOURCE_GROUP + # id_field = 'id' + + _template = { + 'id': '', + 'name': '', + 'state': '', + 'label': '', + 'cs_global': '', + 'pass_global': '', + 'gm_masters': '', + 'gm_sessions': '', + } + + +class ResourceGroupManager(BaseManager, metaclass=ManagerMeta): + """ + Manage Resource Group resources. + """ + + resource_class = ResourceGroup + resource_type = DS8K_RESOURCE_GROUP + + def get(self, resource_id='', url='', obj_class=None, **kwargs): + return self._get( + resource_id=resource_id, url=url, obj_class=obj_class, **kwargs + ) + + def list(self, url='', obj_class=None, body=None, **kwargs): + return self._list(url=url, obj_class=obj_class, body=body, **kwargs) + + def posta(self, url='', body=None): + return self._posta(url=url, body=body) + + def put(self, url='', body=None): + return self._put(url=url, body=body) + + def patch(self, url='', body=None): + return self._patch(url=url, body=body) + + def delete(self, url=''): + return self._delete(url=url) diff --git a/pyds8k/resources/ds8k/v1/systems.py b/pyds8k/resources/ds8k/v1/systems.py old mode 100755 new mode 100644 index bdab844..dd57d92 --- a/pyds8k/resources/ds8k/v1/systems.py +++ b/pyds8k/resources/ds8k/v1/systems.py @@ -17,41 +17,42 @@ """ Storage system interface. """ -import six + from pyds8k.base import ManagerMeta, ResourceMeta -from .common.types import DS8K_SYSTEM + from .common.base import Base, ReadOnlyManager +from .common.types import DS8K_SYSTEM -@six.add_metaclass(ResourceMeta) -class System(Base): +class System(Base, metaclass=ResourceMeta): resource_type = DS8K_SYSTEM id_field = 'id' - _template = {'id': '', - 'name': '', - 'state': '', - 'release': '', - 'bundle': '', - 'MTM': '', - 'sn': '', - 'wwnn': '', - 'cap': '', - 'capalloc': '', - 'capavail': '', - 'capraw': '', - } + _template = { + 'id': '', + 'name': '', + 'state': '', + 'release': '', + 'bundle': '', + 'MTM': '', + 'sn': '', + 'wwnn': '', + 'cap': '', + 'capalloc': '', + 'capavail': '', + 'capraw': '', + } def __repr__(self): - return "".format(self.id) + return f"" def get_system(self): return self.get_systems()[0] -@six.add_metaclass(ManagerMeta) -class SystemManager(ReadOnlyManager): +class SystemManager(ReadOnlyManager, metaclass=ManagerMeta): """ Manage Storage System resources. """ + resource_class = System resource_type = DS8K_SYSTEM diff --git a/pyds8k/resources/ds8k/v1/tserep.py b/pyds8k/resources/ds8k/v1/tserep.py old mode 100755 new mode 100644 index 24aff2d..25fc736 --- a/pyds8k/resources/ds8k/v1/tserep.py +++ b/pyds8k/resources/ds8k/v1/tserep.py @@ -17,39 +17,41 @@ """ TSE Rep interface. """ -import six + from pyds8k.base import ManagerMeta, ResourceMeta -from .common.types import DS8K_TSEREP + from .common.base import SingletonBase, SingletonBaseManager +from .common.types import DS8K_TSEREP from .pools import Pool, PoolManager -@six.add_metaclass(ResourceMeta) -class TSERep(SingletonBase): +class TSERep(SingletonBase, metaclass=ResourceMeta): resource_type = DS8K_TSEREP # id_field = 'id' - _template = {'cap': '', - 'capalloc': '', - 'capavail': '', - 'overprovisioned': '', - 'threshold': '', - 'pool': '', - } + _template = { + 'cap': '', + 'capalloc': '', + 'capavail': '', + 'overprovisioned': '', + 'threshold': '', + 'pool': '', + } readonly_fileds = ('capalloc', 'capavail', 'overprovisioned', 'pool') - related_resource = {'_pool': (Pool, PoolManager), - } + related_resource = { + '_pool': (Pool, PoolManager), + } def __getattr__(self, key): - if key == 'id' or key == self.id_field: - return 'tserep_in_pool_{}'.format(self.pool) - return super(TSERep, self).__getattr__(key) + if key in ('id', self.id_field): + return f'tserep_in_pool_{self.pool}' + return super().__getattr__(key) -@six.add_metaclass(ManagerMeta) -class TSERepManager(SingletonBaseManager): +class TSERepManager(SingletonBaseManager, metaclass=ManagerMeta): """ Manage TSE Rep resources. """ + resource_class = TSERep resource_type = DS8K_TSEREP diff --git a/pyds8k/resources/ds8k/v1/users.py b/pyds8k/resources/ds8k/v1/users.py old mode 100755 new mode 100644 index 5babaf2..8681979 --- a/pyds8k/resources/ds8k/v1/users.py +++ b/pyds8k/resources/ds8k/v1/users.py @@ -17,26 +17,27 @@ """ User interface. """ -import six + from pyds8k.base import ManagerMeta, ResourceMeta -from .common.types import DS8K_USER + from .common.base import Base, ReadOnlyManager +from .common.types import DS8K_USER -@six.add_metaclass(ResourceMeta) -class User(Base): +class User(Base, metaclass=ResourceMeta): resource_type = DS8K_USER id_field = 'name' - _template = {'name': '', - 'state': '', # locked|active - 'group': [], - } + _template = { + 'name': '', + 'state': '', # locked|active + 'group': [], + } -@six.add_metaclass(ManagerMeta) -class UserManager(ReadOnlyManager): +class UserManager(ReadOnlyManager, metaclass=ManagerMeta): """ Manage User resources. """ + resource_class = User resource_type = DS8K_USER diff --git a/pyds8k/resources/ds8k/v1/volumes.py b/pyds8k/resources/ds8k/v1/volumes.py old mode 100755 new mode 100644 index 0c86b8d..9e14e0b --- a/pyds8k/resources/ds8k/v1/volumes.py +++ b/pyds8k/resources/ds8k/v1/volumes.py @@ -17,83 +17,108 @@ """ Storage volume interface. """ -import six + from pyds8k.base import ManagerMeta, ResourceMeta -from .common.base import Base, BaseManager + from .common import types -from .pools import Pool, PoolManager -from .lss import LSS, LSSManager +from .common.base import Base, BaseManager from .hosts import Host, HostManager +from .lss import LSS, LSSManager +from .pools import Pool, PoolManager -@six.add_metaclass(ResourceMeta) -class Volume(Base): +class Volume(Base, metaclass=ResourceMeta): resource_type = types.DS8K_VOLUME # id_field = 'id' # Set the value to None if the field is not required when creation. - _template = {'id': None, - 'name': '', - 'state': '', - 'cap': '', - 'real_cap': None, - 'virtual_cap': None, - 'captype': '', - 'stgtype': '', - 'VOLSER': None, - 'allocmethod': '', - 'tp': '', - 'capalloc': '', - 'MTM': None, - 'datatype': '', - 'easytier': '', - 'tieralloc': [], - 'lss': '', - 'pool': '', - types.DS8K_HOST: None, - types.DS8K_FLASHCOPY: None, - types.DS8K_PPRC: None, - } + _template = { + 'id': None, + 'name': '', + 'state': '', + 'cap': '', + 'real_cap': None, + 'virtual_cap': None, + 'captype': '', + 'stgtype': '', + 'VOLSER': None, + 'allocmethod': '', + 'tp': '', + 'capalloc': '', + 'MTM': None, + 'datatype': '', + 'easytier': '', + 'tieralloc': [], + 'lss': '', + 'pool': '', + 'basevolume': '', + types.DS8K_HOST: None, + types.DS8K_FLASHCOPY: None, + types.DS8K_PPRC: None, + } fb_template = _template.copy() fb_template.update({'stgtype': types.DS8K_VOLUME_TYPE_FB}) ckd_template = _template.copy() - ckd_template.update({'stgtype': types.DS8K_VOLUME_TYPE_FB}) - - template_dict = {types.DS8K_VOLUME_TYPE_FB: fb_template, - types.DS8K_VOLUME_TYPE_CKD: ckd_template, - } - readonly_fileds = ('state', 'stgtype', 'VOLSER', 'allocmethod', 'tp', - 'capalloc', 'MTM', 'datatype', 'easytier', 'tieralloc', - 'lss' - ) - related_resource = {'_pool': (Pool, PoolManager), - '_lss': (LSS, LSSManager) - } - related_resources_collection = (types.DS8K_HOST, - types.DS8K_FLASHCOPY, - types.DS8K_PPRC) - - def __init__(self, client, manager=None, url='', info={}, - resource_id=None, - parent=None, - loaded=False, - volume_type=types.DS8K_VOLUME_TYPE_FB, - ): - super(Volume, self).__init__(client, - manager=manager, - url=url, - info=info, - resource_id=resource_id, - parent=parent, - loaded=loaded, - ) + ckd_template.update({'stgtype': types.DS8K_VOLUME_TYPE_CKD}) + + template_dict = { + types.DS8K_VOLUME_TYPE_FB: fb_template, + types.DS8K_VOLUME_TYPE_CKD: ckd_template, + } + readonly_fileds = ( + 'state', + 'stgtype', + 'VOLSER', + 'allocmethod', + 'tp', + 'capalloc', + 'MTM', + 'datatype', + 'easytier', + 'tieralloc', + 'lss', + ) + related_resource = { + '_pool': (Pool, PoolManager), + '_lss': (LSS, LSSManager), + } + + related_resources_collection = ( + types.DS8K_HOST, + types.DS8K_FLASHCOPY, + types.DS8K_PPRC, + ) + + def __init__( + self, + client, + manager=None, + url='', + info=None, + resource_id=None, + parent=None, + loaded=False, + volume_type=types.DS8K_VOLUME_TYPE_FB, + ): + if info is None: + info = {} + self.related_resource['_basevolume'] = (Volume, VolumeManager) + super().__init__( + client, + manager=manager, + url=url, + info=info, + resource_id=resource_id, + parent=parent, + loaded=loaded, + ) self.volume_type = volume_type self._verify_volume_type() self._template = self.get_template_from_volume_type(volume_type) def __repr__(self): - return "".format(self._get_id()) + return f"" def get_template_from_volume_type(self, volume_type): return self.template_dict[volume_type] @@ -106,34 +131,35 @@ def _set_hosts(self): if host_list: host_obj_list = [] for host in host_list: - host_obj = Host(self.client, - manager=HostManager(self.client), - info=host, - loaded=False, - ) + host_obj = Host( + self.client, + manager=HostManager(self.client), + info=host, + loaded=False, + ) host_obj_list.append(host_obj) - self.representation[types.DS8K_HOST] = [ - h.host_id for h in host_obj_list] + self.representation[types.DS8K_HOST] = [h.host_id for h in host_obj_list] setattr(self, types.DS8K_HOST, host_obj_list) def _add_details(self, info, force=False): - super(Volume, self)._add_details(info, force=force) + super()._add_details(info, force=force) self._start_updating() self._set_hosts() self._stop_updating() -@six.add_metaclass(ManagerMeta) -class VolumeManager(BaseManager): +class VolumeManager(BaseManager, metaclass=ManagerMeta): """ Manage Storage Volume resources. """ + resource_class = Volume resource_type = types.DS8K_VOLUME def get(self, resource_id='', url='', obj_class=None, **kwargs): - return self._get(resource_id=resource_id, url=url, - obj_class=obj_class, **kwargs) + return self._get( + resource_id=resource_id, url=url, obj_class=obj_class, **kwargs + ) def list(self, url='', obj_class=None, body=None, **kwargs): return self._list(url=url, obj_class=obj_class, body=body, **kwargs) diff --git a/pyds8k/resources/utils.py b/pyds8k/resources/utils.py old mode 100755 new mode 100644 index 232deba..8711b90 --- a/pyds8k/resources/utils.py +++ b/pyds8k/resources/utils.py @@ -14,22 +14,21 @@ # limitations under the License. ############################################################################## -from pyds8k.exceptions import URLParseError -from pyds8k import PYDS8K_DEFAULT_LOGGER from logging import getLogger +from pyds8k import PYDS8K_DEFAULT_LOGGER +from pyds8k.exceptions import URLParseError + logger = getLogger(PYDS8K_DEFAULT_LOGGER) def update_resource_id_in_url(old_id, new_id, url, field=''): if not field: if not isinstance(url, str): - raise URLParseError() - else: - return url.replace(old_id, new_id, 1) - else: - try: - url[field] = str(url[field]).replace(old_id, new_id, 1) - except Exception: - raise URLParseError() - return url + raise URLParseError + return url.replace(old_id, new_id, 1) + try: + url[field] = str(url[field]).replace(old_id, new_id, 1) + except Exception as exc: + raise URLParseError from exc + return url diff --git a/pyds8k/settings.py b/pyds8k/settings.py old mode 100755 new mode 100644 index 6635856..a8774c9 --- a/pyds8k/settings.py +++ b/pyds8k/settings.py @@ -15,9 +15,13 @@ ############################################################################## import logging + # from utils import get_config_settings settings = {'debug': 'true'} -LOG_LEVEL = logging.DEBUG if 'debug' in settings and \ - settings['debug'].lower() == 'true' else logging.INFO -LOG_PATH = settings['log_path'] if 'log_path' in settings else '/tmp' +LOG_LEVEL = ( + logging.DEBUG + if 'debug' in settings and settings['debug'].lower() == 'true' + else logging.INFO +) +LOG_PATH = settings.get('log_path', '/tmp') diff --git a/pyds8k/size_converter.py b/pyds8k/size_converter.py index 793b0b5..4796112 100644 --- a/pyds8k/size_converter.py +++ b/pyds8k/size_converter.py @@ -14,18 +14,20 @@ # limitations under the License. ############################################################################## -GiB = 2**30 # Gibibyte = GiB = 2^30 B = 1,073,741,824 bytes -MiB = 2**20 # Mebibyte = MiB = 2^20 B = 1,048,576 bytes -KiB = 2**10 # Kibibyte = kiB = 2^10 B = 1,024 bytes +GiB = 2**30 # Gibibyte = GiB = 2^30 B = 1,073,741,824 bytes +MiB = 2**20 # Mebibyte = MiB = 2^20 B = 1,048,576 bytes +KiB = 2**10 # Kibibyte = kiB = 2^10 B = 1,024 bytes -GB = 10**9 # Gigabyte = GB = 10^9 B = 1,000,000,000 bytes -MB = 10**6 # Megabyte = MB = 10^6 B = 1,000,000 bytes -KB = 10**3 # Kilobyte = kB = 10^3 B = 1,000 bytes +GB = 10**9 # Gigabyte = GB = 10^9 B = 1,000,000,000 bytes +MB = 10**6 # Megabyte = MB = 10^6 B = 1,000,000 bytes +KB = 10**3 # Kilobyte = kB = 10^3 B = 1,000 bytes def validate_number(number): if not isinstance(number, (int, float)): - raise ValueError("Expected types are (int, long, float)") + msg = "Expected types are (int, float)" + raise TypeError(msg) + # ============================================================================= # Methods converting to bytes. diff --git a/pyds8k/test/base.py b/pyds8k/test/base.py old mode 100755 new mode 100644 index 751e9c8..dddcd13 --- a/pyds8k/test/base.py +++ b/pyds8k/test/base.py @@ -15,36 +15,32 @@ ############################################################################## import unittest + +from pyds8k.base import DefaultManager, Resource from pyds8k.httpclient import HTTPClient -from pyds8k.base import Resource, DefaultManager class TestCaseWithConnect(unittest.TestCase): - def setUp(self): self.client = HTTPClient( - "http://localhost:8088/api/", + "https://localhost:8088/api/", # FIXME: requests required https, not sure why 'admin', 'admin', service_type='ds8k', - port=8088 + port=8088, ) self.base_url = self.client.base_url - self.resource = Resource( - self.client, - manager=DefaultManager(self.client) - ) + self.resource = Resource(self.client, manager=DefaultManager(self.client)) self.domain = self.client.domain # self.maxDiff = None def tearDown(self): - super(TestCaseWithConnect, self).tearDown() + super().tearDown() class TestCaseWithoutConnect(unittest.TestCase): - def setUp(self): - super(TestCaseWithoutConnect, self).setUp() + super().setUp() def tearDown(self): - super(TestCaseWithoutConnect, self).tearDown() + super().tearDown() diff --git a/pyds8k/test/data.py b/pyds8k/test/data.py old mode 100755 new mode 100644 index ea554e9..237e352 --- a/pyds8k/test/data.py +++ b/pyds8k/test/data.py @@ -15,43 +15,40 @@ ############################################################################## import json -from .mock import success_response_one, success_response_all + +from .mock import success_response_all, success_response_one def get_response_data_by_type(resource_type): try: return success_response_one[resource_type] - except KeyError: - raise KeyError( - 'Can not get response data by type: {}'.format(resource_type) - ) + except KeyError as exc: + msg = f'Can not get response data by type: {resource_type}' + raise KeyError(msg) from exc def get_response_json_by_type(resource_type): try: return json.dumps(success_response_one[resource_type]) - except KeyError: - raise KeyError( - 'Can not get response json by type: {}'.format(resource_type) - ) + except KeyError as exc: + msg = f'Can not get response json by type: {resource_type}' + raise KeyError(msg) from exc def get_response_list_data_by_type(resource_type): try: return success_response_all[resource_type] - except KeyError: - raise KeyError( - 'Can not get response list data by type: {}'.format(resource_type) - ) + except KeyError as exc: + msg = f'Can not get response list data by type: {resource_type}' + raise KeyError(msg) from exc def get_response_list_json_by_type(resource_type): try: return json.dumps(success_response_all[resource_type]) - except KeyError: - raise KeyError( - 'Can not get response list json by type: {}'.format(resource_type) - ) + except KeyError as exc: + msg = f'Can not get response list json by type: {resource_type}' + raise KeyError(msg) from exc def get_request_json_body(body): @@ -60,11 +57,7 @@ def get_request_json_body(body): # actions fail/success response action_response = { - "server": { - "status": "ok", - "code": "", - "message": "Operation done successfully." - }, + "server": {"status": "ok", "code": "", "message": "Operation done successfully."}, } action_response_json = json.dumps(action_response) @@ -72,87 +65,58 @@ def get_request_json_body(body): "server": { "status": "failed", "code": "888", - "message": "Operation done unsuccessfully." + "message": "Operation done unsuccessfully.", }, } action_response_failed_json = json.dumps(action_response_failed) _delete_response = { - "server": { - "status": "ok", - "code": "", - "message": "Operation done successfully." - }, + "server": {"status": "ok", "code": "", "message": "Operation done successfully."}, } _delete_response_json = json.dumps(_delete_response) _put_post_response = { - "server": { - "status": "ok", - "code": "", - "message": "Operation done successfully." - }, + "server": {"status": "ok", "code": "", "message": "Operation done successfully."}, } _put_post_response_json = json.dumps(_put_post_response) create_volumes_response = { - "server": { - "status": "ok", - "code": "", - "message": "Operation done successfully." - }, + "server": {"status": "ok", "code": "", "message": "Operation done successfully."}, "responses": [ { "server": { "status": "ok", "code": "", - "message": "Operation done successfully." - }, - "data": { - "volumes": [{"name": "lou_test1", - "id": "0010" - } - ] + "message": "Operation done successfully.", }, + "data": {"volumes": [{"name": "lou_test1", "id": "0010"}]}, "link": { "rel": "self", - "href": "https://localhost:8088/api/v1/volumes/0010" - } + "href": "https://localhost:8088/api/v1/volumes/0010", + }, }, { "server": { "status": "ok", "code": "", - "message": "Operation done successfully." - }, - "data": { - "volumes": [{"name": "lou_test2", - "id": "0011" - } - ] + "message": "Operation done successfully.", }, + "data": {"volumes": [{"name": "lou_test2", "id": "0011"}]}, "link": { "rel": "self", - "href": "https://localhost:8088/api/v1/volumes/0011" - } + "href": "https://localhost:8088/api/v1/volumes/0011", + }, }, - ] + ], } create_volumes_response_json = json.dumps(create_volumes_response) create_lss_response = { - "server": { - "status": "ok", - "code": "", - "message": "" - }, + "server": {"status": "ok", "code": "", "message": ""}, "data": { "lss": [ { "id": "FE", - "link": { - "rel": "self", - "href": "http://rest_url/v1/lss/FE" - }, + "link": {"rel": "self", "href": "http://rest_url/v1/lss/FE"}, "group": "0", "addrgrp": "", "type": "ckd", @@ -165,174 +129,129 @@ def get_request_json_body(body): "xrc_session_timeout": "300", "configvols": "0", "volumes": { - "link": { - "rel": "self", - "href": "http://rest_url/v1/lss/FE/volumes" - } - } + "link": {"rel": "self", "href": "http://rest_url/v1/lss/FE/volumes"} + }, } ] }, - "link": { - "rel": "self", - "href": "http://localhost:8080/ds8000-rest-api/v1/lss/FE" - } + "link": {"rel": "self", "href": "http://localhost:8080/ds8000-rest-api/v1/lss/FE"}, } create_lss_response_json = json.dumps(create_lss_response) create_volumes_partial_failed_response = { - "server": { - "status": "ok", - "code": "", - "message": "Operation done successfully." - }, + "server": {"status": "ok", "code": "", "message": "Operation done successfully."}, "responses": [ - {"server": { - "status": "ok", - "code": "", - "message": "Operation done successfully." - }, - "data": { - "volumes": [{"name": "lou_test1", - "id": "0010" - } - ] + { + "server": { + "status": "ok", + "code": "", + "message": "Operation done successfully.", }, + "data": {"volumes": [{"name": "lou_test1", "id": "0010"}]}, "link": { "rel": "self", - "href": "https://localhost:8088/api/v1/volumes/0010" - } - }, - {"server": { - "status": "failed", - "code": "error_code", - "message": "something wrong" + "href": "https://localhost:8088/api/v1/volumes/0010", + }, }, + { + "server": { + "status": "failed", + "code": "error_code", + "message": "something wrong", + }, }, - ] + ], } create_volumes_partial_failed_response_json = json.dumps( create_volumes_partial_failed_response ) create_volume_response = { - "server": { - "status": "ok", - "code": "", - "message": "Operation done successfully." - }, - "data": { - "volumes": - [ - { - "name": "lou_test1", - "id": "0010" - } - ] - }, - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/volumes/0010" - } + "server": {"status": "ok", "code": "", "message": "Operation done successfully."}, + "data": {"volumes": [{"name": "lou_test1", "id": "0010"}]}, + "link": {"rel": "self", "href": "https://localhost:8088/api/v1/volumes/0010"}, } create_volume_response_json = json.dumps(create_volume_response) create_mappings_response = { - "server": { - "status": "ok", - "code": "", - "message": "Operation done successfully." - }, + "server": {"status": "ok", "code": "", "message": "Operation done successfully."}, "responses": [ { "server": { "status": "ok", "code": "", - "message": "Operation done successfully." + "message": "Operation done successfully.", }, "link": { "rel": "self", - "href": "https://localhost:8088/api/v1/hosts/host1/mappings/00" - } + "href": "https://localhost:8088/api/v1/hosts/host1/mappings/00", + }, }, { "server": { "status": "ok", "code": "", - "message": "Operation done successfully." + "message": "Operation done successfully.", }, "link": { "rel": "self", - "href": "https://localhost:8088/api/v1/hosts/host1/mappings/01" - } + "href": "https://localhost:8088/api/v1/hosts/host1/mappings/01", + }, }, - ] + ], } create_mappings_response_json = json.dumps(create_mappings_response) create_mapping_response = { - "server": { - "status": "ok", - "code": "", - "message": "Operation done successfully." - }, + "server": {"status": "ok", "code": "", "message": "Operation done successfully."}, "link": { "rel": "self", - "href": "https://localhost:8088/api/v1/hosts/host1/mappings/00" - } + "href": "https://localhost:8088/api/v1/hosts/host1/mappings/00", + }, } create_mapping_response_json = json.dumps(create_mapping_response) create_host_response = { - "server": { - "status": "ok", - "code": "", - "message": "Operation done successfully." - }, - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/hosts/host1" - } + "server": {"status": "ok", "code": "", "message": "Operation done successfully."}, + "link": {"rel": "self", "href": "https://localhost:8088/api/v1/hosts/host1"}, } create_host_response_json = json.dumps(create_host_response) create_host_port_response = { - "server": { - "status": "ok", - "code": "", - "message": "Operation done successfully." - }, + "server": {"status": "ok", "code": "", "message": "Operation done successfully."}, "link": { "rel": "self", - "href": "https://localhost:8088/api/v1/host_ports/210000E08B10A95C" - } + "href": "https://localhost:8088/api/v1/host_ports/210000E08B10A95C", + }, } create_host_port_response_json = json.dumps(create_host_port_response) # Templates -volume_template = {'name': 'vol1', - 'type': 'fb', - 'lss': '00', - 'cap': '1', - 'sam': 'ESE', - 'pool_id': 'P0', - } - -default_template = {'name': 'vol1', - 'type': 'fb', - 'lss': '00', - 'cap': '1', - 'sam': 'ESE', - 'pool_id': 'P0', - } +volume_template = { + 'name': 'vol1', + 'type': 'fb', + 'lss': '00', + 'cap': '1', + 'sam': 'ESE', + 'pool_id': 'P0', +} + +default_template = { + 'name': 'vol1', + 'type': 'fb', + 'lss': '00', + 'cap': '1', + 'sam': 'ESE', + 'pool_id': 'P0', +} # fail response token_response_error = { "server": { "status": "failed", "code": "NIServerException", - "message": "Operation done successfully." + "message": "Operation done successfully.", } } @@ -346,15 +265,609 @@ def get_request_json_body(body): } create_flashcopy_response = { - 'server': { - 'status': 'ok', - 'code': '', - 'message': 'Operation done successfully.' - }, + 'server': {'status': 'ok', 'code': '', 'message': 'Operation done successfully.'}, 'link': { 'rel': 'self', - 'href': 'https:/9.151.159.203:8452/api/v1/cs/flashcopies/0000:0001' - } + 'href': 'https:/9.151.159.203:8452/api/v1/cs/flashcopies/0000:0001', + }, } create_flashcopy_response_json = json.dumps(create_flashcopy_response) + +create_resource_group_response = { + "server": {"status": "ok", "code": "", "message": "Operation done successfully."}, + "data": { + "resource_groups": [ + { + "id": "RG1", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/resource_groups/RG1", + }, + "name": "group1", + "state": "", + "label": "group1", + "cs_global": "", + "pass_global": "", + "gm_masters": [ + "00", + "01", + "02", + "03", + "04", + "05", + "06", + "07", + "08", + "09", + "0A", + "0B", + "0C", + "0D", + "0E", + "0F", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "1A", + "1B", + "1C", + "1D", + "1E", + "1F", + "20", + "21", + "22", + "23", + "24", + "25", + "26", + "27", + "28", + "29", + "2A", + "2B", + "2C", + "2D", + "2E", + "2F", + "30", + "31", + "32", + "33", + "34", + "35", + "36", + "37", + "38", + "39", + "3A", + "3B", + "3C", + "3D", + "3E", + "3F", + "40", + "41", + "42", + "43", + "44", + "45", + "46", + "47", + "48", + "49", + "4A", + "4B", + "4C", + "4D", + "4E", + "4F", + "50", + "51", + "52", + "53", + "54", + "55", + "56", + "57", + "58", + "59", + "5A", + "5B", + "5C", + "5D", + "5E", + "5F", + "60", + "61", + "62", + "63", + "64", + "65", + "66", + "67", + "68", + "69", + "6A", + "6B", + "6C", + "6D", + "6E", + "6F", + "70", + "71", + "72", + "73", + "74", + "75", + "76", + "77", + "78", + "79", + "7A", + "7B", + "7C", + "7D", + "7E", + "7F", + "80", + "81", + "82", + "83", + "84", + "85", + "86", + "87", + "88", + "89", + "8A", + "8B", + "8C", + "8D", + "8E", + "8F", + "90", + "91", + "92", + "93", + "94", + "95", + "96", + "97", + "98", + "99", + "9A", + "9B", + "9C", + "9D", + "9E", + "9F", + "A0", + "A1", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "AA", + "AB", + "AC", + "AD", + "AE", + "AF", + "B0", + "B1", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "BA", + "BB", + "BC", + "BD", + "BE", + "BF", + "C0", + "C1", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "CA", + "CB", + "CC", + "CD", + "CE", + "CF", + "D0", + "D1", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "DA", + "DB", + "DC", + "DD", + "DE", + "DF", + "E0", + "E1", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "EA", + "EB", + "EC", + "ED", + "EE", + "EF", + "F0", + "F1", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "FA", + "FB", + "FC", + "FD", + "FE", + "FF", + ], + "gm_sessions": [ + "00", + "01", + "02", + "03", + "04", + "05", + "06", + "07", + "08", + "09", + "0A", + "0B", + "0C", + "0D", + "0E", + "0F", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "1A", + "1B", + "1C", + "1D", + "1E", + "1F", + "20", + "21", + "22", + "23", + "24", + "25", + "26", + "27", + "28", + "29", + "2A", + "2B", + "2C", + "2D", + "2E", + "2F", + "30", + "31", + "32", + "33", + "34", + "35", + "36", + "37", + "38", + "39", + "3A", + "3B", + "3C", + "3D", + "3E", + "3F", + "40", + "41", + "42", + "43", + "44", + "45", + "46", + "47", + "48", + "49", + "4A", + "4B", + "4C", + "4D", + "4E", + "4F", + "50", + "51", + "52", + "53", + "54", + "55", + "56", + "57", + "58", + "59", + "5A", + "5B", + "5C", + "5D", + "5E", + "5F", + "60", + "61", + "62", + "63", + "64", + "65", + "66", + "67", + "68", + "69", + "6A", + "6B", + "6C", + "6D", + "6E", + "6F", + "70", + "71", + "72", + "73", + "74", + "75", + "76", + "77", + "78", + "79", + "7A", + "7B", + "7C", + "7D", + "7E", + "7F", + "80", + "81", + "82", + "83", + "84", + "85", + "86", + "87", + "88", + "89", + "8A", + "8B", + "8C", + "8D", + "8E", + "8F", + "90", + "91", + "92", + "93", + "94", + "95", + "96", + "97", + "98", + "99", + "9A", + "9B", + "9C", + "9D", + "9E", + "9F", + "A0", + "A1", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "AA", + "AB", + "AC", + "AD", + "AE", + "AF", + "B0", + "B1", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "BA", + "BB", + "BC", + "BD", + "BE", + "BF", + "C0", + "C1", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "CA", + "CB", + "CC", + "CD", + "CE", + "CF", + "D0", + "D1", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "DA", + "DB", + "DC", + "DD", + "DE", + "DF", + "E0", + "E1", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "EA", + "EB", + "EC", + "ED", + "EE", + "EF", + "F0", + "F1", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "FA", + "FB", + "FC", + "FD", + "FE", + "FF", + ], + } + ] + }, + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/resource_groups/RG1", + }, +} + +create_resource_group_response_json = json.dumps(create_resource_group_response) + +create_hmc_certificate_csr_response = """-----BEGIN CERTIFICATE REQUEST----- +MIIDITCCAgkCAQAwgaQxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOWTEOMAwGA1UE +BwwFQXJtb2sxGzAZBgNVBAoMEkFuc2libGVDb2xsZWN0aW9uczEPMA0GA1UECwwG +RFM4MDAwMSIwIAYDVQQDDBl0YWxvcy50dWMuc3RnbGFicy5pYm0uY29tMSYwJAYJ +KoZIhvcNAQkBFhdhbnNpYmxlQGZha2Vfc2VydmVyLmNvbTCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBAK9FUZZKlY8rNzhqGW92zFeGVe5IueZRSnbxP1FK +uHcUG9qPJEVeqD7yBIPG/QFBqgY8x19l1didFS8T6PfWr/pTOcyFBBnLKnYtKr94 +i1iiKdEoN8DlhyZdVFnjO/SyjGDNfxhjrtVrN5oTbwDirlpO7lwlH0ZktooaC28f +jDvkhBGOmD4FHOcOZ2w9EKYcewtnqH+KM5CX+mw/bh5Fx4OU1KR6ymgFVuIR6Qkg +uYafLXgheppodfNjWRSw/MU4Zc30a2sRV/KjDog/wCBcf603fiwYDPV9MxWY4Yya +J0XaGPAvoIcftD+Mzro6ciVOfcrxWi/Xb5pAyZrwDGi9oWUCAwEAAaA3MDUGCSqG +SIb3DQEJDjEoMCYwJAYDVR0RBB0wG4IZdGFsb3MudHVjLnN0Z2xhYnMuaWJtLmNv +bTANBgkqhkiG9w0BAQsFAAOCAQEAZnHY3s8T53PtkO9/mpOs14CS9tLz3mEJiaFP +jMD1jVHgN4EIG6hQ5y96tFZc79lKE3+/ngRfXQC19SC39PUzzbC+80Nt6oU6+QGE +aGitz0GpG/yGvQXNIG93AKbj2axbOoNhCsMasn/Aby63xPd6kGR259FRmtyPwKzE +RmoflW1VQM4t3MiCY4ZONH+BUVYdi4/lEyYj2TbjlEUaVFBM1hSM6oAQkPaxxXIF +qyaE4npoAsxLO3N5XwC0MpPrxb5vjc7JgbcU3g53RuwWpNJ0xiE4beU4L8TF85Md +bXdP4rTszg7PP4K63BE9Fy+kM52usAUGO7th1lFgy3/U2A7xSA== +-----END CERTIFICATE REQUEST----- +""" + +create_hmc_certificate_csr_response_json = json.dumps( + create_hmc_certificate_csr_response +) + + +upload_hmc_certificate_cert = """-----BEGIN CERTIFICATE----- +MIIEvjCCA6agAwIBAgIQBtjZBNVYQ0b2ii+nVCJ+xDANBgkqhkiG9w0BAQsFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD +QTAeFw0yMTA0MTQwMDAwMDBaFw0zMTA0MTMyMzU5NTlaME8xCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxKTAnBgNVBAMTIERpZ2lDZXJ0IFRMUyBS +U0EgU0hBMjU2IDIwMjAgQ0ExMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAwUuzZUdwvN1PWNvsnO3DZuUfMRNUrUpmRh8sCuxkB+Uu3Ny5CiDt3+PE0J6a +qXodgojlEVbbHp9YwlHnLDQNLtKS4VbL8Xlfs7uHyiUDe5pSQWYQYE9XE0nw6Ddn +g9/n00tnTCJRpt8OmRDtV1F0JuJ9x8piLhMbfyOIJVNvwTRYAIuE//i+p1hJInuW +raKImxW8oHzf6VGo1bDtN+I2tIJLYrVJmuzHZ9bjPvXj1hJeRPG/cUJ9WIQDgLGB +Afr5yjK7tI4nhyfFK3TUqNaX3sNk+crOU6JWvHgXjkkDKa77SU+kFbnO8lwZV21r +eacroicgE7XQPUDTITAHk+qZ9QIDAQABo4IBgjCCAX4wEgYDVR0TAQH/BAgwBgEB +/wIBADAdBgNVHQ4EFgQUt2ui6qiqhIx56rTaD5iyxZV2ufQwHwYDVR0jBBgwFoAU +A95QNVbRTLtm8KPiGxvDl7I90VUwDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQG +CCsGAQUFBwMBBggrBgEFBQcDAjB2BggrBgEFBQcBAQRqMGgwJAYIKwYBBQUHMAGG +GGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBABggrBgEFBQcwAoY0aHR0cDovL2Nh +Y2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0R2xvYmFsUm9vdENBLmNydDBCBgNV +HR8EOzA5MDegNaAzhjFodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRH +bG9iYWxSb290Q0EuY3JsMD0GA1UdIAQ2MDQwCwYJYIZIAYb9bAIBMAcGBWeBDAEB +MAgGBmeBDAECATAIBgZngQwBAgIwCAYGZ4EMAQIDMA0GCSqGSIb3DQEBCwUAA4IB +AQCAMs5eC91uWg0Kr+HWhMvAjvqFcO3aXbMM9yt1QP6FCvrzMXi3cEsaiVi6gL3z +ax3pfs8LulicWdSQ0/1s/dCYbbdxglvPbQtaCdB73sRD2Cqk3p5BJl+7j5nL3a7h +qG+fh/50tx8bIKuxT8b1Z11dmzzp/2n3YWzW2fP9NsarA4h20ksudYbj/NhVfSbC +EXffPgK2fPOre3qGNm+499iTcc+G33Mw+nur7SpZyEKEOxEXGlLzyQ4UfaJbcme6 +ce1XR2bFuAJKZTRei9AqPCCcUZlM51Ke92sRKw2Sfh3oius2FkOH6ipjv3U/697E +A7sKPPcw7+uvTPyLNhBzPvOk +-----END CERTIFICATE----- +""" diff --git a/pyds8k/test/integration/device.py b/pyds8k/test/integration/device.py index f7e6420..cbc5ce6 100644 --- a/pyds8k/test/integration/device.py +++ b/pyds8k/test/integration/device.py @@ -14,18 +14,16 @@ # limitations under the License. ############################################################################## -from collections import namedtuple +from typing import NamedTuple -Device = namedtuple('Device', ['ipaddr', 'username', 'password', 'port']) -ds44 = Device(ipaddr='9.11.108.44', - username='admin', - password='open1sys', - port=8452 - ) +class Device(NamedTuple): + ipaddr: str + username: str + password: str + port: int -ds179 = Device(ipaddr='9.11.217.179', - username='admin', - password='passw0rd', - port=8452 - ) + +ds44 = Device(ipaddr='9.11.108.44', username='admin', password='open1sys', port=8452) + +ds179 = Device(ipaddr='9.11.217.179', username='admin', password='passw0rd', port=8452) diff --git a/pyds8k/test/integration/integration.py b/pyds8k/test/integration/integration.py old mode 100755 new mode 100644 index 6444c06..83d2ed1 --- a/pyds8k/test/integration/integration.py +++ b/pyds8k/test/integration/integration.py @@ -14,18 +14,20 @@ # limitations under the License. ############################################################################## -from functools import wraps, partial -from logging import getLogger -from contextlib import contextmanager import unittest -from nose.tools import nottest -from datetime import datetime +from contextlib import contextmanager +from datetime import datetime, timezone +from functools import partial, wraps +from logging import getLogger + +import pytest + from pyds8k import PYDS8K_DEFAULT_LOGGER -from pyds8k.utils import res_timer_recorder from pyds8k.client.ds8k.v1.client import Client from pyds8k.client.ds8k.v1.sc_client import SCClient from pyds8k.resources.ds8k.v1.common import types from pyds8k.size_converter import convert_size_gib_to_bytes +from pyds8k.utils import res_timer_recorder from .device import ds44 as ds8k_device @@ -37,45 +39,34 @@ def add_logger_deco(func): @wraps(func) def inner(self, route_id=None): if route_id: - logger.info( - 'Starting GET /{}/{} request'.format(route, - route_id - ) - ) + logger.info(f'Starting GET /{route}/{route_id} request') res = func(self, route_id) logger.info( - 'Successfully got {}: {}, detail is: {}'.format( - route, - res, - res.representation - ) - ) - logger.info('Finish GET /{}/{} request'.format(route, res.id)) + f'Successfully got {route}: {res}, detail is: {res.representation}' + ) + logger.info(f'Finish GET /{route}/{res.id} request') else: - logger.info('Starting GET /{} request'.format(route)) + logger.info(f'Starting GET /{route} request') res = func(self) - logger.info( - 'Successfully got {} {}: {}'.format(len(res), - route, - res, - ) - ) - logger.info('Finish GET /{} request'.format(route)) + logger.info(f'Successfully got {len(res)} {route}: {res}') + logger.info(f'Finish GET /{route} request') return res + return inner + return add_logger_deco class TestIntegration(unittest.TestCase): - @classmethod def setup_class(cls): - - cls.client = Client(ds8k_device.ipaddr, - ds8k_device.username, ds8k_device.password, - # hostname='mtc032h.tuc.stglabs.ibm.com', - port=ds8k_device.port, - ) + cls.client = Client( + ds8k_device.ipaddr, + ds8k_device.username, + ds8k_device.password, + # hostname='mtc032h.tuc.stglabs.ibm.com', + port=ds8k_device.port, + ) def __getattr__(self, k): if k.startswith('get_'): @@ -84,19 +75,15 @@ def __getattr__(self, k): @add_logger(route=route) def route_getter(self, route_id=None): return getattr(self.client, k)(route_id) + return partial(route_getter, self) - return super(TestIntegration, self).__getattr__(k) + return super().__getattr__(k) def test_system(self): logger.info('Starting GET /systems request') sys = self.client.get_system() - logger.info( - 'Successfully got system: {}, detail is: {}'.format( - sys, - sys.representation - ) - ) + logger.info(f'Successfully got system: {sys}, detail is: {sys.representation}') logger.info('Finish GET /systems request') def test_nodes(self): @@ -124,17 +111,13 @@ def test_flashcopy(self): self.get_flashcopy() volume = self.get_volumes('0000') - self._get_sub_resource_by(types.DS8K_VOLUME, volume, - types.DS8K_FLASHCOPY - ) + self._get_sub_resource_by(types.DS8K_VOLUME, volume, types.DS8K_FLASHCOPY) def test_pprc(self): self.get_pprc() volume = self.get_volumes('0000') - self._get_sub_resource_by(types.DS8K_VOLUME, volume, - types.DS8K_PPRC - ) + self._get_sub_resource_by(types.DS8K_VOLUME, volume, types.DS8K_PPRC) def test_cs_pprcs(self): pprcs = self.get_cs_pprcs() @@ -142,18 +125,15 @@ def test_cs_pprcs(self): def test_events(self): sys = self.client.get_system() - before = datetime.now() - after = datetime(year=before.year, - month=before.month, - day=before.day) + before = datetime.now(tz=timezone.utc) + after = datetime( + year=before.year, month=before.month, day=before.day, tzinfo=timezone.utc + ) logger.info('Starting GET /events request') - events = sys.get_events_by_filter(warning=True, - error=True, - before=before, - after=after) - logger.info( - 'Successfully got {} events'.format(len(events)) - ) + events = sys.get_events_by_filter( + warning=True, error=True, before=before, after=after + ) + logger.info(f'Successfully got {len(events)} events') logger.info('Finish GET /events request') def test_pools(self): @@ -164,8 +144,7 @@ def test_pools(self): if vols: return - # Skipped, because get all volumes is not allowed. - @nottest + @pytest.mark.skip(reason="get all volumes is not allowed.") def test_volumes(self): volumes = self.get_volumes() self.get_volumes(volumes[0].id) @@ -177,18 +156,18 @@ def test_hosts(self): hosts = self.get_hosts() for h in hosts: host = self.get_hosts(h.id) - volumes = self._get_sub_resource_by(types.DS8K_HOST, host, - types.DS8K_VOLUME - ) - mappings = self._get_sub_resource_by(types.DS8K_HOST, host, - types.DS8K_VOLMAP - ) - ioports = self._get_sub_resource_by(types.DS8K_HOST, host, - types.DS8K_IOPORT - ) - host_ports = self._get_sub_resource_by(types.DS8K_HOST, host, - types.DS8K_HOST_PORT - ) + volumes = self._get_sub_resource_by( + types.DS8K_HOST, host, types.DS8K_VOLUME + ) + mappings = self._get_sub_resource_by( + types.DS8K_HOST, host, types.DS8K_VOLMAP + ) + ioports = self._get_sub_resource_by( + types.DS8K_HOST, host, types.DS8K_IOPORT + ) + host_ports = self._get_sub_resource_by( + types.DS8K_HOST, host, types.DS8K_HOST_PORT + ) if volumes and mappings and ioports and host_ports: return @@ -197,36 +176,33 @@ def test_host_ports(self): if ports: self.get_host_ports(ports[0].id) + def test_resource_groups(self): + resource_groups = self.get_resource_groups() + self.get_resource_groups(resource_groups[0].id) + def _get_volumes_by(self, route, parent_res): return self._get_sub_resource_by(route, parent_res, types.DS8K_VOLUME) @res_timer_recorder def _get_sub_resource_by(self, route, parent_res, sub_route): - logger.info('Starting GET /{}/{}/{} request'.format(route, - parent_res.id, - sub_route - ) - ) + logger.info(f'Starting GET /{route}/{parent_res.id}/{sub_route} request') # Lazy-loading sub_res = getattr(parent_res, sub_route) - logger.info('Successfully got {} {}'.format(len(sub_res), sub_route)) - logger.info('Finish GET /{}/{}/{} request'.format(route, - parent_res.id, - sub_route - ) - ) + logger.info(f'Successfully got {len(sub_res)} {sub_route}') + logger.info(f'Finish GET /{route}/{parent_res.id}/{sub_route} request') return sub_res class TestSCClient(unittest.TestCase): - @classmethod def setup_class(cls): - cls.client = SCClient(ds8k_device.ipaddr, - ds8k_device.username, ds8k_device.password, - # hostname='mtc032h.tuc.stglabs.ibm.com', - port=ds8k_device.port, - ) + cls.client = SCClient( + ds8k_device.ipaddr, + ds8k_device.username, + ds8k_device.password, + # hostname='mtc032h.tuc.stglabs.ibm.com', + port=ds8k_device.port, + ) def test_volume_create_and_delete(self): volume = self._prepare_volume() @@ -237,25 +213,21 @@ def test_volume_rename(self): with self.get_test_volume() as volume: res = self.client.rename_volume(volume.id, new_name) new_volume = self.client.get_volume(volume.id)[0] - self.assertEqual(new_volume.get('name'), new_name) + assert new_volume.get('name') == new_name logger.info( - 'Successfully renamed the volume {}, response is {}'.format( - volume.id, res - ) - ) + f'Successfully renamed the volume {volume.id}, response is {res}' + ) def test_volume_extend(self): new_size = '7' with self.get_test_volume() as volume: res = self.client.extend_volume(volume.id, new_size) new_volume = self.client.get_volume(volume.id)[0] - self.assertEqual(new_volume.get('cap'), - str(convert_size_gib_to_bytes(int(new_size))) - ) + assert new_volume.get('cap') == str( + convert_size_gib_to_bytes(int(new_size)) + ) logger.info( - 'Successfully extended the volume {}, response is {}'.format( - volume.id, res - ) + f'Successfully extended the volume {volume.id}, response is {res}' ) def test_volume_move(self): @@ -267,14 +239,10 @@ def test_volume_move(self): new_pool_id = pool.get('id') res = self.client.relocate_volume(volume.id, new_pool_id) new_volume = self.client.get_volume(volume.id)[0] - self.assertEqual(new_volume.get('pool'), - new_pool_id - ) + assert new_volume.get('pool') == new_pool_id logger.info( - 'Successfully move volume {} from pool {} to pool {}, response is {}'.format( # noqa - volume.id, old_pool_id, new_pool_id, res - ) - ) + f'Successfully move volume {volume.id} from pool {old_pool_id} to pool {new_pool_id}, response is {res}' + ) break def test_host_create_and_delete(self): @@ -282,109 +250,82 @@ def test_host_create_and_delete(self): self._destroy_host(host_name) def test_volume_map_and_unmap(self): - with self.get_test_host() as host_name: - with self.get_test_volume() as volume: - used_lunids = self.client.get_used_lun_numbers_by_host( - host_name - ) - unused_lunids = \ - ['{0:0{1}x}'.format(i, 2) for i in range(256) - if '{0:0{1}x}'.format(i, 2) not in used_lunids - ] - lunid = unused_lunids[0] - logger.info( - 'Trying to map volume {} to host {} with lunid {}'.format( - volume.id, host_name, lunid - ) - ) - res = self.client.map_volume_to_host(host_name=host_name, - volume_id=volume.id, - lunid=lunid - ) - logger.info( - 'Successfully map volume {} to host {}. res is {}'.format( - volume.id, host_name, res - ) - ) - logger.info( - 'Trying to unmap volume {} from host {}.'.format( - volume.id, host_name - ) - ) - res = self.client.unmap_volume_from_host(host_name, lunid) - logger.info( - 'Successfully unmap volume {} from host {}. res is {}'.format( # noqa - volume.id, host_name, res - ) - ) + with self.get_test_host() as host_name, self.get_test_volume() as volume: + used_lunids = self.client.get_used_lun_numbers_by_host(host_name) + unused_lunids = [ + '{0:0{1}x}'.format(i, 2) + for i in range(256) + if '{0:0{1}x}'.format(i, 2) not in used_lunids + ] + lunid = unused_lunids[0] + logger.info( + f'Trying to map volume {volume.id} to host {host_name} with lunid {lunid}' + ) + res = self.client.map_volume_to_host( + host_name=host_name, volume_id=volume.id, lunid=lunid + ) + logger.info( + f'Successfully map volume {volume.id} to host {host_name}. res is {res}' + ) + logger.info(f'Trying to unmap volume {volume.id} from host {host_name}.') + res = self.client.unmap_volume_from_host(host_name, lunid) + logger.info( + f'Successfully unmap volume {volume.id} from host {host_name}. res is {res}' + ) def test_volume_map_and_unmap_to_zlinux_type_host(self): - with self.get_test_zlinux_type_host() as host_name: - with self.get_test_volume() as volume: - logger.info( - 'Trying to map volume {} to host {}'.format( - volume.id, host_name - ) - ) - res = self.client.map_volume_to_host(host_name=host_name, - volume_id=volume.id, - lunid='' - ) - logger.info( - 'Successfully map volume {} to host {}. res is {}'.format( - volume.id, host_name, res - ) - ) - logger.info( - 'Trying to unmap volume {} from host {}.'.format( - volume.id, host_name - ) - ) - lunid = int('40' + volume.id[:2] + '40' + volume.id[2:], 16) - res = self.client.unmap_volume_from_host(host_name, lunid) - logger.info( - 'Successfully unmap volume {} from host {}. res is {}'.format( # noqa - volume.id, host_name, res - ) - ) + with ( + self.get_test_zlinux_type_host() as host_name, + self.get_test_volume() as volume, + ): + logger.info(f'Trying to map volume {volume.id} to host {host_name}') + res = self.client.map_volume_to_host( + host_name=host_name, volume_id=volume.id, lunid='' + ) + logger.info( + f'Successfully map volume {volume.id} to host {host_name}. res is {res}' + ) + logger.info(f'Trying to unmap volume {volume.id} from host {host_name}.') + lunid = int('40' + volume.id[:2] + '40' + volume.id[2:], 16) + res = self.client.unmap_volume_from_host(host_name, lunid) + logger.info( + f'Successfully unmap volume {volume.id} from host {host_name}. res is {res}' + ) def _prepare_volume(self): logger.info('Preparing a new volume for test purpose.') pools = self.client.list_extentpools() - res = self.client.create_volumes(pool_id=pools[0].get('id'), - capacity_in_GiB=2, - sam='ese', - volume_names_list=['loutest_volume1'] - ) - logger.info('Task done, the volume {} is created.'.format(res)) + res = self.client.create_volumes( + pool_id=pools[0].get('id'), + capacity_in_GiB=2, + sam='ese', + volume_names_list=['loutest_volume1'], + ) + logger.info(f'Task done, the volume {res} is created.') return res[0] def _destroy_volume(self, volume_id): - logger.info('Destroying the created volume {}'.format(volume_id)) + logger.info(f'Destroying the created volume {volume_id}') # delete may fail if it is mapped to hosts. self.client.delete_volume(volume_id) logger.info('Task done, the volume is deleted successfully.') def _prepare_host(self): logger.info('Preparing a new host for test purpose.') - res = self.client.crate_host(host_name='loutest_host1', - wwpn='1' - ) - logger.info('Task done, the host {} is created.'.format(res)) + res = self.client.crate_host(host_name='loutest_host1', wwpn='1') + logger.info(f'Task done, the host {res} is created.') return res def _prepare_host_of_zlinux(self): logger.info('Preparing a new zLinux type host for test purpose.') - res = self.client.crate_host(host_name='zlinuxtest_host1', - wwpn='1', - host_type='zLinux' - ) - logger.info('Task done, the zLinux type host {} is ' - 'created.'.format(res)) + res = self.client.crate_host( + host_name='zlinuxtest_host1', wwpn='1', host_type='zLinux' + ) + logger.info(f'Task done, the zLinux type host {res} is created.') return res def _destroy_host(self, host_name): - logger.info('Destroying the created host {}'.format(host_name)) + logger.info(f'Destroying the created host {host_name}') self.client.delete_host(host_name) logger.info('Task done, the host is deleted successfully.') @@ -393,8 +334,6 @@ def get_test_volume(self): volume = self._prepare_volume() try: yield volume - except Exception: - raise finally: self._destroy_volume(volume.id) @@ -403,8 +342,6 @@ def get_test_host(self): host_name = self._prepare_host() try: yield host_name - except Exception: - raise finally: self._destroy_host(host_name) @@ -413,7 +350,5 @@ def get_test_zlinux_type_host(self): host_name = self._prepare_host_of_zlinux() try: yield host_name - except Exception: - raise finally: self._destroy_host(host_name) diff --git a/pyds8k/test/mock/__init__.py b/pyds8k/test/mock/__init__.py old mode 100755 new mode 100644 index 6114d4a..007ab02 --- a/pyds8k/test/mock/__init__.py +++ b/pyds8k/test/mock/__init__.py @@ -14,38 +14,20 @@ # limitations under the License. ############################################################################## -import os from importlib import import_module -_PATH = os.path.abspath(os.path.dirname(__file__)) -mocks = set([os.path.splitext(resource)[0] - for resource in os.listdir(_PATH) - if os.path.isfile(os.path.join(_PATH, resource)) and - not str(resource).startswith('__init__') - ]) -dir_mocks = \ - [resource for resource in os.listdir(_PATH) if os.path.isdir( - os.path.join(_PATH, resource) - )] +from pyds8k.test.utils import get_dir_mocks, get_mocks + +mocks = get_mocks(__file__) +dir_mocks = get_dir_mocks(__file__) success_response_one = {} success_response_all = {} for re in mocks: - success_response_one[re] = import_module( - '{0}.{1}'.format(__name__, re) - ).ONE - success_response_all[re] = import_module( - '{0}.{1}'.format(__name__, re) - ).ALL + success_response_one[re] = import_module(f'{__name__}.{re}').ONE + success_response_all[re] = import_module(f'{__name__}.{re}').ALL for re in dir_mocks: - if re != '__pycache__': - success_response_one.update( - import_module('{0}.{1}'.format(__name__, - re)).success_response_one - ) - success_response_all.update( - import_module('{0}.{1}'.format(__name__, - re)).success_response_all - ) + success_response_one.update(import_module(f'{__name__}.{re}').success_response_one) + success_response_all.update(import_module(f'{__name__}.{re}').success_response_all) diff --git a/pyds8k/test/mock/cs/__init__.py b/pyds8k/test/mock/cs/__init__.py index 52170ab..e001772 100644 --- a/pyds8k/test/mock/cs/__init__.py +++ b/pyds8k/test/mock/cs/__init__.py @@ -14,22 +14,15 @@ # limitations under the License. ############################################################################## -import os from importlib import import_module -_PATH = os.path.abspath(os.path.dirname(__file__)) -mocks = set([os.path.splitext(resource)[0] - for resource in os.listdir(_PATH) - if os.path.isfile(os.path.join(_PATH, resource)) and - not str(resource).startswith('__init__') - ]) +from pyds8k.test.utils import get_dir_mocks, get_mocks + +mocks = get_mocks(__file__) +dir_mocks = get_dir_mocks(__file__) success_response_one = {} success_response_all = {} for re in mocks: - success_response_one[re] = import_module( - '{0}.{1}'.format(__name__, re) - ).ONE - success_response_all[re] = import_module( - '{0}.{1}'.format(__name__, re) - ).ALL + success_response_one[re] = import_module(f'{__name__}.{re}').ONE + success_response_all[re] = import_module(f'{__name__}.{re}').ALL diff --git a/pyds8k/test/mock/cs/pprcs.py b/pyds8k/test/mock/cs/pprcs.py index 39800d4..170fcf8 100644 --- a/pyds8k/test/mock/cs/pprcs.py +++ b/pyds8k/test/mock/cs/pprcs.py @@ -15,136 +15,132 @@ ############################################################################## ALL = { - "server": { - "status": "ok", - "code": "CMUC00183I", - "message": "Operation done successfully." - }, - "counts": { - "data_counts": 32, - "total_counts": 32 - }, - 'data': { - 'pprcs': [{ - "id": "source_ds8k_0000:remote_ds8k_0001", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/cs/pprcs/" - "source_ds8k_0000:remote_ds8k_0001" - }, - 'source_volume': { - 'name': '0000', - 'link': { - 'rel': 'self', - 'href': 'https://localhost:8088/api/v1/volumes/0000', - }, - }, - 'source_system': { - 'id': 'source_ds8k', - 'link': { - 'rel': 'self', - 'href': 'https://localhost:8088/api/v1/systems', - }, - }, - 'target_volume': { - 'name': '0001', - 'link': { - 'rel': 'self', - 'href': 'https://localhost:8088/api/v1/volumes/0001', - }, - }, - 'target_system': { - 'id': 'remote_ds8k', - 'link': {}, - }, - 'type': 'globalcopy', - 'state': 'copy_pending', - }, { - "id": "source_ds8k_1000:remote_ds8k_1001", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/cs/pprcs/" - "source_ds8k_1000:remote_ds8k_1001" - }, - 'source_volume': { - 'id': '1000', - 'link': { - 'rel': 'self', - 'href': 'https://localhost:8088/api/v1/volumes/1000', - }, - }, - 'source_system': { - 'id': 'source_ds8k', - 'link': { - 'rel': 'self', - 'href': 'https://localhost:8088/api/v1/systems', + "server": { + "status": "ok", + "code": "CMUC00183I", + "message": "Operation done successfully.", + }, + "counts": {"data_counts": 32, "total_counts": 32}, + 'data': { + 'pprcs': [ + { + "id": "source_ds8k_0000:remote_ds8k_0001", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/cs/pprcs/" + "source_ds8k_0000:remote_ds8k_0001", + }, + 'source_volume': { + 'name': '0000', + 'link': { + 'rel': 'self', + 'href': 'https://localhost:8088/api/v1/volumes/0000', + }, + }, + 'source_system': { + 'id': 'source_ds8k', + 'link': { + 'rel': 'self', + 'href': 'https://localhost:8088/api/v1/systems', + }, + }, + 'target_volume': { + 'name': '0001', + 'link': { + 'rel': 'self', + 'href': 'https://localhost:8088/api/v1/volumes/0001', + }, + }, + 'target_system': { + 'id': 'remote_ds8k', + 'link': {}, + }, + 'type': 'globalcopy', + 'state': 'copy_pending', }, - }, - 'target_volume': { - 'id': '1001', - 'link': { - 'rel': 'self', - 'href': 'https://localhost:8088/api/v1/volumes/1001', + { + "id": "source_ds8k_1000:remote_ds8k_1001", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/cs/pprcs/" + "source_ds8k_1000:remote_ds8k_1001", + }, + 'source_volume': { + 'id': '1000', + 'link': { + 'rel': 'self', + 'href': 'https://localhost:8088/api/v1/volumes/1000', + }, + }, + 'source_system': { + 'id': 'source_ds8k', + 'link': { + 'rel': 'self', + 'href': 'https://localhost:8088/api/v1/systems', + }, + }, + 'target_volume': { + 'id': '1001', + 'link': { + 'rel': 'self', + 'href': 'https://localhost:8088/api/v1/volumes/1001', + }, + }, + 'target_system': { + 'id': 'remote_ds8k', + 'link': {}, + }, + 'type': 'globalcopy', + 'state': 'copy_pending', }, - }, - 'target_system': { - 'id': 'remote_ds8k', - 'link': {}, - }, - 'type': 'globalcopy', - 'state': 'copy_pending', + ] }, - ] - } } ONE = { - "server": { - "status": "ok", - "code": "CMUC00183I", - "message": "Operation done successfully." - }, - "counts": { - "data_counts": 1, - "total_counts": 1 - }, - 'data': { - 'pprcs': [ - { - "id": "source_ds8k_0000:remote_ds8k_0001", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/cs/pprcs" - "/source_ds8k_0000:remote_ds8k_0001" - }, - 'source_volume': { - 'id': '0000', - 'link': { - 'rel': 'self', - 'href': 'https://localhost:8088/api/v1/volumes/0000', - }, - }, - 'source_system': { - 'id': 'source_ds8k', - 'link': { - 'rel': 'self', - 'href': 'https://localhost:8088/api/v1/systems', - }, - }, - 'target_volume': { - 'id': '0001', - 'link': { - 'rel': 'self', - 'href': 'https://localhost:8088/api/v1/volumes/0001', - }, - }, - 'target_system': { - 'id': 'remote_ds8k', - 'link': {}, - }, - 'type': 'globalcopy', - 'state': 'copy_pending', - }, - ] - } + "server": { + "status": "ok", + "code": "CMUC00183I", + "message": "Operation done successfully.", + }, + "counts": {"data_counts": 1, "total_counts": 1}, + 'data': { + 'pprcs': [ + { + "id": "source_ds8k_0000:remote_ds8k_0001", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/cs/pprcs" + "/source_ds8k_0000:remote_ds8k_0001", + }, + 'source_volume': { + 'id': '0000', + 'link': { + 'rel': 'self', + 'href': 'https://localhost:8088/api/v1/volumes/0000', + }, + }, + 'source_system': { + 'id': 'source_ds8k', + 'link': { + 'rel': 'self', + 'href': 'https://localhost:8088/api/v1/systems', + }, + }, + 'target_volume': { + 'id': '0001', + 'link': { + 'rel': 'self', + 'href': 'https://localhost:8088/api/v1/volumes/0001', + }, + }, + 'target_system': { + 'id': 'remote_ds8k', + 'link': {}, + }, + 'type': 'globalcopy', + 'state': 'copy_pending', + }, + ] + }, } diff --git a/pyds8k/test/mock/default.py b/pyds8k/test/mock/default.py old mode 100755 new mode 100644 index d4d50dd..753dfd2 --- a/pyds8k/test/mock/default.py +++ b/pyds8k/test/mock/default.py @@ -19,18 +19,12 @@ 'default': [ { 'id': 'a', - 'link': { - 'rel': 'self', - 'href': '/default/a' - }, + 'link': {'rel': 'self', 'href': '/default/a'}, }, { 'id': 'b', - 'link': { - 'rel': 'self', - 'href': '/default/b' - }, - } + 'link': {'rel': 'self', 'href': '/default/b'}, + }, ] } } @@ -38,13 +32,11 @@ ONE = { 'data': { 'default': [ - {'name': 'name_a', - 'id': 'a', - 'link': { - 'rel': 'self', - 'href': '/default/a' - }, - } + { + 'name': 'name_a', + 'id': 'a', + 'link': {'rel': 'self', 'href': '/default/a'}, + } ] } } diff --git a/pyds8k/test/mock/encryption_groups.py b/pyds8k/test/mock/encryption_groups.py index 3751057..4a43ea6 100644 --- a/pyds8k/test/mock/encryption_groups.py +++ b/pyds8k/test/mock/encryption_groups.py @@ -15,58 +15,44 @@ ############################################################################## ALL = { - "server": { - "status": "ok", - "code": "", - "message": "Operation done successfully." - }, - "counts": { - "data_counts": 2, - "total_counts": 2 - }, + "server": {"status": "ok", "code": "", "message": "Operation done successfully."}, + "counts": {"data_counts": 2, "total_counts": 2}, "data": { "encryption_groups": [ { "id": "1", "link": { "rel": "self", - "href": "https://localhost:8088/api/v1/encryption_groups/1" - }, + "href": "https://localhost:8088/api/v1/encryption_groups/1", + }, "state": "accessible", }, { "id": "2", "link": { "rel": "self", - "href": "https://localhost:8088/api/v1/encryption_groups/2" - }, + "href": "https://localhost:8088/api/v1/encryption_groups/2", + }, "state": "accessible", }, ] - } + }, } ONE = { - "server": { - "status": "ok", - "code": "", - "message": "Operation done successfully." - }, - "counts": { - "data_counts": 1, - "total_counts": 1 - }, + "server": {"status": "ok", "code": "", "message": "Operation done successfully."}, + "counts": {"data_counts": 1, "total_counts": 1}, "data": { "encryption_groups": [ { "id": "1", "link": { "rel": "self", - "href": "https://localhost:8088/api/v1/encryption_groups/1" - }, + "href": "https://localhost:8088/api/v1/encryption_groups/1", + }, "state": "accessible", }, ] - } + }, } diff --git a/pyds8k/test/mock/eserep.py b/pyds8k/test/mock/eserep.py old mode 100755 new mode 100644 index 9f38bfe..abbfd27 --- a/pyds8k/test/mock/eserep.py +++ b/pyds8k/test/mock/eserep.py @@ -15,39 +15,33 @@ ############################################################################## ALL = { - "server": - { - "status": "ok", - "code": "CMUC00183I", - "message": "Operation done successfully." - }, - "data": - { - "eserep": - [ - { - "link": - { - "rel": "self", - "href": "https://localhost:8088/api/v1/pools/P1/eserep" - }, - "cap": "136286", - "capalloc": "0", - "capavail": "136286", - "overprovisioned": "0.0", - "repcapthreshold": "0", - "pool": - { - "id": "P1", - "link": - { - "rel": "self", - "href": "https://localhost:8088/api/v1/pools/P1" - } - } - } - ] - } - } + "server": { + "status": "ok", + "code": "CMUC00183I", + "message": "Operation done successfully.", + }, + "data": { + "eserep": [ + { + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/pools/P1/eserep", + }, + "cap": "136286", + "capalloc": "0", + "capavail": "136286", + "overprovisioned": "0.0", + "repcapthreshold": "0", + "pool": { + "id": "P1", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/pools/P1", + }, + }, + } + ] + }, +} ONE = ALL diff --git a/pyds8k/test/mock/events.py b/pyds8k/test/mock/events.py index 01d06ff..935fdaa 100644 --- a/pyds8k/test/mock/events.py +++ b/pyds8k/test/mock/events.py @@ -15,132 +15,105 @@ ############################################################################## ALL = { - "server": - { - "status": "ok", - "code": "", - "message": "Operation done successfully." - }, - "counts": - { - "data_counts": 19231, - "total_counts": 19231 - }, - "data": - { - "events": - [ - { - "id": "SE1", - "type": "UserLoginFailed", - "severity": "info", - "time": "2015-03-31T08:00:48-0700", - "resource_id": "EHqqq92V9", - "formatted_parameter": - [ - "EHqqq92V9", - "(2) the use of an account that does not exist", - "lockedfalse", - "1", - "initialPolicy" - ], - "description": "event details", - }, - { - "id": "SE2", - "type": "UserLoginFailed", - "severity": "info", - "time": "2015-03-31T08:00:48-0700", - "resource_id": "EHqqq92V9", - "formatted_parameter": - [ - "EHqqq92V9", - "(2) the use of an account that does not exist", - "lockedfalse", - "1", - "initialPolicy" - ], - "description": "event details", - }, - { - "id": "SE3", - "type": "UserLoggedOn", - "severity": "info", - "time": "2015-03-31T08:00:48-0700", - "resource_id": "admin", - "formatted_parameter": - [ - "admin", - ",Administrator", - "*", - "9.11.217.179", - "DSGUI", - "5.7.40.1303", - "initialPolicy", - "", - "HMCID: 1", - "fbb1149" - ], - "description": "event details", - }, - { - "id": "SE4", - "type": "UserLoggedOn", - "severity": "info", - "time": "2015-03-31T08:00:48-0700", - "resource_id": "admin", - "formatted_parameter": - [ - "admin", - ",Administrator", - "*", - "9.11.217.179", - "DSGUI", - "87.40.141.0", - "initialPolicy", - "9.123.236.47", - "HMCID: 1", - "59d2965a" - ], - "description": "event details", - }, + "server": {"status": "ok", "code": "", "message": "Operation done successfully."}, + "counts": {"data_counts": 19231, "total_counts": 19231}, + "data": { + "events": [ + { + "id": "SE1", + "type": "UserLoginFailed", + "severity": "info", + "time": "2015-03-31T08:00:48-0700", + "resource_id": "EHqqq92V9", + "formatted_parameter": [ + "EHqqq92V9", + "(2) the use of an account that does not exist", + "lockedfalse", + "1", + "initialPolicy", + ], + "description": "event details", + }, + { + "id": "SE2", + "type": "UserLoginFailed", + "severity": "info", + "time": "2015-03-31T08:00:48-0700", + "resource_id": "EHqqq92V9", + "formatted_parameter": [ + "EHqqq92V9", + "(2) the use of an account that does not exist", + "lockedfalse", + "1", + "initialPolicy", + ], + "description": "event details", + }, + { + "id": "SE3", + "type": "UserLoggedOn", + "severity": "info", + "time": "2015-03-31T08:00:48-0700", + "resource_id": "admin", + "formatted_parameter": [ + "admin", + ",Administrator", + "*", + "9.11.217.179", + "DSGUI", + "5.7.40.1303", + "initialPolicy", + "", + "HMCID: 1", + "fbb1149", + ], + "description": "event details", + }, + { + "id": "SE4", + "type": "UserLoggedOn", + "severity": "info", + "time": "2015-03-31T08:00:48-0700", + "resource_id": "admin", + "formatted_parameter": [ + "admin", + ",Administrator", + "*", + "9.11.217.179", + "DSGUI", + "87.40.141.0", + "initialPolicy", + "9.123.236.47", + "HMCID: 1", + "59d2965a", + ], + "description": "event details", + }, ] - } + }, } ONE = { - "server": - { - "status": "ok", - "code": "", - "message": "Operation done successfully." - }, - "counts": - { - "data_counts": 1, - "total_counts": 1 - }, - "data": - { - "events": - [ - { - "id": "SE1", - "type": "UserLoginFailed", - "severity": "info", - "time": "2015-03-31T08:00:48-0700", - "resource_id": "EHqqq92V9", - "formatted_parameter": - [ - "EHqqq92V9", - "(2) the use of an account that does not exist", - "lockedfalse", - "1", - "initialPolicy" - ], - "description": "event details", - }, + "server": {"status": "ok", "code": "", "message": "Operation done successfully."}, + "counts": {"data_counts": 1, "total_counts": 1}, + "data": { + "events": [ + { + "id": "SE1", + "type": "UserLoginFailed", + "severity": "info", + "time": "2015-03-31T08:00:48-0700", + "resource_id": "EHqqq92V9", + "formatted_parameter": [ + "EHqqq92V9", + "(2) the use of an account that does not exist", + "lockedfalse", + "1", + "initialPolicy", + ], + "description": "event details", + }, ] - } + }, } diff --git a/pyds8k/test/mock/flashcopies.py b/pyds8k/test/mock/flashcopies.py index 11a3a9c..b0934a6 100644 --- a/pyds8k/test/mock/flashcopies.py +++ b/pyds8k/test/mock/flashcopies.py @@ -18,102 +18,93 @@ "server": { "status": "ok", "code": "CMUC00183I", - "message": "Operation done successfully." - }, - "counts": { - "data_counts": 32, - "total_counts": 32 + "message": "Operation done successfully.", }, + "counts": {"data_counts": 32, "total_counts": 32}, 'data': { 'flashcopy': [ { 'id': '0000:0001', 'link': { 'rel': 'self', - 'href': 'https://localhost:8088/api/v1/cs/flashcopies/' - '0000:0001' + 'href': 'https://localhost:8088/api/v1/cs/flashcopies/0000:0001', }, 'source_volume': { 'id': '0000', 'link': { 'rel': 'self', - 'href': 'https://localhost:8088/api/v1/volumes/0000' - } + 'href': 'https://localhost:8088/api/v1/volumes/0000', + }, }, 'target_volume': { 'id': '0001', 'link': { 'rel': 'self', - 'href': 'https://localhost:8088/api/v1/volumes/0001' - } + 'href': 'https://localhost:8088/api/v1/volumes/0001', + }, }, 'out_of_sync_tracks': '0', - 'state': 'valid' + 'state': 'valid', }, { 'id': '1000:1001', 'link': { 'rel': 'self', - 'href': 'https://localhost:8088/api/v1/cs/flashcopies/' - '1000:1001' + 'href': 'https://localhost:8088/api/v1/cs/flashcopies/1000:1001', }, 'source_volume': { 'id': '1000', 'link': { 'rel': 'self', - 'href': 'https://localhost:8088/api/v1/volumes/1000' - } + 'href': 'https://localhost:8088/api/v1/volumes/1000', + }, }, 'target_volume': { 'id': '1001', 'link': { 'rel': 'self', - 'href': 'https://localhost:8088/api/v1/volumes/1001' - } + 'href': 'https://localhost:8088/api/v1/volumes/1001', + }, }, 'out_of_sync_tracks': '0', - 'state': 'valid' + 'state': 'valid', }, ] - } + }, } ONE = { "server": { "status": "ok", "code": "CMUC00183I", - "message": "Operation done successfully." - }, - "counts": { - "data_counts": 1, - "total_counts": 1 + "message": "Operation done successfully.", }, + "counts": {"data_counts": 1, "total_counts": 1}, 'data': { 'flashcopy': [ { 'id': '0000:0001', 'link': { 'rel': 'self', - 'href': 'https://localhost:8088/api/v1/cs/flashcopies/' - '0000:0001' + 'href': 'https://localhost:8088/api/v1/cs/flashcopies/0000:0001', }, 'source_volume': { 'id': '0000', 'link': { 'rel': 'self', - 'href': 'https://localhost:8088/api/v1/volumes/0000' - } + 'href': 'https://localhost:8088/api/v1/volumes/0000', + }, }, 'target_volume': { 'id': '0001', 'link': { 'rel': 'self', - 'href': 'https://localhost:8088/api/v1/volumes/0001' - } + 'href': 'https://localhost:8088/api/v1/volumes/0001', + }, }, 'out_of_sync_tracks': '0', - 'state': 'valid' + 'state': 'valid', }, ] - } + }, } diff --git a/pyds8k/test/mock/flashcopy.py b/pyds8k/test/mock/flashcopy.py index a306569..1fd0667 100644 --- a/pyds8k/test/mock/flashcopy.py +++ b/pyds8k/test/mock/flashcopy.py @@ -18,12 +18,9 @@ "server": { "status": "ok", "code": "CMUC00183I", - "message": "Operation done successfully." - }, - "counts": { - "data_counts": 32, - "total_counts": 32 + "message": "Operation done successfully.", }, + "counts": {"data_counts": 32, "total_counts": 32}, 'data': { 'flashcopy': [ { @@ -67,19 +64,16 @@ 'state': 'valid', }, ] - } + }, } ONE = { "server": { "status": "ok", "code": "CMUC00183I", - "message": "Operation done successfully." - }, - "counts": { - "data_counts": 1, - "total_counts": 1 + "message": "Operation done successfully.", }, + "counts": {"data_counts": 1, "total_counts": 1}, 'data': { 'flashcopy': [ { @@ -103,5 +97,5 @@ 'state': 'valid', }, ] - } + }, } diff --git a/pyds8k/test/mock/hmc/__init__.py b/pyds8k/test/mock/hmc/__init__.py new file mode 100644 index 0000000..de02cd2 --- /dev/null +++ b/pyds8k/test/mock/hmc/__init__.py @@ -0,0 +1,28 @@ +############################################################################## +# Copyright 2023 IBM Corp. +# +# 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 +# +# 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 importlib import import_module + +from pyds8k.test.utils import get_dir_mocks, get_mocks + +mocks = get_mocks(__file__) +dir_mocks = get_dir_mocks(__file__) +success_response_one = {} +success_response_all = {} + +for re in mocks: + success_response_one[re] = import_module(f'{__name__}.{re}').ONE + success_response_all[re] = import_module(f'{__name__}.{re}').ALL diff --git a/pyds8k/test/mock/hmc/certificate/__init__.py b/pyds8k/test/mock/hmc/certificate/__init__.py new file mode 100644 index 0000000..de02cd2 --- /dev/null +++ b/pyds8k/test/mock/hmc/certificate/__init__.py @@ -0,0 +1,28 @@ +############################################################################## +# Copyright 2023 IBM Corp. +# +# 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 +# +# 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 importlib import import_module + +from pyds8k.test.utils import get_dir_mocks, get_mocks + +mocks = get_mocks(__file__) +dir_mocks = get_dir_mocks(__file__) +success_response_one = {} +success_response_all = {} + +for re in mocks: + success_response_one[re] = import_module(f'{__name__}.{re}').ONE + success_response_all[re] = import_module(f'{__name__}.{re}').ALL diff --git a/pyds8k/test/mock/hmc/certificate/csr.py b/pyds8k/test/mock/hmc/certificate/csr.py new file mode 100644 index 0000000..6578a89 --- /dev/null +++ b/pyds8k/test/mock/hmc/certificate/csr.py @@ -0,0 +1,37 @@ +############################################################################## +# Copyright 2023 IBM Corp. +# +# 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 +# +# 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. +############################################################################## + +ONE = "-----BEGIN CERTIFICATE REQUEST-----\n\ + MIIDITCCAgkCAQAwgaQxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOWTEOMAwGA1UE\n\ + BwwFQXJtb2sxGzAZBgNVBAoMEkFuc2libGVDb2xsZWN0aW9uczEPMA0GA1UECwwG\n\ + RFM4MDAwMSIwIAYDVQQDDBl0YWxvcy50dWMuc3RnbGFicy5pYm0uY29tMSYwJAYJ\n\ + KoZIhvcNAQkBFhdhbnNpYmxlQGZha2Vfc2VydmVyLmNvbTCCASIwDQYJKoZIhvcN\n\ + AQEBBQADggEPADCCAQoCggEBAK9FUZZKlY8rNzhqGW92zFeGVe5IueZRSnbxP1FK\n\ + uHcUG9qPJEVeqD7yBIPG/QFBqgY8x19l1didFS8T6PfWr/pTOcyFBBnLKnYtKr94\n\ + i1iiKdEoN8DlhyZdVFnjO/SyjGDNfxhjrtVrN5oTbwDirlpO7lwlH0ZktooaC28f\n\ + jDvkhBGOmD4FHOcOZ2w9EKYcewtnqH+KM5CX+mw/bh5Fx4OU1KR6ymgFVuIR6Qkg\n\ + uYafLXgheppodfNjWRSw/MU4Zc30a2sRV/KjDog/wCBcf603fiwYDPV9MxWY4Yya\n\ + J0XaGPAvoIcftD+Mzro6ciVOfcrxWi/Xb5pAyZrwDGi9oWUCAwEAAaA3MDUGCSqG\n\ + SIb3DQEJDjEoMCYwJAYDVR0RBB0wG4IZdGFsb3MudHVjLnN0Z2xhYnMuaWJtLmNv\n\ + bTANBgkqhkiG9w0BAQsFAAOCAQEAZnHY3s8T53PtkO9/mpOs14CS9tLz3mEJiaFP\n\ + jMD1jVHgN4EIG6hQ5y96tFZc79lKE3+/ngRfXQC19SC39PUzzbC+80Nt6oU6+QGE\n\ + aGitz0GpG/yGvQXNIG93AKbj2axbOoNhCsMasn/Aby63xPd6kGR259FRmtyPwKzE\n\ + RmoflW1VQM4t3MiCY4ZONH+BUVYdi4/lEyYj2TbjlEUaVFBM1hSM6oAQkPaxxXIF\n\ + qyaE4npoAsxLO3N5XwC0MpPrxb5vjc7JgbcU3g53RuwWpNJ0xiE4beU4L8TF85Md\n\ + bXdP4rTszg7PP4K63BE9Fy+kM52usAUGO7th1lFgy3/U2A7xSA==\n\ + -----END CERTIFICATE REQUEST-----\n" + +ALL = ONE diff --git a/pyds8k/test/mock/host_ports.py b/pyds8k/test/mock/host_ports.py old mode 100755 new mode 100644 index 6812328..dd3acf2 --- a/pyds8k/test/mock/host_ports.py +++ b/pyds8k/test/mock/host_ports.py @@ -15,102 +15,98 @@ ############################################################################## ALL = { - "server": { - "status": "ok", - "code": "CMUC00183I", - "message": "Operation done successfully." - }, - "counts": { - "data_counts": 2, - "total_counts": 2 - }, - "data": { - "host_ports": [ - { - "wwpn": "210000E08B10A95C", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/host_ports/210000E08B10A95C" - }, - "state": "logged in", - "hosttype": "VMware", - "addrdiscovery": "lunpolling", - "lbs": "512", - "host": { - 'name': 'host1', - 'link': { - 'rel': 'self', - 'href': 'https://localhost:8088/api/v1/hosts/host1' - }, - }, - "ioport": { - 'id': 'I0200', - 'link': { - 'rel': 'self', - 'href': 'https://localhost:8088/api/v1/ioports/I0200' - }, - }, - }, - { - "wwpn": "210000E08B13D0BF", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/host_ports/210000E08B13D0BF" - }, - "state": "logged out", - "hosttype": "VMware", - "addrdiscovery": "lunpolling", - "lbs": "512", - "host": { - 'name': 'host1', - 'link': { - 'rel': 'self', - 'href': 'https://localhost:8088/api/v1/hosts/host1' - }, - }, - "ioport": "", - } - ] - } + "server": { + "status": "ok", + "code": "CMUC00183I", + "message": "Operation done successfully.", + }, + "counts": {"data_counts": 2, "total_counts": 2}, + "data": { + "host_ports": [ + { + "wwpn": "210000E08B10A95C", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/host_ports/210000E08B10A95C", + }, + "state": "logged in", + "hosttype": "VMware", + "addrdiscovery": "lunpolling", + "lbs": "512", + "host": { + 'name': 'host1', + 'link': { + 'rel': 'self', + 'href': 'https://localhost:8088/api/v1/hosts/host1', + }, + }, + "ioport": { + 'id': 'I0200', + 'link': { + 'rel': 'self', + 'href': 'https://localhost:8088/api/v1/ioports/I0200', + }, + }, + }, + { + "wwpn": "210000E08B13D0BF", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/host_ports/210000E08B13D0BF", + }, + "state": "logged out", + "hosttype": "VMware", + "addrdiscovery": "lunpolling", + "lbs": "512", + "host": { + 'name': 'host1', + 'link': { + 'rel': 'self', + 'href': 'https://localhost:8088/api/v1/hosts/host1', + }, + }, + "ioport": "", + }, + ] + }, } ONE = { - "server": { - "status": "ok", - "code": "CMUC00183I", - "message": "Operation done successfully." - }, - "counts": { - "data_counts": 1, - "total_counts": 1 - }, - "data": { - "host_ports": [ - { - "wwpn": "210000E08B10A95C", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/host_ports/210000E08B10A95C" - }, - "state": "logged in", - "hosttype": "VMware", - "addrdiscovery": "lunpolling", - "lbs": "512", - "host": { - 'name': 'host1', - 'link': { - 'rel': 'self', - 'href': 'https://localhost:8088/api/v1/hosts/host1' - }, - }, - "login_ports": [ - {'id': 'I0200', - 'link': {'rel': 'self', - 'href': 'https://localhost:8088/api/v1/ioports/I0200' - }, - }, + "server": { + "status": "ok", + "code": "CMUC00183I", + "message": "Operation done successfully.", + }, + "counts": {"data_counts": 1, "total_counts": 1}, + "data": { + "host_ports": [ + { + "wwpn": "210000E08B10A95C", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/host_ports/210000E08B10A95C", + }, + "state": "logged in", + "hosttype": "VMware", + "addrdiscovery": "lunpolling", + "lbs": "512", + "host": { + 'name': 'host1', + 'link': { + 'rel': 'self', + 'href': 'https://localhost:8088/api/v1/hosts/host1', + }, + }, + "login_ports": [ + { + 'id': 'I0200', + 'link': { + 'rel': 'self', + 'href': 'https://localhost:8088/api/v1/ioports/I0200', + }, + }, + ], + }, ] - }, - ] - } + }, } diff --git a/pyds8k/test/mock/hosts.py b/pyds8k/test/mock/hosts.py old mode 100755 new mode 100644 index b6b5777..7c5ebbe --- a/pyds8k/test/mock/hosts.py +++ b/pyds8k/test/mock/hosts.py @@ -18,12 +18,9 @@ "server": { "status": "ok", "code": "CMUC00183I", - "message": "Operation done successfully." - }, - "counts": { - "data_counts": 32, - "total_counts": 32 + "message": "Operation done successfully.", }, + "counts": {"data_counts": 32, "total_counts": 32}, 'data': { 'hosts': [ { @@ -53,7 +50,7 @@ }, 'link': { 'rel': 'self', - 'href': 'https://localhost:8088/api/v1/hosts/host1' + 'href': 'https://localhost:8088/api/v1/hosts/host1', }, }, { @@ -83,23 +80,20 @@ }, 'link': { 'rel': 'self', - 'href': 'https://localhost:8088/api/v1/hosts/host2' + 'href': 'https://localhost:8088/api/v1/hosts/host2', }, }, ] - } + }, } ONE = { "server": { "status": "ok", "code": "CMUC00183I", - "message": "Operation done successfully." - }, - "counts": { - "data_counts": 1, - "total_counts": 1 + "message": "Operation done successfully.", }, + "counts": {"data_counts": 1, "total_counts": 1}, 'data': { 'hosts': [ { @@ -129,9 +123,9 @@ }, 'link': { 'rel': 'self', - 'href': 'https://localhost:8088/api/v1/hosts/host1' + 'href': 'https://localhost:8088/api/v1/hosts/host1', }, }, ] - } + }, } diff --git a/pyds8k/test/mock/io_enclosures.py b/pyds8k/test/mock/io_enclosures.py index 1ec77e1..915f14e 100644 --- a/pyds8k/test/mock/io_enclosures.py +++ b/pyds8k/test/mock/io_enclosures.py @@ -15,23 +15,16 @@ ############################################################################## ALL = { - "server": { - "status": "ok", - "code": "", - "message": "Operation done successfully." - }, - "counts": { - "data_counts": 2, - "total_counts": 2 - }, + "server": {"status": "ok", "code": "", "message": "Operation done successfully."}, + "counts": {"data_counts": 2, "total_counts": 2}, "data": { "io_enclosures": [ { "id": "1", "link": { "rel": "self", - "href": "https://localhost:8088/api/v1/io_enclosures/1" - }, + "href": "https://localhost:8088/api/v1/io_enclosures/1", + }, "name": "io_enclosure1", "state": "online", }, @@ -39,37 +32,30 @@ "id": "2", "link": { "rel": "self", - "href": "https://localhost:8088/api/v1/io_enclosures/2" - }, + "href": "https://localhost:8088/api/v1/io_enclosures/2", + }, "name": "io_enclosure2", "state": "online", }, ] - } + }, } ONE = { - "server": { - "status": "ok", - "code": "", - "message": "Operation done successfully." - }, - "counts": { - "data_counts": 1, - "total_counts": 1 - }, + "server": {"status": "ok", "code": "", "message": "Operation done successfully."}, + "counts": {"data_counts": 1, "total_counts": 1}, "data": { "io_enclosures": [ { "id": "1", "link": { "rel": "self", - "href": "https://localhost:8088/api/v1/io_enclosures/1" - }, + "href": "https://localhost:8088/api/v1/io_enclosures/1", + }, "name": "io_enclosure1", "state": "online", }, ] - } + }, } diff --git a/pyds8k/test/mock/ioports.py b/pyds8k/test/mock/ioports.py old mode 100755 new mode 100644 index 1026255..874b7bd --- a/pyds8k/test/mock/ioports.py +++ b/pyds8k/test/mock/ioports.py @@ -15,152 +15,146 @@ ############################################################################## ALL = { - "server": { - "status": "ok", - "code": "CMUC00183I", - "message": "Operation done successfully." - }, - "counts": { - "data_counts": 32, - "total_counts": 32 - }, - "data": { - "ioports": [ - { - "id": "I0200", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/ioports/I0200" - }, - "state": "Online", - "protocol": "SCSI-FCP", - "wwpn": "500507630310003D", - "type": "Fibre Channel-SW", - "speed": "8 Gb/s", - "loc": "U1400.1B3.44001B3-P1-C1-T0", - "io_enclosure": { - "id": "2", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/io_enclosures/2" + "server": { + "status": "ok", + "code": "CMUC00183I", + "message": "Operation done successfully.", + }, + "counts": {"data_counts": 32, "total_counts": 32}, + "data": { + "ioports": [ + { + "id": "I0200", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/ioports/I0200", }, - } - }, - { - "id": "I0201", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/ioports/I0201" - }, - "state": "Online", - "protocol": "SCSI-FCP", - "wwpn": "500507630310403D", - "type": "Fibre Channel-SW", - "speed": "8 Gb/s", - "loc": "U1400.1B3.44001B3-P1-C1-T1", - "io_enclosure": { - "id": "2", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/io_enclosures/2" + "state": "Online", + "protocol": "SCSI-FCP", + "wwpn": "500507630310003D", + "type": "Fibre Channel-SW", + "speed": "8 Gb/s", + "loc": "U1400.1B3.44001B3-P1-C1-T0", + "io_enclosure": { + "id": "2", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/io_enclosures/2", + }, }, - } - }, - { - "id": "I0202", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/ioports/I0202" - }, - "state": "Online", - "protocol": "SCSI-FCP", - "wwpn": "500507630310803D", - "type": "Fibre Channel-SW", - "speed": "8 Gb/s", - "loc": "U1400.1B3.44001B3-P1-C1-T2", - "io_enclosure": { - "id": "2", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/io_enclosures/2" + }, + { + "id": "I0201", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/ioports/I0201", }, - } - }, - { - "id": "I0336", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/ioports/I0336" - }, - "state": "Offline", - "protocol": "SCSI-FCP", - "wwpn": "50050763035B803D", - "type": "Fibre Channel-LW", - "speed": "8 Gb/s", - "loc": "U1400.1B4.44001B4-P1-C4-T6", - "io_enclosure": { - "id": "2", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/io_enclosures/2" + "state": "Online", + "protocol": "SCSI-FCP", + "wwpn": "500507630310403D", + "type": "Fibre Channel-SW", + "speed": "8 Gb/s", + "loc": "U1400.1B3.44001B3-P1-C1-T1", + "io_enclosure": { + "id": "2", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/io_enclosures/2", + }, }, - } - }, - { - "id": "I0337", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/ioports/I0337" - }, - "state": "Offline", - "protocol": "SCSI-FCP", - "wwpn": "50050763035BC03D", - "type": "Fibre Channel-LW", - "speed": "8 Gb/s", - "loc": "U1400.1B4.44001B4-P1-C4-T7", - "io_enclosure": { - "id": "2", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/io_enclosures/2" + }, + { + "id": "I0202", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/ioports/I0202", }, - } - } - ] - } + "state": "Online", + "protocol": "SCSI-FCP", + "wwpn": "500507630310803D", + "type": "Fibre Channel-SW", + "speed": "8 Gb/s", + "loc": "U1400.1B3.44001B3-P1-C1-T2", + "io_enclosure": { + "id": "2", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/io_enclosures/2", + }, + }, + }, + { + "id": "I0336", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/ioports/I0336", + }, + "state": "Offline", + "protocol": "SCSI-FCP", + "wwpn": "50050763035B803D", + "type": "Fibre Channel-LW", + "speed": "8 Gb/s", + "loc": "U1400.1B4.44001B4-P1-C4-T6", + "io_enclosure": { + "id": "2", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/io_enclosures/2", + }, + }, + }, + { + "id": "I0337", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/ioports/I0337", + }, + "state": "Offline", + "protocol": "SCSI-FCP", + "wwpn": "50050763035BC03D", + "type": "Fibre Channel-LW", + "speed": "8 Gb/s", + "loc": "U1400.1B4.44001B4-P1-C4-T7", + "io_enclosure": { + "id": "2", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/io_enclosures/2", + }, + }, + }, + ] + }, } ONE = { - "server": { - "status": "ok", - "code": "CMUC00183I", - "message": "Operation done successfully." - }, - "counts": { - "data_counts": 1, - "total_counts": 1 - }, - "data": { - "ioports": [ - { - "id": "I0200", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/ioports/I0200" - }, - "state": "Online", - "protocol": "SCSI-FCP", - "wwpn": "500507630310003D", - "type": "Fibre Channel-SW", - "speed": "8 Gb/s", - "loc": "U1400.1B3.44001B3-P1-C1-T0", - "io_enclosure": { - "id": "2", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/io_enclosures/2" + "server": { + "status": "ok", + "code": "CMUC00183I", + "message": "Operation done successfully.", + }, + "counts": {"data_counts": 1, "total_counts": 1}, + "data": { + "ioports": [ + { + "id": "I0200", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/ioports/I0200", + }, + "state": "Online", + "protocol": "SCSI-FCP", + "wwpn": "500507630310003D", + "type": "Fibre Channel-SW", + "speed": "8 Gb/s", + "loc": "U1400.1B3.44001B3-P1-C1-T0", + "io_enclosure": { + "id": "2", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/io_enclosures/2", + }, }, - } - } - ] - } + } + ] + }, } diff --git a/pyds8k/test/mock/lss.py b/pyds8k/test/mock/lss.py old mode 100755 new mode 100644 index fb943f1..cc652b3 --- a/pyds8k/test/mock/lss.py +++ b/pyds8k/test/mock/lss.py @@ -15,169 +15,112 @@ ############################################################################## ALL = { - "server": - { - "status": "ok", - "code": "CMUC00183I", - "message": "Operation done successfully." - }, - "counts": - { - "data_counts": 5, - "total_counts": 5 - }, - "data": - { - "lss": - [ - { - "id": "00", - "link": - { - "rel": "self", - "href": "https://localhost:8088/api/v1/lss/00" - }, - "group": "0", - "addrgrp": "0", - "type": "fb", - "configvols": "25", - "volumes": - { - "link": - { - "rel": "self", - "href": "https://localhost:8088/api/v1/" - "lss/00/volumes" - } - } - }, - { - "id": "02", - "link": - { - "rel": "self", - "href": "https://localhost:8088/api/v1/" - "lss/02" - }, - "group": "0", - "addrgrp": "0", - "type": "fb", - "configvols": "16", - "volumes": - { - "link": - { - "rel": "self", - "href": "https://localhost:8088/api/v1/" - "lss/02/volumes" - } - } - }, - { - "id": "04", - "link": - { - "rel": "self", - "href": "https://localhost:8088/api/v1/lss/04" - }, - "group": "0", - "addrgrp": "0", - "type": "fb", - "configvols": "256", - "volumes": - { - "link": - { - "rel": "self", - "href": "https://localhost:8088/api/v1/" - "lss/04/volumes" - } - } - }, - { - "id": "06", - "link": - { - "rel": "self", - "href": "https://localhost:8088/api/v1/lss/06" - }, - "group": "0", - "addrgrp": "0", - "type": "fb", - "configvols": "256", - "volumes": - { - "link": - { - "rel": "self", - "href": "https://localhost:8088/api/v1/" - "lss/06/volumes" - } - } - }, - { - "id": "08", - "link": - { - "rel": "self", - "href": "https://localhost:8088/api/v1/lss/08" - }, - "group": "0", - "addrgrp": "0", - "type": "fb", - "configvols": "256", - "volumes": - { - "link": - { - "rel": "self", - "href": "https://localhost:8088/api/v1/" - "lss/08/volumes" - } - } - } - ] - } - } + "server": { + "status": "ok", + "code": "CMUC00183I", + "message": "Operation done successfully.", + }, + "counts": {"data_counts": 5, "total_counts": 5}, + "data": { + "lss": [ + { + "id": "00", + "link": {"rel": "self", "href": "https://localhost:8088/api/v1/lss/00"}, + "group": "0", + "addrgrp": "0", + "type": "fb", + "configvols": "25", + "volumes": { + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/lss/00/volumes", + } + }, + }, + { + "id": "02", + "link": {"rel": "self", "href": "https://localhost:8088/api/v1/lss/02"}, + "group": "0", + "addrgrp": "0", + "type": "fb", + "configvols": "16", + "volumes": { + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/lss/02/volumes", + } + }, + }, + { + "id": "04", + "link": {"rel": "self", "href": "https://localhost:8088/api/v1/lss/04"}, + "group": "0", + "addrgrp": "0", + "type": "fb", + "configvols": "256", + "volumes": { + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/lss/04/volumes", + } + }, + }, + { + "id": "06", + "link": {"rel": "self", "href": "https://localhost:8088/api/v1/lss/06"}, + "group": "0", + "addrgrp": "0", + "type": "fb", + "configvols": "256", + "volumes": { + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/lss/06/volumes", + } + }, + }, + { + "id": "08", + "link": {"rel": "self", "href": "https://localhost:8088/api/v1/lss/08"}, + "group": "0", + "addrgrp": "0", + "type": "fb", + "configvols": "256", + "volumes": { + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/lss/08/volumes", + } + }, + }, + ] + }, +} ONE = { - "server": - { - "status": "ok", - "code": "CMUC00183I", - "message": "Operation done successfully." - }, - "counts": - { - "data_counts": 1, - "total_counts": 1 - }, - "data": - { - "lss": - [ - { - "id": "00", - "link": - { - "rel": "self", - "href": "https://localhost:8088/api/v1/lss/00" - }, - "group": "0", - "addrgrp": "0", - "type": "fb", - "configvols": "25", - "volumes": - { - "link": - { - "rel": "self", - "href": "https://localhost:8088/api/v1/lss/" - "00/volumes" - } - } - } - ] - } - } + "server": { + "status": "ok", + "code": "CMUC00183I", + "message": "Operation done successfully.", + }, + "counts": {"data_counts": 1, "total_counts": 1}, + "data": { + "lss": [ + { + "id": "00", + "link": {"rel": "self", "href": "https://localhost:8088/api/v1/lss/00"}, + "group": "0", + "addrgrp": "0", + "type": "fb", + "configvols": "25", + "volumes": { + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/lss/00/volumes", + } + }, + } + ] + }, +} diff --git a/pyds8k/test/mock/mappings.py b/pyds8k/test/mock/mappings.py index 9d3787e..df18f12 100644 --- a/pyds8k/test/mock/mappings.py +++ b/pyds8k/test/mock/mappings.py @@ -18,12 +18,9 @@ "server": { "status": "ok", "code": "CMUC00183I", - "message": "Operation done successfully." - }, - "counts": { - "data_counts": 32, - "total_counts": 32 + "message": "Operation done successfully.", }, + "counts": {"data_counts": 32, "total_counts": 32}, 'data': { 'mappings': [ { @@ -37,8 +34,7 @@ }, 'link': { 'rel': 'self', - 'href': 'https://localhost:8088/api/v1/hosts/host1/' - 'mappings/00' + 'href': 'https://localhost:8088/api/v1/hosts/host1/mappings/00', }, }, { @@ -52,24 +48,20 @@ }, 'link': { 'rel': 'self', - 'href': 'https://localhost:8088/api/v1/hosts/host1/' - 'mappings/01' + 'href': 'https://localhost:8088/api/v1/hosts/host1/mappings/01', }, }, ] - } + }, } ONE = { "server": { "status": "ok", "code": "CMUC00183I", - "message": "Operation done successfully." - }, - "counts": { - "data_counts": 1, - "total_counts": 1 + "message": "Operation done successfully.", }, + "counts": {"data_counts": 1, "total_counts": 1}, 'data': { 'mappings': [ { @@ -83,10 +75,9 @@ }, 'link': { 'rel': 'self', - 'href': 'https://localhost:8088/api/v1/hosts/host1/' - 'mappings/00' + 'href': 'https://localhost:8088/api/v1/hosts/host1/mappings/00', }, }, ] - } + }, } diff --git a/pyds8k/test/mock/marrays.py b/pyds8k/test/mock/marrays.py index d52edfd..88f2e10 100644 --- a/pyds8k/test/mock/marrays.py +++ b/pyds8k/test/mock/marrays.py @@ -15,229 +15,215 @@ ############################################################################## ALL = { - "server": { - "status": "ok", - "code": "", - "message": "Operation done successfully." - }, - "counts": { - "data_counts": 12, - "total_counts": 12 - }, - "data": { - "marrays": [ - { - "id": "MA1", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/marrays/MA1" - }, - "disk_class": "enterprise", - "pool": { - "id": "P0", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/pools/P0" - } - } - }, - { - "id": "MA2", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/marrays/MA2" - }, - "disk_class": "enterprise", - "pool": { - "id": "P1", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/pools/P1" - } - } - }, - { - "id": "MA3", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/marrays/MA3" - }, - "disk_class": "enterprise", - "pool": { - "id": "P2", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/pools/P2" - } - } - }, - { - "id": "MA4", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/marrays/MA4" - }, - "disk_class": "enterprise", - "pool": { - "id": "P3", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/pools/P3" - } - } - }, - { - "id": "MA5", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/marrays/MA5" - }, - "disk_class": "enterprise", - "pool": { - "id": "P4", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/pools/P4" - } - } - }, - { - "id": "MA6", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/marrays/MA6" - }, - "disk_class": "enterprise", - "pool": { - "id": "P5", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/pools/P5" - } - } - }, - { - "id": "MA7", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/marrays/MA7" - }, - "disk_class": "enterprise", - "pool": { - "id": "P6", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/pools/P6" - } - } - }, - { - "id": "MA8", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/marrays/MA8" - }, - "disk_class": "enterprise", - "pool": { - "id": "P7", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/pools/P7" - } - } - }, - { - "id": "MA9", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/marrays/MA9" - }, - "disk_class": "enterprise", - "pool": { - "id": "P8", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/pools/P8" - } - } - }, - { - "id": "MA10", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/marrays/MA10" - }, - "disk_class": "enterprise", - "pool": { - "id": "P9", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/pools/P9" - } - } - }, - { - "id": "MA11", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/marrays/MA11" - }, - "disk_class": "enterprise", - "pool": { - "id": "P10", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/pools/P10" - } - } - }, - { - "id": "MA12", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/marrays/MA12" - }, - "disk_class": "enterprise", - "pool": { - "id": "P11", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/pools/P11" - } - } - } - ] - } + "server": {"status": "ok", "code": "", "message": "Operation done successfully."}, + "counts": {"data_counts": 12, "total_counts": 12}, + "data": { + "marrays": [ + { + "id": "MA1", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/marrays/MA1", + }, + "disk_class": "enterprise", + "pool": { + "id": "P0", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/pools/P0", + }, + }, + }, + { + "id": "MA2", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/marrays/MA2", + }, + "disk_class": "enterprise", + "pool": { + "id": "P1", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/pools/P1", + }, + }, + }, + { + "id": "MA3", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/marrays/MA3", + }, + "disk_class": "enterprise", + "pool": { + "id": "P2", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/pools/P2", + }, + }, + }, + { + "id": "MA4", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/marrays/MA4", + }, + "disk_class": "enterprise", + "pool": { + "id": "P3", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/pools/P3", + }, + }, + }, + { + "id": "MA5", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/marrays/MA5", + }, + "disk_class": "enterprise", + "pool": { + "id": "P4", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/pools/P4", + }, + }, + }, + { + "id": "MA6", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/marrays/MA6", + }, + "disk_class": "enterprise", + "pool": { + "id": "P5", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/pools/P5", + }, + }, + }, + { + "id": "MA7", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/marrays/MA7", + }, + "disk_class": "enterprise", + "pool": { + "id": "P6", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/pools/P6", + }, + }, + }, + { + "id": "MA8", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/marrays/MA8", + }, + "disk_class": "enterprise", + "pool": { + "id": "P7", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/pools/P7", + }, + }, + }, + { + "id": "MA9", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/marrays/MA9", + }, + "disk_class": "enterprise", + "pool": { + "id": "P8", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/pools/P8", + }, + }, + }, + { + "id": "MA10", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/marrays/MA10", + }, + "disk_class": "enterprise", + "pool": { + "id": "P9", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/pools/P9", + }, + }, + }, + { + "id": "MA11", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/marrays/MA11", + }, + "disk_class": "enterprise", + "pool": { + "id": "P10", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/pools/P10", + }, + }, + }, + { + "id": "MA12", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/marrays/MA12", + }, + "disk_class": "enterprise", + "pool": { + "id": "P11", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/pools/P11", + }, + }, + }, + ] + }, } ONE = { - "server": { - "status": "ok", - "code": "", - "message": "Operation done successfully." - }, - "counts": { - "data_counts": 12, - "total_counts": 12 - }, - "data": { - "marrays": [ - { - "id": "MA1", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/marrays/MA1" - }, - "disk_class": "enterprise", - "pool": { - "id": "P0", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/pools/P0" - } - } - }, - ] - } - } + "server": {"status": "ok", "code": "", "message": "Operation done successfully."}, + "counts": {"data_counts": 12, "total_counts": 12}, + "data": { + "marrays": [ + { + "id": "MA1", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/marrays/MA1", + }, + "disk_class": "enterprise", + "pool": { + "id": "P0", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/pools/P0", + }, + }, + }, + ] + }, +} diff --git a/pyds8k/test/mock/nodes.py b/pyds8k/test/mock/nodes.py index 78f6d6d..515701a 100644 --- a/pyds8k/test/mock/nodes.py +++ b/pyds8k/test/mock/nodes.py @@ -14,67 +14,45 @@ # limitations under the License. ############################################################################## -ALL = {"server": - { - "status": "ok", - "code": "", - "message": "Operation done successfully." - }, - "counts": - { - "data_counts": 2, - "total_counts": 2 - }, - "data": - { - "nodes": - [ - { - "id": "00", - "link": - { - "rel": "self", - "href": "https://localhost:8088/api/v1/nodes/00" - }, - "state": "online" - }, - { - "id": "01", - "link": - { - "rel": "self", - "href": "https://localhost:8088/api/v1/nodes/01" - }, - "state": "online" - } - ] - }} +ALL = { + "server": {"status": "ok", "code": "", "message": "Operation done successfully."}, + "counts": {"data_counts": 2, "total_counts": 2}, + "data": { + "nodes": [ + { + "id": "00", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/nodes/00", + }, + "state": "online", + }, + { + "id": "01", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/nodes/01", + }, + "state": "online", + }, + ] + }, +} -ONE = {"server": - { - "status": "ok", - "code": "", - "message": "Operation done successfully." - }, - "counts": - { - "data_counts": 1, - "total_counts": 1 - }, - "data": - { - "nodes": - [ - { - "id": "00", - "link": - { - "rel": "self", - "href": "https://localhost:8088/api/v1/nodes/00" - }, - "state": "online" - } - ] - } - } +ONE = { + "server": {"status": "ok", "code": "", "message": "Operation done successfully."}, + "counts": {"data_counts": 1, "total_counts": 1}, + "data": { + "nodes": [ + { + "id": "00", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/nodes/00", + }, + "state": "online", + } + ] + }, +} diff --git a/pyds8k/test/mock/pools.py b/pyds8k/test/mock/pools.py old mode 100755 new mode 100644 index 4d82b59..c8b646d --- a/pyds8k/test/mock/pools.py +++ b/pyds8k/test/mock/pools.py @@ -15,223 +15,213 @@ ############################################################################## ALL = { - "server": { - "status": "ok", - "code": "CMUC00183I", - "message": "Operation done successfully." - }, - "counts": { - "data_counts": 6, - "total_counts": 6 - }, - "data": { - "pools": [ - { - "id": "P1", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/pools/P1" - }, - "name": "PoolName_ckd1_b", - "node": "1", - "stgtype": "ckd", - "cap": "439635", - "capalloc": "25599", - "capavail": "414036", - "overprovisioned": "", - 'real_capacity_allocated_on_ese': '0', - 'virtual_capacity_allocated_on_ese': '0', - "easytier": "none", - "tieralloc": - [ - { - "tier": "ENT", - "assigned": "0", - "cap": "446676598784", - "allocated": "0" - } - ], - "threshold": "15", - "eserep": { - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/pools/P1/eserep" - } - }, - "tserep": { - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/pools/P1/tserep" - } - }, - "volumes": "" - }, - { - "id": "P2", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/pools/P2" - }, - "name": "regression", - "node": "0", - "stgtype": "fb", - "cap": "512", - "capalloc": "512", - "capavail": "512", - "overprovisioned": "0.6", - 'real_capacity_allocated_on_ese': '0', - 'virtual_capacity_allocated_on_ese': '0', - "threshold": "15", - "eserep": "", - "tserep": { - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/pools/P2/tserep" - } - }, - "volumes": { - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/pools/P2/volumes" - } - } - }, - { - "id": "P3", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/pools/P3" - }, - "name": "vPool", - "node": "1", - "stgtype": "ckd", - "cap": "518658", - "capalloc": "0", - "capavail": "518658", - "overprovisioned": "0.0", - 'real_capacity_allocated_on_ese': '0', - 'virtual_capacity_allocated_on_ese': '0', - "threshold": "15", - "eserep": "", - "tserep": "", - "volumes": "" - }, - { - "id": "P4", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/pools/P4" - }, - "name": "p5", - "node": "0", - "stgtype": "fb", - "cap": "0", - "capalloc": "0", - "capavail": "0", - "overprovisioned": "", - 'real_capacity_allocated_on_ese': '0', - 'virtual_capacity_allocated_on_ese': '0', - "easytier": "none", - "tieralloc": - [ - { - "tier": "ENT", - "assigned": "0", - "cap": "446676598784", - "allocated": "0" - } - ], - "threshold": "15", - "eserep": "", - "tserep": "", - "volumes": "" - }, - { - "id": "P6", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/pools/P6" - }, - "name": "fb_odd", - "node": "0", - "stgtype": "fb", - "cap": "0", - "capalloc": "0", - "capavail": "0", - "overprovisioned": "", - 'real_capacity_allocated_on_ese': '0', - 'virtual_capacity_allocated_on_ese': '0', - "easytier": "none", - "tieralloc": - [ - { - "tier": "ENT", - "assigned": "0", - "cap": "446676598784", - "allocated": "0" - } - ], - "threshold": "15", - "eserep": "", - "tserep": "", - "volumes": "" - } - ] - } + "server": { + "status": "ok", + "code": "CMUC00183I", + "message": "Operation done successfully.", + }, + "counts": {"data_counts": 6, "total_counts": 6}, + "data": { + "pools": [ + { + "id": "P1", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/pools/P1", + }, + "name": "PoolName_ckd1_b", + "node": "1", + "stgtype": "ckd", + "cap": "439635", + "capalloc": "25599", + "capavail": "414036", + "overprovisioned": "", + 'real_capacity_allocated_on_ese': '0', + 'virtual_capacity_allocated_on_ese': '0', + "easytier": "none", + "tieralloc": [ + { + "tier": "ENT", + "assigned": "0", + "cap": "446676598784", + "allocated": "0", + } + ], + "threshold": "15", + "eserep": { + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/pools/P1/eserep", + } + }, + "tserep": { + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/pools/P1/tserep", + } + }, + "volumes": "", + }, + { + "id": "P2", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/pools/P2", + }, + "name": "regression", + "node": "0", + "stgtype": "fb", + "cap": "512", + "capalloc": "512", + "capavail": "512", + "overprovisioned": "0.6", + 'real_capacity_allocated_on_ese': '0', + 'virtual_capacity_allocated_on_ese': '0', + "threshold": "15", + "eserep": "", + "tserep": { + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/pools/P2/tserep", + } + }, + "volumes": { + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/pools/P2/volumes", + } + }, + }, + { + "id": "P3", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/pools/P3", + }, + "name": "vPool", + "node": "1", + "stgtype": "ckd", + "cap": "518658", + "capalloc": "0", + "capavail": "518658", + "overprovisioned": "0.0", + 'real_capacity_allocated_on_ese': '0', + 'virtual_capacity_allocated_on_ese': '0', + "threshold": "15", + "eserep": "", + "tserep": "", + "volumes": "", + }, + { + "id": "P4", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/pools/P4", + }, + "name": "p5", + "node": "0", + "stgtype": "fb", + "cap": "0", + "capalloc": "0", + "capavail": "0", + "overprovisioned": "", + 'real_capacity_allocated_on_ese': '0', + 'virtual_capacity_allocated_on_ese': '0', + "easytier": "none", + "tieralloc": [ + { + "tier": "ENT", + "assigned": "0", + "cap": "446676598784", + "allocated": "0", + } + ], + "threshold": "15", + "eserep": "", + "tserep": "", + "volumes": "", + }, + { + "id": "P6", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/pools/P6", + }, + "name": "fb_odd", + "node": "0", + "stgtype": "fb", + "cap": "0", + "capalloc": "0", + "capavail": "0", + "overprovisioned": "", + 'real_capacity_allocated_on_ese': '0', + 'virtual_capacity_allocated_on_ese': '0', + "easytier": "none", + "tieralloc": [ + { + "tier": "ENT", + "assigned": "0", + "cap": "446676598784", + "allocated": "0", + } + ], + "threshold": "15", + "eserep": "", + "tserep": "", + "volumes": "", + }, + ] + }, } ONE = { - "server": { - "status": "ok", - "code": "CMUC00183I", - "message": "Operation done successfully." - }, - "counts": { - "data_counts": 1, - "total_counts": 1 - }, - "data": { - "pools": [ - { - "id": "P1", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/pools/P1" - }, - "name": "PoolName_ckd1_b", - "node": "1", - "stgtype": "ckd", - "cap": "439635", - "capalloc": "25599", - "capavail": "414036", - "overprovisioned": "", - 'real_capacity_allocated_on_ese': '0', - 'virtual_capacity_allocated_on_ese': '0', - "easytier": "none", - "tieralloc": - [ - { - "tier": "ENT", - "assigned": "0", - "cap": "446676598784", - "allocated": "0" - } - ], - "threshold": "15", - "eserep": { - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/pools/P1/eserep" - } - }, - "tserep": { - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/pools/P1/tserep" - } - }, - "volumes": "" - }, - ] - } + "server": { + "status": "ok", + "code": "CMUC00183I", + "message": "Operation done successfully.", + }, + "counts": {"data_counts": 1, "total_counts": 1}, + "data": { + "pools": [ + { + "id": "P1", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/pools/P1", + }, + "name": "PoolName_ckd1_b", + "node": "1", + "stgtype": "ckd", + "cap": "439635", + "capalloc": "25599", + "capavail": "414036", + "overprovisioned": "", + 'real_capacity_allocated_on_ese': '0', + 'virtual_capacity_allocated_on_ese': '0', + "easytier": "none", + "tieralloc": [ + { + "tier": "ENT", + "assigned": "0", + "cap": "446676598784", + "allocated": "0", + } + ], + "threshold": "15", + "eserep": { + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/pools/P1/eserep", + } + }, + "tserep": { + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/pools/P1/tserep", + } + }, + "volumes": "", + }, + ] + }, } diff --git a/pyds8k/test/mock/pprc.py b/pyds8k/test/mock/pprc.py index a3163e1..7295bbe 100644 --- a/pyds8k/test/mock/pprc.py +++ b/pyds8k/test/mock/pprc.py @@ -15,97 +15,93 @@ ############################################################################## ALL = { - "server": { - "status": "ok", - "code": "CMUC00183I", - "message": "Operation done successfully." - }, - "counts": { - "data_counts": 32, - "total_counts": 32 - }, - 'data': { - 'pprc': [{ - 'sourcevolume': { - 'id': '0000', - 'link': { - 'rel': 'self', - 'href': 'https://localhost:8088/api/v1/volumes/0000', - }, - }, - 'targetvolume': { - 'id': '0001', - 'link': { - 'rel': 'self', - 'href': 'https://localhost:8088/api/v1/volumes/0001', - }, - }, - 'targetsystem': { - 'id': 'remote_ds8k', - 'link': {}, - }, - 'type': 'globalcopy', - 'state': 'copy_pending', - }, { - 'sourcevolume': { - 'id': '1000', - 'link': { - 'rel': 'self', - 'href': 'https://localhost:8088/api/v1/volumes/1000', + "server": { + "status": "ok", + "code": "CMUC00183I", + "message": "Operation done successfully.", + }, + "counts": {"data_counts": 32, "total_counts": 32}, + 'data': { + 'pprc': [ + { + 'sourcevolume': { + 'id': '0000', + 'link': { + 'rel': 'self', + 'href': 'https://localhost:8088/api/v1/volumes/0000', + }, + }, + 'targetvolume': { + 'id': '0001', + 'link': { + 'rel': 'self', + 'href': 'https://localhost:8088/api/v1/volumes/0001', + }, + }, + 'targetsystem': { + 'id': 'remote_ds8k', + 'link': {}, + }, + 'type': 'globalcopy', + 'state': 'copy_pending', }, - }, - 'targetvolume': { - 'id': '1001', - 'link': { - 'rel': 'self', - 'href': 'https://localhost:8088/api/v1/volumes/1001', + { + 'sourcevolume': { + 'id': '1000', + 'link': { + 'rel': 'self', + 'href': 'https://localhost:8088/api/v1/volumes/1000', + }, + }, + 'targetvolume': { + 'id': '1001', + 'link': { + 'rel': 'self', + 'href': 'https://localhost:8088/api/v1/volumes/1001', + }, + }, + 'targetsystem': { + 'id': 'remote_ds8k', + 'link': {}, + }, + 'type': 'globalcopy', + 'state': 'copy_pending', }, - }, - 'targetsystem': { - 'id': 'remote_ds8k', - 'link': {}, - }, - 'type': 'globalcopy', - 'state': 'copy_pending', + ] }, - ] - } } ONE = { - "server": { - "status": "ok", - "code": "CMUC00183I", - "message": "Operation done successfully." - }, - "counts": { - "data_counts": 1, - "total_counts": 1 - }, - 'data': { - 'pprc': [ - { - 'sourcevolume': { - 'id': '0000', - 'link': { - 'rel': 'self', - 'href': 'https://localhost:8088/api/v1/volumes/0000', - }, - }, - 'targetvolume': { - 'id': '0001', - 'link': { - 'rel': 'self', - 'href': 'https://localhost:8088/api/v1/volumes/0001', - }, - }, - 'targetsystem': { - 'id': 'remote_ds8k', - 'link': {}, - }, - 'type': 'globalcopy', - 'state': 'copy_pending', - }, - ] - } + "server": { + "status": "ok", + "code": "CMUC00183I", + "message": "Operation done successfully.", + }, + "counts": {"data_counts": 1, "total_counts": 1}, + 'data': { + 'pprc': [ + { + 'sourcevolume': { + 'id': '0000', + 'link': { + 'rel': 'self', + 'href': 'https://localhost:8088/api/v1/volumes/0000', + }, + }, + 'targetvolume': { + 'id': '0001', + 'link': { + 'rel': 'self', + 'href': 'https://localhost:8088/api/v1/volumes/0001', + }, + }, + 'targetsystem': { + 'id': 'remote_ds8k', + 'link': {}, + }, + 'type': 'globalcopy', + 'state': 'copy_pending', + }, + ] + }, } diff --git a/pyds8k/test/mock/resource_groups.py b/pyds8k/test/mock/resource_groups.py new file mode 100644 index 0000000..128c7f9 --- /dev/null +++ b/pyds8k/test/mock/resource_groups.py @@ -0,0 +1,2673 @@ +############################################################################## +# Copyright 2022 IBM Corp. +# +# 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 +# +# 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. +############################################################################## + +ALL = { + "server": {"status": "ok", "code": "", "message": "Operation done successfully."}, + "counts": {"data_counts": 4, "total_counts": 4}, + "data": { + "resource_groups": [ + { + "id": "RG0", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/resource_groups/RG0", + }, + "name": "Default_Resource_Group", + "state": "normal", + "label": "PUBLIC", + "cs_global": "PUBLIC", + "pass_global": "PUBLIC", + "gm_masters": [ + "00", + "01", + "02", + "03", + "04", + "05", + "06", + "07", + "08", + "09", + "0A", + "0B", + "0C", + "0D", + "0E", + "0F", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "1A", + "1B", + "1C", + "1D", + "1E", + "1F", + "20", + "21", + "22", + "23", + "24", + "25", + "26", + "27", + "28", + "29", + "2A", + "2B", + "2C", + "2D", + "2E", + "2F", + "30", + "31", + "32", + "33", + "34", + "35", + "36", + "37", + "38", + "39", + "3A", + "3B", + "3C", + "3D", + "3E", + "3F", + "40", + "41", + "42", + "43", + "44", + "45", + "46", + "47", + "48", + "49", + "4A", + "4B", + "4C", + "4D", + "4E", + "4F", + "50", + "51", + "52", + "53", + "54", + "55", + "56", + "57", + "58", + "59", + "5A", + "5B", + "5C", + "5D", + "5E", + "5F", + "60", + "61", + "62", + "63", + "64", + "65", + "66", + "67", + "68", + "69", + "6A", + "6B", + "6C", + "6D", + "6E", + "6F", + "70", + "71", + "72", + "73", + "74", + "75", + "76", + "77", + "78", + "79", + "7A", + "7B", + "7C", + "7D", + "7E", + "7F", + "80", + "81", + "82", + "83", + "84", + "85", + "86", + "87", + "88", + "89", + "8A", + "8B", + "8C", + "8D", + "8E", + "8F", + "90", + "91", + "92", + "93", + "94", + "95", + "96", + "97", + "98", + "99", + "9A", + "9B", + "9C", + "9D", + "9E", + "9F", + "A0", + "A1", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "AA", + "AB", + "AC", + "AD", + "AE", + "AF", + "B0", + "B1", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "BA", + "BB", + "BC", + "BD", + "BE", + "BF", + "C0", + "C1", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "CA", + "CB", + "CC", + "CD", + "CE", + "CF", + "D0", + "D1", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "DA", + "DB", + "DC", + "DD", + "DE", + "DF", + "E0", + "E1", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "EA", + "EB", + "EC", + "ED", + "EE", + "EF", + "F0", + "F1", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "FA", + "FB", + "FC", + "FD", + "FE", + "FF", + ], + "gm_sessions": [ + "00", + "01", + "02", + "03", + "04", + "05", + "06", + "07", + "08", + "09", + "0A", + "0B", + "0C", + "0D", + "0E", + "0F", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "1A", + "1B", + "1C", + "1D", + "1E", + "1F", + "20", + "21", + "22", + "23", + "24", + "25", + "26", + "27", + "28", + "29", + "2A", + "2B", + "2C", + "2D", + "2E", + "2F", + "30", + "31", + "32", + "33", + "34", + "35", + "36", + "37", + "38", + "39", + "3A", + "3B", + "3C", + "3D", + "3E", + "3F", + "40", + "41", + "42", + "43", + "44", + "45", + "46", + "47", + "48", + "49", + "4A", + "4B", + "4C", + "4D", + "4E", + "4F", + "50", + "51", + "52", + "53", + "54", + "55", + "56", + "57", + "58", + "59", + "5A", + "5B", + "5C", + "5D", + "5E", + "5F", + "60", + "61", + "62", + "63", + "64", + "65", + "66", + "67", + "68", + "69", + "6A", + "6B", + "6C", + "6D", + "6E", + "6F", + "70", + "71", + "72", + "73", + "74", + "75", + "76", + "77", + "78", + "79", + "7A", + "7B", + "7C", + "7D", + "7E", + "7F", + "80", + "81", + "82", + "83", + "84", + "85", + "86", + "87", + "88", + "89", + "8A", + "8B", + "8C", + "8D", + "8E", + "8F", + "90", + "91", + "92", + "93", + "94", + "95", + "96", + "97", + "98", + "99", + "9A", + "9B", + "9C", + "9D", + "9E", + "9F", + "A0", + "A1", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "AA", + "AB", + "AC", + "AD", + "AE", + "AF", + "B0", + "B1", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "BA", + "BB", + "BC", + "BD", + "BE", + "BF", + "C0", + "C1", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "CA", + "CB", + "CC", + "CD", + "CE", + "CF", + "D0", + "D1", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "DA", + "DB", + "DC", + "DD", + "DE", + "DF", + "E0", + "E1", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "EA", + "EB", + "EC", + "ED", + "EE", + "EF", + "F0", + "F1", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "FA", + "FB", + "FC", + "FD", + "FE", + "FF", + ], + }, + { + "id": "RG1", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/resource_groups/RG1", + }, + "name": "group1", + "state": "normal", + "label": "group1", + "cs_global": "PUBLIC", + "pass_global": "PUBLIC", + "gm_masters": [ + "00", + "01", + "02", + "03", + "04", + "05", + "06", + "07", + "08", + "09", + "0A", + "0B", + "0C", + "0D", + "0E", + "0F", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "1A", + "1B", + "1C", + "1D", + "1E", + "1F", + "20", + "21", + "22", + "23", + "24", + "25", + "26", + "27", + "28", + "29", + "2A", + "2B", + "2C", + "2D", + "2E", + "2F", + "30", + "31", + "32", + "33", + "34", + "35", + "36", + "37", + "38", + "39", + "3A", + "3B", + "3C", + "3D", + "3E", + "3F", + "40", + "41", + "42", + "43", + "44", + "45", + "46", + "47", + "48", + "49", + "4A", + "4B", + "4C", + "4D", + "4E", + "4F", + "50", + "51", + "52", + "53", + "54", + "55", + "56", + "57", + "58", + "59", + "5A", + "5B", + "5C", + "5D", + "5E", + "5F", + "60", + "61", + "62", + "63", + "64", + "65", + "66", + "67", + "68", + "69", + "6A", + "6B", + "6C", + "6D", + "6E", + "6F", + "70", + "71", + "72", + "73", + "74", + "75", + "76", + "77", + "78", + "79", + "7A", + "7B", + "7C", + "7D", + "7E", + "7F", + "80", + "81", + "82", + "83", + "84", + "85", + "86", + "87", + "88", + "89", + "8A", + "8B", + "8C", + "8D", + "8E", + "8F", + "90", + "91", + "92", + "93", + "94", + "95", + "96", + "97", + "98", + "99", + "9A", + "9B", + "9C", + "9D", + "9E", + "9F", + "A0", + "A1", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "AA", + "AB", + "AC", + "AD", + "AE", + "AF", + "B0", + "B1", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "BA", + "BB", + "BC", + "BD", + "BE", + "BF", + "C0", + "C1", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "CA", + "CB", + "CC", + "CD", + "CE", + "CF", + "D0", + "D1", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "DA", + "DB", + "DC", + "DD", + "DE", + "DF", + "E0", + "E1", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "EA", + "EB", + "EC", + "ED", + "EE", + "EF", + "F0", + "F1", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "FA", + "FB", + "FC", + "FD", + "FE", + "FF", + ], + "gm_sessions": [ + "00", + "01", + "02", + "03", + "04", + "05", + "06", + "07", + "08", + "09", + "0A", + "0B", + "0C", + "0D", + "0E", + "0F", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "1A", + "1B", + "1C", + "1D", + "1E", + "1F", + "20", + "21", + "22", + "23", + "24", + "25", + "26", + "27", + "28", + "29", + "2A", + "2B", + "2C", + "2D", + "2E", + "2F", + "30", + "31", + "32", + "33", + "34", + "35", + "36", + "37", + "38", + "39", + "3A", + "3B", + "3C", + "3D", + "3E", + "3F", + "40", + "41", + "42", + "43", + "44", + "45", + "46", + "47", + "48", + "49", + "4A", + "4B", + "4C", + "4D", + "4E", + "4F", + "50", + "51", + "52", + "53", + "54", + "55", + "56", + "57", + "58", + "59", + "5A", + "5B", + "5C", + "5D", + "5E", + "5F", + "60", + "61", + "62", + "63", + "64", + "65", + "66", + "67", + "68", + "69", + "6A", + "6B", + "6C", + "6D", + "6E", + "6F", + "70", + "71", + "72", + "73", + "74", + "75", + "76", + "77", + "78", + "79", + "7A", + "7B", + "7C", + "7D", + "7E", + "7F", + "80", + "81", + "82", + "83", + "84", + "85", + "86", + "87", + "88", + "89", + "8A", + "8B", + "8C", + "8D", + "8E", + "8F", + "90", + "91", + "92", + "93", + "94", + "95", + "96", + "97", + "98", + "99", + "9A", + "9B", + "9C", + "9D", + "9E", + "9F", + "A0", + "A1", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "AA", + "AB", + "AC", + "AD", + "AE", + "AF", + "B0", + "B1", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "BA", + "BB", + "BC", + "BD", + "BE", + "BF", + "C0", + "C1", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "CA", + "CB", + "CC", + "CD", + "CE", + "CF", + "D0", + "D1", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "DA", + "DB", + "DC", + "DD", + "DE", + "DF", + "E0", + "E1", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "EA", + "EB", + "EC", + "ED", + "EE", + "EF", + "F0", + "F1", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "FA", + "FB", + "FC", + "FD", + "FE", + "FF", + ], + }, + { + "id": "RG2", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/resource_groups/RG2", + }, + "name": "group2", + "state": "normal", + "label": "group2", + "cs_global": "PUBLIC", + "pass_global": "PUBLIC", + "gm_masters": [ + "00", + "01", + "02", + "03", + "04", + "05", + "06", + "07", + "08", + "09", + "0A", + "0B", + "0C", + "0D", + "0E", + "0F", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "1A", + "1B", + "1C", + "1D", + "1E", + "1F", + "20", + "21", + "22", + "23", + "24", + "25", + "26", + "27", + "28", + "29", + "2A", + "2B", + "2C", + "2D", + "2E", + "2F", + "30", + "31", + "32", + "33", + "34", + "35", + "36", + "37", + "38", + "39", + "3A", + "3B", + "3C", + "3D", + "3E", + "3F", + "40", + "41", + "42", + "43", + "44", + "45", + "46", + "47", + "48", + "49", + "4A", + "4B", + "4C", + "4D", + "4E", + "4F", + "50", + "51", + "52", + "53", + "54", + "55", + "56", + "57", + "58", + "59", + "5A", + "5B", + "5C", + "5D", + "5E", + "5F", + "60", + "61", + "62", + "63", + "64", + "65", + "66", + "67", + "68", + "69", + "6A", + "6B", + "6C", + "6D", + "6E", + "6F", + "70", + "71", + "72", + "73", + "74", + "75", + "76", + "77", + "78", + "79", + "7A", + "7B", + "7C", + "7D", + "7E", + "7F", + "80", + "81", + "82", + "83", + "84", + "85", + "86", + "87", + "88", + "89", + "8A", + "8B", + "8C", + "8D", + "8E", + "8F", + "90", + "91", + "92", + "93", + "94", + "95", + "96", + "97", + "98", + "99", + "9A", + "9B", + "9C", + "9D", + "9E", + "9F", + "A0", + "A1", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "AA", + "AB", + "AC", + "AD", + "AE", + "AF", + "B0", + "B1", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "BA", + "BB", + "BC", + "BD", + "BE", + "BF", + "C0", + "C1", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "CA", + "CB", + "CC", + "CD", + "CE", + "CF", + "D0", + "D1", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "DA", + "DB", + "DC", + "DD", + "DE", + "DF", + "E0", + "E1", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "EA", + "EB", + "EC", + "ED", + "EE", + "EF", + "F0", + "F1", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "FA", + "FB", + "FC", + "FD", + "FE", + "FF", + ], + "gm_sessions": [ + "00", + "01", + "02", + "03", + "04", + "05", + "06", + "07", + "08", + "09", + "0A", + "0B", + "0C", + "0D", + "0E", + "0F", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "1A", + "1B", + "1C", + "1D", + "1E", + "1F", + "20", + "21", + "22", + "23", + "24", + "25", + "26", + "27", + "28", + "29", + "2A", + "2B", + "2C", + "2D", + "2E", + "2F", + "30", + "31", + "32", + "33", + "34", + "35", + "36", + "37", + "38", + "39", + "3A", + "3B", + "3C", + "3D", + "3E", + "3F", + "40", + "41", + "42", + "43", + "44", + "45", + "46", + "47", + "48", + "49", + "4A", + "4B", + "4C", + "4D", + "4E", + "4F", + "50", + "51", + "52", + "53", + "54", + "55", + "56", + "57", + "58", + "59", + "5A", + "5B", + "5C", + "5D", + "5E", + "5F", + "60", + "61", + "62", + "63", + "64", + "65", + "66", + "67", + "68", + "69", + "6A", + "6B", + "6C", + "6D", + "6E", + "6F", + "70", + "71", + "72", + "73", + "74", + "75", + "76", + "77", + "78", + "79", + "7A", + "7B", + "7C", + "7D", + "7E", + "7F", + "80", + "81", + "82", + "83", + "84", + "85", + "86", + "87", + "88", + "89", + "8A", + "8B", + "8C", + "8D", + "8E", + "8F", + "90", + "91", + "92", + "93", + "94", + "95", + "96", + "97", + "98", + "99", + "9A", + "9B", + "9C", + "9D", + "9E", + "9F", + "A0", + "A1", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "AA", + "AB", + "AC", + "AD", + "AE", + "AF", + "B0", + "B1", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "BA", + "BB", + "BC", + "BD", + "BE", + "BF", + "C0", + "C1", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "CA", + "CB", + "CC", + "CD", + "CE", + "CF", + "D0", + "D1", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "DA", + "DB", + "DC", + "DD", + "DE", + "DF", + "E0", + "E1", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "EA", + "EB", + "EC", + "ED", + "EE", + "EF", + "F0", + "F1", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "FA", + "FB", + "FC", + "FD", + "FE", + "FF", + ], + }, + { + "id": "RG3", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/resource_groups/RG3", + }, + "name": "group3", + "state": "normal", + "label": "group3", + "cs_global": "PUBLIC", + "pass_global": "PUBLIC", + "gm_masters": [ + "00", + "01", + "02", + "03", + "04", + "05", + "06", + "07", + "08", + "09", + "0A", + "0B", + "0C", + "0D", + "0E", + "0F", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "1A", + "1B", + "1C", + "1D", + "1E", + "1F", + "20", + "21", + "22", + "23", + "24", + "25", + "26", + "27", + "28", + "29", + "2A", + "2B", + "2C", + "2D", + "2E", + "2F", + "30", + "31", + "32", + "33", + "34", + "35", + "36", + "37", + "38", + "39", + "3A", + "3B", + "3C", + "3D", + "3E", + "3F", + "40", + "41", + "42", + "43", + "44", + "45", + "46", + "47", + "48", + "49", + "4A", + "4B", + "4C", + "4D", + "4E", + "4F", + "50", + "51", + "52", + "53", + "54", + "55", + "56", + "57", + "58", + "59", + "5A", + "5B", + "5C", + "5D", + "5E", + "5F", + "60", + "61", + "62", + "63", + "64", + "65", + "66", + "67", + "68", + "69", + "6A", + "6B", + "6C", + "6D", + "6E", + "6F", + "70", + "71", + "72", + "73", + "74", + "75", + "76", + "77", + "78", + "79", + "7A", + "7B", + "7C", + "7D", + "7E", + "7F", + "80", + "81", + "82", + "83", + "84", + "85", + "86", + "87", + "88", + "89", + "8A", + "8B", + "8C", + "8D", + "8E", + "8F", + "90", + "91", + "92", + "93", + "94", + "95", + "96", + "97", + "98", + "99", + "9A", + "9B", + "9C", + "9D", + "9E", + "9F", + "A0", + "A1", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "AA", + "AB", + "AC", + "AD", + "AE", + "AF", + "B0", + "B1", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "BA", + "BB", + "BC", + "BD", + "BE", + "BF", + "C0", + "C1", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "CA", + "CB", + "CC", + "CD", + "CE", + "CF", + "D0", + "D1", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "DA", + "DB", + "DC", + "DD", + "DE", + "DF", + "E0", + "E1", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "EA", + "EB", + "EC", + "ED", + "EE", + "EF", + "F0", + "F1", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "FA", + "FB", + "FC", + "FD", + "FE", + "FF", + ], + "gm_sessions": [ + "00", + "01", + "02", + "03", + "04", + "05", + "06", + "07", + "08", + "09", + "0A", + "0B", + "0C", + "0D", + "0E", + "0F", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "1A", + "1B", + "1C", + "1D", + "1E", + "1F", + "20", + "21", + "22", + "23", + "24", + "25", + "26", + "27", + "28", + "29", + "2A", + "2B", + "2C", + "2D", + "2E", + "2F", + "30", + "31", + "32", + "33", + "34", + "35", + "36", + "37", + "38", + "39", + "3A", + "3B", + "3C", + "3D", + "3E", + "3F", + "40", + "41", + "42", + "43", + "44", + "45", + "46", + "47", + "48", + "49", + "4A", + "4B", + "4C", + "4D", + "4E", + "4F", + "50", + "51", + "52", + "53", + "54", + "55", + "56", + "57", + "58", + "59", + "5A", + "5B", + "5C", + "5D", + "5E", + "5F", + "60", + "61", + "62", + "63", + "64", + "65", + "66", + "67", + "68", + "69", + "6A", + "6B", + "6C", + "6D", + "6E", + "6F", + "70", + "71", + "72", + "73", + "74", + "75", + "76", + "77", + "78", + "79", + "7A", + "7B", + "7C", + "7D", + "7E", + "7F", + "80", + "81", + "82", + "83", + "84", + "85", + "86", + "87", + "88", + "89", + "8A", + "8B", + "8C", + "8D", + "8E", + "8F", + "90", + "91", + "92", + "93", + "94", + "95", + "96", + "97", + "98", + "99", + "9A", + "9B", + "9C", + "9D", + "9E", + "9F", + "A0", + "A1", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "AA", + "AB", + "AC", + "AD", + "AE", + "AF", + "B0", + "B1", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "BA", + "BB", + "BC", + "BD", + "BE", + "BF", + "C0", + "C1", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "CA", + "CB", + "CC", + "CD", + "CE", + "CF", + "D0", + "D1", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "DA", + "DB", + "DC", + "DD", + "DE", + "DF", + "E0", + "E1", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "EA", + "EB", + "EC", + "ED", + "EE", + "EF", + "F0", + "F1", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "FA", + "FB", + "FC", + "FD", + "FE", + "FF", + ], + }, + ] + }, +} + +ONE = { + "server": {"status": "ok", "code": "", "message": "Operation done successfully."}, + "counts": {"data_counts": 1, "total_counts": 1}, + "data": { + "resource_groups": [ + { + "id": "RG1", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/resource_groups/RG1", + }, + "name": "group1", + "state": "normal", + "label": "group1", + "cs_global": "PUBLIC", + "pass_global": "PUBLIC", + "gm_masters": [ + "00", + "01", + "02", + "03", + "04", + "05", + "06", + "07", + "08", + "09", + "0A", + "0B", + "0C", + "0D", + "0E", + "0F", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "1A", + "1B", + "1C", + "1D", + "1E", + "1F", + "20", + "21", + "22", + "23", + "24", + "25", + "26", + "27", + "28", + "29", + "2A", + "2B", + "2C", + "2D", + "2E", + "2F", + "30", + "31", + "32", + "33", + "34", + "35", + "36", + "37", + "38", + "39", + "3A", + "3B", + "3C", + "3D", + "3E", + "3F", + "40", + "41", + "42", + "43", + "44", + "45", + "46", + "47", + "48", + "49", + "4A", + "4B", + "4C", + "4D", + "4E", + "4F", + "50", + "51", + "52", + "53", + "54", + "55", + "56", + "57", + "58", + "59", + "5A", + "5B", + "5C", + "5D", + "5E", + "5F", + "60", + "61", + "62", + "63", + "64", + "65", + "66", + "67", + "68", + "69", + "6A", + "6B", + "6C", + "6D", + "6E", + "6F", + "70", + "71", + "72", + "73", + "74", + "75", + "76", + "77", + "78", + "79", + "7A", + "7B", + "7C", + "7D", + "7E", + "7F", + "80", + "81", + "82", + "83", + "84", + "85", + "86", + "87", + "88", + "89", + "8A", + "8B", + "8C", + "8D", + "8E", + "8F", + "90", + "91", + "92", + "93", + "94", + "95", + "96", + "97", + "98", + "99", + "9A", + "9B", + "9C", + "9D", + "9E", + "9F", + "A0", + "A1", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "AA", + "AB", + "AC", + "AD", + "AE", + "AF", + "B0", + "B1", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "BA", + "BB", + "BC", + "BD", + "BE", + "BF", + "C0", + "C1", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "CA", + "CB", + "CC", + "CD", + "CE", + "CF", + "D0", + "D1", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "DA", + "DB", + "DC", + "DD", + "DE", + "DF", + "E0", + "E1", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "EA", + "EB", + "EC", + "ED", + "EE", + "EF", + "F0", + "F1", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "FA", + "FB", + "FC", + "FD", + "FE", + "FF", + ], + "gm_sessions": [ + "00", + "01", + "02", + "03", + "04", + "05", + "06", + "07", + "08", + "09", + "0A", + "0B", + "0C", + "0D", + "0E", + "0F", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "1A", + "1B", + "1C", + "1D", + "1E", + "1F", + "20", + "21", + "22", + "23", + "24", + "25", + "26", + "27", + "28", + "29", + "2A", + "2B", + "2C", + "2D", + "2E", + "2F", + "30", + "31", + "32", + "33", + "34", + "35", + "36", + "37", + "38", + "39", + "3A", + "3B", + "3C", + "3D", + "3E", + "3F", + "40", + "41", + "42", + "43", + "44", + "45", + "46", + "47", + "48", + "49", + "4A", + "4B", + "4C", + "4D", + "4E", + "4F", + "50", + "51", + "52", + "53", + "54", + "55", + "56", + "57", + "58", + "59", + "5A", + "5B", + "5C", + "5D", + "5E", + "5F", + "60", + "61", + "62", + "63", + "64", + "65", + "66", + "67", + "68", + "69", + "6A", + "6B", + "6C", + "6D", + "6E", + "6F", + "70", + "71", + "72", + "73", + "74", + "75", + "76", + "77", + "78", + "79", + "7A", + "7B", + "7C", + "7D", + "7E", + "7F", + "80", + "81", + "82", + "83", + "84", + "85", + "86", + "87", + "88", + "89", + "8A", + "8B", + "8C", + "8D", + "8E", + "8F", + "90", + "91", + "92", + "93", + "94", + "95", + "96", + "97", + "98", + "99", + "9A", + "9B", + "9C", + "9D", + "9E", + "9F", + "A0", + "A1", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "AA", + "AB", + "AC", + "AD", + "AE", + "AF", + "B0", + "B1", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "BA", + "BB", + "BC", + "BD", + "BE", + "BF", + "C0", + "C1", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "CA", + "CB", + "CC", + "CD", + "CE", + "CF", + "D0", + "D1", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "DA", + "DB", + "DC", + "DD", + "DE", + "DF", + "E0", + "E1", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "EA", + "EB", + "EC", + "ED", + "EE", + "EF", + "F0", + "F1", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "FA", + "FB", + "FC", + "FD", + "FE", + "FF", + ], + } + ] + }, +} diff --git a/pyds8k/test/mock/systems.py b/pyds8k/test/mock/systems.py old mode 100755 new mode 100644 index 8031ded..e2504bb --- a/pyds8k/test/mock/systems.py +++ b/pyds8k/test/mock/systems.py @@ -15,33 +15,30 @@ ############################################################################## ALL = { - "server": { - "status": "ok", - "code": "CMUC00183I", - "message": "Operation done successfully." - }, - "counts": { - "data_counts": 1, - "total_counts": 1 - }, - "data": { - "systems": [ - { - "id": "2107-75DHZ81", - "name": "mtc032h", - "state": "online", - "release": "7.4", - "bundle": "87.40.47.0", - "MTM": "2421-961", - "sn": "75DHZ81", - "wwnn": "5005076306FFD2F0", - "cap": "440659", - "capalloc": "304361", - "capavail": "136810", - "capraw": "73282879488" - } - ] - } + "server": { + "status": "ok", + "code": "CMUC00183I", + "message": "Operation done successfully.", + }, + "counts": {"data_counts": 1, "total_counts": 1}, + "data": { + "systems": [ + { + "id": "2107-75DHZ81", + "name": "mtc032h", + "state": "online", + "release": "7.4", + "bundle": "87.40.47.0", + "MTM": "2421-961", + "sn": "75DHZ81", + "wwnn": "5005076306FFD2F0", + "cap": "440659", + "capalloc": "304361", + "capavail": "136810", + "capraw": "73282879488", + } + ] + }, } ONE = ALL diff --git a/pyds8k/test/mock/tokens.py b/pyds8k/test/mock/tokens.py old mode 100755 new mode 100644 index 3231b79..dee4187 --- a/pyds8k/test/mock/tokens.py +++ b/pyds8k/test/mock/tokens.py @@ -15,16 +15,12 @@ ############################################################################## ALL = { - "server": { - "status": "ok", - "code": "", - "message": "Operation done successfully." - }, - "token": { - "token": "240a3408", - "expired_time": "2014-09-03T17:15:45+0800", - "max_idle_interval": "1800000" - } + "server": {"status": "ok", "code": "", "message": "Operation done successfully."}, + "token": { + "token": "240a3408", + "expired_time": "2014-09-03T17:15:45+0800", + "max_idle_interval": "1800000", + }, } ONE = ALL diff --git a/pyds8k/test/mock/tserep.py b/pyds8k/test/mock/tserep.py old mode 100755 new mode 100644 index f8027bd..c361f19 --- a/pyds8k/test/mock/tserep.py +++ b/pyds8k/test/mock/tserep.py @@ -15,33 +15,33 @@ ############################################################################## ALL = { - "server": { - "status": "ok", - "code": "CMUC00183I", - "message": "Operation done successfully." - }, - "data": { - "tserep": [ - { - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/pools/P1/tserep" - }, - "cap": "22260", - "capalloc": "0", - "capavail": "22260", - "overprovisioned": "0.0", - "repcapthreshold": "80", - "pool": { - "id": "P1", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/pools/P1" - } - } - } - ] - } + "server": { + "status": "ok", + "code": "CMUC00183I", + "message": "Operation done successfully.", + }, + "data": { + "tserep": [ + { + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/pools/P1/tserep", + }, + "cap": "22260", + "capalloc": "0", + "capavail": "22260", + "overprovisioned": "0.0", + "repcapthreshold": "80", + "pool": { + "id": "P1", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/pools/P1", + }, + }, + } + ] + }, } ONE = ALL diff --git a/pyds8k/test/mock/users.py b/pyds8k/test/mock/users.py index 42a7b59..134fdae 100644 --- a/pyds8k/test/mock/users.py +++ b/pyds8k/test/mock/users.py @@ -14,71 +14,48 @@ # limitations under the License. ############################################################################## -ALL = {"server": - { - "status": "ok", - "code": "", - "message": "Operation done successfully." - }, - "counts": - { - "data_counts": 2, - "total_counts": 2 - }, - "data": - { - "users": - [ - { - "name": "superuser", - "link": - { - "rel": "self", - "href": "https://localhost:8088/api/v1/users/superuser" - }, - "state": "active", - "group": ["Administrator"], - }, - { - "name": "admin", - "link": - { - "rel": "self", - "href": "https://localhost:8088/api/v1/users/admin" - }, - "state": "active", - "group": ["Administrator"], - }, - ] - } - } +ALL = { + "server": {"status": "ok", "code": "", "message": "Operation done successfully."}, + "counts": {"data_counts": 2, "total_counts": 2}, + "data": { + "users": [ + { + "name": "superuser", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/users/superuser", + }, + "state": "active", + "group": ["Administrator"], + }, + { + "name": "admin", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/users/admin", + }, + "state": "active", + "group": ["Administrator"], + }, + ] + }, +} -ONE = {"server": - { - "status": "ok", - "code": "", - "message": "Operation done successfully." - }, - "counts": - { - "data_counts": 1, - "total_counts": 1 - }, - "data": - { - "users": - [ - { - "name": "superuser", - "link": - { - "rel": "self", - "href": "https://localhost:8088/api/v1/users/superuser" - }, - "state": "active", - "group": ["Administrator"], - }, - ] - } - } +ONE = { + "server": {"status": "ok", "code": "", "message": "Operation done successfully."}, + "counts": {"data_counts": 1, "total_counts": 1}, + "data": { + "users": [ + { + "name": "superuser", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/users/superuser", + }, + "state": "active", + "group": ["Administrator"], + }, + ] + }, +} diff --git a/pyds8k/test/mock/volumes.py b/pyds8k/test/mock/volumes.py old mode 100755 new mode 100644 index 4760c59..e1d8336 --- a/pyds8k/test/mock/volumes.py +++ b/pyds8k/test/mock/volumes.py @@ -15,292 +15,247 @@ ############################################################################## ALL = { - "server": { - "status": "ok", - "code": "", - "message": "Operation done successfully." - }, - "counts": { - "data_counts": 25, - "total_counts": 25 - }, - "data": { - "volumes": [ - { - "id": "0000", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/volumes/0000" - }, - "name": "RegrFBVol1", - "state": "normal", - "cap": "1073741824", - "stgtype": "FB", - "VOLSER": "", - "lss": { - "id": "00", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/lss/00" - } - }, - "allocmethod": "rotatevols", - "tp": "none", - "MTM": "2107-900", - "datatype": "FB 512", - "easytier": "unknown", - "tieralloc": [ - { - "tier": "ENT", - "allocated": "1073741824" - } - ], - "pool": { - "id": "P2", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/pools/P2" - } - } - }, - { - "id": "0001", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/volumes/0001" - }, - "name": "RegrFBVol1", - "state": "normal", - "cap": "1073741824", - "stgtype": "FB", - "VOLSER": "", - "lss": { - "id": "00", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/lss/00" - } - }, - "allocmethod": "rotatevols", - "tp": "none", - "MTM": "2107-900", - "datatype": "FB 512", - "easytier": "unknown", - "tieralloc": [ - { - "tier": "ENT", - "allocated": "1073741824" - } - ], - "pool": { - "id": "P2", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/pools/P2" - } - } - }, - { - "id": "0002", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/volumes/0002" - }, - "name": "RegrFBVol1", - "state": "normal", - "cap": "1073741824", - "stgtype": "FB", - "VOLSER": "", - "lss": { - "id": "00", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/lss/00" - } - }, - "allocmethod": "rotatevols", - "tp": "none", - "MTM": "2107-900", - "datatype": "FB 512", - "easytier": "unknown", - "tieralloc": [ - { - "tier": "ENT", - "allocated": "1073741824" - } - ], - "pool": { - "id": "P2", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/pools/P2" - } - } - }, - { - "id": "0003", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/volumes/0003" - }, - "name": "RegrFBVol1", - "state": "normal", - "cap": "1073741824", - "stgtype": "FB", - "VOLSER": "", - "lss": { - "id": "00", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/lss/00" - } - }, - "allocmethod": "rotatevols", - "tp": "none", - "MTM": "2107-900", - "datatype": "FB 512", - "easytier": "unknown", - "tieralloc": [ - { - "tier": "ENT", - "allocated": "1073741824" - } - ], - "pool": { - "id": "P2", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/pools/P2" - } - } - }, - { - "id": "0004", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/volumes/0004" - }, - "name": "RegrFBVol1", - "state": "normal", - "cap": "1073741824", - "stgtype": "FB", - "VOLSER": "", - "lss": { - "id": "00", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/lss/00" - } - }, - "allocmethod": "rotatevols", - "tp": "none", - "MTM": "2107-900", - "datatype": "FB 512", - "easytier": "unknown", - "tieralloc": [ - { - "tier": "ENT", - "allocated": "1073741824" - } - ], - "pool": { - "id": "P2", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/pools/P2" - } - } - }, - { - "id": "0018", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/volumes/0018" - }, - "name": "singlevol2", - "state": "normal", - "cap": "1000013824", - "stgtype": "FB", - "VOLSER": "", - "lss": { - "id": "00", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/lss/00" - } - }, - "allocmethod": "rotateexts", - "tp": "none", - "MTM": "2107-900", - "datatype": "FB 512", - "easytier": "unknown", - "tieralloc": [ - { - "tier": "ENT", - "allocated": "1073741824" - } - ], - "pool": { - "id": "P2", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/pools/P2" - } - } - } - ] - } + "server": {"status": "ok", "code": "", "message": "Operation done successfully."}, + "counts": {"data_counts": 25, "total_counts": 25}, + "data": { + "volumes": [ + { + "id": "0000", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/volumes/0000", + }, + "name": "RegrFBVol1", + "state": "normal", + "cap": "1073741824", + "stgtype": "FB", + "VOLSER": "", + "lss": { + "id": "00", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/lss/00", + }, + }, + "allocmethod": "rotatevols", + "tp": "none", + "MTM": "2107-900", + "datatype": "FB 512", + "easytier": "unknown", + "tieralloc": [{"tier": "ENT", "allocated": "1073741824"}], + "pool": { + "id": "P2", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/pools/P2", + }, + }, + }, + { + "id": "0001", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/volumes/0001", + }, + "name": "RegrFBVol1", + "state": "normal", + "cap": "1073741824", + "stgtype": "FB", + "VOLSER": "", + "lss": { + "id": "00", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/lss/00", + }, + }, + "allocmethod": "rotatevols", + "tp": "none", + "MTM": "2107-900", + "datatype": "FB 512", + "easytier": "unknown", + "tieralloc": [{"tier": "ENT", "allocated": "1073741824"}], + "pool": { + "id": "P2", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/pools/P2", + }, + }, + }, + { + "id": "0002", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/volumes/0002", + }, + "name": "RegrFBVol1", + "state": "normal", + "cap": "1073741824", + "stgtype": "FB", + "VOLSER": "", + "lss": { + "id": "00", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/lss/00", + }, + }, + "allocmethod": "rotatevols", + "tp": "none", + "MTM": "2107-900", + "datatype": "FB 512", + "easytier": "unknown", + "tieralloc": [{"tier": "ENT", "allocated": "1073741824"}], + "pool": { + "id": "P2", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/pools/P2", + }, + }, + }, + { + "id": "0003", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/volumes/0003", + }, + "name": "RegrFBVol1", + "state": "normal", + "cap": "1073741824", + "stgtype": "FB", + "VOLSER": "", + "lss": { + "id": "00", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/lss/00", + }, + }, + "allocmethod": "rotatevols", + "tp": "none", + "MTM": "2107-900", + "datatype": "FB 512", + "easytier": "unknown", + "tieralloc": [{"tier": "ENT", "allocated": "1073741824"}], + "pool": { + "id": "P2", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/pools/P2", + }, + }, + }, + { + "id": "0004", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/volumes/0004", + }, + "name": "RegrFBVol1", + "state": "normal", + "cap": "1073741824", + "stgtype": "FB", + "VOLSER": "", + "lss": { + "id": "00", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/lss/00", + }, + }, + "allocmethod": "rotatevols", + "tp": "none", + "MTM": "2107-900", + "datatype": "FB 512", + "easytier": "unknown", + "tieralloc": [{"tier": "ENT", "allocated": "1073741824"}], + "pool": { + "id": "P2", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/pools/P2", + }, + }, + }, + { + "id": "0018", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/volumes/0018", + }, + "name": "singlevol2", + "state": "normal", + "cap": "1000013824", + "stgtype": "FB", + "VOLSER": "", + "lss": { + "id": "00", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/lss/00", + }, + }, + "allocmethod": "rotateexts", + "tp": "none", + "MTM": "2107-900", + "datatype": "FB 512", + "easytier": "unknown", + "tieralloc": [{"tier": "ENT", "allocated": "1073741824"}], + "pool": { + "id": "P2", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/pools/P2", + }, + }, + }, + ] + }, } ONE = { - "server": { - "status": "ok", - "code": "CMUC00183I", - "message": "Operation done successfully." - }, - "counts": { - "data_counts": 1, - "total_counts": 1 - }, - "data": { - "volumes": [ - { - "id": "0000", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/volumes/0000" - }, - "name": "RegrFBVol1", - "state": "normal", - "cap": "1073741824", - "stgtype": "FB", - "VOLSER": "", - "lss": { - "id": "00", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/lss/00" - } - }, - "allocmethod": "rotatevols", - "tp": "none", - "MTM": "2107-900", - "datatype": "FB 512", - "easytier": "unknown", - "tieralloc": [ - { - "tier": "ENT", - "allocated": "1073741824" - } - ], - "pool": { - "id": "P2", - "link": { - "rel": "self", - "href": "https://localhost:8088/api/v1/pools/P2" - } - } - } - ] - } + "server": { + "status": "ok", + "code": "CMUC00183I", + "message": "Operation done successfully.", + }, + "counts": {"data_counts": 1, "total_counts": 1}, + "data": { + "volumes": [ + { + "id": "0000", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/volumes/0000", + }, + "name": "RegrFBVol1", + "state": "normal", + "cap": "1073741824", + "stgtype": "FB", + "VOLSER": "", + "lss": { + "id": "00", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/lss/00", + }, + }, + "allocmethod": "rotatevols", + "tp": "none", + "MTM": "2107-900", + "datatype": "FB 512", + "easytier": "unknown", + "tieralloc": [{"tier": "ENT", "allocated": "1073741824"}], + "pool": { + "id": "P2", + "link": { + "rel": "self", + "href": "https://localhost:8088/api/v1/pools/P2", + }, + }, + } + ] + }, } diff --git a/pyds8k/test/test_client/test_ds8k/test_client.py b/pyds8k/test/test_client/test_ds8k/test_client.py old mode 100755 new mode 100644 index 66b03b3..a844b08 --- a/pyds8k/test/test_client/test_ds8k/test_client.py +++ b/pyds8k/test/test_client/test_ds8k/test_client.py @@ -14,14 +14,17 @@ # limitations under the License. ############################################################################## -from pyds8k.resources.ds8k.v1.common.types import DS8K_SYSTEM, \ - DS8K_VOLUME -from pyds8k.test.base import TestCaseWithConnect -from pyds8k.test.data import get_response_list_json_by_type, \ - get_response_list_data_by_type +import pytest +import responses + from pyds8k.client.ds8k.v1.client import Client +from pyds8k.resources.ds8k.v1.common.types import DS8K_SYSTEM, DS8K_VOLUME from pyds8k.resources.ds8k.v1.volumes import Volume -import httpretty +from pyds8k.test.base import TestCaseWithConnect +from pyds8k.test.data import ( + get_response_list_data_by_type, + get_response_list_json_by_type, +) system_list_response_json = get_response_list_json_by_type(DS8K_SYSTEM) volume_list_response_json = get_response_list_json_by_type(DS8K_VOLUME) @@ -29,37 +32,31 @@ class TestClient(TestCaseWithConnect): - def setUp(self): - super(TestClient, self).setUp() - self.rest_client = Client( - 'http://localhost:8088/api/', - 'admin', - 'admin' - ) + super().setUp() + self.rest_client = Client('https://localhost:8088/api/', 'admin', 'admin') - @httpretty.activate + @responses.activate def test_get_array_method(self): domain = self.client.domain vol_url = '/volumes' sys_url = '/systems' - httpretty.register_uri(httpretty.GET, - domain + self.base_url + vol_url, - body=volume_list_response_json, - content_type='application/json') + responses.get( + domain + self.base_url + vol_url, + body=volume_list_response_json, + content_type='application/json', + ) - httpretty.register_uri(httpretty.GET, - domain + self.base_url + sys_url, - body=system_list_response_json, - content_type='application/json') + responses.get( + domain + self.base_url + sys_url, + body=system_list_response_json, + content_type='application/json', + ) vol_list = self.rest_client.get_volumes() - self.assertIsInstance(vol_list, list) - self.assertIsInstance(vol_list[0], Volume) - self.assertEqual( - len(vol_list), - len(volume_list_response['data']['volumes']) - ) - with self.assertRaises(AttributeError): + assert isinstance(vol_list, list) + assert isinstance(vol_list[0], Volume) + assert len(vol_list) == len(volume_list_response['data']['volumes']) + with pytest.raises(AttributeError): # 'base_url' is an attr from System, not a method - self.rest_client.base_url + self.rest_client.base_url # noqa: B018 Testing only that exception raised diff --git a/pyds8k/test/test_client/test_ds8k/test_sc_client.py b/pyds8k/test/test_client/test_ds8k/test_sc_client.py index 47bfc1b..2779e9b 100644 --- a/pyds8k/test/test_client/test_ds8k/test_sc_client.py +++ b/pyds8k/test/test_client/test_ds8k/test_sc_client.py @@ -14,18 +14,24 @@ # limitations under the License. ############################################################################## +from http import HTTPStatus + +import responses +from responses import matchers + +from pyds8k.base import Resource, get_resource_and_manager_class_by_route +from pyds8k.client.ds8k.v1.sc_client import SCClient +from pyds8k.dataParser.ds8k import RequestParser, ResponseParser from pyds8k.resources.ds8k.v1.common import types from pyds8k.test.base import TestCaseWithConnect +from pyds8k.test.data import ( + create_mappings_response_json, + get_response_data_by_type, + get_response_json_by_type, + get_response_list_data_by_type, + get_response_list_json_by_type, +) from pyds8k.test.test_resources.test_ds8k.base import TestUtils -from pyds8k.dataParser.ds8k import ResponseParser, RequestParser -from pyds8k.test.data import get_response_list_json_by_type, \ - get_response_list_data_by_type, \ - get_response_json_by_type, \ - get_response_data_by_type, \ - get_request_json_body, create_mappings_response_json -from pyds8k.client.ds8k.v1.sc_client import SCClient -import httpretty -from pyds8k.base import Resource, get_resource_and_manager_class_by_route system_list_res_json = get_response_list_json_by_type(types.DS8K_SYSTEM) system_list_res = get_response_list_data_by_type(types.DS8K_SYSTEM) @@ -36,247 +42,243 @@ class TestClient(TestUtils, TestCaseWithConnect): - def setUp(self): - super(TestClient, self).setUp() - self.rest_client = SCClient('localhost:8088/api/', 'admin', 'admin') + super().setUp() + self.rest_client = SCClient('https://localhost:8088/api/', 'admin', 'admin') - def _assert_equal_between_dicts(self, - returned_dict, - origin_dict - ): + def _assert_equal_between_dicts(self, returned_dict, origin_dict): for key, value in origin_dict.items(): if not isinstance(value, dict): - self.assertEqual(value, returned_dict.get(key)) + assert value == returned_dict.get(key) - def _assert_equal_between_obj_and_dict(self, - returned_obj, - origin_dict - ): + def _assert_equal_between_obj_and_dict(self, returned_obj, origin_dict): for key, value in origin_dict.items(): if not isinstance(value, dict): - self.assertEqual(value, getattr(returned_obj, key)) + assert value == getattr(returned_obj, key) def _set_resource_list(self, route): base_route = route.split('.')[-1] resource_response = get_response_data_by_type(base_route) - prefix = '{}.{}'.format(self.client.service_type, - self.client.service_version - ) + prefix = f'{self.client.service_type}.{self.client.service_version}' res_class, _ = get_resource_and_manager_class_by_route( - "{}.{}".format(prefix, str(route).lower()) + f"{prefix}.{str(route).lower()}" ) if res_class.__name__ == Resource.__name__: - raise Exception( - 'Can not get resource class from route: {}'.format(route) - ) + msg = f'Can not get resource class from route: {route}' + raise Exception(msg) id_field = res_class.id_field - route_id = self._get_resource_id_from_resopnse(base_route, - resource_response, - id_field - ) + route_id = self._get_resource_id_from_resopnse( + base_route, resource_response, id_field + ) url = '/{}/{}'.format(route.replace('.', '/'), route_id) - httpretty.register_uri(httpretty.GET, - self.domain + self.base_url + url, - body=get_response_json_by_type(base_route), - content_type='application/json', - status=200, - ) + responses.get( + self.domain + self.base_url + url, + body=get_response_json_by_type(base_route), + content_type='application/json', + status=HTTPStatus.OK.value, + ) return route_id def _set_sub_resource(self, route, route_id, sub_route): - sub_route_url = '/{}/{}/{}'.format(route, route_id, sub_route) - httpretty.register_uri(httpretty.GET, - self.domain + self.base_url + sub_route_url, - body=get_response_list_json_by_type(sub_route), - content_type='application/json', - status=200, - ) + sub_route_url = f'/{route}/{route_id}/{sub_route}' + responses.get( + self.domain + self.base_url + sub_route_url, + body=get_response_list_json_by_type(sub_route), + content_type='application/json', + status=HTTPStatus.OK.value, + ) def _post_sub_resource(self, route, route_id, sub_route, body): - sub_route_url = '/{}/{}/{}'.format(route, route_id, sub_route) - - def _verify_request(request, uri, headers): - self.assertEqual(uri, self.domain + self.base_url + sub_route_url) + sub_route_url = f'/{route}/{route_id}/{sub_route}' + uri = f'{self.domain}{self.base_url}{sub_route_url}' - resq = RequestParser(body) - self.assertEqual(get_request_json_body(request.body), - resq.get_request_data()) - return (200, headers, create_mappings_response_json) + resq = RequestParser(body) - httpretty.register_uri(httpretty.POST, - self.domain + self.base_url + sub_route_url, - body=_verify_request, - content_type='application/json', - ) + responses.post( + uri, + status=HTTPStatus.OK, + body=create_mappings_response_json, + content_type='application/json', + match=[matchers.json_params_matcher(resq.get_request_data())], + ) - @httpretty.activate - def _test_resource_by_route(self, route, func, sub_resource=[]): + @responses.activate + def _test_resource_by_route(self, route, func, sub_resource=None): + if sub_resource is None: + sub_resource = [] base_route = route.split('.')[-1] route_id = self._set_resource_list(route) if sub_resource: for sub_route in sub_resource: self._set_sub_resource(route, route_id, sub_route) res = getattr(self.rest_client, func)(route_id)[0] - self.assertIsInstance(res, dict) - rep = ResponseParser(get_response_data_by_type(base_route), - base_route).get_representations()[0] + assert isinstance(res, dict) + rep = ResponseParser( + get_response_data_by_type(base_route), base_route + ).get_representations()[0] self._assert_equal_between_dicts(res, rep) - @httpretty.activate + @responses.activate def _test_sub_resource(self, route, sub_route, func): route_id = self._set_resource_list(route) self._set_sub_resource(route, route_id, sub_route) res = getattr(self.rest_client, func)(route_id)[0] - self.assertIsInstance(res, dict) + assert isinstance(res, dict) # print "&&&&&&&&&&&{}".format(res) - rep = ResponseParser(get_response_data_by_type(sub_route), - sub_route).get_representations()[0] + rep = ResponseParser( + get_response_data_by_type(sub_route), sub_route + ).get_representations()[0] self._assert_equal_between_dicts(res, rep) - @httpretty.activate + @responses.activate def _test_resource_list_by_route(self, route, func=None): - prefix = '{}.{}'.format(self.client.service_type, - self.client.service_version - ) + prefix = f'{self.client.service_type}.{self.client.service_version}' res_class, _ = get_resource_and_manager_class_by_route( - "{}.{}".format(prefix, str(route).lower()) + f"{prefix}.{str(route).lower()}" ) if res_class.__name__ == Resource.__name__: - raise Exception( - 'Can not get resource class from route: {}'.format(route) - ) + msg = f'Can not get resource class from route: {route}' + raise Exception(msg) url = '/{}'.format(route.replace('.', '/')) base_route = route.split('.')[-1] - httpretty.register_uri(httpretty.GET, - self.domain + self.base_url + url, - body=get_response_list_json_by_type(base_route), - content_type='application/json', - status=200, - ) + responses.get( + self.domain + self.base_url + url, + body=get_response_list_json_by_type(base_route), + content_type='application/json', + status=HTTPStatus.OK.value, + ) func = func or 'get_{}'.format(route.replace('.', '_')) res = getattr(self.rest_client, func)() - self.assertIsInstance(res, list) - self.assertIsInstance(res[0], dict) - rep = ResponseParser(get_response_list_data_by_type(base_route), - base_route).get_representations()[0] + assert isinstance(res, list) + assert isinstance(res[0], dict) + rep = ResponseParser( + get_response_list_data_by_type(base_route), base_route + ).get_representations()[0] self._assert_equal_between_dicts(res[0], rep) - @httpretty.activate + @responses.activate def _test_sub_resource_post(self, route, sub_route, func, body, *params): route_id = self._set_resource_list(route) self._post_sub_resource(route, route_id, sub_route, body) - func = func or 'get_{}'.format(route) + func = func or f'get_{route}' res = getattr(self.rest_client, func)(route_id, *params)[0] - rep = ResponseParser(get_response_data_by_type(sub_route), - sub_route).get_representations()[0] + rep = ResponseParser( + get_response_data_by_type(sub_route), sub_route + ).get_representations()[0] self._assert_equal_between_obj_and_dict(res, rep) - @httpretty.activate + @responses.activate def test_get_system(self): sys_url = '/systems' - httpretty.register_uri(httpretty.GET, - self.domain + self.base_url + sys_url, - body=system_list_res_json, - content_type='application/json') + responses.get( + self.domain + self.base_url + sys_url, + body=system_list_res_json, + content_type='application/json', + ) sys = self.rest_client.get_system()[0] - self.assertIsInstance(sys, dict) - rep = ResponseParser(system_list_res, - types.DS8K_SYSTEM).get_representations()[0] + assert isinstance(sys, dict) + rep = ResponseParser(system_list_res, types.DS8K_SYSTEM).get_representations()[ + 0 + ] self._assert_equal_between_dicts(sys, rep) def test_get_volume(self): self._test_resource_by_route(types.DS8K_VOLUME, 'get_volume') def test_get_extentpool(self): - self._test_resource_by_route(types.DS8K_POOL, 'get_extentpool', - sub_resource=[types.DS8K_ESEREP, ]) + self._test_resource_by_route( + types.DS8K_POOL, + 'get_extentpool', + sub_resource=[ + types.DS8K_ESEREP, + ], + ) def test_list_extentpools(self): self._test_resource_list_by_route(types.DS8K_POOL, 'list_extentpools') def test_list_extentpool_volumes(self): - self._test_sub_resource(types.DS8K_POOL, - types.DS8K_VOLUME, - 'list_extentpool_volumes', - ) + self._test_sub_resource( + types.DS8K_POOL, + types.DS8K_VOLUME, + 'list_extentpool_volumes', + ) def test_list_extentpool_virtualpool(self): - self._test_sub_resource(types.DS8K_POOL, - types.DS8K_ESEREP, - 'list_extentpool_virtualpool', - ) + self._test_sub_resource( + types.DS8K_POOL, + types.DS8K_ESEREP, + 'list_extentpool_virtualpool', + ) def test_list_flashcopies(self): - self._test_resource_list_by_route(types.DS8K_FLASHCOPY, - 'list_flashcopies' - ) + self._test_resource_list_by_route(types.DS8K_FLASHCOPY, 'list_flashcopies') def test_list_cs_flashcopies(self): - self._test_resource_list_by_route('{}.{}'.format( - types.DS8K_COPY_SERVICE_PREFIX, - types.DS8K_CS_FLASHCOPY), - 'list_cs_flashcopies' - ) + self._test_resource_list_by_route( + f'{types.DS8K_COPY_SERVICE_PREFIX}.{types.DS8K_CS_FLASHCOPY}', + 'list_cs_flashcopies', + ) def test_list_volume_flashcopies(self): - self._test_sub_resource(types.DS8K_VOLUME, - types.DS8K_FLASHCOPY, - 'list_volume_flashcopies', - ) + self._test_sub_resource( + types.DS8K_VOLUME, + types.DS8K_FLASHCOPY, + 'list_volume_flashcopies', + ) def test_list_remotecopies(self): - self._test_resource_list_by_route('{}.{}'.format( - types.DS8K_COPY_SERVICE_PREFIX, types.DS8K_CS_PPRC), + self._test_resource_list_by_route( + f'{types.DS8K_COPY_SERVICE_PREFIX}.{types.DS8K_CS_PPRC}', 'list_remotecopies', ) def test_list_volume_remotecopies(self): - self._test_sub_resource(types.DS8K_VOLUME, - types.DS8K_PPRC, - 'list_volume_remotecopies', - ) + self._test_sub_resource( + types.DS8K_VOLUME, + types.DS8K_PPRC, + 'list_volume_remotecopies', + ) def test_get_cs_remotecopy(self): - self._test_resource_by_route('{}.{}'.format( - types.DS8K_COPY_SERVICE_PREFIX, types.DS8K_CS_PPRC), + self._test_resource_by_route( + f'{types.DS8K_COPY_SERVICE_PREFIX}.{types.DS8K_CS_PPRC}', 'get_remotecopy', ) def test_list_logical_subsystems(self): - self._test_resource_list_by_route(types.DS8K_LSS, - 'list_logical_subsystems' - ) + self._test_resource_list_by_route(types.DS8K_LSS, 'list_logical_subsystems') def test_list_lss_volumes(self): - self._test_sub_resource(types.DS8K_LSS, - types.DS8K_VOLUME, - 'list_lss_volumes', - ) + self._test_sub_resource( + types.DS8K_LSS, + types.DS8K_VOLUME, + 'list_lss_volumes', + ) def test_list_fcports(self): - self._test_resource_list_by_route(types.DS8K_IOPORT, - 'list_fcports' - ) + self._test_resource_list_by_route(types.DS8K_IOPORT, 'list_fcports') def test_map_volume_to_host(self): volume_id = '000B' lunid = '09' - body = {"mappings": [{lunid: volume_id}, ]} - self._test_sub_resource_post(types.DS8K_HOST, - types.DS8K_VOLMAP, - 'map_volume_to_host', - body, - volume_id, - lunid - ) + body = {"mappings": [{lunid: volume_id}]} + self._test_sub_resource_post( + types.DS8K_HOST, + types.DS8K_VOLMAP, + 'map_volume_to_host', + body, + volume_id, + lunid, + ) body = {"volumes": [volume_id]} - self._test_sub_resource_post(types.DS8K_HOST, - types.DS8K_VOLMAP, - 'map_volume_to_host', - body, - volume_id, - '' - ) + self._test_sub_resource_post( + types.DS8K_HOST, + types.DS8K_VOLMAP, + 'map_volume_to_host', + body, + volume_id, + '', + ) diff --git a/pyds8k/test/test_dataParser/test_ds8k.py b/pyds8k/test/test_dataParser/test_ds8k.py old mode 100755 new mode 100644 index 76e0ab1..b48f229 --- a/pyds8k/test/test_dataParser/test_ds8k.py +++ b/pyds8k/test/test_dataParser/test_ds8k.py @@ -14,13 +14,17 @@ # limitations under the License. ############################################################################## -from pyds8k.resources.ds8k.v1.common.types import DS8K_VOLUME -from pyds8k.dataParser.ds8k import RequestParser, ResponseParser import json -from .. import base -from ..data import get_response_list_data_by_type, \ - get_response_data_by_type -from ..data import token_response_error, default_request + +from pyds8k.dataParser.ds8k import RequestParser, ResponseParser +from pyds8k.resources.ds8k.v1.common.types import DS8K_VOLUME +from pyds8k.test import base +from pyds8k.test.data import ( + default_request, + get_response_data_by_type, + get_response_list_data_by_type, + token_response_error, +) info = {'id': 'v1', 'name': 'vol1'} @@ -33,53 +37,30 @@ class TestDataParser(base.TestCaseWithoutConnect): - - def test_responseParser(self): + def test_responseParser(self): # noqa: N802 re = ResponseParser(volume_a_response, 'volumes') - self.assertEqual(re.response_key, 'data') - self.assertEqual(re.url_field, 'link') - self.assertEqual( - re.get_representations(), - volume_a_response['data']['volumes'] - ) + assert re.response_key == 'data' + assert re.url_field == 'link' + assert re.get_representations() == volume_a_response['data']['volumes'] re.representation = re.get_representations()[0] - self.assertEqual( - re.get_link(), - volume_a_response['data']['volumes'][0]['link']['href'] - ) + assert re.get_link() == volume_a_response['data']['volumes'][0]['link']['href'] re1 = ResponseParser(volume_list_response, 'volumes') - self.assertEqual(re1.response_key, 'data') - self.assertEqual(re1.url_field, 'link') - self.assertEqual( - re1.get_representations(), - volume_list_response['data']['volumes'] - ) + assert re1.response_key == 'data' + assert re1.url_field == 'link' + assert re1.get_representations() == volume_list_response['data']['volumes'] re1.representation = re1.get_representations()[0] - self.assertEqual( - re1.get_link(), - volume_list_response['data']['volumes'][0]['link']['href'] - ) + assert ( + re1.get_link() == volume_list_response['data']['volumes'][0]['link']['href'] + ) re2 = ResponseParser(token_response_error) - self.assertEqual( - re2.get_status_body(), - token_response_error['server'] - ) - self.assertEqual( - re2.get_error_code(), - token_response_error['server']['code'] - ) - self.assertEqual( - re2.get_error_msg(), - token_response_error['server']['message'] - ) - self.assertEqual( - re2.get_status(), - token_response_error['server']['status'] - ) + assert re2.get_status_body() == token_response_error['server'] + assert re2.get_error_code() == token_response_error['server']['code'] + assert re2.get_error_msg() == token_response_error['server']['message'] + assert re2.get_status() == token_response_error['server']['status'] - def test_requestParser(self): + def test_requestParser(self): # noqa: N802 re = RequestParser(default_request['request']['params']) - self.assertEqual(re.request_key, 'request') - self.assertEqual(re.get_request_data(), default_request) + assert re.request_key == 'request' + assert re.get_request_data() == default_request diff --git a/pyds8k/test/test_dataParser/test_utils.py b/pyds8k/test/test_dataParser/test_utils.py old mode 100755 new mode 100644 index dc41be3..076c833 --- a/pyds8k/test/test_dataParser/test_utils.py +++ b/pyds8k/test/test_dataParser/test_utils.py @@ -14,32 +14,22 @@ # limitations under the License. ############################################################################## -from .. import base from pyds8k.resources import utils +from pyds8k.test import base class TestUtils(base.TestCaseWithoutConnect): - def test_update_resource_id_in_url(self): old_id = '1' new_id = '2' - old_url_str = '/default/{}'.format(old_id) - new_url_str = '/default/{}'.format(new_id) - old_url_dict = { - 'rel': 'self', - 'href': old_url_str - } - new_url_dict = { - 'rel': 'self', - 'href': new_url_str - } - self.assertEqual( - utils.update_resource_id_in_url(old_id, new_id, old_url_str), - new_url_str - ) - self.assertEqual( - utils.update_resource_id_in_url(old_id, new_id, - old_url_dict, 'href' - ), - new_url_dict - ) + old_url_str = f'/default/{old_id}' + new_url_str = f'/default/{new_id}' + old_url_dict = {'rel': 'self', 'href': old_url_str} + new_url_dict = {'rel': 'self', 'href': new_url_str} + assert ( + utils.update_resource_id_in_url(old_id, new_id, old_url_str) == new_url_str + ) + assert ( + utils.update_resource_id_in_url(old_id, new_id, old_url_dict, 'href') + == new_url_dict + ) diff --git a/pyds8k/test/test_http_exceptions.py b/pyds8k/test/test_http_exceptions.py old mode 100755 new mode 100644 index 52aad91..10300b6 --- a/pyds8k/test/test_http_exceptions.py +++ b/pyds8k/test/test_http_exceptions.py @@ -14,272 +14,276 @@ # limitations under the License. ############################################################################## -import httpretty import json -from . import base +from http import HTTPStatus + +import pytest +import responses + from pyds8k import exceptions + +from . import base from .data import get_response_data_by_type default_a_response = get_response_data_by_type('default') response_401 = { - "server": { - "status": "failed", - "code": "BE742607", - "message": "The token is invalid or expired." - } + "server": { + "status": "failed", + "code": "BE742607", + "message": "The token is invalid or expired.", + } } response_token = { - "server": { - "status": "ok", - "code": "", - "message": "Operation done successfully." - }, - "token": { - "token": "54546d2a", - "expired_time": "2014-08-29T20:13:24+0800", - "max_idle_interval": "1800000" - } + "server": {"status": "ok", "code": "", "message": "Operation done successfully."}, + "token": { + "token": "54546d2a", + "expired_time": "2014-08-29T20:13:24+0800", + "max_idle_interval": "1800000", + }, } response_token_error = { - "server": { - "status": "failed", - "code": "NIServerException", - "message": "Operation done successfully." - } + "server": { + "status": "failed", + "code": "NIServerException", + "message": "Operation done successfully.", + } } DEFAULT = 'default' class TestHTTPException(base.TestCaseWithConnect): - def setUp(self): - super(TestHTTPException, self).setUp() + super().setUp() - @httpretty.activate + @responses.activate def test_response_status_400(self): domain = self.client.domain url = '/default/a' + uri = f'{domain}{self.base_url}{url}' - httpretty.register_uri( - httpretty.GET, domain + self.base_url + url, - body=json.dumps( - {'server': {'message': 'error', 'details': 'error'}} - ), + responses.get( + uri, + body=json.dumps({'server': {'message': 'error', 'details': 'error'}}), content_type='application/json', - status=400 + status=HTTPStatus.BAD_REQUEST.value, ) vol = self.resource.one(DEFAULT, 'a') - self.assertRaises(exceptions.BadRequest, vol.get) + with pytest.raises(exceptions.BadRequest): + vol.get() - @httpretty.activate + @responses.activate def test_response_status_401(self): domain = self.client.domain url = '/default/a' - httpretty.register_uri( - httpretty.POST, - domain + self.base_url + '/tokens', + uri_base = f'{domain}{self.base_url}' + uri = f'{uri_base}{url}' + + responses.post( + f'{uri_base}/tokens', body=json.dumps(response_token), content_type='application/json', - status=200 + status=HTTPStatus.OK.value, ) - httpretty.register_uri( - httpretty.GET, - domain + self.base_url + url, - responses=[ - httpretty.Response(body=json.dumps(response_401), - content_type='application/json', - status=401 - ), - httpretty.Response(body=json.dumps(default_a_response), - content_type='application/json', - status=200 - ), - httpretty.Response(body=json.dumps(response_401), - content_type='application/json', - status=401 - ), - ] + responses.get( + uri, + body=json.dumps(response_401), + content_type='application/json', + status=HTTPStatus.UNAUTHORIZED.value, ) + responses.get( + uri, + body=json.dumps(default_a_response), + content_type='application/json', + status=HTTPStatus.OK.value, + ) + responses.get( + uri, + body=json.dumps(response_401), + content_type='application/json', + status=HTTPStatus.UNAUTHORIZED.value, + ) + vol = self.resource.one(DEFAULT, 'a') vol.get() - self.assertEqual( - vol.url, - default_a_response['data']['default'][0]['link']['href'] - ) - self.assertEqual( - vol.name, - default_a_response['data']['default'][0]['name'] - ) - self.assertRaises(exceptions.Unauthorized, vol.get) - - @httpretty.activate + assert vol.url == default_a_response['data']['default'][0]['link']['href'] + assert vol.name == default_a_response['data']['default'][0]['name'] + with pytest.raises(exceptions.Unauthorized): + vol.get() + + @responses.activate def test_auth_fail(self): domain = self.client.domain url = '/default/a' - httpretty.register_uri(httpretty.POST, - domain + self.base_url + '/tokens', - body=json.dumps(response_token_error), - content_type='application/json', - status=401) - - httpretty.register_uri( - httpretty.GET, domain + self.base_url + url, - responses=[ - httpretty.Response(body=json.dumps(response_401), - content_type='application/json', - status=401 - ), - httpretty.Response(body=json.dumps(default_a_response), - content_type='application/json', - status=200 - ), - httpretty.Response(body=json.dumps(response_401), - content_type='application/json', - status=401 - ), - ] + uri_base = f'{domain}{self.base_url}' + uri = f'{uri_base}{url}' + + responses.post( + f'{uri_base}/tokens', + body=json.dumps(response_token_error), + content_type='application/json', + status=HTTPStatus.UNAUTHORIZED.value, + ) + + responses.get( + uri, + body=json.dumps(response_401), + content_type='application/json', + status=HTTPStatus.UNAUTHORIZED.value, + ) + responses.get( + uri, + body=json.dumps(default_a_response), + content_type='application/json', + status=HTTPStatus.OK.value, + ) + responses.get( + uri, + body=json.dumps(response_401), + content_type='application/json', + status=HTTPStatus.UNAUTHORIZED.value, ) + responses.get(uri) vol = self.resource.one(DEFAULT, 'a') - self.assertRaises(exceptions.Unauthorized, vol.get) + with pytest.raises(exceptions.Unauthorized): + vol.get() - @httpretty.activate + @responses.activate def test_response_status_403(self): domain = self.client.domain url = '/default/a' + uri = f'{domain}{self.base_url}{url}' - httpretty.register_uri( - httpretty.GET, - domain + self.base_url + url, - body=json.dumps( - {'server': {'message': 'error', 'details': 'error'}} - ), + responses.get( + uri, + body=json.dumps({'server': {'message': 'error', 'details': 'error'}}), content_type='application/json', - status=403 - ) + status=HTTPStatus.FORBIDDEN.value, + ) vol = self.resource.one(DEFAULT, 'a') - self.assertRaises(exceptions.Forbidden, vol.get) + with pytest.raises(exceptions.Forbidden): + vol.get() - @httpretty.activate + @responses.activate def test_response_status_404(self): domain = self.client.domain url = '/default/a' + uri = f'{domain}{self.base_url}{url}' - httpretty.register_uri( - httpretty.GET, domain + self.base_url + url, - body=json.dumps( - {'server': {'message': 'error', 'details': 'error'}} - ), + responses.get( + uri, + body=json.dumps({'server': {'message': 'error', 'details': 'error'}}), content_type='application/json', - status=404 + status=HTTPStatus.NOT_FOUND.value, ) vol = self.resource.one(DEFAULT, 'a') - self.assertRaises(exceptions.NotFound, vol.get) + with pytest.raises(exceptions.NotFound): + vol.get() - @httpretty.activate + @responses.activate def test_response_status_405(self): domain = self.client.domain url = '/default/a' + uri = f'{domain}{self.base_url}{url}' - httpretty.register_uri( - httpretty.GET, domain + self.base_url + url, - body=json.dumps( - {'server': {'message': 'error', 'details': 'error'}} - ), + responses.get( + uri, + body=json.dumps({'server': {'message': 'error', 'details': 'error'}}), content_type='application/json', - status=405 - ) + status=HTTPStatus.METHOD_NOT_ALLOWED.value, + ) vol = self.resource.one(DEFAULT, 'a') - self.assertRaises(exceptions.MethodNotAllowed, vol.get) + with pytest.raises(exceptions.MethodNotAllowed): + vol.get() - @httpretty.activate + @responses.activate def test_response_status_409(self): domain = self.client.domain url = '/default/a' + uri = f'{domain}{self.base_url}{url}' - httpretty.register_uri( - httpretty.GET, domain + self.base_url + url, - body=json.dumps( - {'server': {'message': 'error', 'details': 'error'}} - ), + responses.get( + uri, + body=json.dumps({'server': {'message': 'error', 'details': 'error'}}), content_type='application/json', - status=409 - ) + status=HTTPStatus.CONFLICT.value, + ) vol = self.resource.one(DEFAULT, 'a') - self.assertRaises(exceptions.Conflict, vol.get) + with pytest.raises(exceptions.Conflict): + vol.get() - @httpretty.activate + @responses.activate def test_response_status_415(self): domain = self.client.domain url = '/default/a' + uri = f'{domain}{self.base_url}{url}' - httpretty.register_uri( - httpretty.GET, domain + self.base_url + url, - body=json.dumps( - {'server': {'message': 'error', 'details': 'error'}} - ), + responses.get( + uri, + body=json.dumps({'server': {'message': 'error', 'details': 'error'}}), content_type='application/json', - status=415 - ) + status=HTTPStatus.UNSUPPORTED_MEDIA_TYPE.value, + ) vol = self.resource.one(DEFAULT, 'a') - self.assertRaises(exceptions.UnsupportedMediaType, vol.get) + with pytest.raises(exceptions.UnsupportedMediaType): + vol.get() - @httpretty.activate + @responses.activate def test_response_status_500(self): domain = self.client.domain url = '/default/a' + uri = f'{domain}{self.base_url}{url}' - httpretty.register_uri( - httpretty.GET, domain + self.base_url + url, - body=json.dumps( - {'server': {'message': 'error', 'details': 'error'}} - ), + responses.get( + uri, + body=json.dumps({'server': {'message': 'error', 'details': 'error'}}), content_type='application/json', - status=500 + status=HTTPStatus.INTERNAL_SERVER_ERROR.value, ) vol = self.resource.one(DEFAULT, 'a') - self.assertRaises(exceptions.InternalServerError, vol.get) + with pytest.raises(exceptions.InternalServerError): + vol.get() - @httpretty.activate + @responses.activate def test_response_status_503(self): domain = self.client.domain url = '/default/a' + uri = f'{domain}{self.base_url}{url}' - httpretty.register_uri( - httpretty.GET, domain + self.base_url + url, - body=json.dumps( - {'server': {'message': 'error', 'details': 'error'}} - ), + responses.get( + uri, + body=json.dumps({'server': {'message': 'error', 'details': 'error'}}), content_type='application/json', - status=503 - ) + status=HTTPStatus.SERVICE_UNAVAILABLE.value, + ) vol = self.resource.one(DEFAULT, 'a') - self.assertRaises(exceptions.ServiceUnavailable, vol.get) + with pytest.raises(exceptions.ServiceUnavailable): + vol.get() - @httpretty.activate + @responses.activate def test_response_status_504(self): domain = self.client.domain url = '/default/a' + uri = f'{domain}{self.base_url}{url}' - httpretty.register_uri( - httpretty.GET, domain + self.base_url + url, - body=json.dumps( - {'server': {'message': 'error', 'details': 'error'}} - ), + responses.get( + uri, + body=json.dumps({'server': {'message': 'error', 'details': 'error'}}), content_type='application/json', - status=504 - ) + status=HTTPStatus.GATEWAY_TIMEOUT.value, + ) vol = self.resource.one(DEFAULT, 'a') - self.assertRaises(exceptions.GatewayTimeout, vol.get) + with pytest.raises(exceptions.GatewayTimeout): + vol.get() diff --git a/pyds8k/test/test_httpclient.py b/pyds8k/test/test_httpclient.py old mode 100755 new mode 100644 index ee05cef..f5b2a7f --- a/pyds8k/test/test_httpclient.py +++ b/pyds8k/test/test_httpclient.py @@ -14,18 +14,26 @@ # limitations under the License. ############################################################################## -from pyds8k.exceptions import URLParseError -from . import base -import httpretty import json import time -from nose.tools import nottest +from functools import partial +from http import HTTPStatus + +import pytest +import responses + +from pyds8k.base import DefaultManager, Resource +from pyds8k.exceptions import URLParseError from pyds8k.httpclient import HTTPClient -from pyds8k.base import Resource, DefaultManager -from .data import get_response_list_json_by_type, \ - get_response_list_data_by_type, \ - get_response_data_by_type, \ - get_response_json_by_type + +from . import base +from .data import ( + get_response_data_by_type, + get_response_json_by_type, + get_response_list_data_by_type, + get_response_list_json_by_type, +) + info = {'id': 'v1', 'name': 'vol1'} custom_method_get = {'data': 'custom_method_get'} @@ -39,62 +47,71 @@ class TestHTTPClient(base.TestCaseWithConnect): - def setUp(self): - super(TestHTTPClient, self).setUp() + super().setUp() # DSANSIBLE-62, removing test_parse_url def test_parse_url(self): url1 = self.domain + '/new' url2 = '/new' _, url3 = url1.split('//') - url4 = 'http://new_domain' + '/new' - self.assertEqual('/new', self.client._parse_url(url1)) - self.assertEqual('/new', self.client._parse_url(url2)) - self.assertEqual('/new', self.client._parse_url(url3)) - with self.assertRaises(URLParseError): + url4 = 'https://new_domain' + '/new' + assert self.client._parse_url(url1) == '/new' + assert self.client._parse_url(url2) == '/new' + assert self.client._parse_url(url3) == '/new' + with pytest.raises(URLParseError): self.client._parse_url(url4) - new_client = HTTPClient('9.115.247.115', 'admin', 'admin', - service_type='ds8k', - secure=True) - with self.assertRaises(URLParseError): + new_client = HTTPClient( + '9.115.247.115', 'admin', 'admin', service_type='ds8k', secure=True + ) + with pytest.raises(URLParseError): new_client._parse_url(url3) - @httpretty.activate + @responses.activate def test_redirect(self): url = '/default/old' new_url = '/default/a' - httpretty.register_uri(httpretty.GET, - self.domain + self.base_url + url, - content_type='application/json', - adding_headers={'Location': new_url}, - status=301) - httpretty.register_uri(httpretty.GET, - self.domain + self.base_url + new_url, - body=default_a_response_json, - content_type='application/json', - status=200) + responses.get( + self.domain + self.base_url + url, + content_type='application/json', + adding_headers={'Location': new_url}, + status=HTTPStatus.MOVED_PERMANENTLY, + ) + responses.get( + self.domain + self.base_url + new_url, + body=default_a_response_json, + content_type='application/json', + status=HTTPStatus.OK, + ) de = self.resource.one(DEFAULT, 'old').get(allow_redirects=False) - self.assertEqual(new_url, de.url) + assert new_url == de.url - # Not work in this way. - @nottest - @httpretty.activate + @pytest.mark.skip(reason="Not work in this way") + @responses.activate def test_timeout(self): url = '/default/a' - new_client = HTTPClient('localhost', 'admin', 'admin', - service_type='ds8k', - timeout=0.01) + new_client = HTTPClient( + 'localhost', 'admin', 'admin', service_type='ds8k', timeout=0.01 + ) + uri = f'{new_client.domain}{self.base_url}{url}' + headers = {} - def _verify_request(request, uri, headers): + def _verify_request(request, _uri=None, _headers=None): + assert _uri == uri time.sleep(10) - return (200, headers, default_a_response_json) + return (HTTPStatus.OK, _headers, default_a_response_json) - httpretty.register_uri(httpretty.GET, - new_client.domain + self.base_url + url, - body=_verify_request, - content_type='application/json', - ) + responses.add_callback( + responses.GET, + uri, + callback=partial(_verify_request, _uri=uri, _headers=headers), + content_type='application/json', + ) + responses.get( + uri, + body=_verify_request, + content_type='application/json', + ) resource = Resource(new_client, DefaultManager(new_client)) resource.one(DEFAULT, 'a').get() diff --git a/pyds8k/test/test_resource.py b/pyds8k/test/test_resource.py old mode 100755 new mode 100644 index 52296f2..3ffdca7 --- a/pyds8k/test/test_resource.py +++ b/pyds8k/test/test_resource.py @@ -14,17 +14,27 @@ # limitations under the License. ############################################################################## -from pyds8k.base import Resource, DefaultManager -from . import base -import httpretty import json +import re +from http import HTTPStatus + +import pytest +import responses + +from pyds8k import messages +from pyds8k.base import DefaultManager, Resource from pyds8k.messages import DEFAULT_SUCCESS_BODY_DICT -from .data import get_response_list_json_by_type, \ - get_response_list_data_by_type, \ - get_response_data_by_type, \ - get_response_json_by_type -from .data import action_response, action_response_json -from .data import default_template + +from . import base +from .data import ( + action_response, + action_response_json, + default_template, + get_response_data_by_type, + get_response_json_by_type, + get_response_list_data_by_type, + get_response_list_json_by_type, +) info = {'id': 'v1', 'name': 'vol1'} @@ -40,410 +50,349 @@ # Note: The ds8k's data parser will be treated as the default parser here. class TestResource(base.TestCaseWithConnect): - def setUp(self): - super(TestResource, self).setUp() + super().setUp() def test_one_all(self): url1 = '/default/a/default/b/default' url2 = '/default/a/default/b/default/c' - vol1 = self.resource.one( - DEFAULT, - 'a' - ).one( - DEFAULT, - 'b' - ).all(DEFAULT) - vol2 = self.resource.one( - DEFAULT, - 'a' - ).one( - DEFAULT, - 'b' - ).one(DEFAULT, 'c') + vol1 = self.resource.one(DEFAULT, 'a').one(DEFAULT, 'b').all(DEFAULT) + vol2 = self.resource.one(DEFAULT, 'a').one(DEFAULT, 'b').one(DEFAULT, 'c') # test rebuild url - vol3 = vol2.one( - DEFAULT, - 'a', - rebuild_url=True - ).one( - DEFAULT, - 'b' - ).all(DEFAULT) - - self.assertIsInstance(vol1, Resource) - self.assertIsInstance(vol2, Resource) - self.assertIsInstance(vol3, Resource) - - self.assertIsInstance(vol1.parent, Resource) - self.assertIsInstance(vol2.parent, Resource) - self.assertIsInstance(vol3.parent, Resource) - - self.assertEqual(vol1.url, url1) - self.assertEqual(vol2.url, url2) - self.assertEqual(vol3.url, url1) - - @httpretty.activate - def test_toUrl(self): + vol3 = vol2.one(DEFAULT, 'a', rebuild_url=True).one(DEFAULT, 'b').all(DEFAULT) + + assert isinstance(vol1, Resource) + assert isinstance(vol2, Resource) + assert isinstance(vol3, Resource) + + assert isinstance(vol1.parent, Resource) + assert isinstance(vol2.parent, Resource) + assert isinstance(vol3.parent, Resource) + + assert vol1.url == url1 + assert vol2.url == url2 + assert vol3.url == url1 + + @responses.activate + def test_toUrl(self): # noqa: N802 domain = self.client.domain url = '/default/a/default/b/default/c' method = 'attach' body = {'test': 'test'} - httpretty.register_uri(httpretty.GET, - domain + self.base_url + url + '/' + method, - body=custom_method_get_json, - content_type='application/json') - httpretty.register_uri(httpretty.POST, - domain + self.base_url + url + '/' + method, - body=action_response_json, - content_type='application/json') - vol = self.resource.one( - DEFAULT, - 'a' - ).one( - DEFAULT, - 'b' - ).one(DEFAULT, 'c') + responses.get( + domain + self.base_url + url + '/' + method, + body=custom_method_get_json, + content_type='application/json', + ) + responses.post( + domain + self.base_url + url + '/' + method, + body=action_response_json, + content_type='application/json', + ) + vol = self.resource.one(DEFAULT, 'a').one(DEFAULT, 'b').one(DEFAULT, 'c') _, body1 = vol.toUrl(method) - self.assertEqual(vol.url, url) - self.assertEqual(body1, custom_method_get) + assert vol.url == url + assert body1 == custom_method_get _, body2 = vol.toUrl(method, body) - self.assertEqual(vol.url, url) - self.assertEqual(body2, action_response['server']) + assert vol.url == url + assert body2 == action_response['server'] - @httpretty.activate + @responses.activate def test_create_from_template_and_save(self): domain = self.client.domain url = '/default/a/default/b/default' - httpretty.register_uri( - httpretty.POST, + responses.post( domain + self.base_url + url, - responses=[ - httpretty.Response( - body=action_response_json, - content_type='application/json', - adding_headers={ - 'Location': self.base_url + url + '/vol1_id' - }, - status=201), - httpretty.Response( - body=action_response_json, - content_type='application/json', - adding_headers={ - 'Location': self.base_url + url + '/vol2_id' - }, - status=201), - httpretty.Response( - body=action_response_json, - content_type='application/json', - adding_headers={ - 'Location': self.base_url + url + '/vol3_id' - }, - status=201), - ] + body=action_response_json, + content_type='application/json', + headers={'Location': self.base_url + url + '/vol1_id'}, + status=HTTPStatus.CREATED.value, ) - httpretty.register_uri( - httpretty.PUT, + responses.post( + domain + self.base_url + url, + body=action_response_json, + content_type='application/json', + headers={'Location': self.base_url + url + '/vol2_id'}, + status=HTTPStatus.CREATED.value, + ) + responses.post( + domain + self.base_url + url, + body=action_response_json, + content_type='application/json', + headers={'Location': self.base_url + url + '/vol3_id'}, + status=HTTPStatus.CREATED.value, + ) + + responses.put( domain + self.base_url + url + '/vol3_id', - responses=[ - httpretty.Response( - body=action_response_json, - content_type='application/json', - adding_headers={ - 'Location': self.base_url + url + '/vol3_id' - }, - status=201), - ] + body=action_response_json, + content_type='application/json', + headers={'Location': self.base_url + url + '/vol3_id'}, + status=HTTPStatus.CREATED.value, + ) + vol1 = ( + self.resource.one(DEFAULT, 'a') + .one(DEFAULT, 'b') + .all(DEFAULT) + .create_from_template(default_template) ) - vol1 = self.resource.one( - DEFAULT, - 'a' - ).one(DEFAULT, - 'b' - ).all(DEFAULT).create_from_template(default_template) - self.assertIsInstance(vol1, Resource) - self.assertIsInstance(vol1.manager, DefaultManager) - self.assertEqual(vol1.name, default_template['name']) - self.assertEqual(vol1.url, url) - self.assertEqual(vol1.representation, default_template) + assert isinstance(vol1, Resource) + assert isinstance(vol1.manager, DefaultManager) + assert vol1.name == default_template['name'] + assert vol1.url == url + assert vol1.representation == default_template resp1, data1 = vol1.save() - self.assertIsInstance(data1[0], Resource) - self.assertEqual(resp1.status_code, 201) - self.assertEqual( - resp1.headers['Location'], - self.base_url + url + '/vol1_id' + assert isinstance(data1[0], Resource) + assert resp1.status_code == HTTPStatus.CREATED + assert resp1.headers['Location'] == self.base_url + url + '/vol1_id' + assert resp1.headers['Location'] == vol1.url + + vol2 = ( + self.resource.one(DEFAULT, 'a') + .one(DEFAULT, 'b') + .one(DEFAULT, 'c') + .create_from_template(default_template) ) - self.assertEqual(resp1.headers['Location'], vol1.url) - - vol2 = self.resource.one( - DEFAULT, - 'a' - ).one( - DEFAULT, - 'b' - ).one( - DEFAULT, - 'c' - ).create_from_template(default_template) vol2._template = default_template vol2.name = 'vol2' - self.assertIsInstance(vol2, Resource) - self.assertIsInstance(vol2.manager, DefaultManager) - self.assertEqual(vol2.name, 'vol2') - self.assertEqual(vol2.url, url) + assert isinstance(vol2, Resource) + assert isinstance(vol2.manager, DefaultManager) + assert vol2.name == 'vol2' + assert vol2.url == url rep = default_template.copy() rep.update({'name': 'vol2'}) - self.assertEqual(vol2.representation, rep) + assert vol2.representation == rep resp2, data2 = vol2.save() - self.assertIsInstance(data2[0], Resource) - self.assertEqual(resp2.status_code, 201) - self.assertEqual( - resp2.headers['Location'], - self.base_url + url + '/vol2_id' - ) - self.assertEqual(resp2.headers['Location'], vol2.url) + assert isinstance(data2[0], Resource) + assert resp2.status_code == HTTPStatus.CREATED + assert resp2.headers['Location'] == self.base_url + url + '/vol2_id' + assert resp2.headers['Location'] == vol2.url rep_with_id = default_template.copy() rep_with_id.update({'name': 'vol3', 'id': 'vol3_id'}) - vol3 = self.resource.one( - DEFAULT, - 'a' - ).one(DEFAULT, - 'b' - ).one(DEFAULT, - 'c' - ).create_from_template(rep_with_id) - self.assertIsInstance(vol3, Resource) - self.assertIsInstance(vol3.manager, DefaultManager) - self.assertEqual(vol3.name, 'vol3') - self.assertEqual(vol3.id, 'vol3_id') - self.assertEqual(vol3.url, url + '/vol3_id') - self.assertEqual(vol3.representation, rep_with_id) + vol3 = ( + self.resource.one(DEFAULT, 'a') + .one(DEFAULT, 'b') + .one(DEFAULT, 'c') + .create_from_template(rep_with_id) + ) + assert isinstance(vol3, Resource) + assert isinstance(vol3.manager, DefaultManager) + assert vol3.name == 'vol3' + assert vol3.id == 'vol3_id' + assert vol3.url == url + '/vol3_id' + assert vol3.representation == rep_with_id resp3, data3 = vol3.save() # default create method is put if id is specified. - self.assertEqual(data3, action_response.get('server')) - self.assertEqual(resp3.status_code, 201) - self.assertEqual( - resp3.headers['Location'], - self.base_url + url + '/vol3_id' - ) + assert data3 == action_response.get('server') + assert resp3.status_code == HTTPStatus.CREATED + assert resp3.headers['Location'] == self.base_url + url + '/vol3_id' def test_create(self): pass - @httpretty.activate + @responses.activate def test_lazy_loading(self): domain = self.client.domain url_list = '/default' vol_id = default_a_response['data']['default'][0]['id'] - url_a = '/default/{}'.format(vol_id) - httpretty.register_uri(httpretty.GET, - domain + self.base_url + url_list, - body=default_list_response_json, - content_type='application/json') - httpretty.register_uri(httpretty.GET, - domain + self.base_url + url_a, - body=default_a_response_json, - content_type='application/json') + url_a = f'/default/{vol_id}' + responses.get( + domain + self.base_url + url_list, + body=default_list_response_json, + content_type='application/json', + ) + responses.get( + domain + self.base_url + url_a, + body=default_a_response_json, + content_type='application/json', + ) de_list = self.resource.all(DEFAULT).list() de0 = de_list[0] - self.assertIsInstance(de0, Resource) - self.assertIsInstance(de0.manager, DefaultManager) + assert isinstance(de0, Resource) + assert isinstance(de0.manager, DefaultManager) de0._template = {'id': '', 'name': ''} - self.assertEqual( - de0.id, - default_list_response['data']['default'][0]['id'] - ) - self.assertFalse('name' in de0.representation) + assert de0.id == default_list_response['data']['default'][0]['id'] + assert 'name' not in de0.representation # 'unknown' is not in _template - self.assertRaises(AttributeError, getattr, de0, 'unknown') - self.assertFalse(de0.is_loaded()) + with pytest.raises(AttributeError): + de0.uknown # noqa: B018 Testing only that exception raised + + assert not de0.is_loaded() # loading details - self.assertEqual( - de0.name, - default_a_response['data']['default'][0]['name'] - ) - self.assertTrue('name' in de0.representation) - self.assertTrue(de0.is_loaded()) + assert de0.name == default_a_response['data']['default'][0]['name'] + assert 'name' in de0.representation + assert de0.is_loaded() def test_get_url(self): - self.assertEqual(self.resource._get_url('/test'), '/test') - self.assertEqual(self.resource._get_url( - {'rel': 'self', 'href': '/test'}), - '/test' - ) - self.assertEqual(self.resource._get_url( - [{'rel': 'self', 'href': '/test'}, - {'rel': 'bookmark', - 'href': '/bookmark'}, ]), - '/test' - ) - self.assertEqual(self.resource._get_url( - [{'rel': 'self_', 'href': '/test'}, - {'rel': 'bookmark', - 'href': '/bookmark'}, ]), - '' - ) - self.assertEqual(self.resource._get_url( - [{'rel': 'self', 'href_': '/test'}, - {'rel': 'bookmark', - 'href_': '/bookmark'}, ]), - '' - ) - self.assertEqual(self.resource._get_url( - [{'rel_': 'self', 'href': '/test'}, - {'rel_': 'bookmark', - 'href': '/bookmark'}, ]), - '' - ) - self.assertRaises(Exception, self.resource._get_url, object()) + assert self.resource._get_url('/test') == '/test' + assert self.resource._get_url({'rel': 'self', 'href': '/test'}) == '/test' + assert ( + self.resource._get_url( + [ + {'rel': 'self', 'href': '/test'}, + {'rel': 'bookmark', 'href': '/bookmark'}, + ] + ) + == '/test' + ) + assert ( + self.resource._get_url( + [ + {'rel': 'self_', 'href': '/test'}, + {'rel': 'bookmark', 'href': '/bookmark'}, + ] + ) + == '' + ) + assert ( + self.resource._get_url( + [ + {'rel': 'self', 'href_': '/test'}, + {'rel': 'bookmark', 'href_': '/bookmark'}, + ] + ) + == '' + ) + assert ( + self.resource._get_url( + [ + {'rel_': 'self', 'href': '/test'}, + {'rel_': 'bookmark', 'href': '/bookmark'}, + ] + ) + == '' + ) + with pytest.raises(Exception, match=messages.CAN_NOT_GET_URL): + self.resource._get_url(object()) def test_id(self): - self.assertFalse(hasattr(self.resource, 'id')) - self.assertFalse(hasattr(self.resource, '_id')) + assert not hasattr(self.resource, 'id') + assert not hasattr(self.resource, '_id') self.resource._add_details(info) - self.assertTrue(hasattr(self.resource, 'id')) - self.assertTrue(hasattr(self.resource, '_id')) + assert hasattr(self.resource, 'id') + assert hasattr(self.resource, '_id') def set_id(_id): self.resource.id = _id - self.assertRaises(Exception, set_id, 'a') + + with pytest.raises(Exception, match=re.escape("The field id is read only.")): + set_id('a') def test_modified_info_dict(self): re = Resource(self.client, DefaultManager(self.client)) - self.assertEqual(re._get_modified_info_dict(), {}) + assert re._get_modified_info_dict() == {} re._set_modified_info_dict('key1', 'val1') - self.assertEqual(re._get_modified_info_dict(), {'key1': 'val1'}) + assert re._get_modified_info_dict() == {'key1': 'val1'} re._del_modified_info_dict_key('key') - self.assertEqual(re._get_modified_info_dict(), {'key1': 'val1'}) + assert re._get_modified_info_dict() == {'key1': 'val1'} re._del_modified_info_dict_key('key1') - self.assertEqual(re._get_modified_info_dict(), {}) + assert re._get_modified_info_dict() == {} re._set_modified_info_dict('key2', 'val2') - self.assertEqual(re._get_modified_info_dict(), {'key2': 'val2'}) + assert re._get_modified_info_dict() == {'key2': 'val2'} re._del_modified_info_dict_keys({'key1': 'val1'}) - self.assertEqual(re._get_modified_info_dict(), {'key2': 'val2'}) + assert re._get_modified_info_dict() == {'key2': 'val2'} re._del_modified_info_dict_keys({'key2': 'val2'}) - self.assertEqual(re._get_modified_info_dict(), {}) + assert re._get_modified_info_dict() == {} - re1 = Resource(self.client, - DefaultManager(self.client), - ) + re1 = Resource( + self.client, + DefaultManager(self.client), + ) re1._template = {'key1': '', 'key2': ''} re1._add_details(info={'key1': 'val1'}) - self.assertEqual(re1._get_modified_info_dict(), {}) - self.assertEqual(re1.key1, 'val1') + assert re1._get_modified_info_dict() == {} + assert re1.key1 == 'val1' re1.key1 = 'val1_changed' - self.assertEqual( - re1._get_modified_info_dict(), - {'key1': 'val1_changed'} - ) - self.assertEqual(re1.key1, 'val1_changed') + assert re1._get_modified_info_dict() == {'key1': 'val1_changed'} + assert re1.key1 == 'val1_changed' # set attr not in _template re1.key3 = 'val3' - self.assertEqual( - re1._get_modified_info_dict(), - {'key1': 'val1_changed'} - ) + assert re1._get_modified_info_dict() == {'key1': 'val1_changed'} def test_force_get(self): - re1 = Resource(self.client, - DefaultManager(self.client), - ) + re1 = Resource( + self.client, + DefaultManager(self.client), + ) re1._template = {'key1': '', 'key2': ''} re1._add_details(info={'key1': 'val1'}) - self.assertEqual(re1._get_modified_info_dict(), {}) - self.assertEqual(re1.key1, 'val1') + assert re1._get_modified_info_dict() == {} + assert re1.key1 == 'val1' re1.key1 = 'val1_changed' - self.assertEqual( - re1._get_modified_info_dict(), - {'key1': 'val1_changed'} - ) - self.assertEqual(re1.key1, 'val1_changed') + assert re1._get_modified_info_dict() == {'key1': 'val1_changed'} + assert re1.key1 == 'val1_changed' re1._add_details(info={'key1': 'val1'}) - self.assertEqual(re1.key1, 'val1_changed') + assert re1.key1 == 'val1_changed' re1._add_details(info={'key1': 'val1'}, force=True) - self.assertEqual(re1.key1, 'val1') + assert re1.key1 == 'val1' - @httpretty.activate + @responses.activate def test_list(self): domain = self.client.domain url = '/default' url1 = default_a_response['data']['default'][0]['link']['href'] - httpretty.register_uri(httpretty.GET, domain + self.base_url + url, - body=default_list_response_json, - content_type='application/json') - httpretty.register_uri(httpretty.GET, domain + self.base_url + url1, - body=default_a_response_json, - content_type='application/json') + responses.get( + domain + self.base_url + url, + body=default_list_response_json, + content_type='application/json', + ) + responses.get( + domain + self.base_url + url1, + body=default_a_response_json, + content_type='application/json', + ) vol = self.resource.all(DEFAULT) - self.assertEqual(vol.url, url) - self.assertRaises(AttributeError, getattr, vol, 'id') + assert vol.url == url + with pytest.raises(AttributeError): + vol.id # noqa: B018 Testing only that exception raised vol_list = vol.list() - self.assertIsInstance(vol_list, list) + assert isinstance(vol_list, list) vol1 = vol_list[0] - self.assertEqual( - vol1.url, - default_list_response['data']['default'][0]['link']['href'] - ) - self.assertEqual( - vol1.id, - default_a_response['data']['default'][0]['id'] - ) + assert vol1.url == default_list_response['data']['default'][0]['link']['href'] + assert vol1.id == default_a_response['data']['default'][0]['id'] # lazy loading vol1._template = {'id': '', 'name': ''} - self.assertEqual( - vol1.name, - default_a_response['data']['default'][0]['name'] - ) + assert vol1.name == default_a_response['data']['default'][0]['name'] - @httpretty.activate + @responses.activate def test_get(self): domain = self.client.domain url = default_a_response['data']['default'][0]['link']['href'] vol_id = default_a_response['data']['default'][0]['id'] - httpretty.register_uri(httpretty.GET, domain + self.base_url + url, - body=default_a_response_json, - content_type='application/json') + responses.get( + domain + self.base_url + url, + body=default_a_response_json, + content_type='application/json', + ) vol = self.resource.one(DEFAULT, vol_id) - self.assertEqual(vol.url, url) - self.assertEqual(vol.id, vol_id) + assert vol.url == url + assert vol.id == vol_id vol.get() - self.assertEqual( - vol.url, - default_a_response['data']['default'][0]['link']['href'] - ) - self.assertEqual( - vol.name, - default_a_response['data']['default'][0]['name'] - ) + assert vol.url == default_a_response['data']['default'][0]['link']['href'] + assert vol.name == default_a_response['data']['default'][0]['name'] vol1 = self.resource.all(DEFAULT).get(vol_id) - self.assertEqual( - vol1.url, - default_a_response['data']['default'][0]['link']['href'] - ) - self.assertEqual( - vol1.name, - default_a_response['data']['default'][0]['name'] - ) - - @httpretty.activate + assert vol1.url == default_a_response['data']['default'][0]['link']['href'] + assert vol1.name == default_a_response['data']['default'][0]['name'] + + @responses.activate def test_post(self): # post append: tested in test_create_from_template_and_save # post: tested in test_toUrl pass - @httpretty.activate + @responses.activate def test_put(self): # put new: tested in test_create_from_template_and_save @@ -451,71 +400,72 @@ def test_put(self): domain = self.client.domain url = default_a_response['data']['default'][0]['link']['href'] vol_id = default_a_response['data']['default'][0]['id'] - httpretty.register_uri(httpretty.GET, domain + self.base_url + url, - body=default_a_response_json, - content_type='application/json') - httpretty.register_uri(httpretty.PUT, domain + self.base_url + url, - body=json.dumps({'status': 'updated'}), - content_type='application/json', - status=200) + responses.get( + domain + self.base_url + url, + body=default_a_response_json, + content_type='application/json', + ) + responses.put( + domain + self.base_url + url, + body=json.dumps({'status': 'updated'}), + content_type='application/json', + status=HTTPStatus.OK.value, + ) vol = self.resource.one(DEFAULT, vol_id).get() - self.assertEqual(vol.name, - default_a_response['data']['default'][0]['name'] - ) + assert vol.name == default_a_response['data']['default'][0]['name'] vol.name = 'vol1_rename' resp, data = vol.put() - self.assertEqual(data, {'status': 'updated'}) - self.assertEqual(resp.status_code, 200) + assert data == {'status': 'updated'} + assert resp.status_code == HTTPStatus.OK - @httpretty.activate + @responses.activate def test_patch(self): domain = self.client.domain url = default_a_response['data']['default'][0]['link']['href'] vol_id = default_a_response['data']['default'][0]['id'] - httpretty.register_uri(httpretty.GET, domain + self.base_url + url, - body=default_a_response_json, - content_type='application/json') - httpretty.register_uri(httpretty.PATCH, domain + self.base_url + url, - body=json.dumps({'status': 'updated'}), - content_type='application/json', - status=200) + responses.get( + domain + self.base_url + url, + body=default_a_response_json, + content_type='application/json', + ) + responses.patch( + domain + self.base_url + url, + body=json.dumps({'status': 'updated'}), + content_type='application/json', + status=HTTPStatus.OK.value, + ) vol = self.resource.one(DEFAULT, vol_id).get() - self.assertEqual( - vol.name, - default_a_response['data']['default'][0]['name'] - ) + assert vol.name == default_a_response['data']['default'][0]['name'] vol._template = default_template vol.name = 'vol1_rename_patch' - self.assertEqual( - vol._get_modified_info_dict(), - {'name': 'vol1_rename_patch'} - ) + assert vol._get_modified_info_dict() == {'name': 'vol1_rename_patch'} resp, data = vol.patch() - self.assertEqual(data, {'status': 'updated'}) - self.assertEqual(resp.status_code, 200) + assert data == {'status': 'updated'} + assert resp.status_code == HTTPStatus.OK - @httpretty.activate + @responses.activate def test_delete(self): domain = self.client.domain url = default_a_response['data']['default'][0]['link']['href'] vol_id = default_a_response['data']['default'][0]['id'] - httpretty.register_uri(httpretty.GET, domain + self.base_url + url, - body=default_a_response_json, - content_type='application/json') - httpretty.register_uri(httpretty.DELETE, domain + self.base_url + url, - content_type='application/json', - status=204) + responses.get( + domain + self.base_url + url, + body=default_a_response_json, + content_type='application/json', + ) + responses.delete( + domain + self.base_url + url, + content_type='application/json', + status=HTTPStatus.OK.value, + ) vol = self.resource.one(DEFAULT, vol_id).get() - self.assertEqual( - vol.name, - default_a_response['data']['default'][0]['name'] - ) + assert vol.name == default_a_response['data']['default'][0]['name'] resp, data = vol.delete() - self.assertEqual(resp.status_code, 204) - self.assertEqual(data, DEFAULT_SUCCESS_BODY_DICT) + assert resp.status_code == HTTPStatus.OK + assert data == DEFAULT_SUCCESS_BODY_DICT def test_save(self): # save new: tested in test_create_from_template_and_save @@ -533,23 +483,21 @@ def test_equal(self): re1 = Resource(self.client, resource_id='test') re2 = Resource(self.client, resource_id='test') re3 = Resource(self.client, resource_id='test3') - self.assertTrue(re1 == re2) - self.assertFalse(re1 == re3) - self.assertFalse(re1 is re2) - self.assertTrue(re1 in [re2]) + assert re1 == re2 + assert re1 != re3 + assert re1 is not re2 + assert re1 in [re2] def test_update_list_field(self): re1 = Resource(self.client, resource_id='test') - re1.re_list = [Resource(self.client, resource_id='test{}'.format(n)) - for n in range(10) - ] + re1.re_list = [Resource(self.client, resource_id=f'test{n}') for n in range(10)] re_not_in = Resource(self.client, resource_id='test11') re_in = Resource(self.client, resource_id='test1') - with self.assertRaises(KeyError): + with pytest.raises(KeyError): re1._update_list_field('re_list', re_not_in, '-') - with self.assertRaises(KeyError): + with pytest.raises(KeyError): re1._update_list_field('re_list', re_in) re1._update_list_field('re_list', re_not_in) - self.assertTrue(re_not_in in re1.re_list) + assert re_not_in in re1.re_list re1._update_list_field('re_list', re_in, '-') - self.assertTrue(re_in not in re1.re_list) + assert re_in not in re1.re_list diff --git a/pyds8k/test/test_resources/__init__.py b/pyds8k/test/test_resources/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyds8k/test/test_resources/test_ds8k/__init__.py b/pyds8k/test/test_resources/test_ds8k/__init__.py old mode 100755 new mode 100644 diff --git a/pyds8k/test/test_resources/test_ds8k/base.py b/pyds8k/test/test_resources/test_ds8k/base.py old mode 100755 new mode 100644 index 17fab6d..02a7521 --- a/pyds8k/test/test_resources/test_ds8k/base.py +++ b/pyds8k/test/test_resources/test_ds8k/base.py @@ -14,37 +14,35 @@ # limitations under the License. ############################################################################## -import httpretty import operator -from functools import partial, cmp_to_key -from pyds8k.test.data import get_response_list_json_by_type, \ - get_response_list_data_by_type, \ - get_response_data_by_type, \ - get_response_json_by_type -from pyds8k.test import base +from functools import cmp_to_key, partial +from http import HTTPStatus + +import responses + from pyds8k.base import Resource, get_resource_and_manager_class_by_route from pyds8k.resources.ds8k.v1.common import types -from pyds8k.resources.ds8k.v1.systems import System, \ - SystemManager +from pyds8k.resources.ds8k.v1.systems import System, SystemManager +from pyds8k.test import base +from pyds8k.test.data import ( + get_response_data_by_type, + get_response_json_by_type, + get_response_list_data_by_type, + get_response_list_json_by_type, +) def cmp(a, b): if a is not None and b is not None: return operator.gt(a, b) - operator.lt(a, b) - else: - return 0 - + return 0 -class TestUtils(object): +class TestUtils: def _sort_by(self, key, obj1, obj2): if isinstance(obj1, dict): - return cmp(obj1.get(key), - obj2.get(key) - ) - return cmp(getattr(obj1, key), - getattr(obj2, key) - ) + return cmp(obj1.get(key), obj2.get(key)) + return cmp(getattr(obj1, key), getattr(obj2, key)) def _sorted_by_volume_name(self, obj1, obj2): return self._sort_by('name', obj1, obj2) @@ -55,174 +53,145 @@ def _sorted_by_id(self, obj1, obj2): def _get_sort_func_by(self, key): return partial(self._sort_by, key) - def _assert_equal_between_dict_and_resource(self, - dicte, - resource - ): + def _assert_equal_between_dict_and_resource(self, dicte, resource): for key, value in dicte.items(): - if not isinstance(value, (dict, list)) and key not in resource.related_resources_collection: # noqa - self.assertEqual(value, getattr(resource, key)) - self.assertEqual(value, resource.representation.get(key)) - - def _assert_equal_between_sorted_dict_and_resource_list(self, - dict_list, - resource_list - ): + if ( + not isinstance(value, (dict, list)) + and key not in resource.related_resources_collection + ): + assert value == getattr(resource, key) + assert value == resource.representation.get(key) + + def _assert_equal_between_sorted_dict_and_resource_list( + self, dict_list, resource_list + ): for index, re in enumerate(resource_list): self._assert_equal_between_dict_and_resource(dict_list[index], re) - @httpretty.activate + @responses.activate def _test_resource_by_route(self, route): resource_response = get_response_data_by_type(route) res_class = self._get_class_by_name(route) id_field = res_class.id_field - route_id = self._get_resource_id_from_resopnse(route, - resource_response, - id_field - ) - url = '/{}/{}'.format(route, route_id) - httpretty.register_uri(httpretty.GET, - self.domain + self.base_url + url, - body=get_response_json_by_type(route), - content_type='application/json', - status=200, - ) - res = getattr(self.system, - 'get_{}'.format(route) - )(route_id) - self.assertIsInstance(res, res_class) + route_id = self._get_resource_id_from_resopnse( + route, resource_response, id_field + ) + url = f'/{route}/{route_id}' + responses.get( + self.domain + self.base_url + url, + body=get_response_json_by_type(route), + content_type='application/json', + status=HTTPStatus.OK.value, + ) + res = getattr(self.system, f'get_{route}')(route_id) + assert isinstance(res, res_class) res_data = resource_response['data'][route][0] self._assert_equal_between_dict_and_resource(res_data, res) - @httpretty.activate + @responses.activate def _test_resource_list_by_route(self, route, cmp_func=None): res_list_resp = get_response_list_data_by_type(route) - url = '/{}'.format(route) + url = f'/{route}' res_class = self._get_class_by_name(route) id_field = res_class.id_field cmp_f = cmp_func if cmp_func else self._get_sort_func_by(id_field) - httpretty.register_uri(httpretty.GET, - self.domain + self.base_url + url, - body=get_response_list_json_by_type(route), - content_type='application/json', - status=200, - ) - res_list = getattr(self.system, 'get_{}'.format(route))() - self.assertIsInstance(res_list[0], res_class) + responses.get( + self.domain + self.base_url + url, + body=get_response_list_json_by_type(route), + content_type='application/json', + status=HTTPStatus.OK.value, + ) + res_list = getattr(self.system, f'get_{route}')() + assert isinstance(res_list[0], res_class) res_list.sort(key=cmp_to_key(cmp_f)) res_list_data = list(res_list_resp['data'][route]) res_list_data.sort(key=cmp_to_key(cmp_f)) - self.assertEqual(len(res_list_data), - len(res_list) - ) + assert len(res_list_data) == len(res_list) self._assert_equal_between_sorted_dict_and_resource_list( - res_list_data, - res_list + res_list_data, res_list ) - @httpretty.activate - def _test_sub_resource_list_by_route(self, route, sub_route, - cmp_func=None - ): + @responses.activate + def _test_sub_resource_list_by_route(self, route, sub_route, cmp_func=None): sub_res_list_resp = get_response_list_data_by_type(sub_route) res_resp = get_response_data_by_type(route) res_class = self._get_class_by_name(route) id_field = res_class.id_field - route_id = self._get_resource_id_from_resopnse(route, - res_resp, - id_field - ) - route_url = '/{}/{}'.format(route, route_id) - sub_route_url = '/{}/{}/{}'.format(route, route_id, sub_route) + route_id = self._get_resource_id_from_resopnse(route, res_resp, id_field) + route_url = f'/{route}/{route_id}' + sub_route_url = f'/{route}/{route_id}/{sub_route}' cmp_f = cmp_func if cmp_func else self._sorted_by_id - httpretty.register_uri(httpretty.GET, - self.domain + self.base_url + sub_route_url, - body=get_response_list_json_by_type(sub_route), - content_type='application/json', - status=200, - ) - httpretty.register_uri(httpretty.GET, - self.domain + self.base_url + route_url, - body=get_response_json_by_type(route), - content_type='application/json', - status=200, - ) + responses.get( + self.domain + self.base_url + sub_route_url, + body=get_response_list_json_by_type(sub_route), + content_type='application/json', + status=HTTPStatus.OK.value, + ) + responses.get( + self.domain + self.base_url + route_url, + body=get_response_json_by_type(route), + content_type='application/json', + status=HTTPStatus.OK.value, + ) try: - res = getattr( - self.system, - 'get_{}'.format(route) - )(route_id) - except AttributeError: + res = getattr(self.system, f'get_{route}')(route_id) + except AttributeError as exc: if route == types.DS8K_LSS: res = self.system.get_lss_by_id(route_id) else: - raise Exception( - 'Failed calling get_{}'.format(route) - ) - self.assertIsInstance(res, res_class) - sub_res_list = getattr(res, 'get_{}'.format(sub_route))() - self.assertIs(getattr(res, sub_route), sub_res_list) + msg = f'Failed calling get_{route}' + raise Exception(msg) from exc + assert isinstance(res, res_class) + sub_res_list = getattr(res, f'get_{sub_route}')() + assert getattr(res, sub_route) is sub_res_list sub_res_list.sort(key=cmp_to_key(cmp_f)) sub_res_list_data = list(sub_res_list_resp['data'][sub_route]) sub_res_list_data.sort(key=cmp_to_key(cmp_f)) - self.assertNotEqual(0, len(sub_res_list)) - self.assertEqual(len(sub_res_list_data), - len(sub_res_list) - ) + assert len(sub_res_list) != 0 + assert len(sub_res_list_data) == len(sub_res_list) self._assert_equal_between_sorted_dict_and_resource_list( - sub_res_list_data, - sub_res_list + sub_res_list_data, sub_res_list ) def _test_related_resource_field(self, route): - info = get_response_data_by_type( - route - )['data'][route][0] + info = get_response_data_by_type(route)['data'][route][0] res_class = self._get_class_by_name(route) for rel, relclass_tuple in res_class.related_resource.items(): rel_class, _ = relclass_tuple rel_id = info[rel[1:]][rel_class.id_field] res = res_class(self.client, info=info) - self.assertEqual(getattr(res, rel[1:]), rel_id) - self.assertEqual(res.representation[rel[1:]], - rel_id - ) - self.assertIsInstance(getattr(res, rel), rel_class) - self.assertEqual(getattr(res, rel).id, rel_id) + assert getattr(res, rel[1:]) == rel_id + assert res.representation[rel[1:]] == rel_id + assert isinstance(getattr(res, rel), rel_class) + assert getattr(res, rel).id == rel_id def _get_resource_id_from_resopnse(self, route, response, id_field='id'): try: return response.get('data').get(route)[0][id_field] - except Exception: - raise Exception( - 'Can not get the id of {} from response.'.format(route) - ) + except Exception as exc: + msg = f'Can not get the id of {route} from response.' + raise Exception(msg) from exc def _get_resource_ids_from_resopnse(self, route, response, id_field='id'): try: return [re[id_field] for re in response.get('data').get(route)] - except Exception: - raise Exception( - 'Can not get the id of {} from response.'.format(route) - ) + except Exception as exc: + msg = f'Can not get the id of {route} from response.' + raise Exception(msg) from exc def _get_class_by_name(self, name): - prefix = '{}.{}'.format(self.client.service_type, - self.client.service_version - ) + prefix = f'{self.client.service_type}.{self.client.service_version}' res_class, _ = get_resource_and_manager_class_by_route( - "{}.{}".format(prefix, str(name).lower()) + f"{prefix}.{str(name).lower()}" ) if res_class.__name__ == Resource.__name__: - raise Exception( - 'Can not get resource class from route: {}'.format(name) - ) + msg = f'Can not get resource class from route: {name}' + raise Exception(msg) return res_class class TestDS8KWithConnect(TestUtils, base.TestCaseWithConnect): - def setUp(self): - super(TestDS8KWithConnect, self).setUp() + super().setUp() self.base_url = self.client.base_url self.system = System(self.client, SystemManager(self.client)) diff --git a/pyds8k/test/test_resources/test_ds8k/test_cs_pprc.py b/pyds8k/test/test_resources/test_ds8k/test_cs_pprc.py index 8f329c7..95f5883 100644 --- a/pyds8k/test/test_resources/test_ds8k/test_cs_pprc.py +++ b/pyds8k/test/test_resources/test_ds8k/test_cs_pprc.py @@ -15,36 +15,34 @@ ############################################################################## from pyds8k.resources.ds8k.v1.common.types import DS8K_CS_PPRC -from pyds8k.test.data import get_response_data_by_type -from .base import TestDS8KWithConnect -from pyds8k.resources.ds8k.v1.volumes import Volume from pyds8k.resources.ds8k.v1.cs.pprcs import PPRC from pyds8k.resources.ds8k.v1.systems import System +from pyds8k.resources.ds8k.v1.volumes import Volume +from pyds8k.test.data import get_response_data_by_type + +from .base import TestDS8KWithConnect class TestPPRC(TestDS8KWithConnect): - def test_related_resource_field(self): - pprc_info = get_response_data_by_type( - DS8K_CS_PPRC - )['data'][DS8K_CS_PPRC][0] + pprc_info = get_response_data_by_type(DS8K_CS_PPRC)['data'][DS8K_CS_PPRC][0] sourcevolume_id = pprc_info['source_volume'][Volume.id_field] targetvolume_id = pprc_info['target_volume'][Volume.id_field] targetsystem_id = pprc_info['target_system'][System.id_field] sourcesystem_id = pprc_info['source_system'][System.id_field] pprc = PPRC(self.client, info=pprc_info) - self.assertEqual(pprc.source_volume, sourcevolume_id) - self.assertEqual(pprc.representation['source_volume'], sourcevolume_id) - self.assertIsInstance(pprc._source_volume, Volume) - self.assertEqual(pprc._source_volume.id, sourcevolume_id) - self.assertEqual(pprc.target_volume, targetvolume_id) - self.assertEqual(pprc.representation['target_volume'], targetvolume_id) - self.assertIsInstance(pprc._target_volume, Volume) - self.assertEqual(pprc._target_volume.id, targetvolume_id) - self.assertEqual(pprc.target_system, targetsystem_id) - self.assertEqual(pprc.representation['target_system'], targetsystem_id) - self.assertIsInstance(pprc._target_system, System) - self.assertEqual(pprc._target_system.id, targetsystem_id) - self.assertEqual(pprc.representation['source_system'], sourcesystem_id) - self.assertIsInstance(pprc._target_system, System) - self.assertEqual(pprc._target_system.id, targetsystem_id) + assert pprc.source_volume == sourcevolume_id + assert pprc.representation['source_volume'] == sourcevolume_id + assert isinstance(pprc._source_volume, Volume) + assert pprc._source_volume.id == sourcevolume_id + assert pprc.target_volume == targetvolume_id + assert pprc.representation['target_volume'] == targetvolume_id + assert isinstance(pprc._target_volume, Volume) + assert pprc._target_volume.id == targetvolume_id + assert pprc.target_system == targetsystem_id + assert pprc.representation['target_system'] == targetsystem_id + assert isinstance(pprc._target_system, System) + assert pprc._target_system.id == targetsystem_id + assert pprc.representation['source_system'] == sourcesystem_id + assert isinstance(pprc._target_system, System) + assert pprc._target_system.id == targetsystem_id diff --git a/pyds8k/test/test_resources/test_ds8k/test_eserep.py b/pyds8k/test/test_resources/test_ds8k/test_eserep.py old mode 100755 new mode 100644 diff --git a/pyds8k/test/test_resources/test_ds8k/test_event.py b/pyds8k/test/test_resources/test_ds8k/test_event.py index 5ed7a63..c036bfc 100644 --- a/pyds8k/test/test_resources/test_ds8k/test_event.py +++ b/pyds8k/test/test_resources/test_ds8k/test_event.py @@ -14,67 +14,63 @@ # limitations under the License. ############################################################################## -import httpretty from datetime import datetime -from .base import TestDS8KWithConnect -from pyds8k.test.data import get_response_list_json_by_type -from pyds8k.resources.ds8k.v1.common.types import DS8K_EVENT + +import pytest +import responses +from responses import matchers +from tzlocal import get_localzone + from pyds8k.exceptions import InvalidArgumentError +from pyds8k.resources.ds8k.v1.common.types import DS8K_EVENT +from pyds8k.test.data import get_response_list_json_by_type + +from .base import TestDS8KWithConnect event_list_response = get_response_list_json_by_type(DS8K_EVENT) class TestHost(TestDS8KWithConnect): - - @httpretty.activate + @responses.activate def test_get_events_by_filter_set_severity(self): url = '/events' - httpretty.register_uri(httpretty.GET, - self.domain + self.base_url + url, - body=event_list_response, - content_type='application/json', - ) + params = {'severity': 'warning,error'} + + responses.get( + self.domain + self.base_url + url, + body=event_list_response, + content_type='application/json', + match=[matchers.query_param_matcher(params)], + ) self.system.get_events_by_filter(warning=True, error=True) - req = httpretty.last_request() - self.assertIsNotNone(req.querystring) - self.assertIn('severity', req.querystring) - self.assertEqual('warning,error', req.querystring.get('severity')[0]) - @httpretty.activate + @responses.activate def test_get_events_by_filter_set_date_error(self): url = '/events' - httpretty.register_uri(httpretty.GET, - self.domain + self.base_url + url, - body=event_list_response, - content_type='application/json', - ) - with self.assertRaises(InvalidArgumentError): + responses.get( + self.domain + self.base_url + url, + body=event_list_response, + content_type='application/json', + ) + with pytest.raises(InvalidArgumentError): self.system.get_events_by_filter(before='test') - @httpretty.activate + @responses.activate def test_get_events_by_filter_set_date(self): url = '/events' - before = datetime(2015, 4, 1) - after = datetime(2015, 1, 1) - - httpretty.register_uri(httpretty.GET, - self.domain + self.base_url + url, - body=event_list_response, - content_type='application/json', - ) + local_time = get_localzone() + before = datetime(2015, 4, 1, tzinfo=local_time) + after = datetime(2015, 1, 1, tzinfo=local_time) + params = { + 'before': before.astimezone().strftime('%Y-%m-%dT%X%z'), + 'after': after.astimezone().strftime('%Y-%m-%dT%X%z'), + } + responses.get( + self.domain + self.base_url + url, + body=event_list_response, + content_type='application/json', + match=[matchers.query_param_matcher(params)], + ) self.system.get_events_by_filter(before=before, after=after) - req = httpretty.last_request() - self.assertIsNotNone(req.querystring) - self.assertIn('before', req.querystring) - self.assertIn('after', req.querystring) - - # httpretty unquote "+" and " " in a wrong way, - # so I can not verify time zone here. - self.assertEqual('2015-04-01T00:00:00', - req.querystring.get('before')[0][:-5] - ) - self.assertEqual('2015-01-01T00:00:00', - req.querystring.get('after')[0][:-5] - ) diff --git a/pyds8k/test/test_resources/test_ds8k/test_flashcopies.py b/pyds8k/test/test_resources/test_ds8k/test_flashcopies.py index e41295c..357f6c6 100644 --- a/pyds8k/test/test_resources/test_ds8k/test_flashcopies.py +++ b/pyds8k/test/test_resources/test_ds8k/test_flashcopies.py @@ -1,108 +1,137 @@ -import json +############################################################################## +# Copyright 2025 IBM Corp. +# +# 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 +# +# 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 httpretty +from http import HTTPStatus + +import responses +from responses import matchers from pyds8k.dataParser.ds8k import RequestParser -from pyds8k.resources.ds8k.v1.common.types import DS8K_CS_FLASHCOPY, \ - DS8K_COPY_SERVICE_PREFIX, DS8K_FLASHCOPY +from pyds8k.resources.ds8k.v1.common.types import ( + DS8K_COPY_SERVICE_PREFIX, + DS8K_CS_FLASHCOPY, + DS8K_FLASHCOPY, +) from pyds8k.resources.ds8k.v1.cs.flashcopies import FlashCopy as FlashCopies from pyds8k.resources.ds8k.v1.flashcopy import FlashCopy -from pyds8k.test.data import get_response_json_by_type, \ - get_response_data_by_type, action_response_json, \ - create_flashcopy_response_json +from pyds8k.test.data import ( + action_response_json, + create_flashcopy_response_json, + get_response_data_by_type, + get_response_json_by_type, +) from pyds8k.test.test_resources.test_ds8k.base import TestDS8KWithConnect class TestFlashCopies(TestDS8KWithConnect): - def setUp(self): - super(TestFlashCopies, self).setUp() + super().setUp() self.maxDiff = None - @httpretty.activate + @responses.activate def test_create_cs_flashcopy(self): url = '/cs/flashcopies' + uri = f'{self.domain}{self.base_url}{url}' source_volume = '0000' target_volume = '0001' - def _verify_request(request, uri, headers): - self.assertEqual(uri, self.domain + self.base_url + url) + req = RequestParser( + { + "volume_pairs": [ + {"source_volume": source_volume, "target_volume": target_volume} + ], + } + ) - req = RequestParser( - {"volume_pairs": [{"source_volume": source_volume, - "target_volume": target_volume - }], - "options": [] - }) - self.assertDictContainsSubset( - req.get_request_data().get('request').get('params'), - json.loads(request.body).get('request').get('params'), - ) - return (201, headers, create_flashcopy_response_json) + responses.post( + uri, + status=HTTPStatus.CREATED, + body=create_flashcopy_response_json, + content_type='application/json', + match=[matchers.json_params_matcher(req.get_request_data())], + ) - httpretty.register_uri(httpretty.POST, - self.domain + self.base_url + url, - body=_verify_request, - content_type='application/json', - ) # Way 1 resp1 = self.system.create_cs_flashcopy( - volume_pairs=[{'source_volume': source_volume, - 'target_volume': target_volume}]) - self.assertEqual(httpretty.POST, httpretty.last_request().method) - self.assertIsInstance(resp1[0], FlashCopies) + volume_pairs=[ + {'source_volume': source_volume, 'target_volume': target_volume} + ] + ) + assert responses.calls[-1].request.method == responses.POST + assert isinstance(resp1[0], FlashCopies) # Way 2 flashcopies = self.system.all( - '{}.{}'.format(DS8K_COPY_SERVICE_PREFIX, DS8K_CS_FLASHCOPY), - rebuild_url=True) - new_fc2 = flashcopies.create(volume_pairs=[ - {'source_volume': source_volume, 'target_volume': target_volume}]) + f'{DS8K_COPY_SERVICE_PREFIX}.{DS8K_CS_FLASHCOPY}', + rebuild_url=True, + ) + new_fc2 = flashcopies.create( + volume_pairs=[ + {'source_volume': source_volume, 'target_volume': target_volume} + ] + ) resp2, data2 = new_fc2.posta() - self.assertEqual(httpretty.POST, httpretty.last_request().method) - self.assertIsInstance(data2[0], FlashCopies) - self.assertEqual(resp2.status_code, 201) + assert responses.calls[-1].request.method == responses.POST + assert isinstance(data2[0], FlashCopies) + assert resp2.status_code == HTTPStatus.CREATED # Way 3 flashcopies = self.system.all( - '{}.{}'.format(DS8K_COPY_SERVICE_PREFIX, DS8K_CS_FLASHCOPY), - rebuild_url=True) - new_fc3 = flashcopies.create(volume_pairs=[ - {'source_volume': source_volume, 'target_volume': target_volume}]) + f'{DS8K_COPY_SERVICE_PREFIX}.{DS8K_CS_FLASHCOPY}', + rebuild_url=True, + ) + new_fc3 = flashcopies.create( + volume_pairs=[ + {'source_volume': source_volume, 'target_volume': target_volume} + ] + ) resp3, data3 = new_fc3.save() - self.assertEqual(httpretty.POST, httpretty.last_request().method) - self.assertIsInstance(data3[0], FlashCopies) - self.assertEqual(resp3.status_code, 201) + assert responses.calls[-1].request.method == responses.POST + assert isinstance(data3[0], FlashCopies) + assert resp3.status_code == HTTPStatus.CREATED - @httpretty.activate + @responses.activate def test_delete_cs_flashcopy(self): response_a_json = get_response_json_by_type(DS8K_CS_FLASHCOPY) response_a = get_response_data_by_type(DS8K_CS_FLASHCOPY) - name = self._get_resource_id_from_resopnse(DS8K_FLASHCOPY, - response_a, - FlashCopy.id_field - ) - url = '/cs/flashcopies/{}'.format(name) - httpretty.register_uri(httpretty.GET, - self.domain + self.base_url + url, - body=response_a_json, - content_type='application/json', - status=200, - ) - httpretty.register_uri(httpretty.DELETE, - self.domain + self.base_url + url, - body=action_response_json, - content_type='application/json', - status=204, - ) + name = self._get_resource_id_from_resopnse( + DS8K_FLASHCOPY, response_a, FlashCopy.id_field + ) + url = f'/cs/flashcopies/{name}' + responses.get( + self.domain + self.base_url + url, + body=response_a_json, + content_type='application/json', + status=HTTPStatus.OK.value, + ) + responses.delete( + self.domain + self.base_url + url, + body=action_response_json, + content_type='application/json', + status=HTTPStatus.OK.value, + ) + # Way 1 _ = self.system.delete_cs_flashcopy(name) - self.assertEqual(httpretty.DELETE, httpretty.last_request().method) + assert responses.calls[-1].request.method == responses.DELETE # Way 2 flashcopy = self.system.get_cs_flashcopies(name) - self.assertIsInstance(flashcopy, FlashCopies) + assert isinstance(flashcopy, FlashCopies) resp2, _ = flashcopy.delete() - self.assertEqual(resp2.status_code, 204) - self.assertEqual(httpretty.DELETE, httpretty.last_request().method) + assert resp2.status_code == HTTPStatus.OK + assert responses.calls[-1].request.method == responses.DELETE diff --git a/pyds8k/test/test_resources/test_ds8k/test_hmc_certificate.py b/pyds8k/test/test_resources/test_ds8k/test_hmc_certificate.py new file mode 100644 index 0000000..60b806f --- /dev/null +++ b/pyds8k/test/test_resources/test_ds8k/test_hmc_certificate.py @@ -0,0 +1,65 @@ +############################################################################## +# Copyright 2023 IBM Corp. +# +# 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 +# +# 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 http import HTTPStatus + +import responses +from responses import matchers + +from pyds8k.resources.ds8k.v1.common.types import DS8K_HMC, DS8K_HMC_CERTIFICATE + +# from pyds8k.resources.ds8k.v1.hmc.certificate import HmcCertificate +from pyds8k.test.data import ( + action_response, + action_response_json, + upload_hmc_certificate_cert, +) +from pyds8k.test.test_resources.test_ds8k.base import TestDS8KWithConnect + + +class TestHmcCertificate(TestDS8KWithConnect): + def setUp(self): + super().setUp() + + @responses.activate + def test_upload_hmc_certificate(self): + url = f'/{DS8K_HMC}/{DS8K_HMC_CERTIFICATE}' + uri = f'{self.domain}{self.base_url}{url}' + + responses.post( + uri, + status=HTTPStatus.CREATED, + body=action_response_json, + content_type='application/json', + match=[matchers.multipart_matcher({"file": upload_hmc_certificate_cert})], + ) + # Way 1 + resp1 = self.system.upload_hmc_signed_certificate(upload_hmc_certificate_cert) + + assert responses.calls[-1].request.method == responses.POST + assert resp1[0].status_code == HTTPStatus.CREATED + assert resp1[1] == action_response + + # ???: Doesn't work because HmcCertificate doesn't have a template? + # # Way 2 + # hmc_certificate = self.system.all( + # '{}.{}'.format(DS8K_HMC, DS8K_HMC_CERTIFICATE), + # rebuild_url=True) + # hmc_certificate2 = hmc_certificate.create(body=cert) + # resp2, data2 = hmc_certificate_csr2.post() + # self.assertEqual(responses.POST, responses.calls[-1].request.method) + # # self.assertIsInstance(data2[0], HmcCertificate) + # self.assertEqual(resp2.status_code, HTTPStatus.CREATED) diff --git a/pyds8k/test/test_resources/test_ds8k/test_hmc_certificate_csr.py b/pyds8k/test/test_resources/test_ds8k/test_hmc_certificate_csr.py new file mode 100644 index 0000000..ec4bfa5 --- /dev/null +++ b/pyds8k/test/test_resources/test_ds8k/test_hmc_certificate_csr.py @@ -0,0 +1,95 @@ +############################################################################## +# Copyright 2023 IBM Corp. +# +# 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 +# +# 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 http import HTTPStatus + +import responses +from responses import matchers + +from pyds8k.dataParser.ds8k import RequestParser +from pyds8k.resources.ds8k.v1.common.types import ( + DS8K_HMC, + DS8K_HMC_CERTIFICATE, + DS8K_HMC_CERTIFICATE_CSR, +) + +# from pyds8k.resources.ds8k.v1.hmc.certificate.csr import HmcCertificateCsr +from pyds8k.test.data import create_hmc_certificate_csr_response_json +from pyds8k.test.test_resources.test_ds8k.base import TestDS8KWithConnect + + +class TestHmcCertificateCsr(TestDS8KWithConnect): + def setUp(self): + super().setUp() + + @responses.activate + def test_create_hmc_certificate_csr(self): + url = f'/{DS8K_HMC}/{DS8K_HMC_CERTIFICATE}/{DS8K_HMC_CERTIFICATE_CSR}' + uri = f'{self.domain}{self.base_url}{url}' + + O = "IBM" # noqa: E741, N806 + OU = "DS8000" # noqa: N806 + C = "US" # noqa: N806 + ST = "NY" # noqa: N806 + L = "Armok" # noqa: N806 + email = "ansible@fake_server.com" + force = "True" + + req = RequestParser( + { + 'O': O, + 'OU': OU, + 'C': C, + 'ST': ST, + 'L': L, + 'email': email, + 'force': force, + } + ) + responses.post( + uri, + status=HTTPStatus.CREATED, + body=create_hmc_certificate_csr_response_json, + content_type='application/json', + match=[matchers.json_params_matcher(req.get_request_data())], + ) + + # Way 1 + resp1 = self.system.create_hmc_csr( + O=O, OU=OU, C=C, ST=ST, L=L, email=email, force=force + ) + + assert responses.calls[-1].request.method == responses.POST + assert '-----BEGIN CERTIFICATE REQUEST-----' in resp1 + + # ???: Doesn't work because HmcCertificateCsr doesn't have a template? + # Way 2 + # hmc_certificate_csr = self.system.all( + # '{}.{}.{}'.format(DS8K_HMC, + # DS8K_HMC_CERTIFICATE, + # DS8K_HMC_CERTIFICATE_CSR), + # rebuild_url=True) + # hmc_certificate_csr2 = hmc_certificate_csr.create(O=O, + # OU=OU, + # C=C, + # ST=ST, + # L=L, + # email=email, + # force=force) + # resp2, data2 = hmc_certificate_csr2.post() + # assert responses.calls[-1].request.method == responses.POST + # # self.assertIsInstance(data2[0], HmcCertificateCsr) + # assert resp2.status_code == HTTPStatus.CREATED diff --git a/pyds8k/test/test_resources/test_ds8k/test_hmc_certificate_selfsigned.py b/pyds8k/test/test_resources/test_ds8k/test_hmc_certificate_selfsigned.py new file mode 100644 index 0000000..97f07e1 --- /dev/null +++ b/pyds8k/test/test_resources/test_ds8k/test_hmc_certificate_selfsigned.py @@ -0,0 +1,81 @@ +############################################################################## +# Copyright 2023 IBM Corp. +# +# 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 +# +# 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 http import HTTPStatus + +import responses +from responses import matchers + +from pyds8k.dataParser.ds8k import RequestParser +from pyds8k.resources.ds8k.v1.common.types import ( + DS8K_HMC, + DS8K_HMC_CERTIFICATE, + DS8K_HMC_CERTIFICATE_SELFSIGNED, +) + +# from pyds8k.resources.ds8k.v1.hmc.certificate.selfsigned \ +# import HmcCertificateSelfSigned +from pyds8k.test.data import action_response, action_response_json +from pyds8k.test.test_resources.test_ds8k.base import TestDS8KWithConnect + + +class TestHmcCertificateSelfsigned(TestDS8KWithConnect): + def setUp(self): + super().setUp() + + @responses.activate + def test_create_hmc_selfsigned_certificate(self): + url = f'/{DS8K_HMC}/{DS8K_HMC_CERTIFICATE}/{DS8K_HMC_CERTIFICATE_SELFSIGNED}' + uri = f'{self.domain}{self.base_url}{url}' + + O = "IBM" # noqa: E741, N806 + OU = "DS8000" # noqa: N806 + C = "US" # noqa: N806 + ST = "NY" # noqa: N806 + L = "Armok" # noqa: N806 + email = "ansible@fake_server.com" + days = 1 + restart = 'False' + + req = RequestParser( + { + 'O': O, + 'OU': OU, + 'C': C, + 'ST': ST, + 'L': L, + 'days': days, + 'email': email, + 'restart': restart, + } + ) + + responses.post( + uri, + status=HTTPStatus.CREATED, + body=action_response_json, + content_type='application/json', + match=[matchers.json_params_matcher(req.get_request_data())], + ) + + # Way 1 + resp1 = self.system.create_hmc_selfsigned_certificate( + O=O, OU=OU, C=C, ST=ST, L=L, email=email, days=days + ) + + assert responses.calls[-1].request.method == responses.POST + assert resp1[0].status_code == HTTPStatus.CREATED + assert resp1[1] == action_response['server'] diff --git a/pyds8k/test/test_resources/test_ds8k/test_hmc_restart.py b/pyds8k/test/test_resources/test_ds8k/test_hmc_restart.py new file mode 100644 index 0000000..01224aa --- /dev/null +++ b/pyds8k/test/test_resources/test_ds8k/test_hmc_restart.py @@ -0,0 +1,52 @@ +############################################################################## +# Copyright 2023 IBM Corp. +# +# 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 +# +# 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 http import HTTPStatus + +import responses +from responses import matchers + +from pyds8k.dataParser.ds8k import RequestParser +from pyds8k.resources.ds8k.v1.common.types import DS8K_HMC, DS8K_HMC_RESTART + +# from pyds8k.resources.ds8k.v1.hmc.restart import HMCRestart +from pyds8k.test.data import action_response, action_response_json +from pyds8k.test.test_resources.test_ds8k.base import TestDS8KWithConnect + + +class TestHmcRestart(TestDS8KWithConnect): + def setUp(self): + super().setUp() + + @responses.activate + def test_hmc_restart(self): + url = f'/{DS8K_HMC}/{DS8K_HMC_RESTART}' + uri = f'{self.domain}{self.base_url}{url}' + + req = RequestParser({}) + responses.post( + uri, + status=HTTPStatus.CREATED, + body=action_response_json, + content_type='application/json', + match=[matchers.json_params_matcher(req.get_request_data())], + ) + # Way 1 + resp1 = self.system.restart_hmc() + + assert responses.calls[-1].request.method == responses.POST + assert resp1[0].status_code == HTTPStatus.CREATED + assert resp1[1] == action_response['server'] diff --git a/pyds8k/test/test_resources/test_ds8k/test_host.py b/pyds8k/test/test_resources/test_ds8k/test_host.py old mode 100755 new mode 100644 index 993a398..450e8cd --- a/pyds8k/test/test_resources/test_ds8k/test_host.py +++ b/pyds8k/test/test_resources/test_ds8k/test_host.py @@ -14,579 +14,548 @@ # limitations under the License. ############################################################################## -import httpretty import json -from nose.tools import nottest -from functools import cmp_to_key import warnings -from pyds8k.exceptions import InternalServerError, FieldReadOnly +from functools import cmp_to_key +from http import HTTPStatus + +import pytest +import responses +from responses import matchers + +from pyds8k.dataParser.ds8k import RequestParser +from pyds8k.exceptions import FieldReadOnly, InternalServerError from pyds8k.messages import DEFAULT_SUCCESS_BODY_DICT -from pyds8k.resources.ds8k.v1.common.types import DS8K_HOST, \ - DS8K_VOLUME, \ - DS8K_IOPORT, \ - DS8K_HOST_PORT -from pyds8k.test.data import get_response_list_json_by_type, \ - get_response_list_data_by_type, \ - get_response_json_by_type, \ - get_response_data_by_type -from pyds8k.test.data import action_response, action_response_json, \ - action_response_failed, action_response_failed_json, \ - create_host_response_json -from .base import TestDS8KWithConnect -from pyds8k.resources.ds8k.v1.ioports import IOPort +from pyds8k.resources.ds8k.v1.common.types import ( + DS8K_HOST, + DS8K_HOST_PORT, + DS8K_IOPORT, + DS8K_VOLUME, +) from pyds8k.resources.ds8k.v1.host_ports import HostPort from pyds8k.resources.ds8k.v1.hosts import Host +from pyds8k.resources.ds8k.v1.ioports import IOPort from pyds8k.resources.ds8k.v1.volumes import Volume -from pyds8k.dataParser.ds8k import RequestParser +from pyds8k.test.data import ( + action_response, + action_response_failed, + action_response_failed_json, + action_response_json, + create_host_response_json, + get_response_data_by_type, + get_response_json_by_type, + get_response_list_data_by_type, + get_response_list_json_by_type, +) + +from .base import TestDS8KWithConnect volume_list_response = get_response_list_data_by_type(DS8K_VOLUME) volume_list_response_json = get_response_list_json_by_type(DS8K_VOLUME) class TestHost(TestDS8KWithConnect): - def test_get_volumes(self): - self._test_sub_resource_list_by_route(DS8K_HOST, DS8K_VOLUME, - self._sorted_by_volume_name - ) + self._test_sub_resource_list_by_route( + DS8K_HOST, DS8K_VOLUME, self._sorted_by_volume_name + ) def test_get_ioports(self): self._test_sub_resource_list_by_route( - DS8K_HOST, DS8K_IOPORT, - self._get_sort_func_by(IOPort.id_field) + DS8K_HOST, DS8K_IOPORT, self._get_sort_func_by(IOPort.id_field) ) def test_get_host_ports(self): self._test_sub_resource_list_by_route( - DS8K_HOST, DS8K_HOST_PORT, - self._get_sort_func_by(HostPort.id_field) + DS8K_HOST, DS8K_HOST_PORT, self._get_sort_func_by(HostPort.id_field) ) - @httpretty.activate + @responses.activate def test_delete_host(self): host_name = 'host1' - url = '/hosts/{}'.format(host_name) - httpretty.register_uri( - httpretty.GET, + url = f'/hosts/{host_name}' + responses.get( self.domain + self.base_url + url, body=get_response_json_by_type(DS8K_HOST), content_type='application/json', - status=200, + status=HTTPStatus.OK.value, ) - httpretty.register_uri( - httpretty.DELETE, + responses.delete( self.domain + self.base_url + url, body=action_response_json, content_type='application/json', - status=204, + status=HTTPStatus.OK.value, ) # Way 1 _ = self.system.delete_host(host_name) - self.assertEqual(httpretty.DELETE, httpretty.last_request().method) + assert responses.calls[-1].request.method == responses.DELETE # self.assertEqual(resp1, action_response['server']) # Way 2 host = self.system.get_host(host_name) - self.assertIsInstance(host, Host) + assert isinstance(host, Host) resp2, _ = host.delete() - self.assertEqual(resp2.status_code, 204) - self.assertEqual(httpretty.DELETE, httpretty.last_request().method) + assert resp2.status_code == HTTPStatus.OK.value + assert responses.calls[-1].request.method == responses.DELETE # self.assertEqual(resp2.text, action_response['server']) # self.assertEqual(data2, action_response['server']) # warnings.warn("TestHost.test_delete_host: do not know why \ - # requests can not get DELETE response's body. Maybe httpretty can \ + # requests can not get DELETE response's body. Maybe responses can \ # not set DELETE response's body correctly") - @httpretty.activate + @responses.activate def test_delete_host_without_resp_body(self): host_name = 'host1' - url = '/hosts/{}'.format(host_name) - httpretty.register_uri(httpretty.DELETE, - self.domain + self.base_url + url, - content_type='application/json', - status=204, - ) + url = f'/hosts/{host_name}' + responses.delete( + self.domain + self.base_url + url, + content_type='application/json', + status=HTTPStatus.NO_CONTENT.value, + ) resp1 = self.system.delete_host(host_name) - self.assertEqual(httpretty.DELETE, httpretty.last_request().method) - self.assertEqual(resp1, DEFAULT_SUCCESS_BODY_DICT) + assert responses.calls[-1].request.method == responses.DELETE + assert resp1 == DEFAULT_SUCCESS_BODY_DICT - @httpretty.activate + @responses.activate def test_delete_host_failed(self): host_name = 'host1' - url = '/hosts/{}'.format(host_name) - httpretty.register_uri(httpretty.DELETE, - self.domain + self.base_url + url, - body=action_response_failed_json, - content_type='application/json', - status=500, - ) - with self.assertRaises(InternalServerError) as cm: + url = f'/hosts/{host_name}' + responses.delete( + self.domain + self.base_url + url, + body=action_response_failed_json, + content_type='application/json', + status=HTTPStatus.INTERNAL_SERVER_ERROR.value, + ) + with pytest.raises(InternalServerError) as cm: self.system.delete_host(host_name) - self.assertEqual(action_response_failed['server'], - cm.exception.error_data - ) - self.assertEqual(httpretty.DELETE, httpretty.last_request().method) + assert action_response_failed['server'] == cm.value.error_data + assert responses.calls[-1].request.method == responses.DELETE - @httpretty.activate + @responses.activate def test_update_host_rm_ioports_all(self): host_name = 'host1' - url = '/hosts/{}'.format(host_name) + url = f'/hosts/{host_name}' + uri = f'{self.domain}{self.base_url}{url}' - def _verify_request(request, uri, headers): - self.assertEqual(uri, self.domain + self.base_url + url) - - resq = RequestParser({'ioports': []}) - self.assertEqual(json.loads(request.body), resq.get_request_data()) - return (200, headers, action_response_json) - - httpretty.register_uri(httpretty.GET, - self.domain + self.base_url + url, - body=get_response_json_by_type(DS8K_HOST), - content_type='application/json', - status=200, - ) - httpretty.register_uri(httpretty.PUT, - self.domain + self.base_url + url, - body=_verify_request, - content_type='application/json', - ) + responses.get( + uri, + body=get_response_json_by_type(DS8K_HOST), + content_type='application/json', + status=HTTPStatus.OK.value, + ) + + req = RequestParser({'ioports': []}) + responses.put( + uri, + status=HTTPStatus.OK, + body=action_response_json, + content_type='application/json', + match=[matchers.json_params_matcher(req.get_request_data())], + ) # Way 1 resp1 = self.system.update_host_rm_ioports_all(host_name) - self.assertEqual(httpretty.PUT, httpretty.last_request().method) - self.assertEqual(resp1, action_response['server']) + assert responses.calls[-1].request.method == responses.PUT + assert resp1 == action_response['server'] host = self.system.get_host(host_name) # Way 2 host.ioports = [] resp2, data2 = host.update() - self.assertEqual(httpretty.PUT, httpretty.last_request().method) - self.assertEqual(data2, action_response['server']) - self.assertEqual(resp2.status_code, 200) + assert responses.calls[-1].request.method == responses.PUT + assert data2 == action_response['server'] + assert resp2.status_code == HTTPStatus.OK # Way 3 in DS8K, save works the same as update host.ioports = [] resp3, data3 = host.save() - self.assertEqual(httpretty.PUT, httpretty.last_request().method) - self.assertEqual(data3, action_response['server']) - self.assertEqual(resp3.status_code, 200) + assert responses.calls[-1].request.method == responses.PUT + assert data3 == action_response['server'] + assert resp3.status_code == HTTPStatus.OK # Way 4 host.ioports = [] resp4, data4 = host.patch() - self.assertEqual(httpretty.PUT, httpretty.last_request().method) - self.assertEqual(data4, action_response['server']) - self.assertEqual(resp4.status_code, 200) + assert responses.calls[-1].request.method == responses.PUT + assert data4 == action_response['server'] + assert resp4.status_code == HTTPStatus.OK # Way 5 in DS8K, put works the same as patch host.ioports = [] resp5, data5 = host.put() - self.assertEqual(httpretty.PUT, httpretty.last_request().method) - self.assertEqual(data5, action_response['server']) - self.assertEqual(resp5.status_code, 200) + assert responses.calls[-1].request.method == responses.PUT + assert data5 == action_response['server'] + assert resp5.status_code == HTTPStatus.OK - @httpretty.activate + @responses.activate def test_update_host_add_ioports_all(self): host_name = 'host1' - url = '/hosts/{}'.format(host_name) - - def _verify_request(request, uri, headers): - self.assertEqual(uri, self.domain + self.base_url + url) - - resq = RequestParser({'ioports': 'all'}) - self.assertEqual(json.loads(request.body), resq.get_request_data()) - return (200, headers, action_response_json) + url = f'/hosts/{host_name}' + uri = f'{self.domain}{self.base_url}{url}' - httpretty.register_uri( - httpretty.GET, - self.domain + self.base_url + url, + responses.get( + uri, body=get_response_json_by_type(DS8K_HOST), content_type='application/json', - status=200, + status=HTTPStatus.OK.value, ) - httpretty.register_uri(httpretty.PUT, - self.domain + self.base_url + url, - body=_verify_request, - content_type='application/json', - ) + + req = RequestParser({'ioports': 'all'}) + responses.put( + uri, + status=HTTPStatus.OK, + body=action_response_json, + content_type='application/json', + match=[matchers.json_params_matcher(req.get_request_data())], + ) + # Way 1 resp1 = self.system.update_host_add_ioports_all(host_name) - self.assertEqual(httpretty.PUT, httpretty.last_request().method) - self.assertEqual(resp1, action_response['server']) + assert responses.calls[-1].request.method == responses.PUT + assert resp1 == action_response['server'] - @nottest - @httpretty.activate + @pytest.mark.skip + @responses.activate def test_update_host_rm_volumes_all(self): host_name = 'host1' - url = '/hosts/{}'.format(host_name) + url = f'/hosts/{host_name}' def _verify_request(request, uri, headers): - self.assertEqual(uri, self.domain + self.base_url + url) + assert uri == f"{self.domain}{self.base_url}{url}" resq = RequestParser({'volumes': []}) - self.assertEqual(json.loads(request.body), resq.get_request_data()) - return (200, headers, action_response_json) + assert json.loads(request.body) == resq.get_request_data() + return (HTTPStatus.OK, headers, action_response_json) - httpretty.register_uri( - httpretty.GET, + responses.get( self.domain + self.base_url + url, body=get_response_json_by_type(DS8K_HOST), content_type='application/json', - status=200, + status=HTTPStatus.OK.value, + ) + responses.put( + self.domain + self.base_url + url, + body=_verify_request, + content_type='application/json', ) - httpretty.register_uri(httpretty.PUT, - self.domain + self.base_url + url, - body=_verify_request, - content_type='application/json', - ) # Way 1 resp1 = self.system.update_host_rm_volumes_all(host_name) - self.assertEqual(httpretty.PUT, httpretty.last_request().method) - self.assertEqual(resp1, action_response['server']) + assert responses.calls[-1].request.method == responses.PUT + assert resp1 == action_response['server'] host = self.system.get_host(host_name) # Way 2 host.volumes = [] resp2, data2 = host.update() - self.assertEqual(httpretty.PUT, httpretty.last_request().method) - self.assertEqual(data2, action_response['server']) - self.assertEqual(resp2.status_code, 200) + assert responses.calls[-1].request.method == responses.PUT + assert data2 == action_response['server'] + assert resp2.status_code == HTTPStatus.OK - @nottest - @httpretty.activate + @pytest.mark.skip + @responses.activate def test_update_host_add_volumes(self): - warnings.warn('test_update_host_add_volumes: not finished yet.') + warnings.warn('test_update_host_add_volumes: not finished yet.', stacklevel=2) - @nottest - @httpretty.activate + @pytest.mark.skip + @responses.activate def test_update_host_rm_volumes(self): - warnings.warn('test_update_host_rm_volumes: not finished yet.') + warnings.warn('test_update_host_rm_volumes: not finished yet.', stacklevel=2) - @httpretty.activate + @responses.activate def test_update_host_add_ioports(self): response_a_json = get_response_json_by_type(DS8K_HOST) response_a = get_response_data_by_type(DS8K_HOST) - host_name = self._get_resource_id_from_resopnse(DS8K_HOST, response_a, - Host.id_field - ) + host_name = self._get_resource_id_from_resopnse( + DS8K_HOST, response_a, Host.id_field + ) res_all = get_response_list_data_by_type(DS8K_IOPORT) - ioport_ids = self._get_resource_ids_from_resopnse(DS8K_IOPORT, res_all, - IOPort.id_field - ) + ioport_ids = self._get_resource_ids_from_resopnse( + DS8K_IOPORT, res_all, IOPort.id_field + ) port_id = 'new_port_id' - url = '/hosts/{}'.format(host_name) - httpretty.register_uri(httpretty.GET, - self.domain + self.base_url + url, - body=response_a_json, - content_type='application/json', - status=200, - ) + url = f'/hosts/{host_name}' + uri = f'{self.domain}{self.base_url}{url}' + ioport_url = f'{url}/{DS8K_IOPORT}' + ioport_uri = f'{self.domain}{self.base_url}{ioport_url}' - def _verify_request(request, uri, headers): - self.assertEqual(uri, self.domain + self.base_url + url) - - ioport_ids.append(port_id) - resq = RequestParser({'ioports': ioport_ids}) - self.assertEqual(json.loads(request.body), resq.get_request_data()) - return (200, headers, action_response_json) - - httpretty.register_uri(httpretty.PUT, - self.domain + self.base_url + url, - body=_verify_request, - content_type='application/json', - ) - ioport_url = '{}/{}'.format(url, DS8K_IOPORT) - httpretty.register_uri(httpretty.GET, - self.domain + self.base_url + ioport_url, - body=get_response_list_json_by_type( - DS8K_IOPORT), - content_type='application/json', - status=200, - ) + responses.get( + uri, + body=response_a_json, + content_type='application/json', + status=HTTPStatus.OK.value, + ) + + ioport_ids.append(port_id) + resq = RequestParser({'ioports': ioport_ids}) + responses.put( + uri, + status=HTTPStatus.OK, + body=action_response_json, + content_type='application/json', + match=[matchers.json_params_matcher(resq.get_request_data())], + ) + + ioport_url = f'{url}/{DS8K_IOPORT}' + responses.get( + ioport_uri, + body=get_response_list_json_by_type(DS8K_IOPORT), + content_type='application/json', + status=HTTPStatus.OK.value, + ) host = self.system.get_host(host_name) resp = host.update_host_add_ioports(port_id) - self.assertEqual(httpretty.PUT, httpretty.last_request().method) - self.assertEqual(resp, action_response['server']) + assert responses.calls[-1].request.method == responses.PUT + assert resp == action_response['server'] - @httpretty.activate + @responses.activate def test_update_host_rm_ioports(self): response_a_json = get_response_json_by_type(DS8K_HOST) response_a = get_response_data_by_type(DS8K_HOST) - host_name = self._get_resource_id_from_resopnse(DS8K_HOST, response_a, - Host.id_field - ) + host_name = self._get_resource_id_from_resopnse( + DS8K_HOST, response_a, Host.id_field + ) res_all = get_response_list_data_by_type(DS8K_IOPORT) - ioport_ids = self._get_resource_ids_from_resopnse(DS8K_IOPORT, res_all, - IOPort.id_field - ) + ioport_ids = self._get_resource_ids_from_resopnse( + DS8K_IOPORT, res_all, IOPort.id_field + ) port_id = ioport_ids[0] - url = '/hosts/{}'.format(host_name) - httpretty.register_uri(httpretty.GET, - self.domain + self.base_url + url, - body=response_a_json, - content_type='application/json', - status=200, - ) + url = f'/hosts/{host_name}' + uri = f'{self.domain}{self.base_url}{url}' + ioport_url = f'{url}/{DS8K_IOPORT}' + ioport_uri = f'{self.domain}{self.base_url}{ioport_url}' - def _verify_request(request, uri, headers): - self.assertEqual(uri, self.domain + self.base_url + url) - - resq = RequestParser({'ioports': ioport_ids[1:]}) - self.assertEqual(json.loads(request.body), resq.get_request_data()) - return (200, headers, action_response_json) - - httpretty.register_uri(httpretty.PUT, - self.domain + self.base_url + url, - body=_verify_request, - content_type='application/json', - ) - ioport_url = '{}/{}'.format(url, DS8K_IOPORT) - httpretty.register_uri(httpretty.GET, - self.domain + self.base_url + ioport_url, - body=get_response_list_json_by_type( - DS8K_IOPORT), - content_type='application/json', - status=200, - ) + responses.get( + uri, + body=response_a_json, + content_type='application/json', + status=HTTPStatus.OK.value, + ) + + resq = RequestParser({'ioports': ioport_ids[1:]}) + responses.put( + uri, + status=HTTPStatus.OK, + body=action_response_json, + content_type='application/json', + match=[matchers.json_params_matcher(resq.get_request_data())], + ) + ioport_url = f'{url}/{DS8K_IOPORT}' + responses.get( + ioport_uri, + body=get_response_list_json_by_type(DS8K_IOPORT), + content_type='application/json', + status=HTTPStatus.OK.value, + ) host = self.system.get_host(host_name) resp = host.update_host_rm_ioports(port_id) - self.assertEqual(httpretty.PUT, httpretty.last_request().method) - self.assertEqual(resp, action_response['server']) + assert responses.calls[-1].request.method == responses.PUT + assert resp == action_response['server'] - @httpretty.activate + @responses.activate def test_update_host_failed(self): host_name = 'host1' - url = '/hosts/{}'.format(host_name) - - httpretty.register_uri(httpretty.PUT, - self.domain + self.base_url + url, - body=action_response_failed_json, - content_type='application/json', - status=500 - ) - with self.assertRaises(InternalServerError) as cm: + url = f'/hosts/{host_name}' + + responses.put( + self.domain + self.base_url + url, + body=action_response_failed_json, + content_type='application/json', + status=HTTPStatus.INTERNAL_SERVER_ERROR.value, + ) + with pytest.raises(InternalServerError) as cm: self.system.update_host_rm_ioports_all(host_name) - self.assertEqual(action_response_failed['server'], - cm.exception.error_data - ) + assert action_response_failed['server'] == cm.value.error_data def test_set_readonly_field(self): host = Host(self.client) - with self.assertRaises(FieldReadOnly): + with pytest.raises(FieldReadOnly): host.name = 'new_name' - with self.assertRaises(FieldReadOnly): + with pytest.raises(FieldReadOnly): host.state = 'new_state' - with self.assertRaises(FieldReadOnly): + with pytest.raises(FieldReadOnly): host.addrmode = 'new_addrmode' - with self.assertRaises(FieldReadOnly): + with pytest.raises(FieldReadOnly): host.addrdiscovery = 'new_addrdiscovery' - with self.assertRaises(FieldReadOnly): + with pytest.raises(FieldReadOnly): host.lbs = 'new_lbs' def test_set_related_resources_collection(self): - volumes = [Volume(self.client, resource_id='volume{}'.format(i)) - for i in range(10) - ] - host_ports = \ - [HostPort(self.client, resource_id='host_port{}'.format(i)) - for i in range(10) - ] - ioports = [IOPort(self.client, resource_id='ioport{}'.format(i)) - for i in range(10) - ] + volumes = [Volume(self.client, resource_id=f'volume{i}') for i in range(10)] + host_ports = [ + HostPort(self.client, resource_id=f'host_port{i}') for i in range(10) + ] + ioports = [IOPort(self.client, resource_id=f'ioport{i}') for i in range(10)] # init without related_resources collection - host = Host(self.client, info={ - 'volumes': 'volumes', # string - 'ioports': '', # empty - 'host_ports': { # link - 'link': { - 'rel': 'self', - 'href': '/api/v1//host_ports' + host = Host( + self.client, + info={ + 'volumes': 'volumes', # string + 'ioports': '', # empty + 'host_ports': { # link + 'link': {'rel': 'self', 'href': '/api/v1//host_ports'}, }, - } - } - ) + }, + ) for i in host.related_resources_collection: - self.assertEqual('', host.representation.get(i)) - self.assertFalse(hasattr(host, i)) + assert host.representation.get(i) == '' + assert not hasattr(host, i) # setting related resources collection - for item in ((DS8K_VOLUME, volumes), - (DS8K_IOPORT, ioports) - ): + for item in ((DS8K_VOLUME, volumes), (DS8K_IOPORT, ioports)): setattr(host, item[0], item[1]) for j, value in enumerate(host.representation[item[0]]): - self.assertEqual(value, - getattr(item[1][j], item[1][j].id_field) - ) + assert value == getattr(item[1][j], item[1][j].id_field) for ind, v in enumerate(host._get_modified_info_dict()[item[0]]): - self.assertEqual(v, - getattr(item[1][ind], item[1][ind].id_field) - ) + assert v == getattr(item[1][ind], item[1][ind].id_field) # loading related resources collection host.volumes = [] host.ioports = [] host._start_updating() - for item in ((DS8K_VOLUME, volumes), - (DS8K_HOST_PORT, host_ports), - (DS8K_IOPORT, ioports) - ): + for item in ( + (DS8K_VOLUME, volumes), + (DS8K_HOST_PORT, host_ports), + (DS8K_IOPORT, ioports), + ): setattr(host, item[0], item[1]) for j, value in enumerate(host.representation[item[0]]): - self.assertEqual(value, - getattr(item[1][j], item[1][j].id_field) - ) + assert value == getattr(item[1][j], item[1][j].id_field) host._stop_updating() - @httpretty.activate + @responses.activate def test_lazy_loading_related_resources_collection(self): response_a_json = get_response_json_by_type(DS8K_HOST) response_a = get_response_data_by_type(DS8K_HOST) - host_name = self._get_resource_id_from_resopnse(DS8K_HOST, response_a, - Host.id_field - ) - url = '/hosts/{}'.format(host_name) - httpretty.register_uri(httpretty.GET, - self.domain + self.base_url + url, - body=response_a_json, - content_type='application/json', - status=200, - ) + host_name = self._get_resource_id_from_resopnse( + DS8K_HOST, response_a, Host.id_field + ) + url = f'/hosts/{host_name}' + responses.get( + self.domain + self.base_url + url, + body=response_a_json, + content_type='application/json', + status=HTTPStatus.OK.value, + ) for item in Host.related_resources_collection: - sub_route_url = '{}/{}'.format(url, item) - httpretty.register_uri(httpretty.GET, - self.domain + self.base_url + sub_route_url, - body=get_response_list_json_by_type(item), - content_type='application/json', - status=200, - ) + sub_route_url = f'{url}/{item}' + responses.get( + self.domain + self.base_url + sub_route_url, + body=get_response_list_json_by_type(item), + content_type='application/json', + status=HTTPStatus.OK.value, + ) host = self.system.get_host(host_name) for item in Host.related_resources_collection: res_collection = getattr(host, item) - self.assertNotEqual(0, len(res_collection)) + assert len(res_collection) != 0 res_collection.sort( - key=cmp_to_key( - self._get_sort_func_by(res_collection[0].id_field)) + key=cmp_to_key(self._get_sort_func_by(res_collection[0].id_field)) ) res_collection_data = list( get_response_list_data_by_type(item)['data'][item] ) res_collection_data.sort( - key=cmp_to_key( - self._get_sort_func_by(res_collection[0].id_field)) + key=cmp_to_key(self._get_sort_func_by(res_collection[0].id_field)) ) - self.assertEqual( - len(res_collection_data), - len(res_collection)) + assert len(res_collection_data) == len(res_collection) self._assert_equal_between_sorted_dict_and_resource_list( - res_collection_data, - res_collection + res_collection_data, res_collection ) def test_set_related_resources_collection_during_loading(self): - host = Host(self.client, info={ - 'volumes': [{ - 'id': '0000', - 'link': { - 'rel': 'self', - 'href': '/api/v1/volumes/0000' - }, + host = Host( + self.client, + info={ + 'volumes': [ + { + 'id': '0000', + 'link': {'rel': 'self', 'href': '/api/v1/volumes/0000'}, + }, + ], + 'ioports': [ + { + 'id': '0030', + 'link': {'rel': 'self', 'href': '/api/v1/ioports/0030'}, + }, + ], # missing + 'host_ports': [ + { + 'wwpn': '50050763030313A2', + 'link': { + 'rel': 'self', + 'href': '/api/v1/host_ports/50050763030313A2', + }, + }, + ], }, - ], - 'ioports': [{ - 'id': '0030', - 'link': { - 'rel': 'self', - 'href': '/api/v1/ioports/0030' - }, - }, - ], # missing - 'host_ports': [{ - 'wwpn': '50050763030313A2', - 'link': { - 'rel': 'self', - 'href': '/api/v1/host_ports/50050763030313A2' - }, - }, - ], - } - ) - - self.assertEqual('0000', host.representation.get('volumes')[0]) - self.assertEqual('0030', host.representation.get('ioports')[0]) - self.assertEqual('50050763030313A2', - host.representation.get('host_ports')[0] - ) - self.assertEqual('0000', host.volumes[0].id) - self.assertEqual('0030', host.ioports[0].id) - self.assertEqual('50050763030313A2', host.host_ports[0].id) - - @httpretty.activate + ) + + assert host.representation.get('volumes')[0] == '0000' + assert host.representation.get('ioports')[0] == '0030' + assert host.representation.get('host_ports')[0] == '50050763030313A2' + assert host.volumes[0].id == '0000' + assert host.ioports[0].id == '0030' + assert host.host_ports[0].id == '50050763030313A2' + + @responses.activate def test_create_host(self): host_type = 'VMware' url = '/hosts' - host_name = 'host1' + uri = f'{self.domain}{self.base_url}{url}' - def _verify_request(request, uri, headers): - self.assertEqual(uri, self.domain + self.base_url + url) + host_name = 'host1' - req = RequestParser({'hosttype': host_type, 'name': host_name}) - self.assertDictContainsSubset( - req.get_request_data().get('request').get('params'), - json.loads(request.body).get('request').get('params'), - ) - return (200, headers, create_host_response_json) + req = RequestParser({'hosttype': host_type, 'name': host_name}) + responses.post( + uri, + status=HTTPStatus.OK, + body=create_host_response_json, + content_type='application/json', + match=[matchers.json_params_matcher(req.get_request_data())], + ) - httpretty.register_uri(httpretty.POST, - self.domain + self.base_url + url, - body=_verify_request, - content_type='application/json', - ) # Way 1 resp1 = self.system.create_host(host_name, host_type) - self.assertEqual(httpretty.POST, httpretty.last_request().method) - self.assertIsInstance(resp1[0], Host) + assert responses.calls[-1].request.method == responses.POST + assert isinstance(resp1[0], Host) # Way 2 host = self.system.all(DS8K_HOST, rebuild_url=True) new_host2 = host.create(hosttype=host_type, name=host_name) resp2, data2 = new_host2.posta() - self.assertEqual(httpretty.POST, httpretty.last_request().method) - self.assertIsInstance(data2[0], Host) - self.assertEqual(resp2.status_code, 200) + assert responses.calls[-1].request.method == responses.POST + assert isinstance(data2[0], Host) + assert resp2.status_code == HTTPStatus.OK # Way 3 host = self.system.all(DS8K_HOST, rebuild_url=True) new_host3 = host.create(hosttype=host_type, name=host_name) resp3, data3 = new_host3.save() - self.assertEqual(httpretty.POST, httpretty.last_request().method) - self.assertIsInstance(data3[0], Host) - self.assertEqual(resp3.status_code, 200) + assert responses.calls[-1].request.method == responses.POST + assert isinstance(data3[0], Host) + assert resp3.status_code == HTTPStatus.OK # Way 4 # Don't init a resource instance by yourself when create new. # use .create() instead. - @httpretty.activate + @responses.activate def test_create_host_failed(self): host_type = 'VMware' url = '/hosts' host_name = 'host1' - httpretty.register_uri(httpretty.POST, - self.domain + self.base_url + url, - body=action_response_failed_json, - content_type='application/json', - status=500 - ) - with self.assertRaises(InternalServerError) as cm: + responses.post( + self.domain + self.base_url + url, + body=action_response_failed_json, + content_type='application/json', + status=HTTPStatus.INTERNAL_SERVER_ERROR.value, + ) + with pytest.raises(InternalServerError) as cm: self.system.create_host(host_name, host_type) - self.assertEqual(action_response_failed['server'], - cm.exception.error_data - ) + assert action_response_failed['server'] == cm.value.error_data diff --git a/pyds8k/test/test_resources/test_ds8k/test_host_port.py b/pyds8k/test/test_resources/test_ds8k/test_host_port.py old mode 100755 new mode 100644 index f8eaec4..3b4aaad --- a/pyds8k/test/test_resources/test_ds8k/test_host_port.py +++ b/pyds8k/test/test_resources/test_ds8k/test_host_port.py @@ -14,294 +14,261 @@ # limitations under the License. ############################################################################## -import httpretty -import json +from http import HTTPStatus + +import pytest +import responses +from responses import matchers + +from pyds8k.dataParser.ds8k import RequestParser + # import warnings -from pyds8k.exceptions import InternalServerError, FieldReadOnly +from pyds8k.exceptions import FieldReadOnly, InternalServerError from pyds8k.messages import DEFAULT_SUCCESS_BODY_DICT from pyds8k.resources.ds8k.v1.common.types import DS8K_HOST_PORT -from pyds8k.resources.ds8k.v1.host_ports import HostPort, \ - HostPortManager -from pyds8k.resources.ds8k.v1.ioports import IOPort +from pyds8k.resources.ds8k.v1.host_ports import HostPort, HostPortManager from pyds8k.resources.ds8k.v1.hosts import Host +from pyds8k.resources.ds8k.v1.ioports import IOPort +from pyds8k.test.data import ( + action_response, + action_response_failed, + action_response_failed_json, + action_response_json, + create_host_port_response_json, + get_response_data_by_type, + get_response_json_by_type, +) + from .base import TestDS8KWithConnect -from pyds8k.test.data import get_response_json_by_type, \ - get_response_data_by_type -from pyds8k.test.data import action_response, action_response_json, \ - action_response_failed, action_response_failed_json, \ - create_host_port_response_json -from pyds8k.dataParser.ds8k import RequestParser response_a = get_response_data_by_type(DS8K_HOST_PORT) response_a_json = get_response_json_by_type(DS8K_HOST_PORT) class TestHostPort(TestDS8KWithConnect): - def setUp(self): - super(TestHostPort, self).setUp() + super().setUp() self.host_port = HostPort(self.client, HostPortManager(self.client)) - self.wwpn = self._get_resource_id_from_resopnse(DS8K_HOST_PORT, - response_a, - HostPort.id_field - ) + self.wwpn = self._get_resource_id_from_resopnse( + DS8K_HOST_PORT, response_a, HostPort.id_field + ) - @httpretty.activate + @responses.activate def test_delete_host_port(self): - url = '/host_ports/{}'.format(self.wwpn) - httpretty.register_uri( - httpretty.GET, - self.domain + self.base_url + url, - body=response_a_json, - content_type='application/json', - status=200, - ) - httpretty.register_uri( - httpretty.DELETE, - self.domain + self.base_url + url, - body=action_response_json, - content_type='application/json', - status=204, - ) + url = f'/host_ports/{self.wwpn}' + responses.get( + self.domain + self.base_url + url, + body=response_a_json, + content_type='application/json', + status=HTTPStatus.OK.value, + ) + responses.delete( + self.domain + self.base_url + url, + body=action_response_json, + content_type='application/json', + status=HTTPStatus.OK.value, + ) # Way 1 _ = self.system.delete_host_port(self.wwpn) - self.assertEqual(httpretty.DELETE, httpretty.last_request().method) + assert responses.calls[-1].request.method == responses.DELETE # self.assertEqual(resp1, action_response['server']) # Way 2 host_port = self.system.get_host_port(self.wwpn) - self.assertIsInstance(host_port, HostPort) + assert isinstance(host_port, HostPort) resp2, _ = host_port.delete() - self.assertEqual(resp2.status_code, 204) - self.assertEqual(httpretty.DELETE, httpretty.last_request().method) + assert resp2.status_code == HTTPStatus.OK.value + assert responses.calls[-1].request.method == responses.DELETE # self.assertEqual(resp2.text, action_response['server']) # self.assertEqual(data2, action_response['server']) # warnings.warn("TestHostPort.test_delete_host_port: do not know why \ -# requests can not get DELETE response's body. Maybe httpretty can \ -# not set DELETE response's body correctly") - @httpretty.activate + # requests can not get DELETE response's body. Maybe responses can \ + # not set DELETE response's body correctly") + + @responses.activate def test_delete_host_port_without_resp_body(self): - url = '/host_ports/{}'.format(self.wwpn) - httpretty.register_uri(httpretty.DELETE, - self.domain + self.base_url + url, - content_type='application/json', - status=204, - ) + url = f'/host_ports/{self.wwpn}' + responses.delete( + self.domain + self.base_url + url, + content_type='application/json', + status=HTTPStatus.NO_CONTENT.value, + ) resp1 = self.system.delete_host_port(self.wwpn) - self.assertEqual(httpretty.DELETE, httpretty.last_request().method) - self.assertEqual(resp1, DEFAULT_SUCCESS_BODY_DICT) + assert responses.calls[-1].request.method == responses.DELETE + assert resp1 == DEFAULT_SUCCESS_BODY_DICT - @httpretty.activate + @responses.activate def test_delete_host_port_failed(self): - url = '/host_ports/{}'.format(self.wwpn) - httpretty.register_uri( - httpretty.DELETE, - self.domain + self.base_url + url, - body=action_response_failed_json, - content_type='application/json', - status=500, - ) - with self.assertRaises(InternalServerError) as cm: + url = f'/host_ports/{self.wwpn}' + responses.delete( + self.domain + self.base_url + url, + body=action_response_failed_json, + content_type='application/json', + status=HTTPStatus.INTERNAL_SERVER_ERROR.value, + ) + with pytest.raises(InternalServerError) as cm: self.system.delete_host_port(self.wwpn) - self.assertEqual(action_response_failed['server'], - cm.exception.error_data - ) - self.assertEqual(httpretty.DELETE, httpretty.last_request().method) + assert action_response_failed['server'] == cm.value.error_data + assert responses.calls[-1].request.method == responses.DELETE - @httpretty.activate + @responses.activate def test_update_host_port(self): - url = '/host_ports/{}'.format(self.wwpn) + url = f'/host_ports/{self.wwpn}' + uri = f'{self.domain}{self.base_url}{url}' + new_host_name = 'new_host' - def _verify_request(request, uri, headers): - self.assertEqual(uri, self.domain + self.base_url + url) - - resq = RequestParser({'host': new_host_name}) - self.assertEqual(json.loads(request.body), resq.get_request_data()) - return (200, headers, action_response_json) - - httpretty.register_uri( - httpretty.GET, - self.domain + self.base_url + url, - body=response_a_json, - content_type='application/json', - status=200, - ) - httpretty.register_uri(httpretty.PUT, - self.domain + self.base_url + url, - body=_verify_request, - content_type='application/json', - ) + responses.get( + uri, + body=response_a_json, + content_type='application/json', + status=HTTPStatus.OK.value, + ) + + resq = RequestParser({'host': new_host_name}) + responses.put( + uri, + status=HTTPStatus.OK, + body=action_response_json, + content_type='application/json', + match=[matchers.json_params_matcher(resq.get_request_data())], + ) + # Way 1 - resp1 = self.system.update_host_port_change_host(self.wwpn, - new_host_name - ) - self.assertEqual(httpretty.PUT, httpretty.last_request().method) - self.assertEqual(resp1, action_response['server']) + resp1 = self.system.update_host_port_change_host(self.wwpn, new_host_name) + assert responses.calls[-1].request.method == responses.PUT + assert resp1 == action_response['server'] host_port = self.system.get_host_port(self.wwpn) # Way 2 host_port.host = new_host_name resp2, data2 = host_port.update() - self.assertEqual(httpretty.PUT, httpretty.last_request().method) - self.assertEqual(data2, action_response['server']) - self.assertEqual(resp2.status_code, 200) + assert responses.calls[-1].request.method == responses.PUT + assert data2 == action_response['server'] + assert resp2.status_code == HTTPStatus.OK # Way 3 in DS8K, save works the same as update host_port.host = new_host_name resp3, data3 = host_port.save() - self.assertEqual(httpretty.PUT, httpretty.last_request().method) - self.assertEqual(data3, action_response['server']) - self.assertEqual(resp3.status_code, 200) + assert responses.calls[-1].request.method == responses.PUT + assert data3 == action_response['server'] + assert resp3.status_code == HTTPStatus.OK # Way 4 host_port.host = new_host_name resp4, data4 = host_port.patch() - self.assertEqual(httpretty.PUT, httpretty.last_request().method) - self.assertEqual(data4, action_response['server']) - self.assertEqual(resp4.status_code, 200) + assert responses.calls[-1].request.method == responses.PUT + assert data4 == action_response['server'] + assert resp4.status_code == HTTPStatus.OK # Way 5 in DS8K, put works the same as patch host_port.host = new_host_name resp5, data5 = host_port.put() - self.assertEqual(httpretty.PUT, httpretty.last_request().method) - self.assertEqual(data5, action_response['server']) - self.assertEqual(resp5.status_code, 200) + assert responses.calls[-1].request.method == responses.PUT + assert data5 == action_response['server'] + assert resp5.status_code == HTTPStatus.OK - @httpretty.activate + @responses.activate def test_update_host_port_failed(self): - url = '/host_ports/{}'.format(self.wwpn) + url = f'/host_ports/{self.wwpn}' new_host_name = 'new_host' - httpretty.register_uri(httpretty.PUT, - self.domain + self.base_url + url, - body=action_response_failed_json, - content_type='application/json', - status=500 - ) - with self.assertRaises(InternalServerError) as cm: + responses.put( + self.domain + self.base_url + url, + body=action_response_failed_json, + content_type='application/json', + status=HTTPStatus.INTERNAL_SERVER_ERROR.value, + ) + with pytest.raises(InternalServerError) as cm: self.system.update_host_port_change_host(self.wwpn, new_host_name) - self.assertEqual(action_response_failed['server'], - cm.exception.error_data - ) + assert action_response_failed['server'] == cm.value.error_data def test_set_readonly_field(self): - with self.assertRaises(FieldReadOnly): + with pytest.raises(FieldReadOnly): self.host_port.state = 'new_state' - with self.assertRaises(FieldReadOnly): + with pytest.raises(FieldReadOnly): self.host_port.wwpn = 'new_wwpn' def test_update_host_field(self): - host_info = get_response_data_by_type( - DS8K_HOST_PORT - )['data'][DS8K_HOST_PORT][0] + host_info = get_response_data_by_type(DS8K_HOST_PORT)['data'][DS8K_HOST_PORT][0] host_name = host_info['host']['name'] self.host_port._add_details(host_info) - self.assertEqual( - self.host_port.host, - host_name - ) - self.assertEqual( - self.host_port.representation['host'], - host_name - ) - self.assertIsInstance(self.host_port._host, Host) - self.assertEqual( - self.host_port._host.id, - host_name - ) + assert self.host_port.host == host_name + assert self.host_port.representation['host'] == host_name + assert isinstance(self.host_port._host, Host) + assert self.host_port._host.id == host_name self.host_port.host = 'new_host' - self.assertEqual( - self.host_port.host, - 'new_host' - ) - self.assertEqual( - self.host_port.representation['host'], - 'new_host' - ) - - @httpretty.activate + assert self.host_port.host == 'new_host' + assert self.host_port.representation['host'] == 'new_host' + + @responses.activate def test_create_host_port(self): url = '/host_ports' + uri = f'{self.domain}{self.base_url}{url}' + host_name = 'host1' - def _verify_request(request, uri, headers): - self.assertEqual(uri, self.domain + self.base_url + url) - - req = RequestParser({'wwpn': self.wwpn, 'host': host_name}) - self.assertDictContainsSubset( - req.get_request_data().get('request').get('params'), - json.loads(request.body).get('request').get('params'), - ) - return (200, headers, create_host_port_response_json) - - httpretty.register_uri(httpretty.POST, - self.domain + self.base_url + url, - body=_verify_request, - content_type='application/json', - ) + req = RequestParser({'wwpn': self.wwpn, 'host': host_name}) + responses.post( + uri, + status=HTTPStatus.OK, + body=create_host_port_response_json, + content_type='application/json', + match=[matchers.json_params_matcher(req.get_request_data())], + ) + # Way 1 resp1 = self.system.create_host_port(self.wwpn, host_name) - self.assertEqual(httpretty.POST, httpretty.last_request().method) - self.assertIsInstance(resp1[0], HostPort) + assert responses.calls[-1].request.method == responses.POST + assert isinstance(resp1[0], HostPort) # Way 2 host_port = self.system.all(DS8K_HOST_PORT, rebuild_url=True) new_host_port2 = host_port.create(wwpn=self.wwpn, host=host_name) resp2, data2 = new_host_port2.posta() - self.assertEqual(httpretty.POST, httpretty.last_request().method) - self.assertIsInstance(data2[0], HostPort) - self.assertEqual(resp2.status_code, 200) + assert responses.calls[-1].request.method == responses.POST + assert isinstance(data2[0], HostPort) + assert resp2.status_code == HTTPStatus.OK # Way 3 host_port = self.system.all(DS8K_HOST_PORT, rebuild_url=True) new_host_port3 = host_port.create(wwpn=self.wwpn, host=host_name) resp3, data3 = new_host_port3.save() - self.assertEqual(httpretty.POST, httpretty.last_request().method) - self.assertIsInstance(data3[0], HostPort) - self.assertEqual(resp3.status_code, 200) + assert responses.calls[-1].request.method == responses.POST + assert isinstance(data3[0], HostPort) + assert resp3.status_code == HTTPStatus.OK # Way 4 # Don't init a resource instance by yourself when create new. # use .create() instead. - @httpretty.activate + @responses.activate def test_create_host_port_failed(self): url = '/host_ports' host_name = 'host1' - httpretty.register_uri(httpretty.POST, - self.domain + self.base_url + url, - body=action_response_failed_json, - content_type='application/json', - status=500 - ) - with self.assertRaises(InternalServerError) as cm: + responses.post( + self.domain + self.base_url + url, + body=action_response_failed_json, + content_type='application/json', + status=HTTPStatus.INTERNAL_SERVER_ERROR.value, + ) + with pytest.raises(InternalServerError) as cm: self.system.create_host_port(self.wwpn, host_name) - self.assertEqual(action_response_failed['server'], - cm.exception.error_data - ) + assert action_response_failed['server'] == cm.value.error_data def test_related_resource_field(self): self._test_related_resource_field(DS8K_HOST_PORT) def test_occupied_ioports(self): - OCCUPIED_IOPORTS = 'login_ports' - info = get_response_data_by_type( - DS8K_HOST_PORT - )['data'][DS8K_HOST_PORT][0] - host_port = HostPort(self.client, - HostPortManager(self.client), - info=info - ) - ioport_ids = [port.get(IOPort.id_field) - for port in info[OCCUPIED_IOPORTS] - ] - self.assertCountEqual(ioport_ids, - host_port.representation.get(OCCUPIED_IOPORTS) - ) - self.assertIsInstance(getattr(host_port, OCCUPIED_IOPORTS)[0], IOPort) - self.assertIn(getattr(host_port, OCCUPIED_IOPORTS)[0].id, ioport_ids) + occupied_ioports = 'login_ports' + info = get_response_data_by_type(DS8K_HOST_PORT)['data'][DS8K_HOST_PORT][0] + host_port = HostPort(self.client, HostPortManager(self.client), info=info) + ioport_ids = [port.get(IOPort.id_field) for port in info[occupied_ioports]] + # self.assertCountEqual(ioport_ids, host_port.representation.get(occupied_ioports)) + assert ioport_ids == host_port.representation.get(occupied_ioports) + assert isinstance(getattr(host_port, occupied_ioports)[0], IOPort) + assert getattr(host_port, occupied_ioports)[0].id in ioport_ids diff --git a/pyds8k/test/test_resources/test_ds8k/test_ioport.py b/pyds8k/test/test_resources/test_ds8k/test_ioport.py old mode 100755 new mode 100644 index e073041..8321037 --- a/pyds8k/test/test_resources/test_ds8k/test_ioport.py +++ b/pyds8k/test/test_resources/test_ds8k/test_ioport.py @@ -15,10 +15,10 @@ ############################################################################## from pyds8k.resources.ds8k.v1.common.types import DS8K_IOPORT + from .base import TestDS8KWithConnect class TestIOPort(TestDS8KWithConnect): - def test_related_resource_field(self): self._test_related_resource_field(DS8K_IOPORT) diff --git a/pyds8k/test/test_resources/test_ds8k/test_lss.py b/pyds8k/test/test_resources/test_ds8k/test_lss.py old mode 100755 new mode 100644 index 5dfd3b5..2a259d7 --- a/pyds8k/test/test_resources/test_ds8k/test_lss.py +++ b/pyds8k/test/test_resources/test_ds8k/test_lss.py @@ -14,143 +14,134 @@ # limitations under the License. ############################################################################## import json - -import httpretty from functools import cmp_to_key -from pyds8k.resources.ds8k.v1.common.types import DS8K_LSS, \ - DS8K_VOLUME -from pyds8k.test.data import get_response_list_json_by_type, \ - get_response_list_data_by_type, \ - get_response_json_by_type, \ - create_lss_response -from .base import TestDS8KWithConnect -from pyds8k.resources.ds8k.v1.lss import LSS, LSSManager -from pyds8k.resources.ds8k.v1.volumes import Volume +from http import HTTPStatus + +import pytest +import responses +from responses import matchers + +from pyds8k.dataParser.ds8k import RequestParser from pyds8k.messages import INVALID_TYPE from pyds8k.resources.ds8k.v1.common import types -from pyds8k.dataParser.ds8k import RequestParser +from pyds8k.resources.ds8k.v1.common.types import DS8K_LSS, DS8K_VOLUME +from pyds8k.resources.ds8k.v1.lss import LSS, LSSManager +from pyds8k.resources.ds8k.v1.volumes import Volume +from pyds8k.test.data import ( + create_lss_response, + get_response_json_by_type, + get_response_list_data_by_type, + get_response_list_json_by_type, +) +from .base import TestDS8KWithConnect -class TestLSS(TestDS8KWithConnect): +class TestLSS(TestDS8KWithConnect): def setUp(self): - super(TestLSS, self).setUp() + super().setUp() self.lss = LSS(self.client, LSSManager(self.client)) def test_get_volumes(self): - self._test_sub_resource_list_by_route(DS8K_LSS, DS8K_VOLUME, - self._sorted_by_volume_name - ) + self._test_sub_resource_list_by_route( + DS8K_LSS, DS8K_VOLUME, self._sorted_by_volume_name + ) def test_set_related_resources_collection(self): - volumes = [Volume(self.client, resource_id='volume{}'.format(i)) - for i in range(10) - ] + volumes = [Volume(self.client, resource_id=f'volume{i}') for i in range(10)] # init without related_resources collection - lss = LSS(self.client, info={ - 'volumes': { - 'link': { - 'rel': 'self', - 'href': '/api/volumes' - }, - } - } - ) + lss = LSS( + self.client, + info={ + 'volumes': { + 'link': {'rel': 'self', 'href': '/api/volumes'}, + } + }, + ) for i in lss.related_resources_collection: - self.assertEqual('', lss.representation.get(i)) - self.assertFalse(hasattr(lss, i)) + assert lss.representation.get(i) == '' + assert not hasattr(lss, i) # loading related resources collection lss._start_updating() - for item in ((DS8K_VOLUME, volumes), - ): + for item in ((DS8K_VOLUME, volumes),): setattr(lss, item[0], item[1]) for j, value in enumerate(lss.representation[item[0]]): - self.assertEqual(value, - getattr(item[1][j], item[1][j].id_field) - ) + assert value == getattr(item[1][j], item[1][j].id_field) lss._stop_updating() - @httpretty.activate + @responses.activate def test_lazy_loading_related_resources_collection(self): lss_id = '00' - url = '/lss/{}'.format(lss_id) - httpretty.register_uri(httpretty.GET, - self.domain + self.base_url + url, - body=get_response_json_by_type(DS8K_LSS), - content_type='application/json', - status=200, - ) + url = f'/lss/{lss_id}' + responses.get( + self.domain + self.base_url + url, + body=get_response_json_by_type(DS8K_LSS), + content_type='application/json', + status=HTTPStatus.OK.value, + ) for item in LSS.related_resources_collection: - sub_route_url = '{}/{}'.format(url, item) - httpretty.register_uri(httpretty.GET, - self.domain + self.base_url + sub_route_url, - body=get_response_list_json_by_type(item), - content_type='application/json', - status=200, - ) + sub_route_url = f'{url}/{item}' + responses.get( + self.domain + self.base_url + sub_route_url, + body=get_response_list_json_by_type(item), + content_type='application/json', + status=HTTPStatus.OK.value, + ) lss = self.system.get_lss_by_id(lss_id) for item in LSS.related_resources_collection: res_collection = getattr(lss, item) - self.assertNotEqual(0, len(res_collection)) + assert len(res_collection) != 0 res_collection.sort( - key=cmp_to_key( - self._get_sort_func_by(res_collection[0].id_field)) + key=cmp_to_key(self._get_sort_func_by(res_collection[0].id_field)) ) res_collection_data = list( get_response_list_data_by_type(item)['data'][item] ) res_collection_data.sort( - key=cmp_to_key( - self._get_sort_func_by(res_collection[0].id_field)) + key=cmp_to_key(self._get_sort_func_by(res_collection[0].id_field)) ) - self.assertEqual( - len(res_collection_data), - len(res_collection)) + assert len(res_collection_data) == len(res_collection) self._assert_equal_between_sorted_dict_and_resource_list( - res_collection_data, - res_collection + res_collection_data, res_collection ) def test_set_related_resources_collection_during_loading(self): - lss = LSS(self.client, info={ - 'volumes': [{ - 'id': '0000', - 'link': { - 'rel': 'self', - 'href': '/api/volumes/0000' - }, + lss = LSS( + self.client, + info={ + 'volumes': [ + { + 'id': '0000', + 'link': {'rel': 'self', 'href': '/api/volumes/0000'}, + }, + ], }, - ], - } - ) + ) - self.assertEqual('0000', lss.representation.get('volumes')[0]) - self.assertEqual('0000', lss.volumes[0].id) + assert lss.representation.get('volumes')[0] == '0000' + assert lss.volumes[0].id == '0000' def test_invalid_lss_type(self): - with self.assertRaises(ValueError) as holder_exception: + with pytest.raises( + ValueError, match=INVALID_TYPE.format(', '.join(types.DS8K_LSS_TYPES)) + ): LSS(self.client, lss_type="fake") - self.assertEqual( - INVALID_TYPE.format(', '.join(types.DS8K_LSS_TYPES)), - str(holder_exception.exception) - ) def test_invalid_ckd_based_cu_type(self): - with self.assertRaises(ValueError) as holder_exception: + with pytest.raises( + ValueError, match=INVALID_TYPE.format(', '.join(types.DS8K_LCU_TYPES)) + ): LSS(self.client, lcu_type="fake") - self.assertEqual( - INVALID_TYPE.format(', '.join(types.DS8K_LCU_TYPES)), - str(holder_exception.exception) - ) - @httpretty.activate + @responses.activate def test_create_lss_ckd(self): url = '/lss' - full_url = self.domain + self.base_url + url + uri = f'{self.domain}{self.base_url}{url}' + struct_request = { 'id': 'FE', 'type': 'ckd', @@ -158,31 +149,20 @@ def test_create_lss_ckd(self): 'ckd_base_cu_type': types.DS8K_LCU_TYPE_3990_6, } - def _verify_request(request, uri, headers): - self.assertEqual( - uri, - full_url - ) - - req = RequestParser(struct_request) - self.assertDictContainsSubset( - req.get_request_data().get('request').get('params'), - json.loads(request.body).get('request').get('params') - ) - return 201, headers, json.dumps(create_lss_response) - - httpretty.register_uri( - httpretty.POST, - full_url, - body=_verify_request, - content_type='application/json' + req = RequestParser(struct_request) + responses.post( + uri, + status=HTTPStatus.OK, + body=json.dumps(create_lss_response), + content_type='application/json', + match=[matchers.json_params_matcher(req.get_request_data())], ) resp = self.system.create_lss_ckd( lss_id=struct_request['id'], lss_type=struct_request['type'], lcu_type=struct_request['ckd_base_cu_type'], - ss_id=struct_request['sub_system_identifier'] + ss_id=struct_request['sub_system_identifier'], ) - self.assertEqual(httpretty.POST, httpretty.last_request().method) - self.assertIsInstance(resp[0], LSS) + assert responses.calls[-1].request.method == responses.POST + assert isinstance(resp[0], LSS) diff --git a/pyds8k/test/test_resources/test_ds8k/test_mapping.py b/pyds8k/test/test_resources/test_ds8k/test_mapping.py old mode 100755 new mode 100644 index 401b9e5..e078a2e --- a/pyds8k/test/test_resources/test_ds8k/test_mapping.py +++ b/pyds8k/test/test_resources/test_ds8k/test_mapping.py @@ -14,21 +14,29 @@ # limitations under the License. ############################################################################## -import httpretty -import json +from http import HTTPStatus + +import pytest +import responses +from responses import matchers + +from pyds8k.dataParser.ds8k import RequestParser from pyds8k.exceptions import InternalServerError -from pyds8k.resources.ds8k.v1.common.types import DS8K_HOST, \ - DS8K_VOLMAP -from pyds8k.test.data import get_response_json_by_type, \ - get_response_data_by_type -from pyds8k.test.data import action_response_json, \ - create_mappings_response_json, create_mapping_response_json, \ - action_response_failed, action_response_failed_json -from .base import TestDS8KWithConnect -from pyds8k.resources.ds8k.v1.mappings import Volmap +from pyds8k.resources.ds8k.v1.common.types import DS8K_HOST, DS8K_VOLMAP from pyds8k.resources.ds8k.v1.hosts import Host +from pyds8k.resources.ds8k.v1.mappings import Volmap from pyds8k.resources.ds8k.v1.volumes import Volume -from pyds8k.dataParser.ds8k import RequestParser +from pyds8k.test.data import ( + action_response_failed, + action_response_failed_json, + action_response_json, + create_mapping_response_json, + create_mappings_response_json, + get_response_data_by_type, + get_response_json_by_type, +) + +from .base import TestDS8KWithConnect host_response = get_response_data_by_type(DS8K_HOST) host_response_json = get_response_json_by_type(DS8K_HOST) @@ -37,17 +45,14 @@ class TestVolmap(TestDS8KWithConnect): - def setUp(self): - super(TestVolmap, self).setUp() - self.host_id = self._get_resource_id_from_resopnse(DS8K_HOST, - host_response, - Host.id_field - ) - self.lunid = self._get_resource_id_from_resopnse(DS8K_VOLMAP, - mapping_response, - Volmap.id_field - ) + super().setUp() + self.host_id = self._get_resource_id_from_resopnse( + DS8K_HOST, host_response, Host.id_field + ) + self.lunid = self._get_resource_id_from_resopnse( + DS8K_VOLMAP, mapping_response, Volmap.id_field + ) self.host = self.system.one( DS8K_HOST, self.host_id, @@ -58,153 +63,140 @@ def test_related_resource_field(self): mapping_info = mapping_response['data'][DS8K_VOLMAP][0] volume_id = mapping_info['volume'][Volume.id_field] mapping = Volmap(self.client, info=mapping_info) - self.assertEqual(mapping.volume, volume_id) - self.assertEqual(mapping.representation['volume'], volume_id) - self.assertIsInstance(mapping._volume, Volume) - self.assertEqual(mapping._volume.id, volume_id) + assert mapping.volume == volume_id + assert mapping.representation['volume'] == volume_id + assert isinstance(mapping._volume, Volume) + assert mapping._volume.id == volume_id def test_get_mappings(self): self._test_sub_resource_list_by_route( - DS8K_HOST, DS8K_VOLMAP, - self._get_sort_func_by(Volmap.id_field) + DS8K_HOST, DS8K_VOLMAP, self._get_sort_func_by(Volmap.id_field) ) - @httpretty.activate + @responses.activate def test_delete_mapping(self): - url = '/hosts/{}/mappings/{}'.format(self.host_id, self.lunid) - httpretty.register_uri( - httpretty.GET, + url = f'/hosts/{self.host_id}/mappings/{self.lunid}' + responses.get( self.domain + self.base_url + url, body=mapping_response_json, content_type='application/json', - status=200, + status=HTTPStatus.OK.value, ) - httpretty.register_uri( - httpretty.DELETE, + responses.delete( self.domain + self.base_url + url, body=action_response_json, content_type='application/json', - status=204, + status=HTTPStatus.OK.value, ) # Way 1 _ = self.host.delete_mapping(self.lunid) - self.assertEqual(httpretty.DELETE, httpretty.last_request().method) + assert responses.calls[-1].request.method == responses.DELETE # self.assertEqual(resp1, action_response['server']) # Way 2 mapping = self.host.get_mapping(self.lunid) - self.assertIsInstance(mapping, Volmap) + assert isinstance(mapping, Volmap) resp2, _ = mapping.delete() - self.assertEqual(resp2.status_code, 204) - self.assertEqual(httpretty.DELETE, httpretty.last_request().method) + assert resp2.status_code == HTTPStatus.OK.value + assert responses.calls[-1].request.method == responses.DELETE - @httpretty.activate + @responses.activate def test_delete_mapping_failed(self): - url = '/hosts/{}/mappings/{}'.format(self.host_id, self.lunid) - httpretty.register_uri( - httpretty.DELETE, - self.domain + self.base_url + url, + url = f'/hosts/{self.host_id}/mappings/{self.lunid}' + uri = f'{self.domain}{self.base_url}{url}' + + responses.delete( + uri, + status=HTTPStatus.INTERNAL_SERVER_ERROR.value, # ???: Why is .value required here? body=action_response_failed_json, content_type='application/json', - status=500, ) - with self.assertRaises(InternalServerError) as cm: + with pytest.raises(InternalServerError) as cm: self.host.delete_mapping(self.lunid) - self.assertEqual(action_response_failed['server'], - cm.exception.error_data - ) - self.assertEqual(httpretty.DELETE, httpretty.last_request().method) + assert action_response_failed['server'] == cm.value.error_data + assert responses.calls[-1].request.method == responses.DELETE - @httpretty.activate + @responses.activate def test_create_mappings_with_volume_id(self): - url = '/hosts/{}/mappings'.format(self.host_id) - volumes = ['000{}'.format(i) for i in range(10)] + url = f'/hosts/{self.host_id}/mappings' + uri = f'{self.domain}{self.base_url}{url}' - def _verify_request(request, uri, headers): - self.assertEqual(uri, self.domain + self.base_url + url) + volumes = [f'000{i}' for i in range(10)] - resq = RequestParser({'volumes': volumes}) - self.assertEqual(json.loads(request.body), resq.get_request_data()) - return (200, headers, create_mappings_response_json) - - httpretty.register_uri(httpretty.POST, - self.domain + self.base_url + url, - body=_verify_request, - content_type='application/json', - ) + resq = RequestParser({'volumes': volumes}) + responses.post( + uri, + status=HTTPStatus.CREATED, + body=create_mappings_response_json, + content_type='application/json', + match=[matchers.json_params_matcher(resq.get_request_data())], + ) # Way 1 resp1 = self.host.create_mappings(volumes=volumes) - self.assertEqual(httpretty.POST, httpretty.last_request().method) - self.assertIsInstance(resp1[0], Volmap) + assert responses.calls[-1].request.method == responses.POST + assert isinstance(resp1[0], Volmap) - @httpretty.activate + @responses.activate def test_create_mappings_with_mappings(self): - url = '/hosts/{}/mappings'.format(self.host_id) - mappings = [{'0{}'.format(i): '000{}'.format(i)} for i in range(10)] - - def _verify_request(request, uri, headers): - self.assertEqual(uri, self.domain + self.base_url + url) + url = f'/hosts/{self.host_id}/mappings' + uri = f'{self.domain}{self.base_url}{url}' - resq = RequestParser({'mappings': mappings}) - self.assertEqual(json.loads(request.body), resq.get_request_data()) - return (200, headers, create_mappings_response_json) + mappings = [{f'0{i}': f'000{i}'} for i in range(10)] - httpretty.register_uri(httpretty.POST, - self.domain + self.base_url + url, - body=_verify_request, - content_type='application/json', - ) + resq = RequestParser({'mappings': mappings}) + responses.post( + uri, + status=HTTPStatus.CREATED, + body=create_mappings_response_json, + content_type='application/json', + match=[matchers.json_params_matcher(resq.get_request_data())], + ) # Way 1 resp1 = self.host.create_mappings(mappings=mappings) - self.assertEqual(httpretty.POST, httpretty.last_request().method) - self.assertIsInstance(resp1[0], Volmap) + assert responses.calls[-1].request.method == responses.POST + assert isinstance(resp1[0], Volmap) - @httpretty.activate + @responses.activate def test_create_mapping_with_volume_and_lunid(self): - url = '/hosts/{}/mappings'.format(self.host_id) + url = f'/hosts/{self.host_id}/mappings' + uri = f'{self.domain}{self.base_url}{url}' + lunid = '00' volume_id = '0000' - def _verify_request(request, uri, headers): - self.assertEqual(uri, self.domain + self.base_url + url) - - resq = RequestParser({'lunid': lunid, 'volume': volume_id}) - self.assertEqual(json.loads(request.body), resq.get_request_data()) - return (200, headers, create_mapping_response_json) - - httpretty.register_uri(httpretty.POST, - self.domain + self.base_url + url, - body=_verify_request, - content_type='application/json', - ) + resq = RequestParser({'lunid': lunid, 'volume': volume_id}) + responses.post( + uri, + status=HTTPStatus.OK, + body=create_mapping_response_json, + content_type='application/json', + match=[matchers.json_params_matcher(resq.get_request_data())], + ) mapping = self.host.all(DS8K_VOLMAP) new_mapping = mapping.create(lunid=lunid, volume=volume_id) resp, data = new_mapping.save() - self.assertEqual(httpretty.POST, httpretty.last_request().method) - self.assertIsInstance(data[0], Volmap) - self.assertEqual(resp.status_code, 200) + assert responses.calls[-1].request.method == responses.POST + assert isinstance(data[0], Volmap) + assert resp.status_code == HTTPStatus.OK - @httpretty.activate + @responses.activate def test_create_mapping_with_volume(self): - url = '/hosts/{}/mappings'.format(self.host_id) - volume_id = '0000' - - def _verify_request(request, uri, headers): - self.assertEqual(uri, self.domain + self.base_url + url) + url = f'/hosts/{self.host_id}/mappings' + uri = f'{self.domain}{self.base_url}{url}' - resq = RequestParser({'lunid': '', 'volume': volume_id}) - self.assertEqual(json.loads(request.body), - resq.get_request_data()) - return (200, headers, create_mapping_response_json) + volume_id = '0000' - httpretty.register_uri(httpretty.POST, - self.domain + self.base_url + url, - body=_verify_request, - content_type='application/json', - ) + resq = RequestParser({'lunid': '', 'volume': volume_id}) + responses.post( + uri, + status=HTTPStatus.OK, + body=create_mapping_response_json, + content_type='application/json', + match=[matchers.json_params_matcher(resq.get_request_data())], + ) mapping = self.host.all(DS8K_VOLMAP) new_mapping = mapping.create(lunid='', volume=volume_id) resp, data = new_mapping.save() - self.assertEqual(httpretty.POST, httpretty.last_request().method) - self.assertIsInstance(data[0], Volmap) - self.assertEqual(resp.status_code, 200) + assert responses.calls[-1].request.method == responses.POST + assert isinstance(data[0], Volmap) + assert resp.status_code == HTTPStatus.OK diff --git a/pyds8k/test/test_resources/test_ds8k/test_pool.py b/pyds8k/test/test_resources/test_ds8k/test_pool.py old mode 100755 new mode 100644 index e252104..b4438d8 --- a/pyds8k/test/test_resources/test_ds8k/test_pool.py +++ b/pyds8k/test/test_resources/test_ds8k/test_pool.py @@ -14,47 +14,50 @@ # limitations under the License. ############################################################################## -import httpretty -import json from functools import cmp_to_key +from http import HTTPStatus + +import responses +from responses import matchers + from pyds8k.dataParser.ds8k import RequestParser -from pyds8k.resources.ds8k.v1.common.types import DS8K_POOL, \ - DS8K_VOLUME, \ - DS8K_TSEREP, \ - DS8K_ESEREP -from pyds8k.test.data import get_response_list_json_by_type, \ - get_response_list_data_by_type, \ - get_response_json_by_type, \ - get_response_data_by_type -from pyds8k.test.data import action_response, action_response_json -from .base import TestDS8KWithConnect -from pyds8k.resources.ds8k.v1.volumes import Volume +from pyds8k.resources.ds8k.v1.common.types import ( + DS8K_ESEREP, + DS8K_POOL, + DS8K_TSEREP, + DS8K_VOLUME, +) +from pyds8k.resources.ds8k.v1.eserep import ESERep from pyds8k.resources.ds8k.v1.pools import Pool from pyds8k.resources.ds8k.v1.tserep import TSERep -from pyds8k.resources.ds8k.v1.eserep import ESERep +from pyds8k.resources.ds8k.v1.volumes import Volume +from pyds8k.test.data import ( + action_response, + action_response_json, + get_response_data_by_type, + get_response_json_by_type, + get_response_list_data_by_type, + get_response_list_json_by_type, +) + +from .base import TestDS8KWithConnect response_a = get_response_data_by_type(DS8K_POOL) response_a_json = get_response_json_by_type(DS8K_POOL) class TestPool(TestDS8KWithConnect): - def setUp(self): - super(TestPool, self).setUp() - self.pool_id = self._get_resource_id_from_resopnse(DS8K_POOL, - response_a, - Pool.id_field - ) - self.pool = self.system.one( - DS8K_POOL, - self.pool_id, - rebuild_url=True + super().setUp() + self.pool_id = self._get_resource_id_from_resopnse( + DS8K_POOL, response_a, Pool.id_field ) + self.pool = self.system.one(DS8K_POOL, self.pool_id, rebuild_url=True) def test_get_volumes(self): - self._test_sub_resource_list_by_route(DS8K_POOL, DS8K_VOLUME, - self._sorted_by_volume_name - ) + self._test_sub_resource_list_by_route( + DS8K_POOL, DS8K_VOLUME, self._sorted_by_volume_name + ) def test_get_tserep(self): self._test_sub_resource_list_by_route(DS8K_POOL, DS8K_TSEREP) @@ -62,72 +65,66 @@ def test_get_tserep(self): def test_get_eserep(self): self._test_sub_resource_list_by_route(DS8K_POOL, DS8K_ESEREP) - @httpretty.activate + @responses.activate def test_delete_tserep(self): - url = '/pools/{}/tserep'.format(self.pool_id) - httpretty.register_uri( - httpretty.DELETE, + url = f'/pools/{self.pool_id}/tserep' + responses.delete( self.domain + self.base_url + url, content_type='application/json', - status=204, + status=HTTPStatus.OK.value, ) self.pool.delete_tserep() - self.assertEqual(httpretty.DELETE, httpretty.last_request().method) + assert responses.calls[-1].request.method == responses.DELETE - @httpretty.activate + @responses.activate def test_delete_eserep(self): - url = '/pools/{}/eserep'.format(self.pool_id) - httpretty.register_uri( - httpretty.DELETE, + url = f'/pools/{self.pool_id}/eserep' + responses.delete( self.domain + self.base_url + url, content_type='application/json', - status=204, + status=HTTPStatus.OK.value, ) self.pool.delete_eserep() - self.assertEqual(httpretty.DELETE, httpretty.last_request().method) + assert responses.calls[-1].request.method == responses.DELETE - @httpretty.activate + @responses.activate def test_update_tserep_cap(self): - url = '/pools/{}/tserep'.format(self.pool_id) + url = f'/pools/{self.pool_id}/tserep' + uri = f'{self.domain}{self.base_url}{url}' + cap = '10' captype = 'gib' - def _verify_request(request, uri, headers): - self.assertEqual(uri, self.domain + self.base_url + url) - - resq = RequestParser({'cap': cap, 'captype': captype}) - self.assertEqual(json.loads(request.body), resq.get_request_data()) - return (200, headers, action_response_json) - - httpretty.register_uri(httpretty.PUT, - self.domain + self.base_url + url, - body=_verify_request, - content_type='application/json', - ) + resq = RequestParser({'cap': cap, 'captype': captype}) + responses.put( + uri, + status=HTTPStatus.OK, + body=action_response_json, + content_type='application/json', + match=[matchers.json_params_matcher(resq.get_request_data())], + ) _, body = self.pool.update_tserep_cap(cap, captype) - self.assertEqual(httpretty.PUT, httpretty.last_request().method) - self.assertEqual(body, action_response['server']) + assert responses.calls[-1].request.method == responses.PUT + assert body == action_response['server'] - @httpretty.activate + @responses.activate def test_update_tserep_threshold(self): - url = '/pools/{}/tserep'.format(self.pool_id) - threshold = '70' + url = f'/pools/{self.pool_id}/tserep' + uri = f'{self.domain}{self.base_url}{url}' - def _verify_request(request, uri, headers): - self.assertEqual(uri, self.domain + self.base_url + url) - - resq = RequestParser({'threshold': threshold}) - self.assertEqual(json.loads(request.body), resq.get_request_data()) - return (200, headers, action_response_json) + threshold = '70' - httpretty.register_uri(httpretty.PUT, - self.domain + self.base_url + url, - body=_verify_request, - content_type='application/json', - ) + resq = RequestParser({'threshold': threshold}) + responses.put( + uri, + status=HTTPStatus.CREATED, + body=action_response_json, + content_type='application/json', + match=[matchers.json_params_matcher(resq.get_request_data())], + ) _, body = self.pool.update_tserep_threshold(threshold) - self.assertEqual(httpretty.PUT, httpretty.last_request().method) - self.assertEqual(body, action_response['server']) + assert responses.calls[-1].request.method == responses.PUT + assert body == action_response['server'] def test_update_eserep_cap(self): pass @@ -136,97 +133,92 @@ def test_update_eserep_threshold(self): pass def test_set_related_resources_collection(self): - volumes = [Volume(self.client, resource_id='volume{}'.format(i)) - for i in range(10) - ] - tserep = [TSERep(self.client, info={'pool': {'name': 'testpool_0'}}), ] - eserep = [ESERep(self.client, info={'pool': {'name': 'testpool_0'}}), ] + volumes = [Volume(self.client, resource_id=f'volume{i}') for i in range(10)] + tserep = [ + TSERep(self.client, info={'pool': {'name': 'testpool_0'}}), + ] + eserep = [ + ESERep(self.client, info={'pool': {'name': 'testpool_0'}}), + ] # init without related_resources collection - pool = Pool(self.client, info={ - 'name': 'testpool_0', - 'eserep': '', - 'tserep': '', - 'volumes': { - 'link': { - 'rel': 'self', - 'href': '/api/volumes' + pool = Pool( + self.client, + info={ + 'name': 'testpool_0', + 'eserep': '', + 'tserep': '', + 'volumes': { + 'link': {'rel': 'self', 'href': '/api/volumes'}, }, - } - } - ) + }, + ) for i in pool.related_resources_collection: - self.assertEqual('', pool.representation.get(i)) - self.assertFalse(hasattr(pool, i)) + assert pool.representation.get(i) == '' + assert not hasattr(pool, i) # loading related resources collection pool._start_updating() - for item in ((DS8K_VOLUME, volumes), - (DS8K_TSEREP, tserep), - (DS8K_ESEREP, eserep), - ): + for item in ( + (DS8K_VOLUME, volumes), + (DS8K_TSEREP, tserep), + (DS8K_ESEREP, eserep), + ): setattr(pool, item[0], item[1]) for j, value in enumerate(pool.representation[item[0]]): - self.assertEqual(value, - getattr(item[1][j], item[1][j].id_field) - ) + assert value == getattr(item[1][j], item[1][j].id_field) pool._stop_updating() - @httpretty.activate + @responses.activate def test_lazy_loading_related_resources_collection(self): - url = '/pools/{}'.format(self.pool_id) - httpretty.register_uri(httpretty.GET, - self.domain + self.base_url + url, - body=response_a_json, - content_type='application/json', - status=200, - ) + url = f'/pools/{self.pool_id}' + responses.get( + self.domain + self.base_url + url, + body=response_a_json, + content_type='application/json', + status=HTTPStatus.OK.value, + ) for item in Pool.related_resources_collection: - sub_route_url = '{}/{}'.format(url, item) - httpretty.register_uri(httpretty.GET, - self.domain + self.base_url + sub_route_url, - body=get_response_list_json_by_type(item), - content_type='application/json', - status=200, - ) + sub_route_url = f'{url}/{item}' + responses.get( + self.domain + self.base_url + sub_route_url, + body=get_response_list_json_by_type(item), + content_type='application/json', + status=HTTPStatus.OK.value, + ) pool = self.system.get_pool(self.pool_id) for item in Pool.related_resources_collection: res_collection = getattr(pool, item) - self.assertNotEqual(0, len(res_collection)) + assert len(res_collection) != 0 res_collection.sort( - key=cmp_to_key( - self._get_sort_func_by(res_collection[0].id_field)) + key=cmp_to_key(self._get_sort_func_by(res_collection[0].id_field)) ) res_collection_data = list( get_response_list_data_by_type(item)['data'][item] ) res_collection_data.sort( - key=cmp_to_key( - self._get_sort_func_by(res_collection[0].id_field)) + key=cmp_to_key(self._get_sort_func_by(res_collection[0].id_field)) ) - self.assertEqual( - len(res_collection_data), - len(res_collection)) + assert len(res_collection_data) == len(res_collection) self._assert_equal_between_sorted_dict_and_resource_list( - res_collection_data, - res_collection + res_collection_data, res_collection ) def test_set_related_resources_collection_during_loading(self): - pool = Pool(self.client, info={ - 'name': 'testpool_0', - 'volumes': [{ - 'id': '0000', - 'link': { - 'rel': 'self', - 'href': '/api/volumes/0000' - }, + pool = Pool( + self.client, + info={ + 'name': 'testpool_0', + 'volumes': [ + { + 'id': '0000', + 'link': {'rel': 'self', 'href': '/api/volumes/0000'}, + }, + ], }, - ], - } - ) + ) - self.assertEqual('0000', pool.representation.get('volumes')[0]) - self.assertEqual('0000', pool.volumes[0].id) + assert pool.representation.get('volumes')[0] == '0000' + assert pool.volumes[0].id == '0000' diff --git a/pyds8k/test/test_resources/test_ds8k/test_pprc.py b/pyds8k/test/test_resources/test_ds8k/test_pprc.py old mode 100755 new mode 100644 index 6d9a1e0..5999b61 --- a/pyds8k/test/test_resources/test_ds8k/test_pprc.py +++ b/pyds8k/test/test_resources/test_ds8k/test_pprc.py @@ -15,32 +15,30 @@ ############################################################################## from pyds8k.resources.ds8k.v1.common.types import DS8K_PPRC -from pyds8k.test.data import get_response_data_by_type -from .base import TestDS8KWithConnect -from pyds8k.resources.ds8k.v1.volumes import Volume from pyds8k.resources.ds8k.v1.pprc import PPRC from pyds8k.resources.ds8k.v1.systems import System +from pyds8k.resources.ds8k.v1.volumes import Volume +from pyds8k.test.data import get_response_data_by_type + +from .base import TestDS8KWithConnect class TestPPRC(TestDS8KWithConnect): - def test_related_resource_field(self): - pprc_info = get_response_data_by_type( - DS8K_PPRC - )['data'][DS8K_PPRC][0] + pprc_info = get_response_data_by_type(DS8K_PPRC)['data'][DS8K_PPRC][0] sourcevolume_id = pprc_info['sourcevolume'][Volume.id_field] targetvolume_id = pprc_info['targetvolume'][Volume.id_field] targetsystem_id = pprc_info['targetsystem'][System.id_field] pprc = PPRC(self.client, info=pprc_info) - self.assertEqual(pprc.sourcevolume, sourcevolume_id) - self.assertEqual(pprc.representation['sourcevolume'], sourcevolume_id) - self.assertIsInstance(pprc._sourcevolume, Volume) - self.assertEqual(pprc._sourcevolume.id, sourcevolume_id) - self.assertEqual(pprc.targetvolume, targetvolume_id) - self.assertEqual(pprc.representation['targetvolume'], targetvolume_id) - self.assertIsInstance(pprc._targetvolume, Volume) - self.assertEqual(pprc._targetvolume.id, targetvolume_id) - self.assertEqual(pprc.targetsystem, targetsystem_id) - self.assertEqual(pprc.representation['targetsystem'], targetsystem_id) - self.assertIsInstance(pprc._targetsystem, System) - self.assertEqual(pprc._targetsystem.id, targetsystem_id) + assert pprc.sourcevolume == sourcevolume_id + assert pprc.representation['sourcevolume'] == sourcevolume_id + assert isinstance(pprc._sourcevolume, Volume) + assert pprc._sourcevolume.id == sourcevolume_id + assert pprc.targetvolume == targetvolume_id + assert pprc.representation['targetvolume'] == targetvolume_id + assert isinstance(pprc._targetvolume, Volume) + assert pprc._targetvolume.id == targetvolume_id + assert pprc.targetsystem == targetsystem_id + assert pprc.representation['targetsystem'] == targetsystem_id + assert isinstance(pprc._targetsystem, System) + assert pprc._targetsystem.id == targetsystem_id diff --git a/pyds8k/test/test_resources/test_ds8k/test_resource_group.py b/pyds8k/test/test_resources/test_ds8k/test_resource_group.py new file mode 100644 index 0000000..aa59463 --- /dev/null +++ b/pyds8k/test/test_resources/test_ds8k/test_resource_group.py @@ -0,0 +1,229 @@ +############################################################################## +# Copyright 2022 IBM Corp. +# +# 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 +# +# 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 http import HTTPStatus + +import responses +from responses import matchers + +from pyds8k.dataParser.ds8k import RequestParser +from pyds8k.resources.ds8k.v1.common.types import DS8K_RESOURCE_GROUP +from pyds8k.resources.ds8k.v1.resource_groups import ResourceGroup +from pyds8k.test.data import ( + action_response, + action_response_json, + create_resource_group_response_json, + get_response_data_by_type, + get_response_json_by_type, +) + +from .base import TestDS8KWithConnect + +response_a = get_response_data_by_type(DS8K_RESOURCE_GROUP) +response_a_json = get_response_json_by_type(DS8K_RESOURCE_GROUP) + + +class TestResourceGroup(TestDS8KWithConnect): + def setUp(self): + super().setUp() + self.resource_group_id = self._get_resource_id_from_resopnse( + DS8K_RESOURCE_GROUP, response_a, ResourceGroup.id_field + ) + self.resource_group = self.system.one( + DS8K_RESOURCE_GROUP, self.resource_group_id, rebuild_url=True + ) + + @responses.activate + def test_delete_resource_group(self): + url = f'/resource_groups/{self.resource_group_id}' + responses.get( + self.domain + self.base_url + url, + body=response_a_json, + content_type='application/json', + status=HTTPStatus.OK.value, + ) + responses.delete( + self.domain + self.base_url + url, + body=action_response_json, + content_type='application/json', + status=HTTPStatus.OK.value, + ) + # Way 1 + _ = self.system.delete_resource_group(self.resource_group_id) + assert responses.calls[-1].request.method == responses.DELETE + # self.assertEqual(resp1, action_response['server']) + + # Way 2 + resource_group = self.system.get_resource_group(self.resource_group_id) + assert isinstance(resource_group, ResourceGroup) + resp2, _ = resource_group.delete() + assert resp2.status_code == HTTPStatus.OK + assert responses.calls[-1].request.method == responses.DELETE + + @responses.activate + def test_update_resource_group(self): + url = f'/resource_groups/{self.resource_group_id}' + uri = f'{self.domain}{self.base_url}{url}' + + new_name = 'new_name' + new_label = 'new_label' + new_cs_global = 'SECRET' + new_pass_global = 'TOP' + new_gm_masters = ['00', '01'] + new_gm_sessions = ['FE', 'FD'] + + responses.get( + uri, + body=response_a_json, + content_type='application/json', + status=HTTPStatus.OK.value, + ) + + resq = RequestParser( + { + 'name': new_name, + 'label': new_label, + 'cs_global': new_cs_global, + 'pass_global': new_pass_global, + 'gm_masters': new_gm_masters, + 'gm_sessions': new_gm_sessions, + }, + ) + responses.put( + uri, + status=HTTPStatus.OK.value, + body=action_response_json, + content_type='application/json', + match=[matchers.json_params_matcher(resq.get_request_data())], + ) + + # Way 1 + res = self.system.update_resource_group( + self.resource_group_id, + label=new_label, + name=new_name, + cs_global=new_cs_global, + pass_global=new_pass_global, + gm_masters=new_gm_masters, + gm_sessions=new_gm_sessions, + ) + assert responses.calls[-1].request.method == responses.PUT + assert res == action_response['server'] + + resource_group = self.system.get_resource_group(self.resource_group_id) + # Way 2 + resource_group.label = new_label + resource_group.name = new_name + resource_group.cs_global = new_cs_global + resource_group.pass_global = new_pass_global + resource_group.gm_masters = new_gm_masters + resource_group.gm_sessions = new_gm_sessions + resp2, data2 = resource_group.update() + assert responses.calls[-1].request.method == responses.PUT + assert data2 == action_response['server'] + assert resp2.status_code == HTTPStatus.OK + + # Way 3 in DS8K, save works the same as update + resource_group.label = new_label + resource_group.name = new_name + resource_group.cs_global = new_cs_global + resource_group.pass_global = new_pass_global + resource_group.gm_masters = new_gm_masters + resource_group.gm_sessions = new_gm_sessions + resp3, data3 = resource_group.save() + assert responses.calls[-1].request.method == responses.PUT + assert data3 == action_response['server'] + assert resp3.status_code == HTTPStatus.OK + + # Way 4 + resource_group.label = new_label + resource_group.name = new_name + resource_group.cs_global = new_cs_global + resource_group.pass_global = new_pass_global + resource_group.gm_masters = new_gm_masters + resource_group.gm_sessions = new_gm_sessions + resp4, data4 = resource_group.patch() + assert responses.calls[-1].request.method == responses.PUT + assert data4 == action_response['server'] + assert resp4.status_code == HTTPStatus.OK + + # Way 5 in DS8K, put works the same as patch + resource_group.label = new_label + resource_group.name = new_name + resource_group.cs_global = new_cs_global + resource_group.pass_global = new_pass_global + resource_group.gm_masters = new_gm_masters + resource_group.gm_sessions = new_gm_sessions + resp5, data5 = resource_group.put() + assert responses.calls[-1].request.method == responses.PUT + assert data5 == action_response['server'] + assert resp5.status_code == HTTPStatus.OK + + @responses.activate + def test_create_resource_group(self): + url = '/resource_groups' + uri = f'{self.domain}{self.base_url}{url}' + + label = 'group1' + name = 'group1' + + req = RequestParser( + { + 'label': label, + 'name': name, + } + ) + responses.post( + uri, + status=HTTPStatus.CREATED, + body=create_resource_group_response_json, + content_type='application/json', + match=[matchers.json_params_matcher(req.get_request_data())], + ) + + # Way 1 + resp1 = self.system.create_resource_group( + label=label, + name=name, + ) + assert responses.calls[-1].request.method == responses.POST + assert isinstance(resp1[0], ResourceGroup) + + # Way 2 + resource_group = self.system.all(DS8K_RESOURCE_GROUP, rebuild_url=True) + resource_group2 = resource_group.create( + label=label, + name=name, + ) + resp2, data2 = resource_group2.posta() + assert responses.calls[-1].request.method == responses.POST + assert isinstance(data2[0], ResourceGroup) + assert resp2.status_code == HTTPStatus.CREATED + + # Way 3 + resource_group = self.system.all(DS8K_RESOURCE_GROUP, rebuild_url=True) + resource_group3 = resource_group.create( + label=label, + name=name, + ) + resp3, data3 = resource_group3.save() + assert responses.calls[-1].request.method == responses.POST + assert isinstance(data3[0], ResourceGroup) + assert resp3.status_code == HTTPStatus.CREATED + + # Way 4 + # Don't init a resource instance by yourself when create new. + # use .create() instead. diff --git a/pyds8k/test/test_resources/test_ds8k/test_root_resource_mixin.py b/pyds8k/test/test_resources/test_ds8k/test_root_resource_mixin.py old mode 100755 new mode 100644 index 3839fae..4ae5512 --- a/pyds8k/test/test_resources/test_ds8k/test_root_resource_mixin.py +++ b/pyds8k/test/test_resources/test_ds8k/test_root_resource_mixin.py @@ -14,21 +14,29 @@ # limitations under the License. ############################################################################## -import httpretty -from nose.tools import nottest +from http import HTTPStatus + +import pytest +import responses +from responses import matchers + from pyds8k.resources.ds8k.v1.common import types -from pyds8k.test.data import get_response_list_json_by_type, \ - get_response_list_data_by_type, \ - get_response_data_by_type, \ - get_response_json_by_type -from pyds8k.test.data import action_response_json -from .base import TestDS8KWithConnect -from pyds8k.resources.ds8k.v1.systems import System +from pyds8k.resources.ds8k.v1.eserep import ESERep from pyds8k.resources.ds8k.v1.lss import LSS +from pyds8k.resources.ds8k.v1.systems import System + # from pyds8k.resources.ds8k.v1.ioports import IOPort from pyds8k.resources.ds8k.v1.tserep import TSERep -from pyds8k.resources.ds8k.v1.eserep import ESERep from pyds8k.resources.ds8k.v1.volumes import Volume +from pyds8k.test.data import ( + action_response_json, + get_response_data_by_type, + get_response_json_by_type, + get_response_list_data_by_type, + get_response_list_json_by_type, +) + +from .base import TestDS8KWithConnect system_list_response = get_response_list_data_by_type(types.DS8K_SYSTEM) system_list_response_json = get_response_list_json_by_type(types.DS8K_SYSTEM) @@ -44,21 +52,24 @@ eserep_list_response_json = get_response_list_json_by_type(types.DS8K_ESEREP) volume_list_response = get_response_list_data_by_type(types.DS8K_VOLUME) volume_list_response_json = get_response_list_json_by_type(types.DS8K_VOLUME) +resource_group_list_response = get_response_list_data_by_type(types.DS8K_RESOURCE_GROUP) +resource_group_list_response_json = get_response_list_json_by_type( + types.DS8K_RESOURCE_GROUP +) class TestRootResourceMixin(TestDS8KWithConnect): - - @httpretty.activate + @responses.activate def test_get_system(self): url = '/systems' - httpretty.register_uri(httpretty.GET, - self.domain + self.base_url + url, - body=system_list_response_json, - content_type='application/json', - status=200, - ) + responses.get( + self.domain + self.base_url + url, + body=system_list_response_json, + content_type='application/json', + status=HTTPStatus.OK.value, + ) sys = self.system.get_system() - self.assertIsInstance(sys, System) + assert isinstance(sys, System) sys_data = system_list_response['data']['systems'][0] self._assert_equal_between_dict_and_resource(sys_data, sys) @@ -71,32 +82,32 @@ def test_get_fb_lss(self): def test_get_ckd_lss(self): self._test_get_lss_by_type(types.DS8K_VOLUME_TYPE_CKD) - @httpretty.activate + @responses.activate def _test_get_lss_by_type(self, lss_type='fb'): url = '/lss' - httpretty.register_uri(httpretty.GET, - self.domain + self.base_url + url, - body=lss_list_response_json, - content_type='application/json', - status=200, - ) + params = {'type': lss_type} + + responses.get( + self.domain + self.base_url + url, + body=lss_list_response_json, + content_type='application/json', + status=HTTPStatus.OK.value, + match=[matchers.query_param_matcher(params)], + ) self.system.get_lss(lss_type=lss_type) - self.assertEqual([lss_type, ], - httpretty.last_request().querystring.get('type') - ) - @httpretty.activate + @responses.activate def test_get_lss_by_id(self): lss_id = '00' - url = '/lss/{}'.format(lss_id) - httpretty.register_uri(httpretty.GET, - self.domain + self.base_url + url, - body=lss_a_response_json, - content_type='application/json', - status=200, - ) + url = f'/lss/{lss_id}' + responses.get( + self.domain + self.base_url + url, + body=lss_a_response_json, + content_type='application/json', + status=HTTPStatus.OK.value, + ) lss = self.system.get_lss_by_id(lss_id) - self.assertIsInstance(lss, LSS) + assert isinstance(lss, LSS) lss_data = lss_a_response['data']['lss'][0] self._assert_equal_between_dict_and_resource(lss_data, lss) @@ -157,14 +168,14 @@ def test_get_encryption_group(self): def test_get_flashcopies(self): self._test_resource_list_by_route(types.DS8K_FLASHCOPY) - @nottest + @pytest.mark.skip def test_get_flashcopy(self): self._test_resource_by_route(types.DS8K_FLASHCOPY) def test_get_pprc(self): self._test_resource_list_by_route(types.DS8K_PPRC) - @nottest + @pytest.mark.skip def test_get_pprc_by_id(self): self._test_resource_by_route(types.DS8K_PPRC) @@ -174,60 +185,57 @@ def test_get_events(self): def test_get_event(self): self._test_resource_by_route(types.DS8K_EVENT) - @httpretty.activate + @responses.activate def test_delete_tserep_by_pool(self): pool_name = 'testpool_0' - url = '/pools/{}/tserep'.format(pool_name) - httpretty.register_uri(httpretty.DELETE, - self.domain + self.base_url + url, - body=action_response_json, - content_type='application/json', - status=204, - ) + url = f'/pools/{pool_name}/tserep' + responses.delete( + self.domain + self.base_url + url, + body=action_response_json, + content_type='application/json', + status=HTTPStatus.OK.value, + ) self.system.delete_tserep_by_pool(pool_name) - self.assertEqual(httpretty.DELETE, httpretty.last_request().method) + assert responses.calls[-1].request.method == responses.DELETE - @httpretty.activate + @responses.activate def test_delete_eserep_by_pool(self): pool_name = 'testpool_0' - url = '/pools/{}/eserep'.format(pool_name) - httpretty.register_uri( - httpretty.DELETE, - self.domain + self.base_url + url, - body=action_response_json, - content_type='application/json', - status=204, - ) + url = f'/pools/{pool_name}/eserep' + responses.delete( + self.domain + self.base_url + url, + body=action_response_json, + content_type='application/json', + status=HTTPStatus.OK.value, + ) self.system.delete_eserep_by_pool(pool_name) - self.assertEqual(httpretty.DELETE, httpretty.last_request().method) + assert responses.calls[-1].request.method == responses.DELETE - @httpretty.activate + @responses.activate def test_get_tserep_by_pool(self): pool_name = 'testpool_0' - url = '/pools/{}/tserep'.format(pool_name) - httpretty.register_uri( - httpretty.GET, - self.domain + self.base_url + url, - body=tserep_list_response_json, - content_type='application/json', - status=200, - ) + url = f'/pools/{pool_name}/tserep' + responses.get( + self.domain + self.base_url + url, + body=tserep_list_response_json, + content_type='application/json', + status=HTTPStatus.OK.value, + ) tserep = self.system.get_tserep_by_pool(pool_name) - self.assertIsInstance(tserep, TSERep) + assert isinstance(tserep, TSERep) - @httpretty.activate + @responses.activate def test_get_eserep_by_pool(self): pool_name = 'testpool_0' - url = '/pools/{}/eserep'.format(pool_name) - httpretty.register_uri( - httpretty.GET, - self.domain + self.base_url + url, - body=eserep_list_response_json, - content_type='application/json', - status=200, - ) + url = f'/pools/{pool_name}/eserep' + responses.get( + self.domain + self.base_url + url, + body=eserep_list_response_json, + content_type='application/json', + status=HTTPStatus.OK.value, + ) eserep = self.system.get_eserep_by_pool(pool_name) - self.assertIsInstance(eserep, ESERep) + assert isinstance(eserep, ESERep) def test_get_volumes(self): self._test_resource_list_by_route(types.DS8K_VOLUME) @@ -235,56 +243,53 @@ def test_get_volumes(self): def test_get_volume(self): self._test_resource_by_route(types.DS8K_VOLUME) - @httpretty.activate + @responses.activate def test_get_volumes_by_host(self): host_name = 'testhost' - url = '/hosts/{}/volumes'.format(host_name) - httpretty.register_uri(httpretty.GET, - self.domain + self.base_url + url, - body=volume_list_response_json, - content_type='application/json', - status=200, - ) + url = f'/hosts/{host_name}/volumes' + responses.get( + self.domain + self.base_url + url, + body=volume_list_response_json, + content_type='application/json', + status=HTTPStatus.OK.value, + ) vol_list = self.system.get_volumes_by_host(host_name=host_name) - self.assertIsInstance(vol_list, list) - self.assertIsInstance(vol_list[0], Volume) - self.assertEqual( - len(vol_list), - len(volume_list_response['data']['volumes']) - ) - - @httpretty.activate + assert isinstance(vol_list, list) + assert isinstance(vol_list[0], Volume) + assert len(vol_list) == len(volume_list_response['data']['volumes']) + + @responses.activate def test_get_volumes_by_lss(self): lss_id = '00' - url = '/lss/{}/volumes'.format(lss_id) - httpretty.register_uri(httpretty.GET, - self.domain + self.base_url + url, - body=volume_list_response_json, - content_type='application/json', - status=200, - ) + url = f'/lss/{lss_id}/volumes' + responses.get( + self.domain + self.base_url + url, + body=volume_list_response_json, + content_type='application/json', + status=HTTPStatus.OK.value, + ) vol_list = self.system.get_volumes_by_lss(lss_id=lss_id) - self.assertIsInstance(vol_list, list) - self.assertIsInstance(vol_list[0], Volume) - self.assertEqual( - len(vol_list), - len(volume_list_response['data']['volumes']) - ) - - @httpretty.activate + assert isinstance(vol_list, list) + assert isinstance(vol_list[0], Volume) + assert len(vol_list) == len(volume_list_response['data']['volumes']) + + @responses.activate def test_get_volumes_by_pool(self): pool_id = 'P0' - url = '/pools/{}/volumes'.format(pool_id) - httpretty.register_uri(httpretty.GET, - self.domain + self.base_url + url, - body=volume_list_response_json, - content_type='application/json', - status=200, - ) + url = f'/pools/{pool_id}/volumes' + responses.get( + self.domain + self.base_url + url, + body=volume_list_response_json, + content_type='application/json', + status=HTTPStatus.OK.value, + ) vol_list = self.system.get_volumes_by_pool(pool_id=pool_id) - self.assertIsInstance(vol_list, list) - self.assertIsInstance(vol_list[0], Volume) - self.assertEqual( - len(vol_list), - len(volume_list_response['data']['volumes']) - ) + assert isinstance(vol_list, list) + assert isinstance(vol_list[0], Volume) + assert len(vol_list) == len(volume_list_response['data']['volumes']) + + def test_get_resource_groups(self): + self._test_resource_list_by_route(types.DS8K_RESOURCE_GROUP) + + def test_get_resource_group(self): + self._test_resource_by_route(types.DS8K_RESOURCE_GROUP) diff --git a/pyds8k/test/test_resources/test_ds8k/test_system.py b/pyds8k/test/test_resources/test_ds8k/test_system.py old mode 100755 new mode 100644 index f2d1a91..7ccf209 --- a/pyds8k/test/test_resources/test_ds8k/test_system.py +++ b/pyds8k/test/test_resources/test_ds8k/test_system.py @@ -14,59 +14,64 @@ # limitations under the License. ############################################################################## -import httpretty +from http import HTTPStatus + +import pytest +import responses + +from pyds8k.exceptions import OperationNotAllowed from pyds8k.resources.ds8k.v1.common.types import DS8K_SYSTEM +from pyds8k.resources.ds8k.v1.systems import System, SystemManager +from pyds8k.test.data import ( + get_response_list_data_by_type, + get_response_list_json_by_type, +) + from .base import TestDS8KWithConnect -from pyds8k.resources.ds8k.v1.systems import System, \ - SystemManager -from pyds8k.test.data import get_response_list_json_by_type, \ - get_response_list_data_by_type -from pyds8k.exceptions import OperationNotAllowed system_list_response = get_response_list_data_by_type(DS8K_SYSTEM) system_list_response_json = get_response_list_json_by_type(DS8K_SYSTEM) class TestSystem(TestDS8KWithConnect): - def setUp(self): - super(TestSystem, self).setUp() + super().setUp() self.system = System(self.client, SystemManager(self.client)) - @httpretty.activate + @responses.activate def test_get_system(self): url = '/systems' - httpretty.register_uri(httpretty.GET, - self.domain + self.base_url + url, - body=system_list_response_json, - content_type='application/json', - status=200, - ) + responses.get( + self.domain + self.base_url + url, + body=system_list_response_json, + content_type='application/json', + status=HTTPStatus.OK.value, + ) sys = self.system.get_system() - self.assertIsInstance(sys, System) + assert isinstance(sys, System) sys_data = system_list_response['data']['systems'][0] self._assert_equal_between_dict_and_resource(sys_data, sys) - @httpretty.activate + @responses.activate def test_not_allowed_operations(self): url = '/systems' - httpretty.register_uri(httpretty.GET, - self.domain + self.base_url + url, - body=system_list_response_json, - content_type='application/json', - status=200, - ) + responses.get( + self.domain + self.base_url + url, + body=system_list_response_json, + content_type='application/json', + status=HTTPStatus.OK.value, + ) sys = self.system.get_system() - with self.assertRaises(OperationNotAllowed): + with pytest.raises(OperationNotAllowed): sys.put() - with self.assertRaises(OperationNotAllowed): + with pytest.raises(OperationNotAllowed): sys.patch() - with self.assertRaises(OperationNotAllowed): + with pytest.raises(OperationNotAllowed): sys.posta() - with self.assertRaises(OperationNotAllowed): + with pytest.raises(OperationNotAllowed): sys.delete() - with self.assertRaises(OperationNotAllowed): + with pytest.raises(OperationNotAllowed): sys.update() - with self.assertRaises(OperationNotAllowed) as cm: + with pytest.raises(OperationNotAllowed) as cm: sys.save() - self.assertEqual(System.__name__, cm.exception.resource_name) + assert System.__name__ == cm.value.resource_name diff --git a/pyds8k/test/test_resources/test_ds8k/test_tserep.py b/pyds8k/test/test_resources/test_ds8k/test_tserep.py old mode 100755 new mode 100644 index 3a579ad..533f598 --- a/pyds8k/test/test_resources/test_ds8k/test_tserep.py +++ b/pyds8k/test/test_resources/test_ds8k/test_tserep.py @@ -14,59 +14,60 @@ # limitations under the License. ############################################################################## -import httpretty -import json +from http import HTTPStatus + +import responses +from responses import matchers + +from pyds8k.dataParser.ds8k import RequestParser from pyds8k.resources.ds8k.v1.common.types import DS8K_TSEREP from pyds8k.resources.ds8k.v1.pools import Pool from pyds8k.resources.ds8k.v1.tserep import TSERep +from pyds8k.test.data import ( + action_response_json, + get_response_list_data_by_type, + get_response_list_json_by_type, +) + from .base import TestDS8KWithConnect -from pyds8k.test.data import get_response_list_json_by_type, \ - get_response_list_data_by_type -from pyds8k.test.data import action_response_json -from pyds8k.dataParser.ds8k import RequestParser tserep_list_response_json = get_response_list_json_by_type(DS8K_TSEREP) class TestTSERep(TestDS8KWithConnect): - def test_pool_field(self): - tserep = get_response_list_data_by_type( - DS8K_TSEREP - )['data'][DS8K_TSEREP][0] + tserep = get_response_list_data_by_type(DS8K_TSEREP)['data'][DS8K_TSEREP][0] pool_id = tserep['pool'][Pool.id_field] tse = TSERep(self.client, info=tserep) - self.assertEqual(tse.pool, pool_id) - self.assertEqual(tse.representation['pool'], pool_id) - self.assertIsInstance(tse._pool, Pool) - self.assertEqual(tse._pool.id, pool_id) + assert tse.pool == pool_id + assert tse.representation['pool'] == pool_id + assert isinstance(tse._pool, Pool) + assert tse._pool.id == pool_id - @httpretty.activate + @responses.activate def test_update(self): pool_id = 'P1' - url = '/pools/{}/tserep'.format(pool_id) + url = f'/pools/{pool_id}/tserep' + uri = f'{self.domain}{self.base_url}{url}' + cap = '10' threshold = '70' - httpretty.register_uri( - httpretty.GET, - self.domain + self.base_url + url, - body=tserep_list_response_json, - content_type='application/json', - status=200, - ) - - def _verify_request(request, uri, headers): - self.assertEqual(uri, self.domain + self.base_url + url) - resq = RequestParser({'cap': cap, 'threshold': threshold}) - self.assertEqual(json.loads(request.body), resq.get_request_data()) - return (200, headers, action_response_json) + responses.get( + uri, + body=tserep_list_response_json, + content_type='application/json', + status=HTTPStatus.OK.value, + ) - httpretty.register_uri(httpretty.PUT, - self.domain + self.base_url + url, - body=_verify_request, - content_type='application/json', - ) + resq = RequestParser({'cap': cap, 'threshold': threshold}) + responses.put( + uri, + status=HTTPStatus.OK, + body=action_response_json, + content_type='application/json', + match=[matchers.json_params_matcher(resq.get_request_data())], + ) tserep = self.system.get_tserep_by_pool(pool_id) tserep.cap = cap diff --git a/pyds8k/test/test_resources/test_ds8k/test_volume.py b/pyds8k/test/test_resources/test_ds8k/test_volume.py old mode 100755 new mode 100644 index c4ee419..812493c --- a/pyds8k/test/test_resources/test_ds8k/test_volume.py +++ b/pyds8k/test/test_resources/test_ds8k/test_volume.py @@ -15,9 +15,11 @@ ############################################################################## import json +from http import HTTPStatus -import httpretty -from nose.tools import nottest +import pytest +import responses +from responses import matchers from pyds8k.dataParser.ds8k import RequestParser from pyds8k.exceptions import FieldReadOnly @@ -29,86 +31,75 @@ from pyds8k.resources.ds8k.v1.lss import LSS from pyds8k.resources.ds8k.v1.pools import Pool from pyds8k.resources.ds8k.v1.pprc import PPRC -from pyds8k.resources.ds8k.v1.volumes import Volume, \ - VolumeManager -from pyds8k.test.data import get_response_json_by_type, \ - get_response_data_by_type, action_response_json, \ - action_response, create_volume_response_json, \ - create_volumes_response_json, \ - create_volumes_partial_failed_response_json, \ - create_volumes_partial_failed_response +from pyds8k.resources.ds8k.v1.volumes import Volume, VolumeManager +from pyds8k.test.data import ( + action_response, + action_response_json, + create_volume_response, + create_volume_response_json, + create_volumes_partial_failed_response, + create_volumes_partial_failed_response_json, + create_volumes_response_json, + get_response_data_by_type, + get_response_json_by_type, +) from pyds8k.test.test_resources.test_ds8k.base import TestDS8KWithConnect class TestVolume(TestDS8KWithConnect): - def setUp(self): - super(TestVolume, self).setUp() + super().setUp() self.volume = Volume(self.client, VolumeManager(self.client)) self.maxDiff = None def test_invalid_volume_type(self): - with self.assertRaises(ValueError) as cm: + with pytest.raises( + ValueError, match=INVALID_TYPE.format(', '.join(types.DS8K_VOLUME_TYPES)) + ): Volume(self.client, volume_type='fake') - self.assertEqual( - INVALID_TYPE.format(', '.join(types.DS8K_VOLUME_TYPES)), - str(cm.exception) - ) def test_related_resource_field(self): - volume_info = get_response_data_by_type( - DS8K_VOLUME - )['data'][DS8K_VOLUME][0] + volume_info = get_response_data_by_type(DS8K_VOLUME)['data'][DS8K_VOLUME][0] pool_id = volume_info['pool'][Pool.id_field] lss_id = volume_info['lss']['id'] volume = Volume(self.client, info=volume_info) - self.assertEqual(volume.pool, pool_id) - self.assertEqual(volume.representation['pool'], pool_id) - self.assertIsInstance(volume._pool, Pool) - self.assertEqual(volume._pool.id, pool_id) - self.assertEqual(volume.lss, lss_id) - self.assertEqual(volume.representation['lss'], lss_id) - self.assertIsInstance(volume._lss, LSS) - self.assertEqual(volume._lss.id, lss_id) + assert volume.pool == pool_id + assert volume.representation['pool'] == pool_id + assert isinstance(volume._pool, Pool) + assert volume._pool.id == pool_id + assert volume.lss == lss_id + assert volume.representation['lss'] == lss_id + assert isinstance(volume._lss, LSS) + assert volume._lss.id == lss_id volume.pool = 'new_pool' - self.assertEqual(volume.pool, 'new_pool') - self.assertEqual(volume.representation['pool'], 'new_pool') + assert volume.pool == 'new_pool' + assert volume.representation['pool'] == 'new_pool' - with self.assertRaises(FieldReadOnly): + with pytest.raises(FieldReadOnly): volume.lss = 'new_lss' def test_related_resources_collection(self): - hosts = [Host(self.client, resource_id='host{}'.format(i)) - for i in range(10) - ] + hosts = [Host(self.client, resource_id=f'host{i}') for i in range(10)] - flashcopies = [FlashCopy(self.client, resource_id='fc{}'.format(i)) - for i in range(10) - ] + flashcopies = [FlashCopy(self.client, resource_id=f'fc{i}') for i in range(10)] - pprc = [PPRC(self.client, resource_id='pprc{}'.format(i)) - for i in range(10) - ] + pprc = [PPRC(self.client, resource_id=f'pprc{i}') for i in range(10)] # init without related_resources collection - volume = Volume(self.client, info={ - 'name': 'a_0000', - 'link': { - 'rel': 'self', - 'href': '/api/volumes/a_0000' - }, - 'hosts': { - 'link': { - 'rel': 'self', - 'href': '/api/hosts' + volume = Volume( + self.client, + info={ + 'name': 'a_0000', + 'link': {'rel': 'self', 'href': '/api/volumes/a_0000'}, + 'hosts': { + 'link': {'rel': 'self', 'href': '/api/hosts'}, }, - } - } - ) + }, + ) for i in volume.related_resources_collection: - self.assertEqual('', volume.representation.get(i)) - self.assertFalse(hasattr(volume, i)) + assert volume.representation.get(i) == '' + assert not hasattr(volume, i) # loading related resources collection volume._start_updating() @@ -117,169 +108,161 @@ def test_related_resources_collection(self): setattr(volume, types.DS8K_PPRC, pprc) volume._stop_updating() for j, value in enumerate(volume.representation[types.DS8K_HOST]): - self.assertEqual(value, - getattr(hosts[j], hosts[j].id_field) - ) + assert value == getattr(hosts[j], hosts[j].id_field) for k, value in enumerate(volume.representation[types.DS8K_FLASHCOPY]): - self.assertEqual(value, - getattr(flashcopies[k], flashcopies[k].id_field) - ) + assert value == getattr(flashcopies[k], flashcopies[k].id_field) for vol, value in enumerate(volume.representation[types.DS8K_PPRC]): - self.assertEqual(value, - getattr(pprc[vol], pprc[vol].id_field) - ) + assert value == getattr(pprc[vol], pprc[vol].id_field) - @httpretty.activate + @responses.activate def test_delete_volume(self): response_a_json = get_response_json_by_type(DS8K_VOLUME) response_a = get_response_data_by_type(DS8K_VOLUME) - name = self._get_resource_id_from_resopnse(DS8K_VOLUME, response_a, - Volume.id_field - ) - url = '/volumes/{}'.format(name) - httpretty.register_uri(httpretty.GET, - self.domain + self.base_url + url, - body=response_a_json, - content_type='application/json', - status=200, - ) - httpretty.register_uri(httpretty.DELETE, - self.domain + self.base_url + url, - body=action_response_json, - content_type='application/json', - status=204, - ) + name = self._get_resource_id_from_resopnse( + DS8K_VOLUME, response_a, Volume.id_field + ) + url = f'/volumes/{name}' + responses.get( + self.domain + self.base_url + url, + body=response_a_json, + content_type='application/json', + status=HTTPStatus.OK.value, + ) + responses.delete( + self.domain + self.base_url + url, + body=action_response_json, + content_type='application/json', + status=HTTPStatus.OK.value, + ) + # Way 1 _ = self.system.delete_volume(name) - self.assertEqual(httpretty.DELETE, httpretty.last_request().method) + assert responses.calls[-1].request.method == responses.DELETE # self.assertEqual(resp1, action_response['server']) # Way 2 volume = self.system.get_volume(name) - self.assertIsInstance(volume, Volume) + assert isinstance(volume, Volume) resp2, _ = volume.delete() - self.assertEqual(resp2.status_code, 204) - self.assertEqual(httpretty.DELETE, httpretty.last_request().method) + assert resp2.status_code == HTTPStatus.OK.value + assert responses.calls[-1].request.method == responses.DELETE - @httpretty.activate + @responses.activate def test_update_volume_rename(self): volume_id = 'a_0000' - url = '/volumes/{}'.format(volume_id) - new_name = 'new_name' + url = f'/volumes/{volume_id}' + uri = f'{self.domain}{self.base_url}{url}' - def _verify_request(request, uri, headers): - self.assertEqual(uri, self.domain + self.base_url + url) + new_name = 'new_name' - resq = RequestParser({'name': new_name}) - self.assertEqual(json.loads(request.body), resq.get_request_data()) - return (200, headers, action_response_json) + resq = RequestParser({'name': new_name}) + responses.put( + uri, + status=HTTPStatus.OK, + body=action_response_json, + content_type='application/json', + match=[matchers.json_params_matcher(resq.get_request_data())], + ) - httpretty.register_uri(httpretty.PUT, - self.domain + self.base_url + url, - body=_verify_request, - content_type='application/json', - ) res = self.system.update_volume_rename(volume_id, new_name) - self.assertEqual(httpretty.PUT, httpretty.last_request().method) - self.assertEqual(res, action_response['server']) + assert responses.calls[-1].request.method == responses.PUT + assert res == action_response['server'] vol = self.system.one(DS8K_VOLUME, volume_id, rebuild_url=True) vol._add_details({'name': volume_id}) vol.name = new_name _, body = vol.save() - self.assertEqual(httpretty.PUT, httpretty.last_request().method) - self.assertEqual(body, action_response['server']) + assert responses.calls[-1].request.method == responses.PUT + assert body == action_response['server'] - @httpretty.activate + @responses.activate def test_update_volume_extend(self): volume_id = 'a_0000' - url = '/volumes/{}'.format(volume_id) + url = f'/volumes/{volume_id}' + uri = f'{self.domain}{self.base_url}{url}' + new_size = '100' captype = 'gib' - def _verify_request(request, uri, headers): - self.assertEqual(uri, self.domain + self.base_url + url) - - resq = RequestParser({'cap': new_size, 'captype': captype}) - self.assertEqual(json.loads(request.body), resq.get_request_data()) - return (200, headers, action_response_json) + resq = RequestParser({'cap': new_size, 'captype': captype}) + responses.put( + uri, + status=HTTPStatus.OK, + body=action_response_json, + content_type='application/json', + match=[matchers.json_params_matcher(resq.get_request_data())], + ) - httpretty.register_uri(httpretty.PUT, - self.domain + self.base_url + url, - body=_verify_request, - content_type='application/json', - ) res = self.system.update_volume_extend(volume_id, new_size, captype) - self.assertEqual(httpretty.PUT, httpretty.last_request().method) - self.assertEqual(res, action_response['server']) + assert responses.calls[-1].request.method == responses.PUT + assert res == action_response['server'] vol = self.system.one(DS8K_VOLUME, volume_id, rebuild_url=True) vol._add_details({'name': volume_id}) vol.cap = new_size vol.captype = captype _, body = vol.save() - self.assertEqual(httpretty.PUT, httpretty.last_request().method) - self.assertEqual(body, action_response['server']) + assert responses.calls[-1].request.method == responses.PUT + assert body == action_response['server'] - @httpretty.activate + @responses.activate def test_update_volume_move(self): volume_id = 'a_0000' - url = '/volumes/{}'.format(volume_id) - new_pool = 'new_pool' + url = f'/volumes/{volume_id}' + uri = f'{self.domain}{self.base_url}{url}' - def _verify_request(request, uri, headers): - self.assertEqual(uri, self.domain + self.base_url + url) + new_pool = 'new_pool' - resq = RequestParser({'pool': new_pool}) - self.assertEqual(json.loads(request.body), resq.get_request_data()) - return (200, headers, action_response_json) + resq = RequestParser({'pool': new_pool}) + responses.put( + uri, + status=HTTPStatus.OK, + body=action_response_json, + content_type='application/json', + match=[matchers.json_params_matcher(resq.get_request_data())], + ) - httpretty.register_uri(httpretty.PUT, - self.domain + self.base_url + url, - body=_verify_request, - content_type='application/json', - ) res = self.system.update_volume_move(volume_id, new_pool) - self.assertEqual(httpretty.PUT, httpretty.last_request().method) - self.assertEqual(res, action_response['server']) + assert responses.calls[-1].request.method == responses.PUT + assert res == action_response['server'] vol = self.system.one(DS8K_VOLUME, volume_id, rebuild_url=True) vol._add_details({'name': volume_id}) vol.pool = new_pool _, body = vol.save() - self.assertEqual(httpretty.PUT, httpretty.last_request().method) - self.assertEqual(body, action_response['server']) + assert responses.calls[-1].request.method == responses.PUT + assert body == action_response['server'] - @nottest - @httpretty.activate + @pytest.mark.skip + @responses.activate def test_update_volume_map(self): volume_id = 'a_0000' - url = '/volumes/{}'.format(volume_id) - host_name = 'host1' + url = f'/volumes/{volume_id}' + uri = f'{self.domain}{self.base_url}{url}' - def _verify_request(request, uri, headers): - self.assertEqual(uri, self.domain + self.base_url + url) + host_name = 'host1' - resq = RequestParser({'host': host_name}) - self.assertEqual(json.loads(request.body), resq.get_request_data()) - return (200, headers, action_response_json) + resq = RequestParser({'host': host_name}) + responses.put( + uri, + status=HTTPStatus.OK, + body=action_response_json, + content_type='application/json', + match=[matchers.json_params_matcher(resq.get_request_data())], + ) - httpretty.register_uri(httpretty.PUT, - self.domain + self.base_url + url, - body=_verify_request, - content_type='application/json', - ) res = self.system.update_volume_map(volume_id, host_name) - self.assertEqual(httpretty.PUT, httpretty.last_request().method) - self.assertEqual(res, action_response['server']) + assert responses.calls[-1].method == responses.PUT + assert res == action_response['server'] vol = self.system.one(DS8K_VOLUME, volume_id, rebuild_url=True) vol._add_details({'name': volume_id}) vol.host = host_name - @httpretty.activate + @responses.activate def test_create_volume(self): url = '/volumes' + uri = f'{self.domain}{self.base_url}{url}' name = 'volume1' cap = '10' @@ -289,25 +272,25 @@ def test_create_volume(self): tp = 'ese' lss = '00' - def _verify_request(request, uri, headers): - self.assertEqual(uri, self.domain + self.base_url + url) - - req = RequestParser({'name': name, 'cap': cap, - 'pool': pool, 'stgtype': stgtype, - 'captype': captype, 'lss': lss, 'tp': tp, - } - ) - self.assertDictContainsSubset( - req.get_request_data().get('request').get('params'), - json.loads(request.body).get('request').get('params'), - ) - return (201, headers, create_volume_response_json) + req = RequestParser( + { + 'name': name, + 'cap': cap, + 'captype': captype, + 'stgtype': stgtype, + 'pool': pool, + 'lss': lss, + 'tp': tp, + } + ) + responses.post( + uri, + status=HTTPStatus.CREATED, + body=create_volume_response_json, + content_type='application/json', + match=[matchers.json_params_matcher(req.get_request_data())], + ) - httpretty.register_uri(httpretty.POST, - self.domain + self.base_url + url, - body=_verify_request, - content_type='application/json', - ) # Way 1 resp1 = self.system.create_volume( name=name, @@ -316,42 +299,55 @@ def _verify_request(request, uri, headers): stgtype=stgtype, captype=captype, lss=lss, - tp=tp + tp=tp, ) - self.assertEqual(httpretty.POST, httpretty.last_request().method) - self.assertIsInstance(resp1[0], Volume) + assert responses.calls[-1].request.method == responses.POST + assert isinstance(resp1[0], Volume) # Way 2 volume = self.system.all(DS8K_VOLUME, rebuild_url=True) - new_vol2 = volume.create(name=name, cap=cap, - pool=pool, stgtype=stgtype, - captype=captype, lss=lss, tp=tp, ) + new_vol2 = volume.create( + name=name, + cap=cap, + pool=pool, + stgtype=stgtype, + captype=captype, + lss=lss, + tp=tp, + ) resp2, data2 = new_vol2.posta() - self.assertEqual(httpretty.POST, httpretty.last_request().method) - self.assertIsInstance(data2[0], Volume) - self.assertEqual(resp2.status_code, 201) + assert responses.calls[-1].request.method == responses.POST + assert isinstance(data2[0], Volume) + assert resp2.status_code == HTTPStatus.CREATED # Way 3 volume = self.system.all(DS8K_VOLUME, rebuild_url=True) - new_vol3 = volume.create(name=name, cap=cap, - pool=pool, stgtype=stgtype, - captype=captype, lss=lss, tp=tp, ) + new_vol3 = volume.create( + name=name, + cap=cap, + pool=pool, + stgtype=stgtype, + captype=captype, + lss=lss, + tp=tp, + ) resp3, data3 = new_vol3.save() - self.assertEqual(httpretty.POST, httpretty.last_request().method) - self.assertIsInstance(data3[0], Volume) - self.assertEqual(resp3.status_code, 201) + assert responses.calls[-1].request.method == responses.POST + assert isinstance(data3[0], Volume) + assert resp3.status_code == HTTPStatus.CREATED # Way 4 # Don't init a resource instance by yourself when create new. # use .create() instead. - @httpretty.activate + @responses.activate def test_create_volumes(self): url = '/volumes' + uri = f'{self.domain}{self.base_url}{url}' name = 'volume1' quantity = '10' - namecol = ['volume{}'.format(i) for i in range(10)] + namecol = [f'volume{i}' for i in range(10)] cap = '10' pool = 'testpool_0' stgtype = types.DS8K_VOLUME_TYPE_FB @@ -359,75 +355,74 @@ def test_create_volumes(self): tp = 'ese' lss = '00' - def _verify_request1(request, uri, headers): - self.assertEqual(uri, self.domain + self.base_url + url) - - resq = RequestParser({'name': name, 'cap': cap, - 'pool': pool, 'stgtype': stgtype, - 'captype': captype, 'lss': lss, 'tp': tp, - 'quantity': quantity - }) - self.assertEqual(json.loads(request.body), resq.get_request_data()) - return (201, headers, create_volumes_response_json) - - def _verify_request2(request, uri, headers): - self.assertEqual(uri, self.domain + self.base_url + url) - - resq = RequestParser({'namecol': namecol, 'cap': cap, - 'pool': pool, 'stgtype': stgtype, - 'name': '', 'quantity': '', - 'captype': captype, 'lss': lss, 'tp': tp, - }) - self.assertEqual(json.loads(request.body), resq.get_request_data()) - return (201, headers, create_volumes_response_json) - - httpretty.register_uri( - httpretty.POST, - self.domain + self.base_url + url, - responses=[ - httpretty.Response(body=_verify_request1, - content_type='application/json', - ), - httpretty.Response(body=_verify_request2, - content_type='application/json', - ), - ] + resq = RequestParser( + { + 'name': name, + 'cap': cap, + 'pool': pool, + 'stgtype': stgtype, + 'captype': captype, + 'lss': lss, + 'tp': tp, + 'quantity': quantity, + } + ) + responses.post( + uri, + status=HTTPStatus.CREATED, + body=create_volumes_response_json, + content_type='application/json', + match=[matchers.json_params_matcher(resq.get_request_data())], + ) + + resq = RequestParser( + { + 'namecol': namecol, + 'cap': cap, + 'captype': captype, + 'stgtype': stgtype, + 'pool': pool, + 'lss': lss, + 'tp': tp, + } + ) + responses.post( + uri, + status=HTTPStatus.CREATED, + body=create_volumes_response_json, + content_type='application/json', + match=[matchers.json_params_matcher(resq.get_request_data())], ) resp1 = self.system.create_volumes_with_same_prefix( - name, cap, pool, + name, + cap, + pool, quantity=quantity, stgtype=stgtype, captype=captype, lss=lss, - tp=tp + tp=tp, ) - self.assertEqual(httpretty.POST, httpretty.last_request().method) - self.assertIsInstance(resp1[0], Volume) + assert responses.calls[-1].request.method == responses.POST + assert isinstance(resp1[0], Volume) resp2 = self.system.create_volumes_without_same_prefix( - namecol, cap, pool, - stgtype=stgtype, - captype=captype, - lss=lss, - tp=tp + namecol, cap, pool, stgtype=stgtype, captype=captype, lss=lss, tp=tp ) - self.assertEqual(httpretty.POST, httpretty.last_request().method) - self.assertIsInstance(resp2[0], Volume) + assert responses.calls[-1].request.method == responses.POST + assert isinstance(resp2[0], Volume) resp3 = self.system.create_volumes_with_names( - namecol, cap, pool, - stgtype=stgtype, - captype=captype, - lss=lss, - tp=tp + namecol, cap, pool, stgtype=stgtype, captype=captype, lss=lss, tp=tp ) - self.assertEqual(httpretty.POST, httpretty.last_request().method) - self.assertIsInstance(resp3[0], Volume) + assert responses.calls[-1].request.method == responses.POST + assert isinstance(resp3[0], Volume) - @httpretty.activate + @responses.activate def test_create_volumes_partial_failed(self): url = '/volumes' + uri = f'{self.domain}{self.base_url}{url}' name = 'volume1' quantity = '10' @@ -438,58 +433,252 @@ def test_create_volumes_partial_failed(self): tp = 'ese' lss = '00' - def _verify_request1(request, uri, headers): - self.assertEqual(uri, self.domain + self.base_url + url) - - resq = RequestParser({'name': name, 'cap': cap, - 'pool': pool, 'stgtype': stgtype, - 'captype': captype, 'lss': lss, 'tp': tp, - 'quantity': quantity - }) - self.assertEqual(json.loads(request.body), resq.get_request_data()) - return (201, headers, create_volumes_partial_failed_response_json) - - httpretty.register_uri( - httpretty.POST, - self.domain + self.base_url + url, - responses=[ - httpretty.Response(body=_verify_request1, - content_type='application/json', - ), - ] + resq = RequestParser( + { + 'name': name, + 'cap': cap, + 'pool': pool, + 'stgtype': stgtype, + 'captype': captype, + 'lss': lss, + 'tp': tp, + 'quantity': quantity, + } + ) + responses.post( + uri, + status=HTTPStatus.CREATED, + body=create_volumes_partial_failed_response_json, + content_type='application/json', + match=[matchers.json_params_matcher(resq.get_request_data())], ) resp1 = self.system.create_volumes_with_same_prefix( - name, cap, pool, + name, + cap, + pool, quantity=quantity, stgtype=stgtype, captype=captype, lss=lss, - tp=tp + tp=tp, ) - self.assertEqual(httpretty.POST, httpretty.last_request().method) + assert responses.calls[-1].request.method == responses.POST # return 1 created volume and 1 error status - self.assertIsInstance(resp1[0], Volume) - self.assertIsInstance(resp1[1], dict) - self.assertEqual( - resp1[1], - create_volumes_partial_failed_response.get('responses')[1].get( - 'server' - ) - ) + assert isinstance(resp1[0], Volume) + assert isinstance(resp1[1], dict) + assert resp1[1] == create_volumes_partial_failed_response.get('responses')[ + 1 + ].get('server') def test_create_volume_type_error(self): - with self.assertRaises(ValueError): + with pytest.raises( + ValueError, match=INVALID_TYPE.format(', '.join(types.DS8K_VOLUME_TYPES)) + ): + self.system.create_volume('name', '10', 'testpool_0', 'fake_stgtype') + with pytest.raises( + ValueError, match=INVALID_TYPE.format(', '.join(types.DS8K_CAPTYPES)) + ): self.system.create_volume( - 'name', '10', 'testpool_0', 'fake_stgtype' + 'name', + '10', + 'testpool_0', + types.DS8K_VOLUME_TYPE_FB, + captype='fake_captype', ) - with self.assertRaises(ValueError): + with pytest.raises( + ValueError, match=INVALID_TYPE.format(', '.join(types.DS8K_TPS)) + ): self.system.create_volume( - 'name', '10', 'testpool_0', types.DS8K_VOLUME_TYPE_FB, - captype='fake_captype' - ) - with self.assertRaises(ValueError): - self.system.create_volume( - 'name', '10', 'testpool_0', types.DS8K_VOLUME_TYPE_FB, - tp='fake_tp' + 'name', '10', 'testpool_0', types.DS8K_VOLUME_TYPE_FB, tp='fake_tp' ) + + @responses.activate + def test_create_volume_with_volid(self): + url = '/volumes' + uri = f'{self.domain}{self.base_url}{url}' + + name = 'volume1' + cap = '10' + pool = 'testpool_0' + stgtype = types.DS8K_VOLUME_TYPE_FB + captype = 'gib' + tp = 'ese' + lss = '00' + _id = '0000' + + req = RequestParser( + { + 'name': name, + 'cap': cap, + 'pool': pool, + 'stgtype': stgtype, + 'captype': captype, + 'lss': lss, + 'tp': tp, + 'id': _id, + } + ) + + prepared_response = create_volume_response.copy() + prepared_response['data']['volumes'][0]['id'] = _id + prepared_href = f"{self.domain}{self.base_url}{url}/{_id}" + prepared_response['link']['href'] = prepared_href + responses.post( + uri, + status=HTTPStatus.CREATED, + body=json.dumps(prepared_response), + content_type='application/json', + match=[matchers.json_params_matcher(req.get_request_data())], + ) + + # Way 1 + resp1 = self.system.create_volume( + name=name, + cap=cap, + pool=pool, + stgtype=stgtype, + captype=captype, + lss=lss, + tp=tp, + id=_id, + ) + assert responses.calls[-1].request.method == responses.POST + assert isinstance(resp1[0], Volume) + + # Way 2 + volume = self.system.all(DS8K_VOLUME, rebuild_url=True) + new_vol2 = volume.create( + name=name, + cap=cap, + pool=pool, + stgtype=stgtype, + captype=captype, + lss=lss, + tp=tp, + id=_id, + ) + resp2, data2 = new_vol2.posta() + assert responses.calls[-1].request.method == responses.POST + assert isinstance(data2[0], Volume) + assert resp2.status_code == HTTPStatus.CREATED + + # Way 3 + volume = self.system.all(DS8K_VOLUME, rebuild_url=True) + new_vol3 = volume.create( + name=name, + cap=cap, + pool=pool, + stgtype=stgtype, + captype=captype, + lss=lss, + tp=tp, + id=_id, + ) + resp3, data3 = new_vol3.save() + assert responses.calls[-1].request.method == responses.POST + assert isinstance(data3[0], Volume) + assert resp3.status_code == HTTPStatus.CREATED + + # Way 4 + # Don't init a resource instance by yourself when create new. + # use .create() instead. + + @responses.activate + def test_create_volumes_with_volids(self): + url = '/volumes' + uri = f'{self.domain}{self.base_url}{url}' + + name_col = ['volume0000'] + cap = '10' + pool = 'testpool_0' + stgtype = types.DS8K_VOLUME_TYPE_FB + captype = 'gib' + tp = 'ese' + lss = '00' + ids = ['0000'] + + req = RequestParser( + { + 'name_col': name_col, + 'cap': cap, + 'captype': captype, + 'stgtype': stgtype, + 'pool': pool, + 'lss': lss, + 'tp': tp, + 'ids': ids, + } + ) + + # CAVEAT: The REST api uses namecol, not name_col. + prepared_request = req.get_request_data() + prepared_request['request']['params']['namecol'] = prepared_request['request'][ + 'params' + ]['name_col'] + del prepared_request['request']['params']['name_col'] + + prepared_response = create_volume_response.copy() + prepared_response['data']['volumes'][0]['id'] = ids[0] + prepared_href = f"{self.domain}{self.base_url}{url}/{ids[0]}" + prepared_response['link']['href'] = prepared_href + + responses.post( + uri, + status=HTTPStatus.CREATED, + body=json.dumps(prepared_response), + content_type='application/json', + match=[matchers.json_params_matcher(prepared_request)], + ) + + resp1 = self.system.create_volumes( + name_col=name_col, + cap=cap, + pool=pool, + stgtype=stgtype, + captype=captype, + lss=lss, + tp=tp, + ids=ids, + ) + assert responses.calls[-1].request.method == responses.POST + assert isinstance(resp1[0], Volume) + + @responses.activate + def test_create_alias_volumes(self): + url = '/volumes' + uri = f'{self.domain}{self.base_url}{url}' + + vol_id = '00FF' + quantity = 2 + alias_create_order = 'decrement' + ckd_base_ids = ['0000', '0001'] + + req = RequestParser( + { + 'id': vol_id, + 'quantity': quantity, + 'alias': 'true', # CAVEAT: The API always sends true. + 'alias_create_order': alias_create_order, + 'ckd_base_ids': ckd_base_ids, + } + ) + + prepared_response = create_volume_response.copy() + prepared_response['data']['volumes'][0]['id'] = vol_id + prepared_href = f"{self.domain}{self.base_url}{url}/{vol_id}" + prepared_response['link']['href'] = prepared_href + + responses.post( + uri, + status=HTTPStatus.CREATED, + body=json.dumps(prepared_response), + content_type='application/json', + match=[matchers.json_params_matcher(req.get_request_data())], + ) + + resp1 = self.system.create_alias_volumes( + vol_id, ckd_base_ids, quantity=quantity + ) + assert responses.calls[-1].request.method == responses.POST + assert isinstance(resp1[0], Volume) diff --git a/pyds8k/test/test_utils.py b/pyds8k/test/test_utils.py old mode 100755 new mode 100644 index 1aae58e..2cf7ba5 --- a/pyds8k/test/test_utils.py +++ b/pyds8k/test/test_utils.py @@ -14,15 +14,19 @@ # limitations under the License. ############################################################################## -from . import base -from nose.tools import nottest +import pytest + from pyds8k import utils +from pyds8k.dataParser.ds8k import RequestParser, ResponseParser + +from . import base +NUM_CONFIG_DICT_KEYS = 5 -class TestUtils(base.TestCaseWithoutConnect): +class TestUtils(base.TestCaseWithoutConnect): def test_get_subclasses(self): - class A(object): + class A: pass class B(A): @@ -37,10 +41,10 @@ class D(B): class E(C): pass - self.assertIn(B, utils.get_subclasses(A)) - self.assertIn(C, utils.get_subclasses(A)) - self.assertIn(D, utils.get_subclasses(A)) - self.assertIn(E, utils.get_subclasses(A)) + assert B in utils.get_subclasses(A) + assert C in utils.get_subclasses(A) + assert D in utils.get_subclasses(A) + assert E in utils.get_subclasses(A) def test_is_absolute_url(self): url1 = 'http://www.example.com/test' @@ -52,50 +56,47 @@ def test_is_absolute_url(self): url7 = 'localhost/test' url8 = '/test' for url in (url1, url2, url3, url4, url5, url6, url7): - self.assertTrue(utils.is_absolute_url(url)) - self.assertFalse(utils.is_absolute_url(url8)) + assert utils.is_absolute_url(url) + assert not utils.is_absolute_url(url8) def test_get_request_parser_class(self): - from pyds8k.dataParser.ds8k import RequestParser - self.assertEqual(RequestParser, utils.get_request_parser_class('ds8k')) + assert RequestParser == utils.get_request_parser_class('ds8k') def test_get_response_parser_class(self): - from pyds8k.dataParser.ds8k import ResponseParser - self.assertEqual(ResponseParser, - utils.get_response_parser_class('ds8k')) + assert ResponseParser == utils.get_response_parser_class('ds8k') # def test_get_default_service_type(self): # self.assertEqual('ds8k', utils.get_default_service_type()) - @nottest + @pytest.mark.skip def test_get_config_settings(self): settings_dict = utils.get_config_settings() - self.assertEqual(5, len(list(settings_dict.keys()))) - self.assertIsNotNone(settings_dict.get('debug')) - self.assertIsNotNone(settings_dict.get('log_path')) - self.assertIsNotNone(settings_dict.get('default_service_type')) - self.assertIsNotNone(settings_dict.get('runtime_service_type')) + assert len(list(settings_dict.keys())) == NUM_CONFIG_DICT_KEYS + assert settings_dict.get('debug') is not None + assert settings_dict.get('log_path') is not None + assert settings_dict.get('default_service_type') is not None + assert settings_dict.get('runtime_service_type') is not None - @nottest + @pytest.mark.skip def test_get_config_all_items(self): config_dict = utils.get_config_all_items() - self.assertEqual(5, len(list(config_dict.keys()))) - self.assertIsNotNone(config_dict.get('debug')) - self.assertIsNotNone(config_dict.get('log_path')) - self.assertIsNotNone(config_dict.get('default_service_type')) - self.assertIsNotNone(config_dict.get('runtime_service_type')) + assert len(list(config_dict.keys())) == NUM_CONFIG_DICT_KEYS + assert config_dict.get('debug') is not None + assert config_dict.get('log_path') is not None + assert config_dict.get('default_service_type') is not None + assert config_dict.get('runtime_service_type') is not None - @nottest + @pytest.mark.skip def test_get_config_all(self): config_dict = utils.get_config_all() - self.assertEqual(1, len(list(config_dict.keys()))) + assert len(list(config_dict.keys())) == 1 settings_dict = config_dict.get('settings') - self.assertIsNotNone(settings_dict) - self.assertEqual(5, len(list(settings_dict.keys()))) - self.assertIsNotNone(settings_dict.get('debug')) - self.assertIsNotNone(settings_dict.get('log_path')) - self.assertIsNotNone(settings_dict.get('default_service_type')) - self.assertIsNotNone(settings_dict.get('runtime_service_type')) + assert settings_dict is not None + assert len(list(settings_dict.keys())) == NUM_CONFIG_DICT_KEYS + assert settings_dict.get('debug') is not None + assert settings_dict.get('log_path') is not None + assert settings_dict.get('default_service_type') is not None + assert settings_dict.get('runtime_service_type') is not None ''' @@ -110,26 +111,17 @@ def tearDown(self): utils.set_runtime_service_type(self.RUNTIME_SERVICE_TYPE) def test_get_runtime_service_type(self): - self.assertEqual( - self.RUNTIME_SERVICE_TYPE, - utils.get_runtime_service_type() - ) + assert utils.get_runtime_service_type() == self.RUNTIME_SERVICE_TYPE def test_set_runtime_service_type(self): utils.set_runtime_service_type('test') - self.assertEqual('test', utils.get_runtime_service_type()) + assert utils.get_runtime_service_type() == 'test' def test_get_service_type(self): if utils.get_runtime_service_type(): - self.assertEqual( - self.RUNTIME_SERVICE_TYPE, - utils.get_service_type() - ) + assert utils.get_service_type() == self.RUNTIME_SERVICE_TYPE else: - self.assertEqual( - utils.get_default_service_type(), - utils.get_service_type() - ) + assert utils.get_default_service_type() == utils.get_service_type() utils.set_runtime_service_type('test') - self.assertEqual('test', utils.get_service_type()) + assert utils.get_service_type() == 'test' ''' diff --git a/pyds8k/test/utils.py b/pyds8k/test/utils.py index 68ad3d5..0bdb4e9 100644 --- a/pyds8k/test/utils.py +++ b/pyds8k/test/utils.py @@ -14,9 +14,10 @@ # limitations under the License. ############################################################################## -from io import StringIO import sys from contextlib import contextmanager +from io import StringIO +from pathlib import Path @contextmanager @@ -26,3 +27,37 @@ def capture_sys_stderr_and_return(command, *args, **kwargs): sys.stderr.seek(0) yield sys.stderr.read() sys.stderr = err + + +def get_mocks(path): + """Get a set of mock file names. + + Args: + path (str): The file path + + Returns: + set: A set containing mock names. + """ + _path = Path(path).parent.absolute() + return { + resource.stem + for resource in _path.iterdir() + if resource.is_file() and not resource.stem.startswith('__init__') + } + + +def get_dir_mocks(path): + """Get a list of mock directory names. + + Args: + path (str): The file path + + Returns: + list: A list of directory names. + """ + _path = Path(path).parent.absolute() + return [ + resource.name + for resource in _path.iterdir() + if resource.is_dir() and resource.name != '__pycache__' + ] diff --git a/pyds8k/utils.py b/pyds8k/utils.py old mode 100755 new mode 100644 index 4a247d8..b676cf3 --- a/pyds8k/utils.py +++ b/pyds8k/utils.py @@ -14,24 +14,20 @@ # limitations under the License. ############################################################################## -import os -import time import configparser +import time from importlib import import_module -from pyds8k.messages import GET_CONFIG_SETTINGS_IOERROR, \ - GET_CONFIG_SETTINGS_ERROR +from logging import getLogger +from pathlib import Path + +from pyds8k import PYDS8K_DEFAULT_LOGGER +from pyds8k.messages import GET_CONFIG_SETTINGS_ERROR, GET_CONFIG_SETTINGS_IOERROR -_PATH = os.path.abspath(os.path.dirname(__file__)) +PATH = Path(__file__).parent.resolve() CONFIG_FILE_NAME = 'config.ini' -CONFIG_FILE_PATH = os.path.join(_PATH, CONFIG_FILE_NAME) +CONFIG_FILE_PATH = PATH.joinpath(CONFIG_FILE_NAME) logger = None -# HTTP STATUS CODES -HTTP200 = 200 -HTTP204 = 204 -HTTP404 = 404 -HTTP500 = 500 - # HTTP METHODS POSTA = 'POST-to-Append' POST = 'POST' @@ -42,8 +38,6 @@ def _get_logger(): - from logging import getLogger - from pyds8k import PYDS8K_DEFAULT_LOGGER global logger if not logger: logger = getLogger(PYDS8K_DEFAULT_LOGGER) @@ -58,58 +52,50 @@ def get_subclasses(cls): def get_config_settings(category="settings"): - result_dict = dict() + result_dict = {} try: config = configparser.ConfigParser() config.read(CONFIG_FILE_PATH) - for setting, value in config.items(category): - result_dict[setting] = value - except IOError as e: - _get_logger().debug(GET_CONFIG_SETTINGS_IOERROR.format( - CONFIG_FILE_PATH, - str(e) - ) + result_dict = {config.items(category)} + except OSError as e: + _get_logger().debug( + GET_CONFIG_SETTINGS_IOERROR.format(CONFIG_FILE_PATH, str(e)) ) - except Exception as e: + except configparser.Error as e: _get_logger().error(GET_CONFIG_SETTINGS_ERROR.format(str(e))) return result_dict def get_config_all(): - result_dict = dict() + result_dict = {} try: config = configparser.ConfigParser() config.read(CONFIG_FILE_PATH) for section in config.sections(): - result_dict[section] = dict() + result_dict[section] = {} for setting, value in config.items(section): result_dict[section][setting] = value - except IOError as e: - _get_logger().debug(GET_CONFIG_SETTINGS_IOERROR.format( - CONFIG_FILE_PATH, - str(e) - ) + except OSError as e: + _get_logger().debug( + GET_CONFIG_SETTINGS_IOERROR.format(CONFIG_FILE_PATH, str(e)) ) - except Exception as e: + except configparser.Error as e: _get_logger().error(GET_CONFIG_SETTINGS_ERROR.format(str(e))) return result_dict def get_config_all_items(): - result_dict = dict() + result_dict = {} try: config = configparser.ConfigParser() config.read(CONFIG_FILE_PATH) for section in config.sections(): - for setting, value in config.items(section): - result_dict[setting] = value - except IOError as e: - _get_logger().debug(GET_CONFIG_SETTINGS_IOERROR.format( - CONFIG_FILE_PATH, - str(e) - ) + result_dict = {config.items(section)} + except OSError as e: + _get_logger().debug( + GET_CONFIG_SETTINGS_IOERROR.format(CONFIG_FILE_PATH, str(e)) ) - except Exception as e: + except configparser.Error as e: _get_logger().error(GET_CONFIG_SETTINGS_ERROR.format(str(e))) return result_dict @@ -125,7 +111,7 @@ def set_config_by_name(name, value): config.read(CONFIG_FILE_PATH) config.set('settings', name, value) - with open(CONFIG_FILE_PATH, 'wb') as config_file: + with CONFIG_FILE_PATH.open('wb') as config_file: config.write(config_file) @@ -153,15 +139,13 @@ def set_runtime_service_type(service_type): def get_request_parser_class(service_type): prefix = service_type - Parser = import_module('{0}.dataParser.{1}'.format(__package__, prefix) - ) + Parser = import_module(f'{__package__}.dataParser.{prefix}') # noqa: N806 return Parser.RequestParser def get_response_parser_class(service_type): prefix = service_type - Parser = import_module('{0}.dataParser.{1}'.format(__package__, prefix) - ) + Parser = import_module(f'{__package__}.dataParser.{prefix}') # noqa: N806 return Parser.ResponseParser @@ -171,12 +155,10 @@ def inner(self, *args, **kwargs): result = func(self, *args, **kwargs) end = time.time() _get_logger().info( - "Successfully called method '{}' in {} seconds".format( - func.__name__, - round(end - start, 2) - ) + f"Successfully called method '{func.__name__}' in {round(end - start, 2)} seconds" ) return result + return inner @@ -187,19 +169,14 @@ def inner(self, *args, **kwargs): end = time.time() sec = round(end - start, 2) if not res: - _get_logger().info( - "Successfully got 0 resources in {} seconds".format(sec) - ) + _get_logger().info(f"Successfully got 0 resources in {sec} seconds") return [] _get_logger().info( - "Successfully got {} resources in {} seconds, \ -{} seconds per 100 instances.".format( - len(res), - sec, - round(sec / len(res) * 100, 2) - ) + f"Successfully got {len(res)} resources in {sec} seconds, \ +{round(sec / len(res) * 100, 2)} seconds per 100 instances." ) return res + return inner @@ -207,19 +184,19 @@ def dictionarize(func): def inner(self, *args, **kwargs): res_obj = func(self, *args, **kwargs) if not isinstance(res_obj, list): - res_obj = [res_obj, ] - coverted = [] - for res in res_obj: - coverted.append(res.representation) - return coverted + res_obj = [ + res_obj, + ] + + return [res.representation for res in res_obj] + return inner def is_absolute_url(url): if url.startswith('/'): return False - elif '//' in url: + if '//' in url: return True # Don't verify the URI's validation here. - else: - return True + return True diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4ea9553 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,159 @@ +[project] +name = "pyds8k" +description = "DS8000 Python Client" +dynamic = ["version"] +readme = "README.md" +license-files = ["LICENSE"] +requires-python = ">=3.9" +authors = [{ name = "Zhang Wu", email = "shwzhang@cn.ibm.com" }] +maintainers = [ + { name = "Randy Blea", email = "blead@us.ibm.com" }, + { name = "NjM3MjY5NzAgNzA3MzA3", email = "102704081+NjM3MjY5NzAgNzA3MzA3@users.noreply.github.com" }, +] +keywords = ["IBM", "DS8000 Storage"] +classifiers = [ + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Developers', + 'Intended Audience :: System Administrators', + 'Environment :: Console', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', + 'Topic :: Software Development :: Libraries :: Python Modules', +] +dependencies = ["requests>=2.32.4"] +[dependency-groups] +dev = ["sphinx>=2.2.1", "ruff>=0.11.6"] +test = [ + "pytest>=7.0.0", + "pytest-cov>=3.0.0", + "responses>=0.24.0", + "tox>=4.22.0", + "tzdata>=2025.2", + "tzlocal>=5.3.1", +] + +[build-system] +requires = ["setuptools >= 77.0.0"] +build-backend = "setuptools.build_meta" + +[project.urls] +Documentation = "https://pyds8k.readthedocs.io/en/latest" +Homepage = "https://github.com/IBM/pyds8k" +Issues = "https://github.com/IBM/pyds8k/issues" +Repository = "https://github.com/IBM/pyds8k.git" + +[tool.setuptools.dynamic] +version = { attr = "pyds8k.version" } # Set in pyds8k/__init__.py + +[tool.setuptools.packages.find] +exclude = ["cover", "images"] + +[tool.ruff] +# Same as Black. +line-length = 88 +indent-width = 4 + +# Assume Python 3.9 +target-version = "py39" + +[tool.ruff.lint] +extend-select = [ + "A", + "B", + "BLE", + "C4", + "C90", + "DTZ", + "EM", + "EXE", + "F", + "FLY", + "FURB", + "I", + "ICN", + "INP", + "INT", + "ISC", + "LOG", + "N", + "PERF", + "PGH", + "PIE", + "PL", + "PT", + "PTH", + "PYI", + "RET", + "RSE", + "RUF", + "SIM", + "SLOT", + "T2", + "T10", + "TID", + "TRY", + "UP", + "W", +] + +ignore = [ + "PLR0913", # Too many arguments in function definition + "RUF012", # FIXME: Mutable class attributes should be annotated with `typing.ClassVar` +] + +# Allow fix for all enabled rules (when `--fix`) is provided. +fixable = ["ALL"] +unfixable = [] + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[tool.ruff.lint.per-file-ignores] +"pyds8k/example.py" = [ + "T201", # allow print +] +"pyds8k/exceptions.py" = [ + "N818", # Allow custom exceptions that omit the Error suffix +] +"pyds8k/utils.py" = [ + "PLW0603", # FIXME: Using the global statement to update logger is discouraged +] +"pyds8k/client/exceptions.py" = [ + "N818", # Allow custom exceptions that omit the Error suffix +] +"pyds8k/httpclient.py" = ["PLR0912", "PLR0915"] +"pyds8k/test/test_client/test_ds8k/test_sc_client.py" = ["TRY002"] +"pyds8k/test/test_resources/test_ds8k/base.py" = ["TRY002"] +"pyds8k/test/test_resources/test_ds8k/test_resource_group.py" = ["PLR0915"] + + +[tool.ruff.format] +# Project has mixed strings +quote-style = "preserve" + +# Like Black, indent with spaces, rather than tabs. +indent-style = "space" + +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false + +# Like Black, automatically detect the appropriate line ending. +line-ending = "auto" + +# Enable auto-formatting of code examples in docstrings. Markdown, +# reStructuredText code/literal blocks and doctests are all supported. +# +# This is currently disabled by default, but it is planned for this +# to be opt-out in the future. +docstring-code-format = false + +# Set the line length limit used when formatting code snippets in +# docstrings. +# +# This only has an effect when the `docstring-code-format` setting is +# enabled. +docstring-code-line-length = "dynamic" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 67824a5..0000000 --- a/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -requests>=2.22.0 -httpretty>=0.9.6 -configparser -six \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index 53c9032..0000000 --- a/setup.py +++ /dev/null @@ -1,51 +0,0 @@ -############################################################################## -# Copyright 2019 IBM Corp. -# -# 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 -# -# 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 setuptools import find_packages, setup - -import pyds8k - -install_requires = ['requests', 'httpretty', 'configparser', 'six'] - -setup( - name='pyds8k', - version=pyds8k.version, - description="DS8000 Python Client", - long_description="DS8000 RESTful API Python Client.", - author="Zhang Wu", - author_email="shwzhang@cn.ibm.com", - maintainer="Zhang Wu", - keywords=["IBM", "DS8000 Storage"], - requires=install_requires, - install_requires=install_requires, - tests_require=['nose', 'mock'], - license="Apache License, Version 2.0", - include_package_data=True, - packages=find_packages(), - provides=['pyds8k'], - url="https://github.com/IBM/pyds8k", - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Developers', - 'Intended Audience :: System Administrators', - 'Environment :: Console', - 'License :: OSI Approved :: Apache Software License', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Topic :: Software Development :: Libraries :: Python Modules', - ]) diff --git a/test-requirements.txt b/test-requirements.txt deleted file mode 100644 index 0b29713..0000000 --- a/test-requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -nose>=1.3.7 -mock>=3.0.5 -flake8>=3.6.0 -sphinx>=2.2.1 \ No newline at end of file diff --git a/tox.ini b/tox.ini index 2bd951e..48e795c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,30 +1,33 @@ [tox] -minversion = 3.14.3 +minversion = 4.22.0 skipdist = True -envlist = py3{6,7,8},flake8,cover,docs +envlist = py3{9,10,11,12,13},cover,docs,lint,format [testenv] setenv = VIRTUAL_ENV={envdir} usedevelop = True install_command = pip install {opts} {packages} -deps = -r{toxinidir}/requirements.txt - -r{toxinidir}/test-requirements.txt +dependency_groups = test commands = - nosetests -v --exe {posargs} + pytest --disable-warnings -v {posargs} [testenv:cover] -deps = -r{toxinidir}/requirements.txt - -r{toxinidir}/test-requirements.txt +dependency_groups = test commands = - nosetests --with-coverage --cover-package=pyds8k --cover-html -v {posargs} + pytest --cov-config=.coveragerc --cov pyds8k --disable-warnings -v {posargs} [testenv:docs] -deps = -r{toxinidir}/requirements.txt - -r{toxinidir}/test-requirements.txt +dependency_groups = dev commands = sphinx-build -b html docs docs/html -[testenv:flake8] +[testenv:lint] +dependency_groups = dev commands = - flake8 {posargs} --exclude=./pyds8k/tests/ ./pyds8k + ruff check {posargs} ./pyds8k + +[testenv:format] +dependency_groups = dev +commands = + ruff format --check --diff {posargs} ./pyds8k \ No newline at end of file diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..25c210c --- /dev/null +++ b/uv.lock @@ -0,0 +1,1289 @@ +version = 1 +revision = 3 +requires-python = ">=3.9" +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version < '3.10'", +] + +[[package]] +name = "alabaster" +version = "0.7.16" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/3e/13dd8e5ed9094e734ac430b5d0eb4f2bb001708a8b7856cbf8e084e001ba/alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65", size = 23776, upload-time = "2024-01-10T00:56:10.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/34/d4e1c02d3bee589efb5dfa17f88ea08bdb3e3eac12bc475462aec52ed223/alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92", size = 13511, upload-time = "2024-01-10T00:56:08.388Z" }, +] + +[[package]] +name = "alabaster" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, +] + +[[package]] +name = "babel" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, +] + +[[package]] +name = "cachetools" +version = "6.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/44/ca1675be2a83aeee1886ab745b28cda92093066590233cc501890eb8417a/cachetools-6.2.2.tar.gz", hash = "sha256:8e6d266b25e539df852251cfd6f990b4bc3a141db73b939058d809ebd2590fc6", size = 31571, upload-time = "2025-11-13T17:42:51.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/46/eb6eca305c77a4489affe1c5d8f4cae82f285d9addd8de4ec084a7184221/cachetools-6.2.2-py3-none-any.whl", hash = "sha256:6c09c98183bf58560c97b2abfcedcbaf6a896a490f534b031b661d3723b45ace", size = 11503, upload-time = "2025-11-13T17:42:50.232Z" }, +] + +[[package]] +name = "certifi" +version = "2025.11.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, +] + +[[package]] +name = "chardet" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/f7b6ab21ec75897ed80c17d79b15951a719226b9fababf1e40ea74d69079/chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", size = 2069618, upload-time = "2023-08-01T19:23:02.662Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385, upload-time = "2023-08-01T19:23:00.661Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, + { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, + { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, + { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, + { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, + { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, + { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, + { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/46/7c/0c4760bccf082737ca7ab84a4c2034fcc06b1f21cf3032ea98bd6feb1725/charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9", size = 209609, upload-time = "2025-10-14T04:42:10.922Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a4/69719daef2f3d7f1819de60c9a6be981b8eeead7542d5ec4440f3c80e111/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d", size = 149029, upload-time = "2025-10-14T04:42:12.38Z" }, + { url = "https://files.pythonhosted.org/packages/e6/21/8d4e1d6c1e6070d3672908b8e4533a71b5b53e71d16828cc24d0efec564c/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608", size = 144580, upload-time = "2025-10-14T04:42:13.549Z" }, + { url = "https://files.pythonhosted.org/packages/a7/0a/a616d001b3f25647a9068e0b9199f697ce507ec898cacb06a0d5a1617c99/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc", size = 162340, upload-time = "2025-10-14T04:42:14.892Z" }, + { url = "https://files.pythonhosted.org/packages/85/93/060b52deb249a5450460e0585c88a904a83aec474ab8e7aba787f45e79f2/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e", size = 159619, upload-time = "2025-10-14T04:42:16.676Z" }, + { url = "https://files.pythonhosted.org/packages/dd/21/0274deb1cc0632cd587a9a0ec6b4674d9108e461cb4cd40d457adaeb0564/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1", size = 153980, upload-time = "2025-10-14T04:42:17.917Z" }, + { url = "https://files.pythonhosted.org/packages/28/2b/e3d7d982858dccc11b31906976323d790dded2017a0572f093ff982d692f/charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3", size = 152174, upload-time = "2025-10-14T04:42:19.018Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ff/4a269f8e35f1e58b2df52c131a1fa019acb7ef3f8697b7d464b07e9b492d/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6", size = 151666, upload-time = "2025-10-14T04:42:20.171Z" }, + { url = "https://files.pythonhosted.org/packages/da/c9/ec39870f0b330d58486001dd8e532c6b9a905f5765f58a6f8204926b4a93/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88", size = 145550, upload-time = "2025-10-14T04:42:21.324Z" }, + { url = "https://files.pythonhosted.org/packages/75/8f/d186ab99e40e0ed9f82f033d6e49001701c81244d01905dd4a6924191a30/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1", size = 163721, upload-time = "2025-10-14T04:42:22.46Z" }, + { url = "https://files.pythonhosted.org/packages/96/b1/6047663b9744df26a7e479ac1e77af7134b1fcf9026243bb48ee2d18810f/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf", size = 152127, upload-time = "2025-10-14T04:42:23.712Z" }, + { url = "https://files.pythonhosted.org/packages/59/78/e5a6eac9179f24f704d1be67d08704c3c6ab9f00963963524be27c18ed87/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318", size = 161175, upload-time = "2025-10-14T04:42:24.87Z" }, + { url = "https://files.pythonhosted.org/packages/e5/43/0e626e42d54dd2f8dd6fc5e1c5ff00f05fbca17cb699bedead2cae69c62f/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c", size = 155375, upload-time = "2025-10-14T04:42:27.246Z" }, + { url = "https://files.pythonhosted.org/packages/e9/91/d9615bf2e06f35e4997616ff31248c3657ed649c5ab9d35ea12fce54e380/charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505", size = 99692, upload-time = "2025-10-14T04:42:28.425Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a9/6c040053909d9d1ef4fcab45fddec083aedc9052c10078339b47c8573ea8/charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966", size = 107192, upload-time = "2025-10-14T04:42:29.482Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c6/4fa536b2c0cd3edfb7ccf8469fa0f363ea67b7213a842b90909ca33dd851/charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50", size = 100220, upload-time = "2025-10-14T04:42:30.632Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.10.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/6c/3a3f7a46888e69d18abe3ccc6fe4cb16cccb1e6a2f99698931dafca489e6/coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a", size = 217987, upload-time = "2025-09-21T20:00:57.218Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/952d30f180b1a916c11a56f5c22d3535e943aa22430e9e3322447e520e1c/coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5", size = 218388, upload-time = "2025-09-21T20:01:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/50/2b/9e0cf8ded1e114bcd8b2fd42792b57f1c4e9e4ea1824cde2af93a67305be/coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17", size = 245148, upload-time = "2025-09-21T20:01:01.768Z" }, + { url = "https://files.pythonhosted.org/packages/19/20/d0384ac06a6f908783d9b6aa6135e41b093971499ec488e47279f5b846e6/coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b", size = 246958, upload-time = "2025-09-21T20:01:03.355Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/5c283cff3d41285f8eab897651585db908a909c572bdc014bcfaf8a8b6ae/coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87", size = 248819, upload-time = "2025-09-21T20:01:04.968Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/02eb98fdc5ff79f423e990d877693e5310ae1eab6cb20ae0b0b9ac45b23b/coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e", size = 245754, upload-time = "2025-09-21T20:01:06.321Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bc/25c83bcf3ad141b32cd7dc45485ef3c01a776ca3aa8ef0a93e77e8b5bc43/coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e", size = 246860, upload-time = "2025-09-21T20:01:07.605Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b7/95574702888b58c0928a6e982038c596f9c34d52c5e5107f1eef729399b5/coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df", size = 244877, upload-time = "2025-09-21T20:01:08.829Z" }, + { url = "https://files.pythonhosted.org/packages/47/b6/40095c185f235e085df0e0b158f6bd68cc6e1d80ba6c7721dc81d97ec318/coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0", size = 245108, upload-time = "2025-09-21T20:01:10.527Z" }, + { url = "https://files.pythonhosted.org/packages/c8/50/4aea0556da7a4b93ec9168420d170b55e2eb50ae21b25062513d020c6861/coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13", size = 245752, upload-time = "2025-09-21T20:01:11.857Z" }, + { url = "https://files.pythonhosted.org/packages/6a/28/ea1a84a60828177ae3b100cb6723838523369a44ec5742313ed7db3da160/coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b", size = 220497, upload-time = "2025-09-21T20:01:13.459Z" }, + { url = "https://files.pythonhosted.org/packages/fc/1a/a81d46bbeb3c3fd97b9602ebaa411e076219a150489bcc2c025f151bd52d/coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807", size = 221392, upload-time = "2025-09-21T20:01:14.722Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5d/c1a17867b0456f2e9ce2d8d4708a4c3a089947d0bec9c66cdf60c9e7739f/coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59", size = 218102, upload-time = "2025-09-21T20:01:16.089Z" }, + { url = "https://files.pythonhosted.org/packages/54/f0/514dcf4b4e3698b9a9077f084429681bf3aad2b4a72578f89d7f643eb506/coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a", size = 218505, upload-time = "2025-09-21T20:01:17.788Z" }, + { url = "https://files.pythonhosted.org/packages/20/f6/9626b81d17e2a4b25c63ac1b425ff307ecdeef03d67c9a147673ae40dc36/coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699", size = 248898, upload-time = "2025-09-21T20:01:19.488Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ef/bd8e719c2f7417ba03239052e099b76ea1130ac0cbb183ee1fcaa58aaff3/coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d", size = 250831, upload-time = "2025-09-21T20:01:20.817Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b6/bf054de41ec948b151ae2b79a55c107f5760979538f5fb80c195f2517718/coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e", size = 252937, upload-time = "2025-09-21T20:01:22.171Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e5/3860756aa6f9318227443c6ce4ed7bf9e70bb7f1447a0353f45ac5c7974b/coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23", size = 249021, upload-time = "2025-09-21T20:01:23.907Z" }, + { url = "https://files.pythonhosted.org/packages/26/0f/bd08bd042854f7fd07b45808927ebcce99a7ed0f2f412d11629883517ac2/coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab", size = 250626, upload-time = "2025-09-21T20:01:25.721Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a7/4777b14de4abcc2e80c6b1d430f5d51eb18ed1d75fca56cbce5f2db9b36e/coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82", size = 248682, upload-time = "2025-09-21T20:01:27.105Z" }, + { url = "https://files.pythonhosted.org/packages/34/72/17d082b00b53cd45679bad682fac058b87f011fd8b9fe31d77f5f8d3a4e4/coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2", size = 248402, upload-time = "2025-09-21T20:01:28.629Z" }, + { url = "https://files.pythonhosted.org/packages/81/7a/92367572eb5bdd6a84bfa278cc7e97db192f9f45b28c94a9ca1a921c3577/coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61", size = 249320, upload-time = "2025-09-21T20:01:30.004Z" }, + { url = "https://files.pythonhosted.org/packages/2f/88/a23cc185f6a805dfc4fdf14a94016835eeb85e22ac3a0e66d5e89acd6462/coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14", size = 220536, upload-time = "2025-09-21T20:01:32.184Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ef/0b510a399dfca17cec7bc2f05ad8bd78cf55f15c8bc9a73ab20c5c913c2e/coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2", size = 221425, upload-time = "2025-09-21T20:01:33.557Z" }, + { url = "https://files.pythonhosted.org/packages/51/7f/023657f301a276e4ba1850f82749bc136f5a7e8768060c2e5d9744a22951/coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a", size = 220103, upload-time = "2025-09-21T20:01:34.929Z" }, + { url = "https://files.pythonhosted.org/packages/13/e4/eb12450f71b542a53972d19117ea5a5cea1cab3ac9e31b0b5d498df1bd5a/coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417", size = 218290, upload-time = "2025-09-21T20:01:36.455Z" }, + { url = "https://files.pythonhosted.org/packages/37/66/593f9be12fc19fb36711f19a5371af79a718537204d16ea1d36f16bd78d2/coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973", size = 218515, upload-time = "2025-09-21T20:01:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/66/80/4c49f7ae09cafdacc73fbc30949ffe77359635c168f4e9ff33c9ebb07838/coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c", size = 250020, upload-time = "2025-09-21T20:01:39.617Z" }, + { url = "https://files.pythonhosted.org/packages/a6/90/a64aaacab3b37a17aaedd83e8000142561a29eb262cede42d94a67f7556b/coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7", size = 252769, upload-time = "2025-09-21T20:01:41.341Z" }, + { url = "https://files.pythonhosted.org/packages/98/2e/2dda59afd6103b342e096f246ebc5f87a3363b5412609946c120f4e7750d/coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6", size = 253901, upload-time = "2025-09-21T20:01:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/dc/8d8119c9051d50f3119bb4a75f29f1e4a6ab9415cd1fa8bf22fcc3fb3b5f/coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59", size = 250413, upload-time = "2025-09-21T20:01:44.469Z" }, + { url = "https://files.pythonhosted.org/packages/98/b3/edaff9c5d79ee4d4b6d3fe046f2b1d799850425695b789d491a64225d493/coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b", size = 251820, upload-time = "2025-09-21T20:01:45.915Z" }, + { url = "https://files.pythonhosted.org/packages/11/25/9a0728564bb05863f7e513e5a594fe5ffef091b325437f5430e8cfb0d530/coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a", size = 249941, upload-time = "2025-09-21T20:01:47.296Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fd/ca2650443bfbef5b0e74373aac4df67b08180d2f184b482c41499668e258/coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb", size = 249519, upload-time = "2025-09-21T20:01:48.73Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/f692f125fb4299b6f963b0745124998ebb8e73ecdfce4ceceb06a8c6bec5/coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1", size = 251375, upload-time = "2025-09-21T20:01:50.529Z" }, + { url = "https://files.pythonhosted.org/packages/5e/75/61b9bbd6c7d24d896bfeec57acba78e0f8deac68e6baf2d4804f7aae1f88/coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256", size = 220699, upload-time = "2025-09-21T20:01:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f3/3bf7905288b45b075918d372498f1cf845b5b579b723c8fd17168018d5f5/coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba", size = 221512, upload-time = "2025-09-21T20:01:53.481Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/3e32dbe933979d05cf2dac5e697c8599cfe038aaf51223ab901e208d5a62/coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf", size = 220147, upload-time = "2025-09-21T20:01:55.2Z" }, + { url = "https://files.pythonhosted.org/packages/9a/94/b765c1abcb613d103b64fcf10395f54d69b0ef8be6a0dd9c524384892cc7/coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d", size = 218320, upload-time = "2025-09-21T20:01:56.629Z" }, + { url = "https://files.pythonhosted.org/packages/72/4f/732fff31c119bb73b35236dd333030f32c4bfe909f445b423e6c7594f9a2/coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b", size = 218575, upload-time = "2025-09-21T20:01:58.203Z" }, + { url = "https://files.pythonhosted.org/packages/87/02/ae7e0af4b674be47566707777db1aa375474f02a1d64b9323e5813a6cdd5/coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e", size = 249568, upload-time = "2025-09-21T20:01:59.748Z" }, + { url = "https://files.pythonhosted.org/packages/a2/77/8c6d22bf61921a59bce5471c2f1f7ac30cd4ac50aadde72b8c48d5727902/coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b", size = 252174, upload-time = "2025-09-21T20:02:01.192Z" }, + { url = "https://files.pythonhosted.org/packages/b1/20/b6ea4f69bbb52dac0aebd62157ba6a9dddbfe664f5af8122dac296c3ee15/coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49", size = 253447, upload-time = "2025-09-21T20:02:02.701Z" }, + { url = "https://files.pythonhosted.org/packages/f9/28/4831523ba483a7f90f7b259d2018fef02cb4d5b90bc7c1505d6e5a84883c/coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911", size = 249779, upload-time = "2025-09-21T20:02:04.185Z" }, + { url = "https://files.pythonhosted.org/packages/a7/9f/4331142bc98c10ca6436d2d620c3e165f31e6c58d43479985afce6f3191c/coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0", size = 251604, upload-time = "2025-09-21T20:02:06.034Z" }, + { url = "https://files.pythonhosted.org/packages/ce/60/bda83b96602036b77ecf34e6393a3836365481b69f7ed7079ab85048202b/coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f", size = 249497, upload-time = "2025-09-21T20:02:07.619Z" }, + { url = "https://files.pythonhosted.org/packages/5f/af/152633ff35b2af63977edd835d8e6430f0caef27d171edf2fc76c270ef31/coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c", size = 249350, upload-time = "2025-09-21T20:02:10.34Z" }, + { url = "https://files.pythonhosted.org/packages/9d/71/d92105d122bd21cebba877228990e1646d862e34a98bb3374d3fece5a794/coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f", size = 251111, upload-time = "2025-09-21T20:02:12.122Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9e/9fdb08f4bf476c912f0c3ca292e019aab6712c93c9344a1653986c3fd305/coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698", size = 220746, upload-time = "2025-09-21T20:02:13.919Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b1/a75fd25df44eab52d1931e89980d1ada46824c7a3210be0d3c88a44aaa99/coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843", size = 221541, upload-time = "2025-09-21T20:02:15.57Z" }, + { url = "https://files.pythonhosted.org/packages/14/3a/d720d7c989562a6e9a14b2c9f5f2876bdb38e9367126d118495b89c99c37/coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546", size = 220170, upload-time = "2025-09-21T20:02:17.395Z" }, + { url = "https://files.pythonhosted.org/packages/bb/22/e04514bf2a735d8b0add31d2b4ab636fc02370730787c576bb995390d2d5/coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c", size = 219029, upload-time = "2025-09-21T20:02:18.936Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/91128e099035ece15da3445d9015e4b4153a6059403452d324cbb0a575fa/coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15", size = 219259, upload-time = "2025-09-21T20:02:20.44Z" }, + { url = "https://files.pythonhosted.org/packages/8b/51/66420081e72801536a091a0c8f8c1f88a5c4bf7b9b1bdc6222c7afe6dc9b/coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4", size = 260592, upload-time = "2025-09-21T20:02:22.313Z" }, + { url = "https://files.pythonhosted.org/packages/5d/22/9b8d458c2881b22df3db5bb3e7369e63d527d986decb6c11a591ba2364f7/coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0", size = 262768, upload-time = "2025-09-21T20:02:24.287Z" }, + { url = "https://files.pythonhosted.org/packages/f7/08/16bee2c433e60913c610ea200b276e8eeef084b0d200bdcff69920bd5828/coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0", size = 264995, upload-time = "2025-09-21T20:02:26.133Z" }, + { url = "https://files.pythonhosted.org/packages/20/9d/e53eb9771d154859b084b90201e5221bca7674ba449a17c101a5031d4054/coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65", size = 259546, upload-time = "2025-09-21T20:02:27.716Z" }, + { url = "https://files.pythonhosted.org/packages/ad/b0/69bc7050f8d4e56a89fb550a1577d5d0d1db2278106f6f626464067b3817/coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541", size = 262544, upload-time = "2025-09-21T20:02:29.216Z" }, + { url = "https://files.pythonhosted.org/packages/ef/4b/2514b060dbd1bc0aaf23b852c14bb5818f244c664cb16517feff6bb3a5ab/coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6", size = 260308, upload-time = "2025-09-21T20:02:31.226Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/7ba2175007c246d75e496f64c06e94122bdb914790a1285d627a918bd271/coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999", size = 258920, upload-time = "2025-09-21T20:02:32.823Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/fac9f7abbc841409b9a410309d73bfa6cfb2e51c3fada738cb607ce174f8/coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2", size = 261434, upload-time = "2025-09-21T20:02:34.86Z" }, + { url = "https://files.pythonhosted.org/packages/ee/51/a03bec00d37faaa891b3ff7387192cef20f01604e5283a5fabc95346befa/coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a", size = 221403, upload-time = "2025-09-21T20:02:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/53/22/3cf25d614e64bf6d8e59c7c669b20d6d940bb337bdee5900b9ca41c820bb/coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb", size = 222469, upload-time = "2025-09-21T20:02:39.011Z" }, + { url = "https://files.pythonhosted.org/packages/49/a1/00164f6d30d8a01c3c9c48418a7a5be394de5349b421b9ee019f380df2a0/coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb", size = 220731, upload-time = "2025-09-21T20:02:40.939Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/5844ab4ca6a4dd97a1850e030a15ec7d292b5c5cb93082979225126e35dd/coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520", size = 218302, upload-time = "2025-09-21T20:02:42.527Z" }, + { url = "https://files.pythonhosted.org/packages/f0/89/673f6514b0961d1f0e20ddc242e9342f6da21eaba3489901b565c0689f34/coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32", size = 218578, upload-time = "2025-09-21T20:02:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/05/e8/261cae479e85232828fb17ad536765c88dd818c8470aca690b0ac6feeaa3/coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f", size = 249629, upload-time = "2025-09-21T20:02:46.503Z" }, + { url = "https://files.pythonhosted.org/packages/82/62/14ed6546d0207e6eda876434e3e8475a3e9adbe32110ce896c9e0c06bb9a/coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a", size = 252162, upload-time = "2025-09-21T20:02:48.689Z" }, + { url = "https://files.pythonhosted.org/packages/ff/49/07f00db9ac6478e4358165a08fb41b469a1b053212e8a00cb02f0d27a05f/coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360", size = 253517, upload-time = "2025-09-21T20:02:50.31Z" }, + { url = "https://files.pythonhosted.org/packages/a2/59/c5201c62dbf165dfbc91460f6dbbaa85a8b82cfa6131ac45d6c1bfb52deb/coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69", size = 249632, upload-time = "2025-09-21T20:02:51.971Z" }, + { url = "https://files.pythonhosted.org/packages/07/ae/5920097195291a51fb00b3a70b9bbd2edbfe3c84876a1762bd1ef1565ebc/coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14", size = 251520, upload-time = "2025-09-21T20:02:53.858Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3c/a815dde77a2981f5743a60b63df31cb322c944843e57dbd579326625a413/coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe", size = 249455, upload-time = "2025-09-21T20:02:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/aa/99/f5cdd8421ea656abefb6c0ce92556709db2265c41e8f9fc6c8ae0f7824c9/coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e", size = 249287, upload-time = "2025-09-21T20:02:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/c3/7a/e9a2da6a1fc5d007dd51fca083a663ab930a8c4d149c087732a5dbaa0029/coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd", size = 250946, upload-time = "2025-09-21T20:02:59.431Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5b/0b5799aa30380a949005a353715095d6d1da81927d6dbed5def2200a4e25/coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2", size = 221009, upload-time = "2025-09-21T20:03:01.324Z" }, + { url = "https://files.pythonhosted.org/packages/da/b0/e802fbb6eb746de006490abc9bb554b708918b6774b722bb3a0e6aa1b7de/coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681", size = 221804, upload-time = "2025-09-21T20:03:03.4Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e8/71d0c8e374e31f39e3389bb0bd19e527d46f00ea8571ec7ec8fd261d8b44/coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880", size = 220384, upload-time = "2025-09-21T20:03:05.111Z" }, + { url = "https://files.pythonhosted.org/packages/62/09/9a5608d319fa3eba7a2019addeacb8c746fb50872b57a724c9f79f146969/coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63", size = 219047, upload-time = "2025-09-21T20:03:06.795Z" }, + { url = "https://files.pythonhosted.org/packages/f5/6f/f58d46f33db9f2e3647b2d0764704548c184e6f5e014bef528b7f979ef84/coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2", size = 219266, upload-time = "2025-09-21T20:03:08.495Z" }, + { url = "https://files.pythonhosted.org/packages/74/5c/183ffc817ba68e0b443b8c934c8795553eb0c14573813415bd59941ee165/coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d", size = 260767, upload-time = "2025-09-21T20:03:10.172Z" }, + { url = "https://files.pythonhosted.org/packages/0f/48/71a8abe9c1ad7e97548835e3cc1adbf361e743e9d60310c5f75c9e7bf847/coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0", size = 262931, upload-time = "2025-09-21T20:03:11.861Z" }, + { url = "https://files.pythonhosted.org/packages/84/fd/193a8fb132acfc0a901f72020e54be5e48021e1575bb327d8ee1097a28fd/coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699", size = 265186, upload-time = "2025-09-21T20:03:13.539Z" }, + { url = "https://files.pythonhosted.org/packages/b1/8f/74ecc30607dd95ad50e3034221113ccb1c6d4e8085cc761134782995daae/coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9", size = 259470, upload-time = "2025-09-21T20:03:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/0f/55/79ff53a769f20d71b07023ea115c9167c0bb56f281320520cf64c5298a96/coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f", size = 262626, upload-time = "2025-09-21T20:03:17.673Z" }, + { url = "https://files.pythonhosted.org/packages/88/e2/dac66c140009b61ac3fc13af673a574b00c16efdf04f9b5c740703e953c0/coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1", size = 260386, upload-time = "2025-09-21T20:03:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f1/f48f645e3f33bb9ca8a496bc4a9671b52f2f353146233ebd7c1df6160440/coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0", size = 258852, upload-time = "2025-09-21T20:03:21.007Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3b/8442618972c51a7affeead957995cfa8323c0c9bcf8fa5a027421f720ff4/coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399", size = 261534, upload-time = "2025-09-21T20:03:23.12Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dc/101f3fa3a45146db0cb03f5b4376e24c0aac818309da23e2de0c75295a91/coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235", size = 221784, upload-time = "2025-09-21T20:03:24.769Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a1/74c51803fc70a8a40d7346660379e144be772bab4ac7bb6e6b905152345c/coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d", size = 222905, upload-time = "2025-09-21T20:03:26.93Z" }, + { url = "https://files.pythonhosted.org/packages/12/65/f116a6d2127df30bcafbceef0302d8a64ba87488bf6f73a6d8eebf060873/coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a", size = 220922, upload-time = "2025-09-21T20:03:28.672Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/d1c25053764b4c42eb294aae92ab617d2e4f803397f9c7c8295caa77a260/coverage-7.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3", size = 217978, upload-time = "2025-09-21T20:03:30.362Z" }, + { url = "https://files.pythonhosted.org/packages/52/2f/b9f9daa39b80ece0b9548bbb723381e29bc664822d9a12c2135f8922c22b/coverage-7.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c", size = 218370, upload-time = "2025-09-21T20:03:32.147Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6e/30d006c3b469e58449650642383dddf1c8fb63d44fdf92994bfd46570695/coverage-7.10.7-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396", size = 244802, upload-time = "2025-09-21T20:03:33.919Z" }, + { url = "https://files.pythonhosted.org/packages/b0/49/8a070782ce7e6b94ff6a0b6d7c65ba6bc3091d92a92cef4cd4eb0767965c/coverage-7.10.7-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40", size = 246625, upload-time = "2025-09-21T20:03:36.09Z" }, + { url = "https://files.pythonhosted.org/packages/6a/92/1c1c5a9e8677ce56d42b97bdaca337b2d4d9ebe703d8c174ede52dbabd5f/coverage-7.10.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594", size = 248399, upload-time = "2025-09-21T20:03:38.342Z" }, + { url = "https://files.pythonhosted.org/packages/c0/54/b140edee7257e815de7426d5d9846b58505dffc29795fff2dfb7f8a1c5a0/coverage-7.10.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a", size = 245142, upload-time = "2025-09-21T20:03:40.591Z" }, + { url = "https://files.pythonhosted.org/packages/e4/9e/6d6b8295940b118e8b7083b29226c71f6154f7ff41e9ca431f03de2eac0d/coverage-7.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b", size = 246284, upload-time = "2025-09-21T20:03:42.355Z" }, + { url = "https://files.pythonhosted.org/packages/db/e5/5e957ca747d43dbe4d9714358375c7546cb3cb533007b6813fc20fce37ad/coverage-7.10.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3", size = 244353, upload-time = "2025-09-21T20:03:44.218Z" }, + { url = "https://files.pythonhosted.org/packages/9a/45/540fc5cc92536a1b783b7ef99450bd55a4b3af234aae35a18a339973ce30/coverage-7.10.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0", size = 244430, upload-time = "2025-09-21T20:03:46.065Z" }, + { url = "https://files.pythonhosted.org/packages/75/0b/8287b2e5b38c8fe15d7e3398849bb58d382aedc0864ea0fa1820e8630491/coverage-7.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f", size = 245311, upload-time = "2025-09-21T20:03:48.19Z" }, + { url = "https://files.pythonhosted.org/packages/0c/1d/29724999984740f0c86d03e6420b942439bf5bd7f54d4382cae386a9d1e9/coverage-7.10.7-cp39-cp39-win32.whl", hash = "sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431", size = 220500, upload-time = "2025-09-21T20:03:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/43/11/4b1e6b129943f905ca54c339f343877b55b365ae2558806c1be4f7476ed5/coverage-7.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07", size = 221408, upload-time = "2025-09-21T20:03:51.803Z" }, + { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version < '3.10'" }, +] + +[[package]] +name = "coverage" +version = "7.12.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/89/26/4a96807b193b011588099c3b5c89fbb05294e5b90e71018e065465f34eb6/coverage-7.12.0.tar.gz", hash = "sha256:fc11e0a4e372cb5f282f16ef90d4a585034050ccda536451901abfb19a57f40c", size = 819341, upload-time = "2025-11-18T13:34:20.766Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/4a/0dc3de1c172d35abe512332cfdcc43211b6ebce629e4cc42e6cd25ed8f4d/coverage-7.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:32b75c2ba3f324ee37af3ccee5b30458038c50b349ad9b88cee85096132a575b", size = 217409, upload-time = "2025-11-18T13:31:53.122Z" }, + { url = "https://files.pythonhosted.org/packages/01/c3/086198b98db0109ad4f84241e8e9ea7e5fb2db8c8ffb787162d40c26cc76/coverage-7.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cb2a1b6ab9fe833714a483a915de350abc624a37149649297624c8d57add089c", size = 217927, upload-time = "2025-11-18T13:31:54.458Z" }, + { url = "https://files.pythonhosted.org/packages/5d/5f/34614dbf5ce0420828fc6c6f915126a0fcb01e25d16cf141bf5361e6aea6/coverage-7.12.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5734b5d913c3755e72f70bf6cc37a0518d4f4745cde760c5d8e12005e62f9832", size = 244678, upload-time = "2025-11-18T13:31:55.805Z" }, + { url = "https://files.pythonhosted.org/packages/55/7b/6b26fb32e8e4a6989ac1d40c4e132b14556131493b1d06bc0f2be169c357/coverage-7.12.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b527a08cdf15753279b7afb2339a12073620b761d79b81cbe2cdebdb43d90daa", size = 246507, upload-time = "2025-11-18T13:31:57.05Z" }, + { url = "https://files.pythonhosted.org/packages/06/42/7d70e6603d3260199b90fb48b537ca29ac183d524a65cc31366b2e905fad/coverage-7.12.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9bb44c889fb68004e94cab71f6a021ec83eac9aeabdbb5a5a88821ec46e1da73", size = 248366, upload-time = "2025-11-18T13:31:58.362Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4a/d86b837923878424c72458c5b25e899a3c5ca73e663082a915f5b3c4d749/coverage-7.12.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4b59b501455535e2e5dde5881739897967b272ba25988c89145c12d772810ccb", size = 245366, upload-time = "2025-11-18T13:31:59.572Z" }, + { url = "https://files.pythonhosted.org/packages/e6/c2/2adec557e0aa9721875f06ced19730fdb7fc58e31b02b5aa56f2ebe4944d/coverage-7.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d8842f17095b9868a05837b7b1b73495293091bed870e099521ada176aa3e00e", size = 246408, upload-time = "2025-11-18T13:32:00.784Z" }, + { url = "https://files.pythonhosted.org/packages/5a/4b/8bd1f1148260df11c618e535fdccd1e5aaf646e55b50759006a4f41d8a26/coverage-7.12.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c5a6f20bf48b8866095c6820641e7ffbe23f2ac84a2efc218d91235e404c7777", size = 244416, upload-time = "2025-11-18T13:32:01.963Z" }, + { url = "https://files.pythonhosted.org/packages/0e/13/3a248dd6a83df90414c54a4e121fd081fb20602ca43955fbe1d60e2312a9/coverage-7.12.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:5f3738279524e988d9da2893f307c2093815c623f8d05a8f79e3eff3a7a9e553", size = 244681, upload-time = "2025-11-18T13:32:03.408Z" }, + { url = "https://files.pythonhosted.org/packages/76/30/aa833827465a5e8c938935f5d91ba055f70516941078a703740aaf1aa41f/coverage-7.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0d68c1f7eabbc8abe582d11fa393ea483caf4f44b0af86881174769f185c94d", size = 245300, upload-time = "2025-11-18T13:32:04.686Z" }, + { url = "https://files.pythonhosted.org/packages/38/24/f85b3843af1370fb3739fa7571819b71243daa311289b31214fe3e8c9d68/coverage-7.12.0-cp310-cp310-win32.whl", hash = "sha256:7670d860e18b1e3ee5930b17a7d55ae6287ec6e55d9799982aa103a2cc1fa2ef", size = 220008, upload-time = "2025-11-18T13:32:05.806Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a2/c7da5b9566f7164db9eefa133d17761ecb2c2fde9385d754e5b5c80f710d/coverage-7.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:f999813dddeb2a56aab5841e687b68169da0d3f6fc78ccf50952fa2463746022", size = 220943, upload-time = "2025-11-18T13:32:07.166Z" }, + { url = "https://files.pythonhosted.org/packages/5a/0c/0dfe7f0487477d96432e4815537263363fb6dd7289743a796e8e51eabdf2/coverage-7.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa124a3683d2af98bd9d9c2bfa7a5076ca7e5ab09fdb96b81fa7d89376ae928f", size = 217535, upload-time = "2025-11-18T13:32:08.812Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f5/f9a4a053a5bbff023d3bec259faac8f11a1e5a6479c2ccf586f910d8dac7/coverage-7.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d93fbf446c31c0140208dcd07c5d882029832e8ed7891a39d6d44bd65f2316c3", size = 218044, upload-time = "2025-11-18T13:32:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/95/c5/84fc3697c1fa10cd8571919bf9693f693b7373278daaf3b73e328d502bc8/coverage-7.12.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:52ca620260bd8cd6027317bdd8b8ba929be1d741764ee765b42c4d79a408601e", size = 248440, upload-time = "2025-11-18T13:32:12.536Z" }, + { url = "https://files.pythonhosted.org/packages/f4/36/2d93fbf6a04670f3874aed397d5a5371948a076e3249244a9e84fb0e02d6/coverage-7.12.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f3433ffd541380f3a0e423cff0f4926d55b0cc8c1d160fdc3be24a4c03aa65f7", size = 250361, upload-time = "2025-11-18T13:32:13.852Z" }, + { url = "https://files.pythonhosted.org/packages/5d/49/66dc65cc456a6bfc41ea3d0758c4afeaa4068a2b2931bf83be6894cf1058/coverage-7.12.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7bbb321d4adc9f65e402c677cd1c8e4c2d0105d3ce285b51b4d87f1d5db5245", size = 252472, upload-time = "2025-11-18T13:32:15.068Z" }, + { url = "https://files.pythonhosted.org/packages/35/1f/ebb8a18dffd406db9fcd4b3ae42254aedcaf612470e8712f12041325930f/coverage-7.12.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22a7aade354a72dff3b59c577bfd18d6945c61f97393bc5fb7bd293a4237024b", size = 248592, upload-time = "2025-11-18T13:32:16.328Z" }, + { url = "https://files.pythonhosted.org/packages/da/a8/67f213c06e5ea3b3d4980df7dc344d7fea88240b5fe878a5dcbdfe0e2315/coverage-7.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3ff651dcd36d2fea66877cd4a82de478004c59b849945446acb5baf9379a1b64", size = 250167, upload-time = "2025-11-18T13:32:17.687Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/e52aef68154164ea40cc8389c120c314c747fe63a04b013a5782e989b77f/coverage-7.12.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:31b8b2e38391a56e3cea39d22a23faaa7c3fc911751756ef6d2621d2a9daf742", size = 248238, upload-time = "2025-11-18T13:32:19.2Z" }, + { url = "https://files.pythonhosted.org/packages/1f/a4/4d88750bcf9d6d66f77865e5a05a20e14db44074c25fd22519777cb69025/coverage-7.12.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:297bc2da28440f5ae51c845a47c8175a4db0553a53827886e4fb25c66633000c", size = 247964, upload-time = "2025-11-18T13:32:21.027Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6b/b74693158899d5b47b0bf6238d2c6722e20ba749f86b74454fac0696bb00/coverage-7.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ff7651cc01a246908eac162a6a86fc0dbab6de1ad165dfb9a1e2ec660b44984", size = 248862, upload-time = "2025-11-18T13:32:22.304Z" }, + { url = "https://files.pythonhosted.org/packages/18/de/6af6730227ce0e8ade307b1cc4a08e7f51b419a78d02083a86c04ccceb29/coverage-7.12.0-cp311-cp311-win32.whl", hash = "sha256:313672140638b6ddb2c6455ddeda41c6a0b208298034544cfca138978c6baed6", size = 220033, upload-time = "2025-11-18T13:32:23.714Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a1/e7f63021a7c4fe20994359fcdeae43cbef4a4d0ca36a5a1639feeea5d9e1/coverage-7.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:a1783ed5bd0d5938d4435014626568dc7f93e3cb99bc59188cc18857c47aa3c4", size = 220966, upload-time = "2025-11-18T13:32:25.599Z" }, + { url = "https://files.pythonhosted.org/packages/77/e8/deae26453f37c20c3aa0c4433a1e32cdc169bf415cce223a693117aa3ddd/coverage-7.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:4648158fd8dd9381b5847622df1c90ff314efbfc1df4550092ab6013c238a5fc", size = 219637, upload-time = "2025-11-18T13:32:27.265Z" }, + { url = "https://files.pythonhosted.org/packages/02/bf/638c0427c0f0d47638242e2438127f3c8ee3cfc06c7fdeb16778ed47f836/coverage-7.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:29644c928772c78512b48e14156b81255000dcfd4817574ff69def189bcb3647", size = 217704, upload-time = "2025-11-18T13:32:28.906Z" }, + { url = "https://files.pythonhosted.org/packages/08/e1/706fae6692a66c2d6b871a608bbde0da6281903fa0e9f53a39ed441da36a/coverage-7.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8638cbb002eaa5d7c8d04da667813ce1067080b9a91099801a0053086e52b736", size = 218064, upload-time = "2025-11-18T13:32:30.161Z" }, + { url = "https://files.pythonhosted.org/packages/a9/8b/eb0231d0540f8af3ffda39720ff43cb91926489d01524e68f60e961366e4/coverage-7.12.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:083631eeff5eb9992c923e14b810a179798bb598e6a0dd60586819fc23be6e60", size = 249560, upload-time = "2025-11-18T13:32:31.835Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a1/67fb52af642e974d159b5b379e4d4c59d0ebe1288677fbd04bbffe665a82/coverage-7.12.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:99d5415c73ca12d558e07776bd957c4222c687b9f1d26fa0e1b57e3598bdcde8", size = 252318, upload-time = "2025-11-18T13:32:33.178Z" }, + { url = "https://files.pythonhosted.org/packages/41/e5/38228f31b2c7665ebf9bdfdddd7a184d56450755c7e43ac721c11a4b8dab/coverage-7.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e949ebf60c717c3df63adb4a1a366c096c8d7fd8472608cd09359e1bd48ef59f", size = 253403, upload-time = "2025-11-18T13:32:34.45Z" }, + { url = "https://files.pythonhosted.org/packages/ec/4b/df78e4c8188f9960684267c5a4897836f3f0f20a20c51606ee778a1d9749/coverage-7.12.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d907ddccbca819afa2cd014bc69983b146cca2735a0b1e6259b2a6c10be1e70", size = 249984, upload-time = "2025-11-18T13:32:35.747Z" }, + { url = "https://files.pythonhosted.org/packages/ba/51/bb163933d195a345c6f63eab9e55743413d064c291b6220df754075c2769/coverage-7.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b1518ecbad4e6173f4c6e6c4a46e49555ea5679bf3feda5edb1b935c7c44e8a0", size = 251339, upload-time = "2025-11-18T13:32:37.352Z" }, + { url = "https://files.pythonhosted.org/packages/15/40/c9b29cdb8412c837cdcbc2cfa054547dd83affe6cbbd4ce4fdb92b6ba7d1/coverage-7.12.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51777647a749abdf6f6fd8c7cffab12de68ab93aab15efc72fbbb83036c2a068", size = 249489, upload-time = "2025-11-18T13:32:39.212Z" }, + { url = "https://files.pythonhosted.org/packages/c8/da/b3131e20ba07a0de4437a50ef3b47840dfabf9293675b0cd5c2c7f66dd61/coverage-7.12.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:42435d46d6461a3b305cdfcad7cdd3248787771f53fe18305548cba474e6523b", size = 249070, upload-time = "2025-11-18T13:32:40.598Z" }, + { url = "https://files.pythonhosted.org/packages/70/81/b653329b5f6302c08d683ceff6785bc60a34be9ae92a5c7b63ee7ee7acec/coverage-7.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5bcead88c8423e1855e64b8057d0544e33e4080b95b240c2a355334bb7ced937", size = 250929, upload-time = "2025-11-18T13:32:42.915Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/250ac3bca9f252a5fb1338b5ad01331ebb7b40223f72bef5b1b2cb03aa64/coverage-7.12.0-cp312-cp312-win32.whl", hash = "sha256:dcbb630ab034e86d2a0f79aefd2be07e583202f41e037602d438c80044957baa", size = 220241, upload-time = "2025-11-18T13:32:44.665Z" }, + { url = "https://files.pythonhosted.org/packages/64/1c/77e79e76d37ce83302f6c21980b45e09f8aa4551965213a10e62d71ce0ab/coverage-7.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:2fd8354ed5d69775ac42986a691fbf68b4084278710cee9d7c3eaa0c28fa982a", size = 221051, upload-time = "2025-11-18T13:32:46.008Z" }, + { url = "https://files.pythonhosted.org/packages/31/f5/641b8a25baae564f9e52cac0e2667b123de961985709a004e287ee7663cc/coverage-7.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:737c3814903be30695b2de20d22bcc5428fdae305c61ba44cdc8b3252984c49c", size = 219692, upload-time = "2025-11-18T13:32:47.372Z" }, + { url = "https://files.pythonhosted.org/packages/b8/14/771700b4048774e48d2c54ed0c674273702713c9ee7acdfede40c2666747/coverage-7.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:47324fffca8d8eae7e185b5bb20c14645f23350f870c1649003618ea91a78941", size = 217725, upload-time = "2025-11-18T13:32:49.22Z" }, + { url = "https://files.pythonhosted.org/packages/17/a7/3aa4144d3bcb719bf67b22d2d51c2d577bf801498c13cb08f64173e80497/coverage-7.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ccf3b2ede91decd2fb53ec73c1f949c3e034129d1e0b07798ff1d02ea0c8fa4a", size = 218098, upload-time = "2025-11-18T13:32:50.78Z" }, + { url = "https://files.pythonhosted.org/packages/fc/9c/b846bbc774ff81091a12a10203e70562c91ae71badda00c5ae5b613527b1/coverage-7.12.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b365adc70a6936c6b0582dc38746b33b2454148c02349345412c6e743efb646d", size = 249093, upload-time = "2025-11-18T13:32:52.554Z" }, + { url = "https://files.pythonhosted.org/packages/76/b6/67d7c0e1f400b32c883e9342de4a8c2ae7c1a0b57c5de87622b7262e2309/coverage-7.12.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bc13baf85cd8a4cfcf4a35c7bc9d795837ad809775f782f697bf630b7e200211", size = 251686, upload-time = "2025-11-18T13:32:54.862Z" }, + { url = "https://files.pythonhosted.org/packages/cc/75/b095bd4b39d49c3be4bffbb3135fea18a99a431c52dd7513637c0762fecb/coverage-7.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:099d11698385d572ceafb3288a5b80fe1fc58bf665b3f9d362389de488361d3d", size = 252930, upload-time = "2025-11-18T13:32:56.417Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f3/466f63015c7c80550bead3093aacabf5380c1220a2a93c35d374cae8f762/coverage-7.12.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:473dc45d69694069adb7680c405fb1e81f60b2aff42c81e2f2c3feaf544d878c", size = 249296, upload-time = "2025-11-18T13:32:58.074Z" }, + { url = "https://files.pythonhosted.org/packages/27/86/eba2209bf2b7e28c68698fc13437519a295b2d228ba9e0ec91673e09fa92/coverage-7.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:583f9adbefd278e9de33c33d6846aa8f5d164fa49b47144180a0e037f0688bb9", size = 251068, upload-time = "2025-11-18T13:32:59.646Z" }, + { url = "https://files.pythonhosted.org/packages/ec/55/ca8ae7dbba962a3351f18940b359b94c6bafdd7757945fdc79ec9e452dc7/coverage-7.12.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b2089cc445f2dc0af6f801f0d1355c025b76c24481935303cf1af28f636688f0", size = 249034, upload-time = "2025-11-18T13:33:01.481Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d7/39136149325cad92d420b023b5fd900dabdd1c3a0d1d5f148ef4a8cedef5/coverage-7.12.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:950411f1eb5d579999c5f66c62a40961f126fc71e5e14419f004471957b51508", size = 248853, upload-time = "2025-11-18T13:33:02.935Z" }, + { url = "https://files.pythonhosted.org/packages/fe/b6/76e1add8b87ef60e00643b0b7f8f7bb73d4bf5249a3be19ebefc5793dd25/coverage-7.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b1aab7302a87bafebfe76b12af681b56ff446dc6f32ed178ff9c092ca776e6bc", size = 250619, upload-time = "2025-11-18T13:33:04.336Z" }, + { url = "https://files.pythonhosted.org/packages/95/87/924c6dc64f9203f7a3c1832a6a0eee5a8335dbe5f1bdadcc278d6f1b4d74/coverage-7.12.0-cp313-cp313-win32.whl", hash = "sha256:d7e0d0303c13b54db495eb636bc2465b2fb8475d4c8bcec8fe4b5ca454dfbae8", size = 220261, upload-time = "2025-11-18T13:33:06.493Z" }, + { url = "https://files.pythonhosted.org/packages/91/77/dd4aff9af16ff776bf355a24d87eeb48fc6acde54c907cc1ea89b14a8804/coverage-7.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:ce61969812d6a98a981d147d9ac583a36ac7db7766f2e64a9d4d059c2fe29d07", size = 221072, upload-time = "2025-11-18T13:33:07.926Z" }, + { url = "https://files.pythonhosted.org/packages/70/49/5c9dc46205fef31b1b226a6e16513193715290584317fd4df91cdaf28b22/coverage-7.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:bcec6f47e4cb8a4c2dc91ce507f6eefc6a1b10f58df32cdc61dff65455031dfc", size = 219702, upload-time = "2025-11-18T13:33:09.631Z" }, + { url = "https://files.pythonhosted.org/packages/9b/62/f87922641c7198667994dd472a91e1d9b829c95d6c29529ceb52132436ad/coverage-7.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:459443346509476170d553035e4a3eed7b860f4fe5242f02de1010501956ce87", size = 218420, upload-time = "2025-11-18T13:33:11.153Z" }, + { url = "https://files.pythonhosted.org/packages/85/dd/1cc13b2395ef15dbb27d7370a2509b4aee77890a464fb35d72d428f84871/coverage-7.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:04a79245ab2b7a61688958f7a855275997134bc84f4a03bc240cf64ff132abf6", size = 218773, upload-time = "2025-11-18T13:33:12.569Z" }, + { url = "https://files.pythonhosted.org/packages/74/40/35773cc4bb1e9d4658d4fb669eb4195b3151bef3bbd6f866aba5cd5dac82/coverage-7.12.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:09a86acaaa8455f13d6a99221d9654df249b33937b4e212b4e5a822065f12aa7", size = 260078, upload-time = "2025-11-18T13:33:14.037Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ee/231bb1a6ffc2905e396557585ebc6bdc559e7c66708376d245a1f1d330fc/coverage-7.12.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:907e0df1b71ba77463687a74149c6122c3f6aac56c2510a5d906b2f368208560", size = 262144, upload-time = "2025-11-18T13:33:15.601Z" }, + { url = "https://files.pythonhosted.org/packages/28/be/32f4aa9f3bf0b56f3971001b56508352c7753915345d45fab4296a986f01/coverage-7.12.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b57e2d0ddd5f0582bae5437c04ee71c46cd908e7bc5d4d0391f9a41e812dd12", size = 264574, upload-time = "2025-11-18T13:33:17.354Z" }, + { url = "https://files.pythonhosted.org/packages/68/7c/00489fcbc2245d13ab12189b977e0cf06ff3351cb98bc6beba8bd68c5902/coverage-7.12.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:58c1c6aa677f3a1411fe6fb28ec3a942e4f665df036a3608816e0847fad23296", size = 259298, upload-time = "2025-11-18T13:33:18.958Z" }, + { url = "https://files.pythonhosted.org/packages/96/b4/f0760d65d56c3bea95b449e02570d4abd2549dc784bf39a2d4721a2d8ceb/coverage-7.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4c589361263ab2953e3c4cd2a94db94c4ad4a8e572776ecfbad2389c626e4507", size = 262150, upload-time = "2025-11-18T13:33:20.644Z" }, + { url = "https://files.pythonhosted.org/packages/c5/71/9a9314df00f9326d78c1e5a910f520d599205907432d90d1c1b7a97aa4b1/coverage-7.12.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:91b810a163ccad2e43b1faa11d70d3cf4b6f3d83f9fd5f2df82a32d47b648e0d", size = 259763, upload-time = "2025-11-18T13:33:22.189Z" }, + { url = "https://files.pythonhosted.org/packages/10/34/01a0aceed13fbdf925876b9a15d50862eb8845454301fe3cdd1df08b2182/coverage-7.12.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:40c867af715f22592e0d0fb533a33a71ec9e0f73a6945f722a0c85c8c1cbe3a2", size = 258653, upload-time = "2025-11-18T13:33:24.239Z" }, + { url = "https://files.pythonhosted.org/packages/8d/04/81d8fd64928acf1574bbb0181f66901c6c1c6279c8ccf5f84259d2c68ae9/coverage-7.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:68b0d0a2d84f333de875666259dadf28cc67858bc8fd8b3f1eae84d3c2bec455", size = 260856, upload-time = "2025-11-18T13:33:26.365Z" }, + { url = "https://files.pythonhosted.org/packages/f2/76/fa2a37bfaeaf1f766a2d2360a25a5297d4fb567098112f6517475eee120b/coverage-7.12.0-cp313-cp313t-win32.whl", hash = "sha256:73f9e7fbd51a221818fd11b7090eaa835a353ddd59c236c57b2199486b116c6d", size = 220936, upload-time = "2025-11-18T13:33:28.165Z" }, + { url = "https://files.pythonhosted.org/packages/f9/52/60f64d932d555102611c366afb0eb434b34266b1d9266fc2fe18ab641c47/coverage-7.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:24cff9d1f5743f67db7ba46ff284018a6e9aeb649b67aa1e70c396aa1b7cb23c", size = 222001, upload-time = "2025-11-18T13:33:29.656Z" }, + { url = "https://files.pythonhosted.org/packages/77/df/c303164154a5a3aea7472bf323b7c857fed93b26618ed9fc5c2955566bb0/coverage-7.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:c87395744f5c77c866d0f5a43d97cc39e17c7f1cb0115e54a2fe67ca75c5d14d", size = 220273, upload-time = "2025-11-18T13:33:31.415Z" }, + { url = "https://files.pythonhosted.org/packages/bf/2e/fc12db0883478d6e12bbd62d481210f0c8daf036102aa11434a0c5755825/coverage-7.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a1c59b7dc169809a88b21a936eccf71c3895a78f5592051b1af8f4d59c2b4f92", size = 217777, upload-time = "2025-11-18T13:33:32.86Z" }, + { url = "https://files.pythonhosted.org/packages/1f/c1/ce3e525d223350c6ec16b9be8a057623f54226ef7f4c2fee361ebb6a02b8/coverage-7.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8787b0f982e020adb732b9f051f3e49dd5054cebbc3f3432061278512a2b1360", size = 218100, upload-time = "2025-11-18T13:33:34.532Z" }, + { url = "https://files.pythonhosted.org/packages/15/87/113757441504aee3808cb422990ed7c8bcc2d53a6779c66c5adef0942939/coverage-7.12.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5ea5a9f7dc8877455b13dd1effd3202e0bca72f6f3ab09f9036b1bcf728f69ac", size = 249151, upload-time = "2025-11-18T13:33:36.135Z" }, + { url = "https://files.pythonhosted.org/packages/d9/1d/9529d9bd44049b6b05bb319c03a3a7e4b0a8a802d28fa348ad407e10706d/coverage-7.12.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fdba9f15849534594f60b47c9a30bc70409b54947319a7c4fd0e8e3d8d2f355d", size = 251667, upload-time = "2025-11-18T13:33:37.996Z" }, + { url = "https://files.pythonhosted.org/packages/11/bb/567e751c41e9c03dc29d3ce74b8c89a1e3396313e34f255a2a2e8b9ebb56/coverage-7.12.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a00594770eb715854fb1c57e0dea08cce6720cfbc531accdb9850d7c7770396c", size = 253003, upload-time = "2025-11-18T13:33:39.553Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b3/c2cce2d8526a02fb9e9ca14a263ca6fc074449b33a6afa4892838c903528/coverage-7.12.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5560c7e0d82b42eb1951e4f68f071f8017c824ebfd5a6ebe42c60ac16c6c2434", size = 249185, upload-time = "2025-11-18T13:33:42.086Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a7/967f93bb66e82c9113c66a8d0b65ecf72fc865adfba5a145f50c7af7e58d/coverage-7.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2e26b481c9159c2773a37947a9718cfdc58893029cdfb177531793e375cfc", size = 251025, upload-time = "2025-11-18T13:33:43.634Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b2/f2f6f56337bc1af465d5b2dc1ee7ee2141b8b9272f3bf6213fcbc309a836/coverage-7.12.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6e1a8c066dabcde56d5d9fed6a66bc19a2883a3fe051f0c397a41fc42aedd4cc", size = 248979, upload-time = "2025-11-18T13:33:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7a/bf4209f45a4aec09d10a01a57313a46c0e0e8f4c55ff2965467d41a92036/coverage-7.12.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f7ba9da4726e446d8dd8aae5a6cd872511184a5d861de80a86ef970b5dacce3e", size = 248800, upload-time = "2025-11-18T13:33:47.546Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b7/1e01b8696fb0521810f60c5bbebf699100d6754183e6cc0679bf2ed76531/coverage-7.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e0f483ab4f749039894abaf80c2f9e7ed77bbf3c737517fb88c8e8e305896a17", size = 250460, upload-time = "2025-11-18T13:33:49.537Z" }, + { url = "https://files.pythonhosted.org/packages/71/ae/84324fb9cb46c024760e706353d9b771a81b398d117d8c1fe010391c186f/coverage-7.12.0-cp314-cp314-win32.whl", hash = "sha256:76336c19a9ef4a94b2f8dc79f8ac2da3f193f625bb5d6f51a328cd19bfc19933", size = 220533, upload-time = "2025-11-18T13:33:51.16Z" }, + { url = "https://files.pythonhosted.org/packages/e2/71/1033629deb8460a8f97f83e6ac4ca3b93952e2b6f826056684df8275e015/coverage-7.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:7c1059b600aec6ef090721f8f633f60ed70afaffe8ecab85b59df748f24b31fe", size = 221348, upload-time = "2025-11-18T13:33:52.776Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5f/ac8107a902f623b0c251abdb749be282dc2ab61854a8a4fcf49e276fce2f/coverage-7.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:172cf3a34bfef42611963e2b661302a8931f44df31629e5b1050567d6b90287d", size = 219922, upload-time = "2025-11-18T13:33:54.316Z" }, + { url = "https://files.pythonhosted.org/packages/79/6e/f27af2d4da367f16077d21ef6fe796c874408219fa6dd3f3efe7751bd910/coverage-7.12.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:aa7d48520a32cb21c7a9b31f81799e8eaec7239db36c3b670be0fa2403828d1d", size = 218511, upload-time = "2025-11-18T13:33:56.343Z" }, + { url = "https://files.pythonhosted.org/packages/67/dd/65fd874aa460c30da78f9d259400d8e6a4ef457d61ab052fd248f0050558/coverage-7.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:90d58ac63bc85e0fb919f14d09d6caa63f35a5512a2205284b7816cafd21bb03", size = 218771, upload-time = "2025-11-18T13:33:57.966Z" }, + { url = "https://files.pythonhosted.org/packages/55/e0/7c6b71d327d8068cb79c05f8f45bf1b6145f7a0de23bbebe63578fe5240a/coverage-7.12.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ca8ecfa283764fdda3eae1bdb6afe58bf78c2c3ec2b2edcb05a671f0bba7b3f9", size = 260151, upload-time = "2025-11-18T13:33:59.597Z" }, + { url = "https://files.pythonhosted.org/packages/49/ce/4697457d58285b7200de6b46d606ea71066c6e674571a946a6ea908fb588/coverage-7.12.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:874fe69a0785d96bd066059cd4368022cebbec1a8958f224f0016979183916e6", size = 262257, upload-time = "2025-11-18T13:34:01.166Z" }, + { url = "https://files.pythonhosted.org/packages/2f/33/acbc6e447aee4ceba88c15528dbe04a35fb4d67b59d393d2e0d6f1e242c1/coverage-7.12.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b3c889c0b8b283a24d721a9eabc8ccafcfc3aebf167e4cd0d0e23bf8ec4e339", size = 264671, upload-time = "2025-11-18T13:34:02.795Z" }, + { url = "https://files.pythonhosted.org/packages/87/ec/e2822a795c1ed44d569980097be839c5e734d4c0c1119ef8e0a073496a30/coverage-7.12.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8bb5b894b3ec09dcd6d3743229dc7f2c42ef7787dc40596ae04c0edda487371e", size = 259231, upload-time = "2025-11-18T13:34:04.397Z" }, + { url = "https://files.pythonhosted.org/packages/72/c5/a7ec5395bb4a49c9b7ad97e63f0c92f6bf4a9e006b1393555a02dae75f16/coverage-7.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:79a44421cd5fba96aa57b5e3b5a4d3274c449d4c622e8f76882d76635501fd13", size = 262137, upload-time = "2025-11-18T13:34:06.068Z" }, + { url = "https://files.pythonhosted.org/packages/67/0c/02c08858b764129f4ecb8e316684272972e60777ae986f3865b10940bdd6/coverage-7.12.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:33baadc0efd5c7294f436a632566ccc1f72c867f82833eb59820ee37dc811c6f", size = 259745, upload-time = "2025-11-18T13:34:08.04Z" }, + { url = "https://files.pythonhosted.org/packages/5a/04/4fd32b7084505f3829a8fe45c1a74a7a728cb251aaadbe3bec04abcef06d/coverage-7.12.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c406a71f544800ef7e9e0000af706b88465f3573ae8b8de37e5f96c59f689ad1", size = 258570, upload-time = "2025-11-18T13:34:09.676Z" }, + { url = "https://files.pythonhosted.org/packages/48/35/2365e37c90df4f5342c4fa202223744119fe31264ee2924f09f074ea9b6d/coverage-7.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e71bba6a40883b00c6d571599b4627f50c360b3d0d02bfc658168936be74027b", size = 260899, upload-time = "2025-11-18T13:34:11.259Z" }, + { url = "https://files.pythonhosted.org/packages/05/56/26ab0464ca733fa325e8e71455c58c1c374ce30f7c04cebb88eabb037b18/coverage-7.12.0-cp314-cp314t-win32.whl", hash = "sha256:9157a5e233c40ce6613dead4c131a006adfda70e557b6856b97aceed01b0e27a", size = 221313, upload-time = "2025-11-18T13:34:12.863Z" }, + { url = "https://files.pythonhosted.org/packages/da/1c/017a3e1113ed34d998b27d2c6dba08a9e7cb97d362f0ec988fcd873dcf81/coverage-7.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e84da3a0fd233aeec797b981c51af1cabac74f9bd67be42458365b30d11b5291", size = 222423, upload-time = "2025-11-18T13:34:15.14Z" }, + { url = "https://files.pythonhosted.org/packages/4c/36/bcc504fdd5169301b52568802bb1b9cdde2e27a01d39fbb3b4b508ab7c2c/coverage-7.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:01d24af36fedda51c2b1aca56e4330a3710f83b02a5ff3743a6b015ffa7c9384", size = 220459, upload-time = "2025-11-18T13:34:17.222Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a3/43b749004e3c09452e39bb56347a008f0a0668aad37324a99b5c8ca91d9e/coverage-7.12.0-py3-none-any.whl", hash = "sha256:159d50c0b12e060b15ed3d39f87ed43d4f7f7ad40b8a534f4dd331adbb51104a", size = 209503, upload-time = "2025-11-18T13:34:18.892Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version >= '3.10' and python_full_version <= '3.11'" }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "docutils" +version = "0.21.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, +] + +[[package]] +name = "docutils" +version = "0.22.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/02/111134bfeb6e6c7ac4c74594e39a59f6c0195dc4846afbeac3cba60f1927/docutils-0.22.3.tar.gz", hash = "sha256:21486ae730e4ca9f622677b1412b879af1791efcfba517e4c6f60be543fc8cdd", size = 2290153, upload-time = "2025-11-06T02:35:55.655Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/a8/c6a4b901d17399c77cd81fb001ce8961e9f5e04d3daf27e8925cb012e163/docutils-0.22.3-py3-none-any.whl", hash = "sha256:bd772e4aca73aff037958d44f2be5229ded4c09927fcf8690c577b66234d6ceb", size = 633032, upload-time = "2025-11-06T02:35:52.391Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "filelock" +version = "3.19.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/40/bb/0ab3e58d22305b6f5440629d20683af28959bf793d98d11950e305c1c326/filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58", size = 17687, upload-time = "2025-08-14T16:56:03.016Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988, upload-time = "2025-08-14T16:56:01.633Z" }, +] + +[[package]] +name = "filelock" +version = "3.20.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922, upload-time = "2025-10-08T18:03:50.056Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "imagesize" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026, upload-time = "2022-07-01T12:21:05.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, + { url = "https://files.pythonhosted.org/packages/56/23/0d8c13a44bde9154821586520840643467aee574d8ce79a17da539ee7fed/markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26", size = 11623, upload-time = "2025-09-27T18:37:29.296Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/07a2cb9a8045d5f3f0890a8c3bc0859d7a47bfd9a560b563899bec7b72ed/markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc", size = 12049, upload-time = "2025-09-27T18:37:30.234Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e4/6be85eb81503f8e11b61c0b6369b6e077dcf0a74adbd9ebf6b349937b4e9/markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c", size = 21923, upload-time = "2025-09-27T18:37:31.177Z" }, + { url = "https://files.pythonhosted.org/packages/6f/bc/4dc914ead3fe6ddaef035341fee0fc956949bbd27335b611829292b89ee2/markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42", size = 20543, upload-time = "2025-09-27T18:37:32.168Z" }, + { url = "https://files.pythonhosted.org/packages/89/6e/5fe81fbcfba4aef4093d5f856e5c774ec2057946052d18d168219b7bd9f9/markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b", size = 20585, upload-time = "2025-09-27T18:37:33.166Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f6/e0e5a3d3ae9c4020f696cd055f940ef86b64fe88de26f3a0308b9d3d048c/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758", size = 21387, upload-time = "2025-09-27T18:37:34.185Z" }, + { url = "https://files.pythonhosted.org/packages/c8/25/651753ef4dea08ea790f4fbb65146a9a44a014986996ca40102e237aa49a/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2", size = 20133, upload-time = "2025-09-27T18:37:35.138Z" }, + { url = "https://files.pythonhosted.org/packages/dc/0a/c3cf2b4fef5f0426e8a6d7fce3cb966a17817c568ce59d76b92a233fdbec/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d", size = 20588, upload-time = "2025-09-27T18:37:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/cd/1b/a7782984844bd519ad4ffdbebbba2671ec5d0ebbeac34736c15fb86399e8/markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7", size = 14566, upload-time = "2025-09-27T18:37:37.09Z" }, + { url = "https://files.pythonhosted.org/packages/18/1f/8d9c20e1c9440e215a44be5ab64359e207fcb4f675543f1cf9a2a7f648d0/markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e", size = 15053, upload-time = "2025-09-27T18:37:38.054Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d3/fe08482b5cd995033556d45041a4f4e76e7f0521112a9c9991d40d39825f/markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8", size = 13928, upload-time = "2025-09-27T18:37:39.037Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pyds8k" +source = { editable = "." } +dependencies = [ + { name = "requests" }, +] + +[package.dev-dependencies] +dev = [ + { name = "ruff" }, + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "sphinx", version = "9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +test = [ + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pytest", version = "9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest-cov" }, + { name = "responses" }, + { name = "tox", version = "4.30.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "tox", version = "4.32.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "tzdata" }, + { name = "tzlocal" }, +] + +[package.metadata] +requires-dist = [{ name = "requests", specifier = ">=2.32.4" }] + +[package.metadata.requires-dev] +dev = [ + { name = "ruff", specifier = ">=0.11.6" }, + { name = "sphinx", specifier = ">=2.2.1" }, +] +test = [ + { name = "pytest", specifier = ">=7.0.0" }, + { name = "pytest-cov", specifier = ">=3.0.0" }, + { name = "responses", specifier = ">=0.24.0" }, + { name = "tox", specifier = ">=4.22.0" }, + { name = "tzdata", specifier = ">=2025.2" }, + { name = "tzlocal", specifier = ">=5.3.1" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyproject-api" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "tomli", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/fd/437901c891f58a7b9096511750247535e891d2d5a5a6eefbc9386a2b41d5/pyproject_api-1.9.1.tar.gz", hash = "sha256:43c9918f49daab37e302038fc1aed54a8c7a91a9fa935d00b9a485f37e0f5335", size = 22710, upload-time = "2025-05-12T14:41:58.025Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/e6/c293c06695d4a3ab0260ef124a74ebadba5f4c511ce3a4259e976902c00b/pyproject_api-1.9.1-py3-none-any.whl", hash = "sha256:7d6238d92f8962773dd75b5f0c4a6a27cce092a14b623b811dba656f3b628948", size = 13158, upload-time = "2025-05-12T14:41:56.217Z" }, +] + +[[package]] +name = "pyproject-api" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/7b/c0e1333b61d41c69e59e5366e727b18c4992688caf0de1be10b3e5265f6b/pyproject_api-1.10.0.tar.gz", hash = "sha256:40c6f2d82eebdc4afee61c773ed208c04c19db4c4a60d97f8d7be3ebc0bbb330", size = 22785, upload-time = "2025-10-09T19:12:27.21Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/cc/cecf97be298bee2b2a37dd360618c819a2a7fd95251d8e480c1f0eb88f3b/pyproject_api-1.10.0-py3-none-any.whl", hash = "sha256:8757c41a79c0f4ab71b99abed52b97ecf66bd20b04fa59da43b5840bac105a09", size = 13218, upload-time = "2025-10-09T19:12:24.428Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "pluggy", marker = "python_full_version < '3.10'" }, + { name = "pygments", marker = "python_full_version < '3.10'" }, + { name = "tomli", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, + { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "pluggy", marker = "python_full_version >= '3.10'" }, + { name = "pygments", marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/56/f013048ac4bc4c1d9be45afd4ab209ea62822fb1598f40687e6bf45dcea4/pytest-9.0.1.tar.gz", hash = "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8", size = 1564125, upload-time = "2025-11-12T13:05:09.333Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/8b/6300fb80f858cda1c51ffa17075df5d846757081d11ab4aa35cef9e6258b/pytest-9.0.1-py3-none-any.whl", hash = "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad", size = 373668, upload-time = "2025-11-12T13:05:07.379Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", version = "7.10.7", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version < '3.10'" }, + { name = "coverage", version = "7.12.0", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version >= '3.10'" }, + { name = "pluggy" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pytest", version = "9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { 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" }, + { url = "https://files.pythonhosted.org/packages/9f/62/67fc8e68a75f738c9200422bf65693fb79a4cd0dc5b23310e5202e978090/pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da", size = 184450, upload-time = "2025-09-25T21:33:00.618Z" }, + { url = "https://files.pythonhosted.org/packages/ae/92/861f152ce87c452b11b9d0977952259aa7df792d71c1053365cc7b09cc08/pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917", size = 174319, upload-time = "2025-09-25T21:33:02.086Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cd/f0cfc8c74f8a030017a2b9c771b7f47e5dd702c3e28e5b2071374bda2948/pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9", size = 737631, upload-time = "2025-09-25T21:33:03.25Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b2/18f2bd28cd2055a79a46c9b0895c0b3d987ce40ee471cecf58a1a0199805/pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5", size = 836795, upload-time = "2025-09-25T21:33:05.014Z" }, + { url = "https://files.pythonhosted.org/packages/73/b9/793686b2d54b531203c160ef12bec60228a0109c79bae6c1277961026770/pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a", size = 750767, upload-time = "2025-09-25T21:33:06.398Z" }, + { url = "https://files.pythonhosted.org/packages/a9/86/a137b39a611def2ed78b0e66ce2fe13ee701a07c07aebe55c340ed2a050e/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926", size = 727982, upload-time = "2025-09-25T21:33:08.708Z" }, + { url = "https://files.pythonhosted.org/packages/dd/62/71c27c94f457cf4418ef8ccc71735324c549f7e3ea9d34aba50874563561/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7", size = 755677, upload-time = "2025-09-25T21:33:09.876Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/6f5e0d58bd924fb0d06c3a6bad00effbdae2de5adb5cda5648006ffbd8d3/pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0", size = 142592, upload-time = "2025-09-25T21:33:10.983Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0c/25113e0b5e103d7f1490c0e947e303fe4a696c10b501dea7a9f49d4e876c/pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007", size = 158777, upload-time = "2025-09-25T21:33:15.55Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "responses" +version = "0.25.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, + { name = "requests" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/95/89c054ad70bfef6da605338b009b2e283485835351a9935c7bfbfaca7ffc/responses-0.25.8.tar.gz", hash = "sha256:9374d047a575c8f781b94454db5cab590b6029505f488d12899ddb10a4af1cf4", size = 79320, upload-time = "2025-08-08T19:01:46.709Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/4c/cc276ce57e572c102d9542d383b2cfd551276581dc60004cb94fe8774c11/responses-0.25.8-py3-none-any.whl", hash = "sha256:0c710af92def29c8352ceadff0c3fe340ace27cf5af1bbe46fb71275bcd2831c", size = 34769, upload-time = "2025-08-08T19:01:45.018Z" }, +] + +[[package]] +name = "roman-numerals" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/5b/1bcda2c6a8acec5b310dd70f732400827b96f05d815834f0f112b91b3539/roman_numerals-3.1.0.tar.gz", hash = "sha256:384e36fc1e8d4bd361bdb3672841faae7a345b3f708aae9895d074c878332551", size = 9069, upload-time = "2025-03-12T00:41:08.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/1d/7356f115a0e5faf8dc59894a3e9fc8b1821ab949163458b0072db0a12a68/roman_numerals-3.1.0-py3-none-any.whl", hash = "sha256:842ae5fd12912d62720c9aad8cab706e8c692556d01a38443e051ee6cc158d90", size = 7709, upload-time = "2025-03-12T00:41:07.626Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/5b/dd7406afa6c95e3d8fa9d652b6d6dd17dd4a6bf63cb477014e8ccd3dcd46/ruff-0.14.7.tar.gz", hash = "sha256:3417deb75d23bd14a722b57b0a1435561db65f0ad97435b4cf9f85ffcef34ae5", size = 5727324, upload-time = "2025-11-28T20:55:10.525Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/b1/7ea5647aaf90106f6d102230e5df874613da43d1089864da1553b899ba5e/ruff-0.14.7-py3-none-linux_armv6l.whl", hash = "sha256:b9d5cb5a176c7236892ad7224bc1e63902e4842c460a0b5210701b13e3de4fca", size = 13414475, upload-time = "2025-11-28T20:54:54.569Z" }, + { url = "https://files.pythonhosted.org/packages/af/19/fddb4cd532299db9cdaf0efdc20f5c573ce9952a11cb532d3b859d6d9871/ruff-0.14.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3f64fe375aefaf36ca7d7250292141e39b4cea8250427482ae779a2aa5d90015", size = 13634613, upload-time = "2025-11-28T20:55:17.54Z" }, + { url = "https://files.pythonhosted.org/packages/40/2b/469a66e821d4f3de0440676ed3e04b8e2a1dc7575cf6fa3ba6d55e3c8557/ruff-0.14.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:93e83bd3a9e1a3bda64cb771c0d47cda0e0d148165013ae2d3554d718632d554", size = 12765458, upload-time = "2025-11-28T20:55:26.128Z" }, + { url = "https://files.pythonhosted.org/packages/f1/05/0b001f734fe550bcfde4ce845948ac620ff908ab7241a39a1b39bb3c5f49/ruff-0.14.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3838948e3facc59a6070795de2ae16e5786861850f78d5914a03f12659e88f94", size = 13236412, upload-time = "2025-11-28T20:55:28.602Z" }, + { url = "https://files.pythonhosted.org/packages/11/36/8ed15d243f011b4e5da75cd56d6131c6766f55334d14ba31cce5461f28aa/ruff-0.14.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24c8487194d38b6d71cd0fd17a5b6715cda29f59baca1defe1e3a03240f851d1", size = 13182949, upload-time = "2025-11-28T20:55:33.265Z" }, + { url = "https://files.pythonhosted.org/packages/3b/cf/fcb0b5a195455729834f2a6eadfe2e4519d8ca08c74f6d2b564a4f18f553/ruff-0.14.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79c73db6833f058a4be8ffe4a0913b6d4ad41f6324745179bd2aa09275b01d0b", size = 13816470, upload-time = "2025-11-28T20:55:08.203Z" }, + { url = "https://files.pythonhosted.org/packages/7f/5d/34a4748577ff7a5ed2f2471456740f02e86d1568a18c9faccfc73bd9ca3f/ruff-0.14.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:12eb7014fccff10fc62d15c79d8a6be4d0c2d60fe3f8e4d169a0d2def75f5dad", size = 15289621, upload-time = "2025-11-28T20:55:30.837Z" }, + { url = "https://files.pythonhosted.org/packages/53/53/0a9385f047a858ba133d96f3f8e3c9c66a31cc7c4b445368ef88ebeac209/ruff-0.14.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c623bbdc902de7ff715a93fa3bb377a4e42dd696937bf95669118773dbf0c50", size = 14975817, upload-time = "2025-11-28T20:55:24.107Z" }, + { url = "https://files.pythonhosted.org/packages/a8/d7/2f1c32af54c3b46e7fadbf8006d8b9bcfbea535c316b0bd8813d6fb25e5d/ruff-0.14.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f53accc02ed2d200fa621593cdb3c1ae06aa9b2c3cae70bc96f72f0000ae97a9", size = 14284549, upload-time = "2025-11-28T20:55:06.08Z" }, + { url = "https://files.pythonhosted.org/packages/92/05/434ddd86becd64629c25fb6b4ce7637dd52a45cc4a4415a3008fe61c27b9/ruff-0.14.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:281f0e61a23fcdcffca210591f0f53aafaa15f9025b5b3f9706879aaa8683bc4", size = 14071389, upload-time = "2025-11-28T20:55:35.617Z" }, + { url = "https://files.pythonhosted.org/packages/ff/50/fdf89d4d80f7f9d4f420d26089a79b3bb1538fe44586b148451bc2ba8d9c/ruff-0.14.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:dbbaa5e14148965b91cb090236931182ee522a5fac9bc5575bafc5c07b9f9682", size = 14202679, upload-time = "2025-11-28T20:55:01.472Z" }, + { url = "https://files.pythonhosted.org/packages/77/54/87b34988984555425ce967f08a36df0ebd339bb5d9d0e92a47e41151eafc/ruff-0.14.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1464b6e54880c0fe2f2d6eaefb6db15373331414eddf89d6b903767ae2458143", size = 13147677, upload-time = "2025-11-28T20:55:19.933Z" }, + { url = "https://files.pythonhosted.org/packages/67/29/f55e4d44edfe053918a16a3299e758e1c18eef216b7a7092550d7a9ec51c/ruff-0.14.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f217ed871e4621ea6128460df57b19ce0580606c23aeab50f5de425d05226784", size = 13151392, upload-time = "2025-11-28T20:55:21.967Z" }, + { url = "https://files.pythonhosted.org/packages/36/69/47aae6dbd4f1d9b4f7085f4d9dcc84e04561ee7ad067bf52e0f9b02e3209/ruff-0.14.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6be02e849440ed3602d2eb478ff7ff07d53e3758f7948a2a598829660988619e", size = 13412230, upload-time = "2025-11-28T20:55:12.749Z" }, + { url = "https://files.pythonhosted.org/packages/b7/4b/6e96cb6ba297f2ba502a231cd732ed7c3de98b1a896671b932a5eefa3804/ruff-0.14.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:19a0f116ee5e2b468dfe80c41c84e2bbd6b74f7b719bee86c2ecde0a34563bcc", size = 14195397, upload-time = "2025-11-28T20:54:56.896Z" }, + { url = "https://files.pythonhosted.org/packages/69/82/251d5f1aa4dcad30aed491b4657cecd9fb4274214da6960ffec144c260f7/ruff-0.14.7-py3-none-win32.whl", hash = "sha256:e33052c9199b347c8937937163b9b149ef6ab2e4bb37b042e593da2e6f6cccfa", size = 13126751, upload-time = "2025-11-28T20:55:03.47Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b5/d0b7d145963136b564806f6584647af45ab98946660d399ec4da79cae036/ruff-0.14.7-py3-none-win_amd64.whl", hash = "sha256:e17a20ad0d3fad47a326d773a042b924d3ac31c6ca6deb6c72e9e6b5f661a7c6", size = 14531726, upload-time = "2025-11-28T20:54:59.121Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d2/1637f4360ada6a368d3265bf39f2cf737a0aaab15ab520fc005903e883f8/ruff-0.14.7-py3-none-win_arm64.whl", hash = "sha256:be4d653d3bea1b19742fcc6502354e32f65cd61ff2fbdb365803ef2c2aec6228", size = 13609215, upload-time = "2025-11-28T20:55:15.375Z" }, +] + +[[package]] +name = "snowballstemmer" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, +] + +[[package]] +name = "sphinx" +version = "7.4.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "alabaster", version = "0.7.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "babel", marker = "python_full_version < '3.10'" }, + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "imagesize", marker = "python_full_version < '3.10'" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "jinja2", marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "pygments", marker = "python_full_version < '3.10'" }, + { name = "requests", marker = "python_full_version < '3.10'" }, + { name = "snowballstemmer", marker = "python_full_version < '3.10'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.10'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.10'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.10'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.10'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.10'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.10'" }, + { name = "tomli", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/be/50e50cb4f2eff47df05673d361095cafd95521d2a22521b920c67a372dcb/sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe", size = 8067911, upload-time = "2024-07-20T14:46:56.059Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/ef/153f6803c5d5f8917dbb7f7fcf6d34a871ede3296fa89c2c703f5f8a6c8e/sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239", size = 3401624, upload-time = "2024-07-20T14:46:52.142Z" }, +] + +[[package]] +name = "sphinx" +version = "8.1.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "babel", marker = "python_full_version == '3.10.*'" }, + { name = "colorama", marker = "python_full_version == '3.10.*' and sys_platform == 'win32'" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "imagesize", marker = "python_full_version == '3.10.*'" }, + { name = "jinja2", marker = "python_full_version == '3.10.*'" }, + { name = "packaging", marker = "python_full_version == '3.10.*'" }, + { name = "pygments", marker = "python_full_version == '3.10.*'" }, + { name = "requests", marker = "python_full_version == '3.10.*'" }, + { name = "snowballstemmer", marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version == '3.10.*'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611, upload-time = "2024-10-13T20:27:13.93Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/60/1ddff83a56d33aaf6f10ec8ce84b4c007d9368b21008876fceda7e7381ef/sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2", size = 3487125, upload-time = "2024-10-13T20:27:10.448Z" }, +] + +[[package]] +name = "sphinx" +version = "9.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", +] +dependencies = [ + { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "babel", marker = "python_full_version >= '3.11'" }, + { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, + { name = "docutils", version = "0.22.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "imagesize", marker = "python_full_version >= '3.11'" }, + { name = "jinja2", marker = "python_full_version >= '3.11'" }, + { name = "packaging", marker = "python_full_version >= '3.11'" }, + { name = "pygments", marker = "python_full_version >= '3.11'" }, + { name = "requests", marker = "python_full_version >= '3.11'" }, + { name = "roman-numerals", marker = "python_full_version >= '3.11'" }, + { name = "snowballstemmer", marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/f5/7aedfbc7833b7e40c9c921c4f813e22c0abc1d8ee5f778c479c102001d7f/sphinx-9.0.1.tar.gz", hash = "sha256:c820d856657ce7cd41ce2c097f478ac3d7ddad9779ad83de4f0136a81ff388fd", size = 8708618, upload-time = "2025-12-01T16:43:36.878Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/12/1b79cd17cca23962a4a973b04afbe7008d6bec51a9b39db1b563e63f427e/sphinx-9.0.1-py3-none-any.whl", hash = "sha256:5a9506b709d6bd3d4c3f142b5b58801c202837b54875727e3adcba98d4839735", size = 3916414, upload-time = "2025-12-01T16:43:34.706Z" }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, +] + +[[package]] +name = "tomli" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, +] + +[[package]] +name = "tox" +version = "4.30.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "cachetools", marker = "python_full_version < '3.10'" }, + { name = "chardet", marker = "python_full_version < '3.10'" }, + { name = "colorama", marker = "python_full_version < '3.10'" }, + { name = "filelock", version = "3.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pluggy", marker = "python_full_version < '3.10'" }, + { name = "pyproject-api", version = "1.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "tomli", marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, + { name = "virtualenv", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/b2/cee55172e5e10ce030b087cd3ac06641e47d08a3dc8d76c17b157dba7558/tox-4.30.3.tar.gz", hash = "sha256:f3dd0735f1cd4e8fbea5a3661b77f517456b5f0031a6256432533900e34b90bf", size = 202799, upload-time = "2025-10-02T16:24:39.974Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/e4/8bb9ce952820df4165eb34610af347665d6cb436898a234db9d84d093ce6/tox-4.30.3-py3-none-any.whl", hash = "sha256:a9f17b4b2d0f74fe0d76207236925a119095011e5c2e661a133115a8061178c9", size = 175512, upload-time = "2025-10-02T16:24:38.209Z" }, +] + +[[package]] +name = "tox" +version = "4.32.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "cachetools", marker = "python_full_version >= '3.10'" }, + { name = "chardet", marker = "python_full_version >= '3.10'" }, + { name = "colorama", marker = "python_full_version >= '3.10'" }, + { name = "filelock", version = "3.20.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "platformdirs", version = "4.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pluggy", marker = "python_full_version >= '3.10'" }, + { name = "pyproject-api", version = "1.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, + { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, + { name = "virtualenv", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/bf/0e4dbd42724cbae25959f0e34c95d0c730df03ab03f54d52accd9abfc614/tox-4.32.0.tar.gz", hash = "sha256:1ad476b5f4d3679455b89a992849ffc3367560bbc7e9495ee8a3963542e7c8ff", size = 203330, upload-time = "2025-10-24T18:03:38.132Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/cc/e09c0d663a004945f82beecd4f147053567910479314e8d01ba71e5d5dea/tox-4.32.0-py3-none-any.whl", hash = "sha256:451e81dc02ba8d1ed20efd52ee409641ae4b5d5830e008af10fe8823ef1bd551", size = 175905, upload-time = "2025-10-24T18:03:36.337Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, +] + +[[package]] +name = "tzlocal" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] + +[[package]] +name = "virtualenv" +version = "20.35.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock", version = "3.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "filelock", version = "3.20.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "platformdirs", version = "4.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/28/e6f1a6f655d620846bd9df527390ecc26b3805a0c5989048c210e22c5ca9/virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c", size = 6028799, upload-time = "2025-10-29T06:57:40.511Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +]