├── .flake8 ├── .gitignore ├── LICENSE ├── README.md ├── beholder ├── __init__.py ├── __main__.py ├── common │ ├── __init__.py │ └── formatter.py ├── facade │ ├── __init__.py │ └── dashboard_facade.py ├── service │ ├── __init__.py │ ├── cpu_info_service.py │ ├── info_service.py │ ├── memory_info_service.py │ ├── network_info_service.py │ ├── service_info_service.py │ ├── storage_info_service.py │ ├── system_info_service.py │ └── temperature_info_service.py ├── static │ ├── css │ │ ├── beholder.css │ │ ├── bootstrap.dark.min.css │ │ └── bootstrap.light.min.css │ └── js │ │ ├── beholder.js │ │ ├── bootstrap.bundle.min.js │ │ └── masonry.min.js └── templates │ ├── dashboard.html │ ├── index.html │ └── widget │ ├── cpu_info.html │ ├── mem_info.html │ ├── network_info.html │ ├── services_info.html │ ├── storage_info.html │ ├── sys_info.html │ └── temperature_info.html ├── images ├── beholder_dark_screenshot.png ├── beholder_light_screenshot.png └── paypal_button.png ├── poetry.lock ├── pyproject.toml └── tests ├── __init__.py ├── common ├── __init__.py └── test_formatter.py ├── facade ├── __init__.py └── test_dashboard_facade.py └── service ├── __init__.py ├── test_cpu_info_service.py ├── test_memory_info_service.py ├── test_network_info_service.py ├── test_service_info.py ├── test_storage_info_service.py ├── test_system_info_service.py └── test_temperature_info_service.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | 3 | max-line-length=120 4 | 5 | color=auto 6 | 7 | count=True 8 | 9 | statistics=True 10 | 11 | exclude= 12 | # Ignoring git repository. 13 | .git, 14 | 15 | # Ignoring pycache. 16 | __pycache__, 17 | 18 | # Ignoring IntelliJ idea settings. 19 | .idea, 20 | 21 | # Ignoring integration tests. 22 | features, 23 | 24 | # Ignoring fixtures. 25 | tests/fixtures 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/*.pyc 2 | **/__pycache__ 3 | 4 | **/*.iml 5 | **/.idea 6 | **/.Makefile.swp 7 | **/*.swp 8 | 9 | **/coverage 10 | **/.coverage 11 | **/test_report.* 12 | **/.pytest_cache 13 | **/lint 14 | 15 | **/*.pem 16 | 17 | **/.vscode 18 | **/.venv 19 | venv 20 | **/dist 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 NullPy Consulting Ltd (Ireland) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Beholder 2 | 3 | ## 1. Introduction. 4 | 5 | **Beholder** is a simple, minimalist and responsive dashboard with basic system information fully implemented in Python 6 | and intended to be used on low resource nodes (e.g.: single board computers) running any Linux distro. 7 | The highlighted features are: 8 | 9 | * Node basic information (hostname, kernel version, locale, timezone, up time, etc.) 10 | * CPU summary and load. 11 | * Physical and SWAP memory usage. 12 | * Storage details and usage. 13 | * Customizable service watch list. 14 | * Network interfaces summary and status. 15 | * Sensors temperature with temperature bands (when available). 16 | * Automatic support to light and dark themes. 17 | 18 | 19 | 20 | 21 | 24 | 27 | 28 | 29 | 32 | 35 | 36 | 37 |
22 | 23 | 25 | 26 |
30 | Dark Mode 31 | 33 | Light Mode 34 |
38 | 39 | ## 2. Usage. 40 | 41 | The following sections will explain how to install and use the application. 42 | 43 | ### 2.1. Cloning and building the application locally. 44 | 45 | In order to clone the repository, build the application locally and install it, your local environment will required: 46 | 47 | | Component | Version | Why do you need it | 48 | |-----------|:-------:|--------------------| 49 | | Python | \>= 3.8 | Python runtime is required to run the application locally. | 50 | | Poetry | \>= 1.4.2 | Poetry manages the project dependency and is used to test and build the application. | 51 | | PIP | >= 22.3.1 | PIP will be used to install the dependencies. 52 | 53 | The installation of the aforementioned components are out of the scope of this documentation and guides to do it can 54 | easily be found on-line. No special settings are required. 55 | 56 | After have the local environment installed and configured, you can clone this repository with the following command: 57 | 58 | ```bash 59 | git clone git@github.com:NullPyDev/beholder.git 60 | ``` 61 | 62 | From the ```/beholder``` root path, install all the project dependencies with the following command: 63 | 64 | ```bash 65 | poetry install 66 | ``` 67 | 68 | Run the test for a quick sanity check: 69 | 70 | ```bash 71 | poetry run task test 72 | ``` 73 | 74 | Build the library locally: 75 | 76 | ```bash 77 | poetry build 78 | ``` 79 | 80 | Install it using **PIP**: 81 | 82 | ```bash 83 | pip install dist/beholder-0.1.0-py3-none-any.whl 84 | ``` 85 | 86 | Finally, you can start the application with the following command: 87 | 88 | ```bash 89 | python -m beholder 90 | ``` 91 | 92 | You can test the application using the address [http://127.0.0.1:2312](http://127.0.0.1:2312). 93 | 94 | ### 2.2 Configuring the application. 95 | 96 | All the configurations are done through environment variables, which are: 97 | 98 | | Variable | Description | Default Value | 99 | |----------|---------------------------------------------------------------------------------------------------------------------|:-------------:| 100 | | BEHOLDER_HOST | Ip address of the interface used to listen for incoming connections. | 0.0.0.0 | 101 | | BEHOLDER_PORT | TCP port used to listen for incoming connections. | 2312 | 102 | | BEHOLDER_SERVICES_WATCHLIST | Commad separated list with the names of services to have its status watched by the application. | None | 103 | | BEHOLDER_TEMPERATURE_SCALE | Temperature scale ([C]elsius or [F]ahrenheit) to be used to display the temperatures probed from available sensors. | C | 104 | 105 | For example, lets start the application listening on port **8080**, watch the services ```crond```, ```cupsd``` and 106 | ```sshd```, and display the temperatures using the Fahrenheit scale. We can use the following command to achieve that: 107 | 108 | ```bash 109 | BEHOLDER_PORT=8080 BEHOLDER_SERVICES_WATCHLIST="crond,cupsd,ssh" BEHOLDER_TEMPERATURE_SCALE="F" python -m beholder 110 | ``` 111 | 112 | ## 3. Development. 113 | 114 | The development environment requires the same components from the step ```2.1 Cloning and building the application locally```. 115 | [Poetry](https://python-poetry.org) is used to manage the project dependencies and automate common task during the development life cycle. 116 | 117 | ### 3.1 Installing all the required dependencies. 118 | 119 | To install all the required dependencies to run and develop the application locally, we can use the following command: 120 | 121 | ```bash 122 | poetry install 123 | ``` 124 | 125 | It will create a local virtual environment and install all the required dependencies in the appropriated version. 126 | 127 | ### 3.2 Running the application on development mode. 128 | 129 | In development mode, the application automatically reloads any changes made in the source (python, html, css, js). To 130 | run the application in development mode we can use the following command: 131 | 132 | ```bash 133 | poetry run task beholder 134 | ``` 135 | 136 | Or alternatively... 137 | 138 | ```bash 139 | poetry run flask --debug --app beholder/__main__.py run 140 | ``` 141 | 142 | It will start the application locally listening on the TCP port ```5000```. 143 | 144 | ### 3.3 Run unit tests. 145 | 146 | The application is covered by unit tests, and we have the **commitment to keep its coverage always over 90%**. To run 147 | all the unit tests, we can use the following command: 148 | 149 | ```bash 150 | poetry run task test 151 | ``` 152 | 153 | Or alternatively... 154 | 155 | ```bash 156 | poetry run coverage run --source=beholder -m pytest tests/ && coverage html --directory coverage/ 157 | ``` 158 | 159 | After execute all tests successfully, a coverage report will be generated on ```/coverage/index.html```. The coverage 160 | report is not versioned. 161 | 162 | ### 3.4 Checking code standards. 163 | 164 | This project uses [Flake8](https://flake8.pycqa.org/en/latest/) and several of its plugins to statically check its 165 | adherence of the code to the code standards, and [Black](https://github.com/psf/black). In order to format and check the 166 | code, we can use the following commands: 167 | 168 | First we execute black to automatically format all the files: 169 | 170 | ```bash 171 | poetry run task format 172 | ``` 173 | 174 | Subsequently, we run ```Flake8``` to check any code standard violation: 175 | 176 | ```bash 177 | poetry run task check 178 | ``` 179 | 180 | ```Flake8``` will generate a violation report on ```/lint/index.html```. Although some violations are false positive, we 181 | should always double-check if there is an alternative or workaround to improve the code. 182 | 183 | ### 3.5 Build the application locally. 184 | 185 | As described in the section ```2.1. Cloning and building the application locally.``` of this document, we can build 186 | the application using the following command: 187 | 188 | ```bash 189 | poetry build 190 | ``` 191 | 192 | ## 4. Roadmap, Collaboration and Support. 193 | 194 | Although our intention is to keep this monitor as simple as possible, we plan to add the following features as soon as 195 | we can: 196 | 197 | * Report of current running processes, including its CPU and memory usage. 198 | * Report with all the open ports and established connections, identified by process. 199 | * Report with all open files, identified by process. 200 | 201 | If you want to collaborate, just fork the project locally, do the changes and open a pull request that I will review as 202 | soon as possible. 203 | 204 | If you found any bug or have any questions, please, use the [Issues](https://github.com/NullPyDev/beholder/issues) in this 205 | repository. 206 | 207 | If you want to support me to keep maintaining this project, consider to pay me a coffee using the following button: 208 | 209 |
210 | 211 | Donate using PayPal. 212 | 213 |
214 | 215 | **Seriously, € 1,00 would help me a lot to keep supporting this and future projects** 216 | 217 | 218 | ## 5. License. 219 | 220 | MIT License 221 | 222 | Copyright (c) 2023 NullPy Consulting Ltd (Ireland) 223 | 224 | Permission is hereby granted, free of charge, to any person obtaining a copy 225 | of this software and associated documentation files (the "Software"), to deal 226 | in the Software without restriction, including without limitation the rights 227 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 228 | copies of the Software, and to permit persons to whom the Software is 229 | furnished to do so, subject to the following conditions: 230 | 231 | The above copyright notice and this permission notice shall be included in all 232 | copies or substantial portions of the Software. 233 | 234 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 235 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 236 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 237 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 238 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 239 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 240 | SOFTWARE. 241 | -------------------------------------------------------------------------------- /beholder/__init__.py: -------------------------------------------------------------------------------- 1 | BEHOLDER_VERSION = "0.1.2" 2 | -------------------------------------------------------------------------------- /beholder/__main__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from flask import Flask, render_template, request 4 | from waitress import serve 5 | 6 | from beholder.facade.dashboard_facade import DashboardFacade 7 | 8 | # Flask app instance. 9 | beholder_app: Flask = Flask(__name__) 10 | 11 | # Used to retrieve all the data required to compose the node dashboard. 12 | facade: DashboardFacade = DashboardFacade() 13 | 14 | 15 | @beholder_app.route("/", methods=["GET"]) 16 | def dashboard(): 17 | """Display the system dashboard. This method accepts an optional query parameter called "template". When omitted, 18 | "index" is adopted as default value, making the response to render then full index page. The other possible value 19 | is "dashboard", which is used when refreshing the dashboard data through Ajax requests and will only render the 20 | dashboard content. 21 | 22 | This method actually delegates the task to gather the system summaries to the :class:`DashboardFacade` class. 23 | 24 | :return: Rendered templated, filled with the summaries retrieved from the available info services. 25 | """ 26 | template_name: str = f"{request.args.get('template', 'index')}.html" 27 | summaries: dict[str, any] = facade.load_summaries() 28 | return render_template(template_name, summaries=summaries) 29 | 30 | 31 | if __name__ == "__main__": 32 | # Application entry point when executed as a stand-alone application. Meant for production environment. 33 | interface: str = os.environ.get("BEHOLDER_HOST", "0.0.0.0") 34 | port: int = int(os.environ.get("BEHOLDER_PORT", "2312")) 35 | serve(beholder_app, host=interface, port=port) 36 | -------------------------------------------------------------------------------- /beholder/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NullPyDev/beholder/d2fe1ddaec90ce5ac57564204ce25d703861e7a1/beholder/common/__init__.py -------------------------------------------------------------------------------- /beholder/common/formatter.py: -------------------------------------------------------------------------------- 1 | def size_formatter(size, suffix="B") -> str: 2 | """Format the received value into a proper human-readable data size information. 3 | 4 | :param size: Size to be formatted, expressed in bytes. 5 | :param suffix: Desired scale suffix. Default: "B". 6 | :return: Formatted data size, with proper suffix and 2 decimal places. 7 | """ 8 | factor = 1_024 9 | for unit in ["", "K", "M", "G", "T"]: 10 | if size < factor: 11 | return f"{size:,.2f} {unit}{suffix}" 12 | size /= factor 13 | 14 | 15 | def frequency_formatter(frequency): 16 | """Format the received frequency expressed in Mhz into a proper human-readable frequency information. 17 | 18 | :param frequency: Frequency to be formatted, expressed in Mhz. 19 | :return: Formatted frequency, with proper suffix and 2 decimal places. 20 | """ 21 | factor = 1_000 22 | for unit in ["Mhz", "Ghz"]: 23 | if frequency < factor: 24 | return f"{frequency:.2f} {unit}" 25 | frequency /= factor 26 | -------------------------------------------------------------------------------- /beholder/facade/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NullPyDev/beholder/d2fe1ddaec90ce5ac57564204ce25d703861e7a1/beholder/facade/__init__.py -------------------------------------------------------------------------------- /beholder/facade/dashboard_facade.py: -------------------------------------------------------------------------------- 1 | from ..service.cpu_info_service import CPUInfoService 2 | from ..service.info_service import InfoService 3 | from ..service.memory_info_service import MemoryInfoService 4 | from ..service.network_info_service import NetworkInfoService 5 | from ..service.service_info_service import ServiceInfoService 6 | from ..service.storage_info_service import StorageInfoService 7 | from ..service.system_info_service import SystemInfoService 8 | from ..service.temperature_info_service import TemperatureInfoService 9 | 10 | 11 | class DashboardFacade: 12 | """Provide a single point to retrieve and gather all the required information to compose the system dashboard.""" 13 | 14 | # Info service used to retrieve basic node information. 15 | __sys_info_service: InfoService = None 16 | 17 | # Info service used to retrieve CPU information and usage statistics. 18 | __cpu_info_service: InfoService = None 19 | 20 | # Info service used to retrieve memory information and usage statistics. 21 | __mem_info_service: InfoService = None 22 | 23 | # Info service used to retrieve storage information and usage statistics. 24 | __storage_info_service: InfoService = None 25 | 26 | # Info service used to retrieve watched services state. 27 | __services_info_service: InfoService = None 28 | 29 | # Info service used to retrieve network information, statistics and state. 30 | __network_info_service: InfoService = None 31 | 32 | # Info service used to retrieve the temperature readings from all the available sensors in the system. 33 | __temperature_info_service: InfoService = None 34 | 35 | def __init__(self): 36 | """Default class constructor. Instantiate all the required infor services used to gather the dashboard 37 | information. 38 | """ 39 | self.__sys_info_service = SystemInfoService() 40 | self.__cpu_info_service = CPUInfoService() 41 | self.__mem_info_service = MemoryInfoService() 42 | self.__storage_info_service = StorageInfoService() 43 | self.__services_info_service = ServiceInfoService() 44 | self.__network_info_service = NetworkInfoService() 45 | self.__temperature_info_service = TemperatureInfoService() 46 | 47 | def load_summaries(self) -> dict[str, any]: 48 | """Retrieve and return the following system summaries: 49 | 50 | * sys_info: Basic system information retrieved using the :class:`SystemInfoService`. 51 | * cpu_info: CPU information and statistics, retrieved using the :class:`CPUInfoService`. 52 | * mem_info: Memory information and statistics, retrieved using the :class:`MemoryInfoService`. 53 | * storage_info: Storage information and statistics, retrieved using the :class:`StorageInfoService`. 54 | * services_info: Watched services statuses, retrieved using the :class:`ServiceInfoService`. 55 | * network_info: Network interfaces information and statistics, retrieved using the :class:`NetworkInfoService`. 56 | * temperature_info: Temperature readings retrieved from available sensors, retrieved using the 57 | :class:`TemperatureInfoService` 58 | 59 | :return: A dictionary filled with the aforementioned summaries. Please refer to each info service documentation 60 | for a proper description of its structure. 61 | """ 62 | summaries: dict[str, any] = { 63 | "sys_info": self.__sys_info_service.get_info(), 64 | "cpu_info": self.__cpu_info_service.get_info(), 65 | "mem_info": self.__mem_info_service.get_info(), 66 | "storage_info": self.__storage_info_service.get_info(), 67 | "services_info": self.__services_info_service.get_info(), 68 | "network_info": self.__network_info_service.get_info(), 69 | "temperature_info": self.__temperature_info_service.get_info(), 70 | } 71 | return summaries 72 | -------------------------------------------------------------------------------- /beholder/service/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NullPyDev/beholder/d2fe1ddaec90ce5ac57564204ce25d703861e7a1/beholder/service/__init__.py -------------------------------------------------------------------------------- /beholder/service/cpu_info_service.py: -------------------------------------------------------------------------------- 1 | import psutil 2 | 3 | from ..common.formatter import frequency_formatter 4 | from ..service.info_service import InfoService 5 | 6 | 7 | class CPUInfoService(InfoService): 8 | """Info service implementation specialized to retrieve details about the system CPU and its usage.""" 9 | 10 | # Retrieved CPU details. 11 | __info: dict[str, any] = None 12 | 13 | def get_info(self) -> dict[str, any]: 14 | """Retrieve, format and return details about the installed CPU and its current load. The returned dictionary 15 | contains the following entries: 16 | 17 | * physical_cores: Number of available physical cores. Retrieved just once. 18 | * total_cores: Number of available physical and local cores. Retrieved just once. 19 | * max_frequency: Maximum CPU frequency. Retrieved just once. 20 | * current_frequency: Current CPU frequency. 21 | * pct_current_frequency: Percentage of the current CPU frequency in relation to the maximum CPU frequency. 22 | * total_load: Total loaded applied to the CPU expressed as percentage. 23 | 24 | :return: A dictionary containing the aforementioned entries. 25 | """ 26 | if not self.__info: 27 | self.__info = { 28 | "physical_cores": psutil.cpu_count(logical=False), 29 | "total_cores": psutil.cpu_count(logical=True), 30 | "max_frequency": frequency_formatter(psutil.cpu_freq().max), 31 | } 32 | self.__info["current_frequency"] = frequency_formatter(psutil.cpu_freq().current) 33 | self.__info["pct_current_frequency"] = (psutil.cpu_freq().current / psutil.cpu_freq().max) * 100 34 | self.__info["total_load"] = psutil.cpu_percent() 35 | return self.__info 36 | -------------------------------------------------------------------------------- /beholder/service/info_service.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class InfoService(ABC): 5 | """Common interface shared among any class that provides system information and statistics for a certain aspect of 6 | the system. 7 | """ 8 | 9 | @abstractmethod 10 | def get_info(self) -> dict[str, any]: 11 | """Retrieves the desired information and statistics for a certain aspect of the system. 12 | 13 | :return: Information and statistics gathered by the info service. 14 | """ 15 | -------------------------------------------------------------------------------- /beholder/service/memory_info_service.py: -------------------------------------------------------------------------------- 1 | import psutil 2 | 3 | from beholder.common.formatter import size_formatter 4 | from ..service.info_service import InfoService 5 | 6 | 7 | class MemoryInfoService(InfoService): 8 | """Infor service implementation specialized to retrieve details about the available memory and its usage.""" 9 | 10 | # Retrieved memory details. 11 | __info: dict[str, any] = None 12 | 13 | def get_info(self) -> dict[str, any]: 14 | """Retrieve, formant and return details about the available memory and its current usage. The returned 15 | dictionary contains the following entries: 16 | 17 | * physical_memory: Dictionary containing the total available and the percentual used of the physical memory. 18 | * swap_memory: Dictionary containing the total available and the percentual used of the SWAP memory. 19 | 20 | :return: Dictionary containing the aforementioned entries. 21 | """ 22 | if not self.__info: 23 | self.__info = { 24 | "physical_memory": {"total_available": size_formatter(psutil.virtual_memory().total)}, 25 | "swap_memory": {"total_available": size_formatter(psutil.swap_memory().total)}, 26 | } 27 | 28 | self.update_pct_used_for_memory(self.__info["physical_memory"], psutil.virtual_memory()) 29 | self.update_pct_used_for_memory(self.__info["swap_memory"], psutil.swap_memory()) 30 | return self.__info 31 | 32 | @staticmethod 33 | def update_pct_used_for_memory(memory_summary: dict[str, any], memory_info): 34 | memory_summary["pct_used"] = memory_info.percent 35 | memory_summary["total_free"] = size_formatter(memory_info.free) 36 | -------------------------------------------------------------------------------- /beholder/service/network_info_service.py: -------------------------------------------------------------------------------- 1 | import psutil 2 | from psutil._common import snicaddr, snicstats 3 | 4 | from ..service.info_service import InfoService 5 | 6 | 7 | class NetworkInfoService(InfoService): 8 | """Info service implementation specialized to retrieved details and statistics about the network interfaces 9 | available in the system. 10 | """ 11 | 12 | def get_info(self) -> dict[str, any]: 13 | """Retrieve, format and return details about the available network interfaces in the system. The returned 14 | dictionary contains the following entries: 15 | 16 | * interface_name: Dictionary entry key. 17 | * active: A boolean flag indicating if the interface is active (True) or not (False). 18 | * addresses: A list of dictionary containing the addresses assigned to the interface, which has the 19 | following entries: 20 | * address: Address assigned to the interface. 21 | * address_type: Type of the address assigned to the interface (E.g. IP, IPv6, MAC). 22 | 23 | The loopback interface is intentionally removed from the list for the simplicity sake. 24 | 25 | :return: A dictionary containing the aforementioned entries. 26 | """ 27 | if_summaries: dict[str, any] = {} 28 | if_addresses: dict[str, list[snicaddr]] = psutil.net_if_addrs() 29 | if_statuses: dict[str, snicstats] = psutil.net_if_stats() 30 | 31 | for interface, addresses in if_addresses.items(): 32 | if interface != "lo": 33 | if_summaries[interface] = { 34 | "active": if_statuses[interface][0], 35 | "addresses": self.load_interface_addresses(addresses), 36 | } 37 | 38 | return if_summaries 39 | 40 | @staticmethod 41 | def load_interface_addresses(addresses: list[snicaddr]) -> list[dict[str, any]]: 42 | """Retrieve the addresses assigned to the network interface. Each dictionary in the list contains the following 43 | entries: 44 | 45 | * address: Address assigned to the interface. 46 | * address_type: Type of the address assigned to the interface (E.g. IP, IPv6, MAC). 47 | 48 | :param addresses: Addresses assigned to the network interface. 49 | :return: A list of dictionaries containing the aforementioned entries. 50 | """ 51 | address_summaries: list[dict[str, any]] = [] 52 | for address in addresses: 53 | address_summaries.append({"address": address.address.split("%")[0], "address_type": address.family.name}) 54 | return address_summaries 55 | -------------------------------------------------------------------------------- /beholder/service/service_info_service.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import psutil 4 | 5 | from ..service.info_service import InfoService 6 | 7 | 8 | class ServiceInfoService(InfoService): 9 | """Info service implementation specialized to retrieve the status of services in the system.""" 10 | 11 | # List of the services to be watched by the application. 12 | __service_watch_list: dict[str, bool] = {} 13 | 14 | def __init__(self): 15 | """Default class constructor. Initializes the service watch list based on the BEHOLDER_SERVICES_WATCHLIST 16 | environment variable, which should contains a coma separated list of all services to be watched by the 17 | application. If the environment variable is not available, an empty string is used by default. 18 | """ 19 | services: list[str] = os.environ.get("BEHOLDER_SERVICES_WATCHLIST", "").strip().split(",") 20 | if len(services) > 0 and services[0] != "": 21 | self.__service_watch_list = {service: False for service in services} 22 | 23 | def get_info(self) -> dict[str, any]: 24 | """Retrieve, format and return the status of all the watched services. The returned dictionary has the service 25 | name and the entry key, and the service status as the entry value. 26 | 27 | :return: A dictionary with the aforementioned entries. 28 | """ 29 | services_statuses: dict[str, any] = {} 30 | 31 | if len(self.__service_watch_list) > 0: 32 | services_statuses = self.__service_watch_list.copy() 33 | for process in psutil.process_iter(): 34 | with process.oneshot(): 35 | if process.name() in services_statuses: 36 | services_statuses[process.name()] = process.is_running() 37 | 38 | return services_statuses 39 | -------------------------------------------------------------------------------- /beholder/service/storage_info_service.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from logging import Logger 3 | 4 | import psutil 5 | 6 | from ..common.formatter import size_formatter 7 | from ..service.info_service import InfoService 8 | 9 | 10 | class StorageInfoService(InfoService): 11 | """Info service implementation specialized to retrieve the details and usage statistics from all the storage 12 | devices available in the node. 13 | """ 14 | 15 | # Local logger. 16 | __logger: Logger = None 17 | 18 | def __init__(self): 19 | """Default class constructor.""" 20 | self.__logger = logging.getLogger(__name__) 21 | 22 | def get_info(self) -> dict[str, any]: 23 | """Retrieve, format and return details about all available storage devices and its usage. The returned 24 | dictionary contains the following entries: 25 | 26 | * device: Entry key. 27 | * total_size: Disk capacity. 28 | * total_free: Total space available in the disk. 29 | * pct_used: Used space expressed in percentage. 30 | 31 | :return: A dictionary containing the aforementioned entries. 32 | """ 33 | disks: dict[str, any] = {} 34 | for partition in psutil.disk_partitions(): 35 | if partition.device not in disks: 36 | try: 37 | usage = psutil.disk_usage(partition.mountpoint) 38 | 39 | except PermissionError: 40 | self.__logger.warning( 41 | "It was not possible to retrieve usage statistics for the device '%s' " 42 | "due the following error:", 43 | partition.device, 44 | exc_info=True, 45 | ) 46 | continue 47 | 48 | disks[partition.device] = { 49 | "total_size": size_formatter(usage.total), 50 | "total_free": size_formatter(usage.free), 51 | "pct_used": usage.percent, 52 | } 53 | 54 | return disks 55 | -------------------------------------------------------------------------------- /beholder/service/system_info_service.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import locale 3 | import platform 4 | 5 | import psutil 6 | 7 | from beholder import BEHOLDER_VERSION 8 | from tzlocal import get_localzone 9 | 10 | from ..service.info_service import InfoService 11 | 12 | 13 | class SystemInfoService(InfoService): 14 | """Info service implementation specialized to retrieve the basic details about the node.""" 15 | 16 | # Details about kernel, os and its versions. 17 | __uname: platform.uname_result = None 18 | 19 | # Cache of immutable system details. 20 | __info: dict[str, any] = None 21 | 22 | def __init__(self): 23 | """Default class constructor. Initialized the "uname" attribute.""" 24 | self.__uname = platform.uname() 25 | 26 | def get_info(self) -> dict[str, any]: 27 | """ 28 | Retrieve, format and return the basic system details, The returned dictionary contains the following entries: 29 | 30 | * system: Operation system. 31 | * host_name: Name of the host. 32 | * release: Kernel version. 33 | * architecture: Node architecture (e.g.: x86, x86_64). 34 | * beholder_version: Version of Beholder service. 35 | * boot_time: Timestamp of when the system started. 36 | * locale: Node's configured locale. 37 | * encoding: Node's configured encoding. 38 | * time_zone: Node's configured timezone. 39 | * current_time: Node's current date and time. 40 | * up_time: Time passed since the node started. 41 | 42 | :return: A dictionary containing the aforementioned entries. 43 | """ 44 | if not self.__info: 45 | self.__info = { 46 | "system": self.__uname.system, 47 | "host_name": self.__uname.node, 48 | "release": self.__uname.release, 49 | "architecture": self.__uname.machine, 50 | "beholder_version": BEHOLDER_VERSION, 51 | "boot_time": datetime.datetime.fromtimestamp(psutil.boot_time()), 52 | } 53 | 54 | self.__info["locale"] = locale.getlocale()[0] 55 | self.__info["encoding"] = locale.getlocale()[1] 56 | self.__info["time_zone"] = get_localzone().key 57 | self.__info["current_time"] = datetime.datetime.now().strftime("%d/%m/%Y - %H:%M:%S") 58 | self.__info["up_time"] = self.get_uptime() 59 | return self.__info 60 | 61 | def get_uptime(self) -> str: 62 | """Format the up-time value, showing by default, days, hours and minutes since the host started. 63 | 64 | :return: Up-time formatted as aforementioned. 65 | """ 66 | now: datetime.datetime = datetime.datetime.utcnow() 67 | delta: datetime.timedelta = now - self.__info["boot_time"] 68 | 69 | days: int = delta.days 70 | hours, rem = divmod(delta.seconds, 3_600) 71 | minutes, seconds = divmod(rem, 60) 72 | 73 | return f"{days} day(s), {hours} hour(s) and {minutes} minute(s)" 74 | -------------------------------------------------------------------------------- /beholder/service/temperature_info_service.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import psutil 4 | from psutil._common import shwtemp 5 | 6 | from ..service.info_service import InfoService 7 | 8 | 9 | class TemperatureInfoService(InfoService): 10 | """Info service implementation specialized to retrieve the temperature reading of all available sensors.""" 11 | 12 | # Temperature scale to be used (C or F) 13 | __temp_scale: str = None 14 | 15 | def __init__(self): 16 | """Default class constructor. Initializes the property with the temperature scale to be used, by reading from 17 | the BEHOLDER_TEMPERATURE_SCALE environment variable. If this variable is not available, Celsius (C) is used by 18 | default. 19 | """ 20 | self.__temp_scale = os.environ.get("BEHOLDER_TEMPERATURE_SCALE", "C").strip().upper() 21 | 22 | def get_info(self) -> dict[str, any]: 23 | """Retrieve, format and return de temperature readings for each available sensor. The returned dictionary 24 | contains the following entries: 25 | 26 | * sensor_name: Entry key. 27 | * sensor_label: Label assigned to the sensor. 28 | * scale: Temperature scale used. 29 | * temperature: Current temperature reading. 30 | * low_band: Final temperature of the thermal lower band. It is calculated as 50% of the high temperature. 31 | * medium_band: Final temperature of the thermal medium band. It is calculated as 75% of the high temperature. 32 | * high_band: Final temperature of the thermal high band. It is the high temperature. 33 | 34 | :return: A dictionary containing the aforementioned entries. 35 | """ 36 | sensor_readings: dict[str, any] = {} 37 | use_fahrenheit: bool = self.__temp_scale == "F" 38 | temp_readings: dict[str, list[shwtemp]] = psutil.sensors_temperatures(fahrenheit=use_fahrenheit) 39 | 40 | for sensor_name, temperatures in temp_readings.items(): 41 | readings: list[dict[str, any]] = [] 42 | 43 | for temperature in temperatures: 44 | high = 0.0 45 | 46 | # Workaround for some sensor with wrong temperature values for high. 47 | if temperature.high and self.__temp_scale == "C": 48 | if temperature.high > 1_000: 49 | high = temperature.high / 1_000 50 | elif temperature.high > 100: 51 | high = temperature.high / 100 52 | else: 53 | high = temperature.high 54 | 55 | readings.append( 56 | { 57 | "sensor_label": temperature.label, 58 | "scale": self.__temp_scale, 59 | "temperature": temperature.current, 60 | "low_band": high * 0.5 if temperature.high else 0.0, 61 | "medium_band": high * 0.75 if temperature.high else 0.0, 62 | "high_band": high if temperature.high else 0.0, 63 | } 64 | ) 65 | 66 | sensor_readings[sensor_name] = readings 67 | 68 | return sensor_readings 69 | -------------------------------------------------------------------------------- /beholder/static/css/beholder.css: -------------------------------------------------------------------------------- 1 | @import url("/static/css/bootstrap.dark.min.css"); 2 | @import url("/static/css/bootstrap.light.min.css"); 3 | 4 | @media (prefers-color-scheme: dark) { 5 | .navbar-brand { 6 | color: #fff !important; 7 | } 8 | 9 | .navbar-text { 10 | color: #c9c9c9 !important; 11 | } 12 | 13 | .card { 14 | border-radius: 7px !important; 15 | } 16 | 17 | .progress-bar { 18 | color: #fff !important; 19 | font-weight: bolder; 20 | } 21 | } 22 | 23 | .content { 24 | padding-top: 60px; 25 | } 26 | 27 | .navbar { 28 | border: none !important; 29 | } 30 | 31 | .navbar-brand { 32 | border: none !important; 33 | } -------------------------------------------------------------------------------- /beholder/static/js/beholder.js: -------------------------------------------------------------------------------- 1 | let httpRequest; 2 | let dashboardContentElement = document.getElementById("dashboard"); 3 | let refreshIntervalId = window.setInterval(refreshDashboard, 10000); 4 | 5 | function refreshDashboard() { 6 | httpRequest = new XMLHttpRequest(); 7 | if (!httpRequest) { 8 | console.log("It was not possible to prepare a request to refresh the dashboard content."); 9 | return; 10 | } 11 | 12 | httpRequest.onreadystatechange = updateDashboardContent; 13 | httpRequest.open("GET", "/?template=dashboard", true); 14 | httpRequest.send(); 15 | } 16 | 17 | function updateDashboardContent() { 18 | if (httpRequest.readyState === XMLHttpRequest.DONE) { 19 | if (httpRequest.status === 200) { 20 | dashboardContentElement.innerHTML = httpRequest.responseText; 21 | new Masonry(dashboardContentElement, { itemSelector: ".widget", percentPosition: true }); 22 | } else { 23 | console.log("It was not possible to update the dashboard: " + httpRequest.status); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /beholder/static/js/masonry.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Masonry PACKAGED v4.2.2 3 | * Cascading grid layout library 4 | * https://masonry.desandro.com 5 | * MIT License 6 | * by David DeSandro 7 | */ 8 | 9 | !function(t,e){"function"==typeof define&&define.amd?define("jquery-bridget/jquery-bridget",["jquery"],function(i){return e(t,i)}):"object"==typeof module&&module.exports?module.exports=e(t,require("jquery")):t.jQueryBridget=e(t,t.jQuery)}(window,function(t,e){"use strict";function i(i,r,a){function h(t,e,n){var o,r="$()."+i+'("'+e+'")';return t.each(function(t,h){var u=a.data(h,i);if(!u)return void s(i+" not initialized. Cannot call methods, i.e. "+r);var d=u[e];if(!d||"_"==e.charAt(0))return void s(r+" is not a valid method");var l=d.apply(u,n);o=void 0===o?l:o}),void 0!==o?o:t}function u(t,e){t.each(function(t,n){var o=a.data(n,i);o?(o.option(e),o._init()):(o=new r(n,e),a.data(n,i,o))})}a=a||e||t.jQuery,a&&(r.prototype.option||(r.prototype.option=function(t){a.isPlainObject(t)&&(this.options=a.extend(!0,this.options,t))}),a.fn[i]=function(t){if("string"==typeof t){var e=o.call(arguments,1);return h(this,t,e)}return u(this,t),this},n(a))}function n(t){!t||t&&t.bridget||(t.bridget=i)}var o=Array.prototype.slice,r=t.console,s="undefined"==typeof r?function(){}:function(t){r.error(t)};return n(e||t.jQuery),i}),function(t,e){"function"==typeof define&&define.amd?define("ev-emitter/ev-emitter",e):"object"==typeof module&&module.exports?module.exports=e():t.EvEmitter=e()}("undefined"!=typeof window?window:this,function(){function t(){}var e=t.prototype;return e.on=function(t,e){if(t&&e){var i=this._events=this._events||{},n=i[t]=i[t]||[];return-1==n.indexOf(e)&&n.push(e),this}},e.once=function(t,e){if(t&&e){this.on(t,e);var i=this._onceEvents=this._onceEvents||{},n=i[t]=i[t]||{};return n[e]=!0,this}},e.off=function(t,e){var i=this._events&&this._events[t];if(i&&i.length){var n=i.indexOf(e);return-1!=n&&i.splice(n,1),this}},e.emitEvent=function(t,e){var i=this._events&&this._events[t];if(i&&i.length){i=i.slice(0),e=e||[];for(var n=this._onceEvents&&this._onceEvents[t],o=0;oe;e++){var i=h[e];t[i]=0}return t}function n(t){var e=getComputedStyle(t);return e||a("Style returned "+e+". Are you running this code in a hidden iframe on Firefox? See https://bit.ly/getsizebug1"),e}function o(){if(!d){d=!0;var e=document.createElement("div");e.style.width="200px",e.style.padding="1px 2px 3px 4px",e.style.borderStyle="solid",e.style.borderWidth="1px 2px 3px 4px",e.style.boxSizing="border-box";var i=document.body||document.documentElement;i.appendChild(e);var o=n(e);s=200==Math.round(t(o.width)),r.isBoxSizeOuter=s,i.removeChild(e)}}function r(e){if(o(),"string"==typeof e&&(e=document.querySelector(e)),e&&"object"==typeof e&&e.nodeType){var r=n(e);if("none"==r.display)return i();var a={};a.width=e.offsetWidth,a.height=e.offsetHeight;for(var d=a.isBorderBox="border-box"==r.boxSizing,l=0;u>l;l++){var c=h[l],f=r[c],m=parseFloat(f);a[c]=isNaN(m)?0:m}var p=a.paddingLeft+a.paddingRight,g=a.paddingTop+a.paddingBottom,y=a.marginLeft+a.marginRight,v=a.marginTop+a.marginBottom,_=a.borderLeftWidth+a.borderRightWidth,z=a.borderTopWidth+a.borderBottomWidth,E=d&&s,b=t(r.width);b!==!1&&(a.width=b+(E?0:p+_));var x=t(r.height);return x!==!1&&(a.height=x+(E?0:g+z)),a.innerWidth=a.width-(p+_),a.innerHeight=a.height-(g+z),a.outerWidth=a.width+y,a.outerHeight=a.height+v,a}}var s,a="undefined"==typeof console?e:function(t){console.error(t)},h=["paddingLeft","paddingRight","paddingTop","paddingBottom","marginLeft","marginRight","marginTop","marginBottom","borderLeftWidth","borderRightWidth","borderTopWidth","borderBottomWidth"],u=h.length,d=!1;return r}),function(t,e){"use strict";"function"==typeof define&&define.amd?define("desandro-matches-selector/matches-selector",e):"object"==typeof module&&module.exports?module.exports=e():t.matchesSelector=e()}(window,function(){"use strict";var t=function(){var t=window.Element.prototype;if(t.matches)return"matches";if(t.matchesSelector)return"matchesSelector";for(var e=["webkit","moz","ms","o"],i=0;is?"round":"floor";r=Math[a](r),this.cols=Math.max(r,1)},n.getContainerWidth=function(){var t=this._getOption("fitWidth"),i=t?this.element.parentNode:this.element,n=e(i);this.containerWidth=n&&n.innerWidth},n._getItemLayoutPosition=function(t){t.getSize();var e=t.size.outerWidth%this.columnWidth,i=e&&1>e?"round":"ceil",n=Math[i](t.size.outerWidth/this.columnWidth);n=Math.min(n,this.cols);for(var o=this.options.horizontalOrder?"_getHorizontalColPosition":"_getTopColPosition",r=this[o](n,t),s={x:this.columnWidth*r.col,y:r.y},a=r.y+t.size.outerHeight,h=n+r.col,u=r.col;h>u;u++)this.colYs[u]=a;return s},n._getTopColPosition=function(t){var e=this._getTopColGroup(t),i=Math.min.apply(Math,e);return{col:e.indexOf(i),y:i}},n._getTopColGroup=function(t){if(2>t)return this.colYs;for(var e=[],i=this.cols+1-t,n=0;i>n;n++)e[n]=this._getColGroupY(n,t);return e},n._getColGroupY=function(t,e){if(2>e)return this.colYs[t];var i=this.colYs.slice(t,t+e);return Math.max.apply(Math,i)},n._getHorizontalColPosition=function(t,e){var i=this.horizontalColIndex%this.cols,n=t>1&&i+t>this.cols;i=n?0:i;var o=e.size.outerWidth&&e.size.outerHeight;return this.horizontalColIndex=o?i+t:this.horizontalColIndex,{col:i,y:this._getColGroupY(i,t)}},n._manageStamp=function(t){var i=e(t),n=this._getElementOffset(t),o=this._getOption("originLeft"),r=o?n.left:n.right,s=r+i.outerWidth,a=Math.floor(r/this.columnWidth);a=Math.max(0,a);var h=Math.floor(s/this.columnWidth);h-=s%this.columnWidth?0:1,h=Math.min(this.cols-1,h);for(var u=this._getOption("originTop"),d=(u?n.top:n.bottom)+i.outerHeight,l=a;h>=l;l++)this.colYs[l]=Math.max(d,this.colYs[l])},n._getContainerSize=function(){this.maxY=Math.max.apply(Math,this.colYs);var t={height:this.maxY};return this._getOption("fitWidth")&&(t.width=this._getContainerFitWidth()),t},n._getContainerFitWidth=function(){for(var t=0,e=this.cols;--e&&0===this.colYs[e];)t++;return(this.cols-t)*this.columnWidth-this.gutter},n.needsResizeLayout=function(){var t=this.containerWidth;return this.getContainerWidth(),t!=this.containerWidth},i}); -------------------------------------------------------------------------------- /beholder/templates/dashboard.html: -------------------------------------------------------------------------------- 1 |
2 | {% include "widget/sys_info.html" %} 3 |
4 |
5 | {% include "widget/cpu_info.html" %} 6 |
7 |
8 | {% include "widget/mem_info.html" %} 9 |
10 |
11 | {% include "widget/storage_info.html" %} 12 |
13 |
14 | {% include "widget/services_info.html" %} 15 |
16 |
17 | {% include "widget/network_info.html" %} 18 |
19 |
20 | {% include "widget/temperature_info.html" %} 21 |
-------------------------------------------------------------------------------- /beholder/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Beholder Monitor Dashboard 8 | 9 | 10 | 11 | 26 | 27 |
28 |
29 | {% include "dashboard.html" %} 30 |
31 |
32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /beholder/templates/widget/cpu_info.html: -------------------------------------------------------------------------------- 1 |
2 |
CPU
3 |
    4 |
  • 5 |
    6 |
    Physical Cores
    7 |
    {{ summaries["cpu_info"]["physical_cores"] }}
    8 |
    9 |
  • 10 |
  • 11 |
    12 |
    Total Cores
    13 |
    {{ summaries["cpu_info"]["total_cores"] }}
    14 |
    15 |
  • 16 |
  • 17 |
    18 |
    Max Frequency
    19 |
    {{ summaries["cpu_info"]["max_frequency"] }}
    20 |
    21 |
  • 22 |
  • 23 |
    24 |
    Current Frequency
    25 |
    26 | {{ summaries["cpu_info"]["current_frequency"] }}  27 | - {{ "%0.f" % summaries["cpu_info"]["pct_current_frequency"]|float }}% 28 |
    29 |
    30 |
  • 31 |
  • 32 | Current Load
    33 | {%- if summaries["cpu_info"]["total_load"] <= 50 -%} 34 | {%- set bar_color = "bg-success" -%} 35 | {%- elif summaries["cpu_info"]["total_load"] > 50 and summaries["cpu_info"]["total_load"] <= 75 -%} 36 | {%- set bar_color = "bg-warning" -%} 37 | {%- else -%} 38 | {%- set bar_color = "bg-danger" -%} 39 | {%- endif -%} 40 |
    41 |
    42 | {% if summaries['cpu_info']['total_load'] >= 12 %} 43 | {{ summaries['cpu_info']['total_load'] }} % 44 | {% endif %} 45 |
    46 | {% if summaries[ 'cpu_info']['total_load'] < 12 %} 47 |  {{ summaries['cpu_info']['total_load'] }} % 48 | {% endif %} 49 |
    50 |
  • 51 |
