diff --git a/.gitignore b/.gitignore
index 61c7ae1..41cf80f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,14 @@
-/tmp/
-/test/.pytest_cache/
-*.json
-*.pyc
-*.idea/
-/build/
-/venv/
+**/tmp/
+**/.pytest_cache/
+*.json
+*.pyc
+*.idea/
+**/build/
+**/venv/
+**/tmp_*
+**/tmp-*
+**/test/
+**/*.egg-info/
+/testing/test_data/*
+/testing/test_output
+*.dat
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index 2ca3cfa..23b2666 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,13 +1,11 @@
-FROM python:3.9.15-slim
-
-ADD src /src
-ADD test /test
-ADD rest /rest
-ADD ephemerality.py /
-ADD requirements.txt /
-ADD _version.py /
-ADD setup.py /
-
-RUN pip install --no-cache-dir --upgrade -r requirements.txt
-
-CMD ["uvicorn", "rest.api:app", "--host", "0.0.0.0", "--port", "8080"]
+FROM python:3.10-slim
+
+ADD dist/ephemerality-2.0.0-py3-none-any.whl /dist/
+ADD LICENSE /
+ADD README.md /
+
+RUN pip install --no-cache-dir /dist/ephemerality-2.0.0-py3-none-any.whl
+
+WORKDIR /usr/local/lib/python3.10/site-packages/ephemerality
+
+ENTRYPOINT ["uvicorn", "rest.runner:app", "--host", "0.0.0.0", "--port", "8080"]
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..2e80ea4
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 Barcelona Supercomputing Center - Centro Nacional de Supercomputación (BSC-CNS)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/README.md b/README.md
index e7ad269..a039031 100644
--- a/README.md
+++ b/README.md
@@ -1,165 +1,309 @@
-# Ephemerality metric
-In [[1]](#1) we formalized the ephemerality metrics used to estimate the healthiness of online discussions. It shows how
-'ephemeral' topics are, that is whether the discussions are more or less uniformly active or only revolve around one or
-several peaks of activity.
-
-We defined 3 versions of ephemerality: original, filtered, and sorted. Let us suppose we have a discussion that we can divide in $N$ bins of equal time length and for each bin we can calculate activity in that time period (e.g. number of tweets, watches, visits etc.). Let $t$ denote a normalized vector of frequency corresponding to this discussion, $t_i$ corresponds to normalized activity in during time bin $i$. Let $\alpha\in\left[0, 1\right)$ denote a parameter showing which portion of activity we consider to be the "core" activity. Then we can define ephemerality as a normalized portion of $t$ that contains the remaining $1-\alpha$ activity. We can interpret this defenition in three slightly different ways depending on what we consider to be the core activity:
-
-1. **Original ephemerality**. We calculate core activity as the minimal portion of $t$ starting from the beginning of the vector that contains at least $\alpha$ of the total activity (which is 1 in case of normalized $t$). Then the ephemerality formula can be computed as follows:
-
-$$
-\varepsilon_{orig}\left(t_i\right) = 1 - \frac1\alpha \frac{\arg\min_{m\in [1,N]}: \left( \sum_{i=1\dots m} t_i \right) \ge \alpha}{N}
-$$
-
-2. **Filtered ephemerality**. We calculate core activity the minimal *central* portion of $t$ that contains at least $\alpha$ of the total activity. For that we exclude portions of $t$ from the beginning and the end of $t$, so that the sum of each of these portions is as close to $\frac{1-\alpha}{2}$ as possible without reaching it:
-
-$$
-\varepsilon_{filt}\left(t_i\right) = 1 - \frac1\alpha \frac{\arg\min_{m\in [1,N]}: \left( \sum_{i=1\dots m} t_i - \max_{p\in [1,N]}: \left( \sum_{j=1\dots p} t_j \right) < \frac{1-\alpha}{2} \right) \ge \alpha}{N}
-$$
-
-3. **Sorted ephemerality**. Finally, we can define the core activity as the minimal number of time bins that cover $\alpha$ portion of the activity. For that we sort $t$ components in descending order (denoted as $\widehat{t}$) and then apply the formula of original ephemerality:
-
-$$
-\varepsilon_{sort}\left(t_i\right) = 1 - \frac1\alpha \frac{\arg\min_{m\in [1,N]}: \left( \sum_{i=1\dots m} \widehat{t}_i \right) \ge \alpha}{N}
-$$
-
-## Requirements
-The code was tested to work with Python 3.8.6 and Numpy 1.21.5, but is expected to also run on their older versions.
-
-## How to run the experiments
-The code can be run directly via the calculate_ephemerality.py script or via a Docker container built with the provided
-Dockerfile.
-
-### Input
-The script/container expect the following input arguments:
-
-* **Frequency vector file**. `[-i PATH, --input PATH]` _Optional_. Path to a file containing one or several arrays of
-numbers in csv format (one array per line), representing temporal frequency vectors. They do not need to be normalized:
-if they are not --- they will be normalized automatically.
-* **Frequency vector**. _Optional_. If input file is not provided, a frequency vector is expected as a positional
-argument (either comma- or space-separated).
-* **Output file**. `[-o PATH, --output PATH]` _Optional_. If it is provided, the results will be written into this file
-in JSON format.
-* **Threshold**. `[-t FLOAT, -threshold FLOAT]` _Optional_. Threshold value for ephemerality computations. Defaults
-to 0.8.
-* **Print**. `[-p, --print]`. _Optional_. If output file is provided, forces the results to still be printed to stdout.
-
-### Output
-If no output file specified or `-p` option is used, results are printed to STDOUT in [
-$\varepsilon_{orig}$ ␣
-span( $\varepsilon_{orig}$ ) ␣
-$\varepsilon_{filt}$ ␣
-span( $\varepsilon_{filt}$ ) ␣
-$\varepsilon_{sort}$ ␣
-span( $\varepsilon_{sort}$ )
-] format, one line per each line of input file (or a single line for command line input).
-
-If the output file was specified among the input arguments, the results will be written into that file in JSON format as
-a list of dictionaries, one per input line:
-
-```
-[
- {
- "ephemerality_original": FLOAT,
- "ephemerality_original_span": INT,
- "ephemerality_filtered": FLOAT,
- "ephemerality_filtered_span": INT,
- "ephemerality_sorted": FLOAT,
- "ephemerality_sorted_span": INT
- },
- ...
-]
-```
-
-### Example
-
-Input file `test_input.csv`:
-```
-0.0,0.0,0.0,0.2,0.55,0.0,0.15,0.1,0.0,0.0
-0,1,1.,0.0,.0
-```
-
-#### Python execution:
-
-Input 1:
-
-```
-python ephemerality.py -i tmp/test_input.csv -t 0.8 --output tmp/test_output.json -P
-```
-
-Output 1:
-```
-0.1250000000000001 7 0.5 4 0.625 3
-0.2500000000000001 3 0.5 2 0.5 2
-```
-
-`test_output.json` content:
-```
-[
- {
- "ephemerality_original": 0.1250000000000001,
- "ephemerality_original_span": 7,
- "ephemerality_filtered": 0.5,
- "ephemerality_filtered_span": 4,
- "ephemerality_sorted": 0.625,
- "ephemerality_sorted_span": 3
- },
- {
- "ephemerality_original": 0.2500000000000001,
- "ephemerality_original_span": 3,
- "ephemerality_filtered": 0.5,
- "ephemerality_filtered_span": 2,
- "ephemerality_sorted": 0.5,
- "ephemerality_sorted_span": 2
- }
-]
-```
-
-Input 2:
-
-```
-python ephemerality.py 0.0 0.0 0.0 0.2 0.55 0.0 0.15 0.1 0.0 0.0 -t 0.5
-```
-
-Output 2:
-```
-0.0 5 0.8 1 0.8 1
-```
-
-#### Docker execution
-```
-docker run -a STDOUT -v [PATH_TO_FOLDER]/tmp/:/tmp/ ephemerality:1.0.0 -i /tmp/test_input.csv -o /tmp/test_output.json -t 0.5 -p
-```
-
-Output:
-```
-0.0 5 0.8 1 0.8 1
-0.19999999999999996 2 0.6 1 0.6 1
-```
-
-`test_output.json` content:
-```
-[
- {
- "ephemerality_original": 0.0,
- "ephemerality_original_span": 5,
- "ephemerality_filtered": 0.8,
- "ephemerality_filtered_span": 1,
- "ephemerality_sorted": 0.8,
- "ephemerality_sorted_span": 1
- },
- {
- "ephemerality_original": 0.19999999999999996,
- "ephemerality_original_span": 2,
- "ephemerality_filtered": 0.6,
- "ephemerality_filtered_span": 1,
- "ephemerality_sorted": 0.6,
- "ephemerality_sorted_span": 1
- }
-]
-```
-
-
-## References
-[1]
-Gnatyshak, D., Garcia-Gasulla, D., Alvarez-Napagao, S., Arjona, J., & Venturini, T. (2022). Healthy Twitter discussions? Time will tell. arXiv preprint arXiv:2203.11261
+# Ephemerality metric
+Ephemerality metrics are used to estimate the healthiness of various (online) activities, e.g. online discussions.
+It shows how 'ephemeral' these activities are, that is whether they are more or less uniform
+over the period of interest or are only clustered around one or several short time periods.
+
+Ephemerality formula is defined as follows:
+
+$$
+\varepsilon_\alpha^{core}=\left(1 - \frac1\alpha \cdot \frac{core\ length}{period\ length}\right)^+
+$$
+
+Essentially, **ephemerality** is a portion of the time period occupied by non‑core activity. The core activity can be
+defined in different ways and is parametrized by $\alpha$ — the minimal percentage of total activity it needs to
+contain.
+
+We defined 4 versions of computing these core periods:
+
+1. **Left $\alpha$-core**. Continuous time span from **the beginning of the period** to the point when $\alpha$% of
+total activity volume is contained within. Measures how fast activity becomes negligible after the start of the period.
+Best suited for the types of activity with well-defined start time, e.g. reactions to posts, videos, news, etc.
+
+2. **Right $\alpha$-core**. Continuous time span from **the end of the period** to the point when $\alpha$% of total
+activity volume is contained within. Measures how far in the past the meaningful past of activity spans. Best suited for
+when you want to analyze activity within a specific time frame (for instance, up until the current date).
+
+3. **Middle $\alpha$-core**. Continuous time span from **in the middle of the period** that contains $\alpha$% of total
+activity volume. Computed by cutting out at most (but as close to) $\frac{1-\alpha}2$% of activity volume from the
+beginning and the end of the period. Best suited for activities with no identifiable start and end times within the
+period of interest, e.g. discussions of certain topics onj social media.
+
+4. **Sorted $\alpha$-core**. **Minimal number of time bins** that together contain $\alpha$% of total activity volume
+(left $\alpha$‑core for activity vectors sorted in descending order). Measures what portion of time is occupied by the
+most prominent activity. Works well in combination with other cores to check whether all of the activity was centered
+around one or more short periods of time.
+
+## Requirements
+The code was tested to work with Python 3.10, Numpy 1.24.2, and pydantic 2.7.1, but is expected to also run with their
+older versions.
+FastAPI ^0.110.2 and uvicorn ^0.22.0 are also needed to run the ephemerality computation web-service.
+Matplotlib ^3.8.4 is needed for visualization of computed activity cores.
+
+## How to run
+Ephemerality package can be run as a standalone Python module, or used inside regular Python scripts.
+
+### Standalone
+
+The code can be run directly as a module `python3 -m ephemerality [args]`. There are two modes provided: a
+command line-based computation and a RESTful service. In case of the latter, there is an option to use a Docker
+container, either from Docker Hub (`hpaibsc/ephemerality:latest`) or built with the provided Dockerfile.
+
+To run the computation from the command lineuse `cmd` argument:
+
+```shell
+python3 -m ephemerality cmd [activity] [-h] [-i INPUT_FILE] [-r] [-o OUTPUT_FILE.json] [-c CORE_TYPES] [-t THRESHOLD] [--plot]
+```
+
+In case of activity vector of moderate size, you can enter it either as a sequence of numbers directly, or as a
+comma/space separated string. Alternatively, you can save the activity vector(s) in a *.csv file (a vector per line) or
+*.json file. In case of the latter, please use the following format for each input (you can have a single dictionary or
+a list of ones):
+
+```NumPy
+JSON/Python input body format
+
+Attributes
+----------
+input_sequence : Sequence[str | float | int]
+ Input sequence of either activity over time bins, or timestamps to be aggregated into an activity vector.
+input_type : Literal['activity', 'a', 'timestamps', 't', 'datetime', 'd'], default='a'
+ Format of the input sequence.
+threshold : float, default=.8
+ Ratio of the activity considered core.
+time_format : str, optional, default="%Y-%m-%dT%H:%M:%S.%fZ"
+ If input type is datetime, specifies the datetime format used (refer to `strptime` format guidelines).
+timezone : float, optional, default=0.
+ If input type is datetime, specifies the offset in hours from the UTC time. Should be within [-24, +24] range.
+range : tuple[str | float | int, str | float | int], optional
+ If input type is timestamp or datetime, specifies the time range of the activity vector.
+ Defaults to (min, max) of the input timestamps.
+granularity : Literal['week', 'day', 'hour'] or str, optional, default='day'
+ If input type is timestamp or datetime, specifies the size of time bins when converting to activity vector.
+ Can be specified as \'{}d\' or \'{}h\' (e.g. '2d' or '7.5h') for a custom time bin size in days or hours.
+reference_name : str, optional
+ Will be added to the output for easier matching between inputs and outputs (besides identical sorting).
+```
+
+The module uses uvicorn package to run a REST service. To start it use `api` argument:
+
+```shell
+python3 -m ephemerality api [-h] [--host HOST] [--port PORT] ...
+```
+
+Any additional arguments will be passed as arguments to the uvicorn call.
+
+The web-service initialized this way accepts GET requests of the following format:
+
+```http
+http://{url}/ephemerality/{api_version}/all?core_types=lmrs&include_input=False
+```
+
+* `api_version` is optional and is used for backward compatibility, it defaults to the latest API version.
+* `core_types` is a string specifying the core types for which ephemerality needs to be computed. `l` for the left
+core, `m` for the middle core, `r` for the right core, `s` for the sorted core, or any combination of thereof.
+It defaults to all core types, `lmrs`.
+* `include_input` is a boolean value specifying whether the input should be also included in the output for each
+computation.
+* `input_data` is a body argument in JSON format. It should contain a list of JSON dictionaries of the input type
+specified above.
+
+The output is in JSON format. It is a list of dictionaries, with one entry per each input:
+
+```json
+[
+ {
+ "input": ...,
+ "output": {
+ "eph_left_core": 1.0,
+ "eph_middle_core": 1.0,
+ "eph_right_core": 1.0,
+ "eph_sorted_core": 1.0,
+ "len_left_core": 0,
+ "len_middle_core": 0,
+ "len_right_core": 0,
+ "len_sorted_core": 0
+ }
+ },
+ ...
+]
+```
+
+`input` value depends on the provided input parameters:
+* If `include_input` was set to True, it will be a copy of the corresponding input dictionary.
+* Otherwise it will be a string:
+ - `reference_name` if it was provided;
+ - or a zero-base counter number of the corresponding input.
+
+#### Docker container
+
+Finally, you can also use the docker container to run the aforementioned REST service. You can either get the image
+from Docker Hub:
+
+```shell
+docker pull hpaibsc/ephemerality:latest
+```
+
+Or build it from the source using the provided Dockerfile.
+
+The web-service will be available at `http://0.0.0.0:8080` inside of the container.
+
+### Python
+
+Finally, it is possible to just import the ephemerality computation function from the module:
+
+```Python
+from ephemerality import compute_ephemerality
+
+activity = [0., 0., 0., .2, .55, 0., .15, .1, 0., 0.]
+threshold = 0.8
+compute_ephemerality(activity, threshold)
+```
+
+Output:
+```pycon
+EphemeralitySet(
+ eph_left_core=0.1250000000000001,
+ eph_middle_core=0.5,
+ eph_right_core=0.2500000000000001,
+ eph_sorted_core=0.625,
+ len_left_core=7,
+ len_middle_core=4,
+ len_right_core=6,
+ len_sorted_core=3)
+```
+
+`compute_ephemerality` function has the following signature:
+
+```NumPy
+Compute ephemerality values for given activity vector.
+
+This function computes ephemerality for a numeric vector using all current definitions of actiovity cores.
+Alpha (desired non-ephemeral core length) can be specified with ``threshold parameter. In case not all cores are
+needed, the required types can be specified in ``types``.
+
+Parameters
+----------
+activity_vector : Sequence[float | int]
+ A sequence of activity values. Time bins corresponding to each value is assumed to be of equal length. Does not
+ need to be normalised.
+threshold : float, default=0.8
+ Desired non-ephemeral core length as fraction. Ephemerality is equal to 1.0 if the core length is at least ``threshold`` of
+ the ``activity_vector`` length.
+types : str, default='lmrs'
+ Activity cores to be computed. A sequence of characters corresponding to core types.
+ 'l' for left core, 'm' for middle core, 'r' for right core, 's' for sorted core. Multiples of the same character
+ will be ignored.
+plot : bool, default=False
+ Set to True to display the activity over time plot with core periods highlighted.
+
+Returns
+-------
+EphemeralitySet
+ Container for the computed core lengths and ephemerality values
+```
+
+EphemeralitySet is a simple container of the following format:
+
+```NumPy
+Container for ephemerality and core size values.
+
+This class is a simple pydantic BaseModel used to store computed core lengths and corresponding ephemerality values
+by core type. Values that were not computed default to None.
+
+Attributes
+----------
+len_left_core : int, optional
+ Length of the left core in time bins
+len_middle_core : int, optional
+ Length of the middle core in time bins
+len_right_core : int, optional
+ Length of the right core in time bins
+len_sorted_core : int, optional
+ Length of the sorted core in time bins
+
+eph_left_core: float, optional
+ Ephemerality value for the left core
+eph_middle_core: float, optional
+ Ephemerality value for the middle core
+eph_right_core: float, optional
+ Ephemerality value for the right core
+eph_sorted_core: float, optional
+ Ephemerality value for the sorted core
+```
+
+## Examples
+
+Below are several examples of activity vectors and corresponding ephemerality computation results, demonstrating
+how this module can be used. All input activity vectors represent one year of activity with one day granularity.
+Threshold of $\alpha=0.8$ was used for all of the examples.
+
+### Example 1
+
+![Activity vector 1](images/example_1.png)
+
+This vector represents a typical reaction activity to a post of any kind (e.g. text post, video, etc.). Most of the
+activity is concentrated at the beginning of the observation period and quickly goes down noise afterwards. Assuming
+we picked the start of the vector well (i.e. the time of posting), we will obtain the following ephemerality
+computation results:
+
+![Results 1](images/results_1.png)
+
+Here you can see that for the selected period ephemerality values for all cores except for the right core are high.
+Essentially, ephemerality value of 0.75 in this case signifies that about a quarter of the period corresponded to
+non-core activity.
+
+The exception of the right core (which is computed from the right end of the activity vector) makes sense, as to
+accumulate the 80% of activity we need to go to almost the beginning of the vector. This can be interpreted as the
+fact, that looking into the past from the last (e.g. current) data this activity goes essentially full period deep.
+That is, this activity existed for quite a while in the past, and did not appear recently.
+
+### Example 2
+
+![Activity vector 2](images/example_2.png)
+
+This activity vector has a few smaller but more gradual active periods and a big peak by its end. This produces the
+following results:
+
+![Results 2](images/results_2.png)
+
+Left and middle core ephemeralities in this case are pretty low, as it takes almost all the period to accumulate the
+80% of activity. The final peak was not enough to offset the previous, more gradual, activity.
+
+In case of the right core, the last two peaks did contain the required amount of activity, so the ephemerality value
+is significant.
+
+Finally, as the majority of the activity is concentrated in the three peaks, the sorted ephemerality value is rather
+high (albeit not close to 1, thanks to the widths of the first two peaks).
+
+### Example 3
+
+![Activity vector 3](images/example_3.png)
+
+In this example the vector contains 4 intense and abrupt peaks of activity among the general noise, a clear indicator
+of the forced activity injections. However, these peaks are spread throughout the observation period, with one being
+close to the beginning of the vector, and another one to its end.
+
+![Results 3](images/results_3.png)
+
+Non-sorted cores in this case do cover most of the observation period due to the spread of the peaks. However, the
+sorted ephemerality value is very high, signifying that most of this activity were contained within a small number of
+actual days.
+
+Here we should note that it is important to look at ephemerality for all the core types. From the current results we
+know that there are several well-spread short peaks of activity. If the middle core ephemerality was also high, that
+would have signified that there is only one peak or a close cluster of peaks in the middle of the period. If either
+the left or right core ephemerality were also high, that would have meant that this peak is closer to the left or the
+right end of the vector respectively.
+
+### Example 4
+
+![Activity vector 4](images/example_4.png)
+
+In the final example, the activity is locally chaotic but more or less uniform over the whole period. There is a small
+evolution of the trend here, but nothing unexpected.
+
+![Results 4](images/results_4.png)
+
+And this uniformity is confirmed by the resulting ephemerality values which are close to 0 (it would have been 0 in
+case of the same amount of activity each day). Here the slightly higher value of the sorted core ephemerality
+corresponds to the general variance of the activity: the lower the local fluctuations are, the closer to 0 it will be.
diff --git a/_version.py b/_version.py
deleted file mode 100644
index d538f87..0000000
--- a/_version.py
+++ /dev/null
@@ -1 +0,0 @@
-__version__ = "1.0.0"
\ No newline at end of file
diff --git a/dist/ephemerality-2.0.0-py3-none-any.whl b/dist/ephemerality-2.0.0-py3-none-any.whl
new file mode 100644
index 0000000..fd4fbfe
Binary files /dev/null and b/dist/ephemerality-2.0.0-py3-none-any.whl differ
diff --git a/dist/ephemerality-2.0.0.tar.gz b/dist/ephemerality-2.0.0.tar.gz
new file mode 100644
index 0000000..c30b371
Binary files /dev/null and b/dist/ephemerality-2.0.0.tar.gz differ
diff --git a/ephemerality.py b/ephemerality.py
deleted file mode 100644
index a1d050a..0000000
--- a/ephemerality.py
+++ /dev/null
@@ -1,89 +0,0 @@
-from _version import __version__
-import sys
-import json
-import argparse
-import numpy as np
-from src import compute_ephemerality
-
-
-HELP_INFO = ""
-
-
-def init_argparse() -> argparse.ArgumentParser:
- parser = argparse.ArgumentParser(
- usage="%(prog)s [FREQUENCY_VECTOR] [-h] [-v] [-i INPUT_FILE] [-o OUTPUT_FILE.json] [-t THRESHOLD]...",
- description="Calculate ephemerality for a given vector of frequencies."
- )
- parser.add_argument(
- "-v", "--version", action="version",
- version=f"{parser.prog} version {__version__}"
- )
- parser.add_argument(
- "-p", "--print", action="store_true",
- help="If output file is provided, forces the results to still be printed to stdout."
- )
- parser.add_argument(
- "-i", "--input", action="store",
- help="Path to the input csv file. If not specified, will use the command line arguments "
- "(delimited either by commas or spaces)."
- )
- parser.add_argument(
- "-o", "--output", action="store",
- help="Path to the output json file. If not specified, will output ephemerality values to stdout in the"
- " following format separated by a space: \"EPH_ORIG EPH_ORIG_SPAN EPH_FILT EPH_FILT_SPAN EPH_SORT "
- "EPH_SORT_SPAN\""
- )
- parser.add_argument(
- "-t", "--threshold", action="store", default=0.8,
- help="Threshold value for ephemerality computations. Defaults to 0.8."
- )
- parser.add_argument(
- 'frequencies',
- help='frequency vector (if the input file is not specified)',
- nargs='*'
- )
- return parser
-
-
-def print_ephemeralities(ephemerality_list: list[dict]):
- for ephemeralities in ephemerality_list:
- print(f"{ephemeralities['ephemerality_original']} {ephemeralities['ephemerality_original_span']} "
- f"{ephemeralities['ephemerality_filtered']} {ephemeralities['ephemerality_filtered_span']} "
- f"{ephemeralities['ephemerality_sorted']} {ephemeralities['ephemerality_sorted_span']}")
-
-
-if __name__ == '__main__':
- parser = init_argparse()
- args = parser.parse_args()
-
- frequency_vectors = list()
-
- if args.input:
- with open(args.input, 'r') as f:
- for line in f.readlines():
- if line.strip():
- frequency_vectors.append(np.array(line.split(','), dtype=float))
- else:
- if len(args.frequencies) > 1:
- frequency_vectors.append(np.array(args.frequencies, dtype=float))
- elif len(args.frequencies) == 1:
- if ' ' in args.frequencies[0]:
- frequency_vectors.append(np.array(args.frequencies[0].split(' '), dtype=float))
- else:
- frequency_vectors.append(np.array(args.frequencies[0].split(','), dtype=float))
- else:
- sys.exit('No input provided!')
-
- threshold = float(args.threshold)
-
- ephemerality_list = list()
- for frequency_vector in frequency_vectors:
- ephemerality_list.append(compute_ephemerality(frequency_vector=frequency_vector, threshold=threshold).dict())
-
- if args.output:
- with open(args.output, 'w+') as f:
- json.dump(ephemerality_list, f, indent=2)
- if args.print:
- print_ephemeralities(ephemerality_list)
- else:
- print_ephemeralities(ephemerality_list)
diff --git a/ephemerality/__init__.py b/ephemerality/__init__.py
new file mode 100644
index 0000000..d682f98
--- /dev/null
+++ b/ephemerality/__init__.py
@@ -0,0 +1,5 @@
+from .src import EphemeralitySet, InputData
+from .src import compute_ephemerality
+
+
+__all__ = ['compute_ephemerality', 'InputData', 'EphemeralitySet']
diff --git a/ephemerality/__main__.py b/ephemerality/__main__.py
new file mode 100644
index 0000000..a2da2c2
--- /dev/null
+++ b/ephemerality/__main__.py
@@ -0,0 +1,36 @@
+from argparse import ArgumentParser
+import importlib.metadata
+
+from ephemerality.scripts import init_cmd_parser, init_api_argparse
+
+PROG = "python3 -m ephemerality"
+
+
+def init_parser() -> ArgumentParser:
+ parser = ArgumentParser(
+ prog=PROG,
+ usage="%(prog)s [-h] [-v] {cmd,api} ...",
+ description="Runs ephemerality computation module in one of the available mode."
+ )
+ parser.add_argument(
+ "--version", action="version",
+ version=f'ephemerality {importlib.metadata.version("ephemerality")}'
+ )
+
+ subparsers = parser.add_subparsers(
+ prog=PROG,
+ help="Use \"cmd\" to run the module once from a command line.\n"
+ "Use \"api\" to start a REST web service offering ephemerality computation on request."
+ )
+ cmd_parser = subparsers.add_parser("cmd")
+ api_parser = subparsers.add_parser("api")
+
+ init_cmd_parser(cmd_parser)
+ init_api_argparse(api_parser)
+
+ return parser
+
+
+parser = init_parser()
+args = parser.parse_args()
+args.func(args)
diff --git a/ephemerality/rest/__init__.py b/ephemerality/rest/__init__.py
new file mode 100644
index 0000000..eeb424f
--- /dev/null
+++ b/ephemerality/rest/__init__.py
@@ -0,0 +1,14 @@
+import ephemerality.rest.api_versions as api_versions
+from ephemerality.rest.api_versions import AbstractRestApi
+from ephemerality.rest.api import router
+from ephemerality.src import InputData
+
+__all__ = [
+ 'InputData',
+ 'router',
+ 'AbstractRestApi'
+]
+
+
+API_VERSION_DICT: dict[str, AbstractRestApi] = {api.version(): api for api in api_versions.__all__ if api.version()}
+DEFAULT_API: AbstractRestApi = API_VERSION_DICT[max(API_VERSION_DICT.keys())]
diff --git a/ephemerality/rest/api.py b/ephemerality/rest/api.py
new file mode 100644
index 0000000..820efbb
--- /dev/null
+++ b/ephemerality/rest/api.py
@@ -0,0 +1,92 @@
+from typing import Annotated, Any, Union
+
+import ephemerality.rest as rest
+from ephemerality.src import InputData, process_input
+from fastapi import APIRouter, status, Query, Response
+from fastapi.responses import JSONResponse
+
+router = APIRouter()
+
+
+def run_computations(
+ input_data: list[InputData],
+ core_types: str,
+ api: rest.AbstractRestApi,
+ include_input: bool = False) -> Union[list[dict[str, Any] | dict[str, dict[str, Any]]], None]:
+ output = []
+ noname_counter = 0
+ for test_case in input_data:
+ case_input = process_input(input_remote_data=test_case)[0]
+ case_output = api.get_ephemerality(
+ input_vector=case_input.activity,
+ threshold=case_input.threshold,
+ types=core_types
+ ).dict(exclude_none=True)
+
+ if include_input:
+ output.append({
+ "input": test_case.dict(),
+ "output": case_output
+ })
+ else:
+ if test_case.reference_name:
+ input_name = test_case.reference_name
+ else:
+ input_name = str(noname_counter)
+ noname_counter += 1
+
+ output.append({
+ "input": input_name,
+ "output": case_output
+ })
+ return output
+
+
+def process_request(
+ input_data: list[InputData],
+ api_version: str,
+ core_types: str,
+ include_input: bool
+) -> Response:
+ if api_version not in rest.API_VERSION_DICT:
+ raise ValueError(f'Unrecognized API version: {api_version}!')
+ else:
+ api = rest.API_VERSION_DICT[api_version]
+
+ output = run_computations(input_data=input_data, core_types=core_types, api=api, include_input=include_input)
+ return JSONResponse(content=output)
+
+
+@router.get("/ephemerality/all", status_code=status.HTTP_200_OK)
+async def compute_all_ephemeralities_default_version(
+ input_data: list[InputData],
+ core_types: Annotated[
+ str, Query(min_length=1, max_length=4, regex="^[lmrs]+$")
+ ] = "lmrs",
+ include_input: bool = False
+) -> Response:
+ default_version = rest.DEFAULT_API.version()
+ return process_request(
+ input_data=input_data,
+ core_types=core_types,
+ api_version=default_version,
+ include_input=include_input
+ )
+
+
+@router.get("/ephemerality/{api_version}/all", status_code=status.HTTP_200_OK)
+async def compute_all_ephemeralities(
+ input_data: list[InputData],
+ api_version: str,
+ core_types: Annotated[
+ str, Query(min_length=1, max_length=4, regex="^[lmrs]+$")
+ ] = "lmrs",
+ include_input: bool = False
+) -> Response:
+
+ return process_request(
+ input_data=input_data,
+ core_types=core_types,
+ api_version=api_version,
+ include_input=include_input,
+ )
diff --git a/ephemerality/rest/api_versions/__init__.py b/ephemerality/rest/api_versions/__init__.py
new file mode 100644
index 0000000..b886d2d
--- /dev/null
+++ b/ephemerality/rest/api_versions/__init__.py
@@ -0,0 +1,11 @@
+from ephemerality.rest.api_versions.api_template import AbstractRestApi
+from ephemerality.rest.api_versions.api11 import RestAPI11
+
+
+__all__ = [
+ AbstractRestApi,
+ RestAPI11
+]
+
+
+
diff --git a/ephemerality/rest/api_versions/api11.py b/ephemerality/rest/api_versions/api11.py
new file mode 100644
index 0000000..f7785d6
--- /dev/null
+++ b/ephemerality/rest/api_versions/api11.py
@@ -0,0 +1,20 @@
+from typing import Sequence, Annotated
+
+from fastapi import Query
+
+from ephemerality.rest.api_versions.api_template import AbstractRestApi
+from ephemerality.src import compute_ephemerality, EphemeralitySet
+
+
+class RestAPI11(AbstractRestApi):
+ @staticmethod
+ def version() -> str:
+ return "1.1"
+
+ @staticmethod
+ def get_ephemerality(
+ input_vector: Sequence[float],
+ threshold: Annotated[float, Query(gt=0., le=1.)],
+ types: Annotated[str, Query(Query(min_length=1, max_length=4, regex="^[lmrs]+$"))]
+ ) -> EphemeralitySet:
+ return compute_ephemerality(activity_vector=input_vector, threshold=threshold, types=types)
diff --git a/ephemerality/rest/api_versions/api_template.py b/ephemerality/rest/api_versions/api_template.py
new file mode 100644
index 0000000..8c95d06
--- /dev/null
+++ b/ephemerality/rest/api_versions/api_template.py
@@ -0,0 +1,19 @@
+from abc import ABC, abstractmethod
+from typing import Annotated, Sequence
+from fastapi import Query
+from ephemerality.src import EphemeralitySet
+
+
+class AbstractRestApi(ABC):
+ @staticmethod
+ @abstractmethod
+ def version() -> str | None:
+ return None
+
+ @staticmethod
+ @abstractmethod
+ def get_ephemerality(input_vector: Sequence[float],
+ threshold: Annotated[float, Query(gt=0., le=1.)],
+ types: Annotated[str, Query(Query(min_length=1, max_length=4, regex="^[lmrs]+$"))]
+ ) -> EphemeralitySet:
+ raise NotImplementedError
diff --git a/ephemerality/rest/runner.py b/ephemerality/rest/runner.py
new file mode 100644
index 0000000..5076734
--- /dev/null
+++ b/ephemerality/rest/runner.py
@@ -0,0 +1,6 @@
+from fastapi import FastAPI
+from ephemerality.rest import router
+
+
+app = FastAPI()
+app.include_router(router)
diff --git a/ephemerality/scripts/__init__.py b/ephemerality/scripts/__init__.py
new file mode 100644
index 0000000..03c7596
--- /dev/null
+++ b/ephemerality/scripts/__init__.py
@@ -0,0 +1,4 @@
+from ephemerality.scripts.ephemerality_api import init_api_argparse
+from ephemerality.scripts.ephemerality_cmd import init_cmd_parser
+
+__all__ = ['init_cmd_parser', 'init_api_argparse']
diff --git a/ephemerality/scripts/ephemerality_api.py b/ephemerality/scripts/ephemerality_api.py
new file mode 100644
index 0000000..5821cc2
--- /dev/null
+++ b/ephemerality/scripts/ephemerality_api.py
@@ -0,0 +1,36 @@
+import argparse
+from argparse import ArgumentParser, Namespace
+from subprocess import call
+
+
+def init_api_argparse(parser: ArgumentParser) -> ArgumentParser:
+ parser.usage = "%(prog)s [-h] [--host HOST] [--port PORT] ..."
+ parser.description = ("Start a REST web service to compute ephemerality computations on requests. Any additional "
+ "arguments will be passed to the uvicorn service initialisation call.")
+ parser.add_argument(
+ "--host", action="store", default="127.0.0.1",
+ help="Bind socket to this host. Defaults to \"127.0.0.1\"."
+ )
+ parser.add_argument(
+ "--port", action="store", type=int, default=8080,
+ help="Bind to a socket with this port. Defaults to 8080."
+ )
+ parser.add_argument(
+ "uvicorn_args", nargs=argparse.REMAINDER,
+ help="Arguments to be passed to uvicorn."
+ )
+ parser.set_defaults(
+ func=exec_start_service_call
+ )
+ return parser
+
+
+def start_service(host: str = "127.0.0.1", port: int = 8080, uvicorn_args: list | None = None) -> None:
+ call_cmd = ['uvicorn', 'ephemerality.rest.runner:app', '--host', host, '--port', str(port)]
+ if uvicorn_args:
+ call_cmd.extend(uvicorn_args)
+ call(call_cmd)
+
+
+def exec_start_service_call(input_args: Namespace) -> None:
+ start_service(host=input_args.host, port=input_args.port, uvicorn_args=input_args.uvicorn_args)
diff --git a/ephemerality/scripts/ephemerality_cmd.py b/ephemerality/scripts/ephemerality_cmd.py
new file mode 100644
index 0000000..9fde6bf
--- /dev/null
+++ b/ephemerality/scripts/ephemerality_cmd.py
@@ -0,0 +1,118 @@
+import json
+import sys
+from argparse import ArgumentParser, Namespace
+from pathlib import Path
+
+import numpy as np
+from ephemerality.src import compute_ephemerality, process_input, ProcessedData
+
+
+def init_cmd_parser(parser: ArgumentParser) -> ArgumentParser:
+ parser.usage = "%(prog)s [activity] [-h] [-i INPUT_FILE] [-r] [-o OUTPUT_FILE.json] [-c CORE_TYPES] [-t THRESHOLD] [--plot]..."
+ parser.description = "Calculate ephemerality for a given activity vector or a set of timestamps."
+ parser.add_argument(
+ "-p", "--print", action="store_true",
+ help="If an output file is specified, forces the results to still be printed to stdout."
+ )
+ parser.add_argument(
+ "-i", "--input", action="store",
+ help="Path to either a JSON or CSV file with input data, or to the folder with files. If not specified, "
+ "will read the activity vector from the command line (as numbers delimited by either commas or spaces)."
+ )
+ parser.add_argument(
+ "-r", "--recursive", action="store_true",
+ help="Used with a folder-type input to specify to also process files in the full subfolder tree. "
+ "Defaults to False."
+ )
+ parser.add_argument(
+ "-o", "--output", action="store",
+ help="Path to an output JSON file. If not specified, will output ephemerality values to stdout in JSON format."
+ )
+ parser.add_argument(
+ "--output_indent", action="store", type=int, default=-1,
+ help="Sets the indentation level of the output (either a JSON file or STDOUT) in terms of number of spaces per "
+ "level. If negative, will output results as a single line. Defaults to -1."
+ )
+ parser.add_argument(
+ "-c", "--core_types", action="store", type=str, default="lmrs",
+ help="Specify core types to be computed. \"l\" for left core, \"m\" for middle core, \"r\" for right core, "
+ "\"s\" for sorted core, or any combination of thereof. Default to \"lmrs\" for all 4 core types. "
+ )
+ parser.add_argument(
+ "-t", "--threshold", action="store", type=float, default=0.8,
+ help="Threshold value for ephemerality computations in case of CSV input. Defaults to 0.8."
+ )
+ parser.add_argument(
+ "--plot", action="store_true",
+ help="Visualize requested core types on the activity vector plot."
+ )
+ parser.add_argument(
+ 'activity',
+ help='Activity vector (if the input file is not specified)',
+ nargs='*'
+ )
+ parser.set_defaults(
+ func=exec_cmd_compute_call
+ )
+ return parser
+
+
+def exec_cmd_compute_call(input_args: Namespace) -> None:
+ if input_args.input:
+ path = Path(input_args.input)
+ if path.is_dir():
+ input_cases = process_input(input_folder=input_args.input, recursive=input_args.recursive)
+ elif path.is_file():
+ input_cases = process_input(input_file=input_args.input, threshold=float(input_args.threshold))
+ else:
+ raise ValueError("Unknown input file format!")
+ else:
+ input_cases: list[ProcessedData] = []
+ if len(input_args.activity) > 1:
+ input_cases.append(
+ ProcessedData(
+ name="cmd-input",
+ activity=np.array(input_args.activity, dtype=float),
+ threshold=float(input_args.threshold)))
+ elif len(input_args.activity) == 1:
+ if ' ' in input_args.activity[0]:
+ input_cases.append(
+ ProcessedData(
+ name="cmd-input",
+ activity=np.array(input_args.activity[0].split(' '), dtype=float),
+ threshold=float(input_args.threshold)))
+ elif ',' in input_args.activity[0]:
+ input_cases.append(
+ ProcessedData(
+ name="cmd-input",
+ activity=np.array(input_args.activity[0].split(','), dtype=float),
+ threshold=float(input_args.threshold)))
+ else:
+ input_cases.append(
+ ProcessedData(
+ name="cmd-input",
+ activity=np.array([input_args.activity[0]], dtype=float),
+ threshold=float(input_args.threshold)))
+ else:
+ sys.exit('No input provided!')
+
+ results = {}
+
+ for input_case in input_cases:
+ results[input_case.name] = compute_ephemerality(activity_vector=input_case.activity,
+ threshold=input_case.threshold,
+ types=input_args.core_types,
+ plot=input_args.plot).dict()
+ if len(results) == 1:
+ results = results.popitem()[1]
+
+ output_indent = input_args.output_indent if input_args.output_indent >= 0 else None
+ if input_args.output:
+ with open(input_args.output, 'w') as f:
+ json.dump(results, f, indent=output_indent, sort_keys=True)
+ if input_args.print:
+ print(json.dumps(results, indent=output_indent, sort_keys=True))
+ else:
+ return None
+ else:
+ print(json.dumps(results, indent=output_indent, sort_keys=True))
diff --git a/ephemerality/src/__init__.py b/ephemerality/src/__init__.py
new file mode 100644
index 0000000..04e58b0
--- /dev/null
+++ b/ephemerality/src/__init__.py
@@ -0,0 +1,5 @@
+from .data_processing import process_input, InputData, ProcessedData
+from .ephemerality_computation import compute_ephemerality
+from .utils import EphemeralitySet
+
+__all__ = ['compute_ephemerality', 'EphemeralitySet', 'process_input', 'InputData', 'ProcessedData']
diff --git a/ephemerality/src/data_processing.py b/ephemerality/src/data_processing.py
new file mode 100644
index 0000000..c87bf13
--- /dev/null
+++ b/ephemerality/src/data_processing.py
@@ -0,0 +1,220 @@
+import json
+import warnings
+from dataclasses import dataclass
+from datetime import datetime, timezone, timedelta
+from pathlib import Path
+from typing import Sequence, Literal
+
+import numpy as np
+from numpy.typing import NDArray
+from pydantic import BaseModel
+
+SECONDS_WEEK = 604800.
+SECONDS_DAY = 86400.
+SECONDS_HOUR = 3600.
+
+
+class InputData(BaseModel):
+ """
+ GET request body format
+
+ Attributes
+ ----------
+ input_sequence : Sequence[str | float | int]
+ Input sequence of either activity over time bins, or timestamps to be aggregated into an activity vector.
+ input_type : Literal['activity', 'a', 'timestamps', 't', 'datetime', 'd'], default='a'
+ Format of the input sequence.
+ threshold : float, default=.8
+ Ratio of the activity considered core.
+ time_format : str, optional, default="%Y-%m-%dT%H:%M:%S.%fZ"
+ If input type is datetime, specifies the datetime format used (refer to `strptime` format guidelines).
+ timezone : float, optional, default=0.
+ If input type is datetime, specifies the offset in hours from the UTC time. Should be within [-24, +24] range.
+ range : tuple[str | float | int, str | float | int], optional
+ If input type is timestamp or datetime, specifies the time range of the activity vector.
+ Defaults to (min, max) of the input timestamps.
+ granularity : Literal['week', 'day', 'hour'] or str, optional, default='day'
+ If input type is timestamp or datetime, specifies the size of time bins when converting to activity vector.
+ Can be specified as \'{}d\' or \'{}h\' (e.g. '2d' or '7.5h') for a custom time bin size in days or hours.
+ reference_name : str, optional
+ Will be added to the output for easier matching between inputs and outputs (besides identical sorting).
+ """
+ input_sequence: Sequence[str | float | int]
+ input_type: Literal['activity', 'a', 'timestamps', 't', 'datetime', 'd'] = 'a'
+ threshold: float = .8
+ time_format: str = "%Y-%m-%dT%H:%M:%S.%fZ"
+ timezone: float = 0.
+ range: None | tuple[str | float | int, str | float | int] = None
+ granularity: Literal['week', 'day', 'hour'] | str = 'day'
+ reference_name: str = ""
+
+ def __init__(self, **kwargs):
+ kwargs['input_sequence'] = [str(item) for item in kwargs['input_sequence']]
+ if 'range' in kwargs and kwargs['range'] is not None:
+ kwargs['range'] = (str(kwargs['range'][0]), str(kwargs['range'][1]))
+ super().__init__(**kwargs)
+
+
+@dataclass
+class ProcessedData:
+ name: str
+ activity: NDArray[float]
+ threshold: float = 0.8
+
+
+def process_input(
+ input_folder: str | Path | None = None,
+ recursive: bool = True,
+ input_file: str | Path | None = None,
+ input_remote_data: InputData | None = None,
+ input_dict: dict | None = None,
+ input_seq: Sequence[float | int | str] | None = None,
+ threshold: float = 0.8) -> list[ProcessedData]:
+ output = []
+
+ if input_folder:
+ output.extend(process_folder(path=Path(input_folder), recursive=recursive, threshold=threshold))
+
+ if input_file:
+ output.extend(process_file(path=Path(input_file), threshold=threshold))
+
+ if input_remote_data:
+ output.append(process_formatted_data(input_remote_data))
+
+ if input_dict:
+ output.append(process_formatted_data(InputData(**input_dict)))
+
+ if input_seq:
+ if threshold is None:
+ raise ValueError('Threshold value is not defined!')
+ output.append(ProcessedData(name="sequence", activity=np.ndarray(input_seq, dtype=float), threshold=threshold))
+
+ return output
+
+
+def process_folder(path: Path, recursive: bool = True, threshold: float | None = None) -> list[ProcessedData]:
+ output = []
+ for file in path.iterdir():
+ if file.is_file():
+ output.extend(process_file(path=file, threshold=threshold))
+ elif file.is_dir() and recursive:
+ output.extend(process_folder(path=file, recursive=recursive, threshold=threshold))
+ return output
+
+
+def process_file(path: Path, threshold: float | None = None) -> list[ProcessedData]:
+ if path.suffix == '.json':
+ return process_json(path)
+ elif path.suffix == '.csv':
+ return [ProcessedData(name=f"{str(path.resolve())}[{i}]", activity=sequence, threshold=threshold)
+ for i, sequence in enumerate(process_csv(path))]
+ else:
+ return []
+
+
+def process_json(path: Path) -> list[ProcessedData]:
+ with open(path, 'r') as f:
+ input_object = json.load(f)
+
+ if isinstance(input_object, dict):
+ input_object = [input_object]
+
+ output = []
+ for i, input_case in enumerate(input_object):
+ input_case = InputData(**input_case)
+ try:
+ case_output = process_formatted_data(input_case)
+ if not case_output.name:
+ case_output.name = f"{str(path.resolve())}[{i}]"
+ output.append(case_output)
+ except ValueError:
+ warnings.warn(
+ f'\"input_type\" is not one of [\"activity\", \"a\", \"timestamps\", \"t\", \"datetime\", \"d\"]!'
+ f' Ignoring file \"{str(path.resolve())}\"!')
+
+ return output
+
+
+def process_formatted_data(input_data: InputData) -> ProcessedData:
+ if input_data.input_type == 'activity' or input_data.input_type == 'a':
+ return ProcessedData(
+ name=input_data.reference_name,
+ activity=np.array(input_data.input_sequence, dtype=float),
+ threshold=input_data.threshold
+ )
+ elif input_data.input_type == 'timestamps' or input_data.input_type == 't':
+ return ProcessedData(
+ name=input_data.reference_name,
+ activity=timestamps_to_activity(np.array(input_data.input_sequence, dtype=float),
+ input_data.range,
+ input_data.granularity),
+ threshold=input_data.threshold
+ )
+ elif input_data.input_type == 'datetime' or input_data.input_type == 'd':
+ timestamps = [datetime.strptime(time_point, input_data.time_format).replace(
+ tzinfo=timezone(timedelta(hours=input_data.timezone))).timestamp()
+ for time_point in input_data.input_sequence]
+ if input_data.range is not None:
+ ts_range = (
+ datetime.strptime(input_data.range[0], input_data.time_format).replace(
+ tzinfo=timezone(timedelta(hours=input_data.timezone))).timestamp(),
+ datetime.strptime(input_data.range[1], input_data.time_format).replace(
+ tzinfo=timezone(timedelta(hours=input_data.timezone))).timestamp()
+ )
+ else:
+ ts_range = None
+ return ProcessedData(
+ name=input_data.reference_name,
+ activity=timestamps_to_activity(np.array(timestamps, dtype=float), ts_range, input_data.granularity),
+ threshold=input_data.threshold
+ )
+ else:
+ raise ValueError("Wrong \"input_type\" value!")
+
+
+def process_csv(path: Path) -> list[NDArray[float]]:
+ output = []
+ with open(path, 'r') as f:
+ for line in f:
+ if line:
+ output.append(np.fromstring(line.strip(), dtype=float, sep=','))
+ return output
+
+
+def timestamps_to_activity(timestamps: Sequence[float | int | str],
+ ts_range: None | tuple[float | int | str, float | int | str] = None,
+ granularity: str = 'day') -> NDArray[float]:
+ if not isinstance(timestamps, np.ndarray) or timestamps.dtype != float:
+ timestamps = np.array(timestamps, dtype=float)
+ if ts_range is None:
+ ts_range = (np.min(timestamps), np.max(timestamps))
+ if granularity == 'week':
+ bin_width = SECONDS_WEEK
+ elif granularity == 'day':
+ bin_width = SECONDS_DAY
+ elif granularity == 'hour':
+ bin_width = SECONDS_HOUR
+ elif granularity[-1] == 'd' and _is_float(granularity[:-1]):
+ bin_width = float(granularity[:-1]) * SECONDS_DAY
+ elif granularity[-1] == 'h' and _is_float(granularity[:-1]):
+ bin_width = float(granularity[:-1]) * SECONDS_HOUR
+ else:
+ raise ValueError(f"Invalid granularity value: {granularity}!")
+
+ # print(f"\n\nDEBUG: ts_range[0]: {ts_range[0]}, ts_range[1]: {ts_range[1]}, bin_width: {bin_width}\n\n")
+
+ bins = np.arange(ts_range[0], ts_range[1], bin_width)
+ if not np.isclose(bins[-1], ts_range[1]):
+ bins = np.append(bins, ts_range[1])
+
+ activity, _ = np.histogram(np.array(timestamps, dtype=float), bins=bins)
+ activity.dtype = float
+ return activity
+
+
+def _is_float(num: str) -> bool:
+ try:
+ float(num)
+ except ValueError:
+ return False
+ return True
diff --git a/ephemerality/src/ephemerality_computation.py b/ephemerality/src/ephemerality_computation.py
new file mode 100644
index 0000000..f8bbb09
--- /dev/null
+++ b/ephemerality/src/ephemerality_computation.py
@@ -0,0 +1,352 @@
+from typing import Sequence
+
+import numpy as np
+from numpy.typing import NDArray
+from matplotlib import pyplot as plt
+from matplotlib.axes import Axes
+from matplotlib.figure import Figure
+from .utils import EphemeralitySet
+
+
+def _check_threshold(threshold: float) -> bool:
+ if threshold <= 0.:
+ raise ValueError('Threshold value must be greater than 0!')
+
+ if threshold > 1.:
+ raise ValueError('Threshold value must be less or equal to 1!')
+
+ return True
+
+
+def _ephemerality_raise_error(threshold: float) -> None:
+ if _check_threshold(threshold):
+ raise ValueError('Input activity vector has not been internally normalized (problematic data format?)!')
+
+
+def _normalize_activity_vector(activity_vector: Sequence[float]) -> NDArray:
+ activity_vector = np.array(activity_vector, dtype=np.float64)
+
+ if sum(activity_vector) != 1.:
+ activity_vector /= np.sum(activity_vector)
+
+ return activity_vector
+
+
+def _str_to_core_types(type_str: str) -> tuple[set[str], int]:
+ types = set()
+ for c in type_str.lower():
+ match c:
+ case 'l':
+ types.add('l')
+ case 'm':
+ types.add('m')
+ case 'r':
+ types.add('r')
+ case 's':
+ types.add('s')
+ case _:
+ raise ValueError(f'Unrecognized core type: {c}')
+ n_types = len(types)
+ return types, n_types
+
+
+def _init_fig(core_types: set[str]) -> tuple[Figure, list[Axes]]:
+ fig = plt.figure()
+ n_cores = len(core_types)
+ axes = list()
+ match n_cores:
+ case 1:
+ gs = fig.add_gridspec(nrows=1, ncols=1)
+ grid = [gs[0, 0]]
+ case 2:
+ gs = fig.add_gridspec(nrows=1, ncols=2)
+ grid = [gs[0, 0], gs[0, 1]]
+ case 3:
+ gs = fig.add_gridspec(nrows=2, ncols=1)
+ gs_0 = gs[0].subgridspec(nrows=1, ncols=2)
+ gs_1 = gs[1].subgridspec(nrows=1, ncols=3, width_ratios=[1, 2, 1])
+ grid = [gs_0[0, 0], gs_0[0, 1], gs_1[0, 1]]
+ case 4:
+ gs = fig.add_gridspec(nrows=2, ncols=2)
+ grid = [gs[0, 0], gs[0, 1], gs[1, 0], gs[1, 1]]
+ for i in range(n_cores):
+ axes.append(fig.add_subplot(grid[i]))
+
+ return fig, axes
+
+
+def _annotate_ax(ax: Axes, x_len: int, core_type: str, core_length: int, ephemerality: float, threshold: float) -> Axes:
+ ax.set_xlim((0, x_len))
+ ax.set_xlabel('Time')
+ ax.set_ylabel('Normalized activity')
+ ax.set_title(rf'{core_type} core (length {core_length}), $\varepsilon_{{{threshold}}}^{core_type[0].lower()}={np.round(ephemerality, 3)}$')
+ return ax
+
+
+def compute_left_core_length(activity_vector: NDArray, threshold: float, axes: Axes | None = None) -> int:
+ """
+ Compute the length of the left core of the activity vector.
+
+ Parameters
+ ----------
+ activity_vector : numpy.typing.NDArray
+ A normalized sequence of activity values. Time bins corresponding to each value is assumed to be of equal
+ length.
+ threshold : float
+ Ratio of activity that is considered to comprise its core part.
+ axes : Optional[Axes]
+ If provided, will plot the histogram of the activity vector and color its core part on it.
+
+ Returns
+ -------
+ int
+ The length of the left core period of the activity vector.
+ """
+ current_sum = 0
+ for i, freq in enumerate(activity_vector):
+ current_sum = current_sum + freq
+ if np.isclose(current_sum, threshold) or current_sum > threshold:
+ if axes is not None:
+ axes.stairs(activity_vector[:(i + 1)], np.arange(i + 2), fill=True, color='C1')
+ axes.stairs(activity_vector, np.arange(len(activity_vector) + 1), fill=False, color='C0')
+ return i + 1
+
+ _ephemerality_raise_error(threshold)
+
+
+def compute_right_core_length(activity_vector: NDArray, threshold: float, axes: Axes | None = None) -> int:
+ """
+ Compute the length of the right core of the activity vector.
+
+ Parameters
+ ----------
+ activity_vector : numpy.typing.NDArray
+ A normalized sequence of activity values. Time bins corresponding to each value is assumed to be of equal
+ length.
+ threshold : float
+ Ratio of activity that is considered to comprise its core part.
+ axes : Optional[Axes]
+ If provided, will plot the histogram of the activity vector and color its core part on it.
+
+ Returns
+ -------
+ int
+ The length of the right core period of the activity vector.
+ """
+ current_sum = 0
+ for i, freq in enumerate(activity_vector[::-1]):
+ current_sum = current_sum + freq
+ if np.isclose(current_sum, threshold) or current_sum > threshold:
+ if axes is not None:
+ bound = len(activity_vector) - (i + 1)
+ axes.stairs(activity_vector[bound:], np.arange(bound, len(activity_vector) + 1), fill=True, color='C1')
+ axes.stairs(activity_vector, np.arange(len(activity_vector) + 1), fill=False, color='C0')
+ return i + 1
+
+ _ephemerality_raise_error(threshold)
+
+
+def compute_middle_core_length(activity_vector: NDArray, threshold: float, axes: Axes | None = None) -> int:
+ """
+ Compute the length of the middle core of the activity vector.
+
+ Parameters
+ ----------
+ activity_vector : numpy.typing.NDArray
+ A normalized sequence of activity values. Time bins corresponding to each value is assumed to be of equal
+ length.
+ threshold : float
+ Ratio of activity that is considered to comprise its core part.
+ axes : Optional[Axes]
+ If provided, will plot the histogram of the activity vector and color its core part on it.
+
+ Returns
+ -------
+ int
+ The length of the middle core period of the activity vector.
+ """
+ lower_threshold = (1. - threshold) / 2
+
+ current_left_sum = 0
+ start_index = -1
+ for i, freq in enumerate(activity_vector):
+ current_left_sum += freq
+ if current_left_sum > lower_threshold and not np.isclose(current_left_sum, lower_threshold):
+ start_index = i
+ break
+
+ current_sum = 0
+ for j, freq in enumerate(activity_vector[start_index:]):
+ current_sum += freq
+ if np.isclose(current_sum, threshold) or current_sum > threshold:
+ if axes is not None:
+ axes.stairs(activity_vector[start_index:(start_index + j + 1)], np.arange(start_index, start_index + j + 2), fill=True, color='C1')
+ axes.stairs(activity_vector, np.arange(len(activity_vector) + 1), fill=False, color='C0')
+ return j + 1
+
+ _ephemerality_raise_error(threshold)
+
+
+def compute_sorted_core_length(activity_vector: np.array, threshold: float, axes: Axes | None = None) -> int:
+ """
+ Compute the length of the sorted core of the activity vector.
+
+ Parameters
+ ----------
+ activity_vector : numpy.typing.NDArray
+ A normalized sequence of activity values. Time bins corresponding to each value is assumed to be of equal
+ length.
+ threshold : float
+ Ratio of activity that is considered to comprise its core part.
+ axes : Optional[Axes]
+ If provided, will plot the histogram of the activity vector and color its core part on it.
+
+ Returns
+ -------
+ int
+ The length of the sorted core period of the activity vector.
+ """
+ indices = np.argsort(activity_vector)[::-1]
+ freq_descending_order = activity_vector[indices]
+
+ current_sum = 0
+ for i, freq in enumerate(freq_descending_order):
+ current_sum += freq
+ if np.isclose(current_sum, threshold) or current_sum > threshold:
+ if axes is not None:
+ core = np.zeros((len(activity_vector),))
+ core[indices[:(i + 1)]] = activity_vector[indices[:(i + 1)]]
+ axes.stairs(core, np.arange(len(activity_vector) + 1), fill=True, color='C1')
+
+ axes.stairs(activity_vector, np.arange(len(activity_vector) + 1), fill=False, color='C0')
+ return i + 1
+
+ _ephemerality_raise_error(threshold)
+
+
+def _compute_ephemerality_from_core(core_length: int, range_length: int, threshold: float) -> float:
+ return max(0., 1 - (core_length / range_length) / threshold)
+
+
+def _plot(axes: list[Axes]) -> None:
+ fig = plt.figure()
+ n_cores = len(axes)
+
+
+def compute_ephemerality(
+ activity_vector: Sequence[float | int],
+ threshold: float = 0.8,
+ types: str = 'lmrs',
+ plot: bool = False) -> EphemeralitySet:
+ """Compute ephemerality values for given activity vector.
+
+ This function computes ephemerality for a numeric vector using all current definitions of actiovity cores.
+ Alpha (desired non-ephemeral core length) can be specified with ``threshold parameter. In case not all cores are
+ needed, the required types can be specified in ``types``.
+
+ Parameters
+ ----------
+ activity_vector : Sequence[float | int]
+ A sequence of activity values. Time bins corresponding to each value is assumed to be of equal length. Does not
+ need to be normalised.
+ threshold : float, default=0.8
+ Desired non-ephemeral core length as fraction. Ephemerality is equal to 1.0 if the core length is at least ``threshold`` of
+ the ``activity_vector`` length.
+ types : str, default='lmrs'
+ Activity cores to be computed. A sequence of characters corresponding to core types.
+ 'l' for left core, 'm' for middle core, 'r' for right core, 's' for sorted core. Multiples of the same character
+ will be ignored.
+ plot : bool, default=False
+ Set to True to display the activity over time plot with core periods highlighted.
+
+ Returns
+ -------
+ EphemeralitySet
+ Container for the computed core lengths and ephemerality values
+ """
+ _check_threshold(threshold)
+
+ if np.sum(activity_vector) == 0.:
+ raise ZeroDivisionError("Activity vector's sum is 0!")
+ types, n_cores = _str_to_core_types(types)
+
+ activity_vector = _normalize_activity_vector(activity_vector)
+ len_range = len(activity_vector)
+
+ if plot:
+ fig, axes = _init_fig(types)
+ else:
+ axes = None
+ subplot_types = list()
+
+ core_ind = 0
+ if 'l' in types:
+ ax = axes[core_ind] if plot else None
+ subplot_types.append('l')
+ length_left_core = compute_left_core_length(activity_vector, threshold, axes=ax)
+ ephemerality_left_core = _compute_ephemerality_from_core(length_left_core, len_range, threshold)
+ core_ind += 1
+ else:
+ length_left_core = None
+ ephemerality_left_core = None
+
+ if 'm' in types:
+ ax = axes[core_ind] if plot else None
+ subplot_types.append('m')
+ length_middle_core = compute_middle_core_length(activity_vector, threshold, axes=ax)
+ ephemerality_middle_core = _compute_ephemerality_from_core(length_middle_core, len_range, threshold)
+ core_ind += 1
+ else:
+ length_middle_core = None
+ ephemerality_middle_core = None
+
+ if 'r' in types:
+ ax = axes[core_ind] if plot else None
+ subplot_types.append('r')
+ length_right_core = compute_right_core_length(activity_vector, threshold, axes=ax)
+ ephemerality_right_core = _compute_ephemerality_from_core(length_right_core, len_range, threshold)
+ core_ind += 1
+ else:
+ length_right_core = None
+ ephemerality_right_core = None
+
+ if 's' in types:
+ ax = axes[core_ind] if plot else None
+ subplot_types.append('s')
+ length_sorted_core = compute_sorted_core_length(activity_vector, threshold, axes=ax)
+ ephemerality_sorted_core = _compute_ephemerality_from_core(length_sorted_core, len_range, threshold)
+ core_ind += 1
+ else:
+ length_sorted_core = None
+ ephemerality_sorted_core = None
+
+ if n_cores == 0:
+ raise ValueError('No valid ephemerality cores requested!')
+
+ ephemeralities = EphemeralitySet(
+ len_left_core=length_left_core,
+ len_middle_core=length_middle_core,
+ len_right_core=length_right_core,
+ len_sorted_core=length_sorted_core,
+
+ eph_left_core=ephemerality_left_core,
+ eph_middle_core=ephemerality_middle_core,
+ eph_right_core=ephemerality_right_core,
+ eph_sorted_core=ephemerality_sorted_core
+ )
+
+ if plot:
+ for i, subplot_type in enumerate(subplot_types):
+ match subplot_type:
+ case 'l':
+ _annotate_ax(axes[i], len_range, 'Left', ephemeralities.len_left_core, ephemeralities.eph_left_core, threshold)
+ case 'm':
+ _annotate_ax(axes[i], len_range, 'Middle', ephemeralities.len_middle_core, ephemeralities.eph_middle_core, threshold)
+ case 'r':
+ _annotate_ax(axes[i], len_range, 'Right', ephemeralities.len_right_core, ephemeralities.eph_right_core, threshold)
+ case 's':
+ _annotate_ax(axes[i], len_range, 'Sorted', ephemeralities.len_sorted_core, ephemeralities.eph_sorted_core, threshold)
+ fig.tight_layout()
+ fig.show()
+
+ return ephemeralities
diff --git a/ephemerality/src/utils.py b/ephemerality/src/utils.py
new file mode 100644
index 0000000..5063891
--- /dev/null
+++ b/ephemerality/src/utils.py
@@ -0,0 +1,73 @@
+import numpy as np
+from pydantic import BaseModel
+
+
+class EphemeralitySet(BaseModel):
+ """
+ Container for ephemerality and core size values.
+
+ This class is a simple pydantic BaseModel used to store computed core lengths and corresponding ephemerality values
+ by core type. Values that were not computed default to None.
+
+ Attributes
+ ----------
+ len_left_core : int, optional
+ Length of the left core in time bins
+ len_middle_core : int, optional
+ Length of the middle core in time bins
+ len_right_core : int, optional
+ Length of the right core in time bins
+ len_sorted_core : int, optional
+ Length of the sorted core in time bins
+
+ eph_left_core: float, optional
+ Ephemerality value for the left core
+ eph_middle_core: float, optional
+ Ephemerality value for the middle core
+ eph_right_core: float, optional
+ Ephemerality value for the right core
+ eph_sorted_core: float, optional
+ Ephemerality value for the sorted core
+ """
+ len_left_core: int | None = None
+ len_middle_core: int | None = None
+ len_right_core: int | None = None
+ len_sorted_core: int | None = None
+
+ eph_left_core: float | None = None
+ eph_middle_core: float | None = None
+ eph_right_core: float | None = None
+ eph_sorted_core: float | None = None
+
+ def __eq__(self, other) -> bool:
+ if isinstance(other, EphemeralitySet):
+ if \
+ self.len_left_core != other.len_left_core or \
+ self.len_middle_core != other.len_middle_core or \
+ self.len_right_core != other.len_right_core or \
+ self.len_sorted_core != other.len_sorted_core or \
+ not self._cmp_float_none(self.eph_left_core, other.eph_left_core) or \
+ not self._cmp_float_none(self.eph_middle_core, other.eph_middle_core) or \
+ not self._cmp_float_none(self.eph_right_core, other.eph_right_core) or \
+ not self._cmp_float_none(self.eph_sorted_core, other.eph_sorted_core):
+ return False
+ else:
+ return True
+ else:
+ return False
+
+ @staticmethod
+ def _cmp_float_none(value1: float | None, value2: float | None) -> bool:
+ if value1 is None and value2 is None:
+ return True
+ elif value1 is None or value2 is None:
+ return False
+ else:
+ return np.isclose(value1, value2)
+
+ def __str__(self) -> str:
+ values = ', '.join([f'{pair[0]}={pair[1]}' for pair in sorted(list(self.dict(exclude_none=True).items()), key=lambda x: x[0])])
+ return f'{self.__class__.__name__}({values})'
+
+ def __repr__(self) -> str:
+ return str(self)
diff --git a/images/example_1.png b/images/example_1.png
new file mode 100644
index 0000000..53d5596
Binary files /dev/null and b/images/example_1.png differ
diff --git a/images/example_2.png b/images/example_2.png
new file mode 100644
index 0000000..369081a
Binary files /dev/null and b/images/example_2.png differ
diff --git a/images/example_3.png b/images/example_3.png
new file mode 100644
index 0000000..0aef215
Binary files /dev/null and b/images/example_3.png differ
diff --git a/images/example_4.png b/images/example_4.png
new file mode 100644
index 0000000..84149f7
Binary files /dev/null and b/images/example_4.png differ
diff --git a/images/results_1.png b/images/results_1.png
new file mode 100644
index 0000000..1afbb7a
Binary files /dev/null and b/images/results_1.png differ
diff --git a/images/results_2.png b/images/results_2.png
new file mode 100644
index 0000000..d07fef7
Binary files /dev/null and b/images/results_2.png differ
diff --git a/images/results_3.png b/images/results_3.png
new file mode 100644
index 0000000..8db530c
Binary files /dev/null and b/images/results_3.png differ
diff --git a/images/results_4.png b/images/results_4.png
new file mode 100644
index 0000000..c8e4238
Binary files /dev/null and b/images/results_4.png differ
diff --git a/poetry.lock b/poetry.lock
new file mode 100644
index 0000000..d0a8124
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,771 @@
+# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
+
+[[package]]
+name = "annotated-types"
+version = "0.6.0"
+description = "Reusable constraint types to use with typing.Annotated"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"},
+ {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"},
+]
+
+[[package]]
+name = "anyio"
+version = "4.3.0"
+description = "High level compatibility layer for multiple asynchronous event loop implementations"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"},
+ {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"},
+]
+
+[package.dependencies]
+exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""}
+idna = ">=2.8"
+sniffio = ">=1.1"
+typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""}
+
+[package.extras]
+doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
+test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
+trio = ["trio (>=0.23)"]
+
+[[package]]
+name = "click"
+version = "8.1.7"
+description = "Composable command line interface toolkit"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
+ {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+description = "Cross-platform colored terminal text."
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+files = [
+ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
+ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
+]
+
+[[package]]
+name = "contourpy"
+version = "1.2.1"
+description = "Python library for calculating contours of 2D quadrilateral grids"
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "contourpy-1.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bd7c23df857d488f418439686d3b10ae2fbf9bc256cd045b37a8c16575ea1040"},
+ {file = "contourpy-1.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5b9eb0ca724a241683c9685a484da9d35c872fd42756574a7cfbf58af26677fd"},
+ {file = "contourpy-1.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c75507d0a55378240f781599c30e7776674dbaf883a46d1c90f37e563453480"},
+ {file = "contourpy-1.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11959f0ce4a6f7b76ec578576a0b61a28bdc0696194b6347ba3f1c53827178b9"},
+ {file = "contourpy-1.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb3315a8a236ee19b6df481fc5f997436e8ade24a9f03dfdc6bd490fea20c6da"},
+ {file = "contourpy-1.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39f3ecaf76cd98e802f094e0d4fbc6dc9c45a8d0c4d185f0f6c2234e14e5f75b"},
+ {file = "contourpy-1.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:94b34f32646ca0414237168d68a9157cb3889f06b096612afdd296003fdd32fd"},
+ {file = "contourpy-1.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:457499c79fa84593f22454bbd27670227874cd2ff5d6c84e60575c8b50a69619"},
+ {file = "contourpy-1.2.1-cp310-cp310-win32.whl", hash = "sha256:ac58bdee53cbeba2ecad824fa8159493f0bf3b8ea4e93feb06c9a465d6c87da8"},
+ {file = "contourpy-1.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:9cffe0f850e89d7c0012a1fb8730f75edd4320a0a731ed0c183904fe6ecfc3a9"},
+ {file = "contourpy-1.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6022cecf8f44e36af10bd9118ca71f371078b4c168b6e0fab43d4a889985dbb5"},
+ {file = "contourpy-1.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef5adb9a3b1d0c645ff694f9bca7702ec2c70f4d734f9922ea34de02294fdf72"},
+ {file = "contourpy-1.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6150ffa5c767bc6332df27157d95442c379b7dce3a38dff89c0f39b63275696f"},
+ {file = "contourpy-1.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c863140fafc615c14a4bf4efd0f4425c02230eb8ef02784c9a156461e62c965"},
+ {file = "contourpy-1.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:00e5388f71c1a0610e6fe56b5c44ab7ba14165cdd6d695429c5cd94021e390b2"},
+ {file = "contourpy-1.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4492d82b3bc7fbb7e3610747b159869468079fe149ec5c4d771fa1f614a14df"},
+ {file = "contourpy-1.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:49e70d111fee47284d9dd867c9bb9a7058a3c617274900780c43e38d90fe1205"},
+ {file = "contourpy-1.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b59c0ffceff8d4d3996a45f2bb6f4c207f94684a96bf3d9728dbb77428dd8cb8"},
+ {file = "contourpy-1.2.1-cp311-cp311-win32.whl", hash = "sha256:7b4182299f251060996af5249c286bae9361fa8c6a9cda5efc29fe8bfd6062ec"},
+ {file = "contourpy-1.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2855c8b0b55958265e8b5888d6a615ba02883b225f2227461aa9127c578a4922"},
+ {file = "contourpy-1.2.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:62828cada4a2b850dbef89c81f5a33741898b305db244904de418cc957ff05dc"},
+ {file = "contourpy-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:309be79c0a354afff9ff7da4aaed7c3257e77edf6c1b448a779329431ee79d7e"},
+ {file = "contourpy-1.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e785e0f2ef0d567099b9ff92cbfb958d71c2d5b9259981cd9bee81bd194c9a4"},
+ {file = "contourpy-1.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1cac0a8f71a041aa587410424ad46dfa6a11f6149ceb219ce7dd48f6b02b87a7"},
+ {file = "contourpy-1.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af3f4485884750dddd9c25cb7e3915d83c2db92488b38ccb77dd594eac84c4a0"},
+ {file = "contourpy-1.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ce6889abac9a42afd07a562c2d6d4b2b7134f83f18571d859b25624a331c90b"},
+ {file = "contourpy-1.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a1eea9aecf761c661d096d39ed9026574de8adb2ae1c5bd7b33558af884fb2ce"},
+ {file = "contourpy-1.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:187fa1d4c6acc06adb0fae5544c59898ad781409e61a926ac7e84b8f276dcef4"},
+ {file = "contourpy-1.2.1-cp312-cp312-win32.whl", hash = "sha256:c2528d60e398c7c4c799d56f907664673a807635b857df18f7ae64d3e6ce2d9f"},
+ {file = "contourpy-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:1a07fc092a4088ee952ddae19a2b2a85757b923217b7eed584fdf25f53a6e7ce"},
+ {file = "contourpy-1.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bb6834cbd983b19f06908b45bfc2dad6ac9479ae04abe923a275b5f48f1a186b"},
+ {file = "contourpy-1.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1d59e739ab0e3520e62a26c60707cc3ab0365d2f8fecea74bfe4de72dc56388f"},
+ {file = "contourpy-1.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd3db01f59fdcbce5b22afad19e390260d6d0222f35a1023d9adc5690a889364"},
+ {file = "contourpy-1.2.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a12a813949e5066148712a0626895c26b2578874e4cc63160bb007e6df3436fe"},
+ {file = "contourpy-1.2.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe0ccca550bb8e5abc22f530ec0466136379c01321fd94f30a22231e8a48d985"},
+ {file = "contourpy-1.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1d59258c3c67c865435d8fbeb35f8c59b8bef3d6f46c1f29f6123556af28445"},
+ {file = "contourpy-1.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f32c38afb74bd98ce26de7cc74a67b40afb7b05aae7b42924ea990d51e4dac02"},
+ {file = "contourpy-1.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d31a63bc6e6d87f77d71e1abbd7387ab817a66733734883d1fc0021ed9bfa083"},
+ {file = "contourpy-1.2.1-cp39-cp39-win32.whl", hash = "sha256:ddcb8581510311e13421b1f544403c16e901c4e8f09083c881fab2be80ee31ba"},
+ {file = "contourpy-1.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:10a37ae557aabf2509c79715cd20b62e4c7c28b8cd62dd7d99e5ed3ce28c3fd9"},
+ {file = "contourpy-1.2.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a31f94983fecbac95e58388210427d68cd30fe8a36927980fab9c20062645609"},
+ {file = "contourpy-1.2.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef2b055471c0eb466033760a521efb9d8a32b99ab907fc8358481a1dd29e3bd3"},
+ {file = "contourpy-1.2.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b33d2bc4f69caedcd0a275329eb2198f560b325605810895627be5d4b876bf7f"},
+ {file = "contourpy-1.2.1.tar.gz", hash = "sha256:4d8908b3bee1c889e547867ca4cdc54e5ab6be6d3e078556814a22457f49423c"},
+]
+
+[package.dependencies]
+numpy = ">=1.20"
+
+[package.extras]
+bokeh = ["bokeh", "selenium"]
+docs = ["furo", "sphinx (>=7.2)", "sphinx-copybutton"]
+mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.8.0)", "types-Pillow"]
+test = ["Pillow", "contourpy[test-no-images]", "matplotlib"]
+test-no-images = ["pytest", "pytest-cov", "pytest-xdist", "wurlitzer"]
+
+[[package]]
+name = "cycler"
+version = "0.12.1"
+description = "Composable style cycles"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30"},
+ {file = "cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"},
+]
+
+[package.extras]
+docs = ["ipython", "matplotlib", "numpydoc", "sphinx"]
+tests = ["pytest", "pytest-cov", "pytest-xdist"]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.2.1"
+description = "Backport of PEP 654 (exception groups)"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"},
+ {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"},
+]
+
+[package.extras]
+test = ["pytest (>=6)"]
+
+[[package]]
+name = "fastapi"
+version = "0.110.3"
+description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "fastapi-0.110.3-py3-none-any.whl", hash = "sha256:fd7600612f755e4050beb74001310b5a7e1796d149c2ee363124abdfa0289d32"},
+ {file = "fastapi-0.110.3.tar.gz", hash = "sha256:555700b0159379e94fdbfc6bb66a0f1c43f4cf7060f25239af3d84b63a656626"},
+]
+
+[package.dependencies]
+pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0"
+starlette = ">=0.37.2,<0.38.0"
+typing-extensions = ">=4.8.0"
+
+[package.extras]
+all = ["email_validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
+
+[[package]]
+name = "fonttools"
+version = "4.51.0"
+description = "Tools to manipulate font files"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "fonttools-4.51.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:84d7751f4468dd8cdd03ddada18b8b0857a5beec80bce9f435742abc9a851a74"},
+ {file = "fonttools-4.51.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8b4850fa2ef2cfbc1d1f689bc159ef0f45d8d83298c1425838095bf53ef46308"},
+ {file = "fonttools-4.51.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5b48a1121117047d82695d276c2af2ee3a24ffe0f502ed581acc2673ecf1037"},
+ {file = "fonttools-4.51.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:180194c7fe60c989bb627d7ed5011f2bef1c4d36ecf3ec64daec8302f1ae0716"},
+ {file = "fonttools-4.51.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:96a48e137c36be55e68845fc4284533bda2980f8d6f835e26bca79d7e2006438"},
+ {file = "fonttools-4.51.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:806e7912c32a657fa39d2d6eb1d3012d35f841387c8fc6cf349ed70b7c340039"},
+ {file = "fonttools-4.51.0-cp310-cp310-win32.whl", hash = "sha256:32b17504696f605e9e960647c5f64b35704782a502cc26a37b800b4d69ff3c77"},
+ {file = "fonttools-4.51.0-cp310-cp310-win_amd64.whl", hash = "sha256:c7e91abdfae1b5c9e3a543f48ce96013f9a08c6c9668f1e6be0beabf0a569c1b"},
+ {file = "fonttools-4.51.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a8feca65bab31479d795b0d16c9a9852902e3a3c0630678efb0b2b7941ea9c74"},
+ {file = "fonttools-4.51.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8ac27f436e8af7779f0bb4d5425aa3535270494d3bc5459ed27de3f03151e4c2"},
+ {file = "fonttools-4.51.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e19bd9e9964a09cd2433a4b100ca7f34e34731e0758e13ba9a1ed6e5468cc0f"},
+ {file = "fonttools-4.51.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2b92381f37b39ba2fc98c3a45a9d6383bfc9916a87d66ccb6553f7bdd129097"},
+ {file = "fonttools-4.51.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5f6bc991d1610f5c3bbe997b0233cbc234b8e82fa99fc0b2932dc1ca5e5afec0"},
+ {file = "fonttools-4.51.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9696fe9f3f0c32e9a321d5268208a7cc9205a52f99b89479d1b035ed54c923f1"},
+ {file = "fonttools-4.51.0-cp311-cp311-win32.whl", hash = "sha256:3bee3f3bd9fa1d5ee616ccfd13b27ca605c2b4270e45715bd2883e9504735034"},
+ {file = "fonttools-4.51.0-cp311-cp311-win_amd64.whl", hash = "sha256:0f08c901d3866a8905363619e3741c33f0a83a680d92a9f0e575985c2634fcc1"},
+ {file = "fonttools-4.51.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4060acc2bfa2d8e98117828a238889f13b6f69d59f4f2d5857eece5277b829ba"},
+ {file = "fonttools-4.51.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:1250e818b5f8a679ad79660855528120a8f0288f8f30ec88b83db51515411fcc"},
+ {file = "fonttools-4.51.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76f1777d8b3386479ffb4a282e74318e730014d86ce60f016908d9801af9ca2a"},
+ {file = "fonttools-4.51.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b5ad456813d93b9c4b7ee55302208db2b45324315129d85275c01f5cb7e61a2"},
+ {file = "fonttools-4.51.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:68b3fb7775a923be73e739f92f7e8a72725fd333eab24834041365d2278c3671"},
+ {file = "fonttools-4.51.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8e2f1a4499e3b5ee82c19b5ee57f0294673125c65b0a1ff3764ea1f9db2f9ef5"},
+ {file = "fonttools-4.51.0-cp312-cp312-win32.whl", hash = "sha256:278e50f6b003c6aed19bae2242b364e575bcb16304b53f2b64f6551b9c000e15"},
+ {file = "fonttools-4.51.0-cp312-cp312-win_amd64.whl", hash = "sha256:b3c61423f22165541b9403ee39874dcae84cd57a9078b82e1dce8cb06b07fa2e"},
+ {file = "fonttools-4.51.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:1621ee57da887c17312acc4b0e7ac30d3a4fb0fec6174b2e3754a74c26bbed1e"},
+ {file = "fonttools-4.51.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e9d9298be7a05bb4801f558522adbe2feea1b0b103d5294ebf24a92dd49b78e5"},
+ {file = "fonttools-4.51.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee1af4be1c5afe4c96ca23badd368d8dc75f611887fb0c0dac9f71ee5d6f110e"},
+ {file = "fonttools-4.51.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c18b49adc721a7d0b8dfe7c3130c89b8704baf599fb396396d07d4aa69b824a1"},
+ {file = "fonttools-4.51.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:de7c29bdbdd35811f14493ffd2534b88f0ce1b9065316433b22d63ca1cd21f14"},
+ {file = "fonttools-4.51.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cadf4e12a608ef1d13e039864f484c8a968840afa0258b0b843a0556497ea9ed"},
+ {file = "fonttools-4.51.0-cp38-cp38-win32.whl", hash = "sha256:aefa011207ed36cd280babfaa8510b8176f1a77261833e895a9d96e57e44802f"},
+ {file = "fonttools-4.51.0-cp38-cp38-win_amd64.whl", hash = "sha256:865a58b6e60b0938874af0968cd0553bcd88e0b2cb6e588727117bd099eef836"},
+ {file = "fonttools-4.51.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:60a3409c9112aec02d5fb546f557bca6efa773dcb32ac147c6baf5f742e6258b"},
+ {file = "fonttools-4.51.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f7e89853d8bea103c8e3514b9f9dc86b5b4120afb4583b57eb10dfa5afbe0936"},
+ {file = "fonttools-4.51.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56fc244f2585d6c00b9bcc59e6593e646cf095a96fe68d62cd4da53dd1287b55"},
+ {file = "fonttools-4.51.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d145976194a5242fdd22df18a1b451481a88071feadf251221af110ca8f00ce"},
+ {file = "fonttools-4.51.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c5b8cab0c137ca229433570151b5c1fc6af212680b58b15abd797dcdd9dd5051"},
+ {file = "fonttools-4.51.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:54dcf21a2f2d06ded676e3c3f9f74b2bafded3a8ff12f0983160b13e9f2fb4a7"},
+ {file = "fonttools-4.51.0-cp39-cp39-win32.whl", hash = "sha256:0118ef998a0699a96c7b28457f15546815015a2710a1b23a7bf6c1be60c01636"},
+ {file = "fonttools-4.51.0-cp39-cp39-win_amd64.whl", hash = "sha256:599bdb75e220241cedc6faebfafedd7670335d2e29620d207dd0378a4e9ccc5a"},
+ {file = "fonttools-4.51.0-py3-none-any.whl", hash = "sha256:15c94eeef6b095831067f72c825eb0e2d48bb4cea0647c1b05c981ecba2bf39f"},
+ {file = "fonttools-4.51.0.tar.gz", hash = "sha256:dc0673361331566d7a663d7ce0f6fdcbfbdc1f59c6e3ed1165ad7202ca183c68"},
+]
+
+[package.extras]
+all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "pycairo", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.1.0)", "xattr", "zopfli (>=0.1.4)"]
+graphite = ["lz4 (>=1.7.4.2)"]
+interpolatable = ["munkres", "pycairo", "scipy"]
+lxml = ["lxml (>=4.0)"]
+pathops = ["skia-pathops (>=0.5.0)"]
+plot = ["matplotlib"]
+repacker = ["uharfbuzz (>=0.23.0)"]
+symfont = ["sympy"]
+type1 = ["xattr"]
+ufo = ["fs (>=2.2.0,<3)"]
+unicode = ["unicodedata2 (>=15.1.0)"]
+woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"]
+
+[[package]]
+name = "h11"
+version = "0.14.0"
+description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
+ {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
+]
+
+[[package]]
+name = "idna"
+version = "3.7"
+description = "Internationalized Domain Names in Applications (IDNA)"
+optional = false
+python-versions = ">=3.5"
+files = [
+ {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
+ {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
+]
+
+[[package]]
+name = "kiwisolver"
+version = "1.4.5"
+description = "A fast implementation of the Cassowary constraint solver"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "kiwisolver-1.4.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:05703cf211d585109fcd72207a31bb170a0f22144d68298dc5e61b3c946518af"},
+ {file = "kiwisolver-1.4.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:146d14bebb7f1dc4d5fbf74f8a6cb15ac42baadee8912eb84ac0b3b2a3dc6ac3"},
+ {file = "kiwisolver-1.4.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ef7afcd2d281494c0a9101d5c571970708ad911d028137cd558f02b851c08b4"},
+ {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9eaa8b117dc8337728e834b9c6e2611f10c79e38f65157c4c38e9400286f5cb1"},
+ {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ec20916e7b4cbfb1f12380e46486ec4bcbaa91a9c448b97023fde0d5bbf9e4ff"},
+ {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39b42c68602539407884cf70d6a480a469b93b81b7701378ba5e2328660c847a"},
+ {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa12042de0171fad672b6c59df69106d20d5596e4f87b5e8f76df757a7c399aa"},
+ {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a40773c71d7ccdd3798f6489aaac9eee213d566850a9533f8d26332d626b82c"},
+ {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:19df6e621f6d8b4b9c4d45f40a66839294ff2bb235e64d2178f7522d9170ac5b"},
+ {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:83d78376d0d4fd884e2c114d0621624b73d2aba4e2788182d286309ebdeed770"},
+ {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e391b1f0a8a5a10ab3b9bb6afcfd74f2175f24f8975fb87ecae700d1503cdee0"},
+ {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:852542f9481f4a62dbb5dd99e8ab7aedfeb8fb6342349a181d4036877410f525"},
+ {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59edc41b24031bc25108e210c0def6f6c2191210492a972d585a06ff246bb79b"},
+ {file = "kiwisolver-1.4.5-cp310-cp310-win32.whl", hash = "sha256:a6aa6315319a052b4ee378aa171959c898a6183f15c1e541821c5c59beaa0238"},
+ {file = "kiwisolver-1.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:d0ef46024e6a3d79c01ff13801cb19d0cad7fd859b15037aec74315540acc276"},
+ {file = "kiwisolver-1.4.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:11863aa14a51fd6ec28688d76f1735f8f69ab1fabf388851a595d0721af042f5"},
+ {file = "kiwisolver-1.4.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8ab3919a9997ab7ef2fbbed0cc99bb28d3c13e6d4b1ad36e97e482558a91be90"},
+ {file = "kiwisolver-1.4.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fcc700eadbbccbf6bc1bcb9dbe0786b4b1cb91ca0dcda336eef5c2beed37b797"},
+ {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dfdd7c0b105af050eb3d64997809dc21da247cf44e63dc73ff0fd20b96be55a9"},
+ {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76c6a5964640638cdeaa0c359382e5703e9293030fe730018ca06bc2010c4437"},
+ {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbea0db94288e29afcc4c28afbf3a7ccaf2d7e027489c449cf7e8f83c6346eb9"},
+ {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ceec1a6bc6cab1d6ff5d06592a91a692f90ec7505d6463a88a52cc0eb58545da"},
+ {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:040c1aebeda72197ef477a906782b5ab0d387642e93bda547336b8957c61022e"},
+ {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f91de7223d4c7b793867797bacd1ee53bfe7359bd70d27b7b58a04efbb9436c8"},
+ {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:faae4860798c31530dd184046a900e652c95513796ef51a12bc086710c2eec4d"},
+ {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:b0157420efcb803e71d1b28e2c287518b8808b7cf1ab8af36718fd0a2c453eb0"},
+ {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:06f54715b7737c2fecdbf140d1afb11a33d59508a47bf11bb38ecf21dc9ab79f"},
+ {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fdb7adb641a0d13bdcd4ef48e062363d8a9ad4a182ac7647ec88f695e719ae9f"},
+ {file = "kiwisolver-1.4.5-cp311-cp311-win32.whl", hash = "sha256:bb86433b1cfe686da83ce32a9d3a8dd308e85c76b60896d58f082136f10bffac"},
+ {file = "kiwisolver-1.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:6c08e1312a9cf1074d17b17728d3dfce2a5125b2d791527f33ffbe805200a355"},
+ {file = "kiwisolver-1.4.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:32d5cf40c4f7c7b3ca500f8985eb3fb3a7dfc023215e876f207956b5ea26632a"},
+ {file = "kiwisolver-1.4.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f846c260f483d1fd217fe5ed7c173fb109efa6b1fc8381c8b7552c5781756192"},
+ {file = "kiwisolver-1.4.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5ff5cf3571589b6d13bfbfd6bcd7a3f659e42f96b5fd1c4830c4cf21d4f5ef45"},
+ {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7269d9e5f1084a653d575c7ec012ff57f0c042258bf5db0954bf551c158466e7"},
+ {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da802a19d6e15dffe4b0c24b38b3af68e6c1a68e6e1d8f30148c83864f3881db"},
+ {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3aba7311af82e335dd1e36ffff68aaca609ca6290c2cb6d821a39aa075d8e3ff"},
+ {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:763773d53f07244148ccac5b084da5adb90bfaee39c197554f01b286cf869228"},
+ {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2270953c0d8cdab5d422bee7d2007f043473f9d2999631c86a223c9db56cbd16"},
+ {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d099e745a512f7e3bbe7249ca835f4d357c586d78d79ae8f1dcd4d8adeb9bda9"},
+ {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:74db36e14a7d1ce0986fa104f7d5637aea5c82ca6326ed0ec5694280942d1162"},
+ {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e5bab140c309cb3a6ce373a9e71eb7e4873c70c2dda01df6820474f9889d6d4"},
+ {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0f114aa76dc1b8f636d077979c0ac22e7cd8f3493abbab152f20eb8d3cda71f3"},
+ {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:88a2df29d4724b9237fc0c6eaf2a1adae0cdc0b3e9f4d8e7dc54b16812d2d81a"},
+ {file = "kiwisolver-1.4.5-cp312-cp312-win32.whl", hash = "sha256:72d40b33e834371fd330fb1472ca19d9b8327acb79a5821d4008391db8e29f20"},
+ {file = "kiwisolver-1.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:2c5674c4e74d939b9d91dda0fae10597ac7521768fec9e399c70a1f27e2ea2d9"},
+ {file = "kiwisolver-1.4.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3a2b053a0ab7a3960c98725cfb0bf5b48ba82f64ec95fe06f1d06c99b552e130"},
+ {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cd32d6c13807e5c66a7cbb79f90b553642f296ae4518a60d8d76243b0ad2898"},
+ {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59ec7b7c7e1a61061850d53aaf8e93db63dce0c936db1fda2658b70e4a1be709"},
+ {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da4cfb373035def307905d05041c1d06d8936452fe89d464743ae7fb8371078b"},
+ {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2400873bccc260b6ae184b2b8a4fec0e4082d30648eadb7c3d9a13405d861e89"},
+ {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1b04139c4236a0f3aff534479b58f6f849a8b351e1314826c2d230849ed48985"},
+ {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:4e66e81a5779b65ac21764c295087de82235597a2293d18d943f8e9e32746265"},
+ {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:7931d8f1f67c4be9ba1dd9c451fb0eeca1a25b89e4d3f89e828fe12a519b782a"},
+ {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b3f7e75f3015df442238cca659f8baa5f42ce2a8582727981cbfa15fee0ee205"},
+ {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:bbf1d63eef84b2e8c89011b7f2235b1e0bf7dacc11cac9431fc6468e99ac77fb"},
+ {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4c380469bd3f970ef677bf2bcba2b6b0b4d5c75e7a020fb863ef75084efad66f"},
+ {file = "kiwisolver-1.4.5-cp37-cp37m-win32.whl", hash = "sha256:9408acf3270c4b6baad483865191e3e582b638b1654a007c62e3efe96f09a9a3"},
+ {file = "kiwisolver-1.4.5-cp37-cp37m-win_amd64.whl", hash = "sha256:5b94529f9b2591b7af5f3e0e730a4e0a41ea174af35a4fd067775f9bdfeee01a"},
+ {file = "kiwisolver-1.4.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:11c7de8f692fc99816e8ac50d1d1aef4f75126eefc33ac79aac02c099fd3db71"},
+ {file = "kiwisolver-1.4.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:53abb58632235cd154176ced1ae8f0d29a6657aa1aa9decf50b899b755bc2b93"},
+ {file = "kiwisolver-1.4.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:88b9f257ca61b838b6f8094a62418421f87ac2a1069f7e896c36a7d86b5d4c29"},
+ {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3195782b26fc03aa9c6913d5bad5aeb864bdc372924c093b0f1cebad603dd712"},
+ {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc579bf0f502e54926519451b920e875f433aceb4624a3646b3252b5caa9e0b6"},
+ {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a580c91d686376f0f7c295357595c5a026e6cbc3d77b7c36e290201e7c11ecb"},
+ {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cfe6ab8da05c01ba6fbea630377b5da2cd9bcbc6338510116b01c1bc939a2c18"},
+ {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d2e5a98f0ec99beb3c10e13b387f8db39106d53993f498b295f0c914328b1333"},
+ {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a51a263952b1429e429ff236d2f5a21c5125437861baeed77f5e1cc2d2c7c6da"},
+ {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3edd2fa14e68c9be82c5b16689e8d63d89fe927e56debd6e1dbce7a26a17f81b"},
+ {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:74d1b44c6cfc897df648cc9fdaa09bc3e7679926e6f96df05775d4fb3946571c"},
+ {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:76d9289ed3f7501012e05abb8358bbb129149dbd173f1f57a1bf1c22d19ab7cc"},
+ {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:92dea1ffe3714fa8eb6a314d2b3c773208d865a0e0d35e713ec54eea08a66250"},
+ {file = "kiwisolver-1.4.5-cp38-cp38-win32.whl", hash = "sha256:5c90ae8c8d32e472be041e76f9d2f2dbff4d0b0be8bd4041770eddb18cf49a4e"},
+ {file = "kiwisolver-1.4.5-cp38-cp38-win_amd64.whl", hash = "sha256:c7940c1dc63eb37a67721b10d703247552416f719c4188c54e04334321351ced"},
+ {file = "kiwisolver-1.4.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9407b6a5f0d675e8a827ad8742e1d6b49d9c1a1da5d952a67d50ef5f4170b18d"},
+ {file = "kiwisolver-1.4.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15568384086b6df3c65353820a4473575dbad192e35010f622c6ce3eebd57af9"},
+ {file = "kiwisolver-1.4.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0dc9db8e79f0036e8173c466d21ef18e1befc02de8bf8aa8dc0813a6dc8a7046"},
+ {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:cdc8a402aaee9a798b50d8b827d7ecf75edc5fb35ea0f91f213ff927c15f4ff0"},
+ {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6c3bd3cde54cafb87d74d8db50b909705c62b17c2099b8f2e25b461882e544ff"},
+ {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:955e8513d07a283056b1396e9a57ceddbd272d9252c14f154d450d227606eb54"},
+ {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:346f5343b9e3f00b8db8ba359350eb124b98c99efd0b408728ac6ebf38173958"},
+ {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9098e0049e88c6a24ff64545cdfc50807818ba6c1b739cae221bbbcbc58aad3"},
+ {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:00bd361b903dc4bbf4eb165f24d1acbee754fce22ded24c3d56eec268658a5cf"},
+ {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7b8b454bac16428b22560d0a1cf0a09875339cab69df61d7805bf48919415901"},
+ {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f1d072c2eb0ad60d4c183f3fb44ac6f73fb7a8f16a2694a91f988275cbf352f9"},
+ {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:31a82d498054cac9f6d0b53d02bb85811185bcb477d4b60144f915f3b3126342"},
+ {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6512cb89e334e4700febbffaaa52761b65b4f5a3cf33f960213d5656cea36a77"},
+ {file = "kiwisolver-1.4.5-cp39-cp39-win32.whl", hash = "sha256:9db8ea4c388fdb0f780fe91346fd438657ea602d58348753d9fb265ce1bca67f"},
+ {file = "kiwisolver-1.4.5-cp39-cp39-win_amd64.whl", hash = "sha256:59415f46a37f7f2efeec758353dd2eae1b07640d8ca0f0c42548ec4125492635"},
+ {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5c7b3b3a728dc6faf3fc372ef24f21d1e3cee2ac3e9596691d746e5a536de920"},
+ {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:620ced262a86244e2be10a676b646f29c34537d0d9cc8eb26c08f53d98013390"},
+ {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:378a214a1e3bbf5ac4a8708304318b4f890da88c9e6a07699c4ae7174c09a68d"},
+ {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf7be1207676ac608a50cd08f102f6742dbfc70e8d60c4db1c6897f62f71523"},
+ {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ba55dce0a9b8ff59495ddd050a0225d58bd0983d09f87cfe2b6aec4f2c1234e4"},
+ {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fd32ea360bcbb92d28933fc05ed09bffcb1704ba3fc7942e81db0fd4f81a7892"},
+ {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5e7139af55d1688f8b960ee9ad5adafc4ac17c1c473fe07133ac092310d76544"},
+ {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dced8146011d2bc2e883f9bd68618b8247387f4bbec46d7392b3c3b032640126"},
+ {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9bf3325c47b11b2e51bca0824ea217c7cd84491d8ac4eefd1e409705ef092bd"},
+ {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5794cf59533bc3f1b1c821f7206a3617999db9fbefc345360aafe2e067514929"},
+ {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e368f200bbc2e4f905b8e71eb38b3c04333bddaa6a2464a6355487b02bb7fb09"},
+ {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5d706eba36b4c4d5bc6c6377bb6568098765e990cfc21ee16d13963fab7b3e7"},
+ {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85267bd1aa8880a9c88a8cb71e18d3d64d2751a790e6ca6c27b8ccc724bcd5ad"},
+ {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:210ef2c3a1f03272649aff1ef992df2e724748918c4bc2d5a90352849eb40bea"},
+ {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:11d011a7574eb3b82bcc9c1a1d35c1d7075677fdd15de527d91b46bd35e935ee"},
+ {file = "kiwisolver-1.4.5.tar.gz", hash = "sha256:e57e563a57fb22a142da34f38acc2fc1a5c864bc29ca1517a88abc963e60d6ec"},
+]
+
+[[package]]
+name = "matplotlib"
+version = "3.8.4"
+description = "Python plotting package"
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "matplotlib-3.8.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:abc9d838f93583650c35eca41cfcec65b2e7cb50fd486da6f0c49b5e1ed23014"},
+ {file = "matplotlib-3.8.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f65c9f002d281a6e904976007b2d46a1ee2bcea3a68a8c12dda24709ddc9106"},
+ {file = "matplotlib-3.8.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce1edd9f5383b504dbc26eeea404ed0a00656c526638129028b758fd43fc5f10"},
+ {file = "matplotlib-3.8.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecd79298550cba13a43c340581a3ec9c707bd895a6a061a78fa2524660482fc0"},
+ {file = "matplotlib-3.8.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:90df07db7b599fe7035d2f74ab7e438b656528c68ba6bb59b7dc46af39ee48ef"},
+ {file = "matplotlib-3.8.4-cp310-cp310-win_amd64.whl", hash = "sha256:ac24233e8f2939ac4fd2919eed1e9c0871eac8057666070e94cbf0b33dd9c338"},
+ {file = "matplotlib-3.8.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:72f9322712e4562e792b2961971891b9fbbb0e525011e09ea0d1f416c4645661"},
+ {file = "matplotlib-3.8.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:232ce322bfd020a434caaffbd9a95333f7c2491e59cfc014041d95e38ab90d1c"},
+ {file = "matplotlib-3.8.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6addbd5b488aedb7f9bc19f91cd87ea476206f45d7116fcfe3d31416702a82fa"},
+ {file = "matplotlib-3.8.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc4ccdc64e3039fc303defd119658148f2349239871db72cd74e2eeaa9b80b71"},
+ {file = "matplotlib-3.8.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b7a2a253d3b36d90c8993b4620183b55665a429da8357a4f621e78cd48b2b30b"},
+ {file = "matplotlib-3.8.4-cp311-cp311-win_amd64.whl", hash = "sha256:8080d5081a86e690d7688ffa542532e87f224c38a6ed71f8fbed34dd1d9fedae"},
+ {file = "matplotlib-3.8.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6485ac1f2e84676cff22e693eaa4fbed50ef5dc37173ce1f023daef4687df616"},
+ {file = "matplotlib-3.8.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c89ee9314ef48c72fe92ce55c4e95f2f39d70208f9f1d9db4e64079420d8d732"},
+ {file = "matplotlib-3.8.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50bac6e4d77e4262c4340d7a985c30912054745ec99756ce213bfbc3cb3808eb"},
+ {file = "matplotlib-3.8.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f51c4c869d4b60d769f7b4406eec39596648d9d70246428745a681c327a8ad30"},
+ {file = "matplotlib-3.8.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b12ba985837e4899b762b81f5b2845bd1a28f4fdd1a126d9ace64e9c4eb2fb25"},
+ {file = "matplotlib-3.8.4-cp312-cp312-win_amd64.whl", hash = "sha256:7a6769f58ce51791b4cb8b4d7642489df347697cd3e23d88266aaaee93b41d9a"},
+ {file = "matplotlib-3.8.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:843cbde2f0946dadd8c5c11c6d91847abd18ec76859dc319362a0964493f0ba6"},
+ {file = "matplotlib-3.8.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1c13f041a7178f9780fb61cc3a2b10423d5e125480e4be51beaf62b172413b67"},
+ {file = "matplotlib-3.8.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb44f53af0a62dc80bba4443d9b27f2fde6acfdac281d95bc872dc148a6509cc"},
+ {file = "matplotlib-3.8.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:606e3b90897554c989b1e38a258c626d46c873523de432b1462f295db13de6f9"},
+ {file = "matplotlib-3.8.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9bb0189011785ea794ee827b68777db3ca3f93f3e339ea4d920315a0e5a78d54"},
+ {file = "matplotlib-3.8.4-cp39-cp39-win_amd64.whl", hash = "sha256:6209e5c9aaccc056e63b547a8152661324404dd92340a6e479b3a7f24b42a5d0"},
+ {file = "matplotlib-3.8.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c7064120a59ce6f64103c9cefba8ffe6fba87f2c61d67c401186423c9a20fd35"},
+ {file = "matplotlib-3.8.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0e47eda4eb2614300fc7bb4657fced3e83d6334d03da2173b09e447418d499f"},
+ {file = "matplotlib-3.8.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:493e9f6aa5819156b58fce42b296ea31969f2aab71c5b680b4ea7a3cb5c07d94"},
+ {file = "matplotlib-3.8.4.tar.gz", hash = "sha256:8aac397d5e9ec158960e31c381c5ffc52ddd52bd9a47717e2a694038167dffea"},
+]
+
+[package.dependencies]
+contourpy = ">=1.0.1"
+cycler = ">=0.10"
+fonttools = ">=4.22.0"
+kiwisolver = ">=1.3.1"
+numpy = ">=1.21"
+packaging = ">=20.0"
+pillow = ">=8"
+pyparsing = ">=2.3.1"
+python-dateutil = ">=2.7"
+
+[[package]]
+name = "numpy"
+version = "1.26.4"
+description = "Fundamental package for array computing in Python"
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"},
+ {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"},
+ {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"},
+ {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"},
+ {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"},
+ {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"},
+ {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"},
+ {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"},
+ {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"},
+ {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"},
+ {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"},
+ {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"},
+ {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"},
+ {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"},
+ {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"},
+ {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"},
+ {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"},
+ {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"},
+ {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"},
+ {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"},
+ {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"},
+ {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"},
+ {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"},
+ {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"},
+ {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"},
+ {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"},
+ {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"},
+ {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"},
+ {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"},
+ {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"},
+ {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"},
+ {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"},
+ {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"},
+ {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"},
+ {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"},
+ {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"},
+]
+
+[[package]]
+name = "packaging"
+version = "24.0"
+description = "Core utilities for Python packages"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"},
+ {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"},
+]
+
+[[package]]
+name = "pillow"
+version = "10.3.0"
+description = "Python Imaging Library (Fork)"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pillow-10.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:90b9e29824800e90c84e4022dd5cc16eb2d9605ee13f05d47641eb183cd73d45"},
+ {file = "pillow-10.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2c405445c79c3f5a124573a051062300936b0281fee57637e706453e452746c"},
+ {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78618cdbccaa74d3f88d0ad6cb8ac3007f1a6fa5c6f19af64b55ca170bfa1edf"},
+ {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:261ddb7ca91fcf71757979534fb4c128448b5b4c55cb6152d280312062f69599"},
+ {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ce49c67f4ea0609933d01c0731b34b8695a7a748d6c8d186f95e7d085d2fe475"},
+ {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b14f16f94cbc61215115b9b1236f9c18403c15dd3c52cf629072afa9d54c1cbf"},
+ {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d33891be6df59d93df4d846640f0e46f1a807339f09e79a8040bc887bdcd7ed3"},
+ {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b50811d664d392f02f7761621303eba9d1b056fb1868c8cdf4231279645c25f5"},
+ {file = "pillow-10.3.0-cp310-cp310-win32.whl", hash = "sha256:ca2870d5d10d8726a27396d3ca4cf7976cec0f3cb706debe88e3a5bd4610f7d2"},
+ {file = "pillow-10.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f0d0591a0aeaefdaf9a5e545e7485f89910c977087e7de2b6c388aec32011e9f"},
+ {file = "pillow-10.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:ccce24b7ad89adb5a1e34a6ba96ac2530046763912806ad4c247356a8f33a67b"},
+ {file = "pillow-10.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:5f77cf66e96ae734717d341c145c5949c63180842a545c47a0ce7ae52ca83795"},
+ {file = "pillow-10.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4b878386c4bf293578b48fc570b84ecfe477d3b77ba39a6e87150af77f40c57"},
+ {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdcbb4068117dfd9ce0138d068ac512843c52295ed996ae6dd1faf537b6dbc27"},
+ {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9797a6c8fe16f25749b371c02e2ade0efb51155e767a971c61734b1bf6293994"},
+ {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9e91179a242bbc99be65e139e30690e081fe6cb91a8e77faf4c409653de39451"},
+ {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1b87bd9d81d179bd8ab871603bd80d8645729939f90b71e62914e816a76fc6bd"},
+ {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:81d09caa7b27ef4e61cb7d8fbf1714f5aec1c6b6c5270ee53504981e6e9121ad"},
+ {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:048ad577748b9fa4a99a0548c64f2cb8d672d5bf2e643a739ac8faff1164238c"},
+ {file = "pillow-10.3.0-cp311-cp311-win32.whl", hash = "sha256:7161ec49ef0800947dc5570f86568a7bb36fa97dd09e9827dc02b718c5643f09"},
+ {file = "pillow-10.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8eb0908e954d093b02a543dc963984d6e99ad2b5e36503d8a0aaf040505f747d"},
+ {file = "pillow-10.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e6f7d1c414191c1199f8996d3f2282b9ebea0945693fb67392c75a3a320941f"},
+ {file = "pillow-10.3.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:e46f38133e5a060d46bd630faa4d9fa0202377495df1f068a8299fd78c84de84"},
+ {file = "pillow-10.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:50b8eae8f7334ec826d6eeffaeeb00e36b5e24aa0b9df322c247539714c6df19"},
+ {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d3bea1c75f8c53ee4d505c3e67d8c158ad4df0d83170605b50b64025917f338"},
+ {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19aeb96d43902f0a783946a0a87dbdad5c84c936025b8419da0a0cd7724356b1"},
+ {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:74d28c17412d9caa1066f7a31df8403ec23d5268ba46cd0ad2c50fb82ae40462"},
+ {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ff61bfd9253c3915e6d41c651d5f962da23eda633cf02262990094a18a55371a"},
+ {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d886f5d353333b4771d21267c7ecc75b710f1a73d72d03ca06df49b09015a9ef"},
+ {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b5ec25d8b17217d635f8935dbc1b9aa5907962fae29dff220f2659487891cd3"},
+ {file = "pillow-10.3.0-cp312-cp312-win32.whl", hash = "sha256:51243f1ed5161b9945011a7360e997729776f6e5d7005ba0c6879267d4c5139d"},
+ {file = "pillow-10.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:412444afb8c4c7a6cc11a47dade32982439925537e483be7c0ae0cf96c4f6a0b"},
+ {file = "pillow-10.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:798232c92e7665fe82ac085f9d8e8ca98826f8e27859d9a96b41d519ecd2e49a"},
+ {file = "pillow-10.3.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4eaa22f0d22b1a7e93ff0a596d57fdede2e550aecffb5a1ef1106aaece48e96b"},
+ {file = "pillow-10.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cd5e14fbf22a87321b24c88669aad3a51ec052eb145315b3da3b7e3cc105b9a2"},
+ {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1530e8f3a4b965eb6a7785cf17a426c779333eb62c9a7d1bbcf3ffd5bf77a4aa"},
+ {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d512aafa1d32efa014fa041d38868fda85028e3f930a96f85d49c7d8ddc0383"},
+ {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:339894035d0ede518b16073bdc2feef4c991ee991a29774b33e515f1d308e08d"},
+ {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:aa7e402ce11f0885305bfb6afb3434b3cd8f53b563ac065452d9d5654c7b86fd"},
+ {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0ea2a783a2bdf2a561808fe4a7a12e9aa3799b701ba305de596bc48b8bdfce9d"},
+ {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c78e1b00a87ce43bb37642c0812315b411e856a905d58d597750eb79802aaaa3"},
+ {file = "pillow-10.3.0-cp38-cp38-win32.whl", hash = "sha256:72d622d262e463dfb7595202d229f5f3ab4b852289a1cd09650362db23b9eb0b"},
+ {file = "pillow-10.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:2034f6759a722da3a3dbd91a81148cf884e91d1b747992ca288ab88c1de15999"},
+ {file = "pillow-10.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2ed854e716a89b1afcedea551cd85f2eb2a807613752ab997b9974aaa0d56936"},
+ {file = "pillow-10.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dc1a390a82755a8c26c9964d457d4c9cbec5405896cba94cf51f36ea0d855002"},
+ {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4203efca580f0dd6f882ca211f923168548f7ba334c189e9eab1178ab840bf60"},
+ {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3102045a10945173d38336f6e71a8dc71bcaeed55c3123ad4af82c52807b9375"},
+ {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:6fb1b30043271ec92dc65f6d9f0b7a830c210b8a96423074b15c7bc999975f57"},
+ {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:1dfc94946bc60ea375cc39cff0b8da6c7e5f8fcdc1d946beb8da5c216156ddd8"},
+ {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b09b86b27a064c9624d0a6c54da01c1beaf5b6cadfa609cf63789b1d08a797b9"},
+ {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d3b2348a78bc939b4fed6552abfd2e7988e0f81443ef3911a4b8498ca084f6eb"},
+ {file = "pillow-10.3.0-cp39-cp39-win32.whl", hash = "sha256:45ebc7b45406febf07fef35d856f0293a92e7417ae7933207e90bf9090b70572"},
+ {file = "pillow-10.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:0ba26351b137ca4e0db0342d5d00d2e355eb29372c05afd544ebf47c0956ffeb"},
+ {file = "pillow-10.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:50fd3f6b26e3441ae07b7c979309638b72abc1a25da31a81a7fbd9495713ef4f"},
+ {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6b02471b72526ab8a18c39cb7967b72d194ec53c1fd0a70b050565a0f366d355"},
+ {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8ab74c06ffdab957d7670c2a5a6e1a70181cd10b727cd788c4dd9005b6a8acd9"},
+ {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:048eeade4c33fdf7e08da40ef402e748df113fd0b4584e32c4af74fe78baaeb2"},
+ {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2ec1e921fd07c7cda7962bad283acc2f2a9ccc1b971ee4b216b75fad6f0463"},
+ {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c8e73e99da7db1b4cad7f8d682cf6abad7844da39834c288fbfa394a47bbced"},
+ {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:16563993329b79513f59142a6b02055e10514c1a8e86dca8b48a893e33cf91e3"},
+ {file = "pillow-10.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dd78700f5788ae180b5ee8902c6aea5a5726bac7c364b202b4b3e3ba2d293170"},
+ {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:aff76a55a8aa8364d25400a210a65ff59d0168e0b4285ba6bf2bd83cf675ba32"},
+ {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b7bc2176354defba3edc2b9a777744462da2f8e921fbaf61e52acb95bafa9828"},
+ {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:793b4e24db2e8742ca6423d3fde8396db336698c55cd34b660663ee9e45ed37f"},
+ {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d93480005693d247f8346bc8ee28c72a2191bdf1f6b5db469c096c0c867ac015"},
+ {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83341b89884e2b2e55886e8fbbf37c3fa5efd6c8907124aeb72f285ae5696e5"},
+ {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1a1d1915db1a4fdb2754b9de292642a39a7fb28f1736699527bb649484fb966a"},
+ {file = "pillow-10.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a0eaa93d054751ee9964afa21c06247779b90440ca41d184aeb5d410f20ff591"},
+ {file = "pillow-10.3.0.tar.gz", hash = "sha256:9d2455fbf44c914840c793e89aa82d0e1763a14253a000743719ae5946814b2d"},
+]
+
+[package.extras]
+docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"]
+fpx = ["olefile"]
+mic = ["olefile"]
+tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
+typing = ["typing-extensions"]
+xmp = ["defusedxml"]
+
+[[package]]
+name = "pydantic"
+version = "2.7.1"
+description = "Data validation using Python type hints"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pydantic-2.7.1-py3-none-any.whl", hash = "sha256:e029badca45266732a9a79898a15ae2e8b14840b1eabbb25844be28f0b33f3d5"},
+ {file = "pydantic-2.7.1.tar.gz", hash = "sha256:e9dbb5eada8abe4d9ae5f46b9939aead650cd2b68f249bb3a8139dbe125803cc"},
+]
+
+[package.dependencies]
+annotated-types = ">=0.4.0"
+pydantic-core = "2.18.2"
+typing-extensions = ">=4.6.1"
+
+[package.extras]
+email = ["email-validator (>=2.0.0)"]
+
+[[package]]
+name = "pydantic-core"
+version = "2.18.2"
+description = "Core functionality for Pydantic validation and serialization"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pydantic_core-2.18.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:9e08e867b306f525802df7cd16c44ff5ebbe747ff0ca6cf3fde7f36c05a59a81"},
+ {file = "pydantic_core-2.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f0a21cbaa69900cbe1a2e7cad2aa74ac3cf21b10c3efb0fa0b80305274c0e8a2"},
+ {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0680b1f1f11fda801397de52c36ce38ef1c1dc841a0927a94f226dea29c3ae3d"},
+ {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:95b9d5e72481d3780ba3442eac863eae92ae43a5f3adb5b4d0a1de89d42bb250"},
+ {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fcf5cd9c4b655ad666ca332b9a081112cd7a58a8b5a6ca7a3104bc950f2038"},
+ {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b5155ff768083cb1d62f3e143b49a8a3432e6789a3abee8acd005c3c7af1c74"},
+ {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:553ef617b6836fc7e4df130bb851e32fe357ce36336d897fd6646d6058d980af"},
+ {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b89ed9eb7d616ef5714e5590e6cf7f23b02d0d539767d33561e3675d6f9e3857"},
+ {file = "pydantic_core-2.18.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:75f7e9488238e920ab6204399ded280dc4c307d034f3924cd7f90a38b1829563"},
+ {file = "pydantic_core-2.18.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ef26c9e94a8c04a1b2924149a9cb081836913818e55681722d7f29af88fe7b38"},
+ {file = "pydantic_core-2.18.2-cp310-none-win32.whl", hash = "sha256:182245ff6b0039e82b6bb585ed55a64d7c81c560715d1bad0cbad6dfa07b4027"},
+ {file = "pydantic_core-2.18.2-cp310-none-win_amd64.whl", hash = "sha256:e23ec367a948b6d812301afc1b13f8094ab7b2c280af66ef450efc357d2ae543"},
+ {file = "pydantic_core-2.18.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:219da3f096d50a157f33645a1cf31c0ad1fe829a92181dd1311022f986e5fbe3"},
+ {file = "pydantic_core-2.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cc1cfd88a64e012b74e94cd00bbe0f9c6df57049c97f02bb07d39e9c852e19a4"},
+ {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05b7133a6e6aeb8df37d6f413f7705a37ab4031597f64ab56384c94d98fa0e90"},
+ {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:224c421235f6102e8737032483f43c1a8cfb1d2f45740c44166219599358c2cd"},
+ {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b14d82cdb934e99dda6d9d60dc84a24379820176cc4a0d123f88df319ae9c150"},
+ {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2728b01246a3bba6de144f9e3115b532ee44bd6cf39795194fb75491824a1413"},
+ {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:470b94480bb5ee929f5acba6995251ada5e059a5ef3e0dfc63cca287283ebfa6"},
+ {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:997abc4df705d1295a42f95b4eec4950a37ad8ae46d913caeee117b6b198811c"},
+ {file = "pydantic_core-2.18.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75250dbc5290e3f1a0f4618db35e51a165186f9034eff158f3d490b3fed9f8a0"},
+ {file = "pydantic_core-2.18.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4456f2dca97c425231d7315737d45239b2b51a50dc2b6f0c2bb181fce6207664"},
+ {file = "pydantic_core-2.18.2-cp311-none-win32.whl", hash = "sha256:269322dcc3d8bdb69f054681edff86276b2ff972447863cf34c8b860f5188e2e"},
+ {file = "pydantic_core-2.18.2-cp311-none-win_amd64.whl", hash = "sha256:800d60565aec896f25bc3cfa56d2277d52d5182af08162f7954f938c06dc4ee3"},
+ {file = "pydantic_core-2.18.2-cp311-none-win_arm64.whl", hash = "sha256:1404c69d6a676245199767ba4f633cce5f4ad4181f9d0ccb0577e1f66cf4c46d"},
+ {file = "pydantic_core-2.18.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:fb2bd7be70c0fe4dfd32c951bc813d9fe6ebcbfdd15a07527796c8204bd36242"},
+ {file = "pydantic_core-2.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6132dd3bd52838acddca05a72aafb6eab6536aa145e923bb50f45e78b7251043"},
+ {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d904828195733c183d20a54230c0df0eb46ec746ea1a666730787353e87182"},
+ {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c9bd70772c720142be1020eac55f8143a34ec9f82d75a8e7a07852023e46617f"},
+ {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b8ed04b3582771764538f7ee7001b02e1170223cf9b75dff0bc698fadb00cf3"},
+ {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6dac87ddb34aaec85f873d737e9d06a3555a1cc1a8e0c44b7f8d5daeb89d86f"},
+ {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ca4ae5a27ad7a4ee5170aebce1574b375de390bc01284f87b18d43a3984df72"},
+ {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:886eec03591b7cf058467a70a87733b35f44707bd86cf64a615584fd72488b7c"},
+ {file = "pydantic_core-2.18.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ca7b0c1f1c983e064caa85f3792dd2fe3526b3505378874afa84baf662e12241"},
+ {file = "pydantic_core-2.18.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b4356d3538c3649337df4074e81b85f0616b79731fe22dd11b99499b2ebbdf3"},
+ {file = "pydantic_core-2.18.2-cp312-none-win32.whl", hash = "sha256:8b172601454f2d7701121bbec3425dd71efcb787a027edf49724c9cefc14c038"},
+ {file = "pydantic_core-2.18.2-cp312-none-win_amd64.whl", hash = "sha256:b1bd7e47b1558ea872bd16c8502c414f9e90dcf12f1395129d7bb42a09a95438"},
+ {file = "pydantic_core-2.18.2-cp312-none-win_arm64.whl", hash = "sha256:98758d627ff397e752bc339272c14c98199c613f922d4a384ddc07526c86a2ec"},
+ {file = "pydantic_core-2.18.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:9fdad8e35f278b2c3eb77cbdc5c0a49dada440657bf738d6905ce106dc1de439"},
+ {file = "pydantic_core-2.18.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1d90c3265ae107f91a4f279f4d6f6f1d4907ac76c6868b27dc7fb33688cfb347"},
+ {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:390193c770399861d8df9670fb0d1874f330c79caaca4642332df7c682bf6b91"},
+ {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:82d5d4d78e4448683cb467897fe24e2b74bb7b973a541ea1dcfec1d3cbce39fb"},
+ {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4774f3184d2ef3e14e8693194f661dea5a4d6ca4e3dc8e39786d33a94865cefd"},
+ {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4d938ec0adf5167cb335acb25a4ee69a8107e4984f8fbd2e897021d9e4ca21b"},
+ {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0e8b1be28239fc64a88a8189d1df7fad8be8c1ae47fcc33e43d4be15f99cc70"},
+ {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:868649da93e5a3d5eacc2b5b3b9235c98ccdbfd443832f31e075f54419e1b96b"},
+ {file = "pydantic_core-2.18.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:78363590ef93d5d226ba21a90a03ea89a20738ee5b7da83d771d283fd8a56761"},
+ {file = "pydantic_core-2.18.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:852e966fbd035a6468fc0a3496589b45e2208ec7ca95c26470a54daed82a0788"},
+ {file = "pydantic_core-2.18.2-cp38-none-win32.whl", hash = "sha256:6a46e22a707e7ad4484ac9ee9f290f9d501df45954184e23fc29408dfad61350"},
+ {file = "pydantic_core-2.18.2-cp38-none-win_amd64.whl", hash = "sha256:d91cb5ea8b11607cc757675051f61b3d93f15eca3cefb3e6c704a5d6e8440f4e"},
+ {file = "pydantic_core-2.18.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:ae0a8a797a5e56c053610fa7be147993fe50960fa43609ff2a9552b0e07013e8"},
+ {file = "pydantic_core-2.18.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:042473b6280246b1dbf530559246f6842b56119c2926d1e52b631bdc46075f2a"},
+ {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a388a77e629b9ec814c1b1e6b3b595fe521d2cdc625fcca26fbc2d44c816804"},
+ {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25add29b8f3b233ae90ccef2d902d0ae0432eb0d45370fe315d1a5cf231004b"},
+ {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f459a5ce8434614dfd39bbebf1041952ae01da6bed9855008cb33b875cb024c0"},
+ {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eff2de745698eb46eeb51193a9f41d67d834d50e424aef27df2fcdee1b153845"},
+ {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8309f67285bdfe65c372ea3722b7a5642680f3dba538566340a9d36e920b5f0"},
+ {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f93a8a2e3938ff656a7c1bc57193b1319960ac015b6e87d76c76bf14fe0244b4"},
+ {file = "pydantic_core-2.18.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:22057013c8c1e272eb8d0eebc796701167d8377441ec894a8fed1af64a0bf399"},
+ {file = "pydantic_core-2.18.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cfeecd1ac6cc1fb2692c3d5110781c965aabd4ec5d32799773ca7b1456ac636b"},
+ {file = "pydantic_core-2.18.2-cp39-none-win32.whl", hash = "sha256:0d69b4c2f6bb3e130dba60d34c0845ba31b69babdd3f78f7c0c8fae5021a253e"},
+ {file = "pydantic_core-2.18.2-cp39-none-win_amd64.whl", hash = "sha256:d9319e499827271b09b4e411905b24a426b8fb69464dfa1696258f53a3334641"},
+ {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a1874c6dd4113308bd0eb568418e6114b252afe44319ead2b4081e9b9521fe75"},
+ {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:ccdd111c03bfd3666bd2472b674c6899550e09e9f298954cfc896ab92b5b0e6d"},
+ {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e18609ceaa6eed63753037fc06ebb16041d17d28199ae5aba0052c51449650a9"},
+ {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e5c584d357c4e2baf0ff7baf44f4994be121e16a2c88918a5817331fc7599d7"},
+ {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43f0f463cf89ace478de71a318b1b4f05ebc456a9b9300d027b4b57c1a2064fb"},
+ {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e1b395e58b10b73b07b7cf740d728dd4ff9365ac46c18751bf8b3d8cca8f625a"},
+ {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0098300eebb1c837271d3d1a2cd2911e7c11b396eac9661655ee524a7f10587b"},
+ {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:36789b70d613fbac0a25bb07ab3d9dba4d2e38af609c020cf4d888d165ee0bf3"},
+ {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3f9a801e7c8f1ef8718da265bba008fa121243dfe37c1cea17840b0944dfd72c"},
+ {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:3a6515ebc6e69d85502b4951d89131ca4e036078ea35533bb76327f8424531ce"},
+ {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20aca1e2298c56ececfd8ed159ae4dde2df0781988c97ef77d5c16ff4bd5b400"},
+ {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:223ee893d77a310a0391dca6df00f70bbc2f36a71a895cecd9a0e762dc37b349"},
+ {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2334ce8c673ee93a1d6a65bd90327588387ba073c17e61bf19b4fd97d688d63c"},
+ {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:cbca948f2d14b09d20268cda7b0367723d79063f26c4ffc523af9042cad95592"},
+ {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b3ef08e20ec49e02d5c6717a91bb5af9b20f1805583cb0adfe9ba2c6b505b5ae"},
+ {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6fdc8627910eed0c01aed6a390a252fe3ea6d472ee70fdde56273f198938374"},
+ {file = "pydantic_core-2.18.2.tar.gz", hash = "sha256:2e29d20810dfc3043ee13ac7d9e25105799817683348823f305ab3f349b9386e"},
+]
+
+[package.dependencies]
+typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
+
+[[package]]
+name = "pyparsing"
+version = "3.1.2"
+description = "pyparsing module - Classes and methods to define and execute parsing grammars"
+optional = false
+python-versions = ">=3.6.8"
+files = [
+ {file = "pyparsing-3.1.2-py3-none-any.whl", hash = "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742"},
+ {file = "pyparsing-3.1.2.tar.gz", hash = "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad"},
+]
+
+[package.extras]
+diagrams = ["jinja2", "railroad-diagrams"]
+
+[[package]]
+name = "python-dateutil"
+version = "2.9.0.post0"
+description = "Extensions to the standard Python datetime module"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
+files = [
+ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
+ {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
+]
+
+[package.dependencies]
+six = ">=1.5"
+
+[[package]]
+name = "six"
+version = "1.16.0"
+description = "Python 2 and 3 compatibility utilities"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
+files = [
+ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
+ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
+]
+
+[[package]]
+name = "sniffio"
+version = "1.3.1"
+description = "Sniff out which async library your code is running under"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
+ {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
+]
+
+[[package]]
+name = "starlette"
+version = "0.37.2"
+description = "The little ASGI library that shines."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "starlette-0.37.2-py3-none-any.whl", hash = "sha256:6fe59f29268538e5d0d182f2791a479a0c64638e6935d1c6989e63fb2699c6ee"},
+ {file = "starlette-0.37.2.tar.gz", hash = "sha256:9af890290133b79fc3db55474ade20f6220a364a0402e0b556e7cd5e1e093823"},
+]
+
+[package.dependencies]
+anyio = ">=3.4.0,<5"
+
+[package.extras]
+full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"]
+
+[[package]]
+name = "typing-extensions"
+version = "4.11.0"
+description = "Backported and Experimental Type Hints for Python 3.8+"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"},
+ {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"},
+]
+
+[[package]]
+name = "uvicorn"
+version = "0.22.0"
+description = "The lightning-fast ASGI server."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "uvicorn-0.22.0-py3-none-any.whl", hash = "sha256:e9434d3bbf05f310e762147f769c9f21235ee118ba2d2bf1155a7196448bd996"},
+ {file = "uvicorn-0.22.0.tar.gz", hash = "sha256:79277ae03db57ce7d9aa0567830bbb51d7a612f54d6e1e3e92da3ef24c2c8ed8"},
+]
+
+[package.dependencies]
+click = ">=7.0"
+h11 = ">=0.8"
+
+[package.extras]
+standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"]
+
+[metadata]
+lock-version = "2.0"
+python-versions = "^3.10"
+content-hash = "c67ad07886d909cfac5f81e7822fd7ddae85b5f1b268b604aa531e24c752baeb"
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..2b9fa9e
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,18 @@
+[tool.poetry]
+name = "ephemerality"
+version = "2.0.0"
+description = "Module for computing ephemerality metrics for temporal activity vectors."
+authors = ["Dmitry Gnatyshak "]
+readme = "README.md"
+
+[tool.poetry.dependencies]
+python = "^3.10"
+numpy = "^1.24.2"
+pydantic = "^2.7.1"
+matplotlib = "^3.8.4"
+fastapi = "^0.110.2"
+uvicorn = "^0.22.0"
+
+[build-system]
+requires = ["poetry-core"]
+build-backend = "poetry.core.masonry.api"
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 8cef061..0000000
--- a/requirements.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-numpy==1.24.1
-fastapi~=0.89.1
-setuptools==66.0.0
-pydantic==1.10.4
-uvicorn~=0.20.0
diff --git a/rest/__init__.py b/rest/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/rest/api.py b/rest/api.py
deleted file mode 100644
index d537a85..0000000
--- a/rest/api.py
+++ /dev/null
@@ -1,48 +0,0 @@
-from fastapi import FastAPI, status
-from pydantic import BaseModel
-import rest.api11 as api11
-from src import EphemeralitySet
-
-
-app = FastAPI()
-
-
-class InputData(BaseModel):
- input_vector: list[float]
- threshold: float
-
-
-@app.post("/ephemerality/{api_version}/all", status_code=status.HTTP_200_OK)
-async def get_all_ephemeralities(api_version: str, input_data: InputData) -> EphemeralitySet:
- if api_version == '1.1':
- return api11.get_all_ephemeralities(input_vector=input_data.input_vector, threshold=input_data.threshold)
- else:
- raise ValueError(f'Unrecognized API version: {api_version}!')
-
-@app.post("/ephemerality/{api_version}/left", status_code=status.HTTP_200_OK)
-async def get_left_core_ephemeralities(api_version: str, input_data: InputData) -> EphemeralitySet:
- if api_version == '1.1':
- return api11.get_left_core_ephemerality(input_vector=input_data['input_vector'], threshold=input_data['threshold'])
- else:
- raise ValueError(f'Unrecognized API version: {api_version}!')
-
-@app.post("/ephemerality/{api_version}/middle", status_code=status.HTTP_200_OK)
-async def get_middle_core_ephemeralities(api_version: str, input_data: InputData) -> EphemeralitySet:
- if api_version == '1.1':
- return api11.get_middle_core_ephemerality(input_vector=input_data['input_vector'], threshold=input_data['threshold'])
- else:
- raise ValueError(f'Unrecognized API version: {api_version}!')
-
-@app.post("/ephemerality/{api_version}/right", status_code=status.HTTP_200_OK)
-async def get_right_core_ephemeralities(api_version: str, input_data: InputData) -> EphemeralitySet:
- if api_version == '1.1':
- return api11.get_right_core_ephemerality(input_vector=input_data['input_vector'], threshold=input_data['threshold'])
- else:
- raise ValueError(f'Unrecognized API version: {api_version}!')
-
-@app.post("/ephemerality/{api_version}/sorted", status_code=status.HTTP_200_OK)
-async def get_sorted_core_ephemeralities(api_version: str, input_data: InputData) -> EphemeralitySet:
- if api_version == '1.1':
- return api11.get_sorted_core_ephemerality(input_vector=input_data['input_vector'], threshold=input_data['threshold'])
- else:
- raise ValueError(f'Unrecognized API version: {api_version}!')
\ No newline at end of file
diff --git a/rest/api11.py b/rest/api11.py
deleted file mode 100644
index b386156..0000000
--- a/rest/api11.py
+++ /dev/null
@@ -1,18 +0,0 @@
-from typing import Sequence
-from src import compute_ephemerality, EphemeralitySet
-
-
-def get_all_ephemeralities(input_vector: Sequence[float], threshold: float) -> EphemeralitySet:
- return compute_ephemerality(frequency_vector=input_vector, threshold=threshold, types='all')
-
-def get_left_core_ephemerality(input_vector: Sequence[float], threshold: float) -> EphemeralitySet:
- return compute_ephemerality(frequency_vector=input_vector, threshold=threshold, types='left')
-
-def get_middle_core_ephemerality(input_vector: Sequence[float], threshold: float) -> EphemeralitySet:
- return compute_ephemerality(frequency_vector=input_vector, threshold=threshold, types='middle')
-
-def get_right_core_ephemerality(input_vector: Sequence[float], threshold: float) -> EphemeralitySet:
- return compute_ephemerality(frequency_vector=input_vector, threshold=threshold, types='right')
-
-def get_sorted_core_ephemerality(input_vector: Sequence[float], threshold: float) -> EphemeralitySet:
- return compute_ephemerality(frequency_vector=input_vector, threshold=threshold, types='sorted')
\ No newline at end of file
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 7a31bb1..0000000
--- a/setup.py
+++ /dev/null
@@ -1,30 +0,0 @@
-import os
-from setuptools import setup
-import re
-
-VERSION_FILE = "_version.py"
-VERSION_REGEX = r"^__version__ = ['\"]([^'\"]*)['\"]"
-
-
-def read(file_name):
- return open(os.path.join(os.path.dirname(__file__), file_name)).read()
-
-
-version_lines = open(VERSION_FILE, 'r').read()
-match = re.search(VERSION_REGEX, version_lines, re.M)
-if match:
- version = match.group(1)
-else:
- raise RuntimeError("Unable to find version string in %s." % (VERSION_FILE,))
-
-setup(
- name='ephemerality',
- version=version,
- packages=['src', 'test'],
- url='https://github.com/HPAI-BSC/ephemerality',
- license='MIT',
- author='HPAI BSC',
- author_email='dmitry.gnatyshak@bsc.es',
- description='Module for computing ephemerality metrics of temporal arrays.',
- long_description=read('README.md')
-)
diff --git a/src/__init__.py b/src/__init__.py
deleted file mode 100644
index 5fae7b4..0000000
--- a/src/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from src.ephemerality_computation import compute_ephemerality, EphemeralitySet
-
-__all__ = ['compute_ephemerality', 'EphemeralitySet']
diff --git a/src/ephemerality_computation.py b/src/ephemerality_computation.py
deleted file mode 100644
index 2486067..0000000
--- a/src/ephemerality_computation.py
+++ /dev/null
@@ -1,174 +0,0 @@
-import numpy as np
-from typing import Sequence
-from pydantic import BaseModel
-import warnings
-
-
-class EphemeralitySet(BaseModel):
- """Class to contain ephemerality values by subtypes"""
- left_core: float = None
- middle_core: float = None
- right_core: float = None
- sorted_core: float = None
-
-
-def _normalize_frequency_vector(frequency_vector: Sequence[float]) -> np.array:
- frequency_vector = np.array(frequency_vector)
-
- if sum(frequency_vector) != 1.:
- frequency_vector /= np.sum(frequency_vector)
-
- return frequency_vector
-
-
-def _ephemerality_raise_error(threshold: float):
- if 0. < threshold <= 1:
- raise ValueError('Input frequency vector has not been internally normalized!')
- else:
- raise ValueError('Threshold value is not within (0, 1] range!')
-
-
-def compute_left_core_length(frequency_vector: np.array, threshold: float) -> int:
- current_sum = 0
- for i, freq in enumerate(frequency_vector):
- current_sum = current_sum + freq
- if np.isclose(current_sum, threshold) or current_sum > threshold:
- return i + 1
-
- _ephemerality_raise_error(threshold)
-
-
-def compute_right_core_length(frequency_vector: np.array, threshold: float) -> int:
- current_sum = 0
- for i, freq in enumerate(frequency_vector[::-1]):
- current_sum = current_sum + freq
- if np.isclose(current_sum, threshold) or current_sum > threshold:
- return i + 1
-
- _ephemerality_raise_error(threshold)
-
-
-def compute_middle_core_length(frequency_vector: np.array, threshold: float) -> int:
- lower_threshold = (1. - threshold) / 2
-
- current_presum = 0
- start_index = -1
- for i, freq in enumerate(frequency_vector):
- current_presum += freq
- if current_presum > lower_threshold and not np.isclose(current_presum, lower_threshold):
- start_index = i
- break
-
- current_sum = 0
- for j, freq in enumerate(frequency_vector[start_index:]):
- current_sum += freq
- if np.isclose(current_sum, threshold) or current_sum > threshold:
- return j + 1
-
- _ephemerality_raise_error(threshold)
-
-
-def compute_sorted_core_length(frequency_vector: np.array, threshold: float) -> int:
- freq_descending_order = np.sort(frequency_vector)[::-1]
-
- current_sum = 0
- for i, freq in enumerate(freq_descending_order):
- current_sum += freq
- if np.isclose(current_sum, threshold) or current_sum > threshold:
- return i + 1
-
- _ephemerality_raise_error(threshold)
-
-
-def _compute_ephemerality_from_core(core_length: int, range_length: int, threshold: float):
- return 1 - (core_length / range_length) / threshold
-
-
-def _check_threshold(threshold: float):
- if threshold <= 0.:
- raise ValueError('Threshold value must be greater than 0!')
-
- if threshold > 1.:
- raise ValueError('Threshold value must be less or equal to 1!')
-
-
-def compute_ephemerality(
- frequency_vector: Sequence[float],
- threshold: float = 0.8,
- types: str = 'all') -> EphemeralitySet:
-
- _check_threshold(threshold)
-
- if np.isclose(np.sum(frequency_vector), 0.):
- return EphemeralitySet(
- left_core=1.,
- middle_core=1.,
- right_core=1.,
- sorted_core=1.
- )
-
- frequency_vector = _normalize_frequency_vector(frequency_vector)
- range_length = len(frequency_vector)
-
- if types == 'all' or types == 'left':
- left_core_length = compute_left_core_length(frequency_vector, threshold)
- ephemerality_left_core = _compute_ephemerality_from_core(left_core_length, range_length, threshold)
- if ephemerality_left_core < 0. and not np.isclose(ephemerality_left_core, 0.):
- warnings.warn(f'Original ephemerality value is less than 0 ({ephemerality_left_core}) and is going to be rounded up! '
- f'This is indicative of the edge case in which ephemerality span is greater than '
- f'[threshold * input_vector_length], i.e. most of the frequency mass lies in a few vector '
- f'elements at the end of the frequency vector. Original ephemerality in this case should be '
- f'considered to be equal to 0. However, please double check the input vector!',
- RuntimeWarning)
- ephemerality_left_core = 0.
- else:
- ephemerality_left_core = None
-
- if types == 'all' or types == 'middle':
- middle_core_length = compute_middle_core_length(frequency_vector, threshold)
- ephemerality_middle_core = _compute_ephemerality_from_core(middle_core_length, range_length, threshold)
- if ephemerality_middle_core < 0. and not np.isclose(ephemerality_middle_core, 0.):
- warnings.warn(f'Filtered ephemerality value is less than 0 ({ephemerality_middle_core}) and is going to be rounded up! '
- f'This is indicative of the edge case in which ephemerality span is greater than '
- f'[threshold * input_vector_length], i.e. most of the frequency mass lies in a few elements '
- f'at the beginning and the end of the frequency vector. Filtered ephemerality in this case should '
- f'be considered to be equal to 0. However, please double check the input vector!',
- RuntimeWarning)
- ephemerality_middle_core = 0.
- else:
- ephemerality_middle_core = None
-
- if types == 'all' or types == 'right':
- right_core_length = compute_right_core_length(frequency_vector, threshold)
- ephemerality_right_core = _compute_ephemerality_from_core(right_core_length, range_length, threshold)
- if ephemerality_right_core < 0. and not np.isclose(ephemerality_right_core, 0.):
- warnings.warn(f'Original ephemerality value is less than 0 ({ephemerality_right_core}) and is going to be rounded up! '
- f'This is indicative of the edge case in which ephemerality span is greater than '
- f'[threshold * input_vector_length], i.e. most of the frequency mass lies in a few vector '
- f'elements at the end of the frequency vector. Original ephemerality in this case should be '
- f'considered to be equal to 0. However, please double check the input vector!',
- RuntimeWarning)
- ephemerality_right_core = 0.
- else:
- ephemerality_right_core = None
-
- if types == 'all' or types == 'sorted':
- sorted_core_length = compute_sorted_core_length(frequency_vector, threshold)
- ephemerality_sorted_core = _compute_ephemerality_from_core(sorted_core_length, range_length, threshold)
- if ephemerality_sorted_core < 0. and not np.isclose(ephemerality_sorted_core, 0.):
- warnings.warn(f'Sorted ephemerality value is less than 0 ({ephemerality_sorted_core}) and is going to be rounded up! '
- f'This is indicative of the rare edge case of very short and mostly uniform frequency vector (so '
- f'that ephemerality span is greater than [threshold * input_vector_length]). '
- f'Sorted ephemerality in this case should be considered to be equal to 0. '
- f'However, please double check the input vector!',
- RuntimeWarning)
- ephemerality_sorted_core = 0.
- else:
- ephemerality_sorted_core = None
-
- ephemeralities = EphemeralitySet(left_core=ephemerality_left_core,
- middle_core=ephemerality_middle_core,
- right_core=ephemerality_right_core,
- sorted_core=ephemerality_sorted_core)
-
- return ephemeralities
diff --git a/test/__init__.py b/test/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/test/ephemerality_test.py b/test/ephemerality_test.py
deleted file mode 100644
index 9bfd3b8..0000000
--- a/test/ephemerality_test.py
+++ /dev/null
@@ -1,510 +0,0 @@
-import warnings
-from unittest import TestCase
-
-from typing import Sequence
-import numpy as np
-from dataclasses import dataclass
-import re
-
-from src import compute_ephemerality
-
-
-@dataclass
-class EphemeralityTestCase:
- input_vector: Sequence[float]
- threshold: float
- expected_output: dict
- warnings: tuple[bool, bool, bool]
-
-
-class TestComputeEphemerality(TestCase):
- _warning_messages = [
- re.compile(
- r'Original ephemerality value is less than 0 [(]-[0-9]*[.][0-9]*[)] and is going to be rounded up! '
- r'This is indicative of the edge case in which ephemerality span is greater than '
- r'\[threshold [*] input_vector_length], i[.]e[.] most of the frequency mass lies in a few vector '
- r'elements at the end of the frequency vector[.] Original ephemerality in this case should be '
- r'considered to be equal to 0[.] However, please double check the input vector!'
- ),
-
- re.compile(
- r'Filtered ephemerality value is less than 0 [(]-[0-9]*[.][0-9]*[)] and is going to be rounded up! '
- r'This is indicative of the edge case in which ephemerality span is greater than '
- r'\[threshold [*] input_vector_length], i[.]e[.] most of the frequency mass lies in a few elements '
- r'at the beginning and the end of the frequency vector[.] Filtered ephemerality in this case should '
- r'be considered to be equal to 0[.] However, please double check the input vector!'
- ),
-
- re.compile(
- r'Sorted ephemerality value is less than 0 [(]-[0-9]*[.][0-9]*[)] and is going to be rounded up! '
- r'This is indicative of the rare edge case of very short and mostly uniform frequency vector [(]so '
- r'that ephemerality span is greater than \[threshold [*] input_vector_length][)][.] '
- r'Sorted ephemerality in this case should be considered to be equal to 0[.] '
- r'However, please double check the input vector!'
- )
- ]
-
- _test_cases = [
- EphemeralityTestCase(
- input_vector=[1.],
- threshold=0.8,
- expected_output={
- 'ephemerality_original': 0., 'ephemerality_original_span': 1,
- 'ephemerality_filtered': 0., 'ephemerality_filtered_span': 1,
- 'ephemerality_sorted': 0., 'ephemerality_sorted_span': 1
- },
- warnings=(True, True, True)
- ),
- EphemeralityTestCase(
- input_vector=[1.],
- threshold=0.3,
- expected_output={
- 'ephemerality_original': 0., 'ephemerality_original_span': 1,
- 'ephemerality_filtered': 0., 'ephemerality_filtered_span': 1,
- 'ephemerality_sorted': 0., 'ephemerality_sorted_span': 1
- },
- warnings=(True, True, True)
- ),
- EphemeralityTestCase(
- input_vector=[1., 0.],
- threshold=0.8,
- expected_output={
- 'ephemerality_original': 0.375, 'ephemerality_original_span': 1,
- 'ephemerality_filtered': 0.375, 'ephemerality_filtered_span': 1,
- 'ephemerality_sorted': 0.375, 'ephemerality_sorted_span': 1
- },
- warnings=(False, False, False)
- ),
- EphemeralityTestCase(
- input_vector=[1., 0.],
- threshold=0.3,
- expected_output={
- 'ephemerality_original': 0., 'ephemerality_original_span': 1,
- 'ephemerality_filtered': 0., 'ephemerality_filtered_span': 1,
- 'ephemerality_sorted': 0., 'ephemerality_sorted_span': 1
- },
- warnings=(True, True, True)
- ),
- EphemeralityTestCase(
- input_vector=[0., 1.],
- threshold=0.8,
- expected_output={
- 'ephemerality_original': 0., 'ephemerality_original_span': 2,
- 'ephemerality_filtered': 0.375, 'ephemerality_filtered_span': 1,
- 'ephemerality_sorted': 0.375, 'ephemerality_sorted_span': 1
- },
- warnings=(True, False, False)
- ),
- EphemeralityTestCase(
- input_vector=[0., 1.],
- threshold=0.3,
- expected_output={
- 'ephemerality_original': 0., 'ephemerality_original_span': 2,
- 'ephemerality_filtered': 0., 'ephemerality_filtered_span': 1,
- 'ephemerality_sorted': 0., 'ephemerality_sorted_span': 1
- },
- warnings=(True, True, True)
- ),
- EphemeralityTestCase(
- input_vector=[.5, .5],
- threshold=0.8,
- expected_output={
- 'ephemerality_original': 0., 'ephemerality_original_span': 2,
- 'ephemerality_filtered': 0., 'ephemerality_filtered_span': 2,
- 'ephemerality_sorted': 0., 'ephemerality_sorted_span': 2
- },
- warnings=(True, True, True)
- ),
- EphemeralityTestCase(
- input_vector=[.5, .5],
- threshold=0.3,
- expected_output={
- 'ephemerality_original': 0., 'ephemerality_original_span': 1,
- 'ephemerality_filtered': 0., 'ephemerality_filtered_span': 1,
- 'ephemerality_sorted': 0., 'ephemerality_sorted_span': 1
- },
- warnings=(True, True, True)
- ),
- EphemeralityTestCase(
- input_vector=[0.7, .3],
- threshold=0.8,
- expected_output={
- 'ephemerality_original': 0., 'ephemerality_original_span': 2,
- 'ephemerality_filtered': 0., 'ephemerality_filtered_span': 2,
- 'ephemerality_sorted': 0., 'ephemerality_sorted_span': 2
- },
- warnings=(True, True, True)
- ),
- EphemeralityTestCase(
- input_vector=[0.7, .3],
- threshold=0.3,
- expected_output={
- 'ephemerality_original': 0., 'ephemerality_original_span': 1,
- 'ephemerality_filtered': 0., 'ephemerality_filtered_span': 1,
- 'ephemerality_sorted': 0., 'ephemerality_sorted_span': 1
- },
- warnings=(True, True, True)
- ),
- EphemeralityTestCase(
- input_vector=[1., 0., 0., 0.],
- threshold=0.8,
- expected_output={
- 'ephemerality_original': 0.6875, 'ephemerality_original_span': 1,
- 'ephemerality_filtered': 0.6875, 'ephemerality_filtered_span': 1,
- 'ephemerality_sorted': 0.6875, 'ephemerality_sorted_span': 1
- },
- warnings=(False, False, False)
- ),
- EphemeralityTestCase(
- input_vector=[1., 0., 0., 0.],
- threshold=0.3,
- expected_output={
- 'ephemerality_original': 1 / 6, 'ephemerality_original_span': 1,
- 'ephemerality_filtered': 1 / 6, 'ephemerality_filtered_span': 1,
- 'ephemerality_sorted': 1 / 6, 'ephemerality_sorted_span': 1
- },
- warnings=(False, False, False)
- ),
- EphemeralityTestCase(
- input_vector=[0., 0., 0., 1.],
- threshold=0.8,
- expected_output={
- 'ephemerality_original': 0., 'ephemerality_original_span': 4,
- 'ephemerality_filtered': 0.6875, 'ephemerality_filtered_span': 1,
- 'ephemerality_sorted': 0.6875, 'ephemerality_sorted_span': 1
- },
- warnings=(True, False, False)
- ),
- EphemeralityTestCase(
- input_vector=[0., 0., 0., 1.],
- threshold=0.3,
- expected_output={
- 'ephemerality_original': 0., 'ephemerality_original_span': 4,
- 'ephemerality_filtered': 1 / 6, 'ephemerality_filtered_span': 1,
- 'ephemerality_sorted': 1 / 6, 'ephemerality_sorted_span': 1
- },
- warnings=(True, False, False)
- ),
- EphemeralityTestCase(
- input_vector=[0., 1., 0., 1.],
- threshold=0.8,
- expected_output={
- 'ephemerality_original': 0., 'ephemerality_original_span': 4,
- 'ephemerality_filtered': 0.0625, 'ephemerality_filtered_span': 3,
- 'ephemerality_sorted': 0.375, 'ephemerality_sorted_span': 2
- },
- warnings=(True, False, False)
- ),
- EphemeralityTestCase(
- input_vector=[0., 1., 0., 1.],
- threshold=0.3,
- expected_output={
- 'ephemerality_original': 0., 'ephemerality_original_span': 2,
- 'ephemerality_filtered': 1 / 6, 'ephemerality_filtered_span': 1,
- 'ephemerality_sorted': 1 / 6, 'ephemerality_sorted_span': 1
- },
- warnings=(True, False, False)
- ),
- EphemeralityTestCase(
- input_vector=[1., 1., 1., 1.],
- threshold=0.8,
- expected_output={
- 'ephemerality_original': 0., 'ephemerality_original_span': 4,
- 'ephemerality_filtered': 0., 'ephemerality_filtered_span': 4,
- 'ephemerality_sorted': 0., 'ephemerality_sorted_span': 4
- },
- warnings=(True, True, True)
- ),
- EphemeralityTestCase(
- input_vector=[1., 1., 1., 1.],
- threshold=0.3,
- expected_output={
- 'ephemerality_original': 0., 'ephemerality_original_span': 2,
- 'ephemerality_filtered': 0., 'ephemerality_filtered_span': 2,
- 'ephemerality_sorted': 0., 'ephemerality_sorted_span': 2
- },
- warnings=(True, True, True)
- ),
- EphemeralityTestCase(
- input_vector=[1., 1., 0., 0.],
- threshold=0.8,
- expected_output={
- 'ephemerality_original': 0.375, 'ephemerality_original_span': 2,
- 'ephemerality_filtered': 0.375, 'ephemerality_filtered_span': 2,
- 'ephemerality_sorted': 0.375, 'ephemerality_sorted_span': 2
- },
- warnings=(False, False, False)
- ),
- EphemeralityTestCase(
- input_vector=[1., 1., 0., 0.],
- threshold=0.3,
- expected_output={
- 'ephemerality_original': 1 / 6, 'ephemerality_original_span': 1,
- 'ephemerality_filtered': 1 / 6, 'ephemerality_filtered_span': 1,
- 'ephemerality_sorted': 1 / 6, 'ephemerality_sorted_span': 1
- },
- warnings=(False, False, False)
- ),
- EphemeralityTestCase(
- input_vector=[1., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
- threshold=0.8,
- expected_output={
- 'ephemerality_original': 0.875, 'ephemerality_original_span': 1,
- 'ephemerality_filtered': 0.875, 'ephemerality_filtered_span': 1,
- 'ephemerality_sorted': 0.875, 'ephemerality_sorted_span': 1
- },
- warnings=(False, False, False)
- ),
- EphemeralityTestCase(
- input_vector=[1., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
- threshold=0.3,
- expected_output={
- 'ephemerality_original': 2 / 3, 'ephemerality_original_span': 1,
- 'ephemerality_filtered': 2 / 3, 'ephemerality_filtered_span': 1,
- 'ephemerality_sorted': 2 / 3, 'ephemerality_sorted_span': 1
- },
- warnings=(False, False, False)
- ),
- EphemeralityTestCase(
- input_vector=[0., 0., 0., 0., 1., 0., 0., 0., 0., 0.],
- threshold=0.8,
- expected_output={
- 'ephemerality_original': 0.375, 'ephemerality_original_span': 5,
- 'ephemerality_filtered': 0.875, 'ephemerality_filtered_span': 1,
- 'ephemerality_sorted': 0.875, 'ephemerality_sorted_span': 1
- },
- warnings=(False, False, False)
- ),
- EphemeralityTestCase(
- input_vector=[0., 0., 0., 0., 1., 0., 0., 0., 0., 0.],
- threshold=0.3,
- expected_output={
- 'ephemerality_original': 0., 'ephemerality_original_span': 5,
- 'ephemerality_filtered': 2 / 3, 'ephemerality_filtered_span': 1,
- 'ephemerality_sorted': 2 / 3, 'ephemerality_sorted_span': 1
- },
- warnings=(True, False, False)
- ),
- EphemeralityTestCase(
- input_vector=[0., 0., 1., 0., 0., 0., 0., 0., 0., 0.],
- threshold=0.8,
- expected_output={
- 'ephemerality_original': 0.625, 'ephemerality_original_span': 3,
- 'ephemerality_filtered': 0.875, 'ephemerality_filtered_span': 1,
- 'ephemerality_sorted': 0.875, 'ephemerality_sorted_span': 1
- },
- warnings=(False, False, False)
- ),
- EphemeralityTestCase(
- input_vector=[0., 0., 1., 0., 0., 0., 0., 0., 0., 0.],
- threshold=0.3,
- expected_output={
- 'ephemerality_original': 0., 'ephemerality_original_span': 3,
- 'ephemerality_filtered': 2 / 3, 'ephemerality_filtered_span': 1,
- 'ephemerality_sorted': 2 / 3, 'ephemerality_sorted_span': 1
- },
- warnings=(False, False, False)
- ),
- EphemeralityTestCase(
- input_vector=[0., 0., 0., 1., 0., 0., 0., 0., 0., 0.],
- threshold=0.8,
- expected_output={
- 'ephemerality_original': 0.5, 'ephemerality_original_span': 4,
- 'ephemerality_filtered': 0.875, 'ephemerality_filtered_span': 1,
- 'ephemerality_sorted': 0.875, 'ephemerality_sorted_span': 1
- },
- warnings=(False, False, False)
- ),
- EphemeralityTestCase(
- input_vector=[0., 0., 0., 1., 0., 0., 0., 0., 0., 0.],
- threshold=0.3,
- expected_output={
- 'ephemerality_original': 0., 'ephemerality_original_span': 4,
- 'ephemerality_filtered': 2 / 3, 'ephemerality_filtered_span': 1,
- 'ephemerality_sorted': 2 / 3, 'ephemerality_sorted_span': 1
- },
- warnings=(True, False, False)
- ),
- EphemeralityTestCase(
- input_vector=[0., 0., 0., 0., 0., 0., 0., 1., 0., 0.],
- threshold=0.8,
- expected_output={
- 'ephemerality_original': 0., 'ephemerality_original_span': 8,
- 'ephemerality_filtered': 0.875, 'ephemerality_filtered_span': 1,
- 'ephemerality_sorted': 0.875, 'ephemerality_sorted_span': 1
- },
- warnings=(False, False, False)
- ),
- EphemeralityTestCase(
- input_vector=[0., 0., 0., 0., 0., 0., 0., 1., 0., 0.],
- threshold=0.3,
- expected_output={
- 'ephemerality_original': 0., 'ephemerality_original_span': 8,
- 'ephemerality_filtered': 2 / 3, 'ephemerality_filtered_span': 1,
- 'ephemerality_sorted': 2 / 3, 'ephemerality_sorted_span': 1
- },
- warnings=(True, False, False)
- ),
- EphemeralityTestCase(
- input_vector=[0., 0., 0., 0., 0., 0., 0., 0., 1., 0.],
- threshold=0.8,
- expected_output={
- 'ephemerality_original': 0., 'ephemerality_original_span': 9,
- 'ephemerality_filtered': 0.875, 'ephemerality_filtered_span': 1,
- 'ephemerality_sorted': 0.875, 'ephemerality_sorted_span': 1
- },
- warnings=(True, False, False)
- ),
- EphemeralityTestCase(
- input_vector=[0., 0., 0., 0., 0., 0., 0., 0., 1., 0.],
- threshold=0.3,
- expected_output={
- 'ephemerality_original': 0., 'ephemerality_original_span': 9,
- 'ephemerality_filtered': 2 / 3, 'ephemerality_filtered_span': 1,
- 'ephemerality_sorted': 2 / 3, 'ephemerality_sorted_span': 1
- },
- warnings=(True, False, False)
- ),
- EphemeralityTestCase(
- input_vector=[0., 0., 0., 0., 0., 0., 0., 0., 0., 1.],
- threshold=0.8,
- expected_output={
- 'ephemerality_original': 0., 'ephemerality_original_span': 10,
- 'ephemerality_filtered': 0.875, 'ephemerality_filtered_span': 1,
- 'ephemerality_sorted': 0.875, 'ephemerality_sorted_span': 1
- },
- warnings=(True, False, False)
- ),
- EphemeralityTestCase(
- input_vector=[0., 0., 0., 0., 0., 0., 0., 0., 0., 1.],
- threshold=0.3,
- expected_output={
- 'ephemerality_original': 0., 'ephemerality_original_span': 10,
- 'ephemerality_filtered': 2 / 3, 'ephemerality_filtered_span': 1,
- 'ephemerality_sorted': 2 / 3, 'ephemerality_sorted_span': 1
- },
- warnings=(True, False, False)
- ),
- EphemeralityTestCase(
- input_vector=[.1, .1, .1, .1, .1, .1, .1, .1, .1, .1],
- threshold=0.8,
- expected_output={
- 'ephemerality_original': 0., 'ephemerality_original_span': 8,
- 'ephemerality_filtered': 0., 'ephemerality_filtered_span': 8,
- 'ephemerality_sorted': 0., 'ephemerality_sorted_span': 8
- },
- warnings=(False, False, False)
- ),
- EphemeralityTestCase(
- input_vector=[.1, .1, .1, .1, .1, .1, .1, .1, .1, .1],
- threshold=0.3,
- expected_output={
- 'ephemerality_original': 0., 'ephemerality_original_span': 3,
- 'ephemerality_filtered': 0., 'ephemerality_filtered_span': 3,
- 'ephemerality_sorted': 0., 'ephemerality_sorted_span': 3
- },
- warnings=(False, False, False)
- ),
- EphemeralityTestCase(
- input_vector=[0., 0., 0., .2, .55, 0., .15, .1, 0., 0.],
- threshold=0.8,
- expected_output={
- 'ephemerality_original': 0.125, 'ephemerality_original_span': 7,
- 'ephemerality_filtered': 0.5, 'ephemerality_filtered_span': 4,
- 'ephemerality_sorted': 0.625, 'ephemerality_sorted_span': 3
- },
- warnings=(False, False, False)
- ),
- EphemeralityTestCase(
- input_vector=[0., 0., 0., .2, .55, 0., .15, .1, 0., 0.],
- threshold=0.3,
- expected_output={
- 'ephemerality_original': 0., 'ephemerality_original_span': 5,
- 'ephemerality_filtered': 2 / 3, 'ephemerality_filtered_span': 1,
- 'ephemerality_sorted': 2 / 3, 'ephemerality_sorted_span': 1
- },
- warnings=(True, False, False)
- ),
- EphemeralityTestCase(
- input_vector=np.eye(1, 10000, k=5000).flatten(),
- threshold=0.8,
- expected_output={
- 'ephemerality_original': 0.375, 'ephemerality_original_span': 5000,
- 'ephemerality_filtered': 0.999875, 'ephemerality_filtered_span': 1,
- 'ephemerality_sorted': 0.999875, 'ephemerality_sorted_span': 1
- },
- warnings=(False, False, False)
- ),
- EphemeralityTestCase(
- input_vector=np.eye(1, 10000, k=5000).flatten(),
- threshold=0.3,
- expected_output={
- 'ephemerality_original': 0., 'ephemerality_original_span': 5000,
- 'ephemerality_filtered': 2999 / 3000, 'ephemerality_filtered_span': 1,
- 'ephemerality_sorted': 2999 / 3000, 'ephemerality_sorted_span': 1
- },
- warnings=(True, False, False)
- ),
- EphemeralityTestCase(
- input_vector=np.ones((10000,)),
- threshold=0.8,
- expected_output={
- 'ephemerality_original': 0., 'ephemerality_original_span': 8000,
- 'ephemerality_filtered': 0., 'ephemerality_filtered_span': 8000,
- 'ephemerality_sorted': 0., 'ephemerality_sorted_span': 8000
- },
- warnings=(False, False, False)
- ),
- EphemeralityTestCase(
- input_vector=np.ones((10000,)),
- threshold=0.3,
- expected_output={
- 'ephemerality_original': 0., 'ephemerality_original_span': 3000,
- 'ephemerality_filtered': 0., 'ephemerality_filtered_span': 3000,
- 'ephemerality_sorted': 0., 'ephemerality_sorted_span': 3000
- },
- warnings=(False, False, False)
- )
- ]
-
- def add_test_case(self,
- input_vector: Sequence[float],
- threshold: float,
- expected_output: dict,
- warnings: tuple[bool, bool, bool]):
- self._test_cases.append(EphemeralityTestCase(
- input_vector=input_vector,
- threshold=threshold,
- expected_output=expected_output,
- warnings=warnings
- ))
-
- def clear(self):
- self._test_cases = list()
-
- @staticmethod
- def round_ephemeralities(ephemeralities: dict, precision: int=8):
- np.round_(ephemeralities['ephemerality_original'], precision)
- np.round_(ephemeralities['ephemerality_filtered'], precision)
- np.round_(ephemeralities['ephemerality_sorted'], precision)
-
- def test_compute_ephemeralities(self):
- for i, test_case in enumerate(self._test_cases):
- print(f'\nRunning test case {i}: {test_case.input_vector}, threshold {test_case.threshold}...')
- with warnings.catch_warnings(record=True) as warns:
- warnings.simplefilter('always', category=RuntimeWarning)
-
- actual_output = compute_ephemerality(frequency_vector=test_case.input_vector,
- threshold=test_case.threshold)
-
- self.assertEqual(self.round_ephemeralities(test_case.expected_output),
- self.round_ephemeralities(actual_output))
-
- warn_messages = ""
- for warn in warns:
- warn_messages += str(warn.message)
-
- actual_warnings = tuple((TestComputeEphemerality._warning_messages[i].search(warn_messages) is not None
- for i in range(3)))
-
- self.assertEqual(test_case.warnings, actual_warnings)
diff --git a/testing/__init__.py b/testing/__init__.py
new file mode 100644
index 0000000..2afb534
--- /dev/null
+++ b/testing/__init__.py
@@ -0,0 +1,4 @@
+from testing.src import generate_data, generate_test_case, clear_data
+
+
+__all__ = ['generate_data', 'generate_test_case', 'clear_data']
diff --git a/testing/run_data_generator.py b/testing/run_data_generator.py
new file mode 100644
index 0000000..bae22fb
--- /dev/null
+++ b/testing/run_data_generator.py
@@ -0,0 +1,61 @@
+#!/usr/bin/env python3
+
+from argparse import ArgumentParser
+import sys
+from pathlib import Path
+
+from testing.src import generate_data
+
+
+def init_parser() -> ArgumentParser:
+ parser = ArgumentParser(
+ usage=f"{Path(sys.executable).stem} %(prog)s [-o OUTPUT_FOLDER][-g] [-d DATA_TYPE] [--data_range START END] "
+ "[-n MAX_TEST_SIZE] [-m CASES_PER_BATCH] [-s SEED] ...",
+ description="Generate data for tests."
+ )
+ parser.add_argument(
+ "-o", "--output_folder", action="store", default="./test_data/",
+ help="Path to the folder to store generated test cases. Defaults to \"./test_data/\"."
+ )
+ parser.add_argument(
+ "-d", "--data_type", action="store", choices=["activity", "a", "timestamps", "t", "datetime", "d"], default="a",
+ help="Type of the generated data. Defaults to \"a\"."
+ )
+ parser.add_argument(
+ "--data_range", action="store", type=float, nargs=2, default=None,
+ help="Value range for timestamps or datetime data types in UNIX timestamp in seconds. "
+ "Passed as 2 integer numbers. Defaults to (0, 31536000)."
+ )
+ parser.add_argument(
+ "-n", "--max_size", action="store", type=int, default=6,
+ help="Maximal size (in power 10) of test size batches. Defaults to 6."
+ )
+ parser.add_argument(
+ "-m", "--cases_per_batch", action="store", type=int, default=20,
+ help="Number of test cases in each size batch. Defaults to 20."
+ )
+ parser.add_argument(
+ "-s", "--seed", action="store", type=int, default=2023,
+ help="Value of the seed to be used for test case generation. Defaults to 2023."
+ )
+ return parser
+
+
+if __name__ == '__main__':
+ parser = init_parser()
+ args = parser.parse_args()
+ if args.data_range is None:
+ args.data_range = (0, 31536000)
+
+ if args.max_size <= 0:
+ raise ValueError("\"max_size\" value should be positive!")
+ if args.cases_per_batch <= 0:
+ raise ValueError("\"cases_per_batch\" value should be positive!")
+
+ generate_data(
+ max_size=args.max_size,
+ inputs_per_n=args.cases_per_batch,
+ data_type=args.data_type,
+ data_range=args.data_range,
+ seed=args.seed,
+ save_dir=args.output_folder)
diff --git a/testing/run_unit_tests.py b/testing/run_unit_tests.py
new file mode 100644
index 0000000..c5856ac
--- /dev/null
+++ b/testing/run_unit_tests.py
@@ -0,0 +1,7 @@
+#!/usr/bin/env python3
+
+from testing.src import test_ephemerality
+
+
+if __name__ == "__main__":
+ test_ephemerality()
diff --git a/testing/src/__init__.py b/testing/src/__init__.py
new file mode 100644
index 0000000..f5367ba
--- /dev/null
+++ b/testing/src/__init__.py
@@ -0,0 +1,5 @@
+from testing.src.data_generator import generate_test_case, generate_data, clear_data
+from testing.src.test_ephemerality import test_ephemerality
+
+
+__all__ = ['generate_test_case', 'generate_data', 'clear_data', 'test_ephemerality']
diff --git a/testing/src/data_generator.py b/testing/src/data_generator.py
new file mode 100644
index 0000000..85ab98e
--- /dev/null
+++ b/testing/src/data_generator.py
@@ -0,0 +1,93 @@
+import json
+import numpy as np
+from pathlib import Path
+import shutil
+from datetime import datetime
+
+
+def generate_test_case(
+ size: int,
+ data_type: str,
+ data_range: tuple[float, float] | None = None,
+ seed: None | int = None,
+ activity_length: None | int = None
+) -> tuple[float, list[float | str]]:
+
+ if activity_length is None:
+ activity_length = size
+ activity = np.zeros((activity_length,))
+ rng = np.random.default_rng(seed)
+ threshold = float(rng.uniform(low=0.1, high=0.9, size=None))
+ activity[0] = rng.normal(scale=10)
+ for i in range(1, activity_length):
+ activity[i] = activity[i - 1] + rng.normal()
+ activity -= np.mean(activity)
+ activity = activity.clip(min=0)
+
+ if data_type == "activity" or data_type == "a":
+ activity /= np.sum(activity)
+ return threshold, list(activity)
+
+ activity_granule_length = int(np.ceil((data_range[1] - data_range[0]) / activity_length))
+ activity = activity.repeat(activity_granule_length)[:(data_range[1] - data_range[0])]
+ activity /= np.sum(activity)
+
+ timestamps = rng.choice(
+ a=np.arange(data_range[0], data_range[1]).astype(int),
+ size=size,
+ p=activity
+ )
+ timestamps.sort()
+
+ if data_type == "timestamps" or data_type == "t":
+ return threshold, list(timestamps.astype(str))
+
+ return threshold, [datetime.fromtimestamp(ts).strftime("%Y-%m-%dT%H:%M:%S.%fZ").replace("000000Z", "000Z")
+ for ts in timestamps]
+
+
+def generate_data(
+ max_size: int = 10,
+ inputs_per_n: int = 100,
+ data_type: str = "a",
+ data_range: tuple[float, float] | None = None,
+ seed: int = 2023,
+ save_dir: str = "./test_data/"
+) -> None:
+ if save_dir and save_dir[-1] != '/':
+ save_dir += '/'
+
+ for n in range(1, max_size + 1):
+ dir_n = Path(f"{save_dir}{n}")
+ dir_n.mkdir(parents=True, exist_ok=True)
+
+ for i in range(inputs_per_n):
+ size = 10 ** n
+ test_case = generate_test_case(
+ size=size,
+ data_type=data_type,
+ data_range=data_range,
+ seed=seed + i,
+ activity_length=None if data_type == "activity" or data_type == "a" else int((data_range[1] - data_range[0]) / 1000)
+ )
+ test_data = [{
+ "threshold": test_case[0],
+ "input_sequence": test_case[1],
+ "input_type": data_type,
+ "range": [str(data_range[0]), str(data_range[1])],
+ "reference_name": f"{data_type}_{size}_{i}"
+ }]
+
+ with open(f"{dir_n}/{i}.json", "w") as f:
+ json.dump(test_data, f)
+
+
+def clear_data(folder: str) -> None:
+ for file in Path(folder).iterdir():
+ try:
+ if file.is_file() or file.is_symlink():
+ file.unlink()
+ elif file.is_dir():
+ shutil.rmtree(file)
+ except Exception as ex:
+ print(f'Failed to delete {file.resolve()}. Reason: {ex}')
diff --git a/testing/src/test_ephemerality.py b/testing/src/test_ephemerality.py
new file mode 100644
index 0000000..cca1727
--- /dev/null
+++ b/testing/src/test_ephemerality.py
@@ -0,0 +1,768 @@
+import json
+import time
+from unittest import TestCase, TextTestRunner
+import numpy as np
+from typing import Sequence, Iterable, Literal
+from typing_extensions import Self
+from itertools import chain, combinations
+import subprocess
+import sys
+import requests
+from testing.src.test_utils import EphemeralityTestCase
+from ephemerality import compute_ephemerality, EphemeralitySet, InputData
+
+
+DEFAULT_TEST_CASES = [
+ EphemeralityTestCase(
+ input_sequence=[1.],
+ threshold=0.8,
+ expected_output=EphemeralitySet(
+ len_left_core=1,
+ len_middle_core=1,
+ len_right_core=1,
+ len_sorted_core=1,
+ eph_left_core=0.,
+ eph_middle_core=0.,
+ eph_right_core=0.,
+ eph_sorted_core=0.
+ )
+ ),
+ EphemeralityTestCase(
+ input_sequence=[1.],
+ threshold=0.3,
+ expected_output=EphemeralitySet(
+ len_left_core=1,
+ len_middle_core=1,
+ len_right_core=1,
+ len_sorted_core=1,
+ eph_left_core=0.,
+ eph_middle_core=0.,
+ eph_right_core=0.,
+ eph_sorted_core=0.
+ )
+ ),
+ EphemeralityTestCase(
+ input_sequence=[1., 0.],
+ threshold=0.8,
+ expected_output=EphemeralitySet(
+ len_left_core=1,
+ len_middle_core=1,
+ len_right_core=2,
+ len_sorted_core=1,
+ eph_left_core=0.375,
+ eph_middle_core=0.375,
+ eph_right_core=0.,
+ eph_sorted_core=0.375
+ )
+ ),
+ EphemeralityTestCase(
+ input_sequence=[1., 0.],
+ threshold=0.3,
+ expected_output=EphemeralitySet(
+ len_left_core=1,
+ len_middle_core=1,
+ len_right_core=2,
+ len_sorted_core=1,
+ eph_left_core=0.,
+ eph_middle_core=0.,
+ eph_right_core=0.,
+ eph_sorted_core=0.
+ )
+ ),
+ EphemeralityTestCase(
+ input_sequence=[0., 1.],
+ threshold=0.8,
+ expected_output=EphemeralitySet(
+ len_left_core=2,
+ len_middle_core=1,
+ len_right_core=1,
+ len_sorted_core=1,
+ eph_left_core=0.,
+ eph_middle_core=0.375,
+ eph_right_core=0.375,
+ eph_sorted_core=0.375
+ )
+ ),
+ EphemeralityTestCase(
+ input_sequence=[0., 1.],
+ threshold=0.3,
+ expected_output=EphemeralitySet(
+ len_left_core=2,
+ len_middle_core=1,
+ len_right_core=1,
+ len_sorted_core=1,
+ eph_left_core=0.,
+ eph_middle_core=0.,
+ eph_right_core=0.,
+ eph_sorted_core=0.
+ )
+ ),
+ EphemeralityTestCase(
+ input_sequence=[.5, .5],
+ threshold=0.8,
+ expected_output=EphemeralitySet(
+ len_left_core=2,
+ len_middle_core=2,
+ len_right_core=2,
+ len_sorted_core=2,
+ eph_left_core=0.,
+ eph_middle_core=0.,
+ eph_right_core=0.,
+ eph_sorted_core=0.
+ )
+ ),
+ EphemeralityTestCase(
+ input_sequence=[.5, .5],
+ threshold=0.3,
+ expected_output=EphemeralitySet(
+ len_left_core=1,
+ len_middle_core=1,
+ len_right_core=1,
+ len_sorted_core=1,
+ eph_left_core=0.,
+ eph_middle_core=0.,
+ eph_right_core=0.,
+ eph_sorted_core=0.
+ )
+ ),
+ EphemeralityTestCase(
+ input_sequence=[0.7, .3],
+ threshold=0.8,
+ expected_output=EphemeralitySet(
+ len_left_core=2,
+ len_middle_core=2,
+ len_right_core=2,
+ len_sorted_core=2,
+ eph_left_core=0.,
+ eph_middle_core=0.,
+ eph_right_core=0.,
+ eph_sorted_core=0.
+ )
+ ),
+ EphemeralityTestCase(
+ input_sequence=[0.7, .3],
+ threshold=0.3,
+ expected_output=EphemeralitySet(
+ len_left_core=1,
+ len_middle_core=1,
+ len_right_core=1,
+ len_sorted_core=1,
+ eph_left_core=0.,
+ eph_middle_core=0.,
+ eph_right_core=0.,
+ eph_sorted_core=0.
+ )
+ ),
+ EphemeralityTestCase(
+ input_sequence=[1., 0., 0., 0.],
+ threshold=0.8,
+ expected_output=EphemeralitySet(
+ len_left_core=1,
+ len_middle_core=1,
+ len_right_core=4,
+ len_sorted_core=1,
+ eph_left_core=0.6875,
+ eph_middle_core=0.6875,
+ eph_right_core=0.,
+ eph_sorted_core=0.6875
+ )
+ ),
+ EphemeralityTestCase(
+ input_sequence=[1., 0., 0., 0.],
+ threshold=0.3,
+ expected_output=EphemeralitySet(
+ len_left_core=1,
+ len_middle_core=1,
+ len_right_core=4,
+ len_sorted_core=1,
+ eph_left_core=1 / 6,
+ eph_middle_core=1 / 6,
+ eph_right_core=0.,
+ eph_sorted_core=1 / 6
+ )
+ ),
+ EphemeralityTestCase(
+ input_sequence=[0., 0., 0., 1.],
+ threshold=0.8,
+ expected_output=EphemeralitySet(
+ len_left_core=4,
+ len_middle_core=1,
+ len_right_core=1,
+ len_sorted_core=1,
+ eph_left_core=0.,
+ eph_middle_core=0.6875,
+ eph_right_core=0.6875,
+ eph_sorted_core=0.6875
+ )
+ ),
+ EphemeralityTestCase(
+ input_sequence=[0., 0., 0., 1.],
+ threshold=0.3,
+ expected_output=EphemeralitySet(
+ len_left_core=4,
+ len_middle_core=1,
+ len_right_core=1,
+ len_sorted_core=1,
+ eph_left_core=0.,
+ eph_middle_core=1 / 6,
+ eph_right_core=1 / 6,
+ eph_sorted_core=1 / 6
+ )
+ ),
+ EphemeralityTestCase(
+ input_sequence=[0., 1., 0., 1.],
+ threshold=0.8,
+ expected_output=EphemeralitySet(
+ len_left_core=4,
+ len_middle_core=3,
+ len_right_core=3,
+ len_sorted_core=2,
+ eph_left_core=0.,
+ eph_middle_core=0.0625,
+ eph_right_core=0.0625,
+ eph_sorted_core=0.375
+ )
+ ),
+ EphemeralityTestCase(
+ input_sequence=[0., 1., 0., 1.],
+ threshold=0.3,
+ expected_output=EphemeralitySet(
+ len_left_core=2,
+ len_middle_core=1,
+ len_right_core=1,
+ len_sorted_core=1,
+ eph_left_core=0.,
+ eph_middle_core=1 / 6,
+ eph_right_core=1 / 6,
+ eph_sorted_core=1 / 6
+ )
+ ),
+ EphemeralityTestCase(
+ input_sequence=[1., 1., 1., 1.],
+ threshold=0.8,
+ expected_output=EphemeralitySet(
+ len_left_core=4,
+ len_middle_core=4,
+ len_right_core=4,
+ len_sorted_core=4,
+ eph_left_core=0.,
+ eph_middle_core=0.,
+ eph_right_core=0.,
+ eph_sorted_core=0.
+ )
+ ),
+ EphemeralityTestCase(
+ input_sequence=[1., 1., 1., 1.],
+ threshold=0.3,
+ expected_output=EphemeralitySet(
+ len_left_core=2,
+ len_middle_core=2,
+ len_right_core=2,
+ len_sorted_core=2,
+ eph_left_core=0.,
+ eph_middle_core=0.,
+ eph_right_core=0.,
+ eph_sorted_core=0.
+ )
+ ),
+ EphemeralityTestCase(
+ input_sequence=[1., 1., 0., 0.],
+ threshold=0.8,
+ expected_output=EphemeralitySet(
+ len_left_core=2,
+ len_middle_core=2,
+ len_right_core=4,
+ len_sorted_core=2,
+ eph_left_core=0.375,
+ eph_middle_core=0.375,
+ eph_right_core=0.,
+ eph_sorted_core=0.375
+ )
+ ),
+ EphemeralityTestCase(
+ input_sequence=[1., 1., 0., 0.],
+ threshold=0.3,
+ expected_output=EphemeralitySet(
+ len_left_core=1,
+ len_middle_core=1,
+ len_right_core=3,
+ len_sorted_core=1,
+ eph_left_core=1 / 6,
+ eph_middle_core=1 / 6,
+ eph_right_core=0,
+ eph_sorted_core=1 / 6
+ )
+ ),
+ EphemeralityTestCase(
+ input_sequence=[1., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
+ threshold=0.8,
+ expected_output=EphemeralitySet(
+ len_left_core=1,
+ len_middle_core=1,
+ len_right_core=10,
+ len_sorted_core=1,
+ eph_left_core=0.875,
+ eph_middle_core=0.875,
+ eph_right_core=0.,
+ eph_sorted_core=0.875
+ )
+ ),
+ EphemeralityTestCase(
+ input_sequence=[1., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
+ threshold=0.3,
+ expected_output=EphemeralitySet(
+ len_left_core=1,
+ len_middle_core=1,
+ len_right_core=10,
+ len_sorted_core=1,
+ eph_left_core=2 / 3,
+ eph_middle_core=2 / 3,
+ eph_right_core=0.,
+ eph_sorted_core=2 / 3
+ )
+ ),
+ EphemeralityTestCase(
+ input_sequence=[0., 0., 0., 0., 1., 0., 0., 0., 0., 0.],
+ threshold=0.8,
+ expected_output=EphemeralitySet(
+ len_left_core=5,
+ len_middle_core=1,
+ len_right_core=6,
+ len_sorted_core=1,
+ eph_left_core=0.375,
+ eph_middle_core=0.875,
+ eph_right_core=0.25,
+ eph_sorted_core=0.875
+ )
+ ),
+ EphemeralityTestCase(
+ input_sequence=[0., 0., 0., 0., 1., 0., 0., 0., 0., 0.],
+ threshold=0.3,
+ expected_output=EphemeralitySet(
+ len_left_core=5,
+ len_middle_core=1,
+ len_right_core=6,
+ len_sorted_core=1,
+ eph_left_core=0.,
+ eph_middle_core=2 / 3,
+ eph_right_core=0.,
+ eph_sorted_core=2 / 3
+ )
+ ),
+ EphemeralityTestCase(
+ input_sequence=[0., 0., 1., 0., 0., 0., 0., 0., 0., 0.],
+ threshold=0.8,
+ expected_output=EphemeralitySet(
+ len_left_core=3,
+ len_middle_core=1,
+ len_right_core=8,
+ len_sorted_core=1,
+ eph_left_core=0.625,
+ eph_middle_core=0.875,
+ eph_right_core=0.,
+ eph_sorted_core=0.875
+ )
+ ),
+ EphemeralityTestCase(
+ input_sequence=[0., 0., 1., 0., 0., 0., 0., 0., 0., 0.],
+ threshold=0.3,
+ expected_output=EphemeralitySet(
+ len_left_core=3,
+ len_middle_core=1,
+ len_right_core=8,
+ len_sorted_core=1,
+ eph_left_core=0.,
+ eph_middle_core=2 / 3,
+ eph_right_core=0.,
+ eph_sorted_core=2 / 3
+ )
+ ),
+ EphemeralityTestCase(
+ input_sequence=[0., 0., 0., 1., 0., 0., 0., 0., 0., 0.],
+ threshold=0.8,
+ expected_output=EphemeralitySet(
+ len_left_core=4,
+ len_middle_core=1,
+ len_right_core=7,
+ len_sorted_core=1,
+ eph_left_core=0.5,
+ eph_middle_core=0.875,
+ eph_right_core=0.125,
+ eph_sorted_core=0.875
+ )
+ ),
+ EphemeralityTestCase(
+ input_sequence=[0., 0., 0., 1., 0., 0., 0., 0., 0., 0.],
+ threshold=0.3,
+ expected_output=EphemeralitySet(
+ len_left_core=4,
+ len_middle_core=1,
+ len_right_core=7,
+ len_sorted_core=1,
+ eph_left_core=0.,
+ eph_middle_core=2 / 3,
+ eph_right_core=0.,
+ eph_sorted_core=2 / 3
+ )
+ ),
+ EphemeralityTestCase(
+ input_sequence=[0., 0., 0., 0., 0., 0., 0., 1., 0., 0.],
+ threshold=0.8,
+ expected_output=EphemeralitySet(
+ len_left_core=8,
+ len_middle_core=1,
+ len_right_core=3,
+ len_sorted_core=1,
+ eph_left_core=0.,
+ eph_middle_core=0.875,
+ eph_right_core=0.625,
+ eph_sorted_core=0.875
+ )
+ ),
+ EphemeralityTestCase(
+ input_sequence=[0., 0., 0., 0., 0., 0., 0., 1., 0., 0.],
+ threshold=0.3,
+ expected_output=EphemeralitySet(
+ len_left_core=8,
+ len_middle_core=1,
+ len_right_core=3,
+ len_sorted_core=1,
+ eph_left_core=0.,
+ eph_middle_core=2 / 3,
+ eph_right_core=0.,
+ eph_sorted_core=2 / 3
+ )
+ ),
+ EphemeralityTestCase(
+ input_sequence=[0., 0., 0., 0., 0., 0., 0., 0., 1., 0.],
+ threshold=0.8,
+ expected_output=EphemeralitySet(
+ len_left_core=9,
+ len_middle_core=1,
+ len_right_core=2,
+ len_sorted_core=1,
+ eph_left_core=0.,
+ eph_middle_core=0.875,
+ eph_right_core=0.75,
+ eph_sorted_core=0.875
+ )
+ ),
+ EphemeralityTestCase(
+ input_sequence=[0., 0., 0., 0., 0., 0., 0., 0., 1., 0.],
+ threshold=0.3,
+ expected_output=EphemeralitySet(
+ len_left_core=9,
+ len_middle_core=1,
+ len_right_core=2,
+ len_sorted_core=1,
+ eph_left_core=0.,
+ eph_middle_core=2 / 3,
+ eph_right_core=1 / 3,
+ eph_sorted_core=2 / 3
+ )
+ ),
+ EphemeralityTestCase(
+ input_sequence=[0., 0., 0., 0., 0., 0., 0., 0., 0., 1.],
+ threshold=0.8,
+ expected_output=EphemeralitySet(
+ len_left_core=10,
+ len_middle_core=1,
+ len_right_core=1,
+ len_sorted_core=1,
+ eph_left_core=0.,
+ eph_middle_core=0.875,
+ eph_right_core=0.875,
+ eph_sorted_core=0.875
+ )
+ ),
+ EphemeralityTestCase(
+ input_sequence=[0., 0., 0., 0., 0., 0., 0., 0., 0., 1.],
+ threshold=0.3,
+ expected_output=EphemeralitySet(
+ len_left_core=10,
+ len_middle_core=1,
+ len_right_core=1,
+ len_sorted_core=1,
+ eph_left_core=0.,
+ eph_middle_core=2 / 3,
+ eph_right_core=2 / 3,
+ eph_sorted_core=2 / 3
+ )
+ ),
+ EphemeralityTestCase(
+ input_sequence=[.1, .1, .1, .1, .1, .1, .1, .1, .1, .1],
+ threshold=0.8,
+ expected_output=EphemeralitySet(
+ len_left_core=8,
+ len_middle_core=8,
+ len_right_core=8,
+ len_sorted_core=8,
+ eph_left_core=0.,
+ eph_middle_core=0.,
+ eph_right_core=0.,
+ eph_sorted_core=0.
+ )
+ ),
+ EphemeralityTestCase(
+ input_sequence=[.1, .1, .1, .1, .1, .1, .1, .1, .1, .1],
+ threshold=0.3,
+ expected_output=EphemeralitySet(
+ len_left_core=3,
+ len_middle_core=3,
+ len_right_core=3,
+ len_sorted_core=3,
+ eph_left_core=0.,
+ eph_middle_core=0.,
+ eph_right_core=0.,
+ eph_sorted_core=0.
+ )
+ ),
+ EphemeralityTestCase(
+ input_sequence=[0., 0., 0., .2, .55, 0., .15, .1, 0., 0.],
+ threshold=0.8,
+ expected_output=EphemeralitySet(
+ len_left_core=7,
+ len_middle_core=4,
+ len_right_core=6,
+ len_sorted_core=3,
+ eph_left_core=0.125,
+ eph_middle_core=0.5,
+ eph_right_core=0.25,
+ eph_sorted_core=0.625
+ )
+ ),
+ EphemeralityTestCase(
+ input_sequence=[0., 0., 0., .2, .55, 0., .15, .1, 0., 0.],
+ threshold=0.3,
+ expected_output=EphemeralitySet(
+ len_left_core=5,
+ len_middle_core=1,
+ len_right_core=6,
+ len_sorted_core=1,
+ eph_left_core=0.,
+ eph_middle_core=2 / 3,
+ eph_right_core=0.,
+ eph_sorted_core=2 / 3
+ )
+ ),
+ EphemeralityTestCase(
+ input_sequence=np.eye(1, 10000, k=5000).flatten(),
+ threshold=0.8,
+ expected_output=EphemeralitySet(
+ len_left_core=5001,
+ len_middle_core=1,
+ len_right_core=5000,
+ len_sorted_core=1,
+ eph_left_core=0.374875,
+ eph_middle_core=0.999875,
+ eph_right_core=0.375,
+ eph_sorted_core=0.999875
+ )
+ ),
+ EphemeralityTestCase(
+ input_sequence=np.eye(1, 10000, k=5000).flatten(),
+ threshold=0.3,
+ expected_output=EphemeralitySet(
+ len_left_core=5001,
+ len_middle_core=1,
+ len_right_core=5000,
+ len_sorted_core=1,
+ eph_left_core=0.,
+ eph_middle_core=2999 / 3000,
+ eph_right_core=0.,
+ eph_sorted_core=2999 / 3000
+ )
+ ),
+ EphemeralityTestCase(
+ input_sequence=np.ones((10000,)),
+ threshold=0.8,
+ expected_output=EphemeralitySet(
+ len_left_core=8000,
+ len_middle_core=8000,
+ len_right_core=8000,
+ len_sorted_core=8000,
+ eph_left_core=0.,
+ eph_middle_core=0.,
+ eph_right_core=0.,
+ eph_sorted_core=0.
+ )
+ ),
+ EphemeralityTestCase(
+ input_sequence=np.ones((10000,)),
+ threshold=0.3,
+ expected_output=EphemeralitySet(
+ len_left_core=3000,
+ len_middle_core=3000,
+ len_right_core=3000,
+ len_sorted_core=3000,
+ eph_left_core=0.,
+ eph_middle_core=0.,
+ eph_right_core=0.,
+ eph_sorted_core=0.
+ )
+ ),
+ EphemeralityTestCase(
+ input_sequence=np.pad(np.zeros((9996,)), pad_width=(2, 2), constant_values=(1., 1.)),
+ threshold=0.8,
+ expected_output=EphemeralitySet(
+ len_left_core=10000,
+ len_middle_core=10000,
+ len_right_core=10000,
+ len_sorted_core=4,
+ eph_left_core=0.,
+ eph_middle_core=0.,
+ eph_right_core=0.,
+ eph_sorted_core=0.9995
+ )
+ ),
+ EphemeralityTestCase(
+ input_sequence=np.pad(np.zeros((9996,)), pad_width=(2, 2), constant_values=(1., 1.)),
+ threshold=0.3,
+ expected_output=EphemeralitySet(
+ len_left_core=2,
+ len_middle_core=9998,
+ len_right_core=2,
+ len_sorted_core=2,
+ eph_left_core=1499 / 1500,
+ eph_middle_core=0.,
+ eph_right_core=1499 / 1500,
+ eph_sorted_core=1499 / 1500
+ )
+ ),
+ EphemeralityTestCase(
+ input_sequence=np.pad(np.eye(1, 9999, k=4999).flatten(), pad_width=(1, 1), constant_values=(1., 1.)),
+ threshold=0.8,
+ expected_output=EphemeralitySet(
+ len_left_core=10001,
+ len_middle_core=10001,
+ len_right_core=10001,
+ len_sorted_core=3,
+ eph_left_core=0.,
+ eph_middle_core=0.,
+ eph_right_core=0.,
+ eph_sorted_core=39989 / 40004
+ )
+ ),
+ EphemeralityTestCase(
+ input_sequence=np.pad(np.eye(1, 9999, k=4999).flatten(), pad_width=(1, 1), constant_values=(1., 1.)),
+ threshold=0.3,
+ expected_output=EphemeralitySet(
+ len_left_core=1,
+ len_middle_core=1,
+ len_right_core=1,
+ len_sorted_core=1,
+ eph_left_core=29993 / 30003,
+ eph_middle_core=29993 / 30003,
+ eph_right_core=29993 / 30003,
+ eph_sorted_core=29993 / 30003
+ )
+ )
+]
+
+
+class PartialCoresIterator:
+ _core_types = ('l', 'm', 'r', 's')
+
+ def __init__(self, expected_output_full: EphemeralitySet) -> None:
+ self._expected_output_full = expected_output_full
+ self._chain = None
+
+ def __iter__(self) -> Self:
+ self._chain = chain.from_iterable(combinations(self._core_types, r) for r in range(1, len(self._core_types) + 1))
+ return self
+
+ def __next__(self) -> tuple[str, EphemeralitySet]:
+ subset = next(self._chain)
+ return ''.join(subset), self._eph_subset(subset)
+
+ def _eph_subset(self, types: Iterable[str]) -> EphemeralitySet:
+ eph_set = EphemeralitySet()
+ for core_type in types:
+ match core_type:
+ case 'l':
+ eph_set.len_left_core = self._expected_output_full.len_left_core
+ eph_set.eph_left_core = self._expected_output_full.eph_left_core
+ case 'm':
+ eph_set.len_middle_core = self._expected_output_full.len_middle_core
+ eph_set.eph_middle_core = self._expected_output_full.eph_middle_core
+ case 'r':
+ eph_set.len_right_core = self._expected_output_full.len_right_core
+ eph_set.eph_right_core = self._expected_output_full.eph_right_core
+ case 's':
+ eph_set.len_sorted_core = self._expected_output_full.len_sorted_core
+ eph_set.eph_sorted_core = self._expected_output_full.eph_sorted_core
+ case _:
+ raise ValueError('Invalid core type!')
+ return eph_set
+
+ def __len__(self):
+ return len(self._chain)
+
+
+class TestComputeEphemerality(TestCase):
+ test_cases: Sequence[EphemeralityTestCase] = DEFAULT_TEST_CASES
+
+ def test_compute_ephemeralities(self):
+ print('IN-SCRIPT-BASED TEST')
+ self.run_test('python')
+
+ def test_cmd(self):
+ print('CMD-BASED TEST')
+ self.run_test('cmd')
+
+ def test_api(self):
+ print('API-BASED TEST')
+ rest_server = subprocess.Popen(
+ [f'{sys.executable}', '-m', 'ephemerality', 'api'], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT
+ )
+ time.sleep(5)
+ self.run_test('api')
+ rest_server.terminate()
+ rest_server.wait()
+
+ def run_test(self, run_type: Literal['python', 'cmd', 'api'] = 'python'):
+ for i, test_case in enumerate(self.test_cases):
+ if run_type == 'cmd' and len(test_case.input_sequence) > 100:
+ continue
+ with self.subTest():
+ print(f'Running test case {i}: {test_case.input_sequence}, threshold {test_case.threshold}...')
+
+ partial_output_iterator = PartialCoresIterator(test_case.expected_output)
+ for core_types, expected_output in partial_output_iterator:
+ print(f'\tCore subset \"{core_types}\"...')
+
+ match run_type:
+ case 'cmd':
+ actual_output = subprocess.check_output([
+ f'{sys.executable}', '-m', 'ephemerality', 'cmd', # f'{Path(root_dir) / "ephemerality"}'
+ " ".join([str(freq) for freq in test_case.input_sequence]),
+ '-t', str(test_case.threshold),
+ '-c', core_types
+ ]).decode('utf-8')
+ actual_output = EphemeralitySet(**json.loads(actual_output))
+ case 'python':
+ actual_output = compute_ephemerality(activity_vector=test_case.input_sequence,
+ threshold=test_case.threshold,
+ types=core_types)
+ case 'api':
+ request_data = InputData(input_sequence=test_case.input_sequence, threshold=test_case.threshold)
+ response = requests.get(f'http://127.0.0.1:8080/ephemerality/all?core_types={core_types}', json=[request_data.model_dump()])
+ actual_output = EphemeralitySet(**json.loads(response.content)[0]['output'])
+
+ try:
+ self.assertEqual(expected_output, actual_output)
+ except AssertionError as ex:
+ print(f"\tAssertion error while processing test case {i}.{core_types}: {test_case.input_sequence}, "
+ f"threshold {test_case.threshold}...")
+ print(f"\t\tExpected output: {test_case.expected_output}\n\t\tActual output: {actual_output}")
+ raise ex
+
+
+def test_ephemerality(test_cases: list[EphemeralityTestCase] | None = None) -> None:
+ if test_cases is None:
+ test_cases = DEFAULT_TEST_CASES
+ test = TestComputeEphemerality('test_compute_ephemeralities')
+ test.test_cases = test_cases
+
+ runner = TextTestRunner()
+ runner.run(test)
diff --git a/testing/src/test_utils.py b/testing/src/test_utils.py
new file mode 100644
index 0000000..4d5b00f
--- /dev/null
+++ b/testing/src/test_utils.py
@@ -0,0 +1,11 @@
+from typing import Sequence
+from dataclasses import dataclass
+
+from ephemerality import EphemeralitySet
+
+
+@dataclass
+class EphemeralityTestCase:
+ input_sequence: Sequence[float]
+ threshold: float
+ expected_output: EphemeralitySet