Repository: zyskarch/pytestarch (language: python)

Issue:
Support for layer architecture



Discussion / hints:

Fix patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 806901f..30e5d6c 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -6,6 +6,7 @@ repos:
     rev: 5.12.0
     hooks:
       - id: isort
+        exclude: .*__init__\.py$
   - repo: https://github.com/psf/black
     rev: 23.3.0
     hooks:
diff --git a/docs/changelog.md b/docs/changelog.md
index 4357e85..7f8ffa8 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -5,6 +5,7 @@ This project uses semantic versioning and follows [keep a changelog](https://kee
 
 ## [Unreleased]
 ### Added
+- Support for layer architecture.
 - Batch support for rule subjects.
 
 ### Changed
diff --git a/docs/details.md b/docs/details.md
deleted file mode 100644
index 89aa107..0000000
--- a/docs/details.md
+++ /dev/null
@@ -1,288 +0,0 @@
-# Details
-
-## General Concept
-PyTestArch creates an Abstract Syntax Tree for each Python file it scans. It then extracts only the information about 
-module imports from the AST and converts them to an internal representation. This is currently a graph supplied by the 
-[NetworkX](https://networkx.org/) library.
-
-When the user defines a rule and evaluates the internal representation against it, the rule is converted into a set of
-graph operations that are then carried out on the previously generated graph.
-
-
-## Simple Example
-
-### Parsing all files
-Consider the following project structure:
-```
-my_project/
-    src/
-        main.py
-        util.py
-        util_test.py
-```
-
-Both `main.py` and `util_test.py` are importing a util function defined in `util.py`; no other imports exist.
-
-With "my_project" as the project's root folder and "src" as the folder to evaluate, this will create the following graph:
-
-![Simple Example Graph](./resources/docu_simple_example.png)
-
-Each module has a direct path to all submodules, in this case: "src" is connected to all three modules it contains, 
-"src.main", "src.util", and "src.util_test".
-
-In addition, a module that imports another module has a directed edge linking it to this module. For example, "src.main"
-imports "src.util" and therefore the graph has an edge connecting these modules, with the arrow head pointing at the imported
-module, in this case "src.util".
-
-### Excluding some files
-
-In the example above, the `util_test.py` file is included in the graph and will be considered when evaluating the architecture.
-If certain files should be excluded from the analysis, this can be done by specifying an exclusion pattern. In our example,
-if we want to exclude `util_test.py`, we could use:
-```
-from pytestarch.pytestarch import get_evaluable_architecture
-
-evaluable = get_evaluable_architecture("/home/my_project", "/home/my_project/src", ("*_test.py"))
-```
-This will exclude all files with names ending in "_test.py". It is also possible to exclude directories.
-
-
-## More complex example
-As a basis for describing the query language, let's consider a more complex example:
-```
-/test_project
-    /src
-       __init__.py
-       /A
-            __init__.py
-            fileA.py
-            /A1
-                __init__.py
-                fileA1.py
-                fileA1_b.py
-                /A11
-                    __init__.py
-                    fileA11.py
-            /A2
-                __init__.py
-                fileA2.py
-       /B
-            __init__.py
-            fileB.py
-            /B1
-                __init__.py
-                fileB1.py
-                fileB2.py
-       /C 
-            __init__.py
-            fileC.py
-```
-
-With the following list of imports:
-<ul>
-    <li>fileA imports fileC</li>
-    <li>fileA11 imports fileB1</li>
-    <li>fileA2 imports fileC</li>
-    <li>fileB imports fileA11</li>
-    <li>fileB2 imports fileA11</li>
-    <li>fileC imports the built-in os module</li>
-</ul>
-
-This creates the following graph (excluding all `__init__.py`):
-
-![Complex Example](./resources/docu_complex_example.png)
-
-Note that the module names in the diagram have been abbreviated: For example, the node "fileA" is actually named "src.A.fileA".
-
-Most of the edges in this graph are due to parent-child relationships between the modules. However, five of the six import
-relationships defined above are present in the graph - only fileC has no connection to an "os" node; in fact, there is no
-node names "os" at all.
-
-This has been achieved by setting the `exclude_external_libraries` flag in the `get_evaluable_architecture` function. All modules that 
-not located hierarchically below the root path, in this case "test_project", will be excluded from the graph.
-
-
-
-## Query Language
-
-### General Structure
-
-The query language that can be used to define architectural rules follows this structure: 
-
-RULE_SUBJECT - VERB_MARKER_1 - IMPORT_TYPE - VERB_MARKER_2 - RULE_OBJECT
-
-The meaning of these structural markers is described in the table below.
-
-| Marker       | Description                                            | Example                            |
-|--------------|--------------------------------------------------------|------------------------------------|
-| RULE_SUBJECT | module(s) to be checked                                | modules that are named 'B'         |
-| RULE_OBJECT  | module(s) to check against                             | modules that are submodules of 'A' |
-| IMPORT_TYPE | expected type of import relationship | be imported by                     |
-| VERB_MARKER_1 | defines the expected behavior, part I                  | should not                         |
-| VERB_MARKER_2 | defines the expected behavior, part II                 | except                             |
-
-The examples given in the table above combined form the rule `modules that are named 'B' (RULE_SUBJECT) should not (VERB_MARKER_1) be imported by (IMPORT_TYPE) modules except (VERB_MARKER_2) modules that are submodules of "A" (RULE_OBJECT)`.
-
-Looking at the diagram in the section Complex Example, we can see that this rule holds - the only module importing 
-from the "B" module is "fileA11", which is a submodule of "A".
-
-
-### Features
-
-Currently, the following markers are supported by PyTestArch:
-
-#### RULE_SUBJECT
-* are_named("X"): applies to module named "X" (and also to its submodules)
-* are_submodules_of("Y"): applies to submodules of module named "Y", but not "Y" itself
-* have_name_containing("*Z*"): applies to modules with names containing Z. The syntax is the same as for the file exclusion mechanism.
-
-#### RULE_OBJECT
-same as RULE_SUBJECT, with an additional
-
-anything: can only be used in combination with should_not()
-
-In order to reduce the number of possible API component combinations, this rule object has been combined with two
-verbs into: `import_anything()` and `be_imported_by_anything()`. 
-
-
-Both RULE_SUBJECTs and RULE_OBJECTs can be specified in batch, i.e. via a list of values.
-For example, the rule
-```
-modules_that()
-    .are_named("1", "2")
-    .should_only()
-    .be_imported_by_modules_that()
-    .are_named(["3", "4"])
-```
-is fulfilled, it the modules "1" and "2" both are not imported by any module other than "3" and "4", and if both "3" and "4" do import "1" and "2".
-
-
-#### VERB_MARKER_1
-* should()
-* should_only()
-* should_not()
-
-#### IMPORT_TYPE + VERB_MARKER_2
-* import_modules_that()
-* import_modules_except_modules_that()
-* be_imported_by_modules_that()
-* be_imported_by_modules_except_modules_that()
-
-VERB_MARKER_2 and IMPORT_TYPE have been conflated into one expression to improve readability.
-
-Markers from each category can be combined freely with all markers of all other categories. Example rules could be <br>
-```
-modules_that()
-    .are_sub_modules_of("A")
-    .should_only()
-    .be_imported_by_modules_that()
-    .are_sub_modules_of("B")
-```
-(True in the above example)
-
-or <br>
-```
-modules_that()
-    .are_named("C")
-    .should_only()
-    .be_imported_by_modules_that()
-    .are_named("A2")
-```
-(False, also imported by module "A")
-
-
-Most rules are so close to the English language that a detailed explanation seems unnecessary. An exception might be the
-VERB_MARKER_2 "except". A combination of this VERB_MARKER_2 and every type of VERB_MARKER_1 and IMPORT_TYPE is given below
-as reference (M1, M2 are used as RULE_SUBJECT and RULE_OBJECT respectively; pseudo-code for brevity):
-
-| Rule | Explanation |
-| ---- | ----------- |
-| M1 should import except M2 | M1 should import at least one module that isn't M2, but can also import M2|
-| M1 should only import except M2| M1 should import at least one module that isn't M2 and should not import M2 |
-| M1 should not import except M2| M1 should not import any module other than M2, but does not have to import M2 |
-| M1 should be imported except by M2 | at least one module that isn't M2 should import M1 (M2 can import M1 as well) |
-| M1 should only be imported except by M2 | at least one module that isn't M2 should import M1, and M2 cannot import M1 |
-| M1 should not be imported except by M2 | no module other than M2 should import M1, but M2 does not have to import M1 |
-
-
-There are two aliases to make rules easier:
-
-* 'M1 should not import anything' is equivalent to: 'M1 should not import anything except itself' (e.g. imports between its submodules are allowed, but no other imports)
-* 'M1 should not be imported by anything' is equivalent to: 'M1 should not be imported by anything except itself' (dito)
-
-
-## Generating the evaluable architecture representation
-When scanning and processing the requested modules, PyTestArch executes the following step:
-1) Parse all files starting at the requested `module_path`. This only takes python source files into account that are not explicitly excluded.
-
-2) Convert the generated AST into custom dependency representations. In this step, it is ensured that all internal modules (either importing or imported) receive their fully 
-qualified name.
-
-3) Generate a list of all modules that were parsed. This list is used to differentiate between external and internal dependencies (external dependencies will not have been parsed).
-
-4) If not requested otherwise, external dependencies will be filtered out.
-
-5) If external dependencies should be included, they will be added to the list of modules.
-
-6) The dependency representations are converted to a graph structure.
-
-### Visualizing architecture
-The graph structure can be plotted with `evaluable_architecture.visualize()` (requires
-matplotlib), which plots the dependency graph.
-By default, the nodes are labeled with their module name. If a module name is long 
-and/or a module has many submodules, the labels may crowd the plot. This can be 
-addressed by specifying (short) aliases for the module names by using the keyword argument 
-`aliases`. An alias will replace the module name in the label of the respective module
-and all its submodules unless the submodule also has an alias, in that case the
-submodule's alias takes priority.
-
-#### Alias Examples:
-Consider an architecture with modules `long_root_name, long_root_name.submodule,
-long_root_name.submodule.sub_submodule, long_root_name.other_submodule` then the 
-resulting labels for a given aliases are:
-
-
-| aliases                                                    |  labels                                               |
-|------------------------------------------------------------|----------------------------------------------------------------|
-| `{'long_root_name': 'r'}`                                   | `r, r.submodule, r.submodule.sub_submodule, r.other_submodule` |
-| `{'long_root_name': 'r', 'long_root_name.submodule': 'sub'}` | `r, sub, sub.sub_submodule, r.other_submodule`                 |
-
-## Testing your architecture based on PlantUML component diagrams
-### General
-Instead of hand-crafting a number of rules, you can supply the path to a PlantUML component diagram via a `DiagramRule`. PyTestArch will
-then parse this file and generate rules based on the connections of nodes in this diagram. Connections between two nodes
-will be converted into 'should only' or 'should' import rules; the absence of a connection will be converted into
-'should not' import rules. All rules are then evaluated against an Evaluable.
-
-Currently, parent-child relationships between modules are not supported. This means that for module X and its submodule Y, an edge between them in the diagram
-will be interpreted as an 'imports' relationship, not a 'submodule relationship'.
-
-There are two options for naming the components in your diagram:
-
-1) Use their fully qualified name, starting at the root module. For example, in the complex example above, this could be "src.A.fileA" instead of just "fileA" <br>
-2) Use only the name of the module itself, for example "fileA". Then you need to supply the prefix left out in the diagrams relative to your root module. In our example,
-    this would be "src.A". PyTestArch will then prefix all components in the diagram with this string. <br>
-
-
-### Supported PlantUML language features
-Syntactical requirements for .puml files:
-* start of dependency definition needs to be tagged with @startuml
-* end of dependency definition needs to be tagged with @enduml
-* all text outside these tags is ignored
-* component names must be enclosed in square brackets
-* exception: if a component as been given an alias via `[module name] as alias`, then the alias should not be wrapped in square brackets
-* dependencies must be with either -->, ->, <--, <-, -text->, or <-text-. The dependee is to be placed on the side of the arrow head, the dependor on the opposite side
-
-
-
-## Additional Notes
-### root_path vs. module_path in get_evaluable_architecture
-The `root_path` should point towards the most top level module. In the example above, this would be `src` - not `test_project`,
-as `test_project` is not the top level code directory.
-The `module_path` is the module where the dependency scan will start. It has to be either identical to `root_path` or 
-a submodule of it.
-
-
-### Module names
-In all rules, modules have to be referred to by their fully qualified name, meaning relative to the `root_path` - not the
-`module_path`! This helps to distinguish between internal and external modules.
diff --git a/docs/features/general.md b/docs/features/general.md
new file mode 100644
index 0000000..f410a2d
--- /dev/null
+++ b/docs/features/general.md
@@ -0,0 +1,129 @@
+# General Concept
+PyTestArch creates an Abstract Syntax Tree for each Python file it scans. It then extracts only the information about 
+module imports from the AST and converts them to an internal representation. This is currently a graph supplied by the 
+[NetworkX](https://networkx.org/) library.
+
+When the user defines a rule and evaluates the internal representation against it, the rule is converted into a set of
+graph operations that are then carried out on the previously generated graph.
+
+## Generating the evaluable architecture representation
+When scanning and processing the requested modules, PyTestArch executes the following step:
+1) Parse all files starting at the requested `module_path`. This only takes python source files into account that are not explicitly excluded.
+
+2) Convert the generated AST into custom dependency representations. In this step, it is ensured that all internal modules (either importing or imported) receive their fully 
+qualified name.
+
+3) Generate a list of all modules that were parsed. This list is used to differentiate between external and internal dependencies (external dependencies will not have been parsed).
+
+4) If not requested otherwise, external dependencies will be filtered out.
+
+5) If external dependencies should be included, they will be added to the list of modules.
+
+6) The dependency representations are converted to a graph structure.
+
+
+## Simple Example
+
+### Parsing all files
+Consider the following project structure:
+```
+my_project/
+    src/
+        main.py
+        util.py
+        util_test.py
+```
+
+Both `main.py` and `util_test.py` are importing a util function defined in `util.py`; no other imports exist.
+
+With "my_project" as the project's root folder and "src" as the folder to evaluate, this will create the following graph:
+
+![Simple Example Graph](../resources/docu_simple_example.png)
+
+Each module has a direct path to all submodules, in this case: "src" is connected to all three modules it contains, 
+"src.main", "src.util", and "src.util_test".
+
+In addition, a module that imports another module has a directed edge linking it to this module. For example, "src.main"
+imports "src.util" and therefore the graph has an edge connecting these modules, with the arrow head pointing at the imported
+module, in this case "src.util".
+
+### Excluding some files
+
+In the example above, the `util_test.py` file is included in the graph and will be considered when evaluating the architecture.
+If certain files should be excluded from the analysis, this can be done by specifying an exclusion pattern. In our example,
+if we want to exclude `util_test.py`, we could use:
+```
+from pytestarch.pytestarch import get_evaluable_architecture
+
+evaluable = get_evaluable_architecture("/home/my_project", "/home/my_project/src", ("*_test.py"))
+```
+This will exclude all files with names ending in "_test.py". It is also possible to exclude directories.
+
+
+## More complex example
+As a basis for describing the query language, let's consider a more complex example:
+```
+/test_project
+    /src
+       __init__.py
+       /A
+            __init__.py
+            fileA.py
+            /A1
+                __init__.py
+                fileA1.py
+                fileA1_b.py
+                /A11
+                    __init__.py
+                    fileA11.py
+            /A2
+                __init__.py
+                fileA2.py
+       /B
+            __init__.py
+            fileB.py
+            /B1
+                __init__.py
+                fileB1.py
+                fileB2.py
+       /C 
+            __init__.py
+            fileC.py
+```
+
+With the following list of imports:
+<ul>
+    <li>fileA imports fileC</li>
+    <li>fileA11 imports fileB1</li>
+    <li>fileA2 imports fileC</li>
+    <li>fileB imports fileA11</li>
+    <li>fileB2 imports fileA11</li>
+    <li>fileC imports the built-in os module</li>
+</ul>
+
+This creates the following graph (excluding all `__init__.py`):
+
+![Complex Example](../resources/docu_complex_example.png)
+
+Note that the module names in the diagram have been abbreviated: For example, the node "fileA" is actually named "src.A.fileA".
+
+Most of the edges in this graph are due to parent-child relationships between the modules. However, five of the six import
+relationships defined above are present in the graph - only fileC has no connection to an "os" node; in fact, there is no
+node names "os" at all.
+
+This has been achieved by setting the `exclude_external_libraries` flag in the `get_evaluable_architecture` function. All modules that 
+not located hierarchically below the root path, in this case "test_project", will be excluded from the graph.
+
+
+<br>
+# Additional Notes
+## root_path vs. module_path in get_evaluable_architecture
+The `root_path` should point towards the most top level module. In the example above, this would be `src` - not `test_project`,
+as `test_project` is not the top level code directory.
+The `module_path` is the module where the dependency scan will start. It has to be either identical to `root_path` or 
+a submodule of it.
+
+
+## Module names
+In all rules, modules have to be referred to by their fully qualified name, meaning relative to the `root_path` - not the
+`module_path`! This helps to distinguish between internal and external modules.
\ No newline at end of file
diff --git a/docs/features/layer_architecture_checks.md b/docs/features/layer_architecture_checks.md
new file mode 100644
index 0000000..5ad7c30
--- /dev/null
+++ b/docs/features/layer_architecture_checks.md
@@ -0,0 +1,84 @@
+# Checking dependencies between layers in a layered architecture
+
+## Why are new rule types needed?
+Sometimes, rules do not concern individual modules, but entire layers of an application, which in turn contains one or 
+more modules. 
+The module dependency check methods can of course also be used here, but often they may not be sufficient. Consider an
+example project with three modules (M, N, O). M imports O, but N does not import anything. M and N belong to one layer -
+let's call it 'import', O belongs to another layer called 'model'. The 'import' layer is expected to import the 'model' 
+layer, but not vice versa.
+
+Using the module dependency query language as described in [Module Dependency Rules](module_import_checks.md), this 
+expected import behavior would be difficult to specify, as a rule like 
+
+```
+Rule() \
+    .modules_that()\ 
+    .are_named(["M", "N"]) \
+    .should() \
+    .import_modules_that() \
+    .are_named("O")
+```
+
+would require *both* M and N to import O.
+
+
+## Layer architecture rules
+Instead, one can use the layer architecture rules. Using this type of rule has two steps: defining a layered 
+architecture, and then defining a rule based on it.
+
+### Layered architecture
+The LayeredArchitecture object should be used to group modules into layers. Not all modules have to belong to a layer
+(for more details on how this influences layer rules, see below). A layered architecture can be defined like this:
+```
+arch = LayeredArchitecture() \
+            .layer("import") \
+            .containing_modules(["M", "N"]) \
+            .layer("model") \
+            .containing_modules("O")
+```
+In this layered architecture, the modules M and N belong to layer 'import'; while module O belongs to layer 'model'.
+
+
+⚠ Note that PyTestArch assumes that all submodules of a module belong to the same layer as their parent module!
+
+
+### Layer rules
+After defining a layered architecture, layer rules can not be defined based on it. Using the `arch` object defined
+above, the dependency rule that could not properly be defined in the regular syntax can then be defined as:
+```
+rule = LayerRule() \
+            .based_on(arch). \  # needs to be passed in first
+            .layers_that() \
+            .are_named("import") \
+            .should_only() \
+            .access_layers_that() \
+            .are_named("model")
+```
+All modules within one layer are treated as one entity. This means that this rule will apply to an evaluable
+architecture, as long either module M or module N imports module O, and neither imports anything else.
+This means that for rules that require no dependency between layers, no module of these layers can import another module
+of these layers. However, for rules that only require one dependency to be present, it is sufficient if there is one 
+dependency between one module from one layer and one module from the other layer.
+
+
+Note that the "import" verb markers of the module dependency rules are replaced by "access" here, to mark the difference
+between these rule types and also to be more in line with ArchUnit's syntax.
+
+
+Not all modules need to belong to layers,
+but then no layer architecture rules can be specified for them. They will however be considered when evaluating whether 
+a layer architecture rules applies. For example, module M of layer A imports module N from layer B and module O, which 
+is not part of any layer. A rule like
+```
+LayerRule().based_on(arch).layers_that().are_named("A").should_only().access_layers_that().are_named("B")
+```
+would raise an AssertionException, as module M also imports module O.
+
+
+For an explanation of the meaning of the different verb markers used (e.g. should, except) refer to 
+[Module Dependency Rules](module_import_checks.md). The two types of query syntax differ in the following points:
+1) "import" is replaced by "access"
+2) Only "are_named" rule subject and object specifications are allowed
+3) The layer architecture rules need to be started by passing in the layered architecture object that contains the layer 
+definitions.
\ No newline at end of file
diff --git a/docs/features/module_import_checks.md b/docs/features/module_import_checks.md
new file mode 100644
index 0000000..20b670d
--- /dev/null
+++ b/docs/features/module_import_checks.md
@@ -0,0 +1,107 @@
+# Checking dependencies between modules
+
+## General Structure
+
+The query language that can be used to define architectural rules follows this structure: 
+
+RULE_SUBJECT - VERB_MARKER_1 - IMPORT_TYPE - VERB_MARKER_2 - RULE_OBJECT
+
+The meaning of these structural markers is described in the table below.
+
+| Marker       | Description                                            | Example                            |
+|--------------|--------------------------------------------------------|------------------------------------|
+| RULE_SUBJECT | module(s) to be checked                                | modules that are named 'B'         |
+| RULE_OBJECT  | module(s) to check against                             | modules that are submodules of 'A' |
+| IMPORT_TYPE | expected type of import relationship | be imported by                     |
+| VERB_MARKER_1 | defines the expected behavior, part I                  | should not                         |
+| VERB_MARKER_2 | defines the expected behavior, part II                 | except                             |
+
+The examples given in the table above combined form the rule `modules that are named 'B' (RULE_SUBJECT) should not (VERB_MARKER_1) be imported by (IMPORT_TYPE) modules except (VERB_MARKER_2) modules that are submodules of "A" (RULE_OBJECT)`.
+
+Looking at the diagram in the section Complex Example, we can see that this rule holds - the only module importing 
+from the "B" module is "fileA11", which is a submodule of "A".
+
+
+## Features
+
+Currently, the following markers are supported by PyTestArch:
+
+### RULE_SUBJECT
+* are_named("X"): applies to module named "X" (and also to its submodules)
+* are_submodules_of("Y"): applies to submodules of module named "Y", but not "Y" itself
+* have_name_containing("*Z*"): applies to modules with names containing Z. The syntax is the same as for the file exclusion mechanism.
+
+### RULE_OBJECT
+same as RULE_SUBJECT, with an additional
+
+anything: can only be used in combination with should_not()
+
+In order to reduce the number of possible API component combinations, this rule object has been combined with two
+verbs into: `import_anything()` and `be_imported_by_anything()`. 
+
+
+Both RULE_SUBJECTs and RULE_OBJECTs can be specified in batch, i.e. via a list of values. If multiple rule subjects are
+specified, this has the same effect as defining a rule per rule subject.
+For example, the rule
+```
+modules_that() \
+    .are_named("1", "2") \
+    .should_only() \
+    .be_imported_by_modules_that() \
+    .are_named(["3", "4"])
+```
+is fulfilled, it the modules "1" and "2" both are not imported by any module other than "3" and "4", and if both "3" and "4" do import "1" and "2".
+
+
+### VERB_MARKER_1
+* should()
+* should_only()
+* should_not()
+
+### IMPORT_TYPE + VERB_MARKER_2
+* import_modules_that()
+* import_modules_except_modules_that()
+* be_imported_by_modules_that()
+* be_imported_by_modules_except_modules_that()
+
+VERB_MARKER_2 and IMPORT_TYPE have been conflated into one expression to improve readability.
+
+Markers from each category can be combined freely with all markers of all other categories. Example rules could be <br>
+```
+modules_that() \
+    .are_sub_modules_of("A") \
+    .should_only() \
+    .be_imported_by_modules_that() \
+    .are_sub_modules_of("B") \
+```
+(True in the above example)
+
+or <br>
+```
+modules_that() \
+    .are_named("C") \
+    .should_only() \
+    .be_imported_by_modules_that() \
+    .are_named("A2") \
+```
+(False, also imported by module "A")
+
+
+Most rules are so close to the English language that a detailed explanation seems unnecessary. An exception might be the
+VERB_MARKER_2 "except". A combination of this VERB_MARKER_2 and every type of VERB_MARKER_1 and IMPORT_TYPE is given below
+as reference (M1, M2 are used as RULE_SUBJECT and RULE_OBJECT respectively; pseudo-code for brevity):
+
+| Rule | Explanation |
+| ---- | ----------- |
+| M1 should import except M2 | M1 should import at least one module that isn't M2, but can also import M2|
+| M1 should only import except M2| M1 should import at least one module that isn't M2 and should not import M2 |
+| M1 should not import except M2| M1 should not import any module other than M2, but does not have to import M2 |
+| M1 should be imported except by M2 | at least one module that isn't M2 should import M1 (M2 can import M1 as well) |
+| M1 should only be imported except by M2 | at least one module that isn't M2 should import M1, and M2 cannot import M1 |
+| M1 should not be imported except by M2 | no module other than M2 should import M1, but M2 does not have to import M1 |
+
+
+There are two aliases to make rules easier:
+
+* 'M1 should not import anything' is equivalent to: 'M1 should not import anything except itself' (e.g. imports between its submodules are allowed, but no other imports)
+* 'M1 should not be imported by anything' is equivalent to: 'M1 should not be imported by anything except itself' (dito)
\ No newline at end of file
diff --git a/docs/features/plantuml.md b/docs/features/plantuml.md
new file mode 100644
index 0000000..a56351e
--- /dev/null
+++ b/docs/features/plantuml.md
@@ -0,0 +1,26 @@
+# Testing your architecture based on PlantUML component diagrams
+## General
+Instead of hand-crafting a number of rules, you can supply the path to a PlantUML component diagram via a `DiagramRule`. PyTestArch will
+then parse this file and generate rules based on the connections of nodes in this diagram. Connections between two nodes
+will be converted into 'should only' or 'should' import rules; the absence of a connection will be converted into
+'should not' import rules. All rules are then evaluated against an Evaluable.
+
+Currently, parent-child relationships between modules are not supported. This means that for module X and its submodule Y, an edge between them in the diagram
+will be interpreted as an 'imports' relationship, not a 'submodule relationship'.
+
+There are two options for naming the components in your diagram:
+
+1) Use their fully qualified name, starting at the root module. For example, in the complex example above, this could be "src.A.fileA" instead of just "fileA" <br>
+2) Use only the name of the module itself, for example "fileA". Then you need to supply the prefix left out in the diagrams relative to your root module. In our example,
+    this would be "src.A". PyTestArch will then prefix all components in the diagram with this string. <br>
+
+
+## Supported PlantUML language features
+Syntactical requirements for .puml files:
+* start of dependency definition needs to be tagged with @startuml
+* end of dependency definition needs to be tagged with @enduml
+* all text outside these tags is ignored
+* component names must be enclosed in square brackets
+* exception: if a component as been given an alias via `[module name] as alias`, then the alias should not be wrapped in square brackets
+* dependencies must be with either -->, ->, <--, <-, -text->, or <-text-. The dependee is to be placed on the side of the arrow head, the dependor on the opposite side
+
diff --git a/docs/features/visualization.md b/docs/features/visualization.md
new file mode 100644
index 0000000..fc9675d
--- /dev/null
+++ b/docs/features/visualization.md
@@ -0,0 +1,20 @@
+## Visualizing architecture
+The graph structure can be plotted with `evaluable_architecture.visualize()` (requires
+matplotlib), which plots the dependency graph.
+By default, the nodes are labeled with their module name. If a module name is long 
+and/or a module has many submodules, the labels may crowd the plot. This can be 
+addressed by specifying (short) aliases for the module names by using the keyword argument 
+`aliases`. An alias will replace the module name in the label of the respective module
+and all its submodules unless the submodule also has an alias, in that case the
+submodule's alias takes priority.
+
+## Alias Examples:
+Consider an architecture with modules `long_root_name, long_root_name.submodule,
+long_root_name.submodule.sub_submodule, long_root_name.other_submodule` then the 
+resulting labels for a given aliases are:
+
+
+| aliases                                                    |  labels                                               |
+|------------------------------------------------------------|----------------------------------------------------------------|
+| `{'long_root_name': 'r'}`                                   | `r, r.submodule, r.submodule.sub_submodule, r.other_submodule` |
+| `{'long_root_name': 'r', 'long_root_name.submodule': 'sub'}` | `r, sub, sub.sub_submodule, r.other_submodule`                 |
diff --git a/docs/index.md b/docs/index.md
index bc512f0..10d9450 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -24,14 +24,14 @@ you could use a fixture for this evaluable object.
 2) Define an architectural rule
 
 ```
-from pytestarch.query_language.base_language import Rule
-
-rule = Rule()
-        .modules_that()
-        .are_named("src.moduleB")
-        .should_not()
-        .be_imported_by_modules_that()
-        .are_sub_modules_of("src.moduleA"),
+from pytestarch.pytestarch import Rule
+
+rule = Rule() \
+        .modules_that() \
+        .are_named("src.moduleB") \
+        .should_not() \
+        .be_imported_by_modules_that() \
+        .are_sub_modules_of("src.moduleA")
 ```
 
 This rule represents the architectural requirements that a module named "src.moduleB" should not be imported by any module
diff --git a/docs/references/diagram_extension.md b/docs/references/diagram_extension.md
index af42679..2c2e477 100644
--- a/docs/references/diagram_extension.md
+++ b/docs/references/diagram_extension.md
@@ -1,11 +1,7 @@
 # Diagram Extension
 
-## ::: src.pytestarch.diagram_extension
-
 ## ::: src.pytestarch.diagram_extension.diagram_rule
 
 ## ::: src.pytestarch.diagram_extension.dependency_to_rule_converter
 
 ## ::: src.pytestarch.diagram_extension.diagram_parser
-
-## ::: src.pytestarch.diagram_extension.parsed_dependencies
\ No newline at end of file
diff --git a/docs/references/eval_structure.md b/docs/references/eval_structure.md
index 4b54fd5..b60baa7 100644
--- a/docs/references/eval_structure.md
+++ b/docs/references/eval_structure.md
@@ -1,11 +1,7 @@
 # Evaluation Structures
 
-## ::: src.pytestarch.eval_structure
-
 ## ::: src.pytestarch.eval_structure.evaluable_architecture
 
-## ::: src.pytestarch.eval_structure.evaluable_structures
-
 ## ::: src.pytestarch.eval_structure.evaluable_graph
 
 ## ::: src.pytestarch.eval_structure.networkxgraph
diff --git a/docs/references/eval_structure_generation.md b/docs/references/eval_structure_generation.md
index 44c9a64..51e7dc5 100644
--- a/docs/references/eval_structure_generation.md
+++ b/docs/references/eval_structure_generation.md
@@ -2,8 +2,6 @@
 
 ## ::: src.pytestarch.eval_structure_generation
 
-## ::: src.pytestarch.eval_structure_generation.file_import
-
 ## ::: src.pytestarch.eval_structure_generation.file_import.file_filter
 
 ## ::: src.pytestarch.eval_structure_generation.file_import.import_filter
@@ -14,7 +12,4 @@
 
 ## ::: src.pytestarch.eval_structure_generation.file_import.parser
 
-## ::: src.pytestarch.eval_structure_generation.graph_generation
-
-## ::: src.pytestarch.eval_structure_generation.graph_generation.graph_generator
 
diff --git a/docs/references/query_language.md b/docs/references/query_language.md
index 5ef8d1b..657aee9 100644
--- a/docs/references/query_language.md
+++ b/docs/references/query_language.md
@@ -1,9 +1,9 @@
 #  Query Language
 
-## ::: src.pytestarch.query_language
-
 ## ::: src.pytestarch.query_language.base_language
 
 ## ::: src.pytestarch.query_language.rule
 
+## ::: src.pytestarch.query_language.layered_architecture_rule
+
 ## ::: src.pytestarch.query_language.multiple_rule_applier
diff --git a/docs/references/rule_assessment.md b/docs/references/rule_assessment.md
index 78854db..fe4a124 100644
--- a/docs/references/rule_assessment.md
+++ b/docs/references/rule_assessment.md
@@ -1,16 +1,12 @@
 # Rule Assessment
 
-## ::: src.pytestarch.rule_assessment
-
-## ::: src.pytestarch.rule_assessment.error_message
-
 ## ::: src.pytestarch.rule_assessment.error_message.message_generator
 
-## ::: src.pytestarch.rule_assessment.rule_check
-
 ## ::: src.pytestarch.rule_assessment.rule_check.rule_matcher
 
-## ::: src.pytestarch.rule_assessment.rule_check.rule_violations
+## ::: src.pytestarch.rule_assessment.rule_check.rule_violation_detector
+
+## ::: src.pytestarch.rule_assessment.rule_check.layer_rule_violation_detector
 
 ## ::: src.pytestarch.rule_assessment.rule_check.behavior_requirement
 
diff --git a/mkdocs.yml b/mkdocs.yml
index bc44c9a..0132399 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -17,9 +17,14 @@ plugins:
   - search
 
 nav:
-  - index.md
-  - details.md
-  - changelog.md
+  - Home: 'index.md'
+  - Features:
+      - 'General Concept': 'features/general.md'
+      - 'Module Dependency Rules': 'features/module_import_checks.md'
+      - 'Layer Architecture Dependency Rules': 'features/layer_architecture_checks.md'
+      - 'Module Dependency Rule Generation from PlantUML Component Diagrams': 'features/plantuml.md'
+      - 'Visualization': 'features/visualization.md'
+  - Changelog: 'changelog.md'
   - Reference:
       - references/general.md
       - references/query_language.md
diff --git a/poetry.lock b/poetry.lock
index c4a023f..fbd12e8 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -2,18 +2,34 @@
 
 [[package]]
 name = "argcomplete"
-version = "2.0.0"
+version = "2.1.2"
 description = "Bash tab completion for argparse"
-category = "dev"
+category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
-    {file = "argcomplete-2.0.0-py2.py3-none-any.whl", hash = "sha256:cffa11ea77999bb0dd27bb25ff6dc142a6796142f68d45b1a26b11f58724561e"},
-    {file = "argcomplete-2.0.0.tar.gz", hash = "sha256:6372ad78c89d662035101418ae253668445b391755cfe94ea52f1b9d22425b20"},
+    {file = "argcomplete-2.1.2-py3-none-any.whl", hash = "sha256:4ba9cdaa28c361d251edce884cd50b4b1215d65cdc881bd204426cdde9f52731"},
+    {file = "argcomplete-2.1.2.tar.gz", hash = "sha256:fc82ef070c607b1559b5c720529d63b54d9dcf2dcfc2632b10e6372314a34457"},
 ]
 
 [package.extras]
-test = ["coverage", "flake8", "pexpect", "wheel"]
+lint = ["flake8", "mypy"]
+test = ["coverage", "flake8", "mypy", "pexpect", "wheel"]
+
+[[package]]
+name = "arrow"
+version = "1.2.3"
+description = "Better dates & times for Python"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+files = [
+    {file = "arrow-1.2.3-py3-none-any.whl", hash = "sha256:5a49ab92e3b7b71d96cd6bfcc4df14efefc9dfa96ea19045815914a6ab6b1fe2"},
+    {file = "arrow-1.2.3.tar.gz", hash = "sha256:3934b30ca1b9f292376d9db15b19446088d12ec58629bc3f0da28fd55fb633a1"},
+]
+
+[package.dependencies]
+python-dateutil = ">=2.7.0"
 
 [[package]]
 name = "attrs"
@@ -34,11 +50,56 @@ docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-
 tests = ["attrs[tests-no-zope]", "zope.interface"]
 tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"]
 
+[[package]]
+name = "b2"
+version = "3.8.0"
+description = "Command Line Tool for Backblaze B2"
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+    {file = "b2-3.8.0.tar.gz", hash = "sha256:c590f89438307b80d136f15889b34b03b09eaa65be0e1b1846492286ed57de45"},
+]
+
+[package.dependencies]
+argcomplete = ">=2,<3"
+arrow = ">=1.0.2,<2.0.0"
+b2sdk = ">=1.20.0,<2.0.0"
+docutils = "0.19"
+idna = {version = ">=2.2.0", markers = "platform_system == \"Java\""}
+phx-class-registry = "4.0.5"
+rst2ansi = "0.1.5"
+tabulate = "0.9.0"
+
+[package.extras]
+doc = ["sadisplay", "sphinx (<7.0)", "sphinx-argparse", "sphinx-autobuild", "sphinx-rtd-theme", "sphinxcontrib-plantuml"]
+license = ["pip-licenses (==3.5.5)", "pip-licenses (==4.0.3)"]
+
+[[package]]
+name = "b2sdk"
+version = "1.20.0"
+description = "Backblaze B2 SDK"
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+    {file = "b2sdk-1.20.0.tar.gz", hash = "sha256:b394d9fbdada1a4ffc0837cd6c930351f5fccc24cd0af23e41edd850d67fb687"},
+]
+
+[package.dependencies]
+arrow = ">=1.0.2,<2.0.0"
+logfury = ">=1.0.1,<2.0.0"
+requests = ">=2.9.1,<3.0.0"
+tqdm = ">=4.5.0,<5.0.0"
+
+[package.extras]
+doc = ["sadisplay", "sphinx (<6.0)", "sphinx-autobuild", "sphinx-rtd-theme", "sphinxcontrib-plantuml"]
+
 [[package]]
 name = "certifi"
 version = "2022.12.7"
 description = "Python package for providing Mozilla's CA Bundle."
-category = "dev"
+category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -60,100 +121,87 @@ files = [
 
 [[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"
+category = "main"
 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]]
@@ -175,7 +223,7 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""}
 name = "colorama"
 version = "0.4.6"
 description = "Cross-platform colored terminal text."
-category = "dev"
+category = "main"
 optional = false
 python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
 files = [
@@ -203,152 +251,138 @@ development = ["black", "flake8", "mypy", "pytest", "types-colorama"]
 
 [[package]]
 name = "contourpy"
-version = "1.0.6"
+version = "1.0.7"
 description = "Python library for calculating contours of 2D quadrilateral grids"
 category = "main"
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "contourpy-1.0.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:613c665529899b5d9fade7e5d1760111a0b011231277a0d36c49f0d3d6914bd6"},
-    {file = "contourpy-1.0.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:78ced51807ccb2f45d4ea73aca339756d75d021069604c2fccd05390dc3c28eb"},
-    {file = "contourpy-1.0.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b3b1bd7577c530eaf9d2bc52d1a93fef50ac516a8b1062c3d1b9bcec9ebe329b"},
-    {file = "contourpy-1.0.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8834c14b8c3dd849005e06703469db9bf96ba2d66a3f88ecc539c9a8982e0ee"},
-    {file = "contourpy-1.0.6-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4052a8a4926d4468416fc7d4b2a7b2a3e35f25b39f4061a7e2a3a2748c4fc48"},
-    {file = "contourpy-1.0.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c0e1308307a75e07d1f1b5f0f56b5af84538a5e9027109a7bcf6cb47c434e72"},
-    {file = "contourpy-1.0.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fc4e7973ed0e1fe689435842a6e6b330eb7ccc696080dda9a97b1a1b78e41db"},
-    {file = "contourpy-1.0.6-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:08e8d09d96219ace6cb596506fb9b64ea5f270b2fb9121158b976d88871fcfd1"},
-    {file = "contourpy-1.0.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f33da6b5d19ad1bb5e7ad38bb8ba5c426d2178928bc2b2c44e8823ea0ecb6ff3"},
-    {file = "contourpy-1.0.6-cp310-cp310-win32.whl", hash = "sha256:12a7dc8439544ed05c6553bf026d5e8fa7fad48d63958a95d61698df0e00092b"},
-    {file = "contourpy-1.0.6-cp310-cp310-win_amd64.whl", hash = "sha256:eadad75bf91897f922e0fb3dca1b322a58b1726a953f98c2e5f0606bd8408621"},
-    {file = "contourpy-1.0.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:913bac9d064cff033cf3719e855d4f1db9f1c179e0ecf3ba9fdef21c21c6a16a"},
-    {file = "contourpy-1.0.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46deb310a276cc5c1fd27958e358cce68b1e8a515fa5a574c670a504c3a3fe30"},
-    {file = "contourpy-1.0.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b64f747e92af7da3b85631a55d68c45a2d728b4036b03cdaba4bd94bcc85bd6f"},
-    {file = "contourpy-1.0.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50627bf76abb6ba291ad08db583161939c2c5fab38c38181b7833423ab9c7de3"},
-    {file = "contourpy-1.0.6-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:358f6364e4873f4d73360b35da30066f40387dd3c427a3e5432c6b28dd24a8fa"},
-    {file = "contourpy-1.0.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c78bfbc1a7bff053baf7e508449d2765964d67735c909b583204e3240a2aca45"},
-    {file = "contourpy-1.0.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e43255a83835a129ef98f75d13d643844d8c646b258bebd11e4a0975203e018f"},
-    {file = "contourpy-1.0.6-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:375d81366afd547b8558c4720337218345148bc2fcffa3a9870cab82b29667f2"},
-    {file = "contourpy-1.0.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b98c820608e2dca6442e786817f646d11057c09a23b68d2b3737e6dcb6e4a49b"},
-    {file = "contourpy-1.0.6-cp311-cp311-win32.whl", hash = "sha256:0e4854cc02006ad6684ce092bdadab6f0912d131f91c2450ce6dbdea78ee3c0b"},
-    {file = "contourpy-1.0.6-cp311-cp311-win_amd64.whl", hash = "sha256:d2eff2af97ea0b61381828b1ad6cd249bbd41d280e53aea5cccd7b2b31b8225c"},
-    {file = "contourpy-1.0.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5b117d29433fc8393b18a696d794961464e37afb34a6eeb8b2c37b5f4128a83e"},
-    {file = "contourpy-1.0.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:341330ed19074f956cb20877ad8d2ae50e458884bfa6a6df3ae28487cc76c768"},
-    {file = "contourpy-1.0.6-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:371f6570a81dfdddbb837ba432293a63b4babb942a9eb7aaa699997adfb53278"},
-    {file = "contourpy-1.0.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9447c45df407d3ecb717d837af3b70cfef432138530712263730783b3d016512"},
-    {file = "contourpy-1.0.6-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:730c27978a0003b47b359935478b7d63fd8386dbb2dcd36c1e8de88cbfc1e9de"},
-    {file = "contourpy-1.0.6-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:da1ef35fd79be2926ba80fbb36327463e3656c02526e9b5b4c2b366588b74d9a"},
-    {file = "contourpy-1.0.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:cd2bc0c8f2e8de7dd89a7f1c10b8844e291bca17d359373203ef2e6100819edd"},
-    {file = "contourpy-1.0.6-cp37-cp37m-win32.whl", hash = "sha256:3a1917d3941dd58732c449c810fa7ce46cc305ce9325a11261d740118b85e6f3"},
-    {file = "contourpy-1.0.6-cp37-cp37m-win_amd64.whl", hash = "sha256:06ca79e1efbbe2df795822df2fa173d1a2b38b6e0f047a0ec7903fbca1d1847e"},
-    {file = "contourpy-1.0.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e626cefff8491bce356221c22af5a3ea528b0b41fbabc719c00ae233819ea0bf"},
-    {file = "contourpy-1.0.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:dbe6fe7a1166b1ddd7b6d887ea6fa8389d3f28b5ed3f73a8f40ece1fc5a3d340"},
-    {file = "contourpy-1.0.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e13b31d1b4b68db60b3b29f8e337908f328c7f05b9add4b1b5c74e0691180109"},
-    {file = "contourpy-1.0.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a79d239fc22c3b8d9d3de492aa0c245533f4f4c7608e5749af866949c0f1b1b9"},
-    {file = "contourpy-1.0.6-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e8e686a6db92a46111a1ee0ee6f7fbfae4048f0019de207149f43ac1812cf95"},
-    {file = "contourpy-1.0.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acd2bd02f1a7adff3a1f33e431eb96ab6d7987b039d2946a9b39fe6fb16a1036"},
-    {file = "contourpy-1.0.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:03d1b9c6b44a9e30d554654c72be89af94fab7510b4b9f62356c64c81cec8b7d"},
-    {file = "contourpy-1.0.6-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b48d94386f1994db7c70c76b5808c12e23ed7a4ee13693c2fc5ab109d60243c0"},
-    {file = "contourpy-1.0.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:208bc904889c910d95aafcf7be9e677726df9ef71e216780170dbb7e37d118fa"},
-    {file = "contourpy-1.0.6-cp38-cp38-win32.whl", hash = "sha256:444fb776f58f4906d8d354eb6f6ce59d0a60f7b6a720da6c1ccb839db7c80eb9"},
-    {file = "contourpy-1.0.6-cp38-cp38-win_amd64.whl", hash = "sha256:9bc407a6af672da20da74823443707e38ece8b93a04009dca25856c2d9adadb1"},
-    {file = "contourpy-1.0.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:aa4674cf3fa2bd9c322982644967f01eed0c91bb890f624e0e0daf7a5c3383e9"},
-    {file = "contourpy-1.0.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6f56515e7c6fae4529b731f6c117752247bef9cdad2b12fc5ddf8ca6a50965a5"},
-    {file = "contourpy-1.0.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:344cb3badf6fc7316ad51835f56ac387bdf86c8e1b670904f18f437d70da4183"},
-    {file = "contourpy-1.0.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b1e66346acfb17694d46175a0cea7d9036f12ed0c31dfe86f0f405eedde2bdd"},
-    {file = "contourpy-1.0.6-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8468b40528fa1e15181cccec4198623b55dcd58306f8815a793803f51f6c474a"},
-    {file = "contourpy-1.0.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1dedf4c64185a216c35eb488e6f433297c660321275734401760dafaeb0ad5c2"},
-    {file = "contourpy-1.0.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:494efed2c761f0f37262815f9e3c4bb9917c5c69806abdee1d1cb6611a7174a0"},
-    {file = "contourpy-1.0.6-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:75a2e638042118118ab39d337da4c7908c1af74a8464cad59f19fbc5bbafec9b"},
-    {file = "contourpy-1.0.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a628bba09ba72e472bf7b31018b6281fd4cc903f0888049a3724afba13b6e0b8"},
-    {file = "contourpy-1.0.6-cp39-cp39-win32.whl", hash = "sha256:e1739496c2f0108013629aa095cc32a8c6363444361960c07493818d0dea2da4"},
-    {file = "contourpy-1.0.6-cp39-cp39-win_amd64.whl", hash = "sha256:a457ee72d9032e86730f62c5eeddf402e732fdf5ca8b13b41772aa8ae13a4563"},
-    {file = "contourpy-1.0.6-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d912f0154a20a80ea449daada904a7eb6941c83281a9fab95de50529bfc3a1da"},
-    {file = "contourpy-1.0.6-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4081918147fc4c29fad328d5066cfc751da100a1098398742f9f364be63803fc"},
-    {file = "contourpy-1.0.6-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0537cc1195245bbe24f2913d1f9211b8f04eb203de9044630abd3664c6cc339c"},
-    {file = "contourpy-1.0.6-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcd556c8fc37a342dd636d7eef150b1399f823a4462f8c968e11e1ebeabee769"},
-    {file = "contourpy-1.0.6-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:f6ca38dd8d988eca8f07305125dec6f54ac1c518f1aaddcc14d08c01aebb6efc"},
-    {file = "contourpy-1.0.6-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c1baa49ab9fedbf19d40d93163b7d3e735d9cd8d5efe4cce9907902a6dad391f"},
-    {file = "contourpy-1.0.6-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:211dfe2bd43bf5791d23afbe23a7952e8ac8b67591d24be3638cabb648b3a6eb"},
-    {file = "contourpy-1.0.6-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c38c6536c2d71ca2f7e418acaf5bca30a3af7f2a2fa106083c7d738337848dbe"},
-    {file = "contourpy-1.0.6-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b1ee48a130da4dd0eb8055bbab34abf3f6262957832fd575e0cab4979a15a41"},
-    {file = "contourpy-1.0.6-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5641927cc5ae66155d0c80195dc35726eae060e7defc18b7ab27600f39dd1fe7"},
-    {file = "contourpy-1.0.6-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ee394502026d68652c2824348a40bf50f31351a668977b51437131a90d777ea"},
-    {file = "contourpy-1.0.6-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b97454ed5b1368b66ed414c754cba15b9750ce69938fc6153679787402e4cdf"},
-    {file = "contourpy-1.0.6-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0236875c5a0784215b49d00ebbe80c5b6b5d5244b3655a36dda88105334dea17"},
-    {file = "contourpy-1.0.6-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84c593aeff7a0171f639da92cb86d24954bbb61f8a1b530f74eb750a14685832"},
-    {file = "contourpy-1.0.6-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:9b0e7fe7f949fb719b206548e5cde2518ffb29936afa4303d8a1c4db43dcb675"},
-    {file = "contourpy-1.0.6.tar.gz", hash = "sha256:6e459ebb8bb5ee4c22c19cc000174f8059981971a33ce11e17dddf6aca97a142"},
+    {file = "contourpy-1.0.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:95c3acddf921944f241b6773b767f1cbce71d03307270e2d769fd584d5d1092d"},
+    {file = "contourpy-1.0.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc1464c97579da9f3ab16763c32e5c5d5bb5fa1ec7ce509a4ca6108b61b84fab"},
+    {file = "contourpy-1.0.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8acf74b5d383414401926c1598ed77825cd530ac7b463ebc2e4f46638f56cce6"},
+    {file = "contourpy-1.0.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c71fdd8f1c0f84ffd58fca37d00ca4ebaa9e502fb49825484da075ac0b0b803"},
+    {file = "contourpy-1.0.7-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f99e9486bf1bb979d95d5cffed40689cb595abb2b841f2991fc894b3452290e8"},
+    {file = "contourpy-1.0.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87f4d8941a9564cda3f7fa6a6cd9b32ec575830780677932abdec7bcb61717b0"},
+    {file = "contourpy-1.0.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9e20e5a1908e18aaa60d9077a6d8753090e3f85ca25da6e25d30dc0a9e84c2c6"},
+    {file = "contourpy-1.0.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a877ada905f7d69b2a31796c4b66e31a8068b37aa9b78832d41c82fc3e056ddd"},
+    {file = "contourpy-1.0.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6381fa66866b0ea35e15d197fc06ac3840a9b2643a6475c8fff267db8b9f1e69"},
+    {file = "contourpy-1.0.7-cp310-cp310-win32.whl", hash = "sha256:3c184ad2433635f216645fdf0493011a4667e8d46b34082f5a3de702b6ec42e3"},
+    {file = "contourpy-1.0.7-cp310-cp310-win_amd64.whl", hash = "sha256:3caea6365b13119626ee996711ab63e0c9d7496f65641f4459c60a009a1f3e80"},
+    {file = "contourpy-1.0.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ed33433fc3820263a6368e532f19ddb4c5990855e4886088ad84fd7c4e561c71"},
+    {file = "contourpy-1.0.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:38e2e577f0f092b8e6774459317c05a69935a1755ecfb621c0a98f0e3c09c9a5"},
+    {file = "contourpy-1.0.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ae90d5a8590e5310c32a7630b4b8618cef7563cebf649011da80874d0aa8f414"},
+    {file = "contourpy-1.0.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:130230b7e49825c98edf0b428b7aa1125503d91732735ef897786fe5452b1ec2"},
+    {file = "contourpy-1.0.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58569c491e7f7e874f11519ef46737cea1d6eda1b514e4eb5ac7dab6aa864d02"},
+    {file = "contourpy-1.0.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d43960d809c4c12508a60b66cb936e7ed57d51fb5e30b513934a4a23874fae"},
+    {file = "contourpy-1.0.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:152fd8f730c31fd67fe0ffebe1df38ab6a669403da93df218801a893645c6ccc"},
+    {file = "contourpy-1.0.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9056c5310eb1daa33fc234ef39ebfb8c8e2533f088bbf0bc7350f70a29bde1ac"},
+    {file = "contourpy-1.0.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a9d7587d2fdc820cc9177139b56795c39fb8560f540bba9ceea215f1f66e1566"},
+    {file = "contourpy-1.0.7-cp311-cp311-win32.whl", hash = "sha256:4ee3ee247f795a69e53cd91d927146fb16c4e803c7ac86c84104940c7d2cabf0"},
+    {file = "contourpy-1.0.7-cp311-cp311-win_amd64.whl", hash = "sha256:5caeacc68642e5f19d707471890f037a13007feba8427eb7f2a60811a1fc1350"},
+    {file = "contourpy-1.0.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fd7dc0e6812b799a34f6d12fcb1000539098c249c8da54f3566c6a6461d0dbad"},
+    {file = "contourpy-1.0.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0f9d350b639db6c2c233d92c7f213d94d2e444d8e8fc5ca44c9706cf72193772"},
+    {file = "contourpy-1.0.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e96a08b62bb8de960d3a6afbc5ed8421bf1a2d9c85cc4ea73f4bc81b4910500f"},
+    {file = "contourpy-1.0.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:031154ed61f7328ad7f97662e48660a150ef84ee1bc8876b6472af88bf5a9b98"},
+    {file = "contourpy-1.0.7-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e9ebb4425fc1b658e13bace354c48a933b842d53c458f02c86f371cecbedecc"},
+    {file = "contourpy-1.0.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efb8f6d08ca7998cf59eaf50c9d60717f29a1a0a09caa46460d33b2924839dbd"},
+    {file = "contourpy-1.0.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6c180d89a28787e4b73b07e9b0e2dac7741261dbdca95f2b489c4f8f887dd810"},
+    {file = "contourpy-1.0.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b8d587cc39057d0afd4166083d289bdeff221ac6d3ee5046aef2d480dc4b503c"},
+    {file = "contourpy-1.0.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:769eef00437edf115e24d87f8926955f00f7704bede656ce605097584f9966dc"},
+    {file = "contourpy-1.0.7-cp38-cp38-win32.whl", hash = "sha256:62398c80ef57589bdbe1eb8537127321c1abcfdf8c5f14f479dbbe27d0322e66"},
+    {file = "contourpy-1.0.7-cp38-cp38-win_amd64.whl", hash = "sha256:57119b0116e3f408acbdccf9eb6ef19d7fe7baf0d1e9aaa5381489bc1aa56556"},
+    {file = "contourpy-1.0.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:30676ca45084ee61e9c3da589042c24a57592e375d4b138bd84d8709893a1ba4"},
+    {file = "contourpy-1.0.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3e927b3868bd1e12acee7cc8f3747d815b4ab3e445a28d2e5373a7f4a6e76ba1"},
+    {file = "contourpy-1.0.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:366a0cf0fc079af5204801786ad7a1c007714ee3909e364dbac1729f5b0849e5"},
+    {file = "contourpy-1.0.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89ba9bb365446a22411f0673abf6ee1fea3b2cf47b37533b970904880ceb72f3"},
+    {file = "contourpy-1.0.7-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:71b0bf0c30d432278793d2141362ac853859e87de0a7dee24a1cea35231f0d50"},
+    {file = "contourpy-1.0.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7281244c99fd7c6f27c1c6bfafba878517b0b62925a09b586d88ce750a016d2"},
+    {file = "contourpy-1.0.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b6d0f9e1d39dbfb3977f9dd79f156c86eb03e57a7face96f199e02b18e58d32a"},
+    {file = "contourpy-1.0.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7f6979d20ee5693a1057ab53e043adffa1e7418d734c1532e2d9e915b08d8ec2"},
+    {file = "contourpy-1.0.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5dd34c1ae752515318224cba7fc62b53130c45ac6a1040c8b7c1a223c46e8967"},
+    {file = "contourpy-1.0.7-cp39-cp39-win32.whl", hash = "sha256:c5210e5d5117e9aec8c47d9156d1d3835570dd909a899171b9535cb4a3f32693"},
+    {file = "contourpy-1.0.7-cp39-cp39-win_amd64.whl", hash = "sha256:60835badb5ed5f4e194a6f21c09283dd6e007664a86101431bf870d9e86266c4"},
+    {file = "contourpy-1.0.7-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ce41676b3d0dd16dbcfabcc1dc46090aaf4688fd6e819ef343dbda5a57ef0161"},
+    {file = "contourpy-1.0.7-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a011cf354107b47c58ea932d13b04d93c6d1d69b8b6dce885e642531f847566"},
+    {file = "contourpy-1.0.7-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31a55dccc8426e71817e3fe09b37d6d48ae40aae4ecbc8c7ad59d6893569c436"},
+    {file = "contourpy-1.0.7-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69f8ff4db108815addd900a74df665e135dbbd6547a8a69333a68e1f6e368ac2"},
+    {file = "contourpy-1.0.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:efe99298ba37e37787f6a2ea868265465410822f7bea163edcc1bd3903354ea9"},
+    {file = "contourpy-1.0.7-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a1e97b86f73715e8670ef45292d7cc033548266f07d54e2183ecb3c87598888f"},
+    {file = "contourpy-1.0.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc331c13902d0f50845099434cd936d49d7a2ca76cb654b39691974cb1e4812d"},
+    {file = "contourpy-1.0.7-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:24847601071f740837aefb730e01bd169fbcaa610209779a78db7ebb6e6a7051"},
+    {file = "contourpy-1.0.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abf298af1e7ad44eeb93501e40eb5a67abbf93b5d90e468d01fc0c4451971afa"},
+    {file = "contourpy-1.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:64757f6460fc55d7e16ed4f1de193f362104285c667c112b50a804d482777edd"},
+    {file = "contourpy-1.0.7.tar.gz", hash = "sha256:d8165a088d31798b59e91117d1f5fc3df8168d8b48c4acc10fc0df0d0bdbcc5e"},
 ]
 
 [package.dependencies]
 numpy = ">=1.16"
 
 [package.extras]
-bokeh = ["bokeh", "selenium"]
-docs = ["docutils (<0.18)", "sphinx (<=5.2.0)", "sphinx-rtd-theme"]
-test = ["Pillow", "flake8", "isort", "matplotlib", "pytest"]
-test-minimal = ["pytest"]
-test-no-codebase = ["Pillow", "matplotlib", "pytest"]
+bokeh = ["bokeh", "chromedriver", "selenium"]
+docs = ["furo", "sphinx-copybutton"]
+mypy = ["contourpy[bokeh]", "docutils-stubs", "mypy (==0.991)", "types-Pillow"]
+test = ["Pillow", "matplotlib", "pytest"]
+test-no-images = ["pytest"]
 
 [[package]]
 name = "coverage"
-version = "7.0.5"
+version = "7.2.3"
 description = "Code coverage measurement for Python"
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "coverage-7.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2a7f23bbaeb2a87f90f607730b45564076d870f1fb07b9318d0c21f36871932b"},
-    {file = "coverage-7.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c18d47f314b950dbf24a41787ced1474e01ca816011925976d90a88b27c22b89"},
-    {file = "coverage-7.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef14d75d86f104f03dea66c13188487151760ef25dd6b2dbd541885185f05f40"},
-    {file = "coverage-7.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66e50680e888840c0995f2ad766e726ce71ca682e3c5f4eee82272c7671d38a2"},
-    {file = "coverage-7.0.5-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9fed35ca8c6e946e877893bbac022e8563b94404a605af1d1e6accc7eb73289"},
-    {file = "coverage-7.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d8d04e755934195bdc1db45ba9e040b8d20d046d04d6d77e71b3b34a8cc002d0"},
-    {file = "coverage-7.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e109f1c9a3ece676597831874126555997c48f62bddbcace6ed17be3e372de8"},
-    {file = "coverage-7.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0a1890fca2962c4f1ad16551d660b46ea77291fba2cc21c024cd527b9d9c8809"},
-    {file = "coverage-7.0.5-cp310-cp310-win32.whl", hash = "sha256:be9fcf32c010da0ba40bf4ee01889d6c737658f4ddff160bd7eb9cac8f094b21"},
-    {file = "coverage-7.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:cbfcba14a3225b055a28b3199c3d81cd0ab37d2353ffd7f6fd64844cebab31ad"},
-    {file = "coverage-7.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:30b5fec1d34cc932c1bc04017b538ce16bf84e239378b8f75220478645d11fca"},
-    {file = "coverage-7.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1caed2367b32cc80a2b7f58a9f46658218a19c6cfe5bc234021966dc3daa01f0"},
-    {file = "coverage-7.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d254666d29540a72d17cc0175746cfb03d5123db33e67d1020e42dae611dc196"},
-    {file = "coverage-7.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19245c249aa711d954623d94f23cc94c0fd65865661f20b7781210cb97c471c0"},
-    {file = "coverage-7.0.5-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b05ed4b35bf6ee790832f68932baf1f00caa32283d66cc4d455c9e9d115aafc"},
-    {file = "coverage-7.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:29de916ba1099ba2aab76aca101580006adfac5646de9b7c010a0f13867cba45"},
-    {file = "coverage-7.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e057e74e53db78122a3979f908973e171909a58ac20df05c33998d52e6d35757"},
-    {file = "coverage-7.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:411d4ff9d041be08fdfc02adf62e89c735b9468f6d8f6427f8a14b6bb0a85095"},
-    {file = "coverage-7.0.5-cp311-cp311-win32.whl", hash = "sha256:52ab14b9e09ce052237dfe12d6892dd39b0401690856bcfe75d5baba4bfe2831"},
-    {file = "coverage-7.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:1f66862d3a41674ebd8d1a7b6f5387fe5ce353f8719040a986551a545d7d83ea"},
-    {file = "coverage-7.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b69522b168a6b64edf0c33ba53eac491c0a8f5cc94fa4337f9c6f4c8f2f5296c"},
-    {file = "coverage-7.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436e103950d05b7d7f55e39beeb4d5be298ca3e119e0589c0227e6d0b01ee8c7"},
-    {file = "coverage-7.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c56bec53d6e3154eaff6ea941226e7bd7cc0d99f9b3756c2520fc7a94e6d96"},
-    {file = "coverage-7.0.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a38362528a9115a4e276e65eeabf67dcfaf57698e17ae388599568a78dcb029"},
-    {file = "coverage-7.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f67472c09a0c7486e27f3275f617c964d25e35727af952869dd496b9b5b7f6a3"},
-    {file = "coverage-7.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:220e3fa77d14c8a507b2d951e463b57a1f7810a6443a26f9b7591ef39047b1b2"},
-    {file = "coverage-7.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ecb0f73954892f98611e183f50acdc9e21a4653f294dfbe079da73c6378a6f47"},
-    {file = "coverage-7.0.5-cp37-cp37m-win32.whl", hash = "sha256:d8f3e2e0a1d6777e58e834fd5a04657f66affa615dae61dd67c35d1568c38882"},
-    {file = "coverage-7.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:9e662e6fc4f513b79da5d10a23edd2b87685815b337b1a30cd11307a6679148d"},
-    {file = "coverage-7.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:790e4433962c9f454e213b21b0fd4b42310ade9c077e8edcb5113db0818450cb"},
-    {file = "coverage-7.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:49640bda9bda35b057b0e65b7c43ba706fa2335c9a9896652aebe0fa399e80e6"},
-    {file = "coverage-7.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d66187792bfe56f8c18ba986a0e4ae44856b1c645336bd2c776e3386da91e1dd"},
-    {file = "coverage-7.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:276f4cd0001cd83b00817c8db76730938b1ee40f4993b6a905f40a7278103b3a"},
-    {file = "coverage-7.0.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95304068686545aa368b35dfda1cdfbbdbe2f6fe43de4a2e9baa8ebd71be46e2"},
-    {file = "coverage-7.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:17e01dd8666c445025c29684d4aabf5a90dc6ef1ab25328aa52bedaa95b65ad7"},
-    {file = "coverage-7.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea76dbcad0b7b0deb265d8c36e0801abcddf6cc1395940a24e3595288b405ca0"},
-    {file = "coverage-7.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:50a6adc2be8edd7ee67d1abc3cd20678987c7b9d79cd265de55941e3d0d56499"},
-    {file = "coverage-7.0.5-cp38-cp38-win32.whl", hash = "sha256:e4ce984133b888cc3a46867c8b4372c7dee9cee300335e2925e197bcd45b9e16"},
-    {file = "coverage-7.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:4a950f83fd3f9bca23b77442f3a2b2ea4ac900944d8af9993743774c4fdc57af"},
-    {file = "coverage-7.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c2155943896ac78b9b0fd910fb381186d0c345911f5333ee46ac44c8f0e43ab"},
-    {file = "coverage-7.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:54f7e9705e14b2c9f6abdeb127c390f679f6dbe64ba732788d3015f7f76ef637"},
-    {file = "coverage-7.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ee30375b409d9a7ea0f30c50645d436b6f5dfee254edffd27e45a980ad2c7f4"},
-    {file = "coverage-7.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b78729038abea6a5df0d2708dce21e82073463b2d79d10884d7d591e0f385ded"},
-    {file = "coverage-7.0.5-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13250b1f0bd023e0c9f11838bdeb60214dd5b6aaf8e8d2f110c7e232a1bff83b"},
-    {file = "coverage-7.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2c407b1950b2d2ffa091f4e225ca19a66a9bd81222f27c56bd12658fc5ca1209"},
-    {file = "coverage-7.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c76a3075e96b9c9ff00df8b5f7f560f5634dffd1658bafb79eb2682867e94f78"},
-    {file = "coverage-7.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f26648e1b3b03b6022b48a9b910d0ae209e2d51f50441db5dce5b530fad6d9b1"},
-    {file = "coverage-7.0.5-cp39-cp39-win32.whl", hash = "sha256:ba3027deb7abf02859aca49c865ece538aee56dcb4871b4cced23ba4d5088904"},
-    {file = "coverage-7.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:949844af60ee96a376aac1ded2a27e134b8c8d35cc006a52903fc06c24a3296f"},
-    {file = "coverage-7.0.5-pp37.pp38.pp39-none-any.whl", hash = "sha256:b9727ac4f5cf2cbf87880a63870b5b9730a8ae3a4a360241a0fdaa2f71240ff0"},
-    {file = "coverage-7.0.5.tar.gz", hash = "sha256:051afcbd6d2ac39298d62d340f94dbb6a1f31de06dfaf6fcef7b759dd3860c45"},
+    {file = "coverage-7.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e58c0d41d336569d63d1b113bd573db8363bc4146f39444125b7f8060e4e04f5"},
+    {file = "coverage-7.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:344e714bd0fe921fc72d97404ebbdbf9127bac0ca1ff66d7b79efc143cf7c0c4"},
+    {file = "coverage-7.2.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:974bc90d6f6c1e59ceb1516ab00cf1cdfbb2e555795d49fa9571d611f449bcb2"},
+    {file = "coverage-7.2.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0743b0035d4b0e32bc1df5de70fba3059662ace5b9a2a86a9f894cfe66569013"},
+    {file = "coverage-7.2.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d0391fb4cfc171ce40437f67eb050a340fdbd0f9f49d6353a387f1b7f9dd4fa"},
+    {file = "coverage-7.2.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4a42e1eff0ca9a7cb7dc9ecda41dfc7cbc17cb1d02117214be0561bd1134772b"},
+    {file = "coverage-7.2.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:be19931a8dcbe6ab464f3339966856996b12a00f9fe53f346ab3be872d03e257"},
+    {file = "coverage-7.2.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:72fcae5bcac3333a4cf3b8f34eec99cea1187acd55af723bcbd559adfdcb5535"},
+    {file = "coverage-7.2.3-cp310-cp310-win32.whl", hash = "sha256:aeae2aa38395b18106e552833f2a50c27ea0000122bde421c31d11ed7e6f9c91"},
+    {file = "coverage-7.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:83957d349838a636e768251c7e9979e899a569794b44c3728eaebd11d848e58e"},
+    {file = "coverage-7.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dfd393094cd82ceb9b40df4c77976015a314b267d498268a076e940fe7be6b79"},
+    {file = "coverage-7.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:182eb9ac3f2b4874a1f41b78b87db20b66da6b9cdc32737fbbf4fea0c35b23fc"},
+    {file = "coverage-7.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bb1e77a9a311346294621be905ea8a2c30d3ad371fc15bb72e98bfcfae532df"},
+    {file = "coverage-7.2.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca0f34363e2634deffd390a0fef1aa99168ae9ed2af01af4a1f5865e362f8623"},
+    {file = "coverage-7.2.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55416d7385774285b6e2a5feca0af9652f7f444a4fa3d29d8ab052fafef9d00d"},
+    {file = "coverage-7.2.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:06ddd9c0249a0546997fdda5a30fbcb40f23926df0a874a60a8a185bc3a87d93"},
+    {file = "coverage-7.2.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:fff5aaa6becf2c6a1699ae6a39e2e6fb0672c2d42eca8eb0cafa91cf2e9bd312"},
+    {file = "coverage-7.2.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ea53151d87c52e98133eb8ac78f1206498c015849662ca8dc246255265d9c3c4"},
+    {file = "coverage-7.2.3-cp311-cp311-win32.whl", hash = "sha256:8f6c930fd70d91ddee53194e93029e3ef2aabe26725aa3c2753df057e296b925"},
+    {file = "coverage-7.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:fa546d66639d69aa967bf08156eb8c9d0cd6f6de84be9e8c9819f52ad499c910"},
+    {file = "coverage-7.2.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b2317d5ed777bf5a033e83d4f1389fd4ef045763141d8f10eb09a7035cee774c"},
+    {file = "coverage-7.2.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be9824c1c874b73b96288c6d3de793bf7f3a597770205068c6163ea1f326e8b9"},
+    {file = "coverage-7.2.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c3b2803e730dc2797a017335827e9da6da0e84c745ce0f552e66400abdfb9a1"},
+    {file = "coverage-7.2.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f69770f5ca1994cb32c38965e95f57504d3aea96b6c024624fdd5bb1aa494a1"},
+    {file = "coverage-7.2.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1127b16220f7bfb3f1049ed4a62d26d81970a723544e8252db0efde853268e21"},
+    {file = "coverage-7.2.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:aa784405f0c640940595fa0f14064d8e84aff0b0f762fa18393e2760a2cf5841"},
+    {file = "coverage-7.2.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3146b8e16fa60427e03884301bf8209221f5761ac754ee6b267642a2fd354c48"},
+    {file = "coverage-7.2.3-cp37-cp37m-win32.whl", hash = "sha256:1fd78b911aea9cec3b7e1e2622c8018d51c0d2bbcf8faaf53c2497eb114911c1"},
+    {file = "coverage-7.2.3-cp37-cp37m-win_amd64.whl", hash = "sha256:0f3736a5d34e091b0a611964c6262fd68ca4363df56185902528f0b75dbb9c1f"},
+    {file = "coverage-7.2.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:981b4df72c93e3bc04478153df516d385317628bd9c10be699c93c26ddcca8ab"},
+    {file = "coverage-7.2.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c0045f8f23a5fb30b2eb3b8a83664d8dc4fb58faddf8155d7109166adb9f2040"},
+    {file = "coverage-7.2.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f760073fcf8f3d6933178d67754f4f2d4e924e321f4bb0dcef0424ca0215eba1"},
+    {file = "coverage-7.2.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c86bd45d1659b1ae3d0ba1909326b03598affbc9ed71520e0ff8c31a993ad911"},
+    {file = "coverage-7.2.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:172db976ae6327ed4728e2507daf8a4de73c7cc89796483e0a9198fd2e47b462"},
+    {file = "coverage-7.2.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d2a3a6146fe9319926e1d477842ca2a63fe99af5ae690b1f5c11e6af074a6b5c"},
+    {file = "coverage-7.2.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f649dd53833b495c3ebd04d6eec58479454a1784987af8afb77540d6c1767abd"},
+    {file = "coverage-7.2.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7c4ed4e9f3b123aa403ab424430b426a1992e6f4c8fd3cb56ea520446e04d152"},
+    {file = "coverage-7.2.3-cp38-cp38-win32.whl", hash = "sha256:eb0edc3ce9760d2f21637766c3aa04822030e7451981ce569a1b3456b7053f22"},
+    {file = "coverage-7.2.3-cp38-cp38-win_amd64.whl", hash = "sha256:63cdeaac4ae85a179a8d6bc09b77b564c096250d759eed343a89d91bce8b6367"},
+    {file = "coverage-7.2.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:20d1a2a76bb4eb00e4d36b9699f9b7aba93271c9c29220ad4c6a9581a0320235"},
+    {file = "coverage-7.2.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ea748802cc0de4de92ef8244dd84ffd793bd2e7be784cd8394d557a3c751e21"},
+    {file = "coverage-7.2.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21b154aba06df42e4b96fc915512ab39595105f6c483991287021ed95776d934"},
+    {file = "coverage-7.2.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd214917cabdd6f673a29d708574e9fbdb892cb77eb426d0eae3490d95ca7859"},
+    {file = "coverage-7.2.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c2e58e45fe53fab81f85474e5d4d226eeab0f27b45aa062856c89389da2f0d9"},
+    {file = "coverage-7.2.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:87ecc7c9a1a9f912e306997ffee020297ccb5ea388421fe62a2a02747e4d5539"},
+    {file = "coverage-7.2.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:387065e420aed3c71b61af7e82c7b6bc1c592f7e3c7a66e9f78dd178699da4fe"},
+    {file = "coverage-7.2.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ea3f5bc91d7d457da7d48c7a732beaf79d0c8131df3ab278e6bba6297e23c6c4"},
+    {file = "coverage-7.2.3-cp39-cp39-win32.whl", hash = "sha256:ae7863a1d8db6a014b6f2ff9c1582ab1aad55a6d25bac19710a8df68921b6e30"},
+    {file = "coverage-7.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:3f04becd4fcda03c0160d0da9c8f0c246bc78f2f7af0feea1ec0930e7c93fa4a"},
+    {file = "coverage-7.2.3-pp37.pp38.pp39-none-any.whl", hash = "sha256:965ee3e782c7892befc25575fa171b521d33798132692df428a09efacaffe8d0"},
+    {file = "coverage-7.2.3.tar.gz", hash = "sha256:d298c2815fa4891edd9abe5ad6e6cb4207104c7dd9fd13aea3fdebf6f9b91259"},
 ]
 
 [package.extras]
@@ -378,16 +412,28 @@ files = [
     {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"},
 ]
 
+[[package]]
+name = "docutils"
+version = "0.19"
+description = "Docutils -- Python Documentation Utilities"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "docutils-0.19-py3-none-any.whl", hash = "sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc"},
+    {file = "docutils-0.19.tar.gz", hash = "sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6"},
+]
+
 [[package]]
 name = "exceptiongroup"
-version = "1.1.0"
+version = "1.1.1"
 description = "Backport of PEP 654 (exception groups)"
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "exceptiongroup-1.1.0-py3-none-any.whl", hash = "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e"},
-    {file = "exceptiongroup-1.1.0.tar.gz", hash = "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"},
+    {file = "exceptiongroup-1.1.1-py3-none-any.whl", hash = "sha256:232c37c63e4f682982c8b6459f33a8981039e5fb8756b2074364e5055c498c9e"},
+    {file = "exceptiongroup-1.1.1.tar.gz", hash = "sha256:d484c3090ba2889ae2928419117447a14daf3c1231d5e30d0aae34f354f01785"},
 ]
 
 [package.extras]
@@ -395,34 +441,34 @@ test = ["pytest (>=6)"]
 
 [[package]]
 name = "filelock"
-version = "3.9.0"
+version = "3.11.0"
 description = "A platform independent file lock."
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "filelock-3.9.0-py3-none-any.whl", hash = "sha256:f58d535af89bb9ad5cd4df046f741f8553a418c01a7856bf0d173bbc9f6bd16d"},
-    {file = "filelock-3.9.0.tar.gz", hash = "sha256:7b319f24340b51f55a2bf7a12ac0755a9b03e718311dac567a0f4f7fabd2f5de"},
+    {file = "filelock-3.11.0-py3-none-any.whl", hash = "sha256:f08a52314748335c6460fc8fe40cd5638b85001225db78c2aa01c8c0db83b318"},
+    {file = "filelock-3.11.0.tar.gz", hash = "sha256:3618c0da67adcc0506b015fd11ef7faf1b493f0b40d87728e19986b536890c37"},
 ]
 
 [package.extras]
-docs = ["furo (>=2022.12.7)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"]
-testing = ["covdefaults (>=2.2.2)", "coverage (>=7.0.1)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-timeout (>=2.1)"]
+docs = ["furo (>=2023.3.27)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"]
+testing = ["covdefaults (>=2.3)", "coverage (>=7.2.2)", "diff-cover (>=7.5)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"]
 
 [[package]]
 name = "fonttools"
-version = "4.38.0"
+version = "4.39.3"
 description = "Tools to manipulate font files"
 category = "main"
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "fonttools-4.38.0-py3-none-any.whl", hash = "sha256:820466f43c8be8c3009aef8b87e785014133508f0de64ec469e4efb643ae54fb"},
-    {file = "fonttools-4.38.0.zip", hash = "sha256:2bb244009f9bf3fa100fc3ead6aeb99febe5985fa20afbfbaa2f8946c2fbdaf1"},
+    {file = "fonttools-4.39.3-py3-none-any.whl", hash = "sha256:64c0c05c337f826183637570ac5ab49ee220eec66cf50248e8df527edfa95aeb"},
+    {file = "fonttools-4.39.3.zip", hash = "sha256:9234b9f57b74e31b192c3fc32ef1a40750a8fbc1cd9837a7b7bfc4ca4a5c51d7"},
 ]
 
 [package.extras]
-all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0,<5)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=14.0.0)", "xattr", "zopfli (>=0.1.4)"]
+all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0,<5)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.0.0)", "xattr", "zopfli (>=0.1.4)"]
 graphite = ["lz4 (>=1.7.4.2)"]
 interpolatable = ["munkres", "scipy"]
 lxml = ["lxml (>=4.0,<5)"]
@@ -432,7 +478,7 @@ repacker = ["uharfbuzz (>=0.23.0)"]
 symfont = ["sympy"]
 type1 = ["xattr"]
 ufo = ["fs (>=2.2.0,<3)"]
-unicode = ["unicodedata2 (>=14.0.0)"]
+unicode = ["unicodedata2 (>=15.0.0)"]
 woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"]
 
 [[package]]
@@ -455,14 +501,14 @@ dev = ["flake8", "markdown", "twine", "wheel"]
 
 [[package]]
 name = "griffe"
-version = "0.25.3"
+version = "0.26.0"
 description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API."
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "griffe-0.25.3-py3-none-any.whl", hash = "sha256:c98e8471a4fc7675a7989f45563a9f7ccbfdfb1713725526d69dec1bbdcda74a"},
-    {file = "griffe-0.25.3.tar.gz", hash = "sha256:a71f156851649b3f0bdad6eb6bf7d7ac70e720a30da9f2d5a60e042480e92c03"},
+    {file = "griffe-0.26.0-py3-none-any.whl", hash = "sha256:38f3f6bbe834501cc199a07b7b7e0e2550aaf19a9d1ee27acf879027e47c9b9e"},
+    {file = "griffe-0.26.0.tar.gz", hash = "sha256:08675ffe8c17139e7769e950dd21f8e98a2e76205cbbd2911d5dec26d2cbf1be"},
 ]
 
 [package.dependencies]
@@ -473,14 +519,14 @@ async = ["aiofiles (>=0.7,<1.0)"]
 
 [[package]]
 name = "identify"
-version = "2.5.13"
+version = "2.5.22"
 description = "File identification library for Python"
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "identify-2.5.13-py2.py3-none-any.whl", hash = "sha256:8aa48ce56e38c28b6faa9f261075dea0a942dfbb42b341b4e711896cbb40f3f7"},
-    {file = "identify-2.5.13.tar.gz", hash = "sha256:abb546bca6f470228785338a01b539de8a85bbf46491250ae03363956d8ebb10"},
+    {file = "identify-2.5.22-py2.py3-none-any.whl", hash = "sha256:f0faad595a4687053669c112004178149f6c326db71ee999ae4636685753ad2f"},
+    {file = "identify-2.5.22.tar.gz", hash = "sha256:f7a93d6cf98e29bd07663c60728e7a4057615068d7a639d132dc883b2d54d31e"},
 ]
 
 [package.extras]
@@ -490,7 +536,7 @@ license = ["ukkonen"]
 name = "idna"
 version = "3.4"
 description = "Internationalized Domain Names in Applications (IDNA)"
-category = "dev"
+category = "main"
 optional = false
 python-versions = ">=3.5"
 files = [
@@ -500,14 +546,14 @@ files = [
 
 [[package]]
 name = "importlib-metadata"
-version = "6.0.0"
+version = "6.2.0"
 description = "Read metadata from Python packages"
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "importlib_metadata-6.0.0-py3-none-any.whl", hash = "sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad"},
-    {file = "importlib_metadata-6.0.0.tar.gz", hash = "sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d"},
+    {file = "importlib_metadata-6.2.0-py3-none-any.whl", hash = "sha256:8388b74023a138c605fddd0d47cb81dd706232569f56c9aca7d9c7fdb54caeba"},
+    {file = "importlib_metadata-6.2.0.tar.gz", hash = "sha256:9127aad2f49d7203e7112098c12b92e4fd1061ccd18548cdfdc49171a8c073cc"},
 ]
 
 [package.dependencies]
@@ -518,6 +564,25 @@ docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker
 perf = ["ipython"]
 testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "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 (>=0.9.2)"]
 
+[[package]]
+name = "importlib-resources"
+version = "5.12.0"
+description = "Read resources from Python packages"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "importlib_resources-5.12.0-py3-none-any.whl", hash = "sha256:7b1deeebbf351c7578e09bf2f63fa2ce8b5ffec296e0d349139d43cca061a81a"},
+    {file = "importlib_resources-5.12.0.tar.gz", hash = "sha256:4be82589bf5c1d7999aedf2a45159d10cb3ca4f19b2271f8792bc8e6da7b22f6"},
+]
+
+[package.dependencies]
+zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""}
+
+[package.extras]
+docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
+testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
+
 [[package]]
 name = "iniconfig"
 version = "2.0.0"
@@ -626,6 +691,18 @@ files = [
     {file = "kiwisolver-1.4.4.tar.gz", hash = "sha256:d41997519fcba4a1e46eb4a2fe31bc12f0ff957b2b81bac28db24744f333e955"},
 ]
 
+[[package]]
+name = "logfury"
+version = "1.0.1"
+description = "('Toolkit for responsible, low-boilerplate logging of library method calls',)"
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+    {file = "logfury-1.0.1-py3-none-any.whl", hash = "sha256:b4f04be1701a1df644afc3384d6167d64c899f8036b7eefc3b6c570c6a9b290b"},
+    {file = "logfury-1.0.1.tar.gz", hash = "sha256:130a5daceab9ad534924252ddf70482aa2c96662b3a3825a7d30981d03b76a26"},
+]
+
 [[package]]
 name = "markdown"
 version = "3.3.7"
@@ -646,114 +723,125 @@ testing = ["coverage", "pyyaml"]
 
 [[package]]
 name = "markupsafe"
-version = "2.1.1"
+version = "2.1.2"
 description = "Safely add untrusted strings to HTML/XML markup."
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"},
-    {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"},
-    {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"},
-    {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"},
-    {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"},
-    {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"},
-    {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"},
-    {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"},
-    {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"},
-    {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"},
-    {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"},
-    {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"},
-    {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"},
-    {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"},
-    {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"},
-    {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"},
-    {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"},
-    {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"},
-    {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"},
-    {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"},
-    {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"},
-    {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"},
-    {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"},
-    {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"},
-    {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"},
-    {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"},
-    {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"},
-    {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"},
-    {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"},
-    {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"},
-    {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"},
-    {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"},
-    {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"},
-    {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"},
-    {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"},
-    {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"},
-    {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"},
-    {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"},
-    {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"},
-    {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"},
+    {file = "MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"},
+    {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"},
+    {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"},
+    {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2"},
+    {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"},
+    {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"},
+    {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"},
+    {file = "MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"},
+    {file = "MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"},
+    {file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"},
 ]
 
 [[package]]
 name = "matplotlib"
-version = "3.6.3"
+version = "3.7.1"
 description = "Python plotting package"
 category = "main"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "matplotlib-3.6.3-cp310-cp310-macosx_10_12_universal2.whl", hash = "sha256:80c166a0e28512e26755f69040e6bf2f946a02ffdb7c00bf6158cca3d2b146e6"},
-    {file = "matplotlib-3.6.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eb9421c403ffd387fbe729de6d9a03005bf42faba5e8432f4e51e703215b49fc"},
-    {file = "matplotlib-3.6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5223affa21050fb6118353c1380c15e23aedfb436bf3e162c26dc950617a7519"},
-    {file = "matplotlib-3.6.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d00c248ab6b92bea3f8148714837937053a083ff03b4c5e30ed37e28fc0e7e56"},
-    {file = "matplotlib-3.6.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca94f0362f6b6f424b555b956971dcb94b12d0368a6c3e07dc7a40d32d6d873d"},
-    {file = "matplotlib-3.6.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59400cc9451094b7f08cc3f321972e6e1db4cd37a978d4e8a12824bf7fd2f03b"},
-    {file = "matplotlib-3.6.3-cp310-cp310-win32.whl", hash = "sha256:57ad1aee29043163374bfa8990e1a2a10ff72c9a1bfaa92e9c46f6ea59269121"},
-    {file = "matplotlib-3.6.3-cp310-cp310-win_amd64.whl", hash = "sha256:1fcc4cad498533d3c393a160975acc9b36ffa224d15a6b90ae579eacee5d8579"},
-    {file = "matplotlib-3.6.3-cp311-cp311-macosx_10_12_universal2.whl", hash = "sha256:d2cfaa7fd62294d945b8843ea24228a27c8e7c5b48fa634f3c168153b825a21b"},
-    {file = "matplotlib-3.6.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:c3f08df2ac4636249b8bc7a85b8b82c983bef1441595936f62c2918370ca7e1d"},
-    {file = "matplotlib-3.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff2aa84e74f80891e6bcf292ebb1dd57714ffbe13177642d65fee25384a30894"},
-    {file = "matplotlib-3.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11011c97d62c1db7bc20509572557842dbb8c2a2ddd3dd7f20501aa1cde3e54e"},
-    {file = "matplotlib-3.6.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c235bf9be052347373f589e018988cad177abb3f997ab1a2e2210c41562cc0c"},
-    {file = "matplotlib-3.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bebcff4c3ed02c6399d47329f3554193abd824d3d53b5ca02cf583bcd94470e2"},
-    {file = "matplotlib-3.6.3-cp311-cp311-win32.whl", hash = "sha256:d5f18430f5cfa5571ab8f4c72c89af52aa0618e864c60028f11a857d62200cba"},
-    {file = "matplotlib-3.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:dfba7057609ca9567b9704626756f0142e97ec8c5ba2c70c6e7bd1c25ef99f06"},
-    {file = "matplotlib-3.6.3-cp38-cp38-macosx_10_12_universal2.whl", hash = "sha256:9fb8fb19d03abf3c5dab89a8677e62c4023632f919a62b6dd1d6d2dbf42cd9f5"},
-    {file = "matplotlib-3.6.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:bbf269e1d24bc25247095d71c7a969813f7080e2a7c6fa28931a603f747ab012"},
-    {file = "matplotlib-3.6.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:994637e2995b0342699b396a320698b07cd148bbcf2dd2fa2daba73f34dd19f2"},
-    {file = "matplotlib-3.6.3-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:77b384cee7ab8cf75ffccbfea351a09b97564fc62d149827a5e864bec81526e5"},
-    {file = "matplotlib-3.6.3-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:73b93af33634ed919e72811c9703e1105185cd3fb46d76f30b7f4cfbbd063f89"},
-    {file = "matplotlib-3.6.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:debeab8e2ab07e5e3dac33e12456da79c7e104270d2b2d1df92b9e40347cca75"},
-    {file = "matplotlib-3.6.3-cp38-cp38-win32.whl", hash = "sha256:acc3b1a4bddbf56fe461e36fb9ef94c2cb607fc90d24ccc650040bfcc7610de4"},
-    {file = "matplotlib-3.6.3-cp38-cp38-win_amd64.whl", hash = "sha256:1183877d008c752d7d535396096c910f4663e4b74a18313adee1213328388e1e"},
-    {file = "matplotlib-3.6.3-cp39-cp39-macosx_10_12_universal2.whl", hash = "sha256:6adc441b5b2098a4b904bbf9d9e92fb816fef50c55aa2ea6a823fc89b94bb838"},
-    {file = "matplotlib-3.6.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:6d81b11ede69e3a751424b98dc869c96c10256b2206bfdf41f9c720eee86844c"},
-    {file = "matplotlib-3.6.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:29f17b7f2e068dc346687cbdf80b430580bab42346625821c2d3abf3a1ec5417"},
-    {file = "matplotlib-3.6.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f56a7252eee8f3438447f75f5e1148a1896a2756a92285fe5d73bed6deebff4"},
-    {file = "matplotlib-3.6.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bbddfeb1495484351fb5b30cf5bdf06b3de0bc4626a707d29e43dfd61af2a780"},
-    {file = "matplotlib-3.6.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:809119d1cba3ece3c9742eb01827fe7a0e781ea3c5d89534655a75e07979344f"},
-    {file = "matplotlib-3.6.3-cp39-cp39-win32.whl", hash = "sha256:e0a64d7cc336b52e90f59e6d638ae847b966f68582a7af041e063d568e814740"},
-    {file = "matplotlib-3.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:79e501eb847f4a489eb7065bb8d3187117f65a4c02d12ea3a19d6c5bef173bcc"},
-    {file = "matplotlib-3.6.3-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2787a16df07370dcba385fe20cdd0cc3cfaabd3c873ddabca78c10514c799721"},
-    {file = "matplotlib-3.6.3-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68d94a436f62b8a861bf3ace82067a71bafb724b4e4f9133521e4d8012420dd7"},
-    {file = "matplotlib-3.6.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81b409b2790cf8d7c1ef35920f01676d2ae7afa8241844e7aa5484fdf493a9a0"},
-    {file = "matplotlib-3.6.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:faff486b36530a836a6b4395850322e74211cd81fc17f28b4904e1bd53668e3e"},
-    {file = "matplotlib-3.6.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:38d38cb1ea1d80ee0f6351b65c6f76cad6060bbbead015720ba001348ae90f0c"},
-    {file = "matplotlib-3.6.3-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12f999661589981e74d793ee2f41b924b3b87d65fd929f6153bf0f30675c59b1"},
-    {file = "matplotlib-3.6.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01b7f521a9a73c383825813af255f8c4485d1706e4f3e2ed5ae771e4403a40ab"},
-    {file = "matplotlib-3.6.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:9ceebaf73f1a3444fa11014f38b9da37ff7ea328d6efa1652241fe3777bfdab9"},
-    {file = "matplotlib-3.6.3.tar.gz", hash = "sha256:1f4d69707b1677560cd952544ee4962f68ff07952fb9069ff8c12b56353cb8c9"},
+    {file = "matplotlib-3.7.1-cp310-cp310-macosx_10_12_universal2.whl", hash = "sha256:95cbc13c1fc6844ab8812a525bbc237fa1470863ff3dace7352e910519e194b1"},
+    {file = "matplotlib-3.7.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:08308bae9e91aca1ec6fd6dda66237eef9f6294ddb17f0d0b3c863169bf82353"},
+    {file = "matplotlib-3.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:544764ba51900da4639c0f983b323d288f94f65f4024dc40ecb1542d74dc0500"},
+    {file = "matplotlib-3.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56d94989191de3fcc4e002f93f7f1be5da476385dde410ddafbb70686acf00ea"},
+    {file = "matplotlib-3.7.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e99bc9e65901bb9a7ce5e7bb24af03675cbd7c70b30ac670aa263240635999a4"},
+    {file = "matplotlib-3.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb7d248c34a341cd4c31a06fd34d64306624c8cd8d0def7abb08792a5abfd556"},
+    {file = "matplotlib-3.7.1-cp310-cp310-win32.whl", hash = "sha256:ce463ce590f3825b52e9fe5c19a3c6a69fd7675a39d589e8b5fbe772272b3a24"},
+    {file = "matplotlib-3.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:3d7bc90727351fb841e4d8ae620d2d86d8ed92b50473cd2b42ce9186104ecbba"},
+    {file = "matplotlib-3.7.1-cp311-cp311-macosx_10_12_universal2.whl", hash = "sha256:770a205966d641627fd5cf9d3cb4b6280a716522cd36b8b284a8eb1581310f61"},
+    {file = "matplotlib-3.7.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f67bfdb83a8232cb7a92b869f9355d677bce24485c460b19d01970b64b2ed476"},
+    {file = "matplotlib-3.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2bf092f9210e105f414a043b92af583c98f50050559616930d884387d0772aba"},
+    {file = "matplotlib-3.7.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89768d84187f31717349c6bfadc0e0d8c321e8eb34522acec8a67b1236a66332"},
+    {file = "matplotlib-3.7.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83111e6388dec67822e2534e13b243cc644c7494a4bb60584edbff91585a83c6"},
+    {file = "matplotlib-3.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a867bf73a7eb808ef2afbca03bcdb785dae09595fbe550e1bab0cd023eba3de0"},
+    {file = "matplotlib-3.7.1-cp311-cp311-win32.whl", hash = "sha256:fbdeeb58c0cf0595efe89c05c224e0a502d1aa6a8696e68a73c3efc6bc354304"},
+    {file = "matplotlib-3.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:c0bd19c72ae53e6ab979f0ac6a3fafceb02d2ecafa023c5cca47acd934d10be7"},
+    {file = "matplotlib-3.7.1-cp38-cp38-macosx_10_12_universal2.whl", hash = "sha256:6eb88d87cb2c49af00d3bbc33a003f89fd9f78d318848da029383bfc08ecfbfb"},
+    {file = "matplotlib-3.7.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:cf0e4f727534b7b1457898c4f4ae838af1ef87c359b76dcd5330fa31893a3ac7"},
+    {file = "matplotlib-3.7.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:46a561d23b91f30bccfd25429c3c706afe7d73a5cc64ef2dfaf2b2ac47c1a5dc"},
+    {file = "matplotlib-3.7.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8704726d33e9aa8a6d5215044b8d00804561971163563e6e6591f9dcf64340cc"},
+    {file = "matplotlib-3.7.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4cf327e98ecf08fcbb82685acaf1939d3338548620ab8dfa02828706402c34de"},
+    {file = "matplotlib-3.7.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:617f14ae9d53292ece33f45cba8503494ee199a75b44de7717964f70637a36aa"},
+    {file = "matplotlib-3.7.1-cp38-cp38-win32.whl", hash = "sha256:7c9a4b2da6fac77bcc41b1ea95fadb314e92508bf5493ceff058e727e7ecf5b0"},
+    {file = "matplotlib-3.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:14645aad967684e92fc349493fa10c08a6da514b3d03a5931a1bac26e6792bd1"},
+    {file = "matplotlib-3.7.1-cp39-cp39-macosx_10_12_universal2.whl", hash = "sha256:81a6b377ea444336538638d31fdb39af6be1a043ca5e343fe18d0f17e098770b"},
+    {file = "matplotlib-3.7.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:28506a03bd7f3fe59cd3cd4ceb2a8d8a2b1db41afede01f66c42561b9be7b4b7"},
+    {file = "matplotlib-3.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8c587963b85ce41e0a8af53b9b2de8dddbf5ece4c34553f7bd9d066148dc719c"},
+    {file = "matplotlib-3.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8bf26ade3ff0f27668989d98c8435ce9327d24cffb7f07d24ef609e33d582439"},
+    {file = "matplotlib-3.7.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:def58098f96a05f90af7e92fd127d21a287068202aa43b2a93476170ebd99e87"},
+    {file = "matplotlib-3.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f883a22a56a84dba3b588696a2b8a1ab0d2c3d41be53264115c71b0a942d8fdb"},
+    {file = "matplotlib-3.7.1-cp39-cp39-win32.whl", hash = "sha256:4f99e1b234c30c1e9714610eb0c6d2f11809c9c78c984a613ae539ea2ad2eb4b"},
+    {file = "matplotlib-3.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:3ba2af245e36990facf67fde840a760128ddd71210b2ab6406e640188d69d136"},
+    {file = "matplotlib-3.7.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3032884084f541163f295db8a6536e0abb0db464008fadca6c98aaf84ccf4717"},
+    {file = "matplotlib-3.7.1-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a2cb34336110e0ed8bb4f650e817eed61fa064acbefeb3591f1b33e3a84fd96"},
+    {file = "matplotlib-3.7.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b867e2f952ed592237a1828f027d332d8ee219ad722345b79a001f49df0936eb"},
+    {file = "matplotlib-3.7.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:57bfb8c8ea253be947ccb2bc2d1bb3862c2bccc662ad1b4626e1f5e004557042"},
+    {file = "matplotlib-3.7.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:438196cdf5dc8d39b50a45cb6e3f6274edbcf2254f85fa9b895bf85851c3a613"},
+    {file = "matplotlib-3.7.1-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:21e9cff1a58d42e74d01153360de92b326708fb205250150018a52c70f43c290"},
+    {file = "matplotlib-3.7.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75d4725d70b7c03e082bbb8a34639ede17f333d7247f56caceb3801cb6ff703d"},
+    {file = "matplotlib-3.7.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:97cc368a7268141afb5690760921765ed34867ffb9655dd325ed207af85c7529"},
+    {file = "matplotlib-3.7.1.tar.gz", hash = "sha256:7b73305f25eab4541bd7ee0b96d87e53ae9c9f1823be5659b806cd85786fe882"},
 ]
 
 [package.dependencies]
 contourpy = ">=1.0.1"
 cycler = ">=0.10"
 fonttools = ">=4.22.0"
+importlib-resources = {version = ">=3.2.0", markers = "python_version < \"3.10\""}
 kiwisolver = ">=1.0.1"
-numpy = ">=1.19"
+numpy = ">=1.20"
 packaging = ">=20.0"
 pillow = ">=6.2.0"
-pyparsing = ">=2.2.1"
+pyparsing = ">=2.3.1"
 python-dateutil = ">=2.7"
 
 [[package]]
@@ -837,14 +925,14 @@ mkdocs = ">=1.1"
 
 [[package]]
 name = "mkdocs-material"
-version = "9.0.4"
+version = "9.1.6"
 description = "Documentation that simply works"
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "mkdocs_material-9.0.4-py3-none-any.whl", hash = "sha256:f5f94f5daa0e07deb2f192453f9812c66c4bc0cd48078c60bdde32af137f7357"},
-    {file = "mkdocs_material-9.0.4.tar.gz", hash = "sha256:4f429b4e50242544020f0fc21b9e10062418d3cd1e591c040c4c155506250a66"},
+    {file = "mkdocs_material-9.1.6-py3-none-any.whl", hash = "sha256:f2eb1d40db89da9922944833c1387207408f8937e1c2b46ab86e0c8f170b71e0"},
+    {file = "mkdocs_material-9.1.6.tar.gz", hash = "sha256:2e555152f9771646bfa62dc78a86052876183eff69ce30db03a33e85702b21fc"},
 ]
 
 [package.dependencies]
@@ -872,14 +960,14 @@ files = [
 
 [[package]]
 name = "mkdocstrings"
-version = "0.19.1"
+version = "0.21.2"
 description = "Automatic documentation from sources, for MkDocs."
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "mkdocstrings-0.19.1-py3-none-any.whl", hash = "sha256:32a38d88f67f65b264184ea71290f9332db750d189dea4200cbbe408d304c261"},
-    {file = "mkdocstrings-0.19.1.tar.gz", hash = "sha256:d1037cacb4b522c1e8c164ed5d00d724a82e49dcee0af80db8fb67b384faeef9"},
+    {file = "mkdocstrings-0.21.2-py3-none-any.whl", hash = "sha256:949ef8da92df9d692ca07be50616459a6b536083a25520fd54b00e8814ce019b"},
+    {file = "mkdocstrings-0.21.2.tar.gz", hash = "sha256:304e56a2e90595708a38a13a278e538a67ad82052dd5c8b71f77a604a4f3d911"},
 ]
 
 [package.dependencies]
@@ -889,6 +977,7 @@ MarkupSafe = ">=1.1"
 mkdocs = ">=1.2"
 mkdocs-autorefs = ">=0.3.1"
 pymdown-extensions = ">=6.3"
+typing-extensions = {version = ">=4.1", markers = "python_version < \"3.10\""}
 
 [package.extras]
 crystal = ["mkdocstrings-crystal (>=0.3.4)"]
@@ -913,20 +1002,20 @@ mkdocstrings = ">=0.19"
 
 [[package]]
 name = "networkx"
-version = "3.0"
+version = "3.1"
 description = "Python package for creating and manipulating graphs and networks"
 category = "main"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "networkx-3.0-py3-none-any.whl", hash = "sha256:58058d66b1818043527244fab9d41a51fcd7dcc271748015f3c181b8a90c8e2e"},
-    {file = "networkx-3.0.tar.gz", hash = "sha256:9a9992345353618ae98339c2b63d8201c381c2944f38a2ab49cb45a4c667e412"},
+    {file = "networkx-3.1-py3-none-any.whl", hash = "sha256:4f33f68cb2afcf86f28a45f43efc27a9386b535d567d2127f8f61d51dec58d36"},
+    {file = "networkx-3.1.tar.gz", hash = "sha256:de346335408f84de0eada6ff9fafafff9bcda11f0a0dfaa931133debb146ab61"},
 ]
 
 [package.extras]
 default = ["matplotlib (>=3.4)", "numpy (>=1.20)", "pandas (>=1.3)", "scipy (>=1.8)"]
-developer = ["mypy (>=0.991)", "pre-commit (>=2.20)"]
-doc = ["nb2plots (>=0.6)", "numpydoc (>=1.5)", "pillow (>=9.2)", "pydata-sphinx-theme (>=0.11)", "sphinx (==5.2.3)", "sphinx-gallery (>=0.11)", "texext (>=0.6.7)"]
+developer = ["mypy (>=1.1)", "pre-commit (>=3.2)"]
+doc = ["nb2plots (>=0.6)", "numpydoc (>=1.5)", "pillow (>=9.4)", "pydata-sphinx-theme (>=0.13)", "sphinx (>=6.1)", "sphinx-gallery (>=0.12)", "texext (>=0.6.7)"]
 extra = ["lxml (>=4.6)", "pydot (>=1.4.2)", "pygraphviz (>=1.10)", "sympy (>=1.10)"]
 test = ["codecov (>=2.1)", "pytest (>=7.2)", "pytest-cov (>=4.0)"]
 
@@ -968,40 +1057,40 @@ tox-to-nox = ["jinja2", "tox"]
 
 [[package]]
 name = "numpy"
-version = "1.24.1"
+version = "1.24.2"
 description = "Fundamental package for array computing in Python"
 category = "main"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "numpy-1.24.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:179a7ef0889ab769cc03573b6217f54c8bd8e16cef80aad369e1e8185f994cd7"},
-    {file = "numpy-1.24.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b09804ff570b907da323b3d762e74432fb07955701b17b08ff1b5ebaa8cfe6a9"},
-    {file = "numpy-1.24.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1b739841821968798947d3afcefd386fa56da0caf97722a5de53e07c4ccedc7"},
-    {file = "numpy-1.24.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e3463e6ac25313462e04aea3fb8a0a30fb906d5d300f58b3bc2c23da6a15398"},
-    {file = "numpy-1.24.1-cp310-cp310-win32.whl", hash = "sha256:b31da69ed0c18be8b77bfce48d234e55d040793cebb25398e2a7d84199fbc7e2"},
-    {file = "numpy-1.24.1-cp310-cp310-win_amd64.whl", hash = "sha256:b07b40f5fb4fa034120a5796288f24c1fe0e0580bbfff99897ba6267af42def2"},
-    {file = "numpy-1.24.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7094891dcf79ccc6bc2a1f30428fa5edb1e6fb955411ffff3401fb4ea93780a8"},
-    {file = "numpy-1.24.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e418681372520c992805bb723e29d69d6b7aa411065f48216d8329d02ba032"},
-    {file = "numpy-1.24.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e274f0f6c7efd0d577744f52032fdd24344f11c5ae668fe8d01aac0422611df1"},
-    {file = "numpy-1.24.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0044f7d944ee882400890f9ae955220d29b33d809a038923d88e4e01d652acd9"},
-    {file = "numpy-1.24.1-cp311-cp311-win32.whl", hash = "sha256:442feb5e5bada8408e8fcd43f3360b78683ff12a4444670a7d9e9824c1817d36"},
-    {file = "numpy-1.24.1-cp311-cp311-win_amd64.whl", hash = "sha256:de92efa737875329b052982e37bd4371d52cabf469f83e7b8be9bb7752d67e51"},
-    {file = "numpy-1.24.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b162ac10ca38850510caf8ea33f89edcb7b0bb0dfa5592d59909419986b72407"},
-    {file = "numpy-1.24.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:26089487086f2648944f17adaa1a97ca6aee57f513ba5f1c0b7ebdabbe2b9954"},
-    {file = "numpy-1.24.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:caf65a396c0d1f9809596be2e444e3bd4190d86d5c1ce21f5fc4be60a3bc5b36"},
-    {file = "numpy-1.24.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0677a52f5d896e84414761531947c7a330d1adc07c3a4372262f25d84af7bf7"},
-    {file = "numpy-1.24.1-cp38-cp38-win32.whl", hash = "sha256:dae46bed2cb79a58d6496ff6d8da1e3b95ba09afeca2e277628171ca99b99db1"},
-    {file = "numpy-1.24.1-cp38-cp38-win_amd64.whl", hash = "sha256:6ec0c021cd9fe732e5bab6401adea5a409214ca5592cd92a114f7067febcba0c"},
-    {file = "numpy-1.24.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:28bc9750ae1f75264ee0f10561709b1462d450a4808cd97c013046073ae64ab6"},
-    {file = "numpy-1.24.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:84e789a085aabef2f36c0515f45e459f02f570c4b4c4c108ac1179c34d475ed7"},
-    {file = "numpy-1.24.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e669fbdcdd1e945691079c2cae335f3e3a56554e06bbd45d7609a6cf568c700"},
-    {file = "numpy-1.24.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef85cf1f693c88c1fd229ccd1055570cb41cdf4875873b7728b6301f12cd05bf"},
-    {file = "numpy-1.24.1-cp39-cp39-win32.whl", hash = "sha256:87a118968fba001b248aac90e502c0b13606721b1343cdaddbc6e552e8dfb56f"},
-    {file = "numpy-1.24.1-cp39-cp39-win_amd64.whl", hash = "sha256:ddc7ab52b322eb1e40521eb422c4e0a20716c271a306860979d450decbb51b8e"},
-    {file = "numpy-1.24.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ed5fb71d79e771ec930566fae9c02626b939e37271ec285e9efaf1b5d4370e7d"},
-    {file = "numpy-1.24.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad2925567f43643f51255220424c23d204024ed428afc5aad0f86f3ffc080086"},
-    {file = "numpy-1.24.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cfa1161c6ac8f92dea03d625c2d0c05e084668f4a06568b77a25a89111621566"},
-    {file = "numpy-1.24.1.tar.gz", hash = "sha256:2386da9a471cc00a1f47845e27d916d5ec5346ae9696e01a8a34760858fe9dd2"},
+    {file = "numpy-1.24.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eef70b4fc1e872ebddc38cddacc87c19a3709c0e3e5d20bf3954c147b1dd941d"},
+    {file = "numpy-1.24.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8d2859428712785e8a8b7d2b3ef0a1d1565892367b32f915c4a4df44d0e64f5"},
+    {file = "numpy-1.24.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6524630f71631be2dabe0c541e7675db82651eb998496bbe16bc4f77f0772253"},
+    {file = "numpy-1.24.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a51725a815a6188c662fb66fb32077709a9ca38053f0274640293a14fdd22978"},
+    {file = "numpy-1.24.2-cp310-cp310-win32.whl", hash = "sha256:2620e8592136e073bd12ee4536149380695fbe9ebeae845b81237f986479ffc9"},
+    {file = "numpy-1.24.2-cp310-cp310-win_amd64.whl", hash = "sha256:97cf27e51fa078078c649a51d7ade3c92d9e709ba2bfb97493007103c741f1d0"},
+    {file = "numpy-1.24.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7de8fdde0003f4294655aa5d5f0a89c26b9f22c0a58790c38fae1ed392d44a5a"},
+    {file = "numpy-1.24.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4173bde9fa2a005c2c6e2ea8ac1618e2ed2c1c6ec8a7657237854d42094123a0"},
+    {file = "numpy-1.24.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cecaed30dc14123020f77b03601559fff3e6cd0c048f8b5289f4eeabb0eb281"},
+    {file = "numpy-1.24.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a23f8440561a633204a67fb44617ce2a299beecf3295f0d13c495518908e910"},
+    {file = "numpy-1.24.2-cp311-cp311-win32.whl", hash = "sha256:e428c4fbfa085f947b536706a2fc349245d7baa8334f0c5723c56a10595f9b95"},
+    {file = "numpy-1.24.2-cp311-cp311-win_amd64.whl", hash = "sha256:557d42778a6869c2162deb40ad82612645e21d79e11c1dc62c6e82a2220ffb04"},
+    {file = "numpy-1.24.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d0a2db9d20117bf523dde15858398e7c0858aadca7c0f088ac0d6edd360e9ad2"},
+    {file = "numpy-1.24.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c72a6b2f4af1adfe193f7beb91ddf708ff867a3f977ef2ec53c0ffb8283ab9f5"},
+    {file = "numpy-1.24.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c29e6bd0ec49a44d7690ecb623a8eac5ab8a923bce0bea6293953992edf3a76a"},
+    {file = "numpy-1.24.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2eabd64ddb96a1239791da78fa5f4e1693ae2dadc82a76bc76a14cbb2b966e96"},
+    {file = "numpy-1.24.2-cp38-cp38-win32.whl", hash = "sha256:e3ab5d32784e843fc0dd3ab6dcafc67ef806e6b6828dc6af2f689be0eb4d781d"},
+    {file = "numpy-1.24.2-cp38-cp38-win_amd64.whl", hash = "sha256:76807b4063f0002c8532cfeac47a3068a69561e9c8715efdad3c642eb27c0756"},
+    {file = "numpy-1.24.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4199e7cfc307a778f72d293372736223e39ec9ac096ff0a2e64853b866a8e18a"},
+    {file = "numpy-1.24.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:adbdce121896fd3a17a77ab0b0b5eedf05a9834a18699db6829a64e1dfccca7f"},
+    {file = "numpy-1.24.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:889b2cc88b837d86eda1b17008ebeb679d82875022200c6e8e4ce6cf549b7acb"},
+    {file = "numpy-1.24.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f64bb98ac59b3ea3bf74b02f13836eb2e24e48e0ab0145bbda646295769bd780"},
+    {file = "numpy-1.24.2-cp39-cp39-win32.whl", hash = "sha256:63e45511ee4d9d976637d11e6c9864eae50e12dc9598f531c035265991910468"},
+    {file = "numpy-1.24.2-cp39-cp39-win_amd64.whl", hash = "sha256:a77d3e1163a7770164404607b7ba3967fb49b24782a6ef85d9b5f54126cc39e5"},
+    {file = "numpy-1.24.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:92011118955724465fb6853def593cf397b4a1367495e0b59a7e69d40c4eb71d"},
+    {file = "numpy-1.24.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9006288bcf4895917d02583cf3411f98631275bc67cce355a7f39f8c14338fa"},
+    {file = "numpy-1.24.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:150947adbdfeceec4e5926d956a06865c1c690f2fd902efede4ca6fe2e657c3f"},
+    {file = "numpy-1.24.2.tar.gz", hash = "sha256:003a9f530e880cb2cd177cba1af7220b9aa42def9c4afc2a2fc3ee6be7eb2b22"},
 ]
 
 [[package]]
@@ -1016,105 +1105,118 @@ files = [
     {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"},
 ]
 
+[[package]]
+name = "phx-class-registry"
+version = "4.0.5"
+description = "Factory+Registry pattern for Python classes"
+category = "main"
+optional = false
+python-versions = ">=3"
+files = [
+    {file = "phx-class-registry-4.0.5.tar.gz", hash = "sha256:1901bdaea34d9cfefa4d149b2f20217e7e024492f7e247797e7c4d36d96cdb5b"},
+    {file = "phx_class_registry-4.0.5-py3-none-any.whl", hash = "sha256:389d9dbe0b5c660708931c4fbc09147ff0871169a34394c1d7021cac74bd4425"},
+]
+
+[package.extras]
+build-system = ["build", "twine"]
+docs-builder = ["sphinx", "sphinx-rtd-theme"]
+test-runner = ["tox"]
+
 [[package]]
 name = "pillow"
-version = "9.4.0"
+version = "9.5.0"
 description = "Python Imaging Library (Fork)"
 category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "Pillow-9.4.0-1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b4b4e9dda4f4e4c4e6896f93e84a8f0bcca3b059de9ddf67dac3c334b1195e1"},
-    {file = "Pillow-9.4.0-1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:fb5c1ad6bad98c57482236a21bf985ab0ef42bd51f7ad4e4538e89a997624e12"},
-    {file = "Pillow-9.4.0-1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:f0caf4a5dcf610d96c3bd32932bfac8aee61c96e60481c2a0ea58da435e25acd"},
-    {file = "Pillow-9.4.0-1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:3f4cc516e0b264c8d4ccd6b6cbc69a07c6d582d8337df79be1e15a5056b258c9"},
-    {file = "Pillow-9.4.0-1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b8c2f6eb0df979ee99433d8b3f6d193d9590f735cf12274c108bd954e30ca858"},
-    {file = "Pillow-9.4.0-1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b70756ec9417c34e097f987b4d8c510975216ad26ba6e57ccb53bc758f490dab"},
-    {file = "Pillow-9.4.0-1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:43521ce2c4b865d385e78579a082b6ad1166ebed2b1a2293c3be1d68dd7ca3b9"},
-    {file = "Pillow-9.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:2968c58feca624bb6c8502f9564dd187d0e1389964898f5e9e1fbc8533169157"},
-    {file = "Pillow-9.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c5c1362c14aee73f50143d74389b2c158707b4abce2cb055b7ad37ce60738d47"},
-    {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd752c5ff1b4a870b7661234694f24b1d2b9076b8bf337321a814c612665f343"},
-    {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a3049a10261d7f2b6514d35bbb7a4dfc3ece4c4de14ef5876c4b7a23a0e566d"},
-    {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16a8df99701f9095bea8a6c4b3197da105df6f74e6176c5b410bc2df2fd29a57"},
-    {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:94cdff45173b1919350601f82d61365e792895e3c3a3443cf99819e6fbf717a5"},
-    {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:ed3e4b4e1e6de75fdc16d3259098de7c6571b1a6cc863b1a49e7d3d53e036070"},
-    {file = "Pillow-9.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5b2f8a31bd43e0f18172d8ac82347c8f37ef3e0b414431157718aa234991b28"},
-    {file = "Pillow-9.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:09b89ddc95c248ee788328528e6a2996e09eaccddeeb82a5356e92645733be35"},
-    {file = "Pillow-9.4.0-cp310-cp310-win32.whl", hash = "sha256:f09598b416ba39a8f489c124447b007fe865f786a89dbfa48bb5cf395693132a"},
-    {file = "Pillow-9.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:f6e78171be3fb7941f9910ea15b4b14ec27725865a73c15277bc39f5ca4f8391"},
-    {file = "Pillow-9.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:3fa1284762aacca6dc97474ee9c16f83990b8eeb6697f2ba17140d54b453e133"},
-    {file = "Pillow-9.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:eaef5d2de3c7e9b21f1e762f289d17b726c2239a42b11e25446abf82b26ac132"},
-    {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4dfdae195335abb4e89cc9762b2edc524f3c6e80d647a9a81bf81e17e3fb6f0"},
-    {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6abfb51a82e919e3933eb137e17c4ae9c0475a25508ea88993bb59faf82f3b35"},
-    {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:451f10ef963918e65b8869e17d67db5e2f4ab40e716ee6ce7129b0cde2876eab"},
-    {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:6663977496d616b618b6cfa43ec86e479ee62b942e1da76a2c3daa1c75933ef4"},
-    {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:60e7da3a3ad1812c128750fc1bc14a7ceeb8d29f77e0a2356a8fb2aa8925287d"},
-    {file = "Pillow-9.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:19005a8e58b7c1796bc0167862b1f54a64d3b44ee5d48152b06bb861458bc0f8"},
-    {file = "Pillow-9.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f715c32e774a60a337b2bb8ad9839b4abf75b267a0f18806f6f4f5f1688c4b5a"},
-    {file = "Pillow-9.4.0-cp311-cp311-win32.whl", hash = "sha256:b222090c455d6d1a64e6b7bb5f4035c4dff479e22455c9eaa1bdd4c75b52c80c"},
-    {file = "Pillow-9.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:ba6612b6548220ff5e9df85261bddc811a057b0b465a1226b39bfb8550616aee"},
-    {file = "Pillow-9.4.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:5f532a2ad4d174eb73494e7397988e22bf427f91acc8e6ebf5bb10597b49c493"},
-    {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dd5a9c3091a0f414a963d427f920368e2b6a4c2f7527fdd82cde8ef0bc7a327"},
-    {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef21af928e807f10bf4141cad4746eee692a0dd3ff56cfb25fce076ec3cc8abe"},
-    {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:847b114580c5cc9ebaf216dd8c8dbc6b00a3b7ab0131e173d7120e6deade1f57"},
-    {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:653d7fb2df65efefbcbf81ef5fe5e5be931f1ee4332c2893ca638c9b11a409c4"},
-    {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:46f39cab8bbf4a384ba7cb0bc8bae7b7062b6a11cfac1ca4bc144dea90d4a9f5"},
-    {file = "Pillow-9.4.0-cp37-cp37m-win32.whl", hash = "sha256:7ac7594397698f77bce84382929747130765f66406dc2cd8b4ab4da68ade4c6e"},
-    {file = "Pillow-9.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:46c259e87199041583658457372a183636ae8cd56dbf3f0755e0f376a7f9d0e6"},
-    {file = "Pillow-9.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:0e51f608da093e5d9038c592b5b575cadc12fd748af1479b5e858045fff955a9"},
-    {file = "Pillow-9.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:765cb54c0b8724a7c12c55146ae4647e0274a839fb6de7bcba841e04298e1011"},
-    {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:519e14e2c49fcf7616d6d2cfc5c70adae95682ae20f0395e9280db85e8d6c4df"},
-    {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d197df5489004db87d90b918033edbeee0bd6df3848a204bca3ff0a903bef837"},
-    {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0845adc64fe9886db00f5ab68c4a8cd933ab749a87747555cec1c95acea64b0b"},
-    {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:e1339790c083c5a4de48f688b4841f18df839eb3c9584a770cbd818b33e26d5d"},
-    {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:a96e6e23f2b79433390273eaf8cc94fec9c6370842e577ab10dabdcc7ea0a66b"},
-    {file = "Pillow-9.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7cfc287da09f9d2a7ec146ee4d72d6ea1342e770d975e49a8621bf54eaa8f30f"},
-    {file = "Pillow-9.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d7081c084ceb58278dd3cf81f836bc818978c0ccc770cbbb202125ddabec6628"},
-    {file = "Pillow-9.4.0-cp38-cp38-win32.whl", hash = "sha256:df41112ccce5d47770a0c13651479fbcd8793f34232a2dd9faeccb75eb5d0d0d"},
-    {file = "Pillow-9.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:7a21222644ab69ddd9967cfe6f2bb420b460dae4289c9d40ff9a4896e7c35c9a"},
-    {file = "Pillow-9.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0f3269304c1a7ce82f1759c12ce731ef9b6e95b6df829dccd9fe42912cc48569"},
-    {file = "Pillow-9.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cb362e3b0976dc994857391b776ddaa8c13c28a16f80ac6522c23d5257156bed"},
-    {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2e0f87144fcbbe54297cae708c5e7f9da21a4646523456b00cc956bd4c65815"},
-    {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:28676836c7796805914b76b1837a40f76827ee0d5398f72f7dcc634bae7c6264"},
-    {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0884ba7b515163a1a05440a138adeb722b8a6ae2c2b33aea93ea3118dd3a899e"},
-    {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:53dcb50fbdc3fb2c55431a9b30caeb2f7027fcd2aeb501459464f0214200a503"},
-    {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:e8c5cf126889a4de385c02a2c3d3aba4b00f70234bfddae82a5eaa3ee6d5e3e6"},
-    {file = "Pillow-9.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6c6b1389ed66cdd174d040105123a5a1bc91d0aa7059c7261d20e583b6d8cbd2"},
-    {file = "Pillow-9.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0dd4c681b82214b36273c18ca7ee87065a50e013112eea7d78c7a1b89a739153"},
-    {file = "Pillow-9.4.0-cp39-cp39-win32.whl", hash = "sha256:6d9dfb9959a3b0039ee06c1a1a90dc23bac3b430842dcb97908ddde05870601c"},
-    {file = "Pillow-9.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:54614444887e0d3043557d9dbc697dbb16cfb5a35d672b7a0fcc1ed0cf1c600b"},
-    {file = "Pillow-9.4.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b9b752ab91e78234941e44abdecc07f1f0d8f51fb62941d32995b8161f68cfe5"},
-    {file = "Pillow-9.4.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3b56206244dc8711f7e8b7d6cad4663917cd5b2d950799425076681e8766286"},
-    {file = "Pillow-9.4.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aabdab8ec1e7ca7f1434d042bf8b1e92056245fb179790dc97ed040361f16bfd"},
-    {file = "Pillow-9.4.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:db74f5562c09953b2c5f8ec4b7dfd3f5421f31811e97d1dbc0a7c93d6e3a24df"},
-    {file = "Pillow-9.4.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e9d7747847c53a16a729b6ee5e737cf170f7a16611c143d95aa60a109a59c336"},
-    {file = "Pillow-9.4.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b52ff4f4e002f828ea6483faf4c4e8deea8d743cf801b74910243c58acc6eda3"},
-    {file = "Pillow-9.4.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:575d8912dca808edd9acd6f7795199332696d3469665ef26163cd090fa1f8bfa"},
-    {file = "Pillow-9.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3c4ed2ff6760e98d262e0cc9c9a7f7b8a9f61aa4d47c58835cdaf7b0b8811bb"},
-    {file = "Pillow-9.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e621b0246192d3b9cb1dc62c78cfa4c6f6d2ddc0ec207d43c0dedecb914f152a"},
-    {file = "Pillow-9.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8f127e7b028900421cad64f51f75c051b628db17fb00e099eb148761eed598c9"},
-    {file = "Pillow-9.4.0.tar.gz", hash = "sha256:a1c2d7780448eb93fbcc3789bf3916aa5720d942e37945f4056680317f1cd23e"},
+    {file = "Pillow-9.5.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:ace6ca218308447b9077c14ea4ef381ba0b67ee78d64046b3f19cf4e1139ad16"},
+    {file = "Pillow-9.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3d403753c9d5adc04d4694d35cf0391f0f3d57c8e0030aac09d7678fa8030aa"},
+    {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ba1b81ee69573fe7124881762bb4cd2e4b6ed9dd28c9c60a632902fe8db8b38"},
+    {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe7e1c262d3392afcf5071df9afa574544f28eac825284596ac6db56e6d11062"},
+    {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f36397bf3f7d7c6a3abdea815ecf6fd14e7fcd4418ab24bae01008d8d8ca15e"},
+    {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:252a03f1bdddce077eff2354c3861bf437c892fb1832f75ce813ee94347aa9b5"},
+    {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:85ec677246533e27770b0de5cf0f9d6e4ec0c212a1f89dfc941b64b21226009d"},
+    {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b416f03d37d27290cb93597335a2f85ed446731200705b22bb927405320de903"},
+    {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1781a624c229cb35a2ac31cc4a77e28cafc8900733a864870c49bfeedacd106a"},
+    {file = "Pillow-9.5.0-cp310-cp310-win32.whl", hash = "sha256:8507eda3cd0608a1f94f58c64817e83ec12fa93a9436938b191b80d9e4c0fc44"},
+    {file = "Pillow-9.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:d3c6b54e304c60c4181da1c9dadf83e4a54fd266a99c70ba646a9baa626819eb"},
+    {file = "Pillow-9.5.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:7ec6f6ce99dab90b52da21cf0dc519e21095e332ff3b399a357c187b1a5eee32"},
+    {file = "Pillow-9.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:560737e70cb9c6255d6dcba3de6578a9e2ec4b573659943a5e7e4af13f298f5c"},
+    {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96e88745a55b88a7c64fa49bceff363a1a27d9a64e04019c2281049444a571e3"},
+    {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d9c206c29b46cfd343ea7cdfe1232443072bbb270d6a46f59c259460db76779a"},
+    {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfcc2c53c06f2ccb8976fb5c71d448bdd0a07d26d8e07e321c103416444c7ad1"},
+    {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:a0f9bb6c80e6efcde93ffc51256d5cfb2155ff8f78292f074f60f9e70b942d99"},
+    {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8d935f924bbab8f0a9a28404422da8af4904e36d5c33fc6f677e4c4485515625"},
+    {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fed1e1cf6a42577953abbe8e6cf2fe2f566daebde7c34724ec8803c4c0cda579"},
+    {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c1170d6b195555644f0616fd6ed929dfcf6333b8675fcca044ae5ab110ded296"},
+    {file = "Pillow-9.5.0-cp311-cp311-win32.whl", hash = "sha256:54f7102ad31a3de5666827526e248c3530b3a33539dbda27c6843d19d72644ec"},
+    {file = "Pillow-9.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfa4561277f677ecf651e2b22dc43e8f5368b74a25a8f7d1d4a3a243e573f2d4"},
+    {file = "Pillow-9.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:965e4a05ef364e7b973dd17fc765f42233415974d773e82144c9bbaaaea5d089"},
+    {file = "Pillow-9.5.0-cp312-cp312-win32.whl", hash = "sha256:22baf0c3cf0c7f26e82d6e1adf118027afb325e703922c8dfc1d5d0156bb2eeb"},
+    {file = "Pillow-9.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:432b975c009cf649420615388561c0ce7cc31ce9b2e374db659ee4f7d57a1f8b"},
+    {file = "Pillow-9.5.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:5d4ebf8e1db4441a55c509c4baa7a0587a0210f7cd25fcfe74dbbce7a4bd1906"},
+    {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:375f6e5ee9620a271acb6820b3d1e94ffa8e741c0601db4c0c4d3cb0a9c224bf"},
+    {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99eb6cafb6ba90e436684e08dad8be1637efb71c4f2180ee6b8f940739406e78"},
+    {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfaaf10b6172697b9bceb9a3bd7b951819d1ca339a5ef294d1f1ac6d7f63270"},
+    {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:763782b2e03e45e2c77d7779875f4432e25121ef002a41829d8868700d119392"},
+    {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:35f6e77122a0c0762268216315bf239cf52b88865bba522999dc38f1c52b9b47"},
+    {file = "Pillow-9.5.0-cp37-cp37m-win32.whl", hash = "sha256:aca1c196f407ec7cf04dcbb15d19a43c507a81f7ffc45b690899d6a76ac9fda7"},
+    {file = "Pillow-9.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322724c0032af6692456cd6ed554bb85f8149214d97398bb80613b04e33769f6"},
+    {file = "Pillow-9.5.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:a0aa9417994d91301056f3d0038af1199eb7adc86e646a36b9e050b06f526597"},
+    {file = "Pillow-9.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f8286396b351785801a976b1e85ea88e937712ee2c3ac653710a4a57a8da5d9c"},
+    {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c830a02caeb789633863b466b9de10c015bded434deb3ec87c768e53752ad22a"},
+    {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fbd359831c1657d69bb81f0db962905ee05e5e9451913b18b831febfe0519082"},
+    {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8fc330c3370a81bbf3f88557097d1ea26cd8b019d6433aa59f71195f5ddebbf"},
+    {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:7002d0797a3e4193c7cdee3198d7c14f92c0836d6b4a3f3046a64bd1ce8df2bf"},
+    {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:229e2c79c00e85989a34b5981a2b67aa079fd08c903f0aaead522a1d68d79e51"},
+    {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9adf58f5d64e474bed00d69bcd86ec4bcaa4123bfa70a65ce72e424bfb88ed96"},
+    {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:662da1f3f89a302cc22faa9f14a262c2e3951f9dbc9617609a47521c69dd9f8f"},
+    {file = "Pillow-9.5.0-cp38-cp38-win32.whl", hash = "sha256:6608ff3bf781eee0cd14d0901a2b9cc3d3834516532e3bd673a0a204dc8615fc"},
+    {file = "Pillow-9.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:e49eb4e95ff6fd7c0c402508894b1ef0e01b99a44320ba7d8ecbabefddcc5569"},
+    {file = "Pillow-9.5.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:482877592e927fd263028c105b36272398e3e1be3269efda09f6ba21fd83ec66"},
+    {file = "Pillow-9.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3ded42b9ad70e5f1754fb7c2e2d6465a9c842e41d178f262e08b8c85ed8a1d8e"},
+    {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c446d2245ba29820d405315083d55299a796695d747efceb5717a8b450324115"},
+    {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aca1152d93dcc27dc55395604dcfc55bed5f25ef4c98716a928bacba90d33a3"},
+    {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:608488bdcbdb4ba7837461442b90ea6f3079397ddc968c31265c1e056964f1ef"},
+    {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:60037a8db8750e474af7ffc9faa9b5859e6c6d0a50e55c45576bf28be7419705"},
+    {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:07999f5834bdc404c442146942a2ecadd1cb6292f5229f4ed3b31e0a108746b1"},
+    {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a127ae76092974abfbfa38ca2d12cbeddcdeac0fb71f9627cc1135bedaf9d51a"},
+    {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:489f8389261e5ed43ac8ff7b453162af39c3e8abd730af8363587ba64bb2e865"},
+    {file = "Pillow-9.5.0-cp39-cp39-win32.whl", hash = "sha256:9b1af95c3a967bf1da94f253e56b6286b50af23392a886720f563c547e48e964"},
+    {file = "Pillow-9.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:77165c4a5e7d5a284f10a6efaa39a0ae8ba839da344f20b111d62cc932fa4e5d"},
+    {file = "Pillow-9.5.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:833b86a98e0ede388fa29363159c9b1a294b0905b5128baf01db683672f230f5"},
+    {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaf305d6d40bd9632198c766fb64f0c1a83ca5b667f16c1e79e1661ab5060140"},
+    {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0852ddb76d85f127c135b6dd1f0bb88dbb9ee990d2cd9aa9e28526c93e794fba"},
+    {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:91ec6fe47b5eb5a9968c79ad9ed78c342b1f97a091677ba0e012701add857829"},
+    {file = "Pillow-9.5.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cb841572862f629b99725ebaec3287fc6d275be9b14443ea746c1dd325053cbd"},
+    {file = "Pillow-9.5.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c380b27d041209b849ed246b111b7c166ba36d7933ec6e41175fd15ab9eb1572"},
+    {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c9af5a3b406a50e313467e3565fc99929717f780164fe6fbb7704edba0cebbe"},
+    {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5671583eab84af046a397d6d0ba25343c00cd50bce03787948e0fff01d4fd9b1"},
+    {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:84a6f19ce086c1bf894644b43cd129702f781ba5751ca8572f08aa40ef0ab7b7"},
+    {file = "Pillow-9.5.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1e7723bd90ef94eda669a3c2c19d549874dd5badaeefabefd26053304abe5799"},
+    {file = "Pillow-9.5.0.tar.gz", hash = "sha256:bf548479d336726d7a0eceb6e767e179fbde37833ae42794602631a070d630f1"},
 ]
 
 [package.extras]
-docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinxext-opengraph"]
+docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"]
 tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
 
 [[package]]
 name = "platformdirs"
-version = "2.6.2"
+version = "3.2.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-2.6.2-py3-none-any.whl", hash = "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490"},
-    {file = "platformdirs-2.6.2.tar.gz", hash = "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2"},
+    {file = "platformdirs-3.2.0-py3-none-any.whl", hash = "sha256:ebe11c0d7a805086e99506aa331612429a72ca7cd52a1f0d277dc4adc20cb10e"},
+    {file = "platformdirs-3.2.0.tar.gz", hash = "sha256:d5b638ca397f25f979350ff789db335903d7ea010ab28903f57b27e1b16c2b08"},
 ]
 
 [package.extras]
-docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"]
-test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"]
+docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"]
+test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"]
 
 [[package]]
 name = "pluggy"
@@ -1168,18 +1270,19 @@ plugins = ["importlib-metadata"]
 
 [[package]]
 name = "pymdown-extensions"
-version = "9.9.1"
+version = "9.11"
 description = "Extension pack for Python Markdown."
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "pymdown_extensions-9.9.1-py3-none-any.whl", hash = "sha256:8a8973933ab45b6fe8f5f8da1de25766356b1f91dee107bf4a34efd158dc340b"},
-    {file = "pymdown_extensions-9.9.1.tar.gz", hash = "sha256:abed29926960bbb3b40f5ed5fa6375e29724d4e3cb86ced7c2bbd37ead1afeea"},
+    {file = "pymdown_extensions-9.11-py3-none-any.whl", hash = "sha256:a499191d8d869f30339de86fcf072a787e86c42b6f16f280f5c2cf174182b7f3"},
+    {file = "pymdown_extensions-9.11.tar.gz", hash = "sha256:f7e86c1d3981f23d9dc43294488ecb54abadd05b0be4bf8f0e15efc90f7853ff"},
 ]
 
 [package.dependencies]
 markdown = ">=3.2"
+pyyaml = "*"
 
 [[package]]
 name = "pyparsing"
@@ -1198,14 +1301,14 @@ diagrams = ["jinja2", "railroad-diagrams"]
 
 [[package]]
 name = "pytest"
-version = "7.2.0"
+version = "7.2.2"
 description = "pytest: simple powerful testing with Python"
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"},
-    {file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"},
+    {file = "pytest-7.2.2-py3-none-any.whl", hash = "sha256:130328f552dcfac0b1cec75c12e3f005619dc5f874f0a06e8ff7263f0ee6225e"},
+    {file = "pytest-7.2.2.tar.gz", hash = "sha256:c99ab0c73aceb050f68929bc93af19ab6db0558791c6a0715723abe9d0ade9d4"},
 ]
 
 [package.dependencies]
@@ -1302,107 +1405,79 @@ pyyaml = "*"
 
 [[package]]
 name = "regex"
-version = "2022.10.31"
+version = "2023.3.23"
 description = "Alternative regular expression module, to replace re."
 category = "dev"
 optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.8"
 files = [
-    {file = "regex-2022.10.31-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a8ff454ef0bb061e37df03557afda9d785c905dab15584860f982e88be73015f"},
-    {file = "regex-2022.10.31-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1eba476b1b242620c266edf6325b443a2e22b633217a9835a52d8da2b5c051f9"},
-    {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0e5af9a9effb88535a472e19169e09ce750c3d442fb222254a276d77808620b"},
-    {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d03fe67b2325cb3f09be029fd5da8df9e6974f0cde2c2ac6a79d2634e791dd57"},
-    {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9d0b68ac1743964755ae2d89772c7e6fb0118acd4d0b7464eaf3921c6b49dd4"},
-    {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a45b6514861916c429e6059a55cf7db74670eaed2052a648e3e4d04f070e001"},
-    {file = "regex-2022.10.31-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8b0886885f7323beea6f552c28bff62cbe0983b9fbb94126531693ea6c5ebb90"},
-    {file = "regex-2022.10.31-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5aefb84a301327ad115e9d346c8e2760009131d9d4b4c6b213648d02e2abe144"},
-    {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:702d8fc6f25bbf412ee706bd73019da5e44a8400861dfff7ff31eb5b4a1276dc"},
-    {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a3c1ebd4ed8e76e886507c9eddb1a891673686c813adf889b864a17fafcf6d66"},
-    {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:50921c140561d3db2ab9f5b11c5184846cde686bb5a9dc64cae442926e86f3af"},
-    {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:7db345956ecce0c99b97b042b4ca7326feeec6b75facd8390af73b18e2650ffc"},
-    {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:763b64853b0a8f4f9cfb41a76a4a85a9bcda7fdda5cb057016e7706fde928e66"},
-    {file = "regex-2022.10.31-cp310-cp310-win32.whl", hash = "sha256:44136355e2f5e06bf6b23d337a75386371ba742ffa771440b85bed367c1318d1"},
-    {file = "regex-2022.10.31-cp310-cp310-win_amd64.whl", hash = "sha256:bfff48c7bd23c6e2aec6454aaf6edc44444b229e94743b34bdcdda2e35126cf5"},
-    {file = "regex-2022.10.31-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b4b1fe58cd102d75ef0552cf17242705ce0759f9695334a56644ad2d83903fe"},
-    {file = "regex-2022.10.31-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:542e3e306d1669b25936b64917285cdffcd4f5c6f0247636fec037187bd93542"},
-    {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c27cc1e4b197092e50ddbf0118c788d9977f3f8f35bfbbd3e76c1846a3443df7"},
-    {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8e38472739028e5f2c3a4aded0ab7eadc447f0d84f310c7a8bb697ec417229e"},
-    {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:76c598ca73ec73a2f568e2a72ba46c3b6c8690ad9a07092b18e48ceb936e9f0c"},
-    {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c28d3309ebd6d6b2cf82969b5179bed5fefe6142c70f354ece94324fa11bf6a1"},
-    {file = "regex-2022.10.31-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9af69f6746120998cd9c355e9c3c6aec7dff70d47247188feb4f829502be8ab4"},
-    {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a5f9505efd574d1e5b4a76ac9dd92a12acb2b309551e9aa874c13c11caefbe4f"},
-    {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5ff525698de226c0ca743bfa71fc6b378cda2ddcf0d22d7c37b1cc925c9650a5"},
-    {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:4fe7fda2fe7c8890d454f2cbc91d6c01baf206fbc96d89a80241a02985118c0c"},
-    {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:2cdc55ca07b4e70dda898d2ab7150ecf17c990076d3acd7a5f3b25cb23a69f1c"},
-    {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:44a6c2f6374e0033873e9ed577a54a3602b4f609867794c1a3ebba65e4c93ee7"},
-    {file = "regex-2022.10.31-cp311-cp311-win32.whl", hash = "sha256:d8716f82502997b3d0895d1c64c3b834181b1eaca28f3f6336a71777e437c2af"},
-    {file = "regex-2022.10.31-cp311-cp311-win_amd64.whl", hash = "sha256:61edbca89aa3f5ef7ecac8c23d975fe7261c12665f1d90a6b1af527bba86ce61"},
-    {file = "regex-2022.10.31-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0a069c8483466806ab94ea9068c34b200b8bfc66b6762f45a831c4baaa9e8cdd"},
-    {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d26166acf62f731f50bdd885b04b38828436d74e8e362bfcb8df221d868b5d9b"},
-    {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac741bf78b9bb432e2d314439275235f41656e189856b11fb4e774d9f7246d81"},
-    {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75f591b2055523fc02a4bbe598aa867df9e953255f0b7f7715d2a36a9c30065c"},
-    {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b30bddd61d2a3261f025ad0f9ee2586988c6a00c780a2fb0a92cea2aa702c54"},
-    {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef4163770525257876f10e8ece1cf25b71468316f61451ded1a6f44273eedeb5"},
-    {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7b280948d00bd3973c1998f92e22aa3ecb76682e3a4255f33e1020bd32adf443"},
-    {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:d0213671691e341f6849bf33cd9fad21f7b1cb88b89e024f33370733fec58742"},
-    {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:22e7ebc231d28393dfdc19b185d97e14a0f178bedd78e85aad660e93b646604e"},
-    {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:8ad241da7fac963d7573cc67a064c57c58766b62a9a20c452ca1f21050868dfa"},
-    {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:586b36ebda81e6c1a9c5a5d0bfdc236399ba6595e1397842fd4a45648c30f35e"},
-    {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:0653d012b3bf45f194e5e6a41df9258811ac8fc395579fa82958a8b76286bea4"},
-    {file = "regex-2022.10.31-cp36-cp36m-win32.whl", hash = "sha256:144486e029793a733e43b2e37df16a16df4ceb62102636ff3db6033994711066"},
-    {file = "regex-2022.10.31-cp36-cp36m-win_amd64.whl", hash = "sha256:c14b63c9d7bab795d17392c7c1f9aaabbffd4cf4387725a0ac69109fb3b550c6"},
-    {file = "regex-2022.10.31-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4cac3405d8dda8bc6ed499557625585544dd5cbf32072dcc72b5a176cb1271c8"},
-    {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23cbb932cc53a86ebde0fb72e7e645f9a5eec1a5af7aa9ce333e46286caef783"},
-    {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74bcab50a13960f2a610cdcd066e25f1fd59e23b69637c92ad470784a51b1347"},
-    {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78d680ef3e4d405f36f0d6d1ea54e740366f061645930072d39bca16a10d8c93"},
-    {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce6910b56b700bea7be82c54ddf2e0ed792a577dfaa4a76b9af07d550af435c6"},
-    {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:659175b2144d199560d99a8d13b2228b85e6019b6e09e556209dfb8c37b78a11"},
-    {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1ddf14031a3882f684b8642cb74eea3af93a2be68893901b2b387c5fd92a03ec"},
-    {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b683e5fd7f74fb66e89a1ed16076dbab3f8e9f34c18b1979ded614fe10cdc4d9"},
-    {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2bde29cc44fa81c0a0c8686992c3080b37c488df167a371500b2a43ce9f026d1"},
-    {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4919899577ba37f505aaebdf6e7dc812d55e8f097331312db7f1aab18767cce8"},
-    {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:9c94f7cc91ab16b36ba5ce476f1904c91d6c92441f01cd61a8e2729442d6fcf5"},
-    {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ae1e96785696b543394a4e3f15f3f225d44f3c55dafe3f206493031419fedf95"},
-    {file = "regex-2022.10.31-cp37-cp37m-win32.whl", hash = "sha256:c670f4773f2f6f1957ff8a3962c7dd12e4be54d05839b216cb7fd70b5a1df394"},
-    {file = "regex-2022.10.31-cp37-cp37m-win_amd64.whl", hash = "sha256:8e0caeff18b96ea90fc0eb6e3bdb2b10ab5b01a95128dfeccb64a7238decf5f0"},
-    {file = "regex-2022.10.31-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:131d4be09bea7ce2577f9623e415cab287a3c8e0624f778c1d955ec7c281bd4d"},
-    {file = "regex-2022.10.31-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e613a98ead2005c4ce037c7b061f2409a1a4e45099edb0ef3200ee26ed2a69a8"},
-    {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:052b670fafbe30966bbe5d025e90b2a491f85dfe5b2583a163b5e60a85a321ad"},
-    {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa62a07ac93b7cb6b7d0389d8ef57ffc321d78f60c037b19dfa78d6b17c928ee"},
-    {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5352bea8a8f84b89d45ccc503f390a6be77917932b1c98c4cdc3565137acc714"},
-    {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20f61c9944f0be2dc2b75689ba409938c14876c19d02f7585af4460b6a21403e"},
-    {file = "regex-2022.10.31-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29c04741b9ae13d1e94cf93fca257730b97ce6ea64cfe1eba11cf9ac4e85afb6"},
-    {file = "regex-2022.10.31-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:543883e3496c8b6d58bd036c99486c3c8387c2fc01f7a342b760c1ea3158a318"},
-    {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7a8b43ee64ca8f4befa2bea4083f7c52c92864d8518244bfa6e88c751fa8fff"},
-    {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6a9a19bea8495bb419dc5d38c4519567781cd8d571c72efc6aa959473d10221a"},
-    {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6ffd55b5aedc6f25fd8d9f905c9376ca44fcf768673ffb9d160dd6f409bfda73"},
-    {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4bdd56ee719a8f751cf5a593476a441c4e56c9b64dc1f0f30902858c4ef8771d"},
-    {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8ca88da1bd78990b536c4a7765f719803eb4f8f9971cc22d6ca965c10a7f2c4c"},
-    {file = "regex-2022.10.31-cp38-cp38-win32.whl", hash = "sha256:5a260758454580f11dd8743fa98319bb046037dfab4f7828008909d0aa5292bc"},
-    {file = "regex-2022.10.31-cp38-cp38-win_amd64.whl", hash = "sha256:5e6a5567078b3eaed93558842346c9d678e116ab0135e22eb72db8325e90b453"},
-    {file = "regex-2022.10.31-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5217c25229b6a85049416a5c1e6451e9060a1edcf988641e309dbe3ab26d3e49"},
-    {file = "regex-2022.10.31-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4bf41b8b0a80708f7e0384519795e80dcb44d7199a35d52c15cc674d10b3081b"},
-    {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cf0da36a212978be2c2e2e2d04bdff46f850108fccc1851332bcae51c8907cc"},
-    {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d403d781b0e06d2922435ce3b8d2376579f0c217ae491e273bab8d092727d244"},
-    {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a37d51fa9a00d265cf73f3de3930fa9c41548177ba4f0faf76e61d512c774690"},
-    {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4f781ffedd17b0b834c8731b75cce2639d5a8afe961c1e58ee7f1f20b3af185"},
-    {file = "regex-2022.10.31-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d243b36fbf3d73c25e48014961e83c19c9cc92530516ce3c43050ea6276a2ab7"},
-    {file = "regex-2022.10.31-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:370f6e97d02bf2dd20d7468ce4f38e173a124e769762d00beadec3bc2f4b3bc4"},
-    {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:597f899f4ed42a38df7b0e46714880fb4e19a25c2f66e5c908805466721760f5"},
-    {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7dbdce0c534bbf52274b94768b3498abdf675a691fec5f751b6057b3030f34c1"},
-    {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:22960019a842777a9fa5134c2364efaed5fbf9610ddc5c904bd3a400973b0eb8"},
-    {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:7f5a3ffc731494f1a57bd91c47dc483a1e10048131ffb52d901bfe2beb6102e8"},
-    {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7ef6b5942e6bfc5706301a18a62300c60db9af7f6368042227ccb7eeb22d0892"},
-    {file = "regex-2022.10.31-cp39-cp39-win32.whl", hash = "sha256:395161bbdbd04a8333b9ff9763a05e9ceb4fe210e3c7690f5e68cedd3d65d8e1"},
-    {file = "regex-2022.10.31-cp39-cp39-win_amd64.whl", hash = "sha256:957403a978e10fb3ca42572a23e6f7badff39aa1ce2f4ade68ee452dc6807692"},
-    {file = "regex-2022.10.31.tar.gz", hash = "sha256:a3a98921da9a1bf8457aeee6a551948a83601689e5ecdd736894ea9bbec77e83"},
+    {file = "regex-2023.3.23-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:845a5e2d84389c4ddada1a9b95c055320070f18bb76512608374aca00d22eca8"},
+    {file = "regex-2023.3.23-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:87d9951f5a538dd1d016bdc0dcae59241d15fa94860964833a54d18197fcd134"},
+    {file = "regex-2023.3.23-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37ae17d3be44c0b3f782c28ae9edd8b47c1f1776d4cabe87edc0b98e1f12b021"},
+    {file = "regex-2023.3.23-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0b8eb1e3bca6b48dc721818a60ae83b8264d4089a4a41d62be6d05316ec38e15"},
+    {file = "regex-2023.3.23-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df45fac182ebc3c494460c644e853515cc24f5ad9da05f8ffb91da891bfee879"},
+    {file = "regex-2023.3.23-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7006105b10b59971d3b248ad75acc3651c7e4cf54d81694df5a5130a3c3f7ea"},
+    {file = "regex-2023.3.23-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93f3f1aa608380fe294aa4cb82e2afda07a7598e828d0341e124b8fd9327c715"},
+    {file = "regex-2023.3.23-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:787954f541ab95d8195d97b0b8cf1dc304424adb1e07365967e656b92b38a699"},
+    {file = "regex-2023.3.23-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:20abe0bdf03630fe92ccafc45a599bca8b3501f48d1de4f7d121153350a2f77d"},
+    {file = "regex-2023.3.23-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:11d00c31aeab9a6e0503bc77e73ed9f4527b3984279d997eb145d7c7be6268fd"},
+    {file = "regex-2023.3.23-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:d5bbe0e1511b844794a3be43d6c145001626ba9a6c1db8f84bdc724e91131d9d"},
+    {file = "regex-2023.3.23-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ea3c0cb56eadbf4ab2277e7a095676370b3e46dbfc74d5c383bd87b0d6317910"},
+    {file = "regex-2023.3.23-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d895b4c863059a4934d3e874b90998df774644a41b349ebb330f85f11b4ef2c0"},
+    {file = "regex-2023.3.23-cp310-cp310-win32.whl", hash = "sha256:9d764514d19b4edcc75fd8cb1423448ef393e8b6cbd94f38cab983ab1b75855d"},
+    {file = "regex-2023.3.23-cp310-cp310-win_amd64.whl", hash = "sha256:11d1f2b7a0696dc0310de0efb51b1f4d813ad4401fe368e83c0c62f344429f98"},
+    {file = "regex-2023.3.23-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8a9c63cde0eaa345795c0fdeb19dc62d22e378c50b0bc67bf4667cd5b482d98b"},
+    {file = "regex-2023.3.23-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dd7200b4c27b68cf9c9646da01647141c6db09f48cc5b51bc588deaf8e98a797"},
+    {file = "regex-2023.3.23-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22720024b90a6ba673a725dcc62e10fb1111b889305d7c6b887ac7466b74bedb"},
+    {file = "regex-2023.3.23-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b190a339090e6af25f4a5fd9e77591f6d911cc7b96ecbb2114890b061be0ac1"},
+    {file = "regex-2023.3.23-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e76b6fc0d8e9efa39100369a9b3379ce35e20f6c75365653cf58d282ad290f6f"},
+    {file = "regex-2023.3.23-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7868b8f218bf69a2a15402fde08b08712213a1f4b85a156d90473a6fb6b12b09"},
+    {file = "regex-2023.3.23-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2472428efc4127374f494e570e36b30bb5e6b37d9a754f7667f7073e43b0abdd"},
+    {file = "regex-2023.3.23-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c37df2a060cb476d94c047b18572ee2b37c31f831df126c0da3cd9227b39253d"},
+    {file = "regex-2023.3.23-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4479f9e2abc03362df4045b1332d4a2b7885b245a30d4f4b051c4083b97d95d8"},
+    {file = "regex-2023.3.23-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e2396e0678167f2d0c197da942b0b3fb48fee2f0b5915a0feb84d11b6686afe6"},
+    {file = "regex-2023.3.23-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:75f288c60232a5339e0ff2fa05779a5e9c74e9fc085c81e931d4a264501e745b"},
+    {file = "regex-2023.3.23-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c869260aa62cee21c5eb171a466c0572b5e809213612ef8d495268cd2e34f20d"},
+    {file = "regex-2023.3.23-cp311-cp311-win32.whl", hash = "sha256:25f0532fd0c53e96bad84664171969de9673b4131f2297f1db850d3918d58858"},
+    {file = "regex-2023.3.23-cp311-cp311-win_amd64.whl", hash = "sha256:5ccfafd98473e007cebf7da10c1411035b7844f0f204015efd050601906dbb53"},
+    {file = "regex-2023.3.23-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6572ff287176c0fb96568adb292674b421fa762153ed074d94b1d939ed92c253"},
+    {file = "regex-2023.3.23-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a610e0adfcb0fc84ea25f6ea685e39e74cbcd9245a72a9a7aab85ff755a5ed27"},
+    {file = "regex-2023.3.23-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086afe222d58b88b62847bdbd92079b4699350b4acab892f88a935db5707c790"},
+    {file = "regex-2023.3.23-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79e29fd62fa2f597a6754b247356bda14b866131a22444d67f907d6d341e10f3"},
+    {file = "regex-2023.3.23-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c07ce8e9eee878a48ebeb32ee661b49504b85e164b05bebf25420705709fdd31"},
+    {file = "regex-2023.3.23-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86b036f401895e854de9fefe061518e78d506d8a919cc250dc3416bca03f6f9a"},
+    {file = "regex-2023.3.23-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78ac8dd8e18800bb1f97aad0d73f68916592dddf233b99d2b5cabc562088503a"},
+    {file = "regex-2023.3.23-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:539dd010dc35af935b32f248099e38447bbffc10b59c2b542bceead2bed5c325"},
+    {file = "regex-2023.3.23-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9bf4a5626f2a0ea006bf81e8963f498a57a47d58907eaa58f4b3e13be68759d8"},
+    {file = "regex-2023.3.23-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cf86b4328c204c3f315074a61bc1c06f8a75a8e102359f18ce99fbcbbf1951f0"},
+    {file = "regex-2023.3.23-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:2848bf76673c83314068241c8d5b7fa9ad9bed866c979875a0e84039349e8fa7"},
+    {file = "regex-2023.3.23-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:c125a02d22c555e68f7433bac8449992fa1cead525399f14e47c2d98f2f0e467"},
+    {file = "regex-2023.3.23-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cd1671e9d5ac05ce6aa86874dd8dfa048824d1dbe73060851b310c6c1a201a96"},
+    {file = "regex-2023.3.23-cp38-cp38-win32.whl", hash = "sha256:fffe57312a358be6ec6baeb43d253c36e5790e436b7bf5b7a38df360363e88e9"},
+    {file = "regex-2023.3.23-cp38-cp38-win_amd64.whl", hash = "sha256:dbb3f87e15d3dd76996d604af8678316ad2d7d20faa394e92d9394dfd621fd0c"},
+    {file = "regex-2023.3.23-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c88e8c226473b5549fe9616980ea7ca09289246cfbdf469241edf4741a620004"},
+    {file = "regex-2023.3.23-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6560776ec19c83f3645bbc5db64a7a5816c9d8fb7ed7201c5bcd269323d88072"},
+    {file = "regex-2023.3.23-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b1fc2632c01f42e06173d8dd9bb2e74ab9b0afa1d698058c867288d2c7a31f3"},
+    {file = "regex-2023.3.23-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fdf7ad455f1916b8ea5cdbc482d379f6daf93f3867b4232d14699867a5a13af7"},
+    {file = "regex-2023.3.23-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5fc33b27b1d800fc5b78d7f7d0f287e35079ecabe68e83d46930cf45690e1c8c"},
+    {file = "regex-2023.3.23-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c49552dc938e3588f63f8a78c86f3c9c75301e813bca0bef13bdb4b87ccf364"},
+    {file = "regex-2023.3.23-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e152461e9a0aedec7d37fc66ec0fa635eca984777d3d3c3e36f53bf3d3ceb16e"},
+    {file = "regex-2023.3.23-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:db034255e72d2995cf581b14bb3fc9c00bdbe6822b49fcd4eef79e1d5f232618"},
+    {file = "regex-2023.3.23-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:55ae114da21b7a790b90255ea52d2aa3a0d121a646deb2d3c6a3194e722fc762"},
+    {file = "regex-2023.3.23-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ef3f528fe1cc3d139508fe1b22523745aa77b9d6cb5b0bf277f48788ee0b993f"},
+    {file = "regex-2023.3.23-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:a81c9ec59ca2303acd1ccd7b9ac409f1e478e40e96f8f79b943be476c5fdb8bb"},
+    {file = "regex-2023.3.23-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:cde09c4fdd070772aa2596d97e942eb775a478b32459e042e1be71b739d08b77"},
+    {file = "regex-2023.3.23-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3cd9f5dd7b821f141d3a6ca0d5d9359b9221e4f051ca3139320adea9f1679691"},
+    {file = "regex-2023.3.23-cp39-cp39-win32.whl", hash = "sha256:7304863f3a652dab5e68e6fb1725d05ebab36ec0390676d1736e0571ebb713ef"},
+    {file = "regex-2023.3.23-cp39-cp39-win_amd64.whl", hash = "sha256:54c3fa855a3f7438149de3211738dd9b5f0c733f48b54ae05aa7fce83d48d858"},
+    {file = "regex-2023.3.23.tar.gz", hash = "sha256:dc80df325b43ffea5cdea2e3eaa97a44f3dd298262b1c7fe9dbb2a9522b956a7"},
 ]
 
 [[package]]
 name = "requests"
 version = "2.28.2"
 description = "Python HTTP for Humans."
-category = "dev"
+category = "main"
 optional = false
 python-versions = ">=3.7, <4"
 files = [
@@ -1420,16 +1495,28 @@ 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 = "rst2ansi"
+version = "0.1.5"
+description = "A rst converter to ansi-decorated console output"
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+    {file = "rst2ansi-0.1.5-py3-none-any.whl", hash = "sha256:b2cf192e38975918d07540bba7d673550cd7d28ca7443410984e22d5ab058fb3"},
+    {file = "rst2ansi-0.1.5.tar.gz", hash = "sha256:1b17fb9a628d40f57933ad1a3aa952346444be069469508e73e95060da33fe6f"},
+]
+
 [[package]]
 name = "setuptools"
-version = "65.7.0"
+version = "67.6.1"
 description = "Easily download, build, install, upgrade, and uninstall Python packages"
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "setuptools-65.7.0-py3-none-any.whl", hash = "sha256:8ab4f1dbf2b4a65f7eec5ad0c620e84c34111a68d3349833494b9088212214dd"},
-    {file = "setuptools-65.7.0.tar.gz", hash = "sha256:4d3c92fac8f1118bb77a22181355e29c239cabfe2b9effdaa665c66b711136d7"},
+    {file = "setuptools-67.6.1-py3-none-any.whl", hash = "sha256:e728ca814a823bf7bf60162daf9db95b93d532948c4c0bea762ce62f60189078"},
+    {file = "setuptools-67.6.1.tar.gz", hash = "sha256:257de92a9d50a60b8e22abfcbb771571fde0dbf3ec234463212027a4eeecbe9a"},
 ]
 
 [package.extras]
@@ -1449,6 +1536,21 @@ files = [
     {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
 ]
 
+[[package]]
+name = "tabulate"
+version = "0.9.0"
+description = "Pretty-print tabular data"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"},
+    {file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"},
+]
+
+[package.extras]
+widechars = ["wcwidth"]
+
 [[package]]
 name = "tomli"
 version = "2.0.1"
@@ -1461,16 +1563,49 @@ files = [
     {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
 ]
 
+[[package]]
+name = "tqdm"
+version = "4.65.0"
+description = "Fast, Extensible Progress Meter"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "tqdm-4.65.0-py3-none-any.whl", hash = "sha256:c4f53a17fe37e132815abceec022631be8ffe1b9381c2e6e30aa70edc99e9671"},
+    {file = "tqdm-4.65.0.tar.gz", hash = "sha256:1871fb68a86b8fb3b59ca4cdd3dcccbc7e6d613eeed31f4c332531977b89beb5"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
+
+[package.extras]
+dev = ["py-make (>=0.1.0)", "twine", "wheel"]
+notebook = ["ipywidgets (>=6)"]
+slack = ["slack-sdk"]
+telegram = ["requests"]
+
+[[package]]
+name = "typing-extensions"
+version = "4.5.0"
+description = "Backported and Experimental Type Hints for Python 3.7+"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"},
+    {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"},
+]
+
 [[package]]
 name = "urllib3"
-version = "1.26.14"
+version = "1.26.15"
 description = "HTTP library with thread-safe connection pooling, file post, and more."
-category = "dev"
+category = "main"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
 files = [
-    {file = "urllib3-1.26.14-py2.py3-none-any.whl", hash = "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"},
-    {file = "urllib3-1.26.14.tar.gz", hash = "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72"},
+    {file = "urllib3-1.26.15-py2.py3-none-any.whl", hash = "sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42"},
+    {file = "urllib3-1.26.15.tar.gz", hash = "sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305"},
 ]
 
 [package.extras]
@@ -1495,61 +1630,60 @@ test = ["coverage", "flake8 (>=3.7)", "mypy", "pretend", "pytest"]
 
 [[package]]
 name = "virtualenv"
-version = "20.17.1"
+version = "20.21.0"
 description = "Virtual Python Environment builder"
 category = "dev"
 optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
 files = [
-    {file = "virtualenv-20.17.1-py3-none-any.whl", hash = "sha256:ce3b1684d6e1a20a3e5ed36795a97dfc6af29bc3970ca8dab93e11ac6094b3c4"},
-    {file = "virtualenv-20.17.1.tar.gz", hash = "sha256:f8b927684efc6f1cc206c9db297a570ab9ad0e51c16fa9e45487d36d1905c058"},
+    {file = "virtualenv-20.21.0-py3-none-any.whl", hash = "sha256:31712f8f2a17bd06234fa97fdf19609e789dd4e3e4bf108c3da71d710651adbc"},
+    {file = "virtualenv-20.21.0.tar.gz", hash = "sha256:f50e3e60f990a0757c9b68333c9fdaa72d7188caa417f96af9e52407831a3b68"},
 ]
 
 [package.dependencies]
 distlib = ">=0.3.6,<1"
 filelock = ">=3.4.1,<4"
-platformdirs = ">=2.4,<3"
+platformdirs = ">=2.4,<4"
 
 [package.extras]
-docs = ["proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-argparse (>=0.3.2)", "sphinx-rtd-theme (>=1)", "towncrier (>=22.8)"]
-testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"]
+docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"]
+test = ["covdefaults (>=2.2.2)", "coverage (>=7.1)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23)", "pytest (>=7.2.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)"]
 
 [[package]]
 name = "watchdog"
-version = "2.2.1"
+version = "3.0.0"
 description = "Filesystem events monitoring"
 category = "dev"
 optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
 files = [
-    {file = "watchdog-2.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a09483249d25cbdb4c268e020cb861c51baab2d1affd9a6affc68ffe6a231260"},
-    {file = "watchdog-2.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5100eae58133355d3ca6c1083a33b81355c4f452afa474c2633bd2fbbba398b3"},
-    {file = "watchdog-2.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e618a4863726bc7a3c64f95c218437f3349fb9d909eb9ea3a1ed3b567417c661"},
-    {file = "watchdog-2.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:102a60093090fc3ff76c983367b19849b7cc24ec414a43c0333680106e62aae1"},
-    {file = "watchdog-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:748ca797ff59962e83cc8e4b233f87113f3cf247c23e6be58b8a2885c7337aa3"},
-    {file = "watchdog-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6ccd8d84b9490a82b51b230740468116b8205822ea5fdc700a553d92661253a3"},
-    {file = "watchdog-2.2.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6e01d699cd260d59b84da6bda019dce0a3353e3fcc774408ae767fe88ee096b7"},
-    {file = "watchdog-2.2.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8586d98c494690482c963ffb24c49bf9c8c2fe0589cec4dc2f753b78d1ec301d"},
-    {file = "watchdog-2.2.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:adaf2ece15f3afa33a6b45f76b333a7da9256e1360003032524d61bdb4c422ae"},
-    {file = "watchdog-2.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:83a7cead445008e880dbde833cb9e5cc7b9a0958edb697a96b936621975f15b9"},
-    {file = "watchdog-2.2.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f8ac23ff2c2df4471a61af6490f847633024e5aa120567e08d07af5718c9d092"},
-    {file = "watchdog-2.2.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d0f29fd9f3f149a5277929de33b4f121a04cf84bb494634707cfa8ea8ae106a8"},
-    {file = "watchdog-2.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:967636031fa4c4955f0f3f22da3c5c418aa65d50908d31b73b3b3ffd66d60640"},
-    {file = "watchdog-2.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:96cbeb494e6cbe3ae6aacc430e678ce4b4dd3ae5125035f72b6eb4e5e9eb4f4e"},
-    {file = "watchdog-2.2.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:61fdb8e9c57baf625e27e1420e7ca17f7d2023929cd0065eb79c83da1dfbeacd"},
-    {file = "watchdog-2.2.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4cb5ecc332112017fbdb19ede78d92e29a8165c46b68a0b8ccbd0a154f196d5e"},
-    {file = "watchdog-2.2.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a480d122740debf0afac4ddd583c6c0bb519c24f817b42ed6f850e2f6f9d64a8"},
-    {file = "watchdog-2.2.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:978a1aed55de0b807913b7482d09943b23a2d634040b112bdf31811a422f6344"},
-    {file = "watchdog-2.2.1-py3-none-manylinux2014_armv7l.whl", hash = "sha256:8c28c23972ec9c524967895ccb1954bc6f6d4a557d36e681a36e84368660c4ce"},
-    {file = "watchdog-2.2.1-py3-none-manylinux2014_i686.whl", hash = "sha256:c27d8c1535fd4474e40a4b5e01f4ba6720bac58e6751c667895cbc5c8a7af33c"},
-    {file = "watchdog-2.2.1-py3-none-manylinux2014_ppc64.whl", hash = "sha256:d6b87477752bd86ac5392ecb9eeed92b416898c30bd40c7e2dd03c3146105646"},
-    {file = "watchdog-2.2.1-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:cece1aa596027ff56369f0b50a9de209920e1df9ac6d02c7f9e5d8162eb4f02b"},
-    {file = "watchdog-2.2.1-py3-none-manylinux2014_s390x.whl", hash = "sha256:8b5cde14e5c72b2df5d074774bdff69e9b55da77e102a91f36ef26ca35f9819c"},
-    {file = "watchdog-2.2.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:e038be858425c4f621900b8ff1a3a1330d9edcfeaa1c0468aeb7e330fb87693e"},
-    {file = "watchdog-2.2.1-py3-none-win32.whl", hash = "sha256:bc43c1b24d2f86b6e1cc15f68635a959388219426109233e606517ff7d0a5a73"},
-    {file = "watchdog-2.2.1-py3-none-win_amd64.whl", hash = "sha256:17f1708f7410af92ddf591e94ae71a27a13974559e72f7e9fde3ec174b26ba2e"},
-    {file = "watchdog-2.2.1-py3-none-win_ia64.whl", hash = "sha256:195ab1d9d611a4c1e5311cbf42273bc541e18ea8c32712f2fb703cfc6ff006f9"},
-    {file = "watchdog-2.2.1.tar.gz", hash = "sha256:cdcc23c9528601a8a293eb4369cbd14f6b4f34f07ae8769421252e9c22718b6f"},
+    {file = "watchdog-3.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:336adfc6f5cc4e037d52db31194f7581ff744b67382eb6021c868322e32eef41"},
+    {file = "watchdog-3.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a70a8dcde91be523c35b2bf96196edc5730edb347e374c7de7cd20c43ed95397"},
+    {file = "watchdog-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:adfdeab2da79ea2f76f87eb42a3ab1966a5313e5a69a0213a3cc06ef692b0e96"},
+    {file = "watchdog-3.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2b57a1e730af3156d13b7fdddfc23dea6487fceca29fc75c5a868beed29177ae"},
+    {file = "watchdog-3.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7ade88d0d778b1b222adebcc0927428f883db07017618a5e684fd03b83342bd9"},
+    {file = "watchdog-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7e447d172af52ad204d19982739aa2346245cc5ba6f579d16dac4bfec226d2e7"},
+    {file = "watchdog-3.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9fac43a7466eb73e64a9940ac9ed6369baa39b3bf221ae23493a9ec4d0022674"},
+    {file = "watchdog-3.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8ae9cda41fa114e28faf86cb137d751a17ffd0316d1c34ccf2235e8a84365c7f"},
+    {file = "watchdog-3.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:25f70b4aa53bd743729c7475d7ec41093a580528b100e9a8c5b5efe8899592fc"},
+    {file = "watchdog-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4f94069eb16657d2c6faada4624c39464f65c05606af50bb7902e036e3219be3"},
+    {file = "watchdog-3.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7c5f84b5194c24dd573fa6472685b2a27cc5a17fe5f7b6fd40345378ca6812e3"},
+    {file = "watchdog-3.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3aa7f6a12e831ddfe78cdd4f8996af9cf334fd6346531b16cec61c3b3c0d8da0"},
+    {file = "watchdog-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:233b5817932685d39a7896b1090353fc8efc1ef99c9c054e46c8002561252fb8"},
+    {file = "watchdog-3.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:13bbbb462ee42ec3c5723e1205be8ced776f05b100e4737518c67c8325cf6100"},
+    {file = "watchdog-3.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8f3ceecd20d71067c7fd4c9e832d4e22584318983cabc013dbf3f70ea95de346"},
+    {file = "watchdog-3.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c9d8c8ec7efb887333cf71e328e39cffbf771d8f8f95d308ea4125bf5f90ba64"},
+    {file = "watchdog-3.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0e06ab8858a76e1219e68c7573dfeba9dd1c0219476c5a44d5333b01d7e1743a"},
+    {file = "watchdog-3.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:d00e6be486affb5781468457b21a6cbe848c33ef43f9ea4a73b4882e5f188a44"},
+    {file = "watchdog-3.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:c07253088265c363d1ddf4b3cdb808d59a0468ecd017770ed716991620b8f77a"},
+    {file = "watchdog-3.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:5113334cf8cf0ac8cd45e1f8309a603291b614191c9add34d33075727a967709"},
+    {file = "watchdog-3.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:51f90f73b4697bac9c9a78394c3acbbd331ccd3655c11be1a15ae6fe289a8c83"},
+    {file = "watchdog-3.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:ba07e92756c97e3aca0912b5cbc4e5ad802f4557212788e72a72a47ff376950d"},
+    {file = "watchdog-3.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:d429c2430c93b7903914e4db9a966c7f2b068dd2ebdd2fa9b9ce094c7d459f33"},
+    {file = "watchdog-3.0.0-py3-none-win32.whl", hash = "sha256:3ed7c71a9dccfe838c2f0b6314ed0d9b22e77d268c67e015450a29036a81f60f"},
+    {file = "watchdog-3.0.0-py3-none-win_amd64.whl", hash = "sha256:4c9956d27be0bb08fc5f30d9d0179a855436e655f046d288e2bcc11adfae893c"},
+    {file = "watchdog-3.0.0-py3-none-win_ia64.whl", hash = "sha256:5d9f3a10e02d7371cd929b5d8f11e87d4bad890212ed3901f9b4d68767bee759"},
+    {file = "watchdog-3.0.0.tar.gz", hash = "sha256:4d98a320595da7a7c5a18fc48cb633c2e73cda78f93cac2ef42d42bf609a33f9"},
 ]
 
 [package.extras]
@@ -1557,21 +1691,21 @@ watchmedo = ["PyYAML (>=3.10)"]
 
 [[package]]
 name = "zipp"
-version = "3.11.0"
+version = "3.15.0"
 description = "Backport of pathlib-compatible object wrapper for zip files"
-category = "dev"
+category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "zipp-3.11.0-py3-none-any.whl", hash = "sha256:83a28fcb75844b5c0cdaf5aa4003c2d728c77e05f5aeabe8e95e56727005fbaa"},
-    {file = "zipp-3.11.0.tar.gz", hash = "sha256:a7a22e05929290a67401440b39690ae6563279bced5f314609d9d03798f56766"},
+    {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"},
+    {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"},
 ]
 
 [package.extras]
-docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"]
-testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
+docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
+testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
 
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.8"
-content-hash = "7c24a70273ce9651d48352e0d0289b7f407e5dbc9d044aed358dc095e62d8678"
+content-hash = "469f0d9c3dee47a69af12fdc330d4d484d5fd2f4e2c5bc6ca5f6bd2a5a1644c1"
diff --git a/pyproject.toml b/pyproject.toml
index 264e81d..c088c03 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -29,6 +29,7 @@ classifiers = [
 python = "^3.8"
 networkx = "^3.0"
 matplotlib = "^3.5.3"
+b2 = "^3.7.1"
 
 [tool.poetry.dev-dependencies]
 mkdocs = "^1.3.1"
@@ -49,7 +50,6 @@ sourcepath = "src"
 
 [tool.isort]
 profile = "black"
-extend_skip = ["src/pytestarch/__init__.py"]
 add_imports = ["from __future__ import annotations"]
 
 [tool.bandit]


Test patch:
diff --git a/src/pytestarch/__init__.py b/src/pytestarch/__init__.py
index 18ca9ce..bfa24e0 100644
--- a/src/pytestarch/__init__.py
+++ b/src/pytestarch/__init__.py
@@ -1,9 +1,13 @@
 from pytestarch.eval_structure.evaluable_architecture import (  # noqa: F401
     EvaluableArchitecture,
 )
+from pytestarch.query_language.rule import Rule  # noqa: F401
 from .pytestarch import get_evaluable_architecture  # noqa: F401 E401
 from .pytestarch import (  # noqa: F401 E401; noqa: F401 E401
     get_evaluable_architecture_for_module_objects,
 )
-from pytestarch.query_language.rule import Rule  # noqa: F401
+from pytestarch.query_language.layered_architecture_rule import (  # noqa: F401
+    LayeredArchitecture,
+    LayerRule,
+)
 from pytestarch.diagram_extension.diagram_rule import DiagramRule  # noqa: F401
diff --git a/src/pytestarch/eval_structure/evaluable_architecture.py b/src/pytestarch/eval_structure/evaluable_architecture.py
index 16ce769..31d67b1 100644
--- a/src/pytestarch/eval_structure/evaluable_architecture.py
+++ b/src/pytestarch/eval_structure/evaluable_architecture.py
@@ -1,9 +1,82 @@
-"""Abstract types to specify the interface of evaluable objects."""
+"""Abstract interface of evaluable objects."""
 
 from __future__ import annotations
 
+from bisect import bisect
 from dataclasses import dataclass
-from typing import Any, Dict, List, Optional, Protocol, Tuple, Union
+from typing import Any, Dict, Iterable, List, Optional, Protocol, Tuple, Union
+
+from pytestarch.eval_structure.exceptions import LayerMismatch
+
+Layer = str
+ModuleName = str
+
+
+class LayerMapping:
+    def __init__(self, layer_mapping: Dict[Layer, List[Module]]) -> None:
+        self._layer_mapping = layer_mapping
+        self._module_mapping = {
+            module: layer
+            for layer, modules in layer_mapping.items()
+            for module in modules
+        }
+
+        self._sorted_module_names = sorted(
+            map(lambda module: module.name, self._module_mapping.keys())
+        )
+
+    def get_modules(self, layer: Layer) -> List[Module]:
+        return self._layer_mapping[layer]
+
+    def get_layer(self, module: Module) -> Optional[Layer]:
+        """Attempts to find the layer the given module belongs to. If the module does not appear in the
+        layer definition itself, it is checked whether the module is a submodule of one of the modules in the layer
+        definition. If so, the layer of the parent module is returned. Otherwise, None is returned.
+        This assumes that if a module is in layer X, all of its submodules are as well.
+        """
+        if not module.name:
+            return None
+
+        layer_candidate = self._module_mapping.get(module, None)
+
+        if layer_candidate is not None:
+            return layer_candidate
+
+        idx_of_module = (
+            bisect(self._sorted_module_names, module.name) - 1
+        )  # -1 since idx is after the insertion point
+
+        candidate_parent_modules = []
+        while idx_of_module >= 0:
+            parent_module_candidate = self._sorted_module_names[idx_of_module]
+            if module.name.startswith(parent_module_candidate):
+                candidate_parent_modules.append(Module(name=parent_module_candidate))
+            idx_of_module -= 1
+
+        candidate_layers = set(
+            map(lambda module: self._module_mapping[module], candidate_parent_modules)
+        )
+
+        if len(candidate_layers) > 1:
+            raise LayerMismatch(
+                f"Multiple possible parent modules found for module {module.name}."
+            )
+
+        if not candidate_layers:
+            return None
+        else:
+            return candidate_layers.pop()
+
+    def get_layer_for_module_name(self, module_name: str) -> Optional[Layer]:
+        return self.get_layer(Module(name=module_name))
+
+    @property
+    def all_modules(self) -> Iterable[Module]:
+        return self._module_mapping.keys()
+
+    @property
+    def all_layers(self) -> Iterable[Layer]:
+        return self._layer_mapping.keys()
 
 
 @dataclass(frozen=True)
diff --git a/src/pytestarch/eval_structure/exceptions.py b/src/pytestarch/eval_structure/exceptions.py
index 28d8e49..ae6d531 100644
--- a/src/pytestarch/eval_structure/exceptions.py
+++ b/src/pytestarch/eval_structure/exceptions.py
@@ -3,3 +3,7 @@
 
 class ImpossibleMatch(Exception):
     pass
+
+
+class LayerMismatch(Exception):
+    pass
diff --git a/src/pytestarch/query_language/LANGUAGE_DEFINTION.md b/src/pytestarch/query_language/LANGUAGE_DEFINTION.md
index 80f172b..1d2defd 100644
--- a/src/pytestarch/query_language/LANGUAGE_DEFINTION.md
+++ b/src/pytestarch/query_language/LANGUAGE_DEFINTION.md
@@ -151,3 +151,86 @@ simplified:
 | should only be imported except -- no import | M1 is not imported by any that is not M2, M3                                                                                                 | concatentation with ","                             | list all M2s  |
 | should only be imported except -- both      | Violator2 imports Violator1. Subviolator2 imports Subviolator1. Violator3 imports Subviolator1. M1 is not imported by any that is not M2, M3 | (submodules of M1, M2, M3); concatentation with "," | list all M2s  |
 | should not be imported except               | Violator2 imports Violator1. Violator3 imports Subviolator1                                                                                  | (submodule of M1)                                   | -             |
+
+
+# Layer Rule Syntax
+LayeredArchitecture().layer("name").containing_modules([M1, M2]).layer(...)...
+
+LayerRule()
+    .layers_that()
+    .are_named("name"/["X", "Y"])
+    .should/not/only()
+    .access_layers_that()/be_accessed_by_layers_that()/access_any_layer_except_layers_that()/be_accessed_by_any_layer_except_layers_that()/access_anything()/be_accessed_by_anything()
+    .are_named("X"/["X", "Y"])
+    .assert_applies(evaluable, architecture)
+
+
+Layer 1 should import Layer 2                       - 1+ module from Layer 1 imports 1+ module from Layer 2
+Layer 1 should not import Layer 2                   - no module from Layer 1 imports any module from Layer 2
+Layer 1 should only import Layer 2                  - 1+ module from Layer 1 imports 1+ module from Layer 2; no module from Layer 1 imports something, that is not in Layer 2
+
+Layer 1 should import except Layer 2                - 1+ module from Layer 1 imports something, that is not in Layer 2
+Layer 1 should not import except Layer 2            - no module from Layer 1 imports something, that is not in Layer 2
+Layer 1 should only import except Layer 2           - 1+ module from Layer 1 imports something, that is not in Layer 2; no module from Layer 1 imports any module from Layer 2
+
+Layer 1 should be imported by Layer 2               - 1+ module from Layer 2 imports 1+ module from Layer 1
+Layer 1 should not be imported by Layer 2           - no module from Layer 1 imports any module from Layer 1
+Layer 1 should only be imported by Layer 2          - 1+ module from Layer 2 imports 1+ module from Layer 1; no module imports something from Layer 1, that is not in Layer 2
+
+Layer 1 should be imported except by Layer 2        - 1+ module imports something from Layer 1, that is not Layer 2
+Layer 1 should not be imported except by Layer 2    - no module imports something from Layer 1, that is not Layer 2
+Layer 1 should only be imported except by Layer 2   - 1+ module imports something from Layer 1, that is not Layer 2; no module from Layer 2 imports any module from Layer 1
+
+
+If multiple rule subjects are given, the rule has to apply to all of them. If multiple rule objects are given, the rule has to apply to all of them.
+
+## Rule Violation Messages
+All concrete modules are listed with "(LAYER X)" behind their name, such as "Violator1 (LAYER X) imports Violator2 (LAYER Y)".
+### One M2
+| type                                        | message                                                                                                              | note                   |
+|---------------------------------------------|----------------------------------------------------------------------------------------------------------------------|------------------------|
+| should import                               | LAYER M1 does not import LAYER M2                                                                                    | -                      |
+| should only import -- forbidden             | Violator1 imports Violator2. Subviolator1 imports Violator3                                                          | (submodule of M1)      |
+| should only import -- no import             | LAYER M1 does not import LAYER M2                                                                                    | -                      |
+| should only import -- both                  | Violator1 imports Violator2. Subviolator1 imports Violator3. LAYER M1 does not import LAYER M2                       | (submodule of M1)      |
+| should not import                           | Violator1 imports Violator2. Subviolator1 imports Subviolator2.                                                      | (submodules of M1, M2) |
+| should import except                        | LAYER M1 does not import any that is not LAYER M2                                                                    | -                      |
+| should only import except -- forbidden      | Violator1 imports Violator2. Subviolator1 imports Subviolator2.                                                      | (submodules of M1, M2) |
+| should only import except -- no import      | LAYER M1 does not import any that is not LAYER M2                                                                    | -                      |
+| should only import except -- both           | Violator1 imports Violator2. Subviolator1 imports Subviolator2. M1 does not import any that is not M2                | (submodules of M1, M2) |
+| should not import except                    | Violator1 imports Violator2. Subviolator1 imports Violator3.                                                         | (submodule of M1)      |
+| should be imported                          | LAYER M1 is not imported by LAYER M2                                                                                 | -                      |
+| should only be imported -- forbidden        | Violator2 imports Violator1. Violator3 imports Subviolator1                                                          | (submodule of M1)      |
+| should only be imported -- no import        | LAYER M1 is not imported by LAYER M2                                                                                 | -                      |
+| should only be imported -- both             | Violator2 imports Violator1. Violator3 imports Subviolator1. LAYER M1 is not imported by LAYER M2                    | (submodule of M1)      |
+| should not be imported                      | Violator2 imports Violator1. Violator 3 imports Subviolator1.                                                        | (submodules of M1, M2) |
+| should be imported except                   | LAYER M1 is not imported by any that is not LAYER M2                                                                 | -                      |
+| should only be imported except -- forbidden | Violator2 imports Violator1. Subviolator2 imports Subviolator1.                                                      | (submodules of M1, M2) |
+| should only be imported except -- no import | LAYER M1 is not imported by any that is not LAYER M2                                                                 | -                      |
+| should only be imported except -- both      | Violator2 imports Violator1. Subviolator2 imports Subviolator1. LAYER M1 is not imported by any that is not LAYER M2 | (submodules of M1, M2) |
+| should not be imported except               | Violator2 imports Violator1. Violator3 imports Subviolator1.                                                         | (submodule of M1)      |
+
+
+### Multiple M2s
+| type                                        | message                                                                                                                                                        | note                                                | diff to One M2 |
+|---------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------|---------------|
+| should import                               | LAYER M1 does not import LAYER M2, LAYER M3                                                                                                                    | concatentation with ","                             | list all M2s  |
+| should only import -- forbidden             | Violator1 imports Violator2. Subviolator1 imports Violator3.                                                                                                   | (submodule of M1)                                   | -             |
+| should only import -- no import             | LAYER M1 does not import LAYER M2(, LAYER M3)                                                                                                                  | concatentation with ","                             | -             |
+| should only import -- both                  | Violator1 imports Violator2. Subviolator1 imports Violator3. M1 does not import M2(, M3)                                                                       | (submodule of M1); concatentation with ","          | -             |
+| should not import                           | Violator1 imports Violator2. Subviolator1 imports Subviolator2. Subviolator1 imports Subviolator2. Subviolator1 imports Violator3                              | (submodules of M1, M2, M3); concatentation with "," | -             |
+| should import except                        | LAYER M1 does not import any that is not LAYER M2, LAYER M3                                                                                                    | concatentation with ","                             | list all M2s  |
+| should only import except -- forbidden      | Violator1 imports Violator2. Subviolator1 imports Subviolator2. Subviolator1 imports Violator3                                                                 | (submodules of M1, M2, M3); concatentation with "," | -             ||
+| should only import except -- no import      | LAYER M1 does not import any that is not LAYER M2, LAYER M3                                                                                                    | concatentation with ","                             | list all M2s  |
+| should only import except -- both           | Violator1 imports Violator2. Subviolator1 imports Subviolator2. Subviolator1 imports Violator3 . LAYER M1 does not import any that is not LAYER M2, LAYER M3   | (submodules of M1, M2, M3); concatentation with "," | list all M2s  |
+| should not import except                    | Violator1 imports Violator2. Subviolator1 imports Subviolator2. Subviolator1 imports Violator3                                                                 | (submodule of M1)                                   | -             |
+| should be imported                          | LAYER M1 is not imported by LAYER M2, LAYER M3                                                                                                                 | concatentation with ","                             | list all M2s  |
+| should only be imported -- forbidden        | Violator2 imports Violator1. Violator2 imports Subviolator1                                                                                                    | (submodule of M1)                                   | -             |
+| should only be imported -- no import        | LAYER M1 is not imported by LAYER M2, LAYER M3                                                                                                                 | concatentation with ","                             | -             |
+| should not be imported                      | Violator2 imports Violator1. Violator3 imports Subviolator1.                                                                                                   | (submodules of M1, M2, M3)                          | -             |
+| should be imported except                   | LAYER M1 is not imported by any that is not LAYER M2, LAYER M3                                                                                                 | concatentation with ","                             | list all M2s  |
+| should only be imported except -- forbidden | Violator2 imports Violator1. Subviolator2 imports Subviolator1. Violator3 imports Subviolator1.                                                                | (submodules of M1, M2, M3)                          | -             |
+| should only be imported except -- no import | LAYER M1 is not imported by any that is not LAYER M2, LAYER M3                                                                                                 | concatentation with ","                             | list all M2s  |
+| should only be imported except -- both      | Violator2 imports Violator1. Subviolator2 imports Subviolator1. Violator3 imports Subviolator1. LAYER M1 is not imported by any that is not LAYER M2, LAYER M3 | (submodules of M1, M2, M3); concatentation with "," | list all M2s  |
+| should not be imported except               | Violator2 imports Violator1. Violator3 imports Subviolator1                                                                                                    | (submodule of M1)                                   | -             |
+
diff --git a/src/pytestarch/query_language/base_language.py b/src/pytestarch/query_language/base_language.py
index d060dfc..d9b5bc3 100644
--- a/src/pytestarch/query_language/base_language.py
+++ b/src/pytestarch/query_language/base_language.py
@@ -5,9 +5,6 @@
 from typing import Generic, List, TypeVar, Union
 
 from pytestarch import EvaluableArchitecture
-from pytestarch.rule_assessment.rule_check.behavior_requirement import (
-    BehaviorRequirement,
-)
 
 
 class FileRule(ABC):
@@ -57,24 +54,31 @@ def assert_applies(self, evaluable: EvaluableArchitecture) -> None:
         raise NotImplementedError()
 
 
-class BehaviorSpecification(ABC):
+U = TypeVar("U", bound="RelationshipSpecification")
+
+
+class BehaviorBaseSpecification(Generic[U], ABC):
     """Offers functionality to specify whether dependencies are expected or not."""
 
     @abstractmethod
-    def should(self) -> DependencySpecification:
+    def should(self) -> U:
         pass
 
     @abstractmethod
-    def should_only(self) -> DependencySpecification:
+    def should_only(self) -> U:
         pass
 
     @abstractmethod
-    def should_not(self) -> DependencySpecification:
+    def should_not(self) -> U:
         pass
 
 
+class BehaviorSpecification(BehaviorBaseSpecification["DependencySpecification"], ABC):
+    pass
+
+
 ModuleSpecificationSuccessor = TypeVar(
-    "ModuleSpecificationSuccessor", BehaviorRequirement, RuleApplier
+    "ModuleSpecificationSuccessor", BehaviorBaseSpecification, RuleApplier
 )
 
 
@@ -86,16 +90,19 @@ class ModuleSpecification(Generic[ModuleSpecificationSuccessor], ABC):
     def are_sub_modules_of(
         self, modules: Union[str, List[str]]
     ) -> ModuleSpecificationSuccessor:
+        """If multiple rule subjects are specified, this has the same effect as defining a rule per rule subject."""
         pass
 
     @abstractmethod
     def are_named(self, names: Union[str, List[str]]) -> ModuleSpecificationSuccessor:
+        """If multiple rule subjects are specified, this has the same effect as defining a rule per rule subject."""
         pass
 
     @abstractmethod
     def have_name_containing(
         self, partial_name: Union[str, List[str]]
     ) -> ModuleSpecificationSuccessor:
+        """If multiple rule subjects are specified, this has the same effect as defining a rule per rule subject."""
         pass
 
 
@@ -107,7 +114,13 @@ class RuleSubject(ModuleSpecification[BehaviorSpecification], ABC):
     pass
 
 
-class DependencySpecification(ABC):
+class RelationshipSpecification(ABC):
+    """Base class for different types of relationships between modules, layers, etc."""
+
+    pass
+
+
+class DependencySpecification(RelationshipSpecification, ABC):
     """Offers functionality to specify which kind of dependencies are expected."""
 
     @abstractmethod
@@ -141,3 +154,101 @@ class RuleBase(ABC):
     @abstractmethod
     def modules_that(self) -> RuleSubject:
         pass
+
+
+class LayerDefinition(ABC):
+    """Offers functionality to define which modules a layer contains. Can either be a single
+    or multiple modules."""
+
+    @abstractmethod
+    def containing_modules(
+        self, modules: Union[str, List[str]]
+    ) -> Union[LayerName, BaseLayeredArchitecture]:
+        """If a module is defined as belonging to layer X, then its submodules are also assumed to be part of layer X."""
+        pass
+
+
+class LayerName(ABC):
+    """Offers functionality to specify the name of a yet to be defined layer."""
+
+    @abstractmethod
+    def layer(self, name: str) -> LayerDefinition:
+        pass
+
+
+class BaseLayeredArchitecture(ABC):
+    pass
+
+
+class AccessSpecification(RelationshipSpecification, ABC):
+    """Offers functionality to specify which kind of imports between layers in a layer architecture are expected.
+    Access is defined as the layer equivalent of import between modules, e.g. if layer L consists of module X, and
+    layer M of module Y, layer L accesses layer M if module X import module Y."""
+
+    @abstractmethod
+    def access_layers_that(self) -> LayerRuleObject:
+        pass
+
+    @abstractmethod
+    def be_accessed_by_layers_that(self) -> LayerRuleObject:
+        pass
+
+    @abstractmethod
+    def access_layers_except_layers_that(self) -> LayerRuleObject:
+        pass
+
+    @abstractmethod
+    def be_accessed_by_layers_except_layers_that(self) -> LayerRuleObject:
+        pass
+
+    @abstractmethod
+    def access_any_layer(self) -> RuleApplier:
+        pass
+
+    @abstractmethod
+    def be_accessed_by_any_layer(self) -> RuleApplier:
+        pass
+
+
+class LayerBehaviorSpecification(BehaviorBaseSpecification["AccessSpecification"], ABC):
+    pass
+
+
+LayerSpecificationSuccessor = TypeVar(
+    "LayerSpecificationSuccessor", LayerBehaviorSpecification, RuleApplier
+)
+
+InputTypes = TypeVar("InputTypes", str, Union[str, List[str]])
+
+
+class LayerSpecification(Generic[LayerSpecificationSuccessor, InputTypes], ABC):
+    """Offers functionality to specify detail information about Layer Rule Subjects or Objects."""
+
+    @abstractmethod
+    def are_named(self, names: InputTypes) -> LayerSpecificationSuccessor:
+        pass
+
+
+class LayerRuleObject(LayerSpecification[RuleApplier, Union[str, List[str]]], ABC):
+    pass
+
+
+class LayerRuleSubject(LayerSpecification[LayerBehaviorSpecification, str], ABC):
+    pass
+
+
+class LayerBase(ABC):
+    """Offers an entry point to specifying detailed information about Layer Rule
+    Subjects."""
+
+    @abstractmethod
+    def layers_that(self) -> LayerRuleSubject:
+        pass
+
+
+class LayerRuleBase(ABC):
+    """Entry point to defining a rule based on a layered architecture."""
+
+    @abstractmethod
+    def based_on(self, architecture: BaseLayeredArchitecture) -> LayerBase:
+        pass
diff --git a/src/pytestarch/query_language/layered_architecture_rule.py b/src/pytestarch/query_language/layered_architecture_rule.py
new file mode 100644
index 0000000..57873e8
--- /dev/null
+++ b/src/pytestarch/query_language/layered_architecture_rule.py
@@ -0,0 +1,205 @@
+from __future__ import annotations
+
+from functools import partial
+from typing import List, Type, Union
+
+from pytestarch import EvaluableArchitecture, Rule
+from pytestarch.eval_structure.evaluable_architecture import LayerMapping, Module
+from pytestarch.query_language.base_language import (
+    AccessSpecification,
+    BaseLayeredArchitecture,
+    LayerBase,
+    LayerBehaviorSpecification,
+    LayerDefinition,
+    LayerName,
+    LayerRuleBase,
+    LayerRuleObject,
+    LayerRuleSubject,
+    LayerSpecification,
+    RuleApplier,
+)
+from pytestarch.query_language.exceptions import ImproperlyConfigured
+from pytestarch.rule_assessment.rule_check.rule_matcher import (
+    LayerRuleMatcher,
+    RuleMatcher,
+)
+
+
+class LayeredArchitecture(BaseLayeredArchitecture, LayerName, LayerDefinition):
+    """Can be used to define layers within an architecture that are each comprised of modules.
+    Note that if a module X is specified as belonging to layer L, all of its submodules are assumed to be part of
+    layer L as well."""
+
+    def __init__(self) -> None:
+        self._modules_by_layer_name = {}
+
+    @property
+    def layer_mapping(self) -> LayerMapping:
+        return LayerMapping(self._modules_by_layer_name)
+
+    def layer(self, name: str) -> LayerDefinition:
+        underspecified_layers = self._get_layers_without_modules()
+
+        if underspecified_layers:
+            raise ImproperlyConfigured(
+                f'Specify the modules of layer(s) {", ".join(underspecified_layers)} first.'
+            )
+
+        if name in self._modules_by_layer_name:
+            raise ImproperlyConfigured(f"Layer {name} already exists.")
+
+        self._modules_by_layer_name[name] = []
+
+        return self
+
+    def _get_layers_without_modules(self) -> List[str]:
+        underspecified_layers = [
+            layer
+            for layer, modules in self._modules_by_layer_name.items()
+            if modules == []
+        ]
+        return underspecified_layers
+
+    def containing_modules(
+        self, modules: Union[str, List[str]]
+    ) -> Union[LayerName, LayeredArchitecture]:
+        layers_without_modules = self._get_layers_without_modules()
+        if not layers_without_modules or len(layers_without_modules) > 1:
+            raise ImproperlyConfigured(
+                "Specify layer name before specifying its modules."
+            )
+
+        modules_list = modules if isinstance(modules, list) else [modules]
+
+        module_set = set(modules)
+        existing_modules = set(
+            [
+                value.name
+                for values in self._modules_by_layer_name.values()
+                for value in values
+            ]
+        )
+
+        duplicates = module_set.intersection(existing_modules)
+
+        if duplicates:
+            raise ImproperlyConfigured(
+                f'Module(s) {", ".join(duplicates)} already assigned to a layer.'
+            )
+
+        self._modules_by_layer_name[
+            layers_without_modules[0]
+        ] = self._to_module_objects(modules_list)
+
+        return self
+
+    def __str__(self) -> str:
+        layers = [
+            f"Layer {layer}: [{', '.join(map(lambda m: m.name, modules))}]"
+            for layer, modules in self._modules_by_layer_name.items()
+        ]
+        return f'Layered Architecture: {"; ".join(layers)}'
+
+    def __getitem__(self, layer: str) -> List[Module]:
+        return self._modules_by_layer_name[layer]
+
+    def _to_module_objects(self, modules: List[str]) -> List[Module]:
+        return [Module(name=module) for module in modules]
+
+
+class LayerRule(
+    RuleApplier,
+    LayerRuleBase,
+    LayerRuleSubject,
+    LayerRuleObject,
+    AccessSpecification,
+    LayerBehaviorSpecification,
+    LayerSpecification,
+    LayerBase,
+):
+    """Represents an architectural rule of the form
+    Layer X [verb, such as 'should'] [access type, such as 'be accessed by'] Layer Y
+    The modules that each layer contains are to be specified via a LayeredArchitecture object.
+    """
+
+    def __init__(
+        self, rule_matcher_class: Type[RuleMatcher] = LayerRuleMatcher
+    ) -> None:
+        self._rule = None
+        self._architecture = None
+        self._rule_matcher_class = rule_matcher_class
+
+    def based_on(self, architecture: BaseLayeredArchitecture) -> LayerBase:
+        if self._architecture is not None:
+            raise ImproperlyConfigured("Layered architecture already specified.")
+
+        self._architecture = architecture
+        return self
+
+    def layers_that(self) -> LayerRuleSubject:
+        if self._architecture is None:
+            raise ImproperlyConfigured(
+                "Specify a LayeredArchitecture before defining layer behavior."
+            )
+
+        self._rule = Rule(rule_matcher_class=partial(self._rule_matcher_class, layer_mapping=self._architecture.layer_mapping)).modules_that()  # type: ignore
+
+        return self
+
+    def are_named(self, layers: Union[str, List[str]]) -> LayerBehaviorSpecification:
+        if not self._rule.rule_subjects and isinstance(layers, list):
+            raise ImproperlyConfigured(
+                "Layer rule subjects cannot be specified in batch."
+            )
+
+        layers = self._listify(layers)
+        modules = self._get_all_modules_in_layers(layers)
+
+        self._rule = self._rule.are_named(modules)
+        return self
+
+    def should(self) -> LayerBehaviorSpecification:
+        self._rule = self._rule.should()
+        return self
+
+    def should_only(self) -> LayerBehaviorSpecification:
+        self._rule = self._rule.should_only()
+        return self
+
+    def should_not(self) -> LayerBehaviorSpecification:
+        self._rule = self._rule.should_not()
+        return self
+
+    def access_layers_that(self) -> LayerRuleObject:
+        self._rule = self._rule.import_modules_that()
+        return self
+
+    def be_accessed_by_layers_that(self) -> LayerRuleObject:
+        self._rule = self._rule.be_imported_by_modules_that()
+        return self
+
+    def access_layers_except_layers_that(self) -> LayerRuleObject:
+        self._rule = self._rule.import_modules_except_modules_that()
+        return self
+
+    def be_accessed_by_layers_except_layers_that(self) -> LayerRuleObject:
+        self._rule = self._rule.be_imported_by_modules_except_modules_that()
+        return self
+
+    def access_any_layer(self) -> RuleApplier:
+        self._rule = self._rule.import_anything()
+        return self
+
+    def be_accessed_by_any_layer(self) -> RuleApplier:
+        self._rule = self._rule.be_imported_by_anything()
+        return self
+
+    def assert_applies(self, evaluable: EvaluableArchitecture) -> None:
+        self._rule.assert_applies(evaluable)
+
+    @classmethod
+    def _listify(cls, layers: Union[str, List[str]]) -> List[str]:
+        return layers if isinstance(layers, List) else [layers]
+
+    def _get_all_modules_in_layers(self, layers: List[str]) -> List[str]:
+        return [module.name for layer in layers for module in self._architecture[layer]]
diff --git a/src/pytestarch/query_language/rule.py b/src/pytestarch/query_language/rule.py
index 0cfc64d..a5d3695 100644
--- a/src/pytestarch/query_language/rule.py
+++ b/src/pytestarch/query_language/rule.py
@@ -1,7 +1,7 @@
 from __future__ import annotations
 
 from dataclasses import dataclass, replace
-from typing import Callable, List, Optional, Type, Union
+from typing import Callable, List, Optional, Union
 
 from pytestarch import EvaluableArchitecture
 from pytestarch.eval_structure.evaluable_architecture import Module
@@ -49,7 +49,10 @@ class Rule(
     """
 
     def __init__(
-        self, rule_matcher_class: Type[RuleMatcher] = DefaultRuleMatcher
+        self,
+        rule_matcher_class: Callable[
+            [ModuleRequirement, BehaviorRequirement], RuleMatcher
+        ] = DefaultRuleMatcher,
     ) -> None:
         self._rule_matcher_class = rule_matcher_class
         self._modules_to_check_to_be_specified_next = None
@@ -256,9 +259,42 @@ def _convert_aliases(cls, configuration: RuleConfiguration) -> RuleConfiguration
 
         # should not import anything is equivalent to should not import except itself
         # should not be imported by anything is equivalent to should not be imported by anything except itself
+
+        modules_to_check_without_parent_and_submodule_combinations = (
+            cls._get_modules_to_check_without_parent_and_submodule_combinations(
+                configuration,
+            )
+        )
+
         return replace(
             configuration,
             rule_object_anything=False,
-            modules_to_check_against=configuration.modules_to_check,
+            modules_to_check=modules_to_check_without_parent_and_submodule_combinations,
+            modules_to_check_against=modules_to_check_without_parent_and_submodule_combinations,
             except_present=True,
         )
+
+    @classmethod
+    def _get_modules_to_check_without_parent_and_submodule_combinations(
+        cls, configuration: RuleConfiguration
+    ) -> Optional[List[Module]]:
+        # if modules_to_check contain a module and its submodule, this can throw off the breadth first search conducted
+        # on the dependency graph - and also, this setup does not really make sense
+        if configuration.modules_to_check is None:
+            return None
+
+        module_names = sorted(
+            map(lambda module: module.name, configuration.modules_to_check)
+        )
+
+        result = []
+        for module in configuration.modules_to_check:
+            parent_module_found = False
+            for module_name in module_names:
+                if module_name in module.name and module.name != module_name:
+                    parent_module_found = True
+
+            if not parent_module_found:
+                result.append(module)
+
+        return result
diff --git a/src/pytestarch/rule_assessment/error_message/message_generator.py b/src/pytestarch/rule_assessment/error_message/message_generator.py
index 2de636b..d4988fa 100644
--- a/src/pytestarch/rule_assessment/error_message/message_generator.py
+++ b/src/pytestarch/rule_assessment/error_message/message_generator.py
@@ -2,13 +2,21 @@
 
 from __future__ import annotations
 
+from abc import ABC, abstractmethod
 from collections import defaultdict
 from dataclasses import dataclass
 from typing import Dict, List, Optional, Set, Tuple
 
-from pytestarch.eval_structure.evaluable_architecture import Module, StrictDependency
+from pytestarch.eval_structure.evaluable_architecture import (
+    LayerMapping,
+    Module,
+    StrictDependency,
+)
 from pytestarch.rule_assessment.rule_check.rule_violations import RuleViolations
 
+LAYER_FIRST_LETTER = "l"
+LAYER_FIRST_LETTER_CAPITAL = "L"
+
 IMPORT = "import"
 IMPORTED_BY = "imported by"
 
@@ -16,6 +24,7 @@
 
 RULE_OBJECT_IS_SUBMODULE_MARKER = "a sub module of "
 ANY_MODULE_THAT_IS_NOT = "any module that is not "
+ANY_LAYER_THAT_IS_NOT = "any layer that is not "
 
 
 @dataclass
@@ -39,27 +48,8 @@ class RuleViolatedMessage:
 )
 
 
-class RuleViolationMessageGenerator:
-    """Generates a message for each violated rule."""
-
-    def __init__(self, import_rule: bool) -> None:
-        """
-        Args:
-            import_rule: True if the underlying rule is a "import" instead of an "is imported" rule
-        """
-        self._import_rule = import_rule
-
-        self._base_verb = IMPORT if self._import_rule else IMPORTED_BY
-
+class RuleViolationMessageBaseGenerator(ABC):
     def create_rule_violation_message(self, rule_violations: RuleViolations) -> str:
-        """Create a message about all rule violations.
-
-        Args:
-            rule_violations: to convert to human-readable format
-
-        Returns:
-            message containing all individual rule violation messages
-        """
         return "\n".join(self.create_rule_violation_messages(rule_violations))
 
     def create_rule_violation_messages(
@@ -118,6 +108,55 @@ def _extend(
         if new_messages is not None:
             messages.extend(new_messages)
 
+    @abstractmethod
+    def _create_should_import_violated_messages(
+        self, rule_violations: RuleViolations
+    ) -> List[RuleViolatedMessage]:
+        pass
+
+    @abstractmethod
+    def _create_should_only_import_violated_messages(
+        self, rule_violations: RuleViolations
+    ) -> List[RuleViolatedMessage]:
+        pass
+
+    @abstractmethod
+    def _create_should_not_import_violated_messages(
+        self, rule_violations: RuleViolations
+    ) -> List[RuleViolatedMessage]:
+        pass
+
+    @abstractmethod
+    def _create_should_import_except_violated_messages(
+        self, rule_violations: RuleViolations
+    ) -> List[RuleViolatedMessage]:
+        pass
+
+    @abstractmethod
+    def _create_should_only_import_except_violated_messages(
+        self, rule_violations: RuleViolations
+    ) -> List[RuleViolatedMessage]:
+        pass
+
+    @abstractmethod
+    def _create_should_not_import_except_violated_messages(
+        self, rule_violations: RuleViolations
+    ) -> List[RuleViolatedMessage]:
+        pass
+
+
+class RuleViolationMessageGenerator(RuleViolationMessageBaseGenerator):
+    """Generates a user-friendly error message for each violated rule defined on modules (as compared to layers)."""
+
+    def __init__(self, import_rule: bool) -> None:
+        """
+        Args:
+            import_rule: True if the underlying rule is an "import" instead of an "is imported" rule
+        """
+        self._import_rule = import_rule
+
+        self._base_verb = IMPORT if self._import_rule else IMPORTED_BY
+
     def _create_should_import_violated_messages(
         self, rule_violations: RuleViolations
     ) -> List[RuleViolatedMessage]:
@@ -145,17 +184,26 @@ def _create_no_import_between_original_subject_and_objects_message(
             for rule_object in rule_objects_for_rule_subject[rule_subject]:
                 rule_objects.append(self._get_rule_object(rule_object))
 
-            combined_rule_object = ", ".join(rule_objects)
-            if combined_rule_object:
-                rule_subject_formatted = self._get_rule_subject_formatted(rule_subject)
-                messages.append(
-                    RuleViolatedMessage(
-                        rule_subject_formatted, rule_verb, combined_rule_object
-                    )
-                )
+            rule_subject_formatted = self._get_rule_subject_formatted(rule_subject)
+            self._add_combined_rule_objects(
+                messages, rule_objects, rule_subject_formatted, rule_verb
+            )
 
         return messages
 
+    def _add_combined_rule_objects(
+        self,
+        messages: List[RuleViolatedMessage],
+        rule_objects: List[str],
+        rule_subject: str,
+        rule_verb: str,
+    ) -> None:
+        combined_rule_object = ", ".join(rule_objects)
+        if combined_rule_object:
+            messages.append(
+                RuleViolatedMessage(rule_subject, rule_verb, combined_rule_object)
+            )
+
     def _get_violating_rule_subjects_and_objects(
         self, rule_violation_dependency_names: List[StrictDependency]
     ) -> Tuple[Dict[Module, List[Module]], Set[Module]]:
@@ -267,17 +315,28 @@ def _create_no_import_other_than_between_original_subject_and_objects_message(
                     self._get_rule_object(rule_object),
                 )
 
-            if rule_objects:
-                combined_rule_object = ANY_MODULE_THAT_IS_NOT + ", ".join(rule_objects)
-                rule_subject_formatted = self._get_rule_subject_formatted(rule_subject)
-                messages.append(
-                    RuleViolatedMessage(
-                        rule_subject_formatted, rule_verb, combined_rule_object
-                    )
-                )
+            rule_subject_formatted = self._get_rule_subject_formatted(rule_subject)
+            self._add_combined_any_rule_objects(
+                messages, rule_objects, rule_subject_formatted, rule_verb
+            )
 
         return messages
 
+    def _add_combined_any_rule_objects(
+        self,
+        messages: List[RuleViolatedMessage],
+        rule_objects: List[str],
+        rule_subject: str,
+        rule_verb: str,
+        rule_object_type: str = ANY_MODULE_THAT_IS_NOT,
+    ) -> None:
+        if rule_objects:
+            combined_rule_object = rule_object_type + ", ".join(rule_objects)
+
+            messages.append(
+                RuleViolatedMessage(rule_subject, rule_verb, combined_rule_object)
+            )
+
     def _create_should_only_import_except_violated_messages(
         self, rule_violations: RuleViolations
     ) -> List[RuleViolatedMessage]:
@@ -370,9 +429,16 @@ def _get_rule_subject_and_object_of_dependency(
         rule_subject_name = self._get_module_name(dependency[0])
         rule_object_name = self._get_module_name(dependency[1])
 
-        return self._get_quoted_name(rule_subject_name), self._get_quoted_name(
+        rule_subject_name_formatted = self._get_quoted_name(rule_subject_name)
+        rule_object_name_formatted = self._get_quoted_name(rule_object_name)
+
+        complete_rule_subject_name = rule_subject_name_formatted + self._get_suffix(
+            rule_subject_name
+        )
+        complete_rule_object_name = rule_object_name_formatted + self._get_suffix(
             rule_object_name
         )
+        return complete_rule_subject_name, complete_rule_object_name
 
     def _get_verb_suffix(self, subject_singular: bool) -> str:
         if not subject_singular:
@@ -383,3 +449,128 @@ def _get_verb_suffix(self, subject_singular: bool) -> str:
 
         # "is imported by" does not need an additional 's'
         return ""
+
+    def _get_suffix(self, xbject: str) -> str:
+        # no suffix needed here
+        return ""
+
+
+class LayerRuleViolationMessageGenerator(RuleViolationMessageGenerator):
+    """Generates a user-friendly error message for each violated rule defined on layers (as compared to modules).
+    These messages differ slightly from messages generated for rules based on modules:
+    1) For absent, but expected dependencies, the verb used mirrors the verb used in the rule itself ("access" instead of "import").
+    2) For unwanted dependencies, the regular syntax is used, but with an added hint on which layers the problematic modules
+     belong to, such as "module X (layer L) imports module Y (layer M)". If a module does not belong to any layer, (no layer)
+     will be used.
+    """
+
+    def __init__(self, import_rule: bool, layer_mapping: LayerMapping) -> None:
+        super().__init__(import_rule)
+        self._layer_mapping = layer_mapping
+
+    def _get_suffix(self, xbject: str) -> str:
+        layer = self._layer_mapping.get_layer_for_module_name(xbject)
+
+        if layer is None:
+            return " (no layer)"
+        else:
+            return f" (layer {self._get_quoted_name(layer)})"
+
+    def _create_no_import_between_original_subject_and_objects_message(
+        self, rule_violations: List[StrictDependency]
+    ) -> List[RuleViolatedMessage]:
+        messages = []
+
+        (
+            rule_object_layers_for_rule_subject_layer,
+            violating_rule_subject_layers,
+        ) = self._get_violating_rule_subject_and_objects_layers(rule_violations)
+
+        for rule_subject_layer in violating_rule_subject_layers:
+            verb_prefix = self._get_verb_prefix(
+                negated=True,
+                subject_singular=True,  # only singular rule subjects allowed for layer rules
+            )
+            rule_verb = self._concatenate_verb(prefix=verb_prefix, verb=self._base_verb)
+
+            rule_object_layers = []
+            for rule_object_layer in sorted(
+                rule_object_layers_for_rule_subject_layer[rule_subject_layer]
+            ):
+                rule_object_layer_formatted = self._prepend_prefix(
+                    self._get_quoted_name(rule_object_layer), False
+                )
+                rule_object_layers.append(rule_object_layer_formatted)
+
+            rule_subject_layer_formatted = self._prepend_prefix(
+                self._get_quoted_name(rule_subject_layer)
+            )
+            self._add_combined_rule_objects(
+                messages, rule_object_layers, rule_subject_layer_formatted, rule_verb
+            )
+
+        return messages
+
+    def _get_violating_rule_subject_and_objects_layers(
+        self, rule_violation_dependency_names: List[StrictDependency]
+    ) -> Tuple[Dict[str, Set[str]], Set[str]]:
+        violating_rule_subject_layers = set()
+        rule_object_layers_for_rule_subject_layer = defaultdict(set)
+
+        for rule_subject, rule_object in rule_violation_dependency_names:
+            rule_subject_layer = self._layer_mapping.get_layer(rule_subject)
+            rule_object_layer = self._layer_mapping.get_layer(rule_object)
+
+            violating_rule_subject_layers.add(rule_subject_layer)
+            rule_object_layers_for_rule_subject_layer[rule_subject_layer].add(
+                rule_object_layer
+            )
+
+        return rule_object_layers_for_rule_subject_layer, violating_rule_subject_layers
+
+    def _create_no_import_other_than_between_original_subject_and_objects_message(
+        self, rule_violations: List[StrictDependency]
+    ) -> List[RuleViolatedMessage]:
+        messages = []
+
+        (
+            rule_object_layers_for_rule_subject,
+            violating_rule_subject_layers,
+        ) = self._get_violating_rule_subject_and_objects_layers(rule_violations)
+
+        for rule_subject_layer in violating_rule_subject_layers:
+            verb_prefix = self._get_verb_prefix(
+                negated=True,
+                subject_singular=True,  # only singular rule subjects allowed for layer rules
+            )
+            rule_verb = self._concatenate_verb(prefix=verb_prefix, verb=self._base_verb)
+
+            rule_objects = []
+            for rule_object_layer in sorted(
+                rule_object_layers_for_rule_subject[rule_subject_layer]
+            ):
+                rule_object_layer_formatted = self._prepend_prefix(
+                    self._get_quoted_name(rule_object_layer), False
+                )
+                rule_objects.append(rule_object_layer_formatted)
+
+            rule_subject_layer_formatted = self._prepend_prefix(
+                self._get_quoted_name(rule_subject_layer)
+            )
+            self._add_combined_any_rule_objects(
+                messages,
+                rule_objects,
+                rule_subject_layer_formatted,
+                rule_verb,
+                rule_object_type=ANY_LAYER_THAT_IS_NOT,
+            )
+
+        return messages
+
+    def _prepend_prefix(self, xbject: str, capital: bool = True) -> str:
+        first_letter = LAYER_FIRST_LETTER_CAPITAL
+
+        if not capital:
+            first_letter = LAYER_FIRST_LETTER
+
+        return f"{first_letter}ayer {xbject}"
diff --git a/src/pytestarch/rule_assessment/rule_check/behavior_requirement.py b/src/pytestarch/rule_assessment/rule_check/behavior_requirement.py
index 48f6d62..2994e07 100644
--- a/src/pytestarch/rule_assessment/rule_check/behavior_requirement.py
+++ b/src/pytestarch/rule_assessment/rule_check/behavior_requirement.py
@@ -17,27 +17,27 @@ def __init__(
         self.should_only = should_only
         self.should_not = should_not
 
-        self._behavior_exception = behavior_exception
+        self.behavior_exception = behavior_exception
 
         self._validate()
 
     @property
     def explicitly_requested_dependency_required(self) -> bool:
         """Returns True if the dependency between two specific modules is required."""
-        return (self.should or self.should_only) and not self._behavior_exception
+        return (self.should or self.should_only) and not self.behavior_exception
 
     @property
     def not_explicitly_requested_dependency_required(self) -> bool:
         """Returns True if a dependency between a specific module and
         any other modules is required. Which other modules are required
         is specified by the module requirement."""
-        return self._behavior_exception and (self.should or self.should_only)
+        return self.behavior_exception and (self.should or self.should_only)
 
     @property
     def explicitly_requested_dependency_not_allowed(self) -> bool:
         """Returns True if the dependency between two specific modules is not allowed."""
-        return (self.should_not and not self._behavior_exception) or (
-            self.should_only and self._behavior_exception
+        return (self.should_not and not self.behavior_exception) or (
+            self.should_only and self.behavior_exception
         )
 
     @property
@@ -45,8 +45,8 @@ def not_explicitly_requested_dependency_not_allowed(self) -> bool:
         """Returns True if no dependency between a specific module
         and a set of other modules is not allowed. Which other modules
         are not allowed is specified by the module requirement."""
-        return (self.should_not and self._behavior_exception) or (
-            self.should_only and not self._behavior_exception
+        return (self.should_not and self.behavior_exception) or (
+            self.should_only and not self.behavior_exception
         )
 
     def _validate(self) -> None:
diff --git a/src/pytestarch/rule_assessment/rule_check/layer_rule_violation_detector.py b/src/pytestarch/rule_assessment/rule_check/layer_rule_violation_detector.py
new file mode 100644
index 0000000..20cf831
--- /dev/null
+++ b/src/pytestarch/rule_assessment/rule_check/layer_rule_violation_detector.py
@@ -0,0 +1,298 @@
+from __future__ import annotations
+
+from collections import defaultdict
+from typing import Any, Dict, List, Optional
+
+from pytestarch.eval_structure.evaluable_architecture import (
+    ExplicitlyRequestedDependenciesByBaseModules,
+    Layer,
+    LayerMapping,
+    Module,
+    NotExplicitlyRequestedDependenciesByBaseModule,
+    StrictDependency,
+)
+from pytestarch.rule_assessment.rule_check.behavior_requirement import (
+    BehaviorRequirement,
+)
+from pytestarch.rule_assessment.rule_check.module_requirement import ModuleRequirement
+from pytestarch.rule_assessment.rule_check.rule_violation_detector import (
+    RuleViolationBaseDetector,
+)
+
+
+class LayerRuleViolationDetector(RuleViolationBaseDetector):
+    """Detects violation of a layer rule based on the rule and detected dependencies relevant to this rule.
+    Compared to the regular RuleViolationDetector, this detector is more lenient: For example, for a rule in the style
+    of "layer X should import layer Y", it is sufficient if only one module from layer X imports something from layer
+    Y. The regular detector would instead require all modules from layer X to import something from layer Y.
+    """
+
+    def __init__(
+        self,
+        module_requirement: ModuleRequirement,
+        behavior_requirement: BehaviorRequirement,
+        layer_mapping: LayerMapping,
+    ) -> None:
+        super().__init__(module_requirement, behavior_requirement)
+        self._layer_to_module_mapping = layer_mapping
+
+    def _should_not_requirement_violations(
+        self,
+        explicitly_requested_dependencies_should_not_be_present: bool,
+        explicitly_requested_dependencies: Optional[
+            ExplicitlyRequestedDependenciesByBaseModules
+        ],
+    ) -> List[StrictDependency]:
+        if (
+            explicitly_requested_dependencies is None
+            or not explicitly_requested_dependencies_should_not_be_present
+        ):
+            return []
+
+        return self._get_realised_dependencies(explicitly_requested_dependencies)
+
+    def _should_requirement_violations(
+        self,
+        explicitly_requested_dependencies_should_be_present: bool,
+        explicitly_requested_dependencies: Optional[
+            ExplicitlyRequestedDependenciesByBaseModules
+        ],
+    ) -> List[StrictDependency]:
+        if (
+            explicitly_requested_dependencies is None
+            or not explicitly_requested_dependencies_should_be_present
+        ):
+            return []
+
+        return self._get_abstract_dependencies_without_any_realisations(
+            explicitly_requested_dependencies
+        )
+
+    def _should_only_requirement_violations_by_no_import(
+        self,
+        explicitly_requested_dependencies_and_no_other_should_be_present: bool,
+        explicitly_requested_dependencies: Optional[
+            ExplicitlyRequestedDependenciesByBaseModules
+        ],
+    ) -> List[StrictDependency]:
+        if (
+            explicitly_requested_dependencies is None
+            or not explicitly_requested_dependencies_and_no_other_should_be_present
+        ):
+            return []
+
+        return self._get_abstract_dependencies_without_any_realisations(
+            explicitly_requested_dependencies
+        )
+
+    def _should_only_requirement_violations_by_not_explicitly_requested_dependency(
+        self,
+        explicitly_requested_dependencies_and_no_other_should_be_present: bool,
+        not_explicitly_requested_dependencies: Optional[
+            NotExplicitlyRequestedDependenciesByBaseModule
+        ],
+    ) -> List[StrictDependency]:
+        if (
+            not_explicitly_requested_dependencies is None
+            or not explicitly_requested_dependencies_and_no_other_should_be_present
+        ):
+            return []
+
+        return self._get_realised_dependencies(not_explicitly_requested_dependencies)
+
+    def _should_except_requirement_violations(
+        self,
+        at_least_one_not_explicitly_requested_dependency_should_be_present: bool,
+        not_explicitly_requested_dependencies: Optional[
+            NotExplicitlyRequestedDependenciesByBaseModule
+        ],
+    ) -> List[StrictDependency]:
+        if (
+            not_explicitly_requested_dependencies is None
+            or not at_least_one_not_explicitly_requested_dependency_should_be_present
+        ):
+            return []
+
+        return self._get_any_missing_dependencies_in_user_specified_order(
+            not_explicitly_requested_dependencies
+        )
+
+    def _should_only_except_requirement_violations_due_to_no_other_imports(
+        self,
+        explicitly_requested_dependency_should_not_but_others_should_be_present: bool,
+        not_explicitly_requested_dependencies: Optional[
+            NotExplicitlyRequestedDependenciesByBaseModule
+        ],
+    ) -> List[StrictDependency]:
+        if (
+            not_explicitly_requested_dependencies is None
+            or not explicitly_requested_dependency_should_not_but_others_should_be_present
+        ):
+            return []
+
+        return self._get_any_missing_dependencies_in_user_specified_order(
+            not_explicitly_requested_dependencies
+        )
+
+    def _should_only_except_requirement_violations_due_to_explicit_dependency_present(
+        self,
+        explicitly_requested_dependency_should_not_but_others_should_be_present: bool,
+        explicitly_requested_dependencies: Optional[
+            ExplicitlyRequestedDependenciesByBaseModules
+        ],
+    ) -> List[StrictDependency]:
+        if (
+            explicitly_requested_dependencies is None
+            or not explicitly_requested_dependency_should_not_but_others_should_be_present
+        ):
+            return []
+
+        return self._get_realised_dependencies(explicitly_requested_dependencies)
+
+    def _should_not_except_requirement_violations(
+        self,
+        not_explicitly_requested_dependencies_should_not_be_present: bool,
+        not_explicitly_requested_dependencies: Optional[
+            NotExplicitlyRequestedDependenciesByBaseModule
+        ],
+    ) -> List[StrictDependency]:
+        if (
+            not_explicitly_requested_dependencies is None
+            or not not_explicitly_requested_dependencies_should_not_be_present
+        ):
+            return []
+
+        return self._get_realised_dependencies(not_explicitly_requested_dependencies)
+
+    def _get_abstract_dependencies_without_any_realisations(
+        self,
+        explicitly_requested_dependencies: Dict[
+            StrictDependency, List[StrictDependency]
+        ],
+    ) -> List[StrictDependency]:
+        """If there is any explicitly requested dependency for each rule object, an empty list will be returned. If there is none at all for at least one rule object,
+        all dependencies will be returned."""
+        explicitly_requested_dependencies_by_layers = (
+            self._group_explicitly_requested_dependencies_by_layers(
+                explicitly_requested_dependencies
+            )
+        )
+
+        result = []
+
+        for layer in self._layer_to_module_mapping.all_layers:
+            explicitly_requested_dependencies_for_layer = (
+                explicitly_requested_dependencies_by_layers[layer]
+            )
+
+            if not explicitly_requested_dependencies_for_layer:
+                continue
+
+            if any(
+                len(concrete_dependencies) > 0
+                for concrete_dependencies in explicitly_requested_dependencies_for_layer.values()
+            ):
+                continue
+
+            result.extend(
+                [
+                    self._get_rule_subject_and_object_in_user_specified_order(
+                        dependency
+                    )
+                    for dependency in explicitly_requested_dependencies_for_layer.keys()
+                ]
+            )
+        return result
+
+    def _get_any_missing_dependencies_in_user_specified_order(
+        self,
+        not_explicitly_requested_dependencies: NotExplicitlyRequestedDependenciesByBaseModule,
+    ) -> List[StrictDependency]:
+        """If no not explicitly requested dependency is present, all possible not present dependencies will be
+        returned. If there is one dependency per rule subject, an empty list will be returned.
+        """
+        dependencies = []
+
+        if any(
+            len(not_explicitly_requested_dependencies_of_module) > 0
+            for not_explicitly_requested_dependencies_of_module in not_explicitly_requested_dependencies.values()
+        ):
+            return []
+
+        self._append_missing_dependencies(
+            dependencies, not_explicitly_requested_dependencies
+        )
+
+        return [
+            self._get_rule_subject_and_object_in_user_specified_order(dependency)
+            for dependency in dependencies
+        ]
+
+    def _append_missing_dependencies(
+        self,
+        dependencies: List[StrictDependency],
+        not_explicitly_requested_dependencies: NotExplicitlyRequestedDependenciesByBaseModule,
+    ) -> None:
+        for (
+            module_with_missing_dependencies
+        ) in not_explicitly_requested_dependencies.keys():
+            if self._module_requirement.rule_specified_with_importer_as_rule_subject:
+                for (
+                    other_module
+                ) in self._module_requirement.importees_as_specified_by_user:
+                    dependencies.append(
+                        (module_with_missing_dependencies, other_module)
+                    )
+            else:
+                for (
+                    other_module
+                ) in self._module_requirement.importees_as_specified_by_user:
+                    dependencies.append(
+                        (other_module, module_with_missing_dependencies)
+                    )
+
+    def _group_explicitly_requested_dependencies_by_layers(
+        self,
+        explicitly_requested_dependencies: ExplicitlyRequestedDependenciesByBaseModules,
+    ) -> Dict[Layer, ExplicitlyRequestedDependenciesByBaseModules]:
+        result = defaultdict(dict)
+
+        for (
+            abstract_dependency,
+            concrete_dependencies,
+        ) in explicitly_requested_dependencies.items():
+            relevant_module_for_layer = self._get_module_relevant_for_layer(
+                abstract_dependency
+            )
+            layer = self._get_layer_for_module(relevant_module_for_layer)
+
+            result[layer][abstract_dependency] = concrete_dependencies
+
+        return result
+
+    def _get_module_relevant_for_layer(self, dependency: StrictDependency) -> Module:
+        if self._module_requirement.rule_specified_with_importer_as_rule_subject:
+            return dependency[1]
+
+        return dependency[0]
+
+    def _get_layer_for_module(self, module: Module) -> Layer:
+        return self._layer_to_module_mapping.get_layer(module)
+
+    def _get_realised_dependencies(
+        self, explicitly_requested_dependencies: Dict[Any, List[StrictDependency]]
+    ) -> List[StrictDependency]:
+        """Removes all dependencies between modules of the same layer, as these are one logical unit."""
+        violating_dependencies = super()._get_realised_dependencies(
+            explicitly_requested_dependencies
+        )
+
+        violating_dependencies_in_different_layers = []
+        for dependency in violating_dependencies:
+            rule_subject_layer = self._layer_to_module_mapping.get_layer(dependency[0])
+            rule_object_layer = self._layer_to_module_mapping.get_layer(dependency[1])
+
+            if rule_subject_layer != rule_object_layer:
+                violating_dependencies_in_different_layers.append(dependency)
+
+        return violating_dependencies_in_different_layers
diff --git a/src/pytestarch/rule_assessment/rule_check/module_requirement.py b/src/pytestarch/rule_assessment/rule_check/module_requirement.py
index 93c2a6a..c6e9fb0 100644
--- a/src/pytestarch/rule_assessment/rule_check/module_requirement.py
+++ b/src/pytestarch/rule_assessment/rule_check/module_requirement.py
@@ -12,7 +12,7 @@ def __init__(
         self,
         importers: List[Module],
         importees: List[Module],
-        flip_importer_and_importees: bool,
+        importer_specified_as_rule_subject: bool,
     ) -> None:
         self._importer_as_specified_by_user = importers
         self._importees_as_specified_by_user = importees
@@ -20,7 +20,7 @@ def __init__(
         self._importers = importers
         self._importees = importees
 
-        self._flip_importer_and_importees = flip_importer_and_importees
+        self._importer_specified_as_rule_subject = importer_specified_as_rule_subject
 
         if self.rule_specified_with_importer_as_rule_object:
             self._importers, self._importees = (
@@ -47,9 +47,9 @@ def importees_as_specified_by_user(self) -> List[Module]:
     @property
     def rule_specified_with_importer_as_rule_object(self) -> bool:
         # True if rule is of format "is imported by"
-        return not self._flip_importer_and_importees
+        return not self._importer_specified_as_rule_subject
 
     @property
     def rule_specified_with_importer_as_rule_subject(self) -> bool:
-        # True if rule is of format "is imported by"
-        return self._flip_importer_and_importees
+        # True if rule is of format "imports"
+        return self._importer_specified_as_rule_subject
diff --git a/src/pytestarch/rule_assessment/rule_check/rule_matcher.py b/src/pytestarch/rule_assessment/rule_check/rule_matcher.py
index da94ea6..cab8c0a 100644
--- a/src/pytestarch/rule_assessment/rule_check/rule_matcher.py
+++ b/src/pytestarch/rule_assessment/rule_check/rule_matcher.py
@@ -6,16 +6,23 @@
 from pytestarch.eval_structure.evaluable_architecture import (
     EvaluableArchitecture,
     ExplicitlyRequestedDependenciesByBaseModules,
+    LayerMapping,
     NotExplicitlyRequestedDependenciesByBaseModule,
 )
 from pytestarch.rule_assessment.error_message.message_generator import (
+    LayerRuleViolationMessageGenerator,
+    RuleViolationMessageBaseGenerator,
     RuleViolationMessageGenerator,
 )
 from pytestarch.rule_assessment.rule_check.behavior_requirement import (
     BehaviorRequirement,
 )
+from pytestarch.rule_assessment.rule_check.layer_rule_violation_detector import (
+    LayerRuleViolationDetector,
+)
 from pytestarch.rule_assessment.rule_check.module_requirement import ModuleRequirement
 from pytestarch.rule_assessment.rule_check.rule_violation_detector import (
+    RuleViolationBaseDetector,
     RuleViolationDetector,
 )
 from pytestarch.rule_assessment.rule_check.rule_violations import RuleViolations
@@ -32,7 +39,6 @@ def __init__(
         self._module_requirement = module_requirement
         self._behavior_requirement = behavior_requirement
 
-    @abstractmethod
     def match(self, evaluable: EvaluableArchitecture) -> None:
         """
         Checks whether an expected behavior is exhibited by the EvaluableArchitecture.
@@ -43,11 +49,6 @@ def match(self, evaluable: EvaluableArchitecture) -> None:
         Raises:
             AssertionError
         """
-        raise NotImplementedError()
-
-
-class DefaultRuleMatcher(RuleMatcher):
-    def match(self, evaluable: EvaluableArchitecture) -> None:
         rule_violations = self._find_rule_violations(evaluable)
 
         if rule_violations:
@@ -63,19 +64,25 @@ def _find_rule_violations(self, evaluable: EvaluableArchitecture) -> RuleViolati
             self._get_not_explicitly_requested_dependencies(evaluable)
         )
 
-        return RuleViolationDetector(
-            self._module_requirement, self._behavior_requirement
-        ).get_rule_violation(
+        return self._get_rule_violation_detector().get_rule_violation(
             explicitly_requested_dependencies,
             not_explicitly_requested_dependencies,
         )
 
+    @abstractmethod
+    def _get_rule_violation_detector(self) -> RuleViolationBaseDetector:
+        pass
+
     def _create_rule_violation_message(self, rule_violations: RuleViolations) -> str:
-        message_generator = RuleViolationMessageGenerator(
-            self._module_requirement.rule_specified_with_importer_as_rule_subject,
-        )
+        message_generator = self._create_rule_violation_message_generator()
         return message_generator.create_rule_violation_message(rule_violations)
 
+    @abstractmethod
+    def _create_rule_violation_message_generator(
+        self,
+    ) -> RuleViolationMessageBaseGenerator:
+        pass
+
     def _get_not_explicitly_requested_dependencies(
         self, evaluable: EvaluableArchitecture
     ) -> Optional[NotExplicitlyRequestedDependenciesByBaseModule]:
@@ -113,3 +120,49 @@ def _get_explicitly_requested_dependencies(
             )
 
         return None
+
+
+class DefaultRuleMatcher(RuleMatcher):
+    """To be used for rules that operate on modules, such as "module X should not import module Y."""
+
+    def _get_rule_violation_detector(self) -> RuleViolationBaseDetector:
+        return RuleViolationDetector(
+            self._module_requirement, self._behavior_requirement
+        )
+
+    def _create_rule_violation_message_generator(self) -> RuleViolationMessageGenerator:
+        return RuleViolationMessageGenerator(
+            self._module_requirement.rule_specified_with_importer_as_rule_subject,
+        )
+
+
+class LayerRuleMatcher(RuleMatcher):
+    """To be used for rules that operate on layers, such as "layer X should not access layer Y.
+    These types of rules differ from the DefaultRuleMatcher in that detected dependencies are interpreted differently.
+    Example: Consider the rule 'modules X, Y should import module Z'. In the default case, this means that both X and Y
+    have to import Z for the rule to apply to the evaluable.
+    For the layer rule 'layer L containing modules X, Y should import layer M containing module Z' it is however
+    sufficient for either X or Y to import Z.
+    """
+
+    def __init__(
+        self,
+        module_requirement: ModuleRequirement,
+        behavior_requirement: BehaviorRequirement,
+        layer_mapping: LayerMapping,
+    ) -> None:
+        super().__init__(module_requirement, behavior_requirement)
+        self._layer_mapping = layer_mapping
+
+    def _get_rule_violation_detector(self) -> RuleViolationBaseDetector:
+        return LayerRuleViolationDetector(
+            self._module_requirement, self._behavior_requirement, self._layer_mapping
+        )
+
+    def _create_rule_violation_message_generator(
+        self,
+    ) -> LayerRuleViolationMessageGenerator:
+        return LayerRuleViolationMessageGenerator(
+            self._module_requirement.rule_specified_with_importer_as_rule_subject,
+            self._layer_mapping,
+        )
diff --git a/src/pytestarch/rule_assessment/rule_check/rule_violation_detector.py b/src/pytestarch/rule_assessment/rule_check/rule_violation_detector.py
index 7e75417..5d2af46 100644
--- a/src/pytestarch/rule_assessment/rule_check/rule_violation_detector.py
+++ b/src/pytestarch/rule_assessment/rule_check/rule_violation_detector.py
@@ -1,5 +1,7 @@
 from __future__ import annotations
 
+from abc import ABC, abstractmethod
+from dataclasses import dataclass
 from typing import Any, Dict, List, Optional
 
 from pytestarch.eval_structure.evaluable_architecture import (
@@ -14,19 +16,19 @@
 from pytestarch.rule_assessment.rule_check.rule_violations import RuleViolations
 
 
-class RuleViolationDetector:
-    """Based on the behavior requirement and the dependencies that have actually been (not) found in the graph,
-    this class decides whether any of the requirements have been violated and how.
+@dataclass
+class DependencyExpectation:
+    not_explicitly_requested_dependencies_should_not_be_present: bool
+    explicitly_requested_dependencies_should_not_be_present: bool
+    explicitly_requested_dependencies_and_no_other_should_be_present: bool
+    explicitly_requested_dependencies_should_not_but_others_should_be_present: bool
+    at_least_one_not_explicitly_requested_dependency_should_be_present: bool
+    explicitly_requested_dependencies_should_be_present: bool
 
-    ExplicitlyRequestedDependenciesByBaseModules follow this structure:
-    {(requested dependency, e.g. from A to B): [list of dependencies found that qualifiy as the requested dependency, e.g. A.a1 imports B, A.a2 imports B]}
 
-    NotExplicitlyRequestedDependenciesByBaseModule on the other hand are structured like this:
-    {Module for which not explicitly requested dependencies (either from or to this module) were found: [list of such dependencies]}.
-
-    The output is a rule violation object which for each type of rule contains a list of (rule subject, rule object) which violate the rule. These are ordered according
-    to the order the user used when specifying the rule, i.e. the rule subject is the original rule subject no matter if the rule is an import
-    or be imported rule.
+class RuleViolationBaseDetector(ABC):
+    """Base class for all classes that detect inconsistencies between the dependencies found within the evaluable
+    architecture and the module and behavior requirements.
     """
 
     def __init__(
@@ -53,66 +55,44 @@ def get_rule_violation(
         Returns:
             overview of all rule violations
         """
-        not_explicitly_requested_dependencies_should_not_be_present = (
-            self._behavior_requirement.should_not
-            and self._behavior_requirement._behavior_exception
-        )
-        explicitly_requested_dependencies_should_not_be_present = (
-            self._behavior_requirement.should_not
-            and not self._behavior_requirement._behavior_exception
-        )
-        explicitly_requested_dependencies_and_no_other_should_be_present = (
-            self._behavior_requirement.should_only
-            and not self._behavior_requirement._behavior_exception
-        )
-        explicitly_requested_dependencies_should_not_but_others_should_be_present = (
-            self._behavior_requirement.should_only
-            and self._behavior_requirement._behavior_exception
-        )
-        at_least_one_not_explicitly_requested_dependency_should_be_present = (
-            self._behavior_requirement.should
-            and self._behavior_requirement._behavior_exception
-        )
-        explicitly_requested_dependencies_should_be_present = (
-            self._behavior_requirement.should
-            and not self._behavior_requirement._behavior_exception
-        )
+        dependency_expectations = self._get_dependency_expectations()
 
         return RuleViolations(
             should_not_violations=self._should_not_requirement_violations(
-                explicitly_requested_dependencies_should_not_be_present,
+                dependency_expectations.explicitly_requested_dependencies_should_not_be_present,
                 explicitly_requested_dependencies,
             ),
             should_violations=self._should_requirement_violations(
-                explicitly_requested_dependencies_should_be_present,
+                dependency_expectations.explicitly_requested_dependencies_should_be_present,
                 explicitly_requested_dependencies,
             ),
             should_only_violations_by_no_import=self._should_only_requirement_violations_by_no_import(
-                explicitly_requested_dependencies_and_no_other_should_be_present,
+                dependency_expectations.explicitly_requested_dependencies_and_no_other_should_be_present,
                 explicitly_requested_dependencies,
             ),
             should_only_violations_by_forbidden_import=self._should_only_requirement_violations_by_not_explicitly_requested_dependency(
-                explicitly_requested_dependencies_and_no_other_should_be_present,
+                dependency_expectations.explicitly_requested_dependencies_and_no_other_should_be_present,
                 not_explicitly_requested_dependencies,
             ),
             should_except_violations=self._should_except_requirement_violations(
-                at_least_one_not_explicitly_requested_dependency_should_be_present,
+                dependency_expectations.at_least_one_not_explicitly_requested_dependency_should_be_present,
                 not_explicitly_requested_dependencies,
             ),
             should_only_except_violations_by_no_import=self._should_only_except_requirement_violations_due_to_no_other_imports(
-                explicitly_requested_dependencies_should_not_but_others_should_be_present,
+                dependency_expectations.explicitly_requested_dependencies_should_not_but_others_should_be_present,
                 not_explicitly_requested_dependencies,
             ),
             should_only_except_violations_by_forbidden_import=self._should_only_except_requirement_violations_due_to_explicit_dependency_present(
-                explicitly_requested_dependencies_should_not_but_others_should_be_present,
+                dependency_expectations.explicitly_requested_dependencies_should_not_but_others_should_be_present,
                 explicitly_requested_dependencies,
             ),
             should_not_except_violations=self._should_not_except_requirement_violations(
-                not_explicitly_requested_dependencies_should_not_be_present,
+                dependency_expectations.not_explicitly_requested_dependencies_should_not_be_present,
                 not_explicitly_requested_dependencies,
             ),
         )
 
+    @abstractmethod
     def _should_not_requirement_violations(
         self,
         explicitly_requested_dependencies_should_not_be_present: bool,
@@ -120,13 +100,77 @@ def _should_not_requirement_violations(
             ExplicitlyRequestedDependenciesByBaseModules
         ],
     ) -> List[StrictDependency]:
-        if (
-            explicitly_requested_dependencies is None
-            or not explicitly_requested_dependencies_should_not_be_present
-        ):
-            return []
+        pass
 
-        return self._get_realised_dependencies(explicitly_requested_dependencies)
+    @abstractmethod
+    def _should_requirement_violations(
+        self,
+        explicitly_requested_dependencies_should_be_present: bool,
+        explicitly_requested_dependencies: Optional[
+            ExplicitlyRequestedDependenciesByBaseModules
+        ],
+    ) -> List[StrictDependency]:
+        pass
+
+    @abstractmethod
+    def _should_only_requirement_violations_by_no_import(
+        self,
+        explicitly_requested_dependencies_and_no_other_should_be_present: bool,
+        explicitly_requested_dependencies: Optional[
+            ExplicitlyRequestedDependenciesByBaseModules
+        ],
+    ) -> List[StrictDependency]:
+        pass
+
+    @abstractmethod
+    def _should_only_requirement_violations_by_not_explicitly_requested_dependency(
+        self,
+        explicitly_requested_dependencies_and_no_other_should_be_present: bool,
+        not_explicitly_requested_dependencies: Optional[
+            NotExplicitlyRequestedDependenciesByBaseModule
+        ],
+    ) -> List[StrictDependency]:
+        pass
+
+    @abstractmethod
+    def _should_except_requirement_violations(
+        self,
+        at_least_one_not_explicitly_requested_dependency_should_be_present: bool,
+        not_explicitly_requested_dependencies: Optional[
+            NotExplicitlyRequestedDependenciesByBaseModule
+        ],
+    ) -> List[StrictDependency]:
+        pass
+
+    @abstractmethod
+    def _should_only_except_requirement_violations_due_to_no_other_imports(
+        self,
+        explicitly_requested_dependency_should_not_but_others_should_be_present: bool,
+        not_explicitly_requested_dependencies: Optional[
+            NotExplicitlyRequestedDependenciesByBaseModule
+        ],
+    ) -> List[StrictDependency]:
+        pass
+
+    @abstractmethod
+    def _should_only_except_requirement_violations_due_to_explicit_dependency_present(
+        self,
+        explicitly_requested_dependency_should_not_but_others_should_be_present: bool,
+        explicitly_requested_dependencies: Optional[
+            ExplicitlyRequestedDependenciesByBaseModules
+        ],
+    ) -> List[StrictDependency]:
+        pass
+
+    @abstractmethod
+    def _should_not_except_requirement_violations(
+        self,
+        not_explicitly_requested_dependencies_should_not_be_present: bool,
+        not_explicitly_requested_dependencies: Optional[
+            NotExplicitlyRequestedDependenciesByBaseModule
+        ],
+    ) -> List[StrictDependency]:
+        pass
 
     def _get_realised_dependencies(
         self, explicitly_requested_dependencies: Dict[Any, List[StrictDependency]]
@@ -156,6 +200,68 @@ def _get_rule_subject_and_object_in_user_specified_order(
 
         return dependency[1], dependency[0]
 
+    def _get_dependency_expectations(self) -> DependencyExpectation:
+        return DependencyExpectation(
+            not_explicitly_requested_dependencies_should_not_be_present=(
+                self._behavior_requirement.should_not
+                and self._behavior_requirement.behavior_exception
+            ),
+            explicitly_requested_dependencies_should_not_be_present=(
+                self._behavior_requirement.should_not
+                and not self._behavior_requirement.behavior_exception
+            ),
+            explicitly_requested_dependencies_and_no_other_should_be_present=(
+                self._behavior_requirement.should_only
+                and not self._behavior_requirement.behavior_exception
+            ),
+            explicitly_requested_dependencies_should_not_but_others_should_be_present=(
+                self._behavior_requirement.should_only
+                and self._behavior_requirement.behavior_exception
+            ),
+            at_least_one_not_explicitly_requested_dependency_should_be_present=(
+                self._behavior_requirement.should
+                and self._behavior_requirement.behavior_exception
+            ),
+            explicitly_requested_dependencies_should_be_present=(
+                self._behavior_requirement.should
+                and not self._behavior_requirement.behavior_exception
+            ),
+        )
+
+
+class RuleViolationDetector(RuleViolationBaseDetector):
+    """Based on the behavior requirement and the dependencies that have actually been (not) found in the graph,
+    this class decides whether any of the requirements have been violated and how.
+
+    ExplicitlyRequestedDependenciesByBaseModules follow this structure:
+    {(requested dependency, e.g. from A to B): [list of dependencies found that qualifiy as the requested dependency, e.g. A.a1 imports B, A.a2 imports B]}
+
+    NotExplicitlyRequestedDependenciesByBaseModule on the other hand are structured like this:
+    {Module for which not explicitly requested dependencies (either from or to this module) were found: [list of such dependencies]}.
+
+    The output is a rule violation object which for each type of rule contains a list of (rule subject, rule object) which violate the rule. These are ordered according
+    to the order the user used when specifying the rule, i.e. the rule subject is the original rule subject no matter if the rule is an import
+    or be imported rule.
+
+    If multiple rule subjects have been specified, the rule needs to apply to each and every one of them in order not
+    to count as being violated.
+    """
+
+    def _should_not_requirement_violations(
+        self,
+        explicitly_requested_dependencies_should_not_be_present: bool,
+        explicitly_requested_dependencies: Optional[
+            ExplicitlyRequestedDependenciesByBaseModules
+        ],
+    ) -> List[StrictDependency]:
+        if (
+            explicitly_requested_dependencies is None
+            or not explicitly_requested_dependencies_should_not_be_present
+        ):
+            return []
+
+        return self._get_realised_dependencies(explicitly_requested_dependencies)
+
     def _should_requirement_violations(
         self,
         explicitly_requested_dependencies_should_be_present: bool,
@@ -173,19 +279,6 @@ def _should_requirement_violations(
             explicitly_requested_dependencies
         )
 
-    def _get_abstract_dependencies_without_realisations(
-        self, explicitly_requested_dependencies: Dict[Any, List[StrictDependency]]
-    ) -> List[StrictDependency]:
-        violating_dependencies = [
-            abstract_dependency
-            for abstract_dependency, concrete_dependency in explicitly_requested_dependencies.items()
-            if len(concrete_dependency) == 0
-        ]
-        return [
-            self._get_rule_subject_and_object_in_user_specified_order(dependency)
-            for dependency in violating_dependencies
-        ]
-
     def _should_only_requirement_violations_by_no_import(
         self,
         explicitly_requested_dependencies_and_no_other_should_be_present: bool,
@@ -282,6 +375,19 @@ def _should_not_except_requirement_violations(
 
         return self._get_realised_dependencies(not_explicitly_requested_dependencies)
 
+    def _get_abstract_dependencies_without_realisations(
+        self, explicitly_requested_dependencies: Dict[Any, List[StrictDependency]]
+    ) -> List[StrictDependency]:
+        violating_dependencies = [
+            abstract_dependency
+            for abstract_dependency, concrete_dependency in explicitly_requested_dependencies.items()
+            if len(concrete_dependency) == 0
+        ]
+        return [
+            self._get_rule_subject_and_object_in_user_specified_order(dependency)
+            for dependency in violating_dependencies
+        ]
+
     def _get_missing_dependencies_in_user_specified_order(
         self,
         not_explicitly_requested_dependencies: NotExplicitlyRequestedDependenciesByBaseModule,
diff --git a/tests/conftest.py b/tests/conftest.py
index 6bb0b8a..94fe88a 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -64,6 +64,20 @@ def graph_including_tests() -> EvaluableArchitecture:
 
 @pytest.fixture(scope="module")
 def flat_project_1() -> EvaluableArchitecture:
+    """
+    Dependencies:
+        - exporter: logging_util, model, util
+        - importer: model, util
+        - logging_util: util
+        - model: -
+        - orchestration: exporter, importer, logging_util, model, simulation, util
+        - persistence: model, util
+        - runtime: logging_util, orchestration, persistence, services, util
+        - services: importer, model, persistence, util
+        - simulation: logging_util, model, util
+        - util: -
+
+    """
     return get_evaluable_architecture(
         os.path.dirname(flat_test_project_1.__file__),
         os.path.dirname(flat_test_project_1.__file__),
diff --git a/tests/integration/interesting_rules_for_tests.py b/tests/integration/interesting_rules_for_tests.py
index cdc6771..70bbe5e 100644
--- a/tests/integration/interesting_rules_for_tests.py
+++ b/tests/integration/interesting_rules_for_tests.py
@@ -1,6 +1,11 @@
 from __future__ import annotations
 
+from copy import deepcopy
+from dataclasses import dataclass
+from typing import Dict, List, Optional
+
 import pytest
+from _pytest.mark import ParameterSet
 
 from pytestarch import Rule
 
@@ -1684,3 +1689,1026 @@
         id="submodule should only be imported by except named - forbidden import - partial name match object",
     ),
 ]
+
+
+@dataclass
+class LayerRuleTestCase:
+    layers: Dict[str, List[str]]
+    rule_setup: LayerRuleSetup
+    expected_error_message: Optional[str] = None
+
+
+@dataclass
+class LayerRuleSetup:
+    behavior: str
+    access_type: str
+    importer: str
+    importee: List[str]
+
+
+PROJECT_ROOT = "flat_test_project_1"
+EXPORTER = f"{PROJECT_ROOT}.exporter"
+IMPORTER = f"{PROJECT_ROOT}.importer"
+LOGGING = f"{PROJECT_ROOT}.logging_util"
+MODEL = f"{PROJECT_ROOT}.model"
+ORCHESTRATION = f"{PROJECT_ROOT}.orchestration"
+PERSISTENCE = f"{PROJECT_ROOT}.persistence"
+RUNTIME = f"{PROJECT_ROOT}.runtime"
+SERVICES = f"{PROJECT_ROOT}.services"
+SIMULATION = f"{PROJECT_ROOT}.simulation"
+UTIL = f"{PROJECT_ROOT}.util"
+
+LAYER_1 = "L1"
+LAYER_2 = "L2"
+LAYER_3 = "L3"
+LAYER_4 = "L4"
+
+layer_1_should_access_layer_2 = LayerRuleSetup(
+    "should", "access_layers_that", LAYER_1, [LAYER_2]
+)
+layer_1_should_access_layer_2_and_3 = LayerRuleSetup(
+    "should", "access_layers_that", LAYER_1, [LAYER_2, LAYER_3]
+)
+
+layer_1_should_not_access_layer_2 = LayerRuleSetup(
+    "should_not", "access_layers_that", LAYER_1, [LAYER_2]
+)
+layer_1_should_not_access_layer_2_and_3 = LayerRuleSetup(
+    "should_not", "access_layers_that", LAYER_1, [LAYER_2, LAYER_3]
+)
+layer_1_should_not_access_anything = LayerRuleSetup(
+    "should_not", "access_any_layer", LAYER_1, []
+)
+
+layer_1_should_only_access_layer_2 = LayerRuleSetup(
+    "should_only", "access_layers_that", LAYER_1, [LAYER_2]
+)
+layer_1_should_only_access_layer_2_and_3 = LayerRuleSetup(
+    "should_only", "access_layers_that", LAYER_1, [LAYER_2, LAYER_3]
+)
+
+layer_1_should_access_except_layer_2 = LayerRuleSetup(
+    "should", "access_layers_except_layers_that", LAYER_1, [LAYER_2]
+)
+layer_1_should_access_except_layer_2_and_3 = LayerRuleSetup(
+    "should", "access_layers_except_layers_that", LAYER_1, [LAYER_2, LAYER_3]
+)
+
+layer_1_should_not_access_except_layer_2 = LayerRuleSetup(
+    "should_not", "access_layers_except_layers_that", LAYER_1, [LAYER_2]
+)
+layer_1_should_not_access_except_layer_2_and_3 = LayerRuleSetup(
+    "should_not", "access_layers_except_layers_that", LAYER_1, [LAYER_2, LAYER_3]
+)
+
+layer_1_should_only_access_except_layer_2 = LayerRuleSetup(
+    "should_only", "access_layers_except_layers_that", LAYER_1, [LAYER_2]
+)
+layer_1_should_only_access_except_layer_2_and_3 = LayerRuleSetup(
+    "should_only", "access_layers_except_layers_that", LAYER_1, [LAYER_2, LAYER_3]
+)
+
+layer_1_should_be_accessed_by_layer_2 = LayerRuleSetup(
+    "should", "be_accessed_by_layers_that", LAYER_1, [LAYER_2]
+)
+layer_1_should_be_accessed_by_layer_2_and_3 = LayerRuleSetup(
+    "should", "be_accessed_by_layers_that", LAYER_1, [LAYER_2, LAYER_3]
+)
+
+layer_1_should_not_be_accessed_by_layer_2 = LayerRuleSetup(
+    "should_not", "be_accessed_by_layers_that", LAYER_1, [LAYER_2]
+)
+layer_1_should_not_be_accessed_by_layer_2_and_3 = LayerRuleSetup(
+    "should_not", "be_accessed_by_layers_that", LAYER_1, [LAYER_2, LAYER_3]
+)
+layer_1_should_not_be_accessed_by_anything = LayerRuleSetup(
+    "should_not", "be_accessed_by_any_layer", LAYER_1, []
+)
+
+layer_1_should_only_be_accessed_by_layer_2 = LayerRuleSetup(
+    "should_only", "be_accessed_by_layers_that", LAYER_1, [LAYER_2]
+)
+layer_1_should_only_be_accessed_by_layer_2_and_3 = LayerRuleSetup(
+    "should_only", "be_accessed_by_layers_that", LAYER_1, [LAYER_2, LAYER_3]
+)
+
+layer_1_should_be_accessed_except_by_layer_2 = LayerRuleSetup(
+    "should", "be_accessed_by_layers_except_layers_that", LAYER_1, [LAYER_2]
+)
+layer_1_should_be_accessed_except_by_layer_2_and_3 = LayerRuleSetup(
+    "should", "be_accessed_by_layers_except_layers_that", LAYER_1, [LAYER_2, LAYER_3]
+)
+
+layer_1_should_not_be_accessed_except_by_layer_2 = LayerRuleSetup(
+    "should_not", "be_accessed_by_layers_except_layers_that", LAYER_1, [LAYER_2]
+)
+layer_1_should_not_be_accessed_except_by_layer_2_and_3 = LayerRuleSetup(
+    "should_not",
+    "be_accessed_by_layers_except_layers_that",
+    LAYER_1,
+    [LAYER_2, LAYER_3],
+)
+
+layer_1_should_only_be_accessed_except_by_layer_2 = LayerRuleSetup(
+    "should_only", "be_accessed_by_layers_except_layers_that", LAYER_1, [LAYER_2]
+)
+layer_1_should_only_be_accessed_except_by_layer_2_and_3 = LayerRuleSetup(
+    "should_only",
+    "be_accessed_by_layers_except_layers_that",
+    LAYER_1,
+    [LAYER_2, LAYER_3],
+)
+
+
+fulfilled_layer_rule_test_cases_access = [
+    # access
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [EXPORTER], LAYER_2: [LOGGING]}, layer_1_should_access_layer_2
+        ),
+        id="should_fulfilled_one_import",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [EXPORTER, MODEL], LAYER_2: [LOGGING]},
+            layer_1_should_access_layer_2,
+        ),
+        id="should_fulfilled_one_import_by_one_module",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [EXPORTER, ORCHESTRATION], LAYER_2: [LOGGING]},
+            layer_1_should_access_layer_2,
+        ),
+        id="should_fulfilled_one_import_by_all_modules_in_layer",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [EXPORTER], LAYER_2: [LOGGING, MODEL]},
+            layer_1_should_access_layer_2,
+        ),
+        id="should_fulfilled_multiple_imports_by_one_module_two_layers",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [EXPORTER, ORCHESTRATION], LAYER_2: [LOGGING, SIMULATION]},
+            layer_1_should_access_layer_2,
+        ),
+        id="should_fulfilled_multiple_imports_by_different_modules_two_layers",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [EXPORTER], LAYER_2: [LOGGING], LAYER_3: [MODEL]},
+            layer_1_should_access_layer_2_and_3,
+        ),
+        id="should_fulfilled_multiple_imports_by_one_module_three_layers",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {
+                LAYER_1: [EXPORTER, ORCHESTRATION],
+                LAYER_2: [LOGGING],
+                LAYER_3: [SIMULATION],
+            },
+            layer_1_should_access_layer_2_and_3,
+        ),
+        id="should_fulfilled_multiple_imports_by_different_modules_three_layers",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [UTIL], LAYER_2: [MODEL]}, layer_1_should_not_access_layer_2
+        ),
+        id="should_not_fulfilled_no_imports",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [UTIL], LAYER_2: [MODEL, SIMULATION]},
+            layer_1_should_not_access_layer_2,
+        ),
+        id="should_not_fulfilled_no_imports_multiple_modules_in_layer",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [UTIL], LAYER_2: [MODEL], LAYER_3: [SIMULATION]},
+            layer_1_should_not_access_layer_2_and_3,
+        ),
+        id="should_not_fulfilled_no_imports_multiple_rule_objects",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [RUNTIME], LAYER_2: [SIMULATION]},
+            layer_1_should_not_access_layer_2,
+        ),
+        id="should_not_fulfilled_import_of_other_layer",
+    ),
+    pytest.param(
+        LayerRuleTestCase({LAYER_1: [MODEL]}, layer_1_should_not_access_anything),
+        id="should_not_fulfilled_import_anything",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {
+                LAYER_1: [RUNTIME],
+                LAYER_2: [PERSISTENCE, LOGGING, UTIL, ORCHESTRATION, SERVICES],
+            },
+            layer_1_should_only_access_layer_2,
+        ),
+        id="should_only_fulfilled_one_import",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {
+                LAYER_1: [RUNTIME],
+                LAYER_2: [PERSISTENCE, LOGGING, UTIL, ORCHESTRATION],
+                LAYER_3: [SERVICES],
+            },
+            layer_1_should_only_access_layer_2_and_3,
+        ),
+        id="should_only_fulfilled_one_import_multiple_rule_objects",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {
+                LAYER_1: [RUNTIME, ORCHESTRATION],
+                LAYER_2: [
+                    PERSISTENCE,
+                    LOGGING,
+                    UTIL,
+                    SERVICES,
+                    EXPORTER,
+                    MODEL,
+                    SIMULATION,
+                    IMPORTER,
+                ],
+            },
+            layer_1_should_only_access_layer_2,
+        ),
+        id="should_only_fulfilled_multiple_imports_by_different_modules",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {
+                LAYER_1: [RUNTIME],
+                LAYER_2: [LOGGING, PERSISTENCE, UTIL, MODEL, ORCHESTRATION],
+            },
+            layer_1_should_access_except_layer_2,
+        ),
+        id="should_except_fulfilled_one_other_import",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {
+                LAYER_1: [RUNTIME],
+                LAYER_2: [LOGGING, PERSISTENCE, UTIL],
+                LAYER_3: [ORCHESTRATION],
+            },
+            layer_1_should_access_except_layer_2_and_3,
+        ),
+        id="should_except_fulfilled_one_other_import_multiple_rule_objects",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {
+                LAYER_1: [RUNTIME, IMPORTER],
+                LAYER_2: [LOGGING, UTIL],
+                LAYER_3: [ORCHESTRATION],
+            },
+            layer_1_should_access_except_layer_2,
+        ),
+        id="should_except_fulfilled_multiple_other_imports_by_one_module",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [UTIL], LAYER_2: [MODEL]},
+            layer_1_should_not_access_except_layer_2,
+        ),
+        id="should_not_except_fulfilled_no_other_imports",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [SIMULATION, UTIL, MODEL], LAYER_2: [LOGGING]},
+            layer_1_should_not_access_except_layer_2,
+        ),
+        id="should_not_except_fulfilled_one_import",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {
+                LAYER_1: [SIMULATION, UTIL, MODEL],
+                LAYER_2: [LOGGING],
+                LAYER_3: [SERVICES],
+            },
+            layer_1_should_not_access_except_layer_2_and_3,
+        ),
+        id="should_not_except_fulfilled_one_import_multiple_rule_objects",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [EXPORTER, IMPORTER, LOGGING], LAYER_2: [MODEL, UTIL]},
+            layer_1_should_not_access_except_layer_2,
+        ),
+        id="should_not_except_fulfilled_multiple_imports",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [IMPORTER], LAYER_2: [LOGGING]},
+            layer_1_should_only_access_except_layer_2,
+        ),
+        id="should_only_except_fulfilled_one_other_import",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [IMPORTER], LAYER_2: [LOGGING], LAYER_3: [SIMULATION]},
+            layer_1_should_only_access_except_layer_2_and_3,
+        ),
+        id="should_only_except_fulfilled_one_other_import_multiple_rule_objects",
+    ),
+]
+
+
+fulfilled_layer_rule_test_cases_be_accessed = [
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [SERVICES], LAYER_2: [RUNTIME]},
+            layer_1_should_be_accessed_by_layer_2,
+        ),
+        id="should_fulfilled_one_be_imported",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [SERVICES, PERSISTENCE], LAYER_2: [RUNTIME]},
+            layer_1_should_be_accessed_by_layer_2,
+        ),
+        id="should_fulfilled_multiple_be_imported_by_one_module",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [SERVICES, MODEL], LAYER_2: [RUNTIME, IMPORTER]},
+            layer_1_should_be_accessed_by_layer_2,
+        ),
+        id="should_fulfilled_multiple_be_imported_by_different_modules",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [SERVICES, MODEL], LAYER_2: [RUNTIME], LAYER_3: [IMPORTER]},
+            layer_1_should_be_accessed_by_layer_2_and_3,
+        ),
+        id="should_fulfilled_multiple_be_imported_by_different_modules_per_rule_object",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [RUNTIME], LAYER_2: [MODEL]},
+            layer_1_should_not_be_accessed_by_layer_2,
+        ),
+        id="should_not_fulfilled_no_be_imported",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [RUNTIME], LAYER_2: [MODEL, UTIL]},
+            layer_1_should_not_be_accessed_by_layer_2,
+        ),
+        id="should_not_fulfilled_no_be_imported_multiple_modules_in_layer",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [RUNTIME], LAYER_2: [MODEL], LAYER_3: [UTIL]},
+            layer_1_should_not_be_accessed_by_layer_2_and_3,
+        ),
+        id="should_not_fulfilled_no_be_imported_multiple_rule_objects",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [RUNTIME, SIMULATION], LAYER_2: [MODEL]},
+            layer_1_should_not_be_accessed_by_layer_2,
+        ),
+        id="should_not_fulfilled_be_imported_of_other_layer",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [RUNTIME]}, layer_1_should_not_be_accessed_by_anything
+        ),
+        id="should_not_fulfilled_be_imported_by_any_layer",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [SERVICES], LAYER_2: [RUNTIME]},
+            layer_1_should_only_be_accessed_by_layer_2,
+        ),
+        id="should_only_fulfilled_one_be_imported",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {
+                LAYER_1: [PERSISTENCE, SIMULATION],
+                LAYER_2: [RUNTIME, SERVICES],
+                LAYER_3: [ORCHESTRATION],
+            },
+            layer_1_should_only_be_accessed_by_layer_2_and_3,
+        ),
+        id="should_only_fulfilled_one_be_imported_multiple_rule_objects",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [UTIL], LAYER_2: [SERVICES]},
+            layer_1_should_be_accessed_except_by_layer_2,
+        ),
+        id="should_except_fulfilled_one_other_be_imported",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [UTIL], LAYER_2: [SERVICES], LAYER_3: [LOGGING]},
+            layer_1_should_be_accessed_except_by_layer_2_and_3,
+        ),
+        id="should_except_fulfilled_one_other_be_imported_multiple_rule_objects",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [UTIL], LAYER_2: [SERVICES, LOGGING]},
+            layer_1_should_be_accessed_except_by_layer_2,
+        ),
+        id="should_except_fulfilled_multiple_other_be_imported_by_one_module",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [UTIL, MODEL], LAYER_2: [SERVICES, IMPORTER]},
+            layer_1_should_be_accessed_except_by_layer_2,
+        ),
+        id="should_except_fulfilled_multiple_other_be_imported_by_different_modules",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [SERVICES], LAYER_2: [RUNTIME, EXPORTER]},
+            layer_1_should_not_be_accessed_except_by_layer_2,
+        ),
+        id="should_not_except_fulfilled_no_other_be_imported",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [SERVICES, EXPORTER], LAYER_2: [RUNTIME, ORCHESTRATION]},
+            layer_1_should_not_be_accessed_except_by_layer_2,
+        ),
+        id="should_not_except_fulfilled_multiple_be_imported",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {
+                LAYER_1: [SERVICES, EXPORTER],
+                LAYER_2: [RUNTIME],
+                LAYER_3: [ORCHESTRATION],
+            },
+            layer_1_should_not_be_accessed_except_by_layer_2_and_3,
+        ),
+        id="should_not_except_fulfilled_multiple_be_imported_rule_objects",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [SERVICES], LAYER_2: [PERSISTENCE]},
+            layer_1_should_only_be_accessed_except_by_layer_2,
+        ),
+        id="should_only_except_fulfilled_one_other_be_imported",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [SERVICES], LAYER_2: [PERSISTENCE], LAYER_3: [SIMULATION]},
+            layer_1_should_only_be_accessed_except_by_layer_2_and_3,
+        ),
+        id="should_only_except_fulfilled_one_other_be_imported_multiple_rule_objects",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [SERVICES], LAYER_2: [PERSISTENCE, SIMULATION]},
+            layer_1_should_only_be_accessed_except_by_layer_2,
+        ),
+        id="should_only_except_fulfilled_multiple_other_be_imported",
+    ),
+]
+
+
+def _add_submodule_of_layer_1_module(parameter_set: ParameterSet) -> ParameterSet:
+    id = parameter_set.id + "_submodule_and_parent_module_in_layer"
+
+    values = deepcopy(parameter_set.values)
+
+    current_modules_in_layer_1 = values[0].layers[LAYER_1]
+
+    module_name_of_first_module = current_modules_in_layer_1[0].split(".")[1]
+
+    values[0].layers[LAYER_1] = current_modules_in_layer_1 + [
+        f"{current_modules_in_layer_1[0]}.{module_name_of_first_module}_importer"
+    ]
+
+    return ParameterSet.param(*values, id=id)
+
+
+fulfilled_layer_rule_test_cases_submodule_and_module_in_layer_access = [
+    _add_submodule_of_layer_1_module(parameter_set)
+    for parameter_set in fulfilled_layer_rule_test_cases_access
+]
+fulfilled_layer_rule_test_cases_submodule_and_module_in_layer_be_accessed = [
+    _add_submodule_of_layer_1_module(parameter_set)
+    for parameter_set in fulfilled_layer_rule_test_cases_be_accessed
+]
+
+fulfilled_layer_rule_test_cases = (
+    fulfilled_layer_rule_test_cases_access
+    + fulfilled_layer_rule_test_cases_be_accessed
+    + fulfilled_layer_rule_test_cases_submodule_and_module_in_layer_access
+    + fulfilled_layer_rule_test_cases_submodule_and_module_in_layer_be_accessed
+)
+
+
+layer_1_does_not_import_layer_2 = (
+    f'Layer "{LAYER_1}" does not import layer "{LAYER_2}".'
+)
+layer_1_does_not_import_layer_3 = (
+    f'Layer "{LAYER_1}" does not import layer "{LAYER_3}".'
+)
+layer_1_does_not_import_any_layer_that_is_not_layer_2 = (
+    f'Layer "{LAYER_1}" does not import any layer that is not layer "{LAYER_2}".'
+)
+layer_1_is_not_imported_by_layer_2 = (
+    f'Layer "{LAYER_1}" is not imported by layer "{LAYER_2}".'
+)
+layer_1_is_not_imported_by_layer_3 = (
+    f'Layer "{LAYER_1}" is not imported by layer "{LAYER_3}".'
+)
+layer_1_is_not_imported_by_any_layer_that_is_not_layer_2 = (
+    f'Layer "{LAYER_1}" is not imported by any layer that is not layer "{LAYER_2}".'
+)
+layer_1_does_not_import_layer_2_3 = (
+    f'Layer "{LAYER_1}" does not import layer "{LAYER_2}", layer "{LAYER_3}".'
+)
+layer_1_does_not_import_any_layer_that_is_not_layer_2_3 = f'Layer "{LAYER_1}" does not import any layer that is not layer "{LAYER_2}", layer "{LAYER_3}".'
+layer_1_is_not_imported_by_layer_2_3 = (
+    f'Layer "{LAYER_1}" is not imported by layer "{LAYER_2}", layer "{LAYER_3}".'
+)
+layer_1_is_not_imported_by_any_layer_that_is_not_layer_2_3 = f'Layer "{LAYER_1}" is not imported by any layer that is not layer "{LAYER_2}", layer "{LAYER_3}".'
+
+EXPORTER_IMPORTER = f"{EXPORTER}.exporter_importer"
+EXPORTER_IMPORTEE = f"{EXPORTER}.exporter_importee"
+RUNTIME_IMPORTER = f"{RUNTIME}.runtime_importer"
+SERVICES_IMPORTEE = f"{SERVICES}.services_importee"
+UTIL_IMPORTEE = f"{UTIL}.util_importee"
+PERSISTENCE_IMPORTEE = f"{PERSISTENCE}.persistence_importee"
+PERSISTENCE_IMPORTER = f"{PERSISTENCE}.persistence_importer"
+ORCHESTRATION_IMPORTEE = f"{ORCHESTRATION}.orchestration_importee"
+ORCHESTRATION_IMPORTER = f"{ORCHESTRATION}.orchestration_importer"
+LOGGING_IMPORTEE = f"{LOGGING}.logging_util_importee"
+SIMULATION_IMPORTEE = f"{SIMULATION}.simulation_importee"
+IMPORTER_IMPORTER = f"{IMPORTER}.importer_importer"
+IMPORTER_IMPORTEE = f"{IMPORTER}.importer_importee"
+MODEL_IMPORTEE = f"{MODEL}.model_importee"
+
+runtime_imports_services_no_layer = f'"{RUNTIME_IMPORTER}" (layer "{LAYER_1}") imports "{SERVICES_IMPORTEE}" (no layer).'
+runtime_imports_services_layer_2 = f'"{RUNTIME_IMPORTER}" (layer "{LAYER_1}") imports "{SERVICES_IMPORTEE}" (layer "{LAYER_2}").'
+
+
+layer_rule_error_messages_test_cases_access = [
+    # single rule subject single rule object
+    # pytest.param(LayerRuleTestCase({LAYER_1: [UTIL], LAYER_2: [MODEL]}, layer_1_should_access_layer_2, layer_1_does_not_import_layer_2), id='should_violated_no_imports_ssso',),
+    # pytest.param(LayerRuleTestCase({LAYER_1: [IMPORTER], LAYER_2: [LOGGING]}, layer_1_should_access_layer_2, layer_1_does_not_import_layer_2), id='should_violated_only_other_imports_ssso',),
+    #
+    # pytest.param(LayerRuleTestCase({LAYER_1: [RUNTIME], LAYER_2: [SERVICES]}, layer_1_should_not_access_layer_2, f'"{RUNTIME_IMPORTER}" (layer "{LAYER_1}") imports "{SERVICES_IMPORTEE}" (layer "{LAYER_2}").'), id='should_not_violated_one_import_ssso',),
+    pytest.param(
+        LayerRuleTestCase(
+            {
+                LAYER_1: [
+                    RUNTIME,
+                    LOGGING,
+                    PERSISTENCE,
+                    UTIL,
+                    ORCHESTRATION,
+                    MODEL,
+                    SIMULATION,
+                    EXPORTER,
+                    IMPORTER,
+                ],
+                LAYER_2: [],
+            },
+            layer_1_should_not_access_anything,
+            f'"{RUNTIME_IMPORTER}" (layer "{LAYER_1}") imports "{SERVICES_IMPORTEE}" (no layer).',
+        ),
+        id="should_not_anything_violated_ssso",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [UTIL], LAYER_2: [MODEL]},
+            layer_1_should_only_access_layer_2,
+            layer_1_does_not_import_layer_2,
+        ),
+        id="should_only_violated_no_imports_ssso",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [RUNTIME], LAYER_2: [EXPORTER]},
+            layer_1_should_only_access_layer_2,
+            layer_1_does_not_import_layer_2,
+        ),
+        id="should_only_violated_only_imports_of_other_layers_ssso",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [RUNTIME], LAYER_2: [SERVICES]},
+            layer_1_should_only_access_layer_2,
+            f'"{RUNTIME_IMPORTER}" (layer "{LAYER_1}") imports "{LOGGING_IMPORTEE}" (no layer).\n"{RUNTIME_IMPORTER}" (layer "{LAYER_1}") imports "{ORCHESTRATION_IMPORTEE}" (no layer).\n"{RUNTIME_IMPORTER}" (layer "{LAYER_1}") imports "{PERSISTENCE_IMPORTEE}" (no layer).\n"{RUNTIME_IMPORTER}" (layer "{LAYER_1}") imports "{UTIL_IMPORTEE}" (no layer).',
+        ),
+        id="should_only_violated_also_imports_of_other_layers_ssso",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [IMPORTER], LAYER_2: [MODEL, UTIL]},
+            layer_1_should_access_except_layer_2,
+            layer_1_does_not_import_any_layer_that_is_not_layer_2,
+        ),
+        id="should_except_violated_no_other_import_ssso",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [SERVICES], LAYER_2: [PERSISTENCE, MODEL, UTIL, IMPORTER]},
+            layer_1_should_access_except_layer_2,
+            layer_1_does_not_import_any_layer_that_is_not_layer_2,
+        ),
+        id="should_except_violated_only_import_ssso",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [RUNTIME], LAYER_2: [SIMULATION, IMPORTER, LOGGING, PERSISTENCE]},
+            layer_1_should_not_access_except_layer_2,
+            runtime_imports_services_no_layer,
+        ),
+        id="should_not_except_violated_other_import_ssso",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [RUNTIME], LAYER_2: [SIMULATION, IMPORTER, ORCHESTRATION]},
+            layer_1_should_not_access_except_layer_2,
+            runtime_imports_services_no_layer,
+        ),
+        id="should_not_except_violated_import_and_other_import_ssso",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [RUNTIME], LAYER_2: [SERVICES, MODEL]},
+            layer_1_should_only_access_except_layer_2,
+            f'"{RUNTIME_IMPORTER}" (layer "{LAYER_1}") imports "{SERVICES_IMPORTEE}" (layer "{LAYER_2}").',
+        ),
+        id="should_only_except_violated_import_ssso",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [SERVICES], LAYER_2: [MODEL, PERSISTENCE, IMPORTER, UTIL]},
+            layer_1_should_only_access_except_layer_2,
+            layer_1_does_not_import_any_layer_that_is_not_layer_2,
+        ),
+        id="should_only_except_violated_no_other_import_ssso",
+    ),
+    # single rule subject multiple rule objects
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [UTIL], LAYER_2: [MODEL], LAYER_3: [LOGGING]},
+            layer_1_should_access_layer_2_and_3,
+            layer_1_does_not_import_layer_2_3,
+        ),
+        id="should_violated_no_imports_ssmo",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [EXPORTER], LAYER_2: [IMPORTER], LAYER_3: [LOGGING]},
+            layer_1_should_access_layer_2_and_3,
+            layer_1_does_not_import_layer_2,
+        ),
+        id="should_violated_one_object_correct_one_object_no_import_ssmo",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [EXPORTER, MODEL], LAYER_2: [IMPORTER], LAYER_3: [PERSISTENCE]},
+            layer_1_should_access_layer_2_and_3,
+            layer_1_does_not_import_layer_2_3,
+        ),
+        id="should_violated_only_other_imports_ssmo",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [IMPORTER], LAYER_2: [MODEL], LAYER_3: [UTIL]},
+            layer_1_should_not_access_layer_2_and_3,
+            f'"{IMPORTER_IMPORTER}" (layer "{LAYER_1}") imports "{MODEL_IMPORTEE}" (layer "{LAYER_2}").\n"{IMPORTER_IMPORTER}" (layer "{LAYER_1}") imports "{UTIL_IMPORTEE}" (layer "{LAYER_3}").',
+        ),
+        id="should_not_violated_both_one_import_sssmo",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [IMPORTER, PERSISTENCE], LAYER_2: [EXPORTER], LAYER_3: [UTIL]},
+            layer_1_should_not_access_layer_2_and_3,
+            f'"{IMPORTER_IMPORTER}" (layer "{LAYER_1}") imports "{UTIL_IMPORTEE}" (layer "{LAYER_3}").\n"{PERSISTENCE_IMPORTER}" (layer "{LAYER_1}") imports "{UTIL_IMPORTEE}" (layer "{LAYER_3}").',
+        ),
+        id="should_not_violated_one_object_correct_one_import_sssmo",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {
+                LAYER_1: [RUNTIME],
+                LAYER_2: [SERVICES, PERSISTENCE, UTIL, ORCHESTRATION],
+                LAYER_3: [SIMULATION],
+            },
+            layer_1_should_only_access_layer_2_and_3,
+            layer_1_does_not_import_layer_3,
+        ),
+        id="should_only_violated_only_one_rule_object_imported",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [RUNTIME], LAYER_2: [SERVICES, SIMULATION], LAYER_3: [EXPORTER]},
+            layer_1_should_only_access_layer_2_and_3,
+            layer_1_does_not_import_layer_3,
+        ),
+        id="should_only_violated_also_imports_of_other_layers_ssmo",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [RUNTIME, SERVICES], LAYER_2: [SIMULATION], LAYER_3: [EXPORTER]},
+            layer_1_should_only_access_layer_2_and_3,
+            layer_1_does_not_import_layer_2_3,
+        ),
+        id="should_only_violated_only_imports_of_other_layers_ssmo",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [LOGGING], LAYER_2: [MODEL], LAYER_3: [UTIL]},
+            layer_1_should_access_except_layer_2_and_3,
+            layer_1_does_not_import_any_layer_that_is_not_layer_2_3,
+        ),
+        id="should_except_violated_no_imports_ssmo",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [IMPORTER, EXPORTER], LAYER_2: [MODEL, LOGGING], LAYER_3: [UTIL]},
+            layer_1_should_access_except_layer_2_and_3,
+            layer_1_does_not_import_any_layer_that_is_not_layer_2_3,
+        ),
+        id="should_except_violated_only_import_no_other_imports_ssmo",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [RUNTIME], LAYER_2: [SERVICES, PERSISTENCE], LAYER_3: [UTIL]},
+            layer_1_should_not_access_except_layer_2_and_3,
+            f'"{RUNTIME_IMPORTER}" (layer "{LAYER_1}") imports "{LOGGING_IMPORTEE}" (no layer).\n"{RUNTIME_IMPORTER}" (layer "{LAYER_1}") imports "{ORCHESTRATION_IMPORTEE}" (no layer).',
+        ),
+        id="should_not_except_violated_other_import_ssmo",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [IMPORTER, SERVICES], LAYER_2: [MODEL], LAYER_3: [RUNTIME]},
+            layer_1_should_only_access_except_layer_2_and_3,
+            f'"{IMPORTER_IMPORTER}" (layer "{LAYER_1}") imports "{MODEL_IMPORTEE}" (layer "{LAYER_2}").',
+        ),
+        id="should_only_except_violated_one_import_ssmo",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [IMPORTER], LAYER_2: [MODEL, SERVICES], LAYER_3: [UTIL]},
+            layer_1_should_only_access_except_layer_2_and_3,
+            f'"{IMPORTER_IMPORTER}" (layer "{LAYER_1}") imports "{MODEL_IMPORTEE}" (layer "{LAYER_2}").\n"{IMPORTER_IMPORTER}" (layer "{LAYER_1}") imports "{UTIL_IMPORTEE}" (layer "{LAYER_3}").',
+        ),
+        id="should_only_except_violated_multiple_imports_ssmo",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {
+                LAYER_1: [SERVICES],
+                LAYER_2: [MODEL, IMPORTER, PERSISTENCE],
+                LAYER_3: [UTIL, RUNTIME],
+            },
+            layer_1_should_only_access_except_layer_2_and_3,
+            layer_1_does_not_import_any_layer_that_is_not_layer_2_3,
+        ),
+        id="should_only_except_violated_no_other_import_ssmo",
+    ),
+]
+
+layer_rule_error_messages_test_cases_be_accessed = [
+    # single rule subject single rule object
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [RUNTIME], LAYER_2: [SIMULATION]},
+            layer_1_should_be_accessed_by_layer_2,
+            layer_1_is_not_imported_by_layer_2,
+        ),
+        id="should_violated_no_be_imported_ssso",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [LOGGING, ORCHESTRATION], LAYER_2: [PERSISTENCE, SERVICES]},
+            layer_1_should_be_accessed_by_layer_2,
+            layer_1_is_not_imported_by_layer_2,
+        ),
+        id="should_violated_only_other_be_imported_ssso",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [PERSISTENCE, UTIL], LAYER_2: [RUNTIME]},
+            layer_1_should_not_be_accessed_by_layer_2,
+            f'"{PERSISTENCE_IMPORTEE}" (layer "{LAYER_1}") is imported by "{RUNTIME_IMPORTER}" (layer "{LAYER_2}").\n"{UTIL_IMPORTEE}" (layer "{LAYER_1}") is imported by "{RUNTIME_IMPORTER}" (layer "{LAYER_2}").',
+        ),
+        id="should_not_violated_one_be_imported_ssso",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [ORCHESTRATION], LAYER_2: []},
+            layer_1_should_not_be_accessed_by_anything,
+            f'"{ORCHESTRATION_IMPORTEE}" (layer "{LAYER_1}") is imported by "{RUNTIME_IMPORTER}" (no layer).',
+        ),
+        id="should_not_be_imported_by_anything_violated_ssso",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [RUNTIME], LAYER_2: [EXPORTER]},
+            layer_1_should_only_be_accessed_by_layer_2,
+            layer_1_is_not_imported_by_layer_2,
+        ),
+        id="should_only_violated_no_be_imported_ssso",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [RUNTIME, SERVICES], LAYER_2: [EXPORTER]},
+            layer_1_should_only_be_accessed_by_layer_2,
+            layer_1_is_not_imported_by_layer_2,
+        ),
+        id="should_only_violated_only_be_imported_of_other_layers_ssso",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [PERSISTENCE, SIMULATION], LAYER_2: [RUNTIME]},
+            layer_1_should_only_be_accessed_by_layer_2,
+            f'"{SIMULATION_IMPORTEE}" (layer "{LAYER_1}") is imported by "{ORCHESTRATION_IMPORTER}" (no layer).',
+        ),
+        id="should_only_violated_also_be_imported_of_other_layers_ssso",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [RUNTIME], LAYER_2: [MODEL]},
+            layer_1_should_be_accessed_except_by_layer_2,
+            layer_1_is_not_imported_by_any_layer_that_is_not_layer_2,
+        ),
+        id="should_except_violated_no_other_be_imported_ssso",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [SERVICES], LAYER_2: [MODEL, RUNTIME]},
+            layer_1_should_be_accessed_except_by_layer_2,
+            layer_1_is_not_imported_by_any_layer_that_is_not_layer_2,
+        ),
+        id="should_except_violated_only_be_imported_ssso",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [ORCHESTRATION], LAYER_2: [MODEL, EXPORTER]},
+            layer_1_should_not_be_accessed_except_by_layer_2,
+            f'"{ORCHESTRATION_IMPORTEE}" (layer "{LAYER_1}") is imported by "{RUNTIME_IMPORTER}" (no layer).',
+        ),
+        id="should_not_except_violated_other_be_imported_ssso",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [LOGGING], LAYER_2: [RUNTIME, SIMULATION, ORCHESTRATION]},
+            layer_1_should_not_be_accessed_except_by_layer_2,
+            f'"{LOGGING_IMPORTEE}" (layer "{LAYER_1}") is imported by "{EXPORTER_IMPORTER}" (no layer).',
+        ),
+        id="should_not_except_violated_be_imported_and_other_be_imported_ssso",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [SIMULATION, LOGGING], LAYER_2: [RUNTIME]},
+            layer_1_should_only_be_accessed_except_by_layer_2,
+            f'"{LOGGING_IMPORTEE}" (layer "{LAYER_1}") is imported by "{RUNTIME_IMPORTER}" (layer "{LAYER_2}").',
+        ),
+        id="should_only_except_violated_be_imported_ssso",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [RUNTIME], LAYER_2: [ORCHESTRATION, SIMULATION]},
+            layer_1_should_only_be_accessed_except_by_layer_2,
+            layer_1_is_not_imported_by_any_layer_that_is_not_layer_2,
+        ),
+        id="should_only_except_violated_no_other_be_imported_ssso",
+    ),
+    # single rule subject multiple rule objects
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [RUNTIME], LAYER_2: [SERVICES], LAYER_3: [ORCHESTRATION]},
+            layer_1_should_be_accessed_by_layer_2_and_3,
+            layer_1_is_not_imported_by_layer_2_3,
+        ),
+        id="should_violated_no_be_imported_ssmo",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {
+                LAYER_1: [IMPORTER, SERVICES],
+                LAYER_2: [ORCHESTRATION],
+                LAYER_3: [EXPORTER],
+            },
+            layer_1_should_be_accessed_by_layer_2_and_3,
+            layer_1_is_not_imported_by_layer_3,
+        ),
+        id="should_violated_one_object_correct_one_object_no_import",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [MODEL], LAYER_2: [EXPORTER], LAYER_3: [IMPORTER]},
+            layer_1_should_not_be_accessed_by_layer_2_and_3,
+            f'"{MODEL_IMPORTEE}" (layer "{LAYER_1}") is imported by "{EXPORTER_IMPORTER}" (layer "{LAYER_2}").\n"{MODEL_IMPORTEE}" (layer "{LAYER_1}") is imported by "{IMPORTER_IMPORTER}" (layer "{LAYER_3}").',
+        ),
+        id="should_not_violated_both_one_be_imported_sssmo",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [MODEL, UTIL], LAYER_2: [EXPORTER], LAYER_3: [LOGGING]},
+            layer_1_should_not_be_accessed_by_layer_2_and_3,
+            f'"{MODEL_IMPORTEE}" (layer "{LAYER_1}") is imported by "{EXPORTER_IMPORTER}" (layer "{LAYER_2}").\n"{UTIL_IMPORTEE}" (layer "{LAYER_1}") is imported by "{EXPORTER_IMPORTER}" (layer "{LAYER_2}").',
+        ),
+        id="should_not_violated_one_object_correct_one_be_imported_sssmo",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {
+                LAYER_1: [MODEL, UTIL],
+                LAYER_2: [SERVICES, LOGGING],
+                LAYER_3: [ORCHESTRATION, EXPORTER],
+            },
+            layer_1_should_only_be_accessed_by_layer_2_and_3,
+            "",
+        ),
+        id="should_only_violated_only_one_rule_object_imported",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [PERSISTENCE], LAYER_2: [EXPORTER], LAYER_3: [IMPORTER]},
+            layer_1_should_only_be_accessed_by_layer_2_and_3,
+            layer_1_is_not_imported_by_layer_2_3,
+        ),
+        id="should_only_violated_only_be_imported_of_other_layers_ssmo",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [RUNTIME], LAYER_2: [MODEL], LAYER_3: [UTIL]},
+            layer_1_should_be_accessed_except_by_layer_2_and_3,
+            layer_1_is_not_imported_by_any_layer_that_is_not_layer_2_3,
+        ),
+        id="should_except_violated_no_be_imported_ssmo",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [ORCHESTRATION], LAYER_2: [RUNTIME, MODEL], LAYER_3: [UTIL]},
+            layer_1_should_be_accessed_except_by_layer_2_and_3,
+            layer_1_is_not_imported_by_any_layer_that_is_not_layer_2_3,
+        ),
+        id="should_except_violated_only_be_imported_no_other_be_imported_ssmo",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {
+                LAYER_1: [LOGGING],
+                LAYER_2: [EXPORTER, MODEL],
+                LAYER_3: [RUNTIME, SIMULATION],
+            },
+            layer_1_should_not_be_accessed_except_by_layer_2_and_3,
+            f'"{LOGGING_IMPORTEE}" (layer "{LAYER_1}") is imported by "{ORCHESTRATION_IMPORTER}" (no layer).',
+        ),
+        id="should_not_except_violated_other_be_imported_ssmo",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {
+                LAYER_1: [IMPORTER, SERVICES],
+                LAYER_2: [ORCHESTRATION, MODEL],
+                LAYER_3: [UTIL],
+            },
+            layer_1_should_only_be_accessed_except_by_layer_2_and_3,
+            f'"{IMPORTER_IMPORTEE}" (layer "{LAYER_1}") is imported by "{ORCHESTRATION_IMPORTER}" (layer "{LAYER_2}").',
+        ),
+        id="should_only_except_violated_one_be_imported_ssmo",
+    ),
+    pytest.param(
+        LayerRuleTestCase(
+            {LAYER_1: [SERVICES], LAYER_2: [RUNTIME], LAYER_3: [MODEL]},
+            layer_1_should_only_be_accessed_except_by_layer_2_and_3,
+            f'"{SERVICES_IMPORTEE}" (layer "{LAYER_1}") is imported by "{RUNTIME_IMPORTER}" (layer "{LAYER_2}").\n'
+            + layer_1_is_not_imported_by_any_layer_that_is_not_layer_2_3,
+        ),
+        id="should_only_except_violated_no_other_be_imported_ssmo",
+    ),
+]
+
+layer_rule_error_messages_test_cases_submodule_and_module_in_layer_access = [
+    _add_submodule_of_layer_1_module(parameter_set)
+    for parameter_set in layer_rule_error_messages_test_cases_access
+]
+layer_rule_error_messages_test_cases_submodule_and_module_in_layer_be_accessed = [
+    _add_submodule_of_layer_1_module(parameter_set)
+    for parameter_set in layer_rule_error_messages_test_cases_be_accessed
+]
+
+# layer_rule_error_messages_test_cases = layer_rule_error_messages_test_cases_access + layer_rule_error_messages_test_cases_be_accessed + layer_rule_error_messages_test_cases_submodule_and_module_in_layer_access + layer_rule_error_messages_test_cases_submodule_and_module_in_layer_be_accessed
+layer_rule_error_messages_test_cases = (
+    layer_rule_error_messages_test_cases_submodule_and_module_in_layer_access
+)
diff --git a/tests/query_language/layered_architecture_rule/__init__.py b/tests/query_language/layered_architecture_rule/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/query_language/layered_architecture_rule/test_integration.py b/tests/query_language/layered_architecture_rule/test_integration.py
new file mode 100644
index 0000000..6ab4b38
--- /dev/null
+++ b/tests/query_language/layered_architecture_rule/test_integration.py
@@ -0,0 +1,70 @@
+from __future__ import annotations
+
+import re
+from typing import Dict, List
+
+import pytest
+from integration.interesting_rules_for_tests import (
+    LayerRuleSetup,
+    LayerRuleTestCase,
+    fulfilled_layer_rule_test_cases,
+    layer_rule_error_messages_test_cases,
+)
+
+from pytestarch import EvaluableArchitecture, LayeredArchitecture, LayerRule
+
+
+def _get_layered_architecture(layers: Dict[str, List[str]]) -> LayeredArchitecture:
+    arch = LayeredArchitecture()
+
+    for layer, modules in layers.items():
+        arch.layer(layer).containing_modules(modules)
+
+    return arch
+
+
+def _get_layer_rule(rule_setup: LayerRuleSetup, arch: LayeredArchitecture) -> LayerRule:
+    rule = LayerRule().based_on(arch)
+    rule = rule.layers_that().are_named(rule_setup.importer)
+
+    behavior_fn = getattr(rule, rule_setup.behavior)
+    rule = behavior_fn()
+
+    access_type_fn = getattr(rule, rule_setup.access_type)
+    rule = access_type_fn()
+
+    if rule_setup.importee:
+        rule = rule.are_named(rule_setup.importee)
+
+    return rule
+
+
+@pytest.mark.parametrize(
+    "test_case",
+    fulfilled_layer_rule_test_cases,
+)
+def test_fulfilled_rules_as_excepted(
+    test_case: LayerRuleTestCase,
+    flat_project_1: EvaluableArchitecture,
+) -> None:
+    arch = _get_layered_architecture(test_case.layers)
+    rule = _get_layer_rule(test_case.rule_setup, arch)
+
+    rule.assert_applies(flat_project_1)
+
+
+@pytest.mark.parametrize(
+    "test_case",
+    layer_rule_error_messages_test_cases,
+)
+def test_rule_violated_raises_proper_error_message(
+    test_case: LayerRuleTestCase,
+    flat_project_1: EvaluableArchitecture,
+) -> None:
+    arch = _get_layered_architecture(test_case.layers)
+    rule = _get_layer_rule(test_case.rule_setup, arch)
+
+    with pytest.raises(
+        AssertionError, match=re.escape(test_case.expected_error_message)
+    ):
+        rule.assert_applies(flat_project_1)
diff --git a/tests/query_language/layered_architecture_rule/test_layered_architecture.py b/tests/query_language/layered_architecture_rule/test_layered_architecture.py
new file mode 100644
index 0000000..dd9e735
--- /dev/null
+++ b/tests/query_language/layered_architecture_rule/test_layered_architecture.py
@@ -0,0 +1,57 @@
+from __future__ import annotations
+
+import re
+
+import pytest
+
+from pytestarch import LayeredArchitecture
+from pytestarch.query_language.exceptions import ImproperlyConfigured
+
+
+def test_architecture_as_expected() -> None:
+    arch = (
+        LayeredArchitecture()
+        .layer("A")
+        .containing_modules("M")
+        .layer("B")
+        .containing_modules(["N", "O"])
+    )
+
+    assert str(arch) == "Layered Architecture: Layer A: [M]; Layer B: [N, O]"
+
+
+def test_layer_name_has_to_be_defined_first() -> None:
+    with pytest.raises(
+        ImproperlyConfigured, match="Specify layer name before specifying its modules."
+    ):
+        LayeredArchitecture().containing_modules("M1")
+
+
+def test_layer_name_has_to_be_unique() -> None:
+    with pytest.raises(ImproperlyConfigured, match="Layer A already exists."):
+        LayeredArchitecture().layer("A").containing_modules("M").layer("A")
+
+
+def test_layer_name_has_to_be_followed_by_modules() -> None:
+    with pytest.raises(
+        ImproperlyConfigured,
+        match=re.escape("Specify the modules of layer(s) A first."),
+    ):
+        LayeredArchitecture().layer("A").layer("B")
+
+
+def test_modules_can_only_be_defined_once_per_layer() -> None:
+    with pytest.raises(
+        ImproperlyConfigured, match="Specify layer name before specifying its modules."
+    ):
+        LayeredArchitecture().layer("A").containing_modules("M").containing_modules("N")
+
+
+def test_one_module_can_only_be_in_one_layer() -> None:
+    with pytest.raises(
+        ImproperlyConfigured,
+        match=re.escape("Module(s) M already assigned to a layer."),
+    ):
+        LayeredArchitecture().layer("A").containing_modules("M").layer(
+            "B"
+        ).containing_modules(["N", "M"])
diff --git a/tests/query_language/layered_architecture_rule/test_layered_architecture_rule.py b/tests/query_language/layered_architecture_rule/test_layered_architecture_rule.py
new file mode 100644
index 0000000..a5fe5ae
--- /dev/null
+++ b/tests/query_language/layered_architecture_rule/test_layered_architecture_rule.py
@@ -0,0 +1,16 @@
+from __future__ import annotations
+
+import pytest
+
+from pytestarch import EvaluableArchitecture, LayerRule
+from pytestarch.query_language.exceptions import ImproperlyConfigured
+
+
+def test_architecture_has_to_be_specified_first(
+    graph_based_on_string_module_names: EvaluableArchitecture,
+) -> None:
+    with pytest.raises(
+        ImproperlyConfigured,
+        match="Specify a LayeredArchitecture before defining layer behavior.",
+    ):
+        LayerRule().layers_that()
diff --git a/tests/query_language/layered_architecture_rule/test_rule_conversion.py b/tests/query_language/layered_architecture_rule/test_rule_conversion.py
new file mode 100644
index 0000000..7793a7b
--- /dev/null
+++ b/tests/query_language/layered_architecture_rule/test_rule_conversion.py
@@ -0,0 +1,215 @@
+from __future__ import annotations
+
+from pytestarch.eval_structure.evaluable_architecture import Module
+from pytestarch.query_language.layered_architecture_rule import (
+    LayeredArchitecture,
+    LayerRule,
+)
+
+MODULE_1 = "M"
+MODULE_2 = "N"
+MODULE_3 = "O"
+MODULE_4 = "P"
+MODULE_5 = "Q"
+
+LAYER_1 = "L1"
+LAYER_2 = "L2"
+
+arch = (
+    LayeredArchitecture()
+    .layer(LAYER_1)
+    .containing_modules([MODULE_1, MODULE_2])
+    .layer(LAYER_2)
+    .containing_modules([MODULE_3, MODULE_4, MODULE_5])
+)
+
+
+def test_layer_rule_subject_converted_to_layer_modules() -> None:
+    rule = LayerRule().based_on(arch).layers_that().are_named(LAYER_1)
+
+    rule_config = rule._rule._configuration
+
+    expected_modules_to_check = [
+        Module(name=MODULE_1, parent_module=None, partial_match=False),
+        Module(name=MODULE_2, parent_module=None, partial_match=False),
+    ]
+
+    assert rule_config.modules_to_check == expected_modules_to_check
+
+
+def test_layer_rule_object_converted_to_layer_modules() -> None:
+    rule = (
+        LayerRule()
+        .based_on(arch)
+        .layers_that()
+        .are_named(LAYER_1)
+        .should()
+        .access_layers_that()
+        .are_named(LAYER_2)
+    )
+
+    rule_config = rule._rule._configuration
+
+    expected_modules_to_check = [
+        Module(name=MODULE_3, parent_module=None, partial_match=False),
+        Module(name=MODULE_4, parent_module=None, partial_match=False),
+        Module(name=MODULE_5, parent_module=None, partial_match=False),
+    ]
+
+    assert rule_config.modules_to_check_against == expected_modules_to_check
+
+
+def test_should_converted_correctly() -> None:
+    rule = (
+        LayerRule()
+        .based_on(arch)
+        .layers_that()
+        .are_named(LAYER_1)
+        .should()
+        .access_layers_that()
+        .are_named(LAYER_2)
+    )
+
+    rule_config = rule._rule._configuration
+
+    assert rule_config.should
+    assert not rule_config.should_only
+    assert not rule_config.should_not
+
+
+def test_should_not_converted_correctly() -> None:
+    rule = (
+        LayerRule()
+        .based_on(arch)
+        .layers_that()
+        .are_named(LAYER_1)
+        .should_not()
+        .access_layers_that()
+        .are_named(LAYER_2)
+    )
+
+    rule_config = rule._rule._configuration
+
+    assert not rule_config.should
+    assert not rule_config.should_only
+    assert rule_config.should_not
+
+
+def test_should_only_converted_correctly() -> None:
+    rule = (
+        LayerRule()
+        .based_on(arch)
+        .layers_that()
+        .are_named(LAYER_1)
+        .should_only()
+        .access_layers_that()
+        .are_named(LAYER_2)
+    )
+
+    rule_config = rule._rule._configuration
+
+    assert not rule_config.should
+    assert rule_config.should_only
+    assert not rule_config.should_not
+
+
+def test_access_converted_correctly() -> None:
+    rule = (
+        LayerRule()
+        .based_on(arch)
+        .layers_that()
+        .are_named(LAYER_1)
+        .should()
+        .access_layers_that()
+        .are_named(LAYER_2)
+    )
+
+    rule_config = rule._rule._configuration
+
+    assert not rule_config.except_present
+    assert rule_config.import_
+
+
+def test_be_accessed_converted_correctly() -> None:
+    rule = (
+        LayerRule()
+        .based_on(arch)
+        .layers_that()
+        .are_named(LAYER_1)
+        .should()
+        .be_accessed_by_layers_that()
+        .are_named(LAYER_2)
+    )
+
+    rule_config = rule._rule._configuration
+
+    assert not rule_config.except_present
+    assert not rule_config.import_
+
+
+def test_access_except_converted_correctly() -> None:
+    rule = (
+        LayerRule()
+        .based_on(arch)
+        .layers_that()
+        .are_named(LAYER_1)
+        .should()
+        .access_layers_except_layers_that()
+        .are_named(LAYER_2)
+    )
+
+    rule_config = rule._rule._configuration
+
+    assert rule_config.except_present
+    assert rule_config.import_
+
+
+def test_be_accessed_except_converted_correctly() -> None:
+    rule = (
+        LayerRule()
+        .based_on(arch)
+        .layers_that()
+        .are_named(LAYER_1)
+        .should()
+        .be_accessed_by_layers_except_layers_that()
+        .are_named(LAYER_2)
+    )
+
+    rule_config = rule._rule._configuration
+
+    assert rule_config.except_present
+    assert not rule_config.import_
+
+
+def test_access_any_converted_correctly() -> None:
+    rule = (
+        LayerRule()
+        .based_on(arch)
+        .layers_that()
+        .are_named(LAYER_1)
+        .should()
+        .access_any_layer()
+    )
+
+    rule_config = rule._rule._configuration
+
+    assert not rule_config.except_present
+    assert rule_config.import_
+    assert rule_config.rule_object_anything
+
+
+def test_be_accessed_by_any_converted_correctly() -> None:
+    rule = (
+        LayerRule()
+        .based_on(arch)
+        .layers_that()
+        .are_named(LAYER_1)
+        .should()
+        .be_accessed_by_any_layer()
+    )
+
+    rule_config = rule._rule._configuration
+
+    assert not rule_config.except_present
+    assert not rule_config.import_
+    assert rule_config.rule_object_anything
diff --git a/tests/query_language/rule/test_rule.py b/tests/query_language/rule/test_rule.py
index 9df398c..cb49b77 100644
--- a/tests/query_language/rule/test_rule.py
+++ b/tests/query_language/rule/test_rule.py
@@ -4,8 +4,12 @@
 from query_language.rule.conftest import MODULE_1, MODULE_2
 
 from pytestarch import Rule
-from pytestarch.eval_structure.evaluable_architecture import EvaluableArchitecture
+from pytestarch.eval_structure.evaluable_architecture import (
+    EvaluableArchitecture,
+    Module,
+)
 from pytestarch.query_language.exceptions import ImproperlyConfigured
+from pytestarch.query_language.rule import RuleConfiguration
 
 
 def test_rule_to_str_as_expected() -> None:
@@ -103,3 +107,22 @@ def assert_rule_applies(rule: Rule, evaluable: EvaluableArchitecture) -> None:
 def assert_rule_does_not_apply(rule: Rule, evaluable: EvaluableArchitecture) -> None:
     with pytest.raises(AssertionError):
         rule.assert_applies(evaluable)
+
+
+def test_only_submodules_are_filtered_out() -> None:
+    module_names = ["X.a", "X.b", "X.a.a", "X.a.b", "Y"]
+    modules = [Module(name=name) for name in module_names]
+
+    rule_configuration = RuleConfiguration(modules_to_check=modules)
+
+    modules_without_submodules = (
+        Rule._get_modules_to_check_without_parent_and_submodule_combinations(
+            rule_configuration
+        )
+    )
+
+    assert list(map(lambda module: module.name, modules_without_submodules)) == [
+        "X.a",
+        "X.b",
+        "Y",
+    ]
diff --git a/tests/resources/flat_test_project_1/exporter/exporter_importer.py b/tests/resources/flat_test_project_1/exporter/exporter_importer.py
index 9748fb5..1deb540 100644
--- a/tests/resources/flat_test_project_1/exporter/exporter_importer.py
+++ b/tests/resources/flat_test_project_1/exporter/exporter_importer.py
@@ -2,4 +2,4 @@
 
 from flat_test_project_1.logging_util.logging_util_importee import l  # noqa: F401 E401
 from flat_test_project_1.model.model_importee import m  # noqa: F401 E401
-from flat_test_project_1.util.util_importer import u  # noqa: F401 E401
+from flat_test_project_1.util.util_importee import u  # noqa: F401 E401
diff --git a/tests/resources/flat_test_project_1/importer/importer_importer.py b/tests/resources/flat_test_project_1/importer/importer_importer.py
index 62a2c5d..0967ad5 100644
--- a/tests/resources/flat_test_project_1/importer/importer_importer.py
+++ b/tests/resources/flat_test_project_1/importer/importer_importer.py
@@ -1,4 +1,4 @@
 from __future__ import annotations
 
 from flat_test_project_1.model.model_importee import m  # noqa: F401 E401
-from flat_test_project_1.util.util_importer import u  # noqa: F401 E401
+from flat_test_project_1.util.util_importee import u  # noqa: F401 E401
diff --git a/tests/resources/flat_test_project_1/persistence/persistence_importer.py b/tests/resources/flat_test_project_1/persistence/persistence_importer.py
index 62a2c5d..0967ad5 100644
--- a/tests/resources/flat_test_project_1/persistence/persistence_importer.py
+++ b/tests/resources/flat_test_project_1/persistence/persistence_importer.py
@@ -1,4 +1,4 @@
 from __future__ import annotations
 
 from flat_test_project_1.model.model_importee import m  # noqa: F401 E401
-from flat_test_project_1.util.util_importer import u  # noqa: F401 E401
+from flat_test_project_1.util.util_importee import u  # noqa: F401 E401
diff --git a/tests/resources/flat_test_project_1/runtime/runtime_importer.py b/tests/resources/flat_test_project_1/runtime/runtime_importer.py
index a499929..ab26d9d 100644
--- a/tests/resources/flat_test_project_1/runtime/runtime_importer.py
+++ b/tests/resources/flat_test_project_1/runtime/runtime_importer.py
@@ -6,4 +6,4 @@
 )
 from flat_test_project_1.persistence.persistence_importee import p  # noqa: F401 E401
 from flat_test_project_1.services.services_importee import s  # noqa: F401 E401
-from flat_test_project_1.util.util_importer import u  # noqa: F401 E401
+from flat_test_project_1.util.util_importee import u  # noqa: F401 E401
diff --git a/tests/rule_assessment/test_layer_rule_violation_detector.py b/tests/rule_assessment/test_layer_rule_violation_detector.py
new file mode 100644
index 0000000..c0acd44
--- /dev/null
+++ b/tests/rule_assessment/test_layer_rule_violation_detector.py
@@ -0,0 +1,976 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import Any, Dict, List, Optional, Tuple
+
+import pytest
+from rule_assessment.test_rule_violation_detector import (
+    BEHAVIOR_EXCEPTION,
+    IMPORTEES,
+    IMPORTERS,
+    SHOULD,
+    SHOULD_EXCEPT_VIOLATIONS,
+    SHOULD_NOT,
+    SHOULD_NOT_EXCEPT_VIOLATIONS,
+    SHOULD_NOT_VIOLATIONS,
+    SHOULD_ONLY,
+    SHOULD_ONLY_EXCEPT_VIOLATIONS_FORBIDDEN_IMPORT,
+    SHOULD_ONLY_EXCEPT_VIOLATIONS_NO_IMPORT,
+    SHOULD_ONLY_VIOLATIONS_FORBIDDEN_IMPORT,
+    SHOULD_ONLY_VIOLATIONS_NO_IMPORT,
+    SHOULD_VIOLATIONS,
+    get_behavior_requirement,
+    get_module_requirement,
+)
+
+from pytestarch.eval_structure.evaluable_architecture import (
+    ExplicitlyRequestedDependenciesByBaseModules,
+    LayerMapping,
+    Module,
+    NotExplicitlyRequestedDependenciesByBaseModule,
+    StrictDependency,
+)
+from pytestarch.rule_assessment.rule_check.layer_rule_violation_detector import (
+    LayerRuleViolationDetector,
+)
+
+# layer 1
+MODULE_NAME_1 = "M1"
+MODULE_NAME_2 = "M2"
+MODULE_NAME_3 = "M3"
+
+# layer 2
+MODULE_NAME_4 = "M4"
+MODULE_NAME_5 = "M5"
+MODULE_NAME_6 = "M6"
+
+# layer 3
+MODULE_NAME_7 = "M7"
+
+# layer 4
+MODULE_NAME_8 = "M8"
+MODULE_NAME_9 = "M9"
+
+# layer 5
+MODULE_NAME_10 = "M10"
+
+MODULE_1 = Module(MODULE_NAME_1)
+MODULE_2 = Module(MODULE_NAME_2)
+MODULE_3 = Module(MODULE_NAME_3)
+
+MODULE_4 = Module(MODULE_NAME_4)
+MODULE_5 = Module(MODULE_NAME_5)
+MODULE_6 = Module(MODULE_NAME_6)
+
+MODULE_7 = Module(MODULE_NAME_7)
+
+MODULE_8 = Module(MODULE_NAME_8)
+MODULE_9 = Module(MODULE_NAME_9)
+
+
+LAYER_1 = "L1"
+LAYER_2 = "L2"
+LAYER_3 = "L3"
+LAYER_4 = "L4"
+
+LAYER_MAPPING = LayerMapping(
+    {
+        LAYER_1: [MODULE_1, MODULE_2, MODULE_3],
+        LAYER_2: [MODULE_4, MODULE_5, MODULE_6],
+        LAYER_3: [MODULE_7],
+        LAYER_4: [MODULE_8, MODULE_9],
+    }
+)
+
+
+@dataclass
+class RuleViolationDetectorTestCase:
+    behavior: Dict[str, Any]
+    expected_violation: Optional[str]
+    expected_violating_dependencies: List[StrictDependency]
+    explicitly_requested_dependencies: Optional[
+        ExplicitlyRequestedDependenciesByBaseModules
+    ]
+    not_explicitly_requested_dependencies: Optional[
+        NotExplicitlyRequestedDependenciesByBaseModule
+    ]
+    multiple_rule_objects: bool = False
+
+
+def _get_no_explicitly_requested_dependencies(
+    multiple_rule_objects: bool = False,
+) -> ExplicitlyRequestedDependenciesByBaseModules:
+    result = {
+        (MODULE_1, MODULE_4): [],
+        (MODULE_1, MODULE_5): [],
+        (MODULE_1, MODULE_6): [],
+        (MODULE_2, MODULE_4): [],
+        (MODULE_2, MODULE_5): [],
+        (MODULE_2, MODULE_6): [],
+        (MODULE_3, MODULE_4): [],
+        (MODULE_3, MODULE_5): [],
+        (MODULE_3, MODULE_6): [],
+    }
+
+    if multiple_rule_objects:
+        result.update(
+            {
+                (MODULE_1, MODULE_8): [],
+                (MODULE_1, MODULE_9): [],
+                (MODULE_2, MODULE_8): [],
+                (MODULE_2, MODULE_9): [],
+                (MODULE_3, MODULE_8): [],
+                (MODULE_3, MODULE_9): [],
+            }
+        )
+
+    return result
+
+
+def _get_explicitly_requested_dependencies(
+    dependencies: List[StrictDependency], multiple_rule_objects: bool = False
+) -> ExplicitlyRequestedDependenciesByBaseModules:
+    no_dependencies = _get_no_explicitly_requested_dependencies(multiple_rule_objects)
+
+    for dependency in dependencies:
+        no_dependencies[dependency] = [dependency]
+
+    return no_dependencies
+
+
+def _get_all_violating_dependencies(
+    objects: Tuple[str] = (LAYER_2,),
+) -> List[StrictDependency]:
+    return [
+        (single_subject, single_object)
+        for o in objects
+        for single_subject in LAYER_MAPPING.get_modules(LAYER_1)
+        for single_object in LAYER_MAPPING.get_modules(o)
+    ]
+
+
+def _get_no_not_explicitly_requested_dependencies() -> (
+    NotExplicitlyRequestedDependenciesByBaseModule
+):
+    return {MODULE_1: [], MODULE_2: [], MODULE_3: []}
+
+
+def _get_not_explicitly_requested_dependencies(
+    dependencies: List[StrictDependency],
+) -> NotExplicitlyRequestedDependenciesByBaseModule:
+    no_dependencies = _get_no_not_explicitly_requested_dependencies()
+
+    for dependency in dependencies:
+        no_dependencies[dependency[0]] = [dependency]
+
+    return no_dependencies
+
+
+test_cases = [
+    # single rule subject single rule object
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD: True},
+            SHOULD_VIOLATIONS,
+            [],
+            _get_explicitly_requested_dependencies([(MODULE_1, MODULE_5)]),
+            _get_no_not_explicitly_requested_dependencies(),
+        ),
+        id="should_fulfilled_one_import_ssso",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD: True},
+            SHOULD_VIOLATIONS,
+            [],
+            _get_explicitly_requested_dependencies(
+                [(MODULE_1, MODULE_5), (MODULE_2, MODULE_6)]
+            ),
+            _get_no_not_explicitly_requested_dependencies(),
+        ),
+        id="should_fulfilled_multiple_imports_ssso",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD: True},
+            SHOULD_VIOLATIONS,
+            _get_all_violating_dependencies(),
+            _get_no_explicitly_requested_dependencies(),
+            _get_no_not_explicitly_requested_dependencies(),
+        ),
+        id="should_violated_no_imports_ssso",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD: True},
+            SHOULD_VIOLATIONS,
+            _get_all_violating_dependencies(),
+            _get_no_explicitly_requested_dependencies(),
+            _get_not_explicitly_requested_dependencies([(MODULE_1, MODULE_7)]),
+        ),
+        id="should_violated_import_of_other_layer_ssso",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_NOT: True}, SHOULD_NOT_VIOLATIONS, [], {}, {}
+        ),
+        id="should_not_fulfilled_no_import_ssso",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_NOT: True},
+            SHOULD_NOT_VIOLATIONS,
+            [],
+            {},
+            _get_not_explicitly_requested_dependencies([(MODULE_2, MODULE_7)]),
+        ),
+        id="should_not_fulfilled_import_of_other_layer_ssso",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_NOT: True},
+            SHOULD_NOT_VIOLATIONS,
+            [(MODULE_1, MODULE_6)],
+            _get_explicitly_requested_dependencies([(MODULE_1, MODULE_6)]),
+            _get_no_not_explicitly_requested_dependencies(),
+        ),
+        id="should_not_violated_one_import_ssso",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_NOT: True},
+            SHOULD_NOT_VIOLATIONS,
+            [(MODULE_1, MODULE_6), (MODULE_2, MODULE_5), (MODULE_3, MODULE_6)],
+            _get_explicitly_requested_dependencies(
+                [(MODULE_1, MODULE_6), (MODULE_2, MODULE_5), (MODULE_3, MODULE_6)]
+            ),
+            _get_no_not_explicitly_requested_dependencies(),
+        ),
+        id="should_not_violated_multiple_imports_ssso",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_ONLY: True},
+            SHOULD_ONLY_VIOLATIONS_NO_IMPORT,
+            [],
+            _get_explicitly_requested_dependencies([(MODULE_1, MODULE_4)]),
+            _get_no_not_explicitly_requested_dependencies(),
+        ),
+        id="should_only_fulfilled_ssso",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_ONLY: True},
+            SHOULD_ONLY_VIOLATIONS_NO_IMPORT,
+            [],
+            _get_explicitly_requested_dependencies(
+                [(MODULE_1, MODULE_4), (MODULE_1, MODULE_5)]
+            ),
+            _get_no_not_explicitly_requested_dependencies(),
+        ),
+        id="should_only_fulfilled_multiple_imports_ssso",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_ONLY: True},
+            SHOULD_ONLY_VIOLATIONS_FORBIDDEN_IMPORT,
+            [],
+            _get_explicitly_requested_dependencies([(MODULE_1, MODULE_4)]),
+            _get_no_not_explicitly_requested_dependencies(),
+        ),
+        id="should_only_fulfilled_ssso",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_ONLY: True},
+            SHOULD_ONLY_VIOLATIONS_FORBIDDEN_IMPORT,
+            [],
+            _get_explicitly_requested_dependencies(
+                [(MODULE_1, MODULE_4), (MODULE_1, MODULE_5)]
+            ),
+            _get_no_not_explicitly_requested_dependencies(),
+        ),
+        id="should_only_fulfilled_multiple_imports_ssso",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_ONLY: True},
+            SHOULD_ONLY_VIOLATIONS_NO_IMPORT,
+            _get_all_violating_dependencies(),
+            _get_no_explicitly_requested_dependencies(),
+            _get_no_not_explicitly_requested_dependencies(),
+        ),
+        id="should_only_violated_no_imports_ssso",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_ONLY: True},
+            SHOULD_ONLY_VIOLATIONS_FORBIDDEN_IMPORT,
+            [(MODULE_3, MODULE_7)],
+            _get_explicitly_requested_dependencies([(MODULE_3, MODULE_4)]),
+            _get_not_explicitly_requested_dependencies([(MODULE_3, MODULE_7)]),
+        ),
+        id="should_only_violated_one_forbidden_import_ssso",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_ONLY: True},
+            SHOULD_ONLY_VIOLATIONS_FORBIDDEN_IMPORT,
+            [(MODULE_1, MODULE_7), (MODULE_3, MODULE_7)],
+            _get_explicitly_requested_dependencies([(MODULE_3, MODULE_4)]),
+            _get_not_explicitly_requested_dependencies(
+                [(MODULE_3, MODULE_7), (MODULE_1, MODULE_7)]
+            ),
+        ),
+        id="should_only_violated_multiple_forbidden_imports_ssso",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD: True, BEHAVIOR_EXCEPTION: True},
+            SHOULD_EXCEPT_VIOLATIONS,
+            [],
+            _get_no_explicitly_requested_dependencies(),
+            _get_not_explicitly_requested_dependencies([(MODULE_2, MODULE_5)]),
+        ),
+        id="should_except_fulfilled_ssso",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD: True, BEHAVIOR_EXCEPTION: True},
+            SHOULD_EXCEPT_VIOLATIONS,
+            [],
+            _get_no_explicitly_requested_dependencies(),
+            _get_not_explicitly_requested_dependencies(
+                [(MODULE_2, MODULE_5), (MODULE_3, MODULE_4)]
+            ),
+        ),
+        id="should_except_fulfilled_multiple_other_imports_ssso",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD: True, BEHAVIOR_EXCEPTION: True},
+            SHOULD_EXCEPT_VIOLATIONS,
+            [],
+            _get_explicitly_requested_dependencies([(MODULE_1, MODULE_4)]),
+            _get_not_explicitly_requested_dependencies([(MODULE_2, MODULE_5)]),
+        ),
+        id="should_except_fulfilled_import_ssso",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD: True, BEHAVIOR_EXCEPTION: True},
+            SHOULD_EXCEPT_VIOLATIONS,
+            _get_all_violating_dependencies(),
+            _get_no_explicitly_requested_dependencies(),
+            _get_no_not_explicitly_requested_dependencies(),
+        ),
+        id="should_except_violated_no_imports_ssso",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD: True, BEHAVIOR_EXCEPTION: True},
+            SHOULD_EXCEPT_VIOLATIONS,
+            _get_all_violating_dependencies(),
+            _get_explicitly_requested_dependencies([(MODULE_1, MODULE_6)]),
+            _get_no_not_explicitly_requested_dependencies(),
+        ),
+        id="should_except_violated_no_other_import_ssso",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_NOT: True, BEHAVIOR_EXCEPTION: True},
+            SHOULD_NOT_EXCEPT_VIOLATIONS,
+            [],
+            _get_no_explicitly_requested_dependencies(),
+            _get_no_not_explicitly_requested_dependencies(),
+        ),
+        id="should_not_except_fulfilled_no_imports_ssso",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_NOT: True, BEHAVIOR_EXCEPTION: True},
+            SHOULD_NOT_EXCEPT_VIOLATIONS,
+            [],
+            _get_explicitly_requested_dependencies([(MODULE_1, MODULE_6)]),
+            _get_no_not_explicitly_requested_dependencies(),
+        ),
+        id="should_not_except_fulfilled_ssso",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_NOT: True, BEHAVIOR_EXCEPTION: True},
+            SHOULD_NOT_EXCEPT_VIOLATIONS,
+            [(MODULE_2, MODULE_7)],
+            _get_no_explicitly_requested_dependencies(),
+            _get_not_explicitly_requested_dependencies([(MODULE_2, MODULE_7)]),
+        ),
+        id="should_not_except_violated_no_allowed_imports_ssso",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_NOT: True, BEHAVIOR_EXCEPTION: True},
+            SHOULD_NOT_EXCEPT_VIOLATIONS,
+            [(MODULE_2, MODULE_7)],
+            _get_explicitly_requested_dependencies([(MODULE_1, MODULE_5)]),
+            _get_not_explicitly_requested_dependencies([(MODULE_2, MODULE_7)]),
+        ),
+        id="should_not_except_violated_allowed_imports_ssso",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_ONLY: True, BEHAVIOR_EXCEPTION: True},
+            SHOULD_ONLY_EXCEPT_VIOLATIONS_NO_IMPORT,
+            [],
+            _get_no_explicitly_requested_dependencies(),
+            _get_not_explicitly_requested_dependencies([(MODULE_2, MODULE_7)]),
+        ),
+        id="should_only_except_fulfilled_ssso",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_ONLY: True, BEHAVIOR_EXCEPTION: True},
+            SHOULD_ONLY_EXCEPT_VIOLATIONS_NO_IMPORT,
+            [],
+            _get_no_explicitly_requested_dependencies(),
+            _get_not_explicitly_requested_dependencies(
+                [(MODULE_2, MODULE_7), (MODULE_3, MODULE_7)]
+            ),
+        ),
+        id="should_only_except_fulfilled_multiple_imports_ssso",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_ONLY: True, BEHAVIOR_EXCEPTION: True},
+            SHOULD_ONLY_EXCEPT_VIOLATIONS_FORBIDDEN_IMPORT,
+            [],
+            _get_no_explicitly_requested_dependencies(),
+            _get_not_explicitly_requested_dependencies([(MODULE_2, MODULE_7)]),
+        ),
+        id="should_only_except_fulfilled_ssso",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_ONLY: True, BEHAVIOR_EXCEPTION: True},
+            SHOULD_ONLY_EXCEPT_VIOLATIONS_FORBIDDEN_IMPORT,
+            [],
+            _get_no_explicitly_requested_dependencies(),
+            _get_not_explicitly_requested_dependencies(
+                [(MODULE_2, MODULE_7), (MODULE_3, MODULE_7)]
+            ),
+        ),
+        id="should_only_except_fulfilled_multiple_imports_ssso",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_ONLY: True, BEHAVIOR_EXCEPTION: True},
+            SHOULD_ONLY_EXCEPT_VIOLATIONS_NO_IMPORT,
+            _get_all_violating_dependencies(),
+            _get_no_explicitly_requested_dependencies(),
+            _get_no_not_explicitly_requested_dependencies(),
+        ),
+        id="should_only_except_violated_no_imports_ssso",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_ONLY: True, BEHAVIOR_EXCEPTION: True},
+            SHOULD_ONLY_EXCEPT_VIOLATIONS_FORBIDDEN_IMPORT,
+            [(MODULE_1, MODULE_4)],
+            _get_explicitly_requested_dependencies([(MODULE_1, MODULE_4)]),
+            _get_not_explicitly_requested_dependencies([(MODULE_1, MODULE_7)]),
+        ),
+        id="should_only_except_violated_one_forbidden_import_ssso",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_ONLY: True, BEHAVIOR_EXCEPTION: True},
+            SHOULD_ONLY_EXCEPT_VIOLATIONS_FORBIDDEN_IMPORT,
+            [(MODULE_1, MODULE_4), (MODULE_1, MODULE_6)],
+            _get_explicitly_requested_dependencies(
+                [(MODULE_1, MODULE_4), (MODULE_1, MODULE_6)]
+            ),
+            _get_not_explicitly_requested_dependencies([(MODULE_1, MODULE_7)]),
+        ),
+        id="should_only_except_violated_multiple_forbidden_imports_ssso",
+    ),
+    # single rule subject multiple rule objects
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD: True},
+            SHOULD_VIOLATIONS,
+            [],
+            _get_explicitly_requested_dependencies(
+                [(MODULE_1, MODULE_5), (MODULE_1, MODULE_8)], True
+            ),
+            _get_no_not_explicitly_requested_dependencies(),
+            True,
+        ),
+        id="should_fulfilled_one_import_each_ssmo",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD: True},
+            SHOULD_VIOLATIONS,
+            [],
+            _get_explicitly_requested_dependencies(
+                [(MODULE_1, MODULE_5), (MODULE_2, MODULE_6), (MODULE_1, MODULE_8)], True
+            ),
+            _get_no_not_explicitly_requested_dependencies(),
+            True,
+        ),
+        id="should_fulfilled_multiple_imports_ssmo",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD: True},
+            SHOULD_VIOLATIONS,
+            _get_all_violating_dependencies(objects=(LAYER_2, LAYER_4)),
+            _get_no_explicitly_requested_dependencies(True),
+            _get_no_not_explicitly_requested_dependencies(),
+            True,
+        ),
+        id="should_violated_no_imports_ssmo",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD: True},
+            SHOULD_VIOLATIONS,
+            _get_all_violating_dependencies(objects=(LAYER_4,)),
+            _get_explicitly_requested_dependencies([(MODULE_1, MODULE_5)], True),
+            _get_no_not_explicitly_requested_dependencies(),
+            True,
+        ),
+        id="should_violated_only_one_rule_object_import_ssmo",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD: True},
+            SHOULD_VIOLATIONS,
+            _get_all_violating_dependencies(objects=(LAYER_2, LAYER_4)),
+            _get_no_explicitly_requested_dependencies(True),
+            _get_not_explicitly_requested_dependencies([(MODULE_1, MODULE_7)]),
+            True,
+        ),
+        id="should_violated_import_of_other_layer_ssmo",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_NOT: True}, SHOULD_NOT_VIOLATIONS, [], {}, {}
+        ),
+        id="should_not_fulfilled_no_import_ssmo",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_NOT: True},
+            SHOULD_NOT_VIOLATIONS,
+            [],
+            {},
+            _get_not_explicitly_requested_dependencies([(MODULE_2, MODULE_7)]),
+            True,
+        ),
+        id="should_not_fulfilled_import_of_other_layer_ssmo",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_NOT: True},
+            SHOULD_NOT_VIOLATIONS,
+            [(MODULE_1, MODULE_6)],
+            _get_explicitly_requested_dependencies([(MODULE_1, MODULE_6)], True),
+            _get_no_not_explicitly_requested_dependencies(),
+            True,
+        ),
+        id="should_not_violated_one_import_ssmo",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_NOT: True},
+            SHOULD_NOT_VIOLATIONS,
+            [(MODULE_1, MODULE_6), (MODULE_3, MODULE_9)],
+            _get_explicitly_requested_dependencies(
+                [(MODULE_1, MODULE_6), (MODULE_3, MODULE_9)], True
+            ),
+            _get_no_not_explicitly_requested_dependencies(),
+            True,
+        ),
+        id="should_not_violated_one_import_each_ssmo",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_NOT: True},
+            SHOULD_NOT_VIOLATIONS,
+            [(MODULE_1, MODULE_6), (MODULE_2, MODULE_5), (MODULE_3, MODULE_6)],
+            _get_explicitly_requested_dependencies(
+                [(MODULE_1, MODULE_6), (MODULE_2, MODULE_5), (MODULE_3, MODULE_6)], True
+            ),
+            _get_no_not_explicitly_requested_dependencies(),
+            True,
+        ),
+        id="should_not_violated_multiple_imports_ssmo",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_NOT: True},
+            SHOULD_NOT_VIOLATIONS,
+            [
+                (MODULE_1, MODULE_6),
+                (MODULE_2, MODULE_5),
+                (MODULE_3, MODULE_6),
+                (MODULE_2, MODULE_8),
+            ],
+            _get_explicitly_requested_dependencies(
+                [
+                    (MODULE_1, MODULE_6),
+                    (MODULE_2, MODULE_5),
+                    (MODULE_3, MODULE_6),
+                    (MODULE_2, MODULE_8),
+                ],
+                True,
+            ),
+            _get_no_not_explicitly_requested_dependencies(),
+            True,
+        ),
+        id="should_not_violated_multiple_imports_each_ssmo",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_ONLY: True},
+            SHOULD_ONLY_VIOLATIONS_NO_IMPORT,
+            [],
+            _get_explicitly_requested_dependencies(
+                [(MODULE_1, MODULE_4), (MODULE_2, MODULE_9)], True
+            ),
+            _get_no_not_explicitly_requested_dependencies(),
+            True,
+        ),
+        id="should_only_fulfilled_ssmo",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_ONLY: True},
+            SHOULD_ONLY_VIOLATIONS_NO_IMPORT,
+            [],
+            _get_explicitly_requested_dependencies(
+                [(MODULE_1, MODULE_4), (MODULE_1, MODULE_5), (MODULE_1, MODULE_8)], True
+            ),
+            _get_no_not_explicitly_requested_dependencies(),
+            True,
+        ),
+        id="should_only_fulfilled_multiple_imports_ssmo",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_ONLY: True},
+            SHOULD_ONLY_VIOLATIONS_NO_IMPORT,
+            _get_all_violating_dependencies(objects=(LAYER_2, LAYER_4)),
+            _get_no_explicitly_requested_dependencies(True),
+            _get_no_not_explicitly_requested_dependencies(),
+            True,
+        ),
+        id="should_only_violated_no_imports_ssmo",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_ONLY: True},
+            SHOULD_ONLY_VIOLATIONS_NO_IMPORT,
+            _get_all_violating_dependencies(objects=(LAYER_4,)),
+            _get_explicitly_requested_dependencies([(MODULE_1, MODULE_4)], True),
+            _get_no_not_explicitly_requested_dependencies(),
+            True,
+        ),
+        id="should_only_violated_no_imports_of_one_ssmo",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_ONLY: True},
+            SHOULD_ONLY_VIOLATIONS_FORBIDDEN_IMPORT,
+            [(MODULE_3, MODULE_7)],
+            _get_explicitly_requested_dependencies(
+                [(MODULE_3, MODULE_4), (MODULE_3, MODULE_9)], True
+            ),
+            _get_not_explicitly_requested_dependencies([(MODULE_3, MODULE_7)]),
+            True,
+        ),
+        id="should_only_violated_one_forbidden_import_ssmo",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_ONLY: True},
+            SHOULD_ONLY_VIOLATIONS_FORBIDDEN_IMPORT,
+            [(MODULE_1, MODULE_7), (MODULE_3, MODULE_7)],
+            _get_explicitly_requested_dependencies(
+                [(MODULE_3, MODULE_4), (MODULE_3, MODULE_8)], True
+            ),
+            _get_not_explicitly_requested_dependencies(
+                [(MODULE_3, MODULE_7), (MODULE_1, MODULE_7)]
+            ),
+            True,
+        ),
+        id="should_only_violated_multiple_forbidden_imports_ssmo",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD: True, BEHAVIOR_EXCEPTION: True},
+            SHOULD_EXCEPT_VIOLATIONS,
+            [],
+            _get_no_explicitly_requested_dependencies(True),
+            _get_not_explicitly_requested_dependencies([(MODULE_2, MODULE_5)]),
+            True,
+        ),
+        id="should_except_fulfilled_ssmo",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD: True, BEHAVIOR_EXCEPTION: True},
+            SHOULD_EXCEPT_VIOLATIONS,
+            [],
+            _get_no_explicitly_requested_dependencies(True),
+            _get_not_explicitly_requested_dependencies(
+                [(MODULE_2, MODULE_5), (MODULE_3, MODULE_4), (MODULE_3, MODULE_9)]
+            ),
+            True,
+        ),
+        id="should_except_fulfilled_multiple_other_imports_ssmo",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD: True, BEHAVIOR_EXCEPTION: True},
+            SHOULD_EXCEPT_VIOLATIONS,
+            [],
+            _get_explicitly_requested_dependencies([(MODULE_1, MODULE_4)], True),
+            _get_not_explicitly_requested_dependencies([(MODULE_2, MODULE_5)]),
+            True,
+        ),
+        id="should_except_fulfilled_import_one_ssmo",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD: True, BEHAVIOR_EXCEPTION: True},
+            SHOULD_EXCEPT_VIOLATIONS,
+            [],
+            _get_explicitly_requested_dependencies(
+                [(MODULE_1, MODULE_4), (MODULE_1, MODULE_8)], True
+            ),
+            _get_not_explicitly_requested_dependencies([(MODULE_2, MODULE_5)]),
+            True,
+        ),
+        id="should_except_fulfilled_import_both_ssmo",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD: True, BEHAVIOR_EXCEPTION: True},
+            SHOULD_EXCEPT_VIOLATIONS,
+            _get_all_violating_dependencies(objects=(LAYER_2, LAYER_4)),
+            _get_no_explicitly_requested_dependencies(True),
+            _get_no_not_explicitly_requested_dependencies(),
+            True,
+        ),
+        id="should_except_violated_no_imports_ssmo",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD: True, BEHAVIOR_EXCEPTION: True},
+            SHOULD_EXCEPT_VIOLATIONS,
+            _get_all_violating_dependencies(objects=(LAYER_2, LAYER_4)),
+            _get_explicitly_requested_dependencies([(MODULE_1, MODULE_9)], True),
+            _get_no_not_explicitly_requested_dependencies(),
+            True,
+        ),
+        id="should_except_violated_import_of_only_one_ssmo",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD: True, BEHAVIOR_EXCEPTION: True},
+            SHOULD_EXCEPT_VIOLATIONS,
+            _get_all_violating_dependencies(objects=(LAYER_2, LAYER_4)),
+            _get_explicitly_requested_dependencies([(MODULE_1, MODULE_6)], True),
+            _get_no_not_explicitly_requested_dependencies(),
+            True,
+        ),
+        id="should_except_violated_no_other_import_one_ssmo",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD: True, BEHAVIOR_EXCEPTION: True},
+            SHOULD_EXCEPT_VIOLATIONS,
+            _get_all_violating_dependencies(objects=(LAYER_2, LAYER_4)),
+            _get_explicitly_requested_dependencies(
+                [(MODULE_1, MODULE_6), (MODULE_1, MODULE_9)], True
+            ),
+            _get_no_not_explicitly_requested_dependencies(),
+            True,
+        ),
+        id="should_except_violated_no_other_import_both_ssmo",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_NOT: True, BEHAVIOR_EXCEPTION: True},
+            SHOULD_NOT_EXCEPT_VIOLATIONS,
+            [],
+            _get_no_explicitly_requested_dependencies(True),
+            _get_no_not_explicitly_requested_dependencies(),
+            True,
+        ),
+        id="should_not_except_fulfilled_no_imports_ssmo",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_NOT: True, BEHAVIOR_EXCEPTION: True},
+            SHOULD_NOT_EXCEPT_VIOLATIONS,
+            [],
+            _get_explicitly_requested_dependencies([(MODULE_1, MODULE_6)], True),
+            _get_no_not_explicitly_requested_dependencies(),
+            True,
+        ),
+        id="should_not_except_fulfilled_one_ssmo",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_NOT: True, BEHAVIOR_EXCEPTION: True},
+            SHOULD_NOT_EXCEPT_VIOLATIONS,
+            [],
+            _get_explicitly_requested_dependencies(
+                [(MODULE_1, MODULE_6), (MODULE_2, MODULE_8)], True
+            ),
+            _get_no_not_explicitly_requested_dependencies(),
+            True,
+        ),
+        id="should_not_except_fulfilled_both_ssmo",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_NOT: True, BEHAVIOR_EXCEPTION: True},
+            SHOULD_NOT_EXCEPT_VIOLATIONS,
+            [(MODULE_2, MODULE_7)],
+            _get_no_explicitly_requested_dependencies(True),
+            _get_not_explicitly_requested_dependencies([(MODULE_2, MODULE_7)]),
+            True,
+        ),
+        id="should_not_except_violated_no_allowed_imports_ssmo",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_NOT: True, BEHAVIOR_EXCEPTION: True},
+            SHOULD_NOT_EXCEPT_VIOLATIONS,
+            [(MODULE_2, MODULE_7)],
+            _get_explicitly_requested_dependencies([(MODULE_1, MODULE_5)], True),
+            _get_not_explicitly_requested_dependencies([(MODULE_2, MODULE_7)]),
+            True,
+        ),
+        id="should_not_except_violated_one_imported_allowed_imports_ssmo",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_NOT: True, BEHAVIOR_EXCEPTION: True},
+            SHOULD_NOT_EXCEPT_VIOLATIONS,
+            [(MODULE_2, MODULE_7)],
+            _get_explicitly_requested_dependencies(
+                [(MODULE_1, MODULE_5), (MODULE_2, MODULE_8)], True
+            ),
+            _get_not_explicitly_requested_dependencies([(MODULE_2, MODULE_7)]),
+            True,
+        ),
+        id="should_not_except_violated_both_imported_allowed_imports_ssmo",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_ONLY: True, BEHAVIOR_EXCEPTION: True},
+            SHOULD_ONLY_EXCEPT_VIOLATIONS_NO_IMPORT,
+            [],
+            _get_no_explicitly_requested_dependencies(True),
+            _get_not_explicitly_requested_dependencies([(MODULE_2, MODULE_7)]),
+            True,
+        ),
+        id="should_only_except_fulfilled_ssmo",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_ONLY: True, BEHAVIOR_EXCEPTION: True},
+            SHOULD_ONLY_EXCEPT_VIOLATIONS_NO_IMPORT,
+            [],
+            _get_no_explicitly_requested_dependencies(True),
+            _get_not_explicitly_requested_dependencies(
+                [(MODULE_2, MODULE_7), (MODULE_3, MODULE_7)]
+            ),
+            True,
+        ),
+        id="should_only_except_fulfilled_multiple_imports_ssmo",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_ONLY: True, BEHAVIOR_EXCEPTION: True},
+            SHOULD_ONLY_EXCEPT_VIOLATIONS_NO_IMPORT,
+            _get_all_violating_dependencies(objects=(LAYER_2, LAYER_4)),
+            _get_no_explicitly_requested_dependencies(True),
+            _get_no_not_explicitly_requested_dependencies(),
+            True,
+        ),
+        id="should_only_except_violated_no_imports_ssmo",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_ONLY: True, BEHAVIOR_EXCEPTION: True},
+            SHOULD_ONLY_EXCEPT_VIOLATIONS_FORBIDDEN_IMPORT,
+            [(MODULE_1, MODULE_4)],
+            _get_explicitly_requested_dependencies([(MODULE_1, MODULE_4)], True),
+            _get_not_explicitly_requested_dependencies([(MODULE_1, MODULE_7)]),
+            True,
+        ),
+        id="should_only_except_violated_one_forbidden_import_ssmo",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_ONLY: True, BEHAVIOR_EXCEPTION: True},
+            SHOULD_ONLY_EXCEPT_VIOLATIONS_FORBIDDEN_IMPORT,
+            [(MODULE_1, MODULE_4), (MODULE_1, MODULE_6)],
+            _get_explicitly_requested_dependencies(
+                [(MODULE_1, MODULE_4), (MODULE_1, MODULE_6)], True
+            ),
+            _get_not_explicitly_requested_dependencies([(MODULE_1, MODULE_7)]),
+            True,
+        ),
+        id="should_only_except_violated_multiple_forbidden_imports_ssmo",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_ONLY: True, BEHAVIOR_EXCEPTION: True},
+            SHOULD_ONLY_EXCEPT_VIOLATIONS_FORBIDDEN_IMPORT,
+            [(MODULE_1, MODULE_4), (MODULE_1, MODULE_8)],
+            _get_explicitly_requested_dependencies(
+                [(MODULE_1, MODULE_4), (MODULE_1, MODULE_8)], True
+            ),
+            _get_not_explicitly_requested_dependencies([(MODULE_1, MODULE_7)]),
+            True,
+        ),
+        id="should_only_except_violated_multiple_forbidden_imports_in_both_objects_ssmo",
+    ),
+]
+
+
+@pytest.mark.parametrize("test_case", test_cases)
+def test_rule_violation_detection_as_expected(
+    test_case: RuleViolationDetectorTestCase,
+) -> None:
+    behavior_requirement = get_behavior_requirement(**test_case.behavior)
+    importees = [MODULE_4, MODULE_5, MODULE_6]
+
+    if test_case.multiple_rule_objects:
+        importees.extend([MODULE_8, MODULE_9])
+
+    module_requirement = get_module_requirement(
+        **{
+            IMPORTERS: [MODULE_1, MODULE_2, MODULE_3],
+            IMPORTEES: importees,
+        }
+    )
+
+    detector = LayerRuleViolationDetector(
+        module_requirement, behavior_requirement, LAYER_MAPPING
+    )
+
+    violations = detector.get_rule_violation(
+        test_case.explicitly_requested_dependencies,
+        test_case.not_explicitly_requested_dependencies,
+    )
+
+    assert set(getattr(violations, test_case.expected_violation)) == set(
+        test_case.expected_violating_dependencies
+    )
diff --git a/tests/rule_assessment/test_rule_violation_detector.py b/tests/rule_assessment/test_rule_violation_detector.py
new file mode 100644
index 0000000..19db00b
--- /dev/null
+++ b/tests/rule_assessment/test_rule_violation_detector.py
@@ -0,0 +1,370 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import Any, Dict, List, Optional
+
+import pytest
+
+from pytestarch.eval_structure.evaluable_architecture import (
+    ExplicitlyRequestedDependenciesByBaseModules,
+    Module,
+    NotExplicitlyRequestedDependenciesByBaseModule,
+    StrictDependency,
+)
+from pytestarch.rule_assessment.rule_check.behavior_requirement import (
+    BehaviorRequirement,
+)
+from pytestarch.rule_assessment.rule_check.module_requirement import ModuleRequirement
+from pytestarch.rule_assessment.rule_check.rule_violation_detector import (
+    RuleViolationDetector,
+)
+
+IMPORTER_SPECIFIED_AS_RULE_SUBJECT = "importer_specified_as_rule_subject"
+IMPORTEES = "importees"
+IMPORTERS = "importers"
+
+BEHAVIOR_EXCEPTION = "behavior_exception"
+SHOULD_ONLY = "should_only"
+SHOULD_NOT = "should_not"
+SHOULD = "should"
+
+SHOULD_VIOLATIONS = "should_violations"
+SHOULD_NOT_VIOLATIONS = "should_not_violations"
+SHOULD_ONLY_VIOLATIONS_NO_IMPORT = "should_only_violations_by_no_import"
+SHOULD_ONLY_VIOLATIONS_FORBIDDEN_IMPORT = "should_only_violations_by_forbidden_import"
+SHOULD_EXCEPT_VIOLATIONS = "should_except_violations"
+SHOULD_NOT_EXCEPT_VIOLATIONS = "should_not_except_violations"
+SHOULD_ONLY_EXCEPT_VIOLATIONS_NO_IMPORT = "should_only_except_violations_by_no_import"
+SHOULD_ONLY_EXCEPT_VIOLATIONS_FORBIDDEN_IMPORT = (
+    "should_only_except_violations_by_forbidden_import"
+)
+
+MODULE_NAME_1 = "M1"
+MODULE_NAME_2 = "M2"
+MODULE_NAME_3 = "M3"
+
+MODULE_1 = Module(MODULE_NAME_1)
+MODULE_2 = Module(MODULE_NAME_2)
+MODULE_3 = Module(MODULE_NAME_3)
+
+
+def get_behavior_requirement(**kwargs) -> BehaviorRequirement:
+    if SHOULD not in kwargs:
+        kwargs[SHOULD] = False
+
+    if SHOULD_NOT not in kwargs:
+        kwargs[SHOULD_NOT] = False
+
+    if SHOULD_ONLY not in kwargs:
+        kwargs[SHOULD_ONLY] = False
+
+    if BEHAVIOR_EXCEPTION not in kwargs:
+        kwargs[BEHAVIOR_EXCEPTION] = False
+
+    return BehaviorRequirement(**kwargs)
+
+
+def get_module_requirement(**kwargs) -> ModuleRequirement:
+    if IMPORTERS not in kwargs:
+        kwargs[IMPORTERS] = [MODULE_1]
+
+    if IMPORTEES not in kwargs:
+        kwargs[IMPORTEES] = [MODULE_2]
+
+    if IMPORTER_SPECIFIED_AS_RULE_SUBJECT not in kwargs:
+        kwargs[IMPORTER_SPECIFIED_AS_RULE_SUBJECT] = True
+
+    return ModuleRequirement(**kwargs)
+
+
+@dataclass
+class RuleViolationDetectorTestCase:
+    behavior: Dict[str, Any]
+    expected_violation: Optional[str]
+    expected_violating_dependencies: List[StrictDependency]
+    explicitly_requested_dependencies: Optional[
+        ExplicitlyRequestedDependenciesByBaseModules
+    ]
+    not_explicitly_requested_dependencies: Optional[
+        NotExplicitlyRequestedDependenciesByBaseModule
+    ]
+
+
+test_cases = [
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD: True},
+            SHOULD_VIOLATIONS,
+            [],
+            {(MODULE_1, MODULE_2): [(MODULE_1, MODULE_2)]},
+            {MODULE_1: []},
+        ),
+        id="should_fulfilled",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase({SHOULD: False}, SHOULD_VIOLATIONS, [], {}, {}),
+        id="should_violated_not_required",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD: True},
+            SHOULD_VIOLATIONS,
+            [(MODULE_1, MODULE_2)],
+            {(MODULE_1, MODULE_2): []},
+            {MODULE_1: []},
+        ),
+        id="should_violated",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_NOT: True},
+            SHOULD_NOT_VIOLATIONS,
+            [],
+            {(MODULE_1, MODULE_2): []},
+            {MODULE_1: []},
+        ),
+        id="should_not_fulfilled",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_NOT: False},
+            SHOULD_NOT_VIOLATIONS,
+            [],
+            {(MODULE_1, MODULE_2): [(MODULE_1, MODULE_2)]},
+            {MODULE_1: []},
+        ),
+        id="should_not_violated_not_required",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_NOT: True},
+            SHOULD_NOT_VIOLATIONS,
+            [(MODULE_1, MODULE_2)],
+            {(MODULE_1, MODULE_2): [(MODULE_1, MODULE_2)]},
+            {MODULE_1: []},
+        ),
+        id="should_not_violated",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_ONLY: True},
+            SHOULD_ONLY_VIOLATIONS_NO_IMPORT,
+            [],
+            {(MODULE_1, MODULE_2): [(MODULE_1, MODULE_2)]},
+            {MODULE_1: []},
+        ),
+        id="should_only_fulfilled",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_ONLY: False},
+            SHOULD_ONLY_VIOLATIONS_NO_IMPORT,
+            [],
+            {(MODULE_1, MODULE_2): []},
+            {MODULE_1: []},
+        ),
+        id="should_only_no_import_violated_not_required",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_ONLY: True},
+            SHOULD_ONLY_VIOLATIONS_NO_IMPORT,
+            [(MODULE_1, MODULE_2)],
+            {(MODULE_1, MODULE_2): []},
+            {MODULE_1: []},
+        ),
+        id="should_only_no_import_violated",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_ONLY: False},
+            SHOULD_ONLY_VIOLATIONS_FORBIDDEN_IMPORT,
+            [],
+            {(MODULE_1, MODULE_2): [(MODULE_1, MODULE_2)]},
+            {MODULE_1: [(MODULE_1, MODULE_3)]},
+        ),
+        id="should_only_no_forbidden_import_violated_not_required",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_ONLY: True},
+            SHOULD_ONLY_VIOLATIONS_FORBIDDEN_IMPORT,
+            [(MODULE_1, MODULE_3)],
+            {(MODULE_1, MODULE_2): [(MODULE_1, MODULE_2)]},
+            {MODULE_1: [(MODULE_1, MODULE_3)]},
+        ),
+        id="should_only_no_forbidden_import_violated",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD: True, BEHAVIOR_EXCEPTION: True},
+            SHOULD_EXCEPT_VIOLATIONS,
+            [],
+            {(MODULE_1, MODULE_2): [(MODULE_1, MODULE_2)]},
+            {MODULE_1: [(MODULE_1, MODULE_3)]},
+        ),
+        id="should_except_fulfilled",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD: False, BEHAVIOR_EXCEPTION: True},
+            SHOULD_EXCEPT_VIOLATIONS,
+            [],
+            {(MODULE_1, MODULE_2): [(MODULE_1, MODULE_2)]},
+            {MODULE_1: []},
+        ),
+        id="should_except_violated_should_not_required",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD: True, BEHAVIOR_EXCEPTION: False},
+            SHOULD_EXCEPT_VIOLATIONS,
+            [],
+            {(MODULE_1, MODULE_2): [(MODULE_1, MODULE_2)]},
+            {MODULE_1: []},
+        ),
+        id="should_except_violated_except_not_required",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD: True, BEHAVIOR_EXCEPTION: True},
+            SHOULD_EXCEPT_VIOLATIONS,
+            [(MODULE_1, MODULE_2)],
+            {(MODULE_1, MODULE_2): [(MODULE_1, MODULE_2)]},
+            {MODULE_1: []},
+        ),
+        id="should_except_violated",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_NOT: True, BEHAVIOR_EXCEPTION: True},
+            SHOULD_NOT_EXCEPT_VIOLATIONS,
+            [],
+            {(MODULE_1, MODULE_2): []},
+            {MODULE_1: []},
+        ),
+        id="should_not_except_fulfilled",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_NOT: False, BEHAVIOR_EXCEPTION: True},
+            SHOULD_NOT_EXCEPT_VIOLATIONS,
+            [],
+            {(MODULE_1, MODULE_2): []},
+            {MODULE_1: [(MODULE_1, MODULE_3)]},
+        ),
+        id="should_not_except_violated_should_not_not_required",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_NOT: True, BEHAVIOR_EXCEPTION: False},
+            SHOULD_NOT_EXCEPT_VIOLATIONS,
+            [],
+            {(MODULE_1, MODULE_2): []},
+            {MODULE_1: [(MODULE_1, MODULE_3)]},
+        ),
+        id="should_not_except_violated_except_not_required",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_NOT: True, BEHAVIOR_EXCEPTION: True},
+            SHOULD_NOT_EXCEPT_VIOLATIONS,
+            [(MODULE_1, MODULE_3)],
+            {(MODULE_1, MODULE_2): []},
+            {MODULE_1: [(MODULE_1, MODULE_3)]},
+        ),
+        id="should_not_except_violated",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_ONLY: True, BEHAVIOR_EXCEPTION: True},
+            SHOULD_ONLY_EXCEPT_VIOLATIONS_NO_IMPORT,
+            [],
+            {(MODULE_1, MODULE_2): []},
+            {MODULE_1: [(MODULE_1, MODULE_3)]},
+        ),
+        id="should_only_except_fulfilled",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_ONLY: False, BEHAVIOR_EXCEPTION: True},
+            SHOULD_ONLY_EXCEPT_VIOLATIONS_NO_IMPORT,
+            [],
+            {(MODULE_1, MODULE_2): []},
+            {MODULE_1: []},
+        ),
+        id="should_only_except_no_import_violated_should_only_not_required",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_ONLY: True, BEHAVIOR_EXCEPTION: False},
+            SHOULD_ONLY_EXCEPT_VIOLATIONS_NO_IMPORT,
+            [],
+            {(MODULE_1, MODULE_2): []},
+            {MODULE_1: []},
+        ),
+        id="should_only_except_no_import_violated_except_not_required",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_ONLY: True, BEHAVIOR_EXCEPTION: True},
+            SHOULD_ONLY_EXCEPT_VIOLATIONS_NO_IMPORT,
+            [(MODULE_1, MODULE_2)],
+            {(MODULE_1, MODULE_2): []},
+            {MODULE_1: []},
+        ),
+        id="should_only_except_no_import_violated",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_ONLY: False, BEHAVIOR_EXCEPTION: True},
+            SHOULD_ONLY_EXCEPT_VIOLATIONS_FORBIDDEN_IMPORT,
+            [],
+            {(MODULE_1, MODULE_2): [(MODULE_1, MODULE_2)]},
+            {MODULE_1: [(MODULE_1, MODULE_3)]},
+        ),
+        id="should_only_except_no_forbidden_import_violated_should_only_not_required",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_ONLY: True, BEHAVIOR_EXCEPTION: False},
+            SHOULD_ONLY_EXCEPT_VIOLATIONS_FORBIDDEN_IMPORT,
+            [],
+            {(MODULE_1, MODULE_2): [(MODULE_1, MODULE_2)]},
+            {MODULE_1: [(MODULE_1, MODULE_3)]},
+        ),
+        id="should_only_except_no_forbidden_import_violated_except_not_required",
+    ),
+    pytest.param(
+        RuleViolationDetectorTestCase(
+            {SHOULD_ONLY: True, BEHAVIOR_EXCEPTION: True},
+            SHOULD_ONLY_EXCEPT_VIOLATIONS_FORBIDDEN_IMPORT,
+            [(MODULE_1, MODULE_2)],
+            {(MODULE_1, MODULE_2): [(MODULE_1, MODULE_2)]},
+            {MODULE_1: [(MODULE_1, MODULE_3)]},
+        ),
+        id="should_only_except_no_forbidden_import_violated",
+    ),
+]
+
+
+@pytest.mark.parametrize("test_case", test_cases)
+def test_rule_violation_detection_as_expected(
+    test_case: RuleViolationDetectorTestCase,
+) -> None:
+    behavior_requirement = get_behavior_requirement(**test_case.behavior)
+    module_requirement = get_module_requirement(
+        **{IMPORTERS: [MODULE_1], IMPORTEES: [MODULE_2]}
+    )
+
+    detector = RuleViolationDetector(module_requirement, behavior_requirement)
+
+    violations = detector.get_rule_violation(
+        test_case.explicitly_requested_dependencies,
+        test_case.not_explicitly_requested_dependencies,
+    )
+
+    assert (
+        getattr(violations, test_case.expected_violation)
+        == test_case.expected_violating_dependencies
+    )