52 |
-------------------------------------------------------------------------------- /beholder/templates/widget/mem_info.html: -------------------------------------------------------------------------------- 1 |
2 |
Memory
3 |
    4 |
  • 5 | Physical Memory - {{ summaries["mem_info"]["physical_memory"]["total_available"] }} total
    6 | {%- if summaries["mem_info"]["physical_memory"]["pct_used"] <= 50 -%} 7 | {%- set bar_color = "bg-success" -%} 8 | {%- elif summaries["mem_info"]["physical_memory"]["pct_used"] > 50 and summaries["mem_info"]["physical_memory"]["pct_used"] <= 75 -%} 9 | {%- set bar_color = "bg-warning" -%} 10 | {%- else -%} 11 | {%- set bar_color = "bg-danger" -%} 12 | {%- endif -%} 13 |
    14 |
    15 | {% if summaries["mem_info"]["physical_memory"]["pct_used"] >= 12 %} 16 | {{ summaries['mem_info']['physical_memory']['pct_used'] }} % 17 | {% endif %} 18 |
    19 | {% if summaries["mem_info"]["physical_memory"]["pct_used"] < 12 %} 20 |  {{ summaries['mem_info']['physical_memory']['pct_used'] }} % 21 | {% endif %} 22 |
    23 | {{ summaries["mem_info"]["physical_memory"]["total_free"] }} free. 24 |
  • 25 |
  • 26 | Swap Memory - {{ summaries["mem_info"]["swap_memory"]["total_available"] }} total
    27 | {%- if summaries["mem_info"]["swap_memory"]["pct_used"] <= 50 -%} 28 | {%- set bar_color = "bg-success" -%} 29 | {%- elif summaries["mem_info"]["swap_memory"]["pct_used"] > 50 and summaries["mem_info"]["swap_memory"]["pct_used"] <= 75 -%} 30 | {%- set bar_color = "bg-warning" -%} 31 | {%- else -%} 32 | {%- set bar_color = "bg-danger" -%} 33 | {%- endif -%} 34 |
    35 |
    36 | {% if summaries["mem_info"]["swap_memory"]["pct_used"] >= 12 %} 37 | {{ summaries['mem_info']['swap_memory']['pct_used'] }} % 38 | {% endif %} 39 |
    40 | {% if summaries["mem_info"]["swap_memory"]["pct_used"] < 12 %} 41 |  {{ summaries['mem_info']['swap_memory']['pct_used'] }} % 42 | {% endif %} 43 |
    44 | {{ summaries["mem_info"]["swap_memory"]["total_free"] }} free. 45 |
  • 46 |
