From f4402ae76bdab12649d80726195449bcb8d92f7e Mon Sep 17 00:00:00 2001 From: LeaveMyYard Date: Mon, 23 Oct 2023 11:25:39 +0300 Subject: [PATCH 001/137] Add --width parameter for the console --- robusta_krr/core/models/config.py | 19 ++++++++++++++++--- robusta_krr/main.py | 4 ++++ robusta_krr/utils/print.py | 11 ++--------- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/robusta_krr/core/models/config.py b/robusta_krr/core/models/config.py index 3d6aabc0..5a26c490 100644 --- a/robusta_krr/core/models/config.py +++ b/robusta_krr/core/models/config.py @@ -53,6 +53,7 @@ class Config(pd.BaseSettings): format: str strategy: str log_to_stderr: bool + width: Optional[int] = pd.Field(None, ge=1) # Outputs Settings file_output: Optional[str] = pd.Field(None) @@ -62,11 +63,11 @@ class Config(pd.BaseSettings): # Internal inside_cluster: bool = False - console: Optional[Console] = None + _logging_console: Optional[Console] = pd.PrivateAttr(None) + _result_console: Optional[Console] = pd.PrivateAttr(None) def __init__(self, **kwargs: Any) -> None: super().__init__(**kwargs) - self.console = Console(stderr=self.log_to_stderr) @property def Formatter(self) -> formatters.FormatterFunc: @@ -112,6 +113,18 @@ def validate_format(cls, v: str) -> str: def context(self) -> Optional[str]: return self.clusters[0] if self.clusters != "*" and self.clusters else None + @property + def logging_console(self) -> Console: + if getattr(self, "_logging_console") is None: + self._logging_console = Console(file=sys.stderr if self.log_to_stderr else sys.stdout, width=self.width) + return self._logging_console + + @property + def result_console(self) -> Console: + if getattr(self, "_result_console") is None: + self._result_console = Console(file=sys.stdout, width=self.width) + return self._result_console + def load_kubeconfig(self) -> None: try: config.load_incluster_config() @@ -130,7 +143,7 @@ def set_config(config: Config) -> None: level="NOTSET", format="%(message)s", datefmt="[%X]", - handlers=[RichHandler(console=Console(file=sys.stderr if settings.log_to_stderr else sys.stdout))], + handlers=[RichHandler(console=config.logging_console)], ) logging.getLogger("").setLevel(logging.CRITICAL) logger.setLevel(logging.DEBUG if config.verbose else logging.CRITICAL if config.quiet else logging.INFO) diff --git a/robusta_krr/main.py b/robusta_krr/main.py index 9710c47a..8c19f1bf 100644 --- a/robusta_krr/main.py +++ b/robusta_krr/main.py @@ -205,6 +205,9 @@ def run_strategy( log_to_stderr: bool = typer.Option( False, "--logtostderr", help="Pass logs to stderr", rich_help_panel="Logging Settings" ), + width: Optional[int] = typer.Option( + None, "--width", help="Width of the output. Will use console width by default.", rich_help_panel="Logging Settings" + ), file_output: Optional[str] = typer.Option( None, "--fileoutput", help="Print the output to a file", rich_help_panel="Output Settings" ), @@ -245,6 +248,7 @@ def run_strategy( memory_min_value=memory_min_value, quiet=quiet, log_to_stderr=log_to_stderr, + width=width, file_output=file_output, slack_output=slack_output, strategy=_strategy_name, diff --git a/robusta_krr/utils/print.py b/robusta_krr/utils/print.py index 1963c3e0..093cc445 100644 --- a/robusta_krr/utils/print.py +++ b/robusta_krr/utils/print.py @@ -1,21 +1,14 @@ -import sys - -from rich import print as r_print - from robusta_krr.core.models.config import settings -py_print = print - def print(*objects, rich: bool = True, force: bool = False) -> None: """ A wrapper around `rich.print` that prints only if `settings.quiet` is False. """ - print_func = r_print if rich else py_print - output = sys.stdout if force or not settings.log_to_stderr else sys.stderr + print_func = settings.logging_console.print if rich else print if not settings.quiet or force: - print_func(*objects, file=output) # type: ignore + print_func(*objects) # type: ignore __all__ = ["print"] From 8ee297b76d832d65150c638ea294b7797e8ba267 Mon Sep 17 00:00:00 2001 From: LeaveMyYard Date: Tue, 5 Dec 2023 10:37:06 +0200 Subject: [PATCH 002/137] Remove output verification in tests --- tests/test_krr.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/tests/test_krr.py b/tests/test_krr.py index 2a2fb947..fe441769 100644 --- a/tests/test_krr.py +++ b/tests/test_krr.py @@ -1,7 +1,4 @@ -import json - import pytest -import yaml from typer.testing import CliRunner from robusta_krr.main import app, load_commands @@ -37,14 +34,3 @@ def test_output_formats(format: str, output: str): assert result.exit_code == 0, result.exc_info except AssertionError as e: raise e from result.exception - - try: - if format == "json": - json_output = json.loads(result.stdout) - assert json_output, result.stdout - assert len(json_output["scans"]) > 0, result.stdout - - if format == "yaml": - assert yaml.safe_load(result.stdout), result.stdout - except Exception as e: - raise Exception(result.stdout) from e From 7527ad6d22e27be3afde934d6a2859735d4599d4 Mon Sep 17 00:00:00 2001 From: Chico Venancio Date: Fri, 8 Dec 2023 11:26:10 -0300 Subject: [PATCH 003/137] feat(VictoriaMetricsDiscovery): set correct url for cluster version --- .../metrics_service/victoria_metrics_service.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/robusta_krr/core/integrations/prometheus/metrics_service/victoria_metrics_service.py b/robusta_krr/core/integrations/prometheus/metrics_service/victoria_metrics_service.py index a0f10100..12aaace6 100644 --- a/robusta_krr/core/integrations/prometheus/metrics_service/victoria_metrics_service.py +++ b/robusta_krr/core/integrations/prometheus/metrics_service/victoria_metrics_service.py @@ -17,14 +17,21 @@ def find_metrics_url(self, *, api_client: Optional[ApiClient] = None) -> Optiona Returns: Optional[str]: The discovered Victoria Metrics URL, or None if not found. """ - return super().find_url( + url = super().find_url( selectors=[ "app.kubernetes.io/name=vmsingle", "app.kubernetes.io/name=victoria-metrics-single", - "app.kubernetes.io/name=vmselect", - "app=vmselect", ] ) + if url is None: + url = super().find_url( + selectors=[ + "app.kubernetes.io/name=vmselect", + "app=vmselect", + ] + ) + url = f"{url}/select/0/prometheus/" + return url class VictoriaMetricsService(PrometheusMetricsService): From e032745f670f1923722571f1be041de7e8b63e5e Mon Sep 17 00:00:00 2001 From: Natan Yellin Date: Sun, 10 Dec 2023 21:51:00 +0200 Subject: [PATCH 004/137] Update README.md --- README.md | 202 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 121 insertions(+), 81 deletions(-) diff --git a/README.md b/README.md index 7a181977..bb7335d8 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ . Usage Β· - How it works + How KRR works . Slack Integration
@@ -62,17 +62,18 @@ Robusta KRR (Kubernetes Resource Recommender) is a CLI tool for optimizing resource allocation in Kubernetes clusters. It gathers pod usage data from Prometheus and recommends requests and limits for CPU and memory. This reduces costs and improves performance. -_Supports: [Prometheus](#prometheus-victoria-metrics-and-thanos-auto-discovery), [Thanos](#prometheus-victoria-metrics-and-thanos-auto-discovery), [Victoria Metrics](#prometheus-victoria-metrics-and-thanos-auto-discovery), [Amazon Managed Prometheus](#amazon-managed-prometheus), [Azure](#azure-managed-prometheus), [Coralogix](#coralogix-managed-prometheus) and [Grafana Cloud](#grafana-cloud-managed-prometheus)_ +_Supports: [Prometheus](#prometheus-victoria-metrics-and-thanos-auto-discovery), [Thanos](#prometheus-victoria-metrics-and-thanos-auto-discovery), [Victoria Metrics](#prometheus-victoria-metrics-and-thanos-auto-discovery), [Google Managed Prometheus](./docs/google-cloud-managed-service-for-prometheus.md), [Amazon Managed Prometheus](#amazon-managed-prometheus), [Azure Managed Prometheus](#azure-managed-prometheus), [Coralogix](#coralogix-managed-prometheus) and [Grafana Cloud](#grafana-cloud-managed-prometheus)_ ### Features - **No Agent Required**: Run a CLI tool on your local machine for immediate results. (Or run in-cluster for weekly [Slack reports](#slack-integration).) - **Prometheus Integration**: Get recommendations based on the data you already have +- **Explainability**: Understand how recommendations were calculated - **Extensible Strategies**: Easily create and use your own strategies for calculating resource recommendations. - **Free SaaS Platform**: See why KRR recommends what it does, by using the [free Robusta SaaS platform](https://home.robusta.dev/). - **Future Support**: Upcoming versions will support custom resources (e.g. GPUs) and custom metrics. -### Resource Allocation Statistics +### Why Use KRR? According to a recent [Sysdig study](https://sysdig.com/blog/millions-wasted-kubernetes/), on average, Kubernetes clusters have: @@ -101,7 +102,10 @@ Additionally to that, [kube-state-metrics](https://github.com/kubernetes/kube-st _Note: If one of last three metrics is absent KRR will still work, but it will only consider currently-running pods when calculating recommendations. Historic pods that no longer exist in the cluster will not be taken into consideration._ -### With brew (MacOS/Linux): +### Installation Methods + +
+ Brew (Mac/Linux) 1. Add our tap: @@ -120,12 +124,16 @@ brew install krr ```sh krr --help ``` +
-### On Windows: +
+ Windows -You can install using brew (see above) on [WSL2](https://docs.brew.sh/Homebrew-on-Linux), or install manually. +You can install using brew (see above) on [WSL2](https://docs.brew.sh/Homebrew-on-Linux), or install from source (see below). +
-### Manual Installation +
+ From Source 1. Make sure you have [Python 3.9](https://www.python.org/downloads/) (or greater) installed 2. Clone the repo: @@ -150,37 +158,73 @@ python krr.py --help Notice that using source code requires you to run as a python script, when installing with brew allows to run `krr`. All above examples show running command as `krr ...`, replace it with `python krr.py ...` if you are using a manual installation. -

(back to top)

+
-### Other Configuration Methods +### Additional Options - [View KRR Reports in a Web UI](#optional-free-saas-platform) -- [Get a Weekly Message in Slack with KRR Recommendations](#slack-integration) -- Setup KRR on [Google Cloud Managed Prometheus - ](./docs/google-cloud-managed-service-for-prometheus.md) -- Setup KRR for [Azure managed Prometheus](#azure-managed-prometheus) +- [Receive KRR Reports Weekly in Slack](#slack-integration) + +### Environment-Specific Instructions +Setup KRR for... +- [Google Cloud Managed Prometheus](./docs/google-cloud-managed-service-for-prometheus.md) +- [Azure Managed Prometheus](#azure-managed-prometheus) +- [Amazon Managed Prometheus](#amazon-managed-prometheus) +- [Coralogix Managed Prometheus](#coralogix-managed-prometheus) +- [Grafana Cloud Managed Prometheus](#grafana-cloud-managed-prometheus) + +

(back to top)

## Usage -Straightforward usage, to run the simple strategy: - +
+ Basic usage + ```sh krr simple ``` +
+ +
+ Tweak the recommendation algorithm + +Most helpful flags: -If you want only specific namespaces (default and ingress-nginx): +- `--cpu-min` Sets the minimum recommended cpu value in millicores +- `--mem-min` Sets the minimum recommended memory value in MB +- `--history_duration` The duration of the prometheus history data to use (in hours) + +More specific information on Strategy Settings can be found using + +```sh +krr simple --help +``` +
+ +
+ Run on specific namespaces + +List as many namespaces as you want with `-n` (in this case, `default` and `ingress-nginx`) ```sh krr simple -n default -n ingress-nginx ``` +
-Filtering by labels (more info [here](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api)): +
+ Run on workloads filtered by label + +Use a label selector ```sh python krr.py simple --selector 'app.kubernetes.io/instance in (robusta, ingress-nginx)' ``` +
+ +
+ Override the kubectl context By default krr will run in the current context. If you want to run it in a different context: @@ -188,36 +232,80 @@ By default krr will run in the current context. If you want to run it in a diffe krr simple -c my-cluster-1 -c my-cluster-2 ``` -If you want to get the output in JSON format (--logtostderr is required so no logs go to the result file): +
+ +
+ Customize output (JSON, YAML, and more + +Currently KRR ships with a few formatters to represent the scan data: + +- `table` - a pretty CLI table used by default, powered by [Rich](https://github.com/Textualize/rich) library +- `json` +- `yaml` +- `pprint` - data representation from python's pprint library + +To run a strategy with a selected formatter, add a `-f` flag: + +```sh +krr simple -f json +``` + +For JSON output, add --logtostderr so no logs go to the result file: ```sh krr simple --logtostderr -f json > result.json ``` -If you want to get the output in YAML format: +For YAML output, do the same: ```sh krr simple --logtostderr -f yaml > result.yaml ``` +
-If you want to see additional debug logs: +
+ Centralized Prometheus (multi-cluster) + +If your Prometheus monitors multiple clusters we require the label you defined for your cluster in Prometheus. + +For example, if your cluster has the Prometheus label `cluster: "my-cluster-name"`, then run this command: ```sh -krr simple -v +krr.py simple --prometheus-label cluster -l my-cluster-name ``` -Other helpful flags: +You may also need the `-p` flag to explicitly give Prometheus' URL. -- `--cpu-min` Sets the minimum recommended cpu value in millicores -- `--mem-min` Sets the minimum recommended memory value in MB -- `--history_duration` The duration of the prometheus history data to use (in hours) -More specific information on Strategy Settings can be found using +
+ Giving an Explicit Prometheus URL + +If your prometheus is not auto-connecting, you can use `kubectl port-forward` for manually forwarding Prometheus. + +For example, if you have a Prometheus Pod called `kube-prometheus-st-prometheus-0`, then run this command to port-forward it: ```sh -krr simple --help +kubectl port-forward pod/kube-prometheus-st-prometheus-0 9090 +``` + +Then, open another terminal and run krr in it, giving an explicit prometheus url: + +```sh +krr simple -p http://127.0.0.1:9090 +``` +
+ + +
+ Debug mode +If you want to see additional debug logs: + +```sh +krr simple -v ``` +
+

(back to top)

## Optional: Free SaaS Platform @@ -232,7 +320,7 @@ With the [free Robusta SaaS platform](https://home.robusta.dev/) you can:

(back to top)

-## How it works +## How KRR works ### Metrics Gathering @@ -278,9 +366,10 @@ Find about how KRR tries to find the default prometheus to connect (back to top)

-## Example of using port-forward for Prometheus - -If your prometheus is not auto-connecting, you can use `kubectl port-forward` for manually forwarding Prometheus. - -For example, if you have a Prometheus Pod called `kube-prometheus-st-prometheus-0`, then run this command to port-forward it: - -```sh -kubectl port-forward pod/kube-prometheus-st-prometheus-0 9090 -``` - -Then, open another terminal and run krr in it, giving an explicit prometheus url: - -```sh -krr simple -p http://127.0.0.1:9090 -``` - -

(back to top)

- -## Scanning with a centralized Prometheus - -If your Prometheus monitors multiple clusters we require the label you defined for your cluster in Prometheus. - -For example, if your cluster has the Prometheus label `cluster: "my-cluster-name"` and your prometheus is at url `http://my-centralized-prometheus:9090`, then run this command: - -```sh -krr.py simple -p http://my-centralized-prometheus:9090 --prometheus-label cluster -l my-cluster-name -``` - -

(back to top)

- -## Azure managed Prometheus +## Azure Managed Prometheus For Azure managed Prometheus you need to generate an access token, which can be done by running the following command: @@ -431,7 +490,7 @@ Additional optional parameters are:

(back to top)

-## Coralogix managed Prometheus +## Coralogix Managed Prometheus For Coralogix managed Prometheus you need to specify your prometheus link and add the flag coralogix_token with your Logs Query Key @@ -443,7 +502,7 @@ python krr.py simple -p "https://prom-api.coralogix..." --coralogix_token

(back to top)

-## Grafana Cloud managed Prometheus +## Grafana Cloud Managed Prometheus For Grafana Cloud managed Prometheus you need to specify prometheus link, prometheus user, and an access token of your Grafana Cloud stack. The Prometheus link and user for the stack can be found on the Grafana Cloud Portal. An access token with a `metrics:read` scope can also be created using Access Policies on the same portal. @@ -457,25 +516,6 @@ python krr.py simple -p $PROM_URL --prometheus-auth-header "Bearer ${PROM_USER}:

(back to top)

- - -## Available formatters - -Currently KRR ships with a few formatters to represent the scan data: - -- `table` - a pretty CLI table used by default, powered by [Rich](https://github.com/Textualize/rich) library -- `json` -- `yaml` -- `pprint` - data representation from python's pprint library - -To run a strategy with a selected formatter, add a `-f` flag: - -```sh -krr simple -f json -``` - -

(back to top)

- ## Creating a Custom Strategy/Formatter From 6f3e2e14bfc8161369baeb5323bd92295bbf4eca Mon Sep 17 00:00:00 2001 From: Natan Yellin Date: Sun, 10 Dec 2023 21:52:12 +0200 Subject: [PATCH 005/137] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bb7335d8..9a98ae5f 100644 --- a/README.md +++ b/README.md @@ -275,7 +275,7 @@ krr.py simple --prometheus-label cluster -l my-cluster-name ``` You may also need the `-p` flag to explicitly give Prometheus' URL. - +
Giving an Explicit Prometheus URL From 0435a3e43b453437572ac00703ced816b3dce6eb Mon Sep 17 00:00:00 2001 From: Natan Yellin Date: Sun, 10 Dec 2023 21:53:09 +0200 Subject: [PATCH 006/137] Update README.md --- README.md | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 9a98ae5f..02fecaf7 100644 --- a/README.md +++ b/README.md @@ -203,6 +203,24 @@ krr simple --help ```
+
+ Giving an Explicit Prometheus URL + +If your prometheus is not auto-connecting, you can use `kubectl port-forward` for manually forwarding Prometheus. + +For example, if you have a Prometheus Pod called `kube-prometheus-st-prometheus-0`, then run this command to port-forward it: + +```sh +kubectl port-forward pod/kube-prometheus-st-prometheus-0 9090 +``` + +Then, open another terminal and run krr in it, giving an explicit prometheus url: + +```sh +krr simple -p http://127.0.0.1:9090 +``` +
+
Run on specific namespaces @@ -277,25 +295,6 @@ krr.py simple --prometheus-label cluster -l my-cluster-name You may also need the `-p` flag to explicitly give Prometheus' URL.
-
- Giving an Explicit Prometheus URL - -If your prometheus is not auto-connecting, you can use `kubectl port-forward` for manually forwarding Prometheus. - -For example, if you have a Prometheus Pod called `kube-prometheus-st-prometheus-0`, then run this command to port-forward it: - -```sh -kubectl port-forward pod/kube-prometheus-st-prometheus-0 9090 -``` - -Then, open another terminal and run krr in it, giving an explicit prometheus url: - -```sh -krr simple -p http://127.0.0.1:9090 -``` -
- -
Debug mode If you want to see additional debug logs: From 38cbd4b8112322f674d02f5344a8067d551a3ed7 Mon Sep 17 00:00:00 2001 From: Natan Yellin Date: Sun, 10 Dec 2023 21:54:32 +0200 Subject: [PATCH 007/137] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 02fecaf7..1508494e 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ According to a recent [Sysdig study](https://sysdig.com/blog/millions-wasted-kub By right-sizing your containers with KRR, you can save an average of 69% on cloud costs. -Read more about [how KRR works](#how-it-works) and [KRR vs Kubernetes VPA](#difference-with-kubernetes-vpa) +Read more about [how KRR works](#how-krr-works) and [KRR vs Kubernetes VPA](#difference-with-kubernetes-vpa) From 618effeb3e3147a4e63dd6eeecbaf146a6ae5ad9 Mon Sep 17 00:00:00 2001 From: Natan Yellin Date: Sun, 10 Dec 2023 21:57:25 +0200 Subject: [PATCH 008/137] Update README.md --- README.md | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 1508494e..f625aacd 100644 --- a/README.md +++ b/README.md @@ -283,16 +283,8 @@ krr simple --logtostderr -f yaml > result.yaml
Centralized Prometheus (multi-cluster) +

See below on filtering output from a centralized prometheus, so it matches only one cluster

-If your Prometheus monitors multiple clusters we require the label you defined for your cluster in Prometheus. - -For example, if your cluster has the Prometheus label `cluster: "my-cluster-name"`, then run this command: - -```sh -krr.py simple --prometheus-label cluster -l my-cluster-name -``` - -You may also need the `-p` flag to explicitly give Prometheus' URL.
@@ -447,6 +439,17 @@ If none of those labels result in finding Prometheus, Victoria Metrics or Thanos

(back to top)

+## Scanning with a Centralized Prometheus +If your Prometheus monitors multiple clusters we require the label you defined for your cluster in Prometheus. + +For example, if your cluster has the Prometheus label `cluster: "my-cluster-name"`, then run this command: + +```sh +krr.py simple --prometheus-label cluster -l my-cluster-name +``` + +You may also need the `-p` flag to explicitly give Prometheus' URL. + ## Azure Managed Prometheus For Azure managed Prometheus you need to generate an access token, which can be done by running the following command: From f95e259ed01bf098424cdd44652e98b96db27985 Mon Sep 17 00:00:00 2001 From: Alexis Boissiere Date: Fri, 5 Jan 2024 17:52:43 +0100 Subject: [PATCH 009/137] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f625aacd..5bf0de51 100644 --- a/README.md +++ b/README.md @@ -339,7 +339,7 @@ By default, we use a _simple_ strategy to calculate resource recommendations. It - For CPU, we set a request at the 99th percentile with no limit. Meaning, in 99% of the cases, your CPU request will be sufficient. For the remaining 1%, we set no limit. This means your pod can burst and use any CPU available on the node - e.g. CPU that other pods requested but aren’t using right now. -- For memory, we take the maximum value over the past week and add a 5% buffer. +- For memory, we take the maximum value over the past week and add a 15% buffer. ### Prometheus connection From 2f03a7178d130e0ccbc742fad7003424115fee8b Mon Sep 17 00:00:00 2001 From: Pavan Gudiwada <25551553+pavangudiwada@users.noreply.github.com> Date: Wed, 10 Jan 2024 14:58:48 +0530 Subject: [PATCH 010/137] First draft changes --- README.md | 166 +++++++++++++++++++----------- images/krr-datasources.png | Bin 0 -> 42120 bytes images/krr-datasources.svg | 72 +++++++++++++ images/krr-other-integrations.png | Bin 0 -> 29457 bytes images/krr-other-integrations.svg | 37 +++++++ 5 files changed, 213 insertions(+), 62 deletions(-) create mode 100644 images/krr-datasources.png create mode 100644 images/krr-datasources.svg create mode 100644 images/krr-other-integrations.png create mode 100644 images/krr-other-integrations.svg diff --git a/README.md b/README.md index f625aacd..03366ad8 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,15 @@ Robusta KRR (Kubernetes Resource Recommender) is a CLI tool for optimizing resou _Supports: [Prometheus](#prometheus-victoria-metrics-and-thanos-auto-discovery), [Thanos](#prometheus-victoria-metrics-and-thanos-auto-discovery), [Victoria Metrics](#prometheus-victoria-metrics-and-thanos-auto-discovery), [Google Managed Prometheus](./docs/google-cloud-managed-service-for-prometheus.md), [Amazon Managed Prometheus](#amazon-managed-prometheus), [Azure Managed Prometheus](#azure-managed-prometheus), [Coralogix](#coralogix-managed-prometheus) and [Grafana Cloud](#grafana-cloud-managed-prometheus)_ +### Data Integrations + +[![Used to send data to KRR](./images/krr-datasources.svg)](#data-source-integrations) + +### Integrations + +[![Used to receive information from KRR](./images/krr-other-integrations.svg)](#data-source-integrations) + + ### Features - **No Agent Required**: Run a CLI tool on your local machine for immediate results. (Or run in-cluster for weekly [Slack reports](#slack-integration).) @@ -194,7 +203,7 @@ Most helpful flags: - `--cpu-min` Sets the minimum recommended cpu value in millicores - `--mem-min` Sets the minimum recommended memory value in MB -- `--history_duration` The duration of the prometheus history data to use (in hours) +- `--history_duration` The duration of the Prometheus history data to use (in hours) More specific information on Strategy Settings can be found using @@ -206,7 +215,7 @@ krr simple --help
Giving an Explicit Prometheus URL -If your prometheus is not auto-connecting, you can use `kubectl port-forward` for manually forwarding Prometheus. +If your Prometheus is not auto-connecting, you can use `kubectl port-forward` for manually forwarding Prometheus. For example, if you have a Prometheus Pod called `kube-prometheus-st-prometheus-0`, then run this command to port-forward it: @@ -214,7 +223,7 @@ For example, if you have a Prometheus Pod called `kube-prometheus-st-prometheus- kubectl port-forward pod/kube-prometheus-st-prometheus-0 9090 ``` -Then, open another terminal and run krr in it, giving an explicit prometheus url: +Then, open another terminal and run krr in it, giving an explicit Prometheus url: ```sh krr simple -p http://127.0.0.1:9090 @@ -299,18 +308,6 @@ krr simple -v

(back to top)

-## Optional: Free SaaS Platform - -With the [free Robusta SaaS platform](https://home.robusta.dev/) you can: - -- See why KRR recommends what it does -- Sort and filter recommendations by namespace, priority, and more -- Copy a YAML snippet to fix the problems KRR finds - -![Robusta UI Screen Shot][ui-screenshot] - -

(back to top)

- ## How KRR works ### Metrics Gathering @@ -343,7 +340,7 @@ By default, we use a _simple_ strategy to calculate resource recommendations. It ### Prometheus connection -Find about how KRR tries to find the default prometheus to connect here. +Find about how KRR tries to find the default Prometheus to connect here.

(back to top)

@@ -365,47 +362,16 @@ Find about how KRR tries to find the default prometheus to connect ` -

(back to top)

- +## Data Source Integrations -## Prometheus, Victoria Metrics and Thanos auto-discovery +
+ Prometheus, Victoria Metrics and Thanos auto-discovery By default, KRR will try to auto-discover the running Prometheus Victoria Metrics and Thanos. -For discovering prometheus it scan services for those labels: +For discovering Prometheus it scan services for those labels: ```python "app=kube-prometheus-stack-prometheus" @@ -439,7 +405,11 @@ If none of those labels result in finding Prometheus, Victoria Metrics or Thanos

(back to top)

-## Scanning with a Centralized Prometheus +
+ +
+Scanning with a Centralized Prometheus + If your Prometheus monitors multiple clusters we require the label you defined for your cluster in Prometheus. For example, if your cluster has the Prometheus label `cluster: "my-cluster-name"`, then run this command: @@ -450,7 +420,11 @@ krr.py simple --prometheus-label cluster -l my-cluster-name You may also need the `-p` flag to explicitly give Prometheus' URL. -## Azure Managed Prometheus +
+ + +
+Azure Managed Prometheus For Azure managed Prometheus you need to generate an access token, which can be done by running the following command: @@ -470,9 +444,13 @@ python krr.py simple --namespace default -p PROMETHEUS_URL --prometheus-auth-hea

(back to top)

-## Amazon Managed Prometheus +
+ + +
+ Amazon Managed Prometheus -For Amazon Managed Prometheus you need to add your prometheus link and the flag --eks-managed-prom and krr will automatically use your aws credentials +For Amazon Managed Prometheus you need to add your Prometheus link and the flag --eks-managed-prom and krr will automatically use your aws credentials ```sh python krr.py simple -p "https://aps-workspaces.REGION.amazonaws.com/workspaces/..." --eks-managed-prom @@ -485,16 +463,18 @@ Additional optional parameters are: --eks-access-key ACCESS_KEY # to specify your access key --eks-secret-key SECRET_KEY # to specify your secret key --eks-service-name SERVICE_NAME # to use a specific service name in the signature ---eks-managed-prom-region REGION_NAME # to specify the region the prometheus is in +--eks-managed-prom-region REGION_NAME # to specify the region the Prometheus is in ```

See here about configuring labels for centralized prometheus

(back to top)

+
-## Coralogix Managed Prometheus +
+Coralogix Managed Prometheus -For Coralogix managed Prometheus you need to specify your prometheus link and add the flag coralogix_token with your Logs Query Key +For Coralogix managed Prometheus you need to specify your Prometheus link and add the flag coralogix_token with your Logs Query Key ```sh python krr.py simple -p "https://prom-api.coralogix..." --coralogix_token @@ -503,12 +483,14 @@ python krr.py simple -p "https://prom-api.coralogix..." --coralogix_token

See here about configuring labels for centralized prometheus

(back to top)

+
-## Grafana Cloud Managed Prometheus +
+ Grafana Cloud Managed Prometheus -For Grafana Cloud managed Prometheus you need to specify prometheus link, prometheus user, and an access token of your Grafana Cloud stack. The Prometheus link and user for the stack can be found on the Grafana Cloud Portal. An access token with a `metrics:read` scope can also be created using Access Policies on the same portal. +For Grafana Cloud managed Prometheus you need to specify Prometheus link, Prometheus user, and an access token of your Grafana Cloud stack. The Prometheus link and user for the stack can be found on the Grafana Cloud Portal. An access token with a `metrics:read` scope can also be created using Access Policies on the same portal. -Next, run the following command, after setting the values of PROM_URL, PROM_USER, and PROM_TOKEN variables with your Grafana Cloud stack's prometheus link, prometheus user, and access token. +Next, run the following command, after setting the values of PROM_URL, PROM_USER, and PROM_TOKEN variables with your Grafana Cloud stack's Prometheus link, Prometheus user, and access token. ```sh python krr.py simple -p $PROM_URL --prometheus-auth-header "Bearer ${PROM_USER}:${PROM_TOKEN}" --prometheus-ssl-enabled @@ -518,7 +500,67 @@ python krr.py simple -p $PROM_URL --prometheus-auth-header "Bearer ${PROM_USER}:

(back to top)

- +
+ +## Integrations + +
+ Free SaaS Platform + +With the [free Robusta SaaS platform](https://home.robusta.dev/) you can: + +- See why KRR recommends what it does +- Sort and filter recommendations by namespace, priority, and more +- Copy a YAML snippet to fix the problems KRR finds + +![Robusta UI Screen Shot][ui-screenshot] + +
+ +
+ Slack Notification + +Put cost savings on autopilot. Get notified in Slack about recommendations above X%. Send a weekly global report, or one report per team. + +![Slack Screen Shot][slack-screenshot] + +### Prerequisites + +- A Slack workspace + +### Setup + +1. [Install Robusta with Helm to your cluster and configure slack](https://docs.robusta.dev/master/installation.html) +2. Create your KRR slack playbook by adding the following to `generated_values.yaml`: + +``` +customPlaybooks: +# Runs a weekly krr scan on the namespace devs-namespace and sends it to the configured slack channel +customPlaybooks: +- triggers: + - on_schedule: + fixed_delay_repeat: + repeat: -1 # number of times to run or -1 to run forever + seconds_delay: 604800 # 1 week + actions: + - krr_scan: + args: "--namespace devs-namespace" ## KRR args here + sinks: + - "main_slack_sink" # slack sink you want to send the report to here +``` + +3. Do a Helm upgrade to apply the new values: `helm upgrade robusta robusta/robusta --values=generated_values.yaml --set clusterName=` + + +

(back to top)

+ +
+
+ k9s Plugin + + Install our k9s Plugin to get recommendations by hitting + +
## Creating a Custom Strategy/Formatter diff --git a/images/krr-datasources.png b/images/krr-datasources.png new file mode 100644 index 0000000000000000000000000000000000000000..4823558e53f064c577c2ae77827a2a03092a7ceb GIT binary patch literal 42120 zcmeFY^;erw(>5Bc1W#~^Cb$=O4^oP|dy89fw*bLuOG|MIlmf+zyF*)~SaElEx062a zch-8|?;kk7oO`V#cXqOpy=Twtx#pUg7!5T=JnZM#0002*r4mdF0C+}^yzc@&Lq4;6 zR!JaVu-udkJpce4qJIw*Kzb%QauCHsOHme3K1{WPe1mQ)qbdUcR3+iuTVenJ1=KHL zGCIB}2U(b*BpZ#7OCrs`xt!!mQsgx%Q97fTK>6fEubedLH)mVHL_{xys_tF$n5x$M z2#FY{V1syqgv~`TeobjShVW^FQu1F=I;EMu>Vv_M#k89@ZE1XbwrZ{;+!Vti+HE&) zhL!zt{BjgE1%SwT{MSV%2m8NABBbO0>kG(RX@xLyl>app8js3@`oBg16c|?M|9gZN zyXXJ z60G$7uM?A&u5*HNE#`ru*IK`({L^Cr%S!)hchJ?=7m};TlDc+62bCc6Ab=m;-NSS+ zHg&7M2(^(ha=$fjSxQQt%5neCIz_%_I)0(}3UFw_oPJ zE%_!ntMxLXxT1{)sh##?^KH>>AzQNk!86fDs!}B+clj_)XU%|wkx`fDx3&5915-(p zSM`x-5a9JPL=+wt`D{|4oTdB3&zVOyv+sJJYcfl`A^eTN`Iq!l1}3Dc`=S58sw+Kg zHwBNvn^4Yigraj7cjfi4lzds49V19sM$TG`N^DQY-)D$)Y!b~h5aAHXeelsBWqXo8 z#wEy7#%lXzzj1gd?OT2LeHU?m_9Jd( zlK}u?{_8oB|B(URp3|5OLcK_Y6BVw)=QYL_G@}+#RKcV&F(x^Sw7uMhMz2L)|EmvW zT~7lnr+Zdtt^&=pDTq2Y*_;Pw9^E8rRe`m>=a_e%k^^1gzT8KfjIbUcRf%7`aP-Ra z*Ff-CTkOqu2X<0?$wlg~{gQiyuN2QA004{59}3N$qJy%D9|zjIW_MwiZ9~2b3(Bpt zO+|GHI}1%e&!(#(-3e2j^ekGY-$V3nC$)M%qg`BW%=&+Q{#1napXtYRnu3nT@zwXf zs&rBlVvYe2gh+7}@Ynl{K~m?xwG+Av!5s|v)AUsRW29!k&k4BOqT9b(3c4(Ocw_p7 zw!)y@m#AV*CfEy?v0plQ=7FwOk~}yFT)im!8i$a(b+bkh08ZH|k7^ET&nD_*o# zySms8(X4!<0)o&9&nuZpnNAzI_puS}kB$i!W~tPkclkhykHNhaJcqc-#cvY0vywAs&@X zJ&-WD6Hf$KSd8mfxLwKVuTf4^*#s+R5zK@hWEyN8$=}$j#t4Y08)6;V~C1w@r2b$8$(#u<*=Z*1%>7!G0W2GA)hpk|5OgKGv z*DE+Ehq_L!z_ytSk1mdj1Uj?A3_cf$Y+;vBNzMGq|Idm5y}zHU#u~`yFng+K zpKaB=>9?(^@@r<2<&Tzhsbo@$FQrvldxkq{hN%gybRL(fAw#Mki^}8asl)A~tC?q+ zfOei?Ae|}#p}?sryRL9d$jU6by)~s}Xzu!*NiiC(xLs4xK&RgQk#eOm$q~~q6hUEj zcRXID>v@{o6X^>eKMb7yaZa*3+7mq-(o!--?IF`!&|UQQSCx*)1$k=(YRWSZ{VDlj z__T8@Xq4)qFBz*rJMT8-Y?SjqhM-YU$w{=u2M-gYTb#Xj_4gPz+TGtN)G7YH{|$%8 z++jLr_xo+6=(F&D#=*6 zC?TTWRwAK3#D{XdDuw)P&r5pOYkl+T|!hXzvq zBS;qME+BX)t9hq4Y3VN?q#AEaIn#R??B2{kr-byXD{i~$*|{O5vvxss9G^-BN@#4# z(m$yxq{1p7%5KTEOiGn3Ja2Dz_?L@*CME(ycx1j5tx;8%eOSPPT$1EPiJPhTM2hGx z-D4%0#K`Uawwy$_VtEGcQl0r@IdRnWn@*V+3q=1GSg%;?0EOL|)8?6U_J$0*S;*M* z&X*@Y&&y+Sat}peHjKKyv! zWF}IP3tQ)=tgHJ|aCHCEY(?3R^Z{=_(#DuSZ3dhtCrbp)o7Ec9Q~lBA7_AV1z|i|# zyStxW`IgbfuZvjzzAtW$dZmFz%&)8UDbBp~tC4{KO7+2KZ%0{JRaX|Fm;MuO8X<-n zTw2Y}{P>0Q!y=Xl33QjMRe}4SFROw(FYt@?317{FZR#JF1Csy9cwMNuIQmiE81>4q zwKj&v_1;!35*Rh|bUatFeTEfY5nX|UT;3zp2e!@9XJzwZ+to9sgKdd@H3+T~JaQ)w zEr9(h^7t*dyRcx1#~AylcVO-nrLor~7jKX|+h4K{Qq0q=-v;$9z7%+5jyjuux*6eh z^#Pvd;H$!?gBK~}Ibkmrj#^&MjYwt99yV$p0Z<~bdJr7r&0a*#?JHUiX#tLErxfV( zei{!6IHe;Cdy=_<;YZqMN9E&3B<=s>hi-n})zDYEjYNjjz4@ge2{Z7OlnI9k$=fqJ zGa^@c^V{4kIEMqH%V@A1L)-UHBo?C;$L$t^OPOW!vBZotPQ%q!=pb;x-k9^U+9_jT z=D?y(NvTe)8nL2MTi3^v`WfKl$RjGMGl^Tj`E(M<3GVS5{5}U!NZ~a}UboO z#rmd($vs&TUw-yg#a7En<0|zyZf21<$%+VMQ?7eia^T&kz82+1Rr{DgI2p6 zN9#1y6ZsU_Gf;j#?{l{`A>&w-hWvD$)@k%SNr5{+8p2CH9S&zzMjUK5DfJMuK{qDG zuCw!hLIlS#tt2x@)v%x1%AfxvkS(ygvyr&)^wjV(w|$cgVl1_A_^vJM z)e@_^TKd+l)+#H%#|Pl38iRGeue3SBBq9{3aXiFdvj}5KzJ>0pjisL zk@M@4d0SXdL|K0JBzlai>NTP6NBtfr+vY-nnmGJC6r{tV3%UON`)CFgPDr)v%ITm%HsZ;OBHZJL~NB`QWvmo4?GbG|!(1x9&bS*gyfgj6X9)$YViEnXE7M{#W zJbBIV97@aw7OTVQrjk3cD|d^bsBk$7BiDoijhPOy0fK>2-ho$vyFM$gCyoX^u-1h@ z^t?)2yCgMxf7w0K@m30ts%(ABxoJHBB&f3U#m(zRC8DRosB|dw*NOZfV@orsi;cxRY8RJ4 zb256|Fqw-T?JC^M5-Q$^`u>WI5(+*~1dmr28xb)|KZdt!+5&@PLy#x-=0{NH`Pp`?MM>g4 z8woat(*6XDrmhXOgMbur*zn*jx_aDQGd1^?3wAv^J*}iaoc{7dl5l2u`=RJRCILHa zXyR~=r1MhcY5o{OLdcQLe{@2s4)a0oX6U_iy#CK{uLOnIO)h@wsuH(o3gL#23 zpJ%`aj(FdhOgS`6!$#L{Eh3dOtc_OwzW?}vE!u>A_~NvI%ZDkTd6%`+;2Tf9MEln` z>67X$SDd1o{-sC|IDCO%Q4xp6KdWwE3u~Pjxqs4o)>j(8=Z#sHEF9^j0`ir-I4>wd z&`AiLXM>{XQ0|4#GHkb@Fj1$^)xAL;hhNu{GXC)u(rrX3Bqt+yw3?99)#p#S2kIaj zBzpDh&M^_&vx*EalNIQJ=M8JX?*QvoxkX=f#4@So=3u! zlxK5A^p)(Zt!1h@2q&k-a?YFpm$gI0NbMey(@Hk>j4PaU8ZAIvW4`)a3CEb>eW|ci zx?QJIVij#jNlnP1kg}tv%9(Iv+^nc;t5mz~si8{{$H;>z<;Yl{ynF~xZe|%Q$NkPD z%Q%kxE1*TlheA&^x4{Auo^y`5VgXr=;UU{Qs^|X>VmL*zLiRRON9FmK?UJw?<(utg z6GWRNshXt)ByTg5orSEDk#s2lfKE^Oyy(l97iVO?i<$KQ5Z5|8<-ig&SECt<&Ms7O zu~3(7k$@a->`NnAsWan+W4(%YGvIp%5q^=mkvjl|Tg4FHHy$jhPBb@nTaGh`LSTmx zk1t6elbhZjt*w-uR=U&lEi#6o)z!5#kkw6smn0s`TzxLsA zE7Wm0UjmuTw!$kNc8wmMG5$gxYm>0Bc`TmQ#VaJlLYEE-l8_Jxk<9YzqnMbe3B$%9 zh59Z601;_ES~ADKKERGPZ8`7&6rB}vxj(*TNX}HHVN)C?PUo8{>iWqK3J(87t|Qs_ zv1MT2NP{7R0p}%~MyWKWws55wQv-V!cIM`lRTdKr#wx4qCvp&I&4?#@`@3qZva5K5 zWH<=sR_p%ECprr0-p+iUnTS<0%`5u-)!N9?_4hIsQl&L~nR~qBy9)+_v6dNOKscTI zQOM=jbhb$8!!Yu9&!*E^mm(3B6Kd_@bxUscR{fbCkzAgh6?dU;u|hC8iY83oa-#hC z^GBMS)2P5!v6Kri_wT&km4%s_QlkKH`>16xI#0>4Lf?N$6Aj`_|GPh(hea59U^osiTQA z7LHmiHQ3SL`8Y6cw9yY_oYEcMWBS8(Jf4D&g*J_6?QKvn2#Q<|F{%t?ICy0wvJV-V zFF6mvLwZmF(&g7@(3QSr61B2x*AG5Ke^p{n%)sGrMyC{tJeZ-#fl;HD6q5p6l zMyZA)`}(Z-^gF|Y;UtX&F6`*P76Ot*iD!O-+QOSMO}a!ZDh7yYB%pggn;2j6f&mHx zW}A$baYiom8skdGEB(U<8!8Bkl6H4VyqkwOcMrPvly~78cPEY7SgpehoF1-U%lk;s zLznw9T)%uTZ%aRoK|U*FkiGpE8WQ6#tUEJT)wVV+bEcy^P1dM3({P9P{3GGhxvfSrH#Du1GL znG|>3dIFHa)VtpR@~9DI(W!0{m|tsc9Itdhjl?wOy9m~so-v+*Qb1(gMwY~OvMyTD z-(vP47?$w?T;5xR!?qSYp%MAr*o3d;6yUS!=!X4rhp~T>_EOE&y^mTBezolU1>B&J zJ)OxX4}5DxP8DEje_6%qFr#fEe_Q6fxBw3Jqd<=G#=Rqv(L&N1JmFSwtbQg&@2oVZ zp-(Ct(!e+-%kiGyhhC}^b5;GMgeo#!q|af@aFUDzV(NQ-lu6eN2Sd@u9}GGk8Lks> z;-Uxf(sb;^OU$GE;nx))ggTone7O!IYoZP_&$L`R)GOSrdEzq1^!*vs=gLU#i!CJY ziNFP)L^Ml@OumKAk(6WJ6s)Y8aLLX8RCnn_B+&l7(??uJ*-JQ%O@oSGP*5w#lv*JE zImwR%c|e^@(__;fi+&~N2lGVv@hH4o^H7mcO5}nDn1wsi$hhxCv9Ldu9dPn5+8Q@A zDe0``XiDeT)+U&RtU1p%*z>hMtvtW=948HO=d-$Ayxo(*jGtVh_3RK9-9^X&ZO9(W zKeeBHdNe&5%L@v*r^a@0bhInDd^lzg(eL9`3&CjI8i||9`x%=r=!rIBln0_QpCb<6znURd7$a@vld-B`qQU3P3w6$7sn3V zGON`TspA9h@H{t44R70K^2*Dz4?~tw(xH#MdwK4YY)$K{aogq1wsQZnG|4{KSYTXZ zN)jT|)<^Y^m!SGfqzJ!alhv3T-KW>xcWe6|%kI~ki0O4;9Ibr|N*fkGC@6r(7`x<@ zk&U-5pZLyucJ$sd=a?3e9q|QmMI9Y!&)&2YjUUdG9+~AWHN*PWj`oUe&E0?mX2;&{ zS*Jd|sAKPDy@H)*M{j+eRVO)*+E|d>Y}DB5uPn|mg1@7)fJup=O}SriaOUyA0+^b= z_+@18__(#ojRmU6A#nuID;XP|fE`?HR+?{@dSpFD0o($G&CotZ<@a3cx-H%t_}wMN zb>hNOXzQ4nMDLD{(b_hwufA2cMr+z^zhE0DWzw>~52kWFIRBopo$BadFqEQ6aY(3GPK3zc^ zYA?R#@jSQQXN{rLHiu_Ml>JJ2>iz>>@YyNXg2hDqSRLKQXoN_@A|Ax&O%Y8vg2N#h zII8EI*0o1S9j-X|QlehG$&w(*cBu{hl(NqDSAQlDBB`S$62;aY7|$imAa^CkUhJ{M zKD(E}8bh>03Pqo%<~fSxEh!FnOI4*)fWvbtT?6)Mev!>*;oqkg~9k;k`tg2Juz!m>Lu!(|%!2q#*cI_o5 ze!ETYxiOfA4sheWQ%vFY;l^HUJ4PwHK7pbDLL(#=dKt?@*O#H^wLm81&aE(Ee2LRp6&o|U5%oLMzK|~JHi3$)6wi=tFe_PfTmZMkW%r>g~QDL zwa6WTiuzYXEO}VK7iol0^5F#E<64)dRmUKp4FS4A^glHGn;i#Qk>{`4@!`o5wb(i- zZJ$DUXz$^2M882dzFkfu-9*HZX~F&OAbZY$?gcztGX;xDEBk3aFP$-;5T8gJ|$echsDTJi$p(rn*hAB9}I7d>2?&|Xgq zGj{!UZz6AChh%@9+?N{s`sCAH!3%{Aa0oTOmfp0CE9~={ySVbN${CD8llK&2;X#=! zsru2X3O8uJ)K*!M+5f#(Y$f$iSkA8}PCFGBW#JUCG6WaS_>Y&m2hMuawKWa;EQv9{ zkDM!*tW_qZq`Cy!g?ujmY~p*KZtyv`w|_XrAjQ3Xy1lv`yDIet3MLNocWBS7D6hcP1fS zMYXf})MoDYV2NsskZ2h_J!I}|?Q#wkBPNxUGw(!_RQYGu3`j;6ePkzUkM2DRD9Nzh zol7x`ym@3$>|g^&o7M96FM1M>#nH51yxb)V3lCD+l_N`H#~SsP@uW<1cEan(=3jSU zT=-V%YKTh$S(&ciAwHknQo3rg!A11RA>kNVPA9?7(aAwM?Of)zUh)G)n{1Q&!5XSO z5O4LV=YIKwRr=LPK3*Z2LG7~@?CzFnceZg{xB^7R@vajD(Rm4SQ?$c**JWn1Ls0#h z&h-jJw;BWLd+e*W(3X5|kM?FH4#)E@&qTXzNf?>;pvIla-Gys=YUbT`{SJ&wasqrH-K_b)(FBc`c$s;y}S3C;$e095kyPO3A6Q@T8YsxB%s}U)8H@i&K|7 zTg_#zSTVeGLO{w~y2KoB{@%4C)=fqRs6v6!XLB0aL15FHY)8RJV6wy}XI;>yvt}2g z3d@*7?@6aMZ0`L_A<0-oVhp0zFr?x$4i52bj(hWB;P1om$|lGFDIG^NOYdKx)hwhy zWMJ-RnS$_AaXG#22+t6n)>VIo*!@|Ow9C(02_jqB36m50*}X+SLlVgR`FIba_3`+H zZqNg9|E&`?cu@|AJZSL#>;*|-9=F; z@bG`Y)VTa|{_ohl%9Nul`WEb(2Qozc9O<*AMYqY0)uTHFE_75T^lzer_11DUI^bpR zukC~C2t>C--Lp|9oUVGj6-`Rjdro6=PNmrBK1dq~V0gI}SY@RYU5vdl1WAUA(myWY zJ;_-v2lSaT-Um?^C+JrMed4UR4tAMn<7*3E6AO5iW6afL6KXkHoIVH?jyg>Ta$k94r!?bP0m`9bo&_%uksJB+J82IAj^jc zu`5>kWWQsf9e@GcjqLI)>@;*3{_8}suk<^)2jHvhOtK}h=@g*@hlJ8yk-eYa5)!rR z2;4MCYhn-!&Tn*Bh34dT$ad^bCMayl205L;d(oLFWT4p~_f4UZ+`hiPj4%+H_Ghi@Punqy~BLPHV-D zP!r(Kv|YFSKsyt37`pp)9JKa8(Uzp{xb?2R`Gw3Xoxo#LVC#d7_(_GbnNo>KoPNP% z$w$*N7TGtI&0rldXW3t=i0z0SRP^FYQNF>Zpe$t8XSBWDE7QwGo={#~jC7r~sRy?n z^d)`mUDdGml*YpZetj-7b{cRWh!U<*)qFHK$ zJEs>|cu}=9E{y=otBT8nVT4uC2&s}WJXc6K70*WU$oHbSUS1cT}?2py-~<^L%h~P9A*2ye92zl3SFmzgyEc@#9n7o zzKbWX5|6;lrtkg8hlr(L&2|C~8qW6=(@l3DXIW$|ZE(Ir(%@Ovd0 zYswl&=n@vb|A?u3m;!1x7*>6=6;T@vJ+M$FkxfBVDrqVxBjZ;8J?H1V2L=K9I|Xe+ zpprYPb%^xzYD}wX0=3eGRP>pHv!2UQ6!|+vPfpYKKgxl~IL^2ru+UqBbD_w-gP^T& zouMCP0X&0}ba`EAdjtJP0&UPcWf7zM@J=5^f15|4vxUL2iZ*XHJdy>vE&rM(+>j{a zj}eevxi;OrSvugr-9`;#BKGBZnD^NEDj(;C4)`kgM`02n2rREqg3Z(fVr)NE+GqEL(wqCe)OX+Sy|vgg%S96 zH3bgbnr0qTFHd~x_&x3}%F;#r_+cnPiGf?TQar|*oYZu6g}KO05!O-;Y+FM9DK7`h zi9x-C6ea!G`5B;P6iM1RGwZL`od3O$WP)}flhBDtFN6#w2*GE%JWlDQ)SjB*Kj?oI z1uZ-?RzKcTOO`94E`L>0tNJl1wV73C>u}hbWW=LUeGT`Q;9I1B!CJ@smr;Ey13~^5 z8O=g8m#F9+h3P=z06vI5&@L^2eS_X)+cpe>Kx(Myr8>#i*6a0C~B<{l@%N}8zd(qZb^p9Y7Ii(xCwu;Z2nF?!R^ znSZA(6NTDj+uZZrXPP`Oc$Y<=5oL36lI#vTh1oP=cXeD!oy5+ZyruL_RP0NmrOi=U z49lqm@o!CiAf{iX_9;mFy@Ijj-m{kR($g_?{)a`c|6K$$gN;>EaiA+A=`=TkcIM9T zhH&$`#EOfd2T!ag-F!~Sso>DGc@TWyQS|B0vK|Spy!2n{{RkH6Kso{$ImWq9%2hDD zUR!a$jqrpl^tbBBJnnAr)(|{Vv&6sH>gi2MW=q0tR&|)eh_T#-ev9J6lOxH&#gc13 zM@zz0v4|t5E_JKCRNZa(8n!Y5R7vOF)w54|d%tFl-fsIG0j6=&hqXoO;e>OfqrZ-D zuFAX2oRANT9mO;}qK5=+7@!wRZUp|a{X?xOn(2A^v&mw4YNbQA(=eUxq^oO7qF#uf z|BtE9NFfRP(nNpb-eI#>*jm0o4!NNEtT(rCMtEf6r%$9g?g@$5r2I$$hsx3w@#TI| z5~c6MquOnD2K4}w6}rUBp3E|>0>rD}7ztN|gCRgd%<1R9)XA6eBVmzAFJxw9sOc!> zY~}h zWxr+oJ~FLDIc$^*BI_yB00%yqVCg;|X*TVmks}ng>iZx?p4ZE6+zedLz62S9IV4k( z9P)$vLmXo8IXI5De#MBLJHNZ-3X4x+ zrZm5}4HbW^SxVmjRQ`17!d-iyiDiS2ly42UFXHs$i!A&+52y;6Jh^a+kp@jQUwH8- z95h@p@E^${@G+%i@){e{xZ1X%O|HXKOFRaipLt)isY+eVHFap}Z#|t!*bcY;i)-8? zf$fRdfY2d88JDp4`&OH9zIOHVA5wF_2gO*K&j55wD*)T}aPc{Qk|tek)7Rz)&s)Uk zpJ|eymdY4meGmbSgtH8i$81WWdr`Xi`XmG1+xfsgH)3RQr`2WhEDhS&54sk=&P_s=Mqvd0dDJg3htd!Dy*~9Z*6=wX*A$OqI5O@E6-q zo=#sevgRczAA_bG55}`AKk95%tKdy4DTkfWAN0~LJHfVJj|53UB?2qCj{yp{Xg;!) zmC1ts-tU?Y3}f4_sCNyUq%PY00y}|3Lc!l-qyDm|a2g{7j(3AX*!NqeoH7hRT16SRa+hZ-d z^iSUS+Zz{3se;NUr@OHtw7~j=yjAR-XN_CAXnlPP%-Q6f);&K0ly&HUvpJDrXlR=c z{^4DUEsed+k|K8VNmoSsxF|?ssUpxR7GO4=MHox0GGZX_Y?;wm``nc^qn^CfHF}xGCXOX}QYr<5b=+!varbzh4+E z|4IuLf_{*WcN=}2x<82BrtHd#VWs4iT@spHcAz+PW1$oeC;WO#Nwyy3^RLi?NyCJ{ z+~u5v7*59{nM)tm3%9|*B87w}X1%b4wzl|IRe(CT%<4Q@Q2>P29g9(RJm7L$fK6sl zOoVfZi(n2UY8G50Wk?dX^@@|me+{nD#0AMZYNKUC=2Aj$5{ejobdAO z$|As5BLSJxmYsg!JVzN9wq(vvnQ;ykm-GF7)i!TuMC2o-Em*0*t@K@sOTel(-wQ7< zDCg0PR0$mRZL;o+s7cho(87#xL?D-$sPx5t35bI9v=D>x3N%2S#m!Z@hyxC9rYmV{ zVRS^+5$$e9pPHq7u2HAR5)cWv0aMxFeJ>nD;gJpZfQphz$=v{b7~O1P(`jIoHxWWvL_l*A=+)WmlRI@V_DepdYM!aMRgi6q;-R0(!d62O53 zU#b-&=m06Alv5ziKxuY|79~I^Znta?%jr{qrG}4;B+)#{mxruR)wFrQ*c@N=%V;9{ z#6&TL>3PHz8XRZzXD4dxQp=LM^!0LilCK}wwihes&0LXhr8Msyfd>}xv#z|CRk8(H z;o&|%!WhIG1b~YDFn}I&R2tD21f!L=GAy17OEr1==CJwF#&e3{<3NQA>z&}wy}>oz z3b|IUF)-m5-<+;V8%R>EpAWH~stC-0zv@A&4FPgFHBDKT-b??rKO@jt!s;O%pS*37 z)$T1}Sq()evBp<=r3c52wqh20)8?ZH6M*D7qcqXSg1HXOkE~O5%+1xWq>kx^KJ#>6 z`Qq0+?z)c`*>^v~4zKsnxWM7&IA5^si~qe-KJz0e5;&Y13PK4LdX>#^dsBbV8>cSC z(}UP}84IRuFTC?SE*JV1Fd{!ugDcoq>7GjiY{Flt zU-n0oeOQfoZs|cEehRf2r7yCEv>bz}hKKU{!`w?teRF&lhz9qi=GCVbkEPt5E2Ix^ z&e;1#lGyauB3Zr+kSfR8DZ7!Jn|8qsTiiBK$a|_8S}GF&J`Vw z5nRyi1`m25UG2?Ka>(fAv_wk9xtL(4ymq0*9`jf8 z)hw{e7f;2|)%7-!t7qQA^S)=Co|BXMcQi{A69qVUNs^~ZgMu_4rp_+=`V^LU%A0PV zABYPfQ?oq7;&PNvql!SlvZ^BGXyV8|BT*%S=SVQXZ#R2>5G_ju;;I`4eVgRomtI&Tv*KEwlCA&5HO*YEaL4S>n)u&q}qv!5FMuxby40_9WCe z?)V#-3T}aze-BS7aM$8ac}e8!0{YLR(F zZW)>6dqcPsZ#~K&tUbqVb{Q2a4s2=VoXn9+oaDT|AOE6H5EU6#YCtz?94FbVxy(@EpMNC1StiCSAo;ZP zzW-i60`6d*@SIPt6ab0i%<#F&6N6^hVFa~)ZojD4DRTQG;PbL*?zDd_8zaW)WZBWw zvzsWZ=5?wvUC=U^_CsRt8LK%~2FbgZ=fjBIbz{VgXvxSmeNEmIWlDvogU9dMZ8Tm? zA1=3*oF7nRL>D_VoF^mK&0=bDod5M}F9`pWCd>4v7!PO*NEViy{GW_E-#kSX?c_M~ z`a+QJ^l?z}Yjw=l#+#*-3Mx@2a~t{4g6SfX%73|qkMEA#6u~*1pQj~ba5m^`BXO?j z&!?(VTySx|n%}sPHG*Xpo|&Xvu@5#U5&hLLL+!_eIHiXV_a3+6%u#gSJ?{2B72@!_ zL%;^O;Ba7^h;6oaUbYNaZyK=EUunl1lsd1bT;A2xJ@9jaSVtS~=0jI0{3Tsb>5~OS zGR-SMK2T>U5Ez7?e%w(p4+b9yl%@Zw8EKsuZ_1ecTmChDG(U@3+xpE`zJkX5fms&u z;-yN-i17Mu5uItgaG=FyY$M~VIkAbe;h82n&Ev|V)5Cg}>Z1!{3J-~mbRwot;}I3! z+Qzot=;2>!Fa9v+T#QwW?7a%O8d1|%sETMe%-A=qLZZNupEq<0xKqvaT3>btDmH7$ zN2_ckC)J*4!n(e-l`nd71dw*{MMl}UXN8ud(bEw(= zmysIk;~~30vg58fl#a&;dG|$VOp>?4{Wa=Q;+SPH9&>S{=_SJR{2tA=)*P*5pe#o# z7s7gKmSw5vfu?-z0WZg7CaYg`PW5ithA|U$fsbuFv46PjP0r*Nm0LAKt%yCXc$qi? zm?45J-dqta`6Lr`?PRD?jY%a+X(VsFVTKf5PIkOKz?V2~*fi@Oc5;(LrqK*hVvv+cw6@mSe{vEm3tKDERbZ)00(JJ^QR7In@>vvBU7!5J z)ntWQt7}K9o>$NSkzI6U2O2=ibCOvp<~F5Hb+9@mUv?kSnl_zXnNl~cHsq46 zLc9%lLuw*M_=hlXguY1nPjU{DI9?)3et7MO(t>D6}aEbA?{^@}QEtUU};fY0vN(w>JpY3DvSm@@qpZvP= zewGOeH!-Xq*t9pvQ~vu zf5~zX1F}T`DKwNOUiBUz6Sb^t8-hJ}->~hov^z*$!_LL!?-}GUgOB<6`UKKUjLgf} zdF%M%fIT+nU+E8LQs^U&#gMH_fC1aK+?RwGIus2A#)2E76Y1S0>Qp{TxuDJ|9?fPN zz;%pouxOhG_MhOqu)zRHWL>+_PaLW5P_vOw+Ggl6cpS&*3)+Xr$CN_297-I)kE8Ii z?A6ZBDd_q0f}D_+dPaq~N;q&y+QB+w75LTr$eJT|K1{mYW8lkV$&ARC^!}YJ&}%)# zNVT3yhjjNoP0wpHnvm=PY7iG#cDVU+5boxwJzxrY4}zHHqyqA8z21E0n68YL_K$H1 zC}e;GWh8-w+V}%mri3_zN2N!M$y6YEe*eG0$SSTjvLhl!YH#(l0^0>l^w{VZ)c8~8 zSK3X;#7j_8h~KDHI*;`;3?A#gjq|ZQc^!TIt;UDMIoEv3^5)?yUs7H=7yvXl@)TkB z*B$1#c(V9-i*s2m5tFWo#6De^`>*qpK46T5rBeI8APgm+E@#ay8%P+t;R{jQ@w zlUj|Hb7V|Zh#sZOC;sGHr=82c9EcGNZToX=l!+>Co=PS|supaD?e*p|l8nRU8Vg@$ zhFFD{RMo}cbK{m!{U5oImRAIYYGirZ&>1J}Wjtg4vZcQY4u8~QL+a(?O+Ps5%Yy|~ zWT=VyM0DI~#>8>uB5aqq@P1DphCa+i&Q0H2Q^6$TW_A$VG%a@G$l`BB}K$3^OPa7a%RXxuC!bLAJX|pSnSw z?4HY1jDD-hxSyNaW4p5t?>RX~)-;apmQfac{Sm<{|JqLkX~I&XPS>A1T!0FLv?Ab8 zoZ%@ZI@q!H{XOv`vOZ%R=iCICYUx;zF#EZztjvEjXXzUsyKG;jTGNtGbI$fMv(^hm zjA@>mNCUo1O7~5B=HAk$yVVd4&`%@&n}puq>J{0=IjI$A7sB4UmKJkdkLBE=N5<${ zwu|gJphi(LV!rpHg`+ZDrTqhBu^&+^k1A%fKqc06dn!C@23tJ6Sh_%Bu*VWmHLHOj zfkIGvDXT1faek3n%=}{u2I=I{{O}8hs%Yp@*=#_`7>`4IlgwX#dF3indB}T#%SuZy!S1aSI{OCF>;V4xQ z_*HzQ8TkH-lbb^$(M)c@0w#z)Hvab3zqLEe`Mq#`Ck`n?x}Z~ThD%%8?yhxE#W$6- z5^hU-dzZpJ0Z7+TATADt&rr51@=TDf0_i1NG`zC9|MRZq_D6y}86zPWfbN#%e zm`+u?JmxU-wbA=JSrg)?1=`KNA$=uuu9h>C`%Uh*^z&yIM63Zf zJkWuz9VO>S7anHnx!|R!Ik%1%$*EeSF0Ojx`)i zeRX*1%`1;uU0?b7!*g~G;l&cOIjW7|9q~d_qG$s}5|8f?wak!vm}81{^!aEn(9tM( z2+RYGZPx9|$B;d7w~ICW;@?&)SDO_fULQ>G;4Awq;;%A1+h=$TBj!Dm;#9j&Yu=VZ zjaNwudmaA|y3voDE+5?>t-Kvd>K}9twBuIndJJU@o8%KIglKD-So#VP|e9?Iv!p-mnhO>uv z6gf4XmAJs0khwh8zkh$8)s%~Ll(2f8mxtE*6sN@O2S~{GnW#7Bi4yRK`UswneP9RK zWdRwVgpjE?byjm$N)r=ORTapznpbOTJ1U71H7N>tdnT~X(h9Ot{B29SyGS#Ig~Mh$ zTtS-%V+f)|i~=Ag2s-JYV9{)-451SEb5|1~`DIh1Kc03hmwT{p^PtAdhtGMwHtX`- zbI+70tJQR3W;{O*|KsbRny;Br6BDD$>Hg2>E3%QDH<5(2HkV(FBubl5N9<5O+-*tF z_|S40NtKdRYGWA=GsrApgYtff_C&|j=n!tRgtJrDwF<5uq8?*O9vXH2gQE3|* zvFj3>oDNjsk*&qRTD0B0e)e2oqHdeBLx};QZ=BqXu`3n(cU5?+Z z{a)9&2WGTlMOE%6Zwv$&hq5n3hF%R|zD2glSVzOQe{9ZUzFa!HQWLe&fnQR_wyQ^d zDd+_bV{V(JwySrh;{6PHm2ZPM|NeU}6Ti@Bk4dXG2k$GONm%-A4-tcbl9EEQok?vh zSL1b_G$h zQZ{yXgf+{_o+@Y+oOt1| zL*vO6gX2uf3A!?*rAu=9sy!e0e*XD!>dW@}R0dhGdB>K*eSfsnY%xXr{{1JXhV9G| z*e!`Ik4f>*C#ik^`VH59TqS}g)v<*~=SdD0s;9{{yP2`C#8Iz*e|qK(o0y#3t5ZSN zp(-Vw@#DE~W*NCJ_2XI1Gg#lHEqyC^*-!4G(53Jk(7HTgN|?_g_2?poC^^4D+Oo?c z0hn#5_tcX#+nA3|!L434Iq3Zy8>(4$P|uHneOr3Dcf)^1zphRyfwMV(nk#?<0d z{NIp8mtex+SLI5!NTZ!8&TWtfVpDZ57F-($$9TN7PUi+MeEL~q&+Bvht0(HSbvsY> zSRiv?%7A?WgCi0CM#n`qMiO~D^Gk%Lb7$}GsF%}XhIgWUZ;NOqW=XyZ)p>!H;uljR6^Ui=E&P+J8{dEmH*9PQW)J z9ARYCA{Oj>|7~+*3hg@z;3v-^$a=^4pf$^R(=UK63%Elr_hz4AnjZxTjSIW&5pC;i z8Jz>wzwDy_^&v`p;`?)9gP{;W^SEJ*s7pt<-vzxls3yhk;0WZE+X8 zQH&2x071I+t62Nd_2J-TbhOFwCM5(~$+wPNWf7vP@JLU5Rs!gz35TrRT%J{M;Wdxf zJ@B~zgPw^{Y5K2&muxPtSUdtp$}y}psq|DfV{`=+{u~q1Wp)=6U-`LvOfzOc6d2Iw z_#T=)57JIm5zC;xz}3#FrXuGgWxCJrN$Hz3-JVHSsuHo_ArZ1PH@BGz9^Rw3%0HZF z>g0R=PVrm?>o9ov^>Awx(7oGh>W1d`+Ua#54HBz}-?YOBmXvGO_>K-}+pV3X%>}^f$rYz;L}MYzIea4Z^qq4`TH-1 ze0hXYs&r9y(LB;=mUZEmm;kfke)tblBLU4>w@k+3CLJxTuoI|YhO-`q2;DYNK?b&V zJes7`Un&&vqFqDkcc`xw_cVopj^%f)E*8HZP{pZO$M=|zVvhss?3&p4xH6g4P zo2dF_S;;5?lZ4s}14W-Y=QLRO5>u(pLVy_NTa{X+Ia<5SDJjbVrpX9x#7W&*i)R&} zb|8#sJ`0tU8Z;(!oY4-KgoCEF*!*`tkCSR^WtbYjx`8VC$elY*98bb+10JH@R!ECS z>*3DRk%os#NGDSil;cMMeuQi)a$rkgmR0Kl^!kv>3Z*Z~;oW3Q(tDCC%Ewlf3MU0D zJrXpdMe}c`Soq`!rlY`5W>rNn<0|rv6+SVTWIpv_zY~+B6%q>LOfE!*{rNyHry=#2 z<0c~fFV6_eg`DopL52^)us|+0OK=pLoTu zb40L*n7Ym~QV(N8!6_8GiE#B8AGzN?nmsngr($`g%v4xo#4M_wQrmksfK>R?`M|*M zN4K$Mu;98o?!v+XAsTIa&x7>I<{rGPo*tdd$6J!w0oN-uu^qcz9dd$?1Bt zqy*ia{`!3q9+>X>E5gHOD8NlbpBBn6>Tv9mLA@z>7*8_ct=MGKr19Kb%7mlqb-2hA zs13|Q3BPWjmp!nO^92+tQCYq|8$#JFF}$->6lD7cu|DP3`GMqm#w%tjFP92pvPjIA zAB?4QrN%1OUKdf9{hO-^Eh2C%R-^h7vU0#yI!|5vg{Dl$3uU-E-{zy}6cjzr7t46V z2~}C|H9D5O{w%EI&}KVf!`kXoo$l~(=}chk$=8)|$Gq3udiB@O2Czlk<(u-7D5Xfm zPQp{m0J$3=w3h1H>MEGc73yCwnapw%UiUrwj)68t0Axu9kw>4VdNa|`?D}@SC2gLL zQJcqpoA?g?3&pu`);GHj5xi5EMlorf{6&|bK`_}iIHhj1(G!zkUB3-RSoF(y%KH*> zIf{Nfx#*$%AvRDdI^RQL7FT{zE}OQHVTLK5<`ZUo%8V3cy^Kw#-D=`^_xligk-~!6 z7)(gxWIkjc4~OpVo(?ioSMX`i)W2F~$-mxQBB6iJ+;*}Zko$7Z3gY_LOnEy#+IFzT zeg*q;cDSYo(22t94h6Co2J3puyQ`G8DyL%&&q2p`S`8Vjuta#H4E-(L+0?Cxznji~ z<)GU`31VyI=IUw7S#fI&3l_Va^1up*3mjH~u9tDg*^+MV|D~2XdZu|lc>cfJch=Vh z=*RU^Y$9Jtz>ANQT3CU?be!`9gtjaO9UVarK6NUUJ6mPL^_oo>q_+`oD7QD_mMnWP z@pkgXwgT2t6mymPjLDlHx%EQ6WB52&j?$+%J~!h&UI-!XoQ!uIr5o&0BqdWb;UN@B zRZY8Dj#YE@)LlXiMH%L-Uaa)6I_Z^CWAEI5B>0sh_dJnHx=Ckl>uDJx*h!E9;nii7 zN#ZjSdC;-+B=<^*wrk@cJG24|>_7UZvx{ytenrBPI?Z&**~6 zY~oFc_u*)EvK!>J62$OLBbw`XmJGgQ)cr1u>o@X6AQ?1N@HL0r?*POTi9A!sVHDDf zMgyV|Z_GmLrf<~2Qrh9Vc({xmM~)V{9To{U=!Lutiv~Pvg=Sk!s6o%St{NU$Cw7AD z7P$iV62xsLw*k3KXAuf*8u^oR4_MDA=Uyhap0<*R(02@4UD6;caQVUzo=3mj@#P&} zx{LY71NwccMJ2Wkj`DvQdK(+|U$vMSAH&vyo;Pt9Eg9gupXQg=;nbUxYH6P^BiUlX z45fkC!XpDDf>WZ5cOk^1hsJws(nGO$BRKr~_|Xl5HLQM#h&!`5QC%BY^~fQ6(rcGs zZzrrLA@fet?{ry?)l=Mh%bNyG>B{W+UgNHcG%^z|KjKH1X@Q4R2^)b_E{@12Ur$!% zfb^Syvc&$v8NDR?Zv2>sh+ePMFgp2lckHgeq@C+B&>k$4rG$!o^FYC{iO-L60Z~vb8uC%QD7UsKHRxXf?w^?V zN{v8oh+}>+6)c{pViC;S)G&xX^Vq$m=XpJ|QONdiP;q;eR%tu5LE!VAvL?us0K?M* z#0eA<#^M#gQ?W}YH_p-eg}xx%D9uz0^2L}^XUu=dmvAcX4_fhn0ripewX{|K$KMT( zN00I}ln(*oUfLP_vPIZJwP}+CNPLivR~OsxK2h7qCYU7GK4QPDtMc+KWd&b|-GT02 zLai}|g=V{$%zSfPQxxONbgBoR*!XP4J-S(LWtunnw3_ke3u`QHh?}5hPK5^W>9~z1 zdL!)~$Ml{)Z-IS9NK$Or6kSUmdf5|Pd^X*>QsAEaZl_w#-p;PPtII#=KD|l{8%-nI zRdA^B{rCOSH~_RInbJ3G-9{42^s<4rKhh39g4H0SM{N zY4>~)JLor>Aw6-aVWI^2IE0xT3Ib21JTn>SI!-QZ(Ax@$1hn5W?Vzd}>zsyM2T=vR zZ28A>s`H?;pz)K7sXOE_2GNfBdU1>!=`+$BmS7Z1$c+wVg^kEqc!a!QnCRVm5hIv( z@}Gm!uG6%#+jtibkeK>E%Rsv#!$y7|5b;s-wzw=*L1wT!#C?rIUfObuK zs?riE!q-58GfvZ*0_HQJPMNa_&g-QK>)rnx^0$?Ea0tI%h)8MTV$8hyo|XpE_R;?*%lLn})FLlVW50}Ad2bAbxWKQO1h zhYv;^p7|!z+447pof0MI&?C@~lBa**=0n~bFDaO$c3cbc^Cu_X?ty{MPFO)c zfeD=DXfZ!0t!91uf2w=}(_y?7Fcgs8Kqa2_$g=;>uz3Syv9)hR!-tv(0`^d&#I8?x ziqUL?4zKtc`i4RJd_X_Rbxxh_Hvxr`9|6|8GhTOz!B{)W&qcrUz3_j#rF*_0saGv-WmBR-9?JQ^RHixKt^O)=EX3=k zt^KJI42?2Y*q_o*TD3-E^E=!Fg!vEj#w zKnSS+^OIY3G(4X5f`Px1eeNEBg6_>mC+}9n9{1h({-Fs=-y`|ZFKg4bCCr;3ApQ2I%t{uyAIz$mKYg+DnkA5o>s>1b z4x3e}ea?{VZ9~}nC=;RYQPlBjeband-_^;o{3C%h6o=jSY;GwdcH8LOi$~NT&NjDU zo0?>`acnF_!y@h<@~mUJh`ABLeKSJ%Je{3xY(*{KyT{8pO?<3e>vtLl#Nkz@W*UD7 zD>g-4n#d4V0N8D;I3b|2RaBvag`;Viv`9Ol{4+$lNm&IBHuChuw2tF9B(yksmPcq% z=y>8<4ImWm#mN75F9sF*hzVCn9p!Lqe`%Gy5HJ`7L%y^FCOWmF}@2Up-)IELqW!fBL!f=%jTE9its<`d6 z4RpKcJKmrRvDop@&DLi&&7?Qm+*nkL|3L>rvofspfG(4D{lj+whC{~Q^PjX|vR>!!2*kWr1XQz` zlNahhg>NoG58;GP0iX^IZ5@u~hZ?GO6U96OR%e2TiyiGc(v`tYWd5$f6-0K`@WLsG zkH=y&A*5J3w0`HTW~%M))WFlY=Yh}7VOaI2ZNbMF-=49P9eFv~*Z%mw$ZW&n){V}+ z$uB$M-y^+A8qTorJ3eTJn{GUwSO(>>r#FBHC*WIyC->ILTN^>y**-o7VRO~?4#XnV z%b=g)yt*b(4Ho5ZA@CQvUUeTYJ!l@ICn|pUnJ#~Px3zphb$%xJo@%FSLqyX8M!s-H z!GA(gzy3Brvoy!j%B3?YrzwtODN{Q>khfZ^W|`$qqeDGrOL}j)?wvjO2wK0y z|DJz!AB^Fs)cuAoj_R2sm$r|h5YIqOK)5KAe|wNU%)kMc{QNBMKMQ0~MV>R9Ap0cQ z9)A$|`i#Yp_Raq-flVy(+7v1ciiQT-F0I*hC-i|LL%t&c_U!oRGg2)p4a=A0u;Em5 z>))DYdQfK=5xt3ms6j;aKh`RG881JYD(cY!_iF{E47IxlnCQrq-rMX_7Tl6mQ&YI0q>V-_Z`*?QN{4FG^>cmRtTLFt7) zgGO)_L1hRx37J7HZbwKqKFn z*Ue?5ez}?vRDvt%RWO(@UkxPbNKOhpQ#rQwo27n`Dr{{fys`g{!~ns9aA|YO0Fqe~ zC|@wmoyF(@2n;a2!g_NeubQ7&*c!KX_l1)oONlGiGuL;s7O;1DUw$gLvDzD+aQ{yd zGPE^C)XScr()WT4IG6AMg2!wRdD*t_+w@gDYX@}Jz}Av z&ks~>RP#}$g34u#hg*{pmuXl$Bpx$;o!xbxu*ydkq_$q6L`>iH*`q^k&j?{X%fXu2TnS&JElEP>)!`w4noP;0CHa7d%nFIGl}-q?H6 z*!AJ5t+sBOLs#cvp2V!^`05qhxs?TFm%yKjw7A-ZZ_v}pfb_~rlsvSX{(ZBNHrKNi zciQdr=hRaFi_!n$l#l7+e`jctV)|N&ma!ISrH*~lV1R^=y#Q^!b_9hE^XW;a#XB*_a~#MASeX&P$I~d6I0~ zdT;&tz}5}6m11un@P((AI)hRKzUaIcCn}fcQk9NOW|qzBei*;hyOQ_eBV(-iYk@Imb7!@`l>Nob>cirr ziG0MdFFNKi_;+y{hB4udl(NSgCtLejQ%tXm3qJD!jBh?Te#rj|a5BCpAg`AYid}Eq zhKW$TFJH?NL#}nm>5DD)W^k5`S7sHh zXT)0cdxw zM}uGcS1r{M_@RW_mAQT+do7O3yrW$JM9Uu#8GNCj&J=a-RW)?>`CPM@XnH!LH0^dt z%`5a_{oG@^tQ(oH8q+t7688A}qp3`}^+12WQyqYq?!D>XnGeM|?AQvo|A89b?dIeITF|RsQ^y?LWfk(a(s2 z3IYieng2JSq1!_?=Pj! zP|RxSKSOEiiG=jo+1a}@@-#&gAB?E&;};SV@+0qv)kRn!QuLbFxgN;-r>!k57|7Ap z8-HK(qCfv%XHl8xlV}`HPlK8J!ong0I~lA<6NT@$6_7rD2J2TRvHv;rip=>AqfGj* zO-MQHW;o>91vRA#rN1nJ?dttsXny0h*}DIgWu|s9R!6YK<^|9mQ9EofIBluH-b(R% zaKLDbZ0RsqtV`{~VefS1M04c!@8eKqV#E zi)4LWdievwNpH4LG=I!L*&EUTu>5wzHkPA2pg0D!+2Z#`T{~^7t;IggmiGe)@+dO; zziR>Yh}(uyCL9Ujknv&iz2P%h(gG}Vr`Mly-Ro*STi>16I!7BZWemFW2V}llK#6)Ay( z+^hPhjD*i6bWw3Rx;>w%RLc_ANw;*+^m-(!#aGJ`sx3omH#g>-tlqd{N!R_*!5p3^ zRDnn*P&6k{gkx|~9Z}8Ct*cu9Asso7q$SkJlg1N14lVvRfWDlK*?FAK@PYaArJvDg zor%y(PHp?W{W4kAH7ZTjGWa_an%&S0&SKSO7T&-9=*wXo*}s;PsURM`MZ58?PyOUd z0w*8RaXXg29{4O0y3PU!s-~B$oLVM*#sHdPBR2qfo^@da#y8}?Vx?~Wqt5sbzauLI zuy86=XK`&Z9geDJDJ(NmdDwJ#Y<>ImhGp)=uY(wp)38n)M;m-Hot+Ex!2WwAf9|bW z!wHl*A^v7u#l7)7zXX5DwgijMT(*CS1i7)hY_tq5J-Tf0C7N=7r)Y-xA-{i*nn#Xj>Ekfes zo-wtm>Z{N9OX2XNY^x6CfiG%(>O0-j<8)IAg6qFKdkZk&4a;r+0p;f zB@Nr2g`SiT{Nh{PLC4=j{O{n(*l1uPG78=M%-tO;>j@PG97W&r~ zOZ-Q+D=PQ$iRu67CKCz9oyYCQ84S2tNGul^R@matl(D)_n=G0R8;ySI)!j?1%@}v- zsy_WHZ6HNV=l>W)oN@H%fd0vPf-{3Uur{t~_=i9Izda93UYIn88D`XfLuef#uW`YG z&|(=3TX0Hx$ahz?CbaTZ`D8&du)a)q^}SQjdFE=Scjy_pN6|xRnp&~vp!#=|7wMYk z+4uZ54JJG32_UqNnA+1nE>F7{Ij2zATqf!V9u4Q`9=)pOpGwx1>+y-mG?4jXhSQ5D z*`hn-b>9!v-<}S^&banlJ8`!nO>eqx?$%lxSG+=GnWPz;B{0^6w;x{oE>L}#vVPUb zEdkI=H9)RK*GHKN=WZy0_j7Ft?*l}uYAduh1XS3Fj*WHX`Tt~io`>G={^&}%wE^RQ zP4cM^8uk(0STwLqgrG^EO+V7T7c@mBo#_`3hCEZwdlwNI9rBR8TY|;B^En{Ob~{7c z9>7u3@AO+YGGJiKIJoL8Tay_2YK(o!*&Z{}@b9j&M~ML55_w{VCK_9(Ke8arz8Myv z0a~u3Wr!u)-CfnAP9usNo?A4RC|bO-4wiNVRQr=Tv>1dsFBd)PW@~jzcZSC=+)9s2 zfSOL6z+w5^XlA}n+Pl`Ilt0Olb(pPo0#y;}^wwN=d5mcd;FQ`rcs46$9wD`4{EyAa zXD05}&xaZ)X1kBH-T`EBmSUvyg6t=w0g0!P&T|YWI8BM<6qBpuoJJI)x>4aZo!7|7 z0x)E79o+Lf78U5upPwxu;e3TXorC$r=L%=z@u0pJ=<3j3gpf3b@>!1HeF+#;^-xyU z9ReXQT?Zj6&$DR$li)W`H-iV25GMu1MCF}lLkbHm(^-P=Hf?~_7wvOz}F!e2FWc0 zJv8Tl%qFSh?v3JqC7HxJ35?0X)V{SjCPo=~oAw`iA+MKIN&d5C0C{8nS;apO<9IHw z4w-WO6Wpc`HH^K*TrM8R(^hmHD_qK7ejK2MV6^mG+^%(<}7^!NiBI(>Kbs*z?!|0r;-B|w5Up$Ne)d2%&$ zrhkTSeSdd@69*1iIjNTE09TN z_7?Bj*oC9Lu)arh@3j)NiT+fKQkv>&_eD2N+5eR(L>S~GD7aJS zV_Cnkey|Du!(NtUK%oRBf9)w%{5js+c}*oCfzpnWF0r-sMTK?hJQ>7?9kec|sU2d& zIDV^`@$<#D#%HRu>(5!zNo6UvF29y$4VrXS*Q$~^c)~@w{dI#Az7af%V#M$bN<#&X zK4n#56&@lnk+F(@uE*!)I!XUCTvlMe8x99Gc}oOM%bKocnt(kfKVmH=<@*_eo?U#>1cf6;b5a^i=eXxMBa3am1@uqRi9>APHh%p_ zeklSc8cy1c+MU~I_`5yq-_kR%<0|HsZ|cUTW*CnTnKQMO(JlSDQ6VIlY0g?;=jbhPcOO$K-fn?0Jah0v2 ze`2FxT-){HS(59Ys3^@BgyOT`XUiG^^+(zB`Nt??y4j$I zz{p7Wrv&vW}yj?rbR#B-~cuW0lW0~AFS2n#|$C|96Klp z@46Y5=Eq@Z$u#ZD;LjY|+Da0yGRhTrPQaGEEWNL9lUhd_rP9r)$7PfgLJnBokw?%q z5i@AJd#fZshJ6CHm8y!xE4+qUc}f2^(v1dN;WS zvScZ-<&zf8$)S_FN(-+V#B-&b7?2EUJzvpx1-;`0OCo5kvcF!G4s zm0}2>6%9>peN}&A#RZ|$>R*W0>kkTi0wRq6n4!a)sZcD__dTkdQ3(^ty_twxACQ}v z80Xv2@aHxAn1G6j^zs1Wi>MkJGk=HW&d(Rn`id%th+#A`QX*24r6x85$WYJ&)8oh3 zDAT%|SG9H*iDuOA5yc3Z+D_%|gd}9q_aj4WScy|owU3O^LkQJ5K!^N>X`@<4yZQW9 zmI0tDJ~;|@rk3?-2QzbYyeX$R#c4A6o1(mVK@wCRUXv3TdvtLTX$VvFMh4~@u5BJO z64oukqW#YTw()R!QKEoS-+++52UZJYh08O-#XhzjpR=zKL{dKs=Ye?*NSfg&nF^dZ zfc!gotJ;xlM<%VyQP$Gmrh2Eo4fxqHpW8s;0ChC}NHA;UCtkUE#z+AHW#66Ks=)i^ zfE=5Vp|akeVYsA7RU(9CuLPTh^Y5M!$=Cr=&dG%U)>enZhq5`AF zuLU>yoczq4=6^lImqgrn>^|L#)vG`g`;>dI=;d(O1=F4>ZQzJjmL(L z;ao+wJ)1j?dfG!oHtLTKm4bv=n12M5LVTd#bV2AApXgw*4Qp{h zsWUGI{>(L{5@iZ3q9jv&0(jvyJ-*!t6VuyQkQeA^(EV15-#qTo2g3S6nGzzfhL|Bu zsoJz3;W8}h6n+#c(*qZwm#PIc;A#$s!^rLjxJ}K_s)4Q09^GS*@8?24S8ib%E+739 z%3t@WqSeJpg8Igt?bC@x-oOaj6oY0}2-7wT1?yA`R6>2(CYuF!r zTq0v}^s=s3uveF z0-pjg{cmo2t#91*!?t!{$b2S0j*s*`XxhM9kW6Yqe zC1)L%5yinm0TA{l1&hWl4~Ql{|8OgCeP_M1k!;L1xptl!VdXeI^V_t^&P7qaZXPRU zK`a9<<2H6ngV(eaoUR2vPS^LH4c{RXRekD<1qFS=>sCtf*8|$cE{tX@Ljq9c{L<1b z>EIrRQ=%sr^rJ87Fp~zu=1jsvLouHs-)#mQ4_}5_-UxBkyv`>sC?KacMSkAA$Nj<< z?)T8V*|-0N?SP1kbVQe+d^Aa6(mk3ZU))*nFn{S%QA5i_k>5JQUw%Z$$>@PH>U+Q) zX6Acpt(Vc$_bZ9&=a%|e<_9d|8|_h4N>XP*HGU=HHjuw3Y#UtEIS7B-Oi`ITBxLYR z>74qufARa3a<5)F|Dyhxzd?K|Vs77j{WV}MgWXSsU!JXO#CgVRCcBRD*eJT1KvzW` z??qbC&*iUP;YL$?pz*Tj;?O*pygHuE+<1BXDP!f|>!Y(Fwl7JiO;;Azh)S3lM0)=7 z5m8r+g1+|wjzvl9FAg*2%Noa>((GYxd5=&(Z&@!-5{hWHD4i5J*&0R#Xh2oBGqW`w zGCVnglcZZ~U^kh&i+&~Bu&Sav=i3C^iW=^pT{mW; zFUYQuLV!i$%mwIb{GeR7UAPCI@ao7;h*8KhcSskQ_o;7i;IY;9d!jtlJDSr!%Gf54 zTI$n_(-WAF%A8$e5Xlps^Iw!cp!;_)sF8NH;9}#&hFrkJYJ0ey?Vpjd3 zk(sm;9KC0E|8{!)9wv7;cFNn?Ph;z^kgfH*9{(cV(7|Z?E%TfIseeVq9>?EAd)L18 z1_y3DdXD+!uU?~2<5<&})YFSR0Rzj!Omf_7dnYswF%MPuQC<7(r&mB7Y${`uX}8nX z*g}1n{Oua%=?K?dGtJAOA^3C4qG(|aoBQq>I>K8kffxU% zQ-_`}I1n_;_mQw1I%6-i)=QHfb+_16&g&HPnO+k?dAjLHsze8H))UkB!ePbt!d*3X z^v@~9vMu~6p-U03JA@cM>ifJN4UT0{$l6-2E>KUyY8or)(qoR&(ZY@;RI%DPsJi-I zugMq&)?5O*{50tOP|y%<7rmt0pfI71JhglonvZf&WtMb-PTTf zL?jiG6+{mOb?iV)H28g5F=Ft817C7>8yqKR`Po>UP1)SVUZ9`y1uxAD<-$8{ufQD}_{vWXfx`)zSkAaKXl0`5Qg`c{uj zj5n+!ItgVIO&-{LLt(7HL_C$4f|wp1BChuhV66XW?#5K<5V?gyzScjv+s1yuF4+Lq zJojTVoeh=|^RC;vr+L=qx~&$&M8Kb!M9uv(><$(gZ7sn7R69$EN7UhFcaGTtsJr$7 z3vKLWpW4Ts2ovi#M%ksODJGue$A5-{%QvP{z^K&=w`N;)aa~D^Ryxuf)!cO=@8~Ft zDS3&4wsar9xdiz|jmK&{v3^G!f@-=L@U4w1Y;z4n#_;W1ZK73>8K8auVx?OA2dk?{Ts&GJy-WxDEO zbkS`;6H|@fVxzDNC|Ejca6K$tUq@u4+*0i2!xtY-Dh@Xb10wqmyVK~VdYt)y= z`22Z^#n0YCY*Az)wuCqZadxD4tl%ANX>3h+AKMJr2%IdAGk{HjrP4tMtnb0$N2SlL~?8ZZoU*_tkW1l ze;(q)`$lh=TsxTe@}P{kXX;Y9f>UiRJf=24$Z(s>5>BQP!JWc&;?wtu_U=mqT7RZW zUUzuX3s-o`^X=jC;a>+#cgadIdJ=bQ{x0HgxweRC&UMtbq)65x^*4n~+-T~BMl*q# zIl4Vybol;$G6jEScAbPD&HKRR-FOgDHJ=T@r>~d#^UesW&)=%)qMjr)26?tiQ!>R4 znt5T4P#J5A+wQq|vK+8{xdlIVfDNp;$?F*OQCVZ#s)cJHqY%b=ru-%56#y%7BqB z6tB3Wx)tuEdod?6Ip=z{Uf}CY4`1Q@i#Rfb|Lk#E!gJ!XU}Kg7UsXWQR5nHz?j4Yr zCH=7(-I0Gw7?skExAYTUTly{%_nhguxDdxx#&Ejlufgsh_89#J1#Mt{3Q|Rl8E1lv z;Ozpq6n*M!fC47glo?w=Av-pg00$45+;3$( z(P#aO%i*HbLU8}N)J-H77o(9KMvmSs3H7}Qz0zdy-j?;9O_b5Ic8*AkNbGtf=9PH0 zM?|Mm!m?f|i^2yLXDMl~f?;2=TS@w%+vF{s^PivZZC8RE?AE88mDpluqKXXlWLp+1 zKT#DyTUD(~`ieR}s zrl8MeqME~wxz2;>sl-A7P9Q-<69rM^F=Tq;fSQpFYV~wo&2Ae2er(xj4j&jenukG5 z$gvG2%20^P!XL6Q%LGRd(o-QyRoT%zyf#qicNXqquz)13TgROF=ql6!L0lWc*qKPu z_SYhglPu|OsRdR!QBo5gqNX%cJRTNu8KHwlzKkEsTs}(8rL z4rVg?giWrpKI$*wUylQP-dT7cqW_M_HXXW2k$2@ja+TJ9=DVD_IpxN}<5(4mf3BzV z(tcJZz54MQ>TB91fUC1+$S;=;zajad*=UglphH(H_~PnQq6|u%g9JFte}d_73W~Bp zVRi5m$#_s`K|UgM2he;GGgIHHi5gGGG4tPrf5uz})p-{jSKXZL)y$RqeR$q*T}Y0i zc}^`KvMn)0e7*MD{u*yO?<4tJUg4h2L{Kk#tkrd+z%TmjA3I;`10`SgIAQazowGZwr8K zNF%an%uV5(Itp6|ao-7_9w6;CZ8jtp5D#MG7*M6Se%~n*e#U;cdHmXBDjM{Q(+~O) zWA)_jC@028ajYSOn2T-P_h0a_Wz0x!SY3?OCTh$W=f4A+aN3^*?z;{~pTaub4vni! z_q`}N`mnIdo-Z2vAYKf!@KIe7P#OvTsSs-+R&`9NR+8DykJbC`?-T0GB zv#yoKyxc<`f(7(V8Zxv+o)(jfKlZ3U43CqLwShe%Xb)yX3%>TE&_MC`l~`0u7!aBN;2s^K%82D1IMs;Bo!t#V^tOQ8p}9ll?KzvjK^pGZ}X$H(JrdCC;1E_57rOo*l7 zVI~x<+5B9q3Gj@|I?OhB0m-VhITDxDgv<&d&DNRce% zrC`M_P=Sw)E`uA@ku*VtSI525$z})T=Vtk)G5UFgR$dCwN$vyQTHeKIZ>KfeziKt9 zl7;^9h$dS)!{i3tN~;9x9Ia2^lK8&vzuY)$PFIDv3qtjVi@m6{zwLUfqBf+)(ILe` z9oK9PHcG(MBsaMB;=AwX$ArxI`f%pqI#8IYD_>f|rahNXNjPQ*>mU zFK}s(jL2HUZ5p80t`~Z~fb{0R;#~k8G*A>lybqTyM!emcHfT$N6!J}Geve{Ð(G_E z7vRM>zZiMkw7Jt+IPEP!4} z2Mw{bEIGbYpT#+5%J+RYaE3sAQosU*EPBxR_B9iSAC+{J4Au~rbB3L{w#iuTRA=4S zcQH1q5wzKFBf(eGWqSD_W@_pvaEiw>yyw5B1e7UlzlSZ<_bMp%Z=4>|5Pt8Oqo@I! zvlSOsoUeAxvB^39A~u3a_=kqLa+6J5u8A0y!MVhr*w8)y{s2 zwb4vhvVrzAYIR0CpF)^vbGos^ku)wgIhCF;$LPqTdVP%>p)ilB^uy1g1Or_$wL$O3 z(?H?4$ec`y2_+9Ii7(Tg#7`Mt$a>~u`PjkyswHT)9IRH^kp9<3hVK_09;LX(b5927 zhKAUlPt-z>KYf%<8zW(XeEtlI)=4GBFn5_1R9?+{P^6}>xg?X#5)^JG=t)YKC1eSe zk{I%Bo-WjnV-&ov`Q|CL9X;(-g!N116fxZe(hB81qlc2iITdUtkeLGed;8@Iew{Nj zW^Iu|oKhgHI;X$#c4duno-&oQjC9AMocMw!+S3<50O@Z1 z&aCnt0wuxHy5f>ogQfzfvf?AnnsmYAwIRuRp0LZ}cN z*rwS73#(@_-Uwn0=9GH)ce>hd@GPh%f$3~VETfnEX=}bCFL$3D3)Q9E9z_I4&Q!N9 zo06&3n#%*yJ23MHxQiAj)Jr_RA!XRHe}+ZY-dUM5HB<&1$G%ipQrn8fn89T9M3I;T zZJgUkn9*-1ZmCG`uOX*M$OeL6?3d*UFnM~U4n7()+QAM?n)0!lG`fIRPU0X*%uyWR zR7W@wZ#?p#L%xr%a)Up{yuo0&UuLS+gJTz6+5>eiZmf~kZMkWYns zR_0rw-4v;vG;c$$d?p6uV!ULz2{=%EjCEl!s${O%Gq>Oc8yeo&?bv`;=C&O5Bg18) zRJ3iOB;izF8ukBCkssUTc8|4)8K8?%EKp`|R1{GRSa!nm6%*@$Ot*kbaTqnhx4n<- zUeL-9KH(hihAJ^{r8I>jWU>0NVWyMAS zt*PB?Ex_=KI|NU5<>^MwI@kguJLolh)4Rm9ic;g);hy~iG9p97Xt60e%J=PVGdUl* zNxCQq)g8A6LRcX`PY+Xw_O!vpFbbdVt8!!fSjo|ZO!^dC@yT2yW}ITtTprKDXk1>K z2c;`6$+1+9O|Ih%O0OKjSjurbBp=fV@|8PKbi^1jw8$S$T=qMl=e3Ywcwl7jqKh5+ z;9+Y$pwwFgLFg2lIFvfJip9BdGJRdhEiebsWefaeTFm;xH$2SO*ID`wZWRtXXg6My z+KQGK z%JZndr(eS{VDRnGNVw>9P4R!j>fC{$VQ6jFdnQ*;TDg3)B)9qm49lFS0U7lT%rqs} zDPjPJa2Vj#S~A`E#GKq(0h00utQc|nOJ#9_a9KQ)MBt7wxxoa~2ky_8OhF_W)ig+MF^^n?MZ0zedZsNuk5lovM$q z-8376-TeLG9$b>Czu%{y$iPX3&xG*m&79aErB1vC=S5Y_j|;O}&|cawP7 zyxk0*sB$=`MvGv(W=pqxyFD*KGZagQ8Lu3)32M}J5@eR2Q&bs*@85nMlTaQIA3FO& z=T-6c+Li{1i+D(M1 zE3YAtB}!QgrZS~XFZlYfybdyvTJmV-?%!CSegx_%PIyx!Q-;teNdU~&!GEgTtze(;Lw@mhqLcu1eyAq$n|)% zS}zvIgorN{vwV&uGjuLHLv&ja77w*AzLZ*AOF-jIYEauoO@(U!bGFFb1h^U{&V0tV z7GIAYo4l{+{T3doR$*_AU*VWRD`!Vz9o7J2#uP+o2Y3*} z0ZNzMCfDD0r8I|n|ES35uDjn=Zk99p^rq+7Z}gaL-3kq|^W6_AirN(LA}5T(0IX&4ZsGCg4*6e?;6*SMj%aa6p%^|NdxmRM+x-@q_(RMSsBCE{4}U4Z}$;hWNIStT!pw)Z4n( zBm%v|9v{oIm?=X9?V#nsB4^YxH){%CVecTnaG0%sQu3Z}W0@~cPlG+O2iM2xJh+uz0+&6Krbur5 zXGHR2RU2RIiNUv?xCEnvajPw{ig{QF=R9q4dV{IJAF>My`Cm@JHL<(5E^!IXbWp_< z&yf)_-vRbWmyt}5juLjfroOhR+e_{{`|5t?kZ;;0uraOTV7t>dj%5{xkNTMbRX7lw z18hBoX~w<^K3d+j0?Js&iQ_mHj^heE56d2`Zws>cJXiF7@OCSPjDQO3qwb?sxkKLI zOlX(CX(X3aU?=iC$pgk&qdIWUbkMe`VI{sHAdHMS9#1OWyyN!WGb!1*S(xfVU&pEp zMa}PPd%C$}@_YQBBm`{~lPAFO1Qw@HQh`X;>yzLn-JerR1hCw_MAA(mNd^nVoDMoC zLzeTO^K=aua{B>k<-ZjHr=B!(FHh*jZ zrf~(B@hKN8^k9C@G*{TAXJ78%%LDH$F_Yh}(X{#r)v3W)aq_yh0X~WQf$c*bNT6D9 z`x#U=M?#0N5&0`tn?yE(pXZ;lrq}Khx%S<5HW^EEc&@L~f@>)6h_I z{k%LH+pF7z#LXSzUp3=CS>H*oPFf*I)QBSr24cvwSe0Nxao1Oqo3_Nr=q2xkuD}f& zgHHH>uN&@A{caAwL!yS(S+7_lw z!->c0O0V9tWgoA;fD)x7sg1%=Z^&XH6IjjOWrV5|9v;}TR7YNSLKgkfu+#QQx-GvX zz2dp~BXT}1Xgea8M};i2pG!rr!tt|H75Bg|1SClrrEUcj3fOEbN;HK6 zk71MC>FAsq2Z6-Tk$50|JnvA+QSjnxw!vzFXE6sO!rdbKiD8sPKiF+cMphKQjpcmn zebL#%3^?3=ynHt%TzVYNt$!c*ov>&UD)-$BCsjhVo5Q*qj>tZ@z4g8aA`%{JR{z7-s zSbaD%Z2U*Tm6_e==VxB2UwgqeG4xU!(5U!fAPxVrF6UJ37oIN+3Y$dsZlX=jtW4Q> z=q@3Ogo};oJ&E9+z0@e0oLWZjuUZH|?X)SP$@Q~xrj2`z7IT&EzKNrkC^m2GqC$Y9 zT!s&x%;1QA3fIm2HQEaDy1ffX`;1g|U0GSjD$xvHV^G0C3&jJ`0}L90U8@5!rH)2n z$?PM+xKv+W1UVM3RIy_fQ*DeLVfEl!`9DFg{S?!24QEXGgja#VYdHVmC{Olt_~`fA z%V%S&Ka|HBE+-=?n0&99iU#`&5l9RE%Bx>W~@$Ytw(kv3D&Ev4JwABxMDBW?Q%{r9-Qch0k6S5aN55tXrLTNg&F zc66kVd-L27S`YB>gDn-`DGGQxLUjUwqDFZtqTrjC4hn1_NYPk$E31Dnx$IHIy%K0N zc1E*t_I}YHlpC6=CrHpLYpMQ#XlPDWgHo{TncBQs3j34Bf8sEWu6518C_)Q?ap zAc5j45EB$>so5iFt^+|H}{J{O(s>!vp^4~uc?ff{zjHuaAVyZ#?OB|Iy zP89IPPe@bn%FR_-Cyp{h`8iBT^mm^k-ABfgVe&Wsq|JroZMduEW@xu5i;E#Pq!;HC z2KU8=pU0-{z!hJ)Nc!#hn+o^%XBsz}`o344>;B#y^kY(Omls69#PmpsnD>f1&t{5h zf%0g{qZmzZ(n70nxXgoR(WJgwy+f{$)+bobNhWdG=PlvmeXj)~@DQtV3}a6CZ?3<@ zZ}|S_^C{YqJEu7~Y&M`{>E7KhQaWClf%2Tu%&Zq!^>XZanUUJclPDDq?L=XNSic5e zs0BmdK1{^lbNh^%JcCaW^=tQ;Sn(-}O@fn$j)45f=k8d8W&WFu0)0W}s;<+Bu8$-T z+9`66s~M6ZEmNHn?QbyY17Iq7yV>0Icx4ka^N4pXzaq0DFNU+PPnMqXg%s{!C+_w< zCRLauM#)wBCf6P^^cDBWRKCb=Y?G>`Yt2~)=6Lk&Pu#-wo!_oeu}09CJ`%c%4HkVf z9h$-s5&UMOS?vY&%Z%{B^pMnJoOZU)ru>793A5DCYB#L7|B&vUAKq$7Glc+z)ag%7 zhxWF|Q)iU4U>jH*-3cxKzBW@4i1_NMeMs4Pz2^F!-@0(w=xYJ!hn~JKk*T!U%0xyPhWR%e(|Cp zO1BtF-`jgo`x9VfVD~2W0-TYuPe>Vu`I^9i5CA&oHAJQeIMI%ekCR>?H;*h9R9{y# zxVSq_4hh1p5W}awGj$&>YJZmLJ`%F;AJ;j;VIc7gP?T~h(u-()WX#uuzVJ*}w_f2P zC6@@i3iO2Cd*HyUX6uL#B05AQL!r3DyY2MfvRHoxUI|H za|$I5pk#fUjYHMuz7G5R9*i^)c!=tl8iFfwoa#_S5l9Jt!syvpMaNgOYZT^H=*z0 zWNdY0&ja#jQS|ybSn5PTEw6Ahcc#5O)Y-a5uku6Uz3Sd=GNYBD6mI!MIfplb>_6S{ zOK+1KX~)kkwW^g;A460TTgf%0ocpiYv&v?Ll^$lZ0J8D7_f1d{)RHl=j8&-~DjHJ= zFwDSvnm0Bin03eELpk#CI}e1RzVUR+*4%@fB3{rW&ty7tgZG>>){O0F8Lp;uL5^02 zF##sykL7?EAru@Z`l;hsU2H43h1y6bUy6unTB6+NoUxz96>vj}xmu6fft8b?-fi`u z0T>=($o$_abCo2Si6aBf&uPDPb1w$eV*^V?>kpx`z2#^2dsbVybtc zMzXRP&0$rsO>%>i3a&@kuJUlS8b`VYyry$Zz!A1|W1**nOC&E&7Ku;sMw>3E#v=Fx zza%lbk9ryKM|l^<<|*qS6H9kOWBpA_w1^}et@0K80)n)Ao?vxGCj;yLF!>b~M;t5- zY*0>H=aa2d4&I14b{8IBIijgne&s3$ihInpYUeKcdDf*ZrBtdC6O#9fkvzeL#3c9n z9R$4mK6iu_x-KSb14IA-3DiYQ(ZpE7OB8dgWzg8Tz{(36ny-)oZ>8nXZ2gcG5_6x{ z21#!|3K@><^Fh1T>!(WV+AKYN9?3C-L4^}bqSrZ3{dD^a_$h)hnv?AWbKQBN$jNTK zsw$G$qO<)SkXD8CfYor>=x=Y<&% zF?=<{{Px?VOMBj@a$SA*hfvZW?1w|u$i;I)7oM}rJF2t+>)sG830Jb2_zy6K9{b5r zh}DP!E*@7IQ1oNQ3L*jJTb8-a739ZzSyIr4-Wj}H{;3@+)fQbZ8%U}?1(YUZy+=Dz zm<(eyenlh908xiPE{e^%0}E@8C3bmJam5{X-&^$=j_Tj5pS6xuYly8rHiM?&>r^kx zsU@G4)2h2Fo;eZe_cP|*zp;2_)nq<>t69gpRBX;OEFeo5;>T{VoEWUP*N%q~oSL2C zopxO$jmK|V<8N9{uJa3ZJTDWGhbYVwl2bPw@a{)8CEiIjr#hzwPd5~eL}L=9LJ5!r zv+uCW)*wp?%lN^eV zrK>>=tVRm8+rGFZoHQ?jX##0?-^N85$;>)^_3{(M1u?J=;y3=qK-&80keNXwqfqUp zs6aHe3Nchm+K$a0q>#)9t-8L4rx47?RZglf7fbE0S&5Mz!{O4rJ8J|jnL3w^JXVC;TXT~}$pr)cpw_NzCu%-Pp?Id9L#Zg$(KO%zTS zs#`^JYBYOxhh-0~PrvtEg+a=-UC6)e%Pwugl;;=}-r*s&NH*~U6_w$wy_+|VQL?d@~g}pe}^diI?OMAG~hl`4w zNygkZhPZX^4K-?iF1-<7Jatlqzw0z^yexyVRD8_srr*JP?_GUuyFw77j@A zN-$LmGi+$+i{?cD$yI^hw{@Me80Zn7l(@xl%^8aE0f^r^l4alDy7h^y@z)gsVA;|~ zfjr3;-89rpPX6)Io2~9T2Bwp#_BZ&k_vDUN*v_Ot1DEl`Pt}_W&$UcZm<3%&+^qt^6Y}z6J=4IYpb=Tobvbe#w`eTiD|~j z@u#aF7@Ke&DM@H~vD0E|5{GNh=_(QCnIO1pUqW31B;DNS%G>t8$Ta%xm6(kz>MFhZ zLa%(@vG`Y+>(6u{QTYg9z##LB7XXTZ?>01l7ZZ-hAGw;RIT>G-<#-}sgJ#2RCta0z zMDs7}ralMg0!{{R-(s$IX)pO4W^+fwTkbVA4MG(G>l5cYGCvXiBc6jHz$LlL-BqJy zIkb1LF*}pEI2b4*;~xSFH*vRxP&ie^?VizBkD z;`mkxY;PZ1?UdzF6b2~2sQ&%daIsOxP;Fl+g*#*S?dog&w>3Bq%>#9I9F;6vTuu7}~}DCPR*w+eK0(^`09un=pI-U{OoC%A99Qq%g^;v1OO4Bm70g zWsQ31AVNrLJ+ProCEtv=hxf((qUT4g`OWH|gs-?nd{r4Kgj@!SW#t~S0s%evcmz9A#ORrZK|4f7c2xkl&%Dua~|yrKJ@!>%kCI3oAzO!ht!Xa zwyM{9wF?wY9Q=h2q}4yX0$k<~FLszI%**Cv?uwlm+G?cr!CW$)iP*f>k+=ls%6!0e zFP-(@yc*dI4;eVN)r}F6>HF=v(^dx0mLEueow{nDZxc^a?()_TYAMK~#Jg@f8tHFC z@JYr`@8l2T)!F%>!lt7oElsODe+$IG%lwaYT-rznM4{nbjMEm=g5j2Od?IdbKN2?O z#AIM3s%DS?(Sn4AdwHToiazBaUxH`tFflsi68f-QIlD6n03DIfH_+BRq!y@=cVMnA z`mNnKlK7ZsVklGX?B=j-?Nyn9V|x$r`*%OYcAGz00Awy#*Mu3K7DnQM&=*ebBT5fX zM@MrW$Fm{{tZUN}%IbnXdwIMe60mdsdQN@~J2GRT_i9?qECR`ZS%4l1YdFZ5!A#*1 z3t$AG#Q@(c^(N%U)*%`eesNmCz-jf|Yevwd^Lt#Eo?5k6d;wMkcL(=V)uM=LZT#Ys z`DjI>D9TCW(97e97y0rb?DPi|Ox?T=;6~z+4U3Ud!bj8+eJwn;yjvD8o9sT8>%RbB zDi)WI$Xdq{#7#T!@HdLecb^WOXtZB`17l%?1p;}<_~TsvmOP)DodDSnpC&wXSXLg! zCNdxW-Hn6Iz8u2~P!PJ^Z>jWEIHn3kuqe*M-D4Qx`N!>B>)C$~)&Y}?M4(c2X_f{5 zY#uMUjGJgf@L<)UKkZ|pj~)DSzwO=j_Ulp#6H=2F*N^T)-B88y4R^}7S<+)bG_(1; zCw{9xQ+7B4`Oe8_Eix0}5_DlXkF(v8T7a36r#tJ!EsX#vysMM-8O?vU9nw#sJUO}) z8Yq?wE-|ju!WTXf`xpWhVYDf<8&o3MYEbx7!8Zu+ z-fX)V=CxRSy5sMk%D)r?b9>4Jbxyk%!De_`Q4~cK!G_u~5N64Fj?8z~*p0GjHIL=O zb&*|@iA;di46dEsNG-d~6Lmtn!y_&L99uV_(M}%`By`VFr;X1qe5T3QXD6TE>o5{F z)B{!JC_>CGmdAtlKy;Icw4ZZwUZRSCzCrh|C=Y{9aLx3==95r<{5AD=+qu|R?{8>$ zwx=do=gB&2Hu>zg(=z^Y;o4C*;-WTw1t<8lU%TxE>D3#*a3^rw00UE`_CTmIMG!fl zWyw?Ow83lt6L0`*o+8g_fo=m#qyZ_4))F^i8zPr>7`kjvr6#G2j8qY4zRUmzm#yaEtx$qbW-j zUZ_K#E1|}){l2T$b;7|IqXUwUxV5t$SmY#(d4z>Ls&VMw@UvHrTON6uZ9f4lvo+h{=8!qP-Vs zOzH@MgRZ~U+?xPm#nL&Q52IckHut+`h;m+~><>n!YRP0~iHK}HGYK*l0ZdPTp_bb- z0Grpf`CsrQj2d}*Lpp}a?30$0PnC{Oj<**GIce0tMBW+~r0w8@a`p}l+ zN_afO6p~;F21S?S&y2$M4Zvdh4cq;7nS%nDbWU%FIsJD51rm{rOTaJa&Jl1K`88LP zZT@S`!OJCEi@?R{`?~+ZG67>DVEX$KH=fw6fZn&4Bi;`oJcTlA3f{ZchLkSCDyLmZw;_tEd;blN_ z=C0Tf{xKYzXTJ1FU>0@_8n;GpHF=9&zf|vwRW|ef#WMUBnC&urVcq8Sj(|nQ{p(p{ zx#h@++H<6^sOV1N(#3g$%i34`UYZ8K`NB@fL@&LlrmXvTnQntTiBPv!u!cbR%D?*- zI5J@Y2*@xWrV9dbbM%`{i%W1G`)kWpT>KYkUCv{fK*BA?4&wIGQyWN7KyPVyu`NNZP`Mio2 zrC02|dghEO%{~UGN?r6>HrN{N^d6lXmo;sdSJZR-1C6`;USXq3y27}mOk5hCz1?}g zdG&`{2-$(fQ!|1)qhEzy&QJwr*qGJtPbvCeR{>-aacmTcI#CEPTTmnH=|YxVikh+O zrq6+0Ii*j5JA35*ga3XzhJ{=Lo6Y#iv+bFd*q}xyRQD{Q>cT>~sU$$VYw(NeNzI=~ z-E;;FD<&3`7?fVwk3jUy6@cY)k++*<9sU1&6aw=f{@8z71)d`QbN;Ur0o?z0%lucX z9iT${ucyFi9~=0||F$ISGARFV3q!G7!vD6&DTh=3i}LdC_OJvj{{Q-q-!)SKMrHqg i`Cmr?{Qqyi&{gPnc74@niJy({E>%rQOYsxb3jTjPUWX(A literal 0 HcmV?d00001 diff --git a/images/krr-datasources.svg b/images/krr-datasources.svg new file mode 100644 index 00000000..00c86eaf --- /dev/null +++ b/images/krr-datasources.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/krr-other-integrations.png b/images/krr-other-integrations.png new file mode 100644 index 0000000000000000000000000000000000000000..a342af24fe7eae0b3664b7f1491b61f9fb6b3220 GIT binary patch literal 29457 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From cff614ef034195e7e68c0da883a4ddfc67895911 Mon Sep 17 00:00:00 2001 From: Pavan Gudiwada <25551553+pavangudiwada@users.noreply.github.com> Date: Wed, 10 Jan 2024 15:04:50 +0530 Subject: [PATCH 011/137] Updated k9s instructions --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 03366ad8..1a783d82 100644 --- a/README.md +++ b/README.md @@ -558,8 +558,13 @@ customPlaybooks:
k9s Plugin - Install our k9s Plugin to get recommendations by hitting + Install our k9s Plugin to get recommendations directly in deployments/daemonsets/statefulsets views. + Plugin: [resource recommender](https://github.com/derailed/k9s/blob/master/plugins/resource-recommendations.yaml) + + Installation instructions: [k9s docs](https://k9scli.io/topics/plugins/) + + Follow the plugins installation guide: here
## Creating a Custom Strategy/Formatter From 6e6b193662092f9d080801adfcd356cf4b23f637 Mon Sep 17 00:00:00 2001 From: Pavan Gudiwada <25551553+pavangudiwada@users.noreply.github.com> Date: Wed, 10 Jan 2024 15:07:57 +0530 Subject: [PATCH 012/137] Fixed k9s plugin docs --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 1a783d82..ad7f1ae4 100644 --- a/README.md +++ b/README.md @@ -563,8 +563,6 @@ customPlaybooks: Plugin: [resource recommender](https://github.com/derailed/k9s/blob/master/plugins/resource-recommendations.yaml) Installation instructions: [k9s docs](https://k9scli.io/topics/plugins/) - - Follow the plugins installation guide: here
## Creating a Custom Strategy/Formatter From c4453f33a8cc9e43c5d318827e1d909e8055dd1a Mon Sep 17 00:00:00 2001 From: Pavan Gudiwada <25551553+pavangudiwada@users.noreply.github.com> Date: Wed, 10 Jan 2024 15:13:20 +0530 Subject: [PATCH 013/137] Fixes broken table --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index f625aacd..dd16f268 100644 --- a/README.md +++ b/README.md @@ -358,7 +358,6 @@ Find about how KRR tries to find the default prometheus to connect Date: Wed, 10 Jan 2024 20:44:32 +0530 Subject: [PATCH 015/137] Integration changes --- README.md | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ad7f1ae4..6c718bae 100644 --- a/README.md +++ b/README.md @@ -62,16 +62,28 @@ Robusta KRR (Kubernetes Resource Recommender) is a CLI tool for optimizing resource allocation in Kubernetes clusters. It gathers pod usage data from Prometheus and recommends requests and limits for CPU and memory. This reduces costs and improves performance. -_Supports: [Prometheus](#prometheus-victoria-metrics-and-thanos-auto-discovery), [Thanos](#prometheus-victoria-metrics-and-thanos-auto-discovery), [Victoria Metrics](#prometheus-victoria-metrics-and-thanos-auto-discovery), [Google Managed Prometheus](./docs/google-cloud-managed-service-for-prometheus.md), [Amazon Managed Prometheus](#amazon-managed-prometheus), [Azure Managed Prometheus](#azure-managed-prometheus), [Coralogix](#coralogix-managed-prometheus) and [Grafana Cloud](#grafana-cloud-managed-prometheus)_ - ### Data Integrations [![Used to send data to KRR](./images/krr-datasources.svg)](#data-source-integrations) -### Integrations +View Instructions for: +* [Prometheus](#prometheus-victoria-metrics-and-thanos-auto-discovery) +* [Thanos](#prometheus-victoria-metrics-and-thanos-auto-discovery) +* [Victoria Metrics](#prometheus-victoria-metrics-and-thanos-auto-discovery) + +* [Google Managed Prometheus](./docs/google-cloud-managed-service-for-prometheus.md) +* [Amazon Managed Prometheus](#amazon-managed-prometheus) +* [Azure Managed Prometheus](#azure-managed-prometheus) +* [Coralogix](#coralogix-managed-prometheus) +* [Grafana Cloud](#grafana-cloud-managed-prometheus) + +### Get Recommendations [![Used to receive information from KRR](./images/krr-other-integrations.svg)](#data-source-integrations) +* [Seeing KRR in a UI](#integrations) +* [Sending recommendations to Slack](#integrations) +* [Setting up KRR as a k9s plugin](#integrations) ### Features @@ -355,7 +367,6 @@ Find about how KRR tries to find the default Prometheus to connect Prometheus, Victoria Metrics and Thanos auto-discovery By default, KRR will try to auto-discover the running Prometheus Victoria Metrics and Thanos. -For discovering Prometheus it scan services for those labels: +For discovering Prometheus it scans services for those labels: ```python "app=kube-prometheus-stack-prometheus" @@ -505,7 +516,7 @@ python krr.py simple -p $PROM_URL --prometheus-auth-header "Bearer ${PROM_USER}: ## Integrations
- Free SaaS Platform + Free UI for KRR recommendations With the [free Robusta SaaS platform](https://home.robusta.dev/) you can: From 6ae218bd1757a7a496584566c6b973dc5915a03f Mon Sep 17 00:00:00 2001 From: Pavan Gudiwada <25551553+pavangudiwada@users.noreply.github.com> Date: Wed, 10 Jan 2024 21:01:03 +0530 Subject: [PATCH 016/137] Minor fix --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6c718bae..203917a5 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,9 @@ View Instructions for: [![Used to receive information from KRR](./images/krr-other-integrations.svg)](#data-source-integrations) -* [Seeing KRR in a UI](#integrations) +View instructions for: + +* [Seeing recommendations in a UI](#integrations) * [Sending recommendations to Slack](#integrations) * [Setting up KRR as a k9s plugin](#integrations) From b278e4dc03a16e131e24f34cca78ea33e911c7bf Mon Sep 17 00:00:00 2001 From: LeaveMyYard Date: Wed, 10 Jan 2024 18:09:26 +0200 Subject: [PATCH 017/137] Fix proposed from Incorrect iteration over multiple selected namespaces #178 Co-authored-by: theboringstuff --- robusta_krr/core/integrations/kubernetes/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/robusta_krr/core/integrations/kubernetes/__init__.py b/robusta_krr/core/integrations/kubernetes/__init__.py index 46970248..e9fb41dd 100644 --- a/robusta_krr/core/integrations/kubernetes/__init__.py +++ b/robusta_krr/core/integrations/kubernetes/__init__.py @@ -171,8 +171,8 @@ async def _list_workflows( tasks = [ loop.run_in_executor( self.executor, - lambda: namespaced_request( - namespace=namespace, + lambda ns=namespace: namespaced_request( + namespace=ns, watch=False, label_selector=settings.selector, ), From e575f240eef127bde5826c0c1987374caf7f80e3 Mon Sep 17 00:00:00 2001 From: LeaveMyYard Date: Wed, 10 Jan 2024 18:24:18 +0200 Subject: [PATCH 018/137] Fix --resource flag --- robusta_krr/core/integrations/kubernetes/__init__.py | 2 +- robusta_krr/core/models/config.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/robusta_krr/core/integrations/kubernetes/__init__.py b/robusta_krr/core/integrations/kubernetes/__init__.py index e9fb41dd..aad7f958 100644 --- a/robusta_krr/core/integrations/kubernetes/__init__.py +++ b/robusta_krr/core/integrations/kubernetes/__init__.py @@ -139,7 +139,7 @@ def __build_obj( def _should_list_resource(self, resource: str): if settings.resources == "*": return True - return resource.lower() in settings.resources + return resource.capitalize() in settings.resources async def _list_workflows( self, kind: KindLiteral, all_namespaces_request: Callable, namespaced_request: Callable diff --git a/robusta_krr/core/models/config.py b/robusta_krr/core/models/config.py index 5a26c490..223b5dff 100644 --- a/robusta_krr/core/models/config.py +++ b/robusta_krr/core/models/config.py @@ -92,7 +92,7 @@ def validate_resources(cls, v: Union[list[str], Literal["*"]]) -> Union[list[str if v == []: return "*" - return [val.lower() for val in v] + return [val.capitalize() for val in v] def create_strategy(self) -> AnyStrategy: StrategyType = AnyStrategy.find(self.strategy) From 120ce8e8b32264a91198ff822e90209f8a2530c2 Mon Sep 17 00:00:00 2001 From: Pavan Gudiwada <25551553+pavangudiwada@users.noreply.github.com> Date: Thu, 11 Jan 2024 12:35:34 +0530 Subject: [PATCH 019/137] Dropdowns --- README.md | 73 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 41 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 203917a5..a378fa94 100644 --- a/README.md +++ b/README.md @@ -66,26 +66,16 @@ Robusta KRR (Kubernetes Resource Recommender) is a CLI tool for optimizing resou [![Used to send data to KRR](./images/krr-datasources.svg)](#data-source-integrations) -View Instructions for: -* [Prometheus](#prometheus-victoria-metrics-and-thanos-auto-discovery) -* [Thanos](#prometheus-victoria-metrics-and-thanos-auto-discovery) -* [Victoria Metrics](#prometheus-victoria-metrics-and-thanos-auto-discovery) -* [Google Managed Prometheus](./docs/google-cloud-managed-service-for-prometheus.md) -* [Amazon Managed Prometheus](#amazon-managed-prometheus) -* [Azure Managed Prometheus](#azure-managed-prometheus) -* [Coralogix](#coralogix-managed-prometheus) -* [Grafana Cloud](#grafana-cloud-managed-prometheus) +_View Instructions for: [Prometheus](#prometheus-victoria-metrics-and-thanos-auto-discovery), [Thanos](#prometheus-victoria-metrics-and-thanos-auto-discovery), [Victoria Metrics](#prometheus-victoria-metrics-and-thanos-auto-discovery), [Google Managed Prometheus](./docs/google-cloud-managed-service-for-prometheus.md), [Amazon Managed Prometheus](#amazon-managed-prometheus), [Azure Managed Prometheus](#azure-managed-prometheus), [Coralogix](#coralogix-managed-prometheus) and [Grafana Cloud](#grafana-cloud-managed-prometheus)_ + + ### Get Recommendations [![Used to receive information from KRR](./images/krr-other-integrations.svg)](#data-source-integrations) -View instructions for: - -* [Seeing recommendations in a UI](#integrations) -* [Sending recommendations to Slack](#integrations) -* [Setting up KRR as a k9s plugin](#integrations) +View instructions for: [Seeing recommendations in a UI](#free-ui-for-krr-recommendations),[Sending recommendations to Slack](#slack-notification), [Setting up KRR as a k9s plugin](#k9s-plugin) ### Features @@ -380,8 +370,10 @@ Find about how KRR tries to find the default Prometheus to connect (back to top)

-
-Coralogix Managed Prometheus + +
+ +#### Coralogix Managed Prometheus + For Coralogix managed Prometheus you need to specify your Prometheus link and add the flag coralogix_token with your Logs Query Key @@ -498,8 +499,10 @@ python krr.py simple -p "https://prom-api.coralogix..." --coralogix_token

(back to top)

-
- Grafana Cloud Managed Prometheus +
+ +#### Grafana Cloud Managed Prometheus + For Grafana Cloud managed Prometheus you need to specify Prometheus link, Prometheus user, and an access token of your Grafana Cloud stack. The Prometheus link and user for the stack can be found on the Grafana Cloud Portal. An access token with a `metrics:read` scope can also be created using Access Policies on the same portal. @@ -517,8 +520,10 @@ python krr.py simple -p $PROM_URL --prometheus-auth-header "Bearer ${PROM_USER}: ## Integrations -
- Free UI for KRR recommendations +
+ + #### Free UI for KRR recommendations + With the [free Robusta SaaS platform](https://home.robusta.dev/) you can: @@ -530,8 +535,10 @@ With the [free Robusta SaaS platform](https://home.robusta.dev/) you can:
-
- Slack Notification +
+ + #### Slack Notification + Put cost savings on autopilot. Get notified in Slack about recommendations above X%. Send a weekly global report, or one report per team. @@ -568,8 +575,10 @@ customPlaybooks:

(back to top)

-
- k9s Plugin +
+ +#### k9s Plugin + Install our k9s Plugin to get recommendations directly in deployments/daemonsets/statefulsets views. From d7ddd4c5ce7886b5bd990b8e1682513ea698b304 Mon Sep 17 00:00:00 2001 From: Pavan Gudiwada <25551553+pavangudiwada@users.noreply.github.com> Date: Thu, 11 Jan 2024 12:52:17 +0530 Subject: [PATCH 020/137] Minor fixes --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a378fa94..485534ae 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ _View Instructions for: [Prometheus](#prometheus-victoria-metrics-and-thanos-aut [![Used to receive information from KRR](./images/krr-other-integrations.svg)](#data-source-integrations) -View instructions for: [Seeing recommendations in a UI](#free-ui-for-krr-recommendations),[Sending recommendations to Slack](#slack-notification), [Setting up KRR as a k9s plugin](#k9s-plugin) +_View instructions for: [Seeing recommendations in a UI](#free-ui-for-krr-recommendations), [Sending recommendations to Slack](#slack-notification), [Setting up KRR as a k9s plugin](#k9s-plugin)_ ### Features From d9202caa1396e4774912edda073e3764b5c4087a Mon Sep 17 00:00:00 2001 From: Pavan Gudiwada <25551553+pavangudiwada@users.noreply.github.com> Date: Thu, 11 Jan 2024 13:04:43 +0530 Subject: [PATCH 021/137] Added spaces after markdown --- README.md | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 485534ae..3e4b2094 100644 --- a/README.md +++ b/README.md @@ -369,7 +369,7 @@ Find about how KRR tries to find the default Prometheus to connect -#### Grafana Cloud Managed Prometheus +#### Grafana Cloud Managed Prometheus + For Grafana Cloud managed Prometheus you need to specify Prometheus link, Prometheus user, and an access token of your Grafana Cloud stack. The Prometheus link and user for the stack can be found on the Grafana Cloud Portal. An access token with a `metrics:read` scope can also be created using Access Policies on the same portal. @@ -517,12 +522,14 @@ python krr.py simple -p $PROM_URL --prometheus-auth-header "Bearer ${PROM_USER}:

(back to top)

+

## Integrations -
+
+ + #### Free UI for KRR recommendations - #### Free UI for KRR recommendations With the [free Robusta SaaS platform](https://home.robusta.dev/) you can: @@ -538,6 +545,7 @@ With the [free Robusta SaaS platform](https://home.robusta.dev/) you can:
#### Slack Notification + Put cost savings on autopilot. Get notified in Slack about recommendations above X%. Send a weekly global report, or one report per team. @@ -577,7 +585,8 @@ customPlaybooks:
-#### k9s Plugin +#### k9s Plugin + Install our k9s Plugin to get recommendations directly in deployments/daemonsets/statefulsets views. From bf6264408e12e295d29883d225f41f1ccac11891 Mon Sep 17 00:00:00 2001 From: Pavan Gudiwada <25551553+pavangudiwada@users.noreply.github.com> Date: Thu, 11 Jan 2024 14:25:29 +0530 Subject: [PATCH 022/137] Fixed random spaces and navigation --- README.md | 65 ++++++++++++++++--------------------------------------- 1 file changed, 19 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 3e4b2094..f237ef20 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ _View Instructions for: [Prometheus](#prometheus-victoria-metrics-and-thanos-aut -### Get Recommendations +### Reporting Integrations [![Used to receive information from KRR](./images/krr-other-integrations.svg)](#data-source-integrations) @@ -369,11 +369,7 @@ Find about how KRR tries to find the default Prometheus to connect Prometheus, Victoria Metrics and Thanos auto-discovery By default, KRR will try to auto-discover the running Prometheus Victoria Metrics and Thanos. For discovering Prometheus it scans services for those labels: @@ -412,11 +408,8 @@ If none of those labels result in finding Prometheus, Victoria Metrics or Thanos
-
- -#### Scanning with a Centralized Prometheus - - +
+Scanning with a Centralized Prometheus If your Prometheus monitors multiple clusters we require the label you defined for your cluster in Prometheus. @@ -431,11 +424,8 @@ You may also need the `-p` flag to explicitly give Prometheus' URL.
-
- -#### Azure Managed Prometheus - - +
+Azure Managed Prometheus For Azure managed Prometheus you need to generate an access token, which can be done by running the following command: @@ -458,11 +448,8 @@ python krr.py simple --namespace default -p PROMETHEUS_URL --prometheus-auth-hea
-
- -#### Amazon Managed Prometheus - - +
+Amazon Managed Prometheus For Amazon Managed Prometheus you need to add your Prometheus link and the flag --eks-managed-prom and krr will automatically use your aws credentials @@ -486,11 +473,8 @@ Additional optional parameters are:
-
- -#### Coralogix Managed Prometheus - - +
+Coralogix Managed Prometheus For Coralogix managed Prometheus you need to specify your Prometheus link and add the flag coralogix_token with your Logs Query Key @@ -503,11 +487,8 @@ python krr.py simple -p "https://prom-api.coralogix..." --coralogix_token

(back to top)

-
- -#### Grafana Cloud Managed Prometheus - - +
+Grafana Cloud Managed Prometheus For Grafana Cloud managed Prometheus you need to specify Prometheus link, Prometheus user, and an access token of your Grafana Cloud stack. The Prometheus link and user for the stack can be found on the Grafana Cloud Portal. An access token with a `metrics:read` scope can also be created using Access Policies on the same portal. @@ -524,13 +505,10 @@ python krr.py simple -p $PROM_URL --prometheus-auth-header "Bearer ${PROM_USER}:

-## Integrations - -
+## Reporting Integrations - #### Free UI for KRR recommendations - - +
+Free UI for KRR recommendations With the [free Robusta SaaS platform](https://home.robusta.dev/) you can: @@ -542,11 +520,8 @@ With the [free Robusta SaaS platform](https://home.robusta.dev/) you can:
-
- - #### Slack Notification - - +
+Slack Notification Put cost savings on autopilot. Get notified in Slack about recommendations above X%. Send a weekly global report, or one report per team. @@ -583,11 +558,9 @@ customPlaybooks:

(back to top)

-
- -#### k9s Plugin - +
+k9s Plugin Install our k9s Plugin to get recommendations directly in deployments/daemonsets/statefulsets views. From 45950a753eebbd5c510fba64e6ec7ae7e708e665 Mon Sep 17 00:00:00 2001 From: Pavan Gudiwada <25551553+pavangudiwada@users.noreply.github.com> Date: Thu, 11 Jan 2024 14:29:34 +0530 Subject: [PATCH 023/137] Final fix --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f237ef20..d4544f3d 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ _View Instructions for: [Prometheus](#prometheus-victoria-metrics-and-thanos-aut ### Reporting Integrations -[![Used to receive information from KRR](./images/krr-other-integrations.svg)](#data-source-integrations) +[![Used to receive information from KRR](./images/krr-other-integrations.svg)](#integrations) _View instructions for: [Seeing recommendations in a UI](#free-ui-for-krr-recommendations), [Sending recommendations to Slack](#slack-notification), [Setting up KRR as a k9s plugin](#k9s-plugin)_ @@ -505,7 +505,7 @@ python krr.py simple -p $PROM_URL --prometheus-auth-header "Bearer ${PROM_USER}:

-## Reporting Integrations +## Integrations
Free UI for KRR recommendations From 648421205c4482d2a884f35fdb32525b52736278 Mon Sep 17 00:00:00 2001 From: Natan Yellin Date: Thu, 11 Jan 2024 11:28:25 +0200 Subject: [PATCH 024/137] Update README.md --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d4544f3d..15a57765 100644 --- a/README.md +++ b/README.md @@ -103,10 +103,14 @@ Read more about [how KRR works](#how-krr-works) and [KRR vs Kubernetes VPA](#dif ### Requirements -KRR requires you to have Prometheus. +KRR requires Prometheus and [kube-state-metrics](https://github.com/kubernetes/kube-state-metrics). -Additionally to that, [kube-state-metrics](https://github.com/kubernetes/kube-state-metrics) needs to be running on your cluster, as KRR is dependant on those metrics: +
+ Which metrics does KRR need? +No setup is required if you use kube-prometheus-stack or Robusta's Embedded Prometheus. +If you have a different setup, make sure the following metrics exist: + - `container_cpu_usage_seconds_total` - `container_memory_working_set_bytes` - `kube_replicaset_owner` @@ -114,6 +118,7 @@ Additionally to that, [kube-state-metrics](https://github.com/kubernetes/kube-st - `kube_pod_status_phase` _Note: If one of last three metrics is absent KRR will still work, but it will only consider currently-running pods when calculating recommendations. Historic pods that no longer exist in the cluster will not be taken into consideration._ +
### Installation Methods From 61dd99c004782e186205256e2d94e0aa5d2bd3e6 Mon Sep 17 00:00:00 2001 From: Pavan Gudiwada <25551553+pavangudiwada@users.noreply.github.com> Date: Wed, 17 Jan 2024 17:15:11 +0530 Subject: [PATCH 025/137] Added KRR run command --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f625aacd..47ac9d87 100644 --- a/README.md +++ b/README.md @@ -119,11 +119,17 @@ brew tap robusta-dev/homebrew-krr brew install krr ``` -3. Check that installation was successfull (First launch might take a little longer): +3. Check that installation was successfull: ```sh krr --help ``` + +4. Run KRR (First launch might take a little longer): + +```sh +krr simple +```
From 54e70935191cf9630bcbf5c94488d0d8e2742fe4 Mon Sep 17 00:00:00 2001 From: Pavan Gudiwada <25551553+pavangudiwada@users.noreply.github.com> Date: Wed, 17 Jan 2024 18:16:09 +0530 Subject: [PATCH 026/137] Fixed typos! --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 47ac9d87..26d58326 100644 --- a/README.md +++ b/README.md @@ -119,13 +119,13 @@ brew tap robusta-dev/homebrew-krr brew install krr ``` -3. Check that installation was successfull: +3. Check that installation was successful: ```sh krr --help ``` -4. Run KRR (First launch might take a little longer): +4. Run KRR (first launch might take a little longer): ```sh krr simple From 6fa8e2a4c8b5fc77686bcf193545edd7df1e66a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20R=C3=BCger?= Date: Thu, 18 Jan 2024 20:57:07 +0100 Subject: [PATCH 027/137] Fix Typo --- robusta_krr/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/robusta_krr/main.py b/robusta_krr/main.py index 8c19f1bf..b01bcb4e 100644 --- a/robusta_krr/main.py +++ b/robusta_krr/main.py @@ -79,7 +79,7 @@ def run_strategy( None, "--resource", "-r", - help="List of resources to run on (Deployment, StatefullSet, DaemonSet, Job, Rollout). By default, will run on all resources. Case insensitive.", + help="List of resources to run on (Deployment, StatefulSet, DaemonSet, Job, Rollout). By default, will run on all resources. Case insensitive.", rich_help_panel="Kubernetes Settings", ), selector: Optional[str] = typer.Option( From 41c1e4d728d914da6b562b990dbd25a149098258 Mon Sep 17 00:00:00 2001 From: Vahan Terzibashian Date: Sat, 3 Feb 2024 19:07:34 +0400 Subject: [PATCH 028/137] Fix typo in the comment --- robusta_krr/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/robusta_krr/main.py b/robusta_krr/main.py index b01bcb4e..a8e1c2a8 100644 --- a/robusta_krr/main.py +++ b/robusta_krr/main.py @@ -38,7 +38,7 @@ def __process_type(_T: type) -> type: elif _T is Optional: return Optional[{__process_type(_T.__args__[0])}] # type: ignore else: - return str # It the type is unknown, just use str and let pydantic handle it + return str # If the type is unknown, just use str and let pydantic handle it def load_commands() -> None: From c3a72ce3f9a842de1bca1aacdf8feb569050983d Mon Sep 17 00:00:00 2001 From: LeaveMyYard Date: Wed, 14 Feb 2024 00:06:20 +0200 Subject: [PATCH 029/137] Add Openshift support with loading sa token --- .../core/integrations/openshift/__init__.py | 3 +++ .../core/integrations/openshift/token.py | 17 +++++++++++++++++ .../prometheus_metrics_service.py | 11 +++++++++++ robusta_krr/core/models/config.py | 1 + robusta_krr/core/runner.py | 1 + robusta_krr/main.py | 13 ++++++++++++- 6 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 robusta_krr/core/integrations/openshift/__init__.py create mode 100644 robusta_krr/core/integrations/openshift/token.py diff --git a/robusta_krr/core/integrations/openshift/__init__.py b/robusta_krr/core/integrations/openshift/__init__.py new file mode 100644 index 00000000..c54df543 --- /dev/null +++ b/robusta_krr/core/integrations/openshift/__init__.py @@ -0,0 +1,3 @@ +from .token import TOKEN_LOCATION, load_token + +__all__ = ["TOKEN_LOCATION", "load_token"] diff --git a/robusta_krr/core/integrations/openshift/token.py b/robusta_krr/core/integrations/openshift/token.py new file mode 100644 index 00000000..54a599f2 --- /dev/null +++ b/robusta_krr/core/integrations/openshift/token.py @@ -0,0 +1,17 @@ +from typing import Optional + +from robusta_krr.core.models.config import settings + +# NOTE: This one should be mounted if openshift is enabled (done by Robusta Runner) +TOKEN_LOCATION = '/var/run/secrets/kubernetes.io/serviceaccount/token' + + +def load_token() -> Optional[str]: + if not settings.openshift: + return None + + try: + with open(TOKEN_LOCATION, 'r') as file: + return file.read() + except FileNotFoundError: + return None diff --git a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py index 091041e4..761909b4 100644 --- a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py +++ b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py @@ -9,6 +9,7 @@ from prometrix import PrometheusNotFound, get_custom_prometheus_connect from robusta_krr.core.abstract.strategies import PodsTimeData +from robusta_krr.core.integrations import openshift from robusta_krr.core.models.config import settings from robusta_krr.core.models.objects import K8sObjectData, PodData from robusta_krr.utils.batched import batched @@ -65,6 +66,16 @@ def __init__( self.auth_header = settings.prometheus_auth_header self.ssl_enabled = settings.prometheus_ssl_enabled + if settings.openshift: + logging.info("Openshift flag is set, trying to load token from service account.") + openshift_token = openshift.load_token() + + if openshift_token: + logging.info("Openshift token is loaded successfully.") + self.auth_header = self.auth_header or f"Bearer {openshift_token}" + else: + logging.warning("Openshift token is not found, trying to connect without it.") + self.prometheus_discovery = self.service_discovery(api_client=self.api_client) self.url = settings.prometheus_url diff --git a/robusta_krr/core/models/config.py b/robusta_krr/core/models/config.py index 223b5dff..54a9ed3f 100644 --- a/robusta_krr/core/models/config.py +++ b/robusta_krr/core/models/config.py @@ -45,6 +45,7 @@ class Config(pd.BaseSettings): eks_service_name: Optional[str] = pd.Field(None) eks_managed_prom_region: Optional[str] = pd.Field(None) coralogix_token: Optional[str] = pd.Field(None) + openshift: bool = pd.Field(False) # Threading settings max_workers: int = pd.Field(6, ge=1) diff --git a/robusta_krr/core/runner.py b/robusta_krr/core/runner.py index 312efdda..787c3a46 100644 --- a/robusta_krr/core/runner.py +++ b/robusta_krr/core/runner.py @@ -240,6 +240,7 @@ async def run(self) -> None: self._strategy.settings.timeframe_duration = min_step result = await self._collect_result() + logger.info("Result collected, displaying...") self._process_result(result) except ClusterNotSpecifiedException as e: logger.error(e) diff --git a/robusta_krr/main.py b/robusta_krr/main.py index a8e1c2a8..aef38485 100644 --- a/robusta_krr/main.py +++ b/robusta_krr/main.py @@ -170,6 +170,13 @@ def run_strategy( help="Adds the token needed to query Coralogix managed prometheus.", rich_help_panel="Prometheus Coralogix Settings", ), + openshift: bool = typer.Option( + False, + "--openshift", + help="Used when running by Robusta inside an OpenShift cluster.", + rich_help_panel="Prometheus Openshift Settings", + hidden=True, + ), cpu_min_value: int = typer.Option( 10, "--cpu-min", @@ -206,7 +213,10 @@ def run_strategy( False, "--logtostderr", help="Pass logs to stderr", rich_help_panel="Logging Settings" ), width: Optional[int] = typer.Option( - None, "--width", help="Width of the output. Will use console width by default.", rich_help_panel="Logging Settings" + None, + "--width", + help="Width of the output. Will use console width by default.", + rich_help_panel="Logging Settings", ), file_output: Optional[str] = typer.Option( None, "--fileoutput", help="Print the output to a file", rich_help_panel="Output Settings" @@ -241,6 +251,7 @@ def run_strategy( eks_secret_key=eks_secret_key, eks_service_name=eks_service_name, coralogix_token=coralogix_token, + openshift=openshift, max_workers=max_workers, format=format, verbose=verbose, From 14d72543df8bbde9494fca23961c53230557b1bd Mon Sep 17 00:00:00 2001 From: Bartosz Fenski Date: Wed, 14 Feb 2024 19:22:09 +0100 Subject: [PATCH 030/137] info about excluded namespace --- "\\" | 14 ++++++++++++++ robusta_krr/main.py | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 "\\" diff --git "a/\\" "b/\\" new file mode 100644 index 00000000..8cd525c9 --- /dev/null +++ "b/\\" @@ -0,0 +1,14 @@ +[user] + email = bfenski@akamai.com + name = Bartosz Fenski + +[includeIf "gitdir:~/homelab/"] + path = ~/homelab/.gitconfig + +[includeIf "gitdir:~/test/"] + path = ~/homelab/.gitconfig + +[alias] + change-commits = "!f() { VAR=$1; OLD=$2; NEW=$3; shift 3; git filter-branch -f --env-filter \"if [[ \\\"$`echo $VAR`\\\" = '$OLD' ]]; then export $VAR='$NEW'; fi\" $@; }; f" +[gui] + recentrepo = /Users/bfenski/cloud-observability diff --git a/robusta_krr/main.py b/robusta_krr/main.py index aef38485..83faebae 100644 --- a/robusta_krr/main.py +++ b/robusta_krr/main.py @@ -72,7 +72,7 @@ def run_strategy( None, "--namespace", "-n", - help="List of namespaces to run on. By default, will run on all namespaces.", + help="List of namespaces to run on. By default, will run on all namespaces except 'kube-system'.", rich_help_panel="Kubernetes Settings", ), resources: List[str] = typer.Option( From ef8117c3ffaaf8eb74d5f02458036fb49f7614bc Mon Sep 17 00:00:00 2001 From: Bartosz Fenski Date: Wed, 14 Feb 2024 19:23:07 +0100 Subject: [PATCH 031/137] remove crap --- "\\" | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 "\\" diff --git "a/\\" "b/\\" deleted file mode 100644 index 8cd525c9..00000000 --- "a/\\" +++ /dev/null @@ -1,14 +0,0 @@ -[user] - email = bfenski@akamai.com - name = Bartosz Fenski - -[includeIf "gitdir:~/homelab/"] - path = ~/homelab/.gitconfig - -[includeIf "gitdir:~/test/"] - path = ~/homelab/.gitconfig - -[alias] - change-commits = "!f() { VAR=$1; OLD=$2; NEW=$3; shift 3; git filter-branch -f --env-filter \"if [[ \\\"$`echo $VAR`\\\" = '$OLD' ]]; then export $VAR='$NEW'; fi\" $@; }; f" -[gui] - recentrepo = /Users/bfenski/cloud-observability From 1172ff8499d5411bae71cbe674464880f6e2a366 Mon Sep 17 00:00:00 2001 From: Natan Yellin Date: Thu, 15 Feb 2024 13:33:15 +0200 Subject: [PATCH 032/137] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 37161514..f4e5c0c8 100644 --- a/README.md +++ b/README.md @@ -212,7 +212,7 @@ krr simple
- Tweak the recommendation algorithm + Tweak the recommendation algorithm (strategy) Most helpful flags: From 72789f84e4ccad81f1549479bc9225e223671157 Mon Sep 17 00:00:00 2001 From: LeaveMyYard Date: Wed, 21 Feb 2024 17:14:16 +0200 Subject: [PATCH 033/137] Update dev version to 1.7.0-dev --- pyproject.toml | 2 +- robusta_krr/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 81bda32c..d8896935 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "robusta-krr" -version = "1.6.0-dev" +version = "1.7.0-dev" description = "Robusta's Resource Recommendation engine for Kubernetes" authors = ["Pavel Zhukov <33721692+LeaveMyYard@users.noreply.github.com>"] license = "MIT" diff --git a/robusta_krr/__init__.py b/robusta_krr/__init__.py index 777c8727..3901edc7 100644 --- a/robusta_krr/__init__.py +++ b/robusta_krr/__init__.py @@ -1,4 +1,4 @@ from .main import run -__version__ = "1.6.0-dev" +__version__ = "1.7.0-dev" __all__ = ["run", "__version__"] From 55e2809361396d828a4597c903ae0abff57cc945 Mon Sep 17 00:00:00 2001 From: Ganesh Rathinavel Medayil Date: Tue, 27 Feb 2024 18:18:48 +0530 Subject: [PATCH 034/137] Upgraded prometrix. --- .gitignore | 1 + requirements.txt | 2 +- robusta_krr/core/integrations/prometheus/metrics/base.py | 7 ++++--- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 7fa9806b..02d0ed42 100644 --- a/.gitignore +++ b/.gitignore @@ -132,3 +132,4 @@ dmypy.json .DS_Store robusta_lib +.idea diff --git a/requirements.txt b/requirements.txt index 646fc560..10f8d09b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,7 +28,7 @@ packaging==23.0 ; python_version >= "3.9" and python_version < "3.12" pandas==1.5.3 ; python_version >= "3.9" and python_version < "3.12" pillow==9.4.0 ; python_version >= "3.9" and python_version < "3.12" prometheus-api-client==0.5.3 ; python_version >= "3.9" and python_version < "3.12" -prometrix==0.1.10 ; python_version >= "3.9" and python_version < "3.12" +prometrix==0.1.16 ; python_version >= "3.9" and python_version < "3.12" pyasn1-modules==0.2.8 ; python_version >= "3.9" and python_version < "3.12" pyasn1==0.4.8 ; python_version >= "3.9" and python_version < "3.12" pydantic==1.10.7 ; python_version >= "3.9" and python_version < "3.12" diff --git a/robusta_krr/core/integrations/prometheus/metrics/base.py b/robusta_krr/core/integrations/prometheus/metrics/base.py index ab095f89..eadec78e 100644 --- a/robusta_krr/core/integrations/prometheus/metrics/base.py +++ b/robusta_krr/core/integrations/prometheus/metrics/base.py @@ -117,16 +117,17 @@ def _step_to_string(self, step: datetime.timedelta) -> str: def _query_prometheus_sync(self, data: PrometheusMetricData) -> list[PrometheusSeries]: if data.type == QueryType.QueryRange: - value = self.prometheus.custom_query_range( + response = self.prometheus.safe_custom_query_range( query=data.query, start_time=data.start_time, end_time=data.end_time, step=data.step, ) - return value + return response["result"] else: # regular query, lighter on preformance - results = self.prometheus.custom_query(query=data.query) + response = self.prometheus.safe_custom_query(query=data.query) + results = response["result"] # format the results to return the same format as custom_query_range for result in results: result["values"] = [result.pop("value")] From 4a7b354e30669c05db59a36e8b931c50ffb35590 Mon Sep 17 00:00:00 2001 From: Ganesh Rathinavel Medayil Date: Tue, 27 Feb 2024 18:47:52 +0530 Subject: [PATCH 035/137] Updated prometrix. --- .../prometheus/metrics_service/prometheus_metrics_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py index 761909b4..c628de7a 100644 --- a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py +++ b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py @@ -108,7 +108,7 @@ def check_connection(self): async def query(self, query: str) -> dict: loop = asyncio.get_running_loop() - return await loop.run_in_executor(self.executor, lambda: self.prometheus.custom_query(query=query)) + return await loop.run_in_executor(self.executor, lambda: self.prometheus.safe_custom_query(query=query)) def validate_cluster_name(self): if not settings.prometheus_cluster_label and not settings.prometheus_label: From 17ff834efd155d8d482f9deb36973f8eab2b27bb Mon Sep 17 00:00:00 2001 From: Robusta Runner Date: Sat, 2 Mar 2024 09:04:29 +0200 Subject: [PATCH 036/137] Update simple.py --- robusta_krr/strategies/simple.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/robusta_krr/strategies/simple.py b/robusta_krr/strategies/simple.py index da71531d..f7a11dfd 100644 --- a/robusta_krr/strategies/simple.py +++ b/robusta_krr/strategies/simple.py @@ -55,6 +55,8 @@ class SimpleStrategy(BaseStrategy[SimpleStrategySettings]): History: {history_duration} hours Step: {timeframe_duration} minutes + All parameters can be customized. For example: `krr simple --cpu_percentile=90 --memory_buffer_percentage=15 --history_duration=24 --timeframe_duration=0.5` + This strategy does not work with objects with HPA defined (Horizontal Pod Autoscaler). If HPA is defined for CPU or Memory, the strategy will return "?" for that resource. From cf9533e34d22a45156d3cc56bb5a16defdc50a75 Mon Sep 17 00:00:00 2001 From: Robusta Runner Date: Sat, 2 Mar 2024 09:33:19 +0200 Subject: [PATCH 037/137] Fix bug that caused a fallback to loading pod names from the APIServer and not Prometheus --- .../prometheus/metrics_service/prometheus_metrics_service.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py index c628de7a..4b673094 100644 --- a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py +++ b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py @@ -190,6 +190,7 @@ async def load_pods(self, object: K8sObjectData, period: datetime.timedelta) -> }}[{period_literal}] """ ) + replicasets = replicasets["result"] pod_owners = [replicaset["metric"]["replicaset"] for replicaset in replicasets] pod_owner_kind = "ReplicaSet" @@ -211,6 +212,7 @@ async def load_pods(self, object: K8sObjectData, period: datetime.timedelta) -> ) """ ) + related_pods_result = related_pods_result["result"] if related_pods_result == []: return [] @@ -231,6 +233,7 @@ async def load_pods(self, object: K8sObjectData, period: datetime.timedelta) -> }} == 1 """ ) + pods_status_result = pods_status_result["result"] current_pods_set |= {pod["metric"]["pod"] for pod in pods_status_result} del pods_status_result From d62bbe10d23210d7c0d8e8f7fed233e16b27fa37 Mon Sep 17 00:00:00 2001 From: Robusta Runner Date: Sat, 2 Mar 2024 10:04:15 +0200 Subject: [PATCH 038/137] Fixes #214 --- robusta_krr/core/runner.py | 23 +++++++++++++++-------- robusta_krr/utils/print.py | 14 -------------- 2 files changed, 15 insertions(+), 22 deletions(-) delete mode 100644 robusta_krr/utils/print.py diff --git a/robusta_krr/core/runner.py b/robusta_krr/core/runner.py index 787c3a46..65b38736 100644 --- a/robusta_krr/core/runner.py +++ b/robusta_krr/core/runner.py @@ -17,13 +17,21 @@ from robusta_krr.core.models.objects import K8sObjectData from robusta_krr.core.models.result import ResourceAllocations, ResourceScan, ResourceType, Result, StrategyData from robusta_krr.utils.logo import ASCII_LOGO -from robusta_krr.utils.print import print from robusta_krr.utils.progress_bar import ProgressBar from robusta_krr.utils.version import get_version logger = logging.getLogger("krr") +def custom_print(*objects, rich: bool = True, force: bool = False) -> None: + """ + A wrapper around `rich.print` that prints only if `settings.quiet` is False. + """ + print_func = settings.logging_console.print if rich else print + if not settings.quiet or force: + print_func(*objects) # type: ignore + + class Runner: EXPECTED_EXCEPTIONS = (KeyboardInterrupt, PrometheusNotFound) @@ -58,18 +66,17 @@ def _greet(self) -> None: if settings.quiet: return - print(ASCII_LOGO) - print(f"Running Robusta's KRR (Kubernetes Resource Recommender) {get_version()}") - print(f"Using strategy: {self._strategy}") - print(f"Using formatter: {settings.format}") - print("") + custom_print(ASCII_LOGO) + custom_print(f"Running Robusta's KRR (Kubernetes Resource Recommender) {get_version()}") + custom_print(f"Using strategy: {self._strategy}") + custom_print(f"Using formatter: {settings.format}") + custom_print("") def _process_result(self, result: Result) -> None: Formatter = settings.Formatter formatted = result.format(Formatter) rich = getattr(Formatter, "__rich_console__", False) - print(formatted, rich=rich, force=True) if settings.file_output or settings.slack_output: if settings.file_output: file_name = settings.file_output @@ -77,7 +84,7 @@ def _process_result(self, result: Result) -> None: file_name = settings.slack_output with open(file_name, "w") as target_file: sys.stdout = target_file - print(formatted, rich=rich, force=True) + custom_print(formatted, rich=rich, force=True) sys.stdout = sys.stdout if settings.slack_output: client = WebClient(os.environ["SLACK_BOT_TOKEN"]) diff --git a/robusta_krr/utils/print.py b/robusta_krr/utils/print.py deleted file mode 100644 index 093cc445..00000000 --- a/robusta_krr/utils/print.py +++ /dev/null @@ -1,14 +0,0 @@ -from robusta_krr.core.models.config import settings - - -def print(*objects, rich: bool = True, force: bool = False) -> None: - """ - A wrapper around `rich.print` that prints only if `settings.quiet` is False. - """ - print_func = settings.logging_console.print if rich else print - - if not settings.quiet or force: - print_func(*objects) # type: ignore - - -__all__ = ["print"] From 70263c4afdd525cf9f69025bada5fc468a4a67c4 Mon Sep 17 00:00:00 2001 From: Robusta Runner Date: Sat, 2 Mar 2024 21:50:10 +0200 Subject: [PATCH 039/137] Warn the user if prometheus_url is in wrong format --- robusta_krr/core/models/config.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/robusta_krr/core/models/config.py b/robusta_krr/core/models/config.py index 54a9ed3f..0f22854c 100644 --- a/robusta_krr/core/models/config.py +++ b/robusta_krr/core/models/config.py @@ -74,6 +74,16 @@ def __init__(self, **kwargs: Any) -> None: def Formatter(self) -> formatters.FormatterFunc: return formatters.find(self.format) + @pd.validator("prometheus_url") + def validate_prometheus_url(cls, v: Optional[str]): + if v is None: + return None + + if not v.startswith("https://") and not v.startswith("http://"): + raise Exception("--prometheus-url must start with https:// or http://") + + return v + @pd.validator("prometheus_other_headers", pre=True) def validate_prometheus_other_headers(cls, headers: Union[list[str], dict[str, str]]) -> dict[str, str]: if isinstance(headers, dict): From 356bd75fd550e2affb1125eece4fe0db1a49c5a7 Mon Sep 17 00:00:00 2001 From: LeaveMyYard Date: Mon, 4 Mar 2024 11:39:30 +0200 Subject: [PATCH 040/137] Fix custom strategy example --- examples/custom_strategy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/custom_strategy.py b/examples/custom_strategy.py index c3623ef6..f9e281d1 100644 --- a/examples/custom_strategy.py +++ b/examples/custom_strategy.py @@ -5,7 +5,7 @@ import robusta_krr from robusta_krr.api.models import K8sObjectData, MetricsPodData, ResourceRecommendation, ResourceType, RunResult from robusta_krr.api.strategies import BaseStrategy, StrategySettings -from robusta_krr.core.integrations.prometheus.metrics import MaxCPULoader, MaxMemoryLoader +from robusta_krr.core.integrations.prometheus.metrics import MaxMemoryLoader, PercentileCPULoader # Providing description to the settings will make it available in the CLI help @@ -22,7 +22,7 @@ class CustomStrategy(BaseStrategy[CustomStrategySettings]): display_name = "custom" # The name of the strategy rich_console = True # Whether to use rich console for the CLI - metrics = [MaxCPULoader, MaxMemoryLoader] # The metrics to use for the strategy + metrics = [PercentileCPULoader(90), MaxMemoryLoader] # The metrics to use for the strategy def run(self, history_data: MetricsPodData, object_data: K8sObjectData) -> RunResult: return { From ccce4c09e15e4b6a88b23768910b0138552d315d Mon Sep 17 00:00:00 2001 From: Pavan Gudiwada <25551553+pavangudiwada@users.noreply.github.com> Date: Mon, 4 Mar 2024 17:18:19 +0530 Subject: [PATCH 041/137] Updated default history details --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f4e5c0c8..3ecc09ad 100644 --- a/README.md +++ b/README.md @@ -373,6 +373,7 @@ Find about how KRR tries to find the default Prometheus to connect Date: Mon, 4 Mar 2024 15:22:50 +0200 Subject: [PATCH 043/137] History check (#212) * Warning on fresh prometheus * Fix typing errors, fix running from in-cluster * Update for new prometrix version * Update prometheus_metrics_service to new prometrix --------- Co-authored-by: LeaveMyYard --- robusta_krr/core/abstract/strategies.py | 5 ++ .../integrations/kubernetes/config_patch.py | 4 +- .../core/integrations/prometheus/loader.py | 7 ++- .../integrations/prometheus/metrics/base.py | 2 +- .../prometheus_metrics_service.py | 46 +++++++++++++-- robusta_krr/core/models/result.py | 1 + robusta_krr/core/runner.py | 59 +++++++++++++++++-- robusta_krr/strategies/simple.py | 6 ++ 8 files changed, 115 insertions(+), 15 deletions(-) diff --git a/robusta_krr/core/abstract/strategies.py b/robusta_krr/core/abstract/strategies.py index 204341fe..5b6521e1 100644 --- a/robusta_krr/core/abstract/strategies.py +++ b/robusta_krr/core/abstract/strategies.py @@ -59,6 +59,11 @@ def history_timedelta(self) -> datetime.timedelta: def timeframe_timedelta(self) -> datetime.timedelta: return datetime.timedelta(minutes=self.timeframe_duration) + def history_range_enough(self, history_range: tuple[datetime.timedelta, datetime.timedelta]) -> bool: + """Override this function to check if the history range is enough for the strategy.""" + + return True + # A type alias for a numpy array of shape (N, 2). ArrayNx2 = Annotated[NDArray[np.float64], Literal["N", 2]] diff --git a/robusta_krr/core/integrations/kubernetes/config_patch.py b/robusta_krr/core/integrations/kubernetes/config_patch.py index 5f294bcb..81cd108b 100644 --- a/robusta_krr/core/integrations/kubernetes/config_patch.py +++ b/robusta_krr/core/integrations/kubernetes/config_patch.py @@ -3,6 +3,8 @@ from __future__ import annotations +from typing import Optional + from kubernetes.client import configuration from kubernetes.config import kube_config @@ -25,7 +27,7 @@ def _set_config(self, client_configuration: Configuration): class Configuration(configuration.Configuration): def __init__( self, - proxy: str | None = None, + proxy: Optional[str] = None, **kwargs, ): super().__init__(**kwargs) diff --git a/robusta_krr/core/integrations/prometheus/loader.py b/robusta_krr/core/integrations/prometheus/loader.py index c0995360..df5af962 100644 --- a/robusta_krr/core/integrations/prometheus/loader.py +++ b/robusta_krr/core/integrations/prometheus/loader.py @@ -65,10 +65,15 @@ def get_metrics_service( loader.validate_cluster_name() return loader except MetricsNotFound as e: - logger.debug(f"{service_name} not found: {e}") + logger.info(f"{service_name} not found: {e}") return None + async def get_history_range( + self, history_duration: datetime.timedelta + ) -> Optional[tuple[datetime.datetime, datetime.datetime]]: + return await self.loader.get_history_range(history_duration) + async def load_pods(self, object: K8sObjectData, period: datetime.timedelta) -> list[PodData]: try: return await self.loader.load_pods(object, period) diff --git a/robusta_krr/core/integrations/prometheus/metrics/base.py b/robusta_krr/core/integrations/prometheus/metrics/base.py index eadec78e..f4265e22 100644 --- a/robusta_krr/core/integrations/prometheus/metrics/base.py +++ b/robusta_krr/core/integrations/prometheus/metrics/base.py @@ -166,7 +166,7 @@ async def load_data( duration_str = self._step_to_string(period) query = self.get_query(object, duration_str, step_str) - end_time = datetime.datetime.now().replace(second=0, microsecond=0).astimezone() + end_time = datetime.datetime.utcnow().replace(second=0, microsecond=0) start_time = end_time - period # Here if we split the object into multiple sub-objects, we query each sub-object recursively. diff --git a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py index 4b673094..017c0b25 100644 --- a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py +++ b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py @@ -1,7 +1,7 @@ import asyncio -import datetime import logging from concurrent.futures import ThreadPoolExecutor +from datetime import datetime, timedelta from typing import List, Optional from kubernetes.client import ApiClient @@ -108,7 +108,19 @@ def check_connection(self): async def query(self, query: str) -> dict: loop = asyncio.get_running_loop() - return await loop.run_in_executor(self.executor, lambda: self.prometheus.safe_custom_query(query=query)) + return await loop.run_in_executor( + self.executor, + lambda: self.prometheus.safe_custom_query(query=query)["result"], + ) + + async def query_range(self, query: str, start: datetime, end: datetime, step: timedelta) -> dict: + loop = asyncio.get_running_loop() + return await loop.run_in_executor( + self.executor, + lambda: self.prometheus.safe_custom_query_range( + query=query, start_time=start, end_time=end, step=f"{step.seconds}s" + )["result"], + ) def validate_cluster_name(self): if not settings.prometheus_cluster_label and not settings.prometheus_label: @@ -137,12 +149,34 @@ def get_cluster_names(self) -> Optional[List[str]]: logger.error("Labels api not present on prometheus client") return [] + async def get_history_range(self, history_duration: timedelta) -> tuple[datetime, datetime]: + """ + Get the history range from Prometheus, based on container_memory_working_set_bytes. + Returns: + float: The first history point. + """ + + now = datetime.now() + result = await self.query_range( + "max(container_memory_working_set_bytes)", + start=now - history_duration, + end=now, + step=timedelta(hours=1), + ) + try: + values = result[0]["values"] + start, end = values[0][0], values[-1][0] + return datetime.fromtimestamp(start), datetime.fromtimestamp(end) + except (KeyError, IndexError) as e: + logger.debug(f"Returned from get_history_range: {result}") + raise ValueError("Error while getting history range") from e + async def gather_data( self, object: K8sObjectData, LoaderClass: type[PrometheusMetric], - period: datetime.timedelta, - step: datetime.timedelta = datetime.timedelta(minutes=30), + period: timedelta, + step: timedelta = timedelta(minutes=30), ) -> PodsTimeData: """ ResourceHistoryData: The gathered resource history data. @@ -164,12 +198,12 @@ async def gather_data( return data - async def load_pods(self, object: K8sObjectData, period: datetime.timedelta) -> list[PodData]: + async def load_pods(self, object: K8sObjectData, period: timedelta) -> list[PodData]: """ List pods related to the object and add them to the object's pods list. Args: object (K8sObjectData): The Kubernetes object. - period (datetime.timedelta): The time period for which to gather data. + period (timedelta): The time period for which to gather data. """ logger.debug(f"Adding historic pods for {object}") diff --git a/robusta_krr/core/models/result.py b/robusta_krr/core/models/result.py index 4d3fe38a..2d5ffbc9 100644 --- a/robusta_krr/core/models/result.py +++ b/robusta_krr/core/models/result.py @@ -63,6 +63,7 @@ class Result(pd.BaseModel): resources: list[str] = ["cpu", "memory"] description: Optional[str] = None strategy: StrategyData + errors: list[dict[str, Any]] = pd.Field(default_factory=list) def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) diff --git a/robusta_krr/core/runner.py b/robusta_krr/core/runner.py index fd0c9e07..0fcb8758 100644 --- a/robusta_krr/core/runner.py +++ b/robusta_krr/core/runner.py @@ -41,6 +41,8 @@ def __init__(self) -> None: self._metrics_service_loaders_error_logged: set[Exception] = set() self._strategy = settings.create_strategy() + self.errors: list[dict] = [] + # This executor will be running calculations for recommendations self._executor = ThreadPoolExecutor(settings.max_workers) @@ -73,6 +75,8 @@ def _greet(self) -> None: custom_print("") def _process_result(self, result: Result) -> None: + result.errors = self.errors + Formatter = settings.Formatter formatted = result.format(Formatter) rich = getattr(Formatter, "__rich_console__", False) @@ -149,13 +153,12 @@ async def _calculate_object_recommendations(self, object: K8sObjectData) -> RunR object.pods = await self._k8s_loader.load_pods(object) # NOTE: Kubernetes API returned pods, but Prometheus did not + # This might happen with fast executing jobs if object.pods != []: object.add_warning("NoPrometheusPods") logger.warning( - f"Was not able to load any pods for {object} from Prometheus.\n\t" - "This could mean that Prometheus is missing some required metrics.\n\t" - "Loaded pods from Kubernetes API instead.\n\t" - "See more info at https://github.com/robusta-dev/krr#requirements " + f"Was not able to load any pods for {object} from Prometheus. " + "Loaded pods from Kubernetes API instead." ) metrics = await prometheus_loader.gather_data( @@ -173,6 +176,43 @@ async def _calculate_object_recommendations(self, object: K8sObjectData) -> RunR logger.info(f"Calculated recommendations for {object} (using {len(metrics)} metrics)") return self._format_result(result) + async def _check_data_availability(self, cluster: Optional[str]) -> None: + prometheus_loader = self._get_prometheus_loader(cluster) + if prometheus_loader is None: + return + + try: + history_range = await prometheus_loader.get_history_range(self._strategy.settings.history_timedelta) + except ValueError: + logger.exception(f"Was not able to get history range for cluster {cluster}") + self.errors.append( + { + "name": "HistoryRangeError", + } + ) + return + + logger.debug(f"History range for {cluster}: {history_range}") + enough_data = self._strategy.settings.history_range_enough(history_range) + + if not enough_data: + logger.error(f"Not enough history available for cluster {cluster}.") + try_after = history_range[0] + self._strategy.settings.history_timedelta + + logger.error( + "If the cluster is freshly installed, it might take some time for the enough data to be available." + ) + logger.error( + f"Enough data is estimated to be available after {try_after}, " + "but will try to calculate recommendations anyway." + ) + self.errors.append( + { + "name": "NotEnoughHistoryAvailable", + "retry_after": try_after, + } + ) + async def _gather_object_allocations(self, k8s_object: K8sObjectData) -> ResourceScan: recommendation = await self._calculate_object_recommendations(k8s_object) @@ -191,13 +231,20 @@ async def _collect_result(self) -> Result: clusters = await self._k8s_loader.list_clusters() if clusters and len(clusters) > 1 and settings.prometheus_url: # this can only happen for multi-cluster querying a single centeralized prometheus - # In this scenario we dont yet support determining which metrics belong to which cluster so the reccomendation can be incorrect + # In this scenario we dont yet support determining + # which metrics belong to which cluster so the reccomendation can be incorrect raise ClusterNotSpecifiedException( - f"Cannot scan multiple clusters for this prometheus, Rerun with the flag `-c ` where is one of {clusters}" + f"Cannot scan multiple clusters for this prometheus, " + f"Rerun with the flag `-c ` where is one of {clusters}" ) logger.info(f'Using clusters: {clusters if clusters is not None else "inner cluster"}') + if clusters is None: + await self._check_data_availability(None) + else: + await asyncio.gather(*[self._check_data_availability(cluster) for cluster in clusters]) + with ProgressBar(title="Calculating Recommendation") as self.__progressbar: scans_tasks = [ asyncio.create_task(self._gather_object_allocations(k8s_object)) diff --git a/robusta_krr/strategies/simple.py b/robusta_krr/strategies/simple.py index f7a11dfd..22c6a20b 100644 --- a/robusta_krr/strategies/simple.py +++ b/robusta_krr/strategies/simple.py @@ -1,3 +1,5 @@ +from datetime import timedelta + import numpy as np import pydantic as pd @@ -47,6 +49,10 @@ def calculate_cpu_proposal(self, data: PodsTimeData) -> float: return np.max(data_) + def history_range_enough(self, history_range: tuple[timedelta, timedelta]) -> bool: + start, end = history_range + return min(end - start, self.history_timedelta) / self.timeframe_timedelta >= self.points_required + class SimpleStrategy(BaseStrategy[SimpleStrategySettings]): """ From 50147bd63ea57246d6b2653a849fb16da26f6339 Mon Sep 17 00:00:00 2001 From: LeaveMyYard Date: Mon, 4 Mar 2024 17:54:41 +0200 Subject: [PATCH 044/137] Fix ["result"] usage in metric_service --- .../prometheus/metrics_service/prometheus_metrics_service.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py index 017c0b25..b4285997 100644 --- a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py +++ b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py @@ -224,7 +224,6 @@ async def load_pods(self, object: K8sObjectData, period: timedelta) -> list[PodD }}[{period_literal}] """ ) - replicasets = replicasets["result"] pod_owners = [replicaset["metric"]["replicaset"] for replicaset in replicasets] pod_owner_kind = "ReplicaSet" @@ -246,7 +245,6 @@ async def load_pods(self, object: K8sObjectData, period: timedelta) -> list[PodD ) """ ) - related_pods_result = related_pods_result["result"] if related_pods_result == []: return [] @@ -267,7 +265,6 @@ async def load_pods(self, object: K8sObjectData, period: timedelta) -> list[PodD }} == 1 """ ) - pods_status_result = pods_status_result["result"] current_pods_set |= {pod["metric"]["pod"] for pod in pods_status_result} del pods_status_result From 4c8e727205221f4d38a638b3ed51dea568b26309 Mon Sep 17 00:00:00 2001 From: LeaveMyYard Date: Thu, 14 Mar 2024 13:06:14 +0200 Subject: [PATCH 045/137] Remove aiostream --- requirements.txt | 1 - .../core/integrations/kubernetes/__init__.py | 42 +++++++--------- robusta_krr/utils/async_gen_merge.py | 49 +++++++++++++++++++ 3 files changed, 67 insertions(+), 25 deletions(-) create mode 100644 robusta_krr/utils/async_gen_merge.py diff --git a/requirements.txt b/requirements.txt index 10f8d09b..87b46032 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ about-time==4.2.1 ; python_version >= "3.9" and python_version < "3.12" -aiostream==0.4.5 ; python_version >= "3.9" and python_version < "3.12" alive-progress==3.1.2 ; python_version >= "3.9" and python_version < "3.12" boto3==1.28.21 ; python_version >= "3.9" and python_version < "3.12" botocore==1.31.21 ; python_version >= "3.9" and python_version < "3.12" diff --git a/robusta_krr/core/integrations/kubernetes/__init__.py b/robusta_krr/core/integrations/kubernetes/__init__.py index aad7f958..2c42d226 100644 --- a/robusta_krr/core/integrations/kubernetes/__init__.py +++ b/robusta_krr/core/integrations/kubernetes/__init__.py @@ -1,9 +1,8 @@ import asyncio import logging from concurrent.futures import ThreadPoolExecutor -from typing import AsyncGenerator, AsyncIterator, Callable, Optional, Union +from typing import AsyncGenerator, AsyncIterable, Callable, Optional, Union -import aiostream from kubernetes import client, config # type: ignore from kubernetes.client import ApiException from kubernetes.client.models import ( @@ -23,6 +22,7 @@ from robusta_krr.core.models.config import settings from robusta_krr.core.models.objects import HPAData, K8sObjectData, KindLiteral, PodData from robusta_krr.core.models.result import ResourceAllocations +from robusta_krr.utils.async_gen_merge import async_gen_merge from . import config_patch as _ from .rollout import RolloutAppsV1Api @@ -67,20 +67,17 @@ async def list_scannable_objects(self) -> AsyncGenerator[K8sObjectData, None]: # https://stackoverflow.com/questions/55299564/join-multiple-async-generators-in-python # This will merge all the streams from all the cluster loaders into a single stream - objects_combined = aiostream.stream.merge( + async for object in async_gen_merge( self._list_deployments(), self._list_rollouts(), self._list_all_statefulsets(), self._list_all_daemon_set(), self._list_all_jobs(), - ) - - async with objects_combined.stream() as streamer: - async for object in streamer: - # NOTE: By default we will filter out kube-system namespace - if settings.namespaces == "*" and object.namespace == "kube-system": - continue - yield object + ): + # NOTE: By default we will filter out kube-system namespace + if settings.namespaces == "*" and object.namespace == "kube-system": + continue + yield object async def list_pods(self, object: K8sObjectData) -> list[PodData]: selector = self._build_selector_query(object._api_resource.spec.selector) @@ -143,7 +140,7 @@ def _should_list_resource(self, resource: str): async def _list_workflows( self, kind: KindLiteral, all_namespaces_request: Callable, namespaced_request: Callable - ) -> AsyncIterator[K8sObjectData]: + ) -> AsyncIterable[K8sObjectData]: if not self._should_list_resource(kind): logger.debug(f"Skipping {kind}s in {self.cluster}") return @@ -198,35 +195,35 @@ async def _list_workflows( logger.exception(f"Error {e.status} listing {kind} in cluster {self.cluster}: {e.reason}") logger.error("Will skip this object type and continue.") - def _list_deployments(self) -> AsyncIterator[K8sObjectData]: + def _list_deployments(self) -> AsyncIterable[K8sObjectData]: return self._list_workflows( kind="Deployment", all_namespaces_request=self.apps.list_deployment_for_all_namespaces, namespaced_request=self.apps.list_namespaced_deployment, ) - def _list_rollouts(self) -> AsyncIterator[K8sObjectData]: + def _list_rollouts(self) -> AsyncIterable[K8sObjectData]: return self._list_workflows( kind="Rollout", all_namespaces_request=self.rollout.list_rollout_for_all_namespaces, namespaced_request=self.rollout.list_namespaced_rollout, ) - def _list_all_statefulsets(self) -> AsyncIterator[K8sObjectData]: + def _list_all_statefulsets(self) -> AsyncIterable[K8sObjectData]: return self._list_workflows( kind="StatefulSet", all_namespaces_request=self.apps.list_stateful_set_for_all_namespaces, namespaced_request=self.apps.list_namespaced_stateful_set, ) - def _list_all_daemon_set(self) -> AsyncIterator[K8sObjectData]: + def _list_all_daemon_set(self) -> AsyncIterable[K8sObjectData]: return self._list_workflows( kind="DaemonSet", all_namespaces_request=self.apps.list_daemon_set_for_all_namespaces, namespaced_request=self.apps.list_namespaced_daemon_set, ) - def _list_all_jobs(self) -> AsyncIterator[K8sObjectData]: + def _list_all_jobs(self) -> AsyncIterable[K8sObjectData]: return self._list_workflows( kind="Job", all_namespaces_request=self.batch.list_job_for_all_namespaces, @@ -372,7 +369,7 @@ def _try_create_cluster_loader(self, cluster: Optional[str]) -> Optional[Cluster logger.error(f"Could not load cluster {cluster} and will skip it: {e}") return None - async def list_scannable_objects(self, clusters: Optional[list[str]]) -> AsyncIterator[K8sObjectData]: + async def list_scannable_objects(self, clusters: Optional[list[str]]) -> AsyncIterable[K8sObjectData]: """List all scannable objects. Yields: @@ -390,13 +387,10 @@ async def list_scannable_objects(self, clusters: Optional[list[str]]) -> AsyncIt # https://stackoverflow.com/questions/55299564/join-multiple-async-generators-in-python # This will merge all the streams from all the cluster loaders into a single stream - objects_combined = aiostream.stream.merge( + async for object in async_gen_merge( *[cluster_loader.list_scannable_objects() for cluster_loader in self.cluster_loaders.values()] - ) - - async with objects_combined.stream() as streamer: - async for object in streamer: - yield object + ): + yield object async def load_pods(self, object: K8sObjectData) -> list[PodData]: try: diff --git a/robusta_krr/utils/async_gen_merge.py b/robusta_krr/utils/async_gen_merge.py new file mode 100644 index 00000000..71528953 --- /dev/null +++ b/robusta_krr/utils/async_gen_merge.py @@ -0,0 +1,49 @@ +import asyncio +import logging +from typing import AsyncIterable, TypeVar + + +logger = logging.getLogger("krr") + + +# Define a type variable for the values yielded by the async generators +T = TypeVar("T") + + +def async_gen_merge(*aiters: AsyncIterable[T]) -> AsyncIterable[T]: + queue = asyncio.Queue(1) + run_count = len(aiters) + cancelling = False + + async def drain(aiter): + nonlocal run_count + try: + async for item in aiter: + await queue.put((False, item)) + except Exception as e: + if not cancelling: + await queue.put((True, e)) + else: + raise + finally: + run_count -= 1 + + async def merged(): + try: + while run_count: + raised, next_item = await queue.get() + if raised: + cancel_tasks() + raise next_item + yield next_item + finally: + cancel_tasks() + + def cancel_tasks(): + nonlocal cancelling + cancelling = True + for t in tasks: + t.cancel() + + tasks = [asyncio.create_task(drain(aiter)) for aiter in aiters] + return merged() From 4eb8eef3db0e6a2fdab2fd3ef9802fd02b52b871 Mon Sep 17 00:00:00 2001 From: LeaveMyYard Date: Thu, 14 Mar 2024 13:06:51 +0200 Subject: [PATCH 046/137] Remove aiostream from poetry, sync versions with requirements --- poetry.lock | 1881 +++++++++++++++++++++++++----------------------- pyproject.toml | 5 +- 2 files changed, 996 insertions(+), 890 deletions(-) diff --git a/poetry.lock b/poetry.lock index 3232575b..f0db2614 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "about-time" @@ -11,26 +11,15 @@ files = [ {file = "about_time-4.2.1-py3-none-any.whl", hash = "sha256:8bbf4c75fe13cbd3d72f49a03b02c5c7dca32169b6d49117c257e7eb3eaee341"}, ] -[[package]] -name = "aiostream" -version = "0.4.5" -description = "Generator-based operators for asynchronous iteration" -optional = false -python-versions = "*" -files = [ - {file = "aiostream-0.4.5-py3-none-any.whl", hash = "sha256:25b7c2d9c83570d78c0ef5a20e949b7d0b8ea3b0b0a4f22c49d3f721105a6057"}, - {file = "aiostream-0.4.5.tar.gz", hash = "sha256:3ecbf87085230fbcd9605c32ca20c4fb41af02c71d076eab246ea22e35947d88"}, -] - [[package]] name = "alive-progress" -version = "3.1.2" +version = "3.1.5" description = "A new kind of Progress Bar, with real-time throughput, ETA, and very cool animations!" optional = false python-versions = ">=3.7, <4" files = [ - {file = "alive-progress-3.1.2.tar.gz", hash = "sha256:b22ba960151f582cdd6d489c56462d2f9adca821608075d0d8d2cd15d1b6845b"}, - {file = "alive_progress-3.1.2-py3-none-any.whl", hash = "sha256:d2b89b60fee2f112668117a9f361ceea44a685a37cafd009f396b87b9816efa3"}, + {file = "alive-progress-3.1.5.tar.gz", hash = "sha256:42e399a66c8150dc507602dff7b7953f105ef11faf97ddaa6d27b1cbf45c4c98"}, + {file = "alive_progress-3.1.5-py3-none-any.whl", hash = "sha256:347220c1858e3abe137fa0746895668c04df09c5261a13dc03f05795e8a29be5"}, ] [package.dependencies] @@ -39,65 +28,44 @@ grapheme = "0.6.0" [[package]] name = "altgraph" -version = "0.17.3" +version = "0.17.4" description = "Python graph (network) package" optional = false python-versions = "*" files = [ - {file = "altgraph-0.17.3-py2.py3-none-any.whl", hash = "sha256:c8ac1ca6772207179ed8003ce7687757c04b0b71536f81e2ac5755c6226458fe"}, - {file = "altgraph-0.17.3.tar.gz", hash = "sha256:ad33358114df7c9416cdb8fa1eaa5852166c505118717021c6a8c7c7abbd03dd"}, + {file = "altgraph-0.17.4-py2.py3-none-any.whl", hash = "sha256:642743b4750de17e655e6711601b077bc6598dbfa3ba5fa2b2a35ce12b508dff"}, + {file = "altgraph-0.17.4.tar.gz", hash = "sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406"}, ] -[[package]] -name = "attrs" -version = "22.2.0" -description = "Classes Without Boilerplate" -optional = false -python-versions = ">=3.6" -files = [ - {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, - {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, -] - -[package.extras] -cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] -dev = ["attrs[docs,tests]"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"] -tests = ["attrs[tests-no-zope]", "zope.interface"] -tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"] - [[package]] name = "black" -version = "23.1.0" +version = "23.12.1" description = "The uncompromising code formatter." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "black-23.1.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:b6a92a41ee34b883b359998f0c8e6eb8e99803aa8bf3123bf2b2e6fec505a221"}, - {file = "black-23.1.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:57c18c5165c1dbe291d5306e53fb3988122890e57bd9b3dcb75f967f13411a26"}, - {file = "black-23.1.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:9880d7d419bb7e709b37e28deb5e68a49227713b623c72b2b931028ea65f619b"}, - {file = "black-23.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6663f91b6feca5d06f2ccd49a10f254f9298cc1f7f49c46e498a0771b507104"}, - {file = "black-23.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:9afd3f493666a0cd8f8df9a0200c6359ac53940cbde049dcb1a7eb6ee2dd7074"}, - {file = "black-23.1.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:bfffba28dc52a58f04492181392ee380e95262af14ee01d4bc7bb1b1c6ca8d27"}, - {file = "black-23.1.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c1c476bc7b7d021321e7d93dc2cbd78ce103b84d5a4cf97ed535fbc0d6660648"}, - {file = "black-23.1.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:382998821f58e5c8238d3166c492139573325287820963d2f7de4d518bd76958"}, - {file = "black-23.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bf649fda611c8550ca9d7592b69f0637218c2369b7744694c5e4902873b2f3a"}, - {file = "black-23.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:121ca7f10b4a01fd99951234abdbd97728e1240be89fde18480ffac16503d481"}, - {file = "black-23.1.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:a8471939da5e824b891b25751955be52ee7f8a30a916d570a5ba8e0f2eb2ecad"}, - {file = "black-23.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8178318cb74f98bc571eef19068f6ab5613b3e59d4f47771582f04e175570ed8"}, - {file = "black-23.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:a436e7881d33acaf2536c46a454bb964a50eff59b21b51c6ccf5a40601fbef24"}, - {file = "black-23.1.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:a59db0a2094d2259c554676403fa2fac3473ccf1354c1c63eccf7ae65aac8ab6"}, - {file = "black-23.1.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:0052dba51dec07ed029ed61b18183942043e00008ec65d5028814afaab9a22fd"}, - {file = "black-23.1.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:49f7b39e30f326a34b5c9a4213213a6b221d7ae9d58ec70df1c4a307cf2a1580"}, - {file = "black-23.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:162e37d49e93bd6eb6f1afc3e17a3d23a823042530c37c3c42eeeaf026f38468"}, - {file = "black-23.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b70eb40a78dfac24842458476135f9b99ab952dd3f2dab738c1881a9b38b753"}, - {file = "black-23.1.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:a29650759a6a0944e7cca036674655c2f0f63806ddecc45ed40b7b8aa314b651"}, - {file = "black-23.1.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:bb460c8561c8c1bec7824ecbc3ce085eb50005883a6203dcfb0122e95797ee06"}, - {file = "black-23.1.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:c91dfc2c2a4e50df0026f88d2215e166616e0c80e86004d0003ece0488db2739"}, - {file = "black-23.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a951cc83ab535d248c89f300eccbd625e80ab880fbcfb5ac8afb5f01a258ac9"}, - {file = "black-23.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0680d4380db3719ebcfb2613f34e86c8e6d15ffeabcf8ec59355c5e7b85bb555"}, - {file = "black-23.1.0-py3-none-any.whl", hash = "sha256:7a0f701d314cfa0896b9001df70a530eb2472babb76086344e688829efd97d32"}, - {file = "black-23.1.0.tar.gz", hash = "sha256:b0bd97bea8903f5a2ba7219257a44e3f1f9d00073d6cc1add68f0beec69692ac"}, + {file = "black-23.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0aaf6041986767a5e0ce663c7a2f0e9eaf21e6ff87a5f95cbf3675bfd4c41d2"}, + {file = "black-23.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c88b3711d12905b74206227109272673edce0cb29f27e1385f33b0163c414bba"}, + {file = "black-23.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920b569dc6b3472513ba6ddea21f440d4b4c699494d2e972a1753cdc25df7b0"}, + {file = "black-23.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:3fa4be75ef2a6b96ea8d92b1587dd8cb3a35c7e3d51f0738ced0781c3aa3a5a3"}, + {file = "black-23.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8d4df77958a622f9b5a4c96edb4b8c0034f8434032ab11077ec6c56ae9f384ba"}, + {file = "black-23.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:602cfb1196dc692424c70b6507593a2b29aac0547c1be9a1d1365f0d964c353b"}, + {file = "black-23.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c4352800f14be5b4864016882cdba10755bd50805c95f728011bcb47a4afd59"}, + {file = "black-23.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:0808494f2b2df923ffc5723ed3c7b096bd76341f6213989759287611e9837d50"}, + {file = "black-23.12.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:25e57fd232a6d6ff3f4478a6fd0580838e47c93c83eaf1ccc92d4faf27112c4e"}, + {file = "black-23.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d9e13db441c509a3763a7a3d9a49ccc1b4e974a47be4e08ade2a228876500ec"}, + {file = "black-23.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1bd9c210f8b109b1762ec9fd36592fdd528485aadb3f5849b2740ef17e674e"}, + {file = "black-23.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:ae76c22bde5cbb6bfd211ec343ded2163bba7883c7bc77f6b756a1049436fbb9"}, + {file = "black-23.12.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1fa88a0f74e50e4487477bc0bb900c6781dbddfdfa32691e780bf854c3b4a47f"}, + {file = "black-23.12.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a4d6a9668e45ad99d2f8ec70d5c8c04ef4f32f648ef39048d010b0689832ec6d"}, + {file = "black-23.12.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b18fb2ae6c4bb63eebe5be6bd869ba2f14fd0259bda7d18a46b764d8fb86298a"}, + {file = "black-23.12.1-cp38-cp38-win_amd64.whl", hash = "sha256:c04b6d9d20e9c13f43eee8ea87d44156b8505ca8a3c878773f68b4e4812a421e"}, + {file = "black-23.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3e1b38b3135fd4c025c28c55ddfc236b05af657828a8a6abe5deec419a0b7055"}, + {file = "black-23.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4f0031eaa7b921db76decd73636ef3a12c942ed367d8c3841a0739412b260a54"}, + {file = "black-23.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97e56155c6b737854e60a9ab1c598ff2533d57e7506d97af5481141671abf3ea"}, + {file = "black-23.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:dd15245c8b68fe2b6bd0f32c1556509d11bb33aec9b5d0866dd8e2ed3dba09c2"}, + {file = "black-23.12.1-py3-none-any.whl", hash = "sha256:78baad24af0f033958cad29731e27363183e140962595def56423e626f4bee3e"}, + {file = "black-23.12.1.tar.gz", hash = "sha256:4ce3ef14ebe8d9509188014d96af1c456a910d5b5cbf434a09fef7e024b3d0d5"}, ] [package.dependencies] @@ -107,180 +75,185 @@ packaging = ">=22.0" pathspec = ">=0.9.0" platformdirs = ">=2" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] +d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "boto3" -version = "1.28.21" +version = "1.34.62" description = "The AWS SDK for Python" optional = false -python-versions = ">= 3.7" +python-versions = ">= 3.8" files = [ - {file = "boto3-1.28.21-py3-none-any.whl", hash = "sha256:28e1ea098e43e764d990b4466e377c322b5d57829e2eb1395eca52d4209a6c11"}, - {file = "boto3-1.28.21.tar.gz", hash = "sha256:0ad6932b2469f4fa4e63f4baf8508ccc1b1bc215b9c835df73505aa85210fc27"}, + {file = "boto3-1.34.62-py3-none-any.whl", hash = "sha256:a464a2fd519a9939357822f0538e7b56023dab26742bcae5131b9aa89603ba91"}, + {file = "boto3-1.34.62.tar.gz", hash = "sha256:7373e50b97e27f55c5b2a15a095e7bb45a7d962ced4d1468650dced57087c56b"}, ] [package.dependencies] -botocore = ">=1.31.21,<1.32.0" +botocore = ">=1.34.62,<1.35.0" jmespath = ">=0.7.1,<2.0.0" -s3transfer = ">=0.6.0,<0.7.0" +s3transfer = ">=0.10.0,<0.11.0" [package.extras] crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.31.21" +version = "1.34.62" description = "Low-level, data-driven core of boto 3." optional = false -python-versions = ">= 3.7" +python-versions = ">= 3.8" files = [ - {file = "botocore-1.31.21-py3-none-any.whl", hash = "sha256:c20a5c46eaf49b18b76fdfaec5583320e18abd551b1bc3cd7b1e718372675e21"}, - {file = "botocore-1.31.21.tar.gz", hash = "sha256:9a13736b16aea3f16829b00edfb2c656fee72ecbfe5eb396cc2f8632e31fd524"}, + {file = "botocore-1.34.62-py3-none-any.whl", hash = "sha256:7d215bb6c47e26a2b2b07a39769060c301d15186a302fba12260529870d64d64"}, + {file = "botocore-1.34.62.tar.gz", hash = "sha256:7e97e7237c50d50850fef0d2cc6c8c42965d236a13abf18b29e5b8bb427514d7"}, ] [package.dependencies] jmespath = ">=0.7.1,<2.0.0" python-dateutil = ">=2.1,<3.0.0" -urllib3 = ">=1.25.4,<1.27" +urllib3 = [ + {version = ">=1.25.4,<1.27", markers = "python_version < \"3.10\""}, + {version = ">=1.25.4,<2.1", markers = "python_version >= \"3.10\""}, +] [package.extras] -crt = ["awscrt (==0.16.26)"] +crt = ["awscrt (==0.19.19)"] [[package]] name = "cachetools" -version = "5.3.0" +version = "5.3.3" description = "Extensible memoizing collections and decorators" optional = false -python-versions = "~=3.7" +python-versions = ">=3.7" files = [ - {file = "cachetools-5.3.0-py3-none-any.whl", hash = "sha256:429e1a1e845c008ea6c85aa35d4b98b65d6a9763eeef3e37e92728a12d1de9d4"}, - {file = "cachetools-5.3.0.tar.gz", hash = "sha256:13dfddc7b8df938c21a940dfa6557ce6e94a2f1cdfa58eb90c805721d58f2c14"}, + {file = "cachetools-5.3.3-py3-none-any.whl", hash = "sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945"}, + {file = "cachetools-5.3.3.tar.gz", hash = "sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105"}, ] [[package]] name = "certifi" -version = "2022.12.7" +version = "2024.2.2" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, - {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, ] [[package]] name = "charset-normalizer" -version = "3.0.1" +version = "3.3.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false -python-versions = "*" -files = [ - {file = "charset-normalizer-3.0.1.tar.gz", hash = "sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-win32.whl", hash = "sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-win32.whl", hash = "sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-win32.whl", hash = "sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-win32.whl", hash = "sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-win32.whl", hash = "sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-win32.whl", hash = "sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59"}, - {file = "charset_normalizer-3.0.1-py3-none-any.whl", hash = "sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24"}, +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, ] [[package]] name = "click" -version = "8.1.3" +version = "8.1.7" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" files = [ - {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, - {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, ] [package.dependencies] @@ -313,98 +286,91 @@ test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] [[package]] name = "contourpy" -version = "1.0.7" +version = "1.2.0" description = "Python library for calculating contours of 2D quadrilateral grids" optional = false -python-versions = ">=3.8" -files = [ - {file = "contourpy-1.0.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:95c3acddf921944f241b6773b767f1cbce71d03307270e2d769fd584d5d1092d"}, - {file = "contourpy-1.0.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc1464c97579da9f3ab16763c32e5c5d5bb5fa1ec7ce509a4ca6108b61b84fab"}, - {file = "contourpy-1.0.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8acf74b5d383414401926c1598ed77825cd530ac7b463ebc2e4f46638f56cce6"}, - {file = "contourpy-1.0.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c71fdd8f1c0f84ffd58fca37d00ca4ebaa9e502fb49825484da075ac0b0b803"}, - {file = "contourpy-1.0.7-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f99e9486bf1bb979d95d5cffed40689cb595abb2b841f2991fc894b3452290e8"}, - {file = "contourpy-1.0.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87f4d8941a9564cda3f7fa6a6cd9b32ec575830780677932abdec7bcb61717b0"}, - {file = "contourpy-1.0.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9e20e5a1908e18aaa60d9077a6d8753090e3f85ca25da6e25d30dc0a9e84c2c6"}, - {file = "contourpy-1.0.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a877ada905f7d69b2a31796c4b66e31a8068b37aa9b78832d41c82fc3e056ddd"}, - {file = "contourpy-1.0.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6381fa66866b0ea35e15d197fc06ac3840a9b2643a6475c8fff267db8b9f1e69"}, - {file = "contourpy-1.0.7-cp310-cp310-win32.whl", hash = "sha256:3c184ad2433635f216645fdf0493011a4667e8d46b34082f5a3de702b6ec42e3"}, - {file = "contourpy-1.0.7-cp310-cp310-win_amd64.whl", hash = "sha256:3caea6365b13119626ee996711ab63e0c9d7496f65641f4459c60a009a1f3e80"}, - {file = "contourpy-1.0.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ed33433fc3820263a6368e532f19ddb4c5990855e4886088ad84fd7c4e561c71"}, - {file = "contourpy-1.0.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:38e2e577f0f092b8e6774459317c05a69935a1755ecfb621c0a98f0e3c09c9a5"}, - {file = "contourpy-1.0.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ae90d5a8590e5310c32a7630b4b8618cef7563cebf649011da80874d0aa8f414"}, - {file = "contourpy-1.0.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:130230b7e49825c98edf0b428b7aa1125503d91732735ef897786fe5452b1ec2"}, - {file = "contourpy-1.0.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58569c491e7f7e874f11519ef46737cea1d6eda1b514e4eb5ac7dab6aa864d02"}, - {file = "contourpy-1.0.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d43960d809c4c12508a60b66cb936e7ed57d51fb5e30b513934a4a23874fae"}, - {file = "contourpy-1.0.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:152fd8f730c31fd67fe0ffebe1df38ab6a669403da93df218801a893645c6ccc"}, - {file = "contourpy-1.0.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9056c5310eb1daa33fc234ef39ebfb8c8e2533f088bbf0bc7350f70a29bde1ac"}, - {file = "contourpy-1.0.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a9d7587d2fdc820cc9177139b56795c39fb8560f540bba9ceea215f1f66e1566"}, - {file = "contourpy-1.0.7-cp311-cp311-win32.whl", hash = "sha256:4ee3ee247f795a69e53cd91d927146fb16c4e803c7ac86c84104940c7d2cabf0"}, - {file = "contourpy-1.0.7-cp311-cp311-win_amd64.whl", hash = "sha256:5caeacc68642e5f19d707471890f037a13007feba8427eb7f2a60811a1fc1350"}, - {file = "contourpy-1.0.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fd7dc0e6812b799a34f6d12fcb1000539098c249c8da54f3566c6a6461d0dbad"}, - {file = "contourpy-1.0.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0f9d350b639db6c2c233d92c7f213d94d2e444d8e8fc5ca44c9706cf72193772"}, - {file = "contourpy-1.0.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e96a08b62bb8de960d3a6afbc5ed8421bf1a2d9c85cc4ea73f4bc81b4910500f"}, - {file = "contourpy-1.0.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:031154ed61f7328ad7f97662e48660a150ef84ee1bc8876b6472af88bf5a9b98"}, - {file = "contourpy-1.0.7-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e9ebb4425fc1b658e13bace354c48a933b842d53c458f02c86f371cecbedecc"}, - {file = "contourpy-1.0.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efb8f6d08ca7998cf59eaf50c9d60717f29a1a0a09caa46460d33b2924839dbd"}, - {file = "contourpy-1.0.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6c180d89a28787e4b73b07e9b0e2dac7741261dbdca95f2b489c4f8f887dd810"}, - {file = "contourpy-1.0.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b8d587cc39057d0afd4166083d289bdeff221ac6d3ee5046aef2d480dc4b503c"}, - {file = "contourpy-1.0.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:769eef00437edf115e24d87f8926955f00f7704bede656ce605097584f9966dc"}, - {file = "contourpy-1.0.7-cp38-cp38-win32.whl", hash = "sha256:62398c80ef57589bdbe1eb8537127321c1abcfdf8c5f14f479dbbe27d0322e66"}, - {file = "contourpy-1.0.7-cp38-cp38-win_amd64.whl", hash = "sha256:57119b0116e3f408acbdccf9eb6ef19d7fe7baf0d1e9aaa5381489bc1aa56556"}, - {file = "contourpy-1.0.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:30676ca45084ee61e9c3da589042c24a57592e375d4b138bd84d8709893a1ba4"}, - {file = "contourpy-1.0.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3e927b3868bd1e12acee7cc8f3747d815b4ab3e445a28d2e5373a7f4a6e76ba1"}, - {file = "contourpy-1.0.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:366a0cf0fc079af5204801786ad7a1c007714ee3909e364dbac1729f5b0849e5"}, - {file = "contourpy-1.0.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89ba9bb365446a22411f0673abf6ee1fea3b2cf47b37533b970904880ceb72f3"}, - {file = "contourpy-1.0.7-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:71b0bf0c30d432278793d2141362ac853859e87de0a7dee24a1cea35231f0d50"}, - {file = "contourpy-1.0.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7281244c99fd7c6f27c1c6bfafba878517b0b62925a09b586d88ce750a016d2"}, - {file = "contourpy-1.0.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b6d0f9e1d39dbfb3977f9dd79f156c86eb03e57a7face96f199e02b18e58d32a"}, - {file = "contourpy-1.0.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7f6979d20ee5693a1057ab53e043adffa1e7418d734c1532e2d9e915b08d8ec2"}, - {file = "contourpy-1.0.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5dd34c1ae752515318224cba7fc62b53130c45ac6a1040c8b7c1a223c46e8967"}, - {file = "contourpy-1.0.7-cp39-cp39-win32.whl", hash = "sha256:c5210e5d5117e9aec8c47d9156d1d3835570dd909a899171b9535cb4a3f32693"}, - {file = "contourpy-1.0.7-cp39-cp39-win_amd64.whl", hash = "sha256:60835badb5ed5f4e194a6f21c09283dd6e007664a86101431bf870d9e86266c4"}, - {file = "contourpy-1.0.7-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ce41676b3d0dd16dbcfabcc1dc46090aaf4688fd6e819ef343dbda5a57ef0161"}, - {file = "contourpy-1.0.7-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a011cf354107b47c58ea932d13b04d93c6d1d69b8b6dce885e642531f847566"}, - {file = "contourpy-1.0.7-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31a55dccc8426e71817e3fe09b37d6d48ae40aae4ecbc8c7ad59d6893569c436"}, - {file = "contourpy-1.0.7-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69f8ff4db108815addd900a74df665e135dbbd6547a8a69333a68e1f6e368ac2"}, - {file = "contourpy-1.0.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:efe99298ba37e37787f6a2ea868265465410822f7bea163edcc1bd3903354ea9"}, - {file = "contourpy-1.0.7-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a1e97b86f73715e8670ef45292d7cc033548266f07d54e2183ecb3c87598888f"}, - {file = "contourpy-1.0.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc331c13902d0f50845099434cd936d49d7a2ca76cb654b39691974cb1e4812d"}, - {file = "contourpy-1.0.7-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:24847601071f740837aefb730e01bd169fbcaa610209779a78db7ebb6e6a7051"}, - {file = "contourpy-1.0.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abf298af1e7ad44eeb93501e40eb5a67abbf93b5d90e468d01fc0c4451971afa"}, - {file = "contourpy-1.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:64757f6460fc55d7e16ed4f1de193f362104285c667c112b50a804d482777edd"}, - {file = "contourpy-1.0.7.tar.gz", hash = "sha256:d8165a088d31798b59e91117d1f5fc3df8168d8b48c4acc10fc0df0d0bdbcc5e"}, +python-versions = ">=3.9" +files = [ + {file = "contourpy-1.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0274c1cb63625972c0c007ab14dd9ba9e199c36ae1a231ce45d725cbcbfd10a8"}, + {file = "contourpy-1.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ab459a1cbbf18e8698399c595a01f6dcc5c138220ca3ea9e7e6126232d102bb4"}, + {file = "contourpy-1.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fdd887f17c2f4572ce548461e4f96396681212d858cae7bd52ba3310bc6f00f"}, + {file = "contourpy-1.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d16edfc3fc09968e09ddffada434b3bf989bf4911535e04eada58469873e28e"}, + {file = "contourpy-1.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c203f617abc0dde5792beb586f827021069fb6d403d7f4d5c2b543d87edceb9"}, + {file = "contourpy-1.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b69303ceb2e4d4f146bf82fda78891ef7bcd80c41bf16bfca3d0d7eb545448aa"}, + {file = "contourpy-1.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:884c3f9d42d7218304bc74a8a7693d172685c84bd7ab2bab1ee567b769696df9"}, + {file = "contourpy-1.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4a1b1208102be6e851f20066bf0e7a96b7d48a07c9b0cfe6d0d4545c2f6cadab"}, + {file = "contourpy-1.2.0-cp310-cp310-win32.whl", hash = "sha256:34b9071c040d6fe45d9826cbbe3727d20d83f1b6110d219b83eb0e2a01d79488"}, + {file = "contourpy-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:bd2f1ae63998da104f16a8b788f685e55d65760cd1929518fd94cd682bf03e41"}, + {file = "contourpy-1.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dd10c26b4eadae44783c45ad6655220426f971c61d9b239e6f7b16d5cdaaa727"}, + {file = "contourpy-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5c6b28956b7b232ae801406e529ad7b350d3f09a4fde958dfdf3c0520cdde0dd"}, + {file = "contourpy-1.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebeac59e9e1eb4b84940d076d9f9a6cec0064e241818bcb6e32124cc5c3e377a"}, + {file = "contourpy-1.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:139d8d2e1c1dd52d78682f505e980f592ba53c9f73bd6be102233e358b401063"}, + {file = "contourpy-1.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e9dc350fb4c58adc64df3e0703ab076f60aac06e67d48b3848c23647ae4310e"}, + {file = "contourpy-1.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18fc2b4ed8e4a8fe849d18dce4bd3c7ea637758c6343a1f2bae1e9bd4c9f4686"}, + {file = "contourpy-1.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:16a7380e943a6d52472096cb7ad5264ecee36ed60888e2a3d3814991a0107286"}, + {file = "contourpy-1.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8d8faf05be5ec8e02a4d86f616fc2a0322ff4a4ce26c0f09d9f7fb5330a35c95"}, + {file = "contourpy-1.2.0-cp311-cp311-win32.whl", hash = "sha256:67b7f17679fa62ec82b7e3e611c43a016b887bd64fb933b3ae8638583006c6d6"}, + {file = "contourpy-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:99ad97258985328b4f207a5e777c1b44a83bfe7cf1f87b99f9c11d4ee477c4de"}, + {file = "contourpy-1.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:575bcaf957a25d1194903a10bc9f316c136c19f24e0985a2b9b5608bdf5dbfe0"}, + {file = "contourpy-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9e6c93b5b2dbcedad20a2f18ec22cae47da0d705d454308063421a3b290d9ea4"}, + {file = "contourpy-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:464b423bc2a009088f19bdf1f232299e8b6917963e2b7e1d277da5041f33a779"}, + {file = "contourpy-1.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:68ce4788b7d93e47f84edd3f1f95acdcd142ae60bc0e5493bfd120683d2d4316"}, + {file = "contourpy-1.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d7d1f8871998cdff5d2ff6a087e5e1780139abe2838e85b0b46b7ae6cc25399"}, + {file = "contourpy-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e739530c662a8d6d42c37c2ed52a6f0932c2d4a3e8c1f90692ad0ce1274abe0"}, + {file = "contourpy-1.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:247b9d16535acaa766d03037d8e8fb20866d054d3c7fbf6fd1f993f11fc60ca0"}, + {file = "contourpy-1.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:461e3ae84cd90b30f8d533f07d87c00379644205b1d33a5ea03381edc4b69431"}, + {file = "contourpy-1.2.0-cp312-cp312-win32.whl", hash = "sha256:1c2559d6cffc94890b0529ea7eeecc20d6fadc1539273aa27faf503eb4656d8f"}, + {file = "contourpy-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:491b1917afdd8638a05b611a56d46587d5a632cabead889a5440f7c638bc6ed9"}, + {file = "contourpy-1.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5fd1810973a375ca0e097dee059c407913ba35723b111df75671a1976efa04bc"}, + {file = "contourpy-1.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:999c71939aad2780f003979b25ac5b8f2df651dac7b38fb8ce6c46ba5abe6ae9"}, + {file = "contourpy-1.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7caf9b241464c404613512d5594a6e2ff0cc9cb5615c9475cc1d9b514218ae8"}, + {file = "contourpy-1.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:266270c6f6608340f6c9836a0fb9b367be61dde0c9a9a18d5ece97774105ff3e"}, + {file = "contourpy-1.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbd50d0a0539ae2e96e537553aff6d02c10ed165ef40c65b0e27e744a0f10af8"}, + {file = "contourpy-1.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11f8d2554e52f459918f7b8e6aa20ec2a3bce35ce95c1f0ef4ba36fbda306df5"}, + {file = "contourpy-1.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ce96dd400486e80ac7d195b2d800b03e3e6a787e2a522bfb83755938465a819e"}, + {file = "contourpy-1.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6d3364b999c62f539cd403f8123ae426da946e142312a514162adb2addd8d808"}, + {file = "contourpy-1.2.0-cp39-cp39-win32.whl", hash = "sha256:1c88dfb9e0c77612febebb6ac69d44a8d81e3dc60f993215425b62c1161353f4"}, + {file = "contourpy-1.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:78e6ad33cf2e2e80c5dfaaa0beec3d61face0fb650557100ee36db808bfa6843"}, + {file = "contourpy-1.2.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:be16975d94c320432657ad2402f6760990cb640c161ae6da1363051805fa8108"}, + {file = "contourpy-1.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b95a225d4948b26a28c08307a60ac00fb8671b14f2047fc5476613252a129776"}, + {file = "contourpy-1.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0d7e03c0f9a4f90dc18d4e77e9ef4ec7b7bbb437f7f675be8e530d65ae6ef956"}, + {file = "contourpy-1.2.0.tar.gz", hash = "sha256:171f311cb758de7da13fc53af221ae47a5877be5a0843a9fe150818c51ed276a"}, ] [package.dependencies] -numpy = ">=1.16" +numpy = ">=1.20,<2.0" [package.extras] -bokeh = ["bokeh", "chromedriver", "selenium"] -docs = ["furo", "sphinx-copybutton"] -mypy = ["contourpy[bokeh]", "docutils-stubs", "mypy (==0.991)", "types-Pillow"] -test = ["Pillow", "matplotlib", "pytest"] -test-no-images = ["pytest"] +bokeh = ["bokeh", "selenium"] +docs = ["furo", "sphinx (>=7.2)", "sphinx-copybutton"] +mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.6.1)", "types-Pillow"] +test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] +test-no-images = ["pytest", "pytest-cov", "pytest-xdist", "wurlitzer"] [[package]] name = "cycler" -version = "0.11.0" +version = "0.12.1" description = "Composable style cycles" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "cycler-0.11.0-py3-none-any.whl", hash = "sha256:3a27e95f763a428a739d2add979fa7494c912a32c17c4c38c4d5f082cad165a3"}, - {file = "cycler-0.11.0.tar.gz", hash = "sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f"}, + {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 = "dateparser" -version = "1.1.7" +version = "1.2.0" description = "Date parsing library designed to parse dates from HTML pages" optional = false python-versions = ">=3.7" files = [ - {file = "dateparser-1.1.7-py2.py3-none-any.whl", hash = "sha256:fbed8b738a24c9cd7f47c4f2089527926566fe539e1a06125eddba75917b1eef"}, - {file = "dateparser-1.1.7.tar.gz", hash = "sha256:ff047d9cffad4d3113ead8ec0faf8a7fc43bab7d853ac8715e071312b53c465a"}, + {file = "dateparser-1.2.0-py2.py3-none-any.whl", hash = "sha256:0b21ad96534e562920a0083e97fd45fa959882d4162acc358705144520a35830"}, + {file = "dateparser-1.2.0.tar.gz", hash = "sha256:7975b43a4222283e0ae15be7b4999d08c9a70e2d378ac87385b1ccf2cffbbb30"}, ] [package.dependencies] @@ -420,13 +386,13 @@ langdetect = ["langdetect"] [[package]] name = "exceptiongroup" -version = "1.1.1" +version = "1.2.0" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.1.1-py3-none-any.whl", hash = "sha256:232c37c63e4f682982c8b6459f33a8981039e5fb8756b2074364e5055c498c9e"}, - {file = "exceptiongroup-1.1.1.tar.gz", hash = "sha256:d484c3090ba2889ae2928419117447a14daf3c1231d5e30d0aae34f354f01785"}, + {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, + {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, ] [package.extras] @@ -434,68 +400,107 @@ test = ["pytest (>=6)"] [[package]] name = "flake8" -version = "6.0.0" +version = "6.1.0" description = "the modular source code checker: pep8 pyflakes and co" optional = false python-versions = ">=3.8.1" files = [ - {file = "flake8-6.0.0-py2.py3-none-any.whl", hash = "sha256:3833794e27ff64ea4e9cf5d410082a8b97ff1a06c16aa3d2027339cd0f1195c7"}, - {file = "flake8-6.0.0.tar.gz", hash = "sha256:c61007e76655af75e6785a931f452915b371dc48f56efd765247c8fe68f2b181"}, + {file = "flake8-6.1.0-py2.py3-none-any.whl", hash = "sha256:ffdfce58ea94c6580c77888a86506937f9a1a227dfcd15f245d694ae20a6b6e5"}, + {file = "flake8-6.1.0.tar.gz", hash = "sha256:d5b3857f07c030bdb5bf41c7f53799571d75c4491748a3adcd47de929e34cd23"}, ] [package.dependencies] mccabe = ">=0.7.0,<0.8.0" -pycodestyle = ">=2.10.0,<2.11.0" -pyflakes = ">=3.0.0,<3.1.0" +pycodestyle = ">=2.11.0,<2.12.0" +pyflakes = ">=3.1.0,<3.2.0" [[package]] name = "fonttools" -version = "4.39.0" +version = "4.49.0" description = "Tools to manipulate font files" optional = false python-versions = ">=3.8" files = [ - {file = "fonttools-4.39.0-py3-none-any.whl", hash = "sha256:f5e764e1fd6ad54dfc201ff32af0ba111bcfbe0d05b24540af74c63db4ed6390"}, - {file = "fonttools-4.39.0.zip", hash = "sha256:909c104558835eac27faeb56be5a4c32694192dca123d073bf746ce9254054af"}, + {file = "fonttools-4.49.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d970ecca0aac90d399e458f0b7a8a597e08f95de021f17785fb68e2dc0b99717"}, + {file = "fonttools-4.49.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac9a745b7609f489faa65e1dc842168c18530874a5f5b742ac3dd79e26bca8bc"}, + {file = "fonttools-4.49.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ba0e00620ca28d4ca11fc700806fd69144b463aa3275e1b36e56c7c09915559"}, + {file = "fonttools-4.49.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdee3ab220283057e7840d5fb768ad4c2ebe65bdba6f75d5d7bf47f4e0ed7d29"}, + {file = "fonttools-4.49.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ce7033cb61f2bb65d8849658d3786188afd80f53dad8366a7232654804529532"}, + {file = "fonttools-4.49.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:07bc5ea02bb7bc3aa40a1eb0481ce20e8d9b9642a9536cde0218290dd6085828"}, + {file = "fonttools-4.49.0-cp310-cp310-win32.whl", hash = "sha256:86eef6aab7fd7c6c8545f3ebd00fd1d6729ca1f63b0cb4d621bccb7d1d1c852b"}, + {file = "fonttools-4.49.0-cp310-cp310-win_amd64.whl", hash = "sha256:1fac1b7eebfce75ea663e860e7c5b4a8831b858c17acd68263bc156125201abf"}, + {file = "fonttools-4.49.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:edc0cce355984bb3c1d1e89d6a661934d39586bb32191ebff98c600f8957c63e"}, + {file = "fonttools-4.49.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:83a0d9336de2cba86d886507dd6e0153df333ac787377325a39a2797ec529814"}, + {file = "fonttools-4.49.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36c8865bdb5cfeec88f5028e7e592370a0657b676c6f1d84a2108e0564f90e22"}, + {file = "fonttools-4.49.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33037d9e56e2562c710c8954d0f20d25b8386b397250d65581e544edc9d6b942"}, + {file = "fonttools-4.49.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8fb022d799b96df3eaa27263e9eea306bd3d437cc9aa981820850281a02b6c9a"}, + {file = "fonttools-4.49.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:33c584c0ef7dc54f5dd4f84082eabd8d09d1871a3d8ca2986b0c0c98165f8e86"}, + {file = "fonttools-4.49.0-cp311-cp311-win32.whl", hash = "sha256:cbe61b158deb09cffdd8540dc4a948d6e8f4d5b4f3bf5cd7db09bd6a61fee64e"}, + {file = "fonttools-4.49.0-cp311-cp311-win_amd64.whl", hash = "sha256:fc11e5114f3f978d0cea7e9853627935b30d451742eeb4239a81a677bdee6bf6"}, + {file = "fonttools-4.49.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d647a0e697e5daa98c87993726da8281c7233d9d4ffe410812a4896c7c57c075"}, + {file = "fonttools-4.49.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f3bbe672df03563d1f3a691ae531f2e31f84061724c319652039e5a70927167e"}, + {file = "fonttools-4.49.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bebd91041dda0d511b0d303180ed36e31f4f54b106b1259b69fade68413aa7ff"}, + {file = "fonttools-4.49.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4145f91531fd43c50f9eb893faa08399816bb0b13c425667c48475c9f3a2b9b5"}, + {file = "fonttools-4.49.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ea329dafb9670ffbdf4dbc3b0e5c264104abcd8441d56de77f06967f032943cb"}, + {file = "fonttools-4.49.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c076a9e548521ecc13d944b1d261ff3d7825048c338722a4bd126d22316087b7"}, + {file = "fonttools-4.49.0-cp312-cp312-win32.whl", hash = "sha256:b607ea1e96768d13be26d2b400d10d3ebd1456343eb5eaddd2f47d1c4bd00880"}, + {file = "fonttools-4.49.0-cp312-cp312-win_amd64.whl", hash = "sha256:a974c49a981e187381b9cc2c07c6b902d0079b88ff01aed34695ec5360767034"}, + {file = "fonttools-4.49.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b85ec0bdd7bdaa5c1946398cbb541e90a6dfc51df76dfa88e0aaa41b335940cb"}, + {file = "fonttools-4.49.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:af20acbe198a8a790618ee42db192eb128afcdcc4e96d99993aca0b60d1faeb4"}, + {file = "fonttools-4.49.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d418b1fee41a1d14931f7ab4b92dc0bc323b490e41d7a333eec82c9f1780c75"}, + {file = "fonttools-4.49.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b44a52b8e6244b6548851b03b2b377a9702b88ddc21dcaf56a15a0393d425cb9"}, + {file = "fonttools-4.49.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7c7125068e04a70739dad11857a4d47626f2b0bd54de39e8622e89701836eabd"}, + {file = "fonttools-4.49.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:29e89d0e1a7f18bc30f197cfadcbef5a13d99806447c7e245f5667579a808036"}, + {file = "fonttools-4.49.0-cp38-cp38-win32.whl", hash = "sha256:9d95fa0d22bf4f12d2fb7b07a46070cdfc19ef5a7b1c98bc172bfab5bf0d6844"}, + {file = "fonttools-4.49.0-cp38-cp38-win_amd64.whl", hash = "sha256:768947008b4dc552d02772e5ebd49e71430a466e2373008ce905f953afea755a"}, + {file = "fonttools-4.49.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:08877e355d3dde1c11973bb58d4acad1981e6d1140711230a4bfb40b2b937ccc"}, + {file = "fonttools-4.49.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fdb54b076f25d6b0f0298dc706acee5052de20c83530fa165b60d1f2e9cbe3cb"}, + {file = "fonttools-4.49.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0af65c720520710cc01c293f9c70bd69684365c6015cc3671db2b7d807fe51f2"}, + {file = "fonttools-4.49.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f255ce8ed7556658f6d23f6afd22a6d9bbc3edb9b96c96682124dc487e1bf42"}, + {file = "fonttools-4.49.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d00af0884c0e65f60dfaf9340e26658836b935052fdd0439952ae42e44fdd2be"}, + {file = "fonttools-4.49.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:263832fae27481d48dfafcc43174644b6706639661e242902ceb30553557e16c"}, + {file = "fonttools-4.49.0-cp39-cp39-win32.whl", hash = "sha256:0404faea044577a01bb82d47a8fa4bc7a54067fa7e324785dd65d200d6dd1133"}, + {file = "fonttools-4.49.0-cp39-cp39-win_amd64.whl", hash = "sha256:b050d362df50fc6e38ae3954d8c29bf2da52be384649ee8245fdb5186b620836"}, + {file = "fonttools-4.49.0-py3-none-any.whl", hash = "sha256:af281525e5dd7fa0b39fb1667b8d5ca0e2a9079967e14c4bfe90fd1cd13e0f18"}, + {file = "fonttools-4.49.0.tar.gz", hash = "sha256:ebf46e7f01b7af7861310417d7c49591a85d99146fc23a5ba82fdb28af156321"}, ] [package.extras] -all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0,<5)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.0.0)", "xattr", "zopfli (>=0.1.4)"] +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", "scipy"] -lxml = ["lxml (>=4.0,<5)"] +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.0.0)"] +unicode = ["unicodedata2 (>=15.1.0)"] woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] [[package]] name = "google-auth" -version = "2.16.2" +version = "2.28.2" description = "Google Authentication Library" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*" +python-versions = ">=3.7" files = [ - {file = "google-auth-2.16.2.tar.gz", hash = "sha256:07e14f34ec288e3f33e00e2e3cc40c8942aa5d4ceac06256a28cd8e786591420"}, - {file = "google_auth-2.16.2-py2.py3-none-any.whl", hash = "sha256:2fef3cf94876d1a0e204afece58bb4d83fb57228aaa366c64045039fda6770a2"}, + {file = "google-auth-2.28.2.tar.gz", hash = "sha256:80b8b4969aa9ed5938c7828308f20f035bc79f9d8fb8120bf9dc8db20b41ba30"}, + {file = "google_auth-2.28.2-py2.py3-none-any.whl", hash = "sha256:9fd67bbcd40f16d9d42f950228e9cf02a2ded4ae49198b27432d0cded5a74c38"}, ] [package.dependencies] cachetools = ">=2.0.0,<6.0" pyasn1-modules = ">=0.2.1" -rsa = {version = ">=3.1.4,<5", markers = "python_version >= \"3.6\""} -six = ">=1.9.0" +rsa = ">=3.1.4,<5" [package.extras] -aiohttp = ["aiohttp (>=3.6.2,<4.0.0dev)", "requests (>=2.20.0,<3.0.0dev)"] +aiohttp = ["aiohttp (>=3.6.2,<4.0.0.dev0)", "requests (>=2.20.0,<3.0.0.dev0)"] enterprise-cert = ["cryptography (==36.0.2)", "pyopenssl (==22.0.0)"] pyopenssl = ["cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"] reauth = ["pyu2f (>=0.1.5)"] -requests = ["requests (>=2.20.0,<3.0.0dev)"] +requests = ["requests (>=2.20.0,<3.0.0.dev0)"] [[package]] name = "grapheme" @@ -526,32 +531,51 @@ requests = ">=1.0.0" [[package]] name = "idna" -version = "3.4" +version = "3.6" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" files = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, + {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, + {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, +] + +[[package]] +name = "importlib-metadata" +version = "7.0.2" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_metadata-7.0.2-py3-none-any.whl", hash = "sha256:f4bc4c0c070c490abf4ce96d715f68e95923320370efb66143df00199bb6c100"}, + {file = "importlib_metadata-7.0.2.tar.gz", hash = "sha256:198f568f3230878cb1b44fbd7975f87906c22336dba2e4a7f05278c281fbd792"}, ] +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +perf = ["ipython"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] + [[package]] name = "importlib-resources" -version = "5.12.0" +version = "6.3.0" description = "Read resources from Python packages" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "importlib_resources-5.12.0-py3-none-any.whl", hash = "sha256:7b1deeebbf351c7578e09bf2f63fa2ce8b5ffec296e0d349139d43cca061a81a"}, - {file = "importlib_resources-5.12.0.tar.gz", hash = "sha256:4be82589bf5c1d7999aedf2a45159d10cb3ca4f19b2271f8792bc8e6da7b22f6"}, + {file = "importlib_resources-6.3.0-py3-none-any.whl", hash = "sha256:783407aa1cd05550e3aa123e8f7cfaebee35ffa9cb0242919e2d1e4172222705"}, + {file = "importlib_resources-6.3.0.tar.gz", hash = "sha256:166072a97e86917a9025876f34286f549b9caf1d10b35a1b372bffa1600c6569"}, ] [package.dependencies] zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["jaraco.collections", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)", "zipp (>=3.17)"] [[package]] name = "iniconfig" @@ -566,20 +590,17 @@ files = [ [[package]] name = "isort" -version = "5.12.0" +version = "5.13.2" description = "A Python utility / library to sort Python imports." optional = false python-versions = ">=3.8.0" files = [ - {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, - {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, + {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, + {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, ] [package.extras] -colors = ["colorama (>=0.4.3)"] -pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] -plugins = ["setuptools"] -requirements-deprecated-finder = ["pip-api", "pipreqs"] +colors = ["colorama (>=0.4.6)"] [[package]] name = "jmespath" @@ -594,79 +615,115 @@ files = [ [[package]] name = "kiwisolver" -version = "1.4.4" +version = "1.4.5" description = "A fast implementation of the Cassowary constraint solver" optional = false python-versions = ">=3.7" files = [ - {file = "kiwisolver-1.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2f5e60fabb7343a836360c4f0919b8cd0d6dbf08ad2ca6b9cf90bf0c76a3c4f6"}, - {file = "kiwisolver-1.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:10ee06759482c78bdb864f4109886dff7b8a56529bc1609d4f1112b93fe6423c"}, - {file = "kiwisolver-1.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c79ebe8f3676a4c6630fd3f777f3cfecf9289666c84e775a67d1d358578dc2e3"}, - {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:abbe9fa13da955feb8202e215c4018f4bb57469b1b78c7a4c5c7b93001699938"}, - {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7577c1987baa3adc4b3c62c33bd1118c3ef5c8ddef36f0f2c950ae0b199e100d"}, - {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8ad8285b01b0d4695102546b342b493b3ccc6781fc28c8c6a1bb63e95d22f09"}, - {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ed58b8acf29798b036d347791141767ccf65eee7f26bde03a71c944449e53de"}, - {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a68b62a02953b9841730db7797422f983935aeefceb1679f0fc85cbfbd311c32"}, - {file = "kiwisolver-1.4.4-cp310-cp310-win32.whl", hash = "sha256:e92a513161077b53447160b9bd8f522edfbed4bd9759e4c18ab05d7ef7e49408"}, - {file = "kiwisolver-1.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:3fe20f63c9ecee44560d0e7f116b3a747a5d7203376abeea292ab3152334d004"}, - {file = "kiwisolver-1.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e0ea21f66820452a3f5d1655f8704a60d66ba1191359b96541eaf457710a5fc6"}, - {file = "kiwisolver-1.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bc9db8a3efb3e403e4ecc6cd9489ea2bac94244f80c78e27c31dcc00d2790ac2"}, - {file = "kiwisolver-1.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d5b61785a9ce44e5a4b880272baa7cf6c8f48a5180c3e81c59553ba0cb0821ca"}, - {file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2dbb44c3f7e6c4d3487b31037b1bdbf424d97687c1747ce4ff2895795c9bf69"}, - {file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6295ecd49304dcf3bfbfa45d9a081c96509e95f4b9d0eb7ee4ec0530c4a96514"}, - {file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bd472dbe5e136f96a4b18f295d159d7f26fd399136f5b17b08c4e5f498cd494"}, - {file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf7d9fce9bcc4752ca4a1b80aabd38f6d19009ea5cbda0e0856983cf6d0023f5"}, - {file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d6601aed50c74e0ef02f4204da1816147a6d3fbdc8b3872d263338a9052c51"}, - {file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:877272cf6b4b7e94c9614f9b10140e198d2186363728ed0f701c6eee1baec1da"}, - {file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:db608a6757adabb32f1cfe6066e39b3706d8c3aa69bbc353a5b61edad36a5cb4"}, - {file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:5853eb494c71e267912275e5586fe281444eb5e722de4e131cddf9d442615626"}, - {file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:f0a1dbdb5ecbef0d34eb77e56fcb3e95bbd7e50835d9782a45df81cc46949750"}, - {file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:283dffbf061a4ec60391d51e6155e372a1f7a4f5b15d59c8505339454f8989e4"}, - {file = "kiwisolver-1.4.4-cp311-cp311-win32.whl", hash = "sha256:d06adcfa62a4431d404c31216f0f8ac97397d799cd53800e9d3efc2fbb3cf14e"}, - {file = "kiwisolver-1.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:e7da3fec7408813a7cebc9e4ec55afed2d0fd65c4754bc376bf03498d4e92686"}, - {file = "kiwisolver-1.4.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:62ac9cc684da4cf1778d07a89bf5f81b35834cb96ca523d3a7fb32509380cbf6"}, - {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41dae968a94b1ef1897cb322b39360a0812661dba7c682aa45098eb8e193dbdf"}, - {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02f79693ec433cb4b5f51694e8477ae83b3205768a6fb48ffba60549080e295b"}, - {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d0611a0a2a518464c05ddd5a3a1a0e856ccc10e67079bb17f265ad19ab3c7597"}, - {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:db5283d90da4174865d520e7366801a93777201e91e79bacbac6e6927cbceede"}, - {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1041feb4cda8708ce73bb4dcb9ce1ccf49d553bf87c3954bdfa46f0c3f77252c"}, - {file = "kiwisolver-1.4.4-cp37-cp37m-win32.whl", hash = "sha256:a553dadda40fef6bfa1456dc4be49b113aa92c2a9a9e8711e955618cd69622e3"}, - {file = "kiwisolver-1.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:03baab2d6b4a54ddbb43bba1a3a2d1627e82d205c5cf8f4c924dc49284b87166"}, - {file = "kiwisolver-1.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:841293b17ad704d70c578f1f0013c890e219952169ce8a24ebc063eecf775454"}, - {file = "kiwisolver-1.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f4f270de01dd3e129a72efad823da90cc4d6aafb64c410c9033aba70db9f1ff0"}, - {file = "kiwisolver-1.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f9f39e2f049db33a908319cf46624a569b36983c7c78318e9726a4cb8923b26c"}, - {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c97528e64cb9ebeff9701e7938653a9951922f2a38bd847787d4a8e498cc83ae"}, - {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d1573129aa0fd901076e2bfb4275a35f5b7aa60fbfb984499d661ec950320b0"}, - {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ad881edc7ccb9d65b0224f4e4d05a1e85cf62d73aab798943df6d48ab0cd79a1"}, - {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b428ef021242344340460fa4c9185d0b1f66fbdbfecc6c63eff4b7c29fad429d"}, - {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2e407cb4bd5a13984a6c2c0fe1845e4e41e96f183e5e5cd4d77a857d9693494c"}, - {file = "kiwisolver-1.4.4-cp38-cp38-win32.whl", hash = "sha256:75facbe9606748f43428fc91a43edb46c7ff68889b91fa31f53b58894503a191"}, - {file = "kiwisolver-1.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:5bce61af018b0cb2055e0e72e7d65290d822d3feee430b7b8203d8a855e78766"}, - {file = "kiwisolver-1.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8c808594c88a025d4e322d5bb549282c93c8e1ba71b790f539567932722d7bd8"}, - {file = "kiwisolver-1.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f0a71d85ecdd570ded8ac3d1c0f480842f49a40beb423bb8014539a9f32a5897"}, - {file = "kiwisolver-1.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b533558eae785e33e8c148a8d9921692a9fe5aa516efbdff8606e7d87b9d5824"}, - {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:efda5fc8cc1c61e4f639b8067d118e742b812c930f708e6667a5ce0d13499e29"}, - {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7c43e1e1206cd421cd92e6b3280d4385d41d7166b3ed577ac20444b6995a445f"}, - {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc8d3bd6c72b2dd9decf16ce70e20abcb3274ba01b4e1c96031e0c4067d1e7cd"}, - {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4ea39b0ccc4f5d803e3337dd46bcce60b702be4d86fd0b3d7531ef10fd99a1ac"}, - {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:968f44fdbf6dd757d12920d63b566eeb4d5b395fd2d00d29d7ef00a00582aac9"}, - {file = "kiwisolver-1.4.4-cp39-cp39-win32.whl", hash = "sha256:da7e547706e69e45d95e116e6939488d62174e033b763ab1496b4c29b76fabea"}, - {file = "kiwisolver-1.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:ba59c92039ec0a66103b1d5fe588fa546373587a7d68f5c96f743c3396afc04b"}, - {file = "kiwisolver-1.4.4-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:91672bacaa030f92fc2f43b620d7b337fd9a5af28b0d6ed3f77afc43c4a64b5a"}, - {file = "kiwisolver-1.4.4-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:787518a6789009c159453da4d6b683f468ef7a65bbde796bcea803ccf191058d"}, - {file = "kiwisolver-1.4.4-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da152d8cdcab0e56e4f45eb08b9aea6455845ec83172092f09b0e077ece2cf7a"}, - {file = "kiwisolver-1.4.4-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ecb1fa0db7bf4cff9dac752abb19505a233c7f16684c5826d1f11ebd9472b871"}, - {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:28bc5b299f48150b5f822ce68624e445040595a4ac3d59251703779836eceff9"}, - {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:81e38381b782cc7e1e46c4e14cd997ee6040768101aefc8fa3c24a4cc58e98f8"}, - {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2a66fdfb34e05b705620dd567f5a03f239a088d5a3f321e7b6ac3239d22aa286"}, - {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:872b8ca05c40d309ed13eb2e582cab0c5a05e81e987ab9c521bf05ad1d5cf5cb"}, - {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:70e7c2e7b750585569564e2e5ca9845acfaa5da56ac46df68414f29fea97be9f"}, - {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9f85003f5dfa867e86d53fac6f7e6f30c045673fa27b603c397753bebadc3008"}, - {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e307eb9bd99801f82789b44bb45e9f541961831c7311521b13a6c85afc09767"}, - {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1792d939ec70abe76f5054d3f36ed5656021dcad1322d1cc996d4e54165cef9"}, - {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6cb459eea32a4e2cf18ba5fcece2dbdf496384413bc1bae15583f19e567f3b2"}, - {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:36dafec3d6d6088d34e2de6b85f9d8e2324eb734162fba59d2ba9ed7a2043d5b"}, - {file = "kiwisolver-1.4.4.tar.gz", hash = "sha256:d41997519fcba4a1e46eb4a2fe31bc12f0ff957b2b81bac28db24744f333e955"}, + {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]] @@ -697,13 +754,13 @@ adal = ["adal (>=1.0.2)"] [[package]] name = "macholib" -version = "1.16.2" +version = "1.16.3" description = "Mach-O header analysis and editing" optional = false python-versions = "*" files = [ - {file = "macholib-1.16.2-py2.py3-none-any.whl", hash = "sha256:44c40f2cd7d6726af8fa6fe22549178d3a4dfecc35a9cd15ea916d9c83a688e0"}, - {file = "macholib-1.16.2.tar.gz", hash = "sha256:557bbfa1bb255c20e9abafe7ed6cd8046b48d9525db2f9b77d3122a63a2a8bf8"}, + {file = "macholib-1.16.3-py2.py3-none-any.whl", hash = "sha256:0e315d7583d38b8c77e815b1ecbdbf504a8258d8b3e17b61165c6feb60d18f2c"}, + {file = "macholib-1.16.3.tar.gz", hash = "sha256:07ae9e15e8e4cd9a788013d81f5908b3609aa76f9b1421bae9c4d7606ec86a30"}, ] [package.dependencies] @@ -711,52 +768,39 @@ altgraph = ">=0.17" [[package]] name = "matplotlib" -version = "3.7.1" +version = "3.8.3" description = "Python plotting package" optional = false -python-versions = ">=3.8" -files = [ - {file = "matplotlib-3.7.1-cp310-cp310-macosx_10_12_universal2.whl", hash = "sha256:95cbc13c1fc6844ab8812a525bbc237fa1470863ff3dace7352e910519e194b1"}, - {file = "matplotlib-3.7.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:08308bae9e91aca1ec6fd6dda66237eef9f6294ddb17f0d0b3c863169bf82353"}, - {file = "matplotlib-3.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:544764ba51900da4639c0f983b323d288f94f65f4024dc40ecb1542d74dc0500"}, - {file = "matplotlib-3.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56d94989191de3fcc4e002f93f7f1be5da476385dde410ddafbb70686acf00ea"}, - {file = "matplotlib-3.7.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e99bc9e65901bb9a7ce5e7bb24af03675cbd7c70b30ac670aa263240635999a4"}, - {file = "matplotlib-3.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb7d248c34a341cd4c31a06fd34d64306624c8cd8d0def7abb08792a5abfd556"}, - {file = "matplotlib-3.7.1-cp310-cp310-win32.whl", hash = "sha256:ce463ce590f3825b52e9fe5c19a3c6a69fd7675a39d589e8b5fbe772272b3a24"}, - {file = "matplotlib-3.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:3d7bc90727351fb841e4d8ae620d2d86d8ed92b50473cd2b42ce9186104ecbba"}, - {file = "matplotlib-3.7.1-cp311-cp311-macosx_10_12_universal2.whl", hash = "sha256:770a205966d641627fd5cf9d3cb4b6280a716522cd36b8b284a8eb1581310f61"}, - {file = "matplotlib-3.7.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f67bfdb83a8232cb7a92b869f9355d677bce24485c460b19d01970b64b2ed476"}, - {file = "matplotlib-3.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2bf092f9210e105f414a043b92af583c98f50050559616930d884387d0772aba"}, - {file = "matplotlib-3.7.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89768d84187f31717349c6bfadc0e0d8c321e8eb34522acec8a67b1236a66332"}, - {file = "matplotlib-3.7.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83111e6388dec67822e2534e13b243cc644c7494a4bb60584edbff91585a83c6"}, - {file = "matplotlib-3.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a867bf73a7eb808ef2afbca03bcdb785dae09595fbe550e1bab0cd023eba3de0"}, - {file = "matplotlib-3.7.1-cp311-cp311-win32.whl", hash = "sha256:fbdeeb58c0cf0595efe89c05c224e0a502d1aa6a8696e68a73c3efc6bc354304"}, - {file = "matplotlib-3.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:c0bd19c72ae53e6ab979f0ac6a3fafceb02d2ecafa023c5cca47acd934d10be7"}, - {file = "matplotlib-3.7.1-cp38-cp38-macosx_10_12_universal2.whl", hash = "sha256:6eb88d87cb2c49af00d3bbc33a003f89fd9f78d318848da029383bfc08ecfbfb"}, - {file = "matplotlib-3.7.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:cf0e4f727534b7b1457898c4f4ae838af1ef87c359b76dcd5330fa31893a3ac7"}, - {file = "matplotlib-3.7.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:46a561d23b91f30bccfd25429c3c706afe7d73a5cc64ef2dfaf2b2ac47c1a5dc"}, - {file = "matplotlib-3.7.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8704726d33e9aa8a6d5215044b8d00804561971163563e6e6591f9dcf64340cc"}, - {file = "matplotlib-3.7.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4cf327e98ecf08fcbb82685acaf1939d3338548620ab8dfa02828706402c34de"}, - {file = "matplotlib-3.7.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:617f14ae9d53292ece33f45cba8503494ee199a75b44de7717964f70637a36aa"}, - {file = "matplotlib-3.7.1-cp38-cp38-win32.whl", hash = "sha256:7c9a4b2da6fac77bcc41b1ea95fadb314e92508bf5493ceff058e727e7ecf5b0"}, - {file = "matplotlib-3.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:14645aad967684e92fc349493fa10c08a6da514b3d03a5931a1bac26e6792bd1"}, - {file = "matplotlib-3.7.1-cp39-cp39-macosx_10_12_universal2.whl", hash = "sha256:81a6b377ea444336538638d31fdb39af6be1a043ca5e343fe18d0f17e098770b"}, - {file = "matplotlib-3.7.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:28506a03bd7f3fe59cd3cd4ceb2a8d8a2b1db41afede01f66c42561b9be7b4b7"}, - {file = "matplotlib-3.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8c587963b85ce41e0a8af53b9b2de8dddbf5ece4c34553f7bd9d066148dc719c"}, - {file = "matplotlib-3.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8bf26ade3ff0f27668989d98c8435ce9327d24cffb7f07d24ef609e33d582439"}, - {file = "matplotlib-3.7.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:def58098f96a05f90af7e92fd127d21a287068202aa43b2a93476170ebd99e87"}, - {file = "matplotlib-3.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f883a22a56a84dba3b588696a2b8a1ab0d2c3d41be53264115c71b0a942d8fdb"}, - {file = "matplotlib-3.7.1-cp39-cp39-win32.whl", hash = "sha256:4f99e1b234c30c1e9714610eb0c6d2f11809c9c78c984a613ae539ea2ad2eb4b"}, - {file = "matplotlib-3.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:3ba2af245e36990facf67fde840a760128ddd71210b2ab6406e640188d69d136"}, - {file = "matplotlib-3.7.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3032884084f541163f295db8a6536e0abb0db464008fadca6c98aaf84ccf4717"}, - {file = "matplotlib-3.7.1-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a2cb34336110e0ed8bb4f650e817eed61fa064acbefeb3591f1b33e3a84fd96"}, - {file = "matplotlib-3.7.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b867e2f952ed592237a1828f027d332d8ee219ad722345b79a001f49df0936eb"}, - {file = "matplotlib-3.7.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:57bfb8c8ea253be947ccb2bc2d1bb3862c2bccc662ad1b4626e1f5e004557042"}, - {file = "matplotlib-3.7.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:438196cdf5dc8d39b50a45cb6e3f6274edbcf2254f85fa9b895bf85851c3a613"}, - {file = "matplotlib-3.7.1-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:21e9cff1a58d42e74d01153360de92b326708fb205250150018a52c70f43c290"}, - {file = "matplotlib-3.7.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75d4725d70b7c03e082bbb8a34639ede17f333d7247f56caceb3801cb6ff703d"}, - {file = "matplotlib-3.7.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:97cc368a7268141afb5690760921765ed34867ffb9655dd325ed207af85c7529"}, - {file = "matplotlib-3.7.1.tar.gz", hash = "sha256:7b73305f25eab4541bd7ee0b96d87e53ae9c9f1823be5659b806cd85786fe882"}, +python-versions = ">=3.9" +files = [ + {file = "matplotlib-3.8.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:cf60138ccc8004f117ab2a2bad513cc4d122e55864b4fe7adf4db20ca68a078f"}, + {file = "matplotlib-3.8.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f557156f7116be3340cdeef7f128fa99b0d5d287d5f41a16e169819dcf22357"}, + {file = "matplotlib-3.8.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f386cf162b059809ecfac3bcc491a9ea17da69fa35c8ded8ad154cd4b933d5ec"}, + {file = "matplotlib-3.8.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3c5f96f57b0369c288bf6f9b5274ba45787f7e0589a34d24bdbaf6d3344632f"}, + {file = "matplotlib-3.8.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:83e0f72e2c116ca7e571c57aa29b0fe697d4c6425c4e87c6e994159e0c008635"}, + {file = "matplotlib-3.8.3-cp310-cp310-win_amd64.whl", hash = "sha256:1c5c8290074ba31a41db1dc332dc2b62def469ff33766cbe325d32a3ee291aea"}, + {file = "matplotlib-3.8.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:5184e07c7e1d6d1481862ee361905b7059f7fe065fc837f7c3dc11eeb3f2f900"}, + {file = "matplotlib-3.8.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d7e7e0993d0758933b1a241a432b42c2db22dfa37d4108342ab4afb9557cbe3e"}, + {file = "matplotlib-3.8.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:04b36ad07eac9740fc76c2aa16edf94e50b297d6eb4c081e3add863de4bb19a7"}, + {file = "matplotlib-3.8.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c42dae72a62f14982f1474f7e5c9959fc4bc70c9de11cc5244c6e766200ba65"}, + {file = "matplotlib-3.8.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bf5932eee0d428192c40b7eac1399d608f5d995f975cdb9d1e6b48539a5ad8d0"}, + {file = "matplotlib-3.8.3-cp311-cp311-win_amd64.whl", hash = "sha256:40321634e3a05ed02abf7c7b47a50be50b53ef3eaa3a573847431a545585b407"}, + {file = "matplotlib-3.8.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:09074f8057917d17ab52c242fdf4916f30e99959c1908958b1fc6032e2d0f6d4"}, + {file = "matplotlib-3.8.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5745f6d0fb5acfabbb2790318db03809a253096e98c91b9a31969df28ee604aa"}, + {file = "matplotlib-3.8.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97653d869a71721b639714b42d87cda4cfee0ee74b47c569e4874c7590c55c5"}, + {file = "matplotlib-3.8.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:242489efdb75b690c9c2e70bb5c6550727058c8a614e4c7716f363c27e10bba1"}, + {file = "matplotlib-3.8.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:83c0653c64b73926730bd9ea14aa0f50f202ba187c307a881673bad4985967b7"}, + {file = "matplotlib-3.8.3-cp312-cp312-win_amd64.whl", hash = "sha256:ef6c1025a570354297d6c15f7d0f296d95f88bd3850066b7f1e7b4f2f4c13a39"}, + {file = "matplotlib-3.8.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c4af3f7317f8a1009bbb2d0bf23dfaba859eb7dd4ccbd604eba146dccaaaf0a4"}, + {file = "matplotlib-3.8.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4c6e00a65d017d26009bac6808f637b75ceade3e1ff91a138576f6b3065eeeba"}, + {file = "matplotlib-3.8.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7b49ab49a3bea17802df6872f8d44f664ba8f9be0632a60c99b20b6db2165b7"}, + {file = "matplotlib-3.8.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6728dde0a3997396b053602dbd907a9bd64ec7d5cf99e728b404083698d3ca01"}, + {file = "matplotlib-3.8.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:813925d08fb86aba139f2d31864928d67511f64e5945ca909ad5bc09a96189bb"}, + {file = "matplotlib-3.8.3-cp39-cp39-win_amd64.whl", hash = "sha256:cd3a0c2be76f4e7be03d34a14d49ded6acf22ef61f88da600a18a5cd8b3c5f3c"}, + {file = "matplotlib-3.8.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:fa93695d5c08544f4a0dfd0965f378e7afc410d8672816aff1e81be1f45dbf2e"}, + {file = "matplotlib-3.8.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9764df0e8778f06414b9d281a75235c1e85071f64bb5d71564b97c1306a2afc"}, + {file = "matplotlib-3.8.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:5e431a09e6fab4012b01fc155db0ce6dccacdbabe8198197f523a4ef4805eb26"}, + {file = "matplotlib-3.8.3.tar.gz", hash = "sha256:7b416239e9ae38be54b028abbf9048aff5054a9aba5416bef0bd17f9162ce161"}, ] [package.dependencies] @@ -764,10 +808,10 @@ contourpy = ">=1.0.1" cycler = ">=0.10" fonttools = ">=4.22.0" importlib-resources = {version = ">=3.2.0", markers = "python_version < \"3.10\""} -kiwisolver = ">=1.0.1" -numpy = ">=1.20" +kiwisolver = ">=1.3.1" +numpy = ">=1.21,<2" packaging = ">=20.0" -pillow = ">=6.2.0" +pillow = ">=8" pyparsing = ">=2.3.1" python-dateutil = ">=2.7" @@ -784,48 +828,49 @@ files = [ [[package]] name = "mypy" -version = "1.0.1" +version = "1.9.0" description = "Optional static typing for Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "mypy-1.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:71a808334d3f41ef011faa5a5cd8153606df5fc0b56de5b2e89566c8093a0c9a"}, - {file = "mypy-1.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:920169f0184215eef19294fa86ea49ffd4635dedfdea2b57e45cb4ee85d5ccaf"}, - {file = "mypy-1.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27a0f74a298769d9fdc8498fcb4f2beb86f0564bcdb1a37b58cbbe78e55cf8c0"}, - {file = "mypy-1.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:65b122a993d9c81ea0bfde7689b3365318a88bde952e4dfa1b3a8b4ac05d168b"}, - {file = "mypy-1.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:5deb252fd42a77add936b463033a59b8e48eb2eaec2976d76b6878d031933fe4"}, - {file = "mypy-1.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2013226d17f20468f34feddd6aae4635a55f79626549099354ce641bc7d40262"}, - {file = "mypy-1.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:48525aec92b47baed9b3380371ab8ab6e63a5aab317347dfe9e55e02aaad22e8"}, - {file = "mypy-1.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c96b8a0c019fe29040d520d9257d8c8f122a7343a8307bf8d6d4a43f5c5bfcc8"}, - {file = "mypy-1.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:448de661536d270ce04f2d7dddaa49b2fdba6e3bd8a83212164d4174ff43aa65"}, - {file = "mypy-1.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:d42a98e76070a365a1d1c220fcac8aa4ada12ae0db679cb4d910fabefc88b994"}, - {file = "mypy-1.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e64f48c6176e243ad015e995de05af7f22bbe370dbb5b32bd6988438ec873919"}, - {file = "mypy-1.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fdd63e4f50e3538617887e9aee91855368d9fc1dea30da743837b0df7373bc4"}, - {file = "mypy-1.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:dbeb24514c4acbc78d205f85dd0e800f34062efcc1f4a4857c57e4b4b8712bff"}, - {file = "mypy-1.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a2948c40a7dd46c1c33765718936669dc1f628f134013b02ff5ac6c7ef6942bf"}, - {file = "mypy-1.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5bc8d6bd3b274dd3846597855d96d38d947aedba18776aa998a8d46fabdaed76"}, - {file = "mypy-1.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:17455cda53eeee0a4adb6371a21dd3dbf465897de82843751cf822605d152c8c"}, - {file = "mypy-1.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e831662208055b006eef68392a768ff83596035ffd6d846786578ba1714ba8f6"}, - {file = "mypy-1.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e60d0b09f62ae97a94605c3f73fd952395286cf3e3b9e7b97f60b01ddfbbda88"}, - {file = "mypy-1.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:0af4f0e20706aadf4e6f8f8dc5ab739089146b83fd53cb4a7e0e850ef3de0bb6"}, - {file = "mypy-1.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:24189f23dc66f83b839bd1cce2dfc356020dfc9a8bae03978477b15be61b062e"}, - {file = "mypy-1.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:93a85495fb13dc484251b4c1fd7a5ac370cd0d812bbfc3b39c1bafefe95275d5"}, - {file = "mypy-1.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f546ac34093c6ce33f6278f7c88f0f147a4849386d3bf3ae193702f4fe31407"}, - {file = "mypy-1.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c6c2ccb7af7154673c591189c3687b013122c5a891bb5651eca3db8e6c6c55bd"}, - {file = "mypy-1.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:15b5a824b58c7c822c51bc66308e759243c32631896743f030daf449fe3677f3"}, - {file = "mypy-1.0.1-py3-none-any.whl", hash = "sha256:eda5c8b9949ed411ff752b9a01adda31afe7eae1e53e946dbdf9db23865e66c4"}, - {file = "mypy-1.0.1.tar.gz", hash = "sha256:28cea5a6392bb43d266782983b5a4216c25544cd7d80be681a155ddcdafd152d"}, + {file = "mypy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8a67616990062232ee4c3952f41c779afac41405806042a8126fe96e098419f"}, + {file = "mypy-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d357423fa57a489e8c47b7c85dfb96698caba13d66e086b412298a1a0ea3b0ed"}, + {file = "mypy-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49c87c15aed320de9b438ae7b00c1ac91cd393c1b854c2ce538e2a72d55df150"}, + {file = "mypy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:48533cdd345c3c2e5ef48ba3b0d3880b257b423e7995dada04248725c6f77374"}, + {file = "mypy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:4d3dbd346cfec7cb98e6cbb6e0f3c23618af826316188d587d1c1bc34f0ede03"}, + {file = "mypy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:653265f9a2784db65bfca694d1edd23093ce49740b2244cde583aeb134c008f3"}, + {file = "mypy-1.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a3c007ff3ee90f69cf0a15cbcdf0995749569b86b6d2f327af01fd1b8aee9dc"}, + {file = "mypy-1.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2418488264eb41f69cc64a69a745fad4a8f86649af4b1041a4c64ee61fc61129"}, + {file = "mypy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:68edad3dc7d70f2f17ae4c6c1b9471a56138ca22722487eebacfd1eb5321d612"}, + {file = "mypy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:85ca5fcc24f0b4aeedc1d02f93707bccc04733f21d41c88334c5482219b1ccb3"}, + {file = "mypy-1.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aceb1db093b04db5cd390821464504111b8ec3e351eb85afd1433490163d60cd"}, + {file = "mypy-1.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0235391f1c6f6ce487b23b9dbd1327b4ec33bb93934aa986efe8a9563d9349e6"}, + {file = "mypy-1.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4d5ddc13421ba3e2e082a6c2d74c2ddb3979c39b582dacd53dd5d9431237185"}, + {file = "mypy-1.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:190da1ee69b427d7efa8aa0d5e5ccd67a4fb04038c380237a0d96829cb157913"}, + {file = "mypy-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:fe28657de3bfec596bbeef01cb219833ad9d38dd5393fc649f4b366840baefe6"}, + {file = "mypy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e54396d70be04b34f31d2edf3362c1edd023246c82f1730bbf8768c28db5361b"}, + {file = "mypy-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5e6061f44f2313b94f920e91b204ec600982961e07a17e0f6cd83371cb23f5c2"}, + {file = "mypy-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a10926e5473c5fc3da8abb04119a1f5811a236dc3a38d92015cb1e6ba4cb9e"}, + {file = "mypy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b685154e22e4e9199fc95f298661deea28aaede5ae16ccc8cbb1045e716b3e04"}, + {file = "mypy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:5d741d3fc7c4da608764073089e5f58ef6352bedc223ff58f2f038c2c4698a89"}, + {file = "mypy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:587ce887f75dd9700252a3abbc9c97bbe165a4a630597845c61279cf32dfbf02"}, + {file = "mypy-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f88566144752999351725ac623471661c9d1cd8caa0134ff98cceeea181789f4"}, + {file = "mypy-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61758fabd58ce4b0720ae1e2fea5cfd4431591d6d590b197775329264f86311d"}, + {file = "mypy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e49499be624dead83927e70c756970a0bc8240e9f769389cdf5714b0784ca6bf"}, + {file = "mypy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:571741dc4194b4f82d344b15e8837e8c5fcc462d66d076748142327626a1b6e9"}, + {file = "mypy-1.9.0-py3-none-any.whl", hash = "sha256:a260627a570559181a9ea5de61ac6297aa5af202f06fd7ab093ce74e7181e43e"}, + {file = "mypy-1.9.0.tar.gz", hash = "sha256:3cc5da0127e6a478cddd906068496a97a7618a21ce9b54bde5bf7e539c7af974"}, ] [package.dependencies] -mypy-extensions = ">=0.4.3" +mypy-extensions = ">=1.0.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = ">=3.10" +typing-extensions = ">=4.1.0" [package.extras] dmypy = ["psutil (>=4.0)"] install-types = ["pip"] -python2 = ["typed-ast (>=1.4.0,<2)"] +mypyc = ["setuptools (>=50)"] reports = ["lxml"] [[package]] @@ -841,39 +886,47 @@ files = [ [[package]] name = "numpy" -version = "1.24.2" +version = "1.26.4" description = "Fundamental package for array computing in Python" optional = false -python-versions = ">=3.8" -files = [ - {file = "numpy-1.24.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eef70b4fc1e872ebddc38cddacc87c19a3709c0e3e5d20bf3954c147b1dd941d"}, - {file = "numpy-1.24.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8d2859428712785e8a8b7d2b3ef0a1d1565892367b32f915c4a4df44d0e64f5"}, - {file = "numpy-1.24.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6524630f71631be2dabe0c541e7675db82651eb998496bbe16bc4f77f0772253"}, - {file = "numpy-1.24.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a51725a815a6188c662fb66fb32077709a9ca38053f0274640293a14fdd22978"}, - {file = "numpy-1.24.2-cp310-cp310-win32.whl", hash = "sha256:2620e8592136e073bd12ee4536149380695fbe9ebeae845b81237f986479ffc9"}, - {file = "numpy-1.24.2-cp310-cp310-win_amd64.whl", hash = "sha256:97cf27e51fa078078c649a51d7ade3c92d9e709ba2bfb97493007103c741f1d0"}, - {file = "numpy-1.24.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7de8fdde0003f4294655aa5d5f0a89c26b9f22c0a58790c38fae1ed392d44a5a"}, - {file = "numpy-1.24.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4173bde9fa2a005c2c6e2ea8ac1618e2ed2c1c6ec8a7657237854d42094123a0"}, - {file = "numpy-1.24.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cecaed30dc14123020f77b03601559fff3e6cd0c048f8b5289f4eeabb0eb281"}, - {file = "numpy-1.24.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a23f8440561a633204a67fb44617ce2a299beecf3295f0d13c495518908e910"}, - {file = "numpy-1.24.2-cp311-cp311-win32.whl", hash = "sha256:e428c4fbfa085f947b536706a2fc349245d7baa8334f0c5723c56a10595f9b95"}, - {file = "numpy-1.24.2-cp311-cp311-win_amd64.whl", hash = "sha256:557d42778a6869c2162deb40ad82612645e21d79e11c1dc62c6e82a2220ffb04"}, - {file = "numpy-1.24.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d0a2db9d20117bf523dde15858398e7c0858aadca7c0f088ac0d6edd360e9ad2"}, - {file = "numpy-1.24.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c72a6b2f4af1adfe193f7beb91ddf708ff867a3f977ef2ec53c0ffb8283ab9f5"}, - {file = "numpy-1.24.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c29e6bd0ec49a44d7690ecb623a8eac5ab8a923bce0bea6293953992edf3a76a"}, - {file = "numpy-1.24.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2eabd64ddb96a1239791da78fa5f4e1693ae2dadc82a76bc76a14cbb2b966e96"}, - {file = "numpy-1.24.2-cp38-cp38-win32.whl", hash = "sha256:e3ab5d32784e843fc0dd3ab6dcafc67ef806e6b6828dc6af2f689be0eb4d781d"}, - {file = "numpy-1.24.2-cp38-cp38-win_amd64.whl", hash = "sha256:76807b4063f0002c8532cfeac47a3068a69561e9c8715efdad3c642eb27c0756"}, - {file = "numpy-1.24.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4199e7cfc307a778f72d293372736223e39ec9ac096ff0a2e64853b866a8e18a"}, - {file = "numpy-1.24.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:adbdce121896fd3a17a77ab0b0b5eedf05a9834a18699db6829a64e1dfccca7f"}, - {file = "numpy-1.24.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:889b2cc88b837d86eda1b17008ebeb679d82875022200c6e8e4ce6cf549b7acb"}, - {file = "numpy-1.24.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f64bb98ac59b3ea3bf74b02f13836eb2e24e48e0ab0145bbda646295769bd780"}, - {file = "numpy-1.24.2-cp39-cp39-win32.whl", hash = "sha256:63e45511ee4d9d976637d11e6c9864eae50e12dc9598f531c035265991910468"}, - {file = "numpy-1.24.2-cp39-cp39-win_amd64.whl", hash = "sha256:a77d3e1163a7770164404607b7ba3967fb49b24782a6ef85d9b5f54126cc39e5"}, - {file = "numpy-1.24.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:92011118955724465fb6853def593cf397b4a1367495e0b59a7e69d40c4eb71d"}, - {file = "numpy-1.24.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9006288bcf4895917d02583cf3411f98631275bc67cce355a7f39f8c14338fa"}, - {file = "numpy-1.24.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:150947adbdfeceec4e5926d956a06865c1c690f2fd902efede4ca6fe2e657c3f"}, - {file = "numpy-1.24.2.tar.gz", hash = "sha256:003a9f530e880cb2cd177cba1af7220b9aa42def9c4afc2a2fc3ee6be7eb2b22"}, +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]] @@ -894,72 +947,96 @@ signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] [[package]] name = "packaging" -version = "23.0" +version = "24.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.7" files = [ - {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, - {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, ] [[package]] name = "pandas" -version = "1.5.3" +version = "2.2.1" description = "Powerful data structures for data analysis, time series, and statistics" optional = false -python-versions = ">=3.8" -files = [ - {file = "pandas-1.5.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3749077d86e3a2f0ed51367f30bf5b82e131cc0f14260c4d3e499186fccc4406"}, - {file = "pandas-1.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:972d8a45395f2a2d26733eb8d0f629b2f90bebe8e8eddbb8829b180c09639572"}, - {file = "pandas-1.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:50869a35cbb0f2e0cd5ec04b191e7b12ed688874bd05dd777c19b28cbea90996"}, - {file = "pandas-1.5.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3ac844a0fe00bfaeb2c9b51ab1424e5c8744f89860b138434a363b1f620f354"}, - {file = "pandas-1.5.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a0a56cef15fd1586726dace5616db75ebcfec9179a3a55e78f72c5639fa2a23"}, - {file = "pandas-1.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:478ff646ca42b20376e4ed3fa2e8d7341e8a63105586efe54fa2508ee087f328"}, - {file = "pandas-1.5.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6973549c01ca91ec96199e940495219c887ea815b2083722821f1d7abfa2b4dc"}, - {file = "pandas-1.5.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c39a8da13cede5adcd3be1182883aea1c925476f4e84b2807a46e2775306305d"}, - {file = "pandas-1.5.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f76d097d12c82a535fda9dfe5e8dd4127952b45fea9b0276cb30cca5ea313fbc"}, - {file = "pandas-1.5.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e474390e60ed609cec869b0da796ad94f420bb057d86784191eefc62b65819ae"}, - {file = "pandas-1.5.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f2b952406a1588ad4cad5b3f55f520e82e902388a6d5a4a91baa8d38d23c7f6"}, - {file = "pandas-1.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:bc4c368f42b551bf72fac35c5128963a171b40dce866fb066540eeaf46faa003"}, - {file = "pandas-1.5.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:14e45300521902689a81f3f41386dc86f19b8ba8dd5ac5a3c7010ef8d2932813"}, - {file = "pandas-1.5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9842b6f4b8479e41968eced654487258ed81df7d1c9b7b870ceea24ed9459b31"}, - {file = "pandas-1.5.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:26d9c71772c7afb9d5046e6e9cf42d83dd147b5cf5bcb9d97252077118543792"}, - {file = "pandas-1.5.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fbcb19d6fceb9e946b3e23258757c7b225ba450990d9ed63ccceeb8cae609f7"}, - {file = "pandas-1.5.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:565fa34a5434d38e9d250af3c12ff931abaf88050551d9fbcdfafca50d62babf"}, - {file = "pandas-1.5.3-cp38-cp38-win32.whl", hash = "sha256:87bd9c03da1ac870a6d2c8902a0e1fd4267ca00f13bc494c9e5a9020920e1d51"}, - {file = "pandas-1.5.3-cp38-cp38-win_amd64.whl", hash = "sha256:41179ce559943d83a9b4bbacb736b04c928b095b5f25dd2b7389eda08f46f373"}, - {file = "pandas-1.5.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c74a62747864ed568f5a82a49a23a8d7fe171d0c69038b38cedf0976831296fa"}, - {file = "pandas-1.5.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c4c00e0b0597c8e4f59e8d461f797e5d70b4d025880516a8261b2817c47759ee"}, - {file = "pandas-1.5.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a50d9a4336a9621cab7b8eb3fb11adb82de58f9b91d84c2cd526576b881a0c5a"}, - {file = "pandas-1.5.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd05f7783b3274aa206a1af06f0ceed3f9b412cf665b7247eacd83be41cf7bf0"}, - {file = "pandas-1.5.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f69c4029613de47816b1bb30ff5ac778686688751a5e9c99ad8c7031f6508e5"}, - {file = "pandas-1.5.3-cp39-cp39-win32.whl", hash = "sha256:7cec0bee9f294e5de5bbfc14d0573f65526071029d036b753ee6507d2a21480a"}, - {file = "pandas-1.5.3-cp39-cp39-win_amd64.whl", hash = "sha256:dfd681c5dc216037e0b0a2c821f5ed99ba9f03ebcf119c7dac0e9a7b960b9ec9"}, - {file = "pandas-1.5.3.tar.gz", hash = "sha256:74a3fd7e5a7ec052f183273dc7b0acd3a863edf7520f5d3a1765c04ffdb3b0b1"}, +python-versions = ">=3.9" +files = [ + {file = "pandas-2.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8df8612be9cd1c7797c93e1c5df861b2ddda0b48b08f2c3eaa0702cf88fb5f88"}, + {file = "pandas-2.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0f573ab277252ed9aaf38240f3b54cfc90fff8e5cab70411ee1d03f5d51f3944"}, + {file = "pandas-2.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f02a3a6c83df4026e55b63c1f06476c9aa3ed6af3d89b4f04ea656ccdaaaa359"}, + {file = "pandas-2.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c38ce92cb22a4bea4e3929429aa1067a454dcc9c335799af93ba9be21b6beb51"}, + {file = "pandas-2.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c2ce852e1cf2509a69e98358e8458775f89599566ac3775e70419b98615f4b06"}, + {file = "pandas-2.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:53680dc9b2519cbf609c62db3ed7c0b499077c7fefda564e330286e619ff0dd9"}, + {file = "pandas-2.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:94e714a1cca63e4f5939cdce5f29ba8d415d85166be3441165edd427dc9f6bc0"}, + {file = "pandas-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f821213d48f4ab353d20ebc24e4faf94ba40d76680642fb7ce2ea31a3ad94f9b"}, + {file = "pandas-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c70e00c2d894cb230e5c15e4b1e1e6b2b478e09cf27cc593a11ef955b9ecc81a"}, + {file = "pandas-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e97fbb5387c69209f134893abc788a6486dbf2f9e511070ca05eed4b930b1b02"}, + {file = "pandas-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101d0eb9c5361aa0146f500773395a03839a5e6ecde4d4b6ced88b7e5a1a6403"}, + {file = "pandas-2.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7d2ed41c319c9fb4fd454fe25372028dfa417aacb9790f68171b2e3f06eae8cd"}, + {file = "pandas-2.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:af5d3c00557d657c8773ef9ee702c61dd13b9d7426794c9dfeb1dc4a0bf0ebc7"}, + {file = "pandas-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:06cf591dbaefb6da9de8472535b185cba556d0ce2e6ed28e21d919704fef1a9e"}, + {file = "pandas-2.2.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:88ecb5c01bb9ca927ebc4098136038519aa5d66b44671861ffab754cae75102c"}, + {file = "pandas-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:04f6ec3baec203c13e3f8b139fb0f9f86cd8c0b94603ae3ae8ce9a422e9f5bee"}, + {file = "pandas-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a935a90a76c44fe170d01e90a3594beef9e9a6220021acfb26053d01426f7dc2"}, + {file = "pandas-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c391f594aae2fd9f679d419e9a4d5ba4bce5bb13f6a989195656e7dc4b95c8f0"}, + {file = "pandas-2.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9d1265545f579edf3f8f0cb6f89f234f5e44ba725a34d86535b1a1d38decbccc"}, + {file = "pandas-2.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:11940e9e3056576ac3244baef2fedade891977bcc1cb7e5cc8f8cc7d603edc89"}, + {file = "pandas-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:4acf681325ee1c7f950d058b05a820441075b0dd9a2adf5c4835b9bc056bf4fb"}, + {file = "pandas-2.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9bd8a40f47080825af4317d0340c656744f2bfdb6819f818e6ba3cd24c0e1397"}, + {file = "pandas-2.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:df0c37ebd19e11d089ceba66eba59a168242fc6b7155cba4ffffa6eccdfb8f16"}, + {file = "pandas-2.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:739cc70eaf17d57608639e74d63387b0d8594ce02f69e7a0b046f117974b3019"}, + {file = "pandas-2.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9d3558d263073ed95e46f4650becff0c5e1ffe0fc3a015de3c79283dfbdb3df"}, + {file = "pandas-2.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4aa1d8707812a658debf03824016bf5ea0d516afdea29b7dc14cf687bc4d4ec6"}, + {file = "pandas-2.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:76f27a809cda87e07f192f001d11adc2b930e93a2b0c4a236fde5429527423be"}, + {file = "pandas-2.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:1ba21b1d5c0e43416218db63037dbe1a01fc101dc6e6024bcad08123e48004ab"}, + {file = "pandas-2.2.1.tar.gz", hash = "sha256:0ab90f87093c13f3e8fa45b48ba9f39181046e8f3317d3aadb2fffbb1b978572"}, ] [package.dependencies] numpy = [ - {version = ">=1.20.3", markers = "python_version < \"3.10\""}, - {version = ">=1.21.0", markers = "python_version >= \"3.10\""}, - {version = ">=1.23.2", markers = "python_version >= \"3.11\""}, + {version = ">=1.22.4,<2", markers = "python_version < \"3.11\""}, + {version = ">=1.23.2,<2", markers = "python_version == \"3.11\""}, ] -python-dateutil = ">=2.8.1" +python-dateutil = ">=2.8.2" pytz = ">=2020.1" +tzdata = ">=2022.7" [package.extras] -test = ["hypothesis (>=5.5.3)", "pytest (>=6.0)", "pytest-xdist (>=1.31)"] +all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)", "beautifulsoup4 (>=4.11.2)", "bottleneck (>=1.3.6)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=2022.12.0)", "fsspec (>=2022.11.0)", "gcsfs (>=2022.11.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.9.2)", "matplotlib (>=3.6.3)", "numba (>=0.56.4)", "numexpr (>=2.8.4)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "pandas-gbq (>=0.19.0)", "psycopg2 (>=2.9.6)", "pyarrow (>=10.0.1)", "pymysql (>=1.0.2)", "pyreadstat (>=1.2.0)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "qtpy (>=2.3.0)", "s3fs (>=2022.11.0)", "scipy (>=1.10.0)", "tables (>=3.8.0)", "tabulate (>=0.9.0)", "xarray (>=2022.12.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)", "zstandard (>=0.19.0)"] +aws = ["s3fs (>=2022.11.0)"] +clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.3.0)"] +compression = ["zstandard (>=0.19.0)"] +computation = ["scipy (>=1.10.0)", "xarray (>=2022.12.0)"] +consortium-standard = ["dataframe-api-compat (>=0.1.7)"] +excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)"] +feather = ["pyarrow (>=10.0.1)"] +fss = ["fsspec (>=2022.11.0)"] +gcp = ["gcsfs (>=2022.11.0)", "pandas-gbq (>=0.19.0)"] +hdf5 = ["tables (>=3.8.0)"] +html = ["beautifulsoup4 (>=4.11.2)", "html5lib (>=1.1)", "lxml (>=4.9.2)"] +mysql = ["SQLAlchemy (>=2.0.0)", "pymysql (>=1.0.2)"] +output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.9.0)"] +parquet = ["pyarrow (>=10.0.1)"] +performance = ["bottleneck (>=1.3.6)", "numba (>=0.56.4)", "numexpr (>=2.8.4)"] +plot = ["matplotlib (>=3.6.3)"] +postgresql = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "psycopg2 (>=2.9.6)"] +pyarrow = ["pyarrow (>=10.0.1)"] +spss = ["pyreadstat (>=1.2.0)"] +sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)"] +test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] +xml = ["lxml (>=4.9.2)"] [[package]] name = "pathspec" -version = "0.11.0" +version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pathspec-0.11.0-py3-none-any.whl", hash = "sha256:3a66eb970cbac598f9e5ccb5b2cf58930cd8e3ed86d393d541eaf2d8b1705229"}, - {file = "pathspec-0.11.0.tar.gz", hash = "sha256:64d338d4e0914e91c1792321e6907b5a593f1ab1851de7fc269557a21b30ebbc"}, + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, ] [[package]] @@ -975,118 +1052,113 @@ files = [ [[package]] name = "pillow" -version = "9.4.0" +version = "10.2.0" description = "Python Imaging Library (Fork)" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "Pillow-9.4.0-1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b4b4e9dda4f4e4c4e6896f93e84a8f0bcca3b059de9ddf67dac3c334b1195e1"}, - {file = "Pillow-9.4.0-1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:fb5c1ad6bad98c57482236a21bf985ab0ef42bd51f7ad4e4538e89a997624e12"}, - {file = "Pillow-9.4.0-1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:f0caf4a5dcf610d96c3bd32932bfac8aee61c96e60481c2a0ea58da435e25acd"}, - {file = "Pillow-9.4.0-1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:3f4cc516e0b264c8d4ccd6b6cbc69a07c6d582d8337df79be1e15a5056b258c9"}, - {file = "Pillow-9.4.0-1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b8c2f6eb0df979ee99433d8b3f6d193d9590f735cf12274c108bd954e30ca858"}, - {file = "Pillow-9.4.0-1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b70756ec9417c34e097f987b4d8c510975216ad26ba6e57ccb53bc758f490dab"}, - {file = "Pillow-9.4.0-1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:43521ce2c4b865d385e78579a082b6ad1166ebed2b1a2293c3be1d68dd7ca3b9"}, - {file = "Pillow-9.4.0-2-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:9d9a62576b68cd90f7075876f4e8444487db5eeea0e4df3ba298ee38a8d067b0"}, - {file = "Pillow-9.4.0-2-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:87708d78a14d56a990fbf4f9cb350b7d89ee8988705e58e39bdf4d82c149210f"}, - {file = "Pillow-9.4.0-2-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:8a2b5874d17e72dfb80d917213abd55d7e1ed2479f38f001f264f7ce7bae757c"}, - {file = "Pillow-9.4.0-2-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:83125753a60cfc8c412de5896d10a0a405e0bd88d0470ad82e0869ddf0cb3848"}, - {file = "Pillow-9.4.0-2-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:9e5f94742033898bfe84c93c831a6f552bb629448d4072dd312306bab3bd96f1"}, - {file = "Pillow-9.4.0-2-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:013016af6b3a12a2f40b704677f8b51f72cb007dac785a9933d5c86a72a7fe33"}, - {file = "Pillow-9.4.0-2-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:99d92d148dd03fd19d16175b6d355cc1b01faf80dae93c6c3eb4163709edc0a9"}, - {file = "Pillow-9.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:2968c58feca624bb6c8502f9564dd187d0e1389964898f5e9e1fbc8533169157"}, - {file = "Pillow-9.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c5c1362c14aee73f50143d74389b2c158707b4abce2cb055b7ad37ce60738d47"}, - {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd752c5ff1b4a870b7661234694f24b1d2b9076b8bf337321a814c612665f343"}, - {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a3049a10261d7f2b6514d35bbb7a4dfc3ece4c4de14ef5876c4b7a23a0e566d"}, - {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16a8df99701f9095bea8a6c4b3197da105df6f74e6176c5b410bc2df2fd29a57"}, - {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:94cdff45173b1919350601f82d61365e792895e3c3a3443cf99819e6fbf717a5"}, - {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:ed3e4b4e1e6de75fdc16d3259098de7c6571b1a6cc863b1a49e7d3d53e036070"}, - {file = "Pillow-9.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5b2f8a31bd43e0f18172d8ac82347c8f37ef3e0b414431157718aa234991b28"}, - {file = "Pillow-9.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:09b89ddc95c248ee788328528e6a2996e09eaccddeeb82a5356e92645733be35"}, - {file = "Pillow-9.4.0-cp310-cp310-win32.whl", hash = "sha256:f09598b416ba39a8f489c124447b007fe865f786a89dbfa48bb5cf395693132a"}, - {file = "Pillow-9.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:f6e78171be3fb7941f9910ea15b4b14ec27725865a73c15277bc39f5ca4f8391"}, - {file = "Pillow-9.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:3fa1284762aacca6dc97474ee9c16f83990b8eeb6697f2ba17140d54b453e133"}, - {file = "Pillow-9.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:eaef5d2de3c7e9b21f1e762f289d17b726c2239a42b11e25446abf82b26ac132"}, - {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4dfdae195335abb4e89cc9762b2edc524f3c6e80d647a9a81bf81e17e3fb6f0"}, - {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6abfb51a82e919e3933eb137e17c4ae9c0475a25508ea88993bb59faf82f3b35"}, - {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:451f10ef963918e65b8869e17d67db5e2f4ab40e716ee6ce7129b0cde2876eab"}, - {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:6663977496d616b618b6cfa43ec86e479ee62b942e1da76a2c3daa1c75933ef4"}, - {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:60e7da3a3ad1812c128750fc1bc14a7ceeb8d29f77e0a2356a8fb2aa8925287d"}, - {file = "Pillow-9.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:19005a8e58b7c1796bc0167862b1f54a64d3b44ee5d48152b06bb861458bc0f8"}, - {file = "Pillow-9.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f715c32e774a60a337b2bb8ad9839b4abf75b267a0f18806f6f4f5f1688c4b5a"}, - {file = "Pillow-9.4.0-cp311-cp311-win32.whl", hash = "sha256:b222090c455d6d1a64e6b7bb5f4035c4dff479e22455c9eaa1bdd4c75b52c80c"}, - {file = "Pillow-9.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:ba6612b6548220ff5e9df85261bddc811a057b0b465a1226b39bfb8550616aee"}, - {file = "Pillow-9.4.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:5f532a2ad4d174eb73494e7397988e22bf427f91acc8e6ebf5bb10597b49c493"}, - {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dd5a9c3091a0f414a963d427f920368e2b6a4c2f7527fdd82cde8ef0bc7a327"}, - {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef21af928e807f10bf4141cad4746eee692a0dd3ff56cfb25fce076ec3cc8abe"}, - {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:847b114580c5cc9ebaf216dd8c8dbc6b00a3b7ab0131e173d7120e6deade1f57"}, - {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:653d7fb2df65efefbcbf81ef5fe5e5be931f1ee4332c2893ca638c9b11a409c4"}, - {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:46f39cab8bbf4a384ba7cb0bc8bae7b7062b6a11cfac1ca4bc144dea90d4a9f5"}, - {file = "Pillow-9.4.0-cp37-cp37m-win32.whl", hash = "sha256:7ac7594397698f77bce84382929747130765f66406dc2cd8b4ab4da68ade4c6e"}, - {file = "Pillow-9.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:46c259e87199041583658457372a183636ae8cd56dbf3f0755e0f376a7f9d0e6"}, - {file = "Pillow-9.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:0e51f608da093e5d9038c592b5b575cadc12fd748af1479b5e858045fff955a9"}, - {file = "Pillow-9.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:765cb54c0b8724a7c12c55146ae4647e0274a839fb6de7bcba841e04298e1011"}, - {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:519e14e2c49fcf7616d6d2cfc5c70adae95682ae20f0395e9280db85e8d6c4df"}, - {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d197df5489004db87d90b918033edbeee0bd6df3848a204bca3ff0a903bef837"}, - {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0845adc64fe9886db00f5ab68c4a8cd933ab749a87747555cec1c95acea64b0b"}, - {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:e1339790c083c5a4de48f688b4841f18df839eb3c9584a770cbd818b33e26d5d"}, - {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:a96e6e23f2b79433390273eaf8cc94fec9c6370842e577ab10dabdcc7ea0a66b"}, - {file = "Pillow-9.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7cfc287da09f9d2a7ec146ee4d72d6ea1342e770d975e49a8621bf54eaa8f30f"}, - {file = "Pillow-9.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d7081c084ceb58278dd3cf81f836bc818978c0ccc770cbbb202125ddabec6628"}, - {file = "Pillow-9.4.0-cp38-cp38-win32.whl", hash = "sha256:df41112ccce5d47770a0c13651479fbcd8793f34232a2dd9faeccb75eb5d0d0d"}, - {file = "Pillow-9.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:7a21222644ab69ddd9967cfe6f2bb420b460dae4289c9d40ff9a4896e7c35c9a"}, - {file = "Pillow-9.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0f3269304c1a7ce82f1759c12ce731ef9b6e95b6df829dccd9fe42912cc48569"}, - {file = "Pillow-9.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cb362e3b0976dc994857391b776ddaa8c13c28a16f80ac6522c23d5257156bed"}, - {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2e0f87144fcbbe54297cae708c5e7f9da21a4646523456b00cc956bd4c65815"}, - {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:28676836c7796805914b76b1837a40f76827ee0d5398f72f7dcc634bae7c6264"}, - {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0884ba7b515163a1a05440a138adeb722b8a6ae2c2b33aea93ea3118dd3a899e"}, - {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:53dcb50fbdc3fb2c55431a9b30caeb2f7027fcd2aeb501459464f0214200a503"}, - {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:e8c5cf126889a4de385c02a2c3d3aba4b00f70234bfddae82a5eaa3ee6d5e3e6"}, - {file = "Pillow-9.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6c6b1389ed66cdd174d040105123a5a1bc91d0aa7059c7261d20e583b6d8cbd2"}, - {file = "Pillow-9.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0dd4c681b82214b36273c18ca7ee87065a50e013112eea7d78c7a1b89a739153"}, - {file = "Pillow-9.4.0-cp39-cp39-win32.whl", hash = "sha256:6d9dfb9959a3b0039ee06c1a1a90dc23bac3b430842dcb97908ddde05870601c"}, - {file = "Pillow-9.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:54614444887e0d3043557d9dbc697dbb16cfb5a35d672b7a0fcc1ed0cf1c600b"}, - {file = "Pillow-9.4.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b9b752ab91e78234941e44abdecc07f1f0d8f51fb62941d32995b8161f68cfe5"}, - {file = "Pillow-9.4.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3b56206244dc8711f7e8b7d6cad4663917cd5b2d950799425076681e8766286"}, - {file = "Pillow-9.4.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aabdab8ec1e7ca7f1434d042bf8b1e92056245fb179790dc97ed040361f16bfd"}, - {file = "Pillow-9.4.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:db74f5562c09953b2c5f8ec4b7dfd3f5421f31811e97d1dbc0a7c93d6e3a24df"}, - {file = "Pillow-9.4.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e9d7747847c53a16a729b6ee5e737cf170f7a16611c143d95aa60a109a59c336"}, - {file = "Pillow-9.4.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b52ff4f4e002f828ea6483faf4c4e8deea8d743cf801b74910243c58acc6eda3"}, - {file = "Pillow-9.4.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:575d8912dca808edd9acd6f7795199332696d3469665ef26163cd090fa1f8bfa"}, - {file = "Pillow-9.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3c4ed2ff6760e98d262e0cc9c9a7f7b8a9f61aa4d47c58835cdaf7b0b8811bb"}, - {file = "Pillow-9.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e621b0246192d3b9cb1dc62c78cfa4c6f6d2ddc0ec207d43c0dedecb914f152a"}, - {file = "Pillow-9.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8f127e7b028900421cad64f51f75c051b628db17fb00e099eb148761eed598c9"}, - {file = "Pillow-9.4.0.tar.gz", hash = "sha256:a1c2d7780448eb93fbcc3789bf3916aa5720d942e37945f4056680317f1cd23e"}, + {file = "pillow-10.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:7823bdd049099efa16e4246bdf15e5a13dbb18a51b68fa06d6c1d4d8b99a796e"}, + {file = "pillow-10.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:83b2021f2ade7d1ed556bc50a399127d7fb245e725aa0113ebd05cfe88aaf588"}, + {file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fad5ff2f13d69b7e74ce5b4ecd12cc0ec530fcee76356cac6742785ff71c452"}, + {file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da2b52b37dad6d9ec64e653637a096905b258d2fc2b984c41ae7d08b938a67e4"}, + {file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:47c0995fc4e7f79b5cfcab1fc437ff2890b770440f7696a3ba065ee0fd496563"}, + {file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:322bdf3c9b556e9ffb18f93462e5f749d3444ce081290352c6070d014c93feb2"}, + {file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:51f1a1bffc50e2e9492e87d8e09a17c5eea8409cda8d3f277eb6edc82813c17c"}, + {file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69ffdd6120a4737710a9eee73e1d2e37db89b620f702754b8f6e62594471dee0"}, + {file = "pillow-10.2.0-cp310-cp310-win32.whl", hash = "sha256:c6dafac9e0f2b3c78df97e79af707cdc5ef8e88208d686a4847bab8266870023"}, + {file = "pillow-10.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:aebb6044806f2e16ecc07b2a2637ee1ef67a11840a66752751714a0d924adf72"}, + {file = "pillow-10.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:7049e301399273a0136ff39b84c3678e314f2158f50f517bc50285fb5ec847ad"}, + {file = "pillow-10.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35bb52c37f256f662abdfa49d2dfa6ce5d93281d323a9af377a120e89a9eafb5"}, + {file = "pillow-10.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c23f307202661071d94b5e384e1e1dc7dfb972a28a2310e4ee16103e66ddb67"}, + {file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:773efe0603db30c281521a7c0214cad7836c03b8ccff897beae9b47c0b657d61"}, + {file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11fa2e5984b949b0dd6d7a94d967743d87c577ff0b83392f17cb3990d0d2fd6e"}, + {file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:716d30ed977be8b37d3ef185fecb9e5a1d62d110dfbdcd1e2a122ab46fddb03f"}, + {file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a086c2af425c5f62a65e12fbf385f7c9fcb8f107d0849dba5839461a129cf311"}, + {file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c8de2789052ed501dd829e9cae8d3dcce7acb4777ea4a479c14521c942d395b1"}, + {file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:609448742444d9290fd687940ac0b57fb35e6fd92bdb65386e08e99af60bf757"}, + {file = "pillow-10.2.0-cp311-cp311-win32.whl", hash = "sha256:823ef7a27cf86df6597fa0671066c1b596f69eba53efa3d1e1cb8b30f3533068"}, + {file = "pillow-10.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:1da3b2703afd040cf65ec97efea81cfba59cdbed9c11d8efc5ab09df9509fc56"}, + {file = "pillow-10.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:edca80cbfb2b68d7b56930b84a0e45ae1694aeba0541f798e908a49d66b837f1"}, + {file = "pillow-10.2.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:1b5e1b74d1bd1b78bc3477528919414874748dd363e6272efd5abf7654e68bef"}, + {file = "pillow-10.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0eae2073305f451d8ecacb5474997c08569fb4eb4ac231ffa4ad7d342fdc25ac"}, + {file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7c2286c23cd350b80d2fc9d424fc797575fb16f854b831d16fd47ceec078f2c"}, + {file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e23412b5c41e58cec602f1135c57dfcf15482013ce6e5f093a86db69646a5aa"}, + {file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:52a50aa3fb3acb9cf7213573ef55d31d6eca37f5709c69e6858fe3bc04a5c2a2"}, + {file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:127cee571038f252a552760076407f9cff79761c3d436a12af6000cd182a9d04"}, + {file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8d12251f02d69d8310b046e82572ed486685c38f02176bd08baf216746eb947f"}, + {file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:54f1852cd531aa981bc0965b7d609f5f6cc8ce8c41b1139f6ed6b3c54ab82bfb"}, + {file = "pillow-10.2.0-cp312-cp312-win32.whl", hash = "sha256:257d8788df5ca62c980314053197f4d46eefedf4e6175bc9412f14412ec4ea2f"}, + {file = "pillow-10.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:154e939c5f0053a383de4fd3d3da48d9427a7e985f58af8e94d0b3c9fcfcf4f9"}, + {file = "pillow-10.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:f379abd2f1e3dddb2b61bc67977a6b5a0a3f7485538bcc6f39ec76163891ee48"}, + {file = "pillow-10.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8373c6c251f7ef8bda6675dd6d2b3a0fcc31edf1201266b5cf608b62a37407f9"}, + {file = "pillow-10.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:870ea1ada0899fd0b79643990809323b389d4d1d46c192f97342eeb6ee0b8483"}, + {file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4b6b1e20608493548b1f32bce8cca185bf0480983890403d3b8753e44077129"}, + {file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3031709084b6e7852d00479fd1d310b07d0ba82765f973b543c8af5061cf990e"}, + {file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:3ff074fc97dd4e80543a3e91f69d58889baf2002b6be64347ea8cf5533188213"}, + {file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:cb4c38abeef13c61d6916f264d4845fab99d7b711be96c326b84df9e3e0ff62d"}, + {file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b1b3020d90c2d8e1dae29cf3ce54f8094f7938460fb5ce8bc5c01450b01fbaf6"}, + {file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:170aeb00224ab3dc54230c797f8404507240dd868cf52066f66a41b33169bdbe"}, + {file = "pillow-10.2.0-cp38-cp38-win32.whl", hash = "sha256:c4225f5220f46b2fde568c74fca27ae9771536c2e29d7c04f4fb62c83275ac4e"}, + {file = "pillow-10.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:0689b5a8c5288bc0504d9fcee48f61a6a586b9b98514d7d29b840143d6734f39"}, + {file = "pillow-10.2.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b792a349405fbc0163190fde0dc7b3fef3c9268292586cf5645598b48e63dc67"}, + {file = "pillow-10.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c570f24be1e468e3f0ce7ef56a89a60f0e05b30a3669a459e419c6eac2c35364"}, + {file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8ecd059fdaf60c1963c58ceb8997b32e9dc1b911f5da5307aab614f1ce5c2fb"}, + {file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c365fd1703040de1ec284b176d6af5abe21b427cb3a5ff68e0759e1e313a5e7e"}, + {file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:70c61d4c475835a19b3a5aa42492409878bbca7438554a1f89d20d58a7c75c01"}, + {file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b6f491cdf80ae540738859d9766783e3b3c8e5bd37f5dfa0b76abdecc5081f13"}, + {file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d189550615b4948f45252d7f005e53c2040cea1af5b60d6f79491a6e147eef7"}, + {file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:49d9ba1ed0ef3e061088cd1e7538a0759aab559e2e0a80a36f9fd9d8c0c21591"}, + {file = "pillow-10.2.0-cp39-cp39-win32.whl", hash = "sha256:babf5acfede515f176833ed6028754cbcd0d206f7f614ea3447d67c33be12516"}, + {file = "pillow-10.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:0304004f8067386b477d20a518b50f3fa658a28d44e4116970abfcd94fac34a8"}, + {file = "pillow-10.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:0fb3e7fc88a14eacd303e90481ad983fd5b69c761e9e6ef94c983f91025da869"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:322209c642aabdd6207517e9739c704dc9f9db943015535783239022002f054a"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eedd52442c0a5ff4f887fab0c1c0bb164d8635b32c894bc1faf4c618dd89df2"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb28c753fd5eb3dd859b4ee95de66cc62af91bcff5db5f2571d32a520baf1f04"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:33870dc4653c5017bf4c8873e5488d8f8d5f8935e2f1fb9a2208c47cdd66efd2"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3c31822339516fb3c82d03f30e22b1d038da87ef27b6a78c9549888f8ceda39a"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a2b56ba36e05f973d450582fb015594aaa78834fefe8dfb8fcd79b93e64ba4c6"}, + {file = "pillow-10.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d8e6aeb9201e655354b3ad049cb77d19813ad4ece0df1249d3c793de3774f8c7"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:2247178effb34a77c11c0e8ac355c7a741ceca0a732b27bf11e747bbc950722f"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15587643b9e5eb26c48e49a7b33659790d28f190fc514a322d55da2fb5c2950e"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753cd8f2086b2b80180d9b3010dd4ed147efc167c90d3bf593fe2af21265e5a5"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7c8f97e8e7a9009bcacbe3766a36175056c12f9a44e6e6f2d5caad06dcfbf03b"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d1b35bcd6c5543b9cb547dee3150c93008f8dd0f1fef78fc0cd2b141c5baf58a"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fe4c15f6c9285dc54ce6553a3ce908ed37c8f3825b5a51a15c91442bb955b868"}, + {file = "pillow-10.2.0.tar.gz", hash = "sha256:e87f0b2c78157e12d7686b27d63c070fd65d994e8ddae6f328e0dcf4a0cd007e"}, ] [package.extras] -docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinxext-opengraph"] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] +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 = "platformdirs" -version = "3.0.0" +version = "4.2.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "platformdirs-3.0.0-py3-none-any.whl", hash = "sha256:b1d5eb14f221506f50d6604a561f4c5786d9e80355219694a1b244bcd96f4567"}, - {file = "platformdirs-3.0.0.tar.gz", hash = "sha256:8a1228abb1ef82d788f74139988b137e78692984ec7b08eaa6c65f1723af28f9"}, + {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, + {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, ] [package.extras] -docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] [[package]] name = "pluggy" -version = "1.0.0" +version = "1.4.0" description = "plugin and hook calling mechanisms for python" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, - {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, + {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, + {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, ] [package.extras] @@ -1114,13 +1186,13 @@ requests = "*" [[package]] name = "prometrix" -version = "0.1.10" -description = "" +version = "0.1.16" +description = "A Python Prometheus client for all Prometheus instances." optional = false python-versions = ">=3.8,<4.0" files = [ - {file = "prometrix-0.1.10-py3-none-any.whl", hash = "sha256:5caa0ee06d49d7ad1f881614edb81fe4b2f47f730bcd4286209627fdc41d550d"}, - {file = "prometrix-0.1.10.tar.gz", hash = "sha256:9ed61c0b77b503d38ce9c66d70742ad81a84b7afc6fbf63e8dbba9316b41a4df"}, + {file = "prometrix-0.1.16-py3-none-any.whl", hash = "sha256:373a6c652ee6381b3f3ab8c332ce56cc6acfa14b58085db52d46c1a0be659f95"}, + {file = "prometrix-0.1.16.tar.gz", hash = "sha256:ef15cf00181e1ff7c734685f364e6a076343c68ca8010f12f5c9cce6422eb8a2"}, ] [package.dependencies] @@ -1131,38 +1203,38 @@ pydantic = ">=1.8.1,<2.0.0" [[package]] name = "pyasn1" -version = "0.4.8" -description = "ASN.1 types and codecs" +version = "0.5.1" +description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" optional = false -python-versions = "*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ - {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"}, - {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"}, + {file = "pyasn1-0.5.1-py2.py3-none-any.whl", hash = "sha256:4439847c58d40b1d0a573d07e3856e95333f1976294494c325775aeca506eb58"}, + {file = "pyasn1-0.5.1.tar.gz", hash = "sha256:6d391a96e59b23130a5cfa74d6fd7f388dbbe26cc8f1edf39fdddf08d9d6676c"}, ] [[package]] name = "pyasn1-modules" -version = "0.2.8" -description = "A collection of ASN.1-based protocols modules." +version = "0.3.0" +description = "A collection of ASN.1-based protocols modules" optional = false -python-versions = "*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ - {file = "pyasn1-modules-0.2.8.tar.gz", hash = "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e"}, - {file = "pyasn1_modules-0.2.8-py2.py3-none-any.whl", hash = "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74"}, + {file = "pyasn1_modules-0.3.0-py2.py3-none-any.whl", hash = "sha256:d3ccd6ed470d9ffbc716be08bd90efbd44d0734bc9303818f7336070984a162d"}, + {file = "pyasn1_modules-0.3.0.tar.gz", hash = "sha256:5bd01446b736eb9d31512a30d46c1ac3395d676c6f3cafa4c03eb54b9925631c"}, ] [package.dependencies] -pyasn1 = ">=0.4.6,<0.5.0" +pyasn1 = ">=0.4.6,<0.6.0" [[package]] name = "pycodestyle" -version = "2.10.0" +version = "2.11.1" description = "Python style guide checker" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "pycodestyle-2.10.0-py2.py3-none-any.whl", hash = "sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610"}, - {file = "pycodestyle-2.10.0.tar.gz", hash = "sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053"}, + {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, + {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, ] [[package]] @@ -1219,48 +1291,49 @@ email = ["email-validator (>=1.0.3)"] [[package]] name = "pyflakes" -version = "3.0.1" +version = "3.1.0" description = "passive checker of Python programs" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "pyflakes-3.0.1-py2.py3-none-any.whl", hash = "sha256:ec55bf7fe21fff7f1ad2f7da62363d749e2a470500eab1b555334b67aa1ef8cf"}, - {file = "pyflakes-3.0.1.tar.gz", hash = "sha256:ec8b276a6b60bd80defed25add7e439881c19e64850afd9b346283d4165fd0fd"}, + {file = "pyflakes-3.1.0-py2.py3-none-any.whl", hash = "sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774"}, + {file = "pyflakes-3.1.0.tar.gz", hash = "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc"}, ] [[package]] name = "pygments" -version = "2.14.0" +version = "2.17.2" description = "Pygments is a syntax highlighting package written in Python." optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "Pygments-2.14.0-py3-none-any.whl", hash = "sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717"}, - {file = "Pygments-2.14.0.tar.gz", hash = "sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297"}, + {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, + {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, ] [package.extras] plugins = ["importlib-metadata"] +windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pyinstaller" -version = "5.9.0" +version = "5.13.2" description = "PyInstaller bundles a Python application and all its dependencies into a single package." optional = false -python-versions = "<3.12,>=3.7" +python-versions = "<3.13,>=3.7" files = [ - {file = "pyinstaller-5.9.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:93d7e8443a6b60745d42aa50f08730f6b419410832b4c616c4f1bb315f087661"}, - {file = "pyinstaller-5.9.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:3b2c34c3c3ddf38f68d9f5afbed82abe0f89d53014c56892326fef10172ee652"}, - {file = "pyinstaller-5.9.0-py3-none-manylinux2014_i686.whl", hash = "sha256:dcd348b174fd72c4df271790ac582969c9423cb099fe92db9ec131a8a9243d5a"}, - {file = "pyinstaller-5.9.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:4b21b0298db44f5f07fc04d8ff81ec31efa47b72798efaecc4e811c50a102111"}, - {file = "pyinstaller-5.9.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:12ca6567be457826e14416637ea54485a185d0ce7a5a044df0d0daf588fff6d1"}, - {file = "pyinstaller-5.9.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:c7dd156c2438f197c168b990bbce03c97d3fb758dd9bbc3ca93626c2f4473a47"}, - {file = "pyinstaller-5.9.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:2ba42038b3bd83e1fba7c8eb9e7cde43bd5938e37ca542c89e8779355d213f52"}, - {file = "pyinstaller-5.9.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:d1ff94347183ae3755cfb8f02e64744eb7fe384469bd61e453c6ff59a81665d6"}, - {file = "pyinstaller-5.9.0-py3-none-win32.whl", hash = "sha256:8476538aec8a0a3be4f74b93388bd6989b91cc437ff86d6f0d3a68961176dce6"}, - {file = "pyinstaller-5.9.0-py3-none-win_amd64.whl", hash = "sha256:e7a4c292810285c2466f3bdcb1e03ba2170177ebe3d7054ff1af3bb348bf61a4"}, - {file = "pyinstaller-5.9.0-py3-none-win_arm64.whl", hash = "sha256:6cf6c032c72ef78fd9aa5e47d8952e784db45b2c3f7862bd44a99df68c216f64"}, - {file = "pyinstaller-5.9.0.tar.gz", hash = "sha256:2bde16a8d664e8eba9aa7b84f729f7ab005c1793be4fe1986b3c9cad6c486622"}, + {file = "pyinstaller-5.13.2-py3-none-macosx_10_13_universal2.whl", hash = "sha256:16cbd66b59a37f4ee59373a003608d15df180a0d9eb1a29ff3bfbfae64b23d0f"}, + {file = "pyinstaller-5.13.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8f6dd0e797ae7efdd79226f78f35eb6a4981db16c13325e962a83395c0ec7420"}, + {file = "pyinstaller-5.13.2-py3-none-manylinux2014_i686.whl", hash = "sha256:65133ed89467edb2862036b35d7c5ebd381670412e1e4361215e289c786dd4e6"}, + {file = "pyinstaller-5.13.2-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:7d51734423685ab2a4324ab2981d9781b203dcae42839161a9ee98bfeaabdade"}, + {file = "pyinstaller-5.13.2-py3-none-manylinux2014_s390x.whl", hash = "sha256:2c2fe9c52cb4577a3ac39626b84cf16cf30c2792f785502661286184f162ae0d"}, + {file = "pyinstaller-5.13.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:c63ef6133eefe36c4b2f4daf4cfea3d6412ece2ca218f77aaf967e52a95ac9b8"}, + {file = "pyinstaller-5.13.2-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:aadafb6f213549a5906829bb252e586e2cf72a7fbdb5731810695e6516f0ab30"}, + {file = "pyinstaller-5.13.2-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:b2e1c7f5cceb5e9800927ddd51acf9cc78fbaa9e79e822c48b0ee52d9ce3c892"}, + {file = "pyinstaller-5.13.2-py3-none-win32.whl", hash = "sha256:421cd24f26144f19b66d3868b49ed673176765f92fa9f7914cd2158d25b6d17e"}, + {file = "pyinstaller-5.13.2-py3-none-win_amd64.whl", hash = "sha256:ddcc2b36052a70052479a9e5da1af067b4496f43686ca3cdda99f8367d0627e4"}, + {file = "pyinstaller-5.13.2-py3-none-win_arm64.whl", hash = "sha256:27cd64e7cc6b74c5b1066cbf47d75f940b71356166031deb9778a2579bb874c6"}, + {file = "pyinstaller-5.13.2.tar.gz", hash = "sha256:c8e5d3489c3a7cc5f8401c2d1f48a70e588f9967e391c3b06ddac1f685f8d5d2"}, ] [package.dependencies] @@ -1268,7 +1341,7 @@ altgraph = "*" macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""} pefile = {version = ">=2022.5.30", markers = "sys_platform == \"win32\""} pyinstaller-hooks-contrib = ">=2021.4" -pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""} +pywin32-ctypes = {version = ">=0.2.1", markers = "sys_platform == \"win32\""} setuptools = ">=42.0.0" [package.extras] @@ -1277,24 +1350,29 @@ hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"] [[package]] name = "pyinstaller-hooks-contrib" -version = "2023.1" +version = "2024.3" description = "Community maintained hooks for PyInstaller" optional = false python-versions = ">=3.7" files = [ - {file = "pyinstaller-hooks-contrib-2023.1.tar.gz", hash = "sha256:ab56c192e7cd4472ff6b840cda4fc42bceccc7fb4234f064fc834a3248c0afdd"}, - {file = "pyinstaller_hooks_contrib-2023.1-py2.py3-none-any.whl", hash = "sha256:d2ea40a7105651aa525bfe5fe309aa264d4d9bb49f839b862243dcf0a56c34cd"}, + {file = "pyinstaller-hooks-contrib-2024.3.tar.gz", hash = "sha256:d18657c29267c63563a96b8fc78db6ba9ae40af6702acb2f8c871df12c75b60b"}, + {file = "pyinstaller_hooks_contrib-2024.3-py2.py3-none-any.whl", hash = "sha256:6701752d525e1f4eda1eaec2c2affc206171e15c7a4e188a152fcf3ed3308024"}, ] +[package.dependencies] +importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""} +packaging = ">=22.0" +setuptools = ">=42.0.0" + [[package]] name = "pyparsing" -version = "3.0.9" +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.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, - {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, + {file = "pyparsing-3.1.2-py3-none-any.whl", hash = "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742"}, + {file = "pyparsing-3.1.2.tar.gz", hash = "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad"}, ] [package.extras] @@ -1302,17 +1380,16 @@ diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pytest" -version = "7.2.2" +version = "7.4.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.2.2-py3-none-any.whl", hash = "sha256:130328f552dcfac0b1cec75c12e3f005619dc5f874f0a06e8ff7263f0ee6225e"}, - {file = "pytest-7.2.2.tar.gz", hash = "sha256:c99ab0c73aceb050f68929bc93af19ab6db0558791c6a0715723abe9d0ade9d4"}, + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, ] [package.dependencies] -attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" @@ -1321,17 +1398,17 @@ pluggy = ">=0.12,<2.0" tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "python-dateutil" -version = "2.8.2" +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.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, - {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, + {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] @@ -1339,202 +1416,204 @@ six = ">=1.5" [[package]] name = "pytz" -version = "2022.7.1" +version = "2024.1" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" files = [ - {file = "pytz-2022.7.1-py2.py3-none-any.whl", hash = "sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a"}, - {file = "pytz-2022.7.1.tar.gz", hash = "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0"}, + {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, + {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, ] -[[package]] -name = "pytz-deprecation-shim" -version = "0.1.0.post0" -description = "Shims to make deprecation of pytz easier" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" -files = [ - {file = "pytz_deprecation_shim-0.1.0.post0-py2.py3-none-any.whl", hash = "sha256:8314c9692a636c8eb3bda879b9f119e350e93223ae83e70e80c31675a0fdc1a6"}, - {file = "pytz_deprecation_shim-0.1.0.post0.tar.gz", hash = "sha256:af097bae1b616dde5c5744441e2ddc69e74dfdcb0c263129610d85b87445a59d"}, -] - -[package.dependencies] -tzdata = {version = "*", markers = "python_version >= \"3.6\""} - [[package]] name = "pywin32-ctypes" -version = "0.2.0" -description = "" +version = "0.2.2" +description = "A (partial) reimplementation of pywin32 using ctypes/cffi" optional = false -python-versions = "*" +python-versions = ">=3.6" files = [ - {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"}, - {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"}, + {file = "pywin32-ctypes-0.2.2.tar.gz", hash = "sha256:3426e063bdd5fd4df74a14fa3cf80a0b42845a87e1d1e81f6549f9daec593a60"}, + {file = "pywin32_ctypes-0.2.2-py3-none-any.whl", hash = "sha256:bf490a1a709baf35d688fe0ecf980ed4de11d2b3e37b51e5442587a75d9957e7"}, ] [[package]] name = "pyyaml" -version = "6.0" +version = "6.0.1" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.6" files = [ - {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, - {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, - {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, - {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, - {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, - {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, - {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, - {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, - {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, - {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, - {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, - {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, - {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, - {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, - {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, - {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, - {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, - {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, - {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, - {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, ] [[package]] name = "regex" -version = "2022.10.31" +version = "2023.12.25" description = "Alternative regular expression module, to replace re." optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "regex-2022.10.31-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a8ff454ef0bb061e37df03557afda9d785c905dab15584860f982e88be73015f"}, - {file = "regex-2022.10.31-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1eba476b1b242620c266edf6325b443a2e22b633217a9835a52d8da2b5c051f9"}, - {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0e5af9a9effb88535a472e19169e09ce750c3d442fb222254a276d77808620b"}, - {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d03fe67b2325cb3f09be029fd5da8df9e6974f0cde2c2ac6a79d2634e791dd57"}, - {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9d0b68ac1743964755ae2d89772c7e6fb0118acd4d0b7464eaf3921c6b49dd4"}, - {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a45b6514861916c429e6059a55cf7db74670eaed2052a648e3e4d04f070e001"}, - {file = "regex-2022.10.31-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8b0886885f7323beea6f552c28bff62cbe0983b9fbb94126531693ea6c5ebb90"}, - {file = "regex-2022.10.31-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5aefb84a301327ad115e9d346c8e2760009131d9d4b4c6b213648d02e2abe144"}, - {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:702d8fc6f25bbf412ee706bd73019da5e44a8400861dfff7ff31eb5b4a1276dc"}, - {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a3c1ebd4ed8e76e886507c9eddb1a891673686c813adf889b864a17fafcf6d66"}, - {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:50921c140561d3db2ab9f5b11c5184846cde686bb5a9dc64cae442926e86f3af"}, - {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:7db345956ecce0c99b97b042b4ca7326feeec6b75facd8390af73b18e2650ffc"}, - {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:763b64853b0a8f4f9cfb41a76a4a85a9bcda7fdda5cb057016e7706fde928e66"}, - {file = "regex-2022.10.31-cp310-cp310-win32.whl", hash = "sha256:44136355e2f5e06bf6b23d337a75386371ba742ffa771440b85bed367c1318d1"}, - {file = "regex-2022.10.31-cp310-cp310-win_amd64.whl", hash = "sha256:bfff48c7bd23c6e2aec6454aaf6edc44444b229e94743b34bdcdda2e35126cf5"}, - {file = "regex-2022.10.31-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b4b1fe58cd102d75ef0552cf17242705ce0759f9695334a56644ad2d83903fe"}, - {file = "regex-2022.10.31-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:542e3e306d1669b25936b64917285cdffcd4f5c6f0247636fec037187bd93542"}, - {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c27cc1e4b197092e50ddbf0118c788d9977f3f8f35bfbbd3e76c1846a3443df7"}, - {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8e38472739028e5f2c3a4aded0ab7eadc447f0d84f310c7a8bb697ec417229e"}, - {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:76c598ca73ec73a2f568e2a72ba46c3b6c8690ad9a07092b18e48ceb936e9f0c"}, - {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c28d3309ebd6d6b2cf82969b5179bed5fefe6142c70f354ece94324fa11bf6a1"}, - {file = "regex-2022.10.31-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9af69f6746120998cd9c355e9c3c6aec7dff70d47247188feb4f829502be8ab4"}, - {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a5f9505efd574d1e5b4a76ac9dd92a12acb2b309551e9aa874c13c11caefbe4f"}, - {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5ff525698de226c0ca743bfa71fc6b378cda2ddcf0d22d7c37b1cc925c9650a5"}, - {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:4fe7fda2fe7c8890d454f2cbc91d6c01baf206fbc96d89a80241a02985118c0c"}, - {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:2cdc55ca07b4e70dda898d2ab7150ecf17c990076d3acd7a5f3b25cb23a69f1c"}, - {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:44a6c2f6374e0033873e9ed577a54a3602b4f609867794c1a3ebba65e4c93ee7"}, - {file = "regex-2022.10.31-cp311-cp311-win32.whl", hash = "sha256:d8716f82502997b3d0895d1c64c3b834181b1eaca28f3f6336a71777e437c2af"}, - {file = "regex-2022.10.31-cp311-cp311-win_amd64.whl", hash = "sha256:61edbca89aa3f5ef7ecac8c23d975fe7261c12665f1d90a6b1af527bba86ce61"}, - {file = "regex-2022.10.31-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0a069c8483466806ab94ea9068c34b200b8bfc66b6762f45a831c4baaa9e8cdd"}, - {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d26166acf62f731f50bdd885b04b38828436d74e8e362bfcb8df221d868b5d9b"}, - {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac741bf78b9bb432e2d314439275235f41656e189856b11fb4e774d9f7246d81"}, - {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75f591b2055523fc02a4bbe598aa867df9e953255f0b7f7715d2a36a9c30065c"}, - {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b30bddd61d2a3261f025ad0f9ee2586988c6a00c780a2fb0a92cea2aa702c54"}, - {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef4163770525257876f10e8ece1cf25b71468316f61451ded1a6f44273eedeb5"}, - {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7b280948d00bd3973c1998f92e22aa3ecb76682e3a4255f33e1020bd32adf443"}, - {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:d0213671691e341f6849bf33cd9fad21f7b1cb88b89e024f33370733fec58742"}, - {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:22e7ebc231d28393dfdc19b185d97e14a0f178bedd78e85aad660e93b646604e"}, - {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:8ad241da7fac963d7573cc67a064c57c58766b62a9a20c452ca1f21050868dfa"}, - {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:586b36ebda81e6c1a9c5a5d0bfdc236399ba6595e1397842fd4a45648c30f35e"}, - {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:0653d012b3bf45f194e5e6a41df9258811ac8fc395579fa82958a8b76286bea4"}, - {file = "regex-2022.10.31-cp36-cp36m-win32.whl", hash = "sha256:144486e029793a733e43b2e37df16a16df4ceb62102636ff3db6033994711066"}, - {file = "regex-2022.10.31-cp36-cp36m-win_amd64.whl", hash = "sha256:c14b63c9d7bab795d17392c7c1f9aaabbffd4cf4387725a0ac69109fb3b550c6"}, - {file = "regex-2022.10.31-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4cac3405d8dda8bc6ed499557625585544dd5cbf32072dcc72b5a176cb1271c8"}, - {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23cbb932cc53a86ebde0fb72e7e645f9a5eec1a5af7aa9ce333e46286caef783"}, - {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74bcab50a13960f2a610cdcd066e25f1fd59e23b69637c92ad470784a51b1347"}, - {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78d680ef3e4d405f36f0d6d1ea54e740366f061645930072d39bca16a10d8c93"}, - {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce6910b56b700bea7be82c54ddf2e0ed792a577dfaa4a76b9af07d550af435c6"}, - {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:659175b2144d199560d99a8d13b2228b85e6019b6e09e556209dfb8c37b78a11"}, - {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1ddf14031a3882f684b8642cb74eea3af93a2be68893901b2b387c5fd92a03ec"}, - {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b683e5fd7f74fb66e89a1ed16076dbab3f8e9f34c18b1979ded614fe10cdc4d9"}, - {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2bde29cc44fa81c0a0c8686992c3080b37c488df167a371500b2a43ce9f026d1"}, - {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4919899577ba37f505aaebdf6e7dc812d55e8f097331312db7f1aab18767cce8"}, - {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:9c94f7cc91ab16b36ba5ce476f1904c91d6c92441f01cd61a8e2729442d6fcf5"}, - {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ae1e96785696b543394a4e3f15f3f225d44f3c55dafe3f206493031419fedf95"}, - {file = "regex-2022.10.31-cp37-cp37m-win32.whl", hash = "sha256:c670f4773f2f6f1957ff8a3962c7dd12e4be54d05839b216cb7fd70b5a1df394"}, - {file = "regex-2022.10.31-cp37-cp37m-win_amd64.whl", hash = "sha256:8e0caeff18b96ea90fc0eb6e3bdb2b10ab5b01a95128dfeccb64a7238decf5f0"}, - {file = "regex-2022.10.31-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:131d4be09bea7ce2577f9623e415cab287a3c8e0624f778c1d955ec7c281bd4d"}, - {file = "regex-2022.10.31-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e613a98ead2005c4ce037c7b061f2409a1a4e45099edb0ef3200ee26ed2a69a8"}, - {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:052b670fafbe30966bbe5d025e90b2a491f85dfe5b2583a163b5e60a85a321ad"}, - {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa62a07ac93b7cb6b7d0389d8ef57ffc321d78f60c037b19dfa78d6b17c928ee"}, - {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5352bea8a8f84b89d45ccc503f390a6be77917932b1c98c4cdc3565137acc714"}, - {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20f61c9944f0be2dc2b75689ba409938c14876c19d02f7585af4460b6a21403e"}, - {file = "regex-2022.10.31-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29c04741b9ae13d1e94cf93fca257730b97ce6ea64cfe1eba11cf9ac4e85afb6"}, - {file = "regex-2022.10.31-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:543883e3496c8b6d58bd036c99486c3c8387c2fc01f7a342b760c1ea3158a318"}, - {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7a8b43ee64ca8f4befa2bea4083f7c52c92864d8518244bfa6e88c751fa8fff"}, - {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6a9a19bea8495bb419dc5d38c4519567781cd8d571c72efc6aa959473d10221a"}, - {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6ffd55b5aedc6f25fd8d9f905c9376ca44fcf768673ffb9d160dd6f409bfda73"}, - {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4bdd56ee719a8f751cf5a593476a441c4e56c9b64dc1f0f30902858c4ef8771d"}, - {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8ca88da1bd78990b536c4a7765f719803eb4f8f9971cc22d6ca965c10a7f2c4c"}, - {file = "regex-2022.10.31-cp38-cp38-win32.whl", hash = "sha256:5a260758454580f11dd8743fa98319bb046037dfab4f7828008909d0aa5292bc"}, - {file = "regex-2022.10.31-cp38-cp38-win_amd64.whl", hash = "sha256:5e6a5567078b3eaed93558842346c9d678e116ab0135e22eb72db8325e90b453"}, - {file = "regex-2022.10.31-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5217c25229b6a85049416a5c1e6451e9060a1edcf988641e309dbe3ab26d3e49"}, - {file = "regex-2022.10.31-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4bf41b8b0a80708f7e0384519795e80dcb44d7199a35d52c15cc674d10b3081b"}, - {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cf0da36a212978be2c2e2e2d04bdff46f850108fccc1851332bcae51c8907cc"}, - {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d403d781b0e06d2922435ce3b8d2376579f0c217ae491e273bab8d092727d244"}, - {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a37d51fa9a00d265cf73f3de3930fa9c41548177ba4f0faf76e61d512c774690"}, - {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4f781ffedd17b0b834c8731b75cce2639d5a8afe961c1e58ee7f1f20b3af185"}, - {file = "regex-2022.10.31-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d243b36fbf3d73c25e48014961e83c19c9cc92530516ce3c43050ea6276a2ab7"}, - {file = "regex-2022.10.31-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:370f6e97d02bf2dd20d7468ce4f38e173a124e769762d00beadec3bc2f4b3bc4"}, - {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:597f899f4ed42a38df7b0e46714880fb4e19a25c2f66e5c908805466721760f5"}, - {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7dbdce0c534bbf52274b94768b3498abdf675a691fec5f751b6057b3030f34c1"}, - {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:22960019a842777a9fa5134c2364efaed5fbf9610ddc5c904bd3a400973b0eb8"}, - {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:7f5a3ffc731494f1a57bd91c47dc483a1e10048131ffb52d901bfe2beb6102e8"}, - {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7ef6b5942e6bfc5706301a18a62300c60db9af7f6368042227ccb7eeb22d0892"}, - {file = "regex-2022.10.31-cp39-cp39-win32.whl", hash = "sha256:395161bbdbd04a8333b9ff9763a05e9ceb4fe210e3c7690f5e68cedd3d65d8e1"}, - {file = "regex-2022.10.31-cp39-cp39-win_amd64.whl", hash = "sha256:957403a978e10fb3ca42572a23e6f7badff39aa1ce2f4ade68ee452dc6807692"}, - {file = "regex-2022.10.31.tar.gz", hash = "sha256:a3a98921da9a1bf8457aeee6a551948a83601689e5ecdd736894ea9bbec77e83"}, + {file = "regex-2023.12.25-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0694219a1d54336fd0445ea382d49d36882415c0134ee1e8332afd1529f0baa5"}, + {file = "regex-2023.12.25-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b014333bd0217ad3d54c143de9d4b9a3ca1c5a29a6d0d554952ea071cff0f1f8"}, + {file = "regex-2023.12.25-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d865984b3f71f6d0af64d0d88f5733521698f6c16f445bb09ce746c92c97c586"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e0eabac536b4cc7f57a5f3d095bfa557860ab912f25965e08fe1545e2ed8b4c"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c25a8ad70e716f96e13a637802813f65d8a6760ef48672aa3502f4c24ea8b400"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9b6d73353f777630626f403b0652055ebfe8ff142a44ec2cf18ae470395766e"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9cc99d6946d750eb75827cb53c4371b8b0fe89c733a94b1573c9dd16ea6c9e4"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88d1f7bef20c721359d8675f7d9f8e414ec5003d8f642fdfd8087777ff7f94b5"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cb3fe77aec8f1995611f966d0c656fdce398317f850d0e6e7aebdfe61f40e1cd"}, + {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7aa47c2e9ea33a4a2a05f40fcd3ea36d73853a2aae7b4feab6fc85f8bf2c9704"}, + {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:df26481f0c7a3f8739fecb3e81bc9da3fcfae34d6c094563b9d4670b047312e1"}, + {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c40281f7d70baf6e0db0c2f7472b31609f5bc2748fe7275ea65a0b4601d9b392"}, + {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:d94a1db462d5690ebf6ae86d11c5e420042b9898af5dcf278bd97d6bda065423"}, + {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ba1b30765a55acf15dce3f364e4928b80858fa8f979ad41f862358939bdd1f2f"}, + {file = "regex-2023.12.25-cp310-cp310-win32.whl", hash = "sha256:150c39f5b964e4d7dba46a7962a088fbc91f06e606f023ce57bb347a3b2d4630"}, + {file = "regex-2023.12.25-cp310-cp310-win_amd64.whl", hash = "sha256:09da66917262d9481c719599116c7dc0c321ffcec4b1f510c4f8a066f8768105"}, + {file = "regex-2023.12.25-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1b9d811f72210fa9306aeb88385b8f8bcef0dfbf3873410413c00aa94c56c2b6"}, + {file = "regex-2023.12.25-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d902a43085a308cef32c0d3aea962524b725403fd9373dea18110904003bac97"}, + {file = "regex-2023.12.25-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d166eafc19f4718df38887b2bbe1467a4f74a9830e8605089ea7a30dd4da8887"}, + {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7ad32824b7f02bb3c9f80306d405a1d9b7bb89362d68b3c5a9be53836caebdb"}, + {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:636ba0a77de609d6510235b7f0e77ec494d2657108f777e8765efc060094c98c"}, + {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fda75704357805eb953a3ee15a2b240694a9a514548cd49b3c5124b4e2ad01b"}, + {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f72cbae7f6b01591f90814250e636065850c5926751af02bb48da94dfced7baa"}, + {file = "regex-2023.12.25-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db2a0b1857f18b11e3b0e54ddfefc96af46b0896fb678c85f63fb8c37518b3e7"}, + {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7502534e55c7c36c0978c91ba6f61703faf7ce733715ca48f499d3dbbd7657e0"}, + {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e8c7e08bb566de4faaf11984af13f6bcf6a08f327b13631d41d62592681d24fe"}, + {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:283fc8eed679758de38fe493b7d7d84a198b558942b03f017b1f94dda8efae80"}, + {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:f44dd4d68697559d007462b0a3a1d9acd61d97072b71f6d1968daef26bc744bd"}, + {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:67d3ccfc590e5e7197750fcb3a2915b416a53e2de847a728cfa60141054123d4"}, + {file = "regex-2023.12.25-cp311-cp311-win32.whl", hash = "sha256:68191f80a9bad283432385961d9efe09d783bcd36ed35a60fb1ff3f1ec2efe87"}, + {file = "regex-2023.12.25-cp311-cp311-win_amd64.whl", hash = "sha256:7d2af3f6b8419661a0c421584cfe8aaec1c0e435ce7e47ee2a97e344b98f794f"}, + {file = "regex-2023.12.25-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8a0ccf52bb37d1a700375a6b395bff5dd15c50acb745f7db30415bae3c2b0715"}, + {file = "regex-2023.12.25-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c3c4a78615b7762740531c27cf46e2f388d8d727d0c0c739e72048beb26c8a9d"}, + {file = "regex-2023.12.25-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ad83e7545b4ab69216cef4cc47e344d19622e28aabec61574b20257c65466d6a"}, + {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7a635871143661feccce3979e1727c4e094f2bdfd3ec4b90dfd4f16f571a87a"}, + {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d498eea3f581fbe1b34b59c697512a8baef88212f92e4c7830fcc1499f5b45a5"}, + {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:43f7cd5754d02a56ae4ebb91b33461dc67be8e3e0153f593c509e21d219c5060"}, + {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51f4b32f793812714fd5307222a7f77e739b9bc566dc94a18126aba3b92b98a3"}, + {file = "regex-2023.12.25-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba99d8077424501b9616b43a2d208095746fb1284fc5ba490139651f971d39d9"}, + {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4bfc2b16e3ba8850e0e262467275dd4d62f0d045e0e9eda2bc65078c0110a11f"}, + {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8c2c19dae8a3eb0ea45a8448356ed561be843b13cbc34b840922ddf565498c1c"}, + {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:60080bb3d8617d96f0fb7e19796384cc2467447ef1c491694850ebd3670bc457"}, + {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b77e27b79448e34c2c51c09836033056a0547aa360c45eeeb67803da7b0eedaf"}, + {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:518440c991f514331f4850a63560321f833979d145d7d81186dbe2f19e27ae3d"}, + {file = "regex-2023.12.25-cp312-cp312-win32.whl", hash = "sha256:e2610e9406d3b0073636a3a2e80db05a02f0c3169b5632022b4e81c0364bcda5"}, + {file = "regex-2023.12.25-cp312-cp312-win_amd64.whl", hash = "sha256:cc37b9aeebab425f11f27e5e9e6cf580be7206c6582a64467a14dda211abc232"}, + {file = "regex-2023.12.25-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:da695d75ac97cb1cd725adac136d25ca687da4536154cdc2815f576e4da11c69"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d126361607b33c4eb7b36debc173bf25d7805847346dd4d99b5499e1fef52bc7"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4719bb05094d7d8563a450cf8738d2e1061420f79cfcc1fa7f0a44744c4d8f73"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5dd58946bce44b53b06d94aa95560d0b243eb2fe64227cba50017a8d8b3cd3e2"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22a86d9fff2009302c440b9d799ef2fe322416d2d58fc124b926aa89365ec482"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2aae8101919e8aa05ecfe6322b278f41ce2994c4a430303c4cd163fef746e04f"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e692296c4cc2873967771345a876bcfc1c547e8dd695c6b89342488b0ea55cd8"}, + {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:263ef5cc10979837f243950637fffb06e8daed7f1ac1e39d5910fd29929e489a"}, + {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:d6f7e255e5fa94642a0724e35406e6cb7001c09d476ab5fce002f652b36d0c39"}, + {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:88ad44e220e22b63b0f8f81f007e8abbb92874d8ced66f32571ef8beb0643b2b"}, + {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:3a17d3ede18f9cedcbe23d2daa8a2cd6f59fe2bf082c567e43083bba3fb00347"}, + {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d15b274f9e15b1a0b7a45d2ac86d1f634d983ca40d6b886721626c47a400bf39"}, + {file = "regex-2023.12.25-cp37-cp37m-win32.whl", hash = "sha256:ed19b3a05ae0c97dd8f75a5d8f21f7723a8c33bbc555da6bbe1f96c470139d3c"}, + {file = "regex-2023.12.25-cp37-cp37m-win_amd64.whl", hash = "sha256:a6d1047952c0b8104a1d371f88f4ab62e6275567d4458c1e26e9627ad489b445"}, + {file = "regex-2023.12.25-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b43523d7bc2abd757119dbfb38af91b5735eea45537ec6ec3a5ec3f9562a1c53"}, + {file = "regex-2023.12.25-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:efb2d82f33b2212898f1659fb1c2e9ac30493ac41e4d53123da374c3b5541e64"}, + {file = "regex-2023.12.25-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b7fca9205b59c1a3d5031f7e64ed627a1074730a51c2a80e97653e3e9fa0d415"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086dd15e9435b393ae06f96ab69ab2d333f5d65cbe65ca5a3ef0ec9564dfe770"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e81469f7d01efed9b53740aedd26085f20d49da65f9c1f41e822a33992cb1590"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:34e4af5b27232f68042aa40a91c3b9bb4da0eeb31b7632e0091afc4310afe6cb"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9852b76ab558e45b20bf1893b59af64a28bd3820b0c2efc80e0a70a4a3ea51c1"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff100b203092af77d1a5a7abe085b3506b7eaaf9abf65b73b7d6905b6cb76988"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cc038b2d8b1470364b1888a98fd22d616fba2b6309c5b5f181ad4483e0017861"}, + {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:094ba386bb5c01e54e14434d4caabf6583334090865b23ef58e0424a6286d3dc"}, + {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5cd05d0f57846d8ba4b71d9c00f6f37d6b97d5e5ef8b3c3840426a475c8f70f4"}, + {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:9aa1a67bbf0f957bbe096375887b2505f5d8ae16bf04488e8b0f334c36e31360"}, + {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:98a2636994f943b871786c9e82bfe7883ecdaba2ef5df54e1450fa9869d1f756"}, + {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:37f8e93a81fc5e5bd8db7e10e62dc64261bcd88f8d7e6640aaebe9bc180d9ce2"}, + {file = "regex-2023.12.25-cp38-cp38-win32.whl", hash = "sha256:d78bd484930c1da2b9679290a41cdb25cc127d783768a0369d6b449e72f88beb"}, + {file = "regex-2023.12.25-cp38-cp38-win_amd64.whl", hash = "sha256:b521dcecebc5b978b447f0f69b5b7f3840eac454862270406a39837ffae4e697"}, + {file = "regex-2023.12.25-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f7bc09bc9c29ebead055bcba136a67378f03d66bf359e87d0f7c759d6d4ffa31"}, + {file = "regex-2023.12.25-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e14b73607d6231f3cc4622809c196b540a6a44e903bcfad940779c80dffa7be7"}, + {file = "regex-2023.12.25-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9eda5f7a50141291beda3edd00abc2d4a5b16c29c92daf8d5bd76934150f3edc"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc6bb9aa69aacf0f6032c307da718f61a40cf970849e471254e0e91c56ffca95"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:298dc6354d414bc921581be85695d18912bea163a8b23cac9a2562bbcd5088b1"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f4e475a80ecbd15896a976aa0b386c5525d0ed34d5c600b6d3ebac0a67c7ddf"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:531ac6cf22b53e0696f8e1d56ce2396311254eb806111ddd3922c9d937151dae"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22f3470f7524b6da61e2020672df2f3063676aff444db1daa283c2ea4ed259d6"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:89723d2112697feaa320c9d351e5f5e7b841e83f8b143dba8e2d2b5f04e10923"}, + {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0ecf44ddf9171cd7566ef1768047f6e66975788258b1c6c6ca78098b95cf9a3d"}, + {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:905466ad1702ed4acfd67a902af50b8db1feeb9781436372261808df7a2a7bca"}, + {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:4558410b7a5607a645e9804a3e9dd509af12fb72b9825b13791a37cd417d73a5"}, + {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:7e316026cc1095f2a3e8cc012822c99f413b702eaa2ca5408a513609488cb62f"}, + {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3b1de218d5375cd6ac4b5493e0b9f3df2be331e86520f23382f216c137913d20"}, + {file = "regex-2023.12.25-cp39-cp39-win32.whl", hash = "sha256:11a963f8e25ab5c61348d090bf1b07f1953929c13bd2309a0662e9ff680763c9"}, + {file = "regex-2023.12.25-cp39-cp39-win_amd64.whl", hash = "sha256:e693e233ac92ba83a87024e1d32b5f9ab15ca55ddd916d878146f4e3406b5c91"}, + {file = "regex-2023.12.25.tar.gz", hash = "sha256:29171aa128da69afdf4bde412d5bedc335f2ca8fcfe4489038577d05f16181e5"}, ] [[package]] name = "requests" -version = "2.28.2" +version = "2.31.0" description = "Python HTTP for Humans." optional = false -python-versions = ">=3.7, <4" +python-versions = ">=3.7" files = [ - {file = "requests-2.28.2-py3-none-any.whl", hash = "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa"}, - {file = "requests-2.28.2.tar.gz", hash = "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"}, + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, ] [package.dependencies] certifi = ">=2017.4.17" charset-normalizer = ">=2,<4" idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<1.27" +urllib3 = ">=1.21.1,<3" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] @@ -1542,13 +1621,13 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "requests-oauthlib" -version = "1.3.1" +version = "1.4.0" description = "OAuthlib authentication support for Requests." optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ - {file = "requests-oauthlib-1.3.1.tar.gz", hash = "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a"}, - {file = "requests_oauthlib-1.3.1-py2.py3-none-any.whl", hash = "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5"}, + {file = "requests-oauthlib-1.4.0.tar.gz", hash = "sha256:acee623221e4a39abcbb919312c8ff04bd44e7e417087fb4bd5e2a2f53d5e79a"}, + {file = "requests_oauthlib-1.4.0-py2.py3-none-any.whl", hash = "sha256:7a3130d94a17520169e38db6c8d75f2c974643788465ecc2e4b36d288bf13033"}, ] [package.dependencies] @@ -1592,46 +1671,46 @@ pyasn1 = ">=0.1.3" [[package]] name = "s3transfer" -version = "0.6.1" +version = "0.10.0" description = "An Amazon S3 Transfer Manager" optional = false -python-versions = ">= 3.7" +python-versions = ">= 3.8" files = [ - {file = "s3transfer-0.6.1-py3-none-any.whl", hash = "sha256:3c0da2d074bf35d6870ef157158641178a4204a6e689e82546083e31e0311346"}, - {file = "s3transfer-0.6.1.tar.gz", hash = "sha256:640bb492711f4c0c0905e1f62b6aaeb771881935ad27884852411f8e9cacbca9"}, + {file = "s3transfer-0.10.0-py3-none-any.whl", hash = "sha256:3cdb40f5cfa6966e812209d0994f2a4709b561c88e90cf00c2696d2df4e56b2e"}, + {file = "s3transfer-0.10.0.tar.gz", hash = "sha256:d0c8bbf672d5eebbe4e57945e23b972d963f07d82f661cabf678a5c88831595b"}, ] [package.dependencies] -botocore = ">=1.12.36,<2.0a.0" +botocore = ">=1.33.2,<2.0a.0" [package.extras] -crt = ["botocore[crt] (>=1.20.29,<2.0a.0)"] +crt = ["botocore[crt] (>=1.33.2,<2.0a.0)"] [[package]] name = "setuptools" -version = "67.4.0" +version = "69.2.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "setuptools-67.4.0-py3-none-any.whl", hash = "sha256:f106dee1b506dee5102cc3f3e9e68137bbad6d47b616be7991714b0c62204251"}, - {file = "setuptools-67.4.0.tar.gz", hash = "sha256:e5fd0a713141a4a105412233c63dc4e17ba0090c8e8334594ac790ec97792330"}, + {file = "setuptools-69.2.0-py3-none-any.whl", hash = "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"}, + {file = "setuptools-69.2.0.tar.gz", hash = "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "shellingham" -version = "1.5.0.post1" +version = "1.5.4" description = "Tool to Detect Surrounding Shell" optional = false python-versions = ">=3.7" files = [ - {file = "shellingham-1.5.0.post1-py2.py3-none-any.whl", hash = "sha256:368bf8c00754fd4f55afb7bbb86e272df77e4dc76ac29dbcbb81a59e9fc15744"}, - {file = "shellingham-1.5.0.post1.tar.gz", hash = "sha256:823bc5fb5c34d60f285b624e7264f4dda254bc803a3774a147bf99c0e3004a28"}, + {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, + {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, ] [[package]] @@ -1647,18 +1726,17 @@ files = [ [[package]] name = "slack-sdk" -version = "3.21.3" +version = "3.27.1" description = "The Slack API Platform SDK for Python" optional = false -python-versions = ">=3.6.0" +python-versions = ">=3.6" files = [ - {file = "slack_sdk-3.21.3-py2.py3-none-any.whl", hash = "sha256:de3c07b92479940b61cd68c566f49fbc9974c8f38f661d26244078f3903bb9cc"}, - {file = "slack_sdk-3.21.3.tar.gz", hash = "sha256:20829bdc1a423ec93dac903470975ebf3bc76fd3fd91a4dadc0eeffc940ecb0c"}, + {file = "slack_sdk-3.27.1-py2.py3-none-any.whl", hash = "sha256:c108e509160cf1324c5c8b1f47ca52fb5e287021b8caf9f4ec78ad737ab7b1d9"}, + {file = "slack_sdk-3.27.1.tar.gz", hash = "sha256:85d86b34d807c26c8bb33c1569ec0985876f06ae4a2692afba765b7a5490d28c"}, ] [package.extras] -optional = ["SQLAlchemy (>=1.4,<3)", "aiodns (>1.0)", "aiohttp (>=3.7.3,<4)", "boto3 (<=2)", "websocket-client (>=1,<2)", "websockets (>=10,<11)"] -testing = ["Flask (>=1,<2)", "Flask-Sockets (>=0.2,<1)", "Jinja2 (==3.0.3)", "Werkzeug (<2)", "black (==22.8.0)", "boto3 (<=2)", "click (==8.0.4)", "databases (>=0.5)", "flake8 (>=5,<6)", "itsdangerous (==1.1.0)", "moto (>=3,<4)", "psutil (>=5,<6)", "pytest (>=6.2.5,<7)", "pytest-asyncio (<1)", "pytest-cov (>=2,<3)"] +optional = ["SQLAlchemy (>=1.4,<3)", "aiodns (>1.0)", "aiohttp (>=3.7.3,<4)", "boto3 (<=2)", "websocket-client (>=1,<2)", "websockets (>=10,<11)", "websockets (>=9.1,<10)"] [[package]] name = "tomli" @@ -1696,140 +1774,169 @@ test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6. [[package]] name = "types-cachetools" -version = "5.3.0.4" +version = "5.3.0.7" description = "Typing stubs for cachetools" optional = false -python-versions = "*" +python-versions = ">=3.7" files = [ - {file = "types-cachetools-5.3.0.4.tar.gz", hash = "sha256:9734ae93fe7fcf42693808bae643ae537a005c39d298718069393e6326f58184"}, - {file = "types_cachetools-5.3.0.4-py3-none-any.whl", hash = "sha256:611b8c11c4a8df39bef25db1ae41d9c48517d0f5fa0fa1d46c62f3bb677309e7"}, + {file = "types-cachetools-5.3.0.7.tar.gz", hash = "sha256:27c982cdb9cf3fead8b0089ee6b895715ecc99dac90ec29e2cab56eb1aaf4199"}, + {file = "types_cachetools-5.3.0.7-py3-none-any.whl", hash = "sha256:98c069dc7fc087b1b061703369c80751b0a0fc561f6fb072b554e5eee23773a0"}, ] [[package]] name = "types-pyyaml" -version = "6.0.12.8" +version = "6.0.12.20240311" description = "Typing stubs for PyYAML" optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "types-PyYAML-6.0.12.8.tar.gz", hash = "sha256:19304869a89d49af00be681e7b267414df213f4eb89634c4495fa62e8f942b9f"}, - {file = "types_PyYAML-6.0.12.8-py3-none-any.whl", hash = "sha256:5314a4b2580999b2ea06b2e5f9a7763d860d6e09cdf21c0e9561daa9cbd60178"}, + {file = "types-PyYAML-6.0.12.20240311.tar.gz", hash = "sha256:a9e0f0f88dc835739b0c1ca51ee90d04ca2a897a71af79de9aec5f38cb0a5342"}, + {file = "types_PyYAML-6.0.12.20240311-py3-none-any.whl", hash = "sha256:b845b06a1c7e54b8e5b4c683043de0d9caf205e7434b3edc678ff2411979b8f6"}, ] [[package]] name = "types-requests" -version = "2.28.11.15" +version = "2.31.0.6" description = "Typing stubs for requests" optional = false -python-versions = "*" +python-versions = ">=3.7" +files = [ + {file = "types-requests-2.31.0.6.tar.gz", hash = "sha256:cd74ce3b53c461f1228a9b783929ac73a666658f223e28ed29753771477b3bd0"}, + {file = "types_requests-2.31.0.6-py3-none-any.whl", hash = "sha256:a2db9cb228a81da8348b49ad6db3f5519452dd20a9c1e1a868c83c5fe88fd1a9"}, +] + +[package.dependencies] +types-urllib3 = "*" + +[[package]] +name = "types-requests" +version = "2.31.0.20240311" +description = "Typing stubs for requests" +optional = false +python-versions = ">=3.8" files = [ - {file = "types-requests-2.28.11.15.tar.gz", hash = "sha256:fc8eaa09cc014699c6b63c60c2e3add0c8b09a410c818b5ac6e65f92a26dde09"}, - {file = "types_requests-2.28.11.15-py3-none-any.whl", hash = "sha256:a05e4c7bc967518fba5789c341ea8b0c942776ee474c7873129a61161978e586"}, + {file = "types-requests-2.31.0.20240311.tar.gz", hash = "sha256:b1c1b66abfb7fa79aae09097a811c4aa97130eb8831c60e47aee4ca344731ca5"}, + {file = "types_requests-2.31.0.20240311-py3-none-any.whl", hash = "sha256:47872893d65a38e282ee9f277a4ee50d1b28bd592040df7d1fdaffdf3779937d"}, ] [package.dependencies] -types-urllib3 = "<1.27" +urllib3 = ">=2" [[package]] name = "types-urllib3" -version = "1.26.25.8" +version = "1.26.25.14" description = "Typing stubs for urllib3" optional = false python-versions = "*" files = [ - {file = "types-urllib3-1.26.25.8.tar.gz", hash = "sha256:ecf43c42d8ee439d732a1110b4901e9017a79a38daca26f08e42c8460069392c"}, - {file = "types_urllib3-1.26.25.8-py3-none-any.whl", hash = "sha256:95ea847fbf0bf675f50c8ae19a665baedcf07e6b4641662c4c3c72e7b2edf1a9"}, + {file = "types-urllib3-1.26.25.14.tar.gz", hash = "sha256:229b7f577c951b8c1b92c1bc2b2fdb0b49847bd2af6d1cc2a2e3dd340f3bda8f"}, + {file = "types_urllib3-1.26.25.14-py3-none-any.whl", hash = "sha256:9683bbb7fb72e32bfe9d2be6e04875fbe1b3eeec3cbb4ea231435aa7fd6b4f0e"}, ] [[package]] name = "typing-extensions" -version = "4.5.0" -description = "Backported and Experimental Type Hints for Python 3.7+" +version = "4.10.0" +description = "Backported and Experimental Type Hints for Python 3.8+" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, - {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, + {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, + {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, ] [[package]] name = "tzdata" -version = "2022.7" +version = "2024.1" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" files = [ - {file = "tzdata-2022.7-py2.py3-none-any.whl", hash = "sha256:2b88858b0e3120792a3c0635c23daf36a7d7eeeca657c323da299d2094402a0d"}, - {file = "tzdata-2022.7.tar.gz", hash = "sha256:fe5f866eddd8b96e9fcba978f8e503c909b19ea7efda11e52e39494bad3a7bfa"}, + {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, + {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, ] [[package]] name = "tzlocal" -version = "4.2" +version = "5.2" description = "tzinfo object for the local timezone" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "tzlocal-4.2-py3-none-any.whl", hash = "sha256:89885494684c929d9191c57aa27502afc87a579be5cdd3225c77c463ea043745"}, - {file = "tzlocal-4.2.tar.gz", hash = "sha256:ee5842fa3a795f023514ac2d801c4a81d1743bbe642e3940143326b3a00addd7"}, + {file = "tzlocal-5.2-py3-none-any.whl", hash = "sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8"}, + {file = "tzlocal-5.2.tar.gz", hash = "sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e"}, ] [package.dependencies] -pytz-deprecation-shim = "*" tzdata = {version = "*", markers = "platform_system == \"Windows\""} [package.extras] -devenv = ["black", "pyroma", "pytest-cov", "zest.releaser"] -test = ["pytest (>=4.3)", "pytest-mock (>=3.3)"] +devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"] [[package]] name = "urllib3" -version = "1.26.14" +version = "1.26.18" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ - {file = "urllib3-1.26.14-py2.py3-none-any.whl", hash = "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"}, - {file = "urllib3-1.26.14.tar.gz", hash = "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72"}, + {file = "urllib3-1.26.18-py2.py3-none-any.whl", hash = "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07"}, + {file = "urllib3-1.26.18.tar.gz", hash = "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +brotli = ["brotli (==1.0.9)", "brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] +[[package]] +name = "urllib3" +version = "2.0.7" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.7" +files = [ + {file = "urllib3-2.0.7-py3-none-any.whl", hash = "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"}, + {file = "urllib3-2.0.7.tar.gz", hash = "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + [[package]] name = "websocket-client" -version = "1.5.1" +version = "1.7.0" description = "WebSocket client for Python with low level API options" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "websocket-client-1.5.1.tar.gz", hash = "sha256:3f09e6d8230892547132177f575a4e3e73cfdf06526e20cc02aa1c3b47184d40"}, - {file = "websocket_client-1.5.1-py3-none-any.whl", hash = "sha256:cdf5877568b7e83aa7cf2244ab56a3213de587bbe0ce9d8b9600fc77b455d89e"}, + {file = "websocket-client-1.7.0.tar.gz", hash = "sha256:10e511ea3a8c744631d3bd77e61eb17ed09304c413ad42cf6ddfa4c7787e8fe6"}, + {file = "websocket_client-1.7.0-py3-none-any.whl", hash = "sha256:f4c3d22fec12a2461427a29957ff07d35098ee2d976d3ba244e688b8b4057588"}, ] [package.extras] -docs = ["Sphinx (>=3.4)", "sphinx-rtd-theme (>=0.5)"] +docs = ["Sphinx (>=6.0)", "sphinx-rtd-theme (>=1.1.0)"] optional = ["python-socks", "wsaccel"] test = ["websockets"] [[package]] name = "zipp" -version = "3.15.0" +version = "3.18.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, - {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, + {file = "zipp-3.18.0-py3-none-any.whl", hash = "sha256:c1bb803ed69d2cce2373152797064f7e79bc43f0a3748eb494096a867e0ebf79"}, + {file = "zipp-3.18.0.tar.gz", hash = "sha256:df8d042b02765029a09b157efd8e820451045890acc30f8e37dd2f94a060221f"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.12" -content-hash = "a5969c788da44c85261987bb04c84e1204b484edf4c463f492d1f7b3507913a2" +content-hash = "944d6c144d42a5ad522e48d8151dbfb7f8245b17b868da1752e12f1c6fbcf7e5" diff --git a/pyproject.toml b/pyproject.toml index d8896935..a9b433cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,12 +27,11 @@ python = ">=3.9,<3.12" typer = { extras = ["all"], version = "^0.7.0" } pydantic = "1.10.7" kubernetes = "^26.1.0" -prometheus-api-client = "^0.5.3" +prometheus-api-client = "0.5.3" numpy = "^1.24.2" alive-progress = "^3.1.2" -prometrix = "^0.1.10" +prometrix = "^0.1.16" slack-sdk = "^3.21.3" -aiostream = "^0.4.5" [tool.poetry.group.dev.dependencies] From 64555cce38691216c706d838786902485adb4ec1 Mon Sep 17 00:00:00 2001 From: Pavan Gudiwada <25551553+pavangudiwada@users.noreply.github.com> Date: Thu, 14 Mar 2024 21:07:00 +0530 Subject: [PATCH 047/137] Minor fix, broken navigation --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3ecc09ad..3a34a1a5 100644 --- a/README.md +++ b/README.md @@ -186,8 +186,8 @@ All above examples show running command as `krr ...`, replace it with `python kr ### Additional Options -- [View KRR Reports in a Web UI](#optional-free-saas-platform) -- [Receive KRR Reports Weekly in Slack](#slack-integration) +- [View KRR Reports in a Web UI](#free-ui-for-krr-recommendations) +- [Receive KRR Reports Weekly in Slack](#slack-notification) ### Environment-Specific Instructions Setup KRR for... From c3beaca5d9f8ba9b35ba8f68c1a65bc3a36baced Mon Sep 17 00:00:00 2001 From: Pavel Zhukov <33721692+LeaveMyYard@users.noreply.github.com> Date: Fri, 15 Mar 2024 18:50:59 +0200 Subject: [PATCH 048/137] CronJobs, DeploymentConfigs and reworked Argo Rollouts (#228) * Add CronJob scan * Implement Rollout using custom resource API * Add deploymentconfig support * Fix deploymentconfig not found * Process workloadRef of argo rollout * Fix for comments * Remove test print * Adjust AsyncIterable --- .../core/integrations/kubernetes/__init__.py | 224 +++++++++--- .../core/integrations/kubernetes/rollout.py | 340 ------------------ .../prometheus_metrics_service.py | 52 ++- robusta_krr/core/models/config.py | 4 +- robusta_krr/core/models/objects.py | 13 +- robusta_krr/strategies/simple.py | 18 +- robusta_krr/utils/object_like_dict.py | 29 ++ 7 files changed, 273 insertions(+), 407 deletions(-) delete mode 100644 robusta_krr/core/integrations/kubernetes/rollout.py create mode 100644 robusta_krr/utils/object_like_dict.py diff --git a/robusta_krr/core/integrations/kubernetes/__init__.py b/robusta_krr/core/integrations/kubernetes/__init__.py index 2c42d226..c7976cfa 100644 --- a/robusta_krr/core/integrations/kubernetes/__init__.py +++ b/robusta_krr/core/integrations/kubernetes/__init__.py @@ -1,7 +1,8 @@ import asyncio import logging +from collections import defaultdict from concurrent.futures import ThreadPoolExecutor -from typing import AsyncGenerator, AsyncIterable, Callable, Optional, Union +from typing import Any, AsyncGenerator, AsyncIterable, Awaitable, Callable, Iterable, Optional, Union from kubernetes import client, config # type: ignore from kubernetes.client import ApiException @@ -11,7 +12,6 @@ V1Deployment, V1HorizontalPodAutoscalerList, V1Job, - V1LabelSelector, V1Pod, V1PodList, V1StatefulSet, @@ -23,9 +23,9 @@ from robusta_krr.core.models.objects import HPAData, K8sObjectData, KindLiteral, PodData from robusta_krr.core.models.result import ResourceAllocations from robusta_krr.utils.async_gen_merge import async_gen_merge +from robusta_krr.utils.object_like_dict import ObjectLikeDict from . import config_patch as _ -from .rollout import RolloutAppsV1Api logger = logging.getLogger("krr") @@ -44,13 +44,16 @@ def __init__(self, cluster: Optional[str]): else None ) self.apps = client.AppsV1Api(api_client=self.api_client) - self.rollout = RolloutAppsV1Api(api_client=self.api_client) + self.custom_objects = client.CustomObjectsApi(api_client=self.api_client) self.batch = client.BatchV1Api(api_client=self.api_client) self.core = client.CoreV1Api(api_client=self.api_client) self.autoscaling_v1 = client.AutoscalingV1Api(api_client=self.api_client) self.autoscaling_v2 = client.AutoscalingV2Api(api_client=self.api_client) - self.__rollouts_available = True + self.__kind_available: defaultdict[KindLiteral, bool] = defaultdict(lambda: True) + + self.__jobs_for_cronjobs: dict[str, list[V1Job]] = {} + self.__jobs_loading_locks: defaultdict[str, asyncio.Lock] = defaultdict(asyncio.Lock) async def list_scannable_objects(self) -> AsyncGenerator[K8sObjectData, None]: """List all scannable objects. @@ -70,27 +73,61 @@ async def list_scannable_objects(self) -> AsyncGenerator[K8sObjectData, None]: async for object in async_gen_merge( self._list_deployments(), self._list_rollouts(), + self._list_deploymentconfig(), self._list_all_statefulsets(), self._list_all_daemon_set(), self._list_all_jobs(), + self._list_all_cronjobs(), ): # NOTE: By default we will filter out kube-system namespace if settings.namespaces == "*" and object.namespace == "kube-system": continue yield object - async def list_pods(self, object: K8sObjectData) -> list[PodData]: - selector = self._build_selector_query(object._api_resource.spec.selector) - if selector is None: - return [] + async def _list_jobs_for_cronjobs(self, namespace: str) -> list[V1Job]: + if namespace not in self.__jobs_for_cronjobs: + loop = asyncio.get_running_loop() + + async with self.__jobs_loading_locks[namespace]: + logging.debug(f"Loading jobs for cronjobs in {namespace}") + ret = await loop.run_in_executor( + self.executor, + lambda: self.batch.list_namespaced_job(namespace=namespace), + ) + self.__jobs_for_cronjobs[namespace] = ret.items + return self.__jobs_for_cronjobs[namespace] + + async def list_pods(self, object: K8sObjectData) -> list[PodData]: loop = asyncio.get_running_loop() + + if object.kind == "CronJob": + namespace_jobs = await self._list_jobs_for_cronjobs(object.namespace) + ownered_jobs_uids = [ + job.metadata.uid + for job in namespace_jobs + if any( + owner.kind == "CronJob" and owner.uid == object._api_resource.metadata.uid + for owner in job.metadata.owner_references or [] + ) + ] + selector = f"batch.kubernetes.io/controller-uid in ({','.join(ownered_jobs_uids)})" + + else: + if object.selector is None: + return [] + + selector = self._build_selector_query(object.selector) + if selector is None: + return [] + ret: V1PodList = await loop.run_in_executor( self.executor, lambda: self.core.list_namespaced_pod( namespace=object._api_resource.metadata.namespace, label_selector=selector ), ) + return [PodData(name=pod.metadata.name, deleted=False) for pod in ret.items] @staticmethod @@ -104,13 +141,24 @@ def _get_match_expression_filter(expression) -> str: return f"{expression.key} {expression.operator} ({values})" @staticmethod - def _build_selector_query(selector: V1LabelSelector) -> Union[str, None]: - label_filters = [f"{label[0]}={label[1]}" for label in selector.match_labels.items()] + def _build_selector_query(selector: Any) -> Union[str, None]: + label_filters = [] + + if selector.match_labels is not None: + label_filters += [f"{label[0]}={label[1]}" for label in selector.match_labels.items()] if selector.match_expressions is not None: - label_filters.extend( - [ClusterLoader._get_match_expression_filter(expression) for expression in selector.match_expressions] - ) + label_filters += [ + ClusterLoader._get_match_expression_filter(expression) for expression in selector.match_expressions + ] + + if label_filters == []: + # NOTE: This might mean that we have DeploymentConfig, + # which uses ReplicationController and it has a dict like matchLabels + if len(selector) != 0: + label_filters += [f"{label[0]}={label[1]}" for label in selector.items()] + else: + return None return ",".join(label_filters) @@ -133,19 +181,24 @@ def __build_obj( obj._api_resource = item return obj - def _should_list_resource(self, resource: str): + def _should_list_resource(self, resource: str) -> bool: if settings.resources == "*": return True - return resource.capitalize() in settings.resources + return resource in settings.resources async def _list_workflows( - self, kind: KindLiteral, all_namespaces_request: Callable, namespaced_request: Callable + self, + kind: KindLiteral, + all_namespaces_request: Callable, + namespaced_request: Callable, + extract_containers: Callable[[Any], Union[Iterable[V1Container], Awaitable[Iterable[V1Container]]]], + filter_workflows: Optional[Callable[[Any], bool]] = None, ) -> AsyncIterable[K8sObjectData]: if not self._should_list_resource(kind): logger.debug(f"Skipping {kind}s in {self.cluster}") return - if kind == "Rollout" and not self.__rollouts_available: + if not self.__kind_available[kind]: return logger.debug(f"Listing {kind}s in {self.cluster}") @@ -153,17 +206,15 @@ async def _list_workflows( try: if settings.namespaces == "*": - ret_multi = await loop.run_in_executor( - self.executor, - lambda: all_namespaces_request( - watch=False, - label_selector=settings.selector, - ), - ) - logger.debug(f"Found {len(ret_multi.items)} {kind} in {self.cluster}") - for item in ret_multi.items: - for container in item.spec.template.spec.containers: - yield self.__build_obj(item, container, kind) + tasks = [ + loop.run_in_executor( + self.executor, + lambda: all_namespaces_request( + watch=False, + label_selector=settings.selector, + ), + ) + ] else: tasks = [ loop.run_in_executor( @@ -177,20 +228,27 @@ async def _list_workflows( for namespace in settings.namespaces ] - total_items = 0 - for task in asyncio.as_completed(tasks): - ret_single = await task - total_items += len(ret_single.items) - for item in ret_single.items: - for container in item.spec.template.spec.containers: - yield self.__build_obj(item, container, kind) + total_items = 0 + for task in asyncio.as_completed(tasks): + ret_single = await task + total_items += len(ret_single.items) + for item in ret_single.items: + if filter_workflows is not None and not filter_workflows(item): + continue + + containers = extract_containers(item) + if asyncio.iscoroutine(containers): + containers = await containers + + for container in containers: + yield self.__build_obj(item, container, kind) - logger.debug(f"Found {total_items} {kind} in {self.cluster}") + logger.debug(f"Found {total_items} {kind} in {self.cluster}") except ApiException as e: - if kind == "Rollout" and e.status in [400, 401, 403, 404]: - if self.__rollouts_available: - logger.debug(f"Rollout API not available in {self.cluster}") - self.__rollouts_available = False + if kind in ("Rollout", "DeploymentConfig") and e.status in [400, 401, 403, 404]: + if self.__kind_available[kind]: + logger.debug(f"{kind} API not available in {self.cluster}") + self.__kind_available[kind] = False else: logger.exception(f"Error {e.status} listing {kind} in cluster {self.cluster}: {e.reason}") logger.error("Will skip this object type and continue.") @@ -200,13 +258,78 @@ def _list_deployments(self) -> AsyncIterable[K8sObjectData]: kind="Deployment", all_namespaces_request=self.apps.list_deployment_for_all_namespaces, namespaced_request=self.apps.list_namespaced_deployment, + extract_containers=lambda item: item.spec.template.spec.containers, ) def _list_rollouts(self) -> AsyncIterable[K8sObjectData]: + async def _extract_containers(item: Any) -> list[V1Container]: + if item.spec.template is not None: + return item.spec.template.spec.containers + + loop = asyncio.get_running_loop() + + logging.debug( + f"Rollout has workloadRef, fetching template for {item.metadata.name} in {item.metadata.namespace}" + ) + + # Template can be None and object might have workloadRef + workloadRef = item.spec.workloadRef + if workloadRef is not None: + ret = await loop.run_in_executor( + self.executor, + lambda: self.apps.read_namespaced_deployment( + namespace=item.metadata.namespace, name=workloadRef.name + ), + ) + return ret.spec.template.spec.containers + + return [] + + # NOTE: Using custom objects API returns dicts, but all other APIs return objects + # We need to handle this difference using a small wrapper return self._list_workflows( kind="Rollout", - all_namespaces_request=self.rollout.list_rollout_for_all_namespaces, - namespaced_request=self.rollout.list_namespaced_rollout, + all_namespaces_request=lambda **kwargs: ObjectLikeDict( + self.custom_objects.list_cluster_custom_object( + group="argoproj.io", + version="v1alpha1", + plural="rollouts", + **kwargs, + ) + ), + namespaced_request=lambda **kwargs: ObjectLikeDict( + self.custom_objects.list_namespaced_custom_object( + group="argoproj.io", + version="v1alpha1", + plural="rollouts", + **kwargs, + ) + ), + extract_containers=_extract_containers, + ) + + def _list_deploymentconfig(self) -> AsyncIterable[K8sObjectData]: + # NOTE: Using custom objects API returns dicts, but all other APIs return objects + # We need to handle this difference using a small wrapper + return self._list_workflows( + kind="DeploymentConfig", + all_namespaces_request=lambda **kwargs: ObjectLikeDict( + self.custom_objects.list_cluster_custom_object( + group="apps.openshift.io", + version="v1", + plural="deploymentconfigs", + **kwargs, + ) + ), + namespaced_request=lambda **kwargs: ObjectLikeDict( + self.custom_objects.list_namespaced_custom_object( + group="apps.openshift.io", + version="v1", + plural="deploymentconfigs", + **kwargs, + ) + ), + extract_containers=lambda item: item.spec.template.spec.containers, ) def _list_all_statefulsets(self) -> AsyncIterable[K8sObjectData]: @@ -214,6 +337,7 @@ def _list_all_statefulsets(self) -> AsyncIterable[K8sObjectData]: kind="StatefulSet", all_namespaces_request=self.apps.list_stateful_set_for_all_namespaces, namespaced_request=self.apps.list_namespaced_stateful_set, + extract_containers=lambda item: item.spec.template.spec.containers, ) def _list_all_daemon_set(self) -> AsyncIterable[K8sObjectData]: @@ -221,6 +345,7 @@ def _list_all_daemon_set(self) -> AsyncIterable[K8sObjectData]: kind="DaemonSet", all_namespaces_request=self.apps.list_daemon_set_for_all_namespaces, namespaced_request=self.apps.list_namespaced_daemon_set, + extract_containers=lambda item: item.spec.template.spec.containers, ) def _list_all_jobs(self) -> AsyncIterable[K8sObjectData]: @@ -228,6 +353,19 @@ def _list_all_jobs(self) -> AsyncIterable[K8sObjectData]: kind="Job", all_namespaces_request=self.batch.list_job_for_all_namespaces, namespaced_request=self.batch.list_namespaced_job, + extract_containers=lambda item: item.spec.template.spec.containers, + # NOTE: If the job has ownerReference and it is a CronJob, then we should skip it + filter_workflows=lambda item: not any( + owner.kind == "CronJob" for owner in item.metadata.owner_references or [] + ), + ) + + def _list_all_cronjobs(self) -> AsyncIterable[K8sObjectData]: + return self._list_workflows( + kind="CronJob", + all_namespaces_request=self.batch.list_cron_job_for_all_namespaces, + namespaced_request=self.batch.list_namespaced_cron_job, + extract_containers=lambda item: item.spec.job_template.spec.template.spec.containers, ) async def __list_hpa_v1(self) -> dict[HPAKey, HPAData]: diff --git a/robusta_krr/core/integrations/kubernetes/rollout.py b/robusta_krr/core/integrations/kubernetes/rollout.py deleted file mode 100644 index 389c65b2..00000000 --- a/robusta_krr/core/integrations/kubernetes/rollout.py +++ /dev/null @@ -1,340 +0,0 @@ -import six -from kubernetes import client -from kubernetes.client.exceptions import ApiTypeError, ApiValueError - - -class RolloutAppsV1Api(client.AppsV1Api): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - def list_rollout_for_all_namespaces(self, **kwargs): # noqa: E501 - """list_rollout_for_all_namespaces # noqa: E501 - - list or watch objects of kind Deployment # noqa: E501 - This method makes a synchronous HTTP request by default. To make an - asynchronous HTTP request, please pass async_req=True - >>> thread = api.list_rollout_for_all_namespaces(async_req=True) - >>> result = thread.get() - - :param async_req bool: execute request asynchronously - :param bool allow_watch_bookmarks: allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored. - :param str _continue: The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\". This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications. - :param str field_selector: A selector to restrict the list of returned objects by their fields. Defaults to everything. - :param str label_selector: A selector to restrict the list of returned objects by their labels. Defaults to everything. - :param int limit: limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true. The server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned. - :param str pretty: If 'true', then the output is pretty printed. - :param str resource_version: resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. Defaults to unset - :param str resource_version_match: resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. Defaults to unset - :param int timeout_seconds: Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity. - :param bool watch: Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion. - :param _preload_content: if False, the urllib3.HTTPResponse object will - be returned without reading/decoding response - data. Default is True. - :param _request_timeout: timeout setting for this request. If one - number provided, it will be total request - timeout. It can also be a pair (tuple) of - (connection, read) timeouts. - :return: V1RolloutList - If the method is called asynchronously, - returns the request thread. - """ - kwargs["_return_http_data_only"] = True - return self.list_rollout_for_all_namespaces_with_http_info(**kwargs) # noqa: E501 - - def list_rollout_for_all_namespaces_with_http_info(self, **kwargs): # noqa: E501 - """list_rollout_for_all_namespaces # noqa: E501 - - list or watch objects of kind Deployment # noqa: E501 - This method makes a synchronous HTTP request by default. To make an - asynchronous HTTP request, please pass async_req=True - >>> thread = api.list_deployment_for_all_namespaces_with_http_info(async_req=True) - >>> result = thread.get() - - :param async_req bool: execute request asynchronously - :param bool allow_watch_bookmarks: allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored. - :param str _continue: The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\". This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications. - :param str field_selector: A selector to restrict the list of returned objects by their fields. Defaults to everything. - :param str label_selector: A selector to restrict the list of returned objects by their labels. Defaults to everything. - :param int limit: limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true. The server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned. - :param str pretty: If 'true', then the output is pretty printed. - :param str resource_version: resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. Defaults to unset - :param str resource_version_match: resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. Defaults to unset - :param int timeout_seconds: Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity. - :param bool watch: Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion. - :param _return_http_data_only: response data without head status code - and headers - :param _preload_content: if False, the urllib3.HTTPResponse object will - be returned without reading/decoding response - data. Default is True. - :param _request_timeout: timeout setting for this request. If one - number provided, it will be total request - timeout. It can also be a pair (tuple) of - (connection, read) timeouts. - :return: tuple(V1DeploymentList, status_code(int), headers(HTTPHeaderDict)) - If the method is called asynchronously, - returns the request thread. - """ - - local_var_params = locals() - - all_params = [ - "allow_watch_bookmarks", - "_continue", - "field_selector", - "label_selector", - "limit", - "pretty", - "resource_version", - "resource_version_match", - "timeout_seconds", - "watch", - ] - all_params.extend(["async_req", "_return_http_data_only", "_preload_content", "_request_timeout"]) - - for key, val in six.iteritems(local_var_params["kwargs"]): - if key not in all_params: - raise ApiTypeError( - "Got an unexpected keyword argument '%s'" " to method list_deployment_for_all_namespaces" % key - ) - local_var_params[key] = val - del local_var_params["kwargs"] - - collection_formats = {} - - path_params = {} - - query_params = [] - if ( - "allow_watch_bookmarks" in local_var_params and local_var_params["allow_watch_bookmarks"] is not None - ): # noqa: E501 - query_params.append(("allowWatchBookmarks", local_var_params["allow_watch_bookmarks"])) # noqa: E501 - if "_continue" in local_var_params and local_var_params["_continue"] is not None: # noqa: E501 - query_params.append(("continue", local_var_params["_continue"])) # noqa: E501 - if "field_selector" in local_var_params and local_var_params["field_selector"] is not None: # noqa: E501 - query_params.append(("fieldSelector", local_var_params["field_selector"])) # noqa: E501 - if "label_selector" in local_var_params and local_var_params["label_selector"] is not None: # noqa: E501 - query_params.append(("labelSelector", local_var_params["label_selector"])) # noqa: E501 - if "limit" in local_var_params and local_var_params["limit"] is not None: # noqa: E501 - query_params.append(("limit", local_var_params["limit"])) # noqa: E501 - if "pretty" in local_var_params and local_var_params["pretty"] is not None: # noqa: E501 - query_params.append(("pretty", local_var_params["pretty"])) # noqa: E501 - if "resource_version" in local_var_params and local_var_params["resource_version"] is not None: # noqa: E501 - query_params.append(("resourceVersion", local_var_params["resource_version"])) # noqa: E501 - if ( - "resource_version_match" in local_var_params and local_var_params["resource_version_match"] is not None - ): # noqa: E501 - query_params.append(("resourceVersionMatch", local_var_params["resource_version_match"])) # noqa: E501 - if "timeout_seconds" in local_var_params and local_var_params["timeout_seconds"] is not None: # noqa: E501 - query_params.append(("timeoutSeconds", local_var_params["timeout_seconds"])) # noqa: E501 - if "watch" in local_var_params and local_var_params["watch"] is not None: # noqa: E501 - query_params.append(("watch", local_var_params["watch"])) # noqa: E501 - - header_params = {} - - form_params = [] - local_var_files = {} - - body_params = None - # HTTP header `Accept` - header_params["Accept"] = self.api_client.select_header_accept( - [ - "application/json", - "application/yaml", - "application/vnd.kubernetes.protobuf", - "application/json;stream=watch", - "application/vnd.kubernetes.protobuf;stream=watch", - ] - ) # noqa: E501 - - # Authentication setting - auth_settings = ["BearerToken"] # noqa: E501 - - return self.api_client.call_api( - "/apis/argoproj.io/v1alpha1/rollouts", - "GET", - path_params, - query_params, - header_params, - body=body_params, - post_params=form_params, - files=local_var_files, - response_type="V1DeploymentList", # noqa: E501 - auth_settings=auth_settings, - async_req=local_var_params.get("async_req"), - _return_http_data_only=local_var_params.get("_return_http_data_only"), # noqa: E501 - _preload_content=local_var_params.get("_preload_content", True), - _request_timeout=local_var_params.get("_request_timeout"), - collection_formats=collection_formats, - ) - - def list_namespaced_rollout(self, namespace, **kwargs): # noqa: E501 - """list_namespaced_rollout # noqa: E501 - - list or watch objects of kind ControllerRevision # noqa: E501 - This method makes a synchronous HTTP request by default. To make an - asynchronous HTTP request, please pass async_req=True - >>> thread = api.list_namespaced_rollout(namespace, async_req=True) - >>> result = thread.get() - - :param async_req bool: execute request asynchronously - :param str namespace: object name and auth scope, such as for teams and projects (required) - :param str pretty: If 'true', then the output is pretty printed. - :param bool allow_watch_bookmarks: allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored. - :param str _continue: The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\". This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications. - :param str field_selector: A selector to restrict the list of returned objects by their fields. Defaults to everything. - :param str label_selector: A selector to restrict the list of returned objects by their labels. Defaults to everything. - :param int limit: limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true. The server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned. - :param str resource_version: resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. Defaults to unset - :param str resource_version_match: resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. Defaults to unset - :param int timeout_seconds: Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity. - :param bool watch: Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion. - :param _preload_content: if False, the urllib3.HTTPResponse object will - be returned without reading/decoding response - data. Default is True. - :param _request_timeout: timeout setting for this request. If one - number provided, it will be total request - timeout. It can also be a pair (tuple) of - (connection, read) timeouts. - :return: V1ControllerRevisionList - If the method is called asynchronously, - returns the request thread. - """ - kwargs["_return_http_data_only"] = True - return self.list_namespaced_rollout_with_http_info(namespace, **kwargs) # noqa: E501 - - def list_namespaced_rollout_with_http_info(self, namespace, **kwargs): # noqa: E501 - """list_namespaced_rollout # noqa: E501 - - list or watch objects of kind ControllerRevision # noqa: E501 - This method makes a synchronous HTTP request by default. To make an - asynchronous HTTP request, please pass async_req=True - >>> thread = api.list_namespaced_rollout_with_http_info(namespace, async_req=True) - >>> result = thread.get() - - :param async_req bool: execute request asynchronously - :param str namespace: object name and auth scope, such as for teams and projects (required) - :param str pretty: If 'true', then the output is pretty printed. - :param bool allow_watch_bookmarks: allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored. - :param str _continue: The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\". This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications. - :param str field_selector: A selector to restrict the list of returned objects by their fields. Defaults to everything. - :param str label_selector: A selector to restrict the list of returned objects by their labels. Defaults to everything. - :param int limit: limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true. The server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned. - :param str resource_version: resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. Defaults to unset - :param str resource_version_match: resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. Defaults to unset - :param int timeout_seconds: Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity. - :param bool watch: Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion. - :param _return_http_data_only: response data without head status code - and headers - :param _preload_content: if False, the urllib3.HTTPResponse object will - be returned without reading/decoding response - data. Default is True. - :param _request_timeout: timeout setting for this request. If one - number provided, it will be total request - timeout. It can also be a pair (tuple) of - (connection, read) timeouts. - :return: tuple(V1ControllerRevisionList, status_code(int), headers(HTTPHeaderDict)) - If the method is called asynchronously, - returns the request thread. - """ - - local_var_params = locals() - - all_params = [ - "namespace", - "pretty", - "allow_watch_bookmarks", - "_continue", - "field_selector", - "label_selector", - "limit", - "resource_version", - "resource_version_match", - "timeout_seconds", - "watch", - ] - all_params.extend(["async_req", "_return_http_data_only", "_preload_content", "_request_timeout"]) - - for key, val in six.iteritems(local_var_params["kwargs"]): - if key not in all_params: - raise ApiTypeError( - "Got an unexpected keyword argument '%s'" " to method list_namespaced_controller_revision" % key - ) - local_var_params[key] = val - del local_var_params["kwargs"] - # verify the required parameter 'namespace' is set - if self.api_client.client_side_validation and ( - "namespace" not in local_var_params or local_var_params["namespace"] is None # noqa: E501 - ): # noqa: E501 - raise ApiValueError( - "Missing the required parameter `namespace` when calling `list_namespaced_controller_revision`" - ) # noqa: E501 - - collection_formats = {} - - path_params = {} - if "namespace" in local_var_params: - path_params["namespace"] = local_var_params["namespace"] # noqa: E501 - - query_params = [] - if "pretty" in local_var_params and local_var_params["pretty"] is not None: # noqa: E501 - query_params.append(("pretty", local_var_params["pretty"])) # noqa: E501 - if ( - "allow_watch_bookmarks" in local_var_params and local_var_params["allow_watch_bookmarks"] is not None - ): # noqa: E501 - query_params.append(("allowWatchBookmarks", local_var_params["allow_watch_bookmarks"])) # noqa: E501 - if "_continue" in local_var_params and local_var_params["_continue"] is not None: # noqa: E501 - query_params.append(("continue", local_var_params["_continue"])) # noqa: E501 - if "field_selector" in local_var_params and local_var_params["field_selector"] is not None: # noqa: E501 - query_params.append(("fieldSelector", local_var_params["field_selector"])) # noqa: E501 - if "label_selector" in local_var_params and local_var_params["label_selector"] is not None: # noqa: E501 - query_params.append(("labelSelector", local_var_params["label_selector"])) # noqa: E501 - if "limit" in local_var_params and local_var_params["limit"] is not None: # noqa: E501 - query_params.append(("limit", local_var_params["limit"])) # noqa: E501 - if "resource_version" in local_var_params and local_var_params["resource_version"] is not None: # noqa: E501 - query_params.append(("resourceVersion", local_var_params["resource_version"])) # noqa: E501 - if ( - "resource_version_match" in local_var_params and local_var_params["resource_version_match"] is not None - ): # noqa: E501 - query_params.append(("resourceVersionMatch", local_var_params["resource_version_match"])) # noqa: E501 - if "timeout_seconds" in local_var_params and local_var_params["timeout_seconds"] is not None: # noqa: E501 - query_params.append(("timeoutSeconds", local_var_params["timeout_seconds"])) # noqa: E501 - if "watch" in local_var_params and local_var_params["watch"] is not None: # noqa: E501 - query_params.append(("watch", local_var_params["watch"])) # noqa: E501 - - header_params = {} - - form_params = [] - local_var_files = {} - - body_params = None - # HTTP header `Accept` - header_params["Accept"] = self.api_client.select_header_accept( - [ - "application/json", - "application/yaml", - "application/vnd.kubernetes.protobuf", - "application/json;stream=watch", - "application/vnd.kubernetes.protobuf;stream=watch", - ] - ) # noqa: E501 - - # Authentication setting - auth_settings = ["BearerToken"] # noqa: E501 - - return self.api_client.call_api( - "/apis/argoproj.io/v1alpha1/namespaces/{namespace}/rollouts", - "GET", - path_params, - query_params, - header_params, - body=body_params, - post_params=form_params, - files=local_var_files, - response_type="V1DeploymentList", # noqa: E501 - auth_settings=auth_settings, - async_req=local_var_params.get("async_req"), - _return_http_data_only=local_var_params.get("_return_http_data_only"), # noqa: E501 - _preload_content=local_var_params.get("_preload_content", True), - _request_timeout=local_var_params.get("_request_timeout"), - collection_formats=collection_formats, - ) diff --git a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py index b4285997..2881e40a 100644 --- a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py +++ b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py @@ -2,7 +2,7 @@ import logging from concurrent.futures import ThreadPoolExecutor from datetime import datetime, timedelta -from typing import List, Optional +from typing import Iterable, List, Optional from kubernetes.client import ApiClient from prometheus_api_client import PrometheusApiClientException @@ -109,7 +109,7 @@ def check_connection(self): async def query(self, query: str) -> dict: loop = asyncio.get_running_loop() return await loop.run_in_executor( - self.executor, + self.executor, lambda: self.prometheus.safe_custom_query(query=query)["result"], ) @@ -210,24 +210,56 @@ async def load_pods(self, object: K8sObjectData, period: timedelta) -> list[PodD days_literal = min(int(period.total_seconds()) // 60 // 24, 32) period_literal = f"{days_literal}d" - pod_owners: list[str] + pod_owners: Iterable[str] pod_owner_kind: str cluster_label = self.get_prometheus_cluster_label() if object.kind in ["Deployment", "Rollout"]: replicasets = await self.query( f""" - kube_replicaset_owner{{ - owner_name="{object.name}", - owner_kind="{object.kind}", - namespace="{object.namespace}" - {cluster_label} - }}[{period_literal}] + kube_replicaset_owner{{ + owner_name="{object.name}", + owner_kind="{object.kind}", + namespace="{object.namespace}" + {cluster_label} + }}[{period_literal}] """ ) - pod_owners = [replicaset["metric"]["replicaset"] for replicaset in replicasets] + pod_owners = {replicaset["metric"]["replicaset"] for replicaset in replicasets} pod_owner_kind = "ReplicaSet" del replicasets + + elif object.kind == "DeploymentConfig": + replication_controllers = await self.query( + f""" + kube_replicationcontroller_owner{{ + owner_name="{object.name}", + owner_kind="{object.kind}", + namespace="{object.namespace}" + {cluster_label} + }}[{period_literal}] + """ + ) + pod_owners = {repl_controller["metric"]["replicationcontroller"] for repl_controller in replication_controllers} + pod_owner_kind = "ReplicationController" + + del replication_controllers + + elif object.kind == "CronJob": + jobs = await self.query( + f""" + kube_job_owner{{ + owner_name="{object.name}", + owner_kind="{object.kind}", + namespace="{object.namespace}" + {cluster_label} + }}[{period_literal}] + """ + ) + pod_owners = {job["metric"]["job_name"] for job in jobs} + pod_owner_kind = "Job" + + del jobs else: pod_owners = [object.name] pod_owner_kind = object.kind diff --git a/robusta_krr/core/models/config.py b/robusta_krr/core/models/config.py index 0f22854c..1d4e6a50 100644 --- a/robusta_krr/core/models/config.py +++ b/robusta_krr/core/models/config.py @@ -103,7 +103,9 @@ def validate_resources(cls, v: Union[list[str], Literal["*"]]) -> Union[list[str if v == []: return "*" - return [val.capitalize() for val in v] + # NOTE: KindLiteral.__args__ is a tuple of all possible values of KindLiteral + # So this will preserve the big and small letters of the resource + return [next(r for r in KindLiteral.__args__ if r.lower() == val.lower()) for val in v] def create_strategy(self) -> AnyStrategy: StrategyType = AnyStrategy.find(self.strategy) diff --git a/robusta_krr/core/models/objects.py b/robusta_krr/core/models/objects.py index 62149c39..e4b400d9 100644 --- a/robusta_krr/core/models/objects.py +++ b/robusta_krr/core/models/objects.py @@ -6,8 +6,9 @@ from robusta_krr.core.models.allocations import ResourceAllocations from robusta_krr.utils.batched import batched +from kubernetes.client.models import V1LabelSelector -KindLiteral = Literal["Deployment", "DaemonSet", "StatefulSet", "Job", "Rollout"] +KindLiteral = Literal["Deployment", "DaemonSet", "StatefulSet", "Job", "CronJob", "Rollout", "DeploymentConfig"] class PodData(pd.BaseModel): @@ -69,6 +70,16 @@ def deleted_pods_count(self) -> int: def pods_count(self) -> int: return len(self.pods) + @property + def selector(self) -> V1LabelSelector: + if self._api_resource is None: + raise ValueError("api_resource is not set") + + if self.kind == 'CronJob': + return self._api_resource.spec.job_template.spec.selector + else: + return self._api_resource.spec.selector + def split_into_batches(self, n: int) -> list[K8sObjectData]: """ Batch this object into n objects, splitting the pods into batches of size n. diff --git a/robusta_krr/strategies/simple.py b/robusta_krr/strategies/simple.py index 22c6a20b..8ec0f044 100644 --- a/robusta_krr/strategies/simple.py +++ b/robusta_krr/strategies/simple.py @@ -85,18 +85,15 @@ def __calculate_cpu_proposal( return ResourceRecommendation.undefined(info="No data") data_count = {pod: values[0, 1] for pod, values in history_data["CPUAmountLoader"].items()} - # Here we filter out pods from calculation that have less than `points_required` data points - filtered_data = { - pod: values for pod, values in data.items() if data_count.get(pod, 0) >= self.settings.points_required - } + total_points_count = sum(data_count.values()) - if len(filtered_data) == 0: + if total_points_count < self.settings.points_required: return ResourceRecommendation.undefined(info="Not enough data") if object_data.hpa is not None and object_data.hpa.target_cpu_utilization_percentage is not None: return ResourceRecommendation.undefined(info="HPA detected") - cpu_usage = self.settings.calculate_cpu_proposal(filtered_data) + cpu_usage = self.settings.calculate_cpu_proposal(data) return ResourceRecommendation(request=cpu_usage, limit=None) def __calculate_memory_proposal( @@ -108,18 +105,15 @@ def __calculate_memory_proposal( return ResourceRecommendation.undefined(info="No data") data_count = {pod: values[0, 1] for pod, values in history_data["MemoryAmountLoader"].items()} - # Here we filter out pods from calculation that have less than `points_required` data points - filtered_data = { - pod: value for pod, value in data.items() if data_count.get(pod, 0) >= self.settings.points_required - } + total_points_count = sum(data_count.values()) - if len(filtered_data) == 0: + if total_points_count < self.settings.points_required: return ResourceRecommendation.undefined(info="Not enough data") if object_data.hpa is not None and object_data.hpa.target_memory_utilization_percentage is not None: return ResourceRecommendation.undefined(info="HPA detected") - memory_usage = self.settings.calculate_memory_proposal(filtered_data) + memory_usage = self.settings.calculate_memory_proposal(data) return ResourceRecommendation(request=memory_usage, limit=memory_usage) def run(self, history_data: MetricsPodData, object_data: K8sObjectData) -> RunResult: diff --git a/robusta_krr/utils/object_like_dict.py b/robusta_krr/utils/object_like_dict.py new file mode 100644 index 00000000..3ad3e462 --- /dev/null +++ b/robusta_krr/utils/object_like_dict.py @@ -0,0 +1,29 @@ +class ObjectLikeDict: + def __init__(self, dictionary): + for key, value in dictionary.items(): + if isinstance(value, dict): + value = ObjectLikeDict(value) # Convert inner dict + if isinstance(value, list): + value = [ObjectLikeDict(item) if isinstance(item, dict) else item for item in value] + self.__dict__[key] = value + + def __getattr__(self, name): + return self.__dict__.get(name) + + def __setattr__(self, name, value): + self.__dict__[name] = value + + def __str__(self): + return str(self.__dict__) + + def __repr__(self): + return repr(self.__dict__) + + def __len__(self): + return len(self.__dict__) + + def get(self, key, default=None): + return self.__dict__.get(key, default) + + def items(self): + return self.__dict__.items() From 1b72bfbf1d4acd1714b45a98d50015556ff691d7 Mon Sep 17 00:00:00 2001 From: "avi@robusta.dev" Date: Sun, 17 Mar 2024 15:25:44 +0200 Subject: [PATCH 049/137] changed query to 3 hours instead of 14 days --- robusta_krr/core/runner.py | 4 ++-- robusta_krr/strategies/simple.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/robusta_krr/core/runner.py b/robusta_krr/core/runner.py index 0fcb8758..cfde3f01 100644 --- a/robusta_krr/core/runner.py +++ b/robusta_krr/core/runner.py @@ -6,7 +6,7 @@ import warnings from concurrent.futures import ThreadPoolExecutor from typing import Optional, Union - +from datetime import timedelta from prometrix import PrometheusNotFound from slack_sdk import WebClient @@ -182,7 +182,7 @@ async def _check_data_availability(self, cluster: Optional[str]) -> None: return try: - history_range = await prometheus_loader.get_history_range(self._strategy.settings.history_timedelta) + history_range = await prometheus_loader.get_history_range(timedelta(hours=4)) except ValueError: logger.exception(f"Was not able to get history range for cluster {cluster}") self.errors.append( diff --git a/robusta_krr/strategies/simple.py b/robusta_krr/strategies/simple.py index 8ec0f044..6b343e24 100644 --- a/robusta_krr/strategies/simple.py +++ b/robusta_krr/strategies/simple.py @@ -51,7 +51,7 @@ def calculate_cpu_proposal(self, data: PodsTimeData) -> float: def history_range_enough(self, history_range: tuple[timedelta, timedelta]) -> bool: start, end = history_range - return min(end - start, self.history_timedelta) / self.timeframe_timedelta >= self.points_required + return (end - start) >= timedelta(hours=3) class SimpleStrategy(BaseStrategy[SimpleStrategySettings]): From c041ac1fe7e463461509a6778ebaf9e24a1811f6 Mon Sep 17 00:00:00 2001 From: "avi@robusta.dev" Date: Sun, 17 Mar 2024 17:34:35 +0200 Subject: [PATCH 050/137] updated query --- .../prometheus/metrics_service/prometheus_metrics_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py index 2881e40a..939dc6e0 100644 --- a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py +++ b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py @@ -158,7 +158,7 @@ async def get_history_range(self, history_duration: timedelta) -> tuple[datetime now = datetime.now() result = await self.query_range( - "max(container_memory_working_set_bytes)", + "prometheus_tsdb_head_series", start=now - history_duration, end=now, step=timedelta(hours=1), From 5e26d6ccffb144894739e968269327280b7c64b4 Mon Sep 17 00:00:00 2001 From: avi robusta Date: Sun, 17 Mar 2024 18:31:57 +0200 Subject: [PATCH 051/137] changed to 5 hours back --- robusta_krr/core/runner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/robusta_krr/core/runner.py b/robusta_krr/core/runner.py index cfde3f01..4aacfb5e 100644 --- a/robusta_krr/core/runner.py +++ b/robusta_krr/core/runner.py @@ -182,7 +182,7 @@ async def _check_data_availability(self, cluster: Optional[str]) -> None: return try: - history_range = await prometheus_loader.get_history_range(timedelta(hours=4)) + history_range = await prometheus_loader.get_history_range(timedelta(hours=5)) except ValueError: logger.exception(f"Was not able to get history range for cluster {cluster}") self.errors.append( From 6b414fbb2012f94f806f1362aa6b2fcfaa3f5b9d Mon Sep 17 00:00:00 2001 From: avi robusta Date: Sun, 17 Mar 2024 19:17:49 +0200 Subject: [PATCH 052/137] fix history range query --- .../prometheus/metrics_service/prometheus_metrics_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py index 939dc6e0..0db59c2b 100644 --- a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py +++ b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py @@ -158,7 +158,7 @@ async def get_history_range(self, history_duration: timedelta) -> tuple[datetime now = datetime.now() result = await self.query_range( - "prometheus_tsdb_head_series", + "max(prometheus_tsdb_head_series)", start=now - history_duration, end=now, step=timedelta(hours=1), From 4fe49d139de02a61c2ad979c3da8c6241e78fdc2 Mon Sep 17 00:00:00 2001 From: Natan Yellin Date: Mon, 18 Mar 2024 16:43:19 +0200 Subject: [PATCH 053/137] Add --allow_hpa flag (#226) Co-authored-by: Pavel Zhukov <33721692+LeaveMyYard@users.noreply.github.com> --- robusta_krr/strategies/simple.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/robusta_krr/strategies/simple.py b/robusta_krr/strategies/simple.py index 6b343e24..8bbd08d4 100644 --- a/robusta_krr/strategies/simple.py +++ b/robusta_krr/strategies/simple.py @@ -30,6 +30,7 @@ class SimpleStrategySettings(StrategySettings): points_required: int = pd.Field( 100, ge=1, description="The number of data points required to make a recommendation for a resource." ) + allow_hpa: bool = pd.Field(False, description="Whether to calculate recommendations even when there is an HPA scaler defined on that resource.") def calculate_memory_proposal(self, data: PodsTimeData) -> float: data_ = [np.max(values[:, 1]) for values in data.values()] @@ -65,6 +66,7 @@ class SimpleStrategy(BaseStrategy[SimpleStrategySettings]): This strategy does not work with objects with HPA defined (Horizontal Pod Autoscaler). If HPA is defined for CPU or Memory, the strategy will return "?" for that resource. + You can override this behaviour by passing the --allow_hpa flag Learn more: [underline]https://github.com/robusta-dev/krr#algorithm[/underline] """ @@ -90,7 +92,7 @@ def __calculate_cpu_proposal( if total_points_count < self.settings.points_required: return ResourceRecommendation.undefined(info="Not enough data") - if object_data.hpa is not None and object_data.hpa.target_cpu_utilization_percentage is not None: + if object_data.hpa is not None and object_data.hpa.target_cpu_utilization_percentage is not None and not self.settings.allow_hpa: return ResourceRecommendation.undefined(info="HPA detected") cpu_usage = self.settings.calculate_cpu_proposal(data) @@ -110,7 +112,7 @@ def __calculate_memory_proposal( if total_points_count < self.settings.points_required: return ResourceRecommendation.undefined(info="Not enough data") - if object_data.hpa is not None and object_data.hpa.target_memory_utilization_percentage is not None: + if object_data.hpa is not None and object_data.hpa.target_memory_utilization_percentage is not None and not self.settings.allow_hpa: return ResourceRecommendation.undefined(info="HPA detected") memory_usage = self.settings.calculate_memory_proposal(data) From 0251ffc903efff22b965503ed2db92d5404d5529 Mon Sep 17 00:00:00 2001 From: Wesley Haakman <25249425+whaakman@users.noreply.github.com> Date: Mon, 18 Mar 2024 17:00:26 +0100 Subject: [PATCH 054/137] CSV Exporter (#227) * CSV Exporter * CSV Exporter * updated readme --------- Co-authored-by: Pavel Zhukov <33721692+LeaveMyYard@users.noreply.github.com> --- README.md | 1 + robusta_krr/formatters/__init__.py | 1 + robusta_krr/formatters/csv.py | 111 +++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+) create mode 100644 robusta_krr/formatters/csv.py diff --git a/README.md b/README.md index 3a34a1a5..908310d1 100644 --- a/README.md +++ b/README.md @@ -285,6 +285,7 @@ Currently KRR ships with a few formatters to represent the scan data: - `json` - `yaml` - `pprint` - data representation from python's pprint library +- `csv_export` - export data to a csv file in the current directory To run a strategy with a selected formatter, add a `-f` flag: diff --git a/robusta_krr/formatters/__init__.py b/robusta_krr/formatters/__init__.py index 325cf016..e34a25f1 100644 --- a/robusta_krr/formatters/__init__.py +++ b/robusta_krr/formatters/__init__.py @@ -2,3 +2,4 @@ from .pprint import pprint from .table import table from .yaml import yaml +from .csv import csv diff --git a/robusta_krr/formatters/csv.py b/robusta_krr/formatters/csv.py new file mode 100644 index 00000000..792c061f --- /dev/null +++ b/robusta_krr/formatters/csv.py @@ -0,0 +1,111 @@ +import itertools +import csv + +import logging + + +from robusta_krr.core.abstract import formatters +from robusta_krr.core.models.allocations import RecommendationValue +from robusta_krr.core.models.result import ResourceScan, ResourceType, Result +from robusta_krr.utils import resource_units +import datetime + +logger = logging.getLogger("krr") + +NONE_LITERAL = "unset" +NAN_LITERAL = "?" + + +def _format(value: RecommendationValue) -> str: + if value is None: + return NONE_LITERAL + elif isinstance(value, str): + return NAN_LITERAL + else: + return resource_units.format(value) + + +def __calc_diff(allocated, recommended, selector, multiplier=1) -> str: + if recommended is None or isinstance(recommended.value, str) or selector != "requests": + return "" + else: + reccomended_val = recommended.value if isinstance(recommended.value, (int, float)) else 0 + allocated_val = allocated if isinstance(allocated, (int, float)) else 0 + diff_val = reccomended_val - allocated_val + diff_sign = "+" if diff_val >= 0 else "-" + return f"{diff_sign}{_format(abs(diff_val) * multiplier)}" + + +def _format_request_str(item: ResourceScan, resource: ResourceType, selector: str) -> str: + allocated = getattr(item.object.allocations, selector)[resource] + recommended = getattr(item.recommended, selector)[resource] + + if allocated is None and recommended.value is None: + return f"{NONE_LITERAL}" + + diff = __calc_diff(allocated, recommended, selector) + if diff != "": + diff = f"({diff}) " + + return ( + diff + + _format(allocated) + + " -> " + + _format(recommended.value) + ) + + +def _format_total_diff(item: ResourceScan, resource: ResourceType, pods_current: int) -> str: + selector = "requests" + allocated = getattr(item.object.allocations, selector)[resource] + recommended = getattr(item.recommended, selector)[resource] + + return __calc_diff(allocated, recommended, selector, pods_current) + + +@formatters.register() +def csv_export(result: Result) -> str: + + current_datetime = datetime.datetime.now().strftime("%Y%m%d%H%M%S") + file_path = f"krr-{current_datetime}.csv" + + # We need to order the resource columns so that they are in the format of Namespace,Name,Pods,Old Pods,Type,Container,CPU Diff,CPU Requests,CPU Limits,Memory Diff,Memory Requests,Memory Limits + resource_columns = [] + for resource in ResourceType: + resource_columns.append(f"{resource.name} Diff") + resource_columns.append(f"{resource.name} Requests") + resource_columns.append(f"{resource.name} Limits") + + with open(file_path, 'w+', newline='') as csvfile: + csv_writer = csv.writer(csvfile) + csv_writer.writerow([ + "Namespace", "Name", "Pods", "Old Pods", "Type", "Container", + *resource_columns + + ]) + + for _, group in itertools.groupby( + enumerate(result.scans), key=lambda x: (x[1].object.cluster, x[1].object.namespace, x[1].object.name) + ): + group_items = list(group) + + for j, (i, item) in enumerate(group_items): + full_info_row = j == 0 + + row = [ + item.object.namespace if full_info_row else "", + item.object.name if full_info_row else "", + f"{item.object.current_pods_count}" if full_info_row else "", + f"{item.object.deleted_pods_count}" if full_info_row else "", + item.object.kind if full_info_row else "", + item.object.container, + ] + + for resource in ResourceType: + row.append(_format_total_diff(item, resource, item.object.current_pods_count)) + row += [_format_request_str(item, resource, selector) for selector in ["requests", "limits"]] + + csv_writer.writerow(row) + + logger.info("CSV File: %s", file_path) + return "" \ No newline at end of file From c63589857942cc4f46744919ef73aa60dd44258b Mon Sep 17 00:00:00 2001 From: Mykola Martynov Date: Tue, 19 Mar 2024 18:56:38 +0200 Subject: [PATCH 055/137] use alias when available for strategy setting (#235) * Add --allow_hpa flag * use `--allow-hpa` option * use OptionInfo instead of Option to support different name for same option * revert back allow_hpa field definition * fix issue when there is no underscore exist in option name --------- Co-authored-by: Robusta Runner --- robusta_krr/main.py | 7 ++++--- robusta_krr/strategies/simple.py | 3 +-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/robusta_krr/main.py b/robusta_krr/main.py index 83faebae..7dff56bd 100644 --- a/robusta_krr/main.py +++ b/robusta_krr/main.py @@ -10,6 +10,7 @@ import typer import urllib3 from pydantic import ValidationError # noqa: F401 +from typer.models import OptionInfo from robusta_krr import formatters as concrete_formatters # noqa: F401 from robusta_krr.core.abstract import formatters @@ -280,9 +281,9 @@ def run_strategy( inspect.Parameter( name=field_name, kind=inspect.Parameter.KEYWORD_ONLY, - default=typer.Option( - field_meta.default, - f"--{field_name}", + default=OptionInfo( + default=field_meta.default, + param_decls=list(set([f"--{field_name}", f"--{field_name.replace('_', '-')}"])), help=f"{field_meta.field_info.description}", rich_help_panel="Strategy Settings", ), diff --git a/robusta_krr/strategies/simple.py b/robusta_krr/strategies/simple.py index 8bbd08d4..64e58ac5 100644 --- a/robusta_krr/strategies/simple.py +++ b/robusta_krr/strategies/simple.py @@ -21,7 +21,6 @@ PrometheusMetric, ) - class SimpleStrategySettings(StrategySettings): cpu_percentile: float = pd.Field(99, gt=0, le=100, description="The percentile to use for the CPU recommendation.") memory_buffer_percentage: float = pd.Field( @@ -66,7 +65,7 @@ class SimpleStrategy(BaseStrategy[SimpleStrategySettings]): This strategy does not work with objects with HPA defined (Horizontal Pod Autoscaler). If HPA is defined for CPU or Memory, the strategy will return "?" for that resource. - You can override this behaviour by passing the --allow_hpa flag + You can override this behaviour by passing the --allow-hpa flag Learn more: [underline]https://github.com/robusta-dev/krr#algorithm[/underline] """ From 4da1933b66178f0b7c5faec69e00b383ed1c3142 Mon Sep 17 00:00:00 2001 From: Natan Yellin Date: Tue, 19 Mar 2024 17:57:40 +0100 Subject: [PATCH 056/137] Update README.md (#222) Co-authored-by: Pavel Zhukov <33721692+LeaveMyYard@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 908310d1..066d325b 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ Read more about [how KRR works](#how-krr-works) and [KRR vs Kubernetes VPA](#dif ### Requirements -KRR requires Prometheus and [kube-state-metrics](https://github.com/kubernetes/kube-state-metrics). +KRR requires Prometheus 2.26+ and [kube-state-metrics](https://github.com/kubernetes/kube-state-metrics).
Which metrics does KRR need? From 0fafdc6fb6cd4778b2838ccb71d3e627e5f2d2ae Mon Sep 17 00:00:00 2001 From: Robusta Runner Date: Wed, 20 Mar 2024 19:53:57 +0100 Subject: [PATCH 057/137] for dgdevops :) --- robusta_krr/core/models/config.py | 1 + robusta_krr/formatters/table.py | 3 ++- robusta_krr/main.py | 4 ++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/robusta_krr/core/models/config.py b/robusta_krr/core/models/config.py index 1d4e6a50..ab2939ba 100644 --- a/robusta_krr/core/models/config.py +++ b/robusta_krr/core/models/config.py @@ -52,6 +52,7 @@ class Config(pd.BaseSettings): # Logging Settings format: str + show_cluster_name: bool strategy: str log_to_stderr: bool width: Optional[int] = pd.Field(None, ge=1) diff --git a/robusta_krr/formatters/table.py b/robusta_krr/formatters/table.py index a1c6e420..2cd8bbbb 100644 --- a/robusta_krr/formatters/table.py +++ b/robusta_krr/formatters/table.py @@ -6,6 +6,7 @@ from robusta_krr.core.abstract import formatters from robusta_krr.core.models.allocations import RecommendationValue from robusta_krr.core.models.result import ResourceScan, ResourceType, Result +from robusta_krr.core.models.config import settings from robusta_krr.utils import resource_units NONE_LITERAL = "unset" @@ -86,7 +87,7 @@ def table(result: Result) -> Table: cluster_count = len(set(item.object.cluster for item in result.scans)) table.add_column("Number", justify="right", no_wrap=True) - if cluster_count > 1: + if cluster_count > 1 or settings.show_cluster_name: table.add_column("Cluster", style="cyan") table.add_column("Namespace", style="cyan") table.add_column("Name", style="cyan") diff --git a/robusta_krr/main.py b/robusta_krr/main.py index 83faebae..23c6f787 100644 --- a/robusta_krr/main.py +++ b/robusta_krr/main.py @@ -203,6 +203,9 @@ def run_strategy( help=f"Output formatter ({', '.join(formatters.list_available())})", rich_help_panel="Logging Settings", ), + show_cluster_name: bool = typer.Option( + False, "--show-cluster-name", help="In table output, always show the cluster name even for a single cluster", rich_help_panel="Output Settings" + ), verbose: bool = typer.Option( False, "--verbose", "-v", help="Enable verbose mode", rich_help_panel="Logging Settings" ), @@ -254,6 +257,7 @@ def run_strategy( openshift=openshift, max_workers=max_workers, format=format, + show_cluster_name=show_cluster_name, verbose=verbose, cpu_min_value=cpu_min_value, memory_min_value=memory_min_value, From 699802a26484a2bc303a6e95dbf61844d642f644 Mon Sep 17 00:00:00 2001 From: Natan Yellin Date: Thu, 21 Mar 2024 11:54:53 +0100 Subject: [PATCH 058/137] Fix fileoutput (#231) * Fix --fileoutput * Remove dead code --------- Co-authored-by: Pavel Zhukov <33721692+LeaveMyYard@users.noreply.github.com> --- robusta_krr/core/models/config.py | 7 ------- robusta_krr/core/runner.py | 6 +++--- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/robusta_krr/core/models/config.py b/robusta_krr/core/models/config.py index 1d4e6a50..dd844230 100644 --- a/robusta_krr/core/models/config.py +++ b/robusta_krr/core/models/config.py @@ -65,7 +65,6 @@ class Config(pd.BaseSettings): # Internal inside_cluster: bool = False _logging_console: Optional[Console] = pd.PrivateAttr(None) - _result_console: Optional[Console] = pd.PrivateAttr(None) def __init__(self, **kwargs: Any) -> None: super().__init__(**kwargs) @@ -132,12 +131,6 @@ def logging_console(self) -> Console: self._logging_console = Console(file=sys.stderr if self.log_to_stderr else sys.stdout, width=self.width) return self._logging_console - @property - def result_console(self) -> Console: - if getattr(self, "_result_console") is None: - self._result_console = Console(file=sys.stdout, width=self.width) - return self._result_console - def load_kubeconfig(self) -> None: try: config.load_incluster_config() diff --git a/robusta_krr/core/runner.py b/robusta_krr/core/runner.py index 4aacfb5e..f651aa81 100644 --- a/robusta_krr/core/runner.py +++ b/robusta_krr/core/runner.py @@ -8,6 +8,7 @@ from typing import Optional, Union from datetime import timedelta from prometrix import PrometheusNotFound +from rich.console import Console from slack_sdk import WebClient from robusta_krr.core.abstract.strategies import ResourceRecommendation, RunResult @@ -89,9 +90,8 @@ def _process_result(self, result: Result) -> None: elif settings.slack_output: file_name = settings.slack_output with open(file_name, "w") as target_file: - sys.stdout = target_file - custom_print(formatted, rich=rich, force=True) - sys.stdout = sys.stdout + console = Console(file=target_file, width=settings.width) + console.print(formatted) if settings.slack_output: client = WebClient(os.environ["SLACK_BOT_TOKEN"]) warnings.filterwarnings("ignore", category=UserWarning) From c89a0037cf47a06cdcfb73ce46f2bf07a90779d4 Mon Sep 17 00:00:00 2001 From: arik Date: Mon, 25 Mar 2024 08:23:00 +0100 Subject: [PATCH 059/137] Remove `app=prometheus-msteams` from prometheus discovery (#242) Log number of used workers --- README.md | 1 - robusta_krr/core/integrations/prometheus/loader.py | 1 + .../prometheus/metrics_service/prometheus_metrics_service.py | 1 - 3 files changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 066d325b..61fc9e4b 100644 --- a/README.md +++ b/README.md @@ -392,7 +392,6 @@ For discovering Prometheus it scans services for those labels: "app=prometheus,component=server" "app=prometheus-server" "app=prometheus-operator-prometheus" -"app=prometheus-msteams" "app=rancher-monitoring-prometheus" "app=prometheus-prometheus" ``` diff --git a/robusta_krr/core/integrations/prometheus/loader.py b/robusta_krr/core/integrations/prometheus/loader.py index df5af962..5593d699 100644 --- a/robusta_krr/core/integrations/prometheus/loader.py +++ b/robusta_krr/core/integrations/prometheus/loader.py @@ -38,6 +38,7 @@ def __init__(self, *, cluster: Optional[str] = None) -> None: """ self.executor = ThreadPoolExecutor(settings.max_workers) + logger.info(f"Prometheus loader max workers: {settings.max_workers}") self.api_client = ( k8s_config.new_client_from_config(config_file=settings.kubeconfig, context=cluster) diff --git a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py index 0db59c2b..f0ebba3b 100644 --- a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py +++ b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py @@ -38,7 +38,6 @@ def find_metrics_url(self, *, api_client: Optional[ApiClient] = None) -> Optiona "app=prometheus,component=server", "app=prometheus-server", "app=prometheus-operator-prometheus", - "app=prometheus-msteams", "app=rancher-monitoring-prometheus", "app=prometheus-prometheus", ] From faa69edc547d36bfa7ad4edf5f5ea79906cb90de Mon Sep 17 00:00:00 2001 From: Natan Yellin Date: Tue, 26 Mar 2024 09:23:24 +0200 Subject: [PATCH 060/137] for dgdevops :) (#243) --- robusta_krr/core/models/config.py | 1 + robusta_krr/formatters/table.py | 3 ++- robusta_krr/main.py | 4 ++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/robusta_krr/core/models/config.py b/robusta_krr/core/models/config.py index dd844230..36787d40 100644 --- a/robusta_krr/core/models/config.py +++ b/robusta_krr/core/models/config.py @@ -52,6 +52,7 @@ class Config(pd.BaseSettings): # Logging Settings format: str + show_cluster_name: bool strategy: str log_to_stderr: bool width: Optional[int] = pd.Field(None, ge=1) diff --git a/robusta_krr/formatters/table.py b/robusta_krr/formatters/table.py index a1c6e420..2cd8bbbb 100644 --- a/robusta_krr/formatters/table.py +++ b/robusta_krr/formatters/table.py @@ -6,6 +6,7 @@ from robusta_krr.core.abstract import formatters from robusta_krr.core.models.allocations import RecommendationValue from robusta_krr.core.models.result import ResourceScan, ResourceType, Result +from robusta_krr.core.models.config import settings from robusta_krr.utils import resource_units NONE_LITERAL = "unset" @@ -86,7 +87,7 @@ def table(result: Result) -> Table: cluster_count = len(set(item.object.cluster for item in result.scans)) table.add_column("Number", justify="right", no_wrap=True) - if cluster_count > 1: + if cluster_count > 1 or settings.show_cluster_name: table.add_column("Cluster", style="cyan") table.add_column("Namespace", style="cyan") table.add_column("Name", style="cyan") diff --git a/robusta_krr/main.py b/robusta_krr/main.py index 7dff56bd..e61db7ff 100644 --- a/robusta_krr/main.py +++ b/robusta_krr/main.py @@ -204,6 +204,9 @@ def run_strategy( help=f"Output formatter ({', '.join(formatters.list_available())})", rich_help_panel="Logging Settings", ), + show_cluster_name: bool = typer.Option( + False, "--show-cluster-name", help="In table output, always show the cluster name even for a single cluster", rich_help_panel="Output Settings" + ), verbose: bool = typer.Option( False, "--verbose", "-v", help="Enable verbose mode", rich_help_panel="Logging Settings" ), @@ -255,6 +258,7 @@ def run_strategy( openshift=openshift, max_workers=max_workers, format=format, + show_cluster_name=show_cluster_name, verbose=verbose, cpu_min_value=cpu_min_value, memory_min_value=memory_min_value, From 4e450f3ecf7a7df161956fe1574392e9c406054c Mon Sep 17 00:00:00 2001 From: Pavel Zhukov <33721692+LeaveMyYard@users.noreply.github.com> Date: Tue, 26 Mar 2024 11:54:14 +0200 Subject: [PATCH 061/137] Improve limited permissions (cherry-picked from #220) (#238) * Add --as option to impersonate a specific user * Update test case * Don't exit if the user lacks permissions to auto-discover prometheus * Add a comment * Add support for HPA w/o cluster-level permissions * feat: cli option for --as-group (#224) * feat: cli option for --as-group * add: as-group example * Improve a message in case of API error * Return the debug log with found items in cluster --------- Co-authored-by: Robusta Runner Co-authored-by: Rohan Katkar Co-authored-by: LeaveMyYard --- README.md | 2 + .../core/integrations/kubernetes/__init__.py | 147 +++++++++--------- .../core/integrations/prometheus/loader.py | 14 +- robusta_krr/core/models/config.py | 11 ++ robusta_krr/main.py | 14 ++ tests/single_namespace_as_group.yaml | 38 +++++ tests/single_namespace_permissions.yaml | 42 +++++ 7 files changed, 191 insertions(+), 77 deletions(-) create mode 100644 tests/single_namespace_as_group.yaml create mode 100644 tests/single_namespace_permissions.yaml diff --git a/README.md b/README.md index 61fc9e4b..51fea85b 100644 --- a/README.md +++ b/README.md @@ -253,6 +253,8 @@ List as many namespaces as you want with `-n` (in this case, `default` and `ingr ```sh krr simple -n default -n ingress-nginx ``` + +See [example ServiceAccount and RBAC permissions](./tests/single_namespace_permissions.yaml)
diff --git a/robusta_krr/core/integrations/kubernetes/__init__.py b/robusta_krr/core/integrations/kubernetes/__init__.py index c7976cfa..335b47af 100644 --- a/robusta_krr/core/integrations/kubernetes/__init__.py +++ b/robusta_krr/core/integrations/kubernetes/__init__.py @@ -10,13 +10,11 @@ V1Container, V1DaemonSet, V1Deployment, - V1HorizontalPodAutoscalerList, V1Job, V1Pod, V1PodList, V1StatefulSet, V2HorizontalPodAutoscaler, - V2HorizontalPodAutoscalerList, ) from robusta_krr.core.models.config import settings @@ -34,15 +32,11 @@ class ClusterLoader: - def __init__(self, cluster: Optional[str]): + def __init__(self, cluster: Optional[str]=None): self.cluster = cluster # This executor will be running requests to Kubernetes API self.executor = ThreadPoolExecutor(settings.max_workers) - self.api_client = ( - config.new_client_from_config(context=cluster, config_file=settings.kubeconfig) - if cluster is not None - else None - ) + self.api_client = settings.get_kube_client(cluster) self.apps = client.AppsV1Api(api_client=self.api_client) self.custom_objects = client.CustomObjectsApi(api_client=self.api_client) self.batch = client.BatchV1Api(api_client=self.api_client) @@ -162,7 +156,7 @@ def _build_selector_query(selector: Any) -> Union[str, None]: return ",".join(label_filters) - def __build_obj( + def __build_scannable_object( self, item: AnyKubernetesAPIObject, container: V1Container, kind: Optional[str] = None ) -> K8sObjectData: name = item.metadata.name @@ -186,7 +180,48 @@ def _should_list_resource(self, resource: str) -> bool: return True return resource in settings.resources - async def _list_workflows( + async def _list_namespaced_or_global_objects( + self, + kind: KindLiteral, + all_namespaces_request: Callable, + namespaced_request: Callable + ) -> AsyncIterable[Any]: + logger.debug(f"Listing {kind}s in {self.cluster}") + loop = asyncio.get_running_loop() + + if settings.namespaces == "*": + tasks = [ + loop.run_in_executor( + self.executor, + lambda: all_namespaces_request( + watch=False, + label_selector=settings.selector, + ), + ) + ] + else: + tasks = [ + loop.run_in_executor( + self.executor, + lambda ns=namespace: namespaced_request( + namespace=ns, + watch=False, + label_selector=settings.selector, + ), + ) + for namespace in settings.namespaces + ] + + total_items = 0 + for task in asyncio.as_completed(tasks): + ret_single = await task + total_items += len(ret_single.items) + for item in ret_single.items: + yield item + + logger.debug(f"Found {total_items} {kind} in {self.cluster}") + + async def _list_scannable_objects( self, kind: KindLiteral, all_namespaces_request: Callable, @@ -201,49 +236,17 @@ async def _list_workflows( if not self.__kind_available[kind]: return - logger.debug(f"Listing {kind}s in {self.cluster}") - loop = asyncio.get_running_loop() - try: - if settings.namespaces == "*": - tasks = [ - loop.run_in_executor( - self.executor, - lambda: all_namespaces_request( - watch=False, - label_selector=settings.selector, - ), - ) - ] - else: - tasks = [ - loop.run_in_executor( - self.executor, - lambda ns=namespace: namespaced_request( - namespace=ns, - watch=False, - label_selector=settings.selector, - ), - ) - for namespace in settings.namespaces - ] - - total_items = 0 - for task in asyncio.as_completed(tasks): - ret_single = await task - total_items += len(ret_single.items) - for item in ret_single.items: - if filter_workflows is not None and not filter_workflows(item): - continue - - containers = extract_containers(item) - if asyncio.iscoroutine(containers): - containers = await containers - - for container in containers: - yield self.__build_obj(item, container, kind) - - logger.debug(f"Found {total_items} {kind} in {self.cluster}") + async for item in self._list_namespaced_or_global_objects(kind, all_namespaces_request, namespaced_request): + if filter_workflows is not None and not filter_workflows(item): + continue + + containers = extract_containers(item) + if asyncio.iscoroutine(containers): + containers = await containers + + for container in containers: + yield self.__build_scannable_object(item, container, kind) except ApiException as e: if kind in ("Rollout", "DeploymentConfig") and e.status in [400, 401, 403, 404]: if self.__kind_available[kind]: @@ -254,7 +257,7 @@ async def _list_workflows( logger.error("Will skip this object type and continue.") def _list_deployments(self) -> AsyncIterable[K8sObjectData]: - return self._list_workflows( + return self._list_scannable_objects( kind="Deployment", all_namespaces_request=self.apps.list_deployment_for_all_namespaces, namespaced_request=self.apps.list_namespaced_deployment, @@ -287,7 +290,7 @@ async def _extract_containers(item: Any) -> list[V1Container]: # NOTE: Using custom objects API returns dicts, but all other APIs return objects # We need to handle this difference using a small wrapper - return self._list_workflows( + return self._list_scannable_objects( kind="Rollout", all_namespaces_request=lambda **kwargs: ObjectLikeDict( self.custom_objects.list_cluster_custom_object( @@ -311,7 +314,7 @@ async def _extract_containers(item: Any) -> list[V1Container]: def _list_deploymentconfig(self) -> AsyncIterable[K8sObjectData]: # NOTE: Using custom objects API returns dicts, but all other APIs return objects # We need to handle this difference using a small wrapper - return self._list_workflows( + return self._list_scannable_objects( kind="DeploymentConfig", all_namespaces_request=lambda **kwargs: ObjectLikeDict( self.custom_objects.list_cluster_custom_object( @@ -333,7 +336,7 @@ def _list_deploymentconfig(self) -> AsyncIterable[K8sObjectData]: ) def _list_all_statefulsets(self) -> AsyncIterable[K8sObjectData]: - return self._list_workflows( + return self._list_scannable_objects( kind="StatefulSet", all_namespaces_request=self.apps.list_stateful_set_for_all_namespaces, namespaced_request=self.apps.list_namespaced_stateful_set, @@ -341,7 +344,7 @@ def _list_all_statefulsets(self) -> AsyncIterable[K8sObjectData]: ) def _list_all_daemon_set(self) -> AsyncIterable[K8sObjectData]: - return self._list_workflows( + return self._list_scannable_objects( kind="DaemonSet", all_namespaces_request=self.apps.list_daemon_set_for_all_namespaces, namespaced_request=self.apps.list_namespaced_daemon_set, @@ -349,7 +352,7 @@ def _list_all_daemon_set(self) -> AsyncIterable[K8sObjectData]: ) def _list_all_jobs(self) -> AsyncIterable[K8sObjectData]: - return self._list_workflows( + return self._list_scannable_objects( kind="Job", all_namespaces_request=self.batch.list_job_for_all_namespaces, namespaced_request=self.batch.list_namespaced_job, @@ -361,7 +364,7 @@ def _list_all_jobs(self) -> AsyncIterable[K8sObjectData]: ) def _list_all_cronjobs(self) -> AsyncIterable[K8sObjectData]: - return self._list_workflows( + return self._list_scannable_objects( kind="CronJob", all_namespaces_request=self.batch.list_cron_job_for_all_namespaces, namespaced_request=self.batch.list_namespaced_cron_job, @@ -370,11 +373,14 @@ def _list_all_cronjobs(self) -> AsyncIterable[K8sObjectData]: async def __list_hpa_v1(self) -> dict[HPAKey, HPAData]: loop = asyncio.get_running_loop() - - res: V1HorizontalPodAutoscalerList = await loop.run_in_executor( - self.executor, lambda: self.autoscaling_v1.list_horizontal_pod_autoscaler_for_all_namespaces(watch=False) + res = await loop.run_in_executor( + self.executor, + lambda: self._list_namespaced_or_global_objects( + kind="HPA-v1", + all_namespaces_request=self.autoscaling_v1.list_horizontal_pod_autoscaler_for_all_namespaces, + namespaced_request=self.autoscaling_v1.list_namespaced_horizontal_pod_autoscaler, + ), ) - return { ( hpa.metadata.namespace, @@ -388,17 +394,19 @@ async def __list_hpa_v1(self) -> dict[HPAKey, HPAData]: target_cpu_utilization_percentage=hpa.spec.target_cpu_utilization_percentage, target_memory_utilization_percentage=None, ) - for hpa in res.items + async for hpa in res } async def __list_hpa_v2(self) -> dict[HPAKey, HPAData]: loop = asyncio.get_running_loop() - - res: V2HorizontalPodAutoscalerList = await loop.run_in_executor( + res = await loop.run_in_executor( self.executor, - lambda: self.autoscaling_v2.list_horizontal_pod_autoscaler_for_all_namespaces(watch=False), + lambda: self._list_namespaced_or_global_objects( + kind="HPA-v2", + all_namespaces_request=self.autoscaling_v2.list_horizontal_pod_autoscaler_for_all_namespaces, + namespaced_request=self.autoscaling_v2.list_namespaced_horizontal_pod_autoscaler, + ), ) - def __get_metric(hpa: V2HorizontalPodAutoscaler, metric_name: str) -> Optional[float]: return next( ( @@ -408,7 +416,6 @@ def __get_metric(hpa: V2HorizontalPodAutoscaler, metric_name: str) -> Optional[f ), None, ) - return { ( hpa.metadata.namespace, @@ -422,7 +429,7 @@ def __get_metric(hpa: V2HorizontalPodAutoscaler, metric_name: str) -> Optional[f target_cpu_utilization_percentage=__get_metric(hpa, "cpu"), target_memory_utilization_percentage=__get_metric(hpa, "memory"), ) - for hpa in res.items + async for hpa in res } # TODO: What should we do in case of other metrics bound to the HPA? diff --git a/robusta_krr/core/integrations/prometheus/loader.py b/robusta_krr/core/integrations/prometheus/loader.py index 5593d699..b9b600f4 100644 --- a/robusta_krr/core/integrations/prometheus/loader.py +++ b/robusta_krr/core/integrations/prometheus/loader.py @@ -7,6 +7,7 @@ from kubernetes import config as k8s_config from kubernetes.client.api_client import ApiClient +from kubernetes.client.exceptions import ApiException from prometrix import MetricsNotFound, PrometheusNotFound from robusta_krr.core.models.config import settings @@ -38,13 +39,7 @@ def __init__(self, *, cluster: Optional[str] = None) -> None: """ self.executor = ThreadPoolExecutor(settings.max_workers) - logger.info(f"Prometheus loader max workers: {settings.max_workers}") - - self.api_client = ( - k8s_config.new_client_from_config(config_file=settings.kubeconfig, context=cluster) - if cluster is not None - else None - ) + self.api_client = settings.get_kube_client(context=cluster) loader = self.get_metrics_service(api_client=self.api_client, cluster=cluster) if loader is None: raise PrometheusNotFound("No Prometheus or metrics service found") @@ -67,6 +62,11 @@ def get_metrics_service( return loader except MetricsNotFound as e: logger.info(f"{service_name} not found: {e}") + except ApiException as e: + logger.warning( + f"Unable to automatically discover a {service_name} in the cluster ({e}). " + "Try specifying how to connect to Prometheus via cli options" + ) return None diff --git a/robusta_krr/core/models/config.py b/robusta_krr/core/models/config.py index 36787d40..ac3b15ca 100644 --- a/robusta_krr/core/models/config.py +++ b/robusta_krr/core/models/config.py @@ -23,6 +23,8 @@ class Config(pd.BaseSettings): clusters: Union[list[str], Literal["*"], None] = None kubeconfig: Optional[str] = None + impersonate_user: Optional[str] = None + impersonate_group: Optional[str] = None namespaces: Union[list[str], Literal["*"]] = pd.Field("*") resources: Union[list[KindLiteral], Literal["*"]] = pd.Field("*") selector: Optional[str] = None @@ -141,6 +143,15 @@ def load_kubeconfig(self) -> None: else: self.inside_cluster = True + def get_kube_client(self, context: Optional[str] = None): + api_client = config.new_client_from_config(context=context, config_file=self.kubeconfig) + if self.impersonate_user is not None: + # trick copied from https://github.com/kubernetes-client/python/issues/362 + api_client.set_default_header("Impersonate-User", self.impersonate_user) + if self.impersonate_group is not None: + api_client.set_default_header("Impersonate-Group", self.impersonate_group) + return api_client + @staticmethod def set_config(config: Config) -> None: global _config diff --git a/robusta_krr/main.py b/robusta_krr/main.py index e61db7ff..7e774860 100644 --- a/robusta_krr/main.py +++ b/robusta_krr/main.py @@ -55,6 +55,18 @@ def run_strategy( help="Path to kubeconfig file. If not provided, will attempt to find it.", rich_help_panel="Kubernetes Settings", ), + impersonate_user: Optional[str] = typer.Option( + None, + "--as", + help="Impersonate a user, just like `kubectl --as`. For example, system:serviceaccount:default:krr-account.", + rich_help_panel="Kubernetes Settings", + ), + impersonate_group: Optional[str] = typer.Option( + None, + "--as-group", + help="Impersonate a user inside of a group, just like `kubectl --as-group`. For example, system:authenticated.", + rich_help_panel="Kubernetes Settings", + ), clusters: List[str] = typer.Option( None, "--context", @@ -238,6 +250,8 @@ def run_strategy( try: config = Config( kubeconfig=kubeconfig, + impersonate_user=impersonate_user, + impersonate_group=impersonate_group, clusters="*" if all_clusters else clusters, namespaces="*" if "*" in namespaces else namespaces, resources="*" if "*" in resources else resources, diff --git a/tests/single_namespace_as_group.yaml b/tests/single_namespace_as_group.yaml new file mode 100644 index 00000000..16f08056 --- /dev/null +++ b/tests/single_namespace_as_group.yaml @@ -0,0 +1,38 @@ +# Test environment for per-namespace scans using a group object ID (for e.g. Microsoft Entra) +# The purpose of this setup is to verify that per-namespace features work without cluster level permissions +# You can test this Group and KRR using: +# A user named aksdev that's part of the appdev group. +# krr simple --as aksdev --as-group -n kube-system +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + namespace: kube-system + name: krr-role +rules: +- apiGroups: [""] + resources: ["pods", "services"] + verbs: ["get", "watch", "list"] +- apiGroups: ["batch"] + resources: ["jobs"] + verbs: ["get", "watch", "list"] +- apiGroups: ["apps"] + resources: ["deployments", "replicasets", "daemonsets", "statefulsets"] + verbs: ["get", "list", "watch"] +- apiGroups: ["autoscaling"] + resources: ["horizontalpodautoscalers"] + verbs: ["get", "list", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: krr-role-binding + namespace: kube-system +subjects: +- kind: Group + # Replace with the actual Group Object ID + name: + apiGroup: rbac.authorization.k8s.io +roleRef: + kind: Role + name: krr-role + apiGroup: rbac.authorization.k8s.io diff --git a/tests/single_namespace_permissions.yaml b/tests/single_namespace_permissions.yaml new file mode 100644 index 00000000..f6e324d8 --- /dev/null +++ b/tests/single_namespace_permissions.yaml @@ -0,0 +1,42 @@ +# Test environment for per-namespace scans +# The purpose of this setup is to verify that per-namespace features work without cluster level permissions +# You can test this ServiceAccount and KRR using: +# krr simple --as system:serviceaccount:kube-system:krr-account -n kube-system +apiVersion: v1 +kind: ServiceAccount +metadata: + name: krr-account + namespace: kube-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + namespace: kube-system + name: krr-role +rules: +- apiGroups: [""] + resources: ["pods", "services"] + verbs: ["get", "watch", "list"] +- apiGroups: ["batch"] + resources: ["jobs"] + verbs: ["get", "watch", "list"] +- apiGroups: ["apps"] + resources: ["deployments", "replicasets", "daemonsets", "statefulsets"] + verbs: ["get", "list", "watch"] +- apiGroups: ["autoscaling"] + resources: ["horizontalpodautoscalers"] + verbs: ["get", "list", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: krr-role-binding + namespace: kube-system +subjects: +- kind: ServiceAccount + name: krr-account + namespace: kube-system +roleRef: + kind: Role + name: krr-role + apiGroup: rbac.authorization.k8s.io From ba140253ac140acba61a0caf55791bed179ef297 Mon Sep 17 00:00:00 2001 From: Pavel Zhukov <33721692+LeaveMyYard@users.noreply.github.com> Date: Tue, 26 Mar 2024 13:24:56 +0200 Subject: [PATCH 062/137] Improving prometheus detection step (#236) * Rework prometheus detection logging, fix #119 * Fix success if no scans were made * Fix get_history_range in tests * Remove unused constant --------- Co-authored-by: LeaveMyYard --- .../core/integrations/prometheus/loader.py | 31 +++++++++++------- .../metrics_service/base_metric_service.py | 6 ++-- .../prometheus_metrics_service.py | 9 +++--- .../victoria_metrics_service.py | 4 +++ robusta_krr/core/runner.py | 32 +++++++++++++------ robusta_krr/main.py | 3 +- tests/conftest.py | 13 ++++++++ 7 files changed, 67 insertions(+), 31 deletions(-) diff --git a/robusta_krr/core/integrations/prometheus/loader.py b/robusta_krr/core/integrations/prometheus/loader.py index b9b600f4..82c7d746 100644 --- a/robusta_krr/core/integrations/prometheus/loader.py +++ b/robusta_krr/core/integrations/prometheus/loader.py @@ -22,12 +22,6 @@ logger = logging.getLogger("krr") -METRICS_SERVICES = { - "Prometheus": PrometheusMetricsService, - "Victoria Metrics": VictoriaMetricsService, - "Thanos": ThanosMetricsService, -} - class PrometheusMetricsLoader: def __init__(self, *, cluster: Optional[str] = None) -> None: @@ -42,24 +36,33 @@ def __init__(self, *, cluster: Optional[str] = None) -> None: self.api_client = settings.get_kube_client(context=cluster) loader = self.get_metrics_service(api_client=self.api_client, cluster=cluster) if loader is None: - raise PrometheusNotFound("No Prometheus or metrics service found") + raise PrometheusNotFound( + f"Wasn't able to connect to any Prometheus service in {cluster or 'inner'} cluster\n" + "Try using port-forwarding and/or setting the url manually (using the -p flag.).\n" + "For more information, see 'Giving the Explicit Prometheus URL' at https://github.com/robusta-dev/krr?tab=readme-ov-file#usage" + ) self.loader = loader - logger.info(f"{self.loader.name} connected successfully for {cluster or 'default'} cluster") + logger.info(f"{self.loader.name()} connected successfully for {cluster or 'default'} cluster") def get_metrics_service( self, api_client: Optional[ApiClient] = None, cluster: Optional[str] = None, ) -> Optional[PrometheusMetricsService]: - for service_name, metric_service_class in METRICS_SERVICES.items(): + if settings.prometheus_url is not None: + logger.info("Prometheus URL is specified, will not auto-detect a metrics service") + metrics_to_check = [PrometheusMetricsService] + else: + logger.info("No Prometheus URL is specified, trying to auto-detect a metrics service") + metrics_to_check = [VictoriaMetricsService, ThanosMetricsService, PrometheusMetricsService] + + for metric_service_class in metrics_to_check: + service_name = metric_service_class.name() try: loader = metric_service_class(api_client=api_client, cluster=cluster, executor=self.executor) loader.check_connection() - logger.info(f"{service_name} found") - loader.validate_cluster_name() - return loader except MetricsNotFound as e: logger.info(f"{service_name} not found: {e}") except ApiException as e: @@ -67,6 +70,10 @@ def get_metrics_service( f"Unable to automatically discover a {service_name} in the cluster ({e}). " "Try specifying how to connect to Prometheus via cli options" ) + else: + logger.info(f"{service_name} found") + loader.validate_cluster_name() + return loader return None diff --git a/robusta_krr/core/integrations/prometheus/metrics_service/base_metric_service.py b/robusta_krr/core/integrations/prometheus/metrics_service/base_metric_service.py index 9adb5b53..a3b0ee0f 100644 --- a/robusta_krr/core/integrations/prometheus/metrics_service/base_metric_service.py +++ b/robusta_krr/core/integrations/prometheus/metrics_service/base_metric_service.py @@ -27,9 +27,9 @@ def __init__( def check_connection(self): ... - @property - def name(self) -> str: - classname = self.__class__.__name__ + @classmethod + def name(cls) -> str: + classname = cls.__name__ return classname.replace("MetricsService", "") if classname != MetricsService.__name__ else classname @abc.abstractmethod diff --git a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py index f0ebba3b..e61ef4c8 100644 --- a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py +++ b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py @@ -60,7 +60,7 @@ def __init__( ) -> None: super().__init__(api_client=api_client, cluster=cluster, executor=executor) - logger.info(f"Connecting to {self.name} for {self.cluster} cluster") + logger.info(f"Trying to connect to {self.name()} for {self.cluster} cluster") self.auth_header = settings.prometheus_auth_header self.ssl_enabled = settings.prometheus_ssl_enabled @@ -82,11 +82,10 @@ def __init__( if not self.url: raise PrometheusNotFound( - f"{self.name} instance could not be found while scanning in {self.cluster} cluster.\n" - "\tTry using port-forwarding and/or setting the url manually (using the -p flag.)." + f"{self.name()} instance could not be found while scanning in {self.cluster} cluster." ) - logger.info(f"Using {self.name} at {self.url} for cluster {cluster or 'default'}") + logger.info(f"Using {self.name()} at {self.url} for cluster {cluster or 'default'}") headers = settings.prometheus_other_headers @@ -182,7 +181,7 @@ async def gather_data( """ logger.debug(f"Gathering {LoaderClass.__name__} metric for {object}") - metric_loader = LoaderClass(self.prometheus, self.name, self.executor) + metric_loader = LoaderClass(self.prometheus, self.name(), self.executor) data = await metric_loader.load_data(object, period, step) if len(data) == 0: diff --git a/robusta_krr/core/integrations/prometheus/metrics_service/victoria_metrics_service.py b/robusta_krr/core/integrations/prometheus/metrics_service/victoria_metrics_service.py index 202055fe..e8fbcd02 100644 --- a/robusta_krr/core/integrations/prometheus/metrics_service/victoria_metrics_service.py +++ b/robusta_krr/core/integrations/prometheus/metrics_service/victoria_metrics_service.py @@ -42,6 +42,10 @@ class VictoriaMetricsService(PrometheusMetricsService): service_discovery = VictoriaMetricsDiscovery + @classmethod + def name(cls) -> str: + return "Victoria Metrics" + def check_connection(self): """ Checks the connection to Prometheus. diff --git a/robusta_krr/core/runner.py b/robusta_krr/core/runner.py index f651aa81..25a85e86 100644 --- a/robusta_krr/core/runner.py +++ b/robusta_krr/core/runner.py @@ -33,6 +33,9 @@ def custom_print(*objects, rich: bool = True, force: bool = False) -> None: print_func(*objects) # type: ignore +class CriticalRunnerException(Exception): ... + + class Runner: EXPECTED_EXCEPTIONS = (KeyboardInterrupt, PrometheusNotFound) @@ -141,11 +144,11 @@ def _format_result(self, result: RunResult) -> RunResult: for resource, recommendation in result.items() } - async def _calculate_object_recommendations(self, object: K8sObjectData) -> RunResult: + async def _calculate_object_recommendations(self, object: K8sObjectData) -> Optional[RunResult]: prometheus_loader = self._get_prometheus_loader(object.cluster) if prometheus_loader is None: - return {resource: ResourceRecommendation.undefined("Prometheus not found") for resource in ResourceType} + return None object.pods = await prometheus_loader.load_pods(object, self._strategy.settings.history_timedelta) if object.pods == []: @@ -213,11 +216,14 @@ async def _check_data_availability(self, cluster: Optional[str]) -> None: } ) - async def _gather_object_allocations(self, k8s_object: K8sObjectData) -> ResourceScan: + async def _gather_object_allocations(self, k8s_object: K8sObjectData) -> Optional[ResourceScan]: recommendation = await self._calculate_object_recommendations(k8s_object) self.__progressbar.progress() + if recommendation is None: + return None + return ResourceScan.calculate( k8s_object, ResourceAllocations( @@ -253,6 +259,8 @@ async def _collect_result(self) -> Result: scans = await asyncio.gather(*scans_tasks) + successful_scans = [scan for scan in scans if scan is not None] + if len(scans) == 0: logger.warning("Current filters resulted in no objects available to scan.") logger.warning("Try to change the filters or check if there is anything available.") @@ -260,10 +268,9 @@ async def _collect_result(self) -> Result: logger.warning( "Note that you are using the '*' namespace filter, which by default excludes kube-system." ) - return Result( - scans=[], - strategy=StrategyData(name=str(self._strategy).lower(), settings=self._strategy.settings.dict()), - ) + raise CriticalRunnerException("No objects available to scan.") + elif len(successful_scans) == 0: + raise CriticalRunnerException("No successful scans were made. Check the logs for more information.") return Result( scans=scans, @@ -274,7 +281,8 @@ async def _collect_result(self) -> Result: ), ) - async def run(self) -> None: + async def run(self) -> int: + """Run the Runner. The return value is the exit code of the program.""" self._greet() try: @@ -298,7 +306,11 @@ async def run(self) -> None: result = await self._collect_result() logger.info("Result collected, displaying...") self._process_result(result) - except ClusterNotSpecifiedException as e: - logger.error(e) + except (ClusterNotSpecifiedException, CriticalRunnerException) as e: + logger.critical(e) + return 1 # Exit with error except Exception: logger.exception("An unexpected error occurred") + return 1 # Exit with error + else: + return 0 # Exit with success diff --git a/robusta_krr/main.py b/robusta_krr/main.py index 7e774860..5c2d01aa 100644 --- a/robusta_krr/main.py +++ b/robusta_krr/main.py @@ -289,7 +289,8 @@ def run_strategy( logger.exception("Error occured while parsing arguments") else: runner = Runner() - asyncio.run(runner.run()) + exit_code = asyncio.run(runner.run()) + raise typer.Exit(code=exit_code) run_strategy.__name__ = strategy_name signature = inspect.signature(run_strategy) diff --git a/tests/conftest.py b/tests/conftest.py index 8906c423..61c389dd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -94,6 +94,19 @@ def mock_prometheus_load_pods(): yield +@pytest.fixture(autouse=True, scope="session") +def mock_prometheus_get_history_range(): + async def get_history_range(self, history_duration: timedelta) -> tuple[datetime, datetime]: + now = datetime.now() + start = now - history_duration + return start, now + + with patch( + "robusta_krr.core.integrations.prometheus.loader.PrometheusMetricsLoader.get_history_range", get_history_range + ): + yield + + @pytest.fixture(autouse=True, scope="session") def mock_prometheus_init(): with patch("robusta_krr.core.integrations.prometheus.loader.PrometheusMetricsLoader.__init__", return_value=None): From 9fc5752297567790563ce1cd6e8f0212756a9373 Mon Sep 17 00:00:00 2001 From: Robusta Runner Date: Wed, 27 Mar 2024 05:57:46 +0200 Subject: [PATCH 063/137] bugfix for --show-cluster-name flag --- robusta_krr/formatters/table.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/robusta_krr/formatters/table.py b/robusta_krr/formatters/table.py index 2cd8bbbb..3d20b8e4 100644 --- a/robusta_krr/formatters/table.py +++ b/robusta_krr/formatters/table.py @@ -110,7 +110,7 @@ def table(result: Result) -> Table: full_info_row = j == 0 cells: list[Any] = [f"[{item.severity.color}]{i + 1}.[/{item.severity.color}]"] - if cluster_count > 1: + if cluster_count > 1 or settings.show_cluster_name: cells.append(item.object.cluster if full_info_row else "") cells += [ item.object.namespace if full_info_row else "", From 6058075e6a95516942085172787e4abcfdc99413 Mon Sep 17 00:00:00 2001 From: Pavel Zhukov <33721692+LeaveMyYard@users.noreply.github.com> Date: Wed, 27 Mar 2024 16:53:55 +0200 Subject: [PATCH 064/137] Fix blocking async merge (#249) * Fix for blocking async merge * Fix the edge case with empty iterables --- robusta_krr/utils/async_gen_merge.py | 42 +++++++++++----------------- 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/robusta_krr/utils/async_gen_merge.py b/robusta_krr/utils/async_gen_merge.py index 71528953..35c2c866 100644 --- a/robusta_krr/utils/async_gen_merge.py +++ b/robusta_krr/utils/async_gen_merge.py @@ -11,39 +11,29 @@ def async_gen_merge(*aiters: AsyncIterable[T]) -> AsyncIterable[T]: - queue = asyncio.Queue(1) - run_count = len(aiters) - cancelling = False + queue = asyncio.Queue() + iters_remaining = set(aiters) async def drain(aiter): - nonlocal run_count try: async for item in aiter: - await queue.put((False, item)) - except Exception as e: - if not cancelling: - await queue.put((True, e)) - else: - raise + await queue.put(item) + except Exception: + logger.exception(f"Error in async generator {aiter}") finally: - run_count -= 1 + iters_remaining.discard(aiter) + await queue.put(None) async def merged(): - try: - while run_count: - raised, next_item = await queue.get() - if raised: - cancel_tasks() - raise next_item - yield next_item - finally: - cancel_tasks() + while iters_remaining or not queue.empty(): + item = await queue.get() + + if item is None: + continue + + yield item - def cancel_tasks(): - nonlocal cancelling - cancelling = True - for t in tasks: - t.cancel() + for aiter in aiters: + asyncio.create_task(drain(aiter)) - tasks = [asyncio.create_task(drain(aiter)) for aiter in aiters] return merged() From be90947b49afa7e69b8024758bb18824d466d34e Mon Sep 17 00:00:00 2001 From: LeaveMyYard Date: Thu, 28 Mar 2024 17:15:24 +0200 Subject: [PATCH 065/137] Add intro file --- intro.txt | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 intro.txt diff --git a/intro.txt b/intro.txt new file mode 100644 index 00000000..a63a93af --- /dev/null +++ b/intro.txt @@ -0,0 +1,12 @@ +[bold magenta] + _____ _ _ _ _______ _____ +| __ \ | | | | | |/ / __ \| __ \ +| |__) |___ | |__ _ _ ___| |_ __ _ | ' /| |__) | |__) | +| _ // _ \| '_ \| | | / __| __/ _` | | < | _ /| _ / +| | \ \ (_) | |_) | |_| \__ \ || (_| | | . \| | \ \| | \ \ +|_| \_\___/|_.__/ \__,_|___/\__\__,_| |_|\_\_| \_\_| \_\ + + +Thanks for using Robusta KRR. If you have any questions or feedback, please feel free to reach out to us at +https://github.com/robusta-dev/krr/issues +[/bold magenta] \ No newline at end of file From 3e1f4ec19502fc1740836c47bb40e8f02ff39778 Mon Sep 17 00:00:00 2001 From: LeaveMyYard Date: Fri, 29 Mar 2024 12:13:13 +0200 Subject: [PATCH 066/137] Fix trail slash in prometheus url --- robusta_krr/core/models/config.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/robusta_krr/core/models/config.py b/robusta_krr/core/models/config.py index ac3b15ca..aaadaa81 100644 --- a/robusta_krr/core/models/config.py +++ b/robusta_krr/core/models/config.py @@ -80,10 +80,12 @@ def Formatter(self) -> formatters.FormatterFunc: def validate_prometheus_url(cls, v: Optional[str]): if v is None: return None - + if not v.startswith("https://") and not v.startswith("http://"): raise Exception("--prometheus-url must start with https:// or http://") - + + v = v.removesuffix("/") + return v @pd.validator("prometheus_other_headers", pre=True) From 61669790edf48c258ca5d904fc0bfc77a659646a Mon Sep 17 00:00:00 2001 From: Ben Foster Date: Fri, 29 Mar 2024 10:42:04 -0400 Subject: [PATCH 067/137] Add selectors to Prometheus discovery Adds labels to the promethues discovery selectors that follow the convention used by the prometheus-community chart --- .../prometheus/metrics_service/prometheus_metrics_service.py | 1 + 1 file changed, 1 insertion(+) diff --git a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py index e61ef4c8..2833496e 100644 --- a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py +++ b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py @@ -34,6 +34,7 @@ def find_metrics_url(self, *, api_client: Optional[ApiClient] = None) -> Optiona return super().find_url( selectors=[ + "app.kubernetes.io/name=prometheus,app.kubernetes.io/component=server", "app=kube-prometheus-stack-prometheus", "app=prometheus,component=server", "app=prometheus-server", From 1540859a030e1590a5d4b47e2293a142a0ae447e Mon Sep 17 00:00:00 2001 From: Arik Alon Date: Sat, 30 Mar 2024 00:30:17 +0300 Subject: [PATCH 068/137] Change selectors order - move the new selectors to be last, so existing behavior won't change for existing users, in case they have more than one match --- .../prometheus/metrics_service/prometheus_metrics_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py index 2833496e..ebcb8353 100644 --- a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py +++ b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py @@ -34,13 +34,13 @@ def find_metrics_url(self, *, api_client: Optional[ApiClient] = None) -> Optiona return super().find_url( selectors=[ - "app.kubernetes.io/name=prometheus,app.kubernetes.io/component=server", "app=kube-prometheus-stack-prometheus", "app=prometheus,component=server", "app=prometheus-server", "app=prometheus-operator-prometheus", "app=rancher-monitoring-prometheus", "app=prometheus-prometheus", + "app.kubernetes.io/name=prometheus,app.kubernetes.io/component=server", ] ) From caa564ce2371f3bc478cc1916169d97291ffeac6 Mon Sep 17 00:00:00 2001 From: Pavel Zhukov <33721692+LeaveMyYard@users.noreply.github.com> Date: Tue, 2 Apr 2024 14:56:13 +0300 Subject: [PATCH 069/137] Live introduction message (#239) * Live introduction * Add intro file into pyinstaller in CI/CD pipelines * Do not exit if file fallback failed for some reason * Make the intro message full colored * Check for a newer version from github releases * Rework intro to load the message from robusta API * Change log of new version error to debug * Add version query param to fetching intro message * Fix request arg * Remove stg from the intro link --- .github/workflows/build-on-release.yml | 1 + robusta_krr/core/runner.py | 35 ++++++++++++++++----- robusta_krr/utils/intro.py | 42 ++++++++++++++++++++++++++ robusta_krr/utils/logo.py | 11 ------- robusta_krr/utils/version.py | 23 ++++++++++++++ 5 files changed, 94 insertions(+), 18 deletions(-) create mode 100644 robusta_krr/utils/intro.py delete mode 100644 robusta_krr/utils/logo.py diff --git a/.github/workflows/build-on-release.yml b/.github/workflows/build-on-release.yml index 73f48bed..f3c3ae10 100644 --- a/.github/workflows/build-on-release.yml +++ b/.github/workflows/build-on-release.yml @@ -85,6 +85,7 @@ jobs: pyinstaller krr.py mkdir -p ./dist/krr/grapheme/data cp $(python -c "import grapheme; print(grapheme.__path__[0] + '/data/grapheme_break_property.json')") ./dist/krr/grapheme/data/grapheme_break_property.json + cp ./intro.txt ./dist/krr/intro.txt - name: Zip the application (Unix) if: matrix.os == 'macos-latest' || matrix.os == 'ubuntu-latest' diff --git a/robusta_krr/core/runner.py b/robusta_krr/core/runner.py index 25a85e86..3649b103 100644 --- a/robusta_krr/core/runner.py +++ b/robusta_krr/core/runner.py @@ -17,9 +17,9 @@ from robusta_krr.core.models.config import settings from robusta_krr.core.models.objects import K8sObjectData from robusta_krr.core.models.result import ResourceAllocations, ResourceScan, ResourceType, Result, StrategyData -from robusta_krr.utils.logo import ASCII_LOGO +from robusta_krr.utils.intro import load_intro_message from robusta_krr.utils.progress_bar import ProgressBar -from robusta_krr.utils.version import get_version +from robusta_krr.utils.version import get_version, load_latest_version logger = logging.getLogger("krr") @@ -68,14 +68,35 @@ def _get_prometheus_loader(self, cluster: Optional[str]) -> Optional[PrometheusM return result - def _greet(self) -> None: + @staticmethod + def __parse_version_string(version: str) -> tuple[int, ...]: + version_trimmed = version.replace("-dev", "").replace("v", "") + return tuple(map(int, version_trimmed.split("."))) + + def __check_newer_version_available(self, current_version: str, latest_version: str) -> bool: + try: + current_version_parsed = self.__parse_version_string(current_version) + latest_version_parsed = self.__parse_version_string(latest_version) + + if current_version_parsed < latest_version_parsed: + return True + except Exception: + logger.debug("An error occurred while checking for a new version", exc_info=True) + return False + + async def _greet(self) -> None: if settings.quiet: return - custom_print(ASCII_LOGO) - custom_print(f"Running Robusta's KRR (Kubernetes Resource Recommender) {get_version()}") + current_version = get_version() + intro_message, latest_version = await asyncio.gather(load_intro_message(), load_latest_version()) + + custom_print(intro_message) + custom_print(f"\nRunning Robusta's KRR (Kubernetes Resource Recommender) {current_version}") custom_print(f"Using strategy: {self._strategy}") custom_print(f"Using formatter: {settings.format}") + if latest_version is not None and self.__check_newer_version_available(current_version, latest_version): + custom_print(f"[yellow bold]A newer version of KRR is available: {latest_version}[/yellow bold]") custom_print("") def _process_result(self, result: Result) -> None: @@ -281,9 +302,9 @@ async def _collect_result(self) -> Result: ), ) - async def run(self) -> int: + async def run(self) -> None: """Run the Runner. The return value is the exit code of the program.""" - self._greet() + await self._greet() try: settings.load_kubeconfig() diff --git a/robusta_krr/utils/intro.py b/robusta_krr/utils/intro.py new file mode 100644 index 00000000..c231773d --- /dev/null +++ b/robusta_krr/utils/intro.py @@ -0,0 +1,42 @@ +import requests +import asyncio +from concurrent.futures import ThreadPoolExecutor + +from .version import get_version + + +ONLINE_LINK = 'https://api.robusta.dev/krr/intro' +LOCAL_LINK = './intro.txt' +TIMEOUT = 0.5 + + +# Synchronous function to fetch intro message +def fetch_intro_message() -> str: + try: + # Attempt to get the message from the URL + response = requests.get(ONLINE_LINK, params={"version": get_version()}, timeout=TIMEOUT) + response.raise_for_status() # Raises an error for bad responses + result = response.json() + return result['message'] + except Exception as e1: + # If there's any error, fallback to local file + try: + with open(LOCAL_LINK, 'r') as file: + return file.read() + except Exception as e2: + return ( + "[red]Failed to load the intro message.\n" + f"Both from the URL: {e1.__class__.__name__} {e1}\n" + f"and the local file: {e2.__class__.__name__} {e2}\n" + "But as that is not critical, KRR will continue to run without the intro message.[/red]" + ) + + +async def load_intro_message() -> str: + loop = asyncio.get_running_loop() + # Use a ThreadPoolExecutor to run the synchronous function in a separate thread + with ThreadPoolExecutor() as pool: + return await loop.run_in_executor(pool, fetch_intro_message) + + +__all__ = ['load_intro_message'] diff --git a/robusta_krr/utils/logo.py b/robusta_krr/utils/logo.py deleted file mode 100644 index 6beb0875..00000000 --- a/robusta_krr/utils/logo.py +++ /dev/null @@ -1,11 +0,0 @@ -ASCII_LOGO = r""" -[bold magenta] - _____ _ _ _ _______ _____ -| __ \ | | | | | |/ / __ \| __ \ -| |__) |___ | |__ _ _ ___| |_ __ _ | ' /| |__) | |__) | -| _ // _ \| '_ \| | | / __| __/ _` | | < | _ /| _ / -| | \ \ (_) | |_) | |_| \__ \ || (_| | | . \| | \ \| | \ \ -|_| \_\___/|_.__/ \__,_|___/\__\__,_| |_|\_\_| \_\_| \_\ -[/bold magenta] - -""" diff --git a/robusta_krr/utils/version.py b/robusta_krr/utils/version.py index 77772dd6..d7b5df7f 100644 --- a/robusta_krr/utils/version.py +++ b/robusta_krr/utils/version.py @@ -1,5 +1,28 @@ import robusta_krr +import requests +import asyncio +from typing import Optional +from concurrent.futures import ThreadPoolExecutor def get_version() -> str: return robusta_krr.__version__ + + +# Synchronous function to fetch the latest release version from GitHub API +def fetch_latest_version() -> Optional[str]: + url = "https://api.github.com/repos/robusta-dev/krr/releases/latest" + try: + response = requests.get(url, timeout=0.5) # 0.5 seconds timeout + response.raise_for_status() # Raises an error for bad responses + data = response.json() + return data.get("tag_name") # Returns the tag name of the latest release + except Exception: + return None + + +async def load_latest_version() -> Optional[str]: + loop = asyncio.get_running_loop() + # Run the synchronous function in a separate thread + with ThreadPoolExecutor() as pool: + return await loop.run_in_executor(pool, fetch_latest_version) From 3213021efa49d00cb8869da43c9775e2c34d6ecc Mon Sep 17 00:00:00 2001 From: LeaveMyYard Date: Tue, 2 Apr 2024 15:15:55 +0300 Subject: [PATCH 070/137] Make non-critical errors to be warnings --- robusta_krr/core/runner.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/robusta_krr/core/runner.py b/robusta_krr/core/runner.py index 3649b103..546dd013 100644 --- a/robusta_krr/core/runner.py +++ b/robusta_krr/core/runner.py @@ -208,7 +208,9 @@ async def _check_data_availability(self, cluster: Optional[str]) -> None: try: history_range = await prometheus_loader.get_history_range(timedelta(hours=5)) except ValueError: - logger.exception(f"Was not able to get history range for cluster {cluster}") + logger.warning( + f"Was not able to get history range for cluster {cluster}. This is not critical, will try continue." + ) self.errors.append( { "name": "HistoryRangeError", @@ -220,13 +222,13 @@ async def _check_data_availability(self, cluster: Optional[str]) -> None: enough_data = self._strategy.settings.history_range_enough(history_range) if not enough_data: - logger.error(f"Not enough history available for cluster {cluster}.") + logger.warning(f"Not enough history available for cluster {cluster}.") try_after = history_range[0] + self._strategy.settings.history_timedelta - logger.error( + logger.warning( "If the cluster is freshly installed, it might take some time for the enough data to be available." ) - logger.error( + logger.warning( f"Enough data is estimated to be available after {try_after}, " "but will try to calculate recommendations anyway." ) @@ -302,7 +304,7 @@ async def _collect_result(self) -> Result: ), ) - async def run(self) -> None: + async def run(self) -> int: """Run the Runner. The return value is the exit code of the program.""" await self._greet() @@ -311,7 +313,7 @@ async def run(self) -> None: except Exception as e: logger.error(f"Could not load kubernetes configuration: {e}") logger.error("Try to explicitly set --context and/or --kubeconfig flags.") - return + return 1 # Exit with error try: # eks has a lower step limit than other types of prometheus, it will throw an error From a005d7ef94e92e5651187586466812803f7ed91f Mon Sep 17 00:00:00 2001 From: LeaveMyYard Date: Tue, 2 Apr 2024 17:34:49 +0300 Subject: [PATCH 071/137] Hotfix for error when running in cluster --- robusta_krr/core/models/config.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/robusta_krr/core/models/config.py b/robusta_krr/core/models/config.py index aaadaa81..d4f04a0e 100644 --- a/robusta_krr/core/models/config.py +++ b/robusta_krr/core/models/config.py @@ -146,6 +146,9 @@ def load_kubeconfig(self) -> None: self.inside_cluster = True def get_kube_client(self, context: Optional[str] = None): + if context is None: + return None + api_client = config.new_client_from_config(context=context, config_file=self.kubeconfig) if self.impersonate_user is not None: # trick copied from https://github.com/kubernetes-client/python/issues/362 From d65cf3d422c825af4f29a5705706c64614ab7820 Mon Sep 17 00:00:00 2001 From: shlomosfez <143969671+shlomosfez@users.noreply.github.com> Date: Tue, 2 Apr 2024 18:14:23 +0300 Subject: [PATCH 072/137] Load kubeconfig first in case it is provided by CLI arg (#251) * Load kubeconfig first in case it is provided by CLI arg * simplify implementation --------- Co-authored-by: Pavel Zhukov <33721692+LeaveMyYard@users.noreply.github.com> --- robusta_krr/core/models/config.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/robusta_krr/core/models/config.py b/robusta_krr/core/models/config.py index d4f04a0e..ff6142a6 100644 --- a/robusta_krr/core/models/config.py +++ b/robusta_krr/core/models/config.py @@ -138,11 +138,10 @@ def logging_console(self) -> Console: def load_kubeconfig(self) -> None: try: - config.load_incluster_config() - except ConfigException: config.load_kube_config(config_file=self.kubeconfig, context=self.context) self.inside_cluster = False - else: + except ConfigException: + config.load_incluster_config() self.inside_cluster = True def get_kube_client(self, context: Optional[str] = None): From c3dccb91fe5907a42a285b34443591d28cb344fc Mon Sep 17 00:00:00 2001 From: FrankFoerster24 <108520305+FrankFoerster24@users.noreply.github.com> Date: Tue, 2 Apr 2024 18:35:33 +0200 Subject: [PATCH 073/137] Add Mimir support (#168) * Add Mimir support * Update url postfix for Mimir * Fix Mimir-specific headers --------- Co-authored-by: Pavel Zhukov <33721692+LeaveMyYard@users.noreply.github.com> Co-authored-by: LeaveMyYard --- .../core/integrations/prometheus/loader.py | 4 +- .../metrics_service/mimir_metrics_service.py | 47 +++++++++++++++++++ .../prometheus_metrics_service.py | 5 ++ 3 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 robusta_krr/core/integrations/prometheus/metrics_service/mimir_metrics_service.py diff --git a/robusta_krr/core/integrations/prometheus/loader.py b/robusta_krr/core/integrations/prometheus/loader.py index 82c7d746..db493927 100644 --- a/robusta_krr/core/integrations/prometheus/loader.py +++ b/robusta_krr/core/integrations/prometheus/loader.py @@ -16,13 +16,13 @@ from .metrics_service.prometheus_metrics_service import PrometheusMetricsService from .metrics_service.thanos_metrics_service import ThanosMetricsService from .metrics_service.victoria_metrics_service import VictoriaMetricsService +from .metrics_service.mimir_metrics_service import MimirMetricsService if TYPE_CHECKING: from robusta_krr.core.abstract.strategies import BaseStrategy, MetricsPodData logger = logging.getLogger("krr") - class PrometheusMetricsLoader: def __init__(self, *, cluster: Optional[str] = None) -> None: """ @@ -56,7 +56,7 @@ def get_metrics_service( metrics_to_check = [PrometheusMetricsService] else: logger.info("No Prometheus URL is specified, trying to auto-detect a metrics service") - metrics_to_check = [VictoriaMetricsService, ThanosMetricsService, PrometheusMetricsService] + metrics_to_check = [VictoriaMetricsService, ThanosMetricsService, MimirMetricsService, PrometheusMetricsService] for metric_service_class in metrics_to_check: service_name = metric_service_class.name() diff --git a/robusta_krr/core/integrations/prometheus/metrics_service/mimir_metrics_service.py b/robusta_krr/core/integrations/prometheus/metrics_service/mimir_metrics_service.py new file mode 100644 index 00000000..ea3af57c --- /dev/null +++ b/robusta_krr/core/integrations/prometheus/metrics_service/mimir_metrics_service.py @@ -0,0 +1,47 @@ +from typing import Optional + +from kubernetes.client import ApiClient +from prometrix import MetricsNotFound + +from robusta_krr.utils.service_discovery import MetricsServiceDiscovery + +from .prometheus_metrics_service import PrometheusMetricsService + +class MimirMetricsDiscovery(MetricsServiceDiscovery): + def find_metrics_url(self, *, api_client: Optional[ApiClient] = None) -> Optional[str]: + """ + Finds the Mimir Metrics URL using selectors. + Args: + api_client (Optional[ApiClient]): A Kubernetes API client. Defaults to None. + Returns: + Optional[str]: The discovered Mimir Metrics URL, or None if not found. + """ + return super().find_url( + selectors=[ + "app.kubernetes.io/name=mimir,app.kubernetes.io/component=query-frontend", + ] + ) + + +class MimirMetricsService(PrometheusMetricsService): + """ + A class for fetching metrics from Mimir Metrics. + """ + + service_discovery = MimirMetricsDiscovery + url_postfix = "/prometheus" + additional_headers = {"X-Scope-OrgID": "anonymous"} + + def check_connection(self): + """ + Checks the connection to Prometheus. + Raises: + MimirMetricsNotFound: If the connection to Mimir Metrics cannot be established. + """ + try: + super().check_connection() + except MetricsNotFound as e: + # This is to clarify which metrics service had the issue and not say its a prometheus issue + raise MetricsNotFound( + f"Couldn't connect to Mimir Metrics found under {self.prometheus.url}\nCaused by {e.__class__.__name__}: {e})" + ) from e diff --git a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py index ebcb8353..423a29ad 100644 --- a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py +++ b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py @@ -51,6 +51,8 @@ class PrometheusMetricsService(MetricsService): """ service_discovery: type[MetricsServiceDiscovery] = PrometheusDiscovery + url_postfix: str = "" + additional_headers: dict[str, str] = {} def __init__( self, @@ -86,9 +88,12 @@ def __init__( f"{self.name()} instance could not be found while scanning in {self.cluster} cluster." ) + self.url += self.url_postfix + logger.info(f"Using {self.name()} at {self.url} for cluster {cluster or 'default'}") headers = settings.prometheus_other_headers + headers |= self.additional_headers if self.auth_header: headers |= {"Authorization": self.auth_header} From 1e7f0aa749f38dee4f81f5ecb4d859a866a4e80f Mon Sep 17 00:00:00 2001 From: LeaveMyYard Date: Tue, 2 Apr 2024 19:36:21 +0300 Subject: [PATCH 074/137] Update dev version --- pyproject.toml | 2 +- robusta_krr/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a9b433cd..a5eba8f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "robusta-krr" -version = "1.7.0-dev" +version = "1.8.2-dev" description = "Robusta's Resource Recommendation engine for Kubernetes" authors = ["Pavel Zhukov <33721692+LeaveMyYard@users.noreply.github.com>"] license = "MIT" diff --git a/robusta_krr/__init__.py b/robusta_krr/__init__.py index 3901edc7..4866168b 100644 --- a/robusta_krr/__init__.py +++ b/robusta_krr/__init__.py @@ -1,4 +1,4 @@ from .main import run -__version__ = "1.7.0-dev" +__version__ = "1.8.2-dev" __all__ = ["run", "__version__"] From 76ed5537567e37a07c691cbb6c2eea9932fcb7c9 Mon Sep 17 00:00:00 2001 From: Pavel Zhukov <33721692+LeaveMyYard@users.noreply.github.com> Date: Thu, 4 Apr 2024 13:41:07 +0300 Subject: [PATCH 075/137] Refactor k8s workloads streaming (#256) * Refactor k8s workloads streaming * Fix tests --- .../core/integrations/kubernetes/__init__.py | 97 +++++++++---------- robusta_krr/core/runner.py | 8 +- robusta_krr/utils/async_gen_merge.py | 39 -------- tests/conftest.py | 13 +-- 4 files changed, 51 insertions(+), 106 deletions(-) delete mode 100644 robusta_krr/utils/async_gen_merge.py diff --git a/robusta_krr/core/integrations/kubernetes/__init__.py b/robusta_krr/core/integrations/kubernetes/__init__.py index 335b47af..a772a5c2 100644 --- a/robusta_krr/core/integrations/kubernetes/__init__.py +++ b/robusta_krr/core/integrations/kubernetes/__init__.py @@ -2,7 +2,7 @@ import logging from collections import defaultdict from concurrent.futures import ThreadPoolExecutor -from typing import Any, AsyncGenerator, AsyncIterable, Awaitable, Callable, Iterable, Optional, Union +from typing import Any, Awaitable, Callable, Iterable, Optional, Union from kubernetes import client, config # type: ignore from kubernetes.client import ApiException @@ -20,7 +20,6 @@ from robusta_krr.core.models.config import settings from robusta_krr.core.models.objects import HPAData, K8sObjectData, KindLiteral, PodData from robusta_krr.core.models.result import ResourceAllocations -from robusta_krr.utils.async_gen_merge import async_gen_merge from robusta_krr.utils.object_like_dict import ObjectLikeDict from . import config_patch as _ @@ -49,7 +48,7 @@ def __init__(self, cluster: Optional[str]=None): self.__jobs_for_cronjobs: dict[str, list[V1Job]] = {} self.__jobs_loading_locks: defaultdict[str, asyncio.Lock] = defaultdict(asyncio.Lock) - async def list_scannable_objects(self) -> AsyncGenerator[K8sObjectData, None]: + async def list_scannable_objects(self) -> list[K8sObjectData]: """List all scannable objects. Returns: @@ -61,10 +60,7 @@ async def list_scannable_objects(self) -> AsyncGenerator[K8sObjectData, None]: logger.debug(f"Resources: {settings.resources}") self.__hpa_list = await self._try_list_hpa() - - # https://stackoverflow.com/questions/55299564/join-multiple-async-generators-in-python - # This will merge all the streams from all the cluster loaders into a single stream - async for object in async_gen_merge( + workload_object_lists = await asyncio.gather( self._list_deployments(), self._list_rollouts(), self._list_deploymentconfig(), @@ -72,11 +68,15 @@ async def list_scannable_objects(self) -> AsyncGenerator[K8sObjectData, None]: self._list_all_daemon_set(), self._list_all_jobs(), self._list_all_cronjobs(), - ): + ) + + return [ + object + for workload_objects in workload_object_lists + for object in workload_objects # NOTE: By default we will filter out kube-system namespace - if settings.namespaces == "*" and object.namespace == "kube-system": - continue - yield object + if not (settings.namespaces == "*" and object.namespace == "kube-system") + ] async def _list_jobs_for_cronjobs(self, namespace: str) -> list[V1Job]: if namespace not in self.__jobs_for_cronjobs: @@ -185,12 +185,12 @@ async def _list_namespaced_or_global_objects( kind: KindLiteral, all_namespaces_request: Callable, namespaced_request: Callable - ) -> AsyncIterable[Any]: + ) -> list[Any]: logger.debug(f"Listing {kind}s in {self.cluster}") loop = asyncio.get_running_loop() if settings.namespaces == "*": - tasks = [ + requests = [ loop.run_in_executor( self.executor, lambda: all_namespaces_request( @@ -200,7 +200,7 @@ async def _list_namespaced_or_global_objects( ) ] else: - tasks = [ + requests = [ loop.run_in_executor( self.executor, lambda ns=namespace: namespaced_request( @@ -212,14 +212,14 @@ async def _list_namespaced_or_global_objects( for namespace in settings.namespaces ] - total_items = 0 - for task in asyncio.as_completed(tasks): - ret_single = await task - total_items += len(ret_single.items) - for item in ret_single.items: - yield item + result = [ + item + for request_result in await asyncio.gather(*requests) + for item in request_result.items + ] - logger.debug(f"Found {total_items} {kind} in {self.cluster}") + logger.debug(f"Found {len(result)} {kind} in {self.cluster}") + return result async def _list_scannable_objects( self, @@ -228,16 +228,17 @@ async def _list_scannable_objects( namespaced_request: Callable, extract_containers: Callable[[Any], Union[Iterable[V1Container], Awaitable[Iterable[V1Container]]]], filter_workflows: Optional[Callable[[Any], bool]] = None, - ) -> AsyncIterable[K8sObjectData]: + ) -> list[K8sObjectData]: if not self._should_list_resource(kind): logger.debug(f"Skipping {kind}s in {self.cluster}") return if not self.__kind_available[kind]: return - + + result = [] try: - async for item in self._list_namespaced_or_global_objects(kind, all_namespaces_request, namespaced_request): + for item in await self._list_namespaced_or_global_objects(kind, all_namespaces_request, namespaced_request): if filter_workflows is not None and not filter_workflows(item): continue @@ -245,8 +246,7 @@ async def _list_scannable_objects( if asyncio.iscoroutine(containers): containers = await containers - for container in containers: - yield self.__build_scannable_object(item, container, kind) + result.extend(self.__build_scannable_object(item, container, kind) for container in containers) except ApiException as e: if kind in ("Rollout", "DeploymentConfig") and e.status in [400, 401, 403, 404]: if self.__kind_available[kind]: @@ -256,7 +256,9 @@ async def _list_scannable_objects( logger.exception(f"Error {e.status} listing {kind} in cluster {self.cluster}: {e.reason}") logger.error("Will skip this object type and continue.") - def _list_deployments(self) -> AsyncIterable[K8sObjectData]: + return result + + def _list_deployments(self) -> list[K8sObjectData]: return self._list_scannable_objects( kind="Deployment", all_namespaces_request=self.apps.list_deployment_for_all_namespaces, @@ -264,7 +266,7 @@ def _list_deployments(self) -> AsyncIterable[K8sObjectData]: extract_containers=lambda item: item.spec.template.spec.containers, ) - def _list_rollouts(self) -> AsyncIterable[K8sObjectData]: + def _list_rollouts(self) -> list[K8sObjectData]: async def _extract_containers(item: Any) -> list[V1Container]: if item.spec.template is not None: return item.spec.template.spec.containers @@ -311,7 +313,7 @@ async def _extract_containers(item: Any) -> list[V1Container]: extract_containers=_extract_containers, ) - def _list_deploymentconfig(self) -> AsyncIterable[K8sObjectData]: + def _list_deploymentconfig(self) -> list[K8sObjectData]: # NOTE: Using custom objects API returns dicts, but all other APIs return objects # We need to handle this difference using a small wrapper return self._list_scannable_objects( @@ -335,7 +337,7 @@ def _list_deploymentconfig(self) -> AsyncIterable[K8sObjectData]: extract_containers=lambda item: item.spec.template.spec.containers, ) - def _list_all_statefulsets(self) -> AsyncIterable[K8sObjectData]: + def _list_all_statefulsets(self) -> list[K8sObjectData]: return self._list_scannable_objects( kind="StatefulSet", all_namespaces_request=self.apps.list_stateful_set_for_all_namespaces, @@ -343,7 +345,7 @@ def _list_all_statefulsets(self) -> AsyncIterable[K8sObjectData]: extract_containers=lambda item: item.spec.template.spec.containers, ) - def _list_all_daemon_set(self) -> AsyncIterable[K8sObjectData]: + def _list_all_daemon_set(self) -> list[K8sObjectData]: return self._list_scannable_objects( kind="DaemonSet", all_namespaces_request=self.apps.list_daemon_set_for_all_namespaces, @@ -351,7 +353,7 @@ def _list_all_daemon_set(self) -> AsyncIterable[K8sObjectData]: extract_containers=lambda item: item.spec.template.spec.containers, ) - def _list_all_jobs(self) -> AsyncIterable[K8sObjectData]: + def _list_all_jobs(self) -> list[K8sObjectData]: return self._list_scannable_objects( kind="Job", all_namespaces_request=self.batch.list_job_for_all_namespaces, @@ -363,7 +365,7 @@ def _list_all_jobs(self) -> AsyncIterable[K8sObjectData]: ), ) - def _list_all_cronjobs(self) -> AsyncIterable[K8sObjectData]: + def _list_all_cronjobs(self) -> list[K8sObjectData]: return self._list_scannable_objects( kind="CronJob", all_namespaces_request=self.batch.list_cron_job_for_all_namespaces, @@ -398,14 +400,10 @@ async def __list_hpa_v1(self) -> dict[HPAKey, HPAData]: } async def __list_hpa_v2(self) -> dict[HPAKey, HPAData]: - loop = asyncio.get_running_loop() - res = await loop.run_in_executor( - self.executor, - lambda: self._list_namespaced_or_global_objects( - kind="HPA-v2", - all_namespaces_request=self.autoscaling_v2.list_horizontal_pod_autoscaler_for_all_namespaces, - namespaced_request=self.autoscaling_v2.list_namespaced_horizontal_pod_autoscaler, - ), + res = await self._list_namespaced_or_global_objects( + kind="HPA-v2", + all_namespaces_request=self.autoscaling_v2.list_horizontal_pod_autoscaler_for_all_namespaces, + namespaced_request=self.autoscaling_v2.list_namespaced_horizontal_pod_autoscaler, ) def __get_metric(hpa: V2HorizontalPodAutoscaler, metric_name: str) -> Optional[float]: return next( @@ -429,7 +427,7 @@ def __get_metric(hpa: V2HorizontalPodAutoscaler, metric_name: str) -> Optional[f target_cpu_utilization_percentage=__get_metric(hpa, "cpu"), target_memory_utilization_percentage=__get_metric(hpa, "memory"), ) - async for hpa in res + for hpa in res } # TODO: What should we do in case of other metrics bound to the HPA? @@ -514,7 +512,7 @@ def _try_create_cluster_loader(self, cluster: Optional[str]) -> Optional[Cluster logger.error(f"Could not load cluster {cluster} and will skip it: {e}") return None - async def list_scannable_objects(self, clusters: Optional[list[str]]) -> AsyncIterable[K8sObjectData]: + async def list_scannable_objects(self, clusters: Optional[list[str]]) -> list[K8sObjectData]: """List all scannable objects. Yields: @@ -529,13 +527,12 @@ async def list_scannable_objects(self, clusters: Optional[list[str]]) -> AsyncIt if self.cluster_loaders == {}: logger.error("Could not load any cluster.") return - - # https://stackoverflow.com/questions/55299564/join-multiple-async-generators-in-python - # This will merge all the streams from all the cluster loaders into a single stream - async for object in async_gen_merge( - *[cluster_loader.list_scannable_objects() for cluster_loader in self.cluster_loaders.values()] - ): - yield object + + return [ + object + for cluster_loader in self.cluster_loaders.values() + for object in await cluster_loader.list_scannable_objects() + ] async def load_pods(self, object: K8sObjectData) -> list[PodData]: try: diff --git a/robusta_krr/core/runner.py b/robusta_krr/core/runner.py index 546dd013..8e08521c 100644 --- a/robusta_krr/core/runner.py +++ b/robusta_krr/core/runner.py @@ -275,12 +275,8 @@ async def _collect_result(self) -> Result: await asyncio.gather(*[self._check_data_availability(cluster) for cluster in clusters]) with ProgressBar(title="Calculating Recommendation") as self.__progressbar: - scans_tasks = [ - asyncio.create_task(self._gather_object_allocations(k8s_object)) - async for k8s_object in self._k8s_loader.list_scannable_objects(clusters) - ] - - scans = await asyncio.gather(*scans_tasks) + workloads = await self._k8s_loader.list_scannable_objects(clusters) + scans = await asyncio.gather(*[self._gather_object_allocations(k8s_object) for k8s_object in workloads]) successful_scans = [scan for scan in scans if scan is not None] diff --git a/robusta_krr/utils/async_gen_merge.py b/robusta_krr/utils/async_gen_merge.py deleted file mode 100644 index 35c2c866..00000000 --- a/robusta_krr/utils/async_gen_merge.py +++ /dev/null @@ -1,39 +0,0 @@ -import asyncio -import logging -from typing import AsyncIterable, TypeVar - - -logger = logging.getLogger("krr") - - -# Define a type variable for the values yielded by the async generators -T = TypeVar("T") - - -def async_gen_merge(*aiters: AsyncIterable[T]) -> AsyncIterable[T]: - queue = asyncio.Queue() - iters_remaining = set(aiters) - - async def drain(aiter): - try: - async for item in aiter: - await queue.put(item) - except Exception: - logger.exception(f"Error in async generator {aiter}") - finally: - iters_remaining.discard(aiter) - await queue.put(None) - - async def merged(): - while iters_remaining or not queue.empty(): - item = await queue.get() - - if item is None: - continue - - yield item - - for aiter in aiters: - asyncio.create_task(drain(aiter)) - - return merged() diff --git a/tests/conftest.py b/tests/conftest.py index 61c389dd..b1d8d228 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,6 @@ import random from datetime import datetime, timedelta -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import AsyncMock, patch import numpy as np import pytest @@ -26,15 +26,6 @@ ) -class AsyncIter: - def __init__(self, items): - self.items = items - - async def __aiter__(self): - for item in self.items: - yield item - - @pytest.fixture(autouse=True, scope="session") def mock_list_clusters(): with patch( @@ -48,7 +39,7 @@ def mock_list_clusters(): def mock_list_scannable_objects(): with patch( "robusta_krr.core.integrations.kubernetes.KubernetesLoader.list_scannable_objects", - new=MagicMock(return_value=AsyncIter([TEST_OBJECT])), + new=AsyncMock(return_value=[TEST_OBJECT]), ): yield From 7fc9aa71bce4e525d78060171209395e87cfb709 Mon Sep 17 00:00:00 2001 From: dgdevops Date: Tue, 9 Apr 2024 09:22:10 +0200 Subject: [PATCH 076/137] Updated KRR requirements in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 51fea85b..6dfafc29 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ Read more about [how KRR works](#how-krr-works) and [KRR vs Kubernetes VPA](#dif ### Requirements -KRR requires Prometheus 2.26+ and [kube-state-metrics](https://github.com/kubernetes/kube-state-metrics). +KRR requires Prometheus 2.26+, [kube-state-metrics](https://github.com/kubernetes/kube-state-metrics) & [cAdvisor](https://github.com/google/cadvisor).
Which metrics does KRR need? From 5695c6633b206fb17755b33424bf740fb8cc2cb8 Mon Sep 17 00:00:00 2001 From: Tomer Date: Tue, 9 Apr 2024 15:28:52 +0300 Subject: [PATCH 077/137] upgrade prometrix and other dependencies (#259) * upgrade prometrix and other dependencies * reverted version --- poetry.lock | 257 +++++++++++++++++++++-------------------------- pyproject.toml | 5 +- requirements.txt | 16 +-- 3 files changed, 126 insertions(+), 152 deletions(-) diff --git a/poetry.lock b/poetry.lock index f0db2614..b3498272 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1052,79 +1052,80 @@ files = [ [[package]] name = "pillow" -version = "10.2.0" +version = "10.3.0" description = "Python Imaging Library (Fork)" optional = false python-versions = ">=3.8" files = [ - {file = "pillow-10.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:7823bdd049099efa16e4246bdf15e5a13dbb18a51b68fa06d6c1d4d8b99a796e"}, - {file = "pillow-10.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:83b2021f2ade7d1ed556bc50a399127d7fb245e725aa0113ebd05cfe88aaf588"}, - {file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fad5ff2f13d69b7e74ce5b4ecd12cc0ec530fcee76356cac6742785ff71c452"}, - {file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da2b52b37dad6d9ec64e653637a096905b258d2fc2b984c41ae7d08b938a67e4"}, - {file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:47c0995fc4e7f79b5cfcab1fc437ff2890b770440f7696a3ba065ee0fd496563"}, - {file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:322bdf3c9b556e9ffb18f93462e5f749d3444ce081290352c6070d014c93feb2"}, - {file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:51f1a1bffc50e2e9492e87d8e09a17c5eea8409cda8d3f277eb6edc82813c17c"}, - {file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69ffdd6120a4737710a9eee73e1d2e37db89b620f702754b8f6e62594471dee0"}, - {file = "pillow-10.2.0-cp310-cp310-win32.whl", hash = "sha256:c6dafac9e0f2b3c78df97e79af707cdc5ef8e88208d686a4847bab8266870023"}, - {file = "pillow-10.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:aebb6044806f2e16ecc07b2a2637ee1ef67a11840a66752751714a0d924adf72"}, - {file = "pillow-10.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:7049e301399273a0136ff39b84c3678e314f2158f50f517bc50285fb5ec847ad"}, - {file = "pillow-10.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35bb52c37f256f662abdfa49d2dfa6ce5d93281d323a9af377a120e89a9eafb5"}, - {file = "pillow-10.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c23f307202661071d94b5e384e1e1dc7dfb972a28a2310e4ee16103e66ddb67"}, - {file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:773efe0603db30c281521a7c0214cad7836c03b8ccff897beae9b47c0b657d61"}, - {file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11fa2e5984b949b0dd6d7a94d967743d87c577ff0b83392f17cb3990d0d2fd6e"}, - {file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:716d30ed977be8b37d3ef185fecb9e5a1d62d110dfbdcd1e2a122ab46fddb03f"}, - {file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a086c2af425c5f62a65e12fbf385f7c9fcb8f107d0849dba5839461a129cf311"}, - {file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c8de2789052ed501dd829e9cae8d3dcce7acb4777ea4a479c14521c942d395b1"}, - {file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:609448742444d9290fd687940ac0b57fb35e6fd92bdb65386e08e99af60bf757"}, - {file = "pillow-10.2.0-cp311-cp311-win32.whl", hash = "sha256:823ef7a27cf86df6597fa0671066c1b596f69eba53efa3d1e1cb8b30f3533068"}, - {file = "pillow-10.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:1da3b2703afd040cf65ec97efea81cfba59cdbed9c11d8efc5ab09df9509fc56"}, - {file = "pillow-10.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:edca80cbfb2b68d7b56930b84a0e45ae1694aeba0541f798e908a49d66b837f1"}, - {file = "pillow-10.2.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:1b5e1b74d1bd1b78bc3477528919414874748dd363e6272efd5abf7654e68bef"}, - {file = "pillow-10.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0eae2073305f451d8ecacb5474997c08569fb4eb4ac231ffa4ad7d342fdc25ac"}, - {file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7c2286c23cd350b80d2fc9d424fc797575fb16f854b831d16fd47ceec078f2c"}, - {file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e23412b5c41e58cec602f1135c57dfcf15482013ce6e5f093a86db69646a5aa"}, - {file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:52a50aa3fb3acb9cf7213573ef55d31d6eca37f5709c69e6858fe3bc04a5c2a2"}, - {file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:127cee571038f252a552760076407f9cff79761c3d436a12af6000cd182a9d04"}, - {file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8d12251f02d69d8310b046e82572ed486685c38f02176bd08baf216746eb947f"}, - {file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:54f1852cd531aa981bc0965b7d609f5f6cc8ce8c41b1139f6ed6b3c54ab82bfb"}, - {file = "pillow-10.2.0-cp312-cp312-win32.whl", hash = "sha256:257d8788df5ca62c980314053197f4d46eefedf4e6175bc9412f14412ec4ea2f"}, - {file = "pillow-10.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:154e939c5f0053a383de4fd3d3da48d9427a7e985f58af8e94d0b3c9fcfcf4f9"}, - {file = "pillow-10.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:f379abd2f1e3dddb2b61bc67977a6b5a0a3f7485538bcc6f39ec76163891ee48"}, - {file = "pillow-10.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8373c6c251f7ef8bda6675dd6d2b3a0fcc31edf1201266b5cf608b62a37407f9"}, - {file = "pillow-10.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:870ea1ada0899fd0b79643990809323b389d4d1d46c192f97342eeb6ee0b8483"}, - {file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4b6b1e20608493548b1f32bce8cca185bf0480983890403d3b8753e44077129"}, - {file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3031709084b6e7852d00479fd1d310b07d0ba82765f973b543c8af5061cf990e"}, - {file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:3ff074fc97dd4e80543a3e91f69d58889baf2002b6be64347ea8cf5533188213"}, - {file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:cb4c38abeef13c61d6916f264d4845fab99d7b711be96c326b84df9e3e0ff62d"}, - {file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b1b3020d90c2d8e1dae29cf3ce54f8094f7938460fb5ce8bc5c01450b01fbaf6"}, - {file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:170aeb00224ab3dc54230c797f8404507240dd868cf52066f66a41b33169bdbe"}, - {file = "pillow-10.2.0-cp38-cp38-win32.whl", hash = "sha256:c4225f5220f46b2fde568c74fca27ae9771536c2e29d7c04f4fb62c83275ac4e"}, - {file = "pillow-10.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:0689b5a8c5288bc0504d9fcee48f61a6a586b9b98514d7d29b840143d6734f39"}, - {file = "pillow-10.2.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b792a349405fbc0163190fde0dc7b3fef3c9268292586cf5645598b48e63dc67"}, - {file = "pillow-10.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c570f24be1e468e3f0ce7ef56a89a60f0e05b30a3669a459e419c6eac2c35364"}, - {file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8ecd059fdaf60c1963c58ceb8997b32e9dc1b911f5da5307aab614f1ce5c2fb"}, - {file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c365fd1703040de1ec284b176d6af5abe21b427cb3a5ff68e0759e1e313a5e7e"}, - {file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:70c61d4c475835a19b3a5aa42492409878bbca7438554a1f89d20d58a7c75c01"}, - {file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b6f491cdf80ae540738859d9766783e3b3c8e5bd37f5dfa0b76abdecc5081f13"}, - {file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d189550615b4948f45252d7f005e53c2040cea1af5b60d6f79491a6e147eef7"}, - {file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:49d9ba1ed0ef3e061088cd1e7538a0759aab559e2e0a80a36f9fd9d8c0c21591"}, - {file = "pillow-10.2.0-cp39-cp39-win32.whl", hash = "sha256:babf5acfede515f176833ed6028754cbcd0d206f7f614ea3447d67c33be12516"}, - {file = "pillow-10.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:0304004f8067386b477d20a518b50f3fa658a28d44e4116970abfcd94fac34a8"}, - {file = "pillow-10.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:0fb3e7fc88a14eacd303e90481ad983fd5b69c761e9e6ef94c983f91025da869"}, - {file = "pillow-10.2.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:322209c642aabdd6207517e9739c704dc9f9db943015535783239022002f054a"}, - {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eedd52442c0a5ff4f887fab0c1c0bb164d8635b32c894bc1faf4c618dd89df2"}, - {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb28c753fd5eb3dd859b4ee95de66cc62af91bcff5db5f2571d32a520baf1f04"}, - {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:33870dc4653c5017bf4c8873e5488d8f8d5f8935e2f1fb9a2208c47cdd66efd2"}, - {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3c31822339516fb3c82d03f30e22b1d038da87ef27b6a78c9549888f8ceda39a"}, - {file = "pillow-10.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a2b56ba36e05f973d450582fb015594aaa78834fefe8dfb8fcd79b93e64ba4c6"}, - {file = "pillow-10.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d8e6aeb9201e655354b3ad049cb77d19813ad4ece0df1249d3c793de3774f8c7"}, - {file = "pillow-10.2.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:2247178effb34a77c11c0e8ac355c7a741ceca0a732b27bf11e747bbc950722f"}, - {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15587643b9e5eb26c48e49a7b33659790d28f190fc514a322d55da2fb5c2950e"}, - {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753cd8f2086b2b80180d9b3010dd4ed147efc167c90d3bf593fe2af21265e5a5"}, - {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7c8f97e8e7a9009bcacbe3766a36175056c12f9a44e6e6f2d5caad06dcfbf03b"}, - {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d1b35bcd6c5543b9cb547dee3150c93008f8dd0f1fef78fc0cd2b141c5baf58a"}, - {file = "pillow-10.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fe4c15f6c9285dc54ce6553a3ce908ed37c8f3825b5a51a15c91442bb955b868"}, - {file = "pillow-10.2.0.tar.gz", hash = "sha256:e87f0b2c78157e12d7686b27d63c070fd65d994e8ddae6f328e0dcf4a0cd007e"}, + {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] @@ -1186,20 +1187,23 @@ requests = "*" [[package]] name = "prometrix" -version = "0.1.16" +version = "0.1.17" description = "A Python Prometheus client for all Prometheus instances." optional = false -python-versions = ">=3.8,<4.0" +python-versions = "<4.0,>=3.8" files = [ - {file = "prometrix-0.1.16-py3-none-any.whl", hash = "sha256:373a6c652ee6381b3f3ab8c332ce56cc6acfa14b58085db52d46c1a0be659f95"}, - {file = "prometrix-0.1.16.tar.gz", hash = "sha256:ef15cf00181e1ff7c734685f364e6a076343c68ca8010f12f5c9cce6422eb8a2"}, + {file = "prometrix-0.1.17-py3-none-any.whl", hash = "sha256:91f8484916addf657e77bf68af9c622b3f2cb7d15dc68de96cd60b43ae0c3c64"}, + {file = "prometrix-0.1.17.tar.gz", hash = "sha256:aa1fab72024b3cad6f233fd45d69cec5c1bbc9ca534383ecfe11c17df8b0b298"}, ] [package.dependencies] boto3 = ">=1.28.15,<2.0.0" botocore = ">=1.31.15,<2.0.0" +fonttools = ">=4.43.0,<5.0.0" +pillow = ">=10.3.0,<11.0.0" prometheus-api-client = ">=0.5.3,<0.6.0" pydantic = ">=1.8.1,<2.0.0" +urllib3 = ">=1.26.18,<2.0.0" [[package]] name = "pyasn1" @@ -1239,47 +1243,47 @@ files = [ [[package]] name = "pydantic" -version = "1.10.7" +version = "1.10.15" description = "Data validation and settings management using python type hints" optional = false python-versions = ">=3.7" files = [ - {file = "pydantic-1.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e79e999e539872e903767c417c897e729e015872040e56b96e67968c3b918b2d"}, - {file = "pydantic-1.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:01aea3a42c13f2602b7ecbbea484a98169fb568ebd9e247593ea05f01b884b2e"}, - {file = "pydantic-1.10.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:516f1ed9bc2406a0467dd777afc636c7091d71f214d5e413d64fef45174cfc7a"}, - {file = "pydantic-1.10.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae150a63564929c675d7f2303008d88426a0add46efd76c3fc797cd71cb1b46f"}, - {file = "pydantic-1.10.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ecbbc51391248116c0a055899e6c3e7ffbb11fb5e2a4cd6f2d0b93272118a209"}, - {file = "pydantic-1.10.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f4a2b50e2b03d5776e7f21af73e2070e1b5c0d0df255a827e7c632962f8315af"}, - {file = "pydantic-1.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:a7cd2251439988b413cb0a985c4ed82b6c6aac382dbaff53ae03c4b23a70e80a"}, - {file = "pydantic-1.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:68792151e174a4aa9e9fc1b4e653e65a354a2fa0fed169f7b3d09902ad2cb6f1"}, - {file = "pydantic-1.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe2507b8ef209da71b6fb5f4e597b50c5a34b78d7e857c4f8f3115effaef5fe"}, - {file = "pydantic-1.10.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10a86d8c8db68086f1e30a530f7d5f83eb0685e632e411dbbcf2d5c0150e8dcd"}, - {file = "pydantic-1.10.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75ae19d2a3dbb146b6f324031c24f8a3f52ff5d6a9f22f0683694b3afcb16fb"}, - {file = "pydantic-1.10.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:464855a7ff7f2cc2cf537ecc421291b9132aa9c79aef44e917ad711b4a93163b"}, - {file = "pydantic-1.10.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:193924c563fae6ddcb71d3f06fa153866423ac1b793a47936656e806b64e24ca"}, - {file = "pydantic-1.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:b4a849d10f211389502059c33332e91327bc154acc1845f375a99eca3afa802d"}, - {file = "pydantic-1.10.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cc1dde4e50a5fc1336ee0581c1612215bc64ed6d28d2c7c6f25d2fe3e7c3e918"}, - {file = "pydantic-1.10.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0cfe895a504c060e5d36b287ee696e2fdad02d89e0d895f83037245218a87fe"}, - {file = "pydantic-1.10.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:670bb4683ad1e48b0ecb06f0cfe2178dcf74ff27921cdf1606e527d2617a81ee"}, - {file = "pydantic-1.10.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:950ce33857841f9a337ce07ddf46bc84e1c4946d2a3bba18f8280297157a3fd1"}, - {file = "pydantic-1.10.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c15582f9055fbc1bfe50266a19771bbbef33dd28c45e78afbe1996fd70966c2a"}, - {file = "pydantic-1.10.7-cp37-cp37m-win_amd64.whl", hash = "sha256:82dffb306dd20bd5268fd6379bc4bfe75242a9c2b79fec58e1041fbbdb1f7914"}, - {file = "pydantic-1.10.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c7f51861d73e8b9ddcb9916ae7ac39fb52761d9ea0df41128e81e2ba42886cd"}, - {file = "pydantic-1.10.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6434b49c0b03a51021ade5c4daa7d70c98f7a79e95b551201fff682fc1661245"}, - {file = "pydantic-1.10.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d34ab766fa056df49013bb6e79921a0265204c071984e75a09cbceacbbdd5d"}, - {file = "pydantic-1.10.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:701daea9ffe9d26f97b52f1d157e0d4121644f0fcf80b443248434958fd03dc3"}, - {file = "pydantic-1.10.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cf135c46099ff3f919d2150a948ce94b9ce545598ef2c6c7bf55dca98a304b52"}, - {file = "pydantic-1.10.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0f85904f73161817b80781cc150f8b906d521fa11e3cdabae19a581c3606209"}, - {file = "pydantic-1.10.7-cp38-cp38-win_amd64.whl", hash = "sha256:9f6f0fd68d73257ad6685419478c5aece46432f4bdd8d32c7345f1986496171e"}, - {file = "pydantic-1.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c230c0d8a322276d6e7b88c3f7ce885f9ed16e0910354510e0bae84d54991143"}, - {file = "pydantic-1.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:976cae77ba6a49d80f461fd8bba183ff7ba79f44aa5cfa82f1346b5626542f8e"}, - {file = "pydantic-1.10.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d45fc99d64af9aaf7e308054a0067fdcd87ffe974f2442312372dfa66e1001d"}, - {file = "pydantic-1.10.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d2a5ebb48958754d386195fe9e9c5106f11275867051bf017a8059410e9abf1f"}, - {file = "pydantic-1.10.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:abfb7d4a7cd5cc4e1d1887c43503a7c5dd608eadf8bc615413fc498d3e4645cd"}, - {file = "pydantic-1.10.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:80b1fab4deb08a8292d15e43a6edccdffa5377a36a4597bb545b93e79c5ff0a5"}, - {file = "pydantic-1.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:d71e69699498b020ea198468e2480a2f1e7433e32a3a99760058c6520e2bea7e"}, - {file = "pydantic-1.10.7-py3-none-any.whl", hash = "sha256:0cd181f1d0b1d00e2b705f1bf1ac7799a2d938cce3376b8007df62b29be3c2c6"}, - {file = "pydantic-1.10.7.tar.gz", hash = "sha256:cfc83c0678b6ba51b0532bea66860617c4cd4251ecf76e9846fa5a9f3454e97e"}, + {file = "pydantic-1.10.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:22ed12ee588b1df028a2aa5d66f07bf8f8b4c8579c2e96d5a9c1f96b77f3bb55"}, + {file = "pydantic-1.10.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:75279d3cac98186b6ebc2597b06bcbc7244744f6b0b44a23e4ef01e5683cc0d2"}, + {file = "pydantic-1.10.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50f1666a9940d3d68683c9d96e39640f709d7a72ff8702987dab1761036206bb"}, + {file = "pydantic-1.10.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82790d4753ee5d00739d6cb5cf56bceb186d9d6ce134aca3ba7befb1eedbc2c8"}, + {file = "pydantic-1.10.15-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:d207d5b87f6cbefbdb1198154292faee8017d7495a54ae58db06762004500d00"}, + {file = "pydantic-1.10.15-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e49db944fad339b2ccb80128ffd3f8af076f9f287197a480bf1e4ca053a866f0"}, + {file = "pydantic-1.10.15-cp310-cp310-win_amd64.whl", hash = "sha256:d3b5c4cbd0c9cb61bbbb19ce335e1f8ab87a811f6d589ed52b0254cf585d709c"}, + {file = "pydantic-1.10.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c3d5731a120752248844676bf92f25a12f6e45425e63ce22e0849297a093b5b0"}, + {file = "pydantic-1.10.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c365ad9c394f9eeffcb30a82f4246c0006417f03a7c0f8315d6211f25f7cb654"}, + {file = "pydantic-1.10.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3287e1614393119c67bd4404f46e33ae3be3ed4cd10360b48d0a4459f420c6a3"}, + {file = "pydantic-1.10.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be51dd2c8596b25fe43c0a4a59c2bee4f18d88efb8031188f9e7ddc6b469cf44"}, + {file = "pydantic-1.10.15-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6a51a1dd4aa7b3f1317f65493a182d3cff708385327c1c82c81e4a9d6d65b2e4"}, + {file = "pydantic-1.10.15-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4e316e54b5775d1eb59187f9290aeb38acf620e10f7fd2f776d97bb788199e53"}, + {file = "pydantic-1.10.15-cp311-cp311-win_amd64.whl", hash = "sha256:0d142fa1b8f2f0ae11ddd5e3e317dcac060b951d605fda26ca9b234b92214986"}, + {file = "pydantic-1.10.15-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7ea210336b891f5ea334f8fc9f8f862b87acd5d4a0cbc9e3e208e7aa1775dabf"}, + {file = "pydantic-1.10.15-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3453685ccd7140715e05f2193d64030101eaad26076fad4e246c1cc97e1bb30d"}, + {file = "pydantic-1.10.15-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bea1f03b8d4e8e86702c918ccfd5d947ac268f0f0cc6ed71782e4b09353b26f"}, + {file = "pydantic-1.10.15-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:005655cabc29081de8243126e036f2065bd7ea5b9dff95fde6d2c642d39755de"}, + {file = "pydantic-1.10.15-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:af9850d98fc21e5bc24ea9e35dd80a29faf6462c608728a110c0a30b595e58b7"}, + {file = "pydantic-1.10.15-cp37-cp37m-win_amd64.whl", hash = "sha256:d31ee5b14a82c9afe2bd26aaa405293d4237d0591527d9129ce36e58f19f95c1"}, + {file = "pydantic-1.10.15-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5e09c19df304b8123938dc3c53d3d3be6ec74b9d7d0d80f4f4b5432ae16c2022"}, + {file = "pydantic-1.10.15-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7ac9237cd62947db00a0d16acf2f3e00d1ae9d3bd602b9c415f93e7a9fc10528"}, + {file = "pydantic-1.10.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:584f2d4c98ffec420e02305cf675857bae03c9d617fcfdc34946b1160213a948"}, + {file = "pydantic-1.10.15-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bbc6989fad0c030bd70a0b6f626f98a862224bc2b1e36bfc531ea2facc0a340c"}, + {file = "pydantic-1.10.15-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d573082c6ef99336f2cb5b667b781d2f776d4af311574fb53d908517ba523c22"}, + {file = "pydantic-1.10.15-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6bd7030c9abc80134087d8b6e7aa957e43d35714daa116aced57269a445b8f7b"}, + {file = "pydantic-1.10.15-cp38-cp38-win_amd64.whl", hash = "sha256:3350f527bb04138f8aff932dc828f154847fbdc7a1a44c240fbfff1b57f49a12"}, + {file = "pydantic-1.10.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:51d405b42f1b86703555797270e4970a9f9bd7953f3990142e69d1037f9d9e51"}, + {file = "pydantic-1.10.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a980a77c52723b0dc56640ced396b73a024d4b74f02bcb2d21dbbac1debbe9d0"}, + {file = "pydantic-1.10.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67f1a1fb467d3f49e1708a3f632b11c69fccb4e748a325d5a491ddc7b5d22383"}, + {file = "pydantic-1.10.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:676ed48f2c5bbad835f1a8ed8a6d44c1cd5a21121116d2ac40bd1cd3619746ed"}, + {file = "pydantic-1.10.15-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:92229f73400b80c13afcd050687f4d7e88de9234d74b27e6728aa689abcf58cc"}, + {file = "pydantic-1.10.15-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2746189100c646682eff0bce95efa7d2e203420d8e1c613dc0c6b4c1d9c1fde4"}, + {file = "pydantic-1.10.15-cp39-cp39-win_amd64.whl", hash = "sha256:394f08750bd8eaad714718812e7fab615f873b3cdd0b9d84e76e51ef3b50b6b7"}, + {file = "pydantic-1.10.15-py3-none-any.whl", hash = "sha256:28e552a060ba2740d0d2aabe35162652c1459a0b9069fe0db7f4ee0e18e74d58"}, + {file = "pydantic-1.10.15.tar.gz", hash = "sha256:ca832e124eda231a60a041da4f013e3ff24949d94a01154b137fc2f2a43c3ffb"}, ] [package.dependencies] @@ -1808,20 +1812,6 @@ files = [ [package.dependencies] types-urllib3 = "*" -[[package]] -name = "types-requests" -version = "2.31.0.20240311" -description = "Typing stubs for requests" -optional = false -python-versions = ">=3.8" -files = [ - {file = "types-requests-2.31.0.20240311.tar.gz", hash = "sha256:b1c1b66abfb7fa79aae09097a811c4aa97130eb8831c60e47aee4ca344731ca5"}, - {file = "types_requests-2.31.0.20240311-py3-none-any.whl", hash = "sha256:47872893d65a38e282ee9f277a4ee50d1b28bd592040df7d1fdaffdf3779937d"}, -] - -[package.dependencies] -urllib3 = ">=2" - [[package]] name = "types-urllib3" version = "1.26.25.14" @@ -1888,23 +1878,6 @@ brotli = ["brotli (==1.0.9)", "brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotl secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] -[[package]] -name = "urllib3" -version = "2.0.7" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.7" -files = [ - {file = "urllib3-2.0.7-py3-none-any.whl", hash = "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"}, - {file = "urllib3-2.0.7.tar.gz", hash = "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84"}, -] - -[package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] - [[package]] name = "websocket-client" version = "1.7.0" @@ -1939,4 +1912,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.12" -content-hash = "944d6c144d42a5ad522e48d8151dbfb7f8245b17b868da1752e12f1c6fbcf7e5" +content-hash = "5c447ec48183cdc4e0bd2617cea276db7ef3b465d0eb6149838477c05baf2fe9" diff --git a/pyproject.toml b/pyproject.toml index a5eba8f1..86c84d87 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,15 +25,16 @@ krr = "robusta_krr.main:run" [tool.poetry.dependencies] python = ">=3.9,<3.12" typer = { extras = ["all"], version = "^0.7.0" } -pydantic = "1.10.7" +pydantic = "^1.10.7" kubernetes = "^26.1.0" prometheus-api-client = "0.5.3" numpy = "^1.24.2" alive-progress = "^3.1.2" -prometrix = "^0.1.16" +prometrix = "^0.1.17" slack-sdk = "^3.21.3" + [tool.poetry.group.dev.dependencies] mypy = "^1.0.1" black = "^23.1.0" diff --git a/requirements.txt b/requirements.txt index 87b46032..ee5fce5d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ alive-progress==3.1.2 ; python_version >= "3.9" and python_version < "3.12" boto3==1.28.21 ; python_version >= "3.9" and python_version < "3.12" botocore==1.31.21 ; python_version >= "3.9" and python_version < "3.12" cachetools==5.3.0 ; python_version >= "3.9" and python_version < "3.12" -certifi==2022.12.7 ; python_version >= "3.9" and python_version < "3.12" +certifi==2024.2.2 ; python_version >= "3.9" and python_version < "3.12" charset-normalizer==3.0.1 ; python_version >= "3.9" and python_version < "3.12" click==8.1.3 ; python_version >= "3.9" and python_version < "3.12" colorama==0.4.6 ; python_version >= "3.9" and python_version < "3.12" and platform_system == "Windows" @@ -11,7 +11,7 @@ commonmark==0.9.1 ; python_version >= "3.9" and python_version < "3.12" contourpy==1.0.7 ; python_version >= "3.9" and python_version < "3.12" cycler==0.11.0 ; python_version >= "3.9" and python_version < "3.12" dateparser==1.1.7 ; python_version >= "3.9" and python_version < "3.12" -fonttools==4.39.0 ; python_version >= "3.9" and python_version < "3.12" +fonttools==4.43.0 ; python_version >= "3.9" and python_version < "3.12" google-auth==2.16.2 ; python_version >= "3.9" and python_version < "3.12" grapheme==0.6.0 ; python_version >= "3.9" and python_version < "3.12" httmock==1.4.0 ; python_version >= "3.9" and python_version < "3.12" @@ -25,13 +25,13 @@ numpy==1.24.2 ; python_version >= "3.9" and python_version < "3.12" oauthlib==3.2.2 ; python_version >= "3.9" and python_version < "3.12" packaging==23.0 ; python_version >= "3.9" and python_version < "3.12" pandas==1.5.3 ; python_version >= "3.9" and python_version < "3.12" -pillow==9.4.0 ; python_version >= "3.9" and python_version < "3.12" +pillow==10.3.0 ; python_version >= "3.9" and python_version < "3.12" prometheus-api-client==0.5.3 ; python_version >= "3.9" and python_version < "3.12" -prometrix==0.1.16 ; python_version >= "3.9" and python_version < "3.12" +prometrix==0.1.17 ; python_version >= "3.9" and python_version < "3.12" pyasn1-modules==0.2.8 ; python_version >= "3.9" and python_version < "3.12" pyasn1==0.4.8 ; python_version >= "3.9" and python_version < "3.12" -pydantic==1.10.7 ; python_version >= "3.9" and python_version < "3.12" -pygments==2.14.0 ; python_version >= "3.9" and python_version < "3.12" +pydantic==1.10.15 ; python_version >= "3.9" and python_version < "3.12" +pygments==2.17.2 ; python_version >= "3.9" and python_version < "3.12" pyparsing==3.0.9 ; python_version >= "3.9" and python_version < "3.12" python-dateutil==2.8.2 ; python_version >= "3.9" and python_version < "3.12" pytz-deprecation-shim==0.1.0.post0 ; python_version >= "3.9" and python_version < "3.12" @@ -39,7 +39,7 @@ pytz==2022.7.1 ; python_version >= "3.9" and python_version < "3.12" pyyaml==6.0 ; python_version >= "3.9" and python_version < "3.12" regex==2022.10.31 ; python_version >= "3.9" and python_version < "3.12" requests-oauthlib==1.3.1 ; python_version >= "3.9" and python_version < "3.12" -requests==2.28.2 ; python_version >= "3.9" and python_version < "3.12" +requests==2.31.0 ; python_version >= "3.9" and python_version < "3.12" rich==12.6.0 ; python_version >= "3.9" and python_version < "3.12" rsa==4.9 ; python_version >= "3.9" and python_version < "3.12" s3transfer==0.6.1 ; python_version >= "3.9" and python_version < "3.12" @@ -51,6 +51,6 @@ typer[all]==0.7.0 ; python_version >= "3.9" and python_version < "3.12" typing-extensions==4.5.0 ; python_version >= "3.9" and python_version < "3.12" tzdata==2022.7 ; python_version >= "3.9" and python_version < "3.12" tzlocal==4.2 ; python_version >= "3.9" and python_version < "3.12" -urllib3==1.26.14 ; python_version >= "3.9" and python_version < "3.12" +urllib3==1.26.18 ; python_version >= "3.9" and python_version < "3.12" websocket-client==1.5.1 ; python_version >= "3.9" and python_version < "3.12" zipp==3.15.0 ; python_version >= "3.9" and python_version < "3.10" From 9542136499aac37a891b2fdfafae44adc050e931 Mon Sep 17 00:00:00 2001 From: Adam Hamsik Date: Tue, 23 Apr 2024 14:07:07 +0200 Subject: [PATCH 078/137] Build universal binaries on mac os not just local architecture. --- .github/workflows/build-on-release.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/build-on-release.yml b/.github/workflows/build-on-release.yml index f3c3ae10..1dc00f91 100644 --- a/.github/workflows/build-on-release.yml +++ b/.github/workflows/build-on-release.yml @@ -78,8 +78,17 @@ jobs: env: GITHUB_REF_NAME: ${{ github.ref_name }} + - name: Build with PyInstaller + if: matrix.os == 'macos-latest' + shell: bash + run: | + pyinstaller --target-architecture universal2 krr.py + mkdir -p ./dist/krr/grapheme/data + cp $(python -c "import grapheme; print(grapheme.__path__[0] + '/data/grapheme_break_property.json')") ./dist/krr/grapheme/data/grapheme_break_property.json + cp ./intro.txt ./dist/krr/intro.txt - name: Build with PyInstaller + if: matrix.os == 'ubuntu-latest' shell: bash run: | pyinstaller krr.py From 447aeaa612eb4d5ab1247b8123981c5438205ff1 Mon Sep 17 00:00:00 2001 From: Adam Hamsik Date: Tue, 23 Apr 2024 14:16:57 +0200 Subject: [PATCH 079/137] bump python --- .github/workflows/build-on-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-on-release.yml b/.github/workflows/build-on-release.yml index 1dc00f91..d8a91237 100644 --- a/.github/workflows/build-on-release.yml +++ b/.github/workflows/build-on-release.yml @@ -18,7 +18,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v2 with: - python-version: '3.9' + python-version: '3.11' - name: Install dependencies run: | From ba120375586841393ee0c8ae79cd4c95615afb3a Mon Sep 17 00:00:00 2001 From: Tomer Keshet Date: Sun, 28 Apr 2024 13:43:04 +0300 Subject: [PATCH 080/137] removed .idea as it should be ignored used git rm -r --cached --- .idea/workspace.xml | 49 --------------------------------------------- 1 file changed, 49 deletions(-) delete mode 100644 .idea/workspace.xml diff --git a/.idea/workspace.xml b/.idea/workspace.xml deleted file mode 100644 index d6f915dd..00000000 --- a/.idea/workspace.xml +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1683614225722 - - - - \ No newline at end of file From dd13deeac4ca62f8187d77d944305675abc9dba8 Mon Sep 17 00:00:00 2001 From: Pavel Zhukov <33721692+LeaveMyYard@users.noreply.github.com> Date: Mon, 29 Apr 2024 17:08:26 +0300 Subject: [PATCH 081/137] Simple strategy with OOMKill data (#257) * Simple strategy with OOMKill data * Merge oom into simple, minor other improvements * Fix join bug in MaxOOMKilledMemoryLoader * Minor improvements * Colored oom message, comments improvements * One more comment improvement --- robusta_krr/core/abstract/strategies.py | 2 +- .../prometheus/metrics/__init__.py | 2 +- .../integrations/prometheus/metrics/base.py | 6 +- .../integrations/prometheus/metrics/memory.py | 38 ++++++++++ .../prometheus_metrics_service.py | 7 +- robusta_krr/formatters/table.py | 14 +++- robusta_krr/strategies/__init__.py | 2 +- robusta_krr/strategies/simple.py | 72 ++++++++++++++++--- 8 files changed, 127 insertions(+), 16 deletions(-) diff --git a/robusta_krr/core/abstract/strategies.py b/robusta_krr/core/abstract/strategies.py index 5b6521e1..0005a227 100644 --- a/robusta_krr/core/abstract/strategies.py +++ b/robusta_krr/core/abstract/strategies.py @@ -28,7 +28,7 @@ class ResourceRecommendation(pd.BaseModel): request: Optional[float] limit: Optional[float] info: Optional[str] = pd.Field( - None, description="Additional information about the recommendation. Currently used to explain undefined." + None, description="Additional information about the recommendation." ) @classmethod diff --git a/robusta_krr/core/integrations/prometheus/metrics/__init__.py b/robusta_krr/core/integrations/prometheus/metrics/__init__.py index e9a0568b..aea497f6 100644 --- a/robusta_krr/core/integrations/prometheus/metrics/__init__.py +++ b/robusta_krr/core/integrations/prometheus/metrics/__init__.py @@ -1,3 +1,3 @@ from .base import PrometheusMetric from .cpu import CPUAmountLoader, CPULoader, PercentileCPULoader -from .memory import MaxMemoryLoader, MemoryAmountLoader, MemoryLoader +from .memory import MaxMemoryLoader, MemoryAmountLoader, MemoryLoader, MaxOOMKilledMemoryLoader diff --git a/robusta_krr/core/integrations/prometheus/metrics/base.py b/robusta_krr/core/integrations/prometheus/metrics/base.py index f4265e22..4169b0f0 100644 --- a/robusta_krr/core/integrations/prometheus/metrics/base.py +++ b/robusta_krr/core/integrations/prometheus/metrics/base.py @@ -59,6 +59,7 @@ class PrometheusMetric(BaseMetric): query_type: QueryType = QueryType.Query filtering: bool = True pods_batch_size: Optional[int] = 50 + warning_on_no_data: bool = True def __init__( self, @@ -126,7 +127,10 @@ def _query_prometheus_sync(self, data: PrometheusMetricData) -> list[PrometheusS return response["result"] else: # regular query, lighter on preformance - response = self.prometheus.safe_custom_query(query=data.query) + try: + response = self.prometheus.safe_custom_query(query=data.query) + except Exception as e: + raise ValueError(f"Failed to run query: {data.query}") from e results = response["result"] # format the results to return the same format as custom_query_range for result in results: diff --git a/robusta_krr/core/integrations/prometheus/metrics/memory.py b/robusta_krr/core/integrations/prometheus/metrics/memory.py index 85031bc1..85dfba6b 100644 --- a/robusta_krr/core/integrations/prometheus/metrics/memory.py +++ b/robusta_krr/core/integrations/prometheus/metrics/memory.py @@ -69,3 +69,41 @@ def get_query(self, object: K8sObjectData, duration: str, step: str) -> str: [{duration}:{step}] ) """ + +# TODO: Need to battle test if this one is correct. +class MaxOOMKilledMemoryLoader(PrometheusMetric): + """ + A metric loader for loading the maximum memory limits that were surpassed by the OOMKilled event. + """ + + warning_on_no_data = False + + def get_query(self, object: K8sObjectData, duration: str, step: str) -> str: + pods_selector = "|".join(pod.name for pod in object.pods) + cluster_label = self.get_prometheus_cluster_label() + return f""" + max_over_time( + max( + max( + kube_pod_container_resource_limits{{ + resource="memory", + namespace="{object.namespace}", + pod=~"{pods_selector}", + container="{object.container}" + {cluster_label} + }} + ) by (pod, container, job) + * on(pod, container, job) group_left(reason) + max( + kube_pod_container_status_last_terminated_reason{{ + reason="OOMKilled", + namespace="{object.namespace}", + pod=~"{pods_selector}", + container="{object.container}" + {cluster_label} + }} + ) by (pod, container, job, reason) + ) by (container, pod, job) + [{duration}:{step}] + ) + """ diff --git a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py index 423a29ad..8331249f 100644 --- a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py +++ b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py @@ -196,9 +196,10 @@ async def gather_data( elif "Memory" in LoaderClass.__name__: object.add_warning("NoPrometheusMemoryMetrics") - logger.warning( - f"{metric_loader.service_name} returned no {metric_loader.__class__.__name__} metrics for {object}" - ) + if LoaderClass.warning_on_no_data: + logger.warning( + f"{metric_loader.service_name} returned no {metric_loader.__class__.__name__} metrics for {object}" + ) return data diff --git a/robusta_krr/formatters/table.py b/robusta_krr/formatters/table.py index 3d20b8e4..b028a058 100644 --- a/robusta_krr/formatters/table.py +++ b/robusta_krr/formatters/table.py @@ -33,6 +33,12 @@ def __calc_diff(allocated, recommended, selector, multiplier=1) -> str: return f"{diff_sign}{_format(abs(diff_val) * multiplier)}" +DEFAULT_INFO_COLOR = "grey27" +INFO_COLORS: dict[str, str] = { + "OOMKill detected": "dark_red", +} + + def _format_request_str(item: ResourceScan, resource: ResourceType, selector: str) -> str: allocated = getattr(item.object.allocations, selector)[resource] info = item.recommended.info.get(resource) @@ -46,6 +52,12 @@ def _format_request_str(item: ResourceScan, resource: ResourceType, selector: st if diff != "": diff = f"({diff}) " + if info is None: + info_formatted = "" + else: + color = INFO_COLORS.get(info, DEFAULT_INFO_COLOR) + info_formatted = f"\n[{color}]({info})[/{color}]" + return ( diff + f"[{severity.color}]" @@ -53,7 +65,7 @@ def _format_request_str(item: ResourceScan, resource: ResourceType, selector: st + " -> " + _format(recommended.value) + f"[/{severity.color}]" - + (f" [grey27]({info})[/grey27]" if info else "") + + info_formatted ) diff --git a/robusta_krr/strategies/__init__.py b/robusta_krr/strategies/__init__.py index 05e029bb..3409ebd8 100644 --- a/robusta_krr/strategies/__init__.py +++ b/robusta_krr/strategies/__init__.py @@ -1 +1 @@ -from .simple import SimpleStrategy +from .simple import SimpleStrategy \ No newline at end of file diff --git a/robusta_krr/strategies/simple.py b/robusta_krr/strategies/simple.py index 64e58ac5..3cecd18e 100644 --- a/robusta_krr/strategies/simple.py +++ b/robusta_krr/strategies/simple.py @@ -19,8 +19,10 @@ MemoryAmountLoader, PercentileCPULoader, PrometheusMetric, + MaxOOMKilledMemoryLoader, ) + class SimpleStrategySettings(StrategySettings): cpu_percentile: float = pd.Field(99, gt=0, le=100, description="The percentile to use for the CPU recommendation.") memory_buffer_percentage: float = pd.Field( @@ -29,14 +31,27 @@ class SimpleStrategySettings(StrategySettings): points_required: int = pd.Field( 100, ge=1, description="The number of data points required to make a recommendation for a resource." ) - allow_hpa: bool = pd.Field(False, description="Whether to calculate recommendations even when there is an HPA scaler defined on that resource.") + allow_hpa: bool = pd.Field( + False, + description="Whether to calculate recommendations even when there is an HPA scaler defined on that resource.", + ) + use_oomkill_data: bool = pd.Field( + False, + description="Whether to bump the memory when OOMKills are detected (experimental).", + ) + oom_memory_buffer_percentage: float = pd.Field( + 25, gt=0, description="What percentage to increase the memory when there are OOMKill events." + ) - def calculate_memory_proposal(self, data: PodsTimeData) -> float: + def calculate_memory_proposal(self, data: PodsTimeData, max_oomkill: float = 0) -> float: data_ = [np.max(values[:, 1]) for values in data.values()] if len(data_) == 0: return float("NaN") - return np.max(data_) * (1 + self.memory_buffer_percentage / 100) + return max( + np.max(data_) * (1 + self.memory_buffer_percentage / 100), + max_oomkill * (1 + self.oom_memory_buffer_percentage / 100), + ) def calculate_cpu_proposal(self, data: PodsTimeData) -> float: if len(data) == 0: @@ -75,7 +90,17 @@ class SimpleStrategy(BaseStrategy[SimpleStrategySettings]): @property def metrics(self) -> list[type[PrometheusMetric]]: - return [PercentileCPULoader(self.settings.cpu_percentile), MaxMemoryLoader, CPUAmountLoader, MemoryAmountLoader] + metrics = [ + PercentileCPULoader(self.settings.cpu_percentile), + MaxMemoryLoader, + CPUAmountLoader, + MemoryAmountLoader, + ] + + if self.settings.use_oomkill_data: + metrics.append(MaxOOMKilledMemoryLoader) + + return metrics def __calculate_cpu_proposal( self, history_data: MetricsPodData, object_data: K8sObjectData @@ -85,13 +110,20 @@ def __calculate_cpu_proposal( if len(data) == 0: return ResourceRecommendation.undefined(info="No data") + # NOTE: metrics for each pod are returned as list[values] where values is [timestamp, value] + # As CPUAmountLoader returns only the last value (1 point), [0, 1] is used to get the value + # So each pod is string with pod name, and values is numpy array of shape (N, 2) data_count = {pod: values[0, 1] for pod, values in history_data["CPUAmountLoader"].items()} total_points_count = sum(data_count.values()) if total_points_count < self.settings.points_required: return ResourceRecommendation.undefined(info="Not enough data") - if object_data.hpa is not None and object_data.hpa.target_cpu_utilization_percentage is not None and not self.settings.allow_hpa: + if ( + object_data.hpa is not None + and object_data.hpa.target_cpu_utilization_percentage is not None + and not self.settings.allow_hpa + ): return ResourceRecommendation.undefined(info="HPA detected") cpu_usage = self.settings.calculate_cpu_proposal(data) @@ -102,20 +134,44 @@ def __calculate_memory_proposal( ) -> ResourceRecommendation: data = history_data["MaxMemoryLoader"] + oomkill_detected = False + + if self.settings.use_oomkill_data: + max_oomkill_data = history_data["MaxOOMKilledMemoryLoader"] + # NOTE: metrics for each pod are returned as list[values] where values is [timestamp, value] + # As MaxOOMKilledMemoryLoader returns only the last value (1 point), [0, 1] is used to get the value + # So each value is numpy array of shape (N, 2) + max_oomkill_value = ( + np.max([values[0, 1] for values in max_oomkill_data.values()]) if len(max_oomkill_data) > 0 else 0 + ) + if max_oomkill_value != 0: + oomkill_detected = True + else: + max_oomkill_value = 0 + if len(data) == 0: return ResourceRecommendation.undefined(info="No data") + # NOTE: metrics for each pod are returned as list[values] where values is [timestamp, value] + # As MemoryAmountLoader returns only the last value (1 point), [0, 1] is used to get the value + # So each pod is string with pod name, and values is numpy array of shape (N, 2) data_count = {pod: values[0, 1] for pod, values in history_data["MemoryAmountLoader"].items()} total_points_count = sum(data_count.values()) if total_points_count < self.settings.points_required: return ResourceRecommendation.undefined(info="Not enough data") - if object_data.hpa is not None and object_data.hpa.target_memory_utilization_percentage is not None and not self.settings.allow_hpa: + if ( + object_data.hpa is not None + and object_data.hpa.target_memory_utilization_percentage is not None + and not self.settings.allow_hpa + ): return ResourceRecommendation.undefined(info="HPA detected") - memory_usage = self.settings.calculate_memory_proposal(data) - return ResourceRecommendation(request=memory_usage, limit=memory_usage) + memory_usage = self.settings.calculate_memory_proposal(data, max_oomkill_value) + return ResourceRecommendation( + request=memory_usage, limit=memory_usage, info="OOMKill detected" if oomkill_detected else None + ) def run(self, history_data: MetricsPodData, object_data: K8sObjectData) -> RunResult: return { From 963de3e035c40cd1b3221e856bb391f95f84c7ea Mon Sep 17 00:00:00 2001 From: Pavan Gudiwada <25551553+pavangudiwada@users.noreply.github.com> Date: Thu, 2 May 2024 05:32:38 +0530 Subject: [PATCH 082/137] Added Grafana mimir and GMP docs --- README.md | 27 ++++++++- images/krr-datasources.png | Bin 42120 -> 53798 bytes images/krr-datasources.svg | 119 ++++++++++++++++++++----------------- 3 files changed, 90 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index 6dfafc29..eec9c60a 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ Robusta KRR (Kubernetes Resource Recommender) is a CLI tool for optimizing resou [![Used to send data to KRR](./images/krr-datasources.svg)](#data-source-integrations) -_View Instructions for: [Prometheus](#prometheus-victoria-metrics-and-thanos-auto-discovery), [Thanos](#prometheus-victoria-metrics-and-thanos-auto-discovery), [Victoria Metrics](#prometheus-victoria-metrics-and-thanos-auto-discovery), [Google Managed Prometheus](./docs/google-cloud-managed-service-for-prometheus.md), [Amazon Managed Prometheus](#amazon-managed-prometheus), [Azure Managed Prometheus](#azure-managed-prometheus), [Coralogix](#coralogix-managed-prometheus) and [Grafana Cloud](#grafana-cloud-managed-prometheus)_ +_View Instructions for: [Prometheus](#prometheus-victoria-metrics-and-thanos-auto-discovery), [Thanos](#prometheus-victoria-metrics-and-thanos-auto-discovery), [Victoria Metrics](#prometheus-victoria-metrics-and-thanos-auto-discovery), [Google Managed Prometheus](./docs/google-cloud-managed-service-for-prometheus.md), [Amazon Managed Prometheus](#amazon-managed-prometheus), [Azure Managed Prometheus](#azure-managed-prometheus), [Coralogix](#coralogix-managed-prometheus),[Grafana Cloud](#grafana-cloud-managed-prometheus) and [Grafana Mimir](#grafana-mimir-auto-discovery)_ @@ -196,6 +196,7 @@ Setup KRR for... - [Amazon Managed Prometheus](#amazon-managed-prometheus) - [Coralogix Managed Prometheus](#coralogix-managed-prometheus) - [Grafana Cloud Managed Prometheus](#grafana-cloud-managed-prometheus) +- [Grafana Mimir](#grafana-mimir-auto-discovery)

(back to top)

@@ -382,7 +383,6 @@ Find about how KRR tries to find the default Prometheus to connect Prometheus, Victoria Metrics and Thanos auto-discovery @@ -461,6 +461,14 @@ python krr.py simple --namespace default -p PROMETHEUS_URL --prometheus-auth-hea
+
+Google Managed Prometheus (GMP) + +Please find the detailed GMP usage instructions [here](https://github.com/robusta-dev/krr/blob/main/docs/google-cloud-managed-service-for-prometheus.md) + +

(back to top)

+ +
Amazon Managed Prometheus @@ -517,7 +525,20 @@ python krr.py simple -p $PROM_URL --prometheus-auth-header "Bearer ${PROM_USER}:

(back to top)

-

+ +
+ Grafana Mimir auto-discovery + +By default, KRR will try to auto-discover the running Grafana Mimir. + +For discovering Prometheus it scans services for those labels: +```python + "app.kubernetes.io/name=mimir,app.kubernetes.io/component=query-frontend" +``` + +

(back to top)

+ +
## Integrations diff --git a/images/krr-datasources.png b/images/krr-datasources.png index 4823558e53f064c577c2ae77827a2a03092a7ceb..d198454c043bde5149a27faadaef94a5efc7eb6a 100644 GIT binary patch literal 53798 zcmeEtby$<(|L#Cg0TF47l#o^wX-0_jh=HJ#f=D+=GfEVYj?tqBsC0LTq;#ir_h78g z{GPwQzw_t$?_B4-uD#oyU3=g6iBH}4{Y2=1RH!NMQvv`0YBklDdH}$60`c?To7ahF zE?M7R;v0pF>Kk_efQtU#iv*CKd6(Eo;;yHn2q+tVxJ&#(X04#50030RQeoed0{~;q zYA+S^y-5zTZsag*HX>nj;NWga%DkW;YKM0!Ex|?=FQ2}OzRzK2q4vwBEZw3XT3pG! zVsOr_>z??P$1YFzTWf9v?E^0U;>SjhIoXx(Y%oX!tT<ice}!fPx0SJ;jkwK>3=r?07>L=kpH8N;{VOIf1Q{20ibUDj}FVH{r|TA zKL*6L@qe`rbkuUR+3TKzXTql@8?j2~K2aA9wP{U%eAL|UO|%AT?bIUSGqgIpXDWY!=5~(O}PIeH}Y_ zCuhgixf1kSwPdLm?Zr#k>QxTv{Eh!SxrLF8$LSlhW0Q~JBM9am6WXmccV|8>d+fewHP;&eX20};^O4*b zEo07BG6=29^{3;(grOkU@8t^YizCCQxONs?BV8ju~-Wk zZaTVK6EOQ!K@09I~ia9ltEar0`DmK7x+ zq-4zGJ_srhhuB0b(6BuTBHjcr3YR}CH@ah-UNV;yXq=4_V5CfOTnsHYEjpp(#otYV z?Tg5~j`?E6#l^lOWVG`q{o)57hG}fE(cNZqAbE0f@};bDM5ly8-{j29M1w0PbwVOt z!j?0dMH;03&th`QM^z6IY;U0k^W{IkC#KKWkB19sO)f8~9cE<>SEZNomzE!C*Cr=F z&wDRDz#uF;(0ZK^6|Q$}ifB(kXREETGZ|~z<)1zjVg%iJo_S5nympNU<6EV3Llw~Wo;o7n^+1W(g{m9< z?VRrhCy=^>O6?=|X%kGGH8r_*3Ka&x5Dvh+FosX2Hk6aM@aVS;6 zeS0Xrt99FlQth9S@vZ;)&f1atj^wcZ0Ng z{0&=Kum={^PYc2?%Rqa47h<@=o7nNbrIOq_$lC>Z04YEoRYCAN%1efcm8fP{K|uZ5 ztp~jQZaGU_O$`WZX-5bm_c`KwukU4Vo=|_DP{9{ibXchG3}ja&q8AGIJxYOCb~RXcZ*HowF@316?$G;axm48D6|-=GUT9*FNQ*?qe>%lsdJ47Q zW&XXq5UXiSs`&5Wch8r5AMBiHG|tCYw5D0D8Ns-<`mJZ_;x=KcsvS=1pFI1>-c5Db z&7W@rqYqHqLYubMSBfuT1}2j!;SALxy$pNybEwV+Y2P-h_6G~F+cxsM+HoxESGBSP zO;BQhY@uEIwUSE#UY$P$-Bxot0AIxgmHIec=_I>PXeN@rQBi;_;Y{l(;&Nhj62@jJ zFUu&9nc{$5GMK#R|KfIFLjN$x!{~#0yB-5%NYU>=km#KOL3+HVr^pmO9KO}fwOkF@ zgV{Iir^~x>)Ih}|;V?7nIn}^Hbp1IVK7V_Vht@?w;0U17AGOqBLEAy;?mZl@p?WHv zFo*K{?JcJHDi6M?=zE8A<@|?ygN{6ow$@Z~tIu5bYK8Hc*lfDM56D}$YVhX+FS`xZ zpGO+L@KnSHfBTz|ZOj%B_m<74qBsl&f(uHbnQF0tIeY+`{6Khi@OTcpfRTr1;3W-T zQ>mB4EA@^x(0-nMiCwdrKJ~q~Rr2^vS|-B>kias3!Rr#rGa_6HhLRs7YQe7E{rz>s ztfX^hDOxm#UFg-fhiXJen`iL-wO&c*did3~fM{(fT`S^dvY6#1uhfPSX^wV$%$2>v ziu#C%j|yd@|JVoC^TJr>vAhwvLVDywwhYoR02v@?d@GG0==m@$$+6A`%!2x%3VW|6 z#ssPI%9a$+<)9BrDlQPxko&X&J9&9>OIrk}t=(S13VG|J-JP~g2{hoGoL@``#7E{- zlDCFoHs&s}Ja&a^$>jMH_NUXwkAw3xP(7XN4w&9&ZhE#I^OoA|fbl}h9&@V&!?8KV z!>#jqSz&ma=5`ZAr^@P{XHCE>1L;7P-F79vhfRC4oy zLSaxs$;GyrS}lDH7huiC#wJIF4U!3xL~SI{1qtH47Xb*dx5i(Wy|V}Ix4B+J-9Xbl zC>ATcGHx%7&&IyXPICSHF4PCxE`k-$YMAM)3D6X~B^S$~ud$#D{O6gZT>3|nVI>&R z@P&Qt++P{*BY=GSM%Uc-Mp^^}Z-D{#muglwC2P8q%4Z+U?aFavl4ZjousXkF1lS_Z zkBsN@NfHU;JRVy$DwR%9yU8I?>p|h$T=`%Zc!xrxT?o=R!C<2e=F1-sGXCSfOE^5| zAW=~Nu;=_U;93`Uc4ON>+W#|u-CQi1{}6^%4g^LHf84u5=$6i*7Rc0N$%4(kfbBBM zB+H4xn|?~X{8N{%K7R!O+m%#mqRtqk|LvhWJ@_l1Yz9!f8%UsBxY@uFiqNOK^ z8ka;Z=Ur6p3%+evkE~aW0Ftv2+PutC)+5$|HK1%}q798Jq+pXe<+!)6VAo3)v-I8h zcj2vduhse@l;3&zG1m&3tsTs;wM|R}qGfoOta-_o2E?SF zMQvZ`8et=*J%?*|OfDW%fa5~Oubq`cZ3B0ZlO9MdeFvA4p<=f*Vi{24d*IE_{dQ>o z7Rk+V1s(vKUuF>~>s8OK*jReV#YN9TC=p@+s6uIMz&{>aj{Po_^J{K5=Q;6SBYVfo zZ5v3LOF<0c4c=S8!A0h9Q;P{5;Zb3vt&tkge%zOEn)KuYN;^L<VpxPH*zTg zrh`lxm2>{*o^e6!^2jan$;L}I64VT?hqUoWIR`jSfbGJ4Qr&ETls^|NwjTlD## zNxcHaQoC(gODOU=&hV?8zmPKjTZPk3(v#^0XZY>}9Q;%?08Q@Zf9D_mFc6$PDF}bu zFv%NLXYp%K_R44DY@{UlwT>;Ki-efR4ci|Fs0+-TTi0c@pL#mkb}@oHx|}Q+yU+cCaKt}!TZfUrGW%dchxYgjqMeJ;j$rXFih`A-^_1t=Ss?S=t&*UtVX(U1xOyWlQuTXJ4 z?}%bE(ypl@<_g=oR`a6CSM+wK#@JIX4T$g?raO+|?C^3JFAiRKiVeUXFv{4-+F(*~o=T*aEBVcod zBK(zqz>GrcQIu%SB(YoMMsl^ofgzD;`u#20N%)8UOG}frSS5Z2|3eOdLQ{o<027Ik zrX?{-0o*!oAe0>%N7~yBuUH`^fAtEq1MdnweuEV%{6oyXa~gacSK%ow6JTFf`=SW> zZsF72o9$r>G&+~~XG3q2ClSk>*7l~CZ+gV3x8+Oy;cT&!6SwkCxP`-ba0jG0@#7T+U)wkwN4iagBy_fT6C> zR3*7>*{@>mGaCIKkJbn_3ouwMcnU>*+f!<=+f+xfL5V?%3lP*3W?8~zguXpiG;%XY zj_3uGCm9;CE8m%zTd%+lv*W4+GZbxrt-klgH>*k2eA@@5UCsgu_x+nbviz*$1xnv$ z<7Y!ITRx|LXrz+k@?5;W54jf_o1zDHulE1>7y0qm$@??fV*}fhdB5+639t<=jLA#+ zqwntZ}<|M zjzm7~hd+kZU3Jl6;^UXR8W5+WylZuxLY-|ddqe7rH@EgboKYK{)cz&=KyZd>UmK!8 z2uZB+)P*NiA_B5`jAwLvBUOq&)>oHguI|S>)L_<1MeY&2q9oe`=2|Bq*m8=b>sLi1 z9DitVZN(Y^dtBEGj<{dTUcOUDl*2csdDL&bIdb3oeCC4;7t^xmvI$Mtmtwr(At@zv~D+CUL-(t$h4(}S2CC0f1Z>#TA+K2*_DawK$!CgT^PuG0df z1i#@WF=AkLp`l;s&pN9ESXXW_rUTE_Ib!CZ-xWZD@! z_0Fu>A?%^~ca`i+WsyFlgYEn=W4riXtJDW9$(we=9HjTV%NwjDTRkpduS>V1W9yq| z6dhy{1r->AE8P=?rNhG-tKHt`?+|5MqF%91L0mE@=F!+;-*B%R8T8!HgQ7NS*lt?yn;#XFSw)rL&+Pk(AE@RykG2gNcIjz1-CLf?5@0Rd z%K~xdSNTH@CfX+3SovK+UBb~?NB)UN(I)(!BnG$UW93gnozV|F*ZyGv#djt_5()Fm z-cR!#>2MPm(S1VgvkAHK;J2{$ql?c_h-MV3&4tvZgTk5?@cs4Ay!=?H1lFhIe;Zua zNeAH%AuSA{+t~qijYsNxj;~@KDtl$Di8u(}ifRY91y=DrnxH=+`T+T-v@`q1D+0`5 zzBBB&*ABv!UJ2@ora7Z|(j;%OI+KVyxs<`3rL2sFonj{Vcv-DM;PK@fa4KFlpqQJa zM(GM+i&@X)B8mB9ySky-IWWFU5Zb$T-gK5uc;FWR%S7?cKl@`@xr6Bus<*}kmT>X5 zZ{LEg&sk^l6bk7D_jG3F4Q<%uawC9`YT9Aqo6JwAiSw?YU^6TE3THG@wqL+@FdZw8s_tPh7Oa<- zVR1Jr5`&q87~8L|Q7`>w*I9N!sS8|e2kJ?|V2b2LQ^?OKiupN37MP5pSpMKM+)Nw7 zOq|@d;q!MsTi)I&&o#Ej)N&sjp8g~>L=WI;>jsGWm#cj2FdWG{B%JsU$OXk`L@KqjmeV}|>;l}}aVO&Yw z3x8R1&rC!XU4)`HT~K&Ju&$b%Y%<+qW5D|SNtp$d?n3(a$7TfYW9M|IHr6!}tvdbb zu_7nD@n@U2SwiN>3hXxtKlWo0$MZ z+?w$vgOOW_B2H~JHsfD&BpMJ2QM-oqi*!xj2JJ7401w5T*!&2qRx;K@4WYuVR~G-U zH1*`<$%l$)`2P;#6*EQgry$rbrSI#Lp8>p9fD?Ts7l6Rz@S6#wN#kc2tn{3V@WSSui>65bkW@#>pfu zWcOUW>9l_u-qP0SDH)dJf2l;;R4pGR_{?>+ z;?{n##qOqmFXQ}|{Uk-Gchq`$ra0_rS2|2c+3i!dzrW8btAIM&Nv!rYVE>%^%YeT` zA6NlOq0&;;p0F*U3vZN3;jg@R1LkhBBI}8h$emeuo>rBpkr&c+I^QLht(WmlCjg)M zfvvH8Y1r@W2r!`)Z*=`Wg)~KKaRpfpYPMx3VEbkllYdKD^2W2wNmzE@1U*mHugnxQ zcw9t>X|j<|K&II>vZgW0e?dq7rd)MdsJsv9DO9fM3M>yu5r4fGyO@(fI3G)NJ0G@o zoo%i3r^AU}SZ610FXk86=wTEhEBpw_W3`VB*$KY#=R-gS<@6tZq0-@dl{erA&b?K_ z^xiO6wYGE$mFrsuM=iXJ53pvg&aVI-#8h5M=^AdoXYRS@-tz(S7MKdL4A|T6CLn)} zi`WVN^RgvGm@b|h2zBDeAv7{OUha;xspaaKs=q5#A7_z84JJj-Y!h*H4*J}gbIZbI zd}^xu)_$^+o2cRQP4>f^RZ1yXla&psPDy2{+M3MOAagaSybDJUggp}d3b(DXAG$_iOl+#RXP z-$b^DUuh&hIfVxS1hqfx#riGS_SQu7jCk-Vb_5HE`QzNYT^je9Pf9 z4EaY+{8a!79nm{R3^K2nA;%A`*JcD?qgo$GR5b9f0HE!yR~(I%aw1L{KqZZ}cR@wn?j^IOsSXPn{{r?GJjqM&d}b}D8pR}3 zli;(}6lk>kbdKxXd*QK9-`a6fIqV@M zbrU1X!`faOzWm!yxoMHPQS{20>V7Lpma(#uQLF)HjWscy+$tlI=MUmBRz5x8%#$@M z{{sgurok3X^i@e4WR9|$2HdCQ+4la#rwE*v-1~b5@eq7cC=F`6of))22Se@PZjP_l zD%(Flww_g!iIP{-5?e8wX&SV>pxl5^^8SH7fl=-2PAh(oD+FdR1;~3gA8l2-DNaNx zN(b=^+DW+SG}y}9hi{L?hrM)?)IAPT8tL+}E54OSHRAKh^ajpF6G&P}6V?86i%c&u z<_i47o&m9rG2V?iM&viW4VB*=ZyW&O=5AspWY?7RF{tn;-=)j3*5~gm_Nw-JG%;&_ z`Jg2*S_)mhJl}Abm0{DNOLONjh@DXN?)S$xJ2PW>dV&_Mbf;{rNw!M`z5ZdD9Hk5r;5mjXBXbUk*t$M_Vo{-wyP%;K+7 zs7-6#3K<~8+%1yDT^sxbkn{-pP)*h%&PV%XzP+d1ouKuy9H`;w&w2VcmM09)gBj zFY*eGXb0kBG=~kMWn$E6+n6jgBKdEQnN0>W^Zvmloz*_}(z$%5)UasXn%j>DRkoVoa%4?&SQ=N$fcH1(UQx0bo}6(UuqoIk&RD^KpC%y zxU{i)hlDpOKNh(B=oA{|mjBOi+9DDlT8^9CJ=67Ja6;ZNTapO=#7EL$2fVwq~b7S)^(}sHg$oQ2xYYT$-cgFyy1ID56_1j3Y6|=9`pZNxnm z)#`zz`t1E&??Mt-mTIi#db0!kHds+FVXcCt>Tqk|W&SElsxx z%?s5ZP|Qv&CBrkH83Y`g|f|0KdJjyt8jvDChemu@esSt5QqRGbXA~&`L6dtRWsw6P%;QjOt zw5XW<=y|mg2J_JfA^&69v?t%_ATq)rwpS5KfS^G~5nni;^^KV{mmkv9^TDD;=B8LV z3FRHOUXETmKtKXWeSq&4Xdszh>(KZS)G-)v0It}`aHM2q@=QyL(EQ@-ZVtKnBcfLZ zSGWBg{dB6K>JY=6tocoP8CWgzy7bVfNtv5+B0eLtZpf3!F1|syku?%gNW{AaQEjbD zocW{o4Q4~}LE-Me$z7`AlzXq)kyprQOe?3x#R_^M z=`afmP)syN`$zDQX0f^6T=56w+oi^oDRfCp>hw53;s zS=2vz{rdBas^Zjv08!ER;=-kft*bbn8;SACBf#39K&`u;4n;&A0O!P)9src!Agq{1 zb=IA<9t#Z-=I`vaIMN3Lw*TW3!i z>e-`uii(Q9;@(CmFnt9CExJr6A3f)Zs=^8qe4A(hXiD9+*oZ`{Le<7Bn#X4PrB4`- z;c>>j1*$M&gh=aHc$vJqD9uxO^*)8KTfY^trXEq0cz@9^_Q07kbS*?9uDSEm3a@-rqL;M9zV)8z%k4t;g>9yHv4Cj z_)b~wv@k(0cEWgAq51v+Z}DvLH?`%dZ&`Oz4T%DeNqYV|wv@{@Bh6RDXhbfIo2#s7 z7-$H==F<7=l2%k!#t%zk9GcvJf9ne%E>Q_rI<2frEk1_*{ru0!Z}+@L3z^L`^{~p^ z*ZTE1T^L0(wMlqF=o^gfC~1zS;T3;_s4^b6NwuPlgPcipohO?P1JDt$PbWg zgsOuAWs9ueOIbXdDFNz8;9p=K!3HMi{PxOd%90iq$V96Ai)mZ~III-sss~wFg2$%_ar0k4c7{)e3}(i@ssg@A z+)$NkRP{S7A}j_FOBOzW#^*W|!7{+U#9cEX@dFoZC~>>E>`j7L$_topQ9vNBV5oru(1hnW!Ps<6$L}PbIM`luiBdpR-#j6X1za|=F7V2>bOJld zX5*q6LYs6$tElh0^UW002cO=q!=%JUf|)n0{d+k&jUiZew;lQ2*y#n_m=h{+%A9=n zpX@;1b!?iV^Tt^($2yH_<1n0-LWUgK^+4jptCCJ|q7C9|B_XMd43< zy0}>XOIPycn~DsZkr6krmF40V;hAg*FJg)s^SivcwYRZyN8|0DcAw`VU5*~<>rv+O zl_Gre!5Wv+kHglF!_`>{v_Vp{OlUt&v2_ zi%gC9P z8MRMeQHK;bwIZJWxcKLOBgW4OY;Tk39@GK#*rdsed3@r?H|US>$0m2D(s19 z*6)9jV(Hh@ClQXp;||WE_)POYaIi)7unso(9YZAY9oK&|^^Wlkq#vceI&M{gWpZQi z!Q)#7!l=gFxi;|br;cjp!|->rr{v{=9x1lpZ`Q`NoTi<~iTAKYIN70Ho>Dv7=SQZ` zkLJHYfi_|ew=f+(LK1*r+QAtNOoRArRl*Vsu1~T5{8ig3WKIPRpFW6&K>3-7DHHWS2rH77$zBk#_O~FrEl~UE z^@goB{-TZI;f@k!-OBD=pbq|5;c)@L|G-tsX&`C#u<_Po_|1vn8R_kXl^8|{7!K0l z!>`eW0uY`?O}i80ZO!J zbbx01yC8!jcSTK$r^!R-iRw%s)%ci2*Q+P$u`WKGSB;8cvsP(l#r*I7MJU7q-zM8bUXvx^-ckxPE4ep+a+^_l$}ekQ61d&Vfp>$?i*W?7>Rut^KLtoZlU}p^a=URRLY|3U4}|on zn)GWc_uMhOeOsn=o36}myESt=;(7b%8>!n8tAhL=_0cp*K*5Q!w!P9(E26&1W26<5 zOXTBJoCu3Qtapna@lKQ`3-H}G3mllyF51q{U~C*l5K;P{^((A=_y!>RFE(OKamy{r zYx(ugQ`vK)DlkAbBm^7J-+a(&_SD64x(B{52p+-@pWDz8Q<)6^RKlLo~-oS_U+!}`j ze37@*_xDfV5UmUADzYproabDI zuOn_dJn?poRSr-*H4!z9^d<@hkxaBbrguVL_11aYyCr4dKn8*;l>6@s!f~uV zCYfI5{G1QZAP$WF&gU&<%U(NAD&2<;AoQRgwt@M>~0SV)h4Sx<{QtBs+)PjH+>034W#i z*6gL6`gSAxmtny19n&4x@N5A~JOTWjLp1Up2**g55@52Nh(R-^N9J~akkm;g6%4zw zWcqPXL@vPoN8|I}QoBMps-6>)`d+0|7M6Ai4L%|KRZM(WcJfGw(fWd1@H4`E^YVty zX56(JQFvFP_vDC*&GwISdbyQWVNo2Cb{x_J2u!%&onjp+ela5wodDD9xs)F8{N8)f zvH~LxFQd|@0?m6TQ_f@4@S}fRAph$+8FzC~Ipbp|iL~ClXg^%^Loxn}Ozx$=`22#Q zsJxPremYNgKB0yoUL=Z2DWYA%^(NpXFD!E{;Zf*5;(>h4y3KcS^B1jGm$)jQe1sA5 zqR8{5i0qo__K?7mr#GtB`VJr1_Zn(5_?&KruTSS zj8a|QqBZjp;H(ddXwYcBhJ>ULyZ~(dMbRTq+ z)@Xu=MsEn5KkXC;(sMODOizL^ijp*-4!ueU!_7tub87So3`u@%J`8ta>wbdAL191g zVBuTzj`{awf^N!LnazzV{4vGZzO$;4C;yx~TGCFkB4>Vy5O^U$-luoALNHMlc)e+e z(INn>;=U8x@4X{znBs3tDnL6Jar$ z+{~MS#poJt^AXa7`EfI%)!u{lZs=Hh_=9V13psZgvcu6Q4?If4tdjtlQjYhdS;Rp_ znDq4fe8zi!)`94&_23ax@@{$BDU zkCr-gcj9-n{$54jkDs@IBiBng0C$-J2+uc}puL)K>#W=G%_N6;``D*7NY=*YT%WI| zoX?zOOk$o1YcWH^&Z8=QjqSpdM@<~5#u^f`O7y(^qdyJFrWGtDh>f#!IOGNX!7{BS!l}?(E(*~KK z-M`emDE;yC;!q#>^@N8}c~_mb04tEt*^bvyjp#jLuEAzq-PIzu{45a;ukeNkBO z1!Wp~T@=<6cNrA|4&wE*;Tp4P=+7x)_Y8A|VYIqgUZ z5~SobO;bcq7UFd#X#`E?SDG{URUW=|+3lGL!C*e?z8@HiyHcn=-*fS|I^na~8V29PCQw$-?yebr7Hzc=;I))qrSk5f zI++=w8`h5uXJZR+V+!u#)ClyIIw*lJw*@%G$`M!>FJw$nnWfp`EFIu};G+*PigONy zF8}O^_+Ei`K<-UMAISx`J#>^|1fyTvl`NNhY+KCfA&zyII6|GD9Q<5@_`0?`B1?8x_qqDLV&2DYNCI0`@_xDh4BM9ext{+; z%|U^%p-75iRv1)jhg%d9Xm^vb2SN==#bdzc4<;oD{IyLA-s3g0za>S0Ow`}d+(DI? zhxeKT7+TtDm-o<~rc=ml25o!fBK$EAJH`xfypJ5A5N9yNvgMthmN_x=^3 zpp-X%B#e4d_5Mlnwe?STX&W7+i86yfvPgodI`D`YT5_VHER2MfB27zK{agpT^&;sl zHdhuHRQPv&RKGfClJg?7Gg0twbfLN@n68<1-WhD{#G@w@`W$`KCTA1i0`UGIa}3X+ ze*>nH=RGg}!|!gFdU`uf&yfjV$GePMmgHh>)gA6K@Go@bF#QqP%Mo{iK3i-eg1*gt zRQwF~MTmvE`$Vwr_xIa*=3*ecr>?u9i+PUAhYYmFa}S-VqiB8ilk0mBgxwybOTARu zQ<7T7y~KLWr={g`)YAV2tgekS+DDslK}pX9OVYfZ)8A1<5&PIU`SfJQA|z3)r!yp;7Z8MEn@EZyJ#e?97F@4_6`G-|+lttz zBX*GWEK>vLHv(vwNymd6x)Um43sGpSN_Fc z*HZ0eN#uM+=orsPO{Oo0E%3NbUA0r7q-=GJ-WA)E@E_D#-zTWs;=b?Ix;^16(w z?MI?}_e=av9n>BC?%rEqbz*(@fS=q55ICKh)OvMB9@Vb0d{9tLqkO!rICk2(!Z+74 zectqOWI`*R1%E6$g~`-#bcgI}SqWsW7(@!#i=G6Ui)gC8YlasxVH zRs;o*ba>}(`}D~2?e^>Mui8z%>Nzc-PTloe;e5vREK2NzzwMezHd&@Ed>I~oxC86?#MH>0-79-SKawL}jhdsyGQj)v_MXk?$BYgg|6(zHz|G(XNI z$}(>7b;z6!Wx#C8_4z=YjIUf+v$6wx`A2z2IR`>0O*!wszCY^z<7JmHg&mwcn#|M7 z13dAC4gE5+)i0M9g}pJsCgy4l=U>eD$=Q&}`O;z*H~00|--++%E8^5)6PMTt;x3H& zzD+@_L~Z>anw3O8xxnFbUmm>R%fV)@*Gr(K@i<#3-r8aZfR08JBoh+&E%bt-wv3`V9eKvAAX=PCf5yZKH(OKmk zZV{_-P$9yB;-2#6?Hf8hH=FOj{3ZE7cW}*Ye?>)2;pe#%ug+a|f})MzxibH2I~w8Z zDDg~w;yyar+<%(kp_;ZZ(WuQ^RANl-mX_$9%FHObpdq60QgNWMWTHqCvx+<0CsOK* zCV`e4b%MCHOIg=(uCG91ImP`4R74h~-cgBNN0{(+t;ZxncaSsUtG~>Pa92iQmEIPs z1=@b|5iS04>Y2(}V3YRthA?vz=QiY{nB>wG^U|}z-LCNja8NZYix0!GmG|&*_e@;R z6S8;5;|+zO*@5n#6PMkO-^`cDGz-Fw$@Q5X`G3C)J9r~Nu_r)dw{xyf0KV@Fgop+h z>2kopc?mDvceR`K(-Q|Yd_x05UhZfa?uQ{J^SpleMgd2PXvJM|o3kywuGS*2c1QAZ zEr$n#eFQJjEKM~XNRZy5p42W*ZCnepo6qgc z@vnSmRnG_TBLn~xBv!Wq?>51lvob^MBk;G&mX2j8aep@t6E9z0=9iDK_^9*R4^B0) z2o?Eb={M=yZYvhy7tKRFS_nE06yx)u!gYgQbUt<(3)>=@Z9(&mlKY8BDYCNoi`mvA z$Gx}67s1%jDwv$k*)uT%>0?{7{HaNLiO$)78t5Mr@>E0*2$%{~K^crc1acWR4e>RK}OR=rNx>O)UY#$9~qYGe0Fxp85kONVg5(XTjF! zW@ffuV867!-7h%)x*i0(Z2$}P!xm;-S>}3Nrme({JHEW{7SThi@hHWye8i9Ug)5XA zo+wdcmipE@v0pHy6tBCZs$Jw6)Y(`|QwT>tV?tTn!BZlL~^lT19CfDEG zFl?SKCsl_q-N-9Uo8N9aizPSz$%nT4y{&SoJYJjjYrb6%DtWKFO_{OT%yD^VAuTeR z$WE8Z>&4%9UF!_8b8M;q&hbm2>To1_ zpf|1#j=5+Z&H0h3eE7Gj6MTY_-^MWotKn2xFL-lw2t#j8Y5wKX3sI0S24aF_M*f%+ zX(6-CbGmDA_^E|Y9X9oXei>(Du5g5mJ6@FiGDVgb@W)8=_3a+quz%Ekgg(kUTxDc7yy`&!8rj!$mE@prWwLCpxB&sI`h1O5rTbK5EB zlX{=p9K&!nv%O-O>kDfm*$G)olZehQv}!WA#!)>6MdQxa)BAJAd>$*lqTB~hIGenZ zB?-IQy6EcBomRZW?u^SSMjx~3`I!+SS3oAX#>F1h0HzsUiLJ(;Ixz=h*9!26kTUlmG?aHqr95t5c3#q|IQEe(N;6P1nHc(xO+@+k zW!7)YYA2I&on=+ubV{6r(vL&PNzX=9o>IZmIpZOk!oN%_`-UFRFQ;ndICqQ-fdIlL z4HYV+arM%Bef@60xfl|_oj*P8JZRL4W+gN2UK{L5|9WP4ix+879$uv{DNEWX@YtVYS8Hn z^(xD~lr_ZF?TO}@>-{G&CdpXM8z3?_Nu896bMpWm0ouMi32498yZ6c1Td3pby-WAd zR4_L8y`ZMp;XQs=xX`(SO3cnV0zaW1osgT1H#rfe?TL_nA3NpsaMd#;J4m^B4l(>y z!s1HHXDVNK$;{)O>Q~rf9`nA* zl>}>9*q3nJy`sSq36kCt84w^mvekjdEp~82$@k5Sf(B3uc(;;m!Fb62j%jev1Z2t& zcH~rk%&YEFDpFMJNxGj-C#ucK&>)XT?YwPEjZC(HTBUVuDn+H0Y0GhqdZ}`+y#Dz% zIT>Jbwd^e7*$!H=-BUx2T?}ezX9m55}AMD_9j3Jf*$XaEoQj-wxtkSH$Y+0;8ewTbZJ|BFWsuM;9c-nzhdLp{b%npHt`J zrsfSf6=4oaH!3K0k{o89BY8BegXCOl3tC0mPql^hQu~s#`LIb|Kvg9~<6uC zy{W5dE444oI;IXFtf+}lCv9oUcvH@bwwGT=52comOm}NNry*Y;CA*eJ2;?mOiy+}! zX;Via>GnE=#Z|VeOlfAmJ`O+lEDm!2FlxErAK3JY?%MSfQ$nK!5Vn!>anH!8#p<2H zQM{G7bChs%Kk4yKeoR55`<9N>P?j|BYwXmFc%LY2Tf0370GM-M(Rx{ri?g{9-4lCJ z<_`Z!3!LA6dQ!vZd6O(i?iw%QQ+{~g=8fAc$+sWY2{1mpk-Dx_4*aWfu}-Y0dRE}} zZUpk~h&mR_fjBgAVkjE*`++>dwpUFeyGRAi{t=2+le1^0`mr4_vq0Ck`@I)W$mTjr zfVmM;hE8*$qtN+h2hVfTJ!FJ6p#i06_zGc8+Z*`WPtL$@a=w0a0P@M}6Zl*u>d-8B z@9}ELaI=@2bDr<364@EQYY9j6)SIU*84}}T)ov$SW&Pmq>;y{11*RTTBuaTl(Am-zY7pOx-ET|@6E&C z>xJ<7r`$0yy1>-#rW!8d6xA#{o-EY-&tCq8V0VtY2f(P zJH@?C?mw|2YDb=AdgJeg9%|p!Wh@0NprCx=U|E%(9$PY_3y}wOVdZx}(gb36(n|Mk z%!HTNbZf>OuQO1O!d04IT*8Tso)81klkJ%YM<|DV!`^(JGQ1RT)-U+t*-$299gD1} zG^GPTpL*Kj&zgxINg(3ajq=`0-I`DANi3!r91vVJ-_1LLFK=565l35bg%talev{Ci zhNec+_T3HNcEsB>!f{c)^P-_yfJh|Ea=vdQ^;xAB3UQdu;|2M*Ix*!lIW}hCUBGc7(W!7~Gz#mpMR~*ON_eevmV#MF66}|1IxtwXAIS@A)Q9CzLwiot_ z{TPthzRP0h{y*VsBcu#njH3kxG~FS31#ZObE&NzzyBH_Gf*iev`F=vy?EZ!+qSlM~ zTMe|B$Q^e1FkpM#l|3ByNgdDgzj>RCMv2n04bJ~Mb`L7SP$jo?g?pB)xKFJ;W_w%% zITsa6-Pf9S|0xv$Wgm;I9W5rk!imX4lu3WrNowrJKW$0Od-*>eiz&oXX0r&~O8Z#K zoaj=Mo!snvQMgK5Kztk_zUuw|ReHuIRG$>#gB7kz-ycWhD3<`ARQm=)V{4zv3S2YEebtH3uJv7(B-|K2rIcTbc4;X;n)jg=3Wgx71=}&l;|L0HnpQ7)>X-VgPBXW zN-mE%?;x&hlTEFxR(qckBmsO^;|cb?(Danr?>J$aKYdG~34pJM&PaW^jja&dRb|-q z`9(5#a*q%xc0iJA&e7=)K&<23N8o{MFSz%{ZBX`p0s`D^eham)Qgcp(EL>AvrNIw}!c%smp^|?x!XAD8{u5ELBUh_Yp`6yg|hML`ZS`~p-Db&h{QE+xW#^cDV z{guGZ&KeUm%*!zON_`jGO6=X+w#J~h;FIEm`-uK`H5&QErnjL-QdyHpc=VU`jjwQ zqz4hXk}>tVyy3D$yPaKI6hDCh|wX0>n>bb#YDDEv^DEoxZ&| zw?s_{)}jtvCU+*ielmp3r&#F;`Se_|TL}#%)PE1XGensL6w7-g!%CEfx%>6V4Snx6 zkA33e^>@c4Yh_K1AZ&eW4oZ{fWAB~fI)+`i;C3*)eezN=#+T?jA?aFo8A}OtAr2pE z)Z{rZ-9b~+7ZM)!a!f zM{2=SQ$9(2Ik#=dMOX4%g2$LS#M4+qy@P(+O2@RWAlfnnCvs!1%mEMPkcM}fFUi_y z-u?Q!TG{$Dtp}eprP})dy~t-&Lquo4XsYaPHdah-9-cC5>HS^{2sA$6bsCoGxtw(% zr4sfGDF~fP%4tNug2Hstuwz2Ql)9I)=MH_c1ReCaEV6;npJYMOXaw$4={??dQac?~ zC*ZHJ0GzgknY=$C`k@MQX&|WFfandWC{8AFb2Jp0*?=B34)h7$&XCpi$@j25u*CQV z6L})G?4KB60!PvU>A>G;W4PCer@Zj@^jC|s7cCj3VbuILC+W$Qd82VH^e=`0d@xrv z!(ESMLBn?>1Vg#HnN<8j(5mJ)!Ns3k>N09h6$serQB#teMtE{WYOLB8?T@!)mmj4T zUZ4LAvYvQ%9bd-6he)lyv~2~m^s`LG@>-PlT?jmt_=AWua}{st;YuCfkN#U@(EN-E ze|rn)`>TClROHf$z^7TGyQW{`La#-ZLvVcucmd0I;et$VzQEal8Xf-4(CFfRXXs~j z?bbN!DDa^Yphq&ejo9J!aEoa+X=e_u|H4x$841Wcqq>OH2c zY;*&YlKs-yV;jmjg{evPvG3<<%ZLXw?IfJR+zM2mbj3wkjKuHBcGaiLCww$h7#_&f zw|Y~F;f>wBXGG%r^!@e9#-hxO&XE{7(ZTFZaypX6hAlQYF`{>o=JX+t)Oubv8%8|6 zv#02bL<#2r8%L()a<)~A2U1Y_U|wiaAn5xy3Ejdu+d5E7&rRu)?L4!`+kH6g^Fb+Ye^_@XtfrVIhw78Z4` zQ=eCuk28z6Cenj17GRAhLyCzU&qrlrSZ?4+Sop3O`*z^!WMVW67ij?CKn?I87vsmT zJsXdJzm+PAawAk0(ihQIBX@O@@+9EWPdKo&iLy853&ns$b$z4=MfZchY4u|zZdN5- z_b@Lvz);e--@{#+vC+_` z3Aa;zgL!FZqprg-dTwP?{@juT?d(^aaQTxe$l1PKn|^aucA0W4oYiw$=}Vcp#q>fRxH-~z z!)9NW=8!V$7Xz&WES;Z5rO9VBxKL$k{2sov!mE63$#KM$zR&Q2mx;X!sFEbN3~1oQ zZCzz}w<(P@m~vZ8kvI4RP;J#5T_BLtR&y0FO;qc7P3Y-~*c0)qm@B=q*aXBpb1*R5 z<^IfbEL^6BgVS9Lnm*Ov_N+Qez0d@uYZN}fuqrU{U9aZ?K)VxpO5_kTHEr#9BP$5& zo^I~zSONmpuSy9j!@2~tZ_|E+I7%~GV;zQxFYoz`fND#?Ut<1vO1Upp&pka`x+-+h zmTaRWd^9NO5XvrATw?tke01wCvGW{@9=I2yecDdLay;U-a^bGXR9G-<*tf@ zM-rdR>P7K;wH)~)6bvu&Ts6AGw?7Y9ohU#G79@KpM+ViD&&nO1+HhPQ+tiL7`E9>V zd%%aWtC7F?`9KMUh2d)1H$?!oN+~<#Th)YyvRLUkct6k?#dR^4N_P(S?XheUWZ6g$ zFCWA+u`VV{I)+k#Ye#WG!GHC54ESU7fvhpHG+e`^1bTs=zTv{V`QX{k_fL{Ky2A<5 zX!w?5^z*n0#HTfQ&^CnySp_ zVu&@LUs{#L8d-H?AA}^Qg-k%594W82OnhNhBw9L}>L zHNDnPI=0-=7hLq6SK)93bY0BbEts)=U!>D{$mNsB%iO9@R3G=nb@TY5Etp@QzNy&| zbytkqf>k!pO%cW$#T^uz8oCJfNJU zTq!kfoO$vSMKeS#fcUG5LUWVxW3$>r2iR+I7fxJE9lDPx*X|q0l5uxQ4`BRW*aI}95>(s_+NSGj(Gz%J zi@am)aHpI1+LLpdOyrnoX)sGUfS}NN+V_!*V5y?I_^%d-`z7LzBdlRmutduyyvWXw zb2BmZBQEt#;5UwWK*KR6(~-?>Qq}-c`IRm81xs9mITvn*BNf5(%}~p6xLh|0e}M(O z^B&eYX(~ux_wIeZeCWzEb!N=!<-3zXQ(Vg&k-zxf_VM z8bWK+dwP?xWwdX;aMQBcy`5=3SH)ajcpx7f$6LOS6pofHK&^GJU3W0_TM0#6fX-`K zHN~onssZa`5>D%wV>^ty5gJ-IJ9w=B1|IZ}?cqR5Wiw^oE z$3mVcOm5&MqZ)Wd$ACyoFLZ>}VtfW-ny@K$ADZTfG`X9=q#TUSpuO>Gn~{qxiTLy%+I#?QVqoW;HC* zp^6?oojPa#eG0i>PB~<;_-+!hDxMfLp|5)RFu39h&GVK5@EdV+pagN7S+AL5#vD1; zbjXFowHth?shYHO1jao5MpB@DG{?edvV&?O}~|Eokr*~(If;o`6{?q@VKoV{4sOqwfUZX^yMdo{j`%y zo23tv>P&A)?^@<3J!JnpH(jvhOJOm}gULRNmF;V@`9uPtYL&^EI(-p@_gix|xQm~! z+Py7SU{b+<0LLlY-c|p1&ZrwQqrvKBSJNX$Mhz@iKnmtwiJztrO&Qw!g=O0-YjSp_ zx9%;{E_-?z&ohHlh1?ki^rbgC)+c9QC6_TtzlPH@BzA;?p8^XdzBwI1P%^{r{N?;n zh$e?N5rGZqYWE31@+La_kQ8#>a_DtcHR42^ph_F;rr2*M1tlDMFw{d`2rmYgn|~DS z6E3-}d@-0G(ri8qJGS8lbIa@8sV`eeZBAG3s-VCb#w_lgTnV@AVRGd=rn+~ZsR5G6 zTPtmKi*po7J8zK~1u2O%UN-gT$09w??QL`oJxh!JnnBZ;ul{Xy0}n`)yO~vWPXoO3 z5hAF++4bH|ZW{7(rCei(jZcPTk*l6KUY1c+#I9{rFnQY1w^Z+#y>%g~fER^_Yo*Nb zwOb5wR?N*yF4wNw8Y!Po-;Qym90`MA6Wy zk*D}J{AGq{IzQ(Yjy43YLTd!^?s(=S#(T0jS9(HvUO3_#;`76az2vL!fP$t;rI2of z&-KN!Qg@Cg-(zWJwe-G+B&V~0zoYa1D%<{@TPH~GP<#bvFfB10R?D(>H=k-1aImOp z1qrc89lR02i#S$yYR?d?-i2o{P8T_QvHMAqAMNxNhZ%MP6)6hDJ4S)q$IJ=62^pC|9xrFEDuP zg=IVG9|?(g@Z#a@7w3VL)0AhWJOQgRU2;&Sz%dyx0`Ojg<-4J-wfZP(`^cbdJ!&W6 z?S)qt$FdCDnBY|uty{*HfjHd>yl9>*`6II<_!oI115LgvFj)^G5Vf%^B5)JX&rA!Z zQu=l9XQ%3vb0Yk1vR`#G-v{HR@^=nOI-S$qgroE86fTH`4}I(`JHZO4@J8@!w~)_# z7|B{5jS3<_h$x)+Jo5=3E@6!x*In@uh!L3Fd%xt6){(>}2buq%6vB8YU37@i(UtZv zb|Gu6R_)QHVrix#7GoQHoV$ocg32z^?r>AkxtWI=jp#v|BMeTg{Hyc2L*eUI+Pv`6 zGTBSwx%y-Kk$pzxgwa5Y^bH32{}>d%e1cTfe>f>e>2d4f+uw}A7*KhdO>7&s*S0*3 z;c6%d?~Gyv@&$a}zhco>@121@f;pcivmkk=etJPegc(@xPx)%hxAu@&(@oiB0fey` zezDg92`_!m{l@!T@O+K!#!n&VtBg2!c2gt1@jkb>F=sWpX7vz5sT@}8Gbe-l*~~Zn z>KhVt5u`C!Dwy9a<4NTML}qlb6gJ9c+4KYLL}cxdPH6gVEPOYn?{+eo-}ERIL8qLlinmIArD8N;a|2VmIm;VcP_g-z^h@k7qbw^3`3 zy6yZ>CQV-MLd(|CqHXr73PwlNltH)^bI$ts7|olxsXHuglvW=fV%#L7h^ls?Gd$UVe_7dC_)NW2E-&M5m~~EH3u`scH;2!IriD@YZAV zl1Ab*#Z4%3)+uj~kAS{%zeG~Ao!l}Iwr(PQu$Tm#+t9>t=Z1l>LM(`^&8D!RjBs$2b|| zIbPnr{6W#mL0bnHzmnj` z?{5qVf=I5(1M5jW6RKS1#VMcV*Y0uhv?-m-(06wX^>Uq|#X%=|ffKTq51QPbdd=V9 z{ol+kyn%c(^#cr7{2IrB_|!GiGsop&DVZ1buCIB;>zumG59O11EePj{K6j}b%X@8n z_57zbwfmnDS8nzIt&qnV8ccD=cG<9vQqP42FZMJ2$M)T5S)FKfrQL=R%qkm^{foil z$1#p;7Rbg>PJSP<>hVOMNDtM&hw-dP(6oushgm_AM~nT*W%IGNRP3+rPj-Jm9{aeN zDn3@~?djtqm3;eELv=7vXNbh|4z$Kq4RglnN)ep!W?Y^~_RfXiq!Gmv9X$7v8^lXG z;$P_> zt>H#^@)3U3gq&pyHd;%>iX%XDG5my-j~(Lp6#mncQtEIyYSVSv|JCj%PTAMh&2z3u>Pmw*)fAJ5^h+B2K;xvo&s03eZ#Y3y-QQFDjTJ-Mfm7_73!IXzboGGxce z>1K9ODT9Ozs`;z?m$DW_5h1gJ3vnnyZ#f%={65%~@^dsRw`^Pp`1)Hevc?|NmDO)? zlxm`PQfH!?!CEC}&EwMv|H1zJX97(wv|CWrN($ghVKy#wCZ&&m)stCS)h{O#)m?*Z zn_;;FbmTG_;gK~xW>@K3p`qySdti=HCB69q6^PD`!P*PB z;6Cp;=ES+B#j2nB;bEP9M6X$lDE{7pg#ULX0Z)T`+Ti_DoDMPA{SR;HM7Fn!h(Z{y zCMa;B6p$Cai5qL#UG@CLJ)MXLX{21=Dty5FuQM51{diZ0|MrGBaS+HhLqEezEL?$|S!!dvWRfqL zM{lO1Yj@0rgO6jrrsWvcdsNXn>l?AW8rSH*nfMFLSEfb2FJaa$r&B2nzzz?J&e@O; zzQVuV{Kyd2uOUE#E@GLO)3V&D?oKtMwASg;&?oJ=_vm%` zIv9XbwWH5$XVOAtfV0|}8tTSmXZ~i5mcR;cF3>N)bml2`8TJB4edm$T?TnHCSr?R< z=-_#Qn}a_Ge;k%<$omY#$02=_XIxiA#{AuUWubhN=-RuV11d!xvS??!_fPR4R}9D= z=zm#y5i4he33bkzYKVDSA2yUq;Vtm<76-l`{REnK#Hk$)vwMX@Fz`T>%4&PTO8OTRU;~N2wv*mt3V?{)d|v-^t(} zL(d&CKi?;69E}cjBxnp4Yhdc!sY}APY*mXVO!IyC--bXVKw#Ih_YyV5*W`1R&5N** z60iEioh<#Qom?)lE#*4MNw3vc!J`#da?iy)xDd{+vrSH+$D(C&Q#!aN(T+WKwgfkS zrR;LBd+BPzJ?m8ld8Z_F!*J=p;Ue>508?w9)$WXMPheJa*M%|nxG7l%(JdYwPY_CZ zCSH2uzqw}<+wHxCA7YRuI0d`SaQOYoWnllBY$n&KOoWoUI*I^d|Brh%1e$t)V= zgEh*AnJaTm90#DEnAWW^R4@i9ZVMHUIhotaf}Z9{rG`KssO&lpXRtA7I`Op zCSAdn;@_q4iyIecP@h^1qK@F3&>&=XmfF#pEf!70V83D2u#d^LiVR|Uz3W-R z0eD=0Sz20IDyUsZDuwH^D{ahmvsZ{|Ve@ui@xE5h?qYeG|#)SegX&YBmQulbC5-Y}~@&b`(s9V;~yGrVT* zI-1wQ>TjRhwT<7DxYI0jw;NAn9<;oaxcP5xu9%r6cHcvqW&6*=?Sz2$6%)j9;@ndo z5}9%(;0&3qM_D3$4WWJ&&hAsMj~E{fSo%!{y%JKn@SMoYhu?(!yURNCAPv(fbbn62 z`(J$?zb-`ePN}6DtCxi2usr~#fXYK18A=?;^vE2z$i_-4y{1Pom%Tpj8 zbeVQ~-N3O1Q}a9C8a!b~3>$Ku$?4)L(2WaB;;zqbS^btEtwgT_4*JaF}_bs7! z?ECN@-GhJU#P@b#kXcpD3QR|ioUG-zZXN)|Fsjperf7C`$XP7<7RWLPnh?noT10!@%Y&! z?RshTNRj;1Kp@*ox(zeP`Y)=bxfT}PchZQP&Pgdfq)0?C2M!5+w5>SUErrbyS>!r8hQR(ZGkiv(BSv%ab8?PAIM>78>eE+Pnk z^)d^I-EB8xej-R;?k^Hg@Wn$R^bCZRB!tQl55|boIk3!3e&}<;n--^ep6`+{1t~mj z#__~vA=Tb)H=GFP4cXrasW8Zk|pn=F|9j*HzMe?TL}Q09JH53lj+gh;|*FPyr&I zcCk!zOk=8V14{yeAxYgHIQPK%7{wP!wwhQ&by+j_03-q9A&#S0edTYB%ioV>TS;f% z6*mAGO;bEK*%lX*JeYS1&dt8WYfl59ZsaGRjf;c`X;z5qiI;m#997+iYwR4IIT9AM zb*8BRDBQ6aAPpf%T{@p8{Og}cIc>;Wzp{2LOO#axHjw+|Y#W?qBJ0VN-X5{yQ)@}%Mq zk0K*n7aAH~TKu=_p}LRu-mxih7e2-tdo|V5HS+H@iw^8gt1_uijt6FtDd>VHxI4@_ zXvsrzUltl9X%!6KvODnc{X(M~=koP~X|6;X{_f8ou|9ud_h1)Ig71zZ(^heZWH%qz zh%e_Z8h052hjrNh({(M^)g9K^Fs3s+dJ>3}^Zb>5P%MDiVs#0&TIZEMFRj%8rifUI z%GIoTsTLbQa^esb+sK}zRjvf}4pAM&Cc0;;)~)q`I{8_+KbG`?1kFU*ZHtT4YB z{Ec}Da!1w(egzB^$zG&-?sNlX;P)g>20n0oT~?pX+dG7*N5O^Qz&DOHzrgvdsc!nM z#hf8474}p^+basQowifVlqU^hhhB3H)!09R$CnWF{yOmO&%_@Z?-)hV*G|CtMJugC z-4bSae_lh^&5Pk3`7P9`difqeaO4w(@u8cGwRDp%Hy1My1AeB&@8GW-r)G<| zFR5jL=8l&qysy#gjbmeO{h26U#yc7vdn}m-xneFAe1P&kqL@@`^9RC@ZR%q*)k+#% zN@``KcaFF;71H^0S)WR~vW$qgF-Z4qCLw%|PO{V0#&;!XxOB#xXA(v~4_dHb&9*$t zyRu*6O9c4!L-jRKv2l~Ma1v5y$J{fK_6KD)%j`fyDIF0;FK}~nIx_AH*D+Z?a1wCe z5Ufb;Xa-PAMN^!fy4R}YY!?;@|!oVEyWneya)6gvsD$BIG9-6<}xuEec{$Wl2 z08Dy4I0>JYTJu#$!a^gX;|-&_9vMS>oD9D|p}I(!tU3z6Uict-klUk2`~W-dQ`&wX zmCL%5tI8Vqh#NeGUCx`nNIGWzriQbp*ap3UFGZI7U}(~G@y`0UueVJeW_DVS5J#(N zWCR}S{wW4vE)oM=7b??;6799JDzifsphegT*}Y>m$$rv7M@a5}U%_?tl@43GZ;*(d zy%E~2inqLZ@YW)ZOx&EL>&SU{0GUqCP2%v(CGx2&i3OmdXumkqoLbVC?i1g#KZVor zq^x+{?nL&{la7c{uY?r`7W{qbJ(AV|#l*q7>q*|)oAo91!C1zBp@Ud=q|hyae|Iu( zx%T*_a;Fqdx31B+%b|G45-(WaK+EHeIMqwI(J5 zvVCbat`8_`cBoGL`?-giD>^|mt@%ILwMg03FTg{Ku|=(6eW*|+X4n#lQsz3nUcE*h z+1ANztu3Ov9{EvbEpOG7Q~^7Z6uY^{g(wo4?u@F|1-J}RLvPMRt2&hP%%4F3xRw!>?XiJN0|j5y-llg`$# zM61BOul$t`dyGe4ym(B;le`aK$B*1v^-Gpa?X<-gNu{|mZ1f97_~MPNtaJ@aLsDn# zV*>d5s8jg(+&VD6e_4b+9e-o&V95urdVuX$dusnY>!8j<5ABVE_l!2#AncfU;c_IU$XzREjrO4OMN?P`2W>Ke zcPn&Wx6v%L`i$mETiq>z&77#ch61>z#e#iW-K$L?buQtTO>Nc9R^;7#ot#GAEmo0n zdTmW!{wxlLX{hCc- z2g%e8%g;;}F%Z}ZIPe>z$d)81!bw z?eHU;k3B=wsQ4#WoMUu4BFJnhm+#yPU5i}v|F>`J*Wkt2U2NEXxJ6~R%Ma*p zymuWp6$lM7M03ko$^Y)@t!vHk2o7ClF!H0~`{&qAp&&ptJU+&vHntXWjN z#@l3CwnO4lzk;F@m5@#rT zl=m!@o&a0OZ_HuPnyqo-qPyP;uRdzK-wYIUg0yjxCt9!!P&bu z)f-3KpK(|CnDU9~VBsrJ7YS@2twH2{9)2v0W#Egz8$7p=l2PM{kVGFGA1{Wrbi?ch!kH@VaNjZCSR{MWL;Ku_N;bbT>9z@A7mycA+tRXonX8?u|xK)Xz7z<;l- zrHB25)5+*i7rLT~EnUw+^ChA+%M+JL@NXJ78z7yz4tOdxAm8T*A4TF=dE_W>3(QF@wn(d-VwDU&|wHJ3`lb` zb-f*<_4*dS>V7-FMEJ&pglF_69PEDKyxdqGz7FJj*V_Lf%be_JQq1?bwK3Y5Q-(g} zmEBhhw2;2-p0X8ZF7S_cJ6`@S12aOVsC>|U*CgD@@;sCD#HBY-)pJ<)@15z~R+$Qc z@5TNThLSVEDdp&uMgdE#_NyI87Us5-MbI@sHWNJzg^s31-{17pXYRtpkfx$S8qjP@$G&- z^7MNJ)1#X-$Lqs7S;8ilXY(Hu_lE7Oyf@_TkZ#8==aGaNsFYyEh@l9cCq_+f{_o?9 z4flu)>6%)W6l6CrGP8^ER6tLP=NCdwRuf`wGOWdhMiA+Dp|I2&+C^961j#eW{CvIL zIWru|jY(Z3PrTD`E;h7j+{Ss-3_) z&tR}MUwBe8_G(hJ9mnd92iUUUYsQ6&_)dtn6OUfN=!dAF=dQO@>6cDiS|y(B6_*aC zpsrf%r?E@&K-Ix2^T(C30MG3*ORD&`|8&%1H8P4SpqA}g*`C4sXcelEM>uA#z}S}*_iLuS(kYx=7k8Pe0Nj~}wA=vgDCXZfvTE~Uc?$v3Dz#bO+BgM-aSxYq~CRZV&ls zPDePJkMCbM+NEP8cWz`FxO7P0kSfRAdv_5x@0*gFYkP!(r|BDyO5R)z-bqGPtq$|_ zl*`f22eg4}@7Wpv+?1qCpiA91xkktDsSFwCY2Dsv=b4M!Zzgc?u&`xcGEL(4nGEq- zn7Y(?RN`dgRqcV0t*LFo2KS`Td1s>XpYtV{va{haLl}7qP|k_#**iT#Fb2mHbF79R zp&5r1Hgf%9s>|*;ckabj*-xE!Fo%;^}z0`od1XjKKXF(HC2Z1u7;`P%#Q{Yp+xSva|03u~X z&s8j47s+sWd4JyXRUgHXK&kTA90wHqW;Sx_3lu*RkSE;llGU>{6!hDYQva(K$F=3p zD88stUw1zdV>yZ4^U1!b>{#m*=}PDETZ=zzUJ>yBw2}8y+3w>l1Qo zML4V&WI&l-P)XVFDqQ|g&Nos*CxR;oP!#;CZDj>6p^sX8u=;E1z29;ZCurO%+2 zo}dMGB~zO{e%`{!7})d7-YxlLDL}dhy@lYU!9(kKxSgJyK4rbYq>u*PTDTU7ed^!-DxaA!Xkb(Vd8P)Vv|;-|CeTZaZ$`(q#8(R>>vJ z_iK?c^)uTRZe07ay0_0vZM;?1gblwAJ~tek@w=7%6bql-vHxd2ae2qpIX z=-T;_;i+LUR_4O^1QI|ZNAEjnb>l`0B}p#Wp#r0$6jpyZ{g)bJeW zEVgDycya@u%?FHVw5Svv`LCKmyIAuqRGx_i+rvH9{u==kN`EfJnI;StZ!G6`iZ#?l zF)5iYhS6l`lMb~C4b6c_$iUuHEy)KxN=eu_d~-DFdmXVM`D0^jc<&5^_V*4MS8wl& zILi;`6G?L{h}xqnY>+2W@vRqU|Z-|IJQ4Eezz*lQoR13-SpHBZI!DA_0S1cC4e|mw?C*^> zWir>5EM)Ba7`K>0Awj#6vdEqVIAB}iXh}BOek)|x=Y2aX_WIj<2k_dL1POj|Ffl<` zL@yh(v89Uo2UadjijR;UyYRggy29-6Q_xy+x;y*haF_BZ3)g#8$~@})tmCJOo6+#3 zLK1KcU$o(XYX45^0OoW0V5Ot5|EZsTDg~$9B!0cBnzJw)qNAzaXaB?9Q>2A_GBfcu z%YffTmb0R42c~U+RpG!yO}WiDExz%0g9Oe7W6xU`SK6V@QKdpeI{K})Lgum8{q_9b z8=)c6(Kd6Z$~UZdp-I{j+zECnFmit{ZVz%&m~q8CSTbYR!Zh;hn2T$me|P@5X0IHS z@>k_gZUX0oFKW2#i<(&-TsaHx+%x77s6h$#*}JSm*~`RX%!K~17x&DEyXNB?dpQ@- z;oqSu+ocUq6u;KD1q;5E7a%b>+pF^rIB$bLr-Hn z*5_s{Jz{p%SM1DG=N6{{x~qF0DGcr|gpV}ft&yac+P8zK)1~>WICXLvvH-9@#7e2} zbLoy!P z`|)GR*>0{oU33#35|B77AQ1;sl0EUc-9pgGyPhPOPTO)}_4L+^;+FIf+v0mHO9R27 zH5~U99D57hP9J_@UL6SBUL0eDo$q+{wQL}yD~FTjE%`@0FFNl7Q!hDLZaR@n@~ zO;xer#~dQw87&sQQAE6!j+fc{oXnL9+2`tah`^;Ra=6=`H{{@cIG}gtAB+(DG{xkO z#3p3^=MONnb2A{*QSUfxxM`a?2lYf}T-ljk8D-C?zNnfM)LB+Z->@sXh6SI#OC6IT z9(=Bt?_B2)WQo$8M*N~)p%5>gd$J&u>opJXQW+Aq>7gq)B6sQ7rvIgs8m)iU9$|FL=6QPRRqSlFUx?x-1drX%hz<_X zL(&lNc7ZpmN^AXX3H?V9u?Z5QKeda!x68TQ0}SZG94u|p@1F2>u3d9?&0ccz|E&+t zX#VtL_te+vpx!ls@>h9jK@;4_L8awBpz_tCCOOt_QT%Ce$LSGtuUbn9MRR>wg&psfivw zzmJ28V_0=d6w@)6I6qgPEh*uYwK++5lE*{^8o?toAY-Ji+LNrLVL)Hmi{!E*-*-ru z7h~<3@AeCw*MAQ#j~fgHuSAYY$^c`}O{jc5mHlBf$k0`Vm_&;lw|!nG7_O;4e(>SR zRx#f6>b(9K7ZsSZy*c_)Xj>WYV<#xZ;!E@+#2qeHmYazKhTb&*h^Q#V9+_56_0bD53-Wv^uEQQC6q8JJ`8{~ud1OZKG{ z3n?#@S%%#(Co<0Fl_+n?`FPYm+cZd_A!j9%R39dl2C9$mX=UGv+_8atTfKo>aHGzO zGza<1k^qaK-Z5e?#OPDH@>SHsux<*j-PRUM0h{chgFoC3r7d!refeMUb8`{Z&XqHz zJLdP(isvAU-P#YU0#>L(Y${#cQfOd@gZ395V`0xP<2Q(>(oPVZk96a>4J6@o1OJbs zvkYs(?cy+?g0#|Iib%IK!$5@5q4boH?vfl*io}qbNDL4d-CdI|VMtB7yK~gGcmMb6 zKJ3G;XXkpJbAIQ(bq&h#qG-#=A;qk!xt-&_e3fzAW2LF?*&2cuCZj81k&9#o7s1|c z*PU*EtS9OeZK(zOL~2)o<8xl>eVsf2g0W zUuyF6LSU3LTwT3?Uev8FQvdLj!Y*Wi+I!_xgVP>j>mFnWU$7hcyWYzAw(hgPOdkEE zLST_+*_Yg{7&XHWeRzZiK)#$OwWHU>Qf8Q9^Z8bvCga~Wtrfqcs~yG*hy!p}HzW9= zzX)7ubEQ`j6Sl1%cGYxU3>)xF(L4E~I)zs|;JM=X$+F%9b-I+wRhsmv#?HR(u@f3E z{3M1^2r}^EI}#2z_wAv7xQ$5H+Lo_-N8?lSww&U=T9oQj^qW}ggN&vVrO77zfFJ>! zt<#k#f?CnZv9XjVI{J>w&oCHS2bf%A{MW%&`5?B82e<2q;QrtYnH>ZdA>q?|RLxTw zr}^nbhVCqtR~%n|TZKk07WfG*_pMX}dj-sTY@;3>#A|z$`|}53IUKM=@K}F8+ec-W zK{3zz+GESVKS!1OUba5obyn6F!B*0-nxcdg=of=i z!b)@5eL1xbAZMJ0-`~I50Uy>(VPASz^ei>2rD>GT$>kD4{ zbmMuAES!a&L>l`j@ZRieMt^6d-C_^Hy--`w2mitT8dSqpojq(mQ%^jvN~AEE=KMtG zkcYd#vGDp?0`+8z01(}=tU~K2!PfU_-_J;3Z&$5Z#mDsGk#taYFV4zwu=XEQi1{v} z^lK0WdyRVE93rjnt4>=TLbmI)C9Uux#M5MK9x+-Gecxo!GIi~4o}AGhO(u3%YO$*l z-v!T&UyU7C=)%qS(Ii($L(T4qB!I=;uljbXR*S6v0}WCm7V+UR^)pbKpo_^!8gbXx1UGeY;@YwkC{z^ysqH6U* zTy@9TF677B*s|9f-oY{US^R?dokMBz$&!lZK{{xIK{;tsqtGX}HsSNC4eg)SgBpiE zdKgYV27glH1chtVL%blT`s28r;WPI%q_;-!1s=kTghmrn`)msP^R=}H2@$9rs!4ss zZTT3FwK0y1c=yeh(&YZy4|cDH3x5nawe@{K()auzLlvSRr<)G=Hh>uB;KFagbgk@gyoc zrSUO+0__x~Mqt5mksC7uP?u&DOfqTucJj7qd#<_YSIE8!d2e)w zHd%{YkKU7Q0T(Gh=mv(IxU2;5$_TujST*k1k1u))aqX}^BtT^$}X`?qTUI4!jg zm2!gnO%^GI8jGneP4oUm12J_<0cFUAY1sGnLn8E@NvIbCc75kZSH3qeQWF=f4#bge za;}|KCABorPVJZ$oku*mKS^t+*@}}l5UTGU1hgy9JxbuAS(rk93DNXKO%Br<3FNQFR!G)F&FG&9`-BKw;0GX?bPS}@f!L;~@lPBzI=VD*Nr z;CJZ$O_u{AJjC@pSktFe?9%P-sDaybG$o>&eC8?NX@M)30{ZblYG@W~!w04djOH?# z*lAVuI{E2Gn?(7O3??y}ji$%6zGR}@BsPyVx9#F1Wv}>XLyaaHU6m-A)>5%_BLGUZ z%O-Svo6H-^z)9T!zSh8)R^RJ3V{W3;xxbAN9ii#51|az73S=B?bt;%Fns^9CCCA0R zA=vnu`s_LG<^>66b^i7n0`r=;uzV`pGc;1yGb!boJk@OiV4Rx=5$-fKE5v~ObP)y> zzcjZ#?W*rwe+s!2SE^Cu^yJBv-cwI*1T@0)Z>ETSvDoEL9J4loTt%0{pAaIGj6z^i{g&z@dis{uCAErWDr z^j)2SVi&4|n|62GA~bNHPoi#aC$+co6?#;u;5Z)WdF7({v&u?q!hT-Sn#d&QD+X|E zG8CdDV&JQ^Ol-ijA>ULuSRB=nuikWPU;g8KujV0!90K=VPOJ?ve!AhA!4+)0Rry=p zZw4OAzMq|wWL)xHX;l`^z` zpH|Y%4@vJcjCWtDkm=@x-m;mJUy-N`bf{NW4A<*_$T->Ice0cKlp?tU)(CP*e5<-w zRsx09@2@Pc&3BqbXW``kw#}O6o5*FRm0hlOFBG}K8Nh$kxDjj1h%d`u3_zPH)Brd= zYeTc-nv!;xR&ZgKCm{vWrjRaE33xGPV99k-oMFocIp%>%Q}4D&H@0hk z4?MH+y(+JZOMZ+d{?o z)>czqO9NIs@QlDiJk55zym8##8T@A>>{lSq>)xX~_PFbZhVPgyId&Dj*a`GKwzB89 za>7SqGF(~xSUwYVIJAu~)azR{;V+>pfkMpvJPCXZW>*`_0xxia z2`56#cqUV9HnCaHR@vt=m@E*`hyuyaLgmoI%bb+WVev`sglj8Dbg^6~-UMR@IDgKU zkF$?BRLYaK9Dn_c6YSqQ$P3~!?9{uskt|L8xf8UmSaN0umGc_vMGhqW(XEbCtT~h9 zL7FJYxK2`Nvy=HL)hTXL@ghj%*E(>1`Bk4To=;fo zI}|{)jjvdNp#3~aFd`*2Bo)a?)VSAR;D2^wZ35Q^bG-US*`?gQzv+Uzew0rR|J}{# zG{2v;CRdL$>)`_X58KPS;PDhVn`!-hubG$sJXSZN|Mcnw8@|%zUXu^Zf*Y&h2){iF z`qeA89ot`R@j>^}FdiymfpkPN3u^3`TMspuPvjRe)_3##GskdK1Z>#`nG zGc!OHkA2p=XF4OvAN2ux=q&o!Wq4PQPtX56UhY3UpGPmS{ybat%*!yQ00vCixw9P% zH@Ghcem)n)cak4Jf@0Use&O)5IL^le>l($LIcZ-3_sCTi+zdHY7{KNnQb`gO6j1v0 z#KQ0~`xJxnXu!b`dUOYW)#(d6Y}Lcj&$dABt-N$vDDeBK>{I`IhdS@h(CX9A6yFF; zcuq`#cRg2}kK2+BY@$bXgqre~Byz(4P;XsZla#MSK!rI>$C=yZJK9S z3U50_OA(ej%V;+kFuE7RRpWFyK-=Lj^x*1{V6=oYxV%lG)4*}W%IpD+e;(AGVRyF0 zQ=@{|@xF`#e9$()?RPbSGkhjau*l`;DGmMAo29}NmnbDmgUe%fO@0EwHJcx+_P5UA zM%UvgPCYj${nPF3wC9e*+YOnM7sYi+er7HP%Og`aSpNly#T#^YVwv|kx9vq*5-HwQ zFx&OU`BUPF_AFU*NQc3hh9@bzvK0|~`|8~p>-0>Af1&%NpFS-2fB?+DlCu3z%K1;<=lz zx&wWuj)(x}I_2Bb1nx^e$5#mI_NxF=cYha2&9A9=mZL-10(gDs87}zIxsP$>aWW9E z5@c8>N_~S`TxwSfkW)`JIX_oIFb^hHHR#eLI5;{cdTRb1KZZ!JI~SnRA`cLj8JT~d znMWnVy_dWOCj%E*!O-lp4{~*NJsLgu+jToboAaXjc67a1BXVIMko#{nY5EK?ajj{ zfBe-=E|O?Sr*Vfd(!2JTmVpiqByQC@PeFy2N%Mj+8O+XrJAU3ifUU2u4h2Re>J|ws zVOv~4aJdUS{LA?~XMsDe?SQBs!J3YS+s$054V!ImuZs>uGaFCBh~L^>l6P9Z&$#54 z)#kz2b>vQ=^pFX_gg@EA7>D za;;e(+3sKuDF1$K5__=_iP7nnTx6GwS&Wwh$FOmd57jeb?Je$LZeT0%4^Mbkot+>k zY~tYCkyJtaG3nf{B;?CbHOp#;s9gOwq^e%ze9BJtcS!VI<D}y#j!wvsUUjl* zrlo36HJ3nUU*UrP^luDd6XWtu{_@bTa`BZc5<+ndWjAwg4PliEOh`z+r~W;BtZ0iCTGVKmVn9M3HFc;EZq zOuc=M+$dCH)%w(D$;CLL-n(H4k2Cv6NGD^NnxXiSSLf<`1><3QUw}$h5=0M`u2IwbYqvd!U#G^>rflqE(AV5mhw|YYqD&#Jlk%!^Vp3n<9sc0@fSV-zdI2P?4_*d z%}x$iFbmUF7s%?{Zdgc&(AblM2$pVU>{xg7od)O^OPa}M_T`(_(EGr?^s67x_wxb> z=VjhuD#x`~nSIHSP-k+}{P#P^rso1ZY3UkXhg!skVq{$nbF|lD=%l^D76L=7apoio ztN{~!G9CTI$A~6KcSiv50_6t=F>Cq zby(H@vW>IX4}KoYoS@}z2(IBwvvouJXB7^)gHyNYalwzi_F{ttYj!>5BxzuDmk{Ar z_ned&-K8_x4Cj^q>L0&`ee5R=p9;)ZfvEk5zV<+6%x-*wwY}f~cjOQF*UP5!7Sivu zRTGD`yY{=8niSUmmib4epk7Mw$x`wM3hO;UA z^24^wk@uO`UA#ePtlr#7UY z`aRwv5uV|^+IUE{PG3-DKp@4OR|2P*)vLer7r|5fvxmYr6xH3|R&gSjFLQQG=J99v z%wbYrOW-8frbr(k#D=L93vlhascz*5)_X5`oP6@1Yr6;AEImI2azjU?ZJzg8NtQ=A zF$FC&2!rL$++MdsHhc`Le;}c#br02!w%cTawcspe9P;~aF*?@YB}qD7_Hr)w`tFJU z?O7Mxs$uiAaej^iZ;p#h4E>O2CX#gT{rR(HiONA4NWR79$HQH!8y{u;z_iZI+Lo)x z1|)Sr*2pOwYn;vDAwwARPI+eQN*DoOggo>ON18A_8=oGbxBGeP*-1yLz?<#6x8Qm% zueSTTAc*L*o%$ z>J2+8xc$1oZ0aLXB7-3nDwbW>*il(Rb;pgWfl zY%iTcE);BhREx!I&X_+uS&dhrL;8y-4yfGJ37@u$_jd1A9@;1t~-$vc)tBz4Ne9;-jopU0_{B6 z9&yqKR(eJP(_eRxHfI%V`D*H=dRa%@T5tCkb;)GGWA!8wY(;?Go{6DC&sP#0$|>&Y zHFrlh$QnO6?)KbHw4g$oj#N4~U$%XDw9W^|(FxUmD%Fz^r%H`5CrW{Its8g^?-$%1 zI%7Gx8KDJro&FKePtw*z%LZlb1O<|(771CIu&8w$(v!cyvK~W3Q)Jf={0!i&cWHz@ zj|mO_T4q1CbW3#6LCD@>uurhW>&h<1w`G^DDgFE{4)A?NC*~{?2*m`jric~7o!%U+ zvdf7b%YI>I!T~aypXr_e&083@IMCNovP?S>>8h)D>7GEwL)5ERqUrSs@J)Uv)Za=a zG#9T(7cj8c>%@|FKTH1Jl)HXtSZiA6Q^JzNe>i=6#?f29&s4cGsQSMhT>-!>2EdgD z?)hzsNXE5-@8U_b7RVNFV)n4m_nhwLZF}{GNQeCM_eV>)Ll&Mp6_`kX{**leb_7Yu zx0rn1#Ls8qb#|}Tr?-`&{(&5;TF;Tv+QsdH&8t0XKgF0kIrJFOJ!V^<;xIGeTy#V$HK5f1*O)H9_H1%%lti3d=D{U3fh+f zA@bLP55Uv8kC$YiX8hvXF`pO0hulN98>Y@j>MKA@F^3+s@f1+%`70YD2K2DWx*?gD zpeu{1r)aF0lWIAI6ore@qsAiBzM67B-sM?olS$cV)}3J67Gdv;&uvNXtv@_jYbqH~ zZcO|vf$7T!(AL_H_x`T;-9u_w+Vd)9s@jNZn|f+L!Axibn2H_Ad;iX|JpjJDWxkN! zSiN;pWyPmLFsO8u+mvVP$=BmW&v>Dmr=5Up0K@&v)*;&_pn<^azgQ$7Ao9Z#+N=80 z{a?)_ZP?qwGf=PfpTDv;Jfh=Hw8y5hAgKkP?x~MEpUy)xZdi1QS4m@$*~NEslEIw6 z@rf~$%3&}rOc{496Hs@%Ib(q49#wFkfh^($Is`qWd|iX8WVS#Ap`ormX|F-Mj+8w~ ziO_BV@~I$RnwE$jWX@lt7AxmuO)9SaSnod2lkbo^92?F{b-0VXfu;TRJ(S8%)oKD zc7skKfn>`=?(znQX7{UKUCb%;?I<+hw;G<%Y6CVYYJ0wDcDSNzj(4$Dtc(6tuR)Pp*fqC zOMlwH zhjPp$K#BI$eO&9KPnkr0Q=DZU{ky=afY*NlQO7^=J#faqH#&F&&!Ndb<68j13QC0sX34`Pw)p{OfTL2sPhZ{W|_t z`y!OFoe7&9r z>pql=7N_AK2RXp85%|UTS_tKp5IMLkxZYu?B=EV=z}js2s&y}4?XETEMZtv=)S%p5 z!}Gd=!~wR=al9Gd#oZIw*M^$`#pvLSik+6^vc7_0{WqBg(j0hGa>DHpjR^Df)xek9 z_3fMc$yDa`^y|um1lgZVs<{G~$pH*n)?mfZxRAQoqO)+0FyxdC2H~rB;|=55ALeBIQrxtS zyMLnx-Br1ZYP0J#1^Aa!e;S(Y&Y!cB^M#sj;2kEOZB7Wi_PDvbNl7}1PFmpp9l=i6OEA2{gMe=H9?QTxHu5ZV1c zJlqanpody2B9WUm(^oQQnx9f=%!!kb(#5yiEnWdrexlkV$8{qG6nsMpyhp~m0;K)= zVks0h4)?F1${EHnNiT%nyP{KIHbuPyEkFikHhy>GS|z80^mU_x%5H^_FKzixt(Rql zc=r2mK0fcI5z~;WCRlZFF-uaT+c~;A{ZvDrf}4H{?Y2?+mn1r>j#d z^;9v#9-c(bw|I#m%xSI0kD2k$VK`ZsWEro7`+Iu{;Qs5ouIA0ra{C8)UXl`fuLC0< z)Vf*X?-PKwCj_cv!Nr5#8V~#PRkR3=^;F|npXuXVNJ=LA)m3L0d%pYRX?Kah1nS&ll6TQ%#jr2O5nyNgzTw%B#c-4Q@-4A?I3a;*32XJFGVC;6=IK>;!%@0i(QGSpclu8AQo{lEOGr76 zK(`KFMY-XARImEwcemS%(}f4-8Q3KKsSuF&i*BYV>{Fh;!Y(4A_^h+ep$irXq5hR4P?zD! z0o=Ns<|+i^Sy6$+;(wJkOY( zYNwvJRv-(%x8$bXKaa?(UP|IsHc=kzhJ8TP_keOcgG`^t?aqPIX!D@EQc`~z75Rn- zxQDh<4J4k z0&J2_o|VlmP)E0~)8FvT4zi6V82c?YlAKZB!1_@fn_S}ScnuuL)Sq=BHBHt*f3Mv@ z+;vX2*v&d~CTF1%a+KL!9bZ%trkOC|SJbAP%IwY^*T=^er0k zUSza998?%)wXusp^fhfgH-00#&0v(3XoC8vQOp)n>&YWRmFlin*p6@LSd1l_1bl|1 zXpCL`Rb_;cLoc1TU7-eEaD{S1UeVQZe*t~crk5o93{67o(r5b$<$RCf>nr7)qMK6d zV~Y7WCXv|zBFUs~ZlLW~1W#_ScKdO+yFkKFSh@eeE$94DK8PpTmJ_{$FFcxItfUc^KdB*IIzTt&k)U zk3XxU_de*t2e96Jx8uX^wZ;e&m923O0YvW1q-c(&#^F1;2^N-G(a);0SUa=qkh<^^ zquOyjlyDqj6!J8WBoh6kj93F~>VAd%)^?bC&=8-a`T0_ix*e?PGY!2tGYH;F(PEZg7ewjc{woH1 zc#u*Jj&eR=c3*0eZ+hjTl}-rr@z0Ovk_VT+ z_%S~vlo@~splG}Cx7V51*8x96I?!v!gir74tv^{!bi`+;H(wtzT1cE?)5{iu(!T03 zfIU!&M(EJu6)KfS1Q!$>Tx}rHQat z-!W(+eihigGv{5A2kjQJksT3cw`2Ezh`Ioxjt<96LZ+YwF{xfuGf@{Z`BQ5OfSh5M z`DCRiO^Lzw*XE_b6ycOMT))PhYJ$?j+>N-mivS&U9sT^XiG^0b@ksZ>%Av$u-!$Ea zAF&fT{8mlAY$YT~P6TC-3*3R>Gm6-U8ejo@0v_IdfEX;qadw0Je+Ofq>@T2Q2fPML zr;;72U)eF`aK3!uN*kFR%_^K`&m;~rn!!?Na9iv{s*A%<`BIG~eA|nlsy==Eed2}a z-Vf(kg`^o6wZ??`&h|n6Y2i?EpwM;al2he5dNFR{vkuj%^z$82=>)Kx)o)U}@Be0A z{YP1g%sx$`E;4>-_7gu*yevr%;QqW4$vfdyla;?(zy|ZQ(C*;zC|VU^rnANZB1w(an5+7oA^TTe zhv9QgR;I0p89$`w5*gC~*qmKV-94!{fNR(F+Q&nXXgtkD2+f#+Zz0PtSD7`ktyCv` z5v{X%2MQbdxPbk2WGnQ?^+xZS=-@9dPMc<@FD4uqRlLRp?ADC0s3(1t&Jo_zJ-YMh zbB(uCNRO64qtIqENAjNubu~+Vc*E(C=5 zK-M-3MXci1l#rsa=PT|JgYDMmepR%{vCPLgv{Isxt3phyf$}L?|w2zK()|J$L%#gTgoGr zSC|M3x!|+})*u(t+CvsKuGeC;2N_;6U3{WAp*u@n2jEK)@VDqx*{>Yy-n%_8mS!%s zHva&E9b097rh9aqyOxo-_@JeH;rIKPt?JQ5THREv`S@!RYPD;~G8Q~B{bpcYCeNI4 zr9$!jG3wp*ZDa=zFcojoE|BgTwd%?eCD}D7yk1JO;Ss`nvQKZncHE>rLgxj8k2XtK z4;W^aYju!$2`_|TRs;0zV0Zn!Z$Yu)kol|Lq+|-9t^A?RQTlj#vdq&5_fQLF8)XT@CcX0^hAs^Z<%=Crdi)W~8==!uH}H1xzg8ZU!QV~a47_z#+P@6& z4mo*9PsC7-e*_c4ug3Ug{Ld%f={>2ui~%CIEAOv8hH>)ZTpN#jji)y^Z^=cD%E3)_ zq70rwDC2(HYCxn2wl{9}%RxJO;de+mV)?bd5JUb4qC>oib4N3Lh+IyW^DyXhW-f_3r0m<=M*BCCgB z&Gv3{{C7g-wN%SrxjZ=%KeRlDCGQ?fw+_=q%r!hnCKTY_f`*YXe9oXU3!h{cYg0y|&4M#@I_WGcKGI9tkDPxq6oiUw}qYJ+K!~T9cO^ zDx0ib(S2hCvAZchw_0^{0ldmOYpj%9;@KmicmdFSpSjaD@~6dk#oD*k6ZSU5KzF!m zacD?3FGvb9dGUMl1Caj`n;_X+xjEJ1Xn$C7ZcUelm4pxe761Qf!Et{(Dtv7{`$7M4 z_=E~}ok;{Y&{wf>uKc>h{QS-qVRKMWY?u8sy8;&bF7{O#bte4Ev*cy=c9Gk? zgMYzf?00Z3{oY%wI@c1}3tf{T>o4-KV|N#8+kXFlGM%1*qmJJALT2D875@Ss$v8&6 zZzsu?T1qwz4!$$*pdo%>&3F&K{=>|zrR;GJ%xdDzE@-AkjwCg!c#l73%sDZffKDM} zEB(ZUh(+kEJ@93KgBqT>t&sqq92%nq3vG`exZAz?pw>qE;#A59Ll9%$*G+uffRo)u zkE4)PnlJf8F(tCbLHS8}CvIK?onP*ZX)*#sjQ@43KjTf$Wts=yKPRqmHD_qKJ;03Q zoxa9EHT9|q*1Z1Y<=Gdw!+K>`BGMDX^=kR1|GeO>bV} z=P7UFpS%FfQ^J0T$pawcuc{8fi)Uvr}*_qdWhQqXqZl zt*8D!`YA)lsQ&NydiuJrGQw*>?;bvD;N1>KD%}6{f$+z_p7iGbF1B3Qu(O+u^C{YW zug=rCyp4Zbj;_^!EdHX5{fvQA7n)|!rbG83!%CjDTh}%qHPn-hWE0GWY4;DmX%ekH z)Z^}a3BF!4*}yXOToE^*^tKUCFN3LRL&*45`MA!Z_%LqUU2+u>g)|*mTU1r0n0kv? zBb~zytEj&6vJ=(uJ5wnssnIj?0$>$>DY+q{ysHSH-QvMhHZi0pVt$zw#HQfOW;`|<|+ypY&>p+{sYRUX|AZ^I}W-7C6cz?oU0Lw6a#(A!>-29`T9&n-n! zeRlfi%tG{yhbF}nY~(cjZ)a9|$c7X|z87`FKrA8tx<*2}UCMHMsa&d(1dGw9TOWJg{K>KU#uQ=t{$0=B+gQU` zHL2cYZle~BXRiwreN@dB^-U{{1wNV`!8}5^F^|TzssRpX`_9Dmu)@bv)&>Q zt}>vatGd~{dz*{OVSZOhO^KBsJCWq4AV`-VUy=Ak89$F0pS#$+kxT(E_?Vf!y{MDg z>>c+Bm%GyjFH5R&iKcC+dN8UL5qWKF<{Eh66U5CQWThnUut&Vg*50K}_o6)LS1hQI zJxYq;Pu6w9oB1!KSpE09WshP+N_j5@Rqn7+Hd}EUT$&Lzcpt65!^~SODezTg*ICFM zE7_8me@Lmbw2R`fHFdd2uDnLJ#-Ek|Qc^%E{;yp#_QbVf{SHVT(NN<$XI)%xm8zyl zOIXQ|1vvmlQpQ5`WREzPLH8VH2Re+hyvk*aaCM58N<{@7P)qd^q_X3}?LEEo#hE&% z(=6%|>|iEBbfUFpIwEQSSVY#PF}8Z&%-8oxw|T<|XZpz9FZE__HwDaY!NRmiSqtTx zZJirJ!RK5756fODY+fIcWtzRg;Dw3YVTFpmg9C&ACTwAo-y@y{kZAY=LeNPemwb}VwQ*d5> zWN!#HQ`)gd@riZnmtOF912KeGurR(^Nl{ z!BQBhd{gS@-)lbcCk=WftO{9XjM_4pSvjeBclA7JgwWWry`?Lf*d2r`0^not(@h?K z9RX$Kdzw9&2cSD$aL$WO%e}puT*8e~Rh~W>^X5=H>tLoy zKLUY&&=P0S-ByLNXOyK3+tzwy97GzbO&JwTehd^=WDNKW=YWm$pf9pq=&9Q~#f6NP z;e@4@&GJ#}dx8@NDR?^e!uci$YW{jQ&C+Xc`bmb!>u_^QtwWceyaW#d_EO5Kt&tsx zL$VjQ!LH5>as$JC&~&AzP>h^jXv!+XLbS^|x$K=0Ci#cx!3-DZ~7D z^mWr=K0CYdIF(2^!6;>zei^}^Pw=yhJJq}4`(wUK**4dc#s-p21H$(#WeHu7&Itc&zoWyL~l=EEP3(D!7}8FL!^mA3&(BNL_DBt zuZEB9(0|=4lyQ_uv%xh?QE#-tp%MKX12_*aw967t@i2|9H+)$e)l2mb!%unj zL?BzQp~t31o8! zT{}q$%ycyp8b@>{RYo>GWLELDb;EXob>U*OPGPP~#XuYl!8Rtd#z%SWI;h&posy*H zE-x3^`8wk?W}6(BQ0x^s4b7Nt<|l012M^T3m0RixiEUY=>Sbio-&&_g+GRv}ts9E? zPDS*JTV5L7`YTbDy)Avhu0#2zL_F#`1Oq{b_-uIP9QyNaV#Zf-JDi+?O|cxa4Xwy} z;&iW8Yax1@9dSo;T}m=S70m5q8J4oIN`qw2AT$yG+bLNiQH+qxxu@uuOe??4PibG? zSjBg^b9U_ZlNYh2iGQ$xnuai(gacm>jS@0nLG0@N-j{J*dWC-jNK(J=J?z=IJjJrh zC#M9rnA-eK$oczWR#<5j{+pNa`d&J6o|ekNlb#&P(l68D zx&L^aOn*h|(Y8ViBhbsMh7>DrdJdPm+|{)I$Ifvztzn?la8ZY{8KJLfOK@o`YTVU0 zVvO>bpUyJq^TCGQ+sNf!(vn%Qx7;1e2Ws8p3P^oxGH;Y$WFVceT^{VSS-{X|=VA)i=*uFc9}~u>*_7_qYCH z9FL5nQKPI5x?ZTu7_D;yyG?DuS6!TxHw7fZ03SdDY>gz&f-=h6Y*F6fbx$!iISOn? zsUR{nQk#J?8W;sOAl8y&Y)1qE2dNnQe@2cJ=NQw=nXdh)xMyYc8s=ys)CK> zX4x#mRjD;$Du<4*4Q#Q$))z(Dc;kFbnrwa2o@@WxFbFFK1d4RUa$=j(Sf;CNV<^r# z0bZW75pFz9Kb1;&L?wzXT#XhnO5%|oKO=|2 zq$0n{0YKSB{Ag z9{s{;mjXjnkBqAw&OyVC(Vj=bh-*=|+s7Pa8^OCR9gB8-j-)@$U)_o|z|dJ=_CYQm zv0XKGX=Kl(-^+Vpe2BpxO5VIIrVid*F0NghcyLgGvJ@(F0wp+3BM$D>ARP^5c9^Kp zy@^Yr7Ml4BYXct_5Vn?=c}Di8;wZ+KPbsld#_1s)_X_fh#_9_!(%f{rUgmq@jI0)3 z1ecvM{E=y>yQWcD%z2*icEw@zGE%iEre1r}y?iVueMGJB<}YUY1_hL@SX_Sa-!;fx zTgGT?`+^~$W9+Qk9Lz!{?Dlig6*#Q#J72teZL$h`KAwd?;PYL?>cz9yUi*b3 zm|Tv}GrOx@0?jnfLE@(LTNBbhRDLvJ<-Z(N4AyGdc4eqatnJJSaF{#XTe^`!mJJ4l z52HiO>KCO@do_vPx88f+6q(P%?`?bSy5K*PSvZpR%8c5n@jqq!p=)5~Y$+aC@0}X; zi;skGX{9G7pQbbPA;GARDfjs(vzL}VWBu;leG9ca-Cwj}W>WWS?&QbsJ({a8S18xu zOX$>SvH5E9wY}amb(drFCxbPTc}^L($Oo z340OU-hG$(X-nRMkaDu%i%xWP8#3eVZ`bK}X(;M!x63O*aCD@L>zSKiM2xME_DUiW z*_BbaZgy#mC0!sYN;TN}d>JBj$tqD<9alX_MUaWN_YuB#--~y+NfSqXX&n!|@!gmB zU_bF~*e9GzZ2Crfg&rI-1~pS|{5Kn2mLyxlk-l58WP)Fflf26C!vSEs{cK-H-)6HA zDF-aid!FW%tVM8J-<$NC5oc~X;k;JM;q@bYVk^~a}Y1SH*tHtbb-zl)P*(jPlp07~fZC+I8-DV37gjqUlaIneN z1tR{`&bV#*xfN>EfpAI4qcDV<{yx-`MR^zoSgg&QC6n%}7bYvffIM zqys?m*6oas9q%ny=c9jA2}mnb&w&w|Fl8mTgLXccG4G}hB6~?*r`*)GoRp9-JJ3Gc z3v&z;Ymm10Z}Jast#e9uNdMt1UG#jg@pISu<@K`X)Wbd>SzWSC!do{t088VRa$<#Q zcsqh!;&lPFsAz)Xcp&>D;9aY51t!O+)trv3`f7?k9z`ljgp~7`HR@On@ruA6fxUKi z8L%a>10uBNQ{!O=c;S!W)LEU2gG+pyP9}5L3vY$>k?4k|%p>RINypkYkNeO`|Ep*I zvr34}P$R&*o-Oe$HfYI=gL<>$ET@1x>~ZLVmLTqgx8!W@_RIWQwd?V|u^nSqey9It zMi^mkdI>Q_jkpY2{-ab}vg0U$dw*!@?d+=Apt1aYg8EB;24gbvbsc5>b?&!5- z!LV@qfc+NjIg6`ziK6R?cF+QV&%a*jU*=nE-5&hh&MsnKMdolp0D%7+os3QSoNUDV z-AlPbvi%bk9}e7LMnd)|Y3edoD_ zj#;83mji-$z96dHaUtv;5y6(_41>fpiv)%5YoD&5K$ceyg;SaIUMY@kMa!0f%LN=r zRM=9j61z!#+rBrNDN1T@LRYWB!nN{dbOQ=}i*+z|6!c@OwRQH3eJ6MLD6|Vza=4$! z9!P!d$JT}XE*Eg8R9RZDq=|~^TI8;zlXC{#C8H4H{HtVp7zu%{-T)) zR&J1NA>gZ!E>sIg#0`_IcQ4m!?g^C<-=0LsZPXKg77gHDih2Iv4bwk=mjAVPEv}YmErZq;OOh^Hn%Wv$#xkgkDr!j^TVpFlX@!U| zs3i6!5^d4ORw1)0^ZmE52i$Rv}J#&l4xDhN0lfOquJ~HC;=0Ja*Il22QC()2Ki%n8)}Vi7Vz_+ z+QEjZNKbblw|#^9#i?UFG69JGyP!i#Uwf*IKPy7Q+Z7wQ>>KjaO!~7IkGrovx2Z>t z_r5H@+sivn|4bU?I!|YQ>OErD{F09hxv4oeUs6y&OC9een3goM&23A&e&{t)r^xV6 zz9@abr>qBmcvTiaK{QX^+1Xwnh#;pW;6O3-g|vPJEY3v*y-rGqaW@ETn}nvFwzyUs z2d{t_Y&M1N1liqQjY}UvJ(L3xY7qR`<}dn{wP196a;2M2OI^F3E>AFTuI{qcxNK0^ zwW!}Q+X`m&Encn=9eclWdOmq5(PpUBB!|qq&aUCZr{bA6n44Xnq+Xb}BTj^f{hKRv zU?Hnb`3Z-J`}2yswed)9+X*LuN?O^0!xt^eoEc9qfmL ze!3Xo&;31ZSYHChx7uXcAytXJ{v-=m1NT^T*lYe4iP;eaVNVqy7za2FHOKVLGG2Ae zPUg7g#_XFf&O}>N1yAo?jHEfaHoPCeV(x_#hQ3fB`~4PgROcw3I7{-ev@BvLcxD$) z%`qA0_|h>GS@kzqbQ6Bz6rsvg1Qf8%Fd1?UQX{ioVape;N^I`F_(rb>j~sl@{AP z+aruxD$rR2my4`$%Eyfv$c4VjwTcC8h>vlV4{}z`b>+=mB1-VzgNSWfuisHSOOHXObF(J~pfnGM!)pK= z6pVwcpI8jqxk4VwFB11MugZcb?Belf852Rs8cA}P>D#z)&yQ!EMZwbch{ld*cvVPe zR+1|XqA}*+N!`gWcNMYEZ03=91W5_v(coK0L1$Q++XqbMCRhu&ul{2%Wi(C(yyB)Y zmQSD8<1!vbMj5MBFEOMO>=lU(q1z{T#hj&6qdNl-gOv)l@ONoYjNF^}XRNg=n2hJ$ zNljuq;0Zn?<%=S98S_RXd$f|IqJgA1n~Xg;IqFj3s?0v2#2oksd$XhYRezuSC0nT8 z=QMlP!r$Xgw}R}a@uY*vRk&F#F$Xe3PiY9M=5n7lZNIBA(-fA~Sbpu!4e!cbFcxES z{y+It_{nGG|<;DTBqn)#nMyW|78fGM~ zTynkpLRIb=E5M5W@GX}0E^xr!NzrzGVBd>`$7j4ad&6~CIn8+y*tgg4S_3~?Yx!J0Fy$U{&^1E6y$hc5(k{9M^I%bQ^cAA2`v7Net-dd`(pc+ebSz84 z0Nd&;dVXl6y%Gz1Hq3LvcTCF5d{WCc^7?He&w#Lf^$OIV?4H)n`iCN1w}M07PV1{A z#M_sxzkunr*e;||{~+aFqz|%5Yhfc^k7S~~#3L@ds1gl&+jBD;!|~%LY8Kbrg+G|q z`O36OKUBL-B;RV3qXLB}E=okVUI)1p8Qg)jj^{F4pWUnh(y~(ZHfqbI3JJkE${I_0x8E8?inb^JJ3RGX&v-NLpl0!#3#6+QW5W?2uu82C3k(LP_OO3Q`c zItY?M7H?9o7Hh$u-6&#A?O#WK)n001?dF zat7-Hc-8=T&=?>(4-o@OeG|CjuhFOy3vYGVgB-Hs-mqBjBd9klaglP#M2&7lHXi;S z@T`9g#r_(d2o4QR(RxR=-Fs^_t~JzCEz}_erOIh&Vn;HAG~6pO$Y1KSHH^oa%-ps3 z?v^b~Vnmrn5Bc1W#~^Cb$=O4^oP|dy89fw*bLuOG|MIlmf+zyF*)~SaElEx062a zch-8|?;kk7oO`V#cXqOpy=Twtx#pUg7!5T=JnZM#0002*r4mdF0C+}^yzc@&Lq4;6 zR!JaVu-udkJpce4qJIw*Kzb%QauCHsOHme3K1{WPe1mQ)qbdUcR3+iuTVenJ1=KHL zGCIB}2U(b*BpZ#7OCrs`xt!!mQsgx%Q97fTK>6fEubedLH)mVHL_{xys_tF$n5x$M z2#FY{V1syqgv~`TeobjShVW^FQu1F=I;EMu>Vv_M#k89@ZE1XbwrZ{;+!Vti+HE&) zhL!zt{BjgE1%SwT{MSV%2m8NABBbO0>kG(RX@xLyl>app8js3@`oBg16c|?M|9gZN zyXXJ z60G$7uM?A&u5*HNE#`ru*IK`({L^Cr%S!)hchJ?=7m};TlDc+62bCc6Ab=m;-NSS+ zHg&7M2(^(ha=$fjSxQQt%5neCIz_%_I)0(}3UFw_oPJ zE%_!ntMxLXxT1{)sh##?^KH>>AzQNk!86fDs!}B+clj_)XU%|wkx`fDx3&5915-(p zSM`x-5a9JPL=+wt`D{|4oTdB3&zVOyv+sJJYcfl`A^eTN`Iq!l1}3Dc`=S58sw+Kg zHwBNvn^4Yigraj7cjfi4lzds49V19sM$TG`N^DQY-)D$)Y!b~h5aAHXeelsBWqXo8 z#wEy7#%lXzzj1gd?OT2LeHU?m_9Jd( zlK}u?{_8oB|B(URp3|5OLcK_Y6BVw)=QYL_G@}+#RKcV&F(x^Sw7uMhMz2L)|EmvW zT~7lnr+Zdtt^&=pDTq2Y*_;Pw9^E8rRe`m>=a_e%k^^1gzT8KfjIbUcRf%7`aP-Ra z*Ff-CTkOqu2X<0?$wlg~{gQiyuN2QA004{59}3N$qJy%D9|zjIW_MwiZ9~2b3(Bpt zO+|GHI}1%e&!(#(-3e2j^ekGY-$V3nC$)M%qg`BW%=&+Q{#1napXtYRnu3nT@zwXf zs&rBlVvYe2gh+7}@Ynl{K~m?xwG+Av!5s|v)AUsRW29!k&k4BOqT9b(3c4(Ocw_p7 zw!)y@m#AV*CfEy?v0plQ=7FwOk~}yFT)im!8i$a(b+bkh08ZH|k7^ET&nD_*o# zySms8(X4!<0)o&9&nuZpnNAzI_puS}kB$i!W~tPkclkhykHNhaJcqc-#cvY0vywAs&@X zJ&-WD6Hf$KSd8mfxLwKVuTf4^*#s+R5zK@hWEyN8$=}$j#t4Y08)6;V~C1w@r2b$8$(#u<*=Z*1%>7!G0W2GA)hpk|5OgKGv z*DE+Ehq_L!z_ytSk1mdj1Uj?A3_cf$Y+;vBNzMGq|Idm5y}zHU#u~`yFng+K zpKaB=>9?(^@@r<2<&Tzhsbo@$FQrvldxkq{hN%gybRL(fAw#Mki^}8asl)A~tC?q+ zfOei?Ae|}#p}?sryRL9d$jU6by)~s}Xzu!*NiiC(xLs4xK&RgQk#eOm$q~~q6hUEj zcRXID>v@{o6X^>eKMb7yaZa*3+7mq-(o!--?IF`!&|UQQSCx*)1$k=(YRWSZ{VDlj z__T8@Xq4)qFBz*rJMT8-Y?SjqhM-YU$w{=u2M-gYTb#Xj_4gPz+TGtN)G7YH{|$%8 z++jLr_xo+6=(F&D#=*6 zC?TTWRwAK3#D{XdDuw)P&r5pOYkl+T|!hXzvq zBS;qME+BX)t9hq4Y3VN?q#AEaIn#R??B2{kr-byXD{i~$*|{O5vvxss9G^-BN@#4# z(m$yxq{1p7%5KTEOiGn3Ja2Dz_?L@*CME(ycx1j5tx;8%eOSPPT$1EPiJPhTM2hGx z-D4%0#K`Uawwy$_VtEGcQl0r@IdRnWn@*V+3q=1GSg%;?0EOL|)8?6U_J$0*S;*M* z&X*@Y&&y+Sat}peHjKKyv! zWF}IP3tQ)=tgHJ|aCHCEY(?3R^Z{=_(#DuSZ3dhtCrbp)o7Ec9Q~lBA7_AV1z|i|# zyStxW`IgbfuZvjzzAtW$dZmFz%&)8UDbBp~tC4{KO7+2KZ%0{JRaX|Fm;MuO8X<-n zTw2Y}{P>0Q!y=Xl33QjMRe}4SFROw(FYt@?317{FZR#JF1Csy9cwMNuIQmiE81>4q zwKj&v_1;!35*Rh|bUatFeTEfY5nX|UT;3zp2e!@9XJzwZ+to9sgKdd@H3+T~JaQ)w zEr9(h^7t*dyRcx1#~AylcVO-nrLor~7jKX|+h4K{Qq0q=-v;$9z7%+5jyjuux*6eh z^#Pvd;H$!?gBK~}Ibkmrj#^&MjYwt99yV$p0Z<~bdJr7r&0a*#?JHUiX#tLErxfV( zei{!6IHe;Cdy=_<;YZqMN9E&3B<=s>hi-n})zDYEjYNjjz4@ge2{Z7OlnI9k$=fqJ zGa^@c^V{4kIEMqH%V@A1L)-UHBo?C;$L$t^OPOW!vBZotPQ%q!=pb;x-k9^U+9_jT z=D?y(NvTe)8nL2MTi3^v`WfKl$RjGMGl^Tj`E(M<3GVS5{5}U!NZ~a}UboO z#rmd($vs&TUw-yg#a7En<0|zyZf21<$%+VMQ?7eia^T&kz82+1Rr{DgI2p6 zN9#1y6ZsU_Gf;j#?{l{`A>&w-hWvD$)@k%SNr5{+8p2CH9S&zzMjUK5DfJMuK{qDG zuCw!hLIlS#tt2x@)v%x1%AfxvkS(ygvyr&)^wjV(w|$cgVl1_A_^vJM z)e@_^TKd+l)+#H%#|Pl38iRGeue3SBBq9{3aXiFdvj}5KzJ>0pjisL zk@M@4d0SXdL|K0JBzlai>NTP6NBtfr+vY-nnmGJC6r{tV3%UON`)CFgPDr)v%ITm%HsZ;OBHZJL~NB`QWvmo4?GbG|!(1x9&bS*gyfgj6X9)$YViEnXE7M{#W zJbBIV97@aw7OTVQrjk3cD|d^bsBk$7BiDoijhPOy0fK>2-ho$vyFM$gCyoX^u-1h@ z^t?)2yCgMxf7w0K@m30ts%(ABxoJHBB&f3U#m(zRC8DRosB|dw*NOZfV@orsi;cxRY8RJ4 zb256|Fqw-T?JC^M5-Q$^`u>WI5(+*~1dmr28xb)|KZdt!+5&@PLy#x-=0{NH`Pp`?MM>g4 z8woat(*6XDrmhXOgMbur*zn*jx_aDQGd1^?3wAv^J*}iaoc{7dl5l2u`=RJRCILHa zXyR~=r1MhcY5o{OLdcQLe{@2s4)a0oX6U_iy#CK{uLOnIO)h@wsuH(o3gL#23 zpJ%`aj(FdhOgS`6!$#L{Eh3dOtc_OwzW?}vE!u>A_~NvI%ZDkTd6%`+;2Tf9MEln` z>67X$SDd1o{-sC|IDCO%Q4xp6KdWwE3u~Pjxqs4o)>j(8=Z#sHEF9^j0`ir-I4>wd z&`AiLXM>{XQ0|4#GHkb@Fj1$^)xAL;hhNu{GXC)u(rrX3Bqt+yw3?99)#p#S2kIaj zBzpDh&M^_&vx*EalNIQJ=M8JX?*QvoxkX=f#4@So=3u! zlxK5A^p)(Zt!1h@2q&k-a?YFpm$gI0NbMey(@Hk>j4PaU8ZAIvW4`)a3CEb>eW|ci zx?QJIVij#jNlnP1kg}tv%9(Iv+^nc;t5mz~si8{{$H;>z<;Yl{ynF~xZe|%Q$NkPD z%Q%kxE1*TlheA&^x4{Auo^y`5VgXr=;UU{Qs^|X>VmL*zLiRRON9FmK?UJw?<(utg z6GWRNshXt)ByTg5orSEDk#s2lfKE^Oyy(l97iVO?i<$KQ5Z5|8<-ig&SECt<&Ms7O zu~3(7k$@a->`NnAsWan+W4(%YGvIp%5q^=mkvjl|Tg4FHHy$jhPBb@nTaGh`LSTmx zk1t6elbhZjt*w-uR=U&lEi#6o)z!5#kkw6smn0s`TzxLsA zE7Wm0UjmuTw!$kNc8wmMG5$gxYm>0Bc`TmQ#VaJlLYEE-l8_Jxk<9YzqnMbe3B$%9 zh59Z601;_ES~ADKKERGPZ8`7&6rB}vxj(*TNX}HHVN)C?PUo8{>iWqK3J(87t|Qs_ zv1MT2NP{7R0p}%~MyWKWws55wQv-V!cIM`lRTdKr#wx4qCvp&I&4?#@`@3qZva5K5 zWH<=sR_p%ECprr0-p+iUnTS<0%`5u-)!N9?_4hIsQl&L~nR~qBy9)+_v6dNOKscTI zQOM=jbhb$8!!Yu9&!*E^mm(3B6Kd_@bxUscR{fbCkzAgh6?dU;u|hC8iY83oa-#hC z^GBMS)2P5!v6Kri_wT&km4%s_QlkKH`>16xI#0>4Lf?N$6Aj`_|GPh(hea59U^osiTQA z7LHmiHQ3SL`8Y6cw9yY_oYEcMWBS8(Jf4D&g*J_6?QKvn2#Q<|F{%t?ICy0wvJV-V zFF6mvLwZmF(&g7@(3QSr61B2x*AG5Ke^p{n%)sGrMyC{tJeZ-#fl;HD6q5p6l zMyZA)`}(Z-^gF|Y;UtX&F6`*P76Ot*iD!O-+QOSMO}a!ZDh7yYB%pggn;2j6f&mHx zW}A$baYiom8skdGEB(U<8!8Bkl6H4VyqkwOcMrPvly~78cPEY7SgpehoF1-U%lk;s zLznw9T)%uTZ%aRoK|U*FkiGpE8WQ6#tUEJT)wVV+bEcy^P1dM3({P9P{3GGhxvfSrH#Du1GL znG|>3dIFHa)VtpR@~9DI(W!0{m|tsc9Itdhjl?wOy9m~so-v+*Qb1(gMwY~OvMyTD z-(vP47?$w?T;5xR!?qSYp%MAr*o3d;6yUS!=!X4rhp~T>_EOE&y^mTBezolU1>B&J zJ)OxX4}5DxP8DEje_6%qFr#fEe_Q6fxBw3Jqd<=G#=Rqv(L&N1JmFSwtbQg&@2oVZ zp-(Ct(!e+-%kiGyhhC}^b5;GMgeo#!q|af@aFUDzV(NQ-lu6eN2Sd@u9}GGk8Lks> z;-Uxf(sb;^OU$GE;nx))ggTone7O!IYoZP_&$L`R)GOSrdEzq1^!*vs=gLU#i!CJY ziNFP)L^Ml@OumKAk(6WJ6s)Y8aLLX8RCnn_B+&l7(??uJ*-JQ%O@oSGP*5w#lv*JE zImwR%c|e^@(__;fi+&~N2lGVv@hH4o^H7mcO5}nDn1wsi$hhxCv9Ldu9dPn5+8Q@A zDe0``XiDeT)+U&RtU1p%*z>hMtvtW=948HO=d-$Ayxo(*jGtVh_3RK9-9^X&ZO9(W zKeeBHdNe&5%L@v*r^a@0bhInDd^lzg(eL9`3&CjI8i||9`x%=r=!rIBln0_QpCb<6znURd7$a@vld-B`qQU3P3w6$7sn3V zGON`TspA9h@H{t44R70K^2*Dz4?~tw(xH#MdwK4YY)$K{aogq1wsQZnG|4{KSYTXZ zN)jT|)<^Y^m!SGfqzJ!alhv3T-KW>xcWe6|%kI~ki0O4;9Ibr|N*fkGC@6r(7`x<@ zk&U-5pZLyucJ$sd=a?3e9q|QmMI9Y!&)&2YjUUdG9+~AWHN*PWj`oUe&E0?mX2;&{ zS*Jd|sAKPDy@H)*M{j+eRVO)*+E|d>Y}DB5uPn|mg1@7)fJup=O}SriaOUyA0+^b= z_+@18__(#ojRmU6A#nuID;XP|fE`?HR+?{@dSpFD0o($G&CotZ<@a3cx-H%t_}wMN zb>hNOXzQ4nMDLD{(b_hwufA2cMr+z^zhE0DWzw>~52kWFIRBopo$BadFqEQ6aY(3GPK3zc^ zYA?R#@jSQQXN{rLHiu_Ml>JJ2>iz>>@YyNXg2hDqSRLKQXoN_@A|Ax&O%Y8vg2N#h zII8EI*0o1S9j-X|QlehG$&w(*cBu{hl(NqDSAQlDBB`S$62;aY7|$imAa^CkUhJ{M zKD(E}8bh>03Pqo%<~fSxEh!FnOI4*)fWvbtT?6)Mev!>*;oqkg~9k;k`tg2Juz!m>Lu!(|%!2q#*cI_o5 ze!ETYxiOfA4sheWQ%vFY;l^HUJ4PwHK7pbDLL(#=dKt?@*O#H^wLm81&aE(Ee2LRp6&o|U5%oLMzK|~JHi3$)6wi=tFe_PfTmZMkW%r>g~QDL zwa6WTiuzYXEO}VK7iol0^5F#E<64)dRmUKp4FS4A^glHGn;i#Qk>{`4@!`o5wb(i- zZJ$DUXz$^2M882dzFkfu-9*HZX~F&OAbZY$?gcztGX;xDEBk3aFP$-;5T8gJ|$echsDTJi$p(rn*hAB9}I7d>2?&|Xgq zGj{!UZz6AChh%@9+?N{s`sCAH!3%{Aa0oTOmfp0CE9~={ySVbN${CD8llK&2;X#=! zsru2X3O8uJ)K*!M+5f#(Y$f$iSkA8}PCFGBW#JUCG6WaS_>Y&m2hMuawKWa;EQv9{ zkDM!*tW_qZq`Cy!g?ujmY~p*KZtyv`w|_XrAjQ3Xy1lv`yDIet3MLNocWBS7D6hcP1fS zMYXf})MoDYV2NsskZ2h_J!I}|?Q#wkBPNxUGw(!_RQYGu3`j;6ePkzUkM2DRD9Nzh zol7x`ym@3$>|g^&o7M96FM1M>#nH51yxb)V3lCD+l_N`H#~SsP@uW<1cEan(=3jSU zT=-V%YKTh$S(&ciAwHknQo3rg!A11RA>kNVPA9?7(aAwM?Of)zUh)G)n{1Q&!5XSO z5O4LV=YIKwRr=LPK3*Z2LG7~@?CzFnceZg{xB^7R@vajD(Rm4SQ?$c**JWn1Ls0#h z&h-jJw;BWLd+e*W(3X5|kM?FH4#)E@&qTXzNf?>;pvIla-Gys=YUbT`{SJ&wasqrH-K_b)(FBc`c$s;y}S3C;$e095kyPO3A6Q@T8YsxB%s}U)8H@i&K|7 zTg_#zSTVeGLO{w~y2KoB{@%4C)=fqRs6v6!XLB0aL15FHY)8RJV6wy}XI;>yvt}2g z3d@*7?@6aMZ0`L_A<0-oVhp0zFr?x$4i52bj(hWB;P1om$|lGFDIG^NOYdKx)hwhy zWMJ-RnS$_AaXG#22+t6n)>VIo*!@|Ow9C(02_jqB36m50*}X+SLlVgR`FIba_3`+H zZqNg9|E&`?cu@|AJZSL#>;*|-9=F; z@bG`Y)VTa|{_ohl%9Nul`WEb(2Qozc9O<*AMYqY0)uTHFE_75T^lzer_11DUI^bpR zukC~C2t>C--Lp|9oUVGj6-`Rjdro6=PNmrBK1dq~V0gI}SY@RYU5vdl1WAUA(myWY zJ;_-v2lSaT-Um?^C+JrMed4UR4tAMn<7*3E6AO5iW6afL6KXkHoIVH?jyg>Ta$k94r!?bP0m`9bo&_%uksJB+J82IAj^jc zu`5>kWWQsf9e@GcjqLI)>@;*3{_8}suk<^)2jHvhOtK}h=@g*@hlJ8yk-eYa5)!rR z2;4MCYhn-!&Tn*Bh34dT$ad^bCMayl205L;d(oLFWT4p~_f4UZ+`hiPj4%+H_Ghi@Punqy~BLPHV-D zP!r(Kv|YFSKsyt37`pp)9JKa8(Uzp{xb?2R`Gw3Xoxo#LVC#d7_(_GbnNo>KoPNP% z$w$*N7TGtI&0rldXW3t=i0z0SRP^FYQNF>Zpe$t8XSBWDE7QwGo={#~jC7r~sRy?n z^d)`mUDdGml*YpZetj-7b{cRWh!U<*)qFHK$ zJEs>|cu}=9E{y=otBT8nVT4uC2&s}WJXc6K70*WU$oHbSUS1cT}?2py-~<^L%h~P9A*2ye92zl3SFmzgyEc@#9n7o zzKbWX5|6;lrtkg8hlr(L&2|C~8qW6=(@l3DXIW$|ZE(Ir(%@Ovd0 zYswl&=n@vb|A?u3m;!1x7*>6=6;T@vJ+M$FkxfBVDrqVxBjZ;8J?H1V2L=K9I|Xe+ zpprYPb%^xzYD}wX0=3eGRP>pHv!2UQ6!|+vPfpYKKgxl~IL^2ru+UqBbD_w-gP^T& zouMCP0X&0}ba`EAdjtJP0&UPcWf7zM@J=5^f15|4vxUL2iZ*XHJdy>vE&rM(+>j{a zj}eevxi;OrSvugr-9`;#BKGBZnD^NEDj(;C4)`kgM`02n2rREqg3Z(fVr)NE+GqEL(wqCe)OX+Sy|vgg%S96 zH3bgbnr0qTFHd~x_&x3}%F;#r_+cnPiGf?TQar|*oYZu6g}KO05!O-;Y+FM9DK7`h zi9x-C6ea!G`5B;P6iM1RGwZL`od3O$WP)}flhBDtFN6#w2*GE%JWlDQ)SjB*Kj?oI z1uZ-?RzKcTOO`94E`L>0tNJl1wV73C>u}hbWW=LUeGT`Q;9I1B!CJ@smr;Ey13~^5 z8O=g8m#F9+h3P=z06vI5&@L^2eS_X)+cpe>Kx(Myr8>#i*6a0C~B<{l@%N}8zd(qZb^p9Y7Ii(xCwu;Z2nF?!R^ znSZA(6NTDj+uZZrXPP`Oc$Y<=5oL36lI#vTh1oP=cXeD!oy5+ZyruL_RP0NmrOi=U z49lqm@o!CiAf{iX_9;mFy@Ijj-m{kR($g_?{)a`c|6K$$gN;>EaiA+A=`=TkcIM9T zhH&$`#EOfd2T!ag-F!~Sso>DGc@TWyQS|B0vK|Spy!2n{{RkH6Kso{$ImWq9%2hDD zUR!a$jqrpl^tbBBJnnAr)(|{Vv&6sH>gi2MW=q0tR&|)eh_T#-ev9J6lOxH&#gc13 zM@zz0v4|t5E_JKCRNZa(8n!Y5R7vOF)w54|d%tFl-fsIG0j6=&hqXoO;e>OfqrZ-D zuFAX2oRANT9mO;}qK5=+7@!wRZUp|a{X?xOn(2A^v&mw4YNbQA(=eUxq^oO7qF#uf z|BtE9NFfRP(nNpb-eI#>*jm0o4!NNEtT(rCMtEf6r%$9g?g@$5r2I$$hsx3w@#TI| z5~c6MquOnD2K4}w6}rUBp3E|>0>rD}7ztN|gCRgd%<1R9)XA6eBVmzAFJxw9sOc!> zY~}h zWxr+oJ~FLDIc$^*BI_yB00%yqVCg;|X*TVmks}ng>iZx?p4ZE6+zedLz62S9IV4k( z9P)$vLmXo8IXI5De#MBLJHNZ-3X4x+ zrZm5}4HbW^SxVmjRQ`17!d-iyiDiS2ly42UFXHs$i!A&+52y;6Jh^a+kp@jQUwH8- z95h@p@E^${@G+%i@){e{xZ1X%O|HXKOFRaipLt)isY+eVHFap}Z#|t!*bcY;i)-8? zf$fRdfY2d88JDp4`&OH9zIOHVA5wF_2gO*K&j55wD*)T}aPc{Qk|tek)7Rz)&s)Uk zpJ|eymdY4meGmbSgtH8i$81WWdr`Xi`XmG1+xfsgH)3RQr`2WhEDhS&54sk=&P_s=Mqvd0dDJg3htd!Dy*~9Z*6=wX*A$OqI5O@E6-q zo=#sevgRczAA_bG55}`AKk95%tKdy4DTkfWAN0~LJHfVJj|53UB?2qCj{yp{Xg;!) zmC1ts-tU?Y3}f4_sCNyUq%PY00y}|3Lc!l-qyDm|a2g{7j(3AX*!NqeoH7hRT16SRa+hZ-d z^iSUS+Zz{3se;NUr@OHtw7~j=yjAR-XN_CAXnlPP%-Q6f);&K0ly&HUvpJDrXlR=c z{^4DUEsed+k|K8VNmoSsxF|?ssUpxR7GO4=MHox0GGZX_Y?;wm``nc^qn^CfHF}xGCXOX}QYr<5b=+!varbzh4+E z|4IuLf_{*WcN=}2x<82BrtHd#VWs4iT@spHcAz+PW1$oeC;WO#Nwyy3^RLi?NyCJ{ z+~u5v7*59{nM)tm3%9|*B87w}X1%b4wzl|IRe(CT%<4Q@Q2>P29g9(RJm7L$fK6sl zOoVfZi(n2UY8G50Wk?dX^@@|me+{nD#0AMZYNKUC=2Aj$5{ejobdAO z$|As5BLSJxmYsg!JVzN9wq(vvnQ;ykm-GF7)i!TuMC2o-Em*0*t@K@sOTel(-wQ7< zDCg0PR0$mRZL;o+s7cho(87#xL?D-$sPx5t35bI9v=D>x3N%2S#m!Z@hyxC9rYmV{ zVRS^+5$$e9pPHq7u2HAR5)cWv0aMxFeJ>nD;gJpZfQphz$=v{b7~O1P(`jIoHxWWvL_l*A=+)WmlRI@V_DepdYM!aMRgi6q;-R0(!d62O53 zU#b-&=m06Alv5ziKxuY|79~I^Znta?%jr{qrG}4;B+)#{mxruR)wFrQ*c@N=%V;9{ z#6&TL>3PHz8XRZzXD4dxQp=LM^!0LilCK}wwihes&0LXhr8Msyfd>}xv#z|CRk8(H z;o&|%!WhIG1b~YDFn}I&R2tD21f!L=GAy17OEr1==CJwF#&e3{<3NQA>z&}wy}>oz z3b|IUF)-m5-<+;V8%R>EpAWH~stC-0zv@A&4FPgFHBDKT-b??rKO@jt!s;O%pS*37 z)$T1}Sq()evBp<=r3c52wqh20)8?ZH6M*D7qcqXSg1HXOkE~O5%+1xWq>kx^KJ#>6 z`Qq0+?z)c`*>^v~4zKsnxWM7&IA5^si~qe-KJz0e5;&Y13PK4LdX>#^dsBbV8>cSC z(}UP}84IRuFTC?SE*JV1Fd{!ugDcoq>7GjiY{Flt zU-n0oeOQfoZs|cEehRf2r7yCEv>bz}hKKU{!`w?teRF&lhz9qi=GCVbkEPt5E2Ix^ z&e;1#lGyauB3Zr+kSfR8DZ7!Jn|8qsTiiBK$a|_8S}GF&J`Vw z5nRyi1`m25UG2?Ka>(fAv_wk9xtL(4ymq0*9`jf8 z)hw{e7f;2|)%7-!t7qQA^S)=Co|BXMcQi{A69qVUNs^~ZgMu_4rp_+=`V^LU%A0PV zABYPfQ?oq7;&PNvql!SlvZ^BGXyV8|BT*%S=SVQXZ#R2>5G_ju;;I`4eVgRomtI&Tv*KEwlCA&5HO*YEaL4S>n)u&q}qv!5FMuxby40_9WCe z?)V#-3T}aze-BS7aM$8ac}e8!0{YLR(F zZW)>6dqcPsZ#~K&tUbqVb{Q2a4s2=VoXn9+oaDT|AOE6H5EU6#YCtz?94FbVxy(@EpMNC1StiCSAo;ZP zzW-i60`6d*@SIPt6ab0i%<#F&6N6^hVFa~)ZojD4DRTQG;PbL*?zDd_8zaW)WZBWw zvzsWZ=5?wvUC=U^_CsRt8LK%~2FbgZ=fjBIbz{VgXvxSmeNEmIWlDvogU9dMZ8Tm? zA1=3*oF7nRL>D_VoF^mK&0=bDod5M}F9`pWCd>4v7!PO*NEViy{GW_E-#kSX?c_M~ z`a+QJ^l?z}Yjw=l#+#*-3Mx@2a~t{4g6SfX%73|qkMEA#6u~*1pQj~ba5m^`BXO?j z&!?(VTySx|n%}sPHG*Xpo|&Xvu@5#U5&hLLL+!_eIHiXV_a3+6%u#gSJ?{2B72@!_ zL%;^O;Ba7^h;6oaUbYNaZyK=EUunl1lsd1bT;A2xJ@9jaSVtS~=0jI0{3Tsb>5~OS zGR-SMK2T>U5Ez7?e%w(p4+b9yl%@Zw8EKsuZ_1ecTmChDG(U@3+xpE`zJkX5fms&u z;-yN-i17Mu5uItgaG=FyY$M~VIkAbe;h82n&Ev|V)5Cg}>Z1!{3J-~mbRwot;}I3! z+Qzot=;2>!Fa9v+T#QwW?7a%O8d1|%sETMe%-A=qLZZNupEq<0xKqvaT3>btDmH7$ zN2_ckC)J*4!n(e-l`nd71dw*{MMl}UXN8ud(bEw(= zmysIk;~~30vg58fl#a&;dG|$VOp>?4{Wa=Q;+SPH9&>S{=_SJR{2tA=)*P*5pe#o# z7s7gKmSw5vfu?-z0WZg7CaYg`PW5ithA|U$fsbuFv46PjP0r*Nm0LAKt%yCXc$qi? zm?45J-dqta`6Lr`?PRD?jY%a+X(VsFVTKf5PIkOKz?V2~*fi@Oc5;(LrqK*hVvv+cw6@mSe{vEm3tKDERbZ)00(JJ^QR7In@>vvBU7!5J z)ntWQt7}K9o>$NSkzI6U2O2=ibCOvp<~F5Hb+9@mUv?kSnl_zXnNl~cHsq46 zLc9%lLuw*M_=hlXguY1nPjU{DI9?)3et7MO(t>D6}aEbA?{^@}QEtUU};fY0vN(w>JpY3DvSm@@qpZvP= zewGOeH!-Xq*t9pvQ~vu zf5~zX1F}T`DKwNOUiBUz6Sb^t8-hJ}->~hov^z*$!_LL!?-}GUgOB<6`UKKUjLgf} zdF%M%fIT+nU+E8LQs^U&#gMH_fC1aK+?RwGIus2A#)2E76Y1S0>Qp{TxuDJ|9?fPN zz;%pouxOhG_MhOqu)zRHWL>+_PaLW5P_vOw+Ggl6cpS&*3)+Xr$CN_297-I)kE8Ii z?A6ZBDd_q0f}D_+dPaq~N;q&y+QB+w75LTr$eJT|K1{mYW8lkV$&ARC^!}YJ&}%)# zNVT3yhjjNoP0wpHnvm=PY7iG#cDVU+5boxwJzxrY4}zHHqyqA8z21E0n68YL_K$H1 zC}e;GWh8-w+V}%mri3_zN2N!M$y6YEe*eG0$SSTjvLhl!YH#(l0^0>l^w{VZ)c8~8 zSK3X;#7j_8h~KDHI*;`;3?A#gjq|ZQc^!TIt;UDMIoEv3^5)?yUs7H=7yvXl@)TkB z*B$1#c(V9-i*s2m5tFWo#6De^`>*qpK46T5rBeI8APgm+E@#ay8%P+t;R{jQ@w zlUj|Hb7V|Zh#sZOC;sGHr=82c9EcGNZToX=l!+>Co=PS|supaD?e*p|l8nRU8Vg@$ zhFFD{RMo}cbK{m!{U5oImRAIYYGirZ&>1J}Wjtg4vZcQY4u8~QL+a(?O+Ps5%Yy|~ zWT=VyM0DI~#>8>uB5aqq@P1DphCa+i&Q0H2Q^6$TW_A$VG%a@G$l`BB}K$3^OPa7a%RXxuC!bLAJX|pSnSw z?4HY1jDD-hxSyNaW4p5t?>RX~)-;apmQfac{Sm<{|JqLkX~I&XPS>A1T!0FLv?Ab8 zoZ%@ZI@q!H{XOv`vOZ%R=iCICYUx;zF#EZztjvEjXXzUsyKG;jTGNtGbI$fMv(^hm zjA@>mNCUo1O7~5B=HAk$yVVd4&`%@&n}puq>J{0=IjI$A7sB4UmKJkdkLBE=N5<${ zwu|gJphi(LV!rpHg`+ZDrTqhBu^&+^k1A%fKqc06dn!C@23tJ6Sh_%Bu*VWmHLHOj zfkIGvDXT1faek3n%=}{u2I=I{{O}8hs%Yp@*=#_`7>`4IlgwX#dF3indB}T#%SuZy!S1aSI{OCF>;V4xQ z_*HzQ8TkH-lbb^$(M)c@0w#z)Hvab3zqLEe`Mq#`Ck`n?x}Z~ThD%%8?yhxE#W$6- z5^hU-dzZpJ0Z7+TATADt&rr51@=TDf0_i1NG`zC9|MRZq_D6y}86zPWfbN#%e zm`+u?JmxU-wbA=JSrg)?1=`KNA$=uuu9h>C`%Uh*^z&yIM63Zf zJkWuz9VO>S7anHnx!|R!Ik%1%$*EeSF0Ojx`)i zeRX*1%`1;uU0?b7!*g~G;l&cOIjW7|9q~d_qG$s}5|8f?wak!vm}81{^!aEn(9tM( z2+RYGZPx9|$B;d7w~ICW;@?&)SDO_fULQ>G;4Awq;;%A1+h=$TBj!Dm;#9j&Yu=VZ zjaNwudmaA|y3voDE+5?>t-Kvd>K}9twBuIndJJU@o8%KIglKD-So#VP|e9?Iv!p-mnhO>uv z6gf4XmAJs0khwh8zkh$8)s%~Ll(2f8mxtE*6sN@O2S~{GnW#7Bi4yRK`UswneP9RK zWdRwVgpjE?byjm$N)r=ORTapznpbOTJ1U71H7N>tdnT~X(h9Ot{B29SyGS#Ig~Mh$ zTtS-%V+f)|i~=Ag2s-JYV9{)-451SEb5|1~`DIh1Kc03hmwT{p^PtAdhtGMwHtX`- zbI+70tJQR3W;{O*|KsbRny;Br6BDD$>Hg2>E3%QDH<5(2HkV(FBubl5N9<5O+-*tF z_|S40NtKdRYGWA=GsrApgYtff_C&|j=n!tRgtJrDwF<5uq8?*O9vXH2gQE3|* zvFj3>oDNjsk*&qRTD0B0e)e2oqHdeBLx};QZ=BqXu`3n(cU5?+Z z{a)9&2WGTlMOE%6Zwv$&hq5n3hF%R|zD2glSVzOQe{9ZUzFa!HQWLe&fnQR_wyQ^d zDd+_bV{V(JwySrh;{6PHm2ZPM|NeU}6Ti@Bk4dXG2k$GONm%-A4-tcbl9EEQok?vh zSL1b_G$h zQZ{yXgf+{_o+@Y+oOt1| zL*vO6gX2uf3A!?*rAu=9sy!e0e*XD!>dW@}R0dhGdB>K*eSfsnY%xXr{{1JXhV9G| z*e!`Ik4f>*C#ik^`VH59TqS}g)v<*~=SdD0s;9{{yP2`C#8Iz*e|qK(o0y#3t5ZSN zp(-Vw@#DE~W*NCJ_2XI1Gg#lHEqyC^*-!4G(53Jk(7HTgN|?_g_2?poC^^4D+Oo?c z0hn#5_tcX#+nA3|!L434Iq3Zy8>(4$P|uHneOr3Dcf)^1zphRyfwMV(nk#?<0d z{NIp8mtex+SLI5!NTZ!8&TWtfVpDZ57F-($$9TN7PUi+MeEL~q&+Bvht0(HSbvsY> zSRiv?%7A?WgCi0CM#n`qMiO~D^Gk%Lb7$}GsF%}XhIgWUZ;NOqW=XyZ)p>!H;uljR6^Ui=E&P+J8{dEmH*9PQW)J z9ARYCA{Oj>|7~+*3hg@z;3v-^$a=^4pf$^R(=UK63%Elr_hz4AnjZxTjSIW&5pC;i z8Jz>wzwDy_^&v`p;`?)9gP{;W^SEJ*s7pt<-vzxls3yhk;0WZE+X8 zQH&2x071I+t62Nd_2J-TbhOFwCM5(~$+wPNWf7vP@JLU5Rs!gz35TrRT%J{M;Wdxf zJ@B~zgPw^{Y5K2&muxPtSUdtp$}y}psq|DfV{`=+{u~q1Wp)=6U-`LvOfzOc6d2Iw z_#T=)57JIm5zC;xz}3#FrXuGgWxCJrN$Hz3-JVHSsuHo_ArZ1PH@BGz9^Rw3%0HZF z>g0R=PVrm?>o9ov^>Awx(7oGh>W1d`+Ua#54HBz}-?YOBmXvGO_>K-}+pV3X%>}^f$rYz;L}MYzIea4Z^qq4`TH-1 ze0hXYs&r9y(LB;=mUZEmm;kfke)tblBLU4>w@k+3CLJxTuoI|YhO-`q2;DYNK?b&V zJes7`Un&&vqFqDkcc`xw_cVopj^%f)E*8HZP{pZO$M=|zVvhss?3&p4xH6g4P zo2dF_S;;5?lZ4s}14W-Y=QLRO5>u(pLVy_NTa{X+Ia<5SDJjbVrpX9x#7W&*i)R&} zb|8#sJ`0tU8Z;(!oY4-KgoCEF*!*`tkCSR^WtbYjx`8VC$elY*98bb+10JH@R!ECS z>*3DRk%os#NGDSil;cMMeuQi)a$rkgmR0Kl^!kv>3Z*Z~;oW3Q(tDCC%Ewlf3MU0D zJrXpdMe}c`Soq`!rlY`5W>rNn<0|rv6+SVTWIpv_zY~+B6%q>LOfE!*{rNyHry=#2 z<0c~fFV6_eg`DopL52^)us|+0OK=pLoTu zb40L*n7Ym~QV(N8!6_8GiE#B8AGzN?nmsngr($`g%v4xo#4M_wQrmksfK>R?`M|*M zN4K$Mu;98o?!v+XAsTIa&x7>I<{rGPo*tdd$6J!w0oN-uu^qcz9dd$?1Bt zqy*ia{`!3q9+>X>E5gHOD8NlbpBBn6>Tv9mLA@z>7*8_ct=MGKr19Kb%7mlqb-2hA zs13|Q3BPWjmp!nO^92+tQCYq|8$#JFF}$->6lD7cu|DP3`GMqm#w%tjFP92pvPjIA zAB?4QrN%1OUKdf9{hO-^Eh2C%R-^h7vU0#yI!|5vg{Dl$3uU-E-{zy}6cjzr7t46V z2~}C|H9D5O{w%EI&}KVf!`kXoo$l~(=}chk$=8)|$Gq3udiB@O2Czlk<(u-7D5Xfm zPQp{m0J$3=w3h1H>MEGc73yCwnapw%UiUrwj)68t0Axu9kw>4VdNa|`?D}@SC2gLL zQJcqpoA?g?3&pu`);GHj5xi5EMlorf{6&|bK`_}iIHhj1(G!zkUB3-RSoF(y%KH*> zIf{Nfx#*$%AvRDdI^RQL7FT{zE}OQHVTLK5<`ZUo%8V3cy^Kw#-D=`^_xligk-~!6 z7)(gxWIkjc4~OpVo(?ioSMX`i)W2F~$-mxQBB6iJ+;*}Zko$7Z3gY_LOnEy#+IFzT zeg*q;cDSYo(22t94h6Co2J3puyQ`G8DyL%&&q2p`S`8Vjuta#H4E-(L+0?Cxznji~ z<)GU`31VyI=IUw7S#fI&3l_Va^1up*3mjH~u9tDg*^+MV|D~2XdZu|lc>cfJch=Vh z=*RU^Y$9Jtz>ANQT3CU?be!`9gtjaO9UVarK6NUUJ6mPL^_oo>q_+`oD7QD_mMnWP z@pkgXwgT2t6mymPjLDlHx%EQ6WB52&j?$+%J~!h&UI-!XoQ!uIr5o&0BqdWb;UN@B zRZY8Dj#YE@)LlXiMH%L-Uaa)6I_Z^CWAEI5B>0sh_dJnHx=Ckl>uDJx*h!E9;nii7 zN#ZjSdC;-+B=<^*wrk@cJG24|>_7UZvx{ytenrBPI?Z&**~6 zY~oFc_u*)EvK!>J62$OLBbw`XmJGgQ)cr1u>o@X6AQ?1N@HL0r?*POTi9A!sVHDDf zMgyV|Z_GmLrf<~2Qrh9Vc({xmM~)V{9To{U=!Lutiv~Pvg=Sk!s6o%St{NU$Cw7AD z7P$iV62xsLw*k3KXAuf*8u^oR4_MDA=Uyhap0<*R(02@4UD6;caQVUzo=3mj@#P&} zx{LY71NwccMJ2Wkj`DvQdK(+|U$vMSAH&vyo;Pt9Eg9gupXQg=;nbUxYH6P^BiUlX z45fkC!XpDDf>WZ5cOk^1hsJws(nGO$BRKr~_|Xl5HLQM#h&!`5QC%BY^~fQ6(rcGs zZzrrLA@fet?{ry?)l=Mh%bNyG>B{W+UgNHcG%^z|KjKH1X@Q4R2^)b_E{@12Ur$!% zfb^Syvc&$v8NDR?Zv2>sh+ePMFgp2lckHgeq@C+B&>k$4rG$!o^FYC{iO-L60Z~vb8uC%QD7UsKHRxXf?w^?V zN{v8oh+}>+6)c{pViC;S)G&xX^Vq$m=XpJ|QONdiP;q;eR%tu5LE!VAvL?us0K?M* z#0eA<#^M#gQ?W}YH_p-eg}xx%D9uz0^2L}^XUu=dmvAcX4_fhn0ripewX{|K$KMT( zN00I}ln(*oUfLP_vPIZJwP}+CNPLivR~OsxK2h7qCYU7GK4QPDtMc+KWd&b|-GT02 zLai}|g=V{$%zSfPQxxONbgBoR*!XP4J-S(LWtunnw3_ke3u`QHh?}5hPK5^W>9~z1 zdL!)~$Ml{)Z-IS9NK$Or6kSUmdf5|Pd^X*>QsAEaZl_w#-p;PPtII#=KD|l{8%-nI zRdA^B{rCOSH~_RInbJ3G-9{42^s<4rKhh39g4H0SM{N zY4>~)JLor>Aw6-aVWI^2IE0xT3Ib21JTn>SI!-QZ(Ax@$1hn5W?Vzd}>zsyM2T=vR zZ28A>s`H?;pz)K7sXOE_2GNfBdU1>!=`+$BmS7Z1$c+wVg^kEqc!a!QnCRVm5hIv( z@}Gm!uG6%#+jtibkeK>E%Rsv#!$y7|5b;s-wzw=*L1wT!#C?rIUfObuK zs?riE!q-58GfvZ*0_HQJPMNa_&g-QK>)rnx^0$?Ea0tI%h)8MTV$8hyo|XpE_R;?*%lLn})FLlVW50}Ad2bAbxWKQO1h zhYv;^p7|!z+447pof0MI&?C@~lBa**=0n~bFDaO$c3cbc^Cu_X?ty{MPFO)c zfeD=DXfZ!0t!91uf2w=}(_y?7Fcgs8Kqa2_$g=;>uz3Syv9)hR!-tv(0`^d&#I8?x ziqUL?4zKtc`i4RJd_X_Rbxxh_Hvxr`9|6|8GhTOz!B{)W&qcrUz3_j#rF*_0saGv-WmBR-9?JQ^RHixKt^O)=EX3=k zt^KJI42?2Y*q_o*TD3-E^E=!Fg!vEj#w zKnSS+^OIY3G(4X5f`Px1eeNEBg6_>mC+}9n9{1h({-Fs=-y`|ZFKg4bCCr;3ApQ2I%t{uyAIz$mKYg+DnkA5o>s>1b z4x3e}ea?{VZ9~}nC=;RYQPlBjeband-_^;o{3C%h6o=jSY;GwdcH8LOi$~NT&NjDU zo0?>`acnF_!y@h<@~mUJh`ABLeKSJ%Je{3xY(*{KyT{8pO?<3e>vtLl#Nkz@W*UD7 zD>g-4n#d4V0N8D;I3b|2RaBvag`;Viv`9Ol{4+$lNm&IBHuChuw2tF9B(yksmPcq% z=y>8<4ImWm#mN75F9sF*hzVCn9p!Lqe`%Gy5HJ`7L%y^FCOWmF}@2Up-)IELqW!fBL!f=%jTE9its<`d6 z4RpKcJKmrRvDop@&DLi&&7?Qm+*nkL|3L>rvofspfG(4D{lj+whC{~Q^PjX|vR>!!2*kWr1XQz` zlNahhg>NoG58;GP0iX^IZ5@u~hZ?GO6U96OR%e2TiyiGc(v`tYWd5$f6-0K`@WLsG zkH=y&A*5J3w0`HTW~%M))WFlY=Yh}7VOaI2ZNbMF-=49P9eFv~*Z%mw$ZW&n){V}+ z$uB$M-y^+A8qTorJ3eTJn{GUwSO(>>r#FBHC*WIyC->ILTN^>y**-o7VRO~?4#XnV z%b=g)yt*b(4Ho5ZA@CQvUUeTYJ!l@ICn|pUnJ#~Px3zphb$%xJo@%FSLqyX8M!s-H z!GA(gzy3Brvoy!j%B3?YrzwtODN{Q>khfZ^W|`$qqeDGrOL}j)?wvjO2wK0y z|DJz!AB^Fs)cuAoj_R2sm$r|h5YIqOK)5KAe|wNU%)kMc{QNBMKMQ0~MV>R9Ap0cQ z9)A$|`i#Yp_Raq-flVy(+7v1ciiQT-F0I*hC-i|LL%t&c_U!oRGg2)p4a=A0u;Em5 z>))DYdQfK=5xt3ms6j;aKh`RG881JYD(cY!_iF{E47IxlnCQrq-rMX_7Tl6mQ&YI0q>V-_Z`*?QN{4FG^>cmRtTLFt7) zgGO)_L1hRx37J7HZbwKqKFn z*Ue?5ez}?vRDvt%RWO(@UkxPbNKOhpQ#rQwo27n`Dr{{fys`g{!~ns9aA|YO0Fqe~ zC|@wmoyF(@2n;a2!g_NeubQ7&*c!KX_l1)oONlGiGuL;s7O;1DUw$gLvDzD+aQ{yd zGPE^C)XScr()WT4IG6AMg2!wRdD*t_+w@gDYX@}Jz}Av z&ks~>RP#}$g34u#hg*{pmuXl$Bpx$;o!xbxu*ydkq_$q6L`>iH*`q^k&j?{X%fXu2TnS&JElEP>)!`w4noP;0CHa7d%nFIGl}-q?H6 z*!AJ5t+sBOLs#cvp2V!^`05qhxs?TFm%yKjw7A-ZZ_v}pfb_~rlsvSX{(ZBNHrKNi zciQdr=hRaFi_!n$l#l7+e`jctV)|N&ma!ISrH*~lV1R^=y#Q^!b_9hE^XW;a#XB*_a~#MASeX&P$I~d6I0~ zdT;&tz}5}6m11un@P((AI)hRKzUaIcCn}fcQk9NOW|qzBei*;hyOQ_eBV(-iYk@Imb7!@`l>Nob>cirr ziG0MdFFNKi_;+y{hB4udl(NSgCtLejQ%tXm3qJD!jBh?Te#rj|a5BCpAg`AYid}Eq zhKW$TFJH?NL#}nm>5DD)W^k5`S7sHh zXT)0cdxw zM}uGcS1r{M_@RW_mAQT+do7O3yrW$JM9Uu#8GNCj&J=a-RW)?>`CPM@XnH!LH0^dt z%`5a_{oG@^tQ(oH8q+t7688A}qp3`}^+12WQyqYq?!D>XnGeM|?AQvo|A89b?dIeITF|RsQ^y?LWfk(a(s2 z3IYieng2JSq1!_?=Pj! zP|RxSKSOEiiG=jo+1a}@@-#&gAB?E&;};SV@+0qv)kRn!QuLbFxgN;-r>!k57|7Ap z8-HK(qCfv%XHl8xlV}`HPlK8J!ong0I~lA<6NT@$6_7rD2J2TRvHv;rip=>AqfGj* zO-MQHW;o>91vRA#rN1nJ?dttsXny0h*}DIgWu|s9R!6YK<^|9mQ9EofIBluH-b(R% zaKLDbZ0RsqtV`{~VefS1M04c!@8eKqV#E zi)4LWdievwNpH4LG=I!L*&EUTu>5wzHkPA2pg0D!+2Z#`T{~^7t;IggmiGe)@+dO; zziR>Yh}(uyCL9Ujknv&iz2P%h(gG}Vr`Mly-Ro*STi>16I!7BZWemFW2V}llK#6)Ay( z+^hPhjD*i6bWw3Rx;>w%RLc_ANw;*+^m-(!#aGJ`sx3omH#g>-tlqd{N!R_*!5p3^ zRDnn*P&6k{gkx|~9Z}8Ct*cu9Asso7q$SkJlg1N14lVvRfWDlK*?FAK@PYaArJvDg zor%y(PHp?W{W4kAH7ZTjGWa_an%&S0&SKSO7T&-9=*wXo*}s;PsURM`MZ58?PyOUd z0w*8RaXXg29{4O0y3PU!s-~B$oLVM*#sHdPBR2qfo^@da#y8}?Vx?~Wqt5sbzauLI zuy86=XK`&Z9geDJDJ(NmdDwJ#Y<>ImhGp)=uY(wp)38n)M;m-Hot+Ex!2WwAf9|bW z!wHl*A^v7u#l7)7zXX5DwgijMT(*CS1i7)hY_tq5J-Tf0C7N=7r)Y-xA-{i*nn#Xj>Ekfes zo-wtm>Z{N9OX2XNY^x6CfiG%(>O0-j<8)IAg6qFKdkZk&4a;r+0p;f zB@Nr2g`SiT{Nh{PLC4=j{O{n(*l1uPG78=M%-tO;>j@PG97W&r~ zOZ-Q+D=PQ$iRu67CKCz9oyYCQ84S2tNGul^R@matl(D)_n=G0R8;ySI)!j?1%@}v- zsy_WHZ6HNV=l>W)oN@H%fd0vPf-{3Uur{t~_=i9Izda93UYIn88D`XfLuef#uW`YG z&|(=3TX0Hx$ahz?CbaTZ`D8&du)a)q^}SQjdFE=Scjy_pN6|xRnp&~vp!#=|7wMYk z+4uZ54JJG32_UqNnA+1nE>F7{Ij2zATqf!V9u4Q`9=)pOpGwx1>+y-mG?4jXhSQ5D z*`hn-b>9!v-<}S^&banlJ8`!nO>eqx?$%lxSG+=GnWPz;B{0^6w;x{oE>L}#vVPUb zEdkI=H9)RK*GHKN=WZy0_j7Ft?*l}uYAduh1XS3Fj*WHX`Tt~io`>G={^&}%wE^RQ zP4cM^8uk(0STwLqgrG^EO+V7T7c@mBo#_`3hCEZwdlwNI9rBR8TY|;B^En{Ob~{7c z9>7u3@AO+YGGJiKIJoL8Tay_2YK(o!*&Z{}@b9j&M~ML55_w{VCK_9(Ke8arz8Myv z0a~u3Wr!u)-CfnAP9usNo?A4RC|bO-4wiNVRQr=Tv>1dsFBd)PW@~jzcZSC=+)9s2 zfSOL6z+w5^XlA}n+Pl`Ilt0Olb(pPo0#y;}^wwN=d5mcd;FQ`rcs46$9wD`4{EyAa zXD05}&xaZ)X1kBH-T`EBmSUvyg6t=w0g0!P&T|YWI8BM<6qBpuoJJI)x>4aZo!7|7 z0x)E79o+Lf78U5upPwxu;e3TXorC$r=L%=z@u0pJ=<3j3gpf3b@>!1HeF+#;^-xyU z9ReXQT?Zj6&$DR$li)W`H-iV25GMu1MCF}lLkbHm(^-P=Hf?~_7wvOz}F!e2FWc0 zJv8Tl%qFSh?v3JqC7HxJ35?0X)V{SjCPo=~oAw`iA+MKIN&d5C0C{8nS;apO<9IHw z4w-WO6Wpc`HH^K*TrM8R(^hmHD_qK7ejK2MV6^mG+^%(<}7^!NiBI(>Kbs*z?!|0r;-B|w5Up$Ne)d2%&$ zrhkTSeSdd@69*1iIjNTE09TN z_7?Bj*oC9Lu)arh@3j)NiT+fKQkv>&_eD2N+5eR(L>S~GD7aJS zV_Cnkey|Du!(NtUK%oRBf9)w%{5js+c}*oCfzpnWF0r-sMTK?hJQ>7?9kec|sU2d& zIDV^`@$<#D#%HRu>(5!zNo6UvF29y$4VrXS*Q$~^c)~@w{dI#Az7af%V#M$bN<#&X zK4n#56&@lnk+F(@uE*!)I!XUCTvlMe8x99Gc}oOM%bKocnt(kfKVmH=<@*_eo?U#>1cf6;b5a^i=eXxMBa3am1@uqRi9>APHh%p_ zeklSc8cy1c+MU~I_`5yq-_kR%<0|HsZ|cUTW*CnTnKQMO(JlSDQ6VIlY0g?;=jbhPcOO$K-fn?0Jah0v2 ze`2FxT-){HS(59Ys3^@BgyOT`XUiG^^+(zB`Nt??y4j$I zz{p7Wrv&vW}yj?rbR#B-~cuW0lW0~AFS2n#|$C|96Klp z@46Y5=Eq@Z$u#ZD;LjY|+Da0yGRhTrPQaGEEWNL9lUhd_rP9r)$7PfgLJnBokw?%q z5i@AJd#fZshJ6CHm8y!xE4+qUc}f2^(v1dN;WS zvScZ-<&zf8$)S_FN(-+V#B-&b7?2EUJzvpx1-;`0OCo5kvcF!G4s zm0}2>6%9>peN}&A#RZ|$>R*W0>kkTi0wRq6n4!a)sZcD__dTkdQ3(^ty_twxACQ}v z80Xv2@aHxAn1G6j^zs1Wi>MkJGk=HW&d(Rn`id%th+#A`QX*24r6x85$WYJ&)8oh3 zDAT%|SG9H*iDuOA5yc3Z+D_%|gd}9q_aj4WScy|owU3O^LkQJ5K!^N>X`@<4yZQW9 zmI0tDJ~;|@rk3?-2QzbYyeX$R#c4A6o1(mVK@wCRUXv3TdvtLTX$VvFMh4~@u5BJO z64oukqW#YTw()R!QKEoS-+++52UZJYh08O-#XhzjpR=zKL{dKs=Ye?*NSfg&nF^dZ zfc!gotJ;xlM<%VyQP$Gmrh2Eo4fxqHpW8s;0ChC}NHA;UCtkUE#z+AHW#66Ks=)i^ zfE=5Vp|akeVYsA7RU(9CuLPTh^Y5M!$=Cr=&dG%U)>enZhq5`AF zuLU>yoczq4=6^lImqgrn>^|L#)vG`g`;>dI=;d(O1=F4>ZQzJjmL(L z;ao+wJ)1j?dfG!oHtLTKm4bv=n12M5LVTd#bV2AApXgw*4Qp{h zsWUGI{>(L{5@iZ3q9jv&0(jvyJ-*!t6VuyQkQeA^(EV15-#qTo2g3S6nGzzfhL|Bu zsoJz3;W8}h6n+#c(*qZwm#PIc;A#$s!^rLjxJ}K_s)4Q09^GS*@8?24S8ib%E+739 z%3t@WqSeJpg8Igt?bC@x-oOaj6oY0}2-7wT1?yA`R6>2(CYuF!r zTq0v}^s=s3uveF z0-pjg{cmo2t#91*!?t!{$b2S0j*s*`XxhM9kW6Yqe zC1)L%5yinm0TA{l1&hWl4~Ql{|8OgCeP_M1k!;L1xptl!VdXeI^V_t^&P7qaZXPRU zK`a9<<2H6ngV(eaoUR2vPS^LH4c{RXRekD<1qFS=>sCtf*8|$cE{tX@Ljq9c{L<1b z>EIrRQ=%sr^rJ87Fp~zu=1jsvLouHs-)#mQ4_}5_-UxBkyv`>sC?KacMSkAA$Nj<< z?)T8V*|-0N?SP1kbVQe+d^Aa6(mk3ZU))*nFn{S%QA5i_k>5JQUw%Z$$>@PH>U+Q) zX6Acpt(Vc$_bZ9&=a%|e<_9d|8|_h4N>XP*HGU=HHjuw3Y#UtEIS7B-Oi`ITBxLYR z>74qufARa3a<5)F|Dyhxzd?K|Vs77j{WV}MgWXSsU!JXO#CgVRCcBRD*eJT1KvzW` z??qbC&*iUP;YL$?pz*Tj;?O*pygHuE+<1BXDP!f|>!Y(Fwl7JiO;;Azh)S3lM0)=7 z5m8r+g1+|wjzvl9FAg*2%Noa>((GYxd5=&(Z&@!-5{hWHD4i5J*&0R#Xh2oBGqW`w zGCVnglcZZ~U^kh&i+&~Bu&Sav=i3C^iW=^pT{mW; zFUYQuLV!i$%mwIb{GeR7UAPCI@ao7;h*8KhcSskQ_o;7i;IY;9d!jtlJDSr!%Gf54 zTI$n_(-WAF%A8$e5Xlps^Iw!cp!;_)sF8NH;9}#&hFrkJYJ0ey?Vpjd3 zk(sm;9KC0E|8{!)9wv7;cFNn?Ph;z^kgfH*9{(cV(7|Z?E%TfIseeVq9>?EAd)L18 z1_y3DdXD+!uU?~2<5<&})YFSR0Rzj!Omf_7dnYswF%MPuQC<7(r&mB7Y${`uX}8nX z*g}1n{Oua%=?K?dGtJAOA^3C4qG(|aoBQq>I>K8kffxU% zQ-_`}I1n_;_mQw1I%6-i)=QHfb+_16&g&HPnO+k?dAjLHsze8H))UkB!ePbt!d*3X z^v@~9vMu~6p-U03JA@cM>ifJN4UT0{$l6-2E>KUyY8or)(qoR&(ZY@;RI%DPsJi-I zugMq&)?5O*{50tOP|y%<7rmt0pfI71JhglonvZf&WtMb-PTTf zL?jiG6+{mOb?iV)H28g5F=Ft817C7>8yqKR`Po>UP1)SVUZ9`y1uxAD<-$8{ufQD}_{vWXfx`)zSkAaKXl0`5Qg`c{uj zj5n+!ItgVIO&-{LLt(7HL_C$4f|wp1BChuhV66XW?#5K<5V?gyzScjv+s1yuF4+Lq zJojTVoeh=|^RC;vr+L=qx~&$&M8Kb!M9uv(><$(gZ7sn7R69$EN7UhFcaGTtsJr$7 z3vKLWpW4Ts2ovi#M%ksODJGue$A5-{%QvP{z^K&=w`N;)aa~D^Ryxuf)!cO=@8~Ft zDS3&4wsar9xdiz|jmK&{v3^G!f@-=L@U4w1Y;z4n#_;W1ZK73>8K8auVx?OA2dk?{Ts&GJy-WxDEO zbkS`;6H|@fVxzDNC|Ejca6K$tUq@u4+*0i2!xtY-Dh@Xb10wqmyVK~VdYt)y= z`22Z^#n0YCY*Az)wuCqZadxD4tl%ANX>3h+AKMJr2%IdAGk{HjrP4tMtnb0$N2SlL~?8ZZoU*_tkW1l ze;(q)`$lh=TsxTe@}P{kXX;Y9f>UiRJf=24$Z(s>5>BQP!JWc&;?wtu_U=mqT7RZW zUUzuX3s-o`^X=jC;a>+#cgadIdJ=bQ{x0HgxweRC&UMtbq)65x^*4n~+-T~BMl*q# zIl4Vybol;$G6jEScAbPD&HKRR-FOgDHJ=T@r>~d#^UesW&)=%)qMjr)26?tiQ!>R4 znt5T4P#J5A+wQq|vK+8{xdlIVfDNp;$?F*OQCVZ#s)cJHqY%b=ru-%56#y%7BqB z6tB3Wx)tuEdod?6Ip=z{Uf}CY4`1Q@i#Rfb|Lk#E!gJ!XU}Kg7UsXWQR5nHz?j4Yr zCH=7(-I0Gw7?skExAYTUTly{%_nhguxDdxx#&Ejlufgsh_89#J1#Mt{3Q|Rl8E1lv z;Ozpq6n*M!fC47glo?w=Av-pg00$45+;3$( z(P#aO%i*HbLU8}N)J-H77o(9KMvmSs3H7}Qz0zdy-j?;9O_b5Ic8*AkNbGtf=9PH0 zM?|Mm!m?f|i^2yLXDMl~f?;2=TS@w%+vF{s^PivZZC8RE?AE88mDpluqKXXlWLp+1 zKT#DyTUD(~`ieR}s zrl8MeqME~wxz2;>sl-A7P9Q-<69rM^F=Tq;fSQpFYV~wo&2Ae2er(xj4j&jenukG5 z$gvG2%20^P!XL6Q%LGRd(o-QyRoT%zyf#qicNXqquz)13TgROF=ql6!L0lWc*qKPu z_SYhglPu|OsRdR!QBo5gqNX%cJRTNu8KHwlzKkEsTs}(8rL z4rVg?giWrpKI$*wUylQP-dT7cqW_M_HXXW2k$2@ja+TJ9=DVD_IpxN}<5(4mf3BzV z(tcJZz54MQ>TB91fUC1+$S;=;zajad*=UglphH(H_~PnQq6|u%g9JFte}d_73W~Bp zVRi5m$#_s`K|UgM2he;GGgIHHi5gGGG4tPrf5uz})p-{jSKXZL)y$RqeR$q*T}Y0i zc}^`KvMn)0e7*MD{u*yO?<4tJUg4h2L{Kk#tkrd+z%TmjA3I;`10`SgIAQazowGZwr8K zNF%an%uV5(Itp6|ao-7_9w6;CZ8jtp5D#MG7*M6Se%~n*e#U;cdHmXBDjM{Q(+~O) zWA)_jC@028ajYSOn2T-P_h0a_Wz0x!SY3?OCTh$W=f4A+aN3^*?z;{~pTaub4vni! z_q`}N`mnIdo-Z2vAYKf!@KIe7P#OvTsSs-+R&`9NR+8DykJbC`?-T0GB zv#yoKyxc<`f(7(V8Zxv+o)(jfKlZ3U43CqLwShe%Xb)yX3%>TE&_MC`l~`0u7!aBN;2s^K%82D1IMs;Bo!t#V^tOQ8p}9ll?KzvjK^pGZ}X$H(JrdCC;1E_57rOo*l7 zVI~x<+5B9q3Gj@|I?OhB0m-VhITDxDgv<&d&DNRce% zrC`M_P=Sw)E`uA@ku*VtSI525$z})T=Vtk)G5UFgR$dCwN$vyQTHeKIZ>KfeziKt9 zl7;^9h$dS)!{i3tN~;9x9Ia2^lK8&vzuY)$PFIDv3qtjVi@m6{zwLUfqBf+)(ILe` z9oK9PHcG(MBsaMB;=AwX$ArxI`f%pqI#8IYD_>f|rahNXNjPQ*>mU zFK}s(jL2HUZ5p80t`~Z~fb{0R;#~k8G*A>lybqTyM!emcHfT$N6!J}Geve{Ð(G_E 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 diff --git a/images/krr-datasources.svg b/images/krr-datasources.svg index 00c86eaf..e99e9b03 100644 --- a/images/krr-datasources.svg +++ b/images/krr-datasources.svg @@ -1,72 +1,85 @@ - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - + - - + + + + + + + + + + + + - - + + From 764d5df62e4194fa5374a118ec3dc18101bfa36a Mon Sep 17 00:00:00 2001 From: Natan Yellin Date: Sun, 5 May 2024 10:51:09 +0300 Subject: [PATCH 083/137] Add details to README on airgapped installs + prometheus authentication --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index eec9c60a..33650c3c 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,18 @@ krr simple You can install using brew (see above) on [WSL2](https://docs.brew.sh/Homebrew-on-Linux), or install from source (see below).
+
+ Airgapped Installation (Offline Environments) + +You can download pre-built binaries from Releases or use the prebuilt Docker container. For example, the container for version 1.8.3 is: + +``` +us-central1-docker.pkg.dev/genuine-flight-317411/devel/krr:v1.8.3 +``` + +We do **not** recommend installing KRR from source in airgapped environments due to the headache of installing Python dependencies. Use one of the above methods instead and contact us (via Slack, GitHub issues, or email) if you need assistance. +
+
From Source @@ -315,6 +327,16 @@ krr simple --logtostderr -f yaml > result.yaml
+
+ Prometheus Authentication + +KRR supports all known authentication schemes for Prometheus, VictoriaMetrics, Coralogix, and other Prometheus compatible metric stores. + +Refer to `krr simple --help`, and look at the flags `--prometheus-url`, `--prometheus-auth-header`, `--prometheus-headers` `--prometheus-ssl-enabled`, `--coralogix-token`, and the various `--eks-*` flags. + +If you need help, contact us on Slack, email, or by opening a GitHub issue. +
+
Debug mode If you want to see additional debug logs: From bb1f3dd01d1486ebe0f80e586f0c0cda6f644da2 Mon Sep 17 00:00:00 2001 From: Robusta Runner Date: Fri, 10 May 2024 00:14:40 +0300 Subject: [PATCH 084/137] Don't show HPA warning if user chooses to run w/ HPA --- robusta_krr/core/abstract/strategies.py | 15 ++-------- robusta_krr/core/runner.py | 2 +- robusta_krr/strategies/simple.py | 38 +++++++++++++++---------- 3 files changed, 27 insertions(+), 28 deletions(-) diff --git a/robusta_krr/core/abstract/strategies.py b/robusta_krr/core/abstract/strategies.py index 0005a227..b63b2cc7 100644 --- a/robusta_krr/core/abstract/strategies.py +++ b/robusta_krr/core/abstract/strategies.py @@ -111,24 +111,15 @@ def __init__(self, settings: _StrategySettings): self.settings = settings def __str__(self) -> str: - return self._display_name.title() - - @property - def _display_name(self) -> str: - return getattr(self, "display_name", self.__class__.__name__.lower().removeprefix("strategy")) + return self.display_name.title() @property def description(self) -> Optional[str]: """ Generate a description for the strategy. - You can use the settings in the description by using the format syntax. - Also you can use Rich's markdown syntax to format the description. + You can use Rich's markdown syntax to format the description. """ - - if self.__doc__ is None: - return None - - return f"[b]{self} Strategy[/b]\n\n" + dedent(self.__doc__.format_map(self.settings.dict())).strip() + raise NotImplementedError() # Abstract method that needs to be implemented by subclass. # This method is intended to calculate resource recommendation based on history data and kubernetes object data. diff --git a/robusta_krr/core/runner.py b/robusta_krr/core/runner.py index 8e08521c..7ae4a40d 100644 --- a/robusta_krr/core/runner.py +++ b/robusta_krr/core/runner.py @@ -293,7 +293,7 @@ async def _collect_result(self) -> Result: return Result( scans=scans, - description=self._strategy.description, + description=f"[b]{self._strategy.display_name.title()} Strategy[/b]\n\n{self._strategy.description}", strategy=StrategyData( name=str(self._strategy).lower(), settings=self._strategy.settings.dict(), diff --git a/robusta_krr/strategies/simple.py b/robusta_krr/strategies/simple.py index 3cecd18e..3a781f4e 100644 --- a/robusta_krr/strategies/simple.py +++ b/robusta_krr/strategies/simple.py @@ -1,3 +1,4 @@ +import textwrap from datetime import timedelta import numpy as np @@ -70,21 +71,7 @@ def history_range_enough(self, history_range: tuple[timedelta, timedelta]) -> bo class SimpleStrategy(BaseStrategy[SimpleStrategySettings]): - """ - CPU request: {cpu_percentile}% percentile, limit: unset - Memory request: max + {memory_buffer_percentage}%, limit: max + {memory_buffer_percentage}% - History: {history_duration} hours - Step: {timeframe_duration} minutes - - All parameters can be customized. For example: `krr simple --cpu_percentile=90 --memory_buffer_percentage=15 --history_duration=24 --timeframe_duration=0.5` - - This strategy does not work with objects with HPA defined (Horizontal Pod Autoscaler). - If HPA is defined for CPU or Memory, the strategy will return "?" for that resource. - You can override this behaviour by passing the --allow-hpa flag - - Learn more: [underline]https://github.com/robusta-dev/krr#algorithm[/underline] - """ - + display_name = "simple" rich_console = True @@ -102,6 +89,27 @@ def metrics(self) -> list[type[PrometheusMetric]]: return metrics + @property + def description(self): + s = textwrap.dedent(f"""\ + CPU request: {self.settings.cpu_percentile}% percentile, limit: unset + Memory request: max + {self.settings.memory_buffer_percentage}%, limit: max + {self.settings.memory_buffer_percentage}% + History: {self.settings.history_duration} hours + Step: {self.settings.timeframe_duration} minutes + + All parameters can be customized. For example: `krr simple --cpu_percentile=90 --memory_buffer_percentage=15 --history_duration=24 --timeframe_duration=0.5` + """) + + if not self.settings.allow_hpa: + s += "\n" + textwrap.dedent(f"""\ + This strategy does not work with objects with HPA defined (Horizontal Pod Autoscaler). + If HPA is defined for CPU or Memory, the strategy will return "?" for that resource. + You can override this behaviour by passing the --allow-hpa flag + """) + + s += "\nLearn more: [underline]https://github.com/robusta-dev/krr#algorithm[/underline]" + return s + def __calculate_cpu_proposal( self, history_data: MetricsPodData, object_data: K8sObjectData ) -> ResourceRecommendation: From 0bc47368c546fd0d546a831467bc964c89fd5a90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Gon=C3=A7alves?= <30077840+joaopedrocg27@users.noreply.github.com> Date: Mon, 13 May 2024 10:05:29 +0100 Subject: [PATCH 085/137] fix(discovery): Added selector for prometheus-stack --- .../prometheus/metrics_service/prometheus_metrics_service.py | 1 + 1 file changed, 1 insertion(+) diff --git a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py index 8331249f..97dcd185 100644 --- a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py +++ b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py @@ -35,6 +35,7 @@ def find_metrics_url(self, *, api_client: Optional[ApiClient] = None) -> Optiona return super().find_url( selectors=[ "app=kube-prometheus-stack-prometheus", + "app=stack-prometheus", "app=prometheus,component=server", "app=prometheus-server", "app=prometheus-operator-prometheus", From 8de631b6e4a36c64319a24d798713641d78bbb1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Gon=C3=A7alves?= <30077840+joaopedrocg27@users.noreply.github.com> Date: Tue, 14 May 2024 08:56:06 +0100 Subject: [PATCH 086/137] Update prometheus_metrics_service.py --- .../prometheus/metrics_service/prometheus_metrics_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py index 97dcd185..90efdc16 100644 --- a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py +++ b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py @@ -35,13 +35,13 @@ def find_metrics_url(self, *, api_client: Optional[ApiClient] = None) -> Optiona return super().find_url( selectors=[ "app=kube-prometheus-stack-prometheus", - "app=stack-prometheus", "app=prometheus,component=server", "app=prometheus-server", "app=prometheus-operator-prometheus", "app=rancher-monitoring-prometheus", "app=prometheus-prometheus", "app.kubernetes.io/name=prometheus,app.kubernetes.io/component=server", + "app=stack-prometheus", ] ) From 00c738d65d1a1684755a13a3f5a13b4a86c67a34 Mon Sep 17 00:00:00 2001 From: Pavan Gudiwada <25551553+pavangudiwada@users.noreply.github.com> Date: Mon, 20 May 2024 16:14:56 +0530 Subject: [PATCH 087/137] Updated KRR readme --- README.md | 58 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 33650c3c..6ab7d671 100644 --- a/README.md +++ b/README.md @@ -18,17 +18,20 @@
Installation . - Usage - Β· How KRR works . Slack Integration + . + KRR UI on Robusta Cloud
+ Usage + Β· Report Bug Β· Request Feature Β· Support +
Like KRR? Please ⭐ this repository to show your support!

@@ -60,7 +63,7 @@ ## About The Project -Robusta KRR (Kubernetes Resource Recommender) is a CLI tool for optimizing resource allocation in Kubernetes clusters. It gathers pod usage data from Prometheus and recommends requests and limits for CPU and memory. This reduces costs and improves performance. +Robusta KRR (Kubernetes Resource Recommender) is a CLI tool for **optimizing resource allocation** in Kubernetes clusters. It gathers pod usage data from Prometheus and **recommends requests and limits** for CPU and memory. This **reduces costs and improves performance**. ### Data Integrations @@ -97,6 +100,23 @@ By right-sizing your containers with KRR, you can save an average of 69% on clou Read more about [how KRR works](#how-krr-works) and [KRR vs Kubernetes VPA](#difference-with-kubernetes-vpa) +## Difference with Kubernetes VPA + +| Feature πŸ› οΈ | Robusta KRR πŸš€ | Kubernetes VPA 🌐 | +| --------------------------- | ---------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | +| Resource Recommendations πŸ’‘ | βœ… CPU/Memory requests and limits | βœ… CPU/Memory requests and limits | +| Installation Location 🌍 | βœ… Not required to be installed inside the cluster, can be used on your own device, connected to a cluster | ❌ Must be installed inside the cluster | +| Workload Configuration πŸ”§ | βœ… No need to configure a VPA object for each workload | ❌ Requires VPA object configuration for each workload | +| Immediate Results ⚑ | βœ… Gets results immediately (given Prometheus is running) | ❌ Requires time to gather data and provide recommendations | +| Reporting πŸ“Š | βœ… Detailed CLI Report, web UI in [Robusta.dev](https://home.robusta.dev/) | ❌ Not supported | +| Extensibility πŸ”§ | βœ… Add your own strategies with few lines of Python | :warning: Limited extensibility | +| Explainability πŸ“– | βœ… See graphs explaining the recommendations | ❌ Not supported | +| Custom Metrics πŸ“ | πŸ”„ Support in future versions | ❌ Not supported | +| Custom Resources πŸŽ›οΈ | πŸ”„ Support in future versions (e.g., GPU) | ❌ Not supported | +| Autoscaling πŸ”€ | πŸ”„ Support in future versions | βœ… Automatic application of recommendations | +| Default History πŸ•’ | 14 days | 8 days | + + ## Installation @@ -120,6 +140,7 @@ If you have a different setup, make sure the following metrics exist: _Note: If one of last three metrics is absent KRR will still work, but it will only consider currently-running pods when calculating recommendations. Historic pods that no longer exist in the cluster will not be taken into consideration._
+ ### Installation Methods
@@ -212,8 +233,15 @@ Setup KRR for...

(back to top)

+ + +## Free KRR UI on Robusta SaaS + +
+ + ## Usage
@@ -385,21 +413,6 @@ Find about how KRR tries to find the default Prometheus to connect (back to top)

-## Difference with Kubernetes VPA - -| Feature πŸ› οΈ | Robusta KRR πŸš€ | Kubernetes VPA 🌐 | -| --------------------------- | ---------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | -| Resource Recommendations πŸ’‘ | βœ… CPU/Memory requests and limits | βœ… CPU/Memory requests and limits | -| Installation Location 🌍 | βœ… Not required to be installed inside the cluster, can be used on your own device, connected to a cluster | ❌ Must be installed inside the cluster | -| Workload Configuration πŸ”§ | βœ… No need to configure a VPA object for each workload | ❌ Requires VPA object configuration for each workload | -| Immediate Results ⚑ | βœ… Gets results immediately (given Prometheus is running) | ❌ Requires time to gather data and provide recommendations | -| Reporting πŸ“Š | βœ… Detailed CLI Report, web UI in [Robusta.dev](https://home.robusta.dev/) | ❌ Not supported | -| Extensibility πŸ”§ | βœ… Add your own strategies with few lines of Python | :warning: Limited extensibility | -| Explainability πŸ“– | βœ… See graphs explaining the recommendations | ❌ Not supported | -| Custom Metrics πŸ“ | πŸ”„ Support in future versions | ❌ Not supported | -| Custom Resources πŸŽ›οΈ | πŸ”„ Support in future versions (e.g., GPU) | ❌ Not supported | -| Autoscaling πŸ”€ | πŸ”„ Support in future versions | βœ… Automatic application of recommendations | -| Default History πŸ•’ | 14 days | 8 days | @@ -567,13 +580,14 @@ For discovering Prometheus it scans services for those labels:
Free UI for KRR recommendations -With the [free Robusta SaaS platform](https://home.robusta.dev/) you can: +With the [free Robusta SaaS platform](https://platform.robusta.dev/signup/?utm_source=github&utm_medium=krr-readme) you can: -- See why KRR recommends what it does +- Understand individual app recommendations with app usage history - Sort and filter recommendations by namespace, priority, and more -- Copy a YAML snippet to fix the problems KRR finds +- Give dev's a YAML snippet to fix the problems KRR finds +- Analyze impact using KRR scan history -![Robusta UI Screen Shot][ui-screenshot] +
From d4ed7ce52cea124669cfed340321a521ce40a828 Mon Sep 17 00:00:00 2001 From: Pavan Gudiwada <25551553+pavangudiwada@users.noreply.github.com> Date: Mon, 20 May 2024 16:28:37 +0530 Subject: [PATCH 088/137] Updated video links --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 6ab7d671..f558b7dd 100644 --- a/README.md +++ b/README.md @@ -242,6 +242,15 @@ Setup KRR for...
+ + + + + + + + + ## Usage
From d8c3d26b0774c781498c27cdc19e20b183b42d5f Mon Sep 17 00:00:00 2001 From: Pavan Gudiwada <25551553+pavangudiwada@users.noreply.github.com> Date: Mon, 20 May 2024 16:36:26 +0530 Subject: [PATCH 089/137] Updated broken links --- README.md | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index f558b7dd..9faf6e5a 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ _View instructions for: [Seeing recommendations in a UI](#free-ui-for-krr-recomm - **Prometheus Integration**: Get recommendations based on the data you already have - **Explainability**: Understand how recommendations were calculated - **Extensible Strategies**: Easily create and use your own strategies for calculating resource recommendations. -- **Free SaaS Platform**: See why KRR recommends what it does, by using the [free Robusta SaaS platform](https://home.robusta.dev/). +- **Free SaaS Platform**: See why KRR recommends what it does, by using the [free Robusta SaaS platform](https://platform.robusta.dev/signup/?utm_source=github&utm_medium=krr-readme). - **Future Support**: Upcoming versions will support custom resources (e.g. GPUs) and custom metrics. ### Why Use KRR? @@ -234,22 +234,21 @@ Setup KRR for...

(back to top)

- - - ## Free KRR UI on Robusta SaaS -
+We highly recommend using the [free Robusta SaaS platform](https://platform.robusta.dev/signup/?utm_source=github&utm_medium=krr-readme). You can: - - - - +- Understand individual app recommendations with app usage history +- Sort and filter recommendations by namespace, priority, and more +- Give dev's a YAML snippet to fix the problems KRR finds +- Analyze impact using KRR scan history - + + + ## Usage @@ -406,7 +405,7 @@ Robusta KRR uses the following Prometheus queries to gather usage data: [_Need to customize the metrics? Tell us and we'll add support._](https://github.com/robusta-dev/krr/issues/new) -Get a free breakdown of KRR recommendations in the [Robusta SaaS](#optional-free-saas-platform). +Get a free breakdown of KRR recommendations in the [Robusta SaaS](#free-krr-ui-on-robusta-saas). ### Algorithm @@ -596,7 +595,9 @@ With the [free Robusta SaaS platform](https://platform.robusta.dev/signup/?utm_s - Give dev's a YAML snippet to fix the problems KRR finds - Analyze impact using KRR scan history -
+ + +
From 88b25afac06becc7f5b2135b688adf3b4ec6d327 Mon Sep 17 00:00:00 2001 From: Pavan Gudiwada <25551553+pavangudiwada@users.noreply.github.com> Date: Mon, 20 May 2024 16:50:31 +0530 Subject: [PATCH 090/137] Fixed inconsistency --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9faf6e5a..c668638f 100644 --- a/README.md +++ b/README.md @@ -588,7 +588,7 @@ For discovering Prometheus it scans services for those labels:
Free UI for KRR recommendations -With the [free Robusta SaaS platform](https://platform.robusta.dev/signup/?utm_source=github&utm_medium=krr-readme) you can: +We highly recommend using the [free Robusta SaaS platform](https://platform.robusta.dev/signup/?utm_source=github&utm_medium=krr-readme). You can: - Understand individual app recommendations with app usage history - Sort and filter recommendations by namespace, priority, and more From 4f4196392168b2813394dbbc9de29950b8b4b167 Mon Sep 17 00:00:00 2001 From: Pavan Gudiwada <25551553+pavangudiwada@users.noreply.github.com> Date: Mon, 20 May 2024 17:01:03 +0530 Subject: [PATCH 091/137] Added HPA suport details --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c668638f..db26bf2a 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ According to a recent [Sysdig study](https://sysdig.com/blog/millions-wasted-kub By right-sizing your containers with KRR, you can save an average of 69% on cloud costs. -Read more about [how KRR works](#how-krr-works) and [KRR vs Kubernetes VPA](#difference-with-kubernetes-vpa) +Read more about [how KRR works](#how-krr-works) ## Difference with Kubernetes VPA @@ -115,11 +115,12 @@ Read more about [how KRR works](#how-krr-works) and [KRR vs Kubernetes VPA](#dif | Custom Resources πŸŽ›οΈ | πŸ”„ Support in future versions (e.g., GPU) | ❌ Not supported | | Autoscaling πŸ”€ | πŸ”„ Support in future versions | βœ… Automatic application of recommendations | | Default History πŸ•’ | 14 days | 8 days | +| Supports HPA πŸ”₯ | βœ… Enable using `--allow-hpa` flag | ❌ Not supported | -## Installation +## Installation ### Requirements From c24f4754aefda2b86778c838b8834ef6dacfe763 Mon Sep 17 00:00:00 2001 From: Pavan Gudiwada <25551553+pavangudiwada@users.noreply.github.com> Date: Mon, 20 May 2024 20:27:05 +0530 Subject: [PATCH 092/137] Fixed minor update --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index db26bf2a..012b19bf 100644 --- a/README.md +++ b/README.md @@ -241,7 +241,7 @@ We highly recommend using the [free Robusta SaaS platform](https://platform.robu - Understand individual app recommendations with app usage history - Sort and filter recommendations by namespace, priority, and more -- Give dev's a YAML snippet to fix the problems KRR finds +- Give devs a YAML snippet to fix the problems KRR finds - Analyze impact using KRR scan history From a36063c0cb04c4a6e7c58ebc3cd646305b53a020 Mon Sep 17 00:00:00 2001 From: Avi-Robusta <97387909+Avi-Robusta@users.noreply.github.com> Date: Wed, 22 May 2024 11:25:27 +0300 Subject: [PATCH 093/137] reducing cpu precentile for better rec (#282) --- robusta_krr/strategies/simple.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/robusta_krr/strategies/simple.py b/robusta_krr/strategies/simple.py index 3a781f4e..c56e6ecb 100644 --- a/robusta_krr/strategies/simple.py +++ b/robusta_krr/strategies/simple.py @@ -25,7 +25,7 @@ class SimpleStrategySettings(StrategySettings): - cpu_percentile: float = pd.Field(99, gt=0, le=100, description="The percentile to use for the CPU recommendation.") + cpu_percentile: float = pd.Field(95, gt=0, le=100, description="The percentile to use for the CPU recommendation.") memory_buffer_percentage: float = pd.Field( 15, gt=0, description="The percentage of added buffer to the peak memory usage for memory recommendation." ) From a6a0cabfbd7f3f4d0ba26e42255f19752ba351a6 Mon Sep 17 00:00:00 2001 From: Natan Yellin Date: Wed, 22 May 2024 12:10:47 +0300 Subject: [PATCH 094/137] Update main.py (#274) Co-authored-by: Avi-Robusta <97387909+Avi-Robusta@users.noreply.github.com> --- robusta_krr/main.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/robusta_krr/main.py b/robusta_krr/main.py index 5c2d01aa..74563217 100644 --- a/robusta_krr/main.py +++ b/robusta_krr/main.py @@ -186,9 +186,8 @@ def run_strategy( openshift: bool = typer.Option( False, "--openshift", - help="Used when running by Robusta inside an OpenShift cluster.", + help="Connect to Prometheus with a token read from /var/run/secrets/kubernetes.io/serviceaccount/token - recommended when running KRR inside an OpenShift cluster", rich_help_panel="Prometheus Openshift Settings", - hidden=True, ), cpu_min_value: int = typer.Option( 10, From cf366e6ea65aa3acbe9de48e35a1e1bfcd6de34b Mon Sep 17 00:00:00 2001 From: Pavel Zhukov <33721692+LeaveMyYard@users.noreply.github.com> Date: Wed, 22 May 2024 12:58:48 +0300 Subject: [PATCH 095/137] Add config to result (#271) Co-authored-by: Avi-Robusta <97387909+Avi-Robusta@users.noreply.github.com> --- .../metrics_service/prometheus_metrics_service.py | 10 +++++++--- .../core/integrations/prometheus/prometheus_utils.py | 4 ++-- robusta_krr/core/models/config.py | 12 ++++++++---- robusta_krr/core/models/result.py | 2 ++ 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py index 90efdc16..440611eb 100644 --- a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py +++ b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py @@ -66,7 +66,9 @@ def __init__( logger.info(f"Trying to connect to {self.name()} for {self.cluster} cluster") - self.auth_header = settings.prometheus_auth_header + self.auth_header = ( + settings.prometheus_auth_header.get_secret_value() if settings.prometheus_auth_header else None + ) self.ssl_enabled = settings.prometheus_ssl_enabled if settings.openshift: @@ -93,7 +95,7 @@ def __init__( logger.info(f"Using {self.name()} at {self.url} for cluster {cluster or 'default'}") - headers = settings.prometheus_other_headers + headers = {k: v.get_secret_value() for k, v in settings.prometheus_other_headers.items()} headers |= self.additional_headers if self.auth_header: @@ -246,7 +248,9 @@ async def load_pods(self, object: K8sObjectData, period: timedelta) -> list[PodD }}[{period_literal}] """ ) - pod_owners = {repl_controller["metric"]["replicationcontroller"] for repl_controller in replication_controllers} + pod_owners = { + repl_controller["metric"]["replicationcontroller"] for repl_controller in replication_controllers + } pod_owner_kind = "ReplicationController" del replication_controllers diff --git a/robusta_krr/core/integrations/prometheus/prometheus_utils.py b/robusta_krr/core/integrations/prometheus/prometheus_utils.py index e40c2805..e561af24 100644 --- a/robusta_krr/core/integrations/prometheus/prometheus_utils.py +++ b/robusta_krr/core/integrations/prometheus/prometheus_utils.py @@ -39,7 +39,7 @@ def generate_prometheus_config( credentials = credentials.get_frozen_credentials() region = settings.eks_managed_prom_region if settings.eks_managed_prom_region else session.region_name access_key = settings.eks_access_key if settings.eks_access_key else credentials.access_key - secret_key = settings.eks_secret_key if settings.eks_secret_key else credentials.secret_key + secret_key = settings.eks_secret_key.get_secret_value() if settings.eks_secret_key else credentials.secret_key service_name = settings.eks_service_name if settings.eks_secret_key else "aps" if not region: raise Exception("No eks region specified") @@ -53,7 +53,7 @@ def generate_prometheus_config( ) # coralogix config if settings.coralogix_token: - return CoralogixPrometheusConfig(**baseconfig, prometheus_token=settings.coralogix_token) + return CoralogixPrometheusConfig(**baseconfig, prometheus_token=settings.coralogix_token.get_secret_value()) if isinstance(metrics_service, VictoriaMetricsService): return VictoriaMetricsPrometheusConfig(**baseconfig) return PrometheusConfig(**baseconfig) diff --git a/robusta_krr/core/models/config.py b/robusta_krr/core/models/config.py index ff6142a6..18add983 100644 --- a/robusta_krr/core/models/config.py +++ b/robusta_krr/core/models/config.py @@ -35,18 +35,18 @@ class Config(pd.BaseSettings): # Prometheus Settings prometheus_url: Optional[str] = pd.Field(None) - prometheus_auth_header: Optional[str] = pd.Field(None) - prometheus_other_headers: dict[str, str] = pd.Field(default_factory=dict) + prometheus_auth_header: Optional[pd.SecretStr] = pd.Field(None) + prometheus_other_headers: dict[str, pd.SecretStr] = pd.Field(default_factory=dict) prometheus_ssl_enabled: bool = pd.Field(False) prometheus_cluster_label: Optional[str] = pd.Field(None) prometheus_label: Optional[str] = pd.Field(None) eks_managed_prom: bool = pd.Field(False) eks_managed_prom_profile_name: Optional[str] = pd.Field(None) eks_access_key: Optional[str] = pd.Field(None) - eks_secret_key: Optional[str] = pd.Field(None) + eks_secret_key: Optional[pd.SecretStr] = pd.Field(None) eks_service_name: Optional[str] = pd.Field(None) eks_managed_prom_region: Optional[str] = pd.Field(None) - coralogix_token: Optional[str] = pd.Field(None) + coralogix_token: Optional[pd.SecretStr] = pd.Field(None) openshift: bool = pd.Field(False) # Threading settings @@ -170,6 +170,10 @@ def set_config(config: Config) -> None: logging.getLogger("").setLevel(logging.CRITICAL) logger.setLevel(logging.DEBUG if config.verbose else logging.CRITICAL if config.quiet else logging.INFO) + @staticmethod + def get_config() -> Optional[Config]: + return _config + # NOTE: This class is just a proxy for _config. # Import settings from this module and use it like it is just a config object. diff --git a/robusta_krr/core/models/result.py b/robusta_krr/core/models/result.py index 2d5ffbc9..4373b71e 100644 --- a/robusta_krr/core/models/result.py +++ b/robusta_krr/core/models/result.py @@ -8,6 +8,7 @@ from robusta_krr.core.models.allocations import RecommendationValue, ResourceAllocations, ResourceType from robusta_krr.core.models.objects import K8sObjectData from robusta_krr.core.models.severity import Severity +from robusta_krr.core.models.config import Config class Recommendation(pd.BaseModel): @@ -64,6 +65,7 @@ class Result(pd.BaseModel): description: Optional[str] = None strategy: StrategyData errors: list[dict[str, Any]] = pd.Field(default_factory=list) + config: Optional[Config] = pd.Field(default_factory=Config.get_config) def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) From 9178d2d211048b290632b9daca6ea669361015d6 Mon Sep 17 00:00:00 2001 From: avi robusta Date: Wed, 29 May 2024 10:02:06 +0300 Subject: [PATCH 096/137] working docker version poetry update --- .github/workflows/build-on-release.yml | 1 + Dockerfile | 12 +-- base.Dockerfile | 10 +++ poetry.lock | 89 ++++++++++---------- pyproject.toml | 8 +- requirements.txt | 111 ++++++++++++------------- 6 files changed, 124 insertions(+), 107 deletions(-) create mode 100644 base.Dockerfile diff --git a/.github/workflows/build-on-release.yml b/.github/workflows/build-on-release.yml index d8a91237..2e6a347e 100644 --- a/.github/workflows/build-on-release.yml +++ b/.github/workflows/build-on-release.yml @@ -88,6 +88,7 @@ jobs: cp ./intro.txt ./dist/krr/intro.txt - name: Build with PyInstaller + continue-on-error: true if: matrix.os == 'ubuntu-latest' shell: bash run: | diff --git a/Dockerfile b/Dockerfile index 690c80df..ca4121a0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,18 @@ # Use the official Python 3.9 slim image as the base image -FROM python:3.9-slim as builder +FROM cgr.dev/chainguard/python:latest-dev as builder +ENV LANG=C.UTF-8 +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 +ENV PATH="/app/venv/bin:$PATH" # Set the working directory WORKDIR /app -# Install system dependencies required for Poetry -RUN apt-get update && \ - dpkg --add-architecture arm64 - COPY ./requirements.txt requirements.txt +RUN pip install --upgrade pip # Install the project dependencies +RUN python -m ensurepip --upgrade RUN pip install -r requirements.txt # Copy the rest of the application code diff --git a/base.Dockerfile b/base.Dockerfile new file mode 100644 index 00000000..301960de --- /dev/null +++ b/base.Dockerfile @@ -0,0 +1,10 @@ +# Use the official Python 3.9 slim image as the base image +FROM cgr.dev/chainguard/python:latest-dev as builder +ENV LANG=C.UTF-8 +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 +ENV PATH="/app/venv/bin:$PATH" +RUN pip install --upgrade pip +# these take a long time to build +RUN pip install matplotlib==3.8.4 +RUN pip install pandas==2.2.2 \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index b3498272..a8f78d2f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. [[package]] name = "about-time" @@ -958,46 +958,47 @@ files = [ [[package]] name = "pandas" -version = "2.2.1" +version = "2.2.2" description = "Powerful data structures for data analysis, time series, and statistics" optional = false python-versions = ">=3.9" files = [ - {file = "pandas-2.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8df8612be9cd1c7797c93e1c5df861b2ddda0b48b08f2c3eaa0702cf88fb5f88"}, - {file = "pandas-2.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0f573ab277252ed9aaf38240f3b54cfc90fff8e5cab70411ee1d03f5d51f3944"}, - {file = "pandas-2.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f02a3a6c83df4026e55b63c1f06476c9aa3ed6af3d89b4f04ea656ccdaaaa359"}, - {file = "pandas-2.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c38ce92cb22a4bea4e3929429aa1067a454dcc9c335799af93ba9be21b6beb51"}, - {file = "pandas-2.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c2ce852e1cf2509a69e98358e8458775f89599566ac3775e70419b98615f4b06"}, - {file = "pandas-2.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:53680dc9b2519cbf609c62db3ed7c0b499077c7fefda564e330286e619ff0dd9"}, - {file = "pandas-2.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:94e714a1cca63e4f5939cdce5f29ba8d415d85166be3441165edd427dc9f6bc0"}, - {file = "pandas-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f821213d48f4ab353d20ebc24e4faf94ba40d76680642fb7ce2ea31a3ad94f9b"}, - {file = "pandas-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c70e00c2d894cb230e5c15e4b1e1e6b2b478e09cf27cc593a11ef955b9ecc81a"}, - {file = "pandas-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e97fbb5387c69209f134893abc788a6486dbf2f9e511070ca05eed4b930b1b02"}, - {file = "pandas-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101d0eb9c5361aa0146f500773395a03839a5e6ecde4d4b6ced88b7e5a1a6403"}, - {file = "pandas-2.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7d2ed41c319c9fb4fd454fe25372028dfa417aacb9790f68171b2e3f06eae8cd"}, - {file = "pandas-2.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:af5d3c00557d657c8773ef9ee702c61dd13b9d7426794c9dfeb1dc4a0bf0ebc7"}, - {file = "pandas-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:06cf591dbaefb6da9de8472535b185cba556d0ce2e6ed28e21d919704fef1a9e"}, - {file = "pandas-2.2.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:88ecb5c01bb9ca927ebc4098136038519aa5d66b44671861ffab754cae75102c"}, - {file = "pandas-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:04f6ec3baec203c13e3f8b139fb0f9f86cd8c0b94603ae3ae8ce9a422e9f5bee"}, - {file = "pandas-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a935a90a76c44fe170d01e90a3594beef9e9a6220021acfb26053d01426f7dc2"}, - {file = "pandas-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c391f594aae2fd9f679d419e9a4d5ba4bce5bb13f6a989195656e7dc4b95c8f0"}, - {file = "pandas-2.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9d1265545f579edf3f8f0cb6f89f234f5e44ba725a34d86535b1a1d38decbccc"}, - {file = "pandas-2.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:11940e9e3056576ac3244baef2fedade891977bcc1cb7e5cc8f8cc7d603edc89"}, - {file = "pandas-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:4acf681325ee1c7f950d058b05a820441075b0dd9a2adf5c4835b9bc056bf4fb"}, - {file = "pandas-2.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9bd8a40f47080825af4317d0340c656744f2bfdb6819f818e6ba3cd24c0e1397"}, - {file = "pandas-2.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:df0c37ebd19e11d089ceba66eba59a168242fc6b7155cba4ffffa6eccdfb8f16"}, - {file = "pandas-2.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:739cc70eaf17d57608639e74d63387b0d8594ce02f69e7a0b046f117974b3019"}, - {file = "pandas-2.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9d3558d263073ed95e46f4650becff0c5e1ffe0fc3a015de3c79283dfbdb3df"}, - {file = "pandas-2.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4aa1d8707812a658debf03824016bf5ea0d516afdea29b7dc14cf687bc4d4ec6"}, - {file = "pandas-2.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:76f27a809cda87e07f192f001d11adc2b930e93a2b0c4a236fde5429527423be"}, - {file = "pandas-2.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:1ba21b1d5c0e43416218db63037dbe1a01fc101dc6e6024bcad08123e48004ab"}, - {file = "pandas-2.2.1.tar.gz", hash = "sha256:0ab90f87093c13f3e8fa45b48ba9f39181046e8f3317d3aadb2fffbb1b978572"}, + {file = "pandas-2.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:90c6fca2acf139569e74e8781709dccb6fe25940488755716d1d354d6bc58bce"}, + {file = "pandas-2.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7adfc142dac335d8c1e0dcbd37eb8617eac386596eb9e1a1b77791cf2498238"}, + {file = "pandas-2.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4abfe0be0d7221be4f12552995e58723c7422c80a659da13ca382697de830c08"}, + {file = "pandas-2.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8635c16bf3d99040fdf3ca3db669a7250ddf49c55dc4aa8fe0ae0fa8d6dcc1f0"}, + {file = "pandas-2.2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:40ae1dffb3967a52203105a077415a86044a2bea011b5f321c6aa64b379a3f51"}, + {file = "pandas-2.2.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8e5a0b00e1e56a842f922e7fae8ae4077aee4af0acb5ae3622bd4b4c30aedf99"}, + {file = "pandas-2.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:ddf818e4e6c7c6f4f7c8a12709696d193976b591cc7dc50588d3d1a6b5dc8772"}, + {file = "pandas-2.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:696039430f7a562b74fa45f540aca068ea85fa34c244d0deee539cb6d70aa288"}, + {file = "pandas-2.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8e90497254aacacbc4ea6ae5e7a8cd75629d6ad2b30025a4a8b09aa4faf55151"}, + {file = "pandas-2.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58b84b91b0b9f4bafac2a0ac55002280c094dfc6402402332c0913a59654ab2b"}, + {file = "pandas-2.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2123dc9ad6a814bcdea0f099885276b31b24f7edf40f6cdbc0912672e22eee"}, + {file = "pandas-2.2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2925720037f06e89af896c70bca73459d7e6a4be96f9de79e2d440bd499fe0db"}, + {file = "pandas-2.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0cace394b6ea70c01ca1595f839cf193df35d1575986e484ad35c4aeae7266c1"}, + {file = "pandas-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:873d13d177501a28b2756375d59816c365e42ed8417b41665f346289adc68d24"}, + {file = "pandas-2.2.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9dfde2a0ddef507a631dc9dc4af6a9489d5e2e740e226ad426a05cabfbd7c8ef"}, + {file = "pandas-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e9b79011ff7a0f4b1d6da6a61aa1aa604fb312d6647de5bad20013682d1429ce"}, + {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cb51fe389360f3b5a4d57dbd2848a5f033350336ca3b340d1c53a1fad33bcad"}, + {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eee3a87076c0756de40b05c5e9a6069c035ba43e8dd71c379e68cab2c20f16ad"}, + {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3e374f59e440d4ab45ca2fffde54b81ac3834cf5ae2cdfa69c90bc03bde04d76"}, + {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:43498c0bdb43d55cb162cdc8c06fac328ccb5d2eabe3cadeb3529ae6f0517c32"}, + {file = "pandas-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:d187d355ecec3629624fccb01d104da7d7f391db0311145817525281e2804d23"}, + {file = "pandas-2.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0ca6377b8fca51815f382bd0b697a0814c8bda55115678cbc94c30aacbb6eff2"}, + {file = "pandas-2.2.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9057e6aa78a584bc93a13f0a9bf7e753a5e9770a30b4d758b8d5f2a62a9433cd"}, + {file = "pandas-2.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:001910ad31abc7bf06f49dcc903755d2f7f3a9186c0c040b827e522e9cef0863"}, + {file = "pandas-2.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66b479b0bd07204e37583c191535505410daa8df638fd8e75ae1b383851fe921"}, + {file = "pandas-2.2.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a77e9d1c386196879aa5eb712e77461aaee433e54c68cf253053a73b7e49c33a"}, + {file = "pandas-2.2.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92fd6b027924a7e178ac202cfbe25e53368db90d56872d20ffae94b96c7acc57"}, + {file = "pandas-2.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:640cef9aa381b60e296db324337a554aeeb883ead99dc8f6c18e81a93942f5f4"}, + {file = "pandas-2.2.2.tar.gz", hash = "sha256:9e79019aba43cb4fda9e4d983f8e88ca0373adbb697ae9c6c43093218de28b54"}, ] [package.dependencies] numpy = [ - {version = ">=1.22.4,<2", markers = "python_version < \"3.11\""}, - {version = ">=1.23.2,<2", markers = "python_version == \"3.11\""}, + {version = ">=1.22.4", markers = "python_version < \"3.11\""}, + {version = ">=1.23.2", markers = "python_version == \"3.11\""}, + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, ] python-dateutil = ">=2.8.2" pytz = ">=2020.1" @@ -1604,13 +1605,13 @@ files = [ [[package]] name = "requests" -version = "2.31.0" +version = "2.32.0" description = "Python HTTP for Humans." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, - {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, + {file = "requests-2.32.0-py3-none-any.whl", hash = "sha256:f2c3881dddb70d056c5bd7600a4fae312b2a300e39be6a118d30b90bd27262b5"}, + {file = "requests-2.32.0.tar.gz", hash = "sha256:fa5490319474c82ef1d2c9bc459d3652e3ae4ef4c4ebdd18a21145a47ca4b6b8"}, ] [package.dependencies] @@ -1825,13 +1826,13 @@ files = [ [[package]] name = "typing-extensions" -version = "4.10.0" -description = "Backported and Experimental Type Hints for Python 3.8+" +version = "4.6.0" +description = "Backported and Experimental Type Hints for Python 3.7+" optional = false -python-versions = ">=3.8" +python-versions = ">=3.7" files = [ - {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, - {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, + {file = "typing_extensions-4.6.0-py3-none-any.whl", hash = "sha256:6ad00b63f849b7dcc313b70b6b304ed67b2b2963b3098a33efe18056b1a9a223"}, + {file = "typing_extensions-4.6.0.tar.gz", hash = "sha256:ff6b238610c747e44c268aa4bb23c8c735d665a63726df3f9431ce707f2aa768"}, ] [[package]] @@ -1911,5 +1912,5 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" -python-versions = ">=3.9,<3.12" -content-hash = "5c447ec48183cdc4e0bd2617cea276db7ef3b465d0eb6149838477c05baf2fe9" +python-versions = ">=3.9,<=3.12.3" +content-hash = "22de259effaf2739aa45bfbe29b6b07d287e335eed12f183fd76a18b67099e6c" diff --git a/pyproject.toml b/pyproject.toml index 86c84d87..0c73c649 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,15 +23,19 @@ plugins = "numpy.typing.mypy_plugin,pydantic.mypy" krr = "robusta_krr.main:run" [tool.poetry.dependencies] -python = ">=3.9,<3.12" +python = ">=3.9,<=3.12.3" typer = { extras = ["all"], version = "^0.7.0" } pydantic = "^1.10.7" kubernetes = "^26.1.0" prometheus-api-client = "0.5.3" -numpy = "^1.24.2" +numpy = ">=1.26.4,<1.27.0" alive-progress = "^3.1.2" prometrix = "^0.1.17" slack-sdk = "^3.21.3" +pandas = "2.2.2" +requests = "2.32.0" +pyyaml = "6.0.1" +typing-extensions = "4.6.0" diff --git a/requirements.txt b/requirements.txt index ee5fce5d..d9989a3f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,56 +1,55 @@ -about-time==4.2.1 ; python_version >= "3.9" and python_version < "3.12" -alive-progress==3.1.2 ; python_version >= "3.9" and python_version < "3.12" -boto3==1.28.21 ; python_version >= "3.9" and python_version < "3.12" -botocore==1.31.21 ; python_version >= "3.9" and python_version < "3.12" -cachetools==5.3.0 ; python_version >= "3.9" and python_version < "3.12" -certifi==2024.2.2 ; python_version >= "3.9" and python_version < "3.12" -charset-normalizer==3.0.1 ; python_version >= "3.9" and python_version < "3.12" -click==8.1.3 ; python_version >= "3.9" and python_version < "3.12" -colorama==0.4.6 ; python_version >= "3.9" and python_version < "3.12" and platform_system == "Windows" -commonmark==0.9.1 ; python_version >= "3.9" and python_version < "3.12" -contourpy==1.0.7 ; python_version >= "3.9" and python_version < "3.12" -cycler==0.11.0 ; python_version >= "3.9" and python_version < "3.12" -dateparser==1.1.7 ; python_version >= "3.9" and python_version < "3.12" -fonttools==4.43.0 ; python_version >= "3.9" and python_version < "3.12" -google-auth==2.16.2 ; python_version >= "3.9" and python_version < "3.12" -grapheme==0.6.0 ; python_version >= "3.9" and python_version < "3.12" -httmock==1.4.0 ; python_version >= "3.9" and python_version < "3.12" -idna==3.4 ; python_version >= "3.9" and python_version < "3.12" -importlib-resources==5.12.0 ; python_version >= "3.9" and python_version < "3.10" -jmespath==1.0.1 ; python_version >= "3.9" and python_version < "3.12" -kiwisolver==1.4.4 ; python_version >= "3.9" and python_version < "3.12" -kubernetes==26.1.0 ; python_version >= "3.9" and python_version < "3.12" -matplotlib==3.7.1 ; python_version >= "3.9" and python_version < "3.12" -numpy==1.24.2 ; python_version >= "3.9" and python_version < "3.12" -oauthlib==3.2.2 ; python_version >= "3.9" and python_version < "3.12" -packaging==23.0 ; python_version >= "3.9" and python_version < "3.12" -pandas==1.5.3 ; python_version >= "3.9" and python_version < "3.12" -pillow==10.3.0 ; python_version >= "3.9" and python_version < "3.12" -prometheus-api-client==0.5.3 ; python_version >= "3.9" and python_version < "3.12" -prometrix==0.1.17 ; python_version >= "3.9" and python_version < "3.12" -pyasn1-modules==0.2.8 ; python_version >= "3.9" and python_version < "3.12" -pyasn1==0.4.8 ; python_version >= "3.9" and python_version < "3.12" -pydantic==1.10.15 ; python_version >= "3.9" and python_version < "3.12" -pygments==2.17.2 ; python_version >= "3.9" and python_version < "3.12" -pyparsing==3.0.9 ; python_version >= "3.9" and python_version < "3.12" -python-dateutil==2.8.2 ; python_version >= "3.9" and python_version < "3.12" -pytz-deprecation-shim==0.1.0.post0 ; python_version >= "3.9" and python_version < "3.12" -pytz==2022.7.1 ; python_version >= "3.9" and python_version < "3.12" -pyyaml==6.0 ; python_version >= "3.9" and python_version < "3.12" -regex==2022.10.31 ; python_version >= "3.9" and python_version < "3.12" -requests-oauthlib==1.3.1 ; python_version >= "3.9" and python_version < "3.12" -requests==2.31.0 ; python_version >= "3.9" and python_version < "3.12" -rich==12.6.0 ; python_version >= "3.9" and python_version < "3.12" -rsa==4.9 ; python_version >= "3.9" and python_version < "3.12" -s3transfer==0.6.1 ; python_version >= "3.9" and python_version < "3.12" -setuptools==67.4.0 ; python_version >= "3.9" and python_version < "3.12" -shellingham==1.5.0.post1 ; python_version >= "3.9" and python_version < "3.12" -six==1.16.0 ; python_version >= "3.9" and python_version < "3.12" -slack-sdk==3.21.3 ; python_version >= "3.9" and python_version < "3.12" -typer[all]==0.7.0 ; python_version >= "3.9" and python_version < "3.12" -typing-extensions==4.5.0 ; python_version >= "3.9" and python_version < "3.12" -tzdata==2022.7 ; python_version >= "3.9" and python_version < "3.12" -tzlocal==4.2 ; python_version >= "3.9" and python_version < "3.12" -urllib3==1.26.18 ; python_version >= "3.9" and python_version < "3.12" -websocket-client==1.5.1 ; python_version >= "3.9" and python_version < "3.12" -zipp==3.15.0 ; python_version >= "3.9" and python_version < "3.10" +about-time==4.2.1 ; python_version >= "3.9" and python_full_version <= "3.12.3" +alive-progress==3.1.5 ; python_version >= "3.9" and python_full_version <= "3.12.3" +boto3==1.34.62 ; python_version >= "3.9" and python_full_version <= "3.12.3" +botocore==1.34.62 ; python_version >= "3.9" and python_full_version <= "3.12.3" +cachetools==5.3.3 ; python_version >= "3.9" and python_full_version <= "3.12.3" +certifi==2024.2.2 ; python_version >= "3.9" and python_full_version <= "3.12.3" +charset-normalizer==3.3.2 ; python_version >= "3.9" and python_full_version <= "3.12.3" +click==8.1.7 ; python_version >= "3.9" and python_full_version <= "3.12.3" +colorama==0.4.6 ; python_version >= "3.9" and python_full_version <= "3.12.3" +commonmark==0.9.1 ; python_version >= "3.9" and python_full_version <= "3.12.3" +contourpy==1.2.0 ; python_version >= "3.9" and python_full_version <= "3.12.3" +cycler==0.12.1 ; python_version >= "3.9" and python_full_version <= "3.12.3" +dateparser==1.2.0 ; python_version >= "3.9" and python_full_version <= "3.12.3" +fonttools==4.49.0 ; python_version >= "3.9" and python_full_version <= "3.12.3" +google-auth==2.28.2 ; python_version >= "3.9" and python_full_version <= "3.12.3" +grapheme==0.6.0 ; python_version >= "3.9" and python_full_version <= "3.12.3" +httmock==1.4.0 ; python_version >= "3.9" and python_full_version <= "3.12.3" +idna==3.6 ; python_version >= "3.9" and python_full_version <= "3.12.3" +importlib-resources==6.3.0 ; python_version >= "3.9" and python_version < "3.10" +jmespath==1.0.1 ; python_version >= "3.9" and python_full_version <= "3.12.3" +kiwisolver==1.4.5 ; python_version >= "3.9" and python_full_version <= "3.12.3" +kubernetes==26.1.0 ; python_version >= "3.9" and python_full_version <= "3.12.3" +matplotlib==3.8.3 ; python_version >= "3.9" and python_full_version <= "3.12.3" +numpy==1.26.4 ; python_version >= "3.9" and python_full_version <= "3.12.3" +oauthlib==3.2.2 ; python_version >= "3.9" and python_full_version <= "3.12.3" +packaging==24.0 ; python_version >= "3.9" and python_full_version <= "3.12.3" +pandas==2.2.2 ; python_version >= "3.9" and python_full_version <= "3.12.3" +pillow==10.3.0 ; python_version >= "3.9" and python_full_version <= "3.12.3" +prometheus-api-client==0.5.3 ; python_version >= "3.9" and python_full_version <= "3.12.3" +prometrix==0.1.17 ; python_version >= "3.9" and python_full_version <= "3.12.3" +pyasn1-modules==0.3.0 ; python_version >= "3.9" and python_full_version <= "3.12.3" +pyasn1==0.5.1 ; python_version >= "3.9" and python_full_version <= "3.12.3" +pydantic==1.10.15 ; python_version >= "3.9" and python_full_version <= "3.12.3" +pygments==2.17.2 ; python_version >= "3.9" and python_full_version <= "3.12.3" +pyparsing==3.1.2 ; python_version >= "3.9" and python_full_version <= "3.12.3" +python-dateutil==2.9.0.post0 ; python_version >= "3.9" and python_full_version <= "3.12.3" +pytz==2024.1 ; python_version >= "3.9" and python_full_version <= "3.12.3" +pyyaml==6.0.1 ; python_version >= "3.9" and python_full_version <= "3.12.3" +regex==2023.12.25 ; python_version >= "3.9" and python_full_version <= "3.12.3" +requests-oauthlib==1.4.0 ; python_version >= "3.9" and python_full_version <= "3.12.3" +requests==2.32.0 ; python_version >= "3.9" and python_full_version <= "3.12.3" +rich==12.6.0 ; python_version >= "3.9" and python_full_version <= "3.12.3" +rsa==4.9 ; python_version >= "3.9" and python_full_version <= "3.12.3" +s3transfer==0.10.0 ; python_version >= "3.9" and python_full_version <= "3.12.3" +setuptools==69.2.0 ; python_version >= "3.9" and python_full_version <= "3.12.3" +shellingham==1.5.4 ; python_version >= "3.9" and python_full_version <= "3.12.3" +six==1.16.0 ; python_version >= "3.9" and python_full_version <= "3.12.3" +slack-sdk==3.27.1 ; python_version >= "3.9" and python_full_version <= "3.12.3" +typer[all]==0.7.0 ; python_version >= "3.9" and python_full_version <= "3.12.3" +typing-extensions==4.6.0 ; python_version >= "3.9" and python_full_version <= "3.12.3" +tzdata==2024.1 ; python_version >= "3.9" and python_full_version <= "3.12.3" +tzlocal==5.2 ; python_version >= "3.9" and python_full_version <= "3.12.3" +urllib3==1.26.18 ; python_version >= "3.9" and python_full_version <= "3.12.3" +websocket-client==1.7.0 ; python_version >= "3.9" and python_full_version <= "3.12.3" +zipp==3.18.0 ; python_version >= "3.9" and python_version < "3.10" From 7b8f185c344a38ac2609b029df1393e94040594d Mon Sep 17 00:00:00 2001 From: avi robusta Date: Wed, 29 May 2024 10:07:43 +0300 Subject: [PATCH 097/137] remove debug code --- .github/workflows/build-on-release.yml | 1 - base.Dockerfile | 10 ---------- 2 files changed, 11 deletions(-) delete mode 100644 base.Dockerfile diff --git a/.github/workflows/build-on-release.yml b/.github/workflows/build-on-release.yml index 2e6a347e..d8a91237 100644 --- a/.github/workflows/build-on-release.yml +++ b/.github/workflows/build-on-release.yml @@ -88,7 +88,6 @@ jobs: cp ./intro.txt ./dist/krr/intro.txt - name: Build with PyInstaller - continue-on-error: true if: matrix.os == 'ubuntu-latest' shell: bash run: | diff --git a/base.Dockerfile b/base.Dockerfile deleted file mode 100644 index 301960de..00000000 --- a/base.Dockerfile +++ /dev/null @@ -1,10 +0,0 @@ -# Use the official Python 3.9 slim image as the base image -FROM cgr.dev/chainguard/python:latest-dev as builder -ENV LANG=C.UTF-8 -ENV PYTHONDONTWRITEBYTECODE=1 -ENV PYTHONUNBUFFERED=1 -ENV PATH="/app/venv/bin:$PATH" -RUN pip install --upgrade pip -# these take a long time to build -RUN pip install matplotlib==3.8.4 -RUN pip install pandas==2.2.2 \ No newline at end of file From 36c4a1d50eb139142f25251c13810f8885b50ff5 Mon Sep 17 00:00:00 2001 From: "avi@robusta.dev" Date: Wed, 29 May 2024 14:55:49 +0300 Subject: [PATCH 098/137] changing base --- Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index ca4121a0..5dd3f032 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Use the official Python 3.9 slim image as the base image -FROM cgr.dev/chainguard/python:latest-dev as builder +FROM us-central1-docker.pkg.dev/genuine-flight-317411/devel/base/python3.12-dev as builder ENV LANG=C.UTF-8 ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONUNBUFFERED=1 @@ -15,8 +15,10 @@ RUN pip install --upgrade pip RUN python -m ensurepip --upgrade RUN pip install -r requirements.txt +FROM us-central1-docker.pkg.dev/genuine-flight-317411/devel/base/python3.12 # Copy the rest of the application code COPY . . +COPY --from=builder /app/venv /venv # Run the application using 'poetry run krr simple' CMD ["python", "krr.py", "simple"] From f103d0a5d920e96d2278b3416f3a1210d087b4c2 Mon Sep 17 00:00:00 2001 From: "avi@robusta.dev" Date: Sun, 2 Jun 2024 09:21:09 +0300 Subject: [PATCH 099/137] python slim --- Dockerfile | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5dd3f032..39b80ef9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Use the official Python 3.9 slim image as the base image -FROM us-central1-docker.pkg.dev/genuine-flight-317411/devel/base/python3.12-dev as builder +FROM python:3.12-slim as builder ENV LANG=C.UTF-8 ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONUNBUFFERED=1 @@ -15,10 +15,8 @@ RUN pip install --upgrade pip RUN python -m ensurepip --upgrade RUN pip install -r requirements.txt -FROM us-central1-docker.pkg.dev/genuine-flight-317411/devel/base/python3.12 # Copy the rest of the application code COPY . . -COPY --from=builder /app/venv /venv # Run the application using 'poetry run krr simple' CMD ["python", "krr.py", "simple"] From ee65dc9f942c37949d8a8e8b59df34d8706dc166 Mon Sep 17 00:00:00 2001 From: "avi@robusta.dev" Date: Mon, 3 Jun 2024 16:57:59 +0300 Subject: [PATCH 100/137] added poetry dependency --- Dockerfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Dockerfile b/Dockerfile index 39b80ef9..3909fb91 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,10 @@ ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONUNBUFFERED=1 ENV PATH="/app/venv/bin:$PATH" +# Install system dependencies required for Poetry +RUN apt-get update && \ + dpkg --add-architecture arm64 + # Set the working directory WORKDIR /app From 71d6d761237cf8771b3c39509e61d2a2c34d2f61 Mon Sep 17 00:00:00 2001 From: Avi-Robusta <97387909+Avi-Robusta@users.noreply.github.com> Date: Sun, 9 Jun 2024 14:58:27 +0300 Subject: [PATCH 101/137] Docker fixes (#286) * cve fix * build fix changes * fix not building windows * fixed version requirements --- .github/workflows/build-on-release.yml | 4 +- .github/workflows/update-code-version.yml | 3 + poetry.lock | 8 +- pyproject.toml | 1 + requirements.txt | 106 +++++++++++----------- 5 files changed, 63 insertions(+), 59 deletions(-) diff --git a/.github/workflows/build-on-release.yml b/.github/workflows/build-on-release.yml index d8a91237..7289a973 100644 --- a/.github/workflows/build-on-release.yml +++ b/.github/workflows/build-on-release.yml @@ -82,13 +82,13 @@ jobs: if: matrix.os == 'macos-latest' shell: bash run: | - pyinstaller --target-architecture universal2 krr.py + pyinstaller --target-architecture arm64 krr.py mkdir -p ./dist/krr/grapheme/data cp $(python -c "import grapheme; print(grapheme.__path__[0] + '/data/grapheme_break_property.json')") ./dist/krr/grapheme/data/grapheme_break_property.json cp ./intro.txt ./dist/krr/intro.txt - name: Build with PyInstaller - if: matrix.os == 'ubuntu-latest' + if: matrix.os != 'macos-latest' shell: bash run: | pyinstaller krr.py diff --git a/.github/workflows/update-code-version.yml b/.github/workflows/update-code-version.yml index bc0087da..91ba7819 100644 --- a/.github/workflows/update-code-version.yml +++ b/.github/workflows/update-code-version.yml @@ -18,6 +18,9 @@ jobs: - name: Extract version from tag run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV + - name: Fetch all branches + run: git fetch --all + - name: Select main branch run: git checkout origin/main diff --git a/poetry.lock b/poetry.lock index a8f78d2f..262f4add 100644 --- a/poetry.lock +++ b/poetry.lock @@ -531,13 +531,13 @@ requests = ">=1.0.0" [[package]] name = "idna" -version = "3.6" +version = "3.7" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" files = [ - {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, - {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, ] [[package]] @@ -1913,4 +1913,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = ">=3.9,<=3.12.3" -content-hash = "22de259effaf2739aa45bfbe29b6b07d287e335eed12f183fd76a18b67099e6c" +content-hash = "0635821f6953d2c4085dc5ad44413fc8d39bb0e861f8b88d6f02f5535fcf8f89" diff --git a/pyproject.toml b/pyproject.toml index 0c73c649..518b39c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ pandas = "2.2.2" requests = "2.32.0" pyyaml = "6.0.1" typing-extensions = "4.6.0" +idna = "3.7" diff --git a/requirements.txt b/requirements.txt index d9989a3f..7a6e4eb5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,55 +1,55 @@ -about-time==4.2.1 ; python_version >= "3.9" and python_full_version <= "3.12.3" -alive-progress==3.1.5 ; python_version >= "3.9" and python_full_version <= "3.12.3" -boto3==1.34.62 ; python_version >= "3.9" and python_full_version <= "3.12.3" -botocore==1.34.62 ; python_version >= "3.9" and python_full_version <= "3.12.3" -cachetools==5.3.3 ; python_version >= "3.9" and python_full_version <= "3.12.3" -certifi==2024.2.2 ; python_version >= "3.9" and python_full_version <= "3.12.3" -charset-normalizer==3.3.2 ; python_version >= "3.9" and python_full_version <= "3.12.3" -click==8.1.7 ; python_version >= "3.9" and python_full_version <= "3.12.3" -colorama==0.4.6 ; python_version >= "3.9" and python_full_version <= "3.12.3" -commonmark==0.9.1 ; python_version >= "3.9" and python_full_version <= "3.12.3" -contourpy==1.2.0 ; python_version >= "3.9" and python_full_version <= "3.12.3" -cycler==0.12.1 ; python_version >= "3.9" and python_full_version <= "3.12.3" -dateparser==1.2.0 ; python_version >= "3.9" and python_full_version <= "3.12.3" -fonttools==4.49.0 ; python_version >= "3.9" and python_full_version <= "3.12.3" -google-auth==2.28.2 ; python_version >= "3.9" and python_full_version <= "3.12.3" -grapheme==0.6.0 ; python_version >= "3.9" and python_full_version <= "3.12.3" -httmock==1.4.0 ; python_version >= "3.9" and python_full_version <= "3.12.3" -idna==3.6 ; python_version >= "3.9" and python_full_version <= "3.12.3" +about-time==4.2.1 ; python_version >= "3.9" and python_full_version < "3.13" +alive-progress==3.1.5 ; python_version >= "3.9" and python_full_version < "3.13" +boto3==1.34.62 ; python_version >= "3.9" and python_full_version < "3.13" +botocore==1.34.62 ; python_version >= "3.9" and python_full_version < "3.13" +cachetools==5.3.3 ; python_version >= "3.9" and python_full_version < "3.13" +certifi==2024.2.2 ; python_version >= "3.9" and python_full_version < "3.13" +charset-normalizer==3.3.2 ; python_version >= "3.9" and python_full_version < "3.13" +click==8.1.7 ; python_version >= "3.9" and python_full_version < "3.13" +colorama==0.4.6 ; python_version >= "3.9" and python_full_version < "3.13" +commonmark==0.9.1 ; python_version >= "3.9" and python_full_version < "3.13" +contourpy==1.2.0 ; python_version >= "3.9" and python_full_version < "3.13" +cycler==0.12.1 ; python_version >= "3.9" and python_full_version < "3.13" +dateparser==1.2.0 ; python_version >= "3.9" and python_full_version < "3.13" +fonttools==4.49.0 ; python_version >= "3.9" and python_full_version < "3.13" +google-auth==2.28.2 ; python_version >= "3.9" and python_full_version < "3.13" +grapheme==0.6.0 ; python_version >= "3.9" and python_full_version < "3.13" +httmock==1.4.0 ; python_version >= "3.9" and python_full_version < "3.13" +idna==3.7 ; python_version >= "3.9" and python_full_version < "3.13" importlib-resources==6.3.0 ; python_version >= "3.9" and python_version < "3.10" -jmespath==1.0.1 ; python_version >= "3.9" and python_full_version <= "3.12.3" -kiwisolver==1.4.5 ; python_version >= "3.9" and python_full_version <= "3.12.3" -kubernetes==26.1.0 ; python_version >= "3.9" and python_full_version <= "3.12.3" -matplotlib==3.8.3 ; python_version >= "3.9" and python_full_version <= "3.12.3" -numpy==1.26.4 ; python_version >= "3.9" and python_full_version <= "3.12.3" -oauthlib==3.2.2 ; python_version >= "3.9" and python_full_version <= "3.12.3" -packaging==24.0 ; python_version >= "3.9" and python_full_version <= "3.12.3" -pandas==2.2.2 ; python_version >= "3.9" and python_full_version <= "3.12.3" -pillow==10.3.0 ; python_version >= "3.9" and python_full_version <= "3.12.3" -prometheus-api-client==0.5.3 ; python_version >= "3.9" and python_full_version <= "3.12.3" -prometrix==0.1.17 ; python_version >= "3.9" and python_full_version <= "3.12.3" -pyasn1-modules==0.3.0 ; python_version >= "3.9" and python_full_version <= "3.12.3" -pyasn1==0.5.1 ; python_version >= "3.9" and python_full_version <= "3.12.3" -pydantic==1.10.15 ; python_version >= "3.9" and python_full_version <= "3.12.3" -pygments==2.17.2 ; python_version >= "3.9" and python_full_version <= "3.12.3" -pyparsing==3.1.2 ; python_version >= "3.9" and python_full_version <= "3.12.3" -python-dateutil==2.9.0.post0 ; python_version >= "3.9" and python_full_version <= "3.12.3" -pytz==2024.1 ; python_version >= "3.9" and python_full_version <= "3.12.3" -pyyaml==6.0.1 ; python_version >= "3.9" and python_full_version <= "3.12.3" -regex==2023.12.25 ; python_version >= "3.9" and python_full_version <= "3.12.3" -requests-oauthlib==1.4.0 ; python_version >= "3.9" and python_full_version <= "3.12.3" -requests==2.32.0 ; python_version >= "3.9" and python_full_version <= "3.12.3" -rich==12.6.0 ; python_version >= "3.9" and python_full_version <= "3.12.3" -rsa==4.9 ; python_version >= "3.9" and python_full_version <= "3.12.3" -s3transfer==0.10.0 ; python_version >= "3.9" and python_full_version <= "3.12.3" -setuptools==69.2.0 ; python_version >= "3.9" and python_full_version <= "3.12.3" -shellingham==1.5.4 ; python_version >= "3.9" and python_full_version <= "3.12.3" -six==1.16.0 ; python_version >= "3.9" and python_full_version <= "3.12.3" -slack-sdk==3.27.1 ; python_version >= "3.9" and python_full_version <= "3.12.3" -typer[all]==0.7.0 ; python_version >= "3.9" and python_full_version <= "3.12.3" -typing-extensions==4.6.0 ; python_version >= "3.9" and python_full_version <= "3.12.3" -tzdata==2024.1 ; python_version >= "3.9" and python_full_version <= "3.12.3" -tzlocal==5.2 ; python_version >= "3.9" and python_full_version <= "3.12.3" -urllib3==1.26.18 ; python_version >= "3.9" and python_full_version <= "3.12.3" -websocket-client==1.7.0 ; python_version >= "3.9" and python_full_version <= "3.12.3" +jmespath==1.0.1 ; python_version >= "3.9" and python_full_version < "3.13" +kiwisolver==1.4.5 ; python_version >= "3.9" and python_full_version < "3.13" +kubernetes==26.1.0 ; python_version >= "3.9" and python_full_version < "3.13" +matplotlib==3.8.3 ; python_version >= "3.9" and python_full_version < "3.13" +numpy==1.26.4 ; python_version >= "3.9" and python_full_version < "3.13" +oauthlib==3.2.2 ; python_version >= "3.9" and python_full_version < "3.13" +packaging==24.0 ; python_version >= "3.9" and python_full_version < "3.13" +pandas==2.2.2 ; python_version >= "3.9" and python_full_version < "3.13" +pillow==10.3.0 ; python_version >= "3.9" and python_full_version < "3.13" +prometheus-api-client==0.5.3 ; python_version >= "3.9" and python_full_version < "3.13" +prometrix==0.1.17 ; python_version >= "3.9" and python_full_version < "3.13" +pyasn1-modules==0.3.0 ; python_version >= "3.9" and python_full_version < "3.13" +pyasn1==0.5.1 ; python_version >= "3.9" and python_full_version < "3.13" +pydantic==1.10.15 ; python_version >= "3.9" and python_full_version < "3.13" +pygments==2.17.2 ; python_version >= "3.9" and python_full_version < "3.13" +pyparsing==3.1.2 ; python_version >= "3.9" and python_full_version < "3.13" +python-dateutil==2.9.0.post0 ; python_version >= "3.9" and python_full_version < "3.13" +pytz==2024.1 ; python_version >= "3.9" and python_full_version < "3.13" +pyyaml==6.0.1 ; python_version >= "3.9" and python_full_version < "3.13" +regex==2023.12.25 ; python_version >= "3.9" and python_full_version < "3.13" +requests-oauthlib==1.4.0 ; python_version >= "3.9" and python_full_version < "3.13" +requests==2.32.0 ; python_version >= "3.9" and python_full_version < "3.13" +rich==12.6.0 ; python_version >= "3.9" and python_full_version < "3.13" +rsa==4.9 ; python_version >= "3.9" and python_full_version < "3.13" +s3transfer==0.10.0 ; python_version >= "3.9" and python_full_version < "3.13" +setuptools==69.2.0 ; python_version >= "3.9" and python_full_version < "3.13" +shellingham==1.5.4 ; python_version >= "3.9" and python_full_version < "3.13" +six==1.16.0 ; python_version >= "3.9" and python_full_version < "3.13" +slack-sdk==3.27.1 ; python_version >= "3.9" and python_full_version < "3.13" +typer[all]==0.7.0 ; python_version >= "3.9" and python_full_version < "3.13" +typing-extensions==4.6.0 ; python_version >= "3.9" and python_full_version < "3.13" +tzdata==2024.1 ; python_version >= "3.9" and python_full_version < "3.13" +tzlocal==5.2 ; python_version >= "3.9" and python_full_version < "3.13" +urllib3==1.26.18 ; python_version >= "3.9" and python_full_version < "3.13" +websocket-client==1.7.0 ; python_version >= "3.9" and python_full_version < "3.13" zipp==3.18.0 ; python_version >= "3.9" and python_version < "3.10" From fd10b7c3aeafe5b9674ab3cf56fdd94ae3bec189 Mon Sep 17 00:00:00 2001 From: Avi-Robusta <97387909+Avi-Robusta@users.noreply.github.com> Date: Tue, 11 Jun 2024 12:46:59 +0300 Subject: [PATCH 102/137] Change build repo dockerhub (#275) * changing krr action build repo * test image-name * changes for manual test * reverting changes after test success * add build to old repo * added in gloud credentials --- .github/workflows/docker-build-on-tag.yml | 46 +++++++++++++++-------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/.github/workflows/docker-build-on-tag.yml b/.github/workflows/docker-build-on-tag.yml index 97829f5d..617f661c 100644 --- a/.github/workflows/docker-build-on-tag.yml +++ b/.github/workflows/docker-build-on-tag.yml @@ -13,21 +13,11 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up gcloud CLI - uses: google-github-actions/setup-gcloud@v0.2.0 + - name: Login to Docker Hub + uses: docker/login-action@v1 with: - service_account_key: ${{ secrets.GCP_SA_KEY }} - project_id: genuine-flight-317411 - export_default_credentials: true - - # Configure Docker to use the gcloud command-line tool as a credential helper for authentication - - name: Configure Docker - run: |- - gcloud auth configure-docker us-central1-docker.pkg.dev - - - name: Verify gcloud configuration - run: |- - gcloud config get-value project + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v1 @@ -38,6 +28,32 @@ jobs: context: . platforms: linux/arm64,linux/amd64 push: true - tags: us-central1-docker.pkg.dev/genuine-flight-317411/devel/krr:${{ github.ref_name }} + tags: robustadev/krr:${{ github.ref_name }} build-args: | BUILDKIT_INLINE_CACHE=1 + + - name: Set up gcloud CLI + uses: google-github-actions/setup-gcloud@v0.2.0 + with: + service_account_key: ${{ secrets.GCP_SA_KEY }} + project_id: genuine-flight-317411 + export_default_credentials: true + + # Configure Docker to use the gcloud command-line tool as a credential helper for authentication + - name: Configure Docker + run: |- + gcloud auth configure-docker us-central1-docker.pkg.dev + + - name: Verify gcloud configuration + run: |- + gcloud config get-value project + + - name: Release Docker to old repo + run: |- + docker buildx build \ + --build-arg BUILDKIT_INLINE_CACHE=1 \ + --platform linux/arm64,linux/amd64 \ + --cache-from robustadev/krr:${{ github.ref_name }} \ + --tag us-central1-docker.pkg.dev/genuine-flight-317411/devel/krr:${{ github.ref_name }} \ + --push \ + . From c27094157fd1faa34b3f4eaf8df068ee137a31c7 Mon Sep 17 00:00:00 2001 From: Natan Yellin Date: Fri, 14 Jun 2024 13:06:45 +0300 Subject: [PATCH 103/137] Update README.md --- README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 012b19bf..6815c0d1 100644 --- a/README.md +++ b/README.md @@ -12,17 +12,16 @@ ![Product Name Screen Shot][product-screenshot]
-

Robusta KRR

-

- Prometheus-based Kubernetes Resource Recommendations -
+

Kubernetes Resource Recommendations Based on Historical Data

+

Get recommendations based on your existing data in Prometheus/Coralogix/Thanos/Mimir and more!

+

Installation . How KRR works . Slack Integration . - KRR UI on Robusta Cloud + Free KRR UI
Usage Β· From 9465cd9f9d6e08c77730e0816f3a3d0458df59a1 Mon Sep 17 00:00:00 2001 From: Natan Yellin Date: Sat, 15 Jun 2024 14:43:53 +0300 Subject: [PATCH 104/137] Fix version command on dev builds (#291) * Remove broken workflow for bumping version * Make version correct on dev builds --- .github/workflows/update-code-version.yml | 40 ----------------------- robusta_krr/__init__.py | 2 +- robusta_krr/utils/version.py | 32 +++++++++++++++--- 3 files changed, 29 insertions(+), 45 deletions(-) delete mode 100644 .github/workflows/update-code-version.yml diff --git a/.github/workflows/update-code-version.yml b/.github/workflows/update-code-version.yml deleted file mode 100644 index 91ba7819..00000000 --- a/.github/workflows/update-code-version.yml +++ /dev/null @@ -1,40 +0,0 @@ -# @format - -name: Update Version - -on: - push: - tags: - - "v*" - -jobs: - update-version: - runs-on: ubuntu-latest - - steps: - - name: Check out code - uses: actions/checkout@v2 - - - name: Extract version from tag - run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV - - - name: Fetch all branches - run: git fetch --all - - - name: Select main branch - run: git checkout origin/main - - - name: Update version in pyproject.toml - run: | - sed -i "s/version = \".*\"/version = \"$VERSION\"/" pyproject.toml - - - name: Update version in robusta_krr/__init__.py - run: | - sed -i "s/__version__ = \".*\"/__version__ = \"$VERSION\"/" robusta_krr/__init__.py - - - name: Commit and push - run: | - git config --local user.email "action@github.com" - git config --local user.name "GitHub Action" - git add pyproject.toml robusta_krr/__init__.py - git commit -m "Update version to $VERSION" && git push diff --git a/robusta_krr/__init__.py b/robusta_krr/__init__.py index 4866168b..c428657b 100644 --- a/robusta_krr/__init__.py +++ b/robusta_krr/__init__.py @@ -1,4 +1,4 @@ from .main import run -__version__ = "1.8.2-dev" +__version__ = "dev" __all__ = ["run", "__version__"] diff --git a/robusta_krr/utils/version.py b/robusta_krr/utils/version.py index d7b5df7f..47f81c55 100644 --- a/robusta_krr/utils/version.py +++ b/robusta_krr/utils/version.py @@ -1,12 +1,36 @@ -import robusta_krr -import requests import asyncio -from typing import Optional +import os +import subprocess +import sys from concurrent.futures import ThreadPoolExecutor +from typing import Optional + +import requests + +import robusta_krr def get_version() -> str: - return robusta_krr.__version__ + # the version string was patched by a release - return __version__ which will be correct + if robusta_krr.__version__ != "dev": + return robusta_krr.__version__ + + # we are running from an unreleased dev version + try: + # Get the latest git tag + tag = subprocess.check_output(["git", "describe", "--tags"]).decode().strip() + + # Get the current branch name + branch = subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"]).decode().strip() + + # Check if there are uncommitted changes + status = subprocess.check_output(["git", "status", "--porcelain"]).decode().strip() + dirty = "-dirty" if status else "" + + return f"{tag}-{branch}{dirty}" + + except Exception: + return robusta_krr.__version__ # Synchronous function to fetch the latest release version from GitHub API From ab85be83a1801599eea24ef3421abd448c341d1a Mon Sep 17 00:00:00 2001 From: Natan Yellin Date: Sat, 15 Jun 2024 14:45:05 +0300 Subject: [PATCH 105/137] Linkify README.md (#288) --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 6815c0d1..80895909 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ _View instructions for: [Seeing recommendations in a UI](#free-ui-for-krr-recomm - **No Agent Required**: Run a CLI tool on your local machine for immediate results. (Or run in-cluster for weekly [Slack reports](#slack-integration).) - **Prometheus Integration**: Get recommendations based on the data you already have -- **Explainability**: Understand how recommendations were calculated +- **Explainability**: [Understand how recommendations were calculated with explanation graphs](#free-krr-ui-on-robusta-saas) - **Extensible Strategies**: Easily create and use your own strategies for calculating resource recommendations. - **Free SaaS Platform**: See why KRR recommends what it does, by using the [free Robusta SaaS platform](https://platform.robusta.dev/signup/?utm_source=github&utm_medium=krr-readme). - **Future Support**: Upcoming versions will support custom resources (e.g. GPUs) and custom metrics. @@ -106,15 +106,15 @@ Read more about [how KRR works](#how-krr-works) | Resource Recommendations πŸ’‘ | βœ… CPU/Memory requests and limits | βœ… CPU/Memory requests and limits | | Installation Location 🌍 | βœ… Not required to be installed inside the cluster, can be used on your own device, connected to a cluster | ❌ Must be installed inside the cluster | | Workload Configuration πŸ”§ | βœ… No need to configure a VPA object for each workload | ❌ Requires VPA object configuration for each workload | -| Immediate Results ⚑ | βœ… Gets results immediately (given Prometheus is running) | ❌ Requires time to gather data and provide recommendations | +| Immediate Results ⚑ | βœ… Gets results immediately (given Prometheus is running) | ❌ Requires time to gather data and provide recommendations | | Reporting πŸ“Š | βœ… Detailed CLI Report, web UI in [Robusta.dev](https://home.robusta.dev/) | ❌ Not supported | | Extensibility πŸ”§ | βœ… Add your own strategies with few lines of Python | :warning: Limited extensibility | -| Explainability πŸ“– | βœ… See graphs explaining the recommendations | ❌ Not supported | +| Explainability πŸ“– | βœ… [See graphs explaining the recommendations](#free-krr-ui-on-robusta-saas) | ❌ Not supported | | Custom Metrics πŸ“ | πŸ”„ Support in future versions | ❌ Not supported | | Custom Resources πŸŽ›οΈ | πŸ”„ Support in future versions (e.g., GPU) | ❌ Not supported | | Autoscaling πŸ”€ | πŸ”„ Support in future versions | βœ… Automatic application of recommendations | -| Default History πŸ•’ | 14 days | 8 days | -| Supports HPA πŸ”₯ | βœ… Enable using `--allow-hpa` flag | ❌ Not supported | +| Default History πŸ•’ | 14 days | 8 days | +| Supports HPA πŸ”₯ | βœ… Enable using `--allow-hpa` flag | ❌ Not supported | From 4d15999881149a4269c95c4f42f7f30faa503574 Mon Sep 17 00:00:00 2001 From: Natan Yellin Date: Sat, 15 Jun 2024 18:09:03 +0300 Subject: [PATCH 106/137] Breaking changes: respect `--fileoutput` in CSV and rename `-f csv-export` to `-f csv` (#290) # Motivation We've had a number of bug reports over email about the csv export which doesn't respect `--fileoutput`. This PR fixes that by changing the csv export so it respects `--fileoutput`. For any users who prefer the old behavior, I've added a `--fileoutput-dynamic` flag that they can use to keep the previous behavior. # Other Changes I've also renamed the csv export so it is consistent with other exporter names. I've removed redundant code and made a tiny improvement to the table output in response to a common source of confusion w/ users. # Testing I tested this PR manually by checking all the new flags and by checking behavior without any flags to verify that everything works as expected. --- README.md | 26 +++--- robusta_krr/core/models/allocations.py | 25 +++++- robusta_krr/core/models/config.py | 3 +- robusta_krr/core/runner.py | 19 +++-- robusta_krr/formatters/csv.py | 111 +++++++++---------------- robusta_krr/formatters/table.py | 39 +++------ robusta_krr/main.py | 6 +- 7 files changed, 104 insertions(+), 125 deletions(-) diff --git a/README.md b/README.md index 80895909..fa55cb58 100644 --- a/README.md +++ b/README.md @@ -106,15 +106,15 @@ Read more about [how KRR works](#how-krr-works) | Resource Recommendations πŸ’‘ | βœ… CPU/Memory requests and limits | βœ… CPU/Memory requests and limits | | Installation Location 🌍 | βœ… Not required to be installed inside the cluster, can be used on your own device, connected to a cluster | ❌ Must be installed inside the cluster | | Workload Configuration πŸ”§ | βœ… No need to configure a VPA object for each workload | ❌ Requires VPA object configuration for each workload | -| Immediate Results ⚑ | βœ… Gets results immediately (given Prometheus is running) | ❌ Requires time to gather data and provide recommendations | -| Reporting πŸ“Š | βœ… Detailed CLI Report, web UI in [Robusta.dev](https://home.robusta.dev/) | ❌ Not supported | +| Immediate Results ⚑ | βœ… Gets results immediately (given Prometheus is running) | ❌ Requires time to gather data and provide recommendations | +| Reporting πŸ“Š | βœ… Json, CSV, Markdown, [Web UI](#free-ui-for-krr-recommendations), and more! | ❌ Not supported | | Extensibility πŸ”§ | βœ… Add your own strategies with few lines of Python | :warning: Limited extensibility | | Explainability πŸ“– | βœ… [See graphs explaining the recommendations](#free-krr-ui-on-robusta-saas) | ❌ Not supported | | Custom Metrics πŸ“ | πŸ”„ Support in future versions | ❌ Not supported | | Custom Resources πŸŽ›οΈ | πŸ”„ Support in future versions (e.g., GPU) | ❌ Not supported | | Autoscaling πŸ”€ | πŸ”„ Support in future versions | βœ… Automatic application of recommendations | -| Default History πŸ•’ | 14 days | 8 days | -| Supports HPA πŸ”₯ | βœ… Enable using `--allow-hpa` flag | ❌ Not supported | +| Default History πŸ•’ | 14 days | 8 days | +| Supports HPA πŸ”₯ | βœ… Enable using `--allow-hpa` flag | ❌ Not supported | @@ -328,7 +328,7 @@ krr simple -c my-cluster-1 -c my-cluster-2

- Customize output (JSON, YAML, and more + Output formats for reporting (JSON, YAML, CSV, and more) Currently KRR ships with a few formatters to represent the scan data: @@ -336,24 +336,18 @@ Currently KRR ships with a few formatters to represent the scan data: - `json` - `yaml` - `pprint` - data representation from python's pprint library -- `csv_export` - export data to a csv file in the current directory +- `csv` - export data to a csv file in the current directory -To run a strategy with a selected formatter, add a `-f` flag: +To run a strategy with a selected formatter, add a `-f` flag. Usually this should be combined with `--fileoutput ` to write clean output to file without logs: ```sh -krr simple -f json +krr simple -f json --fileoutput krr-report.json ``` -For JSON output, add --logtostderr so no logs go to the result file: +If you prefer, you can also use `--logtostderr` to get clean formatted output in one file and error logs in another: ```sh -krr simple --logtostderr -f json > result.json -``` - -For YAML output, do the same: - -```sh -krr simple --logtostderr -f yaml > result.yaml +krr simple --logtostderr -f json > result.json 2> logs-and-errors.log ```
diff --git a/robusta_krr/core/models/allocations.py b/robusta_krr/core/models/allocations.py index cde5aa08..0a8d0c5c 100644 --- a/robusta_krr/core/models/allocations.py +++ b/robusta_krr/core/models/allocations.py @@ -25,7 +25,30 @@ class ResourceType(str, enum.Enum): Self = TypeVar("Self", bound="ResourceAllocations") - +NONE_LITERAL = "unset" +NAN_LITERAL = "?" + +def format_recommendation_value(value: RecommendationValue) -> str: + if value is None: + return NONE_LITERAL + elif isinstance(value, str): + return NAN_LITERAL + else: + return resource_units.format(value) + +def format_diff(allocated, recommended, selector, multiplier=1, colored=False) -> str: + if recommended is None or isinstance(recommended.value, str) or selector != "requests": + return "" + else: + reccomended_val = recommended.value if isinstance(recommended.value, (int, float)) else 0 + allocated_val = allocated if isinstance(allocated, (int, float)) else 0 + diff_val = reccomended_val - allocated_val + if colored: + diff_sign = "[green]+[/green]" if diff_val >= 0 else "[red]-[/red]" + else: + diff_sign = "+" if diff_val >= 0 else "-" + return f"{diff_sign}{format_recommendation_value(abs(diff_val) * multiplier)}" + class ResourceAllocations(pd.BaseModel): requests: dict[ResourceType, RecommendationValue] limits: dict[ResourceType, RecommendationValue] diff --git a/robusta_krr/core/models/config.py b/robusta_krr/core/models/config.py index 18add983..6ad5804c 100644 --- a/robusta_krr/core/models/config.py +++ b/robusta_krr/core/models/config.py @@ -59,8 +59,9 @@ class Config(pd.BaseSettings): log_to_stderr: bool width: Optional[int] = pd.Field(None, ge=1) - # Outputs Settings + # Output Settings file_output: Optional[str] = pd.Field(None) + file_output_dynamic = bool = pd.Field(False) slack_output: Optional[str] = pd.Field(None) other_args: dict[str, Any] diff --git a/robusta_krr/core/runner.py b/robusta_krr/core/runner.py index 7ae4a40d..e61200c3 100644 --- a/robusta_krr/core/runner.py +++ b/robusta_krr/core/runner.py @@ -6,7 +6,7 @@ import warnings from concurrent.futures import ThreadPoolExecutor from typing import Optional, Union -from datetime import timedelta +from datetime import timedelta, datetime from prometrix import PrometheusNotFound from rich.console import Console from slack_sdk import WebClient @@ -108,14 +108,23 @@ def _process_result(self, result: Result) -> None: custom_print(formatted, rich=rich, force=True) - if settings.file_output or settings.slack_output: - if settings.file_output: + if settings.file_output_dynamic or settings.file_output or settings.slack_output: + if settings.file_output_dynamic: + current_datetime = datetime.now().strftime("%Y%m%d%H%M%S") + file_name = f"krr-{current_datetime}.{settings.format}" + logger.info(f"Writing output to file: {file_name}") + elif settings.file_output: file_name = settings.file_output elif settings.slack_output: file_name = settings.slack_output + with open(file_name, "w") as target_file: - console = Console(file=target_file, width=settings.width) - console.print(formatted) + # don't use rich when writing a csv to avoid line wrapping etc + if settings.format == "csv": + target_file.write(formatted) + else: + console = Console(file=target_file, width=settings.width) + console.print(formatted) if settings.slack_output: client = WebClient(os.environ["SLACK_BOT_TOKEN"]) warnings.filterwarnings("ignore", category=UserWarning) diff --git a/robusta_krr/formatters/csv.py b/robusta_krr/formatters/csv.py index 792c061f..812c35bd 100644 --- a/robusta_krr/formatters/csv.py +++ b/robusta_krr/formatters/csv.py @@ -1,41 +1,14 @@ import itertools import csv - import logging - +import io from robusta_krr.core.abstract import formatters -from robusta_krr.core.models.allocations import RecommendationValue +from robusta_krr.core.models.allocations import RecommendationValue, format_recommendation_value, format_diff, NONE_LITERAL, NAN_LITERAL from robusta_krr.core.models.result import ResourceScan, ResourceType, Result -from robusta_krr.utils import resource_units -import datetime logger = logging.getLogger("krr") -NONE_LITERAL = "unset" -NAN_LITERAL = "?" - - -def _format(value: RecommendationValue) -> str: - if value is None: - return NONE_LITERAL - elif isinstance(value, str): - return NAN_LITERAL - else: - return resource_units.format(value) - - -def __calc_diff(allocated, recommended, selector, multiplier=1) -> str: - if recommended is None or isinstance(recommended.value, str) or selector != "requests": - return "" - else: - reccomended_val = recommended.value if isinstance(recommended.value, (int, float)) else 0 - allocated_val = allocated if isinstance(allocated, (int, float)) else 0 - diff_val = reccomended_val - allocated_val - diff_sign = "+" if diff_val >= 0 else "-" - return f"{diff_sign}{_format(abs(diff_val) * multiplier)}" - - def _format_request_str(item: ResourceScan, resource: ResourceType, selector: str) -> str: allocated = getattr(item.object.allocations, selector)[resource] recommended = getattr(item.recommended, selector)[resource] @@ -43,32 +16,26 @@ def _format_request_str(item: ResourceScan, resource: ResourceType, selector: st if allocated is None and recommended.value is None: return f"{NONE_LITERAL}" - diff = __calc_diff(allocated, recommended, selector) + diff = format_diff(allocated, recommended, selector) if diff != "": diff = f"({diff}) " return ( diff - + _format(allocated) + + format_recommendation_value(allocated) + " -> " - + _format(recommended.value) + + format_recommendation_value(recommended.value) ) - def _format_total_diff(item: ResourceScan, resource: ResourceType, pods_current: int) -> str: selector = "requests" allocated = getattr(item.object.allocations, selector)[resource] recommended = getattr(item.recommended, selector)[resource] - return __calc_diff(allocated, recommended, selector, pods_current) - - -@formatters.register() -def csv_export(result: Result) -> str: - - current_datetime = datetime.datetime.now().strftime("%Y%m%d%H%M%S") - file_path = f"krr-{current_datetime}.csv" + return format_diff(allocated, recommended, selector, pods_current) +@formatters.register("csv") +def csv_exporter(result: Result) -> str: # We need to order the resource columns so that they are in the format of Namespace,Name,Pods,Old Pods,Type,Container,CPU Diff,CPU Requests,CPU Limits,Memory Diff,Memory Requests,Memory Limits resource_columns = [] for resource in ResourceType: @@ -76,36 +43,34 @@ def csv_export(result: Result) -> str: resource_columns.append(f"{resource.name} Requests") resource_columns.append(f"{resource.name} Limits") - with open(file_path, 'w+', newline='') as csvfile: - csv_writer = csv.writer(csvfile) - csv_writer.writerow([ - "Namespace", "Name", "Pods", "Old Pods", "Type", "Container", - *resource_columns - - ]) - - for _, group in itertools.groupby( - enumerate(result.scans), key=lambda x: (x[1].object.cluster, x[1].object.namespace, x[1].object.name) - ): - group_items = list(group) - - for j, (i, item) in enumerate(group_items): - full_info_row = j == 0 - - row = [ - item.object.namespace if full_info_row else "", - item.object.name if full_info_row else "", - f"{item.object.current_pods_count}" if full_info_row else "", - f"{item.object.deleted_pods_count}" if full_info_row else "", - item.object.kind if full_info_row else "", - item.object.container, - ] - - for resource in ResourceType: - row.append(_format_total_diff(item, resource, item.object.current_pods_count)) - row += [_format_request_str(item, resource, selector) for selector in ["requests", "limits"]] - - csv_writer.writerow(row) + output = io.StringIO() + csv_writer = csv.writer(output) + csv_writer.writerow([ + "Namespace", "Name", "Pods", "Old Pods", "Type", "Container", + *resource_columns + ]) + + for _, group in itertools.groupby( + enumerate(result.scans), key=lambda x: (x[1].object.cluster, x[1].object.namespace, x[1].object.name) + ): + group_items = list(group) + + for j, (i, item) in enumerate(group_items): + full_info_row = j == 0 + + row = [ + item.object.namespace if full_info_row else "", + item.object.name if full_info_row else "", + f"{item.object.current_pods_count}" if full_info_row else "", + f"{item.object.deleted_pods_count}" if full_info_row else "", + item.object.kind if full_info_row else "", + item.object.container, + ] + + for resource in ResourceType: + row.append(_format_total_diff(item, resource, item.object.current_pods_count)) + row += [_format_request_str(item, resource, selector) for selector in ["requests", "limits"]] + + csv_writer.writerow(row) - logger.info("CSV File: %s", file_path) - return "" \ No newline at end of file + return output.getvalue() diff --git a/robusta_krr/formatters/table.py b/robusta_krr/formatters/table.py index b028a058..7d027541 100644 --- a/robusta_krr/formatters/table.py +++ b/robusta_krr/formatters/table.py @@ -4,34 +4,11 @@ from rich.table import Table from robusta_krr.core.abstract import formatters -from robusta_krr.core.models.allocations import RecommendationValue +from robusta_krr.core.models.allocations import RecommendationValue, format_recommendation_value, format_diff, NONE_LITERAL, NAN_LITERAL from robusta_krr.core.models.result import ResourceScan, ResourceType, Result from robusta_krr.core.models.config import settings from robusta_krr.utils import resource_units -NONE_LITERAL = "unset" -NAN_LITERAL = "?" - - -def _format(value: RecommendationValue) -> str: - if value is None: - return NONE_LITERAL - elif isinstance(value, str): - return NAN_LITERAL - else: - return resource_units.format(value) - - -def __calc_diff(allocated, recommended, selector, multiplier=1) -> str: - if recommended is None or isinstance(recommended.value, str) or selector != "requests": - return "" - else: - reccomended_val = recommended.value if isinstance(recommended.value, (int, float)) else 0 - allocated_val = allocated if isinstance(allocated, (int, float)) else 0 - diff_val = reccomended_val - allocated_val - diff_sign = "[green]+[/green]" if diff_val >= 0 else "[red]-[/red]" - return f"{diff_sign}{_format(abs(diff_val) * multiplier)}" - DEFAULT_INFO_COLOR = "grey27" INFO_COLORS: dict[str, str] = { @@ -48,7 +25,7 @@ def _format_request_str(item: ResourceScan, resource: ResourceType, selector: st if allocated is None and recommended.value is None: return f"[{severity.color}]{NONE_LITERAL}[/{severity.color}]" - diff = __calc_diff(allocated, recommended, selector) + diff = format_diff(allocated, recommended, selector, colored=True) if diff != "": diff = f"({diff}) " @@ -61,9 +38,9 @@ def _format_request_str(item: ResourceScan, resource: ResourceType, selector: st return ( diff + f"[{severity.color}]" - + _format(allocated) + + format_recommendation_value(allocated) + " -> " - + _format(recommended.value) + + format_recommendation_value(recommended.value) + f"[/{severity.color}]" + info_formatted ) @@ -74,7 +51,13 @@ def _format_total_diff(item: ResourceScan, resource: ResourceType, pods_current: allocated = getattr(item.object.allocations, selector)[resource] recommended = getattr(item.recommended, selector)[resource] - return __calc_diff(allocated, recommended, selector, pods_current) + # if we have more than one pod, say so (this explains to the user why the total is different than the recommendation) + if pods_current == 1: + pods_info = "" + else: + pods_info = f"\n({pods_current} pods)" + + return f"{format_diff(allocated, recommended, selector, pods_current, colored=True)}{pods_info}" @formatters.register(rich_console=True) diff --git a/robusta_krr/main.py b/robusta_krr/main.py index 74563217..c208647c 100644 --- a/robusta_krr/main.py +++ b/robusta_krr/main.py @@ -234,7 +234,10 @@ def run_strategy( rich_help_panel="Logging Settings", ), file_output: Optional[str] = typer.Option( - None, "--fileoutput", help="Print the output to a file", rich_help_panel="Output Settings" + None, "--fileoutput", help="Filename to write output to (if not specified, file output is disabled)", rich_help_panel="Output Settings" + ), + file_output_dynamic: bool = typer.Option( + False, "--fileoutput-dynamic", help="Ignore --fileoutput and write files to the current directory in the format krr-{datetime}.{format} (e.g. krr-20240518223924.csv)", rich_help_panel="Output Settings" ), slack_output: Optional[str] = typer.Option( None, @@ -279,6 +282,7 @@ def run_strategy( log_to_stderr=log_to_stderr, width=width, file_output=file_output, + file_output_dynamic=file_output_dynamic, slack_output=slack_output, strategy=_strategy_name, other_args=strategy_args, From 02f6efef62dec90dcdc77b422a8a76832ee7ef52 Mon Sep 17 00:00:00 2001 From: Natan Yellin Date: Sat, 15 Jun 2024 19:50:15 +0300 Subject: [PATCH 107/137] Update README.md (#292) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fa55cb58..8db9de31 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ _View instructions for: [Seeing recommendations in a UI](#free-ui-for-krr-recomm - **Free SaaS Platform**: See why KRR recommends what it does, by using the [free Robusta SaaS platform](https://platform.robusta.dev/signup/?utm_source=github&utm_medium=krr-readme). - **Future Support**: Upcoming versions will support custom resources (e.g. GPUs) and custom metrics. -### Why Use KRR? +### How Much Can I Expect to Save with KRR? According to a recent [Sysdig study](https://sysdig.com/blog/millions-wasted-kubernetes/), on average, Kubernetes clusters have: From c9554c00a73357bacec35793ab3623b62903d186 Mon Sep 17 00:00:00 2001 From: Natan Yellin Date: Thu, 20 Jun 2024 10:24:48 +0300 Subject: [PATCH 108/137] Better --help text in krr (#296) # Motivation Users often run `krr --help` to learn about KRR cli flags, but that command doesn't output most flags! Most of the flags are only shown for `krr simple --help`. This PR tells users where to look for the real cli flags. # Alternatives Ideally `krr --help` would show all cli flags, but I don't know how to do this with the library we use (typer) and I suspect it isn't possible. So while I would prefer doing that, this was the only solution I found. --- robusta_krr/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/robusta_krr/main.py b/robusta_krr/main.py index c208647c..dd9ee03b 100644 --- a/robusta_krr/main.py +++ b/robusta_krr/main.py @@ -19,7 +19,7 @@ from robusta_krr.core.runner import Runner from robusta_krr.utils.version import get_version -app = typer.Typer(pretty_exceptions_show_locals=False, pretty_exceptions_short=True, no_args_is_help=True) +app = typer.Typer(pretty_exceptions_show_locals=False, pretty_exceptions_short=True, no_args_is_help=True, help="IMPORTANT: Run `krr simple --help` to see all cli flags!") # NOTE: Disable insecure request warnings, as it might be expected to use self-signed certificates inside the cluster urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) From 99ce16cb12fdaa5ea548e1cea5989c5fa924e721 Mon Sep 17 00:00:00 2001 From: Natan Yellin Date: Thu, 20 Jun 2024 10:25:33 +0300 Subject: [PATCH 109/137] Correct percentile numbers in README (#298) Fixes #297 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8db9de31..43baa8e6 100644 --- a/README.md +++ b/README.md @@ -405,7 +405,7 @@ Get a free breakdown of KRR recommendations in the [Robusta SaaS](#free-krr-ui-o By default, we use a _simple_ strategy to calculate resource recommendations. It is calculated as follows (_The exact numbers can be customized in CLI arguments_): -- For CPU, we set a request at the 99th percentile with no limit. Meaning, in 99% of the cases, your CPU request will be sufficient. For the remaining 1%, we set no limit. This means your pod can burst and use any CPU available on the node - e.g. CPU that other pods requested but aren’t using right now. +- For CPU, we set a request at the 95th percentile with no limit. Meaning, in 95% of the cases, your CPU request will be sufficient. For the remaining 5%, we set no limit. This means your pod can burst and use any CPU available on the node - e.g. CPU that other pods requested but aren’t using right now. - For memory, we take the maximum value over the past week and add a 15% buffer. From c400fc85f0ad1f772fabdf1f43d4d03cc3f25658 Mon Sep 17 00:00:00 2001 From: Avi-Robusta <97387909+Avi-Robusta@users.noreply.github.com> Date: Thu, 20 Jun 2024 13:25:44 +0300 Subject: [PATCH 110/137] MAIN-1753 - patched pod conditions (#295) This can be tested with https://raw.githubusercontent.com/kubernetes/website/main/content/en/examples/controllers/job-pod-failure-policy-failjob.yaml --- robusta_krr/core/runner.py | 3 ++- robusta_krr/utils/patch.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 robusta_krr/utils/patch.py diff --git a/robusta_krr/core/runner.py b/robusta_krr/core/runner.py index e61200c3..b28d0718 100644 --- a/robusta_krr/core/runner.py +++ b/robusta_krr/core/runner.py @@ -20,6 +20,7 @@ from robusta_krr.utils.intro import load_intro_message from robusta_krr.utils.progress_bar import ProgressBar from robusta_krr.utils.version import get_version, load_latest_version +from robusta_krr.utils.patch import create_monkey_patches logger = logging.getLogger("krr") @@ -312,7 +313,6 @@ async def _collect_result(self) -> Result: async def run(self) -> int: """Run the Runner. The return value is the exit code of the program.""" await self._greet() - try: settings.load_kubeconfig() except Exception as e: @@ -321,6 +321,7 @@ async def run(self) -> int: return 1 # Exit with error try: + create_monkey_patches() # eks has a lower step limit than other types of prometheus, it will throw an error step_count = self._strategy.settings.history_duration * 60 / self._strategy.settings.timeframe_duration if settings.eks_managed_prom and step_count > 11000: diff --git a/robusta_krr/utils/patch.py b/robusta_krr/utils/patch.py new file mode 100644 index 00000000..e12114f8 --- /dev/null +++ b/robusta_krr/utils/patch.py @@ -0,0 +1,15 @@ +import logging + +from kubernetes.client.models.v1_pod_failure_policy_rule import V1PodFailurePolicyRule + +def create_monkey_patches(): + """ + The python kubernetes client will throw exceptions for specific fields that were not allowed to be None on older versions of kubernetes. + """ + logger = logging.getLogger("krr") + logger.debug("Creating kubernetes python cli monkey patches") + + def patched_setter_pod_failure_policy(self, on_pod_conditions): + self._on_pod_conditions = on_pod_conditions + + V1PodFailurePolicyRule.on_pod_conditions = V1PodFailurePolicyRule.on_pod_conditions.setter(patched_setter_pod_failure_policy) From dc3cd2d186daacf19d3465672201479288372e4b Mon Sep 17 00:00:00 2001 From: Natan Yellin Date: Sun, 21 Jul 2024 10:54:58 +0300 Subject: [PATCH 111/137] Fix #314 (#315) --- robusta_krr/core/integrations/kubernetes/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/robusta_krr/core/integrations/kubernetes/__init__.py b/robusta_krr/core/integrations/kubernetes/__init__.py index a772a5c2..f2131366 100644 --- a/robusta_krr/core/integrations/kubernetes/__init__.py +++ b/robusta_krr/core/integrations/kubernetes/__init__.py @@ -231,10 +231,10 @@ async def _list_scannable_objects( ) -> list[K8sObjectData]: if not self._should_list_resource(kind): logger.debug(f"Skipping {kind}s in {self.cluster}") - return + return [] if not self.__kind_available[kind]: - return + return [] result = [] try: From 089285b533635015f092ecc3603416d67c1e8bcc Mon Sep 17 00:00:00 2001 From: Avi-Robusta <97387909+Avi-Robusta@users.noreply.github.com> Date: Mon, 22 Jul 2024 09:20:23 +0300 Subject: [PATCH 112/137] patching cves - [Main-1889, Main-1890, Main-1891] (#316) --- poetry.lock | 33 ++++++++++++++++----------------- pyproject.toml | 3 +++ requirements.txt | 6 +++--- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/poetry.lock b/poetry.lock index 262f4add..b5227813 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "about-time" @@ -1693,19 +1693,18 @@ crt = ["botocore[crt] (>=1.33.2,<2.0a.0)"] [[package]] name = "setuptools" -version = "69.2.0" +version = "70.3.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-69.2.0-py3-none-any.whl", hash = "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"}, - {file = "setuptools-69.2.0.tar.gz", hash = "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e"}, + {file = "setuptools-70.3.0-py3-none-any.whl", hash = "sha256:fe384da74336c398e0d956d1cae0669bc02eed936cdb1d49b57de1990dc11ffc"}, + {file = "setuptools-70.3.0.tar.gz", hash = "sha256:f171bab1dfbc86b132997f26a119f6056a57950d058587841a0082e8830f9dc5"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "shellingham" @@ -1865,13 +1864,13 @@ devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3) [[package]] name = "urllib3" -version = "1.26.18" +version = "1.26.19" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ - {file = "urllib3-1.26.18-py2.py3-none-any.whl", hash = "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07"}, - {file = "urllib3-1.26.18.tar.gz", hash = "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0"}, + {file = "urllib3-1.26.19-py2.py3-none-any.whl", hash = "sha256:37a0344459b199fce0e80b0d3569837ec6b6937435c5244e7fd73fa6006830f3"}, + {file = "urllib3-1.26.19.tar.gz", hash = "sha256:3e3d753a8618b86d7de333b4223005f68720bcd6a7d2bcb9fbd2229ec7c1e429"}, ] [package.extras] @@ -1897,20 +1896,20 @@ test = ["websockets"] [[package]] name = "zipp" -version = "3.18.0" +version = "3.19.2" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" files = [ - {file = "zipp-3.18.0-py3-none-any.whl", hash = "sha256:c1bb803ed69d2cce2373152797064f7e79bc43f0a3748eb494096a867e0ebf79"}, - {file = "zipp-3.18.0.tar.gz", hash = "sha256:df8d042b02765029a09b157efd8e820451045890acc30f8e37dd2f94a060221f"}, + {file = "zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"}, + {file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<=3.12.3" -content-hash = "0635821f6953d2c4085dc5ad44413fc8d39bb0e861f8b88d6f02f5535fcf8f89" +content-hash = "a05011cd5f48d872dbb0e81c6f6f87a483f4bb2cb76194d549d9608acb429793" diff --git a/pyproject.toml b/pyproject.toml index 518b39c6..477a4762 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,9 @@ requests = "2.32.0" pyyaml = "6.0.1" typing-extensions = "4.6.0" idna = "3.7" +urllib3 = "1.26.19" +setuptools = "^70.0.0" +zipp = "^3.19.1" diff --git a/requirements.txt b/requirements.txt index 7a6e4eb5..165f962d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -42,7 +42,7 @@ requests==2.32.0 ; python_version >= "3.9" and python_full_version < "3.13" rich==12.6.0 ; python_version >= "3.9" and python_full_version < "3.13" rsa==4.9 ; python_version >= "3.9" and python_full_version < "3.13" s3transfer==0.10.0 ; python_version >= "3.9" and python_full_version < "3.13" -setuptools==69.2.0 ; python_version >= "3.9" and python_full_version < "3.13" +setuptools==70.3.0 ; python_version >= "3.9" and python_full_version < "3.13" shellingham==1.5.4 ; python_version >= "3.9" and python_full_version < "3.13" six==1.16.0 ; python_version >= "3.9" and python_full_version < "3.13" slack-sdk==3.27.1 ; python_version >= "3.9" and python_full_version < "3.13" @@ -50,6 +50,6 @@ typer[all]==0.7.0 ; python_version >= "3.9" and python_full_version < "3.13" typing-extensions==4.6.0 ; python_version >= "3.9" and python_full_version < "3.13" tzdata==2024.1 ; python_version >= "3.9" and python_full_version < "3.13" tzlocal==5.2 ; python_version >= "3.9" and python_full_version < "3.13" -urllib3==1.26.18 ; python_version >= "3.9" and python_full_version < "3.13" +urllib3==1.26.19 ; python_version >= "3.9" and python_full_version < "3.13" websocket-client==1.7.0 ; python_version >= "3.9" and python_full_version < "3.13" -zipp==3.18.0 ; python_version >= "3.9" and python_version < "3.10" +zipp==3.19.2 ; python_version >= "3.9" and python_version < "3.13" From 2e8278ce9ffcaf38229f47de8aeb8d5b70e236a6 Mon Sep 17 00:00:00 2001 From: Natan Yellin Date: Tue, 23 Jul 2024 10:19:29 +0300 Subject: [PATCH 113/137] Add x86 macos builds (#302) --- .github/workflows/build-on-release.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-on-release.yml b/.github/workflows/build-on-release.yml index 7289a973..b3493929 100644 --- a/.github/workflows/build-on-release.yml +++ b/.github/workflows/build-on-release.yml @@ -8,7 +8,8 @@ jobs: build: strategy: matrix: - os: [ubuntu-latest, windows-latest, macos-latest] + # we build on macos-13 for x86 builds + os: [ubuntu-latest, windows-latest, macos-latest, macos-13] runs-on: ${{ matrix.os }} @@ -32,7 +33,7 @@ jobs: sudo apt-get install -y binutils - name: Install the Apple certificate and provisioning profile - if: matrix.os == 'macos-latest' + if: matrix.os == 'macos-latest' || matrix.os == 'macos-13' env: BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} P12_PASSWORD: ${{ secrets.P12_PASSWORD }} @@ -62,7 +63,7 @@ jobs: cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles - name: Set version in code (Unix) - if: matrix.os == 'macos-latest' || matrix.os == 'ubuntu-latest' + if: matrix.os == 'macos-latest' || matrix.os == 'ubuntu-latest' || matrix.os == 'macos-13' run: | awk 'NR==3{$0="__version__ = \"'${{ github.ref_name }}'\""}1' ./robusta_krr/__init__.py > temp && mv temp ./robusta_krr/__init__.py cat ./robusta_krr/__init__.py @@ -79,7 +80,7 @@ jobs: GITHUB_REF_NAME: ${{ github.ref_name }} - name: Build with PyInstaller - if: matrix.os == 'macos-latest' + if: matrix.os == 'macos-latest' shell: bash run: | pyinstaller --target-architecture arm64 krr.py @@ -97,7 +98,7 @@ jobs: cp ./intro.txt ./dist/krr/intro.txt - name: Zip the application (Unix) - if: matrix.os == 'macos-latest' || matrix.os == 'ubuntu-latest' + if: matrix.os == 'macos-latest' || matrix.os == 'ubuntu-latest' || matrix.os == 'macos-13' run: | cd dist zip -r krr-${{ matrix.os }}-${{ github.ref_name }}.zip krr @@ -129,7 +130,7 @@ jobs: path: ./krr-${{ matrix.os }}-${{ github.ref_name }}.zip - name: Clean up keychain and provisioning profile - if: (matrix.os == 'macos-latest') && always() + if: (matrix.os == 'macos-latest' || matrix.os == 'macos-13') && always() run: | security delete-keychain $RUNNER_TEMP/app-signing.keychain-db rm ~/Library/MobileDevice/Provisioning\ Profiles/build_pp.mobileprovision From 50cb7751dfb89f1866c943c0b14bdddb528912fb Mon Sep 17 00:00:00 2001 From: Avi-Robusta <97387909+Avi-Robusta@users.noreply.github.com> Date: Wed, 24 Jul 2024 10:50:25 +0300 Subject: [PATCH 114/137] add cluster summary of cores and memory (#309) --- .../core/integrations/prometheus/loader.py | 9 +++- .../metrics_service/base_metric_service.py | 6 ++- .../prometheus_metrics_service.py | 41 ++++++++++++++++++- robusta_krr/core/models/result.py | 1 + robusta_krr/core/runner.py | 7 ++++ 5 files changed, 61 insertions(+), 3 deletions(-) diff --git a/robusta_krr/core/integrations/prometheus/loader.py b/robusta_krr/core/integrations/prometheus/loader.py index db493927..cf0c1554 100644 --- a/robusta_krr/core/integrations/prometheus/loader.py +++ b/robusta_krr/core/integrations/prometheus/loader.py @@ -3,7 +3,7 @@ import datetime import logging from concurrent.futures import ThreadPoolExecutor -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Optional, Dict, Any from kubernetes import config as k8s_config from kubernetes.client.api_client import ApiClient @@ -89,6 +89,13 @@ async def load_pods(self, object: K8sObjectData, period: datetime.timedelta) -> logger.exception(f"Failed to load pods for {object}: {e}") return [] + async def get_cluster_summary(self) -> Dict[str, Any]: + try: + return await self.loader.get_cluster_summary() + except Exception as e: + logger.exception(f"Failed to get cluster summary: {e}") + return {} + async def gather_data( self, object: K8sObjectData, diff --git a/robusta_krr/core/integrations/prometheus/metrics_service/base_metric_service.py b/robusta_krr/core/integrations/prometheus/metrics_service/base_metric_service.py index a3b0ee0f..714a9f5c 100644 --- a/robusta_krr/core/integrations/prometheus/metrics_service/base_metric_service.py +++ b/robusta_krr/core/integrations/prometheus/metrics_service/base_metric_service.py @@ -1,7 +1,7 @@ import abc import datetime from concurrent.futures import ThreadPoolExecutor -from typing import List, Optional +from typing import List, Optional, Dict, Any from kubernetes.client.api_client import ApiClient @@ -36,6 +36,10 @@ def name(cls) -> str: def get_cluster_names(self) -> Optional[List[str]]: ... + @abc.abstractmethod + async def get_cluster_summary(self) -> Dict[str, Any]: + ... + @abc.abstractmethod async def gather_data( self, diff --git a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py index 440611eb..5870a105 100644 --- a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py +++ b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py @@ -2,7 +2,7 @@ import logging from concurrent.futures import ThreadPoolExecutor from datetime import datetime, timedelta -from typing import Iterable, List, Optional +from typing import Iterable, List, Optional, Dict, Any from kubernetes.client import ApiClient from prometheus_api_client import PrometheusApiClientException @@ -206,6 +206,45 @@ async def gather_data( return data + async def get_cluster_summary(self) -> Dict[str, Any]: + cluster_label = self.get_prometheus_cluster_label() + memory_query = f""" + sum(max by (instance) (machine_memory_bytes{{ {cluster_label} }})) + """ + cpu_query = f""" + sum(max by (instance) (machine_cpu_cores{{ {cluster_label} }})) + """ + + try: + cluster_memory_result = await self.query(memory_query) + cluster_cpu_result = await self.query(cpu_query) + + # Verify that there is exactly one value in each result + if len(cluster_memory_result) != 1 or len(cluster_cpu_result) != 1: + logger.warning("Error: Expected exactly one result from Prometheus query.") + return {} + + cluster_memory_value = cluster_memory_result[0].get("value") + cluster_cpu_value = cluster_cpu_result[0].get("value") + + # Verify that the "value" list has exactly two elements (timestamp and value) + if not cluster_memory_value or not cluster_cpu_value: + logger.warning("Error: Missing value in Prometheus result.") + return {} + + if len(cluster_memory_value) != 2 or len(cluster_cpu_value) != 2: + logger.warning("Error: Prometheus result values are not of expected size.") + return {} + + return { + "cluster_memory": float(cluster_memory_value[1]), + "cluster_cpu": float(cluster_cpu_value[1]) + } + + except Exception as e: + logger.error(f"Exception occurred while getting cluster summary: {e}") + return {} + async def load_pods(self, object: K8sObjectData, period: timedelta) -> list[PodData]: """ List pods related to the object and add them to the object's pods list. diff --git a/robusta_krr/core/models/result.py b/robusta_krr/core/models/result.py index 4373b71e..514590f9 100644 --- a/robusta_krr/core/models/result.py +++ b/robusta_krr/core/models/result.py @@ -65,6 +65,7 @@ class Result(pd.BaseModel): description: Optional[str] = None strategy: StrategyData errors: list[dict[str, Any]] = pd.Field(default_factory=list) + clusterSummary: dict[str, Any] = {} config: Optional[Config] = pd.Field(default_factory=Config.get_config) def __init__(self, *args, **kwargs) -> None: diff --git a/robusta_krr/core/runner.py b/robusta_krr/core/runner.py index b28d0718..d418b8eb 100644 --- a/robusta_krr/core/runner.py +++ b/robusta_krr/core/runner.py @@ -286,6 +286,12 @@ async def _collect_result(self) -> Result: with ProgressBar(title="Calculating Recommendation") as self.__progressbar: workloads = await self._k8s_loader.list_scannable_objects(clusters) + if not clusters or len(clusters) == 1: + cluster_name = clusters[0] if clusters else None # its none if krr is running inside cluster + prometheus_loader = self._get_prometheus_loader(cluster_name) + cluster_summary = await prometheus_loader.get_cluster_summary() + else: + cluster_summary = {} scans = await asyncio.gather(*[self._gather_object_allocations(k8s_object) for k8s_object in workloads]) successful_scans = [scan for scan in scans if scan is not None] @@ -308,6 +314,7 @@ async def _collect_result(self) -> Result: name=str(self._strategy).lower(), settings=self._strategy.settings.dict(), ), + clusterSummary=cluster_summary ) async def run(self) -> int: From 3991e7ddb16a742d1878c7489a11279175f7f0ca Mon Sep 17 00:00:00 2001 From: tlipoca9 <160737620+tlipoca9@users.noreply.github.com> Date: Wed, 24 Jul 2024 16:41:52 +0800 Subject: [PATCH 115/137] fix typo (#310) --- .../prometheus/metrics_service/prometheus_metrics_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py index 5870a105..1901c7b0 100644 --- a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py +++ b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py @@ -255,7 +255,7 @@ async def load_pods(self, object: K8sObjectData, period: timedelta) -> list[PodD logger.debug(f"Adding historic pods for {object}") - days_literal = min(int(period.total_seconds()) // 60 // 24, 32) + days_literal = min(int(period.total_seconds()) // 3600 // 24, 32) period_literal = f"{days_literal}d" pod_owners: Iterable[str] pod_owner_kind: str From 2fd16a33869dc5109269c4cceb7efe7a9057152d Mon Sep 17 00:00:00 2001 From: Natan Yellin Date: Wed, 24 Jul 2024 11:42:48 +0300 Subject: [PATCH 116/137] Allow bumping OOMs by 0% extra (#306) --- robusta_krr/strategies/simple.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/robusta_krr/strategies/simple.py b/robusta_krr/strategies/simple.py index c56e6ecb..fa9cd777 100644 --- a/robusta_krr/strategies/simple.py +++ b/robusta_krr/strategies/simple.py @@ -41,7 +41,7 @@ class SimpleStrategySettings(StrategySettings): description="Whether to bump the memory when OOMKills are detected (experimental).", ) oom_memory_buffer_percentage: float = pd.Field( - 25, gt=0, description="What percentage to increase the memory when there are OOMKill events." + 25, ge=0, description="What percentage to increase the memory when there are OOMKill events." ) def calculate_memory_proposal(self, data: PodsTimeData, max_oomkill: float = 0) -> float: From 77c8c6136829879652193d50a371791acf0adc81 Mon Sep 17 00:00:00 2001 From: tlipoca9 <160737620+tlipoca9@users.noreply.github.com> Date: Wed, 24 Jul 2024 16:48:42 +0800 Subject: [PATCH 117/137] fix: avoid exceed search.maxQueryLen (#312) --- .../prometheus_metrics_service.py | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py index 1901c7b0..44bbadb1 100644 --- a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py +++ b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py @@ -313,20 +313,22 @@ async def load_pods(self, object: K8sObjectData, period: timedelta) -> list[PodD pod_owners = [object.name] pod_owner_kind = object.kind - owners_regex = "|".join(pod_owners) - related_pods_result = await self.query( - f""" - last_over_time( - kube_pod_owner{{ - owner_name=~"{owners_regex}", - owner_kind="{pod_owner_kind}", - namespace="{object.namespace}" - {cluster_label} - }}[{period_literal}] - ) - """ - ) - + related_pods_result = [] + for owner_group in batched(pod_owners, 10): + owners_regex = "|".join(owner_group) + related_pods_result_item = await self.query( + f""" + last_over_time( + kube_pod_owner{{ + owner_name=~"{owners_regex}", + owner_kind="{pod_owner_kind}", + namespace="{object.namespace}" + {cluster_label} + }}[{period_literal}] + ) + """ + ) + related_pods_result.extend(related_pods_result_item) if related_pods_result == []: return [] From d2b63de68401e63c913783016487f74b350445f8 Mon Sep 17 00:00:00 2001 From: Avi-Robusta <97387909+Avi-Robusta@users.noreply.github.com> Date: Wed, 24 Jul 2024 11:56:53 +0300 Subject: [PATCH 118/137] increasing batch size (#318) --- .../prometheus/metrics_service/prometheus_metrics_service.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py index 44bbadb1..c93cf35e 100644 --- a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py +++ b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py @@ -1,5 +1,6 @@ import asyncio import logging +import os from concurrent.futures import ThreadPoolExecutor from datetime import datetime, timedelta from typing import Iterable, List, Optional, Dict, Any @@ -314,7 +315,8 @@ async def load_pods(self, object: K8sObjectData, period: timedelta) -> list[PodD pod_owner_kind = object.kind related_pods_result = [] - for owner_group in batched(pod_owners, 10): + batch_size = int(os.environ.get("KRR_OWNER_BATCH_SIZE", 100)) + for owner_group in batched(pod_owners, batch_size): owners_regex = "|".join(owner_group) related_pods_result_item = await self.query( f""" From 560d423bb7b6472a4a01d3027c7c92bc1764585f Mon Sep 17 00:00:00 2001 From: Avi-Robusta <97387909+Avi-Robusta@users.noreply.github.com> Date: Tue, 30 Jul 2024 16:33:13 +0300 Subject: [PATCH 119/137] [MAIN-1923] kube-system summary (#320) --- .../prometheus_metrics_service.py | 56 +++++++++++-------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py index c93cf35e..ff1ad1de 100644 --- a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py +++ b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py @@ -207,6 +207,25 @@ async def gather_data( return data + async def query_and_validate(self, prom_query) -> Any: + result = await self.query(prom_query) + if len(result) != 1: + logger.warning(f"Error: Expected exactly one result from Prometheus query. {prom_query}") + return None + + result_value = result[0].get("value") + + # Verify that the "value" list has exactly two elements (timestamp and value) + if not result_value: + logger.warning(f"Error: Missing value in Prometheus result. {prom_query}") + return None + + if len(result_value) != 2: + logger.warning(f"Error: Prometheus result values are not of expected size. {prom_query}") + return None + + return result_value[1] + async def get_cluster_summary(self) -> Dict[str, Any]: cluster_label = self.get_prometheus_cluster_label() memory_query = f""" @@ -215,31 +234,22 @@ async def get_cluster_summary(self) -> Dict[str, Any]: cpu_query = f""" sum(max by (instance) (machine_cpu_cores{{ {cluster_label} }})) """ - + kube_system_requests_mem = f""" + sum(max(kube_pod_container_resource_requests{{ namespace='kube-system', resource='memory' {cluster_label} }}) by (job) ) + """ + kube_system_requests_cpu = f""" + sum(max(kube_pod_container_resource_requests{{ namespace='kube-system', resource='cpu' {cluster_label} }}) by (job) ) + """ try: - cluster_memory_result = await self.query(memory_query) - cluster_cpu_result = await self.query(cpu_query) - - # Verify that there is exactly one value in each result - if len(cluster_memory_result) != 1 or len(cluster_cpu_result) != 1: - logger.warning("Error: Expected exactly one result from Prometheus query.") - return {} - - cluster_memory_value = cluster_memory_result[0].get("value") - cluster_cpu_value = cluster_cpu_result[0].get("value") - - # Verify that the "value" list has exactly two elements (timestamp and value) - if not cluster_memory_value or not cluster_cpu_value: - logger.warning("Error: Missing value in Prometheus result.") - return {} - - if len(cluster_memory_value) != 2 or len(cluster_cpu_value) != 2: - logger.warning("Error: Prometheus result values are not of expected size.") - return {} - + cluster_memory_result = await self.query_and_validate(memory_query) + cluster_cpu_result = await self.query_and_validate(cpu_query) + kube_system_mem_result = await self.query_and_validate(kube_system_requests_mem) + kube_system_cpu_result = await self.query_and_validate(kube_system_requests_cpu) return { - "cluster_memory": float(cluster_memory_value[1]), - "cluster_cpu": float(cluster_cpu_value[1]) + "cluster_memory": float(cluster_memory_result), + "cluster_cpu": float(cluster_cpu_result), + "kube_system_mem_req": float(kube_system_mem_result), + "kube_system_cpu_req": float(kube_system_cpu_result) } except Exception as e: From 4671a171fbf7f0de813cd204ba73e62fb1881b41 Mon Sep 17 00:00:00 2001 From: Avi-Robusta <97387909+Avi-Robusta@users.noreply.github.com> Date: Thu, 1 Aug 2024 14:36:14 +0300 Subject: [PATCH 120/137] [MAIN-1923] Bugfix - Kube system summary (#321) --- .../prometheus/metrics_service/prometheus_metrics_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py index ff1ad1de..47020019 100644 --- a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py +++ b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py @@ -235,10 +235,10 @@ async def get_cluster_summary(self) -> Dict[str, Any]: sum(max by (instance) (machine_cpu_cores{{ {cluster_label} }})) """ kube_system_requests_mem = f""" - sum(max(kube_pod_container_resource_requests{{ namespace='kube-system', resource='memory' {cluster_label} }}) by (job) ) + sum(max(kube_pod_container_resource_requests{{ namespace='kube-system', resource='memory' {cluster_label} }}) by (job, pod, container) ) """ kube_system_requests_cpu = f""" - sum(max(kube_pod_container_resource_requests{{ namespace='kube-system', resource='cpu' {cluster_label} }}) by (job) ) + sum(max(kube_pod_container_resource_requests{{ namespace='kube-system', resource='cpu' {cluster_label} }}) by (job, pod, container) ) """ try: cluster_memory_result = await self.query_and_validate(memory_query) From bf98c3ba46eb74f0af28dbd36135fdd35dd61107 Mon Sep 17 00:00:00 2001 From: arik Date: Fri, 16 Aug 2024 07:40:20 +0300 Subject: [PATCH 121/137] add link in the welcome message to the krr video (#326) --- intro.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/intro.txt b/intro.txt index a63a93af..4fe39f54 100644 --- a/intro.txt +++ b/intro.txt @@ -9,4 +9,7 @@ Thanks for using Robusta KRR. If you have any questions or feedback, please feel free to reach out to us at https://github.com/robusta-dev/krr/issues + +Watch our latest video to optimize your workloads and save costs: https://www.youtube.com/watch?v=TYRA2QcDIuI + [/bold magenta] \ No newline at end of file From 3dcc0eb7539f4b5d2713da7e582a599368106542 Mon Sep 17 00:00:00 2001 From: arik Date: Fri, 16 Aug 2024 09:55:50 +0300 Subject: [PATCH 122/137] queries with no label filters bug (#327) bug fix - On queries with no labels, adding the cluster label with a comma prefix created an invalid query ( `machine_memory_bytes{, cluster="foo"}` ) that failed Fixes #325 --- .../metrics_service/prometheus_metrics_service.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py index 47020019..71d5ae8f 100644 --- a/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py +++ b/robusta_krr/core/integrations/prometheus/metrics_service/prometheus_metrics_service.py @@ -228,11 +228,14 @@ async def query_and_validate(self, prom_query) -> Any: async def get_cluster_summary(self) -> Dict[str, Any]: cluster_label = self.get_prometheus_cluster_label() + + # use this for queries with no labels. turn ', cluster="xxx"' to 'cluster="xxx"' + single_cluster_label = cluster_label.replace(",", "") memory_query = f""" - sum(max by (instance) (machine_memory_bytes{{ {cluster_label} }})) + sum(max by (instance) (machine_memory_bytes{{ {single_cluster_label} }})) """ cpu_query = f""" - sum(max by (instance) (machine_cpu_cores{{ {cluster_label} }})) + sum(max by (instance) (machine_cpu_cores{{ {single_cluster_label} }})) """ kube_system_requests_mem = f""" sum(max(kube_pod_container_resource_requests{{ namespace='kube-system', resource='memory' {cluster_label} }}) by (job, pod, container) ) From 14ed7c9db6aabe56846212f3411a2e5c46f71308 Mon Sep 17 00:00:00 2001 From: arik Date: Sun, 18 Aug 2024 19:48:24 +0300 Subject: [PATCH 123/137] allow trusting certificate from custom certificate authorities (#328) --- README.md | 4 ++++ krr.py | 12 ++++++++++ robusta_krr/common/ssl_utils.py | 40 +++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 robusta_krr/common/ssl_utils.py diff --git a/README.md b/README.md index 43baa8e6..44798e5b 100644 --- a/README.md +++ b/README.md @@ -234,6 +234,10 @@ Setup KRR for...

(back to top)

+**Trusting custom Certificate Authority (CA) certificate:** + +If your llm provider url uses a certificate from a custom CA, in order to trust it, base-64 encode the certificate, and store it in an environment variable named ``CERTIFICATE`` + ## Free KRR UI on Robusta SaaS We highly recommend using the [free Robusta SaaS platform](https://platform.robusta.dev/signup/?utm_source=github&utm_medium=krr-readme). You can: diff --git a/krr.py b/krr.py index 3c7ac1ea..319ee615 100644 --- a/krr.py +++ b/krr.py @@ -1,3 +1,15 @@ +import os + +from robusta_krr.common.ssl_utils import add_custom_certificate + +ADDITIONAL_CERTIFICATE: str = os.environ.get("CERTIFICATE", "") + +if add_custom_certificate(ADDITIONAL_CERTIFICATE): + print("added custom certificate") + +# DO NOT ADD ANY CODE ABOVE THIS +# ADDING IMPORTS BEFORE ADDING THE CUSTOM CERTS MIGHT INIT HTTP CLIENTS THAT DOESN'T RESPECT THE CUSTOM CERT + from robusta_krr import run if __name__ == "__main__": diff --git a/robusta_krr/common/ssl_utils.py b/robusta_krr/common/ssl_utils.py new file mode 100644 index 00000000..fd77d610 --- /dev/null +++ b/robusta_krr/common/ssl_utils.py @@ -0,0 +1,40 @@ +import base64 +import os + +import certifi + +CUSTOM_CERTIFICATE_PATH = "/tmp/custom_ca.pem" + + +def append_custom_certificate(custom_ca: str) -> None: + with open(certifi.where(), "ab") as outfile: + outfile.write(base64.b64decode(custom_ca)) + + os.environ["WEBSOCKET_CLIENT_CA_BUNDLE"] = certifi.where() + + +def create_temporary_certificate(custom_ca: str) -> None: + with open(certifi.where(), "rb") as base_cert: + base_cert_content = base_cert.read() + + with open(CUSTOM_CERTIFICATE_PATH, "wb") as outfile: + outfile.write(base_cert_content) + outfile.write(base64.b64decode(custom_ca)) + + os.environ["REQUESTS_CA_BUNDLE"] = CUSTOM_CERTIFICATE_PATH + os.environ["WEBSOCKET_CLIENT_CA_BUNDLE"] = CUSTOM_CERTIFICATE_PATH + certifi.where = lambda: CUSTOM_CERTIFICATE_PATH + + +def add_custom_certificate(custom_ca: str) -> bool: + if not custom_ca: + return False + + # NOTE: Sometimes (Openshift) the certifi.where() is not writable, so we need to + # use a temporary file in case of PermissionError. + try: + append_custom_certificate(custom_ca) + except PermissionError: + create_temporary_certificate(custom_ca) + + return True From 46bf9452ee9aaff5165fb7cb1ebc069e98fdc161 Mon Sep 17 00:00:00 2001 From: Pratik Raj Date: Sat, 21 Sep 2024 14:03:35 +0530 Subject: [PATCH 124/137] feat : use --no-cache-dir flag to pip in dockerfiles to save space (#334) using the "--no-cache-dir" flag in pip install, make sure downloaded packages by pip don't cache on the system. This is a best practice that makes sure to fetch from a repo instead of using a local cached one. Further, in the case of Docker Containers, by restricting caching, we can reduce image size. In terms of stats, it depends upon the number of python packages multiplied by their respective size. e.g for heavy packages with a lot of dependencies it reduces a lot by don't cache pip packages. Further, more detailed information can be found at https://medium.com/sciforce/strategies-of-docker-images-optimization-2ca9cc5719b6 --------- Signed-off-by: Pratik Raj --- Dockerfile | 4 ++-- docker/aws.Dockerfile | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3909fb91..12315714 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,10 +14,10 @@ WORKDIR /app COPY ./requirements.txt requirements.txt -RUN pip install --upgrade pip +RUN pip install --no-cache-dir --upgrade pip # Install the project dependencies RUN python -m ensurepip --upgrade -RUN pip install -r requirements.txt +RUN pip install --no-cache-dir -r requirements.txt # Copy the rest of the application code COPY . . diff --git a/docker/aws.Dockerfile b/docker/aws.Dockerfile index 13d79b8d..7a529344 100644 --- a/docker/aws.Dockerfile +++ b/docker/aws.Dockerfile @@ -11,7 +11,7 @@ RUN apt-get update && \ COPY ./requirements.txt requirements.txt # Install the project dependencies -RUN pip install -r requirements.txt +RUN pip install --no-cache-dir -r requirements.txt # Install curl and unzip for awscli RUN apt-get -y update; apt-get -y install curl; apt-get -y install unzip From b02fabfab56d11e818b77b9beab03a071b4fec1c Mon Sep 17 00:00:00 2001 From: Avi-Robusta <97387909+Avi-Robusta@users.noreply.github.com> Date: Mon, 23 Sep 2024 17:05:06 +0300 Subject: [PATCH 125/137] [MAIN-2169] upgrading the prometrix version (#340) ### Tests ran - Ran krr before the upgrade and after the upgrade to compare the output --- poetry.lock | 31 +++++++++++++++++++------------ pyproject.toml | 4 ++-- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/poetry.lock b/poetry.lock index b5227813..4ba3defa 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1188,23 +1188,26 @@ requests = "*" [[package]] name = "prometrix" -version = "0.1.17" +version = "0.2.0" description = "A Python Prometheus client for all Prometheus instances." optional = false python-versions = "<4.0,>=3.8" files = [ - {file = "prometrix-0.1.17-py3-none-any.whl", hash = "sha256:91f8484916addf657e77bf68af9c622b3f2cb7d15dc68de96cd60b43ae0c3c64"}, - {file = "prometrix-0.1.17.tar.gz", hash = "sha256:aa1fab72024b3cad6f233fd45d69cec5c1bbc9ca534383ecfe11c17df8b0b298"}, + {file = "prometrix-0.2.0-py3-none-any.whl", hash = "sha256:583a38ed3ae9c81ded4b9d57afc908997d3802d33ad4ad2f63b9621ce7536053"}, + {file = "prometrix-0.2.0.tar.gz", hash = "sha256:8d3bcf6291fe1aa0fb707b617a054f09f7606a519b422659c229a0ecf17bf430"}, ] [package.dependencies] boto3 = ">=1.28.15,<2.0.0" botocore = ">=1.31.15,<2.0.0" fonttools = ">=4.43.0,<5.0.0" +idna = ">=3.7,<4.0" pillow = ">=10.3.0,<11.0.0" prometheus-api-client = ">=0.5.3,<0.6.0" pydantic = ">=1.8.1,<2.0.0" -urllib3 = ">=1.26.18,<2.0.0" +requests = ">=2.32.0,<3.0.0" +urllib3 = ">=1.26.20,<2.0.0" +zipp = ">=3.20.1,<4.0.0" [[package]] name = "pyasn1" @@ -1864,13 +1867,13 @@ devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3) [[package]] name = "urllib3" -version = "1.26.19" +version = "1.26.20" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ - {file = "urllib3-1.26.19-py2.py3-none-any.whl", hash = "sha256:37a0344459b199fce0e80b0d3569837ec6b6937435c5244e7fd73fa6006830f3"}, - {file = "urllib3-1.26.19.tar.gz", hash = "sha256:3e3d753a8618b86d7de333b4223005f68720bcd6a7d2bcb9fbd2229ec7c1e429"}, + {file = "urllib3-1.26.20-py2.py3-none-any.whl", hash = "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e"}, + {file = "urllib3-1.26.20.tar.gz", hash = "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32"}, ] [package.extras] @@ -1896,20 +1899,24 @@ test = ["websockets"] [[package]] name = "zipp" -version = "3.19.2" +version = "3.20.2" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" files = [ - {file = "zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"}, - {file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"}, + {file = "zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350"}, + {file = "zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29"}, ] [package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<=3.12.3" -content-hash = "a05011cd5f48d872dbb0e81c6f6f87a483f4bb2cb76194d549d9608acb429793" +content-hash = "ad3dbd10365d7b7557e62bc639a1d58545814f1b451633c6f1aa35f4d6451b62" diff --git a/pyproject.toml b/pyproject.toml index 477a4762..22b852fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,14 +30,14 @@ kubernetes = "^26.1.0" prometheus-api-client = "0.5.3" numpy = ">=1.26.4,<1.27.0" alive-progress = "^3.1.2" -prometrix = "^0.1.17" +prometrix = "0.2.0" slack-sdk = "^3.21.3" pandas = "2.2.2" requests = "2.32.0" pyyaml = "6.0.1" typing-extensions = "4.6.0" idna = "3.7" -urllib3 = "1.26.19" +urllib3 = "^1.26.20" setuptools = "^70.0.0" zipp = "^3.19.1" From 6dfe7e8cfd15515d3ee93d32cbfeac77147958fc Mon Sep 17 00:00:00 2001 From: arik Date: Tue, 24 Sep 2024 18:49:02 +0300 Subject: [PATCH 126/137] update github release action to download-artifact/upload-artifact v4 (#343) --- .github/workflows/build-on-release.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-on-release.yml b/.github/workflows/build-on-release.yml index b3493929..2781c403 100644 --- a/.github/workflows/build-on-release.yml +++ b/.github/workflows/build-on-release.yml @@ -124,7 +124,7 @@ jobs: asset_content_type: application/octet-stream - name: Upload build as artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: krr-${{ matrix.os }}-${{ github.ref_name }} path: ./krr-${{ matrix.os }}-${{ github.ref_name }}.zip @@ -159,7 +159,7 @@ jobs: - name: Checkout Repository uses: actions/checkout@v2 - name: Download MacOS artifact - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v4 with: name: krr-macos-latest-${{ github.ref_name }} - name: Calculate hash @@ -177,7 +177,7 @@ jobs: - name: Checkout Repository uses: actions/checkout@v2 - name: Download Linux artifact - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v4 with: name: krr-ubuntu-latest-${{ github.ref_name }} - name: Calculate hash From 04cadbb362cc8173ef8b02bdb677cb6585f53f9a Mon Sep 17 00:00:00 2001 From: arik Date: Tue, 8 Oct 2024 11:47:09 +0300 Subject: [PATCH 127/137] update expat to fix critical CVEs (#347) --- Dockerfile | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Dockerfile b/Dockerfile index 12315714..9b948028 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,6 +9,11 @@ ENV PATH="/app/venv/bin:$PATH" RUN apt-get update && \ dpkg --add-architecture arm64 +# We're installing here libexpat1, to upgrade the package to include a fix to 3 high CVEs. CVE-2024-45491,CVE-2024-45490,CVE-2024-45492 +RUN apt-get update \ + && apt-get install -y --no-install-recommends libexpat1 \ + && rm -rf /var/lib/apt/lists/* + # Set the working directory WORKDIR /app From f171a4d227b32521880e88f10ccac1d630b03b65 Mon Sep 17 00:00:00 2001 From: Omar Mochtar Date: Thu, 24 Oct 2024 16:59:06 +0700 Subject: [PATCH 128/137] feat(namespace): regex match (#336) This MR will add ability in selecting some namespaces that will be scanned by using regex pattern --- README.md | 6 +++ .../core/integrations/kubernetes/__init__.py | 48 +++++++++++++++++-- tests/test_krr.py | 43 +++++++++++++++++ 3 files changed, 92 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 44798e5b..6df91ca9 100644 --- a/README.md +++ b/README.md @@ -307,6 +307,12 @@ List as many namespaces as you want with `-n` (in this case, `default` and `ingr krr simple -n default -n ingress-nginx ``` +The -n flag also supports regex matches like -n kube-.*. To use regexes, you must have permissions to list namespaces in the target cluster. + +```sh +krr simple -n default -n 'ingress-.*' +``` + See [example ServiceAccount and RBAC permissions](./tests/single_namespace_permissions.yaml)
diff --git a/robusta_krr/core/integrations/kubernetes/__init__.py b/robusta_krr/core/integrations/kubernetes/__init__.py index f2131366..78419fdc 100644 --- a/robusta_krr/core/integrations/kubernetes/__init__.py +++ b/robusta_krr/core/integrations/kubernetes/__init__.py @@ -1,8 +1,9 @@ import asyncio import logging +import re from collections import defaultdict from concurrent.futures import ThreadPoolExecutor -from typing import Any, Awaitable, Callable, Iterable, Optional, Union +from typing import Any, Awaitable, Callable, Iterable, Optional, Union, Literal from kubernetes import client, config # type: ignore from kubernetes.client import ApiException @@ -47,6 +48,43 @@ def __init__(self, cluster: Optional[str]=None): self.__jobs_for_cronjobs: dict[str, list[V1Job]] = {} self.__jobs_loading_locks: defaultdict[str, asyncio.Lock] = defaultdict(asyncio.Lock) + self.__namespaces: Union[list[str, None]] = None + + @property + def namespaces(self) -> Union[list[str], Literal["*"]]: + """wrapper for settings.namespaces, which will do expand namespace list if some regex pattern included + + Returns: + A list of namespace that will be scanned + """ + # just return the list if it's already initialized + if self.__namespaces != None: + return self.__namespaces + + setting_ns = settings.namespaces + if setting_ns == "*": + self.__namespaces = setting_ns + return self.__namespaces + + self.__namespaces = [] + expand_list: list[re.Pattern] = [] + ns_regex_chars = re.compile(r"[\\*|\(.*?\)|\[.*?\]|\^|\$]") + for ns in setting_ns: + if ns_regex_chars.search(ns): + logger.debug(f"{ns} is detected as regex pattern in expanding namespace list") + expand_list.append(re.compile(ns)) + else: + self.__namespaces.append(ns) + + if expand_list: + logger.info("found regex pattern in provided namespace argument, expanding namespace list") + all_ns = [ ns.metadata.name for ns in self.core.list_namespace().items ] + for expand_ns in expand_list: + for ns in all_ns: + if expand_ns.fullmatch(ns) and ns not in self.__namespaces: + self.__namespaces.append(ns) + + return self.__namespaces async def list_scannable_objects(self) -> list[K8sObjectData]: """List all scannable objects. @@ -56,7 +94,7 @@ async def list_scannable_objects(self) -> list[K8sObjectData]: """ logger.info(f"Listing scannable objects in {self.cluster}") - logger.debug(f"Namespaces: {settings.namespaces}") + logger.debug(f"Namespaces: {self.namespaces}") logger.debug(f"Resources: {settings.resources}") self.__hpa_list = await self._try_list_hpa() @@ -75,7 +113,7 @@ async def list_scannable_objects(self) -> list[K8sObjectData]: for workload_objects in workload_object_lists for object in workload_objects # NOTE: By default we will filter out kube-system namespace - if not (settings.namespaces == "*" and object.namespace == "kube-system") + if not (self.namespaces == "*" and object.namespace == "kube-system") ] async def _list_jobs_for_cronjobs(self, namespace: str) -> list[V1Job]: @@ -189,7 +227,7 @@ async def _list_namespaced_or_global_objects( logger.debug(f"Listing {kind}s in {self.cluster}") loop = asyncio.get_running_loop() - if settings.namespaces == "*": + if self.namespaces == "*": requests = [ loop.run_in_executor( self.executor, @@ -209,7 +247,7 @@ async def _list_namespaced_or_global_objects( label_selector=settings.selector, ), ) - for namespace in settings.namespaces + for namespace in self.namespaces ] result = [ diff --git a/tests/test_krr.py b/tests/test_krr.py index fe441769..d4c6d6a9 100644 --- a/tests/test_krr.py +++ b/tests/test_krr.py @@ -1,7 +1,11 @@ import pytest +from typing import Literal, Union +from unittest.mock import patch, Mock, MagicMock from typer.testing import CliRunner from robusta_krr.main import app, load_commands +from robusta_krr.core.integrations.kubernetes import ClusterLoader +from robusta_krr.core.models.config import settings runner = CliRunner(mix_stderr=False) load_commands() @@ -34,3 +38,42 @@ def test_output_formats(format: str, output: str): assert result.exit_code == 0, result.exc_info except AssertionError as e: raise e from result.exception + +@pytest.mark.parametrize( + "setting_namespaces,cluster_all_ns,expected",[ + ( + # default settings + "*", + ["kube-system", "robusta-frontend", "robusta-backend", "infra-grafana"], + "*" + ), + ( + # list of namespace provided from arguments without regex pattern + ["robusta-krr", "kube-system"], + ["kube-system", "robusta-frontend", "robusta-backend", "robusta-krr"], + ["robusta-krr", "kube-system"] + ), + ( + # list of namespace provided from arguments with regex pattern and will not duplicating in final result + ["robusta-.*", "robusta-frontend"], + ["kube-system", "robusta-frontend", "robusta-backend", "robusta-krr"], + ["robusta-frontend", "robusta-backend", "robusta-krr"] + ), + ( + # namespace provided with regex pattern and will match for some namespaces + [".*end$"], + ["kube-system", "robusta-frontend", "robusta-backend", "robusta-krr"], + ["robusta-frontend", "robusta-backend"] + ) + ] + ) +def test_cluster_namespace_list( + setting_namespaces: Union[Literal["*"], list[str]], + cluster_all_ns: list[str], + expected: Union[Literal["*"], list[str]], + ): + cluster = ClusterLoader() + with patch("robusta_krr.core.models.config.settings.namespaces", setting_namespaces): + with patch.object(cluster.core, "list_namespace", return_value=MagicMock( + items=[MagicMock(**{"metadata.name": m}) for m in cluster_all_ns])): + assert sorted(cluster.namespaces) == sorted(expected) From 6a5cfa75b530809c571d6cef02bea5c5ee1a01c7 Mon Sep 17 00:00:00 2001 From: itisallgood <25401000+itisallgood@users.noreply.github.com> Date: Thu, 24 Oct 2024 16:39:31 +0200 Subject: [PATCH 129/137] Added validation for namespaces (#349) --- robusta_krr/core/models/config.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/robusta_krr/core/models/config.py b/robusta_krr/core/models/config.py index 6ad5804c..d7c92976 100644 --- a/robusta_krr/core/models/config.py +++ b/robusta_krr/core/models/config.py @@ -101,6 +101,11 @@ def validate_namespaces(cls, v: Union[list[str], Literal["*"]]) -> Union[list[st if v == []: return "*" + if isinstance(v, list): + for val in v: + if val.startswith("*"): + raise ValueError("Namespace's values cannot start with an asterisk (*)") + return [val.lower() for val in v] @pd.validator("resources", pre=True) From 7cc686133b84811cfb604faf5d88139bea561505 Mon Sep 17 00:00:00 2001 From: Natan Yellin Date: Fri, 25 Oct 2024 11:02:22 +0300 Subject: [PATCH 130/137] Better logging message about history (#351) --- robusta_krr/core/runner.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/robusta_krr/core/runner.py b/robusta_krr/core/runner.py index d418b8eb..76e1474a 100644 --- a/robusta_krr/core/runner.py +++ b/robusta_krr/core/runner.py @@ -218,8 +218,8 @@ async def _check_data_availability(self, cluster: Optional[str]) -> None: try: history_range = await prometheus_loader.get_history_range(timedelta(hours=5)) except ValueError: - logger.warning( - f"Was not able to get history range for cluster {cluster}. This is not critical, will try continue." + logger.info( + f"Unable to check how much historical data is available on cluster {cluster}. Will assume it is sufficient and calculate recommendations anyway. (You can usually ignore this. Not all Prometheus compatible metric stores support checking history settings.)" ) self.errors.append( { From b25e2ec0a32097729baaea52c754b7995b929ee5 Mon Sep 17 00:00:00 2001 From: Saravanan Palanisamy Date: Mon, 28 Oct 2024 13:01:14 +0400 Subject: [PATCH 131/137] add html formatter (#353) **Problem Statement** Default option `table` provides pretty output in console using styles (colors, etc..) but when we save it into a file using `--fileoutput` styles will not be passed. If we share the output file to others via s3 or something, styles will be missed and looks plain. It would be nice to keep the styles when rendering from the file. **Solution** Default option `table` using `Rich` library and same library has function `export_html` to generate html from table output. It will keep the same styles used in default table formatter. --- README.md | 13 +++++++------ robusta_krr/core/runner.py | 4 ++-- robusta_krr/formatters/__init__.py | 1 + robusta_krr/formatters/html.py | 12 ++++++++++++ 4 files changed, 22 insertions(+), 8 deletions(-) create mode 100644 robusta_krr/formatters/html.py diff --git a/README.md b/README.md index 6df91ca9..93cd35ec 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Request Feature Β· Support -
Like KRR? Please ⭐ this repository to show your support! +
Like KRR? Please ⭐ this repository to show your support!

@@ -119,7 +119,7 @@ Read more about [how KRR works](#how-krr-works) -## Installation +## Installation ### Requirements @@ -130,7 +130,7 @@ KRR requires Prometheus 2.26+, [kube-state-metrics](https://github.com/kubernete No setup is required if you use kube-prometheus-stack or Robusta's Embedded Prometheus. If you have a different setup, make sure the following metrics exist: - + - `container_cpu_usage_seconds_total` - `container_memory_working_set_bytes` - `kube_replicaset_owner` @@ -179,7 +179,7 @@ You can install using brew (see above) on [WSL2](https://docs.brew.sh/Homebrew-o
Airgapped Installation (Offline Environments) - + You can download pre-built binaries from Releases or use the prebuilt Docker container. For example, the container for version 1.8.3 is: ``` @@ -258,7 +258,7 @@ We highly recommend using the [free Robusta SaaS platform](https://platform.robu
Basic usage - + ```sh krr simple ``` @@ -266,7 +266,7 @@ krr simple
Tweak the recommendation algorithm (strategy) - + Most helpful flags: - `--cpu-min` Sets the minimum recommended cpu value in millicores @@ -347,6 +347,7 @@ Currently KRR ships with a few formatters to represent the scan data: - `yaml` - `pprint` - data representation from python's pprint library - `csv` - export data to a csv file in the current directory +- `html` To run a strategy with a selected formatter, add a `-f` flag. Usually this should be combined with `--fileoutput ` to write clean output to file without logs: diff --git a/robusta_krr/core/runner.py b/robusta_krr/core/runner.py index 76e1474a..9a8b1a81 100644 --- a/robusta_krr/core/runner.py +++ b/robusta_krr/core/runner.py @@ -120,8 +120,8 @@ def _process_result(self, result: Result) -> None: file_name = settings.slack_output with open(file_name, "w") as target_file: - # don't use rich when writing a csv to avoid line wrapping etc - if settings.format == "csv": + # don't use rich when writing a csv or html to avoid line wrapping etc + if settings.format == "csv" or settings.format == "html": target_file.write(formatted) else: console = Console(file=target_file, width=settings.width) diff --git a/robusta_krr/formatters/__init__.py b/robusta_krr/formatters/__init__.py index e34a25f1..7e1d1641 100644 --- a/robusta_krr/formatters/__init__.py +++ b/robusta_krr/formatters/__init__.py @@ -3,3 +3,4 @@ from .table import table from .yaml import yaml from .csv import csv +from .html import html diff --git a/robusta_krr/formatters/html.py b/robusta_krr/formatters/html.py new file mode 100644 index 00000000..a028d969 --- /dev/null +++ b/robusta_krr/formatters/html.py @@ -0,0 +1,12 @@ +from rich.console import Console + +from robusta_krr.core.abstract import formatters +from robusta_krr.core.models.result import Result +from .table import table + +@formatters.register("html") +def html(result: Result) -> str: + console = Console(record=True) + table_output = table(result) + console.print(table_output) + return console.export_html(inline_styles=True) From 090e25ccd7a0f96e70dcb9bffa4e58cd2bead03a Mon Sep 17 00:00:00 2001 From: Natan Yellin Date: Fri, 1 Nov 2024 18:47:15 +0200 Subject: [PATCH 132/137] Update README.md (#355) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 93cd35ec..ea53d27c 100644 --- a/README.md +++ b/README.md @@ -178,7 +178,7 @@ You can install using brew (see above) on [WSL2](https://docs.brew.sh/Homebrew-o
- Airgapped Installation (Offline Environments) + Docker image, binaries, and airgapped installation (offline environments) You can download pre-built binaries from Releases or use the prebuilt Docker container. For example, the container for version 1.8.3 is: From 94d006346e66ca147ecf39b91156962745aa8bfe Mon Sep 17 00:00:00 2001 From: Natan Yellin Date: Tue, 5 Nov 2024 09:06:52 +0200 Subject: [PATCH 133/137] Update README.md (#357) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ea53d27c..35ae0388 100644 --- a/README.md +++ b/README.md @@ -669,7 +669,7 @@ _We use pytest to run tests._ 1. Install the project manually (see above) 2. Navigate to the project root directory -3. Install poetry (https://python-poetry.org/docs/#installing-with-the-official-installer) +3. Install [poetry](https://python-poetry.org/docs/#installing-with-the-official-installer) 4. Install dev dependencies: ```sh From 3d7c9a1b0056cddfa8a750c049c48238f560ab69 Mon Sep 17 00:00:00 2001 From: moshemorad Date: Mon, 11 Nov 2024 13:07:38 +0200 Subject: [PATCH 134/137] Handle resources allocation with scientific notation better (#361) --- .gitignore | 1 + robusta_krr/utils/resource_units.py | 9 +++--- tests/models/test_resource_allocations.py | 36 +++++++++++++++++++++++ 3 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 tests/models/test_resource_allocations.py diff --git a/.gitignore b/.gitignore index 02d0ed42..4a103fe0 100644 --- a/.gitignore +++ b/.gitignore @@ -133,3 +133,4 @@ dmypy.json .DS_Store robusta_lib .idea +.vscode \ No newline at end of file diff --git a/robusta_krr/utils/resource_units.py b/robusta_krr/utils/resource_units.py index 9fd290dc..dbda08bb 100644 --- a/robusta_krr/utils/resource_units.py +++ b/robusta_krr/utils/resource_units.py @@ -1,6 +1,6 @@ from typing import Literal, Union -UNITS = { +UNITS: dict[str, float] = { "m": 0.001, "Ki": 1024, "Mi": 1024**2, @@ -23,15 +23,14 @@ def parse(x: str, /) -> Union[float, int]: for unit, multiplier in UNITS.items(): if x.endswith(unit): return float(x[: -len(unit)]) * multiplier - if "." in x: - return float(x) - return int(x) + + return float(x) def get_base(x: str, /) -> Literal[1024, 1000]: """Returns the base of the unit.""" - for unit, multiplier in UNITS.items(): + for unit, _ in UNITS.items(): if x.endswith(unit): return 1024 if unit in ["Ki", "Mi", "Gi", "Ti", "Pi", "Ei"] else 1000 return 1000 if "." in x else 1024 diff --git a/tests/models/test_resource_allocations.py b/tests/models/test_resource_allocations.py new file mode 100644 index 00000000..49ca949f --- /dev/null +++ b/tests/models/test_resource_allocations.py @@ -0,0 +1,36 @@ +from typing import Union + +import pytest + +from robusta_krr.core.models.allocations import ResourceAllocations, ResourceType + + +@pytest.mark.parametrize( + "cpu", + [ + {"request": "5m", "limit": None}, + {"request": 0.005, "limit": None}, + ], +) +@pytest.mark.parametrize( + "memory", + [ + {"request": 128974848, "limit": 128974848}, + {"request": 128.974848e6, "limit": 128.974848e6}, + {"request": "128.9748480M", "limit": "128.9748480M"}, + {"request": "128974848000m", "limit": "128974848000m"}, + {"request": "123Mi", "limit": "123Mi"}, + {"request": "128974848e0", "limit": "128974848e0"}, + ], +) +def test_resource_allocation_supported_formats( + cpu: dict[str, Union[str, int, float, None]], memory: dict[str, Union[str, int, float, None]] +): + allocations = ResourceAllocations( + requests={ResourceType.CPU: cpu["request"], ResourceType.Memory: memory["request"]}, + limits={ResourceType.CPU: cpu["limit"], ResourceType.Memory: memory["limit"]}, + ) + assert allocations.requests[ResourceType.CPU] == 0.005 + assert allocations.limits[ResourceType.CPU] == None + assert (allocations.requests[ResourceType.Memory] // 1) == 128974848.0 + assert (allocations.limits[ResourceType.Memory] // 1) == 128974848.0 From 1515b3d96f50c2b116236739fd157af893a69f2f Mon Sep 17 00:00:00 2001 From: moshemorad Date: Tue, 12 Nov 2024 11:15:52 +0200 Subject: [PATCH 135/137] Add columns to csv (#359) --- .gitignore | 2 +- robusta_krr/core/models/config.py | 5 +- robusta_krr/core/models/result.py | 5 +- robusta_krr/formatters/csv.py | 87 +++++--- robusta_krr/main.py | 32 ++- tests/formatters/test_csv_formatter.py | 291 +++++++++++++++++++++++++ tests/test_krr.py | 2 +- tests/test_runner.py | 21 ++ 8 files changed, 405 insertions(+), 40 deletions(-) create mode 100644 tests/formatters/test_csv_formatter.py create mode 100644 tests/test_runner.py diff --git a/.gitignore b/.gitignore index 4a103fe0..cec2b2c3 100644 --- a/.gitignore +++ b/.gitignore @@ -133,4 +133,4 @@ dmypy.json .DS_Store robusta_lib .idea -.vscode \ No newline at end of file +.vscode diff --git a/robusta_krr/core/models/config.py b/robusta_krr/core/models/config.py index d7c92976..32241ed1 100644 --- a/robusta_krr/core/models/config.py +++ b/robusta_krr/core/models/config.py @@ -58,10 +58,11 @@ class Config(pd.BaseSettings): strategy: str log_to_stderr: bool width: Optional[int] = pd.Field(None, ge=1) + show_severity: bool = True # Output Settings file_output: Optional[str] = pd.Field(None) - file_output_dynamic = bool = pd.Field(False) + file_output_dynamic: bool = pd.Field(False) slack_output: Optional[str] = pd.Field(None) other_args: dict[str, Any] @@ -105,7 +106,7 @@ def validate_namespaces(cls, v: Union[list[str], Literal["*"]]) -> Union[list[st for val in v: if val.startswith("*"): raise ValueError("Namespace's values cannot start with an asterisk (*)") - + return [val.lower() for val in v] @pd.validator("resources", pre=True) diff --git a/robusta_krr/core/models/result.py b/robusta_krr/core/models/result.py index 514590f9..827f8690 100644 --- a/robusta_krr/core/models/result.py +++ b/robusta_krr/core/models/result.py @@ -17,8 +17,8 @@ class Recommendation(pd.BaseModel): class ResourceRecommendation(pd.BaseModel): - requests: dict[ResourceType, RecommendationValue] - limits: dict[ResourceType, RecommendationValue] + requests: dict[ResourceType, Union[RecommendationValue, Recommendation]] + limits: dict[ResourceType, Union[RecommendationValue, Recommendation]] info: dict[ResourceType, Optional[str]] @@ -40,6 +40,7 @@ def calculate(cls, object: K8sObjectData, recommendation: ResourceAllocations) - current_severity = Severity.calculate(current, recommended, resource_type) + #TODO: consider... changing field after model created doesn't validate it. getattr(recommendation_processed, selector)[resource_type] = Recommendation( value=recommended, severity=current_severity ) diff --git a/robusta_krr/formatters/csv.py b/robusta_krr/formatters/csv.py index 812c35bd..d32f5f94 100644 --- a/robusta_krr/formatters/csv.py +++ b/robusta_krr/formatters/csv.py @@ -1,14 +1,31 @@ -import itertools import csv -import logging import io +import itertools +import logging +from typing import Any from robusta_krr.core.abstract import formatters -from robusta_krr.core.models.allocations import RecommendationValue, format_recommendation_value, format_diff, NONE_LITERAL, NAN_LITERAL +from robusta_krr.core.models.allocations import NONE_LITERAL, format_diff, format_recommendation_value +from robusta_krr.core.models.config import settings from robusta_krr.core.models.result import ResourceScan, ResourceType, Result logger = logging.getLogger("krr") + +NAMESPACE_HEADER = "Namespace" +NAME_HEADER = "Name" +PODS_HEADER = "Pods" +OLD_PODS_HEADER = "Old Pods" +TYPE_HEADER = "Type" +CONTAINER_HEADER = "Container" +CLUSTER_HEADER = "Cluster" +SEVERITY_HEADER = "Severity" + +RESOURCE_DIFF_HEADER = "{resource_name} Diff" +RESOURCE_REQUESTS_HEADER = "{resource_name} Requests" +RESOURCE_LIMITS_HEADER = "{resource_name} Limits" + + def _format_request_str(item: ResourceScan, resource: ResourceType, selector: str) -> str: allocated = getattr(item.object.allocations, selector)[resource] recommended = getattr(item.recommended, selector)[resource] @@ -20,12 +37,8 @@ def _format_request_str(item: ResourceScan, resource: ResourceType, selector: st if diff != "": diff = f"({diff}) " - return ( - diff - + format_recommendation_value(allocated) - + " -> " - + format_recommendation_value(recommended.value) - ) + return diff + format_recommendation_value(allocated) + " -> " + format_recommendation_value(recommended.value) + def _format_total_diff(item: ResourceScan, resource: ResourceType, pods_current: int) -> str: selector = "requests" @@ -34,43 +47,57 @@ def _format_total_diff(item: ResourceScan, resource: ResourceType, pods_current: return format_diff(allocated, recommended, selector, pods_current) + @formatters.register("csv") def csv_exporter(result: Result) -> str: # We need to order the resource columns so that they are in the format of Namespace,Name,Pods,Old Pods,Type,Container,CPU Diff,CPU Requests,CPU Limits,Memory Diff,Memory Requests,Memory Limits - resource_columns = [] + csv_columns = ["Namespace", "Name", "Pods", "Old Pods", "Type", "Container"] + + if settings.show_cluster_name: + csv_columns.insert(0, "Cluster") + + if settings.show_severity: + csv_columns.append("Severity") + for resource in ResourceType: - resource_columns.append(f"{resource.name} Diff") - resource_columns.append(f"{resource.name} Requests") - resource_columns.append(f"{resource.name} Limits") + csv_columns.append(RESOURCE_DIFF_HEADER.format(resource_name=resource.name)) + csv_columns.append(RESOURCE_REQUESTS_HEADER.format(resource_name=resource.name)) + csv_columns.append(RESOURCE_LIMITS_HEADER.format(resource_name=resource.name)) output = io.StringIO() - csv_writer = csv.writer(output) - csv_writer.writerow([ - "Namespace", "Name", "Pods", "Old Pods", "Type", "Container", - *resource_columns - ]) + csv_writer = csv.DictWriter(output, csv_columns, extrasaction="ignore") + csv_writer.writeheader() for _, group in itertools.groupby( enumerate(result.scans), key=lambda x: (x[1].object.cluster, x[1].object.namespace, x[1].object.name) ): group_items = list(group) - for j, (i, item) in enumerate(group_items): + for j, (_, item) in enumerate(group_items): full_info_row = j == 0 - row = [ - item.object.namespace if full_info_row else "", - item.object.name if full_info_row else "", - f"{item.object.current_pods_count}" if full_info_row else "", - f"{item.object.deleted_pods_count}" if full_info_row else "", - item.object.kind if full_info_row else "", - item.object.container, - ] + row: dict[str, Any] = { + NAMESPACE_HEADER: item.object.namespace if full_info_row else "", + NAME_HEADER: item.object.name if full_info_row else "", + PODS_HEADER: f"{item.object.current_pods_count}" if full_info_row else "", + OLD_PODS_HEADER: f"{item.object.deleted_pods_count}" if full_info_row else "", + TYPE_HEADER: item.object.kind if full_info_row else "", + CONTAINER_HEADER: item.object.container, + SEVERITY_HEADER: item.severity, + CLUSTER_HEADER: item.object.cluster, + } for resource in ResourceType: - row.append(_format_total_diff(item, resource, item.object.current_pods_count)) - row += [_format_request_str(item, resource, selector) for selector in ["requests", "limits"]] + row[RESOURCE_DIFF_HEADER.format(resource_name=resource.name)] = _format_total_diff( + item, resource, item.object.current_pods_count + ) + row[RESOURCE_REQUESTS_HEADER.format(resource_name=resource.name)] = _format_request_str( + item, resource, "requests" + ) + row[RESOURCE_LIMITS_HEADER.format(resource_name=resource.name)] = _format_request_str( + item, resource, "limits" + ) csv_writer.writerow(row) - + return output.getvalue() diff --git a/robusta_krr/main.py b/robusta_krr/main.py index dd9ee03b..f82224b8 100644 --- a/robusta_krr/main.py +++ b/robusta_krr/main.py @@ -7,6 +7,7 @@ from typing import List, Optional from uuid import UUID +import click import typer import urllib3 from pydantic import ValidationError # noqa: F401 @@ -19,7 +20,12 @@ from robusta_krr.core.runner import Runner from robusta_krr.utils.version import get_version -app = typer.Typer(pretty_exceptions_show_locals=False, pretty_exceptions_short=True, no_args_is_help=True, help="IMPORTANT: Run `krr simple --help` to see all cli flags!") +app = typer.Typer( + pretty_exceptions_show_locals=False, + pretty_exceptions_short=True, + no_args_is_help=True, + help="IMPORTANT: Run `krr simple --help` to see all cli flags!", +) # NOTE: Disable insecure request warnings, as it might be expected to use self-signed certificates inside the cluster urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) @@ -216,7 +222,16 @@ def run_strategy( rich_help_panel="Logging Settings", ), show_cluster_name: bool = typer.Option( - False, "--show-cluster-name", help="In table output, always show the cluster name even for a single cluster", rich_help_panel="Output Settings" + False, + "--show-cluster-name", + help="In table output, always show the cluster name even for a single cluster", + rich_help_panel="Output Settings", + ), + show_severity: bool = typer.Option( + True, + " /--exclude-severity", + help="Whether to include the severity in the output or not", + rich_help_panel="Output Settings", ), verbose: bool = typer.Option( False, "--verbose", "-v", help="Enable verbose mode", rich_help_panel="Logging Settings" @@ -234,10 +249,16 @@ def run_strategy( rich_help_panel="Logging Settings", ), file_output: Optional[str] = typer.Option( - None, "--fileoutput", help="Filename to write output to (if not specified, file output is disabled)", rich_help_panel="Output Settings" + None, + "--fileoutput", + help="Filename to write output to (if not specified, file output is disabled)", + rich_help_panel="Output Settings", ), file_output_dynamic: bool = typer.Option( - False, "--fileoutput-dynamic", help="Ignore --fileoutput and write files to the current directory in the format krr-{datetime}.{format} (e.g. krr-20240518223924.csv)", rich_help_panel="Output Settings" + False, + "--fileoutput-dynamic", + help="Ignore --fileoutput and write files to the current directory in the format krr-{datetime}.{format} (e.g. krr-20240518223924.csv)", + rich_help_panel="Output Settings", ), slack_output: Optional[str] = typer.Option( None, @@ -248,6 +269,8 @@ def run_strategy( **strategy_args, ) -> None: f"""Run KRR using the `{_strategy_name}` strategy""" + if not show_severity and format != "csv": + raise click.BadOptionUsage("--exclude-severity", "--exclude-severity works only with format=csv") try: config = Config( @@ -284,6 +307,7 @@ def run_strategy( file_output=file_output, file_output_dynamic=file_output_dynamic, slack_output=slack_output, + show_severity=show_severity, strategy=_strategy_name, other_args=strategy_args, ) diff --git a/tests/formatters/test_csv_formatter.py b/tests/formatters/test_csv_formatter.py new file mode 100644 index 00000000..150b2aa5 --- /dev/null +++ b/tests/formatters/test_csv_formatter.py @@ -0,0 +1,291 @@ +import csv +import io +import json +from typing import Any + +import pytest + +from robusta_krr.core.models.config import Config +from robusta_krr.core.models.result import Result +from robusta_krr.formatters.csv import csv_exporter + +RESULT = """ +{ + "scans": [ + { + "object": { + "cluster": "mock-cluster", + "name": "mock-object-1", + "container": "mock-container-1", + "pods": [ + { + "name": "mock-pod-1", + "deleted": false + }, + { + "name": "mock-pod-2", + "deleted": false + }, + { + "name": "mock-pod-3", + "deleted": true + } + ], + "hpa": null, + "namespace": "default", + "kind": "Deployment", + "allocations": { + "requests": { + "cpu": "50m", + "memory": "2048Mi" + }, + "limits": { + "cpu": 2.0, + "memory": 2.0 + }, + "info": {} + }, + "warnings": [] + }, + "recommended": { + "requests": { + "cpu": { + "value": 0.0065, + "severity": "UNKNOWN" + }, + "memory": { + "value": 0.5, + "severity": "CRITICAL" + } + }, + "limits": { + "cpu": { + "value": "?", + "severity": "UNKNOWN" + }, + "memory": { + "value": 0.5, + "severity": "CRITICAL" + } + }, + "info": { + "cpu": "Not enough data", + "memory": "Not enough data" + } + }, + "severity": "CRITICAL" + } + ], + "score": 100, + "resources": [ + "cpu", + "memory" + ], + "description": "tests data", + "strategy": { + "name": "simple", + "settings": { + "history_duration": 336.0, + "timeframe_duration": 1.25, + "cpu_percentile": 95.0, + "memory_buffer_percentage": 15.0, + "points_required": 100, + "allow_hpa": false, + "use_oomkill_data": false, + "oom_memory_buffer_percentage": 25.0 + } + }, + "errors": [], + "clusterSummary": {}, + "config": { + "quiet": false, + "verbose": false, + "clusters": [], + "kubeconfig": null, + "impersonate_user": null, + "impersonate_group": null, + "namespaces": "*", + "resources": [], + "selector": null, + "cpu_min_value": 10, + "memory_min_value": 100, + "prometheus_url": null, + "prometheus_auth_header": null, + "prometheus_other_headers": {}, + "prometheus_ssl_enabled": false, + "prometheus_cluster_label": null, + "prometheus_label": null, + "eks_managed_prom": false, + "eks_managed_prom_profile_name": null, + "eks_access_key": null, + "eks_secret_key": null, + "eks_service_name": "aps", + "eks_managed_prom_region": null, + "coralogix_token": null, + "openshift": false, + "max_workers": 10, + "format": "csv", + "show_cluster_name": false, + "strategy": "simple", + "log_to_stderr": false, + "width": null, + "file_output": null, + "slack_output": null, + "other_args": { + "history_duration": "336", + "timeframe_duration": "1.25", + "cpu_percentile": "95", + "memory_buffer_percentage": "15", + "points_required": "100", + "allow_hpa": false, + "use_oomkill_data": false, + "oom_memory_buffer_percentage": "25" + }, + "inside_cluster": false, + "file_output_dynamic": false + } +} +""" + + +def _load_result(override_config: dict[str, Any]) -> Result: + res_data = json.loads(RESULT) + res_data["config"].update(override_config) + result = Result(**res_data) + Config.set_config(result.config) + return result + + +@pytest.mark.parametrize( + "override_config, expected_headers", + [ + ( + {}, + [ + "Namespace", + "Name", + "Pods", + "Old Pods", + "Type", + "Container", + "Severity", + "CPU Diff", + "CPU Requests", + "CPU Limits", + "Memory Diff", + "Memory Requests", + "Memory Limits", + ], + ), + ( + {"show_severity": False}, + [ + "Namespace", + "Name", + "Pods", + "Old Pods", + "Type", + "Container", + "CPU Diff", + "CPU Requests", + "CPU Limits", + "Memory Diff", + "Memory Requests", + "Memory Limits", + ], + ), + ( + {"show_cluster_name": True}, + [ + "Cluster", + "Namespace", + "Name", + "Pods", + "Old Pods", + "Type", + "Container", + "Severity", + "CPU Diff", + "CPU Requests", + "CPU Limits", + "Memory Diff", + "Memory Requests", + "Memory Limits", + ], + ), + ], +) +def test_csv_headers(override_config: dict[str, Any], expected_headers: list[str]) -> None: + result = _load_result(override_config=override_config) + output = csv_exporter(result) + reader = csv.DictReader(io.StringIO(output)) + + assert reader.fieldnames == expected_headers + + +@pytest.mark.parametrize( + "override_config, expected_first_row", + [ + ( + {}, + { + "Namespace": "default", + "Name": "mock-object-1", + "Pods": "2", + "Old Pods": "1", + "Type": "Deployment", + "Container": "mock-container-1", + 'Severity': 'CRITICAL', + "CPU Diff": "-87m", + "CPU Requests": "(-43m) 50m -> 6m", + "CPU Limits": "2.0 -> ?", + "Memory Diff": "-4096Mi", + "Memory Requests": "(-2048Mi) 2048Mi -> 500m", + "Memory Limits": "2.0 -> 500m", + }, + ), + ( + {"show_severity": False}, + { + "Namespace": "default", + "Name": "mock-object-1", + "Pods": "2", + "Old Pods": "1", + "Type": "Deployment", + "Container": "mock-container-1", + "CPU Diff": "-87m", + "CPU Requests": "(-43m) 50m -> 6m", + "CPU Limits": "2.0 -> ?", + "Memory Diff": "-4096Mi", + "Memory Requests": "(-2048Mi) 2048Mi -> 500m", + "Memory Limits": "2.0 -> 500m", + }, + ), + ( + {"show_cluster_name": True}, + { + "Cluster": "mock-cluster", + "Namespace": "default", + "Name": "mock-object-1", + "Pods": "2", + "Old Pods": "1", + "Type": "Deployment", + "Container": "mock-container-1", + 'Severity': 'CRITICAL', + "CPU Diff": "-87m", + "CPU Requests": "(-43m) 50m -> 6m", + "CPU Limits": "2.0 -> ?", + "Memory Diff": "-4096Mi", + "Memory Requests": "(-2048Mi) 2048Mi -> 500m", + "Memory Limits": "2.0 -> 500m", + }, + ), + ], +) +def test_csv_row_value(override_config: dict[str, Any], expected_first_row: list[str]) -> None: + result = _load_result(override_config=override_config) + output = csv_exporter(result) + reader = csv.DictReader(io.StringIO(output)) + + first_row: dict[str, Any] = next(reader) + assert first_row == expected_first_row diff --git a/tests/test_krr.py b/tests/test_krr.py index d4c6d6a9..90ea2af5 100644 --- a/tests/test_krr.py +++ b/tests/test_krr.py @@ -30,7 +30,7 @@ def test_run(log_flag: str): raise e from result.exception -@pytest.mark.parametrize("format", ["json", "yaml", "table", "pprint"]) +@pytest.mark.parametrize("format", ["json", "yaml", "table", "pprint", "csv"]) @pytest.mark.parametrize("output", ["--logtostderr", "-q"]) def test_output_formats(format: str, output: str): result = runner.invoke(app, [STRATEGY_NAME, output, "-f", format]) diff --git a/tests/test_runner.py b/tests/test_runner.py new file mode 100644 index 00000000..dcf4e19b --- /dev/null +++ b/tests/test_runner.py @@ -0,0 +1,21 @@ +import pytest +from click.testing import Result +from typer.testing import CliRunner + +from robusta_krr.main import app, load_commands + +runner = CliRunner(mix_stderr=False) +load_commands() + + +@pytest.mark.parametrize( + "args, expected_exit_code", + [ + (["--exclude-severity", "-f", "csv"], 0), + (["--exclude-severity", "-f", "table"], 2), + (["--exclude-severity"], 2), + ], +) +def test_exclude_severity_option(args: list[str], expected_exit_code: int) -> None: + result: Result = runner.invoke(app, ["simple", *args]) + assert result.exit_code == expected_exit_code From 1da622116d0fb8e1dfb4a8f0f7f53edfafb5367e Mon Sep 17 00:00:00 2001 From: Chico Venancio Date: Tue, 10 Dec 2024 09:04:25 -0300 Subject: [PATCH 136/137] feat: expose labels and annotations in the model (#369) We would like to use labels and annotations to configure the strategy to behave differently for some workloads. This will expose labels and annotations in `object_data.annotations`/ `object_data.labels` to be used in strategies. --- robusta_krr/core/integrations/kubernetes/__init__.py | 10 ++++++++++ robusta_krr/core/models/objects.py | 4 ++++ 2 files changed, 14 insertions(+) diff --git a/robusta_krr/core/integrations/kubernetes/__init__.py b/robusta_krr/core/integrations/kubernetes/__init__.py index 78419fdc..1ede3409 100644 --- a/robusta_krr/core/integrations/kubernetes/__init__.py +++ b/robusta_krr/core/integrations/kubernetes/__init__.py @@ -201,6 +201,14 @@ def __build_scannable_object( namespace = item.metadata.namespace kind = kind or item.__class__.__name__[2:] + labels = {} + annotations = {} + if item.metadata.labels: + labels = item.metadata.labels + + if item.metadata.annotations: + annotations = item.metadata.annotations + obj = K8sObjectData( cluster=self.cluster, namespace=namespace, @@ -209,6 +217,8 @@ def __build_scannable_object( container=container.name, allocations=ResourceAllocations.from_container(container), hpa=self.__hpa_list.get((namespace, kind, name)), + labels=labels, + annotations= annotations ) obj._api_resource = item return obj diff --git a/robusta_krr/core/models/objects.py b/robusta_krr/core/models/objects.py index e4b400d9..b80cf041 100644 --- a/robusta_krr/core/models/objects.py +++ b/robusta_krr/core/models/objects.py @@ -46,6 +46,8 @@ class K8sObjectData(pd.BaseModel): kind: KindLiteral allocations: ResourceAllocations warnings: set[PodWarning] = set() + labels: Optional[dict[str, str]] + annotations: Optional[dict[str, str]] _api_resource = pd.PrivateAttr(None) @@ -98,6 +100,8 @@ def split_into_batches(self, n: int) -> list[K8sObjectData]: namespace=self.namespace, kind=self.kind, allocations=self.allocations, + labels=self.labels, + annotations=self.annotations, ) for batch in batched(self.pods, n) ] From 5a0c464e7ae1d978f8a56dc9fac806f6466f6e93 Mon Sep 17 00:00:00 2001 From: Roi Glinik Date: Tue, 10 Dec 2024 16:12:59 +0200 Subject: [PATCH 137/137] new simple cpu limit strategy (#374) New KRR strategy. the default simple strategy suggests to set cpu limits to none always. Some users use multi-tenants clusters and would like a strategy where cpu limit can be set to specific cpu percentile. New simple_limit strategy will support this use case. supports cpu request and limit percentiles and use them for the cpu recommendations. --- robusta_krr/strategies/__init__.py | 3 +- robusta_krr/strategies/simple_limit.py | 190 +++++++++++++++++++++++++ 2 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 robusta_krr/strategies/simple_limit.py diff --git a/robusta_krr/strategies/__init__.py b/robusta_krr/strategies/__init__.py index 3409ebd8..8b9752b4 100644 --- a/robusta_krr/strategies/__init__.py +++ b/robusta_krr/strategies/__init__.py @@ -1 +1,2 @@ -from .simple import SimpleStrategy \ No newline at end of file +from .simple import SimpleStrategy +from .simple_limit import SimpleLimitStrategy \ No newline at end of file diff --git a/robusta_krr/strategies/simple_limit.py b/robusta_krr/strategies/simple_limit.py new file mode 100644 index 00000000..4d99ab0c --- /dev/null +++ b/robusta_krr/strategies/simple_limit.py @@ -0,0 +1,190 @@ +import textwrap +from datetime import timedelta + +import numpy as np +import pydantic as pd + +from robusta_krr.core.abstract.strategies import ( + BaseStrategy, + K8sObjectData, + MetricsPodData, + PodsTimeData, + ResourceRecommendation, + ResourceType, + RunResult, + StrategySettings, +) +from robusta_krr.core.integrations.prometheus.metrics import ( + CPUAmountLoader, + MaxMemoryLoader, + MemoryAmountLoader, + CPULoader, + PrometheusMetric, + MaxOOMKilledMemoryLoader, +) + + +class SimpleLimitStrategySettings(StrategySettings): + cpu_request: float = pd.Field(66, gt=0, le=100, description="The percentile to use for the CPU request.") + cpu_limit: float = pd.Field(96, gt=0, le=100, description="The percentile to use for the CPU limit.") + memory_buffer_percentage: float = pd.Field( + 15, gt=0, description="The percentage of added buffer to the peak memory usage for memory recommendation." + ) + points_required: int = pd.Field( + 100, ge=1, description="The number of data points required to make a recommendation for a resource." + ) + allow_hpa: bool = pd.Field( + False, + description="Whether to calculate recommendations even when there is an HPA scaler defined on that resource.", + ) + use_oomkill_data: bool = pd.Field( + False, + description="Whether to bump the memory when OOMKills are detected (experimental).", + ) + oom_memory_buffer_percentage: float = pd.Field( + 25, ge=0, description="What percentage to increase the memory when there are OOMKill events." + ) + + def calculate_memory_proposal(self, data: PodsTimeData, max_oomkill: float = 0) -> float: + data_ = [np.max(values[:, 1]) for values in data.values()] + if len(data_) == 0: + return float("NaN") + + return max( + np.max(data_) * (1 + self.memory_buffer_percentage / 100), + max_oomkill * (1 + self.oom_memory_buffer_percentage / 100), + ) + + def calculate_cpu_percentile(self, data: PodsTimeData, percentile: float) -> float: + if len(data) == 0: + return float("NaN") + + if len(data) > 1: + data_ = np.concatenate([values[:, 1] for values in data.values()]) + else: + data_ = list(data.values())[0][:, 1] + + return np.percentile(data_, percentile) + + def history_range_enough(self, history_range: tuple[timedelta, timedelta]) -> bool: + start, end = history_range + return (end - start) >= timedelta(hours=3) + + +class SimpleLimitStrategy(BaseStrategy[SimpleLimitStrategySettings]): + + display_name = "simple_limit" + rich_console = True + + @property + def metrics(self) -> list[type[PrometheusMetric]]: + metrics = [ + CPULoader, + MaxMemoryLoader, + CPUAmountLoader, + MemoryAmountLoader, + ] + + if self.settings.use_oomkill_data: + metrics.append(MaxOOMKilledMemoryLoader) + + return metrics + + @property + def description(self): + s = textwrap.dedent(f"""\ + CPU request: {self.settings.cpu_request}% percentile, limit: {self.settings.cpu_limit}% percentile + Memory request: max + {self.settings.memory_buffer_percentage}%, limit: max + {self.settings.memory_buffer_percentage}% + History: {self.settings.history_duration} hours + Step: {self.settings.timeframe_duration} minutes + + All parameters can be customized. For example: `krr simple_limit --cpu_request=66 --cpu_limit=96 --memory_buffer_percentage=15 --history_duration=24 --timeframe_duration=0.5` + """) + + if not self.settings.allow_hpa: + s += "\n" + textwrap.dedent(f"""\ + This strategy does not work with objects with HPA defined (Horizontal Pod Autoscaler). + If HPA is defined for CPU or Memory, the strategy will return "?" for that resource. + You can override this behaviour by passing the --allow-hpa flag + """) + + s += "\nLearn more: [underline]https://github.com/robusta-dev/krr#algorithm[/underline]" + return s + + def __calculate_cpu_proposal( + self, history_data: MetricsPodData, object_data: K8sObjectData + ) -> ResourceRecommendation: + data = history_data["CPULoader"] + + if len(data) == 0: + return ResourceRecommendation.undefined(info="No data") + + # NOTE: metrics for each pod are returned as list[values] where values is [timestamp, value] + # As CPUAmountLoader returns only the last value (1 point), [0, 1] is used to get the value + # So each pod is string with pod name, and values is numpy array of shape (N, 2) + data_count = {pod: values[0, 1] for pod, values in history_data["CPUAmountLoader"].items()} + total_points_count = sum(data_count.values()) + + if total_points_count < self.settings.points_required: + return ResourceRecommendation.undefined(info="Not enough data") + + if ( + object_data.hpa is not None + and object_data.hpa.target_cpu_utilization_percentage is not None + and not self.settings.allow_hpa + ): + return ResourceRecommendation.undefined(info="HPA detected") + + cpu_request = self.settings.calculate_cpu_percentile(data, self.settings.cpu_request) + cpu_limit = self.settings.calculate_cpu_percentile(data, self.settings.cpu_limit) + return ResourceRecommendation(request=cpu_request, limit=cpu_limit) + + def __calculate_memory_proposal( + self, history_data: MetricsPodData, object_data: K8sObjectData + ) -> ResourceRecommendation: + data = history_data["MaxMemoryLoader"] + + oomkill_detected = False + + if self.settings.use_oomkill_data: + max_oomkill_data = history_data["MaxOOMKilledMemoryLoader"] + # NOTE: metrics for each pod are returned as list[values] where values is [timestamp, value] + # As MaxOOMKilledMemoryLoader returns only the last value (1 point), [0, 1] is used to get the value + # So each value is numpy array of shape (N, 2) + max_oomkill_value = ( + np.max([values[0, 1] for values in max_oomkill_data.values()]) if len(max_oomkill_data) > 0 else 0 + ) + if max_oomkill_value != 0: + oomkill_detected = True + else: + max_oomkill_value = 0 + + if len(data) == 0: + return ResourceRecommendation.undefined(info="No data") + + # NOTE: metrics for each pod are returned as list[values] where values is [timestamp, value] + # As MemoryAmountLoader returns only the last value (1 point), [0, 1] is used to get the value + # So each pod is string with pod name, and values is numpy array of shape (N, 2) + data_count = {pod: values[0, 1] for pod, values in history_data["MemoryAmountLoader"].items()} + total_points_count = sum(data_count.values()) + + if total_points_count < self.settings.points_required: + return ResourceRecommendation.undefined(info="Not enough data") + + if ( + object_data.hpa is not None + and object_data.hpa.target_memory_utilization_percentage is not None + and not self.settings.allow_hpa + ): + return ResourceRecommendation.undefined(info="HPA detected") + + memory_usage = self.settings.calculate_memory_proposal(data, max_oomkill_value) + return ResourceRecommendation( + request=memory_usage, limit=memory_usage, info="OOMKill detected" if oomkill_detected else None + ) + + def run(self, history_data: MetricsPodData, object_data: K8sObjectData) -> RunResult: + return { + ResourceType.CPU: self.__calculate_cpu_proposal(history_data, object_data), + ResourceType.Memory: self.__calculate_memory_proposal(history_data, object_data), + }