diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7c184c8..5b2e1b4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ on: env: PYTHON_VERSION: "3.10" - POETRY_VERSION: "1.3.2" + POETRY_VERSION: "1.4.0" jobs: ci: diff --git a/.github/workflows/github-pages.yml b/.github/workflows/github-pages.yml index e0b1eb8..76e7fcb 100644 --- a/.github/workflows/github-pages.yml +++ b/.github/workflows/github-pages.yml @@ -5,7 +5,7 @@ on: workflow_dispatch env: PYTHON_VERSION: "3.10" POETRY_VERSION: "1.3.2" - PARAMDB_VERSION: "0.1.0" + PARAMDB_VERSION: "0.2.0" jobs: build: diff --git a/.gitignore b/.gitignore index a7e28b5..b72ab1d 100644 --- a/.gitignore +++ b/.gitignore @@ -12,9 +12,8 @@ jupyter_execute/ # Python __pycache__/ .mypy_cache/ -.coverage .pytest_cache/ -.ipynb_checkpoints/ +.coverage # Data files *.db diff --git a/CHANGELOG.md b/CHANGELOG.md index f6d15c3..187d155 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,14 +7,30 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] -- Implement loading data from a specified commit ID +## [0.2.0] (Mar 8 2023) + +### Added + +- Ability to specify commit ID in `ParamDB.load()` +- `ParamData.parent` and `ParamData.root` properties +- Mixins `ParentType[PT]` and `RootType[PT]` to type cast parent and root +- Parameter collection classes `ParamList` and `ParamDict` +- Database now ignores dataclass fields where `init` is `False` + +### Removed + +- `CommitNotFoundError` (replaced with built-in `IndexError`) +- Private `_last_updated` dataclass field in parameter dataclasses ## [0.1.0] (Feb 24 2023) -- Create parameter data classes (`Param` and `Struct`) -- Create database class `ParamDB` to store parameters in a SQLite file -- Add ability to retrieve the commit history as `CommitEntry` objects -- Create [documentation website](https://painterqubits.github.io/paramdb) +### Added + +- Parameter data base class `ParamData` +- Parameter base dataclasses (`Param` and `Struct`) +- Database class `ParamDB` to store parameters in a SQLite file +- Ability to retrieve the commit history as `CommitEntry` objects -[unreleased]: https://github.com/PainterQubits/paramdb/compare/v0.1.0...main +[unreleased]: https://github.com/PainterQubits/paramdb/compare/v0.2.0...main +[0.2.0]: https://github.com/PainterQubits/paramdb/releases/tag/v0.2.0 [0.1.0]: https://github.com/PainterQubits/paramdb/releases/tag/v0.1.0 diff --git a/README.md b/README.md index 8f2016f..2ca78ac 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,18 @@ Install the latest version of ParamDB using pip: pip install -U paramdb --extra-index-url https://painterqubits.github.io/paramdb/releases ``` -The `extra-index-url` parameter is needed since ParamDB is not published to PyPI yet. +The `extra-index-url` parameter is needed since ParamDB is not published to PyPI yet. If +you are using a Python package manager, add +`https://painterqubits.github.io/paramdb/releases` as a secondary source. For example, for +[Poetry] the command is + +``` +poetry source add --secondary paramdb https://painterqubits.github.io/paramdb/releases +``` + +Then the package can be installed like any other (e.g. `poetry add paramdb`). + +[poetry]: https://python-poetry.org @@ -24,16 +35,14 @@ The `extra-index-url` parameter is needed since ParamDB is not published to PyPI ParamDB has two main components: -- **Parameted Data**: Base classes that are used to defined the structure of parameter - data, which consists of parameters - ([`Param`](https://painterqubits.github.io/paramdb/api-reference#paramdb.Param)) and - groups of parameters, called structures - ([`Struct`](https://painterqubits.github.io/paramdb/api-reference#paramdb.Struct)). +- [**Parameter Data**]: Base classes that are used to defined the structure and + functionality of parameter data. + +- [**Database**]: A database object that commits and loads parameter data to a persistent + file. -- **Database**: A database object - ([`ParamDB`](https://painterqubits.github.io/paramdb/api-reference#paramdb.ParamDB)) - that commits and loads parameter data to a persistent file. +See the [api reference] for more information. -See the [usage page](https://painterqubits.github.io/paramdb/usage) on the documentation -website for examples and more information. Also see the -[api reference](https://painterqubits.github.io/paramdb/api-reference). +[**parameter data**]: https://painterqubits.github.io/paramdb/parameter-data.html +[**database**]: https://painterqubits.github.io/paramdb/database.html +[api reference]: https://painterqubits.github.io/paramdb/api-reference diff --git a/docs/_static/jupyter-sphinx.css b/docs/_static/jupyter-sphinx.css new file mode 100644 index 0000000..0999959 --- /dev/null +++ b/docs/_static/jupyter-sphinx.css @@ -0,0 +1,9 @@ +/* Override styles for Jupyter Sphinx. */ + +.jupyter_container .cell_input > div { + margin-bottom: 0.4rem; +} + +.jupyter_container .cell_output > div { + margin-top: 0; +} diff --git a/docs/api-reference.md b/docs/api-reference.md index 1d5b7e3..daafb32 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -10,26 +10,36 @@ .. autoclass:: ParamData ``` +```{eval-rst} +.. autoclass:: Param +``` + ```{eval-rst} .. autoclass:: Struct - :show-inheritance: ``` ```{eval-rst} -.. autoclass:: Param - :show-inheritance: +.. autoclass:: ParamList ``` -## Database +```{eval-rst} +.. autoclass:: ParamDict +``` ```{eval-rst} -.. autoclass:: ParamDB +.. autoclass:: ParentType ``` ```{eval-rst} -.. autoclass:: CommitEntry +.. autoclass:: RootType +``` + +## Database + +```{eval-rst} +.. autoclass:: ParamDB ``` ```{eval-rst} -.. autoclass:: CommitNotFoundError +.. autoclass:: CommitEntry ``` diff --git a/docs/conf.py b/docs/conf.py index 7f4989d..18e55a8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,27 +1,23 @@ -# pylint: skip-file -# type: ignore - # See https://www.sphinx-doc.org/en/master/usage/configuration.html for all options # Project information project = "ParamDB" copyright = "2023, California Institute of Technology" author = "Alex Hadley" -release = "0.1.0" +release = "0.2.0" # General configuration extensions = [ "myst_parser", "sphinx.ext.autodoc", "sphinx.ext.viewcode", - "sphinx_rtd_theme", "sphinx_copybutton", "jupyter_sphinx", ] # HTML output options -html_theme = "sphinx_rtd_theme" -html_theme_options = {"navigation_depth": 3} +html_theme = "furo" +html_static_path = ["_static"] # MyST options myst_heading_anchors = 3 @@ -29,12 +25,15 @@ # Autodoc options # See https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#configuration autodoc_default_options = {"members": True, "member-order": "bysource"} +autodoc_inherit_docstrings = False # Autodoc custom signature and return annotation processing def process_signature(app, what, name, obj, options, signature, return_annotation): if isinstance(signature, str): signature = signature.replace("~", "") + signature = signature.replace("collections.abc.", "") + signature = signature.replace("paramdb._param_data._collections.", "") signature = signature.replace("paramdb._database.", "") if isinstance(return_annotation, str): return_annotation = return_annotation.replace("paramdb._database.", "") diff --git a/docs/database.md b/docs/database.md new file mode 100644 index 0000000..73d74dd --- /dev/null +++ b/docs/database.md @@ -0,0 +1,99 @@ +# Database + +```{py:currentmodule} paramdb + +``` + + + +```{jupyter-execute} +:hide-code: + +from __future__ import annotations +import os +from tempfile import TemporaryDirectory +tmp_dir = TemporaryDirectory() +os.chdir(tmp_dir.name) +os.makedirs("path/to") +``` + +The database is represented by a {py:class}`ParamDB` object. A path is passed, and a new +database file is created if it does not already exist. We can parameterize the class with +the root data type in order for its methods (e.g. {py:meth}`ParamDB.commit`) work properly +with type checking. For example: + +```{jupyter-execute} +from dataclasses import dataclass +from paramdb import Struct, Param, ParamDB + +@dataclass +class Root(Struct): + param: CustomParam + +@dataclass +class CustomParam(Param): + value: float + +param_db = ParamDB[Root]("path/to/param.db") +``` + +```{important} +The {py:class}`ParamDB` object should be created once per project and imported by other +files that access the database. +``` + +```{note} +Dataclass fields created with `init=False` will not be stored in or restored from the +database. See [`dataclasses.field`] for more information. +``` + +## Commit and Load + +Data can be committed using {py:meth}`ParamDB.commit` and loaded using +{py:meth}`ParamDB.load`, which either loads the most recent commit, or takes a specific +commit ID. Note that commit IDs start from 1. For example: + +```{jupyter-execute} +root = Root(param=CustomParam(value=1.23)) +param_db.commit("Initial commit", root) +root.param.value += 1 +param_db.commit("Increment param value", root) +``` + +We can then load the most recent commit: + +```{jupyter-execute} +param_db.load() +``` + +Or a specific previous commit: + +```{jupyter-execute} +param_db.load(1) +``` + +```{warning} +Simultaneous database operations have not been tested yet. Simultaneous read operations +(e.g. calls to {py:meth}`ParamDB.load`) are likely ok, but simultaneous write operations +(e.g. calls to {py:meth}`ParamDB.commit`) may raise an exception. +``` + +## Commit History + +We can get a list of commits (as {py:class}`CommitEntry` objects) using the +{py:meth}`ParamDB.commit_history` method. + +```{jupyter-execute} +param_db.commit_history() +``` + + + +```{jupyter-execute} +:hide-code: + +# Gets ride of PermissionError on Windows +param_db._engine.dispose() +``` + +[`dataclasses.field`]: https://docs.python.org/3/library/dataclasses.html#dataclasses.field diff --git a/docs/index.md b/docs/index.md index 3bb84d0..603de73 100644 --- a/docs/index.md +++ b/docs/index.md @@ -11,7 +11,8 @@ :maxdepth: 2 installation -usage +parameter-data +database api-reference changelog license diff --git a/docs/parameter-data.md b/docs/parameter-data.md new file mode 100644 index 0000000..94f889b --- /dev/null +++ b/docs/parameter-data.md @@ -0,0 +1,227 @@ +# Parameter Data + +```{py:currentmodule} paramdb + +``` + + + +```{jupyter-execute} +:hide-code: + +from __future__ import annotations +``` + +A ParamDB database stores parameter data. The abstract base class {py:class}`ParamData` +defines some core functionality for this data, including the +{py:class}`~ParamData.last_updated`, {py:class}`~ParamData.parent`, and +{py:class}`~ParamData.root` properties. Internally, any subclasses of +{py:class}`ParamData` automatically registered with ParamDB so that they can be loaded +to and from JSON, which is how they are stored in the database. + +All of the classes described on this page are subclasses of {py:class}`ParamData`. + +```{important} +Any data that is going to be stored in a ParamDB database must be a JSON serializable +type (`str`, `int`, `float`, `bool`, `None`, `dict`, or `list`), a [`datetime`], or an +instance of a {py:class}`ParamData` subclass. Otherwise, a `TypeError` will be raised +when they are committed to the database. +``` + +## Parameters + +A parameter is defined from the base class {py:class}`Param`. This custom class is +intended to be defined using the [`@dataclass`] decorator, meaning that class variables +with type annotations are automatically become object properties, and the corresponding +[`__init__`] function is generated. An example of a defining a custom parameter is shown +below. + +```{jupyter-execute} +from dataclasses import dataclass +from paramdb import Param + +@dataclass +class CustomParam(Param): + value: float + +param = CustomParam(value=1.23) +``` + +These properties can then be accessed and updated. + +```{jupyter-execute} +param.value += 0.004 +param.value +``` + +Methods can also be added, including dynamic read-only properties using the +[`@property`](https://docs.python.org/3/library/functions.html#property) decorator. For +example: + +```{jupyter-execute} +@dataclass +class ParamWithProperty(Param): + value: int + + @property + def value_cubed(self) -> int: + return self.value ** 3 + +param_with_property = ParamWithProperty(value=16) +param_with_property.value_cubed +``` + +````{important} +Since [`__init__`] is generated by the [`@dataclass`] decorator, other initialization must +be done using the [`__post_init__`] function. Furthermore, since [`__post_init__`] is used +internally by {py:class}`ParamData`, {py:class}`Param`, and {py:class}`Struct` to perform +initialization, always call the superclass's [`__post_init__`] at the end. For example: + +```{jupyter-execute} +@dataclass +class ParamCustomInit(Param): + def __post_init__(self) -> None: + print("Initializing...") # Replace with custom initialization code + super().__post_init__() + +param_custom_init = ParamCustomInit() +``` +```` + +Parameters track when any of their properties was last updated in the read-only +{py:attr}`~Param.last_updated` property. For example: + +```{jupyter-execute} +param.last_updated +``` + +```{jupyter-execute} +import time + +time.sleep(1) +param.value += 1 +param.last_updated +``` + +## Structures + +A structure is defined from the base class {py:class}`Struct` and is intended +to be defined as a dataclass. The key difference from {py:class}`Param` is that +structures do not store their own last updated time; instead, the +{py:attr}`ParamData.last_updated` property returns the most recent last updated time +of any {py:class}`ParamData` they contain. For example: + +```{jupyter-execute} +from paramdb import Struct, ParamDict + +@dataclass +class CustomStruct(Struct): + value: float + param: CustomParam + +struct = CustomStruct(value=1.23, param=CustomParam(value=4.56)) +struct.last_updated +``` + +```{jupyter-execute} +time.sleep(1) +struct.param.value += 1 +struct.last_updated +``` + +You can access the parent of any parameter data using the {py:attr}`ParamData.parent` +property. For example: + +```{jupyter-execute} +struct.param.parent == struct +``` + +Similarly, the root can be accessed via {py:attr}`ParamData.root`: + +```{jupyter-execute} +struct.param.root == struct +``` + +See [Type Mixins](#type-mixins) for information on how to get the parent and root +properties to work better with static type checkers. + +## Collections + +Ordinary lists and dictionaries can be used within parameter data; however, any +parameter data objects they contain will not have a parent object. This is because +internally, the parent is set by the {py:class}`ParamData` object that most recently +added the given parameter data as a child. Therefore, it is not recommended to use +ordinary lists and dictionaries to store parameter data. Instead, {py:class}`ParamList` +and {py:class}`ParamDict` can be used. + +{py:class}`ParamList` implements the abstract base class `MutableSequence` from +[`collections.abc`], so it behaves similarly to a list. It is also a subclass of +{py:class}`ParamData`, so the parent and root properties will work properly. For +example, + +```{jupyter-execute} +from paramdb import ParamList + +param_list = ParamList([CustomParam(1), CustomParam(2), CustomParam(3)]) +param_list[1].parent == param_list +``` + +Similarly, {py:class}`ParamDict` implements `MutableMapping` from [`collections.abc`], +so it behaves similarly to a dictionary. Additionally, its items can be accessed via +dot notation in addition to index brackets (unless they begin with an underscore). For +example, + +```{jupyter-execute} +from paramdb import ParamDict + +param_dict = ParamDict({ + "p1": CustomParam(value=1.23), + "p2": CustomParam(value=4.56), + "p3": CustomParam(value=7.89), +}) +param_dict.p2.root == param_dict +``` + +Parameter collections can also be subclassed to provide custom functionality. For example: + +```{jupyter-execute} +class CustomDict(ParamDict[CustomParam]): + @property + def total(self) -> float: + return sum(param.value for param in self.values()) + +custom_dict = CustomDict(param_dict) +custom_dict.total +``` + +## Type Mixins + +The return type hint for {py:attr}`ParamData.parent` and {py:attr}`ParamData.root` is +{py:class}`ParamData`. Since the parent and root objects can change, it is not possible +to automatically infer a more specific type for the parent or root. However, a type hint +can be given using the {py:class}`ParentType` and {py:class}`RootType` mixins. For +example: + +```{jupyter-execute} +from paramdb import ParentType + +@dataclass +class ParentStruct(Struct): + param: Child + +@dataclass +class ChildParam(Param, ParentType[ParentStruct]): + value: float + +struct = ParentStruct(param=ChildParam(value=1.23)) +``` + +This does nothing to the functionality, but static type checkers will now know that +`struct.param.parent` in the example above is a `ParentStruct` object. + +[`datetime`]: https://docs.python.org/3/library/datetime.html#datetime-objects +[`@dataclass`]: https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass +[`__init__`]: https://docs.python.org/3/reference/datamodel.html#object.__init__ +[`@property`]: https://docs.python.org/3/library/functions.html#property +[`__post_init__`]: https://docs.python.org/3/library/dataclasses.html#post-init-processing +[`collections.abc`]: https://docs.python.org/3/library/collections.abc.html diff --git a/docs/usage.md b/docs/usage.md deleted file mode 100644 index 55a8f8c..0000000 --- a/docs/usage.md +++ /dev/null @@ -1,147 +0,0 @@ -# Usage - -```{py:currentmodule} paramdb - -``` - - - -```{jupyter-execute} -:hide-code: - -import os -from tempfile import TemporaryDirectory -tmp_dir = TemporaryDirectory() -os.chdir(tmp_dir.name) -os.makedirs("path/to") -``` - -ParamDB has two main components: - -- [Parameter Data](#parameter-data): Base classes that are used to defined the structure - of parameter data, which consists of parameters ({py:class}`Param`) and groups of - parameters, called structures ({py:class}`Struct`). - -- [Database](#database): A database object ({py:class}`ParamDB`) that commits and loads - parameter data to a persistent file. - -The usage of each of these components is explained in more detail below. - -## Parameter Data - -### Parameters - -A parameter is defined from the base class {py:class}`Param`. This custom class is -intended to be defined using the -[`@dataclass`](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass) -decorator, meaning that class variables with type annotations are automatically become -object properties, and the corresponding `__init__` function is generated. An example of -a defining a custom parameter is shown below. - -```{jupyter-execute} -from paramdb import Param -from dataclasses import dataclass - -@dataclass -class CustomParam(Param): - value: float - -param = CustomParam(value=1.23) -``` - -```{tip} -Methods can also be added to {py:class}`Param` and {py:class}`Struct` subclasses, -including read-only properties using the -[`@property`](https://docs.python.org/3/library/functions.html#property) decorator. - -Also, since the `__init__` function is generated, other initialization should be done -using the -[`__post_init__`](https://docs.python.org/3/library/dataclasses.html#post-init-processing) -function. -``` - -Parameters track when any of their properties was last updated in the read-only -{py:attr}`Param.last_updated` property. - -```{jupyter-execute} -param.last_updated -``` - -### Structures - -A structure is defined from the base class {py:class}`Struct` and is intended -to be defined as a dataclass. A structure can contain any data, but it is intended to -store parameters and lists and dictionaries of parameters. For example: - -```{jupyter-execute} -from paramdb import Struct - -@dataclass -class CustomStruct(Struct): - param: CustomParam - param_dict: dict[str, CustomParam] - -struct = CustomStruct( - param = param, - param_dict = { - "p1": CustomParam(value=4.56), - "p2": CustomParam(value=7.89), - } -) -``` - -Structures also have a {py:attr}`Struct.last_updated` property that computes the most -recent last updated time from any of its child parameters (including those within -structures, lists, and dictionaries). - -```{jupyter-execute} -struct.last_updated -``` - -## Database - -The database is represented by a {py:class}`ParamDB` object. A path is passed, and a new -database file is created if it does not already exist. We can parameterize the class with -the root data type in order for its methods (e.g. {py:meth}`ParamDB.commit`) work properly -with type checking. For example: - -```{jupyter-execute} -from paramdb import ParamDB - -param_db = ParamDB[CustomStruct]("path/to/param.db") -``` - -```{note} -The {py:class}`ParamDB` object should be created once per project and imported by other -files that access the database. -``` - -Data can be committed using {py:meth}`ParamDB.commit` and loaded using -{py:meth}`ParamDB.load`. Note that commit IDs start from 1. For example: - -```{jupyter-execute} -param_db.commit("Initial commit", struct) - -param_db.load() == struct -``` - -```{warning} -Simultaneous database operations have not been tested yet. Simultaneous read operations -(e.g. calls to {py:meth}`ParamDB.load`) are likely ok, but simultaneous write operations -(e.g. calls to {py:meth}`ParamDB.commit`) may raise an error. -``` - -We can get a list of commits using the {py:meth}`ParamDB.commit_history` method. - -```{jupyter-execute} -param_db.commit_history() -``` - - - -```{jupyter-execute} -:hide-code: - -# Gets ride of PermissionError on Windows -param_db._engine.dispose() -``` diff --git a/paramdb/__init__.py b/paramdb/__init__.py index 4fd1746..801cc41 100644 --- a/paramdb/__init__.py +++ b/paramdb/__init__.py @@ -2,18 +2,20 @@ Database for storing and retrieving QPU parameters during quantum control experiments. """ +from paramdb._param_data._param_data import ParamData +from paramdb._param_data._dataclasses import Param, Struct +from paramdb._param_data._collections import ParamList, ParamDict +from paramdb._param_data._type_mixins import ParentType, RootType from paramdb._database import ParamDB, CommitEntry -from paramdb._param_data import ParamData, Struct, Param -from paramdb._exceptions import CommitNotFoundError - -__version__ = "0.1.0" __all__ = [ - "__version__", - "ParamDB", - "CommitEntry", "ParamData", - "Struct", "Param", - "CommitNotFoundError", + "Struct", + "ParamList", + "ParamDict", + "ParentType", + "RootType", + "ParamDB", + "CommitEntry", ] diff --git a/paramdb/_database.py b/paramdb/_database.py index 767ea60..fdc478e 100644 --- a/paramdb/_database.py +++ b/paramdb/_database.py @@ -13,11 +13,12 @@ Mapped, mapped_column, ) -from paramdb._param_data import ParamData, get_param_class -from paramdb._exceptions import CommitNotFoundError +from paramdb._param_data._param_data import ParamData, get_param_class +T = TypeVar("T") -T = TypeVar("T", bound=Any) +# Name of the key storing the class name of a JSON-encoded object +CLASS_NAME_KEY = "__type" def _compress(text: str) -> bytes: @@ -37,37 +38,41 @@ def _to_dict(obj: Any) -> Any: Note that objects within the dictionary do not need to be JSON serializable, since they will be recursively processed by `json.dumps`. """ - class_name_dict = {"__class__": obj.__class__.__name__} + class_name = type(obj).__name__ + class_name_dict = {CLASS_NAME_KEY: class_name} if isinstance(obj, datetime): return class_name_dict | {"isoformat": obj.isoformat()} if isinstance(obj, ParamData): return class_name_dict | obj.to_dict() - raise TypeError(f"{repr(obj)} is not JSON serializable") + raise TypeError( + f"'{class_name}' object {repr(obj)} is not JSON serializable, so the commit" + " failed" + ) def _from_dict(json_dict: dict[str, Any]) -> dict[str, Any] | datetime | ParamData: """ - If the given dictionary created by `json.loads` has the key __class__, attempt to - construct an object of the named class from it. Otherwise, return the dictionary - unchanged. + If the given dictionary created by `json.loads` has the key `CLASS_NAME_KEY`, + attempt to construct an object of the named type from it. Otherwise, return the + dictionary unchanged. """ - if "__class__" in json_dict: - class_name = json_dict.pop("__class__") + if CLASS_NAME_KEY in json_dict: + class_name = json_dict.pop(CLASS_NAME_KEY) if class_name == datetime.__name__: return datetime.fromisoformat(json_dict["isoformat"]) param_class = get_param_class(class_name) if param_class is not None: - return cast(ParamData, param_class.from_dict(json_dict)) - raise ValueError(f"class '{class_name}' is not known to paramdb") + return param_class.from_dict(json_dict) + raise ValueError( + f"class '{class_name}' is not known to ParamDB, so the load failed" + ) return json_dict -# pylint: disable-next=too-few-public-methods class _Base(MappedAsDataclass, DeclarativeBase): """Base class for defining SQLAlchemy declarative mapping classes.""" -# pylint: disable-next=too-few-public-methods class _Snapshot(_Base): """Snapshot of the database.""" @@ -120,9 +125,8 @@ def commit(self, message: str, data: T) -> None: def load(self, commit_id: int | None = None) -> T: """ Load and return data from the database. If a commit ID is given, load from that - commit; otherwise, load from the most recent commit. Raise a - :py:exc:`CommitNotFoundError` if the specified commit does not exist or if the - database is empty. + commit; otherwise, load from the most recent commit. Raise a ``IndexError`` if + the specified commit does not exist. Note that commit IDs begin at 1. """ @@ -135,8 +139,8 @@ def load(self, commit_id: int | None = None) -> T: with self._Session() as session: data = session.scalar(select_stmt) if data is None: - raise CommitNotFoundError( - f"cannot load most recent data because database" + raise IndexError( + f"cannot load most recent commit because database" f" '{self._engine.url.database}' has no commits" if commit_id is None else f"commit {commit_id} does not exist in database" diff --git a/paramdb/_exceptions.py b/paramdb/_exceptions.py deleted file mode 100644 index d9af946..0000000 --- a/paramdb/_exceptions.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Parameter database exceptions.""" - - -class CommitNotFoundError(Exception): - """Commit does not exist in the database.""" diff --git a/paramdb/_param_data.py b/paramdb/_param_data.py deleted file mode 100644 index 789b1a4..0000000 --- a/paramdb/_param_data.py +++ /dev/null @@ -1,166 +0,0 @@ -"""Base classes for parameter data, including structures and parameters.""" - -from __future__ import annotations - -from typing import Any -from collections.abc import Iterable, Mapping -from abc import ABCMeta, abstractmethod -from weakref import WeakValueDictionary -from datetime import datetime -from dataclasses import dataclass, field, fields - -# Stores weak references to existing parameter classes -_param_classes: WeakValueDictionary[str, _ParamClass] = WeakValueDictionary() - - -def get_param_class(class_name: str) -> _ParamClass | None: - """Get a parameter class given its name, or ``None`` if the class does not exist.""" - return _param_classes[class_name] if class_name in _param_classes else None - - -class _ParamClass(ABCMeta): - """ - Metaclass for all parameter classes. Inherits from ABCMeta to allow for abstract - methods. - """ - - def __new__( - mcs: type[_ParamClass], - name: str, - bases: tuple[type, ...], - namespace: dict[str, Any], - **kwargs: Any, - ) -> _ParamClass: - """ - Construct a new parameter class and add it to the dictionary of parameter - classes. - """ - param_class = super().__new__(mcs, name, bases, namespace, **kwargs) - _param_classes[name] = param_class - return param_class - - def from_dict(cls, json_dict: dict[str, Any]) -> Any: - """ - Construct a parameter data object from the given dictionary, usually created by - `json.loads`. - """ - return cls(**json_dict) - - -@dataclass -class ParamData(metaclass=_ParamClass): - """ - Abstract base class for all parameter data. The base classes :py:class:`Struct` and - :py:class:`Param` are subclasses of this class. - - Custom parameter data classes are intended to be dataclasses. If they are not - dataclasses, then custom :py:meth:`to_dict` and ``__init__`` methods should be - defined so that the parameter data object can be properly converted to and from - JSON. See :py:meth:`to_dict` for more information. - """ - - @property - @abstractmethod - def last_updated(self) -> datetime | None: - """ - When this parameter data was last updated, or None if no last updated time - exists. - """ - - def to_dict(self) -> dict[str, Any]: - """ - Convert this parameter data object into a dictionary to be passed to - ``json.dumps``. This dictionary will later be passed to the subclass's - ``__init__`` function to reconstruct the object. By default, the dictionary maps - from Python dataclass fields to values. - - Note that objects within the dictionary do not need to be JSON serializable, - since they will be recursively processed by ``json.dumps``. - """ - return {f.name: getattr(self, f.name) for f in fields(self)} - - def __getitem__(self, name: str) -> Any: - """Enable getting attributes via indexing.""" - return getattr(self, name) - - def __setitem__(self, name: str, value: Any) -> Any: - """Enable setting attributes via indexing.""" - return setattr(self, name, value) - - -class Struct(ParamData): - """ - Base class for parameter structures. Custom structures should be subclasses of this - class and are intended to be dataclasses. For example:: - - from dataclasses import dataclass - from paramdb import Struct - - @dataclass - class CustomStruct(Struct): - custom_param: CustomParam # CustomParam class is defined somewhere else - - A structure can contain any data, but it is intended to store parameters and lists - and dictionaries of parameters. - """ - - @property - def last_updated(self) -> datetime | None: - """ - When any parameter within this structure (including those nested within lists, - dictionaries, and other structures) was last updated, or ``None`` if this - structure contains no parameters. - """ - return self._get_last_updated(getattr(self, f.name) for f in fields(self)) - - def _get_last_updated(self, obj: Any) -> datetime | None: - """ - Get the last updated time from a :py:class:`ParamData` object, or recursively - search through any iterable type to find the latest last updated time. - """ - if isinstance(obj, ParamData): - return obj.last_updated - if isinstance(obj, Iterable) and not isinstance(obj, str): - # Strings are excluded because they will never contain ParamData and contain - # strings, leading to infinite recursion. - values = obj.values() if isinstance(obj, Mapping) else obj - return max( - filter(None, (self._get_last_updated(v) for v in values)), - default=None, - ) - return None - - -@dataclass(kw_only=True) -class Param(ParamData): - """ - Base class for parameters. Custom parameters should be subclasses of this class and - are intended to be dataclasses. For example:: - - from dataclasses import dataclass - from paramdb import Param - - @dataclass - class CustomParam(Param): - value: float - """ - - _last_updated: datetime = field(default_factory=datetime.now) - - _initialized = False - - def __post_init__(self) -> None: - """Register that the object is done initializing.""" - # Use the superclass setattr method to avoid updating _last_updated. - super().__setattr__("_initialized", True) - - @property - def last_updated(self) -> datetime: - """When this parameter was last updated.""" - return self._last_updated - - def __setattr__(self, name: str, value: Any) -> None: - """Set the given attribute and update the last updated time.""" - super().__setattr__(name, value) - if self._initialized: - super().__setattr__("_last_updated", datetime.now()) diff --git a/paramdb/_param_data/__init__.py b/paramdb/_param_data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/paramdb/_param_data/_collections.py b/paramdb/_param_data/_collections.py new file mode 100644 index 0000000..58f2122 --- /dev/null +++ b/paramdb/_param_data/_collections.py @@ -0,0 +1,174 @@ +"""Parameter data collection classes.""" + +from typing import TypeVar, Generic, SupportsIndex, Any, overload +from collections.abc import ( + Iterator, + Collection, + Iterable, + Mapping, + MutableSequence, + MutableMapping, +) +from datetime import datetime +from typing_extensions import Self +from paramdb._param_data._param_data import ParamData + + +T = TypeVar("T") + + +# pylint: disable-next=abstract-method +class _ParamCollection(ParamData): + """Base class for parameter collections.""" + + _contents: Collection[Any] + + def __len__(self) -> int: + return len(self._contents) + + def __eq__(self, other: Any) -> bool: + # Equal if they have are of the same class and their contents are equal + return ( + isinstance(other, _ParamCollection) + and type(other) is type(self) + and self._contents == other._contents + ) + + def __repr__(self) -> str: + return f"{type(self).__name__}({self._contents})" + + @property + def last_updated(self) -> datetime | None: + return self._get_last_updated(self._contents) + + +class ParamList(_ParamCollection, MutableSequence[T], Generic[T]): + """Mutable sequence that is also parameter data.""" + + _contents: list[T] + + def __init__(self, iterable: Iterable[T] | None = None) -> None: + self._contents = [] if iterable is None else list(iterable) + if iterable is not None: + for item in self._contents: + self._add_child(item) + + @overload + def __getitem__(self, index: SupportsIndex) -> T: + ... + + @overload + def __getitem__(self, index: slice) -> list[T]: + ... + + def __getitem__(self, index: Any) -> Any: + return self._contents[index] + + @overload + def __setitem__(self, index: SupportsIndex, value: T) -> None: + ... + + @overload + def __setitem__(self, index: slice, value: Iterable[T]) -> None: + ... + + def __setitem__(self, index: SupportsIndex | slice, value: Any) -> None: + old_value: Any = self._contents[index] + self._contents[index] = value + if isinstance(index, slice): + for item in old_value: + self._remove_child(item) + for item in value: + self._add_child(item) + else: + self._remove_child(old_value) + self._add_child(value) + + def __delitem__(self, index: SupportsIndex | slice) -> None: + old_value = self._contents[index] + del self._contents[index] + self._remove_child(old_value) + + def insert(self, index: SupportsIndex, value: T) -> None: + self._contents.insert(index, value) + self._add_child(value) + + def to_dict(self) -> dict[str, list[T]]: + return {"items": self._contents} + + @classmethod + def from_dict(cls, json_dict: dict[str, list[T]]) -> Self: + return cls(json_dict["items"]) + + +class ParamDict(_ParamCollection, MutableMapping[str, T], Generic[T]): + """ + Mutable mapping that is also parameter data. + + Keys that do not beginning with an underscore can be set via dot notation. + """ + + _contents: dict[str, T] + + def __init__(self, mapping: Mapping[str, T] | None = None): + # Use superclass __setattr__ to set actual attribute, not dictionary item + self._contents = {} if mapping is None else dict(mapping) + if mapping is not None: + for item in self._contents.values(): + self._add_child(item) + + def __getitem__(self, key: str) -> T: + return self._contents[key] + + def __setitem__(self, key: str, value: T) -> None: + old_value = self._contents[key] if key in self._contents else None + self._contents[key] = value + self._remove_child(old_value) + self._add_child(value) + + def __delitem__(self, key: str) -> None: + old_value = self._contents[key] if key in self._contents else None + del self._contents[key] + self._remove_child(old_value) + + def __iter__(self) -> Iterator[str]: + yield from self._contents + + def __getattr__(self, name: str) -> T: + # Enable accessing items via dot notation + if self._is_attribute(name): + # It is important to raise an attribute error rather than a key error for + # attribute names. For example, this allows deepcopy to work properly. + raise AttributeError( + f"'{type(self).__name__}' object has no attribute '{name}'" + ) + return self[name] + + def __setattr__(self, name: str, value: T) -> None: + # Enable setting items via dot notation + if self._is_attribute(name): + super().__setattr__(name, value) + else: + self[name] = value + + def __delattr__(self, name: str) -> None: + # Enable deleting items via dot notation + if self._is_attribute(name): + super().__delattr__(name) + else: + del self[name] + + def _is_attribute(self, name: str) -> bool: + """ + Names beginning with underscores are considered to be attributes when accessed + via dot notation. This is both to allow internal Python variables to be set + (i.e. dunder variables), and to allow for true attributes to be used if needed. + """ + return len(name) > 0 and name[0] == "_" + + def to_dict(self) -> dict[str, T]: + return self._contents + + @classmethod + def from_dict(cls, json_dict: dict[str, T]) -> Self: + return cls(json_dict) diff --git a/paramdb/_param_data/_dataclasses.py b/paramdb/_param_data/_dataclasses.py new file mode 100644 index 0000000..f491199 --- /dev/null +++ b/paramdb/_param_data/_dataclasses.py @@ -0,0 +1,100 @@ +"""Base classes for parameter dataclasses.""" + +from __future__ import annotations +from typing import Any +from abc import abstractmethod +from datetime import datetime +from dataclasses import dataclass, fields +from typing_extensions import Self +from paramdb._param_data._param_data import ParamData + + +@dataclass +class _ParamDataclass(ParamData): + """Base class for parameter dataclasses.""" + + def __getitem__(self, name: str) -> Any: + # Enable getting attributes via indexing + return getattr(self, name) + + def __setitem__(self, name: str, value: Any) -> None: + # Enable setting attributes via indexing + setattr(self, name, value) + + @property + @abstractmethod + def last_updated(self) -> datetime | None: + ... + + def to_dict(self) -> dict[str, Any]: + return {f.name: getattr(self, f.name) for f in fields(self) if f.init} + + @classmethod + def from_dict(cls, json_dict: dict[str, Any]) -> Self: + return cls(**json_dict) + + +@dataclass +class Param(_ParamDataclass): + """ + Base class for parameters. Custom parameters should be subclasses of this class and + are intended to be dataclasses. For example:: + + @dataclass + class CustomParam(Param): + value: float + """ + + def __post_init__(self) -> None: + self.__last_updated = datetime.now() + + def __setattr__(self, name: str, value: Any) -> None: + # Set the given attribute and update the last updated time if the object is + # initialized and the variable name does not have a double underscore in it (to + # exclude private variables, like __initialized, and dunder variables). + super().__setattr__(name, value) + if "__" not in name: + self.__last_updated = datetime.now() + + @property + def last_updated(self) -> datetime: + """When this parameter was last updated.""" + return self.__last_updated + + def to_dict(self) -> dict[str, Any]: + return {"__last_updated": self.__last_updated} | super().to_dict() + + @classmethod + def from_dict(cls, json_dict: dict[str, Any]) -> Self: + last_updated = json_dict.pop("__last_updated") + obj = cls(**json_dict) + obj.__last_updated = last_updated # pylint: disable=unused-private-member + return obj + + +@dataclass +class Struct(_ParamDataclass): + """ + Base class for parameter structures. Custom structures should be subclasses of this + class and are intended to be dataclasses. For example:: + + @dataclass + class CustomStruct(Struct): + value: float + custom_param: CustomParam + """ + + def __post_init__(self) -> None: + # Add fields as children. + for f in fields(self): + self._add_child(getattr(self, f.name)) + + def __setattr__(self, name: str, value: Any) -> None: + old_value = getattr(self, name) if hasattr(self, name) else None + super().__setattr__(name, value) + self._remove_child(old_value) + self._add_child(value) + + @property + def last_updated(self) -> datetime | None: + return self._get_last_updated(getattr(self, f.name) for f in fields(self)) diff --git a/paramdb/_param_data/_param_data.py b/paramdb/_param_data/_param_data.py new file mode 100644 index 0000000..486bc3f --- /dev/null +++ b/paramdb/_param_data/_param_data.py @@ -0,0 +1,134 @@ +"""Base class for all parameter data.""" + +from __future__ import annotations +from typing import Any, cast +from collections.abc import Iterable, Mapping +from abc import ABCMeta, abstractmethod +from weakref import WeakValueDictionary +from datetime import datetime +from typing_extensions import Self + +# Stores weak references to existing parameter classes +_param_classes: WeakValueDictionary[str, type[ParamData]] = WeakValueDictionary() + + +def get_param_class(class_name: str) -> type[ParamData] | None: + """Get a parameter class given its name, or ``None`` if the class does not exist.""" + return _param_classes[class_name] if class_name in _param_classes else None + + +class _ParamClass(ABCMeta): + """ + Metaclass for all parameter data classes. Inherits from ABCMeta to allow for + abstract methods. + """ + + def __new__( + mcs, + name: str, + bases: tuple[type, ...], + namespace: dict[str, Any], + **kwargs: Any, + ) -> _ParamClass: + """ + Construct a new parameter data class and add it to the dictionary of parameter + classes. + """ + param_class = cast( + "type[ParamData]", super().__new__(mcs, name, bases, namespace, **kwargs) + ) + _param_classes[name] = param_class + return param_class + + +class ParamData(metaclass=_ParamClass): + """Abstract base class for all parameter data.""" + + # Most recently initialized structure that contains this parameter data + _parent: ParamData | None = None + + def _add_child(self, child: Any) -> None: + """Add the given object as a child, if it is ``ParamData``.""" + + if isinstance(child, ParamData): + # Use ParamData __setattr__ to avoid updating _last_updated + ParamData.__setattr__(child, "_parent", self) + + def _remove_child(self, child: Any) -> None: + """Remove the given object as a child, if it is ``ParamData``.""" + + if isinstance(child, ParamData): + # Use ParamData __setattr__ to avoid updating _last_updated + ParamData.__setattr__(child, "_parent", None) + + def _get_last_updated(self, obj: Any) -> datetime | None: + """ + Get the last updated time from a ``ParamData`` object, or recursively search + through any iterable type to find the latest last updated time. + """ + if isinstance(obj, ParamData): + return obj.last_updated + if isinstance(obj, Iterable) and not isinstance(obj, str): + # Strings are excluded because they will never contain ParamData and contain + # strings, leading to infinite recursion. + values = obj.values() if isinstance(obj, Mapping) else obj + return max( + filter(None, (self._get_last_updated(v) for v in values)), + default=None, + ) + return None + + @property + @abstractmethod + def last_updated(self) -> datetime | None: + """ + When any parameter within this parameter data were last updated, or ``None`` if + this object contains no parameters. + """ + + @property + def parent(self) -> ParamData: + """ + Parent of this parameter data. The parent is defined to be the + :py:class:`ParamData` object that most recently had this object added as a + child. + + Raises a ``ValueError`` if there is currently no parent, which can occur if the + parent is still being initialized. + """ + if self._parent is None: + raise ValueError( + f"'{type(self).__name__}' object has no parent, or its parent has not" + " been initialized yet" + ) + return self._parent + + @property + def root(self) -> ParamData: + """ + Root of this parameter data. The root is defined to be the first object with no + parent when going up the chain of parents. + """ + root = self + while root._parent is not None: # pylint: disable=protected-access + root = root._parent # pylint: disable=protected-access + return root + + @abstractmethod + def to_dict(self) -> dict[str, Any]: + """ + Convert this parameter data object into a dictionary to be passed to + ``json.dumps``. This dictionary will later be passed to :py:meth:`from_dict` + to reconstruct the object. + + Note that objects within the dictionary do not need to be JSON serializable, + since they will be recursively processed by ``json.dumps``. + """ + + @classmethod + @abstractmethod + def from_dict(cls, json_dict: dict[str, Any]) -> Self: + """ + Construct a parameter data object from the given dictionary, usually created by + `json.loads` and originally constructed by :py:meth:`from_dict`. + """ diff --git a/paramdb/_param_data/_type_mixins.py b/paramdb/_param_data/_type_mixins.py new file mode 100644 index 0000000..76e9c02 --- /dev/null +++ b/paramdb/_param_data/_type_mixins.py @@ -0,0 +1,45 @@ +"""Type mixins for parameter data.""" + +from __future__ import annotations +from typing import TypeVar, Generic, cast +from paramdb._param_data._param_data import ParamData + +PT = TypeVar("PT", bound=ParamData) + + +# pylint: disable-next=abstract-method +class ParentType(ParamData, Generic[PT]): + """ + Mixin for :py:class:`ParamData` that sets the type hint for + :py:attr:`ParamData.parent` to type parameter ``PT``. For example:: + + @dataclass + class CustomParam(ParentType[ParentStruct], Param): + ... + + Note that if the parent actually has a different type, the type hint will be + incorrect. + """ + + @property + def parent(self) -> PT: + return cast(PT, super().parent) + + +# pylint: disable-next=abstract-method +class RootType(ParamData, Generic[PT]): + """ + Mixin for :py:class:`ParamData` that sets the type hint for + :py:attr:`ParamData.root` to type parameter ``PT``. For example:: + + @dataclass + class CustomParam(RootType[RootStruct], Param): + ... + + Note that if the root actually has a different type, the type hint will be + incorrect. + """ + + @property + def root(self) -> PT: + return cast(PT, super().root) diff --git a/poetry.lock b/poetry.lock index 7206e87..7d66472 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry and should not be changed by hand. +# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand. [[package]] name = "alabaster" @@ -26,14 +26,14 @@ files = [ [[package]] name = "astroid" -version = "2.14.2" +version = "2.15.0" description = "An abstract syntax tree for Python with inference support." category = "dev" optional = false python-versions = ">=3.7.2" files = [ - {file = "astroid-2.14.2-py3-none-any.whl", hash = "sha256:0e0e3709d64fbffd3037e4ff403580550f14471fd3eaae9fa11cc9a5c7901153"}, - {file = "astroid-2.14.2.tar.gz", hash = "sha256:a3cf9f02c53dd259144a7e8f3ccd75d67c9a8c716ef183e0c1f291bc5d7bb3cf"}, + {file = "astroid-2.15.0-py3-none-any.whl", hash = "sha256:e3e4d0ffc2d15d954065579689c36aac57a339a4679a679579af6401db4d3fdb"}, + {file = "astroid-2.15.0.tar.gz", hash = "sha256:525f126d5dc1b8b0b6ee398b33159105615d92dc4a17f2cd064125d57f6186fa"}, ] [package.dependencies] @@ -83,19 +83,16 @@ tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy [[package]] name = "babel" -version = "2.11.0" +version = "2.12.1" description = "Internationalization utilities" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "Babel-2.11.0-py3-none-any.whl", hash = "sha256:1ad3eca1c885218f6dce2ab67291178944f810a10a9b5f3cb8382a5a232b64fe"}, - {file = "Babel-2.11.0.tar.gz", hash = "sha256:5ef4b3226b0180dedded4229651c8b0e1a3a6a2837d45a073272f313e4cf97f6"}, + {file = "Babel-2.12.1-py3-none-any.whl", hash = "sha256:b4246fb7677d3b98f501a39d43396d3cafdc8eadb045f4a31be01863f655c610"}, + {file = "Babel-2.12.1.tar.gz", hash = "sha256:cc2d99999cd01d44420ae725a21c9e3711b3aadc7976d6147f622d8581963455"}, ] -[package.dependencies] -pytz = ">=2015.7" - [[package]] name = "backcall" version = "0.2.0" @@ -286,100 +283,87 @@ pycparser = "*" [[package]] name = "charset-normalizer" -version = "3.0.1" +version = "3.1.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.7.0" files = [ - {file = "charset-normalizer-3.0.1.tar.gz", hash = "sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-win32.whl", hash = "sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-win32.whl", hash = "sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-win32.whl", hash = "sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-win32.whl", hash = "sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-win32.whl", hash = "sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-win32.whl", hash = "sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59"}, - {file = "charset_normalizer-3.0.1-py3-none-any.whl", hash = "sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24"}, + {file = "charset-normalizer-3.1.0.tar.gz", hash = "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-win32.whl", hash = "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-win32.whl", hash = "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-win32.whl", hash = "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-win32.whl", hash = "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-win32.whl", hash = "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b"}, + {file = "charset_normalizer-3.1.0-py3-none-any.whl", hash = "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d"}, ] [[package]] @@ -559,14 +543,14 @@ graph = ["objgraph (>=1.7.2)"] [[package]] name = "docutils" -version = "0.18.1" +version = "0.19" description = "Docutils -- Python Documentation Utilities" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.7" files = [ - {file = "docutils-0.18.1-py2.py3-none-any.whl", hash = "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c"}, - {file = "docutils-0.18.1.tar.gz", hash = "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06"}, + {file = "docutils-0.19-py3-none-any.whl", hash = "sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc"}, + {file = "docutils-0.19.tar.gz", hash = "sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6"}, ] [[package]] @@ -631,6 +615,24 @@ mccabe = ">=0.7.0,<0.8.0" pycodestyle = ">=2.10.0,<2.11.0" pyflakes = ">=3.0.0,<3.1.0" +[[package]] +name = "furo" +version = "2022.12.7" +description = "A clean customisable Sphinx documentation theme." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "furo-2022.12.7-py3-none-any.whl", hash = "sha256:7cb76c12a25ef65db85ab0743df907573d03027a33631f17d267e598ebb191f7"}, + {file = "furo-2022.12.7.tar.gz", hash = "sha256:d8008f8efbe7587a97ba533c8b2df1f9c21ee9b3e5cad0d27f61193d38b1a986"}, +] + +[package.dependencies] +beautifulsoup4 = "*" +pygments = ">=2.7" +sphinx = ">=5.0,<7.0" +sphinx-basic-ng = "*" + [[package]] name = "greenlet" version = "2.0.2" @@ -743,14 +745,14 @@ files = [ [[package]] name = "ipykernel" -version = "6.21.2" +version = "6.21.3" description = "IPython Kernel for Jupyter" category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "ipykernel-6.21.2-py3-none-any.whl", hash = "sha256:430d00549b6aaf49bd0f5393150691edb1815afa62d457ee6b1a66b25cb17874"}, - {file = "ipykernel-6.21.2.tar.gz", hash = "sha256:6e9213484e4ce1fb14267ee435e18f23cc3a0634e635b9fb4ed4677b84e0fdf8"}, + {file = "ipykernel-6.21.3-py3-none-any.whl", hash = "sha256:24ebd9715e317c185e37156ab3a87382410185230dde7aeffce389d6c7d4428a"}, + {file = "ipykernel-6.21.3.tar.gz", hash = "sha256:c8ff581905d70e7299bc1473a2f7c113bec1744fb3746d58e5b4b93bd8ee7001"}, ] [package.dependencies] @@ -777,14 +779,14 @@ test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio" [[package]] name = "ipython" -version = "8.10.0" +version = "8.11.0" description = "IPython: Productive Interactive Computing" category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "ipython-8.10.0-py3-none-any.whl", hash = "sha256:b38c31e8fc7eff642fc7c597061fff462537cf2314e3225a19c906b7b0d8a345"}, - {file = "ipython-8.10.0.tar.gz", hash = "sha256:b13a1d6c1f5818bd388db53b7107d17454129a70de2b87481d555daede5eb49e"}, + {file = "ipython-8.11.0-py3-none-any.whl", hash = "sha256:5b54478e459155a326bf5f42ee4f29df76258c0279c36f21d71ddb560f88b156"}, + {file = "ipython-8.11.0.tar.gz", hash = "sha256:735cede4099dbc903ee540307b9171fbfef4aa75cfcacc5a273b2cda2f02be04"}, ] [package.dependencies] @@ -796,7 +798,7 @@ jedi = ">=0.16" matplotlib-inline = "*" pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} pickleshare = "*" -prompt-toolkit = ">=3.0.30,<3.1.0" +prompt-toolkit = ">=3.0.30,<3.0.37 || >3.0.37,<3.1.0" pygments = ">=2.4.0" stack-data = "*" traitlets = ">=5" @@ -1175,14 +1177,14 @@ files = [ [[package]] name = "mdit-py-plugins" -version = "0.3.4" +version = "0.3.5" description = "Collection of plugins for markdown-it-py" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "mdit-py-plugins-0.3.4.tar.gz", hash = "sha256:3278aab2e2b692539082f05e1243f24742194ffd92481f48844f057b51971283"}, - {file = "mdit_py_plugins-0.3.4-py3-none-any.whl", hash = "sha256:4f1441264ac5cb39fa40a5901921c2acf314ea098d75629750c138f80d552cdf"}, + {file = "mdit-py-plugins-0.3.5.tar.gz", hash = "sha256:eee0adc7195e5827e17e02d2a258a2ba159944a0748f59c5099a4a27f78fcf6a"}, + {file = "mdit_py_plugins-0.3.5-py3-none-any.whl", hash = "sha256:ca9a0714ea59a24b2b044a1831f48d817dd0c817e84339f20e7889f392d77c4e"}, ] [package.dependencies] @@ -1278,30 +1280,30 @@ files = [ [[package]] name = "myst-parser" -version = "0.18.1" -description = "An extended commonmark compliant parser, with bridges to docutils & sphinx." +version = "0.19.1" +description = "An extended [CommonMark](https://spec.commonmark.org/) compliant parser," category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "myst-parser-0.18.1.tar.gz", hash = "sha256:79317f4bb2c13053dd6e64f9da1ba1da6cd9c40c8a430c447a7b146a594c246d"}, - {file = "myst_parser-0.18.1-py3-none-any.whl", hash = "sha256:61b275b85d9f58aa327f370913ae1bec26ebad372cc99f3ab85c8ec3ee8d9fb8"}, + {file = "myst-parser-0.19.1.tar.gz", hash = "sha256:f2dc168ed380e01d77973ad22a64fff1377cc72a3d1ac4bced423f28258d0a42"}, + {file = "myst_parser-0.19.1-py3-none-any.whl", hash = "sha256:356b38aef29ed09144285ad222e5c3cb7a8e7fae8015d53dba40dbb8b9f73e2c"}, ] [package.dependencies] docutils = ">=0.15,<0.20" jinja2 = "*" markdown-it-py = ">=1.0.0,<3.0.0" -mdit-py-plugins = ">=0.3.1,<0.4.0" +mdit-py-plugins = ">=0.3.4,<0.4.0" pyyaml = "*" -sphinx = ">=4,<6" -typing-extensions = "*" +sphinx = ">=5,<7" [package.extras] -code-style = ["pre-commit (>=2.12,<3.0)"] +code-style = ["pre-commit (>=3.0,<4.0)"] linkify = ["linkify-it-py (>=1.0,<2.0)"] -rtd = ["ipython", "sphinx-book-theme", "sphinx-design", "sphinxcontrib.mermaid (>=0.7.1,<0.8.0)", "sphinxext-opengraph (>=0.6.3,<0.7.0)", "sphinxext-rediraffe (>=0.2.7,<0.3.0)"] -testing = ["beautifulsoup4", "coverage[toml]", "pytest (>=6,<7)", "pytest-cov", "pytest-param-files (>=0.3.4,<0.4.0)", "pytest-regressions", "sphinx (<5.2)", "sphinx-pytest"] +rtd = ["ipython", "pydata-sphinx-theme (==v0.13.0rc4)", "sphinx-autodoc2 (>=0.4.2,<0.5.0)", "sphinx-book-theme (==1.0.0rc2)", "sphinx-copybutton", "sphinx-design2", "sphinx-pyscript", "sphinx-tippy (>=0.3.1)", "sphinx-togglebutton", "sphinxext-opengraph (>=0.7.5,<0.8.0)", "sphinxext-rediraffe (>=0.2.7,<0.3.0)"] +testing = ["beautifulsoup4", "coverage[toml]", "pytest (>=7,<8)", "pytest-cov", "pytest-param-files (>=0.3.4,<0.4.0)", "pytest-regressions", "sphinx-pytest"] +testing-docutils = ["pygments", "pytest (>=7,<8)", "pytest-param-files (>=0.3.4,<0.4.0)"] [[package]] name = "nbclient" @@ -1479,14 +1481,14 @@ files = [ [[package]] name = "platformdirs" -version = "3.0.0" +version = "3.1.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "platformdirs-3.0.0-py3-none-any.whl", hash = "sha256:b1d5eb14f221506f50d6604a561f4c5786d9e80355219694a1b244bcd96f4567"}, - {file = "platformdirs-3.0.0.tar.gz", hash = "sha256:8a1228abb1ef82d788f74139988b137e78692984ec7b08eaa6c65f1723af28f9"}, + {file = "platformdirs-3.1.0-py3-none-any.whl", hash = "sha256:13b08a53ed71021350c9e300d4ea8668438fb0046ab3937ac9a29913a1a1350a"}, + {file = "platformdirs-3.1.0.tar.gz", hash = "sha256:accc3665857288317f32c7bebb5a8e482ba717b474f3fc1d18ca7f9214be0cef"}, ] [package.extras] @@ -1511,14 +1513,14 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "prompt-toolkit" -version = "3.0.37" +version = "3.0.38" description = "Library for building powerful interactive command lines in Python" category = "dev" optional = false python-versions = ">=3.7.0" files = [ - {file = "prompt_toolkit-3.0.37-py3-none-any.whl", hash = "sha256:6a2948ec427dfcc7c983027b1044b355db6aaa8be374f54ad2015471f7d81c5b"}, - {file = "prompt_toolkit-3.0.37.tar.gz", hash = "sha256:d5d73d4b5eb1a92ba884a88962b157f49b71e06c4348b417dd622b25cdd3800b"}, + {file = "prompt_toolkit-3.0.38-py3-none-any.whl", hash = "sha256:45ea77a2f7c60418850331366c81cf6b5b9cf4c7fd34616f733c5427e6abbb1f"}, + {file = "prompt_toolkit-3.0.38.tar.gz", hash = "sha256:23ac5d50538a9a38c8bde05fecb47d0b403ecd0662857a86f886f798563d5b9b"}, ] [package.dependencies] @@ -1631,14 +1633,14 @@ plugins = ["importlib-metadata"] [[package]] name = "pylint" -version = "2.16.2" +version = "2.16.4" description = "python code static checker" category = "dev" optional = false python-versions = ">=3.7.2" files = [ - {file = "pylint-2.16.2-py3-none-any.whl", hash = "sha256:ff22dde9c2128cd257c145cfd51adeff0be7df4d80d669055f24a962b351bbe4"}, - {file = "pylint-2.16.2.tar.gz", hash = "sha256:13b2c805a404a9bf57d002cd5f054ca4d40b0b87542bdaba5e05321ae8262c84"}, + {file = "pylint-2.16.4-py3-none-any.whl", hash = "sha256:4a770bb74fde0550fa0ab4248a2ad04e7887462f9f425baa0cd8d3c1d098eaee"}, + {file = "pylint-2.16.4.tar.gz", hash = "sha256:8841f26a0dbc3503631b6a20ee368b3f5e0e5461a1d95cf15d103dab748a0db3"}, ] [package.dependencies] @@ -1697,14 +1699,14 @@ files = [ [[package]] name = "pytest" -version = "7.2.1" +version = "7.2.2" description = "pytest: simple powerful testing with Python" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.2.1-py3-none-any.whl", hash = "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5"}, - {file = "pytest-7.2.1.tar.gz", hash = "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42"}, + {file = "pytest-7.2.2-py3-none-any.whl", hash = "sha256:130328f552dcfac0b1cec75c12e3f005619dc5f874f0a06e8ff7263f0ee6225e"}, + {file = "pytest-7.2.2.tar.gz", hash = "sha256:c99ab0c73aceb050f68929bc93af19ab6db0558791c6a0715723abe9d0ade9d4"}, ] [package.dependencies] @@ -1734,18 +1736,6 @@ files = [ [package.dependencies] six = ">=1.5" -[[package]] -name = "pytz" -version = "2022.7.1" -description = "World timezone definitions, modern and historical" -category = "dev" -optional = false -python-versions = "*" -files = [ - {file = "pytz-2022.7.1-py2.py3-none-any.whl", hash = "sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a"}, - {file = "pytz-2022.7.1.tar.gz", hash = "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0"}, -] - [[package]] name = "pywin32" version = "305" @@ -1932,23 +1922,6 @@ urllib3 = ">=1.21.1,<1.27" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] -[[package]] -name = "setuptools" -version = "67.4.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "setuptools-67.4.0-py3-none-any.whl", hash = "sha256:f106dee1b506dee5102cc3f3e9e68137bbad6d47b616be7991714b0c62204251"}, - {file = "setuptools-67.4.0.tar.gz", hash = "sha256:e5fd0a713141a4a105412233c63dc4e17ba0090c8e8334594ac790ec97792330"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] - [[package]] name = "six" version = "1.16.0" @@ -2041,43 +2014,41 @@ sphinx = "*" test = ["pytest", "pytest-cov"] [[package]] -name = "sphinx-copybutton" -version = "0.5.1" -description = "Add a copy button to each of your code cells." +name = "sphinx-basic-ng" +version = "1.0.0b1" +description = "A modern skeleton for Sphinx themes." category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "sphinx-copybutton-0.5.1.tar.gz", hash = "sha256:366251e28a6f6041514bfb5439425210418d6c750e98d3a695b73e56866a677a"}, - {file = "sphinx_copybutton-0.5.1-py3-none-any.whl", hash = "sha256:0842851b5955087a7ec7fc870b622cb168618ad408dee42692e9a5c97d071da8"}, + {file = "sphinx_basic_ng-1.0.0b1-py3-none-any.whl", hash = "sha256:ade597a3029c7865b24ad0eda88318766bcc2f9f4cef60df7e28126fde94db2a"}, + {file = "sphinx_basic_ng-1.0.0b1.tar.gz", hash = "sha256:89374bd3ccd9452a301786781e28c8718e99960f2d4f411845ea75fc7bb5a9b0"}, ] [package.dependencies] -sphinx = ">=1.8" +sphinx = ">=4.0" [package.extras] -code-style = ["pre-commit (==2.12.1)"] -rtd = ["ipython", "myst-nb", "sphinx", "sphinx-book-theme", "sphinx-examples"] +docs = ["furo", "ipython", "myst-parser", "sphinx-copybutton", "sphinx-inline-tabs"] [[package]] -name = "sphinx-rtd-theme" -version = "1.2.0" -description = "Read the Docs theme for Sphinx" +name = "sphinx-copybutton" +version = "0.5.1" +description = "Add a copy button to each of your code cells." category = "dev" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +python-versions = ">=3.7" files = [ - {file = "sphinx_rtd_theme-1.2.0-py2.py3-none-any.whl", hash = "sha256:f823f7e71890abe0ac6aaa6013361ea2696fc8d3e1fa798f463e82bdb77eeff2"}, - {file = "sphinx_rtd_theme-1.2.0.tar.gz", hash = "sha256:a0d8bd1a2ed52e0b338cbe19c4b2eef3c5e7a048769753dac6a9f059c7b641b8"}, + {file = "sphinx-copybutton-0.5.1.tar.gz", hash = "sha256:366251e28a6f6041514bfb5439425210418d6c750e98d3a695b73e56866a677a"}, + {file = "sphinx_copybutton-0.5.1-py3-none-any.whl", hash = "sha256:0842851b5955087a7ec7fc870b622cb168618ad408dee42692e9a5c97d071da8"}, ] [package.dependencies] -docutils = "<0.19" -sphinx = ">=1.6,<7" -sphinxcontrib-jquery = {version = ">=2.0.0,<3.0.0 || >3.0.0", markers = "python_version > \"3\""} +sphinx = ">=1.8" [package.extras] -dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client", "wheel"] +code-style = ["pre-commit (==2.12.1)"] +rtd = ["ipython", "myst-nb", "sphinx", "sphinx-book-theme", "sphinx-examples"] [[package]] name = "sphinxcontrib-applehelp" @@ -2127,21 +2098,6 @@ files = [ lint = ["docutils-stubs", "flake8", "mypy"] test = ["html5lib", "pytest"] -[[package]] -name = "sphinxcontrib-jquery" -version = "2.0.0" -description = "Extension to include jQuery on newer Sphinx releases" -category = "dev" -optional = false -python-versions = ">=2.7" -files = [ - {file = "sphinxcontrib-jquery-2.0.0.tar.gz", hash = "sha256:8fb65f6dba84bf7bcd1aea1f02ab3955ac34611d838bcc95d4983b805b234daa"}, - {file = "sphinxcontrib_jquery-2.0.0-py3-none-any.whl", hash = "sha256:ed47fa425c338ffebe3c37e1cdb56e30eb806116b85f01055b158c7057fdb995"}, -] - -[package.dependencies] -setuptools = "*" - [[package]] name = "sphinxcontrib-jsmath" version = "1.0.1" @@ -2191,53 +2147,53 @@ test = ["pytest"] [[package]] name = "sqlalchemy" -version = "2.0.4" +version = "2.0.5.post1" description = "Database Abstraction Library" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "SQLAlchemy-2.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b67d6e626caa571fb53accaac2fba003ef4f7317cb3481e9ab99dad6e89a70d6"}, - {file = "SQLAlchemy-2.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b01dce097cf6f145da131a53d4cce7f42e0bfa9ae161dd171a423f7970d296d0"}, - {file = "SQLAlchemy-2.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:738c80705e11c1268827dbe22c01162a9cdc98fc6f7901b429a1459db2593060"}, - {file = "SQLAlchemy-2.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6363697c938b9a13e07f1bc2cd433502a7aa07efd55b946b31d25b9449890621"}, - {file = "SQLAlchemy-2.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a42e6831e82dfa6d16b45f0c98c69e7b0defc64d76213173456355034450c414"}, - {file = "SQLAlchemy-2.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:011ef3c33f30bae5637c575f30647e0add98686642d237f0c3a1e3d9b35747fa"}, - {file = "SQLAlchemy-2.0.4-cp310-cp310-win32.whl", hash = "sha256:c1e8edc49b32483cd5d2d015f343e16be7dfab89f4aaf66b0fa6827ab356880d"}, - {file = "SQLAlchemy-2.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:77a380bf8721b416782c763e0ff66f80f3b05aee83db33ddfc0eac20bcb6791f"}, - {file = "SQLAlchemy-2.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2a2f9120eb32190bdba31d1022181ef08f257aed4f984f3368aa4e838de72bc0"}, - {file = "SQLAlchemy-2.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:679b9bd10bb32b8d3befed4aad4356799b6ec1bdddc0f930a79e41ba5b084124"}, - {file = "SQLAlchemy-2.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:582053571125895d008d4b8d9687d12d4bd209c076cdbab3504da307e2a0a2bd"}, - {file = "SQLAlchemy-2.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c82395e2925639e6d320592943608070678e7157bd1db2672a63be9c7889434"}, - {file = "SQLAlchemy-2.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:25e4e54575f9d2af1eab82d3a470fca27062191c48ee57b6386fe09a3c0a6a33"}, - {file = "SQLAlchemy-2.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9946ee503962859f1a9e1ad17dff0859269b0cb453686747fe87f00b0e030b34"}, - {file = "SQLAlchemy-2.0.4-cp311-cp311-win32.whl", hash = "sha256:c621f05859caed5c0aab032888a3d3bde2cae3988ca151113cbecf262adad976"}, - {file = "SQLAlchemy-2.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:662a79e80f3e9fe33b7861c19fedf3d8389fab2413c04bba787e3f1139c22188"}, - {file = "SQLAlchemy-2.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3f927340b37fe65ec42e19af7ce15260a73e11c6b456febb59009bfdfec29a35"}, - {file = "SQLAlchemy-2.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67901b91bf5821482fcbe9da988cb16897809624ddf0fde339cd62365cc50032"}, - {file = "SQLAlchemy-2.0.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1644c603558590f465b3fa16e4557d87d3962bc2c81fd7ea85b582ecf4676b31"}, - {file = "SQLAlchemy-2.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:9a7ecaf90fe9ec8e45c86828f4f183564b33c9514e08667ca59e526fea63893a"}, - {file = "SQLAlchemy-2.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8a88b32ce5b69d18507ffc9f10401833934ebc353c7b30d1e056023c64f0a736"}, - {file = "SQLAlchemy-2.0.4-cp37-cp37m-win32.whl", hash = "sha256:2267c004e78e291bba0dc766a9711c389649cf3e662cd46eec2bc2c238c637bd"}, - {file = "SQLAlchemy-2.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:59cf0cdb29baec4e074c7520d7226646a8a8f856b87d8300f3e4494901d55235"}, - {file = "SQLAlchemy-2.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:dd801375f19a6e1f021dabd8b1714f2fdb91cbc835cd13b5dd0bd7e9860392d7"}, - {file = "SQLAlchemy-2.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d8efdda920988bcade542f53a2890751ff680474d548f32df919a35a21404e3f"}, - {file = "SQLAlchemy-2.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:918c2b553e3c78268b187f70983c9bc6f91e451a4f934827e9c919e03d258bd7"}, - {file = "SQLAlchemy-2.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d05773d5c79f2d3371d81697d54ee1b2c32085ad434ce9de4482e457ecb018"}, - {file = "SQLAlchemy-2.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:fdb2686eb01f670cdc6c43f092e333ff08c1cf0b646da5256c1237dc4ceef4ae"}, - {file = "SQLAlchemy-2.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8ff0a7c669ec7cdb899eae7e622211c2dd8725b82655db2b41740d39e3cda466"}, - {file = "SQLAlchemy-2.0.4-cp38-cp38-win32.whl", hash = "sha256:57dcd9eed52413f7270b22797aa83c71b698db153d1541c1e83d45ecdf8e95e7"}, - {file = "SQLAlchemy-2.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:54aa9f40d88728dd058e951eeb5ecc55241831ba4011e60c641738c1da0146b7"}, - {file = "SQLAlchemy-2.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:817aab80f7e8fe581696dae7aaeb2ceb0b7ea70ad03c95483c9115970d2a9b00"}, - {file = "SQLAlchemy-2.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dc7b9f55c2f72c13b2328b8a870ff585c993ba1b5c155ece5c9d3216fa4b18f6"}, - {file = "SQLAlchemy-2.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f696828784ab2c07b127bfd2f2d513f47ec58924c29cff5b19806ac37acee31c"}, - {file = "SQLAlchemy-2.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce54965a94673a0ebda25e7c3a05bf1aa74fd78cc452a1a710b704bf73fb8402"}, - {file = "SQLAlchemy-2.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f342057422d6bcfdd4996e34cd5c7f78f7e500112f64b113f334cdfc6a0c593d"}, - {file = "SQLAlchemy-2.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b5deafb4901618b3f98e8df7099cd11edd0d1e6856912647e28968b803de0dae"}, - {file = "SQLAlchemy-2.0.4-cp39-cp39-win32.whl", hash = "sha256:81f1ea264278fcbe113b9a5840f13a356cb0186e55b52168334124f1cd1bc495"}, - {file = "SQLAlchemy-2.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:954f1ad73b78ea5ba5a35c89c4a5dfd0f3a06c17926503de19510eb9b3857bde"}, - {file = "SQLAlchemy-2.0.4-py3-none-any.whl", hash = "sha256:0adca8a3ca77234a142c5afed29322fb501921f13d1d5e9fa4253450d786c160"}, - {file = "SQLAlchemy-2.0.4.tar.gz", hash = "sha256:95a18e1a6af2114dbd9ee4f168ad33070d6317e11bafa28d983cc7b585fe900b"}, + {file = "SQLAlchemy-2.0.5.post1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7fe933831e17f93947b4e3db4e4a7470dae24340f269baf06cdfcc0538c8d1cb"}, + {file = "SQLAlchemy-2.0.5.post1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5c00d2b3607f9ae5c0827ebb2d01020c26cfce4064aa664db21d1fd6a47f8f60"}, + {file = "SQLAlchemy-2.0.5.post1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c12f0455f30d637644f4d6df2bda1475c61398483edb58d55c670be31a31d549"}, + {file = "SQLAlchemy-2.0.5.post1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7e55659740e768a1bf1257275b565137a3d28839789c85193dd6a1e642b3cc9"}, + {file = "SQLAlchemy-2.0.5.post1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:15c92107437770087ad4fece6ed9553ab97474f3b92d15eb62cea9686228f252"}, + {file = "SQLAlchemy-2.0.5.post1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:50b15fce441b8eb13bfd06df1088aa52c9a3d72d4894e3456040857d48a622da"}, + {file = "SQLAlchemy-2.0.5.post1-cp310-cp310-win32.whl", hash = "sha256:ffda70373ddfe8ec733d518e4e41eb5599480d48e8496c44bb0cac0e37b281c0"}, + {file = "SQLAlchemy-2.0.5.post1-cp310-cp310-win_amd64.whl", hash = "sha256:e40c39cfcbe416a7722a226ecd98fad0e08f8ae33e8f94b0858afe094583bfbc"}, + {file = "SQLAlchemy-2.0.5.post1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:22f60d214899b573edc8aeb9ba84f7e832505511ce68974636e6da4a27c957a3"}, + {file = "SQLAlchemy-2.0.5.post1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4756878d0ceb6e0e9c6cfcfaa9df81adbfcca8cc4b9ec37934918008c0f20507"}, + {file = "SQLAlchemy-2.0.5.post1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6df48bb7af217fd086617aae1f9606ff91cfab9a29c3e77dd80e4bab8aaf29fc"}, + {file = "SQLAlchemy-2.0.5.post1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1012c0440108c360b94f43667525365c43516e8c7f1f7de8dfb73471181055df"}, + {file = "SQLAlchemy-2.0.5.post1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2bffc27ec0386ef4af7c8923f0f809f88671859b907c9e11f000c39b97195e99"}, + {file = "SQLAlchemy-2.0.5.post1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8e7d5766fb99743eb70126daaff45f43bee3f4b79ba6047a0749912e8538f0ff"}, + {file = "SQLAlchemy-2.0.5.post1-cp311-cp311-win32.whl", hash = "sha256:c2c41bf05b4cf4ffead35896affa3b457c17755d0fd83b1ba72f7f55abb3a3f1"}, + {file = "SQLAlchemy-2.0.5.post1-cp311-cp311-win_amd64.whl", hash = "sha256:56d38c3638965df5257ac4648ba2887aaf1e3409397192dd85ddfe7b96dc7680"}, + {file = "SQLAlchemy-2.0.5.post1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:68c4c5ab13997fa7af37d5780da11ddc184d4e88fb2d8a26525044c233f03bc7"}, + {file = "SQLAlchemy-2.0.5.post1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:869ca02661f6d4cece5823997a364dfa97601de11151fca3ebc3429eb9ffa2e0"}, + {file = "SQLAlchemy-2.0.5.post1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6cb241c5c1af422c0fa742bd09a8eaf158da1433617ded1ffcbb56de6ff8047"}, + {file = "SQLAlchemy-2.0.5.post1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:9bb63e22ddf01cbbb290e61f31471480d2e40283e558cdd924b94dc4fc2e186b"}, + {file = "SQLAlchemy-2.0.5.post1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2c3869a7bdd5bb76fb50976abe339e30cce8e9f7c778a50811d310ec82cdf51a"}, + {file = "SQLAlchemy-2.0.5.post1-cp37-cp37m-win32.whl", hash = "sha256:1edb6621782f9a3e80750ba1859580b778a424242d4e6b9bcd46fa6beca75c12"}, + {file = "SQLAlchemy-2.0.5.post1-cp37-cp37m-win_amd64.whl", hash = "sha256:3c4e673da09af37b7a5c13b947fdb387c3800d43dcd86c1d553e3c70369e4749"}, + {file = "SQLAlchemy-2.0.5.post1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac12d8bef707d02ef179f0586c848db2954668dca2b72c69df950e08dc8cddb4"}, + {file = "SQLAlchemy-2.0.5.post1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e13cea675937125417953fd011138f13cf979051567f48074fffb3bb0b64b917"}, + {file = "SQLAlchemy-2.0.5.post1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:744b01fcdfef66b7373262bf75d714a4339f85c05b74b1732749f25ed65f33f6"}, + {file = "SQLAlchemy-2.0.5.post1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03cd5709839fc376538f102c58f632d48bd0b92715bd290c3b2c066e0dd0f214"}, + {file = "SQLAlchemy-2.0.5.post1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7fab3d4062472e1e6002bfcd53cc7446189941be083a5465760aa794092004ee"}, + {file = "SQLAlchemy-2.0.5.post1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:059ddd4ddbfb8f1c872d601b168273dfaab0eae458736c7c754187b9a8e92ad5"}, + {file = "SQLAlchemy-2.0.5.post1-cp38-cp38-win32.whl", hash = "sha256:9a4d9a7e9b344bf8ce2ed699baa8a43d9fbdad3aecff259f1d0daf6bb2e7e0c0"}, + {file = "SQLAlchemy-2.0.5.post1-cp38-cp38-win_amd64.whl", hash = "sha256:b52be78c5e86ade646c82a10b2be4b6ed8f623052b4405b26681880df1a15d5a"}, + {file = "SQLAlchemy-2.0.5.post1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e33867e09820c98630f7faec535a8cc4116fd362787404b41883d373437290b"}, + {file = "SQLAlchemy-2.0.5.post1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0433abeb650c72c872e31010bff8536907fb05f6fa29a9b880046570c03383ca"}, + {file = "SQLAlchemy-2.0.5.post1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d81b2fa605939c437f8b0b8522ec2c19508f3036d6043cb70a15dd56760ab710"}, + {file = "SQLAlchemy-2.0.5.post1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d811b97f58d99e5948752087903cb414fd77a60a5e09293be16c924219178c3b"}, + {file = "SQLAlchemy-2.0.5.post1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:adf597e756e27173be57f243cc17bea7af1ac74b35c0120aace2738f59c92a48"}, + {file = "SQLAlchemy-2.0.5.post1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f53542654124d30a3c3ebff9f99e5749add75e4cf28895a2ca6cd1458039bd8f"}, + {file = "SQLAlchemy-2.0.5.post1-cp39-cp39-win32.whl", hash = "sha256:b21694c8543becc2bc4f05f8d07970860e6ad005024712b7195abb1f4e0daf47"}, + {file = "SQLAlchemy-2.0.5.post1-cp39-cp39-win_amd64.whl", hash = "sha256:72e8d65b20147df71297d863981d8e56e429a8ae2bb835bd5e89efd7ef849866"}, + {file = "SQLAlchemy-2.0.5.post1-py3-none-any.whl", hash = "sha256:621e92ace804e19da2e472e349736d7ba5e2e4a14d41c4de9e2474e5f40a11ed"}, + {file = "SQLAlchemy-2.0.5.post1.tar.gz", hash = "sha256:13eb2a5882cfd9f4eedaaec14a5603a096f0125f7c3cb48611b3bfa3c253f25d"}, ] [package.dependencies] @@ -2587,4 +2543,4 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "c837df80e08540d7e9aafc0973e6b53ff3c39cb8b54e44a3280549ffa4c376ac" +content-hash = "23b88725ad7d4e6ccf7b7d2828a51a3f5b82f5ac745fdb2267b263d0fa0da42c" diff --git a/pyproject.toml b/pyproject.toml index c78afdc..1dd9278 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "paramdb" -version = "0.1.0" +version = "0.2.0" description = "Python library for storing and retrieving experiment parameters." authors = ["Alex Hadley "] license = "BSD-3-Clause" @@ -9,12 +9,13 @@ repository = "https://github.com/PainterQubits/paramdb" [tool.poetry.dependencies] python = "^3.10" -sqlalchemy = "^2.0.3" +typing-extensions = "^4.5.0" +sqlalchemy = "^2.0.5" zstandard = "^0.20.0" [tool.poetry.group.dev.dependencies] -pytest = "^7.2.1" -pylint = "^2.16.2" +pytest = "^7.2.2" +pylint = "^2.16.4" flake8 = "^6.0.0" mypy = "^1.0.0" black = "^23.1.0" @@ -23,18 +24,22 @@ coverage = "^7.2.1" [tool.poetry.group.docs.dependencies] sphinx = "^5.3.0" sphinx-autobuild = "^2021.3.14" -myst-parser = "^0.18.1" -sphinx-rtd-theme = "^1.2.0" +myst-parser = "^0.19.1" +furo = "^2022.12.7" sphinx-copybutton = "^0.5.1" jupyter-sphinx = "^0.4.0" + [tool.pytest.ini_options] # See https://docs.pytest.org/en/latest/explanation/goodpractices.html#tests-outside-application-code addopts = ["--import-mode=importlib"] [tool.pylint.basic] -# Allow one and two character variable names (e.g. "id"). -good-names-rgxs = ["^[a-z][a-z]?$"] +# Allow one and two character variable names (e.g. "id" or "p1"). +good-names-rgxs = ["^[a-z][a-z0-9]?$"] + +[tool.pylint.messages_control] +disable = ["too-few-public-methods"] [tool.pylint.typecheck] # Pylint has trouble with SQLAlchemy sessionmaker (see https://github.com/PyCQA/pylint/issues/7090) diff --git a/tests/_param_data/test_collections.py b/tests/_param_data/test_collections.py new file mode 100644 index 0000000..563ecfc --- /dev/null +++ b/tests/_param_data/test_collections.py @@ -0,0 +1,402 @@ +"""Tests for the paramdb._param_data._collections module.""" + +from typing import Any +from copy import deepcopy +from datetime import datetime +import pytest +from tests.helpers import CustomParamList, CustomParamDict +from paramdb import ParamData, ParamList, ParamDict + +ParamCollection = ParamList | ParamDict +Contents = list[Any] | dict[str, Any] +CustomParamCollection = CustomParamList | CustomParamDict + + +@pytest.fixture( + name="param_collection", + params=["param_list", "param_dict"], +) +def fixture_param_collection(request: pytest.FixtureRequest) -> ParamCollection: + """Parameter collection.""" + param_collection: ParamCollection = deepcopy(request.getfixturevalue(request.param)) + return param_collection + + +@pytest.fixture(name="param_collection_type") +def fixture_param_collection_type( + param_collection: ParamCollection, +) -> type[ParamCollection]: + """Type of the parameter collection.""" + return type(param_collection) + + +@pytest.fixture(name="contents") +def fixture_contents( + param_collection: ParamCollection, request: pytest.FixtureRequest +) -> Contents: + """Contents of the parameter collection.""" + contents: Contents + if isinstance(param_collection, ParamList): + contents = request.getfixturevalue("param_list_contents") + elif isinstance(param_collection, ParamDict): + contents = request.getfixturevalue("param_dict_contents") + return deepcopy(contents) + + +@pytest.fixture(name="contents_type") +def fixture_contents_type(contents: Contents) -> type[Contents]: + """Type of the parameter collection contents.""" + return type(contents) + + +@pytest.fixture(name="custom_param_collection_type") +def fixture_custom_param_collection_type( + param_collection: ParamCollection, +) -> type[CustomParamCollection]: + """Custom parameter collection subclass.""" + if isinstance(param_collection, ParamList): + return CustomParamList + if isinstance(param_collection, ParamDict): + return CustomParamDict + + +@pytest.fixture(name="custom_param_collection") +def fixture_param_collection_subclass( + custom_param_collection_type: type[CustomParamCollection], contents: Any +) -> CustomParamCollection: + """Custom parameter collection object.""" + return custom_param_collection_type(deepcopy(contents)) + + +def test_param_collection_init_empty( + param_collection_type: type[ParamCollection], + contents_type: type[Contents], +) -> None: + """Can initialize an empty parameter collection.""" + assert not contents_type(param_collection_type()) + + +def test_param_list_init( + param_list: ParamList[Any], param_list_contents: list[Any] +) -> None: + """ + Can initialize a parameter list from a list, and its children correctly identify it + as the parent. + """ + assert list(param_list) == param_list_contents + for item in param_list: + if isinstance(item, ParamData): + assert item.parent is param_list + + +def test_param_dict_init( + param_dict: ParamDict[Any], param_dict_contents: dict[str, Any] +) -> None: + """ + Can initialize a parameter dictionary from a dictionary, and its children correctly + identify it as the parent. + """ + assert dict(param_dict) == param_dict_contents + for item in param_dict.values(): + if isinstance(item, ParamData): + assert item.parent is param_dict + + +def test_param_collection_len_empty( + param_collection_type: type[ParamCollection], +) -> None: + """Can get the length of an empty parameter collection.""" + assert len(param_collection_type()) == 0 + + +def test_param_collection_len_nonempty( + param_collection: ParamCollection, + contents: Contents, +) -> None: + """Can get the length of a nonempty parameter collection.""" + assert len(param_collection) == len(contents) + + +def test_param_collection_eq( + param_collection_type: type[ParamCollection], + param_collection: ParamCollection, + contents: Any, +) -> None: + """ + Two parameter collections are equal if they have the same class and contents. + """ + assert param_collection == param_collection_type(contents) + + +def test_param_collection_neq_contents( + param_collection_type: type[ParamCollection], + param_collection: ParamCollection, +) -> None: + """ + Two parameter collections are not equal if they have the same class but different + contents. + """ + assert param_collection != param_collection_type() + + +def test_param_collection_neq_class( + contents_type: type[Contents], + param_collection: ParamCollection, + custom_param_collection: CustomParamCollection, + contents: Contents, +) -> None: + """ + Two parameter collections are not equal if they have the same contents but different + classes. + """ + assert contents_type(param_collection) == contents_type(custom_param_collection) + assert param_collection != custom_param_collection + assert param_collection != contents + + +def test_param_collection_repr( + param_collection_type: type[ParamCollection], + param_collection: ParamCollection, + contents: Any, +) -> None: + """Parameter collection can be represented as a string.""" + assert repr(param_collection) == f"{param_collection_type.__name__}({contents})" + + +def test_param_collection_repr_subclass( + custom_param_collection_type: type[CustomParamCollection], + custom_param_collection: CustomParamCollection, + contents: Any, +) -> None: + """Custom parameter collection can be represented as a string.""" + assert ( + repr(custom_param_collection) + == f"{custom_param_collection_type.__name__}({contents})" + ) + + +def test_param_collection_no_last_updated( + param_collection_type: type[ParamCollection], +) -> None: + """Empty parameter collection has no last updated time.""" + assert param_collection_type().last_updated is None + + +def test_param_list_last_updated( + param_list: ParamList[Any], + updated_param_data: ParamData, + start: datetime, + end: datetime, +) -> None: + """Parameter list can correctly get the last updated time from its contents.""" + param_list.append(updated_param_data) + assert param_list.last_updated is not None + assert start < param_list.last_updated < end + + +def test_param_dict_last_updated( + param_dict: ParamDict[Any], + updated_param_data: ParamData, + start: datetime, + end: datetime, +) -> None: + """Parameter list can correctly get the last updated time from its contents.""" + param_dict["param_data"] = updated_param_data + assert param_dict.last_updated is not None + assert start < param_dict.last_updated < end + + +def test_param_list_get_index( + param_list: ParamList[Any], param_list_contents: list[Any] +) -> None: + """Can get an item by index from a parameter list.""" + assert param_list[0] == param_list_contents[0] + + +def test_param_list_get_index_parent(param_list: ParamList[Any]) -> None: + """Items gotten from a parameter list via an index have the correct parent.""" + assert param_list[2].parent is param_list + + +def test_param_list_get_slice( + param_list: ParamList[Any], param_list_contents: list[Any] +) -> None: + """Can get an item by slice from a parameter list.""" + assert isinstance(param_list[0:2], list) + assert param_list[0:2] == param_list_contents[0:2] + + +def test_param_list_get_slice_parent(param_list: ParamList[Any]) -> None: + """Items gotten from a parameter list via a slice have the correct parent.""" + sublist = param_list[2:4] + assert sublist[0].parent is param_list + assert sublist[1].parent is param_list + + +def test_param_list_set_index(param_list: ParamList[Any]) -> None: + """Can set an item by index in a parameter list.""" + new_number = 4.56 + assert param_list[0] != new_number + param_list[0] = new_number + assert param_list[0] == new_number + + +def test_param_list_set_index_parent( + param_list: ParamList[Any], param_data: ParamData +) -> None: + """Parameter data added to a parameter list via indexing has the correct parent.""" + with pytest.raises(ValueError): + _ = param_data.parent + for _ in range(2): # Run twice to check reassigning the same parameter data + param_list[0] = param_data + assert param_data.parent is param_list + param_list[0] = None + with pytest.raises(ValueError): + _ = param_data.parent + + +def test_param_list_set_slice(param_list: ParamList[Any]) -> None: + """Can set items by slice in a parameter list.""" + new_numbers = [4.56, 7.89] + assert param_list[0:2] != new_numbers + param_list[0:2] = new_numbers + assert param_list[0:2] == new_numbers + + +def test_param_list_set_slice_parent( + param_list: ParamList[Any], param_data: ParamData +) -> None: + """Parameter data added to a parameter list via slicing has the correct parent.""" + for _ in range(2): # Run twice to check reassigning the same parameter data + param_list[0:2] = [None, param_data] + assert param_data.parent is param_list + param_list[0:2] = [] + with pytest.raises(ValueError): + _ = param_data.parent + + +def test_param_list_insert(param_list: ParamList[Any]) -> None: + """Can insert an item into a parameter list.""" + new_number = 4.56 + param_list.insert(1, new_number) + assert param_list[1] == new_number + + +def test_param_list_insert_parent( + param_list: ParamList[Any], param_data: ParamData +) -> None: + """Parameter data added to a parameter list via insertion has the correct parent.""" + param_list.insert(1, param_data) + assert param_data.parent is param_list + + +def test_param_list_del( + param_list: ParamList[Any], param_list_contents: list[Any] +) -> None: + """Can delete an item from a parameter list.""" + assert list(param_list) == param_list_contents + del param_list[0] + assert list(param_list) == param_list_contents[1:] + + +def test_param_list_del_parent( + param_list: ParamList[Any], param_data: ParamData +) -> None: + """An item deleted from a parameter list has no parent.""" + param_list.append(param_data) + assert param_data.parent is param_list + del param_list[-1] + with pytest.raises(ValueError): + _ = param_data.parent + + +def test_param_dict_key_error(param_dict: ParamDict[Any]) -> None: + """Getting or deleting a nonexistent key raises a KeyError.""" + with pytest.raises(KeyError): + _ = param_dict["nonexistent"] + with pytest.raises(KeyError): + del param_dict["nonexistent"] + with pytest.raises(KeyError): + _ = param_dict.nonexistent + with pytest.raises(KeyError): + del param_dict.nonexistent + + +def test_param_dict_attribute_error(param_dict: ParamDict[Any]) -> None: + """Getting or deleting a nonexistent attribute raises an AttributeError.""" + with pytest.raises(AttributeError): + _ = param_dict._nonexistent # pylint: disable=protected-access + with pytest.raises(AttributeError): + del param_dict._nonexistent # pylint: disable=protected-access + + +def test_param_dict_get( + param_dict: ParamDict[Any], param_dict_contents: dict[str, Any] +) -> None: + """ + Can get an item from a parameter dictionary using index bracket or dot notation. + """ + assert param_dict["number"] == param_dict_contents["number"] + assert param_dict.number == param_dict_contents["number"] + + +def test_param_dict_get_parent(param_dict: ParamDict[Any]) -> None: + """An item gotten from a parameter dictionary has the correct parent.""" + assert param_dict["param"].parent is param_dict + assert param_dict.param.parent is param_dict + + +def test_param_dict_set(param_dict: ParamDict[Any]) -> None: + """Can set an item in a parameter list.""" + new_number_1 = 4.56 + new_number_2 = 7.89 + assert param_dict["number"] != new_number_1 + param_dict["number"] = new_number_1 + assert param_dict["number"] == new_number_1 + param_dict.number = new_number_2 + assert param_dict["number"] == new_number_2 + + +def test_param_dict_set_parent( + param_dict: ParamDict[Any], param_data: ParamData +) -> None: + """Parameter data added to a parameter dictionary has the correct parent.""" + with pytest.raises(ValueError): + _ = param_data.parent + for _ in range(2): # Run twice to check reassigning the same parameter data + param_dict["param_data"] = param_data + assert param_data.parent is param_dict + param_dict["param_data"] = None + with pytest.raises(ValueError): + _ = param_data.parent + + +def test_param_dict_del( + param_dict: ParamDict[Any], param_dict_contents: dict[str, Any] +) -> None: + """Can delete items from a parameter dictionary.""" + assert dict(param_dict) == param_dict_contents + del param_dict["number"] + del param_dict.string + del param_dict_contents["number"] + del param_dict_contents["string"] + assert dict(param_dict) == param_dict_contents + + +def test_param_dict_del_parent( + param_dict: ParamDict[Any], param_data: ParamData +) -> None: + """An item deleted from a parameter dictionary has no parent.""" + param_dict["param_data"] = param_data + assert param_data.parent is param_dict + del param_dict["param_data"] + with pytest.raises(ValueError): + _ = param_data.parent + + +def test_param_dict_iter( + param_dict: ParamDict[Any], param_dict_contents: dict[str, Any] +) -> None: + """A parameter dictionary correctly supports iteration.""" + for key, contents_key in zip(param_dict, param_dict_contents): + assert key == contents_key diff --git a/tests/_param_data/test_dataclasses.py b/tests/_param_data/test_dataclasses.py new file mode 100644 index 0000000..c73f60d --- /dev/null +++ b/tests/_param_data/test_dataclasses.py @@ -0,0 +1,84 @@ +"""Tests for the paramdb._param_data._dataclasses module.""" + +from copy import deepcopy +from datetime import datetime +import pytest +from tests.helpers import CustomParam, CustomStruct, sleep_for_datetime +from paramdb import ParamData + +ParamDataclass = CustomParam | CustomStruct + + +@pytest.fixture( + name="param_dataclass", + params=["param", "struct"], +) +def fixture_param_dataclass(request: pytest.FixtureRequest) -> ParamDataclass: + """Parameter dataclass.""" + param_dataclass: ParamDataclass = deepcopy(request.getfixturevalue(request.param)) + return param_dataclass + + +def test_param_dataclass_get( + param_dataclass: ParamDataclass, number: float, string: str +) -> None: + """ + Parameter dataclass properties can be accessed via dot notation and index brackets. + """ + assert param_dataclass.number == number + assert param_dataclass.string == string + assert param_dataclass["number"] == number + assert param_dataclass["string"] == string + + +def test_param_dataclass_set(param_dataclass: ParamDataclass, number: float) -> None: + """Parameter data properties can be updated via dot notation and index brackets.""" + param_dataclass.number += 1 + assert param_dataclass.number == number + 1 + param_dataclass["number"] -= 1 + assert param_dataclass.number == number + + +def test_param_default_last_updated() -> None: + """Parameter object initializes the last updated time to the current time.""" + start = datetime.now() + sleep_for_datetime() + param = CustomParam() + sleep_for_datetime() + end = datetime.now() + assert start < param.last_updated < end + + +def test_struct_no_last_updated() -> None: + """Structure object that contains no parameters has no last updated time.""" + struct = CustomStruct() + assert struct.last_updated is None + + +def test_struct_last_updated( + struct: CustomStruct, updated_param_data: ParamData, start: datetime, end: datetime +) -> None: + """Structure can correctly get the last updated time from its contents.""" + struct.param_data = updated_param_data + assert struct.last_updated is not None + assert start < struct.last_updated < end + + +def test_struct_init_parent(struct: CustomStruct) -> None: + """Structure children correctly identify it as a parent after initialization.""" + assert struct.param is not None + assert struct.struct is not None + assert struct.param.parent is struct + assert struct.struct.parent is struct + + +def test_struct_set_parent(struct: CustomStruct, param_data: ParamData) -> None: + """Parameter data added to a structure has the correct parent.""" + with pytest.raises(ValueError): + _ = param_data.parent + for _ in range(2): # Run twice to check reassigning the same parameter data + struct.param_data = param_data + assert param_data.parent is struct + struct.param_data = None + with pytest.raises(ValueError): + _ = param_data.parent diff --git a/tests/_param_data/test_param_data.py b/tests/_param_data/test_param_data.py new file mode 100644 index 0000000..d60a263 --- /dev/null +++ b/tests/_param_data/test_param_data.py @@ -0,0 +1,105 @@ +"""Tests for the paramdb._param_data._param_data module.""" + +from copy import deepcopy +from datetime import datetime +import pytest +from tests.helpers import CustomStruct, sleep_for_datetime +from paramdb import ParamData +from paramdb._param_data._param_data import get_param_class + + +def test_is_param_data(param_data: ParamData) -> None: + """Parameter data object is an instance of the `ParamData` class.""" + assert isinstance(param_data, ParamData) + + +def test_get_param_class(param_data: ParamData) -> None: + """Parameter classes can be retrieved by name.""" + param_class = type(param_data) + assert get_param_class(param_class.__name__) is param_class + + +def test_param_data_last_updated( + updated_param_data: ParamData, start: datetime, end: datetime +) -> None: + """Updating simple parameter data updates the last updated time.""" + assert updated_param_data.last_updated is not None + assert start < updated_param_data.last_updated < end + + +def test_list_or_dict_last_updated( + updated_param_data: ParamData, start: datetime, end: datetime +) -> None: + """Can get last updated from a Python list or dictionary.""" + # Can get last updated time from within a list + struct_with_list = CustomStruct( + list=[CustomStruct(), [updated_param_data, CustomStruct()]] + ) + assert struct_with_list.last_updated is not None + assert start < struct_with_list.last_updated < end + + # Can get last updated time from within a dictionary + struct_with_dict = CustomStruct( + dict={ + "p1": CustomStruct(), + "p2": {"p1": updated_param_data, "p2": CustomStruct()}, + } + ) + assert struct_with_dict.last_updated is not None + assert start < struct_with_list.last_updated < end + + +def test_child_does_not_change(param_data: ParamData) -> None: + """ + Including a parameter data object as a child within a parent structure does not + change the parameter in terms of equality comparison (i.e. public properties, + importantly last_updated, have not changed). + """ + param_data_original = deepcopy(param_data) + sleep_for_datetime() + _ = CustomStruct(param_data=param_data) + assert param_data == param_data_original + + +def test_to_and_from_dict(param_data: ParamData) -> None: + """Parameter data can be converted to and from a dictionary.""" + param_data_dict = param_data.to_dict() + assert isinstance(param_data_dict, dict) + sleep_for_datetime() + param_data_from_dict = param_data.from_dict(param_data_dict) + assert param_data_from_dict == param_data + assert param_data_from_dict.last_updated == param_data.last_updated + + +def test_no_parent_fails(param_data: ParamData) -> None: + """Fails to get the parent when there is no parent.""" + with pytest.raises(ValueError) as exc_info: + _ = param_data.parent + assert ( + str(exc_info.value) + == f"'{type(param_data).__name__}' object has no parent, or its parent has not" + " been initialized yet" + ) + + +def test_self_is_root(param_data: ParamData) -> None: + """Parameter data object with no parent returns itself as the root.""" + assert param_data.root is param_data + + +def test_parent_is_root(param_data: ParamData) -> None: + """ + Parameter data object with a parent that has no parent returns the parent as the + root. + """ + parent = CustomStruct(param_data=param_data) + assert param_data.root is parent + + +def test_parent_of_parent_is_root(param_data: ParamData) -> None: + """ + Parameter data object with a parent that has a parent returns the highest level + parent as the root. + """ + root = CustomStruct(struct=CustomStruct(param_data=param_data)) + assert param_data.root is root diff --git a/tests/conftest.py b/tests/conftest.py index 7added7..ae75729 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,84 +1,139 @@ -""" -Test configuration file that defines fixtures and parameter data classes. - -Called automatically by Pytest before running tests. -""" - -from __future__ import annotations - -from dataclasses import dataclass, field -from pathlib import Path -from pytest import fixture, FixtureRequest -from paramdb import Struct, Param - - -DEFAULT_NUMBER = 1.23 -DEFAULT_STRING = "test" - - -@dataclass -class CustomStruct(Struct): - """Custom parameter structure.""" - - number: float = DEFAULT_NUMBER - string: str = DEFAULT_STRING - param: CustomParam | None = None - struct: CustomStruct | None = None - param_list: list[CustomParam | list[CustomParam] | dict[str, CustomParam]] = field( - default_factory=list - ) - param_dict: dict[ - str, CustomParam | list[CustomParam] | dict[str, CustomParam] - ] = field(default_factory=dict) - - -@dataclass -class CustomParam(Param): - """Custom parameter.""" - - number: float = DEFAULT_NUMBER - string: str = DEFAULT_STRING - - -@fixture -def number() -> float: +"""Defines global fixtures. Called automatically by Pytest before running tests.""" + +from typing import Any +from copy import deepcopy +from datetime import datetime +import pytest +from paramdb import ParamData, ParamList, ParamDict +from tests.helpers import ( + DEFAULT_NUMBER, + DEFAULT_STRING, + CustomStruct, + CustomParam, + sleep_for_datetime, +) + + +@pytest.fixture(name="number") +def fixture_number() -> float: """Number used to initialize parameter data.""" return DEFAULT_NUMBER -@fixture -def string() -> str: +@pytest.fixture(name="string") +def fixture_string() -> str: """String used to initialize parameter data.""" return DEFAULT_STRING -@fixture(params=[CustomStruct, CustomParam]) -def param_data( - request: FixtureRequest, - number: float, # pylint: disable=redefined-outer-name - string: str, # pylint: disable=redefined-outer-name -) -> CustomStruct | CustomParam: - """Parameter data object, either a parameter or structure.""" - param_data_class: type[CustomStruct | CustomParam] = request.param - return param_data_class(number=number, string=string) +@pytest.fixture(name="param") +def fixture_param(number: float, string: str) -> CustomParam: + """Parameter.""" + return CustomParam(number=number, string=string) -@fixture(name="complex_struct") -def fixture_complex_struct() -> CustomStruct: - """Structure that contains parameters, structures, lists, and dictionaries.""" +@pytest.fixture(name="struct") +def fixture_struct(number: float, string: str) -> CustomStruct: + """Structure.""" return CustomStruct( + number=number, + string=string, param=CustomParam(), - struct=CustomStruct(param=CustomParam()), - param_list=[CustomParam(), [CustomParam()], {"p1": CustomParam()}], - param_dict={ - "param": CustomParam(), - "list": [CustomParam()], - "dict": {"p1": CustomParam()}, - }, + struct=CustomStruct(), + param_list=ParamList(), + param_dict=ParamDict(), ) -@fixture -def db_path(tmp_path: Path) -> str: - """Return a path to use for a `ParamDB`.""" - return str(tmp_path / "param.db") +@pytest.fixture(name="param_list_contents") +def fixture_param_list_contents(number: float, string: str) -> list[Any]: + """Contents to initialize a parameter list.""" + return [number, string, CustomParam(), CustomStruct(), ParamList(), ParamDict()] + + +@pytest.fixture(name="param_dict_contents") +def fixture_param_dict_contents( + number: float, string: str, param: CustomParam, struct: CustomStruct +) -> dict[str, Any]: + """Contents to initialize a parameter dictionary.""" + return { + "number": number, + "string": string, + "param": deepcopy(param), + "struct": deepcopy(struct), + "param_list": ParamList(), + "param_dict": ParamDict(), + } + + +@pytest.fixture(name="param_list") +def fixture_param_list(param_list_contents: list[Any]) -> ParamList[Any]: + """Parameter list.""" + return ParamList(deepcopy(param_list_contents)) + + +@pytest.fixture(name="param_dict") +def fixture_param_dict(param_dict_contents: dict[str, Any]) -> ParamDict[Any]: + """Parameter dictionary.""" + return ParamDict(deepcopy(param_dict_contents)) + + +@pytest.fixture( + name="param_data", + params=["param", "struct", "param_list", "param_dict"], +) +def fixture_param_data( + request: pytest.FixtureRequest, +) -> ParamData: + """Parameter data.""" + param_data: ParamData = deepcopy(request.getfixturevalue(request.param)) + return param_data + + +@pytest.fixture(name="updated_param_data_and_datetimes") +def fixture_updated_param_data_and_datetimes( + param_data: ParamData, +) -> tuple[ParamData, datetime, datetime]: + """ + Parameter data that has been updated between the returned start and end datetimes. + Broken down into individual fixtures for parameter data, start, and end below. + """ + updated_param_data = deepcopy(param_data) + start = datetime.now() + sleep_for_datetime() + if isinstance(updated_param_data, CustomParam): + updated_param_data.number += 1 + if isinstance(updated_param_data, CustomStruct): + assert updated_param_data.param is not None + updated_param_data.param.number += 1 + if isinstance(updated_param_data, ParamList): + updated_param_data[2].number += 1 + if isinstance(updated_param_data, ParamDict): + updated_param_data.param.number += 1 + sleep_for_datetime() + end = datetime.now() + return updated_param_data, start, end + + +@pytest.fixture(name="updated_param_data") +def fixture_updated_param_data( + updated_param_data_and_datetimes: tuple[ParamData, datetime, datetime] +) -> ParamData: + """Parameter data that has been updated.""" + return updated_param_data_and_datetimes[0] + + +@pytest.fixture(name="start") +def fixture_start( + updated_param_data_and_datetimes: tuple[ParamData, datetime, datetime] +) -> datetime: + """Datetime before param_data fixture was updated.""" + return updated_param_data_and_datetimes[1] + + +@pytest.fixture(name="end") +def fixture_end( + updated_param_data_and_datetimes: tuple[ParamData, datetime, datetime] +) -> datetime: + """Datetime after param_data fixture was updated.""" + return updated_param_data_and_datetimes[2] diff --git a/tests/helpers.py b/tests/helpers.py index 16ea480..b5674c9 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -1,8 +1,47 @@ """Helper functions for paramdb tests.""" +from __future__ import annotations +from typing import Any +from dataclasses import dataclass, field import time -from datetime import datetime -from tests.conftest import CustomStruct, CustomParam +from paramdb import ParamData, Param, Struct, ParamList, ParamDict + +DEFAULT_NUMBER = 1.23 +DEFAULT_STRING = "test" + + +@dataclass +class CustomParam(Param): + """Custom parameter.""" + + number: float = DEFAULT_NUMBER + number_init_false: float = field(init=False, default=DEFAULT_NUMBER) + string: str = DEFAULT_STRING + + +@dataclass +# pylint: disable-next=too-many-instance-attributes +class CustomStruct(Struct): + """Custom parameter structure.""" + + number: float = DEFAULT_NUMBER + number_init_false: float = field(init=False, default=DEFAULT_NUMBER) + string: str = DEFAULT_STRING + list: list[Any] = field(default_factory=list) + dict: dict[str, Any] = field(default_factory=dict) + param: CustomParam | None = None + struct: CustomStruct | None = None + param_list: ParamList[Any] = field(default_factory=ParamList) + param_dict: ParamDict[Any] = field(default_factory=ParamDict) + param_data: ParamData | None = None + + +class CustomParamList(ParamList[Any]): + """Custom parameter list subclass.""" + + +class CustomParamDict(ParamDict[Any]): + """Custom parameter dictionary subclass.""" def sleep_for_datetime() -> None: @@ -15,20 +54,4 @@ def sleep_for_datetime() -> None: computers execute instructions faster than this. So without waiting, it is difficult to ensure that something is using `datetime.now()`. """ - time.sleep(0.001) # Wait for one millisecond - - -def update_param_and_assert_last_updated_changed( - param: CustomParam, param_data: CustomStruct | CustomParam -) -> None: - """ - Update the given parameter (assumed to be or exist within the given parameter data) - and assert that the structure's last updated time correctly reflects that something - was just updated. - """ - start = datetime.now() - sleep_for_datetime() - param.number += 1 - sleep_for_datetime() - end = datetime.now() - assert param_data.last_updated is not None and start < param_data.last_updated < end + time.sleep(100e-6) # Wait for 100 microseconds diff --git a/tests/test_database.py b/tests/test_database.py index 203ca64..22b208d 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -1,17 +1,31 @@ """Tests for the paramdb._database module.""" from typing import Any +from dataclasses import dataclass +from copy import deepcopy import os from datetime import datetime -from pytest import raises -from tests.conftest import CustomStruct, CustomParam -from tests.helpers import sleep_for_datetime -from paramdb import Struct, ParamDB, CommitEntry, CommitNotFoundError -from paramdb._param_data import _param_classes - - -def test_create(db_path: str) -> None: - """Parameter DB can be created.""" +from pathlib import Path +import pytest +from tests.helpers import ( + CustomStruct, + CustomParam, + CustomParamList, + CustomParamDict, + sleep_for_datetime, +) +from paramdb import ParamData, Struct, ParamList, ParamDict, ParamDB, CommitEntry +from paramdb._param_data._param_data import _param_classes + + +@pytest.fixture(name="db_path") +def fixture_db_path(tmp_path: Path) -> str: + """Return a path to use for a `ParamDB`.""" + return str(tmp_path / "param.db") + + +def test_create_database(db_path: str) -> None: + """Parameter DB can be created on disk.""" assert not os.path.exists(db_path) ParamDB[Any](db_path) assert os.path.exists(db_path) @@ -20,14 +34,18 @@ def test_create(db_path: str) -> None: def test_commit_not_json_serializable_fails(db_path: str) -> None: """Fails to commit a class that ParamDB does not know how to convert to JSON.""" - class NotJSONSerializable: # pylint: disable=too-few-public-methods + class NotJSONSerializable: """Class that ParamDB does not know how to serialize as JSON.""" param_db = ParamDB[NotJSONSerializable](db_path) data = NotJSONSerializable() - with raises(TypeError) as exc_info: + with pytest.raises(TypeError) as exc_info: param_db.commit("Initial commit", data) - assert str(exc_info.value) == f"{repr(data)} is not JSON serializable" + assert ( + str(exc_info.value) + == f"'{NotJSONSerializable.__name__}' object {repr(data)} is not JSON" + " serializable, so the commit failed" + ) def test_load_unknown_class_fails(db_path: str) -> None: @@ -37,7 +55,7 @@ def test_load_unknown_class_fails(db_path: str) -> None: defined. """ - class Unknown(Struct): # pylint: disable=too-few-public-methods + class Unknown(Struct): """ Class that is unknown to ParamDB. By default, it will get added to the private param class dictionary when created, but on the next line we manually delete it. @@ -47,102 +65,158 @@ class Unknown(Struct): # pylint: disable=too-few-public-methods param_db = ParamDB[Unknown](db_path) data = Unknown() param_db.commit("Initial commit", data) - with raises(ValueError) as exc_info: + with pytest.raises(ValueError) as exc_info: param_db.load() - assert str(exc_info.value) == f"class '{Unknown.__name__}' is not known to paramdb" + assert ( + str(exc_info.value) + == f"class '{Unknown.__name__}' is not known to ParamDB, so the load failed" + ) def test_load_empty_fails(db_path: str) -> None: """Fails to loading from an empty database.""" param_db = ParamDB[Any](db_path) - with raises(CommitNotFoundError) as exc_info: + with pytest.raises(IndexError) as exc_info: param_db.load() assert ( str(exc_info.value) - == f"cannot load most recent data because database '{db_path}' has no commits" + == f"cannot load most recent commit because database '{db_path}' has no commits" ) def test_load_nonexistent_commit_fails(db_path: str) -> None: """Fails to loading a commit that does not exist.""" + # Empty database param_db = ParamDB[Any](db_path) - with raises(CommitNotFoundError) as exc_info: + with pytest.raises(IndexError) as exc_info: param_db.load(1) assert str(exc_info.value) == f"commit 1 does not exist in database '{db_path}'" + + # Database with one commit param_db.commit("Initial commit", {}) - with raises(CommitNotFoundError) as exc_info: + with pytest.raises(IndexError) as exc_info: param_db.load(100) assert str(exc_info.value) == f"commit 100 does not exist in database '{db_path}'" -def test_commit_load(db_path: str, param_data: CustomStruct | CustomParam) -> None: - """Can commit and load parameters and structures.""" - param_db = ParamDB[CustomStruct | CustomParam](db_path) +def test_commit_and_load(db_path: str, param_data: ParamData) -> None: + """Can commit and load parameter data.""" + param_db = ParamDB[ParamData](db_path) param_db.commit("Initial commit", param_data) # Can load the most recent commit + sleep_for_datetime() param_data_loaded_most_recent = param_db.load() assert param_data_loaded_most_recent == param_data + assert param_data_loaded_most_recent.last_updated == param_data.last_updated # Can load by commit ID + sleep_for_datetime() param_data_loaded_first_commit = param_db.load(1) assert param_data_loaded_first_commit == param_data_loaded_most_recent + assert ( + param_data_loaded_first_commit.last_updated + == param_data_loaded_most_recent.last_updated + ) -def test_commit_load_complex(db_path: str, complex_struct: CustomStruct) -> None: - """Can commit and load a complex structure.""" - test_commit_load(db_path, complex_struct) +# pylint: disable-next=too-many-arguments +def test_commit_and_load_complex( + db_path: str, + number: float, + string: str, + param_list_contents: list[Any], + param_dict_contents: dict[str, Any], + param: CustomParam, + struct: CustomStruct, + param_list: ParamList[Any], + param_dict: ParamDict[Any], +) -> None: + """Can commit and load a complex parameter structure.""" + + @dataclass + # pylint: disable-next=too-many-instance-attributes + class Root(Struct): + """Complex root structure to test the database.""" + + number: float + string: str + list: list[Any] + dict: dict[str, Any] + param: CustomParam + struct: CustomStruct + param_list: ParamList[Any] + param_dict: ParamDict[Any] + custom_param_list: CustomParamList + custom_param_dict: CustomParamDict + + root = Root( + number=number, + string=string, + list=param_list_contents, + dict=param_dict_contents, + param=param, + struct=struct, + param_list=param_list, + param_dict=param_dict, + custom_param_list=CustomParamList(deepcopy(param_list_contents)), + custom_param_dict=CustomParamDict(deepcopy(param_dict_contents)), + ) + param_db = ParamDB[Root](db_path) + param_db.commit("Initial commit", root) + root_loaded = param_db.load() + assert root_loaded == root def test_commit_load_multiple(db_path: str) -> None: """Can commit multiple times and load previous commits.""" param_db = ParamDB[CustomParam](db_path) - # Commit several different parameters + # Make 10 commits params = [CustomParam(number=i + 1) for i in range(10)] for i, param in enumerate(params): param_db.commit(f"Commit {i + 1}", param) - # Load them back + # Load and verify the commits for i, param in enumerate(params): param_loaded = param_db.load(i + 1) assert param_loaded == param -def test_separate_connections(db_path: str, complex_struct: CustomStruct) -> None: +def test_separate_connections(db_path: str, param: CustomParam) -> None: """ Can commit and load using separate connections. This simulates committing to the database in one program and loading in another program at a later time. """ # Commit using one connection - param_db1 = ParamDB[CustomStruct](db_path) - param_db1.commit("Initial commit", complex_struct) + param_db1 = ParamDB[CustomParam](db_path) + param_db1.commit("Initial commit", param) del param_db1 # Load back using another connection - param_db2 = ParamDB[CustomStruct](db_path) - complex_struct_loaded = param_db2.load() - assert complex_struct == complex_struct_loaded + param_db2 = ParamDB[CustomParam](db_path) + simple_param_loaded = param_db2.load() + assert param == simple_param_loaded def test_empty_commit_history(db_path: str) -> None: - """Loads an empty commit history for an empty database.""" + """Loads an empty commit history from an empty database.""" param_db = ParamDB[CustomStruct](db_path) commit_history = param_db.commit_history() assert commit_history == [] -def test_commit_history(db_path: str, complex_struct: CustomStruct) -> None: +def test_commit_history(db_path: str, param: CustomParam) -> None: """Loads the correct commit history for a series of commits.""" starts = [] ends = [] - param_db = ParamDB[CustomStruct](db_path) + param_db = ParamDB[CustomParam](db_path) - # Make several commits + # Make 10 commits for i in range(10): starts.append(datetime.now()) sleep_for_datetime() - param_db.commit(f"Commit {i}", complex_struct) + param_db.commit(f"Commit {i}", param) sleep_for_datetime() ends.append(datetime.now()) diff --git a/tests/test_param_data.py b/tests/test_param_data.py deleted file mode 100644 index df9928d..0000000 --- a/tests/test_param_data.py +++ /dev/null @@ -1,175 +0,0 @@ -"""Tests for the paramdb._param_data module.""" - -from datetime import datetime, timedelta -from tests.conftest import CustomStruct, CustomParam -from tests.helpers import ( - sleep_for_datetime, - update_param_and_assert_last_updated_changed, -) -from paramdb import ParamData -from paramdb._param_data import get_param_class - - -def test_is_param_data(param_data: CustomStruct | CustomParam) -> None: - """Parameter data object is an instance of the `ParamData` class.""" - assert isinstance(param_data, ParamData) - - -def test_get_param_class(param_data: CustomStruct | CustomParam) -> None: - """Parameter classes can be retrieved by name.""" - param_class = param_data.__class__ - param_class_name = param_data.__class__.__name__ - assert get_param_class(param_class_name) is param_class - - -def test_property_access( - param_data: CustomStruct | CustomParam, number: float, string: str -) -> None: - """Parameter data properties can be accessed via dot notation and index brackets.""" - assert param_data.number == number - assert param_data.string == string - assert param_data["number"] == number - assert param_data["string"] == string - - -def test_struct_property_update( - param_data: CustomStruct | CustomParam, number: float -) -> None: - """Parameter data properties can be updated via dot notation and index brackets.""" - param_data.number += 1 - assert param_data.number == number + 1 - param_data["number"] -= 1 - assert param_data.number == number - - -def test_param_default_last_updated() -> None: - """Parameter object initializes the last updated time to the current time.""" - start = datetime.now() - sleep_for_datetime() - param = CustomParam() - sleep_for_datetime() - end = datetime.now() - assert start < param.last_updated < end - - -def test_param_initialize_last_updated() -> None: - """ - Parameter object initializes the last updated time to the given value instead of the - current time. - """ - last_updated = datetime.now() - timedelta(days=1) - param = CustomParam(_last_updated=last_updated) - assert param.last_updated == last_updated - - -def test_param_update_last_updated() -> None: - """ - Parameter object updates the last updated time when a property is updated with dot - notation or index brackets. - """ - # Dot notation access - param = CustomParam() - update_param_and_assert_last_updated_changed(param, param) - - # Index bracket access - param = CustomParam() - start = datetime.now() - sleep_for_datetime() - param["number"] += 1 - sleep_for_datetime() - end = datetime.now() - assert start < param.last_updated < end - - -def test_struct_no_last_updated() -> None: - """Structure object that contains no parameters has no last updated time.""" - struct = CustomStruct() - assert struct.last_updated is None - - -def test_struct_last_updated_from_param(complex_struct: CustomStruct) -> None: - """Structure object can find the most recent last updated time from a parameter.""" - param = complex_struct.param - assert param is not None - update_param_and_assert_last_updated_changed(param, complex_struct) - - -def test_struct_last_updated_from_struct(complex_struct: CustomStruct) -> None: - """Structure object can find the most recent last updated time from a structure.""" - struct = complex_struct.struct - assert struct is not None - param_in_struct = struct.param - assert param_in_struct is not None - update_param_and_assert_last_updated_changed(param_in_struct, complex_struct) - - -def test_struct_last_updated_from_param_in_list(complex_struct: CustomStruct) -> None: - """ - Structure object can find the most recent last updated time from a parameter within - a list. - """ - param_in_list = complex_struct.param_list[0] - assert isinstance(param_in_list, CustomParam) - update_param_and_assert_last_updated_changed(param_in_list, complex_struct) - - -def test_struct_last_updated_from_list_in_list(complex_struct: CustomStruct) -> None: - """ - Structure object can find the most recent last updated time from a parameter in a - list within a list. - """ - list_in_list = complex_struct.param_list[1] - assert isinstance(list_in_list, list) - param_in_list_in_list = list_in_list[0] - update_param_and_assert_last_updated_changed(param_in_list_in_list, complex_struct) - - -def test_struct_last_updated_from_dict_in_list(complex_struct: CustomStruct) -> None: - """ - Structure object can find the most recent last updated time from a parameter in a - dictionary within a list. - """ - dict_in_list = complex_struct.param_list[2] - assert isinstance(dict_in_list, dict) - param_in_dict_in_list = dict_in_list["p1"] - update_param_and_assert_last_updated_changed(param_in_dict_in_list, complex_struct) - - -def test_struct_last_updated_from_param_in_dict(complex_struct: CustomStruct) -> None: - """ - Structure object can find the most recent last updated time from a parameter within - a dictionary. - """ - param_in_dict = complex_struct.param_dict["param"] - assert isinstance(param_in_dict, CustomParam) - update_param_and_assert_last_updated_changed(param_in_dict, complex_struct) - - -def test_struct_last_updated_from_list_in_dict(complex_struct: CustomStruct) -> None: - """ - Structure object can find the most recent last updated time from a parameter in a - list within a list. - """ - list_in_dict = complex_struct.param_dict["list"] - assert isinstance(list_in_dict, list) - param_in_list_in_list = list_in_dict[0] - update_param_and_assert_last_updated_changed(param_in_list_in_list, complex_struct) - - -def test_struct_last_updated_from_dict_in_dict(complex_struct: CustomStruct) -> None: - """ - Structure object can find the most recent last updated time from a parameter in a - dictionary within a list. - """ - dict_in_dict = complex_struct.param_dict["dict"] - assert isinstance(dict_in_dict, dict) - param_in_dict_in_list = dict_in_dict["p1"] - update_param_and_assert_last_updated_changed(param_in_dict_in_list, complex_struct) - - -def test_to_and_from_dict(param_data: CustomStruct | CustomParam) -> None: - """Parameter data can be converted to and from a dictionary.""" - param_data_dict = param_data.to_dict() - assert isinstance(param_data_dict, dict) - param_data_from_dict = param_data.__class__.from_dict(param_data_dict) - assert param_data_from_dict == param_data