47 |
-------------------------------------------------------------------------------- /beholder/templates/widget/network_info.html: -------------------------------------------------------------------------------- 1 |
2 |
Network
3 |
    4 | {% for interface, details in summaries["network_info"].items() %} 5 |
  • 6 |
    7 |
    8 | {{ interface }}

    9 | 10 | {% for address in details["addresses"] %} 11 | {%- if address["address_type"] == "AF_INET6" -%} 12 | {%- set address_type = "IPv6" -%} 13 | {%- elif address["address_type"] == "AF_PACKET" -%} 14 | {%- set address_type = "MAC" -%} 15 | {%- else -%} 16 | {%- set address_type = "IP" -%} 17 | {%- endif -%} 18 | {{ address_type }}: {{ address["address"] }}
    19 | {% endfor %} 20 |
    21 |
    22 | {%- if details["active"] -%} 23 | {%- set text_color = "bg-success" -%} 24 | {%- set status = "Active" -%} 25 | {%- else -%} 26 | {%- set text_color = "bg-danger" -%} 27 | {%- set status = "Inactive" -%} 28 | {%- endif -%} 29 |
    30 |
    {{ status }}
    31 |
    32 |
    33 |
  • 34 | {% endfor %} 35 |
36 |
37 | -------------------------------------------------------------------------------- /beholder/templates/widget/services_info.html: -------------------------------------------------------------------------------- 1 |
2 |
Services
3 | {% if summaries["services_info"]|length == 0 %} 4 |
5 |

