From 6a865bb91dbbcd6a4bea5773df113fd479cccf33 Mon Sep 17 00:00:00 2001 From: Charlotte Kostelic Date: Tue, 26 Mar 2024 15:29:00 -0400 Subject: [PATCH] Releases/v1.0.0 (#92) * move cov & pytest config to pyproject.toml * remove temp file * remove pytest-recording * add py311 * dependencies updates * drop py3.7 * copy main config * drop py3.7 * add requests to dev dependencies * fix missing dependencies * dependencies final update * version bump to 1.0.0 * adds all dependencies * add py3.12 * typing cleanup & tests refactoring * Authentication updates (#69) * ingnore E501 in tests * add types-requests to dev dependencies * scopes as str * scopes as str & types cleanup * dev dependencies moved to tool.poetry.group.dev.dependencies section * token_expires_at as datetime obj * token_expires_at as datetime * changed utcnow to now(timezone.utc) (#71) * Ocn-parsing-refactor (#72) * prep_oclc_numbe_strr refactor * oclcNumber stripped * verification refactor * utc fixes (#73) * fixed datetime type errors * moved datetime edits to _hasten_expiration_time * Errors-refactor (#74) * removed WorldcatSessionError * incorrect AttributeError replaced with TypeError * replaces WorldcatAuthorizationError with TypeError and ValueError for configuration * removed unused WorldcatAuthorizationError import * removed unused WorldcatRequestError import * added safe decoding for bytes-str * ignore F401 * None type added to possible timeout types * added type ingnore * removed unused InvalidOclcNumber import & typing fixes * changed endpoints in metadata api 2.0 (#77) * changed endpoints in metadata api 2.0 * fixed tests with typos * changed response_format default in get_full_bib * Changed search endpoints (#78) * changed search endpoints in metadata api 2.0 * fixed types * fixed spacing and indentation * type hint fixes and refactored test * Removed principalID and principalIDNS from token requests (#79) * removed unnecessary params from token requests * fixed docstring * MetadataSession cleanup (#80) * reordered methods in MetadataSession * removed obsolete 409 error handling from query.py * simplified changes * added new api endpoints (#81) * added new api endpoints * added tests * added test * added to doc string, fixed typos (#82) @charlottekostelic This is something that should be brought to users attention in the documentation. Will create an issue as a reminder. Besides that, looks good. Thanks! * Query updates (#84) * work in progress * added retries to query * removed test with stale token * added stale token test back in * added stale token test back in * moved retries to _session module, added tests * added retry status_forcelist tests * added custom adapter test * changed default retry behavior * added another test * more testing * Reordered metadata methods (#85) * renamed/reordered metadata_api methods * fixed optional/required args, added to doc strings * fixed default values to match API defaults * fixed error in live test * dev status update to 5, removes py3.7 & adds py3.11 & py3.12 (#87) * Update docs (#88) * reorganized docs, added mkdocs-material theme * added css for NYPL colors * reorganized docs, added examples * changed structure of docs, added to docs * changed snake case to camel case in args * typo fixes * added contributing.md, python versions for black * added 1.0 to changelog, migration section in docs * Added migration section to README * typo fixes, link fixes * added py.typed file * fixed links, made edits per PR 88 * added mkdocstrings, removed mkapi, doc edits (#91) * edited changelog * pyproject.toml edits * update unit-tests.yaml * fixed unit-tests.yaml * unit-tests.yaml indentation issues --------- Co-authored-by: klinga --- .coveragerc | 3 - .flake8 | 3 + .github/workflows/unit-tests.yaml | 15 +- .gitignore | 3 + README.md | 232 ++-- bookops_worldcat/__init__.py | 6 +- bookops_worldcat/__version__.py | 2 +- bookops_worldcat/_session.py | 81 +- bookops_worldcat/authorize.py | 138 +- bookops_worldcat/errors.py | 10 +- bookops_worldcat/metadata_api.py | 2115 ++++++++++++++++++++--------- bookops_worldcat/py.typed | 0 bookops_worldcat/query.py | 40 +- bookops_worldcat/utils.py | 51 +- dev-requirements.txt | 754 +++++----- docs/about.md | 5 +- docs/advanced.md | 141 ++ docs/api/authorize.md | 1 + docs/api/errors.md | 1 + docs/api/metadata_api.md | 1 + docs/api/query.md | 1 + docs/api/utils.md | 1 + docs/changelog.md | 90 +- docs/contributing.md | 88 ++ docs/index.md | 673 +++------ docs/local.md | 499 +++++++ docs/manage_bibs.md | 354 +++++ docs/manage_holdings.md | 159 +++ docs/search.md | 430 ++++++ docs/start.md | 36 + docs/stylesheets/extra.css | 6 + docs/tutorials.md | 39 +- mkdocs.yml | 81 +- poetry.lock | 1267 +++++++++++------ pyproject.toml | 53 +- pytest.ini | 5 - requirements.txt | 118 +- tests/conftest.py | 64 +- tests/test_authorize.py | 170 +-- tests/test_metadata_api.py | 1286 +++++++++--------- tests/test_query.py | 62 +- tests/test_session.py | 61 +- tests/test_utils.py | 37 +- tests/test_version.py | 2 +- 44 files changed, 6115 insertions(+), 3069 deletions(-) delete mode 100644 .coveragerc create mode 100644 .flake8 create mode 100644 bookops_worldcat/py.typed create mode 100644 docs/advanced.md create mode 100644 docs/api/authorize.md create mode 100644 docs/api/errors.md create mode 100644 docs/api/metadata_api.md create mode 100644 docs/api/query.md create mode 100644 docs/api/utils.md create mode 100644 docs/contributing.md create mode 100644 docs/local.md create mode 100644 docs/manage_bibs.md create mode 100644 docs/manage_holdings.md create mode 100644 docs/search.md create mode 100644 docs/start.md create mode 100644 docs/stylesheets/extra.css delete mode 100644 pytest.ini diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 6dfebb5..0000000 --- a/.coveragerc +++ /dev/null @@ -1,3 +0,0 @@ -[run] -omit = bookops_worldcat/temp.py -relative_files = True \ No newline at end of file diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..28639c5 --- /dev/null +++ b/.flake8 @@ -0,0 +1,3 @@ +[flake8] +per-file-ignores = + tests/*:E501, W503 \ No newline at end of file diff --git a/.github/workflows/unit-tests.yaml b/.github/workflows/unit-tests.yaml index 8cefe44..f070ffb 100644 --- a/.github/workflows/unit-tests.yaml +++ b/.github/workflows/unit-tests.yaml @@ -13,10 +13,10 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 - - name: Set up Python + - name: Set up Python ${{ matrix.python-version}} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version}} @@ -29,4 +29,13 @@ jobs: - name: Send report to Coveralls uses: AndreMiras/coveralls-python-action@develop with: - github-token: ${{ secrets.GITHUB_TOKEN}} \ No newline at end of file + parallel: true + github-token: ${{ secrets.GITHUB_TOKEN}} + finish: + needs: test + runs-on: ubuntu-latest + steps: + - name: Coveralls Finished + uses: AndreMiras/coveralls-python-action@develop + with: + parallel-finished: true \ No newline at end of file diff --git a/.gitignore b/.gitignore index ad9ead8..0a65dac 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,6 @@ temp/ 1.0/ venv-activate.sh venvcmd.sh + +# OSX +.DS_Store \ No newline at end of file diff --git a/README.md b/README.md index 53f2454..36973fa 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,10 @@ [![Build Status](https://github.com/BookOps-CAT/bookops-marc/actions/workflows/unit-tests.yaml/badge.svg?branch=main)](https://github.com/BookOps-CAT/bookops-worldcat/actions) [![Coverage Status](https://coveralls.io/repos/github/BookOps-CAT/bookops-worldcat/badge.svg?branch=main)](https://coveralls.io/github/BookOps-CAT/bookops-worldcat?branch=main) [![PyPI version](https://badge.fury.io/py/bookops-worldcat.svg)](https://badge.fury.io/py/bookops-worldcat) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/bookops-worldcat) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) # bookops-worldcat -**Early ALPHA version** +BookOps-Worldcat provides a Python interface for the WorldCat Metadata API. This wrapper simplifies requests to OCLC web services making them more accessible to OCLC member libraries. -BookOps-Worldcat provides a Python interface for the WorldCat Metadata API. -This wrapper simplifies requests to OCLC web services making them ideally more accessible to OCLC member libraries. - -Due to major changes introduced by OCLC in May 2020, the version 0.3.0 of the wrapper dropped functionality related to WorldCat Search API. New search endopoints of the Metadata API supported in the 0.3.0 version should fill that gap. While WorldCat Metadata API is our primary focus, we plan in the future to expand wrapper's functionality to other related OCLC web services, including the now dropped Search API. +Bookops-Worldcat version 1.0 supports changes released in version 2.0 (May 2023) of the OCLC Metadata API. ## Installation @@ -21,146 +18,145 @@ For full documentation please see https://bookops-cat.github.io/bookops-worldcat ## Features -This package takes advantage of the functionality of the popular [Requests library](https://requests.readthedocs.io/en/master/). Interactions with [OCLC](https://www.oclc.org/en/home.html)'s services are built around 'Requests' sessions. Authorizing a web service session simply requires passing an access token to `MetadataSession`. Opening a session allows the user to call specific methods to facilitate communication between the user's script/client and particular endpoint of OCLC API service. Many of the hurdles related to making valid requests are hidden under the hood of this package, making it as simple as possible. -Please note, not all endpoints of the Metadata API are implemented at the moment. This tool was primarily built for the specific needs of BookOps but we are open to collaboration to expand and improve this package. - -At the moment, BookOps-Worldcat supports requests to following OCLC's web services: - -+ [Authentication via Client Credential Grant](https://www.oclc.org/developer/develop/authentication/oauth/client-credentials-grant.en.html) -+ [Worldcat Metadata API](https://www.oclc.org/developer/develop/web-services/worldcat-metadata-api.en.html) - + [Metadata API Search Functionality](https://developer.api.oclc.org/wc-metadata-v1-1) - + member shared print holdings - + member general holdings - + searching bibliographic resources: - + search brief bibs - + retrieve specific brief bib - + retrieve other editions related to a specific bibliographic resource - + [Metadata API](https://developer.api.oclc.org/wc-metadata) - + bibliographic records - + retrieve full bib - + find current OCLC number - + holdings - + set institution holding for a single resource - + unset institution holding for a single resource - + retrieve holding status of a single resource - + set institution holdings for a batch of resources - + unset institution holdings for a batch of resouces - + set holdings for a single resource for multiple institutions - + unset holdings for a single resource for multiple institutions - - -#### Basic usage: - -Obtaining access token +Bookops-Worldcat takes advantage of the functionality of the popular [Requests library](https://requests.readthedocs.io/) and interactions with OCLC's services are built around 'Requests' sessions. `MetadataSession` inherits all `requests.Session` properties. Server responses are `requests.Response` objects with [all of their properties and methods](https://requests.readthedocs.io/en/latest/user/quickstart/). + +Authorizing a web service session simply requires passing an access token to `MetadataSession`. Opening a session allows the user to call specific methods to facilitate communication between the user's script/client and a particular endpoint of the Metadata API. Many of the hurdles related to making valid requests are hidden under the hood of this package, making it as simple as possible. + +BookOps-Worldcat supports requests to all endpoints of the WorldCat Metadata API 2.0 and Authentication using the [Client Credential Grant](https://www.oclc.org/developer/api/keys/oauth/client-credentials-grant.en.html) flow: + ++ [Authentication via Client Credential Grant](https://www.oclc.org/developer/api/keys/oauth/client-credentials-grant.en.html) ++ [Worldcat Metadata API](https://www.oclc.org/developer/api/oclc-apis/worldcat-metadata-api.en.html) + + Manage Bibliographic Records + + Manage Institution Holdings + + Manage Local Bibliographic Data + + Manage Local Holdings Records + + Search Member Shared Print Holdings + + Search Member General Holdings + + Search Bibliographic Resources + + Search Local Holdings Resources + + Search Local Bibliographic Resources + +### Basic usage: + +Authorizing a MetadataSession ```python ->>> from bookops_worldcat import WorldcatAccessToken ->>> token = WorldcatAccessToken( - key="my_WSkey", - secret="my_WSsecret", - scopes="selected_scope", - principal_id="my_principalID", - principal_idns="my_principalIDNS", - agent="my_client" - ) ->>> print(token.token_str) - "tk_Yebz4BpEp9dAsghA7KpWx6dYD1OZKWBlHjqW" +from bookops_worldcat import WorldcatAccessToken +token = WorldcatAccessToken( + key="my_WSKey", + secret="my_secret", + scopes="WorldCatMetadataAPI", +) +print(token) +#>"access_token: 'tk_Yebz4BpEp9dAsghA7KpWx6dYD1OZKWBlHjqW', expires_at: '2024-01-01 12:00:00Z'" +print(token.is_expired()) +#>False ``` -Metadata API +Search for brief bibliographic resources ```python ->>> from bookops_worldcat import MetadataSession ->>> session = MetadataSession(authorization=token) ->>> result = session.get_brief_bib(oclcNumber=1143317889) ->>> print(result) - ->>> print(result.json()) +with MetadataSession(authorization=token) as session: + response = session.brief_bibs_search(q="ti:The Power Broker AND au: Caro, Robert") + print(response.json()) ``` ```json { - "oclcNumber": "1143317889", - "title": "Blueprint : the evolutionary origins of a good society", - "creator": "Nicholas A. Christakis", - "date": "2020", - "language": "eng", - "generalFormat": "Book", - "specificFormat": "PrintBook", - "edition": "First Little, Brown Spark trade paperback edition.", - "publisher": "Little, Brown Spark", - "catalogingInfo": { - "catalogingAgency": "NYP", - "transcribingAgency": "NYP" - } + "numberOfRecords": 89, + "briefRecords": [ + { + "oclcNumber": "1631862", + "title": "The power broker : Robert Moses and the fall of New York", + "creator": "Robert A. Caro", + "date": "1975", + "machineReadableDate": "1975", + "language": "eng", + "generalFormat": "Book", + "specificFormat": "PrintBook", + "edition": "Vintage Books edition", + "publisher": "Vintage Books", + "catalogingInfo": { + "catalogingAgency": "DLC", + "catalogingLanguage": "eng", + "levelOfCataloging": " ", + "transcribingAgency": "DLC" + } + } + ] } ``` - -Using a context manager: +MetadataSession as Context Manager: ```python with MetadataSession(authorization=token) as session: - results = session.get_full_bib(1143317889) - print(results.text) + result = session.bib_get("1631862") + print(result.text) ``` ```xml - - - - - - 00000cam a2200000 i 4500 - on1143317889 - OCoLC - 20200328101446.1 - 200305t20202019nyuabf b 001 0 eng c - - 2018957420 + + + 00000cam a2200000 i 4500 + ocm01631862 + OCoLC + 20240201163642.4 + 750320t19751974nyuabf b 001 0beng + + 75009557 - - NYP - eng - rda - NYP - - 9780316230049 - (pbk.) + + 9780394720241 + (paperback) - - Christakis, Nicholas A., - author. + + Caro, Robert A., + author. - - Blueprint : - the evolutionary origins of a good society / - Nicholas A. Christakis. + + The power broker : + Robert Moses and the fall of New York / + by Robert A. Caro. - - First Little, Brown Spark trade paperback edition. + + Robert Moses and the fall of New York - - New York, NY : - Little, Brown Spark, - 2020 + + Vintage Books edition. + + + New York : + Vintage Books, + 1975. - - - - http://worldcat.org/oclc/1143317889 - - + ``` +## Changes in Version 1.0 + +New functionality available in version 1.0: + ++ Send requests to all endpoints of WorldCat Metadata API + + Match bib records and retrieve bib classification + + Create, update, and validate bib records + + Create, retrieve, update, and delete local bib and holdings records ++ Add automatic retries to failed requests ++ Authenticate and authorize for multiple institutions within `MetadataSession` ++ Support for Python 3.11 and 3.12 ++ Dropped support for Python 3.7 + +### Migration Information +Bookops-Worldcat 1.0 introduces many breaking changes for users of previous versions. Due to a complete refactor of the Metadata API, the methods from Bookops-Worldcat 0.5.0 have been rewritten. Most of the functionality from previous versions of the Metadata API is still available in Version 2.0. For a comparison of the functionality available in Versions 1.0, 1.1, and 2.0 of the Metadata API, see [OCLC's documentation](https://www.oclc.org/developer/api/oclc-apis/worldcat-metadata-api.en.html) and their [functionality comparison table](https://www.oclc.org/content/dam/developer-network/worldcat-metadata-api/worldcat-metadata-api-functionality-comparison.pdf). + +Versions 1.0 and 1.1 of the Metadata API will be sunset after April 30, 2024 at which point tools that rely on Bookops-Worldcat 0.5 will no longer be able to query the Metadata API. + +For more information on changes made in Version 1.0, see [Features in Version 1.0](https://bookops-cat.github.io/bookops-worldcat/latest/#features-in-version-1.0) in the docs. + ## Changelog -Consult the [Changelog page](https://bookops-cat.github.io/bookops-worldcat/latest/changelog/) for fixes and enhancements of each version. +Consult the [Changelog page](https://bookops-cat.github.io/bookops-worldcat/latest/changelog/) for a full list of fixes and enhancements for each version. ## Bugs/Requests -Please use [Github issue tracker](https://github.com/BookOps-CAT/bookops-worldcat/issues) to submit bugs or request features. +Please use the [Github issue tracker](https://github.com/BookOps-CAT/bookops-worldcat/issues) to submit bugs or request features. -## Todo +## Contributing -+ Metadata API: - + support for local holdings resources endpoints of the search functionality of the Metadata API - + support for local bibliographic data endpoints - + record validation endpoints - + methods to create and update bibliographic records +See [Contribution Guidelines](https://bookops-cat.github.io/bookops-worldcat/latest/contributing) for information on how to contribute to bookops-worldcat. \ No newline at end of file diff --git a/bookops_worldcat/__init__.py b/bookops_worldcat/__init__.py index 15d26cf..edbf411 100644 --- a/bookops_worldcat/__init__.py +++ b/bookops_worldcat/__init__.py @@ -1,4 +1,4 @@ -from .__version__ import __title__, __version__ +from .__version__ import __title__, __version__ # noqa: F401 -from .authorize import WorldcatAccessToken -from .metadata_api import MetadataSession +from .authorize import WorldcatAccessToken # noqa: F401 +from .metadata_api import MetadataSession # noqa: F401 diff --git a/bookops_worldcat/__version__.py b/bookops_worldcat/__version__.py index 80422d6..33f6473 100644 --- a/bookops_worldcat/__version__.py +++ b/bookops_worldcat/__version__.py @@ -1,4 +1,4 @@ __title__ = "bookops-worldcat" -__version__ = "0.5.0" +__version__ = "1.0.0" __author__ = "Tomasz Kalata" __author_email__ = "klingaroo@gmail.com" diff --git a/bookops_worldcat/_session.py b/bookops_worldcat/_session.py index 0518ef3..04a32ee 100644 --- a/bookops_worldcat/_session.py +++ b/bookops_worldcat/_session.py @@ -5,13 +5,13 @@ and others. """ -from typing import Optional, Tuple, Union +from typing import Optional, Tuple, Union, List import requests +from urllib3.util import Retry -from . import __title__, __version__ # type: ignore +from . import __title__, __version__ from .authorize import WorldcatAccessToken -from .errors import WorldcatSessionError, WorldcatAuthorizationError class WorldcatSession(requests.Session): @@ -21,9 +21,14 @@ def __init__( self, authorization: WorldcatAccessToken, agent: Optional[str] = None, - timeout: Optional[ - Union[int, float, Tuple[int, int], Tuple[float, float]] - ] = None, + timeout: Union[int, float, Tuple[int, int], Tuple[float, float], None] = ( + 5, + 5, + ), + totalRetries: int = 0, + backoffFactor: float = 0, + statusForcelist: Optional[List[int]] = None, + allowedMethods: Optional[List[str]] = None, ) -> None: """ Args: @@ -32,27 +37,68 @@ def __init__( request in the session timeout: how long to wait for server to send data before giving up + totalRetries: optional number of times to retry a request that + failed or timed out. if totalRetries argument is + not passed, any arguments passed to + backoffFactor, statusForcelist, and + allowedMethods will be ignored. default is 0 + backoffFactor: if totalRetries is not 0, the backoff + factor as a float to use to calculate amount of + time session will sleep before attempting request + again. default is 0 + statusForcelist: if totalRetries is not 0, a list of HTTP + status codes to automatically retry requests on. + if not specified, failed requests with status codes + 413, 429, and 503 will be retried up to number of + totalRetries. + example: [500, 502, 503, 504] + allowedMethods: if totalRetries is not 0, set of HTTP methods that + requests should be retried on. if not specified, + requests using any HTTP method verbs will be + retried. example: ["GET", "POST"] """ super().__init__() - self.authorization = authorization if not isinstance(self.authorization, WorldcatAccessToken): - raise WorldcatSessionError( + raise TypeError( "Argument 'authorization' must be 'WorldcatAccessToken' object." ) if agent is None: self.headers.update({"User-Agent": f"{__title__}/{__version__}"}) - elif type(agent) is str: + elif agent and isinstance(agent, str): self.headers.update({"User-Agent": agent}) else: - raise WorldcatSessionError("Argument 'agent' must be a str") + raise ValueError("Argument 'agent' must be a string.") - if timeout is None: - self.timeout = (5, 5) - else: - self.timeout = timeout # type: ignore + self.timeout = timeout + + # if user provides retry args, create Retry object and mount adapter to session + if totalRetries != 0: + if statusForcelist is None: + retries = Retry( + total=totalRetries, + backoff_factor=backoffFactor, + status_forcelist=Retry.RETRY_AFTER_STATUS_CODES, + allowed_methods=allowedMethods, + ) + elif ( + isinstance(statusForcelist, List) + and all(isinstance(x, int) for x in statusForcelist) + and len(statusForcelist) > 0 + ): + retries = Retry( + total=totalRetries, + backoff_factor=backoffFactor, + status_forcelist=statusForcelist, + allowed_methods=allowedMethods, + ) + else: + raise ValueError( + "Argument 'statusForcelist' must be a list of integers." + ) + self.mount("https://", requests.adapters.HTTPAdapter(max_retries=retries)) self._update_authorization() @@ -61,11 +107,8 @@ def _get_new_access_token(self) -> None: Allows to continue sending request with new access token after the previous one expired """ - try: - self.authorization._request_token() - self._update_authorization() - except WorldcatAuthorizationError as exc: - raise WorldcatSessionError(exc) + self.authorization._request_token() + self._update_authorization() def _update_authorization(self) -> None: self.headers.update({"Authorization": f"Bearer {self.authorization.token_str}"}) diff --git a/bookops_worldcat/authorize.py b/bookops_worldcat/authorize.py index 3943c50..6945ee3 100644 --- a/bookops_worldcat/authorize.py +++ b/bookops_worldcat/authorize.py @@ -6,13 +6,11 @@ import datetime import sys -from typing import Dict, List, Optional, Tuple, Union +from typing import Dict, Optional, Tuple, Union import requests -from requests import Response - -from . import __title__, __version__ # type: ignore +from . import __title__, __version__ from .errors import WorldcatAuthorizationError @@ -23,15 +21,22 @@ class WorldcatAccessToken: Explicit Authorization Code and Refresh Token flows. Token with correctly bonded scopes can then be passed into a session of particular web service to authorize requests for resources. - More on OCLC's web services authorization: - https://www.oclc.org/developer/develop/authentication/oauth/client-credentials-grant.en.html + More on OCLC's client credentials grant: + https://www.oclc.org/developer/api/keys/oauth/client-credentials-grant.en.html Args: key: your WSKey public client_id secret: your WSKey secret - scopes: request scopes for the access token - principal_id: principalID (required for read/write endpoints) - principal_idns: principalIDNS (required for read/write endpoints) + scopes: request scopes for the access token as a string, + separate different scopes with space + users with WSKeys set up to act as multiple institutions + should provide scope and registryID in the format + "{scope} context:{registryID}" + examples: + single institution WSKey: + "WorldCatMetadataAPI" + multi-institution WSKey: + "WorldCatMetadataAPI context:00001" agent: "User-agent" parameter to be passed in the request header; usage strongly encouraged timeout: how long to wait for server to send data before @@ -43,9 +48,7 @@ class WorldcatAccessToken: >>> token = WorldcatAccessToken( key="my_WSKey_client_id", secret="my_WSKey_secret", - scope="WorldCatMetadataAPI", - principal_id="your principalID here", - principal_idns="your principalIDNS here", + scopes="WorldCatMetadataAPI", agent="my_app/1.0.0") >>> token.token_str "tk_Yebz4BpEp9dAsghA7KpWx6dYD1OZKWBlHjqW" @@ -75,10 +78,8 @@ def __init__( self, key: str, secret: str, - scopes: Union[str, List[str]], - principal_id: str, - principal_idns: str, - agent: Optional[str] = None, + scopes: str, + agent: str = "", timeout: Optional[ Union[int, float, Tuple[int, int], Tuple[float, float]] ] = None, @@ -89,55 +90,41 @@ def __init__( self.grant_type = "client_credentials" self.key = key self.oauth_server = "https://oauth.oclc.org" - self.principal_id = principal_id - self.principal_idns = principal_idns self.scopes = scopes self.secret = secret - self.server_response = None + self.server_response: Optional[requests.Response] = None self.timeout = timeout - self.token_expires_at = None - self.token_str = None - self.token_type = None + self.token_expires_at: Optional[datetime.datetime] = None + self.token_str = "" + self.token_type = "" # default bookops-worldcat request header - if self.agent is None: - self.agent = f"{__title__}/{__version__}" + if isinstance(self.agent, str): + if not self.agent.strip(): + self.agent = f"{__title__}/{__version__}" else: - if type(self.agent) is not str: - raise WorldcatAuthorizationError("Argument 'agent' must be a string.") + raise TypeError("Argument 'agent' must be a string.") - # asure passed arguments are valid - if not self.key: - raise WorldcatAuthorizationError("Argument 'key' is required.") + # ensure passed arguments are valid + if isinstance(self.key, str): + if not self.key.strip(): + raise ValueError("Argument 'key' cannot be an empty string.") else: - if type(self.key) is not str: - raise WorldcatAuthorizationError("Argument 'key' must be a string.") + raise TypeError("Argument 'key' must be a string.") - if not self.secret: - raise WorldcatAuthorizationError("Argument 'secret' is required.") + if isinstance(self.secret, str): + if not self.secret.strip(): + raise ValueError("Argument 'secret' cannot be an empty string.") else: - if type(self.secret) is not str: - raise WorldcatAuthorizationError("Argument 'secret' must be a string.") - - if not self.principal_id: - raise WorldcatAuthorizationError( - "Argument 'principal_id' is required for read/write endpoint of Metadata API." - ) - if not self.principal_idns: - raise WorldcatAuthorizationError( - "Argument 'principal_idns' is required for read/write endpoint of Metadata API." - ) + raise TypeError("Argument 'secret' must be a string.") # validate passed scopes - if type(self.scopes) is list: - self.scopes = " ".join(self.scopes) - elif type(self.scopes) is not str: - raise WorldcatAuthorizationError( - "Argument 'scopes' must a string or a list." - ) - self.scopes = self.scopes.strip() # type: ignore - if self.scopes == "": - raise WorldcatAuthorizationError("Argument 'scope' is missing.") + if isinstance(self.scopes, str): + if not self.scopes.strip(): + raise ValueError("Argument 'scopes' cannot be an empty string.") + else: + raise TypeError("Argument 'scopes' must a string.") + self.scopes = self.scopes.strip() # assign default value for timout if not self.timeout: @@ -149,7 +136,7 @@ def __init__( def _auth(self) -> Tuple[str, str]: return (self.key, self.secret) - def _hasten_expiration_time(self, utc_stamp_str: str) -> str: + def _hasten_expiration_time(self, utc_stamp_str: str) -> datetime.datetime: """ Resets expiration time one second earlier to account for any delays between expiration check and request for @@ -164,14 +151,15 @@ def _hasten_expiration_time(self, utc_stamp_str: str) -> str: utcstamp = datetime.datetime.strptime( utc_stamp_str, "%Y-%m-%d %H:%M:%SZ" ) - datetime.timedelta(seconds=1) - return datetime.datetime.strftime(utcstamp, "%Y-%m-%d %H:%M:%SZ") + utcstamp = utcstamp.replace(tzinfo=datetime.timezone.utc) + return utcstamp - def _parse_server_response(self, response: Response) -> None: + def _parse_server_response(self, response: requests.Response) -> None: """Parses authorization server response""" - self.server_response = response # type: ignore + self.server_response = response if response.status_code == requests.codes.ok: self.token_str = response.json()["access_token"] - self.token_expires_at = self._hasten_expiration_time( # type: ignore + self.token_expires_at = self._hasten_expiration_time( response.json()["expires_at"] ) self.token_type = response.json()["token_type"] @@ -182,12 +170,10 @@ def _payload(self) -> Dict[str, str]: """Preps requests params""" return { "grant_type": self.grant_type, - "scope": self.scopes, # type: ignore - "principalID": self.principal_id, - "principalIDNS": self.principal_idns, + "scope": self.scopes, } - def _post_token_request(self) -> Response: + def _post_token_request(self) -> requests.Response: """ Fetches Worldcat access token for specified scope (web service) @@ -222,7 +208,7 @@ def _request_token(self): self._parse_server_response(response) def _token_headers(self) -> Dict[str, str]: - return {"User-Agent": self.agent, "Accept": "application/json"} # type: ignore + return {"User-Agent": self.agent, "Accept": "application/json"} def _token_url(self) -> str: return f"{self.oauth_server}/token" @@ -234,24 +220,24 @@ def is_expired(self) -> bool: Returns: bool - Example: - >>> token.is_expired() - False + Examples: + >>> token.is_expired() + False + """ - try: - if ( - datetime.datetime.strptime(self.token_expires_at, "%Y-%m-%d %H:%M:%SZ") # type: ignore - < datetime.datetime.utcnow() - ): + if isinstance(self.token_expires_at, datetime.datetime): + if self.token_expires_at < datetime.datetime.now(datetime.timezone.utc): return True else: return False - except TypeError: - raise - except ValueError: - raise + else: + raise TypeError( + "Attribute 'WorldcatAccessToken.token_expires_at' is of invalid type. " + "Expected `datetime.datetime` object." + ) def __repr__(self): return ( - f"access_token: '{self.token_str}', expires_at: '{self.token_expires_at}'" + f"access_token: '{self.token_str}', " + f"expires_at: '{self.token_expires_at:%Y-%m-%d %H:%M:%SZ}'" ) diff --git a/bookops_worldcat/errors.py b/bookops_worldcat/errors.py index 242cfea..7b563ce 100644 --- a/bookops_worldcat/errors.py +++ b/bookops_worldcat/errors.py @@ -19,15 +19,7 @@ class WorldcatAuthorizationError(BookopsWorldcatError): pass -class WorldcatSessionError(BookopsWorldcatError): - """ - Exception raised during WorlCat session - """ - - pass - - -class WorldcatRequestError(WorldcatSessionError): +class WorldcatRequestError(BookopsWorldcatError): """ Exceptions raised on HTTP errors returned by web service """ diff --git a/bookops_worldcat/metadata_api.py b/bookops_worldcat/metadata_api.py index 0466004..ff5dc21 100644 --- a/bookops_worldcat/metadata_api.py +++ b/bookops_worldcat/metadata_api.py @@ -4,16 +4,12 @@ This module provides MetadataSession class for requests to WorldCat Metadata API. """ -from typing import Callable, Dict, List, Optional, Tuple, Union +from typing import Callable, Dict, List, Optional, Tuple, Union, BinaryIO from requests import Request, Response from ._session import WorldcatSession from .authorize import WorldcatAccessToken -from .errors import ( - WorldcatSessionError, - InvalidOclcNumber, -) from .query import Query from .utils import verify_oclc_number, verify_oclc_numbers @@ -21,135 +17,174 @@ class MetadataSession(WorldcatSession): """OCLC Metadata API wrapper session. Inherits `requests.Session` methods""" + BASE_URL = "https://metadata.api.oclc.org/worldcat" + def __init__( self, authorization: WorldcatAccessToken, agent: Optional[str] = None, - timeout: Optional[ - Union[int, float, Tuple[int, int], Tuple[float, float]] - ] = None, + timeout: Union[int, float, Tuple[int, int], Tuple[float, float], None] = None, + totalRetries: int = 0, + backoffFactor: float = 0, + statusForcelist: Optional[List[int]] = None, + allowedMethods: Optional[List[str]] = None, ) -> None: - """ + """Initializes MetadataSession + Args: - authorization: WorlcatAccessToken object + authorization: WorldcatAccessToken object agent: "User-agent" parameter to be passed in the request header; usage strongly encouraged timeout: how long to wait for server to send data before giving up; default value is 5 seconds + totalRetries: optional number of times to retry a request that + failed or timed out. if totalRetries argument is + not passed, any arguments passed to + backoffFactor, statusForcelist, and + allowedMethods will be ignored. default is 0 + backoffFactor: if totalRetries is not 0, the backoff + factor as a float to use to calculate amount of + time session will sleep before attempting request + again. default is 0 + statusForcelist: if totalRetries is not 0, a list of HTTP + status codes to automatically retry requests on. + if not specified, failed requests with status codes + 413, 429, and 503 will be retried up to number of + totalRetries. + example: [500, 502, 503, 504] + allowedMethods: if totalRetries is not 0, set of HTTP methods that + requests should be retried on. if not specified, + requests using any HTTP method verbs will be + retried. example: ["GET", "POST"] """ - super().__init__(authorization, agent=agent, timeout=timeout) + super().__init__( + authorization, + agent=agent, + timeout=timeout, + totalRetries=totalRetries, + backoffFactor=backoffFactor, + statusForcelist=statusForcelist, + allowedMethods=allowedMethods, + ) - def _split_into_legal_volume( - self, oclc_numbers: List[str] = [], n: int = 50 - ) -> List[str]: - """ - OCLC requries that no more than 50 numbers are passed for batch processing + def _url_manage_bibs_validate(self, validationLevel: str) -> str: + return f"{self.BASE_URL}/manage/bibs/validate/{validationLevel}" - Args: - oclc_numbers: list of oclc numbers - n: batch size, default (max) 50 + def _url_manage_bibs_current_oclc_number(self) -> str: + return f"{self.BASE_URL}/manage/bibs/current" - Yields: - n-sized batch - """ + def _url_manage_bibs_create(self) -> str: + return f"{self.BASE_URL}/manage/bibs" - for i in range(0, len(oclc_numbers), n): - yield ",".join(oclc_numbers[i : i + n]) + def _url_manage_bibs(self, oclcNumber: str) -> str: + return f"{self.BASE_URL}/manage/bibs/{oclcNumber}" - def _url_base(self) -> str: - return "https://worldcat.org" + def _url_manage_bibs_match(self) -> str: + return f"{self.BASE_URL}/manage/bibs/match" - def _url_search_base(self) -> str: - return "https://americas.metadata.api.oclc.org/worldcat/search/v1" + def _url_manage_ih_current(self) -> str: + return f"{self.BASE_URL}/manage/institution/holdings/current" - def _url_member_shared_print_holdings(self) -> str: - base_url = self._url_search_base() - return f"{base_url}/bibs-retained-holdings" + def _url_manage_ih_set(self, oclcNumber: str) -> str: + return f"{self.BASE_URL}/manage/institution/holdings/{oclcNumber}/set" - def _url_member_general_holdings(self) -> str: - base_url = self._url_search_base() - return f"{base_url}/bibs-summary-holdings" + def _url_manage_ih_unset(self, oclcNumber: str) -> str: + return f"{self.BASE_URL}/manage/institution/holdings/{oclcNumber}/unset" - def _url_brief_bib_search(self) -> str: - base_url = self._url_search_base() - return f"{base_url}/brief-bibs" + def _url_manage_ih_set_with_bib(self) -> str: + return f"{self.BASE_URL}/manage/institution/holdings/set" - def _url_brief_bib_oclc_number(self, oclcNumber: str) -> str: - base_url = self._url_search_base() - return f"{base_url}/brief-bibs/{oclcNumber}" + def _url_manage_ih_unset_with_bib(self) -> str: + return f"{self.BASE_URL}/manage/institution/holdings/unset" - def _url_brief_bib_other_editions(self, oclcNumber: str) -> str: - base_url = self._url_search_base() - return f"{base_url}/brief-bibs/{oclcNumber}/other-editions" + def _url_manage_ih_codes(self) -> str: + return f"{self.BASE_URL}/manage/institution/holding-codes" - def _url_lhr_control_number(self, controlNumber: str) -> str: - base_url = self._url_search_base() - return f"{base_url}/my-holdings/{controlNumber}" + def _url_manage_lbd_create(self) -> str: + return f"{self.BASE_URL}/manage/lbds" - def _url_lhr_search(self) -> str: - base_url = self._url_search_base() - return f"{base_url}/my-holdings" + def _url_manage_lbd(self, controlNumber: Union[str, int]) -> str: + return f"{self.BASE_URL}/manage/lbds/{controlNumber}" - def _url_lhr_shared_print(self) -> str: - base_url = self._url_search_base() - return f"{base_url}/retained-holdings" + def _url_manage_lhr_create(self) -> str: + return f"{self.BASE_URL}/manage/lhrs" - def _url_bib_oclc_number(self, oclcNumber: str) -> str: - base_url = self._url_base() - return f"{base_url}/bib/data/{oclcNumber}" + def _url_manage_lhr(self, controlNumber: Union[str, int]) -> str: + return f"{self.BASE_URL}/manage/lhrs/{controlNumber}" - def _url_bib_check_oclc_numbers(self) -> str: - base_url = self._url_base() - return f"{base_url}/bib/checkcontrolnumbers" + def _url_search_shared_print_holdings(self) -> str: + return f"{self.BASE_URL}/search/bibs-retained-holdings" - def _url_bib_holding_libraries(self) -> str: - base_url = self._url_base() - return f"{base_url}/bib/holdinglibraries" + def _url_search_general_holdings(self) -> str: + return f"{self.BASE_URL}/search/bibs-summary-holdings" - def _url_bib_holdings_action(self) -> str: - base_url = self._url_base() - return f"{base_url}/ih/data" + def _url_search_general_holdings_summary(self) -> str: + return f"{self.BASE_URL}/search/summary-holdings" - def _url_bib_holdings_check(self) -> str: - base_url = self._url_base() - return f"{base_url}/ih/checkholdings" + def _url_search_brief_bibs(self) -> str: + return f"{self.BASE_URL}/search/brief-bibs" - def _url_bib_holdings_batch_action(self) -> str: - base_url = self._url_base() - return f"{base_url}/ih/datalist" + def _url_search_brief_bibs_oclc_number(self, oclcNumber: str) -> str: + return f"{self.BASE_URL}/search/brief-bibs/{oclcNumber}" - def _url_bib_holdings_multi_institution_batch_action(self) -> str: - base_url = self._url_base() - return f"{base_url}/ih/institutionlist" + def _url_search_brief_bibs_other_editions(self, oclcNumber: str) -> str: + return f"{self.BASE_URL}/search/brief-bibs/{oclcNumber}/other-editions" - def get_brief_bib( - self, oclcNumber: Union[int, str], hooks: Optional[Dict[str, Callable]] = None - ) -> Response: + def _url_search_classification_bibs(self, oclcNumber: str) -> str: + return f"{self.BASE_URL}/search/classification-bibs/{oclcNumber}" + + def _url_search_lhr_shared_print(self) -> str: + return f"{self.BASE_URL}/search/retained-holdings" + + def _url_search_lhr_control_number(self, controlNumber: Union[str, int]) -> str: + return f"{self.BASE_URL}/search/my-holdings/{controlNumber}" + + def _url_search_lhr(self) -> str: + return f"{self.BASE_URL}/search/my-holdings" + + def _url_browse_lhr(self) -> str: + return f"{self.BASE_URL}/browse/my-holdings" + + def _url_search_lbd_control_number(self, controlNumber: Union[str, int]) -> str: + return f"{self.BASE_URL}/search/my-local-bib-data/{controlNumber}" + + def _url_search_lbd(self) -> str: + return f"{self.BASE_URL}/search/my-local-bib-data" + + def bib_create( + self, + record: Union[str, bytes, BinaryIO], + recordFormat: str, + responseFormat: str = "application/marcxml+xml", + hooks: Optional[Dict[str, Callable]] = None, + ) -> Optional[Response]: """ - Retrieve specific brief bibliographic resource. - Uses /brief-bibs/{oclcNumber} endpoint. + Create a bib record in OCLC if it does not already exist. + Uses /manage/bibs endpoint. Args: - oclcNumber: OCLC bibliographic record number; can be - an integer, or string that can include - OCLC # prefix - hooks: Requests library hook system that can be - used for signal event handling, see more at: - https://requests.readthedocs.io/en/master/user/advanced/#event-hooks + record: MARC record to be created + recordFormat: format of MARC record; options: + 'application/marcxml+xml'; 'application/marc' + responseFormat: format of returned record; options: + 'application/marcxml+xml', 'application/marc' + default is 'application/marcxml+xml' + hooks: Requests library hook system that can be used for + signal event handling. For more information see the + [Requests docs](https://requests.readthedocs.io/en/ + master/user/advanced/#event-hooks) + Returns: `requests.Response` instance """ - - try: - oclcNumber = verify_oclc_number(oclcNumber) - except InvalidOclcNumber: - raise WorldcatSessionError("Invalid OCLC # was passed as an argument") - - header = {"Accept": "application/json"} - url = self._url_brief_bib_oclc_number(oclcNumber) + url = self._url_manage_bibs_create() + header = { + "Accept": responseFormat, + "content-type": recordFormat, + } # prep request - req = Request("GET", url, headers=header, hooks=hooks) + req = Request("POST", url, data=record, headers=header, hooks=hooks) prepared_request = self.prepare_request(req) # send request @@ -157,37 +192,35 @@ def get_brief_bib( return query.response - def get_full_bib( + def bib_get( self, oclcNumber: Union[int, str], - response_format: Optional[str] = None, + responseFormat: str = "application/marcxml+xml", hooks: Optional[Dict[str, Callable]] = None, - ) -> Response: + ) -> Optional[Response]: """ Send a GET request for a full bibliographic resource. - Uses /bib/data/{oclcNumber} endpoint. + Uses /manage/bibs/{oclcNumber} endpoint. Args: oclcNumber: OCLC bibliographic record number; can be an - integer, or string with or without OCLC # prefix - response_format: format of returned record - hooks: Requests library hook system that can be - used for signal event handling, see more at: - https://requests.readthedocs.io/en/master/user/advanced/#event-hooks + integer or string with or without OCLC Number + prefix + responseFormat: format of returned record, options: + 'application/marcxml+xml', 'application/marc', + default is 'application/marcxml+xml' + hooks: Requests library hook system that can be used for + signal event handling. For more information see the + [Requests docs](https://requests.readthedocs.io/en/ + master/user/advanced/#event-hooks) + Returns: - `requests.Response` object + `requests.Response` instance """ - try: - oclcNumber = verify_oclc_number(oclcNumber) - except InvalidOclcNumber: - raise WorldcatSessionError("Invalid OCLC # was passed as an argument.") + oclcNumber = verify_oclc_number(oclcNumber) - url = self._url_bib_oclc_number(oclcNumber) - if not response_format: - response_format = ( - 'application/atom+xml;content="application/vnd.oclc.marc21+xml"' - ) - header = {"Accept": response_format} + url = self._url_manage_bibs(oclcNumber) + header = {"Accept": responseFormat} # prep request req = Request("GET", url, headers=header, hooks=hooks) @@ -198,47 +231,35 @@ def get_full_bib( return query.response - def holding_get_status( + def bib_get_classification( self, oclcNumber: Union[int, str], - inst: Optional[str] = None, - instSymbol: Optional[str] = None, - response_format: Optional[str] = "application/atom+json", hooks: Optional[Dict[str, Callable]] = None, - ) -> Response: + ) -> Optional[Response]: """ - Retrieves Worlcat holdings status of a record with provided OCLC number. - The service automatically recognizes institution based on the issued access - token. - Uses /ih/checkholdings endpoint. + Given an OCLC number, retrieve classification recommendations for the bib + record. + Uses /search/classification-bibs/{oclcNumber} endpoint. Args: oclcNumber: OCLC bibliographic record number; can be an - integer, or string with or without OCLC # prefix - inst: registry ID of the institution whose holdings - are being checked - instSymbol: optional; OCLC symbol of the institution whose - holdings are being checked - response_format: 'application/atom+json' (default) or - 'application/atom+xml' - hooks: Requests library hook system that can be - used for signal event handling, see more at: - https://requests.readthedocs.io/en/master/user/advanced/#event-hooks + integer or string with or without OCLC Number + prefix + hooks: Requests library hook system that can be used for + signal event handling. For more information see the + [Requests docs](https://requests.readthedocs.io/en/ + master/user/advanced/#event-hooks) Returns: - `requests.Response` object + `requests.Response` instance """ - try: - oclcNumber = verify_oclc_number(oclcNumber) - except InvalidOclcNumber as exc: - raise WorldcatSessionError(exc) + oclcNumber = verify_oclc_number(oclcNumber) - url = self._url_bib_holdings_check() - header = {"Accept": response_format} - payload = {"oclcNumber": oclcNumber, "inst": inst, "instSymbol": instSymbol} + url = self._url_search_classification_bibs(oclcNumber) + header = {"Accept": "application/json"} # prep request - req = Request("GET", url, params=payload, headers=header, hooks=hooks) + req = Request("GET", url, headers=header, hooks=hooks) prepared_request = self.prepare_request(req) # send request @@ -246,57 +267,38 @@ def holding_get_status( return query.response - def holding_set( + def bib_get_current_oclc_number( self, - oclcNumber: Union[int, str], - inst: Optional[str] = None, - instSymbol: Optional[str] = None, - holdingLibraryCode: Optional[str] = None, - classificationScheme: Optional[str] = None, - response_format: str = "application/atom+json", + oclcNumbers: Union[str, List[Union[str, int]]], hooks: Optional[Dict[str, Callable]] = None, - ) -> Response: + ) -> Optional[Response]: """ - Sets institution's Worldcat holding on an individual record. - Uses /ih/data endpoint. + Given one or more OCLC Numbers, retrieve current OCLC numbers. + Uses /manage/bibs/current endpoint. Args: - oclcNumber: OCLC bibliographic record number; can be an - integer, or string with or without OCLC # prefix - inst: registry ID of the institution whose holdings - are being checked - instSymbol: optional; OCLC symbol of the institution whose - holdings are being checked - holdingLibraryCode: four letter holding code to set the holing on - classificationScheme: whether or not to return group availability - information - response_format: 'application/atom+json' (default) or - 'application/atom+xml' - hooks: Requests library hook system that can be - used for signal event handling, see more at: - https://requests.readthedocs.io/en/master/user/advanced/#event-hooks + oclcNumbers: string or list containing one or more OCLC numbers + to be checked; numbers can be integers or strings + with or without OCLC Number prefix; + if str, the numbers must be separated by a comma + hooks: Requests library hook system that can be used for + signal event handling. For more information see the + [Requests docs](https://requests.readthedocs.io/en/ + master/user/advanced/#event-hooks) + Returns: - `requests.Response` object + `requests.Response` instance """ - try: - oclcNumber = verify_oclc_number(oclcNumber) - except InvalidOclcNumber as exc: - raise WorldcatSessionError(exc) + vetted_numbers = verify_oclc_numbers(oclcNumbers) - url = self._url_bib_holdings_action() - header = {"Accept": response_format} - payload = { - "oclcNumber": oclcNumber, - "inst": inst, - "instSymbol": instSymbol, - "holdingLibraryCode": holdingLibraryCode, - "classificationScheme": classificationScheme, - } + header = {"Accept": "application/json"} + url = self._url_manage_bibs_current_oclc_number() + payload = {"oclcNumbers": ",".join(vetted_numbers)} # prep request - req = Request("POST", url, params=payload, headers=header, hooks=hooks) + req = Request("GET", url, params=payload, headers=header, hooks=hooks) prepared_request = self.prepare_request(req) # send request @@ -304,65 +306,38 @@ def holding_set( return query.response - def holding_unset( + def bib_match( self, - oclcNumber: Union[int, str], - cascade: Union[int, str] = "0", - inst: Optional[str] = None, - instSymbol: Optional[str] = None, - holdingLibraryCode: Optional[str] = None, - classificationScheme: Optional[str] = None, - response_format: str = "application/atom+json", + record: Union[str, bytes, BinaryIO], + recordFormat: str, hooks: Optional[Dict[str, Callable]] = None, - ) -> Response: + ) -> Optional[Response]: """ - Deletes institution's Worldcat holding on an individual record. - Uses /ih/data endpoint. + Given a bib record in MARC21 or MARCXML identify the best match in WorldCat. + Record must contain at minimum an 008 and 245. Response contains number of + potential matches in numberOfRecords and best match in briefRecords. + Uses /manage/bibs/match endpoint. Args: - oclcNumber: OCLC bibliographic record number; can be an - integer, or string with or without OCLC # prefix - if str the numbers must be separated by comma - cascade: 0 or 1, default 0; - 0 - don't remove holdings if local holding - record or local bibliographic records exists; - 1 - remove holding and delete local holdings - record and local bibliographic record - inst: registry ID of the institution whose holdings - are being checked - instSymbol: optional; OCLC symbol of the institution whose - holdings are being checked - holdingLibraryCode: four letter holding code to set the holing on - classificationScheme: whether or not to return group availability - information - response_format: 'application/atom+json' (default) or - 'application/atom+xml' - hooks: Requests library hook system that can be - used for signal event handling, see more at: - https://requests.readthedocs.io/en/master/user/advanced/#event-hooks + record: MARC record to be matched + recordFormat: format of MARC record, options: + 'application/marcxml+xml', 'application/marc' + hooks: Requests library hook system that can be used for + signal event handling. For more information see the + [Requests docs](https://requests.readthedocs.io/en/ + master/user/advanced/#event-hooks) Returns: - `requests.Response` object + `requests.Response` instance """ - - try: - oclcNumber = verify_oclc_number(oclcNumber) - except InvalidOclcNumber as exc: - raise WorldcatSessionError(exc) - - url = self._url_bib_holdings_action() - header = {"Accept": response_format} - payload = { - "oclcNumber": oclcNumber, - "cascade": cascade, - "inst": inst, - "instSymbol": instSymbol, - "holdingLibraryCode": holdingLibraryCode, - "classificationScheme": classificationScheme, + url = self._url_manage_bibs_match() + header = { + "Accept": "application/json", + "content-type": recordFormat, } # prep request - req = Request("DELETE", url, params=payload, headers=header, hooks=hooks) + req = Request("POST", url, data=record, headers=header, hooks=hooks) prepared_request = self.prepare_request(req) # send request @@ -370,170 +345,101 @@ def holding_unset( return query.response - def holdings_set( + def bib_replace( self, - oclcNumbers: Union[str, List], - inst: Optional[str] = None, - instSymbol: Optional[str] = None, - response_format: str = "application/atom+json", + oclcNumber: Union[int, str], + record: Union[str, bytes, BinaryIO], + recordFormat: str, + responseFormat: str = "application/marcxml+xml", hooks: Optional[Dict[str, Callable]] = None, - ) -> List[Response]: + ) -> Optional[Response]: """ - Set institution holdings for multiple OCLC numbers - Uses /ih/datalist endpoint. + Given an OCLC number and MARC record, find record in WorldCat and replace it. + If the record does not exist in WorldCat, a new bib record will be created. + Uses /manage/bibs/{oclcNumber} endpoint. Args: - oclcNumbers: list of OCLC control numbers for which holdings - should be set; - they can be integers or strings with or - without OCLC # prefix; - if str the numbers must be separated by comma - inst: registry ID of the institution whose holdings - are being checked - instSymbol: optional; OCLC symbol of the institution whose - holdings are being checked - response_format: 'application/atom+json' (default) or - 'application/atom+xml' - hooks: Requests library hook system that can be - used for signal event handling, see more at: - https://requests.readthedocs.io/en/master/user/advanced/#event-hooks - Returns: - list of `requests.Response` objects - """ - responses = [] - - try: - vetted_numbers = verify_oclc_numbers(oclcNumbers) - except InvalidOclcNumber as exc: - raise WorldcatSessionError(exc) - - url = self._url_bib_holdings_batch_action() - header = {"Accept": response_format} - - # split into batches of 50 and issue request for each batch - for batch in self._split_into_legal_volume(vetted_numbers): - payload = { - "oclcNumbers": batch, - "inst": inst, - "instSymbol": instSymbol, - } - - # prep request - req = Request("POST", url, params=payload, headers=header, hooks=hooks) - prepared_request = self.prepare_request(req) - - # send request - query = Query(self, prepared_request, timeout=self.timeout) - - responses.append(query.response) - - return responses - - def holdings_unset( - self, - oclcNumbers: Union[str, List], - cascade: str = "0", - inst: Optional[str] = None, - instSymbol: Optional[str] = None, - response_format: str = "application/atom+json", - hooks: Optional[Dict[str, Callable]] = None, - ) -> List[Response]: - """ - Set institution holdings for multiple OCLC numbers - Uses /ih/datalist endpoint. + oclcNumber: OCLC bibliographic record number; can be an + integer or string with or without OCLC Number + prefix + record: MARC record to replace existing WorldCat record + recordFormat: format of MARC record, options: + 'application/marcxml+xml', 'application/marc' + responseFormat: format of returned record; options: + 'application/marcxml+xml', 'application/marc' + default is 'application/marcxml+xml' + hooks: Requests library hook system that can be used for + signal event handling. For more information see the + [Requests docs](https://requests.readthedocs.io/en/ + master/user/advanced/#event-hooks) - Args: - oclcNumbers: list of OCLC control numbers for which holdings - should be set; - they can be integers or strings with or - without OCLC # prefix; - if str the numbers must be separated by comma - cascade: 0 or 1, default 0; - 0 - don't remove holdings if local holding - record or local bibliographic records exists; - 1 - remove holding and delete local holdings - record and local bibliographic record - inst: registry ID of the institution whose holdings - are being checked - instSymbol: optional; OCLC symbol of the institution whose - holdings are being checked - response_format: 'application/atom+json' (default) or - 'application/atom+xml' - hooks: Requests library hook system that can be - used for signal event handling, see more at: - https://requests.readthedocs.io/en/master/user/advanced/#event-hooks Returns: - list of `requests.Response` objects + `requests.Response` instance """ - responses = [] + oclcNumber = verify_oclc_number(oclcNumber) - try: - vetted_numbers = verify_oclc_numbers(oclcNumbers) - except InvalidOclcNumber as exc: - raise WorldcatSessionError(exc) - - url = self._url_bib_holdings_batch_action() - header = {"Accept": response_format} - - # split into batches of 50 and issue request for each batch - for batch in self._split_into_legal_volume(vetted_numbers): - payload = { - "oclcNumbers": batch, - "cascade": cascade, - "inst": inst, - "instSymbol": instSymbol, - } - - # prep request - req = Request("DELETE", url, params=payload, headers=header, hooks=hooks) - prepared_request = self.prepare_request(req) + url = self._url_manage_bibs(oclcNumber) + header = { + "Accept": responseFormat, + "content-type": recordFormat, + } - # send request - query = Query(self, prepared_request, timeout=self.timeout) + # prep request + req = Request("PUT", url, data=record, headers=header, hooks=hooks) + prepared_request = self.prepare_request(req) - responses.append(query.response) + # send request + query = Query(self, prepared_request, timeout=self.timeout) - return responses + return query.response - def holdings_set_multi_institutions( + def bib_validate( self, - oclcNumber: Union[int, str], - instSymbols: str, - response_format: str = "application/atom+json", + record: Union[str, bytes, BinaryIO], + recordFormat: str, + validationLevel: str = "validateFull", hooks: Optional[Dict[str, Callable]] = None, - ) -> Response: + ) -> Optional[Response]: """ - Batch sets intitution holdings for multiple intitutions - - Uses /ih/institutionlist endpoint + Given a bib record, validate that record conforms to MARC standards. + Uses /manage/bibs/validate/{validationLevel} endpoint. Args: - oclcNumber: OCLC bibliographic record number; can be an - integer, or string with or without OCLC # prefix - instSymbols: a comma-separated list of OCLC symbols of the - institution whose holdings are being set - response_format: 'application/atom+json' (default) or - 'application/atom+xml' - hooks: Requests library hook system that can be - used for signal event handling, see more at: - https://requests.readthedocs.io/en/master/user/advanced/#event-hooks + record: MARC record to be validated + recordFormat: format of MARC record, options: + 'application/marcxml+xml', 'application/marc' + validationLevel: Level at which to validate records + available values: 'validateFull', 'validateAdd', + 'validateReplace' + default is 'validateFull' + hooks: Requests library hook system that can be used for + signal event handling. For more information see the + [Requests docs](https://requests.readthedocs.io/en/ + master/user/advanced/#event-hooks) + Returns: - `requests.Response` object + `requests.Response` instance """ - try: - oclcNumber = verify_oclc_number(oclcNumber) - except InvalidOclcNumber: - raise WorldcatSessionError("Invalid OCLC # was passed as an argument") + if validationLevel not in ["validateFull", "validateAdd", "validateReplace"]: + raise ValueError( + "Invalid argument 'validationLevel'." + "Must be either 'validateFull', 'validateAdd', or 'validateReplace'" + ) - url = self._url_bib_holdings_multi_institution_batch_action() - header = {"Accept": response_format} - payload = { - "oclcNumber": oclcNumber, - "instSymbols": instSymbols, + url = self._url_manage_bibs_validate(validationLevel) + header = { + "Accept": "application/json", + "content-type": recordFormat, } # prep request - req = Request("POST", url, params=payload, headers=header, hooks=hooks) + req = Request( + "POST", + url, + data=record, + headers=header, + hooks=hooks, + ) prepared_request = self.prepare_request(req) # send request @@ -541,52 +447,32 @@ def holdings_set_multi_institutions( return query.response - def holdings_unset_multi_institutions( - self, - oclcNumber: Union[int, str], - instSymbols: str, - cascade: str = "0", - response_format: str = "application/atom+json", - hooks: Optional[Dict[str, Callable]] = None, - ) -> Response: + def brief_bibs_get( + self, oclcNumber: Union[int, str], hooks: Optional[Dict[str, Callable]] = None + ) -> Optional[Response]: """ - Batch unsets intitution holdings for multiple intitutions - - Uses /ih/institutionlist endpoint + Retrieve specific brief bibliographic resource. + Uses /search/brief-bibs/{oclcNumber} endpoint. Args: oclcNumber: OCLC bibliographic record number; can be an - integer, or string with or without OCLC # prefix - instSymbols: a comma-separated list of OCLC symbols of the - institution whose holdings are being set - cascade: 0 or 1, default 0; - 0 - don't remove holdings if local holding - record or local bibliographic records exists; - 1 - remove holding and delete local holdings - record and local bibliographic record - response_format: 'application/atom+json' (default) or - 'application/atom+xml' - hooks: Requests library hook system that can be - used for signal event handling, see more at: - https://requests.readthedocs.io/en/master/user/advanced/#event-hooks + integer or string with or without OCLC Number + prefix + hooks: Requests library hook system that can be used for + signal event handling. For more information see the + [Requests docs](https://requests.readthedocs.io/en/ + master/user/advanced/#event-hooks) + Returns: - `requests.Response` object + `requests.Response` instance """ - try: - oclcNumber = verify_oclc_number(oclcNumber) - except InvalidOclcNumber: - raise WorldcatSessionError("Invalid OCLC # was passed as an argument") + oclcNumber = verify_oclc_number(oclcNumber) - url = self._url_bib_holdings_multi_institution_batch_action() - header = {"Accept": response_format} - payload = { - "oclcNumber": oclcNumber, - "instSymbols": instSymbols, - "cascade": cascade, - } + url = self._url_search_brief_bibs_oclc_number(oclcNumber) + header = {"Accept": "application/json"} # prep request - req = Request("DELETE", url, params=payload, headers=header, hooks=hooks) + req = Request("GET", url, headers=header, hooks=hooks) prepared_request = self.prepare_request(req) # send request @@ -594,45 +480,61 @@ def holdings_unset_multi_institutions( return query.response - def search_brief_bib_other_editions( + def brief_bibs_search( self, - oclcNumber: Union[int, str], - deweyNumber: Optional[str] = None, - datePublished: Optional[str] = None, + q: str, + deweyNumber: Optional[Union[str, List[str]]] = None, + datePublished: Optional[Union[str, List[str]]] = None, heldByGroup: Optional[str] = None, - heldBySymbol: Optional[str] = None, - heldByInstitutionID: Optional[Union[str, int]] = None, - inLanguage: Optional[str] = None, - inCatalogLanguage: Optional[str] = None, + heldBySymbol: Optional[Union[str, List[str]]] = None, + heldByInstitutionID: Optional[Union[str, int, List[str], List[int]]] = None, + inLanguage: Optional[Union[str, List[str]]] = None, + inCatalogLanguage: Optional[str] = "eng", materialType: Optional[str] = None, catalogSource: Optional[str] = None, - itemType: Optional[str] = None, - itemSubType: Optional[str] = None, - retentionCommitments: Optional[bool] = None, + itemType: Optional[Union[str, List[str]]] = None, + itemSubType: Optional[Union[str, List[str]]] = None, + retentionCommitments: bool = False, spProgram: Optional[str] = None, genre: Optional[str] = None, topic: Optional[str] = None, subtopic: Optional[str] = None, audience: Optional[str] = None, - content: Optional[str] = None, + content: Optional[Union[str, List[str]]] = None, openAccess: Optional[bool] = None, peerReviewed: Optional[bool] = None, - facets: Optional[str] = None, - groupVariantRecords: Optional[bool] = None, - preferredLanguage: Optional[str] = None, - offset: Optional[int] = None, - limit: Optional[int] = None, - orderBy: Optional[str] = None, + facets: Optional[Union[str, List[str]]] = None, + groupRelatedEditions: bool = False, + groupVariantRecords: bool = False, + preferredLanguage: str = "eng", + showHoldingsIndicators: bool = False, + lat: Optional[float] = None, + lon: Optional[float] = None, + distance: Optional[int] = None, + unit: str = "M", + orderBy: str = "bestMatch", + offset: int = 1, + limit: int = 10, hooks: Optional[Dict[str, Callable]] = None, - ) -> Response: + ) -> Optional[Response]: """ - Retrieve other editions related to bibliographic resource with provided - OCLC #. - Uses /brief-bibs/{oclcNumber}/other-editions endpoint. + Search for brief bibliographic resources using WorldCat query syntax. + See https://help.oclc.org/Librarian_Toolbox/Searching_WorldCat_Indexes/ + Bibliographic_records/Bibliographic_record_indexes for more information on + available indexes. Request may contain only one of: heldByInstitutionID, + heldByGroup, heldBySymbol, or combination of lat and lon. + Uses /search/brief-bibs endpoint. Args: - oclcNumber: OCLC bibliographic record number; can be an - integer, or string with or without OCLC # prefix + q: query in the form of a keyword search or + fielded search; + examples: + ti:Zendegi + ti:"Czarne oceany" + bn:9781680502404 + kw:python databases + ti:Zendegi AND au:greg egan + (au:Okken OR au:Myers) AND su:python deweyNumber: limits the response to the specified dewey classification number(s); for multiple values repeat the parameter, @@ -645,68 +547,79 @@ def search_brief_bib_other_editions( '2000-2005' '2000,2005' heldByGroup: restricts to holdings held by group symbol - heldBySymbol: restricts to holdings with specified intitution - symbol - heldByInstitutionID: restrict to specified institution regisgtryId - inLanguage: restrics the response to the single + heldBySymbol: restricts response to holdings held by specified + institution symbol + heldByInstitutionID: restricts response to holdings held by specified + institution registryId + inLanguage: restricts the response to the single specified language, example: 'fre' - inCataloglanguage: restrics the response to specified + inCatalogLanguage: restricts the response to specified cataloging language, example: 'eng'; default 'eng' materialType: restricts responses to specified material type, example: 'bks', 'vis' catalogSource: restrict to responses to single OCLC symbol as the cataloging source, example: 'DLC' - itemType: restricts reponses to single specified OCLC + itemType: restricts responses to single specified OCLC top-level facet type, example: 'book' itemSubType: restricts responses to single specified OCLC sub facet type, example: 'digital' retentionCommitments: restricts responses to bibliographic records - with retention commitment; True or False, - default False + with retention commitment; options: True, False, + (default is False) spProgram: restricts responses to bibliographic records associated with particular shared print program - genre: genre to limit results to - topic: topic to limit results to - subtopic: subtopic to limit results to + genre: genre to limit results to (ge index) + topic: topic to limit results to (s0 index) + subtopic: subtopic to limit results to (s1 index) audience: audience to limit results to, - example: - juv, - nonJuv - content: content to limit resutls to, - example: - fic, - nonFic, - fic,bio - openAccess: filter to only open access content, False or True - peerReviewed: filter to only peer reviewed content, False or True + available values: 'juv', 'nonJuv' + content: content to limit results to + available values: 'fic', 'nonFic', 'bio' + openAccess: restricts response to just open access content + peerReviewed: restricts response to just peer reviewed content facets: list of facets to restrict responses + groupRelatedEditions: whether or not use FRBR grouping, + options: False, True (default is False) groupVariantRecords: whether or not to group variant records. - options: False, True (default False) + options: False, True (default is False) preferredLanguage: language of metadata description, + default value "eng" (English) + showHoldingsIndicators: whether or not to show holdings indicators in + response. options: True, False, (default is False) + lat: limit to latitude, example: 37.502508 + lon: limit to longitute, example: -122.22702 + distance: distance from latitude and longitude + unit: unit of distance param; options: + 'M' (miles) or 'K' (kilometers), default is 'M' + orderBy: results sort key; + options: + 'recency' + 'bestMatch' + 'creator' + 'library' + 'publicationDateAsc' + 'publicationDateDesc' + 'mostWidelyHeld' + 'title' + default is 'bestMatch' offset: start position of bibliographic records to - return; default 1 - limit: maximum nuber of records to return; - maximum 50, default 10 - orderBy: sort of restuls; - available values: - +date, -date, +language, -language; - default value: -date - hooks: Requests library hook system that can be - used for signal event handling, see more at: - https://requests.readthedocs.io/en/master/user/advanced/#event-hooks + return; default is 1 + limit: maximum number of records to return; + maximum is 50, default is 10 + hooks: Requests library hook system that can be used for + signal event handling. For more information see the + [Requests docs](https://requests.readthedocs.io/en/ + master/user/advanced/#event-hooks) + Returns: - `requests.Response` object + `requests.Response` instance """ - try: - oclcNumber = verify_oclc_number(oclcNumber) - except InvalidOclcNumber: - raise WorldcatSessionError("Invalid OCLC # was passed as an argument") - - url = self._url_brief_bib_other_editions(oclcNumber) + url = self._url_search_brief_bibs() header = {"Accept": "application/json"} payload = { + "q": q, "deweyNumber": deweyNumber, "datePublished": datePublished, "heldByGroup": heldByGroup, @@ -714,6 +627,7 @@ def search_brief_bib_other_editions( "heldByInstitutionID": heldByInstitutionID, "inLanguage": inLanguage, "inCatalogLanguage": inCatalogLanguage, + "materialType": materialType, "catalogSource": catalogSource, "itemType": itemType, "itemSubType": itemSubType, @@ -727,11 +641,17 @@ def search_brief_bib_other_editions( "openAccess": openAccess, "peerReviewed": peerReviewed, "facets": facets, + "groupRelatedEditions": groupRelatedEditions, "groupVariantRecords": groupVariantRecords, "preferredLanguage": preferredLanguage, + "showHoldingsIndicators": showHoldingsIndicators, + "lat": lat, + "lon": lon, + "distance": distance, + "unit": unit, + "orderBy": orderBy, "offset": offset, "limit": limit, - "orderBy": orderBy, } # prep request @@ -743,46 +663,50 @@ def search_brief_bib_other_editions( return query.response - def search_brief_bibs( + def brief_bibs_get_other_editions( self, - q: str, - deweyNumber: Optional[str] = None, - datePublished: Optional[str] = None, + oclcNumber: Union[int, str], + deweyNumber: Optional[Union[str, List[str]]] = None, + datePublished: Optional[Union[str, List[str]]] = None, heldByGroup: Optional[str] = None, - inLanguage: Optional[str] = None, + heldBySymbol: Optional[Union[str, List[str]]] = None, + heldByInstitutionID: Optional[Union[str, int, List[Union[str, int]]]] = None, + inLanguage: Optional[Union[str, List[str]]] = None, inCatalogLanguage: Optional[str] = "eng", materialType: Optional[str] = None, catalogSource: Optional[str] = None, - itemType: Optional[str] = None, - itemSubType: Optional[str] = None, - retentionCommitments: Optional[bool] = None, + itemType: Optional[Union[str, List[str]]] = None, + itemSubType: Optional[Union[str, List[str]]] = None, + retentionCommitments: bool = False, spProgram: Optional[str] = None, - facets: Optional[str] = None, - groupRelatedEditions: Optional[bool] = None, - groupVariantRecords: Optional[bool] = None, - preferredLanguage: Optional[str] = None, - orderBy: Optional[str] = "mostWidelyHeld", - offset: Optional[int] = None, - limit: Optional[int] = None, + genre: Optional[str] = None, + topic: Optional[str] = None, + subtopic: Optional[str] = None, + audience: Optional[str] = None, + content: Optional[Union[str, List[str]]] = None, + openAccess: Optional[bool] = None, + peerReviewed: Optional[bool] = None, + facets: Optional[Union[str, List[str]]] = None, + groupVariantRecords: bool = False, + preferredLanguage: str = "eng", + showHoldingsIndicators: bool = False, + offset: int = 1, + limit: int = 10, + orderBy: str = "publicationDateDesc", hooks: Optional[Dict[str, Callable]] = None, - ) -> Response: + ) -> Optional[Response]: """ - Send a GET request for brief bibliographic resources. - Uses /brief-bibs endpoint. + Retrieve other editions related to bibliographic resource with provided + OCLC Number. Query may contain only one of: heldByInstitutionID, + heldByGroup, heldBySymbol, or spProgram. + Uses /brief-bibs/{oclcNumber}/other-editions endpoint. Args: - q: query in the form of a keyword search or - fielded search; - examples: - ti:Zendegi - ti:"Czarne oceany" - bn:9781680502404 - kw:python databases - ti:Zendegi AND au:greg egan - (au:Okken OR au:Myers) AND su:python + oclcNumber: OCLC bibliographic record number; can be an + integer or string with or without OCLC Number + prefix deweyNumber: limits the response to the - specified dewey classification number(s); - for multiple values repeat the parameter, + specified dewey classification number(s) example: '794,180' datePublished: restricts the response to one or @@ -792,31 +716,52 @@ def search_brief_bibs( '2000-2005' '2000,2005' heldByGroup: restricts to holdings held by group symbol - inLanguage: restrics the response to the single + heldBySymbol: restricts to holdings with specified intitution + symbol + heldByInstitutionID: restrict to specified institution registryId + inLanguage: restricts the response to the single specified language, example: 'fre' - inCataloglanguage: restrics the response to specified + inCatalogLanguage: restricts the response to specified cataloging language, example: 'eng'; default 'eng' materialType: restricts responses to specified material type, example: 'bks', 'vis' catalogSource: restrict to responses to single OCLC symbol as the cataloging source, example: 'DLC' - itemType: restricts reponses to single specified OCLC + itemType: restricts responses to single specified OCLC top-level facet type, example: 'book' itemSubType: restricts responses to single specified OCLC sub facet type, example: 'digital' retentionCommitments: restricts responses to bibliographic records - with retention commitment; True or False + with retention commitment; options: False, True + (default is False) spProgram: restricts responses to bibliographic records associated with particular shared print program + genre: genre to limit results to + topic: topic to limit results to + subtopic: subtopic to limit results to + audience: audience to limit results to, + example: + juv, + nonJuv + content: content to limit results to, + example: + fic, + nonFic, + fic,bio + openAccess: filter to only open access content, False or True + peerReviewed: filter to only peer reviewed content, False or True facets: list of facets to restrict responses - groupRelatedEditions: whether or not use FRBR grouping, - options: False, True (default is False) groupVariantRecords: whether or not to group variant records. - options: False, True (default False) - preferredLanguage: language of metadata description, - default value "en" (English) + options: False, True (default is False) + preferredLanguage: language of metadata description, default is 'eng' + showHoldingsIndicators: whether or not to show holdings indicators in + response. options: True, False (default is False) + offset: start position of bibliographic records to + return; default is 1 + limit: maximum number of records to return; + maximum is 50, default is 10 orderBy: results sort key; options: 'recency' @@ -827,28 +772,25 @@ def search_brief_bibs( 'publicationDateDesc' 'mostWidelyHeld' 'title' - offset: start position of bibliographic records to - return; default 1 - limit: maximum nuber of records to return; - maximum 50, default 10 - hooks: Requests library hook system that can be - used for signal event handling, see more at: - https://requests.readthedocs.io/en/master/user/advanced/#event-hooks + default is 'publicationDateDesc' + hooks: Requests library hook system that can be used for + signal event handling. For more information see the + [Requests docs](https://requests.readthedocs.io/en/ + master/user/advanced/#event-hooks) Returns: - `requests.Response` object - + `requests.Response` instance """ - if not q: - raise WorldcatSessionError("Argument 'q' is requried to construct query.") + oclcNumber = verify_oclc_number(oclcNumber) - url = self._url_brief_bib_search() + url = self._url_search_brief_bibs_other_editions(oclcNumber) header = {"Accept": "application/json"} payload = { - "q": q, "deweyNumber": deweyNumber, "datePublished": datePublished, "heldByGroup": heldByGroup, + "heldBySymbol": heldBySymbol, + "heldByInstitutionID": heldByInstitutionID, "inLanguage": inLanguage, "inCatalogLanguage": inCatalogLanguage, "materialType": materialType, @@ -857,13 +799,20 @@ def search_brief_bibs( "itemSubType": itemSubType, "retentionCommitments": retentionCommitments, "spProgram": spProgram, + "genre": genre, + "topic": topic, + "subtopic": subtopic, + "audience": audience, + "content": content, + "openAccess": openAccess, + "peerReviewed": peerReviewed, "facets": facets, - "groupRelatedEditions": groupRelatedEditions, "groupVariantRecords": groupVariantRecords, "preferredLanguage": preferredLanguage, - "orderBy": orderBy, + "showHoldingsIndicators": showHoldingsIndicators, "offset": offset, "limit": limit, + "orderBy": orderBy, } # prep request @@ -875,39 +824,68 @@ def search_brief_bibs( return query.response - def search_current_control_numbers( + def holdings_get_codes( + self, + hooks: Optional[Dict[str, Callable]] = None, + ) -> Optional[Response]: + """ + Retrieve the all holding codes for the authenticated institution. + Uses /manage/institution/holding-codes endpoint. + + Args: + hooks: Requests library hook system that can be used for + signal event handling. For more information see the + [Requests docs](https://requests.readthedocs.io/en/ + master/user/advanced/#event-hooks) + Returns: + `requests.Response` instance + """ + url = self._url_manage_ih_codes() + header = {"Accept": "application/json"} + + # prep request + req = Request("GET", url, headers=header, hooks=hooks) + prepared_request = self.prepare_request(req) + + # send request + query = Query(self, prepared_request, timeout=self.timeout) + + return query.response + + def holdings_get_current( self, oclcNumbers: Union[str, List[Union[str, int]]], - response_format: str = "application/atom+json", hooks: Optional[Dict[str, Callable]] = None, - ) -> Response: + ) -> Optional[Response]: """ - Retrieve current OCLC control numbers - Uses /bib/checkcontrolnumbers endpoint. + Retrieves WorldCat holdings status of a record with provided OCLC number. + The service automatically recognizes the user's institution based on the + issued access token. + Uses /manage/institution/holdings/current endpoint. Args: - oclcNumbers: list of OCLC control numbers to be checked; - they can be integers or strings with or - without OCLC # prefix; - if str the numbers must be separated by comma - response_format: 'application/atom+json' (default) or - 'application/atom+xml' - hooks: Requests library hook system that can be - used for signal event handling, see more at: - https://requests.readthedocs.io/en/master/user/advanced/#event-hooks + oclcNumbers: string or list containing one or more OCLC numbers + to be checked; numbers can be integers or strings + with or without OCLC Number prefix; + if str, the numbers must be separated by a comma + hooks: Requests library hook system that can be used for + signal event handling. For more information see the + [Requests docs](https://requests.readthedocs.io/en/ + master/user/advanced/#event-hooks) Returns: - `requests.Response` object + `requests.Response` instance """ + vetted_numbers = verify_oclc_numbers(oclcNumbers) - try: - vetted_numbers = verify_oclc_numbers(oclcNumbers) - except InvalidOclcNumber as exc: - raise WorldcatSessionError(exc) + # check that no more than 10 oclc numbers were passed + if len(vetted_numbers) > 10: + raise ValueError("Too many OCLC Numbers passed to 'oclcNumbers' argument.") - header = {"Accept": response_format} - url = self._url_bib_check_oclc_numbers() - payload = {"oclcNumbers": ",".join(vetted_numbers)} + url = self._url_manage_ih_current() + header = {"Accept": "application/json"} + + payload = {"oclcNumbers": vetted_numbers} # prep request req = Request("GET", url, params=payload, headers=header, hooks=hooks) @@ -918,72 +896,907 @@ def search_current_control_numbers( return query.response - def search_general_holdings( + def holdings_set( self, - oclcNumber: Union[int, str] = None, - isbn: Optional[str] = None, - issn: Optional[str] = None, - holdingsAllEditions: Optional[bool] = None, - holdingsAllVariantRecords: Optional[bool] = None, - preferredLanguage: Optional[str] = None, - heldInCountry: Optional[str] = None, - heldByGroup: Optional[str] = None, - lat: Optional[float] = None, - lon: Optional[float] = None, - distance: Optional[int] = None, - unit: Optional[str] = None, - offset: Optional[int] = None, - limit: Optional[int] = None, + oclcNumber: Union[int, str], hooks: Optional[Dict[str, Callable]] = None, - ) -> Response: + ) -> Optional[Response]: """ - Given a known item gets summary of holdings. - Uses /bibs-summary-holdings endpoint. + Sets institution's WorldCat holdings on an individual record. + Uses /manage/institions/holdings/{oclcNumber}/set endpoint. Args: - oclcNumber: OCLC bibliographic record number; can be - an integer, or string that can include - OCLC # prefix - isbn: ISBN without any dashes, - example: '978149191646x' + oclcNumber: OCLC bibliographic record number; can be an + integer or string with or without OCLC Number + prefix + hooks: Requests library hook system that can be used for + signal event handling. For more information see the + [Requests docs](https://requests.readthedocs.io/en/ + master/user/advanced/#event-hooks) + + Returns: + `requests.Response` instance + """ + oclcNumber = verify_oclc_number(oclcNumber) + + url = self._url_manage_ih_set(oclcNumber) + header = {"Accept": "application/json"} + + # prep request + req = Request("POST", url, headers=header, hooks=hooks) + prepared_request = self.prepare_request(req) + + # send request + query = Query(self, prepared_request, timeout=self.timeout) + + return query.response + + def holdings_unset( + self, + oclcNumber: Union[int, str], + hooks: Optional[Dict[str, Callable]] = None, + ) -> Optional[Response]: + """ + Unsets institution's WorldCat holdings on an individual record. + Uses /manage/institions/holdings/{oclcNumber}/unset endpoint. + + Args: + oclcNumber: OCLC bibliographic record number; can be an + integer or string with or without OCLC Number + prefix + hooks: Requests library hook system that can be used for + signal event handling. For more information see the + [Requests docs](https://requests.readthedocs.io/en/ + master/user/advanced/#event-hooks) + + Returns: + `requests.Response` instance + """ + oclcNumber = verify_oclc_number(oclcNumber) + + url = self._url_manage_ih_unset(oclcNumber) + header = {"Accept": "application/json"} + + # prep request + req = Request("POST", url, headers=header, hooks=hooks) + prepared_request = self.prepare_request(req) + + # send request + query = Query(self, prepared_request, timeout=self.timeout) + + return query.response + + def holdings_set_with_bib( + self, + record: str, + recordFormat: str, + hooks: Optional[Dict[str, Callable]] = None, + ) -> Optional[Response]: + """ + Given a MARC record in MARC XML or MARC21, set institution holdings on the + record. MARC record must contain OCLC number in 001 or 035 subfield a. + Only one MARC record is allowed in the request body. + Uses /manage/institution/holdings/set endpoint. + + Args: + record: MARC record on which to set holdings + recordFormat: format of MARC record, options: + 'application/marcxml+xml', 'application/marc' + hooks: Requests library hook system that can be used for + signal event handling. For more information see the + [Requests docs](https://requests.readthedocs.io/en/ + master/user/advanced/#event-hooks) + + Returns: + `requests.Response` instance + """ + url = self._url_manage_ih_set_with_bib() + header = { + "Accept": "application/json", + "content-type": recordFormat, + } + + # prep request + req = Request("POST", url, data=record, headers=header, hooks=hooks) + prepared_request = self.prepare_request(req) + + # send request + query = Query(self, prepared_request, timeout=self.timeout) + + return query.response + + def holdings_unset_with_bib( + self, + record: str, + recordFormat: str, + hooks: Optional[Dict[str, Callable]] = None, + ) -> Optional[Response]: + """ + Given a MARC record in MARC XML or MARC21, unset institution holdings on the + record. MARC record must contain OCLC number in 001 or 035 subfield a. + Only one MARC record is allowed in the request body. + Uses /manage/institution/holdings/unset endpoint. + + Args: + record: MARC record on which to unset holdings + recordFormat: format of MARC record, options: + 'application/marcxml+xml', 'application/marc' + hooks: Requests library hook system that can be used for + signal event handling. For more information see the + [Requests docs](https://requests.readthedocs.io/en/ + master/user/advanced/#event-hooks) + + Returns: + `requests.Response` instance + """ + url = self._url_manage_ih_unset_with_bib() + header = { + "Accept": "application/json", + "content-type": recordFormat, + } + + # prep request + req = Request("POST", url, data=record, headers=header, hooks=hooks) + prepared_request = self.prepare_request(req) + + # send request + query = Query(self, prepared_request, timeout=self.timeout) + return query.response + + def lbd_create( + self, + record: str, + recordFormat: str, + responseFormat: str = "application/marcxml+xml", + hooks: Optional[Dict[str, Callable]] = None, + ) -> Optional[Response]: + """ + Given a local bibliographic data record, create it in WorldCat. + Uses /manage/lbds endpoint. + + Args: + record: MARC record to be created + recordFormat: format of MARC record, options: + 'application/marcxml+xml', 'application/marc' + responseFormat: format of returned record; options: + 'application/marcxml+xml', 'application/marc' + default is 'application/marcxml+xml' + hooks: Requests library hook system that can be used for + signal event handling. For more information see the + [Requests docs](https://requests.readthedocs.io/en/ + master/user/advanced/#event-hooks) + + Returns: + `requests.Response` instance + """ + url = self._url_manage_lbd_create() + header = { + "Accept": responseFormat, + "content-type": recordFormat, + } + + # prep request + req = Request("POST", url, data=record, headers=header, hooks=hooks) + prepared_request = self.prepare_request(req) + + # send request + query = Query(self, prepared_request, timeout=self.timeout) + + return query.response + + def lbd_delete( + self, + controlNumber: Union[int, str], + responseFormat: str = "application/marcxml+xml", + hooks: Optional[Dict[str, Callable]] = None, + ) -> Optional[Response]: + """ + Given a control number, delete the associated Local Bibliographic Data record. + Uses /manage/lbds/{controlNumber} endpoint. + + Args: + controlNumber: control number associated with Local Bibliographic + Data record; can be an integer or string + responseFormat: format of returned record, options: + 'application/marcxml+xml', 'application/marc', + default is 'application/marcxml+xml' + hooks: Requests library hook system that can be used for + signal event handling. For more information see the + [Requests docs](https://requests.readthedocs.io/en/ + master/user/advanced/#event-hooks) + + Returns: + `requests.Response` instance + """ + url = self._url_manage_lbd(controlNumber) + header = {"Accept": responseFormat} + + # prep request + req = Request("DELETE", url, headers=header, hooks=hooks) + prepared_request = self.prepare_request(req) + + # send request + query = Query(self, prepared_request, timeout=self.timeout) + + return query.response + + def lbd_get( + self, + controlNumber: Union[int, str], + responseFormat: str = "application/marcxml+xml", + hooks: Optional[Dict[str, Callable]] = None, + ) -> Optional[Response]: + """ + Given a Control Number, retrieve a Local Bibliographic Data record. + Uses /manage/lbds/{controlNumber} endpoint. + + Args: + controlNumber: control number associated with Local Bibliographic + Data record; can be an integer or string + responseFormat: format of returned record, options: + 'application/marcxml+xml', 'application/marc', + default is 'application/marcxml+xml' + hooks: Requests library hook system that can be used for + signal event handling. For more information see the + [Requests docs](https://requests.readthedocs.io/en/ + master/user/advanced/#event-hooks) + + Returns: + `requests.Response` instance + """ + url = self._url_manage_lbd(controlNumber) + header = {"Accept": responseFormat} + + # prep request + req = Request("GET", url, headers=header, hooks=hooks) + prepared_request = self.prepare_request(req) + + # send request + query = Query(self, prepared_request, timeout=self.timeout) + + return query.response + + def lbd_replace( + self, + controlNumber: Union[int, str], + record: str, + recordFormat: str, + responseFormat: str = "application/marcxml+xml", + hooks: Optional[Dict[str, Callable]] = None, + ) -> Optional[Response]: + """ + Given a Control Number, find the associated Local Bibliographic Data + Record and replace it. If the Control Number is not found in + WorldCat, then the provided Local Bibliographic Data Record will be created. + Uses /manage/lbds/{controlNumber} endpoint. + + Args: + controlNumber: control number associated with Local Bibliographic + Data record; can be an integer or string + record: MARC record to replace existing local + bibliographic record + recordFormat: format of MARC record, options: + 'application/marcxml+xml', 'application/marc' + responseFormat: format of returned record; options: + 'application/marcxml+xml', 'application/marc' + default is 'application/marcxml+xml' + hooks: Requests library hook system that can be used for + signal event handling. For more information see the + [Requests docs](https://requests.readthedocs.io/en/ + master/user/advanced/#event-hooks) + + Returns: + `requests.Response` instance + """ + url = self._url_manage_lbd(controlNumber) + header = { + "Accept": responseFormat, + "content-type": recordFormat, + } + + # prep request + req = Request("PUT", url, data=record, headers=header, hooks=hooks) + prepared_request = self.prepare_request(req) + + # send request + query = Query(self, prepared_request, timeout=self.timeout) + + return query.response + + def lhr_create( + self, + record: str, + recordFormat: str, + responseFormat: str = "application/marcxml+xml", + hooks: Optional[Dict[str, Callable]] = None, + ) -> Optional[Response]: + """ + Given a local holdings record, create it in WorldCat + Uses /manage/lhrs endpoint. + + Args: + record: MARC holdings record to be created + recordFormat: format of MARC holdings record, options: + 'application/marcxml+xml', 'application/marc' + responseFormat: format of returned record; options: + 'application/marcxml+xml', 'application/marc' + default is 'application/marcxml+xml' + hooks: Requests library hook system that can be used for + signal event handling. For more information see the + [Requests docs](https://requests.readthedocs.io/en/ + master/user/advanced/#event-hooks) + + Returns: + `requests.Response` instance + """ + url = self._url_manage_lhr_create() + header = { + "Accept": responseFormat, + "content-type": recordFormat, + } + + # prep request + req = Request("POST", url, data=record, headers=header, hooks=hooks) + prepared_request = self.prepare_request(req) + + # send request + query = Query(self, prepared_request, timeout=self.timeout) + + return query.response + + def lhr_delete( + self, + controlNumber: Union[int, str], + responseFormat: str = "application/marcxml+xml", + hooks: Optional[Dict[str, Callable]] = None, + ) -> Optional[Response]: + """ + Given a control number, delete a Local Holdings record. + Uses /manage/lhrs/{controlNumber} endpoint. + + Args: + controlNumber: control number associated with Local Holdings + record; can be an integer or string + responseFormat: format of returned record, options: + 'application/marcxml+xml', 'application/marc', + default is 'application/marcxml+xml' + hooks: Requests library hook system that can be used for + signal event handling. For more information see the + [Requests docs](https://requests.readthedocs.io/en/ + master/user/advanced/#event-hooks) + + Returns: + `requests.Response` instance + """ + url = self._url_manage_lhr(controlNumber) + header = {"Accept": responseFormat} + + # prep request + req = Request("DELETE", url, headers=header, hooks=hooks) + prepared_request = self.prepare_request(req) + + # send request + query = Query(self, prepared_request, timeout=self.timeout) + + return query.response + + def lhr_get( + self, + controlNumber: Union[int, str], + responseFormat: str = "application/marcxml+xml", + hooks: Optional[Dict[str, Callable]] = None, + ) -> Optional[Response]: + """ + Send a GET request for a local holdings record + Uses /manage/lhrs/{controlNumber} endpoint. + + Args: + controlNumber: control number associated with Local Holdings + record; can be an integer or string + responseFormat: format of returned record, options: + 'application/marcxml+xml', 'application/marc', + default is 'application/marcxml+xml' + hooks: Requests library hook system that can be used for + signal event handling. For more information see the + [Requests docs](https://requests.readthedocs.io/en/ + master/user/advanced/#event-hooks) + + Returns: + `requests.Response` instance + """ + url = self._url_manage_lhr(controlNumber) + header = {"Accept": responseFormat} + + # prep request + req = Request("GET", url, headers=header, hooks=hooks) + prepared_request = self.prepare_request(req) + + # send request + query = Query(self, prepared_request, timeout=self.timeout) + + return query.response + + def lhr_replace( + self, + controlNumber: Union[int, str], + record: str, + recordFormat: str, + responseFormat: str = "application/marcxml+xml", + hooks: Optional[Dict[str, Callable]] = None, + ) -> Optional[Response]: + """ + Given a Control Number, find the associated Local Holdings + Record and replace it. If the Control Number is not found in + WorldCat, then the provided Local Holdings Record will be created. + Uses /manage/lhrs/{controlNumber} endpoint. + + Args: + controlNumber: control number associated with Local Holdings + record; can be an integer or string + record: MARC holdings record to replace existing local + holdings record + recordFormat: format of MARC holdings record, options: + 'application/marcxml+xml', 'application/marc' + responseFormat: format of returned record; options: + 'application/marcxml+xml', 'application/marc' + default is 'application/marcxml+xml' + hooks: Requests library hook system that can be used for + signal event handling. For more information see the + [Requests docs](https://requests.readthedocs.io/en/ + master/user/advanced/#event-hooks) + + Returns: + `requests.Response` instance + """ + url = self._url_manage_lhr(controlNumber) + header = { + "Accept": responseFormat, + "content-type": recordFormat, + } + + # prep request + req = Request("PUT", url, data=record, headers=header, hooks=hooks) + prepared_request = self.prepare_request(req) + + # send request + query = Query(self, prepared_request, timeout=self.timeout) + + return query.response + + def local_bibs_get( + self, + controlNumber: Union[int, str], + hooks: Optional[Dict[str, Callable]] = None, + ) -> Optional[Response]: + """ + Retrieve LBD Resource. + Uses /search/my-local-bib-data/{controlNumber} endpoint. + + Args: + controlNumber: control number associated with Local Bibliographic + Data record; can be an integer or string + hooks: Requests library hook system that can be used for + signal event handling. For more information see the + [Requests docs](https://requests.readthedocs.io/en/ + master/user/advanced/#event-hooks) + + Returns: + `requests.Response` instance + """ + url = self._url_search_lbd_control_number(controlNumber) + header = {"Accept": "application/json"} + + # prep request + req = Request("GET", url, headers=header, hooks=hooks) + prepared_request = self.prepare_request(req) + + # send request + query = Query(self, prepared_request, timeout=self.timeout) + + return query.response + + def local_bibs_search( + self, + q: str, + offset: int = 1, + limit: int = 10, + hooks: Optional[Dict[str, Callable]] = None, + ) -> Optional[Response]: + """ + Search LBD Resources using WorldCat query syntax. + See https://help.oclc.org/Librarian_Toolbox/Searching_WorldCat_Indexes/ + Local_bibliographic_data_records/Local_bibliographic_data_record_indexes_A-Z + for more information on available indexes. + Uses /search/my-local-bib-data endpoint. + + Args: + q: query in the form of a keyword search or + fielded search; + examples: + ti:Zendegi + ti:"Czarne oceany" + bn:9781680502404 + kw:python databases + ti:Zendegi AND au:greg egan + (au:Okken OR au:Myers) AND su:python + offset: start position of bibliographic records to + return; default is 1 + limit: maximum number of records to return; + maximum is 50, default is 10 + hooks: Requests library hook system that can be used for + signal event handling. For more information see the + [Requests docs](https://requests.readthedocs.io/en/ + master/user/advanced/#event-hooks) + + Returns: + `requests.Response` instance + """ + url = self._url_search_lbd() + header = {"Accept": "application/json"} + payload = {"q": q, "offset": offset, "limit": limit} + + # prep request + req = Request("GET", url, params=payload, headers=header, hooks=hooks) + prepared_request = self.prepare_request(req) + + # send request + query = Query(self, prepared_request, timeout=self.timeout) + + return query.response + + def local_holdings_browse( + self, + holdingLocation: str, + shelvingLocation: str, + callNumber: str, + oclcNumber: Optional[Union[int, str]] = None, + browsePosition: int = 0, + limit: int = 10, + hooks: Optional[Dict[str, Callable]] = None, + ) -> Optional[Response]: + """ + Browse local holdings. + Uses /browse/my-holdings endpoint. + + Args: + holdingLocation: holding location for item + shelvingLocation: shelving location for item + callNumber: call number for item + oclcNumber: OCLC bibliographic record number; can be an + integer or string with or without OCLC Number + prefix + browsePosition: position within browse list where the matching + record should be, default is 0 + limit: maximum number of records to return; + maximum is 50, default is 10 + hooks: Requests library hook system that can be used for + signal event handling. For more information see the + [Requests docs](https://requests.readthedocs.io/en/ + master/user/advanced/#event-hooks) + + Returns: + `requests.Response` instance + """ + if oclcNumber is not None: + oclcNumber = verify_oclc_number(oclcNumber) + + url = self._url_browse_lhr() + header = {"Accept": "application/json"} + payload = { + "callNumber": callNumber, + "oclcNumber": oclcNumber, + "holdingLocation": holdingLocation, + "shelvingLocation": shelvingLocation, + "browsePosition": browsePosition, + "limit": limit, + } + + # prep request + req = Request("GET", url, params=payload, headers=header, hooks=hooks) + prepared_request = self.prepare_request(req) + + # send request + query = Query(self, prepared_request, timeout=self.timeout) + + return query.response + + def local_holdings_get( + self, + controlNumber: Union[int, str], + hooks: Optional[Dict[str, Callable]] = None, + ) -> Optional[Response]: + """ + Retrieve LHR Resource. + Uses /search/my-holdings/{controlNumber} endpoint. + + Args: + controlNumber: control number associated with Local Holdings + record; can be an integer or string + hooks: Requests library hook system that can be used for + signal event handling. For more information see the + [Requests docs](https://requests.readthedocs.io/en/ + master/user/advanced/#event-hooks) + + Returns: + `requests.Response` instance + """ + url = self._url_search_lhr_control_number(controlNumber) + header = {"Accept": "application/json"} + + # prep request + req = Request("GET", url, headers=header, hooks=hooks) + prepared_request = self.prepare_request(req) + + # send request + query = Query(self, prepared_request, timeout=self.timeout) + + return query.response + + def local_holdings_search( + self, + oclcNumber: Optional[Union[int, str]] = None, + barcode: Optional[str] = None, + orderBy: str = "oclcSymbol", + offset: int = 1, + limit: int = 10, + hooks: Optional[Dict[str, Callable]] = None, + ) -> Optional[Response]: + """ + Search LHR Resources. Query must contain, at minimum, either an + OCLC Number or barcode. + Uses /search/my-holdings endpoint. + + Args: + oclcNumber: OCLC bibliographic record number; can be an + integer or string with or without OCLC Number + prefix + barcode: barcode as a string, + orderBy: results sort key; + options: + 'commitmentExpirationDate' + 'location' + 'oclcSymbol' + default is 'oclcSymbol' + offset: start position of bibliographic records to + return; default is 1 + limit: maximum number of records to return; + maximum is 50, default is 10 + hooks: Requests library hook system that can be used for + signal event handling. For more information see the + [Requests docs](https://requests.readthedocs.io/en/ + master/user/advanced/#event-hooks) + + Returns: + `requests.Response` instance + """ + if oclcNumber is not None: + oclcNumber = verify_oclc_number(oclcNumber) + + url = self._url_search_lhr() + header = {"Accept": "application/json"} + payload = { + "oclcNumber": oclcNumber, + "barcode": barcode, + "orderBy": orderBy, + "offset": offset, + "limit": limit, + } + + # prep request + req = Request("GET", url, params=payload, headers=header, hooks=hooks) + prepared_request = self.prepare_request(req) + + # send request + query = Query(self, prepared_request, timeout=self.timeout) + + return query.response + + def local_holdings_search_shared_print( + self, + oclcNumber: Optional[Union[int, str]] = None, + barcode: Optional[str] = None, + heldBySymbol: Optional[List[str]] = None, + heldByInstitutionID: Optional[List[int]] = None, + spProgram: Optional[List[str]] = None, + orderBy: str = "oclcSymbol", + offset: int = 1, + limit: int = 10, + hooks: Optional[Dict[str, Callable]] = None, + ) -> Optional[Response]: + """ + Search for shared print LHR Resources. Query must contain, at minimum, + either an OCLC Number or barcode and a value for either heldBySymbol, + heldByInstitutionID or spProgram. + Uses /search/retained-holdings endpoint. + + Args: + oclcNumber: OCLC bibliographic record number; can be an + integer or string with or without OCLC Number + prefix + barcode: barcode as a string, + heldBySymbol: restricts to holdings with specified institution + symbol + heldByInstitutionID: restrict to specified institution registryId + spProgram: restricts responses to bibliographic records + associated with particular shared print program + orderBy: results sort key; + options: + 'commitmentExpirationDate' + 'location' + 'oclcSymbol' + default is 'oclcSymbol' + offset: start position of bibliographic records to + return; default is 1 + limit: maximum number of records to return; + maximum is 50, default is 10 + hooks: Requests library hook system that can be used for + signal event handling. For more information see the + [Requests docs](https://requests.readthedocs.io/en/ + master/user/advanced/#event-hooks) + + Returns: + `requests.Response` instance + """ + if oclcNumber is not None: + oclcNumber = verify_oclc_number(oclcNumber) + + url = self._url_search_lhr_shared_print() + header = {"Accept": "application/json"} + payload = { + "oclcNumber": oclcNumber, + "barcode": barcode, + "heldBySymbol": heldBySymbol, + "heldByInstitutionID": heldByInstitutionID, + "spProgram": spProgram, + "orderBy": orderBy, + "offset": offset, + "limit": limit, + } + + # prep request + req = Request("GET", url, params=payload, headers=header, hooks=hooks) + prepared_request = self.prepare_request(req) + + # send request + query = Query(self, prepared_request, timeout=self.timeout) + + return query.response + + def shared_print_holdings_search( + self, + oclcNumber: Optional[Union[int, str]] = None, + isbn: Optional[str] = None, + issn: Optional[str] = None, + heldByGroup: Optional[str] = None, + heldInState: Optional[str] = None, + itemType: Optional[List[str]] = None, + itemSubType: Optional[List[str]] = None, + hooks: Optional[Dict[str, Callable]] = None, + ) -> Optional[Response]: + """ + Finds member shared print holdings for specified item. Query must + contain, at minimum, either an OCLC Number, ISBN, or ISSN. + Uses /search/bibs-retained-holdings endpoint. + + Args: + oclcNumber: OCLC bibliographic record number; can be an + integer or string with or without OCLC Number + prefix + isbn: ISBN without any dashes, example: '978149191646x' + issn: ISSN hyphenated, example: '0099-1234' + heldByGroup: restricts to holdings held by group symbol + heldInState: restricts to holdings held by institutions + in requested state, example: "NY" + itemType: restricts results to specified item type (example + 'book' or 'vis') + itemSubType: restricts results to specified item sub type + examples: 'book-digital' or 'audiobook-cd' + hooks: Requests library hook system that can be used for + signal event handling. For more information see the + [Requests docs](https://requests.readthedocs.io/en/ + master/user/advanced/#event-hooks) + + Returns: + `requests.Response` instance + """ + if oclcNumber is not None: + oclcNumber = verify_oclc_number(oclcNumber) + + url = self._url_search_shared_print_holdings() + header = {"Accept": "application/json"} + payload = { + "oclcNumber": oclcNumber, + "isbn": isbn, + "issn": issn, + "heldByGroup": heldByGroup, + "heldInState": heldInState, + "itemType": itemType, + "itemSubType": itemSubType, + } + + # prep request + req = Request("GET", url, params=payload, headers=header, hooks=hooks) + prepared_request = self.prepare_request(req) + + # send request + query = Query(self, prepared_request, timeout=self.timeout) + + return query.response + + def summary_holdings_search( + self, + oclcNumber: Optional[Union[int, str]] = None, + isbn: Optional[str] = None, + issn: Optional[str] = None, + holdingsAllEditions: Optional[bool] = None, + holdingsAllVariantRecords: Optional[bool] = None, + preferredLanguage: str = "eng", + holdingsFilterFormat: Optional[List[str]] = None, + heldInCountry: Optional[str] = None, + heldInState: Optional[str] = None, + heldByGroup: Optional[str] = None, + heldBySymbol: Optional[List[str]] = None, + heldByInstitutionID: Optional[List[int]] = None, + heldByLibraryType: Optional[List[str]] = None, + lat: Optional[float] = None, + lon: Optional[float] = None, + distance: Optional[int] = None, + unit: str = "M", + hooks: Optional[Dict[str, Callable]] = None, + ) -> Optional[Response]: + """ + Given a known item, get summary of holdings and brief bib record. Query must + contain, at minimum, either an OCLC Number, ISBN, or ISSN. Query may contain + only one of: heldByInstitutionId, heldByGroup, heldBySymbol, heldInCountry, + heldInState or combination of lat, lon and distance. If using lat/lon + arguments, query must contain a valid distance argument. + Uses /search/bibs-summary-holdings endpoint. + + Args: + oclcNumber: OCLC bibliographic record number; can be an + integer or string with or without OCLC Number + prefix + isbn: ISBN without any dashes, + example: '978149191646x' issn: ISSN (hyphenated, example: '0099-1234') holdingsAllEditions: get holdings for all editions; options: True or False - holdingsAllVariantRecords: get holdings for specific edition across variant - records; options: False, True + holdingsAllVariantRecords: get holdings for specific edition across + variant records; options: False, True preferredLanguage: language of metadata description; - default 'en' (English) + default 'eng' (English) + holdingsFilterFormat: get holdings for specific itemSubType, + example: book-digital heldInCountry: restricts to holdings held by institutions in requested country + heldInState: limits to holdings held by institutions + in requested state, example: 'US-NY' heldByGroup: limits to holdings held by indicated by symbol group - lat: limit to latitude, example: 37.502508 + heldBySymbol: limits to holdings held by institutions + indicated by institution symbol + heldByInstitutionID: limits to holdings held by institutions + indicated by institution registryID + heldByLibraryType: limits to holdings held by library type, + options: 'PUBLIC', 'ALL' + lat: limit to latitude, example: 37.502508, lon: limit to longitute, example: -122.22702 distance: distance from latitude and longitude unit: unit of distance param; options: - 'M' (miles) or 'K' (kilometers) - offset: start position of bibliographic records to - return; default 1 - limit: maximum nuber of records to return; - maximum 50, default 10 - hooks: Requests library hook system that can be - used for signal event handling, see more at: - https://requests.readthedocs.io/en/master/user/advanced/#event-hooks + 'M' (miles) or 'K' (kilometers), default is 'M' + hooks: Requests library hook system that can be used + for signal event handling. For more information + see the [Requests docs](https://requests. + readthedocs.io/en/master/user/advanced/ + #event-hooks) + Returns: - `requests.Response` object + `requests.Response` instance """ - if not any([oclcNumber, isbn, issn]): - raise WorldcatSessionError( - "Missing required argument. " - "One of the following args are required: oclcNumber, issn, isbn" - ) if oclcNumber is not None: - try: - oclcNumber = verify_oclc_number(oclcNumber) - except InvalidOclcNumber: - raise WorldcatSessionError("Invalid OCLC # was passed as an argument") + oclcNumber = verify_oclc_number(oclcNumber) - url = self._url_member_general_holdings() + url = self._url_search_general_holdings() header = {"Accept": "application/json"} payload = { "oclcNumber": oclcNumber, @@ -992,14 +1805,17 @@ def search_general_holdings( "holdingsAllEditions": holdingsAllEditions, "holdingsAllVariantRecords": holdingsAllVariantRecords, "preferredLanguage": preferredLanguage, + "holdingsFilterFormat": holdingsFilterFormat, "heldInCountry": heldInCountry, + "heldInState": heldInState, "heldByGroup": heldByGroup, + "heldBySymbol": heldBySymbol, + "heldByInstitutionID": heldByInstitutionID, + "heldByLibraryType": heldByLibraryType, "lat": lat, "lon": lon, "distance": distance, "unit": unit, - "offset": offset, - "limit": limit, } # prep request @@ -1011,67 +1827,86 @@ def search_general_holdings( return query.response - def search_shared_print_holdings( + def summary_holdings_get( self, - oclcNumber: Union[int, str] = None, - isbn: Optional[str] = None, - issn: Optional[str] = None, - heldByGroup: Optional[str] = None, + oclcNumber: Union[int, str], + holdingsAllEditions: Optional[bool] = None, + holdingsAllVariantRecords: Optional[bool] = None, + holdingsFilterFormat: Optional[List[str]] = None, + heldInCountry: Optional[str] = None, heldInState: Optional[str] = None, - itemType: Optional[str] = None, - itemSubType: Optional[str] = None, - offset: Optional[int] = None, - limit: Optional[int] = None, + heldByGroup: Optional[str] = None, + heldBySymbol: Optional[List[str]] = None, + heldByInstitutionID: Optional[List[int]] = None, + heldByLibraryType: Optional[List[str]] = None, + lat: Optional[float] = None, + lon: Optional[float] = None, + distance: Optional[int] = None, + unit: str = "M", hooks: Optional[Dict[str, Callable]] = None, - ) -> Response: + ) -> Optional[Response]: """ - Finds member shared print holdings for specified item. - Uses /bibs-retained-holdings endpoint. + Given an OCLC number, get summary of holdings. Query may contain + only one of: heldByInstitutionId, heldByGroup, heldBySymbol, heldInCountry, + heldInState or combination of lat, lon and distance. If using lat/lon + arguments, query must contain a valid distance argument. + Uses /search/summary-holdings endpoint. Args: - oclcNumber: OCLC bibliographic record number; can be - an integer, or string that can include - OCLC # prefix - isbn: ISBN without any dashes, - example: '978149191646x' - issn: ISSN (hyphenated, example: '0099-1234') - heldByGroup: restricts to holdings held by group symbol - heldInState: restricts to holings held by institutions - in requested state, example: "NY" - itemType: restricts results to specified item type (example - 'book' or 'vis') - itemSubType: restricts results to specified item sub type - examples: 'book-digital' or 'audiobook-cd' - offset: start position of bibliographic records to - return; default 1 - limit: maximum nuber of records to return; - maximum 50, default 10 - "" + oclcNumber: OCLC bibliographic record number; can be an + integer or string with or without OCLC Number + prefix + holdingsAllEditions: get holdings for all editions; + options: True, False + holdingsAllVariantRecords: get holdings for specific edition across + all variant records; options: True, False + holdingsFilterFormat: get holdings for specific itemSubType, + example: book-digital + heldInCountry: limits to holdings held by institutions + in requested country + heldInState: limits to holdings held by institutions + in requested state, example: 'US-NY' + heldByGroup: limits to holdings held by institutions + indicated by group symbol + heldBySymbol: limits to holdings held by institutions + indicated by institution symbol + heldByInstitutionID: limits to holdings held by institutions + indicated by institution registryID + heldByLibraryType: limits to holdings held by library type, + options: 'PUBLIC', 'ALL' + lat: limit to latitude, example: 37.502508 + lon: limit to longitute, example: -122.22702 + distance: distance from latitude and longitude + unit: unit of distance param; options: + 'M' (miles) or 'K' (kilometers), default is 'M' + hooks: Requests library hook system that can be used + for signal event handling. For more information + see the [Requests docs](https://requests. + readthedocs.io/en/master/user/advanced/ + #event-hooks) + Returns: - `requests.Response` object + `requests.Response` instance """ - if not any([oclcNumber, isbn, issn]): - raise WorldcatSessionError( - "Missing required argument. " - "One of the following args are required: oclcNumber, issn, isbn" - ) - - if oclcNumber is not None: - try: - oclcNumber = verify_oclc_number(oclcNumber) - except InvalidOclcNumber: - raise WorldcatSessionError("Invalid OCLC # was passed as an argument") + oclcNumber = verify_oclc_number(oclcNumber) - url = self._url_member_shared_print_holdings() + url = self._url_search_general_holdings_summary() header = {"Accept": "application/json"} payload = { "oclcNumber": oclcNumber, - "isbn": isbn, - "issn": issn, - "heldByGroup": heldByGroup, + "holdingsAllEditions": holdingsAllEditions, + "holdingsAllVariantRecords": holdingsAllVariantRecords, + "holdingsFilterFormat": holdingsFilterFormat, + "heldInCountry": heldInCountry, "heldInState": heldInState, - "offset": offset, - "limit": limit, + "heldByGroup": heldByGroup, + "heldBySymbol": heldBySymbol, + "heldByInstitutionID": heldByInstitutionID, + "heldByLibraryType": heldByLibraryType, + "lat": lat, + "lon": lon, + "distance": distance, + "unit": unit, } # prep request diff --git a/bookops_worldcat/py.typed b/bookops_worldcat/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/bookops_worldcat/query.py b/bookops_worldcat/query.py index 21ec2f5..41fb7b9 100644 --- a/bookops_worldcat/query.py +++ b/bookops_worldcat/query.py @@ -8,8 +8,7 @@ import sys from requests.models import PreparedRequest -from requests.exceptions import ConnectionError, HTTPError, Timeout - +from requests.exceptions import ConnectionError, HTTPError, Timeout, RetryError from .errors import WorldcatRequestError @@ -19,12 +18,13 @@ class Query: """ - Sends a request to OClC service and unifies received exceptions + Sends a request to OCLC service and unifies exceptions. Query object handles refreshing expired token before request is made to the web service. `Query.response` attribute is `requests.Response` instance that - can be parsed to exctract received information from the web service. + can be parsed to extract information received from the web service. + """ def __init__( @@ -35,46 +35,36 @@ def __init__( Union[int, float, Tuple[int, int], Tuple[float, float]] ] = None, ) -> None: - """ + """Initializes Query object. + Args: session: `metadata_api.MetadataSession` instance prepared_request: `requests.models.PreparedRequest` instance timeout: how long to wait for server to send data before giving up - Raises: - WorldcatRequestError + Raises: + WorldcatRequestError: If the request encounters an error """ if not isinstance(prepared_request, PreparedRequest): - raise AttributeError("Invalid type for argument 'prepared_request'.") + raise TypeError("Invalid type for argument 'prepared_request'.") # make sure access token is still valid and if not request a new one if session.authorization.is_expired(): session._get_new_access_token() - self.response = None - try: self.response = session.send(prepared_request, timeout=timeout) - - if "/ih/data" in prepared_request.url: - if self.response.status_code == 409: - # HTTP 409 code returns when trying to set/unset - # holdings on already set/unset record - # It is reasonable not to raise any exceptions - # in this case - pass # pragma: no cover - else: - self.response.raise_for_status() - else: - self.response.raise_for_status() + self.response.raise_for_status() except HTTPError as exc: raise WorldcatRequestError( - f"{exc}. Server response: {self.response.content}" + f"{exc}. Server response: " # type: ignore + f"{self.response.content.decode('utf-8')}" ) - except (Timeout, ConnectionError): + except (Timeout, ConnectionError, RetryError): raise WorldcatRequestError(f"Connection Error: {sys.exc_info()[0]}") - except: + + except Exception: raise WorldcatRequestError(f"Unexpected request error: {sys.exc_info()[0]}") diff --git a/bookops_worldcat/utils.py b/bookops_worldcat/utils.py index ed0cd29..5605d3d 100644 --- a/bookops_worldcat/utils.py +++ b/bookops_worldcat/utils.py @@ -11,7 +11,7 @@ def _str2list(s: str) -> List[str]: """Converts str into list - use for list of OCLC numbers""" - return [n.strip() for n in s.split(",")] + return [n.strip() for n in s.split(",") if n.strip()] def prep_oclc_number_str(oclcNumber: str) -> str: @@ -22,11 +22,12 @@ def prep_oclc_number_str(oclcNumber: str) -> str: oclcNumber: OCLC record as string Returns: - oclcNumber as int + oclcNumber as str """ - if "ocm" in oclcNumber or "ocn" in oclcNumber: + + if oclcNumber.strip().startswith("ocm") or oclcNumber.strip().startswith("ocn"): oclcNumber = oclcNumber.strip()[3:] - elif "on" in oclcNumber: + elif oclcNumber.strip().startswith("on"): oclcNumber = oclcNumber.strip()[2:] try: @@ -44,20 +45,20 @@ def verify_oclc_number(oclcNumber: Union[int, str]) -> str: oclcNumber: OCLC record number Returns: - oclcNumber + oclcNumber as str """ - if oclcNumber is None: + if not oclcNumber: raise InvalidOclcNumber("Argument 'oclcNumber' is missing.") - elif type(oclcNumber) is int: + elif isinstance(oclcNumber, int): return str(oclcNumber) - elif type(oclcNumber) is str: - return prep_oclc_number_str(oclcNumber) # type: ignore + elif isinstance(oclcNumber, str): + return prep_oclc_number_str(oclcNumber) else: - raise InvalidOclcNumber("Argument 'oclc_number' is of invalid type.") + raise InvalidOclcNumber("Argument 'oclcNumber' is of invalid type.") def verify_oclc_numbers(oclcNumbers: Union[str, List[Union[str, int]]]) -> List[str]: @@ -70,21 +71,25 @@ def verify_oclc_numbers(oclcNumbers: Union[str, List[Union[str, int]]]) -> List[ they can be integers or strings with or without OCLC # prefix; if str the numbers must be separated by comma - Returns: - vetted_numbers: list of vetted oclcNumbers - """ - # change to list if comma separated string - if type(oclcNumbers) is str and oclcNumbers != "": - oclcNumbers = _str2list(oclcNumbers) # type: ignore + Returns: + vetted_numbers as list - if not oclcNumbers or type(oclcNumbers) is not list: + """ + if isinstance(oclcNumbers, str): + oclcNumbers_lst = _str2list(oclcNumbers) + elif isinstance(oclcNumbers, list): + oclcNumbers_lst = oclcNumbers # type: ignore + else: + raise InvalidOclcNumber( + "Argument 'oclcNumbers' must be a list or comma separated string " + "of valid OCLC #s." + ) + if not oclcNumbers_lst: raise InvalidOclcNumber( - "Argument 'oclcNumbers' must be a list or comma separated string of valid OCLC #." + "Argument 'oclcNumbers' must be a list or comma separated string " + "of valid OCLC #s." ) - try: - vetted_numbers = [str(verify_oclc_number(n)) for n in oclcNumbers] - return vetted_numbers - except InvalidOclcNumber: - raise InvalidOclcNumber("One of passed OCLC #s is invalid.") + vetted_numbers = [verify_oclc_number(n) for n in oclcNumbers_lst] + return vetted_numbers diff --git a/dev-requirements.txt b/dev-requirements.txt index 8766a7e..1159bdc 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,352 +1,434 @@ -atomicwrites==1.4.0 ; python_version >= "3.7" and python_version < "4.0" and sys_platform == "win32" \ - --hash=sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197 \ - --hash=sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a -attrs==21.4.0 ; python_version >= "3.7" and python_version < "4.0" \ - --hash=sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4 \ - --hash=sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd -black==22.1.0 ; python_version >= "3.7" and python_version < "4.0" \ - --hash=sha256:07e5c049442d7ca1a2fc273c79d1aecbbf1bc858f62e8184abe1ad175c4f7cc2 \ - --hash=sha256:0e21e1f1efa65a50e3960edd068b6ae6d64ad6235bd8bfea116a03b21836af71 \ - --hash=sha256:1297c63b9e1b96a3d0da2d85d11cd9bf8664251fd69ddac068b98dc4f34f73b6 \ - --hash=sha256:228b5ae2c8e3d6227e4bde5920d2fc66cc3400fde7bcc74f480cb07ef0b570d5 \ - --hash=sha256:2d6f331c02f0f40aa51a22e479c8209d37fcd520c77721c034517d44eecf5912 \ - --hash=sha256:2ff96450d3ad9ea499fc4c60e425a1439c2120cbbc1ab959ff20f7c76ec7e866 \ - --hash=sha256:3524739d76b6b3ed1132422bf9d82123cd1705086723bc3e235ca39fd21c667d \ - --hash=sha256:35944b7100af4a985abfcaa860b06af15590deb1f392f06c8683b4381e8eeaf0 \ - --hash=sha256:373922fc66676133ddc3e754e4509196a8c392fec3f5ca4486673e685a421321 \ - --hash=sha256:5fa1db02410b1924b6749c245ab38d30621564e658297484952f3d8a39fce7e8 \ - --hash=sha256:6f2f01381f91c1efb1451998bd65a129b3ed6f64f79663a55fe0e9b74a5f81fd \ - --hash=sha256:742ce9af3086e5bd07e58c8feb09dbb2b047b7f566eb5f5bc63fd455814979f3 \ - --hash=sha256:7835fee5238fc0a0baf6c9268fb816b5f5cd9b8793423a75e8cd663c48d073ba \ - --hash=sha256:8871fcb4b447206904932b54b567923e5be802b9b19b744fdff092bd2f3118d0 \ - --hash=sha256:a7c0192d35635f6fc1174be575cb7915e92e5dd629ee79fdaf0dcfa41a80afb5 \ - --hash=sha256:b1a5ed73ab4c482208d20434f700d514f66ffe2840f63a6252ecc43a9bc77e8a \ - --hash=sha256:c8226f50b8c34a14608b848dc23a46e5d08397d009446353dad45e04af0c8e28 \ - --hash=sha256:ccad888050f5393f0d6029deea2a33e5ae371fd182a697313bdbd835d3edaf9c \ - --hash=sha256:dae63f2dbf82882fa3b2a3c49c32bffe144970a573cd68d247af6560fc493ae1 \ - --hash=sha256:e2f69158a7d120fd641d1fa9a921d898e20d52e44a74a6fbbcc570a62a6bc8ab \ - --hash=sha256:efbadd9b52c060a8fc3b9658744091cb33c31f830b3f074422ed27bad2b18e8f \ - --hash=sha256:f5660feab44c2e3cb24b2419b998846cbb01c23c7fe645fee45087efa3da2d61 \ - --hash=sha256:fdb8754b453fb15fad3f72cd9cad3e16776f0964d67cf30ebcbf10327a3777a3 -certifi==2021.10.8 ; python_version >= "3.7" and python_version < "4.0" \ - --hash=sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872 \ - --hash=sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569 -charset-normalizer==2.0.11 ; python_version >= "3.7" and python_version < "4.0" \ - --hash=sha256:2842d8f5e82a1f6aa437380934d5e1cd4fcf2003b06fed6940769c164a480a45 \ - --hash=sha256:98398a9d69ee80548c762ba991a4728bfc3836768ed226b3945908d1a688371c -click==8.0.3 ; python_version >= "3.7" and python_version < "4.0" \ - --hash=sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3 \ - --hash=sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b -colorama==0.4.4 ; python_version >= "3.7" and python_version < "4.0" and (sys_platform == "win32" or platform_system == "Windows") \ - --hash=sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b \ - --hash=sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2 -coverage[toml]==6.3.1 ; python_version >= "3.7" and python_version < "4.0" \ - --hash=sha256:1245ab82e8554fa88c4b2ab1e098ae051faac5af829efdcf2ce6b34dccd5567c \ - --hash=sha256:1bc6d709939ff262fd1432f03f080c5042dc6508b6e0d3d20e61dd045456a1a0 \ - --hash=sha256:25e73d4c81efa8ea3785274a2f7f3bfbbeccb6fcba2a0bdd3be9223371c37554 \ - --hash=sha256:276b13cc085474e482566c477c25ed66a097b44c6e77132f3304ac0b039f83eb \ - --hash=sha256:2aed4761809640f02e44e16b8b32c1a5dee5e80ea30a0ff0912158bde9c501f2 \ - --hash=sha256:2dd70a167843b4b4b2630c0c56f1b586fe965b4f8ac5da05b6690344fd065c6b \ - --hash=sha256:352c68e233409c31048a3725c446a9e48bbff36e39db92774d4f2380d630d8f8 \ - --hash=sha256:3f2b05757c92ad96b33dbf8e8ec8d4ccb9af6ae3c9e9bd141c7cc44d20c6bcba \ - --hash=sha256:448d7bde7ceb6c69e08474c2ddbc5b4cd13c9e4aa4a717467f716b5fc938a734 \ - --hash=sha256:463e52616ea687fd323888e86bf25e864a3cc6335a043fad6bbb037dbf49bbe2 \ - --hash=sha256:482fb42eea6164894ff82abbcf33d526362de5d1a7ed25af7ecbdddd28fc124f \ - --hash=sha256:56c4a409381ddd7bbff134e9756077860d4e8a583d310a6f38a2315b9ce301d0 \ - --hash=sha256:56d296cbc8254a7dffdd7bcc2eb70be5a233aae7c01856d2d936f5ac4e8ac1f1 \ - --hash=sha256:5e15d424b8153756b7c903bde6d4610be0c3daca3986173c18dd5c1a1625e4cd \ - --hash=sha256:618eeba986cea7f621d8607ee378ecc8c2504b98b3fdc4952b30fe3578304687 \ - --hash=sha256:61d47a897c1e91f33f177c21de897267b38fbb45f2cd8e22a710bcef1df09ac1 \ - --hash=sha256:621f6ea7260ea2ffdaec64fe5cb521669984f567b66f62f81445221d4754df4c \ - --hash=sha256:6a5cdc3adb4f8bb8d8f5e64c2e9e282bc12980ef055ec6da59db562ee9bdfefa \ - --hash=sha256:6c3f6158b02ac403868eea390930ae64e9a9a2a5bbfafefbb920d29258d9f2f8 \ - --hash=sha256:704f89b87c4f4737da2860695a18c852b78ec7279b24eedacab10b29067d3a38 \ - --hash=sha256:72128176fea72012063200b7b395ed8a57849282b207321124d7ff14e26988e8 \ - --hash=sha256:78fbb2be068a13a5d99dce9e1e7d168db880870f7bc73f876152130575bd6167 \ - --hash=sha256:7bff3a98f63b47464480de1b5bdd80c8fade0ba2832c9381253c9b74c4153c27 \ - --hash=sha256:84f2436d6742c01136dd940ee158bfc7cf5ced3da7e4c949662b8703b5cd8145 \ - --hash=sha256:9976fb0a5709988778ac9bc44f3d50fccd989987876dfd7716dee28beed0a9fa \ - --hash=sha256:9ad0a117b8dc2061ce9461ea4c1b4799e55edceb236522c5b8f958ce9ed8fa9a \ - --hash=sha256:9e3dd806f34de38d4c01416344e98eab2437ac450b3ae39c62a0ede2f8b5e4ed \ - --hash=sha256:9eb494070aa060ceba6e4bbf44c1bc5fa97bfb883a0d9b0c9049415f9e944793 \ - --hash=sha256:9fde6b90889522c220dd56a670102ceef24955d994ff7af2cb786b4ba8fe11e4 \ - --hash=sha256:9fff3ff052922cb99f9e52f63f985d4f7a54f6b94287463bc66b7cdf3eb41217 \ - --hash=sha256:a06c358f4aed05fa1099c39decc8022261bb07dfadc127c08cfbd1391b09689e \ - --hash=sha256:a4f923b9ab265136e57cc14794a15b9dcea07a9c578609cd5dbbfff28a0d15e6 \ - --hash=sha256:c5b81fb37db76ebea79aa963b76d96ff854e7662921ce742293463635a87a78d \ - --hash=sha256:d5ed164af5c9078596cfc40b078c3b337911190d3faeac830c3f1274f26b8320 \ - --hash=sha256:d651fde74a4d3122e5562705824507e2f5b2d3d57557f1916c4b27635f8fbe3f \ - --hash=sha256:de73fca6fb403dd72d4da517cfc49fcf791f74eee697d3219f6be29adf5af6ce \ - --hash=sha256:e647a0be741edbb529a72644e999acb09f2ad60465f80757da183528941ff975 \ - --hash=sha256:e92c7a5f7d62edff50f60a045dc9542bf939758c95b2fcd686175dd10ce0ed10 \ - --hash=sha256:eeffd96882d8c06d31b65dddcf51db7c612547babc1c4c5db6a011abe9798525 \ - --hash=sha256:f5a4551dfd09c3bd12fca8144d47fe7745275adf3229b7223c2f9e29a975ebda \ - --hash=sha256:fac0bcc5b7e8169bffa87f0dcc24435446d329cbc2b5486d155c2e0f3b493ae1 -ghp-import==2.0.2 ; python_version >= "3.7" and python_version < "4.0" \ - --hash=sha256:5f8962b30b20652cdffa9c5a9812f7de6bcb56ec475acac579807719bf242c46 \ - --hash=sha256:947b3771f11be850c852c64b561c600fdddf794bab363060854c1ee7ad05e071 -idna==3.3 ; python_version >= "3.7" and python_version < "4.0" \ - --hash=sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff \ - --hash=sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d -importlib-metadata==4.10.1 ; python_version >= "3.7" and python_version < "4.0" \ - --hash=sha256:899e2a40a8c4a1aec681feef45733de8a6c58f3f6a0dbed2eb6574b4387a77b6 \ - --hash=sha256:951f0d8a5b7260e9db5e41d429285b5f451e928479f19d80818878527d36e95e -iniconfig==1.1.1 ; python_version >= "3.7" and python_version < "4.0" \ - --hash=sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3 \ - --hash=sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32 -jinja2==3.0.3 ; python_version >= "3.7" and python_version < "4.0" \ - --hash=sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8 \ - --hash=sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7 -markdown==3.3.6 ; python_version >= "3.7" and python_version < "4.0" \ - --hash=sha256:76df8ae32294ec39dcf89340382882dfa12975f87f45c3ed1ecdb1e8cefc7006 \ - --hash=sha256:9923332318f843411e9932237530df53162e29dc7a4e2b91e35764583c46c9a3 -markupsafe==2.0.1 ; python_version >= "3.7" and python_version < "4.0" \ - --hash=sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298 \ - --hash=sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64 \ - --hash=sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b \ - --hash=sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194 \ - --hash=sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567 \ - --hash=sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff \ - --hash=sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724 \ - --hash=sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74 \ - --hash=sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646 \ - --hash=sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35 \ - --hash=sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6 \ - --hash=sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a \ - --hash=sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6 \ - --hash=sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad \ - --hash=sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26 \ - --hash=sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38 \ - --hash=sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac \ - --hash=sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7 \ - --hash=sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6 \ - --hash=sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047 \ - --hash=sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75 \ - --hash=sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f \ - --hash=sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b \ - --hash=sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135 \ - --hash=sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8 \ - --hash=sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a \ - --hash=sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a \ - --hash=sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1 \ - --hash=sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9 \ - --hash=sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864 \ - --hash=sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914 \ - --hash=sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee \ - --hash=sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f \ - --hash=sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18 \ - --hash=sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8 \ - --hash=sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2 \ - --hash=sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d \ - --hash=sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b \ - --hash=sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b \ - --hash=sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86 \ - --hash=sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6 \ - --hash=sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f \ - --hash=sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb \ - --hash=sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833 \ - --hash=sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28 \ - --hash=sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e \ - --hash=sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415 \ - --hash=sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902 \ - --hash=sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f \ - --hash=sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d \ - --hash=sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9 \ - --hash=sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d \ - --hash=sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145 \ - --hash=sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066 \ - --hash=sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c \ - --hash=sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1 \ - --hash=sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a \ - --hash=sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207 \ - --hash=sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f \ - --hash=sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53 \ - --hash=sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd \ - --hash=sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134 \ - --hash=sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85 \ - --hash=sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9 \ - --hash=sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5 \ - --hash=sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94 \ - --hash=sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509 \ - --hash=sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51 \ - --hash=sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872 -mergedeep==1.3.4 ; python_version >= "3.7" and python_version < "4.0" \ +black==23.12.1 ; python_version >= "3.8" and python_version < "4.0" \ + --hash=sha256:0808494f2b2df923ffc5723ed3c7b096bd76341f6213989759287611e9837d50 \ + --hash=sha256:1fa88a0f74e50e4487477bc0bb900c6781dbddfdfa32691e780bf854c3b4a47f \ + --hash=sha256:25e57fd232a6d6ff3f4478a6fd0580838e47c93c83eaf1ccc92d4faf27112c4e \ + --hash=sha256:2d9e13db441c509a3763a7a3d9a49ccc1b4e974a47be4e08ade2a228876500ec \ + --hash=sha256:3e1b38b3135fd4c025c28c55ddfc236b05af657828a8a6abe5deec419a0b7055 \ + --hash=sha256:3fa4be75ef2a6b96ea8d92b1587dd8cb3a35c7e3d51f0738ced0781c3aa3a5a3 \ + --hash=sha256:4ce3ef14ebe8d9509188014d96af1c456a910d5b5cbf434a09fef7e024b3d0d5 \ + --hash=sha256:4f0031eaa7b921db76decd73636ef3a12c942ed367d8c3841a0739412b260a54 \ + --hash=sha256:602cfb1196dc692424c70b6507593a2b29aac0547c1be9a1d1365f0d964c353b \ + --hash=sha256:6d1bd9c210f8b109b1762ec9fd36592fdd528485aadb3f5849b2740ef17e674e \ + --hash=sha256:78baad24af0f033958cad29731e27363183e140962595def56423e626f4bee3e \ + --hash=sha256:8d4df77958a622f9b5a4c96edb4b8c0034f8434032ab11077ec6c56ae9f384ba \ + --hash=sha256:97e56155c6b737854e60a9ab1c598ff2533d57e7506d97af5481141671abf3ea \ + --hash=sha256:9c4352800f14be5b4864016882cdba10755bd50805c95f728011bcb47a4afd59 \ + --hash=sha256:a4d6a9668e45ad99d2f8ec70d5c8c04ef4f32f648ef39048d010b0689832ec6d \ + --hash=sha256:a920b569dc6b3472513ba6ddea21f440d4b4c699494d2e972a1753cdc25df7b0 \ + --hash=sha256:ae76c22bde5cbb6bfd211ec343ded2163bba7883c7bc77f6b756a1049436fbb9 \ + --hash=sha256:b18fb2ae6c4bb63eebe5be6bd869ba2f14fd0259bda7d18a46b764d8fb86298a \ + --hash=sha256:c04b6d9d20e9c13f43eee8ea87d44156b8505ca8a3c878773f68b4e4812a421e \ + --hash=sha256:c88b3711d12905b74206227109272673edce0cb29f27e1385f33b0163c414bba \ + --hash=sha256:dd15245c8b68fe2b6bd0f32c1556509d11bb33aec9b5d0866dd8e2ed3dba09c2 \ + --hash=sha256:e0aaf6041986767a5e0ce663c7a2f0e9eaf21e6ff87a5f95cbf3675bfd4c41d2 +certifi==2024.2.2 ; python_version >= "3.8" and python_version < "4.0" \ + --hash=sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f \ + --hash=sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1 +charset-normalizer==3.3.2 ; python_version >= "3.8" and python_version < "4.0" \ + --hash=sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027 \ + --hash=sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087 \ + --hash=sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786 \ + --hash=sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8 \ + --hash=sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09 \ + --hash=sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185 \ + --hash=sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574 \ + --hash=sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e \ + --hash=sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519 \ + --hash=sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898 \ + --hash=sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269 \ + --hash=sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3 \ + --hash=sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f \ + --hash=sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6 \ + --hash=sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8 \ + --hash=sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a \ + --hash=sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73 \ + --hash=sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc \ + --hash=sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714 \ + --hash=sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2 \ + --hash=sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc \ + --hash=sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce \ + --hash=sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d \ + --hash=sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e \ + --hash=sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6 \ + --hash=sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269 \ + --hash=sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96 \ + --hash=sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d \ + --hash=sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a \ + --hash=sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4 \ + --hash=sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77 \ + --hash=sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d \ + --hash=sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0 \ + --hash=sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed \ + --hash=sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068 \ + --hash=sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac \ + --hash=sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25 \ + --hash=sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8 \ + --hash=sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab \ + --hash=sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26 \ + --hash=sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2 \ + --hash=sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db \ + --hash=sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f \ + --hash=sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5 \ + --hash=sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99 \ + --hash=sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c \ + --hash=sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d \ + --hash=sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811 \ + --hash=sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa \ + --hash=sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a \ + --hash=sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03 \ + --hash=sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b \ + --hash=sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04 \ + --hash=sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c \ + --hash=sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001 \ + --hash=sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458 \ + --hash=sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389 \ + --hash=sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99 \ + --hash=sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985 \ + --hash=sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537 \ + --hash=sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238 \ + --hash=sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f \ + --hash=sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d \ + --hash=sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796 \ + --hash=sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a \ + --hash=sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143 \ + --hash=sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8 \ + --hash=sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c \ + --hash=sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5 \ + --hash=sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5 \ + --hash=sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711 \ + --hash=sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4 \ + --hash=sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6 \ + --hash=sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c \ + --hash=sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7 \ + --hash=sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4 \ + --hash=sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b \ + --hash=sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae \ + --hash=sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12 \ + --hash=sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c \ + --hash=sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae \ + --hash=sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8 \ + --hash=sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887 \ + --hash=sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b \ + --hash=sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4 \ + --hash=sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f \ + --hash=sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5 \ + --hash=sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33 \ + --hash=sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519 \ + --hash=sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561 +click==8.1.7 ; python_version >= "3.8" and python_version < "4.0" \ + --hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \ + --hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de +colorama==0.4.6 ; python_version >= "3.8" and python_version < "4.0" and (sys_platform == "win32" or platform_system == "Windows") \ + --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ + --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 +coverage[toml]==7.4.1 ; python_version >= "3.8" and python_version < "4.0" \ + --hash=sha256:0193657651f5399d433c92f8ae264aff31fc1d066deee4b831549526433f3f61 \ + --hash=sha256:02f2edb575d62172aa28fe00efe821ae31f25dc3d589055b3fb64d51e52e4ab1 \ + --hash=sha256:0491275c3b9971cdbd28a4595c2cb5838f08036bca31765bad5e17edf900b2c7 \ + --hash=sha256:077d366e724f24fc02dbfe9d946534357fda71af9764ff99d73c3c596001bbd7 \ + --hash=sha256:10e88e7f41e6197ea0429ae18f21ff521d4f4490aa33048f6c6f94c6045a6a75 \ + --hash=sha256:18e961aa13b6d47f758cc5879383d27b5b3f3dcd9ce8cdbfdc2571fe86feb4dd \ + --hash=sha256:1a78b656a4d12b0490ca72651fe4d9f5e07e3c6461063a9b6265ee45eb2bdd35 \ + --hash=sha256:1ed4b95480952b1a26d863e546fa5094564aa0065e1e5f0d4d0041f293251d04 \ + --hash=sha256:23b27b8a698e749b61809fb637eb98ebf0e505710ec46a8aa6f1be7dc0dc43a6 \ + --hash=sha256:23f5881362dcb0e1a92b84b3c2809bdc90db892332daab81ad8f642d8ed55042 \ + --hash=sha256:32a8d985462e37cfdab611a6f95b09d7c091d07668fdc26e47a725ee575fe166 \ + --hash=sha256:3468cc8720402af37b6c6e7e2a9cdb9f6c16c728638a2ebc768ba1ef6f26c3a1 \ + --hash=sha256:379d4c7abad5afbe9d88cc31ea8ca262296480a86af945b08214eb1a556a3e4d \ + --hash=sha256:3cacfaefe6089d477264001f90f55b7881ba615953414999c46cc9713ff93c8c \ + --hash=sha256:3e3424c554391dc9ef4a92ad28665756566a28fecf47308f91841f6c49288e66 \ + --hash=sha256:46342fed0fff72efcda77040b14728049200cbba1279e0bf1188f1f2078c1d70 \ + --hash=sha256:536d609c6963c50055bab766d9951b6c394759190d03311f3e9fcf194ca909e1 \ + --hash=sha256:5d6850e6e36e332d5511a48a251790ddc545e16e8beaf046c03985c69ccb2676 \ + --hash=sha256:6008adeca04a445ea6ef31b2cbaf1d01d02986047606f7da266629afee982630 \ + --hash=sha256:64e723ca82a84053dd7bfcc986bdb34af8d9da83c521c19d6b472bc6880e191a \ + --hash=sha256:6b00e21f86598b6330f0019b40fb397e705135040dbedc2ca9a93c7441178e74 \ + --hash=sha256:6d224f0c4c9c98290a6990259073f496fcec1b5cc613eecbd22786d398ded3ad \ + --hash=sha256:6dceb61d40cbfcf45f51e59933c784a50846dc03211054bd76b421a713dcdf19 \ + --hash=sha256:7ac8f8eb153724f84885a1374999b7e45734bf93a87d8df1e7ce2146860edef6 \ + --hash=sha256:85ccc5fa54c2ed64bd91ed3b4a627b9cce04646a659512a051fa82a92c04a448 \ + --hash=sha256:869b5046d41abfea3e381dd143407b0d29b8282a904a19cb908fa24d090cc018 \ + --hash=sha256:8bdb0285a0202888d19ec6b6d23d5990410decb932b709f2b0dfe216d031d218 \ + --hash=sha256:8dfc5e195bbef80aabd81596ef52a1277ee7143fe419efc3c4d8ba2754671756 \ + --hash=sha256:8e738a492b6221f8dcf281b67129510835461132b03024830ac0e554311a5c54 \ + --hash=sha256:918440dea04521f499721c039863ef95433314b1db00ff826a02580c1f503e45 \ + --hash=sha256:9641e21670c68c7e57d2053ddf6c443e4f0a6e18e547e86af3fad0795414a628 \ + --hash=sha256:9d2f9d4cc2a53b38cabc2d6d80f7f9b7e3da26b2f53d48f05876fef7956b6968 \ + --hash=sha256:a07f61fc452c43cd5328b392e52555f7d1952400a1ad09086c4a8addccbd138d \ + --hash=sha256:a3277f5fa7483c927fe3a7b017b39351610265308f5267ac6d4c2b64cc1d8d25 \ + --hash=sha256:a4a3907011d39dbc3e37bdc5df0a8c93853c369039b59efa33a7b6669de04c60 \ + --hash=sha256:aeb2c2688ed93b027eb0d26aa188ada34acb22dceea256d76390eea135083950 \ + --hash=sha256:b094116f0b6155e36a304ff912f89bbb5067157aff5f94060ff20bbabdc8da06 \ + --hash=sha256:b8ffb498a83d7e0305968289441914154fb0ef5d8b3157df02a90c6695978295 \ + --hash=sha256:b9bb62fac84d5f2ff523304e59e5c439955fb3b7f44e3d7b2085184db74d733b \ + --hash=sha256:c61f66d93d712f6e03369b6a7769233bfda880b12f417eefdd4f16d1deb2fc4c \ + --hash=sha256:ca6e61dc52f601d1d224526360cdeab0d0712ec104a2ce6cc5ccef6ed9a233bc \ + --hash=sha256:ca7b26a5e456a843b9b6683eada193fc1f65c761b3a473941efe5a291f604c74 \ + --hash=sha256:d12c923757de24e4e2110cf8832d83a886a4cf215c6e61ed506006872b43a6d1 \ + --hash=sha256:d17bbc946f52ca67adf72a5ee783cd7cd3477f8f8796f59b4974a9b59cacc9ee \ + --hash=sha256:dfd1e1b9f0898817babf840b77ce9fe655ecbe8b1b327983df485b30df8cc011 \ + --hash=sha256:e0860a348bf7004c812c8368d1fc7f77fe8e4c095d661a579196a9533778e156 \ + --hash=sha256:f2f5968608b1fe2a1d00d01ad1017ee27efd99b3437e08b83ded9b7af3f6f766 \ + --hash=sha256:f3771b23bb3675a06f5d885c3630b1d01ea6cac9e84a01aaf5508706dba546c5 \ + --hash=sha256:f68ef3660677e6624c8cace943e4765545f8191313a07288a53d3da188bd8581 \ + --hash=sha256:f86f368e1c7ce897bf2457b9eb61169a44e2ef797099fb5728482b8d69f3f016 \ + --hash=sha256:f90515974b39f4dea2f27c0959688621b46d96d5a626cf9c53dbc653a895c05c \ + --hash=sha256:fe558371c1bdf3b8fa03e097c523fb9645b8730399c14fe7721ee9c9e2a545d3 +exceptiongroup==1.2.0 ; python_version >= "3.8" and python_version < "3.11" \ + --hash=sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14 \ + --hash=sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68 +ghp-import==2.1.0 ; python_version >= "3.8" and python_version < "4.0" \ + --hash=sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619 \ + --hash=sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343 +idna==3.6 ; python_version >= "3.8" and python_version < "4.0" \ + --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ + --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f +importlib-metadata==7.0.1 ; python_version >= "3.8" and python_version < "4.0" \ + --hash=sha256:4805911c3a4ec7c3966410053e9ec6a1fecd629117df5adee56dfc9432a1081e \ + --hash=sha256:f238736bb06590ae52ac1fab06a3a9ef1d8dce2b7a35b5ab329371d6c8f5d2cc +importlib-resources==6.1.1 ; python_version >= "3.8" and python_version < "4.0" \ + --hash=sha256:3893a00122eafde6894c59914446a512f728a0c1a45f9bb9b63721b6bacf0b4a \ + --hash=sha256:e8bf90d8213b486f428c9c39714b920041cb02c184686a3dee24905aaa8105d6 +iniconfig==2.0.0 ; python_version >= "3.8" and python_version < "4.0" \ + --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \ + --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 +jinja2==3.1.3 ; python_version >= "3.8" and python_version < "4.0" \ + --hash=sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa \ + --hash=sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90 +markdown==3.5.2 ; python_version >= "3.8" and python_version < "4.0" \ + --hash=sha256:d43323865d89fc0cb9b20c75fc8ad313af307cc087e84b657d9eec768eddeadd \ + --hash=sha256:e1ac7b3dc550ee80e602e71c1d168002f062e49f1b11e26a36264dafd4df2ef8 +markupsafe==2.1.5 ; python_version >= "3.8" and python_version < "4.0" \ + --hash=sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf \ + --hash=sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff \ + --hash=sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f \ + --hash=sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3 \ + --hash=sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532 \ + --hash=sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f \ + --hash=sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617 \ + --hash=sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df \ + --hash=sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4 \ + --hash=sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906 \ + --hash=sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f \ + --hash=sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4 \ + --hash=sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8 \ + --hash=sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371 \ + --hash=sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2 \ + --hash=sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465 \ + --hash=sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52 \ + --hash=sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6 \ + --hash=sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169 \ + --hash=sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad \ + --hash=sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2 \ + --hash=sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0 \ + --hash=sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029 \ + --hash=sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f \ + --hash=sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a \ + --hash=sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced \ + --hash=sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5 \ + --hash=sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c \ + --hash=sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf \ + --hash=sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9 \ + --hash=sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb \ + --hash=sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad \ + --hash=sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3 \ + --hash=sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1 \ + --hash=sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46 \ + --hash=sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc \ + --hash=sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a \ + --hash=sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee \ + --hash=sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900 \ + --hash=sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5 \ + --hash=sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea \ + --hash=sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f \ + --hash=sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5 \ + --hash=sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e \ + --hash=sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a \ + --hash=sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f \ + --hash=sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50 \ + --hash=sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a \ + --hash=sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b \ + --hash=sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4 \ + --hash=sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff \ + --hash=sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2 \ + --hash=sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46 \ + --hash=sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b \ + --hash=sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf \ + --hash=sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5 \ + --hash=sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5 \ + --hash=sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab \ + --hash=sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd \ + --hash=sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68 +mergedeep==1.3.4 ; python_version >= "3.8" and python_version < "4.0" \ --hash=sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8 \ --hash=sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307 -mike==1.1.2 ; python_version >= "3.7" and python_version < "4.0" \ - --hash=sha256:4c307c28769834d78df10f834f57f810f04ca27d248f80a75f49c6fa2d1527ca \ - --hash=sha256:56c3f1794c2d0b5fdccfa9b9487beb013ca813de2e3ad0744724e9d34d40b77b -mkapi==1.0.14 ; python_version >= "3.7" and python_version < "4.0" \ +mike==2.0.0 ; python_version >= "3.8" and python_version < "4.0" \ + --hash=sha256:566f1cab1a58cc50b106fb79ea2f1f56e7bfc8b25a051e95e6eaee9fba0922de \ + --hash=sha256:87f496a65900f93ba92d72940242b65c86f3f2f82871bc60ebdcffc91fad1d9e +mkapi==1.0.14 ; python_version >= "3.8" and python_version < "4.0" \ --hash=sha256:88bff6a183f09a5c80acf3c9edfc32e0ff3e2585589a9b6a962aae0467a79a12 \ --hash=sha256:b9b75ffeeeb6c29843ca703abf30464acac76c0867ca3ca66e3a825c0f258bb1 -mkdocs==1.2.3 ; python_version >= "3.7" and python_version < "4.0" \ - --hash=sha256:89f5a094764381cda656af4298727c9f53dc3e602983087e1fe96ea1df24f4c1 \ - --hash=sha256:a1fa8c2d0c1305d7fc2b9d9f607c71778572a8b110fb26642aa00296c9e6d072 -mypy-extensions==0.4.3 ; python_version >= "3.7" and python_version < "4.0" \ - --hash=sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d \ - --hash=sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8 -mypy==0.931 ; python_version >= "3.7" and python_version < "4.0" \ - --hash=sha256:0038b21890867793581e4cb0d810829f5fd4441aa75796b53033af3aa30430ce \ - --hash=sha256:1171f2e0859cfff2d366da2c7092b06130f232c636a3f7301e3feb8b41f6377d \ - --hash=sha256:1b06268df7eb53a8feea99cbfff77a6e2b205e70bf31743e786678ef87ee8069 \ - --hash=sha256:1b65714dc296a7991000b6ee59a35b3f550e0073411ac9d3202f6516621ba66c \ - --hash=sha256:1bf752559797c897cdd2c65f7b60c2b6969ffe458417b8d947b8340cc9cec08d \ - --hash=sha256:300717a07ad09525401a508ef5d105e6b56646f7942eb92715a1c8d610149714 \ - --hash=sha256:3c5b42d0815e15518b1f0990cff7a705805961613e701db60387e6fb663fe78a \ - --hash=sha256:4365c60266b95a3f216a3047f1d8e3f895da6c7402e9e1ddfab96393122cc58d \ - --hash=sha256:50c7346a46dc76a4ed88f3277d4959de8a2bd0a0fa47fa87a4cde36fe247ac05 \ - --hash=sha256:5b56154f8c09427bae082b32275a21f500b24d93c88d69a5e82f3978018a0266 \ - --hash=sha256:74f7eccbfd436abe9c352ad9fb65872cc0f1f0a868e9d9c44db0893440f0c697 \ - --hash=sha256:7b3f6f557ba4afc7f2ce6d3215d5db279bcf120b3cfd0add20a5d4f4abdae5bc \ - --hash=sha256:8c11003aaeaf7cc2d0f1bc101c1cc9454ec4cc9cb825aef3cafff8a5fdf4c799 \ - --hash=sha256:8ca7f8c4b1584d63c9a0f827c37ba7a47226c19a23a753d52e5b5eddb201afcd \ - --hash=sha256:c89702cac5b302f0c5d33b172d2b55b5df2bede3344a2fbed99ff96bddb2cf00 \ - --hash=sha256:d8f1ff62f7a879c9fe5917b3f9eb93a79b78aad47b533911b853a757223f72e7 \ - --hash=sha256:d9d2b84b2007cea426e327d2483238f040c49405a6bf4074f605f0156c91a47a \ - --hash=sha256:e839191b8da5b4e5d805f940537efcaa13ea5dd98418f06dc585d2891d228cf0 \ - --hash=sha256:f9fe20d0872b26c4bba1c1be02c5340de1019530302cf2dcc85c7f9fc3252ae0 \ - --hash=sha256:ff3bf387c14c805ab1388185dd22d6b210824e164d4bb324b195ff34e322d166 -packaging==21.3 ; python_version >= "3.7" and python_version < "4.0" \ - --hash=sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb \ - --hash=sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522 -pathspec==0.9.0 ; python_version >= "3.7" and python_version < "4.0" \ - --hash=sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a \ - --hash=sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1 -platformdirs==2.5.0 ; python_version >= "3.7" and python_version < "4.0" \ - --hash=sha256:30671902352e97b1eafd74ade8e4a694782bd3471685e78c32d0fdfd3aa7e7bb \ - --hash=sha256:8ec11dfba28ecc0715eb5fb0147a87b1bf325f349f3da9aab2cd6b50b96b692b -pluggy==1.0.0 ; python_version >= "3.7" and python_version < "4.0" \ - --hash=sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159 \ - --hash=sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3 -py==1.11.0 ; python_version >= "3.7" and python_version < "4.0" \ - --hash=sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719 \ - --hash=sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378 -pyparsing==3.0.7 ; python_version >= "3.7" and python_version < "4.0" \ - --hash=sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea \ - --hash=sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484 -pytest-cov==3.0.0 ; python_version >= "3.7" and python_version < "4.0" \ +mkdocs==1.5.3 ; python_version >= "3.8" and python_version < "4.0" \ + --hash=sha256:3b3a78e736b31158d64dbb2f8ba29bd46a379d0c6e324c2246c3bc3d2189cfc1 \ + --hash=sha256:eb7c99214dcb945313ba30426c2451b735992c73c2e10838f76d09e39ff4d0e2 +mypy-extensions==1.0.0 ; python_version >= "3.8" and python_version < "4.0" \ + --hash=sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d \ + --hash=sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782 +mypy==1.8.0 ; python_version >= "3.8" and python_version < "4.0" \ + --hash=sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6 \ + --hash=sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d \ + --hash=sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02 \ + --hash=sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d \ + --hash=sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3 \ + --hash=sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3 \ + --hash=sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3 \ + --hash=sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66 \ + --hash=sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259 \ + --hash=sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835 \ + --hash=sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd \ + --hash=sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d \ + --hash=sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8 \ + --hash=sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07 \ + --hash=sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b \ + --hash=sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e \ + --hash=sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6 \ + --hash=sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae \ + --hash=sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9 \ + --hash=sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d \ + --hash=sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a \ + --hash=sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592 \ + --hash=sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218 \ + --hash=sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817 \ + --hash=sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4 \ + --hash=sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410 \ + --hash=sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55 +packaging==23.2 ; python_version >= "3.8" and python_version < "4.0" \ + --hash=sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5 \ + --hash=sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7 +pathspec==0.12.1 ; python_version >= "3.8" and python_version < "4.0" \ + --hash=sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08 \ + --hash=sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712 +platformdirs==4.2.0 ; python_version >= "3.8" and python_version < "4.0" \ + --hash=sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068 \ + --hash=sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768 +pluggy==1.4.0 ; python_version >= "3.8" and python_version < "4.0" \ + --hash=sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981 \ + --hash=sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be +pyparsing==3.1.1 ; python_version >= "3.8" and python_version < "4.0" \ + --hash=sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb \ + --hash=sha256:ede28a1a32462f5a9705e07aea48001a08f7cf81a021585011deba701581a0db +pytest-cov==3.0.0 ; python_version >= "3.8" and python_version < "4.0" \ --hash=sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6 \ --hash=sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470 -pytest-mock==3.7.0 ; python_version >= "3.7" and python_version < "4.0" \ - --hash=sha256:5112bd92cc9f186ee96e1a92efc84969ea494939c3aead39c50f421c4cc69534 \ - --hash=sha256:6cff27cec936bf81dc5ee87f07132b807bcda51106b5ec4b90a04331cba76231 -pytest==7.0.0 ; python_version >= "3.7" and python_version < "4.0" \ - --hash=sha256:42901e6bd4bd4a0e533358a86e848427a49005a3256f657c5c8f8dd35ef137a9 \ - --hash=sha256:dad48ffda394e5ad9aa3b7d7ddf339ed502e5e365b1350e0af65f4a602344b11 -python-dateutil==2.8.2 ; python_version >= "3.7" and python_version < "4.0" \ +pytest-mock==3.12.0 ; python_version >= "3.8" and python_version < "4.0" \ + --hash=sha256:0972719a7263072da3a21c7f4773069bcc7486027d7e8e1f81d98a47e701bc4f \ + --hash=sha256:31a40f038c22cad32287bb43932054451ff5583ff094bca6f675df2f8bc1a6e9 +pytest==7.4.4 ; python_version >= "3.8" and python_version < "4.0" \ + --hash=sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280 \ + --hash=sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8 +python-dateutil==2.8.2 ; python_version >= "3.8" and python_version < "4.0" \ --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \ --hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9 -pyyaml-env-tag==0.1 ; python_version >= "3.7" and python_version < "4.0" \ +pyyaml-env-tag==0.1 ; python_version >= "3.8" and python_version < "4.0" \ --hash=sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb \ --hash=sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069 -pyyaml==6.0 ; python_version >= "3.7" and python_version < "4.0" \ - --hash=sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf \ - --hash=sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293 \ - --hash=sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b \ - --hash=sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57 \ - --hash=sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b \ - --hash=sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4 \ - --hash=sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07 \ - --hash=sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba \ - --hash=sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9 \ - --hash=sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287 \ - --hash=sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513 \ - --hash=sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0 \ - --hash=sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782 \ - --hash=sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0 \ - --hash=sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92 \ - --hash=sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f \ - --hash=sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2 \ - --hash=sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc \ - --hash=sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1 \ - --hash=sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c \ - --hash=sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86 \ - --hash=sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4 \ - --hash=sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c \ - --hash=sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34 \ - --hash=sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b \ - --hash=sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d \ - --hash=sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c \ - --hash=sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb \ - --hash=sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7 \ - --hash=sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737 \ - --hash=sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3 \ - --hash=sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d \ - --hash=sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358 \ - --hash=sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53 \ - --hash=sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78 \ - --hash=sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803 \ - --hash=sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a \ - --hash=sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f \ - --hash=sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174 \ - --hash=sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5 -requests==2.27.1 ; python_version >= "3.7" and python_version < "4.0" \ - --hash=sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61 \ - --hash=sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d -six==1.16.0 ; python_version >= "3.7" and python_version < "4.0" \ +pyyaml==6.0.1 ; python_version >= "3.8" and python_version < "4.0" \ + --hash=sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5 \ + --hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \ + --hash=sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df \ + --hash=sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741 \ + --hash=sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206 \ + --hash=sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27 \ + --hash=sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595 \ + --hash=sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62 \ + --hash=sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98 \ + --hash=sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696 \ + --hash=sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290 \ + --hash=sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9 \ + --hash=sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d \ + --hash=sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6 \ + --hash=sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867 \ + --hash=sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47 \ + --hash=sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486 \ + --hash=sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6 \ + --hash=sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3 \ + --hash=sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007 \ + --hash=sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938 \ + --hash=sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0 \ + --hash=sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c \ + --hash=sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735 \ + --hash=sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d \ + --hash=sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28 \ + --hash=sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4 \ + --hash=sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba \ + --hash=sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8 \ + --hash=sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5 \ + --hash=sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd \ + --hash=sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3 \ + --hash=sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0 \ + --hash=sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515 \ + --hash=sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c \ + --hash=sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c \ + --hash=sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924 \ + --hash=sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34 \ + --hash=sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43 \ + --hash=sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859 \ + --hash=sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673 \ + --hash=sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54 \ + --hash=sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a \ + --hash=sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b \ + --hash=sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab \ + --hash=sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa \ + --hash=sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c \ + --hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \ + --hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \ + --hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f +requests==2.31.0 ; python_version >= "3.8" and python_version < "4.0" \ + --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \ + --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1 +six==1.16.0 ; python_version >= "3.8" and python_version < "4.0" \ --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 -tomli==2.0.1 ; python_version >= "3.7" and python_version < "4.0" \ +tomli==2.0.1 ; python_version >= "3.8" and python_full_version <= "3.11.0a6" \ --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f -typed-ast==1.5.2 ; python_version < "3.8" and python_version >= "3.7" \ - --hash=sha256:0eb77764ea470f14fcbb89d51bc6bbf5e7623446ac4ed06cbd9ca9495b62e36e \ - --hash=sha256:1098df9a0592dd4c8c0ccfc2e98931278a6c6c53cb3a3e2cf7e9ee3b06153344 \ - --hash=sha256:183b183b7771a508395d2cbffd6db67d6ad52958a5fdc99f450d954003900266 \ - --hash=sha256:18fe320f354d6f9ad3147859b6e16649a0781425268c4dde596093177660e71a \ - --hash=sha256:26a432dc219c6b6f38be20a958cbe1abffcc5492821d7e27f08606ef99e0dffd \ - --hash=sha256:294a6903a4d087db805a7656989f613371915fc45c8cc0ddc5c5a0a8ad9bea4d \ - --hash=sha256:31d8c6b2df19a777bc8826770b872a45a1f30cfefcfd729491baa5237faae837 \ - --hash=sha256:33b4a19ddc9fc551ebabca9765d54d04600c4a50eda13893dadf67ed81d9a098 \ - --hash=sha256:42c47c3b43fe3a39ddf8de1d40dbbfca60ac8530a36c9b198ea5b9efac75c09e \ - --hash=sha256:525a2d4088e70a9f75b08b3f87a51acc9cde640e19cc523c7e41aa355564ae27 \ - --hash=sha256:58ae097a325e9bb7a684572d20eb3e1809802c5c9ec7108e85da1eb6c1a3331b \ - --hash=sha256:676d051b1da67a852c0447621fdd11c4e104827417bf216092ec3e286f7da596 \ - --hash=sha256:74cac86cc586db8dfda0ce65d8bcd2bf17b58668dfcc3652762f3ef0e6677e76 \ - --hash=sha256:8c08d6625bb258179b6e512f55ad20f9dfef019bbfbe3095247401e053a3ea30 \ - --hash=sha256:90904d889ab8e81a956f2c0935a523cc4e077c7847a836abee832f868d5c26a4 \ - --hash=sha256:963a0ccc9a4188524e6e6d39b12c9ca24cc2d45a71cfdd04a26d883c922b4b78 \ - --hash=sha256:bbebc31bf11762b63bf61aaae232becb41c5bf6b3461b80a4df7e791fabb3aca \ - --hash=sha256:bc2542e83ac8399752bc16e0b35e038bdb659ba237f4222616b4e83fb9654985 \ - --hash=sha256:c29dd9a3a9d259c9fa19d19738d021632d673f6ed9b35a739f48e5f807f264fb \ - --hash=sha256:c7407cfcad702f0b6c0e0f3e7ab876cd1d2c13b14ce770e412c0c4b9728a0f88 \ - --hash=sha256:da0a98d458010bf4fe535f2d1e367a2e2060e105978873c04c04212fb20543f7 \ - --hash=sha256:df05aa5b241e2e8045f5f4367a9f6187b09c4cdf8578bb219861c4e27c443db5 \ - --hash=sha256:f290617f74a610849bd8f5514e34ae3d09eafd521dceaa6cf68b3f4414266d4e \ - --hash=sha256:f30ddd110634c2d7534b2d4e0e22967e88366b0d356b24de87419cc4410c41b7 -typing-extensions==4.0.1 ; python_version >= "3.7" and python_version < "4.0" \ - --hash=sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e \ - --hash=sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b -urllib3==1.26.8 ; python_version >= "3.7" and python_version < "4" \ - --hash=sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed \ - --hash=sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c -verspec==0.1.0 ; python_version >= "3.7" and python_version < "4.0" \ +typing-extensions==4.9.0 ; python_version >= "3.8" and python_version < "4.0" \ + --hash=sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783 \ + --hash=sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd +urllib3==2.2.0 ; python_version >= "3.8" and python_version < "4.0" \ + --hash=sha256:051d961ad0c62a94e50ecf1af379c3aba230c66c710493493560c0c223c49f20 \ + --hash=sha256:ce3711610ddce217e6d113a2732fafad960a03fd0318c91faa79481e35c11224 +verspec==0.1.0 ; python_version >= "3.8" and python_version < "4.0" \ --hash=sha256:741877d5633cc9464c45a469ae2a31e801e6dbbaa85b9675d481cda100f11c31 \ --hash=sha256:c4504ca697b2056cdb4bfa7121461f5a0e81809255b41c03dda4ba823637c01e -watchdog==2.1.6 ; python_version >= "3.7" and python_version < "4.0" \ - --hash=sha256:25fb5240b195d17de949588628fdf93032ebf163524ef08933db0ea1f99bd685 \ - --hash=sha256:3386b367e950a11b0568062b70cc026c6f645428a698d33d39e013aaeda4cc04 \ - --hash=sha256:3becdb380d8916c873ad512f1701f8a92ce79ec6978ffde92919fd18d41da7fb \ - --hash=sha256:4ae38bf8ba6f39d5b83f78661273216e7db5b00f08be7592062cb1fc8b8ba542 \ - --hash=sha256:8047da932432aa32c515ec1447ea79ce578d0559362ca3605f8e9568f844e3c6 \ - --hash=sha256:8f1c00aa35f504197561060ca4c21d3cc079ba29cf6dd2fe61024c70160c990b \ - --hash=sha256:922a69fa533cb0c793b483becaaa0845f655151e7256ec73630a1b2e9ebcb660 \ - --hash=sha256:9693f35162dc6208d10b10ddf0458cc09ad70c30ba689d9206e02cd836ce28a3 \ - --hash=sha256:a0f1c7edf116a12f7245be06120b1852275f9506a7d90227648b250755a03923 \ - --hash=sha256:a36e75df6c767cbf46f61a91c70b3ba71811dfa0aca4a324d9407a06a8b7a2e7 \ - --hash=sha256:aba5c812f8ee8a3ff3be51887ca2d55fb8e268439ed44110d3846e4229eb0e8b \ - --hash=sha256:ad6f1796e37db2223d2a3f302f586f74c72c630b48a9872c1e7ae8e92e0ab669 \ - --hash=sha256:ae67501c95606072aafa865b6ed47343ac6484472a2f95490ba151f6347acfc2 \ - --hash=sha256:b2fcf9402fde2672545b139694284dc3b665fd1be660d73eca6805197ef776a3 \ - --hash=sha256:b52b88021b9541a60531142b0a451baca08d28b74a723d0c99b13c8c8d48d604 \ - --hash=sha256:b7d336912853d7b77f9b2c24eeed6a5065d0a0cc0d3b6a5a45ad6d1d05fb8cd8 \ - --hash=sha256:bd9ba4f332cf57b2c1f698be0728c020399ef3040577cde2939f2e045b39c1e5 \ - --hash=sha256:be9be735f827820a06340dff2ddea1fb7234561fa5e6300a62fe7f54d40546a0 \ - --hash=sha256:cca7741c0fcc765568350cb139e92b7f9f3c9a08c4f32591d18ab0a6ac9e71b6 \ - --hash=sha256:d0d19fb2441947b58fbf91336638c2b9f4cc98e05e1045404d7a4cb7cddc7a65 \ - --hash=sha256:e02794ac791662a5eafc6ffeaf9bcc149035a0e48eb0a9d40a8feb4622605a3d \ - --hash=sha256:e0f30db709c939cabf64a6dc5babb276e6d823fd84464ab916f9b9ba5623ca15 \ - --hash=sha256:e92c2d33858c8f560671b448205a268096e17870dcf60a9bb3ac7bfbafb7f5f9 -zipp==3.7.0 ; python_version >= "3.7" and python_version < "4.0" \ - --hash=sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d \ - --hash=sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375 +watchdog==3.0.0 ; python_version >= "3.8" and python_version < "4.0" \ + --hash=sha256:0e06ab8858a76e1219e68c7573dfeba9dd1c0219476c5a44d5333b01d7e1743a \ + --hash=sha256:13bbbb462ee42ec3c5723e1205be8ced776f05b100e4737518c67c8325cf6100 \ + --hash=sha256:233b5817932685d39a7896b1090353fc8efc1ef99c9c054e46c8002561252fb8 \ + --hash=sha256:25f70b4aa53bd743729c7475d7ec41093a580528b100e9a8c5b5efe8899592fc \ + --hash=sha256:2b57a1e730af3156d13b7fdddfc23dea6487fceca29fc75c5a868beed29177ae \ + --hash=sha256:336adfc6f5cc4e037d52db31194f7581ff744b67382eb6021c868322e32eef41 \ + --hash=sha256:3aa7f6a12e831ddfe78cdd4f8996af9cf334fd6346531b16cec61c3b3c0d8da0 \ + --hash=sha256:3ed7c71a9dccfe838c2f0b6314ed0d9b22e77d268c67e015450a29036a81f60f \ + --hash=sha256:4c9956d27be0bb08fc5f30d9d0179a855436e655f046d288e2bcc11adfae893c \ + --hash=sha256:4d98a320595da7a7c5a18fc48cb633c2e73cda78f93cac2ef42d42bf609a33f9 \ + --hash=sha256:4f94069eb16657d2c6faada4624c39464f65c05606af50bb7902e036e3219be3 \ + --hash=sha256:5113334cf8cf0ac8cd45e1f8309a603291b614191c9add34d33075727a967709 \ + --hash=sha256:51f90f73b4697bac9c9a78394c3acbbd331ccd3655c11be1a15ae6fe289a8c83 \ + --hash=sha256:5d9f3a10e02d7371cd929b5d8f11e87d4bad890212ed3901f9b4d68767bee759 \ + --hash=sha256:7ade88d0d778b1b222adebcc0927428f883db07017618a5e684fd03b83342bd9 \ + --hash=sha256:7c5f84b5194c24dd573fa6472685b2a27cc5a17fe5f7b6fd40345378ca6812e3 \ + --hash=sha256:7e447d172af52ad204d19982739aa2346245cc5ba6f579d16dac4bfec226d2e7 \ + --hash=sha256:8ae9cda41fa114e28faf86cb137d751a17ffd0316d1c34ccf2235e8a84365c7f \ + --hash=sha256:8f3ceecd20d71067c7fd4c9e832d4e22584318983cabc013dbf3f70ea95de346 \ + --hash=sha256:9fac43a7466eb73e64a9940ac9ed6369baa39b3bf221ae23493a9ec4d0022674 \ + --hash=sha256:a70a8dcde91be523c35b2bf96196edc5730edb347e374c7de7cd20c43ed95397 \ + --hash=sha256:adfdeab2da79ea2f76f87eb42a3ab1966a5313e5a69a0213a3cc06ef692b0e96 \ + --hash=sha256:ba07e92756c97e3aca0912b5cbc4e5ad802f4557212788e72a72a47ff376950d \ + --hash=sha256:c07253088265c363d1ddf4b3cdb808d59a0468ecd017770ed716991620b8f77a \ + --hash=sha256:c9d8c8ec7efb887333cf71e328e39cffbf771d8f8f95d308ea4125bf5f90ba64 \ + --hash=sha256:d00e6be486affb5781468457b21a6cbe848c33ef43f9ea4a73b4882e5f188a44 \ + --hash=sha256:d429c2430c93b7903914e4db9a966c7f2b068dd2ebdd2fa9b9ce094c7d459f33 +zipp==3.17.0 ; python_version >= "3.8" and python_version < "4.0" \ + --hash=sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31 \ + --hash=sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0 diff --git a/docs/about.md b/docs/about.md index 0f80740..3e3bc40 100644 --- a/docs/about.md +++ b/docs/about.md @@ -1,5 +1,4 @@ -## BookOps - +# BookOps [BookOps](https://sites.google.com/a/nypl.org/bookops/home) is a fully consolidated, shared library technical services organization that serves the [Brooklyn Public Library](https://www.bklynlibrary.org/) (BPL) and the [New York Public Library](https://www.nypl.org/) (NYPL). -BookOps-Worldcat was glued together by [Tomasz Kalata](mailto:klingaroo@gmail.com) with contribution by [Miriam Gloger](). \ No newline at end of file +BookOps-Worldcat was glued together by [Tomasz Kalata](mailto:klingaroo@gmail.com) and [Charlotte Kostelic](mailto:charlottekostelic@gmail.com) with contributions by [Miriam Gloger](). \ No newline at end of file diff --git a/docs/advanced.md b/docs/advanced.md new file mode 100644 index 0000000..34b9afe --- /dev/null +++ b/docs/advanced.md @@ -0,0 +1,141 @@ + +# Advanced Usage + +## OCLC Number Formatting +`MetadataSession` accepts OCLC numbers in methods' arguments as integers or strings with or without a prefix (eg. "ocm", "ocn", or "on"). The following are all acceptable: + +```python title="Acceptable oclcNumber arguments" +session.brief_bibs_get(oclcNumber="ocm00012345") +session.brief_bibs_search(oclcNumber="00054321") +session.bib_get_classification(oclcNumber=12121) +``` +The `bib_get_current_oclc_number` and `holdings_get_current` methods accept multiple OCLC Numbers passed to the `oclcNumbers` argument. For these methods OCLC Numbers can be passed as a list of strings and/or integers or a string with the numbers separated by commas. The following are all acceptable: + +```python title="Acceptable oclcNumbers arguments" +session.holdings_get_current(oclcNumbers=["ocm00012345", "00012346", "12347"]) +session.holdings_get_current(oclcNumbers=["ocm00012345", "00012346", 12347]) +session.bib_get_current_oclc_number(oclcNumbers="ocm00012345, 00012346, 12347") +``` + +## Authentication +### WorldcatAccessToken +A `WorldcatAccessToken` object retains the underlying Requests object functionality ([`requests.Request`](https://requests.readthedocs.io/en/latest/api/#requests.request)) which can be accessed via the `.server_response` attribute: + +```python title="Obtaining an Access Token" +from bookops_worldcat import WorldcatAccessToken + +token = WorldcatAccessToken( + key="my_WSKey", + secret="my_secret", + scopes="WorldCatMetadataAPI", + agent="my_app/version 1.0.0" +) +print(token.server_response.status_code) +#>200 +print(token.server_response.elapsed): +#>0:00:00.650108 +``` +Detailed information can be accessed using the `.json()` method. +```{ .json title="token.server_response.json()" .no-copy} +{ + "access_token": "tk_TokenString", + "expires_at": "2024-03-14 19:52:37Z", + "authenticating_institution_id": "00001", + "principalID": "", + "context_institution_id": "00001", + "scopes": "WorldCatMetadataAPI:view_brief_bib", + "token_type": "bearer", + "expires_in": 1199, + "principalIDNS": "" +} +``` +Users can check if the token has expired by calling the `is_expired` method: +```python title="token.is_expired()" +print(token.is_expired()) +#>False +``` +A failed token request raises a `WorldcatAuthorizationError` which provides the error code and detailed message returned by the server. + +```python title="WorldcatAuthorizationError" +from bookops_worldcat import WorldcatAccessToken + +token = WorldcatAccessToken( + key="my_WSKey", + secret="my_secret", + scopes="MetadataAPI", + agent="my_app/version 1.0.0" +) +print(token) +#>bookops_worldcat.errors.WorldcatAuthorizationError: b'{"code":403,"message":"Invalid scope(s): MetadataAPI (MetadataAPI) [Invalid service specified, Not on key]"}' +``` + +#### Identifying your institution +Though uncommon, users can request that OCLC set up their WSKeys to allow them to work on behalf of multiple institutions. The user can then authenticate on behalf of any of the institutions associated with that WSKey. + +If your WSKey is set up to work on behalf of multiple institutions, you can identify your institution when initiating a `WorldcatAccessToken` object. Pass the Registry ID for the institution you wish to work on behalf of to the scopes parameter as context. + +```python title="Access Token with Context" +token = WorldcatAccessToken( + key="my_WSKey", + secret="my_secret", + scopes="WorldCatMetadataAPI context:00001", + agent="my_app/1.0.0" +) +``` + +### MetadataSession +#### Event hooks +`MetadataSession` methods support [Requests event hooks](https://requests.readthedocs.io/en/latest/user/advanced/#event-hooks) which can be passed as an argument: + +```python title="Event Hooks" +def print_url(response, *args, **kwargs): + print(response.url) + +hooks = {'response': print_url} +session.brief_bibs_get(850939579, hooks=hooks) +#>https://metadata.api.oclc.org/worldcat/search/brief-bibs/850939579 +``` + +#### Identifying your application +BookOps-Worldcat provides a default `user-agent` value in the headers of all requests to OCLC web services: `bookops-worldcat/{version}`. Users are encouraged to update the `user-agent` value to properly identify your application to OCLC servers. This will provide a useful piece of information for OCLC staff if they need to assist with any troubleshooting problems that may arise. + +To set a custom `user-agent` in a session simply pass it as an argument when initiating the session: +```python title="Custom user-agent" +session = MetadataSession(authorization=token, agent="my_client_name") +``` + +Alternatively, users can update the `.headers` attribute after initializing the session: +```python title="Update MetadataSession headers" +session.headers.update({"user-agent": "my-app/version 1.0"}) +``` + +The `user-agent` header can be set for an access token request as well. To do that simply pass it as the `agent` parameter when initiating `WorldcatAccessToken` object: +```python title="WorldcatAccessToken with custom agent" +token = WorldcatAccessToken( + key="my_WSKey", + secret="my_secret", + scopes="WorldCatMetadataAPI", + agent="my_app/1.0.0" +) +``` +#### Automatic Token Refresh +All requests made within a `MetadataSession` have a built-in access token auto-refresh feature. While a session is open, the current token will be checked for expiration before sending a request. If the token has expired, a new token will be obtained and the `MetadataSession` will continue to send requests. + + +#### Retry Failed Requests +Users can configure a `MetadataSession` to automatically retry failed requests. This functionality is customizable with the `totalRetries`, `backoffFactor`, `statusForcelist`, and `allowedMethods` arguments. + +!!! note + It is recommended that users only allow for automatic retries on timeouts or other server errors. Users should also keep their automatic retries as low as possible in order to not overburden the web service. Users should not set up automatic retries for authentication (401, 403) or malformed request errors (400). + +```python title="MetadataSession with Retries" +with MetadataSession( + authorization=token, + totalRetries=3, + backoffFactor=0.1, + statusForcelist=[500, 502, 503, 504], + allowedMethods=["GET"], +) as session: + session.bib_get("12334") +``` +Bookops-Worldcat will return a `RetryError` if a request is attempted up to the value of `totalRetries` and still fails. \ No newline at end of file diff --git a/docs/api/authorize.md b/docs/api/authorize.md new file mode 100644 index 0000000..cc77728 --- /dev/null +++ b/docs/api/authorize.md @@ -0,0 +1 @@ +::: bookops_worldcat.authorize \ No newline at end of file diff --git a/docs/api/errors.md b/docs/api/errors.md new file mode 100644 index 0000000..fd65109 --- /dev/null +++ b/docs/api/errors.md @@ -0,0 +1 @@ +::: bookops_worldcat.errors \ No newline at end of file diff --git a/docs/api/metadata_api.md b/docs/api/metadata_api.md new file mode 100644 index 0000000..fb7b52c --- /dev/null +++ b/docs/api/metadata_api.md @@ -0,0 +1 @@ +::: bookops_worldcat.metadata_api \ No newline at end of file diff --git a/docs/api/query.md b/docs/api/query.md new file mode 100644 index 0000000..c608714 --- /dev/null +++ b/docs/api/query.md @@ -0,0 +1 @@ +::: bookops_worldcat.query \ No newline at end of file diff --git a/docs/api/utils.md b/docs/api/utils.md new file mode 100644 index 0000000..a3ae6b4 --- /dev/null +++ b/docs/api/utils.md @@ -0,0 +1 @@ +::: bookops_worldcat.utils \ No newline at end of file diff --git a/docs/changelog.md b/docs/changelog.md index 7514d26..2e6bfc4 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,81 @@ # Changelog +## [1.0.0] - (3/22/2024) +### Added ++ Support for OCLC Metadata API Version 2.0 + + `MetadataSession` methods to support new functionality released in Metadata API 2.0 + + `bib_match` + + `bib_get_classification` + + `holdings_set_with_bib` and `holdings_unset_with_bib` + + New `MetadataSession` methods to support existing Metadata API functionality + + Bib Record Management and Validation + + `bib_create` + + `bib_replace` + + `bib_validate` + + Local Holdings Records + + `lhr_create` + + `lhr_delete` + + `lhr_get` + + `lhr_replace` + + Local Bibliographic Data + + `lbd_create` + + `lbd_delete` + + `lbd_get` + + `lbd_replace` + + Holdings Management + + `holdings_get_codes` ++ Support for [automatic retries of failed requests](https://bookops-cat.github.io/bookops-worldcat/advanced/#token-refresh-and-request-retries) ++ Support for [multi-institution WSKeys](https://bookops-cat.github.io/bookops-worldcat/advanced/#identifying-your-institution) ++ Support for Python 3.11 and 3.12 ++ New dev dependencies: + + types-requests (2.31.0.20240125) + + mkdocs-material (9.5.13) + +### Changed ++ `MetadataSession` methods that have been renamed and updated (replacing existing functionality in Bookops-Worldcat): + + `get_brief_bib` is now `brief_bibs_get` + + `get_full_bib` is now `bib_get` + + `holding_get_status` is now `holdings_get_current` + + `holding_set` is now `holdings_set` + + `holding_unset` is now `holdings_unset` + + `search_brief_bib_other_editions` is now `brief_bibs_get_other_editions` + + `search_brief_bibs` is now `brief_bibs_search` + + `search_current_control_numbers` is now `bib_get_current_oclc_number` + + `search_general_holdings` is now `summary_holdings_search` + + `search_shared_print_holdings` is now `shared_print_holdings_search` ++ `WorldcatAccessToken` + + `scopes` arg now only accepts strings. A `TypeError` is raised if `scopes` arg is passed a list + + `token_expires_at` attribute is now an aware `datetime` object (change made due to [`datetime.utcnow()`](https://docs.python.org/3/library/datetime.html#datetime.datetime.utcnow) deprecation) ++ Error handling: + + `TypeError` and `ValueError` replace `WorldcatAuthorizationError` when `WorldcatAccessToken` is passed an invalid arg. + + `MetadataSession` now raises `InvalidOclcNumber` exception when invalid OCLC identifiers are given ++ `pytest` configuration moved from `pytest.ini` to `pyproject.toml` ++ Updated and clarified type annotations for `MetadataSession` methods ++ Updated dependencies: + + requests: (2.31) ++ Updated dev dependencies: + + black (23.3.0) + + mike (2.0.0) + + mypy (1.0.14) ++ Documentation on [https://bookops-cat.github.io/bookops-worldcat/](https://bookops-cat.github.io/bookops-worldcat/) has been rewritten and reorganized + +### Fixed ++ `AttributeError` changed to `TypeError` if arg passed to `Query.prepared_request` is not a `PreparedRequest` ++ All args for methods within `MetadataSession` have been changed to camel case to be consisted with Metadata API documentation + + +### Removed ++ `principalID` and `principalIDNS` as args for `WorldcatAccessToken` ++ Automatic handling of large sets of oclcNumbers + + `_split_into_legal_volume` removed from `MetadataSession`; a `ValueError` is now raised if a method is passed too many oclcNumbers + + +### Deprecated ++ Support for Python 3.7 ++ 409 error handling for holdings set/unset requests ++ `WorldcatSessionError` + + Replaced with `TypeError` or `ValueError` in `WorldcatSession` + ## [0.5.0] - (3/11/2022) ### Added + feature to set and unset holdings for individual record for multiple institutions (/ih/institutionlist endpoint) @@ -27,10 +103,10 @@ ### Changed + Changes to `MetadataSession.search_brief_bibs` method due to /brief-bibs endpoint changes: + removed deprecated argument `heldBy` - + added `groupVariantRecord` and `preferredLanuage` argument + + added `groupVariantRecord` and `preferredLanguage` argument + modified `groupRelatedEditions` to allow boolean arguments + Changes to `MetadataSession.search_general_holdings` method due to API changes: - + added following arguements: `holdingsAllVariantRecords`, `preferredLanguage` + + added following arguments: `holdingsAllVariantRecords`, `preferredLanguage` + removed deprecated `heldBy` argument + Changes to `MetadataSession.search_brief_bib_other_editions`: + added `deweyNumber`, `datePublished`, `heldByGroup`, `heldBySymbol`, @@ -56,7 +132,7 @@ ## [0.3.3] - (12/28/2020) ### Added + Type hints -+ Default timeout in the MetadataSesssion extended to 5 seconds ++ Default timeout in the MetadataSession extended to 5 seconds ### Changed + Dependencies: @@ -65,7 +141,7 @@ ## [0.3.2] - (11/25/2020) ### Fixed -+ MetadataSession timeout paramerter correctly passed into every session request ++ MetadataSession timeout parameter correctly passed into every session request ## [0.3.1] - (11/24/2020) @@ -77,7 +153,7 @@ + Dependencies bump + certifi from 2020.6.20 to 2020.11.8 + requests from 2.24.0 to 2.25.0 - + urlib3 from 1.25.10 to 1.26.2 + + urllib3 from 1.25.10 to 1.26.2 ### Added + Added Python 3.9 testing to CI @@ -88,7 +164,7 @@ + Introduces multiple breaking changes compared to the previous version! + Dropped features related to the WorldCat Search API + Support for Worldcat Metadata API v.1.1 introduced in May 2020 -+ Supported Metdata API endpoints: ++ Supported Metadata API endpoints: + /bibs-retained-holdings + /bibs-summary-holdings + /brief-bibs @@ -119,7 +195,7 @@ ### Fixed + fixed hooks info in docstrings in `SearchSession` and `MetadataSession` - +[1.0.0]: https://github.com/BookOps-CAT/bookops-worldcat/compare/v0.5.0...v1.0.0 [0.5.0]: https://github.com/BookOps-CAT/bookops-worldcat/compare/v0.4.1...v0.5.0 [0.4.1]: https://github.com/BookOps-CAT/bookops-worldcat/compare/v0.4.0...v0.4.1 [0.4.0]: https://github.com/BookOps-CAT/bookops-worldcat/compare/v0.3.5...v0.4.0 diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 0000000..4d03cb7 --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,88 @@ +# How to Contribute +We welcome collaborators who would like to help expand and improve Bookops-Worldcat. Here are some ways to contribute. + +### Report bugs or suggest enhancements +Please use our [Github issue tracker](https://github.com/BookOps-CAT/bookops-worldcat/issues) to submit bug reports or request new features. + +### Contribute code or documentation +??? info + This page contains a draft of our contribution guidelines but there is still more for us to add. + + TO DO: + + + Add style guide for documentation + + docstring style conventions + + type hints + + how to build docs after making edits + + Add CI/CD info + +#### Style and Requirements +For new code contributions, please use the tools and standards in place for Bookops-Worldcat: + + + Code style: + + Formatting with [black](https://github.com/psf/black) + + Linting with [flake8](https://www.flake8rules.com/) + + Static type checking with [mypy](https://mypy-lang.org/) + + Dependency management and package publishing with [Poetry](https://github.com/python-poetry/poetry) + + Documentation written in Markdown using [MkDocs](https://www.mkdocs.org/) and plugins + + Theme is [Material for MkDocs](https://github.com/squidfunk/mkdocs-material) + + Versioning maintained with [Mike](https://github.com/jimporter/mike) + + API Documentation built with [MkAPI](https://github.com/daizutabi/mkapi/) + + Tests written with [pytest](https://docs.pytest.org/en/8.0.x/) + +??? tip + If you use VS Code there are certain extensions which will automate code formatting and support some of our code style requirements which may make your work easier while contributing to Bookops-Worldcat. Similar extensions are available on other IDEs. Those extensions include: + + + [Black Formatter](https://marketplace.visualstudio.com/items?itemName=ms-python.black-formatter) + + [Flake8](https://marketplace.visualstudio.com/items?itemName=ms-python.flake8) + + [Mypy Type Checker](https://marketplace.visualstudio.com/items?itemName=ms-python.mypy-type-checker) + + [Markdown All in One](https://marketplace.visualstudio.com/items?itemName=yzhang.markdown-all-in-one) + + Additions to add to your settings.json file: + ```json title="settings.json for VS Code" + { + "editor.defaultFormatter": "ms-python.black-formatter", + "editor.formatOnSave": true, + "flake8.args": [ + "--max-line-length=88", + ], + } + ``` + +#### Install and Setup +To get started contributing code to Bookops-Worldcat you will need: + + + Python 3.8 or newer + + `git` + + [`poetry`](https://python-poetry.org/docs/#installation) + +##### Install Poetry +Bookops-Worldcat uses `poetry` to manage virtual environments, dependencies, and publishing workflows. We use [`pipx`](https://python-poetry.org/docs/#installing-with-pipx) to run poetry (if you don't have `pipx`, see the [installation instructions](https://pipx.pypa.io/stable/installation/)). For other installation options, see the [`poetry`](https://python-poetry.org/docs/#installation) documentation. + +##### Fork the repo +Fork the repository in GitHub and clone your fork locally +```bash +git clone https://github.com//bookops-worldcat +cd bookops-worldcat +``` +##### Create a new branch for your changes +```bash +git checkout -b new-branch +``` +##### Create a virtual environment and install dependencies +Poetry will create a virtual environment, read the `pyproject.toml` and `poetry.lock` files, resolve dependencies, and install them with one command. +```python +poetry install +``` + +##### Run tests +Run tests before making changes on your fork. +??? info + Our live tests are designed to look for API credentials in a specific file/directory in a Windows environment. We will need to refactor the live tests to allow contributors to run live tests with their own API credentials and run live tests in a macOS environment. + +```python +# basic usage without webtests +python -m pytest "not webtest" +# with test coverage and without webtests +python -m pytest "not webtest" --cov=bookops_worldcat/ +``` \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index d8ac1e1..3ac4bf6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,518 +1,249 @@ # BookOps-Worldcat -[![Build Status](https://github.com/BookOps-CAT/bookops-marc/actions/workflows/unit-tests.yaml/badge.svg?branch=master)](https://github.com/BookOps-CAT/bookops-worldcat/actions) [![Coverage Status](https://coveralls.io/repos/github/BookOps-CAT/bookops-worldcat/badge.svg?branch=master&service=github)](https://coveralls.io/github/BookOps-CAT/bookops-worldcat?branch=master) [![PyPI version](https://badge.fury.io/py/bookops-worldcat.svg)](https://badge.fury.io/py/bookops-worldcat) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/bookops-worldcat) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) - -## Overview - -Requires Python 3.7 and up. - -Bookops-Worldcat is a Python wrapper around [OCLC's](https://www.oclc.org/en/home.html) [Worldcat](https://www.worldcat.org/) [Metadata](https://www.oclc.org/developer/develop/web-services/worldcat-metadata-api.en.html) API which supports changes released in the version 1.1 (May 2020) of the web service. The package features methods that utilize [search functionality](https://developer.api.oclc.org/wc-metadata-v1-1) of the API as well as [read-write endpoints](https://developer.api.oclc.org/wc-metadata). - -The Bookops-Worldcat package simplifies some of the OCLC API boilerplate, and ideally lowers the technological threshold for cataloging departments that may not have sufficient programming support to access and utilize those web services. Python language, with its gentle learning curve, has the potential to be a perfect vehicle towards this goal. +[![Build Status](https://github.com/BookOps-CAT/bookops-marc/actions/workflows/unit-tests.yaml/badge.svg?branch=main)](https://github.com/BookOps-CAT/bookops-worldcat/actions) [![Coverage Status](https://coveralls.io/repos/github/BookOps-CAT/bookops-worldcat/badge.svg?branch=main&service=github)](https://coveralls.io/github/BookOps-CAT/bookops-worldcat?branch=main) [![PyPI version](https://badge.fury.io/py/bookops-worldcat.svg)](https://badge.fury.io/py/bookops-worldcat) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/bookops-worldcat) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -This package takes advantage of the functionality of the popular [Requests library](https://requests.readthedocs.io/en/master/). Interaction with OCLC's services is built around Requests sessions. `MetadataSession` inherits all `requests.Session` properties. Returned server responses are `requests.Response` objects with [all of their properties and methods](https://requests.readthedocs.io/en/master/user/quickstart/#response-content). +Bookops-Worldcat is a Python wrapper around [OCLC's](https://www.oclc.org/en/home.html) [WorldCat](https://www.worldcat.org/) [Metadata API](https://www.oclc.org/developer/develop/web-services/worldcat-metadata-api.en.html). The package features methods that enable interactions with each endpoint of the API. -Authorizing a session simply requires passing an access token into `MetadataSession`. Opening a session allows the user to call specific methods which facilitate communication between the user's script/client and a particular endpoint of OCLC's service. Many of the hurdles related to making valid requests are hidden under the hood of this package, making it as simple as possible to access the functionalities of OCLC APIs. -Please note, not all features of the Metadata API are implemented because this tool was primarily built for our organization's specific needs. However, we are open to any collaboration to expand and improve the package. +The Bookops-Worldcat package simplifies some of the OCLC API boilerplate and ideally lowers the technological threshold for cataloging departments that may not have sufficient programming support to access and utilize the web services. Python, with its gentle learning curve, has the potential to be a perfect vehicle towards this goal. +Bookops-Worldcat version 1.0 supports changes released in version 2.0 (May 2023) of the OCLC Metadata API. -**Supported OCLC web services:** +## Overview -At the moment, the wrapper supports only [OAuth 2.0 endpoints and flows](https://www.oclc.org/developer/develop/authentication/oauth.en.html). The specific protocols are [Client Credential Grant](https://www.oclc.org/developer/develop/authentication/oauth/client-credentials-grant.en.html) and [Access Token](https://www.oclc.org/developer/develop/authentication/access-tokens.en.html) for authorization. +Requires Python 3.8 and up. -[Worldcat Metadata API](https://www.oclc.org/developer/develop/web-services/worldcat-metadata-api.en.html) is a read-write service for WorldCat. It allows adding and updating records in WorldCat, maintaining holdings, and working with local bibliographic data. Access to Metadata API requires OCLC credentials. The BookOps wrapper focuses on the following API operations: +Bookops-Worldcat takes advantage of the functionality of the popular [Requests library](https://requests.readthedocs.io/) and interactions with OCLC's services are built around 'Requests' sessions. `MetadataSession` inherits all `requests.Session` properties. Server responses are `requests.Response` objects with [all of their properties and methods](https://requests.readthedocs.io/en/latest/user/quickstart/). -+ Search functionality - + Find member shared print holdings (`/bibs-retained-holdings`) - + Get summary of holdings for known items (`/bibs-summary-holdings`) - + Brief bibliographic resources: - + Search brief bibliographic resources (`/brief-bibs`) - + Retrieve specific brief bibliographic resource (`/brief-bibs/{oclcNumber}`) - + Retrieve other editions related to a particular bibliographic resource (`/brief-bibs/{oclcNumber}/other-editions`) -+ Full bibliographic resources - + Retrieve full bibliographic record (`/bib-data`) - + Get current OCLC number (`/bib/checkcontrolnumber`) -+ Holdings - + Set and unset institution holding (`/ih/data`) - + Retrieve status of institution holdings (`/ih/checkholdings`) - + Set and unset institution holdings for a batch or records (`/ih/datalist`) - + Set and unset holdings for a single record for multiple intitutions (`/ih/institutionlist`) +Authorizing a web service session simply requires passing an access token to `MetadataSession`. Opening a session allows the user to call specific methods to facilitate communication between the user's script/client and a particular endpoint of the Metadata API. Many of the hurdles related to making valid requests are hidden under the hood of this package, making it as simple as possible. +Bookops-Worldcat supports [OAuth 2.0 endpoints and flows](https://www.oclc.org/developer/api/keys/oauth.en.html) and uses the [Client Credential Grant](https://www.oclc.org/developer/api/keys/oauth/client-credentials-grant.en.html) flow. ## Installation -To install use pip: +Use pip to install: `$ pip -m install bookops-worldcat` +## Interacting with the Metadata API +Users of the WorldCat Metadata API must have OCLC credentials. A web service key, or [WSKey](https://www.oclc.org/developer/develop/authentication/what-is-a-wskey.en.html), can be obtained via the [OCLC Developer Network](https://platform.worldcat.org/wskey/) site. More information about WSKeys is available on the [OCLC Developer Network site](https://www.oclc.org/developer/develop/authentication/how-to-request-a-wskey.en.html). -## Quickstart - -Worldcat Metadata API requires OCLC credentials which can be obtained at the [OCLC Developer Network](https://www.oclc.org/developer/home.en.html) site. +Querying the WorldCat Metadata API is a two step process. Users first pass their API credentials to the WorldCat Authorization Server to obtain an Access Token and then use that Access Token to query the Metadata API. -#### Obtaining Access Token +### Examples +Users obtain an Access Token by passing credential parameters into the `WorldcatAccessToken` object. -The Worldcat access token can be obtained by passing credential parameters into the `WorldcatAccessToken` object. +```python title="Authorizing a MetadataSession" +from bookops_worldcat import WorldcatAccessToken, MetadataSession -```python ->>> from bookops_worldcat import WorldcatAccessToken ->>> token = WorldcatAccessToken( +token = WorldcatAccessToken( key="my_WSKey", secret="my_secret", - scopes=["WorldCatMetadataAPI"], - principal_id="my_principal_id", - principlal_idns="my_principal_idns" + scopes="WorldCatMetadataAPI", ) ->>> print(token) -"access_token: 'tk_Yebz4BpEp9dAsghA7KpWx6dYD1OZKWBlHjqW', expires_at: '2020-01-01 17:19:58Z'" ->>> print(token.is_expired()) -False -``` - -Created `token` object can be directly passed into `MetadataSession` to authorize requests to the Metadata API web service: - -```Python ->>> from bookops_worldcat import MetadataSession ->>> session = MetadataSession(authorization=token) +print(token) +#>"access_token: 'tk_O4WFpJuidaaXJmb8wPb7aMSfJdYZg5XC9Ovo', expires_at: '2024-03-20 15:25:32Z'" +print(token.is_expired()) +#>False +session = MetadataSession(authorization=token) +print(session.headers) +#> {'User-Agent': 'bookops-worldcat/1.0.0', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive', 'Authorization': 'Bearer tk_xS0qvZs5j04ewpJeHUqNxQ1Y4LFprOKLw1ek'} ``` - -#### Searching Brief Bibliographic Records Using Metadata API - -The `MetadataSession` is authenticated using the `WorldcatAccessToken` object. The session allows searching brief records as well as retrieving full bibs in the MARC XML format. - -Basic usage: -```python +Once a `MetadataSession` is authenticated using a `WorldcatAccessToken` object, users can search WorldCat for bibliographic resources. Brief bib resources are returned in JSON format which can be parsed using the `.json()` method. +```python title="Brief Bib Search" from bookops_worldcat import MetadataSession with MetadataSession(authorization=token) as session: - results = session.search_brief_bibs(q="ti:zendegi AND au:greg egan") - print(results.json()) + response = session.brief_bibs_search( + q="ti:The Power Broker AND au: Caro, Robert" + ) + print(response.json()) ``` - -Returned brief bibliographic records are in the JSON format that can be parsed via `.json()` method. - -```json +```{ .json title="Brief Bib JSON Response" .no-copy} { - "numberOfRecords": 24, - "briefRecords": [ - { - "oclcNumber": "430840771", - "title": "Zendegi", - "creator": "Greg Egan", - "date": "2010", - "language": "eng", - "generalFormat": "Book", - "specificFormat": "PrintBook", - "edition": "First edition.", - "publisher": "Night Shade Books", - "mergedOclcNumbers": [ - "664026825" - ], - "catalogingInfo": { - "catalogingAgency": "BTCTA", - "transcribingAgency": "DLC" - } - }, - { - "oclcNumber": "961162511", - "title": "Zendegi", - "creator": "Greg Egan", - "date": "2013", - "language": "eng", - "generalFormat": "AudioBook", - "specificFormat": "CD", - "publisher": "Audible Studios on Brilliance Audio", - "mergedOclcNumbers": [ - "947806980" - ], - "catalogingInfo": { - "catalogingAgency": "AU@", - "transcribingAgency": "AU@" - } - }, - ] + "numberOfRecords": 89, + "briefRecords": [ + { + "oclcNumber": "1631862", + "title": "The power broker : Robert Moses and the fall of New York", + "creator": "Robert A. Caro", + "date": "1975", + "machineReadableDate": "1975", + "language": "eng", + "generalFormat": "Book", + "specificFormat": "PrintBook", + "edition": "Vintage Books edition", + "publisher": "Vintage Books", + "publicationPlace": "New York", + "isbns": [ + "0394720245", + "9780394720241" + ], + "mergedOclcNumbers": [ + "750986288", + "979848451", + "1171296546", + "1200988349", + "1200988563", + "1201968774", + "1202023560", + "1222888365", + "1282059511", + "1376480175" + ], + "catalogingInfo": { + "catalogingAgency": "DLC", + "catalogingLanguage": "eng", + "levelOfCataloging": " ", + "transcribingAgency": "DLC" + } + } + ] } ``` - -#### Retrieving Full Bibliographic Records - -To retrieve a full bibliographic record from WorldCat use the `.get_full_bib` method. The server returns records in MARC XML format by default. - -```python +Users can retrieve full bib records from WorldCat by passing the `bib_get` method an OCLC Number: +```python title="Get Full Bib Record" from bookops_worldcat import MetadataSession with MetadataSession(authorization=token) as session: - results = session.get_full_bib(oclcNumber=430840771) - print(results.text) -``` -```xml - - - - - - 00000cam a2200000 i 4500 - on1143317889 - OCoLC - 20200328101446.1 - 200305t20202019nyuabf b 001 0 eng c - - 2018957420 + result = session.bib_get("1631862") + print(result.text) +``` +```{ .xml title="Full Bib MARCXML Response" .no-copy} + + + 00000cam a2200000 i 4500 + ocm01631862 + OCoLC + 20240201163642.4 + 750320t19751974nyuabf b 001 0beng + + 75009557 - - NYP - eng - rda - NYP - - 9780316230049 - (pbk.) + + 9780394720241 + (paperback) - - Christakis, Nicholas A., - author. + + Caro, Robert A., + author. + + + The power broker : + Robert Moses and the fall of New York / + by Robert A. Caro. - - Blueprint : - the evolutionary origins of a good society / - Nicholas A. Christakis. + + Robert Moses and the fall of New York - - First Little, Brown Spark trade paperback edition. + + Vintage Books edition. - - New York, NY : - Little, Brown Spark, - 2020 + + New York : + Vintage Books, + 1975. - - - - http://worldcat.org/oclc/1143317889 - - -``` - -#### Updating Holdings - -`MetadataSession` can be used to check or set/unset your library holdings on a master record in Worldcat: - -example: -```python -result = session.holding_set(oclc_number="850939579") -print(result) - -``` - -```python -result = session.holding_get_status("850939579") -print(result.json()) -``` -```json -{ - "title": "850939579", - "content": { - "requestedOclcNumber": "850939579", - "currentOclcNumber": "850939579", - "institution": "NYP", - "holdingCurrentlySet": true, - "id": "http://worldcat.org/oclc/850939579" - }, - "updated": "2020-10-01T04:10:13.017Z" -} -``` + + +``` +Additional examples and a full outline of the functionality available in Bookops-Worldcat are available in the [Get Started](start.md) section. + +## Supported OCLC web services + +The [WorldCat Metadata API](https://www.oclc.org/developer/develop/web-services/worldcat-metadata-api.en.html) is a read-write service for WorldCat. It allows users to add and update records in WorldCat; maintain institution holdings; search WorldCat using the full suite of bibliographic record indexes; retrieve MARC records in MARCXML or MARC21; and work with local bibliographic and holdings data. Access to the Metadata API requires OCLC credentials. The BookOps-Worldcat wrapper supports requests to all endpoints of the WorldCat Metadata API: + ++ [Manage Bibliographic Records](manage_bibs.md) + + Validate bib record `/manage/bibs/validate/{validationLevel}` + + Get current OCLC number `/manage/bibs/current` + + Create bib record `/manage/bibs` + + Retrieve full bib record `/manage/bibs/{oclcNumber}` + + Replace bib record `/manage/bibs/{oclcNumber}` + + Find match for a bib record in WorldCat `/manage/bibs/match` ++ [Manage Institution Holdings](manage_holdings.md) + + Retrieve status of institution holdings `/manage/institution/holdings/current` + + Set institution holding with OCLC Number `/manage/institution/holdings/set/{oclcNumber}/set` + + Unset institution holding with OCLC Number `/manage/institution/holdings/unset/{oclcNumber}/unset` + + Set institution holding with MARC record `/manage/institution/holdings` + + Unset institution holding with MARC record `/manage/institution/holdings` + + Retrieve institution holding codes `/manage/institution/holdings/current` ++ [Manage Local Bibliographic Data](local.md) + + Create local bib data record `/manage/lbds` + + Retrieve local bib data record `/manage/lbds/{controlNumber}` + + Replace local bib data record `/manage/lbds/{controlNumber}` + + Delete local bib data record `/manage/lbds/{controlNumber}` ++ [Manage Local Holdings Records](local.md) + + Create local holdings record `/manage/lhrs` + + Retrieve local holdings record `/manage/lhrs/{controlNumber}` + + Replace local holdings record `/manage/lhrs/{controlNumber}` + + Delete local holdings record `/manage/lhrs/{controlNumber}` ++ [Search Member Shared Print Holdings](search.md) `/search/bibs-retained-holdings` ++ [Search Member General Holdings](search.md) + + Get summary of holdings for known items `/search/bibs-summary-holdings` + + Search and retrieve summary of holdings `/search/summary-holdings` ++ [Search Bibliographic Resources](search.md) + + Search brief bib resources `/search/brief-bibs` + + Retrieve specific brief bib resource `/search/brief-bibs/{oclcNumber}` + + Retrieve other editions related to a particular bib resource `/search/brief-bibs/{oclcNumber}/other-editions` + + Retrieve classification recommendations for an OCLC Number `/search/classification-bibs/{oclcNumber}` ++ [Search Local Holdings Resources](local.md) + + Search shared print local holdings resources `/search/retained-holdings` + + Retrieve local holdings resource `/search/my-holdings/{controlNumber}` + + Search local holdings resources `/search/my-holdings` + + Browse my local holdings resources `/browse/my-holdings` ++ [Search Local Bibliographic Resources](local.md) + + Retrieve local bibliographic resource `/search/my-local-bib-data/{controlNumber}` + + Search local bibliographic resources `/search/my-local-bib-data` + +## What's new in Bookops-Worldcat +See the [Changelog page](changelog.md) for a full outline of fixes and enhancements with each version. + +### Features in Version 1.0 + +New functionality available in version 1.0: + ++ Send requests to all endpoints of WorldCat Metadata API + + Match bib records and retrieve bib classification + + Create, update, and validate bib records + + Create, retrieve, update, and delete local bib and holdings records ++ Add automatic retries to failed requests ++ Authenticate and authorize for multiple institutions within `MetadataSession` ++ Support for Python 3.11 and 3.12 + +### Migration Information +Bookops-Worldcat 1.0 introduces many breaking changes for users of previous versions. Due to a complete refactor of the Metadata API, the methods from Bookops-Worldcat 0.5.0 have been rewritten. Most of the functionality from previous versions of the Metadata API is still available in Version 2.0. For a comparison of the functionality available in Versions 1.0, 1.1, and 2.0 of the Metadata API, see [OCLC's documentation](https://www.oclc.org/developer/api/oclc-apis/worldcat-metadata-api.en.html) and their [functionality comparison table](https://www.oclc.org/content/dam/developer-network/worldcat-metadata-api/worldcat-metadata-api-functionality-comparison.pdf). + +Versions 1.0 and 1.1 of the Metadata API will be sunset after April 30, 2024 at which point tools that rely on Bookops-Worldcat 0.5 will no longer be able to query the Metadata API. + + +##### Similar functionality, new method names +Certain functionality has been retained from Bookops-Worldcat Version 0.5 but the methods have been renamed. See below for changes: + + +|Functionality| Bookops-Worldcat 0.5 | Bookops-Worldcat 1.0 | +|-----------| ----------- | ------------ | +|get brief bib resource| `get_brief_bib` | `brief_bibs_get` | +|get full bib record| `get_full_bib` | `bib_get` | +|get current holdings for record| `holding_get_status` | `holdings_get_current` | +|set holdings on a record| `holding_set` | `holdings_set` | +|unset holdings on a record| `holding_unset` | `holdings_unset` | +|search for brief bibs| `search_brief_bibs` | `brief_bibs_search` | +|get other editions of brief bibs| `search_brief_bib_other_editions` |`brief_bibs_get_other_editions` | +|get current oclc number| `search_current_control_numbers` | `bib_get_current_oclc_number` | +|search member holdings| `search_general_holdings` | `summary_holdings_search` | +|search shared print holdings| `search_shared_print_holdings` | `shared_print_holdings_search` | + +##### Deprecated functionality +Certain functionality has been deprecated within Version 2.0 of the Metadata API in including the following methods from Bookops-Worldcat 0.5: + ++ `holdings_set` and `holdings_unset` + + Users are no longer able to set holdings on multiple records in a single request and should instead send separate requests for each record. ++ `holdings_set_multi_institutions` and `holdings_unset_multi_institutions` + + Users are no longer able to set holdings for multiple institutions with one request. + + If your WSKey is valid for multiple institutions, see section on [Identifying Your Institution](advanced.md#identifying-your-application) for an explanation of how to pass your RegistryID to the OCLC Authentication Server and obtain an Access Token. -For holdings operations on batches of records see [Advanced Usage>MetadataSession>Updating Holdings](https://bookops-cat.github.io/bookops-worldcat/#holdings) - -## Advanced Usage - -**Identifying your application** - -BookOps-Worldcat provides a default `user-agent` value in headers of all requests to OCLC web services: `bookops-worldcat/{version}`. It is encouraged to update the `user-agent` value to properly identify your application to OCLC servers. This will provide a useful piece of information for OCLC staff if they need to assist with troubleshooting problems that may arise. -To set a custom "user-agent" in a session simply pass is as an argument when initiating the session: -```python -session = MetadataSession(authorization=token, agent="my_client_name") -``` - -... or simply update its headers attribute: -```python -session.headers.update({"user-agent": "my-app/version 1.0"}) -``` - -The `user-agent` header can be set for an access token request as well. To do that simply pass it as the `agent` parameter when initiating `WorldcatAccessToken` object: -```python -token = WorldcatAccessToken( - key="my_WSKey", - secret="my_secret", - scopes=["WorldCatMetadataAPI"], - principal_id="my_principal_id", - principlal_idns="my_principal_idns", - agent="my_app/1.0.0" -) -``` - -**Event hooks** - -`MetadataSession` methods support [Requests event hooks](https://requests.readthedocs.io/en/latest/user/advanced/#event-hooks) which can be passed as an argument: - -```python -def print_url(response, *args, **kwargs): - print(response.url) - -hooks = {'response': print_url} -session.get_brief_bib(850939579, hooks=hooks) -``` - -#### WorldcatAccessToken - -Bookops-Worldcat utilizes OAuth 2.0 and Client Credential Grant flow to acquire Access Token. Please note, your OCLC credentials must allow access to the Metadata API in their scope to be permitted to make requests to the web service. - -Obtaining: -```python -from bookops_worldcat import WorldcatAccessToken -token = WorldcatAccessToken( - key="my_WSKey", - secret="my_secret", - scopes=["WorldCatMetadataAPI"], - principal_id="my_principal_id", - principlal_idns="my_principal_idns", - agent="my_app/version 1.0" -) -``` - -Token object retains underlying Requests object functionality (`requests.Request`) that can be accessed via the `.server_response` attribute: - -```python -print(token.server_response.status_code) -200 -print(token.server_response.elapsed): -0:00:00.650108 -print(token.server_response.json()) -``` -```json -{ -"user-agent": "bookops-worldcat/0.1.0", -"Accept-Encoding": "gzip, deflate", -"Accept": "application/json", -"Connection": "keep-alive", -"Content-Length": "67", -"Content-Type": "application/x-www-form-urlencoded", -"Authorization": "Basic encoded_authorization_here=" -} -``` - -Checking if the token has expired can be done by calling the `is_expired` method: -```python -print(token.is_expired()) -True -``` -A failed token request raises `WorldcatAuthorizationError` which provides a returned by the server error code and detailed message. -#### MetadataSession - -A wrapper around WorldCat Metadata API. `MetadataSession` inherits `requests.Session` methods. -Returned full bibliographic records are by default in MARC/XML format, but it is possible to receive OCLC's native CDF XML and the CDF translation into JSON serializations by supplying appropriate values in the `response_format` argument to the `get_full_bib` method. Search endpoints of the Metadata API return responses serialized into JSON format only. -All `MetadataSession` issued requests have a build-in access token auto-refresh feature. While a session is open, before any request is sent, a current token is checked for expiration and if needed a new access token is automatically obtained. - - -**OCLC numbers in methods' arguments** - -`MetadataSession` accepts OCLC numbers in methods' arguments as integers or strings with or without a prefix ("ocm", "ocn", "on"). The following are all acceptable: -```python -session.get_brief_bib(oclcNumber="ocm00012345") -session.get_brief_bib(oclcNumber="00012345") -session.get_brief_bib(oclcNumber=12345) -session.search_current_control_numbers(oclcNumbers=["ocm00012345", "00012346", 12347]) -``` - -##### Search Functionality - -MetadataSession supports the following search functionality: - -+ `get_brief_bib` retrieves a specific brief bibliographic resource -+ `search_brief_bib_other_editions` retrieves other editions related to a bibliographic resource specified with an OCLC # -+ `search_brief_bibs` retrieves brief resouces for a keyword or a fielded query -+ `search_current_control_numbers` retrieves current OCLC control numbers -+ `search_general_holdings` retrieves a summary of holdings for a specified item -+ `search_shared_print_holdings` finds member library holdings with a commitment to retain (Shared Print) - - -The server responses are returned in JSON format by default. - -**Obtaining brief record** - -```python -with MetadataSession(authorization=token) as session: - result = session.get_brief_bib(850940548) - print(results.json()) -``` -```json -{ - "oclcNumber": "850940548", - "title": "Record Builder Added This Test Record On 06/26/2013 13:07:06.", - "creator": "OCLC RecordBuilder.", - "date": "2012", - "language": "eng", - "generalFormat": "Book", - "specificFormat": "PrintBook", - "catalogingInfo": { - "catalogingAgency": "OCPSB", - "transcribingAgency": "OCPSB" - } -} -``` - -**Quering WorldCat** - -Metadata API provides quite robust methods to query WorldCat. In addition to a flexible query string that supports keyword and fielded searches, it is possibile to set further limits using various elements such as type of item, language, publishing date, etc. It is possible to specify the order of returned records by using the `orderBy` argument. Results are returned as brief records in JSON format. - -The query syntax is case-insensitive and allows keyword and phrase search (use quotation marks), boolean operators (AND, OR, NOT), wildcards (# - single character, ? - any number of additional characters), and truncation (use \* character). - - -keyword search with item type, language, and publishing date limiters: -```python -session.search_brief_bibs( - q="czarne oceany dukaj", - itemType="book", - inLanguage="pol", - datePublished="2015-2020" - orderBy="publicationDateDesc" -) -``` - -fielded query: -```python -session.search_brief_bibs( - q='ti="czarne oceany" AND au:jacek dukaj AND ge="science fiction"') -``` - -More about the query syntax can be found in [OCLC documentation](https://www.oclc.org/developer/develop/worldshare-platform/architecture/query-syntax.en.html) - - -##### Obtaining Full Bibliographic Records - -`session.get_full_bib()` method with OCLC number as an argument sends a request for a matching full bibliographic record in WorldCat. The Metadata API correctly matches requested OCLC numbers of records that have been merged together by returning the current master record. By default `get_full_bib` returns records in MARC XML format. - -Returned response is a `requests.Response` object with all its features: -```python -with MetadataSession(authorization=token) as session: - result = session.get_full_bib("00000000123") - print(result.status_code) - print(result.url) -200 -"https://worldcat.org/bib/data/00000000123" -``` -To avoid any `UnicodeEncodeError` it is recommended to access retrieved data with `.content` attribute of the response object: -```python -print(response.content) -``` - -##### Retrieving Current OCLC Number - -`MetadataSession.search_current_control_numbers` method allows retrieval of a current control number of the master record in WorldCat. Occasionally, records identified as duplicates in WorldCat have been merged. In that case a local control number may not correctly refer to an OCLC master record. Returned responses are in JSON format by default, but it's possible to pass `'application/atom+xml'` in the `response_format` argument to have the response serialized into xml. - -`search_current_control_numbers` method accepts control numbers as a list or a comma separated string: -```python -session.search_current_control_numbers(oclcNumbers="00012345,00012346,00012347") -session.search_current_control_numbers(oclcNumbers=[12345, 12346, 12347], response_format="application/atom+xml") -``` - -##### Holdings - -`MetadataSession` supports the following holdings operations: - -+ `holding_get_status` retrieves holding status of a requested record -+ `holding_set` sets holding on an individual bibliographic record -+ `holding_unset` deletes holding on an individual bibliographic record -+ `holdings_set` allows holdings to be set on multiple records, and is not limited by OCLC's 50 bib record limit -+ `holdings_unset` allows holdings to be deleted on multiple records, and is not limited to OCLC's 50 bib record restriction -+ `holdings_set_multi_institutions` allows to set holdings for a single record for multiple institutions -+ `holdings_unset_multi_institutions` deletes holdings on a single record for multiple institutions - -By default, responses are returned in `atom+json` format, but `atom+xml` can be specified: -```python -result = session.holding_get_status(oclcNumber="ocn123456789", response_format="application/atom+xml") -print(result.text) -``` -```xml - - - 1143317889 - 2020-04-25T05:21:10.233Z - - - 1143317889 - 1143317889 - NYP - true - http://worldcat.org/oclc/1143317889 - - - -``` - -Pass OCLC record numbers for batch operations as a list of strings or integers or comma separated string with or without a prefix: -```python -session.holdings_set( - oclcNumbers="00000000123,00000000124,00000000125,00000000126") - -session.holdings_unset(oclcNumbers=[123, 124, 125, 126]) -``` -The OCLC web service limits the number of records in a batch operation to 50, but `MetadataSession` permits larger batches by splitting the batch into chunks of 50 and automatically issuing multiple requests. The return object is a list of returned from server responses. - -```python -results = session.holdings_unset(oclcNumbers=[123, 124, 125, 126]) - -# print results of each batch of 50s -for r in results: - print(r.json()) -``` - -A consortium type of organizations that serve multiple libraries can utilize `holdings_set_multi_institutions` and `holdings_unset_multi_institutions` methods to set and unset holdings for selected libraries. List of library OCLC codes is passed as a comma separated string to instSymbol parameter. - -```python -results = session.holdings_set_multi_institutions(oclcNumber=1234, instSymbols="BKL,NYP") -``` - -## Examples - -Complex search query: -```python -from bookops_worldcat import WorldcatAccessToken, MetadataSession - -# obtain access token -token = WorldcatAccessToken( - key="my_WSKey", - secret="my_secret", - scopes=["WorldCatMetadataAPI"], - principal_id="my_principal_id", - principal_idns="my_principal_idns", - agent="my_app/version 1.0" -) - - -with MetadataSession(authorization=token) as session: - - # search Worlcat - response = session.search_brief_bibs( - q="su:civil war AND (su:antietam OR su:sharpsburg)", - datePublished="2000-2020", - inLanguage="eng", - inCatalogLanguage="eng", - catalogSource="dlc", - itemType="book", - itemSubType="digital", - orderBy="mostWidelyHeld", - limit=20) - first_bib = response.json()["briefRecords"][0] - first_bib_number = first_bib["oclcNumber"] - - # get full bib - response = session.get_full_bib(oclcNumber=first_bib_number) - print(response.content) -``` diff --git a/docs/local.md b/docs/local.md new file mode 100644 index 0000000..a128b15 --- /dev/null +++ b/docs/local.md @@ -0,0 +1,499 @@ +# Search and Manage Local Data +New functionality available in Version 1.0 of Bookops-Worldcat allows users to search and manage local bibliographic and holdings data via the Metadata API. + + +### Manage Local Bib Records +Users can manage local bib records in WorldCat in the same way that they manage WorldCat records (see [Managing Bibliographic Records](manage_bibs.md) for more information). Records can be retrieved in MARCXML or MARC21 formats. The default format for records is MARCXML. + +=== "lbd_create" + + ```python title="lbd_create MARCXML Request" + from bookops_worldcat import MetadataSession + from io import BytesIO + + with open("file.xml","rb") as xml_file: + for r in xml_file: + xml_record = BytesIO(r) + session = MetadataSession(authorization=token) + response = session.lbd_create( + record=xml_record, + recordFormat="application/marcxml+xml" + ) + print(response.content) + ``` + ```{ .xml title="lbd_create MARCXML Response" .no-copy} + + 00000n a2200000 4500 + 12345 + 3160 + 20240320120824.8 + + MyLSN + + + NYP + + + ``` +=== "lbd_get" + + ```python title="lbd_get MARCXML Request" + from bookops_worldcat import MetadataSession + + with MetadataSession(authorization=token) as session: + response = session.lbd_get("12345") + print(response.content) + + ``` + ```{ .xml title="lbd_get MARCXML Response" .no-copy} + + 00000n a2200000 4500 + 12345 + 3160 + 20240320120824.8 + + MyLSN + + + NYP + + + ``` + +=== "lbd_replace" + + ```python title="lbd_replace MARCXML Request" + from bookops_worldcat import MetadataSession + from io import BytesIO + + with open("file.xml","rb") as xml_file: + for r in xml_file: + xml_record = BytesIO(r) + session = MetadataSession(authorization=token) + response = session.lbd_replace( + controlNumber="12345", + record=xml_record, + recordFormat="application/marcxml+xml" + ) + print(response.content) + + ``` + ```{ .xml title="lbd_replace MARCXML Response" .no-copy} + + 00000n a2200000 4500 + 12345 + 3160 + 20240320120824.8 + + MyLSN + + + NYP + + + ``` + +=== "lbd_delete" + + ```python title="lbd_delete MARCXML Request" + from bookops_worldcat import MetadataSession + + with MetadataSession(authorization=token) as session: + response = session.lbd_delete("12345") + print(response.content) + + ``` + ```{ .xml title="lbd_delete MARCXML Response" .no-copy} + + 00000n a2200000 4500 + 12345 + 3160 + 20240320120824.8 + + MyLSN + + + NYP + + + ``` + +## Search Local Bib Resources +Users can search for and retrieve brief local bib resources `local_bibs_get` and `local_bibs_search` methods. The response will be in JSON format. + +=== "local_bibs_get" + + ```python title="local_bibs_get Request" + from bookops_worldcat import MetadataSession + + with MetadataSession(authorization=token) as session: + response = session.local_bibs_get(123456789) + print(response.json()) + ``` + ```{ .json title="local_bibs_get Response" .no-copy} + { + "controlNumber": 123456789, + "oclcNumber": "987654321", + "title": { + "uniformTitles": [ + "Test Book" + ] + }, + "contributor": { + "creators": [ + { + "firstName": { + "text": "Test" + }, + "secondName": { + "text": "Author" + }, + "type": "person" + } + ] + }, + "localSystemNumber": "System.Supplied@2024-03-20,11:50:40", + "lastUpdated": 20240320 + } + ``` +=== "local_bibs_search" + + ```python title="local_bibs_search Request" + from bookops_worldcat import MetadataSession + + with MetadataSession(authorization=token) as session: + response = session.local_bibs_search(q="ti: Test Book AND au: Test Author") + print(response.json()) + ``` + ```{ .json title="local_bibs_search Response" .no-copy} + { + "numberOfRecords": 1, + "records": [ + { + "controlNumber": 123456789, + "oclcNumber": "987654321", + "title": { + "uniformTitles": [ + "Test Book" + ] + }, + "contributor": { + "creators": [ + { + "firstName": { + "text": "Test" + }, + "secondName": { + "text": "Author" + }, + "type": "person" + } + ] + }, + "localSystemNumber": "System.Supplied@2024-03-20,11:50:40", + "lastUpdated": 20240320 + } + ] + } + ``` + + +## Manage Local Holdings Records +Users can manage local holdings records using Bookops-Worldcat in the same way that they manage local bib records (see above: [Managing Local Bib Records](#managing-local-bib-records) for more information). + +=== "lhr_create" + + ```python title="lhr_create MARCXML Request" + from bookops_worldcat import MetadataSession + from io import BytesIO + + with open("file.xml","rb") as xml_file: + for r in xml_file: + xml_record = BytesIO(r) + session = MetadataSession(authorization=token) + response = session.lhr_create( + record=xml_record, + recordFormat="application/marcxml+xml" + ) + print(response.content) + ``` + ```{ .xml title="lhr_create MARCXML Response" .no-copy} + + 00000nx a2200000zi 4500 + 00001 + 20240320085741.4 + zu + 2403200p 0 4001uueng0210908 + + NYP + TEST + TEST-STACKS + + + 00001 + + + ``` +=== "lhr_get" + + ```python title="lhr_get MARCXML Request" + from bookops_worldcat import MetadataSession + + with MetadataSession(authorization=token) as session: + response = session.lhr_get("12345") + print(response.content) + + ``` + ```{ .xml title="lhr_get MARCXML Response" .no-copy} + + 00000nx a2200000zi 4500 + 12345 + 00001 + 20240320085741.4 + zu + 2403200p 0 4001uueng0210908 + + NYP + TEST + TEST-STACKS + + + 00001 + + + ``` + +=== "lhr_replace" + + ```python title="lhr_replace MARCXML Request" + from bookops_worldcat import MetadataSession + from io import BytesIO + + with open("file.xml","rb") as xml_file: + for r in xml_file: + xml_record = io.BytesIO(r) + session = MetadataSession(authorization=token) + response = session.lhr_replace( + controlNumber="12345", + record=xml_record, + recordFormat="application/marcxml+xml" + ) + print(response.content) + + ``` + ```{ .xml title="lhr_replace MARCXML Response" .no-copy} + + 00000nx a2200000zi 4500 + 12345 + 00001 + 20240320085741.4 + zu + 2403200p 0 4001uueng0210908 + + NYP + TEST + TEST-STACKS + + + 00001 + + + ``` + +=== "lhr_delete" + + ```python title="lhr_delete MARCXML Request" + from bookops_worldcat import MetadataSession + + with MetadataSession(authorization=token) as session: + response = session.lhr_delete("12345") + print(response.content) + + ``` + ```{ .xml title="lhr_delete MARCXML Response" .no-copy} + + 00000nx a2200000zi 4500 + 12345 + 00001 + 20240320085741.4 + zu + 2403200p 0 4001uueng0210908 + + NYP + TEST + TEST-STACKS + + + 00001 + + + ``` +### Managing Shared Print Commitments +Users can manage Shared Print collections using the Metadata API by adding Shared Print flags to their Local Holdings Records. More information on managing Shared Print commitments is available on [OCLC's Developer Network Site](https://www.oclc.org/developer/api/oclc-apis/worldcat-metadata-api/wcmetadata-faqs.en.html). + +## Search Local Holdings Resources +Users can browse, search for and retrieve brief local holdings data in JSON format using the `local_holdings_get`, `local_holdings_search`, `local_holdings_browse`, and `local_holdings_search_shared_print` methods: + +=== "local_holdings_get" + + ```python title="local_holdings_get Request" + from bookops_worldcat import MetadataSession + + with MetadataSession(authorization=token) as session: + response = session.local_holdings_get(controlNumber="111111111") + print(response.json()) + ``` + ```{ .json title="local_holdings_get Response" .no-copy} + { + "numberOfHoldings": 1, + "detailedHoldings": [ + { + "lhrControlNumber": "111111111", + "lhrDateEntered": "20240101", + "lhrLastUpdated": "20240201", + "oclcNumber": "123456789", + "format": "zu", + "location": { + "holdingLocation": "NYP", + "sublocationCollection": "TEST", + "shelvingLocation": "TEST-STACKS" + }, + "copyNumber": "1", + "callNumber": { + "displayCallNumber": "TEST", + "classificationPart": "TEST" + }, + "hasSharedPrintCommitment": "N", + "summary": "Local holdings available.", + "holdingParts": [ + { + "pieceDesignation": "TEST12345" + } + ] + } + ] + } + ``` +=== "local_holdings_search" + + ```python title="local_holdings_search Request" + from bookops_worldcat import MetadataSession + + with MetadataSession(authorization=token) as session: + response = session.local_holdings_search( + oclcNumber="123456789", + orderBy="location" + ) + print(response.json()) + ``` + ```{ .json title="local_holdings_search Response" .no-copy} + { + "numberOfHoldings": 1, + "detailedHoldings": [ + { + "lhrControlNumber": "111111111", + "lhrDateEntered": "20240101", + "lhrLastUpdated": "20240201", + "oclcNumber": "123456789", + "format": "zu", + "location": { + "holdingLocation": "NYP", + "sublocationCollection": "TEST", + "shelvingLocation": "TEST-STACKS" + }, + "copyNumber": "1", + "callNumber": { + "displayCallNumber": "TEST", + "classificationPart": "TEST" + }, + "hasSharedPrintCommitment": "N", + "summary": "Local holdings available.", + "holdingParts": [ + { + "pieceDesignation": "TEST12345" + } + ] + } + ] + } + ``` +=== "local_holdings_browse" + + ```python title="local_holdings_browse Request" + from bookops_worldcat import MetadataSession + + with MetadataSession(authorization=token) as session: + response = session.local_holdings_browse( + callNumber="ReCAP-000000", + holdingLocation="TEST-LOCATION", + shelvingLocation="TEST-STACKS", + ) + print(response.json()) + ``` + ```{ .json title="local_holdings_browse Response" .no-copy} + { + "numberOfRecords": 1, + "entries": [ + { + "displayCallNumber": "ReCAP-000000", + "holdingLocation": "TEST-LOCATION", + "shelvingLocation": "TEST-STACKS", + "pieceDesignation": "123456789", + "oclcNumber": 000000000, + "title": "Test", + "creator": "Author, Test", + "date": "2024", + "language": "eng", + "generalFormat": "Book", + "specificFormat": "PrintBook", + "edition": "1st ed.", + "publisher": "Test", + "publicationPlace": "New York :" + }, + ] + } + ``` + +=== "local_holdings_search_shared_print" + + ```python title="local_holdings_search_shared_print Request" + from bookops_worldcat import MetadataSession + + with MetadataSession(authorization=token) as session: + response = session.local_holdings_search_shared_print( + oclcNumber="123456789", + orderBy="location" + ) + print(response.json()) + ``` + ```{ .json title="local_holdings_search_shared_print Response" .no-copy} + { + "numberOfHoldings": 1, + "detailedHoldings": [ + { + "lhrControlNumber": "111111111", + "lhrDateEntered": "20240101", + "lhrLastUpdated": "20240201", + "oclcNumber": "123456789", + "format": "zu", + "location": { + "holdingLocation": "NYP", + "sublocationCollection": "TEST", + "shelvingLocation": "TEST-STACKS" + }, + "copyNumber": "1", + "callNumber": { + "displayCallNumber": "TEST", + "classificationPart": "TEST" + }, + "hasSharedPrintCommitment": "Y", + "summary": "Local holdings available.", + "holdingParts": [ + { + "pieceDesignation": "TEST12345" + } + ] + } + ] + } + ``` \ No newline at end of file diff --git a/docs/manage_bibs.md b/docs/manage_bibs.md new file mode 100644 index 0000000..ed8ce32 --- /dev/null +++ b/docs/manage_bibs.md @@ -0,0 +1,354 @@ +# Manage Bibliographic Records + +!!! note + Users must have "WorldCatMetadataAPI:manage_bibs" in the list of scopes for their WSKeys in order to manage bib records using the Metadata API. To check if your WSKey has access to these endpoints, log into your [WSKey Management portal](https://platform.worldcat.org/wskey/). + +## Get Full MARC Records +Users can retrieve full MARC records from WorldCat by passing the `bib_get` method an OCLC number. The Metadata API correctly matches OCLC numbers of records that have been merged together and returns the current master record. Records can be retrieved in MARCXML or MARC21 formats. The default format is MARCXML. + +```python title="bib_get" +from bookops_worldcat import MetadataSession + +with MetadataSession(authorization=token) as session: + response = session.bib_get("321339") + print(response.status_code) +#>200 + print(response.url) +#>"https://metadata.api.oclc.org/worldcat/manage/bibs/321339" +``` + +To avoid raising a `UnicodeEncodeError` when requesting full bib records it is recommended that one access the response data using the `.content` attribute of the response object: + +=== "MARCXML" + + ```python title="bib_get MARCXML Request" + from bookops_worldcat import MetadataSession + + with MetadataSession(authorization=token) as session: + response = session.bib_get("321339") + print(response.content) + + ``` + ```{ .xml title="bib_get MARCXML Response" .no-copy} + + + 00000cam a22000001a 4500 + ocm00321339 + OCoLC + 20240202180208.2 + 711005s1967 nyu 000 f eng + + 67022898 + + + + Bulgakov, Mikhail, + 1891-1940. + + + Master i Margarita. + English + + + The master and Margarita / + Mikhail Bulgakov ; translated from the Russian by Michael Glenny. + + + + ``` + +=== "MARC21" + + ```python title="bib_get MARC21 Request" + from bookops_worldcat import MetadataSession + + with MetadataSession(authorization=token) as session: + response = session.bib_get("321339", responseFormat="application/marc") + print(response.content) + ``` + ```{ .text title="bib_get MARC21 Response" .no-copy} + 04305cam a22007691a 4500001001200000003000600012005001700018008004100035010001700076040024300093016002500336019009500361029002200456029002100478029001800499029002200517035016600539037001700705041001300722043001200735050001900747050002500766055002600791082001700817100003500834240003300869245009800902250001701000260003901017300002701056336002601083337002801109338002701137500001101164500016401175500008201339500003201421520019801453546004501651505080201696583004802498651002702546650001602573650002402589651004702613651003002660651002302690651002502713655002302738655001602761655005802777655004802835655002102883655001802904655002802922655002302950655003002973655002303003655002903026655002203055655002503077700002103102700004003123758015803163776017003321938004403491\x1eocm00321339\x1eOCoLC\x1e20240202180208.2\x1e711005s1967 nyu 000 f eng \x1faBulgakov, Mikhail,\x1fd1891-1940.\x1e10\x1faMaster i Margarita.\x1flEnglish\x1e14\x1faThe master and Margarita /\x1fcMikhail Bulgakov ; translated from the Russian by Michael Glenny. + ``` + + +## Get Current OCLC Numbers +The `bib_get_current_oclc_number` method allows users to retrieve the current control number of a WorldCat record. Occasionally, records identified as duplicates in WorldCat have been merged and in that case a local control number may not correctly refer to the WorldCat record. + +```python title="bib_get_current_oclc_number Request" +from bookops_worldcat import MetadataSession + +with MetadataSession(authorization=token) as session: + response = session.bib_get_current_oclc_number("992611164") + print(response.json()) +``` +```{ .json title="bib_get_current_oclc_number Response" .no-copy} +{ + "controlNumbers": [ + { + "requested": "992611164", + "current": "321339" + } + ] +} +``` +## Advanced Bib Record Functionality +Several of the `/manage/bibs/` endpoints take a MARC record in the body of the request. When passing MARC records to any of these endpoints, users should ensure that the format passed in the `recordFormat` argument matches the format of the data passed in the request body using the `record` argument. + +### Match Bib Records + +Users can pass a bib record in MARCXML or MARC21 to the `bib_match` method and the web service will identify the best match for the record in WorldCat. The response will be a brief bib resource in JSON. + +=== "MARCXML" + + ```python title="bib_match Request" + from bookops_worldcat import MetadataSession + from io import BytesIO + + with open("file.xml","rb") as xml_file: + for r in xml_file: + xml_record = BytesIO(r) + session = MetadataSession(authorization=token) + response = session.bib_match( + record=xml_record, + recordFormat="application/marcxml+xml" + ) + print(response.json()) + ``` + +=== "MARC21" + + ```python title="bib_match Request" + from bookops_worldcat import MetadataSession + from io import BytesIO + + with open("file.mrc","rb") as mrc_file: + for r in mrc_file: + mrc_record = BytesIO(r) + session = MetadataSession(authorization=token) + response = session.bib_match( + record=mrc_record, + recordFormat="application/marc" + ) + print(response.json()) + ``` + +```{ .json title="bib_match Response" .no-copy} +{ + "numberOfRecords": 1, + "briefRecords": [ + { + "oclcNumber": "321339", + "title": "The master and Margarita", + "creator": "Mikhail Bulgakov", + "date": "©1967", + "machineReadableDate": "1967", + "language": "eng", + "generalFormat": "Book", + "specificFormat": "PrintBook", + "edition": "1st U.S. ed", + "publisher": "Harper & Row", + "publicationPlace": "New York", + "isbns": [], + "issns": [], + "mergedOclcNumbers": [ + "68172169", + "977269772", + "992611164", + "1053636208", + "1086334687", + "1089359174", + "1126595016", + "1154557860" + ], + "catalogingInfo": { + "catalogingAgency": "DLC", + "catalogingLanguage": "eng", + "levelOfCataloging": "1", + "transcribingAgency": "DLC" + } + } + ] +} +``` + + +### Create Bib Records +Users can create new WorldCat records using the `bib_create` method. The web service will check if the record exists in WorldCat and create a new record if it does not. Users should pass passed a valid MARC record in MARCXML or MARC21 format in the body of the request. + +The response returned by the web service will contain the WorldCat record in the format specified in the `responseFormat` parameter with its newly added OCLC Number. + +!!! note + It is recommended that users validate their records before trying to create new records in WorldCat in order to avoid errors. See [`bib_validate`](#validate-bib-records) below. + +=== "MARCXML" + + ```python title="bib_create Request" + from bookops_worldcat import MetadataSession + from io import BytesIO + + with open("file.xml","rb") as xml_file: + for r in xml_file: + xml_record = BytesIO(r) + session = MetadataSession(authorization=token) + response = session.bib_create( + record=xml_record, + recordFormat="application/marcxml+xml" + ) + print(xml_record) + print(response.content) + ``` + ```{ .xml title="bib_create Test Record" .no-copy} + + 00000nam a2200000 a 4500 + 240320s2024 nyua 000 0 eng d + + 12345678 + + + NYP + eng + NYP + + + BookOps + + + Test Record + + + BOOKOPS-WORLDCAT DOCUMENTATION + + + ``` + ```{ .xml title="bib_create Response" .no-copy} + + 00000nam a2200000 a 4500 + ocn123456789 + 240320s2024 nyua 000 0 eng d + + 12345678 + + + NYP + eng + NYP + + + BookOps + + + Test Record + + + BOOKOPS-WORLDCAT DOCUMENTATION + + + ``` + +=== "MARC21" + + ```python title="bib_create Request" + from bookops_worldcat import MetadataSession + from io import BytesIO + + with open("file.mrc","rb") as mrc_file: + for r in mrc_file: + mrc_record = BytesIO(r) + session = MetadataSession(authorization=token) + response = session.bib_create( + record=mrc_record, + recordFormat="application/marc" + ) + print(mrc_record) + print(response.text) + ``` + ```{ .text title="bib_create Test Record" .no-copy} + 00266nam a2200097 a 4500008004100000010001700041040002200058100002700080245001600107500004500123\u001E240320s2024 nyua 000 0 eng d\u001E \u001Fa 63011276 \u001E \u001FaNYP\u001Fbeng\u001FcNYP\u001E0 \u001FaBookOps\u001E10\u001FaTest Record\u001E \u001FaBOOKOPS-WORLDCAT DOCUMENTATION\u001E\u001D + ``` + ```{ .text title="bib_create Response" .no-copy} + 00291nam a2200109 a 4500001001300000008004100013010001700054040002200071100002700093245001600120500004500136\u001Eocn123456789\u001E240320s2024 nyua 000 0 eng d\u001E \u001Fa 63011276 \u001E \u001FaNYP\u001Fbeng\u001FcNYP\u001E0 \u001FaBookOps\u001E10\u001FaTest Record\u001E \u001FaBOOKOPS-WORLDCAT DOCUMENTATION\u001E\u001D + ``` + +### Replace Bib Records +The `bib_replace` method will retrieve a record in WorldCat and replace it with the record it is passed in the request body. If the record does not exist, a new WorldCat record will be created. + +=== "MARCXML" + + ```python title="bib_replace Request" + from bookops_worldcat import MetadataSession + from io import BytesIO + + with open("file.xml","rb") as xml_file: + for r in xml_file: + xml_record = BytesIO(r) + session = MetadataSession(authorization=token) + response = session.bib_replace( + oclcNumber="123456789" + record=xml_record, + recordFormat="application/marcxml+xml", + ) + ``` + +=== "MARC21" + + ```python title="bib_replace Request" + from bookops_worldcat import MetadataSession + from io import BytesIO + + with open("file.mrc","rb") as mrc_file: + for r in mrc_file: + mrc_record = BytesIO(r) + session = MetadataSession(authorization=token) + response = session.bib_replace( + record=mrc_record, + recordFormat="application/marc" + ) + ``` + +### Validate Bib Records +Users can first pass their MARC records to the `bib_validate` method in order to avoid parsing errors when creating or updating WorldCat records. This will check the formatting and quality of the bib record and return either errors identified in the record or a brief JSON response confirming that the record is valid. + +=== "MARCXML" + + ```python title="bib_validate Request" + from bookops_worldcat import MetadataSession + from io import BytesIO + + with open("file.xml","rb") as xml_file: + for r in xml_file: + xml_record = BytesIO(r) + session = MetadataSession(authorization=token) + response = session.bib_validate( + record=xml_record, + recordFormat="application/marcxml+xml", + validationLevel="validateFull", + ) + print(response.json()) + ``` +=== "MARC21" + + ```python title="bib_validate Request" + from bookops_worldcat import MetadataSession + from io import BytesIO + + with open("file.mrc","rb") as mrc_file: + for r in mrc_file: + mrc_record = BytesIO(r) + session = MetadataSession(authorization=token) + response = session.bib_validate( + record=mrc_record, + recordFormat="application/marc", + validationLevel="validateFull", + ) + print(response.json()) + ``` + +```{ .json title="bib_validate Response" .no-copy} +{ + "httpStatus": "OK", + "status": { + "summary": "VALID", + "description": "The provided Bib is valid" + } +} +``` \ No newline at end of file diff --git a/docs/manage_holdings.md b/docs/manage_holdings.md new file mode 100644 index 0000000..a0d48f5 --- /dev/null +++ b/docs/manage_holdings.md @@ -0,0 +1,159 @@ +# Manage Institution Holdings + +Server responses are returned in JSON format for requests made to any `/manage/holdings/` endpoints. These responses can be accessed and parsed with the `.json()` method. + +## Get Institution Holdings Codes + +The `holdings_get_codes` method retrieves an institution's holdings codes. The web service identifies the institution based on the data passed to the `WorldcatAccessToken`. + +```python title="holdings_get_codes Request" +from bookops_worldcat import MetadataSession + +with MetadataSession(authorization=token) as session: + response = session.holdings_get_codes() + print(response.json()) +``` +```{ .json title="holdings_get_codes Response" .no-copy} +{ + "holdingLibraryCodes": [ + { + "code": "Rodgers & Hammerstein", + "name": "NYPH" + }, + { + "code": "Schomburg Center", + "name": "NYP3" + }, + ] +} +``` + +## Get Current Holdings +The `holdings_get_current` method retrieves the holding status of a requested record for the authenticated institution. + +```python title="holdings_get_current Request" +from bookops_worldcat import MetadataSession + +with MetadataSession(authorization=token) as session: + response = session.holdings_get_current(oclcNumbers=123456789) + print(response.json()) +``` +```{ .json title="holdings_get_current Response" .no-copy} +{ + "holdings": [ + { + "requestedControlNumber": "123456789", + "currentControlNumber": "123456789", + "institutionSymbol": "NYP", + "holdingSet": true + } + ] +} +``` +## Set and Unset Holdings +Users can set and/or unset holdings in WorldCat by passing an OCLC Number to the `holdings_set` and/or `holdings_unset` methods. + +!!! info + In version 2.0 of the Metadata API, users are no longer able to set holdings on multiple records with one request. Users should now pass one OCLC Number per request to `holdings_set` and `holdings_unset`. + +Version 2.0 of the Metadata API provides new functionality to set and unset holdings in WorldCat by passing the Metadata API a MARC record in MARCXML or MARC21 format. The record must have an OCLC number in the 035 or 001 field in order to set holdings in WorldCat. + +Bookops-Worldcat supports this functionality with the `holdings_set_with_bib` and `holdings_unset_with_bib` methods which can be passed a MARC record in the body of the request in the same way that one would pass a record to a method that uses any of the `/manage/bibs/` endpoints. + +=== "holdings_set" + + ```python title="holdings_set Request" + from bookops_worldcat import MetadataSession + + with MetadataSession(authorization=token) as session: + response = session.holdings_set(oclcNumber=123456789) + print(response.json()) + ``` + ```{ .json title="holdings_set Response" .no-copy} + { + "controlNumber": "123456789", + "requestedControlNumber": "123456789", + "institutionCode": "58122", + "institutionSymbol": "NYP", + "success": true, + "message": "Holding Updated Successfully", + "action": "Set Holdings" + } + ``` + +=== "holdings_unset" + + ```python title="holdings_unset Request" + from bookops_worldcat import MetadataSession + + with MetadataSession(authorization=token) as session: + response = session.holdings_unset(oclcNumber=123456789) + print(response.json()) + ``` + ```{ .json title="holdings_unset Response" .no-copy} + { + "controlNumber": "123456789", + "requestedControlNumber": "123456789", + "institutionCode": "58122", + "institutionSymbol": "NYP", + "success": true, + "message": "Holding Updated Successfully", + "action": "Unset Holdings" + } + ``` + +=== "holdings_set_with_bib" + + ```python title="holdings_set_with_bib Request" + from bookops_worldcat import MetadataSession + from io import BytesIO + + with open("file.xml","rb") as xml_file: + for r in xml_file: + xml_record = BytesIO(r) + session = MetadataSession(authorization=token) + response = session.holdings_set_with_bib( + record=xml_record, + recordFormat="application/marcxml+xml" + ) + print(response.json()) + ``` + ```{ .json title="holdings_set_with_bib Response" .no-copy} + { + "controlNumber": "123456789", + "requestedControlNumber": "123456789", + "institutionCode": "58122", + "institutionSymbol": "NYP", + "success": true, + "message": "Holding Updated Successfully", + "action": "Set Holdings" + } + ``` + +=== "holdings_unset_with_bib" + + ```python title="holdings_unset_with_bib Request" + from bookops_worldcat import MetadataSession + from io import BytesIO + + with open("file.xml","rb") as xml_file: + for r in xml_file: + xml_record = BytesIO(r) + session = MetadataSession(authorization=token) + response = session.holdings_unset_with_bib( + record=xml_record, + recordFormat="application/marcxml+xml" + ) + print(response.json()) + ``` + ```{ .json title="holdings_unset_with_bib Response" .no-copy} + { + "controlNumber": "123456789", + "requestedControlNumber": "123456789", + "institutionCode": "58122", + "institutionSymbol": "NYP", + "success": true, + "message": "Holding Updated Successfully", + "action": "Unset Holdings" + } + ``` \ No newline at end of file diff --git a/docs/search.md b/docs/search.md new file mode 100644 index 0000000..eef1305 --- /dev/null +++ b/docs/search.md @@ -0,0 +1,430 @@ +# Search WorldCat +Bookops-Worldcat provides functionality that allows users to search WorldCat for brief bibliographic resources, holdings data, and classification recommendations. + +Requests made to any `/search/` endpoints return server responses in JSON format. These responses can be accessed and parsed with the `.json()` method. + +## Brief Bib Resources +### Search Brief Bibs +The `brief_bibs_search` method allows users to query WorldCat using WorldCat's [bibliographic record indexes](https://help.oclc.org/Librarian_Toolbox/Searching_WorldCat_Indexes/Bibliographic_records/Bibliographic_record_indexes). + +The Metadata API many limiters that one can use to restrict query results. A full list of available parameters for the `brief_bibs_search` method is available in the [API Documentation](api/metadata_api.md#bookops_worldcat.metadata_api.MetadataSession.brief_bibs_search). Additional search examples are also available in the [Advanced Search Functionality](#advanced-search-functionality) section of this page. + +Basic usage: +```python title="brief_bibs_search Request" +from bookops_worldcat import MetadataSession + +with MetadataSession(authorization=token) as session: + response = session.brief_bibs_search( + q="ti: My brilliant friend AND au: Ferrante, Elena", + inCatalogLanguage="eng", + itemSubType="book-printbook", + orderBy="mostWidelyHeld", + ) + print(response.json()) +``` +```{ .json title="brief_bibs_search Response" .no-copy} +{ + "numberOfRecords": 79, + "briefRecords": [ + { + "oclcNumber": "778419313", + "title": "My brilliant friend", + "creator": "Elena Ferrante", + "date": "2012", + "machineReadableDate": "2012", + "language": "eng", + "generalFormat": "Book", + "specificFormat": "PrintBook", + "publisher": "Europa Editions", + "publicationPlace": "New York, New York", + "isbns": [ + "9781609450786", + "1609450787" + ], + "mergedOclcNumbers": [ + "811639683", + "818678733", + "824701856", + "829903719", + "830036387", + "1302347443" + ], + "catalogingInfo": { + "catalogingAgency": "BTCTA", + "catalogingLanguage": "eng", + "levelOfCataloging": " ", + "transcribingAgency": "BTCTA" + } + }, + ] +} +``` + +### Get Brief Bibs +Users can retrieve a brief bib resource for a known item by passing the OCLC Number for the resource to the `brief_bibs_get` method. + +Basic usage: +```python title="brief_bibs_get Request" +from bookops_worldcat import MetadataSession + +with MetadataSession(authorization=token) as session: + response = session.brief_bibs_get(778419313) + print(response.json()) +``` +```{ .json title="brief_bibs_get Response" .no-copy} +{ + "oclcNumber": "778419313", + "title": "My brilliant friend", + "creator": "Elena Ferrante", + "date": "2012", + "machineReadableDate": "2012", + "language": "eng", + "generalFormat": "Book", + "specificFormat": "PrintBook", + "publisher": "Europa Editions", + "publicationPlace": "New York, New York", + "isbns": [ + "9781609450786", + "1609450787" + ], + "mergedOclcNumbers": [ + "811639683", + "818678733", + "824701856", + "829903719", + "830036387", + "1302347443" + ], + "catalogingInfo": { + "catalogingAgency": "BTCTA", + "catalogingLanguage": "eng", + "levelOfCataloging": " ", + "transcribingAgency": "BTCTA" + } +} +``` +### Get Brief Bibs for Other Editions +Users can retrieve brief bib resources for other editions of a title by passing an OCLC Number to the `brief_bibs_get_other_editions` method. + +Basic usage: +```python title="brief_bibs_get_other_editions Request" +from bookops_worldcat import MetadataSession + +with MetadataSession(authorization=token) as session: + response = session.brief_bibs_get_other_editions( + oclcNumber="321339", + itemSubType="book-digital", + inCatalogLanguage="eng", + orderBy="bestMatch" + ) + print(response.json()) +``` +```{ .json title="brief_bibs_get_other_editions Response" .no-copy} +{ + "numberOfRecords": 15, + "briefRecords": [ + { + "oclcNumber": "859323121", + "title": "My brilliant friend. book one : childhood, adolescence", + "creator": "Elena Ferrante", + "date": "2012", + "machineReadableDate": "2012", + "language": "eng", + "generalFormat": "Book", + "specificFormat": "Digital", + "publisher": "Europa Editions", + "publicationPlace": "New York", + "isbns": [ + "9781609458638", + "160945863X", + "9781787701151", + "1787701158" + ], + "mergedOclcNumbers": [ + "883320518", + "907236505", + "1030261956", + "1031563997", + "1032076035", + "1052184907", + "1124391373", + "1155208541", + "1191036210", + "1196835133" + ], + "catalogingInfo": { + "catalogingAgency": "TEFOD", + "catalogingLanguage": "eng", + "levelOfCataloging": " ", + "transcribingAgency": "TEFOD" + } + }, + ] +} +``` +## Member Holdings +Users can query WorldCat for holdings data and return holdings summaries using Bookops-Worldcat and the Metadata API. Requests made using the `summary_holdings_search` and `shared_print_holdings_search` methods return brief bib resources with the holdings summaries in their responses, while requests made using the `summary_holdings_get` method only return holdings summaries. + +### Get Holdings Summary +Users can retrieve a summary of holdings data from WorldCat for a known item by passing an OCLC Number to the `summary_holdings_get` method. + +Basic Usage: +```python title="Basic summary_holdings_get Request" +from bookops_worldcat import MetadataSession + +with MetadataSession(authorization=token) as session: + response = session.summary_holdings_get("778419313") + print(response.json()) +``` +```{ .json title="Basic summary_holdings_get Response" .no-copy} +{ + "totalHoldingCount": 1626, + "totalSharedPrintCount": 5, + "totalEditions": 1 +} +``` +Users can limit their search results to specific institutions, library types, or geographic areas. + +Limit holdings search by state: +```python title="summary_holdings_get Request with heldInState limiter" +from bookops_worldcat import MetadataSession + +with MetadataSession(authorization=token) as session: + response = session.summary_holdings_get("778419313", heldInState="US-NY") + print(response.json()) +``` +```{ .json title="summary_holdings_get Response with heldInState limiter" .no-copy} +{ + "totalHoldingCount": 56, + "totalSharedPrintCount": 0 +} +``` +### Search General Holdings +Users can pass either an OCLC Number, ISBN, or ISSN to the `summary_holdings_search` method to search for bibliographic resources their holdings. + +Basic Usage: +```python title="summary_holdings_search Request" +from bookops_worldcat import MetadataSession + +with MetadataSession(authorization=token) as session: + response = session.summary_holdings_search( + isbn="9781609458638", + heldInCountry="US" + ) + print(response.json()) +``` +```{ .json title="summary_holdings_search Response" .no-copy} +{ + "numberOfRecords": 1, + "briefRecords": [ + { + "oclcNumber": "859323121", + "title": "My brilliant friend. book one : childhood, adolescence", + "creator": "Elena Ferrante", + "date": "2012", + "machineReadableDate": "2012", + "language": "eng", + "generalFormat": "Book", + "specificFormat": "Digital", + "publisher": "Europa Editions", + "publicationPlace": "New York", + "isbns": [ + "9781609458638", + "160945863X", + "9781787701151", + "1787701158" + ], + "mergedOclcNumbers": [ + "883320518", + "907236505", + "1030261956", + "1031563997", + "1032076035", + "1052184907", + "1124391373", + "1155208541", + "1191036210", + "1196835133" + ], + "catalogingInfo": { + "catalogingAgency": "TEFOD", + "catalogingLanguage": "eng", + "levelOfCataloging": " ", + "transcribingAgency": "TEFOD" + }, + "institutionHolding": { + "totalHoldingCount": 159 + } + } + ] +} +``` +### Search Shared Print Holdings +To search just for holdings with retention commitments, users can pass an OCLC Number, ISBN, or ISSN to the `shared_print_holdings_search` method. The response includes the brief bib resource, a summary of shared print holdings for that resource, and data about the institutions with retention commitments for the resource. + +```python title="shared_print_holdings_search Request" +from bookops_worldcat import MetadataSession + +with MetadataSession(authorization=token) as session: + response = session.shared_print_holdings_search(321339) + print(response.json()) +``` +```{ .json title="shared_print_holdings_search Response" .no-copy} +{ + "numberOfRecords": 1, + "briefRecords": [ + { + "oclcNumber": "778419313", + "title": "My brilliant friend", + "creator": "Elena Ferrante", + "date": "2012", + "machineReadableDate": "2012", + "language": "eng", + "generalFormat": "Book", + "specificFormat": "PrintBook", + "publisher": "Europa Editions", + "publicationPlace": "New York, New York", + "isbns": [ + "9781609450786", + "1609450787" + ], + "mergedOclcNumbers": [ + "811639683", + "818678733", + "824701856", + "829903719", + "830036387", + "1302347443" + ], + "catalogingInfo": { + "catalogingAgency": "BTCTA", + "catalogingLanguage": "eng", + "levelOfCataloging": " ", + "transcribingAgency": "BTCTA" + }, + "institutionHolding": { + "totalHoldingCount": 5, + "briefHoldings": [ + { + "country": "US", + "state": "US-ME", + "oclcSymbol": "CBY", + "registryId": 1233, + "institutionNumber": 90, + "institutionName": "Colby College", + "alsoCalled": "Miller Library", + "hasOPACLink": True, + "self": "https://worldcat.org/oclc-config/institution/data/1233", + "address": { + "street1": "Miller Library", + "street2": "5124 Mayflower Hill", + "city": "Waterville", + "state": "US-ME", + "postalCode": "04901-8851", + "country": "US", + "lat": 44.564102, + "lon": -69.66333 + }, + "institutionType": "ACADEMIC" + } + ] + } + } + ] +} +``` +## Classification Recommendations +Version 2.0 of the Metadata API added a new endpoint that users can query to retrieve classification recommendations for known items. With Bookops-Worldcat, users can pass an OCLC Number to the `bib_get_classification` method and the response will contain the most popular classification for that item in both Dewey and LC. + +```python title="bib_get_classification Request" +from bookops_worldcat import MetadataSession + +with MetadataSession(authorization=token) as session: + response = session.bib_get_classification("778419313") + print(response.json()) +``` +```{ .json title="bib_get_classification Response" .no-copy} +{ + "dewey": { + "mostPopular": [ + "853/.92" + ] + }, + "lc": { + "mostPopular": [ + "PQ4866.E6345 A8113 2012" + ] + } +} +``` +## Advanced Search Functionality + +!!! info + While most arguments passed to `/search/` endpoints (such as `brief_bibs_search`, `local_bibs_search`, and `summary_holdings_search`) are joined using the 'AND' operator, when both `itemType` and `itemSubType` are used in a query, they are joined using the 'OR' operator. + +### Keyword and Fielded Queries +The Metadata API provides robust search functionality for bib resources. In addition to a flexible query string that supports keyword and fielded searches, it is possible to set further limits using various elements such as item type, language, and publishing date. Users can specify the order of returned records by using the `orderBy` argument. + +The query syntax is case-insensitive and allows keyword and phrase search (use quotation marks around phrases), boolean operators (AND, OR, NOT), wildcards (# - single character, ? - any number of additional characters), and truncation (use \* character). + +#### Advanced Search for Brief Bib Resources + +More about the query syntax available for brief bib resource searches can be found in [OCLC's documentation](https://help.oclc.org/Librarian_Toolbox/Searching_WorldCat_Indexes/Bibliographic_records/Bibliographic_record_indexes/Bibliographic_record_index_lists). + +Two equivalent `brief_bibs_search` examples with item type and language limiters: + +=== "Keyword Search" + + ```python + response = session.brief_bibs_search( + q="ti=my brilliant friend", + itemType="video", + inLanguage="eng", + orderBy="bestMatch", + ) + print(response.json()) + ``` +=== "Fielded Search" + + ```python + response = session.brief_bibs_search( + q='ti="my brilliant friend" AND x0: video AND ln: eng', + orderBy="bestMatch" + ) + print(response.json()) + ``` + +```{ .json .no-copy} +{ + "numberOfRecords": 37, + "briefRecords": [ + { + "oclcNumber": "1091307669", + "title": "My Brilliant Friend", + "date": "2019", + "machineReadableDate": "2019", + "language": "eng", + "generalFormat": "Video", + "specificFormat": "DVD", + "edition": "Widescreen ed", + "publisher": "Home Box Office", + "publicationPlace": "[United States]", + "catalogingInfo": { + "catalogingAgency": "CNWPU", + "catalogingLanguage": "eng", + "levelOfCataloging": "M", + "transcribingAgency": "CNWPU" + } + }, + ] +} +``` + +#### Advanced Search for Local Bib Resources + +The `local_bibs_search` method also allows for fielded queries. The available indexes are slightly different from those available for brief bib resource searches. For more information about the query syntax for local bib resources see [OCLC's documentation](https://help.oclc.org/Librarian_Toolbox/Searching_WorldCat_Indexes/Local_bibliographic_data_records/Local_bibliographic_data_record_indexes_A-Z). + +`local_bibs_search` with language and date created as MARC limiters: +```python title="local_bibs_search Fielded Query" +session.local_bibs_search(q="ti=My Local Bib Record AND dc=2024? AND ln=eng") +``` diff --git a/docs/start.md b/docs/start.md new file mode 100644 index 0000000..5c996f3 --- /dev/null +++ b/docs/start.md @@ -0,0 +1,36 @@ +# Get Started +## Authentication and Authorization +An Access Token can be obtained by passing credential parameters into the `WorldcatAccessToken` object. This will authenticate the user against OCLC's Authorization Server and allow the user to send requests to the OCLC Metadata API. + +```python title="Get Access Token" +from bookops_worldcat import WorldcatAccessToken +token = WorldcatAccessToken( + key="my_WSKey", + secret="my_secret", + scopes="WorldCatMetadataAPI", +) +print(token) +#>"access_token: 'tk_Yebz4BpEp9dAsghA7KpWx6dYD1OZKWBlHjqW', expires_at: '2024-01-01 17:19:58Z'" +print(token.is_expired()) +#>False +``` + +This `token` object can be passed directly into `MetadataSession` to authorize requests to the Metadata API web service: + +```python title="Open MetadataSession" +from bookops_worldcat import MetadataSession + +session = MetadataSession(authorization=token) +session.brief_bibs_get("321339") +``` + +## MetadataSession as Context Manager +A `MetadataSession` can also be used as a context manager. This allows users to use the same parameters and configuration for each request they send to the Metadata API and to ensure that the session is closed after their code has finished running. + +`MetadataSession` inherits all `requests.Session` methods and properties (see [Advanced Usage > MetadataSession](advanced.md#metadatasession) for more information). + +```python title="Metadata Session as Context Manager" +with MetadataSession(authorization=token) as session: + response = session.brief_bibs_get("321339") +``` +A `MetadataSession` has methods that allow users to interact with each endpoint of the OCLC Metadata API. See the tabs on the left of this page for more information about `MetadataSession` methods and examples of their usage. \ No newline at end of file diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css new file mode 100644 index 0000000..1005dee --- /dev/null +++ b/docs/stylesheets/extra.css @@ -0,0 +1,6 @@ +[data-md-color-scheme="NYPL"] { + --md-primary-fg-color: #d52828; + --md-typeset-a-color: #d52828; + --md-accent-fg-color: #d52828; + +} \ No newline at end of file diff --git a/docs/tutorials.md b/docs/tutorials.md index 1e2038a..c43ad41 100644 --- a/docs/tutorials.md +++ b/docs/tutorials.md @@ -1,37 +1,2 @@ -# Tutorials - -## Save a full bib XML response to a file in MARC21 format - -This recipe shows how to query Worldcat for specific full bibliographic records and save the results to a file in MARC21 format. - -A conversion from the response to the MARC format is handled by the pymarc library (see more at [https://pypi.org/project/pymarc/](https://pypi.org/project/pymarc/)), specifically its `parse_xml_to_array` and `as_marc` methods. - -The code below requires an access token ([`WorldcatAccessToken` object](https://bookops-cat.github.io/bookops-worldcat/0.4/#worldcataccesstoken)) to be passed to the MetadataSession for authorization. - - -```python -from io import BytesIO - -from bookops_worldcat import MetadataSession -from pymarc import parse_xml_to_array - -oclc_numbers = [850939580, 850939581, 850939582] - -# obtain first an access token using the WorldcatAccessToken and -# your OCLC Metadata API credentials - -with MetadataSession(authorization=token) as session: - - for o in oclc_numbers: - response = session.get_full_bib(oclcNumber=o) - data = BytesIO(response.content) - - # convert into pymarc Record object - bib = parse_xml_to_array(data)[0] - - # manipulate bib to your liking before saving to a file - - # append to a MARC21 file: - with open("retrieved_bibs.mrc", "ab") as out: - out.write(bib.as_marc()) -``` \ No newline at end of file +!!! warning "Work in Progress" + TBD \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index e9538af..f33a402 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,20 +1,83 @@ site_name: BookOps-Worldcat Documentation site_description: Documentation of BookOps-Worldcat library +site_url: https://bookops-cat.github.io/bookops-worldcat/ +repo_name: bookops-worldcat +repo_url: https://github.com/BookOps-CAT/bookops-worldcat + +theme: + name: 'material' + palette: + scheme: NYPL + features: + - content.tabs.link + - content.code.copy + - navigation.tabs + - navigation.tabs.sticky + - navigation.footer + - navigation.sections + - navigation.expand + nav: - - Home: index.md - - Changelog: changelog.md - - Source Code: https://github.com/BookOps-CAT/bookops-worldcat - - Issue Tracker: https://github.com/BookOps-CAT/bookops-worldcat/issues - - Tutorials: tutorials.md - - About: about.md - - API: mkapi/api/bookops_worldcat -theme: readthedocs +- BookOps-WorldCat: + - Overview: index.md + - How To: + - Get Started: start.md + - Search WorldCat: search.md + - Manage Bibliographic Records: manage_bibs.md + - Manage Institution Holdings: manage_holdings.md + - Search and Manage Local Data: local.md + - Advanced Usage: advanced.md + - About: + - About BookOps: about.md + - Contributing: contributing.md + - Changelog: changelog.md +- API Documentation: + - bookops_worldcat: + - bookops_worldcat.authorize: api/authorize.md + - bookops_worldcat.errors: api/errors.md + - bookops_worldcat.metadata_api: api/metadata_api.md + - bookops_worldcat.query: api/query.md + - bookops_worldcat.utils: api/utils.md +- Tutorials: tutorials.md + + +markdown_extensions: +- admonition +- pymdownx.details +- pymdownx.superfences +- pymdownx.highlight: + pygments_lang_class: true +- pymdownx.extra +- pymdownx.tabbed: + alternate_style: true +- tables + +watch: +- bookops_worldcat + plugins: - search - - mkapi + - mkdocstrings: + handlers: + python: + options: + show_source: true + separate_signature: true + filters: ["!^_"] + docstring_options: + ignore_init_summary: true + merge_init_into_class: true + show_signature_annotations: true + import: + - https://docs.python.org/3/objects.inv - mike: version_selector: true # set to false to leave out the version selector css_dir: css # the directory to put the version selector's CSS javascript_dir: js # the directory to put the version selector's JS canonical_version: null # the version for ; `null` # uses the version specified via `mike deploy` +extra: + version: + provider: mike +extra_css: + - stylesheets/extra.css \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index c9830af..9332bda 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,196 +1,308 @@ # This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] -name = "atomicwrites" -version = "1.4.0" -description = "Atomic file writes." +name = "astunparse" +version = "1.6.3" +description = "An AST unparser for Python" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = "*" files = [ - {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, - {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, + {file = "astunparse-1.6.3-py2.py3-none-any.whl", hash = "sha256:c2652417f2c8b5bb325c885ae329bdf3f86424075c4fd1a128674bc6fba4b8e8"}, + {file = "astunparse-1.6.3.tar.gz", hash = "sha256:5ad93a8456f0d084c3456d059fd9a92cce667963232cbf763eac3bc5b7940872"}, ] +[package.dependencies] +six = ">=1.6.1,<2.0" +wheel = ">=0.23.0,<1.0" + [[package]] -name = "attrs" -version = "21.4.0" -description = "Classes Without Boilerplate" +name = "babel" +version = "2.14.0" +description = "Internationalization utilities" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.7" files = [ - {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, - {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, + {file = "Babel-2.14.0-py3-none-any.whl", hash = "sha256:efb1a25b7118e67ce3a259bed20545c29cb68be8ad2c784c83689981b7a57287"}, + {file = "Babel-2.14.0.tar.gz", hash = "sha256:6919867db036398ba21eb5c7a0f6b28ab8cbc3ae7a73a44ebe34ae74a4e7d363"}, ] +[package.dependencies] +pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""} + [package.extras] -dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six", "sphinx", "sphinx-notfound-page", "zope.interface"] -docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] -tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six", "zope.interface"] -tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six"] +dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] [[package]] name = "black" -version = "22.1.0" +version = "23.12.1" description = "The uncompromising code formatter." optional = false -python-versions = ">=3.6.2" -files = [ - {file = "black-22.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1297c63b9e1b96a3d0da2d85d11cd9bf8664251fd69ddac068b98dc4f34f73b6"}, - {file = "black-22.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2ff96450d3ad9ea499fc4c60e425a1439c2120cbbc1ab959ff20f7c76ec7e866"}, - {file = "black-22.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e21e1f1efa65a50e3960edd068b6ae6d64ad6235bd8bfea116a03b21836af71"}, - {file = "black-22.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2f69158a7d120fd641d1fa9a921d898e20d52e44a74a6fbbcc570a62a6bc8ab"}, - {file = "black-22.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:228b5ae2c8e3d6227e4bde5920d2fc66cc3400fde7bcc74f480cb07ef0b570d5"}, - {file = "black-22.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b1a5ed73ab4c482208d20434f700d514f66ffe2840f63a6252ecc43a9bc77e8a"}, - {file = "black-22.1.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35944b7100af4a985abfcaa860b06af15590deb1f392f06c8683b4381e8eeaf0"}, - {file = "black-22.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:7835fee5238fc0a0baf6c9268fb816b5f5cd9b8793423a75e8cd663c48d073ba"}, - {file = "black-22.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dae63f2dbf82882fa3b2a3c49c32bffe144970a573cd68d247af6560fc493ae1"}, - {file = "black-22.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fa1db02410b1924b6749c245ab38d30621564e658297484952f3d8a39fce7e8"}, - {file = "black-22.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c8226f50b8c34a14608b848dc23a46e5d08397d009446353dad45e04af0c8e28"}, - {file = "black-22.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2d6f331c02f0f40aa51a22e479c8209d37fcd520c77721c034517d44eecf5912"}, - {file = "black-22.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:742ce9af3086e5bd07e58c8feb09dbb2b047b7f566eb5f5bc63fd455814979f3"}, - {file = "black-22.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fdb8754b453fb15fad3f72cd9cad3e16776f0964d67cf30ebcbf10327a3777a3"}, - {file = "black-22.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5660feab44c2e3cb24b2419b998846cbb01c23c7fe645fee45087efa3da2d61"}, - {file = "black-22.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:6f2f01381f91c1efb1451998bd65a129b3ed6f64f79663a55fe0e9b74a5f81fd"}, - {file = "black-22.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:efbadd9b52c060a8fc3b9658744091cb33c31f830b3f074422ed27bad2b18e8f"}, - {file = "black-22.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8871fcb4b447206904932b54b567923e5be802b9b19b744fdff092bd2f3118d0"}, - {file = "black-22.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccad888050f5393f0d6029deea2a33e5ae371fd182a697313bdbd835d3edaf9c"}, - {file = "black-22.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07e5c049442d7ca1a2fc273c79d1aecbbf1bc858f62e8184abe1ad175c4f7cc2"}, - {file = "black-22.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:373922fc66676133ddc3e754e4509196a8c392fec3f5ca4486673e685a421321"}, - {file = "black-22.1.0-py3-none-any.whl", hash = "sha256:3524739d76b6b3ed1132422bf9d82123cd1705086723bc3e235ca39fd21c667d"}, - {file = "black-22.1.0.tar.gz", hash = "sha256:a7c0192d35635f6fc1174be575cb7915e92e5dd629ee79fdaf0dcfa41a80afb5"}, +python-versions = ">=3.8" +files = [ + {file = "black-23.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0aaf6041986767a5e0ce663c7a2f0e9eaf21e6ff87a5f95cbf3675bfd4c41d2"}, + {file = "black-23.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c88b3711d12905b74206227109272673edce0cb29f27e1385f33b0163c414bba"}, + {file = "black-23.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920b569dc6b3472513ba6ddea21f440d4b4c699494d2e972a1753cdc25df7b0"}, + {file = "black-23.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:3fa4be75ef2a6b96ea8d92b1587dd8cb3a35c7e3d51f0738ced0781c3aa3a5a3"}, + {file = "black-23.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8d4df77958a622f9b5a4c96edb4b8c0034f8434032ab11077ec6c56ae9f384ba"}, + {file = "black-23.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:602cfb1196dc692424c70b6507593a2b29aac0547c1be9a1d1365f0d964c353b"}, + {file = "black-23.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c4352800f14be5b4864016882cdba10755bd50805c95f728011bcb47a4afd59"}, + {file = "black-23.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:0808494f2b2df923ffc5723ed3c7b096bd76341f6213989759287611e9837d50"}, + {file = "black-23.12.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:25e57fd232a6d6ff3f4478a6fd0580838e47c93c83eaf1ccc92d4faf27112c4e"}, + {file = "black-23.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d9e13db441c509a3763a7a3d9a49ccc1b4e974a47be4e08ade2a228876500ec"}, + {file = "black-23.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1bd9c210f8b109b1762ec9fd36592fdd528485aadb3f5849b2740ef17e674e"}, + {file = "black-23.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:ae76c22bde5cbb6bfd211ec343ded2163bba7883c7bc77f6b756a1049436fbb9"}, + {file = "black-23.12.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1fa88a0f74e50e4487477bc0bb900c6781dbddfdfa32691e780bf854c3b4a47f"}, + {file = "black-23.12.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a4d6a9668e45ad99d2f8ec70d5c8c04ef4f32f648ef39048d010b0689832ec6d"}, + {file = "black-23.12.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b18fb2ae6c4bb63eebe5be6bd869ba2f14fd0259bda7d18a46b764d8fb86298a"}, + {file = "black-23.12.1-cp38-cp38-win_amd64.whl", hash = "sha256:c04b6d9d20e9c13f43eee8ea87d44156b8505ca8a3c878773f68b4e4812a421e"}, + {file = "black-23.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3e1b38b3135fd4c025c28c55ddfc236b05af657828a8a6abe5deec419a0b7055"}, + {file = "black-23.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4f0031eaa7b921db76decd73636ef3a12c942ed367d8c3841a0739412b260a54"}, + {file = "black-23.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97e56155c6b737854e60a9ab1c598ff2533d57e7506d97af5481141671abf3ea"}, + {file = "black-23.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:dd15245c8b68fe2b6bd0f32c1556509d11bb33aec9b5d0866dd8e2ed3dba09c2"}, + {file = "black-23.12.1-py3-none-any.whl", hash = "sha256:78baad24af0f033958cad29731e27363183e140962595def56423e626f4bee3e"}, + {file = "black-23.12.1.tar.gz", hash = "sha256:4ce3ef14ebe8d9509188014d96af1c456a910d5b5cbf434a09fef7e024b3d0d5"}, ] [package.dependencies] click = ">=8.0.0" mypy-extensions = ">=0.4.3" +packaging = ">=22.0" pathspec = ">=0.9.0" platformdirs = ">=2" -tomli = ">=1.1.0" -typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} -typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] +d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "certifi" -version = "2021.10.8" +version = "2024.2.2" description = "Python package for providing Mozilla's CA Bundle." optional = false -python-versions = "*" +python-versions = ">=3.6" files = [ - {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, - {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, ] [[package]] name = "charset-normalizer" -version = "2.0.11" +version = "3.3.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false -python-versions = ">=3.5.0" +python-versions = ">=3.7.0" files = [ - {file = "charset-normalizer-2.0.11.tar.gz", hash = "sha256:98398a9d69ee80548c762ba991a4728bfc3836768ed226b3945908d1a688371c"}, - {file = "charset_normalizer-2.0.11-py3-none-any.whl", hash = "sha256:2842d8f5e82a1f6aa437380934d5e1cd4fcf2003b06fed6940769c164a480a45"}, + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, ] -[package.extras] -unicode-backport = ["unicodedata2"] - [[package]] name = "click" -version = "8.0.3" +version = "8.1.7" description = "Composable command line interface toolkit" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"}, - {file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"}, + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, ] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "colorama" -version = "0.4.4" +version = "0.4.6" description = "Cross-platform colored terminal text." optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ - {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, - {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] [[package]] name = "coverage" -version = "6.3.1" +version = "7.4.1" description = "Code coverage measurement for Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "coverage-6.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeffd96882d8c06d31b65dddcf51db7c612547babc1c4c5db6a011abe9798525"}, - {file = "coverage-6.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:621f6ea7260ea2ffdaec64fe5cb521669984f567b66f62f81445221d4754df4c"}, - {file = "coverage-6.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84f2436d6742c01136dd940ee158bfc7cf5ced3da7e4c949662b8703b5cd8145"}, - {file = "coverage-6.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de73fca6fb403dd72d4da517cfc49fcf791f74eee697d3219f6be29adf5af6ce"}, - {file = "coverage-6.3.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78fbb2be068a13a5d99dce9e1e7d168db880870f7bc73f876152130575bd6167"}, - {file = "coverage-6.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f5a4551dfd09c3bd12fca8144d47fe7745275adf3229b7223c2f9e29a975ebda"}, - {file = "coverage-6.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7bff3a98f63b47464480de1b5bdd80c8fade0ba2832c9381253c9b74c4153c27"}, - {file = "coverage-6.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a06c358f4aed05fa1099c39decc8022261bb07dfadc127c08cfbd1391b09689e"}, - {file = "coverage-6.3.1-cp310-cp310-win32.whl", hash = "sha256:9fff3ff052922cb99f9e52f63f985d4f7a54f6b94287463bc66b7cdf3eb41217"}, - {file = "coverage-6.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:276b13cc085474e482566c477c25ed66a097b44c6e77132f3304ac0b039f83eb"}, - {file = "coverage-6.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:56c4a409381ddd7bbff134e9756077860d4e8a583d310a6f38a2315b9ce301d0"}, - {file = "coverage-6.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9eb494070aa060ceba6e4bbf44c1bc5fa97bfb883a0d9b0c9049415f9e944793"}, - {file = "coverage-6.3.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e15d424b8153756b7c903bde6d4610be0c3daca3986173c18dd5c1a1625e4cd"}, - {file = "coverage-6.3.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61d47a897c1e91f33f177c21de897267b38fbb45f2cd8e22a710bcef1df09ac1"}, - {file = "coverage-6.3.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:25e73d4c81efa8ea3785274a2f7f3bfbbeccb6fcba2a0bdd3be9223371c37554"}, - {file = "coverage-6.3.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:fac0bcc5b7e8169bffa87f0dcc24435446d329cbc2b5486d155c2e0f3b493ae1"}, - {file = "coverage-6.3.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:72128176fea72012063200b7b395ed8a57849282b207321124d7ff14e26988e8"}, - {file = "coverage-6.3.1-cp37-cp37m-win32.whl", hash = "sha256:1bc6d709939ff262fd1432f03f080c5042dc6508b6e0d3d20e61dd045456a1a0"}, - {file = "coverage-6.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:618eeba986cea7f621d8607ee378ecc8c2504b98b3fdc4952b30fe3578304687"}, - {file = "coverage-6.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ed164af5c9078596cfc40b078c3b337911190d3faeac830c3f1274f26b8320"}, - {file = "coverage-6.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:352c68e233409c31048a3725c446a9e48bbff36e39db92774d4f2380d630d8f8"}, - {file = "coverage-6.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:448d7bde7ceb6c69e08474c2ddbc5b4cd13c9e4aa4a717467f716b5fc938a734"}, - {file = "coverage-6.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9fde6b90889522c220dd56a670102ceef24955d994ff7af2cb786b4ba8fe11e4"}, - {file = "coverage-6.3.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e647a0be741edbb529a72644e999acb09f2ad60465f80757da183528941ff975"}, - {file = "coverage-6.3.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a5cdc3adb4f8bb8d8f5e64c2e9e282bc12980ef055ec6da59db562ee9bdfefa"}, - {file = "coverage-6.3.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2dd70a167843b4b4b2630c0c56f1b586fe965b4f8ac5da05b6690344fd065c6b"}, - {file = "coverage-6.3.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9ad0a117b8dc2061ce9461ea4c1b4799e55edceb236522c5b8f958ce9ed8fa9a"}, - {file = "coverage-6.3.1-cp38-cp38-win32.whl", hash = "sha256:e92c7a5f7d62edff50f60a045dc9542bf939758c95b2fcd686175dd10ce0ed10"}, - {file = "coverage-6.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:482fb42eea6164894ff82abbcf33d526362de5d1a7ed25af7ecbdddd28fc124f"}, - {file = "coverage-6.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c5b81fb37db76ebea79aa963b76d96ff854e7662921ce742293463635a87a78d"}, - {file = "coverage-6.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a4f923b9ab265136e57cc14794a15b9dcea07a9c578609cd5dbbfff28a0d15e6"}, - {file = "coverage-6.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56d296cbc8254a7dffdd7bcc2eb70be5a233aae7c01856d2d936f5ac4e8ac1f1"}, - {file = "coverage-6.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1245ab82e8554fa88c4b2ab1e098ae051faac5af829efdcf2ce6b34dccd5567c"}, - {file = "coverage-6.3.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f2b05757c92ad96b33dbf8e8ec8d4ccb9af6ae3c9e9bd141c7cc44d20c6bcba"}, - {file = "coverage-6.3.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9e3dd806f34de38d4c01416344e98eab2437ac450b3ae39c62a0ede2f8b5e4ed"}, - {file = "coverage-6.3.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d651fde74a4d3122e5562705824507e2f5b2d3d57557f1916c4b27635f8fbe3f"}, - {file = "coverage-6.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:704f89b87c4f4737da2860695a18c852b78ec7279b24eedacab10b29067d3a38"}, - {file = "coverage-6.3.1-cp39-cp39-win32.whl", hash = "sha256:2aed4761809640f02e44e16b8b32c1a5dee5e80ea30a0ff0912158bde9c501f2"}, - {file = "coverage-6.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:9976fb0a5709988778ac9bc44f3d50fccd989987876dfd7716dee28beed0a9fa"}, - {file = "coverage-6.3.1-pp36.pp37.pp38-none-any.whl", hash = "sha256:463e52616ea687fd323888e86bf25e864a3cc6335a043fad6bbb037dbf49bbe2"}, - {file = "coverage-6.3.1.tar.gz", hash = "sha256:6c3f6158b02ac403868eea390930ae64e9a9a2a5bbfafefbb920d29258d9f2f8"}, + {file = "coverage-7.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:077d366e724f24fc02dbfe9d946534357fda71af9764ff99d73c3c596001bbd7"}, + {file = "coverage-7.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0193657651f5399d433c92f8ae264aff31fc1d066deee4b831549526433f3f61"}, + {file = "coverage-7.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d17bbc946f52ca67adf72a5ee783cd7cd3477f8f8796f59b4974a9b59cacc9ee"}, + {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3277f5fa7483c927fe3a7b017b39351610265308f5267ac6d4c2b64cc1d8d25"}, + {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dceb61d40cbfcf45f51e59933c784a50846dc03211054bd76b421a713dcdf19"}, + {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6008adeca04a445ea6ef31b2cbaf1d01d02986047606f7da266629afee982630"}, + {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c61f66d93d712f6e03369b6a7769233bfda880b12f417eefdd4f16d1deb2fc4c"}, + {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9bb62fac84d5f2ff523304e59e5c439955fb3b7f44e3d7b2085184db74d733b"}, + {file = "coverage-7.4.1-cp310-cp310-win32.whl", hash = "sha256:f86f368e1c7ce897bf2457b9eb61169a44e2ef797099fb5728482b8d69f3f016"}, + {file = "coverage-7.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:869b5046d41abfea3e381dd143407b0d29b8282a904a19cb908fa24d090cc018"}, + {file = "coverage-7.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8ffb498a83d7e0305968289441914154fb0ef5d8b3157df02a90c6695978295"}, + {file = "coverage-7.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3cacfaefe6089d477264001f90f55b7881ba615953414999c46cc9713ff93c8c"}, + {file = "coverage-7.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d6850e6e36e332d5511a48a251790ddc545e16e8beaf046c03985c69ccb2676"}, + {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18e961aa13b6d47f758cc5879383d27b5b3f3dcd9ce8cdbfdc2571fe86feb4dd"}, + {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfd1e1b9f0898817babf840b77ce9fe655ecbe8b1b327983df485b30df8cc011"}, + {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6b00e21f86598b6330f0019b40fb397e705135040dbedc2ca9a93c7441178e74"}, + {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:536d609c6963c50055bab766d9951b6c394759190d03311f3e9fcf194ca909e1"}, + {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7ac8f8eb153724f84885a1374999b7e45734bf93a87d8df1e7ce2146860edef6"}, + {file = "coverage-7.4.1-cp311-cp311-win32.whl", hash = "sha256:f3771b23bb3675a06f5d885c3630b1d01ea6cac9e84a01aaf5508706dba546c5"}, + {file = "coverage-7.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:9d2f9d4cc2a53b38cabc2d6d80f7f9b7e3da26b2f53d48f05876fef7956b6968"}, + {file = "coverage-7.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f68ef3660677e6624c8cace943e4765545f8191313a07288a53d3da188bd8581"}, + {file = "coverage-7.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23b27b8a698e749b61809fb637eb98ebf0e505710ec46a8aa6f1be7dc0dc43a6"}, + {file = "coverage-7.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e3424c554391dc9ef4a92ad28665756566a28fecf47308f91841f6c49288e66"}, + {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0860a348bf7004c812c8368d1fc7f77fe8e4c095d661a579196a9533778e156"}, + {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe558371c1bdf3b8fa03e097c523fb9645b8730399c14fe7721ee9c9e2a545d3"}, + {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3468cc8720402af37b6c6e7e2a9cdb9f6c16c728638a2ebc768ba1ef6f26c3a1"}, + {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:02f2edb575d62172aa28fe00efe821ae31f25dc3d589055b3fb64d51e52e4ab1"}, + {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ca6e61dc52f601d1d224526360cdeab0d0712ec104a2ce6cc5ccef6ed9a233bc"}, + {file = "coverage-7.4.1-cp312-cp312-win32.whl", hash = "sha256:ca7b26a5e456a843b9b6683eada193fc1f65c761b3a473941efe5a291f604c74"}, + {file = "coverage-7.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:85ccc5fa54c2ed64bd91ed3b4a627b9cce04646a659512a051fa82a92c04a448"}, + {file = "coverage-7.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8bdb0285a0202888d19ec6b6d23d5990410decb932b709f2b0dfe216d031d218"}, + {file = "coverage-7.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:918440dea04521f499721c039863ef95433314b1db00ff826a02580c1f503e45"}, + {file = "coverage-7.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:379d4c7abad5afbe9d88cc31ea8ca262296480a86af945b08214eb1a556a3e4d"}, + {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b094116f0b6155e36a304ff912f89bbb5067157aff5f94060ff20bbabdc8da06"}, + {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2f5968608b1fe2a1d00d01ad1017ee27efd99b3437e08b83ded9b7af3f6f766"}, + {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:10e88e7f41e6197ea0429ae18f21ff521d4f4490aa33048f6c6f94c6045a6a75"}, + {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a4a3907011d39dbc3e37bdc5df0a8c93853c369039b59efa33a7b6669de04c60"}, + {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6d224f0c4c9c98290a6990259073f496fcec1b5cc613eecbd22786d398ded3ad"}, + {file = "coverage-7.4.1-cp38-cp38-win32.whl", hash = "sha256:23f5881362dcb0e1a92b84b3c2809bdc90db892332daab81ad8f642d8ed55042"}, + {file = "coverage-7.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:a07f61fc452c43cd5328b392e52555f7d1952400a1ad09086c4a8addccbd138d"}, + {file = "coverage-7.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e738a492b6221f8dcf281b67129510835461132b03024830ac0e554311a5c54"}, + {file = "coverage-7.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46342fed0fff72efcda77040b14728049200cbba1279e0bf1188f1f2078c1d70"}, + {file = "coverage-7.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9641e21670c68c7e57d2053ddf6c443e4f0a6e18e547e86af3fad0795414a628"}, + {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeb2c2688ed93b027eb0d26aa188ada34acb22dceea256d76390eea135083950"}, + {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d12c923757de24e4e2110cf8832d83a886a4cf215c6e61ed506006872b43a6d1"}, + {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0491275c3b9971cdbd28a4595c2cb5838f08036bca31765bad5e17edf900b2c7"}, + {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8dfc5e195bbef80aabd81596ef52a1277ee7143fe419efc3c4d8ba2754671756"}, + {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1a78b656a4d12b0490ca72651fe4d9f5e07e3c6461063a9b6265ee45eb2bdd35"}, + {file = "coverage-7.4.1-cp39-cp39-win32.whl", hash = "sha256:f90515974b39f4dea2f27c0959688621b46d96d5a626cf9c53dbc653a895c05c"}, + {file = "coverage-7.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:64e723ca82a84053dd7bfcc986bdb34af8d9da83c521c19d6b472bc6880e191a"}, + {file = "coverage-7.4.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:32a8d985462e37cfdab611a6f95b09d7c091d07668fdc26e47a725ee575fe166"}, + {file = "coverage-7.4.1.tar.gz", hash = "sha256:1ed4b95480952b1a26d863e546fa5094564aa0065e1e5f0d4d0041f293251d04"}, ] [package.dependencies] -tomli = {version = "*", optional = true, markers = "extra == \"toml\""} +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} [package.extras] toml = ["tomli"] +[[package]] +name = "exceptiongroup" +version = "1.2.0" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, + {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, +] + +[package.extras] +test = ["pytest (>=6)"] + [[package]] name = "ghp-import" -version = "2.0.2" +version = "2.1.0" description = "Copy your docs directly to the gh-pages branch." optional = false python-versions = "*" files = [ - {file = "ghp-import-2.0.2.tar.gz", hash = "sha256:947b3771f11be850c852c64b561c600fdddf794bab363060854c1ee7ad05e071"}, - {file = "ghp_import-2.0.2-py3-none-any.whl", hash = "sha256:5f8962b30b20652cdffa9c5a9812f7de6bcb56ec475acac579807719bf242c46"}, + {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, + {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, ] [package.dependencies] @@ -199,57 +311,89 @@ python-dateutil = ">=2.8.1" [package.extras] dev = ["flake8", "markdown", "twine", "wheel"] +[[package]] +name = "griffe" +version = "0.42.1" +description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." +optional = false +python-versions = ">=3.8" +files = [ + {file = "griffe-0.42.1-py3-none-any.whl", hash = "sha256:7e805e35617601355edcac0d3511cedc1ed0cb1f7645e2d336ae4b05bbae7b3b"}, + {file = "griffe-0.42.1.tar.gz", hash = "sha256:57046131384043ed078692b85d86b76568a686266cc036b9b56b704466f803ce"}, +] + +[package.dependencies] +astunparse = {version = ">=1.6", markers = "python_version < \"3.9\""} +colorama = ">=0.4" + [[package]] name = "idna" -version = "3.3" +version = "3.6" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" files = [ - {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, - {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, + {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, + {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, ] [[package]] name = "importlib-metadata" -version = "4.10.1" +version = "7.0.1" description = "Read metadata from Python packages" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "importlib_metadata-4.10.1-py3-none-any.whl", hash = "sha256:899e2a40a8c4a1aec681feef45733de8a6c58f3f6a0dbed2eb6574b4387a77b6"}, - {file = "importlib_metadata-4.10.1.tar.gz", hash = "sha256:951f0d8a5b7260e9db5e41d429285b5f451e928479f19d80818878527d36e95e"}, + {file = "importlib_metadata-7.0.1-py3-none-any.whl", hash = "sha256:4805911c3a4ec7c3966410053e9ec6a1fecd629117df5adee56dfc9432a1081e"}, + {file = "importlib_metadata-7.0.1.tar.gz", hash = "sha256:f238736bb06590ae52ac1fab06a3a9ef1d8dce2b7a35b5ab329371d6c8f5d2cc"}, ] [package.dependencies] -typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] -docs = ["jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "sphinx"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy", "pytest-perf (>=0.9.2)"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] + +[[package]] +name = "importlib-resources" +version = "6.1.1" +description = "Read resources from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_resources-6.1.1-py3-none-any.whl", hash = "sha256:e8bf90d8213b486f428c9c39714b920041cb02c184686a3dee24905aaa8105d6"}, + {file = "importlib_resources-6.1.1.tar.gz", hash = "sha256:3893a00122eafde6894c59914446a512f728a0c1a45f9bb9b63721b6bacf0b4a"}, +] + +[package.dependencies] +zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff", "zipp (>=3.17)"] [[package]] name = "iniconfig" -version = "1.1.1" -description = "iniconfig: brain-dead simple config-ini parsing" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" optional = false -python-versions = "*" +python-versions = ">=3.7" files = [ - {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, - {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] [[package]] name = "jinja2" -version = "3.0.3" +version = "3.1.3" description = "A very fast and expressive template engine." optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "Jinja2-3.0.3-py3-none-any.whl", hash = "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8"}, - {file = "Jinja2-3.0.3.tar.gz", hash = "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"}, + {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, + {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, ] [package.dependencies] @@ -260,97 +404,89 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "markdown" -version = "3.3.6" -description = "Python implementation of Markdown." +version = "3.5.2" +description = "Python implementation of John Gruber's Markdown." optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "Markdown-3.3.6-py3-none-any.whl", hash = "sha256:9923332318f843411e9932237530df53162e29dc7a4e2b91e35764583c46c9a3"}, - {file = "Markdown-3.3.6.tar.gz", hash = "sha256:76df8ae32294ec39dcf89340382882dfa12975f87f45c3ed1ecdb1e8cefc7006"}, + {file = "Markdown-3.5.2-py3-none-any.whl", hash = "sha256:d43323865d89fc0cb9b20c75fc8ad313af307cc087e84b657d9eec768eddeadd"}, + {file = "Markdown-3.5.2.tar.gz", hash = "sha256:e1ac7b3dc550ee80e602e71c1d168002f062e49f1b11e26a36264dafd4df2ef8"}, ] [package.dependencies] importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} [package.extras] +docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] testing = ["coverage", "pyyaml"] [[package]] name = "markupsafe" -version = "2.0.1" +version = "2.1.5" description = "Safely add untrusted strings to HTML/XML markup." optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, - {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, ] [[package]] @@ -366,194 +502,321 @@ files = [ [[package]] name = "mike" -version = "1.1.2" +version = "2.0.0" description = "Manage multiple versions of your MkDocs-powered documentation" optional = false python-versions = "*" files = [ - {file = "mike-1.1.2-py3-none-any.whl", hash = "sha256:4c307c28769834d78df10f834f57f810f04ca27d248f80a75f49c6fa2d1527ca"}, - {file = "mike-1.1.2.tar.gz", hash = "sha256:56c3f1794c2d0b5fdccfa9b9487beb013ca813de2e3ad0744724e9d34d40b77b"}, + {file = "mike-2.0.0-py3-none-any.whl", hash = "sha256:87f496a65900f93ba92d72940242b65c86f3f2f82871bc60ebdcffc91fad1d9e"}, + {file = "mike-2.0.0.tar.gz", hash = "sha256:566f1cab1a58cc50b106fb79ea2f1f56e7bfc8b25a051e95e6eaee9fba0922de"}, ] [package.dependencies] -jinja2 = "*" +importlib-metadata = "*" +importlib-resources = "*" +jinja2 = ">=2.7" mkdocs = ">=1.0" +pyparsing = ">=3.0" pyyaml = ">=5.1" verspec = "*" [package.extras] -dev = ["coverage", "flake8 (>=3.0)", "shtab"] -test = ["coverage", "flake8 (>=3.0)", "shtab"] - -[[package]] -name = "mkapi" -version = "1.0.14" -description = "An Auto API Documentation tool." -optional = false -python-versions = ">=3.7" -files = [ - {file = "mkapi-1.0.14-py3-none-any.whl", hash = "sha256:88bff6a183f09a5c80acf3c9edfc32e0ff3e2585589a9b6a962aae0467a79a12"}, - {file = "mkapi-1.0.14.tar.gz", hash = "sha256:b9b75ffeeeb6c29843ca703abf30464acac76c0867ca3ca66e3a825c0f258bb1"}, -] - -[package.dependencies] -jinja2 = "*" -markdown = "*" +dev = ["coverage", "flake8 (>=3.0)", "flake8-quotes", "shtab"] +test = ["coverage", "flake8 (>=3.0)", "flake8-quotes", "shtab"] [[package]] name = "mkdocs" -version = "1.2.3" +version = "1.5.3" description = "Project documentation with Markdown." optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "mkdocs-1.2.3-py3-none-any.whl", hash = "sha256:a1fa8c2d0c1305d7fc2b9d9f607c71778572a8b110fb26642aa00296c9e6d072"}, - {file = "mkdocs-1.2.3.tar.gz", hash = "sha256:89f5a094764381cda656af4298727c9f53dc3e602983087e1fe96ea1df24f4c1"}, + {file = "mkdocs-1.5.3-py3-none-any.whl", hash = "sha256:3b3a78e736b31158d64dbb2f8ba29bd46a379d0c6e324c2246c3bc3d2189cfc1"}, + {file = "mkdocs-1.5.3.tar.gz", hash = "sha256:eb7c99214dcb945313ba30426c2451b735992c73c2e10838f76d09e39ff4d0e2"}, ] [package.dependencies] -click = ">=3.3" +click = ">=7.0" +colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} ghp-import = ">=1.0" -importlib-metadata = ">=3.10" -Jinja2 = ">=2.10.1" -Markdown = ">=3.2.1" +importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""} +jinja2 = ">=2.11.1" +markdown = ">=3.2.1" +markupsafe = ">=2.0.1" mergedeep = ">=1.3.4" packaging = ">=20.5" -PyYAML = ">=3.10" +pathspec = ">=0.11.1" +platformdirs = ">=2.2.0" +pyyaml = ">=5.1" pyyaml-env-tag = ">=0.1" watchdog = ">=2.0" [package.extras] i18n = ["babel (>=2.9.0)"] +min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.3)", "jinja2 (==2.11.1)", "markdown (==3.2.1)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "packaging (==20.5)", "pathspec (==0.11.1)", "platformdirs (==2.2.0)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "typing-extensions (==3.10)", "watchdog (==2.0)"] + +[[package]] +name = "mkdocs-autorefs" +version = "1.0.1" +description = "Automatically link across pages in MkDocs." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_autorefs-1.0.1-py3-none-any.whl", hash = "sha256:aacdfae1ab197780fb7a2dac92ad8a3d8f7ca8049a9cbe56a4218cd52e8da570"}, + {file = "mkdocs_autorefs-1.0.1.tar.gz", hash = "sha256:f684edf847eced40b570b57846b15f0bf57fb93ac2c510450775dcf16accb971"}, +] + +[package.dependencies] +Markdown = ">=3.3" +markupsafe = ">=2.0.1" +mkdocs = ">=1.1" + +[[package]] +name = "mkdocs-material" +version = "9.5.13" +description = "Documentation that simply works" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_material-9.5.13-py3-none-any.whl", hash = "sha256:5cbe17fee4e3b4980c8420a04cc762d8dc052ef1e10532abd4fce88e5ea9ce6a"}, + {file = "mkdocs_material-9.5.13.tar.gz", hash = "sha256:d8e4caae576312a88fd2609b81cf43d233cdbe36860d67a68702b018b425bd87"}, +] + +[package.dependencies] +babel = ">=2.10,<3.0" +colorama = ">=0.4,<1.0" +jinja2 = ">=3.0,<4.0" +markdown = ">=3.2,<4.0" +mkdocs = ">=1.5.3,<1.6.0" +mkdocs-material-extensions = ">=1.3,<2.0" +paginate = ">=0.5,<1.0" +pygments = ">=2.16,<3.0" +pymdown-extensions = ">=10.2,<11.0" +regex = ">=2022.4" +requests = ">=2.26,<3.0" + +[package.extras] +git = ["mkdocs-git-committers-plugin-2 (>=1.1,<2.0)", "mkdocs-git-revision-date-localized-plugin (>=1.2.4,<2.0)"] +imaging = ["cairosvg (>=2.6,<3.0)", "pillow (>=10.2,<11.0)"] +recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2.0)", "mkdocs-rss-plugin (>=1.6,<2.0)"] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +description = "Extension pack for Python Markdown and MkDocs Material." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"}, + {file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"}, +] + +[[package]] +name = "mkdocstrings" +version = "0.24.1" +description = "Automatic documentation from sources, for MkDocs." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocstrings-0.24.1-py3-none-any.whl", hash = "sha256:b4206f9a2ca8a648e222d5a0ca1d36ba7dee53c88732818de183b536f9042b5d"}, + {file = "mkdocstrings-0.24.1.tar.gz", hash = "sha256:cc83f9a1c8724fc1be3c2fa071dd73d91ce902ef6a79710249ec8d0ee1064401"}, +] + +[package.dependencies] +click = ">=7.0" +importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""} +Jinja2 = ">=2.11.1" +Markdown = ">=3.3" +MarkupSafe = ">=1.1" +mkdocs = ">=1.4" +mkdocs-autorefs = ">=0.3.1" +platformdirs = ">=2.2.0" +pymdown-extensions = ">=6.3" +typing-extensions = {version = ">=4.1", markers = "python_version < \"3.10\""} + +[package.extras] +crystal = ["mkdocstrings-crystal (>=0.3.4)"] +python = ["mkdocstrings-python (>=0.5.2)"] +python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] + +[[package]] +name = "mkdocstrings-python" +version = "1.9.0" +description = "A Python handler for mkdocstrings." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocstrings_python-1.9.0-py3-none-any.whl", hash = "sha256:fad27d7314b4ec9c0359a187b477fb94c65ef561fdae941dca1b717c59aae96f"}, + {file = "mkdocstrings_python-1.9.0.tar.gz", hash = "sha256:6e1a442367cf75d30cf69774cbb1ad02aebec58bfff26087439df4955efecfde"}, +] + +[package.dependencies] +griffe = ">=0.37" +markdown = ">=3.3,<3.6" +mkdocstrings = ">=0.20" [[package]] name = "mypy" -version = "0.931" +version = "1.8.0" description = "Optional static typing for Python" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "mypy-0.931-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3c5b42d0815e15518b1f0990cff7a705805961613e701db60387e6fb663fe78a"}, - {file = "mypy-0.931-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c89702cac5b302f0c5d33b172d2b55b5df2bede3344a2fbed99ff96bddb2cf00"}, - {file = "mypy-0.931-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:300717a07ad09525401a508ef5d105e6b56646f7942eb92715a1c8d610149714"}, - {file = "mypy-0.931-cp310-cp310-win_amd64.whl", hash = "sha256:7b3f6f557ba4afc7f2ce6d3215d5db279bcf120b3cfd0add20a5d4f4abdae5bc"}, - {file = "mypy-0.931-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:1bf752559797c897cdd2c65f7b60c2b6969ffe458417b8d947b8340cc9cec08d"}, - {file = "mypy-0.931-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4365c60266b95a3f216a3047f1d8e3f895da6c7402e9e1ddfab96393122cc58d"}, - {file = "mypy-0.931-cp36-cp36m-win_amd64.whl", hash = "sha256:1b65714dc296a7991000b6ee59a35b3f550e0073411ac9d3202f6516621ba66c"}, - {file = "mypy-0.931-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e839191b8da5b4e5d805f940537efcaa13ea5dd98418f06dc585d2891d228cf0"}, - {file = "mypy-0.931-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:50c7346a46dc76a4ed88f3277d4959de8a2bd0a0fa47fa87a4cde36fe247ac05"}, - {file = "mypy-0.931-cp37-cp37m-win_amd64.whl", hash = "sha256:d8f1ff62f7a879c9fe5917b3f9eb93a79b78aad47b533911b853a757223f72e7"}, - {file = "mypy-0.931-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f9fe20d0872b26c4bba1c1be02c5340de1019530302cf2dcc85c7f9fc3252ae0"}, - {file = "mypy-0.931-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1b06268df7eb53a8feea99cbfff77a6e2b205e70bf31743e786678ef87ee8069"}, - {file = "mypy-0.931-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8c11003aaeaf7cc2d0f1bc101c1cc9454ec4cc9cb825aef3cafff8a5fdf4c799"}, - {file = "mypy-0.931-cp38-cp38-win_amd64.whl", hash = "sha256:d9d2b84b2007cea426e327d2483238f040c49405a6bf4074f605f0156c91a47a"}, - {file = "mypy-0.931-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ff3bf387c14c805ab1388185dd22d6b210824e164d4bb324b195ff34e322d166"}, - {file = "mypy-0.931-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5b56154f8c09427bae082b32275a21f500b24d93c88d69a5e82f3978018a0266"}, - {file = "mypy-0.931-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8ca7f8c4b1584d63c9a0f827c37ba7a47226c19a23a753d52e5b5eddb201afcd"}, - {file = "mypy-0.931-cp39-cp39-win_amd64.whl", hash = "sha256:74f7eccbfd436abe9c352ad9fb65872cc0f1f0a868e9d9c44db0893440f0c697"}, - {file = "mypy-0.931-py3-none-any.whl", hash = "sha256:1171f2e0859cfff2d366da2c7092b06130f232c636a3f7301e3feb8b41f6377d"}, - {file = "mypy-0.931.tar.gz", hash = "sha256:0038b21890867793581e4cb0d810829f5fd4441aa75796b53033af3aa30430ce"}, + {file = "mypy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3"}, + {file = "mypy-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4"}, + {file = "mypy-1.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d"}, + {file = "mypy-1.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9"}, + {file = "mypy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410"}, + {file = "mypy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae"}, + {file = "mypy-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3"}, + {file = "mypy-1.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817"}, + {file = "mypy-1.8.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d"}, + {file = "mypy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835"}, + {file = "mypy-1.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd"}, + {file = "mypy-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55"}, + {file = "mypy-1.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218"}, + {file = "mypy-1.8.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3"}, + {file = "mypy-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e"}, + {file = "mypy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6"}, + {file = "mypy-1.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66"}, + {file = "mypy-1.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6"}, + {file = "mypy-1.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d"}, + {file = "mypy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02"}, + {file = "mypy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8"}, + {file = "mypy-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259"}, + {file = "mypy-1.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b"}, + {file = "mypy-1.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592"}, + {file = "mypy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a"}, + {file = "mypy-1.8.0-py3-none-any.whl", hash = "sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d"}, + {file = "mypy-1.8.0.tar.gz", hash = "sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07"}, ] [package.dependencies] -mypy-extensions = ">=0.4.3" -tomli = ">=1.1.0" -typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""} -typing-extensions = ">=3.10" +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.1.0" [package.extras] dmypy = ["psutil (>=4.0)"] -python2 = ["typed-ast (>=1.4.0,<2)"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] [[package]] name = "mypy-extensions" -version = "0.4.3" -description = "Experimental type system extensions for programs checked with the mypy typechecker." +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." optional = false -python-versions = "*" +python-versions = ">=3.5" files = [ - {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, - {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] [[package]] name = "packaging" -version = "21.3" +version = "23.2" description = "Core utilities for Python packages" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, - {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] -[package.dependencies] -pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" +[[package]] +name = "paginate" +version = "0.5.6" +description = "Divides large result sets into pages for easier browsing" +optional = false +python-versions = "*" +files = [ + {file = "paginate-0.5.6.tar.gz", hash = "sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d"}, +] [[package]] name = "pathspec" -version = "0.9.0" +version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +python-versions = ">=3.8" files = [ - {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, - {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, ] [[package]] name = "platformdirs" -version = "2.5.0" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +version = "4.2.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "platformdirs-2.5.0-py3-none-any.whl", hash = "sha256:30671902352e97b1eafd74ade8e4a694782bd3471685e78c32d0fdfd3aa7e7bb"}, - {file = "platformdirs-2.5.0.tar.gz", hash = "sha256:8ec11dfba28ecc0715eb5fb0147a87b1bf325f349f3da9aab2cd6b50b96b692b"}, + {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, + {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, ] [package.extras] -docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] -test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] [[package]] name = "pluggy" -version = "1.0.0" +version = "1.4.0" description = "plugin and hook calling mechanisms for python" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, - {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, + {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, + {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, ] -[package.dependencies] -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} - [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] [[package]] -name = "py" -version = "1.11.0" -description = "library with cross-python path, ini-parsing, io, code, log facilities" +name = "pygments" +version = "2.17.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, + {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, +] + +[package.extras] +plugins = ["importlib-metadata"] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pymdown-extensions" +version = "10.7.1" +description = "Extension pack for Python Markdown." optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.8" files = [ - {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, - {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, + {file = "pymdown_extensions-10.7.1-py3-none-any.whl", hash = "sha256:f5cc7000d7ff0d1ce9395d216017fa4df3dde800afb1fb72d1c7d3fd35e710f4"}, + {file = "pymdown_extensions-10.7.1.tar.gz", hash = "sha256:c70e146bdd83c744ffc766b4671999796aba18842b268510a329f7f64700d584"}, ] +[package.dependencies] +markdown = ">=3.5" +pyyaml = "*" + +[package.extras] +extra = ["pygments (>=2.12)"] + [[package]] name = "pyparsing" -version = "3.0.7" -description = "Python parsing module" +version = "3.1.1" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" optional = false -python-versions = ">=3.6" +python-versions = ">=3.6.8" files = [ - {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"}, - {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, + {file = "pyparsing-3.1.1-py3-none-any.whl", hash = "sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb"}, + {file = "pyparsing-3.1.1.tar.gz", hash = "sha256:ede28a1a32462f5a9705e07aea48001a08f7cf81a021585011deba701581a0db"}, ] [package.extras] @@ -561,28 +824,25 @@ diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pytest" -version = "7.0.0" +version = "7.4.4" description = "pytest: simple powerful testing with Python" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "pytest-7.0.0-py3-none-any.whl", hash = "sha256:42901e6bd4bd4a0e533358a86e848427a49005a3256f657c5c8f8dd35ef137a9"}, - {file = "pytest-7.0.0.tar.gz", hash = "sha256:dad48ffda394e5ad9aa3b7d7ddf339ed502e5e365b1350e0af65f4a602344b11"}, + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, ] [package.dependencies] -atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} -attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" -py = ">=1.8.2" -tomli = ">=1.0.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-cov" @@ -604,13 +864,13 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale [[package]] name = "pytest-mock" -version = "3.7.0" +version = "3.12.0" description = "Thin-wrapper around the mock package for easier use with pytest" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pytest-mock-3.7.0.tar.gz", hash = "sha256:5112bd92cc9f186ee96e1a92efc84969ea494939c3aead39c50f421c4cc69534"}, - {file = "pytest_mock-3.7.0-py3-none-any.whl", hash = "sha256:6cff27cec936bf81dc5ee87f07132b807bcda51106b5ec4b90a04331cba76231"}, + {file = "pytest-mock-3.12.0.tar.gz", hash = "sha256:31a40f038c22cad32287bb43932054451ff5583ff094bca6f675df2f8bc1a6e9"}, + {file = "pytest_mock-3.12.0-py3-none-any.whl", hash = "sha256:0972719a7263072da3a21c7f4773069bcc7486027d7e8e1f81d98a47e701bc4f"}, ] [package.dependencies] @@ -633,53 +893,74 @@ files = [ [package.dependencies] six = ">=1.5" +[[package]] +name = "pytz" +version = "2024.1" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, + {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, +] + [[package]] name = "pyyaml" -version = "6.0" +version = "6.0.1" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.6" files = [ - {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, - {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, - {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, - {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, - {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, - {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, - {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, - {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, - {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, - {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, - {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, - {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, - {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, - {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, - {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, - {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, - {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, - {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, - {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, - {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, ] [[package]] @@ -696,26 +977,128 @@ files = [ [package.dependencies] pyyaml = "*" +[[package]] +name = "regex" +version = "2023.12.25" +description = "Alternative regular expression module, to replace re." +optional = false +python-versions = ">=3.7" +files = [ + {file = "regex-2023.12.25-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0694219a1d54336fd0445ea382d49d36882415c0134ee1e8332afd1529f0baa5"}, + {file = "regex-2023.12.25-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b014333bd0217ad3d54c143de9d4b9a3ca1c5a29a6d0d554952ea071cff0f1f8"}, + {file = "regex-2023.12.25-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d865984b3f71f6d0af64d0d88f5733521698f6c16f445bb09ce746c92c97c586"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e0eabac536b4cc7f57a5f3d095bfa557860ab912f25965e08fe1545e2ed8b4c"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c25a8ad70e716f96e13a637802813f65d8a6760ef48672aa3502f4c24ea8b400"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9b6d73353f777630626f403b0652055ebfe8ff142a44ec2cf18ae470395766e"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9cc99d6946d750eb75827cb53c4371b8b0fe89c733a94b1573c9dd16ea6c9e4"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88d1f7bef20c721359d8675f7d9f8e414ec5003d8f642fdfd8087777ff7f94b5"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cb3fe77aec8f1995611f966d0c656fdce398317f850d0e6e7aebdfe61f40e1cd"}, + {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7aa47c2e9ea33a4a2a05f40fcd3ea36d73853a2aae7b4feab6fc85f8bf2c9704"}, + {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:df26481f0c7a3f8739fecb3e81bc9da3fcfae34d6c094563b9d4670b047312e1"}, + {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c40281f7d70baf6e0db0c2f7472b31609f5bc2748fe7275ea65a0b4601d9b392"}, + {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:d94a1db462d5690ebf6ae86d11c5e420042b9898af5dcf278bd97d6bda065423"}, + {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ba1b30765a55acf15dce3f364e4928b80858fa8f979ad41f862358939bdd1f2f"}, + {file = "regex-2023.12.25-cp310-cp310-win32.whl", hash = "sha256:150c39f5b964e4d7dba46a7962a088fbc91f06e606f023ce57bb347a3b2d4630"}, + {file = "regex-2023.12.25-cp310-cp310-win_amd64.whl", hash = "sha256:09da66917262d9481c719599116c7dc0c321ffcec4b1f510c4f8a066f8768105"}, + {file = "regex-2023.12.25-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1b9d811f72210fa9306aeb88385b8f8bcef0dfbf3873410413c00aa94c56c2b6"}, + {file = "regex-2023.12.25-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d902a43085a308cef32c0d3aea962524b725403fd9373dea18110904003bac97"}, + {file = "regex-2023.12.25-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d166eafc19f4718df38887b2bbe1467a4f74a9830e8605089ea7a30dd4da8887"}, + {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7ad32824b7f02bb3c9f80306d405a1d9b7bb89362d68b3c5a9be53836caebdb"}, + {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:636ba0a77de609d6510235b7f0e77ec494d2657108f777e8765efc060094c98c"}, + {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fda75704357805eb953a3ee15a2b240694a9a514548cd49b3c5124b4e2ad01b"}, + {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f72cbae7f6b01591f90814250e636065850c5926751af02bb48da94dfced7baa"}, + {file = "regex-2023.12.25-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db2a0b1857f18b11e3b0e54ddfefc96af46b0896fb678c85f63fb8c37518b3e7"}, + {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7502534e55c7c36c0978c91ba6f61703faf7ce733715ca48f499d3dbbd7657e0"}, + {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e8c7e08bb566de4faaf11984af13f6bcf6a08f327b13631d41d62592681d24fe"}, + {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:283fc8eed679758de38fe493b7d7d84a198b558942b03f017b1f94dda8efae80"}, + {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:f44dd4d68697559d007462b0a3a1d9acd61d97072b71f6d1968daef26bc744bd"}, + {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:67d3ccfc590e5e7197750fcb3a2915b416a53e2de847a728cfa60141054123d4"}, + {file = "regex-2023.12.25-cp311-cp311-win32.whl", hash = "sha256:68191f80a9bad283432385961d9efe09d783bcd36ed35a60fb1ff3f1ec2efe87"}, + {file = "regex-2023.12.25-cp311-cp311-win_amd64.whl", hash = "sha256:7d2af3f6b8419661a0c421584cfe8aaec1c0e435ce7e47ee2a97e344b98f794f"}, + {file = "regex-2023.12.25-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8a0ccf52bb37d1a700375a6b395bff5dd15c50acb745f7db30415bae3c2b0715"}, + {file = "regex-2023.12.25-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c3c4a78615b7762740531c27cf46e2f388d8d727d0c0c739e72048beb26c8a9d"}, + {file = "regex-2023.12.25-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ad83e7545b4ab69216cef4cc47e344d19622e28aabec61574b20257c65466d6a"}, + {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7a635871143661feccce3979e1727c4e094f2bdfd3ec4b90dfd4f16f571a87a"}, + {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d498eea3f581fbe1b34b59c697512a8baef88212f92e4c7830fcc1499f5b45a5"}, + {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:43f7cd5754d02a56ae4ebb91b33461dc67be8e3e0153f593c509e21d219c5060"}, + {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51f4b32f793812714fd5307222a7f77e739b9bc566dc94a18126aba3b92b98a3"}, + {file = "regex-2023.12.25-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba99d8077424501b9616b43a2d208095746fb1284fc5ba490139651f971d39d9"}, + {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4bfc2b16e3ba8850e0e262467275dd4d62f0d045e0e9eda2bc65078c0110a11f"}, + {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8c2c19dae8a3eb0ea45a8448356ed561be843b13cbc34b840922ddf565498c1c"}, + {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:60080bb3d8617d96f0fb7e19796384cc2467447ef1c491694850ebd3670bc457"}, + {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b77e27b79448e34c2c51c09836033056a0547aa360c45eeeb67803da7b0eedaf"}, + {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:518440c991f514331f4850a63560321f833979d145d7d81186dbe2f19e27ae3d"}, + {file = "regex-2023.12.25-cp312-cp312-win32.whl", hash = "sha256:e2610e9406d3b0073636a3a2e80db05a02f0c3169b5632022b4e81c0364bcda5"}, + {file = "regex-2023.12.25-cp312-cp312-win_amd64.whl", hash = "sha256:cc37b9aeebab425f11f27e5e9e6cf580be7206c6582a64467a14dda211abc232"}, + {file = "regex-2023.12.25-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:da695d75ac97cb1cd725adac136d25ca687da4536154cdc2815f576e4da11c69"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d126361607b33c4eb7b36debc173bf25d7805847346dd4d99b5499e1fef52bc7"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4719bb05094d7d8563a450cf8738d2e1061420f79cfcc1fa7f0a44744c4d8f73"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5dd58946bce44b53b06d94aa95560d0b243eb2fe64227cba50017a8d8b3cd3e2"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22a86d9fff2009302c440b9d799ef2fe322416d2d58fc124b926aa89365ec482"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2aae8101919e8aa05ecfe6322b278f41ce2994c4a430303c4cd163fef746e04f"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e692296c4cc2873967771345a876bcfc1c547e8dd695c6b89342488b0ea55cd8"}, + {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:263ef5cc10979837f243950637fffb06e8daed7f1ac1e39d5910fd29929e489a"}, + {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:d6f7e255e5fa94642a0724e35406e6cb7001c09d476ab5fce002f652b36d0c39"}, + {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:88ad44e220e22b63b0f8f81f007e8abbb92874d8ced66f32571ef8beb0643b2b"}, + {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:3a17d3ede18f9cedcbe23d2daa8a2cd6f59fe2bf082c567e43083bba3fb00347"}, + {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d15b274f9e15b1a0b7a45d2ac86d1f634d983ca40d6b886721626c47a400bf39"}, + {file = "regex-2023.12.25-cp37-cp37m-win32.whl", hash = "sha256:ed19b3a05ae0c97dd8f75a5d8f21f7723a8c33bbc555da6bbe1f96c470139d3c"}, + {file = "regex-2023.12.25-cp37-cp37m-win_amd64.whl", hash = "sha256:a6d1047952c0b8104a1d371f88f4ab62e6275567d4458c1e26e9627ad489b445"}, + {file = "regex-2023.12.25-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b43523d7bc2abd757119dbfb38af91b5735eea45537ec6ec3a5ec3f9562a1c53"}, + {file = "regex-2023.12.25-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:efb2d82f33b2212898f1659fb1c2e9ac30493ac41e4d53123da374c3b5541e64"}, + {file = "regex-2023.12.25-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b7fca9205b59c1a3d5031f7e64ed627a1074730a51c2a80e97653e3e9fa0d415"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086dd15e9435b393ae06f96ab69ab2d333f5d65cbe65ca5a3ef0ec9564dfe770"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e81469f7d01efed9b53740aedd26085f20d49da65f9c1f41e822a33992cb1590"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:34e4af5b27232f68042aa40a91c3b9bb4da0eeb31b7632e0091afc4310afe6cb"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9852b76ab558e45b20bf1893b59af64a28bd3820b0c2efc80e0a70a4a3ea51c1"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff100b203092af77d1a5a7abe085b3506b7eaaf9abf65b73b7d6905b6cb76988"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cc038b2d8b1470364b1888a98fd22d616fba2b6309c5b5f181ad4483e0017861"}, + {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:094ba386bb5c01e54e14434d4caabf6583334090865b23ef58e0424a6286d3dc"}, + {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5cd05d0f57846d8ba4b71d9c00f6f37d6b97d5e5ef8b3c3840426a475c8f70f4"}, + {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:9aa1a67bbf0f957bbe096375887b2505f5d8ae16bf04488e8b0f334c36e31360"}, + {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:98a2636994f943b871786c9e82bfe7883ecdaba2ef5df54e1450fa9869d1f756"}, + {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:37f8e93a81fc5e5bd8db7e10e62dc64261bcd88f8d7e6640aaebe9bc180d9ce2"}, + {file = "regex-2023.12.25-cp38-cp38-win32.whl", hash = "sha256:d78bd484930c1da2b9679290a41cdb25cc127d783768a0369d6b449e72f88beb"}, + {file = "regex-2023.12.25-cp38-cp38-win_amd64.whl", hash = "sha256:b521dcecebc5b978b447f0f69b5b7f3840eac454862270406a39837ffae4e697"}, + {file = "regex-2023.12.25-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f7bc09bc9c29ebead055bcba136a67378f03d66bf359e87d0f7c759d6d4ffa31"}, + {file = "regex-2023.12.25-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e14b73607d6231f3cc4622809c196b540a6a44e903bcfad940779c80dffa7be7"}, + {file = "regex-2023.12.25-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9eda5f7a50141291beda3edd00abc2d4a5b16c29c92daf8d5bd76934150f3edc"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc6bb9aa69aacf0f6032c307da718f61a40cf970849e471254e0e91c56ffca95"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:298dc6354d414bc921581be85695d18912bea163a8b23cac9a2562bbcd5088b1"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f4e475a80ecbd15896a976aa0b386c5525d0ed34d5c600b6d3ebac0a67c7ddf"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:531ac6cf22b53e0696f8e1d56ce2396311254eb806111ddd3922c9d937151dae"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22f3470f7524b6da61e2020672df2f3063676aff444db1daa283c2ea4ed259d6"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:89723d2112697feaa320c9d351e5f5e7b841e83f8b143dba8e2d2b5f04e10923"}, + {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0ecf44ddf9171cd7566ef1768047f6e66975788258b1c6c6ca78098b95cf9a3d"}, + {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:905466ad1702ed4acfd67a902af50b8db1feeb9781436372261808df7a2a7bca"}, + {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:4558410b7a5607a645e9804a3e9dd509af12fb72b9825b13791a37cd417d73a5"}, + {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:7e316026cc1095f2a3e8cc012822c99f413b702eaa2ca5408a513609488cb62f"}, + {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3b1de218d5375cd6ac4b5493e0b9f3df2be331e86520f23382f216c137913d20"}, + {file = "regex-2023.12.25-cp39-cp39-win32.whl", hash = "sha256:11a963f8e25ab5c61348d090bf1b07f1953929c13bd2309a0662e9ff680763c9"}, + {file = "regex-2023.12.25-cp39-cp39-win_amd64.whl", hash = "sha256:e693e233ac92ba83a87024e1d32b5f9ab15ca55ddd916d878146f4e3406b5c91"}, + {file = "regex-2023.12.25.tar.gz", hash = "sha256:29171aa128da69afdf4bde412d5bedc335f2ca8fcfe4489038577d05f16181e5"}, +] + [[package]] name = "requests" -version = "2.27.1" +version = "2.31.0" description = "Python HTTP for Humans." optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +python-versions = ">=3.7" files = [ - {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, - {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, ] [package.dependencies] certifi = ">=2017.4.17" -charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} -idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} -urllib3 = ">=1.21.1,<1.27" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" [package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<5)"] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "six" @@ -740,64 +1123,46 @@ files = [ ] [[package]] -name = "typed-ast" -version = "1.5.2" -description = "a fork of Python 2 and 3 ast modules with type comment support" +name = "types-requests" +version = "2.31.0.20240125" +description = "Typing stubs for requests" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "typed_ast-1.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:183b183b7771a508395d2cbffd6db67d6ad52958a5fdc99f450d954003900266"}, - {file = "typed_ast-1.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:676d051b1da67a852c0447621fdd11c4e104827417bf216092ec3e286f7da596"}, - {file = "typed_ast-1.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc2542e83ac8399752bc16e0b35e038bdb659ba237f4222616b4e83fb9654985"}, - {file = "typed_ast-1.5.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74cac86cc586db8dfda0ce65d8bcd2bf17b58668dfcc3652762f3ef0e6677e76"}, - {file = "typed_ast-1.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:18fe320f354d6f9ad3147859b6e16649a0781425268c4dde596093177660e71a"}, - {file = "typed_ast-1.5.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:31d8c6b2df19a777bc8826770b872a45a1f30cfefcfd729491baa5237faae837"}, - {file = "typed_ast-1.5.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:963a0ccc9a4188524e6e6d39b12c9ca24cc2d45a71cfdd04a26d883c922b4b78"}, - {file = "typed_ast-1.5.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0eb77764ea470f14fcbb89d51bc6bbf5e7623446ac4ed06cbd9ca9495b62e36e"}, - {file = "typed_ast-1.5.2-cp36-cp36m-win_amd64.whl", hash = "sha256:294a6903a4d087db805a7656989f613371915fc45c8cc0ddc5c5a0a8ad9bea4d"}, - {file = "typed_ast-1.5.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:26a432dc219c6b6f38be20a958cbe1abffcc5492821d7e27f08606ef99e0dffd"}, - {file = "typed_ast-1.5.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7407cfcad702f0b6c0e0f3e7ab876cd1d2c13b14ce770e412c0c4b9728a0f88"}, - {file = "typed_ast-1.5.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f30ddd110634c2d7534b2d4e0e22967e88366b0d356b24de87419cc4410c41b7"}, - {file = "typed_ast-1.5.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8c08d6625bb258179b6e512f55ad20f9dfef019bbfbe3095247401e053a3ea30"}, - {file = "typed_ast-1.5.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:90904d889ab8e81a956f2c0935a523cc4e077c7847a836abee832f868d5c26a4"}, - {file = "typed_ast-1.5.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bbebc31bf11762b63bf61aaae232becb41c5bf6b3461b80a4df7e791fabb3aca"}, - {file = "typed_ast-1.5.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c29dd9a3a9d259c9fa19d19738d021632d673f6ed9b35a739f48e5f807f264fb"}, - {file = "typed_ast-1.5.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:58ae097a325e9bb7a684572d20eb3e1809802c5c9ec7108e85da1eb6c1a3331b"}, - {file = "typed_ast-1.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:da0a98d458010bf4fe535f2d1e367a2e2060e105978873c04c04212fb20543f7"}, - {file = "typed_ast-1.5.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:33b4a19ddc9fc551ebabca9765d54d04600c4a50eda13893dadf67ed81d9a098"}, - {file = "typed_ast-1.5.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1098df9a0592dd4c8c0ccfc2e98931278a6c6c53cb3a3e2cf7e9ee3b06153344"}, - {file = "typed_ast-1.5.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42c47c3b43fe3a39ddf8de1d40dbbfca60ac8530a36c9b198ea5b9efac75c09e"}, - {file = "typed_ast-1.5.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f290617f74a610849bd8f5514e34ae3d09eafd521dceaa6cf68b3f4414266d4e"}, - {file = "typed_ast-1.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:df05aa5b241e2e8045f5f4367a9f6187b09c4cdf8578bb219861c4e27c443db5"}, - {file = "typed_ast-1.5.2.tar.gz", hash = "sha256:525a2d4088e70a9f75b08b3f87a51acc9cde640e19cc523c7e41aa355564ae27"}, + {file = "types-requests-2.31.0.20240125.tar.gz", hash = "sha256:03a28ce1d7cd54199148e043b2079cdded22d6795d19a2c2a6791a4b2b5e2eb5"}, + {file = "types_requests-2.31.0.20240125-py3-none-any.whl", hash = "sha256:9592a9a4cb92d6d75d9b491a41477272b710e021011a2a3061157e2fb1f1a5d1"}, ] +[package.dependencies] +urllib3 = ">=2" + [[package]] name = "typing-extensions" -version = "4.0.1" -description = "Backported and Experimental Type Hints for Python 3.6+" +version = "4.9.0" +description = "Backported and Experimental Type Hints for Python 3.8+" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"}, - {file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"}, + {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, + {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, ] [[package]] name = "urllib3" -version = "1.26.8" +version = "2.2.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +python-versions = ">=3.8" files = [ - {file = "urllib3-1.26.8-py2.py3-none-any.whl", hash = "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed"}, - {file = "urllib3-1.26.8.tar.gz", hash = "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"}, + {file = "urllib3-2.2.0-py3-none-any.whl", hash = "sha256:ce3711610ddce217e6d113a2732fafad960a03fd0318c91faa79481e35c11224"}, + {file = "urllib3-2.2.0.tar.gz", hash = "sha256:051d961ad0c62a94e50ecf1af379c3aba230c66c710493493560c0c223c49f20"}, ] [package.extras] -brotli = ["brotlipy (>=0.6.0)"] -secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)"] -socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] [[package]] name = "verspec" @@ -815,55 +1180,73 @@ test = ["coverage", "flake8 (>=3.7)", "mypy", "pretend", "pytest"] [[package]] name = "watchdog" -version = "2.1.6" +version = "3.0.0" description = "Filesystem events monitoring" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "watchdog-2.1.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9693f35162dc6208d10b10ddf0458cc09ad70c30ba689d9206e02cd836ce28a3"}, - {file = "watchdog-2.1.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:aba5c812f8ee8a3ff3be51887ca2d55fb8e268439ed44110d3846e4229eb0e8b"}, - {file = "watchdog-2.1.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ae38bf8ba6f39d5b83f78661273216e7db5b00f08be7592062cb1fc8b8ba542"}, - {file = "watchdog-2.1.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ad6f1796e37db2223d2a3f302f586f74c72c630b48a9872c1e7ae8e92e0ab669"}, - {file = "watchdog-2.1.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:922a69fa533cb0c793b483becaaa0845f655151e7256ec73630a1b2e9ebcb660"}, - {file = "watchdog-2.1.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b2fcf9402fde2672545b139694284dc3b665fd1be660d73eca6805197ef776a3"}, - {file = "watchdog-2.1.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3386b367e950a11b0568062b70cc026c6f645428a698d33d39e013aaeda4cc04"}, - {file = "watchdog-2.1.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f1c00aa35f504197561060ca4c21d3cc079ba29cf6dd2fe61024c70160c990b"}, - {file = "watchdog-2.1.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b52b88021b9541a60531142b0a451baca08d28b74a723d0c99b13c8c8d48d604"}, - {file = "watchdog-2.1.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8047da932432aa32c515ec1447ea79ce578d0559362ca3605f8e9568f844e3c6"}, - {file = "watchdog-2.1.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e92c2d33858c8f560671b448205a268096e17870dcf60a9bb3ac7bfbafb7f5f9"}, - {file = "watchdog-2.1.6-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b7d336912853d7b77f9b2c24eeed6a5065d0a0cc0d3b6a5a45ad6d1d05fb8cd8"}, - {file = "watchdog-2.1.6-py3-none-manylinux2014_aarch64.whl", hash = "sha256:cca7741c0fcc765568350cb139e92b7f9f3c9a08c4f32591d18ab0a6ac9e71b6"}, - {file = "watchdog-2.1.6-py3-none-manylinux2014_armv7l.whl", hash = "sha256:25fb5240b195d17de949588628fdf93032ebf163524ef08933db0ea1f99bd685"}, - {file = "watchdog-2.1.6-py3-none-manylinux2014_i686.whl", hash = "sha256:be9be735f827820a06340dff2ddea1fb7234561fa5e6300a62fe7f54d40546a0"}, - {file = "watchdog-2.1.6-py3-none-manylinux2014_ppc64.whl", hash = "sha256:d0d19fb2441947b58fbf91336638c2b9f4cc98e05e1045404d7a4cb7cddc7a65"}, - {file = "watchdog-2.1.6-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:3becdb380d8916c873ad512f1701f8a92ce79ec6978ffde92919fd18d41da7fb"}, - {file = "watchdog-2.1.6-py3-none-manylinux2014_s390x.whl", hash = "sha256:ae67501c95606072aafa865b6ed47343ac6484472a2f95490ba151f6347acfc2"}, - {file = "watchdog-2.1.6-py3-none-manylinux2014_x86_64.whl", hash = "sha256:e0f30db709c939cabf64a6dc5babb276e6d823fd84464ab916f9b9ba5623ca15"}, - {file = "watchdog-2.1.6-py3-none-win32.whl", hash = "sha256:e02794ac791662a5eafc6ffeaf9bcc149035a0e48eb0a9d40a8feb4622605a3d"}, - {file = "watchdog-2.1.6-py3-none-win_amd64.whl", hash = "sha256:bd9ba4f332cf57b2c1f698be0728c020399ef3040577cde2939f2e045b39c1e5"}, - {file = "watchdog-2.1.6-py3-none-win_ia64.whl", hash = "sha256:a0f1c7edf116a12f7245be06120b1852275f9506a7d90227648b250755a03923"}, - {file = "watchdog-2.1.6.tar.gz", hash = "sha256:a36e75df6c767cbf46f61a91c70b3ba71811dfa0aca4a324d9407a06a8b7a2e7"}, + {file = "watchdog-3.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:336adfc6f5cc4e037d52db31194f7581ff744b67382eb6021c868322e32eef41"}, + {file = "watchdog-3.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a70a8dcde91be523c35b2bf96196edc5730edb347e374c7de7cd20c43ed95397"}, + {file = "watchdog-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:adfdeab2da79ea2f76f87eb42a3ab1966a5313e5a69a0213a3cc06ef692b0e96"}, + {file = "watchdog-3.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2b57a1e730af3156d13b7fdddfc23dea6487fceca29fc75c5a868beed29177ae"}, + {file = "watchdog-3.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7ade88d0d778b1b222adebcc0927428f883db07017618a5e684fd03b83342bd9"}, + {file = "watchdog-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7e447d172af52ad204d19982739aa2346245cc5ba6f579d16dac4bfec226d2e7"}, + {file = "watchdog-3.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9fac43a7466eb73e64a9940ac9ed6369baa39b3bf221ae23493a9ec4d0022674"}, + {file = "watchdog-3.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8ae9cda41fa114e28faf86cb137d751a17ffd0316d1c34ccf2235e8a84365c7f"}, + {file = "watchdog-3.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:25f70b4aa53bd743729c7475d7ec41093a580528b100e9a8c5b5efe8899592fc"}, + {file = "watchdog-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4f94069eb16657d2c6faada4624c39464f65c05606af50bb7902e036e3219be3"}, + {file = "watchdog-3.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7c5f84b5194c24dd573fa6472685b2a27cc5a17fe5f7b6fd40345378ca6812e3"}, + {file = "watchdog-3.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3aa7f6a12e831ddfe78cdd4f8996af9cf334fd6346531b16cec61c3b3c0d8da0"}, + {file = "watchdog-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:233b5817932685d39a7896b1090353fc8efc1ef99c9c054e46c8002561252fb8"}, + {file = "watchdog-3.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:13bbbb462ee42ec3c5723e1205be8ced776f05b100e4737518c67c8325cf6100"}, + {file = "watchdog-3.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8f3ceecd20d71067c7fd4c9e832d4e22584318983cabc013dbf3f70ea95de346"}, + {file = "watchdog-3.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c9d8c8ec7efb887333cf71e328e39cffbf771d8f8f95d308ea4125bf5f90ba64"}, + {file = "watchdog-3.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0e06ab8858a76e1219e68c7573dfeba9dd1c0219476c5a44d5333b01d7e1743a"}, + {file = "watchdog-3.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:d00e6be486affb5781468457b21a6cbe848c33ef43f9ea4a73b4882e5f188a44"}, + {file = "watchdog-3.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:c07253088265c363d1ddf4b3cdb808d59a0468ecd017770ed716991620b8f77a"}, + {file = "watchdog-3.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:5113334cf8cf0ac8cd45e1f8309a603291b614191c9add34d33075727a967709"}, + {file = "watchdog-3.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:51f90f73b4697bac9c9a78394c3acbbd331ccd3655c11be1a15ae6fe289a8c83"}, + {file = "watchdog-3.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:ba07e92756c97e3aca0912b5cbc4e5ad802f4557212788e72a72a47ff376950d"}, + {file = "watchdog-3.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:d429c2430c93b7903914e4db9a966c7f2b068dd2ebdd2fa9b9ce094c7d459f33"}, + {file = "watchdog-3.0.0-py3-none-win32.whl", hash = "sha256:3ed7c71a9dccfe838c2f0b6314ed0d9b22e77d268c67e015450a29036a81f60f"}, + {file = "watchdog-3.0.0-py3-none-win_amd64.whl", hash = "sha256:4c9956d27be0bb08fc5f30d9d0179a855436e655f046d288e2bcc11adfae893c"}, + {file = "watchdog-3.0.0-py3-none-win_ia64.whl", hash = "sha256:5d9f3a10e02d7371cd929b5d8f11e87d4bad890212ed3901f9b4d68767bee759"}, + {file = "watchdog-3.0.0.tar.gz", hash = "sha256:4d98a320595da7a7c5a18fc48cb633c2e73cda78f93cac2ef42d42bf609a33f9"}, ] [package.extras] watchmedo = ["PyYAML (>=3.10)"] +[[package]] +name = "wheel" +version = "0.43.0" +description = "A built-package format for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "wheel-0.43.0-py3-none-any.whl", hash = "sha256:55c570405f142630c6b9f72fe09d9b67cf1477fcf543ae5b8dcb1f5b7377da81"}, + {file = "wheel-0.43.0.tar.gz", hash = "sha256:465ef92c69fa5c5da2d1cf8ac40559a8c940886afcef87dcf14b9470862f1d85"}, +] + +[package.extras] +test = ["pytest (>=6.0.0)", "setuptools (>=65)"] + [[package]] name = "zipp" -version = "3.7.0" +version = "3.17.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "zipp-3.7.0-py3-none-any.whl", hash = "sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375"}, - {file = "zipp-3.7.0.tar.gz", hash = "sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d"}, + {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"}, + {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"}, ] [package.extras] -docs = ["jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "sphinx"] -testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] [metadata] lock-version = "2.0" -python-versions = "^3.7" -content-hash = "ac45f5bd9007ca02401e6b8e3d069527b9235a20fd38a2bbd3c8e27a3cf4218f" +python-versions = "^3.8" +content-hash = "f4ad737b24c3992e5060ab2a08aa88bd67635c7f2a6f0f854cd7d325ac9dddd2" diff --git a/pyproject.toml b/pyproject.toml index 94defb8..1975eb4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,53 +1,74 @@ [tool.poetry] name = "bookops-worldcat" -version = "0.5.0" +version = "1.0.0" description = "OCLC WorldCat Metadata APIs wrapper" -authors = ["Tomasz Kalata "] +authors = ["Tomasz Kalata ", "Charlotte Kostelic "] license = "MIT" exclude = ["bookops_worldcat/temp.py", "bookops_worldcat/temp/*"] +packages = [ + {include = "bookops_worldcat"}, + {include = "bookops_worldcat/py.typed"}, +] + readme = "README.md" repository = "https://github.com/BookOps-CAT/bookops-worldcat" homepage = "https://bookops-cat.github.io/bookops-worldcat/" -keywords = ["api", "worldcat", "cataloging", "bibliographic records", "holdings", "library metadata"] +keywords = ["api", "api-wrapper", "oclc", "worldcat", "cataloging", "bibliographic records", "marcxml", "holdings", "library metadata", "marc21"] classifiers = [ - "Development Status :: 3 - Alpha", + "Development Status :: 5 - Production/Stable", "Intended Audience :: Education", "Intended Audience :: Information Technology", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Operating System :: OS Independent", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Topic :: Software Development :: Build Tools", "Topic :: Software Development :: Libraries :: Python Modules" ] [tool.poetry.dependencies] -python = "^3.7" -requests = "^2.27" +python = "^3.8" +requests = "^2.31" + +[tool.poetry.urls] +"Bug Tracker" = "https://github.com/BookOps-CAT/bookops-worldcat/issues" -[tool.poetry.dev-dependencies] +[tool.poetry.group.dev.dependencies] pytest = "^7.0" pytest-cov = "^3.0" pytest-mock = "^3.7" -mkdocs = "^1.2" -black = "^22.1.0" -mike = "^1.1.2" -mkapi = "^1.0.14" -mypy = "^0.931" +mkdocs = "^1.4" +black = "^23.3.0" +mike = "^2.0.0" +mypy = "^1.8" +types-requests = "^2.31.0.20240125" +mkdocs-material = "^9.5.13" +mkdocstrings = "^0.24.1" +mkdocstrings-python = "^1.9.0" -[tool.poetry.urls] -"Bug Tracker" = "https://github.com/BookOps-CAT/bookops-worldcat/issues" +[tool.pytest.ini_options] +testpaths = ["tests"] +markers = [ + "webtest: mark a test hitting live endpoints", + "holdings: mark holdings live endpoint tests", + "http_code: use to pass returned http code to 'mock_session_response' fixture that mocks 'requests.Session.send' method", +] + +[tool.coverage.run] +relative_files = true +source = ["."] [tool.black] line-length = 88 -target-version = ['py37'] +target-version = ['py38', 'py39', 'py310', 'py311', 'py312'] include = '\.pyi?$' exclude = ''' ( diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index 835c16c..0000000 --- a/pytest.ini +++ /dev/null @@ -1,5 +0,0 @@ -[pytest] -markers = - webtest: mark a test hitting live endpoints - holdings: mark holdings live endpoint tests - http_code: use to pass returned http code to 'mock_session_reponse' fixture that mocks 'requests.Session.send' method diff --git a/requirements.txt b/requirements.txt index 238cc23..e2b1f0e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,15 +1,103 @@ -certifi==2021.10.8; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" \ - --hash=sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569 \ - --hash=sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872 -charset-normalizer==2.0.11; python_full_version >= "3.6.0" and python_version >= "3" \ - --hash=sha256:98398a9d69ee80548c762ba991a4728bfc3836768ed226b3945908d1a688371c \ - --hash=sha256:2842d8f5e82a1f6aa437380934d5e1cd4fcf2003b06fed6940769c164a480a45 -idna==3.3; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.5" \ - --hash=sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff \ - --hash=sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d -requests==2.27.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0") \ - --hash=sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d \ - --hash=sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61 -urllib3==1.26.8; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version < "4" \ - --hash=sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed \ - --hash=sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c +certifi==2024.2.2 ; python_version >= "3.8" and python_version < "4.0" \ + --hash=sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f \ + --hash=sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1 +charset-normalizer==3.3.2 ; python_version >= "3.8" and python_version < "4.0" \ + --hash=sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027 \ + --hash=sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087 \ + --hash=sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786 \ + --hash=sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8 \ + --hash=sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09 \ + --hash=sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185 \ + --hash=sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574 \ + --hash=sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e \ + --hash=sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519 \ + --hash=sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898 \ + --hash=sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269 \ + --hash=sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3 \ + --hash=sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f \ + --hash=sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6 \ + --hash=sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8 \ + --hash=sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a \ + --hash=sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73 \ + --hash=sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc \ + --hash=sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714 \ + --hash=sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2 \ + --hash=sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc \ + --hash=sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce \ + --hash=sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d \ + --hash=sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e \ + --hash=sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6 \ + --hash=sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269 \ + --hash=sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96 \ + --hash=sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d \ + --hash=sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a \ + --hash=sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4 \ + --hash=sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77 \ + --hash=sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d \ + --hash=sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0 \ + --hash=sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed \ + --hash=sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068 \ + --hash=sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac \ + --hash=sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25 \ + --hash=sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8 \ + --hash=sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab \ + --hash=sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26 \ + --hash=sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2 \ + --hash=sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db \ + --hash=sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f \ + --hash=sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5 \ + --hash=sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99 \ + --hash=sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c \ + --hash=sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d \ + --hash=sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811 \ + --hash=sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa \ + --hash=sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a \ + --hash=sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03 \ + --hash=sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b \ + --hash=sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04 \ + --hash=sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c \ + --hash=sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001 \ + --hash=sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458 \ + --hash=sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389 \ + --hash=sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99 \ + --hash=sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985 \ + --hash=sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537 \ + --hash=sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238 \ + --hash=sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f \ + --hash=sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d \ + --hash=sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796 \ + --hash=sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a \ + --hash=sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143 \ + --hash=sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8 \ + --hash=sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c \ + --hash=sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5 \ + --hash=sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5 \ + --hash=sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711 \ + --hash=sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4 \ + --hash=sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6 \ + --hash=sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c \ + --hash=sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7 \ + --hash=sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4 \ + --hash=sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b \ + --hash=sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae \ + --hash=sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12 \ + --hash=sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c \ + --hash=sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae \ + --hash=sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8 \ + --hash=sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887 \ + --hash=sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b \ + --hash=sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4 \ + --hash=sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f \ + --hash=sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5 \ + --hash=sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33 \ + --hash=sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519 \ + --hash=sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561 +idna==3.6 ; python_version >= "3.8" and python_version < "4.0" \ + --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ + --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f +requests==2.31.0 ; python_version >= "3.8" and python_version < "4.0" \ + --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \ + --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1 +urllib3==2.2.0 ; python_version >= "3.8" and python_version < "4.0" \ + --hash=sha256:051d961ad0c62a94e50ecf1af379c3aba230c66c710493493560c0c223c49f20 \ + --hash=sha256:ce3711610ddce217e6d113a2732fafad960a03fd0318c91faa79481e35c11224 diff --git a/tests/conftest.py b/tests/conftest.py index fd4865a..95ff734 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,18 +21,38 @@ def live_keys(): os.environ["WCKey"] = data["key"] os.environ["WCSecret"] = data["secret"] os.environ["WCScopes"] = data["scopes"] - os.environ["WCPrincipalID"] = data["principal_id"] - os.environ["WCPrincipalIDNS"] = data["principal_idns"] + + +@pytest.fixture +def stub_marc_xml(): + stub_marc_xml = "00000nam a2200000 a 4500120827s2012 nyua 000 0 eng d 63011276 ocn850940548OCWMSengOCWMSOCLC Developer NetworkTest RecordFOR OCLC DEVELOPER NETWORK DOCUMENTATION" + return stub_marc_xml + + +@pytest.fixture +def stub_holding_xml(): + stub_holding_xml = "00000nx a2200000zi 4500312010zu1103280p 0 4001uueng0210908OCWMSEASTEAST-STACKS879456" + return stub_holding_xml + + +@pytest.fixture +def stub_marc21(): + fh = os.path.join( + os.environ["USERPROFILE"], "github/bookops-worldcat/temp/test.mrc" + ) + with open(fh, "rb") as stub: + stub_marc21 = stub.read() + return stub_marc21 class FakeUtcNow(datetime.datetime): @classmethod - def utcnow(cls): - return cls(2020, 1, 1, 17, 0, 0, 0) + def now(cls, tzinfo=datetime.timezone.utc): + return cls(2020, 1, 1, 17, 0, 0, 0, tzinfo=datetime.timezone.utc) @pytest.fixture -def mock_utcnow(monkeypatch): +def mock_now(monkeypatch): monkeypatch.setattr(datetime, "datetime", FakeUtcNow) @@ -44,7 +64,7 @@ def __init__(self): def json(self): expires_at = datetime.datetime.strftime( - datetime.datetime.utcnow() + datetime.timedelta(0, 1199), + datetime.datetime.now() + datetime.timedelta(0, 1199), "%Y-%m-%d %H:%M:%SZ", ) @@ -102,6 +122,11 @@ def __init__(self, *args, **kwargs): raise requests.exceptions.ConnectionError +class MockRetryError: + def __init__(self, *args, **kwargs): + raise requests.exceptions.RetryError + + class MockHTTPSessionResponse(Response): def __init__(self, http_code): self.status_code = http_code @@ -134,19 +159,17 @@ def mock_credentials(): return { "key": "my_WSkey", "secret": "my_WSsecret", - "scopes": ["scope1", "scope2"], - "principal_id": "my_principalID", - "principal_idns": "my_principalIDNS", + "scopes": "scope1 scope2", } @pytest.fixture -def mock_oauth_server_response(mock_utcnow, *args, **kwargs): +def mock_oauth_server_response(mock_now, *args, **kwargs): return MockAuthServerResponseSuccess() @pytest.fixture -def mock_successful_post_token_response(mock_utcnow, monkeypatch): +def mock_successful_post_token_response(mock_now, monkeypatch): def mock_oauth_server_response(*args, **kwargs): return MockAuthServerResponseSuccess() @@ -182,6 +205,13 @@ def mock_connection_error(monkeypatch): monkeypatch.setattr("requests.Session.send", MockConnectionError) +@pytest.fixture +def mock_retry_error(monkeypatch): + monkeypatch.setattr("requests.post", MockRetryError) + monkeypatch.setattr("requests.get", MockRetryError) + monkeypatch.setattr("requests.Session.send", MockRetryError) + + @pytest.fixture def mock_token(mock_credentials, mock_successful_post_token_response): return WorldcatAccessToken(**mock_credentials) @@ -193,6 +223,18 @@ def stub_session(mock_token): yield session +@pytest.fixture +def stub_retry_session(mock_token): + with MetadataSession( + authorization=mock_token, + totalRetries=3, + backoffFactor=0.5, + statusForcelist=[500, 502, 503, 504], + allowedMethods=["GET", "POST", "PUT"], + ) as session: + yield session + + @pytest.fixture def mock_400_response(monkeypatch): def mock_api_response(*args, **kwargs): diff --git a/tests/test_authorize.py b/tests/test_authorize.py index e440d65..07625bb 100644 --- a/tests/test_authorize.py +++ b/tests/test_authorize.py @@ -19,17 +19,17 @@ class TestWorldcatAccessToken: [ ( None, - pytest.raises(WorldcatAuthorizationError), - "Argument 'key' is required.", + pytest.raises(TypeError), + "Argument 'key' must be a string.", ), ( "", - pytest.raises(WorldcatAuthorizationError), - "Argument 'key' is required.", + pytest.raises(ValueError), + "Argument 'key' cannot be an empty string.", ), ( 124, - pytest.raises(WorldcatAuthorizationError), + pytest.raises(TypeError), "Argument 'key' must be a string.", ), ], @@ -40,27 +40,25 @@ def test_key_exceptions(self, argm, expectation, msg): key=argm, secret="my_secret", scopes=["scope1"], - principal_id="my_principalID", - principal_idns="my_principalIDNS", ) - assert msg in str(exp.value) + assert msg in str(exp.value) @pytest.mark.parametrize( "argm,expectation,msg", [ ( None, - pytest.raises(WorldcatAuthorizationError), - "Argument 'secret' is required.", + pytest.raises(TypeError), + "Argument 'secret' must be a string.", ), ( "", - pytest.raises(WorldcatAuthorizationError), - "Argument 'secret' is required.", + pytest.raises(ValueError), + "Argument 'secret' cannot be an empty string.", ), ( 123, - pytest.raises(WorldcatAuthorizationError), + pytest.raises(TypeError), "Argument 'secret' must be a string.", ), ], @@ -71,107 +69,41 @@ def test_secret_exceptions(self, argm, expectation, msg): key="my_key", secret=argm, scopes=["scope1"], - principal_id="my_principalID", - principal_idns="my_principalIDNS", ) - assert msg in str(exp.value) + assert msg in str(exp.value) def test_agent_exceptions(self): - with pytest.raises(WorldcatAuthorizationError) as exp: + with pytest.raises(TypeError) as exp: WorldcatAccessToken( key="my_key", secret="my_secret", scopes="scope1", - principal_id="my_principalID", - principal_idns="my_principalIDNS", agent=124, ) - assert "Argument 'agent' must be a string." in str(exp.value) - - def test_agent_default_values(self, mock_successful_post_token_response): - token = WorldcatAccessToken( - key="my_key", - secret="my_secret", - scopes="scope1", - principal_id="my_principalID", - principal_idns="my_principalIDNS", - ) - assert token.agent == f"{__title__}/{__version__}" - - @pytest.mark.parametrize( - "arg,expectation,msg", - [ - ( - None, - pytest.raises(WorldcatAuthorizationError), - "Argument 'principal_id' is required for read/write endpoint of Metadata API.", - ), - ( - "", - pytest.raises(WorldcatAuthorizationError), - "Argument 'principal_id' is required for read/write endpoint of Metadata API.", - ), - ], - ) - def test_principal_id_exception(self, arg, expectation, msg): - with expectation as exc: - WorldcatAccessToken( - key="my_key", - secret="my_secret", - scopes="scope1", - principal_id=arg, - principal_idns="my_principalIDNS", - ) - assert msg in str(exc.value) - - @pytest.mark.parametrize( - "arg,expectation,msg", - [ - ( - None, - pytest.raises(WorldcatAuthorizationError), - "Argument 'principal_idns' is required for read/write endpoint of Metadata API.", - ), - ( - "", - pytest.raises(WorldcatAuthorizationError), - "Argument 'principal_idns' is required for read/write endpoint of Metadata API.", - ), - ], - ) - def test_principal_idns_exception(self, arg, expectation, msg): - with expectation as exc: - WorldcatAccessToken( - key="my_key", - secret="my_secret", - scopes="scope1", - principal_id="my_principalID", - principal_idns=arg, - ) - assert msg in str(exc.value) + assert "Argument 'agent' must be a string." in str(exp.value) @pytest.mark.parametrize( "argm,expectation,msg", [ ( None, - pytest.raises(WorldcatAuthorizationError), - "Argument 'scope' must a string or a list.", + pytest.raises(TypeError), + "Argument 'scopes' must a string.", ), ( 123, - pytest.raises(WorldcatAuthorizationError), - "Argument 'scope' must a string or a list.", + pytest.raises(TypeError), + "Argument 'scopes' must a string.", ), ( " ", - pytest.raises(WorldcatAuthorizationError), - "Argument 'scope' is missing.", + pytest.raises(ValueError), + "Argument 'scopes' cannot be an empty string.", ), ( ["", ""], - pytest.raises(WorldcatAuthorizationError), - "Argument 'scope' is missing.", + pytest.raises(TypeError), + "Argument 'scopes' is required.", ), ], ) @@ -181,8 +113,6 @@ def test_scope_exceptions(self, argm, expectation, msg): key="my_key", secret="my_secret", scopes=argm, - principal_id="my_principalID", - principal_idns="my_principalIDNS", ) assert msg in str(exp.value) @@ -203,15 +133,13 @@ def test_timeout_argument( key="my_key", secret="my_secret", scopes="scope1", - principal_id="my_principalID", - principal_idns="my_principalIDNS", timeout=argm, ) assert token.timeout == expectation @pytest.mark.parametrize( "argm,expectation", - [("scope1", "scope1"), (["scope1", "scope2"], "scope1 scope2")], + [("scope1 ", "scope1"), (" scope1 scope2 ", "scope1 scope2")], ) def test_scope_manipulation( self, argm, expectation, mock_successful_post_token_response @@ -220,8 +148,6 @@ def test_scope_manipulation( key="my_key", secret="my_secret", scopes=argm, - principal_id="my_principalID", - principal_idns="my_principalIDNS", ) assert token.scopes == expectation @@ -230,8 +156,6 @@ def test_token_url(self, mock_successful_post_token_response): key="my_key", secret="my_secret", scopes="scope1", - principal_id="my_principalID", - principal_idns="my_principalIDNS", ) assert token._token_url() == "https://oauth.oclc.org/token" @@ -240,8 +164,6 @@ def test_token_headers(self, mock_successful_post_token_response): key="my_key", secret="my_secret", scopes="scope1", - principal_id="my_principalID", - principal_idns="my_principalIDNS", agent="foo", ) assert token._token_headers() == { @@ -254,8 +176,6 @@ def test_auth(self, mock_successful_post_token_response): key="my_key", secret="my_secret", scopes="scope1", - principal_id="my_principalID", - principal_idns="my_principalIDNS", agent="foo", ) assert token._auth() == ("my_key", "my_secret") @@ -263,22 +183,22 @@ def test_auth(self, mock_successful_post_token_response): def test_hasten_expiration_time(self, mock_token): utc_stamp = "2020-01-01 17:19:59Z" token = mock_token - assert token._hasten_expiration_time(utc_stamp) == "2020-01-01 17:19:58Z" + timestamp = token._hasten_expiration_time(utc_stamp) + assert isinstance(timestamp, datetime.datetime) + assert timestamp == datetime.datetime( + 2020, 1, 1, 17, 19, 58, 0, tzinfo=datetime.timezone.utc + ) def test_payload(self, mock_successful_post_token_response): token = WorldcatAccessToken( key="my_key", secret="my_secret", scopes="scope1", - principal_id="my_principalID", - principal_idns="my_principalIDNS", agent="foo", ) assert token._payload() == { "grant_type": "client_credentials", "scope": "scope1", - "principalID": "my_principalID", - "principalIDNS": "my_principalIDNS", } def test_post_token_request_timout(self, mock_credentials, mock_timeout): @@ -288,8 +208,6 @@ def test_post_token_request_timout(self, mock_credentials, mock_timeout): key=creds["key"], secret=creds["secret"], scopes=creds["scopes"], - principal_id=creds["principal_id"], - principal_idns=creds["principal_idns"], ) def test_post_token_request_connectionerror( @@ -301,8 +219,6 @@ def test_post_token_request_connectionerror( key=creds["key"], secret=creds["secret"], scopes=creds["scopes"], - principal_id=creds["principal_id"], - principal_idns=creds["principal_idns"], ) def test_post_token_request_unexpectederror( @@ -314,8 +230,6 @@ def test_post_token_request_unexpectederror( key=creds["key"], secret=creds["secret"], scopes=creds["scopes"], - principal_id=creds["principal_id"], - principal_idns=creds["principal_idns"], ) def test_invalid_post_token_request( @@ -327,35 +241,30 @@ def test_invalid_post_token_request( key=creds["key"], secret=creds["secret"], scopes=creds["scopes"], - principal_id=creds["principal_id"], - principal_idns=creds["principal_idns"], ) def test_is_expired_false( - self, mock_utcnow, mock_credentials, mock_successful_post_token_response + self, mock_now, mock_credentials, mock_successful_post_token_response ): creds = mock_credentials token = WorldcatAccessToken( key=creds["key"], secret=creds["secret"], scopes=creds["scopes"], - principal_id=creds["principal_id"], - principal_idns=creds["principal_idns"], ) assert token.is_expired() is False - def test_is_expired_true(self, mock_utcnow, mock_token): + def test_is_expired_true(self, mock_now, mock_token): mock_token.is_expired() is False - mock_token.token_expires_at = datetime.datetime.strftime( - datetime.datetime.utcnow() - datetime.timedelta(0, 1), - "%Y-%m-%d %H:%M:%SZ", - ) + mock_token.token_expires_at = datetime.datetime.now( + datetime.timezone.utc + ) - datetime.timedelta(0, 1) assert mock_token.is_expired() is True @pytest.mark.parametrize( "arg,expectation", - [(None, pytest.raises(TypeError)), ("20-01-01", pytest.raises(ValueError))], + [(None, pytest.raises(TypeError))], ) def test_is_expired_exception(self, arg, expectation, mock_token): mock_token.token_expires_at = arg @@ -373,8 +282,6 @@ def test_post_token_request( key=creds["key"], secret=creds["secret"], scopes=creds["scopes"], - principal_id=creds["principal_id"], - principal_idns=creds["principal_idns"], ) assert token.token_str == "tk_Yebz4BpEp9dAsghA7KpWx6dYD1OZKWBlHjqW" assert token.token_type == "bearer" @@ -389,7 +296,7 @@ def test_post_token_request( def test_token_repr( self, mock_token, - mock_utcnow, + mock_now, ): assert ( str(mock_token) @@ -401,8 +308,6 @@ def test_cred_in_env_variables(self, live_keys): assert os.getenv("WCKey") is not None assert os.getenv("WCSecret") is not None assert os.getenv("WCScopes") == "WorldCatMetadataAPI" - assert os.getenv("WCPrincipalID") is not None - assert os.getenv("WCPrincipalIDNS") is not None @pytest.mark.webtest def test_post_token_request_with_live_service(self, live_keys): @@ -410,8 +315,6 @@ def test_post_token_request_with_live_service(self, live_keys): key=os.getenv("WCKey"), secret=os.getenv("WCSecret"), scopes=os.getenv("WCScopes"), - principal_id=os.getenv("WCPrincipalID"), - principal_idns=os.getenv("WCPrincipalIDNS"), ) assert token.server_response.status_code == 200 @@ -436,5 +339,6 @@ def test_post_token_request_with_live_service(self, live_keys): assert sorted(params) == sorted(response.keys()) # test if token looks right - assert token.token_str is not None + assert token.token_str.startswith("tk_") assert token.is_expired() is False + assert isinstance(token.token_expires_at, datetime.datetime) diff --git a/tests/test_metadata_api.py b/tests/test_metadata_api.py index 65823e8..0f18459 100644 --- a/tests/test_metadata_api.py +++ b/tests/test_metadata_api.py @@ -3,13 +3,16 @@ from contextlib import contextmanager import datetime import os -from types import GeneratorType import pytest from bookops_worldcat import MetadataSession, WorldcatAccessToken -from bookops_worldcat.errors import WorldcatSessionError, WorldcatRequestError +from bookops_worldcat.errors import ( + WorldcatRequestError, + WorldcatAuthorizationError, + InvalidOclcNumber, +) @contextmanager @@ -34,628 +37,746 @@ def test_missing_authorization(self): with pytest.raises(TypeError): MetadataSession() - def test_invalid_authorizaiton(self): + def test_invalid_authorization(self): err_msg = "Argument 'authorization' must be 'WorldcatAccessToken' object." - with pytest.raises(WorldcatSessionError) as exc: + with pytest.raises(TypeError) as exc: MetadataSession(authorization="my_token") assert err_msg in str(exc.value) - def test_get_new_access_token(self, mock_token, mock_utcnow): + def test_get_new_access_token(self, mock_token, mock_now): assert mock_token.is_expired() is False with MetadataSession(authorization=mock_token) as session: - session.authorization.token_expires_at = datetime.datetime.strftime( - datetime.datetime.utcnow() - datetime.timedelta(0, 1), - "%Y-%m-%d %H:%M:%SZ", - ) + session.authorization.token_expires_at = datetime.datetime.now( + datetime.timezone.utc + ) - datetime.timedelta(0, 1) assert session.authorization.is_expired() is True session._get_new_access_token() - assert session.authorization.token_expires_at == "2020-01-01 17:19:58Z" + assert session.authorization.token_expires_at == datetime.datetime( + 2020, 1, 1, 17, 19, 58, tzinfo=datetime.timezone.utc + ) assert session.authorization.is_expired() is False def test_get_new_access_token_exceptions(self, stub_session, mock_timeout): - with pytest.raises(WorldcatSessionError): + with pytest.raises(WorldcatAuthorizationError): stub_session._get_new_access_token() + def test_url_base(self, stub_session): + assert stub_session.BASE_URL == "https://metadata.api.oclc.org/worldcat" + @pytest.mark.parametrize( - "oclcNumbers,expectation", - [ - pytest.param([], [], id="empty list"), - pytest.param(["1", "2", "3"], ["1,2,3"], id="list of str"), - pytest.param(["1"], ["1"], id="list of one"), - pytest.param(["1"] * 50, [",".join(["1"] * 50)], id="full batch"), - pytest.param(["1"] * 51, [",".join(["1"] * 50), "1"], id="2 batches"), - pytest.param( - ["1"] * 103, - [",".join(["1"] * 50), ",".join(["1"] * 50), "1,1,1"], - id="3 batches", - ), - ], + "validationLevel", + ["vaildateFull", "validateAdd", "validateReplace"], ) - def test_split_into_legal_volume(self, stub_session, oclcNumbers, expectation): - batches = stub_session._split_into_legal_volume(oclcNumbers) - assert isinstance(batches, GeneratorType) - all_batches = [b for b in batches] - assert all_batches == expectation + def test_url_manage_bibs_validate(self, validationLevel, stub_session): + assert ( + stub_session._url_manage_bibs_validate(validationLevel) + == f"https://metadata.api.oclc.org/worldcat/manage/bibs/validate/{validationLevel}" + ) - def test_url_base(self, stub_session): - assert stub_session._url_base() == "https://worldcat.org" + def test_url_manage_bib_current_oclc_number(self, stub_session): + assert ( + stub_session._url_manage_bibs_current_oclc_number() + == "https://metadata.api.oclc.org/worldcat/manage/bibs/current" + ) + + def test_url_manage_bibs_create(self, stub_session): + assert ( + stub_session._url_manage_bibs_create() + == "https://metadata.api.oclc.org/worldcat/manage/bibs" + ) + + def test_url_manage_bibs(self, stub_session): + assert ( + stub_session._url_manage_bibs(oclcNumber="12345") + == "https://metadata.api.oclc.org/worldcat/manage/bibs/12345" + ) + + def test_url_manage_bibs_match(self, stub_session): + assert ( + stub_session._url_manage_bibs_match() + == "https://metadata.api.oclc.org/worldcat/manage/bibs/match" + ) + + def test_url_manage_ih_current(self, stub_session): + assert ( + stub_session._url_manage_ih_current() + == "https://metadata.api.oclc.org/worldcat/manage/institution/holdings/current" + ) + + def test_url_manage_ih_set(self, stub_session): + assert ( + stub_session._url_manage_ih_set(oclcNumber="12345") + == "https://metadata.api.oclc.org/worldcat/manage/institution/holdings/12345/set" + ) + + def test_url_manage_ih_unset(self, stub_session): + assert ( + stub_session._url_manage_ih_unset(oclcNumber="12345") + == "https://metadata.api.oclc.org/worldcat/manage/institution/holdings/12345/unset" + ) - def test_url_search_base(self, stub_session): + def test_url_manage_ih_set_with_bib(self, stub_session): assert ( - stub_session._url_search_base() - == "https://americas.metadata.api.oclc.org/worldcat/search/v1" + stub_session._url_manage_ih_set_with_bib() + == "https://metadata.api.oclc.org/worldcat/manage/institution/holdings/set" ) - def test_url_shared_print_holdings(self, stub_session): + def test_url_manage_ih_unset_with_bib(self, stub_session): assert ( - stub_session._url_member_shared_print_holdings() - == "https://americas.metadata.api.oclc.org/worldcat/search/v1/bibs-retained-holdings" + stub_session._url_manage_ih_unset_with_bib() + == "https://metadata.api.oclc.org/worldcat/manage/institution/holdings/unset" ) - def test_url_member_general_holdings(self, stub_session): + def test_url_manage_ih_codes(self, stub_session): assert ( - stub_session._url_member_general_holdings() - == "https://americas.metadata.api.oclc.org/worldcat/search/v1/bibs-summary-holdings" + stub_session._url_manage_ih_codes() + == "https://metadata.api.oclc.org/worldcat/manage/institution/holding-codes" ) - def test_url_brief_bib_search(self, stub_session): + def test_url_manage_lbd_create(self, stub_session): assert ( - stub_session._url_brief_bib_search() - == "https://americas.metadata.api.oclc.org/worldcat/search/v1/brief-bibs" + stub_session._url_manage_lbd_create() + == "https://metadata.api.oclc.org/worldcat/manage/lbds" ) @pytest.mark.parametrize( - "argm, expectation", - [ - ( - "12345", - "https://americas.metadata.api.oclc.org/worldcat/search/v1/brief-bibs/12345", - ), - ( - 12345, - "https://americas.metadata.api.oclc.org/worldcat/search/v1/brief-bibs/12345", - ), - ], + "controlNumber", + ["12345", 12345], ) - def test_url_brief_bib_oclc_number(self, argm, expectation, stub_session): + def test_url_manage_lbd(self, controlNumber, stub_session): assert ( - stub_session._url_brief_bib_oclc_number(oclcNumber=argm) - == "https://americas.metadata.api.oclc.org/worldcat/search/v1/brief-bibs/12345" + stub_session._url_manage_lbd(controlNumber) + == f"https://metadata.api.oclc.org/worldcat/manage/lbds/{controlNumber}" ) - def test_url_brief_bib_other_editions(self, stub_session): + def test_url_manage_lhr_create(self, stub_session): assert ( - stub_session._url_brief_bib_other_editions(oclcNumber="12345") - == "https://americas.metadata.api.oclc.org/worldcat/search/v1/brief-bibs/12345/other-editions" + stub_session._url_manage_lhr_create() + == "https://metadata.api.oclc.org/worldcat/manage/lhrs" ) - def test_url_lhr_control_number(self, stub_session): + @pytest.mark.parametrize( + "controlNumber", + ["12345", 12345], + ) + def test_url_manage_lhr(self, controlNumber, stub_session): assert ( - stub_session._url_lhr_control_number(controlNumber="12345") - == "https://americas.metadata.api.oclc.org/worldcat/search/v1/my-holdings/12345" + stub_session._url_manage_lhr(controlNumber) + == f"https://metadata.api.oclc.org/worldcat/manage/lhrs/{controlNumber}" ) - def test_url_lhr_search(self, stub_session): + def test_url_search_shared_print_holdings(self, stub_session): assert ( - stub_session._url_lhr_search() - == "https://americas.metadata.api.oclc.org/worldcat/search/v1/my-holdings" + stub_session._url_search_shared_print_holdings() + == "https://metadata.api.oclc.org/worldcat/search/bibs-retained-holdings" ) - def test_url_lhr_shared_print(self, stub_session): + def test_url_search_general_holdings(self, stub_session): assert ( - stub_session._url_lhr_shared_print() - == "https://americas.metadata.api.oclc.org/worldcat/search/v1/retained-holdings" + stub_session._url_search_general_holdings() + == "https://metadata.api.oclc.org/worldcat/search/bibs-summary-holdings" ) - def test_url_bib_oclc_number(self, stub_session): + def test_url_search_general_holdings_summary(self, stub_session): assert ( - stub_session._url_bib_oclc_number(oclcNumber="12345") - == "https://worldcat.org/bib/data/12345" + stub_session._url_search_general_holdings_summary() + == "https://metadata.api.oclc.org/worldcat/search/summary-holdings" ) - def test_url_bib_check_oclc_numbers(self, stub_session): + def test_url_search_brief_bibs(self, stub_session): assert ( - stub_session._url_bib_check_oclc_numbers() - == "https://worldcat.org/bib/checkcontrolnumbers" + stub_session._url_search_brief_bibs() + == "https://metadata.api.oclc.org/worldcat/search/brief-bibs" ) - def test_url_bib_holding_libraries(self, stub_session): + @pytest.mark.parametrize( + "argm", + ["12345", 12345], + ) + def test_url_search_brief_bibs_oclc_number(self, argm, stub_session): assert ( - stub_session._url_bib_holding_libraries() - == "https://worldcat.org/bib/holdinglibraries" + stub_session._url_search_brief_bibs_oclc_number(oclcNumber=argm) + == "https://metadata.api.oclc.org/worldcat/search/brief-bibs/12345" ) - def test_url_bib_holdings_action(self, stub_session): - assert stub_session._url_bib_holdings_action() == "https://worldcat.org/ih/data" + def test_url_search_brief_bibs_other_editions(self, stub_session): + assert ( + stub_session._url_search_brief_bibs_other_editions(oclcNumber="12345") + == "https://metadata.api.oclc.org/worldcat/search/brief-bibs/12345/other-editions" + ) - def test_url_bib_holdings_check(self, stub_session): + @pytest.mark.parametrize( + "oclcNumber", + ["850940461", "850940463", 850940467], + ) + def test_url_search_classification_bibs(self, oclcNumber, stub_session): assert ( - stub_session._url_bib_holdings_check() - == "https://worldcat.org/ih/checkholdings" + stub_session._url_search_classification_bibs(oclcNumber) + == f"https://metadata.api.oclc.org/worldcat/search/classification-bibs/{oclcNumber}" ) - def test_url_bib_holdings_batch_action(self, stub_session): + def test_url_search_lhr_shared_print(self, stub_session): assert ( - stub_session._url_bib_holdings_batch_action() - == "https://worldcat.org/ih/datalist" + stub_session._url_search_lhr_shared_print() + == "https://metadata.api.oclc.org/worldcat/search/retained-holdings" ) - def test_url_bib_holdings_multi_institution_batch_action(self, stub_session): + @pytest.mark.parametrize( + "controlNumber", + ["12345", 12345], + ) + def test_url_search_lhr_control_number(self, controlNumber, stub_session): assert ( - stub_session._url_bib_holdings_multi_institution_batch_action() - == "https://worldcat.org/ih/institutionlist" + stub_session._url_search_lhr_control_number(controlNumber) + == f"https://metadata.api.oclc.org/worldcat/search/my-holdings/{controlNumber}" + ) + + def test_url_search_lhr(self, stub_session): + assert ( + stub_session._url_search_lhr() + == "https://metadata.api.oclc.org/worldcat/search/my-holdings" + ) + + def test_url_browse_lhr(self, stub_session): + assert ( + stub_session._url_browse_lhr() + == "https://metadata.api.oclc.org/worldcat/browse/my-holdings" + ) + + @pytest.mark.parametrize( + "controlNumber", + ["12345", 12345], + ) + def test_url_search_lbd_control_number(self, controlNumber, stub_session): + assert ( + stub_session._url_search_lbd_control_number(controlNumber) + == f"https://metadata.api.oclc.org/worldcat/search/my-local-bib-data/{controlNumber}" + ) + + def test_url_search_lbd(self, stub_session): + assert ( + stub_session._url_search_lbd() + == "https://metadata.api.oclc.org/worldcat/search/my-local-bib-data" + ) + + @pytest.mark.http_code(200) + def test_bib_create(self, stub_session, mock_session_response, stub_marc_xml): + assert ( + stub_session.bib_create( + stub_marc_xml, recordFormat="application/marcxml+xml" + ).status_code + == 200 ) @pytest.mark.http_code(200) - def test_get_brief_bib(self, stub_session, mock_session_response): - assert stub_session.get_brief_bib(12345).status_code == 200 + def test_bib_get(self, stub_session, mock_session_response): + assert stub_session.bib_get(12345).status_code == 200 - def test_get_brief_bib_no_oclcNumber_passed(self, stub_session): + def test_bib_get_no_oclcNumber_passed(self, stub_session): with pytest.raises(TypeError): - stub_session.get_brief_bib() + stub_session.bib_get() - def test_get_brief_bib_None_oclcNumber_passed(self, stub_session): - with pytest.raises(WorldcatSessionError): - stub_session.get_brief_bib(oclcNumber=None) + def test_bib_get_None_oclcNumber_passed(self, stub_session): + with pytest.raises(InvalidOclcNumber): + stub_session.bib_get(oclcNumber=None) @pytest.mark.http_code(200) - def test_get_brief_bib_with_stale_token( - self, mock_utcnow, stub_session, mock_session_response - ): - stub_session.authorization.token_expires_at = datetime.datetime.strftime( - datetime.datetime.utcnow() - datetime.timedelta(0, 1), - "%Y-%m-%d %H:%M:%SZ", - ) - assert stub_session.authorization.is_expired() is True - response = stub_session.get_brief_bib(oclcNumber=12345) - assert stub_session.authorization.token_expires_at == "2020-01-01 17:19:58Z" - assert stub_session.authorization.is_expired() is False - assert response.status_code == 200 + def test_bib_get_classification(self, stub_session, mock_session_response): + assert stub_session.bib_get_classification(12345).status_code == 200 - @pytest.mark.http_code(206) - def test_get_brief_bib_odd_206_http_code(self, stub_session, mock_session_response): - with does_not_raise(): - response = stub_session.get_brief_bib(12345) - assert response.status_code == 206 + def test_bib_get_classification_no_oclcNumber_passed(self, stub_session): + with pytest.raises(TypeError): + stub_session.bib_get_classification() - @pytest.mark.http_code(404) - def test_get_brief_bib_404_error_response( + @pytest.mark.http_code(207) + def test_bib_get_current_oclc_number(self, stub_session, mock_session_response): + assert ( + stub_session.bib_get_current_oclc_number( + oclcNumbers=["12345", "65891"] + ).status_code + == 207 + ) + + @pytest.mark.http_code(207) + def test_bib_get_current_oclc_number_passed_as_str( self, stub_session, mock_session_response ): - with pytest.raises(WorldcatRequestError) as exc: - stub_session.get_brief_bib(12345) - assert ( - "404 Client Error: 'foo' for url: https://foo.bar?query. Server response: b'spam'" - in (str(exc.value)) + stub_session.bib_get_current_oclc_number( + oclcNumbers="12345,65891" + ).status_code + == 207 ) + @pytest.mark.parametrize("argm", [(None), (""), ([])]) + def test_bib_get_current_oclc_number_missing_numbers(self, stub_session, argm): + err_msg = "Argument 'oclcNumbers' must be a list or comma separated string of valid OCLC #s." + with pytest.raises(InvalidOclcNumber) as exc: + stub_session.bib_get_current_oclc_number(argm) + assert err_msg in str(exc.value) + @pytest.mark.http_code(200) - def test_get_full_bib(self, stub_session, mock_session_response): - assert stub_session.get_full_bib(12345).status_code == 200 + def test_bib_match(self, stub_session, mock_session_response, stub_marc_xml): + assert ( + stub_session.bib_match( + stub_marc_xml, recordFormat="application/marcxml+xml" + ).status_code + == 200 + ) - def test_get_full_bib_no_oclcNumber_passed(self, stub_session): - with pytest.raises(TypeError): - stub_session.get_full_bib() + @pytest.mark.http_code(200) + def test_bib_replace(self, stub_session, mock_session_response, stub_marc_xml): + assert ( + stub_session.bib_replace( + "12345", stub_marc_xml, recordFormat="application/marcxml+xml" + ).status_code + == 200 + ) - def test_get_full_bib_None_oclcNumber_passed(self, stub_session): - with pytest.raises(WorldcatSessionError): - stub_session.get_full_bib(oclcNumber=None) + @pytest.mark.http_code(200) + def test_bib_validate(self, stub_session, mock_session_response, stub_marc_xml): + assert ( + stub_session.bib_validate( + stub_marc_xml, + recordFormat="application/marcxml+xml", + validationLevel="validateFull", + ).status_code + == 200 + ) @pytest.mark.http_code(200) - def test_get_full_bib_with_stale_token(self, stub_session, mock_session_response): - stub_session.authorization.token_expires_at = datetime.datetime.strftime( - datetime.datetime.utcnow() - datetime.timedelta(0, 1), - "%Y-%m-%d %H:%M:%SZ", + def test_bib_validate_default( + self, stub_session, mock_session_response, stub_marc_xml + ): + assert ( + stub_session.bib_validate( + stub_marc_xml, recordFormat="application/marcxml+xml" + ).status_code + == 200 ) - assert stub_session.authorization.is_expired() is True - response = stub_session.get_full_bib(12345) - assert stub_session.authorization.is_expired() is False - assert stub_session.authorization.token_expires_at == "2020-01-01 17:19:58Z" - assert response.status_code == 200 @pytest.mark.http_code(200) - def test_holding_get_status(self, stub_session, mock_session_response): - assert stub_session.holding_get_status(12345).status_code == 200 + def test_bib_validate_error( + self, stub_session, mock_session_response, stub_marc_xml + ): + with pytest.raises(ValueError) as exc: + stub_session.bib_validate( + stub_marc_xml, + recordFormat="application/marcxml+xml", + validationLevel="validateFoo", + ) + assert "Invalid argument 'validationLevel'." in str(exc.value) - def test_holding_get_status_no_oclcNumber_passed(self, stub_session): + @pytest.mark.http_code(200) + def test_brief_bibs_get(self, stub_session, mock_session_response): + assert stub_session.brief_bibs_get(12345).status_code == 200 + + def test_brief_bibs_get_no_oclcNumber_passed(self, stub_session): with pytest.raises(TypeError): - stub_session.holding_get_status() + stub_session.brief_bibs_get() - def test_holding_get_status_None_oclcNumber_passed(self, stub_session): - with pytest.raises(WorldcatSessionError): - stub_session.holding_get_status(oclcNumber=None) + def test_brief_bibs_get_None_oclcNumber_passed(self, stub_session): + with pytest.raises(InvalidOclcNumber): + stub_session.brief_bibs_get(oclcNumber=None) - @pytest.mark.http_code(200) - def test_holding_get_status_with_stale_token( + @pytest.mark.http_code(206) + def test_brief_bibs_get_odd_206_http_code( + self, stub_session, mock_session_response + ): + with does_not_raise(): + response = stub_session.brief_bibs_get(12345) + assert response.status_code == 206 + + @pytest.mark.http_code(404) + def test_brief_bibs_get_404_error_response( self, stub_session, mock_session_response ): - stub_session.authorization.token_expires_at = datetime.datetime.strftime( - datetime.datetime.utcnow() - datetime.timedelta(0, 1), - "%Y-%m-%d %H:%M:%SZ", + with pytest.raises(WorldcatRequestError) as exc: + stub_session.brief_bibs_get(12345) + + assert ( + "404 Client Error: 'foo' for url: https://foo.bar?query. Server response: spam" + in (str(exc.value)) ) - assert stub_session.authorization.is_expired() is True - response = stub_session.holding_get_status(12345) - assert stub_session.authorization.is_expired() is False - assert stub_session.authorization.token_expires_at == "2020-01-01 17:19:58Z" - assert response.status_code == 200 - @pytest.mark.http_code(201) - def test_holding_set(self, stub_session, mock_session_response): - assert stub_session.holding_set(850940548).status_code == 201 + @pytest.mark.http_code(200) + def test_brief_bibs_search(self, stub_session, mock_session_response): + assert stub_session.brief_bibs_search(q="ti:Zendegi").status_code == 200 - def test_holding_set_no_oclcNumber_passed(self, stub_session): - with pytest.raises(TypeError): - stub_session.holding_set() + @pytest.mark.http_code(200) + def test_brief_bibs_get_other_editions(self, stub_session, mock_session_response): + assert stub_session.brief_bibs_get_other_editions(12345).status_code == 200 - def test_holding_set_None_oclcNumber_passed(self, stub_session): - with pytest.raises(WorldcatSessionError): - stub_session.holding_set(oclcNumber=None) + def test_brief_bibs_get_other_editions_invalid_oclc_number(self, stub_session): + msg = "Argument 'oclcNumber' does not look like real OCLC #." + with pytest.raises(InvalidOclcNumber) as exc: + stub_session.brief_bibs_get_other_editions("odn12345") + assert msg in str(exc.value) - @pytest.mark.http_code(201) - def test_holding_set_stale_token(self, stub_session, mock_session_response): - stub_session.authorization.token_expires_at = datetime.datetime.strftime( - datetime.datetime.utcnow() - datetime.timedelta(0, 1), - "%Y-%m-%d %H:%M:%SZ", - ) - assert stub_session.authorization.is_expired() is True - response = stub_session.holding_set(850940548) - assert stub_session.authorization.token_expires_at == "2020-01-01 17:19:58Z" - assert stub_session.authorization.is_expired() is False - assert response.status_code == 201 + @pytest.mark.http_code(200) + def test_holdings_get_codes(self, stub_session, mock_session_response): + assert stub_session.holdings_get_codes().status_code == 200 @pytest.mark.http_code(200) - def test_holding_unset(self, stub_session, mock_session_response): - assert stub_session.holding_unset(850940548).status_code == 200 + def test_holdings_get_current(self, stub_session, mock_session_response): + assert stub_session.holdings_get_current("12345").status_code == 200 - def test_holding_unset_no_oclcNumber_passed(self, stub_session): + def test_holdings_get_current_no_oclcNumber_passed(self, stub_session): with pytest.raises(TypeError): - stub_session.holding_unset() + stub_session.holdings_get_current() - def test_holding_unset_None_oclcNumber_passed(self, stub_session): - with pytest.raises(WorldcatSessionError): - stub_session.holding_unset(oclcNumber=None) + def test_holdings_get_current_None_oclcNumber_passed(self, stub_session): + with pytest.raises(InvalidOclcNumber): + stub_session.holdings_get_current(oclcNumbers=None) - @pytest.mark.http_code(200) - def test_holding_unset_stale_token( - self, mock_utcnow, stub_session, mock_session_response - ): - stub_session.authorization.token_expires_at = datetime.datetime.strftime( - datetime.datetime.utcnow() - datetime.timedelta(0, 1), - "%Y-%m-%d %H:%M:%SZ", + def test_holdings_get_current_too_many_oclcNumbers_passed(self, stub_session): + with pytest.raises(ValueError) as exc: + stub_session.holdings_get_current( + oclcNumbers=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] + ) + assert "Too many OCLC Numbers passed to 'oclcNumbers' argument." in str( + exc.value ) - assert stub_session.authorization.is_expired() is True - response = stub_session.holding_unset(850940548) - assert stub_session.authorization.token_expires_at == "2020-01-01 17:19:58Z" - assert stub_session.authorization.is_expired() is False - assert response.status_code == 200 - @pytest.mark.parametrize( - "argm,expectation", - [ - (None, pytest.raises(WorldcatSessionError)), - ([], pytest.raises(WorldcatSessionError)), - (["bt2111111111"], pytest.raises(WorldcatSessionError)), - (["850940548"], does_not_raise()), - (["ocn850940548"], does_not_raise()), - ("850940548,850940552, 850940554", does_not_raise()), - (["850940548", "850940552", "850940554"], does_not_raise()), - ([850940548, 850940552, 850940554], does_not_raise()), - ], - ) - @pytest.mark.http_code(207) - def test_holdings_set(self, argm, expectation, stub_session, mock_session_response): - with expectation: - stub_session.holdings_set(argm) + @pytest.mark.http_code(201) + def test_holdings_set(self, stub_session, mock_session_response): + assert stub_session.holdings_set(850940548).status_code == 201 def test_holdings_set_no_oclcNumber_passed(self, stub_session): with pytest.raises(TypeError): stub_session.holdings_set() - @pytest.mark.http_code(207) - def test_holdings_set_stale_token( - self, mock_utcnow, stub_session, mock_session_response - ): - stub_session.authorization.token_expires_at = datetime.datetime.strftime( - datetime.datetime.utcnow() - datetime.timedelta(0, 1), - "%Y-%m-%d %H:%M:%SZ", - ) - with does_not_raise(): - assert stub_session.authorization.is_expired() is True - stub_session.holdings_set([850940548, 850940552, 850940554]) - assert stub_session.authorization.token_expires_at == "2020-01-01 17:19:58Z" - assert stub_session.authorization.is_expired() is False + def test_holdings_set_None_oclcNumber_passed(self, stub_session): + with pytest.raises(InvalidOclcNumber): + stub_session.holdings_set(oclcNumber=None) - @pytest.mark.parametrize( - "argm,expectation", - [ - (None, pytest.raises(WorldcatSessionError)), - ([], pytest.raises(WorldcatSessionError)), - (["bt2111111111"], pytest.raises(WorldcatSessionError)), - (["850940548"], does_not_raise()), - (["ocn850940548"], does_not_raise()), - ("850940548,850940552, 850940554", does_not_raise()), - (["850940548", "850940552", "850940554"], does_not_raise()), - ([850940548, 850940552, 850940554], does_not_raise()), - ], - ) - @pytest.mark.http_code(207) - def test_holdings_unset( - self, argm, expectation, stub_session, mock_session_response - ): - with expectation: - stub_session.holdings_unset(argm) + @pytest.mark.http_code(200) + def test_holdings_unset(self, stub_session, mock_session_response): + assert stub_session.holdings_unset(850940548).status_code == 200 def test_holdings_unset_no_oclcNumber_passed(self, stub_session): with pytest.raises(TypeError): stub_session.holdings_unset() - @pytest.mark.http_code(207) - def test_holdings_unset_stale_token( - self, mock_utcnow, stub_session, mock_session_response - ): - stub_session.authorization.token_expires_at = datetime.datetime.strftime( - datetime.datetime.utcnow() - datetime.timedelta(0, 1), - "%Y-%m-%d %H:%M:%SZ", - ) - with does_not_raise(): - assert stub_session.authorization.is_expired() is True - stub_session.holdings_unset([850940548, 850940552, 850940554]) - assert stub_session.authorization.token_expires_at == "2020-01-01 17:19:58Z" - assert stub_session.authorization.is_expired() is False + def test_holdings_unset_None_oclcNumber_passed(self, stub_session): + with pytest.raises(InvalidOclcNumber): + stub_session.holdings_unset(oclcNumber=None) @pytest.mark.http_code(200) - def test_holdings_set_multi_institutions(self, stub_session, mock_session_response): - results = stub_session.holdings_set_multi_institutions( - oclcNumber=850940548, instSymbols="BKL,NYP" + def test_holdings_set_with_bib( + self, stub_session, mock_session_response, stub_marc_xml + ): + assert ( + stub_session.holdings_set_with_bib( + stub_marc_xml, recordFormat="application/marcxml+xml" + ).status_code + == 200 ) - assert results.status_code == 200 - - def test_holdings_set_multi_institutions_missing_oclc_number(self, stub_session): - with pytest.raises(TypeError): - stub_session.holdings_set_multi_institutions(instSymbols="NYP,BKL") - - def test_holdings_set_multi_institutions_missing_inst_symbols(self, stub_session): - with pytest.raises(TypeError): - stub_session.holdings_set_multi_institutions(oclcNumber=123) - - def test_holdings_set_multi_institutions_invalid_oclc_number(self, stub_session): - with pytest.raises(WorldcatSessionError): - stub_session.holdings_set_multi_institutions( - oclcNumber="odn1234", instSymbols="NYP,BKL" - ) @pytest.mark.http_code(200) - def test_holdings_set_multi_institutions_stale_token( - self, mock_utcnow, stub_session, mock_session_response + def test_holdings_unset_with_bib( + self, stub_session, mock_session_response, stub_marc_xml ): - stub_session.authorization.token_expires_at = datetime.datetime.strftime( - datetime.datetime.utcnow() - datetime.timedelta(0, 1), - "%Y-%m-%d %H:%M:%SZ", + assert ( + stub_session.holdings_unset_with_bib( + record=stub_marc_xml, recordFormat="application/marcxml+xml" + ).status_code + == 200 ) - with does_not_raise(): - assert stub_session.authorization.is_expired() is True - stub_session.holdings_set_multi_institutions( - oclcNumber=850940548, instSymbols="NYP,BKL" - ) - assert stub_session.authorization.token_expires_at == "2020-01-01 17:19:58Z" - assert stub_session.authorization.is_expired() is False - - @pytest.mark.http_code(403) - def test_holdings_set_multi_institutions_permission_error( - self, stub_session, mock_session_response - ): - with pytest.raises(WorldcatRequestError) as exc: - stub_session.holdings_set_multi_institutions( - oclcNumber=850940548, instSymbols="NYP,BKL" - ) + @pytest.mark.http_code(200) + def test_lbd_create(self, stub_session, mock_session_response, stub_marc_xml): assert ( - "403 Client Error: 'foo' for url: https://foo.bar?query. Server response: b'spam'" - in str(exc.value) + stub_session.lbd_create( + stub_marc_xml, recordFormat="application/marcxml+xml" + ).status_code + == 200 ) @pytest.mark.http_code(200) - def test_holdings_unset_multi_institutions( - self, stub_session, mock_session_response - ): - results = stub_session.holdings_unset_multi_institutions( - 850940548, "BKL,NYP", cascade="1" - ) - assert results.status_code == 200 + def test_lbd_delete(self, stub_session, mock_session_response): + assert stub_session.lbd_delete("12345").status_code == 200 - def test_holdings_unset_multi_institutions_missing_oclc_number(self, stub_session): - with pytest.raises(TypeError): - stub_session.holdings_unset_multi_institutions(instSymbols="NYP,BKL") + @pytest.mark.http_code(200) + def test_lbd_get(self, stub_session, mock_session_response): + assert stub_session.lbd_get(12345).status_code == 200 - def test_holdings_unset_multi_institutions_missing_inst_symbols(self, stub_session): + def test_lbd_get_no_controlNumber_passed(self, stub_session): with pytest.raises(TypeError): - stub_session.holdings_unset_multi_institutions(oclcNumber=123) - - def test_holdings_unset_multi_institutions_invalid_oclc_number(self, stub_session): - with pytest.raises(WorldcatSessionError): - stub_session.holdings_unset_multi_institutions( - oclcNumber="odn1234", instSymbols="NYP,BKL" - ) + stub_session.lbd_get() @pytest.mark.http_code(200) - def test_holdings_unset_multi_institutions_stale_token( - self, mock_utcnow, stub_session, mock_session_response - ): - stub_session.authorization.token_expires_at = datetime.datetime.strftime( - datetime.datetime.utcnow() - datetime.timedelta(0, 1), - "%Y-%m-%d %H:%M:%SZ", + def test_lbd_replace(self, stub_session, mock_session_response, stub_marc_xml): + assert ( + stub_session.lbd_replace( + "12345", stub_marc_xml, recordFormat="application/marcxml+xml" + ).status_code + == 200 ) - with does_not_raise(): - assert stub_session.authorization.is_expired() is True - stub_session.holdings_unset_multi_institutions( - oclcNumber=850940548, instSymbols="NYP,BKL" - ) - assert stub_session.authorization.token_expires_at == "2020-01-01 17:19:58Z" - assert stub_session.authorization.is_expired() is False @pytest.mark.http_code(200) - def test_search_brief_bibs_other_editions( - self, stub_session, mock_session_response - ): - assert stub_session.search_brief_bib_other_editions(12345).status_code == 200 + def test_lhr_create(self, stub_session, mock_session_response, stub_holding_xml): + assert ( + stub_session.lhr_create( + stub_holding_xml, recordFormat="application/marcxml+xml" + ).status_code + == 200 + ) @pytest.mark.http_code(200) - def test_search_brief_bibs_other_editions_stale_token( - self, mock_utcnow, stub_session, mock_session_response - ): - stub_session.authorization.token_expires_at = datetime.datetime.strftime( - datetime.datetime.utcnow() - datetime.timedelta(0, 1), - "%Y-%m-%d %H:%M:%SZ", - ) - assert stub_session.authorization.is_expired() is True - response = stub_session.search_brief_bib_other_editions(12345) - assert stub_session.authorization.token_expires_at == "2020-01-01 17:19:58Z" - assert stub_session.authorization.is_expired() is False - assert response.status_code == 200 - - def test_search_brief_bibs_other_editions_invalid_oclc_number(self, stub_session): - msg = "Invalid OCLC # was passed as an argument" - with pytest.raises(WorldcatSessionError) as exc: - stub_session.search_brief_bib_other_editions("odn12345") - assert msg in str(exc.value) + def test_lhr_delete(self, stub_session, mock_session_response): + assert stub_session.lhr_delete("12345").status_code == 200 @pytest.mark.http_code(200) - def test_seach_brief_bibs(self, stub_session, mock_session_response): - assert stub_session.search_brief_bibs(q="ti:Zendegi").status_code == 200 + def test_lhr_get(self, stub_session, mock_session_response): + assert stub_session.lhr_get(12345).status_code == 200 - @pytest.mark.parametrize("argm", [(None), ("")]) - def test_search_brief_bibs_missing_query(self, stub_session, argm): - with pytest.raises(WorldcatSessionError) as exc: - stub_session.search_brief_bibs(argm) - assert "Argument 'q' is requried to construct query." in str(exc.value) + def test_lhr_get_no_controlNumber_passed(self, stub_session): + with pytest.raises(TypeError): + stub_session.lhr_get() @pytest.mark.http_code(200) - def test_search_brief_bibs_with_stale_token( - self, mock_utcnow, stub_session, mock_session_response - ): - stub_session.authorization.token_expires_at = datetime.datetime.strftime( - datetime.datetime.utcnow() - datetime.timedelta(0, 1), - "%Y-%m-%d %H:%M:%SZ", + def test_lhr_replace(self, stub_session, mock_session_response, stub_holding_xml): + assert ( + stub_session.lhr_replace( + "12345", stub_holding_xml, recordFormat="application/marcxml+xml" + ).status_code + == 200 ) - assert stub_session.authorization.is_expired() is True - response = stub_session.search_brief_bibs(q="ti:foo") - assert stub_session.authorization.token_expires_at == "2020-01-01 17:19:58Z" - assert stub_session.authorization.is_expired() is False - assert response.status_code == 200 - @pytest.mark.http_code(207) - def test_seach_current_control_numbers(self, stub_session, mock_session_response): + @pytest.mark.http_code(200) + def test_local_bibs_get(self, stub_session, mock_session_response): + assert stub_session.local_bibs_get(12345).status_code == 200 + + def test_local_bibs_get_no_controlNumber_passed(self, stub_session): + with pytest.raises(TypeError): + stub_session.local_bibs_get() + + @pytest.mark.http_code(200) + def test_local_bibs_search(self, stub_session, mock_session_response): + assert stub_session.local_bibs_search(q="ti:foo").status_code == 200 + + @pytest.mark.http_code(200) + def test_local_holdings_browse(self, stub_session, mock_session_response): assert ( - stub_session.search_current_control_numbers( - oclcNumbers=["12345", "65891"] + stub_session.local_holdings_browse( + callNumber="12345", holdingLocation="foo", shelvingLocation="bar" ).status_code - == 207 + == 200 ) - @pytest.mark.http_code(207) - def test_seach_current_control_numbers_passed_as_str( + @pytest.mark.http_code(200) + def test_local_holdings_browse_oclc_number( self, stub_session, mock_session_response ): assert ( - stub_session.search_current_control_numbers( - oclcNumbers="12345,65891" + stub_session.local_holdings_browse( + callNumber="12345", + oclcNumber="54321", + holdingLocation="foo", + shelvingLocation="bar", ).status_code - == 207 + == 200 ) - @pytest.mark.parametrize("argm", [(None), (""), ([])]) - def test_search_current_control_numbers_missing_numbers(self, stub_session, argm): - err_msg = "Argument 'oclcNumbers' must be a list or comma separated string of valid OCLC #." - with pytest.raises(WorldcatSessionError) as exc: - stub_session.search_current_control_numbers(argm) - assert err_msg in str(exc.value) + @pytest.mark.http_code(200) + def test_local_holdings_get(self, stub_session, mock_session_response): + assert stub_session.local_holdings_get(12345).status_code == 200 - @pytest.mark.http_code(207) - def test_search_current_control_numbers_with_stale_token( - self, mock_utcnow, stub_session, mock_session_response - ): - stub_session.authorization.token_expires_at = datetime.datetime.strftime( - datetime.datetime.utcnow() - datetime.timedelta(0, 1), - "%Y-%m-%d %H:%M:%SZ", - ) - assert stub_session.authorization.is_expired() is True - response = stub_session.search_current_control_numbers(["12345", "65891"]) - assert stub_session.authorization.token_expires_at == "2020-01-01 17:19:58Z" - assert stub_session.authorization.is_expired() is False - assert response.status_code == 207 + def test_local_holdings_get_no_controlNumber_passed(self, stub_session): + with pytest.raises(TypeError): + stub_session.local_holdings_get() @pytest.mark.http_code(200) - def test_search_general_holdings(self, stub_session, mock_session_response): - assert stub_session.search_general_holdings(oclcNumber=12345).status_code == 200 + def test_local_holdings_search(self, stub_session, mock_session_response): + assert stub_session.local_holdings_search(oclcNumber=12345).status_code == 200 - def test_search_general_holdings_missing_arguments(self, stub_session): - msg = "Missing required argument. One of the following args are required: oclcNumber, issn, isbn" - with pytest.raises(WorldcatSessionError) as exc: - stub_session.search_general_holdings(holdingsAllEditions=True, limit=20) - assert msg in str(exc.value) + def test_local_holdings_search_no_oclcNumber_passed( + self, stub_session, mock_session_response + ): + assert stub_session.local_holdings_search(barcode=12345).status_code == 200 - def test_search_general_holdings_invalid_oclc_number(self, stub_session): - msg = "Invalid OCLC # was passed as an argument" - with pytest.raises(WorldcatSessionError) as exc: - stub_session.search_general_holdings(oclcNumber="odn12345") + def test_local_holdings_search_invalid_oclc_number(self, stub_session): + msg = "Argument 'oclcNumber' does not look like real OCLC #." + with pytest.raises(InvalidOclcNumber) as exc: + stub_session.local_holdings_search(oclcNumber="odn12345") assert msg in str(exc.value) @pytest.mark.http_code(200) - def test_search_general_holdings_with_stale_token( - self, mock_utcnow, stub_session, mock_session_response + def test_local_holdings_search_shared_print( + self, stub_session, mock_session_response ): - stub_session.authorization.token_expires_at = datetime.datetime.strftime( - datetime.datetime.utcnow() - datetime.timedelta(0, 1), - "%Y-%m-%d %H:%M:%SZ", + assert ( + stub_session.local_holdings_search_shared_print( + oclcNumber=12345 + ).status_code + == 200 ) - assert stub_session.authorization.is_expired() is True - response = stub_session.search_general_holdings(oclcNumber=12345) - assert stub_session.authorization.token_expires_at == "2020-01-01 17:19:58Z" - assert stub_session.authorization.is_expired() is False - assert response.status_code == 200 @pytest.mark.http_code(200) - def test_search_shared_print_holdings(self, stub_session, mock_session_response): + def test_local_holdings_search_shared_print_no_oclcNumber_passed( + self, stub_session, mock_session_response + ): assert ( - stub_session.search_shared_print_holdings(oclcNumber=12345).status_code + stub_session.local_holdings_search_shared_print(barcode=12345).status_code == 200 ) - def test_search_shared_print_holdings_missing_arguments(self, stub_session): - msg = "Missing required argument. One of the following args are required: oclcNumber, issn, isbn" - with pytest.raises(WorldcatSessionError) as exc: - stub_session.search_shared_print_holdings(heldInState="NY", limit=20) - assert msg in str(exc.value) - - def test_search_shared_print_holdings_with_invalid_oclc_number_passsed( + def test_local_holdings_search_shared_print_with_invalid_oclc_number_passed( self, stub_session ): - msg = "Invalid OCLC # was passed as an argument" - with pytest.raises(WorldcatSessionError) as exc: - stub_session.search_shared_print_holdings(oclcNumber="odn12345") + msg = "Argument 'oclcNumber' does not look like real OCLC #." + with pytest.raises(InvalidOclcNumber) as exc: + stub_session.local_holdings_search_shared_print(oclcNumber="odn12345") assert msg in str(exc.value) @pytest.mark.http_code(200) - def test_search_shared_print_holdings_with_stale_token( - self, mock_utcnow, stub_session, mock_session_response - ): - stub_session.authorization.token_expires_at = datetime.datetime.strftime( - datetime.datetime.utcnow() - datetime.timedelta(0, 1), - "%Y-%m-%d %H:%M:%SZ", + def test_summary_holdings_get(self, stub_session, mock_session_response): + assert stub_session.summary_holdings_get(oclcNumber=12345).status_code == 200 + + def test_summary_holdings_get_no_oclcNumber_passed(self, stub_session): + with pytest.raises(TypeError): + stub_session.summary_holdings_get(holdingsAllVariantRecords=True) + + @pytest.mark.http_code(200) + def test_summary_holdings_search(self, stub_session, mock_session_response): + assert stub_session.summary_holdings_search(oclcNumber=12345).status_code == 200 + + def test_summary_holdings_search_invalid_oclc_number(self, stub_session): + msg = "Argument 'oclcNumber' does not look like real OCLC #." + with pytest.raises(InvalidOclcNumber) as exc: + stub_session.summary_holdings_search(oclcNumber="odn12345") + assert msg in str(exc.value) + + @pytest.mark.http_code(200) + def test_shared_print_holdings_search(self, stub_session, mock_session_response): + assert ( + stub_session.shared_print_holdings_search(oclcNumber=12345).status_code + == 200 ) - assert stub_session.authorization.is_expired() is True - response = stub_session.search_shared_print_holdings(oclcNumber=12345) - assert stub_session.authorization.token_expires_at == "2020-01-01 17:19:58Z" - assert stub_session.authorization.is_expired() is False - assert response.status_code == 200 + + def test_shared_print_holdings_search_with_invalid_oclc_number_passed( + self, stub_session + ): + msg = "Argument 'oclcNumber' does not look like real OCLC #." + with pytest.raises(InvalidOclcNumber) as exc: + stub_session.shared_print_holdings_search(oclcNumber="odn12345") + assert msg in str(exc.value) @pytest.mark.webtest class TestLiveMetadataSession: """Runs rudimentary tests against live Metadata API""" - def test_get_brief_bib_print_mat_request(self, live_keys): + def test_bib_get(self, live_keys): + token = WorldcatAccessToken( + key=os.getenv("WCKey"), + secret=os.getenv("WCSecret"), + scopes=os.getenv("WCScopes"), + ) + + with MetadataSession(authorization=token) as session: + response = session.bib_get(41266045) + + assert ( + response.url + == "https://metadata.api.oclc.org/worldcat/manage/bibs/41266045" + ) + assert response.status_code == 200 + + def test_bib_get_classification(self, live_keys): + token = WorldcatAccessToken( + key=os.getenv("WCKey"), + secret=os.getenv("WCSecret"), + scopes=os.getenv("WCScopes"), + ) + + with MetadataSession(authorization=token) as session: + response = session.bib_get_classification(41266045) + + assert ( + response.url + == "https://metadata.api.oclc.org/worldcat/search/classification-bibs/41266045" + ) + assert response.status_code == 200 + assert sorted(response.json().keys()) == [ + "dewey", + "lc", + ] + + def test_bib_get_current_oclc_number(self, live_keys): + token = WorldcatAccessToken( + key=os.getenv("WCKey"), + secret=os.getenv("WCSecret"), + scopes=os.getenv("WCScopes"), + ) + + with MetadataSession(authorization=token) as session: + response = session.bib_get_current_oclc_number([41266045, 519740398]) + + assert response.status_code == 200 + assert ( + response.request.url + == "https://metadata.api.oclc.org/worldcat/manage/bibs/current?oclcNumbers=41266045%2C519740398" + ) + jres = response.json() + assert sorted(jres.keys()) == ["controlNumbers"] + assert sorted(jres["controlNumbers"][0].keys()) == ["current", "requested"] + + def test_bib_get_current_oclc_number_str(self, live_keys): + token = WorldcatAccessToken( + key=os.getenv("WCKey"), + secret=os.getenv("WCSecret"), + scopes=os.getenv("WCScopes"), + ) + + with MetadataSession(authorization=token) as session: + response = session.bib_get_current_oclc_number("41266045") + + assert response.status_code == 200 + assert ( + response.request.url + == "https://metadata.api.oclc.org/worldcat/manage/bibs/current?oclcNumbers=41266045" + ) + jres = response.json() + assert sorted(jres.keys()) == ["controlNumbers"] + assert sorted(jres["controlNumbers"][0].keys()) == ["current", "requested"] + + def test_bib_match_marcxml(self, live_keys, stub_marc_xml): + token = WorldcatAccessToken( + key=os.getenv("WCKey"), + secret=os.getenv("WCSecret"), + scopes=os.getenv("WCScopes"), + ) + + with MetadataSession(authorization=token) as session: + response = session.bib_match( + stub_marc_xml, recordFormat="application/marcxml+xml" + ) + assert response.status_code == 200 + assert sorted(response.json().keys()) == sorted( + ["numberOfRecords", "briefRecords"] + ) + + def test_bib_validate(self, live_keys, stub_marc21): + token = WorldcatAccessToken( + key=os.getenv("WCKey"), + secret=os.getenv("WCSecret"), + scopes=os.getenv("WCScopes"), + ) + + with MetadataSession(authorization=token) as session: + response = session.bib_validate( + stub_marc21, recordFormat="application/marc" + ) + assert response.status_code == 200 + assert ( + response.url + == "https://metadata.api.oclc.org/worldcat/manage/bibs/validate/validateFull" + ) + assert sorted(response.json().keys()) == sorted(["httpStatus", "status"]) + + def test_brief_bibs_get(self, live_keys): fields = sorted( [ "catalogingInfo", @@ -679,310 +800,243 @@ def test_get_brief_bib_print_mat_request(self, live_keys): key=os.getenv("WCKey"), secret=os.getenv("WCSecret"), scopes=os.getenv("WCScopes"), - principal_id=os.getenv("WCPrincipalID"), - principal_idns=os.getenv("WCPrincipalIDNS"), ) with MetadataSession(authorization=token) as session: - response = session.get_brief_bib(41266045) + response = session.brief_bibs_get(41266045) assert response.status_code == 200 assert sorted(response.json().keys()) == fields - def test_get_brief_bib_401_error(self, live_keys): + def test_brief_bibs_get_401_error(self, live_keys): token = WorldcatAccessToken( key=os.getenv("WCKey"), secret=os.getenv("WCSecret"), scopes=os.getenv("WCScopes"), - principal_id=os.getenv("WCPrincipalID"), - principal_idns=os.getenv("WCPrincipalIDNS"), ) token.token_str = "invalid-token" - err_msg = "401 Client Error: Unauthorized for url: https://americas.metadata.api.oclc.org/worldcat/search/v1/brief-bibs/41266045" + err_msg = "401 Client Error: Unauthorized for url: https://metadata.api.oclc.org/worldcat/search/brief-bibs/41266045" with MetadataSession(authorization=token) as session: - session.headers.update({"Authorization": f"Bearer invalid-token"}) + session.headers.update({"Authorization": "Bearer invalid-token"}) with pytest.raises(WorldcatRequestError) as exc: - session.get_brief_bib(41266045) + session.brief_bibs_get(41266045) - assert err_msg == str(exc.value) + assert err_msg in str(exc.value) - def test_get_brief_bib_with_stale_token(self, live_keys): + def test_brief_bibs_search(self, live_keys): + fields = sorted(["briefRecords", "numberOfRecords"]) token = WorldcatAccessToken( key=os.getenv("WCKey"), secret=os.getenv("WCSecret"), scopes=os.getenv("WCScopes"), - principal_id=os.getenv("WCPrincipalID"), - principal_idns=os.getenv("WCPrincipalIDNS"), ) + with MetadataSession(authorization=token) as session: - session.authorization.is_expired() is False - session.authorization.token_expires_at = datetime.datetime.strftime( - datetime.datetime.utcnow() - datetime.timedelta(0, 1), - "%Y-%m-%d %H:%M:%SZ", + response = session.brief_bibs_search( + "ti:zendegi AND au:egan", + inLanguage="eng", + inCatalogLanguage="eng", + itemType="book", + itemSubType=["book-printbook", "book-digital"], + catalogSource="dlc", + orderBy="mostWidelyHeld", + limit=5, ) - assert session.authorization.is_expired() is True - response = session.get_brief_bib(oclcNumber=41266045) - assert session.authorization.is_expired() is False assert response.status_code == 200 + assert sorted(response.json().keys()) == fields + assert ( + response.request.url + == "https://metadata.api.oclc.org/worldcat/search/brief-bibs?q=ti%3Azendegi+AND+au%3Aegan&inLanguage=eng&inCatalogLanguage=eng&catalogSource=dlc&itemType=book&itemSubType=book-printbook&itemSubType=book-digital&retentionCommitments=False&groupRelatedEditions=False&groupVariantRecords=False&preferredLanguage=eng&showHoldingsIndicators=False&unit=M&orderBy=mostWidelyHeld&offset=1&limit=5" + ) - def test_get_full_bib(self, live_keys): + def test_brief_bibs_get_other_editions(self, live_keys): + fields = sorted(["briefRecords", "numberOfRecords"]) token = WorldcatAccessToken( key=os.getenv("WCKey"), secret=os.getenv("WCSecret"), scopes=os.getenv("WCScopes"), - principal_id=os.getenv("WCPrincipalID"), - principal_idns=os.getenv("WCPrincipalIDNS"), ) with MetadataSession(authorization=token) as session: - response = session.get_full_bib(41266045) + response = session.brief_bibs_get_other_editions(41266045) - assert response.url == "https://worldcat.org/bib/data/41266045" assert response.status_code == 200 + assert sorted(response.json().keys()) == fields - def test_holding_get_status(self, live_keys): + def test_holdings_get_current(self, live_keys): token = WorldcatAccessToken( key=os.getenv("WCKey"), secret=os.getenv("WCSecret"), scopes=os.getenv("WCScopes"), - principal_id=os.getenv("WCPrincipalID"), - principal_idns=os.getenv("WCPrincipalIDNS"), ) with MetadataSession(authorization=token) as session: - response = session.holding_get_status(982651100) + response = session.holdings_get_current("982651100") assert ( response.url - == "https://worldcat.org/ih/checkholdings?oclcNumber=982651100" + == "https://metadata.api.oclc.org/worldcat/manage/institution/holdings/current?oclcNumbers=982651100" ) assert response.status_code == 200 - assert sorted(response.json().keys()) == ["content", "title", "updated"] - assert sorted(response.json()["content"].keys()) == sorted( + assert sorted(response.json().keys()) == ["holdings"] + assert sorted(response.json()["holdings"][0].keys()) == sorted( [ - "requestedOclcNumber", - "currentOclcNumber", - "institution", - "holdingCurrentlySet", - "id", + "requestedControlNumber", + "currentControlNumber", + "institutionSymbol", + "holdingSet", ] ) @pytest.mark.holdings - def test_holding_set_unset(self, live_keys): + def test_holdings_set_unset(self, live_keys): token = WorldcatAccessToken( key=os.getenv("WCKey"), secret=os.getenv("WCSecret"), scopes=os.getenv("WCScopes"), - principal_id=os.getenv("WCPrincipalID"), - principal_idns=os.getenv("WCPrincipalIDNS"), ) with MetadataSession(authorization=token) as session: - response = session.holding_get_status(850940548) - holdings = response.json()["content"]["holdingCurrentlySet"] + response = session.holdings_get_current("850940548") + holdings = response.json()["holdings"] # make sure no holdings are set initially - if holdings is True: - response = session.holding_unset(850940548) + if len(holdings) > 0: + response = session.holdings_unset(850940548) - response = session.holding_set( - 850940548, response_format="application/atom+json" + response = session.holdings_set(850940548) + assert ( + response.url + == "https://metadata.api.oclc.org/worldcat/manage/institution/holdings/850940548/set" ) - assert response.url == "https://worldcat.org/ih/data?oclcNumber=850940548" - assert response.status_code == 201 - assert response.text == "" - - # test setting holdings on bib with already existing holding - response = session.holding_set(850940548) - assert response.status_code == 409 - assert response.url == "https://worldcat.org/ih/data?oclcNumber=850940548" - assert response.json() == { - "code": {"value": "WS-409", "type": "application"}, - "message": "Trying to set hold while holding already exists", - "detail": None, - } + assert response.status_code == 200 + assert response.json()["action"] == "Set Holdings" # test deleting holdings - response = session.holding_unset(850940548) + response = session.holdings_unset(850940548) assert response.status_code == 200 assert ( response.request.url - == "https://worldcat.org/ih/data?oclcNumber=850940548&cascade=0" - ) - assert response.text == "" - - # test deleting holdings on bib without any - response = session.holding_unset(850940548) - assert response.status_code == 409 - assert ( - response.request.url - == "https://worldcat.org/ih/data?oclcNumber=850940548&cascade=0" + == "https://metadata.api.oclc.org/worldcat/manage/institution/holdings/850940548/unset" ) - assert response.json() == { - "code": {"value": "WS-409", "type": "application"}, - "message": "Trying to unset hold while holding does not exist", - "detail": None, - } + assert response.json()["action"] == "Unset Holdings" @pytest.mark.holdings - def test_holdings_set(self, live_keys): + def test_holdings_set_unset_marcxml(self, live_keys, stub_marc_xml): token = WorldcatAccessToken( key=os.getenv("WCKey"), secret=os.getenv("WCSecret"), scopes=os.getenv("WCScopes"), - principal_id=os.getenv("WCPrincipalID"), - principal_idns=os.getenv("WCPrincipalIDNS"), ) with MetadataSession(authorization=token) as session: - response = session.holdings_set([850940548, 850940552, 850940554]) - assert type(response) is list - assert response[0].status_code == 207 - assert ( - response[0].url - == "https://worldcat.org/ih/datalist?oclcNumbers=850940548%2C850940552%2C850940554" + response = session.holdings_get_current("850940548") + holdings = response.json()["holdings"] + + # make sure no holdings are set initially + if len(holdings) > 0: + response = session.holdings_unset_with_bib( + stub_marc_xml, recordFormat="application/marcxml+xml" + ) + + response = session.holdings_set_with_bib( + stub_marc_xml, recordFormat="application/marcxml+xml" ) - assert sorted(response[0].json().keys()) == sorted( - ["entries", "extensions"] + assert ( + response.url + == "https://metadata.api.oclc.org/worldcat/manage/institution/holdings/set" ) - assert sorted(response[0].json()["entries"][0]) == sorted( - ["title", "content", "updated"] + assert response.status_code == 200 + assert response.json()["action"] == "Set Holdings" + + response = session.holdings_unset_with_bib( + stub_marc_xml, recordFormat="application/marcxml+xml" ) - assert sorted(response[0].json()["entries"][0]["content"]) == sorted( - [ - "requestedOclcNumber", - "currentOclcNumber", - "institution", - "status", - "detail", - ] + assert response.status_code == 200 + assert ( + response.request.url + == "https://metadata.api.oclc.org/worldcat/manage/institution/holdings/unset" ) + assert response.json()["action"] == "Unset Holdings" - @pytest.mark.holdings - def test_holdings_unset(self, live_keys): + def test_holdings_get_codes(self, live_keys): token = WorldcatAccessToken( key=os.getenv("WCKey"), secret=os.getenv("WCSecret"), scopes=os.getenv("WCScopes"), - principal_id=os.getenv("WCPrincipalID"), - principal_idns=os.getenv("WCPrincipalIDNS"), ) with MetadataSession(authorization=token) as session: - response = session.holdings_unset([850940548, 850940552, 850940554]) - assert type(response) is list - assert response[0].status_code == 207 + response = session.holdings_get_codes() + assert ( - response[0].url - == "https://worldcat.org/ih/datalist?oclcNumbers=850940548%2C850940552%2C850940554&cascade=0" - ) - assert sorted(response[0].json().keys()) == sorted( - ["entries", "extensions"] - ) - assert sorted(response[0].json()["entries"][0]) == sorted( - ["title", "content", "updated"] - ) - assert sorted(response[0].json()["entries"][0]["content"]) == sorted( - [ - "requestedOclcNumber", - "currentOclcNumber", - "institution", - "status", - "detail", - ] + response.url + == "https://metadata.api.oclc.org/worldcat/manage/institution/holding-codes" ) + assert response.status_code == 200 + assert sorted(response.json().keys()) == ["holdingLibraryCodes"] + assert {"code": "Print Collection", "name": "NYPC"} in response.json()[ + "holdingLibraryCodes" + ] - def test_brief_bib_other_editions(self, live_keys): + def test_summary_holdings_search_oclc(self, live_keys): fields = sorted(["briefRecords", "numberOfRecords"]) token = WorldcatAccessToken( key=os.getenv("WCKey"), secret=os.getenv("WCSecret"), scopes=os.getenv("WCScopes"), - principal_id=os.getenv("WCPrincipalID"), - principal_idns=os.getenv("WCPrincipalIDNS"), ) with MetadataSession(authorization=token) as session: - response = session.search_brief_bib_other_editions(41266045) + response = session.summary_holdings_search(oclcNumber="41266045") assert response.status_code == 200 assert sorted(response.json().keys()) == fields - def test_search_brief_bibs(self, live_keys): + def test_summary_holdings_search_isbn(self, live_keys): fields = sorted(["briefRecords", "numberOfRecords"]) token = WorldcatAccessToken( key=os.getenv("WCKey"), secret=os.getenv("WCSecret"), scopes=os.getenv("WCScopes"), - principal_id=os.getenv("WCPrincipalID"), - principal_idns=os.getenv("WCPrincipalIDNS"), ) with MetadataSession(authorization=token) as session: - response = session.search_brief_bibs( - "ti:zendegi AND au:egan", - inLanguage="eng", - inCatalogLanguage="eng", - itemType="book", - # itemSubType="printbook", - catalogSource="dlc", - orderBy="mostWidelyHeld", - limit=5, - ) + response = session.summary_holdings_search(isbn="9781597801744") + assert response.status_code == 200 assert sorted(response.json().keys()) == fields - # removed temp &itemSubType=printbook due to OCLC error/issue - assert ( - response.request.url - == "https://americas.metadata.api.oclc.org/worldcat/search/v1/brief-bibs?q=ti%3Azendegi+AND+au%3Aegan&inLanguage=eng&inCatalogLanguage=eng&catalogSource=dlc&itemType=book&orderBy=mostWidelyHeld&limit=5" - ) - def test_search_general_holdings(self, live_keys): - fields = sorted(["briefRecords", "numberOfRecords"]) + def test_default_retries(self, live_keys, stub_marc21): token = WorldcatAccessToken( key=os.getenv("WCKey"), secret=os.getenv("WCSecret"), scopes=os.getenv("WCScopes"), - principal_id=os.getenv("WCPrincipalID"), - principal_idns=os.getenv("WCPrincipalIDNS"), ) with MetadataSession(authorization=token) as session: - response = session.search_general_holdings(isbn="9781597801744") - - assert response.status_code == 200 - assert sorted(response.json().keys()) == fields + with pytest.raises(WorldcatRequestError) as exc: + session.bib_validate(stub_marc21, recordFormat="foo/bar") + assert "406 Client Error: Not Acceptable for url: " in (str(exc.value)) + assert session.adapters["https://"].max_retries.total == 0 - def test_search_current_control_numbers(self, live_keys): + def test_custom_retries(self, live_keys, stub_marc21): token = WorldcatAccessToken( key=os.getenv("WCKey"), secret=os.getenv("WCSecret"), scopes=os.getenv("WCScopes"), - principal_id=os.getenv("WCPrincipalID"), - principal_idns=os.getenv("WCPrincipalIDNS"), ) - with MetadataSession(authorization=token) as session: - response = session.search_current_control_numbers([41266045, 519740398]) - - assert response.status_code == 207 - assert ( - response.request.url - == "https://worldcat.org/bib/checkcontrolnumbers?oclcNumbers=41266045%2C519740398" - ) - jres = response.json() - assert sorted(jres.keys()) == ["entries", "extensions"] - assert sorted(jres["entries"][0].keys()) == ["content", "title", "updated"] - assert sorted(jres["entries"][0]["content"].keys()) == sorted( - [ - "currentOclcNumber", - "detail", - "found", - "id", - "institution", - "merged", - "requestedOclcNumber", - "status", - ] + with MetadataSession( + authorization=token, + totalRetries=3, + backoffFactor=0.5, + statusForcelist=[406], + allowedMethods=["GET", "POST"], + ) as session: + with pytest.raises(WorldcatRequestError) as exc: + session.bib_validate(stub_marc21, recordFormat="foo/bar") + assert "Connection Error: " in ( + str(exc.value) ) + assert session.adapters["https://"].max_retries.total == 3 diff --git a/tests/test_query.py b/tests/test_query.py index 1f01876..3186f0d 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -18,12 +18,10 @@ def test_query_live(live_keys): key=os.getenv("WCKey"), secret=os.getenv("WCSecret"), scopes=os.getenv("WCScopes"), - principal_id=os.getenv("WCPrincipalID"), - principal_idns=os.getenv("WCPrincipalIDNS"), ) with MetadataSession(authorization=token) as session: header = {"Accept": "application/json"} - url = "https://americas.metadata.api.oclc.org/worldcat/search/v1/brief-bibs/41266045" + url = "https://metadata.api.oclc.org/worldcat/search/brief-bibs/41266045" req = Request( "GET", url, @@ -38,18 +36,17 @@ def test_query_live(live_keys): def test_query_not_prepared_request(stub_session): - with pytest.raises(AttributeError) as exc: + with pytest.raises(TypeError) as exc: req = Request("GET", "https://foo.org") Query(stub_session, req, timeout=2) assert "Invalid type for argument 'prepared_request'." in str(exc.value) @pytest.mark.http_code(200) -def test_query_with_stale_token(stub_session, mock_utcnow, mock_session_response): - stub_session.authorization.token_expires_at = datetime.datetime.strftime( - datetime.datetime.utcnow() - datetime.timedelta(0, 1), - "%Y-%m-%d %H:%M:%SZ", - ) +def test_query_with_stale_token(stub_session, mock_now, mock_session_response): + stub_session.authorization.token_expires_at = datetime.datetime.now( + datetime.timezone.utc + ) - datetime.timedelta(0, 1) assert stub_session.authorization.is_expired() is True req = Request("GET", "http://foo.org") @@ -98,16 +95,17 @@ def test_query_http_207_response(stub_session, mock_session_response): @pytest.mark.http_code(404) def test_query_http_404_response(stub_session, mock_session_response): header = {"Accept": "application/json"} - url = ( - "https://americas.metadata.api.oclc.org/worldcat/search/v1/brief-bibs/41266045" - ) + url = "https://metadata.api.oclc.org/worldcat/search/brief-bibs/41266045" req = Request("GET", url, headers=header, hooks=None) prepped = stub_session.prepare_request(req) with pytest.raises(WorldcatRequestError) as exc: Query(stub_session, prepped) - assert "404 Client Error: 'foo' for url: https://foo.bar?query" in str(exc.value) + assert ( + "404 Client Error: 'foo' for url: https://foo.bar?query. Server response: spam" + in str(exc.value) + ) @pytest.mark.http_code(500) @@ -117,7 +115,10 @@ def test_query_http_500_response(stub_session, mock_session_response): with pytest.raises(WorldcatRequestError) as exc: Query(stub_session, prepped) - assert "500 Server Error: 'foo' for url: https://foo.bar?query" in str(exc.value) + assert ( + "500 Server Error: 'foo' for url: https://foo.bar?query. Server response: spam" + in str(exc.value) + ) def test_query_timeout_exception(stub_session, mock_timeout): @@ -140,6 +141,17 @@ def test_query_connection_exception(stub_session, mock_connection_error): ) +def test_query_retry_exception(stub_session, mock_retry_error): + req = Request("GET", "https://foo.org") + prepped = stub_session.prepare_request(req) + with pytest.raises(WorldcatRequestError) as exc: + Query(stub_session, prepped) + + assert "Connection Error: " in str( + exc.value + ) + + def test_query_unexpected_exception(stub_session, mock_unexpected_error): req = Request("GET", "https://foo.org") prepped = stub_session.prepare_request(req) @@ -149,11 +161,21 @@ def test_query_unexpected_exception(stub_session, mock_unexpected_error): assert "Unexpected request error: " in str(exc.value) -@pytest.mark.http_code(409) -def test_query_holding_endpoint_409_http_code(stub_session, mock_session_response): - req = Request("POST", "https://worldcat.org/ih/data", params={"foo": "bar"}) +def test_query_timeout_retry(stub_retry_session, caplog): + req = Request("GET", "https://foo.org") + prepped = stub_retry_session.prepare_request(req) + with pytest.raises(WorldcatRequestError): + Query(stub_retry_session, prepped) + + assert "Retry(total=0, " in caplog.records[2].message + assert "Retry(total=1, " in caplog.records[1].message + assert "Retry(total=2, " in caplog.records[0].message + + +def test_query_timeout_no_retry(stub_session, caplog): + req = Request("GET", "https://foo.org") prepped = stub_session.prepare_request(req) - with does_not_raise(): - query = Query(stub_session, prepped) + with pytest.raises(WorldcatRequestError): + Query(stub_session, prepped) - assert query.response.status_code == 409 + assert "Retry" not in caplog.records diff --git a/tests/test_session.py b/tests/test_session.py index 5119ab5..2717a34 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -5,7 +5,6 @@ from bookops_worldcat._session import WorldcatSession from bookops_worldcat.__version__ import __title__, __version__ -from bookops_worldcat.errors import WorldcatSessionError class TestWorldcatSession: @@ -24,22 +23,60 @@ def test_custom_user_agent_header(self, mock_token): ) @pytest.mark.parametrize( - "argm,expectation", - [ - (123, pytest.raises(WorldcatSessionError)), - ({}, pytest.raises(WorldcatSessionError)), - ((), pytest.raises(WorldcatSessionError)), - ], + "arg", + [123, {}, (), ""], ) - def test_invalid_user_agent_arguments(self, argm, expectation, mock_token): - with expectation: - WorldcatSession(mock_token, agent=argm) - assert "Argument 'agent' must be a str" in str(expectation.value) + def test_invalid_user_agent_arguments(self, arg, mock_token): + with pytest.raises(ValueError) as exc: + WorldcatSession(mock_token, agent=arg) + assert "Argument 'agent' must be a string." in str(exc.value) def test_default_timeout(self, mock_token): - with WorldcatSession(mock_token, timeout=None) as session: + with WorldcatSession(mock_token) as session: assert session.timeout == (5, 5) def test_custom_timeout(self, mock_token): with WorldcatSession(mock_token, timeout=1) as session: assert session.timeout == 1 + + def test_default_adapter(self, mock_token): + with WorldcatSession(mock_token) as session: + assert session.adapters["https://"].max_retries.total == 0 + + def test_adapter_retries(self, mock_token): + with WorldcatSession( + authorization=mock_token, + totalRetries=3, + backoffFactor=0.5, + statusForcelist=[500, 502, 503, 504], + allowedMethods=["GET", "POST", "PUT"], + ) as session: + assert session.adapters["https://"].max_retries.status_forcelist == [ + 500, + 502, + 503, + 504, + ] + + def test_no_statusForcelist(self, mock_token): + with WorldcatSession( + authorization=mock_token, + totalRetries=2, + backoffFactor=0.1, + allowedMethods=["GET"], + ) as session: + assert session.adapters[ + "https://" + ].max_retries.status_forcelist == frozenset({413, 429, 503}) + + @pytest.mark.parametrize("arg", [[], "", 123, {}, ["123", "234"]]) + def test_statusForcelist_error(self, mock_token, arg): + with pytest.raises(ValueError) as exc: + WorldcatSession( + authorization=mock_token, + totalRetries=2, + backoffFactor=0.1, + statusForcelist=arg, + allowedMethods=["GET"], + ) + assert "Argument 'statusForcelist' must be a list of integers" in str(exc.value) diff --git a/tests/test_utils.py b/tests/test_utils.py index 5bbbe51..e3101c1 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -16,7 +16,12 @@ class TestUtils: @pytest.mark.parametrize( "argm,expectation", - [("ocm00012345", "12345"), ("ocn00012346", "12346"), ("on000012347", "12347")], + [ + ("ocm00012345", "12345"), + ("ocn00012346", "12346"), + ("on000012347", "12347"), + (" ocm00012348", "12348"), + ], ) def test_prep_oclc_number_str(self, argm, expectation): assert prep_oclc_number_str(argm) == expectation @@ -34,6 +39,8 @@ def test_prep_oclc_number_str_exception(self): ("12345", ["12345"]), ("12345,67890", ["12345", "67890"]), ("12345, 67890", ["12345", "67890"]), + (" , ", []), + ("", []), ], ) def test_str2list(self, argm, expectation): @@ -45,34 +52,34 @@ def test_str2list(self, argm, expectation): ( None, pytest.raises(InvalidOclcNumber), - "Argument 'oclc_number' is missing.", + "Argument 'oclcNumber' is missing.", ), ( [12345], pytest.raises(InvalidOclcNumber), - "Argument 'oclc_number' is of invalid type.", + "Argument 'oclcNumber' is of invalid type.", ), ( 12345.5, pytest.raises(InvalidOclcNumber), - "Argument 'oclc_number' is of invalid type.", + "Argument 'oclcNumber' is of invalid type.", ), ( "bt12345", pytest.raises(InvalidOclcNumber), - "Argument 'oclc_number' does not look like real OCLC #.", + "Argument 'oclcNumber' does not look like real OCLC #.", ), ( "odn12345", pytest.raises(InvalidOclcNumber), - "Argument 'oclc_number' does not look like real OCLC #.", + "Argument 'oclcNumber' does not look like real OCLC #.", ), ], ) def test_verify_oclc_number_exceptions(self, argm, expectation, msg): with expectation as exp: verify_oclc_number(argm) - assert msg == str(exp.value) + assert msg == str(exp.value) @pytest.mark.parametrize( "argm,expectation", @@ -94,44 +101,44 @@ def test_verify_oclc_number_success(self, argm, expectation): ( None, pytest.raises(InvalidOclcNumber), - "Argument 'oclcNumbers' must be a list or comma separated string of valid OCLC #.", + "Argument 'oclcNumbers' must be a list or comma separated string of valid OCLC #s.", ), ( "", pytest.raises(InvalidOclcNumber), - "Argument 'oclcNumbers' must be a list or comma separated string of valid OCLC #.", + "Argument 'oclcNumbers' must be a list or comma separated string of valid OCLC #s.", ), ( [], pytest.raises(InvalidOclcNumber), - "Argument 'oclcNumbers' must be a list or comma separated string of valid OCLC #.", + "Argument 'oclcNumbers' must be a list or comma separated string of valid OCLC #s.", ), ( ",,", pytest.raises(InvalidOclcNumber), - "Argument 'oclcNumbers' must be a list or comma separated string of valid OCLC #.", + "Argument 'oclcNumbers' must be a list or comma separated string of valid OCLC #s.", ), ( 12345.5, pytest.raises(InvalidOclcNumber), - "One of passed OCLC #s is invalid.", + "Argument 'oclcNumbers' must be a list or comma separated string of valid OCLC #s.", ), ( "bt12345", pytest.raises(InvalidOclcNumber), - "One of passed OCLC #s is invalid.", + "Argument 'oclcNumber' does not look like real OCLC #.", ), ( "odn12345", pytest.raises(InvalidOclcNumber), - "One of passed OCLC #s is invalid.", + "Argument 'oclcNumber' does not look like real OCLC #.", ), ], ) def test_verify_oclc_numbers_exceptions(self, argm, expectation, msg): with expectation as exp: verify_oclc_numbers(argm) - assert msg == str(exp.value) + assert msg == str(exp.value) @pytest.mark.parametrize( "argm,expectation", diff --git a/tests/test_version.py b/tests/test_version.py index 632c4c0..0c9388a 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -10,7 +10,7 @@ def test_version(): - assert __version__ == "0.5.0" + assert __version__ == "1.0.0" def test_title():