No service in the services watch list.

6 |
7 | {% else %} 8 |
    9 | {% for service, status in summaries["services_info"].items() %} 10 |
  • 11 |
    12 |
    13 | {{ service }} 14 |
    15 | {%- if status -%} 16 | {%- set text_color = "bg-success" -%} 17 | {%- set status_text = "Active" -%} 18 | {%- else -%} 19 | {%- set text_color = "bg-danger" -%} 20 | {%- set status_text = "Inactive" -%} 21 | {%- endif -%} 22 |
    23 |
    {{ status_text }}
    24 |
    25 |
    26 |
  • 27 | {% endfor %} 28 |
29 | {% endif %} 30 |
-------------------------------------------------------------------------------- /beholder/templates/widget/storage_info.html: -------------------------------------------------------------------------------- 1 |
2 |
Storage
3 |
    4 | {% for device, summary in summaries["storage_info"].items() %} 5 |
  • 6 | {{ device }} - {{ summary["total_size"] }} total
    7 | {%- if summary["pct_used"] <= 50 -%} 8 | {%- set bar_color = "bg-success" -%} 9 | {%- elif summary["pct_used"] > 50 and summary["pct_used"] <= 75 -%} 10 | {%- set bar_color = "bg-warning" -%} 11 | {%- else -%} 12 | {%- set bar_color = "bg-danger" -%} 13 | {%- endif -%} 14 |
    15 |
    16 | {% if summary['pct_used'] >= 12 %} 17 | {{ summary['pct_used'] }} % 18 | {% endif %} 19 |
    20 | {% if summary['pct_used'] < 12 %} 21 |  {{ summary['pct_used'] }} % 22 | {% endif %} 23 |
    24 | {{ summary["total_free"] }} free. 25 |
  • 26 | {% endfor %} 27 |
28 |
-------------------------------------------------------------------------------- /beholder/templates/widget/sys_info.html: -------------------------------------------------------------------------------- 1 |
2 |
Node
3 |
    4 |
  • 5 |
    6 |
    Host
    7 |
    {{ summaries["sys_info"]["host_name"] }}
    8 |
    9 |
  • 10 |
  • 11 |
    12 |
    OS
    13 |
    {{ summaries["sys_info"]["system"] }}
    14 |
    15 |
  • 16 |
  • 17 |
    18 |
    Kernel Version
    19 |
    {{ summaries["sys_info"]["release"] }}
    20 |
    21 |
  • 22 |
  • 23 |
    24 |
    Architecture
    25 |
    {{ summaries["sys_info"]["architecture"] }}
    26 |
    27 |
  • 28 |
  • 29 |
    30 |
    Locale
    31 |
    {{ summaries["sys_info"]["locale"] }}
    32 |
    33 |
  • 34 |
  • 35 |
    36 |
    Encoding
    37 |
    {{ summaries["sys_info"]["encoding"] }}
    38 |
    39 |
  • 40 |
  • 41 |
    42 |
    Timezone
    43 |
    {{ summaries["sys_info"]["time_zone"] }}
    44 |
    45 |
  • 46 |
  • 47 |
    48 |
    Current System Time
    49 |
    {{ summaries["sys_info"]["current_time"] }}
    50 |
    51 |
  • 52 |
  • 53 |
    54 |
    Up Time
    55 |
    {{ summaries["sys_info"]["up_time"] }}
    56 |
    57 |
  • 58 |
59 |
-------------------------------------------------------------------------------- /beholder/templates/widget/temperature_info.html: -------------------------------------------------------------------------------- 1 |
2 |
Temperature
3 |
    4 | {% for sensor, sensor_readings in summaries["temperature_info"].items() %} 5 | {% for reading in sensor_readings %} 6 |
  • 7 |
    8 |
    9 | {%- if reading["sensor_label"] != "" -%} 10 | {%- set sensor_label = "(" + reading["sensor_label"] + ")" -%} 11 | {%- else -%} 12 | {%- set sensor_label = "" -%} 13 | {%- endif -%} 14 | {{ sensor }} 15 | {{ sensor_label }}
    16 | {% if reading["low_band"] > 0 %} 17 | {{ "%0.1f" % reading["low_band"]|float }} {{ reading["scale"] }}  18 | {{ "%0.1f" % reading["medium_band"]|float }} {{ reading["scale"] }}  19 | {{ "%0.1f" % reading["high_band"]|float }} {{ reading["scale"] }} 20 | {% else %} 21 | No bands available. 22 | {% endif%} 23 |
    24 | {%- set text_color = "" -%} 25 | {%- if reading["low_band"] > 0 and reading["temperature"] <= reading["low_band"] -%} 26 | {%- set text_color = "text-success" -%} 27 | {%- elif reading["low_band"] > 0 and reading["temperature"] > reading["low_band"] and reading["temperature"] <= reading["medium_band"] -%} 28 | {%- set text_color = "text-warning" -%} 29 | {%- elif reading["low_band"] > 0 and reading["temperature"] > reading["medium_band"] -%} 30 | {%- set text_color = "text-danger" -%} 31 | {%- endif -%} 32 |
    33 | {{ reading["temperature"] }} {{ reading["scale"] }} 34 |
    35 |
    36 |
  • 37 | {% endfor %} 38 | {% endfor %} 39 |
40 |
-------------------------------------------------------------------------------- /images/beholder_dark_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NullPyDev/beholder/d2fe1ddaec90ce5ac57564204ce25d703861e7a1/images/beholder_dark_screenshot.png -------------------------------------------------------------------------------- /images/beholder_light_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NullPyDev/beholder/d2fe1ddaec90ce5ac57564204ce25d703861e7a1/images/beholder_light_screenshot.png -------------------------------------------------------------------------------- /images/paypal_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NullPyDev/beholder/d2fe1ddaec90ce5ac57564204ce25d703861e7a1/images/paypal_button.png -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "assertpy" 5 | version = "1.1" 6 | description = "Simple assertion library for unit testing in python with a fluent API" 7 | category = "dev" 8 | optional = false 9 | python-versions = "*" 10 | files = [ 11 | {file = "assertpy-1.1.tar.gz", hash = "sha256:acc64329934ad71a3221de185517a43af33e373bb44dc05b5a9b174394ef4833"}, 12 | ] 13 | 14 | [[package]] 15 | name = "astroid" 16 | version = "2.15.6" 17 | description = "An abstract syntax tree for Python with inference support." 18 | category = "dev" 19 | optional = false 20 | python-versions = ">=3.7.2" 21 | files = [ 22 | {file = "astroid-2.15.6-py3-none-any.whl", hash = "sha256:389656ca57b6108f939cf5d2f9a2a825a3be50ba9d589670f393236e0a03b91c"}, 23 | {file = "astroid-2.15.6.tar.gz", hash = "sha256:903f024859b7c7687d7a7f3a3f73b17301f8e42dfd9cc9df9d4418172d3e2dbd"}, 24 | ] 25 | 26 | [package.dependencies] 27 | lazy-object-proxy = ">=1.4.0" 28 | wrapt = {version = ">=1.14,<2", markers = "python_version >= \"3.11\""} 29 | 30 | [[package]] 31 | name = "black" 32 | version = "23.7.0" 33 | description = "The uncompromising code formatter." 34 | category = "dev" 35 | optional = false 36 | python-versions = ">=3.8" 37 | files = [ 38 | {file = "black-23.7.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:5c4bc552ab52f6c1c506ccae05681fab58c3f72d59ae6e6639e8885e94fe2587"}, 39 | {file = "black-23.7.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:552513d5cd5694590d7ef6f46e1767a4df9af168d449ff767b13b084c020e63f"}, 40 | {file = "black-23.7.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:86cee259349b4448adb4ef9b204bb4467aae74a386bce85d56ba4f5dc0da27be"}, 41 | {file = "black-23.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:501387a9edcb75d7ae8a4412bb8749900386eaef258f1aefab18adddea1936bc"}, 42 | {file = "black-23.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb074d8b213749fa1d077d630db0d5f8cc3b2ae63587ad4116e8a436e9bbe995"}, 43 | {file = "black-23.7.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:b5b0ee6d96b345a8b420100b7d71ebfdd19fab5e8301aff48ec270042cd40ac2"}, 44 | {file = "black-23.7.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:893695a76b140881531062d48476ebe4a48f5d1e9388177e175d76234ca247cd"}, 45 | {file = "black-23.7.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:c333286dc3ddca6fdff74670b911cccedacb4ef0a60b34e491b8a67c833b343a"}, 46 | {file = "black-23.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831d8f54c3a8c8cf55f64d0422ee875eecac26f5f649fb6c1df65316b67c8926"}, 47 | {file = "black-23.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:7f3bf2dec7d541b4619b8ce526bda74a6b0bffc480a163fed32eb8b3c9aed8ad"}, 48 | {file = "black-23.7.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:f9062af71c59c004cd519e2fb8f5d25d39e46d3af011b41ab43b9c74e27e236f"}, 49 | {file = "black-23.7.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:01ede61aac8c154b55f35301fac3e730baf0c9cf8120f65a9cd61a81cfb4a0c3"}, 50 | {file = "black-23.7.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:327a8c2550ddc573b51e2c352adb88143464bb9d92c10416feb86b0f5aee5ff6"}, 51 | {file = "black-23.7.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1c6022b86f83b632d06f2b02774134def5d4d4f1dac8bef16d90cda18ba28a"}, 52 | {file = "black-23.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:27eb7a0c71604d5de083757fbdb245b1a4fae60e9596514c6ec497eb63f95320"}, 53 | {file = "black-23.7.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:8417dbd2f57b5701492cd46edcecc4f9208dc75529bcf76c514864e48da867d9"}, 54 | {file = "black-23.7.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:47e56d83aad53ca140da0af87678fb38e44fd6bc0af71eebab2d1f59b1acf1d3"}, 55 | {file = "black-23.7.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:25cc308838fe71f7065df53aedd20327969d05671bac95b38fdf37ebe70ac087"}, 56 | {file = "black-23.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:642496b675095d423f9b8448243336f8ec71c9d4d57ec17bf795b67f08132a91"}, 57 | {file = "black-23.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:ad0014efc7acf0bd745792bd0d8857413652979200ab924fbf239062adc12491"}, 58 | {file = "black-23.7.0-py3-none-any.whl", hash = "sha256:9fd59d418c60c0348505f2ddf9609c1e1de8e7493eab96198fc89d9f865e7a96"}, 59 | {file = "black-23.7.0.tar.gz", hash = "sha256:022a582720b0d9480ed82576c920a8c1dde97cc38ff11d8d8859b3bd6ca9eedb"}, 60 | ] 61 | 62 | [package.dependencies] 63 | click = ">=8.0.0" 64 | mypy-extensions = ">=0.4.3" 65 | packaging = ">=22.0" 66 | pathspec = ">=0.9.0" 67 | platformdirs = ">=2" 68 | 69 | [package.extras] 70 | colorama = ["colorama (>=0.4.3)"] 71 | d = ["aiohttp (>=3.7.4)"] 72 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 73 | uvloop = ["uvloop (>=0.15.2)"] 74 | 75 | [[package]] 76 | name = "blinker" 77 | version = "1.6.2" 78 | description = "Fast, simple object-to-object and broadcast signaling" 79 | category = "main" 80 | optional = false 81 | python-versions = ">=3.7" 82 | files = [ 83 | {file = "blinker-1.6.2-py3-none-any.whl", hash = "sha256:c3d739772abb7bc2860abf5f2ec284223d9ad5c76da018234f6f50d6f31ab1f0"}, 84 | {file = "blinker-1.6.2.tar.gz", hash = "sha256:4afd3de66ef3a9f8067559fb7a1cbe555c17dcbe15971b05d1b625c3e7abe213"}, 85 | ] 86 | 87 | [[package]] 88 | name = "click" 89 | version = "8.1.6" 90 | description = "Composable command line interface toolkit" 91 | category = "main" 92 | optional = false 93 | python-versions = ">=3.7" 94 | files = [ 95 | {file = "click-8.1.6-py3-none-any.whl", hash = "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"}, 96 | {file = "click-8.1.6.tar.gz", hash = "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd"}, 97 | ] 98 | 99 | [package.dependencies] 100 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 101 | 102 | [[package]] 103 | name = "colorama" 104 | version = "0.4.6" 105 | description = "Cross-platform colored terminal text." 106 | category = "main" 107 | optional = false 108 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 109 | files = [ 110 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 111 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 112 | ] 113 | 114 | [[package]] 115 | name = "coverage" 116 | version = "7.2.7" 117 | description = "Code coverage measurement for Python" 118 | category = "dev" 119 | optional = false 120 | python-versions = ">=3.7" 121 | files = [ 122 | {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, 123 | {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, 124 | {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, 125 | {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, 126 | {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, 127 | {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, 128 | {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, 129 | {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, 130 | {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, 131 | {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, 132 | {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, 133 | {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, 134 | {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, 135 | {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, 136 | {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, 137 | {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, 138 | {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, 139 | {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, 140 | {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, 141 | {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, 142 | {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, 143 | {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, 144 | {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, 145 | {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, 146 | {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, 147 | {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, 148 | {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, 149 | {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, 150 | {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, 151 | {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, 152 | {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, 153 | {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, 154 | {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, 155 | {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, 156 | {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, 157 | {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, 158 | {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, 159 | {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, 160 | {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, 161 | {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, 162 | {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, 163 | {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, 164 | {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, 165 | {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, 166 | {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, 167 | {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, 168 | {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, 169 | {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, 170 | {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, 171 | {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, 172 | {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, 173 | {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, 174 | {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, 175 | {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, 176 | {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, 177 | {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, 178 | {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, 179 | {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, 180 | {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, 181 | {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, 182 | ] 183 | 184 | [package.extras] 185 | toml = ["tomli"] 186 | 187 | [[package]] 188 | name = "dill" 189 | version = "0.3.7" 190 | description = "serialize all of Python" 191 | category = "dev" 192 | optional = false 193 | python-versions = ">=3.7" 194 | files = [ 195 | {file = "dill-0.3.7-py3-none-any.whl", hash = "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e"}, 196 | {file = "dill-0.3.7.tar.gz", hash = "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03"}, 197 | ] 198 | 199 | [package.extras] 200 | graph = ["objgraph (>=1.7.2)"] 201 | 202 | [[package]] 203 | name = "flask" 204 | version = "2.3.2" 205 | description = "A simple framework for building complex web applications." 206 | category = "main" 207 | optional = false 208 | python-versions = ">=3.8" 209 | files = [ 210 | {file = "Flask-2.3.2-py3-none-any.whl", hash = "sha256:77fd4e1249d8c9923de34907236b747ced06e5467ecac1a7bb7115ae0e9670b0"}, 211 | {file = "Flask-2.3.2.tar.gz", hash = "sha256:8c2f9abd47a9e8df7f0c3f091ce9497d011dc3b31effcf4c85a6e2b50f4114ef"}, 212 | ] 213 | 214 | [package.dependencies] 215 | blinker = ">=1.6.2" 216 | click = ">=8.1.3" 217 | itsdangerous = ">=2.1.2" 218 | Jinja2 = ">=3.1.2" 219 | Werkzeug = ">=2.3.3" 220 | 221 | [package.extras] 222 | async = ["asgiref (>=3.2)"] 223 | dotenv = ["python-dotenv"] 224 | 225 | [[package]] 226 | name = "icdiff" 227 | version = "2.0.6" 228 | description = "improved colored diff" 229 | category = "dev" 230 | optional = false 231 | python-versions = "*" 232 | files = [ 233 | {file = "icdiff-2.0.6.tar.gz", hash = "sha256:a2673b335d671e64fc73c44e1eaa0aa01fd0e68354e58ee17e863ab29912a79a"}, 234 | ] 235 | 236 | [[package]] 237 | name = "iniconfig" 238 | version = "2.0.0" 239 | description = "brain-dead simple config-ini parsing" 240 | category = "dev" 241 | optional = false 242 | python-versions = ">=3.7" 243 | files = [ 244 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 245 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 246 | ] 247 | 248 | [[package]] 249 | name = "isort" 250 | version = "5.12.0" 251 | description = "A Python utility / library to sort Python imports." 252 | category = "dev" 253 | optional = false 254 | python-versions = ">=3.8.0" 255 | files = [ 256 | {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, 257 | {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, 258 | ] 259 | 260 | [package.extras] 261 | colors = ["colorama (>=0.4.3)"] 262 | pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] 263 | plugins = ["setuptools"] 264 | requirements-deprecated-finder = ["pip-api", "pipreqs"] 265 | 266 | [[package]] 267 | name = "itsdangerous" 268 | version = "2.1.2" 269 | description = "Safely pass data to untrusted environments and back." 270 | category = "main" 271 | optional = false 272 | python-versions = ">=3.7" 273 | files = [ 274 | {file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"}, 275 | {file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"}, 276 | ] 277 | 278 | [[package]] 279 | name = "jinja2" 280 | version = "3.1.2" 281 | description = "A very fast and expressive template engine." 282 | category = "main" 283 | optional = false 284 | python-versions = ">=3.7" 285 | files = [ 286 | {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, 287 | {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, 288 | ] 289 | 290 | [package.dependencies] 291 | MarkupSafe = ">=2.0" 292 | 293 | [package.extras] 294 | i18n = ["Babel (>=2.7)"] 295 | 296 | [[package]] 297 | name = "lazy-object-proxy" 298 | version = "1.9.0" 299 | description = "A fast and thorough lazy object proxy." 300 | category = "dev" 301 | optional = false 302 | python-versions = ">=3.7" 303 | files = [ 304 | {file = "lazy-object-proxy-1.9.0.tar.gz", hash = "sha256:659fb5809fa4629b8a1ac5106f669cfc7bef26fbb389dda53b3e010d1ac4ebae"}, 305 | {file = "lazy_object_proxy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b40387277b0ed2d0602b8293b94d7257e17d1479e257b4de114ea11a8cb7f2d7"}, 306 | {file = "lazy_object_proxy-1.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8c6cfb338b133fbdbc5cfaa10fe3c6aeea827db80c978dbd13bc9dd8526b7d4"}, 307 | {file = "lazy_object_proxy-1.9.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:721532711daa7db0d8b779b0bb0318fa87af1c10d7fe5e52ef30f8eff254d0cd"}, 308 | {file = "lazy_object_proxy-1.9.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:66a3de4a3ec06cd8af3f61b8e1ec67614fbb7c995d02fa224813cb7afefee701"}, 309 | {file = "lazy_object_proxy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1aa3de4088c89a1b69f8ec0dcc169aa725b0ff017899ac568fe44ddc1396df46"}, 310 | {file = "lazy_object_proxy-1.9.0-cp310-cp310-win32.whl", hash = "sha256:f0705c376533ed2a9e5e97aacdbfe04cecd71e0aa84c7c0595d02ef93b6e4455"}, 311 | {file = "lazy_object_proxy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea806fd4c37bf7e7ad82537b0757999264d5f70c45468447bb2b91afdbe73a6e"}, 312 | {file = "lazy_object_proxy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:946d27deaff6cf8452ed0dba83ba38839a87f4f7a9732e8f9fd4107b21e6ff07"}, 313 | {file = "lazy_object_proxy-1.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79a31b086e7e68b24b99b23d57723ef7e2c6d81ed21007b6281ebcd1688acb0a"}, 314 | {file = "lazy_object_proxy-1.9.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f699ac1c768270c9e384e4cbd268d6e67aebcfae6cd623b4d7c3bfde5a35db59"}, 315 | {file = "lazy_object_proxy-1.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfb38f9ffb53b942f2b5954e0f610f1e721ccebe9cce9025a38c8ccf4a5183a4"}, 316 | {file = "lazy_object_proxy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:189bbd5d41ae7a498397287c408617fe5c48633e7755287b21d741f7db2706a9"}, 317 | {file = "lazy_object_proxy-1.9.0-cp311-cp311-win32.whl", hash = "sha256:81fc4d08b062b535d95c9ea70dbe8a335c45c04029878e62d744bdced5141586"}, 318 | {file = "lazy_object_proxy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:f2457189d8257dd41ae9b434ba33298aec198e30adf2dcdaaa3a28b9994f6adb"}, 319 | {file = "lazy_object_proxy-1.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d9e25ef10a39e8afe59a5c348a4dbf29b4868ab76269f81ce1674494e2565a6e"}, 320 | {file = "lazy_object_proxy-1.9.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cbf9b082426036e19c6924a9ce90c740a9861e2bdc27a4834fd0a910742ac1e8"}, 321 | {file = "lazy_object_proxy-1.9.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f5fa4a61ce2438267163891961cfd5e32ec97a2c444e5b842d574251ade27d2"}, 322 | {file = "lazy_object_proxy-1.9.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8fa02eaab317b1e9e03f69aab1f91e120e7899b392c4fc19807a8278a07a97e8"}, 323 | {file = "lazy_object_proxy-1.9.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e7c21c95cae3c05c14aafffe2865bbd5e377cfc1348c4f7751d9dc9a48ca4bda"}, 324 | {file = "lazy_object_proxy-1.9.0-cp37-cp37m-win32.whl", hash = "sha256:f12ad7126ae0c98d601a7ee504c1122bcef553d1d5e0c3bfa77b16b3968d2734"}, 325 | {file = "lazy_object_proxy-1.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:edd20c5a55acb67c7ed471fa2b5fb66cb17f61430b7a6b9c3b4a1e40293b1671"}, 326 | {file = "lazy_object_proxy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d0daa332786cf3bb49e10dc6a17a52f6a8f9601b4cf5c295a4f85854d61de63"}, 327 | {file = "lazy_object_proxy-1.9.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cd077f3d04a58e83d04b20e334f678c2b0ff9879b9375ed107d5d07ff160171"}, 328 | {file = "lazy_object_proxy-1.9.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:660c94ea760b3ce47d1855a30984c78327500493d396eac4dfd8bd82041b22be"}, 329 | {file = "lazy_object_proxy-1.9.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:212774e4dfa851e74d393a2370871e174d7ff0ebc980907723bb67d25c8a7c30"}, 330 | {file = "lazy_object_proxy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f0117049dd1d5635bbff65444496c90e0baa48ea405125c088e93d9cf4525b11"}, 331 | {file = "lazy_object_proxy-1.9.0-cp38-cp38-win32.whl", hash = "sha256:0a891e4e41b54fd5b8313b96399f8b0e173bbbfc03c7631f01efbe29bb0bcf82"}, 332 | {file = "lazy_object_proxy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:9990d8e71b9f6488e91ad25f322898c136b008d87bf852ff65391b004da5e17b"}, 333 | {file = "lazy_object_proxy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9e7551208b2aded9c1447453ee366f1c4070602b3d932ace044715d89666899b"}, 334 | {file = "lazy_object_proxy-1.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f83ac4d83ef0ab017683d715ed356e30dd48a93746309c8f3517e1287523ef4"}, 335 | {file = "lazy_object_proxy-1.9.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7322c3d6f1766d4ef1e51a465f47955f1e8123caee67dd641e67d539a534d006"}, 336 | {file = "lazy_object_proxy-1.9.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:18b78ec83edbbeb69efdc0e9c1cb41a3b1b1ed11ddd8ded602464c3fc6020494"}, 337 | {file = "lazy_object_proxy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:09763491ce220c0299688940f8dc2c5d05fd1f45af1e42e636b2e8b2303e4382"}, 338 | {file = "lazy_object_proxy-1.9.0-cp39-cp39-win32.whl", hash = "sha256:9090d8e53235aa280fc9239a86ae3ea8ac58eff66a705fa6aa2ec4968b95c821"}, 339 | {file = "lazy_object_proxy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f"}, 340 | ] 341 | 342 | [[package]] 343 | name = "markupsafe" 344 | version = "2.1.3" 345 | description = "Safely add untrusted strings to HTML/XML markup." 346 | category = "main" 347 | optional = false 348 | python-versions = ">=3.7" 349 | files = [ 350 | {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, 351 | {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, 352 | {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, 353 | {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, 354 | {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, 355 | {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, 356 | {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, 357 | {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, 358 | {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, 359 | {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, 360 | {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, 361 | {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, 362 | {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, 363 | {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, 364 | {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, 365 | {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, 366 | {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, 367 | {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, 368 | {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, 369 | {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, 370 | {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, 371 | {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, 372 | {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, 373 | {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, 374 | {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, 375 | {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, 376 | {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, 377 | {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, 378 | {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, 379 | {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, 380 | {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, 381 | {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, 382 | {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, 383 | {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, 384 | {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, 385 | {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, 386 | {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, 387 | {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, 388 | {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, 389 | {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, 390 | {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, 391 | {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, 392 | {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, 393 | {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, 394 | {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, 395 | {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, 396 | {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, 397 | {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, 398 | {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, 399 | {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, 400 | ] 401 | 402 | [[package]] 403 | name = "mccabe" 404 | version = "0.7.0" 405 | description = "McCabe checker, plugin for flake8" 406 | category = "dev" 407 | optional = false 408 | python-versions = ">=3.6" 409 | files = [ 410 | {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, 411 | {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, 412 | ] 413 | 414 | [[package]] 415 | name = "mslex" 416 | version = "0.3.0" 417 | description = "shlex for windows" 418 | category = "dev" 419 | optional = false 420 | python-versions = ">=3.5" 421 | files = [ 422 | {file = "mslex-0.3.0-py2.py3-none-any.whl", hash = "sha256:380cb14abf8fabf40e56df5c8b21a6d533dc5cbdcfe42406bbf08dda8f42e42a"}, 423 | {file = "mslex-0.3.0.tar.gz", hash = "sha256:4a1ac3f25025cad78ad2fe499dd16d42759f7a3801645399cce5c404415daa97"}, 424 | ] 425 | 426 | [[package]] 427 | name = "mypy-extensions" 428 | version = "1.0.0" 429 | description = "Type system extensions for programs checked with the mypy type checker." 430 | category = "dev" 431 | optional = false 432 | python-versions = ">=3.5" 433 | files = [ 434 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 435 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 436 | ] 437 | 438 | [[package]] 439 | name = "packaging" 440 | version = "23.1" 441 | description = "Core utilities for Python packages" 442 | category = "dev" 443 | optional = false 444 | python-versions = ">=3.7" 445 | files = [ 446 | {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, 447 | {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, 448 | ] 449 | 450 | [[package]] 451 | name = "pathspec" 452 | version = "0.11.2" 453 | description = "Utility library for gitignore style pattern matching of file paths." 454 | category = "dev" 455 | optional = false 456 | python-versions = ">=3.7" 457 | files = [ 458 | {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, 459 | {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, 460 | ] 461 | 462 | [[package]] 463 | name = "platformdirs" 464 | version = "3.10.0" 465 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 466 | category = "dev" 467 | optional = false 468 | python-versions = ">=3.7" 469 | files = [ 470 | {file = "platformdirs-3.10.0-py3-none-any.whl", hash = "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"}, 471 | {file = "platformdirs-3.10.0.tar.gz", hash = "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d"}, 472 | ] 473 | 474 | [package.extras] 475 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] 476 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] 477 | 478 | [[package]] 479 | name = "pluggy" 480 | version = "1.2.0" 481 | description = "plugin and hook calling mechanisms for python" 482 | category = "dev" 483 | optional = false 484 | python-versions = ">=3.7" 485 | files = [ 486 | {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, 487 | {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, 488 | ] 489 | 490 | [package.extras] 491 | dev = ["pre-commit", "tox"] 492 | testing = ["pytest", "pytest-benchmark"] 493 | 494 | [[package]] 495 | name = "pprintpp" 496 | version = "0.4.0" 497 | description = "A drop-in replacement for pprint that's actually pretty" 498 | category = "dev" 499 | optional = false 500 | python-versions = "*" 501 | files = [ 502 | {file = "pprintpp-0.4.0-py2.py3-none-any.whl", hash = "sha256:b6b4dcdd0c0c0d75e4d7b2f21a9e933e5b2ce62b26e1a54537f9651ae5a5c01d"}, 503 | {file = "pprintpp-0.4.0.tar.gz", hash = "sha256:ea826108e2c7f49dc6d66c752973c3fc9749142a798d6b254e1e301cfdbc6403"}, 504 | ] 505 | 506 | [[package]] 507 | name = "psutil" 508 | version = "5.9.5" 509 | description = "Cross-platform lib for process and system monitoring in Python." 510 | category = "main" 511 | optional = false 512 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 513 | files = [ 514 | {file = "psutil-5.9.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:be8929ce4313f9f8146caad4272f6abb8bf99fc6cf59344a3167ecd74f4f203f"}, 515 | {file = "psutil-5.9.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ab8ed1a1d77c95453db1ae00a3f9c50227ebd955437bcf2a574ba8adbf6a74d5"}, 516 | {file = "psutil-5.9.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:4aef137f3345082a3d3232187aeb4ac4ef959ba3d7c10c33dd73763fbc063da4"}, 517 | {file = "psutil-5.9.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ea8518d152174e1249c4f2a1c89e3e6065941df2fa13a1ab45327716a23c2b48"}, 518 | {file = "psutil-5.9.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:acf2aef9391710afded549ff602b5887d7a2349831ae4c26be7c807c0a39fac4"}, 519 | {file = "psutil-5.9.5-cp27-none-win32.whl", hash = "sha256:5b9b8cb93f507e8dbaf22af6a2fd0ccbe8244bf30b1baad6b3954e935157ae3f"}, 520 | {file = "psutil-5.9.5-cp27-none-win_amd64.whl", hash = "sha256:8c5f7c5a052d1d567db4ddd231a9d27a74e8e4a9c3f44b1032762bd7b9fdcd42"}, 521 | {file = "psutil-5.9.5-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:3c6f686f4225553615612f6d9bc21f1c0e305f75d7d8454f9b46e901778e7217"}, 522 | {file = "psutil-5.9.5-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a7dd9997128a0d928ed4fb2c2d57e5102bb6089027939f3b722f3a210f9a8da"}, 523 | {file = "psutil-5.9.5-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89518112647f1276b03ca97b65cc7f64ca587b1eb0278383017c2a0dcc26cbe4"}, 524 | {file = "psutil-5.9.5-cp36-abi3-win32.whl", hash = "sha256:104a5cc0e31baa2bcf67900be36acde157756b9c44017b86b2c049f11957887d"}, 525 | {file = "psutil-5.9.5-cp36-abi3-win_amd64.whl", hash = "sha256:b258c0c1c9d145a1d5ceffab1134441c4c5113b2417fafff7315a917a026c3c9"}, 526 | {file = "psutil-5.9.5-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:c607bb3b57dc779d55e1554846352b4e358c10fff3abf3514a7a6601beebdb30"}, 527 | {file = "psutil-5.9.5.tar.gz", hash = "sha256:5410638e4df39c54d957fc51ce03048acd8e6d60abc0f5107af51e5fb566eb3c"}, 528 | ] 529 | 530 | [package.extras] 531 | test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] 532 | 533 | [[package]] 534 | name = "pylint" 535 | version = "2.17.5" 536 | description = "python code static checker" 537 | category = "dev" 538 | optional = false 539 | python-versions = ">=3.7.2" 540 | files = [ 541 | {file = "pylint-2.17.5-py3-none-any.whl", hash = "sha256:73995fb8216d3bed149c8d51bba25b2c52a8251a2c8ac846ec668ce38fab5413"}, 542 | {file = "pylint-2.17.5.tar.gz", hash = "sha256:f7b601cbc06fef7e62a754e2b41294c2aa31f1cb659624b9a85bcba29eaf8252"}, 543 | ] 544 | 545 | [package.dependencies] 546 | astroid = ">=2.15.6,<=2.17.0-dev0" 547 | colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} 548 | dill = {version = ">=0.3.6", markers = "python_version >= \"3.11\""} 549 | isort = ">=4.2.5,<6" 550 | mccabe = ">=0.6,<0.8" 551 | platformdirs = ">=2.2.0" 552 | tomlkit = ">=0.10.1" 553 | 554 | [package.extras] 555 | spelling = ["pyenchant (>=3.2,<4.0)"] 556 | testutils = ["gitpython (>3)"] 557 | 558 | [[package]] 559 | name = "pytest" 560 | version = "7.4.0" 561 | description = "pytest: simple powerful testing with Python" 562 | category = "dev" 563 | optional = false 564 | python-versions = ">=3.7" 565 | files = [ 566 | {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"}, 567 | {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"}, 568 | ] 569 | 570 | [package.dependencies] 571 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 572 | iniconfig = "*" 573 | packaging = "*" 574 | pluggy = ">=0.12,<2.0" 575 | 576 | [package.extras] 577 | testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 578 | 579 | [[package]] 580 | name = "pytest-icdiff" 581 | version = "0.6" 582 | description = "use icdiff for better error messages in pytest assertions" 583 | category = "dev" 584 | optional = false 585 | python-versions = ">=3.6" 586 | files = [ 587 | {file = "pytest-icdiff-0.6.tar.gz", hash = "sha256:e8f1ef4550a893b4f0a0ea7e7a8299b12ded72c086101d7811ddec0d85fd1bad"}, 588 | ] 589 | 590 | [package.dependencies] 591 | icdiff = "*" 592 | pprintpp = "*" 593 | pytest = "*" 594 | 595 | [[package]] 596 | name = "pytest-randomly" 597 | version = "3.13.0" 598 | description = "Pytest plugin to randomly order tests and control random.seed." 599 | category = "dev" 600 | optional = false 601 | python-versions = ">=3.8" 602 | files = [ 603 | {file = "pytest_randomly-3.13.0-py3-none-any.whl", hash = "sha256:e78d898ef4066f89744e5075083aa7fb6f0de07ffd70ca9c4435cda590cf1eac"}, 604 | {file = "pytest_randomly-3.13.0.tar.gz", hash = "sha256:079c78b94693189879fbd7304de4e147304f0811fa96249ea5619f2f1cd33df0"}, 605 | ] 606 | 607 | [package.dependencies] 608 | pytest = "*" 609 | 610 | [[package]] 611 | name = "pytest-sugar" 612 | version = "0.9.7" 613 | description = "pytest-sugar is a plugin for pytest that changes the default look and feel of pytest (e.g. progressbar, show tests that fail instantly)." 614 | category = "dev" 615 | optional = false 616 | python-versions = "*" 617 | files = [ 618 | {file = "pytest-sugar-0.9.7.tar.gz", hash = "sha256:f1e74c1abfa55f7241cf7088032b6e378566f16b938f3f08905e2cf4494edd46"}, 619 | {file = "pytest_sugar-0.9.7-py2.py3-none-any.whl", hash = "sha256:8cb5a4e5f8bbcd834622b0235db9e50432f4cbd71fef55b467fe44e43701e062"}, 620 | ] 621 | 622 | [package.dependencies] 623 | packaging = ">=21.3" 624 | pytest = ">=6.2.0" 625 | termcolor = ">=2.1.0" 626 | 627 | [package.extras] 628 | dev = ["black", "flake8", "pre-commit"] 629 | 630 | [[package]] 631 | name = "taskipy" 632 | version = "1.12.0" 633 | description = "tasks runner for python projects" 634 | category = "dev" 635 | optional = false 636 | python-versions = ">=3.6,<4.0" 637 | files = [ 638 | {file = "taskipy-1.12.0-py3-none-any.whl", hash = "sha256:38306fbc952a7ca314b8f842a74b2fc38535cdab21031fe89e714a83e6259a84"}, 639 | {file = "taskipy-1.12.0.tar.gz", hash = "sha256:e3dd7c53f7c9c4fd17dc908b1037f545afc452907eb0953b84e91c0a9a9d809d"}, 640 | ] 641 | 642 | [package.dependencies] 643 | colorama = ">=0.4.4,<0.5.0" 644 | mslex = {version = ">=0.3.0,<0.4.0", markers = "sys_platform == \"win32\""} 645 | psutil = ">=5.7.2,<6.0.0" 646 | tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} 647 | 648 | [[package]] 649 | name = "termcolor" 650 | version = "2.3.0" 651 | description = "ANSI color formatting for output in terminal" 652 | category = "dev" 653 | optional = false 654 | python-versions = ">=3.7" 655 | files = [ 656 | {file = "termcolor-2.3.0-py3-none-any.whl", hash = "sha256:3afb05607b89aed0ffe25202399ee0867ad4d3cb4180d98aaf8eefa6a5f7d475"}, 657 | {file = "termcolor-2.3.0.tar.gz", hash = "sha256:b5b08f68937f138fe92f6c089b99f1e2da0ae56c52b78bf7075fd95420fd9a5a"}, 658 | ] 659 | 660 | [package.extras] 661 | tests = ["pytest", "pytest-cov"] 662 | 663 | [[package]] 664 | name = "tomli" 665 | version = "2.0.1" 666 | description = "A lil' TOML parser" 667 | category = "dev" 668 | optional = false 669 | python-versions = ">=3.7" 670 | files = [ 671 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 672 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 673 | ] 674 | 675 | [[package]] 676 | name = "tomlkit" 677 | version = "0.12.1" 678 | description = "Style preserving TOML library" 679 | category = "dev" 680 | optional = false 681 | python-versions = ">=3.7" 682 | files = [ 683 | {file = "tomlkit-0.12.1-py3-none-any.whl", hash = "sha256:712cbd236609acc6a3e2e97253dfc52d4c2082982a88f61b640ecf0817eab899"}, 684 | {file = "tomlkit-0.12.1.tar.gz", hash = "sha256:38e1ff8edb991273ec9f6181244a6a391ac30e9f5098e7535640ea6be97a7c86"}, 685 | ] 686 | 687 | [[package]] 688 | name = "tzdata" 689 | version = "2023.3" 690 | description = "Provider of IANA time zone data" 691 | category = "main" 692 | optional = false 693 | python-versions = ">=2" 694 | files = [ 695 | {file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"}, 696 | {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, 697 | ] 698 | 699 | [[package]] 700 | name = "tzlocal" 701 | version = "5.0.1" 702 | description = "tzinfo object for the local timezone" 703 | category = "main" 704 | optional = false 705 | python-versions = ">=3.7" 706 | files = [ 707 | {file = "tzlocal-5.0.1-py3-none-any.whl", hash = "sha256:f3596e180296aaf2dbd97d124fe76ae3a0e3d32b258447de7b939b3fd4be992f"}, 708 | {file = "tzlocal-5.0.1.tar.gz", hash = "sha256:46eb99ad4bdb71f3f72b7d24f4267753e240944ecfc16f25d2719ba89827a803"}, 709 | ] 710 | 711 | [package.dependencies] 712 | tzdata = {version = "*", markers = "platform_system == \"Windows\""} 713 | 714 | [package.extras] 715 | devenv = ["black", "check-manifest", "flake8", "pyroma", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"] 716 | 717 | [[package]] 718 | name = "waitress" 719 | version = "2.1.2" 720 | description = "Waitress WSGI server" 721 | category = "main" 722 | optional = false 723 | python-versions = ">=3.7.0" 724 | files = [ 725 | {file = "waitress-2.1.2-py3-none-any.whl", hash = "sha256:7500c9625927c8ec60f54377d590f67b30c8e70ef4b8894214ac6e4cad233d2a"}, 726 | {file = "waitress-2.1.2.tar.gz", hash = "sha256:780a4082c5fbc0fde6a2fcfe5e26e6efc1e8f425730863c04085769781f51eba"}, 727 | ] 728 | 729 | [package.extras] 730 | docs = ["Sphinx (>=1.8.1)", "docutils", "pylons-sphinx-themes (>=1.0.9)"] 731 | testing = ["coverage (>=5.0)", "pytest", "pytest-cover"] 732 | 733 | [[package]] 734 | name = "werkzeug" 735 | version = "2.3.6" 736 | description = "The comprehensive WSGI web application library." 737 | category = "main" 738 | optional = false 739 | python-versions = ">=3.8" 740 | files = [ 741 | {file = "Werkzeug-2.3.6-py3-none-any.whl", hash = "sha256:935539fa1413afbb9195b24880778422ed620c0fc09670945185cce4d91a8890"}, 742 | {file = "Werkzeug-2.3.6.tar.gz", hash = "sha256:98c774df2f91b05550078891dee5f0eb0cb797a522c757a2452b9cee5b202330"}, 743 | ] 744 | 745 | [package.dependencies] 746 | MarkupSafe = ">=2.1.1" 747 | 748 | [package.extras] 749 | watchdog = ["watchdog (>=2.3)"] 750 | 751 | [[package]] 752 | name = "wrapt" 753 | version = "1.15.0" 754 | description = "Module for decorators, wrappers and monkey patching." 755 | category = "dev" 756 | optional = false 757 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 758 | files = [ 759 | {file = "wrapt-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1"}, 760 | {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29"}, 761 | {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2"}, 762 | {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46"}, 763 | {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c"}, 764 | {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09"}, 765 | {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079"}, 766 | {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e"}, 767 | {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a"}, 768 | {file = "wrapt-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923"}, 769 | {file = "wrapt-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee"}, 770 | {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727"}, 771 | {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7"}, 772 | {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0"}, 773 | {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec"}, 774 | {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90"}, 775 | {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975"}, 776 | {file = "wrapt-1.15.0-cp310-cp310-win32.whl", hash = "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1"}, 777 | {file = "wrapt-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e"}, 778 | {file = "wrapt-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7"}, 779 | {file = "wrapt-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72"}, 780 | {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb"}, 781 | {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e"}, 782 | {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c"}, 783 | {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3"}, 784 | {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92"}, 785 | {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98"}, 786 | {file = "wrapt-1.15.0-cp311-cp311-win32.whl", hash = "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416"}, 787 | {file = "wrapt-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705"}, 788 | {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29"}, 789 | {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd"}, 790 | {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb"}, 791 | {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248"}, 792 | {file = "wrapt-1.15.0-cp35-cp35m-win32.whl", hash = "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559"}, 793 | {file = "wrapt-1.15.0-cp35-cp35m-win_amd64.whl", hash = "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639"}, 794 | {file = "wrapt-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba"}, 795 | {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752"}, 796 | {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364"}, 797 | {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475"}, 798 | {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8"}, 799 | {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418"}, 800 | {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2"}, 801 | {file = "wrapt-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1"}, 802 | {file = "wrapt-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420"}, 803 | {file = "wrapt-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317"}, 804 | {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e"}, 805 | {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e"}, 806 | {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0"}, 807 | {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019"}, 808 | {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034"}, 809 | {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653"}, 810 | {file = "wrapt-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0"}, 811 | {file = "wrapt-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e"}, 812 | {file = "wrapt-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145"}, 813 | {file = "wrapt-1.15.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f"}, 814 | {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd"}, 815 | {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b"}, 816 | {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f"}, 817 | {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6"}, 818 | {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094"}, 819 | {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7"}, 820 | {file = "wrapt-1.15.0-cp38-cp38-win32.whl", hash = "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b"}, 821 | {file = "wrapt-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1"}, 822 | {file = "wrapt-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86"}, 823 | {file = "wrapt-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c"}, 824 | {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d"}, 825 | {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc"}, 826 | {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29"}, 827 | {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a"}, 828 | {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8"}, 829 | {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9"}, 830 | {file = "wrapt-1.15.0-cp39-cp39-win32.whl", hash = "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff"}, 831 | {file = "wrapt-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6"}, 832 | {file = "wrapt-1.15.0-py3-none-any.whl", hash = "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640"}, 833 | {file = "wrapt-1.15.0.tar.gz", hash = "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a"}, 834 | ] 835 | 836 | [metadata] 837 | lock-version = "2.0" 838 | python-versions = "^3.11" 839 | content-hash = "e6c400317d2dcc0b22bd5a1043b373cb37bfa5f1351d364df96c4b7444b3a418" 840 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "beholder" 3 | version = "0.1.2" 4 | description = "Tiny and simple system statistics monitor for SBCs." 5 | authors = ["NullPy "] 6 | license = "MIT" 7 | readme = "README.md" 8 | keywords = ["system", "resources", "monitor", "web", "dashboard"] 9 | classifiers = [ 10 | "License :: OSI Approved :: MIT License", 11 | "Programming Language :: Python", 12 | "Programming Language :: Python :: 3.8", 13 | "Topic :: Software Development :: Utilities :: System Monitor" 14 | ] 15 | 16 | [project.urls] 17 | Homepage = "https://github.com/NullPyDev/beholder" 18 | 19 | [tool.poetry.dependencies] 20 | python = "^3.8" 21 | flask = "^2.3.2" 22 | psutil = "^5.9.5" 23 | tzlocal = "^5.0.1" 24 | waitress = "^2.1.2" 25 | 26 | 27 | [tool.poetry.group.dev.dependencies] 28 | black = "^23.7.0" 29 | pytest = "^7.4.0" 30 | pylint = "^2.17.5" 31 | pytest-sugar = "^0.9.7" 32 | pytest-icdiff = "^0.6" 33 | assertpy = "^1.1" 34 | coverage = "^7.2.7" 35 | pytest-randomly = "^3.13.0" 36 | taskipy = "^1.12.0" 37 | 38 | [build-system] 39 | requires = ["poetry-core"] 40 | build-backend = "poetry.core.masonry.api" 41 | 42 | [tool.taskipy.tasks] 43 | beholder = { cmd = "flask --debug --app beholder/__main__.py run", help = "run the project in development mode." } 44 | test = { cmd = "coverage run --source=beholder -m pytest tests/ && coverage html --directory coverage/", help = "run all unit tests and generate the coverage report at '/coverage'." } 45 | format-source = { cmd = "black --line-length 120 beholder", help = "execute black to proper format the source code under '/beholder'." } 46 | format-tests = { cmd = "black --line-length 120 tests", help = "execute black to proper format the source code under '/tests'." } 47 | format = { cmd = "task format-source && task format-tests", help = "execute black to proper format all the python code in the project." } 48 | check = { cmd = "flake8 --format=html --htmldir=lint ./beholder ./tests", help = "execute flake8 against '/beholder' and '/tests'. All violations are reported on '/lint'." } -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NullPyDev/beholder/d2fe1ddaec90ce5ac57564204ce25d703861e7a1/tests/__init__.py -------------------------------------------------------------------------------- /tests/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NullPyDev/beholder/d2fe1ddaec90ce5ac57564204ce25d703861e7a1/tests/common/__init__.py -------------------------------------------------------------------------------- /tests/common/test_formatter.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from assertpy import assert_that 4 | 5 | from beholder.common.formatter import size_formatter, frequency_formatter 6 | 7 | 8 | class TestSizeFormatter(unittest.TestCase): 9 | def test_formatBytes(self): 10 | result: str = size_formatter(124) 11 | assert_that(result).is_equal_to("124.00 B") 12 | 13 | def test_formatWithThousandSeparator(self): 14 | result: str = size_formatter(1010) 15 | assert_that(result).is_equal_to("1,010.00 B") 16 | 17 | def test_formatKiloBytes(self): 18 | result: str = size_formatter(2_560) 19 | assert_that(result).is_equal_to("2.50 KB") 20 | 21 | def test_formatMegaBytes(self): 22 | result: str = size_formatter(1_572_864) 23 | assert_that(result).is_equal_to("1.50 MB") 24 | 25 | def test_formatGigaBytes(self): 26 | result: str = size_formatter(1_395_864_371) 27 | assert_that(result).is_equal_to("1.30 GB") 28 | 29 | def test_formatTeraBytes(self): 30 | result: str = size_formatter(1_759_218_604_000) 31 | assert_that(result).is_equal_to("1.60 TB") 32 | 33 | 34 | class TestFrequencyFormatter(unittest.TestCase): 35 | def test_formatMegaHertz(self): 36 | result: str = frequency_formatter(200) 37 | assert_that(result).is_equal_to("200.00 Mhz") 38 | 39 | def test_formatGigaHertz(self): 40 | result: str = frequency_formatter(2_300) 41 | assert_that(result).is_equal_to("2.30 Ghz") 42 | -------------------------------------------------------------------------------- /tests/facade/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NullPyDev/beholder/d2fe1ddaec90ce5ac57564204ce25d703861e7a1/tests/facade/__init__.py -------------------------------------------------------------------------------- /tests/facade/test_dashboard_facade.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from assertpy import assert_that 4 | 5 | from beholder.facade.dashboard_facade import DashboardFacade 6 | 7 | 8 | class TestDashboardFacade(unittest.TestCase): 9 | def setUp(self) -> None: 10 | self.facade: DashboardFacade = DashboardFacade() 11 | self.result: dict[str, any] = self.facade.load_summaries() 12 | 13 | def test_sysInfoIsAvailable(self): 14 | assert_that(self.result).contains("sys_info") 15 | 16 | def test_cpuInfoIsAvailable(self): 17 | assert_that(self.result).contains("cpu_info") 18 | 19 | def test_memInfoIsAvailable(self): 20 | assert_that(self.result).contains("mem_info") 21 | 22 | def test_storageInfoIsAvailable(self): 23 | assert_that(self.result).contains("storage_info") 24 | 25 | def test_servicesInfoIsAvailable(self): 26 | assert_that(self.result).contains("services_info") 27 | 28 | def test_networkInfoIsAvailable(self): 29 | assert_that(self.result).contains("network_info") 30 | 31 | def test_temperatureInfoIsAvailable(self): 32 | assert_that(self.result).contains("temperature_info") 33 | -------------------------------------------------------------------------------- /tests/service/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NullPyDev/beholder/d2fe1ddaec90ce5ac57564204ce25d703861e7a1/tests/service/__init__.py -------------------------------------------------------------------------------- /tests/service/test_cpu_info_service.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from assertpy import assert_that 4 | 5 | from beholder.service.cpu_info_service import CPUInfoService 6 | 7 | 8 | class TestCPUInfoService(unittest.TestCase): 9 | def setUp(self) -> None: 10 | self.service: CPUInfoService = CPUInfoService() 11 | self.result: dict[str, any] = self.service.get_info() 12 | 13 | def test_physicalCoresIsAvailable(self): 14 | assert_that(self.result).contains("physical_cores") 15 | assert_that(self.result["physical_cores"]).is_greater_than(0) 16 | 17 | def test_totalCoresIsAvailable(self): 18 | assert_that(self.result).contains("total_cores") 19 | assert_that(self.result["total_cores"]).is_greater_than(0) 20 | 21 | def test_maxFrequencyIsAvailable(self): 22 | assert_that(self.result).contains("max_frequency") 23 | assert_that(self.result["max_frequency"]).matches("[0-9\\.]*?hz") 24 | 25 | def test_currentFrequencyIsAvailable(self): 26 | assert_that(self.result).contains("current_frequency") 27 | assert_that(self.result["current_frequency"]).matches("[0-9\\.]*?hz") 28 | 29 | def test_pctCurrentFrequencyIsAvailable(self): 30 | assert_that(self.result).contains("pct_current_frequency") 31 | assert_that(self.result["pct_current_frequency"]).is_greater_than_or_equal_to(0) 32 | 33 | def test_totalLoadIsAvailable(self): 34 | assert_that(self.result).contains("total_load") 35 | assert_that(self.result["total_load"]).is_greater_than_or_equal_to(0) 36 | -------------------------------------------------------------------------------- /tests/service/test_memory_info_service.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from assertpy import assert_that 4 | 5 | from beholder.service.memory_info_service import MemoryInfoService 6 | 7 | 8 | class TestMemoryInfoService(unittest.TestCase): 9 | def setUp(self) -> None: 10 | self.service: MemoryInfoService = MemoryInfoService() 11 | self.result: dict[str, any] = self.service.get_info() 12 | 13 | def test_physicalMemoryIsAvailable(self): 14 | assert_that(self.result).contains("physical_memory") 15 | 16 | def test_swapMemoryIsAvailable(self): 17 | assert_that(self.result).contains("swap_memory") 18 | 19 | def test_physicalMemoryTotalAvailableIsAvailable(self): 20 | assert_that(self.result["physical_memory"]).contains("total_available") 21 | assert_that(self.result["physical_memory"]["total_available"]).matches("[0-9\\.]*?B") 22 | 23 | def test_swapMemoryTotalAvailableIsAvailable(self): 24 | assert_that(self.result["swap_memory"]).contains("total_available") 25 | assert_that(self.result["swap_memory"]["total_available"]).matches("[0-9\\.]*?B") 26 | 27 | def test_physicalMemoryPctUsedIsAvailable(self): 28 | assert_that(self.result["physical_memory"]).contains("pct_used") 29 | assert_that(self.result["physical_memory"]["pct_used"]).is_greater_than(0.0) 30 | 31 | def test_swapMemoryPctUsedIsAvailable(self): 32 | assert_that(self.result["swap_memory"]).contains("pct_used") 33 | assert_that(self.result["swap_memory"]["pct_used"]).is_greater_than(0.0) 34 | 35 | def test_physicalMemoryTotalFreeIsAvailable(self): 36 | assert_that(self.result["physical_memory"]).contains("total_free") 37 | assert_that(self.result["physical_memory"]["total_free"]).matches("[0-9\\.]*?B") 38 | 39 | def test_swapMemoryTotalFreeIsAvailable(self): 40 | assert_that(self.result["swap_memory"]).contains("total_free") 41 | assert_that(self.result["swap_memory"]["total_free"]).matches("[0-9\\.]*?B") 42 | -------------------------------------------------------------------------------- /tests/service/test_network_info_service.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from assertpy import assert_that 4 | 5 | from beholder.service.network_info_service import NetworkInfoService 6 | 7 | 8 | class TestNetworkInfoService(unittest.TestCase): 9 | def setUp(self) -> None: 10 | self.service: NetworkInfoService = NetworkInfoService() 11 | self.result: dict[str, any] = self.service.get_info() 12 | 13 | def test_loopBackInterfaceIsNotListed(self): 14 | assert_that(self.result).does_not_contain("lo") 15 | 16 | def test_availableInterfacesHaveStatus(self): 17 | for _interface, details in self.result.items(): 18 | assert_that(details).contains("active") 19 | assert_that(details["active"]).is_type_of(bool) 20 | 21 | def test_availableInterfacesHaveAddresses(self): 22 | for _interface, details in self.result.items(): 23 | assert_that(details).contains("addresses") 24 | assert_that(details["addresses"]).is_not_empty() 25 | 26 | def test_forEachInterfaceAddressThereIsOneAddressAvailable(self): 27 | for _interface, details in self.result.items(): 28 | for address in details["addresses"]: 29 | assert_that(address).contains("address") 30 | assert_that(address).contains("address_type") 31 | -------------------------------------------------------------------------------- /tests/service/test_service_info.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | from assertpy import assert_that 5 | 6 | from beholder.service.service_info_service import ServiceInfoService 7 | 8 | 9 | class TestWithNoServiceOnWatchList(unittest.TestCase): 10 | def setUp(self) -> None: 11 | os.environ["BEHOLDER_SERVICES_WATCHLIST"] = "" 12 | self.service: ServiceInfoService = ServiceInfoService() 13 | self.result: dict[str, any] = self.service.get_info() 14 | 15 | def test_resultIsEmpty(self): 16 | assert_that(self.result).is_empty() 17 | 18 | 19 | class TestWithServiceOnWatchlist(unittest.TestCase): 20 | def setUp(self) -> None: 21 | os.environ["BEHOLDER_SERVICES_WATCHLIST"] = "crond,cupsd,fake" 22 | self.service: ServiceInfoService = ServiceInfoService() 23 | self.result: dict[str, any] = self.service.get_info() 24 | del os.environ["BEHOLDER_SERVICES_WATCHLIST"] 25 | 26 | def test_resultIsNotEmpty(self): 27 | assert_that(self.result.items()).is_not_empty() 28 | 29 | def test_listedServicesAreInTheResult(self): 30 | assert_that(self.result).contains_key("crond", "cupsd", "fake") 31 | assert_that(self.result["crond"]).is_type_of(bool) 32 | assert_that(self.result["cupsd"]).is_type_of(bool) 33 | assert_that(self.result["fake"]).is_type_of(bool) 34 | -------------------------------------------------------------------------------- /tests/service/test_storage_info_service.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch 3 | 4 | from assertpy import assert_that 5 | 6 | from beholder.service.storage_info_service import StorageInfoService 7 | 8 | 9 | class TestStorageInfoService(unittest.TestCase): 10 | def setUp(self) -> None: 11 | self.service: StorageInfoService = StorageInfoService() 12 | self.result: dict[str, any] = self.service.get_info() 13 | 14 | def test_resultIsNotEmpty(self): 15 | assert_that(self.result).is_not_empty() 16 | 17 | def test_eachDeviceHasTotalSize(self): 18 | for _device, info in self.result.items(): 19 | assert_that(info).contains_key("total_size") 20 | assert_that(info["total_size"]).matches("[0-9\\.]*\\s?B") 21 | 22 | def test_eachDeviceHastTotalFree(self): 23 | for _device, info in self.result.items(): 24 | assert_that(info).contains("total_free") 25 | assert_that(info["total_free"]).matches("[0-9\\.]*\\s?B") 26 | 27 | def test_eachDeviceHasPctUsed(self): 28 | for _device, info in self.result.items(): 29 | assert_that(info).contains("pct_used") 30 | assert_that(info["pct_used"]).is_type_of(float) 31 | 32 | 33 | class TestFailSilently(unittest.TestCase): 34 | def setUp(self) -> None: 35 | self.service: StorageInfoService = StorageInfoService() 36 | 37 | @patch("psutil.disk_usage", autospec=True) 38 | def test_failSilentlyWithException(self, mock_disk_usage): 39 | mock_disk_usage.side_effect = PermissionError("Fake Error") 40 | 41 | result: dict[str, any] = self.service.get_info() 42 | 43 | assert_that(result).is_not_none() 44 | assert_that(result).is_empty() 45 | mock_disk_usage.assert_called() 46 | -------------------------------------------------------------------------------- /tests/service/test_system_info_service.py: -------------------------------------------------------------------------------- 1 | import locale 2 | import platform 3 | import unittest 4 | from datetime import datetime 5 | 6 | import psutil 7 | from assertpy import assert_that 8 | from tzlocal.unix import get_localzone 9 | 10 | from beholder import BEHOLDER_VERSION 11 | from beholder.service.system_info_service import SystemInfoService 12 | 13 | 14 | class TestSystemInfoService(unittest.TestCase): 15 | def setUp(self) -> None: 16 | self.service: SystemInfoService = SystemInfoService() 17 | self.result: dict[str, any] = self.service.get_info() 18 | 19 | def test_systemIsAvailable(self): 20 | assert_that(self.result).contains("system") 21 | assert_that(self.result["system"]).is_equal_to(platform.uname().system) 22 | 23 | def test_hostNameIsAvailable(self): 24 | assert_that(self.result).contains("host_name") 25 | assert_that(self.result["host_name"]).is_equal_to(platform.uname().node) 26 | 27 | def test_releaseIsAvailable(self): 28 | assert_that(self.result).contains("release") 29 | assert_that(self.result["release"]).is_equal_to(platform.uname().release) 30 | 31 | def test_architectureIsAvailable(self): 32 | assert_that(self.result).contains("architecture") 33 | assert_that(self.result["architecture"]).is_equal_to(platform.uname().machine) 34 | 35 | def test_beholderVersionIsAvailable(self): 36 | assert_that(self.result).contains("beholder_version") 37 | assert_that(self.result["beholder_version"]).is_equal_to(BEHOLDER_VERSION) 38 | 39 | def test_bootTimeIsAvailable(self): 40 | assert_that(self.result).contains("boot_time") 41 | assert_that(self.result["boot_time"]).is_equal_to(datetime.fromtimestamp(psutil.boot_time())) 42 | 43 | def test_localeIsAvailable(self): 44 | assert_that(self.result).contains("locale") 45 | assert_that(self.result["locale"]).is_equal_to(locale.getlocale()[0]) 46 | 47 | def test_encodingIsAvailable(self): 48 | assert_that(self.result).contains("encoding") 49 | assert_that(self.result["encoding"]).is_equal_to(locale.getlocale()[1]) 50 | 51 | def test_timeZoneIsAvailable(self): 52 | assert_that(self.result).contains("time_zone") 53 | assert_that(self.result["time_zone"]).is_equal_to(get_localzone().key) 54 | 55 | def test_currentTimeIsAvailable(self): 56 | assert_that(self.result).contains("current_time") 57 | assert_that(self.result["current_time"]).is_not_empty() 58 | 59 | def test_upTimeIsAvailable(self): 60 | assert_that(self.result).contains("up_time") 61 | assert_that(self.result["up_time"]).matches("[0-9]* day\\(s\\)\\, [0-9]* hour\\(s\\)* and [0-9]* minute\\(s\\)") 62 | -------------------------------------------------------------------------------- /tests/service/test_temperature_info_service.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | from assertpy import assert_that 5 | 6 | from beholder.service.temperature_info_service import TemperatureInfoService 7 | 8 | 9 | class TestTemperatureInfoServiceWithCelsius(unittest.TestCase): 10 | def setUp(self) -> None: 11 | self.service: TemperatureInfoService = TemperatureInfoService() 12 | self.result: dict[str, any] = self.service.get_info() 13 | 14 | def test_sensorNameIsAvailable(self): 15 | assert_that(self.result.keys()).is_not_empty() 16 | 17 | def test_eachSensorHasAtLeastOneLabel(self): 18 | for _sensor, readings in self.result.items(): 19 | for reading in readings: 20 | assert_that(reading).contains("sensor_label") 21 | 22 | def test_scaleIsSetToCelsiusByDefault(self): 23 | for _sensor, readings in self.result.items(): 24 | for reading in readings: 25 | assert_that(reading["scale"]).is_equal_to("C") 26 | 27 | def test_eachSensorHasTheCurrentTemperature(self): 28 | for _sensor, readings in self.result.items(): 29 | for reading in readings: 30 | assert_that(reading).contains("temperature") 31 | 32 | def test_eachSensorHasLowBand(self): 33 | for _sensor, readings in self.result.items(): 34 | for reading in readings: 35 | assert_that(reading).contains("low_band") 36 | 37 | def test_eachSensorHasMediumBand(self): 38 | for _sensor, readings in self.result.items(): 39 | for reading in readings: 40 | assert_that(reading).contains("medium_band") 41 | 42 | def test_eachSensorHasHighBand(self): 43 | for _sensor, readings in self.result.items(): 44 | for reading in readings: 45 | assert_that(reading).contains("high_band") 46 | 47 | 48 | class TestTemperatureInfoServiceWithFahrenheitScale(unittest.TestCase): 49 | def setUp(self) -> None: 50 | os.environ["BEHOLDER_TEMPERATURE_SCALE"] = "F" 51 | self.service: TemperatureInfoService = TemperatureInfoService() 52 | self.result: dict[str, any] = self.service.get_info() 53 | del os.environ["BEHOLDER_TEMPERATURE_SCALE"] 54 | 55 | def test_eachSensorHasFahrenheitAsScale(self): 56 | for _sensor, readings in self.result.items(): 57 | for reading in readings: 58 | assert_that(reading["scale"]).is_equal_to("F") 59 | --------------------------------------------------------------------------------