pax_global_header00006660000000000000000000000064147126533340014522gustar00rootroot0000000000000052 comment=09a2fab3d6c4315b876345d246349dfd4059ed50 unioslo-zabbix-cli-09a2fab/000077500000000000000000000000001471265333400157225ustar00rootroot00000000000000unioslo-zabbix-cli-09a2fab/.github/000077500000000000000000000000001471265333400172625ustar00rootroot00000000000000unioslo-zabbix-cli-09a2fab/.github/workflows/000077500000000000000000000000001471265333400213175ustar00rootroot00000000000000unioslo-zabbix-cli-09a2fab/.github/workflows/build.yml000066400000000000000000000131071471265333400231430ustar00rootroot00000000000000name: build zabbix-cli on: push: tags: - '[0-9]+.[0-9]+.[0-9]+*' concurrency: group: build-zabbix-cli-${{ github.head_ref }} jobs: build_pypi: name: Build wheels and source distribution runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install uv uses: astral-sh/setup-uv@v2 - name: Set up Python 3.12 run: uv python install 3.12 - name: Install build dependencies run: uv sync --all-extras --dev - name: Build source distribution run: uv run python -m build - uses: actions/upload-artifact@v4 with: name: pypi_artifacts path: dist/* if-no-files-found: error build_pyinstaller: name: Build pyinstaller binary strategy: matrix: os: - ubuntu-latest - windows-latest - macos-13 # only non-large x86 macOS runner image available - macos-latest include: - os: ubuntu-latest platform: linux-x86_64 container: redhat/ubi8:latest - os: windows-latest platform: win-x86_64 - os: macos-13 platform: macos-x86_64 - os: macos-latest platform: macos-arm64 python-version: - '3.12' runs-on: ${{ matrix.os }} container: image: ${{ matrix.container }} steps: - name: Install RHEL 8 dependencies if: contains(matrix.container, 'redhat/ubi8') run: dnf install -y git binutils - name: Ensure git is available run: git --version - uses: actions/checkout@v4 - name: Install uv uses: astral-sh/setup-uv@v2 - name: Set up Python ${{ matrix.python-version }} run: uv python install ${{ matrix.python-version }} - name: Install build dependencies run: uv sync --all-extras --dev - name: Build binary with PyInstaller run: uv run pyinstaller --onefile zabbix_cli/main.py --name zabbix-cli - name: Set platform binary names shell: bash run: | VERSION="${{ github.ref_name }}" BASE_NAME="zabbix-cli-${VERSION}-${{ matrix.platform }}" if [[ "${{ matrix.os }}" == "windows-latest" ]]; then echo "BINARY_NAME=${BASE_NAME}.exe" >> $GITHUB_ENV echo "SOURCE_NAME=dist/zabbix-cli.exe" >> $GITHUB_ENV else echo "BINARY_NAME=${BASE_NAME}" >> $GITHUB_ENV echo "SOURCE_NAME=dist/zabbix-cli" >> $GITHUB_ENV fi - name: Rename binary shell: bash run: mv "${{ env.SOURCE_NAME }}" "dist/${{ env.BINARY_NAME }}" - uses: actions/upload-artifact@v4 with: name: ${{ env.BINARY_NAME }} path: dist/${{ env.BINARY_NAME }} if-no-files-found: error publish_pypi: name: Publish PyPI release needs: - build_pypi runs-on: ubuntu-latest permissions: id-token: write steps: - uses: actions/download-artifact@v4 with: name: pypi_artifacts path: dist - name: Push build artifacts to PyPI uses: pypa/gh-action-pypi-publish@v1.8.14 publish_github: name: Publish GitHub release needs: - build_pypi - build_pyinstaller - publish_pypi runs-on: ubuntu-latest steps: - name: Download PyInstaller binaries uses: actions/download-artifact@v4 with: pattern: zabbix-cli-* path: dist merge-multiple: true - name: Download wheel and source distributions uses: actions/download-artifact@v4 with: pattern: pypi_artifacts path: dist merge-multiple: true - name: Generate SHA256 checksums id: sha run: | cd dist echo "checksums<> $GITHUB_OUTPUT sha256sum * >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT - name: Create GitHub release uses: softprops/action-gh-release@v2 with: files: dist/* body: | Release ${{ github.ref_name }} ## Binary Downloads Platform | Architecture | Download ---------|--------------|---------- Linux | x86_64 | [zabbix-cli-${{ github.ref_name }}-linux-x86_64](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/zabbix-cli-${{ github.ref_name }}-linux-x86_64) Windows | x86_64 | [zabbix-cli-${{ github.ref_name }}-win-x86_64.exe](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/zabbix-cli-${{ github.ref_name }}-win-x86_64.exe) macOS | x86_64 | [zabbix-cli-${{ github.ref_name }}-macos-x86_64](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/zabbix-cli-${{ github.ref_name }}-macos-x86_64) macOS | ARM64 | [zabbix-cli-${{ github.ref_name }}-macos-arm64](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/zabbix-cli-${{ github.ref_name }}-macos-arm64) ## PyPI Package ### uv ```bash uv tool install zabbix-cli-uio==${{ github.ref_name }} ``` ### pipx ```bash pipx install zabbix-cli-uio==${{ github.ref_name }} ``` ### pip ```bash pip install zabbix-cli-uio==${{ github.ref_name }} ``` ## SHA256 Checksums ``` ${{ steps.sha.outputs.checksums }} ``` draft: false prerelease: false unioslo-zabbix-cli-09a2fab/.github/workflows/docs.yml000066400000000000000000000012701471265333400227720ustar00rootroot00000000000000name: build-docs on: push: branches: - master - docs-dev paths: - "docs/**" - "mkdocs.yml" - ".github/workflows/docs.yml" - "pyproject.toml" - "zabbix_cli/**" concurrency: group: docs-deploy jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.12" - name: Install uv uses: astral-sh/setup-uv@v2 - name: Install hatch run: | uv pip install --system hatch - name: Build documentation and publish run: hatch run docs:mkdocs gh-deploy --force unioslo-zabbix-cli-09a2fab/.github/workflows/test.yml000066400000000000000000000013451471265333400230240ustar00rootroot00000000000000on: push: paths-ignore: - 'docs/**' - 'debian/**' - 'rpm/**' - 'README.md' pull_request: name: CI jobs: test: name: Test runs-on: ubuntu-latest strategy: matrix: python-version: - '3.8' - '3.9' - '3.10' - '3.11' - '3.12' steps: - name: Checkout uses: actions/checkout@v4 - name: Install Python uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install uv uses: astral-sh/setup-uv@v2 - name: Install dependencies run: | uv sync --all-extras --dev - name: Test run: uv run pytest -vv tests unioslo-zabbix-cli-09a2fab/.gitignore000066400000000000000000000004741471265333400177170ustar00rootroot00000000000000*.pyc # Build files dist/ build/ *.egg-info/ .coverage .venv/ venv/ .dccache .vscode/launch.json my_tests/ # Hatch Pyapp pyapp # Dev commands and directories zabbix_cli/commands/_dev.py dev/ # Auto-generated docs files docs/guide/commands/* !docs/guide/commands/index.md docs/data site/ # Pyinstaller *.spec unioslo-zabbix-cli-09a2fab/.pre-commit-config.yaml000066400000000000000000000014531471265333400222060ustar00rootroot00000000000000# See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml - id: check-added-large-files - repo: https://github.com/asottile/pyupgrade rev: v3.19.0 hooks: - id: pyupgrade args: [--py37-plus, --keep-runtime-typing] - repo: https://github.com/charliermarsh/ruff-pre-commit rev: "v0.7.0" hooks: # Run the linter. - id: ruff args: [--fix] # Run the formatter. - id: ruff-format - repo: https://github.com/RobertCraigie/pyright-python rev: v1.1.386 hooks: - id: pyright exclude: ^(tests|scripts|docs)/ unioslo-zabbix-cli-09a2fab/AUTHORS000066400000000000000000000010311471265333400167650ustar00rootroot00000000000000# Based on `git shortlog --all -es` with duplicates removed Alexandru Tică Andreas Dobloug Boris Manojlovic Carl Morten Boger Emanuele Borin Fabian Arrotin Fabian Stelzer Florian Tham Fredrik Larsen Ganesh Hegde Herdir Jarle Bjørgeengen Jean-Baptiste Denis Jelmer Vernooij Kim Rioux-Paradis Logan V Marius Bakke Mathieu Marleix Michael Gindonis Mustafa Ocak Mélissa Bertin Paal Braathen Peder Hovdan Andresen Peet Whittaker Petter Reinholdtsen Rafael Martinez Guerrero Retyunskikh Dmitriy Steve McDuff Terje Kvernes Volker Fröhlich unioslo-zabbix-cli-09a2fab/CHANGELOG000066400000000000000000000331571471265333400171450ustar00rootroot00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [3.3.0] ### Added - New configuration file table `[app.output]`: - `format`: Default output format for commands. Defaults to `"table"`. - `color`: Enable or disable color output. Defaults to `true`. - `paging`: Enable or disable paging of output. Defaults to `false`. - `theme`: Color theme for output. Defaults to `"default"`. - Application now automatically assigns deprecated config options to their new equivalents internally. - New command `update_config` to update an outdated configuration file with new options, as well as any currently applied overrides. - `show_config --secrets ` option for controlling the display mode of sensitive information in the configuration file. Defaults to `mask`. - New command `update_host` to update basic information about a host. - New command `show_proxy_hosts` to show hosts monitored by a given proxy. ### Changed - Custom auth (token) file paths in config now take precedence over the default path if both exist. - Application now prompts for Zabbix API URL if missing from config. - Default logging configuration is performed before loading the configuration file. Ensures a default logging configuration is always present. - Authentication method + source is now logged on successful authentication. - No longer attempts to add a user to the logging context when logging in with an auth token. - Require at least one option to be set for `update_*` commands. Previously, these command would state that the resource was updated even if no changes were made. - Command `add_proxy_to_group` no longer requires a local address and port argument. If not provided, the application attempts to use the proxy's `local_address` and `local_port` fields. If the proxy does not have these fields, the command fails. ### Deprecated - Config options moved to `[app.output]` table: - `app.use_colors` → `app.output.color` - `app.use_paging` → `app.output.paging` - `app.output_format` → `app.output.format` ## [3.2.0] ### Added - Configurable error handling modes for bulk mode under `app.bulk_mode`: - `strict`: Stop on first error. - `continue`: Continue on command execution error, report at the end. - `ignore`: Ignore all errors, including command file parsing errors. - REPL autocompletion for enums and paths. - Auto completion for `export_configuration --directory` argument. ### Fixed - Screen flickering on application startup when not authenticating via username and password prompt. - `define_host_usermacro` not working as expected when using a macro name that already exists elsewhere. ## [3.1.3] ### Fixed - Empty macro names not throwing an error in macro commands. - Pyinstaller built binary on certain Linux versions. ## 3.1.2 ### Changed - Reduced source distribution size by excluding unnecessary files. ## 3.1.1 ### Added - Publish to PyPI. ## 3.1.0 ### Added - Plugin support. See the [plugins documentation](https://unioslo.github.io/zabbix-cli/plugins/) for more information. ### Changed - `--config` now always creates the config file at the given location if it doesn't exist. - `show_config` now shows the absolute path to the active configuration file. ## 3.0.3 ### Added - `--limit` option for `show_*` commands to limit the number of results shown: - `show_usermacro_host_list` - `show_usermacro_template_list` - `show_maintenance_periods` ### Changed - `show_host_usermacros` rendering of `automatic` field. - Now shows a human readable string instead of `0` or `1`. - Example formatting. - Hide defaults for required positional arguments. - `show_dirs` and `init` no longer requires logging in to the Zabbix API or an existing configuration file. - Log record format: - No longer includes the process ID. - Now includes filename, line number and function name. - Rich markup is no longer included in log messages. - Accessing the config when it is not loaded now uses the same sample config as `sample_config` instead of raising an exception. ### Fixed - `show_usermacro_host_list` not showing all hosts with the given macro. - `show_usermacro_template_list` not showing all templates with the given macro. - Auth token file using username from config file instead of from prompt. ## 3.0.2 ### Added - `show_hosts`: `--hostgroup` option for filtering by host group names or IDs. - `show_last_values`: ` Item ID filtering. - `show_usergroup`: Group ID filtering. - `show_usergroups`: Group name or ID filtering. - `show_users`: `--sort` option for sorting results by a field. - Status messages when fetching data from the Zabbix API in most `show_*` commands. - `--limit` option for most `show_*` commands to limit the number of results shown. - Environment variable `ZABBIX_API_TOKEN` for logging in with an API token. ### Fixed - Markup errors when rendering Zabbix items with keys containing special characters. - Environment variables not matching V2 names. - Before: `ZABBIX_CLI_USERNAME`, `ZABBIX_CLI_PASSWORD` - After: `ZABBIX_USERNAME`, `ZABBIX_PASSWORD` ## 3.0.1 ### Changed - `migrate_config` no longer requires logging in to the Zabbix API. ### Fixed - `migrate_config` not migrating username to the new `api.username` field in the resulting TOML configuration file. - `migrate_config` using `legacy_json_format = true` in the resulting TOML configuration file by default. - Can force the old JSON format with the new `--legacy-json-format` flag. ## 3.0.0 ### Added - New CLI powered by `typer` and `click-repl` - Shell autocompletion - TOML configuration file support - Old configuration format is deprecated. - Usage examples for most commands. - **New configuration options:** - `app.default_format`: Sets the default CLI output format. Defaults to `table`. - `app.legacy_json_format`: Enables the old JSON output format. Defaults to `false`. - **New commands:** - `add_proxy_to_group`: Add a proxy to a proxy group. - `create_templategroup`: Create a template group. - `extend_hostgroup`: Add all hosts from a host group to other host group(s) without removing them from the original group. - `extend_templategroup`: Add all templates from a group to other group(s) without removing them from the original group. - `init`: Initialize the CLI configuration file. - `link_template_to_template`: Link template(s) to template(s). - `move_hosts`: Move all hosts from one host group to another. - `move_templates`: Move all templates from one group to another. - `open`: Open a CLI directory in the system's file manager. - `remove_hostgroup`: Delete a host group. - `remove_host_interface`: Delete a host interface. - `remove_proxy_from_group`: Remove a proxy from a proxy group. - `remove_templategroup`: Delete a template group. - `show_dirs`: Show directories used by the CLI. - `show_host_interfaces`: Show interfaces for a host. - `show_media_types`: Show media types. - `show_proxies`: Show proxies. - `show_proxy_groups`: Show proxy groups. - `show_proxy_group_hosts`: Show hosts in a proxy group. - `show_templategroup`: Show a single template group. - `show_templategroups`: Show all template groups. - `show_user`: Show details for a single user. - `unlink_template_from_template`: Unlink template(s) from template(s). - `update_host_interface`: Update a host interface. - `update_user`: Update a user. - `update_hostgroup_proxy`: Assign a proxy to all hosts in one or more host groups. - `update_hostgroup_proxygroup`: Assign a proxy group to all hosts in one or more host groups. - **New command options:** - `add_host_to_hostgroup`: - `--dryrun`: Preview changes without making them. - `create_host`: - `--name`: Host name - Host name still defaults to host DNS name or IP address if not specified. - `--description`: Host description - `create_hostgroup`: - `--rw-groups`: User groups to give RW permissions to the host group. - `--ro-groups`: User groups to give RO permissions to the host group. Uses groups from config file if not specified. - `--no-usergroup-permissions`: Do not set user group permissions. Defaults to `false`. - `create_host_interface`: - `--snmp...` TODO - `import_configuration`: - `--dryrun`: Preview files to import. - `--delete-missing`: Delete objects not found in the import file(s). - `link_template_to_host`: - `--dryrun`: Preview changes without making them. - `remove_host_from_hostgroup`: - `--dryrun`: Preview changes without making them. - `show_host`: - `--monitored/--unmonitored`: Filter by monitored status - `--maintenance/--no-maintenance`: Filter by maintenance status - `--active [available | unavailable | unknown ]`: Filter by active interface availability - Old positional filter argument syntax is deprecated. - `show_hosts`: - `--limit`: Limit number of hosts to show. - As well as the new `show_host` options. - `show_hostgroup`: - `--hosts/--no-hosts`: Show hosts in the group - `show_hostgroups`: - `--hosts/--no-hosts`: Show hosts in the group - `show_trigger_events` - `--trigger-id`: Trigger ID(s) to get events for. - Corresponds to old positional argument 1. - `--host`: Host(s) to get events for. - `--hostgroup`: Host group(s) to get events for. - `--limit`: Limit number of events to show - Corresponds to old positional argument 2. - Defaults to 10 (was 1). - `show_usergroup`: - `--sort`: Sort results by a field. - `show_usergroups`: - `--sort`: Sort results by a field. - `show_usergroup_permissions`: - `--sort`: Sort results by a field. - `unlink_template_from_host`: - `--dryrun`: Preview changes. - `update_host_proxy`: - `--dryrun`: Preview changes. - **New command arguments:** - `show_templates`: - `template_names`: Template name(s) to filter by. Shows all templates by default. Supports wildcards. - `show_hostgroup`: - `name`: Host group name(s) to filter by. Shows all host groups by default. Supports wildcards. ### Changed - Commands now take named options instead of positional arguments. - Positional arguments are deprecated. - JSON output is no longer always a dict with numeric string keys. - See V3 migration guide for more information. - The old format can be enabled with the new option `app.legacy_json_format` in the new TOML configuration file. - When loading configuration from a legacy `.conf` file, the old format is assumed. - **TOML configuration file option names:** - Table [zabbix_api] → [api] - `zabbix_api_url` → `url` - `cert_verify` → `verify_ssl` - Table [zabbix_config] → [app] - `system_id` → `username` - `default_directory_exports` → `export_directory` - `default_export_format` → `export_format` - `include_timestamp_export_filename` → `export_timestamps` - `allow_insecure_authfile` → `allow_insecure_auth_file` - `logging.logging` → `logging.enabled` - The original names are deprecated and slated for removal in a future version. - **Configuration file defaults:** - `app.default_admin_usergroups` defaults to `[]` (empty list) - `app.default_create_user_usergroups` defaults to `[]` (empty list) - `app.export_timestamps` defaults to `false` - Exports are automatically overwritten if the file already exists. - `app.export_format` defaults to `json` (was `xml`) - Application now creates a config file on launch if it doesn't exist. - **Command changes:** - `create_host_interface` - Default port number is now determined by interface type. - Agent: 10050 - SNMP: 161 - IPMI: 623 - JMX: 12345 - `define_host_monitoring_status`: - Renamed to `monitor_host`. - `link_template_to_hostgroup`: - Renamed to `add_template_to_group`. - `show_host_inventory` - Now shows shows any inventory field that is set for the host in the table output. - Includes _all_ inventory fields in JSON output. - `show_hostgroup_permissions`: - Shows permissions for all host groups by default. - `show_proxies`: - Now takes a `name_or_id` argument to filter by proxy name or ID. Comma-separated. Supports wildcards. - `show_zabbixcli_config`: - Renamed to `show_config`. - `unlink_template_from_hostgroup`: - Renamed to `remove_template_from_group`. - No longer unlinks and clears templates from each other. - This was a bug/misunderstanding of the Zabbix API in the old version. - Use `unlink_template_from_template` to unlink and clear templates from each other. - `update_host_proxy` - Now supports setting proxy for multiple hosts at once using wildcards. - Output format is changed. - Now groups hosts by proxy prior to update. ### Deprecated - `zabbix-cli.conf` format. Prefer the new TOML configuration file format. - Config file options: - `zabbix_config.system_id` → `api.username` - **Commands:** - `unlink_template_from_hostgroup`: - Renamed to `remove_template_from_group`. - `define_host_monitoring_status`: - Renamed to `monitor_host` - `show_zabbixcli_config`: - Renamed to `show_config` - `zabbix-cli-init` script. - Replaced by `zabbix-cli init` command. - `zabbix-cli-bulk-execution` script. - Replaced by `zabbix-cli --file`. ### Removed - Support for Zabbix <1.8 login using `user.authenticate`. ### Internal - Use Hatch for building and publishing. - Switch from setup.py to pyproject.toml. - Add `pre-commit` hooks. - Add `pytest` tests - Use Ruff for linting and formatting. - Use Pyright for static type checking. - API code rewritten with Pydantic data models. unioslo-zabbix-cli-09a2fab/LICENSE000066400000000000000000001044611471265333400167350ustar00rootroot00000000000000GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. {one line to give the program's name and a brief idea of what it does.} Copyright (C) {year} {name of author} This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: {project} Copyright (C) {year} {fullname} This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . unioslo-zabbix-cli-09a2fab/README.md000066400000000000000000000253511471265333400172070ustar00rootroot00000000000000# Zabbix-cli [![PyPI](https://img.shields.io/pypi/v/zabbix-cli-uio)](https://pypi.org/project/zabbix-cli-uio/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/zabbix-cli-uio)]() [![PyPI - License](https://img.shields.io/pypi/l/zabbix-cli-uio)]() ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/unioslo/zabbix-cli/test.yml?branch=master&label=tests)

**Zabbix-CLI v3 has been completely rewritten from the ground up. The old version can be found [here](https://github.com/unioslo/zabbix-cli/tree/2.3.2).** ## About Zabbix-cli is a command line interface for performing common administrative tasks tasks in [Zabbix monitoring system](https://www.zabbix.com/) via the [Zabbix API](https://www.zabbix.com/documentation/current/en/manual/api). The zabbix-cli code is written in [Python](https://www.python.org/) and distributed under the GNU General Public License v3. It has been developed and tested by [University Center for Information Technology](https://www.usit.uio.no/) at [the University of Oslo, Norway](https://www.uio.no/). The project home page is on [GitHub](https://github.com/unioslo/zabbix-cli). Please report any issues or improvements there. The manual is available online at . ## Install ### From source > [!NOTE] > We are in the process of acquiring the name `zabbix-cli` on PyPI. Until then, installation must be done via the mirror package `zabbix-cli-uio`. #### [uv](https://docs.astral.sh/uv/getting-started/installation/) ```bash uv tool install zabbix-cli-uio ``` #### [uvx](https://docs.astral.sh/uv/#tool-management) ```bash uvx --from zabbix-cli-uio zabbix-cli ``` #### [pipx](https://pipx.pypa.io/stable/) ```bash pipx install zabbix-cli-uio ``` ### Homebrew A homebrew package exists, but it is maintained by a third party. It can be installed with: ```bash brew install zabbix-cli ``` ### Binary Binaries built with PyInstaller can be found on the [releases page](https://github.com/unioslo/zabbix-cli/releases). We build binaries for Linux (x86), macOS (ARM & x86) and Windows (x86) for each release. ## Quick start Running `zabbix-cli` for the first time will prompt for a Zabbix URL, username and password. The URL should be the URL of the Zabbix web server without the `/api_jsonrpc.php` path. Running without arguments will start the REPL: ```bash zabbix-cli ``` ## Usage Zabbix-cli is a command line interface for Zabbix. It can be used in three ways: 1. **Interactive mode**: Start the REPL by running `zabbix-cli`. This will start a shell where you can run multiple commands in a persistent session. 2. **Single command**: Run a single command by running `zabbix-cli COMMAND`. This will run the command and print the output. 3. **Batch mode**: Run multiple commands from a file by running `zabbix-cli -f FILE`. The file should contain one command per line. Command reference can be found in the [online user guide](https://unioslo.github.io/zabbix-cli/guide/commands/) or by running `zabbix-cli --help`. ### Authentication By default, the application will prompt for a username and password. Once authenticated, the application stores the session token in a file for future use. For more information about the various authentication methods, see the [authentication guide](https://unioslo.github.io/zabbix-cli/guide/authentication/). ### Configuration Zabbix-cli needs a config file. It is created when the application is started for the first time. The config file can be created manually with the `init` command: ```bash zabbix-cli init --zabbix-url https://zabbix.example.com/ ``` For more detailed information about the configuration file, see the [configuration guide](https://unioslo.github.io/zabbix-cli/guide/configuration/). ### Formats Zabbix-cli supports two output formats: table and JSON. The default format is table, but it can be changed with the `--format` parameter: ```bash # Show hosts in table format (default) zabbix-cli show_hosts # Show hosts in JSON format zabbix-cli --format json show_hosts # Set format in REPL mode > --format json show_hosts ``` The default format can be configured with the `app.output.format` config option: ```toml [app.output] format = "json" ``` #### Table format-table The default rendering mode is a [Rich](https://github.com/Textualize/rich) table that adapts to the width of the terminal. #### JSON format-json The JSON output format is always in this format, where `ResultT` is the expected result type: ```json { "message": "", "errors": [], "return_code": "Done", "result": ResultT } ``` The type of the `result` field varies based on the command run. For `show_host` it is a single Host object, while for `show_hosts` it is an _array_ of Host objects.
show_host foo.example.com* ```json { "message": "", "errors": [], "return_code": "Done", "result": { "hostid": "10648", "host": "foo.example.com", "description": "", "groups": [ { "groupid": "22", "name": "All-hosts", "hosts": [], "flags": 0, "internal": null, "templates": [] }, { "groupid": "46", "name": "Source-foosource", "hosts": [], "flags": 0, "internal": null, "templates": [] }, { "groupid": "47", "name": "Hostgroup-bob-hosts", "hosts": [], "flags": 0, "internal": null, "templates": [] }, { "groupid": "48", "name": "Importance-X", "hosts": [], "flags": 0, "internal": null, "templates": [] }, { "groupid": "49", "name": "Hostgroup-alice-hosts", "hosts": [], "flags": 0, "internal": null, "templates": [] } ], "templates": [], "inventory": {}, "monitored_by": "proxy", "proxyid": "2", "proxy_groupid": "0", "maintenance_status": "0", "active_available": "0", "status": "0", "macros": [], "interfaces": [ { "type": 1, "ip": "", "dns": "foo.example.com", "port": "10050", "useip": 0, "main": 1, "interfaceid": "49", "available": 0, "hostid": "10648", "bulk": null, "connection_mode": "Dns", "type_str": "Agent" } ], "proxy": { "proxyid": "2", "name": "proxy-prod02.example.com", "hosts": [], "status": null, "operating_mode": 0, "address": "127.0.0.1", "proxy_groupid": "1", "compatibility": 0, "version": 0, "local_address": "192.168.0.1", "local_port": "10051", "mode": "Active", "compatibility_str": "Undefined" }, "zabbix_agent": "Unknown" } } ```
show_hosts foo.* ```json { "message": "", "errors": [], "return_code": "Done", "result": [ { "hostid": "10648", "host": "foo.example.com", "description": "", "groups": [ { "groupid": "22", "name": "All-hosts", "hosts": [], "flags": 0, "internal": null, "templates": [] }, { "groupid": "46", "name": "Source-foosource", "hosts": [], "flags": 0, "internal": null, "templates": [] }, { "groupid": "47", "name": "Hostgroup-bob-hosts", "hosts": [], "flags": 0, "internal": null, "templates": [] }, { "groupid": "48", "name": "Importance-X", "hosts": [], "flags": 0, "internal": null, "templates": [] }, { "groupid": "49", "name": "Hostgroup-alice-hosts", "hosts": [], "flags": 0, "internal": null, "templates": [] } ], "templates": [], "inventory": {}, "monitored_by": "proxy", "proxyid": "2", "proxy_groupid": "0", "maintenance_status": "0", "active_available": "0", "status": "0", "macros": [], "interfaces": [], "proxy": { "proxyid": "2", "name": "proxy-prod02.example.com", "hosts": [], "status": null, "operating_mode": 0, "address": "127.0.0.1", "proxy_groupid": "1", "compatibility": 0, "version": 0, "local_address": "192.168.0.1", "local_port": "10051", "mode": "Active", "compatibility_str": "Undefined" }, "zabbix_agent": "Unknown" } ] } ```
## Development Zabbix-cli currently uses [Hatch](https://hatch.pypa.io/latest/) for project management and packaging. To start off, clone the repository: ```bash git clone https://github.com/unioslo/zabbix-cli.git ``` Then make a virtual environment using Hatch: ```bash hatch shell ``` This will create a new virtual environment, install the required dependencies and enter the environment. If you do not wish to use Hatch, you can create a virtual environment manually: ```bash python -m venv .venv source .venv/bin/activate pip install -U -e ".[test]" ``` ### Testing Run unit tests (without coverage): ```bash hatch run test ``` Generate coverage report: ```bash hatch run cov ``` ### Documentation To serve the documentation locally: ```bash hatch run docs:serve ``` This will start a local web server on `http://localhost:8001` that is automatically refreshed when you make changes to the documentation. However, some hooks are only run on startup, such as the creation of pages for each command. Changes to command examples or docstrings will require a restart. unioslo-zabbix-cli-09a2fab/TODO000066400000000000000000000261751471265333400164250ustar00rootroot00000000000000* Configure file logging for scripts as well??? # IMPORTANT * add_host_to_hostgroup & remove_host_from_hostgroup should probably take 2 positional arguments as opposed to 2 options. The current implementation is over-complicated. * Auto relogin + custom error when this happens: ``` ✗ ERROR: Failed to get all host groups: ("Error -32602: Invalid params.: Session terminated, re-login, please. while sending {'jsonrpc': '2.0', 'method': 'hostgroup.get', 'params': {'output': 'extend', 'selectHosts': 'extend', 'sortorder': 'ASC', 'sortfield': 'name'}, 'id': 3, 'auth': '1234abc'}", -32602) ``` THIS WILL NOT BE CAUGHT WHEN A METHOD RE-RAISES ZabbixAPICallError for some reason! * Add type checking method for return value of API calls. E.g. `update` and `massupdate` should return a dict with a key called something like `hostids` containing a list of updated hosts. We should have a method that can be called something like this: ```python ret = self.host.massupdate(...) updated = check_return(ret, "hostids") reveal_type(updated) # List[str] ``` ---------------- ## API * Add some sort of `process_params` method that can be used to process params and add them to the ParamsType mapping, to reduce code duplication within each method. * Add support for both names and IDs in certain API methods. This adds a bunch of complexity, but is required to support some V2 features. Drop them? * Move all APIStrEnum classes into pyzabbix. Add `enums` module to pyzabbix? * Wrap most POST/UPDATE/DELETEs in a try/except block that catches ZabbixAPIExceptions and re-raises it with a more understandable error message. * Some methods already do this, but not all. * Wrap GETs in try/except as well. About half of the methods do this... Very inconsistent. * Some sort of type checking of API results. * If we expect a list, we should have a function that does the appropriate isinstance checks, and possibly also converts each item in the list to the correct type. * Similarly, each response is likely a dict of some sort, but we should ensure it actually is a dict before we try to access it. * Log all API calls and responses. * Successful calls should be logged at INFO level. * I.e. `logger.info("Acknowledged event: %s. Closed: %s", event_id, close)` * Failures that raise exceptions don't need to log anything (they are logged by the exception handler). * In debug mode, we can also log the request and response. * This will help us debug issues with the API. ### Utils Use APIStr enums in functions that take in a code and return a string representation. That way we don't create duplicate definitions for the same objects/concepts/choices. ## Args * Hide positional args from help? * Consistent hostname vs hostname_or_id arguments ## Auth * Add configurable auth token file location. Requires a minor refactoring of functions in `auth.py` to locate the custom file. Also might not be possible to automatically clear the file when we encounter an error with an exipred token. ## Bulk / -C mode * Add timeout to commands when running in bulk mode in case command prompts for input. * Populate cache when in bulk mode. * Automatically enable headless mode when running in bulk mode. ## Cache * Implement caching (do we need it?) * If implememented: * Add option to enable/disable cache. * Both in REPL mode and in bulk mode. * Add command to clear cache. ## Commands * Add command aliases: * Two options: * Add new name `show_host` -> `host-show`, hide old name. (easy) * Need some sort of decorator like @app.old_name("show_host") * Registers the command with hidden=True * Adds `Old name: show_host` to the bottom of the help text. * This lets us traverse all the commands and create a mapping of old to new names, as well as providing it in the help text. * Add new sub-apps a la Harbor CLI: `show_host` -> `host show` (hard) * Requires a bigger refactoring and is semantically different from the current set up. * Sort command categories by help panel names. * Sorting order seems to be alphabetical BUT BASED ON THE COMMAND NAMES. So if one category has a command that starts with "A" and another category has a command that starts with "B", then the category with the "A" command will be listed first, regardless of the category name. Why... * `show_template` and `show_templates`: * Remove one of the commands and set the default to `*` to the other command, so we mimick the old behavior. Since we can filter names with both of them, the only difference is whether we show all results or just one (meaningless distinction). * `show_alarms` * Color severity levels. * `remove_template_from_group`: * Add `--dryrun` option. ## Config * Catch when users use deprecated names and warn them. Also warn when we load a .conf config. * Add support for .fixed.conf files * Add loading config from multiple sources. Each new config adds to the existing config and overrides values, like how it was in v2. * Use `ctx.config_map` to set command param defaults from config. * Lets us set defaults for all commands in one place. * * ## Documentation * Convert all docstrings to google docstring style. ### Examples * Add examples to all commands. * Render examples in documentation. Extract rich-formatted text and render it as markdown. * Append `zabbix-cli` to the beginning of each example (we omit this in the app since the REPL is the primary use case). Should we? * Define examples in own mapping in a separate module. This module is not imported on runtime and is only used when --help is invoked. The command looks up its own examples based on name. I.e. ```python EXAMPLES = { "show_host": [ Example( "Show a host by name", "show_host foo.example.com" ), Example( "Show first host with a name that starts with 'foo'", "show_host 'foo*'" ) ] } ``` This way we can have an arbitrary number of examples without impacting startup time. Need to override the the way the help is looked up to make this work. ## Exceptions ### Zabbix API Exceptions * Store error code and message in ZabbixAPIException, so that we can automatically add it to the error message when we write custom ZabbixAPIExceptions. * **VERY VERY IMPORTANT:** If we raise ZabbixAPIException from a ZabbixAPIException, we should be able to extract the data from the original exception (`__cause__`) and add it to whatever we are printing. * Don't log traceback of "expected" errors such as `ZabbixNotFound`. * Perhaps add some sort of base class we handle for this such as `ZabbixStatusError`? * Handle connection errors more gracefully. Example: if we try to connect to UIO Zabbix without SOCKS proxy, we get a `HTTPStatusError` with a 403 status code. We should catch this and raise a more meaningful error. ``` HTTPStatusError: Client error '403 Forbidden' for url 'https://example.com/api_jsonrpc.php' For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403 ``` ### Pydantic Exceptions * Log all data and API version whenever we get a validation error. * This will help us debug issues with the API. * We should also log the full request and response. * Add a `--debug` flag that enables debug logging??? ## Hosts * Go through all `get_hosts()` calls. Make sure we only search for one term (why?). * Show templates and proxies in `show_host` and `show_hosts` ## Logging * Document that setting `log_file = ""` causes logs to be sent to stderr. * This is not recommended, but useful for real-time debugging. * Maybe toggle normal console messages when this is set? * Either only use root logger OR add a logger for each module. * Currently we have a mix of both. * Log record markup normalizer. Remove Rich formattings from log records. ## Prompts * Remove most prompts * Huge offenders: * `_handle_hostgroup_args` * `_handle_templategroup_args` * `_handle_template_arg` * Turn some text prompts into arrow key selection prompts: * `create_notification_user`: `mediatype` * Fetch all media types then display them in a list. ## Rendering * [ ] Turn `ColsRowsType` into some sort of BaseModel type that validates that row length matches cols length. * Rewrite `TableRenderable.__cols_rows__` to use this new type. * **Performance implications?** * Add some sort of converting of iterable values to newline separated strings in `__rows__` * Prevent "no results found" from being printed when we are trying to render the result of a create/modify action that created/modified 0 objects. * Example: `add_host_to_hostgroup` when host is already in the host group. ## Legacy * Assume legacy JSON format when loading from .conf file. * Render some names as legacy names when using legacy JSON format. * E.g. `name` instead of `macro` for `show_host_usermacros` ## Serialization * Always serialize string representations of codes (`Maintenance (0)`) * We do not strive for 1:1 compatibility with the Zabbix API. It's more important that users can read the output. * We need to fix `Host` to convert to string representations instead of codes. ## scripts * Some sort of shared callback function between all scripts and the main CLI. This way we can pass in config etc, and have a common way of handling errors etc. ## Templates * Remove `--strict` option. Adds complexity without much extra value. Users can use `--dryrun` to ensure that the command will work as expected. ## Tests * Choice enum mappings contain all choices. * I.e. `InterfaceConnectionMode` & `InterfaceType` -------------------- ## Repo ### Run pyupgrade on all files not referenced by Typer We only need runtime typing on the functions that typer use for type inference. Modules such as Pyzabbix can be upgraded to use modern typing features such as built-in generics (3.9) and | (3.10). -------------------- ### Imports * [x] Reduce number of cross-imports * [x] Perform more inline imports * [x] Define config model in separate file that is not imported on startup. * [x] Profile using py-spy. Preliminary tests show that the majority of the slowdowns are from defining Pydantic models on runtime. -------------------- ## New command ideas ### Assign unproxied hosts to proxies: `assign_host_to_proxy` * Allow for pattern matching in host names. * E.g. `assign_host_to_proxy "proxy-prod01.example.com" "*.example.com"` ### Assign all hosts in host group to proxy: `assign_hostgroup_to_proxy` * Allow for pattern matching in host names. * E.g. `assign_hostgroup_to_proxy "proxy-prod01.example.com" "Siteadmin-iti-*"` ### Show help for a command or category: `help` Usage: * `zabbix-cli help host` * `zabbix-cli help show_host` ### Details about a specific host interface: `show_host_interface` Usage: * `zabbix-cli show_host_interface ` * Shows more details about the interface than `show_host_interfaces` ### Set a host interface as default (unsetting other as default): `set_default_host_interface` Basically, when we create a new host interface of a type we already have default for, we cannot unset the old default. This command allows us to do that. ### Create template: `create_template` .... unioslo-zabbix-cli-09a2fab/bin/000077500000000000000000000000001471265333400164725ustar00rootroot00000000000000unioslo-zabbix-cli-09a2fab/bin/zabbix-cli000077500000000000000000000010501471265333400204400ustar00rootroot00000000000000#!/usr/bin/env python3.12 """Run script for pyinstaller. Create a standalone executable as: pyinstaller --name zabbix-cli bin/zabbix-cli -F --hidden-import=zabbix_cli.app The finished binary will be in the `dist` directory. The hidden import is required to include the host submodules in the executable, due to using dynamic imports. """ from __future__ import annotations import re import sys from zabbix_cli.main import main if __name__ == "__main__": sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0]) sys.exit(main()) unioslo-zabbix-cli-09a2fab/bin/zabbix-cli-bulk-execution000077500000000000000000000010711471265333400233770ustar00rootroot00000000000000#!/usr/bin/env python3.12 """Run script for pyinstaller. Create a standalone executable as: pyinstaller --name zabbix-cli-bulk-execution bin/zabbix-cli-bulk-execution -F The finished binary will be in the `dist` directory. The hidden import is required to include the host submodules in the executable, due to using dynamic imports. """ from __future__ import annotations import re import sys from zabbix_cli.scripts.bulk_execution import main if __name__ == "__main__": sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0]) sys.exit(main()) unioslo-zabbix-cli-09a2fab/bin/zabbix-cli-init000077500000000000000000000010331471265333400214020ustar00rootroot00000000000000#!/usr/bin/env python3.12 """Run script for pyinstaller. Create a standalone executable as: pyinstaller --name zabbix-cli-init bin/zabbix-cli-init -F The finished binary will be in the `dist` directory. The hidden import is required to include the host submodules in the executable, due to using dynamic imports. """ from __future__ import annotations import re import sys from zabbix_cli.scripts.init import main if __name__ == "__main__": sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0]) sys.exit(main()) unioslo-zabbix-cli-09a2fab/docs/000077500000000000000000000000001471265333400166525ustar00rootroot00000000000000unioslo-zabbix-cli-09a2fab/docs/.includes/000077500000000000000000000000001471265333400205365ustar00rootroot00000000000000unioslo-zabbix-cli-09a2fab/docs/.includes/admonition-pypi.md000066400000000000000000000002511471265333400241760ustar00rootroot00000000000000!!! note "PyPI package name" We are in the process of acquiring the PyPI project `zabbix-cli`. Until then, installation must be done via the alias `zabbix-cli-uio`. unioslo-zabbix-cli-09a2fab/docs/.includes/config-locations.md000066400000000000000000000007331471265333400243210ustar00rootroot00000000000000=== "Linux" - `./zabbix-cli/zabbix-cli.toml` - `$XDG_CONFIG_HOME/zabbix-cli/zabbix-cli.toml` - `$XDG_CONFIG_DIRS/zabbix-cli/zabbix-cli.toml` === "macOS" - `./zabbix-cli/zabbix-cli.toml` - `~/Library/Application Support/zabbix-cli/zabbix-cli.toml` - `~/Library/Preferences/zabbix-cli/zabbix-cli.toml` === "Windows" - `.\zabbix-cli\zabbix-cli.toml` - `%LOCALAPPDATA%\zabbix-cli\zabbix-cli.toml` - `%APPDATA%\zabbix-cli\zabbix-cli.toml` unioslo-zabbix-cli-09a2fab/docs/.includes/pipx-multiple.md000066400000000000000000000010351471265333400236700ustar00rootroot00000000000000pipx supports installing multiple versions of the same package by giving each installation a custom suffix. For example, if we have an existing installation of Zabbix CLI, and we wish to install a newer version of Zabbix CLI without shadowing or overwriting the existing installation, we can do so: ```bash pipx install zabbix-cli>=3.0.0 --suffix @v3 ``` This installs Zabbix CLI >= 3.0.0 with the suffix `@v3`, and we can run it with: ```bash zabbix-cli@v3 ``` and the existing installation can be run as usual: ```bash zabbix-cli ``` unioslo-zabbix-cli-09a2fab/docs/.includes/quick-install.md000066400000000000000000000025241471265333400236430ustar00rootroot00000000000000=== "uv" Install with [`uv`](https://docs.astral.sh/uv/getting-started/installation/) to avoid conflicts with other Python packages in your system: ```bash uv tool install zabbix-cli-uio ``` To try out Zabbix-CLI without installing it, run it directly with [`uvx`](https://docs.astral.sh/uv/#tool-management): ```bash uvx --from zabbix-cli-uio zabbix-cli ``` {% include-markdown ".includes/admonition-pypi.md" %} === "pipx" Install with [`pipx`](https://pipx.pypa.io/stable/) to avoid conflicts with other Python packages in your system: ```bash pipx install zabbix-cli-uio ``` {% include-markdown ".includes/admonition-pypi.md" %} === "Homebrew" You can install `zabbix-cli` with Homebrew: ```bash brew install zabbix-cli ``` !!! warning The Homebrew package is maintained by a third party. It may be outdated or contain bugs. For the most up to date version, follow the installation instructions for pipx. === "Binary" Binaries are built with PyInstaller for each release and can be downloaded from the [GitHub releases page](https://github.com/unioslo/zabbix-cli/releases). Download the correct binary for your platform and save it as `zabbix-cli`. !!! warning "Linux & macOS" Remember to make the binary executable with `chmod +x zabbix-cli`. unioslo-zabbix-cli-09a2fab/docs/.includes/upgrade.md000066400000000000000000000015421471265333400225110ustar00rootroot00000000000000=== "uv" ```bash uv tool upgrade zabbix-cli-uio ``` === "pipx" ```bash pipx upgrade zabbix-cli-uio ``` === "Homebrew" ```bash brew upgrade zabbix-cli ``` === "Binary (Automatic)" Zabbix-cli has experimental support for updating itself. You can use the `zabbix-cli update` command to update the application to the latest version. !!! danger "Write access required" The application must have write access to itself and the directory it resides in. ```bash zabbix-cli update ``` === "Binary (Manual)" The latest binary can be downloaded from [GitHub releases page](https://github.com/unioslo/zabbix-cli/releases). Download the binary for your platform and replace the current one. !!! warning "Linux & macOS" Remember to make the binary executable with `chmod +x zabbix-cli`. unioslo-zabbix-cli-09a2fab/docs/changelog.md000066400000000000000000000002401471265333400211170ustar00rootroot00000000000000# Changelog This page documents all the changes to released versions of Zabbix CLI. {% include-markdown "../CHANGELOG" start="" %} unioslo-zabbix-cli-09a2fab/docs/guide/000077500000000000000000000000001471265333400177475ustar00rootroot00000000000000unioslo-zabbix-cli-09a2fab/docs/guide/authentication.md000066400000000000000000000060011471265333400233050ustar00rootroot00000000000000# Authentication Zabbix-cli provides several ways to authenticate. They are tried in the following order: 1. [Token - Config file](#api-token-config-file) 1. [Token - Environment variables](#api-token-environment-variables) 1. [Token - Auth token file](#auth-token-file) 1. [Password - Config file](#config-file_1) 1. [Password - Auth file](#auth-file) 1. [Password - Environment variables](#environment-variables_1) 1. [Password - Prompt](#prompt) ## Token The application supports authenticating with an API or session token. API tokens are created in the Zabbix frontend or via `zabbix-cli create_token`. A session token is obtained by logging in to the Zabbix API with a username and password. !!! info "Session vs API token" Semantically, a session token and API token are the same thing from an API authentication perspective. They are both sent as the `auth` parameter in the Zabbix API requests. ### Config file The token can be set directly in the config file: ```toml [api] auth_token = "API_TOKEN" ``` ### Environment variables The API token can be set as an environment variable: ```bash export ZABBIX_API_TOKEN="API TOKEN" ``` ### Auth token file The application can store and reuse session tokens between runs. This feature is enabled by default and configurable via the following options: ```toml [app] # Enable token file storage (default: true) use_auth_token_file = true # Customize token file location (optional) auth_token_file = "/path/to/auth/token/file" # Enforce secure file permissions (default: true, no effect on Windows) allow_insecure_auth_file = false ``` **How it works:** - Log in once with username and password - Token is automatically saved to the file - Subsequent runs will use the saved token for authentication When `allow_insecure_auth_file` is set to `false`, the application will attempt to set `600` (read/write for owner only) permissions on the token file when creating/updating it. ## Username and Password The application supports authenticating with a username and password. The password can be set in the config file, an auth file, as environment variables, or prompted for when starting the application. ### Config file The password can be set directly in the config file: ```toml [api] username = "Admin" password = "zabbix" ``` ### Auth file A file named `.zabbix-cli_auth` can be created in the user's home directory or in the application's data directory. The file should contain a single line of text in the format `USERNAME::PASSWORD`. ```bash echo "Admin::zabbix" > ~/.zabbix-cli_auth ``` The location of the auth file file can be changed in the config file: ```toml [app] auth_file = "~/.zabbix-cli_auth" ``` ### Environment variables The username and password can be set as environment variables: ```bash export ZABBIX_USERNAME="Admin" export ZABBIX_PASSWORD="zabbix" ``` ### Prompt When all other authentication methods fail, the application will prompt for a username and password. The default username in the prompt can be configured: ```toml [api] username = "Admin" ``` unioslo-zabbix-cli-09a2fab/docs/guide/bulk.md000066400000000000000000000026131471265333400212300ustar00rootroot00000000000000# Bulk Operations Zabbix-CLI supports performing bulk operations with the `--file` option: ```bash zabbix-cli --file /path/to/commands.txt ``` The `--file` option takes in a file containing commands to run in bulk. Each line in the file should be a separate command. Comments are added by prepending a `#` to the line. ```bash # /path/to/commands.txt # This is a comment show_hostgroup "Linux servers" create_host foobarbaz.example.com --hostgroup "Linux servers,Applications" --proxy .+ --status on --no-default-hostgroup --description "Added in bulk mode" show_host foobarbaz.example.com create_hostgroup "My new group" add_host_to_hostgroup foobarbaz.example.com "My new group" remove_host_from_hostgroup foobarbaz.example.com "My new group" remove_hostgroup "My new group" remove_host foobarbaz.example.com ``` *Example of a bulk operation file that adds a host and a host group, then removes them.* ## Errors By default, all errors are fatal. If a command fails, the bulk operation is aborted. This behavior can be changed with the `app.bulk_mode` setting in the configuration file: ```toml [app] bulk_mode = "strict" # strict|continue|skip ``` - `strict`: The operation will stop at the first encountered error. - `continue`: The operation will continue on errors and report them afterwards. - `skip`: Same as continue, but invalid lines in the bulk file are also skipped. Errors are completely ignored. unioslo-zabbix-cli-09a2fab/docs/guide/commands/000077500000000000000000000000001471265333400215505ustar00rootroot00000000000000unioslo-zabbix-cli-09a2fab/docs/guide/commands/index.md000066400000000000000000000002631471265333400232020ustar00rootroot00000000000000# Commands Each command and its arguments and/or options are documented in the following pages. The commands are grouped by category. unioslo-zabbix-cli-09a2fab/docs/guide/configuration.md000066400000000000000000000235271471265333400231510ustar00rootroot00000000000000# Configuration !!! note "Configuration file directory" The application uses the [platformdirs](https://pypi.org/project/platformdirs/) package to determine the configuration directory. The application is configured with a TOML file. The file is created on startup if it doesn't exist. The configuration file is searched for in the following locations: {% include ".includes/config-locations.md" %} ## Create a config The configuration file is automatically created when the application is started for the first time. The config file can also manually be created with the `init` command: ```bash zabbix-cli init ``` The application will print the location of the created configuration file. To bootstrap the config with a URL and username, use the options `--url` and `--user`: ```bash zabbix-cli init --url https://zabbix.example.com --user Admin ``` To overwrite an existing configuration file, use the `--overwrite` option: ``` zabbix-cli init --overwrite ``` ## Config directory The default configuration directory can be opened in the system's file manager with the `open` command: ```bash zabbix-cli open config ``` To print the path instead of opening it, use the `--path` option: ```bash zabbix-cli open config --path ``` ## Show config The contents of the current configuration file can be displayed with `show_config`: ```bash zabbix-cli show_config ``` ## Sample config A sample configuration file can be printed to the terminal with the `sample_config` command. This can be redirected to a file to create a configuration file in an arbitrary location: ``` zabbix-cli sample_config > /path/to/config.toml ``` A more convoluted way of creating a default config file in the default location would be: ``` zabbix-cli sample_config > "$(zabbix-cli open --path config)/zabbix-cli.toml" ``` The created config looks like this: ```toml {% include "data/sample_config.toml" %} ``` ## Options === "`api`" The `api` section configures the application's Zabbix API connection. ```toml [api] url = "https://zabbix.example.com" username = "Admin" password = "" auth_token = "" verify_ssl = true ``` #### `url` URL of the Zabbix API host. Should not include the `/api_jsonrpc.php` path. Type: `str` ```toml [api] url = "https://zabbix.example.com" ``` ---- #### `username` Username for Zabbix API authentication. Can be used in combination with `password`, or to provide a default username for the login prompt. Type: `str` Default: `Admin` ```toml [api] username = "Admin" ``` ---- #### `password` Password to use in combination with a username. Type: `str` ```toml [api] password = "password123" ``` ---- #### `auth_token` Session token or API token to use for authentication. Takes precedence over `username` and `password` if set. Type: `str` ```toml [api] auth_token = "API_TOKEN_123" ``` ---- #### `verify_ssl` Whether to verify SSL certificates. Type: `bool` Default: `true` ```toml [api] verify_ssl = true ``` === "`app`" The `app` section configures general application settings, such as defaults for Zabbix host and group creation, export configuration, and more. ```toml [app] default_hostgroups = [ "All-hosts", ] default_admin_usergroups = [] default_create_user_usergroups = [] default_notification_users_usergroups = [ "All-notification-users", ] export_directory = "/path/to/exports" export_format = "json" export_timestamps = true use_auth_token_file = true auth_token_file = "/path/to/auth_token_file" auth_file = "/path/to/auth_token_file" history = true history_file = "/path/to/history_file.history" bulk_mode = "strict" allow_insecure_auth_file = true legacy_json_format = false ``` ---- #### `default_hostgroups` Default host groups to assign to hosts created with `create_host`. Hosts are always added to these groups unless `--no-default-hostgroup` is provided. Type: `List[str]` Default: `["All-hosts"]` ```toml [app] default_hostgroups = ["All-hosts"] ``` ---- #### `default_admin_usergroups` Default user groups to give read/write permissions to groups created with `create_hostgroup` and `create_templategroup` when `--rw-groups` option is not provided. Type: `List[str]` Default: `[]` ```toml [app] default_admin_usergroups = ["All-admins"] ``` ---- #### `default_create_user_usergroups` Default user groups to add users created with `create_user` to when `--usergroups` is not provided. Type: `List[str]` Default: `[]` ```toml [app] default_create_user_usergroups = ["All-users"] ``` ---- #### `default_notification_users_usergroups` Default user groups to add notification users created with `create_notification_user` to when `--usergroups` is not provided. Type: `List[str]` Default: `["All-notification-users"]` ```toml [app] default_create_user_usergroups = ["All-notification-users"] ``` ---- #### `export_directory` Directory for exports. Type: `str` Default: `"/zabbix-cli/exports"` ```toml [app] default_create_user_usergroups = "/path/to/exports" ``` ---- #### `export_format` Format for exports. Type: `str` Default: `"json"` ```toml [app] export_format = "json" ``` ---- #### `export_timestamps` Whether to include timestamps in export filenames. Type: `bool` Default: `false` ```toml [app] export_timestamps = false ``` ---- #### `use_auth_token_file` Whether to use an auth token file to save session token once authenticated. Allows for reusing the token in subsequent sessions. Type: `bool` Default: `true` ```toml [app] use_auth_token_file = true ``` ---- #### `auth_token_file` Paht to the auth token file. Type: `str` Default: `"/zabbix-cli/.zabbix-cli_auth_token"` ```toml [app] auth_token_file = "/path/to/auth_token_file" ``` ---- #### `auth_file` Paht to a file containing username and password in the format `username:password`. Alternative to specifying `username` and `password` in the configuration file. Type: `str` Default: `"/zabbix-cli/.zabbix-cli_auth"` ```toml [app] auth_token = "/path/to/auth_file" ``` ---- #### `history` Whether to keep a history of commands. Type: `bool` Default: `true` ```toml [app] history = true ``` ---- #### `history_file` File for storing the history of commands. Type: `str` Default: `"/zabbix-cli/history"` ```toml [app] history_file = "/path/to/history_file.history" ``` ---- #### `bulk_mode` Strictness of error handling in bulk operations. If `strict`, the operation will stop at the first error. If `continue`, the operation will continue after errors and report them afterwards. If `skip`, the operation will skip invalid lines in bulk file, as well as ignore all errors when executing the operation. Type: `str` Choices: `"strict"`, `"continue"`, `"skip"` Default: `"strict"` ```toml [app] bulk_mode = "strict" ``` ---- #### `allow_insecure_auth_file` Whether to allow insecure auth files. Type: `bool` Default: `true` ```toml [app] allow_insecure_auth_file = false ``` ---- #### `legacy_json_format` Whether to use the legacy JSON format (pre-Zabbix CLI 3.0), where the output is a JSON mapping with numeric string keys for each result. See the [migration guide](./migration.md) for more information. Type: `bool` Default: `false` ```toml [app] legacy_json_format = false ``` === "`app.output`" The `app.output` section configures the output format of the application. ```toml [app.output] format = "table" color = true paging = false theme = "default" ``` ---- #### `format` Format of the application output. Type: `str` Default: `"table"` Choices: `"table"`, `"json"` ```toml [app.output] format = "table" ``` ---- #### `color` Whether to use color in the terminal output. Type: `bool` Default: `true` ```toml [app.output] color = true ``` ---- #### `paging` Whether to use paging in the output. Type: `bool` Default: `false` ```toml [app.output] paging = false ``` === "`logging`" The `logging` section configures logging. ```toml [logging] enabled = true log_level = "INFO" log_file = "/path/to/zabbix-cli.log" ``` ---- #### `enabled` Whether logging is enabled. Type: `bool` Default: `true` ```toml [logging] enabled = true ``` ---- #### `log_level` Level for logging. Type: `str` Default: `"ERROR"` Choices: `"DEBUG"`, `"INFO"`, `"WARNING"`, `"ERROR"`, `"CRITICAL"` ```toml [logging] log_level = "ERROR" ``` ---- #### `log_file` File for storing logs. Can be omitted to log to stderr (**warning:** NOISY). Type: `Optional[str]` Default: `"/zabbix-cli.log"` ```toml [logging] log_file = "/path/to/zabbix-cli.log" ``` unioslo-zabbix-cli-09a2fab/docs/guide/index.md000066400000000000000000000004631471265333400214030ustar00rootroot00000000000000# User Guide Zabbix CLI is an application that provides a command line interface for interacting with the Zabbix API. Once installed, it can be invoked with `zabbix-cli`. The application is intended to provide a more user-friendly interface to the Zabbix API, and make it easier to automate common tasks. unioslo-zabbix-cli-09a2fab/docs/guide/installation.md000066400000000000000000000004531471265333400227740ustar00rootroot00000000000000# Installation The application is primarily distributed with `pip`, but other installation methods are also available. ## Install {% include-markdown ".includes/quick-install.md" %} ## Upgrade The upgrade process depends on the chosen installation method. {% include ".includes/upgrade.md" %} unioslo-zabbix-cli-09a2fab/docs/guide/logging.md000066400000000000000000000026551471265333400217270ustar00rootroot00000000000000# Logging The application supports logging to a file or directly to the terminal. By default, file logging is enabled and set to the `ERROR` level. ## Enable/disable logging Logging is enabled by default. To disable logging, set the `enabled` option to `false` in the configuration file: ```toml [logging] enabled = true ``` ## Levels The application only logs messages with a level equal to or higher than the configured level. By default, the level is set to `ERROR`. The available levels are: - `DEBUG` - `INFO` - `WARNING` - `ERROR` - `CRITICAL` The level can be set in the configuration file: ```toml [logging] level = "DEBUG" ``` ## Log file The default location of the log file is a file named `zabbix-cli.log` in the application's logs directory. The log file location can be changed in the configuration file: ```toml [logging] log_file = "/path/to/zabbix-cli.log" ``` The default logs directory can be opened with the command: ```bash zabbix-cli open logs ``` ## Log to terminal !!! warning "Verbose output" Logging to the terminal can produce a lot of output, especially when the log level is set to `DEBUG`. Furthermore, some of the output messages may be shown twice, as they are printed once by the application and once by the logging library. If the `log_file` option is set to an empty string or an invalid file path, the application will log to the terminal instead of a file. ```toml [logging] log_file = "" ``` unioslo-zabbix-cli-09a2fab/docs/guide/migration.md000066400000000000000000000167271471265333400222770ustar00rootroot00000000000000# Migration Guide Zabbix CLI 3.0 introduces a whole range of new features and improvements, as well as deprecating some old ones. This guide is intended to help you migrate from Zabbix CLI 2.x to 3.0. Notable changes include: **Config** - [**New configuration file format**](#config-file) - [**New default configuration file location.**](#new-default-configuration-file-location) - [**New configuration options**](#new-configuration-options) - [**Renamed configuration options**](#renamed-configuration-options) **Exports** - [**New export formats**](#new-export-formats) - `yaml` - `php` - [**New default export filenames**](#new-default-export-filenames) - Exported files are no longer prefixed with `zabbix_export_` - Exported files no longer include a timestamp in the filename by default. Newer exports overwrite older ones automatically. **Commands** - [**Command invocation syntax**](#command-invocation-syntax) - Using `zabbix-cli -C 'command args ...'` is no longer required. - Commands can be invoked directly with `zabbix-cli command args ...` - [**Command syntax**](#command-syntax) - Commands use positional arguments to a lesser degree than in 2.x. Named options are now preferred. - Legacy positional arguments are deprecated and will generate a warning when used. - Most prompts have been removed and replaced with named options due to the increase in scope of the commands. **Output** - [**JSON output format**](#json-output-format) - The JSON output format has changed. The old format can be enabled with the `app.legacy_json_format` option in the new TOML configuration file format. - When using a legacy `.conf` configuration file, the old JSON format is assumed. ## Config file Multiple changes have been made to the application's configuration file, in terms of format, location and option names. ### New configuration file format The configuration file is now in [TOML](https://toml.io/en/) format. The old `.conf` format is deprecated but can still be loaded. Old configs generate a warning when used. See [configuration](./configuration.md) for more information on the new format. An old configuration file can be migrated using the `migrate_config` command: ```bash zabbix-cli migrate_config ``` The command uses the currently loaded configuration file to generate a new TOML configuration file. The new file is saved in the default TOML configuration file location. Custom source and destination files can be specified with the `--source` and `--destination` options, respectively: ```bash zabbix-cli migrate_config --source /path/to/old/config.conf --destination /path/to/new/config.toml ``` ### New default configuration file location The location of the configuration file is now determined by [platformdirs](https://pypi.org/project/platformdirs/). See [Configuration](./configuration.md) for a brief summary of the new default location. To open the default configuration file directory, use the command: ```bash zabbix-cli open config ``` ### New configuration options New configuration options have been introduced to the configuration file: | Option | Description | Default | | --- | --- | --- | | `app.default_format` | Default output format in the CLI | `table` | | `app.legacy_json_format` | Enable [legacy json format](#json-output-format) | `false` | ### Renamed configuration options Several configuration options have been renamed to better reflect their purpose. The following table lists the old config section names and their new counterparts: | Old Config Section | New Config Section | | --- | --- | | `zabbix_api` | `api` | | `zabbix_config` | `app` | The following table lists the old option names and their new counterparts: | Old Config Section | Old Option Name | New Config Section | New Option Name | | --- | --- | --- | --- | | `zabbix_config` | `zabbix_api_url` | `api` | `url` | | `zabbix_config` | `cert_verify` | `api` | `verify_ssl` | | `zabbix_config` | `system_id` | `api` | `username` | | `zabbix_config` | `default_directory_exports` | `app` | `export_directory` | | `zabbix_config` | `default_export_format` | `app` | `export_format` | | `zabbix_config` | `include_timestamp_export_filename` | `app` | `export_timestamps` | | `logging` | `logging` | `logging` | `enabled` | For backwards compatibility, all the old option names are still supported, but will be removed in a future version. See [Sample configuration file](./configuration.md#sample-configuration-file) to see an example of the new configuration file format. ## Exports ### New export formats Zabbix CLI 3.0 introduces two new export formats: `yaml` and `php`. The availability of these formats depends on the Zabbix version you are using. Furthermore, the formats are no longer case-sensitive. For example, `YAML` and `yaml` are now equivalent. ### New default export filenames* Exported files are no longer prefixed with `zabbix_export_`. This behavior can be re-enabled with the `--legacy-filenames` option. Exported files no longer include a timestamp in the filename by default. Newer exports overwrite older ones automatically. Timestamps can be re-anbled by setting the `app.export_timestamps` option in the configuration file. ## Commands ### Command invocation syntax In Zabbix CLI 2.x, invoking single commands without entering the REPL required the `-C` option followed by the command and its arguments as a single string: ```bash zabbix-cli -C 'show_hostgroup "Linux servers"' ``` In Zabbix CLI 3.0, the `-C` option is no longer required. Commands can be invoked directly: ```bash zabbix-cli show_hostgroup "Linux servers" ``` ### Command syntax In Zabbix CLI 3.0, the majority of positional arguments are replaced with named options. Each command required a specific number of positional arguments that _had_ to be specified. For example, the `export_configuration` command in Zabbix CLI 2.x required the following syntax, even when we wanted to export all hosts: ```bash zabbix-cli -C 'export_configuration /tmp/zabbix_export.conf hosts #all#' ``` In Zabbix CLI 3.0, the same command would look like this: ```bash zabbix-cli export_configuration --directory /tmp/exports --type hosts ``` We don't have to pass in a special name argument to indicate that we want to export all hosts. Instead, we can simply omit the `--name` option. ## Output ### JSON output format In Zabbix CLI 2.x, the output format of commands generally took the form of a JSON mapping with numeric string keys for each result. For example: ```json { "0": { "hostid": "10609", "host": "foo.example.com", "groups": [], // ... } } ``` In the new default JSON format introduced in Zabbix CLI 3.0, the output is always a JSON mapping with the keys `message`, `errors`, `return_code` and `result`. For example: ```json { "message": "", "errors": [], "return_code": "Done", "result": { "hostid": "10609", "host": "foo.example.com", "groups": [], // ... } } ``` Which means when a command fails to execute or returns an error, the shape of the JSON output will be consistent with the successful output, making it significantly easier to parse: ```json { "message": "Host 'foobar.example.com' not found. Check your search pattern and filters.", "errors": [ "Host 'foobar.example.com' not found. Check your search pattern and filters." ], "return_code": "Error", "result": null } ``` In case of a chain of errors, the application makes an attempt to populate the `errors` array with all the errors encountered during the execution of the command. unioslo-zabbix-cli-09a2fab/docs/guide/usage.md000066400000000000000000000055351471265333400214050ustar00rootroot00000000000000# Usage ## Interactive mode Invoking `zabbix-cli` without any arguments will start the application in an interactive shell. This is the default mode of operation, and is the most user-friendly way to use the application. ```bash zabbix-cli ``` Within the interactive shell, commands can be entered and executed. Command and argument hints, tab autocompletion and history are supported out of the box. ``` % zabbix-cli ╭────────────────────────────────────────────────────────────╮ │ Welcome to the Zabbix command-line interface (v3.0.0) │ │ Connected to server http://localhost:8082 (v7.0.0) │ ╰────────────────────────────────────────────────────────────╯ Type --help to list commands, :h for REPL help, :q to exit. > ``` ## Single command mode Commands can also be invoked directly from the command line. This is useful for scripting and automation, as well for just running one-off commands. ```bash zabbix-cli show_hostgroup "Linux servers" ``` ## Bulk mode Zabbix CLI also supports running commands sourced from a file with the `--file` option. This is useful for running a series of commands in bulk. The file should contain one command per line, with arguments separated by spaces. Comments can be added with `#`. ``` $ cat /path/to/commands.txt # This is a comment show_hostgroup "Linux servers" create_host --host "foo.example.com" --hostgroup "Linux servers,Applications" --proxy .+ --status on --no-default-hostgroup --description "Added in bulk mode" create_hostgroup "My new group" add_host_to_hostgroup foo.example.com "My new group" ``` ``` $ zabbix-cli --file /path/to/commands.txt ╭────┬───────────────┬───────┬───────╮ │ ID │ Name │ Flag │ Hosts │ ├────┼───────────────┼───────┼───────┤ │ 2 │ Linux servers │ Plain │ │ ╰────┴───────────────┴───────┴───────╯ ✓ Created host 'foobarbaz.example.com' (10634) ✓ Created host group My new group (31). ╭──────────────┬───────────────────────╮ │ Hostgroup │ Hosts │ ├──────────────┼───────────────────────┤ │ My new group │ foobarbaz.example.com │ ╰──────────────┴───────────────────────╯ ✓ Added 1 host to 1 host group. ``` unioslo-zabbix-cli-09a2fab/docs/index.md000066400000000000000000000006031471265333400203020ustar00rootroot00000000000000# Zabbix CLI Zabbix CLI is a command line application for interacting with Zabbix version 6 or later. It is written in Python and uses the Zabbix API to interact with a Zabbix server. ## Installation {% include-markdown ".includes/quick-install.md" %} For the next steps or ways to customize the installation, head over to the detailed [installation](./guide/installation.md) guide. unioslo-zabbix-cli-09a2fab/docs/plugins/000077500000000000000000000000001471265333400203335ustar00rootroot00000000000000unioslo-zabbix-cli-09a2fab/docs/plugins/external-plugins.md000066400000000000000000000114321471265333400241570ustar00rootroot00000000000000# External plugins !!! important This page assumes you have read the [Writing plugins](./guide.md) page to understand the basics of writing plugins. External plugins are plugins that are packaged as Python packages and can be installed with Pip. Using [`pyproject.toml` entry points](https://packaging.python.org/en/latest/specifications/entry-points/), the application can automatically discover and load these plugins. A complete example of an external plugin can be found here: ## Packaging Assuming you have written a plugin module as outlined in[Writing plugins](./guide.md), you can package it as a Python package that defines an entry point for Zabbix-CLI to discover. Similar to local plugins, the entry point is a Python file or module that contains the plugin's functionality, except for external plugins, the entry point is defined in the `pyproject.toml` file - _not_ the configuration file. ### Directory structure The plugin package should have the following directory structure: ```plaintext . ├── my_plugin/ │ ├── __init__.py │ └── plugin.py └── pyproject.toml ``` Alternatively, if using the src layout: ```plaintext . ├── src/ │ └── my_plugin/ │ ├── __init__.py │ └── plugin.py └── pyproject.toml ``` ### pyproject.toml The package must contain a `pyproject.toml` file that instructs your package manager how to build and install the package. The following is a good starting point for a project using `hatchling` as the build backend: ```toml [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "my_plugin" authors = [ {name = "Firstname Lastname", email = "mail@example.com"}, ] version = "0.1.0" description = "My first Zabbix CLI plugin" readme = "README.md" requires-python = ">=3.8" license = "MIT" dependencies = [ "zabbix-cli@git+https://github.com/unioslo/zabbix-cli.git", ] [tool.hatch.metadata] allow-direct-references = true [project.entry-points.'zabbix-cli.plugins'] my_plugin = "my_plugin.plugin" ``` !!! info "Build backend" If you prefer setuptools, you can omit the `[tool.hatch.metadata]` section and replace the `[build-system]` section with the following: ```toml [build-system] requires = ["setuptools", "setuptools-scm"] build-backend = "setuptools.build_meta" ``` #### Declaring the entry point In your plugin's `pyproject.toml` file, you _must_ declare an entry point that Zabbix-CLI can find and load. The entry point is defined in the `[project.entry-points.'zabbix-cli.plugins']` section, where the key is the name of the plugin and the value is the import path to your plugin module. Recall that we defined a directory structure like this: ```plaintext . ├── my_plugin/ │ ├── __init__.py │ └── plugin.py └── pyproject.toml ``` In which case, the entry point should be defined as follows: ```toml [project.entry-points.'zabbix-cli.plugins'] my_plugin = "my_plugin.plugin" ``` ## Configuration !!! info "Loading external plugins" External plugins are automatically discovered by the application and do not require manual configuration to be loaded. Much like local plugins, external plugins define their configuration in the application's configuration file. However, the configuration is not used to _load_ the plugin, and is only used to provide additional configuration options or customization. The name of the plugin in the configuration file must match the name used in the entry point section in the `pyproject.toml` file. Given that we used the name `my_plugin` in the entrypoint section, its configuration should look like this in the Zabbix-CLI configuration file: ```toml [plugins.my_plugin] # module must be omitted for external plugins enabled = true extra_option_1 = "Some value" extra_option_2 = 42 ``` !!! warning "Local plugin migration" If rewriting a local plugin as an external one, remember to remove the `module` key from the plugin's configuration. If a `module` key is present, the application will attempt to load the plugin as a local plugin. ## Installation How to install the plugins depends on how Zabbix-CLI is installed. The plugin must be installed in the same Python environment as Zabbix-CLI, which is different for each installation method. ### uv `uv` can install plugins using the same `uv tool install` command, but with the `--with` flag: ```bash uv tool install zabbix-cli-uio --with my_plugin ``` ### pipx `pipx` Zabbix-CLI installations require the plugin to be injected into the environment: ```bash pipx install zabbix-cli-uio pipx inject zabbix-cli-uio my_plugin ``` ### pip If Zabbix-CLI is installed with `pip`, the plugin can be installed as a regular Python package: ```bash pip install my_plugin ``` unioslo-zabbix-cli-09a2fab/docs/plugins/guide.md000066400000000000000000000433531471265333400217620ustar00rootroot00000000000000# Writing plugins This guide goes over everything required to write a local plugin loaded by the application on startup. ## Basics ### Directory structure Start off by creating a new directory containing a Python module that defines the plugin, as well as an `__init__.py` file: ```plaintext . └── my_plugin/ ├── __init__.py └── plugin.py ``` The `__init__.py` file can be empty, but it's a good practice to define one for Python to treat the directory as a package. The `plugin.py` file is where the plugin's functionality is defined. Defining your plugin as a package lets you split your plugin into multiple files, making it easier to manage as it grows in complexity. It also allows you to more easily publish the plugin as an external package later on should you choose to do so. ### Module In order to define new commands in our plugin module, we need to import `zabbix_cli.app.app`. This is the main Typer application object that we will use to access the application state and define new commands with. A simple command that prints a message to the console can be defined like this: ```python # /path/to/my_plugin/plugin.py from zabbix_cli.app import app @app.command(name="my_command") def my_command() -> None: print("Hello, world!") ``` ### Activating the plugin We will look at plugin configuration more in-depth in the [Configuration](#configuration) section, but for now, we can add the plugin to the configuration file like this: ```toml [plugins.my_plugin] module = "/path/to/my_plugin/my_plugin.py" ``` This tells the application to load the plugin module when it starts up. Running `zabbix-cli --help` should now show the new command in the list of available commands.
![Console output](../static/img/plugins/help_empty.png){ width="100%" }
The command from the plugin is loaded
However, we can see that the command does not have a description or belong to any particular category. In the next section we will look at adding help text to commands and defining a category. ### Help text and categories Commands can have a long and short description and belong to a category. The category is used to group commands in the help output. We can define a category for our plugin commands by providing an argument to the `rich_help_panel` parameter when defining the command. This will add a new section to the help output with the given name we chose. ```python RICH_HELP_PANEL = "My custom commands" @app.command(name="my_command", rich_help_panel=RICH_HELP_PANEL) def my_command() -> None: """Short description of the command. Longer description going over the command in more detail. """ print("Hello, world!") ``` The command will now be added to its own category in the help output:
![Console output](../static/img/plugins/help_cat.png){ width="100%" }
Invoking `zabbix-cli my_command --help` shows the long description we provided:
![Console output](../static/img/plugins/help_command_empty.png){ width="100%" }
### Printing messages One of the most common operations in a CLI is printing messages to the console. The application provides several convience methods for printing messages to the console with specific formatting, which we can use in our commands to print messages consistent with the rest of the application. These are: - `zabbix_cli.output.console.success` - `zabbix_cli.output.console.info` - `zabbix_cli.output.console.warning` - `zabbix_cli.output.console.error` As well as [Rich](https://rich.readthedocs.io/en/latest/introduction.html) Console objects for stdout and stderr for more advanced formatting: - `zabbix_cli.output.console.console` - `zabbix_cli.output.console.err_console` We can use these to print to the console like this: ```python from zabbix_cli.app import app from zabbix_cli.output.console import success from zabbix_cli.output.console import info from zabbix_cli.output.console import warning from zabbix_cli.output.console import error from zabbix_cli.output.console import err_console from zabbix_cli.output.console import console @app.command(name="my_command") def my_command() -> None: success("Success message") info("Info message") warning("Warning message") error("Error message") err_console.print("Error message") console.print("Output of some sort") ``` This will print messages to the console using the same formatting as the built-in commands:
![Console output](../static/img/plugins/console01.png){ width="100%" }
### Command arguments In general, it is best to refer to the [Typer](https://typer.tiangolo.com/tutorial/) documentation when it comes to defining command arguments. However, a minimal example is provided here, showing how to define a command with a positional argument and a named option and use them to interact with the Zabbix API client: ```python from typing import Optional import typer from zabbix_cli.app import app from zabbix_cli.render import render_result # Define a new command @app.command(name="my_command") def my_command( arg1: str = typer.Argument(help="Some positional argument"), opt1: Optional[str] = typer.Option(None, "--opt1", "-O", help="Some named option"), ) -> None: """Short description of the command.""" # We can use the Zabbix API client host = app.state.client.get_host(arg1) # We can use the same rendering machinery as the built-in commands render_result(host) ``` ### Post-import configuration The module can define a function called `__configure__` that will be called after the application has finished its own configuration. This function can be used to perform any necessary setup or configuration that the plugin requires. The function takes a single `PluginConfig` argument. ```python from zabbix_cli.app import app import logging logger = logging.getLogger(__name__) def __configure__(config: PluginConfig) -> None: logger.info(f"Running post-import configuration for {config.module}") # We can access anything we need from the application state as long as the plugin module imports `zabbix_cli.app.app` # Set custom HTTP headers app.state.client.session.headers["X-Plugin-Header"] = "Some value" # Ensure that a certain configuration key is set app.state.config.api.legacy_json_format = False ``` You are free to perform any configuration you want in the `__configure__` function. However, be aware that modifying certain config options, especially those found in `app.state.config.api`, will not have any effect on the rest of the application. By the time `__configure__` is called, the application has already configured the API client. ### Configuration file Plugins are configured in the application's configuration file. Given that we have a plugin named `my_plugin`, its configuration file entry should look like this: ```toml [plugins.my_plugin] ``` Depending on whether you are writing a local or external plugin, the configuration requires different options. External plugins do not require _any_ configuration by default, while local plugins _must_ have a `module` key defined. #### `module` Local plugins must define this key. Its value can be a module path or a a file path. If using a file path, it is highly recommended to use an absolute path. ```toml [plugins.my_plugin] module = "/path/to/my_plugin/my_plugin.py" # OR # module = "path.to.my_plugin" ``` #### `enabled` Enable or disable plugin. Plugins are enabled by default unless otherwise specified. ```toml [plugins.my_plugin] enabled = false ``` #### `optional` Mark a plugin as optional, meaning the application will not exit if the plugin module cannot be imported. This is useful for plugins that are not required for the application to function. Plugins are not optional by default. ```toml [plugins.my_plugin] optional = true ``` #### Extra options The plugin configuration can contain any number of extra options that the plugin module can access. These options can be accessed through the `PluginConfig` object that is passed to the `__configure__` function. ```toml [plugins.my_plugin] module = "path.to.my_plugin" extra_option_str = "foo" extra_option_int = 42 extra_option_list = ["a", "b", "c"] ``` The `PluginConfig.get()` method can be used to retrieve the value of these extra options. The method takes the key of the option as the first argument, and an optional default value as the second argument. The method also takes an optional type hint as the third argument `type`. ```python from zabbix_cli.app import app from zabbix_cli.config.model import PluginConfig def __configure__(config: PluginConfig) -> None: # Access extra options opt1 = config.get("extra_option_str") # Validate the type of the option # Also lets type checkers know the type of the variable opt1 = config.get("extra_option_str", type=str) # Types are optional opt2 = config.get("extra_option_int") # reveal_type(opt2) # reveals Any because no type hint # Types from the TOML file are preserved assert isinstance(opt2, int) # We can validate more complex types too opt4 = config.get("extra_option_list", type=list[str]) # reveal_type(opt4) # reveals list[str] # We can also provide a default value opt4 = config.get("non_existent_option", "default") assert opt4 == "default" # Type hints are supported here too opt5 = config.get("non_existent_option", "default", type=str) # reveal_type(opt5) # reveals str assert opt5 == "default" # Use our config options: app.state.client.session.headers["X-Plugin-Header"] = config.get( "extra_option_str", type=str ) ``` !!! tip Providing a type for the `get()` method will also give you better auto completion and type checking in your editor. ### Accessing plugin configuration from commands Inside commands, the plugin's configuration can be accessed through the `app.get_plugin_config()` method. The name of the plugin, as denoted by its `[plugins.]` key, is passed as the argument to the method. If no configuration can be found, an empty `PluginConfig` object is returned. Given the following configuration: ```toml [plugins.my_plugin] ``` We can access its configuration like this: ```python from zabbix_cli.app import app @app.command() def my_command() -> None: config = app.get_plugin_config("my_plugin") ``` !!! note Should no config be available, an empty `PluginConfig` is returned. This is to facilitate external plugins that do not _require_ a configuration to be defined. ## Advanced ### Rendering Most zabbix-cli commands render a table or JSON depdening on the active output format. The functionality that powers this is the `zabbix_cli.models.TableRenderable` class. This class is a Pydantic model that can be subclassed and used to define data models that the application can render. ```python from typing import List from zabbix_cli.models import TableRenderable from zabbix_cli.output.render import render_result class MyModel(TableRenderable): host: str status: str ip: str templates: List[str] @app.command(name="my_command") def my_command() -> None: m = MyModel( host="foo.example.com", status="Good", ip="192.168.0.2", templates=["Template OS Linux", "Template App MySQL"], ) render_result(m) ``` Invoking the command will render the model as a table: ```bash zabbix-cli my_command ``` ![Console output](../static/img/plugins/render_table.png){ width="75%" } Adding `-o json` will render the model as JSON: ```bash zabbix-cli -o json my_command ``` ```json { "message": "", "errors": [], "return_code": "Done", "result": { "host": "foo.example.com", "status": "Good", "ip": "192.168.0.2", "templates": [ "Template OS Linux", "Template App MySQL" ] } } ``` ### Field-level customization By default, column headers and cells are determined by the field names and values of the model. We can customize this behavior by adding metadata to the model fields using something called "Meta Keys". These are special keys that can be added to the `json_schema_extra` dict of a field to change how it is rendered. #### Column headers If we just want to change the column header for a single field, we can pass a `zabbix_cli.models.MetaKey` object to the field's `json_schema_extra` dict when defining it: ```python from pydantic import Field from zabbix_cli.models import MetaKey from zabbix_cli.models import TableRenderable class MyModel(TableRenderable): host: str status: str ip: str = Field(..., json_schema_extra={MetaKey.HEADER: "IP Address"}) templates: List[str] ``` This will change the column header for the `ip` field to "IP Address": ![Console output](../static/img/plugins/render_header.png){ width="75%" } #### Lists Lists are rendered as newline-separated strings by default. We can change this by passing a `zabbix_cli.models.MetaKey` object to the field's `json_schema_extra` dict with the `MetaKey.JOIN_CHAR` key set to the desired separator: ```python from pydantic import Field from zabbix_cli.models import MetaKey from zabbix_cli.models import TableRenderable class MyModel(TableRenderable): host: str status: str ip: str templates: List[str] = Field(..., json_schema_extra={MetaKey.JOIN_CHAR: ", "}) ``` This will render the `templates` field as a comma-separated string: ![Console output](../static/img/plugins/render_list.png){ width="75%" } ## Example A complete example of a plugin that defines a new command and uses the plugin configuration to set a custom HTTP header on the Zabbix API client: ```python # /path/to/my_plugin.py from __future__ import annotations from typing import Optional import typer from zabbix_cli.app import app from zabbix_cli.render import render_result # Header for the rich help panel shown in the --help output CATEGORY = "My custom commands" def __configure__(config: PluginConfig) -> None: app.state.client.session.headers["X-Plugin-Header"] = config.get("extra_option_str", type=str) @app.command(name="my_command", rich_help_panel=CATEGORY) def my_command( arg1: str = typer.Argument(help="Some positional argument"), opt1: Optional[str] = typer.Option(None, "--opt1", "-O", help="Some named option"), ) -> None: """Short description of the command.""" host = app.state.client.get_host(arg1) render_result(host) ``` And the corresponding configuration: ```toml [plugins.my_plugin] module = "path.to.my_plugin" enabled = true optional = false extra_option_str = "foo" ``` An example of an external plugin with several commands, tests, and a `pyproject.toml` file can be found here: ## Performance The application at large makes use of inline imports inside functions to improve the startup time of the application. For the most part, the modules that are most the important to lazily import are ones that define Pydantic models. In order of performance impact, they are: 1. `zabbix_cli.pyzabbix.types` 2. `zabbix_cli.pyzabbix.models` 3. `zabbix_cli.commands.results` ### Inline imports Consider creating a separate module for your own models that you can import inside your commands that need them. This will prevent a cascade of imports that can add several hundred milliseconds of startup time to the application. Pydantic is notoriously slow at defining models, so avoiding importing these modules until they are needed is crucial. **BEFORE** ```python # /path/to/my_plugin.py import typer from zabbix_cli.app import app from zabbix_cli.models import TableRenderable from zabbix_cli.output.render import render_result from zabbix_cli.pyzabbix.types import Host class MyModel(TableRenderable): host: Host @app.command(name="my_command") def my_command(ctx: typer.Context, name: str = typer.Argument()) -> None: host = app.state.client.get_host(name) model = MyModel(host=host, foo="foo", bar=42) render_result(model) ``` **AFTER** ```python # /path/to/models.py from zabbix_cli.models import TableRenderable from zabbix_cli.pyzabbix.types import Host class MyModel(TableRenderable): host: Host foo: str bar: int # /path/to/plugin.py import typer from zabbix_cli.app import app from zabbix_cli.output.render import render_result @app.command(name="my_command") def my_command(ctx: typer.Context, name: str = typer.Argument()) -> None: from .models import MyModel # or use absolute import host = app.state.client.get_host(name) model = MyModel(host=host, foo="foo", bar=42) render_result(model) ``` ### Profiling !!! warning `py-spy` does not support Python 3.12 at the time of writing. Consider using [`py-spy`](https://github.com/benfred/py-spy) to profile the application before you package and distribute your plugin to ensure that it does not have a significant impact on the application's startup time. Profiling `--help` lets us profile the application startup time before any network I/O can occur. Install `py-spy` with: ```bash pip install py-spy ``` We can then use `py-spy` to profile the different imports the application performs and generate an SVG file: ```bash sudo py-spy record --subprocesses -o profile.svg --format speedscope -- zabbix-cli --help ``` The generated SVG file can be viewed on its own, or uploaded to the [speedscope](https://www.speedscope.app/) web application for a more interactive experience.
![Speescope profiling](../static/img/plugins/speedscope.png){ width="100%" }
Visual profiling with speedscope
The width of a bar indicates the time in milliseconds the given import adds, while its height indicates the number of stack frames generated as a result. A wide bar indicates a slow import, and can often be traced back to a cascade of numerous dependent imports and/or specific time-consuming imports. unioslo-zabbix-cli-09a2fab/docs/plugins/index.md000066400000000000000000000040531471265333400217660ustar00rootroot00000000000000# Plugins !!! warning "Work in progress" The plugin system is still under development and may change in future releases. The functionality of the application can be extended with user-defined plugins. Plugins can be used to add new commands, modify existing commands, or add new functionality to the application. Plugins can installed as local Python modules or external Python packages. ## Local plugins Local plugins are local Python modules (files) that are loaded by the application. They are the easiest way to add new functionality to the application, but are harder to distribute and share in a consistent manner. They are not automatically discovered by the application, and must be manually configured in the configuration file. See the [local plugins](./local-plugins.md) page for more information. ## External plugins External plugins are Python packages that can be installed with Pip and are automatically discovered by the application. They are easier to distribute and share, but require more effort on the part of the plugin author to create and maintain. See the [external plugins](./external-plugins.md) page for more information. ## Choosing the correct plugin type Both local and external plugins are essentially written in the same manner, following the application’s guidelines for plugin development outlined in [Writing plugins](./guide.md). This common foundation ensures that the core functionality is consistent whether the plugin is distributed as a local module or an external package. The difference lies primarily in how they are packaged for distribution and how the application loads them. While local plugins require manual configuration to be recognized by the application, external plugins are designed to be discovered automatically once installed. An easy way to decide which type of plugin to use is to consider whether you intend to share your plugin or not. If you do, an external plugin is likely the way to go. If you are developing a plugin for personal use or for a specific environment, a local plugin may be more appropriate. unioslo-zabbix-cli-09a2fab/docs/plugins/local-plugins.md000066400000000000000000000026211471265333400234270ustar00rootroot00000000000000# Local plugins !!! important This page assumes you have read the [Writing plugins](./guide.md) page to understand the basics of writing plugins. A local plugin is a Python module that is loaded by the application on startup. It _must_ be manually configured in the configuration file for the application to find it. ## Directory structure Given your plugin is structured like this: ```plaintext /path/to/ └── my_plugin/ ├── __init__.py └── plugin.py ``` You can add the following to your configuration file: ```toml [plugins.my_plugin] module = "/path/to/my_plugin/plugin.py" # or # module = "my_plugin.plugin" ``` An absolute path to the plugin file is preferred, but a Python module path can also be used. The differences are outlined below. ### File path It is recommended to use an absolute path to the plugin file. This ensures that the application can find the plugin regardless of the current working directory. The path should point to the plugin file itself, not the directory containing it. ### Module path One can also use a Python module path to the plugin file. This is useful if the plugin is part of a larger Python package. The path must be available in the Python path (`$PYTHONPATH`) for the application to find it. The import path can point to the plugin file itself or the directory containing it as long as `__init__.py` is present and imports the plugin file. unioslo-zabbix-cli-09a2fab/docs/scripts/000077500000000000000000000000001471265333400203415ustar00rootroot00000000000000unioslo-zabbix-cli-09a2fab/docs/scripts/__init__.py000066400000000000000000000000001471265333400224400ustar00rootroot00000000000000unioslo-zabbix-cli-09a2fab/docs/scripts/common.py000066400000000000000000000007561471265333400222130ustar00rootroot00000000000000from __future__ import annotations from pathlib import Path # Directory of all docs files DOC_DIR = Path(__file__).parent.parent # Directory of data files for Jinja2 templates DATA_DIR = DOC_DIR / "data" if not DATA_DIR.exists(): DATA_DIR.mkdir(parents=True) # Directory of Jinja2 templates TEMPLATES_DIR = DOC_DIR / "templates" # Directory of generated command doc pages COMMANDS_DIR = DOC_DIR / "guide" / "commands" if not COMMANDS_DIR.exists(): COMMANDS_DIR.mkdir(parents=True) unioslo-zabbix-cli-09a2fab/docs/scripts/gen_cli_data.py000066400000000000000000000034761471265333400233160ustar00rootroot00000000000000"""Script that runs various CLI commands and collects the result for use in the documentation. The commands are run in a limited environment (no color, limited width) to make the output more readable in the documentation. """ from __future__ import annotations import os import subprocess import sys from pathlib import Path from typing import NamedTuple from typing import Optional from typing import Protocol import tomli import tomli_w sys.path.append(Path(__file__).parent.as_posix()) from common import DATA_DIR # noqa # Set up environment variables for the CLI env = os.environ.copy() env["LINES"] = "40" env["COLUMNS"] = "90" # limit width so it looks nicer in MD code blocks env["TERM"] = "dumb" # disable color output (color codes mangle it) class CommandCallback(Protocol): def __call__(self, output: str) -> str: ... class Command(NamedTuple): command: list[str] filename: str callback: Optional[CommandCallback] = None def add_config_bogus_defaults(output: str) -> str: """Give bogus defaults to certain config values.""" config = tomli.loads(output) # TODO: replace local username with a default value return tomli_w.dumps(config) COMMAND_HELP = Command(["zabbix-cli", "--help"], "help.txt") COMMAND_SAMPLE_CONFIG = Command( ["zabbix-cli", "sample_config"], "sample_config.toml", callback=add_config_bogus_defaults, ) # List of commands to run COMMANDS = [ COMMAND_HELP, COMMAND_SAMPLE_CONFIG, ] def main() -> None: """Run the commands and save the output to files.""" for cmd in COMMANDS: output = subprocess.check_output(cmd.command, env=env).decode("utf-8") if cmd.callback: output = cmd.callback(output) with open(DATA_DIR / cmd.filename, "w") as f: f.write(output) if __name__ == "__main__": main() unioslo-zabbix-cli-09a2fab/docs/scripts/gen_cli_options.py000066400000000000000000000045011471265333400240660ustar00rootroot00000000000000"""Generates a YAML file containing all the global options for the CLI.""" from __future__ import annotations import sys from pathlib import Path from typing import NamedTuple import yaml # type: ignore from zabbix_cli.main import app sys.path.append(Path(__file__).parent.as_posix()) from common import DATA_DIR # noqa from utils.commands import get_app_callback_options # noqa def convert_envvar_value(text: str | list[str] | None) -> list[str] | None: # The envvars might actually be instances of `harbor_cli.config.EnvVar`, # which the YAML writer does not convert to strings. Hence `str(...)` if isinstance(text, list): return [str(t) for t in text] elif isinstance(text, str): # convert to str (might be enum) and wrap in list return [str(text)] elif text is None: return [] else: raise ValueError(f"Unexpected option env var type {type(text)} ({text})") # name it OptInfo to avoid confusion with typer.models.OptionInfo class OptInfo(NamedTuple): params: list[str] help: str | None envvar: list[str] config_value: str | None @property def fragment(self) -> str | None: if self.config_value is None: return None return self.config_value.replace(".", "") def to_dict(self) -> dict[str, str | list[str] | None]: return { "params": ", ".join(f"`{p}`" for p in self.params), "help": self.help or "", "envvar": convert_envvar_value(self.envvar), "config_value": self.config_value, "fragment": self.fragment, } def main() -> None: options = [] # type: list[OptInfo] for option in get_app_callback_options(app): if not option.param_decls: continue conf_value = None if hasattr(option, "config_override"): conf_value = option.config_override h = option._help_original if hasattr(option, "_help_original") else option.help o = OptInfo( params=option.param_decls, help=h, envvar=option.envvar, config_value=conf_value, ) options.append(o) to_dump = [o.to_dict() for o in options] with open(DATA_DIR / "options.yaml", "w") as f: yaml.dump(to_dump, f, sort_keys=False) if __name__ == "__main__": main() unioslo-zabbix-cli-09a2fab/docs/scripts/gen_command_list.py000066400000000000000000000024301471265333400242140ustar00rootroot00000000000000"""Generate documentation of commands and categories. Generates the following files: - `commandlist.yaml`: List with names of all commands. - `commands.yaml`: Mapping of all categories to detailed information about each command. """ from __future__ import annotations import sys from pathlib import Path from typing import Any from typing import Dict from typing import List import yaml # type: ignore from zabbix_cli.app import app sys.path.append(Path(__file__).parent.as_posix()) from common import DATA_DIR # noqa from utils.commands import get_app_commands # noqa: E402 def main() -> None: commands = get_app_commands(app) command_names = [c.name for c in commands] categories: Dict[str, List[Dict[str, Any]]] = {} for command in commands: category = command.category or "" if category not in categories: categories[category] = [] cmd_dict = command.model_dump(mode="json") # cmd_dict["usage"] = command.usage categories[category].append(cmd_dict) with open(DATA_DIR / "commands.yaml", "w") as f: yaml.dump(categories, f, sort_keys=True) with open(DATA_DIR / "commandlist.yaml", "w") as f: yaml.dump(command_names, f, sort_keys=True) if __name__ == "__main__": main() unioslo-zabbix-cli-09a2fab/docs/scripts/gen_commands.py000066400000000000000000000054451471265333400233550ustar00rootroot00000000000000"""Generate the code reference pages and navigation.""" from __future__ import annotations import sys from pathlib import Path from typing import Any from typing import Dict from typing import List import jinja2 import yaml # type: ignore from sanitize_filename import sanitize from zabbix_cli.app import app sys.path.append(Path(__file__).parent.as_posix()) sys.path.append(Path(__file__).parent.parent.parent.as_posix()) from common import COMMANDS_DIR # noqa from common import DATA_DIR # noqa from common import TEMPLATES_DIR # noqa from utils.commands import CommandSummary # noqa: E402 from utils.commands import get_app_commands # noqa: E402 def gen_command_list(commands: list[CommandSummary]) -> None: """Generates a YAML file with a list of the names of all commands.""" command_names = [c.name for c in commands] with open(DATA_DIR / "commandlist.yaml", "w") as f: yaml.dump(command_names, f, sort_keys=False) def gen_category_command_map(commands: list[CommandSummary]) -> None: """Generates a YAML file with all categories and detailed information about their respective commands. """ categories: Dict[str, List[Dict[str, Any]]] = {} for command in commands: category = command.category or "" if category not in categories: categories[category] = [] cmd_dict = command.model_dump(mode="json") # cmd_dict["usage"] = command.usage categories[category].append(cmd_dict) with open(DATA_DIR / "commands.yaml", "w") as f: yaml.dump(categories, f, sort_keys=True) def gen_category_pages(commands: list[CommandSummary]) -> None: """Renders markdown pages for each category with detailed information about each command. """ categories: Dict[str, List[CommandSummary]] = {} for command in commands: if command.hidden: continue category = command.category or command.name if category not in categories: categories[category] = [] categories[category].append(command) loader = jinja2.FileSystemLoader(searchpath=TEMPLATES_DIR) env = jinja2.Environment(loader=loader) # Render each individual command page pages = {} # type: dict[str, str] # {category: filename} for category_name, cmds in categories.items(): template = env.get_template("category.md.j2") filename = sanitize(category_name.replace(" ", "_")) filepath = COMMANDS_DIR / f"{filename}.md" with open(filepath, "w") as f: f.write(template.render(category=category_name, commands=cmds)) pages[category_name] = filename def main() -> None: commands = get_app_commands(app) gen_category_command_map(commands) gen_command_list(commands) gen_category_pages(commands) if __name__ == "__main__": main() unioslo-zabbix-cli-09a2fab/docs/scripts/gen_formats.py000066400000000000000000000007111471265333400232160ustar00rootroot00000000000000from __future__ import annotations import sys from pathlib import Path import yaml # type: ignore from zabbix_cli.config.constants import OutputFormat sys.path.append(Path(__file__).parent.as_posix()) from common import DATA_DIR # noqa def main() -> None: fmts = [fmt.value for fmt in OutputFormat] with open(DATA_DIR / "formats.yaml", "w") as f: yaml.dump(fmts, f, default_flow_style=False) if __name__ == "__main__": main() unioslo-zabbix-cli-09a2fab/docs/scripts/gen_ref_pages.py000066400000000000000000000013161471265333400235000ustar00rootroot00000000000000"""Generate the code reference pages.""" from __future__ import annotations from pathlib import Path import mkdocs_gen_files src = Path(__file__).parent.parent / "src" for path in sorted(src.rglob("*.py")): module_path = path.relative_to(src).with_suffix("") doc_path = path.relative_to(src).with_suffix(".md") full_doc_path = Path("reference", doc_path) parts = tuple(module_path.parts) if parts[-1] == "__init__": parts = parts[:-1] elif parts[-1] == "__main__": continue with mkdocs_gen_files.open(full_doc_path, "w") as fd: identifier = ".".join(parts) print("::: " + identifier, file=fd) mkdocs_gen_files.set_edit_path(full_doc_path, path) unioslo-zabbix-cli-09a2fab/docs/scripts/run.py000066400000000000000000000010201471265333400215100ustar00rootroot00000000000000from __future__ import annotations import sys from pathlib import Path from typing import Any sys.path.append(Path(__file__).parent.as_posix()) import docs.scripts.gen_commands as gen_commands # noqa import gen_cli_data # noqa import gen_cli_options # noqa import gen_command_list # noqa import gen_formats # noqa def main(*args: Any, **kwargs: Any) -> None: for mod in [ gen_cli_data, gen_cli_options, gen_command_list, gen_commands, gen_formats, ]: mod.main() unioslo-zabbix-cli-09a2fab/docs/scripts/utils/000077500000000000000000000000001471265333400215015ustar00rootroot00000000000000unioslo-zabbix-cli-09a2fab/docs/scripts/utils/__init__.py000066400000000000000000000000001471265333400236000ustar00rootroot00000000000000unioslo-zabbix-cli-09a2fab/docs/scripts/utils/commands.py000066400000000000000000000260301471265333400236550ustar00rootroot00000000000000from __future__ import annotations from functools import lru_cache from typing import Any from typing import Dict from typing import List from typing import Optional from typing import Union from typing import cast import click import typer from pydantic import BaseModel from pydantic import Field from pydantic import ValidationError from pydantic import computed_field from pydantic import model_validator from typer.core import TyperArgument from typer.core import TyperCommand from typer.core import TyperGroup from typer.models import DefaultPlaceholder from zabbix_cli.exceptions import ZabbixCLIError from .markup import markup_as_plain_text from .markup import markup_to_markdown def get(param: Any, attr: str) -> Any: """Getattr that defaults to None""" return getattr(param, attr, None) class ParamSummary(BaseModel): """Serializable representation of a click.Parameter.""" allow_from_autoenv: Optional[bool] = None confirmation_prompt: Optional[bool] = None choices: Optional[List[str]] = None count: Optional[bool] = None default: Optional[Any] = None envvar: Optional[str] expose_value: bool flag_value: Optional[Any] = None help: str hidden: Optional[bool] = None human_readable_name: str is_argument: bool is_eager: bool = False is_bool_flag: Optional[bool] = None is_flag: Optional[bool] = None is_option: Optional[bool] max: Optional[int] = None min: Optional[int] = None metavar: Optional[str] multiple: bool name: Optional[str] nargs: int opts: List[str] prompt: Optional[str] = None prompt_required: Optional[bool] = None required: bool secondary_opts: List[str] = [] show_choices: Optional[bool] = None show_default: Optional[bool] = None show_envvar: Optional[bool] = None type: str @classmethod def from_param(cls, param: click.Parameter) -> ParamSummary: """Construct a new ParamSummary from a click.Parameter.""" try: help_ = param.help or "" # type: ignore except AttributeError: help_ = "" is_argument = isinstance(param, (click.Argument, TyperArgument)) return cls( allow_from_autoenv=get(param, "allow_from_autoenv"), confirmation_prompt=get(param, "confirmation_prompt"), count=get(param, "count"), choices=get(param.type, "choices"), default=param.default, envvar=param.envvar, # TODO: support list of envvars expose_value=param.expose_value, flag_value=get(param, "flag_value"), help=help_, hidden=get(param, "hidden"), human_readable_name=param.human_readable_name, is_argument=is_argument, is_bool_flag=get(param, "is_bool_flag"), is_eager=param.is_eager, is_flag=get(param, "is_flag"), is_option=get(param, "is_option"), max=get(param.type, "max"), min=get(param.type, "min"), metavar=param.metavar, multiple=param.multiple, name=param.name, nargs=param.nargs, opts=param.opts, prompt=get(param, "prompt"), prompt_required=get(param, "prompt_required"), required=param.required, secondary_opts=param.secondary_opts, show_choices=get(param, "show_choices"), show_default=get(param, "show_default"), show_envvar=get(param, "show_envvar"), type=param.type.name, ) @property def help_plain(self) -> str: return markup_as_plain_text(self.help) @property def help_md(self) -> str: return markup_to_markdown(self.help) @model_validator(mode="before") @classmethod def _fmt_metavar(cls, data: Any) -> Any: if isinstance(data, dict): metavar = data.get("metavar", "") or data.get( # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType] "human_readable_name", "" ) assert isinstance(metavar, str), "metavar must be a string" metavar = metavar.upper() if data.get("multiple"): # pyright: ignore[reportUnknownMemberType] new_metavar = f"<{metavar},[{metavar}...]>" else: new_metavar = f"<{metavar}>" data["metavar"] = new_metavar return data # pyright: ignore[reportUnknownVariableType] @computed_field @property def show(self) -> bool: if self.hidden: return False if "deprecated" in self.help.lower(): return False return True # TODO: split up CommandSummary into CommandSummary and CommandSearchResult # so that the latter can have the score field class CommandSummary(BaseModel): """Convenience class for accessing information about a command.""" category: Optional[str] = None # not part of TyperCommand deprecated: bool epilog: Optional[str] help: str hidden: bool name: str options_metavar: str params: List[ParamSummary] = Field([], exclude=True) score: int = 0 # match score (not part of TyperCommand) short_help: Optional[str] @model_validator(mode="before") @classmethod def _replace_placeholders(cls, values: Any) -> Any: """Replace DefaultPlaceholder values with empty strings.""" if not isinstance(values, dict): return values values = cast(Dict[str, Any], values) for key, value in values.items(): if isinstance(value, DefaultPlaceholder): # Use its value, otherwise empty string values[key] = value.value or "" return values @classmethod def from_command( cls, command: TyperCommand, name: str | None = None, category: str | None = None ) -> CommandSummary: """Construct a new CommandSummary from a TyperCommand.""" try: return cls( category=category, deprecated=command.deprecated, epilog=command.epilog or "", help=command.help or "", hidden=command.hidden, name=name or command.name or "", options_metavar=command.options_metavar or "", params=[ParamSummary.from_param(p) for p in command.params], short_help=command.short_help or "", ) except ValidationError as e: raise ZabbixCLIError( f"Failed to construct command summary for {name or command.name}: {e}" ) from e @property def help_plain(self) -> str: return markup_as_plain_text(self.help) @property def help_md(self) -> str: return markup_to_markdown(self.help) @computed_field @property def usage(self) -> str: parts = [self.name] # Assume arg list is sorted by required/optional # ` [OPTIONAL_ARG1] [OPTIONAL_ARG2]` for arg in self.arguments: metavar = arg.metavar or arg.human_readable_name parts.append(metavar) # Command with both required and optional options: # `--option1 --option2 [OPTIONS]` has_optional = False for option in self.options: if option.required: metavar = option.metavar or option.human_readable_name if option.opts: s = f"{max(option.opts)} {metavar}" else: # this shouldn't happen, but just in case. A required # option without any opts is not very useful. # NOTE: could raise exception here instead s = metavar parts.append(s) else: has_optional = True if has_optional: parts.append("[OPTIONS]") return " ".join(parts) @computed_field @property def options(self) -> List[ParamSummary]: return [p for p in self.params if _include_opt(p)] @computed_field @property def arguments(self) -> List[ParamSummary]: return [p for p in self.params if _include_arg(p)] def _include_arg(arg: ParamSummary) -> bool: """Determine if an argument or option should be included in the help output.""" if not arg.is_argument: return False return arg.show def _include_opt(opt: ParamSummary) -> bool: """Determine if an argument or option should be included in the help output.""" if opt.is_argument: return False return opt.show def get_parent_ctx( ctx: typer.Context | click.core.Context, ) -> typer.Context | click.core.Context: """Get the top-level parent context of a context.""" if ctx.parent is None: return ctx return get_parent_ctx(ctx.parent) def get_command_help(command: typer.models.CommandInfo) -> str: """Get the help text of a command.""" if command.help: return command.help if command.callback and command.callback.__doc__: lines = command.callback.__doc__.strip().splitlines() if lines: return lines[0] if command.short_help: return command.short_help return "" @lru_cache(maxsize=None) def get_app_commands(app: typer.Typer) -> list[CommandSummary]: """Get a list of commands from a typer app.""" return _get_app_commands(app) def _get_app_commands( app: typer.Typer, cmds: list[CommandSummary] | None = None, ) -> list[CommandSummary]: if cmds is None: cmds = [] # NOTE: incorrect type annotation for get_command() here: # The function can return either a TyperGroup or click.Command cmd = typer.main.get_command(app) cmd = cast(Union[TyperGroup, click.Command], cmd) groups: dict[str, TyperCommand] = {} try: groups = cmd.commands # type: ignore except AttributeError: pass # If we have subcommands, we need to go deeper. for command in groups.values(): if command.deprecated: # skip deprecated commands continue category = command.rich_help_panel # rich_help_panel can also be a DefaultPlaceholder # even if the type annotation says it's str | None if category and not isinstance(category, str): # pyright: ignore[reportUnnecessaryIsInstance] raise ValueError(f"{command.name} is missing a rich_help_panel (category)") cmds.append( CommandSummary.from_command(command, name=command.name, category=category) ) return sorted(cmds, key=lambda x: x.name) def get_app_callback_options(app: typer.Typer) -> list[typer.models.OptionInfo]: """Get the options of the main callback of a Typer app.""" options: List[typer.models.OptionInfo] = [] if not app.registered_callback: return options callback = app.registered_callback.callback if not callback: return options if not hasattr(callback, "__defaults__") or not callback.__defaults__: return options for option in callback.__defaults__: options.append(option) return options unioslo-zabbix-cli-09a2fab/docs/scripts/utils/markup.py000066400000000000000000000112741471265333400233570ustar00rootroot00000000000000from __future__ import annotations import itertools from dataclasses import dataclass from functools import cmp_to_key from typing import List from rich.text import Text from zabbix_cli.output.style import CodeBlockStyle from zabbix_cli.output.style import CodeStyle CODEBLOCK_STYLES = list(CodeBlockStyle) CODE_STYLES = list(CodeStyle) CODEBLOCK_LANGS = { "python": "py", } @dataclass class MarkdownSpan: start: int end: int italic: bool = False bold: bool = False code: bool = False codeblock: bool = False language: str = "" def to_symbols(self) -> tuple[MarkdownSymbol, MarkdownSymbol]: start = MarkdownSymbol.from_span(self, end=False) end = MarkdownSymbol.from_span(self, end=True) return start, end @dataclass class MarkdownSymbol: position: int italic: bool = False bold: bool = False code: bool = False codeblock: bool = False end: bool = False language: str = "" @property def symbol(self) -> str: symbol: List[str] = [] if self.codeblock: # Only insert language when opening codeblock lang = self.language if not self.end else "" symbol.append(f"```{lang}\n") # TODO: add support for language in fences (codeblock) else: if self.italic: symbol.append("*") if self.bold: symbol.append("**") if self.code: symbol.append("`") s = "".join(symbol) if self.end: s = f"{s[::-1]}" return s @classmethod def from_span(cls, span: MarkdownSpan, end: bool = False) -> MarkdownSymbol: return cls( position=span.end if end else span.start, italic=span.italic, bold=span.bold, code=span.code, codeblock=span.codeblock, end=end, language=span.language, ) # Easier than implementing rich comparison methods on MarkdownSymbol def mdsymbol_cmp(a: MarkdownSymbol, b: MarkdownSymbol) -> int: if a.position < b.position: return -1 elif a.position > b.position: return 1 else: # code tags cannot have other tags inside them if a.code and not b.code: return 1 if b.code and not a.code: return -1 return 0 # TODO: rename `markup_to_markdown` to `markup_as_markdown` # OR rename `markup_to_plaintext` to `markup_as_plaintext` # I am partial to `x_to_y`. def markup_to_markdown(s: str) -> str: """Parses a string that might contain markup formatting and converts it to Markdown. This is a very naive implementation that only supports a subset of Rich markup, but it's good enough for our purposes. """ t = Text.from_markup(normalize_spaces(s)) spans: List[MarkdownSpan] = [] # Markdown has more limited styles than Rich markup, so we just # identify the ones we care about and ignore the rest. for span in t.spans: new_span = MarkdownSpan(span.start, span.end) styles = str(span.style).lower().split(" ") # Code (block) styles ignore other styles if any(s in CODEBLOCK_STYLES for s in styles): new_span.codeblock = True lang = next((s for s in styles if s in CODEBLOCK_LANGS), "") new_span.language = CODEBLOCK_LANGS.get(lang, "") elif any(s in CODE_STYLES for s in styles): new_span.code = True else: if "italic" in styles: new_span.italic = True if "bold" in styles: new_span.bold = True spans.append(new_span) # Convert MarkdownSpans to MarkdownSymbols # Each MarkdownSymbol represents a markdown formatting character along # with its position in the string. symbols = list(itertools.chain.from_iterable(sp.to_symbols() for sp in spans)) symbols = sorted(symbols, key=cmp_to_key(mdsymbol_cmp)) # List of characters that make up string plaintext = list(str(t.plain.strip())) # remove leading and trailing whitespace offset = 0 for symbol in symbols: plaintext.insert(symbol.position + offset, symbol.symbol) offset += 1 return "".join(plaintext) def normalize_spaces(s: str) -> str: """Normalizes spaces in a string while keeping newlines intact.""" split = filter(None, s.split(" ")) parts: List[str] = [] for part in split: if part.endswith("\n"): parts.append(part) else: parts.append(f"{part} ") return "".join(parts) def markup_as_plain_text(s: str) -> str: """Renders a string that might contain markup formatting as a plain text string.""" return Text.from_markup(s).plain unioslo-zabbix-cli-09a2fab/docs/static/000077500000000000000000000000001471265333400201415ustar00rootroot00000000000000unioslo-zabbix-cli-09a2fab/docs/static/img/000077500000000000000000000000001471265333400207155ustar00rootroot00000000000000unioslo-zabbix-cli-09a2fab/docs/static/img/plugins/000077500000000000000000000000001471265333400223765ustar00rootroot00000000000000unioslo-zabbix-cli-09a2fab/docs/static/img/plugins/console01.png000066400000000000000000000700511471265333400247120ustar00rootroot00000000000000PNG  IHDRf8 @iCCPICC ProfileHWXS[@h)H6B AŎ,*TD(v(XPPł]y+ߛ;Μ;sNpDlTa8:ȏL'p01##j^j/g-<~.$T^.7q5W$(ͦ牤V%BXZSx&6qJ*8ːsӡj?ĎB@bZ1b|A|8paY1 q!$Z9|a\OF+Krrmx^Fl=HFdP %Qao d΁ ͆U!;ÄL y Y`b ǽqO< ^}au<:  ]S⟢~"?.ա2{a>г dYY4vdG2JA%[?[bY$v;5:vkڱR_W; ?o*`G ttB1ppn;"C"D$".%'! vH$ɎE qHyb:.qR7郒RRPP\i1+Jϔ>drGI^NJn!_"w?S4(V/J,%RAܣQVV6UvWR(WPޫ|NGM[D2*'TnRT_j25ZK=E}@JSuPeTV6^Q}FVPcMV+P+W;vIOnRURoV>ATcyMf&OHs)4fFcѸӴn-[+STkVVv *]:['[g~:F`X2~ĕuGuKt^G [רw_׷ՏҟA~H##KFy556eŠ`0PdaQjcF4cocjt&=^AoHL6t|623-4czߌb0K3[mjonl>|y "bYV ,-{tVVuV>Ӭkm6Y6m.ۢ.UP;W;zQQjFݴWg?tqs(thtx9|t蕣ώ1!c Ǵydur6:6p켱Mc_9978rwYUZfVvňd,es's?#c_Y;={Y:kW7;{wǧ瑯/w3 3Ow=˃5u/ xhX4+D0!84xeM!ˮe i U }f&kjppaxc`Gi9-p1*2*igch1SbvƼ]{7:N?16}BYBW9IMɤm&=eb&͘t~GML9BHIHٙ SS,Z /o5/?KJ+KIJ_ޛQ'` *237fϊڞ5'G)'%Y)M5:cjNT,1mʹ~qx[.;))O ȷK%H{W? 3mg. Y,&~89g\dnyfuceAւ  .LXRdX4/Ao.\q1Xc%|+\(u,-/¯c~upYڲ7 g2ǫƯjXM_])kΗ;o\KY+YUVѴ|݊u_*3*WU6^R~=o 7n,i`ӭAj,kʷoy5~nVv;jjkw\^IzwMuyz{t{%{Kwc-V*i@f67f4v5%5u64x:px#UG.?F9Vtlx}'O>nzTkmQmCO;xY9qBE׋ .~wPkG%KM/t-fǩHo`wWƟ/_WW{ցȁr}~_AÎg?%|zyҗ6_[~738(9_ V4- &@3OVU3PߣMn/6H*;vdJi!s9nT t;|KeXIfMM*>F(iNxfASCIIScreenshotLc pHYs%%IR$iTXtXML:com.adobe.xmp 182 1126 Screenshot V?iDOT[([[0n0RIDATxwxNB  H˥+bCR> U)"MAT""bK :I:߼;ɜIr29~d3Z/~OHHHHHHHH@& 3Μ "@a?       H!fR<%       %ȹsx$@$@$@$@$@$@$@$*{afРA rIYn̟?,gHHHHHHH@feԨQ%K8y>3qֳHHHHHHH@ff̘!sV.]$v풍7 3V iq\       KaN:o޽[Ce̙nXH$@$@$@$@$@$@$@IM 3Yʽw-*M4Qc 8P8x>>ׯ_T6[l={ĨGr6m*Nu}͛7̙3h"P[rkm޼-[V .,/^Ç˼y$"""Zj֚'Ov횄+%Kx_h۶˗O6o,7RJɱc/ٳK$00PN8!Çw33L\ʗ/; U ݗ.jԨ~_NRtR؈IGͧߋ93$@$@$@$@$@$@^%*M+I|\W6ٯ_?U믿N9sH֬YСCqWEmƍg-[J6m'F =:F~Փ={&L 6ĘW^ҰaӐ!CtÌ>{+W:r .]4M \F!*TpYۀٳb3f;v쐪UbNg͚)AB$@$@$@$@$@$U). W:O9>mm7K#Hӧ\wiQ0k'|`+W.)Q2ҿ 2NYka X`>XR޽{JEN5k4{Sxq)PݻW~.xͱB Ily9rPe\pA ǫdN>:;VƿwyGG[dJ*WhfrI$@$@$@$@$@$4RTɗC~N2g,7"Ɏ>s˹sX*^:ΫMZH0+"SNUǰV;vnV1E'N7/ؾ}9fW ؼf[o%kV~,[̬CF 6mr)we_R޽؄"wz W~'UVkN];պ iӦ »gʔInܸ!=zX !Z~C 3NHHHHHH 0S'$_ɂjs[/;.zmxw###eժU_ĘO f'I\:w_xL2%Tp9: O~moz]L [nUaGoÕEN>cǎҢE oV?(0.zl>IHHHHH@ 3*HCԮfDA(//T\ټb+1plMZH0Jɒ%U.,`/ 1/_6h XVW^OGyY"/9߸,_\&Ohk裏Tٳ'8ϔePZ&It޼yM\MBF4hC]w8ֿg0ߋZ $dfpuߑS TR|2ȅOʟ.M ꁋ+&(Z.? (Paf I?\B"OD Hk%8 F䴟+g`<0oQQXk\TPPP_JwUYpkm#     HQW_ɯO<3AҜ[Gva&-qk!/tw8pp E?3#GTVa&\HHHHHH{EQTDٿ%g|*ơsMFv,ʈdOE?; >߰aTShk4͚5S׽&ɓ=8&Sa&pU78E>v$wff>dܝ{ݺu7PCYor$@$@$@$@$@$@#, 2JK-$R /Ƹ'ÌƯJz!:t* 3֏f8Z b{qᡛ6mF>|KD& zET)A5<_ls*̤.֐5jȀ0ӫW/SkQF ![Snݤqƪ*$̓ 8#lLpKJAj~tbzGҥ^bKڙիW}f3mrMiӦY̐!Ce va駟6ljS$""YX{hh WfUJ0_lkv*̤.ߑ;Γ&MRa*Q0"àl֬Y3gNd]e'5 $@ 3Ytr=YN*5o\^z%c…eӂzңGڵtwX2iڵ2~xoݺO'0rCёԨըK.IQd&'/Fׯ_w3f BL[1(39cޅ:gn2 H \pIGztt!bGg˖M1,tZ̠)=$@$@$@$@$@$@IC لYQ0cؠCIr?~UO(ƍ3 }>{ݝ0X4X#ݺuK2gl:=x` aI?\98qKp8:3g{j>P,[*1< y&FI+\/#ɚ 3p zz 8Ν;z 3N$     HRʕ3?v`mp o*VS~O>Qͭ44h#P3f0CW[U\?U.?e]tF)]vVXaF=x:gӼf`$-d,^XLʴ?UV |YSjSy.[Yy|„ J|  N87X =sLY`b> @HŒTPP/^\0rQ" "-:… a=WPAQ׍0'I?Xcǎ8G= 2,9ɨt/q>s)S###(PԠAi$4[J(a9 @{a&qx؛wўNR     8(dN8FX+W rԱcGiѢz+*n' @ @a&uWA^WBw3={g╉HHHHHH 0#+`۷,X3cǎ̓ nfRpu$+ P1/^+HHHHHHR/ 3l2       tNL:?`nHHHHHHH 0zφ+#      H(̤HHHHHHHR/ 3l2       tNLp…QFRxqɞ=iΝp$C )&v-~r_kر*p|o߾%yҩS'ɔ)KÇK>}\B$@$@$@$@$@$@$1 0SaDAsl6gΜ)rw\86l]B̝;Wdɢ]tIN:A [as΂DHyrʩիWe*ӧO'OԨQC Z|L<r        tF 3seܸqh޽2x`k-[^;hР$*Es@       d'@aƆM6ĔkSO=%)X>}Z-Z$۶mPQB RmpMʕ+f{X^|י+ʣ>*|}}̙3j Ml۶˗O6o,7RJɱc/TΉ;t r >|sʖ-,.^(3oknݺ%[v[o*C;-@ņNX=ݻwO:$J ȝ;jwey>N3ֽNB$[lpΝSxrȡ^ypשSG|M-,m0 k$vt5kJw:X`o#^@qwiK%sX8 k~ppwyG~aUe*&1څ\!     (؎*^#G- &>zN / <֭[ oQ׈ j :T`=G}d^{U޽{+QO?U]pu{* W~'QNXl߾]_e믿nA]z\˖-3тɦM\ʝk׮*v/s Ti\ oܸ!=zX !ZÞۅ\G$@$@$@$@$@$ 0S~ ߒpSI_-^,քt-ʷ~k6 3!!!ҷo_ϝ@Au Q֯_o:Cւ֭[eԨQjxu \!-^XL`g5FѠ O~E'A|jޱcGiѢʻ;W(0.$@$@$@$@$@$@@ 3z4*%zo^v"/c]~'ƨG C+QY̘1C]Aoja[0pHEٳg+Npzg) QAE V=ڒH"''I1GAr wΏU?+WV~a7o޼ŋ{ne}hK'=M4W_}UYX9by      K ńNuX2rD{waaqƞ0c:Ty7nݺ;?΂>o|Z5\A0(׫X3vX u$9uw4Ydq qP.9퇫QVeV %}enVTI}]jfE' @&bŒI9$=' /ƺ) ,;vje9mka~c?I 3YE0!M| !F'X@*Yii髯_UO'1M2`$X =H'OVΝcWجLbHHHHHH CH1a&9VacP0x`eaXeo߻Sa8 5kL RzѤUVq.ߓ~mH>֬ŒՇ MHVaƛ\; @ @avZH͛K/V'pkOz=4xJ y6m>|KD&OvnԩfXq\?:pf1bg yN`Q 0@gfz% 6Tp GքZ7VEVa&XfHHHHHH }H1a&{""^:I)V%J"?^v¡QF*t3 BK8f*V()44T dx#*QjP fbgnI& #Yy:"}l֬Y3gNd],fHHHHHH2f|吚ژw_"N v O>ٳ?S}h"9,f͛7￯odj1)%駟X]~]^{5],cƌ___e dƄDwrtI?\cҡ! a z 3xᾑGĦӧTK`` UZ̠)HHHHHH H 2ʕ+EdDI% |3|)ܹc 2X׎;LN'O7'LUx9rSkZ@6mdJr9ڵkkHN?۶m+=zs*Ϡ %͛ץ"? 0pBe1~/^,SLQ UV25UX Õ5"K.+Y';+V0AYs &(.[[hQ !a}A(.g`|&ّ      M E MsگL2kH%7g AdHhvڥ%1XF$@$@$@$@$@$qP8g͝:$`O.ړaٍHHHHHH $`^ ,ʕ+WtرhB>|X飫$      D0h =^W~޽k:d;SՋHHHHHHEŒHr4M 88X+ tql AرcIӧœ @%@a& WB% @Y\x1ViIHHHHH2 3ᔹG       TIL<.HHHHHHH #0N{$      H(̤cHHHHHHH2 3T#_|Q/!C̐ NfR$hU >f6n<'/=Ac1 @HWL"`Abeϟ"ŊTW8-o웖2fiq$@$@$@$@$@$@$M ] 3MݍWO5홝;ofUl)kIx0&$      ҝ0gOɒGmt֬#2b=&>>idú*M=5\yV;m3C$@$@$@$@$@$@$: ;afӦF/_6E{3k;c%o0`ÙX۲HHHHHHHM 3RW?gOjYWe/ߒ+$ ^ɳe˵oU^9v,R z@*Vӧɸqd1F ͋H WN&~Ѹv816mc' \ȥK…Hcap)9{nt,1fB$@$@$@$@$@$5)&.U}Pu`iڵԘ'O^{L\Ey>UvPx`!䑯v\bUlY=Cȥӂ~͚RPvx̘}ҧOE]gL*Des]7]10f|BĊ/2ej۷ի]{ԫnSMTE5:o OtMuʟ?K]/ͥi?Ĝ.. 03gNMyҵY&k:n]R7 ti= [:wl|g?ჿR*v#Ro?b|ۅk.=zlU>7=}>dwefcƕ7w:j2wn-,3r9_JɒQVf-qV:wrq,_HHHHHHN EZ kn+ WymcpTݺucv5ҷo5 >j֌sٌN9jejC?)jX1i 38s?~EYWZ`I{^Z NQv9yȪԮ]1 CH*W.?5[̠'jI;mtNYԪU.ɛŧz.bGE ə3wÆsҹvÚhB̵=/m,htht4qTlk7d˖p\Nue'ewc3N99=p11C$@$@$@$@$@$$RDYDDܐ UW}(B2cFmD3*eKtĉҤZէE†_0 m3VE_uxZ~i"E|7dS12OݺM2ā(߆Hvg|˗u]Q.ʛ z`C*Ђ'TGv7?בR|_2,ZTGJ:t̙s()7*qi?wr:HHHHHH@ 3[!C**p7pk; 12p`%^%VT۷Iv[6iӦkXed6-Wthm.@؅ѣfCuOPr-}h911 3ηmۿMou\Jh_=sAc`*V\"/(GYAiK#.Wuף6(W&ϝ[<ưFfnߓxVqi?ߙ}_B3HHHHHHG م|$"ӑ Qa[#쳃2qCVܷ1"ʎaFq@+F?.oݺkD$1 tZ,.'P؅|mCdN a,9Ĵ*wwg10zU7^ʚKhXD a?/_=-rTr{$ߙsx7v.nHHHHHHKU7Q7U5F]aޟ~ wމ]-]Z߈”eh0`|}=r6Y| 'u6T_~+pޫSFfΓ4Y%'N\WJavigJ 3Vڱ#h__Y bYN+]8ln#2rA%!<=yzc,       df&Lxpz`Zd5D֨ LϞMbwVSk̛ |i}I7kZ+2E۶ nf8~ieŒ"-&OüvҥKc^ *MaУ,9gKsܸJFDbf] CVMf-W?3\l+ C"++˖5bIs/$_~y|$ui9X{oKdŊR5><Яk-(7N30cu .5e!ɜ9rV%'&4%*Ռ7IxsŋVYڅ\DᆱΜjE|`At&_'ΜCb $03{vM%ԯ{ykq !G"Mo M߾]i25>S6lh$Qh4k֜5dΤ0IG$Z2eŸϒ vC>1>mYO^{bXav`9߷{5Mɚ5:u~NE `᷀ٲE?u;';󙛈#!1\U$@$@$@$@$@$@^ Œ-VIe .]V50"EJ_qhy]OM 1K[y"2lXe."ԮRCzf]|fƏť֩b'Fy n;wZo]#`<\ݻcfb4 FBCECO{ΜS.3K$@$@$@$@$@$\LtX9%\z[ΟaD]֭sul)؎HHHHH 3i㜸J       tHL:0O9, LR%'t`Ol޵k5kdkPl9xH? 7oG :c*Ҩl)qqq0S-n̸xnyu9|/ 4Ǥ0qv'%ͺl!M 3ގ& /])N;lj*izu<'      CŒy~Ah5vMF]4uf5u˨#u|CdyZG\,5u {%H'Z6Uc[U.-c2 <7ICn\=$@$@$@$@$@$ Ѕ.U-95!D-ʗXؼp=y @"}1Ɯ`Iv'K#K0ָo-[3QSJŒ%"-^!R1ɓA Z=$xn\a<Y,3X{~{uVX;>InFXj|nDEׂ{ш+-1=|W]iJ6RݺswO^ @P 3_wk^A Q{EvXgBj`^]>h歉ppxxx!la;{>-%x[dXiBjr 3 Kx.Vaq +brt7C p0 0oc(cߤQu뤱 _z٥ ,ضÄ!햳UPf93;0b= L&iCsxڏc,a)췽*xYݜe slJNN5dbvܬݠ𝝱e<cdzƷi%MLT2슶E}xNZ?IHHHHH/Ѕ ʥu}З, U+$+x&l[լQTq&i6EΛ+f{9hyP#H;/ x2yM:Va^ޭQdɸ"ckX> 3ڡ.}DܵLI +Fgͬ\|6A1v8z؄euz]ZAӶSf?M$\jˋ(de3fdfq }~9xh\)(dz$@$@$@$@$@$P3ɈɷwȐ%sS NQN蝍H\#} r3i7yU#yi3gkf!R|yYp̞=[5k&]v֭[Çe<6^ ڮUVr9HFtҲi&utNrˤr2gˠqƲzj?~)SFJ[]VF7޽p RfMAn+VȻ+;vM4^zIzzTXQ'roO?v-ZZ0_[N,Y"SN;n`m:tZjɆ >3UVs#( @ 0;H%dŲeܹs5B<0`@TyvnݺE!nټyEAo,Y2[={rIj۷o~=yr7[Q7DѣG7|e2uYʵ߯#9m7rHiٲe C|Fͼym۶>I&EU %^  3z馛Nq۶m1w^9J"OUڗn!v={Z=zT-[&/*T}޽[ꪨqxf֯_/5jԈJ ,e˖^wu\;v(~{d8xڠxuCک*C {8KjdѢEr}i{  X/7 `# ({vBD͛Km9>LP.yB$@$@$@$@$@$@.4^}lT xu<j95|Mqb6tJ{駥^z_w}=8q>x۷o/Æ zwqqc͜93r'*|wQA]q6?XzL+AYbV aQX/LP.3~ @@R'b %9;eXe e6kfk'TGe:{7 .kޛ1c7.ې`7=z܇X|40AJƂ˩o =z߾} /ȕ[oEuGL*\ $0bu͚51_Ud'&k~]z饂?BilL0 Jɺs.?mΚ5+"M<ن8!-  , QOڵkD\:ugsف ɂ~۴ic oJ"/M-Xz6{䐹Qe{\fR5/HHHHHHH'Ѕe<2m&:u!K]{ƃ|,ȳ|4^3m* <02 R_)YQVJ7 0qq/m(U}"s=4O:$y+̤EHHHHHH(tҥKn<.VdaG!xh܄XX^~ss  LlY"WQQ mM6nൂ1 6x=yWO>Ѣ1H;;:Agl|1Z֯_?A/"A2d>+(=z_b=?Ԅm!a~Q]6)Sܩs\u      A 4aǜ Lմ4).\(˗//0s˭4jHc… v4mnKp+WpDmp/hf͚ oşdzܹ  f<և_.` DLNx!槉u, !     (4(Gʼn&C}mKA(Z߾} /+V;SoH$@$@$@$@$@$@FLyA WBG$r5|Ͳyf\HHHHHHH _ PW< 4iD,իWJl AfmHHHHHH %fR@*U$==zܹ Os#     ((e ? 3 8       "JL}\6 @0π3       ((e ? 3 j֬)]tK2e:~':uj]'N$@$@$@$@$@$@a]4hmV*V(ŊL1cdŊyM~/_'VeXoꫯ]?w(L"|     | 0OHƍc.2bY`AA ի'O?m;ɓv <X(Q^ڵK6l`1{4S#     &r-?ɮ2m4ٴi-[r3zUŰC2tP>sysc@a^;Թ     D 4a*J#Gȭ*k׮pzᇥEz2}ȽTO ˋaa'GϞ=/{9(>FE$@$@$@$@$P "tQnv_!CDaiҤ5ʖf=z4Yd̞=;R'W]u e…s/J*駟nW^-K.զ8eF #˗_Riժ:tȶvaS zĚ9iԨ.]z/A`ycvٻwopNx}7g{ŋˬY"ש/th߾}!~|q.L*W,s̑]p;|W "W^)uֵb"bȻs '"ܹLzweǎTwo f͚YV[x!DRcHHHHHPnM:udyN8Q nxߖ%KZxdie䮻>xY|'91q{O/.7n5jDh_x 9r̟?_1yF ҭ[ z}Qb*b6r s3嬳rz>HpG+닷6NNBR۾}O/3ϔo9w7}=zpwSۖ-[ǹ}HHHHHu328-Kk/TV_|őGE6mlxP xyt_UVETuԱ(G8+,yb !=zԮm *.w-Dr;*U"xTZ՞CZj0LN=Ԉgx#0]Yhw}QCY>;t/)rS]ʖ-k:ٶm=wPOF` g7 ;Efk=p~0ar 7X?wL @Q 0GP^`G >pH0w$+͜9SƎBgd`'o ρX0|H(qCg9!q8k= |wnY~ǐ۹CTg^ D۷aÆk6ȭŝ 3(vXCOF`ߟۖ$@$@$@$@$@$P"̼+o }}l ?6 9?k6[~}AEjaSsD&Mܗhx}#!<6_ Ə'yt}p8)V#ءo!\p;1c7Ξ AAT-L.\u(K$ڹmp~GAݮ;o^ @XBf^{5A^X,ɝS^zSO_ lBV5!@:{7Py]>;wo"0&<`g}&/A~nU*yP69ٴi"cNu;fСC[ )$Û$@$@$@$@$@$03d=_dC?i$)W\!}A &du󃸝}1TaexnWaV[p|嗑>žgd`'n9s)tIrF C!`ҥK[w7&l3(%c\IȝmvE ҵkW`XE ԋ' 1nŊ1KB:aӝs$dENJBsÃgq%ضHe˖~r_~6 ^3^ u$}Safԩ43dܸqL}W2f̘aR*W8 G?- z ;Gf@hoO$@$@$@$@$@@Œ cǎRzuA4%c:uy^m۶d3 ./_IIHHHHHX oLAK)sHHHHHHH 7 3Ga$@$@$@$@$@$@$@yFŒ-^  3txHHHHHHHrcɞHHHHHHHH 3p2  3ǒ= /f|be       =PHIENDB`unioslo-zabbix-cli-09a2fab/docs/static/img/plugins/help_cat.png000066400000000000000000000474721471265333400247010ustar00rootroot00000000000000PNG  IHDRfa_: @iCCPICC ProfileHWXS[@h)H6B AŎ,*TD(v(XPPł]y+ߛ;Μ;sNpDlTa8:ȏL'p01##j^j/g-<~.$T^.7q5W$(ͦ牤V%BXZSx&6qJ*8ːsӡj?ĎB@bZ1b|A|8paY1 q!$Z9|a\OF+Krrmx^Fl=HFdP %Qao d΁ ͆U!;ÄL y Y`b ǽqO< ^}au<:  ]S⟢~"?.ա2{a>г dYY4vdG2JA%[?[bY$v;5:vkڱR_W; ?o*`G ttB1ppn;"C"D$".%'! vH$ɎE qHyb:.qR7郒RRPP\i1+Jϔ>drGI^NJn!_"w?S4(V/J,%RAܣQVV6UvWR(WPޫ|NGM[D2*'TnRT_j25ZK=E}@JSuPeTV6^Q}FVPcMV+P+W;vIOnRURoV>ATcyMf&OHs)4fFcѸӴn-[+STkVVv *]:['[g~:F`X2~ĕuGuKt^G [רw_׷ՏҟA~H##KFy556eŠ`0PdaQjcF4cocjt&=^AoHL6t|623-4czߌb0K3[mjonl>|y "bYV ,-{tVVuV>Ӭkm6Y6m.ۢ.UP;W;zQQjFݴWg?tqs(thtx9|t蕣ώ1!c Ǵydur6:6p켱Mc_9978rwYUZfVvňd,es's?#c_Y;={Y:kW7;{wǧ瑯/w3 3Ow=˃5u/ xhX4+D0!84xeM!ˮe i U }f&kjppaxc`Gi9-p1*2*igch1SbvƼ]{7:N?16}BYBW9IMɤm&=eb&͘t~GML9BHIHٙ SS,Z /o5/?KJ+KIJ_ޛQ'` *237fϊڞ5'G)'%Y)M5:cjNT,1mʹ~qx[.;))O ȷK%H{W? 3mg. Y,&~89g\dnyfuceAւ  .LXRdX4/Ao.\q1Xc%|+\(u,-/¯c~upYڲ7 g2ǫƯjXM_])kΗ;o\KY+YUVѴ|݊u_*3*WU6^R~=o 7n,i`ӭAj,kʷoy5~nVv;jjkw\^IzwMuyz{t{%{Kwc-V*i@f67f4v5%5u64x:px#UG.?F9Vtlx}'O>nzTkmQmCO;xY9qBE׋ .~wPkG%KM/t-fǩHo`wWƟ/_WW{ցȁr}~_AÎg?%|zyҗ6_[~738(9_ V4- &@3OVU3PߣMn/6H*;vdJi!s9nT t;|KeXIfMM*>F(iNxfASCIIScreenshot pHYs%%IR$iTXtXML:com.adobe.xmp 102 1954 Screenshot uiDOT3(33IDATxy՝{C/.k#D@"h\$5Z?R2SE1ItLU*U1&"AvAEfwÜo۷{o_既}y|9FN5        A!IA@@@@@@pѼ        ('!      D       dT :T       @;       QrR       A4       @F3Ie       Ѽ        ('!      D       dT :T       @;Y=zY.B@@@@@Ȯ@ xk֬ӻt}G$ M6n7laCb͛'OL|{(Cgq{+۴iSG @@@@@@* T zV~X[,^؎?[”)Sf͚mNl>Dܲe&?# w :*      Pm~[revjÆ k G"D'"Nf@@@@@@T ڵk6k֬X3?~#A(ߧ Q;D]چ-      @uvA˗N:n%KXqqkSZ5̙3֤I{;ܹs};dW޽{w=ZFLW{9rHc999v1۷o_}Z0`ժU#?{ ͣo 6u/w<:wl-[tիgyyynԩS=_n]sJVL%}) ݗs֭;Ӷb 7vje˖Ozy_,ϗK-{NםޡDѣGo޽{[AwСCgϞ=]vָqc;{{,YYl@@@@@@42VxٺukSP{իW]Lя> [1cFQ0:tPo׮]=.Ս x*UO?];\|Чp%vkhr‰BKvEZXfݻV=z(ͯ(~Vb}@!vڵc.\`~l=݅TA2vXSX\t͛Wj'@˦M, z!b~.UN :      @Az19sM0U8 Z7 Ze۶mqR-[ӦMv`:( 꽬DAtlȑ0zص4*(T/r_km۶u c;X SOi_w@S_Po=seMr=gֹjC;|t\^jsQQ/i.k~N%xMmW 걯?*=ztQo{V{5"/ :@@@@@@ T ZV\i -Zpf2e;& `9O_^j:X`~}YlTh՛[E!֐މ3GO>HKo#ĆG_~!)\p M|Am_~}׉nۣC6/W;v>|~];–wA[|]3fLgz xK}`~ xX3~.      A^U{3g) D5́裏\oa:^zU™tT <w]/X]#jOb ' ;vzZ᾵M\E=t?C-86C/ lݺ6o΍WvUoS}A__3p7|ܸq`{{GCvLD@@@@@i_?rXXX)ɓ'[c=+lV˞={lڵ f͚!u[ڟ㏻a EIϿ/+.Dʚ7Z ݦpYO<ޑ`YlEEEU|{<*s;"|7U6mڸ^aR€f/e/jz8S֧/:}      )3ƚ%cAB('7<.jop:}e_oYaCCe:͋Re߾}nm_>VYf6LѺ/Pz. 1Y|:up5&Nق=`8SO^pכgvy(.y}I_Ss{ǥNBG=ئA#hmKTɊdDZ@@@@@@hM [sգRZ(ZgΜqCa[2#ZW:tŋcsD!s DN _PaÆs {!^ 2x۷{>(Q\\ֲԻ`Y?YU޽|>Ӓ}/pu*|G` Z7$V`X h6i$7yuya*=Uիީ=zpkժIv/ VK,q_N߃op٢˻|Q]_CӇ]|f"N6s^^\>@@@@@@*F@X`ݻ^w( 2      @ ҏ2Ji~~] NVDhԳ3X 5<&e>NԻ9^o'?.8dt*At0,O}[blϦMV9͟?1]vaÆm4jZWC/K}Xxi`Qж`;h_3~[r{rrrz*Aty؅X@@@@@@PΝ;ݔe:^Dnƌc>WwVe*M?#Glҥ~5c`.6kFUK0\ܻwYׯ њ{Woݺt/>5gt݇:nݺuk._MPRf͚ ZgG|Q]o}#%]/_3 Zu=n(-_vܩEZ=} љx?}|"     Tg-ZdN*uAtnEEE.,Us7MGn{5ͯKy[CiPV @&LpBa56k=D?#֤IrEjnhmTJpn1Wvճ|ܸqu|Cٲe)w,|> G z;!h_~Nrh;;w,i_8'      @ X.]\`GXAzZ-[w>}s8͌5ڷov57ի6{xUelyӵ57`QH.`w3gу6 ^ᢰP!z&+ %M@~8{kiz`|{챵kHk9vŲ|(0;_^6}2er@qq k'zϴ_p4I}Kzԋɮ7oYX,      TwӦMbbh?Bhթz`ZزeK uaC^^5hɖu^F\'Oڱcǒ~VsbWj;Y*ޭݻwg_!VȭN7C@@@@@%,/&L4;cƌ8G @@@@@@{ShmZ~~'JaSC@@@@@@@j'@]&sï_n3g|"      At9_D߸qΝ;g ,(      ]wW{       @ DW81@@@@@@@. Moٲ .U@@@@@@5:umd w:9@@@@@@@ $ˋ +r       @ T       U_ !OOڍ'Y'sA(CuV\\\fӦM\[~͟?cy[7ըQΝk7n.\~,.\h[lU> ئM;vݜrrr7l`Į ~0XD@@@DW6(xmmUtq?LRj@K/d}zŞڵkv[b͚5+] թSvaӧO/Ν;-*{7ܵgϞmopі-[ڛonE[oUaNg>+ *8qG?r_$V_TTd/bpS˩_U6)2y؉    c\8љ>NݻwW_}j׮]c=%W'//?g1c(y-k=Ofە#^N֤XC۽.[ n_xNYwYcwkw5a Z5c|xyˇ[o3Ԛte4s̞J=U<9YVu4jծcK/W{OMm8w]9~ؚi- /ىE [V=gFTb-݆_Wm۶;,SRC:t2{?4ƍg]vuR>|ZṨ÷PaÆyf[rs=CY~~iNk?ٝ6zhӧ[nҤ >-ݻ׶o/o9sȑ#E^wk [^mlf͚ٳ)@9uiw}N<~4ԩS]Oƍۅ ĉwE?lm۶vS{ x%v۽#~jݻw:YFիW]kt=}Nojs˗/TuSz~ZΝ;W_}e~il=B:泌?Qӟ/~`M6k׺ߕݺu3_j999sYǎm߾}/۲ sͪ[~FAWy{7޽ @@@*V@CO0nG:u6-$8J:GЗp߸~jֺ=ՋnG֗Wتi _wvn0ٸ}6p+mǷmO1Vkځy]bZRb_9wxmk7^.Q^}ոV}lؾ:mmOl+yV!cm[ym { /RݵoߒͰ:[x|"wӭ=wK|k.2wYO~0aBA넡VWqq) ?O^sAwxunB]?U+ٳgowQFٴibsw*d]ܮ^x|غBz*Yfpsl>?uJ;sܹsMC+3gt/7jժ\^ym(kGy9ϛg:Zċ/2~xF+ajy uq_?n?jt~&|O}Fûߕw2   dO Y^Ț"IDAT MnlCcB&$$hhH*R-$R$-D Iʣ,E?? c7$UcgΜ3sØy<{lsg)PzcjذJ\\XRJ-i{WfPl+o [Mpy[N<(kזRJmoԩS%""B8 %K4Zw=./_\ã>*z}76+VLj֬iu;vCQFƍӔYӧs#GȪUxRn])PsȯZƎ,_|38˛6m[KIu&۶mskѢ fĉf^Z&&QQQo޽{;5Կ)|[l15jԐHYvs|vmUW]e-*UV5|3n :evROWuNᶻz}_쵆귞>+WΜNot}iɒ%mN{s~??6{bŊRHv2~NԹsgٽ{O1d}ol߾YI? Z"   */.pߎ MZ CWTv't}D·~~=^w;&7o'||+yNG}$ C?/'O4KRUw4c@@@N@guZ_x49w}; :AS{aַIwʢ^AfRͳ&ㆆMz0A= @UY]\ZOduK?BS{87eϓg=Kԉqn݇HǏǮ3h=N%j֗ O tyʺQtVXJFW6#n@ٷpYD~ ^je4L9s|UNH퍪֯_ 'L e˖s)udo/]w=wr}t}SDw}r7:ǠALhD_wu)_zړPnYk9c5Pv6̸ 4 ej=$ݓGRRh@m`oGk v9zϑP<ù.}͞=[x#{lp`8ptݸqP _5뜔@@@\D^ˑ!rBd6;:ԝgڑ9]^&9u6IzILN]xg^֍{j;!-8C7vvQtj9v CqcX_YODdܓDJRLy(2;,ux;>&frY`Ųj苦հ$-!R$\7D;4 C!K."'ػw74̘1Qtۡ=:D+Cos=yaÇCNyV3Y_K6mj6=F/w=[N[C{zϓP<ù.}M>]ni_#E(:dԩSeĈg|t$?'AL'k{R 9tr0.=~;k__(omIjY&+t7u߰JiN^O@ >{M6L\9u w;`Æ C{'D{=lCLf2VAGhXv 7 ׵nܹsgٽ{wl+*5kS; @suY <m+4jDjJ*/hgۓڮ??"j( ;uf]0 ~;WAڵk^;޻ Ͳwvp=zs9L8t*7ѡ|.{msm?>u2eePc|bb˞={L}k(sT    c2|Ra@LkK~Hܟ-UʁNϤX(J6{4U&߾Sۻ/%}zjeA{pF-zfgxY: @O&vؙA@>Z{y'}V;z3{z 6 $[~^sUVNqwݯ_?5 <-0^{ .l60]3sL0]:`&mm}vykVv{0 }>r.>ęp>U^ z=~ϾB ϰرnJq{Ph:   W _х"K6A{_.< GvntO 9`>[EO{~rsz/voslX)UY7ZgN J5h*?99AC  СCjժ'007nL81]qloC6jҤIYfǼ{ݻ^{r7"wïS޿Yx{7F}N:Lիc 0@4h`$v6`o;6>tu]vw F{0 }moŇ8N=ݧ:AtTn3?oYDg5\~͝,#nw}޸a@@@ȗA5xbT8i:#MWC$Ykg˲an E㇓dطLYwJ[wsY6 [mRJD-ڲԳy-cy# o=U6|9|ӾKgD[u^@x֩SGtx֌&88p@'unըQC bN k8ttzPϏ空fy?&+v!Qw}Wbbb s5+~?wm2h [StaС    d@ K'~.곍vC**[^Z4ͳ:vHR|[gOIUt}uZx19+SFgV|2XΝ#AtåtN}OΚ,KJ+nqLAtdrn=$IKmyEDl;}~+g6k̄¥K6TK.}:l~ y:wyK/IDDYΨn8'sQMfΕ?Ǝ+%J0Ԑ>YoܢE T Z =4Ѡ\{I+m/Z9]hQӻ\S˖-0hv}^lٲe͢7Y 므f;psޣ`wޑYwI7--PෞS smC{]Bsi?krh[IXQ{t]ܱu ~jt"Nq'   S ъr$)\<uy~pz%rvMQs;fDG6l..`u)a"Y=)ʉ%ϦHv+ph=/(*UMw AErh@ Z(P]pRAzDՀT'=ǝZ˖,YeN=jy=k,g>vv! *D:tx 6;vH׮]uL_l#G>݆ӧOÇMWcO{/Yvm۷o2 68vv}eZׇ~}>3\;z.59]A:|R$ v%Ϯ 箭A@@@'袕ϖ7BsdfҤSm,Y*^voM[RYhrvZK$/Pu^7~$^EHGH5ҔkuGɮ'9~]6N^izok6H֩w˱ Q\X|Kr/:gO^XV }Y3݇H:e+zٻ/I#hdS~pO,y 3 @~S ˖-3={ҬACFANW];=@jLl{[r Ll踸8ݻ5W^m۶ײ [u X^z tM7 &n?t([N${??[i~2֞;%%%ɘ1cdԩUj;0wqG*ޓ#^'Dk{GFFO2Vܽ]z.?p#2吏㻍t"vr>%_^/Az1zhSe?ȑ#MٸqafΜ)Cׯo7ttz|9nJ8Z L   Z\Dg'Gt׽y UzR4ٿG&p[B{.UV6/mם EcKr}a)RdRˑRr@ hQzuThqFYz)2 a7n,q~)LaAa˚5k22* u_ 5^MѢE{koP IC{ 'DC{o뤽g̘ x- | C:E/dSVs7c@@@O _хʔ7'U.Ns턑'ˑ@@D#!    +=/Ie;ķ"bxZ:gl=8m@v@@@@*z]YZk*vL81D_yeϊfB@@@@@@ZjժɦMdܹ/6 Z',xV @@@@@@@r>Z} 0aB+֣ڱ>,s̑;wfx2V       NMJݺuM$Q,7p(Qœ0!!A'G5@@@@@@@ȝ)RP!spL/([h=C6m$&&&ӓ@@@@@@@ w 8p@֭['k֬-gZ]H[+       @.Б$11ь%d{@@@@@@@ o Dv@@@@@@@ 1zN      Mٮ\       c9Fω@@@@@@@)@7ەB@@@@@@rL :91       7frU       @ D='F@@@@@@AtlW @@@@@@1       @ ΛU!      9&@c@@@@@@țy]*@@@@@@@ s#      ySv9tIENDB`unioslo-zabbix-cli-09a2fab/docs/static/img/plugins/help_command_empty.png000066400000000000000000001277141471265333400267640ustar00rootroot00000000000000PNG  IHDR>A @iCCPICC ProfileHWXS[@h)H6B AŎ,*TD(v(XPPł]y+ߛ;Μ;sNpDlTa8:ȏL'p01##j^j/g-<~.$T^.7q5W$(ͦ牤V%BXZSx&6qJ*8ːsӡj?ĎB@bZ1b|A|8paY1 q!$Z9|a\OF+Krrmx^Fl=HFdP %Qao d΁ ͆U!;ÄL y Y`b ǽqO< ^}au<:  ]S⟢~"?.ա2{a>г dYY4vdG2JA%[?[bY$v;5:vkڱR_W; ?o*`G ttB1ppn;"C"D$".%'! vH$ɎE qHyb:.qR7郒RRPP\i1+Jϔ>drGI^NJn!_"w?S4(V/J,%RAܣQVV6UvWR(WPޫ|NGM[D2*'TnRT_j25ZK=E}@JSuPeTV6^Q}FVPcMV+P+W;vIOnRURoV>ATcyMf&OHs)4fFcѸӴn-[+STkVVv *]:['[g~:F`X2~ĕuGuKt^G [רw_׷ՏҟA~H##KFy556eŠ`0PdaQjcF4cocjt&=^AoHL6t|623-4czߌb0K3[mjonl>|y "bYV ,-{tVVuV>Ӭkm6Y6m.ۢ.UP;W;zQQjFݴWg?tqs(thtx9|t蕣ώ1!c Ǵydur6:6p켱Mc_9978rwYUZfVvňd,es's?#c_Y;={Y:kW7;{wǧ瑯/w3 3Ow=˃5u/ xhX4+D0!84xeM!ˮe i U }f&kjppaxc`Gi9-p1*2*igch1SbvƼ]{7:N?16}BYBW9IMɤm&=eb&͘t~GML9BHIHٙ SS,Z /o5/?KJ+KIJ_ޛQ'` *237fϊڞ5'G)'%Y)M5:cjNT,1mʹ~qx[.;))O ȷK%H{W? 3mg. Y,&~89g\dnyfuceAւ  .LXRdX4/Ao.\q1Xc%|+\(u,-/¯c~upYڲ7 g2ǫƯjXM_])kΗ;o\KY+YUVѴ|݊u_*3*WU6^R~=o 7n,i`ӭAj,kʷoy5~nVv;jjkw\^IzwMuyz{t{%{Kwc-V*i@f67f4v5%5u64x:px#UG.?F9Vtlx}'O>nzTkmQmCO;xY9qBE׋ .~wPkG%KM/t-fǩHo`wWƟ/_WW{ցȁr}~_AÎg?%|zyҗ6_[~738(9_ V4- &@3OVU3PߣMn/6H*;vdJi!s9nT t;|KeXIfMM*>F(iNx>ASCIIScreenshot,P2 pHYs%%IR$iTXtXML:com.adobe.xmp 318 1968 Screenshot Wp3iDOT(JrJ@IDATx S?T$,"Rʰ~-oB%Uא+!P7"7P9_3"VF~?{>9s?~kq}>k{=* 0;Kz8#O~se=o @ @ @`x; @ @ @  @ @ @B@=& @ @ @ 0Cw@ @ @ @`E5  @ @ @ { @ @ @ @`(CQn @ @ @/ :t @ @ @ PT @ @ @ @  @ @ @B@=& @ @ @ 0Cw@ @ @ @`E5  @ @ @ { @ @ @ @`(CQn @ @ @/ :t @ @ @ PT @ @ @ @  @ @ @B@=& @ @ @ 0Cw@ @ @ @`E5  @ @ @ { @ @ @ @`(CQn @ @ @/ :t @ @ @ PT @ @ @ @  @ @ @B@=& @ @ @ 0Cw@ @ @ @`E5  @ @ @ { @ @ @ @`(CQn @ @ @/ :t @ @ @ PT @ @ @ @  @ @ @B@=&Zk8PW^ye׾V?['<[k} @ @ @澀{;(~fmV=3ӭZϏ6v8oNgA1cFo:^Aqy-|{z _X]tqͱ\a;<~~vLK.}qO曧WUm+oB @ @/0)iEtAFYbrI'K/4P~|g}w $W虉a j]vt7o'>z~ar>\~z2eJuQ9vlAw};o-<N>=iE^z衇҉'⅗DO~\TMMwK? 7Po⋧c9fꫯzXn?nvI?;;S9眓\V]u/K3sO^bUXpa_y#L/~l6n  @ @\I`\.iM7c3g&Smv0f4s?mxj饗x_cM]vYo~sWj_/&ntVnݔ;3>#6o[>F;S=:`tMw[;?3W_򒗔il$ @ @ @``[uwm5#jiʳ)SO\sMvO}^l{)/]_Fwy ~ _y睗%Wi 7`gY @ @xv m]MK,D5Ol5cN:5-2 ,@O袋f4 Zm)J6qghJ+UL7._7 #xGR ?pݼp hF0:묳FճS]ξ]JkfE|QGQʍ7رU殻:b /0w}iӦhǩ7q":~ы^T'>`~_thѪu櫶N9z~'Cso|S",x֥~Em[>saf8礓N*OSOdzۮ'LW]uU5kV,vcѹs[oG vƇzh4}_M/|=aw]vǫM`nk{G >͗o}+y;h-GS @ @'0'}ZtA*F]vۼ[7 fO?]}hcqߜ,g}vWVtnmmf_d>~K.m?ϥ^;jgl wqi喫 r^8 z#9C^Qw/[`q)t%6|a*x.78ޱYU?w1Vqvs7#6j9Ӧz;ߑᄏZ֥~<ҷ{﯂)SV*\rIx~>^zCy)^hA<| ?Zh?9ϩ7s=G.z.shSVxQ6cMK|/m.{Ʋ6m3.ݜ>~jqagBs|U֫g {!Y ]ն/.6Zy~yUWulIG>U#b-cƵ͉wscjOo ۺz͠6^L$mȢ[h!Yͥ|!E;[nm~Z9{ID 0wqGw}g un3JNvtw|#~[z=cD7,?R(֎c^K/-<4W=j{n]X+\Z/>8f_9);\aMw,Nߗ|ћE<_|q^4gy 1$@ @ @@ Uݻҩun#,.Kd?xanFy/j\XHGwevtƎc=Ľ1ens?׫"L֯Ep.mQ1~.ZoCzMqqܿWrv<#m]n?т7?Υ>Tiyomeree~3r@ v/q6m@24ZeWEiAr|)*^cˈY⅖\;~'E^/o?kApLt8׈.s[xeNv_|?Đ}7&cRYI @ @; vcz9nikh]馛V- ,PmRK ͮWS`6(b4}.e:Bf` "̋`"K8G@Yzl;œ0%ƽ߹D7qqg6EY$,e N~mM<vnZϹǃiԩS9red ;]Gt7r.ңv^3ޝ+`NjW1Gz&^"?bHN%ӧO3.AI2 @ @ ͖{eD]Ԗlfʼgђ LW41f*v[kkXשZl[6/' Yg X1qt,|BqRK-N8z6]R]~Ϣ+v!81%s-Kevޮgz({J M61o^/fݍK)fmO߻horysNjuv]/mvۻ;+^:zLt8{܅xF?5\?oS5K @ @lsD4[_h5[@"`:Ztc]ni]0-دmݨFDt-ֳDϚ5+|ᛁP+W6v.GqĈC5 >sl&1xt;[#%z^({l[e@<{֋ō'=/:`g϶m3x*'sn6/gм C|x`5yP5"o^ErK-wxoۨ`2<[l~^E5\3[pW6]l?/æ2y~fM]?yk*}[J=o-onS:1=Dk)m\9nlS[81r{7$9Ў~Jz(N/4PNqm~:èM/-#ԍe) Ir7Zca'ӏt_+v\Smů>Zbw*5; YF @ @q[nZ6;wY/\%}4Vvi#`7(1lq|*~_-[lzo*O -P}{7Eh8Zi ]eG0c-mukqgA?G|F2ǘsx߸/>m^k_\n2t.Ê9` ػStjܦ6Pvj]1>me} S:w52>C: b:ZK{m ;ꗿ3{vZGݩ4먿5;iZF @ @vuZnk6۬j\rG4˪6ъ8QΥ7?_զq;ZM:^ekZ P#oN뮻n^\.,CJ|+St1ܩ4[@ӶmA*w:n[fǎ1y=*(4A`KP7!XD-#|-SO=Uͭ{c9^hk1~O榳2,v"eelshS=K.3><@z[Lvm ˗^8m^ͮz"dy*[_WE6U%z2/Jł /0E7Qn_ Yg~:v`'A:E˃>X1lA>x>if0Unl=\=k<^7mDytj^fM=Dkx;Qu'n6Ӧ{5yKƐo>:@rlb+L_җ`2̸Os1iVu7[+{1KY~s}Njnazs;"L ׭ڪڥot.[o3w#վqO;&lR-+̀CMNDiSUst{_v^zi#D,kr)xyn=foftK`wVOL/| U/_W2oim` F~xָZ;ei~^{{A(4=gw/kCce͗ byWLK^F)qQ+;pE?Y,$@ @ @ L;Fk"qGo馼jW\ ^z*+'ϱJtkT⊱6s]]v.no~Ƹ/yK. ezln\ͱ'6oq5~Y\Lkk(_({sƱC1@HK̉綗{ZveFmT ]5\~_Vg+RK-U -̭[a@{OӟR/mU^WWc-آ_ Ԭ#@ @ @` D=Ig_~y[SVddE6Kt/Ǹ N&w_.3=y<4M @ @\vuxMdwƣr,Aꫯ>+uv\stG4w5k`O: أXN @ @`Y]w]5vj;@.e̙)ok.j^@m<4KG=$i>B< @ @U@gM:5moK+R5 ,Pcc>cv< h /D̟uYi֬YMm+x-$@ @ @Z@5  @ @ @ @`" @ @ @ еk* @ @ @ @D 'Rױ  @ @ @ @kvT6$@ @ @ @`Oc @ @ @ @@쮩lH @ @ @) H]&@ @ @ @]Sِ @ @ @&R@=M @ @ @] ! @ @ @L{"u @ @ @`wMeC @ @ @HD:6 @ @ @t- ʆ @ @ @ 0ul @ @ @Z@5  @ @ @ @`" @ @ @ еk* @ @ @ @D 'Rױ  @ @ @ @kvT6$@ @ @ @`Oc @ @ @ @@쮩lH @ @ @) H]&@ @ @ @]Sِ @ @ @&R@=M @ @ @] ! @ @ @L{"u @ @ @`wMeC @ @ @HD:6 @ @ @t- ʆ @ @ @ 0ul @ @ @Z@5  @ @ @ @`" @ @ @ еk* @ @ @ @D 'Rױ  @ @ @ @k Yfys[Zb%u]?13fH7_5kV馛Y8{T\pA['\\veԩS*Tӹ;/>ߥD!w0I @ @^^鷿m+YgU/}{iLwuW>}uuf5֨.'_NrjAt9]OD5t s:;w.` Ҿ[]%\; ^O=aU1;U  @ @ @`&e׽.խ|3LG>};nЂ^7;f)sLzrb=im5{A] ;  @ @ @`&e뮻mM~oN23\~;1[ծ:-oyKZmժq|*kݼӟ[n%]uUi5LlIZwuS[s1no|k]M/bi7_;cġ9CXL׼&-UѪ=Z7/bߗ%)<ŸgqFzF ~뭷Z/'L^xŊhm=1[n-E}DƋ J/K_Z}G|1<;3'&;߫:To{}{_:+H>G~8Ov[>GYy>?)WUoxnV-\p_rczwOozӛ|ջ7;(ZHF㰌Q+xmi}~j]fUVY%-)3f0~׻ޕgE p\uUS;D/֘ /\̌uQwYqbyw|ru6:~>G^{l{ԧ _t/D`\n/Sm~צJMAZhExw*ʇ?#1:7$I%~Qb]S-e]i @ @<&e\)FWy.ˠ?y:C!\4 #Fnmm1v%ZA箉N]_o/h%Kױt}{VTG(vջF!rv1qD믿7".ޣDO~^k_j>'u1#GGNqe;bߘi[@ 'Pv`:?heַݳow{mm{n{9-ܲZ/tKyn{mn]Me"DRL~Vroq}%?0א{8Gl5vJf @ @<&m]OG8eʔ٪$B.,}ߘm]_c41viVcPtVeX-qh!ws/ǘ9sfZ| m1:#Zbew3} `Z^b[mU*D=6[6&)Bz)m!`8w!Q,K;f޻~m=v;uߥ~ҥ6_߱V!3^{m:êKR!%׍CŚ._& @ @ @T`Q>܍lYO쮻Z.Í:mP?[FwKf_Go}^\k6mZ|~?O_4P-+21nha]1sh 6D+h%B7tE$f,/}KE/zlxu.׶ c`GpC|ows|s`sT/\gyA{+]znKqW]f~~"z/D]tQ׾V_ug=OUXl(Zkjo#k @ @<[&u]VJ-K1& +P*CX{7E7͒{^WǜmcN-bqm ssVyWXeCؼx.E%%_}պN;餓qo{} m= c`7N/޻~m|~s!wte׶wiVǵsv8V ]qcY#kz,#@ @ @& JyVnvќ+M>ܭ`xU+pzGf/S+qi}mdvry,̨{:GydK`>Vċ;/Ÿ,rKѢ:b|N?3Xt#u3Ӷ c`yi}ɻןh薹t{7sT|\gyA{]+mnqc|(^xahvbs{ң>(Jm>_k/w#@ @ @b rlU&hnsp1cƌ%bV{mrp >m:Mdv:fsY9u}mZ >V_w?dz޻~m{:S [=w95k[;kv*mvUnt/859{ 6< @ @x l}1ǤVZi1 mYi?SYg5 -7jF{lus9+<.kVqwawZTo{CI_}[o~qo}.|L|n6_Ҷnw?sN!}lows|'Ts3xNW5<m/̭{n7`9`- ch7>  @ @ @ L;^c5Rt#;ZAO<~z騣>.bbԩxl >o3kai-o[ g?K%.ee:~զo^zzO~[lQgt{{VSqn.1-eoƘJ Q:}~꽛{9g?Yknq^ꯍD=Ys׶ ˮΛ;կ~5-Iw<_k/WurG5\^׿5M67A @ @Q`RعE_Qz]¯yk0y㦛nJtP]7mعkIDy_84eʔj~V~j׋/:ל)Vp5=7o;-#=ZWW ,Ҏ1^x5{tכo\uWcb7x/x5 V[m*q~!NM0ەvJ{w-_W~.~lK`7 uv6+]znl2></oTRv뛿;Z{q Q @ @ @K`ReUD9|:v6hXkRtj8?:e7pC"9Op-7tpt +1;\][/6%X|EoP?|я~4&ݢG7˹oK1uY\rI:&g<֒c\?wy fcSL{Xe7k{DzZ_^u^}ڔYknؽ_[϶_tMe`̙3B -TR/Tw7_;K$@ @ @g #_#BRn*X}GGhg7Bmx>+\Թuw^aY:ؿ/Ҍ3]4Y WIDATF7lIJ: Xozӛ #>n38#vitr7.C_EX]G+fs^zUk<-{n'|2|颋.jJCtwtZ5~`G}/)Q.Xݟu[m3{sT_/m<~i뮻~\=!_>lvqI'U.t 'TN=/\vecIm/^|iGoŗřx6W-[; @ @vI`*iK\w}o/7ެz)Z?r-瘗'ZYFHswt)(ǾG*.l5y_/ъ9Z7R^{*xΣ ~Dk(2?񼾼\gG ^7'.o/ƕze941 @ @ @a0a`ݹ @ @ @F@=oܝu^. @ @ @`R 'e. 5 @ @ @Q@=駟LntAq @ @ @A@=  @ @ @ 0!D@ @ @ @a`C- @ @ @ {*- @ @ @ @`P @ @ @C Jt  @ @ @0Ԣ{ @ @ @ @ @ @ @A@=  @ @ @ 0!D@ @ @ @a`C- @ @ @ {*- @ @ @ @`P @ @ @C Jt  @ @ @0Ԣ{ @ @ @ @ @ @ @A@=  @ @ @ 0!D@ @ @ @a`C- @ @ @ {*- @ @ @ @`P @ @ @C Jt  @ @ @0Ԣ{ @ @ @ @ @ @ @A@=  @ @ @ 0!D@ @ @ @a`C- @ @ @ {*- @ @ @ @`P @ @ @C Jt  @ @ @0Ԣ{ @ @ @ @ @ @ @A@=  @ @ @ 0!D@ @ @ @a`C- @ @ @ {*- @ @ @ @`Pf̘o4k֬tM7+c=* . zFAQ.-9d\n햖Xbtuץ?\ sߥy[ss2'cu\Ӵif;5\<ٖ[@ @ 0\6{Ӻ[iå>$w˦?>;r)CrgRK-N8jO>kFwln^:S}hY$07n]/D_z=S~?f5wܑwb @ @C&0i죎:*[n9dq;?{O+r:裫Ü~i̙=rn]gO5kq>CwixD}lNv\mԩSGˍ&1A @j@IDAT]^"MAH'Ƃ˓cĈcL&4FlbAi! HU<ܹ{ݙݻ9w933sN֭[oӬ~n1c؉'h￿5n-ZdÇ/Uqo޼}믿ngu;߻k+vnS*WZes̱Cʕ+m;M<{=ر׺tb7nٳg۽7Syѣh"(ĉl|۷ִiSilڵb kZs1ǘX~IRijSNΣ,?ϧ~j_|{]6lp^|E[dICvaֹsg^z֫W/7=|OxT'ɧ?f#޸I￸SxnK\G}s=va(٨Q#;SݲѣGӋOSy>3+I]ƜIs~;G}侳ڵkgz=::쳭e˖m>~ocU .tעzHq&LmRYy ϴt8YIrLg2Mroh*Km `GEG@@@ [ ]t})kNAoXΝ~nذaz꩔K;|'ũ>C/w]"FU{?Mf_}GhRK/.=_RL:`6wW^b>S?SLAU\?vXXl]ϒ9l0{'C {97ѤIbuzM/ŽKTovMS}T$1fJ>˺OJ=/1l ѿ"3zuQn;qULL=7DgśDzjݺ^8iv٤ig=SK@O8./sfe}og:ϥ8yӶ2_Y{=_Ice]/q,}:Hr}jpHa@@@@(~?3\mٲŵ \nݺn~ksNZ ,p-x2Z)Uwu=X3y%TIpj#     @a \_Tm_uj1z! wEq??~78p`X?ੋq%֭.bLI,FO> *n„ n^_:?ӼZ*p>)ȩ`ʕIK/C) rwW[޽yx{7sj>"էjyCGpu~}W3zA p+U[另LǬL7Lw_q¦+]\?_Lw>^ѽL"SPQ.[Ľ{.>~&}~&^+K[Y5DߙJZ_q1ݥV֯f$`6>!ɿ ~oX3\J5o\g<~^/3,} M{}FH;*<    P^{Tt?֩Z7\~z}@[cS?n7zǻr`pwOS;f#F~?}*a p|Un mpA|:33ëtNU'v,H'"YL{6|}ƹ2)_4sxj\>_yLw>|ЍWG/ӄZ *<{l7]Q.?{%ltgś{ >żjH_:r?)ş3 R=K N|.%͞[Ip>eїi:_{,|}E%Gҽ>z$a@@@(L `kU?k>MYg?k??X'sF ZtM^/vZTkB%V~fߢY-*NItղ1yjyz駇Wem:=s@VRGʠP Ω nOOxY  \PKvڕXLpN'3ˤ|}|&ϗhӝEAk579rdpZ88쳃9dxD/Ig%tnx>X*_ \ ի[Wd3~t~N %u _֏IpeN'|~u}teRh>|D|.9skqܹB3ဥ+sPy)޳ggś{ʈomj'OC/h-wAIߛ+|.͋[ƭי?>|+{%)+ >4nd8<-p43.7T/Eoϩ.{1{W" j~9YL 3gδ믿Xtpn{'H'yLw>IyKSc󙺨׋6my*hأGK.O9ϸ_zφK8L'y~&^ƵJ C]<+_XVOzb%5ο ~o dOy>f[ƭY8qs}%^ϸ3O6} M{}FS}G@@@xVwZSEFV+ 6ݍkjc\j CJK.ny܊2?[SwAklgTݽ{ń5jũ}ᇭQF. L=A-Zdկek#\G<;ˤ|Ѽ}r|1L]C&(0@L2k=|gzߦs6q$ pәNLzx̤|_1Q})EO9 f4O̗s)iezOz9/|yerB^|=;*<    P>엿).mjժ`[m+?ܝ;qƹiG-f^?cn&LwI=aNu VklOs}RW%2)_4Cxj\>_yLw>.>R^Rk<~q$ pә 3M2Wְ ￿p )}I7өt)Ryn-{,|~\ox?/|yery[KE]~9@ @@@@*PPlu_nlRuGe]G?Zݕ{KTc<ԩև{zn… M)qӧOJxرɷfײl/|?}7~fN::,.<ZFS :ZP}AVԂ\-!R]~CY+ 8'\Gq6It:./IʆnٲM{ᲒZƹ{6\y.k>3M2:!֢E G$,/R|MZI|Kzd+>2ut~%F}ExY;ﴎ;[N?` @@@@lSˉ%upx>R׮]nիh+3j:4i9i${ǭf͚.۲eK\^͇[ DUO (ʝ8?c& \cOk={V4vZ\yuS^\w/mq.@D۶mMȿ曔cOg~Lkq/o#Gת&oY_L}TZK[KZh^uK4gn__g|V`8Ӿ.ӹq=Sp~ӝNLrx\}v1}Ǩp7̚~oƭߛC6Ry?[ƭw+uc\IlKYϳLp]h:3W_mFIð h"a@@@X"]i%ZJ vnڴ)\k'|ּ/՝:ur?mzj[x}gkvQJc-^.`?Y_O?})ժU*U4DZ~zvMOHR+:W~|/2zxsc~яezuyGkXRcƌ 'g 1'I˗3ϗI]lݼe~u}]yj^O@@@@b D;ڽuV\^zEjEaÆ70WRSZ%}7첋{f͚@jc-6SKMTb~+Vؿo_?xSLqDUx hY9tP{駋?n,WWjM:[oZGyVK{kO*!   (vvIx|~-/R2B |p R@@@@@H @;Zx=z%\b#G4uyӹkM[h/+@;u[_/WGC@@@@@B a- ס4kxLb|ے%K2<b^^  @@@@@ Avؾ}{kqVJh \ϟ?߮` 2ĪWC׸ +eV     (@;ڠAkڴkmjժ,C!@Y~zʪ_#     @ ]9R"      y/@;靖 "      CvgJ      켯"2      Tؕ)%               P9`Wz       @ *"       @ ]9R"      y/@;靖 "      CvgJ      켯"2      Tؕ)%               P9`Wz       @ *"       @ ]9R"      y/@;靖 "      CvgJ      켯"2      Tؕ)%               P9`Wz       @ *"       @ ]9R"      y/@;靖 "      CvgJ      켯"2      Tؕ)%               P9`Wz       @ *"       @ ]9R"      y/@;靖 "      CvgJ      켯3xfϞmK.-{@@@@@@@PvR8kѢլYӪTb6mիWG}d+Wl %=~ӧO'f'S@@@@@@ȱx߾}aÆ%?Nn޼y[dɏ 4SN)S>ߟvl:v@@@@@@@< Ceڴi6l`3gt-۵kgM4q:th֫?p3fآE?      B'|U^ݶlbC )bߧOkܸ['v 24@@@@@@ [UVֻwoW!˗/7|H[qu/_y`}nݬFk Z=iӦnl?jjr׺UV Tk]v%^`;FjM޶m[ӸժU5kW_}eӦM md.]Ly\t:urS(wMqݸ7nd5{O      Tz[/C?~-8m݊,LYc`~֡Cbi^bu4M4)egϞ.Xk<ѣGY>@@@@@@@ zqմz˖-[V~o}`` uzZ zּys-Wkh%4a„b5.wf_R*-ݹsgSkq%Gi5kt׭[gÆ s`y믿u|ӦMsM箻j|_r;;,{ԨQEc@@@@@@*uApp:|YM8nGJsεq/-ݯ_?չjO᲍?H_~Z>u1שKvuͮrᤠWW$@@@@@@@@"VvmwEjo]ة=SM݊ /Sڔ)S˜nٲtA8Vc9ƭWK?9};ڊ\h,>}m?3]^˺tNm)pI'9h׮]kÇ>ͳc_k_;8 _        e'j8ݴ؇zJ*9v%SnY??N;%G-RYiĈnhMثV^{M$EnYIA `xֺukQy;u ~7 Lp4n<ɟ?       }g͚e3g,-9WAS7t 2iZAZkx<˗ۛouݭJwxC-a^9y!o%=U[W:͛O?uj8qiW@@@@@@(իFvxb7\signl?ףF*-NC Vwԯ_?QE[(vthGaM6u_O.~g:tp̣ǦM Vgj޼˃>U+=tP        Pq_~icƌ)%>{V:wlݺus(){ذaA};:v.-X=m47V7Omĉv}m}u5M>PK/˓r-z뭷lٲeL"      @! Fƍs,v `x≮ȑ#mҥ)3>qF{狜G־}{L}O0!XӂѣGے%Kue P>|"/w}/N_R[cwp5k/\m۶={eՋO NK      @a iz冖~WS6lun5P+2ScGiM4qXSN{õ0O8aܾ%uM>H%mOYlz…{#>4FvժU|tn-5lzNl      ^UV+_ڵsϙ3kh;.MKʂ'O.:+4YJk}15N|-X X^{Y.]&V\iD@cZh"{w4c=իG}TR%Xn= @@@@@@*`}E<{Z׮]mԩ6ecV35̙3Ëݴ`ZjYlb {7,K51ܯYf o4ൂpKrS3f?;i{ ;㣫G@@@@@@PX1I&ٴiӊ4l߂Xk5jԩ1J:t08WZAL[3Qe˖86      @YbV\vdw@@@@@@@ `s.g`      #@v9sx@@@@@@(\zUZ/^lov9#      Wvŭ;r      삪N       T\ `wŔSڔ)S*,9G@@@@@@%PVJ֭:bQ       eŋ `iő-@@@@@@@`ZR@@@@@@@ .ku1Ѿ26D~U?p,D`{ 4mԾRq[Æ m/meZM7[UlĈ6uHY+5>l08qbA B     @Dv$56^x<8w_y 9Qn};ϖx?6Z^zV KnsOUVpڍ7_|a=rM["ÙVZ_W!C3|xӧm֍xb:ujG7x̙Sf#۷kd裏;`g}l>sR~vqn `ZСCmʕ)¤KASY… %MA/t%2Kiy)i]viB1c,.=zX-'qw=k?gӛ6q؍><cdoU|;2@)]wpnzX]jΝ[ 6,yIǼ袋裏.(z7A^xm۟y{«Ҟ_~I2q+/vaÆz/)^o&e`VF "_|r /     @.`v mqПZ?c-|QZ ryF[_rM+X1:'f֘J=ZtBWҒKj}ԯ_6M{]1jD3 ꫭwnxu>~a2Ll"\/ 0us饗3VZ_v4;j. n:L-ЕԽ냵@mRw׽N~.zIEIAmt.o3Oժk`qw]}|    Zv9 `x[Wui]{wpc~>.[f ? zڏ${ipڍ 1lejW^lKFm?pu>`ZUv>ՂQIcI_{n:UW`Gj{۹Q`SN%uV Z7>n `3 E$)>V=K8vIZlxkhO^N+[޷`q/~?>@@@@@\ TAŪV+$oγOoؒ `H;Ӆ+fLw_օ2/evn^?g+OS@b]w=PXKppR }@kܹíu6ꪫW7:ӏAg_Y>}.~mbW_3n)K kРn=ڂ^AbJl^[]h+EZ滉AmGޮ];J+)R%O];s_Z ໾wp6츟Iʧss>nu擤lyu]EesPVҽoq>_~|"     ]x } '8c8PI{6:A&OV-cرu.M7ϟgD +ᖣk׮3<38h͚5ˮ`H]VmsڲezioJ ʞs9KFqUjQ`"n)K ^"mh3IDAT݂;J@ߗMZk<`%~ׂ`y4_Jz4|p8>%-_|un3gZJ'B\-ŕ|.܊{ j3u #$ӱ}ǹ"BӦMΧ麷.mLg3T-ciys8q/u     @y TvgkIsٶG-~{7ܪըxwDy^5svmڝszQn<6._̇'|yf-t0>[o gyȖ|!gl euiQZFZoYJNr*8P|w] *P֭[7qkN27%U15jpeoӗ-n{֥KwL_.HZt%z<_c\*zP+^p6.|OZ>}v8ʥwA aTyOgY6Pjժ`pKM;0 vy睶tRU8<&vI]nҎ'Зxqa5Ş>}z^7s;mf&LHm|%W֘￿p .KW\a~[~wظqdK.}=Ár)rL\!ך=7']ęԳCyq׬YcG6ӭ4qĠ|e~qǟO@@@@@ 4ݦ-40DXNGd_"4i =z^3e~")i+ǎ:o>?<[dae P ڨҐ!CLt+>% hi{۵E]> +.H-N[ :4fv6kJ L*@YZJxqa.o?j{v뭷|,\FJZ wqhe=|}zt8 w;b\| >7`O8[iz誓Tlشa}Еlۂ~~#[Dkߗ^zX=zpzCE[ & h@1?#Q˻vj7t k+,2l8xKpͩӲL/}衇7(~8-US,@Vsu>u\jb6q?}y-Fe'uQ-I}S뒖/~>\l+)Юc!=ևؚuѤIMڤI5kڤ.xq>3)׵ +?p0_o={ u VĜHOzɗ=ӽo>~Yl/'    Jv9Jg޹]vxּǡ% `+]ZyX>-RuO.qZ{챇)ηiӦ pe|IbWJ jEʕ+sS[ck+ekgw?/h(3fLuRK'͝;7;3uC/hkRIc~6/[2W^k׮_>-^.R?IR.T Z)< `wB/ԭh6?vϤu~x\pAMyjՊ\Zu v93|jA@J~s9A K߷M<_~     +(]a;3,{`Jyglڽ74QQ ܫAǮE/>c {/*Sl:M J֎}x^}݃5oӲ3lp`&*7VAe^I \|yU>VҘ H)) hժ P~Æ K9δVJрZL*`}O8 ߵ.NO=TR?*X-ԕTjM [upt]yTk}0V1b 4-SU5`'uͭ +Vؿo_|IS Z赩 s=.H=e0`Ϧe]5ڬY3w_c=OR.ӝOZ>_Iw}<v嗻gt=Ugzz vZJCY;m$>_~Z%#-     @6`gC1ǨբЬ7ۆmӷ+rh{Ҡ q˾_-Zsq$r֭yvZ7oM>\qׯo?X&'OƜ-4x?S5meWR%O ŎνPZR5app-/)u)8e-ϴ|e?zU Ϙ1#&Y_+Ϭg"    y&@;*d{d DH.?ۏݞh     @`5C@@@@@`;qA؞g:o?}7g@ ڷoo^{5nتTdL:a@@@@@" F@*РAkڴkmj*L@@@@@yҥ)SSN)STx`        @Y*[ޒޡUY윅       @YbVc@@@@@@(PZ @@@@@@h+Z_@@@@@@@@`hR,@@@@@@@ h5F~@@@@@@@ ]K@@@@@@@&wΝ;[nlԩ6eʔI~@@@@@@@ tĞꨣQFn lMB@@@@@@(l޽{[Vl,v `+S\ie!       @иz6t]ʍ|ݺuؒ%KJ$+@@@@@@@*@ݭcǎ.SLznlg?թSetҥrJ۰a       @^N֬Y3V+Ĝ9s?.@5z֢ER3J@@@@@@@)f5k͘1la˖-]k5kE|"      T@vZ[z;"M;        @a .zT       @ ]᪌ #      )@0R!      Nv22      ¬WJ      T80       P ^)       P`W*#       @a .zT       @ ]᪌ #      )@0R!      Nv22      ¬WJ      T80       P ^)       P`W*#       @a .zT       @ ]᪌ #      )@0R!      Nv22      ¬WJ      T80       P ^)       P`W*#       @a .zT       @ ]᪌ #      )@0R!      Nv22      tj1AIENDB`unioslo-zabbix-cli-09a2fab/docs/static/img/plugins/help_empty.png000066400000000000000000003662331471265333400252670ustar00rootroot00000000000000PNG  IHDRh!_ @iCCPICC ProfileHWXS[@h)H6B AŎ,*TD(v(XPPł]y+ߛ;Μ;sNpDlTa8:ȏL'p01##j^j/g-<~.$T^.7q5W$(ͦ牤V%BXZSx&6qJ*8ːsӡj?ĎB@bZ1b|A|8paY1 q!$Z9|a\OF+Krrmx^Fl=HFdP %Qao d΁ ͆U!;ÄL y Y`b ǽqO< ^}au<:  ]S⟢~"?.ա2{a>г dYY4vdG2JA%[?[bY$v;5:vkڱR_W; ?o*`G ttB1ppn;"C"D$".%'! vH$ɎE qHyb:.qR7郒RRPP\i1+Jϔ>drGI^NJn!_"w?S4(V/J,%RAܣQVV6UvWR(WPޫ|NGM[D2*'TnRT_j25ZK=E}@JSuPeTV6^Q}FVPcMV+P+W;vIOnRURoV>ATcyMf&OHs)4fFcѸӴn-[+STkVVv *]:['[g~:F`X2~ĕuGuKt^G [רw_׷ՏҟA~H##KFy556eŠ`0PdaQjcF4cocjt&=^AoHL6t|623-4czߌb0K3[mjonl>|y "bYV ,-{tVVuV>Ӭkm6Y6m.ۢ.UP;W;zQQjFݴWg?tqs(thtx9|t蕣ώ1!c Ǵydur6:6p켱Mc_9978rwYUZfVvňd,es's?#c_Y;={Y:kW7;{wǧ瑯/w3 3Ow=˃5u/ xhX4+D0!84xeM!ˮe i U }f&kjppaxc`Gi9-p1*2*igch1SbvƼ]{7:N?16}BYBW9IMɤm&=eb&͘t~GML9BHIHٙ SS,Z /o5/?KJ+KIJ_ޛQ'` *237fϊڞ5'G)'%Y)M5:cjNT,1mʹ~qx[.;))O ȷK%H{W? 3mg. Y,&~89g\dnyfuceAւ  .LXRdX4/Ao.\q1Xc%|+\(u,-/¯c~upYڲ7 g2ǫƯjXM_])kΗ;o\KY+YUVѴ|݊u_*3*WU6^R~=o 7n,i`ӭAj,kʷoy5~nVv;jjkw\^IzwMuyz{t{%{Kwc-V*i@f67f4v5%5u64x:px#UG.?F9Vtlx}'O>nzTkmQmCO;xY9qBE׋ .~wPkG%KM/t-fǩHo`wWƟ/_WW{ցȁr}~_AÎg?%|zyҗ6_[~738(9_ V4- &@3OVU3PߣMn/6H*;vdJi!s9nT t;|KeXIfMM*>F(iNxASCIIScreenshotj pHYs%%IR$iTXtXML:com.adobe.xmp 508 1962 Screenshot !iDOT( j.e@IDATx SӢ$T$*%"% jl3}`,36uf,%Fd(M"B p~=>}}~ۗwsSo 6P@@@@@@@(@="I@@@@@@@+@P͍       PTr3@@@@@@@j@@@@@@@ T!      Ts       U @@@@@@@{@@@@@@@Efg              E .*7;C@@@@@@ @@@@@@@(AuQ       A5       @Q@@@@@@@@@@@@@@*@P]Tnv       @P=       PTr3@@@@@@@j@@@@@@@ T!      Ts       U @@@@@@@{@@@@@@@Efg              E .*7;C@@@@@@ @@@@@@@(AuQ       A5       @Qj@nE]ȑ#?h\p)+y#      .yq뭷6q3yh<@i;^xa4w1^zi4^.bS^þbM,K_0V\i әPZ2ϯrG쳏j*,B~oٲꫯөtQ,|F1]v[s8h88sM͚kio̞=s=FEa駟O˗/7Y`ÿ́ eZhanh<_o۶*fرIr~^{QʓO>i+;vG}fljEe'X#FDu]g:wF?ܜr)x^{uY4nn~R1.$SOG Fƍ3L7mG:t0W]uUƴ~p 4x≦O>P?gÍau$.gXX.]jk3k,Cp\KYP~};KI&z׶ #    (iPcE?;,N0{g4M?#8S mvPlBSP[ZnK[o5J4v)8U`}*hހK.Ć[ qs)ӦM3w^ƢI{5͛7}4fU'Nhޱ5ڣyk׮$;\AgϞ&,[~nԾ&ҥK f̘=6{|`T)@K٦yzbbv8u׃ oy zT״Auf7W_]a92rUX`ՄE>:UGс3    r!05F\3fy饗h{㎦Yfn$Q]L0RM7|3:po5jZܹsKT90y}QlAj$OvW[Te+qA T_SNn+ AuRϸ7rYgQYչzb֫p^fnZ.׽mKM]wqn InI\r mSOݘ6mؖ)u>r+     Pk(c)uP]6(:$AuRW ΌRHpYC\s]6 UsZ5I_k#k5}%N{~q1w}|g <Ȍ.<،?> [ζvQ:.@VMc5٭[7{~P3n?Nd sY/ x܃ S%uj)]k*qjNP%g0V BoaWc/4wI&Fk7e@@@@I>VwS?"%K>KdWVߵ}?` u|uPmKհӏj&UPźVXauW5;Qټy+DsqUe;=zjx}Mj ]vv=k[qyէ_ b>hUJI_D{ 7ѽ~VoAu'$R?z︢ B&WڬaP6{mvڹUm0.,a?O/0zi/.?[P}嗛=zDǫg݀Vw  Q:MxbsQGCz*VI-Yn:@=sLsꩧ5ujհW7|c~ߺѼ_ΒAE]dfmO+;}צg]w5Nmz0     PjuP}g4s~cɘ|w}3&# AN0UxonآU ŸrJ)VW,qAL3 U V}Aiݫt]8K+LM۶mz ,FR\y啶`2I4.' n 7lFF<`6mZVTa}SGn -^s]/ڷ -V?ڪʠA>K#C1QFUzi<*uE˪ᬚ:?r5TU}93)ڦ=tAv㛻izWTko@![cH~ױ$u,sȻa-8 VZja&     PBZT0۷..k5n#< ̨ƸJX3Lwf{-lt@@@@@&ʠZu%橹n~Qf…d;R|aÆ 3\E?hZMv5_?+PwEsqEQa)FPS XͱG~WxnX{bN;V}ZYPu€mG NuT֏|UwQ^V$Q_7p@á;=H \qàZ4hܸ-^DZTwPRvӧ=|}F丠Z 7O5_hm= PzZCB4jU /ضj5Oʂ\sA{iN8h?o_+<|7Or1j)b[!p^'(I_0v8w_=3a_mOB9j7     @MA 0BY5j^XTkU?fmР]D}~h~խ[7s5E])UiRV~ TE?,qUpO5~|Iqvk@Vk6d_tpX!k7pn8^׃긖[oe Mq.܎W̠:|qMW_5y*<5e>SVVPZ$%dkE + s]ax愵|to:+ æGm$ cu_K.Ao̞=;: ,WzCAtXE>]#w"    5Q>>+ֈ2aUpXC7rHƢ~S~TL2%,^m㚟v5 g5Gds!bcNTj|93g/fLPգ>ڬ:;kRF3*A u t+?Q֨vWzCg+T=0To>=hҳgO3`g4& y1:Vp5{ۍg{M~àZ7j}qUչj,Z*zqTz7 /Z#y4NKP|r{ B{\j>r+     PjUPjەf7ݽ.[,oi5`v)RZ~̮85=6͝6VE5]ʢZjwp5Ja ǝ&V. kAV٫ +iTMsKB"W=j5UjW;WU;N{~qNW*zA-v󵬖+м-WܯG55oɒ%ѹi<|Tu KgչpY5\ Բ sU_ԩSt8.&1$NzChذatU&]\>EWn{i#w\"    5QAkvMZ}B;K/lnҹsg릛njZlj9G0PXuE@Gˆ 'k:tֳi}W\״eݫ?wӋF0Q_C\=W_57tSt~袉OEi MP&6mnʨgy&cP?ȹ3gFPŋ3e{aK fRҸݸeW~0ms)1cTǎ9esy ??'y=?=X| ݲ7vZ[ƽPZwVc2/ à:\\R BQ\_UIi] ZȮjN=V¿+7\+6wh15}sxLB@@@@j@Ioݴm6C0,0q 5 CV<> ME vܸMm֬ĻzU͛7qx!T L!LFM>=hOi=` lZAvErvk@6q綣WuΜ9Ï>hFjǪk N\% JQ\qMk\#t> kԠ:e ,@ҹZV·ރ'xb?lN|{.]=[ ?Cu͛g|s?꺇f㺸Wr%[PcTj3$ l WR~/(zΐW@@@@@ NAj5լ#jHV 5M5G_y[c_F+~a 4ƏoժAٷo_INj[ 40B2;/MUAqgrKsAEa,;Vm?g8kZM훺6z' dwyg]QsGsO7Ɇx QfҥFs1kUZE5cS |p I\*pB*l]R)t~mj^C$׻{PE b0aN{~{,S׎\PW}}PڢE W.bKO? PAE(Ef;|}>WXuƳA;04Z}ݣio亇ALA(0ZU1֥Z3}Õl=gx6m2r6~N:\{=g+     'PҠ: Mӏ#WI Ph 6Ȩ6tP*W8ZM:o暔Q\Pn dqA4_~~W@`E7֋i vS: PTzڞ_PJT@!kjzx@%]mr.p>Cdi~[ 誑wOy!O:$ҶڂԪIx_WwjuChr)Ks~,5w]P/SQ^a@@@@JT`~ R?\crJ^\p_T+1MQjIp_ϟe ^ePZ0p㾎C‹/h)zguӧOe1{7Ǯ̚56񰦵B;/6^[& :eZ< ,K4Wn څ2z9rI}]4WY`wUTGhހ>?0j[g+1TWP@Xs5?PRb՟~ׯ_t>FA;àKP> Yr}olZ޽{(y3tvXÿ? s=\hz^PB?@@@@@_A樣2Ol/\qQkMf ϟonV;U쫂c(e͘2eݿWQ]<ݱcGc3lۋ/8 MxBLWԌ!VWxmܲi% G&jHuWTkY_M!jE+iuqf|]d8p9c2 X5#j]MXT#_ miҤIF3i\<ȸ%kpoX͞+R߽~Q4x`_z̘1ufϞ]a$-DL:BIϯ5B4ZP:={F[h{v iunjY}ݺ9~~& kÇuw jCUk纞vѶԼ4AvLut $FR>O"+L"@wWIe׮]s]FDV (d??ԍHrE)tWk"_R~/(z     jDPN5ԿV3'Ntkǭ[jժjoբSm^{-TjU?t5cT 6SAj*;<횰jI{z_Z58C\=|'vzpYgZ HZ)on C-Zd/s}vQFvjU聖>O=PU)gUQ}MXN{B3>|    KF:4Qs DUkF5U X_XaMs`j@\&1\sTX5g_(¸$~~}C     P9^uQUվr@ \+OV{]{EyGM    '@P5%7nC7Me-fâsi?\ u2Qϋ~t}    Au׽o߾f=0믿퓵Ae˖ {L0!ǭ       P{9s@@@@@@@$%ag       @ T@@@@@@@T"      +@P]מ3G@@@@@@J"@P]vv      Au^{@@@@@@(AuI)       P{9s@@@@@@@$%ag       @ T@@@@@@@T"      +@P]מ3G@@@@@@J"@P]vv      Au^{@@@@@@(AuI)       P{9s@@@@@@@$%ag       @ T@@@@@@@T"      +@P]מ3G@@@@@@J"@P]vv      Au^{@@@@@@(AuI)       P{9s@@@@@@@$%ag       @ T@@@@@@@T"      +@P]מ3G@@@@@@J"@P]vv      Au^{@@@@@@(AuI)       P{9s@@@@@@@$%ag       @ T@@@@@@@T"      +PvAifZhaƎk}ٜ|vpgz^xL<9n3m6 ,1SMסͶJ'pAĉ>^zTѣ?^aze_e׶y뮻۷` ꫯn}SO=Uju~./qKi׋;Rq*LC@@@@ n{4lL2\xqNK^uYyvª$nƏ>hӻwoӪU+k|>3<3fLs9ÎuQzӦM3_%̯ סJU {̓>zFeԩ'gb_.]~ջ߿~g飏>2g}?K|*[E@@@@%Z>sxriQ$w.7xرcEUcpxӴiӪ3 _*K@MIχk@um W  ȏzw!zJ3`OM.믿6_|=\ըּr-uͅY?Y<    @JT_p[o6l*]Rը]~0մj/Քr]wZ:e˖^{L0(j̖[ni.\XZa;5\t/Yv#F ZSCAOÓsoIjmq~)vmZC5~{x;x]t{VUo*N@@@@JJTWrlf=0'x]d&Iٶ난RَwygsCXhQ_ Ҷm[뮻fL[߮j}]~|)5T5ROm : s[.ZX(5w|G@@@@|jtP裏V[͞_l}oZha[3sLV+~Pٿofmf~Z}1]OqVXFi>pw}Mvʕ+Åg6xc;7 I4~=^ۮgmc)Nzsܹ9Clڪ7ߘʼfȐ!y \POfͲwy'vz^7O<<0`YkѣSNfƌY}(z//+2Z?zni<;0sQ}u/ҚTfmX)>8qbv^T.5Ӽulvj[T0yF veӽ{w{͛77{úuOٳgpZ#s]7tSA/5=K+`yΜ9FyXO gMN>rHoi]1%~+߳2[r)B\/    @]Au>}Yge. UOp* ںuk;y ~vTOTY9UTZ}(M_]OS 750@Hj>k?lP]uj>E59 Տ׹wj4a3w=^yvj7klO︇ 2N0Lz_'EEj>2AӦMMǎmӦMj^zv AuCZwќyY3[El Qu~ji]s Bk3 rwgd~վ7SWXI'dCW]w`(ʄ Zp%H|wi]|ǔ;,c*|#    PWjdPXVžq6H\Wv50eMk5ծ]ϭ^U;ﴣZ@56r) 9)_5&yԯ~ _|y[. 5=O*C?q楗^7=X(H:.2UӺ TZZuϫ_A[o56׭pK/5Uj&EMSV~k>⪸;zFZOOmcرꫯ֠-_~yT3ZwEM+Vh} S}&!<`S/zfD\``B>?wIUhmt6V$['(K.Ӽ&qI9S>NagVYi     P5.V`B%)\/  oV>m-hĄӮo[ù&Z[n6 S=95jn30;n̘zPlWa jNS=;*~ ~X+TKVŽԗKVq1c5\c~LXdtC" z]ps1f}:GyLj{PE! ^x!zPǹU4<ПU\?w\w{?-{hC!~ ' dmM>JRw\i^suI9S>Nag,VY?!@@@@jTPZ.|:[mmyQE;vd?᯿Kn{Ipvk[^MiYdQȨ~s-jbWZ?պS{O?&V-Xc:(dmwY5FoZiB}oᆉB~X^Wj#]8tPsi}%SVQ&]ٍOROO];ߔv/4_bzeǽI\TCZ ƴw}檱ڷ*ZѰ3S=u)IP]Y|0`(UJ>JRs?|$ú|K=+sM@@@@@ _T>uRUT\ aԳBJ-v=50׻2qU h>aqS]Guob֪u۬5ubÆ )W٩yx+dI{_= $ka5.0v\s.NsF aqA_;5yV[niֶj{PEw)$:B!KiIBK~?T6k m}|)Ke]ռ\]~I2x\gůT@@@@H+Pc?=,-}?gWcz~isu&nyƙ 5D͵(Wîy氩U׃j۶mmXǮ&sQgy7)״uڝ=vTOT?yUXͅjPi!CD3in[N9>0{V(qA6mdJ :{hAU~U^esMs& .M:՜T9k }wϗBTy,KA\?;o̵C@@@@|jDPߪYkDՇGa'0˟a$M2vs1:,ٚM^\uRiٚ-wMo'WAf{oԺfVͳ:}>.ӪU+G{/6j ,j^ѿ;&rU\4pN^>nIY?pҧOvv9՗_~ѣsk84?$Au!\ߥLنs dy|)Ksez.i?ceYٕ=+ޙ    @y ԈZ+O-Lvrmj?[~ ôo߾Bi k`tb|㪾~_y~nU&Nh.2;\\7m뤓N2 s)T&w}iٲf}v|;itݎvi9d߿oy衇 .\-+ko ϶<.~Tܹs '\?Ҹ^{eWWz,1:?[ŘrU@IDAT/'}BK9?نs dy|)Ksez.i?ceYJ|N֗-[V2@@@@@ɃjG\ԁMnvi׮=|ðfGƷz.q5+sqǹQ՚dh? h|4iĆj~?4rYTkU}f}R3 jF5h9V[oe*}]v+#<.RuW :v5O?r+k_~=o}s/UWPO}n+ɸ_.|Yx=ymzQo袋LÆ ~-mw+kZ'B !n4ٖ uOq/u>[<\նwEjF ݼBV˼K|]ߥ},#4/d9&ꢍ"oag 2TYYՕyC@@@@ 7~ߗ\k֭y5paحWajZ?W۩L>݆: O?xpѢEF Q6ds饗FڦBX=HJhnl~ CM6Ͷh47z4v7j:h[oը9_Wto^MjnvH^i|v5emK  ^zs֭Q UtX"c&Ljj+>zcMT?^Eѿ_4jm;J=EM뽴k٨ݬY'lO?V5{a}^Wuq:;_k@6HOKe]ռ$.i>sT6LH=:Ӗ1@@@@4Z͛gxb;]5F_-0k׮< 13&I<Ŏ~T`?p?p|qSay̽m.IPj85k,No76;Ij(tTS֪UmÆ &yiiS aol_|1cjI=zРAu!SO=e>-龺;4@S}ici=q=U+u]mWs)M 8_nZ4I\1sT6LB~kdk/Qi    4ZFVҥKmhXqUM5H7;봴e` ѹsg[|:uj ;¿.]O?FbR:uK,1Q϶=P={49i$ۗzeK5 TT!b9w⮹B\G}O1ARQ~ ici/]/    Wbo        T+       q0       @ >       @"D\,       Au       H: #      +@P #      $ N        T+       q0       @ >       @"D\,       Au       H: #      +@P #      $ N        T+       q0       @ >       @"D\,       Au       H: #      +@P #      $ N        T+       q0       @ >       @"D\,       Au       H: #      +@P #      $ N        T+       q0       @ >       @"D\,       Au       H: #      +@P #      $ NUڅwa{~;wni#      )6~MvLFLz̊+ŋѣ͂ RrVkmHC=Ԯ'V3"      5T,wݴl2\Ҽfٱɓ'w}7ՉX@@@@@@j@ս{6;v3ӦM5;udZnm+8p`/ ջ=S/@@@@@@@b]P} ~l UY!+7.In?/lpip7OM&u:(Wsj      H}Ǭg Gj1"j=Wv4O<ټ*ق_v;qm^{es=ap-g}̤I~_VZ8hA@@@@@@@T+LUǮW"M~ƍl߯t\Pg^\ 7l3ӣG;ڇ 3x`x?6oV4Z밯h<y VE@@@@@@ (wTNEphA/~ _szr=̱ _5\NͶ8Vc^xӬaWz…fȐ!Qt~ڵӲmۅTf 6ǠcԱ)0PwA~饗Ռ;O?(MOS?؟}Q(n@@@@@@@ (̴iӪxj6zwx䯿|U\ԨVP6,.t?pvAСCl۪-.lz]vh2.v~jm_M?A+W~ސq$FuR1G@@@@@@6lh+ `׺Yl7˕ڃj/_~cyمjZX0 k:٭n6mQ?v zkӥKKY<,zX<|jmck dt      uP@Ynk+1^zTo喦[nl2.u޽b-adK]  þ&ve)Sl_n\_8qbպvjj+;]}Ek?aqeSO=v֨6 P\ f͛e@@@@@@ꢀ~h[O^y3{h\1ύe>s3rH79W~Z}kio%Kgy&cmn4]]W#&vҼ"      P:vhzm~cOڂj5a>RL۷iݺݵv0lٳ6~PY}ٚLkcǎP?UZz̙fԨQvKú~v7j(# j s)~pkvݍUԩScNT˨Iqժ<R7.8      aeʌy3eʔ gZmAZauM-:t0M4neZK.F5?;^Qj\}R+L"f͚Y7o^um#      @ */.:T.!      $@P]MP] H6      u^@@l@@@@@@Au.lׯof͚eFQ@@@@@@@ T׽k!      5ZF_@@@@@@{% {aɓ'w}rF       U6`bsbU ygu@@@@@@@*PU^LP]C/      VzJxܫl~|yS8    #[U2f ̨Q21@]aLV2N?7&LȘ d TgzXӮ=ML55s?6l 3c8  8^zu@o߾6pY}a橧*8303/wkm_|]w57ߘe]2SN5fͪr /ĵZ4lnB>KUڶmk6| 9s2eJLyկ̚kq`7C͘ d TgzX+5MVՐ1Nձ,LDN`6_U^l?3gyOȑ *,&t<`o7j_GksƴF \rse͞{ǘE?vf +gx48M~{ z衇cNҥ/׉w>Ȝ}uYyvڠAߟ1G?gug-=Rce.V}oZxqYwy]-ܲj⋅UmhÕ͛g N_{tKM){キYm~>MuxM]=CX&"x暊A;w97̙Y #1}M^OwvX7ߜ*lg6{?Ա楗E㯿׬-hb/D@x{4omb?XaZ;ϓa(@6m^{e:wlڶmk[ۜ_ҜekT,sIoG ;-|lǖ;.4vrUsG-2 '*,JH@^vinwUeG^=r駛3fT\]veU3x޽~y'ٳIvXMhXE>.\hT{J͑VwMze5jQӛᄏѣɾ[vaFR3ojL/oQڵlo_3g4Qܰ%<$iNTAmܸ.'x"vi[O=L__b;W+uXFi>prƸM-j^7ް}S q{D˰x5Y[o{7ž?,:^_.nYQp!>ϒ^)|5B_7ߘʾ ;b\B& Y'dZӮVܲeK3Z֭[Z~~Uu=F黈O^+u ZUo2}6q3gN4=%رuuoApeǷzhzދpMruxUAn5d$k1QߤI]O*+Ip;|h;a@M?7Yϟ7c~GNm47dL3 hWVkݨ3aYۖf&?"ZZ->W6QSgרzfo45B:/*nv7Ɍ”˓غd AAOY1`D̨`VDQH$H9-3ߧ[S3ݓv{o+W:uo iiiqsSBT:fzܳg >8MK\dO? 8_{yҭ[d]v%35f0@fA*oaÆI+V=z@zHlSTOx&ssEKqȅXzܱ>{63k ^~+ Q `NKwACSO=5kgc!0n$O۶mp>츩 GP2‰/Vm2p`cdžpܺFA y "Fzr bᄈ݀ MYpgfqffz  '3BTDZxeJHifv*y";~'ǎҌIT;9/>HׯPTM_-͉x:"<)@{+c6mڤvC˱;?'|N#!߿]sMBsl{ﭠ^wD+,42>fo!@{V ǍG_|a}˧ >7 5q"4h XDIˇ~ڪU+z$XZЊ`s lh탦'h֢n .]8&0tX|B,@cU?UТFk XR { l~bf!мCٺ'x^G1qD1b{D H4٠DTՋ6 eq"j׮D$v;=><=^N?qfkшXf<{DWiӦtxe  xbݻwP쇠c%J6׎EOE/dϞ=j SgzpsBTȈ_A@E@PL|یz_P=3)w4*V:t:2 ެ<[,wePV5*NDˤxƢٖs{i/i㨏:*_XXD@7 S2w \I#9X&ŋKH/MeejӨ.\wvpãfQHU  'IW_}U!ɽ[z@$F֢M+k>];,宱˖]j'Ojԋ-bB;l Խo:f9_j BLuB*u5/9LE0X04yAG>LXD۰0Ҹqczꩧ6̕C9^DeaV^SI~px@48 A5?#4nK߳>d yH7۹馛qN&}fXTmӬ0I$Mj4͸NԱ-/f1̶s-QӬ׭om/^jlP`bwޡ1hޘMW]Rzլa A>{4+Rx?L:5hL:C 2rx獧 <ٚ U&#hMM$lF&lfFP2Q+2 $ƱxC>`p|[b#}ʔ)Aұf!0YXID;m}h/X1P/D5#+WA@A <BTSL^RMd\79TfMTo}7N2 2 Se3ʝώg"׉SNzqܟiŇevav3мgnӣúqw:]_uHhoTr\G5:iCPxA@E~,$>v --<_ސ\s/TWeyمBfkBKC!Z;Nc:6 xwt-k7D20B`\vevH!+Nh,c玚:j haQRL\C |i¹y+,ٸ4 J4wJv2ZЊgG?$@ۨcǎNAH$C/xOzթ/Nvq~EzT0}nG{n┆/^j}s0-8Օ~pj07D5q"Su:Q]fMH;t4~8r/DS~=Ia5cXd%c9Ƿ0ngJ`ac uy^ c\x;9Z08CY\@CDA@@$LSf"~ v?9nPJ\y 릩pmoE,җ$jsٌITܱds3N`FS(f`1>.ꆨNxŝq~E"tMY,-Ni9fhnBl4rN ,MnИ{d^ND k$x8M|O4vء~j w4=\> c`@FP{.ո.SXI gk$2xh0Saր65ѣ0?{lq¸~!5) Aذ͌ ?Zűԡnٷi -x K>ݫf2[16}xMAIJYO)%LfO"bihe?T.{MTpAȕ\#@ x#K7 ϤO^\%#>r}zyPs\` lRŸqU|F֙sлkAcKP^% [msշ3C8 lnŀ1u&zz/V=wߥ &P%0:oN،d4+D\j}6lؠ@&[hsG,8_~} u,\6fƩ5P>/D5A@"#/:~Lp+S49cѯ|p9Q:{`:<~:7$Q@V!vۿfu.tBT#A@ s}0o(E^v]$ &Qqfʰ4FҺu,ᠵIs* %:cvˌ^5 ̚˃ '=n=IL駟bа.^3VQՂ:3q~0F,C۔db=ݧO/{g5d/V~rqM48x`gT|HD%dO^v!S=^^gNq\өn7a^g8y` ߓ\|asQ*nO͡Ʀ7|7 h֨FoΠFF СCÆ4t"H9~k՘0wv2KۉF۷Wy,0nc003 (f ghK[  p##w=My=޷am͇QNHk甿hID> }vօ{EB4*QeD*Zn&K/'D 8A|a'6xW$D)[Kߣ6ᇫYMkEM&M /!]A( PJ)f7XnN_~Tn]]'1򢹹}C`@oTE ,WSX;\;پh~?ifwmrSplaO?e'@Ϡv&($Aӭ[,/>ߣqmԯnT^܉gNmQ!O݄yyn&e☇vک 9|[&{uCTC LFa,lRD5.^F$[N:kZqgpDMސ­NTbYyȑϐD@ 5{2l&tf&?i^aÆ('²I&ԳgOżh~?i;T!0H^M'@Xdju$4jhezTd c:C!S=^zq'z9s7O݄yyn&T&8w+$K/QZB>L7p*?"6fV5`]:a ^[n5vwKTG;Kc0jĆ寱c GW{@'H!|W8pƌ$$WDm۶CqjcuӨF:t`*fITccd…jL( ao֎p/ghD톊CA@6vFB x |<4]{> hzqv=kӢ*i4U\v!^x5ռ>v ?ʕ;7ձΕ'&!m!.+oV)i!ZWvrQ$_z‹(--@D 1~N4a-^c!D5#!WA  N*Zvڪ1 Ͱ$\$gY[ '؅ۿqF1UATku ˗WpD5" {ZwȀ|P5%; `uѣ>oذaj~f G_YUO 1>n_:ũnRATyåq<-t)Y<9s@IDAT_5kluΝmnN;6m4Y\qT@K6Խ{KGs%u8Jd.la&Q䫩ዴzшj.Mc#4aGfM6-Zr/;-6 <:"ТSN̶U'aZZu|=IZ PS3$o&P?`+cƒL\N/W?i j}իWy]6TQvcb!~eE$*URgfh?yATb|!>Eow*jX^p0vfn~WY7oέƙNb퇬LT܃̝;V\P2NT#L7% seStKTsgl&1~S2eZ')y3)W]xg!09q5gZzu*.D5ЩS'5OyAon@k>}g0NcIiC<ʏV?҈ @NqiOpĒLM^>!į^Υsi[OA&+\v#Uh;6ӼMB]_?'?&QѐxA@0K Z?0Ƃ  N~?"O[+W~v,YrՇر|()%D5%WA DXLhC녭^p~e2X2a"\A qǒLr)D IsSmb=+^ R6eASuwv$n4a6XlVwϷ5=~@^DBLb3~?gTp`ITckIO\Ԧ @Qr=>H2ƙSfXf^^g~K{~[oDَ1oY;x`kՉj9alN&:.7jYm! wx~p[6wݦ&I={&AW>ZNtf3g4lo25hq(6ƙ\p=>'>ǩSrvuF#9q2Տ4"  @@8Q xṊ[/ߥ}LE:>T|NЮ^͇}Ƞ饩M|Ok>h[޹l-]Y'4poיw ?m#6MDiS;QZ/ܴ{"wsA\!l!@8VX@2k :&L`F?tXҘl`4y2Hmc„srBvTǎ?ӊl?;ƏoMUvS?rӊW/b]|K#g7Lej=#u@: 8fO˕+g/"JhW9 .HF'M”*lͅoC3SL`'o޽98 p׵`QKX(ZhPX`Go֎dH?ʕ++" ӣFr}d43u746:"mgNTUD3]^~f^^g~k{~w߭UC'*+ f|ohҤIZl|ҥs:Cr˗?ӯuUg "L'C4AÆCLD섀Q cƌN!m|#о}{*R$ †dnDx( !SP5겍΍zs_xYZ"   T7nL]tDÇon;*U[nv8A@A@A@9g}!%mQi4CA@A@ F 8U?wg>Cm۶4@JA@A@A@BTgȉM)T>mv!Y  Ǐ]ZZ?tdxrO   !Pzu޽:窲^nhR3 rA@A@A@8)fIA@A@A@@%L2J{zYiA@A@A@A )[.%KŋrR       @:SID3U:LA@A@A@A@A@A@A@#/NQM;H A@A@A@A@A@A@A@@4XD.e        p# DuR^mvJ,k qg5.r޶UVQn q ֭KЫ6)sھ M ^FA6yPӧ_VdE<[OeԷowFESZ7Q˖8q@&W^y%ύۗuRJ6W:tS2H, ~7ZF ]rGiiib ѣcxW:@cQ7/~iEgV2[yJ;2?N6l/~7 ݻw',zBn;u0lF0aBcN!aIH`N&+vNkmjgm$;Q1(EE{͛7:Q8#ЩSYzPڬiǎt 3i#f=T-jA wiV\u^Ph_am^K|Z/m'uc1-(|:uqLtB]tQf@pvnBO;4e%n@:Hʕmr9$D5,vFJ*ΑV(N;^OUW}*T(Z2k۫4cƌ$AA +^Fs)"#Y2h?6Yo#S,j_}MT_{;wnվ#Gо} Шgx+GK,5kTՌUV뮻Nvܼp5PbW0Β5{,)DNO&[Q]B:眀9X7YUPiD56;6v&v!Du< 8" D#,xc$_PrA;~jqǓTvV!m(r#D=#*<3)9p&Q SY~ -es-;jXkp0x>x% lkذ$إEvY旎i&Nf$6"bC@'A*5[.f. &#sxwJy^LK+G*pmKe80}Q,OL#!]+߯F~ФyT4ap~HE YD>~K[ʈYae?#y Qruԡ:Ku LPǒSj eh"DwQt-ZoQ!Ԩ.xZ*\;nDu>K ȩ5)tpzڷa%6N/>۹u+usMđCҹsAx+S۵`&QTTv:v/_AX:7бI,z`xD⏀ITOtݮh֖Lm?_C, Q6MǏm/0[j>댯vnn ]{8$D_BT~L6֬Yc;.rf#pV n9h|_<:c {7כn .lmYDӧo^qiӦz硼E^n27ȑ#MSNOUe=zkvۂǸ WTI=j{):t@%SC9^+˫v=\p<"dĈ͓X䣏>6PE8aHTlK/WN 1n`}\H`R=*=LL~!tܙ+FfRZjj}Bl8{~q~'^Auٸqc5^q̙3qΨ0F鯿R!4ߟk,AKs?X//1癗̶zW_}~zo4{lF\<ƙ;{rC>_ܶG~ԯ\De]fYL2E}xq^A]~~ǧxJT@|wjwwTݺu tӱcAZ.%KTH O8÷&Ʒ` i;~575-7D5xL"886:t.]ޘl/ 8d;N?}j![5^1#gÆ bNM:}ak,AT}7:k}>ϼ֧|b ѣ^r$6[u?/\x}XeGN/T?Z~ԆStGs;z!#6@@@N=Tj֬ [jLF$O۶mpp4fP/Aߵ O-!aq7W+ÁI#"o;s|x2QAbc$sR)Dz5w}g@PDtgf\q` !fN^[ -.6 ԻpPh䆖ؐxՑZ8*ITGʷhy!#Hcd{'X,Рa?Hb\@4:t5U a{k6֏Sf{oUώ_-z7~W^YBӦe}yKkrSg d"1IΝ !CH"VZ??,bMxW@1 )Wp.&HʂfL.C@܆x#\`Ik,F&`Q ,M ^pˊe˖1OL]@A8B2V\[x߱8x?Y {ƿj~` I;KGͩ{*54s@zꪫf Jɤ18}hw`CPBe*jOr$ X8Y@C1_|vxp)o{Gi ͇NvHg Ă'Zb1Mbي)5bywg?Wϗdy>|e?{>_8׫yNmڐ c7}~~ǧRg(4y S{$-oS|wd-O0tA -s4 ~ oA" g&=k>D#(wC<38N|w. dհ~׏b.W\;_/M zFE/dϞ=j wSgzpsBTȈ?Q'4+_-\t]*47L sdwm4Ҏ/b.niB IgxkTp5y].9qΙB{,ҥC%-M_SóF5e =clK۽}8%mDT~R[P.Y~2\E4a]+Zۇ22'l^Cjg,d\ǂM&0ʋ RmۖOں"5j~䴲r8p_R=yV^laGoؗ~ᗨ61BT_ Ց09ÁMmW6{>S~B L>S x/g}V E),hGm^q!X6ˆ__Ԅ^xN֯_?[SL'BEVu7'嶀ǂ $yy!q&) r&7!|0ퟗ:Aឰ/&aJz̘1ZX< &XmdV"HnF+#D5$)Ԙi]so矫^<'0˂y&{=b<kcAWEv&|(|:Q#,cun:8AhbZ4q{}w?<~/zq;co}~;q?Xnc'|>߹\W^԰a`x.6+ߛ*;>·CfD5~k7M+T$C`mIp dA|B0&" @kll>=x\'_lcnn$D5f͌M,ڵSG; pˢ̀o?܇V;% eqBT3rMBT'ՖGS9U-*UOƛ}߳T"PY~: ̬u3T:=i̅=.Wx:ӺLRtj#6gZa`Q$AO0_FYX\;flẔqx5Qp+0cǰ;k~%K믯`p^;gxAp}3IT뚵 (SWNm z^<s)1+>b|=|KXzg~/{}1^/}ꆨ&.|CCTX,`:Q +%dy:tP1$^jz°Hq|'+Q | : W,<5 u)V]zi@}9iD͛gna-#[gSp;@sQ!NA DqČ@?bÉۅP3B]sgֲ& 4+N׬ɏ'hrB?յzR߿Sqj7>n4ͽ,s΅F05y S劅<>6o|iР"-jk6h²06D!7WMMp^7L2C x߼fgWZ,(b8 CU8O?D|IpYo>U|59 /rD\<{,幔-yyR{2_,qc<<_biy~{!  O{>;>·xb,7D54>1E' uZ`ƆEz)|Q"l{ҤIlY` s\>j4 Q{yp=6`s/Id <6C0k@\iс–5P /D8l$SrY cXh+O@M96G~udUBO'\6;7NCkS0xg6/QeDZ5$S;C?릪2C#MBzIר2&x Z(nsLD$9~7D#'[F͙se7ImV[4ODa-S좞{ōH丝IINT4(./p]5tPժU32$¦{x0;+|`=9V/ 稐5W^m7j];GfQF0 k>.\&,]Vϫ&t3`sӡ͚gt-PNT1nBTYXܹ37?U4yY櫱k ұ4IigL^͇|g.s6qg+hITcZΔ0D~~/~ū>ϳX3<^ƙe2Fnc^Wy:~{!9/8Б6qݸ޼>O Nㆨ+`uRoa>F'uMk=}87LZ*$ Q :iRؿHM,J&HQmOՉ9^xø!ҢE x6Ʊ2N$_nag M0*Hg(h$ h|gLSMdޞk_o>a~MӚg疨^fiSﴴY <<6oh Q=i'G;rM;? tbf?Neak<džqV S6I$32ӬY&Qk{H$U*>38;]gnu>/Qݵ 3ZH8چF@فfr I4t,(^T XEI6ݻw'hBYNZ\bNeyxQ3[GQ cH@F&Xâ0@k?,n\oNk! BGN4\li*]j&y={222 8/ L6jP>m2߱qL2AE;‰ukpX&3/^;vPG͠- `?^9w|)rհ4pD5ABt!$Yq,Xy!- CTQ[hpD#(RΫ_Zmo'Qa\YDTy,0n|` |.53^vZh"G Qo1M doxݻ7A;[0[F@u C)ArE[O8cY?ک* ĜW7)>rHu&,9]yQ qT0h9qs2‘ɜI&ԳgO GT=zp^)o6UPAi:fyk>|/E*h )S ,}A Hv!yȎ:~aGt~aV;PÇWL4<_`rWboXe&aAZ^)hD~h so@?ϗdb,w9nٲEѡ)g}V#3k4K#G(/~6{4'] uV\܌< n=n1?.s;[IrG$~Y^4Sΰr駫l:Qu™YwKTG;Kϸ֭[M5abرAM) *ooXw/ƭaUMmp3 KiT:XGTiYmյj"l,\Pm^P~^45Јh  BT^$Jבj){ׯCE*VVzџg)ZʷNmۉftm~kjO^]'QjgPnf gnHT}K񽻨APy GTjݎN$Qx61(S{y:XIYG@c ^d 8 ~NvV ZNoCjFB@rH5Qd~D~qӿvƍԵk&B=]<ݼvx9^亾+™a0Q^B\uyu닓nL~|$˫#EٳgtF]wIQxgK 6L-daC-f8*UJF;[+PE&Ddݺu4 Q ltЎ޵kJWJ|P!Ā|| lA2GTX q*"<={ M|I{,>b|G&ӆ$}#Qq5v= N; q^wխZIi >OA`f;wބ]հ\ӴiS3D|(P@dkҥ2tƍgsvKT&a d.la&Q䫩ዴzшj.߾njkUnd/;)U$Q -jl^0yCa'Ōgh{FD37)L-Ϩ6o"~^WRNhcܹ)WAXپh+$*O 49;av6NӺd>@Ujxf!gTsiث{ׯ/;tSpDuụH޶Tk~qg?NM!5*JÆ_K͚!C2?-m NBTA y٧l>ZQ}= ,bb:$ \hE`%ة>9U'Ou/Gt@Xx8f% B\++Z8Lǂ{1`{0͛pf|$ Dl,C :k}Ao>Um gb>t^5rrgݺu 6wAR֝wi՘k/Xyd6=">}:t}s\`c Ƣc L6Q_^=zgqJ |:w&q\v8\ih`<ٍ3,yX?'[LwhLЁϋDT{g~C!nc6Gwg8eիW( Ώpx}.A|`\2Q6w[;wI eh*D5tNZ3>35LD5m8 $qTXLT' L(q> 0kC;֏$LaΜ9MRITm:uRCz;1_Y3 `%iϞ=-*Q21 j7R@sSLЫpٍT[8[i^Cռ)*ۨMH8sR樈W=0~QF4^nTمm_2Ƞr ױ{RZ`X#= ?v섵JQ E/Qm.Gjd;}R/d4 [o)3!093{?{}_AvCM&p.8\`]PD#YxQ3 i?U025 $$|\ԯo>?bߣd2xІ$GE?X`1щjNk.TPvJ3f̰8H}~arLk&l&xm!>C^Puz8WFJXAIT㙫/cb5x@q9ciKc~y>?V^{-s?;pX1Qe?z0Z^%ZΣx;ɩ ӿ?#H駟R"ET|+^Gs;NT6c-&Q06 77u ʎ[\7&Qm\INSj3cMWs_gΜ̍f{diT oذ!7G]1_Yp9~7NY5Q9ĩbg(7ZH#"Ċձ"?Ӹ1-IDAT C.#yCQAAmfr#^t UhՎ *D:EVվ *^^)ahܸ2~3~8.WxM/M͟f7w/|:sbuAV1cZQ)]7L0e~4i_ >|oo!苴 %&O?#$SQОc8Yh0#[\9{aiA8AA/q0 N]SFh$b0c4C/\ X/rD5^&aʛQum3zͳɫ UA\o]PphjcQ ;{m'Ed,b፵9aЊy߯yY'*?hQ9'hn 2. QsСTp.p+VˋIhB0]&L? 0 Gr;83{Uao>w>pw;&^|F;;C=/G܆ShU'NTV̸x>Q~~/\gc^PB\g~|";Wqo 2P3^,6"},(6ԏڀ%4I cln{me|h ?6p p&TpwxEUR&MÑZldҥhѢb9ulD˗?ӯ0 bzv E]{߀#GTi1iƃv76rH)׭_߉od|jfKpD56lu0:a8i^#-{fD1߳Qbg/D5PI4BT',^~rrU,3' :?p/ t2IT/y3o*ΤC[Q/+SNPm^c_)RRqhI*  , GD@CKR$#h`$94k'M)|*Iaի+3 p׮]v4Mj, b6P^WrpISNl ©>˰d6dTKc٩>xA@{:##Ͱ9<̿ :RNXƧ ,놵pfpkF_ /X|D4aѾc~΍Uhc#u8æs3Nt Q}r9UxBix.U];hqE'd9"a@ZuFHCtB³cgGhnzծocԨTv1;}Siʃ?;;b%ݦM:|sU[Is|apQt]˷^Z*R9;wׅe˖m۪ (~wo#BcQ\hdɒisjC=D4w\3fLNM"w%IRtd&XNl<޽{oQFт baÆԹsrf͚E#F  3:ŻckM;<.ѣGAQmڴQi/_N[no [X1ʛ7*uTIr^z!oܸ-[.YS"EsN駟#Qm"'C`!ٻ~qCHd96LK֮$; h̿~mdڶMA:b:ó[uו}fot.׷j?祊$#@\hk,&|ZQ]Nˢ+ γ󘎎+qьU0aBh$0QeVA'N4%J~S^kNC{0&Mش/ζ1Ɔ!lڱ%@ŋ j07*y*1JTWx.s'ѡ'6]LE;eTCh+$$# Du:Ϧ֭UxqeY^j++u]?;n^k(O<*岻Q{29:87>*_.yˬo]lѽiܟ!DY|{:v kVHHNG$vC?nۏ0Mfӥ~pɪD5:gOK+XǷ 0vcƌTDެHT2eʨ󧠥mq6i~ٰ{*_~*=LuNDW{+KK\l*K+9ne{A+{͉1~;d+Q x$N/edB t9ۂe9sxŤeB=0v,E!H@G@tp7FRU'\ZWBŷ h̿б~mt=rQȑ-3:p^lcQ'6bL nvŏn3Ϥtn*ƂeB\E+KK\l*)/?S"=<6#YdYѢd{Du:u,K^g)h`zǎɆu}9| Mc]Ɇq2_aTm7U4[6UjQނEhմwr3ot׌ 1u +WلOo]=ؽ6TtdWm:f2/>Oy.e(:m; QݦM:kWj.fig棽{[!CЬY{a t%lSPVqf̠oL۶SJN ኀ w_o *l44EFm JWB0/$},s ޟ_G˖%C TNQ*\8/]{!3}ֱ ~\SEt >} [p@sΖՋbַ,ZjnQ@=ʣse# ѣGiƍX_0%b}$Po  .6pK|{/(C>7L{ h;˕+ט9s:8C"CURqVd.R^ ŷ[13yy}mDX5c5nܘʗ/o jԥ:3ߑe\dࢷ3VW0p/l>tF5t+o|Ga Rtiշ+ҥdɒه8?A&h9Biaԯ__n߾݌n*U~Ap5M"8xFz?V9F4:ᆉ(0NƳ  TI31m"#~'~3q"[_WΨpT2g͓6ҢlJXvwY>~85 qʕ'/G2W4k;h mc zkvT?E3͜ݫ:d WNC:: O+UbmųdvTG2Wj~C@$ɟ%v2{KG >ҟ~EzDŽ՟}ZpUDOT;i<@b؁-!@́͞={}G2^]9ybcΫ5:uzr%yK}Qo.B/p;<'|^C~ ] Tn/Y]W ^\k"X"BHYВE jA*RJBi<9 "K*+I=|==ov^<0{ݯϿQDn%\0Ϟ}Y}nA;Yj~8!u\I(DRgP:|g^'gJC8 >930en*ξVؠ1@5 Tϓ7M'Y_|I˸?YgזjbE @um|,_0[T̏}xFU۳αaC7J 3hâTXP(-*9kn־.)Z.RՑE.u]O+[~>o^o7߼FZF]> "/P?7krL HZ\\[@uP A,[ װ|I^*% ޠ)i3nѢb @ܹE۶ؤ߯a1n&>jPZ<`ݥ"Gpƚ : B9r: -9`DI@hf=,&AajPP?ֲϷr QB[ޙ4sq[8Y.T:$PM;//O!(ǤirF "XA"u0%`M@zTA ,6lƒV=L}tn5z5k,p g4zmYϬ߯0X_1ϰ>sHy^Q#ͱsgϼF>q/hg(,PnEayu}jX wE7u{A+[0MB|;{#&{´$x iP@&g ,~P]XXy QߤCqFˌ}@5ڄJkb{½/kY*5(7 ?yf֍{֟lsc9:Cvvvk_k|ksKVz_{]TKwyE׉zMkmMNhmP kݟno .KwIm!âϛ%> A _8] q޹⫷#vmy[T﬐ޣW. "a^6v 5+LW´}ĮuK/ k99a\3գng:{b;?WmlƎxgbňQ9 Tx=:5fUVnwɍdDKMC~D˖ 7O15@v[3f|"&MT7oV S. = Ȳ3c?/FO=hUUvJcx!{2,;tY 6n%-6ʏt7QKt7.@uQQSF*-{d}[}uZh687{l%z Hp8zh=|p>J­m~TM pO'aj?~.Ԗ>58)cY.Wa_ɺ?w"\i"‹ ־ݻwWAn~≘g+DDN);~l! kE3:tH^¥$Z(״F> ?2 ,Paʢ%SO=u凹IymuVg x}C!zNuP ?};FE3 ]E(~ d @vL^,#!e5x(t෻{nysk+1x?+h#m ^ی,r17 GGuh?_ ?m?+@eF}lQ_:x!'>lM2^W7ʵk+k҄Fy c)6Pg%%u{jMT/XP$ رɏ ޼9ZgyF$,ի|cJ06 "X0bp~hev~W < \C ~Μ؞=S&lֹuS @׮4jqXD/)vta߿tƏ#NlH^)߰a0a*e! "Gu( EDII4hJ n*x%J7hhԩJ"}O5>[j N0 ?\!IÜ|ot]WtɾJ+PjJ*]9\3|A2<{k+M8QYVL7"@ _Gr\bΓtP(LE?=I@5֭[ϣݻtw|A84yEJP2߱ TO x\x_A  (k6OT,_{5-`EiSLyrY7r@5ӓƭVR1aO`|vav*un8 ?a~o}MoM| TӕxſP%%o8Xg7iAA3Ey. tYĻ,;~|/jM7whvon~&%*-Q8 cZa p 7uepeבdt%[\ᒺupV1 "mgH;ui=;iR[ LNe0@55QE%jN?Tc:|GΑL\հcb @5h 0㾅WG@˗/vcKXjf ccx' (1@uF[={7D z(@) ijM|s w-m8;o޼Xb?a۶mڃQa9`z6)8a%׎D8UQq qAan˝7]~X__`o#zwx{t\ͥ_:p37m: f6zsQo}7ݩ}T|0%抱ݟ%fAu}IQewmSEW)Q!][_yZ|*|.ȍ˸]gi?{†i6=6biyʆsO hsnѢXXWZYY-[n^|[ixõhu?nErsKo_'p#A_IKΘA]ke ˼ajmC`RVs4(7mRx_^ârM/Kիe/@mϺ}nKwHp;_'yĘ1q[d8"Q9.@/]i-7H+˚sYVt Hhu"@$I6| 絉@iu eNP+? T'sYL8u;ٿta߿tƏ#st%/MK Pk ^,j;@嚠y\ ׯ\0-_ pg3W&ݧ>W~w/vO>|v_;#y5@&h1p6NxV|XErgVuuT}K9|M\/ T%}$ukq/\I'+`MҬ0 1jL],Xڹs2/)@I.l{6A@5QApǿvZ x_A;wGݵT80DQ[XH(8"!S uPms>@*;ƌ@IDAT]|~PAzGP+vD,"""P@QP/(b^Ez% o3v.۽Vl3RfMU-[/_.{oeˑunůt$zΎ.:R{ӂ8_M> ջj?N[k8;^P*taU;>v]5|qJ>}8r ՘<_%{7Q[Vӓ.l$fgvNI||ӫطӧ|[7ܕln~ף X㧴c*W5~3;o)q̛v^,iJ+?Ŏ\vl7M6z:`Bu/}?:?s+WM[ҨQo߾tE*>(ٳǯ;C\pJ;%!4k@Æ5e>@z/.nxnq?ZPb'~>*G&\aMw_N+WKmγ=~Rn$ڿEƝc喿hݺe[zʖ-*vqjdOSOUӾ~JY3?N_hn=׎lߞL͚e\Ln h]v\Z"z >}N8m=CG]wM[^bg!Cn+Aֵ3gǎ#0n%TZz럴v2PU XdfTpʳ($׏j׮Z֭_%7?vXʞ=;;vҥ}#F%J"Sޗ\ON06oެ'O,ԩ]ӯPhQ9rJ?~<=ZM ~7gZѣGj?B3f̠7xC5?\$3uo:da~|O+k֬^oJC7V᧞zXo` nH֏]Ι3'^zĉO>Qep)S>Pi_~%ɓO48|zݻN<\wurJ+7Jx:x`X":tڷoO~!)RDu]}nӦj06z`e).Q9ߴ>|8,YN>m}ٵ=*cƌ+iBZ/A7m><3>O/ϗ/T3ՐWOKH^;zfצk\xqꪫo:r;pBvMӦI9߭lժѬYw߭~_omo{Q,zq7Rl]`0f^+R}Ni9hт JyoP37;!@88[f$+wE9s6-?&zΎe }I x=qvOSŮnIUl-އ.ccETD绗ΦڹĎQm~t\^g~y+ԤR iRzfj^Zt쁟 jwؑb>}~-Ed })/D5ZI@jd} A"K<>D%~fν#k# Y.ӣB !ˍ;PD5xߢE)wOY/nյj ^[fmؒij}~ĿvX?O9M|ռ9 vP}GbYd ;رM,V[/ q=|֏ xp>=/|_:2#Q6xѡC5ͩ$-Qɴ|w4"c=H>ߙP;lpJzx2Q}vzT8v `1wIL6KqxmmZnWI=}tk/p>z(w}vӌ")ڍ9Y^3 -? `>6mJ]vUC D#"9$1}x%ME><_^{5R0]zRVz 7@IK乫mΫ)熨k35q cD5@F QҜ9ڂ]믾'Peu@DC6nH~yׯO*UBk[oKNaUeDŽ sNuKxf0%TIycs>LwmΏQtO~NX, ^Cy ("ϿU1e7 .f?F, We<iiw#Teu^Ѳ>g FT{iQ}մtP*{ο$b>@ XPj/!1~7C=/ԋ;g#,11.,#!^lO!W16mf҂-vŋѽεVmj"6 ܚ5}?|*[XξCoڴ+T*m̰4cd{饗>EWh@'nO$9iE VD% j,k[;W ~~zϲjXOe*^KqMݿ$<,kO!QO'QƗ:ϝFUAeo8}AD?ڋD}L+W"St7M||'Ԙ`逳@g^U_F&)鄵`mZln=g=)wc6xo#5U;r|ފt睾?,.V Py=X&2tS{?UĉS֮ITYٲhyӦÖyu1;ks=*]2Pk0>tOazF>Njmx(}YdҀnGj@墖kֹ{7Ȁ^b !7Fd6nPp9$c_VlٲBDeNњ_qI m䗓^^,z~|ǭ^tjT'ը˟E!1{Jǩ_$CB8:^hH||<]vQ'hI(Vؼܯ߁{](fuaGA?Qљ7O.n2Sy9uN$\ i5͞+^é@ԗ-Buߓ,l*բϦ姗9אyغ;q ഌBTc9#-燲2 MdIs3??m9|Zz*Dٔ]<6\aٴ)>\'mw)ީܸ7iyPD5;5?9jYN\|qe>i_ZM/gjfr85첩>$މ>xF5~4e~ „ :nр Tuvrveܧ~M'%/ax˨~b60O(1rNT~y|9/Ĝ#/^^_7xAsQytwS(w/U9`DuJƿ0ֹ}TOY;n4ǒ _۶,Gu7oNvNJO]eۃ:t#u8 gHsYڡ>W>VzvӂDu(We `XE52`:Df}]Ν Q$&}rDDw^_Fc~au_# lY2L?AFX0Nblj}y (E? _wu3C!NOHclb<3cD 1LgZ/֟cp1Mg2Ç)h䊗>ֱYw(\Dsu̴0}r^/#ˀgp.3M3|Z?c=Ϋ'F=uNTc/\,`$QjugxvO4 A?qKT"F΍]Sa'Q$~$Hzw̍`-hu֬Y>/+j|, + 5 }ͱGp(h"3!(^}hAC5;#qA(#5m\MWwJuQa' >Bj%.P '^QiI7R[R{R^9\Wk7*nk"e˞J7mٽvq=P涇x)O">ÉӲ!]*ܠA;6@gq^z]ׯP*taU{'N=9N:}2m-; ITw&ͩHs&tbɈtx i5NLrE3]Xֻ tLJ=S#0!#\0ROY:Gpqw7 Yrݝ;XKh{׫J#Rlqچ ,38 Q=ieTB1OG2z s3?r,# ZgjHg֭IT.dLq)U*S6,ΘI]Z;ܹsdd^r6o/ˍkE6lS .n ϕ]l`Ò֋ڜCdsC͚hذTD*)wIZpE6rhQ[8A2/`k. xw:8z}Y^~Rtnݚr&T_zY KX"B3͓.v5ITg~e_1ѣlwx2Q  %XSiXtPIڧO$~Ꮃ` a=Eȑ#UKСCU9Kx~[D 7:5gf^TM>| ^BaYMg0X砩^OEƦD&]1b=䓈(?oDoݧ)Xw*[# E4r6 4) PڄF<&cMTO{|qJ kz/^Y<,QsDTH  "n-R1*)y::wGzuM.>}KJ>z(Pb7YF뗫xkm"8m#ߵ5M$~8,rXWIGvGǭ$ _~3/pvK#4FR,:3AUriq"}-0AaJȢnh/!!2+7͚%)PQKh$: q:34)%⬍ %-:lul M񖷇3JGׯO=+L~Ry:Tݶgm xM6%X={7 %&wK4uӪc1Nc1]X,K~-]gٹ~E`M d ] V ^lEƍs0 C|f ,"d箩^G"թ+Jp…3^9z;X  PGp=g r;1c,E?[ _Ӌs# @ΨMȰ:}ppX0:U$'QO%)aBh׮U[/k33aL84ر/9jIJ DTgiȐA@~n;d  DTcc ܭYoQy犍ò!TGD@HZAݙa@ݻw+I4":2nٲJLqիX.gq4_ QA VQ+A@HkpZNhڴiW,$%$$?C'N Y6+e .wvJٲeɓ'3@c<%JPfͨlٲ'OE3ߦ姞zi4i$H Afҳ… MC"@O>Y3~xZxq-eI3j2A@A@6BTg7FI%A@A@ zM5jԠܹs9y$mݺON7|CqqqvZѣO޹TҺuN-+ ThѢ4rH5"GG@K|*RM[nzQ ;Õ[ʙ3'\za g=k߾=5i҄)߼y3}4o<{ZݻwZjxvp!C6D|nfxd  FQm[֮$Du^  dM*VH `(q>YY l dPUp AF3!Z2<=&&hi۶m XT#/RQV#Du`z-*W\L+?w~؅>sʗ/_$3gM1!~nf%   A@ HrprRiH\^h%   p!󫉁Yf)k8XX׫WON"^_g\2п5lnV RϞ= SNS6/BȦׯo[so4lذe7QeT=5xQi…Mu֥/BTa )PUZUXFvp\D5ŋ^{<]   H   h\veϪ%KK/۶mK%^_g\t]pD஻O c}nu]G;vT>|xF -=/rzg:D8.%KkEn7PI˖-#|.˹JT{Y3^ږ  !Vůnime]Ȏs@/e#%]YꚫdYQ L ѡh}鑂S%UҙSh_)W (fCJVWo-{JܫWQBƔXI;?C/E)Uґph*:iuWÛ;pݦTĚ۩cGhߊh)>u$"  6$YU\w$do6&oN&Li,\s ;vPD,l&~W¹WK.3f+0+ի \ٴi^xܸqsNHq'\Je2 [^<ɓG/+@kn8.y}*WL͛7W\>tP!E=ܣ,aLSSPZ5pƍ>qɕ^& {a~v^Mlj~ p Oɕ4(A7ʔ)=~8mٲE"NT:[n!`tu⹄bZxճ-[RRو{2vXڳgL VuaDTK- 4P F$iuFnzU# .  oZj6lٲgb:N>Ӫͦ#R_x$N z?U%ͩF?}Fʎ=1ex6pqZ1j%vTX(~Tc_gktZ3e;n"ђd_I*s>i@wyLJЋX=> D<>@8qZjp;wkV y"Nڵ@n8?K.]'ǝW}^ Voċ~r;8ݺu W8flx7}}2FnM"baLΜ9}sd>ЈW==] ڵkS~`ܾ}{z^-3 Qm/ܷ&lq / <3fQs7R=ב|n"A@A@2<b!],aQEފNJuUTܲfb{Y:t]2\6'ITdjЭڹDڲH}DuVW߉2~f['QmgEA@A@X¢ Ĕ ;Gpg"Buwðr\v-9rD糲urg‚&[Ws+HO2%uE]ځ1֘#0]>#\S\t7h[nUX2@dr/yGnPMÉq+*D:N&qV-,!=`86<@;u5jD? da ]6_3w}k\}XBB^S5h1]wj:{ζ}L@LcYpv1,yB.R8 S&H1&xhbŊS7 XW҇3?#6~L^S^PΓ&MBPW_}L@;8usmҤ u] u qnoW}1o1?#'<͛WYg˖͞jn$zU#ܴ  9QX"^Ov[Z1j0=Վ_:te˥GکpSΜ:I;A/bu/"{ncҼVׁj Jں$KfպôҲ>b+XX.n:tЏ.`܎?KI(F%Z4+ZOA]U/}Y eɲud[*N\Y:ײ꾑r>ѿS-?A@A@H X.PϧW_}`m2p_* p&6aY`̮?KgS/C HXALIg^4g. ,X`eϚ x  v]tbiܹ%q5Q>X7i bwp N Y °9rJET{Otݹ׫8~&l pIEo$D,BNiӦ"u`3 [;I^]֬Ycor@]7tjn q&i S dOhĉܴU_SUk `c] WǙ?Om*q7G@Hcfy3Qs7zEMO  BTGi=uxuSv)SJuU;D.vZb/ړL}$O>@ ?CwmD@zthQ(.y-x (xi6ΒLEkN9JퟣRMt9 [? |)ŭc ^Xxns}G/;   DJYvE'x ?C>?HXwꂳ- e_ B ÷z+LeZƅ,HG&),q"EA@A@XvA2S8SuN-\gí3\CmV&f_:[M2/27 w\tKpp}={v5ñx DuM6ͮxN'ѱcǨuvجpWl, aU-&0Yw8bMTڔw>.=:uUPp: K,Q^W*BشW7zeq[}`z<..N2 7pR?Szzff#}/Rz)^tbtb9Du4^_M/   ;DeMl9r^Cůt$zxۨ)!>ض}dXXa3-U+$?N\ݗƸ?l( .p]B54\F*|rH3lkq2!Cj &X^;-A}3܈T?ѿɺw2So/D5/mKwX_cC3{dp]cAL9wg:Vgo#9B/*{}Ԯ];TQRV2 Qm/&-Q̻_hxb,n eU~n:  :F_.B;|c 2:j';$7W.sy%u?i&ZQ ջj?N[k8?Z?u܄ᓀ  u]Gp INN&[,ڵԣGNjXS 裏Ҟ={`qVL.Rr[ .H^_< r8L06oX|&Mkf'W=yP*Y qږ<9s駟ݘh1Ody^S+DO?6Y %|7)ݫyql>,7qgZqԭq/‚6 =W\A G<8]|6>WcqFܷ|lqi,H4hy=mEcUǔD=q%mWTM 8q:~ln;dBI0rp} 7 tl bdݝctdz"+WL8̭>.ΛNWazmi=^箻{^s[։b @TG/&Nz^_z饶hxFg^&\A@A@-I: >W*դí.y3*@Tx"Ѝv.O΄bd;-x9-Q]ŽT\N'ӜgTybQmC&A@A@2 {ʔ)AX6/Wc=ͩkf'P`&;N `n#iMayC6m ~r-[P.)߭Cvs+HUY FcO5\7ׯ__UQ~ɜQ'q}ƂI67.1>ݵyu6l*UJMũG_5wcG:tPmi=/ޘ1cS9OXn 488S:[lϤw @TG/&5\C?=Ӆ -Duhf,^su X;F[\A@A@2BTGy:.z(FՇwlV׼EKQydz9&D5W܉{[)Wx<.!}>/ śNnKhA<[~a1rhzZ*}H:D,^Ӿ) Q##aA@A@̄;Q:|6j i +e?Ϡ3gvԻwo™ӐzVB>L" ⬗_+$_ bQtK2!rـs&@IDAT>zŒI΋[r;8ۖϴ4F:N&#WX,M4Qu[aֱ\F%ZLt1z S#j}6 &py=c !ظN=bBqƍU/YǴـ[=#6Pu]zQF AT1]_|>"3zmO 0n>7Ռ\A@A@87:xPwʭӲا#MѴOTnF7 7)*{-bI]f#ݩ߻Zn@ Q#aA@A@&Ų& A0BߩSli .-%D??Y?gYŪ`}K 9e1.}]ʛ7/7F{wA;wF0jFlHɄ` Ҁ Ȳ|԰gÇ稺jՊߍlذ>ST?M1VZzºnN"gCSX>@yHן\δu@?3< u r!5t4ZjK/8MމNTn9 R;_|А!Ch̙a˅+`/ 7@<=,|ȑ繄h&HLkO2H@~E|nr=!   pn Du1 |Zݽx~%4=W$U}9T[OG] c߷m*T$ -PTNcҭlJ\~.۾Nv_mg $B _J]xiٱ}7Wh״qN7[Dji:"ޕ hОvA@A@@$%K(p޽>YL ;~r KKfkmn p qqnVp$;$#1H]1uĔV~. Z7'ӧO}@%<r~SV<օ%>~Fܷ8u:vHvҳ# ~\@u*R8za*U| F1(@z"/pu+PɁK;{L2~ggy!q=O"g74{lGDr^uUjӐϤ#F)}.aDϼW&-h  @G@ FJ^@yK3O=t(sJ)r=gc{wЩ4K|J(gxJ޼NX  d.@z-[J,I]UttEŖ.]hM3Q˗') VQb5%Jԡ /@8LK,YiV8i96%o6m$%$$?C'N<VUp.!k4}^;J" i`Y={whrK^&hSzGkHOS=,  U2d/^SnL| ڵkGn2/歅Xڴ74  @:  [[7Z~3+i4Xs3= 9$2 A@A%9˗/li3g [. \p;n[Ӥ+kQnoT\8ڴ#W_ H\'q̙S-i۶m* jeV&3+긽Kq~>MOOA@ABT{Ui.r^Ўg@$]7Sex0j0g7.H,3G>@KDA@8x .|! UpRR\RgԨQuV;^" GT;q1lG) hYՠ iذU`ԩ4|xX}^=D`f\W}[?Mצb'  !CsFB Rdëj0!Tbg,uA@DVMobٲeԧOΨ"L_3' Tga .P{av^=hY0GdV3ymZ %%  VQ%d+__X%`k6#Vӭ]oZKǭ mJEj6SǎоޙSEb(Vc[$ŝWOGQ₿}np(Ҋ׿+ִ5wgǏAͱ.]꠯)w"-gƋ{Lc7#i=oaY Xwܪ+ߺuk*T͙3GYB|''Oj׮x+o۶r1аĉj Og#^񄎢*U(s;ڿO3N>}:mܸљӹAApÇU?x7ztR1cU\Y^ZɓsōF Ps;TL<+Sڷo\ϝ;FN1]?z^d3X$k&Nofbƅ2{@yxxh;}xE8@/!RHgh솃P +hg"5H @m~ S,ki=gnZ=A\uKO7 7Ƒ[h黽EC@7|1EΜ:Ir׆Sǒ>kzq%ex$zfrxDzqrA@"^j~aEd$$$A_$lN,TW>AG[kڹb!b':RGB iɲ^!- A_/Ok@(:c m]\-bV;(:QlLM''Q,m9m8+=K_r]?X`ɰ>~$zfraL^u"P_DA@ՌS?eg^ZUV\D]5jD? >}mۦMOfZ*RmE]rD J>zޢr)beS,A򆒌V-[eX˗Ok֬qGA}3gDԑ#G.X ȡCm08~rlE$yG}٣LTsa,]tJܮe]F>,1f&yȆB"3ԇuץ|džu:,!ju@Z CYypª*0) q,\PmJ>wRRgcc 8ASN\5u7]<1G/Ry]wS7чPOՅQj   u:Jk]r̓PJXYZT|Zzҹu#y;[2c;tDɇiezdF˕ta傼lDA5w(h$nKZ ȞԗwI6ҾS@TZ],ٕ+ 7xKqx%H#^ Z<ߧu%(⼐;J҉8-x?r/kA`:}2m-jm0^t& r<=dh$KۦOڸg=a=U}u}~yj}:,w_N^NE,,Ǽ)Vב}dw  U0%ϟO [{'$(KU^^z1~WRW&gϞgiӆ`5=i$n¾>S Ψ/<,a%'x~x0>ȫ14)ι: QDF₀  d Nu z [BTOR/ը~w6vZ!9 %%}e9]|s49o}rݚtdw.R-JZ>Zޢ 5TO䓏TlZOoM!z<ϧ}s~S?G43n(ouNj%4j<]s{Ks˟mR7j' ^CyY%= U;D,eŨ><ڛ[穳,ړL}tzm>2Yvs  0!YLh&qW_~Dži A鴈&T1>pv ={*ΊҭQWVȰ:0须YٲeyU9 ypщ~|hA.馛}@&Olz a":E}~"!nju@Ǽ^￐v閨d~<^oMu7Yhy<'/D3u򦟛&O/_yC=~5!aA@A@G$A ;gzΎlN,y6ZA9jzE@=i>WkF(s]\MĂ5Qox-vn!}& :oEJT{]?/ ǘUO' D5TY\Gq@tZsC OI|lkdͱ[dvMGT{㕸  0!M%K*rl8O?~gNz%4E#޽{yHpQ*QQ_3Q }ڈK0<裴gfTT3q̤Νʔ)&<QfzU]O=q* nqv qL>Ʉ g>զ뀾x-L?XՑ̏fl4^d^0>6R/<#W}ѱ64_$x:e6$,  Du(߇6-?_zetK9-M2QTf*d-ɪ/ߡ=9DOr,b$rvGj7sg؞Na R/I@s鸜8|tǧX[R{:iKcvGF'.nJmf8^tmudD?:K]X&pp!  XnPi\\Y}=bgiFUISn WHTTHEL/;v zO krkX8qvΤG$DwvGZjt& Y n mjӼѯnju@?d~%wGz& :O`|uK9MKH4Y?^EI"9'^Gv"#qA@A@$QKK9R7>`'o1Em' K;pc8$-և.,:9{e:.fen{.>%⢷ëuLk/XzR\Bvܾ][x@gSB:vܾ[uq([\]MKuY:K$.p%.G u|ʩ g^yZ`_Yզx-22QS&ݟ\>ҫu7Yhy<'y@LԩC/JzՑi~jgxn¡>7M 9y^; kFUT 9c  @L: `vKEBU|'\>g9Qc4繻Q T=Ҫaָv]+#}NttU`Ts?bWJU[ZDor(ٴ XDXc]""Xd:[}(X*XgF^m-Ud{R::qt}"0yN]BԈjtsqyjPn)9s{^XƲktZ3e+p)*6!ƣ2q'~ r-[P.@…Yvզx-22QX˺C1rs5yN]>~8{>]$#d|@"^Y7P&O|'Q.eŨoԨk=.o--P'ޱY3 wW/zfz3:BTA@A=M4Q$&je˖Ν;駟~rm _7 ,7}7E%ilcKU օ֭= 4`i8/:>>^e;p{A3̙Cx]6ݛrL9zHf#%A"tƏ݌{M`@qna2` xԯnj7Yed:.$=jͺsY LA_,a9 .Z@^zOC,܆}:^uTKX" @V(L/.ӬY2*D,HHHHHHHHHHHHHH gL.̔Ѱ$@$@$@$@$@$@$@$@$@$@$@$@$@$@ P^L:Y& bK륓@N݆/5\Vz?8 dL.']e$Y768NvhzE"elwƌφJMZI۟2gc @ׯ/+VHڵkeĈ dڵ~6vXy gS_^FF)SNͦ- 4?\5k&*U2eO%T_2XH_6[R4%"E9Ή|͗-KQ.1ȩ@zrЂţGʼU)>f~sq|ܠP.ϓ $"0h g}bŊA;vŋe;y7%''Gf͚%:ǃ'0`_8̜9SnptZ[nmʛ={su֕C_>|]΅ea>Zv̙#7t;/{.%R`QOoB<x *sgO=i|2:.Pԟcb'8r|N;_*\qhy"ŝʊ[n|yҢn˖C-sP֩HHHhժ8:QZhavg#fZnڴ56Lx mBRےLZ\~,YpE5uySG:=v/5Rz~ޯ}%O")}ے[4}Ϟ {=et\ǡ?1u|~mKK(TR$s*qAٍ{-UsǙNE>A6ΜsHHHbEjj)c&M^pBu,z%'NlA{1 KJ޽{`O^gO=>N~R.{WT|}܋CIsg8~.sOq(ϱTfC*켾캓Old4q)T#4W[$aA,T $In[ϳ8#ojoEq]\v2}fPN3jJ=r\ɩR]_+]%ɺ DK^8%_/owٹu6^VIT>bRcwѼԷq\Y˄db[~ԹA6j&zYypcR=Tl]J6/_,+|&ۖ-YfCf㦅s$FmiadH 1H֝ds%_+V,6uV8ҮnAk'}/R[jOZ*fNu "V50iF*֪gڶalY=pRۡS}/_>RվAE_0y5וd}X=*-:HΆ3dvT}q@f cm2HHH {\z?4n3f$ݸ?^` \K.?0+~qdX{ٲeF 6"|guz gu2e|%O<ˏ&ڦ2_\WbdrOtIf>uOƍQ8o>W:]TK1z$RzuټyYx䓂m{~ޯo>͟m:>J\wǵz|w ,h_|-[|5ytk58h>L~nj]ug~1~.dtȃ4}l3ϻ~n's8X24>=Dö([3-Zd~{T2%k;Sϸ++ :_^ncx7f^^^4kYS-J/ *U#q~lPb0RQ"amt򨳂c{gE2eGD DMc'#kuCv&K>|9^ҤRzL>,_Q]WqK~3&o;wHrNܹufeھM:=uzg^=wˑψ*'Ս9WZ9eңQx_5\7ާ_%M"c_o峋ue?y #  (^=zFkNp̝;]vʼK_~#IGǎ_<)}X+ z表S.CFeUHBeNNի墋.CyMr@z5D߸t׸X[r\uUB1^{1B'b {WˡZ q@9:>:^;k}^Ҹ馛O b äp#_~y8:T]LC 'f܇Ac1|'O2_|\_ RwyOW:K5z3MuSzG=>|gc@ \ve^DTC(t^qкօKzo4}W˷x[o ~}?e{O;S[qHt"yR/ϐ@az1.[u? lX4G&ݿ|X %:+DZvl1ߐ 5jdKOX .iϵK(,_$ꠐBvţXEsX `\!dâ K.fⰢG@[!^(`%j{X $Ad@IDATzP3( ';'xRV90> LâBĉK z?ͯi&3{v^bs٦23?]+}B#( ϗOy%s2_-DO/ ,wH:u Uy@1Se8qO>/ݷ>r目ߡ/ .i\]?Sw׶izqzesSgz_r\2~>}:?1_9x>*_>ߐ*Rn󨧰8LQO2HWC}>ۿq1 pN>Le~>sA]y '@pFI-o(#ְ[ϋe\.7{lߴPЫ(M EaڒqEki}6\7>@FţGʼW'u#f,ُ˘Δr}CWkUl+۶PӿP=×nFĥo]=Y* :we;_-KF)@8ʁuמE[b}гA^X/+eT!R'>^|G(ʷ wV`ew̓5c>bzdk@Aux>2c ՛"WMXE{^!  JVpҤIƪ5+pݏ?(<!sq e qg 1kbt {Nri:wl,u7Qo#x≉z ’Va=mnذBu,2>:o!'ziķFoǚx#u}!WUPk>_?:r/uE: \'njcG{'|yFU9q,~|r2u:]{]+Mx)SYND_+(d? q|[qz+3;{T.37NݺOͧ[[_kyn]A|rHv|WÙgiO裏gxCbzT:?_Mv}/whMLBub>I-Wm9i.BuK}Yk*-7=Yeyd}Y/?kuҰgܜ%K#U)=%JFe?|HE,E,TŚG}]x6JPUq.'WH֌D/,Tϵ{!w)kxZ9wg%}*s_yPw^J5Lˍiw]zԋ4$JTn{ Y\OO*~ǞqO~&C-sljvVx۟}&וn/kP=H d%Tv#aW_XֺXSJkYZo7ߜ=kUH4UC ;eBMQG%aȑ`l `!;Q{Qo.WyP;Ce^kR&+T?m7=C:k}C*(h|%_*<ב|Q.[,жm[ a_*lnp|j^ۿ/{\c?,?s؎}zͰ4G ~[~QVϧ{:>㐎o >sɗ Ou:_/Cva& K1s >Kg=:<8LĹ{:}Si; (A-Ho ׸Gvڙd'O6^<ޟ2w /P8)ThDT͇:ZE,wF,xcKTݮ.,-X^lzϓe7|}kWʏE[<ԍnnyS_|X6FrXZ#^x-;;.v͝&yCv4&?m^Bv'}?i{ʥn+`Q=3" jW::xY|owm&?v# ̝}a/u"Ȏjv_FJc1A8:._* թOۧ:Z^a[qt>s@?WAA?|^GEn><,#Ew}g*ӧ A1feqtJ;S|oX})pe?dmoLd^q2Z^wghޗ\?W|e|A=Y۩[pBۡM}.7}q@ys8 ?P`D!'2;ޟ2 vOoOf}/(w /C(TGC6Kd{0_,o?' պ&M[MK{-5^]{KnCgeŗol="vץjRa}eD$+ wv}rv}53&ȴZ@q:\C6e#.L ]ۃmQ>y;qHHH`"?I.4ehA@;4o_,V*^ZU}8kASy 7B| wPXL&o%֝Z?醐H-M 8PzaN <З/.>Ń q@t,\ua=PJ-ɌMu}!ӵz'POKTx?bq5/[z2 ^|_*<=uG̓o>L{:}Si >'|qҘVXb%!Ae|.ST׏X B96T2ueP2u[SɎO{ᤓN ^e;ztu~ ̸_P~ b(HD Y|1 .=/hFuCvA@X<~biy 7l pTK:.ϸƸ/Sl ׾*p1dG3-zk[LNŴ տoLyi-ωp;0"_M:8&_ {OO"gNJ *U67rzԚmcΑηB7l|"kXp3zl}w^sHHH`$OJ&M q裏?px׌[.Hk>}, j?Âp 5Y):d,r|}y Klۮ8=GiaK}lY֥dTڡcΥ&+T?_hKӧuwqHt}uWre ^뮻LSL|%_*<}|k^ڵk`:cƌh3d<8w-H Lqxw/T\=A׶\充3[:ud;<]˸\Sy|> e|A>ߓuX'0C{^+K,Ѣ ly/Px!.〢v?o롰噺w.ziNa}>Qp2NdR/=m6hvARC:M꼛dSEXv=yA{i ʴհ7N2izpE;K~. (݈߶~xvS noͫ䈈jԒ5wD,ZIӿ[MF#7[rzS~f~l"]yd FBÊقKOf/]}L9_ڟym*o8NB8^y| [i#{ߪ}>}EdTگc.~.Y:iz _gz>[qtO'PZm۶MN;-8Tx_| M1\ltթ 'u8}8hy mLsEiotɎ8gdw^8=cD!D;8 2u[C?D!׃}/=. 64M*; N&_2?MɌդ:_|6moݺUN?ิo7u84ki(4TQE;gX*7]6/_,4.aA׋q*^ޑ*մXeMBDôȚ1U"ziDۯ'WMAf=ޖYGzr`-4G[Fvm1dEcm(=e1.E/ D7m\ЌcHYƲlںwcCVGܹz?AZos<47,#S]S@$@$@$}c{eܸqݺu31&M$^zر"egРAja+f:r$V>]fM=Ef ,4OY<dG1uՍ:MKmf7(t',{Kq<܂u5+$i5̱g} &i\!_:BB5ퟶk}*d]ӺC:d}47|3`_VHf\ bq`9 xгgOn5֬򎶭gkn}e-C?{8[e̸zit|ÒZǡSL ~Nmpw;˾8hE3|?W|[-{N';?d.,rJis;(3Oaq>׻o;0xLEc9m2v>}q:)My{T.غ8={ sCC:Zf[qPwۿž=D۰=*<_ ts/)T+[|Ѣ:6X\s͞ql]:?_G2/.Z-*>A?ހP{zuֻ/Ȋ/Ө`ksf/DYNy^ATw*5i)"n6hRM&=+"Vvp_F{KAϙv.WgP{.z'NB~m7JGS6F!QhditpR77JƘעΩ+λQj_ (\._1a,xkcW(usJޣכtu҃qv6-_$$.k@$@$@$]G(pF\zu)j5>ܶ\ӦMm[ĚÇ\.Xr !,B*M1x7o_~Y>ӨSǰjǃ1;rH:t{um+ Wn܏KN ƹQ3^,?~|TrØ؂&Xf|O[<<۷TR%&,Tm)V:ȧ}뻵/VX(i0 EQd=qHu~O.ܾ˭XOw|LuEӻn/"+V,/|Cds.k;K>xVA[5ZS]ϥs2}s3]},oϮqs>2v>}q}u3|>W|8=w~ǯ.ǺM}^r&;9L|ϷE}=`x &3|_w|'/]Lv~;zA'z5PNȗ߫Ԫ/9eȶظu2Y9W)Ζ9TnN*FmVG,ma! S_~`j--ȶ_&CܦR9yMU3>8yHHH~E`͛'3fhCK.J(gϞR-Cx 0J vٿyԨQsx kl E1޿TtOK+qg.d}ާOvڿ1^߲eyIfM{J;4_in$j DtxT'Tγ$@$@$@$@@٥$ @F <% T6(Ņ]kt-R]'  (b8>oL"    Hǃ r7gד;,˩*m۶5E[®$@$@$@$nMq(TqC 8}X "pAc P|?uMjmmk˗k$@$@$@$P$(T fV|?뺠SW~itp     6Lʗ//&M{OO$@$@$!B;dٲe2d㺝PIHHH P.j⬏HHHHHHHHHHHHHJ9b;v(|ԩWʇ'             C0Lf~*D,HHHHHHHHHHHHHH gL.̔Ѱ$@$@$@$@$@$@$@$@$@$@$@$@$@$@ P^L:Y& bK$@$@$@$@$@$@$@$@$@$@$@$@$@$P(Tzӓ.Je$Y768NN/)S&f 3$@$@$@$@$nח+V$,ڵkeĈ ӖrG~#GԩSKF#+V ٱc,^XF-N7|Srrrd֬Y2ps%u֦Kg.kRn]:4eUx/wI`4h@yw}W^}=Չsݺ_γ$@$@$@$@$@$=(Ta,(T"    jZ|Pʗ/'xbt QfAӦMiٰa7Htq6'I Pqd9$@$@$@$@$@$h?QB5ZU5}TmӢ:@    4njLׯ7w{oXXwՈERƀs=f\}b %Qи\Kdf7HHHHH P]{DQ v [v?,P @|r 7&O,wuW̒>lyWΥr8,33gN;묳)So-4}}:S5jԐ={̙3{| ..۷7 ~ GGÅj,W.7o5k?$B 98ecJ߾}e˖&?RJr9H&Md…r},f͚rGK۶mBu̸[vژy?|}A{m&-2 Ann,YO/{ K_.ON;47|#;tM6lp4iR֨vyqǙvڵ+X>6mژ~[VZ$qnSrHHHHH4 PX^1qtDTwU am|̹gήdK]:K3 ;ei )S.zx 2rHHHHT8p^+ 'ܹsUvy%H~ Go=}]モG}4f;ROاnjL_~!4.W_}z>|E{/hwV.`ƍc^8sW.HC*z;v0q1[o5v!CAݎĉe3јjT_.Q$y?{=<رc\.Iv@2W !M*SNrw 6ȹu> d3 iXBd!9ah?L Zǯ9S~H]s[ׯNUFKF"u(?XC;n=!;$@$@$@$@aâ,\!%/Qx=XJB` q QkbWf+s=g\V#MΝxHD8%M\ CDB7(tI3vY*3Ǝ׾7^ƼMj#F0qXjժFU 6%4\#@8jo3ǃZGBùv%"i/ 5c>Ci{ YZOO*~PTj^y/xU*E0/7ʦYj<$@$@$@$@\MC`,_|C\ꫯ/pN+:90,7lcصED=c3y `BD+^XCU駟6ׯ)Q7HXX&+xٖǰw}oXT4qA;ꨣ .0F&x1xᤓN N7_Pq1y &"vm թpѲӽ?ԡcr\b];lsS WG/Kqw؎ȏ޲R{BÞ7_*B>?8By:EZpY;Z8NpHHHHT^xqѫnmnp57T5jSi,~tnnԨQ#0yd ֠ewdm\Po*Gzݡb 6~'\=7`Ys9'f%{ja- d;)TLcy2K7_X^: Q-TyL{&;>^E_;Ƨ"Tό+-Zηn@sG,K>zMHHHH7޽{ zmԨQoTjܹ7Bm}{K^nᆨ5?WåmkN6ͬ|WGa:h ㏵ȔBX LVUq5a}[lF҈S*^c-[TR6Dn惫t!GD.SNmA6.Z֧Cz/\psȐ!Q㾥 kW׭z|ܒ @Pt ՋGyvm ҩ~%PԜJE^PMg!z(?WVA!   (QlK͛7YgOYfx݉%TyRi.bYj&*n!B0U8zWu1VC;qX/(4W!ɇK^{w`ebm_|1-½ Nc^?@ ㅇ‡~(X[o|iQ7,i̙c֛G\7_0'~g{Tu$?crㅃGtb9me?]p8zh'Ν;˯+.UH]w7}NN;cMվ stTKK&֗e{ !     l P"+T7xTm4BW^:>49ٻߩA򟞸E6Μ;a͔)_$iy 7ny @<O>4iD!T}r盪^{5#8UAlݺuAZb>}kSC~eʕRbEfulcۥ{gրNTEЋU 5zƌ%}sa=Gz>0aBu&Jۿx [S{r뭷&BW_-z~je]&}濄l Uu?c2mW7tNc}>MN㲟*֭[ Mߴi|Wu&MoO&u/ ./h=ܒ / վ|BuWHC JZ1q<;jԒ(?^s8.R fˬן-gjT 81Ňe/J}Mݕܸ;/m+aHHHH qnaÆ kCeyM!._r%Z~rW}[D9$`&S[o%X:"HX ^ʀkߜ*N?N7ТEn:o]]6lhnOG^e WvxWJ*&d3ŮXG#7N A' bqO1X`٢/h/gO|[֍tڴiuV9Ӄc @qPQǰ޸lVPrTC /맻6'ǽ uY'km2Lt,'vEҨ rͿ~,`?HHHHtP g1Ib֭kԨa- }+dA=vXcN:ɠA(k&j]X[$yu%l]q+֫}>[uhо'^u׭]~aGZIpK$@$@$@$@$(TadR[:]t{R%T#c o[ ˔+3_"130HHHHPZ,qʪ*\kN .C@E@};w jM811KXh"]V.55Bb6Y`ݯ_K|MڗˀɄsk>Sm&XVO?[훏P hkʕ1^װl2/>xBpFBub^V-E EvOzz]tQLyr'Nw|x^,x6l {n*;ogǾn$.GZIpK$@$@$@$@$(Tad׮'=5(i7ɼm "q 2[ɕv(5[w׃_׮F˂ը}OF*usg{P ߹=J]`]=;ec $@$@$@$@$+cd` oQT&4'[ hڴZ[!Z><:pa afVZۤ[=:p";΅j_.p{)hu 9D@LUu8@/oϱ0wȑ2tPkTXK[Hk֬>io|Ap(O1://O4F9ڠA3Gs_ 7~Wk+pd}scn|``3UW]enɭe/XIDATĘN;(Zݻ9g1nÕ${|8bLi%     @8'BnS<{l[Jvo9?<2kvʖe d[䏁HHHHxܬY3͕͛7˼ydƌ%f͚}a9e&lZ .@z?Vބo-Z~B+D/^زe-9i} ^,nW.),>V~1gΜ+I㊊gIHHHHH`'@z@]`Z      ׷ֵ_ K$      !@:{"-PqĬHHHHH֭\ve_ \k8c5s̑nIOqK$@$@$@$@$@$@%;;F Ɛ @Q{]vEe5ˋIHHHHHHbC_=HiuASW~itp      hժ|Rn])SLPӒ:       @BuieHHHHHH ԪUKׯo׭[UmccHHHHHHH(PݱcGASN+              e5k[&YB:Y& @(L/P]c @"@D ';C$@$@$@$@$@$@$@$@$@$@$@$@$@OBu[H$@$@$@$@$@$@$@$@$@$@$@$@$@%5 d? ?Fl! (Kp3$@$@$@$@$@$@$@$@$@$@$@$@$@$MСtYN*yyyO-$             H ; )Sȴi YYfMCDV[n2{l?~|Jd$@$@$@$@$@$@$@$@$@$@$@$@$@$@{8@Zh!ƍyhrƄ J޽eŊ2jԨ3HHHHHHHHHHHHHHd۷Ԯ]hЌ!cB5*0`o2lذp<&             (رCy睘=̨P?Qԩc֨Z $@$@$@$@$@$@$@$@$@$@$@$@$@$@%A$M6˘1cbv6B5*G#)1[H              =֥Իv풷z+n3*TV=uVeq$@$@$@$@$@$@$@$@$@$@$@$@$@$@{&]J6mL$ h1#UT1 Zr]VonHHHHHHHHHHHHHH`$P|y^4h@ʕ+g:1gvHjwҰaÄI              =Md֬Y2sB;PdBI&ƺB - H7o,7n4B 6HHHHHHHHHHHHHHJ& %s\+             Zvh0             ((TqeHHHHHHHHHHHHHH k PڡaHHHHHHHHHHHHHHdP]2Ǖ"             %@:k #             IBuWHHHHHHHHHHHHH6HHHHHHHHHHHHHJ& %s\+             Zvh0             ((TqeHHHHHHHHHHHHHH k PڡaHHHHHHHHHHHHHHdP]2Ǖ"             %@:k #             IBuWHHHHHHHHHHHHH6HHHHHHHHHHHHHJ& %s\W}1m9s,[,kņ R-TwE4i"UTe˚Qصk_^d72%L>]&O\Bzn B 5keePNS @i"P*c9XQco. ,0naYk~W i(T"             F ͚5< ֭[e1cǎw̓LQ1! N>crfKYr`׫WOZh!jՒrɦMdŊ5bwŊZ-[4y~=zq;޹sgQY믿ԩ#Z2eWTIviXj̜9SEmNyQF|ƍ&ҥKm۶ JeÆ h"3g&T:ZTPAv!pg϶> @ !PjNm&0Һuybi}'1;˗/ڼy|72~1=^Kis͚5qna1}Jڵɍ_~}!pߎ/_._}}             @T M4> ے%KoIz;t F.ַ@ K`+qqnFiXc#oNNțoi_~ZV԰lU|ƹ~?NAxwjbӧYa m\ʔ)hBuƍW^xk׮58quI`5=k,SgϞҼysuyakpwꩧAᆱFݻw V:H6mj2w /vZHHHHHHHHHHHHHd(UB(͚53#ύu-cGi2?Wk>CEPawfjĝr)RlY6mL2%]‚  BaÂxԭ[W8mA.]Z؟:ua7KP?Xy#ZC$@$@$@$@$@$@$@$@$@$@$@$@$P P .]+ݰnQ?O?$3gL*m(N8'۲X9sȏ?h(=|3f #Q2LakРjn93BXp:5*pϛ7OƍgNbaÆf?ܦMڵ9Q'^t;HG$@$@$@$@$@$@$@$@$@$@$@$@$;QK&LNtpxXckM#6_'lKO>D֭[W Gm\U*ui-^tIU4Ш5k XW\@Z'f_jV&lgxc kP8߱|/\-捵v:8             X%s2.TÂC14!_^mVXbOŢz=zr!~w1GBlO(WN/,aqծP}lbdĵj Yr|f]W?<|mXlk׮-pKF(iݺI;j(#3j?w)o9⯫G d2Id,dO$=;Ifd>{99gʕ1{\'GP>C ;/{o$ۡC:sO/ @ @ @X m묳bѱ}ى靇]?gqF}YuK90׵TP؞у ??[}ĉqgfk,.CTCŦM/+ @ @ @ ЊU~X|y=m֠xGi<<=~̩+fN}رcGw}Y93|2eJvZtP]DyZ;-]~\r%ٴTWtH.իW?U]vYt5;?u#lyuT?ŵN=0`@ݻ7nz =_N3^׭4s<}ӊ+Z{)$?:0dȐ޽{6:oڴp]#@ @ @ @ 4 [ @ @ @ښ @ @ @ @ > @ @ @ښ @ @ @ @ > @ @ @ښ @ @ @ @ :.\555mQ  @ @ @ @z;v%G,m  @ @ @ @4 X) @ @ @ڄM N @ @ @ @rՕ3o~Wt+k{2 @ @ @! nUlSxot=7׽US @ @ @TGB @ @ @ TP~/oIAuKj{ @ @ @C mJ +l@} @ @ @@Ƞ߅eeѩgUvQ޺)>_K^7>vXbφ5y5b _Y|Z8 ];bˬKu5Qg`޶)v_fw߿j]^Փ3c%}#~$.!G{_ށ.FDՄqRӢ)]NQVMU/ĞRS>?zw-'(ڗ:}&1z{w?/?D˕  @ @ @8 S(:o9=_бݿ{gn'=G.s:^Y"y*(۲Xc) K?X;\?#vUdձ3b_)MѺ}y?~ QuߵO,ŵNUc.7,N}VtֽKT\v:??S.,us @ @ @ZF.b3Źpk)$͏Y3}`;?F7QbۼG--QU XwyX( å`w y m.n\[ i.L&|+s訆թc_du: µQ7|'Ų C @ @ @@ T|PwE K?ŰW=xޯG~7?+`}ߎ?^q^E(>|c;ܓѾD)%ʫΘAuҲS?wg{֕--o> o=O-yX ꒖g6䙎T4<^rsvh׾}isc%ڤ?iϼ/;o(QZ|ӂbKCn}+;VAkKS=N @ @ @-*PAczsTM(^ v[~?A'JKO絥{\e}̙쾞K5 W?X,~6oyhnD~Ŗe#cഋ&JK+- =zNԹ Ϩn˚ϬT7Uǎ5E_ -zUemY:7'~fs?fg}i t㷢[iXxbN s% @ @ @Z^|syȜ+b:D߉/F?׭Qϙ^Ϲ]8?Ҟ8cyW^y4/#qYUQϨ>9nJ g@t=4#oazȿ~POq=`yOfλE@#ct7(|_sAuA@ @ @ @ek%Oo_gEscK{EU3:?rLۖ/[GK3kn9_U>ϼ7Y[b皕MpYo}gu:Zd}J3ɗwEr-|.Zyc}'ʯ 8!@ @ @ ТŢ|mVnҟzѩ)@j,c‡>?Kg/h .}m fŒCKˏS&M:=_K=9,o}ɳCif];=]yu.-z:g;oX8*.+q-U @ @ @hyAuɼ~P}ۯob4S哟2~,ейgOѥgR쏿N^uMQW?lQϙ^ܿ>}_:BƊݖ x.+Ă[>_;-]>.FxP]Dyܜ?g9~jLƬ9֠~@|1{* @ @ @T {Nx}}Ȥȥu:nv ҒѾCt={ߚSxoq-Uؿ/x%:c]ƖYz⌯F#yyk~'1Qc  ;v>WP%@ @ @ yCAuiftҭ5_j.*CęaT?9nJ ټhn,YՉQ}zmFsyU>֠:='|+?2hߩsQ/.( @ @ @@է? 2cxMYݎub^uMVW薏__Zpzqiq~< ]1e YSxyu^ٹߠԧ/3.G9&ϴhߩS/ڵKY3}[6FǪ1kJW||͋?Y\KF-ҬOGd[7Ɯ*( @ @ @ rTOյ[ǜ=?N6:{mݛocGܱ{Y ֍Ӓ;'KZ_ݡG8yS|_f @ @ @ $AuLN^ 8W @ @ @ @T1:h| Yc1W7|ݬew}jD @ @ @JTk>}O+yXǶ8S1swTߔuewڔ @ @ @ @e939ӳM/Ųw>Q֪nqUYg[m0n @ @ @ @ 8 @ @ @ڦ #|…QSS6 @ @ @XСCSpCc y  @ @ @AbAuT]&@ @ @ @@kT7 @ @ @T' @ @ @ @5 [ @ @ @*P@P] @ @ @ Кխyt @ @ @(p‚1cĄ b…QSSS> @ @ @V/X -ZtPvC=pPq9rd{tҘ;wqxG @ @ @ @@[2eJ >d/1Ԅ>S՟*pRT|3Ylj)53{l+";?TRk_3U\IµNd?ŁP_Q>yxLT|5Wz6.2m)eYG6 &Hߒ 1=(PuhslUT_>w~(fJzz0<}~j pTj_.wɄWsjC2SY}mɨy |3b>sE Bzp yU~!Hk p䜝3<%5:B jΛ /:|6dSvvEb{5a+0/ůL wGW#d]QNѫy+hː'EZͩ;0Xyp Ag\G^Or$CMO(QM6!Έ;ҽVܩ_M)M|=9s5R:Ef+p3%¥t$tce"8.zKH"!=fXAџj5M4x#j8Ztq$޷}ϛM~v24$WbF.bowslp4Ө)):5{H_D>&@}nr|2SK3vc9;՞(bңj"225s> e ji j.ޘB:wJ1lV;P_/yR:hf1/#dcdHc7 1ht.3bAHyt?IHTMBL"kjY qj8G&'1> Xmjs)ޝM!k ѣyPJSת;|jh%QvwN3)fex2\lZH"!?S2r|Iҹj?t 4x#juX?K^!M/Fnm ]&5B&5\w`lF͜x?pFF[gP2QjP^&61CM>#:vP5 ϙ[ˎ{ji Wh<(_N']M]_.T &[bO$^Q:jjKFꢚT)TwvYy(/sVS ܟV73BAHW#TMz5ƙE**5<8I5A{} [45tyb;dy+QGN©I})%ІжFÏ)QMs|:VBbC#׼}իlYC|ᤚCJN" o7/aW6 b?{":zG[[k.y&K1T.qBkj#H{g,ۣ|@dD5*UXu1Ы^q^ SB j.+@ڟzeR0yyd#(w b5;xެ]At ZJsQͣ\wٹ,).DY[S5Oо̉StIf_)QMzDw_6rm-P)FMhg@_Dm٣femC@o@ju-_>sR|ēRF&ɅPI~$S#phyt]fE;;b+R5d1uj>[2`w┨-,\Pg\[RSH/n}~d_bӲ6!pTk)`# |3[QIyL%O ~ʛ:\57Ϩ_'ϡq5[\j6QL[9Qd|Fʵ(Qۖҍ \9j^f1U.CЦ%R7f.ՊB6WYQO\_0={ _$EU|cҫ+&uMo/LJb?}~[ \ˤϾVJ x߷}6rm) }(B]z'j_F$p~YrBpAJ+=4+k _@<(?{E[]\b{y>6xd@>ëiPe^JII46'|ک[1SAռM YY_ແlE:frA5eA5jՔմiTSSC?QnT6,nyPM۠!A5mjմ )bpT6,nyPM۠!A5mjմ )bpT6,nyPM۠!A5mjմ )bpT6,nyPM۠!A5mjմ )bpլ~؉TROVdzlpC Γz2J5 !iK`kw@tNKE?t*FYD ?a@+wBvlm$K9J"ZX 4tb|Bmv6,Lj6̜ptiXpvZK>u׏ǎk) YZ5 MO<'hn Saj>9 V; g@MOʫy!(xK- %{|z_1O0^kSGîzfFsBÜt]cP᧠&\IWXQj{M,m(jʻ̋B1_n>3U5X:5H2Td&o1S!sviz1c_P-N 1nUMMRgtB6sNJB+jBzf#{5N:[2%CũyT7~ˎ/,m(j\~\Q/8ڗb&tXioӿ6#G砏!O?*4:w<Э bY U5 A?N7KOܟz*[DWk7nCH~I .FG7?F&Gqr"6&Sg6g,H-$Ԛ[z\D3/K1`)\JpΛHtPv%hiUHbS<^v49a vHޫ2ȣVʹligV`ќ]/;xz]G j>@ RDd]{2vV_&AdFܺ|a ]F_o+z:UUަ(TL;mfUfӪ'c{&vg=!zZ-7Բ$%sE!0LϢQB{bfpu6q0!n~ 1Q#+[N$Øju$bXS|mmN^s"Nнo;׀lQozg=sz–!g{H#YRٰ(*6y/m/S 9Ӥ`9svn~OsRΫ٦08MvQwѯIr5p@˝*0b\8: ;YSS?.J a&䑿l& =ג»?>!$Z&MdwHM-~h4jju$b࿠sӒKN']>S-H4t9\RE?ho)TEm yKcə&˱7+ԗYIڜM8&"A]}r , Gʱ;;sgXiЦf{L']>7YSS?.5Mٵf4);["UFO KHMRoyk5b4)X=dgjiw #;\Bt|)KȏwuX&.g%2W0$b%5^AX{AM6QI Yw _g\MViFͰ'$}N hDn|Cy)dW$ժz<g<ō6jhZM錞(`鶩GUU#LkX&~kV5KCjs4qޔ:f=zvL#}]UȒ ,!jgIM;5ڄwH5TMtr仺L';.NMM6I5ɉ5ɞD:=Bߎ߰2yrюsI=IHMMN9Ӥ`9S(joOsLY%u}gEo)nE>{$sB{ uѥ]mFLϒbYwY~riut槂÷ 7Ko&ݩdԪnבpRs {T3%FuY}n\⹐p'vrk#Uofoڦ V~*E9Ӥ`9v]YWyԄK4VC|2[ob3ރtyO#%٬ ,)֟z j b~j́@9juju$T&3PS"Yu,ݥ97$9@h*mSMϵb΢UtuLYzXΪtNMB"ZK?,"z$|c,ERS-<=KjgfmᦧЭHº݊5U:"NZ-&'Nہ67AĪ6.,tSa[zF@nEٴ0TG=w9VM^ LMI7}:&aG:$Lt]6CUZ/w҆Kݦ&=1z0|>Wѵ Dlzς;Q~ ]GI5= 'B] I:}Adnr.NMvNS*QRMuz*_ܩYjLgҖ4;ff)uY. q={JZsnڐGI7s;|M;OBjgBI>kjnE_o칖SSݮ#}Mrfu0x!;DSbe}EDæmb-Op 틄W3'ŷDĸTMMyVqekrd0pעޛ5McO?=T,՜\-rʾ9 XcRgJBXzh|Ur _t[a#658Tҳp_S?. iGilS+9NԲ795oY(}~iMy Ԯ#⨚#mnJ[>!zB#az=<Ӎ~ &@bA>ZB_kLe Q0WyPOݮ#{5m Nc hvJsS%`FD9!=lZBvd'M- UT=5&%zYmo.EV7]3M vbUr$vv?ۛԭ%"IwMѐىメbyք֣w|OxڬwJZٝʹj.=1otV՟z cR;bOQ{WGu.&ן3$4;Hy#??# /W%Sv ['' 4d<~^GލO_4U&u4͙i/"lYiMRW1nAF„zza '^Fb$Fg^l|ϑ{\aTƻ_TTv**g]P^ޠA5eA5jՔմB%W3Xj"CE{ySլPMY󠚶A5eqC ΃jՔ 18iTS7Dˎ_V- Q-ů=>~q1q,2T3XSG cl9[>D9+ﲅZާotv㍉cՔŚz5Օ;xcc>"C5c)%5{1E>jZ5NxchP͘jbf9Fnb d_Ռ7pY.%T3<,,ߑª%B`\d:B¨IGJck+آȞUJfxPMYLbžxz)ccN10A`a>:tPxcCA;ޘRB5Ãjb=)(H# 4٧G S^2x7>mPMY󠚶A5eqC ΃jՔ 18iTS72/Y~LM=6!'IOlR mP}9BDϗ>jiAxNDX|2 =PMKPMKPMKPMKPMKPMKPMKPMKPMKPMKPMKPMKPMKPMKPMKPMKTX0Q}IENDB`unioslo-zabbix-cli-09a2fab/docs/static/img/plugins/render_list.png000066400000000000000000000210771471265333400254250ustar00rootroot00000000000000PNG  IHDResRGB, pHYs%%IR$PLTE333XXXkkkuuulllZZZBBB888̵jjjFFFʠ///cccIII]]]JJJ???\\\ssswwwxxxqqq111===666...555;;;VVV[[[gggKKKAAA222{{{EEEdddȣoooLLLrrreeeiiiOOO999MMMttt444000|||mmmYYYʼnNNN^^^zzzyyyTTTSSSnnn~~~___CCCPPP}}}RRRvvvaaaQQQ```GGGɨ8ßD +IDATx |ED‘I(%@$JDKEEP]A"_B(KPQ.#@LWtO{gҾ2U^uUu _ @ * * * * * * * * * * s!. 0',g.A1;l ]龓>ߡƐV^ۂ뼙{t ;q&BHogTsUk 6.vH U E<5*x-f? ՙ{3D85*x5n9!Bu֯\ S`v4`~Pb}SS rl #kk̏0?.vqj $TN {{# j$TN {D0P9 6ABe *x=HlAB/@ -H  S`$T rl ܃$TN {Pق)^p*[P9 6ABe *x=Hlaj{{ !6+(&I$.NZ<8Aۺ|9~mA=z{R[ HxzoTmXm9|YPCPP @EH] #m+泿fٷ vNnކ|uWS$T55*[>`eʜ \þ2*BBe .( TB҇vu6hf/Vÿ ..ե6)} t#;p;v 7,Hu:-;RQghmvPZNWyz:ޥoY{ͱ1 H*~MGn S)+)m!}PZHak"1P]xk|23L:*kJNJUbۯ}` ʖƾԠ1}W| p0+yЦF>]HX%oY_/ɗW}A4p9:'>PZH"H b#Tī'P5,?OnMo%iekkB%xz Qd~ ױ&TS>Ulч:Bek!Ⱦ&Qc{Wdէpp/JM>Wxa>=h 2}L[勚}d=wFr~~cɆ;4-YƄo[TIn>A}-DbkbKނ UZrK?_S'9vPZHsND_AJ' rׄ &\F8d G5=*Ks KjɆ QwFdJYBJ#,4ܦOL7ꈾ&4,H`3u)`!K{NPpJׅfxsVPiTEa)d_i4&T>=A&}j2}M$OGzQ @f=vk@{%W%wK0*(TOKg-TS >PZHsvD_A*c%S^aTԮs,ɑ' X?Rj3*ؽk?ɛa8-(\)TE!1Y}c%T@zc};c|-(H b-Tu!zLzJiߖn4 l?5`[O]ϟ |7վ۱g߭ Cܲ;C*n|l('WJ^Vjk!Mc_A7x8|1-c'Uv0K6atۇ-H97|I"  i 7վSؖ^89*,boKZ2.%bd_o #T},/bZH3 +((؉-cTdX|Ri n7&PO20fWeGNuv>qOdBŇ<ͱlS, ucX 'Tj=B}M$1Q dn>A Qxp1`aU?ڊoi{|s^prBAzT6S`zTYrl ܃&mAB/@ -H  S`$T rl ܃$TN {Pق)^p*[P9 6ABe *x=HlAB/@ -H  S`$T rl ܃$TN {Pق)^p*[P9 6ABe *x=b%T{].L\vS:ǹy֏j#sLPŬMzlzǝ`Pb]29B&q:#~<{k 寁ܡ< /hv4.yoL\ gu\w1ο9sھv4_fO l}sç kTut?rwRښ%ylm,1/X-nD xYHU)o؛^{!0}Hjhyұ}>; ԥYJ(B'c.T} ~rj,UZ/}W?iq{ b Hlݝ7ϼbIw~;2RÊ qRNZ|PFjx3aOluJe/n5}ȑMK?{p< R+kY6'~vl Gbjh#v@1D8xSEP*dKfUH?_T762ltՋ R~ &ʄӕgjRc<ɍ|.&˶ Zl&!jGw2raƒU7>izޝVK˸w`4|MT!/3qA/MgQWzeUI7 bbSP!{[oC[{"Ջ$[2sKmᒷqy%Kk<#OjcI m}w8;YˤzDؕcF Xa;a9AlYƫi%^ؔ ^iUCO˔](&˶W&~F9{7i34:}63;(O}`-9?`8B0ϚP Wz?߰4&w7wMCq&sAU S7VaѮPa27!{9hJ'-y%6 1}~ (TU9ze&Txy{H)SuxΓKtaq|^!iDfpdH͸d„CZ-m- 0^t[:lCіR U&Mk+ ; hJgmf{5kJpn+QK?M:؝wl&!Nd%m)W~7?^8w q~քJjԻ/ ̘^h6|n]eYvgvu^CVĸ@<(q+)P-*,tL5?5PY%uu?Clѳyq"_@ׯjOu_ݖC~&mXw#fŒirlo.({#6FBus?&TLcAgF~+0&䈮6#gI^b?&TlJbI04pQuzUOϟw:oc8G5["AhAcebHi>x.l`ÙҺPd-Ljҧ"D5/Le1?ɬ! Ql|Զc%TPo جwf}W^ v\QK*Y#KYנ, , X]YL`o!@;ĉ$Tji *#?OAX|Q )pl j@Kg?CƆo(DkJ:4i3Hz+TPUդKS]TSb {c\lSHR { D9t0\#}$?l¾A_ :ǐ`zWT;[zH,}v.?6SjԻ/ %@DjS&&iVY^H#Tl_A:P ,~6(&?0B"A=3ҡO=-C!QpY,-snifJ̚m$4g#c. wO x`";/E>Y@׻1UM49T<{&o]DdbVɅ c+ҒrFIzD;9'v.?6oQBpy%ƿ}sT ^6,Kkik^I,fx# O^ba`Jc{*:ff6?wv;oOsosT`?i"O|U,iDLm=(Pԅg6;*rkf5#aa뼫C_mO6:\LUFg^b&֓ӫ .h,S{pO2P- pF9DjqZV(\d<m.C?6ģޣ]XCkZsB`_4¥$TN!HI_bnD Uq34Pق)$TA ˴[!qM~2ȷ4u<5$TNqnC<=1'YzZ(x@B/@ -H  S`$T rl ܃$TN {Pق)^p*[P9 6ABe *x=HlAB/@1_PH2xB5xQ؟]@B/@eiMTPM} .NM)^p7UByf< S`tCFB/@Ɛǐ0[_"y*x-}hʬHc,TԤF=H8D/2ވfji^s^puZH?ePAE@Aat W}AY ! * * * * * * * * N|̓IENDB`unioslo-zabbix-cli-09a2fab/docs/static/img/plugins/render_table.png000066400000000000000000000214071471265333400255360ustar00rootroot00000000000000PNG  IHDR$Lh&sRGB, pHYs%%IR$PLTEMp\8\333XXXkkkuuulllZZZBBB888̵jjjFFFʠ///cccIII]]]JJJ???\\\ssswwwxxxqqq111===666...555;;;VVV[[[gggKKKAAA222{{{EEEdddȣoooLLLrrreeeiiiOOO999MMMttt444000|||mmmYYYʼnNNN^^^zzzyyyTTTSSSnnn~~~___CCCPPPRRRvvvaaaQQQ}}}GGG```ɨXe IDATx |EAp' r@DAH! x' cUתʀC\6VAC2gtOe&AfzwWuts|xo_7 gOI\OI\OI\OI\OI\OI\OI\OI\OI\OI\OI\OI\OI\kW3[|v"$|s;RdM=6x!g.-*O& =zdM=6x!$O.4fl Ox!٤4glɏ1ݞ/ 6,2ͩ9nB .k^`6S?k.ƛSc@?e{LhuAxK?8qhpL@(yߚ^&p~NE@-=`:uR:T7*]:7W{٤ytC*mvܱIx>4/=N P%92xH@\B>eQ[`/Ī+> hɜ}~5NEWq'=0ZRzZL3rI;0CI$˩mxjC|yͭ3z^~oBAiN 2oozDym\Wy,6t_Y]!DSnKWnx!JF(III>4 bivZiOM^s4j^~zMOнmKh fעLP.Vh CO.SIW֏5&^D(aŤ *I1v6$GĿ/sY5UQ:Hsl*b7#0~ttkᔐ̞  ϓϥ*yʺ*C>_:̍~5G"m'tYQisA?IHʰߦ-R:df`>:'=ӤMxV~~3ħ.u~5t?K7Q ,]W̞hA*Yī?j>pe>:SF$&ArQ稟6:f |k:/r.)zeL7iXtrF^)i1ߚ~?/|3z X|SSx?CB^мB=C'_ZD8e/:t?o~"mҳA3)g'W|/ >X~#߿]A.>҇&CJP滌ħK%ԡ.YYEbr}NIsB~ҋ#?uwrֲkn:ȿ6g+v@ZWtFjo yiO'# Gn 63Y-H`{k; R*B~^`d6>vu~l:u|e ~Qtp){UCy J5REXƮE)~֢XF?Jދ Z?4.raO>ЙCy]g/$9{o3Ҭ 2gR6-(sCIempq"m:%'5O>f&2nP2`;oZhm=5>uDlj,U!=㗬CQ-RSi.댞zo CzƯX&`b{eݽY(k%wME5מv]aQk~\#CΨSo5l3\`OgnA?E@?~tF <0qs:<؀~:x!@?~:tb -^A>3Ox!@?~:tb -^A>3Ox!@?~:tb -^A>3Ox!@?~:tb -^A>cxBxcbO17~xcсO'-^A>'onB C@?~:tb -^A>3Ox!@?~:tb -^A>3Ox!@?~:tb -^A>3Ox!@?~:tb -^A>3Ox!H`*v9瑭ol_Swb5.DNM/~J+ /9?8qY~68ArTkU AlE?O.u\Ry_u/xMܷnO @їwW<:1!'?sl:[6&Jz1veGZ{2TKz_N {#Ҽq~x^tsH?dwL昜}qoʛ;(~h2B)ڹKn*ve9)a+ȟ_ʧ=2.7='7YpکCKc_kDV3Jwȩ~-Na :[喑xyհ:]qٕ3VS?Q>Ê}%a̗ 6}G%~9hXF&n} F rs _٦gCzj'ѩݦKZc]~^ 1P1j^=}3"~Bc*nq4$9n4uS χB>=^ZQL4#qճѦEeqah-$]$#/~rimzv1RNq?qg~IRêucRv3idn2H )y3M'#͸gÓF Hy:Q?VpZ'Ǐa9'KCіfF&$k(=GecoqޠCzh|yŌ cI#[G6+k~4-mjR1M.)o=ށ[e^:AB鴒6 ȶrb~fg%ncӳz>M_nضP ϼ;v~}VS5?})`:{ ڻ`25kHny6& 5HSNu)kjLpݷ@뵚iؕVc=?aHOJ@i0O> r>5jlf:ymP:D 4X'觶P\}!-M]2 ~D䕒V B[|:?bW3. ,Cy>ipz ~_ 6fM`v es)ܤ$Ma;YϺʳb..g4|[ +C]LiQ"įSԇ}3-ӥ {pڊ]goܣ6';ѣ@U˝ncyN>X:bMYˠSrNobs,+*;W_AMmD;I3H];;GBils,lY tt~u4$?~RV쳈uUK?緃wOx %z҈/vv\^i6o#-4-)&Yn{XYSt.PԙWnt)v/Whvh,?I7=ptURJH>XE^WF Ȯ9Q"zɵ+*;W_ Uz #m3mڝ%~ R'_!9ɠx.&s×etkgJ^$@S}="HOhWb~ep9A ol$ecguW?>yRaHAIgYr ^#_svPECe;\uzB~'^aX}O:UBpF?hxƍx iZ'iodW\n7Z42 a&啶\GR/鸊U(dXGԔeco`-4~vׯ/-ʼ3$$+YXrv,%;?+ԝEȜ-/!_s?m/?r]A p}L3lYkO]GǭӃdO\&`\R:JAyNJhX1)Fr2SC۽㧅'-ӥ {㬪~~RM4?=Ȋ-)=-nxWmr$K?"du g 5=!?/w]xxztB)ë~=[Hz ~9!OzZ7ayCNbHn1t>lmS%??TN8XjɸN %TRE:^}|JV0B0+kW7rS/᎝Ov!t4{32gRz hx<`|;__ӮNw';+v\MB-~j4ѐgM|}UN~M'Sg&i~*"+aZ&М̩ŧd~NʈmYR=y~Ms]eT,GuM61k&ϩuzQ]a|;__ӮNLQB*%Ƴ~(HAz )!?lWlo{d:O]x:ЛCM3SY!K-m&ԦGeދ[O,vnzMoI0KH8mNp!J/zVὊRrH4}{TԥW"~'^|}!\U2 ߓn?ru~⅊z~^niShNSΓnjGS%م pz␴ ^d}yj@vujӣWM-s'LZv?rWrlYq|rާ ~9Jt2Z7҆.kɐӧW"~'^|}7 -j+t6z ps sdz?Ioҗ`p=?;XS|==6`SF^)m4ZB&~_)w(k"vL\ ^ׯSW+o}^NeJݖ9쀓 ~]i{ ڠӾU> ~:2+f^c*Kf n; ɯQa48l6W-G ]:hj3'w+?r ԣM֩j&M|*zK౹LvρAc4+?3":*moK^̍4vv6%]E(yu҃v]]6X̱}^A_e ~*ZL36~ߑ}{#[v uhvCEPDuAH`.\'喿` ɕ_ʝ/JzK_'/VPH ])K#'rF_2ΙvWߧѐg׭P=܌UI6M*!с.@?DR"MŤ괧5x?(:o̬nt)T%l~MwQ;v'9D+pQLbx&eӸW_ zufe X;Ti3'MOiMUܹ& 4_'8WFқ{vL!lJsc?Z'‡FQik)9I7N!SzjyP]g,ґ3o.쾸j߰SoU1 cT>x{u~:t ) -^s_Zo,A?>x{u?/ [|OgnB A?~ 1t/ [|Ognu 3ncGA@?~<_SB-m[7 ~{&siGH3ƃ~:ty qT=ěͰѺQиҢ~:ty =AyjK~M&~#r@?" ?. *̡~ߥ7(hi$OY}dB{<-1e7A@}v_?~jQOInz/:.Nj ~|Њ)҃cGAGHC?%~Ej=%s ~]gR?T(hi$OY㗁pre) A\Wz*_r'鹩m! !?{2e S0yx+ɿl{.ɟ(hO9n/U4| BP?7 *~rz%Uy&v4ig/F?%~E~=1OnK?Qcz6q}(h͌FDE?~E[1O'Qܽl4"wh4<+-B~VewEȧ`L/ϢXJg6 /O9n/.Su!!st)Ӌ,3̏!n)-b?yЀߗ}JǚS0'x^(hҺ.3M8:OYխ | &驘IoS6֍VSv !? ZrI tM q'z.9JS0IB3+3j{ N1v%!A? ZrI tx =~U hOKۡh&ՠ@?>)T)I3Lnu+$U [~:t,FgLn>l['<B@?>)E:t/ [|OgnB A?~ 1t/ [|OgnB A?~ 1t/ 7~zm7ƀ~ 1ȧ֡"#9biU1ޜ/ 1 j0ǞxoNtbϨLϖliOx!].2sI[t_r"y_2vR~ 1fڔ?0}r rj [d< nRM!X8zlB 2I{Z:wt,s BD˨Da'Y>/qY AAAAAAAAAAk˰IENDB`unioslo-zabbix-cli-09a2fab/docs/static/img/plugins/speedscope.png000066400000000000000000014503411471265333400252460ustar00rootroot00000000000000PNG  IHDRg#}j~sRGB, pHYs  PIDATxxw-MB -THH EE,W\P@6,(\E@*'@HBzOv7_2:)?9s~̞w39V`aaaaa7/EP#E2o)2U*R:ڣ:{$u 0 0 0 0LQWz2J6bԕh+eԁ܎ 1Kxj#⿋ԡjY@j !0 0 0 0 0Z:NquXZZj2ʱHQQBMϷr]pÝaaleHKQ%bߛ4itСz~={F,^TaǭVkl+66qwѣ`ؿ_ h #ݛC{oQ0wmڴIIIIHH3 0 w+Man&uS*l%JHͳ9Uӡ 7:fpQ;Ym Jk~'zWfm;jKkWJX,$6qPlInnnU t:,ߺ;O wa^}U^2Ć ^*U$0 ,GGG 4xp._{Y͛Ϙ16/99ykӧZ f۱cÇ+ג%KFիWXX駟uuŊ5;+W*C؅n߾Ҏ郃uV<6lظq`g}v'㘼QQQZz'Qay_|8vݻ޽{߾}:h4 ~aJJJ˖-gϞ%e_~cC- GFFa ;w4`ի۵k.[g;wn=עE*=P8AAA˗/mN<֯_밎ƍz꩸!F4jơŴi^~Ra%e{/o_deވ>o)-*<Jnآ_`ZrrΔ'$NJ(@ :TpǓ PK|f&'4~]A4|.v`)-%yp7s7 (4L'G]9}Ъ2a¤K+(ÅAAEs=Qث5l0pgS.]nܸOTlc8kZ6խh󣥖Xo߾(MK.C:|J@߾ (Q9yVH 0 >7xɓk4Oxq=JӧSSSe,/ZրN:Wǎ>n׮Pnnse}[ߞ8q"'67nj0Z ~;s…ݻw^7߄[{W]v_/"<;nܸ3f HldXnҤI&Ljz??#s=ݺu;p KHH@o+{߻w/x-nݺSݻ׫W86 vGF<U£nݺދ_`dq[l 1??gϞC-,,C=ԪU5k֌5jcǎ6}6l؈#Gy. gffv4 !N^I՗^z CuP@eCVf͚uy ʕ+"Ί Hjժ~ ̜9v_(ҬY#GVШQ#BIII;vD://OQ㏱5<۸q#6S8 6X,8oCBBp^* !Wm۶9|q7hoe!N dϞ=7ocaa@h ;k.e)6RiS(2s}c;a ࣄǰF?s F7{ҥ>gpo{#O}_ѻ݂+,w~aM“dN:UѶiqI( Vb/[c>|߹غug(E*6}R7{*ƩNh4c7o[ѢEnnn',RI:zACc{v|P 6wFu(9jNO7oLMܜZuPjٺܾ ޽;,Hƒ-=G'`x$?Rai}C~ƣ6xW;on jSZ(SQU<'|򩧞c[[lAUO7܇ݪW0^۪eH~n_{'QpB///L(`P>;2SV51]]]t(2}ԥwޣ0 0?Ch#GѣܹsG=|pǏ_rWz bH׿`͛pw)lҤ̙35jvav۷x ipeO?4o߾GyD}zjMa ͋~饗ӽ{w4q;x5ܱ5NnT*̸M6uСL͛ G0|Ϗ5 Tl ~qݻ7>m۶;vڴic4%K>s>J{{{WXxƌ?믱֋/xmP.~+V@x8hK.!hU77=wZ䛮$Z;J"6&vӄUataqcbbKII O>% C<̩ԃ?/1V}aP huvN!˗/TlkL;{O:8X8֭[0III0嶉CgݵkW5#)pwQ)_1nnz}BbjN捭~ֿ) "^^{Ƿ ăKOħ{m[l~r:|Ev_ƦI5;= ^1E+$t7pSߞHRΖشzz픰z!ns\Uj lmZ{5ru(:姤 8p mbvjhyprGXXb]‹N$8e322P砊G?s ;ٳg@2ȑ#}@*mӣ6)CC/Q(nAI7ߠx# (󰝊ڈ wgӦgT8JR :0gcA… FX ~ EԫW/m۶E-A(fw}7 9H/G֩S'(P }`Ix[e7aTh(\w؁REnB[}/gܹoR`D'?PhǗ7lձ[f= ~\\jS(/8g}V|=ܬ0m LqYēbB``]wmڴBZEX >̙30X wԿ {)OaO\4[5?L=|?-2"HHc0`aa aqaWK6m!KFC p8?V^W]tR~Rb]TZ֖7 \ufr d&xe 1F Bհlٲ_UhvQ8qDH7=UO׳Q뾧N_*)- o\Ss;܆eR$FuW9J,/h|lcv cN  2F7}776joyFRy\l_+ĭOK-(.7mLjބݚIy#s]fW wgh{~'4#ɹdc)Mp?wFӦ-otKIqlpGָqcLxG%( Nj+P@+W`lde[@(|BQb]T0;vFBԢE`QN̜9U f͚К8qbEņ{C];hIJ ߌN(Kƌ3 *"1 p J5T5qpb`!D;rH$$ H۲e J<o[612mp`qvbpb]QaY>S8r8 8###e`Ȑ!Ff͚5|p٣6HLL1b|իW"? \ \5DSKҀC9VMT/_CiþҀ9v{k߾}0QZ,^2k sЂٺu+ܑp!"u<\rҤIpn;wDV^=I|0//9ܱ}ۆw9w+fꭴ᎝ϰ(;6 LG>:§dѣǼy pӧO<^N=8x۷czꩧPdr&*IJÈ'>>eϠA%5ܱ`ͰIoaa4iw1w\I؆bhp% [ axIV*)W<*U#n:xO> M6MFXOOk~J4zٔM*ݾ.VmpVnsܢ2 :7ПK=bzŝSt\aK=Otl8bEa~nk6)e1́뙅%^.E6m@g 'Wpw+ԏ4l9p'Lxb}Ϟ=ǎ)(3f܋A< gΜ4ƍcTJcT(P(T NAyzcǎaaa~!NG}ƍP4@ߢDUiؠFzϽ|Ԝ4*E-~\(պukrAɋ/iӦ[n)CDzBXr˖-(YQo_v8 ~),,9$?x`pǙa)8 M+ݣ*%fa_)poԨ /Ǐv?ŒP̞=^CK}׿|rHH? 7oĉO =¡ s 9dff wDV߿ `\]]aPu(Ktn=Dw:t;=Ehvņ;*(:$111,Y BXqС 4enk!<@ăxG'n@6f̘D849s< 1>|8;$|)9X~x,C`ErPJKBW^y[oyrAO>MA`8’5p)vP0N㈽K(pqv`n3 0B}o߾9تiժS~83f̥T{ )ry=xE;x o %+ƍ+((ӧɓҩS'˖-[~L)3ӂܹ3lm.]Լyt,MUuVTpf)]aU<ᢿ\ &9}Ij;CMXߊ_?Y~oґ vlZ78}彥,mzkկ}6EjQq*6&.}{zhZFpsE3 mzk.,KfW*^HJ^Tk{o:_xzv *4r{N.QI># EרTRe C f{'"~BYa&TNFFȑ#gΜ)|Y9]9"ȳG }- g.}WR-\Yt[Ш@{s/qԳm6bUҥKDMj @mv1NP7ܱٛ7o S"3bOņ{ŌfGUJ0 0L w+R^z$׌j06 g``<۷߿?^[n;©"<,n:ď D% !`n)F$o!CEE`Dx޼yT&N# |N`ZϟcxQ@(`%;vآE 5k־}f.]pj) {L2Xwww^h ##yL|Fa1L8\%]\C 410 0Fh{v WfGZǬBRt8 Xt=w {jժUp&gNJJi۷9scčl޼ı" A m=6gwAiǁ&gm_;_\ =z,[ ao}РAsٵk ObbR9hk&Q?vwe[n>/&$uܺp%%%C AxSLv6nwߵla`8xnVMW^؋/:O=a6Sk[>8[qJ(ܹsGœbԇ"\>$lk%=btU{/w׺yo 3J4OHrNY߬:"wV۠)y={}>?=&p(Ϙ\P KӶ~}I-<\z* w7/{g,-kǖBo0Ry辳~$q~@TA '~U>s:>|'JOעފ?8pB!!Ͳh"jK)ʌ8)1^}UaSP /T_V)Mo_kњ4 KsMϗ3VDrƩzjT}(NPTlCu ## .)ȃF)l[nBi@Dፂ ɂ 0 0 0 0=BSx/]{mܹe|OG'|)~aqT>`^sfff_~ݺuGӧС3ft 6^;v؈#/dɒ%Ox}}s6sXȔ{{y1՚߻^:t@lÇ߷o_llΝ;;wҥbý~?СC;®\rw/[ *m ^|hxOtBSSrL͆o.(6o髹lbʲ/گ\`ׅ zM6)zpի`p 7xC+!k׮9sfժUFҥKM6ݰaT6ܻXzҩl4ܷm۶b Juf͚ :uj8hX駟(Vu…#)psݍJTRZh2n7UzJ(6xz_R+Yr._*y[jq4xԹť:ufA k,[Qttog홏5 ݝO,V۵\cNqiBY9Xr(9h6Ыܪ菆{ԹZm6F]\s]>#Åsĉ8|ڵ 1œ8'y@o8b0BCCϟΝ;߸q琷wq&$$!;HW^nBރ7cpGZK*D!|VF87ZnݢE NKKd&ڵ+maaa۶mq^byd'OgNpR6n±6S{A%^#TR6^e*m۶{igv&7+sNJkdJ¦t:צP\q+u?j [WXBi**8͇Y)7%GtJ^LQ]վ[ ] 0 0 0 0Bh 9e~y'VO;/)ej6N{V:3I%|^t1XVh+6܁^Ԫ['R,Nm4Vxg̙ۼy=@wwU*uZbK5bsWE{q;Pށ 8"dHߥ]vzܹsppÝaaaaFVMa''' oME` ͭ0pW(>˘Vk%NTE6mRRR6jJ~U=I҆{+NQ)px waaaaNSSMaG׆:5p}j^~odAMɼHVT~ YYYJio J00:I δ`5)&8R&%f]iY˨~fZfXʱWRr; lЪU(`j`5)&%8R$YJI δ;)8Ւ,%x`RդgZJLK V f)&%8RTKγI Vi)jR3-%XM2pLJLK nS-8R&%XMJpI δ`5i<0)jR3-%@۶msRWW=z9r:tHNNp6hР]v[ߑSCrfkѢEhh(ĺyf%;w 333gϞѣ|/γth4B#lݺuHHHUjvSPSvӡ`4m4..cOÇ!)9jIt;V\\&>,w׮]322N: /_\8 PWF<$U `_jJFi޼50q-kAMδ5H95rQA/&&& j?~R+'k_˖-arҤI0X M'6fk\410ĢP] OpfϞ E8׮];w?{n:H3TeP=Z4C ر#'ҩSCA͟?Y.lWZqY`-ry"={\lY1 O?ݶm[A^zU&, &ֵX,\H? A?3_-Н7nؗ'lh%˫WlRPS@gΜhhWp"xС^xaÆcƌY`2T?a}4hpʕnݺ toH_yvg}>esu?Lhڗ^z u .xǎ{衇pDb0/ܵkpߴ@Ԅ7BR8qԩSRܷoVfA2|p(Ǩ+W:HD:o޼{PdQ @!!ڵk5k*e5[kҤIaa!D9 Wd)ؠW_}UHgbVQf7om6 qa*USOT9@ j t4jB9sO2~-{5DC 5C 58C:QM$XTwwwAMTDH' ' .׮]ya06M2~79r޽{*O2fu0v  Tnb4jԨ .X jlh Tq!#<"9c J˓[n j̜9S,OpnBImҽ5k "+Ɉ#> - s6 K`#. hh˕'7䆻cB)"ܻ j.zꩧԩyVËvBJMLLٳ'(}w/ŋéA}PmϞ=> >}:{O>ƐÐ.**:pF102q^|9Lm֭a7oA{ xԼz*QUI95˓zh͢pJH\89y@ ,ȂUZw^<WUWjh98C%oՏ,spåtΜ9+XƍȧXrܹɰiԩ/9s&.XgϞ0=Æ 23fXn}©AL0 <.xV$Q`o߾^{ vĉǯz{e˖p.\뮻;k,9:m&1fD5 &F%&w}W0gΜ&LӃAtСTS0_A>$dAM\_~eQM>Pc,,#Y71Pi(f#RȐX*#,.{g`ԓ5Ō ȇ:B;,#,,lܸq;v8xik{9 5.] $ *)' 4rHHh`D5`XMG#(_I^@Q!Z0*ĕQ4P3??_0P?7>.Z!oe˖!= IvPXVU;jo6"6GȄ@ Cݽ*C ]Ε!ZNU'RCiqrr2dH=VXa41'@MѰaj'޽{C 5;tASOqxAR---EDdby5Q `@I"'pJ8[íDH)6523'&'44' @k׮1/W[r -5ZN5*D%Ch,OEC[Q#Gj&''uj¨` Y[lCUTSc&䫴00G^P8ٰahh?Idd-&7kY"~.fH&Ipww_9222_T[jJ)q٫ 9IkPMC___|XjbC"ihBJ{i.xZN,{p,EUik@8ZMδE9KLRzUT>YMt.Oj Z .OjrJYM((>\H:i1:ZMnS-8R&%XMJpI δ`5i<0)jR3-%N NdQ05LJLK Vi)j,%x`Rդg?qRڜ?.|x<8W$uL 瑖+uL&%dimRPyHS3pIN,p8ށ~Y)RG %ܐ: }6*6K?G9Ȝf`jʜWÈ5i%RG <0)5M٥RG @maJp$^'K`CKo7uV>gZ *JTi<6)kQ(uL煉II: ru`jsI5ԩSQQӠr8a05ʔ+<Tn&%t*c2D&6t.HoLjC <9|" ."nS )Mc(M)ʔ:uK: \b[fgHɸ%u7ܙ-tjs'}2&%*[7܉3W3~y]d z?1o;@o:XeΓ:6&o*۟͠gA1)/u1ux{Gzڢ[p'LJ}iV=/Mf6^`,.@gOdCSKlʁ 2˜/u L P$[٭z7o DrsnS"G^؈ MiLI 6'Dp`.F_?TA6$|[Cp8V~F 7ıXJM?*6PA 8 &$$L]B&:PhO<->~ zmM 跁dLJhuDhɦ?U(~SOi d-dɰ]:ڀ;AdIȽi}&zT#.P^\r5SpL2mup2>"=eSc̡ptK?mLI VYH]5XWHDm,&cBQ8'/!2d'%d : VDDsg\ϔ: i2ɴՁB}ڄ97W2A20[DI *t6Yf wΜ& d>h W*.nE9RGp~U5w 6P`C]RGp<\пV)%4K d@>u|d:=:/Y/ Ad@&[SH_MFRK1xjs3gګWKBm8ɵXp85:@f̦.:ڀ;AdI([Wp8p8ӿpL2mupx84 2}&%TnTjQ8(RPȤ>m!2dۤ6Ndf: g?JRlFR,4K d@>ybDj2?WmQcyQ RGpOM}2(C dɰvHBm 2$6l0O(w)K(NBlzB >4K d@>eLDB`jƘ}= 2DI nӴoQ8TvdI}ro6к|-uw 43Q8Og;Se^ܜf)!L[(4#&D];4<p'LJ5~'u{ p8%~%h dxRPh\\yRGp;! "LҬ?E(Kdp87df)!L[(4ܧLsSp'LpW)4jF(Ue.oVf?wCRB&jl,;eɰ#uw 4k=oKɍ+ȋw)YJ$V qGIS3xxdJS HI: p8~ýV\Jo=RG h)!5Gud؎<;SI 2zVnf$fH4K d@sSB&7Ý<0)!9e;ZJD:@5;6Ndf[ԫߢQ87!'+F(N: i2ɴՁBÝpLn<;%d20mBJ޴89e;ZJDAmJBm)(/G韱 B&i~OPSop8z?'boƤI4K d@>mBd^"[HdMUO 9.,: fTLRGpL: f`GK 9XC ]Y9]P/6NdfZK mTG~[ײRbpL2mup0q͛ nn|ߴ)|}22F:Er+#%E(w=]: f`GK 9,p/eo_jiҹJtp8V묑: RPf||xp8h)2QsXL)s3-IY֪;o$]LH;ܙgZ iG\+sWH+\5Ei'!Ym A8(~}-pgh)!5-uoy8ӒdI xo'^ w.VB}ʸ:=jSCB`]~"Fmp8OMI6E\N;S`GK 9,l!Wp%_7 4+7ki+9̜Tp8f)VB}CWKS֘-\MRGplV'+Nv6Q8(!5 JC ,Bδd&dfeLJLK qVL(qVMZU @*-#Ia4ٸ%XMJLt1w3-IY&%8RB9 t %J'Oɣvq*GOL"L]դ;ZJDMo'Y4܋p_7 4+x`RդgZJ(OB_M6KiPJ'*ID2dΕ: S\r?yQդ;ZJDM,1yJ:u 2I2&%XMJprRD|Z^(Meʣ?LXw3u Vh)jR3-%XM2pLJLK 1&}=P \I wUI.{JY w.jR-%XMJpINI Vi)Pw NwkT#lFÝ[`GK Vi)j,%x`RդgZJ('<`GK Vi)j,%x`RդgZJpÝrHݒc: cVTwSS>`GK Vi)j,%x`RդgZJpÝrH2=lVKÑ&%RդgZJd4K `5)p'RNsV餎L`j9 LjR-%XMJpINI Vi) wR!ꜜ_(E!uL )XMJI δ`5i<0)jR3-%N 9ZYSJTgN9 LjR-%XMJpINI Vi) wR!. w'Wuz>7܉ )XMJI δ`5i<0)jR3-%N 9ZJTIYyRG rդ;ZJLK V f)&%8RCU5*Q8^wAS>`GK Vi)j,%x`RդgZJpÝrH*'Ze: WJS3a`Vh)jR3-%XM2pLJLK i,a05J"RƎJQ%|$|`5)VKKKXMJpINI Vi)9s10 0 0 0 0 0LGټyׯKSxgNNԁ05OHH[;wN(#0,.a8-G.aX\p09eHSpN n/W;arEG.aX\pZ& \°TaNU w0p' _Ý% KN˄K* 7 9*N\aCaq i0۷o˖-օ>fXX­~ǎf}zZZӞ={N<FߢŅ`L?,+P.#M@.M{-c4}Swt{CBQi H޿?|#1@3;瞷?3wKXS}rwi͚5Hssg}VVV6$>^xW]Ƞe Ƙb {HovEE֭[;::H0>AY #VSĚYYYaM,sдi|) >MMMQϜ96rΜ9HHqE>*ٳgÆ mmmX[#d/^ k;X?~>ꫯ\]]QMNBBB||< \XX N555$XXs:u֎;29+//G;vLMMmԩdA3rp\?C$LԊ /0zUUh?5?cww͛ϗC7bXFrvZNN ,rGA_~%VMh233exd…H7b$/@AkC]m۶!K àOvwwϟ?/(10.dDYlxҥnOиĚ|OOO-m{{{t@Z뫫xʔ)555㰴rkb8]bZTp_`ݻtI|D@뤢 *J V$hq_ JOOT$) KKK8J"666X A邯9UCCƒq X)Μ9{jjĉ?3ѐw [ʦ dCwԩcccǎ! FDD0}qssò2==}C\\\7Ƃ#--Xʕ+X`k?5׮]fYD17.+Wr5%裏֭[WXX$D-?@ ;_ѣX\ľ;w `Ms-**"");# &)^B0aa>---,4cccBq]hb_GA/˼*YkM777T,XeeegΜ>^X.Y:tՄX4;zBXdddFyyyw}%^&0B!Bwk'b lh0(|D:xjO>A[U1q&셐D"$MMMHX>WDE8XJ / E"(chE}9(%kرbM&˗m|_-www,kA'a;w999իHjjjRh[a!,K t}Ec8EDŽ?sr32w\ -`jE0>^wID]㣏>B j&L` (jƜzR7oF޹sԩSI0>RHDihh EJ' _\&ĚX'߿󋌌>}:bMRRR0|(0. y1m4l`hiia`< Lp%XMzxx ա@ϰמ?^AA[,oىA /8ɓ'8ڿ/ X5tD00 n Ê/**bvooȌh#8p˖-LPpOJJ}qE>*`рp{w\\\`+W a0qRqrr"f>>>X=sXb)#(#f 9+TVVWdj@;{ADXb[I^d < bw}#닍}װݷo.%%UȔ~D_~I644Lhlmma:=؋XDEEWQQA/^XP$b;r`wmݺk}r 02ʕ+0b͛gnnc_|kbwX} $$$DPp"JII)++ 3!++v%܌u:pك[ZZJv$WA$b3bM,X;/_|VXr mba+&&<K#Z,-Z%qUU&$$ W^y'ZlkK"քaG4O?wlF-y;v"w@)DU5k'|筷B"e.U4nܸqeXsYIIwy0;%iiiylѰ0qGDިHXV +KE*FwID]/#қՙ1j6moE}ԑܵkn`|tIĚ(ւ=/ (B 4MM.GS5`r $7x #J-1. رcK+% _p' JS0&(,XpÂϯ6 &&|Y[ؠEGG#1.A DD!9wE^ 3gĨI0LZ.\YN2 ^FFF(lo߆KX`!O +!]t A0 [ m<*ߚ;o<$ߟ?>vqpp@eYqd?36OQY $Oj[[Mb>A[ $9ʷfxxxFF5HẌ́` ƪ,..Ҳº6n+2egPWWG.GĜ VWhfarQd3f5QOgϞ\J]RRjooz 泴444D5D\"w@T¸ȜW^ 鑂@0ˆ$6 26СC=Ě:::p SNHXjc ;.\z 3ȁvtt\vL;VȺ999$6Q(, GDՅ/VD0:6ƍǏ)+//36X|}+Ggff" !ZBBnkXW# GX pnɡ̸#Mr--'NL0 ի/^H́D %=/Q0 f`8؝)ѣD/S\QQppp0fwѼ`EI yIK l _\&H|kb1L:Y+b$|cϞ=Ʌ5 H4%566FZ@e Ɵ 0bXP \aƥ)E.0 S0( rp0ZX&S +WÔaK1̸2Űȥf\Za 0bXN&S +WwabC1,r)bXZÌK+L&S wjabN1\Q Sv(E.0R K"bqi)wa9y8N-\Q )+aŰȥf\aibXR 3.0bN1,'eceIX쑕eƥ[Zz g [Z127MQ0FÌK1,-S \aƥ`P ¶)TrppVka0=\(OJ`\k0=J<"-Y4֣`,r)bXZÌK+Lmo{S0`;rE1LpV();"bq)eaK1̸ a;Ű<LpV( 0ebXR 3.ŰL1,r)VA1Lp 0bXP \aƥ)E.0 S0( rp0ZX&S +WÔaK1̸2Űȥf\Za 0bXN.*&OnxĦQQޞWVlWV?nh5!?5crC^QnlMYiVbڟ=#()(()=ž|mr319bҲ2m-=>NWT$;5%w9܌]O*ZFz5-mߌ/:Ag<#"{1B( YCI$2%%$$%DO _X0qA]#,. 11ήήG8=3ܤ̧~EAiQfSO1nAG|E_Q1$'wrzBνG?iAF^Ld0Sa5<;$TuLk˪$yR0MG[;l˕zkcsg[OV'!!>`kḻ#=BDhCun(=I< y8Ԃr)T{k(b sڊ]9ؽ[ `Q Rs0#r8+Q VJpm0Zx~[S˟:5*Xs"zaz#*hi 5# -5=M fgrpu)^cЊiXA1LpÌK+Lp&S 1;s\\x3G+3ή!퓼t卓aeklq$dLQF~ҍDUc/s7/7Ae U%Aǿ[2=EGfξ⽍ If&v~} :&v.'?uL1 qɵ[߈v{n]Eв7~/ T]޺?I2E\B"NRY^sZ_䙫5[r\8u冪;r*SOV*64AYQM9S'2d rS7N]] %[AY"rQ'y9JƜ<ҤOr=K*3y\Ppqn _6WQPѦ, zȠv6cBӍ ^×nhmhy64]6AN֍ %rOhZ O> f(#0=)^}DC.ӽdK+1<n^辳Fzr]]G.HaNii RҼU8zfH-$}6'F޹|<P A0S!'!Zyq5`du#YwqPm{:)+gIJKt8ZW9Y>ǩ :К '9'gHr4йr6+f%EW##`Q#seGP-o 'pVO&|@Pg Qgmg3búa;Űȥf\Za;0bXNw% K ލ؞ I-ZFəe+PYT:XhCZ=dye9IJ]lp̪:"%م鹂#*(+{d۲,d)}s#1q!7ۏ9Y֔TNr6ONfߏSSQTW_U~UyΞyjCuWVfߔӏGaw Tېv'YJ7Q\Rë((Ź(hgy2>,wwrޖW_l\Ʃ)c<8A=SFmw\mEE%1z-mɑ N-9Xjf%PfV8jaDc̆g&b O_U0u)îWR`o!&.s/– w[]{U WouuXش%ʉni`b;;qu4FIg-.S5~p⩷m &z-m ᷍'i)UxVc6ýqiVv8+{~XIHIz{1 #dZgMdaIĈy%s6/>w}kV@|LvOF:xKs濼8X6FpruLKI Tl}0ow݁qRd)щ1yDINanR&>،EpGL:zm5-FI=0^գ3AX"# Rs}lťN5uPRSb%pX3Kϙ)ʈK+"ᥣF`pi8-&-;1 E L{{{eUągۑ,wQ'(8i6jzQ[䓂"@rJ|N+HHKB.Bg %&#䇕 \ldc#JJKH2 rP1 sǀa8 R\JT"0y8=Ml1ZjH`_ǩq5RJ$H(%uD^r֐k#7"qԘ{p8cp vq 4#}y/-?aomj`gAd%*kh';bD! įV8GLZUQ5؋$-Rd<1P DE|χ{?iHj4Yb:Z1]pUOp[OՔ`8L 2*ʡ^twu#&:Y[Ov(L˅Ւn-xu5OVΏab7 ?D޶t(( |`AppYnQ~Z=? >zfT 8RP?Ljd=#LG`g0' !Db!-'魯L K?#ᬑZ1KJSrDpG\`pZDi1Yڞqo0FEP )E.0 ) rp< u&uREakk\}/l@^WOUG6NMmOkUbbnI7[IƖR8کٞfnkcsY^n2ç?Θ)7) -JP0%*uOs&Fq q4Dw#! }s|y΋K31?.A^ q{myƀAb2͸ƚJlsOWgnk늳#(F:g13՘ڊ;gry.ݴ``EQ<aO>ZV,? ',JxkDd]'\NkT Iɢ|E.8V[77kooN L8wDi֖Uja&qa1u"+&2^\SA|~R$S{ ˜]~]=3C~Xau|ڝlLiKcuFUTQQȈru|f~ejX;8G>ڎHY'B5 crd3NVڊjQgqRc#(7KgWjKrE85u=MQ̅ey%ծL% ^^I1=ygÑd`KOa*ZjQMu|%K{N@}@NQ~E!Ӵ8p[ET gqfWVUX9k?욽i omh6s:8mnrfK}?G+*Crֵ|IIa8٣=JȨqӽ3 7*˘y$/G#3 v/,q's ~2N9lihrT.6jjcv$e ge-_[~~]y5vDd UJmP d{$d+Zo޺9\xOwoWGD㪢 <1\dzyiT"|ag'ۗIB>m?s[eIvѲwe#lZ{6-ɈMF ",HI!.j ,MԕQjVy%ssg\X4*Yx9"vZ {u$ /㡘Rq7-@ y-'r6vuvw8D&VcOVލo?(C =%;U ).DeDw={a d_[[<|]0 N)}bN.BܒJA \% K7[ς@5%k.tp(x)}3 &S \aƥ&S )*;b76x~ä́\^""ԕ* DyhkG|f`q74 u(ȢFq÷5m1Gϟw7"/5fCazu3{Vvx mČ Szؑ7k>r--Kzg:y?^$`K+-r )KːKʨh}'4tP&STS’!78n㬧]zT.a;_'>,f?70m]SFiy.3*)77J_[QsYJZxj//}^}e ,%E+/ ~=a/`dKyy/8<˿0̽U8~#̂WW8-}+kuwټ ) Y8g;t'7sjm}>;YacEqM-7OKJ,gþ~&2u pǤ /΅|$uRoGPpD%Lr#q ;;Z~z{zs2kg~y25R8R8 |zBȮl}\*twYۖ7*ߏJUꭻ ^YßȩytQ5eUnaqM+Sw%ZTTteu uyYUnRr2ih>z,"pID ´<G~gl~'vݧ`t.֐I2қ2 2M5 *w[2;aٽ{C~jB@> $2x$`T׈", 6dI%G&|e rv5}MRkk?ܲݝ]>{N5;w6 'rQ$,xu! 2x-xm}[oTҲҪ:%N:Nj)i^EaiKNm&46c)0l?%MU2um [q;L+.oxx `͝1xʢr2 Q?mܴ;Iuw18ko%Y/,y {8ڬD GN%eHj4NwEDp:&Zy?!i rzl Z3WTL ɤVu 8zrDo]ݑFPțOHMN<x!?T 2^溘7 k 䮲*ݙ1`JkJ+9o?6}- B>ʼn*9 wutK>a0- k?|i~8oA~&w`m\n[s`jTX$\5Dp'a%?*O>`3uwaK1̸wa;Ű<Cw[/xΝR[z~r4wy*MsgՔk˪Ͷ%݌Gg^WQm`a2,zcO똌//WR|汯+ 5w.G!I.yc彧m̚ЋjO~=UnĶ5tw*i/v#/Vdzqf>Que{i~ \5;?5/9gm[ZVsӥݧ;kN|]IC5h}gg>O̝ee 7O] X9O-#]2 3Sr2q({?U 1%AkjNl3Giذ3OD'+%fqvbZyɑ9ER2DVrMkjJ ]YTN}Ce8w^VQ>5N800#.%N 3KNpan5.Gy)fxe~=1eθLO4rE|*^xN ~f?+!-=Qq1cƚ/!D6d=3I))2f;!v^Jsń㶨w?l&;E&{A&>1˯'79w4ΗadgVf7{ɔfM|'> 8那KI((+êz*?DES}N;^: ~qOs{1BbƮ̿ܶĺ^)d{O>ǿ%*&FosaI>Nݝ]Dh8p郰!a5DpőmYqX#(8N*HH5,~c-VQXhZYNtOG ڄi *Eud䙲"o=Og>sq(K }Ù<ϟ3$$%.9ju 5 3sxGC5c"( "bqi wa9y8 +kjo #{?'!%6qaZ^QQQ@-Cv4}}E*jb_774wF^xD'fK'W2ǒ}50ŭ%~ǩ:89؂'B ,M=~t9J3|s]C5<i%A<Œa[B%zf&wu~tƍ@=yዲ>N[XٶQVSJLeV9N]utIVa՘ \;EXߍ#‰pgfh:̉fy;-݂*)Y0p]J:L U'ȺrVWa3R'y95MyfP#VPV +8mEQY{skw8}p6tq"r8")0%UĐ+5ed—ob'yGj8"KI]-~sIvAK}`juQP]Ov>8 LLv@hTT&F^M;tU%==W\![֔U£Ȑj`;"݃F~ oaTX+  Y$.ySb0BSw 1@IC6F-PT<3pY9C Q u.ޑÆVC+T(~T|W^LW<\kʸ;!7pM[=g?#wuL=QhOH[b"7O]milW+| j q | _8~tbY`QZlmV:HGHeEC~^(794Caziqa +OVE*+{) 'o ~'ؠ" Ln`aAQQ.̻M~(#g&8SP1Nr/"s;ZZ1KQg#HZ Li9p ](@Wob2IHU E]Cfa]0bXR 3.0bN1,'ǣ/)3c!MUuJ漸ލ8t?pLOۏ3oihLx?]RRWq r<"-l >(J51sqOaȕ=T%݈O;rȖ)Y-cEJd׏]Kʐ<&>^4őgu@"( "bqi wa9y8ܥdxҲu{^>hEL\Ht刊/+-'#!%*blBLBgE71qޞ= ;GdKjԖWV2FӉ2.ÎQz̔SWF'?ODMOjeSO35ʟᔝ$2H厶/-+AU[1F8f&S \aƥ&S )x@pg+WCwV+aŰȥf\aibXR 3.0bN1,'ece 1. eƥna~u bdn3֣`,r)bXZÌK+LmkS S06509IIyI)f\*q񷹳sxh+3UꪱcD(-I+)@F myҦcD0SȨf-"ȥf\ZT0¸)c=c @`GWc?gVY{B%J`Ym0=ZҲ,liTQ)nG*r D$Tz PW0UTlǫ,r)VJmmk;֣=N1Lp&SP+a Za;U2A LpÌK+B`0A Lp&SP+a Za;U2A LpÌK+B`0A Lp&SP+a Za;U2A LpÌK+B`0A Lpg +4 >(++.&~qlr%l0bX A1Lpg"c/(D+l0bXR 3.w50}8-v;<ồ+0}QĽ3Q"#%cf<BU &S k!( #T;.-%īm_}o/-n3TW Y2NV T6N1,r)VJ`;>Cw.c*`Ǐwu*uH446)''WY_2:zyނOknƚJՍ ==$%_\ ORc]PpwwLv9\ LpBP )F"aG fK6eyNi)nUT գ}w٪~&65+44`1gJ~ET`(&*Z\]ݍ]2K1G|ʸ)E.0ҊP)LpgP܇]JBbdcmCWrʖ[먪66&de&,Z^WG>ڿTGoԀ2|?yW^ߺ{Wuc>ԬgnMsv64:UR<3@-BU &S k!( #T;Dpq++ZBNZZKYeSJj$$9~ +0__z{̞=|.5/wOBrܞА %2waK1̸"T x@p''ɺ nEܹ˦LEï5'fgF$o U\]5fҁДo^|C^_仓jx@p{YmmZlƈĄQ7rEʕwa-0b*rX=n'V[,`w RjAUD.ʊ>߰{̙݂"M[8G1<>j+T6N1,r)VJ`;>C/) #3}OtLIzcaӜ\j_0%??T_|{_pEUlzݝT54 awfȝE\.7%/T{9cw#TJ`;ŰbN1B_RFAFvOlՁi*誫_ٺ"6s +|o/oɾ1ӻ.~'mx;$dep12>[V:+T6N1,r)VJ`;>TI~yY{dJ r9eXsvvuu9kTT^{+iή W0Qk W5})Mu;.3/_\ LpBP )F"?*&*bnu?K}w-e{ o-^^P^SVL"Ǔ~m &[55uvu~~xkGVo,ZVVSݿNup rvkH1'[OH'T6N1,r)VJ`;>]-!&p:xm}yؠs89 8\ LpBP )F"crRmAka<{XҊvz#cJJˍ@F m%:҉nziX1"ȥf\ZI*Baܾή֜wapp\j*E*exҲLp&S )Fd;a #Tw50VN7,- ) `;0bN1B% Lp\&3 waiVN1Lp& vn֕eա'w[zRveip߾g;^+VJ:hij}CٹY671) #Tw +7Sv( {CiÊ20bN1Lp?%$7zwǺ:[[]^FaUy-ـ+@d[+zq+qk~OpЕpgU(**x1qN_TVT'N1Lp &SPEÂ;{aO\\}Rt|ZJrȳ}C1LpwҨi]ZH/g늚brWqGy'eZa;0b>7LMO3d[UuV檚*Jm!G彂t 4xrnjmF]=<)1I5 ye.E9ۙYJHGviʇ/)3}buy$3.M[{𕎶ε->9+~C/)#6Q\BFHx+GX^Zz2 $Il* ) #Tw}Xp?qe7=,oJq)pq0$\A=*~Jz[DÔa;<,rE\^:۷J[]%ސk+TTGDSa<i wap<3}UOwOmemGZ0N~_ >W 9Ґ1p(/*.nmn[qέ83$wqnLwZa;l'l0b*r/Vjk(͚'+wZy~1t?뷒cҘ?K˴wa;0}8ܥey2vgGgSCFAV1uZ*=u :5k:̺zˋ*4SV0hPU^ͪk4tհMooݜeqFqyE9cOOqq~iq^NUYMAvDMe]S}3lba((J ;:Gi0bN1B% Lp!vIyfqyByǗήҪֶ5xV~q~جt|N~)6Sy"C1Lp!|Hr^ax]R <)’ʎήZlTSרߏTYU䡿,- ) x@p3oKnWd$֣`<{zzu KxS :z%Ec= ƳGQ^AA^AX.o-N1LpB=c= ƳG?.#l}Xz {ki.ri\bb{ H,{ck AA4 0_y3~o{;3}bɅprIp2n[XQDvhGx"Pm 1 Jg4ځ 2^CpcJhGx;[q\}F>ځ T~g@<~*?FNYm]C_ "1HЂ; 1HC ; 1H p; 1HC ; 1HC ; 1HC ; +HC ` -HC ; 1HC ; 7H ; >HC ; 1HC ; +H ; 1Hƃ;FvpW)]"ʳ(dJa"!O"M H&Ri# t5{ZMîgj!FGv"I`}7Ƥb1X\5/HC ; GۻzSU^g`0\[K:Xf*B b#w{Zs=΢)$JhǁRݭ14"B7^すt ! ։/>BqȀb]kS[dQ)xN|}-$! wXy݁gǦ$JY: TX.钍 ' wXAbp$ż٥j(xuaŢuy:]"z+=0iLĐNwJY՜5Ӯ_*nkރ;O^{`]7'.% ^FmO$! wAb(}<.I;{Ya?iAAq|֩]!g uP^!nQ>ìgYo,jXRPCmxgO!Iרup _Bw`2X]T"+DŽ x5;'<\LS:GpwsR+61xgxBNkmGGS7{AcD}dW88۩4B(t7uwܝC>^ܦTtu]-e=c2kgE"cï%FlwNP:ʫLf])X M$CI>tU Dbb2u8) +Nff&T`A; +%mټ6}<,,-5]ҞeśR H ; >RgǙ.m,xzf"pIn,}qxM]W2Sʸ8Xz-`c3VN9^gX=_.vwJ_ hzH6j)TTw#VcN)sp/+;kn̐d>Xl{gZC?tߙHw4:޸ UY\b|V޹>[N)eO$! wy23WOn+>M' /~W6ٻ}&5H^]wJ 4'΢8Twn<4I8y N)cK\|ݕ9I Yz{7W}fz2{0뷶{%͉yqfʱ8?pv{WیtB'̍8v}|DKx\OEy0;uل9$є2#n/-z)Iџ}kѬB;~H,xaZr|GwOIYU=jʚ7;ml,~~v]:gߑ3a傴7n̊w$ZSX,/ϯ:v`1/;`O }˩1 /=w!jb=CbFCvHNҌ؊5|!B䜾6cY;e>'sk۪JEqg+gi`m^T_zw9~Em^y(K3 Fh'L)eO$! w5V&o i\̴mLH0Âgݝ͢(Cߜ0A504*uڪ{G?VhqNIvyGS?޹LW )~c4uֿܸKw|t0r8y(g髳7uӔʼn?Gвתa^nsψ͵)K|}L%~}LJ匼I v}r,PBJV6 l~w/#.q-[]X^PUۘ$/R5Z~jRlB]_Ks'8lfK{+9s Iq5K?rŮZ4oLc07_^[U}oAjp~^ӖvpLャ\f#W˯,ImRLo4z- >cA ; 1H;ENL) (e S#*Di`VjW)4x;iZ0x~8l|KFq]Qj:{*]<;S+4gQi9 Š7V)'h:'wp03N!UjԺܳM5-S'垿*q7 WB^PPRqSump j|޲ª &yLpեE5=*HC ;Ľ +HC `BG$D%YkJ3_yp8bW`=9A;9b'M.J%;%6f  _f k61`d°R֔I^A3/6T482!q|8bp8?gqW̷߱`9G $! wAbw O3%FCߥ:cdkbuqݗ0Xseh(Aim馭aM?=V7ݮ4~UU-{şb}2涾sc}_$(~ons9Gq/ 3Zc=ubBs}Xp"ȴ2;Ȧ4h2^Ӂ; +8 i+˅%Yq)U%x Zok~nYpX.>VASi0`٦okyUUЂ}.hx; 1HC '_p$! wy/cYSȤP{d0LLxKs[gIYL|#1HC=݁; 1HC ; 1HC ;Če=HC ; +HC ``&tHF; JPtr\QiG;/V. Q É'jv#T׃㐩th2B9=юDpqon(J2d]Ί$ph;B4#'hֵq``,QtC$ d<:PN.x5`&VLd%EM(O*/AHDe?ځ4Y-EpqKU#w 2fA<tH5VU9:v#J#hQh2 1Ղ$! ,VD< pD E'JfwC-m:&hGx"xb| ZVN&\d 1qzX-A=f&StHZ%^?ځ <2K+wfNqs(F,P11`84QHC ; 1HC ; 1HC ; +HC ` -HC ; 1HC ; 7H ; >HC ; 1HC ; +H ; 1HЂ; 1HC ; 1H p; 1HC =)5'Уle:eVK}USs-C}_7W :`f-!L 7} L9 wAbpuyo€H&wc VӇlgD%G$CL^Ñ%H`ẙ\[N s5i^?bqg'w^3x;> Hutc$܇EӵM$MFcHўz{/6ֆ[V wAbp;H-ķわ3 :l0}C_3{}\EX;77^Hzd\>[rl&)47m3sW9wuwr/\_<\23Odݨ_W::[nU_+<`W6_ί^j($2LQWϼifYP{M:wR91VxdqWBƏoWѵ[u^\:coooN(t' ;Op#{x<9ki kM_ep۴TE":dz|?^мUi *wo<=az`WIa>d*9t^MYW ,3RQ'9 RyQ3i^Ym`ZBjݱ=g-u0;[?+.~zZpc $a^qxog^9m&Iҷ9:.4'JӹnIc<7 D&hϳ]3chok}?VkZF{*FOvm #ȗod_{{*-WUtLP@hhoF@}0cfk4eeه~ErӿH&y=pYoÛo<$! wA}0.q!>U`\vĩ1wlipMw2]SV_[ҟkwi}v7V}O/}zu'](TW|﾿&˞wFei]AS3#]9wmEy,n]}UShLP"wUT؋8؉;:Fu7MNKhklkK[4 ?kϦt7oQ`a')5Va%M}OgR;T:≜S+KjpG<HC 3|gCNzƖ1L7_sJbDx <1f  sO[Td4m?>qXv#py+nT]7('snٖ)azώ?hպUyBDN8|L'ܑp'7aX\бMYvƉ:nު5e xhso? YR}dpga> & lڊo?n :7)t;>`11itg$hTڂbg;GAҴC[OEUW6C[#>@Vw<so{//p.2DNMp#|tWL`Oϝ0tL$鍆{[^[҉sӖpٓ߬-_:u#>T F͆f`O?7w|ϧf'NaJHhBb#p(p/D2y?7`٠\]&H;;l݊o $Ѐ` %}<B}2vq̸`˧sRbnVֹy9:e{d{zoT* SPu7Woxg:wr/C uH@GRJYA yOȽY\P^k: =Z^y{7Vd^\xFRmmh0-yEy<|]o>*5EZ[{V)SZ, o <}4$ܑpfAE]Eӯ8W&l6\6uwDaٍ5-R,&9b^ջ[v+RrM}" ڽƠ~uuJI"! ` &q$zBإWȹSWv/n9 wX O^gL!sPSK@s&h ws>ȼ#; >C w*hLHF JYF@GxDNխ:.`̶bQ+,0ZP KgaĝϿzvZ]AScڛ;. WKA%թs&tall. rUxZ p w,09M('4kYRcju&g~iQ9%`"K` 8nwopG<HC @6qG&Ν`ӉdRCyCՒe/-iklgr8S'j[[/Rů,ijsN嘌<*0LlJBdИ+:[R==5-_\tz=78&WO]2O$ x]O{܄qA )e P;'Ag}$2q,Й= ?9+bW:M-i'[6y mG ;D1*bܴ>+g6 B {Ǻ Z{VID2&F;8<ΠPkQf asYoP)֟b jd I0YtVaUJ d*Z&QMc"FJҦ@SF;ɮ)'ށ5e3ih6Xqwh+R%юb`LkL{MfH9QHC Y2}5;Y(8\(' hQ;mюD FX k`RG;jKРYG Aգ?_nM-&?.!яSY{-o!x2GDŽ5t Yȁ; >HC ; 1HC ; +H ; 1HЂ; 1HC ; 1H p; 1HC ; 1HC ; 1HC ; +HC ` -HC ; 1HC ; 7H ; >NA~=WJjps]\,hlPEq^PX\="ELg4g4{z7uU&p$! w]%KE^.M57kF+08@}~^~5% e868޿<ڠ&SIeתLƇw1(u' aIA:!HH14{ w59J'_xSdLJصEC qvŝҡ!H w ^-˓ @}7뵲O.'; >ٷV{ޠ37wu m<֪}6O0c{O$nɳӷww1{E+%]L 1HC 3}57k)֙\8{WmGG+08@½?$ q_q7v6$B‹*yMagܴp6uj:i)zU{8z:L8;gʼnkX>?~?t9R*/F$aa{Op Fx\ >n>.^r?HC ` %\E\<kJ|`x"G- ghΞrd*9uA2w1*1pJqK}{d8伙_~5*)O+e H".yzE]'p)-]9 ,Ĵxgq w$a wAbw:Ocn]++M5d0_9 &GkrZlƄ9 dbŠ:z8k6F8(|B{wkW C(ߞ)VU  R[plUf$rWKг~ :MY<Ě.:ybqOi83ȵVs|=gʒDOcQ F$cl0dKE/^:|58/t| Iij  };>Cqq. e͹gn eVkmG8{Tvbjkj:*%L9rҜ^scY~URpvzEaU;'̈k s@=}G8HgVZ{@7ߑuڸX_TuPV?#ۑg>c#rpOLwZLrq!R,IoPɔܛoB %S0h^˭ 5>$ dv|ZJL!eU7-Mq4@L Te ֥9Fm,ܟBtXtwۢc@cqxLx|*v<4HOo6Q(0Abp$c('^NS$r\b' 2bι)s8|9 {K'rB8i A`H#+HC 3@=5i;3) [n_S~㊿ia.Trn>O6x-`Fsiş4=rR;pM yH..(tju+<|ҧ^_}S-yF_|7mxgyinE[}SW!UtVc`xUe%<{Nuq>+3tƤ.=gxr8- ;.  wX ܱX{Ͽo\U.\%LY5cc{[ܵrb%o}hp2;llox;{n<@& nJcq<1>-GuD_/mQzU:K֣*rVq]`XX#M&SOۿoo~|!-䝹tZGcN=};7_%1׬Sʸ{d/W/ &V-#ZjG{\|#Co:u7ed|*܋nU75{>x}'wJ,?=/ $-{՟I[Bڕ!{>˸26 ZՓGĢ2;Bazץq2dwwJ^_s!zU{{ɾөu+_[p ].pfڦwvٴĶs>ww`nyk]bbWKH-i5W)eF$a'?z yn9'΄iݒV]Ѭ3QB3wF ^ݺ9u/m_U7VāWnpeՌ[nmU>{od},0KokUbhG=}]UZ 28헦, |=N.HC `<6=1]:?yϾTι| zp}~}=g=|]tJ΁OpOϮ(ik QHi&ebi A'_[JS}\.Npby$ܑp$! w SN3%D1 *l27V4ެvvKMKgS;km^򲚛բ6Ǻy{k 3/g\YSRm6cC 2f$^=poL]E"äSeuv9;^,2hMX1ȿ$3|Y7 zc-/iLꚷe/sOeAw#'HE{;Lx飵G8+(ؔ+ϊvCږWZ;K>d*~ΆM5@/ܖfœy=dR]TVsEaGygyk omz ԟ4f۫]l#/炭">b +;w׷5T3>:%ݒ qr2*H&_UoPZ”ƚ겺Fr]x]SGkkwm5/˻a=|KU%X &.$-_/H[Җn6^s'Dh@}0~!ܩt N.uzLEy޺[?ԻS!Uzt΀0.MDBAoR+ &nII)Wuڲ@:UR$"YSm+Ͱyk[A_{G[T"!8VԴ6KDP"FteJk; 1HCNc4]TUT2>Stk % mmeuL.+867K{Kk]+ܮd4 *J&` o[';XV{Dž :VNejuqF!aa:$H%W S)QܺVj4"e, wAj)v?hp_Eahl;SBKs+\FXbFUԥy\["vL0%W+'zƠGy^, uK gKM;Ǵkrq%P[g"lRhiQ4-Z>~zk 2K(t2V%+NA^{RiXbЭ*p !ՠɳcǦFCx`U 8BxKUQ}ܴӅT%,)" _GzS`A߻]2a; ,㕥*k4:HztdvH*&$"G&ւJ*68qw,.x,N\Hۚ=|$"VSTµMF)zGϘ~2{t`T!.N~>B}d6g *=3 4FF_cqAyw wAbp_wq^a>Gwުm`9Y>vb`y#0Abp$!fpdIֆR!_θDJƲp{gfney~#׼YLߕmX}ylgDYrO>r VLdܽ&DAkmՖF_> 2@; M g;tyоD/g7r~Yqwv[L}qmuK{~ 2; 1HC )e_p$! wy\d, wAbp{tr %▮?:4ՋL":[-0 bX;._$LF< H ; >HC ; 1HC ; +h*&p$! wA}00!A-B 8UPv Ǐmaϫ"885;Wo2Xx,~!\ZkvǏ6ځ .Nmcc٠a2Y,&e z{-F~9p$3/nzlJ4E'JF5zBУcsc+gru-u@FoTd$ EcoSmVK@O!ӡv#<{BhG1BPpd2$G;@\C"1HЂ; 1HC ; 1H p; 1HC ; 1HC ; 1HC ; +HC ` -HC ; 1HC ; 7H ; >HC ; 1HC ; +H ; 1Hƃ;Nv*efXs0։/M=$TE*+DM&pزT2Aދ-BtI202 Y4, wAbpG8tu@$ywWSMmNNg,Z9 [] 0%';2G.La*xseJ{d^0/XD <͐>"<6$! wXw*̽q(dg[YP$S)~^~# >|O) wAbp oC*^Ͼk5ݕ<6 ׀hc[OdrI=v}|X9fu%{|Z_WUQ}Ήf~_}<`}<1<^p$! wydN"c&{yJ#c衱Aa{8VSÌSDsJUwwePdGi &U56vԖ7LtJD`594k׹p-A탄<g($FaqnϳAkwH F%R[$2Hjj3~ $! wA}0,c*Kr*\`w7UN_Z3ol Il ή}҂H";.z}hi $2qۇּHY4Xc/,3\Y_a\3kTq;} Wfl}?]8P6l` v.N7_ܦ OL0ߟ?#*8ޯiPdş?{ŵ6`v{c颂 ֘'7h^PQ"Jウ. lg?^D1Xٙ3|S̻g:7Þ(ȷVw4xp7UD wȓ w;ypwrp"WNe)*()wX!W6^^)["tS*ilr0s&蓍խ:G97ż2Yz  MUt8c$L((K$3Z)1?}z<&>pXxݯoh!˓K(acDe#~ښ]Cn8󅻅GШBүWMl<xW{zF^I>K󅻂|戫GiiڸZe_Oݸ+5Odu~FƮ /McT:ȧFepl`Vb/592U4U}#)}T/NAᾤ+&ͯ:fۛ^3>, #d ĎʦގM Y:I;Gz?(ze8kLO?~F_\+__{N~??,^R5ܗ\?no4hN9lnbf$84F;4?׉G:z9ہ>"ڮļHe  s򙿧uCpcs=(+(} k|nm{]YcJ(UE*}B%}M/7hjU<ypG0P#(QTZb3dek$`fVo}38*bs\c&}=O++Xj)X]I2\7+uODʓͷp6Seu2cޖ#*knȽ\2p-jy띧><>:9_~p7OK'ܶOt$ԁowRh4LhdvotyV 2w2ɿRjmCoF;I;3&mh  A[ڙެ ^ &ǙfWOe?7D|Ժ ǟl̹tcdpd厘FJRVdn{⋓6\:4iCMK(Dn 55[^GGuԂ~w^jp76tH;almlh^SX~KJM-rro6U6+)LԴU4umPԓP/)pG* 4|rǝ.gh*T)qX܏WNغiϽOcvn6fMA+s=Uw6Up{W{};ľm?^=99a{z_?/Ͻuv7Trw܉$Ws;+Gkr`t8?_B||{CnWj pc|csY [hvNӜo~釫')Ƿ֟PKRՌ3']K>I"ʚh}Cr!屟 9dDx4pG0P#(/ I:̭ H?;E^\ _OBiLI:ɝx.|7~v7p2sdp2[ވM=~ct`k`t@;-aG١{[uf89(LX ܷl=9^Qg 羽u" 7/ʯ؇Rn߶wRq_렼9>ueY>YSMǞꊀ'D:*~G><WBy@`pG0_+y_N,XQY&v=}"6vƐ|+Ƃ8>n-u]Wx66\]+ಹQ#RREֳ֮`o00w]1&y ܉ƠbM:6w(0ە ^*\]HHy?L@'CW^RN~چmm ]<s (Eo,+D- cک *| /DYD%D/'y\$ $; w@8yÓ?yAK##CHez&ō|l\z{b{-y)8IHl|3IWWSH[#9ōM2N}!3\#=-ayKZ9Εз"X|Fm@`pG0P/]H&%ep _GU[vCnG&۹+DL+N> XIɘqW w;Y UԔ$_/&ƙP.)J<.O J2I59}ǐ>^cpp2{͙YEã/#9 w@㱸-W+Qe-5mVYN'H)Meh P0FSnHdÀWs͜ FZ]IUr #y4H2_R,eᮩqLӞEFO(  .e \pG0P#(̃Y@`pG* ;a,elpG0P#(llȃ! $9"ܸl\ )BUmex"cy>g]*7 (#=DJJ!cgHGwFGEs3H74dxJeY*iO!_8޷QP#(9E_z(qX"n\D6.[d? H}YgV)u"O RٸK URK8D%O#9E:k|MkX*G2GpM%A`p_ -wCcphq ظ࿈} :ftTn DIR9rizR$mEgSBF+`^G ND 9{y@ XT~2~4Yghs/s[JJ#-=+wZVg>T>e@`p_?R$GZ'V"TBׅ`q2tft ] X451r"U@k`@(+/{=i3xwWQzoIN0T^.>Fr2 ıqqW8e e9>㯨Y9`+EH!'A;R@`w,V&r9-d>񯗷sԴ,S^.ޮZJvQ[wX9hGjV|{6kU=K̩{K7ŠgK,A$B_YZ)ǮIceQ(٠r_\6w~wvi6?}Rm}^2X6uk7_SPbn}gKB-ƠOYeypG0P#{w3<3==x6}VnsoB`pG0P/Ɵ!k锱҆ ]M]]rJd9lC/_^Y2=m@g=#B/<{=zkd޵iR󄻮N@kh3ߜbxvH/d[8[YZ:[ [lQz V_ ;H w;Y =MKas㇯Uhj[\q\wۚ8aefdc^Vz oS<_[믎8u9HWME|\'l&W}sc,eEL0ωAcDoQR+*ml sye ~156&qf Yŷgv|ArJ0kGʓC-kڗ7#csG K?%H]-ح 9_L_IM)B_Y^( HepǠ1ɑﶯ/)7A~ՔT:Yi e> sRdS3{w ߴ'9/sST\ZawTַ5 RqXg}koVW ϭ(\C`OWo'BD e>s? T6>@[E).||{ǾCOLN1VimY~-/@K÷u DR[_)im ׷/_+jj=\]:aidv.eq[.\::N7Gmlmk tA1`9Vn(PQTN&uw՛*ûb7ND#AbVt/_=X}/Sz{fv>~)W=# 1f~v{z@"C-CϵTtzEV6ǏJ>FmPVU&-# C6l꬯ki/7쩩qH\rW>顷_qΨP? PCA׎ځAeu%[7;pU{o;Y9Y:u9knV"1:na{o2_@飒Hrr#6/a M݃]Wwhח6lػNOMº~1鞖ЎN'r!D*mp䉯1P#( }fࣟ8E&f7K_{.X_(ٷ}maEysgojN1JX58<2Dصy&_,p䱰ZĖz<__\Y]3%=S=Phl%uzf>jQZ!{=?D8[N &LLK27YۻѢ8Fm~mk[m(ڎv)gtx,/ w@V*akAC)9*(j t2Fz~23qy.Κ;iy`Zm1*]SO6Hu3G#ښv7+˃\" , 0>pVOK/YP" |A}:@[1R w;Wp6F"HD19 h3wRFcãh \ iQhnxJjJz 2QtME)/ 1CES̋6@+k+X> + ;,ʱQ-=JF:z?''7r$r@7N('/JG5utԵ@X`nC;{VFfã4"AcZ#@fMHĒw뛴8Pg4ңO0| Ԕt Hc0f go4Q@’:&ʪAڰ>@K>9be^Rwx`rj+9KCn< fs9:z! ‚iA52QT(6wR&Y@^EelxHQM]]O>< #k۞F eb0.XZIP-28$kbӭkf.nٺʚZ^ZX,ӓ3,7ut 5Sֶjj4Z[-=5el0*HfNن?P#( qp1wA2Bl|7xзoRu%V@`pG0P# w?%.POzE%" w@+X[EQ2[Yi̬T%queUG [I26ȡ]dɠ ڛFc'ſKY?2:&z2}x|@`pG0P/t);@`pG0P#( H>Ix2}1a/>H w;}1pG,P#(  w;@T$;R@`p_ гN!Hg񣭧[bbi}A .#/igSB[G{xhYgy"XZ[6=, Pe50TNK =݁?Dl }Y<%Ni gxK|( >Hx  :>J`qYg䉀XKIiUT}։<%pd ojlܥQYX*MYRP\m |hgSSdcxJ`p4/`-;6u>,P#( w;@`pG0P#( H wd;R@`p_ (  w;@`pG0P#(ܑ H w;}1pG,P#(  w;@TpG6P#(  ŀ@`pG0P#(  w;R@TpG0P#(Y w,k5 QhlcBj\3I @`pG0P#? ބ,/Rq:"Te`148lj#P/!`q2&=!1ЗW죌Qࣅ D?`ZK>(H X,u:[z0sb#܍ u" wg; .'ճB%J.Gr۳f=Wbgdg<0BlSbyWnD<زn˹;Rp_g)UVD23+Х(  w#й2@U5}rS5W?yEɅ}ks,fzf6F8".LCQRUX%!$Cbla1TI-:8RWP[Xd]͗|\,PQY~]XlB 뵍% 6O[^Zz>{|։ (ܑʟzF*O4gJIY?@r`vczt9̙?5uG|S\yi.Q&@A P/Bkkbo61:PR^f`>> S頠cC饴մV)venło=B=epXGAE֍ )2WPZg)*{٣1K-KU$oؿ)\D+kxŠY w;ypqp}$2qllHclE"ZUQ8}RKOBAHma bs GA+,\-<.kfFu1]Udc,/Knhj7[& r:5$')v.frmuCM]0/pFVfaRbkɓȍ=VV&5͵]-ҞZfW[7Ob(+ͩ.QKր,ځfV,i &X.C=-uNFݥMVj 5͂ѻN` c{cˮ†[` G{YS?2.'PU2de1]_!kG@_{u0ܖ9ZmZ;cm`69TVP+֌pwKzAGE宾ōU`_ wƠ`auzyuqa1!|4 a\rC$*(ẙ@VQR51ͺμ@[:K#Tu=+gܙ)=sbމT66U7 ]}K'78hVxש\ٺ{jjtU`q&6v<.ªuYYAQ tx.]M Mр@`p_ֲҌb gѦJ덬S~fbkjbUS]]Z”5mʦ]V - 47گcV8kdq:-rkt{muѫ+w>o6&vfY2 wp?`SQjtQڣ<Ƿ4@`pG0:j^ Vk}@:ssx4z4FUC9ZbsS,XS ;h}C=(+|Ai鈵A7S=m1}QVEKGS,ܣ6pfe^Ǿ>c%VT74HڦZj;P()[W#_ykw04]*FJjcU+Y^V]KENAQNN̚`)*bɅ#$Ջs*f9<ok2nO3 2Vfh<.o^ 8ht"=ae~y:a: 8 )4YyC/_@MZso6~֎eyUqmxlؒJ,TFFF{I9.$'j:N7M9 9"}sӋ;d]Cƈ?_1;RY A˫|{خ9UE-nOmGhuSUPʯ-.иbԵvDeyE ~L__\rIN`OK_ hjs:ꁵG(4ZWM_xǁy\>o=:=.h4ͩΚxmc[j$Eǟ̺}HFZkg^\9Vەzz3s7kJ+:4Tɫ-k6Ag0'l?=+7^M5t\k_1z ?*Hd| ϴq461d^A`p7?rח5YS?)*x4JjCЊ?~ۛՔkĢўA`UL-{OAIIY[p1?c`tsu;ՕTsF'S'앬ҶJr&ֹ5`e7C)ݗ9z~dx[:y_9whU$W<,D['fbzJ0+>Gcq_ydc-=YJa֧^R-O"eJu4L1ݖ]1갑 S F%qm>J<#-#3:ϩ}Ԭs&v+wKRwSWH}si4+ST Pk kw绻ijeqA?> >:5ω$2)*aEwCKWQFuej@`pG0P#?R&tu}d n奣X̎~rjkGR4A+|AM9rsM(aagr6+ ‹jkwuܷK)i5o]څ,—ᩨ+>xy sD&.epDzo Clx\P~xup!w+Y4`j(ႀ/^ GInL:.uKU^ )*z⇹h~FZGWn [ʲa89"! j:w FD9ed5FT,K,˯JnT:zd&m̸|sMBԩ.{YڙL@uZ:O{?R;RW> Et醠hVZckg|l]t4j[XxFyޞm#]Qfls]"I!._^8"ϝO,.`s9*G:{'YA?_=mcdl=}.7Ij輰r?|GP3-J-~r2cΫ-7?>+6o_^bb)/+w$|/u}X8{Vs7RFˁ:%5Pv*񈤦J~ ߽:|?'%RMAI[E=𺫅=HN^O t򿳻l aߕlsbGZ6mchO>~DV<\vt><8lciM2(ظ͵%Ms틇dP#` }W} 㦐mNărI‡R]R~FmW˝iz{o~C?z5T_ZEYVm>Φ^=5]Qo';o[x8x BxO"zۚ[k~oaV[ٲ&o\`hab5J g__O$)d}ދLl=zۚLl.px|}YQMa w;}19sٜi8VӪmv2N@0%OďNٛYX^QPUdPSS.ޑ>)YhsIr$ ×pv^1܍ӚW ˣ{7Hp"k  w;K ?:bkۢN~+؅/F^WNSG(t ;㔳~a<.H"(+槗nxne҉{Wl g־>ڝ1ǿ=- OnH9%K&z,s>Ǹ@~+3go 9Jc#~qV{S]+N]NXfkZ4+rpVR-ϯb͈fB9i qY9(zn[ ܷ&B@ Hp310Cmß  øscd(c~(KY9PqAg_a} }@V߽*|4QT/3K\w\}Z;A,(]aӾ`Ԗ7s+P?FpG* ;pO/ tk)K9{93net:*ϛ'նXsu;||CЊ^7_d Do/&,_}4=1tp?vA_]S+#Ӿ~v:>, #-)d727+'\@͔ܵ=n^+Ή\fcx:I6@௬61|ޢƪ dc`dfÕ9q0Xx~A4FCID t\.p+?^=%jLεnX=Xuv6J4_9%_F:x烈/_[VeK6KV)W^8Ѧ[Y[.I x,dnqy_g&/Ͻ陟tlH~{ {@GEcϊ?prR(T~w}牯>Ohj5DPw^aՕ\H4+Xw% XCWz֚[m-_:p5wA{ۚ'F4ʜ,zqZ;@w w4maglc.˳J D%uze.#4J5]@޼rṆkhs؜Iܙ^JR٬(S+BU2s0gMS"zza&X6"ZQzӋLlMmb`3BW׸9e]]KqAVd}~܎*-#,+Ti C[]R:k' 踴4ffh`hdad[ڛY 掦p-xdV8{=cW{ `Yi*HdbGsS~F.}t(E,ʩ`h4HCH C jpT#1lEmpG* ;;aHgMtŽ_.`_Zͩ..kj[X4gV[C֭"[#!̙)hQC60M/ϓTtqf+L,).fM18'dVw.ĉ D1 +rpz6'Y_CTL`lHWw4*)l Yy>7ut)2 $/dVҝ:[CWϊDG.`eeSKs[Tz 2&9y9X * ?-@C{(bYQ[E'thʾ7zeu"Y~U >9^l_X_WakhR;?l{:vQ$<1Ŝb\)^ RQ'=~@g)䒅߭6L-9y?(e=ݔ~ɨ;41/̆rɀj q|\];!<6N /eٻ6uIQS:+ U 254_]_EH\[%HdgdL8|Cg^[3G2{Y 7=3-%eQ<õ (óBaTGo@mpnep8(%UuXTLQ]BamQ`wV=ii zE^կV.^# yE岜)愃 uX$@`p_/M}dbvRRSټ}=&;@`]olBQR͋GS'UCs%־䢇6(@RξZ;CKYFzXJʷj˛9W7QsH,>lARj^ȡ827u,X<+pG* &' FRN/VQt)x33uB|~D𱌋 m6&QʞqCfE~SVR:֟zܷK8x9.{ZSPy]ݝÃN~=-m"@`pG0P/cP#( psY,&! wrO2`g/UM*cV{'t>=L!D?, \/1]|.i@VPqpc*IP#( ;b@`pG0P#(  w@CR w;}1P#=%˒Ȳp"qg񣮥[e5Ի%yEu F[zwP{ILMMIrc]łT( Hﰰl}` ! $y̙ywv9vwSX #w C\dQ[U?Q@>ź>܁@{vpG1Dr;:u"T*Jh;Wo4#9;} ,eUJIG` s عNd!2O[b 2(Dwt(VDf"L2W( =}4E7X>Q]YH`ۭ2fpG1t |ʢ(b)X2U_mb@>3"4GHc E;9Q@-VwCDRbK"FHv4|L;Q@Or|+`P#(  w;@`pG*P#(ܑ  w@>Bɖd2QP9z 1FA.>"x`0 V@`pG0")tdFҒ@ƫDâ1(IT J+3qQwdRn4A6:e8 Ҫ;2VA DžN%?2&FrxDk1>MpG0P?O"?ݕ0h4B-;La2Zw wFǾC;DIbL{`ht0!JV;$"IMGq(ܑ @sl{ϴe1t׽<}L HPxx\nEfn`u}+z>E9l?zFלϾ MFa2mۖ(KD !,9aE0J{O֛o;Z'BYzzX<cGUO 2B'yal{hhQW_uKp&dW/ܳTw_~k2ut['3.VNyGc"\i/:! w7gL\f0Ma#1wZza{ (o|o6*;3=[TVmbP7W)Qrr( q!)Q_Ϣ x,|a>d"N%/yw,.^X6]2};L)I'gl(CV]L#NJDzSi]>YIɠPqXO|o3D}*ߧ3xbAbNS.k2_s;Ł v=L{S$3ؿ`{-Uq>I)L*mי"o>T@kD -_;x0- 8[ o?yp]9߫~;%V F#`qi34y x"IW 8"/qbq8U#Qjd4Fm14R.뽔B+R. < w@J$R'Vܩ%wiR7.x˜J)/n-ox+ʻUmifO3祃*l:u@15Й#O{BZ[׷{_W˜hTVO<?&ѕiAA )@ ]8+M[0/fƎ[_cf-͔IֶJzҜ4_|2<' w;yp0!RдԵܕ˰Q_p/|>6:)pJor홓@nv~9f0};+-,),S"G+*og`/G7͘bb477΄eǘjݯ|3.Q= hCsLxEn]- r>yWtؘ;5%7%?B9D%h1Y/̪lώA;=1mL=Va P{=%/9t% ڱSRJneY3]bOY0N ' w;yhKxm맋{,ys֎?[ߛYϾ8)j˧GX=ܽBܼB..IS~w$cA/sh_9yr}'KM%wXF rvg6?ˎ1uEOEx=Xnq㽽{ wvpmSU`pN)07<%у#eM5G5״Gzqă?h}h#4W`;]}X>Ks'ޭmdi^.{/oY̼ϛ4j.wVM-&Ӫ#wk5K+z}rD;ZzO{CV_(Hf~~;[X9w',ί3~[gg/K:iD$,Lےse^jcdw|s^mͤYZZ~)j D :ZX@'SYYfY 9cCll}~DRqϹQ^v6%,.* 賭Ԕ1$2%[+, >z~YTqXNu D7=D;:6&v#nU왔jO_ܧ>}a6K_xVuBJ[ ~{Kʂ v3?H߀h奻5U[t0//'sCzum's#N^/|qK%Wr٣:]&NKhfٗ?۹ ?<fLii( .N%=vw ܃Ӓtvv a۷cZh5/[qd0MhhxmnVna~MkgϿ>{O cZ:~JWrk_w b3V[;%`0-3y* W{dpG_Z*J /EŎUr1nm}RP(oTt=ۛ*;>@2A v.% <¦ ON2ƕkW۹O  w@%2hbJ'Zte(HOggfM̿q'RkWo?rՁD$T7,͝oîoDI;tՋ^=~Ci[rN&XhlX6-ÉmݎC2c Wˤ Ά;q"dV^GL`+aٓ>Q(~y͜E犮FU7{(çCnIQ/}ᄆn` {57-HҬƎ(u%Sg9wGА;z1:M1&TחY{?y_U/yGsjHd7zM¥s;@>x~)^ͿbSr%˚ypAUYӊb$ĕf.䥹³7Ye0[.S:D7upK6|%gGOt-֣ wҜwO?3ƀLlX_2 7/wnܝ5 DYrI wvwPTt&u:ퟆ p7 wpMaIQ½)s~y)d"N9ts]+E]-ZF殜f=vm jؾ3PC ( }Ғ4>Gxce9kW8nY_j[>ٷt@cQ+jw76;ʹͼh{]m,~c&(j㽜znSsnCOUxmOLYnfb Xi@&o4|OüRRaA$ w KkbpʢkU ^ JTY/^DY:Y;P8I&^N 4CG*$J }P FŅ,pmb۰V S$KoH-W4_m7~TFS5_t)ܜ-\O(*g3GŞRgA w3…E两3WoWGrpwać Қϯ NgI-WFp7pflzFky]ç_[2Ijuw J%Fv[CCZsnUSJQ^VZ_NP#ϗ8{zNKQ’YO,IQ(ԧ[~:C7Щ HDb_ dz,pcWb $D U>xEJ)J6)@p'pYRS(`e}fJZBpx8~b|`Hf(Cլ;$X3s?پw>lՇ+^ mnq?6uquּ#,_wBs"}1`#ɥnlw7(uFkڣW.Xt p5.3F_.}pOZw`W?Nzc2nHhMg| l|5bv{潽hx1 _)5Mǯ^*oTp ]>I*KUl}}SE -,:1X\eѵ+gLqk'XUupgxUϲwٓ^_BMcIaAiޥxMZo88]__V~zpXN)B#Ǥ;y)PĶOӨ-P#( 'd +K#FP"`@բx&*)*jz^ˉbdP* t7+"|2;=/>S( p@untzԷmAA:hdeR/}:~X.[1yF3+ʺv&K{ʵ1F b3 EI]՛ V"l~_|J--,{VOei$XxP+ewZ$<la.>(TPa)$}G- d0zK1_:[Wv=1򁇸!:ZS_2d44_W@L^kKU5<%R6S{SW9Zx$Ɛhd@`pG0Pğ;L$QiFg6O-޴X(5>iGcPJH揀;-Ģ' w;#m}1i!<}Oѭ5b5*]o!Ph,*PU?aa4ͲcT,GP#( Hp'f%v}*u24"`)Rvu`W#ӣRB#z]5Y01Њ4Ͳ2O ^7{t/}bױcZa1P&Qk vACceF=C>=mk')$⿘-J%QiZ`u9h0S;}_@`p )G;@`:  w;R?;#O!eOlpG0P#(rLpJD;kaDZ t+ \ rP#(2,9 HT\Dv./SVĈHF)])G,;)dE c:q՜bh3=;!BD-<::l=;ʺC((a]pG1D""w CVo㏔ wD@X&̚S?!cqd \Dv.x@F?["HNmp֍Q8FXF/+II:w C ߦ 3A2RƳi!s:H^,7 =n:8T7Rܚ"PxA:"L9}j:pG0P w;@`pG0P#(ܑ  w;@>P#(  w;@`pG*P#(ܑ  w@@XpG0P#(  w;@lpG*P#( ;b@`pG0P#(  w;@`pG0PS.)VսgNMŢѮ߽P"bqAޡ37s08LDjXmIylp$X iT2BVk",:Z_G4 ]bTF+ 4 uMS(=2[.WW? 2:Vׄhh2>G*ml^,;6. z m0>j#Y&%WR+`N"eqHBA/01:baK ez@GpёE|ei}-K{ .q>zݾ]\w&$N`΍3R++HW_AgᱥWʫkجM6 wPO(_,s>CLcf3Q!tj83!+*@%OL?.\`0=wynfZMC뭲V}t;OhgsEbfgg0s^jiQdI)lnX>gRGe}ӝښIchԕe86#9ƚI͜T}ڴ h4Ν N(PW˫Zq8ĉd2D$tx yܸ\ĉA--<\'>>H3g&^qpwtC A& AWv]ii U֢pw`,,-_b0ăN`oߪzZ}d|mlGExz|^]K3}V*ӻ*o4;jR"m K?[0F'Xl9:>)<҆em]*1!68\ӝTk쬨-*/_ߑ,D'u%%w /':yyYPE9q$2ɼUpnL8+d%oZtT['gV[x4W.;4/@&iЭO`0B.Q L 4ڙ}{V +RS]vcTخ}Ae0b1 tLWyi8 E;C3h,+Iɂ6B}M IH$H-ͥW x@A Pğ5%+=usΘv`q_uewE(K[7:X|Yo6:}O&Jz =sڕ6`QƄGTs9a w;wѠ K >=,13kj SNǥWԗ^--ܓ'%E ÓNOO d*&I^ȽX49kRjO7=?F%NH &;(j-y r8vʊ[?5fV\,/X>>̒2D25p'3_zTF3;ۧ mK=/h)$Yj5'iZ{7Ф싉xg4#F+s)pq ^ȹĩo9Gq:hL^"䊤BY߂@HlO_=;o^X*$?yBv֬s'ۼ_AN%삟w:C?36=jg v߸iOpjEM n;gO\}s UpGP~|ӦJV*Uj4:& ::ZMpХǀ>N.G\Y(̙FYYc`głxT7o>{oї_u1#i˖S2(E~BH$_xZcdJNys_y/-<-4<vcw;B;kN,Z_GGiƶic<3j#K Jŗoj\Bթ˗OD.(D h2 $[e/shK ~ܽid+i\}Ƶ}o_WHsl]W__0ϰ9kG[9u3/"‘Nl`G%:DR/>NL;+y̜s)PHꓖ,XnokmE24ڵSyDrs׼ ":FMy`>I8܁}J ǷmQiG6mXߏ7mDJ;4.nY[)P[AV:?xp? w4 T^ycc8k3'ӣc5zSFĉs/۷Y6n~_lGoynC:16 &XE,?(@ȓ;@`][ٱ%׏˘3ʤ %WK#r~;HЄ3S=;Į:(fo>e;KMoh joj/tۜU}8?^-I zhەՅ}D!NΚ½2Wnza;&)cpb3J.wɁہ3Wt )[l+DG޸}Z#Ny]g y1맋|½cbG49|{eǜ*njWs۱xlRfOC RA0#YϞ4&<[RmQ^UTQkވ#ܭ5Kg _uZ-> hk+-{u jo6!Mzyw@DLpҒYMT2ic`=6jT?rjyy6E)»,u͚խD"nγ)!>wY߱elTU0=uڟ1AUʄ6$J"󋋊*ʗMbSSM%MKV^fF2P#( =86/ܷfeuiʹS\BY)X﬋Bp8ss_}ԵH>1s7cD?_1UǮy&GO5 Jr{;mELfش5pJAi!C͈h4~i\Z9/3/ZbtyHs P/:rճ&*qߴ-rmg.=pw~~6mF#mlFVWqܮlz=kŊ EEѣB{ w6I$Z[Q6mj͙jii rkiӣD"d vݾv2paJmHwĤTg}mKsy]m9i)otAj]`W0Y//ZZRUqW{U~ѵȓR‰S߸F!kޙrqL(6;PCqރP*trnqVU͍.v&n;I $ yyc O n m};ܡqXl|`0+]l4ü},VesS]kVw;@`w+e#Jbc zC2k:}{S% ZjhU T$1<\" 6B)f.(d4]𰶷)ܘ֌ƪ&"{XJւ JPߝa4pڛ Bp_@k[~{ƤIxR"QrEN^bh4kT O*RL!Uqj9?W!Q䪶.8 8L0Z{b) GZSe3Ȑe˴>gya~ \m6Nshvv6w4W8Z#V>ӖR B}L^Z1!NU6xzYuM]5;uB*[fd q̓({s3.I]2Sv౶uuWUwj8^ W)DdENXzwnj0L.LT9 Zb(*/ePiN(KTeCX}Gpw67F߆n]ӢǷG ]\RU{gPpk]-ΊmQ@>,G$P! Itk!H "*R.絶ܛ* /HlrG@°y,vL"^y] ++ElOocRQLGɔfL*>8]ݜ@V#Ʋ+-1g;1@>4P#( px &Sg ɲeGG9 m.P!HQi.^5(/.2)c猶nKvP#(  w;@`pG*+ w;@>!Aa-Cip"oL6܁@>NNE*>~^2dqtq43WkYX1(dprqm (-WYPL$v" gk+w"t:9H('gpG1D42 0Q K9-#@&IA/@>s',ʞf-< ; E*2 V( KGʯ*ګИV[:n"HLm t_iH`xF9Q \K+(p|tb PPHi].I( ѫLRU6Q P#( w;@`pG0P#( H wd;R@`p(  w;@`pG0P#(ܑ H w;} pG,P#(  w;@TpG6P#(  @`pG0P#(  w;R@TpG0P#(H"ػ)dJ^|d0^o( Jė-pxLtyl'[ 'Ê)H w; p[5*V=Ƣј4T!߿ w{o,--mly}h[3hj=qxB y_x{k&8(OѮ8C"h5*P 0-&4..6"9o(ܩ42*L[{k>WW3oHr(  xpG,ֆv\Yp p̢JZn<,H7Op]zdh cwxP?bVSf]1 (  w3}¢I5%յ%5gz[ W` '&,nqiRƌKJf$ w7jtL̲`mɤ+'UU  O<硫#_[ZZrJN+(wڴ`E'N$1֖߮wahmM8!24˯rGm=$¯V-}DӢ;=s$g}!>>5JAq zWV\;w w@D!y UJlFluBB#{2m4:Ȯi71 w[kX imE,mymNc{ggӚ{%(ܡpG*P#(L.>$*Y&x5pݱObqupD|km8Ich4J"vpmlخ"f[D޺p$1(h]܂Bz ӆ " rӁĐtVko4yS鬆2# ,.uKsFCmy1CЕ%׍F#H2n&Xj0*J|"&{ލ*⎣^qpҨ  w3{PHlDt:-:%b7B[& x䆺 ˫*B{y3lY,p s lVjj۽̦&^s3Ζ!(XʪVH'sOO/l%xc3[&!M]=H$A!#h! Q w3V$c`цL,AX{}6$܉$_MINEgK/[8y٧@Xڴ>W˱gwo2A%7Vnc >kv-j1( ī3˜.ڴ+M, oW | xN^O\#i#|j\|. wXCXCXC'OKOsdhTH'#9QD3' BΏT,!j&@# v.z;R~ghnnN^A c*I%n~T]KF_L7{bX$:74g|,淨 Q`uI_XCy.܃BKoR;]+08f7I%Șh4=>oh4m~WWG%;vRiOJIJX,]ܿ$|qD@ rs5on奤LSQA&eWkiPH;;9+ c=}EE}eqGY㋗.Lm𑕝iei`?32.Ҩ{zVA^Vvf>~MՠtB<~H(^&.^vf0'-TVS &F\_橘} Jb=#Sefc"sJm{;>,\u8r<^WscuSf_WKSƭue8<(O ,ٲե>6֋䔖DEHSSW^Yr s~.լ {Xo`&X, E:RFC=8qIOm]7G^A^,ճ ?|WgkD"54߇AUcR^-.U}+,&0;@x;_8e`j3f{:4rkS܊(KJ l;Z{W[xP:,9y(71պ^n|DmES{KϽg7>!r/D_|ٞv`d/]N:fCSËyiC◁[y7&'" s0O8!C['|>+][#0X"ٺ'˝.w~?` k$vXlhnyp0p0pg Wl( ~{6wfp r[z4mENd&Y?u[쀃Ʌ3b@Qkv%de 8z:yڞ]&&ھ6NΒOhkkghOZ[o ŤP(fﲟ2C/n_yw}A4=</nsb LlYHC &] }L@)P(71Sc]^\qۛ%ܫZۚw^{:1m|rFWfТIc݈~yp_OL*+vr/7lY+ *m]74B-|tbߏ1.HZC BΨD# bGC7 ձxқ3Xe'D(m,߰noܿ5(i K_Rr3}9:<;;lR.*ow @Aکl3ؔq1]Hgx?NL!& "|`sz&R92~A}uk_ XzK,S"D7t 2.v E" HbdXVxpƻHJK(Jn{aB!tt|ehrRXkKIG&<{7O"1=vRэ^Ti'<}=$h)KBL.G9(4J&Ͷ.|nc4XQ/~L2 % @)wj\P10TYs`a`a`afw%kY*GP!5ԬiE`,p0u׹3Aܮ' Ԥ`TzP϶?.YG.b襚yC[S5{%7EU7ۛbVh.v,G1R韇 L*;{?R.`A_scxT XعƬکn5[m+8{if> G@FuNTܾ&+ J{h];uy ʡQ0D"ZQ_ :TF&]i , rϋT7gmo^a ,Q23"9Sh[bՔ-H2y@ǯ{N(߱i|܍ 5a1V%ŏmA*hfs,׼fƙQ,כ|M<:H$%w{\&M% I0S-q1uf9 NserUB|JEl>Ā;;l< w.p0p0p00Pa> Ci% U7kfbf`;4i#;h󢣘#D"Hc@T>|x: w PO8Bpr!Oy)m UH(񢣀y.P$( HO$S4_t sF/'waO/_uUϗx|O0_0w{Q -(k=/:9K DBk~o70O DɅ$R<!hHtGihsKFyLnChP5_t sm7ߠScrV]tB(֨XSLA~сWl(ɟ7wnMP7/,O(ʮ/c}6`Y`a`a`a`a`a`a`U`m`U`a`a`>p,p0p0p0p0p0p0p*p6p*p0p0p XCXCXCXCXCXCXCXCXCXCXCXCX,! ,! ,! ,! ,! ,! ,! ,ܡ ,ܡ ,ܡ ,! ,! ,g=lUE27ʮUu6Ζ~+Dzcp#+}k32h"px̉/GUأo=n[nivu&o>8k ucx=Q;ԌQWSɔw:[Y2Wnxe,Ck@|L:[5v ^2FY)=](TBPoqu4 R: RփwNbr*[P;K%Rbr}GhQjT*M!LS+Cy][ Be{ ՂD"TW/FMi9PMwwǤ6F䷯; 򙜼sQah_oGpw\Ϲ5}-E*ec̞Zbkf w[{//.ܡx8n%uupi0@(9b/E;nI BcFGT,7#MkWb[w>잍 w QY*tB{T揊xA%tӆřY%Cu+/ko($rʪfլ2[ ZDɠl\3Yut*Z+,1?*?0;0B%d +:zcݜ3υ;P&w|˿heҩq6\\NR;?QtMm赛rj9? p0p0p wM=uiݬ˥JУtws !x"𰡅җ\!J3w"kZvk[[NR\Z HzOtyFf ap.Ópj?`pUK! 7}\*1tiH!ܷo?uD @&H}t=XJ{Y0-ˮv5A4=-\{g{ɥ-v1gu5iS X<`a`a, MWI8751' Oe@OXYptʰozm;]9^Zv 2^T]Z ‚2--ۃbxzVA =I={-p#SHR|R=,l*pBL"}q8 kMխkv.mЁr:{9}+,š%P4/dGh4zi^eO{ҍt9~TֆWW@x"QTL. dత&r?|&'q6p3 [M%uu_e@"&z)%]1^$ƦbQh4df,HrP0^yi9hs1F *a.L&ݬL)k%+-jm뭮iwlW w"qbpQk#/,!|B0ޤcY$@$=pAB!m/{ͯAAfH$W1ݹ#5ute2:jsnb_;V`1ؓǿ&k^z}*XF޲3Tct`ߧs&AWVH24u41]SH2n5h2(F51ɫDXPQ~I#hbr|s[Hf'Ĥt~U+B: 5U5-`V]]mYBCm2p&kFTך[zX{kp'p+P(Dhbkdztc^o"wWZԼU-+օji:ϟ11]>J#kۯg=ixnggf0P4VՕl{1U RzFh &Lo`n1/cOcd%ǡij=3{aXqrR-1']K,Q e&Vji6y"sYXCXCXCpԣ|-Oy+ՊB@'<#9wf)X #);|2Cu&6Iߟy%^vcOlx[:xۜʌ wgK'!#DUScAﵔ|C@tlѪ7b ܚ!Uqv6Î_D n/Ψxoˇ>?1L}''&w|3 2~'|gΪK'ӃǾ> TWpyvAn~/ۚ:䖃h''" 21Z塭mU-ӅfUn6mmEf> wos[L#o|L臟8󧓊ɍ[?N9cl¶&D 7cC326vK~DK7]-! 3OgϙpG"o`gHzD7/-kpw12ξqWW|mb|LB1y9՗ed 3MutEe2ǷlY|'f`P[Aup0C"Wҋmq)cCoo䓙ӅmPK^A8FnŌ<:eAG~'~4N^6ӹ`ZW{췙=|k6)ځNBOVF\}q%^ta:7jO_e)%4.HHLv/ʽzsK^v4~Vꖮm4VeW?=>wͭW>7X<6vu<3=3vt3TBެټ!!2h╱q:Iix۞89n pDZPXze8D4==͕BoruILJ rC" &z%pЖ?m ?'dȠS@`\ ,! ,ܧ wwϐP}g[jj[v|z{y<_@ɤ=#&=CCV] z[*n /R9̺wv,LxH}}MkK#7W뜼o(fFpƮq^A(,31e0(UխS^-KGﻡU+B::TDԴYԼխ ljuQ{20Z䖤vS @gw6096:c+O#p͗.mƈ*w w=Fdf2;H27%ȗo_|]yg1KRJ1m{ rPT\&eѵtlUCʤ</M@v t56m?-/ȉ]/{2`a`a`>)UC)ܯUXڸYY#+_|}+&xBOO{)~{Ĺ;}lh8C9{pWWW_+ O5U5WvzX K¨ Mm .㶅w7Y8$p㮇/b<:1{GtMTCT+k_m9q[[9^ҟs&Hk699Hpwe >2ٳs;] A&$m5]}l\d,Nӓ w w w`amw~^_a?HQnla8=XMwĒd qg捻kR_d^zpU6i}͸5}]zaɔp7vp'L&֙# K'3}z` 90Rb{ 6QtߤcUTe~ *ɭX{[/ξTeWWF5Ӈ/EΞvCi0&ƺ'NeG댌p֭o R4iE M]~} yj_ظ9؛55wߪhޱ5sN@"gSsv@rZ!H/XAѿjE( GfѓG\^nipR5/wp`N3g> ($ Wl~_;C[7ac{ZepB@k֔v657E5WWd᮫_~sF\c#ȗ> fdh`9O$y2}6fvF&V]I3ԠƙB'ORD!F/0BH.ȕӉy_urLeG]ˀe W@I$M2:4N |D(N~julAIr]@XklᒩD;Q!mCM DBxBOqggnc q3 RDHEbHBסrFA`r5av)u6d̾QؐjXq9CV>}8yBahbyFfpQ]MNN'D4A`;LiQAepn`z:|@T;5)A |h6ƚVMIarTHx\!p:Z8$Vn]mTT wDk:ET\pd = tmX<2=F |XCM ap x\JguYLےZ:$ P_w@DL*heN w w ٸKk0T屰Vfo A#YhB^1ZQޖ~s;;;!m,uHO(DS=49cjecSj݃=d<Àwl{pRb+K)ITW؁>#`ap2XΚH fwNfe:n.V Ƹ49B^o߃0C;XZKSڝ~ZmUoE[PMxbTGc2`> '{}]E<x0p0p0p )Cl ꓓJcz~~<.$2H&' y8ׁo[o#@`/:9€Bp'&(KxсzCsڬe&{HQ"TGz;<ޠB>ęWrDi/:9zdsG6 !ɇK wH3B}c&)WÀy $\HN.5~ X5Pm UF!EG1Gv0֋degE=]=}ыdy3^|Myjዎbbr1D !E2G6EK_y&w(Bb@`0h\0/޽ǝHU񢣘;`aUX+ w w0Pf^XCX@yU&?p,p0p0p0p0p0p0p*p6J2̫2;d;;;;;;;T;WrP0a^ɏŃ5g[sstulj$x#o?p0O@O ܗ**w[V,ɫa2"jO?_֢\I/{|`aW'lz}lapl|J52 =' Y w w3!C5+_^h,ӞIlj!Nx~{OĻ/pSWWu9 >-CmFSVlnlV w#K# {3P\W!K54m\͕-}*nfg&J; YM ׉  wXCXCXC@ؘTU-"qCcT2)FL.˯p- =lI:}ެ04э]8 f_ITd Yq/Dsؼ+U 2(9sTWG$EnZŠC>yIC#G ,%ɖ`0hpghhzpή.~'eWq@ۭ A6twGxh@ENu0i7yB#m-/&jt2w{UNfzcU42`~`Ơ=(t TZ]-=V:q^mYhyfw_G"(K@ iPhTYn`1u#lcKC ;3@TQX%K-'&VFC}c,O'L[ND$iH$[%7vX,^K[dhkcGjit-{GπE챑ښxA4G_G=-A\]&CC*D +]I|lD\csoMMgXȪML6F]-pxl((462lpq~Mmg'zW wO+3S]&]X`hX[0@l*_UzVNuLXQBm\ #xaE~n(aUMUU8 ;V)epَ=X<6rMDzՄq|c/_N wmɶ}c=u$ p;+ xlB.jV- F‒k%^]M](VN_p;T;;!ܽ}]>~?XWA?}vMTDqmD2Own=}J&dKL%goL:tkWlm5/OH3C6m(?ۦ ]~5j,6DW'zsTWO,uե ??a2}w~E Mnd}zw@K+Ϊ#A0VRϏp`>Zc`P*9Y#_ecdH8}#@Nl>a_ Alnthf!"ĵ0OUHb7+uvͦ58 T7fV)Bjk_ ×&qp70Q5asX$14J9pn+_ՠkd]%6/#7.'l07k٣PBiԕ58c_XCXφ_n [ikqN>GĒcc|Gol#op`ss_,Q(oSʙ<SW߭fbm[Pfi]PXyc}}}l 4srkVe A#QX/Nӧ/Y82mmJl o-^OhhUXKKMǎ_DxDJކ!@$-} e.l,-kGp03{--  s|y$ұf+We=-흞m;r.^0s8]M~e7nڂ;dyeAoxTf,,! ,!̼*q[bkj[z|}lWSWP6mdixrwH]q|O2D{􆨫uH!J)ܭ]$ɄH;~5ruxISY '2<z`ooάRF fpפR7.()zS, _aeu˞';I:ciɯ H}{ת~& L׶3MX'T&#o8CȄwkČ޾O6?221htFiYekۚE {_OfyoΦv ; ~146&$zOʙҬ n{劂[:tچ~w*Fӷ'RhdҿwHʺek鳪!eʛ[{}A`9_uu%Nf"nTT]Y~D^#"¢VH*ӏ>>i%KOKbc|㼂{;8nc~ry(FNK; &'Ֆ$ X85򮘋GglHJc&?Xw?hG>=$NͺYZ[Lɋ^flmnjqu1?rZp]9c[p03{2LJm`m|=XS~Q~L )cdjc;dz,b*_Z\9C\!##musp0p0L~,"mK?16UUR40&8F16& qPlPVJ,aU`a`aBx㰘9Ӆ{zAQtau L6;z?_KVD<>uk]N 8}Dv6GnL142*W(6Dlۑ;S/.X#FZ7*,&ERIys˫K|]qm,6{]xk?G {yx\,,jhޚ{eCDؤbtnD:ѽc5).\ƲG^12֡l̬/O{zEfQy8 f7~N0PIMYD<3̜˝}pv imkqr+*)`a҅8:<(!7K/w5S=*RYtKl {sTvB nioRQ,HY]GfcmJom^6MZDEz5-3 ήuk0mkDOkX$.r X[_5%wZZY?BK?mGeD.euJ[X[r.˦m}:JQk֪*b]/Ws$My:E* n½/2u\gأ#h f{dInZu%AѱWYXCXCyU&?@WM=[ ##t-@`cE]zJbBJR>\2enondgVΖmrױ"Obr p,26BrXkcb 檖A J*ble\WZo`nhaו։& +h,o/ME80ǻAh4woG{LWY5X[9YY08܊nF:ڝNcgѡ\(U׌q8 ?/N*9W;:VhWzjj6&_$WBQFi?2 s* @\~."7 >%p6KSɥI5 Q.wB$62YWoegJr .6FCc}Lfu{ǚE,spd[5B7gs -A.NڭrP499)\wG?zXPEX!v51=/K彗ޫ% b/(X@zr` W?|fgggvweU(&`^3ݽ;ۺͬLsʲr=XE]d*yZ\IӋT^d\< b$qY}e]KAf:[.LEo&Q 5֔ՖO-Abp 2be%*oNfF Jc88=Υ^3pRIx\fDvP"7,e2E}plns3ꚖҲFT^S6cS]];F=E/knVO/wilP`jRqfטA=Uc|ܘa ~log&JS28y66&8,F2$!FO/ٴEUvv~:1¸ʚRoq\|–{wCc2m]2yR;77+;{LV-OYIvֶ8+s wAb sꖫknK}|7:A1Qi@# HC ;Č*aQVF'/ rp-g,bY%cd X`$8'zpL%o}=xz1^pCkbwqyf%đoG,7 hsp Z[,/lXXQ_q@O ֮[.,=pgmWEH47u$_|W; 5*HOW)AQǿZW ,-w G?*Hyb?HC cP ' 1HC ;p׃H&:1ٌֆ6Sk,kjoih{*M$!Ɛ;p$!Ơ whAbp$! wAbp$aEp=7HC cP ijhb > dƅиD3A<},貅G7.$1Ph0 ?$f SV*`J`8ե@&{5L{TbPVFaE!h4 A, Ġ`554LuA&W:ʫa w1{l ߠOÑpq4. "V:].$Q\DS̨S]ITc)RLU2Pp'1q ̈́n(O.+5g5P1ոL2)iK1I`,]#_-RLHCAݓ'Ђ; 1HC ; 1HC !Ơ wXA uOHC ; 1HC ; 1(j\$a w4=yB -HC ; 1HC ;Ġb qp$`P ; 1HC ; 1HC 1EVpG@Aݓ'T w,K{ZT#QO6?up$! wp= D `02$=*1rٓ0TE&g)T2Dʤqbr;HᱲEIHO'UW iWD0h DsCvɠ4PPLމI:6h,nM4wNfpb%pjq&k"6FhaF Jx8̔ w0G<$rvtWoǶǙZvSz^fw/:#_dnZx/9cB57߸o;֟ևȥ[ wFgTudyK@'TaAHC cP /ܩt*O$^qH&IDH`32\& l>[!tkk4ȥHDiB75lT*+K>B :Yݑ`"ž ]m]F_x\B%l>O ; 1c wh\Ƚ_%8Ntuaa0v\I\{W<-^VL6kutd JTҩVAt=|כswYԂ#p}2p84&U"ꣳ כ)OpXPSɀJҥT8ǧ@]o6V~s'r3@4hw'9mp8 I:3~oS$ gb1Ҵ7 jp3iCWהn7Um~ew:8ј;`0l.evu d]h^VHC 1"giPN:@'Wrw( qG|mlCbb&H퍍*Bs{E"lKcViA2:v wAb tD`1a) el>ˢF×ou4 ޒ~L erL~հp"`ٽgo>OGoG/  ^:9ӘYY\p/ *$! wAbӃgϟ'miuެ`?1a{@">FakHhkL~WtgԒ_]ZA\tG[ϱ8̥j,lT(7GVyMw;ɱDKOIb$  ڞȕ1 S =XyayVչ=A_$rsslnq~9nsN~߾WO_d&nh'>==|/x>)ɒ^Q/ةJ1HTHLl+hE)8,ZxkeYfGn -/%)My ʛhlz@y$%-nR43*ˬcvݒWZkWؑp瘰cZS_#prz]iy͋Rfngzq-1{:{_xV4殘1>B(#Ź m6lc wpG66Ԕקql\m~'gb,a$qߞ~Hcfr.Aj Z*yUmji2*0zX+eنV k#2E.w9j8yχp7(pCA5pYCMp…fEFx_"s>iΒ%mm׮vݛoU,,n߮.,XZK{; ّ^sk}|ɒHC cP p7sgE~9E!٥ ށ>E\K.J/ [`3kLLZZLzHнo;S޴ Iuu!XW9I dsWl_y wu%yi9X,\U:HC ;ꧯO{>]y'x]lqn殺!ޜg,p^[ejcZhvkk򏗚kZWnq&mJ)tLzg~wB ^d&̯)ˬ?;_c Wm[|L*gKAfIeq?m9W 8i.ߧ9ƌe8}{`L# l]02JO 6]4շLFݠ@bP1ոz}ӹ" -mӟy* ??o#=~=fͶtv64x˥Xҡ +w45:Go;=yB<$==MwFz:[;G2،;is9yiXw*b$HC ;*F7/۰(rFȒ9{> ZOTdƈ/M-Kښ;\\1[.SXݒ F wGgim|&}7s6tҺ5 >{?:%B}rT/ w+'._=W/0&& w<qMWPq9kkљsm9ڶ>޾/2EөLJʩ4+_XVPUQXє21a6R^d# f_vqY?M=m_9OfXQrbFtw )t7:~svd$Ũg-VWZ:9; 1uO w MkiL_:B\:rq)]]Qjq_ NuuTUOefxr% h+ O\!l^=}.h\g?ʆkWC׆Xٓt$! wAb+Ewwe)Z3(ܓ$o~j̚gD<78C\3mu`1fphٱ\"er{Z;zlG[Wʅ;!buȀjƹK,8l@lOrEzE}W=zVOM$'|wgE/ w y<,+lHܝ Z )|<>V[ژT$;=yB4hU*j`/EuidTJRVa34F72ZZ6qX,vQ)2u0};GSTl}71+ģ@bp$!FOGo`1`tΦJ>~(Tq` !t+kZ2n@=}1uY\7F'xL}`JN>~n.nƌKzdzFH O~*dڋ"(4r,4 \{`}@/׍72(^>hhETlط` IוD;NSxO'H~%r#!@JTF R mюG/?p9W`sCeW>jDgТD2 ^ s $l yN'.s2jr``;tV:u 1HC !ƠWOǾV-~שhnƁQ u4&SX ӭ}~EP =F` vVDJ*&eہ; 1uO'c{F"(!&$! wAb;31??"durbB(Dr)zʠҘTݲL*tI֎~' w%3غ#MGSԕ7AO?%>7} /okǂ; 1(j\=3wg6?m GR*ߩz` 1HCAݓ'SHC ;2Z) o5P1ոcTb dQx/b"șRLHCAݓ'Ђ; 1HC ; 1HC !Ơ wXA uOHC ; 1HC ; 1(j\$a w4=yB -HC ; 1HC ;Ġb qp$`P ; 1HC ; 1HC 1EVpG@Aݓ'ē 0[GAeI,q@ kkpMCg^s3o揿 ~t}H[?h<'o(I颎QhNԌg*dڣ^n5A~y'gƟ- o%p1KYKGS+[Έ=N3+̯yFpCA5HfESigol~✱ V547@~y.eca]PQ >*HC cP x#F*\m}x:ؕL!9yejqǛ~8\֚g8 R0<7ᴴ_&)t+AbFl@5k4 FV=20XPQju 0 GW'>Gw,cUEOA>$! wAb+"8RJvҬ-`oK:;P6u1n_QNY%);߾{HF۷a1C=Ѱn4,fnk^W`%%gż?h8:Ѱ{p``]+%nnhKXW80/XMcPWnJ>y|3h[^^Ϳ<=< \hHfb(;gWR>VYԘW}rd(뗃uX_#j>g7hq!m76jQB,%3ux8, x)M5 1HCAݓ'Xܻ/MI+CRW8g.x9x:rS0 %ϔkb LꊍQfmM{72X!pY ƫnpcjoih+)˹_8?":XݿUQTbCF%Av.6=" ǘ؎e jLpo[ 42Cwel }~W(zc$2E_TŬy~3rCɦ<+; Թam2eKJ*b|9ɢ'_Cd 1HC=rcȌJJyӄe.7״%B ּe W*_bo/(W̺זqM9>ّd}r18\t6-zG,ά'>jig&iʖ"N|}F|>}0j-(3%5j77`Й400.8bZBGnɹW#=E=N^ ૜W]4 t&(g:6f}fo]Ueu7,wYH$> 1 GJ1ۗ9fS TntF( K(bsYZ^b>8S b/;u<6f9ssf{Μںud-ܝ}n 2]݉?^qԝ\0ƤZ;[֖4$lh0 ;Tx?^O\J|zSu d {* t5*0<Ϸ=״H&.޼?Y20\l>e"H魕9x__O/X c'.߾Ƥ(4P+,ɪ iAKrZlݬl vۉu=0jE`UtY<+[L $;Ġb q%~>IWO7v?XTzapՀէ= |TumF+SȎ$]N_FQ\qf. :ӅE+몚- fN>'d/{NwurK:z/sf5յ%Kc_<턭f@e"x2p$!O/ٲ+[^iLxgE[*˷3tHhbRn ˸Rpdkhwg ;3 ;_`3zE} bX9nG>=R۾kR;D,8eҳYwxjWC-mB:Twut;p6"D =H"U.rML/JϏyPAWo{tw4 jg^[wԍ w<晥)I + Jt+;s1ҢqNҹoeG I:vyyZ0ځ䴆tS M~fL*=37c]9qᇿ={>sqO:>1v}fš5-?OΛ w#)=]Eञp7a8Ws~7U_9 wP_+3PISuknjߒ F?=3mW[0fKl䌞p֪wk[o9o) jڶE=Ce/gb!d?- :[7Pdzc_&f~cu"aϥ/D+C}@bP1ո#p߶|㽼 c{+!K>:z 1NRNs󵷴e 5ܜgT*Ytugz!ȝ. Xhab~01Ř^saqD/Xz#Vimpgr3 2O|HC cP 1pwuq\<Ĭ <Ľi2 aOx"+JrBa- ׇp"W-^nz$xFPZÖͫnȺ5֥{ћsls.4M֝tJRff/ό;#~? K76ieH78̌ܕ"A_,8⪽LqVmY\[pf{iN]ZP5t7fSԘqL EņKr̒l fё1Rj*R/W9`93-'D54x;%nyy-8j*2gN0p ̸bCľώڻڸ?є2c ۅ{郭G??6 y)e\غ`#sx?~W}~tr |FВx,:i_sɏՍ[k¢ * Cp.!s++ dp߱rkZW7pҩ'>X$! w1{K3Xt/t]UD 9[eU p]>ğeEsފ ƫ*gX6{F}Gur>zlYAUWmH"}fS? w{Ls(y=:uˮ`fAV)Hȹ_b@5 Κ7 쥯^\8vъ8<].S5 uKc]:u#lypi~uqnE)wkd߹A6 eu4&u~Ĝܲiגn#x2p$!}¦Όk}GVrH&O_٣5z/Dun۟צ_qX٢,wF_˝]Qnnk"Yؚ6״Q#&NovJ_[`j^Svl>ss3灕m  `wscYuQJZwbnk:)e`),4Iƍ䴐qo扺]BJ wƠphgqNeavYav)WcO\+)/ +ͯq742 y|oBK~092`F55l.+JV?g`E9 F+6DE0(ɫ,6הLb/?U֗8`,[V9 & Bymݠpli&*Ơw9'ARm~q ۓf8 oaVɖWb3,fJ*~(xd1cpOsI/yyiNVc V)2&ftE~-Ԋ{G^Ws]g8 o/%+^T6-(L/rM}Ce^ckJCz}ҁ^2c[G zp\SW÷4'2lc_b6Ϻ|)PkZ|hܼYaRp4pCA5D ̔k{Z↨y充zg7l~Tj]19]t;y^qqU] w<(`AFaκ",[o]ez\%[J:*WOf8~p$!Ơ!K{DVZZʒ fVE]=[;0ٌޞ>ӁL!Ul-0?d;˕TY| ɵVv`=D4wul͇ NVgAoeg(=}9A=8,fA9𕛷xu6h$,| (8AJ,>Yya+l-[e+J\vd3{D-~Ebp$!FO{v v<#ME}6&l\˛@iS[M̫ mM-;z{e6?OQgOCELg r3}~<_S`eؔ]_e3V-ruaRى;%`_C 6dr5m U- gN=LY]6!?ve`9>j! L@)Wrx*nO 뫚JrP `:e0fymm=U6; .uwp) qKC7!|K*콡dU'v]#vsi?X 9}0iiIx ͖@$T29NS]/?|#ȪI z]d2um N*1a5UmbdXݪP -$ mcD/oju8tQ/W8!]FMCeȫ,eԫ@{2; -rce 8P$]AabP+ pQi\#SIe0Wk_1WW`&%Q],ZNK@7pY4g?{\]P.O-0skX\ՀJ]]T+]"aSgOAHC !Ơwp>mOJEQU im&Žg9L6H&mB;K w7:IMٌgZҠѨ9̟Ǩ`{Wj{dd`b̗U pX'L,ٲ$ n]U]?1HC cP p[:X[; 8P,)n,8떅O\o5{O^HC ; X,hI/y~o[nkezo2=z=(K!uy2b=E􄻻[*+m|!d圡z9<ū`f/E3E :&vg=;Ġb qG ogO Ϗ9VWV5Ԍۡ)e~}1p$!Ơb)e 1HC )e`b)ec)e S@;Ġb q'G8Q?>@bp'O$ܡ wAbp$! wAbpCA5_?Ep$!ƠbjA %qOqϯ#D`e.[Xqwv*&F!W1L&f֎.$PqX,GL˦.7ӭdK1I{ (j\,G!Mu!& f %F=@;=y"`Bbt SxԸPTS]ӇK&VX>KTb(i*&YƆ{}o5uK1IT4SVS]I+{P '$>Q.TLu)&kWz!ƠG#3fOu)& $S]`O&yP<եV8Gw[:VY+T1\. vw`ñu4յ(.6T:K8Phmj[?p[yֆG,22io($gkƨG"準 -u-*~ߊ; 1(j w:b3)k[O"\yclˤzM+)x esT67ʕi (+) p$!ƠbtnIowpd{C1,OOwԑؚ7 Kp$! w[{{Msϸ`N2fL52Yڹ9y;IcE ;rH᱗l^|#z+ܱX ٖs$O W^XMw/@lg%tqJ=o=[^]{5HQX>g kqc /Qg/Үf`yg&ww<Tzx|c wK; {w{uqE%ݸT?dƙglϜ9'q#k[Ǧ76;,p$!cPX>-jO?ԇZގ}o?zԆX,H/)^H5\=/v~VH4ŠPig4w Gnš3ܽ/ڑ;=yB w]oHC ;p8P^X-3iNv,c&p1;_&< X,\`\TъgjiYAHD3.w5)72 wAbp w2cEYn`sY/d^kխ.."p [𯞼^W,%lM4^9i٥Å̐fܲ˂?᱃9;v-mn<Y,ʅ%:}EYN1LJelN"=,~trM8Aa7in ]>O`oqN2MPՔEYzwL&pYn⪺.b_7ק$53Xʭ$ u'{[kces/6fNI8&侤>nfe46$Ҙvv:|t\FܹUΤlk(quu, xt|u;kPYk}1w?{=29ߜkd}z3ig_gB=(*c9x鼓?TZK6/w]=ߝ.V7F&K41~%ڳn-ʠrMgL4&Y\]o$lmκ[ү*K}2Pp98x8$]0 *zƜW3LL̬MKJrHHC !ƠWO Z6$Ķr[K?V|^I~]k17Wtrz@eRI`!`}[WMԬ[W/:rÝU.o7QT_U.uS)R-rŮ/*۴h0QΠPJ~uEs0{_|ݻE;hp0}N+=p9q*yJ@ uO w2(D$]=?$hʕgn>CT*^:wFס)e߿.jESkgʐG.](E ۀ; 1HC;xڰ{}o 79*Jcd/%Q)ܝ.(f.ܗnY$ YT]RZߚ*(*'L#}җBt NƼfitM)so 5+D=H wqwσ۹k]heȑNKĽN)c,>[&L|~|e^K^^ujQ1zX-7`.x99_8gs;8ڔU?2v,ϸS]V33^" +,:8~o`n,P[: L _B&P*N{7.lkj1䎶Q1zX?fN^Ēw|١z{)e[9@RPxSTy^Eg[g>>أ_#)7a{?̸G>׊!Nr{wǯ8[{x\ykumJRNdoR҈12Rk46ޯ&&ߠ:G ᎀ'OQpuvY6/B.&57g뒥 1)䕘O~ڴxIzaa[gΕ>8vOnefvM[nd>lh m]K3Oҽ[; 1HCcyXR4xYo/L4]n^engolZ3 t=1!7X_Y27LQoZt{; *U] j F\Ԫ?\Yuō3if̻x"~dxdH@ uւZVUtbe]pA}[_]>;1epnDzL}C2 Y7v./jji|?kw;S|ͽ겦Nvj&.|7~s~x+0Sҥ[},^>4p*Q@`M~h*ΐHG=:TWݩXARz.ee믏`0s.#,: h7"HR!z|i4ڊ 7<|#??42T);@`* -Egl]]TE%CC1,H %7 -qO[oM}:ٺF8&BibeR[()ZkI  w*dVX7t":.I MM\Ao{o䪈T<B8EHģb(;IY[T:% ;3䜠ž͵mAX$ik^Љ- Ie"vrLv==½-R470$ڿN\& θceo.؅˦iXR_8v1SE1uMCek#Q[Vek<4}wXK p^8@FLWǜ訴yj!LzpBQIJXj?"\*+T*pi'ت0pp2dyUEu->&FQp¤_Ξ~ʹ9z9M/ 02B{J)c;8xU<1he'8#VE(d 7( VUprvG1sQށr2*P0%tM#7Z{Q2F_C%0M"v/_͢1Nâ1ۢcKJT/lO~T*|EXxv@"wt]&M#K r2ٶ蕇/9,Q)`~59%$`,F1wV}Gpj^Wg/XkZIa PwLnVioGĀRW7XVgyy;n>f6f/ @$ Tu(D8.s^ue㦾# xW aq`+リN6>VS]57;r?ѪX F6m+O=]z5/kx,NcX+ony =ѕg~|@ Z>%B"/UMXuopUc"(!A91g;@`z;xW#\$pG0P#8G0ZUs_HBgmKcHw wbЪ6yN@XpG0P#(  w;?Ѫt< wD;Ѫ6yN: ȓN?X, :ȓ~l@T@Gcp$QUU9}y#KD62ěekpG0P#( 떴׵׷OJxpBJ]+77V{t+٪O"bkIɬꎇ.-?l5.֘Pc:H0"L?9z/-aޘU&`tC^EnRpL=8zV[8Y81u&@7rb#ko y5Τ߽?u_ӈu߇pG0p`rS~a[ = :[v F%E?=V#= G'_|(!A91pwwq||&'ס1`8 LJŸ`1h40qdƠs.穗oIQڂzJ5spx .D-u9,zlLGQ]}w|wERTɓ/6ͮ[Xw w;.QhI\OD#@%{F(UH&pD|eM͕Mظغ%@&Rg={*inOG C7sO;8=x߻DGf|^S[WܬC R:P lt O^~dQ,X2㍄c[46PƲuCi=RlrC~K<@YEeE|ؘ|4ezksb'$.l@PL36ANI;L|ڀ G˦$*Y"?paOwL^U)ǔ ]"hfoqoCD!߼唭$'R\F |($@$( PY5x)'?[;Cy}@L7"( #܇ w b%ҟDHqF, A*@!^ku{ikeRޔ#[T:—W@QX,{.~uWu.|]n첬JK'Ϭfh8{kL*'3/P#( FC;x: r8ҖorxlMd%*)F"!P`ƹ:DoXP1-/oU($ ⷉ`fR`KrrJw+TpGZٷz+.=ϚY2SfOKnr1 \mh:WXbiR1O_=da 8=}go! q`1IGp늛8t2t`؝KܚW>G~wKՂΆ/ƃ)tRWA޲睍|NI.|Hwk 2iH?S]@2K)'H7C-cv, $a/L =R#Oxvek'c54`O[T]W7Ƥ K' 偷N?ct}qwD Mq37Q8"+NB׆za仕9qϬbHTRosOevEPl $QىYx"~|p'׷ :q+}r$,Y&1bRL×׏^wSEە;biLhn_li)Щ %u!Q˂u 3 i~pFs@d*9XP/VU J"xuXD$ j*Ne\aV&]Auʦ=t M-ؾ3߸P*DA.NUiJAԒ,"HX}5:C(ɔhU>`EX-v7?e ½JUySܛomK=4[\$w$<w(7fi eTjf`/- ƭgyFLN᾵cH"fuzKH`2 ,P#jC"ϯZ)Un`jlee{o[0ʾO[s.-ݶ8Ѝ>?5>?PV*LP monRZ5C5ʼnx[goM 3]qX Fh~npl@)q{cT&r}-(˪ۻ"R^gCzK6E v+!wpG0P# }~K~*CT Җ;QiHLxꝽG?qT< ,S/&ՙFt&$hIBtkM.K7t]]0L#g_3ɚ3).R^#jϳƖq{}qm[\2[_?r%~wK=L\/z;|+w>/]Ќ+ȿ3t[~KN>Ks =C/~:Fiw4m9ͩg  [Sڜ}bφZ /ȶ;s'px?o.Y!拗o<ީolfs{p g2{”V8:_?RѼpRVx/)QU]1L< gnPO΁x͋?>M9he6.BT "}>24K~ UPY&B4W6ۄꖍ/n7ֿ6 v ,zʝqo`X&71X+ |}}q`֘;s|".}7ҟKq Z<6M/ZjnoQyՆ;-H{@fVU^gu}Oº#Bɓ? rr;|՛>`B=| J^[kښr*K*#"[qWnUiq}[>s3y w'/}4*}_cc:omc߾㙃 "+_~=pwk*[z c_v<￰\vF& R rGؑށ;k~.J$Zۥ8u_ o(VsᷔI:xCXT(\n^!$ldWxd+Y3WWpZɄ^Ŋ>;5o!J첅o7(ak(lu𥯮{~wHeP~o~sՓ N8K6Fؕ5;@`4wwt vn|qJ:YE5B wԑK%7Rps75л{#o"x31+˜rv/tMG Y:Zyy_;|::cT9ȓ#<0핵7cwF2i{5XֺrgY %B 6<W[T[mw?̄gW&~}} Boo(13s^KKP(8ց'+vE𢵳M`ξKY*vnDwwTwB'(4J8"s :'/{/z9Zfrz/%QJ"kFǯ|oPqmun% Z7*HjKEW6R=55-\Ts$ir\}I߁7dWWEVR%QI^ٔpo\\#=.d̷}>P!nq!`zg}y(,ܣ7.miAcMMB`Ӌ[0=,JF T}Ma旷ч#2ؔŇwD?ڋLm{__/ZP[UP=Tg?@cq؝lA4&Wlp?Ώ  8&R /˪Y1翝sAK.!uv@`pG0)|UЈD$8jjJxr sxKK|"x $otںgKţT]څop5<ˈz2E.޻3ft}yfwHW_wK[G9Wo+5ցM9q]=Bmx![M^sp(7 op8"<g`I!\=}Xޭ3&@BeP.~V۹v +G3XXޚu)_}L#6Tq:KT v =]rp' pMieY`/ Yw{}uw泫nasd3 +D y"_ ޲t0]L#O3|l۫k/7KSwD2i5E2zMmg Lekw Gy^J8dy8}l%Fa׿!H>:+X ~M„p>djcnVfWn~c ( j{_c ƒJg[۱8LsiSQj淶0X̝+mU3TCfn`j#T.Jt[;81ZYkӈՑ~6.[:YEmX~+S{½F8F)4Jum;-A4d;{re^EaFP-ul +{ZaWfVUӗ88¡)9Ӆ{K{bM&onߓ6dgKk"?捠ѨL!_5$@$o}T.۰x"woRYt1;m{tZ-oy ,C%QjۚOfw:c?X^Wp9eϩ(YUq~HH&>O<FP_.8qW' ĠUmO[XظZ 4&QI о, ̔ wQ#=*h|6 S_?3d* JՃaaJ4>Wq{:,oB@j.8pG0P#(Jk1h"(* C K1YgVM_BLbc $Y+]x.DХtA̝.nΡ965Xcci"x<E8H9&9\R;s:U"C3 <`~!TyT"SF =Q=j"u o]ț΂0(Deq#Ի~,P`|"N<O$19O|&oP0aҩO*Ѐ_ d$ I:*U3 %S8nWՕ>I6*{ʅ?ѪlZS r쫯r}ch#=V?aiS^T&4pؽb0SY"&b9d#D!ttDswqe *[NV?4ɧNA %bY-xh'PCVs>o2WO=~1DC[{PZD@`pG0ӅAePMm~R|hw3 [@GԽ]*M|^4[p Sy @h8;;P:mխ[@˅#툚(c:cM)U?ѪA½؄/~؛Yꩧ{ y4: I$݃};Ϧ]$DcbD4 wD;Ѫ6yN>~tDD%E$rQ ="n7@=҃VxdP #ԅbpx=T.ØזffoF‘:yB`}h'*{%2mvAVU.;X.tD=d>PJeF?Vp*P#j*2$=]0 O2Da"PXE&e?HEט2!}1x:Ё*-,tİ`@pLݣ@x2  ^20,`1w f΄u12I:v }zun~ ظX;{;1oEe%?PcsCE!nie಻ճLwmh jEap ;m0fat&VϼTT67<;׸_QCA ZUs6V.6Ygeo$lݹ_=0P#(Vsb&B=|rEvZbCtޮWZ< wPtT (4jl2c9JBsthneqK6]2K1ֵ;b]lZZ*|C66VS֫3q`0  )/ػڀAvVz5u&ǰ>8]mE9`"a1KP?AhUjmL=]AVچ"_Fcpg$jO_[JIMoŒ-X,Vir!.C?:"ԮNA/;hU<'fFq]>B"㿞Fuc:: ;cNg#AJ.=!n[S'ݺw= dҶ=p%\bvXdz 2N~{qT2 Ɩ[i+[-cǕ룒ΦG-o\:I:*$TD /.1o^A]H<$:aW\Qn9 Mp+ncto '0ܑ+]>a7rv>ByBqwAg .3򵑥yUPC ( ffv{Lo@SM+Ioo20gptܟRo޻6~ӲkgC>8J6褺{]<@:J.mël^c3i`] -OaOOmZ [hr-{d\`Р؊{x\*ZyKQ2'vƞ Q5]-M8}N~r@"زW@e i?&Ma춘kG٘ژf$gFұvsi+P%{䖥h ?|LO-* U*յ3O#SɏpK=y˂8Qtm'Rbw9E $[z(aˋ~x#|t;z:8x9\?L7>1dʲMN|Ju #-~{O+2j&`1¦cٲFX<l 7o)9LIT(2gߩ*:y@ceSeWLx3Sp0I^Y b,o)0޾"ѩ 6aJ'tzwCGĦ衮|_vmfz>Xw_G-}.%&FsV$2-vCTڕ U-&F1 KKD._q5{x+1nϽp6N'NNtp=Ws! D~gڄp6qH j)/*A|b.^;&^ $i躴/lHFC#&g5#۞M8چ34To7Feu$27vi :©{RQzfyO%RQo_BC@`M w2D` fskgO0-SFüTab`w(34^,ǒ 4X5@2QvE]=zMYzaw*:w`4'gxDZ$L!ٻX7Tui)'C_VL,T&7U`lyx5`djbK=q]M ŵT:;ЍD&6ֶַ1d0 =܌L%{U[AJmeo.#`jztg!_xͩ^10syi~X(o&;J@`pG0P# 茍#e z['+7~0^gV]'xi`@U5:Ź律<* j,d2C x(Fd9ڀ5;JЍήgYwۭ*ә-( &=0DʢZ K0@(]\lJ[= [kZ۩ j`D<* 2y L12<\5Qh+שEPO(_/g[Sfdn担f0^2=3 w;Ѫ6yNtK(  w3-eCo)0zK$!,S}Ag]ۨCGm痄;"r5;@FT ү wUm@`pG0P#( wd;?Ѫ w;Ѫ6yN<ݽ{$-NtXT. :ȓ ~l]S}BG1OH-t 󄾉pBG1OHRJ.t 󄹥ywgBG1OHRFc:y͹~'D W>8G0ZU(^( :vuw,t J5&բpG0Z& 敻|x5% V." +Z4jt ">_d-tDoSjgЁV{BG1O5)ZQ|@5]@扂r}s.Z@ }#ꅎbJ$%whUWb i'[@QQP#j w;@`pG*P#( #\(ܑ ĠUm@`pG0P#( H wd;U ;RMP#(  w;@lpG0p`rpG*PCVs w;@`pG0P#(ܑ VU.H wbЪ6yN<\DK#!_4; ^2 t#B\K8C#J圅'S#ԙ851sLcjs~8Wn*˫k H w;<赩3i4`ߞ= oʹz{{`L@w;w;H~6nnZ"  uSN]}j=fLX9UͰ̒ ;[[5[:YK5?cjw{wO.%Jw#+rW$SR9zc4"\%\+|YVXeqN~jИXn`zJ7ܳA˄w` w w w## 5<**b>F(ߟ›=%lNrNё-],X")^xrRp;+wp'p6P]k[XQU^H!}]]DBi`q2+#* 耖&]S΁5KMq^|. |rYcYRDB37)GApࣞnE&M(Ibi/Eb2|֪-Sld4RCwK%n+,fQmtzo[oyNWF{/.{JU[5 gm_G%Ӄ 3|AEnd]t̞nWxoCYQtK}IKUñ^[|-u8{pw\CDFS銫6nuYrX$rqmn0uᆪ.gj3/ B})4*XuxKM"S,]>Tz8\u]xZGS€;9rSH_{DYQQ°~P`g}5}J,]lt5w񢚮RשIP@ciYblgcsu] _NgRY~zCcLS]YYkhϣ6|,'1w/ Dx`<[WG=DCx<5ʥ,Nf`i?•<]WUO $odxFZ4֩BR o\ܷ|C}˞;GieAK{탲Acj7V+F!e,M\ydR>9~S+ rgpzZ3BCb~.I%ʦO׵! J* {qLmH߈eIjh/wYD#Yu5n̼}?9k gtpk\;tU!#ܙja~"W ߵcã,U٭M=F*܎/ގ;r B yDt"+RS< Vtteu]Mk%Tw-񧮾r BF#-}B2щkk[rPqx,\bI| Am5-˝X*:iW[B{KFnq`}qg,2*ʲb|Ա?job:fqWz/~;IdŨ >=<}Os}?~?l9rCFdCfR> xnISajchd=0Xt䮰{3FmZ8Œghڭ>͝μui` w w w2tp9 Ҋ.N' S۴?Ybnbel_V.룧B}Db\,+2ȥX쬉n 37nX),?xFH*<%-Ze\O pbFG3] $|qz1 dITRƵt(f[F{^~lIgn}71x5-=.m~yZP[ꝣޏNMai(nJ E`/ WFoӮ̑Q4mb`zDьhǛ;E ӂIyS< "p)& #|&Uu*+6}%]pJ[kZ6~l}  'ˤ3 lh,ohk! :2T ۾5 Oėfɱ M?D탪/&+$;:N8A|sm "@"D2'-(tJ50M!>{c1=E"SS?ܸWWxc ڇ33>{^ w w#WuxpWT.q?wa5% =C.6/ܳ BE3 Nʍ,? p1qsi`[/K t12uf'?76K^rEPVbsMC6#i.`$!; ,! ,! ,!3_B"4šdk(egRDr^zg_1{=014Hy0缝mt37ԗ&f QHnw4]HgV"JT+4XyrG4W5wXGcf͈70Tb8L̀{>H/H.B3{VA8ٚ fi &eͭ88t߻|R,?H+jh|wUm|AvgdwJ]-CmCk;-Z]57ڙ-wQ).|~;OqW[w/&m ҩCCq:5`C RJ}=E$׼N$eg $esoKμ/O?qۯ*(ki1hͳqiM5ߵ™r[:'e`a)eLp33yv&8-_flc;zwM1yi<#'ܑmAc1xbd<-#[3gHPZWVb|X*-y73' (pkJ-ޢyɩax\poC>~V:fXX-[krrO@:"<++#NK۸iq8XCaper4}ѨVfNA~"a]cif^u?]5+6GWUfZ*2r|-LDbk*[NڕE p0p0rU'/'t#fBo W`zקȹ:=qsbttEBѯmFD0L}=ѡkoIWizCPxv0I9p0p0p0D*<0h{F2+%?(Q(}cn闦*̽-g%ώ "W{X<7+LKt#-u 6Ó͏- $ ~7*<% жDn" :&Hնe'd> "A `P0FHu4^% *:)Ըa枇DИfo69:B=q?\Dc0X7&d;'2X, yG4V4#v[bT9;> 2(_5w7@!JyXs ɩ_S ƊŸK`'ʠx)-/oC*\4sy$uC7}hl5`a`aN^/xXCXCXC i)!wA)  qE1ܗx:[MOj *;ڞ #WB%`a`aN^p,p0p0p0p0,ܡcCXCX@*\XCX@gdpQ_O8I".$ o: l~Q,aKSt 4ʋT"M"Y8Ӂ,4#/'u[3^ IB,a\.TVǿ( J&)u@P2'/ XC>po+A<\.?pM9e UȺidD% MHpgIyyoT.:RadGtr4r]r\.tH!R复!̎ U .xCmF pBP࿐ <A*3 jo:EJ1t DDMGH4u׀7!oW5o2F.~837!oW\!W%"5o:E1and1$=%So:F w w w w w w#oXCX@*\XCX@,! ,! ,! ,! ,! ,!"`U`paU`N^p,p0p0p0p0p0֋;T; d…;T; d:yA[/P0A P0AbbAo@‰{`vݾi>[pݡenl~-BRv?<{ĉK&֭*noWS_84+o$?[w 4]Q1¼PS!8LJ|voG?Esu!0 ^[^:2če~{swΝLJ#_#u^O{_f}0kԝR [QU::4.;I^J/$Osjow+ENnJrXCw@ޞ*|65xW"r1W;vc5ΰ'Jt ]QD[gfp U`p_S{2cS,7ӳ0z{d"uCnm574v w w#Wuxp'oAo%fF7 L#QH7[/ؠ셄D#h }XtEXh[:^?D|E~cwF~XT=$Ҷc +H^Bb--+|ޯF-,7uq@ .ɤ|B!}5u%i_׬, sak8K;inl9+LNp4.G$*Vf6b~z W/w,WT-r]#mC͂>tM$%ǥDs ґ  ^ kYj , ʮìrzy0 >t:;C.Yu~$L%F%rrzSS75M RQ(W+hԖ5eE qD2Ze'H+[J*\}lAw޼|O& XC w-# ;>8~Mc;C '$Zӑ~9k;P< OSڪS/g-qY*"p4<ݐzpwpt1 M*ݨ$'ʋH%4%iwa:y#URGI;gR: lsDE"Qm=g خd) 󵍉C׺LcGM|z[w$hQd%g>kP*V¤baa`aN^/xF_NБ?SC u6l轘^~-oǽKY]Mu2BSQ6V7տmH3B(g_6Xӧglt-L-|nmmh-0HmAcR=BbsuL4\~ʿ]"#ܥqu}x`癯c=;{ }ӞS<><9_[vtw~Ol4"Z^{]7Bq"J_/j6&J6F2w0J7:Sl֠,UmqN@JO RyF悄;(CcG'ާjJL//#6t\]UFg? ` -? h[;H۱i;?z2qcW[/óv4˼~w88,)W2pv8IW'|>14YRwϥ UB~tb1cџW\M𷃏1]MW~5 .>9@{{W[~/bq ̪Ҍ 0sKѵK,x/,RFʥ]y/|`gmSͪ3Fޮ\B wA5bGDGust3ճ:tDY 7ѵX [FbOO5TG$~5);nκԂ.g4w֏MٯO|&|/zOapQpݳ O?퓉FZ}bdLGV^jÓ2k]mYzprGokþ蔱̐2 O# wUN$*^LR (j0\nd^]Z7!ev|\PwgyNMSE]xpmpntkј6=9ʦ*咊&+[s_NML "Wsp(܃!e`~'`a`a^Gh0U!:\8x!eu; t 5'؃}#7/xtg4tTZ{K*!e愻y9ӽӍ'oN_,qe>WO$tp:4s[u@T}n ?nqA)v0Pp}~0͝I*܊՛Ҋun34ӳXbs/8ʷ{Oq BK{ʹGq{zuޯڅ)C$"wј,f&X+ԑ($onK}Ѱ]a8H˴ƞ`Vf1)Ȥ)py=7wlz{MuiǗH$M]炾1<"^8LKeۆ# nji†+bGJsQ?㈳_}'xlNHMQ}cIBN`.[+{:I:qga; K%:Z/> l_~PTČF#{j"4wHvHН^oլds66Uik$J!~Q`IƵ)6/xocY+=͵޶x^fda=޳q%;MXjsѬO$LX2$ qk:[z\Cu Hkxp(;AuC GXȝ5 Ź弩Im{?39֑;ȍNqxƺ9()}(){`uۃ{;Av 2K[Nw|sE)Aݯ{Է[ٛ]:zgH&GRLwgHu_$ȵX$|<sBA״{f9;[S8?o_9δ["Ty5յ)()[]Pvah ΞP)WO&ŒoG`澥lgK=M-t3cDxH9Ep, b)FG??pjJGTtHծ:Z97}Z8S\̎@[~z$ܱ?YXkiCT3-ծ{hLpgi04bwDp?.yԴ֕o8K/djOO>~*; d]p߽fɯw/oG{#ܵU2yE칶HG>N#áޑ"˛2ճ ,! ,! ,!p75B (gg:2(K9wI/wS*C:͞ $X?#Epm} G{ikq`|("BNYc%b jN;y|*8c`[p(Tx?Ix#l;=,I<}Vw3H$@ FNUb"u>:6216<׺ *C zqAc(L*1i@g)#D$>X H 88 D&ž J: |P2;/54@W: ."p=Ƴ" HJMWej+cAʠr9XF@mr Ṯ4>ei21XP08-5{uTkU;hZӔicM1Jý#`!)YPT],2X4><uSys)J =oX@*\Q+hV^:;;P\DeL x|/ #bI\u4uPHT@,h=J"P0h4SIAcI7QS LkDHL?HX )AтщᖮE:oX@]TSB[:=5\ wmei՟Z{iR[8>T nIz+`dᾱWNXCXCXCr,}/h*tsGzw56d=uٍͣRFkjHcMo\}H$̖qm>NiJfe}y~ wh3_9+k=R0 #} NñY`pe{($b${+ 81+EKUO:]ZQzQ,M'+wiS7pK%!7,a \ C w w w!e󇔁Rb\Ɏaʕ+p1MGHpT7yӁ,i`-MGxU v.}Ld D!Å I@S95**e U:tWF0XMHH\{Q,֋`T(̛b04oil{Q,"=!oW\!WB#LyyLRȔx]$O@bSf![_0CpHp! (\_"@`~{0T4|BX$: h}EƛdlF+(ơvl>֋ t=77X<ʕ+pdb 4˗w%y'ͳ8U `Y`a`a`a`a`a w w W w w Wu򂀅;d;;;;;^,ܡ ,a \.,ܡ ,a \ Fzp*p rUp*p rU'/XCXCXCXCXCXCyE2U2U 'ܕl^Ku˧hhi%_r} cD׷s&^fck篣1:0_2H w w wBB,truA(OiVwJ$ϾjJxmcPU2Tcq^WS?O`X}sSMAKg`1Hhy[^_0ݜ^<6׵/(;}'N'B0A 2J۩q/'.kr$ ,! ,!\ Cʐ{"X8+1i}MD2zHo(o$vKɵ7d;FZ4WcX*)Q*ܵ ӂҜ2@Tcm˛{]LO$ikח6H$kg+E"H!-. p*p0p0&Kyl^kmWQJY4>2Y_v$BYQY1hhklOW w6k|h(B4#{:Qț? wp/1u]mHDC_ulh<#d@pKUUIfpM=LU JLi}p+\Ҩ+IwtԔ!mnv`*:+q8=2?tvMcoaIaEH$\j`]Bvpumm=fnK4/~d;53 tup< U`mel0dq:*L8XbT7hζv ~+OKu4Tc MWQɤԼbp|-p6 v4c('U%E1mol{I,K; h݊ڶQOG?N`1EcD [:4jzP(Mgk/).>qTeյe <.b1AM[AnX,7#qu <,˞n˃r f nQ3W ll/t8S%y:<~z-=9mjmnj19UVy]Yi aBr"!vrU2][Ӏ'hj᱄۩L5[`yEuĈO+JMlu ۩qƆ:&S\vyU_ u ;jd ^[* ͵5@MUTLXaX uҊ|ڙH 7Twt*kZ9*~A;;:yA,@/qb1݃SR& ü|s;sm]X˝t(;n no;Ϲ w%RVSf-w?{2{_ز&`W6tg`xۿ?}/ ޾QKsOoJ!Y%M[f/p_boaw18zk}hWߠH,6l[RsiT򝬂-M&96; Vz8u/hp7 uOJ_foU^X1w~se9LwUI^L' xn\_Rz x;Ԕ֫iWPH-= ,MQҊ&9t&'=nEkCGqNTYzRno ޾e5&BD~{qߗkU5bqnjmM]rpr"!bKؐ?-{*җJ/,cOr&ѻó+(DBQ$ihe9ٖW8ږU`hSݤD&dj*T.MI̶3me#a"oW\!W+#]\|Rٜݛ?+LEPKcp_rtuk޽~̞35YX98-CxK>3Q^U86128+LӕX>ARb{ׯޅ@"˫ CVF:BLcK؄ӡ؜J"mj]Pfaa`aN^ [VUTu6u9}\|t`R[Vƙz22=hc̆O|uRap7621}!D!|'сAGo'DZ1W]TTX) wH w w!eVm+NE'mlox;_o{T4{r].C 5{#+@f/{%`+* [ROwQΟN76:Z[\ul jw]MVdhBWwML,sydl"Ĵ\+s7C<_9; ueP;`w9=8ʷS:o03dioz˜IK w+3*ѼR:Pdjmun+<| J ]Y)%>K&5 5e\Er.sm$-=ue݃Vmܻ&=)W]GL!6׵/,('QG9eG ?hxU"}A*7YuHA. F7նgs+mqx܊mGŝI Z8&{vƅ1xHyr *ܧ;OLL ꝫc'wgY5]3c3!e&FVD tј䴸m%$ >󅻔 kܓ Ɩߺ/Si.L=#DvK*g<3_Fa~9uÔU{;tdsuK=ʛW { _LM1iNInn\y7ZX364TD BJ8e~#c]M 6twk_9m9I gߥqiH3q+3@lQ~Y Yp;T;;y[}K ? =|@FZFz\>1x~?P㉸wJqoi`'4]Y~Hܡ*ZL[7S*JýN` _Dž07'SHnA/|] q a$T%Wo tk~5+*&&B9z/8/RSM/dslX[R]։V!+PH䭌hQpWNp+ -.^717M+^TlJa+]N^nHwp6h)xLa'4UYH[ʰp4ϻoB_z{)6Aƃ߳XCXCXC w;OKTKu%ιIEh,gҙ4*gz5dPh_`E?EcPaVb- J:}*ΦkݘJ5EMe(4#dHX|#kvWS׎q(w"s?DT= e@H2yq8HdW^0AWSAU2U&3DB^+Arf 28: BJ UOP+=]B[wObgg3_D $";XMLq-3 J t4YMo> uǺE𸶮ޡʇ/\sw0/_V V6ӚȟpAN33YXCw rK16m:;^_`yw}O P#( D? `iz#@]kK3_ݑ>~K# w;)g'Fz;/\hu%`0lv|~ËItv6.()ίxT^!|5wd"A6vp T5pB.r7.޺tv@`PpI@`pG0I@`pG6<~K#(!U;1 w<.pG,P#(  w;A, w;1qpG*PCrO|e@42|<~,m,aE*6-w:'; B`h"2فLlOB-Ɏb@,϶R_8QLn^;QLh빨UcɎbә :ct:Cl8pG0dvB)" P6."- ^J͚K(j6QL $"ϲ@&]ʞ(&yWgm`( -rcCD(P`sQH%8e4E%Ow4 msqdG1q@`PfZfÀ<~dH@WJ?ف@?v[G,'; NG̲Ov DJε(&Yk]Y t s hy/Zn_ȮȔ)%ā*Pոd:#-)2JSU}DijsuHU9y\@XpG0P#(  wYH wb@UBTp Tq;b@`pG0P#( fP#(!U ;RP w;@`pG0hE@Tp T5.H wb@UN)ܱX,JVz@,GcP0fr`0LJ` w;.LUUZXj8ID22C D RP.妵E,Z6nNy) >4rpxHP*Ta97RTER;)\>ހ@l*茠*bJct[3 F'Ł$FILe[ w*PոcX `cIJ @bq*$aИr Ygδěm#[C wb@UN)Y<:8r%ǂij '%0d;@`Fs:ٔeB)L.ƹ1%G&#uՕ׏Rc_1GL֏Eږڛ;ozueޥ;uU~++pBT?4r<0r%ϙ2܊w"XG+\9}C`݂yvI{fP)];]5-h|; Ϥd@lkʸ}HE+G  %8<~+{@`sQwF/_z+E-q{L!Sj9Wֿv(`ɳ?=Iak8Ը*\' wb@UNJ R7ƲDQ: eFR!Ik4 L5jGhv-#<+W)Td*Y)0`"FK [ }};Q&+_YuGdׂkC-P#( 桏 ." QȠ7)$6Bpm+'|9Ij4ITXWUP!I[%e'<FWai *ǒKb8*>RWSRbd0Z$W+`/|Х<~윬WI~J&c0_`a*FtWXvѺH3Q(. 4-I`൯GR$ϒkmgfwbOIXoN>vQo=hRNX:ݍaIdboPљ4PW0qd[gRp-(IgЌ;Uq-x ( wD*Pո&BcA/UU\6C!Rw_Z3tc{N<+P){iǬи#YJ[~H$ҩ fr@fPoK %o`&۲\6Ղ N%0hp=m:P疿/g?(cAL*ź =xb` 0P[<^p TqqpgpVΗH̯ͭVHf=;+u#xVlaΓ %[ft=cY(_ <=gn>#iσ9ɛRv=` 6$:PzqMq$?'P#( f\f%J"X_Px̅TՕ/\@Ts-8&=`Ņ筘R.SbZ,l-ooh)=NeN=i &\j@AgDpp?o\"G/M67^wXFl.sn,G7;x<;ĞƎ9.B1x峃N!Q}=N?}DDعXvlMIPHdYǮ$!*|:p1`Y+[9x^.S-Vi,m{3Hdb]m==P6!08e2O1QζX wsg{: Ă-NQ[+k۶QIʆwj/T?sȜZ;F4wf --muŽ.Ru*;{l(ܑ(\2\vZwgh~uR 8,3sgL",Yc6gLЭ-yOin5f3]}vգ wg }< sȸP@{J %{AhsQD/_ˠ15ZuS{nWKgm;1;J[c;z>t6ƹN~鍑{a38fRnge˱}C}@^ξoN.ɳuxw/߅R: C>nHdbPƞq+@ 8,NcEnIp5UgX$LVg^tzGk}wĢRu :.]BM wb@UNZ0i =-tNX%stվLi|` &Imm>u5U5,P&u뒜s7xD!%_x6W?W7CanbhsjY^3NMIs;@`.koO(6&x'ׂ5QP >㱩AL6DFm\r^KM @N=̌ђ~IQvHj~ӝڤ 쇮yx^EG8Y[9Xvߧ<=xb3}D!'p~ 3TJՍIɽT8')ԁ.w23iSy9Yy;?;|KrƑ riO-yjz;{.ܧE[q3^`icy􊫗ݔ`';v.ʊcW*x畷F&2=3O(A/~/nyT Dr95C}VǾ/< P tuq r:;?;KW/[T\S[V)9WPPYnGo@lFLP-t5YCUib-G~9xCh0^P^8qm٦_>;P[ķ₎`FI5i*xv]ecK]-JZ5wa;A[Ej\"a5߶fiaΖ߷o2WֿA<g%L RL` W`УD"#O/~F%'m~{{?}m"ܷ-ve<"j}Bc~#VZ>c0n| W;;BϤ1Qۏ~2gPǠ1=j=+J&m**(SN wb@UNwPS2sapުxܙsFt Sz :~SL: u~ۦo~نD&TJ}E[84Wt +^Z߿obv2eG.]sOۓ] y@`pG0P# px\|aa45~_^ڪG.($)E׋v}'<.B%wP_?/ޚZu \=3dNQi ަ(˟_á[Yy*uSJB[ۡ+kd*YՁywϧLw wqVvɒ rbgd]n_>=h6Kto.۔tyB%G~9ԯW@/߼(A0(!4&%<&Bzvm.~鍬O;7Im[ܺ+r T7~w.,ksi?ϟ繧IK AtɊ G.]foy)ra Skyjk?хk+A/o$ЕV= E,X1g'w6rsiitJ`_ƑKA%.#j+윬<3+qN_Siu/.[yWh.YzHN`o/Zqr9d3( z.@Uw6ä|\?w_`~-P :Xη]TsEk#7$ܺV9'9Z/t}f|\  JSݫ皳bM OR׾d'Rų, -_r,-V|K2@qiOE惝)+{oN HY\fϧ"$y7fЩQR aaR}M)N6V42G;U۾2U,cآڣ-=Tg_Ǝg-d_^cmSL{WoJ:?3bv z/ w T24#R_̄c;nxyRe7 emzc^/V,& *!/3jnD,cqs+( wD*Pո&}mV6X,K~?~Fn|JLR im{Om7+=2nio<7\\Z{ʱ{pZU8 S2L;bM$Yp>&eFN{GQa`\ɪ]Y_nzx[,-nW4~Q*Ag߾b" s7-yJ5.2AV1h/| rĀ<.L4@t^`GJ5=֪aVJ k4N A.yVԻ} 7zLT:5J4ՠe@X@`pG0&}D,cvy5WIDpiE OfZkO͑֎b+;:o_+\pa5V@8,*eJbH@d*I)W `"ܽ}z; r i1 Bzt@FqC!UP*,f*8r@$t\Rl@a{1 |^L;NUᓃ⿛!H~K!Zqc?Z!RX9B+P#=tf˯/h_Υ"lzܢډ8fS2=@#I{Ȟ ( gΑ~pG0h빨Uk"D5* gjѩ Z@3tT.  3lh|<5YHx^]e9`@r)tRmAC[$`g[9^D}."Mc?pPdDQkԆ_KR4fPCrxp4lwqP: 5޴DpG0P#(pYrs 0]="0j;' NQ qYHGCҠYxLdmuZ=8/ fpg$ Ѹ,=qx#1{m=UqÐkҩ TT)9ǂ']zu߼$25{m4}!Ꭷ̟(!U9y\<6;@`H$G ( f?Ra@`sQw^WW>!pG6 w'ofwB#OOq`X58 ^Ȼg+o]܂sdU;9z7{X_ 8V٨c€Pո pxn藈`d64\`u78,.*|>cqP[ kl[c@`PhL_FLwW4jMxL0B H/9{8L1 wWO?;Xd9ES4,.c Vw>JY]Z' )]yQs}(}fgkȪcʹ`/U5  DEiAe[s BX~Tv ;'k\IgЮ_)IfΟN{; xizA-̿z$f[+pVv`}boMPXc#qXFU*TN_urjt`q ۹e|+%!Q<0;嘳l-@PC ( Ds,x"NaU *m[^fd畵ֶmas#XLsMKYNjm]lf+5h(v{z|$(/8s c>/mαrJMw(Q"8z9M 7 w ֓(ގۗoog%uqsF $Ӯ :EѾv|[RJߞ Ym=ly{2j(ϭ.ϯ_^XPWmTf1zܾ+GsЬ)ff%7*k,&uz{cwSek6N29L-6|Ba3gcʆƼr_WH*]YPPQ5ٌ"c~fopAi;NF7verKJ}UӍfwo[X̜YǯFN˿Zh"| f9Rf\eco1gdUjq;}NҌW*TeՖEyWnk؅QhfKC/&1cB~_Y }? h.\wbo Ft mΦ]簲 wȣ;/omio$/'o["ʳd \Ukf.> tvxnjm] jeAEX\ĝ[nǾ?hVHlL\I#*ʾu`GNH4]=gG>؇nn]\^⥤̢51D[[uڜeQԕ6om,0GisM]my߾!,.wg_lO/(X$}7kId©|C="s)8_0$ Kg}YwkslZ㾮._LNrsK˺O^{O(ܑwO`hqzO0H~ WOu4kɮ{sqxg@3Zcr_?}{OGinU)>8F 6+r.~pU,*T`Ü= ?"n %7s#&Y!esL.c_gI{I"13b԰S[ʂhڒk[7}OfNOr,z:VUU=ˮv=x^Ad@`pG0b4l[6FX-y- 3gTS`egjkō5-ss.jmoGV5~P0^)ܟ}{CbPF-p߸0=(|oGEQMDL7L{wG) ZiAe \%gX[rJjob0m=#{։Sb˝=ۚ;s/ݲqZ)Vi ]).&1c^XQny 'Ay4pG0P#>#yׂ[WZK$CbB ^WHٷ.P*X"MXBAYdBtE^Y.Hg"kj"G)J\;MQA22\{HLVMO&셏6_`3N0M.S*J{w_Am`S]T_ݺXjj.~sY_W$u:}~Vl^K)ϻ"Hb1iԖ4:x؆͝zbܕ3v;m" X>'_Ril.P#(ܫ X\o0 wCy.f"U.v#4.po~[j:|C=*kBǁad gv]Z:Jxl]yV]/nN5cKdXDŽ;1q qW;O|~{:X\YeH/o{?薶K> @Ze_  wb@UNw*BS*Z*,8vz =]/KS,*Kje9xNQՁ.3bk; Z7wwXb#w'v_TK2ngg9[w xw ';eqn5X& =G7; k~_D@cКGܑ8LZo'wRoS)2 ( )o0-I.ݝ|hX]TJUÝfJb.zI{rM ;@`L;st9]-]]͝~n-5COGt/#I^NtS.pNwp:.RQg`γٻ;׷ #ǒ%^BUkeWRk˜]}J&9x8Z9X> ZLjp1ǸV")LwnhFݯ*Z,;x؊z4*M@X#u7s-A.=m}jf@ p5VWlvtK5ƨC -ᷝo%Hum$"ҲNX6O9LBi5dkS#1h4J\)$[B5.pG6#; Q~?&J@XTrW=`ˠ;xڶ7tY4`7Б5*mk]=ٲ t6{4 {6?Gk~O{X(9${[W+OAU. k mKAWUEWK^-{X^ e18 wq+C/5^)S%{LPCD[`0A-L 彂)wj${mC'Xmŝ]|[V/H\:G;*'{qϨ9X6,-W<]r< OqY-.F=zpG0P#(pd<ϩ^F}+K?&W&*xt];+%Śݽ^߼P!&=qFqPaVX43df<^ *ւo|{kP#7cՂoo7T%ln <}h`'̀O0PCD#( U9y\Hȟ(  w>Ra2jpG6?Ra@`sQ w;1*' (  w;@`6@A j\(ܑ Ā<.0>b2l\DZ%d"4  B@&;?ЗA#Տ?j pdYuNvA?āYLw Tܙ(&ZTā*PոX,L%Nv]joo@&aP@h w<.03.STAl\DZ*td"dҦFd`Ibz<bs=Zm\d(U˹hi\YkI}"Lˤ0'; 'hKWz.@UR餀AS8T2uIvdG1q@`P w;@`pG0hE@Tp T5.H wb@UNP#(  w;m;RPոP#(!U9y\@XpG0P#(  wYH wb@UBTp Tq;b@`pG0P#( fP#(!U ;RPc|~HGH"рL"<"P#( VU{0bo4 ;]c0'7{.d3[3YMEZ9_"yռ 疞=r~X1g.`Ť-ѺSe`5e ^W٨T}\h jSmd@f|ppZ-t^N4:E/%>x?q wI[mpwY:;]}8<Akoho +qD9{;ֵp8K;.K{ lncP(RKkjka:y9*ʺ!_L@fDn|s<Q::IwYr3f=T3N_Q$Sɤi> Fgy=TD(o4A8,_*spknn;X*UjRn,|7L^ ǃ4!(쒤[U5y;;JZLn!fGʸOq7 wL$2%Nqr+RЅ]\x4_NeP|N{c 6C-vɄ΢ՔԂG$en,oagah-t4u,.A5T5x;P,@W9ݭ]ѳm=Uq%mr^nrbaO[hF{bJSKq;E:֤*KR(. _U\-8::G[g)dg F !`sB UԖ;DɹNo:@`P=F<'9w`0,1gCҶP_?ٿqv.OO?=x';{bH99Y?hh Rfo))+UUz }̙6{N>>axn@"b/o^JAAZ9q%3<{ۅoxAH>/jjͲȠݧNDLm˒%5'xpG0 avN65B0X3[GkLy٘;X%#wɺģ_N}YyaNf#;Oغꥺj Z;$~( U9y\+5Jի2bqLmG/_ԨsJKScbMĔ)?tw)3p7V6_ ##3ːG wr>7b;z{}|NiMu̴ƎvN&To?7^Po0\3>RftxW$3n\LUYpv/ftv: uts%\s-cq3*MHd@aN'G~:wL'wnQWb[QT\lNR+>RW`^R^רh\l>ړ')?|qs~Ŧӟ[_L;p_%|Et+3dTo`f䧒xJH ~z##{x\^u+jg` O칥x>Ӽw9;mH't9.W77սrG{lX )A{+s9_7n]]A$><-w(A4cjpfn;R畕{ʜ6 **W{׮Oz떝%;\ps۲?>!ehk&̐ Q3D 6:M\P6 ;]/ݸt,:0B!O 勡YPٵ]}Ư\5%j@KE߮njut&M&x曝O$;[ƌTCP)iW@+7/bn\֍'[۟yk4u49 w5G?+VWq*nIs9Sx\Uy+fȜhfYnyoGoݟO?uM 8ӉMəTJp7(0wp,zس{3 |58*GϧH43&!łAł[R=P.W\"fWe Heڡ|iV /: ?ݛAs9X[>5iF(I G w"W{ύlh.{k~[pK{iejAEu]I=6)Qԟmlc5}GL wd3pwpr>Rj~}E}o ycRwGR6-*^Qk"#|tsK@ePOmlă1䐻=q\l"}2Ʒ7Zvqvp H;;9DXYE_(>/@24L%ֶl^4J0Ѡ T5x{YMIՈ wܺwQ6mӃgXmNd#ܧLp,r:ݽ/92NCkQmmz&68>~>/ϚߔRzg[[PR09)S(r+ocRJ>a0R̐@pb&:3J@ T2$6ɤzr$-2|O!TuAe\ذ,Y>==T^S"L;w+ER)(L&@ZTȤӅb1Hq|P# wA$&'$Wc-rR}+pr ))/C{Gn(g/DB[IiEejaERD'}i+BR/e5ucQɋ Ĩm;иPbZӗ,)Ii\U 7?W`R&UުH ]VD_ 6?R]=?3_&rpS֚knf9ۺؕ, h`:aM<{strBlr,&}N63LSwJgǔ`ӟ]2^,GO\iRU|hPN+om\ccIdzoxy,ipFF}\zGAƖ+f]ZHI̸ŵ]=C#;W.TPa2Җ  Ujݦ8Dz8o݋2oD;yp6.j47qx\xB(ݧtyrR,Y:B>-N\.2l*c %"I[m \/`]+cHL[=${AhsQwN}qh\Phl.Dyں*䄃|6c|Q<lmaj65q=@+ɉX>4ڟldjf-c*4ݍ%yIHЁ$&v["HhhWjC^@:]#h""|2hdIQ*:y"dZQh빨UKb\:yiB2eʊ΅b*' (  w;@`6@A j\(ܑ Ā<'pG,P#(  w;A, w;1qpG*PCr@`pG0P#( m(ܑ Āƅ@A s w;@`pG0P#"pG*PC w;1*'ωG \mdž^/VuGS'r%sρܸXho[-D&8sfUqޙ𛿷በ*l|bpG0hE w(]z0 89p3>cnB00Ԫw/Y\V^M]0̍l>}+A~Pf( z.@U>Pc1X)g#`lFA)LR}GrDp ˺~R'EdFe2cQsg>=ZY Ei I{91Xg w;AUN< =i\sN^ț1`3cmTL[e :z3&=4: 8ǶwWz3h''Ǻz9 `ȅ&aWNNDgT엗Zˉ$8%Z b>y(ܡpG*Pƞ[@~hlnoNۖLoVTy$ƀ?E?}|ڐ"@#1I`[N?)gc9Ac r[ViZ-H/&:{;A:_ۉ@gџ!ނq|db+:HP bWGs7)B,񹜷i¢Ј_t9m}r3SW-[KTd"?>n"H_8462>vemUSA&B^A0dO3lƒ q[7lDjoܝ1>y}uvtx=4:2u/;g,,lIYqI^eΔ[PCTp7B#oڳki+НFTޮw 9( I$ϒkHr\Iqn6ʼnaG>>,wvܶ֝?tERmM>C;LNUڜ'\(_ĚkcN`pG&=&40*ߩ t Ϟ?hdk_Zs u՟?K_7>8N\Wy{IʿmkibedVONx X2?k"TVZ[}Ć$fe&n[Fg*[݂=4Z9YcqfyW\¿^u=JjhKڱ"x5;$ 1ar׽upqIy]|cɽYw9^( f=a]"Ik̯jfό{a`X0?(e+v;s{O4"om$=nghJ" n(΢1QiN᱁ T5}=ϵZ ym;WKpߟ^?ݪM7n] _z'ёKY N@gҵ.Yx+Gx85oxZ`u{jK_3R{gH_|tʉsG$w*dSsvB`pG0s; wHR^PawjUqݲqGLJ'v4,[ұ^:3-W>斓.lzv9vp' K>sS0*X1g_;X ww{ph٭5;SJp@nÅWc9KR)U-goIa.v%,:\Wܳ~uy뛏jS.;־ֺ9{U;γ\R܆gRAҩ.6-A|OiC*L[=}|s9y.ڸOQ(/n[/n[yyyUծ]\ ]Q@"^ڶ߇N0u.՛ RDYfߘ|G|wL{𿮠D(5m^QS3ki`0Umar"0pY良֮t#q{zӆ77e z o=z}˯z/q tZ_]yح}uPpD]] wYpOܸgw5vbqvX-,>;sc_,@m,k' HJ7S<&5voD"ą5W4:{ N+Z O3N^\?=(-2hqbӖ8z97crP#\Tƽ__^Ufq_,Ѩu:]]Sŋ3[mJ&հC'?^̠{W$/(ћѾΧqă csKk{U]IkG_r|׺ۇGAگi(gK@[kǏ|;*'ωGw9{Δ{5[ʈEYn`$Jlv^tNgra['V(Qnmw.76B|8LDjhU^ʎb%H]Ʈ=Ö|nKgυD#}ilhyyP,(H|nUn>ow&r*ԫoTY.ܛzL^KܱlwL%Φh_\^s'}tD7BIS7S_mv0`y̧~koa',>km,%a]b GwQ,wgWhBHB?p$!q7N__2F!WyMl}ukl{wsUSYCYn)v`k u걁Qc`pG0h빨Urc_,OVՕ{p?|gw50cinۖ魯Z-d\>mտv43[6m_z-<$vx_1-/*y` :B`pG0sfnoX4״<`'9MWLM 4͘Omik~N+2,zYz3}Ea-X[b( LT<meҩʢ:@Q,l"X$rlX:9!yMJR9YpҔLe';Ap[qMRa ޜD&[L01:%[΁@`P=o5"PߓI+mVw?70Xt['+M334;ApSSeW!Zj;8<\#L ]?OpG0hE'=cD^+! T5puf31;*'ωGR wfxpG0P#( fߗpI;A[Ej?pG0P#T9IO4%21(x"? T4iJ)Z@  -Rdq'& B2Ot{@mI#:yi˒ Ӥ)BG1 T5.J _(  Kdd>PUU ;AUN0E#~(R3it&6."߽LYFRKY$B2OXYnT+*:KfQ䅎b (pT60g[qbpthZ(VQh빨Uc|BG1O0L982Ё:N*/tU9yN`Fmh4H@ゟ)TxdJ-푴:yjA&n.tGu-r'jTpT6 Z:y'P}AR9Z.7C_Ej\2+x*OM i6P w;@`pG0hE@Tp T5.H wb@UNP#(  w;m;RPոP#(!U9yN@XpG0P#(  wYH wb@UBTp T9;b@`pG0P#( fP#(!U ;RPģmPĝkUepows@mJ[ʹ6gY[;W29EC*͝*l1 qR6Ks^.#pG0P`E$) մd-F;5mb|;" $V>B O ? ;m?1Tyv7OIv :[6=f̐zE<=( fO,#B(4rk}pFcI^k{KPu3 gs-8M+}N1x{`oK]rz wb@U>p'K#㰸)JGg: 'fqrR5<6?%bhzv ;1*'ωGw{{2e+ʁ{fQ[MpطmǹǷNx9GyJ"n|{©/ YWTt?ޭ×J'3JYMqS wc8֟vv 4jOFmM+o)EYqֿ{z=抎ǬRC;ۜaj}K[ksO< u#w1c|GpKłɮG1p,+9/lm'Vv㢒J˸勪45~OG^[9} .d q^?WFnh!@A samJ߳ӮODyaq؜S!aP/Cw $锴]I]'zD;yG-5ɩmXBm,n5nr pJ %Y&a\5p: rOHPPȭ wtzM6+ )?ϛ͗oɍemSBApsl)gIK i,꡿ 7.o4wgo)edDRgFx RoI?ExmD;WGO[UlVeHA,D2TWRJI v)+tV*T4Q+B^,jLjL,R:8uA#8}6App!vfz3@zhR``JOT@XiR",\0>0j\J]bh-:{eF'($`Ge,KeҎi?"bE4DjO mwѢ",Dœ;kQ|(n5em 3,apڔup\3;h4Z@,S[lac9/#B&bx-#oJz7lRYZt޾Q&ŅTj[WӪdbK}gmY)x"^$ գ1x t寐OyHn\*tv:ٹ8EgsOcukXL k6,:lX&5=g=T^XC$XZؗz.@Uw_xN_RSۜFӨY3W-bo_bT0̢7l`Mu>K-rhmij]^K7s,{.X,]ݧSG/}A$|H$;x{w lJ;+%Vn2vwO\ 7Hij_yvhdo;Z+;RvͿu;3OF?lU!%n[q_B`6xptwue pl}Hi}ϣ*1{XhҰ+;2ӑkɼFv61AiuªK'r"CdžoWE$ݼ"B2eK2 ۻ7uV|^ҷ{KyvÌT.J+sgJNL[r|ҺZ{&"Hݼ܊w l^ ۞[ Y*H_P_Wޕ+7VUj0Wwl޲0NʺF*Ue[99(ܿGsQDκ"$ ~߿8)YOpaq<6-gu tYr\jl*wإ?Tܮ.xiyDůS%Wy@ϐmiD VMX,6!2yfLHqx.봷*nj$m:oEږ*Ŵ<6|K_%kcQKNd54`};1*'ωGRo~{dv͖g$AO `y3'uWW9%>2ቁ Չ_H߳NOMhOo)cvp_`9- z,e,c\ri(m/iy-en뭌Y^]Vw l,9_o{l<P՛ދ/|a׍ v`׳]p@n AֺeͦHj> $nVm )⁷uJ۽ )GO;Ww o)c 䶚s[޺R>1:[>͡q|N7-W"WS-l|%p1)@`nN!_ wIԦWR}oֶ~}owK^qFa<{6['hl W4ݩs>ѸIa>ju^y}auі핹 _߯qebٶ_?s-u7R{\9/N DZF ""Ӷ$7Vv~}uOEQӗ>)b"@{p ppuu`r_\yٻ^ϼ{+ޒ-eBk&:Z'\|K,g-gwQ1ݡxqK6tk奥N`>:mdxi8,/{gcR7/kk4ݩc`iLD%eN߶S9*/ [ϼ깛1"qpIfnO|raʺ\[| T5pwwOI5%5w>9ճ~G wC`pGS3>.-a>/UL\j_ٲږM+v?o)gɺYox|0slŷٞcloz`lL ܃CeO_VXB# ̭ږ1i~c.e wb@UNߛp^652;+i̭*<};wUWGzc+qx܎k,mk"H$giWUaKn]eIZe[2_3i_\hۇ7ݱ{hS=5ӛ/mIWcj{̈),_o;_<-WN//J kC1/q8!E}aű2LR=ɠvw?k"7RթRb|˩ glPw\=r͚cos)O Edˎ^~/u֒uI֡ w{7U,[tmq B ;~e^ymQSꊛA >Α٬kn=p/,y6k_U <;$/_ TM DSbq=aQNUwܶʦ{֎ -w^)">Q'6 u ǺsmwOWP69$%jd`T,._ǥbYHm՝QZ;Z:뱴1zP Jc5۹"|Zsx9 {VxOLYՇL{@OCCtLE"@0q &{Iݼ[LڶʾVrp/-5⺶N–H?w;LZGswʺ/yMOB=*Pո&=.<>h}lb$%6ͳ`=ζnT2eR:T~b89PCrGӘT߰+B)NpY7_gǐxB4-Wt / kqdKeCD8*s (dӝ ^!M6,!$&0G0,{ZY\a-ʡQ ,7;8C#q(Hi)耠m8E)aI~ }l08WsOwVNI%fz8g>,P R`!q֝V(ݙ$6@0D }m.2|kO'pV Fk q1>vVC] Gzu6ʥ (/#%X\FG}@fD4X:?M App](0yl2|]=\2yM~ŌaǘŮYJc+rJ4-7l(U _tHm~%JM$I7&EK^hRT( MT&- 6eU46C@,X3YdRVy:X jͭx>`|jC1Ki |#b"*UaN)CePc F1A-=v6`2ٌ#|휬[#c):Y:v 4׵.`5 l`^zL&ƿ+ߒ+ ɦߵN= PQTV<+(J p.0 Z wb@UwG/`,[ZW<19s*/*$psȤgM[rYƎ}Ntp[KwSKWJT(TPHu]~#aL*n^LIѨ46ԛ gdT\iɳ[׶|U'><&$dܒ |1@0h:zxxv7W5' }=rvnѫ iwR >1Du5,et~ @`,$)bkiԇ8{.Ղ-RAp nhvӨԹLogLd&i_=5?pG0hE ;'kr魪oa0XX'oNy, s1,7״wab $] HicZ$"ɜR:Qi&'A:x8 $x~cr}p T 76 GU7_HW*$ Fڗga:.uo`Ǡ[۵قunvs99ј wYp)EiŋoA"Eo0Y7?DD2N$e6RN8c_|WNLް538~禕 , )1+,/R>%#_ńG-8<4|NWeSRIVѧeOggƩZfݖ]2 ƇKny.;862h,];fEJ]fx;̊m)eHDg~+]NZ%U;V\=Vmc۾?03n>>^5}6=U;W~7gxﯪ҆Eq!!K{GfLVS7ߒkl@;Rc^]rTŎ-y&)b+ngw?]Đv &=.H]zȿn#WD_3zN$ض|k;9W] OA*p%$!"Pesi%]]kbGzF7şܒKHT2ߚPнjϪ;WL \*{Åٻ't5t|%łu kC+ W;z\g[ 3<_8%P0O[_/Obs2L$EKy΃N{;jgZ\}c @$c¼֦jP}]r/AAj;; Zgo./z/Z;oY[9AU\ЪmuT2mJ.;;(/e"ҫtQMԛ^;"E{/+5\DEQy3g7gޝ=϶?^?;7( U9yVőUA=9e4U7먃Œ;O ^z1k;[b&ܟhשO|K߰pk~djxmhcecmIoNihyz$Oߞq4+q.EwNLT \YC;R}*"~Bntغ[N$;:m\ɡ]H["6>lb⚆on8ә ^Tռ͍ǃ轀L%$wsǙώ)(yaա/,ߵ,+1Hgpv-ZSƕ >W2096E,D`ý$b  ILL|sX,vQ̢֞QwҊC7NU % pi{b7}%{|&]U)*5W6ɤݟ%"_X$DMm:Q!|c{c[K\QN"K1xRJj7Ⱌ>i;A]Ĵ!e f@r,wi][0+-e+:Igy?NQ}p砪Șu5eE+79{|8W1`o2Ȝq\}>Mm½XWQ$UTb]|*fҢl}CsBZRW>=# ~[|n4pG4C#צ&]|pPC~΁i]헿|>8|/jnXʵ3A{+TzUm%oN_1;7( U9yV "8bS_. -];;c՘>?ruoiBkDZ]\S^k_/Yڙ_X槬_NrElRkC( q73Stk$].K_1iC"-LObصSקch?J$Kqsj(ohorFgLt]Z7 =Aut}UyRVU r/S[DVUXU[|gd`tIN}@> LVQgMNB@Mx{/nū+';:u7w($=s] CM1_ZvMy 1/\wI"?Y4 5l_2?RxmpZZq{z45&'XZ gOF^u= qi%3_qsi])4 r*RU./M5r*YkOyuuġ> (+-;M$ b 9Q}AP#guB&35A^^J3T2K*M ZxDd&SI$;H$.[ ;Fb,Yy>-J/a0Vnx+GG-eRi,U͞V? ;1q w7$uwIiVOY*nD"*Oɸ*9-CT V/]twSC;*'ϊ{|(Xa݀$2I_Mn}}5" |@dcوM=x,5.}T2'!2,%IDX<&nMX(@at\>n/I Pԛ|P#4 w-Mg?'&OO|W_R&^(MOSxQSSB_G6 d)A҇XP4C8&|V`HdP#4 ws'sKKtG]GaaL&Dd Y@?!jWdyNEgC"0@wdJ"p0q?@$h*s(Y3U9yV@XpG0P#( v4  w;1qpG*PCr@`pG0P#( h"5.H wb@UBTp TY;b@`pG0P#D0hk\(ܑ Āƅ@A ɳ w;@`pG0`ָP#(!U ;RPg| w~|ԅ,M_?K<fK` w\x]Xbx¨mK Zz\jW3B`pG0`ָ3 w<cyvEAf CTt&39bDbao*k.wrpHUH%R8`Qg{ 2:./~X(Y p\9f&N J,^@Rfwt?fUa2*D:666yA=)@Aj'x8X5 TFeS#$"gKWk.7;bx&Vx"_xUXxHʱ'S4r d).Y:~9tE=S=}sL6\? K|- @w٤};Apwst;%fn]WR2X]G]walk ϭ-5Sk_rD$!QHɧ#l@il̼m2+1}z lCxꅛ2٘WwJl2k wN. gUT>B`pG0`ָ3w+%k|OG_[F&NHĒeTSXӚv=2@[ڪ#B`aI+E2蠪[%cPlݞrᮬduLx$A[_cP0FZb /Jcq-Ql.hXL nUyDwUWb<vOӄ<*1kR|4Uʌގ׌sc'^$ zcXZp[ ZI8{읚H$\<5rL wb@UJ ZG%Q [{ZN%exyE%`ѧ[3*@TbWKW9:0pG0h 'D툮x|dp$/9WXgk Rng8x- /UHbRa_E\y%sw{]#]ۖ}.rnohGȺ7\9/A& :%Uonjj 4R( v4 7^<( W팺tڊ{B#/6mHpwzgTGK816e%}c%Md 19.=CL7U6T5/ n% >EYbo.]zKCߦ'RffniorDbw@kb[U)cd K/qr:mci~(]ۊ\r2 S;KC/ E=v氮ޱ1QUq wb@UVWVO.+|)O.zZ<@?|g_+ވycp?;uE|_*jgg+ԅߺPR[$_.R~^2diڙ.H/L169rwag*{F;״TʼY(!U9yV#Y Kj(`0͕2lȄWׄ^;R +ҩ%w,uae9*E]|F.ܵ IRkuBK/Jʊ)#Du6vPTu]3?֒;! `u]>XѰuUwZ: K/fLʬTQڳ$1>lwLce#modjӄ@esUb=<)T2ÅBe(wVZ8ZX,jm). \`z;z_( v4 7MQCۛ4ڴ!eTԔW(64#Sӄ{rܭ#rS]K8gioS`9YW6;z񬻿Ć.yg˗PۚX:Os<@j例]m(UπCӄ{@ikwAppGz+J4z:+j"U՚ /AA j ۅKl<, \K"!7:<:?ᮮwhnNKpqM{p3rGBݗacG=?`Iǒ[9eY1MX &9v w~Ay*qigAɇ w;_&]&?9Sqji0 qi,຃SzaJVI:otkO38/~fu*W; ;1*'ϊ/Mei芄& #:z5 |Pk(JZZ|.D!W1ԡV$pXP^ڙaئF0dhi ŭuF:}}bw z2loPbXV\fב;ApPT#+cT*YEY]D&uvɟU%S@3՗H$ , CD'SI?KKI3{3 R220L ,ZEbW^FWZ ۺeRu*d*i w;MƝ*u 鶦N0GI ҙX6մdcF-]RLHƤ[EBqw{1cˏUݩ7UTb1m:FZM5TEMRnhRW[T`Եʦ!vG7QVmkUԔ{:T(8|0+H3m=Z&Jt]Cmyix&WQTC# ECڣ]QSk# tU7HFfoXEM B O[ڛr<>mngB~&跰3 5J,ECs=OX]2weU^  3@A jisrQ>=6[mc:4:0,d1̖&0P3Wa4tkhRHRTߩ0QP4 CUW:VX;4"@tr2 luj 'ߙӄdT* :9[Z- ̱D&8 ku]FC{]C[D*1m7[&VKUB`AaUP,, ZF ƻ-蚁%m5 uఏN]ԀAiL  wb@UNӅീʜ&cc55=Z;ApbUӭ- xa΀@``Gq wK{S?dw坺cBQz]L6u3gch+)m3M4TArFy9O>7Iយ4Hp T54hUϥAC&Gty25 e5-UO]TFKKS& i wgk7= Ctec)q7ξ'܍e .Pg3P#(  w;m;2HC (!U;!ejH;PCr@`pG0P#( h"5.H wb@UBTp TY]?Ɓa2 &6." 2T(RHdՁ-R14)**(  `ָg}7 \1®y3QUiˡp T5 w:hP"W irFKjr±T$M.6iJ%X V66.yp8ec{@XY w bi`UX óY;1*'ϊfzf4:%5qnhnkL /JzU5Tݮ!e~I2Kq_:Ј {VW5qGUO_5e 3~@`pLXjfn>?||j-:L\*څTgZ(h+k@h!|eT*hjogp?濎pwvPRfވ5߁ ( h"5]COuK3. 7U)&sFx,T^R>~ꊖN=?# {Zn͜+l<#;ٳd]UP'yʮO8z?cfϮG~߽e|"]qx\Ŝiˡp T5 wl?瞁 i[YڹG1bŔsM\wu3O%42U/?74+̅Ke|vcsĀ<+!5l-bqaVX$wVPii.;X˅;~n` 9\,E*rӋcaVm9rvj}O-\iNGKwS]@ *Z:@O K*ԵUZG9yڪTFS LuAZz߹]&igȇd@>:$Jp[ڛ .nˣ_sL ;q\Bu-Uv-G&y8d!5;WTJj8{9x:ה5tK2c }wpo8LX[H3c*[:@TW_` j RSfZD@ăDW]62 PQY^\  XQY ȴ lA 8SCAvCTs3_,jYؙ*0GGp@``GqgZ+TԕskJ*44԰x܍3 k+ -uZT4]pxwLJL$]ȖHwV-K_m_ M J&Pw׋L ,lXVC_ \{x7pb!k}R:A.,2KU[E@ zoWOOӄ; sf(*rkߖ']ȅ;Jpɲ  }1lNIB# -5vf}}tŗֽvpgPFV%jj 1mu]w5X"U|kihO~An!i[iϭ|D}'a)opי;Dqp& }mZFUeMe`׬ ʛsn3(H'?Yp&YYb1X{{|44;1*'ϊb]He*'Fh +`)7wD2Y=#5!W&y>WU٩T:J$Ħ.c`(t(]= /3KA_z@_/.s[T-ywZPI+-RRa<+#Mw4[+D50to^ʹ7)ͯ\1Og\|q8PWc\3|C(!O`]on(έI"U6y<z512crB&ťS(H _ˣbT\DҵU]m @ViQkC3out"?;[}C=Z;--o`(AH&w*Sxauu`vo{_u qw[IPgLvֵY"=(t(5]H-o|LN$zbΉA_sL*빷70bCʨiBw~Zm*y˫}1 莯uBZoW|6$PQSy5:ZβE9,%M]uE%:çIq#C1$!֝i&Tvxh0'hGW[OCMkಂjum֥A͉-wn_:y `W lD"8 i} m𕁇~8-΀o -U:Vq&Fc w,ˋj|3ȿ<ƒߟ|y#WCB~v$P2MU%u*xiĶNX=rXu5ZT"d;y9kCTTkZS >lC؁NG)$@r}WoJcES- ~ Ɵ?=3<4X"IVlaOA&)ʙd83 w;m;2.' Op[1g_’ßiRq ^`a)Ku?Se%XU:{T4.~vjz? Jv}^QL^~OώlH*~>rO(O.m\MZ̴sko'SKVww DP?CpG0`ָO+ܹ oʶV_:Ce2ֿ'<,M  uYbc7dc}|*xjjG=y-׎bе~k^*]vAH^?qro0t_>y=U~ՔΤS}ļrfq]ѷ_ៗ@01>+>5PCrIc0{+ [Hy#Iҡʦζt{:lXL fV,#5MVvjy̐E7.g&휭l-{sR DE*JC}IZz =x|s}[Afɴ'ܵ5t KZn)򆦺6t;yj\tPD$-v/-? -I&0']IaJr+{I+>( S7qY&Z:W\N*/Zhd_U"W he7ִLDʰƥtwQ7x\H+U,']J~; 7y4=nݑv|a-2?p=07,JAZ3M/vqHO1^2ɭ\*=]m'%>c( x/HMȒςPb _]VXD"{q8#Ht f q(&. WTփzt z:zA%Oz{&A[,܍m ՘ieKl v2\(C$bH׋ ,t\G3|waWxiw5K[砶Fj2s0K"%Ӳ"fW^wKߍL_\o&SI,FYN)ȭ Z壪Po Ciq9"xjD2'5%odq e-:ƚzZm-A$-58:+`e0. LgRJs+Z.,۵X^9.A} no{~!7xGRHbP@P55,i;1qg]MYm @/(J\TG]U"$f0kOSGC|%; cci+WPx2}-ÜL=Ӗ[&.302`kj_+`1ADbqO_ﺙwC(ZZx,GfD%qL0 ƎO,j5'ς^AOMl逅M]B\ΎSWXz33sIe \G'˛/e9[gFOĀ<+ =윽UM7f=q;^_b&(=|ex1'L/x"tCusʕ̙?1=EpG0h 'E 64O'_Jo{{%-؉d2فoO>qHY /pG0`ָS~Gn65?Y*JZ0~TIe?}|z w;%Aw6k~V;1q v~ȭ5q)LB8;˧'\О=PCr w( ;AC@``Gqg~I< w;1qg~a;€Pg w;@``GqpG*PC w;1*' gw88d3ht6." sd.Hlި25߁E*:Ov4 WǢAKdb3dHdb@ۙ*Pո8/p-tJѨm>]( h"5 L%I@$|Ԋb(T2u| ! rꠥ-CO=Cpg2hWeW4U|*4*˻۸}iSۺVVYhofge|TK0D]'/]Ϛ} v4 wRq‰Ǎ?.82YD!%}{/L%o|c,Va!npQ%,MVVbG`u0;Nxzj 1 FY-OgF?$1ƥuv+< P9˗G;8y){3?~0# H&JRdH]n'N"Ā}pXI$^?-{.,Ni"]]a"ҩ +OjQ1 $p4%L b"89bH*apqaNgr8wDN ĉy}Ͽ~:)X'HLyQ.op8otonf\J@pwT ?4&{@@A ɳRm3]ͫWו5 X=أʼntMnjqr~c;ļ7n[_Ǐ/} Dz$%aNCa:M u I=_Q?@`P.lݜloVԁЉ##26^?:wT&[?8ljw5Xo0'ŝݽj, ?7pՌ\_YyugocduNUmӜ/ʅׂ6I9 K{XѨa({%)g}L(Ky ">&_b}nV6ƽ*v le}"+RlZjnM}ۜ/v4 w1M{sS${sBj`L #>$n`a|nyckcsֿ*)+(" wJʠѡQUMkDԖH(D"0`fh4&=`WCFƎ^f<2[X3g4A]#zwPkb"~qwk8>{M_X}-}m9oIy, ֩ObwГ w%jڪX,L;w[p`{܌+CVu<@"Йʺ5/ը,mg.@UNFaQw nId *reXJjD ;۸9~;;MH緽;<2qx%+zU5 537.>C#&({H&]JNݝ;·twzyJ9[\Ͼ>+^_z|7?Uxsoam1q`u:g\ʾ2@`P.7;|[smM5e%fUm]]}+Nl:84B&RoZR^Ygf?W/tODnk 㤛958v݊0@{Ɯ/ʅ/+nL1w_ػo0ɄB TI/PRКW\KNp-,mB& ( w9;;;;{Ů*{Ġ(wM=w} V6ߛ+~q@aD*E~hOs:N&A[r^gs no˲vV)>a>6Nff=^=BN]狇t_NY}N^NA1AG~:/AK{>/hrygSFpkioyzיk3$BWK˭sy?}_-=xJ%+0:z8x{^˺nn,YW nooh_?M/-[|}o~cd%>3wC&8 rT~bǷ:ei2il{斫W&M `0P퓋*P\9m{+W>X#kX6m&-z'|m5U7AKӡA)!|jA)+:zۯ]N+K]<0գ{:y61쵙/߭]Zs/Fpx#/i秺o'/ont-]*<8p^-O؎XYyڡ[ wݦwLM̗ tsnH)1c[*U Ƌ/,k\ՎPCj0\RIc'iNL{o۸DXPX[Bښ9G֗~¿?;k/ﺰW>pǿd*zqo~w"7wLZk\&{@q1vu Ԍͪn khe1O_hi D2ś&W9;)+o/tr& e fJ{Ս#LGW/LZ47&l<H$I135{Ξi^$F%<2eocQЖW8q%&"/o.fQޠ05rL"/o] _Yp~>3wFه<  21x.{Mr.{7{^/¥o,r,@i^E~ECe?<شʢ,? gtݾ\ht_jha~lqTN$2ź)t?4DL7-|FT:+#{٨ `(EJ%eB}&e8 lbIݒkR wica?iD*ują4 p K7\qIKܾ8;aD&vs9׎\QyC_uʢ7]Jqu[Ooҧap?ϺYI ziEkh<^zn } +ޮ)iZP=m~5Um P7y~ !p{AΎ13o_(W ;1&G1DLh6mH}eCIӏz9càO#U{zOӂN d"_{ pe mNo uN!I6TZLa( ʅkK3`bx ܫV.%YL}Y~Y|鑓J}\S{`&7VqT S8X,ɋN@w{\@p,]ݧ^\.+$"x̠S7,Ov_o>bce-boPhnjK[]m̤ϙ9dOou*<ȫ;0 w'm]/}cqWswfaaJ;5x"@ƒqlsQkvW Er 4˻jNGό}|Kp|ƺi nL?D$~ s8nfmw>.N4^N95+3$6D"S=_p$-d0Jo.~}\"ӅAcPS$r9=9fzy6G~:Э''G xx%e*TN8=;y:޺z.jKk.GEΈ*5όZfV 8|\YW^_}z͟W\-LJoAᎶO.@UrkqbdJ\XBw>,M,_\wcef}n umkjZxqmKLV]~^CK$χ4+ȆEE,zpA?`G __9<:Su},m"Bd )f?]fqzrɳ<ENm퍠~Aawř5Z-97i)s~k+cw7A JVGHeP">$ I-p7d{yUvl0y`1{nGo :K,>RtB>|XpL/0'J hhH\`ccH,P1@1erpK- R Ā<&,!+@`pG0P#( h"%W_Y!{P!W<[8w 3y Ā>pnp;1& L΁wAf0)t\D ~ eAPԘ>D2N\d< rD"1 -WV,lQaDG]M8Rb<*ffT|}PVy?EJ.4( `m}r}2eS *TU5yL`"q yPi*H@rOWM +fOt ㄙ%=[TfOt ㄉ5] N4 ڒbL“&:@۹Ot DS38䉎b@'U*x"x'95с*k;AUM)+,_*$,E$ J>" *]&:qġck68!%e h"%7'y@&$YI|D2N\#u퓋*P\4Q~ـ3с2VDG1~@`PU w;@`D-P#(!UɅ@A c w;@`pG0p`Ж\(ܑ ĀBTp T1;b@`pG0P#8D0hK.H wb@UrpG*PCj@`pG0P#( h"% w;1*P#(!U5yLu wT'm$ 9bzÄ:Ȥ檦[7LڤiDBgSgYn ECy/(>AqS8L"lJ}l`@H+G+1f畹i*i,חlf߳elf,J .IOG#)=<E޹[xJz\[^!NKMKH\P=}O/8!Εfv2B䝽%r;fVkj?"&] OZXޠ.v! $c[]3"LiqXX&"۲xk˷/̙:zq1p T11pqnvSn##*2#sKEM4<\513"[:0{ߏZ!W8{uvYwn w~}h l?mbch5pwqN >x)(W"߳Jdi_;oamjic~BARgp@ᮏeKY78repr=]̘t搮@Pid]X6Ծ %HcЈ$BҤ]JH5r+s}y s*LlrΤxmYN6~.wS"x M?lxl/}`=RtR**[o'^$+nf,KλRd`V+KjO 2,`Dc0S'+6o i}?_.ڙ<,фǖW.AQBzL[CH(wM[ wT'ma Lkw3r8]KZ. +JnO_4kL]9+{ǩS{ZiLZO[7͓ͬ$wv@YbNoϲuus;$~rc #Zm;3+A1g>R2e0̮tccQɓXǍMY3$tsҩ%5/|p!3{}fO[[GWSB0~]ims{DNijo_C'QtL. w2nkx<.6#W{`p T%wnknr֚&\ٰf 1u{>w yzO~8X* FERɴ'VZS\UPыz7nu@,$oz4nj^;{Cy}YC{̗9_o??z߹ –ZOo,ykmtI~_ ngT219&S+2֟rV_3iL2r|igvO7m\ YTp9bӡoGOp'H I8]z$ڝ%#a2 bnby1̌ɩ uX &:d3($JSGAPCjx’2&˓zU+g, wFճʓ`@W\RfJRKk]R'>pQ&ykѡ-җ&mbO2F @̹o;{8E];s+ci232fTܭ=Ԕ5Tצ/@F^6 e+НF脻W}c#2D|}%eg̉kiw=|D\pyQucuwݑۿ:x0XRFǰp7dZ4}7h ~?mt/ DBui˩}"B2O\RAɞg@*"p)9wws=n+}g%e"}C&W6Gui'vcЂY{Y1sߏi %et w@77o샗ĽA'%/eOֈER\+s>P#8D0hKpOYjn?d1F7N]lH])e 2L՞ݝ=c?٧y̬)=m=~H\X}Yy4&mkous=LH\(?H>}Q{;VV>wg!Q s7ǘ{FQNaplHlԾdon޳SUTQnbnbᘵ@8{ا`@/tגB)܍MViU_Gs若z5 Pх{.;-<ԍ/ ^;ՅoR\J 61#xzdB?v刕%8qت5UMC |wFk37u4^`/~p&7ӥ/gD waSg@ďK8pa/MWg @pj_EŪԪKygl(//u|'7RGthvsGOp13vX" w_;gM#I,=yo))NJ$R~PCjxn=(_Tq{yw~s@vlӲ* S}ۗ_|E.*s1`ҺZB_) ts)ɯ>)r XW5-؎l(ܡpG*P]!WJD}?3(IsROG_̌I64)yJmbc#{jZ;5ykqrkG+Pnd,K[#K[ڂgB2QSTG˶)I[>k0Y"ʨП?m (V[48Xm\!ڳꍅ׊ۛۥ/K~.?|JPGsw{sg\Jtu(Qh"%W_{xM .+367)~ĂTpzGhs[jc3;WgFYוZY\ݙ@Ⱥ`z٭R+GkD~ڝ%o.;w;ԧ.mMF۠eYn xWemVMZRxB6W6qH ;B^†zP,J*o\Xy^԰ K^^x6js6̭S:zZ@m]Z|hjf8j@ ÄZWRy\T1fY}<^V.0&p T%֔xZ#msgKκv"* Z(]}~r@tMs5tt7M+r!?$ܩdumݭ~--|`KWeo_4]QC#ӚcC#\?9&8fWb[X<0;+֮38=01*m Y⹛Zky!ͭ/Wd2}M2^]nNЛ'B)$jkg?qk++\}}P8|ez$  ą?ep T1p)4:UX. EDѪB`de6fJ?hd=+J0/B%v]xASs_74X[6jþ t%8L|J&[{sg{S]_O{DN/O0 0i`nD>Ng | Js%Exp;1Á`,zW[Z t%7K$HdnP0XoL*A9b(9{:t6wx _$Iz& ML]"VUT@wHR]:tz0`ޢTQP6A=8~r ;'_ Jr' -"|h J:hL*K}]Dcl 𠫵4sIW-l3%DW6Ξb|/C݇OQwsDƳA i1 ƠWK> =q?00Y?30( h"%@?3ҰF9o3R?3qv.v5euĀs^.[NlznwR떔yf)fFid2-2sB;9q75 PCjx’2]pG0h@`D-Ky(!UeI1p'FFZJ9QN@A c w;@`pG0p`Ж\(ܑ ĀBTp T1h0 & E$ ী/@VI8D2N9ءcҨ@~8D2NxOt8D0hK.LbqxѨ}k˿eBݺ(m\Tb@"\S)F-V [58A c'锨NLdPXqoݜ( p`Жܸ5QB jIi8q8䉎b@'U*% - *XdM,XA(AD!zhl;@gѰX`@1<_pG0P'\JUgsF1(V*= Dqz4x#ȟǕQe՘䙊 ;Z6_ui,;4a+I;z&˔+GI3A6V"i4I.ĆA`D-,ܭ,<P" ous2q̘YZ w*bja"{`Τ|" T #^@A Jw&ۘi,Iz9Co3s! }prkh4+ (!U5yLtci1~~)c~3jG ƊpFW )w쬐)ʚA#WBl﫫ͬM[?''o{ѠnۺZ[ٛFx])U&A[rGV<~kCBrvws63}:u ips$D,5f3211eٻ؊v1xG0RnNܤ<#ʙ `Pjb3}]0Iw{tbU)dOpR.m,m| .:Ly ?µ#;̭̺;z L*34l5~z:䖆vp=\Hd8 \S@Q\ ecm c0q\<j[,=V4j4+[s+; A. w}rQ;pQn֨kAr+ʩjOXw«Bp^j4qwh5U2Ү J9bE*px8yFD; $H=` ;ss$v͕=}NV]M6VNxl\z;Z@ -RϤ6 @' -sPUhƤƦGD-jhNS]lHYojP_m]ͽI![@v3t)ʠd︲9}09wTƶ0> }nf^jZE~}p_{by1->srЩG:v(m]~D$.BD|="Iw2Ae3trŦMVmQNP<]G$66g*UEc@E]xS `}0/̚qt>)_P+Gn%zVAX|pPxPL\ۻۀ݇.F皱1ÂC*hs<^-A䀳\_AA.vxq?\lXF prbٹ=9^N:]P|dӅm}ޙ{v [f,^:t wT'mE:XϊHQ*1qN{O`mo z~ս]J̒SS&szln)gfϏGm">xi܄nØ.JJoW.Zq۩ w}rQ;paPZT)]< eQkW H szVee3ZѬjd 95Ny%Faq.> p8k` ;D}/[jzZtب꺻~.:"ν82,M0c[uF6V`G<\f&.WsG({9@A c KʘY習_ ?:>䟿ݹ {>=62NAS|2'\x_w}|d?%e$ ҡq`헢gQdgEnNuMU\Rf_9zp_Wr{> X_K߽]5z ;inσ"\A7o̮*`1l 3GφK@~?pׇʠ,{;Ogˎo\ wIG<;cQH sҕG.)d32"TJ׾/N /ko#1,ms_N{XaSm\,X2QJDt}pϫ1i4&R. >[G\72r&{ܬ+y5=ɦ~`7(/kYsuBpsU 5w?0/NW_Y-}\R2,>@!W}Nሥ󇅻~-?'υO LSWL|_VȺ A!p`ЖQ{ƚݦyP(۾/RXlfdvS =?_=8/6)L!I%2m7zZ=2$qz -UuSj5Z8Ğs+f36q!?>5S3,)cfN_DŇمu+wRx6nMd\%"~Womgk78ve/\QzR$EVM1}K~xgrp敃ԷLLKlсO.@UrG߼Vx#UV  7{6NĶyޫLbdW7U]-E;۹L)J@ŠZvdH[WxF+Ǟ6kxerM[Ώx}%e]=ဃ=Z;;;z~ĩ(p T1;<^+3]nAٱ&羘rkL,{_+v~|,=p gv]mXTREn }jǥ en6JkKޚ}XM[yUEM/|l {u _GLӮZo_c yg 7ִ.Xpn_' LL3'MdPC~pgt~짳Eˤ skDlc?.)fX({mCߞN /IP<} PF Y[;1}78yۃjpjY+t+g7ey1#12뻫<ډaាzzoRaSu[ voj{?+N5=Sdfm2,|-H&bU^pdD2A!SWɠAnt]wrܬ[j:_x.k`{`x?+}WkWs/Fya[[#{jA?R!8 Od\qtO ePi>G7KYM̌{dJnkLXq? .wrˣskh==#vmgāASKv_uیiiSdh T%wtqkMO ǷJR2fZekFVTOW xP,IDL:E|c]M¦<lYxLs|AsS#O76gN_lƶ>{DtN_軞w^JK^unwGgӟ^[O5% xB+'  Ā<& ܍͘IK} RŗVv̚$VH(ⰳ%er>WxfSic]%"H 9z+~ˇnʥi b|=D|qkm֬ WPs,oΆd;Wk7Y m˩,F")kZ, f'^{n.`߁>X 9'':5nCkMǒ7g=ܗfgۚ,{wjiDDxz듣 WR|=3T柿[zjY+HZvJpG0PCfJtpq pO+zWKoD˔ͺ+OmdP"fHd|zX_al9cQT,vd" sKZ`=o;/wyqPтW꿙5;mM/NƦO v}kCECҵMm;M"P<ΤIIJG8{8N\qHbfɞjH(9`P"xϏ9<7Mz^NQHoW\w!Sɝ->7Qi Eu3ԍԘ3/g,Ib0z:/+윬g̞zhkt^PZlsKvʼه.)p hwnז7,0c45vW)s_{l.}!L?rT= Ww›1O\O[<IS *} FT\>u#sejMyD,w+QljN W)`u/^?^.(H?F{'^q킵C+U%ߣ7 PQ;ݸuV Ŷ[<§Қk9D1s<s>^ӻpC4ż 5Msϳ_^BtNXE%S^U%\(UJKs@?R+ GhÇj,@T* & wb@UMO;( ;Z}7N4 ڒ;70p T%w%e||B%ŝsi;bPU w;@`D-P#(!UE#( U5yL`b;CePhL*L."?%BD2(A9ǂj;с润؊B_D2NoOt8D0hK;@(Rnbێ1^G]Zm\THxKgbp&޾'7Gwɞ)@`PUffl#|rC$*H%"\S.AUT!u[Ot Ì-WcMt /̟( p`Ж\;c27Q2ك8сu-r*8R>U%^hjڒ;;с g?pG0c| `=8&䂟c韛$RAugU[D2Nx|l;y ޶>8ȿ1Q@~D-}-idDG1eJZR zYgO.@Ur)tR@DG1N9j{k HE;AUMP#(  w;MBTp T% w;1& (  w;&A[rpG*PC ;RPU w;@`D-P#(!UɅ@A c w;@`pG0p`Ж\(ܑ ĀBTp T11pwp>; qg5B Znֶ*H}qL7 {s;'s\H=ti-.0΅bxۓ(Yci,ݔ{m m <{I*=ƀHpG0P2g\:O$\:|YUG*,ȫ͍oI87ҔO7-- lNp'}rm[7ŌA٫mK4΍Z75( ,4u;OP*#ٝdO l`!B]b+mv|RsQ0;-ajCE@k@```s w9K7L^VaXʐ?1p,ej`嶓{IxͫUyKtZV/﷼ճ;ZxpbqTTPp0ĕpKT=,<C'=a GpxCS+#tPCQuh;LeXݿfRjKX S Ӂ>iw>;H 2c[gdDE~s.K}K$*OP 9޺O8R|rCO wb0<*~2:e%nnc2eIe)RYL0 e\缽}|,DJQNxfxoDKv"-n,,K;,`9Յue,f֎V8<ґm=b#}}TJUafQ郲l]/'}M"Qٹ[_nWyanUiȴ1K=C1̽e9斶nlmɾuqҿ8Å[E9E^ҹ+7E"SMt&'K-[@ O ?xvowgp4pgYXJ`cB6#>ƕ`˱9Y(!¨:wH _Ig =e2m{\~m+nU)7NO;m1H|!/?J D21r E}5֐"iխCRĊזgֵn`c)7fm-:#  wB_~#_YZFd65T4X9X񌙍ٜUywA:usͺ1#"sfF S݋. sK9R\ ij)]ڋS -  FGJJ|mٽLx . 6c`Գ_1+X80!F,x[Ce#|e3dۚShnN]0 BnTB[vN w$ ̾wg}7kњ"4cqBgk< t`؅LY}vNwI9uϗVvNkwjښH@ ~q-Vsp3snΜXQcf,"|mjy-5iɪWˊr\=8o{Ӌg~z\/Y*(y37FrMe!KH܎`N؜ aT;;-mf7f,mkO<]6;=iWV^xI?l_p>Unjl- (2\~J 佺ϟ|3E[Voiloqs-p ɣb}ǒ_ܵs ̋I ǥ~iT[ɷgxR_wߦ6 ̶^9Isħ.)iվO_3?7`*M_qk=U@S3KS? ]]T5M kk-.*>R w` ia$*)ROcQv1We]|] )b+?R&ew 7SO)TXٴ/6Xf$rßױ_NZn릞ىNY\:AX.J4jwC THQ șFGP]G (*(lmh[ᏔYuQn9eK/ƠE[^=6 z0-X`Å gFD$(~1yփG< e3\ &~/P +{S3-\ ebrʉhWB߹~f񊝕e]EB^o~rzOWghĴ3}2i[KDWU'~Hcs&PCQuuK.-&Åae Uod=SQe)#KgDLfumNvWq ?r`pZVS :G~PCQQFm|oC{cGUaN7)>kncv~oo3t`'q>''Βu:] NY=;c=|ZkcecQvsdR9xpQ}yܵsHX j**M?=1=ʠ͏_1K8瓩Kg79z8^<:o ( %.qB+klZ5kigvD`JgK-HBА"\|]0$EM 367̘7bBcBx ,93cCƠ鋦YY2 [_{L6 w1cx0ޑƊƇwM]cnc. 2 0OG{"F[[R'bnow/ރݨ&1 w 5mpx\&  ` U D ʄLoJ' >?V+^QMfNH^ OTI1Yf mxvUvuX:ك`,f\}\8i.Twdݝ-f֫6\y@ MTFovuM /PCQus wS;$RnӶ+nomͿpv5y^GOKyvˊ]SeX 呩d9SPiZö`HDX VEZ=(1 wS+S Cz([CB22pBB0͙"Lzl҅D$U "D_b怪jAB!*B#+d [ _ BV:cGv)K fdHh X rA 00hLM9J!W$!k^^÷x` /⋨t*Ì!b[Yh57d mxU\? b?vdj0:U/@G`1V`Dn (na><sdD<adb_pG.D0ֹÅls+JcJ2-t)  s9&jhmPȻښx r.`T;D,,ُDO7{Q FШs \XxJFQ "65ށ6V)*w cm^AxGy)B[-hj%ЭXp4x; Ubh)i#8igWx2Z^Q\UNqUu&b0 \D:k;@(UvVE:w c7m;x;1g&|9Q@^ D0ֹQqb,D_HF@ƈk2`;kTUx(c]ާsȹX +; wcT9yT@XpG0P#( : w;1UBTp ɣ w;@`pG0D0ֹP#(!: w;1UNP#(  w;&1΅@A FչP#(!r@`pG0P#( ,4u.H wb0΅@A FGx wYzdB*?1Aɥq @`pK'hԏ3"HdRIn1w.p3d[[գ*J=OćE14};&1}pwuQH["Y*2>֒2n VG/{@C+[O Ft:;ר0}IF9zV<[~~>!lj^QZqRL4L&1,que/ɳ*',sּ :2x!]qynQ|@>mI_MPtHMQu]i{8( %kߜK۹,fGGB[>CZ3a䭱XKv-.Z4T}:# d ZY(KC(025BFWwMaNss̯Ш5 qz e`ʼ򾮾9_#^n, .^M]..XenU䔩j0 F[y2lp!3BL~_^h'GU lkӀ)Xק<<+WYP{[&H|PaV`V-W$B```s-ܵj-n)(e[&z`KGƲ暂:+'K(4eC`ՒVWH;ͦWYtuKd*Ƥy? <(` ӋA;$ 1rv8) `bCi+ʻ]4Ua=(ͦweN匨m,s}. WOҔ'4>B&Avq;!i{Zz&NKǑxJ.;5[(aƮS % h;+r/eKM/rP#Xh"cܑ ]^qpp_ٗP{p!ō.wKRE ,,d|w3ۮ~Ui7Oهw<7.PpR4rb9ߧw.6khn ynkcmܒ@ޤiWVU4.OO?kBumKgg7n=:%83@@A FչC_w3~׏I_ v,󴌔m+:z`b,w"u;yiJ zk^s⛭+޼v/xq'm}و^94VUm̘ odތV7k7?iiE>@k 2cg]{ڡ7~]& s/qu$K >.'~Ƣ쑟,`T9yT< w ^)S?*qL3f}igWo' *;R&fEЦFb}|VB *[<<\ث47ɶK5듎DPķ.:*{^MpG0,Mpv@5UB߽,jnd@YR=(? Hh-5 D -djJj(<%T!2|z;{A֪)֨5:eҜhH}%m AC{nY]4Juu^JpΫ ;:܂٘≸^{Y^av}ܢR[WkPe_q;+v!>bVyqfikVF{]Oh`!n^"F˲`Fąm2S>!3yR@eU]K[wk[O[gW_ObfL ᰜ^~oL"͌khV%`T;Dyyt4jB}pJFٿqa#N~T&5Fu)Š~A(TQ>befSpwN  8,./V"r1?MUr+"I 6ؠ"ϔeA&SZݎvM10f2LkWmanc6-pvP(eŹ* Fc Hg;1<*/-Xl0xB^( f©.{y%[9`bK௝{4/ ( ,4u`j[a:Ʋq %0D ?_GQ

w"L&R($ߜ;1U\h[KΦdl(!r@`pG0P#( ,4u.H wb0pGP#(QQ loi0 ?4ΠE$s"p 4:R Ha`g7#?qU4W5C@`p!NY t%pG~<;Id7W&|VYGW6ê_9s}j=fzy`徼CVM$*󹏸'8$ʟ w Mcl}βօK nv7.{YkT qYB3%M/;JNzaKSC/' 4n5pCnՌ/2s N$,Fi 6bq~r<Ͱk0 N4Z`(  X `0#q3SUJ5 elCtJGKא{6K6;L*`3Fkj"it0-h *Fd ٧Rl:؞˗e(4OS~C=0*D&4crt:h@$(JV $}=\3`;H&ZXb>WәAy5 C>( oLOPљTKz&wحݱdG ٕ$!ްbǩj U `!Liu>ז޼+ ],^?^B# H=`$~D!TdE%9M`kE`S38#pr_3BA A r Lt&dH\ Yܟ@#,3&H4, rzZmrv᰷.eX>H!Y[IDZ#'&E,L n?IJa<Щ,K\*Ɯ@&vpR9x,„L2%QHl;:Ա,S/X,Ԝ/Pf+zwnzag֖7 /wL1X4wo $6\DVD&Ѫ;zڵZ β`Yr=<og@&9C`N~d i%-]--ldL'wQR5Ba_lfeKA`\<wqH$LS_הկ{mYGk78h]eSG3}=JU4ֶ4T,Y7L%wwp*~i^5H Šsq SZm] psq7.f<%SvI'y޴ѠED,-)]2Pǃҏ>x^"#@NO[+^[nds/-\$oiu| w`$Y{sWANɊ- {{.ɇ/tP̘ ofLq~@ws};x 7wnhy꫚JUΝG-h4b@}npS 윬{ Sݿ[ea_>#-ˉƠ%r0ynLwu)C*-Xjnez>GӘZ@ q rKl|:z@'?uIDpaԍ *o=ZJޯ,mL/^;G.Ws/] /5-c,6xZxKXnfyHM!RH mJ[elɛ+G.Y:لMvoF9\g?}:s#(})4}/9B[>Cq aMT:ΕL{'gOc?j#W̪* _ZPj_]V?.x N3Kvi~%Ewv(s䁑 n>`l-~X1/1A6TZĞ` urձQ w s'[[PTocN,M,eD"SQ6#׹|o.md1))WSXw-͛XVL&Nψ una1}O ob)?l2HP&R(UE7–^p;%]_Y+A5 6; Vf6;VWvn_qELjqb"`45_RcWFՕ(Kx((ܘiajUXy%#Q 13Up7#Ng+=RժV5x|[KSySU6-'4fPCOY集Y`т']9{qΤ-4_RcNW e_&i4zu{˔|_L1?CZ#^O}HDWn_lkה}@w)7֥̠{i)-a2EJjqtUF&FV4+ζ!{r>2;V(3Sc'vvW,X[6e+[KgW̚y!8%*t A}Ѽ5PC(`1K@22 ,+ʽ7BAytldJ*/[mY{ߧ ]~#W ozcH}Jj?}zhx`\q!O3ۛ;Vx:ZYz;5uhA8tm8+=,{Zxf jZ@)wȳ,7-1Jƪm\!+p*H :YS>,U EkgV["^/?au<8_ ~)¢,X w W}wb9Xk z ;"ME͏QTr >ܚ,[keIfA[Ǯz۾|ǿllWyN\ NݓfB[>C\0 φYPFFU,PF)MfR4 PR61S 8А[GM e_KW~^޷R3&N .Ӄ;L0AhtGKV3Gotzy;]:Jޱ$?Z yY~!^I]#&B` xN=^ESot"ۉJ#߾&D;}v?VȕD"Ƥ.XwMp/n>$Qw%̮۹엯7&:7WhneƠf-1Ϡ>sAOD=tip psq6w $[+k#ܛ篘uxZĔ` k3 ζ:r@*+,h,p1x\^ӡp<P\{*ʚ y}| S_[)+eTZT%zec brpPg%JOY#˯ RV}<2<*?ptz;``/2 .\r_JT_g'gu#67 XO01 I= Av92H Z_^_djtWy9%u֪Ǝɳ&:өlkWo_2x o_~xpܮ> ʲo_JeLP&lnbV}WSRY0)*`K,i`2pF֮G/ƄNK$ ?:D/.ޭWp8D0ֹom]+:˧ozyҚB}N$9,0 BTEs99} )&nD,G:;M0g|Z{ xܾ#iM-=}';Iݺ!D"ddr ::~K,o^ $)3I \qWZݩ oO%RD"nWӦMo/+&:F f86~)aiyH@l)?,rFPC\2D{9,MH%f, >S(E_|R w|HMOnh3_ϘHX`N~ wPz;e韨ZKFs`MXB o]oB _Iq 5NPv?8A?PTh4@vXݾ5<` N_GUk DZ2cƘ>'L 1.iF)` ޼D2A.UnrAY ;'k6ttf0`GǮi y d)[OAcp0DĐ1LsT*7֟)-r:(Ͱ_Q3b~Gwkiq8FWB jj l'L!3"l W R cώ'4`(ῼRB[OwHd@ i0.< J0* v9a>PoFP&(A%oO)O\P(uQ d7#g<:{]tb%6)l)ػ( HG{\C(X̙ss~/9/9=Ӌ_WɤU_~ *Y4Vhťc*qw#>e[H$?vέ_'Φ]0QG3T FQ^.%PL> w4$ᮀIgG@Y" nZ'$"'1T:"]&8t}?v dYWS?;||(P9{ʸsg2˜a|_q;؅'GpG@Sqp3htIP zK>ZjY5@|'\S iz~ Xq@b,ܕkD,|Y&JVl Le HPUL'Ie\Ou>8L$ gizgoe^YktsP*B(yC3Ο $!M4!kUOsT=P>-& w*&㣹X,>?0t~F(P ~ lg=ہu&vNS{!َ̼L@MZrL\hlG1 [OORO"+_V{g.@Ʌ.k1ǤMَb@bPM '`,\(?Fg;@&j 5QaИ@f';?{@<Db\c- 22Q"PB+F"r{ڙ)PraZjwvaoX'oA jT\պTYEH \(?qtToᙻ[v 3c/ZH  deevghB ֒Ƥg;`?D! LYA \L +\.w(&pwg; wA5y*p$! wAbp4ф% wXA (; 1&OЂ; 1HC &"+H#%Vp$!@Zp$! wAbDb\$a w4 ;Ġ<HC ; 1HC hB ֒; ᎀ\XAbpT)܈Tڪ K_' wAqwkhmhhoT޹e͜k9)/Ƨ O 21C}CuMuo_][$2I,?gY͵"St>&N/܉$bHNa֝"{967`{&1/-܍'ɂH ܹ]ˡ7cŧ;E_qG HKS5-C]{p|a>.IWc0$Ѐ +OEy%6~P$xZQ=#pG@SN%SD'妴uqxe2DQ"= >{4VB"G qa1OP* N=h wAǻظF**qc%J@ H҉Ơ[Y\URRS f:+7Ǝ]2/ܮ-ɞÐtOrޒ8$& vv"H"/t;3/e>S}m&^>q*ԏ vxTS@< ~*:RTA 1h 1XKp~ljAU d2vUݫ*M 9j}q2Z' /sB@VUlY[C*6?Q{U 9_-"?r'bd HTou/6~9oo<P]YI"$Ѐ +S w",kw__O g߽㏌O -DY^@ &Odngs[9YW<q>#m\ʟU]:w_3}]X D # ngs&<r;=!皑 gn^1| 1L:sU PpJyOH,奺RZŠ3# @cR_KH<;[P~{ IZa Fm\b@?o\RKdr8Y3}pW5i\E~ڶՁ)'RD/05s23(4G;~n\{/{2|Ã"-čD&=Py{AWS/< s'shL~>0hM0NE<<+1 eb Z7:8%\DB(M] b O>Cp4ф%wnaghu0DM>wT=%xfEQL;y!c0~vTN%`3cpw$?_f!UOnm]&DHJdO*wͯ}aB3EB8j溶)jKօSԵTm4 _\a3^olߺ. {ŃkbU,>_xT_Do~>#KR۰2D"'ehinY"Wa1갶ښ \.HO %V& w2v+29LiE%;4[O`EK[ߨioY&l?_RjNAjzV3۱QvT;PM{Wr]W;}㼛՜FJA+7zꍕ/~{vOSW돯}t * ^O\zB{_s?=}G_~O? 1y.!=̨ں꛲Wyw,>'*`H,03>tr -kδ6vu) !1əW+_g">X_Iexpu/wBKSԹ3=D2: -sêгo  x;|㩗d+wbU,:auW?Z"$95߯UT떇KO %V& w*ޛ?:sQ>2d-x~W}N̢P<;/xa]{O}}? 2X * J"vor$!P |AxkW[NV::\b7Db`#I3XS\Ubeb?ԟk'DŽf/ Į.JZAzWogw֥ .LA}c[cy]R^-Ks.縇zIcQR;v5wk[" oW?`P׭Xy`(rpk_kc^1ilTnd\twq [~~?owowm C}2R1s;M 3&;PMw,pVRϸpA7]} |p9m4TD{+$萧;>g{azi5KUYܒ2|"ԗ65ܟ!HC ƅ8shTp?aѺ̢LW; /qH߈|߹>Q)?7o|N$wm|-$TOS7+8NDUtM&ef<`1gS溒:T:3*2&dG$nlkX`m*wQO2\S ׊Uo]2Yj7G~suse3_t(>!_t'!_ܵ"Τ"cKKss ·FTWA&>pƚ8%d]hK};A1s2j+[s󶉥!F$ܯĥ-߶(;ݶ~N _ǖܛ5Ξv5e .}{+avx{ݎƠvw[9kcBҳ=Jkk[\XuiE2iBvdd(wf3;;{}tRÞ}~huLD06~Y \L +~O w[s R9lqaA+'Jt ϗ/{{,47S:zh&_G}@bPM z{ ;[tմ5t*kID\.捖63t]=|!A(QHd/[W V%J]-٪୪:|FBMp$'Pesd*LTլomV63mjၶvl,IFZZ+ۛ۩UKF,,Դzzjj3`YTY6&H& xc} =3.ƘnohimkMM+Q%V.V$2ΤL!dL,dPhsgs2Z 2w2;*:TTx#J:Y"Hv.Vddhm٧ & w.ij7 _ kh31֭n,FSKFi =}fjfЀA6vkcY5d5D9p4ф%w%e`b%e w4 ;Ġ<HC ; 1HC hB ֒; ᎀ\XAbpT0ip(Pr ~ aI&$B*+9m%R@)3gNaalGx&&`-4"v3L.uSV9ہ"\; \L +\.W[Gi d29/(f$! $g")D @B H.)bi?Tv{!(lvN۵́;Ġ&`-:A k FT;ZV~_SrYĽ vb \Xr|G=Lf6v3jT -HC ; 1h 1XK.;Pra wAbPM $ܡ wAbp$!M4!kEVpG@J. 1HC S; 1HC ;Ġ&`-H h@Ʌ$! wA5y*p$! wAbp4ф% wXA (; 1&OÅ;˴r0{ZnOGP A[cX,yP hRIQ,L%bImEXq1&H,; > 2ld.KR*[& LT JD奵C&H"Z#v]#1HC hB ֒PkejkX[܃4nL" ,t˚=afo &wrژ"Sݭ733jlQU&of[YH6Dˢ(I"C}-+ i=;pG@J.N>6$՗LMm[Zj+\ c#KHX^Q;B &OC;{[ng J/,K+*ꐝxr *XNn2Xn!qc $! wejڅbFy)A^]y܍'(jk{L$Is,]9޺WUR]y +9#1HC hB ֒;pSn 7iq׼uء[]Gu+xУű]oiS7{my?"P9pش/>J=MMt:UmM^IH*l|>:H#%V~pWSZG,^z'N{GG;B &OtJ8z^;څt5|mEo7c=<0\SހlXI'vMt&ƘD!ܩ1&ޮ~&aioQc9 w{WWmJ8uU!ܵ4 Mz:kZxv[SgkC\.70VqL;8y؁Q\:7djiښ:XG \E^]Z/H%9XA ,͘,zCMsOGT*30mk)$kG  ݩ=w+@%Tw:G5%F*Ljjec53;8@MZrZY -jZ\:@edrS[CXgX2l暓D_ޙ,ܵ Ml E|QqfŤ*϶|ϊz&چVEfF\!Ml uU[j$\G{/l]rb:I/,f&ƺUc7[iwUնzX[Vմ @I]8餦U׶8v}SGCS#ei;Pra屄;H640u^GNY$XT)p8qKk]k[;yNcVזS,*u3UN]g .3@Oڍۍ͝k+G]w;Pra屄;_dd^brl՘m_|H$\0/V u?^m}Yoo:\fNAejzh?S#󉷜Kʬ_uV:YZ[M7Fd斕7]}u8 $Ѐ ++ܷmzߑ('H$|~?Ro)QW,)#_Ͼzc|8;y _:crkS+ w~>Tzn"ᎀT)[*^USرjGԑ$wmcRl D 9{ !|E`d0#ߺkn{ćOYڛh?ryR D"a.}c]@׋'8{ aVWnTרkY+Sђ2HC fOk[Of^*o`x݊5RT!vxp[v7t |d ᎀ\Xy\e F*䥪r5ZZoygL x/>ϾzS-M}=]#2ݮoVr'G]MsB u#ƞᒮR/`O{ЅKCC|Y$ЀjT=.X1sGGTD&aqCa~_瀾Fno63ہt-vN.^َb@bDb`Džl{ C=.Takg.@Ʌ%eG$o'ʭ刯`ΘN$kg.@Ʌ Yz; Գ w4<,.c{k~xL*잶P0JEd-_ßuN>$B&0ش!bL%:xZpo*t&mݮ$)јT~\\~%h! ;vKmCDWx~~ eN ~*ėUX]_m=PhdR0̃; ߎI< *\.g2p8qxXx\%b[_"@9o?L!q4Kr R~%[VԔ( Jd2eф|D18>D/xS 'Qg{cՓi-R|;O!'U=g} ᎀ4ф%w+gK3;=ɿXtwd!>$"|G8ч~ E$"GS7n{'{&$0Kku[oHpwd$@ o? Tp%V(ܵ5DQ#޽Ȣ1ѻT{0:]]($ЀjTL'uTSkHO#s 4}X>D&sԘ_kh jpv$L*ZRb3ֽB%Į\y!BHD"Զ0@oǜ{L%mZ0ggWb'gz;+F@o|xrMޚ3ͯ}܁ ]맩.u$#]؁´qNlW.w⮯/Ky7m1֢3iWNݬ,*M[yH#HOfQ;?Xk]^Vqvَ0~)7j-!&jq[rE7cJTPOV J NEEJb1-$z.<\&EK:Z[H TTPߚ@D*DQ#g38 (O߼D\?Gp [ L ^|Փi9k5t&J?>T3 ᎀ4ф%w%e,,,-3C"tv&n`ֺ֔sLl &?;܉{ۙblXݺ~׺:'Rzqf B=7&9j%IRvvϹ!^%Lo^L:Q[>T:Nؚ/a| wL3S$n`驭~րO/[X2|D"#?zR[O#j]\)0bFO<7<_~pG@S;5tV?~%~uIfRBik̭﮺/n׾v3;M7ҷp21w0>٘"Mw{/w|[1upW{i۝4tx5=Sm#M(+'Ll ;.xiQfb$kɨ/kz ڢ7G'[#`/;%b~;E r8[e 6CCO5͵GW+ >nGcwWݼeP\^F|Ѻ7fj?mXs7[oᅫMT] gIツ;DbG˶/Mѳ͍q DzVߟ\c=TJ_sɸp?SBg-}.G̜91VYҙ]}jpjZlL&rp074t$qE۱y^ɧtvo_GS`(;L-ld m_Oȶv4_Ǻ37<ښښ#=Y=OOpG@S@ ^ztεˆ ~|zpխuU7-$D`}rf[˾|}T o\Jwkwjŷ#6\9fiXb`Zy+1oˋ2&ٴ-ʹr/m'BݾYZUXǛ}w(g(w!GM[q~or{CdzG{cb7]˔JeJO[Ô3;v_\"=-H=@dš↲*u5E5@-:I]YAjMS"@R7a% zK)b 6-->gΖP@sr>n:imeAmIfJĠ2ӘTpQzYݝZ[^64NmIDu >C 1h 1XKfce;8^ YY_^noi.lp74[sˣvn&Ʒ.g- /S$2i_7p\.t'INF &6&Dp0^\UXIS^ںPՈKjb/tn&$ܱ\L +4-9o7M{3S eYpn04dx۞ܛ8[ ᎀT]uK~,+[W 5\GF$7CHTL<]m ysߦ T:Qe0#;UYWn?uQ8Y3_S_=LFgKc= zS3%p|==&lKo>>. l꾰k8"5_`D,9]_mj^ckM.\ >Q$ɩ*({?GxZpG<}pWf1o.1AƄ;OXժ(?:9D w3<0Vg7m#IE`b@l_*WQ! 9)8NCWVRfĺ-6s ҙ_ES`(2pO)T,)sp32,z]Pw{D򅊧=%'T,"@Q&j( CLWC3{.S"ꗋ3 2L'{:ML6}gNTGg{V ᎀ4ф%WY[9[j(+oWv)L! Mʼn$"&P0GG&h2aSy9 ǏϖG L٪H`ĻT؟pM4ƢB H.v 3Pg;BMvHϕI=΢-!7X;s1J.<]'!>m%rH#7/:r`6}7h@5y* X3F!IҼ^: [~}tjCEcAm O;I- LKH'INU@4NyAh!vB[8Z8z +gb#ʿ\ۢ6 xpYO{wNJT*[1_MKM][$ܑp$ܕ!SKG_9uU!]|ژtvg^u#]@3j\.W#'y^սYW\mQSusnj8o.P!մy̾>Pz̚ڒRp70704 qvJRGyZ:[ҙ4 w$p&* w"mRӒ=N4U7GJH^X_^YXAQG,hp[Z\A*gD%Z:Y:x; yk ֺ:8[SR0WvK<?pl%[$=7Envb \Xy,N&/`0h2qa<KkXۚ4ԷD+I}\,>OpM:lYQV ]76,ɮ$f yb2?Z=H#x qKKBW-̾KgR䆈_^Xw%e~83Io﹣_(&MZy+>%e6,%}S==Sgm~{#"g;[JJ_2073^CYSOVRfв[H#+H+,lM==@ n),6aC3244%bsQM*_^\Ӽ-g~L&aDa%~~T0Xz`oT*D۞_vD2J Z葛}ƘU{BfwpG@S}EEB<˶hz G#Opfh`Kx#_P aI. p_QY8&ܭ,--/N`+^\x41deȑ/Ntouut䮸f`QUTUq-); ᮌpN8l s6 XrS[SEx2^^ ljΖNonp 4e?E@q['snǍ, m{;+%%eTpG@hB ֒,GZ-[:YB77($@.'U?O ]|::UUjj^<.4VeMƥts×Ku&K|pYjlߩyo;`(XL?*&wr­$| gN^^!DC^{÷_U,[PS缲kMksnVS]fCOߝ=  $ЀjT N!~oV '}żvks P,.nsZ<TW^~*6' gKW̿ti zK_;\YƎt.hś`/*dLeҴ.RdD wZ(X=47"ʿgPόiUM+vz~[KH$k:=WM +H$h$ܟHC Sqp3it&ݼQkebX trnbbQd3gEjDإ3buZX n`vL`c``Nj iEPo SRvD ĕWػ)*bs a"fdhdVr*%= 2JuB5g U.UGS wp'SɖKsy 9{8F6m u+9T: pMR^qڼWRTC||kԪ~G;`b \X$I$bVN҅j@ׁ~L4Zq6߹h|ˣ@Sl xt0h@5y*& wkk'/gHeɱdrNr\&['[!Oo1OWJVj2p?$lfkM 㺚Tr%$S!q? N62ٖC'HC ; K6/1 v񭥯,o +?W&⏬~gmcE(b}}K^^C A2pPyGD( TnPc_HtpHּ̎Sq[iTq'B=c/;Ġ&`-.vVͯX_tvwtp>A{# UZfŇivvn3*Jz^]U]OG?Iwr쭒E w[B..Hj!1-sjBN|[f5o{pX빘%V& WȾPtm0iNfVlNg\SBz{;N֎N܌~S-_̱bʤJ6{Dž_|74$nHjz8H-gN[ AbPM{Y)у]T:U!S93;N5W5-Xej?24²fմl\y_/_9y ̌.O&寭U|3v~-Lz3;O-yiٮOv{ˮ$d'ZZ9{n'9QɈd{3ߜk3O|}ppb@׀^su?"1HC hB ֒ឹlnkcg{cye͞3aIur{d\%9{Nd[cxjJf|Wk/FQ+)'LL9p!!8nK\,Ҏ>߿e$2I,9\)pSQ$1z.@ɅIsϜ,8:[UZp뻫*-:8🛶򿇴g=Rix'w/eZQk?hyo-p 1&O}=0:($6ұ&I֨ipPpdzKɑ bZv' ei4e%f͙'E&>3WGq&݇}p[ܨnn^SWR_>zqwho_kpP`HYvlaJ3{^X(njfX2\u=D8󽰞$! wAMZr"Ң ͫU4:Ugv7׷_IqQD"[3jkiԎkT^߰N;ƠHDdaJI:mdcЉ½O]ps^@\qz勹`AWS4wjDК7V^8uuw~~ wL)Praep]@,}6,|aś-9wvKS_⸆ƅonK"7sn9ҫY;/<;vm;̎~ྫh@5y*D1)'d4^9yL!]/M[Z:50oeǖca8IB`]Ao8D(V.el ,R5YXܺP|)jnth\Zev<ᾡs7a 1z4;tBy~ 1XP@*35$3ɑ`˝I^!Fh?*Ш7֞-ͫZ1g{e߬]yLl*ՙZP<ERZfRHNp  ; 1h 1XKC;LZ!b(#Ş~nQ aF.Vȕ6ܼ9rsR9;o\سvDŠro\=+.thQk]=tjנo'fω 4ꍣwa3Dqd\%iIg_\vʒ?*]<3~ Tppxg=7+/|C4? wZ(2d \\mjᅳRj[{,;{mxa>rdYKSۋP{z;\ϟ+'\W^3K}]$bZ۽<80!9xނY]ow\7;PMMD`2xD4 H%Kv\,_>vkҁY,Ɵz E1Vsz|"<FOL},p wz@-z!h21 *hLYmxJe8O7 MG~WH5ѯS 1HC hB ֒;Q$yx/kqhH$ uudہcr0Ȃ-`ψc%FұQu|~` Ans1J.LĨjF戀* `f\/ SH$8(2Z6N:H#xpG; 1HC hB ֒oCp)Pra}"FzF ᎀT whAbp$! wAMZrpGz.@Ʌ w@ &OnnlG=ӄƤ(P2 ~䪙d:5y&\t[Z/($!M4!kp%(3ti8 Tak=S p f:@W D*1HC SL]$g: ӇB#Sd\(?5*L|ZQ {g:imNz6&[QɄb:Pk t+wTak=S =2,@NΫ($!YI#č)#R(P ~DEE't Œn;(l>QLHC hB ֒Ƞf:@)q, z.@ɅyPCn+($!@Zp$! wAbDb\$p)Pra wAbPM $ܡ wAbp$!M4!kEX빘%Vp$!@Zp$! wAbDb\$p)Pra wAbPM $ܡ wAbp$!M4!kEX빘%Vp$!xr9pX ׵|neY4|nsC#`ŜdΉo rlXLPtb_tr߹'z{)p$O_h`5072>DhL!m9PTU%_ZveTǂzR/:"xOG8J)pG@hB ֒@N&SZͣbsS3NhTitfl+S{;Bj?&' wZ(@7Kj:[f*gh@5y*~Xd0ͫTEpp_&a>^~nv<[і_`ֿ V/,}$2Q= J֨d YӛP)K`$z{āC4+W xE;姃ǑH$`0c`>C&4Q>' wA}xtP^צP,ǚ-=>(%" ̷\4mXx|=Sՠ)dʩ ȺW_n3 h-!M?q8()`% v6Wݩ(Π荗8s]OJ+7 ^5|lDl0NJuL|VOh@MZr/]Wf'J$/.1ɠϔ T]g=\#C8܍K]xbo!(9+q:&9"(gvV[cUml5mH?Ep4ф%wpNWrdGk?)R:Ypb7@p@s3a~{WpƂ;)Zzc{M-_f}uY c [|C[n\=;m wZ(2Ix͎Rɇwͱf鴺m;?7\Ň,6BCGK<F9pu:݁m̾;PM w_ׄ_(9;tw~o# wh5::;cͯc=՘f}eqvjRko'WPk^^rfݛuc_wvaq,cSf;g֍KgUݬөt&D"6ն͞Yz&x# ڻ7ytY}iXڲڊFVZ>y{;z/Ny2lv-G}<Ž8۫ʟ ݠ7d-{OyB/*YvA$O0,Ͽr5j-*ŒLH~΍GmzgҪI\/ ;q糏'^|wͥkBW|wWS(Jry*2a?z:ؐ^</J@a?|qln]Z5Ht {vyd/ys"Y_ (чZKgHQGwp8P;[ɫn*U/.RpL%]ʿ?77b#@C);Ġ&`-կ%o#{.O\<9޻Mo- Wo_ooF1}me1A;:{ų)TjdLW_Y_wS$k=SwZ.es'o`E񙲀0oxX;#|Iw@ƒ]rW|CJt߯A &OÄ{@wͫA~5b#n_\>Yx>@p7 T:ZC$]0߮}pQnֿZ&6I \&w9׫B}[B+[aᝲ+kh03d2Z;dm'h'0' `emx= c4zS2{ڛ{r̯9+6geLΈ^n<>Z'tvsq%E9[ $zo߼ʶbmʩV(I]5*_3W Ũ"_#9lT (q s7lFh˟v=yi]cΠziZP$N!1=k ~A~eg)l칹k2@xյ.D (q5^U|ÑA9 /3 7 ԛB!_)AQ,r4+Z},cF'[<]p4ф%wp ~A ٷ[ί?_h$).vG_~ŷO>KVr`ǧ_m&S־YMu>Ay޾~#( q\cm&3p)Pra^o(=_E7Kj=V٥^Vzrޢ8^@8,B7\="mwH#xpg0n(Mm,Oh$(ܷv{r[:kC#֫^Z\WTwɬjD6z~į|C]=7<Ե?p'S+_X1 HY>N] 6 ʲZ5cԖ*r,!-L!$_V/||<ǧ$:p*;޽hMF KRjYyinRi1v-j l$O)HΎV޹Q˵{a1AwN<(5q>;y՗_;.Fp:"!B<{"(koK=h\UGf! ᎀ4ф%pgX,Zw|&2b4}C$b!cw'7iD?}CTpq82w`_OWm|OVI%#2j٧I"7X빘%Vv}OFiIKHlka/:ϊVgbHrhhRG?\BЍ=J:8٪@ѣ$LFPU@^5Y4֌-<.}BwPiLX(ÌB׃jxk\LkZ@Z#|1xGVUC[̏H"ePdH$"g7LvFqxP` 4N { j8/hDՁF]rpG@hB ֒}>A"0lN*+Dd [.k5jTvst 1,FQJ% ).$e׫Ṱup\L +l+KpML$/`JfFPޱڰC"pE"itD$\3h@5y*N>eMLK&ӝZzo,d4U?y?p, њ0O@!UT=u1/7VEOA GC2替?1KTi w4&`-;`b \Xyp$ЀjT<25HC ;kv+ǫ;FL;`\’N6LA*:0ɓ'Ֆ1AζN \)NHn)e\Esr4htwUV9Xy\oOEbp4ф%ݨ7ġZ#7$>B%7WwwG}:[9x؇& x׭/-3,ܝFCp  tqjJjΤV[r,]<BjA͊!BΥ)YE]⾿Ens1J.}d\̊͸QWN!Q7֦ښ#>x6ʸTrw׉{_/Ly0v?[$;Y;Z3 @ q\0:Z|LQELJSA-ǦƹN\997znecRLVT~>;).W^ipOIv,{-+!hޡ^1Ѕ~p$!M4!k}pg[jgmGL*K|Y-ΈWzH W;,&w&+KOޠ)1~ztY\}_,>x%4!hW@,_O;^~opX;q'3֕8+5> 6̫. z~};`b \Xy,97=fϮx<;caޝgl2._mz{xN6T*eђǻ;._=đK:~oW]TABZ"(Tr[K/Xo?upG@Sqp(y)K=}z@"\z]ٻyoxhA-e{cˉ= ;ܯ8L72LH`w[c]*Ab0.ܣg|1&(ʶj΁.ZyZA_xtP4Ojo^0R>0bIc)>Z^4ui{HC &-evlx38E⢸N^XR^c_(5?\qSC 6}[fe< h}ϒ$]=q+#*=B082˟4';' w3yo/UtҊ:~s7Ez  wZ(XmnzLwоsD"{iPfCց=y>aw?\ \k[6I6+Uk6d~R~H#x=}^}>K:Qwdj -c{7/l*5,~s+oD0/jױS#R?եut O0a!^;Szp wՆEeE,kp\qu~ܼm'YCӆpdco^puvs.*/z{[GIcn^}t~<.{z(S<]gvKiO $! wAMZr~쭨6pG+Ujfy'y7l-dr 9{ VǫO;_!U$-نʠ DzHYx#ubpȯ=wP)U ;S&uzo4-{# -( zķ;`b \Xy,Y_.)sJUML&}po4V]PvZ&Qd d({ 4*R~_PU$ gjTD% y]CIKZﴱ-ϒ*鲄n4Eq*]"$BBtxt~Ω˓e-An8Fhr2m>^B=+ǮExFg x2h*}U͢;m`5.+npp1!'D" ]56N(y^:"vp\L +%"Rb"NJYr3ca<_i=ScZS%Rs9gJFX S4yz9%!nT`sL&3qNseZ S wA5y*,; 1HC &N2/kz 7X빘%V& xrkK‚3с#BIg{ٷj[EH>. %[̫M] ˞e$ЀjT -HC ; 1h 1XKCH z.@Ʌ;J(-=!dz=]pG@S; 1HC ;Ġ&`-H#k=Sc ;H#DŽf: Ӈd,\(?%i}La =݊>ӁLbЩuJr>p4ф%׆jGg:@oԻ8 +X빘%V8L2 :hp>pT(v!߁L % VLSCWi{]#f:iaEU34{1d:p4ф%7& NetӁR-Qe, z.@Ʌ4W$t ӁF+o,($!HXtAPrdb^+d:dHd)&2 jg @bDbܔ,K:{dJbeXZ(póbLRF6tjT -HC ; 1h 1XK.8Z(; 1&OЂ; 1HC &"ᎀ\L +HC jT -HC ; 1h 1XK.8Z(; 1&OÄ;D$j=*%@"j/cZ1 tDf4 <oaR *L&L?)AS5^j&GQUJ5h|#c0 \?jRd(|Z; 1h 1XK w&'U N_UȔGt& 6q@vs1J._!;У?93\lW6Wި X5jN]9;_& wOZTC_\6Ò͔K6]N W,6sߖD"!gM ϊuFMmEcz*fT@7obŮ w$p&>D[pmX&IO8;=B:h4>p]Q_2˿+c߇HļMd9üSVghASƥU$1z.@Ʌ wK="whPٟyGQ1 rҠ7sTVWh4zA &Oc_X.H*8qpOB]z_~|*>2I֪v\^|O~rEyaEGm'X^jxX 4M>^Qiq%25\:"Dq cfIRJ{k+ o57l/]9g__>ΟHC 1+_=H$25~a1w/JHmmKnzouѱ˓;H vp #bZ;*^_Xң Ek2m;ε,!g_ss-~^MխGwǧGq8$ܑpGhB ֒>7;Q04R[H$z=A s/*dGDv[]rbՠysjn׳X8N$`C-wx'Q3'#NiQ'FO/{n!By'͏?t`dGS`b \XANϞ*{6}>ݺ/>=Tj~v %ݾ1ܝ_ 3h@5y*~@Dxo?Vj]9/f7'Τ_\1ZlJdƮ;3WqFewyi]MխkW|ɷ. S6nNgDr,5;A#ᎄ;Db܇XJs.NJŲʦ-DE$]7e.T:O?Ԭ0"sY&ĞF"#ұ;2?yh2)dJo>Os|O z;rU٥$DYydD+KsdXp $"9敥]~;??;Z(2I'yz9}矺7._q²ku͍=8~߬ݺҼ9Kk{_z5g߮: ᎀTa?7r\bE$TIS)?eL](Hҁ \\H̗ؒ㔺2Y.RŅFo0\k>ȻYTyOTqO2hcˡ2(~<(6,܏ p>^MoV^'ܥUWjΉb ӊ}#6e>Y? wA}"KN??Q;7jB}ƅ{Wk͋b:`}="68)cT"'I/Ŧ'|g%FgBcF[O-Ju(y}+_5YR?tqNـ;8@MZr"ܙ,ƚj"ЅШoZðd1Dᔷ1{h@pXᗖ4v4_p4!=z') rD_GCS1z.@ɅIfR0h4}v,m~?vDJ~M/g(~qL pipT>8?$>мw\ŤT9eB2_p8,T\ZfO`d:S?%JiF̤/OnIWU/)$RLe;`Y;;Νe. ij Ǔ$fDz9_)d0āW役,۸puoDSZ~p^3=OK'AMt?;Ġ&`-ʫwzxc`g `l~ǬT y8qh2{nЌÃ1F`"h^$hvaF5?C 8#7X빘%V& w& &p"FO $ЀjT'tn^XوY@$XssnI?~eNYlX b[ͤeXU?FgcUL041|$! wAMZr' с:ap4ф%7#FtӁJ$sL, z.@Ʌ'ĥV($!ħ E3Cgh *J.䂟J#V.344bPdb@bDb\w;22QL`aDeVs1J.p8?0>6 #$!YI# 0#R(P ~xJ0|Ȕpd׵*f:iwpmF-$${=03Mf5i_N4;㽍'6`l66{=! mAQ۴'{~qЭxv 1(фun{<Эxcc~ak#S΅6|\WH ݊gb\ -HC ; 1(фu.8sa wAbPL $ܡ wAbp$!%EXu. 1HC s; 1HC ;ĠDbֹH#k#S΅$! wA1y.p$! wAbphB : w`mb Թ; 1(& K{ֆJ(6ga\VVX03̐(ߌ\j"A@S#}ͳix\@|c`w$! YYD.Jd27?GBX:WZ7#nb}֝5.!*lel$B}yd]5!6.Oj1n%*[vF=UX?zp*gT5SG ʻvG)W܎;P 1X;@ u*˫(u#BAt- ;Zc 4T:40q9᫊^sXgBn6r1\XyTN"V,H+H?FӍ RDJ_FpG@s`5:]G$U*X I vrx<)d/{+;'?́zt`;ZۿX6]݇@BVߧa`ޑtX('&&4FNhp8~B}wǁ.M15T]og XMXX3Abp _Yu;W.UAn F\k]8/( g{;gU5sςK //ta#}8TxyO{?3w"@ϜYzpt95p8pTnh_H"?@#9kt ӪSipܛew} w4< w<aakrߢR\OJO=Tbh';'w w]I,:v XlR~&6hS}E0qۛ@[׋z;1b ^e)8JmYCKcgcu_'LUtF%˷#|̭y)IϹQ$PɃCŷJFz?\]2f&M̍@~hO;G / # $Wr윭 U\hLI~ vN"o3KcPfjJ7`"x0Ks#IĮŒ1G ,0,gw6pCc(wǁ+H/qwrht FMҦGN&=-O e)kG3Y%0X41Nsqz"W 'D/p9&I)DzFf?g<-{gc7q39&ٚr<Ȍ[V 6gTLK6ƞ6+*E?3H#+(фusdAQ~ I+(d1rR {:AU\cwS;[_^t=Z :ZrP͔0G&j&&mx?<;sa呄B>wB b3 {n{JJr]sUE3(yͧ}"h@1y.ּU?;ZRE XlȤWoO8w(k!yhLB3i`˿?j}ݝ^^w䋳 c9s@3-/imMId -Yv>;,&0t%w*De607`*BhQ\qSԭ/.*Qȕ1+?rmȎ/^s(t:jsDhޱocz:Fb/Mqtuæ&WϤkXSZ;@}&4:u+/N.tέ '/㟟_.k$SOg+n @8LU[vS#L˿;5_C_Y]]X j^Rpv\ߐzW|,|yd]ʋ]q˂*c<.T.S]S_w@"ySGÖppvwa2E )c7uI)GV=RA3ɀ?2 tvr14ܩ̫ƪΡͯ:a֤}1gpGfVP 1Xp q763L=5wR|m7l'UI?\+.щY|XRy2Yky;^p_휭DuIWo.r~npdfV[Or*_3ggo-,io巻A#pXu.<;_gw_$IM:; r+=~~ntގ/>9znǞV$Ѐb\']-RehWm-"}zA*^P2[ہ~.x˿%+ Dڅl~νww}I>J~ºc_'ͪmpSf`Įl16-ɯsZfGbiA>mk4ё1`SKc|:]!Փ.^.6WNM ˍ-= RVɭhi\%v#Ϊ>9"\"wuDOڢT+q{XJcL.F-f6l+e=mZ;C/p} gf _4^~|kk:2(Ж|{A >>jW6caWCc2oM&E&G?<鵕{#e+e(TW($Tqh欣^]^_\UPhy@@<Ϲr׀xܷT08ZtNVK 8}q8ܺ+ephB :\8ϯt,uݞǿLuJLx\"UF)g3^*5Z&W+J,mkLE5tz^:j[ܩ. F_NJ)P(7wSF: wGT߳ w4< w6u% W48%)hָe c"SKel3zc0Ƨ޽>S9e ф) w`7҆~Wsg A`J^Q\#"I 32%ܭQV"bKl\чzf kV8Zug64ܪ۵WrL6d! }zlfD\Pi~UpߍK7pG<Hᅻ\ *ɩYȿ ui΁'2}Zj; Y4<33ȥeӋ+z@,)qK JhY'ڟۀu#WԴ|h|dۛ3/{֖=+etZ#"kgsJ}+3+14yӍ39>(3[㦊6Sc'p_Ε> ;zm팊ӟ_a+槔sk xKӼkw־pu@G*>h BB6)$ܑفhB :I {=6$ AYɹ.^$ ipL_wk~a} _W,̜<@hß ]Kw/ :~*jx`$0盿AS`mb ԹT+TQ0.ݮZ'( ᎀNcP vY&b[ٙTӊ'Ʈ?leՔ7$`ӁFU6dwBQ.Wo{snmV8舰l'SUrLX:Sg yFKXy½OS҇pGJ4!khC'1R0FaV6r1\XaًC$ظFO#`@Zp$! wAbP 1X\$p)P ;Ġ<HC ; 1HC J4!k;6r1\XAbp whAbp$! wA&`spGF.@ +HC b\ -HC ; 1(фu.8sa wAbPL w&n' Z:5oX.SU޼_T=jSht38s.bjRZ; CVebn$X wA}d 6J s71RW5ImPl\,Z;p8ekMFs j;L6 $5ō,2ɺd~|sղ{;`mb Թ; 1(&;{w{ %jekT4VεKyK!6ʉ37ҙ4WP]|XrRGL; >plRpg9P_$+ʢvM+C-5ȥ/?!+q tN>m0qb|>{10޺;O}c~v^&܎;P 1X)go6f]klIc#GnV|#-)be_ {s7^xw8.3!7Xu.f6 M&IYO|Ioy{p߈Z60<%ppA<.@w~gMa-טӹmJlܬ}$}q!(.n)Jm`VWT_vbpwqXV[7t경86φgla+z)G -4'-8<^<*vftgCmIQϒ'q?Oc8eglit{93;K/~}9lE{#vK̑wwrH;KîSK}zE)ŋViХ::dd#+gKP:{o;ІbRE}I.]uw]WȔa+O~| wdv`%}|ם?w֝2y{.~JgѢV|ޡw)Ǝ^gs9F,.yw֤z)Cy}~F*;{Xg[,@8,l])W<n%@>7/F ;3QkB\ۙTnKUwΨu Hc \L:VI[{7:*ѫ^YS}.СRmϿU6E=/o(+W'Bwv:TFc`>R F7ֵ"ᎀGJΛ\6<(wH[ڃ6tɤ% B"ϸ>gmig˥;y>%yJr񲰴ف>Vʟ7+Ӿ:*1ϸKF- hre]y8vu$S*UiEGV@bp B+ͩ;V#p#`_@O$Ѐb\#}C="M5mWr.Ą,?6 3 $"zsK$Tf8ZƮ3XI:x7$;m0Q[LW[Ab,ܭ-ԙ #~Hwـ'S!bp;WbTZ+vmGCH~YD$*gı1K{u5ǘ4::S|jbV $*}ZhjdK4Ng򺍀EU-IRd*W#yj)@k'0qH#%Ν)E9zߝhxHdrjJ7bo{6W'z}[`wӏ3yr1w3 VrΜnOt[ wsaepup.Wpl{'+jZrN[s{7-oXdƍ& A/?il-▇k:Fզ>Y$Ѐb\< w.HC 3rxP => w@ (фuOp)P#=h@1y.p$! wAbphB : w`mb Թ; 1(&.7d龝 0t::J@OH yJTO.tC4U28rP SJnų wA&`s9FD m5HtG %sRIimB7Ѭp8/t+݅  ݊gMZ.XG[V< ƤcCAak#S΅$! wA1y.p$! wAbphB : w`mb Թ; 1(&Ђ; 1HC MZ"ᎀ\L:Vp$!@Zp$! wAbP 1X\$p)P ;Ġ<HC ; 1HC J4!k;6r1\XAbp}sq]S]XruoScpg?37s׽HQpN03aқU]C Xff+H+)^~?'D,L+}6sJŴնvԵ=;ыݰKq Bš 6bϏ6iovp)ҟuMҖākNVP "$ЀMZ#`j{x-NI`L4lYg*3Ye;~r709Ӂ ui;ݟOK&3L3ׄSE8"|2!vĿO͚%p\L:Vp$!乘O8^1$OJ䥾\v4$l_lfgBR){܎xɘɦ{`w@HӔwWID}mQ^6ɳA%oKӆzGȺmo2tu4\T^YF%\'w)*%symu]xߡIghLBᯧTJuX?B*/ @kogT8zۀF2ɇ!f}hɕkC}(T7z´R$O, wp3|joƎ^\OSΥXY% uF$-ۻL! )g w# +R5іF]&]P?a \tD v[NGG8,L;viY}[;HdJ"R2g2hkK iBq.=z/Ru7ǝ=qkbCECKM  s0a6, q pPը][Q4[CSÄ- RRtRpKK}\4|,eoW䂡t'vpt = w4DbֹRA+%i&4`ɽxSUs^J~`g_'J}dj혟VMB22/d֕ԓ)]';HGQ5:K?\M\c.8cpM5kXz* L+w-W Y7-әC<;sa wAbPLlEDKeSmq5LRa',=_XI$\|\_ƎK.oxi/NȤMqYWG_:xB$l?k;d9rA%>||rt&ŇRNƅ^E`;Y}65&Nڪ}Iߤ$?xg'o|osNvzn{C xW9{T%\|T[cG}UO@~ʝ.ϻvgpM7[>5Vucy۫>E?]*%K_,ם_Ю6\}@7B_Ǡ,f_,P'$qZ /^ x >p޸ćG#| L %cb"s1{Uwf w`1\|"N~tshTXygkK2bC]8jU5EU w^*oYyR\_RxbxO[78W_/ ږ|4yk|tB,ݰ4bVƥ\`1,>I9gjmjʢ≉ ٸ~AiLfBXJR<܇b!"WD\8pF3ގ>|N}y3S$%lM+ V[R%A놄;P 1XգܼtsdPmW'nX9V:_^˳Tn¶Y2rEOX;Yص 15[v3`r$21/+Ҹ@$nɆ% 4+ڻ؁㛪=4h{ۅ;sa wAbPL wXl^}O=n/p0=3w׏D2s++-=WC[/ YpXtíy! eN>`cy&;i=N|rf=a<L )˙`)i)'b(lbiwN ^yOm2.l\\нlM*@UO$! )~~7荱Z55+ݚ0glqzs=%Q2&tYHKg^>Cq! a}x>l:(,=@$ΥKO|zH̰qq::b7RtɠNK>qR"d]'_)gfV^r$v ZWT.K$! w[,]%RHd"-7z .M@"I@I6.I]m Mt6,H D[R*[#Z/6g"d 㰘=(wpgE"6n2dq@dV*$ ِg ecsc176H2(`j $2c'FFAI'oG!5 N Zi"y&<׭aљtp0rEC̀d /ҶAO_G6dpG@J4!k;Sb:ח5 >>Ȑ9SSRDcP9\0`F& Lq ^䍛JtZ_0SNMRe0OJ#s} x!!Jz K)0рjFD8 d8:lFF@Volfp&Sp1Lp\L:Vp$!乸Gq JG5QU].}gX8e+Tuf.M>ӣh*xՂ;`mb Թ; 1(&^) wAi'5S{9Cld4:wݐpG@J4!k;+e` wsa wAbPL \bڱ{ Aa( "\( >bLkT%x\!ψ>!3B10_^Эxv 1(фunc(XV< $r62`mb Թ; 1(&.7dw`x0t::J@OH yJXOb/tCtoO$!%53" ݊gJٚ +Xu. 1HC sۑS9Zf yIj*}> ݐgD'z[x* 1(фuh=]BY0& GPX\L:Vp$!@Zp$! wAbP 1X\$p)P ;Ġ<HC ; 1HC J4!k;6r1\XAbp whAbp$! wA&`spGF.@ +HC b\ -HC ; 1(фu.8sa wAbPLpxkvo,oYL!E ۵C~a^ys?d wAp:8̈́ɶ遄; 1(фu wpAeP;:hn,ɋn>!7Xu. 1HC s1p1hּEayk/>:B  %.*)kny`}9I_[m$! ێkʹ}m wAbP 1X~,+B̍4&,fo=p)P ;Ġ<x ]uZNBsMme94Ҝ2*.&ipYs-D&yx{SF5u e Zna&oPʕ&& DBIvIWKV9ƃ]\mřEa#טs5$ܑp$gpPp-{?WXLKWEuٕ-e3w2Q37Oe{DzZW߾Z "L}fM^M]A-J ^bbmUU\V"1HC J4!k;SIĀ@c ;.~23jkx>8.ԗ5Ru =w@Zid/'H11H;)e&ee!4aUT{8},׷w`F[L&7ڟ;`mb Թ; 1(&#PI-߱[cILLܳ''nio__%ܟzBm^ró.x<(s)N.%q}#)<Fg6ڱԸMG?9>];z Tړz2?4F0u [ƊJ$ܡ /oV:Hٱ97rEx ?D{Cǒ1^ .>řY}{d0XLkgW.cFef\j[9<+L̹FDx/Ǒ-qy _lQB8=dh /qH p_~z}a}oSO}q=SGntt{GFsс{,.MW<2+Fgkskbv.931>d]rխ*&X&cʭ(707ɭO|p$!%Ν)=CKo2X0.K 3#W/q e ojZqJllsKd&Y½05/|eKP_Wߤ 1{hl}[c[\ilJz @F91W}|c@c L41h_\`_7@R"փ>&W#B4jͭVVn1oH"P*f<437Jd=,6TRO(Rz/KЫҏވ16s%ZcneFRRlH>r/ hK( c@c ɆxD sצ78fѷ%gw(3\-4V68|D&/ظXdrGnb8֓~|"𥸥 t#Lx'K{a_ؗn4B)ז{J&;1Dd2yzBx`tpݽ;s"f-]WNfYY.zeY}E͕sVmmqq(PYo"29TTj4ZZ?<j]pLHK]ڹG̍ji3e3c@c L41hG MVS^s^svG{SɞMGZS eMZZXbWyfi4ז4Y L^N<W.SO]XlmgyE֮{s%S 79;ؔiS;ں]=hwBA%\\1 1dC<"c joeR4}Y 5)S"΢// rǶ_rⲵ5hhY8k?873'4nM\ܵ‚l{pwpLYV_^G"ٝ'o1DzVΓ_<lio3F !VAcN9KG w3+Q䁞 4EFܹ^\22(&DRV6Ǿ94ka\ռ{Mi@c w&-1u5ƦiۏHO)ܐf&{>:f/ՙaM C>$j1; =]GOP(h9iѬc;.y>O=qN02~sfR'/rWG@ U. w10&:5hNS2,yh}ɑ,373B!K$>'H&V)Ujjd{Y؇k/O<>&"~?lkl{~9eo8t[dcb8څF';1 fb_ |~K)畍GyɫSuJ /kWg5o{Jv&,Kjk)YqC2F /w10%w>+H\b3*uҢM-e5\@*>qaL}usEIv]G7wZXT*Չ=,F,:#wku:46upoiV*TٗrbSo/y>ms##7yI\(UJWɫ/rA U. w10&Ngg-mbnjD2~CzNrJ]t*N;L".d({uvlwˋ2sE6":Z汃gt:NɍpWV+<ܵBK3ɀlY!SB}7e2}UKn˿7 w>wMՓYQdm9,8vz3"{eɫRH$B~JcuFf(J rjn~2PKmY榉+rV=4߄?{QLB_wnH2 /w10%wd1K!t:.;y$wrdbh#;8XE'JLqbo;ŃpU. w10&b",Zh&괺*49/o^3mkhF B&U?|~΃A%H?=FkPhFGգ_;ƀpzа>+Åhd__q~IѰ>`*c̴RzH5rhbY!ٳSc@c L41h)܃#m9W [;|Uq> :0T0G+ؔH#!#$2dDB>%[?GtiVԏ 5;.4c_U(O+iB~̔ ;r $W@c w1φ1J}r wlp\_ɤs).!yk]OO;ƀphb ђ;RuI/KFBּΨ'3oV+ 1;lMipc@c L41h2oV+ 1;lRLhtoLAre=$MCPݒn[SdаuՕc`1DKƘ(&JalɁaWV+ 1;lRbt@d~~,:Mb J.zT 2н;+1PC:YQ4dG1(U: ˸B%\\1 1dCS qc A 2$KPrѣF{ Z>QLp1DKn; pRtloqhK( c@c Ɇ- 1; c@c T.- < Z H.pc!@c w1p1DK.wV+ 1;lc@c \0;@b ђ U. w10&;p+1PCphK( c@c ɆxvjwtV k{˪kwBs3U*5zJSUzq>9lVdPztg?_n 1; c@c T.-?QeۭTpK׾GΟT_s3oV+ 1;l' w*4)ŐhBx:z{~x3OcԎcEqV;^{ЂPx}ʖTBxh]c!Bԯ3jZP(HFKHBCU?PïR*ڋ`I4MZ&a})z^Bv cAOGWT>M_= 1; c@c T.-c9gL1j%JW|PI$4~.֛-zgǦ GAb0XKּ[sӴ Tlߌ{Z'oV+ 1;l;Bih鮞!)EtJh'w{`opGxLuq+4ځ~X*2L*Nrxv6 ]xi`\-NY dҩ}P]MLx6@c \0;@b ђ;JOJ J[]QYI$l/)JҼjQn敋Bf$.GaVw-{/,vR 5|\ bn2)/9mVshZ ;RxC%\\1 1dC'm"(hd2o{>\*zNRbsY퍵-%w;sуoxi,a1A;95_h//-(Ql@ E&L63.5‰+g̛2N]6`1rUq^Y̜7-EWߒRШxE'ot e/0/;#W!S$,>\lJdS]kkSG\[}wogֿ|M!y\jGW;Ss㮶3 ǯHWTx6@c \0;@b ђ;Rx-ym ڻɋ6Vg+4W[ݗ~MPzwۗ[?vl=]_5yٽ1/}F,_zû?q;r $W@c w1 ).GO.) .,-}\<>FHW_'+*4?w$Q{kCRۏMQhY=]}詍eҢwn:ؖUN zUs oͭ$2Wݹ}s 9 ţ7ssr<}Z6 ־Q!Wj4amM5Jʭ̏N[}URzoQ_EO[iihCMiys-eg; c@c T.-#;L  J9bkW3gKtב uޗv~;~ulo o}L+e7ݼWYzLK>FzL hK( c@c ɆO6.(}e5Դ[YڈZ;#/ZyFd)L[Pwvwlʤ좚tdS/j@qGeCr}wB啥5=mO/ )G~SyV߫ z pO*Uj*4>+Nݹ}F BH%|I/)rh ԩ+;Z~ )ɊKYQRnR3 ߾Rx##bzuqi~9Dlщ*fx6@c \0;@b ђ;B3%nkW<:Eфf֝m S|>Fsw_xuե" ;^A]kfaS[U\4Ww I%Eyk_C_tM䑂pU. w10&nfa*- ,TRwHV6"Z# eJBI"\MP6"h\d (FCPfƽò J;zܾ qлlFD"M}ވ7IRodK[Zڑ ), /d *,7 T;ZbȀtFt&]< ѯA\Q(*3/ѩVf!p/=J`ʗJU~fZZr:4'jg]xhkp+1PC13LJ9LI,m4z[sR!g0Xv^B6ߍ6vlo@[t Ȇ$M tʑFctK%{oV+ 1;lG~PzxI+U:$=[;Bs/?Cj,}a:g?cr1 1*cq;fpU. w10&b[Հp+1PCphK( c@c Ɇ- 1; c@c T.- < Z H.pc!H޾S&; B5$grX|Aa)LdG1A̢ǁɹ~f c@c T.-l-$hZw/WqhK( c@c Ɇ \-LvD"Ɏb`Rh u@_3"P:QL*=JՊd"PId-7J';.Wcr1hɍID RaXU. w10&;p+1PCphK( c@c Ɇ- 1; c@c T.- < Z H.pc!@c w1p1DK.wV+ 1;lc@c \0;@b ђ U. w10&gJȕA^SylnuS}sgO wsS!lloT`TrMAXlEd2uJGK[`~1p1DKD&9{8[5onihѩUwk4jc3hveR&f]#WZڈl$ҚoǹAy$HxC%\\1 1dC<^{;6wJJyÛ27ǯtvطLummʆ9gb;ڗVW?Yymuf N;a }3fz_"pFiOZJŕ;9va>鷯-ѫS]g  wo~n-z>DćPiTO?N(V+ 1;lG;}k3:݂ 2+(TgdjxNMG{oYoۿڸ`SZ:>x7gpmζJ,*kŽvpA++ں;*Wwl\ k-[s)u7=Redֶ c眾9-^)LNhk&` .WƧٚ[m>O<2ձݯ-\)}w\vgјia5- db򻵕7Aᾁ3L}xcG+\0ϔ'v 1.Wcr1hG܈{N[cϘ]o;TJՆ߬lolih%/޻SE"M ទсm^~n&jﭺ|:\,Y6+}Z ܧ:Y܍cl\6$OZ?vŦEʆų6; Z H.pc! =5rGz$ohVzLPBZSV[9m<'#[:_oں:jd֎YAa3|?Y7Z%ё.K.ͽ[d%2O;?3hśfO+֝5u뉃[ʌ/g@;zX y̗&ܫxڄ,{/W؊̌M\}ysyBіS,MZ>BBX/?^$l&kƩ6Rsˋ/L\rAc \0;@b ђ;-e|,lD\~fk~OT=2 w[c{-eH$RTt+{ ɀoM?`m2j.2!L&_M[=*M.UZWћ(3+m::/[U. w10&1pw[苃;BF Vw+dՑ{e>9 K{_H[m_ Ϝde{FV+7,)[[b0z̞f҇3UjD[Шԥ {%# .n7'G_ٞ# Eays ݷ<)^MUFN6xo+)V3=V ƺ%Νhȃ%pGU W'ep?z%}̄9cj+N1Ӄijzt%N^tn!:iv=8$ vu8qb5ᶞ M.wƀp\!Zrp)lH=s,<{0cO ޑXXgQow'ÙznlVxn[OϜ3V3j念CYt:-?xꤳ3BfV,z>)ewZږnH{@ U. w10&?b0UjuOuS}\ȌeE>y7͌YV]}݄/X=gZ)dGۈ,Rbۺ;ҌԔ/XB&Sv;FPGsٜƺ3!LM>GSI$͡P(Ӽ|$CRogk-D1rȥs(ڨP/'ޞւҥ)|.Wg@)3wR~rBV(| 2l6"KSOTߋ<{+kgxHQJ".(euu+3hFtvkwkc\m2s֨QLp\a w%w|fgl/){&~f׊TJUجEv6a>}bdٛ#!SQ-E-wn u:;٩h3P_TQrl13-^7U6\)dr[B Zn-g H?{?[ c7D\Bc`L6wl1 1.Wcr1h8oV+ 1;lc@c \0;@b ђ U. w10&6aL~p x\.mh@&qoØ;QL|.=JēDUkd+1PCT m% ˸B%\\1 1dCf/'; ":;-b h,*I%Nr˛'; %=dcT!)Ξ(&\a w%wt3QL2b+aWV+ 1;lҤ5Ad1A\{>'q 2A"Nr p!PrѣZ@&lUdG1q c@c T.-!94dG1HUbe ˸B%\\1 1dCpcr1 1*c\r $W@c w1 ܱ;ƀp\a w%;D\Bc`L6wl1 1.Wcr1h*P@rq;ƀp dNQyRS+eɇZdžBRh4\xd%sPkhZE&TDN"4@B4ِi=s2plِlTGCTo(ApJ.Ϙ\J&lh{N!Y~Q&t$Z|UʧM.:Ȕ cr1 1*c' w&ᱥCI;W ͵X\&@=Ph<+~94S=a*᳇IJQ3xC%\\1 1dCwO3$l}ɁN! hK( c@c ɆOSiTk{yn?;/h4ʬY< E$o̕˔]}h{B:'4:TdLRz:zJ!豧_&4}G?rὠ $b@;@`߰66sxZ;F55pfFvvWt N/P>F2ZBG1E)WGe,TJUwG/@d)DkR?ݩQE&CDE=C&PG3X ޣVjjt@135sriihGI16OR7ն\,ԙ(]m((Y(3j+slL& L\>gw8UCe3"~Ro("cNBѢQZKӱLSG*qڨ#1p1DK8aGxO q?+kg͝D^Tr\ZV uHt&]dc6]]&J5$zk>~1$h`-LZ{Q0ŒCf|T)MD#Kv"41[+<217D\Bc`L6x]`KXj[YZS_NRJomMs@>nCEVwtUpfOm ietY;Z5?%E)J щ &=Ķ'ߝQD"GM}͘3|*c&BӺz,Mܺ[{7hek~7bp (*BenmsnCBzc˖(_rҩz<\բ~<8֜aHߔJՁ'F!EU6٩Ʀ| G/[Y,y!Udi{mޮ~tN% 껏F'o?=>? j|*2;- (ʆۗ gDB~qνʒ ̑BsS3wp\!Zr6sfue79zٻYO K_WzH"h4 yߧ'VF:o4D"Yd1D6¼«v7eM ݃f6X^1Ÿ~&W`3V%|N.s[}p jum *P@rq;ƀp [E&i}rk]/~ ܣ2y/ϿyjŮ>(s{:Z<.p֐D{7]>廏x9x =8m@`f;53uWζkEipG>p7Q+~}71,8ˇ{="rM=L 24Jd }057|꺺F;{ ό/_>o3Z!:##/7;'k3 ?7 |yu;±sƢ~4|v3Xn%/mi%ܩT/{g} UO#M͌N:$Z_}tJ:rEፒ} x Axcr1h2,Oop[z~ ի_sbqdo7v}xa,ymo[;Yx;OIdco83RG a陇C,*z͖uP(g_۰t&+5Y g\GS7 ?N(V+ 1;l$-Z7fIܼ_lozw o.-/okHZ2ދIgCZ}8{4:miguwEY؈̘2ŦnmD\ȅcWVW;͌ys}醴o&:9,ʝi3|s<">H%]QTeFOCQ:u; qQLW[mYA{zőm ~B: 3Gko隷:ЖS"!/7W/Ǿ&qZʊͣ@+{k\In +gH7.嚘!ܷ}z`ïW;EQ\] 8 Axcr1h}fViT݅:wLs+ɛ9c3/;RGOx́#8K%L٘5T6O q *Tn)bow#LIeU ~Xvsm o~7 Pr $W@c w1O"yk]+jmM?T"+(頔ct8]! Mitwb׹^]7ΧP)L cH^ka-*/..^RLn9SY^=gɆԮ Ѿ5x"nAH7ooo)3^17(47).%6#2ҍݒų{,m̿1a V]O(B(ÿa`2hMM.*tJwLƀp\!ZrG )vςJoUt&\qLuCLtZc7ѴgPhEFj:.~k,_eQi ؝OlX}F&QkUl oV+ 1;lG;bXeL9"fYΨg@~Z|W^J? ?Q}dzl~3 (4>Y pGP, ]<$-SUD@c \0;@b ђ;RMyL1>[hbfja*Ngc/w!Z H.pc!pKx-ep{@c \0;@b ђ;-ep;r $W@c w1]c(o*(&&Љܺ*' Ɏb@ERVDp1DKš(&Fu°+D\Bc`L6)??:'fq-i <&Nr;Z6ѐGF"(&\a w%…AeLvB[`XU. w10&4?칡gXΞ(&B㐉ܲ)Ɏb@E*)!$UQNv\0;@b ђ;5Ovl 2r $W@c w1 ܱ;ƀp\a w%;D\Bc`L6wl1 1.Wcr1h*P@rq;ƀp [@c wƀp\!ZrAx@%\\1 1dCpcr1 1*c\r $W@c w1O~3Bb@OcDVw>~yԍyASF2SQoC權9.\l&טslszoGTnߙJ˫~bc[y3=TG,u*./Xؙ o~TGk[Y\5fs#n_Q)pU. w10& Τ yB53u4u7PZ1^hv.$D&5ъD2bq_L*ZZe&SȺտh۟n-#[P)#EOmHȹxmԾ(笚%eYZvp𴽕^C$> “6EQԪj>?5,9ZanDBkƀNu Qû%4-J{glȬo0pGnҪ3mIt/ܹvFEub7]/ӿsO]T݆N˱MQiTçowTo09yaR}ס&urH }a+QSj)4ts*fF/nid{cW魊/ m-"R6Ϯq; 1*cq;N s;7]F?y8MSD Y`*jO>qBS okK6M)fF̠P=Q 7+JoWגު _w'JsneI) z*D*vF/Ó'Lv4 ]9~K$,pl,|~G w4GME5vc_ⲺZ{P\gƜ 7e|-*- uYn(bL8^IVL-MPx7399+cHdæw #6hɜ^vs7Bb&Qu$?$ ljGSja-z(kX/ܹƜa豱<*0T:8ufX4/t>3kwM{fڹآij|R1gF\`zo)03FuZ- ; Z H.pc!T>qe̾OKH$ǚv*Vp'n/{3ķWԕ7m=8#۹Y+dJ'o(?$RCnh98encn{z?\> Jhz Y}.4.Y}L6Au& 2P;τ|ڲƁAc34<)h?Z8`agvd΂|q:,1P'%Q6NhƟBC׹|wW|TZ.UV]!5/s{uipwuH <Դ`6j0O rnWH17^R_93gpAZWemHn)˭JZYzJҦ/%p7" k~^ɀD$ps22=#eddux]8p e9U;:+<܃NCUv#yƜ^N2ۏN-tFOE J/\jONj3%C,On2^TG;XyrH, ŢWO|{Ad+~~@{c@8$+KꗾXՂ'\XŨP^v|qp;@b ђ=eur}eC{c{>(0+J;W)ASl>Ego)IᣄCpLo:Oqwyחy^h%%̎L[3+Vʕ(_i/ď]YMN'\xpW[ܘ!J-@nEEUR_LHͱSVה w`_h\q6op_VZ^fQMijܚ742<)HVo_hQ_}᳗8r1 1*cܧ _~EXe^tswX|dy}BNuKGؐ_~(S$^[g93|yhpO+ xNl/~bj_ !N\xӑ~߮[ U. w10&gA1~*7|N?_pݝwf|@#7|<L,s>龮,ϯnX?K2m\,;{ykH$ ~whjoZRRN^UTkn?-/(*5tg'9~b˅wťCRfJGDrpҨԐ'nhŪ)fA1VDM -\zců7tjǿ=ε"G zz2Dcv~pxd#"k5XyzʂJ{L-g-,헾NK}>fz@CG{7lL:wȰ.R*o/\(}>Wֿ@Wީ8"Ȕ7G YfXؘkxƮ%GVg|77{g+˫z-5*c۝?]=}4==?,C_wSu(akr<3spG}W\m7%Ľ"pai/:1PάzzI20Twq雩[2Jw R咍]N)uѿn4Ip;@b ђ;prLZxbNsMs`2eϧVnn߄ ^_Sz+Y[757XGQ{/.+ ^R..K+ɕ$r=na0齈^B;7yٙov"VlyQnol@p[f@%'Tܩ |rw`V^+1h]D9/WVw+,&veY^s:OýHc e.@$! wAsD'l:߼%J2on=A,*5.<ݰl8JdCáqH"qFI>}"fX%W]mvh؆ϭ[bY2BbڢFwZ1X}W`o'D/iRHT>a -T:QPbd: kt~l. ԪW; wpv:ye"E VE\+Z,Mkm'o.\1U(o(@oݜG#qphehlôd cb<ӏD!߬1Y}DY%z (|G¡Xѽ2pg%E֡ @c[j:8\8#Hcꊛ`vA-ﬖԳXT ;|B\y5t&-h/jPf?;iùvVo 3< fo{?A9mA.6܁Of2)t0&ct_N;C&ʂk@3F$u%׫mk :0\}@L`ȁ*yꁓJj`NsRTe2@[{x6 ^)#dW ϑTiq/n~N -e2eUHT\84~do2}q 7o~KZhisSt;o?!N(lDKc;L^?F矺I~N; O~hvgHpi;K@pN=eޘLpG@2bܱ=bVy(D!|2fbF½uh:20;`-s1 . 1HC ' whAbpt$!e.`-H#k)Ppa wAbМ<Р$ҪTb`ql6 ; khdTbJb+2LF=&t$!e.`- Ou-&F2`-s1 . 1HC '!eNjLW. ^0յ$Qvte𹮵vEbFR .YRSk.-'^Z.({c"fE;۵ Cfc%tL, qplҩ.ؚ^r$<>&uCO«[Zj[t1u}rCX;-w`πH{ϳ,YK/:xG__iuS""ºZ\6-Xז=-ě w-7yBPgPe/.Q|W>h|Bf?2QSk W'vVW䷙J2}Ӣ:pږVsQ39L:~Cd*tWWSnx7'S%'`y{ghco>͍f\;@vHk4 r zKQg!*JoZW.h+wmG<U^IߜvfY\.8#H} ěF5ZN < @bPB ւ;V'M9DBx}VKus `rhph/u*1ۓۗ:g,6|^ܢxaP!G$D5ۻ:Ͳb-{yy>aYϙo)JK,kZr H 2S ;Ġ9y"#ܩt꺟^ޝlT$3zwJS_셥]bsy=[~U߱|񃕍elrY)^<|iH] 쐴jA`c:y8[>w="gE\8|x#rҚ#_]gg$sb~D(ATǧ7elIsSKttjث_]/A5f.N%7'"| g. j>oavNgn,qwG;V[ :P$߮mL<UpP \so\"/-Y)MB2A +^^OtZݚo#'Gpp_ʊkrzE>EKgu&4&D!*ե,ޚ\Rvt7U7o{΁aL gyӮ؅3IdH# wA 1X XDzb,BKpx_+Һ;i[2d Y;Uuŵ6Jrԏ"m̅1W#˞AǾ8%j<;?d4Ň]:rD"AS wZb \XAbp4'Oc; cKϲ0c$˗~:{qfpsO_K+  +ek`=~RsEq.-ژrS` Yiه/Mo03T)Qs-LՅ5^XrL'ǐ?\Kuv29'S/ޖzÕcW9 ws5c_1+T,М \T$nOiw*biIߒ5*MQn1AШ3O t&=mS*+s4j͢t2iesbcJo+*'I)&k]/@ >ШQY=u:]gu+^Znaai.VYc}锴r>C`z 򆞶/.#ܾ,Kܥs$"Z_&ypB(J^dviHp3_8wM5rAR𮳧RqmAb bp`:o|K+јɈΦΌmKp@$w \>D&I%W^jeZ"Ž"j yo@tXsV&nJEտXG E#=kK[1d;?~Ek$k)Ppa wAbМ< ИYږgF$nB Odt[FZLwDQ{;3`d"l, ő-8ԌepFP0G>%@WE۽0P@># 1p?5g W`Dp_Aי:6B0iBzGқecC/`p1CQxĞHC \A 2bq?jqoA'y1ѝO7vw?x}lNJWb̐($ݰ[]= H 2S ;Ġ9y" wx7azpt$!e.`- wXAnVp$!; 1HC \A 2b\$p(; 1hNs$}$a9յ$hLANp%]\$ 5*TWd2-}͏ bpz)DTb2 #M˰(; 1hN\J\r@1՘$*CA!) nc7f jcNSU=յ< bp(d-MhZe.@$! wAsD6$ɇkLu-& <@܂֢S]I<{R̝ZLr1HC \Zp’YtTb2PhZe.@$! wAsD -HC .W;Ġ̅ w`-s1 . 1HC ' whAbpt$!e.`-H#k)Ppa wAbМ<HC ;Ġ 1(s!kEX\L +HC @Zp$!]  wA 1X .8Zb \XAbp4'OT w%+!#sc_hicID|8}Ẕ>R]|]n_|Odhsk*jVηwW.S|O;Ġ 1(s!k}jាz(r]ǟ̒ Yrpe.@$! wAsD5"aJg wY0F8 d?<#kXwnH6;W@pyuúgk+0.W;Ġ̅U[ 7 l-1?MY>WPP zI?zql2ix΂H$2 ,ᶽNh;`-s1 . 1HC '>NcJbZTg)Ts:.VVޠRp]Y8Xm{I_0[fX2hG?;9;d4Ք\/6o_T6ؾdG{<=C/_K/H$`,'}B}cSzCsUcѵ´ŵ.fʉL D dח6:Q]PUzdlq{bUTz `%/.~{>?V[XԱכOwJ!Uxxk9z^'|DIp/hF0LV_!(_ @޹?:r_noKH2e7K}=}b;g;"+xy$ ,pAM7'mx;԰ygꦴB-YB-5ٷHC \A 2bGwr[dӇ.:΍=‹'sq,rNN]9Ax²jAuog{sW}|VqMW/P,Xu4w]-XiI3=ÕURl0՝ܗbrZa2;?9;Zb \XAbp4'O}=pZPDbdIn#|RD7?wAYKwf'XGtI2Iv2f*/:djj|1Mw,~:w9;ER6,:K~3+l1nKd_: X]Pf}/AS4C@ld*9hFɯ*Z1iflqfEe[12Ե%sb_\q|6^=v94nDw4vl͖oOK^X*(zZO~u\: }bY1 —wD;wvrNZPP(T^!_> 'LҨ .E9Rop}MiqFLن _o9E/ ~'Ǎgi,.W;Ġ̅=vn^o(QJS4Z&w̉o՚'1s UWd˷>wT,}-ywll,B"$o_-/ep_ֱ^[G!UMQqsMvVW{KooHc e.@$! wAsD'#"e#7^ֶH9#)ډ V'ٌ.ngSg XV|ns(TJ{}es}daZ򊢫qFY9ܞ3^^yyyx..RKo,BQSQ W>| khph4,Y+__OΤof'U+vᲗoZlܢY c>)MP&0 ܝ>ghP.rBbÚ+>UKdRT}j? , w5g4|]k{omnN(媸y'3lz I *y'~q'$: fntٝ*-8عѧdgMZ;KgΎ, nmzuej3/ޱX;6T7ܚ~r(䅷֣[` e.@$! wAsD'\µ) g+U'feZ.%M8gټ> rt.JY9f\̕[fws;wԷ6<*og-X4rsF:qϴd%[]1tto]깹YZ;NG͉ h M^Z_RgdG$od^6|TiAo$.c÷bG<̢i:I: Y]9zaJOx渭}DBW= k]-5M+xи><{*YeAR~>cǒG.LJ+$kUP&Xr啎-?= __mQmljOs=Y`xyްpwG\2n Mw/D7HC \A 2bGwk[R{)4Kw"fslfiǔr뼴"j0Ymo9kHO`ҚBwp$\e͎c9u[Y[f `IώbG7~RFBwgFxzDL\pp!vl5pe.@$! wAsDT:nekI$$2 Oj&i4j ˒űRkF r9Z͐X( gBPZo4J5Da`ͷq>Nҩ6|ޠRm0b9ˊͱ(rBR2t@ v\"8v>;12Ld[bHK 0`x`w&0@``ȔX$!]  wA 1X X[kD}X[rLP6v!V7Q`w XՃŘxW!UFƆ'SJ1h{ jC,gB@` kf:2T`d-}j  .i4,-lfpH/ 0;`Tȕ&}j$k)Ppa wAbМ<SFR=~CqarmTe>w0.ܟ Ƽ*@R bp.lKyWyOp?d bķwA3p(; 1hN&O p$!]  wA 1X #n)H 2S ;Ġ9y"p, Nu5&;3=Ou-& Np,xitS]@U̝ZLr1HC \ZpTTb2RJ4- 2S ;Ġ9y"p#c=}S]Ib@&YOu-& &d1\RlŲZL Q| A2wk1y 1(s!k𤩮d`0\}hZe.@$! wAsD֤m*1qc 섰䩮$A ̞ZL ak)Ppa wAbМ<HC ;Ġ 1(s!kEX\L +HC @Zp$!]  wA 1X .8Zb \XAbp4'OЂ; 1r1HC \ZppG2S ;Ġ9y"p$! wA+AbPB ւ;Vp$!/ɶCPGYqZ{ڞs:^}jןZ،i& ʡ!42.T7} KhK`&ӈ- v ȥ$1ֶVU'p(n^΂YZ[󬂣Gij?x> ̧K)WY 9#v?"D1:>a޺ZVGsN$2I;Tyx:iJnW;M%Ů Ք @a-9NO^E1X֞ƚ:Ao(ɯT)`//>yM/w_`9Qq5Z:@}z x|yaBwV{512H$ŕŵ&} ?{zb ږ zَL%ddFMՕW9[;y̒ەj =4TלQASCusw0 Z2 ( @ |А62&B%7׵8Fņvu~vsQݺFawm`Zs97Q?--(? (_07K4qd^^_+Πyy4 AJErW{ܲj\0sNdkC'.[zT𭂎o[K.`YI3zz]<)2DFw^g5dq lIUMN`ݐdDpR.UV k֙L!y]2U%]y +,6Nn19$:|ld0-L1ŷ+֖a> D}'W_7D" \A 2bqޙpyXi=;cn(Ǒ-m؅`D&Z6|nt|LpTPwEA=D.QZRJe,삢c*X~6gyIk)p(; 1hNG wk͋rnTaπhrti.c2 7xKZpzUJuwҺ,n^m3vw/I׳ݥ"9x7e:>#!B.S*dʄs#[ډDB@OͲ/g^Iǥ,V vt3⺈r" W7x2≅WܮL^6‰k32d9̍~!go;/Z.Quw-\K,N8\}7wp\T6uFƄDDžݼ|W%W4t*d* \cytWn8yǯY?\;߿B%xs}kcGCuˀPVt|Z|xޕRJ"|{E|';BޛhNl}sOOU/&c;kgR锠(⦅dp8@5Ǿ˞4玄;`-s1 . 1HC 'Q=(ɕTo^A .|yp ~p/Q;4 CCײno'%V5NJ]WtCrb s9)1׼8?x_ ;7Z*cK+=꫚j2LcՎS̥ӝU |O,tpe7lH.Uc@]EŘ[ʬڞQxc\i3;# A$nqP=de-5> )ӛZpPPސ{!g)(TvЖ?1?]j>K祬wlل1=B"iO9VMʿZDgҢ@s4ogYFcֱ#eҙ#oɆo>g~ 0y\a4CZ0POK7)dA{g[OM^{^iэKwb1sG.X(/1}VҌas gƂ @pI뫚 ktOXPW iWVp,Yj ؜u*,c:8\HFQdkd.d{>;n)A+AbPB ւ;VRPz:_'{~jVD!xO켤_kN7+>nn̶{;#_gYY[NO^];ȓF A+AbPB ւ;2 w.#v$QKמּu26 7T[,lJ1k;zEʢ$ wZb \XAbp4'Oc+pTNS՝#J&ELJ1Y¼2X̉"bPNXr}=^CVu{OȂDB]Pә4*!./vvvrݪIB뫚%R<(eFLJRiԑ; TߩqpgsA WPgzCuI=?ԧpiyx7յ '&>=vEdlS)WB{: ȯ-YQqD"$gg+dA+hY:xڃ9J CoghyVZ-  Fikmm<+Xfp?Ej-opr Krc`4v :'40,+,f`SCMKS]r@!Fxt\B{zmi͉ 0  PP Zxu>dr,bm5KSE}GWڲ;зNz,Y`m٣h4? %6FrDյ$@pBLFA=4uB bpRIL<?յ &_ak)Ppa wAbМ<ɾ2yTWch#ELu-& 2I!1jTWd83qO4a|xk1y 1(s!kf0NiVVp$!; 1HC \A 2b\$p(; 1hN$ܡ wAb bp"ᎀe.@$! wAsD -HC .W;Ġ̅ w`-s1 . 1HC ' whAbpt$!e.`-H#k)Ppa wAbМ<AVAh"%IAV;L|xE HC \Zp!笜ZZYZr쟙);z:\wY8?<1B#z;ڂwwR=p(; 1hNF/xXrx:B%jܚkoee߮TH='B[ovc2r^8J}~N*2r~lmtvOoFZ:s2I w|K"J1-D"¾N2 wZSQ 7FFGl [}_) ԅ[BY,h뢣$&,-UqwI+9klyN'\iV}چo*ϡvQI [? q74 gXg',6,<=VsvV\־ZP寶w.\:4iŸp M %QqRrAcrۻ]=_W?i,q/i4+7/{X&텺P\)qsW /\?>i'LˍK.\=R7Q;D6p /'ɘHq?~>t k~#mxZDxu59yGg^gs~phǟ7TMgRĢIŶfsO wKZ <-9z}Y׷t8xo{{,QhhP{cW_Wt{&.olsO|~:fѴiI]M=*[n;`-s1 . 1HC '>fsnz3CiKJs6ozbk4ʒkUշLDG?z쓶_j箽غ?Ͽ8џ^ BvC_}ŲW޼wjZ;N\0oEb;e2Q\Lʕ˷\߾tiwKd++[AcImv4NLH-ۻ^I߿7iŸp_h:}X?-tmέVWt뻣wuܰ[,Y;f-%úa9"ܗ^8^oXۺn:Ik86g7W7 v'YBE}ׇ }ӗt֎Uo}7 |qo1݆kk_^#b%d?i"ᎀ$!e.`-[nسLƲd^>t7+pft@90kIN/{%+,V!trH۾d4c7D6 wZb \XAbp4'O[0sg9:׹W[>.nB.jmSib.v}o7ݙ []|T*L_drwbͼS]s$)a{7*l~9H 2S ;Ġ9y"8'1*bnM]so{uaw_=C. lw~7$$.<4Q*RG}P\],w={ȌJ !^aB!>GS-96|da_B)}pTxHnj\O&SZZ n[\L$kꋊN[5< h(/INZG ΪR[[In,[k>fЙleINdFiSe/~K;u/Ib2z*A1]/1{y#g尹`~)x菦Rȴ[ŗ}>[L>W; ۱Ď8sK^8۱ ؀Mt !!TQjX,/+$aZ|ܹg̙{OWs{;#WKi^.\V'm77h8מqi1DbaCKMn/Ub,v d;E9w=4-#텤ʼ* Gs+' Ѥ;ٹmه'%)g`O-~Ww@–XKDN2llj;\|p*qKRҩ'\zc}d\}LH6zH(+r1U[z=}b)O{ 1[_荓Ï; hC%عw;9y6 śWmh Hy%>fE 1l(   3\0(J-b2"PMP_ S=nR\.~`}n [XLWiSK·Ӆ)d U!  ;' s@dDm2b$ MS/aRٔM] U$/A,# \!s%%M]lD$S,S$UH%2t*= HD2L& !t@f8vֿCԓ,yL:Bhֆw!Z \T;`a<?OwiA,# \!swOS`lhL< hC%عw;9y6,XpG,# \! G.͹s,)XpGE.E,# ɳA ph]l3=!n!.f($i3g, BbiM !$E[i+;E9YםIa- H>ɲiU;U0XpG<')*8vwhX Z{ۊa,8{TrX &<#`A_^XH־Ŷb+;E9Q2CDŽeT!Z \T;`a<c6cȫ鑴V,T ǹM!m8<*Vȧhi_Vw'o yzc;08rhΝWp׸OOBaNcڻ <$LgeS)Կ#)g0$ܔ hC%عw;9y6t\g9PsF{z:B ̬CڞT(XցNhHfK\drI^.k`WY|/?/30⮶345:Ǯ1vfC/Riqz|8݅# r=E#<wBA%عw;9y6y]P7}K:2gն7+tƥGԬ׷bsQ)EKj[^Ԑ MIdoίږs`d} ;+g3/kؠ.9{t`(Ⱦ BH^]bnm`/j{J;Mw?{%:EQ+p/z'.=b"BCji`7ae|b1lNwKjmLˋ_|kӱg硱 b͎#@Zk{ Lw;|Ԩ 'z3+br7tk[5w偃VmMa^E!C%^LX +%z |笭}'2u}-eY9{^~{$H7n{'+'skk'L%/ߘgԨGeWO{bʭ'0I|bh=.'+-[ vbk2QssXp G.͹sK½y|n &|8igH(zJ=21xR ]RSQodfx%FfgfdYٙO F&"|.Π]=w3yuLw{D,vr:_lѥ)a2NAeܲܲ؀p˿GGN(;U0XpG<'GnUّVvn#_y\q:Ts1_ rÑ]gWmK6nKh=.hܬ[.֞E9!Y74nmwiRQ|og[Rfl!_r;1siXU,*BⲊ&u5ˀ8sIL^Zqd|E"I70WFBȿR.v2.!Kʋ\4[\RFkJp;zi|yv&Z_Z13824-UqLg[Rf} ˡBu/.?sҎ_l;X%eTWVu@8yeul(5>(z\pyznTrT*_]Vp70=A,# \!sSU5Ar`ac oNN Ξn>‰I6C+ j 81qo_GQon}CWoS/nzyLc VnI>-//)C4;U0XpG<'Ə&;y:8!>!mƚf6jsxcCe=3kcD6sIr[Xs]s/,Ufs*ԯ~IwQNL&]R}o.txGƄZu=~EC_]};+QCjtj6xUp &PTGƎh)x9D/KR½ܺh򯜺bKrv{JF=Xp`O/aji;!:5Np+%{h\Qgkw`߉okQD3w8E@Ͻ[F)\yh/6YښAdUpGc ˮ9-mjesX&_Rڲu%y6LsJp_ul(59eO9pSXyL0/6{ (^nsXp G.͹sfV&kc+uuH_H?^{><4>:W֋&!1KL̍Z!uWGG 9 a2  7 >A}t{e tW{΅Z8ճ7!% LDwGmhL@Ui-)Q4%f znцhK(sQ wsl<&꫶G;[ IK}6CÓT*1nN2lbLkfzR͵vb&FPh['ƚ}#]k1P0!1{xj[&ol itGs - Tk:5 L8r ȿEBFk5wWoTޯhК0 &NI,̚j[~kcȄI`z;*mth R>|Rr}(/mN-BQ[cPuTGKFQ4*ܮ^'O{V1fC}C~ E=%sQi:YH2<.xd3n_q`>F(nP(FGplQ5oxHK\d2ypcibT(뫚*,!PlCp E5!ñF7O"8?}Ms]B!gikCgҝ=FxԃD I=}0({`FIc +{s#3ہ!(g ϙ^bD81 Cgi;08rhU!H.Cy2o`cnefegoѧ0{G7ö.XpBz;B1@.I, "P`  xLpR53H&vwɧZIb[@8LV,T@?_H!rۊ_ #aH$.V,cc<- "P`  ;`a0r0XpGC4bD\B*XpG,# g Ȃw;`ap" ќw -r v.`a0xN ,#  /Ww0Ds.1h@%عw;9y6,XpG,# \! G.͹XpǠ"P`  ٘_p\{ZࣥY_ǀxR2[y[7.DPiTg?{2|mbT8oyO%s`fc<;Τ.cKCP4:8697oaK3(9G(iMu>ɦhղ'HvV*O.L,Lk5{&c'ʠ?-艳1Ȁw0DsEK}[_ $֎- mZ%4Ikc3X i`c)`\\=k.4 hC%عw;9y6dTzr|TXsl{]9?ؙϓCOHt;-0ɂw5wկ_)A+G, u-o/020zSsP)72 5{&g5:~I7Xu:x_9wCs}`xټC_=?w1S4Gg^=TT I748rp{3|¼7rc7{;4tuBt hWp70[ƻ-zF:Ͻɱ'7]enfOfFzD.ۋ[xjY_=6(zw 2`ap" ќ;oUWGԵkޑvtי''z:+&3XOPKwZ;ڸ8}nWmIci*цhK(sQ wsl%tQ+\K+*P{k2\]>20iCQl,zGaDZ*F9q LP{IN-˩z¸iU*7]0&;[;\va:z۲y,hHgv{8N݃:L z<訉QApAL;m_DXfG|z%!L"ʺ368Q=$<7T/GHEK'nv.: 9_Y5-{aBUQ|#Zב9Fxo<7[H72=NlfgbagRYP:Oo =C\2yrJ- =O-MSccs}P&<ؗg|\V͝0َ>ý#p J=]Kw1:^qLPĮ Kuན+^L&5t±_N}]׼sVo{o[Imao9 ]ѫBsNF)R!ۙX9 IoNLjK]pƊ=!I྘5a\mN鍻&ۛ~2-1no_^*(޶Yrz3ΣWpgbet+|)7vAn񑉎n([|#=>7UA\v :kte0^sN 6@Yˤr Sў.`^sQ s;08rhΝCpis%m<.IJ%' g'׽D pΠ}ގ`疽-9M, e魻k^:u7أu=8֬:a҃90͍&_}\z$t4׵&Xp'D\B*XpG,# gc%e L4?o67RJpMo,R=v1CBi-#ؿt8^+߭tIe7.tE`͸squq]uQ3/)Ԋ{w[TSq=c^ћ{ܭW]*us8'.)`16DjC.=>湒/$v{X_=9%eO~G7kNlh/XĪWR})nB\l3R׿ :% N ƅo u]#aZj;lhH(F}߭j;|f݃l]-fao9c%ez-Tf.ꓟ|#ܷ+3;9:?T,ozpyl1t<2NvɞvBIͿZا w߅Ad.\^[hhW_ټ͕UlL$'|?&F*]9/VKxlk銐OWl.~Wiә!s;+e0@S~ma(f" 4UB9iP>0& aKPO,.c߭c;08rh;\׽́K^]}p6da;^3>=nauJp3_RQt/ ދa p1Spg/秄и-^g|Cc{eukO;{J\zݒ֞/ Xp'D\B*XpG,# gc Ir$ozOXʒ]C>qa}Esxj@cU AL$ѩ'F$ٸZ 'EBQk]{pbR${ulhjlv<Ҫ*>u[}S"s%!If2 'Ƅ[bdT*9uk=Z֯Ez8]D]L3*O޴T@n*j˛jJ+5Ԏ[ɥRyuQR98OmiXsQRLP\:pC~ɡҬ9A~$-+&ۙNNLeᣭ`L8=WJo )%)/|KG3S[աfv&Rב!| 8]-iV+syUގT: "8W,?"5BYjDѫBLpȥ7aHO 0.ɵ7TM`s;=+[8[^t"uk̥CӈePD,XTt'upz%d :Á1 &1~[7S_N=`n]sڥ+ LG&.ɓKK,L`^*%HQA&j4įFTX1{` XpGC4-8Z+LVF]; FLZXI^T" YrfdSqPiԥ)\>G8! & ]?o`$iM4čk64#iu4_WZp?"1c ݸx,hhRT(o]B60bDXw!Z \T;`a< N!.ֺK߫. Tj) %b$Pjiee T$LRZd I.Sҩ ~R`cC2Dxgi9V(VNP2Kj_[姌T*~r{5Ш-r tzߦ ҒOT O蝣ooG3}~ۼTShd2yQ0a_)UsL0t@C,c #a\u=0T]SժLg1Mł;-r v.`a0xNypGypG B㐉ypGypG ,c #aܹpG , "P`  ;`a0r0XpGC4bD\B*XpG,# g#wX DzLŶb9\>8i3ۊRph1 L!- \! G.͹&T mXdr QhK(sQ wslĭO. DaíŶb4&8έj^l+V;c{W>E xb[p`ap" ќi̢rۊ`R&X*D\B*XpG,# g%etpJgb[@d L79/ D@B䗑"Db[p`ap" ќhˡۊ@  iU;U0XpG<'ܑ w+;E9 4 Z \T;`a<XpG,# _ #a\,cЀhK(sQ wsl`Y0XpG|B,# \!sAE.E,# ɳ1NXL& F65))ʧL9P.[c 泇bH$R\SCp38\6L~J3T@H$X`%bH(0X$u(T 16,OCa؀ciEj!eс,.Fē b<ΥҨ *?d3 S?#؅^U(@yɌh d;E9w^aLecBp*j'Fgd2˄Oo P'$cCѧ < O*%XpGE.E,# ɳ1nng`2N|qk\+G̟"`i&V&-k~v|2K#Kj4!! vy ^ԝ}=݆Gʙpº9w(6-qr~,O`ao~jѧ0<%mXզkƙy ?s|]~QvwP/O1C%vut3OY^aFo-laksFŞ8{` XpGC4-s -NL/myOۀ͕q|6]ǀlG\{Sefr] Lt/ܞ7`m;U0XpG<'\;DVb?OdSd1)xT,B!I[|j鑁aP)jjT_1tD602 SRwc; |,6sRntҊ3ν "~M\ӽ`qtwCgBg@D&ٺظ-q;£4:U)fyR0բ/ϿX03n$T"ZWgM ꀛtݥ×T{4''{G-n*7! d\W:y;ڹ]<5}^9/˜M~yΥҨtӅ!ܖjsfs pFk_̠(4wsx):8ocaRT_u@oysW~X}~z' Х2YD WR61Ȁw0Ds܂[OOu2[H0 zZbeNN<Win̪ #I&RiO_}g?bzzIS(2+B|I3տoOk‚;-r v.`a0xNyp9k_^l̒6$&ET*G̍VHD:AGoǥ#'F'zßՐ^΃b]JٔLg)4`oA-)uuMT*%eK _ⰠdGS(;7[~YmY^ηw@*oUu6wnNzŃY2043<+k HgS7'sz3.vv{u d:e,Ⱥͬ-eAbDP.v P N^Q鑓ɱ,6$ &4ė/ۙ{_<*K{;r2o$oLRiԑ_i>:w {m#W`*at6wm|mD,1:tybd<}gT,1017$ԶUozcoEٸX+ o;߈"G/GOGZ/X9Z&Ie\1йr4;<9FPVܫ*^- Q9<cKTfH 3z;{'̛L!w>輞y#$1IK^z%:qkN|q;xm}VAo]ܵoO2ve Ƅ@7yaCDB8}2JlC츍1Ȁw0Ds$Cڗu0/ +J&??z3#P(Ǟ!Hkj_=W_q"H~ގ%liox=]!Cڶ'&F\dM%W1 # p7V==$o5=Xp'D\B*XpG,# g{I%5ՔO\[U\m`i`ft䓣c}_ej x,FↄҶd:)m|}×KKMpq < C33.<{T2<0їsgG{E~p0_720z;N䲼"ܙ 3𐮡gpp}eoO3pϠ!/Y-7'זrxl]#ݼ;=\UT.o~c#[W֔\+qqh꨹sΠKĒ/sރCWT޺+LFEѕ/6SOX  L 0|#|O9chtcƖƆ>aޭumI}|~{ֺ҆:pٽ#D+^]à]]pX8Bן)ƙܮ.:.Iҟ_.KLmL6s6#S(~y ~~:(O`ƶ_m=Pdaond ?G>=嗛`tљ-<;DA_sW EkcO>cfy?'wl.+B>􉮁NП0n =tY˶ɤ-!e塉sXp G.͹s?mbmt=`o?3/f́Om|cE?OYu?Kٙx6V]pK YGB6kw;A&NUA.vV?T/`K1BOF~Xp'D\B*XpG,# gccu7MIMUǬҨv.݊]s=3' zwvL'zGY\vꘋR6']:tY4)]GUyCUcF;W[HS_e&mH::$HitYv]\|¼%QKz{KR7'=&5Y}iSN\u YymYGG! JRCp=5ܽuc399wV@TTZ9Zyzw o}sWf˃utr&k՝Kkڟ{m}͝cSlo[sŞwv vuw4jyT֡K^]yC@爄"7/'oLhwt߻S͙2#F`@NnK]3ĂN9mK`X2 Hbg:47FǶzkѕbL&dRY@C |Ƙgʌ1M`~Ka o{kw8MImi:臇Xؙl( ѩ C4Cx(T 4Fiڸ:&yn$-V2 ŕsXp G.͹,G}Ҙo>^/G<֚WS?{z%< ܐ{X<)9٩wR"g׾#bY Av ִR)Z$[UEulvnzs%XBkn7 >=1;C "P`  ٘Gp'H/~Xy~D҆bgwĘWff~~q>#rtC\!/ZRw(?Pֺܳy˶,sufBQKmKQvη>aަ6ygomM\|jيsy b + O|qW9 *3z=\6wfGSǺW׎ y쑁+Glys𸞑nEAnfcam=m( *o1ЁFo^("E@NNLwpՑQ&{j]5Yb˲e[9ZEG**Jg2FF>2ܵzE6K_~iiSt󺆺7/W?.{GEs_;Tu5wgnbI{S";wUSkPԒ2YdR ß sist+k+6iOܧ(Z c,,9 qcKcPi4ډ/O%A)PF_mOhg߭˅+f>cagvcPf+k(4jSuS}e=RH2S8!\J%TB\ m`jpRaʦږ F\T<w 2`ap" ќ;$tUKS Zu qMh4\.ܹڜ03NN@m汚k=.oҩw6.x=!lUq\ L20~`OS+o|c@ xWkHĈ7$Mw!Z \T;`a< V#I¶`\*d3eR>2X \.L} I &HS{Z|sщ9Ljs"ͨ,S,LƠQ(dPV; R @5uqko՝KRa>呩Nގm Ml. <;=g2\M564* ]7tMO||BK ?V^NMeӂ;d+^\uay.Z }rKSP`|Ry~7g,FXjĩ/Nlzs {jQr=ȳMHe^`cUWYYPUY}jJ5m/O {Rgn,w& qJDGw/ܛ1X_{gT'@6@X響" n(VjiuLLWWϙ9guҮ.,wEd!,a'!{HBH GCG,lEGoԩ^~f**9zud  { ;f.}Rkak5:kk҆{>.MWJm5m>~uGwߑH:ͅLIJ7@Mc,{VuQugAqxWsgջ׌&dNl0XsuZwphB?f90̾aE v#])HLO~,00D,zX!Uԗ,j w 2`0x"^XKl2hCO>7/~dh4i73nҏU.ҨnNMe -3Ljj _|8(d*y\3n2,_snd?>ј ǿֻ'T,$']y-%y3>,-&O{ۅID}ѡљG@>Csm;GqlZ*H^4.5Π7@ gj;n+F/u•JI+/( >b|%~oAky,\U;G?]*ӤK*ۻpܖgMkf\} IٴTU+] &%R6,7Œ;WV.ɤ6aG:Ó"^{ Ig8aX Dټ9]2uJ(\yBz>,1/¿arc6:^~9 |P:lGU\^Icf*65ޑT ך40><A.z9$&HTٴ oNr* +Hdҡ@-WC\Z &Qʔ n,daEK 7wŖE7"YJ2Z3/Jٰ]β{L63zi^Uٲ3y!NnN>Rp^̲(enM>=vĕ Prbzbcy#Rh뽫 \Mg< )y:7V}9}pq+fk^…Glv W}Y(uad2}w |`A,\ e>3UEUý )LݔT;浪o hci#עE, ws|3`0P;pGY.?%j퇮{H"Q-'Oc|Z'D|^CW}*a~Z? apGP%QFE*X# YpG, w#a,-XcFE*X# YpG, w#a,-XcFE*X# YpG, w#a,-XcFE*X# YpG, w#a,-XcFE*X# Xpwc*B:FSUﺒo5 9/X)d'W$L'ᥫP5K8A\%FZ;xgLs_ӎ~|;Ddyǂ &AspH uÆL&9ᅪpw:Rm(QvL /]=dxXU]8rba&daEK 4D!_3,FFpGK.`0X# c=8" w`^4w?zו|cVoI+o yݖ͌ gɓK2>9 ^~_MU4 ӎؑ\>qKX5~$85r.eљ3ܙh;C2uߘP=nٜF.h*)1]m·[r9*^ X# ci=bq$*{b %4!7?n;Xȵ(ppQ wa<B9,O_waM899aa$Wgd| wgv uޜB\&q8L")TD$3߁jC"Xu )a ~|gށuitg^?#Ppхŕ&ih~ѝҡzݚhɴH2h?`0 `}nͿP*6O~3/ ՖI^rsuhtnxXu0AC VQXv۽*4 =収ȌF_ Fҭӌ3lDܗ.o'ZID~/ņ=qqszC[4eMn=˳s yu!=ks7Yl`FL>1lZ+B@UGsHL Q C@p<] zCa9Gb ATȔ$K{}* pG3;gH4ww4w)Ë+ |}H6fƦFsEZ=I@=|GŢQH-xg\gBKa*L/W$(b$-[s ;pG9rb0iѩPȖn 9AώTjэݞ~ wW;la\{q]ݝٓS)ܡB@̢vO?ٿɤhmz8u2t7C 5[Wu| s j7[TF0A&8{',6e@8$YX`0x"\{ĭLi Iu&ݖ|sjmqMp|L{tDžd}5lQx@_Ac+M=ΑwIFL&'^ZQfgc ,)FKw|d2gwW ӓ`TYXa.ݜh*o VX)lYpKpGK.`0X# U=82yc*,/+=Ask wH5 r9t`edkJ\'u[}#=GWp؛u&wt؆'G Y=.6asM]5OHo̹pW&S77fQ86'.ů)lIP)A~ ټmUILS,KZi֮޽n-}𬧣oNiSbYMY#?l[kmʔ?OUKhkurpwd?J z;K< jC9o}a\붯tuwʡGU+7,zVʚ$218"ʩ[*zu ʸ.^=>oy%_'C ٯLM[uІG@:r$bY΅$qۡ 1mpIa%'/\l`Ԩ8 iKtg.NX_w(*FĘNvOW]v\r=M;dtT>[7D-':?Ҽpy',l::۟?qm&CfA Tu\;FGKV_-퍝r#iy Hn[q;d{L!+JgcQ~iWO(c?ۗu: w|B,\ {NK.9;=} 8?SVuuJM9ސ_.ӑ딼~H߈JImzqsN݄EO=+2o͋I7|or[w_ⰺ*}냅;Xȵ(ppQ wa<W_JKٔ>|y.O?%ޙ/;".PcrL8rr.)$01as`%>ߙu:w||Vn<svI̢K==LZJ/{34:0<.01ӮHitEe)TF y$qAy_-i4N]N\svs]~7al)Y,I o=_eOZ*92!go7V')dg-½wa?)܉D4on8pWך Ede LO5aڐIE+&Ye?Ն{fuit8RݗI$6:__&H6?~ J[:w+;I;3ӱl!ȸr*' ԏýq.N4i C<˳/wݶAܠȀZYYv,hVHPpO۰A8x+ӪMYNMMQ`5iK˳KVCゥp_"aK>k43v ۛͮʂdA*)T`A0;kǑ#b7'p|>dTmLܽ)0aqj.!aiEG2a7iaeƎqDKs!l5, UpG 0 -%} {% for param in param.opts %} **`{{ param }}`**{% if not loop.last %}, {% endif %}{% endfor -%} {% endif -%} {% if param.secondary_opts | length > 0 -%} /{% for param in param.secondary_opts -%} **`{{ param }}`**{% if not loop.last %}, {% endif %}{% endfor -%} {% endif -%} {%- if not param.is_flag %} `{{ param.metavar }}` {% endif -%} {% endif %}
{%- if param.help -%} {{ param.help_md }} {%- endif -%} {%- if param.multiple -%}
*Separate multiple values with commas {%- if param.opts%}, or use `{{ param.opts | first }}` multiple times{% endif %}.* {%- endif -%}
{#- bandaid to patch in harborapi query usage -#} {%- if not param.is_argument and param.name == "query" -%} See [harborapi docs](https://unioslo.github.io/harborapi/usage/methods/read/#query) for more information.
{%- endif -%} **Type:** `{{ param.type}}` {% if param.is_flag %}(flag){% endif %}
{%- if param.type == "choice" -%} **Choices:** {% for element in param.choices -%}{{ "`" + element + "`" if loop.first else ", `" + element + "`" }}{% endfor %}
{%- endif -%} {%- if param.min is not none -%} **Min:** `{{ param.min }}`
{%- endif -%} {%- if param.max is not none -%} **Max:** `{{ param.max }}`
{%- endif -%} {%- if param.default is not none -%} **Default:** `{{ param.default }}`
{%- endif -%} {%- if param.required -%} **Required:** ✅
{%- endif -%} {%- endmacro %} {% if category | length > 0 %} # {{ category }} {% else %} # Top-level commands {% endif %} {% for command in commands %} ## {{ command.name }} {% if command.deprecated -%} !!! warning "Deprecated" This command is deprecated and will be unsupported in the future. {% endif -%} ``` {{ command.usage }} ``` {{ command.help_md }} {# Only show this section if we have arguments #} {% if command.arguments | length > 0 %} **Arguments** {% for param in command.arguments %} {{ render_param(param) }} {# End param loop #} {% endfor %} {# End argument listing #} {% endif %} {# Only show this section if we have options #} {% if command.options | length > 0 %} **Options** {# Opts. Example (--wizard/--no-wizard) #} {% for param in command.options %} {{ render_param(param) }} {# End param loop #} {% endfor %} {# End params listing #} {% endif %} ---- {# End category commands loop #} {% endfor %} unioslo-zabbix-cli-09a2fab/docs/templates/command_list.md.j2000066400000000000000000000006451471265333400241620ustar00rootroot00000000000000# Commands There are currently {{ commandlist | length }} available commands. For more information about a specific command, use: ``` zabbix-cli --help ``` See [Commands](../commands/index.md) for usage info of each command. ## List of commands ``` {% for command in commandlist -%} {{ command }} {% endfor %} ``` {% for category, path in pages.items() -%} * [{{ category }}]({{ path }}) {% endfor %} unioslo-zabbix-cli-09a2fab/mkdocs.yml000066400000000000000000000053741471265333400177360ustar00rootroot00000000000000site_name: zabbix-cli repo_url: https://github.com/unioslo/zabbix-cli repo_name: unioslo/zabbix-cli edit_uri: edit/master/docs/ theme: name: material palette: scheme: slate primary: blue accent: orange language: en features: - navigation.tracking - content.code.annotate - content.code.copy - content.tabs.link - content.action.edit - toc.follow - navigation.path - navigation.top - navigation.tabs - navigation.footer - navigation.sections - navigation.indexes watch: - zabbix_cli plugins: - autorefs - search - glightbox # images - mkdocs-simple-hooks: hooks: # NOTE: we would like to run this on_pre_build, but it causes an infinite # loop due to generating new files. # This might be solveable by migrating all these scripts to mkdocs-gen-files on_startup: docs.scripts.run:main - literate-nav: # enhanced nav section (enables wildcards) - include-markdown: preserve_includer_indent: true - gen-files: scripts: - docs/scripts/gen_ref_pages.py - macros: on_error_fail: true include_dir: docs/data include_yaml: - commandlist: docs/data/commandlist.yaml - formats: docs/data/formats.yaml - options: docs/data/options.yaml - mkdocstrings: enable_inventory: true handlers: python: import: - https://docs.python.org/3/objects.inv options: docstring_style: numpy members_order: source docstring_section_style: table heading_level: 1 show_source: true show_if_no_docstring: true show_signature_annotations: true show_root_heading: true show_category_heading: true markdown_extensions: - pymdownx.highlight: anchor_linenums: true - admonition - attr_list - def_list - footnotes - md_in_html - pymdownx.details - pymdownx.inlinehilite - pymdownx.snippets - pymdownx.superfences - pymdownx.details - pymdownx.keys - pymdownx.superfences - pymdownx.tabbed: alternate_style: true - mdx_gh_links: user: unioslo repo: zabbix-cli - toc: toc_depth: 3 nav: - Home: - index.md - User Guide: - Installation: guide/installation.md - Configuration: guide/configuration.md - Authentication: guide/authentication.md - Usage: guide/usage.md - Logging: guide/logging.md - Bulk Operations: guide/bulk.md - Migration: guide/migration.md - Commands: - guide/commands/*.md - Plugins: - Introduction: plugins/index.md - plugins/guide.md - plugins/local-plugins.md - plugins/external-plugins.md # - Reference: # - reference/*.md # - contributing.md unioslo-zabbix-cli-09a2fab/pyproject.toml000066400000000000000000000103161471265333400206370ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] description = "ZABBIX-CLI - Zabbix terminal client" name = "zabbix-cli-uio" readme = "README.md" requires-python = ">=3.8" license = "GPL-3.0-or-later" keywords = [] maintainers = [{ name = "Peder Hovdan Andresen", email = "pederhan@uio.no" }] authors = [ # Historical authors (pre-V3) { name = "Rafael Martinez Guerrero", email = "rafael@postgresql.org.es" }, { name = "Paal Braathen", email = "paal.braathen@usit.uio.no" }, { name = "Marius Bakke", email = "marius.bakke@usit.uio.no" }, { name = "Others (see AUTHORS)" }, ] classifiers = [ "Environment :: Console", "Development Status :: 5 - Production/Stable", "Topic :: System :: Monitoring", "Intended Audience :: System Administrators", "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", "Development Status :: 4 - Beta", "Programming Language :: Python", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: Implementation :: CPython", ] dependencies = [ "typer>=0.9.0", "rich>=13.3.1", "prompt_toolkit>=3.0.47", "httpx[socks]>=0.26.0", "packaging>=22.0", "pydantic>=2.7.0", "tomli>=2.0.1", "tomli-w>=1.0.0", "platformdirs>=2.5.4", "strenum>=0.4.15", "typing-extensions>=4.8.0", "importlib-metadata>=8.5.0; python_version < '3.10'", ] dynamic = ["version"] [project.urls] Documentation = "https://github.com/unioslo/zabbix-cli#readme" Issues = "https://github.com/unioslo/zabbix-cli/issues" Source = "https://github.com/unioslo/zabbix-cli" [project.scripts] zabbix-cli = "zabbix_cli.main:main" zabbix-cli-init = "zabbix_cli.scripts.init:main" zabbix-cli-bulk-execution = "zabbix_cli.scripts.bulk_execution:main" [project.optional-dependencies] test = ["pytest", "pytest-cov", "freezegun", "inline-snapshot"] build = ["pyinstaller", "build"] [tool.hatch.version] path = "zabbix_cli/__about__.py" [tool.hatch.build.targets.sdist] include = ["zabbix_cli", "AUTHORS", "CHANGELOG"] exclude = [".venv*"] [tool.hatch.build.targets.wheel] packages = ["zabbix_cli"] [tool.hatch.build.targets.app] pyapp-version = "0.12.0" python-version = "3.11" [tool.hatch.envs.default] dependencies = [ # Linting and formatting only here # Dependencies used for testing are specified in the main metadata # optional dependencies "ruff", "pyright", "zabbix-cli-uio[test]", ] installer = "uv" [tool.hatch.envs.default.scripts] test = "pytest {args}" cov = "pytest --cov-report=term-missing --cov-config=pyproject.toml --cov=zabbix_cli --cov=tests {args}" no-cov = "cov --no-cov {args}" lint = "ruff check zabbix_cli {args}" [[tool.hatch.envs.test.matrix]] python = ["38", "39", "310", "311"] [tool.hatch.envs.docs] dependencies = [ "mkdocs>=1.5.3", "mkdocs-material>=9.5.13", "mkdocs-include-markdown-plugin>=6.0.4", "pymdown-extensions>=9.11", "mdx-gh-links>=0.3", "mkdocs-simple-hooks>=0.1.5", "mkdocs-version-annotations>=1.0.0", "mkdocstrings[python]", "mkdocs-macros-plugin", "mkdocs-literate-nav", "mkdocs-simple-hooks", "mkdocs-gen-files", "mkdocs-glightbox", "sanitize-filename", "jinja2", "pyyaml", ] [tool.hatch.envs.docs.scripts] build = "mkdocs build --clean --strict {args}" serve = "mkdocs serve --dev-addr localhost:8001 {args}" [tool.coverage.run] branch = true parallel = true omit = ["zabbix_cli/__about__.py"] [tool.coverage.report] exclude_lines = ["no cov", "if __name__ == .__main__.:", "if TYPE_CHECKING:"] [tool.pyright] pythonVersion = "3.10" venvPath = "." venv = ".venv" ignore = ["zabbix_cli/commands/_dev.py"] typeCheckingMode = "strict" [tool.ruff] src = ["zabbix_cli"] extend-exclude = ["zabbix_cli/__init__.py"] extend-include = [ "pyproject.toml", "zabbix_cli/**/*.py", "scripts/**/*.py", "tests/**/*.py", ] [tool.ruff.lint.pydocstyle] convention = "google" [tool.ruff.lint] extend-select = ["I"] [tool.ruff.lint.isort] # Force one line per import to simplify diffing and merging force-single-line = true # Add annotations import to every file required-imports = ["from __future__ import annotations"] unioslo-zabbix-cli-09a2fab/resources/000077500000000000000000000000001471265333400177345ustar00rootroot00000000000000unioslo-zabbix-cli-09a2fab/resources/help.png000066400000000000000000003261751471265333400214100ustar00rootroot00000000000000PNG  IHDR/6sRGB, pHYs%%IR$"IDATxXׇw&({Xc5KbMF{4Qc-{;l( *([nov *Br~̝s={+D"H$D/"H$D"/wH$D"(H]$D"H$pD"H$2-͛ŋ {*T0 vzѳD$D"H$z޶m[y]\rլYiӦ}٭['"H$D"Q߂c僂bcclpp`ػwobbg2e܎?~Mlҥ{V^JuH$D"?w''aÆ}ܑi7m400`޼y&\={GEEi +WtRttǘ1cI D"H$k[e5kꫯM:Ç[[jjղd~W͛ʔ)+R5'|nݺ͛Xb횿;+:v5k?hH$D"()5p (дiҥK=c P>ͽD"H$D0xzzFFFF0`@||{c"H$D"Kͩ{#""^#"H$D"KE"H$D"E"H$D wH$D"(H]$D"H$pD"H$2E"H$D wH$D"(H]$D"H$pD"H$2E"H$D wH$D"(H]$D"H$pD"H$2_9::/_͛e?GNJHHxQD"H$D颧"E-Zt֭:^zAAAΝ{=f͚%K˗/L&WW &,Yd޽/گ˖-ۨQGhxȐ!3f8y򤥍ܹsٳg?}~~~no6mڴjO?HЂ}||~׳Zjg7n+WN䕻`0V^bm׮]<<M>}jq8Bll,Ȏx/MGIG7oS·5k 7mڔ y󦽓uѣdSLaup|L9r?>{,9g g6#F;vo >|x9Zlp^z1G75E?bͻ˜:u ꫯv :t۷9(g%9FuM A0w"H$^6#p߱cLS`;w@QRL&Ӷm~g5Fݻ`mf͚9;;ֿ`nT[bˋA'.]ʕ+/\ B'N`%ܶ`ЪC*T8s H^*z =zŋ$n6h@-_~fߢE>s`jٳ6BGpٲeaVv߽{u>믿޻wsk֬F|%b mB@K:ePpWf֪UȞ=;"[`50ߏ?H nGVX.[ne^^D۽{\rU xP?)9@"ѵ,HBl6a0pgwL3Ƃ܆SwJŋ2b:g4V]`k׮111T͚5j$0!7h h8(pg*ilKL),Yb(EL>]!&* e˖$̰aè(jHiCc #L'.D"ѿCZjrd Ѻ J(ըQ @ի7j(RL6 aaaW\V`5jn&a2X'-g62/_xK֭ P`ȑ#$8ojLu )R O4Co6CtEo6>a~dz2Yv-I՟hhl.Pܓ X#o߾ >|bwR|#HrSa)MeV]$D/=zi`HKAO0gϞp! wAS7n38 8lhX {PߘLp`p1o8p`„ 4(]4 ([7{RJ>},OPd(|˖-;]$Ӿkkj_)4"H$ ['0dx4ܤI={@E:u! o֬YhZ*::Z|̙3S2eFuW`$_~ǎA~mŚ{_}U6{܁޽{CHۉ'F ϟذaSLn`dx} 󟏏Ɓ*)** L`qTq"EXFCi:Fd(`@OpWFZr%[_0P]jPàw3LJ f~e pt6p'ڎ@'e SP_2mRweIdX`>le~… @}b"*g%P=Sf2m%?Maxx8رڃPLekpwqq!4a/OXf l׮3g $L&+}K˼yv.ȱ-[5J;u#C%EKJ2OL+۽{ Çaew.?9tޭ[7עE JbR葑&8 q ̛7/՗*{lX(QBuGC=ƾH$DJO*U&)00ޅJk׮ A9k׮VZѣVjz=,Yrs4hse?kmt)SիWǏ~Ǧ:u긹EEEݻ^[>իW*)œEVz hH6NNN~~~ׯ_7LOVf9͎IbblE EGG_vͲa;wn^L+WŠc޼yLpqqqeM Gl{D"H$RӁՂƍ,X`ߠoV۵k\4z_? Fh[nԨtС3f̙3gذapjиqczUVo)ܹsDDĦM|/ɓ[!MN"EbcceeǎV\S@۶m{rJׯiv"!Cdɒɓs}[hѼyseq̘1Νnжm[9b˚%K=yE"H$JOYfܹscǎ=uT|huYp0a}ҥ8{ԅaOOO8;;?h4{xxxBBkgNUT޽A>|H_ǎ[x_~iL BЭ5]Ne//[nEEE5 dG8A_|6;w',7nܠ>>b&Gj UZ~ -%UD_g111a.s]Kc#h(^{… /Z1ZmR0tlX` @e˖֭[4>>5 6,,ReΜP0Rzv`-GM8͛Pe>#Y|,Zj+W.]SL񌂹⧟~bvH3ӧO'd5h(VWbRD"H$C=2` |+V4Hb'Od%@ QfsFfڿ:u6lg@Rɒ%G9`Mw}Ytt?hYc&3gΧ~jaAkݺu_U]gwƱ&Meʔ)i0j({vUo 3gtrrٳg1hт6P&ݛpq"KhZjÆ Ǐ_P&=0K(sN5$z pL`7mڴvZ{;VPΦ9zYYT)GpXb,LϬ1P5M `CJR:rЁC QNر>rӦMDI>}]tM=.\x ̙o {qaՋhӦ ƍsʕ8{ٓ:P EuzqrBѾGH$D͛OerȬtҎ}5h $wc\Z5$ߜ )pSĎ,}A]toqyZpWjE4_֭[C;I'~~~0ɓJ?ܹsa5ͻw5QBbbbȃ  ټy30 j///<]v%ڃ Y9IЀ`s=|p>z*S? mԩl,X8r C9rE͚5R/1Θ1'x&P"[n$D"H$'z:p1=s̠ >µٳgAkȑ-Axx8\~ooo *T:ʕ+gcǎ=x૯ Z;ydٲee˖YVsp:3n29mڴ[n,YrҤI677# ~qСmʕ+[os˗gw8swwg2d{M8¬ 萱-ZLJ[ݺu;Dh=zt}ҥTtMTBBT,[; ÇՎL_`` JcKkpSNNCLHF,dNR9|e EOT^Q_~>؈ "g/ P\0LIۣG(Xq\%s@t^z/^)KD"H$}РA@<>N2h(__#F_}Յ ۟ƂYGpWܡg ÈFmPvmM&k[le`O?p@h?mMo}+jZvm>k֬.]Ѕ+L 8ڀ… iL,'SJ*iI4irM(EY[Xd [;V9rַtgΜQhd T8M:vXX1r`„ 6lPW$X'>9;f&O71-s.]}dwA^Zͻu5ZPҒ5y?\$D" 'OJ-ԩS'444{`ȑ#A7n@T>?~oy.\xԩ@^ !+Bu8XJpʖ- K4Vv17k֌Q/}v8.?> ؄rnnn^`1wܹs9s1cm|a8d{i0zhpCj$)W\`e= b?&5W\a5L^z X#{ـ;qf slܸa7o^SK.mٲE͛3pOw'$Abccr3f믿VG&DA3;AfS?e@9]\hdu+з~GҞŒev_|޽{s١PI H$DS3bY )@ 67.ؚ9sfbd/R]33A4oaJ 0BөMtzzzЛ#&B=E jtGG 5ĉ#=0co$??O#.,joc%SΝ;Ėhмz4 k$?܆86$,V,Y蔈$S_eAjmAr|^b >SxͰIK.b *T$ aQ!Qf*3|,NN ݩg[2PZG<#GY悐Tz #%qb ˋ ˂9#D"($oND"H$2E"H$D wH$D"(H]$D"H$pD"H$2E"H$D wH$D"(H]$D"H$pD"H$2E"H$D wH$D"(H]$D"H$pD"H$2E"H$D wH$D"(()k֬Z6"""!!!6zM\h|֭tD$D"H$7)A=t]L&}^{ѣݳɝ;w|||xxx:z"D"H$UF[:u;v wwwSdR`A>FEEİŋwttzjDDF||| . 9s&:::G2eb%Doo"E8;;\r%D"H$^f'(P(߷olW^pp+ * {zz-[M<\I&߹spoA3;w̛7/ *VXXXعs7n GFF׮]K/E"H$DYOYdɗ/ʨ[γfͺ~֭{Uٳg7nܨIV%J$$$p?˻v2 WzȑܹsS`h4^~|W)SH$D"eУGi۲ Ll>11ÇkrYx;v@ 4lٲ/_p7ͩ;h^H={(p?{,;+YT=H_6ѻ;88=B"H$D"K 77ܳwlFGGS 0m۶H$D"襓˫sNu^v@(|2Aax;..6H3HԩS V!r\^rի9sttt}\BOOOl,"H$D"˫jժ>|X=fk߿~B* clV>OR^Ǿ\kD"H$ꫯ ܭ+W.N'E"H$D4H$D"H$J27]5߹|D|}}Yyn߾}D4ewt:hN%S'nΓ1iز zj*sJ9$$qݺ[ NN}|3&LwȲoVG#ӶmMdhwoe:D?ݽs;vPoK֭[ժU\2zh JдZmϞ= mk7x@7N>իW22-rtt qK.\cȑmڴAK.3?wwwN c}5jԨ(z)WaV{'+TеkWɓ'O.Yd=t߾} ,v999il=uŠ^:]x1駟0+V$5apѢEXgҤI* GDD+VN׬YFYfA (f̘qy͛I !8~ҥEUNz{{S#8pf8 gϞ=<<)Bdѣo޼L">#F:ktd˗trll,s@$!)SX@}a`ĉӺuu&&&#)$9f $06`wr zaC2KEBdp} g__cpݻ}֬Y{ɾ|;U>[o6k!hRt nڴ)88V?{ge;v,=j qop^E^e˖aL+W.W>ZwVjlٲQF dw5 F. (qpرc_}ʕ+A=$8N3n3L/I,;HdXؙ<ܪdwҒ.p|'ׯ6lJ[l 7m|6m[@ »dxJvd.zw&I+SPQP0}aaa,IϪ"O?oN5k[NWh&2ĿvڜPԻA0/_>Upgw* LIIWNIa%^FǏJ*QQ`頤pD]Nt~HLHcJ=VMGo$|\ѽΩy=4f 뼳*ԊyVګW/777'Эp³g϶@a{ѢEoDD G1tP2up2eʀO_ʀ(R\`AÆ b{poժ/0 `NegΜyá dSN&MN4`nݺ1Zt /}ȑ*R0"O2|鋁4fMTT4֭[߹sGQ 8UppBm۶iTGLb0E_'T8N&IH0Zew53r68&[e8@-AmD0)85SLuadNH4/ DTN,EWLGwO ЕG=KAl2l[ZsӃgje<`5]:_?q@Ivd p|AaiqLѨ0LiQ~[7SW?tvL^bMڟ~3q];=#cV7e|M͛c/^LxNpA$~Z&Ѕ//] U5jԀ_#f˖VZT ЀYYEM(i6NjYv1hiwz>Aʜ9Gԝ,Yfc680<(E6%q0I+mށ6eʔTH)RDן>}ڂ .9z6NLcbTQY.X !:{y迬 nj= WdoSWR¸%pq#a̧IN_i|msysx5qơA;=a73Gʊ6K&L 3h Ǎgtׇzթn4$.Zx̙x77ĉ=<z4LCřVץ׷c#_O4y^iy߿O>Æ <8OXd ڢEβO?͙3Z5ksTԯ_={>k%Gp_g`NjK.ݼy3kڶmKHk"oݺ>z?~XY[߽{w /^Щ"$q.\լp~(o>L]IH=+D¾'[lIJI$YY[ӻ_&,g|Vfx^shd pK-Gw.'yӺg6tI?7N G٬uvIx,ki2y$],O8pH:Ύ|;bq:fL}Ǜ5$]VwqܺqvվR K'lsy&X}EV^mYhԨ9WsL͎׽֬}96U-V̹DI+8HM 2i$7 ,t`ܹS,[ݻG;+-===q zyyiګWrgO>8߻w&'Ni/s^ {^RG1jtŋ3m(]TTS_>|j vnԃ&rM%ϙ3G=\>@ h(z70j|pKD)** <;g=sϚ?w0LuS|LKh@={ܹW] Ŏ 1ٿ Ꮚ$^хhd 5at,hyQ(j 6 Qo$ρ۷/{ |?P%5ӧQv$ hKIF ÓdH *VxEIa;uMlbijq߅8Gj I5 J׫IX/? -oC#Ptʐ;vރb]6TP3[ًC zZ2pR=au1Yok~ԍǎcO/ fq cs*Etw]Ѻc5NT9f!䟖{ ֽRouPϋ :lX4! kӅo*1t6߉p=t7Jgz!q7 Iߩ q{sǘ/Kz1Kfѹerrï?w'=VO<|nOSu[0ưf` O{=z4rҝ:u+Wuwپga : nwa?}[/%,ZC>p3b|ʕ+׵kWλcƌ4hкuk]>Ճ a` ףGL=N>@@C/vӦM9sCSP! N B~0 BѢE !s1|s} j|5eʔwMU5j@O8Uĉ' \O?J#F`A`ʕ+IٲeY?Q… jjރ( 3:P0 |xKd5ߴis [l x"y &?pWdƎˤ3LĶqFY[f ݻw|$ -[Ƽ3 5-Ҁ Ϙ1BӦM[jEdH9`9r3du T$Y16w%֭[/_NI(N<^heĜ.~gZ8@TB)VFJz05a Ν;+f~7o3|Zbœ\Ҷw2f͚)D׀;"t$MIB蠶ɓ'1Yd ̂5(@5L ѣG6{uBz:ŸVREU,.]"6$8p©|ٳg#SOw)]WV^аemӃ]N{Oj˥oE>kpזɨ꛿bit!{N.4Y|}0>٭зZ`>iڼy Þ0c}]PD7gO)pÎ_k"ui]Ei®&2?iPyORh]sMmfi}WWGf6 |?AIWe:p D {M]12Õ+ uWt=[v?KNaaaӧO$_Z5kay.}}} F0m۶`lhӃ ꭥd!w>rچW` jF ơ,RL.].[{e&o߾8o-?385$$᭺_SrXnHenZd t `C^:SpQ4obːXU 3QS 0q؝w&skwY/&9Iq] N&4t zg3Gk]A1G?\`t^h)}޸Iҭ2W.'l{L|r[4 N~sal+&SN9»%'x(j:CCC97z*$ŘB,#˖-h3{9$zd.RXFARX[n8HIcs= w߭Zr5F?dz~Zj׭[Xj׮MӱcGDLak֬Y/E:`o~zFccc:ӭZpqlҞj/UʨHjQhQWD6oqš3$P)o^(!upd:Oݻw'O-(rp 0H~?@Q#Qtm߾*.]:azQ)YABMgӎe 5O`b*C š5k2@pLL 5 q#T`dϟg ^`w6C"L`Cԟ,&ϸ*.=#߭2J?M&mLǴ"5 ]z;͇Q5Ѱx9'nʹyr䄗t󫗯9_B5 ^jaqcd15_8xJWKils$pw [6W ]mH5,l[cܰ]dvr1.t}@Sඤ_=pWܼ4ʕ]/ضA\nq"/g{ahgPŒ%K8sMcC^P( \x;4fξ߷o:[s ( *ց)م؁MzaaӦM֧O*F\z!ZhxϞ=qxx8ɋ/Λ7ϦGj)Bӂa iusu `)p7bנ+W<ÄًM,O0M@OX?}tO+8:w3n$`믿fI' |9uڦON߶m[ /U3.V@t(('#KldP; ?&`Ta6I jN$ NMol رc38F(84h@KқÍ '$I^%KXIwwOOO*"ҥKsz|XՕ3  3kU׵jղZiвeKML:卽{ qY&&toNqB] . ܹC [pSeԿ_uq3F4iG]w4?1^0b#9:R˰{5k4Y[{S]WwwwӝZhH6H[0ٕzOh#oNw6=t%8M`i>4`9BXӵY۾oe% 6q\3ҥK6_N6ޣ׻y,]㚵]ܨ^m4y>@HE[o˗/P81CQ@`ll1 t 6l/_>&!iHS6n^@0]qyr 5;w7 RJ`7 7nܘ3g7ɹsڰS$$]BҦʕ+ˉ'ۓ'OB~#H,ǏC_+Vڵ+=<<  =057||w 'XjU 'pgVTTL Oʖ-KlAXyG2,"};qV&OpfL14m㛥SLMb6[~=hk% `ڀngΜj91E۷'3`4 ӒX˗/0,~cEE-8AԨもUwb=,X~1?'w=ϙAXbv? ~%8N{͞=˫M63d .]Ƥ[rt ;>cwKգq 1vMUNK3F,a=v!]9._o4.1YF*$E !#) СCEt}WVLAqwO"e,[`t2kzB#ZOmRI Lz&XDDv]@,>St+&/3MW5.ek={+vk:ӅS)UWAo2hYWxn6wa]Bt9\OT+*Kt#sGo/޽M&Mʮ.Zʖs=F"+?(6T_Tpq9n:t`:XKQ;T]h+u(ZΰxƐ23%aݿ{cxz= 2kH7矇GG֮ެihtt I|3e-\=vLT5s7gRzQׯL2qqq˖-MyԨQI~ԩSG+cǎ|5(<2 RQΝ-]< f͚Yn/0l0ի'N|N(TtF˗4i 4hР;w\u/^޲|>`S퓨tҀ3gϭ%J oO5RN+J8%=0efc6-M][P/mժ֭[߿cTXA)\ܽ*շt&|TzmfVsd:yg4a7OvZ';5,h=hUpqo;ͬ|K%u|ɞt-M|X>_ܩǠuE4^GޓJZ8䪏v%*&|&z|̟>sCܚ,>f fP)CIq]Ȝ-@{Yk?<5魴O9 >[nݼ:vv֟ r\ݻ@hQr\1CWӡCqf}9pϓGx{LÊJk"jr˜c̴ϙ]7o~a.N]dyA.]J8~<>[65\z%*pr-lnk6'8k?߷c͚n~#:u2M^xQ=<$$d֬Yi…9Z= r޽ynݺu|TGn$A ݥ-N)PX@ݝR~) vfx-o#ݻsϜ93s6wgǏ< $-/~V9rs} ~Q;v,w܀%KH8e̘eri%JtEƘX9*|}}CBB]v:={d AAAyXɓ5$BqA]ȏɓٲeۻw/uѨԩSF>O4ijӧO? "Pҥ&>"ߵp ̖-[8͛"<7 Ν;X&]R<@Lĉi2m85&!3+-&=///bE'NȞ={kd|geH{H8gI2I\;9TRq+S w^B0Nx~Y2eJӧOb/$.)R@I)vI~:u$J3gLKhX1ni)!d*j'cM`qXF5#ēݺ8FѹbH,XP,(.OwtTqgwƍTM6Q[ dxx8 #-w0x/9)~o]tũjR`{diOCMG9`WI~JϵXΟP2#-+sXv:m<[mc&JP"#Hoh4LIF4ZΩ]M*$BբƋoy}KH>&߶UU3撽}i)!nuke>q_=g2eͮ=>oZ}[Ԓ4U,CٺZdQZ۞MC^81~Zf y8HRϞ=}G ?<׉>*[ Ps+>:yiɾhy _棉L%hy 4 .3ǵ 3g>>#iYV-Z5h XQu\}O߷8ߤI ի]o:t(fʕᏝ;wFEE-\+SLMAhwѢEéXp# >txy<  9P2 UVTǴ6mnݺQعy׮]AڵkEPǎa>F ͟?%S)1&9 XUbm{oΜ9>|8 v+WDhմiS +j+\k@K:~zfmۖ#W;TtM6h+(P &U:tp-8I9'-Z8Vn ߟNā 6L]]v߫W/WϜ9 ۷!N %,O o$zxK@o;rHqK@upy䖕kb-]4xmۙf\}uڴi urwY~ݗUsjTn` 8waǟ^hvwO X;@?%εIC!2GF$VAF\\=aBU ԯwu˽{V$PJuOl^>7i+YzOU_Fn\ly7pgz޽;91YB3ŋ TѣGnڙqn z2 zG͂[Lo ׻EtBBB8U0׫WQnwM羭N!^xv^f  E/Y'-Z<ǝe˖nN B⬕+W{T҆ Fu$_~%=M֭[%JΝ;©鐧Y0Nk@Yp oٲ~b] pD\VZj Wq6$RcZ3Bo~oȝK҉\rnݺ *Pד'];@pLF,=d̝;lOt;^n @IZgXjժE1lʻ: 1Lr`16 #FpIb '@VC&wFw]ދ-~hSmK'*gS%fm?&\ڣGk>yӓYmk]'Kn&gܿ*U렒%[3eZ$;b 73Jr<^z-hy _4|xŒ9\%L0w]}znժI]M}-WR%-vAO.~;+6FQƲeJ*pP~h"N~Jr)۶m65Pp|lCP)p(?̙3]͂kCcv [h oU f]t)٠xK51N6LXMI;u#yeɝ;7]bVׯΩA|+S Q+d_kiʹtuz&v5]q0M[BVJN(VD2I$tq藟Y([79 ˗Ki!A 8b)+CEwoB@WIS|E3fd*<].PqX Uw]ދߣ2 B;CX>&=};`ֹ͒4E5lait«P%ȪAnݾZ(gΜN:@ s,bꃗrԦFk"|PeULvvt* ;rX3%5s']2F{xd?UqHw2>?4i#w5]6f%F]8x8yO,+vhд Cjj- ۷;EN0J H`9%mC4zmD>>dЏM}&ضWT.Rw\-]KM34*)yxW].,$x[ 7p/[oC||>8o^_?'DMp?I5*7!y7ntObE?9ܷk.i'?yœiԡCfhT)ՊʂtF+f[ RSLj+W̓&;N'Sߛ:E\$ fasͤjp5mڔT 1_+ ,Y>εS2vpLٹsg\-Zpp/r"Ilpp00MTÇ r\UF.k.W<.~J2eJX.41_Ps<{BB-Kuz/7`w H:9:_ 1gN-'_d~/ z!,#IFir?Hzo$=$qxq/&˼mZ{. O͛/6[6׭8/A5Y2ÍVآ\[mjMU|EGƫW-N (WMF:߭n2ɉ7oB6rfs|ޱcpT78$Jv?#7Y}( B'~YFͤ^|yŹ偃׮T &{ 灪jP #qp`t>&a8P?t[FP[(hn_zW&I3IG """3nP+Ʃ 20mX LLjkwsq.:u%J:SM hF5y*tz^ŭZjR C!qsY[)4Ԑ1c?}*鯕+`aro_|qvt^\I2d0}ϟ ޷o߿ڑc0ɓ';ХK.]Gy}NyW -u%˲_.]tz7K.]tҥK@7p=leT6Wt* 88.***::t҇~~o(`4N<%rb>>UPݻw&ӧߺuH jI&_%???X,N](Q"$^S] o苲e޽[oUU}=tҥ ܽ| ط,c ]yJ2e~m۶bŊCΛ7Oln۴ig|s 4ر#mx/i9}K\[(W\M6BBBz}Y ԿQFM6mgϞoR :w|Ν7^~aÆݻww 3g7Nlqnӧ\'Ro߾&M}A" zggtҥKןj3Ӈԙԙl;ɉ KvtvO-ZQw[NR_*)? FC䤩`~7cƕSQ +vj7.*sI4AVZ7/{ 6=~߅ ~)V/22ryvsՋڵk[uĉ*UH"yU8ZjǏf(3((dɒTPaKJU޽{aY>}T#u޽L2%9sfƍŋ-[=r\p3D+SbEn5˗E3݄p619pѣG)C^SJE:;wyʲo>,:uw܉hif+S a!¤=>>>+W+0]7.jbW{wk%EϮfغxu/Zs1?ʖ1= jV]8lY?Ͼ}#w]vTOCP8rHSw$I;R W\᪪Br}&ޅP˗/O_t իWgȐ fMH 6 yw!۾}|z(IHƸqY>ZreHYrHӧ:tZ4l0|h޼X=z4%_Ӻwrh\8IY(ޣGrr$}رnZ/s [|9ĉ'$5k$ $rhѢK,aCAtҥ{wCN/جK 9?;&yZ{77Ųad굳n]!Y,OXͳ؀.MU{J[U۞ډ[}Fhj1fH{ޱ JTjr;Vul#q:O?u{ )R,[ `S r_uN*pO?PZ޼ywz>ٓsŷp|j࿴iy'Oիl f͚ѣ0N:]| `L.ݑ#G|}}&Mp.9 f4N4i¾ 4ܡLg3< 6qʕ+sY7AC ٸq#z9 &=]v.]`bI\۴i/X@f4aРAxR۷B ӧwVk.~88qb@8I"QF boW[l ;>L@:}ԬY3BGwQ~).yͤK.]Z7pW;բ>,gT;vWU?kX[Pv vh1םS6Pd"-chO\3SզA4}uB%Ab%g a}H^>>>ʕU4|z̟?KV^4iґ#GBrM65[lO:q9m48{ʄ= r CԩAX*󡚽{r9s_-63eDΓ'?۷o~5ԩSE+֭ 8ĎQ}0k^z$'}z{{$7nܨX"i>:F6{l4oޜh?\ޅIoK@$I8;u0aw$1 ۹vp; p_n]ׯwwЧ7޽{9rÙ3gH܉mv%K(3ѥK.]>S^SAZsJڬFh{\SrTڅңn|Z~$/o)QWzx쉩%T0Ooa?~ Nk;j…XD%K!Ǐ׬Y2sΉ]_~pPMy.\m۶*Up+ N;wH  Xٳg}Wps2Wd,XpiZf n1nĈ" 7A^"-Z֭ڹsg}ԨQԻtRC4b^reŊ# ԳK.4sرKAAAM4z>WyZ[\iQ9.t;(bRbn&RDĖ ؿX~ SJE*Bg9!ejҥK?YtUt-C kf;Vl(!hQw_\s?H4ѽ Ma^زY(Zwooo(WOɘ< [TmwSAիWy! S],/XO<9|?uTނ١Cڴi8i`5jn'NL$ r$o޼Ջ?~ttY܊~ҥ죮,Y@W\a)0x`>ϦM"|Μ9˗/sM& (ZX#G9?~pĉ3fd Pn< TPSXÉ֭[=zԠAH,ZhΝX#H ŋ'L/^={:-04Ѽy8 삿aaa64ݻ-[g)L/-Aq$fO>u:I0f:T8kbf o׮Y *4d̙۶m;|pnT! Adfʔ 9NN:yV.]t}0Ad0I׎,$Msx\ҥLbwze+P ~Ez7,/{LbW8INVbGv}*g:nNO_:"Fț.]t_ tҥK.]tzK.]tҥK@eEIj@~$nOҥK.]t:}Pp}F..lݲMxdzJJL;wLRTC뾎DZ?N[)(kP G791ndgU{.]VĉJk׮;QO9T:ug~%@{„ ŏNrMPUm4lpݺuVڵ_~`H: Ϙ1dG[rx%۴[ժM\EIlo^Gy) u\3B&ot-%]6-utn34na`rζ_d2=zܸq'OmFX=}ܹs}r`:udΜ9""ѣGSL`̚5Kk+T7]1c˨:]tիW:dɒI↯Ν;af͚a_4UUtĉ-ZuՅy^PI&'ZnxJKCpyYp!#HΝ+>ig>lٲ0f‡۷o9ye7$H޸qg˖sy,X/^,ZѥKG pGtik.]t]NZ-XP}O}Ƭ_>ԭ[PϜ9;Du$[]~Ϟ=N>쳊+R w[nTӧ[ƠanNR͇36@pFiw@+GTAo+ uҥP ܍G)b9uИSۑ=֍ e_?ѫ\:Qט=Џ[Nf۱Qr~*!I~͕T^'eME+ 2]j;YV 7i3$Kí*X,<>p6<{^GY,mXZ`ۑlV5 kf{>*c-wcqI |{2`pܫ:I珽swG  ݺu۾}hgϞݷo_ttۣ29RCl;wOQoׯ.\x޼y 4?~<ܹsVZ=|p…U@fsҤI͚5Q׭U10aX3gÁmyzxi>|88H$k׮,nT^)S$g+N2d(+/^k׮Nw<=vM= GejԨQX1(<$ɟ?V$ITjʔ)t?JmVY9Mڅځmn1Tt7]9/)geUĺ~u)Y*SבUljLyn]2IPM,l߽^TdR]V9J0C1j R5jq2+Ղ-,y|_c~汽9nvmlV,drm:# دTilLLž0;,S28/G,!Lv;v}СP2_ҦM3gΒ%Kn޼^+W oذaĉׯ_թSGp̙3a5OCbԩS:txoݺUPٳgS'۵k7qD#?~<|*%JD $$zݻwӧOϹt "^x'Oء!駟Dwo߃bs˂^oݺuϞ=A V>lذ5kڵKuZ ӱcG{PP~-PO*) s-[}m۶ϻO?~|>)RxcYW0li۶RZOX/ jc'>eʥ~RN$RC3-h.(rڬb ݼDM^jٺd;%ܝ`Zm[Wڏq^[C3,mݴX-S˺uwmm'۷s*4Y ejI҃(몟7d/B}5$o۹X~pb(USΐՕ3$ؼ'q 70|Ʋz~«럪iVVM׬YӠAxڵk@$G - !T˔)S֨QL.`\ꡡSNJ(+W.wfǓf%v\9iԄ b5k'6mjܸ;+qr0Y6) #UM4Fqx$$xXB2 qUTI>8S .7aA.@ā|ȧPBToR/,@g[ɂ>|I!i)ѮUɓI5jĝV;N˗/<͉.\Dιk:qN ,/^<3*&*vRpǩb1G»o>KfY}b :M>]9gΜ!Cȝ;7gݿBK.]޻S_ύC;yvEFE ѥK.]tJ9~/nv_.]'5i_~֥K.]ޣtpץK.]tҥo ^h%Dz7/ծҥK.]tw$)#oM!T-cw)/qhTr֮]nuQ檮 CZRLuucǎkٲe[.[n 2eer}u9%UTbiwS5J(շow8= SLS0gHzת_~ .|E+*TPdIMӺv6mp5kVAa?Ce'Nn} i9 ŊsM%M4~gΜyJG=vXe4j\PիVx{{NcǎbHn;xs׊

x*n={=A]_omܸ[Yx[&LXfD1ׯw&M2Z,Va$[loL\=yjCdғG{wwۥӲQrpCvrTY Zo87_ 5'V ȡ)Xg-T5w%w9~Jcl$I>x~SALw*'Ia\ hp,$o66訄~=gk]0Nh֌9zqnݺuڴi߿xb)v+":ĉs!2])S-Z+W jJgɒ%Yd)FRJR'N|%VdDيK:wMr+1׮]2dHϞ=)y͛8qb߾}%H ((H@-TO<9iҤ:uϟYٔ)S ܜtsdܹ-˷n݊}pSpB;RƨP```ΝM"|SF]7.8V0qȚ4i/^g2HN;F+W^g)Ba$I0J=<,{%n) ^?aUֵkWwޥ+CBB@>[zS,ڵ/v{fϞMדS;n,YdƍՍ8-HR^nݺ"*/q7|VZRbs!%K2ׯj_5 wNsoȘ\f͛lի_ըQ+%NϘ1#cĝ'i&qѥߩrPYkzZ3;S-iU|}ˌ!jWIpXɲz j]߸ Jr%~W;S$Y/1h; n0tdx&!%K> O-1Anh]͜۲nx:e5V+o;M^]uykPA3;jb(Z2LmˢJlY'2Їޱc;믿~WcƌI&M%xW@8`n@؅ ĎNТE AQX3 jʕUVt *staʕ+/]\rf O"| [0S >3a3s}`,_vŋLP/߻Łܠ޽{C"pZpNaÆQ Z>s V4"۶mΝ W^V:-nHpiP@ ‡=Lg޼y%U1lnq;/9jVV?\&ܾ};Dד0`)n(Yj `&BMBg4%,7lذM6`?,wYNal\z8EBM=ŅdI*e 6Fr;F1Y . :#U}:&ᒫQ ;̈́I>^`z ]!k]\$_g 3ϝ:ugD$fd#0}QF=f͚@<9N "IzƆN3gΜc 3:˖Hi)"7ѯwM H eT[F~۷oDC%U<Х_Q+E}bq)秶=N[?=j[>U2[Qz\;v [ێ}|I%K -ݸ)ZḚN^gMiv iKNɜe4ٟ*HݠUZzz޼y4h`Տ8=qXsΝW"8܅\iԨ5m4É[a҉G,@ FIXrȁq)Ik쐰S> &'!Ho…t OA8p/47p'g. KXZԩSs"+W"_΢d50:`k>O0Y{ 'rI ' 'zLӯꃛL vrs{T$v'!+qHZ}8vh2|@&M3f &(t b9]z+jU%IP._Kյź|x.czn\chKInZ%@l&In7nc{FXcY4Aֱߤ4Y O+il5f pg۲߽ij_ ]"sذw1!$Y&,$N)ӳ!n2 ]vyyy+֭[ڵϸF\2eb:9sfXX3%AGO~ժU7ntI1\@,KY@Xb;vҥKC5j޽; 4bĈzIϺ5(sw{}'Np>!0Iv\2üNVWpwp%R !/ &c v'MJeާO7MF9g߿?-9"˂D!^CnUxq(5nߤ4i)+}~}{bCcv?s7j4Ųqݶ{9!d'%LYYV4U1Ud@]2 f0~\R6Iו(rI6ABռ1bۿOda/jn}dRW8}:P2QBSZ,W?=ukg>fzt@G*U L}6W/"aeX ͛7Xg˗/ ϟ?W\\L̲Xȑ#63^z5UTW\I:5֘e NyLɜ93|@'Q{UU~j)tʟ)HD 8 Cy=K [W Rc` 8?A .A!"DL#Dy:8Y+S@ Xa`)Sj֬IsR`J*WDCpr!p&3ӂ[D bGhcŊqu ژ1cWGI%;wF =x o( <LݵkT,<7oyW"^3n>( 2],7mǣEaaK?# <>E3)09FdɈ2g+BW%&Oi*UJ'>n?vyqR_dHp̂k$S, p6;I4 'O*"F;/J,ɝjC)38B30PvlP8]Ԕ8 I:D3ou ڀn`vDH/Vu3ϟ?j'05 poӦ9^:u+V|tҥ+iJ]N  `T/FTʔ)]̙3n?qJ>tҥU.pץK.]tҥo$_?)K $w,%Y-*K׿RA<؍Nܹe/ ͛nǓ'O~p??I^^^I&7x"""ޯWtҥKkP-yp?V ^?~4Jޡ ]y2L:tW\k׮.\s7(K>sXV/_|%J:t[=z駟_%ҩS: Γ'ѣ۲e:#K.]tpn󥒳yx)^|y!U fB궓C:W$QJ"=~-2\zu5TZ2f8g___~Y{ѷn݊bFݻwƍĉ?~ٳK0qQ߼yfo&"E-Z߿ѣg޼yӧcܹsD%OСCT$H&l&Mj0dY&%xXnW\)v?tҹZ =gy}6fi8 r"ctl!7 ɒ%09"Eu)S͛$K.J40B4dBԺukI K.]tyÂ{A##glMI~uh86H,Ww`K#ߥtղeK`qڴiw>|8lmXkIϞ=!TtٲepX]`omp69?|:zf{ԨQR3sLҸqcZ𽫅%Knw5k$I$,,,^JQEĉ5kF$9;vkf͚%Q@|pqƝԥK.]T7p7Y-+ʋ7ǥlc7Zk [dt匡Jc-%u YƖ()q/5lgfRd`iӶlmT'OBɡ_V-@3SLgOf˖-;`]K]]UU;u =WgϞ _~YxݹKN r傿4H?2dЫW/˲ 7 pSw޽,YRP,Y8ێ5imKQҏw.^8k֬ަM*FѢE[htҥ{T&A$wMslJO?3Vj,ӳw.C.IR-+C1H"_|jqӗ,+۶s)~b_@`e=,,,((hܸq+ }VŋEņ gk]/^<҆ɓ't;nknڵ5"7w-f7ϜRZ5{ƌ%Kݺuܱ`XHEƎ ejڴx; &pQ'S[dJ{;VSLwg$؍j֬I6oݫK.]t>2uT^>dѲeS25'yX_ۣveL/-\_A׿N˙3#G .|i;𨊯߻-=@H@^4T"E]@@THAtJ{ $@m{7et~ޝ{3ٽ; իW ehrڵV4hPLL o^zl27,h[ps L\FC{ٳ'ĭ)48{ltt4KUV]|y}rNwuƍf͚>|iO>s?? tܙp<`N8;w8oݺE\Dzeʔ7\-Pu/R (ؚwݵ=zɒ%;v|+m ѸӯJ$I3gΨB ܹsxEȪU5l:1̒CxϹ{)))))4sRnOHO;oNJJRǎ)W80RRRRRRe=Mjpv;qLҥ l7nܐKIIIII)5KIIIIIIIIII$KIIIIIIIII.%%%%%%%%%菁{ʕۗ&%%%%%%%%%%Tcȓ="""&&F&%%%%%%%%%%((((o޼;wO &RJݓKUU__߰_~%>>Ow%yxRRRRRRRRRRu}o߾m6 ܥYIpz$]JJJJJJJJJw)))))))))@ܥ^Ipzf[>^!!!:.22y@QQQW\ICǤ^h17i۰8w{=}FCG2gi蘔 U\9::رcs玏4_p!66pO.]bN:eٔdN 5kVNӧ3eq]yf`|W\9ydZ ùsF#VZJM5̙3'dQۿ0JLF=`63glZfX]v4_JJJJJJJJJS7`z0&&n-6m [ ݻ711p?~L2EDDlݺ^cҧOCˌ3/^|ڵ隦Q L& ;Np$8DJJJJJJJJJy}';^l .ay<))ݻnŭ2CqXXX\on۫Uvt F *]4K:u) uzм(005ERRRRRRRRRR@_???@÷oL2ϟyxpSζm۠rڵ+444K,7n̓'Opp0/)ɷd͚_~6(QtNKј>}z<ܥ7m$>=2WRe݉OcF;wwPܹ#̙3_|<8uٳgat ڝ4Z8qDdddR2d_|9!!AX;4gȂ۷l6+O;W, <އ&HIIIIIIIII/pzIpz$]JJJJJJJJJru" &<boYOU46~G+>ɓ.]zHg}nxd~͞MJ I)qn::3{lufd2>3={: uѣÇ?s3f6(UfaҤIcǎbghۣG>M{"VڰaÞ^{Dw^lٻIB@&RRRRRRR ՜ S2dn\-BqENK5WJJJJJJoPhy5%EÛݴ[-LS[6 dQt!Y#*=jOLgϛ4Z} ՛hGj>ʹ{@'PX͒2=%l~3}#GK.o;vpƍ,X.SLѢEnƌK%%!cʀiӦQBrkn `+Tĉ)Nـ0>tjժumݺuNT9, Q.]@GJ#F 'n۶m֭)*gɒRe,%%%%%%*H9}Zp`P$ Z˲KE 5`Y:Ͷa[m&M}vцEێJPf@KLTDY Z͍;؎Uv}ֵ뵲6i{BvpQNJh6S3Wgͩ3lݏu4po=3npC wPDa40Ӈ2jȑ:{,[oժU+O<&7{!>}X,2cƌSNeVZuѢE}}~A|mݺ_~YvmjWpvCBB޺+WlҤ uhcǎ LϟՃ v횦iT.]C pk@ի{v튌ǝ `zQЩۭ2pkCej17[Luk`߻cr{i^{rSm1_P@@̙3fȐaذaL68xܸqv3Ǚ+W޹sŋwZ~W_}u…. ڎ;vŊ˗^^^fYzٲe_{50ǃ޷o<>_:۷t:qc,߬_O+Kj4Xyjm _ێђ#&MH9}XJcnӇN)?0X`~~\Kue򗰜SW;c{mOjP">}wpy&O :u*_|k֬ČHVDD+9rk׮qFn2eʴkga} %O>'NիcyŌ3( xL<{֭[_|>8qH"Ǐw[N٠J*:uھ}ih@r6Ȕ)cG|ozjb ͓'ɓ';t@*-Z:hj TR4u:]Ϟ=y:|cǎv%+Wı}ߺuZjs΅O?ի_5RRRRRRRiqK)q[No+%\d.{> ضq@ ]x>뎟`hݦ+mJ}5lym[T̉zg(?) qC_U5 Hk򃚳HY% uZн@3'*5jX]Z`FcƖCWTV_`Կ7~1m)ЗlݹVzxe_oGY6 joRϟ={vPB``]vV9r$**hѢ䁗fn2?`OOO&!6 RJ6l(Ydtt4+bNs 8R|yl6p׮]}M*W R!g̘I+W!U[ )`5՜9snݺmoi@AIvƺ%KB ILL駟u%L"Kӧ9- ]PKp ^ըQ#!!a:9?^7`2 ݻ˒SK7nNohEM#qw-ͥ~GDy@w}˖-0?S¦]v+Wܼy?틔ߧ9WB*DZŇ^SU}ї/I|EF>>>u;{]f?SVZjOv)))))).%%%%%%%%%^Ѥ}swq۔{Ohhb06<]>Wt ߥt҉mB=<<)wS``ݻw& &Mh L7'0mݻw~}aJV\\֣ԿCk6E{Yto0 [7,?R{umYß5KN5SGuzzL޺ e` ݇aNXyJ(1uE+hWk/KiE JO8۶m[Xׯ51zaæLGtE=wܯ$VwZh֬'*7<<O#?ݻw?MFV#F] 2dѢENzw_aРAӧOɐ&e˖-RȌ3ҤG))))UUWLJR~ juKVILO9x*.s%M# Dq^&j0*Fa{50ST_MS:8`{]OuvuTw(IIALN{$t#km(U%iJBcw\27v4XʥJt>e޾yUMU=I툋\qбYrke˝4È/JX(W8٣doW㕔X<:݋q@6#(,ƃO\5f ~.$ӳo߾HΝ;fY,ya___nøʿ111k۷o%f9 69K+] 0;ҥK~~~ )"G@^``ᄏf͚'OƖ)Sf͚tG^t?dA-L`dC|r/KN+>>.\_3f yHJ"4łF8ŵ?f.6m|'vL2 :3b8p E縊.X肴g}`W 䒌u:RtAH;pI5Cc? $4`ҀȌp֭[b_%1OF*,, g3a9I&KlUTw(vN(chQM޲~3~._1CjHV{EWq zi7#}i5{[%CE !fZ˼q&{79>ƳRUE_s*uZ˼ }m^,`xj3*wwc 9ZLa{]x>8]rC}el?Wo/[W(QW Ҷ~mFGwa7?0Qܻ=H|1aBmg]>C7tPW͜M[-}Q)wCA k玒5cͮJlJBuj6Ian̟]r 5)h7~JWb>X_}ՆJt*82l]:MK{bOT\mܸQؠA*U@B;wwH}<͙3'fϞ}W_}aÆW^ݱcG,:8%r-v%<<<11믿>x =֪U $ {AT`˗lٲ7ӧOO2훁ٳwڕsAvix 'K*մiXС ~)iySaf˖W^`ɜ9s"o&z-lBW\! K,qt9wx;o<pdF?6((Eɒ%A/XO^7T7oFߦONڵkOl#Gt[D^Vq C*.m۶PB8|۷@{ "&ܻ~z-L-8d1:[f,p!͚5cD0Œڲe[) ~^v-O<ǎVZ-Z7^c0E~s{ $y K2uMƸ3pO8QERRRRRJ:y!1Ѧfݔ]=F,[VW2kG+ח%&(c{:lJTNZ]o޺nOJT"//Qv]x~583*oewZ=c7- 'jP˚E7Y]0ϳhO) SˌjzY-K0i a~DE|jWnGiWvWolv@pBUȣoɒṡ(>{ֽ;7pTyLO꿭]9dѸy ]M&՜ݸ/nzzuvcyWd/lz4ytYs 3OЪ'ef{pC7,hf; ]CdiϞ=/wAŋuɚ5̙3g͚K H-Y%J8t|ݻוha2zꫯ`ʊ+3xܷo_Fę=z@3f,`zȑ>h…)ox0 `=zTIXC~f@bKWyʕ)ftvNVPh^gAyЀHV)r@@<2d6Ǐi&ȕԩSy楪zR6cAE| \Z*4ZBx*. 6UNv޽e˖n1f Sp}ױ g]tt饋Rƈ\z]v U%|TnKb u떒Rjرb_gd 'GѤIdbe˖D'$jƋzΕfnZ_SUfdNSWOo;QXֵXWTw˼Jb9s{exC2lrܼXulWpGmwES1nnuDv ݆NA f4:sǐwqf.{luGB pO\ys=tvmzW΃\m[ [`n tgpbO2w qf)Nբ*=̵YlK{}JU@|=jzwqƦ],UŸ5Ptz}Ẉ54w2]|ܹs]gȐǝ:u&ͬXt)><Ν;7Ɍu:v t;wNFe8p' J#q,SL}G ?ʂ[>3gy 3c̮S |+QDϞ/pGα ETwy`bM܌.@5 xbjWW')|@$j̙#l 1I&oUn߾]U,%%%%QGkwC2ڢ:1R8~:ǀiZ=㗺;kjv>$kڞ='dY *$}OpBW靏l6 tڝj@|E XoF2e^8i8x7]Ut^֙ôtLot_=f72}Wۣ#/&"Ci3z} /_M2c>~kI;te3Kbu-)ۏ=δ;z2g|9VחܻYвs)T%83|rЍ&|Т#~`5kZ>K:|.ZjUX14 O\[fOcƌYt)L #u=#ڥK>}v7ϑό3:;wܺu+P+&LpyNA"cĉƍ}@^X0<<*H ]m(\+!֮] o;^Zؽ{M"̽{ 44#ժU.&pBB%(o2eDt7n<{,Y˖-7mDw<H":uT).HSByabOOOF&jڵkgΜ[d޼yAXsʅ;v(Zby??\@ȑ#CIc,xJ]G';N5 0qRyIEqap4i%K۷oƍsر $]v߿anλ@|TJ>nݺ;R>*RRRRR/nU]j=pS1yڏStz]jXv OLY`oLYԬ9'_8j3GElyWkh~IUT^*j7;|H#:Ru$pzDig50#' F'wX̙y h7 !~4<}tDDĕ+W̙3ܺu nhoϞ={QƐ˗^KŊ\!! c߼y t bAU-QGxE:D\9CHQ8-qpG-.ŒAv@*v8@ZH&Q۷< ~"HFHɓ'9BL@/EݻwaJF Guhr9󔱠p:ID|(. 0CLL dL&)pĝH)Ä?|7'vI1\b*d=&'A\M  JOIɉ9;x7xc!J '~_Hg*ŋ{{{_xԿUDQQQ~(SLX=""" ::qRJJJJ9ѿܥ+=KN3,8jQn\I3Rt:900h4FEEܹs_p̓'Μ9& p͖&-`뉉ҎM\z\zƍGW8op:7$~f=aJIIIe玱Z6 c;_9URڵ[li6ݻ׳gOjf|r̹zj믿~ kݺuDDѣS*sŋwvDQ}\K[=C<t㟡SPBe~_߹3rJ>gΜ"3V\dRJѢE'N&=Q-[^zbWݺu;vſƩ Ƕlٲcpov޽g v-h))))T*ȋN; K7)}ͱ&I`&ǎIszz+>~4s7d S5ve0Sǝ@W+by1 Ĝ8ӛ>M%tUD!֐q(&!IbqBܹ޽{ii_"iym&Q<Zh 8oN;N<0'INʢ&sΉ.N ?~:f&MΝ;E)!j r qhwؗՇ?ߠ4w]Ҧõ;ъepiv/V"?}_͞r+F } ͻ۷mqn7XFtsit)P<1kv?uԔ /ۓ>~O-j=)Q,\i:ıE7NOT =zرcm~`˗^SNP]&Mdé@}Ŋ}&w}aWh'|RǏ/6޷o߷~`\]lٲRJ DҧOZB'0"p@[o%߾}O\7nL% ,|GFN ݻ7ISkȲ[ntdvbS@*Ub%8k„ <-_<~vNjݺ5=Jw&]t%?6RJO0njXPXM6 [۶mqfBK__LƑ<}m۶\TJ8'\"9cJ"evڕJȫL ^iӦAN' |& 6mZzu :g޼ydBO j,Ӟڵ(QF#F`L2`L LGԵӬDbFa)5kVreS pg>0!)@8İ~" AM)j  ,|2d2^ hHw EA)yfp!S>0"a]0? ԱcG>Go> QF9}Xx3)%%%+аZc7#KjWn~n+=hf``GGwnj9PިfS-TV6a? h:6j=B3 rGhW:>}7z(QWOǧB_!j>6nܸl2.'0?""bܹ|Vv`^{  8pfjҥKCpȑ#C1Tvw޽(3ҿ曢 8÷Wܸ@EX u:~mK`A|atĉcP5Ck%6r.>4on~Dt3cƌs΁q.CubpI({)cr={p S,X@YV,b-Z(P@t <cȨو74iBŲpBƅ ԩSs{E\ѣG0)@s`}їŜ)^8+rB&DCC!!!YfݺuI.+&âE%J0F"&@Sc2 LN?1YǏ'8;2jHwXNM,cM'm01PQv2IP Ə?(@AnTÜ?>¤_2LA(6e ְa>sN$c,YR@삂!%AAԟ惔s4w5KN;+sy҇ʵK[eS1XmZ ײۢ ɪhVbkvXW<~ݿkKLg ge'l{ӿnJjxr+F=}c۾jXvbI\mou\\/n sɕ pzH3֭[ >)c Jf fj*K ;R9o¦ sK5ERPwΚ5r"dڴi@$;Wyx⮥)ܒXן=8]0:)1!' d`[FJdU1&ك@!" .XX;oYMcf #ړ% =!-oCB/E5&4 t-OBժU5M?&sGf;عsgJpg `lhOn>HIII=J;pʬW2zպdqbpo]Qݍk޾3#|,Gj>n+cIz3[04r32e'Zb{qQWH-2L&sj v 0a TpJ*߿NS_9Q00[&ϩ;$)S={R0#G餒|r $.`;,;{8IN>'#q"@ɓ<֭ۖdЍh.p̙3̫C)o9Ss׮] Ք= fygQ1n|YqٳŇn}9=U%Nݻ_=O<"z4A6-K{@@SX]pa͙oeʔa p… &OèQR腾SKJJJW^$&We09{ъ,]<Ȯ7vy`!)ՌF@zՠRnSڃUUTOMiI lo//35j)r5 /ѣTR@*,ҩS{߀ .@k׮ux.c`.R }ѢEcƌi Ҟ7]E9w(ɷ]v@ ,hռysJ~{! FXXjՆ  tk@BpoС$ Wɒk`ŀt?~ <ˈR2Ib!oG y 96uΝ;YJH'K,ݻww;=$$ }ϏD.^'֭5k4k֬bŊd&t NBmT8qWZ#'OAI2O?pj"@"u᤺E&M_|&ov;w kժEVXA1~TF(z5M @Z(f!2eٳg1Ȅq~"$.8Fvڄ ,f71Xv1dlr:޼y3,,bֹ;"EqvOSfS&%$$0|`a3LJIII=JUe|LYT/_~XE yI }v ! 2gǂ݊vz*+>>\M&ÿ\J pJ\wźZưL\A  O9sA-JXù] ؝bvB 8#@$۾}]rJJ(\ݢ .=fAU,04bN F 739ea(xR4g8kTXh5iA8)~.bwӄ a2c&S5̔1y_(U,7ܺPgXIlA{+WΥ 1rz))VΩotv=bY6~`ۣKII@QO8̙3Gmۖ^*$g[J*0UGI.b^oX*VcIaǴ޽{sK+))))E +Fz@Ks|X.%%KӅz{{Ĥ_yK.((H|!:ϒz61./Ȑ!'>RRRRϳpz2pt%hi)CCMyĞР>q}ϩAz>$$`0ߺu+{잞r{sfX?[OCCCIyطo_.P@9!CLӧyϟϯ_3 /^9ٳ<!a%\\ؕ,YZj8ɫ:0v䖗$X7o:uJI^n\r@BU)paW0&\⏤FgӦMur._L Oe/_ڵK,#K*H)Y)7XϜ9ô$.̙334̫ǏOAX`/_؀II.ƍvu '2ܸx"N<ηo3Lb( d0SbZ&cGt0dH8$kg4yWҀ.b(P7)))_i{:ֽ4X9 udiSNkCWwn'*mmX8vs}ҖOB-Wz]@w~ugC8=źexWC޺K{GW鵷)I5ZU4U)^>/_Cڝj%vWoln-}s84h.PKybc]L}.8ȶamۏn_ >SNw)eܹC`@jɒ%bw1dȐmVޣGm͚56kԨd͚U#-[ ~֜ugϞ .ՏZ,|5_Ν+U!.>#g*V@?&Ltq^j֬ XoR" . HK.ڵڵk,ˆ \o k޽ۭGRTN޼ |w{;AEU/O?Z\^e\@U" p8:u1dtrQ>@?í[^ zXyiӦ%'B"sRJ ՋeBv$-8IhbN m6ی3kTcǎU?`fvNk)$X@f |h0e?j1"4>cƌ{ԒN1ʌ&, Q&('3W0T.;R2h#;}{BIIII=J3p7Py|v-jܭfhZLy ]8&>PbA[s7xw_ٶ:Aw]Y#b즋(-8[9ԏMy-5驆e-3[Pޱo.4[zAȋѤX|T-Xʣhʹ__{ۺk3c,ڕsn TtOQc\[M>z;0kmʼvgu ,YYf9lذ_ CE+{=jܸq+Vhٲ%H[o-\_7Tw\2WeJc;p択-[6$%\=((lĥƛo)  s{P +| G JXDd2=QG?Sһh"vwƍJ]:-ո ~嗵jCSqܚrY`1b_M5^]J΃.@oݵ~GS,_{+i`[5,ܭQ_bd?d~enHTfk&ulo`ٳg.q; #G5jԸy&e[TtwS}bp&RNp?~ݻw;wUV]p=z'bp6w,@ϰxzu$06Y&PBu5kV=uwp`w}tye5v)ݹs*We8Ok.TCi'N`K.M͖y:a„yVaO DsjJpꫯy\w3#ň*d" ` tdcjժ%d4n)8 D (tNq< wI$]@BմeS3hLk׮e +V}df.]P-0a4a ] I\PWW&`QϷn9XܝoJ ȻB惔s|ZRj8h]< z}piM|bg_1g=t5o]FC=MW(wUd1nSqDLXH9VG{֜Ɓ3,_߯dϫܺ]`>\3=!ܧC B+Wrw?& +7`}fS\"E@xp 8}4 _f #s5XvPBq8888`9p k׆4M#%ADpgR='9EٝΜ66mڅ Gm@5*W\߾}]}\|K>̙3 3ɓ'Iy`ަz2mDBU 4oڴ - 6$W(SÝ!9GBžE+Vpw<|2L\vMt'RA8~z<ZqZ8IOUڋ)IÙQʥG4G1C Pf_3ţSmf悅-؍kA^&1/X\rN;,+ܽnuٟ;u}ZZ^'<7I޼!/sh c8rouIܵ)I9C}PF&SwГfOԪuP322 dpwHB/plҤZ~)-Z愿ݻwu[?# ֭[=66@K[՟b>}2A/TVA'@asѩ8&pq4oW޾}{)+W@nH^٨zc`K(- cRx sd/9::( r# I5K.A\]]!n;Ƃ#6J.-(PBϏCY\2VqΠkyx|t;;;KUi@;< 8|Ν;R8pFrז-[Μ9c$pS'''/ = pPmϞ=9342ZG HIIAUÄC/җ˗GK  u8Ea({p;"תUcǎVx3f ' )IoAZ]&F#E0q&ZXX0r&7CEp 3:Rr( ]j L5Hsf`;.@543ʁĝ Ϝ\]Ra˜3Ruݶ-e 777LN9QHI09sH9༅Ytg=b(o aaaH%  B́ _ss*WrhN}THiyNҔVX$;`dT3ڳA.wr "'pÇAV^,׼yŋ,~3R7X(S 7>d;v!&bK*UjժĞ={m۶PI׸_vSG;A_7cM#I<$$P}_ H   ĝ   H   ĽZj7nxx=   >)u9s挴޽{b  ,T*o߾z{ŝ2eHw   k755MKKv+W%H,%  !bV6?T  !q'  /w   q'  /w   q'  /wl"##ŋ)))KT*J   >rS!뮮/^ju}ADD{*˗s+  eq.\Ћ{BBB _zu9FYH㢢U^F&V2AA5^lٴ4<677 yfEQvZƍŋr˗...ΐxJVs+  x,?~X=..ݻ-!hvUV[zE޾};666_|%J8vXʕmll?Cw\*$  /Q5MDDDXX~ FG.9q /^2 nkk{)#K.8\MAA_,B===>|z{ŽjժoarI .[J*dgggooiؿ;w^| %  /k֬yY5!^|9++뽡my(^[[[xGХ5㥄ݕJ%  +J*ׯ_T<7ŝ   \rH  Ⳇĝ   >䌩9h~_vܒ&=ոҔ+WoR?c(JB!*agg,l l#  >^ܽ;լK.ePִ WjJnjΕ,oo s5(R>.LΝΝk(իW/] <MddW=z;@NNN׮]Ǖ-[6$$5*W\ZfΜ)}lAA',˘[1 &+IR`,ފnc'w kFfz?6}9JYzJ1nb"rj2#fjbƘY2,&|~5j/k-;)G-|\XX>9Y˞o7r@H0.+)^u~ťLf a9W n*TY8u s@d(3]ԂxkR ,--a bȑQQQL-u`>>>f͂ɓGL2O={fa#F|23SwLMM_edrjgg. ##VSb6==ݨQ|LW(JV߅+V0.$) [*11ʂ  'ĝPG޺7TA[ AqԗzU`j.>|WNHKLUkCe>U\;ݲ*2J,b~E󮬹!JV!>~lFfY k}Fw|Ԥ~LTQ:qz#3ֶsP*T'1vY)/TCdM~7_ıN4'v w~#M'J dEsVT,GH"8q dG`=zP.Qrm۞?7MMM2dH޼yǍg^ڵk͚5kڴiJJ Сz\reǎF {xxZ?qDXرca.va֭;whذ!822rvHD၍f.R/ڵk7DΝ;}qqqG>(B.bccʖ-[fٳgϟwޘ0QdΟ?Ǖ  \wOQx%iVkOw޶u-U-h_Tejvw~Vqk7W-%C5gifcV$}ij ylZj@L#Ck(_>1-u+ghY j\EL,.}?o]gXVV2UǙޡZ6ILIRڇ՜/WS5^k&v}HӺuk' 0`@bb⯿ +Ynذ>h +++)t3üM {aeʔ%Kl߾ Y> SN5Ə/Eѣw}֭lxʔ).C;v8x`$j*%$$dcYHÛ3330.^Z?gۨR &Mb/GU#u1a„ÇT6l#t}"L1R2  8rMzq r˱l| ,T57VXmjT]WoĽL?*'$ (Z;NJDZ ׬.E2ݕhמlt~o _zvFnߢR,LĽܷp焬 ] sİ9مbjmTvT~J=W=JL?r3dz#q\U=Q0H2Ϛ+V'%%5jԨsƍ;pN(Q411CXs-[hɓ'?ھ}ϟ9sСCLq#C|!.CܑHkFUУGtM:uoܸ1ydT#Xtt#i0LI?~+\~Ņ{@@MnݺյkW[w))]~=r  \wcGY#pVꋇ:p|󮪥2nLWNZ}׸n&jO)R)rz"NȰ\!oyꍳojP-ѝ|&%I=gdY=kdw}G*GSF,GF] `-mk+0{گ27*`sQSן{IwOegȱqwwYBAVچ / O͗/߮]Fw[qB%n 5oРt J*}322j֬ ^0j`h|Ѡ <0`Z^laH׸ch\rԨQϏ3gƍB3 {aaaQFaOh0L($?rÇ1!͚53f rhӦ v7oҥK?22Pի;t萚|% #]6ptV|t#cj(jYB^Ml 8B⮁4𗏘Yۉ #Ǩsekpi\˅sSE)l~wmBfB&,$GZrMe"AmxqsqO6|.oFcS.cYLZhMUu5wBE~RCcە30rwZ} /skg5 ޼KaÆ˗c(,ӭ[c{hhܹsqBQ\aÆWMMM*00dɒԽ{B#""lll/޿ E:tŋ!W^ g0… ߹s ׯ]5׫W_S.661ܨQ#H6>|]@ߍ֖֣pppUN2 ŝ8}%J7n9=ugРAwIK >9v%Abbs(AIɽUelR،t10]c]XBl8ge+> 3Ӹ2TFa lMUx9Iem¥E]g^&v\R(ܸh5-Xa9ѝ_RbU ^K/OF.- s`JV9s`y64uײ%LTOqqU(B[aG|^'!8/_}tkf)\?d*%R`ApO>e?8޼ytNNN0iJbkk[R/^h4tL'O@m+F4Z Wvssjx*-\ o޼qqq?~k# ~,1l}FɃ [ Sa"Fj&vLvJŌWJJ L]jN[ׇ&A̙3g#L>suGV  I;rh6~B| ]^ְ͡-Aiĕ-oMdVT]#˅Fe$Yfa,--a!}ƍ MjcqƏ5d3"b#^1\ &L\/܋0JӜH\]]U*UPP_[DϞ=KIyOAǿ&AAA|8_x0/⌖t$>gx.v' ĝلw+4hƷ76T.ܫ^;S{GtQ6_vA"ΣG677W(ӦMFw˖- RV+&sh۶s"""r%  $Wŝ.Ecn6da<3qG1iX@LeuͭX$ bL8;q-F5g|fVW H]{3I ҥKh(C7|+6nXbŒ%K(P-˗/߸qc<صk׽{:uThQ4tP &@ʝ`۷o7VZ/T"Hqo޼yhhl H CQT̙3ѣG5r@>Ejj*rz  $rM:?ZVM-&'rvBc;Wj}!òA3YZuA;E5;[̗ure)(^9YxtoKVT֬Lz*]Acjr&+]ުzZ%q.$'` &+S_m kfdup}) 19cio+.uũ$ށѣK@W\ɲ˴ivnѢΝ;qÒK* ,>}t.^=rH3(QV*U ڵk[nSLyܹsWZeiiٽ{1cph{wFA'ڢ+Wjp *Ez\yVͤ&᱘9Um6򩐖*+8G{~x9.,qGyqE24G52f1A3kg4fͻ*˚EV1W>_yIf5$XWM͛wwa!!!c!CN<)t''2@i) GQ}a׸K=ݶm۠A8~WOK]$AA[䚸s) T- $py{׌]1aq- E^I\!o%kdFcz*Z<ιy{OZ%> f]Ĩ013ʨ_;ߢ4Q.8 3E}ԗ|ouabO-.ܙE}@|Ν{ihVsC܃+Xa&LDc[n>|CnXB}7mڄVVVFbDK׷Wܗ,Y" ӧg^xgXE~Ĭ_91ҥK.\4o޼f͚k֬i$ L]ne9.xfzyA\zqgQt(M!_ˇaD`>q*)1 .+ϗȨU ˪O^}GS kg=k8H&Z'ܔderfڧ~O=zMw+H|P!C>^+++RW@P"OdggRoIJ+AA|">uWU/-+|&AAň;AAA!q'  /LYVw7I};j.޽*~ۅɬmݫgBVl p"߽=GrtC\hѢa>LѼ,۸qGT!'NNNժU߿v$ wϲB%U GVy WD| 3%#[o9Pyԛ|x_ 9rdRRRz:1.JKUb&$q߽I6gqpi1x##S0_uQ6Q.z}1ò\ĸh`1 dE],mxHRyIҔ+UQ|9 LpʚvҾcu /YQtF>LG\\ڨP1<؂Ÿ"ePFk0, ޵kɓ'a "==lٲ8hSV-?yOCBB-,,qgϞ}ÇoѢE+V]r."Eȗ/_pp0"^d~)))VVV5kִA6x$''8qOwxr0뛙y9Zakk+uWV^zRlIKKC5jGVT)<<-=6id߾} H )i4K.2 ŋ,R+WST  YrMasVƄΐW^H%n$>% kD1m$9[] Pd_02Eco-. Z&3C5kSY޺66Ҭٱ\{j2`x˜[ wg-_ws(be]xd}ɒCQfdehQ]T I&k 63C9n(/D.6+)feq˺Uo$?} 7.WOl q,%b\׬gt|޼y9 >s W^m׮]||g &Lw*Tp9rZl ~ӧԝ:uŋù,Yb)NnD#–-[@ܛ7o!wަMf͚ׯܸq7PZ0#B1k>OVkוߍ{P.\;õ$/^Nw&UWm?=Z-f7KweKnjhybcǻzeMa2w6Q51^*״b]5P.g(.R^1zWnp ]s7Pf&}MF'fBt x">Lzʯңۖ2 %^YXan(_8S, [/n}SLxJ*_޽{?^VCd9 ,>}: 5F_^m(+QW9_&NSGVm۶խ[޾}[9w-ݽ@W\f͚默6mQA12eʠwU? իWff&zxx6@RJ!IH|Ϟ=GJ39fxTk֬ɍcHAĿL{|&Y3 mщ{;*a3tYڐ[v5AtyΩŽ+5,>d.]@׸3,Ycd7.iL[5 WԗT-XIQ/'J]u]u9ݰ -\j7I o .0s+lQ-qe.[ rixy:%J 4h͂;pjA7www%q1cFxx8@###qN?Q׸KwN6lĈ.P۷/00FK.X;Ν;w1yd*!!ҾW\ɛ7/4w$Jਨ >|bLQݻ]Ą] + .)_[}z*!.33^x6p/4KMMkB@9dyΝ{WWWh~ݻW\ϟiӦWbΝ;c;qDT;qơ0[5_|y;5nP$ '~jj&W hm5ȹR-~mKM_./#ۚDZLrke޿ Aؗ><Tfx9ٺHxrM̔kjY.=YG19L3ȅ +Shi/c4jY.{gYVdD4VFa!FT4/=kO&)DicFN 6%bՒqlAoyM6#Mԏ11Q`'_5>ڭ[7-v͚5=ԩӓ'O1֬YN:jLLn:{{֭['''cE\Jh=O{ EUVX|yDpÕ W^!>>>0l({ Uhbcc׷iӦxl"гjժ=9^#/\0BCCQN=f؅aTL,--1 6 >=~.Bd(Y\ %WׇL&[fj57<#F<8W/޷~R]!f8YA0l6[]h,W$y5P +n| =J.?28oj5+\8W/\?ٲP5~cf=oЎ.ǹx"r N kiyU*-8( (G/>q\ k6 zVNZ#>ڷd&m&9_Rqm[(PԩShhC)}vxtcǎS{={lek֬k׮ׯzUR62eN .jBŋ5t.]Azz ̙3ݻ}ٳK.pqa.]W6qG(?&Mt޽^z >=a„ t˦`:txӧO>|htt\֭۷13EE>&GR;v Kc$&^Ehjժ;w$ !]g^+Z(MLݝSfMǤ$1\x{,ZXe]–-[ŋ3gܵk _zCqFFFƻ/ԩ( /;|}۶mL.0 d$(?P\~&_ͥHAĿI;߬vA͘[*SlK 1"DUZ-BVa;/󴗏ݻ\ք*GYnߞ٘paku1kUSJUDl'ܷOT5s5Y^WUOl~7Ea=t]79kR9u*W9ikq_5@sfyHJ0`zdd~#޿x\xqٳ[/J۷o/SJ, ?~$3f̀{xhY'5`ĽXbP^ i0o#_z}ĝV֨WxzT{RCq++[C}zf@i0_̗m"F6񺋰J! cfs_*wN1u|]dXγzXT%Y֚ e5 I. |j~­˹r`>VVV[ni3{pp#Gb<e>yCIIIΝ, ƿfͺp7 6,[M#8bD8v[`H̙Μ9ۖr(_|۶mQ* <իK,1`$}„ xG=~;BEEEu dРA gϞ5)*^ҥ ]wT8DѣǢE8cf7o^@R8ӧ{Ac7F -w+AAT^hĕppiZCeuOQ ohO:1ጹ!-ʖk؎Lko*}2LR1*&#g2 mS-Lz*u4W3 F5. tkuBĽ+h&r'O~.ܽRfL5w kfZt3XA+>{$ge+D>Yh*`mڴLIIٰaCdddF`e6m݄KGDDK*U ~ U]reZZCOAdϞ=F=,X <<\hΝ; .k.䀬 ِ׫W(%JhҤ  ܿ?wݱcd_T~}s8qP dff+Wnԩ=2(}x޽[&A1oJ%b~@bii*ݷoCIAĿ;` xn޸$FNRJ-[G[ݩM.ٶDDôvUٳoڙ*Mpڭ?0L{&fST%e9{`Y1na.w8[{M͖-ιG7f˚vbfre͖yGR׶SzXhvcɺfRYgWQRWh$ncې3[SjD1EB\%F>Qd5jTtt4,yüq:a͚5_|?~wNqs#Vuի(֬YĽaÆ&kppp||o߾8퓓!!!իWoٲ%J)St޽dɒy䉈 ܽ{Q] 0plٲΝ;[[[as(P~:㍇!NNN*JT=quuDIIIٸqznv O>8+VB1/jaժU]vXٳgq :t`ff8a0E/^8:u*ͤQl0qhpa4 oB0-80F9`F]ڪUW^a7n ̌CרQc…޽{*)yvpp@p N2eH9>}:AUk.7S}Y*S0Xq(U(fr!#f ctfxA;Y.BcF9wmҔ+[#kxk夵&,R5^Xej/eKFd-0M ^;C֪_&:R ~SHxtGr\{(2|dͺ&ug]=iVN~} 4V(UI{R"4Ƕ.9]lz_9H{:6__tttN~g???(N'hJbŎ;֥K8\g޾}{),'ɓu?Jܷn:xXtd2Rϡ$h۶-B  *dr\*AN7l =BpdXCMv -ZԿH|~ѧ ۫W/ 4Cوa1X-*"H**C/)Uz8ITt~Q|w"*_>ڽ{4>PF^d U mذ09.iڴ)%)/2Q;Ry͛7C%z"A?ϟ=ztҤI8޽{a銯w\*XpJ9rDwS&&F2"<<FkbG; gdYw1nc'Ed]GsETyY|z\~w!HqGyR\FUƲ[9G_g]l2A3fCݫ\iWnH޼@x2 9 4w\(EN0xtdzqw;NN'33m .۸qpf͚VB$M4`w ׈#`S? "l~}HP9̚5 \pA.cCFgٳFez`rFqy.5ʀ{B *ԢE (8&A&aΝ;reYHpeIa0Hi8}111͛7GLHTL.Joߞ#>8N 8ڵ3jl`d+  IOOFLD0QӧOwmmmf^¹g($/OIIQ$HnccrJ&vF]DEEx3f N<GI~o߾x<<< sر" rMf]du[ ΊrQb|Uj&:~qL|w]>YP5W_}a>DYJTP)]+P|z+PupQ(xW/o?u2Z(j';cb&} kn7BVig hiIŤx1%s b MZ OCRyVVĉД ԩS* tH!mPRH'@>8pDJsxʔ)vyf] ^fHjNU?CΝah3c Xxxx8 5g}c𼄄ؠkw  <~8ڛWѝ ra(nݺ%94T#1Q0ZÀhyMJJ֯_n$:F.$!00e˖8po]]00Ǥ$6fèq 0Bppp"E ]/_>?J"($0"LHJz4)ITVhKÀ(L:.N`ZS"##So߾d}:tgªU0*ThaL{V:. !V 9bF-ܻ?YYC"Q+VUsvyh!$WXX lSͩ{!-Uz+U+ZF4ϛO~V| rY p>xtUum3uAŇ ysplkClz#!ڿY[{UerCblkfk-$>F-S7@ADž Bd8>} yE>jРAVP@k0"(2|sIH}q $Ν;)-Z8vXZZ|/]ZE Yn]OOOQϞ=(:uuu .om`AP=ȟH ssӧO?|E4ZTPՐsqq|bƬѣG]@Qb},!9ԩSt H?"0$ F$UPh 1q?(ҥK#UH"g Ο?9GU68p@:b~qb`&9FaZZZ"4 4ݿ4(ZPJ9>A(L> *Ulٲs^`$t.-N!9`&9Lb#NBY sO #|17`JW7bEwp m(aLZo{f& \޷o߿j*weZlBZ_vUe$Awܒ͛pў.:>cODHI R݋KE %H ';~7oP mhK{?ٝsιwf.wls*+㳏WkARjҟ@ wQ?ww7oޤON1(>|$@ $_@ efqg֧ xnʘR(?៉$I& *JJW!7Hbux)E{'^)$?H Xw `*zcTF>'1!#|\luҤI uh9BSNϟcsI\rԩgϞB<==6l`4iŋ+@(8q/RbY:+C2S|m_\h*:L+1RXpvTa[SP)^Cg:\+WNғ)<"h}A[I4M ~aK[XO07F(btFBJEQ]$\<\):_1cpM6vڵ[|9.$H|crrWjOLL,Y$dQ^VhѢŊKHHxJCw""(#( H0ڗ*UeG^|dee/q,@㸸8SSS@:6H##lEXprrB{9233366[ڵkh`kk koڴ) "c?#ESpa BTTTz7x077Ȩj4`YeРAwyNW$S@ndCɢȒ(%kVxq=0ewp-z05JXXw-_WYHu4q絚- 2c#2UE+'/S/qTJ%^Xxx1XCqswI6V=[Gd+:@/^۳gCݺuk˖-N4\yʔ)!C)S?իWJ`p5kXYY!^C:hGDDlذ>}TP6-1]v!~6mڸ-]zu]lKGsA.=,\m۶e|F~ajIѣG/]T\ۣӧN:UZϞ=;qDΝu|}}!Ӻ"a[n5(sgffbsر7 ZB4$IQŋu)0aё ԩS޽ڵ+fJmڴB ) q763Zu\ghv4*N_#rm_͖E;r'+)ֽg7VH<ۮ[YKIi?D#HoTs uH ~lpg<)ԫ)OE i6풳q.G%Qj5UrqOT\4ф? ^CMNN={ Wh}AH˗/W\9o ޶mׯpz7&NشiSy@ ;v  Y"]tʕ+""/ .8q2|lkXR̜9WL:U~*$ 5By/k@ ]j"Zo0o畵DQ9uSȖ3j6>>ZX=w;kgJC7lG?xĝp?iUh/CYYKQn)5eam|lpğMoqmC߿ߤIbÆ ܹݻ7P!cM?"N~RJ!>tYVc?޺u [s˗w#Ç=ŔʎOA$EHLtC45/^u)FGd !N ?/$>S{kVLV9G5[U{^e!>GqU 2ꍔ#HAZq?w?彿)ۗTO5x͔,=^|fRR??jv1={6lZjqqq0lrԩ{>,]o޼cDΟ?_~nɒ%eevڅK?~̙ҡCOOӧiڍ"!z={V~}ddܬY3}{ҥ"tβUH =rơgϞC 1h  `>K1y'OS[[clقF=7o޼z}L?@ 7ond]()fצ/?~~f7ٟEp4{(&f7cTJ%9mbqq纎O!ybKE`fw☢hDR3+ 9ٚ53OZT\Jziqz^57n܈! ~1\oB)GiӦ E F)ǝJ {^w~WwٴiS'S6l;wN3X~DDĥK)w/_•GEh˗/7ho?\qt)*U"͎;"EÆ >޽{c{P'NVQ3f>8e ) q.R\JXFJхbX)I[XSFRl$m SH9b-`$08wڣߚ@ |-|ONe9Ռn3o@ @r|rͭ+D)-+@ @|N @ wG;])KoQre;;POU^o6/^҅)JǏ\BJ޾}+@O8kd+j16_8F#gk\meʕ2e \yΜ9W4lذZj3g"E4k֬I&˖-1랯)X޽իW? `xOݻw/CZl~/WRX6m| R,_|5P@ |9 NܝJkW}IO ҉1R;JKVh F.eX*92So(KVJJ6^w-SXJ.]sqB_Rڷ*ct%ZeDz"%'ߔb*x7-H㨲Uu `1cR_~ƍW\ |o޼yFBvwwy$Ib/<۷ZjjJw+Vlݺ#~1###| x8bŊ(Fw.\z)))CU؋nݺC@6---+Wkwa>z(%tKiɚT|edqh~*ʁ^bJ"mYXuxr6JiD%HfT6-|(WB>;0)) :p@rr26BO˔)sY5l{ܸqYYY;s  ,l1֭[vm8,ҥKS>W۵kCUW\9rȴiӠw:~xԼ`۷oeh&$$ݻJk%Qk`~'O_zzqݻB\x1::zС޽ߠ; Z0rŋENF RcV&&&Θ1ߏM:/'M!8I 3HQ;kԀk1P+NSn;V`JPT)#(t9ڵkx1D'''(ţٳg#Aǰa[穨0iSYW6 gS}sVx){O2՛+O-FNAXVlg.OeQl4 M@RRR`8sV w߳gOÆ 5jB`/1bİaìnݺuss:ujvvUMMM!d0Xܹsw*U#Qd88TsEiii<!G\]> V\Cܡ&L@ EN *8sӦM7駟4iQ_>S?֬Y1&0~ c.]d֭ۺu}0ԳA0s6ӧO \"`2`@wN8IFFC?x~1_ر#&6:t6 YRݻO8s9s;\0p1-Y䙛>YftR`Vq=k,9~ *Vkx<ӕEbs)\>}pN>pBL6T*>@ (_ɩ[D^5ukzԜÙ*>Hкp2}foztnbE~2@ۦ9Fi[GMTNӮf8 Aݘ:->ɩ,>}tyr@@ y=z5o޼RJo޼ C8<33ٳAh Э2D7n wgf۶mLqvض~Y;to߾^tٳgO8@%aW&A:"GP^DC~5=YtҌ5@L1\]v-[l֭ŋwdG geq߹s)S'L!Qbb"jӥ/m۶lb sѷ… B7o3Kcл6mڠ6 v!W]W0 8^\8vlnEB~;U): To]$=E峅R$VOVō'zB9R1һ<~Jiދ;۾?kj<0 5ch7w):ZSİ ew53S8ߠ@G~1C˗/qܖ-[ *;;KB֡J݃?jF ß(۷o;wnNNNWVO>^^^06>i??3g\~]15z ס4Mtx'2OlFFFD_͛׿yo`0-Z`ʁ~\ܭ;vHŋV_1+@1WB⎑kb8򺺺8)W^z`/tPwGLE r713ץ@c\AAA}Q'NĉUGB0 6p L)RÎD!ʼn' LBar9,\@rʸB@[[h/mq pqAbx9 @  JMU+38|^O <}4װs%87ͶŔF qY5 mY> yXyiJ^w\YQaн{vZL'pT֭a舃\Ӡw318|\QD^Xf՛6m^GQ $e˖:u^ݻ ߼yٳgr (f8;lE *wmM'8' Ua# }qGXc^ EԌ IkX8)ŋq~ceEEPq2d 9WO0XP!Ą ;@ Br&P)Ȅ2ִU=ea2 )R؞JE'X*B @}hmvk5 }@U0@={a0]HvVe [xchho]w:99!GEvsDD>Rڿ{NaIDQH }p*aP:`@ ]`FԢE LpB x?bVd"*֏`mmmbbYP Sqg<*EshxZ'"NB/_bddp?*,_<@ Sĝ@ |4 k"&KAĝ@ @ ŝ6_I?(>NU33r )őVZ/^۷Ν8&&&-BJ>tҏ`dd7@  _֋nF8:U(ͧ灵{{{'%%)G>}: *ԨQQ~R~8Cu. ?0|gH @ H)[)Q^Da֑Rb9-)4mҙb)#6߉%*u)~kmCzB|~.Q)QVvsdkSB3!JhxԥݨpJ2GSӤ3[SܕxDz&޻R0H=ʗ/?jԨcǖ(Qb̙SLUVŊ/\ -M4щ~Rټyƍ9r$33 *UcuEB _t)Z%U>|PR9s&66޾QF7o|)ԯ_3 {EF4(((..N@ wHQ՜bv.TsTI kv.UM_=K<ة&JYhLӴf4n Wk's|i3ZJ)+CFX?'cq ǩg=kS|)U?[8wP{11V ƺ1ֶ_7/{ p( A'p+V߿]ڵknnnYYYk֬{͚5ݻvGOLL駟TW^-S.>e˖aרQ۷{1hqo۶a[hQtttdddӦM'N:zBx%z&O@  k.lhiAu8F_[l.SU欟4]s騲eţʱCkAwO5k~JG"hNN˓1W르d8i!W53!3Y9sޅ 7JOJqv5 ]V/[رcճ|U"EN/3f0h oʕp60xDRlذ!v+Vaoܡ(N:w={̙3cbb8fIaӦM?y<@ --}{4-{rK/Q9.꣹yVQz0ITSVO7kP4Ճ8O7<\w;MQPzpCu0γA@lky)3]x?qH :t.I{֭:ts˷={h M6o:?A/ `DO:xE^rD%K穏wɩ L'nݺ Rʾ}Ξ=_ܭJ@ (_J2ikky*b,}qWo5O|\bfYҫ'JM[_<">ҳ<ϓ/ll*GVMgej7W/$bvX'F^7OB@a5klܸСCuroKpww.ooĞ={4iZ& ?>0ϟC⦦+K.ݲe˦M0sfddp9~;vI&oӦ͒%K@ 'l#?lޕz5J#)#?nzx;5\0uv fc;xF{ ۤ3W]#T?ZeoiF&*[$k?@w1)ٲJV%n ߯A{$ ^8##Ck@ /Yǝ)YkV __ @ Ո;@ _;@ 5B !^<*=XN{|C4싋1[l, %lذ!^$''eP:u,,,t˿|"fffJ DFFn߾ xbhh²R$Q+wr­~>.SE5яzJ|D gT=\0JO3pss|}.SLI… _m#+Xȑ#!!!мݻ֨PB׮]wI D-inv"EaJ^ؿ)Wրb9uuaTڼ.xa=J4%\\M723+G61kUSB%Jdz22d Rfls):qlLWOsJ=!dyxK qH^j ؆׊B`'w511tbELqWBbL$Sv)48O\q"CyHJz?m׏RW[;b+^A[T+ZrBǶ/&1"{W%sJ xxm3ڮp{nK[#ȷkE O2>_ĉǎRK,vӏ;cn۶%eƍݻwwvv7h`ǎ(j*!!.")S&-- ޾}kgjjzW^UXuus|TR!ӣGeFFF&''j~/^(Y$TRo޼9t ={LӧO# sssn(Qs(+++gH]wޡBun޼]vUVŇǨԩS1C<rXtiy{9EJJ "E^^޾}{aXmذ!gf߾}aaahܩS'] {@ &@%-"ي5ԻUC;Y/WY7wIK@fR\~UQ(ufJYLm>&)았i5 毟f]R>)<+=O -{2\rbŊ]x,_G4 W'TWn߾}͚58ۗ/_l2H'"ϟovBLDXv?6lɓGj*bsAL7ฐ`Ԧ pYhJPޥK޽kffK>}6mԨQ#Ν;Ç=<<@ bׯ_cȑ#Q.|…ɤ$u"E_^xqz֭oDDDlܸqwݥK*w|k׮lbb]fB ![I&$I#Fdr֬Yr nmmy@ X׋W[ywEA“;(e=k'نr'i:KsDѴrr)=U1K=wrJ͞崵]g'v(wjr<(fB(ֽv쭒U%ğϊ/w2uUggnMeko VXGDRW~ͰB%'ˏy"C&("*|N|z{ ^[1bbYu}YgCڛ@J5U#i{ #Iu/[TsWN~Q`ΝjooSN-^<_pyec@@CpϜ9SZnݺ !AAAcƌ wrʕ dgg-ϯ{^xxx8rɽDs+W!`8"߿S Ǵ#[e1/'VZ[Y0 RVZ͜9㉪z٫W/DCG02r @ 4,½Vܻq QZ#'棾aaE&.Oofo¸3#[eo_Sw;ɁWMF̑e{*T*FCvs\7^bU|4{ `?,{JѺU{RH}y$V /""e˖ٳgGHMM5xFw\P={@Q{ɒ%GA"kkkΝ;C:q>~ w-^466ʽׯ_ի(OM4:iFXLrT1UH|T5j@"|˗/?}t#iƍeqGSNEd"-[&;&LuP-ĽM6cggb ;rqqiڴiݺu--/^HLLW @ K;m$>'?]+o+L#+),Ќsi /*k5RF пp˵<;QWQ!E{W]K١ӻllj0A;pq?Б;qQ] :5]g;S.잰a\$5+R(`x[OJYkNY*"ߊ 1GݜE8o=_qug.% /r5W[edq4mۆAF# ݾ}{˖-aV5B[a޽kժ~7lŋׯ_=?jbbbcc٧OoBUqԸq01FEE9;;o`nҥK!ŝ;w>WAE@ .DwtmĈÇ̄p#E ֩SZq^za󋻮Hh="&E/_ ӯYFU J*UjU0׀1p@̈꫋@ T+pB晼)oMYHq1C\-zM;3.nlJOjRސtK]E^KJ)&ͻJT1k˟O%q;nUȷ‘͔:1u%9m*ceQ٥qGz0ZMJ5`JWxб}H,'d7)EM:kEFee/RuE2һ08)< N4fKrOs$m{ \Δ6/Eq.Ĭ :=JO^/98AE g[ޱcZPoΝihe>{իWK>{ln8<"E!9J\F̠<>|8gΜcRV-D;>@ F"E_o!?DDu26uKvO Ȑ`)pυ'5j})hb5$ܽ"g5TvڙGDD\|YVnbŊc@{jĝ@ @/C=BQ5.@ @x,J#&ϟ~]$m(>A]:&f XS"Ӱ=2^.]?yFFƵk^|wWUV+WCqwwqٳ>{O>Fs@ ӌ~qu6edB4kfOҒkbk(J,'[Ԓ)mcIL-5jS@ʩt}f.ABy9K+TڤiF4eQbX*=EČmٓlx4A{RUJAO(w*/02a[Fni;L+p2r2!1mj!!Efzn/ژx9g󟋛ی3_h:uL0!99YTZYYegg* ###lƘelllaaOOOGJTM߬,D(T^ "`/wߕ+WhIII$dZZZ"l||*<^[[[k44@3=zӧ_xRqALiffV~}Qx+`Y3FwG~…u@SSSF4yfܹ;w  A4Aj^J{)%11Q )l@ [Ҫb)Q^՚^lVl w+Bd4J .bvak5+7pSWJ~~6̔Ԭ6hF8Gk'=f _< ۳3nRLfR*!k׏ވV(ć?VqSVvhs1:L 4\s5B8H}ińX 9w2[~Ti"'wܠiE$׬)A=khcm Kk*9Aة?.ebb,q.Bqj׳OmC?uqB߼y3|p޽{4CLqQQQhܾ};ܴs΋-ao߾}~$TZ"#x׮]eF!>[l>޶mfO֭[!ƍ~ o͏1`&0ߩW\{ `(A9;;aذaJB/ۇ1ԥlٲ.\!#K.8p`ժUQz$;ٲef͚!KPPЊ+<==tq(,@ v LBRpjdbFGF\8,ee*wݢ6Rst}]&ۗh^b9EVs媪gP"/3R1)=Ua <s] R89mq ߩWxm8yVp:߻JNPZ"nQv>2O!EW>R46k^c4o\Ewn)jz1;?SR$$u*=AD Ў%ĸ(eQTVz0ՂݒkmDÛlL mhKk!r 3g`T//=zرZju=vҥKջxb 4՚5k:táR ӧOoժ~S<9{a0~D.V&UA7n<~xo7mڴyf/2¡)iӎ=ɓJB(:`5kĄDb0OSҡ>}`vW^E xBLEowލβe3<<;uǏC 8@ j ɩx˵͵ #6/_;oTo oBr a):L9z~RYn6\:Os[doٹΐʍ[Lݽuh3@9f]#\AxG1)j6Z1=sORHA#U))\;}mP!/5;#Ӭ+۲2PRqQ9s;޿F]fomG ? s ۷wܹ`[[ی a UR%ݓSq]\p?iҤ={‰_| D'uuu1S@g͚eP0Ѫ@{$BL?y$חeY|H>ĽEPj8::ʽ.LHLMM1ի.c.hx]p[ݺuBna2eJBB$Ia0!t @ o46?iqzpHٲw,E1 57IsI79c1uZ!E{qKirދiR[~ ntM={ Te&oJN2m([@Rh]/SbB4FX|zV?SS_ޛ(IMc `ZsR:xrD%3 72+VL<?{۠A׮]#Gm߾=dA&L;(/4!?hǏ7o?(ʕ3gl۷o;w3;R]A€^^^p&qo֬. (Rwxb ۢ/%K=z4(ի5n8FA+QĈ#Q*U.]͛y?L K;2V.KdK .}qY0B1q- ƚX5-!|[9b!`ŝv*< :oW,'};&߱RRm3EIbL/R{; f*ŔD1)NزX5pxr,l(]D(/<PT/@^6#2zteeABh04EC'YYYo+}Ȑ!iqvv~葁o߾}!={k.###HR?ƍ 5H"/^Dweˠo޼ܹСC+Wʟ?7n8z(9r˗uXL00777wpwh: ۳g{zzΟ?nkk+ۅq˖-ׯ_ڵu|F˗/:u*`-[իWh0`֭[/Y @(UeTFlyO ȱ,̔JYb"GLiw[)]mjƔN]{#JEUB6/_qE˱Uj##%Hc\RĈ7Swtꌃ3% {Rҥ*RjCB(Xۼ#ljyRF01kޡ)!FI]aWCm/*uhuM)DIL*uP?c@;i*gRO*F௥K .Y=<k4vCXuvTR1jTR5nܘibYm @-,,0+(\0l/0+Go޼ F1UVE381>'-&&&*Ubש"G֥ЏdffsWZ5Y^xVPA_FP#|?FȞkjj ewpp@!:]]]pTZZ&6؋C0uN @ |5Cvۦ]LrQk;)9Au@ FiSsAݟxJVҨpJ k@ q|5N @ ŽX *1w׉ERRRxx_822IG gDh/opʧD8 [bJ\\\Fw7o=:tPjմ4__aSNϟOII)ڷo[~&M`͝E1<߱T`kW(l1#U3wbLlKx|߲Ph47`tLIA~{ew!511B~h!4,z.ViMT,Z2gm9JMi6/C9Y9z74sVW$uSzptVz\}|Ѣ=BJkl&dB%q5jرcq9\6k֬4kkSN8q+vvv ?~ C@R)S,---[ {CCCQCg?Hfذal8d„ Py|j0عsgʕk׮-B:B]vbĉ @ ~nSeT=FgWt4_1pzReѭ^c՛J9VݗqtaROﭜUxtK ~޿D-y2~ᰘ^1۵xCsz5i~#nnŶUd]?E&9>(uӪmߜكh'Po~Ӽ{i:FCf!H!>WM_|N83t~m۶U*vڕ/h➿5kw: R~=tf 16"P;5l/Ɲ ~lwJz_$[DQi^]LMyèwCcbb,-->>]]]6M.GDD %k#&"T*\XXX +;R + tW$ ~k4:Bb n*AALf>yܹ59GNc ++5;O\^Z t1MwhSF~ܹqύ  Fw? 5fl .M <%2B12Q(bԩv];YKTZ,e3  >q'  Ī;qv>Ru 1P[3I]]Qcű>υ6vv" 4,AJ6$Lxt=Ľ\|-c+#l4fV"щN̥-c핿zBzL8p@~~jU*F⎃_Tj{{{ؽf9s&6a<ϋӷ痔P(?>88]9s- 8Q#t:fxSZm6[[[9Cٳg#&4ŋD.߼y޽{@[[a޼y]]]8[nJJJMM ÇԪ b2n-t6VF\z v}ePǩSd1}Zhɏrd{6 g`ݜ"NInH CXP WΊ{PĘ 9~xEEŝ;wy!WnnnQUUUvׯ[TT-^ɓׯLOO_p!X]]QiTTԮ]b}]h˗Qcǎ[n! 322֭[$CCC]KLMMKRYY8~yG%K_=GV%%%,m޼ߏn@bb۷2G%,,/' x7q|Iß ~Y*kv)^|7XS/CÜ,>0XplEqC[eWuW g4ܪJ.s-: geҥ+WTT---ǎ[jUxxxii)UJJٳg>o٠0lF(C!.\hkks|\\v֬Yٳǣ 6>}ٳgVG.]}vNwСWBӧOWՐx=7G!z93L&CX`߾}!xDpѣ/^[WVVv̙sN2ɓ-]Z(kGnk׮^@  IR~+ȰUeM%4_%IҖ;͈e}az/~-Ǹ? :\+p޽{7<5**J.ZZ]]? uuu+V0 P|x슙Z֎P^˲lVV"檪[xbΝ]ld7448ȑ#xDpgd;~ƍzdd$NHH@!q' bwq7s>>;;;,,,XXXPPP...!!!EEEHHHÉwww¬ooohhhWWWxxxqqq___[[[}}}jjj{{{Ėaaa˥ƣTTTBBBGGGrrrfffmmm]]]\\\SSSbbbtttnnnlllsssRRR~~~|||UUUdddZZZ]KIDATxw@ǟR2A@@Dq:'pqnŅ7P=qBQD@ )bIsm >띪X3JQyil3q'[} '&}TUBls{>P^Mo*MZknkT'V,QAP[4QZ( $BtEn-F) εGK5Pvԭ/"Y[ k#&*jl^?f2YDž3>@bO_Dx:ėx6~\|HW,1;`[ݺnw =ElgWv܎.p$Uvw,֚,wۢJ?KM&U.p Ƕ/t7H%cllvHIm%, EM"w'֬d+2]k0}߆GސkrrzL!HͩB޳&<;q_X 3':)oLHrw|ۭu;3`X8)ԕ57rVpt3%m23?ckEu̹$U~F Gxk]'"Tpӎ)hՄƪmhE;Owyv:'. ߀f4X9O+*hV tM;pja"~ѝ+EV%`d68R^Fe{6 o3WDP]aa)On 5n.5^D wѡ: YW~Kܴ,Է& D7F+6m= ]c..<ʧ~c8g6f|R3>Cf5}s6#]0|Tݘ}, )~QO[;6 ~`;W3ʱ8BBZW?Ϊ~uKM||6gwNx߶|&CXbfrgɤ-KE3+УKbvY:Ak^#Wpo~3~~sU<ߴ!N z넾MC>G9ҚQN>d/mTʯRY6(k35v_&_) {>Zm~Hr @]Dbt@& E7m!xvM~<֛_xvaqѭu| isC;rds׊|U T):˓1)8_+ر߳}w~FO!]sIqPkC~C|᧢>3ѕˌېP픊.;My+ReuJE?T|UisbŸ#vuOܮa.LfY[oX/1DOC~P*YA99DHS"ROhCD6ů DHcJּ#{鮜{@Cw%<5tZ2%2r~%+-J3_\L*}^AoC%֗r͔݊nmsy{k|KOmjd:kU=fp%gzo>K=oPC QtBtC V4Z(9+ nG-\˃ZQ^ܛ䂌fe| е̺6;BeV.,#EQvT9ݴ!%w~#aѻP+Za3?NJ8+l8Wu]VyK'-(%z'GB{c_*;tm]'Af?;HGp2iqpz&r*5?y^A_ؒ24= EG*DXkF݅&-:t%!jiKzy_dP%4T" cnry  u ]~VsS7{#ovoPJlgcYK3B՗VأRѻE쏣QD5Ͷ iq;IC^XVw\Ks6z-еM}[Ht~jwLҞ]N@ё:Cwnq;FzE/ nݯ S*Xw 5 ӕx9\$>^ /2o38g!A8|saE4@&psb;75p킣*Q{|96>ơ!MF^CN@T 7OAiSo׻0wo;[NR㟕QJ13m 8awo ~MOy~CI#&ȱM li:jOQj,jglGYլZBA|ǏUhrzQV7m%ۖg"ApleY5 Ԋ:WYWtW%ފVDLD%M1ѽuZВ:]ɚ~Dmsς@/tޑT[A1B#@Pta(:0A EG#@D6:4 " *AIsݹ[0}V,%7BՊშR឴#Hϊ,!S蝬eKPs LB_knN寢") R5WCWvqT#mu/ѡij_3{G4z! Ft=cZ&:ig@N5t)+(PtKYFS ]ʊ0 jRVQTC"Ea(:Х3##<̯C"Bu}y?kWV]o$@o.e|nO$nN.VmA) ^ٻLݬ6z i3O |(+ sb|*ȡG|">uTR9⊾ u9W{_7ED#ׁSL2-+k dV @?xnbu㊩{Y˲~}jmg.n"d?pc?֧aUe2M3{d%C䷹zPtD ):ϗoLxn洋wiv^c/MDуqX '[?v|y2>($u6St'G 1<ޛ-2v ZGEDitfG Onz{;]([-@) <>擵`qxkuQDtK#iMou=szU&:w5([-@) ]C]xe–%m Gp(uH"b|,%šQ#ƺ '>(:"xm= |fXxrsCUD+/y(bn ڞ"6+"K.ߕs\]bn ,o)0\5`Uobо"k%욫BXm/?X3?{khΤ(:"Ŀ2.Uvm}_/V'ƺYDQ-a¹mvEW;2&eܵ80\E=߻z}҉ HEHΔ.-gbҺZqj,Fno Ry=.U:K'6(:"{euO2ʞnVÈYJDWH ! e T[B=d!rDBr$P {aW,ؠT 5!1+O2DMOY\cSw/BEG/ߏ&K$~*,LuyVEG_EwӴ WIwKYFS ]ʊ0 jRVQTC"Ea(:Х@ѩ.eEN5t)+(PtKYFS ]ʊ0 jRVQT+:PKUaKbYkjEPj1$!EGZYq}E I EGTC"Ea(:Х@ѩ.eEN5t)+(PtKYFS ]ʊ0 jRVQTC"Ea(:Х@ѩ.eEsYyu2*n2e>QچK.QTXEeϬn/ʢSt Y֧SEͺp!`[jW'ImCQ3wLX}4Xe4&/Q7bn#[q*C] R#Ff܂r;FPtD #[^Tb/O| 'Z:7k&9q,r NFg$QDtY6SI_ ]^m_Z[œDTYSN&YIQu"d*UbpR=y{mDhb&0Ab3Ϛ*uPtD #z7`:Qʏ)\;c}еsVcd#loΗz$ͣd49hֿ)Gip/tdL\JBOl\s|Q pI<ν)F17>p0["u\ռcl$2>5!D Nz ?(:"ݡ=W޷@i}Y AGU~FyK4Aq>_)zvQ49݈xWVnBdd6k}:m_ h!؟t54@)ko^aBiX.',Ï'j)M}|A XohSng 2ƒ~S~(:"j;F,#-C}۫<9u!p;F醏a6PH՜#>$L_M?kle8:0ߐ_] #;,8zwʧP ,p˒ynITx~"`ng|Wrᒓq%Z'(6|葮>z;>S~(:"j++`~)g`|O}6]bÜhIF?zujG렳zXwŒlK& M"[MZϟjfsz \v aUDtZ7\wՂ40N|} 4/"gΕVgӹ]n(:"j+zglBǟ>94Е?ӛ(zM#/ph=' Ji Ut`*pÓkeɽػvT*z~KvcZ T7F/9+)}eO]ID^ύ9I֊#R֔!z g$T++cw}TRYBts#Q _ijUk u9:i`GI?ř^zKD:.zgf >^X*zI5^[+H1E_G0uxAK #y¥5 b%.,3e+⭽8@dۜ{353<>pOkKY_~+eÿCߗG揸#x}St7R0i 1Neݔ%(jr\+ݠpH{h_JN~%,ING8n:Z(6e3|]~?Oؒ24=X}qB}D)!ޜOD7--|vbޡOߠEVVR?f4K9.,:Kw9Aon꿉c-{aJR m&;X^_֥Lx˔VOLn$Dw:' n^f~ XD5"~僠Sa幧7)t< T]Ń@Xkꊼ{Mn2@G /Gn*vnȵ=lzG&L_rwgP(X ~(Q\07I,q\M;Oya 쯾Fc'_b+)cuNl僠m!ɃgBwG/9m5B6U* d-rJ()ıI'vOeX#rHڊqfy)c #gqHڊ>7MNO9Hڊ%,ew\L-(~/QPtD WIQ ]ʊ0 jRVQTC"Ea(:Х@ѩ.eEN5t)+(PtKYFS ]ʊ0 jRVQTC"ZYq}E I EGZ8 @z+xgw9!obce$QDv%/e9 `۫2`zxMxUC#/v>1WS! zk\U9n.LfVLb%*7@};77]*me&wGΙe?("Y%jO9}S~eC.N_rP {H{YPSnEVa,"t{7mo][] kV*+ QQtD )-6{v^+CQTۼ~ۡfBED%dLt9AXo ,ք>\aڍ}~{h;re+0$+PjvRmm1oM3p~`fq[IH1EZǗ4[`,,Jߪ6~mM[/hDYz]2Y&ZDѻ8>9ZF?TppĀ~הor38jrQ{q诓St݄xCx-P`E֯;Bq#=BF%QD ֍,,=OKEӛ0NP;4XSdk?+[߆^%Ɵm[MO\ q,zm+b-4.Z`yQ6^+"Ɉ"^Aα3n=2ѵ/%CfP _v:U!d6TB#Ĝx-IrPtD ):+Q v}&Y4XcJD#l "oM}T0)cy;?AuGapRtgwsxG2sK r-ේ:upHZ>ĥ sADѭs#mJ<1G6|ch{sjߺ ODWiHCD5T򻃢#R7+jp]DV-iVk>b ]oBFr>IrwPtD J0w7fDo?#6qO s \?I4%&1oya <~V7؞9'pR\V ǂnw$9),nn 7sct;9sdI.SDD*;E^ZR6zeOt XpR 瑳5 N&9H"c݈?m R3N$s#:F6rnW>韣K Dג+/0UU$WHDo?󛖾wR\(6@~2)׳~(":KOmɤeKןˈstvd-8sIhAByZ~[] z)O迥W̕D@) 쯺?+aWACrfkW/%QD􅇆<;'ck$תl/<7 G̹4i^Iཉs)$WHDWX9_x Cgec3Z +Wd6{AGs@/ ("zOso<܅`9"KYa6akכ*~>?#]+4zq .3RA[se4X*K"zedo79-":Y}3fW$QDt j,M=^vXXM+:]Ć9σ{ʒJu7p,ާ%蕁#R@,ѻ(!x.as>ZLAg*`*M \o ]+wkv8Up`ozS8V2[i wo:'{ۜܙY"'+EG8[|Lqln9fI[gj>XŕqƣM q =uF hVoΒdGrjJ"zeG./j[yNFKPtD Zkƹjtv%s[P7U`J% |+>+KIG6id%2PtD TC"Ea(:Х@ѩ.eEN5t)+(PtKYFS ]ʊ0 jRVQTC"Ea(:Х@ѩ.eEN5t)+(PtKYFQ8T>"pl#TSyTM"1dT+=TMgE Iܹ1P ѩ.eEN5t)+(PtKYFS ]ʊ0 jRVQTC"Ea(:Х@ѩ.eEN5t)+(PtKYFS ]ʊ0,n\uנKYF]Y(Ԝ/fN^\I/%5'16b3v^bx a_R- a=aamLaKVK$u|Tʵ1PkF U=}Tg#R@,mm98Hidؓu]?o_iJ+͓Z_V"ԛB omFMi.0ƾTNᾜ;A}_+p+ĢcLtu]HqD7lN8bˋu־ڥJ}_?κZ^  D@'.OmÁ_g/Zcu;>uAcUcϔ bOnodt`-w"p iy}qw72nG( ybdtݠ; '(rw wwr]㓙_qo|cu;(:"}cCBB-AAB8T"yjn+> !ѿ咢scfh~ =i"lw5iv_Hve.\t+ݬ n<zwғu;(:"zGM&sȧ~WVcnD'ED}bӉTN>=&$ޟǜ:&Lk08/ѿjz[+-O҇DA) ˔@VnAß֍l%-Gw¸3CfM~`9oeo]y\Iv7I{6.j qy NsfwPtD #.K<(a~O\ZGAq3;9'ƞڕcfF? Hm(GO\-􎮘8?G<tni7&^7#R@+*po3x,(8."z[[&gaVM3gɨ DoL uE=\"$bgN[9r"Ht|gwJ<*TGPtDweO)gh[uʹ'?h>X2l0闎cӍ ̲`=ezG.wk(y_r[;(|UW#R@K`~QMOy.2b SMGs?ɫ0wHHm OiP5EG8t)+(PtKYFS ]ʊ0 jRVQTC"Ea(:Х@ѩ.eEN5t)+(PtKYFS ]ʊ0 jRVQTC"EaՊ۞ UhCRCV<uA&hCRQtjG]ZĐPtjpN5t)+(PtKYFS ]ʊ0 jRVQTC"Ea(:Х@ѩ.eEN5t)+(PtKYFS ]ʊ0 jRVQHXWSl7b+2ѻ;!Ǽ+fkUт~Ongo (j{`T14T!`8ѴASM& X|:-~o*Gt_(:"}s," =GAoLJ{HѕhdQ3z)]#Dw|rxִ~`ZCѳ {~= ,aNNp͛;od.w LbȬs;V\WDZwfv3j㣠VWA) m_vwͨ& ]Oɇ/ʨ83HcJּ5#Du+Ue{RXɮM~{3!ZL33?k~Ur/_\u=$HD zцE_Ƀɑn攣A@%_ ZnG-jE|!DEjcqL  ۖf\JӒ:(xq'^N6{g1]5 }gy&>b..46i Ͷ nS>^i٪O.t)6HeO~JjC ?7YbV,e!*,mD%{6M:"|э0^7ivc݌sg-k&{ː^NRnڲ#kQC@) Y<#!q'ޗ@^k)M%J>-[?|foׁi[NsN[w ~~Ex}}zÂi; w)^g狾1hȿz.* #>k`rӹ9ISF9BA) ݲ~Ě`n[`ۄmC+u/d#N>V^SfI*ή PtD H~t'njɍjU5HWePtU 35Kҫ:pFWa(:Х@ѩ.eEN5t)+(PtKYFS ]ʊ0 jRVQTC"Ea(:Х@ѩ.eEE R5@!PMq R5y@BƐ#TSK1$s(:B58Fa(:Х@ѩ.eEN5t)+(PtKYFS ]ʊ0 jRVQTC"Ea(:Х@ѩ.eEN5t)+(o^kb%5*%bdc"?\ IPtD E2Dxo|OJ4A)}l#qg{RqEo#,-fM].@1$D-C}2uin޻,]>|@Ƣ_(betZ,7CV"`۫2`zFbKTng'wonND݂|G{!o{!qV-9ddNs þb9DLtc8{sO}-bv t2PtD )-6{v^+C󍉤~J_[&ft{dl#7 ʛV|bCQ3w^9e93. be$>Yo̒~^VpbMW @) ցV}äJ  %d;d&3 nYGI>8O beS2(<%q Stݤ_ (B>}9HqWN,VT^yn)+ŝz}hR7HkS'~2ѽЯmd ޒ> @} k’o6>sc~~@)PKћ {5ݮr&=+?1ds}2u/RIX6o_T "V#z"qVGw*nFNhߧVjPtD J0w7fcH Mo_Z5C[˜Vk`N%0&9'_DLt͒o[)sNqR3y + ckTA) ]zu`4qyAt!2̲=Ay4Źyǔ7ƜXu}w jc Nni|ɹ]YZǴŞSf/7âzPtD ~tsus&:b-%WqzMJ4 |  H KYFS ]ʊ0 jRVQTC"Ea(:Х@ѩ.eEN5t)+(PtKYFS ]ʊ0 jRVQTC"Ea(:ХVG@"&PMq R5y@BƐ#TS8T>"pl#TctKYFS ]ʊ0 jRVQTC"Ea(:Х@ѩ.eEN5t)+(PtKYFS ]ʊ0 jRVQTC"B,͆y^ZCD$be*JD2'~uhۚesz%[x&,yPtD %`ޱ5Yo+bXk*} lW]lGC٧ |1~i6f'Kd}wOѻ$;o]%3‚~j vKqO 69+Ɂ_p?(\Ќ38*1j1|o J|5("ؗgD@ӨQj6iѕz+GV8nilWu$ɶЍ5HLHrjk:PtD Vtǚ#O WFy-coQ<~@S &(6Py*E$pU}"׳~'R-ODq~ޏj[9phƕ:'1󛖾1W~LM$qyOykG1W 7]و6 &3*^otkkD}ԍynAq*|kW䣅d{6-&v:5|5@dǷkn(+6mSK% <]~``oD&V <FhTn^r2p>~LorN "z{GXڸ޵'syu3l/΁lKlܴ%=CȂzG|Nvb ;nq0^pJ⥢V؆!0U_m[Aޟ#RM8B~>~P2,>+(G\`10%| "><%rLҒN 3ٙMlѯ,Rt!DO1>C]ΫgDY[ǚ9y!`}r~x-SR?EG@-E\ˍ!|Fl`|_JGo ._'&:{3?Kf_"`;w'S Mlo.kOe#吏&55ب':uֈ?Y?EG@-E?XHe3 2t,ɝRg~ y1zϊ|^&KDl:a]qya!џs| ;ËO 2SG%">]Oi O@) 'p<Ñ#OD䂶)5%}JD?G_6u'+s݈KFׇ&7vv=;E!ont|6Z1=x/9{dI'\jus7o\?EG[& sx .i> ܖZuŞ00?5"FkʂІڜӼZ|e KINۉncy=0/a.c- -CD>;.`؍;.6%&)}tt/.(:"E*蠥^ҳvWC^XEoZ-But!e}~Mt{-VY0&ǬIn[T2ǖ٪ZvJ+-J1|fV9AH'\ ÔWxayIѡ&'b߽fZ7p{V^ yP(S!]f%gzl(#; y*$"/0o 5Ym9M^M~ѝ$DH Q Eh离eO^urY\+/#Rv(ᘷ'yG88WքԘnSj!tMglgv"ZT=5;xwM>)(:"G`qnH ٴG%"aLT~D pOt)+(EX$*a(:Х@ѩ.eEN5t)+(PtKYFS ]ʊ0 jRVQT#z/;ѕI;8:uuALtov ig@G5k\E]?-~3 {ٔ&wI;8-TsU15i@GuCUC#H]dv yTqX'RyvIqbXFQEi@Oޯm^O]t=U7"HAEG#@8RIENDB`unioslo-zabbix-cli-09a2fab/resources/hosts.png000066400000000000000000001124151471265333400216060ustar00rootroot00000000000000PNG  IHDR SsRGB, pHYs%%IR$ PLTEKKK555CCCUUUuuu!!!PPP???"""zzzyyySSShhhooorrr;;;mmmxxxLLLXXXIIIppp333 {{{ZZZ###fffjjj'''111lll}}}¨[[[,,,```Ŗ///|||***cccɻ]]]TTT>>>+++)))iii---%%%___~~~eee===kkkwwwsss$$$777gggbbbFFFWWWAAA@@@vvv(((\\\^^^222RRR&&&999OOOJJJQQQ<<;΃ԟN??"QQzH!9? E4,Eu$#BrТӣP/1PMypP߾1v7! xQ4U8zi(˘9Q^J4@}D>@ 34J\ÃʑyJ@=U=SBIEY<)t^% ;*q. Uˍ QT1dtLsrGb^Z9#(PqVP~\gz^ķaJh\[]U-diƫuO0Cw$`Uj 9:*{֨z\@e~uU. s&AhL oE NoN^M ňnG@~ƠfDV\79yɵ u1*T7b3h/,wz"`\ڕ jO8\:Lc E1-kxMۑ}j 'omGE6tOpN  >:SG@~]/U,W6Rt!~(gt%.lp=Ű>Q:O@ ѾG733M[Q/k-ŏ.=B ֓#u@ u?A dE@&PtdE@&PtdE@&Ptd0,ٹ22LS]D_Oe1~#U:!pO@$Ow"IJPoƾMv/&a*# Ý 8$Ocqf{i/Tkt@tlITb4mr߁Lv2V%5붍9@|^$]{՘c@I I(^ZwFE'-Pt|&H EN$P(:>Pt"}@  Sf#*C?ի !H]/!+CG=,K䎨 u+Ӊ(G -fGtQ9}\C3fHp9Z\;dD'spҕF?f+q~*R'qS?4r?s$M a$WtT[?Te$']ϖ4|y壔jL~Л8}ņ|U!eB3so tq*XPLE#td9oشŝ8KXRD698̿|QƑ_wDKxmYP[҇i{)V=oE^g'c?xVIt `%RK>x Sxf=waj.R79S5҉f,&u7/+M˓i2}I:uƳ^0-([oq[9Q&=9z2:ʷQw~?J%zqWq *(fhlF*4i&[ ko]fI\qoI!TȄ3.;B)w"i]ȵ~[}n{IkhZ/vח>2ocZ~q^."ь!R3>*U ]uQ!mY-^)qX/EYQؕ9\%ɽvٌYѿUoULS.E"¹PuTnD-YZO(?2]yP=+s] {E=9 >.TJ(|ҢLKK')|~Xѧo=n۩~f#~5L拔6}{7cѷ=+2O1* (֫V_?R{ 9_/W_)U+LV$ÐYJ]k79@׳ܟJjFcW 0q)qu$oiyRf8PDYy$Ҕ殶[ZLH%}4u2o䗔n0f#ztOxHzø_Bvu~X!pGvmB2Ǿ4Ϭ^uU)/ۋIykڤl,´S.3tE?a(跷n\Ybxjm.R7'!a2Btqr3_-|yձCz""Ѣ/sI-54+z=]=>.&$]n5Qb1!xS+F%_1Cw(M >캿'8BtP nTfb3ӥ{9Gg_Qӆ-mL?[?Yt762_`[xgꠍIJ Qw$ !696aK3ț"YuYiu:)C}_ O =+aDW(*?BMf(~ie@؇R~`D?1۞yn FP|Ob}ydZ*i[/KM<#D93DvS46H6#Dd߄] rbE_hVʬ&˽&$]g7bGEp3\\Va8(X#Zt9u /k8njAzС^U.w-{@j;FtOfl%}HtsۄYZ(i~%zMJ'p%ˆ~~0ҋH1JvΞkh!R kh~nڗx#FtCW(I"##DUɚq,cEN JBS gv_Ҋi&t-5+MX/љȖ.|6C1Fn jE;"zLy5(\ _}L(LRz" 2<-:r,{X jYE .b_4hDŽ}ZiĢuHtUȞLt۪z0) pg4b>m>MOUgMajM]ky@#v-:X d"_V#=85)$JHŊN>(J&`$z}t$͂:]lf AD&+yT l6.SLj$`%I3]' x'u#1@=u=A ӿ_i& YJj#D<:Fֽbٖo TbsGmX EXPTI&a/:Mmy(rej&S\VC+KVIW*~+BDݒ}ַ}: I#Y*z!ѻ礄2ɽCf Xl&jݼTtWC1pw|kGD/aašixPzQ!g9j֘rҦY9n&@GnJF*9V(f9Vtmv tϵHf:H%X9J6jcy>M 9(NUVQEW>Gمw027{Zo^!^AFYi<8_a&SwBAk@Q{.M+>B 'qDϑHIy`1[WE¬ dO=W}oHe@A}V*/уX"AaiwSUlgmi,(|P<(G6`I^FI_5HS]l92SZ 껆SkUa$UE:t43fF,.2q8=M>sCk!00u0(p,;ޅTN y)_+:T5,s8/U6zrk\k4@6cx|ݰDT>LHr=YQH}Kȝ/+=5&ZJn/p#we6U-(W0ɍON4j#z,vE=/0|qRrJ rromK>jhjsYՎ w"·J\d.=2y9炶rlLgVYF"~CwW:y=g=GPJgJl™!WƩ G bo`k9e "D|w_j?lr< elǿ|ug+2:H`k]o<'9V*__0=rPt"}@ HPt|DEE'((:  2$fW?%K㷙@%Dt$Le`iZ;{rH8fҞ`Hni*IJxEJ蠫HDA p E E'(:N0Pt(`P(:@ѡ.:}]"W7rQZ ƌ270q{"-FD/l!"PFt_1v'9t#qu{]s#b tK\ޘxnaI"co5\B#} 8;k$[5ˁStʥm@Y-C&iSG^=4ہE9qqE_v^ޡH(= }k׼򼓰9 ]t6nd/ufo13pO*qJt_y3oTћ邻ВQEW:_W@.~3p vpS%Zl[3 P'KC*/-._:PX07އǒxSJ$robg}2 _J 0=5MOPY]"+)%$VZȞp 8GGD?j]yAm3Ef ;%-gC \Mcv9 DjY/lKalVcmvn=vUrdkN }Xx!V_Pj[0k1e;ZH͌.6f@ ~{7=/(fJWuEym(;jiNn~՞--3L{!/U9}R|IHWGgUu<+x;Igj)&uqƣv${eݸbâ_r+xD'jOҼ>GsתFԚНgozD֎<0 v dIG^([>ƭJ]tT[)n>.` ~pvݽ Dx]p[r'[[J]t?BX ígs*ʤ‚kBFOh7oeu0#8E,Wt)U~]#viVW}[\UUhZ fٟzq ;꘨nu;K vgC^49p0[8n%RX~)]7.ḙ[J\Ku֠գSH7Q\ O;9T.%nP 񗦋DJ(I 7] 8_I0t_'W6cj7mp;e5-8(|V |}w9 NOzӫw]՞:]^I}|3F^;X^ta.SJ>0И!| Kd#_+4GPdq~ g|TiK\UY v`q.u \ѧ}oprx5"wS8ӏ.-4eɮ?l5ĉv$Zw盷kCL5#!8E>s}#߅FPt9"(:}F۟}bw>R]3(u?}C=E{@ fDV.Ez,g9Pt|D2Dj?+9s'kPt|D2D?_pppdSpД{LÙ , lfY<}E@ɼyQxGM amwz]-u3b/a40m@tI>D2lܽ΄dZP'9ݸ\ӗG۰[ .]/'''XiX\#6׀NMvQ|NN]HpYMtI>D68~]׉ D'9ފL:7G3/3[jgZI\'&7MS>n'pHsAlB}uY 1^3EN$Nt6|!a~Ytj<Ⱥ_+8]gwS'5FWձ'CG0пR̞1Pt|D2DGWf=/"!' gi4Vs `TYF!"IGZ)+n ZVLKo%T051Q!4]⁢C'CPӤPՒO,b-MN,rE 3F)C+E7Rl0^{eE]'5]W&IISr2yoHt_5Wb>KY ]EO:t,/|ޑ@%8$ˬc-Rd}@щEN$P(:>Pt"W睏ߟ^Ǚ?п<@ɤAv|odO?s,>Lfa΢T;=_9Pt|D2Dwg\A\ٜ:"CC^Saa[> zќN.ő@~޶g}_hE.4]n'sY$U3(Tʒы/ڠ@щd. ܙ(sy{ω:ZUHWq1&@tluTMH8e{DБ"|V^ U^Bogd"ߡEls?gmH TL5 .IC'\xkLsJcO\Ϭu?>1R.O߸h}y d} ; kNjH3эud|{k ~EqZxi_a Yb{OtLE$E|goĜ儳L _ puas~ɫ~DEݐsZ|u[3rzCnڍd$p7*I7n YлB0Yn@śֽ@щd>x>pyʦtJ߃*蜫ADGuy1擠!+FJtjSu<:/d*ZE"=y %Hkj Vt׋3U؀$MѨ5@JI ȉbt[z[˥򮤞-˒Y'5¢x,k%22Q;ìŠEWMCUz܍ % Pt($㗷USjzM*_= lNq}a+dhdULNeA&Hn7KLjI.PNl-GKPI'Pc;X4+Πz\[**"O\*uO} 28?VZ'C3zȱ)%Fd5(a^ߍ]dZUUr ٯ>n$[;'Pt|D2D'D8He(:>Pt"g^F' EÕ@щkoFR@щEN$P(:>Pt"tSFys*0_n{K?H&Ө4.'|W`e&fM@щd҉hgA ):wS*JR2`yV`:G7Mkʢcw~ڻy[33WCDM8K PI*Eaek)dGuVZQ)H(.INM25>{pfb~"MJ=gJEeJW]tI>YEϩ6y~If:3ٕ͊ @1Н)˾y_gE[] .4xdJ)[k;f%Pt(d=OӨYU:7j.1(zTCyI9A`tOݸ?DYwRξ^`?(:}򊮳0js.qc<EP=nbCuKlO9أ[b9VuM@ѡNtCNKzX;s*l34ϲ7IR n_ #nǐs ,\@y*TKI7qL$@ѡNg=t6Ro4YB^DPnۡ":Pt"o\~(:>Pt"ܢ\~(:>Pt"ܢ@ H&aQ H%Ӕ]]K|E'I'MtX*tfd&G ),oF/Y<ݝzs~H&$doe=#":Uۯ艓MQ,n4i6#H$$5uչ.IC'(&z#{sݴO6C%\&Olݫ/qc'JM$q+ o[3tI>Du[ gnVUy$Iو&VƑ8$"r__k(Lo8]R$3hO4BoEܜ.$ mI>vȫdH$@ѡOstcV`*[2EZ$$F#N`kP,ZBG9)lЮ8Bt-w EQA]i)OC"m.^Տ]bzKPI*Q1 /mm3PXŋ{L=%{ޕyQgd\,(,*Hv@DO-z[݀޾0^AQH7$.{.e_?MPt|D2IEw0a̔E7yUƢtY$QGn5i-*%{nq7hM6 ʲ},3r&|1S ST"$l]ϸ:(ڲV7 R]Mk)n+a(:}҉v 0tq* u}֨Wm4yQ>zE@=Tˬ(/mÆ0N7GόU59e.;-V!V2QQTKn?y!ApO EN$N_(?M c?gsJDEΉB:Gcd @щdN$P(:>Pt"}@ HPt|DEE'((: N;j1[ix{&[hJx^":>2Q)czDٌ޺*HTjY߅:Н@=9:@ѡPtC=E{@ E E'(:N0Pt(δKhW Jcw ѹһp`,/MO?a^s~9/ե"`-Tn>1g0zoˁv3+$4g5jB;CPZ⢏݄:-DBknX+ėh̹s]t j` Ram[zO=E.=Nj1Bt>ͧG@ˀ1lI/"E > ϙCc`|OoGXcͷJE@pg؟*%.:|GޱH(=enCX2moyjAq!U M6V{wJ¬E^m +pD7Zrv^#T$K{ Օ% h38tWGXziXweT:!D.xUqf t,ۣZLt?~ Mg/=B0!Ňⴎ`L^`dU^R&wz/iUv+iF*YZm\2Ê|~:E #OOy6uB!VAZ-}!RyEO?UHo[%fQ3_b:5[&/⚀#T tkIw(w~g8S1_= *2'\ozVEq>r~6o:nv^ҥ{Z%MW ut`(. i}O Eh.opb)7,8Tθz|"L']_/(}!U73'ҒoRtd]Uf}uW4}?ck9{A?'ָ<'uVTէi7 l ;Exoh7|Ztj KWWcעX"Q]ojǑ 8'&y 0h~QWo 0Dix *Rh1{GԼ >N+Sl]Z'Q% 304yk7/#g [)804ũe'3m3\[TkN\pD+h4QNg,6ʸiXPQ0ҍIޅ~u9ع]Z(^N)\^K/xm媕v )Ŀ${.9WbM\T |W4(;Md<"g2ĬWBcqAoOOm$Oƙ(ܺ_f~Sp7Oё҄xv2=I TvE҇ZGQ:qY5mUzA|y Dy83N gogV`if2ޚ1N.[wl3| jz߳wq*eI$jDs|pPP'Ssr9`ZZ0 DfnSX֩G-(RNg=y7 }`A&BsWFmj{d}|bN b1"2,T,P(tتJM"^bO-,xgʸ[fabq3jmw7_ rfnw?)]PiT 8Tw<=#Ȱ $<\Za|<*}5#Y/ +.#hE'=4(H- kIڜԛoOtts?%YѝtDgX&)A٣v|نw.}C \2J"׬9vEz `,["VNv61c1yS^|R$8?\Ԗڛ}8Iƈ~2#"j+ >|vEYwùKDMo ѕJYi'rio hs'`"DXcGq.(HHy f|1 FǎY.p}~Dg ksVʸ]śwȓP. kU=~G-ApTﻖK9*Ml["KV~UIȭ ܟZJYNo 0-58!G%,WMjfѮ/# ~lA\SC;L"m򊵴Mu h7A1eiAaȋF\37Gng t QTܺqKrn4X|Dt>ڥ^fiNvȋAoxvEz;0Km&{kך>!8_mN&l.'J(ؑway;{$;KѢ_OwEu&$3d ?*YbM2}NoWOԕ@ϩvxESTE@]ffP\ѴV#.jm@^\7MKS/ ^^RrM=aE''E?r[|r:]H: TH![\^Y*Ji9cDF ac|J7k.a8DZP`KJ zL"rΧ)-zj+ES],V`/EǩZ}7K~܎x xozq]R{nLM@D DE )5o`OA72ʦ2o%^5<,:* ț:GNGy՘NdQ#scǂ[.JtL4|qEbhԟv<)=k{HL9S=6\:#:/1k {yT`DЍ^MHx."&89ocD|ǃ4*B׷\=>ėl*46ѭ"i}稢zTp2\{Y|Z0_ܧ}O2s+u$/}K.O*nhjTfuEâD">f]h!8)f(8[yG-bG|ϞWѽZI͊% ?֏/:'GxtBuTdZ _j͉V*-%rZѻ}ۄ4n~¬zY=ώzhя^㓥OvY\*W \JcʥJIf Sg59aEhf`EOEѓ REo0ө,|nP1;$֍Mtͪ)X|Dτ;c7jy LP [B/`D7w6T.'OBZf){[>.@Q9~sk};4W 9 ~Ֆ2 ]vȌQC5*z%Rߖhэؑ`w[XoaENLDQ#DiFxƆ9EZ7{7^A~ v)ߐvh 0lpLe@c8)ݫ3K˕t:R2ݠa pb{.}^-~w)G2{ \p@qك9p_ RIX`et]T7m[םHYַ<˸.|x[1GZF7^oeZ0"VRh :5 W {Vk6С0WtWb‚+T#Hjr O\y%T!ٗl;RT50O '*.w!lq~QAtK0Cwt33x82kL#葡Գ?&ӶZJ58|EN$Nt5w"ɬ b4*<}] k%R`xkfY/~:]EO:ѯr%qx}7ws%F >TפI*MPks09Cn *!YFI$@ѡN$qAəEcڮ/0=:ƭs1뚡E}DB)텈@/ߜ.IC'.WJsyUM;kZtBq.Vu3-̜A&S(,ze>E-zYIdkEl%,:cn~i!d+z[zF=TbZ0|U3䃋t{+@i>onq8IE'&2ɟxg‰<0i$;?$(:>Pt"m,Pt|D2YEB_b@doHPt|D2D'c4='y@d}>9E'I'l\tc/k c.坒?NKTߜ1o:~5Y쥴eHaG8XբFT(:}҉>#YW'j3XE`a5Svl\}y|:侣Tcgص^7!@щd҉9s,7x?45׃()?P[A$H2D$9/5I.V7-5]EO>ыCLdtc_jqݠ|S uYF"drQD|n}Vt]`UfL4@ѡOۊRK/bGXz5Zm̢Ul0S2e|Rp0Y>-8EJD2D.>1yЄzїwsF ]|krcEW!k:< =ұ܅q.iC'ɷ2[  nn}WLizSK,^1U^W49=?'2MtI>D\@rp(:}>^oK %F&O@щd1Oשׁ!6"αM@doHPt|DEE'((:@>@щEN$P(:>Pt"1ǘWOlƉux@%y˶0"lAoߨ'RոM (M\[] /J\s˫tqDWOXCPP$74؏SΡ NISWq|wNcT,[gDDٕ\2٘ˌ5; RmsujJTp5@QEg7,QO^&`F;F&@d{E}-EsR|+FR0bzE| N|x&@ѡ=\ѷT ͏y{LxYr_3Wb]xaٱ20sN5~/nu3np0VL `_22}ѭG\\,N ꦉy=zaF$Eͮ3iwL9S+xrD(t^?i/{bl V ssVG->v.zE dUvz_"dYdja-ծ`rc t3걢sRL¬E-sXT-ˠCE.z6u*"vQE x߯z10R$R)XMT];F&@tLk7vrU˂lMWt@MeL1U2O-8/twJ#W pagկ sO\ѩ ?)y[LjT&ݤ> É/3ډ)XY5 {K+\pH|QPHCNtNʦY螤MKbdmDzSTt?B" [!ɘ1y$xyS0J[reCN>٪:Ԯ4-|F w(ħx#KJ=ٕ͊w5y[cU3YPm?xU_Jen S&FtߏVs_ìu╺#Ҹ`ve]w1>ik/ҝ+Õ+ щ$I9J')]BR)##W*:֭Kv n}~;?>;̼1yvgzp9un)It"]M ]IM/._KkT$ [. #O,] tD+ZH\[,GTc6kU_ bp&1oӏ ?~|Džfwa5_.)Q-a)C;CGSvcpqk_nwD_ȸhc'Rgؙ=}R9k?2ۥ|) yWv"g aޠ$:ᴮڇ zf8L~pkv02GO>ŗ`J`jDsz/ӧ|[^qeez ^, E:t,h)+Mw$6r`WZO!׼;ޔ}VWl9A-u)y܇.E i_WeNNh8[oDw ˹u?^3JŶG:3ebؖ0>E%Y{dC Vg^@ŵ{˺BgS hX%^+nnzf<2*Pl]V| =+gk[#b5Gy/RҹR|C,fOX;TʏqK3ߞz1$o*g˫wUf8ghnq$L 4y%g4yvw4u-w[Ŏ:ņNWhM~–D)HI\ȁת 8f0GɈ~YoҪ@F`Dt%[4r?f'^WiRċBN7ɣ#-c܌t;JO/VqptDrfS\>gyE]p¬"t&#C Brps;`4"F%͠]yTIB7sa,gXqm;~|Up'(Y#'_Ǘ6~ Xf!{΅\{\r;aRt1W:k T,p*8=>mZ&a_ }Wcx Ǽ"ļ.|9 hx}rcwEr*tD_IoK}]S“8^c 8z꛻ Kk7/ٸ8c z6*o͓^GStUY#?0 @f| isŅ2-* GtrUU͙v#:z>͂rdl2=.؜K@{ؚ|JO3E&PmO -KUNXwF6n6Fi,2 [rNyeEOUzڋb߼{겷;༪,l[r/M.k-NJdfectDH\6_~}#Cl30aip |EwloXq!k^\8иP*?3Hȶ@ ؂It-B.&KKjLmjZR ~u4߶yܔyʷ34E6Y8#( Kíw 3>ЀwP}=)|Jimst{:z_Z7;WA>;(*F;27}4fO :unn "ޥ|a#fKqC(VKgH[fx Bߜ0 \75.,KTd?^t@|I`]]xR^DgHLS$>V H=nв+wƣK#~|$fzǭn Au84 3 =n_PZv)4M$‹@4$a2wƉY?]ܛ$ kqIq./1PtYڒ[t:7\v$*Ny&c:~pd}L{Iz뎈FNDVwйe1/]{#gGCqF{.(:䯷Q`[y}n?Mr!PEE6Pt((ur03j~(:P_|nbP2<[@I Չ>}w7 ~/pDz.` 豳&!nl_Vo汼LZ >%;eW KűUCE:#?w bIn8%wG6k[*sz\AMwq q̘#t2/hخQyp$ (1j7\%5;=p.ECѩNt{=ݒ/ l 1siSQ?vI9+70 ėeY8X}mqjA [?U/ыy&[iGN LNta;KDuD1'$z+ʋ_^㧙XlkKJN']/8}i;5a&"`# $b =yuw(:)Pt2:\%|́,猐0#o#b@,襣YYOE_T}e j~[9(]Cxv<fԂLNtNݗm|ED$MS=*$LD&tĉ왫?#~9 Ljo¥Pt(:Չ~?۹u*EYYnL!:}qFrO}.l5.vɽ+1=1 &mg}zZ`9v3sLp)Nu֪C_*&8}Y 3d֢iX|Kߟ y\meb`TֈKhoc0WAXb_X"(.ECѩNPtRdE{@I }E'N&P(:@чO(怉Yj4׹G(:PSɶ3cׅ&uv?潭$@_xa<-E (:P転gpVR4+\%@t&ի~ @I Sݺ=b xjںS~G>tkd %X--qEW)G{# KVbYiv|O&;n|k5,KimCL|y@ǡPE'z'X#}թܛOΚP3[; \dF1`P~YlKT+ڴu d/Œu݁6W3[tٸW6țg Zxs!eQ\ ,rIEWfE>ƀO5W2h.Ag-3!t7/x%<ũݢKB>vⱜA ˩O*x }$c6v 5~2MsR(:D/Ef'nœv/Ư4\L@݀Kd-krM@: 5Ig~1)}aKPt*[3@2}{WU-ߖ/ż,ig<1̕[vuօ5|vQB01)p]n>>=Ix?􁖶>&՜uUk.ECѩTtZyqN Sʻ|*)_ D|QEq+͙y1Cuƌ#i󴹊XNJݯuK 5+|=kSݽU2d%ڿKPt :^/=CD`)\7{ 5ۺ i yU8{D֦"/S~!e^qjEnMaW7x1̊y -,% C9PtRdBuAWR>mIXWy3FׄtHIxkB2u~$s.Y@ѡT+ 0 1Ns,I̞1(:)Pt2@mcnçb@>E'(N LNtKt߮GRu/8؁s@I Չ:9QYO1R#^b@I Չy|„u>ߗ}t$n t1^>9nB3]jvpuݖM(W/[F&.%CѩTt3ڜ7sgXxg\]&Oe {+c2"gfM{w_n?+ӬC@ѡ*zCur>]%b7+ <6_^{t G>]eC(7|hK Ptj&lNѶ/lA|ze?=p`//d' 8ܼ'?I](8/h>4WNs[@ѡ*zd|2A՗ BsLiF7 \~`6^4nJ忋LNttۆۗdwYCŢQty I>cȣl԰=n=x "@ѡT':pQVP3yZ|<iUJ#e+Casn?~ּF 2*epXn3Q "@ѡT' :9s=F;{Z-* ފwW%qW6KJ [-܀唇uZx}JdBuλ[vX(:P.q,CN L[ (:@PtRdE1}9evg@ɄD. ,`K9~퇡3ӧ3EZ cNq9;#z(:)Pt2:)wX 6<}PyN LUt7KyC_W=?Y^Qc?R'čK,jx;Pҵ~" Nk2Rko 漟G\yNZ+ 8D%ܞNj{,:-3S.ECѩOtp/VOMd|J:2%fߑ&;ÌtwV)%.iw|oEЯ4^@2 #eu`9NSDZX,ښPʙ^dY 5 e;ܯ6d=\ESR.䵏խu4*,+Y 8.ɄEN22+EVHǯrfcgӰCww_< yGU2~^f/%}}iHIO^NaFovw_ @I sXYNn3˪E eBY 3H%D+|%\rF:'YmFC^7@^g?^%d|{UPA-}^=\ESZzJ="%gjlΘ ᄵפyMWܭ+gul!4 L{?HGE7}R+E_mK0}T\ऍ3~jJIN LTb gZ: tg)R)Y>P{(6WM3lJcV]Eh]r#*n׷BP&}I٥ y}Vf@.tݒ:S0'E@ `u7olQ7{E:щ8*nC^}j~ZI)%nJ(^X 3?iZ\ewM*SSF|,Pt}YE'(N Lc(:)Pt2=@>E'eF\" E3Jy=w"ET>zCWʌ4ف#.#YBьVS_ukLc+dt%  _ч <39kF'^ }E'N&P(:@PtRdE{@I }E'N&P(: &m){pwޖ+S-J y 8=0u%J~Ň^4bSΌ&)C`TLhs|Qv" ZaTK]m躂ؙ;H=7 F]=kRyQCݺmx&$k}rMB=%  Jq.1EgF}F"yťśG@[[>{@^y2Ag;SPt1+eXԷbu{+?F]Kfe=6HW'^(piי MDM l̎n8UC Wk؜lovze>ZޢOztj|>,( @IW|Ygt tca#謹CD*C; B[KDDa4E?>_y)*dG>HУC~غH7*9y%w|<{lf:ÔD?柘Vj2m$q@Ci{# |wUpesm3rG>KC8NeU2h\02 b ڍy_^&g,؎H![&te@.йj6}1:?9<['6r D-6z=Q]HK347 zzVA& :3>z _GaDnteŎ3ִFlCam)Lɍ9E?u|Xiwi3 4LAw]>ɒ/0 CUzz|I<+eQFz+>a(x˩ "•yaiVrNy2p\!y;c]`jʄ<[FTKV=}6|iհ$ Dr t)qs9|u3\p,^DnstI a2SaXA3~(-]$SmD e>}Ӻu.|;$k?ҭS%{j AJW?= |Nk}^]Sl&KNeEؽՆeN&F/13!Rév}6. qUIdLҋ/F$U<g|90WqC9u~)NkTi>Xڇ=9<@^Ζ.&+<:!WH@t -9 7Ӑ GGp7;>eD'eWuq8MM2[4@t#PȀ+9oP.=nX6v~QLX ,4G]-oMa 3Ws.n~=Jf}SO-f.ȡ/3m2{?uzI:͋NYצ#%{eU[U^,-6wN[#FDgYyGAT']x@ۻloRɔ:q!zqq@Mɛ]莐76op0 BV_iy1q`'kW\eXa~~wsM[n!"nͶU(uf-A: ڟDc cTzmR $۬I\R,s\_vd5'uY ! Gl|]}t VC~ Wɞ]̋@`(ʧڙ46ݺuٯ*g]t-:[Vp՘3`<t "Pq2=k6f wkE(!E,N!2ySs0qbURj}{zmKk5 Wjv b4a s+ҩsp^W\W9ZB{^LP)^tv.$?gYN dY_g`B_b肝Ȼqk6MIg9U}^$Hy+Yi2ӐN˜ɉ5@g|GWb5IsK \80xtDKVf㗰El}:T^Pr@~ĦI K@`AXS7z*OeCusY/{OVW9}k^s߭;W+8[=nwfCewjm"`874 8Dk ~=uT3f[T^ʥWE;ڎo醴 |w)K z}88Jx `H9yqk^gw {f#tsؗ ?ErI:xWv1 `u75kiG#+QtEW ɣ㏮+>}fdNؔϣ}stDRKa?ͮepŬ"1]ؚ[yK`.+ KÀ^׮ mҴ-}YS7BTQGNYvGr^Kstzw!QFݑm;RPIA7[n}ҭ:ο=/mt*%4'NuNBe>"U*h%9?DΪ)&򎅡`co/[n`ޝJy2>_}+kK)JXO6ҳϩ g SE2;d.ę h ޞ ;0l]N):Df#ػl% ẑu>F/<]^RrDGMUȅ計#ŪF&-nOXv(.!Υ){rE4.$hH~7[^S{;46l|}rEOGJx{<8<|.q|c;YZt!g0(v0}σmlǫIU_O.6sF&c x˔QV'B9Eb\۾\8g(c{9bn9OBVx|Pl{g~,#>_̐9uon)l0:7q!|,omЗ5ajoKVօK,B+.֑9*BìZ3-B")\C;5Ɛٸ'#|bg1_@ؾ#H Su 7 Uo"s L3ֿp|i樆x3ALtGz"MA.|!Ua3a"i{GVmcl-X"dȖ[W Y/n:u>AQPLtElZ:+#gFz/>ƀC{@ч >l~n}APjS(:)Pt2:kJO 'S'~ (:P23gzFTN LNej4ǧ2J90LhWM:~@g.c9)ZL x^0wmdL4́*2Z[)wo#˽[Hɠi&,no[ ¥Pt(:Չ~D$j}Mg\`DN&leůV./{!w??ɏUSj>Ir|\^?ԁ38#et/jAגSk'ߔgN>E'N&T':2M|l%~^?ڤBV~]e^oUkx:}M e5qE6L`2Tw< F~E:ё2, |1 ,@/捫Nj]QT|oz_ lJ[8]G!d^%(:D7-V{.aSHq$E5ؠEyhQхC5KS󻾺} }PsZ=ztYEל*ykD'¥Pt(:ՉX E'(N Lc(:)Pt2=@ɄDaGuX+)nIXXn%[/ E'f9X!s}tF{ ~l-.u;(:P転 ~t]_ yKlPxtnz<\Hke\1ZHBfv;Ò W/!&xڂ$$Dt3tmkCz<N LOt,.n168eM/^9-e'X~<`WYwd{PY6EvT`P1IkEcIz8&^kdb d>?@ɄDA^Ԏm& eWs*\9> 1Ǐߵ76-~]t_aFp?!5|Z%9cnw5똲ǽҵF)<6+oaI*+XVNtPtRdB}tN}`0ЛZ/:.=^ }'"|vES}O4D{̣y$l v4&}M{[At잃C|N LOt5έ9;Ef$4E7bUBB7e|ԞLdLғf5OᓊiߧzkE$iJ Vًl耞.jP@ɄJEl|8pnX){}!-m9'Ύ0ƜYrR$6WN3 ^9~*q"[!\&$E/&lB>t֋OVѵfE>ѕסߺ3YodZʗA\zY2X\,Ș1_tݺo~ѮL%GG&06+6}ji呴7-Z&8N`ËejrGJU~r߆.ECѩNt-}qr[7'Re#lR >!]0^k`VUrCbC.iDmp_=qNrV[]S)Ax>N{HbKE:Bsw$g617E'("XBwna, L.PtRdECnJ%@>E'(N LNUj?`Ha"~}|@ɄDϒθ[ӄn Tri#78R@ɄDL$9&$5v`]6mq*Mg]R "ť;f&{kuW$6t L=͠5R.\JESf9Ne[/YudZ[$ _Ԉ9՗"xRqˉ{hK Ptj}N9Ϻup@up%\n”_~Uf&{n\L8)p)Zuߧ*i.%CѩU^me}F'Mv{8٭!xtC_>!ѽ30vKЛ֭p)Nu[v;2djvd>D]f}ݩ=R5|wHr9L2[J).ECѩNӴy %]l[MZt2p{YLu,S_TP% 03{W4_tv[.{bTåPt(:Չ;nQRio ĈnF>|3JAN L^AJE'N&-mPE'}lE'N&P(:P!M`_r(4@_M9>~P@ɄD'y݇΢@-S\N>.N LVμ'=QÃw߹7ڻ[u X 6벉o9ƿ+"=;ڬFKk"oN߷i@`:R|FokZs&7Mr(:)Pt2>/Jۥr^ x,Uqbwx?() 4n-ʿ+(BB\aɋB!sD)ɢEg|_%7ϯ)Y̬%&]9%0R(:D|Z,;L0Y]x^!hq°+(ݴbh )ډק& ?mll\yUs_/9%ԑ}پnylAECVKPt=\p3h,)NDX{-`jPX0 c3 #h kBWFq L_;|كu:l ^ )c:HAKf*(:)Pt2>G,P. cc(*\Aj"E*,W;"_@D_g{ۑ3[Wr-m?^t@jkAD@:yҿKPt*=hӼi+ib97݋EaƵY\Tk.ݠ^+z]=VBD)EoGE]Ubs{>dSsyֵ"%iؔhx0=) "@ѡ'z"qL,ś_0ө5):UȢYYY\goٙ g !g`9a<+[[VzPWy R=U5R h_Ă U"@ѡT'QA&0W&ėXtIYK+A֘ɀ>fH{=]QQ[$XfEiہ^D 䣖hs{-2\ܹ@,-.~ٵQ Mqi骏>}UoeE'О}$(:@W8k(y(:@PtRdE{@I }E'N&P(:@PtRdE{@I }E'N&DuF\FWM E3Jly=4?t2:hr 5u4D7O62Q(Qb#oݿzPA%+GaQǿ݇ áx>lPPaE(:}@ѡ=EE6Pt((C{?T .NIy Io^%V^~kDwaB_6FQZ)zpE?Rp!r*:t㸞wr=X:6N%4;< OLgd U0' ҄}d4Em2~XMEJ~42q"6nR*SndW _wL@}4j4[v^۾/L*kVe ݷIoxU-=l;(e@hy3nLWY .ˢ;V[r[5R5&TT2{ oE$/Ȩg D2)sc%_'P6rD{0Tqg(+ٳfK_p`N鬭^_ZY$:E0lV_-e`ݓ{ 0x\%lxt&{чF@h^%;ʱ2/ l^S/I .CYKG/v8)B5ށ_嵖vmD -2K6}G/Ĉ&gj  ># kM*+JYib{F@m M"F%K HEf}AsTV39[$]5!l *rp]l#+֒i.)=<$k-noԯqg(oѵ5C3Ul88ҧ<":tV>kOyYe `5Nxk1 ^ryAn?!v'z-/Pl }ħB3ׯvvȁS`Z}u+ :ĜNKˏ\}Ub0xű :-6JJ 1I4=l_eu`6_[9I1|xZʼu$.Xxh%r]%=KHEgɡ98%穎|nA^?jYOD5W}wDALJh=u{«_.:O^0jk΅d6&P?rłu,ݵTW$ѯa˰/?2vp9Z?_> \El~DgnrKYUgc88AS#fXaYa`嚯K/i/^?tw哖Ds,iIhY ] '}5I)NLmӮ>cvU/!R$0t4^Xo`RD;o(:`k3W'ћgUU wKm[u6aF[BzBd.;C :ᴮڇ z'89;tggZݿ1PtI>`.3L3p.CZԥkd|?yꆨE;!P`1'U/naFlJo2D~ ywFgŽDWT dhk3慂Er.2qmpo7@-tD.{4CчoE˔ ߌ}piۋ_U:$2D8#Ң6I@f}%u9EW vghOHrT!hpXvݲ yQ۾ub>{ 8,UEykt3NJoX?XA'^7>4RIc~dUk ?]X}~(rO hhgVFh@ :s-:1F(|4_> p)`a@\Ր5j[I[%6(BLWAԙ'qiVZT^hRq\0%VǶKб0Ly"fs:]+ـ ip)_&K;l;ToD^혥:}pGV1HW0 !Mp9^q%;,;FMhFѝߕ1ȝqwki'}Gw 9lk6fӂi8I.!?64s$+vBߖ ϾI%»YѦB|5{@%{gbJ9[HJ)?gY0رx ,@PtRdE{@I }E'N&P(:@PtRP B{WوˀS(:)T()K#=|@I>P873(:eBut9)E]]92WtS*!gÂu À_PM<4`;'P.&A!T)#g<~Y~Y (:BMnA@GIENDB`unioslo-zabbix-cli-09a2fab/resources/open-autocomplete.png000066400000000000000000002614111471265333400241070ustar00rootroot00000000000000PNG  IHDR sRGB, pHYs%%IR$bIDATx}`T7{̸w 6 {H۴ݷoKJH/CS WfJϹw<3!! [$#;mҤ QPPPPPPPPPPm*⮠ۇ" (⮠;" (⮠;" (⮠;/EcQQQWPPPPPP^4 _GFF^s5-[T]AAAAAWMܳ^(((7➚z-tȑ#۷o?s}B-HHHСCRRRqq?cǎ #L#""xN:M:uժUjNAAAAA %%XPPppp{.]{uֽ+ g֭[w_`̙3/t^3q8qb޽zm۶*((((((&M>uԣG^(((\gC 瀡C^z饟|Ɇ .t^~.3q࿟#Gk?~g}z  E-wujժ  E-qWPOP]AAAAABwE-qWPOP]AAAAABwE-qWPOP]voO;:;N]ۑŋ>g;9]}Y&E:EB" o^JO Q_t niXx=YXm*p~ju=M}}{_4ÿ/h ( sѷ~'o콴3)oRޓw[&]cI$6eh]#5)VI_7>B2ax&ammǴy;H')r2Ub9g;ݑKQCm19 R|P]A>wN ȶ Nao$btNӻޡy_⢦qP()>l#{7tW{2p=!6XwH49j}B7%Oi[*k ??ϬMG8 ׼YbL~0tCZtCw!{6#a~:L?q fRRClUgkkMghNqHr+K:6#dj &yuw3-^/-.BzT/-}uwVɯiq!f"j&9ӵXrwTO`lP]A>ϸ{gO3]FfVKtM_:㏍iw? ]؎e`y|3Q7~h_n`z_ih>5$FVMr̖}V煸A`x۾D7>2_Neqig 6sFBj$aK'?]Gby @>[)T49[ܔ9$/4"5IѬCdRۘIvz•ݼ н&^HN1l6ݕ2Tt2btGqUw]vJf 奝eE9Eي ?EL!tpkc+_-(;K' Vlx++('\`.?͌`KvkY #Dc/~7R? sP!La8V9δ– "@ܧ' DR$-&GS1kNg(:Ir`5ǶH.^+Sc5-Zk N"%2fU%)n0Wﴓ e,0!#u 8QRM3*Bz 7;C# H\04ct*< U e`#yC$6XT36*q0̛ /"{OxD:eXr<]2#,9FDY^%8 jBJ=ñBt&fo@"ŽPNƩc[>G<'[$k.`a@لpdc]twg=&{y֥w$g(ŵH]7؍}HŞrYB5=.ҏ  W }jdO[L.sJZPаlgYO:t~`٧%^_Ov6A.@ٵuB`(⮠PpgsIo`/7GkYf;!ո y4yCod3e2+_w%ډՌ_ۓp"q?U>Z6cdh~]/)7(wa1@7n/^Ud{5NO|mټ7ѼuԿ&tN_mj2 j.i@#ΌLYoLʌ .< a~l&?q׷D |t6s=DahgJ /wJض |Bo?F[ײUkAbB/cLY@|E% ]U'E%3ڣVB2n}{\ֶ+ivg*PwϠ2S\sƛK ׺[#qwUgeT_WmQo>kq((!7[gaMN@\졻NFMĝWjZIA4tڧlw}aP$[َ~s;y2/JR0HA>/⏏_ێ&), vk+Bw LͿ\*{67N,O%a[KɞXFǙ "xFJ`0lyj!y"(i0r2s3ǘapN&rqdFvn, hBn4ַ6?8'kuyi#4u0`wril pW~$ ]*w~Ja7yWs)T0bKD 3od.@_ Js֣|<8(Ϗm,[74D^vs?|9؊33u cԉC_a)g3A` lI} ?!BGS |SqO5Q<8ܮˋ&r"UZE7n헣ᐌ+˻2>>~TB;쌱;ˁ+WӖ' o˵6)WW! rZw⪶eȾ+=z`%ž/H{cL^[5)$/GRSr?yf68BU{ 3'"~{6\Q>?l`n`{h_m`ʹ]l mUEx$Sgj_ƑWqKqY|X[gmanv͘qu:s\+cMv'c_ ) ;н/ֳ-GĽR^n֙ =ڋE s_M-C_;" kzP5B)l;oǾt;bc^VjN5 0`{ySw $mu8)C#3Mۜ le^(&tI-9Z\y?#G׬^ƘNU4ٝ눎evo"tf\lCOUBĩʻv!^c^q8 JmSh[Nۥ[#FƦm>29Bl}߈]3Dn9FG6}|xnM9^8j: R3q}[p lMtcvq4rc)v#O<}JWcsv](o=@bv©y;%qd{yWû˩Օ׉771m΄qdnDJ[vS]n~T 3š' ;?$^ Z3.Rl%`s ptcH|iB4=3oح-I[I4DNI"}1՘NOpV*;#5FrGF#~Ь6g(ёbh[)Ldh2:YFcE')АI}9pwAH8ۏH _ c;٩||O(dI)7њ'zi T\Kp嗣xt$ 'm@"Mňvu2 B6n+R Qel⮙"KO M[%?2ܦ8q8^` juڨ2 1]T-|[մ9՚~ƶd~gviA'Mtd",ڡ?D vq,{>9&Dřqzs B1x~(EBoa#CėQ9y9=d'я因+ϩwh5=u~ͤ!F2~jH_]$Vӆ(ָ59(P޶y{{:T7)4fReL!d|E\0]hd]d[jawيo/PZ묱۶N}Ulw K[8q;-(⮠Pp7~rԘ~<_$GkScy8}~rT[ 1Sk"N!xMڻ+*R##0^^(@ܗf>sQ\Fboߖ,9Z H6c?C Rq̼s"Xd~iiBq*oy6˜"-_CVb)ӛz\y<PF RJo9O]ĹX Ȋ Enۈ~n>*.ʯnYZAQa H, ©c6")38AJ¹}q= &$;n>5Fn蜻]n;yl,vqw+^w ةx\"QNe>PUBr  ُ wQPZRf{^F}&'Fhu',ȼ}{^ Ax,3YmNA&~ ?leۗ/A>9 "uSfzg|b[ATJ2߽A(;bVuviC= ֲ KٯhӈXMyJ#Ca",#|y/qHǸWPɟB'" VMΧ*w삠̭>n8YA65X hnd\8yp ]Mט|=85s삙*⮠Pp;|rp[K'ӽ&?V|&Ȍ嗰}x7KK:{훍콕"c6z`lr,4Lgl#[ˁz*,:6eBZ{gG41 IE'I,#A ;P(mu|5~Z=髉E(};wg" (Kд@܇t+R>0 `ʮ2̶Sm\;R qݚ7-`~؊ba?.Sfju1KIK#wE@#qMe^ӂ;PdnX. ۧ=prcw9yNc?Γa䩬C 0,i8(ά>^:Q;qS :J'g&;k"⍛"f~9h[H 3si 'FGP"pwp 뻲=|s3TjdwX,5;gw']jN<8횶ALF-}o)B@sȦ9N[*({eYbUJq;R$ۧLܧ\mOXRL-euIl/QVП޼dl_X+('\wXA*͙rnHߞwkYxlt|ƒ0O[c'$#'pPGK]V`/̫D+/B%8b2'7Q>-טm[D^oNxqFw]NݎkiЃc $`QێYXwa>"37ډ;ypwdUU %eքk/N}pWBo k;eJ+O@ Jُ['H*0:=3ۼӵv٭J#De FZz2F!>ZJ6(> 39{k)Bhrq~IzK;HI~q[dI=S;q){)n BPS*:|Ǡ3'hg87Ѧ$gbBOqQ3bݪ훍cԊyaS⊹Jc8n#:YS<`Zfmu&ěRY*1cт2Ҷ!U\.O` (3a HJmC^B 5K ;!m.#ί&n@tŝYV9w{(ne}[A1|_M{ 7k z<!-2p5Y7];qeTLҖk𶞇|NOٺ:;}h6B._A([Nm?|]>99!1%A Եg YeWw:b´W=b1:K[a<~kʗР){z6qK?_fݬ˻{2]gK:!HA4 L G|a)vxj%x,c98wk{_O;kA%hj, ^(Óq ׺#g߾%{,{o,'9g9$ʅsc [^OMx[bڜj||T{q@_ ,OjwtL]aȗ}6ۀU뽑8`D5b6!6N+{n6^.i_ε? ~—}qx[H?BqGꈻ^PBozWvI7m.>($l.p@6zX.yd)~b>6 R+lSn6zɻV+LOSX1KanƏ[IyZ ]ü=٥v!>~~y7;&#@^MsXuxJofrt|R~1CqS8Ev6g;Im){%8>t` 85ÍmW!WՕ$T#r^ y;̏b;I_D8;;;PE mлR'#^Zh{"B(h:ٖ#a'EhTA;;Hh_UJ}ĸ/fVAndp[sF=]yϽﳈ$|,LRq!@*kM$VVaA%mZSVw | t2"ۊ :&+|fΐ@|kG [Iy o6ڊ-olIQ[Tdʪ#C2uf:Ƕ"qWPO=TĆɴ vL}Abt>V #Djz¾ϪMht,q1;;k4GrC~rўPZAzSY\RZ]K+Pdz9Kdarùvp0G,v茤˂_ːMqja ƅֽWBlIKL iy I`SXZMUEAwz EqWPPPPPP]A>Awz Eq ))Ž瓒b * p+;+vcumrdYW  I6hQ :"K >1?ź w4!⅁};t, Xz/{" 1VqB(啐ٿNߡclS7S?^+w~:O:O_k \W#H.&$)d#&7Ԍ=Dm_.c~niu=ǍO 8Լ"hNE$Zƒ ^165V$FR%3!;77PӣxB0Z5q4)3NEg]DRTB'E'Ts喐V E>j]IvjPTF&%0ޡ)bP]A>B~,!Bhx~8l;vv!=HBa_tn~-| )g5Cy(ࡀKo-xo 2!6?E{-K$%:'ݛ8V7_Y\K:˛BUWs{kVCn^Z@Yn6e6{35M\ ~>' O 5GkQ݃D2Y,--G請}ܮ!R =l/z631 4gwQCm+n@Pu!'@}CJó }C؎Tr J<TBUBr ɴ^KKpQX8x9>;-2U@N! D֚SOѤ̋74e^y{ 5@i#/ׄ{Co,!\dŹn^{xO]$j#RUpTWQNB}…'lh#߿yeamUNfRw''uxV5(Ƞ6dm*rdGsuPF1'p]c~f43Wki>5,Tv}&>:tXSga W wM<0O=~t$=" ED;TdZ\E16̇G{}6;Ϙ~vZB}…&Z[PdҐ3ٽ xrb(rӤH`t1Ԝ ) JIl.?`ێUNqIةx +78TP*4F#0u!og>E(H.yszK?o2Ic$ea)BO|M{YWIVgHbCp0Rk[X#}|mc}&Dzq6KC'_NR'%&!pI3|R[mvbuzU7- /FL!J#el^8mQHIKrR2<Ϛ㺠.'Y ""Z1*1LKz|/ahu2z2ڋȤ~X,|Vɸ&.:7 08*I]UrE%>D:̤([$3~_+Jo6V;7qWPO=!=D6d$(Hh`-&z C9ˑNlnDaqj1^Ƈȗ*vIְjQd56:OZB}L哗HmEǑ&ڇIRqɦrR_AU2/̕-Pa~ԘKΑWuZl)VtGkqѡB=yOܞ8_Cǹ~.37y3VTp=Vf} x?k@;E hYJ('!K":_ͳ.i4 ='.A$Df6p\ ق]ښ@@7M`6GwK$])jLe6@<.-Gmn|_`a*R&&8w6qGKmt^vCoc*ڴU덾 Eeh1a?eVUףj"tH\*¼OV>a*^{R_b+}s["O.ϸ0[h̡}Nt{ig?km'| eram1i-,`=t2iw4eKRKn7HJZ>I3Pq'Q.rD`ޘL[E J%ޱ`c ǼE V+ؼ<)bډ M-ೳ 0T"]$̠n/oAZlFpocNeܕ?hii}o7 㛍何.T_dh`HE2c!|߾ڻ9.[W ͆QB}&A!Y~ NmFwsHԖes*ih̴)eV6Qn9undЙwWi ?MGGzug6g;oI&qj3_žՄ#ڢ f7gS]3Ip'Bv!gN]ԏnƄ2+yP]ecw--"0/ӽ'2 * ZP;O vXzw.zln5M#|FRqiD!ݜri"Ek|UOUݙ[,nF [d'6 #{NҾ- r9Q ~V5D7}x{u!V&K-(ew}HJ+DGq}0lj<8;9icHV5)6Mo`^ݝuHa`@}X/gaCj'zڦpK1[s#ܜ*F;)S.(ג=:5S#Q}ͭEN?GX.Jn+~b٫/2R_JSbrYlB͉"5e9E~腀Yz?N屶lw M0]0Jt>'^ZCN lwL %#EWؠsBroc^sR!g(٪Yjw_ +('\hMYW2GR[OCXPȖZ&\$]jO>kr?qS[ ir~Z,gjOwN(?U8gmᏌ0ń?g4(vWbT{O֒u웤wWt3]f-N^( &`6T h,=XtLoθO:z?r -ͳyl:?q7d?K9ʃoLN <'sl׈l;jsߙ$c&5US/Γ+&n;pk^wOZgX &E+vj(*Ѡ50:6g;</т,SN v~ `-OܢІ&؜l!O`ҐI}]hNn-v~g+ .<3G;p~cA E3W khrbl99娽ͭ0-nͯҟN lŇSРާKYBbE# Efu6;E 6 [WVAvj UGbqP]A>B0 ے WieЍ;^Chd#qG?X%P#qRIEn>1ZlWfYS+A߂Q#qD**Ͱ<-㏡'VRb;>5̸NVe׾h>5Vu7&k$z77{7'Q.—ب}BW-OǽR͆FF;K_[d>s \:ZY)9SjT$+!&sĽʬdyH3rd~`E B _-z4;Nw|PQ0Pa=*=& {{m` V>:ڎskӵ=oYdqѢO%ڜP6(~n/O4Ok/-q3Ŷ\\Fn'&)'ܟU'{4;-Sc\'d{}-{ۆ 31B} vO=8TإPE,mEїZ kAS@YpLuMiM$0ɰrKSfT0ϒ Cɞ3شfGG ?n5 ٺ]W0( W`EW-6M#UGWF?Xz[E{Z[z6/E.4q\{>##=U(铽'SǓM$v1=+gh;3q\`E`<1:r +\#wfFED\j,^K;tkQ"'9^@ғp"l=gq>-ώC]aD\j?+at'6EN}Fl!^Ղ7 $pI}e$߄zp>]g/!-\y8m~B>\/y;+@<&Dc12*#Qض I샕Ho|2٭)Δq{rNܽm+'[9Q 7<`>2rJ[1c4Ĝ,`sh}M[JGΡ[G^]vg֚)9u.g  +Z[y@\՝jا=HX5H Ԡz$6yܜҞBZJ,njȬcXğ+iږ 2Re*D ԯBD:k+rY5NiS((n֗k&ڜz!B}oG:śy Rdz6iPR޼?ٝ8YmĚ,d36h?nDϊ~#efsjo!e, i`޳2ذ5']ӈC;n~H0nzt1IՁ.B k'.&KVu Z*k+?M-^͆ѓgn\R)nͯC?ӵuDTEB}oTM&vICaJܸ1D=% Ms%+.D_o9B*9;Igq!ܺ: i#u~06fȿf%QaBǑ7#ęGi)ֈ{90lLGfE/0p6OgsX8 .a q^^K#+>Pi~|;B x@{c T]]K x)aflĝϓcVx'[<6$8ח ۟ q0UGO9N刮`6I>)zp<;6$R"Cyf?LHPO@!u|9]=8wk8HǸW|LGڢ-J zWw^AXJRNċ;)`pRbm]}f6ov[Sv Dj~2;xJc#O&5I*@gogE锊.AK/^YDg_ƙR}B{W#NYVa3Fw1wII-ϾVyz/q?nPˋS^3=p 7žpHB|ĽR@_drL+FpnBσ\? +0 *dХ{}z`Ftm;\56eNKceEJ܎b_sc 5k)?$D:ufZR%,9q0T?'e /535ddqE7f>Qfn¢S#G>8Uy&E4ӓTXHs&vCV6ʂ[ά~ŶB"bC d\Ԍ7{*]t8)rd{Ccfnf{N-p<=n9kq.6ރsR_8NƄ&}-*n@HJZK7$%utSfbLG٣)2~4BA>fY"؍25s7ɾ:hnA+?YlNQI:HpV⾡"Y5hL.Ft\U N^@Q8*] _ e_5+e&ե 8c^Qӓǃ,.תsR5B}ot0)&#{I(NCq1jCNa\,* %0sHkJᣆF;Já^7^CwbB 6J,YT >Oabj{B[^*MzRAPQPRD˥~& MK KI ͬw>3I2#)W{֙}ȟܑೆwLc/Cd >h ]J݈H}K?dV+KQ[0&LJpɴ}݇>=c9R%y$Q&"CȜ6GT[vyxhTCxS9ۥA ?B㹎;̙ _CB,)# E<+qw(!le/ct}DZ>̭ӇK g˳% ų% EL 0ßq>]P Y_ܾ?<ԅ.Jg J!Y3;R$Nw Þ=  % ų% BPVP( E @]Ο?رcpK//Zȇ* B.ҥK׻w?iY cHH4)oܸÑ BPD M6mٲ_~y'-B|LA7n|__P( $N8 tUV]hь3D 7=qO(Q…yTR]zUBP(Bݻ`K 3w(Q"_|ӧO$W( BnݺuҥӧO۷ʕ+OZB3 qW( BP(wBP( "@BP( B]P( BPP( BP(qW( BP(+ BP(J BP(x% BP( E<wBP( "@BP( B]P( BP)6ly+WD܌3֮][>ׯt钯=^xᅂ 3gܹ324hpɽ{>F=ʗ/"E={\p Qd,Y̱ u͛7M4of˗/Jx̙3WPaǎ.rJJ ׵k׶o~֭'-BPD% ޽s=m۶Oś|70[x.]! ɓ'SNpp0þ0 >@N>=fbO~رh;wnX#'FI;SLgϦ{矏c~;w~XXիWᇠ  邗_~SN޽{4Û6mZj/6N0v9 :vDZ9Ҷ։/B-5ke˖#GFW01믿N6-zI!9`'OP>>|8}ĉ4~]fO B!R4i.wj'OYz{yj'88C K.1cƌ˗%K{5_Sܹs +-K>=$ a„l?vTdI肎2#36-q91ɓe{HHݻw3sY9 ya@sιw1ged*">l'F)7I17}ҸrU\޽{608^8 nUrhV18/^_uu! {eF4;[tr:j8qcLi9G l뭷r!L& 6(EP؜(d vCܱ kul/@TRMN7ox1<<\q\+[G@p޺u+,ƒ=f͚ϟ_>cŋ[)SQ 9 44HDxj0j9apilT}vYU #09=zիǿx#eoRxⲗs疽2n-R.̹r9}\^ 6m!"\M6 ?GTCeyt*U`Ͱf͚hY*2@kZhQ@ʅ q{…VTf 8%^δ`k(˟$hѢ(aÆUVY)l ,޶m۬Y7.q>.;IA`׮] =q_v#G0%^g"'3# @`gqOqcR"1͛WT7`bmܹo݅s6mX $%GEbIc\ =Z d"H׍*\d!<5q8uLؒ; J q"Ή;FŢ(gJϘ1W^T2/dJH(eZpGl׮Μ9- /ZlϽ{2dB Y5kV*0>ѣA<wTtQ/Ee9Q;>ٳ3Eʞyx)+Bz:Էo;6w 7x`.'J̙3+O<;vd_p 0H̄ ̽m/֯_~љ1ap|@f̨Lf&*3/gAd!L8TCC~mb8+{R8{;u"۷BBhEo|3[h`v:81< g;tA;*^|EPhY*2Z;|7c~F_fc{} /8|K]v ;;v ʕc;sK@ PNܻw^jU+q@?#m9喪;qׯM_Ğ3\qo&`Qb!adO>Ě"#1#](|2h1 ;By˖-uC, a0 $AnH=AGkuvbbVX-h\۶mK>|o<_G6e- ʈ͹| GP<͈stRF~aƍɛAAABM-QīJ_*?~ԩS " //3 r!yX4ˬ q/TW` r5\+N86bwM9r`pth vXO?<аq46' g]ρ[\",!XͰ.g1LIܡtƍap`g̘3=q1֭[sſ4x8!P:ut)}٦\|5}y!t>&hq'Nѕ#<wz ܃k1!9Zj̶ "[ ‘E1d]$8ɑ#G:vH]\^T]P(~<^pAQ~ON5mt۶m~-H;uf/K?)uu%R9E1}Ν]RݷS6 bV}aUǎG^[7ɢCf{' k׺LͰ3Ցw)<ʖʕ+#ի2G .@i<3NJ:5+T.Tn@L,xa-%$Xt=Mxɀ2f(hիb fnùb/$2A5̙a(!Hſ~C@YlY ޾d.nM&msȈ;:t@d`Bʕ+Gt[̭p/+C."4$GʣBKщwI_rve.#mPMC( q'C22oR8DԲeLŋb̂͛W^}Ȑ!mJ1i'K۷od}L;YfUS]Pk<L4iR5Iٲe[p|.}$S9Ѯ];*UpVpp0XCrܯr.grq]'ZQY̛7СC|Ikժ"E sG|r ,:JDF)/ɓe-Y۷>Ȃwf yr8g"ĘwhP^;ǂTAyǹ.PI_`ϸ-{Nv[0,Wn]t^t֍ʮB&##=qb]e<:"#X^_[u1.ρ_tI4D(ioܸ!l=q'}Y}ƻo+vҗN D]:\r_+}˗kYRl=qoР=h>ͬa5kCHGm +xDܥ4n۶mŤiRd̓w2hPɺv ˵\B1sy\1;[n|@=9p ըQٳr? t~SU׭DȈ  [qw8ZAY•0$q kӦTOa~E;:rzbGp;Gdwؕy!q'" qO:1r vt"st&6ٟu" d݄@NիB%T5ժUㆄ?^|GT+|:H+qDžKK撾wT)-ԙ̝;7^pkɢ(%\Œ7iҤCE1ddɒ1EҤIW\O?]Pk<&y%;Cܛ6m qܲM%^xF"ᅸEH{` /ܹlzjwm.5 2d-[n땅ʲCg0GɃK(cY ۬L'O,TP%K̙3f|*`u T<-[6 'Jk5J0!lv<ݓD I<"ZqXݲ{/u<TkV?}РA/ Wou;ĚÙEv6mZ+%`ZJT.ҳ܃i{.gQ g}&M8}⎐T a?Ȣv<EL:u EH>.o px%k.K,|8p3BxlxL=Yd=۾}{K}hh($ѣEʓd*UPoN{*\rAn|_dB^tiy6N\@T (˖- LSZD3rgu,0'2'H'cr #r-X;d(.A.x$>r?#qĘ~."P4882~, jLe$0aBlҥKHҐ;kc:ujՊAN `S{9T=ȫ.]La%|qYfΝ Z>hѢ0E|ENձcyYUb Kw|ӑl.w^n]f?<$$zM&$Q 3Z8&%pf YK' i2m.3p*SJ<^~eҝMn"ܬ HE̺x 9}t%)BFz*YVɕL2IM%K+.~Uɑ;@0LJ0uT!swmJ*[} |뭷R"Tݹʜ9oM&f/vܹQF۷O0\6_]Z4iR~}/QQC${~3۰FYsEeH*<38`.ʔXG;##12ZFBkG##L)qfB6.Ș0œ'O.^>/+ L:uwѱ퀤!^k%1>z(h~;.c6k #B_x!$ 2N&ĚԩC^+Z<#k;y•O0?VEH*Ym|/b׮]V~\- KE`&>aĈ= ǰ=[llhժsM$(I&,/"SP( |IܽZj-ZPe_ q[%J8~[0yBH6 gΜoܡCl"}֬Y2DQϋJiR)w@gt~<*gɒCVe7WD5;OdɒA  m 2̝;wp._~]b)r?X>:?ƍC*U*:OF_BHk\+Z;b1Ⱥ Blqw I µ.]?[Z{\KJܟ,~*U"{9♇!T?qEIsP(qw8<<ɓ pY(GwTWQdɒ5l0C - y(Ν{)ĝLe˖ 6ĩ$J ) ߸q?tA 𸿷Y*(P`ƍ EtAҥUVYx" .\0-R(goBP( B1jnޅ1xBP( BO]P( BPP( BP(qW( BP(+ BP(J BP(xw *;v,6͖-[2eҥKwΝ}Ͽ+qW( BP(%I&ڵk@@Ç .DwJ*ժU+k֬ɓ'w޵kBCC[n)qW( BP(%%jذaժU3ggϞu֝>}:((-ZPBppJ*#G{~ӧOrwBP( ?Y/'e˖k׮ڵkQ>}z3g,Yt%SLaÆy9QBP( ?:uʕ+.]D }0`S&MZhђ%KʕcK4iܹ/8p+ BP(91qϒ%K```"E5jM6ARL?y7o޼ugA߅{'J BP(ʕjժE'JرcVڻwS6m^z*Thʕ6l޾}9s*qW( BP( bK&LX~MmCƣm۶m̘1~i\|'ӈk׮a)Z'-"ڰOwpZjŊz,Y227nܘ;wnHH۴ibŊx͛7/P|JA0aB'/_~f͚eȐz;v̠Zjɒ%;9}tٳg "7kNЋf͚D{K128|XXرc/^ܮ]^x!ƣܹK(C!6?ܹswɇw9ر}'OP!6}ڵk*]vEKoaE  "7xVc%v=^&qpBuֵO߿ ڴn=J;~o߾dɒf͚)RӧO0/^͛G^lw "7Cs>{ݻw%_|)Rp9*)S̟?Μ9e qu։''o޼ ) $G?~2A5M:l`??ٳK*w8Ϙ1c,Y\'O d(ⅸs1\X1?cp!ϟGG*@qH,9=((($$޽{]`4&2Ȇ0èFL.`Ġe˖ͪԩS2edUJf'N:_paeb߯_jժ5h@?Mm۶ӧ.]'R 1;q9r]tʝ I^xD9sf>󭉂r jQ&Me xM{'s9Of'.{.مve,ҦMkNg˱cDQ hAp18aK'm(DM$ [l0J0S:I`|f}`2#+' %AĝgtojYq-[WB[)Nr۷oJSL'~m+:B%8}4%>qW_}}v * TF7|矻H6lԨQz$ԭ7~NF ŧ~Z\9Q9 7o% 6l5k~'j3{?#[?}LZZhƷZ:c Cp.0xY:yd޽{z8w˖-3V (TC:iӆ}޽{dj9ܰ)JOw<ҤI0"bŊ x>xKɒ%_u%C\xq2eڶm9sܱo!ON$c08ጩ'Ҩn۷~!7o:u*mdeG⎨{!$q]d&ipC;$HA):R cɒ HSvm4*.E6 僌Tω(!a͑ œw JwASN:-Zy;2GcqѦM>-ZT%֚5kF[ܚ5k:,ĝqe˖%/ĝ1ج_~t q}\LV˖-7o;9+[q\M5j1(uTK.ւ5Hi!}5oFPuTO~Gzz;̀]oܫH=|p׮]k׮<:3m4t_.ᑸ/X`Ŋ8sOC'O[{)S@$ʣd3B;T{СV+J -Q_7aHD;6 HhJ'ÿ43`TPP/AA?H'Ɲ+7hSyM̋}꫋/J<_91t̐!*ۿM⎟08‘RAP<)(q $0u9GDD:tLMݭ\2ǎ+B)]7o^(u/=?'o{yWx77nwRV!}UH-Z<%<\|ya0lTۻw/ '^x1%|ĉ?ʕe/z/ĝ zAĦ5^ .,n3‘#GΞ= ۦӔmiӦރȹ])έ[!&M8~ڵaRdx ;O 裏p$Z;K sðvܛ?7{!{ Ϧ]+1̄,V&MKc|q80R #>ӧL$#q٠ad nπtˈ9h"!5ٺ'ٳ:u9s3F c(Ί.L&tD:Mk~vX$9ٽ{ɓά"J7.%M"!3d0N5^;$ItJ2ebެ {.4'$9s=zr8ԨYvt:js.?7x Bo%qIy(׏O1bt!?seٲe^{d` Pazڴi/5kV^JLJ!zJr.#Kx${޵kWÆ ]͞=;# -aaaQF 5{j:t0#><`0VTPAk׮x6WGYf̝;!BݕbŊݷ޽{wsQ;=AJ5wR˪uw5?aBBBۃ␲]q?x|`G{jբ'wFӉqHf?íZC¯JhNH LsDٳa:v,VlGe6 q_~=JYPO!5km|`Ez=x Af << f@D5kFVr?W"'Cs_]tBܻufDBܯ]\ŋɒ%cLw;VW; Ȳp!3/^|8̗_~G.~0 S%A]V2DdDDe~ƍBܩ.cǎ=rwB2eJ|L;{] uu aaSLz*b$I)rD߬?֭[۴iSNq+WxwРAcR'Nj`>#5&M2e쥆!u}*U?*C@0z̘QZ~}9رc)%k} 2ܹstWi}Bx PDIܿ+u8$A?.ĝ4gΜ_պ [em֧OjʻɼK.QΞ=kKk޿7lݲիW`ܻw/C o}=´i(uڭ8c $}+;YM}z|={d/~ K@ >ZDO@~0((hرΞj,YnKL);ĝ^zEB}֬Yׯ7!%aW9sR'O\Zɐַ²nw^awVڤILCI;i9~{+Wh-7lWȒO>lU 0_E'wz#G^~/0Ԓ'Mćth]TP<(q $cǎ={6d""E a{l,6eʔ\r/Xr+KS\>dy~ᕸ;@^E"|˗/ٳgwΌ5#Gd(Э[~TΝ;Q!QD4/ʥa%K7ڵ 5L'1ݻwvB&ֽԒ7W\14ǪU`h5a e̘1 f=W^UswHQwan7n܀\ 6,R<z7J.}]<$D=ѣ9K{!Dp``  o0Z% "#VAiayO,< 7ՑѸȉJ"ݝt/Ç' ԹG:2(n:y4Js9@˖-#cĬ<DDwS)ܕ-[Rv;o[7o6hϙ3/_&ܺuSǎmhU&MڴM/ˇ{Q鯰B,Yle跮7vލQ$h׬Y3gΜx)-UT1o%vX{tt\v.P('Q#G1òyΝ'OBCe˦MJ H.7:ʚl!͓"H(^Z5H@ȉ;w^*t $n(@:tL2of֬Yֶm[ʤ BP~ma!)6 qѢE!(ua{'O*t 5ߵ{fNQp$;$LȉW\Awf=xM({i _֭[ߴ|3mkQ김wR=z\ewB,ܹsaH+8+Nx'>XIWm66S" Y""py}waY&6݈{ŊYfXX\?Q;W"Q(,e5k-^#߿Ix;$v6qGn;|Zz2Zl#G-\e 'N: 7n,W$\D"С,Z!KLɓ 41E׮pt/q"=e])!9s0P<wz 6@p|Y*9BF_s)]j_qߺe[\rouɒ%۵kJ!Ph. 6mZ!KqAΤIQͻz]xʡ7SBCC͘1#i5,Pi0j F_TS*U$f)AAA.^d n޼D\pa$U|JՁp0v?0_R8ÍΞ;ljN5$$.N1PV)2ebE,LMN4?'q )b&5eMLۀ`7W'%rQ9ysUĊR˹Y3#[xq#*^\9;)x'Q@ʕȥP(q֬Yi&{)cǎ+Vm۶U+ XzÇCCCFDc P(P(qVZ5v̘+W:_֭[uQV)8 1P؄?> A+-W.thA~)cƌ˗p ų %ݻBP BwP(qW( (qW( S %wـwBP}bc:th)rm<ŋAAA%J%Omۆy ,eŋJi ⚸.\ȑ#޽{Q^AN8ydΜ9{ܟP1efǃaG9͛!!!bI]l,0 Ut .HM+qMSc.sΥm`ۯ/֮];p@˩q RϞ=[j 5j(\g}fXjС n~ 's^@^ݻwGYfA{i_N}W^z%7nXhݾu :txb<ȨԸƹs>\tio|:1Zn]޽{'-)OM47n}]zx֩SǷډRYdGo@1N4iҩSDkr{ځDqNW\#""bӦM1Rzm|7xV;Qܽ{_~ GɵH?S&#6o!._ω+WZl ݄pظƒ%Kh}޾}=zț7qy&~9sf۶mqhN>>rq~/VΝ;7%qF\4Z'O9qj֬ICd0q18}ƍt _>=EG'vpqZի?='ʎGJ5l%J ;vӈ{|XJ ',YppJ6R`?O&vA{ws R.%q_pIbUTl^4'(q $7oܶmܹspJtyyּyƍݻL@ 4$r!vڼys$IZj"E ߿Xbcǎʕ+g'Glذ!88/{ :$H.2ҥKիg_d=,,luwp-]49eˎ=g =wí*00dɒOӦMQ8r躵kCCC\xqFfF.z%&ٳ+WE^P!9ֵb *n9a«W.Sqԩ~MF˔)II6Լyb3U庢{R+/?QsQk֬9x '^~gxb_{5?)呸s9D:tg*s!ugfdɓ͛H"rʛoigGŎ+3ϖe˚eڵk'<lLD 8pHHȖ-[p\vژR|ի{n9s[wIujfg*֭[ڡ5{q3:+Bܹ"|50 M͓'vf E% Ǐǎ(ii,Ugf|WE3lsifC\Tҧ?bw$"wًw\+_|ȏX'!Q ؑ#Gș OX" +33Q۷O"͗(x֭[X "-۹4Ir)>}4#RFed mlG⎎H>x s̸uuk׮QrEL(I$I>x|#`y7&?a8!|ؑÇ !Z9^ncjHh#~ Glqxb/ZILǴ-sSv抴oNL~SܵEu7ZM<FE`үi>ChGێG^bGa;hM ۷oDkQ%m-Pch' B XǚHEQ9",Xp-B $)S+GxyVСW4 G!>As?398q%KpY!)|w<D;ԪQFm+oݢ >.Tʏ?sʗG.MHٓ믿vvqy'Sw޼/xƉ'x5q8F\ 0}ʔ)Yd ב e[ϖ_|ȟo[G9s:Gpe 3=C͖5+D"qtoe/3/tYΰ?g DlDCe/%JaRȑp\,“AsfޤIZx$paÆCIwIb/^0s=zf?m4!ED2޳cU;H?M׻woRIH>}LǿE|=c\l`ȌN>fExߢE Uqg9Yr%SN ;uMpw-<~ܹ7m|%X?QN;5: 7!0!-[`qޘ]_lH܉>:m۷ߋ ØWriժUTnbBt`C9wʕ~ ,P d0"U&n)J :t`|)2p"%7Y)m D]F.5o^Z5u: JQ `6.~uihd{xXK0'`Ҥ1…OQllG2׬Y믿LjGȭAYIgF~ĝaaÆ9n7r۶5m֌Gj֬:a(%,uh߾`B#Fp8o^(&e_,edp|XR!_Up{ Nv*L\߸!b^ (ޱcftDoGL$J=_䠯񅸳Wp|;Z;"!$)KAaNB5"ML렘-1vtQ$i?L ܒo΅1ZR믿N)sr""a 䐬ha$2'NbhmڴT%MÕшwP1bMx_oF#\R7|SB#\R)eԩX L.w&sZ2D c᯾J&-F1[*R((Z<>pޤgWUq{&V~}\ uԉEX.]%K¤I%1"Z!A-??3L{z{7c/AFuM<4ģ6FfG %;d?#0};z2w+XVbE:NH!T (ń92$4Ts;y$H𒢅#wוKl Q8AGfh׮Gs 6׸cKzJ*v%@S , 8q͛g͞w4Emr/"&a_6hv; B!J;nCI(6i\#qAt"ߠ=xWXu!d.ϝ;",VnQe4ٻw^: Ȋ]adNgRe1G! &OoA)d=Ɇ-R-{R7#ݕv3.f/|]n\vK04>\\xqƌGiG?nݺInJP3#Hu8лTiҤ^ڸ='@g2tqSBx ֻwo>H hP /ބ*U[Ke|9ъ;A#8  PZh!ɐxealTz%:a_9S&*z;-}8 R9dqF6'x5Pti.ǒrKPx+ۼ.$wHtKeBi\Gqp]|׮]TQwW‡ʸ72| qNQMVtp!,ZǕ$1#Q)N}!UVv|q]L}5h-hGfG:˗/]| 44Ծ#JNHdNeBĐ%H"LgkKBQ% ձ@Sh"@o%qA#_ [nA;q,Ft!pzNA7{!I'vB&r>ݻw;^S#!w7o-[6iFn.ᑸu_>00,ܤ$~R@A.[&nܸ#,N60\f<G6 d~OSLsqϒ93{ NgU Uc[ObҥKR4HӦMwWˍb,^scyB9r<'7$\Nġ.=f=J;2?FZܱ`>q.~L~ tFbGg;[̗QqDܥ2Ҳ~t5$Wѷ(.N+3d@(PyuѤ;+ 'f>i(C9 E`B` IWNHZG0yap+dԱc V@J-T`˖-8u/,薺7]70v$7ܶl֬(ȢǢqE)/w8Q"wKQQ#dU]]xϔZqs(8uTa㜤/11#Qڑ~)HB0Ġ0o940!\OxN!([₸7Zd :~8 C1Bܙ^.u 5Y<$vQ{2%g]&tҴE6IP$|(HNxw]B\K+#,c%Aĝ1bR"'zdOBA|N8BܡS0˗ׯ__7=E业fRn]|f_$ku޽xg2ejٲe<2tPH E۷oH_B?b$(?8"R'@̚4uij=q/Q8% F)B"%REG&^hwcիGGwRA˄"aQ(4 AČGiPR*,d֭>|x z8\43.ʂQ;-v{w~$nsJ&sB)cسgOʩ|AN"nnC+Tq'?PB87PǨdt\rgN20ZHH񐠠 ?6"=f͚qdP7b>^DM1c2TQ9(0x(ս{wD);ωF2bvڡ+ *3#qe>xNr-h9矧pQ*ݎ'MDQ߿?C 8!NeK\wѣaAe'O\`];:w11w&qw<#VF֎%qB`/okp'2!ϔ26[(EGd W,E|\Rz"+򥄉:k[J}(;jn0*=88vyYwVԩ1bhQ/[?, "l\TjJtZp{\3x+WU%/_?>)LX߼GoرD  i B5ǷO q/Y ĝɼއP$aBrkb/j`pwJ q&RVOiɗ}$&D={;n37Ϙ>TUIT-Z;w/LvLPowoĆ4H\H }7FIrؘCժ]3c=R4!MLXレYwx%;YP}2+#E% '2f4xU8GR$0a֯׭bMBTF*&+!IR~O!.8b?5ĝ]BQGٽv%y@e+qw80-qR!"Jgl cǎ$F{u.DF<„<.tOхG2d"!!!sZx1 ק%O͓dߟ{M^!İ(E!<%z+V@qXK1qg@h#66\޺uKĖٳgK3J1J$d(Ej0$;],!!sJnp :@! 2={@GYS.RYc 0vtQjGO:՜nذK.Iҟkˋk~ڵk;JN,˫9-ddRӊ@s8},O~^oB؋lҤ(BӦMKk."ť?vy8E施w.O&ѦM'H'M0s%5|%L0bW)_xq *H]&~7gO&֦͛;(º;vQl^gϞS|ᅸsḻQ7vfXG}D_7^!\iwۉ)Sy\'@)N:.Jy#84p̙,Y2}eB2! 9EB*"{xx_~PX1Dr};F qNVq I3f̹s$A=4̙d/#KNNrȈ#g _'W⎸q Sp>-ru&_).qxjLV䅸;3Tռys|t…GB ֚<1 x0B>}X ޝ02ZBSiOYH 4jȑ#p&( w8۟?$ y"{ - (+Qė.^?+f͞=nX81 H]BA(Ks*UJȄKՑ Aƍ#‡"uЦFxwpr@@7o hḱ$q]46Jg^{:g}!a#qt 1G qrt @soAe!U_\EglBPS\*C_Os[̄t2ԬQcepyu~3o eq$Á j(^ʵXgV3g~䈍ۼJs B:9qϛ7GL0>K-W699:i&]n]I]qQuw`&AgqS!1eYk| EO<#dS^ g(#*dq>COcoz@XXfH>cLʖ%k۷%iAY.ӄ>"ZYG aGݸ_}󟛅Ihsb':t! e8:UD;e8#Gʒ9HejQmRd!#%IL˗bCA p6*]:e-a,Vu~|| M6ܹ&֭[R~d͚޽{2g ?ӄnj3d>K=aѠaCoڔAhh \pa֭׮_X~qN#111JqU=w}pq}Qh-w:40 @:Eijt9wΝu "===ݾICTʕ+tXԡ!1:::99YGU!#]]p7(sJ-[6fF.{{_3CISp._SM糖BCCÒlF8wPz{=]C&ikh<]CCCC#Bw@w khhhhYhYGjՒ:Ƕ/mNCrrrBB|Qk!wY#-i. Wl TCw qOHH"t%%%cEž ={vF4]tE1n<ѷoߦ9r//u#0͛Wpp׮]kժUǎ9Hf$wޥS}Z|kZnqe̘1_|Z.S͛7S2MSqqq\6bSիWi`2d(D=jfE_(4 /PP!eN`8컌EZU!򤂫00OOOٲe}($q2Ξ=ڵkʕk׮qylh2^6]>cHQ4Q{0a gҥƊhZMU W_}ꫯGI"6&iNPE!%/S3.K19e@B~3MH(6RDJ{SsZiBl2QCC#@w%q'/[ܹso,`S!~CK,rJEU)9ʔ)Cy7mСCDuݺu;uHƍn`S9ݡCu9cƌB #E'"DueE~/… \<Ə/R- ^Dݻ.kcDŽwy92AW._)X"~iѢE_~%88vϟ?qM @^W.] N7nr!9$D|uԡ_^t)Ev#"-Zr7.@$q_jU@` l{޽{+H]|,VΝ;5x)zytL*U0+WXI>U=,/_;nwQGDD0|T;BmZ*$$HH/FU۶m8Y{ժUb.e?6o%Eg ^{5u~;|"͚5P˚-;U#y׮] Yz53K>B*V໪S"('ѣαi&eڥZ<saÆRzEƝ9);#1qnG4iRlY9¿\^~},M% 6Í"K.]ھBMF&K~ҥ qJ<5\Hh"ٳg̙,XN #f(>x`)-\u'k֬!VMii}Сm3+{U҉۷&۴iCt @$ (dHUB?NK,֯xz{{K@w0>ϵjvĉНzu6h" ͔)S~O[ose%:JVλLh$_r \cڵ|8Nݹc1Cm.RmJ!IC .w#}t?o53lzH 6掬s!|ٳgǎP~6A$R{54,, VZ5Jh}ݴ -^85Uvq&ްas ɟ??bX)5cft !4Mi:Rd$-,Y R[``t CX>9RfZA@IIyY,Nrv˼8rګYr!ӧG`O/ݕ*WDFF!_}իdI&# 2)PD:%i9:%bBP}j&*[\5ƍQ&1@rBxT /mGBh:H_G/upB 䶅GNi㨡F{޽cI QJ*-[ #F3t>o^T))5Oׯ_?`<}r={"w}G$>|8PmӧAXhQʳD+\IqCt믿?kFJua&]E{8ObeQ q߰~} /aCgtq$lp6ӵkWrO5 G0C_P^ Aܶv!ݻwX6L: &4>%d֭3OT0H ".^8 I80uYVNW͗/_nĘj0Nָ;r-|Nh; 6:D*əB Ϋ e| ~~~}(7\2 a";Tָ3r](PjIZ֬YcO?>D|/3e|MqnDGCr1QCCCw˩7n܀Cpgώ;^z-[|'-[3g̘zj!|ݾc-'ǎmР"t16M p°aUbx Xpbؤn'ĝAK*e wd8?sW+,Bqk\'O얅7(_N9Wȣwϕ+ef7gΒ?7fQq4C\.;P|޳Q54484qw\ׯOoߞ/_>@ """,6lؐ؀Åc}׮]/+ |6l}#q!ca 㪧~;8_M-\xͬYt=hOZF j޼9wܸq㢣 Ʃ2t "]&Ru !3e%aw)>@q{q{.#]3gٝt%q T[k7if 3}̙OׯҥK'M"}ZbKe}{ሐ6)ruEJoݺHkܛ5kk$|{añEd~,K^:~$E/pbf SwwI'.(DJ>cԡ#%!W KmxZ!Z܂B̑]tH"$6JqgmFdS3 ZQq4CNҧO㨡̡{^Fą q6mAeԨQ>]vAQ qӐ!|[l&ڗz;HEH`yTp52-[W|2oK)6p!m/^0-ҳ'c\ʫpe˖]pRșMUZm=t}f\ %2ϟ??vX_ XYĢqMW FΚ%Kb9qg3%?~\;ꔽBd1 WZEiaO1!cdAU_xYZg$sD~|6mtB?[}&Ԡ{{{ժU3f̸}Yf6BWT)8blG\ei޼y?;!&`ufʔPC1aQ^z)k֬L9Rݡ'* XŊ Tչsgub}yj ?~<>° qϟ?t J teP"Mɦ"\r1K.F!E6.OeR9bN 'O\OI>MlBT(X'$$=\zn>|rܸFDTRFBTnٲO|4Whȵ*X/Ν;*mOifg'ĝ(.]&"Sw9޽`Ė.+ԉ'J`]FX⮺L$.ΕN,^b ףl;d.3 G⌆s8'h/(TP}6Kց@7nܨQ%KO)>Nuvک7ƍGHՆ 䛈0m#Zy:6 hAx!Gz޸Mщ'J {%JAF-N($ʽպ,ur{nB.o\R첈-]6m;wfȘN̟3gbz+/7h@}?EA|q)]nԸس{;|)QqG7teN-L?~߾}pe1#09rㇻcsr3JCR8t0y{ʕ[װ4x$ Q$󌈈뼞T`{4.(99:^RKԠcMI??f, D-J"Ccxa&?rݹkWuBz Q!Jh4.3[6LEJ(Tז,YҾSHKLyinx|OM2ȴR)bH|0($[֬qXw'M(oNWq!3M`ժUS.\#7Zi~M4444qwLngaMP}Cٽ 3w9%|=)Gf>zpe~?-`/77=R%N V:o&p/%= Qc" lFwNŇaZ. 3jy:Aw:2B!'=YG9u(5QR8/Mf͚!ȷQStk8jhh@{0'ڐƳ&"ZfB u]7"!!agϞutBժU;t萺]vֽ4qx|ܼyo!xxx$&&̷hтI q'h⮡BwYwBB\\gɒIF&ۉݻw ,6R#G\r] 9Nbbb<==}d4222[l?A&ۉѣGgϞ+MR #66Cp`ȧNN ZfMZ^OꫯjXn{$>校ׂ&ۉ={ƍW^qT kn;wZn]|ѣGZ?ƍ /'N̟?{ 8I8pҥK S&&Ok<}$ódɒ{ծ]љ?>4qh!D_z5!!*UO<zHH q%KFFF'۷ ,x̙7n͛׆dܺuҥK/B"E8-{UV͝;~Η/_ҥ)t'..bŊ*\r-Zz2fmlwޥ9޽{F*VXsq.T߿zESʕ+R ]FɅ FF^zImiF#"">}z\r*TȨq.Tn9rUi͛${Ii &ѣGժUc`9]tɐ!*ŀ9QoάtJC 1OL̔)f|IIILhr'VzK(&0QM/^\7#6|ETTU3&&vXg ϟ_#P1;v WpV}ʯ],TҷAgtɓLa3(Q8ǑerKyKZiDz(#Wxhz4hȸ 'dΜ٥<]tƸ0@ eP1F6b1lsƞqBE*k9s~T.qŲeֺukY;rȌ3k/z. U}Rף JM<=$'R9x`:l;wN|+yם;wN>=/fʜNe,͛ױcv)I'CiԨرcCwt 6 [lٝ۷soE˖-6lhN)S0M+Vݻ\Rnݻ+JrJ|ȑ#ljPիWMA҄dl۶-@87cQ9ٯ_?3b6l@ϦS͚53СC@(wN!@5kD?.]BtJ*ݺu; ,i9M8qƈML{8m\.5O0A~դ h&Q>2s9vNdqEAp#^F84h ` GW]nݷzK]r٠^z)kijթSrq$o("Q544@w%qGp%K=D8Ǐƌ 9s&aUݺI);,4tĉ'7o>_IHv 3IL!M; xvSBq/pիSϺuHŇ3 o%koEJi#su`SN%iq$e[P5k{РA/_1cp2)F<_ƌ5[#F8/"?qĦMz)p( `R!xt&ĄGܬݺ x I~7Gz7!ṱqF)>OV{OVZGι_FfXd8 4Yd r4fjDaT.rػqq5;fL8;vBskצEH6Ba|څc 2$sI8ZjE[\գGɆXO0VZоUVuСO>R q?s վ;1B)pw0*H_յ8kΔ&k`bcc 6\լY34N˯\!!!b׉UtsNh;G 7}|*U[u"RKnKXf?ޞ֭)STPx+fϞm?:$4F3H(D) qvǖ-Z@^4mڔS (\JK(K5p#fl L!3Mh=H˖-lݡe3-Z+eCO/'c4<JEnq 8"^f qWMR 8rB܋/N%QbsɨIj)qh60~֭[,$i zxx{.mܤI7oɓ';tJlO___Δ[3fфFxrJ =pIܓg`4xܹ(\a So@Qw%޾U˖>}:`֬Y.e8"T^yq~v/v^gXX9!!w͞'B,;6 ! o߾o߾f;Z:u`k׮mܸѤN44 6`؊@(gY܈:ɿ녚K)KŊ\ެ1A晘2ߋ[omc"yz% 7D,YB_|qM +g!ELZdPg䦎ӧO+QzY /А .$o#qo7jHr0T?$$;0. pR fBqB˖-#O·30B /5OܷmۆU0;Oysʿݻw9NC}{M4),4+Qᅦ'N՞:uZFb~]NTÆ 3ޒwaTR4g7n܀(ÀUQJ$0),!kڵ$q'"Qo2Q T)'[|uC Q -[|o߾}lJ1^rŊر͛77n۶b0PB7 3gNw}E€͛׹sg#G`RI!)1`ܣ-z}@p 6t=V9`)z<2I܏?NB!KbٜO6kcތ/~M6EE߱ccb}ӁK}f{if+& r sX9̒RNR R:Lm آE#$̛kCzu ej+䈸_fzLL ;#͜9kOdޚ5k$SqڤoQ<g"wXMs4\Z[ׯOߕoN!lDWi>lRYrj/~k3et 31O+eӇ˷mۆoРd2lٲ}v f\eبϽyM4Q 'Oٯӌ| i C{k9dz4߱GhUڬY3gIJm^N;X6A(«5VFw =pI 00bŊ/] qΝ1j;ȝ;wJ)CdT^t)ĝsA`L*XZ*ĽVZzklƍL3/W >\;v,z.LkԨ&BMHu/;ի UK!Z2f̘N'3 =~ݙӧ CJzmVCܫTl A( b(kll|~# w ٲe#ʛaʠ4w0YY%>;НGBS`H.cMVB8\NJʟ?xxq'MT <=WP'$L{w{ӮzY☸[;Q4ӵƍ3(:~ DZc~&WK5LBsf=S-),GufJD_+^N\$d mg([l+'q&ŋԐÃ' 67nO2C"OKJo81sp,q…/_N.ޏ4 0< =Ņ ElG5j@e"~ӗ+_رcuH3|"} "5~Iq0MĝM+pXeʔpͲ?'8}4Qh!G[pBrO)?̬͛(xq{2r̞c9Mȇ [ĿÛOiax@U?ǥiP;GɴN˩Ѕjժ9 >+H3Pl/'ȖN% XFhHȩ?!gΜXzA4"Bj 9Psd`i zEb]6 :DZ+Vȇ$!0^PЇ`~a 9dWڂ$q!maư(~ǝlFQm2c)ƌ/]pwe _\& q2~Ee7pfxP^"s3… 4w-|)\Mk(87Fw5;1Ub*F)%#~WĄW&ptTYoo1.F!TKi|I%s[Alrj_*Y辰s?tTfQ!hu?{j/w 3=H>nT4Y18Ս ~#!Q¡6j$"AU6\!B D+W駟ڶm++5(5 *Wݻ?*s [l9Llw3q ǎ[f j\.ȅf͚E0a„'#Aqw AwYwQׯ>|:toެ7nE.M~Ə_xS(֪U꫊H{r=CwĉLyb׮]xV=&&?8x`DDD;&]WnڴaÆΓ{{HH#ԭ[ww}.[sծ]FS&{\v[)S&Yn~al2s˖-ih>^+TMxT3.]/_>G[S8)M#)ܹs*Uʣ&ۉlɢb-99رc ֒ Ƣ_gTCbb͛_|իgʔɍpky1cO>ĉ}6 o tRj\vժU{Vݻȑ#޽U|y3l6ph?F9`oooc;‰'M:xG%wiݺ5,a~n پ}ɓg̘y>tPAJ,4wh(zҥ?Ӹ8UWzT(ݻwo-էFsĈM6řDwcǎ͕+t7pbq>}zŋ]CCC@w%q'b=z k/UTTao߆:tߥKΒ%ˬYqW%Kb)5C܃hƍ]H|^YFK.![B7hPbE͛gܪU+@ʖ- ȑ#Ghh('+W.,,ŋ2dY&fـ#~I TbE 6Ěԟ'Oիg͚(6<iԩSΛ7/;÷lقB\РAQHbŠ_ryll oݺUHZ4* >x`իG :oݻwY*߷o͛7 .ܡC2(L[rjժre,4kL03g<5j]vQK/|ҥ?#_|on]xq.1;$qNJ׼YǏc6zR Wݻs1_z%LzՊSc{!o3=&& 6vb֭[gXr _vC嘐=<<0 b©8#""0Z~0ӑE5j$̎[zܡDX(94̙3L2edΜUVU)(`t`.3qt;F=wmcx cN{aSD 90H%e82O^3Iԩ#Wi#8yB~dƃɇpt!j_b}灓yOUݗBTVjK c/X :O_.;./G{{{| ѣ-C]|9?K#q_hIII!F-V]<«j>(B$ NpݻwuھC#&>!PG9v:Bٲq-?mmG}ȑHEC!?B OLP^w^6K}r!9WիWWXB`?Psª(ʹk ~~~7ny0ԌLdu^3Q z P:(S]NV 41p6m`s̉wVڌ ЛX~=ҧK~hҤ O?w'xCe?z뭧1 f;)O?L޽ϝ=ۻwobmڴiǎ<=+Fle45) PتZ|ZRbG;wb-p;4ߑ#o2ODf:3Pr! h XڠA28"䐡aa R fݱcGbV]*FNVX ' aeN1(R[tt4B`CIrɸĆso߾=;yڵkL1&>ܥK5f+ABb+K#.Q>U Wc$$V|nݺ1KgWEW\Hd#92R\%~RI 1֭۳gĿTZ3H=pIQ%UQ.\q`WD,*|ie˕~Y?&#vh^RU)}24"j֬I-LNJ=f{ H :uj֬-Zݞ={ q'2& P'N~k׮!Պ#~BO`8(D8Z*C%A[4bѣ@ Iq us߇@0CPQ-1G`)SX7 L!azG\vmu@a'sFoYdyOw< z h۶-cI׿…9dF'3Bhՙ3g"Hjz,V%#3aN4-NJs̉á)%Ef=RZ o^W[lI ջĽXѢn*wܙ{TBad3ӪjժGʕ+4TRݰa}bJʈ$h}"6U2f bSgϞ]lYq\dfeqT=,_9>|pyG:#_$rӦN|=qGNX5 !9< dŊǏԱc鲘rcC9H9-Z`HF0[L3BCC/Mk܉/L4:z(N:ӦM)of֭B0C /}|8nL@yv>/&w"GZڷo߸q~ q+=[o|Яjw# t" RGIA(64bp:3CwFa+ 3L2Ϋq$B7""bٲeief8tP ᐤ-[2ʅ XW@ bo dmk2 DGs~]*DC q?v uօ35 .q2^-LQN^hÇI))F"5kX\HHVn-nݺạFPpf͚3g̜9SXLw J :3zt} 5,;#c_|8nx~3{d]k jժu]JkO cZ)}r;~&t UTDbW}&aTOL.W BZ…{ZqD(D|ĝ #'[P+4wj2Nz$a,tDʔ-[dɒo[w??}E6l(GBCCRJt^'مqQW_}eO[hR8ZBBVZP!8'T),XK@]xGRF&Ks)SK-:)S& \ :t(%O_V@6l'Pg vjs ~+nI&ZWX[nUT q5Bi{)wgɒM6ND!r#N&ڋz I/҄sTÇ'ZHA>lB6=0 ޡC;5p\?̝;0ۣgOx򽼼tĉRa.{BBT!C+Vlz83EJaJ'ty|~w}%y+3ZժΙׯ o%gFZ!d}Lv)Ru6E⎝í/\leX0Z(￯N$X;BBBF̙wu G]TB~% R ~[|qQÞw׵k8NPJMʕ+:ԡ}{%=q֫gk{S#EeȘSN&եơ{d~p||<*1t;]o>??;71xdpa&իWɂHP).q0 9e˖*g`aÆt^'HBw$ Ͷ'hZjktPvqqqav[@HǩĽr-'K6h$.9qGV;/^ܽ{7?rقFZ&K>~x|ŋ ,hF3qHx*~l 扻źp޳gObLC37I \. = ZӮ];f&ᴪU >\D ,m0zNkҤI%`/:MIR$d/|zN<€tAVCͼ MըQ믿X~#Af;_lY-֕XE?sUUT!7Oy9 X:{l˖-t0]ˊI)sңGRDs5܁ cmerc$q/[\:u׸ܻw/=B~ FbRJ4Yb{6bK*؅ mt!6207Dl *7;wGǧF|W_,.5M߰!k,f^pK`$i#zlw͘ٳg߷ozYe|? u{}j``ʕ-;74>]@WWU뚸kh{$Ԑ-YiǏg 6LHHED٫Tk.E8.q !R\S""ѸG _"_jF$H<ܸL޽{wB"aFv' ESLK'4zzz.X`ڵxB -^D.H *!ִzvŐ-¶("Tp cP"gkuSI͌ V^=k,wc7@SS<=jڴAIDy(#qBIƢCڏs8'j&qE?ٹsg`!sZlYFH)D<&s,Y#3 BG~hu7wƍ{vf.c0ѺuB.,Q| 0mub'&$j)SFĆXVd;2g.Ucǎ(#,u)Lg:'ǽ0Ptr,kԉHb2d,gSS P;)q?M!߳gZm*(%4,] pP#͙(#$AልѣbĈ _ݻw_D!.A)$ *dܴ@hYxƕ+W|a?EwGѢEo&TyC=S|P#8*}L%0]&}AY|q|<]^?)9sfϵZ 3$]c.k)B^pNd1ɾS#[޽{*Uui?Aw$qw;=yfG h׮]6?6#qBM<*+YG$lݺuN4oZjYډ3fHE#qm=pqkHܟ, @sE[m`$O! M݃gE<) &KA\![O 66o9||||  +=?>ڵ5 "]YݻS@OOh&{뮋O&sh<+4cƌrz.\Xr%'E+Ν;u;HZTQ);[l0kׯsEVVMDqӦ+/kZ| '{+Wܹk3F@c߿wv6wH5l&W%4qwqHHJJ_O⮑j$&&>#6=]C&ikh<]CCCC#Bw@w khhhhYhYG vTZV_ݽ>}{xxWH449[na^^^NN ۽{wrjԨ ,pR 11b}cY &ۉ={ƍbݧ8:Ұa:u<'44ٳgfȐɷ{N81|r~;@RRҶmۘ>>>&&Ý;wZnݡCm.QCm߾}9rP_%Ku;99ƍ v||<ǩ&rK k֬c)Eٳg;]trʝ;sٷoVZ5o\.)P: <_|4Ჳλ-[6٢.s˷nݢ,ݔ.3(L^  9zjM:j)4j` * ]y&#Wa`h~'&3gfϞ}ڵrʵkθK' FjC1J)JbxI%2PLC:'Mˬ5p99Ԑ B6!`QPQÃ4߈ko߾=8֙{CXJQ!xi[a#6NP#NɈ(8JhK}*"OrZ3gά>DtJVߜt NI\081 f]vYCC Mĝlٲsjq<=Uu R?M`(Yʕ+qW\K-S &UΜիW"Eȇ~XNN:ɿׯ߶m[}S4k֬0%J0` >ܻT)_n$/^OquIr]^.\E▄K Ec_{o* *K\\\cPR#͈Vc0\ULK0ʾ)iaTTBE ޅ,hU#`s/@)ܟ|iӧ_z%tz"3>|{|{3&,4i믿C߰aáC)n͟g&GuTE,)nAwqɛz'c)$&ʆٿt,XЬY3#wG5RM½iӦT6O?4 wvC=4/rb۷o~30)";찤C!t/TX2i6N8I _'b}Y'gQFŭ[mΝˡ[jš'OL1Q @$ StCHMpJ wߏ"{Ig}6`Y뇄=֭;vXE.]tQ,־>}CYZp_}+!P~_&+23ΠU-[XW*>ާ=uS:t^{͛7Z~ݻ'~uju7kڔJ,`]"ߋ sp>j!Cb!NYp VxH'V[muYg|mOɌҶ,ZWjgoVa2圞d4M^M|F (QƝC29DJFpi\9nܸ5WD3$pI9Hb#j`a8Ā*fCC2Q'R?u|@+xW-Z!%3C(-!#2OHmqtR0!!"oɒ%XN`6Eg[}4hؘ=oi QQ߱c&3ʓ&N,d/>cpBCܥKTqEd3@^5t%1\k$1/F @~Ud*矿x|.;'Kg*hVs9(ۯ[;@uQ'|=.j2{*J] /@V1+[ ]f͚N:iF֩So?=3s JeM&S[%&WdZd2A7|3_"(τPpj0 GGcQ\8#\p;u.$:;jD9E1Ш+#>k1eȸtϞ=̈́pE沇oIKD{R1ҿJ;l ť#>ta!CtI]pqo1Aн9BlveLc{ "_v-X/F44ӕڊ/]Tr;*Va}G!͛6mZd PW eޜJ^p!I26hРYf]s5h^xabM8E%#Խ{U"ܩUTvMF70-aÆL52dE]*FC=NȦpG~.7&Mݻ75﮻hC,MNL*ǵ.s.< .#Ȝk|`5ի\T>;]MD'ꫯf"7şYQJ9)f@;I&2t!!͛7ܹsq̿9; #eUr;32g,3f -Ztڵ"pJ l;j(2/ɺF-QZ[KHh,kIKDiӦŽ-Z8]6vm~mT,LH@f z?1~O!=FofϞMcÇhCɨe˖}%983ɵjd9&_zTݻH^zȑ8:dcr|ijHLׯ K ӧ4Ql7pƔo3x7I9;D%uYqY׃>ȜoD'1}B9!oBRى'KW\٣G*+~+V Ca'3P̙3{ѱc0:4> N:LKsLnˡp1!QqJvp=z4CCP萝w9Ү]:({q$_s5ٶw儜o W^d PW %O>k߾=E7m݆V+=P*w'xqgӧO2w/_uU{\ x^|IhשW^ .8p`>I۸QSN9e#.($'O|ee@DRZIFwL~7W2w؁h2)1b55GO6NjӦMɨ8l;0GԩSYZ^Xz? JIk׮MNrڵkWV;[w3%@,Y2w".*栐pG;AHw~si}yqZ![Ν@8W::͎ܹs wQ=y޽;>gے&3'N,dTwδi"&f½j()?=zԨQ/  ('X]wݕ\bj_~y2;mC: 5{=GyвeK_bE\7C%U0TlƍuC;!k_^7Ύ tm^?lɉp&qoÇ3(XĿG}tС9=2d \ڋӟvS'L>/_^h/a~mKe %;ָqdL,]:a„8ӭok׮yVT{TX}ꊱ+S6l-f+កN.f*W\wuE.wI qM71ePB3af F*d"ԣgϊ ʹTflNba6QgH2O:B/g}8P(G4Q& ½m۶;vmǎ;d2^Wꫯ."]C^5Թ#G][q SOׯ^zmv…ݻwOnjDťtٲe>KwR~>Sdwߝ%KPJ@XԪ$s1=#@hU"{#܁7hNHPV|ys*GDg B@Sؖ,Am\xU_S_}/2hG8̣z)fGL4i)l<๐!dJ +lɕ wXkS1D#w؁)\ (*|=!}̙3q_C-R^Oum۶mӦ<0qDfK=g$.-҅)i4YZKN'M>Åpf`?p|ڴiPgS* ~wuW| `9S~ǾEM?vI)”p&sɆ? wZU 3ظzm0>aB#jxWԻKl:3[n|269/+2gƾ6[oF#9z9½ɴG eV%Fp߰a9n}N֭i$S& wTpJ u֡5kvaӦ} #+ժU?}穠Qbj' e?4cGMw܀\x{ 2d4Ay@C6eʊ+Y|:FWd~r_| !T t<g&#VZ0F/nxM&#PƌSc-;=e yJBCG;Zcǎ1KA2K zkܸqhedV9+k׮egϞHթS%א5&8'KAv$ԩSi!3m,4 sEE殌Fyi9sH>(?$>n[^B @Ԯ].?0[&t&!8m޼yfg:d$zʼo ?蠃r:VV rY &:$w"m" l/c_Ν;v؊k>I Aɀ+tlD /ū7-0vtqu#jySa&5yᇇGy'yq\h+MT<ѾqԒ|wq.|pBVB!C+}z/BWuHAVrEB-';"lfE:$FG `?䓓;JˡɅ ޭ[x)]3,"E^5)ܫW_}駟.t]wԩSD?>'s@4j(yWmڴI C0ae˖v>#M}lᾩySU]6o͛tRR·3_:Ӓ>m"&=|7QW ߖp4iҠA -mN<짵|3 8k֬B+4mڴK.&o~}ٸM>[ouĉRёѿo߆S7T}Rƍ>|8![~ui׮p ܳU÷%ׯ_f͚BKW[oo_ZE -b-_%(iV[mm:L^pͬ{v}]w8oAwߜWE.@-]z5kٷl;{gzf½jT- w΢p" w΢p" w΢p\Z>[nm9n^z;Wzd,Zf͚jP]vʕo|?LTmN'QnN|6lY}mNoI:uLn< ʅڷo=zTXz͒)S.t&g}3z9 &,\O>a"԰a7j͚5kȐ!'tRFMdx1Svs8GDj]}Vlp.UۄxC9o)y7HM{w7A?>Ν:u=&PW U._xkk1j+b|lZvadB}r5k}W^y%9ks=6L8G}tiU,իW_r%ڵ7T5zUV7&zd|:V\Iϝ;._n"T!<~+V$/w/0nf½jPKۮw f3QpGCoذVZ5~V8,]|9=wi'2JyM{'Zw}ME0K.e,(cٺBR$ݎ~MK ww8E4%Ff %F3nI_U;>öY=n{= "<c]y0BU (#_4>ɇU.漍fe=;Zco[X.Z]Šѡ80Ki$M=3 ^b9qPxl&[ܘp vBUZBPeCc,I ,Q(QhfgZEr5B<ɛ8.Vg/GZEGF~L=Fr["~/^:u?~zɸf_RǞi]wլY:(~6}B\`2{9r\%܉,ghРAEL1\QlIxѣGyx3X~(:u7|crExWN<240Uq}(D5GѣG˖-c׫;> {4G#~a얝,%hg#zW_mڴ)S:3tܙA|K/ߧnݺi&K.]jfRQ'Nm͚f.$ pċ.sl;j(BG2d!TdJj!brtHO>=$F!qg?p?3Zsn1O*2"I1䠜Keka $JG|1Bw&7\*C?3nvU6YN"^?ՋtIl!3gCzI +MJ aÆ9cǎ~x+0v?8qKepK|rs=ÌW^>ccDFΤ׿.BRr O(0fP'{kѢGTկ,FEC'~E$+G,윿G}tm~zҺu0yYK.C`2E%!jժۊh3HG8FD$LI1;AB ؜zb\Уgϕ9k1kX 2訣F a6ɹT&G@8R3Q>#BZс80:;RrH=p&!b{צL=KgN837n2i$Xۤr.šlH?_)ǘ0Ox衇>3ݡC ׮Y`+$k*5݅Qń (3QF2fq%fG$c=6gME*oߞ` J?%]@0vV޴4O) 8Iv fI1}%Z7܏,u͛5f$2 8?Bv b5ORPIם5s/~gs=$jXjrE׭_i(BǦFV)ǨB(ܫ~|Ax sehrWOvw2ERa;p ,b8tCt=Gʫ])J"M0j-{^qE2KF~=F~D8j/~իfZ?B3bl*2w3P<@ `c͛7yoΘqo~Q'Kp(N%O?Q>lr~w1<Ȓ{p'z 4 -MB-y r̯~Aj!Ο(OjLb%(:{>/؃lE'+l$4GӘĆ&נqF!YJNqȜwd%$GU1tiuY$ I'8xwh3DtIKcX1u&%;#NBHv 7 -I_~9^Q+9Eh Lqfْ )< , sիSO=30p%_T\R3?<~@ÐؤIX; oPؐ"bFt+Dd2 "u]ÆjNEUZ*(Og>rI'|NϞ=ٶs\N2H9Y)s$Zi((e9'0?-[&FVwKWAq*oàAឌ#èqs {KF#4hI8vN⋤29D!_z%R ɜV^X0.̢4nN+HZF-5Kv5Ba5yVnVVn֤/..%iӦ=쳌i-d<էO0—ȖaT1܈#%&H0Rg?>Gڧ7{|y'pGxi*1edBJ''x"IŤ p⮛PpNAt4;E٘LL3,}tC'1:8f՚5&@PVt]p7IA%]pGf~KwO9 RL]f)WKZT{PRyKՋ&PH=G޽{Ef٥Μ5+4.$Kq]tKD㲴,sh*oь?OԘzըQH EOv! iHbWO<RDc5NYO ƶȐTMk?q)9`C=SQpGw{+2I[%AaoFNP/wuBCyשuVQ!arƙg2a?믻nܧ#F0OVޜt#EIh c\rrnNe5J59QBRG" N-Q,m}2@L4s3T2I:碩JoN"̿tM7DgX!&;QJj\I~%2 /)bXbi[|%◹w4O^^{|4dh$PO( 5f9( 6;k{)1/N6bq!(VڀՑe P3ksP썸(ޙ_ w1 ; .e]L0*0 &1 )p_l0yd>/;g{`7d7WR.ѱ7'|rEݛ {K2$v̬ # ]X@heV$F&oTE/Ce4fC6/d2M`r2Q%q'հ=fe嘜#Dѣ[ouЁ6lH8둩p"}J{9)50!|Tǎ44X+01g#o6&N=RPL#14%]pg糔 2;Lʼ p'\gq={61ƍwՕWx K"Y/Ꮏ}9 N٬Y3&I?`8r g}q_^BPTy?z&\s5Ǐg_yww9mٖ8N7pPC32;&ӷI$="!eG7e"4B2)ԮUjNBc=ƿI2нGFLD dGDq!G'giNVZ]~屔?66lUs8{쑣_EIY y9.$/34|@Đ׬T]LrR}}tTr E;3}hHL)BRChXvΝ;3[HZ_4dx2q<-sF1'NJ0ZN wx@pSNLN1.TxF;=:yE׮]UlaHtqT*ܳSP w&QB/:3!PVw%eH' TҦMXOo7i ؅c 1}URUCIN}Gdw+S.'2ƍKDyaРATիXMbF1NF}~8*#ߚ={a/Yfȑ͐-_2l;qD޽{# A/gy ᎘fi({4׾}!I&1LlKcSN˜4QjFߓ8 lTyq }Pq1Rd͚5I̐GQ#s.Lx&z 3!܏3D+hܨ@D`[qĕ SP!NšdA, 4L&}y))-)ܑLɍv'>c#}]{(ܫ;@W%)g!ܙ;6[8dhܸ "i2Jt$׫%=`#F̚5Bv31|"apoҸ11^޽{fuį>0 G>}:m6θq5H%6 0ɛ$BF!qk&WϯN|EϞeq'&Шd8D;Q__?a ̝D{U2,ms行cgIyHI*&:0!ܙy晱4G0 |Qp/ !r{M#_͐Ԓh/﫾vlo(~g|>3g=$q(֭鯿>fh a$Nj޼yfGդeyIlEK2IG9½^z~z8!)I.~qіGc2EwuqL6U뮻&18|q'SI2GQ_S#YHхy2$ۦMf/(. DQH h-='ӦMA͘q2_*2o*_(y{\}vJ6qp/drc!YYLN1GBDK/DےLAar~ U8qPT4P9½pƏh62s>GT /,M8~qH|hwGwX pG 2${Ga^5kBS#FqRh\ݙ,PiXZpG#yI4drgy&ѸL%UӦMkks街gfSq>I"&p މcU’(GSb97T/'&xSN9c6!Z(|DmTܜ LdON@ 6 mXEM:I^{ܭ;^Zb? ]~qf8!0ϸ# %ZN=IOŅ;'3(EEC׾|3&TRųbW+$CKo@aǔE\DNe9s&+_Ͳe'ug2N`ˀ#ܓD+&S(|B"h#K.Jni4hq# 1Q1{A0yD&̚5+"s]]ǛnJ4kwLy/RW %;G|뭷 $Qу%cGm6lqD?[>H*AKaGhը^E˖eqs>}pqBh'TT׏~D%KD?#úO?OB^M stj<7EW^}} %QQXVd\o߾sE[ֶש3*Ô:"H+F!Q… <*P- p2mvهbɅq!YID)[jŸp=Gy$I$$e0Ruv+jla?yRǑdh,-{&U <ܴv͚k#: Gu:t[p3!u|xgP׮]t:̐2vZf(t!'5f$^ܨQCF^s1]Mdz xyE&ƽ҂۶fM|5R4O>٢FvMTDFѤ-ZstqA4+Dդ\tQz`SOM}5nCa]I/q1 G=z4 zmVZ*exR>L{ߣfp'cŶ?ǝ Ff&Ŗ[r}ݗ s`xPL2IFpɉ&w~Fl?8ⷤq̗hy28;Xܱ9]b %y?>pgq=YI ,Zx?-`ڐm/{#T7*ZdVdT_hs$^<5)"NrE\x&T2swu֭U϶-4rή\2^?IL%/qr 2fI2ʅz7v< 7)(B_{ɴ7T6]c83ؒ34^Ő?%KfdÆ |C)I… 8I'kq+Y/},btz}H!ɴQ/(%X@{&ʼ͙3+6lwMNH1 iOG.bf1 e<yoq&'=196g<& 5v1co/\ f~ClS LOb8"4,eԈNWtdcz#;AM@'3)8VdC}ѱpܹ3e`0mHŋo]~#7B'oݻ|l)[! ocpO*S΃8Es7~0ou)E^WUp֭ۦ>P6 \BKk/k9ׇl U}LLw#Gj̀+fj)$ܙU'Og뮻n g)/&M #V\9f9ss)P˷N^Fo)u݉'Ι3'nYh֭[iӦ {q0o޼";x>.`{׭[ǿ{-v no/iɓO>YdÃ>G}g/r' /X`Æ ժUؼyK/'7TlĶ=9uG$>>___III̘\\\XXX]]]cccZZZJJJtttbbbʀRRRgggVVV^^^yyyTTT---[[[~~~GGG{{{nnnLLL```aaa;;;}}}MMMHHH???444mmmUUUxxx|||fffzzzKKKPPPwwwWWWqqqvvvhhh111QQQeeelllOOOFFFkkkYYYooorrr̻=IDATxy@"**!""KuFծ"fYgKذXEˮcS It)DQ3׼}f j@APt(:pU,+U1 Sux7U^Az< ժ Tˀ nLb1Q6_tWd3J\n.i^Il3u_?jjV8MkdvMzt|ڼIe"AwμtJ}.<^_z|yyeگnwzx7H\/הyUPqxاb:Z7s%>WF}c"/q=9Z۶+ɳJt=jsmC$߂;J7av#xWcg1L?.DN#e)kG!Fࣳ\Xޭ=A۬MNOB)ŧbUü6ΆY$/ 7tg4Iw:(pћwi7tYl%_44LKJLsG)1h(H\7HU6`TphA*DDqMG|SoI$.Ҕwgƌ~_}4:7)Dt q-Oϲrhٻx;,sՔQtw6$pэMz٨s8'U4ݨ.e#MǭZq3"6fd|&qVF .VSQ_Nv;_;@}! ~2NU[f/1ێRv1PTCSr]nPt(:pA8 EG#&y c<[P#$DJa:O>_./elﺩ:GYk }T‹3iHD1wv+}>*F~ә|d]K+(~$٦ƿٓ&ryCHr=gk⌈4S:q#;f'1E[ D'ۃI|Vo8tߗ%>rE\8~exFt[ˣ:$EvYTOwo.6-xjy Y&p7/77k߷]~ۦ '7Ux$E?7<_j*6L7mqu_㛛.p.ԥ 맾oG?^џȧ9Pg&5\e*_d /;/?l3mS86Xu# Wڼ\rChs'?{a_[+wwatZGn|ߧ-Ýoz3l\ݧ,֨$fՉAT; 肇<;*tVp?~\Yf)L]j!dhN$z}NI##_Ȫ}t|Izy,QWu@+%g^7~o82sEe̓|5Om!tmRxpE艑MnW=Rf)oڝq{+w8PUyv%/8Nyza~ߊoe23?|aRHqOz%Py i%>gPLכȗ|"&6[#e1h'x#yI]w5\gr:Ic^sW^t~\jmQvsd̓|}4:86,w2k=S>ߤH-Xns蹇}Q ?̇GϷ+̡j%Џ5C}z\q Y}+~I*OqƈSBAT/ 1f'8sɰSOd]h>$u^k9<`{=2 SI\v5;.D孤tf8ou˜Qz5isc ;/sm3 iћ'Yd׹W䱆ʆ}:3 =rР1* TѳRyN4ؙ~ @$X$o+.]J4{!JJg4[MgD\KrueO"x͈zhfd΃(v<aȈ~uvo"z*6 kkC/\[wܩs32_w׉VmEjhՇ3M CnFMm[N?I螝=j#G|{-z? xG%!ƨ'NI<|dݫƂ ]TZZQH^QR\ĬueS#*mVvc^ qx65b E3jm"Cl.?Nde9u3G2OЏcfWtZ_KԭWD/Y2^1 :a^&ͼsȭ( R 򿗿&o*Ѹ3*k] (:򿗢lXt58siD#5EgSRz(:쪯Flʏ)?ήjΦ(:(:쪯Flʏ)?ήjΦ(:@+ާuʯj7lQABFޣI9SsU~U} ԝM;(:쪯Flʏ)?ήjΦ(:(:쪯Flʏ)?ήjΦ(:Ltlc˲5.֒sORtj=TxțV^g+mzŽ۴_Sx$N>ܣLu.3NםmtO)kZü ".YUw(ŧnPP>kzɾkX>צsWkE#+YǞ)_*K >Խ}8gvϸY\ FWgv@m~OY8nx/ψlZ~QA/ֺZ ^#X 0MhN_ޜ(z!O)ig<S [2}Iֲ[Fx܈ 7ʏOη3[q'[e3{O^K{U#_%U̽4&/$VN1?]f-+z:Yshm06 {ޏaa_LWCΊe#%g-[_#]t<fH]ʢSȜsGTA5p[3׃.77q12mTrF)[pZ0w_uZ 80^J':ђ2ܥaƫ -+$58KD'L]h6-4z#?'% rv&C>ndi׾phn ȊRCN5Clkt>js;@|{]%ɜ@x%# W4y5 =U\G"IEF`M7Y=ɵso ®$.`3+v!;6SB7t"]O$UI#NLLw|I!N'aRA,o66+GԽ굫{AK%cѥ][d>pd9伮α1{_ zN⦑/I<r |F~0b4]l ->}:;̳h=f,dBox"56Yt%>}eχy)х$+ʘKX%xG_-;2w-,S6( ˚f@Bt>ӖqJQǢK黶}#zF8T\ ?`77rEOτayzA/l/~Ow$$ᢇKw GH_:q\D#/^??Iܛ=cn7k:/%0$n(rLK #C ~;VTi4rfO۠@tV}57x,髚(DͺsԱR-bLHM~>=6@ZSqg~B*EoE9x}9'.kl>sWWs#zwh_EK& 35ql~)Ql^SwѾ\YDt:/g:æO zY5]s<%~;|ɺabDS*5oNQ@kt߆ ^S=(wh#ϦƯu1ip_x^Lg?B- p`MRtel3K?/ahgCyT_Sm_.ED_mRfpnYǿzEgV|%mh9sH}ٿQjw^C?gɖ_xoHp9Na|BXSmLOL󇞻f!/%`cۺppd7o\f_\J^G뙾F9v @yxe#* Wm':њ}uϦF*(bA恍΀^& v[ 0?^cS7zώk]ζie$ب;=w1/e;}UOdEo{_x h^[S6ݿAIHK}[oc.uQzo=$F2̆I+3GG ˯bERQ޷j\u ᔓ (^YTϋ#V_'c$!5DmEʢz^޷j;5EgG)?ήjΦ(:(:쪯Flʏ)?ήjΦ(:(:쪯FH^wz򫚧㦰~(nϾj^>PsU~U}ֿa ԝM;(:쪯Flʏ)?ήjΦ(:(:쪯Flʏ)?ήjΦ(:L*- IPiu[R篆Tn<D;5& )z}f+ܦ/kHTJ%.LuL,>R0f?'ܦ?zGdذᣱ5S$чdߵF,6 T0TW7_/oNZGT]n+p@FmCqM$}@|~ҧm^f~*ɢ}8!wMU4Bp^[ӵ:lϑ7x|# Ot>{yEkWƯ@޵`45ɧ_-L枊v71+p#Kf& ?2>LbO_M€0S L^ntHֽmzF1d͵;mm㛠)҇c z?sm^q9fa^#FƼL-\ۥR^m<ՎBH=$pVyG!ٖAdN`9x%AT@=x\S^b:U+~4tp}ӟ1{'((/ml|MU7\Y7:.μ>$ycG6kUe%ZCYy$iofp1넯o粆]JI1/1E-^Z~o\@p#U+2:Y QhR.lg}ǻ_Fk }>>)@2'q~:^*eAoM#o8Ԣ\k}w{mbyu΍lz댟OF>'4=ihw$y):zNZVt^Jt&&~-.ȘKvt-?dnj-ҧmPpdi;zF 9Y6bld,;I!=N5@lkt] 99C̻9ĕ$Eo䊞f n~O}j-ͽ7 "oҢn;yi,@mws0{IhOǿ܅K.' J ud%"*ǘD DwF>A/?)ޯZ9\آǩwm!z[%5T8my17rEIτYyz3ʲݒ C4jˋVZ G̥/^-;je GL~~A*ɻC&-y)E);^3}%5O۠HMW?^d?髚0/9G>ŎS zߵE,I2":i&6@ZG;CJţ \S怛U|"usߙ,EН'_]{i;J_~DyOnO{祛cnC/A}Lmd)b}+X>!g^x<}z^8Uqid *Eof跍ݏJ\;X'o副y&X(q +^ci@I6|_-l\G,d^J,wӪWD9X]Ie斑Gƈ4`')9!9~x!f7)D\Xߥ).ggӳ,43Aa_&p t謁<^9L ECF49j2ܧ{N(GmyT|Nͻ }'B 5esӇ_S$m^?JJ3G2I_: ߜ~rhٻHshgQ2X39F<-+sˇC*xe#* W-~Yky=x^^XRa?·~:^]79Bϣ&(^YTϋƎ|[}5B>~3+JQEgG~MAAّ}Flʏ)?ήjΦ(:(:쪯Flʏ)?ήjΦ(:@+uʯjvi(z !} H}:^窫9)E$D/J@+SuʯjOP_)?FgW}5EgS~]MQtvW#Pt6GU_@ٔEgW}5EgS~]MQtvW#Pt6GU_@ٔEgW}5]n,Y.`2% y3 l=nB*LM?g{4iuyWZ]E^n>#EϠш* p}ۙU}Dޕ]=m;0-{Qtkv#ee=ݿt|<رq~OYdج0w[SmU[ּOɺQu|K.|fw\p^[t:)ͫ$sGI+9D_Q`KJ?T'lqVgR&?0-Z)%ED v)O$* X'q˥VEZ:W}UY'k.wC{ܣLO)b凣\q6+m|siLC^o외lU%3$vqI--GDƓn qJ ׽1PT)w 6)}>Q?@Btpp=9$HUg2}|~L79$D۩x!+zbb KNsL eGVտc]ڙN5Dg^'=$L4u*?N޵QHXX rh>-?:]OzA NR~~շf+,ҧmP IM jM&:|&$#"T]/7tc?r^}Dţ h'.p04LKJʆĝ*UvxV\u5Fu Qf*; oSafuSSx ^4ϘS6(}A]k~C.:gv{tz,*E8<Ƣǫf^n> r-{'K>OzRE H<3^A~IٓiiЭ_n4]s/+?[Q8ܭœ#c~I,`Z4d4z}aݥI. uNיKG`d ^ XC(]^QW}5s.|%C2s+mMGS}?5tz/[N\benQu|agތ<3h"7r^D,~!ׁ * W0#S4ݨ.ec]"l/Qi5 z8Xs7FFO"aݎ~rӷ?M:$j"`ez0S걊3וɘ qpzy=[B!3O6~hs }_ocۺh9 df?Sƨ^~>Fe?ɅW*c[H$) Ӈ@y+uǛLn;kYl~nMⵁz2~36׌3^gȦGOH+/1RJR Svd!9dEuQ *MjCz?l}Di. v(@d[Q޷j Jtۮ^c6 67ʹ]89k=Tkj`eI3p3M?O2}Jߦmt1zR%SS}|M环 ֊FTWs=˛Sצ+}oAznts8Yׁ! ҅}J&eo#"M`ICVC#mhs <ѷM1lR%[N[oq#:r`sSC6(?> ;߲n nf٨WnE?%yiG4J {i5L^Zt*^eݲ"Ǭ5̞y njf[-3}Jߦ D/x`B?hk%&~Qqi##GelB7YަmmWn{ Z*9 wm!!}xZ8midK7rEOn&BI*̶Z!EZ𴸬Hxǂ}ǯ^+{zO6eEfsv>ؽ9+I^Yמh+{>輔xGc WA{#c.Mu˂?fv>'EO*ʸ8CAAf ?CwH5Elk ^>wy=/CBt*)l!7rEOτayzA3_!lpwLᢇKwT}+xDS/nKN|={xƋȍ9^ nr$Wso#lK.'}v7w|9(?:-{+ '53[ }. Dwlnw[sǒqf]9WSK黶3Yp"5/V8T߮GtQo9N\`c`0j- |m^p9ooZ$uxJ.Yw(%HқOoUC~wyS7 NJē4rw,!j*#YTL>3i۠@t _[5hW"M/Y_qWҰDŽsS_M[/[iim<]y !Udc7_6LxiEF׵Fy6-0~ۍO{ȴn?tpS ECF̳YxM!y?yt^0b :!7/n{Pt_xna?}{I>Od}6/ahgCyT_Sm_^uY"j7˄scWc9J"ޏ{_~5EG<REC羚eAT@ە?y F6]_1y0fFX\a' 40Xiik!xmN~Q7݇bStWeMb Z$o[t^]3S]zyТ)&d^ +% MvWc>emSE^,'@ 2yR` b7{wA:A1[#Pt(:pA8 EG#@Pt(:pA8 EG#@Pt(:pA8 EG#@Pt(:pA8 EG#@Pt(:pA8 EG#@Pt(:pA8 EG#@Pt(:pA8 EG#@Pt(:pA8 EG#@Pt(:pA8 EG#@Pt(:pA8 EG#@Pt(:pA8 EG#@Pt(:pA8 EG#@Pt(:pA8 EG#@Pt(:pA8 EG#@Pt(:pA8 EG#@Pt(:pA8 EG#@Pt(:pA8 EG#@Pt(:pA8 EG#@Pt(:pA8 EG#@Pt(:pA8 EG#@Pt(:pA8 EG#@Pt(:pA8 EG#@Pt(:pA8 EG#@Pt(:pA8 EG#@Pt(:pA8&DIENDB`unioslo-zabbix-cli-09a2fab/tests/000077500000000000000000000000001471265333400170645ustar00rootroot00000000000000unioslo-zabbix-cli-09a2fab/tests/__init__.py000066400000000000000000000000001471265333400211630ustar00rootroot00000000000000unioslo-zabbix-cli-09a2fab/tests/commands/000077500000000000000000000000001471265333400206655ustar00rootroot00000000000000unioslo-zabbix-cli-09a2fab/tests/commands/__init__.py000066400000000000000000000000001471265333400227640ustar00rootroot00000000000000unioslo-zabbix-cli-09a2fab/tests/commands/test_macro.py000066400000000000000000000013241471265333400233770ustar00rootroot00000000000000from __future__ import annotations import pytest from zabbix_cli.commands.macro import fmt_macro_name from zabbix_cli.exceptions import ZabbixCLIError MARK_FAIL = pytest.mark.xfail(raises=ZabbixCLIError, strict=True) @pytest.mark.parametrize( "name, expected", [ pytest.param("my_macro", "{$MY_MACRO}"), pytest.param("MY_MACRO", "{$MY_MACRO}"), pytest.param("mY_maCrO", "{$MY_MACRO}"), pytest.param("foo123", "{$FOO123}"), pytest.param(" ", "", marks=MARK_FAIL), pytest.param("", "", marks=MARK_FAIL), pytest.param("{$}", "", marks=MARK_FAIL), ], ) def test_fmt_macro_name(name: str, expected: str) -> None: assert fmt_macro_name(name) == expected unioslo-zabbix-cli-09a2fab/tests/conftest.py000066400000000000000000000053571471265333400212750ustar00rootroot00000000000000from __future__ import annotations from pathlib import Path from typing import Iterator import pytest import typer from typer.testing import CliRunner from zabbix_cli.app import StatefulApp from zabbix_cli.config.model import Config from zabbix_cli.main import app from zabbix_cli.pyzabbix.client import ZabbixAPI from zabbix_cli.state import State from zabbix_cli.state import get_state runner = CliRunner() @pytest.fixture(name="app") def _app() -> Iterator[StatefulApp]: yield app @pytest.fixture def ctx(app: StatefulApp) -> typer.Context: """Create context for the main command.""" # Use the CliRunner to invoke a command and capture the context obj = {} with runner.isolated_filesystem(): @app.callback(invoke_without_command=True) def callback(ctx: typer.Context): obj["ctx"] = ctx # Capture the context in a non-local object runner.invoke(app, [], obj=obj) return obj["ctx"] DATA_DIR = Path(__file__).parent / "data" # Read sample configs once per test run to avoid too much I/O TOML_CONFIG = DATA_DIR / "zabbix-cli.toml" TOML_CONFIG_STR = TOML_CONFIG.read_text() CONF_CONFIG = DATA_DIR / "zabbix-cli.conf" CONF_CONFIG_STR = CONF_CONFIG.read_text() @pytest.fixture() def data_dir() -> Iterator[Path]: yield Path(__file__).parent / "data" @pytest.fixture() def config_path(tmp_path: Path) -> Iterator[Path]: config_copy = tmp_path / "zabbix-cli.toml" config_copy.write_text(TOML_CONFIG_STR) yield config_copy @pytest.fixture() def legacy_config_path(tmp_path: Path) -> Iterator[Path]: config_copy = tmp_path / "zabbix-cli.conf" config_copy.write_text(CONF_CONFIG_STR) yield config_copy @pytest.fixture(name="state") def state(config: Config, zabbix_client: ZabbixAPI) -> Iterator[State]: """Return a fresh State object with a config and client. The client is not logged in to the Zabbix API. Modifies the State singleton to ensure a fresh state is returned each time. """ State._instance = None # pyright: ignore[reportPrivateUsage] state = get_state() state.config = config state.client = zabbix_client yield state # reset after test State._instance = None # pyright: ignore[reportPrivateUsage] @pytest.fixture(name="config") def config(tmp_path: Path) -> Iterator[Config]: """Return a sample config.""" conf = Config.sample_config() # Set up logging for the test environment log_file = tmp_path / "zabbix-cli.log" conf.logging.log_file = log_file conf.logging.log_level = "DEBUG" # we want to see all logs yield conf @pytest.fixture(name="zabbix_client") def zabbix_client() -> Iterator[ZabbixAPI]: config = Config.sample_config() client = ZabbixAPI.from_config(config) yield client unioslo-zabbix-cli-09a2fab/tests/data/000077500000000000000000000000001471265333400177755ustar00rootroot00000000000000unioslo-zabbix-cli-09a2fab/tests/data/zabbix-cli.conf000066400000000000000000000105651471265333400226770ustar00rootroot00000000000000; ; Authors: ; rafael@e-mc2.net / https://e-mc2.net/ ; ; Copyright (c) 2014-2017 USIT-University of Oslo ; ; This file is part of Zabbix-CLI ; https://github.com/usit-gd/zabbix-cli ; ; Zabbix-cli is free software: you can redistribute it and/or modify ; it under the terms of the GNU General Public License as published by ; the Free Software Foundation, either version 3 of the License, or ; (at your option) any later version. ; ; Zabbix-cli is distributed in the hope that it will be useful, ; but WITHOUT ANY WARRANTY; without even the implied warranty of ; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ; GNU General Public License for more details. ; ; You should have received a copy of the GNU General Public License ; along with Zabbix-CLI. If not, see . ; ; Zabbix-cli uses a multilevel configuration approach to implement a ; flexible configuration system. ; ; Zabbix-cli will check these configuration files if they exist and ; will merge the information to get the configuration to use. ; ; 1. /usr/share/zabbix-cli/zabbix-cli.fixed.conf ; 2. /etc/zabbix-cli/zabbix-cli.fixed.conf ; 3. Configuration file defined with the parameter -c / --config when executing zabbix-cli ; 4. $HOME/.zabbix-cli/zabbix-cli.conf ; 5. /etc/zabbix-cli/zabbix-cli.conf ; 6. /usr/share/zabbix-cli/zabbix-cli.conf ; ; Check the documentation for full details on how to configurate the ; system. ; ; ###################### ; Zabbix API section ; ###################### [zabbix_api] ; Zabbix API URL without /api_jsonrpc.php ; NB: http does not work, and does so in unexpected ways (with json parse error) zabbix_api_url=http://zabbix.example.net/zabbix ; Configure certificate verification for the API server. ; ON/OFF is passed as verify=True/False to the requests library. ; Everything else is passed as is and assumed to be a file path to a CA bundle. ;cert_verify=ON ; ############################ ; zabbix_config section ; ############################ [zabbix_config] ; system ID. This ID will be use in the zabbix-cli prompt ; to identify the system we are connected to. ; Default: zabbix-ID system_id=Test ; Default hostgroup. We need a default hostgroup when ; creating a host. ; Default: All-hosts default_hostgroup=All-hosts ; Default admin usergroup/s(root). We need a default Admin usergroup when ; creating a hostgroup to get the right privileges in place. ; Default: Zabbix-root default_admin_usergroup=Zabbix-root ; Default usergroup. We need a default usergroup/s when ; creating a user to get the default access in place. ; Default: All-users default_create_user_usergroup=All-users ; Default notification usergroup. We need a default notification usergroup when ; creating a notification user to get the default access in place. ; Default: All-notification-users default_notification_users_usergroup=All-notification-users ; Default directory to save exported configuration files. ; Default: $HOME/zabbix_exports ; default_directory_exports= ; Default export format. ; default_export_format: JSON, XML ; Default: XML ; default_export_format=XML ; ; We deactivate this parameter until ; https://support.zabbix.com/browse/ZBX-10607 gets fixed. ; We use XML as the export format. ; Include timestamp in export filenames ; include_timestamp_export_filename: ON, OFF ; Default: ON include_timestamp_export_filename=ON ; Use colors when showing alarm information ; use_colors: ON, OFF ; Default: ON use_colors=ON ; Generate a file $HOME/.zabbix-cli_auth_token with the API-token ; delivered by the API after the last authentication of a user. If ; this file exists and the token is still active, the user would not ; have to write the username/password to login ; use_auth_token_file: ON,OFF ; Default: OFF use_auth_token_file=OFF ; Use paging when printing any output. ; If active, Zabbix-CLI will use the pager defined in the environment ; variable PAGER. If this variable is not defined, it will try to use ; the POSIX command 'more' ; ; use_paging: ON, OFF ; Default:OFF use_paging=OFF ; ###################### ; Logging section ; ###################### [logging] ; The user running zabbix-cli has to have RW access to log_file if ; logging is active ; Logging: ON, OFF ; Default: OFF logging=OFF ; Log level: DEBUG, INFO, WARN, ERROR, CRITICAL ; Default: ERROR log_level=INFO ; Log file used by zabbix-cli ; Default: /var/log/zabbix-cli/zabbix-cli.log ;log_file=/var/log/zabbix-cli/zabbix-cli.log unioslo-zabbix-cli-09a2fab/tests/data/zabbix-cli.toml000066400000000000000000000013261471265333400227200ustar00rootroot00000000000000[api] url = "https://zabbix.example.com" username = "Admin" password = "" verify_ssl = true auth_token = "" [app] default_hostgroups = ["All-hosts"] default_admin_usergroups = [] default_create_user_usergroups = [] default_notification_users_usergroups = ["All-notification-users"] export_directory = "/path/to/exports" export_format = "json" export_timestamps = false use_colors = true use_auth_token_file = true auth_token_file = "/path/to/auth_token_file" auth_file = "/path/to/auth_file" use_paging = false output_format = "table" history = true history_file = "/path/to/history_file" allow_insecure_auth_file = true legacy_json_format = false [logging] enabled = true log_level = "ERROR" log_file = "/path/to/log_file" unioslo-zabbix-cli-09a2fab/tests/pyzabbix/000077500000000000000000000000001471265333400207145ustar00rootroot00000000000000unioslo-zabbix-cli-09a2fab/tests/pyzabbix/__init__.py000066400000000000000000000000001471265333400230130ustar00rootroot00000000000000unioslo-zabbix-cli-09a2fab/tests/pyzabbix/test_client.py000066400000000000000000000033721471265333400236100ustar00rootroot00000000000000from __future__ import annotations from typing import Any from typing import Dict import pytest from zabbix_cli.pyzabbix.client import add_param from zabbix_cli.pyzabbix.client import append_param @pytest.mark.parametrize( "inp, key, value, expect", [ pytest.param( {"hostids": 1}, "hostids", 2, {"hostids": [1, 2]}, id="non-list (int)", ), pytest.param( {"hostids": [1]}, "hostids", 2, {"hostids": [1, 2]}, id="list (int)", ), pytest.param( {"hostids": "1"}, "hostids", "2", {"hostids": ["1", "2"]}, id="non-list (str)", ), pytest.param( {"hostids": ["1"]}, "hostids", "2", {"hostids": ["1", "2"]}, id="list (str)", ), pytest.param( {"lists": [[1, 2, 3]]}, "lists", [4, 5, 6], {"lists": [[1, 2, 3], [4, 5, 6]]}, id="list of lists", ), ], ) def test_append_param(inp: Any, key: str, value: Any, expect: Dict[str, Any]) -> None: result = append_param(inp, key, value) assert result == expect # Check in-place modification assert result is inp @pytest.mark.parametrize( "inp, subkey, value, expect", [ ( {"output": "extend"}, "hostids", 2, {"output": "extend", "search": {"hostids": 2}}, ), ], ) def test_add_param(inp: Any, subkey: str, value: Any, expect: Dict[str, Any]) -> None: result = add_param(inp, "search", subkey, value) assert result == expect # Check in-place modification unioslo-zabbix-cli-09a2fab/tests/pyzabbix/test_enums.py000066400000000000000000000110251471265333400234530ustar00rootroot00000000000000from __future__ import annotations from typing import Type import pytest from zabbix_cli.pyzabbix.enums import AckStatus from zabbix_cli.pyzabbix.enums import ActiveInterface from zabbix_cli.pyzabbix.enums import APIStr from zabbix_cli.pyzabbix.enums import APIStrEnum from zabbix_cli.pyzabbix.enums import DataCollectionMode from zabbix_cli.pyzabbix.enums import EventStatus from zabbix_cli.pyzabbix.enums import ExportFormat from zabbix_cli.pyzabbix.enums import GUIAccess from zabbix_cli.pyzabbix.enums import HostgroupFlag from zabbix_cli.pyzabbix.enums import HostgroupType from zabbix_cli.pyzabbix.enums import InterfaceConnectionMode from zabbix_cli.pyzabbix.enums import InterfaceType from zabbix_cli.pyzabbix.enums import InventoryMode from zabbix_cli.pyzabbix.enums import ItemType from zabbix_cli.pyzabbix.enums import MacroType from zabbix_cli.pyzabbix.enums import MaintenanceStatus from zabbix_cli.pyzabbix.enums import MaintenanceType from zabbix_cli.pyzabbix.enums import MaintenanceWeekType from zabbix_cli.pyzabbix.enums import MonitoredBy from zabbix_cli.pyzabbix.enums import MonitoringStatus from zabbix_cli.pyzabbix.enums import ProxyCompatibility from zabbix_cli.pyzabbix.enums import ProxyGroupState from zabbix_cli.pyzabbix.enums import ProxyMode from zabbix_cli.pyzabbix.enums import ProxyModePre70 from zabbix_cli.pyzabbix.enums import SNMPAuthProtocol from zabbix_cli.pyzabbix.enums import SNMPPrivProtocol from zabbix_cli.pyzabbix.enums import SNMPSecurityLevel from zabbix_cli.pyzabbix.enums import TriggerPriority from zabbix_cli.pyzabbix.enums import UsergroupPermission from zabbix_cli.pyzabbix.enums import UserRole from zabbix_cli.pyzabbix.enums import ValueType APISTR_ENUMS = [ AckStatus, ActiveInterface, DataCollectionMode, EventStatus, GUIAccess, HostgroupFlag, HostgroupType, InterfaceConnectionMode, InterfaceType, InventoryMode, ItemType, MacroType, MaintenanceStatus, MaintenanceType, MaintenanceWeekType, MonitoredBy, MonitoringStatus, ProxyCompatibility, ProxyGroupState, ProxyMode, ProxyModePre70, SNMPSecurityLevel, SNMPAuthProtocol, SNMPPrivProtocol, TriggerPriority, UsergroupPermission, UserRole, ValueType, ] @pytest.mark.parametrize("enum", APISTR_ENUMS) def test_apistrenum(enum: Type[APIStrEnum]) -> None: assert enum.__members__ members = list(enum) assert members for member in members: # Narrow down type assert isinstance(member, enum) assert isinstance(member.value, str) assert isinstance(member.value, APIStr) # Methods assert member.as_api_value() is not None assert member.__choice_name__ is not None assert member.__fmt_name__() # non-empty string # Test instantiation assert enum(member) == member assert enum(member.value) == member # NOTE: to support multiple versions of the Zabbix API, some enums # have multiple members with the same API value, and we cannot blindly # test instantiation with the API value for those specific enums. # To not overcomplicate things, we just skip that test for the affected members if member in (SNMPPrivProtocol.AES, SNMPPrivProtocol.AES128): continue assert enum(member.as_api_value()) == member assert enum(member.value.api_value) == member # Test string_from_value for value in [member.as_api_value(), member.value]: s = enum.string_from_value(value) if member.name != "UNKNOWN": assert "Unknown" not in s, f"{value} can't be converted to string" assert s == member.as_status() def test_interfacetype() -> None: # We already test normal behavior in test_apistrenum, check special behavior here for member in InterfaceType: assert member.value.metadata assert member.get_port() assert InterfaceType.AGENT.get_port() == "10050" assert InterfaceType.SNMP.get_port() == "161" assert InterfaceType.IPMI.get_port() == "623" assert InterfaceType.JMX.get_port() == "12345" def test_exportformat() -> None: assert ExportFormat.PHP not in ExportFormat.get_importables() assert ExportFormat("json") == ExportFormat("JSON") assert ExportFormat("xml") == ExportFormat("XML") assert ExportFormat("yaml") == ExportFormat("YAML") assert ExportFormat.JSON in ExportFormat.get_importables() assert ExportFormat.XML in ExportFormat.get_importables() assert ExportFormat.YAML in ExportFormat.get_importables() unioslo-zabbix-cli-09a2fab/tests/pyzabbix/test_types.py000066400000000000000000000103051471265333400234700ustar00rootroot00000000000000from __future__ import annotations from datetime import datetime import pytest from zabbix_cli.pyzabbix.enums import ProxyGroupState from zabbix_cli.pyzabbix.types import CreateHostInterfaceDetails from zabbix_cli.pyzabbix.types import DictModel from zabbix_cli.pyzabbix.types import Event from zabbix_cli.pyzabbix.types import GlobalMacro from zabbix_cli.pyzabbix.types import Host from zabbix_cli.pyzabbix.types import HostGroup from zabbix_cli.pyzabbix.types import HostInterface from zabbix_cli.pyzabbix.types import Image from zabbix_cli.pyzabbix.types import ImportRules from zabbix_cli.pyzabbix.types import Item from zabbix_cli.pyzabbix.types import Macro from zabbix_cli.pyzabbix.types import MacroBase from zabbix_cli.pyzabbix.types import Maintenance from zabbix_cli.pyzabbix.types import Map from zabbix_cli.pyzabbix.types import MediaType from zabbix_cli.pyzabbix.types import ProblemTag from zabbix_cli.pyzabbix.types import Proxy from zabbix_cli.pyzabbix.types import ProxyGroup from zabbix_cli.pyzabbix.types import Role from zabbix_cli.pyzabbix.types import Template from zabbix_cli.pyzabbix.types import TemplateGroup from zabbix_cli.pyzabbix.types import TimePeriod from zabbix_cli.pyzabbix.types import Trigger from zabbix_cli.pyzabbix.types import UpdateHostInterfaceDetails from zabbix_cli.pyzabbix.types import User from zabbix_cli.pyzabbix.types import Usergroup from zabbix_cli.pyzabbix.types import UserMedia from zabbix_cli.pyzabbix.types import ZabbixAPIBaseModel from zabbix_cli.pyzabbix.types import ZabbixRight @pytest.mark.parametrize( "model", [ # TODO: replace with hypothesis tests when Pydantic v2 support is available # Test in order of definition in types.py ZabbixRight(permission=2, id="str"), User(userid="123", username="test"), Usergroup(name="test", usrgrpid="123", gui_access=0, users_status=0), Template(templateid="123", host="test"), TemplateGroup(groupid="123", name="test", uuid="test123"), HostGroup(name="test", groupid="123"), DictModel(), Host(hostid="123"), HostInterface( type=1, main=1, ip="127.0.0.1", dns="", port="10050", useip=1, bulk=1 ), CreateHostInterfaceDetails(version=2), UpdateHostInterfaceDetails(), Proxy(proxyid="123", name="test", address="127.0.0.1"), ProxyGroup( proxy_groupid="123", name="test", description="yeah", failover_delay="60", min_online=1, state=ProxyGroupState.ONLINE, ), MacroBase(macro="foo", value="bar", type=0, description="baz"), Macro( hostid="123", hostmacroid="1234", macro="foo", value="bar", type=0, description="baz", ), GlobalMacro( globalmacroid="123g", macro="foo", value="bar", type=0, description="baz" ), Item(itemid="123"), Role(roleid="123", name="test", type=1, readonly=0), MediaType(mediatypeid="123", name="test", type=0), UserMedia(mediatypeid="123", sendto="foo@example.com"), TimePeriod(period=123, timeperiod_type=2), ProblemTag(tag="foo", operator=2, value="bar"), Maintenance(maintenanceid="123", name="test"), Event( eventid="source", object=1, objectid="123", source=2, acknowledged=0, clock=datetime.now(), name="test", severity=2, ), Trigger(triggerid="123"), Image(imageid="123", name="test", imagetype=1), Map(sysmapid="123", name="test", width=100, height=100), ImportRules.get(), ], ) def test_model_dump(model: ZabbixAPIBaseModel) -> None: """Test that the model can be dumped to JSON.""" try: model.model_dump_json() except Exception as e: pytest.fail(f"Failed to dump model {model} to JSON: {e}") try: model.model_dump_api() except Exception as e: pytest.fail(f"Failed to dump model {model} to API-compatible dict: {e}") try: model.model_dump() except Exception as e: pytest.fail(f"Failed to dump model {model} to dict: {e}") unioslo-zabbix-cli-09a2fab/tests/test_app.py000066400000000000000000000021621471265333400212560ustar00rootroot00000000000000from __future__ import annotations from inline_snapshot import snapshot from pytest import LogCaptureFixture from zabbix_cli.app.app import StatefulApp from zabbix_cli.config.model import PluginConfig from zabbix_cli.state import State def test_get_plugin_config( app: StatefulApp, state: State, caplog: LogCaptureFixture ) -> None: """Test that we can get a plugin's configuration.""" # Add a plugin configuration state.config.plugins.root = { # From module specification "my_commands": PluginConfig( module="path.to.my_commands", ), # From path "my_commands2": PluginConfig( module="path/to/my_commands2.py", ), # From package with entrypoint "my_commands3": PluginConfig(), } # With name config = app.get_plugin_config("my_commands") assert config.module == "path.to.my_commands" # Missing config returns empty config config = app.get_plugin_config("missing") assert config.module == "" assert caplog.records[-1].message == snapshot( "Plugin 'missing' not found in configuration" ) unioslo-zabbix-cli-09a2fab/tests/test_auth.py000066400000000000000000000034521471265333400214420ustar00rootroot00000000000000from __future__ import annotations from pathlib import Path from zabbix_cli._v2_compat import AUTH_FILE as AUTH_FILE_LEGACY from zabbix_cli._v2_compat import AUTH_TOKEN_FILE as AUTH_TOKEN_FILE_LEGACY from zabbix_cli.auth import get_auth_file_paths from zabbix_cli.auth import get_auth_token_file_paths from zabbix_cli.config.constants import AUTH_FILE from zabbix_cli.config.constants import AUTH_TOKEN_FILE from zabbix_cli.config.model import Config def test_get_auth_file_paths_defafult(config: Config) -> None: """Test the default auth file paths.""" # Path from config is not present (same as default AUTH_FILE) assert get_auth_file_paths(config) == [ AUTH_FILE, AUTH_FILE_LEGACY, ] def test_get_auth_file_paths_override(tmp_path: Path, config: Config) -> None: """Test overriding the default auth file path in the config.""" auth_file = tmp_path / "auth" config.app.auth_file = auth_file # Path from config is first (highest priority) assert get_auth_file_paths(config) == [ auth_file, AUTH_FILE, AUTH_FILE_LEGACY, ] def test_get_auth_token_file_paths_defafult(config: Config) -> None: """Test the default auth token file paths.""" # Path from config is not present (same as default AUTH_TOKEN_FILE) assert get_auth_token_file_paths(config) == [ AUTH_TOKEN_FILE, AUTH_TOKEN_FILE_LEGACY, ] def test_get_auth_token_file_paths_override(tmp_path: Path, config: Config) -> None: """Override the default auth token file path in the config.""" auth_file = tmp_path / "auth_token" config.app.auth_token_file = auth_file # Path from config is first (highest priority) assert get_auth_token_file_paths(config) == [ auth_file, AUTH_TOKEN_FILE, AUTH_TOKEN_FILE_LEGACY, ] unioslo-zabbix-cli-09a2fab/tests/test_bulk.py000066400000000000000000000542351471265333400214430ustar00rootroot00000000000000from __future__ import annotations from pathlib import Path from typing import Optional import pytest import typer from inline_snapshot import snapshot from zabbix_cli.app.app import StatefulApp from zabbix_cli.bulk import BulkCommand from zabbix_cli.bulk import BulkRunner from zabbix_cli.bulk import BulkRunnerMode from zabbix_cli.bulk import CommandExecution from zabbix_cli.bulk import CommandResult from zabbix_cli.bulk import CommentLine from zabbix_cli.bulk import EmptyLine from zabbix_cli.exceptions import CommandFileError from zabbix_cli.output.console import exit_err from zabbix_cli.output.console import exit_ok from zabbix_cli.output.console import info @pytest.mark.parametrize( "line, expect", [ pytest.param( "show_zabbixcli_config", BulkCommand(args=["show_zabbixcli_config"], line="show_zabbixcli_config"), id="simple", ), pytest.param( "create_user username name surname passwd role autologin autologout groups", BulkCommand( args=[ "create_user", "username", "name", "surname", "passwd", "role", "autologin", "autologout", "groups", ], line="create_user username name surname passwd role autologin autologout groups", ), id="Legacy positional args", ), pytest.param( "create_user username --firstname name --lastname surname --passwd mypass --role 1 --autologin --autologout 86400 --groups '1,2'", BulkCommand( args=[ "create_user", "username", "--firstname", "name", "--lastname", "surname", "--passwd", "mypass", "--role", "1", "--autologin", "--autologout", "86400", "--groups", "1,2", ], line="create_user username --firstname name --lastname surname --passwd mypass --role 1 --autologin --autologout 86400 --groups '1,2'", ), id="args and kwargs", ), pytest.param( "create_user username myname --passwd mypass surname", BulkCommand( args=[ "create_user", "username", "myname", "--passwd", "mypass", "surname", ], line="create_user username myname --passwd mypass surname", ), id="kwarg between args", ), pytest.param( "create_user myuser --firstname myname --passwd mypasswd --role 1 # comment here --option value", BulkCommand( args=[ "create_user", "myuser", "--firstname", "myname", "--passwd", "mypasswd", "--role", "1", ], line="create_user myuser --firstname myname --passwd mypasswd --role 1 # comment here --option value", ), id="Trailing comment", ), pytest.param( "", BulkCommand(), id="fails (empty)", marks=pytest.mark.xfail(raises=EmptyLine, strict=True), ), pytest.param( "#", BulkCommand(), id="fails (comment symbol)", marks=pytest.mark.xfail(raises=CommentLine, strict=True), ), pytest.param( "# create_user myuser myname mypasswd --role 1", BulkCommand(), id="fails (commented out line)", marks=pytest.mark.xfail(raises=CommentLine, strict=True), ), ], ) def test_bulk_command_from_line(line: str, expect: BulkCommand) -> None: assert BulkCommand.from_line(line) == expect def test_load_command_file(tmp_path: Path, ctx: typer.Context) -> None: """Test loading a command file.""" file = tmp_path / "commands.txt" file.write_text( """# comment show_zabbixcli_config # next line will be blank create_user username --firstname name --lastname surname mypass 1 1 86400 1,2 create_user username --firstname name --lastname surname --passwd mypass --role 1 --autologin --autologout 86400 --groups '1,2' # comment explaining the next command create_user username --firstname name --lastname surname --passwd mypass # trailing comment # Command with flag acknowledge_event 123,456,789 --message "foo message" --close # Command with negative flag show_templategroup mygroup --no-templates # Command with optional boolean flags show_host *.example.com --no-maintenance --monitored # Command with enum option (human-readable string) show_host *.example.com --active available # Command with enum option (API values) show_host *.example.com --active 0 show_host *.example.com --active 1 show_host *.example.com --active 2 # we will end with a blank line """ ) b = BulkRunner(ctx, file) commands = b.load_command_file() assert len(commands) == snapshot(11) assert commands == snapshot( [ BulkCommand( args=["show_zabbixcli_config"], line="show_zabbixcli_config # next line will be blank", line_number=2, ), BulkCommand( args=[ "create_user", "username", "--firstname", "name", "--lastname", "surname", "mypass", "1", "1", "86400", "1,2", ], line="create_user username --firstname name --lastname surname mypass 1 1 86400 1,2", line_number=4, ), BulkCommand( args=[ "create_user", "username", "--firstname", "name", "--lastname", "surname", "--passwd", "mypass", "--role", "1", "--autologin", "--autologout", "86400", "--groups", "1,2", ], line="create_user username --firstname name --lastname surname --passwd mypass --role 1 --autologin --autologout 86400 --groups '1,2'", line_number=5, ), BulkCommand( args=[ "create_user", "username", "--firstname", "name", "--lastname", "surname", "--passwd", "mypass", ], line="create_user username --firstname name --lastname surname --passwd mypass # trailing comment", line_number=7, ), BulkCommand( args=[ "acknowledge_event", "123,456,789", "--message", "foo message", "--close", ], line='acknowledge_event 123,456,789 --message "foo message" --close', line_number=9, ), BulkCommand( args=["show_templategroup", "mygroup", "--no-templates"], line="show_templategroup mygroup --no-templates", line_number=11, ), BulkCommand( args=["show_host", "*.example.com", "--no-maintenance", "--monitored"], line="show_host *.example.com --no-maintenance --monitored", line_number=13, ), BulkCommand( args=["show_host", "*.example.com", "--active", "available"], line="show_host *.example.com --active available", line_number=15, ), BulkCommand( args=["show_host", "*.example.com", "--active", "0"], line="show_host *.example.com --active 0", line_number=17, ), BulkCommand( args=["show_host", "*.example.com", "--active", "1"], line="show_host *.example.com --active 1", line_number=18, ), BulkCommand( args=["show_host", "*.example.com", "--active", "2"], line="show_host *.example.com --active 2", line_number=19, ), ] ) @pytest.mark.parametrize( "mode", [BulkRunnerMode.STRICT, BulkRunnerMode.CONTINUE], ) def test_bulk_runner_mode_invalid_line_strict( tmp_path: Path, ctx: typer.Context, mode: BulkRunnerMode ) -> None: """Test loading a command file with invalid lines in strict/continue mode.""" file = tmp_path / "commands.txt" file.write_text( """\ # comment show_host "*.example.com # Missing closing quote """ ) b = BulkRunner(ctx, file, mode=mode) with pytest.raises(CommandFileError): b.load_command_file() def test_bulk_runner_mode_invalid_line_skip(tmp_path: Path, ctx: typer.Context) -> None: """Test loading a command file with invalid lines in skip mode.""" file = tmp_path / "commands.txt" file.write_text( """\ # comment show_host *.example.com --active 0 show_host "*.example.com # Missing closing quote create_host foo.example.com --hostgroup "Linux servers" """ ) b = BulkRunner(ctx, file, mode=BulkRunnerMode.SKIP) commands = b.load_command_file() assert len(commands) == 2 # show_host, create_host assert len(b.executions) == 0 assert len(b.skipped) == 2 # comment, invalid command # First line is comment # Second line is invalid command result = b.skipped[1] assert result.command == snapshot( BulkCommand( line='show_host "*.example.com # Missing closing quote', line_number=3 ) ) assert result.result == CommandResult.SKIPPED assert repr(result.error) == snapshot("ValueError('No closing quotation')") assert result.line_number == snapshot(3) def test_load_command_file_not_found(tmp_path: Path, ctx: typer.Context) -> None: """Test loading a command file that does not exist.""" file = tmp_path / "commands.txt" assert not file.exists() b = BulkRunner(ctx, file) with pytest.raises(CommandFileError): b.load_command_file() @pytest.mark.parametrize("mode", [BulkRunnerMode.STRICT, BulkRunnerMode.CONTINUE]) def test_bulk_runner_exit_code_handling( tmp_path: Path, app: StatefulApp, ctx: typer.Context, mode: BulkRunnerMode ) -> None: """Test handling of exit codes.""" file = tmp_path / "commands.txt" file.write_text( """\ # comment no_exit exits_ok exits_error """ ) @app.command(name="exits_ok") def exits_ok() -> None: exit_ok("This command exits with code 0") @app.command(name="exits_error") def exits_error() -> None: exit_err("This command exits with code 1") @app.command(name="no_exit") def on_exit() -> None: info("We just print a message here") cmd = typer.main.get_command(app) ctx.command = cmd b = BulkRunner(ctx, file, mode) with pytest.raises(CommandFileError) as excinfo: b.run_bulk() assert len(b.executions) == 3 assert b.executions[0].result == CommandResult.SUCCESS assert b.executions[1].result == CommandResult.SUCCESS assert b.executions[2].result == CommandResult.FAILURE # Differing error messages between strict and continue exc = excinfo.exconly() if mode == BulkRunnerMode.STRICT: assert ( exc == "zabbix_cli.exceptions.CommandFileError: Command failed: [command]exits_error[/]: 1" ) else: assert ( exc == "zabbix_cli.exceptions.CommandFileError: 1 commands failed:\nLine 4: [command]exits_error[/] [i](1)[/]" ) @pytest.mark.parametrize( "mode", [BulkRunnerMode.STRICT, BulkRunnerMode.CONTINUE, BulkRunnerMode.SKIP] ) def test_bulk_commands_complex( tmp_path: Path, app: StatefulApp, ctx: typer.Context, mode: BulkRunnerMode ) -> None: """Test bulk execution of commands with multiple options and arguments.""" file = tmp_path / "commands.txt" file.write_text( """\ # comment # Every type of option and argument mixed_command "some arg" "optional arg" --opt value --reqopt 42 --flag --boolopt mixed_command "some arg" "optional arg" --opt value --reqopt 42 --flag --no-boolopt # Again with short options (no optional arg) mixed_command "some arg" -O value -R 123 -F --boolopt mixed_command "some arg" -O value -R 123 -F --no-boolopt # Omit optional options mixed_command "some arg" -O value -R 123 # Only required arguments/options mixed_command "some arg" --reqopt 42 # Only arguments (with quotes) only_args "arg1" "arg2" "arg3" # Only arguments (no quotes) only_args arg1 arg2 arg3 # Only options only_options --opt1 value --opt2 value --opt3 42 --opt4 42 only_options -O "str" -S "Optional[str]" -I 42 -N 42 """ ) @app.command(name="mixed_command") def mixed_command( ctx: typer.Context, reqarg: str = typer.Argument(), optarg: Optional[str] = typer.Argument(None), opt: Optional[str] = typer.Option(None, "--opt", "-O"), reqopt: int = typer.Option( ..., # type: ignore "--reqopt", "-R", ), flag: bool = typer.Option(False, "--flag", "-F", is_flag=True), boolopt: Optional[bool] = typer.Option( False, # Not specifying anything here should generate the options # --boolopt / --no-boolopt ), ) -> None: """Command with every type of option and argument""" exit_ok("Running mixed_command") @app.command(name="only_args") def only_args( ctx: typer.Context, arg1: str = typer.Argument(), arg2: str = typer.Argument("default value"), arg3: Optional[str] = typer.Argument(None), ) -> None: exit_ok("Running only_args") @app.command(name="only_options") def only_options( ctx: typer.Context, opt1: str = typer.Option(..., "--opt1", "-O"), opt2: Optional[str] = typer.Option(None, "--opt2", "-S"), opt3: int = typer.Option(..., "--opt3", "-I"), opt4: Optional[int] = typer.Option(None, "--opt4", "-N"), ) -> None: exit_ok("Running only_options") cmd = typer.main.get_command(app) ctx.command = cmd b = BulkRunner(ctx, file, mode) b.run_bulk() assert len(b.executions) == snapshot(10) assert b.executions == snapshot( [ CommandExecution( command=BulkCommand( args=[ "mixed_command", "some arg", "optional arg", "--opt", "value", "--reqopt", "42", "--flag", "--boolopt", ], line='mixed_command "some arg" "optional arg" --opt value --reqopt 42 --flag --boolopt', line_number=3, ), result=CommandResult.SUCCESS, line_number=3, ), CommandExecution( command=BulkCommand( args=[ "mixed_command", "some arg", "optional arg", "--opt", "value", "--reqopt", "42", "--flag", "--no-boolopt", ], line='mixed_command "some arg" "optional arg" --opt value --reqopt 42 --flag --no-boolopt', line_number=4, ), result=CommandResult.SUCCESS, line_number=4, ), CommandExecution( command=BulkCommand( args=[ "mixed_command", "some arg", "-O", "value", "-R", "123", "-F", "--boolopt", ], line='mixed_command "some arg" -O value -R 123 -F --boolopt', line_number=6, ), result=CommandResult.SUCCESS, line_number=6, ), CommandExecution( command=BulkCommand( args=[ "mixed_command", "some arg", "-O", "value", "-R", "123", "-F", "--no-boolopt", ], line='mixed_command "some arg" -O value -R 123 -F --no-boolopt', line_number=7, ), result=CommandResult.SUCCESS, line_number=7, ), CommandExecution( command=BulkCommand( args=["mixed_command", "some arg", "-O", "value", "-R", "123"], line='mixed_command "some arg" -O value -R 123', line_number=9, ), result=CommandResult.SUCCESS, line_number=9, ), CommandExecution( command=BulkCommand( args=["mixed_command", "some arg", "--reqopt", "42"], line='mixed_command "some arg" --reqopt 42', line_number=11, ), result=CommandResult.SUCCESS, line_number=11, ), CommandExecution( command=BulkCommand( args=["only_args", "arg1", "arg2", "arg3"], line='only_args "arg1" "arg2" "arg3"', line_number=13, ), result=CommandResult.SUCCESS, line_number=13, ), CommandExecution( command=BulkCommand( args=["only_args", "arg1", "arg2", "arg3"], line="only_args arg1 arg2 arg3", line_number=15, ), result=CommandResult.SUCCESS, line_number=15, ), CommandExecution( command=BulkCommand( args=[ "only_options", "--opt1", "value", "--opt2", "value", "--opt3", "42", "--opt4", "42", ], line="only_options --opt1 value --opt2 value --opt3 42 --opt4 42", line_number=17, ), result=CommandResult.SUCCESS, line_number=17, ), CommandExecution( command=BulkCommand( args=[ "only_options", "-O", "str", "-S", "Optional[str]", "-I", "42", "-N", "42", ], line='only_options -O "str" -S "Optional[str]" -I 42 -N 42', line_number=18, ), result=CommandResult.SUCCESS, line_number=18, ), ] ) assert len(b.skipped) == snapshot(8) assert b.skipped == snapshot( [ CommandExecution( command=BulkCommand(line="# comment", line_number=1), result=CommandResult.SKIPPED, line_number=1, ), CommandExecution( command=BulkCommand( line="# Every type of option and argument", line_number=2 ), result=CommandResult.SKIPPED, line_number=2, ), CommandExecution( command=BulkCommand( line="# Again with short options (no optional arg)", line_number=5 ), result=CommandResult.SKIPPED, line_number=5, ), CommandExecution( command=BulkCommand(line="# Omit optional options", line_number=8), result=CommandResult.SKIPPED, line_number=8, ), CommandExecution( command=BulkCommand( line="# Only required arguments/options", line_number=10 ), result=CommandResult.SKIPPED, line_number=10, ), CommandExecution( command=BulkCommand( line="# Only arguments (with quotes)", line_number=12 ), result=CommandResult.SKIPPED, line_number=12, ), CommandExecution( command=BulkCommand( line="# Only arguments (no quotes)", line_number=14 ), result=CommandResult.SKIPPED, line_number=14, ), CommandExecution( command=BulkCommand(line="# Only options", line_number=16), result=CommandResult.SKIPPED, line_number=16, ), ] ) unioslo-zabbix-cli-09a2fab/tests/test_compat.py000066400000000000000000000056151471265333400217670ustar00rootroot00000000000000from __future__ import annotations import pytest from packaging.version import Version from zabbix_cli.pyzabbix import compat def test_packaging_version_release_sanity(): """Ensures that the `Version.release` tuple is in the correct format and supports users running pre-release versions of Zabbix. """ assert Version("7.0.0").release == (7, 0, 0) # Test that all types of pre-releases evaluate to the same release tuple for pr in ["a1", "b1", "rc1", "alpha1", "beta1"]: assert Version(f"7.0.0{pr}").release == (7, 0, 0) assert Version(f"7.0.0{pr}").release >= (7, 0, 0) assert Version(f"7.0.0{pr}").release <= (7, 0, 0) @pytest.mark.parametrize( "version, expect", [ # TODO (pederhan): decide on a set of versions we test against # instead of coming up with them on the fly, such as here. # Do we test against only major versions or minor versions as well? (Version("7.0.0"), "proxyid"), (Version("6.0.0"), "proxy_hostid"), (Version("5.0.0"), "proxy_hostid"), (Version("3.0.0"), "proxy_hostid"), (Version("2.0.0"), "proxy_hostid"), (Version("1.0.0"), "proxy_hostid"), ], ) def test_host_proxyid(version: Version, expect: str): assert compat.host_proxyid(version) == expect @pytest.mark.parametrize( "version, expect", [ (Version("7.0.0"), "username"), (Version("6.0.0"), "username"), (Version("6.2.0"), "username"), (Version("6.4.0"), "username"), (Version("5.4.0"), "username"), (Version("5.4.1"), "username"), (Version("5.3.9"), "user"), (Version("5.2.0"), "user"), (Version("5.2"), "user"), (Version("5.0"), "user"), (Version("4.0"), "user"), (Version("2.0"), "user"), ], ) def test_login_user_name(version: Version, expect: str): assert compat.login_user_name(version) == expect @pytest.mark.parametrize( "version, expect", [ (Version("7.0.0"), "name"), (Version("6.0.0"), "host"), (Version("5.0.0"), "host"), (Version("3.0.0"), "host"), (Version("2.0.0"), "host"), (Version("1.0.0"), "host"), ], ) def test_proxy_name(version: Version, expect: str): assert compat.proxy_name(version) == expect @pytest.mark.parametrize( "version, expect", [ (Version("7.0.0"), "username"), (Version("6.4.0"), "username"), (Version("6.0.0"), "username"), # NOTE: special case here where we use "alias" instead of "username" # even though it was deprecated in 5.4.0 (matches historical zabbix_cli behavior) (Version("5.4.0"), "alias"), (Version("5.0.0"), "alias"), (Version("3.0.0"), "alias"), (Version("2.0.0"), "alias"), (Version("1.0.0"), "alias"), ], ) def test_user_name(version: Version, expect: str): assert compat.user_name(version) == expect unioslo-zabbix-cli-09a2fab/tests/test_config.py000066400000000000000000000442141471265333400217470ustar00rootroot00000000000000from __future__ import annotations import sys from pathlib import Path from typing import Any from typing import Dict from typing import List from typing import Optional from typing import Type from typing import Union import pytest import tomli from inline_snapshot import snapshot from pydantic import BaseModel from pydantic import Field from pydantic import model_validator from typing_extensions import Self from zabbix_cli.config.constants import OutputFormat from zabbix_cli.config.constants import SecretMode from zabbix_cli.config.model import APIConfig from zabbix_cli.config.model import AppConfig from zabbix_cli.config.model import Config from zabbix_cli.config.model import PluginConfig from zabbix_cli.config.model import PluginsConfig from zabbix_cli.config.utils import DeprecatedField from zabbix_cli.config.utils import get_deprecated_fields_set from zabbix_cli.config.utils import update_deprecated_fields from zabbix_cli.exceptions import ConfigOptionNotFound from zabbix_cli.exceptions import PluginConfigTypeError def test_config_default() -> None: """Assert that the config can be instantiated with no arguments.""" assert Config() def test_sample_config() -> None: """Assert that the sample config can be instantiated.""" assert Config.sample_config() @pytest.mark.parametrize( "bespoke", [True, False], ) def test_load_config_file_legacy(legacy_config_path: Path, bespoke: bool) -> None: if bespoke: conf = Config.from_conf_file(legacy_config_path) else: conf = Config.from_file(legacy_config_path) assert conf # Should be loaded from the file we specified assert conf.config_path == legacy_config_path # Should be marked as legacy assert conf.app.is_legacy is True # Should use legacy JSON format automatically assert conf.app.legacy_json_format is True def remove_path_options(config_path: Path, tmp_path: Path) -> None: """Remove all path options from a TOML config file. Some config options require a directory or file to exist, which is not always possible or desirable in a test environment.""" contents = config_path.read_text() new_contents = "\n".join( line for line in contents.splitlines() if "/path/to" not in line ) config_path.write_text(new_contents) def replace_paths(config_path: Path, tmp_path: Path) -> None: """Replace all /path/to paths in a file with temporary directories.""" contents = config_path.read_text() new_contents = contents.replace("/path/to", str(tmp_path)) tmp_path.mkdir(exist_ok=True) config_path.write_text(new_contents) @pytest.mark.parametrize( "bespoke", [True, False], ) @pytest.mark.parametrize( "with_paths", [True, False], ) def test_load_config_file( config_path: Path, tmp_path: Path, bespoke: bool, with_paths: bool ) -> None: """Test loading a TOML configuration file.""" # Test with and without custom file paths if with_paths: replace_paths(config_path, tmp_path) else: remove_path_options(config_path, tmp_path) # Use bespoke method for loading the given format if bespoke: conf = Config.from_toml_file(config_path) else: conf = Config.from_file(config_path) assert conf # Should be loaded from the file we specified assert conf.config_path == config_path assert conf.app.is_legacy is False assert conf.app.legacy_json_format is False def test_plugins_config_get() -> None: """Test that we can get a plugin configuration.""" config = PluginsConfig( root={ "test1": PluginConfig( enabled=True, module="zabbix_cli.plugins.test1", ), "test2": PluginConfig( enabled=True, module="zabbix_cli.plugins.test2", ), } ) assert config.get("test1") assert config.get("test2") assert not config.get("test3") def test_plugin_config_get() -> None: config = PluginConfig(module="test") assert config.get("module") == "test" # With type validation assert config.get("module", type=str) == "test" # Missing option with pytest.raises(ConfigOptionNotFound): assert config.get("missing") is None # Default value assert config.get("missing", "default") == "default" # Default value with type validation assert config.get("missing", "default", type=str) == "default" # Extra values config = PluginConfig( module="test", extra1="foo", extra2=2, extra3=True, extra4=[1, 2, 3], ) assert config.get("extra1") == "foo" assert config.get("extra2") == 2 assert config.get("extra3") is True assert config.get("extra4") == [1, 2, 3] # With type validation assert config.get("extra1", type=str) == "foo" assert config.get("extra2", type=int) == 2 assert config.get("extra3", type=bool) is True assert config.get("extra4", type=list) == [1, 2, 3] # Type validation with coercion assert config.get("extra3", type=int) == 1 assert config.get("extra4", type=tuple) == (1, 2, 3) # Cannot coerce int to str by default in Pydantic with pytest.raises(PluginConfigTypeError): assert config.get("extra2", type=str) == "2" def test_config_get_with_annotations() -> None: """Test PluginConfig.get with more complex annotations""" config = PluginConfig( module="test", extra1="foo", extra2=2, extra3=True, extra4=[1, 2, 3], extra5={"foo": [1, 2, 3]}, ) assert config.get("extra1", type=Optional[str]) == "foo" assert config.get("extra1", 123, type=Optional[str]) == "foo" assert config.get("extra1", type=Union[str, int]) # Invalid default type with pytest.raises(PluginConfigTypeError): config.get("wrong", 123, type=Optional[str]) assert config.get("wrong", "123", type=Optional[str]) == "123" # List type assert config.get("extra4", type=list) == [1, 2, 3] assert config.get("extra4", type=List[int]) == [1, 2, 3] if sys.version_info >= (3, 9): assert config.get("extra4", type=list[int]) == [1, 2, 3] # Dict type assert config.get("extra5", type=dict) == {"foo": [1, 2, 3]} assert config.get("extra5", type=Dict[str, List[int]]) == {"foo": [1, 2, 3]} if sys.version_info >= (3, 9): assert config.get("extra5", type=dict[str, list[int]]) == {"foo": [1, 2, 3]} def test_plugin_config_set() -> None: config = PluginConfig() # Existing model field config.set("module", "new") assert config.module == "new" # New attribute/field config.set("extra", "new") assert config.extra == "new" # Any type goes obj = object() config.set("object", obj) assert config.object is obj # Instantiated with extra values config = PluginConfig( module="test", extra1="extra1", extra2=2, extra3=True, ) # Override existing values config.set("extra1", "new") config.set("extra2", 3) config.set("extra3", False) assert config.extra1 == "new" assert config.extra2 == 3 assert config.extra3 is False assert config.model_dump() == snapshot( { "module": "test", "enabled": True, "optional": False, "extra1": "new", "extra2": 3, "extra3": False, } ) @pytest.mark.parametrize( "context, expect", [ ({"secrets": SecretMode.HIDE}, SecretMode.HIDE), ({"secrets": SecretMode.MASK}, SecretMode.MASK), ({"secrets": SecretMode.PLAIN}, SecretMode.PLAIN), ({"secrets": "hide"}, SecretMode.HIDE), ({"secrets": "mask"}, SecretMode.MASK), ({"secrets": "plain"}, SecretMode.PLAIN), ({}, SecretMode.MASK), (None, SecretMode.MASK), (lambda: {}, SecretMode.MASK), # type: ignore ({"secrets": True}, SecretMode.PLAIN), ({"secrets": False}, SecretMode.MASK), ], ) def test_secret_mode_from_context(context: Any, expect: SecretMode) -> None: assert SecretMode.from_context(context) == expect def test_secret_mode_default() -> None: assert SecretMode._DEFAULT is SecretMode.MASK # type: ignore def _get_config_with_secrets() -> Config: return Config( api=APIConfig( username="Admin", password="password", auth_token="token123", ) ) def test_config_dump_to_file_masked(tmp_path: Path) -> None: conf_file = tmp_path / "zabbix-cli.toml" conf = _get_config_with_secrets() conf.dump_to_file(conf_file, secrets=SecretMode.MASK) toml_str = conf_file.read_text() config_dict = tomli.loads(toml_str) assert config_dict["api"]["password"] == snapshot("**********") assert config_dict["api"]["auth_token"] == snapshot("**********") def test_deprecated_field_warnings(caplog: pytest.LogCaptureFixture) -> None: """Test that deprecated fields are logged.""" Config( app=AppConfig( output_format=OutputFormat.JSON, use_colors=True, use_paging=True, system_id="System-User", ) ) assert caplog.record_tuples == snapshot( [ ( "zabbix_cli", 30, "Config option [configopt]output_format[/] is deprecated. Use [configopt]app.output.format[/] instead.", ), ( "zabbix_cli", 30, "Config option [configopt]system_id[/] is deprecated. Use [configopt]api.username[/] instead.", ), ( "zabbix_cli", 30, "Config option [configopt]use_colors[/] is deprecated. Use [configopt]app.output.color[/] instead.", ), ( "zabbix_cli", 30, "Config option [configopt]use_paging[/] is deprecated. Use [configopt]app.output.paging[/] instead.", ), ( "zabbix_cli", 30, """\ Your configuration file contains deprecated options. To update your config file with the new options, run: [command]zabbix-cli update_config[/] For more information, see the documentation.\ """, ), ] ) def test_get_deprecated_fields_set() -> None: config = Config( app=AppConfig( output_format=OutputFormat.JSON, use_colors=True, use_paging=True, system_id="System-User", ) ) # Sort because order of `BaseModel.model_fields_set` is not guaranteed assert sorted(get_deprecated_fields_set(config)) == snapshot( [ DeprecatedField( field_name="app.output_format", value=OutputFormat.JSON, replacement="app.output.format", ), DeprecatedField( field_name="app.system_id", value="System-User", replacement="api.username", ), DeprecatedField( field_name="app.use_colors", value=True, replacement="app.output.color" ), DeprecatedField( field_name="app.use_paging", value=True, replacement="app.output.paging" ), ] ) def test_get_deprecated_fields_deep_nesting() -> None: """Test that deprecated fields are found in nested models.""" class Baz(BaseModel): qux: str = "" class Bar(BaseModel): baz: Baz = Field(default_factory=Baz) qux_deprecated: str = Field( default="", deprecated=True, json_schema_extra={"replacement": "bar.baz.qux"}, ) class Foo(BaseModel): bar: Bar = Field(default_factory=Bar) # Copied from zabbix_cli.config.model.Config # TODO: to test this better, we could declare a new BaseModel # subclass intended for top-level models which automatically implement # this validator. That way we can be sure the validator behavior # in the test is consistent with the behavior in the actual code. @model_validator(mode="after") def _set_deprecated_fields_in_new_location(self) -> Self: """Set deprecated fields in their new location.""" update_deprecated_fields(self) return self foo = Foo(bar=Bar(qux_deprecated="test123")) # Deprecated field should be found assert sorted(get_deprecated_fields_set(foo)) == snapshot( [ DeprecatedField( field_name="bar.qux_deprecated", value="test123", replacement="bar.baz.qux", ) ] ) # Deprecated field should be automatically assigned to the new field assert foo.model_dump() == snapshot( {"bar": {"baz": {"qux": "test123"}, "qux_deprecated": "test123"}} ) def test_deprecated_fields_updated() -> None: """Test that deprecated fields are assigned to the new fields if specified.""" conf = Config( app=AppConfig( # Set fields to non-default values for comparison output_format=OutputFormat.JSON, use_colors=False, use_paging=True, system_id="System-User", ) ) assert sorted(conf.app.model_fields_set) == snapshot( ["output_format", "system_id", "use_colors", "use_paging"] ) assert sorted(conf.app.output.model_fields_set) == snapshot( ["color", "format", "paging"] ) assert conf.app.output.format == OutputFormat.JSON assert conf.app.output.color is False assert conf.app.output.paging is True assert conf.api.username == "System-User" def get_deprecated_fields(model: Union[Type[BaseModel], BaseModel]) -> List[str]: """Get a set of names of deprecated fields in a model and its submodels.""" fields: List[str] = [] for field_name, field in model.model_fields.items(): if field.deprecated: fields.append(field_name) if not field.annotation: continue try: if issubclass(field.annotation, BaseModel): submodel_fields = get_deprecated_fields(field.annotation) fields.extend( f"{field_name}.{subfield}" for subfield in submodel_fields ) except TypeError: pass return fields def test_get_deprecated_fields() -> None: """Ensure we are aware of all deprecated fields in the Config model""" assert get_deprecated_fields(Config) == snapshot( ["app.output_format", "app.use_colors", "app.use_paging", "app.system_id"] ) def test_load_deprecated_config(tmp_path: Path) -> None: conf = tmp_path / "zabbix-cli.toml" conf.write_text( f""" [api] url = "http://localhost:8080" # No username option specified (assigned from app.system_id) password = "" auth_token = "" verify_ssl = true [app] default_hostgroups = ["All-hosts"] default_admin_usergroups = [] default_create_user_usergroups = [] default_notification_users_usergroups = ["All-notification-users"] export_directory = "{tmp_path}/exports" export_format = "json" export_timestamps = false use_auth_token_file = true auth_token_file = "{tmp_path}/.zabbix-cli_auth_token" auth_file = "{tmp_path}/.zabbix-cli_auth" history = true history_file = "{tmp_path}/history" bulk_mode = "strict" # Deprecated options (moved) use_colors = false use_paging = true output_format = "json" system_id = "System-User" # Deprecated options (slated for removal) allow_insecure_auth_file = true legacy_json_format = false # No [app.output] section specified (assigned from deprecated app options) [logging] enabled = true log_level = "DEBUG" log_file = "{tmp_path}/zabbix-cli.log" [plugins] """ ) # Check that we can actually load the config config = Config.from_file(conf) # Check that the deprecated fields are assigned to the new fields assert config.app.output.color is False assert config.app.output.paging is True assert config.app.output.format == OutputFormat.JSON assert config.api.username == "System-User" def test_load_deprecated_config_with_new_and_old_options(tmp_path: Path) -> None: """Test loading a config file where both new and deprecated options are present. The deprecated options should _not_ be assigned to the new options, as the new options are already set""" conf = tmp_path / "zabbix-cli.toml" conf.write_text( """ [api] username = "Admin" [app] use_colors = false use_paging = true output_format = "json" system_id = "System-User" [app.output] color = true format = "table" paging = false """ ) config = Config.from_file(conf) # New fields should NOT be overwritten by deprecated fields assert config.api.username == "Admin" assert config.app.output.color is True assert config.app.output.paging is False assert config.app.output.format == OutputFormat.TABLE def test_load_deprecated_config_legacy(legacy_config_path: Path) -> None: """Test loading a legacy .conf config file with deprecated options.""" config_str = legacy_config_path.read_text() assert "system_id=Test" in config_str assert "use_colors=ON" in config_str assert "use_paging=OFF" in config_str # Manipulate config to set default boolean values to opposite config_str = config_str.replace("use_colors=ON", "use_colors=OFF") config_str = config_str.replace("use_paging=OFF", "use_paging=ON") legacy_config_path.write_text(config_str) config = Config.from_conf_file(legacy_config_path) # Check that the deprecated fields are assigned to the new fields assert config.api.username == "Test" assert config.app.output.color is False assert config.app.output.paging is True # Check that the assigned fields are counted as set assert "username" in config.api.model_fields_set assert "color" in config.app.output.model_fields_set assert "paging" in config.app.output.model_fields_set assert "format" not in config.app.output.model_fields_set # Check that the deprecated fields are also set assert "system_id" in config.app.model_fields_set assert "use_colors" in config.app.model_fields_set assert "use_paging" in config.app.model_fields_set assert "output_format" not in config.app.model_fields_set unioslo-zabbix-cli-09a2fab/tests/test_console.py000066400000000000000000000164061471265333400221460ustar00rootroot00000000000000from __future__ import annotations import logging from typing import Any from typing import Dict import pytest from inline_snapshot import snapshot from zabbix_cli.config.constants import OutputFormat from zabbix_cli.exceptions import ZabbixAPIException from zabbix_cli.exceptions import ZabbixAPIRequestError from zabbix_cli.output.console import RESERVED_EXTRA_KEYS from zabbix_cli.output.console import debug from zabbix_cli.output.console import debug_kv from zabbix_cli.output.console import error from zabbix_cli.output.console import exit_err from zabbix_cli.output.console import get_extra_dict from zabbix_cli.output.console import info from zabbix_cli.output.console import success from zabbix_cli.output.console import warning from zabbix_cli.pyzabbix.types import ZabbixAPIError from zabbix_cli.pyzabbix.types import ZabbixAPIResponse from zabbix_cli.state import State @pytest.mark.parametrize( "inp,expect", [ # No extras ({}, {}), # 1 extra ({"name": "foo"}, {"name_": "foo"}), # 2 extras ({"name": "foo", "level": "DEBUG"}, {"name_": "foo", "level_": "DEBUG"}), # 3 extras (2 reserved) ( {"name": "foo", "level": "DEBUG", "key": "value"}, {"name_": "foo", "level_": "DEBUG", "key": "value"}, ), ], ) def test_get_extra_dict(inp: Dict[str, Any], expect: Dict[str, Any]) -> None: extra = get_extra_dict(**inp) assert extra == expect def test_get_extra_dict_reserved_keys() -> None: """Test that all reserved keys are renamed.""" d: Dict[str, Any] = {} for key in RESERVED_EXTRA_KEYS: d[key] = key extra = get_extra_dict(**d) assert extra == snapshot( { "name_": "name", "level_": "level", "pathname_": "pathname", "lineno_": "lineno", "msg_": "msg", "args_": "args", "exc_info_": "exc_info", "func_": "func", "sinfo_": "sinfo", } ) def test_debug_kv( capsys: pytest.CaptureFixture[str], caplog: pytest.LogCaptureFixture, state: State ) -> None: state.config.logging.log_level = "DEBUG" caplog.set_level(logging.DEBUG) debug_kv("some", "error") captured = capsys.readouterr() assert captured.err == snapshot("some : error\n") assert caplog.record_tuples == snapshot( [("zabbix_cli", 10, "some : error")] ) assert caplog.records[0].funcName == snapshot("test_debug_kv") def test_debug( capsys: pytest.CaptureFixture[str], caplog: pytest.LogCaptureFixture, state: State ) -> None: state.config.logging.log_level = "DEBUG" caplog.set_level(logging.DEBUG) debug("Some error") captured = capsys.readouterr() assert captured.err == snapshot("Some error\n") assert caplog.record_tuples == snapshot([("zabbix_cli", 10, "Some error")]) assert caplog.records[0].funcName == snapshot("test_debug") def test_info( capsys: pytest.CaptureFixture[str], caplog: pytest.LogCaptureFixture ) -> None: caplog.set_level(logging.INFO) info("Some error") captured = capsys.readouterr() assert captured.err == snapshot("! Some error\n") assert caplog.record_tuples == snapshot([("zabbix_cli", 20, "Some error")]) assert caplog.records[0].funcName == snapshot("test_info") def test_success( capsys: pytest.CaptureFixture[str], caplog: pytest.LogCaptureFixture ) -> None: caplog.set_level(logging.INFO) success("Some error") captured = capsys.readouterr() assert captured.err == snapshot("✓ Some error\n") assert caplog.record_tuples == snapshot([("zabbix_cli", 20, "Some error")]) assert caplog.records[0].funcName == snapshot("test_success") def test_warning( capsys: pytest.CaptureFixture[str], caplog: pytest.LogCaptureFixture ) -> None: caplog.set_level(logging.INFO) warning("Some error") captured = capsys.readouterr() assert captured.err == snapshot("⚠ Some error\n") assert caplog.record_tuples == snapshot([("zabbix_cli", 30, "Some error")]) assert caplog.records[0].funcName == snapshot("test_warning") def test_error( capsys: pytest.CaptureFixture[str], caplog: pytest.LogCaptureFixture ) -> None: caplog.set_level(logging.INFO) error("Some error") captured = capsys.readouterr() assert captured.err == snapshot("✗ ERROR: Some error\n") assert caplog.record_tuples == snapshot([("zabbix_cli", 40, "Some error")]) assert caplog.records[0].funcName == snapshot("test_error") def test_exit_err_table( capsys: pytest.CaptureFixture[str], caplog: pytest.LogCaptureFixture, state: State ) -> None: state.config.app.output.format = OutputFormat.TABLE caplog.set_level(logging.INFO) with pytest.raises(SystemExit): exit_err("Some error") captured = capsys.readouterr() assert captured.err == snapshot("✗ ERROR: Some error\n") assert captured.out == snapshot("") def test_exit_err_json( capsys: pytest.CaptureFixture[str], caplog: pytest.LogCaptureFixture, state: State ) -> None: state.config.app.output.format = OutputFormat.JSON with pytest.raises(SystemExit): exit_err("Some error") captured = capsys.readouterr() assert captured.err == snapshot("✗ ERROR: Some error\n") assert captured.out == snapshot( """\ { "message": "Some error", "errors": [], "return_code": "Error", "result": null } """ ) assert caplog.record_tuples == snapshot([("zabbix_cli", 40, "Some error")]) def test_exit_err_json_with_errors( capsys: pytest.CaptureFixture[str], caplog: pytest.LogCaptureFixture, state: State ) -> None: outer_exc = TypeError("Outer exception") outer_exc.__cause__ = ValueError("Inner exception") state.config.app.output.format = OutputFormat.JSON with pytest.raises(SystemExit): exit_err("Some error", exception=outer_exc) captured = capsys.readouterr() assert captured.err == snapshot("✗ ERROR: Some error\n") assert captured.out == snapshot( """\ { "message": "Some error", "errors": [ "Outer exception", "Inner exception" ], "return_code": "Error", "result": null } """ ) assert caplog.record_tuples == snapshot([("zabbix_cli", 40, "Some error")]) def test_exit_err_json_with_zabbix_api_request_error( capsys: pytest.CaptureFixture[str], caplog: pytest.LogCaptureFixture, state: State ) -> None: api_resp = ZabbixAPIResponse( jsonrpc="2.0", result=None, id=1, error=ZabbixAPIError( code=-123, message="API response error", data='{"foo": 42}' ), ) outer_exc = ZabbixAPIException("Outer exception") outer_exc.__cause__ = ZabbixAPIRequestError( "Inner exception", api_response=api_resp ) state.config.app.output.format = OutputFormat.JSON with pytest.raises(SystemExit): exit_err("Some error", exception=outer_exc) captured = capsys.readouterr() assert captured.err == snapshot("✗ ERROR: Some error\n") assert captured.out == snapshot( """\ { "message": "Some error", "errors": [ "Outer exception", "Inner exception", "(-123) API response error {\\"foo\\": 42}" ], "return_code": "Error", "result": null } """ ) assert caplog.record_tuples == snapshot([("zabbix_cli", 40, "Some error")]) unioslo-zabbix-cli-09a2fab/tests/test_docs/000077500000000000000000000000001471265333400210535ustar00rootroot00000000000000unioslo-zabbix-cli-09a2fab/tests/test_docs/__init__.py000066400000000000000000000000001471265333400231520ustar00rootroot00000000000000unioslo-zabbix-cli-09a2fab/tests/test_docs/test_markup.py000066400000000000000000000144221471265333400237660ustar00rootroot00000000000000from __future__ import annotations import sys from pathlib import Path import pytest sys.path.append(str((Path(__file__) / "../../../docs").resolve())) # Skip entire test if not found markup = pytest.importorskip("docs.scripts.utils.markup") MarkdownSymbol = markup.MarkdownSymbol # type: ignore @pytest.mark.parametrize( "inp, expect", [ ( MarkdownSymbol( position=0, italic=True, bold=False, code=False, codeblock=False, end=False, ), "*", ), ( MarkdownSymbol( position=0, italic=False, bold=True, code=False, codeblock=False, end=False, ), "**", ), ( MarkdownSymbol( position=0, italic=False, bold=False, code=True, codeblock=False, end=False, ), "`", ), ( MarkdownSymbol( position=0, italic=False, bold=False, code=False, codeblock=True, end=False, ), "```\n", ), ( MarkdownSymbol( position=0, italic=True, bold=True, code=False, codeblock=False, end=False, ), "***", ), ( MarkdownSymbol( position=0, italic=True, bold=False, code=True, codeblock=False, end=False, ), "*`", ), ( MarkdownSymbol( position=0, italic=True, bold=False, code=False, codeblock=True, end=False, ), "```\n", ), ( MarkdownSymbol( position=0, italic=False, bold=True, code=True, codeblock=False, end=False, ), "**`", ), ( MarkdownSymbol( position=0, italic=False, bold=True, code=False, codeblock=True, end=False, ), "```\n", ), ( MarkdownSymbol( position=0, italic=False, bold=False, code=True, codeblock=True, end=False, ), "```\n", ), ( MarkdownSymbol( position=0, italic=True, bold=True, code=True, codeblock=False, end=False, ), "***`", ), ( MarkdownSymbol( position=0, italic=True, bold=True, code=False, codeblock=True, end=False, ), "```\n", ), ( MarkdownSymbol( position=0, italic=True, bold=True, code=True, codeblock=True, end=False ), "```\n", ), ], ) def test_markdownsymbol_symbol( inp: MarkdownSymbol, # type: ignore expect: str, ) -> None: assert inp.symbol == expect # type: ignore @pytest.mark.parametrize( "inp,expect", [ pytest.param( "[example]zabbix-cli --version[/]", "```\nzabbix-cli --version\n```", id="Example (short end)", ), pytest.param( "[example]zabbix-cli --version[/example]", "```\nzabbix-cli --version\n```", # Same result as above id="Example (explicit end style)", ), pytest.param( "[bold example]zabbix-cli --version[/bold example]", "```\nzabbix-cli --version\n```", # Ignoring bold id="Example + bold", ), pytest.param( "[example python]zabbix-cli --version[/example python]", "```py\nzabbix-cli --version\n```", # Adding language id="Example with language", ), pytest.param( " [example]zabbix-cli --version[/example]", "```\nzabbix-cli --version\n```", # Ignoring leading spaces id="Leading spaces outside style", ), pytest.param( "[example] zabbix-cli --version[/example]", "```\nzabbix-cli --version\n```", # Ignoring leading spaces inside id="Leading spaces inside style", ), ], ) def test_markup_to_markdown(inp: str, expect: str) -> None: assert markup.markup_to_markdown(inp) == expect # type: ignore @pytest.mark.parametrize( "style", markup.CODE_STYLES, ) def test_markup_to_markdown_code_styles(style: str) -> None: expect = "`--version`" assert markup.markup_to_markdown(f"[{style}]--version[/]") == expect # type: ignore # Adding bold or italic doesnt change anything # code takes precedence assert markup.markup_to_markdown(f"[bold {style}]--version[/]") == expect assert markup.markup_to_markdown(f"[italic {style}]--version[/]") == expect assert markup.markup_to_markdown(f"[bold italic {style}]--version[/]") == expect @pytest.mark.parametrize( "style", markup.CODEBLOCK_STYLES, ) def test_markup_to_markdown_codeblock_styles(style: str) -> None: text = "zabbix-cli remove_hostgroup foo" expect = f"```\n{text}\n```" assert markup.markup_to_markdown(f"[{style}]{text}[/]") == expect # type: ignore # Adding bold or italic doesnt change anything # code takes precedence assert markup.markup_to_markdown(f"[italic {style}]{text}[/]") == expect assert markup.markup_to_markdown(f"[bold {style}]{text}[/]") == expect assert markup.markup_to_markdown(f"[bold {style} italic]{text}[/]") == expect # TODO: test the example spans of all commands and ensure they render as codeblocks unioslo-zabbix-cli-09a2fab/tests/test_exceptions.py000066400000000000000000000036641471265333400226670ustar00rootroot00000000000000from __future__ import annotations from typing import Type import pytest from inline_snapshot import snapshot from zabbix_cli.exceptions import ZabbixAPIException from zabbix_cli.exceptions import ZabbixAPIRequestError from zabbix_cli.exceptions import ZabbixCLIError from zabbix_cli.exceptions import get_cause_args from zabbix_cli.pyzabbix.types import ZabbixAPIError from zabbix_cli.pyzabbix.types import ZabbixAPIResponse @pytest.mark.parametrize( "outer_t", [TypeError, ValueError, ZabbixCLIError, ZabbixAPIException] ) def test_get_cause_args(outer_t: Type[Exception]) -> None: try: try: try: raise ZabbixAPIException("foo!") except ZabbixAPIException as e: raise TypeError("foo", "bar") from e except TypeError as e: raise outer_t("outer") from e except outer_t as e: args = get_cause_args(e) assert args == snapshot(["outer", "foo", "bar", "foo!"]) def test_get_cause_args_no_cause() -> None: e = ZabbixAPIException("foo!") args = get_cause_args(e) assert args == snapshot(["foo!"]) def test_get_cause_args_with_api_response() -> None: api_resp = ZabbixAPIResponse( jsonrpc="2.0", result=None, id=1, error=ZabbixAPIError(code=-123, message="Some error"), ) e = ZabbixAPIRequestError("foo!", api_response=api_resp) args = get_cause_args(e) assert args == snapshot(["foo!", "(-123) Some error"]) def test_get_cause_args_with_api_response_with_data() -> None: """Get the cause args from an exception with an API response with data.""" api_resp = ZabbixAPIResponse( jsonrpc="2.0", result=None, id=1, error=ZabbixAPIError(code=-123, message="Some error", data='{"foo": 42}'), ) e = ZabbixAPIRequestError("foo!", api_response=api_resp) args = get_cause_args(e) assert args == snapshot(["foo!", '(-123) Some error {"foo": 42}']) unioslo-zabbix-cli-09a2fab/tests/test_logging.py000066400000000000000000000043721471265333400221310ustar00rootroot00000000000000from __future__ import annotations import logging import unittest import zabbix_cli.logs logger = logging.getLogger(__name__) class CollectHandler(logging.Handler): def __init__(self): super().__init__(logging.NOTSET) self.records = [] def emit(self, record): print("emit", repr(record)) self.records.append(record) class TestSafeFormatting(unittest.TestCase): def _make_log_record(self, msg, *args): # locals for readability record_logger = "example-logger-name" record_level = logging.ERROR record_pathname = __file__ record_lineno = 1 record_exc_info = None return logging.LogRecord( record_logger, record_level, record_pathname, record_lineno, msg, args, record_exc_info, ) def _make_safe_record(self, msg, *args): return zabbix_cli.logs.SafeRecord(self._make_log_record(msg, *args)) def test_safe_record(self): self._make_safe_record("foo") self.assertTrue(True) # reached def test_safe_record_basic_message(self): message = "foo" record = self._make_safe_record(message) self.assertEqual(message, record.getMessage()) def test_safe_record_formatted_message(self): expect = "foo 01 02" record = self._make_safe_record("foo %02d %02d", 1, 2) self.assertEqual(expect, record.getMessage()) def test_safe_record_attr(self): msg = ("foo",) args = (1, 2) record = self._make_safe_record(msg, *args) self.assertEqual(msg, record.msg) self.assertEqual(args, record.args) def test_safe_record_missing_dict(self): fmt = "%(msg)s-%(this_probably_does_not_exist)s" record = self._make_safe_record("foo") expect = f"foo-{None}" result = fmt % record.__dict__ self.assertEqual(expect, result) def test_safe_formatter(self): fmt = "%(name)s - %(something)s - %(msg)s" formatter = zabbix_cli.logs.SafeFormatter(fmt) record = self._make_log_record("foo") expect = fmt % {"name": record.name, "something": None, "msg": record.msg} self.assertEqual(expect, formatter.format(record)) unioslo-zabbix-cli-09a2fab/tests/test_models.py000066400000000000000000000060671471265333400217710ustar00rootroot00000000000000from __future__ import annotations import logging from typing import List import pytest from inline_snapshot import snapshot from pydantic import BaseModel from pydantic import Field from pytest import LogCaptureFixture from zabbix_cli.models import MetaKey from zabbix_cli.models import TableRenderable @pytest.mark.parametrize( "header, expect", [ pytest.param(None, "Foo", id="None is default"), pytest.param("", "Foo", id="Empty string is default"), ("Foo Header", "Foo Header"), ], ) def test_table_renderable_metakey_header(header: str, expect: str) -> None: class TestTableRenderable(TableRenderable): foo: str = Field(..., json_schema_extra={MetaKey.HEADER: header}) t = TestTableRenderable(foo="bar") assert t.__cols__() == [expect] assert t.__rows__() == [["bar"]] assert t.__cols_rows__() == ([expect], [["bar"]]) @pytest.mark.parametrize( "content, join_char, expect", [ (["a", "b", "c"], ",", ["a,b,c"]), (["a", "b", "c"], "|", ["a|b|c"]), (["a", "b", "c"], " ", ["a b c"]), (["a", "b", "c"], "", ["abc"]), # Test empty list ([], ",", [""]), ([], "|", [""]), ([], " ", [""]), ([], "", [""]), ], ) def test_table_renderable_metakey_join_char( content: List[str], join_char: str, expect: str ) -> None: class TestTableRenderable(TableRenderable): foo: List[str] = Field(..., json_schema_extra={MetaKey.JOIN_CHAR: join_char}) t = TestTableRenderable(foo=content) assert t.__rows__() == [expect] def test_all_metakeys() -> None: class TestTableRenderable(TableRenderable): foo: List[str] = Field( ..., json_schema_extra={MetaKey.JOIN_CHAR: "|", MetaKey.HEADER: "Foo Header"}, ) t = TestTableRenderable(foo=["foo", "bar"]) assert t.__cols__() == ["Foo Header"] assert t.__rows__() == [["foo|bar"]] assert t.__cols_rows__() == (["Foo Header"], [["foo|bar"]]) def test_rows_with_unknown_base_model(caplog: LogCaptureFixture) -> None: """Test that we log when we try to render a BaseModel instance that does not inherit from TableRenderable.""" class FooModel(BaseModel): foo: str bar: int baz: float qux: List[str] class TestTableRenderable(TableRenderable): foo: FooModel t = TestTableRenderable(foo=FooModel(foo="foo", bar=1, baz=1.0, qux=["a", "b"])) caplog.set_level(logging.WARNING) # Non-TableRenderable models are rendered as JSON assert t.__rows__() == snapshot( [ [ """\ { "foo": "foo", "bar": 1, "baz": 1.0, "qux": [ "a", "b" ] }\ """ ] ] ) # Check that we logged info on what happened and how we got there assert caplog.record_tuples == snapshot( [("zabbix_cli", 30, "Cannot render FooModel as a table.")] ) record = caplog.records[0] assert record.funcName == "__rows__" assert record.stack_info is not None assert "test_rows_with_unknown_base_model" in record.stack_info unioslo-zabbix-cli-09a2fab/tests/test_prompts.py000066400000000000000000000042731471265333400222070ustar00rootroot00000000000000from __future__ import annotations import os import pytest from zabbix_cli.output.prompts import HEADLESS_VARS_SET from zabbix_cli.output.prompts import TRUE_ARGS from zabbix_cli.output.prompts import is_headless @pytest.mark.parametrize("envvar", HEADLESS_VARS_SET) @pytest.mark.parametrize("value", TRUE_ARGS) def test_is_headless_set_true(envvar: str, value: str): """Returns True when the envvar is set to a truthy value.""" _do_test_is_headless(envvar, value, True) @pytest.mark.parametrize("envvar", HEADLESS_VARS_SET) @pytest.mark.parametrize("value", ["0", "false", "", None]) def test_is_headless_set_false(envvar: str, value: str): """Returns False when the envvar is set to a falsey value or unset""" _do_test_is_headless(envvar, value, False) @pytest.mark.parametrize( "envvar, value, expected", [ ("DEBIAN_FRONTEND", "noninteractive", True), ("DEBIAN_FRONTEND", "teletype", False), ("DEBIAN_FRONTEND", "readline", False), ("DEBIAN_FRONTEND", "dialog", False), ("DEBIAN_FRONTEND", "gtk", False), ("DEBIAN_FRONTEND", "text", False), ("DEBIAN_FRONTEND", "anything", False), ("DEBIAN_FRONTEND", None, False), ("DEBIAN_FRONTEND", "", False), ("DEBIAN_FRONTEND", "0", False), ("DEBIAN_FRONTEND", "false", False), ("DEBIAN_FRONTEND", "1", False), ("DEBIAN_FRONTEND", "true", False), ], ) def test_is_headless_map(envvar: str, value: str, expected: bool) -> None: """Returns True when the envvar is set to a specific value.""" _do_test_is_headless(envvar, value, expected) def _do_test_is_headless(envvar: str, value: str | None, expected: bool): """Helper function for testing is_headless. Sets/clears envvar before testing, then clears cache and envvar after test. """ _orig_environ = os.environ.copy() os.environ.clear() try: if value is None: os.environ.pop(envvar, None) else: os.environ[envvar] = value assert is_headless() == expected finally: # IMPORTANT: Remove envvar and clear cache after each test os.environ = _orig_environ # type: ignore is_headless.cache_clear() unioslo-zabbix-cli-09a2fab/tests/test_style.py000066400000000000000000000007251471265333400216410ustar00rootroot00000000000000from __future__ import annotations from zabbix_cli.output.style import Color def test_color_call() -> None: assert Color.INFO("test") == "[default]test[/]" assert Color.SUCCESS("test") == "[green]test[/]" assert Color.WARNING("test") == "[yellow]test[/]" assert Color.ERROR("test") == "[red]test[/]" # Ensure it doesnt break normal instantiation with a value assert Color("default") == Color.INFO assert Color("yellow") == Color.WARNING unioslo-zabbix-cli-09a2fab/tests/test_update.py000066400000000000000000000015521471265333400217620ustar00rootroot00000000000000from __future__ import annotations import pytest from zabbix_cli.update import PyInstallerUpdater @pytest.mark.parametrize( "os, arch, version, expect_info", [ ("linux", "x86_64", "1.2.3", "1.2.3-linux-x86_64"), ("linux", "arm64", "1.2.3", "1.2.3-linux-arm64"), ("linux", "armv7l", "1.2.3", "1.2.3-linux-armv7l"), ("darwin", "x86_64", "1.2.3", "1.2.3-macos-x86_64"), ("darwin", "arm64", "1.2.3", "1.2.3-macos-arm64"), ("win32", "x86_64", "1.2.3", "1.2.3-win-x86_64.exe"), ], ) def test_pyinstaller_updater_get_url( os: str, arch: str, version: str, expect_info: str ): BASE_URL = ( "https://github.com/unioslo/zabbix-cli/releases/latest/download/zabbix-cli" ) expect_url = f"{BASE_URL}-{expect_info}" url = PyInstallerUpdater.get_url(os, arch, version) assert url == expect_url unioslo-zabbix-cli-09a2fab/tests/test_utils.py000066400000000000000000000116021471265333400216350ustar00rootroot00000000000000from __future__ import annotations from datetime import datetime from datetime import timedelta from typing import Tuple import pytest from freezegun import freeze_time from zabbix_cli.utils import convert_duration from zabbix_cli.utils.utils import convert_time_to_interval from zabbix_cli.utils.utils import convert_timestamp from zabbix_cli.utils.utils import convert_timestamp_interval @pytest.mark.parametrize( "input,expect", [ ( "1 hour", timedelta(hours=1), ), ( "1 hour 30 minutes", timedelta(hours=1, minutes=30), ), ( "1 hour 30 minutes 30 seconds", timedelta(hours=1, minutes=30, seconds=30), ), ( "1 day 1 hour 30 minutes 30 seconds", timedelta(days=1, hours=1, minutes=30, seconds=30), ), ( "2 hours 30 seconds", timedelta(hours=2, seconds=30), ), ( "2h30s", timedelta(hours=2, seconds=30), ), ( "2 hours 30 minutes 30 seconds", timedelta(hours=2, minutes=30, seconds=30), ), ( "2 hour 30 minute 30 second", timedelta(hours=2, minutes=30, seconds=30), ), ( "2h30m30s", timedelta(hours=2, minutes=30, seconds=30), ), ( "9030s", timedelta(seconds=9030), ), ( "9030", timedelta(seconds=9030), ), ( "0", timedelta(seconds=0), ), ( "0d0h0m0s", timedelta(days=0, hours=0, minutes=0, seconds=0), ), ], ) def test_convert_duration(input: str, expect: timedelta) -> None: assert convert_duration(input) == expect assert convert_duration(input).total_seconds() == expect.total_seconds() @pytest.mark.parametrize( "input, expect", [ pytest.param( "2016-11-21T22:00 to 2016-11-21T23:00", (datetime(2016, 11, 21, 22, 0, 0), datetime(2016, 11, 21, 23, 0, 0)), id="Legacy format", # (ISO w/o timezone and seconds) ), pytest.param( "2016-11-21T22:00:00 to 2016-11-21T23:00:00", (datetime(2016, 11, 21, 22, 0, 0), datetime(2016, 11, 21, 23, 0, 0)), id="ISO w/o timezone", ), pytest.param( "2016-11-21 22:00:00 to 2016-11-21 23:00:00", (datetime(2016, 11, 21, 22, 0, 0), datetime(2016, 11, 21, 23, 0, 0)), id="ISO w/o timezone (space-separated)", ), ], ) def test_convert_timestamp_interval( input: str, expect: Tuple[datetime, datetime] ) -> None: assert convert_timestamp_interval(input) == expect # TODO: test with mix of formats. e.g. "2016-11-21T22:00 to 2016-11-21 23:00:00" @pytest.mark.parametrize( "input,expect", [ pytest.param( "2016-11-21T22:00", datetime(2016, 11, 21, 22, 0, 0), id="Legacy", ), pytest.param( "2016-11-21T22:00:00", datetime(2016, 11, 21, 22, 0, 0), id="ISO w/o timezone", ), pytest.param( "2016-11-21 22:00:00", datetime(2016, 11, 21, 22, 0, 0), id="ISO w/o timezone (space-separated)", ), ], ) def test_convert_timestamp(input: str, expect: datetime) -> None: assert convert_timestamp(input) == expect @pytest.mark.parametrize( "input,expect_duration", [ pytest.param( "2016-11-21T22:00 to 2016-11-21T23:00", timedelta(hours=1), id="Range: Legacy", ), pytest.param( "2016-11-21T22:00:00 to 2016-11-21T23:00:00", timedelta(hours=1), id="Range: ISO w/o timezone", ), pytest.param( "2016-11-21 22:00:00 to 2016-11-21 23:00:00", timedelta(hours=1), id="Range: ISO w/o timezone (space-separated)", ), pytest.param( "1 hour", timedelta(hours=1), id="Duration: 1h (long form)", ), pytest.param( "1 day 1 hour 30 minutes 30 seconds", timedelta(days=1, hours=1, minutes=30, seconds=30), id="Duration: 1d1h30m30s (long form)", ), pytest.param( "1h", timedelta(hours=1), id="Duration: 1h (short form)", ), pytest.param( "1d1h30m30s", timedelta(days=1, hours=1, minutes=30, seconds=30), id="Duration: 1d1h30m30s (short form)", ), ], ) @freeze_time("2016-11-21 22:00:00") def test_convert_time_to_interval(input: str, expect_duration: timedelta) -> None: start, end = convert_time_to_interval(input) assert start == datetime(2016, 11, 21, 22, 0, 0) assert end == start + expect_duration unioslo-zabbix-cli-09a2fab/uv.lock000066400000000000000000002643571471265333400172470ustar00rootroot00000000000000version = 1 requires-python = ">=3.8" resolution-markers = [ "python_full_version < '3.13'", "python_full_version >= '3.13'", ] [[package]] name = "altgraph" version = "0.17.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/de/a8/7145824cf0b9e3c28046520480f207df47e927df83aa9555fb47f8505922/altgraph-0.17.4.tar.gz", hash = "sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406", size = 48418 } wheels = [ { url = "https://files.pythonhosted.org/packages/4d/3f/3bc3f1d83f6e4a7fcb834d3720544ca597590425be5ba9db032b2bf322a2/altgraph-0.17.4-py2.py3-none-any.whl", hash = "sha256:642743b4750de17e655e6711601b077bc6598dbfa3ba5fa2b2a35ce12b508dff", size = 21212 }, ] [[package]] name = "annotated-types" version = "0.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.9'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, ] [[package]] name = "anyio" version = "4.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "idna" }, { name = "sniffio" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e6/e3/c4c8d473d6780ef1853d630d581f70d655b4f8d7553c6997958c283039a2/anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94", size = 163930 } wheels = [ { url = "https://files.pythonhosted.org/packages/7b/a2/10639a79341f6c019dedc95bd48a4928eed9f1d1197f4c04f546fc7ae0ff/anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7", size = 86780 }, ] [[package]] name = "asttokens" version = "2.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] sdist = { url = "https://files.pythonhosted.org/packages/45/1d/f03bcb60c4a3212e15f99a56085d93093a497718adf828d050b9d675da81/asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0", size = 62284 } wheels = [ { url = "https://files.pythonhosted.org/packages/45/86/4736ac618d82a20d87d2f92ae19441ebc7ac9e7a581d7e58bbe79233b24a/asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24", size = 27764 }, ] [[package]] name = "black" version = "24.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "mypy-extensions" }, { name = "packaging" }, { name = "pathspec" }, { name = "platformdirs" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/04/b0/46fb0d4e00372f4a86a6f8efa3cb193c9f64863615e39010b1477e010578/black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f", size = 644810 } wheels = [ { url = "https://files.pythonhosted.org/packages/47/6e/74e29edf1fba3887ed7066930a87f698ffdcd52c5dbc263eabb06061672d/black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6", size = 1632092 }, { url = "https://files.pythonhosted.org/packages/ab/49/575cb6c3faee690b05c9d11ee2e8dba8fbd6d6c134496e644c1feb1b47da/black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb", size = 1457529 }, { url = "https://files.pythonhosted.org/packages/7a/b4/d34099e95c437b53d01c4aa37cf93944b233066eb034ccf7897fa4e5f286/black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42", size = 1757443 }, { url = "https://files.pythonhosted.org/packages/87/a0/6d2e4175ef364b8c4b64f8441ba041ed65c63ea1db2720d61494ac711c15/black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a", size = 1418012 }, { url = "https://files.pythonhosted.org/packages/08/a6/0a3aa89de9c283556146dc6dbda20cd63a9c94160a6fbdebaf0918e4a3e1/black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1", size = 1615080 }, { url = "https://files.pythonhosted.org/packages/db/94/b803d810e14588bb297e565821a947c108390a079e21dbdcb9ab6956cd7a/black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af", size = 1438143 }, { url = "https://files.pythonhosted.org/packages/a5/b5/f485e1bbe31f768e2e5210f52ea3f432256201289fd1a3c0afda693776b0/black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4", size = 1738774 }, { url = "https://files.pythonhosted.org/packages/a8/69/a000fc3736f89d1bdc7f4a879f8aaf516fb03613bb51a0154070383d95d9/black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af", size = 1427503 }, { url = "https://files.pythonhosted.org/packages/a2/a8/05fb14195cfef32b7c8d4585a44b7499c2a4b205e1662c427b941ed87054/black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368", size = 1646132 }, { url = "https://files.pythonhosted.org/packages/41/77/8d9ce42673e5cb9988f6df73c1c5c1d4e9e788053cccd7f5fb14ef100982/black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed", size = 1448665 }, { url = "https://files.pythonhosted.org/packages/cc/94/eff1ddad2ce1d3cc26c162b3693043c6b6b575f538f602f26fe846dfdc75/black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018", size = 1762458 }, { url = "https://files.pythonhosted.org/packages/28/ea/18b8d86a9ca19a6942e4e16759b2fa5fc02bbc0eb33c1b866fcd387640ab/black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2", size = 1436109 }, { url = "https://files.pythonhosted.org/packages/9f/d4/ae03761ddecc1a37d7e743b89cccbcf3317479ff4b88cfd8818079f890d0/black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd", size = 1617322 }, { url = "https://files.pythonhosted.org/packages/14/4b/4dfe67eed7f9b1ddca2ec8e4418ea74f0d1dc84d36ea874d618ffa1af7d4/black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2", size = 1442108 }, { url = "https://files.pythonhosted.org/packages/97/14/95b3f91f857034686cae0e73006b8391d76a8142d339b42970eaaf0416ea/black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e", size = 1745786 }, { url = "https://files.pythonhosted.org/packages/95/54/68b8883c8aa258a6dde958cd5bdfada8382bec47c5162f4a01e66d839af1/black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920", size = 1426754 }, { url = "https://files.pythonhosted.org/packages/13/b2/b3f24fdbb46f0e7ef6238e131f13572ee8279b70f237f221dd168a9dba1a/black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c", size = 1631706 }, { url = "https://files.pythonhosted.org/packages/d9/35/31010981e4a05202a84a3116423970fd1a59d2eda4ac0b3570fbb7029ddc/black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e", size = 1457429 }, { url = "https://files.pythonhosted.org/packages/27/25/3f706b4f044dd569a20a4835c3b733dedea38d83d2ee0beb8178a6d44945/black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47", size = 1756488 }, { url = "https://files.pythonhosted.org/packages/63/72/79375cd8277cbf1c5670914e6bd4c1b15dea2c8f8e906dc21c448d0535f0/black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb", size = 1417721 }, { url = "https://files.pythonhosted.org/packages/27/1e/83fa8a787180e1632c3d831f7e58994d7aaf23a0961320d21e84f922f919/black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed", size = 206504 }, ] [[package]] name = "build" version = "1.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "os_name == 'nt'" }, { name = "importlib-metadata", marker = "python_full_version < '3.10.2'" }, { name = "packaging" }, { name = "pyproject-hooks" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/dd/bb/4a1b7e3a7520e310cf7bfece43788071604e1ccf693a7f0c4638c59068d6/build-1.2.2.tar.gz", hash = "sha256:119b2fb462adef986483438377a13b2f42064a2a3a4161f24a0cca698a07ac8c", size = 46516 } wheels = [ { url = "https://files.pythonhosted.org/packages/91/fd/e4bda6228637ecae5732162b5ac2a5a822e2ba8e546eb4997cde51b231a3/build-1.2.2-py3-none-any.whl", hash = "sha256:277ccc71619d98afdd841a0e96ac9fe1593b823af481d3b0cea748e8894e0613", size = 22823 }, ] [[package]] name = "certifi" version = "2024.8.30" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 } wheels = [ { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 }, ] [[package]] name = "click" version = "8.1.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "platform_system == 'Windows'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } wheels = [ { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] [[package]] name = "coverage" version = "7.6.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f7/08/7e37f82e4d1aead42a7443ff06a1e406aabf7302c4f00a546e4b320b994c/coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d", size = 798791 } wheels = [ { url = "https://files.pythonhosted.org/packages/7e/61/eb7ce5ed62bacf21beca4937a90fe32545c91a3c8a42a30c6616d48fc70d/coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16", size = 206690 }, { url = "https://files.pythonhosted.org/packages/7d/73/041928e434442bd3afde5584bdc3f932fb4562b1597629f537387cec6f3d/coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36", size = 207127 }, { url = "https://files.pythonhosted.org/packages/c7/c8/6ca52b5147828e45ad0242388477fdb90df2c6cbb9a441701a12b3c71bc8/coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02", size = 235654 }, { url = "https://files.pythonhosted.org/packages/d5/da/9ac2b62557f4340270942011d6efeab9833648380109e897d48ab7c1035d/coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc", size = 233598 }, { url = "https://files.pythonhosted.org/packages/53/23/9e2c114d0178abc42b6d8d5281f651a8e6519abfa0ef460a00a91f80879d/coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23", size = 234732 }, { url = "https://files.pythonhosted.org/packages/0f/7e/a0230756fb133343a52716e8b855045f13342b70e48e8ad41d8a0d60ab98/coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34", size = 233816 }, { url = "https://files.pythonhosted.org/packages/28/7c/3753c8b40d232b1e5eeaed798c875537cf3cb183fb5041017c1fdb7ec14e/coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c", size = 232325 }, { url = "https://files.pythonhosted.org/packages/57/e3/818a2b2af5b7573b4b82cf3e9f137ab158c90ea750a8f053716a32f20f06/coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959", size = 233418 }, { url = "https://files.pythonhosted.org/packages/c8/fb/4532b0b0cefb3f06d201648715e03b0feb822907edab3935112b61b885e2/coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232", size = 209343 }, { url = "https://files.pythonhosted.org/packages/5a/25/af337cc7421eca1c187cc9c315f0a755d48e755d2853715bfe8c418a45fa/coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0", size = 210136 }, { url = "https://files.pythonhosted.org/packages/ad/5f/67af7d60d7e8ce61a4e2ddcd1bd5fb787180c8d0ae0fbd073f903b3dd95d/coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93", size = 206796 }, { url = "https://files.pythonhosted.org/packages/e1/0e/e52332389e057daa2e03be1fbfef25bb4d626b37d12ed42ae6281d0a274c/coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3", size = 207244 }, { url = "https://files.pythonhosted.org/packages/aa/cd/766b45fb6e090f20f8927d9c7cb34237d41c73a939358bc881883fd3a40d/coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff", size = 239279 }, { url = "https://files.pythonhosted.org/packages/70/6c/a9ccd6fe50ddaf13442a1e2dd519ca805cbe0f1fcd377fba6d8339b98ccb/coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d", size = 236859 }, { url = "https://files.pythonhosted.org/packages/14/6f/8351b465febb4dbc1ca9929505202db909c5a635c6fdf33e089bbc3d7d85/coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6", size = 238549 }, { url = "https://files.pythonhosted.org/packages/68/3c/289b81fa18ad72138e6d78c4c11a82b5378a312c0e467e2f6b495c260907/coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56", size = 237477 }, { url = "https://files.pythonhosted.org/packages/ed/1c/aa1efa6459d822bd72c4abc0b9418cf268de3f60eeccd65dc4988553bd8d/coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234", size = 236134 }, { url = "https://files.pythonhosted.org/packages/fb/c8/521c698f2d2796565fe9c789c2ee1ccdae610b3aa20b9b2ef980cc253640/coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133", size = 236910 }, { url = "https://files.pythonhosted.org/packages/7d/30/033e663399ff17dca90d793ee8a2ea2890e7fdf085da58d82468b4220bf7/coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c", size = 209348 }, { url = "https://files.pythonhosted.org/packages/20/05/0d1ccbb52727ccdadaa3ff37e4d2dc1cd4d47f0c3df9eb58d9ec8508ca88/coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6", size = 210230 }, { url = "https://files.pythonhosted.org/packages/7e/d4/300fc921dff243cd518c7db3a4c614b7e4b2431b0d1145c1e274fd99bd70/coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778", size = 206983 }, { url = "https://files.pythonhosted.org/packages/e1/ab/6bf00de5327ecb8db205f9ae596885417a31535eeda6e7b99463108782e1/coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391", size = 207221 }, { url = "https://files.pythonhosted.org/packages/92/8f/2ead05e735022d1a7f3a0a683ac7f737de14850395a826192f0288703472/coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8", size = 240342 }, { url = "https://files.pythonhosted.org/packages/0f/ef/94043e478201ffa85b8ae2d2c79b4081e5a1b73438aafafccf3e9bafb6b5/coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d", size = 237371 }, { url = "https://files.pythonhosted.org/packages/1f/0f/c890339dd605f3ebc269543247bdd43b703cce6825b5ed42ff5f2d6122c7/coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca", size = 239455 }, { url = "https://files.pythonhosted.org/packages/d1/04/7fd7b39ec7372a04efb0f70c70e35857a99b6a9188b5205efb4c77d6a57a/coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163", size = 238924 }, { url = "https://files.pythonhosted.org/packages/ed/bf/73ce346a9d32a09cf369f14d2a06651329c984e106f5992c89579d25b27e/coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a", size = 237252 }, { url = "https://files.pythonhosted.org/packages/86/74/1dc7a20969725e917b1e07fe71a955eb34bc606b938316bcc799f228374b/coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d", size = 238897 }, { url = "https://files.pythonhosted.org/packages/b6/e9/d9cc3deceb361c491b81005c668578b0dfa51eed02cd081620e9a62f24ec/coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5", size = 209606 }, { url = "https://files.pythonhosted.org/packages/47/c8/5a2e41922ea6740f77d555c4d47544acd7dc3f251fe14199c09c0f5958d3/coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb", size = 210373 }, { url = "https://files.pythonhosted.org/packages/8c/f9/9aa4dfb751cb01c949c990d136a0f92027fbcc5781c6e921df1cb1563f20/coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106", size = 207007 }, { url = "https://files.pythonhosted.org/packages/b9/67/e1413d5a8591622a46dd04ff80873b04c849268831ed5c304c16433e7e30/coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9", size = 207269 }, { url = "https://files.pythonhosted.org/packages/14/5b/9dec847b305e44a5634d0fb8498d135ab1d88330482b74065fcec0622224/coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c", size = 239886 }, { url = "https://files.pythonhosted.org/packages/7b/b7/35760a67c168e29f454928f51f970342d23cf75a2bb0323e0f07334c85f3/coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a", size = 237037 }, { url = "https://files.pythonhosted.org/packages/f7/95/d2fd31f1d638df806cae59d7daea5abf2b15b5234016a5ebb502c2f3f7ee/coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060", size = 239038 }, { url = "https://files.pythonhosted.org/packages/6e/bd/110689ff5752b67924efd5e2aedf5190cbbe245fc81b8dec1abaffba619d/coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862", size = 238690 }, { url = "https://files.pythonhosted.org/packages/d3/a8/08d7b38e6ff8df52331c83130d0ab92d9c9a8b5462f9e99c9f051a4ae206/coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388", size = 236765 }, { url = "https://files.pythonhosted.org/packages/d6/6a/9cf96839d3147d55ae713eb2d877f4d777e7dc5ba2bce227167d0118dfe8/coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155", size = 238611 }, { url = "https://files.pythonhosted.org/packages/74/e4/7ff20d6a0b59eeaab40b3140a71e38cf52547ba21dbcf1d79c5a32bba61b/coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a", size = 209671 }, { url = "https://files.pythonhosted.org/packages/35/59/1812f08a85b57c9fdb6d0b383d779e47b6f643bc278ed682859512517e83/coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129", size = 210368 }, { url = "https://files.pythonhosted.org/packages/9c/15/08913be1c59d7562a3e39fce20661a98c0a3f59d5754312899acc6cb8a2d/coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e", size = 207758 }, { url = "https://files.pythonhosted.org/packages/c4/ae/b5d58dff26cade02ada6ca612a76447acd69dccdbb3a478e9e088eb3d4b9/coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962", size = 208035 }, { url = "https://files.pythonhosted.org/packages/b8/d7/62095e355ec0613b08dfb19206ce3033a0eedb6f4a67af5ed267a8800642/coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb", size = 250839 }, { url = "https://files.pythonhosted.org/packages/7c/1e/c2967cb7991b112ba3766df0d9c21de46b476d103e32bb401b1b2adf3380/coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704", size = 246569 }, { url = "https://files.pythonhosted.org/packages/8b/61/a7a6a55dd266007ed3b1df7a3386a0d760d014542d72f7c2c6938483b7bd/coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b", size = 248927 }, { url = "https://files.pythonhosted.org/packages/c8/fa/13a6f56d72b429f56ef612eb3bc5ce1b75b7ee12864b3bd12526ab794847/coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f", size = 248401 }, { url = "https://files.pythonhosted.org/packages/75/06/0429c652aa0fb761fc60e8c6b291338c9173c6aa0f4e40e1902345b42830/coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223", size = 246301 }, { url = "https://files.pythonhosted.org/packages/52/76/1766bb8b803a88f93c3a2d07e30ffa359467810e5cbc68e375ebe6906efb/coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3", size = 247598 }, { url = "https://files.pythonhosted.org/packages/66/8b/f54f8db2ae17188be9566e8166ac6df105c1c611e25da755738025708d54/coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f", size = 210307 }, { url = "https://files.pythonhosted.org/packages/9f/b0/e0dca6da9170aefc07515cce067b97178cefafb512d00a87a1c717d2efd5/coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657", size = 211453 }, { url = "https://files.pythonhosted.org/packages/81/d0/d9e3d554e38beea5a2e22178ddb16587dbcbe9a1ef3211f55733924bf7fa/coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0", size = 206674 }, { url = "https://files.pythonhosted.org/packages/38/ea/cab2dc248d9f45b2b7f9f1f596a4d75a435cb364437c61b51d2eb33ceb0e/coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a", size = 207101 }, { url = "https://files.pythonhosted.org/packages/ca/6f/f82f9a500c7c5722368978a5390c418d2a4d083ef955309a8748ecaa8920/coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b", size = 236554 }, { url = "https://files.pythonhosted.org/packages/a6/94/d3055aa33d4e7e733d8fa309d9adf147b4b06a82c1346366fc15a2b1d5fa/coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3", size = 234440 }, { url = "https://files.pythonhosted.org/packages/e4/6e/885bcd787d9dd674de4a7d8ec83faf729534c63d05d51d45d4fa168f7102/coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de", size = 235889 }, { url = "https://files.pythonhosted.org/packages/f4/63/df50120a7744492710854860783d6819ff23e482dee15462c9a833cc428a/coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6", size = 235142 }, { url = "https://files.pythonhosted.org/packages/3a/5d/9d0acfcded2b3e9ce1c7923ca52ccc00c78a74e112fc2aee661125b7843b/coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569", size = 233805 }, { url = "https://files.pythonhosted.org/packages/c4/56/50abf070cb3cd9b1dd32f2c88f083aab561ecbffbcd783275cb51c17f11d/coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989", size = 234655 }, { url = "https://files.pythonhosted.org/packages/25/ee/b4c246048b8485f85a2426ef4abab88e48c6e80c74e964bea5cd4cd4b115/coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7", size = 209296 }, { url = "https://files.pythonhosted.org/packages/5c/1c/96cf86b70b69ea2b12924cdf7cabb8ad10e6130eab8d767a1099fbd2a44f/coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8", size = 210137 }, { url = "https://files.pythonhosted.org/packages/19/d3/d54c5aa83268779d54c86deb39c1c4566e5d45c155369ca152765f8db413/coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255", size = 206688 }, { url = "https://files.pythonhosted.org/packages/a5/fe/137d5dca72e4a258b1bc17bb04f2e0196898fe495843402ce826a7419fe3/coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8", size = 207120 }, { url = "https://files.pythonhosted.org/packages/78/5b/a0a796983f3201ff5485323b225d7c8b74ce30c11f456017e23d8e8d1945/coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2", size = 235249 }, { url = "https://files.pythonhosted.org/packages/4e/e1/76089d6a5ef9d68f018f65411fcdaaeb0141b504587b901d74e8587606ad/coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a", size = 233237 }, { url = "https://files.pythonhosted.org/packages/9a/6f/eef79b779a540326fee9520e5542a8b428cc3bfa8b7c8f1022c1ee4fc66c/coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc", size = 234311 }, { url = "https://files.pythonhosted.org/packages/75/e1/656d65fb126c29a494ef964005702b012f3498db1a30dd562958e85a4049/coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004", size = 233453 }, { url = "https://files.pythonhosted.org/packages/68/6a/45f108f137941a4a1238c85f28fd9d048cc46b5466d6b8dda3aba1bb9d4f/coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb", size = 231958 }, { url = "https://files.pythonhosted.org/packages/9b/e7/47b809099168b8b8c72ae311efc3e88c8d8a1162b3ba4b8da3cfcdb85743/coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36", size = 232938 }, { url = "https://files.pythonhosted.org/packages/52/80/052222ba7058071f905435bad0ba392cc12006380731c37afaf3fe749b88/coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c", size = 209352 }, { url = "https://files.pythonhosted.org/packages/b8/d8/1b92e0b3adcf384e98770a00ca095da1b5f7b483e6563ae4eb5e935d24a1/coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca", size = 210153 }, { url = "https://files.pythonhosted.org/packages/a5/2b/0354ed096bca64dc8e32a7cbcae28b34cb5ad0b1fe2125d6d99583313ac0/coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df", size = 198926 }, ] [package.optional-dependencies] toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] [[package]] name = "exceptiongroup" version = "1.2.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } wheels = [ { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, ] [[package]] name = "executing" version = "2.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/8c/e3/7d45f492c2c4a0e8e0fad57d081a7c8a0286cdd86372b070cca1ec0caa1e/executing-2.1.0.tar.gz", hash = "sha256:8ea27ddd260da8150fa5a708269c4a10e76161e2496ec3e587da9e3c0fe4b9ab", size = 977485 } wheels = [ { url = "https://files.pythonhosted.org/packages/b5/fd/afcd0496feca3276f509df3dbd5dae726fcc756f1a08d9e25abe1733f962/executing-2.1.0-py2.py3-none-any.whl", hash = "sha256:8d63781349375b5ebccc3142f4b30350c0cd9c79f921cde38be2be4637e98eaf", size = 25805 }, ] [[package]] name = "freezegun" version = "1.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "python-dateutil" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2c/ef/722b8d71ddf4d48f25f6d78aa2533d505bf3eec000a7cacb8ccc8de61f2f/freezegun-1.5.1.tar.gz", hash = "sha256:b29dedfcda6d5e8e083ce71b2b542753ad48cfec44037b3fc79702e2980a89e9", size = 33697 } wheels = [ { url = "https://files.pythonhosted.org/packages/51/0b/0d7fee5919bccc1fdc1c2a7528b98f65c6f69b223a3fd8f809918c142c36/freezegun-1.5.1-py3-none-any.whl", hash = "sha256:bf111d7138a8abe55ab48a71755673dbaa4ab87f4cff5634a4442dfec34c15f1", size = 17569 }, ] [[package]] name = "h11" version = "0.14.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } wheels = [ { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, ] [[package]] name = "httpcore" version = "1.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "h11" }, ] sdist = { url = "https://files.pythonhosted.org/packages/17/b0/5e8b8674f8d203335a62fdfcfa0d11ebe09e23613c3391033cbba35f7926/httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61", size = 83234 } wheels = [ { url = "https://files.pythonhosted.org/packages/78/d4/e5d7e4f2174f8a4d63c8897d79eb8fe2503f7ecc03282fee1fa2719c2704/httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5", size = 77926 }, ] [[package]] name = "httpx" version = "0.27.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "certifi" }, { name = "httpcore" }, { name = "idna" }, { name = "sniffio" }, ] sdist = { url = "https://files.pythonhosted.org/packages/78/82/08f8c936781f67d9e6b9eeb8a0c8b4e406136ea4c3d1f89a5db71d42e0e6/httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2", size = 144189 } wheels = [ { url = "https://files.pythonhosted.org/packages/56/95/9377bcb415797e44274b51d46e3249eba641711cf3348050f76ee7b15ffc/httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", size = 76395 }, ] [package.optional-dependencies] socks = [ { name = "socksio" }, ] [[package]] name = "idna" version = "3.8" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e8/ac/e349c5e6d4543326c6883ee9491e3921e0d07b55fdf3cce184b40d63e72a/idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603", size = 189467 } wheels = [ { url = "https://files.pythonhosted.org/packages/22/7e/d71db821f177828df9dea8c42ac46473366f191be53080e552e628aad991/idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac", size = 66894 }, ] [[package]] name = "importlib-metadata" version = "8.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "zipp", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304 } wheels = [ { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514 }, ] [[package]] name = "iniconfig" version = "2.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } wheels = [ { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, ] [[package]] name = "inline-snapshot" version = "0.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asttokens" }, { name = "black" }, { name = "click" }, { name = "executing" }, { name = "rich" }, { name = "toml" }, { name = "types-toml" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/67/a0/119ec5e5a9e8f5d53d821754d2cbe5c77f507c2d2ab221c215c6037f29cb/inline_snapshot-0.13.0.tar.gz", hash = "sha256:a8a773eedfdc78f4bdbcc481a7f147f715e822756885342c8e27a8b9a995073d", size = 82427 } wheels = [ { url = "https://files.pythonhosted.org/packages/26/f6/6d2aec8e171d4fe8e2e81a1e290e7f93a3f26a55e2951865d2a4680c4621/inline_snapshot-0.13.0-py3-none-any.whl", hash = "sha256:bf7f02c005027f6ebe026a69c6c796d631aaf4a725e5108e017eeadb3ed524ad", size = 30822 }, ] [[package]] name = "macholib" version = "1.16.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "altgraph" }, ] sdist = { url = "https://files.pythonhosted.org/packages/95/ee/af1a3842bdd5902ce133bd246eb7ffd4375c38642aeb5dc0ae3a0329dfa2/macholib-1.16.3.tar.gz", hash = "sha256:07ae9e15e8e4cd9a788013d81f5908b3609aa76f9b1421bae9c4d7606ec86a30", size = 59309 } wheels = [ { url = "https://files.pythonhosted.org/packages/d1/5d/c059c180c84f7962db0aeae7c3b9303ed1d73d76f2bfbc32bc231c8be314/macholib-1.16.3-py2.py3-none-any.whl", hash = "sha256:0e315d7583d38b8c77e815b1ecbdbf504a8258d8b3e17b61165c6feb60d18f2c", size = 38094 }, ] [[package]] name = "markdown-it-py" version = "3.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } wheels = [ { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, ] [[package]] name = "mdurl" version = "0.1.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, ] [[package]] name = "mypy-extensions" version = "1.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } wheels = [ { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, ] [[package]] name = "packaging" version = "24.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/51/65/50db4dda066951078f0a96cf12f4b9ada6e4b811516bf0262c0f4f7064d4/packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", size = 148788 } wheels = [ { url = "https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124", size = 53985 }, ] [[package]] name = "pathspec" version = "0.12.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } wheels = [ { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, ] [[package]] name = "pefile" version = "2024.8.26" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/03/4f/2750f7f6f025a1507cd3b7218691671eecfd0bbebebe8b39aa0fe1d360b8/pefile-2024.8.26.tar.gz", hash = "sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632", size = 76008 } wheels = [ { url = "https://files.pythonhosted.org/packages/54/16/12b82f791c7f50ddec566873d5bdd245baa1491bac11d15ffb98aecc8f8b/pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f", size = 74766 }, ] [[package]] name = "platformdirs" version = "4.3.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/75/a0/d7cab8409cdc7d39b037c85ac46d92434fb6595432e069251b38e5c8dd0e/platformdirs-4.3.2.tar.gz", hash = "sha256:9e5e27a08aa095dd127b9f2e764d74254f482fef22b0970773bfba79d091ab8c", size = 21276 } wheels = [ { url = "https://files.pythonhosted.org/packages/da/8b/d497999c4017b80678017ddce745cf675489c110681ad3c84a55eddfd3e7/platformdirs-4.3.2-py3-none-any.whl", hash = "sha256:eb1c8582560b34ed4ba105009a4badf7f6f85768b30126f351328507b2beb617", size = 18417 }, ] [[package]] name = "pluggy" version = "1.5.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } wheels = [ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, ] [[package]] name = "prompt-toolkit" version = "3.0.48" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wcwidth" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2d/4f/feb5e137aff82f7c7f3248267b97451da3644f6cdc218edfe549fb354127/prompt_toolkit-3.0.48.tar.gz", hash = "sha256:d6623ab0477a80df74e646bdbc93621143f5caf104206aa29294d53de1a03d90", size = 424684 } wheels = [ { url = "https://files.pythonhosted.org/packages/a9/6a/fd08d94654f7e67c52ca30523a178b3f8ccc4237fce4be90d39c938a831a/prompt_toolkit-3.0.48-py3-none-any.whl", hash = "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e", size = 386595 }, ] [[package]] name = "pydantic" version = "2.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, { name = "pydantic-core" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/14/15/3d989541b9c8128b96d532cfd2dd10131ddcc75a807330c00feb3d42a5bd/pydantic-2.9.1.tar.gz", hash = "sha256:1363c7d975c7036df0db2b4a61f2e062fbc0aa5ab5f2772e0ffc7191a4f4bce2", size = 768511 } wheels = [ { url = "https://files.pythonhosted.org/packages/e4/28/fff23284071bc1ba419635c7e86561c8b9b8cf62a5bcb459b92d7625fd38/pydantic-2.9.1-py3-none-any.whl", hash = "sha256:7aff4db5fdf3cf573d4b3c30926a510a10e19a0774d38fc4967f78beb6deb612", size = 434363 }, ] [[package]] name = "pydantic-core" version = "2.23.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5c/cc/07bec3fb337ff80eacd6028745bd858b9642f61ee58cfdbfb64451c1def0/pydantic_core-2.23.3.tar.gz", hash = "sha256:3cb0f65d8b4121c1b015c60104a685feb929a29d7cf204387c7f2688c7974690", size = 402277 } wheels = [ { url = "https://files.pythonhosted.org/packages/a8/fb/fc7077473d843fd70bd1e09177c3225be95621881765d6f7d123036fb9c7/pydantic_core-2.23.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:7f10a5d1b9281392f1bf507d16ac720e78285dfd635b05737c3911637601bae6", size = 1845897 }, { url = "https://files.pythonhosted.org/packages/92/8c/c6f1a0f72328c5687acc0847baf806c4cb31c1a9321de70c3cbcbb37cece/pydantic_core-2.23.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3c09a7885dd33ee8c65266e5aa7fb7e2f23d49d8043f089989726391dd7350c5", size = 1777037 }, { url = "https://files.pythonhosted.org/packages/bd/fc/89e2a998218230ed8c38f0ba11d8f73947df90ac59a1e9f2fb4e1ba318a5/pydantic_core-2.23.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6470b5a1ec4d1c2e9afe928c6cb37eb33381cab99292a708b8cb9aa89e62429b", size = 1801481 }, { url = "https://files.pythonhosted.org/packages/d7/f3/81a5f69ea1359633876ea2283728d0afe2ed62e028d91d747dcdfabc594e/pydantic_core-2.23.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9172d2088e27d9a185ea0a6c8cebe227a9139fd90295221d7d495944d2367700", size = 1807280 }, { url = "https://files.pythonhosted.org/packages/7a/91/b20f5646d7ef7c2629744b49e6fb86f839aa676b1aa11fb3998371ac5860/pydantic_core-2.23.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86fc6c762ca7ac8fbbdff80d61b2c59fb6b7d144aa46e2d54d9e1b7b0e780e01", size = 2003100 }, { url = "https://files.pythonhosted.org/packages/89/71/59172c61f2ecd4b33276774512ef31912944429fabaa0f4483151f788a35/pydantic_core-2.23.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0cb80fd5c2df4898693aa841425ea1727b1b6d2167448253077d2a49003e0ed", size = 2662832 }, { url = "https://files.pythonhosted.org/packages/80/d1/c6f8e23987dc166976996a910876596635d71e529335b846880d856589fd/pydantic_core-2.23.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03667cec5daf43ac4995cefa8aaf58f99de036204a37b889c24a80927b629cec", size = 2057218 }, { url = "https://files.pythonhosted.org/packages/ae/f3/f4381383b65cf16392aead51643fd5fb3feeb69972226d276ce5c6cfb948/pydantic_core-2.23.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:047531242f8e9c2db733599f1c612925de095e93c9cc0e599e96cf536aaf56ba", size = 1923455 }, { url = "https://files.pythonhosted.org/packages/a1/8d/d845077d39e55763bdb99d64ef86f8961827f8896b6e58ce08ce6b255bde/pydantic_core-2.23.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5499798317fff7f25dbef9347f4451b91ac2a4330c6669821c8202fd354c7bee", size = 1966890 }, { url = "https://files.pythonhosted.org/packages/53/f8/56355d7b1cf84df63f93b1a455ebb53fd9588edbb63a44fd4d801444a060/pydantic_core-2.23.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bbb5e45eab7624440516ee3722a3044b83fff4c0372efe183fd6ba678ff681fe", size = 2112163 }, { url = "https://files.pythonhosted.org/packages/06/32/a0a7a3a318b4ae98a0e6b9e18db31fadbd3cfc46b31191e4ed4ca658e2d4/pydantic_core-2.23.3-cp310-none-win32.whl", hash = "sha256:8b5b3ed73abb147704a6e9f556d8c5cb078f8c095be4588e669d315e0d11893b", size = 1717086 }, { url = "https://files.pythonhosted.org/packages/e3/31/38aebe234508fc30c80b4825661d3c1ef0d51b1c40a12e50855b108acd35/pydantic_core-2.23.3-cp310-none-win_amd64.whl", hash = "sha256:2b603cde285322758a0279995b5796d64b63060bfbe214b50a3ca23b5cee3e83", size = 1918933 }, { url = "https://files.pythonhosted.org/packages/4a/60/ef8eaad365c1d94962d158633f66313e051f7b90cead647e65a96993da22/pydantic_core-2.23.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:c889fd87e1f1bbeb877c2ee56b63bb297de4636661cc9bbfcf4b34e5e925bc27", size = 1843251 }, { url = "https://files.pythonhosted.org/packages/57/f4/20aa352e03379a3b5d6c2fb951a979f70718138ea747e3f756d63dda69da/pydantic_core-2.23.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea85bda3189fb27503af4c45273735bcde3dd31c1ab17d11f37b04877859ef45", size = 1776367 }, { url = "https://files.pythonhosted.org/packages/f1/b9/e5482ac4ea2d128925759d905fb05a08ca98e67ed1d8ab7401861997c6c8/pydantic_core-2.23.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7f7f72f721223f33d3dc98a791666ebc6a91fa023ce63733709f4894a7dc611", size = 1800135 }, { url = "https://files.pythonhosted.org/packages/78/9f/387353f6b6b2ed023f973cffa4e2384bb2e52d15acf5680bc70c50f6c48f/pydantic_core-2.23.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b2b55b0448e9da68f56b696f313949cda1039e8ec7b5d294285335b53104b61", size = 1805896 }, { url = "https://files.pythonhosted.org/packages/4f/70/9a153f19394e2ef749f586273ebcdb3de97e2fa97e175b957a8e5a2a77f9/pydantic_core-2.23.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c24574c7e92e2c56379706b9a3f07c1e0c7f2f87a41b6ee86653100c4ce343e5", size = 2001492 }, { url = "https://files.pythonhosted.org/packages/a5/1c/79d976846fcdcae0c657922d0f476ca287fa694e69ac1fc9d397b831e1cc/pydantic_core-2.23.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2b05e6ccbee333a8f4b8f4d7c244fdb7a979e90977ad9c51ea31261e2085ce0", size = 2659827 }, { url = "https://files.pythonhosted.org/packages/fd/89/cdd76ae363cabae23a4b70df50d603c81c517415ff9d5d65e72e35251cf6/pydantic_core-2.23.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2c409ce1c219c091e47cb03feb3c4ed8c2b8e004efc940da0166aaee8f9d6c8", size = 2055160 }, { url = "https://files.pythonhosted.org/packages/1a/82/7d62c3dd4e2e101a81ac3fa138d986bfbad9727a6275fc2b4a5efb98bdbd/pydantic_core-2.23.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d965e8b325f443ed3196db890d85dfebbb09f7384486a77461347f4adb1fa7f8", size = 1922282 }, { url = "https://files.pythonhosted.org/packages/85/e6/ef09f395c974d08674464dd3d49066612fe7cc0466ef8ce9427cadf13e5b/pydantic_core-2.23.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f56af3a420fb1ffaf43ece3ea09c2d27c444e7c40dcb7c6e7cf57aae764f2b48", size = 1965827 }, { url = "https://files.pythonhosted.org/packages/a4/5e/e589474af850c77c3180b101b54bc98bf812ad09728ba2cff4989acc9734/pydantic_core-2.23.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5b01a078dd4f9a52494370af21aa52964e0a96d4862ac64ff7cea06e0f12d2c5", size = 2110810 }, { url = "https://files.pythonhosted.org/packages/e0/ff/626007d5b7ac811f9bcac6d8af3a574ccee4505c1f015d25806101842f0c/pydantic_core-2.23.3-cp311-none-win32.whl", hash = "sha256:560e32f0df04ac69b3dd818f71339983f6d1f70eb99d4d1f8e9705fb6c34a5c1", size = 1715479 }, { url = "https://files.pythonhosted.org/packages/4f/ff/6dc33f3b71e34ef633e35d6476d245bf303fc3eaf18a00f39bb54f78faf3/pydantic_core-2.23.3-cp311-none-win_amd64.whl", hash = "sha256:c744fa100fdea0d000d8bcddee95213d2de2e95b9c12be083370b2072333a0fa", size = 1918281 }, { url = "https://files.pythonhosted.org/packages/8f/35/6d81bc4aa7d06e716f39e2bffb0eabcbcebaf7bab94c2f8278e277ded0ea/pydantic_core-2.23.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:e0ec50663feedf64d21bad0809f5857bac1ce91deded203efc4a84b31b2e4305", size = 1845250 }, { url = "https://files.pythonhosted.org/packages/18/42/0821cd46f76406e0fe57df7a89d6af8fddb22cce755bcc2db077773c7d1a/pydantic_core-2.23.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:db6e6afcb95edbe6b357786684b71008499836e91f2a4a1e55b840955b341dbb", size = 1769993 }, { url = "https://files.pythonhosted.org/packages/e5/55/b969088e48bd8ea588548a7194d425de74370b17b385cee4d28f5a79013d/pydantic_core-2.23.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98ccd69edcf49f0875d86942f4418a4e83eb3047f20eb897bffa62a5d419c8fa", size = 1791250 }, { url = "https://files.pythonhosted.org/packages/43/c1/1d460d09c012ac76b68b2a1fd426ad624724f93b40e24a9a993763f12c61/pydantic_core-2.23.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a678c1ac5c5ec5685af0133262103defb427114e62eafeda12f1357a12140162", size = 1802530 }, { url = "https://files.pythonhosted.org/packages/70/8e/fd3c9eda00fbdadca726f17a0f863ecd871a65b3a381b77277ae386d3bcd/pydantic_core-2.23.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01491d8b4d8db9f3391d93b0df60701e644ff0894352947f31fff3e52bd5c801", size = 1997848 }, { url = "https://files.pythonhosted.org/packages/f0/67/13fa22d7b09395e83721edc31bae2bd5c5e2c36a09d470c18f5d1de46958/pydantic_core-2.23.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fcf31facf2796a2d3b7fe338fe8640aa0166e4e55b4cb108dbfd1058049bf4cb", size = 2662790 }, { url = "https://files.pythonhosted.org/packages/fa/1b/1d689c53d15ab67cb0df1c3a2b1df873b50409581e93e4848289dce57e2f/pydantic_core-2.23.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7200fd561fb3be06827340da066df4311d0b6b8eb0c2116a110be5245dceb326", size = 2074114 }, { url = "https://files.pythonhosted.org/packages/3d/d9/b565048609db77760b9a0900f6e0a3b2f33be47cd3c4a433f49653a0d2b5/pydantic_core-2.23.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dc1636770a809dee2bd44dd74b89cc80eb41172bcad8af75dd0bc182c2666d4c", size = 1918153 }, { url = "https://files.pythonhosted.org/packages/41/94/8ee55c51333ed8df3a6f1e73c6530c724a9a37d326e114c9e3b24faacff9/pydantic_core-2.23.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:67a5def279309f2e23014b608c4150b0c2d323bd7bccd27ff07b001c12c2415c", size = 1969019 }, { url = "https://files.pythonhosted.org/packages/f7/49/0233bae5778a5526cef000447a93e8d462f4f13e2214c13c5b23d379cb25/pydantic_core-2.23.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:748bdf985014c6dd3e1e4cc3db90f1c3ecc7246ff5a3cd4ddab20c768b2f1dab", size = 2121325 }, { url = "https://files.pythonhosted.org/packages/42/a1/2f262db2fd6f9c2c9904075a067b1764cc6f71c014be5c6c91d9de52c434/pydantic_core-2.23.3-cp312-none-win32.whl", hash = "sha256:255ec6dcb899c115f1e2a64bc9ebc24cc0e3ab097775755244f77360d1f3c06c", size = 1725252 }, { url = "https://files.pythonhosted.org/packages/9a/00/a57937080b49500df790c4853d3e7bc605bd0784e4fcaf1a159456f37ef1/pydantic_core-2.23.3-cp312-none-win_amd64.whl", hash = "sha256:40b8441be16c1e940abebed83cd006ddb9e3737a279e339dbd6d31578b802f7b", size = 1920660 }, { url = "https://files.pythonhosted.org/packages/e1/3c/32958c0a5d1935591b58337037a1695782e61261582d93d5a7f55441f879/pydantic_core-2.23.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6daaf5b1ba1369a22c8b050b643250e3e5efc6a78366d323294aee54953a4d5f", size = 1845068 }, { url = "https://files.pythonhosted.org/packages/92/a1/7e628e19b78e6ffdb2c92cccbb7eca84bfd3276cee4cafcae8833452f458/pydantic_core-2.23.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d015e63b985a78a3d4ccffd3bdf22b7c20b3bbd4b8227809b3e8e75bc37f9cb2", size = 1770095 }, { url = "https://files.pythonhosted.org/packages/bb/17/d15fd8ce143cd1abb27be924eeff3c5c0fe3b0582f703c5a5273c11e67ce/pydantic_core-2.23.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3fc572d9b5b5cfe13f8e8a6e26271d5d13f80173724b738557a8c7f3a8a3791", size = 1790964 }, { url = "https://files.pythonhosted.org/packages/24/cc/37feff1792f09dc33207fbad3897373229279d1973c211f9562abfdf137d/pydantic_core-2.23.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f6bd91345b5163ee7448bee201ed7dd601ca24f43f439109b0212e296eb5b423", size = 1802384 }, { url = "https://files.pythonhosted.org/packages/44/d8/ca9acd7f5f044d9ff6e43d7f35aab4b1d5982b4773761eabe3317fc68e30/pydantic_core-2.23.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc379c73fd66606628b866f661e8785088afe2adaba78e6bbe80796baf708a63", size = 1997824 }, { url = "https://files.pythonhosted.org/packages/35/0f/146269dba21b10d5bf86f9a7a7bbeab4ce1db06f466a1ab5ec3dec68b409/pydantic_core-2.23.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbdce4b47592f9e296e19ac31667daed8753c8367ebb34b9a9bd89dacaa299c9", size = 2662907 }, { url = "https://files.pythonhosted.org/packages/5a/7d/9573f006e39cd1a7b7716d1a264e3f4f353cf0a6042c04c01c6e31666f62/pydantic_core-2.23.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc3cf31edf405a161a0adad83246568647c54404739b614b1ff43dad2b02e6d5", size = 2073953 }, { url = "https://files.pythonhosted.org/packages/7e/a5/25200aaafd1e97e2ec3c1eb4b357669dd93911f2eba252bc60b6ba884fff/pydantic_core-2.23.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8e22b477bf90db71c156f89a55bfe4d25177b81fce4aa09294d9e805eec13855", size = 1917822 }, { url = "https://files.pythonhosted.org/packages/3e/b4/ac069c58e3cee70c69f03693222cc173fdf740d20d53167bceafc1efc7ca/pydantic_core-2.23.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:0a0137ddf462575d9bce863c4c95bac3493ba8e22f8c28ca94634b4a1d3e2bb4", size = 1968838 }, { url = "https://files.pythonhosted.org/packages/d1/3d/9f96bbd6212b4b0a6dc6d037e446208d3420baba2b2b81e544094b18a859/pydantic_core-2.23.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:203171e48946c3164fe7691fc349c79241ff8f28306abd4cad5f4f75ed80bc8d", size = 2121468 }, { url = "https://files.pythonhosted.org/packages/ac/50/7399d536d6600d69059a87fff89861332c97a7b3471327a3663c7576e707/pydantic_core-2.23.3-cp313-none-win32.whl", hash = "sha256:76bdab0de4acb3f119c2a4bff740e0c7dc2e6de7692774620f7452ce11ca76c8", size = 1725373 }, { url = "https://files.pythonhosted.org/packages/24/ba/9ac8744ab636c1161c598cc5e8261379b6b0f1d63c31242bf9d5ed41ed32/pydantic_core-2.23.3-cp313-none-win_amd64.whl", hash = "sha256:37ba321ac2a46100c578a92e9a6aa33afe9ec99ffa084424291d84e456f490c1", size = 1920594 }, { url = "https://files.pythonhosted.org/packages/fc/19/88cf661dd393be8f7a2c9823e33a2ed904b1aad29135cc6754e5fd958375/pydantic_core-2.23.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d063c6b9fed7d992bcbebfc9133f4c24b7a7f215d6b102f3e082b1117cddb72c", size = 1846037 }, { url = "https://files.pythonhosted.org/packages/18/0e/f8da8467a6b8a7c6b09b11b6b4c32d4a24e1472768dd20f90703e186b9a5/pydantic_core-2.23.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6cb968da9a0746a0cf521b2b5ef25fc5a0bee9b9a1a8214e0a1cfaea5be7e8a4", size = 1729915 }, { url = "https://files.pythonhosted.org/packages/b4/93/16f77056eeeffe094b0c3909c78a321d3a3a9bce02932982e5ebf08156c5/pydantic_core-2.23.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edbefe079a520c5984e30e1f1f29325054b59534729c25b874a16a5048028d16", size = 1801738 }, { url = "https://files.pythonhosted.org/packages/6a/43/a0ee125c96841c98d284d9ed04b48344c46e02765b9ad13bd0c5115bcb20/pydantic_core-2.23.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cbaaf2ef20d282659093913da9d402108203f7cb5955020bd8d1ae5a2325d1c4", size = 1807157 }, { url = "https://files.pythonhosted.org/packages/9c/04/e2fcad9e0e2d6c22b7047ab6d0131df0432fc93c442abe9aa39ce29478b0/pydantic_core-2.23.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb539d7e5dc4aac345846f290cf504d2fd3c1be26ac4e8b5e4c2b688069ff4cf", size = 2003210 }, { url = "https://files.pythonhosted.org/packages/f8/ae/5b62d4989323f5c8f1aa265035d6f3b348a20b79f6e4e8a5f2e13da17e31/pydantic_core-2.23.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e6f33503c5495059148cc486867e1d24ca35df5fc064686e631e314d959ad5b", size = 2658837 }, { url = "https://files.pythonhosted.org/packages/b1/b4/4f7713870898f06928968ec0d44d5a1216fb0ef2b539db4a1620881a4815/pydantic_core-2.23.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04b07490bc2f6f2717b10c3969e1b830f5720b632f8ae2f3b8b1542394c47a8e", size = 2057433 }, { url = "https://files.pythonhosted.org/packages/79/62/8a7bf68bc90c305f1961f540c421500c0f75adaf9c21e5d7eb3ed2550007/pydantic_core-2.23.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03795b9e8a5d7fda05f3873efc3f59105e2dcff14231680296b87b80bb327295", size = 1922962 }, { url = "https://files.pythonhosted.org/packages/d9/31/a851c3191c4da40c8dc7baa529e4508d882f3a9ace610ea0fb8c1442a92c/pydantic_core-2.23.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c483dab0f14b8d3f0df0c6c18d70b21b086f74c87ab03c59250dbf6d3c89baba", size = 1967335 }, { url = "https://files.pythonhosted.org/packages/0b/09/51af6a1c12830c199b7410290894ba6c723a79b8fc1760324c9f0914aa31/pydantic_core-2.23.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8b2682038e255e94baf2c473dca914a7460069171ff5cdd4080be18ab8a7fd6e", size = 2112467 }, { url = "https://files.pythonhosted.org/packages/55/d9/75d178e0619b09d9c675692baceaf465fdcd0416160e549d7302d2b1f581/pydantic_core-2.23.3-cp38-none-win32.whl", hash = "sha256:f4a57db8966b3a1d1a350012839c6a0099f0898c56512dfade8a1fe5fb278710", size = 1716719 }, { url = "https://files.pythonhosted.org/packages/c8/e0/0ec00fa4fa380847f98189f7e1899366dfdeb3d87487b54622f91bcd9a68/pydantic_core-2.23.3-cp38-none-win_amd64.whl", hash = "sha256:13dd45ba2561603681a2676ca56006d6dee94493f03d5cadc055d2055615c3ea", size = 1921490 }, { url = "https://files.pythonhosted.org/packages/b8/9c/cb69375fd9488869c4c29edf6666050ce5c88baf755926f4121aacd9f01f/pydantic_core-2.23.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:82da2f4703894134a9f000e24965df73cc103e31e8c31906cc1ee89fde72cbd8", size = 1846402 }, { url = "https://files.pythonhosted.org/packages/b5/7d/99d47c7084e39465781552f65889f92b1673a31c179753e476385326a3b6/pydantic_core-2.23.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dd9be0a42de08f4b58a3cc73a123f124f65c24698b95a54c1543065baca8cf0e", size = 1730388 }, { url = "https://files.pythonhosted.org/packages/80/0d/e6be39d563846de02a1a61fa942758e6d2409f5a87bb5853f65abde2470a/pydantic_core-2.23.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89b731f25c80830c76fdb13705c68fef6a2b6dc494402987c7ea9584fe189f5d", size = 1801656 }, { url = "https://files.pythonhosted.org/packages/3e/4a/6d9e8ad6c95be4af18948d400284382bc7f8b00d795f2222f3f094bc4dcb/pydantic_core-2.23.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c6de1ec30c4bb94f3a69c9f5f2182baeda5b809f806676675e9ef6b8dc936f28", size = 1807884 }, { url = "https://files.pythonhosted.org/packages/a9/09/751832a0938384cf78ce0353d38ef350c9ecbf2ebd5dc7ff0b3b3a0f8bfd/pydantic_core-2.23.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb68b41c3fa64587412b104294b9cbb027509dc2f6958446c502638d481525ef", size = 2003488 }, { url = "https://files.pythonhosted.org/packages/4b/1f/77c720b6ca179f59c44a5698163b38be58e735974db28d761b31462da42e/pydantic_core-2.23.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c3980f2843de5184656aab58698011b42763ccba11c4a8c35936c8dd6c7068c", size = 2664470 }, { url = "https://files.pythonhosted.org/packages/47/71/5aa475102a31edc15bb0df9a6627de64f62b11be99be49f2a4a0d2a19eea/pydantic_core-2.23.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94f85614f2cba13f62c3c6481716e4adeae48e1eaa7e8bac379b9d177d93947a", size = 2057855 }, { url = "https://files.pythonhosted.org/packages/d2/66/15d6378783e2ede05416194848030b35cf732d84cf6cb8897aa916f628a6/pydantic_core-2.23.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:510b7fb0a86dc8f10a8bb43bd2f97beb63cffad1203071dc434dac26453955cd", size = 1923691 }, { url = "https://files.pythonhosted.org/packages/6e/c5/7172805d806012aaff6547d2c819a98bc318313d36a9b10cd48241d85fb1/pydantic_core-2.23.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1eba2f7ce3e30ee2170410e2171867ea73dbd692433b81a93758ab2de6c64835", size = 1967678 }, { url = "https://files.pythonhosted.org/packages/2b/51/6e1f5b06a3e70de9ac4d14d5ddf74564c2831ed403bb86808742c26d4240/pydantic_core-2.23.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4b259fd8409ab84b4041b7b3f24dcc41e4696f180b775961ca8142b5b21d0e70", size = 2112758 }, { url = "https://files.pythonhosted.org/packages/3f/e5/1ee8f68f9425728541edb9df26702f95f8243c9e42f405b2a972c64edb1b/pydantic_core-2.23.3-cp39-none-win32.whl", hash = "sha256:40d9bd259538dba2f40963286009bf7caf18b5112b19d2b55b09c14dde6db6a7", size = 1716954 }, { url = "https://files.pythonhosted.org/packages/96/67/663492ab80a625d07ca4abd3178023fa79a9f6fa1df4acc3213bff371e9d/pydantic_core-2.23.3-cp39-none-win_amd64.whl", hash = "sha256:5a8cd3074a98ee70173a8633ad3c10e00dcb991ecec57263aacb4095c5efb958", size = 1921529 }, { url = "https://files.pythonhosted.org/packages/c0/2d/1f4ec8614225b516366f6c4c49d55ec42ebb93004c0bc9a3e0d21d0ed3c0/pydantic_core-2.23.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f399e8657c67313476a121a6944311fab377085ca7f490648c9af97fc732732d", size = 1834597 }, { url = "https://files.pythonhosted.org/packages/4d/f0/665d4cd60147992b1da0f5a9d1fd7f309c7f12999e3a494c4898165c64ab/pydantic_core-2.23.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:6b5547d098c76e1694ba85f05b595720d7c60d342f24d5aad32c3049131fa5c4", size = 1721339 }, { url = "https://files.pythonhosted.org/packages/a7/02/7b85ae2c3452e6b9f43b89482dc2a2ba771c31d86d93c2a5a250870b243b/pydantic_core-2.23.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0dda0290a6f608504882d9f7650975b4651ff91c85673341789a476b1159f211", size = 1794316 }, { url = "https://files.pythonhosted.org/packages/61/09/f0fde8a9d66f37f3e08e03965a9833d71c4b5fb0287d8f625f88d79dfcd6/pydantic_core-2.23.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65b6e5da855e9c55a0c67f4db8a492bf13d8d3316a59999cfbaf98cc6e401961", size = 1944713 }, { url = "https://files.pythonhosted.org/packages/61/2b/0bfe144cac991700dbeaff620fed38b0565352acb342f90374ebf1350084/pydantic_core-2.23.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:09e926397f392059ce0afdcac920df29d9c833256354d0c55f1584b0b70cf07e", size = 1916385 }, { url = "https://files.pythonhosted.org/packages/02/4f/7d1b8a28e4a1dd96cdde9e220627abd4d3a7860eb79cc682ccf828cf93e4/pydantic_core-2.23.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:87cfa0ed6b8c5bd6ae8b66de941cece179281239d482f363814d2b986b79cedc", size = 1959666 }, { url = "https://files.pythonhosted.org/packages/5d/9a/b2c520ef627001c68cf23990b2de42ba66eae58a3f56f13375ae9aecb88d/pydantic_core-2.23.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e61328920154b6a44d98cabcb709f10e8b74276bc709c9a513a8c37a18786cc4", size = 2103742 }, { url = "https://files.pythonhosted.org/packages/cd/43/b9a88a4e6454fcad63317e3dade687b68ae7d9f324c868411b1ea70218b3/pydantic_core-2.23.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ce3317d155628301d649fe5e16a99528d5680af4ec7aa70b90b8dacd2d725c9b", size = 1916507 }, { url = "https://files.pythonhosted.org/packages/e7/52/fd89a422e922174728341b594612e9c727f5c07c55e3e436dc3dd626f52d/pydantic_core-2.23.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e89513f014c6be0d17b00a9a7c81b1c426f4eb9224b15433f3d98c1a071f8433", size = 1835707 }, { url = "https://files.pythonhosted.org/packages/be/14/07f8fa279d8c7b414c7e547f868dd1b9f8e76f248f49fb44c2312be62cb0/pydantic_core-2.23.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:4f62c1c953d7ee375df5eb2e44ad50ce2f5aff931723b398b8bc6f0ac159791a", size = 1722073 }, { url = "https://files.pythonhosted.org/packages/18/02/09c3ec4f9b270fd5af8f142b5547c396a1cb2aba6721b374f77a60e4bae4/pydantic_core-2.23.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2718443bc671c7ac331de4eef9b673063b10af32a0bb385019ad61dcf2cc8f6c", size = 1794805 }, { url = "https://files.pythonhosted.org/packages/e7/5c/2ab3689816702554ac73ea5c435030be5461180d5b18f252ea7890774227/pydantic_core-2.23.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0d90e08b2727c5d01af1b5ef4121d2f0c99fbee692c762f4d9d0409c9da6541", size = 1945670 }, { url = "https://files.pythonhosted.org/packages/12/ef/c16db2dc939e2686b63a1cd19e80fda55fff95b7411cc3a34ca7d7d2463e/pydantic_core-2.23.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2b676583fc459c64146debea14ba3af54e540b61762dfc0613dc4e98c3f66eeb", size = 1916745 }, { url = "https://files.pythonhosted.org/packages/00/58/c55081fdfc1a1c26c4d90555c013bbb6193721147154b5ba3dff16c36b96/pydantic_core-2.23.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:50e4661f3337977740fdbfbae084ae5693e505ca2b3130a6d4eb0f2281dc43b8", size = 1960193 }, { url = "https://files.pythonhosted.org/packages/10/0e/664177152393180ca06ed393a3d4b16804d0a98ce9ccb460c1d29950ab77/pydantic_core-2.23.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:68f4cf373f0de6abfe599a38307f4417c1c867ca381c03df27c873a9069cda25", size = 2104209 }, { url = "https://files.pythonhosted.org/packages/88/6a/df8adefd9d1052c72ee98b8c50a5eb042cdb3f2fea1f4f58a16046bdac02/pydantic_core-2.23.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:59d52cf01854cb26c46958552a21acb10dd78a52aa34c86f284e66b209db8cab", size = 1917304 }, ] [[package]] name = "pygments" version = "2.18.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 } wheels = [ { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 }, ] [[package]] name = "pyinstaller" version = "6.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "altgraph" }, { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "macholib", marker = "sys_platform == 'darwin'" }, { name = "packaging" }, { name = "pefile", marker = "sys_platform == 'win32'" }, { name = "pyinstaller-hooks-contrib" }, { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, { name = "setuptools" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5c/df/30b1f66d35defa37e676556acca4eb775b49637bb73054b0c31af134cd8a/pyinstaller-6.10.0.tar.gz", hash = "sha256:143840f8056ff7b910bf8f16f6cd92cc10a6c2680bb76d0a25d558d543d21270", size = 2464746 } wheels = [ { url = "https://files.pythonhosted.org/packages/a6/fd/9b3c208c9cc822555b88c6af051da5f7291f367e4337ea44b138008ba2fb/pyinstaller-6.10.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:d60fb22859e11483af735aec115fdde09467cdbb29edd9844839f2c920b748c0", size = 984872 }, { url = "https://files.pythonhosted.org/packages/b7/1c/1071c69e21b27ca5b1ea185fb897a0e724e85b8c4456b0b142b0cac3c33b/pyinstaller-6.10.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:46d75359668993ddd98630a3669dc5249f3c446e35239b43bc7f4155bc574748", size = 705041 }, { url = "https://files.pythonhosted.org/packages/c4/44/4e20bf97d464cb27fd9b25321c94beb8d1f5ed7176af38fe6a22d8766d2e/pyinstaller-6.10.0-py3-none-manylinux2014_i686.whl", hash = "sha256:3398a98fa17d47ccb31f8779ecbdacec025f7adb2f22757a54b706ac8b4fe906", size = 707309 }, { url = "https://files.pythonhosted.org/packages/65/e7/452dc579bd38b50142a0886a3a0fd67aa14fb84ea0376f339a568931319c/pyinstaller-6.10.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e9989f354ae4ed8a3bec7bdb37ae0d170751d6520e500f049c7cd0632d31d5c3", size = 712476 }, { url = "https://files.pythonhosted.org/packages/6e/56/5ce76d7dd53f8ed13d91d0ac9688b5624c849ad2dfcee18ff5709955d150/pyinstaller-6.10.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:b7c90c91921b3749083115b28f30f40abf2bb481ceff196d2b2ce0eaa2b3d429", size = 702859 }, { url = "https://files.pythonhosted.org/packages/19/78/604bec9cd73cd02ed29f7bd72ec25dff6eccc57554ec3ab096959fae65de/pyinstaller-6.10.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6cf876d7d93b8b4f28d1ad57fa24645cf43119c79e985dd5e5f7a801245e6f53", size = 703343 }, { url = "https://files.pythonhosted.org/packages/ba/8f/f21b92a90bb48a0e59f448ff6f92a823457922207cede7ae3abedcbfb974/pyinstaller-6.10.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:db05e3f2f10f9f78c56f1fb163d9cb453433429fe4281218ebaf1ebfd39ba942", size = 707763 }, { url = "https://files.pythonhosted.org/packages/fa/77/eeac52914f24d952524493d3190f6a383582d4abb117076f50264eba0f9e/pyinstaller-6.10.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:28eca3817f176fdc19747e1afcf434f13bb9f17a644f611be2c5a61b1f498ed7", size = 703742 }, { url = "https://files.pythonhosted.org/packages/a2/fd/dcbeb93783b983c8bf5d9cab929361761b0c3bc560de5377041eb2aec2b0/pyinstaller-6.10.0-py3-none-win32.whl", hash = "sha256:703e041718987e46ba0568a2c71ecf2459fddef57cf9edf3efeed4a53e3dae3f", size = 1272503 }, { url = "https://files.pythonhosted.org/packages/c2/13/fe1ea2fb379a4ea86fa38c224b853b94227246840484feccbb4f2fded615/pyinstaller-6.10.0-py3-none-win_amd64.whl", hash = "sha256:95b55966e563e8b8f31a43882aea10169e9a11fdf38e626d86a2907b640c0701", size = 1332831 }, { url = "https://files.pythonhosted.org/packages/4e/4f/d46f182cae5b43d97b18fa5b167a233ec04fd616d60514aece08d5c45ab8/pyinstaller-6.10.0-py3-none-win_arm64.whl", hash = "sha256:308e0a8670c9c9ac0cebbf1bbb492e71b6675606f2ec78bc4adfc830d209e087", size = 1259686 }, ] [[package]] name = "pyinstaller-hooks-contrib" version = "2024.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "packaging" }, { name = "setuptools" }, ] sdist = { url = "https://files.pythonhosted.org/packages/20/17/58309bc981794907429f352ed628008db5ead987dafc5b2bfb318804a949/pyinstaller_hooks_contrib-2024.8.tar.gz", hash = "sha256:29b68d878ab739e967055b56a93eb9b58e529d5b054fbab7a2f2bacf80cef3e2", size = 131815 } wheels = [ { url = "https://files.pythonhosted.org/packages/a0/8f/a7eb7b85ea2015d1c9a8c1686eaeb61f3ce35e1047fc03c0ac71d22d68f2/pyinstaller_hooks_contrib-2024.8-py3-none-any.whl", hash = "sha256:0057fe9a5c398d3f580e73e58793a1d4a8315ca91c3df01efea1c14ed557825a", size = 322803 }, ] [[package]] name = "pyproject-hooks" version = "1.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c7/07/6f63dda440d4abb191b91dc383b472dae3dd9f37e4c1e4a5c3db150531c6/pyproject_hooks-1.1.0.tar.gz", hash = "sha256:4b37730834edbd6bd37f26ece6b44802fb1c1ee2ece0e54ddff8bfc06db86965", size = 7838 } wheels = [ { url = "https://files.pythonhosted.org/packages/ae/f3/431b9d5fe7d14af7a32340792ef43b8a714e7726f1d7b69cc4e8e7a3f1d7/pyproject_hooks-1.1.0-py3-none-any.whl", hash = "sha256:7ceeefe9aec63a1064c18d939bdc3adf2d8aa1988a510afec15151578b232aa2", size = 9184 }, ] [[package]] name = "pytest" version = "8.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8b/6c/62bbd536103af674e227c41a8f3dcd022d591f6eed5facb5a0f31ee33bbc/pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", size = 1442487 } wheels = [ { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341 }, ] [[package]] name = "pytest-cov" version = "5.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage", extra = ["toml"] }, { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/74/67/00efc8d11b630c56f15f4ad9c7f9223f1e5ec275aaae3fa9118c6a223ad2/pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857", size = 63042 } wheels = [ { url = "https://files.pythonhosted.org/packages/78/3a/af5b4fa5961d9a1e6237b530eb87dd04aea6eb83da09d2a4073d81b54ccf/pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652", size = 21990 }, ] [[package]] name = "python-dateutil" version = "2.9.0.post0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, ] [[package]] name = "pywin32-ctypes" version = "0.2.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471 } wheels = [ { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756 }, ] [[package]] name = "rich" version = "13.8.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, { name = "typing-extensions", marker = "python_full_version < '3.9'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/92/76/40f084cb7db51c9d1fa29a7120717892aeda9a7711f6225692c957a93535/rich-13.8.1.tar.gz", hash = "sha256:8260cda28e3db6bf04d2d1ef4dbc03ba80a824c88b0e7668a0f23126a424844a", size = 222080 } wheels = [ { url = "https://files.pythonhosted.org/packages/b0/11/dadb85e2bd6b1f1ae56669c3e1f0410797f9605d752d68fb47b77f525b31/rich-13.8.1-py3-none-any.whl", hash = "sha256:1760a3c0848469b97b558fc61c85233e3dafb69c7a071b4d60c38099d3cd4c06", size = 241608 }, ] [[package]] name = "setuptools" version = "74.1.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/3e/2c/f0a538a2f91ce633a78daaeb34cbfb93a54bd2132a6de1f6cec028eee6ef/setuptools-74.1.2.tar.gz", hash = "sha256:95b40ed940a1c67eb70fc099094bd6e99c6ee7c23aa2306f4d2697ba7916f9c6", size = 1356467 } wheels = [ { url = "https://files.pythonhosted.org/packages/cb/9c/9ad11ac06b97e55ada655f8a6bea9d1d3f06e120b178cd578d80e558191d/setuptools-74.1.2-py3-none-any.whl", hash = "sha256:5f4c08aa4d3ebcb57a50c33b1b07e94315d7fc7230f7115e47fc99776c8ce308", size = 1262071 }, ] [[package]] name = "shellingham" version = "1.5.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } wheels = [ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, ] [[package]] name = "six" version = "1.16.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041 } wheels = [ { url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053 }, ] [[package]] name = "sniffio" version = "1.3.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } wheels = [ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, ] [[package]] name = "socksio" version = "1.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f8/5c/48a7d9495be3d1c651198fd99dbb6ce190e2274d0f28b9051307bdec6b85/socksio-1.0.0.tar.gz", hash = "sha256:f88beb3da5b5c38b9890469de67d0cb0f9d494b78b106ca1845f96c10b91c4ac", size = 19055 } wheels = [ { url = "https://files.pythonhosted.org/packages/37/c3/6eeb6034408dac0fa653d126c9204ade96b819c936e136c5e8a6897eee9c/socksio-1.0.0-py3-none-any.whl", hash = "sha256:95dc1f15f9b34e8d7b16f06d74b8ccf48f609af32ab33c608d08761c5dcbb1f3", size = 12763 }, ] [[package]] name = "strenum" version = "0.4.15" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/85/ad/430fb60d90e1d112a62ff57bdd1f286ec73a2a0331272febfddd21f330e1/StrEnum-0.4.15.tar.gz", hash = "sha256:878fb5ab705442070e4dd1929bb5e2249511c0bcf2b0eeacf3bcd80875c82eff", size = 23384 } wheels = [ { url = "https://files.pythonhosted.org/packages/81/69/297302c5f5f59c862faa31e6cb9a4cd74721cd1e052b38e464c5b402df8b/StrEnum-0.4.15-py3-none-any.whl", hash = "sha256:a30cda4af7cc6b5bf52c8055bc4bf4b2b6b14a93b574626da33df53cf7740659", size = 8851 }, ] [[package]] name = "toml" version = "0.10.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253 } wheels = [ { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588 }, ] [[package]] name = "tomli" version = "2.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c0/3f/d7af728f075fb08564c5949a9c95e44352e23dee646869fa104a3b2060a3/tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f", size = 15164 } wheels = [ { url = "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", size = 12757 }, ] [[package]] name = "tomli-w" version = "1.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/49/05/6bf21838623186b91aedbda06248ad18f03487dc56fbc20e4db384abde6c/tomli_w-1.0.0.tar.gz", hash = "sha256:f463434305e0336248cac9c2dc8076b707d8a12d019dd349f5c1e382dd1ae1b9", size = 6531 } wheels = [ { url = "https://files.pythonhosted.org/packages/bb/01/1da9c66ecb20f31ed5aa5316a957e0b1a5e786a0d9689616ece4ceaf1321/tomli_w-1.0.0-py3-none-any.whl", hash = "sha256:9f2a07e8be30a0729e533ec968016807069991ae2fd921a78d42f429ae5f4463", size = 5984 }, ] [[package]] name = "typer" version = "0.12.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "rich" }, { name = "shellingham" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c5/58/a79003b91ac2c6890fc5d90145c662fd5771c6f11447f116b63300436bc9/typer-0.12.5.tar.gz", hash = "sha256:f592f089bedcc8ec1b974125d64851029c3b1af145f04aca64d69410f0c9b722", size = 98953 } wheels = [ { url = "https://files.pythonhosted.org/packages/a8/2b/886d13e742e514f704c33c4caa7df0f3b89e5a25ef8db02aa9ca3d9535d5/typer-0.12.5-py3-none-any.whl", hash = "sha256:62fe4e471711b147e3365034133904df3e235698399bc4de2b36c8579298d52b", size = 47288 }, ] [[package]] name = "types-toml" version = "0.10.8.20240310" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/86/47/3e4c75042792bff8e90d7991aa5c51812cc668828cc6cce711e97f63a607/types-toml-0.10.8.20240310.tar.gz", hash = "sha256:3d41501302972436a6b8b239c850b26689657e25281b48ff0ec06345b8830331", size = 4392 } wheels = [ { url = "https://files.pythonhosted.org/packages/da/a2/d32ab58c0b216912638b140ab2170ee4b8644067c293b170e19fba340ccc/types_toml-0.10.8.20240310-py3-none-any.whl", hash = "sha256:627b47775d25fa29977d9c70dc0cbab3f314f32c8d8d0c012f2ef5de7aaec05d", size = 4777 }, ] [[package]] name = "typing-extensions" version = "4.12.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } wheels = [ { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, ] [[package]] name = "wcwidth" version = "0.2.13" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 } wheels = [ { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 }, ] [[package]] name = "zabbix-cli-uio" version = "3.1.3" source = { editable = "." } dependencies = [ { name = "httpx", extra = ["socks"] }, { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "packaging" }, { name = "platformdirs" }, { name = "prompt-toolkit" }, { name = "pydantic" }, { name = "rich" }, { name = "strenum" }, { name = "tomli" }, { name = "tomli-w" }, { name = "typer" }, { name = "typing-extensions" }, ] [package.optional-dependencies] build = [ { name = "build" }, { name = "pyinstaller" }, ] test = [ { name = "freezegun" }, { name = "inline-snapshot" }, { name = "pytest" }, { name = "pytest-cov" }, ] [package.metadata] requires-dist = [ { name = "build", marker = "extra == 'build'" }, { name = "freezegun", marker = "extra == 'test'" }, { name = "httpx", extras = ["socks"], specifier = ">=0.26.0" }, { name = "importlib-metadata", marker = "python_full_version < '3.10'", specifier = ">=8.5.0" }, { name = "inline-snapshot", marker = "extra == 'test'" }, { name = "packaging", specifier = ">=22.0" }, { name = "platformdirs", specifier = ">=2.5.4" }, { name = "prompt-toolkit", specifier = ">=3.0.47" }, { name = "pydantic", specifier = ">=2.7.0" }, { name = "pyinstaller", marker = "extra == 'build'" }, { name = "pytest", marker = "extra == 'test'" }, { name = "pytest-cov", marker = "extra == 'test'" }, { name = "rich", specifier = ">=13.3.1" }, { name = "strenum", specifier = ">=0.4.15" }, { name = "tomli", specifier = ">=2.0.1" }, { name = "tomli-w", specifier = ">=1.0.0" }, { name = "typer", specifier = ">=0.9.0" }, { name = "typing-extensions", specifier = ">=4.8.0" }, ] [[package]] name = "zipp" version = "3.20.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d3/8b/1239a3ef43a0d0ebdca623fb6413bc7702c321400c5fdd574f0b7aa0fbb4/zipp-3.20.1.tar.gz", hash = "sha256:c22b14cc4763c5a5b04134207736c107db42e9d3ef2d9779d465f5f1bcba572b", size = 23848 } wheels = [ { url = "https://files.pythonhosted.org/packages/07/9e/c96f7a4cd0bf5625bb409b7e61e99b1130dc63a98cb8b24aeabae62d43e8/zipp-3.20.1-py3-none-any.whl", hash = "sha256:9960cd8967c8f85a56f920d5d507274e74f9ff813a0ab8889a5b5be2daf44064", size = 8988 }, ] unioslo-zabbix-cli-09a2fab/zabbix_cli/000077500000000000000000000000001471265333400200305ustar00rootroot00000000000000unioslo-zabbix-cli-09a2fab/zabbix_cli/__about__.py000066400000000000000000000001451471265333400223100ustar00rootroot00000000000000from __future__ import annotations __version__ = "3.3.0" APP_NAME = "zabbix-cli" AUTHOR = "unioslo" unioslo-zabbix-cli-09a2fab/zabbix_cli/__init__.py000066400000000000000000000001331471265333400221360ustar00rootroot00000000000000from __future__ import annotations from zabbix_cli._patches import patch_all patch_all() unioslo-zabbix-cli-09a2fab/zabbix_cli/__main__.py000066400000000000000000000001541471265333400221220ustar00rootroot00000000000000from __future__ import annotations from zabbix_cli.main import main if __name__ == "__main__": main() unioslo-zabbix-cli-09a2fab/zabbix_cli/_patches/000077500000000000000000000000001471265333400216165ustar00rootroot00000000000000unioslo-zabbix-cli-09a2fab/zabbix_cli/_patches/__init__.py000066400000000000000000000004171471265333400237310ustar00rootroot00000000000000from __future__ import annotations from zabbix_cli._patches import typer def patch_all() -> None: """Apply all patches to all modules.""" typer.patch() # NOTE: we patch click_repl only when we actually launch the REPL # See: zabbix_cli.main.start_repl unioslo-zabbix-cli-09a2fab/zabbix_cli/_patches/click_repl.py000066400000000000000000000111221471265333400242740ustar00rootroot00000000000000# type: ignore """Patches for click_repl package.""" from __future__ import annotations import shlex import sys from typing import TYPE_CHECKING from typing import Any from typing import Dict from typing import Optional import click import click_repl from click.exceptions import Exit as ClickExit from click_repl import ExitReplException from click_repl import bootstrap_prompt from click_repl import dispatch_repl_commands from click_repl import handle_internal_commands from prompt_toolkit.shortcuts import prompt from zabbix_cli._patches.common import get_patcher from zabbix_cli.exceptions import handle_exception if TYPE_CHECKING: from click.core import Context from zabbix_cli.app import StatefulApp patcher = get_patcher(f"click_repl version: {click_repl.__version__}") def repl( # noqa: C901 old_ctx: Context, prompt_kwargs: Dict[str, Any] = None, allow_system_commands: bool = True, allow_internal_commands: bool = True, app: Optional[StatefulApp] = None, ) -> None: """Start an interactive shell. All subcommands are available in it. :param old_ctx: The current Click context. :param prompt_kwargs: Parameters passed to :py:func:`prompt_toolkit.shortcuts.prompt`. If stdin is not a TTY, no prompt will be printed, but only commands read from stdin. """ # parent should be available, but we're not going to bother if not group_ctx = old_ctx.parent or old_ctx group = group_ctx.command isatty = sys.stdin.isatty() # Delete the REPL command from those available, as we don't want to allow # nesting REPLs (note: pass `None` to `pop` as we don't want to error if # REPL command already not present for some reason). repl_command_name = old_ctx.command.name if isinstance(group_ctx.command, click.CommandCollection): available_commands = { cmd_name: cmd_obj for source in group_ctx.command.sources for cmd_name, cmd_obj in source.commands.items() } else: available_commands = group_ctx.command.commands available_commands.pop(repl_command_name, None) # Remove hidden commands available_commands = { cmd_name: cmd_obj for cmd_name, cmd_obj in available_commands.items() if not cmd_obj.hidden } group.commands = available_commands prompt_kwargs = bootstrap_prompt(prompt_kwargs, group) if isatty: def get_command(): return prompt(**prompt_kwargs) else: get_command = sys.stdin.readline while True: try: command = get_command() except KeyboardInterrupt: continue except EOFError: break if not command: if isatty: continue else: break if allow_system_commands and dispatch_repl_commands(command): continue if allow_internal_commands: try: result = handle_internal_commands(command) if isinstance(result, str): click.echo(result) continue except ExitReplException: break try: args = shlex.split(command) except ValueError as e: click.echo(f"{type(e).__name__}: {e}") continue try: if app: group = app.as_click_group() with group.make_context(None, args, parent=group_ctx) as ctx: group.invoke(ctx) ctx.exit() except click.ClickException as e: e.show() except ClickExit: pass except SystemExit: pass except ExitReplException: break # PATCH: Handle zabbix-cli exceptions except Exception as e: try: handle_exception(e) except SystemExit: pass # PATCH: Continue on keyboard interrupt except KeyboardInterrupt: from zabbix_cli.output.console import err_console # User likely pressed Ctrl+C during a prompt or when a spinner # was active. Ensure message is printed on a new line. # TODO: determine if last char in terminal was newline somehow! Can we? err_console.print("\n[red]Aborted.[/]") pass def patch_exception_handling() -> None: """Patch click_repl's exception handling to fall back on zabbix-cli exception handlers.""" with patcher("click_repl.repl"): click_repl.repl = repl def patch() -> None: """Apply all patches.""" patch_exception_handling() unioslo-zabbix-cli-09a2fab/zabbix_cli/_patches/common.py000066400000000000000000000037211471265333400234630ustar00rootroot00000000000000from __future__ import annotations from abc import ABC from abc import abstractmethod from typing import TYPE_CHECKING if TYPE_CHECKING: from types import TracebackType from typing import Optional from typing import Type class BasePatcher(ABC): """Context manager that logs and prints diagnostic info if an exception occurs. """ def __init__(self, description: str) -> None: self.description = description @abstractmethod def __package_info__(self) -> str: raise NotImplementedError def __enter__(self) -> BasePatcher: return self def __exit__( self, exc_type: Optional[Type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType], ) -> bool: if not exc_type: return True import sys import rich from rich.table import Table from zabbix_cli.__about__ import __version__ # Rudimentary, but provides enough info to debug and fix the issue console = rich.console.Console(stderr=True) console.print_exception() console.print() table = Table( title="Diagnostics", show_header=False, show_lines=False, ) table.add_row( "[b]Package [/]", self.__package_info__(), ) table.add_row( "[b]zabbix-cli [/]", __version__, ) table.add_row( "[b]Python [/]", sys.version, ) table.add_row( "[b]Platform [/]", sys.platform, ) console.print(table) console.print(f"[bold red]ERROR: Failed to patch {self.description}[/]") raise SystemExit(1) def get_patcher(info: str) -> Type[BasePatcher]: """Returns a patcher for a given package.""" class Patcher(BasePatcher): def __package_info__(self) -> str: return info return Patcher unioslo-zabbix-cli-09a2fab/zabbix_cli/_patches/typer.py000066400000000000000000000264511471265333400233430ustar00rootroot00000000000000# type: ignore """Patching of Typer to extend functionality and change styling. Will probably break for some version of Typer at some point. """ from __future__ import annotations import inspect from datetime import datetime from enum import Enum from pathlib import Path from typing import TYPE_CHECKING from typing import Any from typing import Callable from typing import Iterable from typing import Type from typing import Union from typing import cast from uuid import UUID import click import typer from typer.main import lenient_issubclass from typer.models import ParameterInfo from zabbix_cli._patches.common import get_patcher from zabbix_cli.pyzabbix.enums import APIStrEnum if TYPE_CHECKING: from typing import Dict from rich.style import Style patcher = get_patcher(f"Typer version: {typer.__version__}") def patch_help_text_style() -> None: """Remove dimming of help text. https://github.com/tiangolo/typer/issues/437#issuecomment-1224149402 """ with patcher("typer.rich_utils.STYLE_HELPTEXT"): typer.rich_utils.STYLE_HELPTEXT = "" def patch_help_text_spacing() -> None: """Adds a single blank line between short and long help text of a command when using `--help`. As of Typer 0.9.0, the short and long help text is printed without any blank lines between them. This is bad for readability (IMO). """ from rich.console import group from rich.markdown import Markdown from rich.text import Text from typer.rich_utils import DEPRECATED_STRING from typer.rich_utils import MARKUP_MODE_MARKDOWN from typer.rich_utils import MARKUP_MODE_RICH from typer.rich_utils import STYLE_DEPRECATED from typer.rich_utils import STYLE_HELPTEXT from typer.rich_utils import STYLE_HELPTEXT_FIRST_LINE from typer.rich_utils import MarkupMode from typer.rich_utils import _make_rich_rext @group() def _get_help_text( *, obj: Union[click.Command, click.Group], markup_mode: MarkupMode, ) -> Iterable[Union[Markdown, Text]]: """Build primary help text for a click command or group. Returns the prose help text for a command or group, rendered either as a Rich Text object or as Markdown. If the command is marked as deprecated, the deprecated string will be prepended. """ # Prepend deprecated status if obj.deprecated: yield Text(DEPRECATED_STRING, style=STYLE_DEPRECATED) # Fetch and dedent the help text help_text = inspect.cleandoc(obj.help or "") # Trim off anything that comes after \f on its own line help_text = help_text.partition("\f")[0] # Get the first paragraph first_line = help_text.split("\n\n")[0] # Remove single linebreaks if markup_mode != MARKUP_MODE_MARKDOWN and not first_line.startswith("\b"): first_line = first_line.replace("\n", " ") yield _make_rich_rext( text=first_line.strip(), style=STYLE_HELPTEXT_FIRST_LINE, markup_mode=markup_mode, ) # Get remaining lines, remove single line breaks and format as dim remaining_paragraphs = help_text.split("\n\n")[1:] if remaining_paragraphs: if markup_mode != MARKUP_MODE_RICH: # Remove single linebreaks remaining_paragraphs = [ x.replace("\n", " ").strip() if not x.startswith("\b") else "{}\n".format(x.strip("\b\n")) for x in remaining_paragraphs ] # Join back together remaining_lines = "\n".join(remaining_paragraphs) else: # Join with double linebreaks if markdown remaining_lines = "\n\n".join(remaining_paragraphs) yield _make_rich_rext( text="\n", style=STYLE_HELPTEXT, markup_mode=markup_mode, ) yield _make_rich_rext( text=remaining_lines, style=STYLE_HELPTEXT, markup_mode=markup_mode, ) with patcher("typer.rich_utils._get_help_text"): typer.rich_utils._get_help_text = _get_help_text def patch_generate_enum_convertor() -> None: """Patches enum value converter with an additional fallback to instantiating the enum with the value directly. """ def generate_enum_convertor(enum: Type[Enum]) -> Callable[[Any], Any]: lower_val_map = {str(val.value).lower(): val for val in enum} def convertor(value: Any) -> Any: if value is not None: low = str(value).lower() if low in lower_val_map: key = lower_val_map[low] return enum(key) # Fall back to passing in the value as-is try: return enum(value) except ValueError: return None return convertor with patcher("typer.main.generate_enum_convertor"): typer.main.generate_enum_convertor = generate_enum_convertor def patch_get_click_type() -> None: """Adds support for our custom `APIStrEnum` type. Used in conjunction with our custom generate_enum_convertor to support instantiating `APIStrEnum` with both the human-readable value and the API value (e.g. `"Enabled"` and `0`). Uses the `APIStrEnum.all_choices()` method to get the list of choices. """ def get_click_type( *, annotation: Any, parameter_info: ParameterInfo ) -> click.ParamType: if parameter_info.click_type is not None: return parameter_info.click_type elif parameter_info.parser is not None: return click.types.FuncParamType(parameter_info.parser) elif annotation == str: # noqa: E721 return click.STRING elif annotation == int: # noqa: E721 if parameter_info.min is not None or parameter_info.max is not None: min_ = None max_ = None if parameter_info.min is not None: min_ = int(parameter_info.min) if parameter_info.max is not None: max_ = int(parameter_info.max) return click.IntRange(min=min_, max=max_, clamp=parameter_info.clamp) else: return click.INT elif annotation == float: # noqa: E721 if parameter_info.min is not None or parameter_info.max is not None: return click.FloatRange( min=parameter_info.min, max=parameter_info.max, clamp=parameter_info.clamp, ) else: return click.FLOAT elif annotation == bool: # noqa: E721 return click.BOOL elif annotation == UUID: return click.UUID elif annotation == datetime: return click.DateTime(formats=parameter_info.formats) elif ( annotation == Path or parameter_info.allow_dash or parameter_info.path_type or parameter_info.resolve_path ): return click.Path( exists=parameter_info.exists, file_okay=parameter_info.file_okay, dir_okay=parameter_info.dir_okay, writable=parameter_info.writable, readable=parameter_info.readable, resolve_path=parameter_info.resolve_path, allow_dash=parameter_info.allow_dash, path_type=parameter_info.path_type, ) elif lenient_issubclass(annotation, typer.FileTextWrite): return click.File( mode=parameter_info.mode or "w", encoding=parameter_info.encoding, errors=parameter_info.errors, lazy=parameter_info.lazy, atomic=parameter_info.atomic, ) elif lenient_issubclass(annotation, typer.FileText): return click.File( mode=parameter_info.mode or "r", encoding=parameter_info.encoding, errors=parameter_info.errors, lazy=parameter_info.lazy, atomic=parameter_info.atomic, ) elif lenient_issubclass(annotation, typer.FileBinaryRead): return click.File( mode=parameter_info.mode or "rb", encoding=parameter_info.encoding, errors=parameter_info.errors, lazy=parameter_info.lazy, atomic=parameter_info.atomic, ) elif lenient_issubclass(annotation, typer.FileBinaryWrite): return click.File( mode=parameter_info.mode or "wb", encoding=parameter_info.encoding, errors=parameter_info.errors, lazy=parameter_info.lazy, atomic=parameter_info.atomic, ) # our patch for APIStrEnum elif lenient_issubclass(annotation, APIStrEnum): annotation = cast(Type[APIStrEnum], annotation) return click.Choice( annotation.all_choices(), case_sensitive=parameter_info.case_sensitive, ) elif lenient_issubclass(annotation, Enum): return click.Choice( [item.value for item in annotation], case_sensitive=parameter_info.case_sensitive, ) raise RuntimeError(f"Type not yet supported: {annotation}") # pragma no cover """Patch typer's get_click_type to support more types.""" with patcher("typer.main.get_click_type"): typer.main.get_click_type = get_click_type def patch__get_rich_console() -> None: from rich.console import Console from typer.rich_utils import COLOR_SYSTEM from typer.rich_utils import FORCE_TERMINAL from typer.rich_utils import MAX_WIDTH from typer.rich_utils import STYLE_METAVAR from typer.rich_utils import STYLE_METAVAR_SEPARATOR from typer.rich_utils import STYLE_NEGATIVE_OPTION from typer.rich_utils import STYLE_NEGATIVE_SWITCH from typer.rich_utils import STYLE_OPTION from typer.rich_utils import STYLE_SWITCH from typer.rich_utils import STYLE_USAGE from typer.rich_utils import highlighter from zabbix_cli.output.style import RICH_THEME theme: Dict[str, Union[str, Style]] = RICH_THEME.styles.copy() # type: ignore[assignment] theme.update( { "option": STYLE_OPTION, "switch": STYLE_SWITCH, "negative_option": STYLE_NEGATIVE_OPTION, "negative_switch": STYLE_NEGATIVE_SWITCH, "metavar": STYLE_METAVAR, "metavar_sep": STYLE_METAVAR_SEPARATOR, "usage": STYLE_USAGE, }, ) def _get_rich_console(stderr: bool = False) -> Console: return Console( theme=RICH_THEME, highlighter=highlighter, color_system=COLOR_SYSTEM, force_terminal=FORCE_TERMINAL, width=MAX_WIDTH, stderr=stderr, ) typer.rich_utils._get_rich_console = _get_rich_console def patch() -> None: """Apply all patches.""" patch_help_text_style() patch_help_text_spacing() patch_generate_enum_convertor() patch_get_click_type() patch__get_rich_console() unioslo-zabbix-cli-09a2fab/zabbix_cli/_types.py000066400000000000000000000003151471265333400217040ustar00rootroot00000000000000from __future__ import annotations import sys if sys.version_info >= (3, 10): from types import EllipsisType EllipsisType = EllipsisType else: from typing import Any EllipsisType = Any unioslo-zabbix-cli-09a2fab/zabbix_cli/_v2_compat.py000066400000000000000000000062651471265333400224440ustar00rootroot00000000000000"""Compatibility functions going from Zabbix-CLI v2 to v3. The functions in this module are intended to ease the transition by providing fallbacks to deprecated functionality in Zabbix-CLI v2. """ from __future__ import annotations import os from pathlib import Path from typing import TYPE_CHECKING from typing import List from typing import Optional import typer if TYPE_CHECKING: from click.core import CommandCollection from click.core import Group CONFIG_FILENAME = "zabbix-cli.conf" CONFIG_FIXED_NAME = "zabbix-cli.fixed.conf" # Config file locations CONFIG_DEFAULT_DIR = "/usr/share/zabbix-cli" CONFIG_SYSTEM_DIR = "/etc/zabbix-cli" CONFIG_USER_DIR = os.path.expanduser("~/.zabbix-cli") # Any item will overwrite values from the previous (NYI) CONFIG_PRIORITY = tuple( Path(os.path.join(d, f)) for d, f in ( (CONFIG_DEFAULT_DIR, CONFIG_FIXED_NAME), (CONFIG_SYSTEM_DIR, CONFIG_FIXED_NAME), (CONFIG_USER_DIR, CONFIG_FILENAME), (CONFIG_SYSTEM_DIR, CONFIG_FILENAME), (CONFIG_DEFAULT_DIR, CONFIG_FILENAME), ) ) AUTH_FILE = Path.home() / ".zabbix-cli_auth" AUTH_TOKEN_FILE = Path.home() / ".zabbix-cli_auth_token" def run_command_from_option(ctx: typer.Context, command: str) -> None: """Runs a command via old-style --command/-C option.""" from zabbix_cli.output.console import error from zabbix_cli.output.console import exit_err from zabbix_cli.output.console import warning warning( "The [i]--command/-C[/] option is deprecated and will be removed in a future release. " "Invoke command directly instead." ) if not isinstance(ctx.command, (CommandCollection, Group)): exit_err( # TODO: find out if this could ever happen? f"Cannot run command {command!r}. Ensure it is a valid command and try again." ) cmd_obj = ctx.command.get_command(ctx, command) if not cmd_obj: exit_err( f"Cannot run command {command!r}. Ensure it is a valid command and try again." ) try: ctx.invoke(cmd_obj, *ctx.args) except typer.Exit: pass except Exception as e: error( f"Command {command!r} failed with error: {e}. Try re-running without --command." ) def args_callback( ctx: typer.Context, value: Optional[List[str]] ) -> Optional[List[str]]: if ctx.resilient_parsing: return # for auto-completion if value: from zabbix_cli.output.console import warning warning( f"Detected deprecated positional arguments {value}. Use options instead." ) # NOTE: Must NEVER return None. The "fix" in Typer 0.10.0 for None defaults # somehow broke the parsing of callback values by causing values returned by # callbacks to be passed to the internal converter, which then fails # because it expects a list but gets None. # https://github.com/tiangolo/typer/pull/664 # https://github.com/tiangolo/typer/blob/142422a14ca4c6a8ad579e9bd0fd0728364d86e3/typer/main.py#L639 return value or [] ARGS_POSITIONAL = typer.Argument( None, help="DEPRECATED: V2-style positional arguments.", show_default=False, hidden=True, callback=args_callback, ) unioslo-zabbix-cli-09a2fab/zabbix_cli/app/000077500000000000000000000000001471265333400206105ustar00rootroot00000000000000unioslo-zabbix-cli-09a2fab/zabbix_cli/app/__init__.py000066400000000000000000000027041471265333400227240ustar00rootroot00000000000000from __future__ import annotations from zabbix_cli.commands import bootstrap_commands # type: ignore # noqa: E402, F401 from .app import * # noqa: F403 # wildcard import to avoid circular import (why?) from .app import StatefulApp # explicit import for type checker app = StatefulApp( name="zabbix-cli", help="Zabbix-CLI is a command line interface for Zabbix.", add_completion=True, rich_markup_mode="rich", ) # Import commands to register them with the app from zabbix_cli.commands import cli # type: ignore # noqa: E402, F401, I001 from zabbix_cli.commands import export # type: ignore # noqa: E402, F401 from zabbix_cli.commands import host # type: ignore # noqa: E402, F401 from zabbix_cli.commands import hostgroup # type: ignore # noqa: E402, F401 from zabbix_cli.commands import item # type: ignore # noqa: E402, F401 from zabbix_cli.commands import macro # type: ignore # noqa: E402, F401 from zabbix_cli.commands import maintenance # type: ignore # noqa: E402, F401 from zabbix_cli.commands import problem # type: ignore # noqa: E402, F401 from zabbix_cli.commands import proxy # type: ignore # noqa: E402, F401 from zabbix_cli.commands import template # type: ignore # noqa: E402, F401 from zabbix_cli.commands import templategroup # type: ignore # noqa: E402, F401 from zabbix_cli.commands import user # type: ignore # noqa: E402, F401 # Import dev commands # TODO: Disable by default, enable with a flag. bootstrap_commands() unioslo-zabbix-cli-09a2fab/zabbix_cli/app/app.py000066400000000000000000000164701471265333400217520ustar00rootroot00000000000000"""In order to mimick the API of Zabbix-cli < 3.0.0, we define a single app object here, which we share between the different command modules. Thus, every command is part of the same command group. """ from __future__ import annotations import inspect import logging from types import ModuleType from typing import TYPE_CHECKING from typing import Any from typing import Callable from typing import Dict from typing import Iterable from typing import List from typing import NamedTuple from typing import Optional from typing import Protocol from typing import Tuple from typing import Type from typing import Union import typer from typer.core import TyperCommand from typer.core import TyperGroup from typer.main import Typer from typer.main import get_group from typer.models import CommandFunctionType from typer.models import CommandInfo as TyperCommandInfo from typer.models import Default from zabbix_cli.app.plugins import PluginLoader from zabbix_cli.logs import logger from zabbix_cli.state import State from zabbix_cli.state import get_state if TYPE_CHECKING: from rich.console import RenderableType from rich.status import Status from rich.style import StyleType from zabbix_cli.config.model import Config from zabbix_cli.config.model import PluginConfig class Example(NamedTuple): """Example command usage.""" description: str command: str return_value: Optional[str] = None def __str__(self) -> str: return f" [i]{self.description}[/]\n\n [example]{self.command}[/]" # TODO: Trigger example rendering only when user calls --help, so we don't build # the help text for every command on startup. # Need to investigate ctx.get_help() and how it's used. # The question is whether we have to monkeypatch this or if we can do it with # the current typer/click API class CommandInfo(TyperCommandInfo): def __init__( self, *args: Any, examples: Optional[List[Example]] = None, **kwargs: Any ) -> None: super().__init__(*args, **kwargs) self.examples = examples or [] self.set_command_help() def set_command_help(self) -> None: if not self.help: self.help = inspect.getdoc(self.callback) or "" if not self.short_help: self.short_help = self.help.split("\n")[0] self._set_command_examples() def _set_command_examples(self) -> None: if not self.examples or not self.help: return examples = [str(e) for e in self.examples] examples.insert(0, "\n\n[bold underline]Examples[/]") self.help = self.help.strip() self.help += "\n\n".join(examples) class StatusCallable(Protocol): """Function that returns a Status object. Protocol for rich.console.Console.status method. """ def __call__( self, status: RenderableType, *, spinner: str = "dots", spinner_style: StyleType = "status.spinner", speed: float = 1.0, refresh_per_second: float = 12.5, ) -> Status: ... class StatefulApp(typer.Typer): """A Typer app that provides access to the global state.""" parent: Optional[StatefulApp] plugins: Dict[str, ModuleType] # NOTE: might be a good idea to add a typing.Unpack definition for the kwargs? def __init__(self, **kwargs: Any) -> None: self.parent = None self._plugin_loader = PluginLoader() super().__init__(**kwargs) @property def logger(self) -> logging.Logger: return logger # Methods for adding subcommands and keeping track of hierarchy def add_typer(self, typer_instance: Typer, **kwargs: Any) -> None: kwargs.setdefault("no_args_is_help", True) if isinstance(typer_instance, StatefulApp): typer_instance.parent = self return super().add_typer(typer_instance, **kwargs) def add_subcommand(self, app: typer.Typer, *args: Any, **kwargs: Any) -> None: kwargs.setdefault("rich_help_panel", "Subcommands") self.add_typer(app, **kwargs) def load_plugins(self, config: Config) -> None: """Load plugins.""" self._plugin_loader.load(config) def configure_plugins(self, config: Config) -> None: """Configure plugins.""" self._plugin_loader.configure_plugins(config) def parents(self) -> Iterable[StatefulApp]: """Get all parent apps.""" app = self while app.parent: yield app.parent app = app.parent def find_root(self) -> StatefulApp: """Get the root app.""" app = self for parent in self.parents(): app = parent return app def as_click_group(self) -> TyperGroup: """Return the Typer app as a Click group.""" return get_group(self) def command( self, name: Optional[str] = None, *, cls: Optional[Type[TyperCommand]] = None, context_settings: Optional[Dict[Any, Any]] = None, help: Optional[str] = None, epilog: Optional[str] = None, short_help: Optional[str] = None, options_metavar: str = "[OPTIONS]", add_help_option: bool = True, no_args_is_help: bool = False, hidden: bool = False, deprecated: bool = False, # Rich settings rich_help_panel: Union[str, None] = Default(None), # Zabbix-cli kwargs examples: Optional[List[Example]] = None, ) -> Callable[[CommandFunctionType], CommandFunctionType]: if cls is None: cls = TyperCommand def decorator(f: CommandFunctionType) -> CommandFunctionType: self.registered_commands.append( CommandInfo( name=name, cls=cls, context_settings=context_settings, callback=f, help=help, epilog=epilog, short_help=short_help, options_metavar=options_metavar, add_help_option=add_help_option, no_args_is_help=no_args_is_help, hidden=hidden, deprecated=deprecated, # Rich settings rich_help_panel=rich_help_panel, # Zabbix-cli kwargs examples=examples, ) ) return f return decorator @property def state(self) -> State: return get_state() @property def api_version(self) -> Tuple[int, ...]: """Get the current API version. Will fail if not connected to the API.""" return self.state.client.version.release @property def status(self) -> StatusCallable: return self.state.err_console.status def get_plugin_config(self, name: str) -> PluginConfig: """Get a plugin's configuration by name. Returns an empty PluginConfig object if no config is found. """ conf = self.state.config.plugins.get(name) if not conf: # NOTE: can we import this top-level? We have probably already imported # the config at this point? Unless we refactor config loading _again_...? from zabbix_cli.config.model import PluginConfig logger.error(f"Plugin '{name}' not found in configuration") return PluginConfig() return conf unioslo-zabbix-cli-09a2fab/zabbix_cli/app/plugins.py000066400000000000000000000154641471265333400226550ustar00rootroot00000000000000from __future__ import annotations import logging import sys from pathlib import Path from types import ModuleType from typing import TYPE_CHECKING from typing import Dict from typing import Protocol from typing import Tuple from typing import cast from typing import runtime_checkable if sys.version_info < (3, 10): from importlib_metadata import EntryPoint else: from importlib.metadata import EntryPoint from zabbix_cli.exceptions import PluginLoadError from zabbix_cli.exceptions import PluginPostImportError from zabbix_cli.output.console import error if TYPE_CHECKING: from zabbix_cli.config.model import Config from zabbix_cli.config.model import PluginConfig logger = logging.getLogger(__name__) @runtime_checkable class PluginModule(Protocol): def __configure__(self, config: PluginConfig) -> None: ... class PluginLoader: def __init__(self) -> None: self.plugins: Dict[str, ModuleType] = {} def load(self, config: Config) -> None: self._load_plugins(config) self._load_plugins_from_metadata(config) def _load_plugins(self, config: Config) -> None: """Load plugins from local Python modules.""" logger.debug("Loading plugins from modules in configuration file.") for name, plugin_config in config.plugins.root.items(): if not plugin_config.module: logger.debug("Plugin %s has no module defined. Skipping", name) continue if not plugin_config.enabled: logger.debug("Plugin %s is disabled, skipping", name) continue logger.debug("Loading plugin: %s", name) try: module = load_plugin_module(name, plugin_config) except Exception as e: # If the exception is already a PluginLoadError # or a subclass, use that, otherwise create a new # PluginLoadError so we get the correct message format. if not isinstance(e, PluginLoadError): exc = PluginLoadError(name, plugin_config) else: exc = e if not plugin_config.optional: raise exc from e else: # Use message created by the PluginLoadError msg = exc.args[0] if exc.args else str(exc) error(msg, exc_info=True) else: self._add_plugin(name, module) def _load_plugins_from_metadata(self, config: Config) -> None: """Load plugins from Python package entry points.""" # Use backport for Python < 3.10 if sys.version_info < (3, 10): from importlib_metadata import ( entry_points, # pyright: ignore[reportUnknownVariableType] ) else: from importlib.metadata import entry_points discovered_plugins = entry_points(group="zabbix-cli.plugins") # HACK: Cast Tuple[Any, ...] result to concrete type. # In order to pass type checking on all Python versions, # we need to pretend that we are developing on 3.8 and # using the backport. The problem is that the backport # does not define a generic tuple type for the result, # and instead just subclasses tuple. So we need to cast # the result to the correct type. # This is one of the drawbacks of running in 3.8 mode, but # it's necessary to ensure we don't introduce features that # do not exist in our minimum supported version. discovered_plugins = cast(Tuple[EntryPoint], discovered_plugins) for plugin in discovered_plugins: conf = config.plugins.get(plugin.name) try: module = load_plugin_from_entry_point(plugin) except Exception as e: # By default, broken plugings will not break the application # unless they are explicitly marked as required in the config. # This is a deviation from the standard behavior of plugins, but # since these are third party plugins, we want to be more lenient # and not break the entire application if a plugin is broken. if not conf or not conf.optional: raise PluginLoadError(plugin.name, conf) from e else: error(f"Error loading plugin {plugin.name}: {e}", exc_info=True) else: self._add_plugin(plugin.name, module) def _add_plugin(self, name: str, module: ModuleType) -> None: self.plugins[name] = module logger.info("Plugin loaded: %s", name) def configure_plugins(self, config: Config) -> None: for name, module in self.plugins.items(): if not isinstance(module, PluginModule): logger.debug("Plugin %s has no __configure__ function. Skipping", name) continue plugin_config = config.plugins.root.get(name) if not plugin_config: logger.warning( "No configuration found for plugin '%s'. Cannot run __configure__", name, ) continue try: module.__configure__(plugin_config) except Exception as e: raise PluginPostImportError(name, plugin_config) from e def load_plugin_module(plugin_name: str, plugin_config: PluginConfig) -> ModuleType: """Load plugin from a Python module.""" name_or_path = plugin_config.module p = Path(name_or_path) if p.exists(): logger.debug("Loading plugin %s from file: %s", plugin_name, p) mod = _load_module_from_file(name_or_path, p) else: logger.debug("Importing plugin %s as module '%s'", plugin_name, name_or_path) mod = _load_module(name_or_path) return mod def _load_module_from_file(mod: str, file: Path) -> ModuleType: """Load a module from a file.""" import importlib.util spec = importlib.util.spec_from_file_location(mod, str(file.resolve())) if not spec or not spec.loader: raise ImportError(f"Could not load module from file: {file}") module = importlib.util.module_from_spec(spec) sys.modules[mod] = module spec.loader.exec_module(module) logger.info("Loaded module from file: %s", file) return module def _load_module(mod: str) -> ModuleType: """Load a module.""" import importlib module = importlib.import_module(mod) logger.info("Loaded module: %s", mod) return module def load_plugin_from_entry_point(entry_point: EntryPoint) -> ModuleType: """Load a plugin from an entry point.""" logger.debug("Loading plugin %s from metadata", entry_point.name) module = entry_point.load() if not isinstance(module, ModuleType): return sys.modules[entry_point.module] return module unioslo-zabbix-cli-09a2fab/zabbix_cli/auth.py000066400000000000000000000403101471265333400213410ustar00rootroot00000000000000""" "Module for authenticating with the Zabbix API as well as loading/storing authentication information. Manages the following: - Loading and saving auth token files (file containing API session token) - Loading and saving auth files (file containing username and password) - Loading username and password from environment variables - Prompting for username and password - Logging in to the Zabbix API using one of the above methods """ from __future__ import annotations import logging import os import sys from functools import cached_property from pathlib import Path from typing import TYPE_CHECKING from typing import Final from typing import List from typing import NamedTuple from typing import Optional from typing import Tuple from typing import Union from rich.console import ScreenContext from strenum import StrEnum from zabbix_cli._v2_compat import AUTH_FILE as AUTH_FILE_LEGACY from zabbix_cli._v2_compat import AUTH_TOKEN_FILE as AUTH_TOKEN_FILE_LEGACY from zabbix_cli.config.constants import AUTH_FILE from zabbix_cli.config.constants import AUTH_TOKEN_FILE from zabbix_cli.config.constants import ConfigEnvVars from zabbix_cli.exceptions import AuthError from zabbix_cli.exceptions import AuthTokenFileError from zabbix_cli.exceptions import ZabbixAPIException from zabbix_cli.logs import add_user from zabbix_cli.output.console import err_console from zabbix_cli.output.console import error from zabbix_cli.output.console import exit_err from zabbix_cli.output.console import warning from zabbix_cli.output.prompts import str_prompt from zabbix_cli.pyzabbix.client import ZabbixAPI if TYPE_CHECKING: from zabbix_cli.config.model import Config logger = logging.getLogger(__name__) # Auth file location SECURE_PERMISSIONS: Final[int] = 0o600 SECURE_PERMISSIONS_STR = format(SECURE_PERMISSIONS, "o") class LoginCredentialType(StrEnum): """Types of valid login credentials.""" PASSWORD = "username and password" AUTH_TOKEN = "auth token" class CredentialsSource(StrEnum): """Source of login credentials.""" ENV = "env" FILE = "file" PROMPT = "prompt" CONFIG = "config" class LoginInfo(NamedTuple): credentials: Credentials token: str class Credentials(NamedTuple): """Credentials for logging in to the Zabbix API.""" source: Optional[CredentialsSource] = None username: Optional[str] = None password: Optional[str] = None auth_token: Optional[str] = None @property def type(self) -> Optional[LoginCredentialType]: if self.auth_token: return LoginCredentialType.AUTH_TOKEN if self.username and self.password: return LoginCredentialType.PASSWORD return None def is_valid(self) -> bool: """Check if credentials are valid (non-empty).""" return self.type is not None class Authenticator: """Encapsulates logic for authenticating with the Zabbix API using various methods, as well as storing and loading auth tokens.""" config: Config def __init__(self, config: Config) -> None: self.config = config @cached_property def screen(self) -> ScreenContext: return err_console.screen() def login(self) -> ZabbixAPI: """Log in to the Zabbix API using the configured credentials. Returns the Zabbix API client object. If multiple methods are available, they are tried in the following order: 1. API token in config file 2. API token in environment variables 3. API token in file (if `use_auth_token_file=true`) 4. Username and password in config file 5. Username and password in auth file 6. Username and password in environment variables 7. Username and password from prompt """ # Ensure we have a Zabbix API URL self.config.api.url = self.get_zabbix_url() client = ZabbixAPI.from_config(self.config) info = self._do_login(client) if self.config.app.use_auth_token_file: write_auth_token_file(self.config.api.username, info.token) if info.credentials.username: add_user(info.credentials.username) if info.credentials.type == LoginCredentialType.AUTH_TOKEN: logger.info("Logged in using auth token from %s", info.credentials.source) else: logger.info( "Logged in as %s using username and password from %s", info.credentials.username, info.credentials.source, ) self._update_config(info.credentials) return client def _do_login(self, client: ZabbixAPI) -> LoginInfo: for func in [ self._get_auth_token_config, self._get_auth_token_env, self._get_auth_token_file, self._get_username_password_config, self._get_username_password_auth_file, self._get_username_password_env, self._get_username_password_prompt, ]: try: credentials = func() if not credentials.is_valid(): logger.debug("No valid credentials found with %s", func.__name__) continue logger.debug( "Attempting to log in with %s from %s", credentials.type, credentials.source, ) token = self.login_with_credentials(client, credentials) return LoginInfo(credentials, token) except ZabbixAPIException as e: logger.warning("Failed to log in with %s: %s", func.__name__, e) continue except Exception as e: logger.error( "Unexpected error logging in with %s: %s", func.__name__, e ) continue else: raise AuthError( f"No authentication method succeeded for {self.config.api.url}. Check the logs for more information." ) def login_with_credentials( self, client: ZabbixAPI, credentials: Credentials ) -> str: """Log in to the Zabbix API using the provided credentials.""" return client.login( user=credentials.username, password=credentials.password, auth_token=credentials.auth_token, ) def _update_config(self, credentials: Credentials) -> None: """Update the config with credentials from the successful login.""" from pydantic import SecretStr if credentials.username: self.config.api.username = credentials.username # Only update secrets if they were already set in config and new ones are different. # we do not want to assign secrets to a config that does not already have them. # I.e. user logs in via file, prompt, env var, etc., which should not # assign those secrets to the config. ONLY prompt should assign secrets. # TODO: Better introspection of login method to determine if secrets should be updated. if ( # we have a token in the config file (config_password := self.config.api.password.get_secret_value()) and credentials.password and config_password != credentials.password ): self.config.api.password = SecretStr(credentials.password) if ( # we have a token in the config file (config_token := self.config.api.auth_token.get_secret_value()) and credentials.auth_token and config_token != credentials.auth_token ): self.config.api.auth_token = SecretStr(credentials.auth_token) def get_zabbix_url(self) -> str: """Get the URL of the Zabbix server from the config, or prompt for it.""" if not self.config.api.url: with self.screen: return str_prompt("Zabbix URL (without /api_jsonrpc.php)") return self.config.api.url def _get_username_password_env(self) -> Credentials: """Get username and password from environment variables.""" return Credentials( username=os.environ.get(ConfigEnvVars.USERNAME), password=os.environ.get(ConfigEnvVars.PASSWORD), source=CredentialsSource.ENV, ) def _get_auth_token_env(self) -> Credentials: """Get auth token from environment variables.""" return Credentials( auth_token=os.environ.get(ConfigEnvVars.API_TOKEN), source=CredentialsSource.ENV, ) def _get_username_password_auth_file( self, ) -> Credentials: """Get username and password from environment variables.""" path, contents = self.load_auth_file() if path: logger.debug("Loaded auth file %s", path) username, password = _parse_auth_file_contents(contents) return Credentials( username=username, password=password, source=CredentialsSource.FILE, ) def _get_username_password_config( self, ) -> Credentials: """Get username and password from config file.""" return Credentials( username=self.config.api.username, password=self.config.api.password.get_secret_value(), source=CredentialsSource.CONFIG, ) def _get_username_password_prompt( self, ) -> Credentials: """Get username and password from a prompt in a separate screen.""" with self.screen: username = str_prompt( "Username", default=self.config.api.username, empty_ok=False ) password = str_prompt("Password", password=True, empty_ok=False) return Credentials( username=username, password=password, source=CredentialsSource.PROMPT ) def _get_auth_token_config(self) -> Credentials: return Credentials( auth_token=self.config.api.auth_token.get_secret_value(), source=CredentialsSource.CONFIG, ) def _get_auth_token_file(self) -> Credentials: if not self.config.app.use_auth_token_file: logger.debug("Not configured to use auth token file.") return Credentials() path, contents = self.load_auth_token_file() username, auth_token = _parse_auth_file_contents(contents) # Found token, but does not match configured username if auth_token and username and username != self.config.api.username: warning( f"Ignoring existing auth token in auth file {path}: " f"Username {username!r} in file does not match username {self.config.api.username!r} in configuration file." ) auth_token = None return Credentials( username=username, auth_token=auth_token, source=CredentialsSource.FILE ) def load_auth_token_file(self) -> Union[Tuple[Path, str], Tuple[None, None]]: paths = get_auth_token_file_paths(self.config) for path in paths: contents = self._do_load_auth_file(path) if contents: return path, contents logger.info( f"No auth token file found. Searched in {', '.join(str(p) for p in paths)}" ) return None, None def load_auth_file(self) -> Tuple[Optional[Path], Optional[str]]: """Attempts to load an auth file.""" paths = get_auth_file_paths(self.config) for path in paths: contents = self._do_load_auth_file(path) if contents: return path, contents logger.info( f"No auth file found. Searched in {', '.join(str(p) for p in paths)}" ) return None, None def _do_load_auth_file(self, file: Path) -> Optional[str]: """Attempts to read the contents of an auth (token) file. Returns None if the file does not exist or is not secure. """ if not file.exists(): return None if ( not self.config.app.allow_insecure_auth_file and not file_has_secure_permissions(file) ): error( f"Auth file {file} must have {SECURE_PERMISSIONS_STR} permissions, has {oct(get_file_permissions(file))}. Refusing to load." ) return None return file.read_text().strip() def login(config: Config) -> ZabbixAPI: """Log in to the Zabbix API using credentials from the config. Returns the Zabbix API client object. """ auth = Authenticator(config) client = auth.login() return client def logout(client: ZabbixAPI, config: Config) -> None: """Log out of the current Zabbix API session.""" try: client.logout() if config.app.use_auth_token_file: clear_auth_token_file(config) except ZabbixAPIException as e: exit_err(f"Failed to log out of Zabbix API session: {e}") except AuthTokenFileError as e: exit_err(str(e)) def prompt_username_password(username: str) -> Tuple[str, str]: """Re-useable prompt for username and password.""" username = str_prompt("Username", default=username, empty_ok=False) password = str_prompt("Password", password=True, empty_ok=False) return username, password def _parse_auth_file_contents( contents: Optional[str], ) -> Tuple[Optional[str], Optional[str]]: """Parse the contents of an auth file. We store auth files in the format `username::secret`. """ if contents: lines = contents.splitlines() if lines: line = lines[0].strip() username, _, secret = line.partition("::") return username, secret return None, None def get_auth_file_paths(config: Optional[Config] = None) -> List[Path]: """Get all possible auth token file paths.""" paths = [ AUTH_FILE, AUTH_FILE_LEGACY, ] if config and config.app.auth_file not in paths: # config has custom path paths.insert(0, config.app.auth_file) return paths def get_auth_token_file_paths(config: Optional[Config] = None) -> List[Path]: """Get all possible auth token file paths.""" paths = [ AUTH_TOKEN_FILE, AUTH_TOKEN_FILE_LEGACY, ] if config and config.app.auth_token_file not in paths: # config has custom path paths.insert(0, config.app.auth_token_file) return paths def write_auth_token_file( username: str, auth_token: str, file: Path = AUTH_TOKEN_FILE ) -> Path: """Write a username/auth token pair to the auth token file.""" contents = f"{username}::{auth_token}" if not file.exists(): try: file.touch(mode=SECURE_PERMISSIONS) except OSError as e: raise AuthTokenFileError( f"Unable to create auth token file {file}. " "Change the location or disable auth token file in config." ) from e elif not file_has_secure_permissions(file): try: file.chmod(SECURE_PERMISSIONS) except OSError as e: raise AuthTokenFileError( f"Unable to set secure permissions ({SECURE_PERMISSIONS_STR}) on {file} when saving auth token. " "Change permissions manually or delete the file." ) from e file.write_text(contents) logger.info(f"Wrote auth token file {file}") return file def clear_auth_token_file(config: Optional[Config] = None) -> None: """Clear the contents of the auth token file. Attempts to clear both the new and the old auth token file locations. Optionally also clears the loaded auth token from the config object. """ for file in get_auth_token_file_paths(config): if not file.exists(): continue try: file.write_text("") logger.debug("Cleared auth token file contents '%s'", file) except OSError as e: raise AuthTokenFileError( f"Unable to clear auth token file {file}: {e}" ) from e def file_has_secure_permissions(file: Path) -> bool: """Check if a file has secure permissions. Always returns True on Windows. """ if sys.platform == "win32": return True return get_file_permissions(file) == SECURE_PERMISSIONS def get_file_permissions(file: Path) -> int: """Get the 3 digit octal permissions of a file.""" return file.stat().st_mode & 0o777 unioslo-zabbix-cli-09a2fab/zabbix_cli/bulk.py000066400000000000000000000210041471265333400213340ustar00rootroot00000000000000"""Module for running commands in bulk from a file. Uses a very rudimentary parser to parse commands from a file, then passes them to typer.Context.invoke() to run them. """ from __future__ import annotations import logging import shlex from contextlib import contextmanager from dataclasses import dataclass from enum import Enum from pathlib import Path from typing import Counter from typing import List from typing import Optional import typer import typer.core from pydantic import BaseModel from pydantic import Field from strenum import StrEnum from typing_extensions import Self from zabbix_cli.exceptions import CommandFileError from zabbix_cli.output.console import warning from zabbix_cli.state import get_state from zabbix_cli.utils.fs import read_file logger = logging.getLogger(__name__) class LineParseError(CommandFileError): """Line cannot be parsed.""" # NOTE: these exceptions are used for control flow only class SkippableLine(LineParseError): """Line will be skipped during parsing.""" class EmptyLine(SkippableLine): """Line is empty.""" class CommentLine(SkippableLine): """Line is a comment.""" class BulkCommand(BaseModel): """A command to be run in bulk.""" args: List[str] = Field(default_factory=list) line: str = "" # original line from file line_number: int = 0 def __str__(self) -> str: if self.line: return self.line return " ".join(self.args) @classmethod def from_line(cls, line: str, line_number: int = 0) -> Self: """Parse a command line into a BulkCommand.""" # Early returns for empty lines and comments line = line.strip() if not line: raise EmptyLine("Cannot parse empty line") if line.startswith("#"): raise CommentLine("Cannot parse comment line") # Split the line into tokens, handling quotes and comments args = shlex.split(line, comments=True) if not args: raise LineParseError("No command specified") return cls(args=args, line=line, line_number=line_number) def __repr__(self) -> str: return f"<{self.__class__.__name__} {self}>" class CommandResult(Enum): """Result of a command execution.""" SUCCESS = "success" FAILURE = "failure" SKIPPED = "skipped" @dataclass class CommandExecution: """Represents the execution of a single command.""" command: BulkCommand result: CommandResult error: Optional[BaseException] = None line_number: Optional[int] = None class BulkRunnerMode(StrEnum): """Mode of operation for BulkRunner.""" STRICT = "strict" # Stop on first error CONTINUE = "continue" # Continue on errors, report at end SKIP = "skip" # Skip lines that can't be parsed class BulkRunner: def __init__( self, ctx: typer.Context, file: Path, mode: BulkRunnerMode = BulkRunnerMode.STRICT, ) -> None: self.ctx = ctx self.file = file self.mode = mode self.executions: List[CommandExecution] = [] """Commands that were executed.""" self.skipped: List[CommandExecution] = [] """Lines that were skipped during parsing.""" @contextmanager def _command_context(self, command: BulkCommand): """Context manager for command execution with proper error handling.""" def add_success() -> None: self.executions.append( CommandExecution( command, CommandResult.SUCCESS, line_number=command.line_number ) ) logger.info("Command succeeded: %s", command) def add_failure(e: BaseException) -> None: self.executions.append( CommandExecution( command, CommandResult.FAILURE, error=e, line_number=command.line_number, ) ) if self.mode == BulkRunnerMode.STRICT: raise CommandFileError( f"Command failed: [command]{command}[/]: {e}" ) from e else: logger.error("Command failed: %s - %s", command, e) try: yield add_success() except (SystemExit, typer.Exit) as e: # If we get return code 0 on an exit, we consider it a success code = e.code if isinstance(e, SystemExit) else e.exit_code if code == 0: add_success() else: add_failure(e) except Exception as e: add_failure(e) def run_bulk(self) -> Counter[CommandResult]: """Run commands in bulk from a file where each line is a CLI command. Returns: Dict[CommandResult, int]: Count of commands by result status Raises: CommandFileError: If command file cannot be parsed or a command fails (in STRICT mode) Example: ```bash $ cat /tmp/commands.txt # We can add comments and empty lines # We can use old-form positional arguments create_host test000001.example.net All-manual-hosts .+ 1 # Or new-form keyword arguments create_host test000002.example.net --hostgroup All-manual-hosts --proxy .+ --status 1 """ # top-level Click command group for the application # Contains all commands defined via @app.command() group = self.ctx.command commands = self.load_command_file() for command in commands: with group.make_context(None, command.args, parent=self.ctx) as ctx: with self._command_context(command): group.invoke(ctx) # Generate summary results: Counter[CommandResult] = Counter() for execution in self.executions: results[execution.result] += 1 # Log summary total = sum(results.values()) logger.info( "Bulk execution complete. Total: %d, Succeeded: %d, Failed: %d, Skipped: %d", total, results[CommandResult.SUCCESS], results[CommandResult.FAILURE], results[CommandResult.SKIPPED], ) # In CONTINUE mode, raise error if any commands failed if self.mode == BulkRunnerMode.CONTINUE and results[CommandResult.FAILURE] > 0: failed_commands = [ f"Line {e.line_number}: [command]{e.command}[/] [i]({e.error})[/]" for e in self.executions if e.result == CommandResult.FAILURE ] raise CommandFileError( f"{results[CommandResult.FAILURE]} commands failed:\n" + "\n".join(failed_commands) ) return results def load_command_file(self) -> List[BulkCommand]: """Parse the contents of a command file.""" try: contents = read_file(self.file) except Exception as e: raise CommandFileError(f"Could not read command file: {e}") from e commands: List[BulkCommand] = [] def add_skipped( line: str, line_number: int, error: Optional[BaseException] = None ) -> None: self.skipped.append( CommandExecution( BulkCommand(line=line, line_number=line_number), CommandResult.SKIPPED, error=error, line_number=line_number, ) ) for lineno, line in enumerate(contents.splitlines(), start=1): try: command = BulkCommand.from_line(line, line_number=lineno) commands.append(command) except SkippableLine: logger.debug("Skipping line %d: %s", lineno, line) add_skipped(line, lineno) except Exception as e: if self.mode == BulkRunnerMode.SKIP: add_skipped(line, lineno, e) warning( f"Ignoring invalid line {lineno}: [i default]{line}[/] ({e})" ) else: raise CommandFileError( f"Unable to parse line {lineno} '{line}': {e}" ) from e return commands def run_bulk(ctx: typer.Context, file: Path, mode: BulkRunnerMode) -> None: state = get_state() runner = BulkRunner(ctx, file, mode) try: state.bulk = True runner.run_bulk() finally: state.bulk = False state.logout_on_exit() logger.debug("Bulk execution complete.") unioslo-zabbix-cli-09a2fab/zabbix_cli/cache.py000066400000000000000000000063741471265333400214570ustar00rootroot00000000000000"""Simple in-memory caching of frequently used Zabbix objects.""" # TODO: add on/off toggle for caching from __future__ import annotations import logging from typing import TYPE_CHECKING from typing import Dict from typing import Optional from zabbix_cli.exceptions import ZabbixCLIError if TYPE_CHECKING: from zabbix_cli.pyzabbix.client import ZabbixAPI class ZabbixCache: """In-memory cache of frequently used Zabbix objects.""" def __init__(self, client: ZabbixAPI) -> None: self.client = client self._hostgroup_name_cache: Dict[str, str] = {} """Mapping of hostgroup names to hostgroup IDs""" self._hostgroup_id_cache: Dict[str, str] = {} """Mapping of hostgroup IDs to hostgroup names""" self._templategroup_name_cache: Dict[str, str] = {} """Mapping of templategroup names to templategroup IDs""" self._templategroup_id_cache: Dict[str, str] = {} # NOTE: unused """Mapping of templategroup IDs to templategroup names""" def populate(self) -> None: try: self._populate_hostgroup_cache() self._populate_templategroup_cache() except Exception as e: raise ZabbixCLIError(f"Failed to populate Zabbix cache: {e}") def _populate_hostgroup_cache(self) -> None: """Populates the hostgroup caches with data from the Zabbix API.""" hostgroups = self.client.hostgroup.get(output=["name", "groupid"]) self._hostgroup_name_cache = { hostgroup["name"]: hostgroup["groupid"] for hostgroup in hostgroups } self._hostgroup_id_cache = { hostgroup["groupid"]: hostgroup["name"] for hostgroup in hostgroups } def _populate_templategroup_cache(self) -> None: """Populates the templategroup caches with data from the Zabbix API on Zabbix >= 6.2.0. """ if self.client.version.release < (6, 2, 0): logging.debug( "Skipping template group caching. API version is %s", self.client.version, ) return templategroups = self.client.templategroup.get(output=["name", "groupid"]) self._templategroup_name_cache = { templategroup["name"]: templategroup["groupid"] for templategroup in templategroups } self._templategroup_id_cache = { templategroup["groupid"]: templategroup["name"] for templategroup in templategroups } def get_hostgroup_name(self, hostgroup_id: str) -> Optional[str]: """Returns the name of a host group given its ID.""" return self._hostgroup_id_cache.get(hostgroup_id) def get_hostgroup_id(self, hostgroup_name: str) -> Optional[str]: """Returns the ID of a host group given its name.""" return self._hostgroup_name_cache.get(hostgroup_name) def get_templategroup_name(self, templategroup_id: str) -> Optional[str]: """Returns the name of a template group given its ID.""" return self._templategroup_id_cache.get(templategroup_id) def get_templategroup_id(self, templategroup_name: str) -> Optional[str]: """Returns the ID of a template group given its name.""" return self._templategroup_name_cache.get(templategroup_name) unioslo-zabbix-cli-09a2fab/zabbix_cli/commands/000077500000000000000000000000001471265333400216315ustar00rootroot00000000000000unioslo-zabbix-cli-09a2fab/zabbix_cli/commands/__init__.py000066400000000000000000000006071471265333400237450ustar00rootroot00000000000000from __future__ import annotations import importlib from pathlib import Path def bootstrap_commands() -> None: """Bootstrap all command defined in the command modules.""" module_dir = Path(__file__).parent for module in module_dir.glob("*.py"): if module.stem == "__init__": continue importlib.import_module(f".{module.stem}", package=__package__) unioslo-zabbix-cli-09a2fab/zabbix_cli/commands/cli.py000066400000000000000000000264301471265333400227570ustar00rootroot00000000000000"""Commands that interact with the application itself.""" from __future__ import annotations from enum import Enum from pathlib import Path from typing import TYPE_CHECKING from typing import Optional import typer from zabbix_cli.app import app from zabbix_cli.commands.common.args import OPTION_LIMIT from zabbix_cli.config.constants import SecretMode from zabbix_cli.dirs import CONFIG_DIR from zabbix_cli.dirs import DATA_DIR from zabbix_cli.dirs import EXPORT_DIR from zabbix_cli.dirs import LOGS_DIR from zabbix_cli.dirs import SITE_CONFIG_DIR from zabbix_cli.exceptions import ConfigExistsError from zabbix_cli.exceptions import ZabbixCLIError from zabbix_cli.output.console import exit_err from zabbix_cli.output.console import info from zabbix_cli.output.console import print_path from zabbix_cli.output.console import print_toml from zabbix_cli.output.console import success from zabbix_cli.output.formatting.path import path_link from zabbix_cli.output.prompts import str_prompt from zabbix_cli.output.render import render_result from zabbix_cli.utils.fs import open_directory if TYPE_CHECKING: from zabbix_cli.config.model import Config HELP_PANEL = "CLI" @app.command( "show_zabbixcli_config", rich_help_panel=HELP_PANEL, hidden=True, deprecated=True ) @app.command("show_config", rich_help_panel=HELP_PANEL) def show_config( ctx: typer.Context, secrets: SecretMode = typer.Option( SecretMode.MASK, "--secrets", help="Display mode for secrets." ), ) -> None: """Show the current application configuration.""" config = app.state.config print_toml(config.as_toml(secrets=secrets)) if config.config_path: info(f"Config file: {config.config_path.absolute()}") class DirectoryType(Enum): """Directory types.""" CONFIG = "config" DATA = "data" LOGS = "logs" SITE_CONFIG = "siteconfig" EXPORTS = "exports" def as_path(self) -> Path: DIR_MAP = { DirectoryType.CONFIG: CONFIG_DIR, DirectoryType.DATA: DATA_DIR, DirectoryType.LOGS: LOGS_DIR, DirectoryType.SITE_CONFIG: SITE_CONFIG_DIR, DirectoryType.EXPORTS: EXPORT_DIR, } d = DIR_MAP.get(self) if d is None: raise ZabbixCLIError(f"No default path available for {self!r}.") return d def get_directory(directory_type: DirectoryType, config: Optional[Config]) -> Path: if config: if directory_type == DirectoryType.CONFIG and config.config_path: return config.config_path.parent elif directory_type == DirectoryType.EXPORTS: return config.app.export_directory return directory_type.as_path() @app.command("show_dirs", rich_help_panel=HELP_PANEL) def show_directories(ctx: typer.Context) -> None: """Show the default directories used by the application.""" from zabbix_cli.commands.results.cli import DirectoriesResult result = DirectoriesResult.from_directory_types(list(DirectoryType)) render_result(result) @app.command("open", rich_help_panel=HELP_PANEL) def open_config_dir( ctx: typer.Context, directory_type: DirectoryType = typer.Argument( help="The type of directory to open.", case_sensitive=False, show_default=False, ), force: bool = typer.Option( False, "--force", is_flag=True, help="LINUX: Try to open desite no detected window manager.", ), path: bool = typer.Option( False, "--path", is_flag=True, help="Show path instead of opening directory.", ), open_command: Optional[str] = typer.Option( None, "--command", help="Specify command to use to use for opening.", ), ) -> None: """Open an app directory in the system's file manager. Use --force to attempt to open when no DISPLAY env var is set. """ # Try to load the config, but don't fail if it's not available try: config = app.state.config except ZabbixCLIError: config = None directory = get_directory(directory_type, config) if path: print_path(directory) else: open_directory(directory, command=open_command, force=force) success(f"Opened {directory}") @app.command("debug", hidden=True, rich_help_panel=HELP_PANEL) def debug_cmd( ctx: typer.Context, with_auth: bool = typer.Option( False, "--auth", help="Include auth token in the result." ), ) -> None: """Print debug info.""" from zabbix_cli.commands.results.cli import DebugInfo render_result(DebugInfo.from_debug_data(app.state, with_auth=with_auth)) @app.command(name="login", rich_help_panel=HELP_PANEL) def login( ctx: typer.Context, username: str = typer.Option( None, "--username", "-u", help="Username to log in with." ), password: str = typer.Option( None, "--password", "-p", help="Password to log in with." ), token: str = typer.Option(None, "--token", "-t", help="API token to log in with."), ) -> None: """Reauthenticate with the Zabbix API. Creates a new auth token file if enabled in the config. """ from pydantic import SecretStr if not app.state.repl: raise ZabbixCLIError("This command is only available in the REPL.") config = app.state.config # Prompt for password if username is specified if username and not password: password = str_prompt( "Password", password=True, empty_ok=False, ) if username: config.api.username = username config.api.password = SecretStr(password) config.api.auth_token = SecretStr("") # Clear token if it exists elif token: config.api.auth_token = SecretStr(token) config.api.password = SecretStr("") # End current session if it's active app.state.logout() app.state.login() success(f"Logged in to {config.api.url} as {config.api.username}.") @app.command("show_history", rich_help_panel=HELP_PANEL) def show_history( ctx: typer.Context, limit: int = OPTION_LIMIT, # TODO: Add --session option to limit to current session # In order to add that, we need to store the history len at the start of the session ) -> None: """Show the command history.""" # Load the entire history, then limit afterwards from zabbix_cli.commands.results.cli import HistoryResult history = list(app.state.history.get_strings()) history = history[-limit:] render_result(HistoryResult(commands=history)) @app.command("sample_config", rich_help_panel=HELP_PANEL) def sample_config(ctx: typer.Context) -> None: """Print a sample configuration file.""" # Load the entire history, then limit afterwards from zabbix_cli.config.model import Config conf = Config.sample_config() print_toml(conf.as_toml()) @app.command("init", rich_help_panel=HELP_PANEL) def init( ctx: typer.Context, config_file: Optional[Path] = typer.Option( None, "--config-file", "-c", help="Location of the config file." ), overwrite: bool = typer.Option( False, "--overwrite", help="Overwrite existing config" ), url: Optional[str] = typer.Option( None, "--url", "-u", help="Zabbix API URL to use." ), ) -> None: """Create and initialize config file.""" from zabbix_cli.config.utils import init_config try: init_config(config_file=config_file, overwrite=overwrite, url=url) except ConfigExistsError as e: raise ZabbixCLIError(f"{e}. Use [option]--overwrite[/] to overwrite it") from e @app.command("migrate_config", rich_help_panel=HELP_PANEL) def migrate_config( ctx: typer.Context, source: Optional[Path] = typer.Option( None, "--source", "-s", help="Location of the config file to migrate." ), destination: Optional[Path] = typer.Option( None, "--destination", "-d", help="Path of the new config file to create. Uses the default config path if not specified.", ), overwrite: bool = typer.Option( False, "--overwrite", help="Overwrite destination config file if it exists." ), legacy_json: bool = typer.Option( False, "--legacy-json-format", help="Use legacy JSON format mode in the new config file.", ), ) -> None: """Migrate a legacy .conf config to a new .toml config. The new config file will be created in the default location if no destination is specified. The new config enables the new JSON format by default. """ from zabbix_cli.config.constants import DEFAULT_CONFIG_FILE from zabbix_cli.config.model import Config if source: conf = Config.from_file(source) else: if not app.state.is_config_loaded: # this should never happen! exit_err( "Application was unable to load a config. Use [option]--source[/] to specify one." ) conf = app.state.config if not conf.app.is_legacy: exit_err( "Unable to detect legacy config file. Use [option]--source[/] to specify one." ) if not destination: destination = DEFAULT_CONFIG_FILE if not destination.suffix == ".toml": destination = destination.with_suffix(".toml") if destination.exists() and not overwrite: exit_err( f"File {destination} already exists. Use [option]--overwrite[/] to overwrite it." ) # Set the legacy JSON format flag in the new file # By default, we move users over to the new format. conf.app.legacy_json_format = legacy_json conf.dump_to_file(destination) success(f"Config migrated to {destination}") @app.command("update_config", rich_help_panel=HELP_PANEL) def update_config( ctx: typer.Context, config_file: Optional[Path] = typer.Option( None, "--config-file", "-c", help="Location of the config file to update." ), secrets: SecretMode = typer.Option( SecretMode.PLAIN, "--secrets", help="Secret dump mode" ), force: bool = typer.Option(False, "--force", help="Skip confirmation prompt."), ) -> None: from zabbix_cli.output.prompts import bool_prompt """Update the TOML config file with the currently active settings. Useful if you authenticate with a new user or change the URL, and want to save the changes to the config file. Furthermore, this helps to migrate an outdated config file to the newest version.""" config_file = config_file or app.state.config.config_path if not config_file: exit_err("No config file specified and no config loaded.") if not force: if not bool_prompt("Update config file?", default=False): exit_err("Update cancelled.") config = app.state.config config.dump_to_file(config_file, secrets=secrets) success(f"Config saved to {path_link(config_file)}") @app.command("update", rich_help_panel=HELP_PANEL, hidden=True) def update_application(ctx: typer.Context) -> None: """Update the application to the latest version. Primarily intended for use with PyInstaller builds, but can also be used for updating other installations (except Homebrew).""" from zabbix_cli.__about__ import __version__ from zabbix_cli.update import update info = update() if info and info.version: success(f"Application updated from {__version__} to {info.version}") else: success("Application updated.") unioslo-zabbix-cli-09a2fab/zabbix_cli/commands/common/000077500000000000000000000000001471265333400231215ustar00rootroot00000000000000unioslo-zabbix-cli-09a2fab/zabbix_cli/commands/common/__init__.py000066400000000000000000000000001471265333400252200ustar00rootroot00000000000000unioslo-zabbix-cli-09a2fab/zabbix_cli/commands/common/args.py000066400000000000000000000010041471265333400244220ustar00rootroot00000000000000from __future__ import annotations from typing import Any from typing import Optional import typer def get_limit_option( limit: Optional[int] = 0, resource: str = "results", long_option: str = "--limit", short_option: str = "-n", ) -> Any: # TODO: Can we type this better? """Limit option factory.""" return typer.Option( limit, long_option, short_option, help=f"Limit the number of {resource}. 0 to show all.", ) OPTION_LIMIT = get_limit_option(0) unioslo-zabbix-cli-09a2fab/zabbix_cli/commands/export.py000066400000000000000000000606441471265333400235360ustar00rootroot00000000000000from __future__ import annotations import time from datetime import datetime from pathlib import Path from typing import TYPE_CHECKING from typing import Any from typing import Dict from typing import Iterator from typing import List from typing import NamedTuple from typing import Optional from typing import Protocol import typer from strenum import StrEnum from zabbix_cli._v2_compat import ARGS_POSITIONAL from zabbix_cli.app import Example from zabbix_cli.app import app from zabbix_cli.config.constants import OutputFormat from zabbix_cli.exceptions import ZabbixCLIError from zabbix_cli.logs import logger from zabbix_cli.output.console import console from zabbix_cli.output.console import err_console from zabbix_cli.output.console import error from zabbix_cli.output.console import exit_err from zabbix_cli.output.console import info from zabbix_cli.output.console import success from zabbix_cli.output.console import warning from zabbix_cli.output.formatting.path import path_link from zabbix_cli.output.render import render_result from zabbix_cli.pyzabbix.enums import ExportFormat from zabbix_cli.utils.args import parse_bool_arg from zabbix_cli.utils.args import parse_list_arg from zabbix_cli.utils.args import parse_path_arg from zabbix_cli.utils.fs import open_directory from zabbix_cli.utils.fs import sanitize_filename from zabbix_cli.utils.utils import convert_seconds_to_duration if TYPE_CHECKING: from typing_extensions import TypedDict from typing_extensions import Unpack from zabbix_cli.config.model import Config from zabbix_cli.pyzabbix.client import ZabbixAPI from zabbix_cli.pyzabbix.types import Host from zabbix_cli.pyzabbix.types import HostGroup from zabbix_cli.pyzabbix.types import Image from zabbix_cli.pyzabbix.types import Map from zabbix_cli.pyzabbix.types import MediaType from zabbix_cli.pyzabbix.types import Template from zabbix_cli.pyzabbix.types import TemplateGroup class ExportKwargs(TypedDict, total=False): hosts: List[Host] host_groups: List[HostGroup] images: List[Image] maps: List[Map] media_types: List[MediaType] templates: List[Template] template_groups: List[TemplateGroup] HELP_PANEL = "Import/Export" class ExportType(StrEnum): HOST_GROUPS = "host_groups" # >=6.2 TEMPLATE_GROUPS = "template_groups" # >=6.2 HOSTS = "hosts" IMAGES = "images" MAPS = "maps" TEMPLATES = "templates" MEDIA_TYPES = "mediaTypes" # >= 6.0 (but should work on 5.0 too) @classmethod def _missing_(cls, value: Any) -> ExportType: if str(value.lower()) == "groups": return cls.HOST_GROUPS raise ValueError(f"Invalid export type: {value}") def human_readable(self) -> str: if self.value == "mediaTypes": return "media types" return self.value.replace("_", " ").lower() class ExporterFunc(Protocol): def __call__(self) -> Iterator[Optional[Path]]: ... class Exporter(NamedTuple): func: ExporterFunc type: ExportType class ZabbixExporter: """Export Zabbix configuration for one or more components.""" def __init__( self, client: ZabbixAPI, config: Config, types: List[ExportType], names: List[str], directory: Path, format: ExportFormat, legacy_filenames: bool, pretty: bool, ignore_errors: bool, ) -> None: self.client = client self.config = config self.export_types = types self.names = names self.directory = directory self.format = format self.legacy_filenames = legacy_filenames self.pretty = pretty self.ignore_errors = ignore_errors # Ideally, we fetch and write at the same time, so we keep memory usage low, # while utilizing I/O and CPU as much as possible. # Will need to be rewritten to use threads to achieve this. # TODO: test that mapping contains all export types self.exporter_map: Dict[ExportType, ExporterFunc] = { ExportType.HOST_GROUPS: self.export_host_groups, ExportType.TEMPLATE_GROUPS: self.export_template_groups, ExportType.HOSTS: self.export_hosts, ExportType.IMAGES: self.export_images, ExportType.MAPS: self.export_maps, ExportType.TEMPLATES: self.export_templates, ExportType.MEDIA_TYPES: self.export_media_types, } self.check_export_types() def run(self) -> List[Path]: """Run exporters.""" files: List[Path] = [] with err_console.status("") as status: for exporter in self.get_exporters(): status.update(f"Exporting {exporter.type.human_readable()}...") for file in exporter.func(): if file: files.append(file) success(f"Exported {exporter.type.human_readable()}") return files def check_export_types(self) -> None: """Check export types for compatibility.""" # If we have no specific exports, export all object types for export_type in self.export_types: self._check_export_type_compat(export_type) def _check_export_type_compat(self, export_type: ExportType) -> None: if export_type == ExportType.TEMPLATE_GROUPS: if self.client.version.release < (6, 2, 0): raise ZabbixCLIError( "Template group exports are not supported in Zabbix versions < 6.2." ) def get_exporters(self) -> List[Exporter]: """Get a list of exporters to run.""" exporters: List[Exporter] = [] for export_type in self.export_types: exporter = self.exporter_map.get(export_type, None) if not exporter: # should never happen - tests should catch this raise ZabbixCLIError( f"No exporter available for export type: {export_type}" ) exporters.append(Exporter(exporter, export_type)) return exporters def get_filename(self, name: str, id: str, export_type: ExportType) -> Path: """Get path to export file given a .""" stem = self.get_filename_stem(name, id) directory = self.directory / export_type.value return directory / f"{stem}.{self.format.value}" def get_filename_stem(self, name: str, id: str) -> str: if self.legacy_filenames: fn = self._get_legacy_filename_stem(name, id) else: fn = self._get_filename_stem(name, id) if self.config.app.export_timestamps: ts = datetime.now().strftime("%Y-%m-%dT%H%M%S%Z") fn = f"{fn}_{ts}" return sanitize_filename(fn) def _get_legacy_filename_stem(self, name: str, id: str) -> str: """Format legacy filename.""" return f"zabbix_export_{name}_{id}" def _get_filename_stem(self, name: str, id: str) -> str: """Format filename.""" return f"{name}_{id}" # TODO: refactor export methods if we want to add --ignore-errors # We have to find a way to generalize the export process while keeping # type safety intact. # The challenge is that we need to continue if a single export fails, # so we can't just wrap the call of the export method in a try/except # Each method needs to guard the export call of each object in a try/except # and that will be extremely verbose and repetitive. def export_host_groups(self) -> Iterator[Optional[Path]]: hostgroups = self.client.get_hostgroups(*self.names, search=True) for hg in hostgroups: filename = self.get_filename(hg.name, hg.groupid, ExportType.HOST_GROUPS) yield self.do_run_export(filename, host_groups=[hg]) def export_template_groups(self) -> Iterator[Optional[Path]]: template_groups = self.client.get_templategroups(*self.names, search=True) for tg in template_groups: filename = self.get_filename( tg.name, tg.groupid, ExportType.TEMPLATE_GROUPS ) yield self.do_run_export(filename, template_groups=[tg]) def export_hosts(self) -> Iterator[Optional[Path]]: hosts = self.client.get_hosts(*self.names) for host in hosts: filename = self.get_filename(host.host, host.hostid, ExportType.HOSTS) yield self.do_run_export(filename, hosts=[host]) def export_images(self) -> Iterator[Optional[Path]]: images = self.client.get_images(*self.names, select_image=False) for image in images: filename = self.get_filename(image.name, image.imageid, ExportType.IMAGES) yield self.do_run_export(filename, images=[image]) def export_maps(self) -> Iterator[Optional[Path]]: maps = self.client.get_maps(*self.names) for m in maps: filename = self.get_filename(m.name, m.sysmapid, ExportType.MAPS) yield self.do_run_export(filename, maps=[m]) def export_media_types(self) -> Iterator[Optional[Path]]: media_types = self.client.get_media_types(*self.names) for mt in media_types: filename = self.get_filename( mt.name, mt.mediatypeid, ExportType.MEDIA_TYPES ) yield self.do_run_export(filename, media_types=[mt]) def export_templates(self) -> Iterator[Optional[Path]]: templates = self.client.get_templates(*self.names) for template in templates: filename = self.get_filename( template.host, template.templateid, ExportType.TEMPLATES ) yield self.do_run_export(filename, templates=[template]) def do_run_export( self, filename: Path, **kwargs: Unpack[ExportKwargs] ) -> Optional[Path]: """Runs the export process.""" try: exported = self.client.export_configuration( pretty=self.pretty, format=self.format, **kwargs, ) return self.write_exported(exported, filename) except Exception as e: # HACKY: since we do some ugly metaprogramming to generalize the export process, # we don't have the actual object on hand to print a useful representation of it. # If every object had a __pretty__ or similar, we could use `next(iter(kwargs.values()))` # then get the firsty entry of that list and call __pretty__ on it. But as it stands, # it's prettier to just print the expected filename than to leak the entire object repr msg = f"Failed to export {filename}: {e}" if self.ignore_errors: error(msg, exc_info=True) return None else: raise ZabbixCLIError(msg) from e def write_exported(self, exported: str, filename: Path) -> Path: """Writes an exported object to a file. Returns path to file.""" # TODO: add some logging here to show progress # run some callback that updates a progress bar or something if not filename.parent.exists(): try: filename.parent.mkdir(parents=True) except Exception as e: raise ZabbixCLIError( f"Failed to create directory {filename.parent}: {e}. Ensure you have permissions to create directories in the export directory." ) from e with open(filename, "w", encoding="utf-8") as f: f.write(exported) return filename def parse_export_types(value: List[str]) -> List[ExportType]: # If we have no specific exports, export all object types if not value: value = list(ExportType) elif "#all#" in value: warning("#all# is a deprecated value and will be removed in a future version.") value = list(ExportType) objs: List[ExportType] = [] for obj in value: try: export_type = ExportType(obj) # self._check_export_type_compat(export_type) objs.append(export_type) except ZabbixCLIError as e: raise e # should this be exit_err instead? except Exception as e: raise ZabbixCLIError(f"Invalid export type: {obj}") from e # dedupe # TODO: test that StrEnum is hashable on all Python versions return sorted(set(objs)) def parse_export_types_callback( ctx: typer.Context, param: typer.CallbackParam, value: List[str] ) -> List[ExportType]: """Parses list of object export type names. In V2, this was called "objects", which isn't very descriptive... """ if ctx.resilient_parsing: return [] # pragma: no cover return parse_export_types(value) @app.command( name="export_configuration", rich_help_panel=HELP_PANEL, examples=[ Example( "Export everything", "export_configuration", ), Example( "Export all host groups", "export_configuration --type host_groups", ), Example( "Export all host groups containing 'Linux'", "export_configuration --type host_groups --name '*Linux*'", ), Example( "Export all template groups and templates containing 'Linux' or 'Windows'", "export_configuration --type template_groups --type templates --name '*Linux*,*Windows*'", ), ], ) def export_configuration( ctx: typer.Context, directory: Optional[Path] = typer.Option( None, "--directory", help="Directory to export configuration to. Overrides directory in config.", writable=True, file_okay=False, ), # NOTE: We can't accept comma-separated values AND multiple values when using enums! # Typer performs its parsing before callbacks are run, sadly. types: List[ExportType] = typer.Option( [], "--type", help="Type(s) of objects to export. Can be specified multiple times. Defaults to all object types.", callback=parse_export_types_callback, ), names: Optional[str] = typer.Option( None, "--name", help="Name(s) of objects to export. Comma-separated list." ), format: Optional[ExportFormat] = typer.Option( None, "--format", help="Format to export to. Overrides export format in config.", ), # TODO: move/add this option to config legacy_filenames: bool = typer.Option( False, "--legacy-filenames", help="DEPRECATED: Use legacy filename scheme for exported objects.", ), pretty: bool = typer.Option( False, "--pretty", is_flag=True, help="Pretty-print output. Not supported for XML.", ), open_dir: bool = typer.Option( False, "--open", is_flag=True, help="Open export directory in file explorer after exporting.", ), ignore_errors: bool = typer.Option( False, "--ignore-errors", is_flag=True, help="Enable best-effort exporting. Print errors but continue exporting.", ), # TODO: add --ignore-errors option # Legacy positional args args: Optional[List[str]] = ARGS_POSITIONAL, ) -> None: r"""Export Zabbix configuration for one or more components. Uses defaults from Zabbix-CLI configuration file if not specified. [b]NOTE:[/] [option]--name[/] arguments are globs, not regex patterns. [b]Filename scheme is as follows:[/] [code]DIRECTORY/OBJECT_TYPE/NAME_ID_\[timestamp].FORMAT[/] [b]But it can be changed to the legacy scheme with [option]--legacy-filenames[/option]:[/b] [code]DIRECTORY/OBJECT_TYPE/zabbix_export_OBJECT_TYPE_NAME_ID_\[timestamp].FORMAT[/] Timestamps are disabled by default, but can be enabled with the [configopt]app.export_timestamps[/] configuration option. Shows detailed information about exported files in JSON output mode. """ from zabbix_cli.commands.results.export import ExportResult from zabbix_cli.models import Result if args: if not len(args) == 3: exit_err("Invalid number of arguments. Use options instead.") directory = parse_path_arg(args[0]) types = parse_export_types(parse_list_arg(args[1])) names = args[2] # No format arg in V2... if legacy_filenames: warning( "--legacy-filenames is deprecated and will be removed in a future version." ) exportdir = directory or app.state.config.app.export_directory # V2 compat: passing in #all# exports all names if names == "#all#": obj_names = [] else: obj_names = parse_list_arg(names) if not format: format = app.state.config.app.export_format # TODO: guard this in try/except and render useful error if it fails exporter = ZabbixExporter( client=app.state.client, config=app.state.config, types=types, names=obj_names, directory=exportdir, format=format, legacy_filenames=legacy_filenames, pretty=pretty, ignore_errors=ignore_errors, ) exported = exporter.run() # NOTE: record duration similar to import_configuration? render_result( Result( message=f"Exported {len(exported)} files to {exportdir}", result=ExportResult( exported=exported, types=types, names=obj_names, format=format ), table=False, ) ) if open_dir: open_directory(exportdir) class ZabbixImporter: def __init__( self, client: ZabbixAPI, config: Config, files: List[Path], create_missing: bool, update_existing: bool, delete_missing: bool, ignore_errors: bool, ) -> None: self.client = client self.config = config self.files = files self.ignore_errors = ignore_errors self.create_missing = create_missing self.update_existing = update_existing self.delete_missing = delete_missing self.imported: List[Path] = [] self.failed: List[Path] = [] def run(self) -> None: """Runs the importer.""" from rich.progress import BarColumn from rich.progress import Progress from rich.progress import SpinnerColumn from rich.progress import TaskProgressColumn from rich.progress import TextColumn from rich.progress import TimeElapsedColumn progress = Progress( SpinnerColumn(), TextColumn("[progress.description]{task.description}"), BarColumn(), TaskProgressColumn(), TimeElapsedColumn(), transient=True, console=err_console, ) with progress: task = progress.add_task("Importing files...", total=len(self.files)) for file in self.files: self.import_file(file) progress.update(task, advance=1) def import_file(self, file: Path) -> None: # API method will return true if successful, but does failure return false # or does it raise an exception? try: self.client.import_configuration(file) except Exception as e: self.failed.append(file) msg = f"Failed to import {file}: {e}" if self.ignore_errors: error(msg, exc_info=True) else: raise ZabbixCLIError(msg) from e else: self.imported.append(file) logger.info(f"Imported file {file}") def filter_valid_imports(files: List[Path]) -> List[Path]: """Filter list of files to include only valid imports.""" importables = [i.casefold() for i in ExportFormat.get_importables()] valid: List[Path] = [] for f in files: if not f.exists(): continue if f.is_dir(): continue if f.suffix.strip(".").casefold() not in importables: continue valid.append(f) return valid @app.command(name="import_configuration", rich_help_panel=HELP_PANEL) def import_configuration( ctx: typer.Context, to_import: Optional[str] = typer.Argument( None, help="Path to file or directory to import configuration from. Accepts glob pattern. Uses default export directory if not specified.", ), dry_run: bool = typer.Option( False, "--dryrun", is_flag=True, help="Preview files to import." ), create_missing: bool = typer.Option( True, "--create-missing/--no-create-missing", help="Create missing objects." ), update_existing: bool = typer.Option( True, "--update-existing/--no-update-existing", help="Update existing objects." ), delete_missing: bool = typer.Option( False, "--delete-missing/--no-delete-missing", help="Delete missing objects." ), ignore_errors: bool = typer.Option( False, "--ignore-errors", is_flag=True, help="Enable best-effort importing. Print errors from failed imports but continue importing.", ), # Legacy positional args args: Optional[List[str]] = ARGS_POSITIONAL, ) -> None: """Import Zabbix configuration from file, directory or glob pattern. Imports all files in all subdirectories if a directory is specified. Uses default export directory if no argument is specified. Determines format to import based on file extensions. """ import glob from zabbix_cli.commands.results.export import ImportResult from zabbix_cli.models import Result from zabbix_cli.models import ReturnCode if args: if not len(args) == 2: exit_err("Invalid number of positional arguments. Use options instead.") to_import = args[0] dry_run = parse_bool_arg(args[1]) # Use default export directory if no path is specified if not to_import: to_import = str(app.state.config.app.export_directory) # Determine if we are dealing with a directory, file or glob import_path = Path(to_import) if import_path.exists(): if import_path.is_dir(): files = list(import_path.glob("**/*")) else: files = [import_path] else: # Arg doesn't exist, must be glob pattern # If user passes in empty string, that's on them! files = [Path(p) for p in glob.glob(to_import)] files = filter_valid_imports(files) # HACK: in order to print a list of files without messing with line wrapping # and other formatting headaches, we just print using the console here # TODO: print properly with just render_result instead of this hack if dry_run: msg = f"Found {len(files)} files to import" if app.state.config.app.output.format == OutputFormat.TABLE: to_print = [path_link(f) for f in files] console.print("\n".join(to_print), highlight=False, no_wrap=True) info(msg) else: render_result( Result( message=msg, result=ImportResult(success=True, dryrun=True, imported=files), ) ) return if not files: exit_err(f"No files found to import matching: {to_import}") info(f"Found {len(files)} files to import") importer = ZabbixImporter( client=app.state.client, config=app.state.config, files=files, create_missing=create_missing, update_existing=update_existing, ignore_errors=ignore_errors, delete_missing=delete_missing, ) try: start_time = time.monotonic() importer.run() except Exception as e: res = Result( message=f"{e}. See log for further details. Use [cyan]--ignore-errors[/] to ignore failed files.", return_code=ReturnCode.ERROR, result=ImportResult( success=False, dryrun=False, imported=importer.imported, failed=importer.failed, ), table=False, # only render this in JSON mode ) else: duration = time.monotonic() - start_time msg = f"Imported {len(importer.imported)} files in {convert_seconds_to_duration(int(duration))}" if importer.failed: msg += f", failed to import {len(importer.failed)} files" res = Result( message=msg, result=ImportResult( success=len(importer.failed) == 0, imported=importer.imported, failed=importer.failed, duration=duration, ), table=False, # only render this in JSON mode ) render_result(res) unioslo-zabbix-cli-09a2fab/zabbix_cli/commands/host.py000066400000000000000000000700171471265333400231650ustar00rootroot00000000000000from __future__ import annotations import ipaddress from typing import List from typing import Optional import typer from zabbix_cli._v2_compat import ARGS_POSITIONAL from zabbix_cli.app import Example from zabbix_cli.app import app from zabbix_cli.commands.common.args import OPTION_LIMIT from zabbix_cli.exceptions import ZabbixCLIError from zabbix_cli.exceptions import ZabbixNotFoundError from zabbix_cli.output.console import exit_err from zabbix_cli.output.console import info from zabbix_cli.output.render import render_result from zabbix_cli.pyzabbix.enums import ActiveInterface from zabbix_cli.pyzabbix.enums import InterfaceConnectionMode from zabbix_cli.pyzabbix.enums import InterfaceType from zabbix_cli.pyzabbix.enums import InventoryMode from zabbix_cli.pyzabbix.enums import MonitoringStatus from zabbix_cli.pyzabbix.enums import SNMPAuthProtocol from zabbix_cli.pyzabbix.enums import SNMPPrivProtocol from zabbix_cli.pyzabbix.enums import SNMPSecurityLevel from zabbix_cli.utils.args import check_at_least_one_option_set from zabbix_cli.utils.args import parse_list_arg HELP_PANEL = "Host" @app.command(name="create_host", rich_help_panel=HELP_PANEL) def create_host( ctx: typer.Context, hostname_or_ip: str = typer.Argument( help="Hostname or IP", show_default=False, ), hostgroups: Optional[str] = typer.Option( None, "--hostgroup", help="Hostgroup name(s) or ID(s). Comma-separated.", ), proxy: Optional[str] = typer.Option( ".+", "--proxy", help="Proxy server used to monitor the host. Supports regular expressions.", ), status: MonitoringStatus = typer.Option( MonitoringStatus.ON.value, "--status", help="Host monitoring status.", ), default_hostgroup: bool = typer.Option( True, "--default-hostgroup/--no-default-hostgroup", help="Add host to default host group(s) defined in config.", ), name: Optional[str] = typer.Option( None, "--name", help="Visible name of the host. Uses hostname or IP if omitted.", ), description: Optional[str] = typer.Option( None, "--description", help="Description of the host.", ), # LEGACY: V2-style positional args args: Optional[List[str]] = ARGS_POSITIONAL, ) -> None: """Create a host. Always adds the host to the default host group unless --no-default-hostgroup is specified. Selects a random proxy by default unless [option]--proxy[/] [value]-[/] is specified. """ # NOTE: this was one of the first commands ported over to V3, and as such # it uses a lot of V2 semantics and patterns. It should be changed to have # less implicit behavior such as default hostgroups. from zabbix_cli.models import Result from zabbix_cli.output.formatting.grammar import pluralize_no_count as pnc from zabbix_cli.pyzabbix.types import HostInterface from zabbix_cli.pyzabbix.utils import get_random_proxy if args: if len(args) != 3: # Hostname + legacy args = 4 exit_err("create_host takes exactly 4 positional arguments.") hostgroups = args[0] proxy = args[1] status = MonitoringStatus(args[2]) host_name = name or hostname_or_ip if app.state.client.host_exists(host_name): exit_err(f"Host {host_name!r} already exists") # Check if we are using a hostname or IP try: ipaddress.ip_address(hostname_or_ip) except ValueError: useip = False interface_ip = "" interface_dns = hostname_or_ip else: useip = True interface_ip = hostname_or_ip interface_dns = "" interfaces = [ HostInterface( type=InterfaceType.AGENT.as_api_value(), main=True, useip=useip, ip=interface_ip, dns=interface_dns, port=InterfaceType.AGENT.get_port(), ) ] # Determine host group IDs hg_args: List[str] = [] # Default host groups from config def_hgs = app.state.config.app.default_hostgroups if default_hostgroup and def_hgs: grp = pnc("group", len(def_hgs)) # pluralize info(f"Will add host to default host {grp}: {', '.join(def_hgs)}") hg_args.extend(def_hgs) # Host group args if hostgroups: hostgroup_args = parse_list_arg(hostgroups) hg_args.extend(hostgroup_args) hgs = [app.state.client.get_hostgroup(hg) for hg in set(hg_args)] if not hgs: raise ZabbixCLIError("Unable to create a host without at least one host group.") # Find a proxy (No match = monitored by zabbix server) try: prox = get_random_proxy(app.state.client, pattern=proxy) except ZabbixNotFoundError: prox = None if app.state.client.host_exists(hostname_or_ip): exit_err(f"Host {hostname_or_ip!r} already exists.") host_id = app.state.client.create_host( host_name, groups=hgs, proxy=prox, status=status, interfaces=interfaces, inventory_mode=InventoryMode.AUTOMATIC, inventory={"name": hostname_or_ip}, description=description, ) render_result(Result(message=f"Created host {host_name!r} ({host_id})")) @app.command(name="update_host", rich_help_panel=HELP_PANEL) def update_host( ctx: typer.Context, hostname_or_ip: str = typer.Argument( help="Hostname or IP", show_default=False, ), name: Optional[str] = typer.Option( None, "--name", help="Visible name of the host.", ), description: Optional[str] = typer.Option( None, "--description", help="Description of the host.", ), ) -> None: """Update basic information about a host. Other notable commands to update a host: - [command]add_host_to_hostgroup[/] - [command]create_host_interface[/] - [command]monitor_host[/] - [command]remove_host_from_hostgroup[/] - [command]update_host_interface[/] - [command]update_host_inventory[/] """ from zabbix_cli.models import Result check_at_least_one_option_set(ctx) host = app.state.client.get_host(hostname_or_ip) app.state.client.update_host( host, name=name, description=description, ) render_result(Result(message=f"Updated host {host}.")) @app.command( name="create_host_interface", rich_help_panel=HELP_PANEL, examples=[ Example( "Create an SNMPv2 interface on host 'foo.example.com' with derived DNS name 'foo.example.com' (default)", "create_host_interface foo.example.com", ), Example( "Create an SNMPv2 interface on host 'foo.example.com' with IP connection", "create_host_interface foo.example.com --type snmp --ip 127.0.0.1", ), Example( "Create an SNMPv2 interface on host 'foo.example.com' with different DNS name", "create_host_interface foo.example.com --type snmp --dns snmp.example.com", ), Example( "Create an SNMPv2 interface on host 'foo' with both IP and DNS, using DNS as enabled address", "create_host_interface foo --type snmp --connection dns --dns snmp.example.com --ip 127.0.0.1", ), Example( "Create an SNMPv3 interface on host 'foo.example.com'", "create_host_interface foo.example.com --type snmp --snmp-version 3 --snmp-context-name mycontext --snmp-security-name myuser --snmp-security-level authpriv --snmp-auth-protocol MD5 --snmp-auth-passphrase mypass --snmp-priv-protocol DES --snmp-priv-passphrase myprivpass", ), Example( "Create an Agent interface on host 'foo.example.com'", "create_host_interface foo.example.com --type agent", ), ], ) def create_host_interface( ctx: typer.Context, hostname: str = typer.Argument( help="Name of host to create interface on.", show_default=False, ), connection: Optional[InterfaceConnectionMode] = typer.Option( None, "--connection", help="Interface connection mode. Required if both --ip and --dns are specified.", case_sensitive=False, ), type_: InterfaceType = typer.Option( InterfaceType.SNMP, "--type", help="Interface type. SNMP enables --snmp-* options.", case_sensitive=False, ), port: Optional[str] = typer.Option( None, "--port", help="Interface port. Defaults to 10050 for agent, 161 for SNMP, 623 for IPMI, and 12345 for JMX.", ), ip: Optional[str] = typer.Option( None, "--ip", help="IP address of interface.", show_default=False, ), dns: Optional[str] = typer.Option( None, "--dns", help="DNS address of interface.", show_default=False, ), default: bool = typer.Option( True, "--default/--no-default", help="Whether this is the default interface." ), snmp_version: int = typer.Option( 2, "--snmp-version", help="SNMP version.", min=1, max=3, show_default=False, ), snmp_bulk: bool = typer.Option( True, "--snmp-bulk/--no-snmp-bulk", help="Use bulk SNMP requests.", ), snmp_community: str = typer.Option( "${SNMP_COMMUNITY}", "--snmp-community", help="SNMPv{1,2} community.", show_default=False, ), snmp_max_repetitions: int = typer.Option( 10, "--snmp-max-repetitions", help="Max repetitions for SNMPv{2,3} bulk requests.", min=1, ), snmp_security_name: Optional[str] = typer.Option( None, "--snmp-security-name", help="SNMPv3 security name.", show_default=False, ), snmp_context_name: Optional[str] = typer.Option( None, "--snmp-context-name", help="SNMPv3 context name.", show_default=False, ), snmp_security_level: Optional[SNMPSecurityLevel] = typer.Option( None, "--snmp-security-level", help="SNMPv3 security level.", show_default=False, case_sensitive=False, ), snmp_auth_protocol: Optional[SNMPAuthProtocol] = typer.Option( None, "--snmp-auth-protocol", help="SNMPv3 auth protocol (authNoPriv & authPriv).", show_default=False, case_sensitive=False, ), snmp_auth_passphrase: Optional[str] = typer.Option( None, "--snmp-auth-passphrase", help="SNMPv3 auth passphrase (authNoPriv & authPriv).", show_default=False, ), snmp_priv_protocol: Optional[SNMPPrivProtocol] = typer.Option( None, "--snmp-priv-protocol", help="SNMPv3 priv protocol (authPriv)", show_default=False, case_sensitive=False, ), snmp_priv_passphrase: Optional[str] = typer.Option( None, "--snmp-priv-passphrase", help="SNMPv3 priv passphrase (authPriv).", show_default=False, ), # V2-style positional args (deprecated) args: Optional[List[str]] = ARGS_POSITIONAL, ) -> None: """Create a host interface. Creates an SNMPv2 interface by default. Use --type to specify a different type. One of --dns and --ip is required. If both are specified, --connection is required. [b]NOTE:[/] Can only create secondary host interfaces for interfaces of types that already have a default interface. (API limitation) """ from zabbix_cli.models import Result from zabbix_cli.pyzabbix.types import CreateHostInterfaceDetails # Handle V2 positional args (deprecated) if args: if len(args) != 6: # 7 - 1 (hostname) exit_err( "create_host_interface takes exactly 6 positional arguments (deprecated)." ) connection = InterfaceConnectionMode(args[0]) type_ = InterfaceType(args[1]) port = args[2] ip = args[3] dns = args[4] default = args[5] == "1" # Determine connection if not connection: if ip and dns: exit_err("Cannot specify both IP and DNS address without --connection.") elif ip: connection = InterfaceConnectionMode.IP else: connection = InterfaceConnectionMode.DNS if not dns: dns = hostname use_ip = connection == InterfaceConnectionMode.IP # Use default port for type if not specified if port is None: port = type_.get_port() host = app.state.client.get_host(hostname, select_interfaces=True) for interface in host.interfaces: if not interface.type == type_.as_api_value(): continue if interface.main and interface.interfaceid: info( f"Host already has a default {type_} interface. New interface will be created as non-default." ) default = False break else: # No default interface of this type found info(f"No default {type_} interface found. Setting new interface as default.") default = True details: Optional[CreateHostInterfaceDetails] = None if type_ == InterfaceType.SNMP: details = CreateHostInterfaceDetails( version=snmp_version, community=snmp_community, bulk=int(snmp_bulk), ) if snmp_version > 1: details.max_repetitions = snmp_max_repetitions # V3-specific options if snmp_version == 3: if snmp_security_name: details.securityname = snmp_security_name if snmp_context_name: details.contextname = snmp_context_name if snmp_security_level: details.securitylevel = snmp_security_level.as_api_value() # authNoPriv and authPriv: if snmp_security_level != SNMPSecurityLevel.NO_AUTH_NO_PRIV: if snmp_auth_protocol: details.authprotocol = snmp_auth_protocol.as_api_value() if snmp_auth_passphrase: details.authpassphrase = snmp_auth_passphrase # authPriv: if snmp_security_level == SNMPSecurityLevel.AUTH_PRIV: if snmp_priv_protocol: details.privprotocol = snmp_priv_protocol.as_api_value() if snmp_priv_passphrase: details.privpassphrase = snmp_priv_passphrase ifaceid = app.state.client.create_host_interface( host=host, main=default, type=type_, use_ip=use_ip, ip=ip, dns=dns, port=str(port), details=details, ) render_result(Result(message=f"Created host interface with ID {ifaceid}.")) @app.command( name="update_host_interface", rich_help_panel=HELP_PANEL, examples=[ Example( "Update the IP address of interface 123.", "update_host_interface 123 --ip 127.0.0.1", ), Example( "Change connection type of interface 123 to IP.", "update_host_interface 123 --connection ip", ), Example( "Change SNMP community of interface 234 to 'public'.", "update_host_interface 234 --snmp-community public", ), ], ) def update_host_interface( ctx: typer.Context, interface_id: str = typer.Argument( help="ID of interface to update.", show_default=False, ), connection: Optional[InterfaceConnectionMode] = typer.Option( None, "--connection", help="Interface connection mode.", case_sensitive=False, ), port: Optional[str] = typer.Option( None, "--port", help="Interface port.", ), ip: Optional[str] = typer.Option( None, "--ip", help="IP address of interface.", show_default=False, ), dns: Optional[str] = typer.Option( None, "--dns", help="DNS address of interface.", show_default=False, ), default: bool = typer.Option( True, "--default/--no-default", help="Default interface." ), snmp_version: Optional[int] = typer.Option( None, "--snmp-version", help="SNMP version.", min=1, max=3, show_default=False, ), snmp_bulk: Optional[bool] = typer.Option( None, "--snmp-bulk/--no-snmp-bulk", help="Use bulk SNMP requests.", show_default=False, ), snmp_community: Optional[str] = typer.Option( None, "--snmp-community", help="SNMPv{1,2} community.", show_default=False, ), snmp_max_repetitions: Optional[int] = typer.Option( None, "--snmp-max-repetitions", help="Max repetitions for SNMPv{2,3} bulk requests.", min=1, ), snmp_security_name: Optional[str] = typer.Option( None, "--snmp-security-name", help="SNMPv3 security name.", show_default=False, ), snmp_context_name: Optional[str] = typer.Option( None, "--snmp-context-name", help="SNMPv3 context name.", show_default=False, ), snmp_security_level: Optional[SNMPSecurityLevel] = typer.Option( None, "--snmp-security-level", help="SNMPv3 security level.", show_default=False, case_sensitive=False, ), snmp_auth_protocol: Optional[SNMPAuthProtocol] = typer.Option( None, "--snmp-auth-protocol", help="SNMPv3 auth protocol (authNoPriv & authPriv).", show_default=False, case_sensitive=False, ), snmp_auth_passphrase: Optional[str] = typer.Option( None, "--snmp-auth-passphrase", help="SNMPv3 auth passphrase (authNoPriv & authPriv).", show_default=False, ), snmp_priv_protocol: Optional[SNMPPrivProtocol] = typer.Option( None, "--snmp-priv-protocol", help="SNMPv3 priv protocol (authPriv)", show_default=False, case_sensitive=False, ), snmp_priv_passphrase: Optional[str] = typer.Option( None, "--snmp-priv-passphrase", help="SNMPv3 priv passphrase (authPriv).", show_default=False, ), ) -> None: """Update a host interface. Host interface type cannot be changed. """ from zabbix_cli.models import Result from zabbix_cli.pyzabbix.types import UpdateHostInterfaceDetails check_at_least_one_option_set(ctx) interface = app.state.client.get_hostinterface(interface_id) details = UpdateHostInterfaceDetails( version=snmp_version, community=snmp_community, bulk=int(snmp_bulk) if snmp_bulk is not None else None, max_repetitions=snmp_max_repetitions, securityname=snmp_security_name, contextname=snmp_context_name, securitylevel=snmp_security_level.as_api_value() if snmp_security_level else None, authprotocol=snmp_auth_protocol.as_api_value() if snmp_auth_protocol else None, authpassphrase=snmp_auth_passphrase, privprotocol=snmp_priv_protocol.as_api_value() if snmp_priv_protocol else None, privpassphrase=snmp_priv_passphrase, ) if connection: if connection == InterfaceConnectionMode.IP: use_ip = True elif connection == InterfaceConnectionMode.DNS: use_ip = False else: use_ip = None app.state.client.update_host_interface( interface, main=default, use_ip=use_ip, ip=ip, dns=dns, port=port, details=details, ) render_result(Result(message=f"Updated host interface ({interface_id}).")) @app.command(name="remove_host_interface", rich_help_panel=HELP_PANEL) def remove_host_interface( ctx: typer.Context, interface_id: str = typer.Argument( help="ID of interface to remove.", show_default=False, ), ) -> None: """Remove a host interface.""" from zabbix_cli.models import Result app.state.client.delete_host_interface(interface_id) render_result(Result(message=f"Removed host interface ({interface_id}).")) @app.command( name="define_host_monitoring_status", rich_help_panel=HELP_PANEL, hidden=True, deprecated=True, ) @app.command(name="monitor_host", rich_help_panel=HELP_PANEL) def monitor_host( hostname: str = typer.Argument( help="Name of host", show_default=False, ), new_status: MonitoringStatus = typer.Argument( help="Monitoring status", case_sensitive=False, show_default=False, ), ) -> None: """Monitor or unmonitor a host.""" from zabbix_cli.models import Result host = app.state.client.get_host(hostname) app.state.client.update_host_status(host, new_status) render_result( Result( message=f"Updated host {hostname!r}. New monitoring status: {new_status}" ) ) @app.command(name="remove_host", rich_help_panel=HELP_PANEL) def remove_host( ctx: typer.Context, hostname: str = typer.Argument( help="Name of host to remove.", show_default=False, ), ) -> None: """Delete a host.""" from zabbix_cli.models import Result host = app.state.client.get_host(hostname) app.state.client.delete_host(host.hostid) render_result(Result(message=f"Removed host {hostname!r}.")) @app.command(name="show_host", rich_help_panel=HELP_PANEL) def show_host( ctx: typer.Context, hostname_or_id: str = typer.Argument( help="Hostname or ID.", show_default=False, ), active: Optional[ActiveInterface] = typer.Option( None, "--active", help="Active interface availability.", case_sensitive=False, ), maintenance: Optional[bool] = typer.Option( None, "--maintenance/--no-maintenance", help="Maintenance status.", show_default=False, ), monitored: Optional[bool] = typer.Option( None, "--monitored/--no-monitored", help="Monitoring status.", show_default=False, ), # This is the legacy filter argument from V2 filter_legacy: Optional[str] = typer.Argument(None, hidden=True), ) -> None: """Show a specific host.""" from zabbix_cli.commands.results.host import HostFilterArgs from zabbix_cli.pyzabbix.utils import get_proxy_map args = HostFilterArgs.from_command_args( filter_legacy, active, maintenance, monitored ) host = app.state.client.get_host( hostname_or_id, select_groups=True, select_templates=True, select_interfaces=True, sort_field="host", sort_order="ASC", search=True, # we allow wildcard patterns maintenance=args.maintenance_status, monitored=args.status, active_interface=args.active, ) # HACK: inject proxy map to host for rendering # TODO: cache the proxy map for some time? In case we run show_host multiple times proxy_map = get_proxy_map(app.state.client) host.set_proxy(proxy_map) render_result(host) @app.command( name="show_hosts", rich_help_panel=HELP_PANEL, examples=[ Example( "Show all monitored (enabled) hosts", "show_hosts --monitored", ), Example( "Show all hosts with names ending in '.example.com'", "show_hosts '*.example.com'", ), Example( "Show all hosts with names ending in '.example.com' or '.example.net'", "show_hosts '*.example.com,*.example.net'", ), Example( "Show all hosts with names ending in '.example.com' or '.example.net'", "show_hosts '*.example.com,*.example.net'", ), Example( "Show all hosts from a given hostgroup", "show_hosts --hostgroup 'Linux servers'", ), ], ) def show_hosts( ctx: typer.Context, hostname_or_id: Optional[str] = typer.Argument( None, help="Hostname pattern or ID to filter by. Comma-separated. Supports wildcards.", show_default=False, ), hostgroup: Optional[str] = typer.Option( None, "--hostgroup", help="Hostgroup name(s) or ID(s). Comma-separated.", ), active: Optional[ActiveInterface] = typer.Option( None, "--active", help="Active interface availability.", case_sensitive=False, ), maintenance: Optional[bool] = typer.Option( None, "--maintenance/--no-maintenance", help="Maintenance status.", show_default=False, ), monitored: Optional[bool] = typer.Option( None, "--monitored/--unmonitored", help="Monitoring status.", show_default=False, ), limit: int = OPTION_LIMIT, # V2 Legacy filter argument filter_legacy: Optional[str] = typer.Argument(None, hidden=True), # TODO: add sorting mode? ) -> None: """Show all hosts. Hosts can be filtered by agent, monitoring and maintenance status. Hosts are sorted by name. """ from zabbix_cli.commands.results.host import HostFilterArgs from zabbix_cli.models import AggregateResult from zabbix_cli.pyzabbix.utils import get_proxy_map # Unified parsing of legacy and V3-style filter arguments args = HostFilterArgs.from_command_args( filter_legacy, active, maintenance, monitored ) hostnames_or_ids = parse_list_arg(hostname_or_id) hgs = parse_list_arg(hostgroup) hostgroups = [app.state.client.get_hostgroup(hg) for hg in hgs] with app.status("Fetching hosts..."): hosts = app.state.client.get_hosts( *hostnames_or_ids, select_groups=True, select_templates=True, sort_field="host", sort_order="ASC", search=True, # we use a wildcard pattern here! maintenance=args.maintenance_status, monitored=args.status, active_interface=args.active, limit=limit, hostgroups=hostgroups, ) # HACK: inject proxy map for each host # By default, each host only has a proxy ID. # We need to determine inside each host object which # Proxy object to select proxy_map = get_proxy_map(app.state.client) for host in hosts: host.set_proxy(proxy_map) render_result(AggregateResult(result=hosts)) @app.command(name="show_host_interfaces", rich_help_panel=HELP_PANEL) def show_host_interfaces( hostname_or_id: str = typer.Argument( help="Hostname or ID", show_default=False, ), ) -> None: """Show host interfaces.""" from zabbix_cli.models import AggregateResult host = app.state.client.get_host(hostname_or_id, select_interfaces=True) render_result(AggregateResult(result=host.interfaces)) @app.command(name="show_host_inventory", rich_help_panel=HELP_PANEL) def show_host_inventory( hostname_or_id: str = typer.Argument( help="Hostname or ID", show_default=False, ), ) -> None: """Show host inventory details for a specific host.""" # TODO: support undocumented filter argument from V2 # TODO: Add mapping of inventory keys to human readable names (Web GUI names) host = app.state.client.get_host(hostname_or_id, select_inventory=True) render_result(host.inventory) @app.command(name="update_host_inventory", rich_help_panel=HELP_PANEL) def update_host_inventory( ctx: typer.Context, hostname_or_id: str = typer.Argument( help="Hostname or ID of host.", show_default=False, ), key: str = typer.Argument( help="Inventory key", show_default=False, ), value: str = typer.Argument( help="Inventory value", show_default=False, ), ) -> None: """Update a host inventory field. Inventory fields in the API do not always match Web GUI field names. Use `zabbix-cli -o json show_host_inventory ` to see the available fields. """ from zabbix_cli.models import Result host = app.state.client.get_host(hostname_or_id) to_update = {key: value} app.state.client.update_host_inventory(host, to_update) render_result(Result(message=f"Updated inventory field {key!r} for host {host}.")) unioslo-zabbix-cli-09a2fab/zabbix_cli/commands/hostgroup.py000066400000000000000000000402061471265333400242370ustar00rootroot00000000000000from __future__ import annotations from typing import List from typing import Optional import typer from zabbix_cli.app import Example from zabbix_cli.app import app from zabbix_cli.commands.common.args import OPTION_LIMIT from zabbix_cli.commands.host import HELP_PANEL from zabbix_cli.output.console import error from zabbix_cli.output.console import exit_err from zabbix_cli.output.console import info from zabbix_cli.output.console import success from zabbix_cli.output.formatting.grammar import pluralize as p from zabbix_cli.output.render import render_result from zabbix_cli.pyzabbix.enums import UsergroupPermission from zabbix_cli.utils.args import parse_hostgroups_arg from zabbix_cli.utils.args import parse_hostnames_arg from zabbix_cli.utils.args import parse_list_arg @app.command( "add_host_to_hostgroup", rich_help_panel=HELP_PANEL, examples=[ Example( "Add a host to a host group", "add_host_to_hostgroup 'My host' 'My host group'", ), Example( "Add multiple hosts to a host group", "add_host_to_hostgroup 'host1,host2' 'My host group'", ), Example( "Add multiple hosts to multiple host groups", "add_host_to_hostgroup 'host1,host2' 'My host group,Another group'", ), ], ) def add_host_to_hostgroup( ctx: typer.Context, hostnames_or_ids: str = typer.Argument( help="Host names or IDs. Comma-separated. Supports wildcards.", metavar="HOSTS", show_default=False, ), hostgroups: str = typer.Argument( help="Host group names or IDs. Comma-separated. Supports wildcards.", metavar="HOSTGROUPS", show_default=False, ), dryrun: bool = typer.Option( False, "--dryrun", help="Preview changes", ), ) -> None: """Add hosts to host groups. Host name and group arguments are interpreted as IDs if they are numeric. """ import itertools from zabbix_cli.commands.results.hostgroup import AddHostsToHostGroup from zabbix_cli.models import AggregateResult hosts = parse_hostnames_arg(app, hostnames_or_ids) hgs = parse_hostgroups_arg(app, hostgroups, select_hosts=True) if not dryrun: with app.status("Adding hosts to host groups..."): app.state.client.add_hosts_to_hostgroups(hosts, hgs) result: List[AddHostsToHostGroup] = [] for hg in hgs: r = AddHostsToHostGroup.from_result(hosts, hg) if not r.hosts: continue result.append(r) total_hosts = len(set(itertools.chain.from_iterable((r.hosts) for r in result))) total_hgs = len(result) if not total_hosts: exit_err("No hosts to add.") render_result(AggregateResult(result=result)) base_msg = f"{p('host', total_hosts)} to {p('host group', total_hgs)}" if dryrun: info(f"Would add {base_msg}.") else: success(f"Added {base_msg}.") @app.command( "remove_host_from_hostgroup", rich_help_panel=HELP_PANEL, examples=[ Example( "Remove a host to a host group", "remove_host_from_hostgroup 'My host' 'My host group'", ), Example( "Remove multiple hosts from a host group", "remove_host_from_hostgroup 'host1,host2' 'My host group'", ), Example( "Remove multiple hosts from multiple host groups", "remove_host_from_hostgroup 'host1,host2' 'My host group,Another group'", ), ], ) def remove_host_from_hostgroup( hostnames_or_ids: str = typer.Argument( help="Host names or IDs. Comma-separated. Supports wildcards.", metavar="HOSTS", show_default=False, ), hostgroups: str = typer.Argument( help="Host group names or IDs. Comma-separated. Supports wildcards.", metavar="HOSTGROUPS", show_default=False, ), dryrun: bool = typer.Option( False, "--dryrun", help="Preview changes", ), ) -> None: """Remove hosts from host groups.""" import itertools from zabbix_cli.commands.results.hostgroup import RemoveHostsFromHostGroup from zabbix_cli.models import AggregateResult hosts = parse_hostnames_arg(app, hostnames_or_ids) hgs = parse_hostgroups_arg(app, hostgroups, select_hosts=True) if not dryrun: with app.status("Removing hosts from host groups..."): app.state.client.remove_hosts_from_hostgroups(hosts, hgs) result: List[RemoveHostsFromHostGroup] = [] for hg in hgs: r = RemoveHostsFromHostGroup.from_result(hosts, hg) if not r.hosts: continue result.append(r) total_hosts = len(set(itertools.chain.from_iterable((r.hosts) for r in result))) total_hgs = len(result) render_result(AggregateResult(result=result)) base_msg = f"{p('host', total_hosts)} from {p('host group', total_hgs)}" if dryrun: info(f"Would remove {base_msg}.") else: success(f"Removed {base_msg}.") @app.command( "create_hostgroup", rich_help_panel=HELP_PANEL, examples=[ Example( "Create a host group with default user group permissions", "create_hostgroup 'My Host Group'", ), Example( "Create a host group with specific RO and RW groups", "create_hostgroup 'My Host Group' --ro-groups users --rw-groups admins", ), Example( "Create a host group with no user group permissions", "create_hostgroup 'My Host Group' --no-usergroup-permissions", ), ], ) def create_hostgroup( hostgroup: str = typer.Argument( help="Name of host group.", show_default=False, ), rw_groups: Optional[str] = typer.Option( None, "--rw-groups", help="User group(s) to give read/write permissions. Comma-separated.", ), ro_groups: Optional[str] = typer.Option( None, "--ro-groups", help="User group(s) to give read-only permissions. Comma-separated.", ), no_usergroup_permissions: bool = typer.Option( False, "--no-usergroup-permissions", help="Do not assign user group permissions.", ), ) -> None: """Create a new host group. Assigns default user group permissions by default. * [option]--rw-groups[/] defaults to config option [configopt]app.default_admin_usergroups[/]. * [option]--ro-groups[/] defaults to config option [configopt]app.default_create_user_usergroups[/]. * Use [option]--no-usergroup-permissions[/] to create a group without any user group permissions. """ from zabbix_cli.models import Result if app.state.client.hostgroup_exists(hostgroup): exit_err(f"Host group {hostgroup!r} already exists.") with app.status("Creating host group..."): hostgroup_id = app.state.client.create_hostgroup(hostgroup) app_config = app.state.config.app rw_grps: List[str] = [] ro_grps: List[str] = [] if not no_usergroup_permissions: rw_grps = parse_list_arg(rw_groups) or app_config.default_admin_usergroups ro_grps = parse_list_arg(ro_groups) or app_config.default_create_user_usergroups try: # Admin group(s) gets Read/Write for usergroup in rw_grps: app.state.client.update_usergroup_rights( usergroup, [hostgroup], UsergroupPermission.READ_WRITE, hostgroup=True ) info(f"Assigned Read/Write permission for user group {usergroup!r}") # Default group(s) gets Read for usergroup in ro_grps: app.state.client.update_usergroup_rights( usergroup, [hostgroup], UsergroupPermission.READ_ONLY, hostgroup=True ) info(f"Assigned Read-only permission for user group {usergroup!r}") except Exception as e: # All or nothing. Delete group if we fail to assign permissions. error(f"Failed to assign permissions to host group {hostgroup!r}: {e}") info("Deleting host group...") app.state.client.delete_hostgroup(hostgroup_id) exit_err(f"Failed to create host group {hostgroup!r}.") render_result(Result(message=f"Created host group {hostgroup} ({hostgroup_id}).")) @app.command("remove_hostgroup", rich_help_panel=HELP_PANEL) def delete_hostgroup( hostgroup: str = typer.Argument( help="Name of host group(s) to delete. Comma-separated.", show_default=False, ), force: bool = typer.Option( False, "--force", help="Remove host group even if it contains hosts.", ), ) -> None: """Delete a host group.""" from zabbix_cli.commands.results.hostgroup import HostGroupDeleteResult from zabbix_cli.models import Result hostgroup_names = parse_list_arg(hostgroup) hostgroups = [ app.state.client.get_hostgroup(hg, select_hosts=True) for hg in hostgroup_names ] for hg in hostgroups: if hg.hosts and not force: exit_err( f"Host group {hg.name!r} contains {p('host', len(hg.hosts))}. Use --force to delete." ) app.state.client.delete_hostgroup(hg.groupid) render_result( Result( message=f"Host group {hostgroup!r} deleted.", result=HostGroupDeleteResult(groups=hostgroup_names), ), ) @app.command("extend_hostgroup", rich_help_panel=HELP_PANEL) def extend_hostgroup( src_group: str = typer.Argument( help="Group to get hosts from.", show_default=False, ), dest_group: str = typer.Argument( help="Group(s) to add hosts to. Comma-separated. Supports wildcards.", show_default=False, ), dryrun: bool = typer.Option( False, "--dryrun", help="Show hosts and groups without making changes.", ), ) -> None: """Add all hosts from a host group to other host groups. The source group is not modified. Existing hosts in the destination group(s) are not removed or modified. """ from zabbix_cli.commands.results.hostgroup import ExtendHostgroupResult dest_args = parse_list_arg(dest_group) src = app.state.client.get_hostgroup(src_group, select_hosts=True) dest = app.state.client.get_hostgroups(*dest_args, select_hosts=True) if not dest: exit_err(f"No host groups found matching {dest_group!r}.") if not src.hosts: exit_err(f"No hosts found in host group {src_group!r}.") # TODO: calculate the number of hosts that would be added like the other commands if not dryrun: app.state.client.add_hosts_to_hostgroups(src.hosts, dest) success( f"Copied {len(src.hosts)} hosts from {src.name!r} to {len(dest)} groups." ) else: info(f"Would copy {len(src.hosts)} hosts from {src.name!r}:") render_result(ExtendHostgroupResult.from_result(src, dest)) @app.command("move_hosts", rich_help_panel=HELP_PANEL) def move_hosts( src_group: str = typer.Argument( help="Group to move hosts from.", show_default=False, ), dest_group: str = typer.Argument( help="Group to move hosts to.", show_default=False, ), rollback: bool = typer.Option( True, "--rollback/--no-rollback", help="Rollback changes if hosts cannot be removed from source group afterwards.", ), dryrun: bool = typer.Option( False, "--dryrun", help="Show hosts and groups without making changes.", ), ) -> None: """Move all hosts from one host group to another.""" from zabbix_cli.commands.results.hostgroup import MoveHostsResult src = app.state.client.get_hostgroup(src_group, select_hosts=True) dest = app.state.client.get_hostgroup(dest_group, select_hosts=True) if not src.hosts: exit_err(f"No hosts found in host group {src_group!r}.") # TODO: calculate the number of hosts that would be added like the other commands if dryrun: info(f"Would move {len(src.hosts)} hosts to {dest.name!r}:") else: app.state.client.add_hosts_to_hostgroups(src.hosts, [dest]) info(f"Added hosts to {dest.name!r}") try: app.state.client.remove_hosts_from_hostgroups(src.hosts, [src]) except Exception as e: if rollback: error( f"Failed to remove hosts from {src.name!r}. Attempting to roll back changes." ) app.state.client.remove_hosts_from_hostgroups(src.hosts, [dest]) raise e else: info(f"Removed hosts from {src.name!r}.") success(f"Moved {len(src.hosts)} hosts from {src.name!r} to {dest.name!r}.") render_result(MoveHostsResult.from_result(src, dest)) @app.command("show_hostgroup", rich_help_panel=HELP_PANEL) def show_hostgroup( ctx: typer.Context, hostgroup: str = typer.Argument( help="Name of host group.", show_default=False, ), ) -> None: """Show details of a host group.""" from zabbix_cli.commands.results.hostgroup import HostGroupResult hg = app.state.client.get_hostgroup(hostgroup, select_hosts=True) render_result(HostGroupResult.from_hostgroup(hg)) # TODO: match V2 behavior @app.command( "show_hostgroups", rich_help_panel=HELP_PANEL, examples=[ Example( "Show all host groups", "show_hostgroups --limit 0", ), Example( "Show all host groups starting with 'Web-'", "show_hostgroups 'Web-*'", ), Example( "Show host groups with 'web' in the name", "show_hostgroups '*web*'", ), ], ) def show_hostgroups( name: Optional[str] = typer.Argument( help="Name of host group(s). Comma-separated. Supports wildcards.", show_default=False, ), select_hosts: bool = typer.Option( True, "--hosts/--no-hosts", help="Show hosts in each host group." ), limit: int = OPTION_LIMIT, ) -> None: """Show details for host groups. Limits results to 20 by default. Fetching all host groups with hosts can be extremely slow.""" from zabbix_cli.commands.results.hostgroup import HostGroupResult from zabbix_cli.models import AggregateResult names = parse_list_arg(name) with app.status("Fetching host groups..."): hostgroups = app.state.client.get_hostgroups( *names, select_hosts=select_hosts, search=True, sort_field="name", sort_order="ASC", limit=limit, ) render_result( AggregateResult( result=[HostGroupResult.from_hostgroup(hg) for hg in hostgroups] ) ) @app.command("show_hostgroup_permissions", rich_help_panel=HELP_PANEL) def show_hostgroup_permissions( hostgroups: str = typer.Argument( help="Host group name(s). Comma-separated. Supports wildcards.", show_default=False, ), ) -> None: """Show usergroups with permissions for the given hostgroup. Shows permissions for all host groups by default. """ from zabbix_cli.commands.results.hostgroup import HostGroupPermissions from zabbix_cli.models import AggregateResult hg_names = parse_list_arg(hostgroups) with app.status("Fetching host groups..."): usergroups = app.state.client.get_usergroups() hgs = app.state.client.get_hostgroups( *hg_names, sort_field="name", sort_order="ASC", select_hosts=False, search=True, ) result: List[HostGroupPermissions] = [] for hg in hgs: permissions: List[str] = [] for usergroup in usergroups: if app.api_version >= (6, 2, 0): rights = usergroup.hostgroup_rights else: rights = usergroup.rights for right in rights: if right.id == hg.groupid: perm = UsergroupPermission.string_from_value(right.permission) permissions.append(f"{usergroup.name} ({perm})") break result.append( HostGroupPermissions( groupid=hg.groupid, name=hg.name, permissions=permissions, ) ) return render_result(AggregateResult(result=result)) unioslo-zabbix-cli-09a2fab/zabbix_cli/commands/item.py000066400000000000000000000046751471265333400231550ustar00rootroot00000000000000from __future__ import annotations from typing import List from typing import Optional import typer from zabbix_cli._v2_compat import ARGS_POSITIONAL from zabbix_cli.app import Example from zabbix_cli.app import app from zabbix_cli.commands.common.args import OPTION_LIMIT from zabbix_cli.output.console import exit_err from zabbix_cli.output.render import render_result from zabbix_cli.utils.args import parse_list_arg HELP_PANEL = "Item" @app.command( name="show_last_values", rich_help_panel=HELP_PANEL, examples=[ Example( "Get items starting with 'MongoDB'", "show_last_values 'MongoDB*'", ), Example( "Get items containing 'memory'", "show_last_values '*memory*'", ), Example( "Get all items (WARNING: slow!)", "show_last_values '*'", ), ], ) def show_last_values( ctx: typer.Context, item: str = typer.Argument( help="Item names or IDs. Comma-separated. Supports wildcards.", show_default=False, ), group: bool = typer.Option( False, "--group", is_flag=True, help="Group items with the same value." ), limit: Optional[int] = OPTION_LIMIT, args: Optional[List[str]] = ARGS_POSITIONAL, ) -> None: """Show the last values of given items of monitored hosts.""" from zabbix_cli.commands.results.item import ItemResult from zabbix_cli.commands.results.item import group_items from zabbix_cli.models import AggregateResult if args: if not len(args) == 1: exit_err("Invalid number of positional arguments. Use options instead.") group = args[0] == "1" # No format arg in V2... names_or_ids = parse_list_arg(item) with app.status("Fetching items..."): items = app.state.client.get_items( *names_or_ids, select_hosts=True, monitored=True, limit=limit ) # HACK: not super elegant, but this allows us to match V2 output while # with and without the --group flag, as well as ALSO rendering the entire # Item object instead of just a subset of fields. # Ideally, it would be nice to not have to re-validate when not grouping # but I'm not sure how to do that in Pydantic V2? if group: res = group_items(items) render_result(AggregateResult(result=res)) else: res = [ItemResult.from_item(item) for item in items] render_result(AggregateResult(result=res)) unioslo-zabbix-cli-09a2fab/zabbix_cli/commands/macro.py000066400000000000000000000207361471265333400233140ustar00rootroot00000000000000"""Commands to view and manage macros.""" from __future__ import annotations from typing import Optional import typer from zabbix_cli.app import Example from zabbix_cli.app import app from zabbix_cli.commands.common.args import get_limit_option from zabbix_cli.config.constants import OutputFormat from zabbix_cli.exceptions import ZabbixCLIError from zabbix_cli.exceptions import ZabbixNotFoundError from zabbix_cli.output.console import exit_err from zabbix_cli.output.render import render_result HELP_PANEL = "Macro" def fmt_macro_name(macro: str) -> str: """Format macro name for use in a query.""" macro = macro.strip() if not macro: # TODO: More specific exception class raise ZabbixCLIError("Macro name cannot be empty.") if not macro.isupper(): macro = macro.upper() if not macro.startswith("{"): macro = "{" + macro if not macro.endswith("}"): macro = macro + "}" if not macro[1] == "$": # NOTE: refactor could break this macro = "{$" + macro[1:] if macro == "{$}": raise ZabbixCLIError(f"Invalid macro name {macro!r}") return macro @app.command( name="define_host_usermacro", rich_help_panel=HELP_PANEL, examples=[ Example( "Create a macro named {$SNMP_COMMUNITY} for a host", "define_host_usermacro foo.example.com '{$SNMP_COMMUNITY}' public", ), Example( "Create a macro named {$SITE_URL} for a host (automatic name conversion)", "define_host_usermacro foo.example.com site_url https://example.com", ), ], ) def define_host_usermacro( # NOTE: should this use old style args? hostname: str = typer.Argument( help="Host to define macro for.", show_default=False ), macro_name: str = typer.Argument( help=( "Name of macro. " "Names will be converted to the Zabbix format, " "i.e. [value]site_url[/] becomes [value]{$SITE_URL}[/]." ), show_default=False, ), macro_value: str = typer.Argument( help="Default value of macro.", show_default=False ), ) -> None: """Create or update a host usermacro.""" from zabbix_cli.models import Result host = app.state.client.get_host(hostname) macro_name = fmt_macro_name(macro_name) # Determine if we should create or update macro try: macro = app.state.client.get_macro(host=host, macro_name=macro_name) except ZabbixNotFoundError: macro_id = app.state.client.create_macro( host=host, macro=macro_name, value=macro_value ) action = "Created" else: macro_id = app.state.client.update_macro( macroid=macro.hostmacroid, value=macro_value ) action = "Updated" render_result( Result( message=f"{action} macro {macro_name!r} with ID {macro_id} for host {hostname!r}." ) ) @app.command(name="show_host_usermacros", rich_help_panel=HELP_PANEL, hidden=False) def show_host_usermacros( hostname_or_id: str = typer.Argument( help="Hostname or ID to show macros for", show_default=False, ), ) -> None: """Show all macros defined for a host.""" from zabbix_cli.commands.results.macro import ShowHostUserMacrosResult from zabbix_cli.models import AggregateResult # By getting the macros via the host, we also ensure the host exists. host = app.state.client.get_host(hostname_or_id, select_macros=True) render_result( AggregateResult( result=[ ShowHostUserMacrosResult.from_result(macro) # Sort macros by name when rendering for macro in sorted(host.macros, key=lambda m: m.macro) ] ) ) @app.command(name="show_usermacro_host_list", rich_help_panel=HELP_PANEL, hidden=False) def show_usermacro_host_list( usermacro: str = typer.Argument( help=( "Name of macro to find hosts with. " "Macro names are automatically formatted, e.g. [value]site_url[/] becomes [value]{$SITE_URL}[/]." ), show_default=False, ), limit: Optional[int] = get_limit_option(), ) -> None: """Find all hosts with a user macro of the given name. Renders a list of the complete macro object and its hosts in JSON mode. """ from zabbix_cli.commands.results.macro import MacroHostListV2 from zabbix_cli.commands.results.macro import MacroHostListV3 from zabbix_cli.models import AggregateResult usermacro = fmt_macro_name(usermacro) macros = app.state.client.get_macros( macro_name=usermacro, select_hosts=True, limit=limit ) macros = [macro for macro in macros if macro.hosts] # This is a place where we need to differentiate between legacy and # new JSON modes instead of sharing a single model and # letting the render function figure it out. # The V2 command only renders a single host, but the whole point of this # command is to list _all_ hosts with the given macro, so we want to render # the macro and a list of EVERY host with that macro. if ( app.state.config.app.output.format == OutputFormat.JSON and app.state.config.app.legacy_json_format ): render_result( AggregateResult(result=[MacroHostListV2(macro=macro) for macro in macros]) ) else: if not macros: exit_err(f"Macro {usermacro!r} not found.") render_result( AggregateResult(result=[MacroHostListV3(macro=macro) for macro in macros]) ) @app.command("define_global_macro", rich_help_panel=HELP_PANEL) def define_global_macro( ctx: typer.Context, name: str = typer.Argument(help="Name of the macro", show_default=False), value: str = typer.Argument(help="Value of the macro", show_default=False), ) -> None: """Create a global macro.""" from zabbix_cli.commands.results.macro import GlobalMacroResult from zabbix_cli.models import Result name = fmt_macro_name(name) try: macro = app.state.client.get_global_macro(macro_name=name) except ZabbixNotFoundError: pass else: exit_err(f"Macro {name!r} already exists with value {macro.value!r}") macro_id = app.state.client.create_global_macro(macro=name, value=value) render_result( Result( message=f"Created macro {name!r} with ID {macro_id}.", result=GlobalMacroResult(macro=name, globalmacroid=macro_id, value=value), ), ) @app.command("show_global_macros", rich_help_panel=HELP_PANEL) def show_global_macros(ctx: typer.Context) -> None: """Show all global macros.""" from zabbix_cli.commands.results.macro import GlobalMacroResult from zabbix_cli.models import AggregateResult macros = app.state.client.get_global_macros() render_result( AggregateResult( result=[ GlobalMacroResult( macro=m.macro, globalmacroid=m.globalmacroid, value=m.value ) for m in macros ] ) ) @app.command( "show_usermacro_template_list", rich_help_panel=HELP_PANEL, examples=[ Example( "Show all templates with a user macro named {$SNMP_COMMUNITY}", "show_usermacro_template_list SNMP_COMMUNITY", ) ], ) def show_usermacro_template_list( ctx: typer.Context, macro_name: str = typer.Argument( help="Name of the macro to find templates with. Automatically formatted.", show_default=False, ), limit: Optional[int] = get_limit_option(), ) -> None: """Find all templates with a user macro of the given name.""" import itertools from zabbix_cli.commands.results.macro import ShowUsermacroTemplateListResult from zabbix_cli.models import AggregateResult macro_name = fmt_macro_name(macro_name) macros = app.state.client.get_macros( macro_name=macro_name, select_templates=True, limit=limit ) macros = [macro for macro in macros if macro.templates] results = itertools.chain.from_iterable( [ [ ShowUsermacroTemplateListResult( macro=macro.macro, value=macro.value, templateid=template.templateid, template=template.host, ) for template in macro.templates ] for macro in macros ] ) render_result( AggregateResult( result=list(results), ) ) unioslo-zabbix-cli-09a2fab/zabbix_cli/commands/maintenance.py000066400000000000000000000202071471265333400244660ustar00rootroot00000000000000from __future__ import annotations from typing import Optional import typer from zabbix_cli.app import Example from zabbix_cli.app import app from zabbix_cli.commands.common.args import OPTION_LIMIT from zabbix_cli.output.console import exit_err from zabbix_cli.output.prompts import str_prompt_optional from zabbix_cli.output.render import render_result from zabbix_cli.pyzabbix.enums import DataCollectionMode from zabbix_cli.utils.args import parse_list_arg from zabbix_cli.utils.utils import convert_time_to_interval HELP_PANEL = "Maintenance" @app.command( name="create_maintenance_definition", rich_help_panel=HELP_PANEL, examples=[ Example( "Create a maintenance for a host from now for 1 hour (default)", "create_maintenance_definition 'My maintenance' --host 'My host'", ), Example( "Create a maintenance for a host group in a specific time period", "create_maintenance_definition 'My maintenance' --hostgroup 'Linux servers' --period '2022-12-31T23:00 to 2023-01-01T01:00'", ), Example( "Create a maintenance definition with all options", "create_maintenance_definition 'My maintenance' --hostgroup 'Linux servers' --period '2 hours 30 minutes 15 seconds' --description 'Maintenance for Linux servers' --data-collection ON", ), ], ) def create_maintenance_definition( ctx: typer.Context, name: str = typer.Argument( help="Maintenance name.", show_default=False, ), description: Optional[str] = typer.Option( None, "--description", help="Description.", ), hosts: Optional[str] = typer.Option( None, "--host", help="Host(s). Comma-separated.", ), hostgroups: Optional[str] = typer.Option( None, "--hostgroup", help="Host group(s). Comma-separated.", ), period: str = typer.Option( "1 hour", "--period", help="Time period in seconds, minutes, hours, days, or as ISO timestamp.", ), data_collection: DataCollectionMode = typer.Option( DataCollectionMode.ON.value, "--data-collection", help="Enable or disable data collection.", ), ) -> None: """Create a new one-time maintenance definition. One can define an interval between two timestamps in ISO format or a time period in minutes, hours or days from the moment the definition is created. Periods are assumed to be in seconds if no unit is specified. If no period is specified, the default is 1 hour. """ from zabbix_cli.commands.results.maintenance import ( CreateMaintenanceDefinitionResult, ) from zabbix_cli.models import Result hosts_arg = parse_list_arg(hosts) hgs_arg = parse_list_arg(hostgroups) start, end = convert_time_to_interval(period) hostlist = app.state.client.get_hosts(*hosts_arg) if hosts_arg else [] hglist = app.state.client.get_hostgroups(*hgs_arg) if hgs_arg else [] with app.status("Creating maintenance definition..."): maintenance_id = app.state.client.create_maintenance( name=name, description=description, active_since=start, active_till=end, hosts=hostlist, hostgroups=hglist, data_collection=data_collection, ) render_result( Result( message=f"Created maintenance definition ({maintenance_id}).", result=CreateMaintenanceDefinitionResult(maintenance_id=maintenance_id), ) ) # TODO: remove maintenances affecting certain hosts or host groups # Either by removing the maintenance definition itself or by removing the hosts # or host groups from the maintenance definition... # TODO: Add remove maintenance by name @app.command(name="remove_maintenance_definition", rich_help_panel=HELP_PANEL) def remove_maintenance_definition( ctx: typer.Context, maintenance_id: str = typer.Argument( help="ID(s) of maintenance(s) to remove. Comma-separated.", show_default=False, ), ) -> None: """Remove a maintenance definition.""" from zabbix_cli.models import Result maintenance_ids = parse_list_arg(maintenance_id) if not maintenance_ids: exit_err("Must specify at least one maintenance ID.") for mid in maintenance_ids: # Check that each ID exists app.state.client.get_maintenance(mid) with app.status("Removing maintenance definition..."): app.state.client.delete_maintenance(*maintenance_ids) render_result(Result(message="Removed maintenance definition(s).")) @app.command(name="show_maintenance_definitions", rich_help_panel=HELP_PANEL) def show_maintenance_definitions( ctx: typer.Context, maintenance_id: Optional[str] = typer.Option( None, "--maintenance-id", help="Maintenance IDs. Comma-separated." ), hostgroup: Optional[str] = typer.Option( None, "--hostgroup", help="Host group names. Comma-separated." ), host: Optional[str] = typer.Option( None, "--host", help="Host names. Comma-separated." ), ) -> None: """Show maintenance definitions for IDs, host groups or hosts. At least one of [option]--maintenance-id[/], [option]--hostgroup[/], or [option]--host[/] is required. """ from zabbix_cli.commands.results.maintenance import ShowMaintenanceDefinitionsResult from zabbix_cli.models import AggregateResult if not any((maintenance_id, hostgroup, host)): maintenance_id = str_prompt_optional("Maintenance ID") hostgroup = str_prompt_optional("Host group(s)") host = str_prompt_optional("Host(s)") mids = parse_list_arg(maintenance_id) hg_names = parse_list_arg(hostgroup) host_names = parse_list_arg(host) if not any((mids, hg_names, host_names)): exit_err("Must specify at least one maintenance ID, host group, or host.") # Checks that host(group)s exist and gets their IDs if hg_names: hostgroups = app.state.client.get_hostgroups(*hg_names) else: hostgroups = [] if host_names: hosts = app.state.client.get_hosts(*host_names) else: hosts = [] with app.status("Fetching maintenance definitions..."): maintenances = app.state.client.get_maintenances( maintenance_ids=mids, hostgroups=hostgroups, hosts=hosts, ) render_result( AggregateResult( result=[ ShowMaintenanceDefinitionsResult( maintenanceid=m.maintenanceid, name=m.name, type=m.maintenance_type, active_till=m.active_till, # type: ignore # validator handles None hosts=[h.host for h in m.hosts], groups=[hg.name for hg in m.hostgroups], description=m.description, ) for m in maintenances ] ) ) @app.command(name="show_maintenance_periods", rich_help_panel=HELP_PANEL) def show_maintenance_periods( ctx: typer.Context, maintenance_id: Optional[str] = typer.Argument( None, help="Maintenance IDs. Comma-separated. Supports wildcards.", show_default=False, ), limit: int = OPTION_LIMIT, ) -> None: """Show maintenance periods for one or more maintenance definitions. Shows all maintenance definitions by default. """ from zabbix_cli.commands.results.maintenance import ShowMaintenancePeriodsResult from zabbix_cli.models import AggregateResult mids = parse_list_arg(maintenance_id) with app.status("Fetching maintenance periods..."): maintenances = app.state.client.get_maintenances( maintenance_ids=mids, limit=limit ) render_result( AggregateResult( result=[ ShowMaintenancePeriodsResult( maintenanceid=m.maintenanceid, name=m.name, timeperiods=m.timeperiods, hosts=[h.host for h in m.hosts], groups=[hg.name for hg in m.hostgroups], ) for m in maintenances ] ) ) unioslo-zabbix-cli-09a2fab/zabbix_cli/commands/problem.py000066400000000000000000000211751471265333400236510ustar00rootroot00000000000000from __future__ import annotations from typing import List from typing import Optional import typer from zabbix_cli._v2_compat import ARGS_POSITIONAL from zabbix_cli.app import Example from zabbix_cli.app import app from zabbix_cli.output.console import err_console from zabbix_cli.output.console import exit_err from zabbix_cli.output.render import render_result from zabbix_cli.pyzabbix.enums import TriggerPriority from zabbix_cli.utils.args import parse_bool_arg from zabbix_cli.utils.args import parse_int_arg from zabbix_cli.utils.args import parse_list_arg HELP_PANEL = "Problem" @app.command(name="acknowledge_event", rich_help_panel=HELP_PANEL) def acknowledge_event( ctx: typer.Context, event_ids: str = typer.Argument( help="Comma-separated list of event ID(s)", show_default=False, ), message: str = typer.Option( "[Zabbix-CLI] Acknowledged via acknowledge_events", "--message", "-m", help="Message to add to the event", ), close: bool = typer.Option( False, "--close", "-c", help="Close the event after acknowledging it", ), # Legacy positional args args: Optional[List[str]] = ARGS_POSITIONAL, ) -> None: """Acknowledge events by ID.""" from zabbix_cli.commands.results.problem import AcknowledgeEventResult from zabbix_cli.models import Result eids = parse_list_arg(event_ids) if not eids: exit_err("No event IDs specified.") if args: if len(args) != 2: exit_err("Invalid number of positional arguments.") message = args[0] close = parse_bool_arg(args[1]) # Don't prompt for missing message. It's optional. acknowledged_ids = app.state.client.acknowledge_event( *eids, message=message, close=close ) msg = "Event(s) acknowledged" if close: msg += " and closed" render_result( Result( message=msg, result=AcknowledgeEventResult( event_ids=acknowledged_ids, close=close, message=message ), ) ) @app.command( name="acknowledge_trigger_last_event", rich_help_panel=HELP_PANEL, examples=[ Example( "Acknowledge the last event for trigger 12345", "acknowledge_trigger_last_event 12345", ), Example( "Acknowledge the last event for trigger 12345 with a message and close it", "acknowledge_trigger_last_event 12345 --message 'Acked via CLI' --close", ), Example( "Acknowledge multiple triggers", "acknowledge_trigger_last_event 12345,12346", ), ], ) def acknowledge_trigger_last_event( ctx: typer.Context, trigger_ids: str, message: Optional[str] = typer.Option( None, "--message", help="Acknowledgement message", ), close: bool = typer.Option( False, "--close", "-c", help="Close event", ), # Legacy positional args args: Optional[List[str]] = ARGS_POSITIONAL, ) -> None: """Acknowledge the the last event for the given triggers.""" from zabbix_cli.commands.results.problem import AcknowledgeTriggerLastEventResult from zabbix_cli.models import Result tids = parse_list_arg(trigger_ids) if not tids: exit_err("No trigger IDs specified.") if args: if len(args) != 2: exit_err("Invalid number of positional arguments.") message = args[0] close = parse_bool_arg(args[1]) # Message is optional, so we don't prompt for it if it's missing events = [app.state.client.get_event(object_id=tid) for tid in tids] event_ids = [e.eventid for e in events] acknowledged_ids = app.state.client.acknowledge_event( *event_ids, message=message, close=close ) msg = "Event(s) acknowledged" if close: msg += " and closed" render_result( Result( message=msg, result=AcknowledgeTriggerLastEventResult( trigger_ids=tids, event_ids=acknowledged_ids, close=close, message=message, ), ) ) @app.command( name="show_trigger_events", rich_help_panel=HELP_PANEL, examples=[ Example( "Show recent events for host foo.example.com", "show_trigger_events --host foo.example.com", ), Example( "Show recent events for hosts in host group 'Linux servers'", "show_trigger_events --hostgroup 'Linux servers'", ), Example( "Show 20 most recent events for triggers 12345 & 12346", "show_trigger_events --trigger-id 12345,12346 --limit 20", ), ], ) def show_trigger_events( ctx: typer.Context, trigger_id: Optional[str] = typer.Option( None, "--trigger-id", help="ID of trigger(s) to show events for.", ), hostgroups: Optional[str] = typer.Option( None, "--hostgroup", help="Host group(s) to show events for.", ), hosts: Optional[str] = typer.Option( None, "--host", help="Host(s) to show events for.", ), limit: int = typer.Option( 10, "--limit", "-l", help="Maximum number of events to show.", ), args: Optional[List[str]] = ARGS_POSITIONAL, ) -> None: """Show the latest events for the given triggers, hosts, and/or host groups. At least one trigger ID, host or host group must be specified. """ from zabbix_cli.models import AggregateResult if args: if len(args) != 2: exit_err("Invalid number of positional arguments.") trigger_id = args[0] limit = parse_int_arg(args[1]) # Parse commma-separated args trigger_ids = parse_list_arg(trigger_id) hostgroups_args = parse_list_arg(hostgroups) hosts_args = parse_list_arg(hosts) if not trigger_ids and not hostgroups_args and not hosts_args: err_console.print(ctx.get_help()) exit_err("At least one trigger ID, host or host group must be specified.") # Fetch the host(group)s if specified hostgroups_list = [app.state.client.get_hostgroup(hg) for hg in hostgroups_args] hosts_list = [app.state.client.get_host(host) for host in hosts_args] with app.status("Fetching events..."): events = app.state.client.get_events( object_ids=trigger_ids, group_ids=[hg.groupid for hg in hostgroups_list], host_ids=[host.hostid for host in hosts_list], sort_field="clock", sort_order="DESC", limit=limit, ) render_result(AggregateResult(result=events)) @app.command(name="show_alarms", rich_help_panel=HELP_PANEL) def show_alarms( ctx: typer.Context, description: Optional[str] = typer.Option( None, "--description", help="Description of alarm(s) to show.", ), # Could this be a list of priorities in V2? priority: Optional[TriggerPriority] = typer.Option( None, "--priority", help="Priority of alarm(s) to show.", ), hostgroups: Optional[str] = typer.Option( None, "--hostgroup", help="Host group(s) to show alarms for. Comma-separated.", ), unacknowledged: bool = typer.Option( True, "--unack/--ack", help="Show only alarms whose last event is unacknowledged.", ), args: Optional[List[str]] = ARGS_POSITIONAL, ) -> None: """Show the latest events for the given triggers, hosts, and/or host groups. At least one trigger ID, host or host group must be specified. """ from zabbix_cli.models import AggregateResult if args: if len(args) != 4: exit_err("Invalid number of positional arguments.") description = args[0] priority = TriggerPriority(args[1]) hostgroups = args[2] # in V2, "*" was used to indicate "false" if args[3] == "*": unacknowledged = False else: unacknowledged = parse_bool_arg(args[3]) hostgroups_args = parse_list_arg(hostgroups) hgs = [app.state.client.get_hostgroup(hg) for hg in hostgroups_args] with app.status("Fetching triggers..."): triggers = app.state.client.get_triggers( hostgroups=hgs, description=description, priority=priority, unacknowledged=unacknowledged, select_hosts=True, skip_dependent=True, monitored=True, active=True, expand_description=True, filter={"value": 1}, # why? ) render_result(AggregateResult(result=triggers)) unioslo-zabbix-cli-09a2fab/zabbix_cli/commands/proxy.py000066400000000000000000000534411471265333400233730ustar00rootroot00000000000000from __future__ import annotations import logging from typing import TYPE_CHECKING from typing import Dict from typing import List from typing import NamedTuple from typing import Optional from typing import Set import typer from zabbix_cli.app import Example from zabbix_cli.app import app from zabbix_cli.exceptions import ZabbixAPICallError from zabbix_cli.exceptions import ZabbixCLIError from zabbix_cli.output.console import error from zabbix_cli.output.console import exit_err from zabbix_cli.output.console import info from zabbix_cli.output.console import success from zabbix_cli.output.render import render_result from zabbix_cli.utils.args import get_hostgroup_hosts from zabbix_cli.utils.args import parse_int_list_arg from zabbix_cli.utils.args import parse_list_arg from zabbix_cli.utils.utils import compile_pattern if TYPE_CHECKING: from zabbix_cli.app import StatefulApp from zabbix_cli.pyzabbix.types import Host from zabbix_cli.pyzabbix.types import Proxy HELP_PANEL = "Proxy" class PrevProxyHosts(NamedTuple): hosts: List[Host] proxy: Optional[Proxy] = None def ensure_proxy_group_support() -> None: if app.state.client.version.major < 7: exit_err("Proxy groups require Zabbix 7.0 or later.") def group_hosts_by_proxy( app: StatefulApp, hosts: List[Host], default_proxy_id: str = "" ) -> Dict[str, PrevProxyHosts]: """Group hosts by the proxy they had prior to the update.""" proxy_ids: Set[str] = set() for host in hosts: if host.proxyid: proxy_ids.add(host.proxyid) # Fetch proxies for all observed proxy IDs proxy_mapping: Dict[str, PrevProxyHosts] = {} for proxy_id in proxy_ids: try: p = app.state.client.get_proxy(proxy_id) except ZabbixAPICallError as e: # Should be nigh-impossible, but someone might delete the proxy # while the command is running error(f"{e}") p = None proxy_mapping[proxy_id] = PrevProxyHosts(hosts=[], proxy=p) # The default is a special case - no prev proxy exists for these hosts proxy_mapping[default_proxy_id] = PrevProxyHosts(hosts=[], proxy=None) for host in hosts: if not host.proxyid: host_proxyid = default_proxy_id else: host_proxyid = host.proxyid proxy_mapping[host_proxyid].hosts.append(host) # No hosts without previous proxy - remove from mapping if not proxy_mapping[default_proxy_id].hosts: del proxy_mapping[default_proxy_id] return proxy_mapping @app.command(name="update_host_proxy", rich_help_panel=HELP_PANEL) def update_host_proxy( ctx: typer.Context, hostname: str = typer.Argument( help="Hostnames. Comma-separated Supports wildcards.", show_default=False, ), proxy: str = typer.Argument( help="Proxy name. Supports wildcards.", show_default=False, ), dryrun: bool = typer.Option( False, help="Preview changes", is_flag=True, ), ) -> None: """Assign hosts to a proxy. Supports wildcards for both hosts and proxy names. If multiple proxies match the proxy name, the first match is used. """ from zabbix_cli.commands.results.proxy import UpdateHostProxyResult from zabbix_cli.models import AggregateResult from zabbix_cli.output.console import info hostnames = parse_list_arg(hostname) hosts = app.state.client.get_hosts(*hostnames, search=True) dest_proxy = app.state.client.get_proxy(proxy) to_update: List[Host] = [] for host in hosts: if host.proxyid != dest_proxy.proxyid: to_update.append(host) if not dryrun: with app.status("Updating host proxies..."): hostids = app.state.client.update_hosts_proxy(to_update, dest_proxy) else: hostids = [host.hostid for host in to_update] updated = set(hostids) updated_hosts = [host for host in to_update if host.hostid in updated] proxy_hosts = group_hosts_by_proxy(app, updated_hosts) render_result( AggregateResult( empty_ok=True, result=[ UpdateHostProxyResult.from_result( hosts=prev.hosts, source_proxy=prev.proxy, dest_proxy=dest_proxy, ) for _, prev in proxy_hosts.items() ], ) ) total_hosts = len(updated) if dryrun: info(f"Would update proxy for {total_hosts} hosts.") else: success(f"Updated proxy for {total_hosts} hosts.") @app.command(name="clear_host_proxy", rich_help_panel=HELP_PANEL) def clear_host_proxy( ctx: typer.Context, hostname: str = typer.Argument( help="Hostnames. Comma-separated Supports wildcards.", show_default=False, ), dryrun: bool = typer.Option(False, help="Preview changes", is_flag=True), ) -> None: """Clear the proxy for one or more hosts. Sets the hosts to be monitored by the Zabbix server instead of a proxy. """ # NOTE: this command is _VERY_ similar to `update_host_proxy` # can we refactor them to avoid code duplication? from zabbix_cli.commands.results.proxy import ClearHostProxyResult from zabbix_cli.models import AggregateResult from zabbix_cli.output.console import error from zabbix_cli.output.console import info hostnames = parse_list_arg(hostname) hosts = app.state.client.get_hosts(*hostnames, search=True) to_update = [host for host in hosts if host.proxyid] if not dryrun: with app.status("Clearing host proxies..."): if not to_update: exit_err("No matching hosts have a proxy assigned.") try: app.state.client.clear_host_proxies(to_update) except Exception as e: error(f"Failed to clear proxies for hosts: {e}") proxy_map = group_hosts_by_proxy(app, to_update) render_result( AggregateResult( empty_ok=True, result=[ ClearHostProxyResult.from_result( hosts=prev.hosts, source_proxy=prev.proxy, ) for _, prev in proxy_map.items() ], ) ) total_hosts = len(hosts) if dryrun: info(f"Would clear proxy for {total_hosts} hosts.") else: success(f"Cleared proxy for {total_hosts} hosts.") @app.command( name="move_proxy_hosts", rich_help_panel=HELP_PANEL, examples=[ Example( "Move all hosts from one proxy to another", "move_proxy_hosts proxy1 proxy2", ), Example( "Move all hosts with names matching a regex pattern", "move_proxy_hosts proxy1 proxy2 --filter '$www.*'", ), ], ) def move_proxy_hosts( ctx: typer.Context, proxy_src: str = typer.Argument( help="Proxy to move hosts from.", show_default=False, ), proxy_dst: str = typer.Argument( help="Proxy to move hosts to.", show_default=False, ), # Prefer --filter over positional arg host_filter: Optional[str] = typer.Option( None, "--filter", help="Regex pattern of hosts to move." ), # LEGACY: matches old command signature (deprecated) host_filter_arg: Optional[str] = typer.Argument( None, help="Filter hosts to move.", hidden=True ), ) -> None: """Move hosts from one proxy to another.""" from zabbix_cli.commands.results.proxy import MoveProxyHostsResult from zabbix_cli.models import Result hfilter = host_filter_arg or host_filter if hfilter: # Compile before we fetch to fail fast filter_pattern = compile_pattern(hfilter) else: filter_pattern = None source_proxy = app.state.client.get_proxy(proxy_src) destination_proxy = app.state.client.get_proxy(proxy_dst) hosts = app.state.client.get_hosts(proxy=source_proxy) if not hosts: exit_err(f"Source proxy {source_proxy.name!r} has no hosts.") # Do filtering client-side to get full regex support if filter_pattern: hosts = [host for host in hosts if filter_pattern.match(host.host)] if not hosts: exit_err(f"No hosts matched filter {hfilter!r}.") app.state.client.move_hosts_to_proxy(hosts, destination_proxy) render_result( Result( message=f"Moved {len(hosts)} hosts from {source_proxy.name!r} to {destination_proxy.name!r}", result=MoveProxyHostsResult( source=source_proxy.proxyid, destination=destination_proxy.proxyid, hosts=[host.host for host in hosts], ), ) ) @app.command( name="load_balance_proxy_hosts", rich_help_panel=HELP_PANEL, examples=[ Example( "Load balance hosts evenly between two proxies", "load_balance_proxy_hosts proxy1,proxy2", ), Example( "Place twice as many hosts on proxy1 as proxy2", "load_balance_proxy_hosts proxy1,proxy2 2,1", ), Example( "Load balance hosts evenly between three proxies", "load_balance_proxy_hosts proxy1,proxy2,proxy3", ), Example( "Load balance hosts unevenly between three proxies", "load_balance_proxy_hosts proxy1,proxy2,proxy3 1,1,2", ), ], ) def load_balance_proxy_hosts( ctx: typer.Context, proxy: str = typer.Argument( help="Comma delimited list of proxies to share hosts between.", metavar="", show_default=False, ), weight: Optional[str] = typer.Argument( None, help="Weights for each proxy. Comma-separated. Defaults to equal weights.", metavar="[weight1,weight2,...]", show_default=False, ), ) -> None: """Spread hosts between multiple proxies. Hosts are determined by the hosts monitored by the given proxies Hosts monitored by other proxies or not monitored at all are not affected. Weighting for the load balancing is optional, and defaults to equal weights. Number of proxies must match number of weights if specified. """ import itertools import random from zabbix_cli.commands.results.proxy import LBProxy from zabbix_cli.commands.results.proxy import LBProxyResult proxy_names = [p.strip() for p in proxy.split(",")] if weight: weights = parse_int_list_arg(weight) else: weights = [1] * len(proxy_names) # default to equal weights if len(proxy_names) != len(weights): exit_err("Number of proxies must match number of weights.") elif len(proxy_names) < 2: exit_err("Must specify at least two proxies to load balance.") elif all(w == 0 for w in weights): exit_err("All weights cannot be zero.") # Fetch proxies one by one to ensure each one exists proxies = [app.state.client.get_proxy(p, select_hosts=True) for p in proxy_names] # TODO: Make sure list of proxies is in same order we specified them? all_hosts = list(itertools.chain.from_iterable(p.hosts for p in proxies)) if not all_hosts: exit_err("Proxies have no hosts to load balance.") logging.debug(f"Found {len(all_hosts)} hosts to load balance.") lb_proxies = { p.proxyid: LBProxy(proxy=p, weight=w) for p, w in zip(proxies, weights) } for host in all_hosts: p = random.choices(proxies, weights=weights, k=1)[0] lb_proxies[p.proxyid].hosts.append(host) # Abort on failure try: for lb_proxy in lb_proxies.values(): n_hosts = len(lb_proxy.hosts) logging.debug( "Proxy '%s' has %d hosts after balancing.", lb_proxy.proxy.name, n_hosts, ) if not n_hosts: logging.debug( "Proxy '%s' has no hosts after balancing.", lb_proxy.proxy.name ) continue logging.debug(f"Moving {n_hosts} hosts to proxy {lb_proxy.proxy.name!r}") app.state.client.move_hosts_to_proxy( hosts=lb_proxy.hosts, proxy=lb_proxy.proxy, ) lb_proxy.count = n_hosts except Exception as e: raise ZabbixCLIError(f"Failed to load balance hosts: {e}") from e render_result(LBProxyResult(proxies=list(lb_proxies.values()))) # HACK: render_result doesn't print a message for table results success(f"Load balanced {len(all_hosts)} hosts between {len(proxies)} proxies.") @app.command(name="update_hostgroup_proxy", rich_help_panel=HELP_PANEL) def update_hostgroup_proxy( ctx: typer.Context, hostgroup: str = typer.Argument( help="Host group(s). Comma-separated. Supports wildcards.", show_default=False, ), proxy: str = typer.Argument( help="Proxy to assign. Supports wildcards.", show_default=False, ), dryrun: bool = typer.Option(False, help="Preview changes.", is_flag=True), ) -> None: """Assign a proxy to all hosts in one or more host groups.""" from zabbix_cli.commands.results.proxy import UpdateHostGroupProxyResult from zabbix_cli.output.console import warning prx = app.state.client.get_proxy(proxy) hosts = get_hostgroup_hosts(app, hostgroup) to_update: List[Host] = [] for host in hosts: if host.proxyid != prx.proxyid: to_update.append(host) if not dryrun: if not to_update: exit_err("All hosts already have the specified proxy.") with app.status("Updating host proxies..."): hostids = app.state.client.update_hosts_proxy(to_update, prx) if not hostids: warning("No hosts were updated.") else: hostids = [host.hostid for host in to_update] updated_hosts = [host for host in to_update if host.hostid in hostids] render_result(UpdateHostGroupProxyResult.from_result(prx, updated_hosts)) total_hosts = len(hostids) if dryrun: info(f"Would update proxy for {total_hosts} hosts.") else: success(f"Updated proxy for {total_hosts} hosts.") @app.command(name="show_proxies", rich_help_panel=HELP_PANEL) def show_proxies( ctx: typer.Context, name_or_id: Optional[str] = typer.Argument( None, help="Filter by proxy name or ID. Comma-separated. Supports wildcards.", show_default=False, ), hosts: bool = typer.Option( False, "--hosts", help="Show hostnames of each host for every proxy.", is_flag=True, ), ) -> None: """Show all proxies. Shows number of hosts for each proxy unless [option]--hosts[/] is passed in, in which case the hostnames of each host are displayed instead. """ from zabbix_cli.commands.results.proxy import ShowProxiesResult from zabbix_cli.models import AggregateResult names_or_ids = parse_list_arg(name_or_id) with app.status("Fetching proxies..."): proxies = app.state.client.get_proxies( *names_or_ids, select_hosts=True, ) render_result( AggregateResult( result=[ShowProxiesResult.from_result(p, show_hosts=hosts) for p in proxies] ) ) @app.command(name="show_proxy_hosts", rich_help_panel=HELP_PANEL) def show_proxy_hosts( ctx: typer.Context, proxy: str = typer.Argument( help="Proxy name or ID. Supports wildcards.", show_default=False, ), ) -> None: """Show all hosts with for a given proxy.""" from zabbix_cli.commands.results.proxy import ShowProxyHostsResult with app.status("Fetching proxy..."): prox = app.state.client.get_proxy(proxy, select_hosts=True) render_result(ShowProxyHostsResult.from_result(prox)) @app.command( name="show_proxy_groups", rich_help_panel=HELP_PANEL, examples=[ Example( "Show all proxy groups", "show_proxy_groups", ), Example( "Show proxy groups with a specific proxy", "show_proxy_groups --proxy proxy1", ), Example( "Show proxy groups with either proxy1 or proxy2", "show_proxy_groups --proxy proxy1,proxy2", ), ], ) def show_proxy_groups( ctx: typer.Context, name_or_id: Optional[str] = typer.Argument( None, help="Filter by proxy name or ID. Comma-separated. Supports wildcards.", show_default=False, ), proxies: Optional[str] = typer.Option( None, "--proxy", help="Show only groups containing these proxies. Comma-separated.", ), ) -> None: """Show all proxy groups and their proxies. Optionally takes in a list of names or IDs to filter by. """ from zabbix_cli.models import AggregateResult ensure_proxy_group_support() names_or_ids = parse_list_arg(name_or_id) proxy_names = parse_list_arg(proxies) with app.status("Fetching proxy groups..."): # Fetch proxies if specified proxy_list = [app.state.client.get_proxy(p) for p in proxy_names] groups = app.state.client.get_proxy_groups( *names_or_ids, proxies=proxy_list, select_proxies=True, ) render_result(AggregateResult(result=groups)) @app.command( name="add_proxy_to_group", rich_help_panel=HELP_PANEL, examples=[ Example( "Add a proxy to a proxy group", "add_proxy_to_group proxy1 group1 192.168.0.2 10051", ) ], ) def add_proxy_to_group( ctx: typer.Context, name_or_id: str = typer.Argument( help="Name or ID of proxy to add.", show_default=False, ), proxy_group: str = typer.Argument( help="Name or ID of proxy group to add proxy to.", show_default=False, ), local_address: Optional[str] = typer.Argument( None, help="Address for active agents.", show_default=False, ), local_port: Optional[str] = typer.Argument( None, help="Address for active agents.", show_default=False, ), ) -> None: """Add a proxy to a proxy group. Requires a local address and port for active agent redirection if if the proxy does not have it set. """ ensure_proxy_group_support() proxy = app.state.client.get_proxy(name_or_id) # Determine address + port local_address = local_address or proxy.local_address if not local_address: exit_err(f"Proxy {proxy.name} requires a local address for active agents.") local_port = local_port or proxy.local_port if not local_port: exit_err(f"Proxy {proxy.name} requires a local port for active agents.") group = app.state.client.get_proxy_group(proxy_group) app.state.client.add_proxy_to_group(proxy, group, local_address, local_port) success(f"Added proxy {proxy.name!r} to group {group.name!r}.") @app.command(name="remove_proxy_from_group", rich_help_panel=HELP_PANEL) def remove_proxy_from_group( ctx: typer.Context, name_or_id: str = typer.Argument( help="Name or ID of proxy to remove.", show_default=False, ), ) -> None: """Remove a proxy from a proxy group.""" ensure_proxy_group_support() proxy = app.state.client.get_proxy(name_or_id) if proxy.proxy_groupid is None or proxy.proxy_groupid == "0": exit_err(f"Proxy {proxy.name!r} is not in a proxy group.") app.state.client.remove_proxy_from_group(proxy) success(f"Removed proxy {proxy.name!r} from group with ID {proxy.proxy_groupid}.") @app.command(name="update_hostgroup_proxygroup", rich_help_panel=HELP_PANEL) def update_hostgroup_proxygroup( ctx: typer.Context, hostgroup: str = typer.Argument( help="Host group(s). Comma-separated. Supports wildcards.", show_default=False, ), proxygroup: str = typer.Argument( help="Proxy group to assign. Supports wildcards.", show_default=False, ), dryrun: bool = typer.Option(False, help="Preview changes.", is_flag=True), ) -> None: """Assign a proxy group to all hosts in one or more host groups.""" from zabbix_cli.commands.results.proxy import UpdateHostGroupProxyGroupResult from zabbix_cli.output.console import warning ensure_proxy_group_support() grp = app.state.client.get_proxy_group(proxygroup) hosts = get_hostgroup_hosts(app, hostgroup) to_update: List[Host] = [] for host in hosts: if host.proxy_groupid != grp.proxy_groupid: to_update.append(host) # Sort hosts by host group updated: List[str] = [] # list of host IDs if not dryrun: if not to_update: exit_err("All hosts already have the specified proxy group.") with app.status("Updating hosts..."): updated = app.state.client.add_hosts_to_proxygroup(to_update, grp) if not updated: warning("No hosts were updated.") else: updated = [host.hostid for host in to_update] updated_hosts = [host for host in to_update if host.hostid in updated] render_result(UpdateHostGroupProxyGroupResult.from_result(grp, updated_hosts)) total_hosts = len(updated) if dryrun: info(f"Would update proxy group for {total_hosts} hosts.") else: success(f"Updated proxy group for {total_hosts} hosts.") @app.command(name="show_proxy_group_hosts", rich_help_panel=HELP_PANEL) def show_proxy_group_hosts( ctx: typer.Context, proxygroup: str = typer.Argument( help="Proxy group name or ID. Supports wildcards.", show_default=False, ), ) -> None: """Show all hosts in a proxy group.""" from zabbix_cli.commands.results.proxy import ShowProxyGroupHostsResult ensure_proxy_group_support() with app.status("Fetching proxy groups...") as status: group = app.state.client.get_proxy_group(proxygroup) status.update("Fetching hosts...") hosts = app.state.client.get_hosts(proxy_group=group) render_result(ShowProxyGroupHostsResult(proxy_group=group, hosts=hosts)) unioslo-zabbix-cli-09a2fab/zabbix_cli/commands/results/000077500000000000000000000000001471265333400233325ustar00rootroot00000000000000unioslo-zabbix-cli-09a2fab/zabbix_cli/commands/results/__init__.py000066400000000000000000000007021471265333400254420ustar00rootroot00000000000000"""Models for rendering results of commands. Should not be imported on startup, as we don't want to build Pydantic models until we actually need them - this has a massive startup time impact. Each command module should have a corresponding module in this package that defines the models for its results. i.e. `zabbix_cli.commands.host` should define its result models in `zabbix_cli.commands.results.host`. """ from __future__ import annotations unioslo-zabbix-cli-09a2fab/zabbix_cli/commands/results/cli.py000066400000000000000000000111101471265333400244450ustar00rootroot00000000000000from __future__ import annotations import sys from pathlib import Path from typing import TYPE_CHECKING from typing import Any from typing import Dict from typing import List from typing import Optional from typing import Tuple from packaging.version import Version from pydantic import ConfigDict from pydantic import FieldSerializationInfo from pydantic import field_serializer from rich.box import SIMPLE_HEAD from rich.table import Table from typing_extensions import Self from typing_extensions import TypedDict from zabbix_cli.models import TableRenderable from zabbix_cli.output.console import error from zabbix_cli.output.formatting.path import path_link if TYPE_CHECKING: from zabbix_cli.commands.cli import DirectoryType from zabbix_cli.models import ColsRowsType from zabbix_cli.models import RowsType from zabbix_cli.state import State class ImplementationInfo(TypedDict): name: str version: Tuple[Any, ...] hexversion: int cache_tag: str class PythonInfo(TypedDict): version: str implementation: ImplementationInfo platform: str class DebugInfo(TableRenderable): config_path: Optional[Path] = None api_version: Optional[Version] = None url: Optional[str] = None user: Optional[str] = None auth_token: Optional[str] = None connected_to_zabbix: bool = False python: PythonInfo model_config = ConfigDict(arbitrary_types_allowed=True) @field_serializer("api_version") def ser_api_version(self, _info: FieldSerializationInfo) -> str: return str(self.api_version) @property def config_path_str(self) -> str: return ( path_link(self.config_path) if self.config_path else str(self.config_path) ) @classmethod def from_debug_data(cls, state: State, with_auth: bool = False) -> DebugInfo: # So far we only use state, but we can expand this in the future from zabbix_cli.exceptions import ZabbixCLIError obj = cls( python={ "version": sys.version, "implementation": { "name": sys.implementation.name, "version": sys.implementation.version, "hexversion": sys.implementation.hexversion, "cache_tag": sys.implementation.cache_tag, }, "platform": sys.platform, } ) # Config might not be configured try: obj.config_path = state.config.config_path except ZabbixCLIError: pass # We might not be connected to the API try: obj.api_version = state.client.version obj.url = state.client.url obj.user = state.config.api.username if with_auth: obj.auth_token = state.client.auth obj.connected_to_zabbix = True except ZabbixCLIError: error("Unable to retrieve API info: Not connected to Zabbix API. ") except Exception as e: error(f"Unable to retrieve API info: {e}") return obj def as_table(self) -> Table: table = Table(title="Debug Info") table.add_column("Key", justify="right", style="cyan") table.add_column("Value", justify="left", style="magenta") table.add_row("Config File", self.config_path_str) table.add_row("API URL", str(self.url)) table.add_row("API Version", str(self.api_version)) table.add_row("User", str(self.user)) table.add_row("Auth Token", str(self.auth_token)) table.add_row("Connected to Zabbix", str(self.connected_to_zabbix)) table.add_row("Python Version", str(self.python["version"])) table.add_row("Platform", str(self.python["platform"])) return table class HistoryResult(TableRenderable): """Result type for `show_history` command.""" __show_lines__ = False __box__ = SIMPLE_HEAD commands: List[str] = [] class DirectoriesResult(TableRenderable): """Result type for `show_dirs` command.""" directories: List[Dict[str, Path]] = [] @classmethod def from_directory_types(cls, dirs: List[DirectoryType]) -> Self: return cls(directories=[{str(d.value): d.as_path()} for d in dirs]) def __cols_rows__(self) -> ColsRowsType: from zabbix_cli.output.style import Emoji cols = ["Type", "Path", "Exists"] rows: RowsType = [] for d in self.directories: for key in d: p = d[key] exists = Emoji.fmt_bool(p.exists()) rows.append([key, str(d[key]), exists]) return cols, rows unioslo-zabbix-cli-09a2fab/zabbix_cli/commands/results/export.py000066400000000000000000000031721471265333400252300ustar00rootroot00000000000000from __future__ import annotations from pathlib import Path from typing import TYPE_CHECKING from typing import List from typing import Optional from pydantic import field_serializer from zabbix_cli.commands.export import ExportType from zabbix_cli.models import TableRenderable from zabbix_cli.output.formatting.path import path_link from zabbix_cli.pyzabbix.enums import ExportFormat if TYPE_CHECKING: from zabbix_cli.models import ColsRowsType from zabbix_cli.models import RowsType class ExportResult(TableRenderable): """Result type for `export_configuration` command.""" exported: List[Path] = [] """List of paths to exported files.""" types: List[ExportType] = [] names: List[str] = [] format: ExportFormat class ImportResult(TableRenderable): """Result type for `import_configuration` command.""" success: bool = True dryrun: bool = False imported: List[Path] = [] failed: List[Path] = [] duration: Optional[float] = None """Duration it took to import files in seconds. Is None if import failed.""" @field_serializer("imported", "failed", when_used="json") def _serialize_files(self, files: List[Path]) -> List[str]: """Serializes files as list of normalized, absolute paths with symlinks resolved.""" return [str(f.resolve()) for f in files] def __cols_rows__(self) -> ColsRowsType: cols: List[str] = ["Imported", "Failed"] rows: RowsType = [ [ "\n".join(path_link(f) for f in self.imported), "\n".join(path_link(f) for f in self.failed), ] ] return cols, rows unioslo-zabbix-cli-09a2fab/zabbix_cli/commands/results/host.py000066400000000000000000000042031471265333400246600ustar00rootroot00000000000000from __future__ import annotations from typing import Optional from pydantic import BaseModel from pydantic import ConfigDict from zabbix_cli.exceptions import ZabbixCLIError from zabbix_cli.pyzabbix.enums import ActiveInterface from zabbix_cli.pyzabbix.enums import MaintenanceStatus from zabbix_cli.pyzabbix.enums import MonitoringStatus # TODO: don't use BaseModel for this # Use a normal class with __init__ instead class HostFilterArgs(BaseModel): """Unified processing of old filter string and new filter options.""" active: Optional[ActiveInterface] = None maintenance_status: Optional[MaintenanceStatus] = None status: Optional[MonitoringStatus] = None model_config = ConfigDict(validate_assignment=True) @classmethod def from_command_args( cls, filter_legacy: Optional[str], active: Optional[ActiveInterface], maintenance: Optional[bool], monitored: Optional[bool], ) -> HostFilterArgs: args = cls() if filter_legacy: items = filter_legacy.split(",") for item in items: try: key, value = (s.strip("'\"") for s in item.split(":")) except ValueError as e: raise ZabbixCLIError( f"Failed to parse filter argument at: {item!r}" ) from e if key == "available": args.active = ActiveInterface(value) elif key == "maintenance": args.maintenance_status = MaintenanceStatus(value) elif key == "status": args.status = MonitoringStatus(value) else: if active is not None: args.active = active if monitored is not None: # Inverted API values (0 = ON, 1 = OFF) - use enums directly args.status = MonitoringStatus.ON if monitored else MonitoringStatus.OFF if maintenance is not None: args.maintenance_status = ( MaintenanceStatus.ON if maintenance else MaintenanceStatus.OFF ) return args unioslo-zabbix-cli-09a2fab/zabbix_cli/commands/results/hostgroup.py000066400000000000000000000114721471265333400257430ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from typing import List from typing import Optional from typing import Set from pydantic import computed_field from typing_extensions import TypedDict from zabbix_cli.models import TableRenderable from zabbix_cli.pyzabbix.enums import HostgroupFlag from zabbix_cli.pyzabbix.enums import HostgroupType from zabbix_cli.pyzabbix.types import Host from zabbix_cli.pyzabbix.types import HostGroup from zabbix_cli.pyzabbix.types import HostList if TYPE_CHECKING: from zabbix_cli.models import ColsRowsType from zabbix_cli.models import RowsType class AddHostsToHostGroup(TableRenderable): """Result type for `add_host_to_hostgroup` and `remove_host_from_hostgroup` commands.""" hostgroup: str hosts: List[str] @classmethod def from_result( cls, hosts: List[Host], hostgroup: HostGroup, ) -> AddHostsToHostGroup: to_add: Set[str] = set() # names of templates to link for host in hosts: for hg_host in hostgroup.hosts: if host.host == hg_host.host: break else: to_add.add(host.host) return cls( hostgroup=hostgroup.name, hosts=sorted(to_add), ) class RemoveHostsFromHostGroup(TableRenderable): """Result type for `remove_host_from_hostgroup`.""" hostgroup: str hosts: List[str] @classmethod def from_result( cls, hosts: List[Host], hostgroup: HostGroup, ) -> RemoveHostsFromHostGroup: to_remove: Set[str] = set() # names of templates to link for host in hosts: for hg_host in hostgroup.hosts: if host.host == hg_host.host: to_remove.add(host.host) break return cls( hostgroup=hostgroup.name, hosts=sorted(to_remove), ) class ExtendHostgroupResult(TableRenderable): """Result type for `extend_hostgroup` command.""" source: str destination: List[str] hosts: List[str] @classmethod def from_result( cls, source: HostGroup, destination: List[HostGroup] ) -> ExtendHostgroupResult: return cls( source=source.name, destination=[dst.name for dst in destination], hosts=[host.host for host in source.hosts], ) class MoveHostsResult(TableRenderable): """Result type for `move_hosts` command.""" source: str destination: str hosts: List[str] @classmethod def from_result(cls, source: HostGroup, destination: HostGroup) -> MoveHostsResult: return cls( source=source.name, destination=destination.name, hosts=[host.host for host in source.hosts], ) class HostGroupDeleteResult(TableRenderable): groups: List[str] class HostGroupHost(TypedDict): hostid: str host: str class HostGroupResult(TableRenderable): """Result type for hostgroup.""" groupid: str name: str hosts: HostList = [] flags: int internal: Optional[int] = None # <6.2 @classmethod def from_hostgroup(cls, hostgroup: HostGroup) -> HostGroupResult: return cls( groupid=hostgroup.groupid, name=hostgroup.name, flags=hostgroup.flags, internal=hostgroup.internal, # <6.2 hosts=hostgroup.hosts, ) # LEGACY # Mimicks old behavior by also writing the string representation of the # flags and internal fields to the serialized output. @computed_field @property def flags_str(self) -> str: return HostgroupFlag.string_from_value(self.flags, with_code=False) # VERSION: 6.0 # Internal groups are not a thing in Zabbix >=6.2 @computed_field @property def type(self) -> str: return HostgroupType.string_from_value(self.internal, with_code=True) def __cols_rows__(self) -> ColsRowsType: cols = ["ID", "Name", "Flag", "Hosts"] rows: RowsType = [ [ self.groupid, self.name, self.flags_str, ", ".join(host.host for host in self.hosts), ] ] # VERSION: 6.0 if self.zabbix_version.release < (6, 2): cols.insert(3, "Type") t = HostgroupType.string_from_value(self.internal, with_code=False) rows[0].insert(3, t) # without code in table return cols, rows class HostGroupPermissions(TableRenderable): """Result type for hostgroup permissions.""" groupid: str name: str permissions: List[str] def __cols_rows__(self) -> ColsRowsType: cols = ["GroupID", "Name", "Permissions"] rows: RowsType = [[self.groupid, self.name, "\n".join(self.permissions)]] return cols, rows unioslo-zabbix-cli-09a2fab/zabbix_cli/commands/results/item.py000066400000000000000000000072561471265333400246540ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from typing import Dict from typing import List from typing import Optional from pydantic import Field from pydantic import computed_field from zabbix_cli.models import TableRenderable from zabbix_cli.pyzabbix.types import Item if TYPE_CHECKING: from zabbix_cli.models import ColsRowsType from zabbix_cli.models import RowsType class UngroupedItem(TableRenderable): itemid: str name: Optional[str] key: Optional[str] lastvalue: Optional[str] host: Optional[str] @classmethod def from_item(cls, item: Item) -> UngroupedItem: return cls( itemid=item.itemid, name=item.name, key=item.key, lastvalue=item.lastvalue, host=item.hosts[0].host if item.hosts else None, ) def __cols_rows__(self) -> ColsRowsType: cols = ["Item ID", "Name", "Key", "Last value", "Host"] rows: RowsType = [ [ self.itemid, self.name or "", self.key or "", self.lastvalue or "", self.host or "", ] ] return cols, rows class ItemResult(Item): """Alternate rendering of Item.""" grouped: bool = Field(False, exclude=True) @classmethod def from_item(cls, item: Item) -> ItemResult: return cls.model_validate(item, from_attributes=True) @computed_field @property def host(self) -> str: """LEGACY: serialize list of hosts as newline-delimited string.""" return "\n".join(h.host for h in self.hosts) def __cols_rows__(self) -> ColsRowsType: # As long as we include the "host" computed field, we need to # override the __cols_rows__ method. cols = ["Name", "Key", "Last value", "Hosts"] rows: RowsType = [ [ self.name or "", self.key or "", self.lastvalue or "", "\n".join(h.host for h in self.hosts), ], ] if self.grouped: cols.insert(0, "Item ID") rows[0].insert(0, self.itemid) return cols, rows def group_items(items: List[Item]) -> List[ItemResult]: """Group items by key+lastvalue. Keeps first item for each key+lastvalue pair, and adds hosts from duplicate items to the first item. Example: ```py # Given the following items: >>> items = [ Item(itemid="1", key="foo", lastvalue="bar", hosts=[Host(hostid="1")]), Item(itemid="2", key="foo", lastvalue="bar", hosts=[Host(hostid="2")]), Item(itemid="3", key="baz", lastvalue="baz", hosts=[Host(hostid="3")]), Item(itemid="4", key="baz", lastvalue="baz", hosts=[Host(hostid="4")]), ] >>> group_items(items) [ Item(itemid="1", key="foo", lastvalue="bar", hosts=[Host(hostid="1"), Host(hostid="2")]), Item(itemid="3", key="baz", lastvalue="baz", hosts=[Host(hostid="3"), Host(hostid="4")]), ] # Hosts from items 2 and 4 were added to item 1 and 3, respectively. ``` """ from zabbix_cli.commands.results.item import ItemResult item_map: Dict[str, ItemResult] = {} for item in items: if not item.name or not item.lastvalue or not item.key or not item.hosts: continue key = item.key + item.lastvalue for host in item.hosts: if key in item_map: item_map[key].hosts.append(host) else: res = ItemResult.from_item(item) res.grouped = True item_map[key] = res return list(item_map.values()) unioslo-zabbix-cli-09a2fab/zabbix_cli/commands/results/macro.py000066400000000000000000000061161471265333400250110ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from typing import Any from typing import Dict from typing import List from typing import Optional from pydantic import Field from pydantic import model_serializer from typing_extensions import Self from zabbix_cli.models import TableRenderable from zabbix_cli.pyzabbix.enums import MacroAutomatic from zabbix_cli.pyzabbix.types import Macro if TYPE_CHECKING: from zabbix_cli.models import ColsRowsType from zabbix_cli.models import RowsType class ShowHostUserMacrosResult(TableRenderable): hostmacroid: str macro: str value: Optional[str] = None type: str description: Optional[str] = None hostid: str automatic: Optional[int] = None @classmethod def from_result(cls, macro: Macro) -> Self: return cls( hostmacroid=macro.hostmacroid, macro=macro.macro, value=macro.value, type=macro.type_str, description=macro.description, hostid=macro.hostid, automatic=macro.automatic, ) def __cols_rows__(self) -> ColsRowsType: cols = [ "Macro ID", "Macro", "Value", "Type", "Description", "Host ID", "Automatic", ] rows: RowsType = [ [ self.hostmacroid, self.macro, str(self.value), self.type, self.description or "", self.hostid, MacroAutomatic.string_from_value(self.automatic), ] ] return cols, rows class MacroHostListV2(TableRenderable): macro: Macro def __cols_rows__(self) -> ColsRowsType: rows: RowsType = [ [self.macro.macro, str(self.macro.value), host.hostid, host.host] for host in self.macro.hosts ] return ["Macro", "Value", "HostID", "Host"], rows @model_serializer() def model_ser(self) -> Dict[str, Any]: if not self.macro.hosts: return {} # match V2 output return { "macro": self.macro.macro, "value": self.macro.value, "hostid": self.macro.hosts[0].hostid, "host": self.macro.hosts[0].host, } class MacroHostListV3(TableRenderable): macro: Macro def __cols_rows__(self) -> ColsRowsType: rows: RowsType = [ [host.hostid, host.host, self.macro.macro, str(self.macro.value)] for host in self.macro.hosts ] return ["Host ID", "Host", "Macro", "Value"], rows class GlobalMacroResult(TableRenderable): """Result of `define_global_macro` command.""" globalmacroid: str = Field(json_schema_extra={"header": "Global Macro ID"}) macro: str value: Optional[str] = None # for usermacro.get calls class ShowUsermacroTemplateListResult(TableRenderable): macro: str value: Optional[str] = None templateid: str template: str def __cols__(self) -> List[str]: return ["Macro", "Value", "Template ID", "Template"] unioslo-zabbix-cli-09a2fab/zabbix_cli/commands/results/maintenance.py000066400000000000000000000055461471265333400262000ustar00rootroot00000000000000from __future__ import annotations from datetime import datetime from typing import Any from typing import List from typing import Optional from pydantic import Field from pydantic import computed_field from pydantic import field_validator from typing_extensions import Literal from zabbix_cli.models import ColsRowsType from zabbix_cli.models import TableRenderable from zabbix_cli.pyzabbix.enums import MaintenanceType from zabbix_cli.pyzabbix.types import TimePeriod class CreateMaintenanceDefinitionResult(TableRenderable): """Result type for `create_maintenance_definition` command.""" maintenance_id: str class ShowMaintenancePeriodsResult(TableRenderable): maintenanceid: str = Field(title="Maintenance ID") name: str timeperiods: List[TimePeriod] hosts: List[str] groups: List[str] class ShowMaintenanceDefinitionsResult(TableRenderable): """Result type for `show_maintenance_definitions` command.""" maintenanceid: str name: str type: Optional[int] active_till: datetime description: Optional[str] hosts: List[str] groups: List[str] @computed_field @property def state(self) -> Literal["Active", "Expired"]: now_time = datetime.now(tz=self.active_till.tzinfo) if self.active_till > now_time: return "Active" return "Expired" @computed_field @property def maintenance_type(self) -> str: return MaintenanceType.string_from_value(self.type) @field_validator("active_till", mode="before") @classmethod def validate_active_till(cls, v: Any) -> datetime: if v is None: return datetime.now() return v @property def state_str(self) -> str: if self.state == "Active": color = "green" else: color = "red" return f"[{color}]{self.state}[/]" @property def maintenance_type_str(self) -> str: # FIXME: This is very brittle! We are beholden to self.maintenance_type... if "With DC" in self.maintenance_type: color = "green" else: color = "red" return f"[{color}]{self.maintenance_type}[/]" def __cols_rows__(self) -> ColsRowsType: return ( [ "ID", "Name", "Type", "Active till", "Hosts", "Host groups", "State", "Description", ], [ [ self.maintenanceid, self.name, self.maintenance_type_str, self.active_till.strftime("%Y-%m-%d %H:%M"), ", ".join(self.hosts), ", ".join(self.groups), self.state_str, self.description or "", ] ], ) unioslo-zabbix-cli-09a2fab/zabbix_cli/commands/results/problem.py000066400000000000000000000007601471265333400253470ustar00rootroot00000000000000from __future__ import annotations from typing import List from typing import Optional from zabbix_cli.models import TableRenderable class AcknowledgeEventResult(TableRenderable): """Result type for `acknowledge_event` command.""" event_ids: List[str] = [] close: bool = False message: Optional[str] = None class AcknowledgeTriggerLastEventResult(AcknowledgeEventResult): """Result type for `acknowledge_trigger_last_event` command.""" trigger_ids: List[str] = [] unioslo-zabbix-cli-09a2fab/zabbix_cli/commands/results/proxy.py000066400000000000000000000132421471265333400250670ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from typing import List from typing import Optional from pydantic import BaseModel from pydantic import Field from pydantic import model_serializer from typing_extensions import Self from zabbix_cli.models import ColsRowsType from zabbix_cli.models import MetaKey from zabbix_cli.models import TableRenderable from zabbix_cli.pyzabbix.types import Host from zabbix_cli.pyzabbix.types import HostList from zabbix_cli.pyzabbix.types import Proxy from zabbix_cli.pyzabbix.types import ProxyGroup if TYPE_CHECKING: from zabbix_cli.models import ColsRowsType from zabbix_cli.models import RowsType class BaseHostProxyResult(TableRenderable): source: str = Field(..., json_schema_extra={MetaKey.HEADER: "Source Proxy"}) """Name of the old proxy.""" hosts: List[str] = [] """Name of the hosts that were updated.""" class UpdateHostProxyResult(BaseHostProxyResult): """Result type for `update_host_proxy` command.""" destination: str = Field( ..., json_schema_extra={MetaKey.HEADER: "Destination Proxy"} ) """Name of the new proxy""" @classmethod def from_result( cls, hosts: List[Host], source_proxy: Optional[Proxy], dest_proxy: Optional[Proxy], ) -> Self: return cls( source=source_proxy.name if source_proxy else "", destination=dest_proxy.name if dest_proxy else "", hosts=[h.host for h in hosts], ) class ClearHostProxyResult(BaseHostProxyResult): """Result type for `update_host_proxy` command.""" @classmethod def from_result( cls, hosts: List[Host], source_proxy: Optional[Proxy], ) -> Self: return cls( source=source_proxy.name if source_proxy else "", hosts=[h.host for h in hosts], ) class MoveProxyHostsResult(TableRenderable): """Result type for `move_proxy_hosts` command.""" source: Optional[str] = None """ID of the source (old) proxy.""" destination: Optional[str] = None """ID of the destination (new) proxy.""" hosts: List[str] = [] class LBProxy(BaseModel): """A load balanced proxy.""" proxy: Proxy hosts: List[Host] = [] weight: int count: int = 0 @model_serializer def ser_model(self): return { "name": self.proxy.name, "proxyid": self.proxy.proxyid, "weight": self.weight, "count": self.count, "hosts": [h.host for h in self.hosts], } class LBProxyResult(TableRenderable): """Result type for `load_balance_proxy_hosts` command.""" proxies: List[LBProxy] def __cols_rows__(self) -> ColsRowsType: cols = ["Proxy", "Weight", "Hosts"] rows: RowsType = [] for proxy in self.proxies: rows.append([proxy.proxy.name, str(proxy.weight), str(len(proxy.hosts))]) return cols, rows class UpdateHostGroupProxyResult(TableRenderable): """Result type for `update_hostgroup_proxy` command.""" proxy: str hosts: List[str] = [] @classmethod def from_result(cls, proxy: Proxy, hosts: List[Host]) -> Self: return cls(proxy=proxy.name, hosts=[host.host for host in hosts]) class UpdateHostGroupProxyGroupResult(TableRenderable): """Result type for `update_hostgroup_proxygroup` command.""" proxy_group: str hosts: List[str] = [] @classmethod def from_result(cls, proxy_group: ProxyGroup, hosts: List[Host]) -> Self: return cls(proxy_group=proxy_group.name, hosts=[host.host for host in hosts]) class ShowProxiesResult(TableRenderable): """Result type for `show_proxy` command.""" proxy: Proxy show_hosts: bool = Field(default=False, exclude=True) @classmethod def from_result(cls, proxy: Proxy, show_hosts: bool = False) -> Self: return cls(proxy=proxy, show_hosts=show_hosts) @property def hosts_fmt(self) -> str: if self.show_hosts: return ", ".join(f"{host.host}" for host in self.proxy.hosts) else: return str(len(self.proxy.hosts)) def __cols_rows__(self) -> ColsRowsType: cols = [ "Name", "Address", "Mode", "Hosts", ] rows: RowsType = [ [ self.proxy.name, str(self.proxy.address), self.proxy.mode, self.hosts_fmt, ] ] if self.zabbix_version.release >= (7, 0, 0): cols.extend(["Version", "Compatibility"]) rows[0].extend([str(self.proxy.version), self.proxy.compatibility_rich]) return cols, rows class ShowProxyHostsResult(TableRenderable): proxy: Proxy hosts: HostList = [] @classmethod def from_result(cls, proxy: Proxy) -> Self: # HACK: remove the list of hosts from the proxy and store it separately # so that we can render the proxy separately from the hosts hosts = proxy.hosts proxy.hosts = [] return cls(proxy=proxy, hosts=hosts) def __cols_rows__(self) -> ColsRowsType: cols = ["Proxy", "Hosts"] rows: RowsType = [ [ self.proxy.name, "\n".join(h.host for h in self.hosts), ] ] return cols, rows class ShowProxyGroupHostsResult(TableRenderable): proxy_group: ProxyGroup hosts: HostList = [] def __cols_rows__(self) -> ColsRowsType: cols = ["Proxy Group", "Hosts"] rows: RowsType = [ [ self.proxy_group.name, "\n".join(h.host for h in self.hosts), ] ] return cols, rows unioslo-zabbix-cli-09a2fab/zabbix_cli/commands/results/template.py000066400000000000000000000062271471265333400255260ustar00rootroot00000000000000from __future__ import annotations from typing import List from typing import Literal from typing import Set from typing import Union from zabbix_cli.models import TableRenderable from zabbix_cli.pyzabbix.types import Host from zabbix_cli.pyzabbix.types import HostGroup from zabbix_cli.pyzabbix.types import Template from zabbix_cli.pyzabbix.types import TemplateGroup class LinkTemplateToHostResult(TableRenderable): host: str templates: List[str] action: str @classmethod def from_result( cls, templates: List[Template], host: Host, action: str, ) -> LinkTemplateToHostResult: to_link: Set[str] = set() # names of templates to link for t in templates: for h in t.hosts: if h.host == host.host: break else: to_link.add(t.host) return cls( host=host.host, templates=sorted(to_link), action=action, ) class UnlinkTemplateFromHostResult(TableRenderable): host: str templates: List[str] action: str @classmethod def from_result( cls, templates: List[Template], host: Host, action: str, ) -> UnlinkTemplateFromHostResult: """Only show templates that are actually unlinked.""" to_remove: Set[str] = set() for t in templates: for h in t.hosts: if h.host == host.host: to_remove.add(t.host) # name of template break return cls( host=host.host, templates=list(to_remove), action=action, ) class LinkTemplateResult(TableRenderable): """Result type for (un)linking templates to templates.""" source: List[str] destination: List[str] action: str @classmethod def from_result( cls, source: List[Template], destination: List[Template], action: Literal["Link", "Unlink", "Unlink and clear"], ) -> LinkTemplateResult: return cls( source=[t.host for t in source], destination=[t.host for t in destination], action=action, ) class TemplateGroupResult(TableRenderable): templates: List[str] groups: List[str] @classmethod def from_result( cls, templates: List[Template], groups: Union[List[TemplateGroup], List[HostGroup]], ) -> TemplateGroupResult: return cls( templates=[t.host for t in templates], groups=[h.name for h in groups], ) class RemoveTemplateFromGroupResult(TableRenderable): group: str templates: List[str] @classmethod def from_result( cls, templates: List[Template], group: Union[TemplateGroup, HostGroup], ) -> RemoveTemplateFromGroupResult: to_remove: Set[str] = set() for template in group.templates: for t in templates: if t.host == template.host: to_remove.add(t.host) break return cls( templates=list(to_remove), group=group.name, ) unioslo-zabbix-cli-09a2fab/zabbix_cli/commands/results/templategroup.py000066400000000000000000000061631471265333400266020ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from typing import Any from typing import Dict from typing import List from typing import Union from pydantic import Field from pydantic import computed_field from pydantic import field_serializer from zabbix_cli.models import TableRenderable from zabbix_cli.pyzabbix.types import HostGroup from zabbix_cli.pyzabbix.types import Template from zabbix_cli.pyzabbix.types import TemplateGroup if TYPE_CHECKING: from zabbix_cli.models import ColsRowsType from zabbix_cli.models import RowsType class ShowTemplateGroupResult(TableRenderable): """Result type for templategroup.""" groupid: str = Field(..., json_schema_extra={"header": "Group ID"}) name: str templates: List[Template] = [] show_templates: bool = Field(True, exclude=True) @classmethod def from_result( cls, group: Union[HostGroup, TemplateGroup], show_templates: bool ) -> ShowTemplateGroupResult: return cls( groupid=group.groupid, name=group.name, templates=group.templates, show_templates=show_templates, ) @computed_field @property def template_count(self) -> int: return len(self.templates) @field_serializer("templates") def templates_serializer(self, value: List[Template]) -> List[Dict[str, Any]]: if self.show_templates: return [t.model_dump(mode="json") for t in value] return [] def __rows__(self) -> RowsType: tpls = self.templates if self.show_templates else [] return [ [ self.groupid, self.name, "\n".join(str(t.host) for t in sorted(tpls, key=lambda t: t.host)), str(self.template_count), ] ] class ExtendTemplateGroupResult(TableRenderable): source: str destination: List[str] templates: List[str] @classmethod def from_result( cls, src_group: Union[HostGroup, TemplateGroup], dest_group: Union[List[HostGroup], List[TemplateGroup]], templates: List[Template], ) -> ExtendTemplateGroupResult: return cls( source=src_group.name, destination=[grp.name for grp in dest_group], templates=[t.host for t in templates], ) class MoveTemplatesResult(TableRenderable): """Result type for `move_templates` command.""" source: str destination: str templates: List[str] @classmethod def from_result( cls, source: Union[HostGroup, TemplateGroup], destination: Union[HostGroup, TemplateGroup], ) -> MoveTemplatesResult: return cls( source=source.name, destination=destination.name, templates=[template.host for template in source.templates], ) def __cols_rows__(self) -> ColsRowsType: """Only print the template names in the table. Source and destination are apparent from the surrounding context. """ cols = ["Templates"] rows: RowsType = [["\n".join(self.templates)]] return cols, rows unioslo-zabbix-cli-09a2fab/zabbix_cli/commands/results/user.py000066400000000000000000000165551471265333400246760ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from typing import Any from typing import Dict from typing import List from typing import Mapping from typing import Union import rich import rich.box from pydantic import Field from pydantic import computed_field from pydantic import field_validator from pydantic import model_serializer from zabbix_cli.models import MetaKey from zabbix_cli.models import TableRenderable from zabbix_cli.pyzabbix.enums import GUIAccess from zabbix_cli.pyzabbix.enums import UsergroupPermission from zabbix_cli.pyzabbix.enums import UsergroupStatus from zabbix_cli.pyzabbix.types import HostGroup from zabbix_cli.pyzabbix.types import TemplateGroup from zabbix_cli.pyzabbix.types import Usergroup from zabbix_cli.pyzabbix.types import ZabbixRight if TYPE_CHECKING: from zabbix_cli.models import ColsRowsType from zabbix_cli.models import RowContent from zabbix_cli.models import RowsType class UgroupUpdateUsersResult(TableRenderable): usergroups: List[str] users: List[str] def __cols_rows__(self) -> ColsRowsType: return ( ["Usergroups", "Users"], [["\n".join(self.usergroups), ", ".join(self.users)]], ) class UsergroupAddUsers(UgroupUpdateUsersResult): __title__ = "Added Users" class UsergroupRemoveUsers(UgroupUpdateUsersResult): __title__ = "Removed Users" class GroupRights(TableRenderable): __box__ = rich.box.MINIMAL groups: Union[Dict[str, HostGroup], Dict[str, TemplateGroup]] = Field( default_factory=dict, ) rights: List[ZabbixRight] = Field( default_factory=list, description="Group rights for the user group.", ) def __cols_rows__(self) -> ColsRowsType: cols = ["Name", "Permission"] rows: RowsType = [] for right in self.rights: group = self.groups.get(right.id, None) if group: group_name = group.name else: group_name = "Unknown" rows.append([group_name, str(UsergroupPermission(right.permission))]) return cols, rows class ShowUsergroupResult(TableRenderable): usrgrpid: str = Field(..., json_schema_extra={MetaKey.HEADER: "ID"}) name: str gui_access: str = Field(..., json_schema_extra={MetaKey.HEADER: "GUI Access"}) status: str users: List[str] = Field( default_factory=list, json_schema_extra={MetaKey.JOIN_CHAR: ", "} ) @classmethod def from_usergroup(cls, usergroup: Usergroup) -> ShowUsergroupResult: return cls( name=usergroup.name, usrgrpid=usergroup.usrgrpid, gui_access=usergroup.gui_access_str, status=usergroup.users_status_str, users=[user.username for user in usergroup.users], ) @field_validator("gui_access") @classmethod def _validate_gui_access(cls, v: Any) -> str: if isinstance(v, int): return GUIAccess.string_from_value(v, with_code=False) return v @field_validator("status") @classmethod def _validate_status(cls, v: Any) -> str: if isinstance(v, int): return UsergroupStatus.string_from_value(v, with_code=False) return v class ShowUsergroupPermissionsResult(TableRenderable): usrgrpid: str name: str hostgroups: Dict[str, HostGroup] = Field( default_factory=dict, exclude=True, description="Host groups the user group has access to. Used to render host group rights.", ) templategroups: Dict[str, TemplateGroup] = Field( default_factory=dict, exclude=True, description="Mapping of all template groups. Used to render template group rights.", ) hostgroup_rights: List[ZabbixRight] = [] templategroup_rights: List[ZabbixRight] = [] @model_serializer def model_ser(self) -> Dict[str, Any]: """LEGACY: Include the permission strings in the serialized output if we have legacy JSON output enabled. """ d: Dict[str, Any] = { "usrgrpid": self.usrgrpid, "name": self.name, "hostgroup_rights": self.hostgroup_rights, "templategroup_rights": self.templategroup_rights, } if self.legacy_json_format: d["permissions"] = self.permissions d["usergroupid"] = self.usrgrpid return d @property def permissions(self) -> List[str]: """LEGACY: The field `hostgroup_rights` was called `permissions` in V2.""" r: List[str] = [] def permission_str( right: ZabbixRight, groups: Mapping[str, Union[HostGroup, TemplateGroup]] ) -> str: group = groups.get(right.id, None) if group: group_name = group.name else: group_name = "Unknown" perm = UsergroupPermission.string_from_value( right.permission, with_code=True ) return f"{group_name} ({perm})" for right in self.hostgroup_rights: r.append(permission_str(right, self.hostgroups)) for right in self.templategroup_rights: r.append(permission_str(right, self.templategroups)) return r @classmethod def from_usergroup( cls, usergroup: Usergroup, hostgroups: List[HostGroup], templategroups: List[TemplateGroup], ) -> ShowUsergroupPermissionsResult: cls.model_rebuild() # TODO: can we avoid this? res = cls( usrgrpid=usergroup.usrgrpid, name=usergroup.name, hostgroups={hg.groupid: hg for hg in hostgroups}, templategroups={tg.groupid: tg for tg in templategroups}, templategroup_rights=usergroup.templategroup_rights, ) if res.zabbix_version.release >= (6, 2, 0): res.hostgroup_rights = usergroup.hostgroup_rights else: res.hostgroup_rights = usergroup.rights return res def __cols_rows__(self) -> ColsRowsType: cols = ["ID", "Name", "Host Groups"] row: RowContent = [self.usrgrpid, self.name] # Host group rights table row.append( GroupRights(groups=self.hostgroups, rights=self.hostgroup_rights).as_table() ) # Template group rights table if self.zabbix_version.release >= (6, 2, 0): cols.append("Template Groups") row.append( GroupRights( groups=self.templategroups, rights=self.templategroup_rights ).as_table() ) return cols, [row] class AddUsergroupPermissionsResult(TableRenderable): usergroup: str hostgroups: List[str] templategroups: List[str] permission: UsergroupPermission @computed_field @property def permission_str(self) -> str: # FIXME: remove this? Serializing a Choice enum should dump the same value? return self.permission.as_status() def __cols_rows__(self) -> ColsRowsType: return ( [ "Usergroup", "Host Groups", "Template Groups", "Permission", ], [ [ self.usergroup, ", ".join(self.hostgroups), ", ".join(self.templategroups), self.permission_str, ], ], ) unioslo-zabbix-cli-09a2fab/zabbix_cli/commands/template.py000066400000000000000000000513641471265333400240270ustar00rootroot00000000000000from __future__ import annotations from itertools import chain from typing import TYPE_CHECKING from typing import List from typing import Optional from typing import Union import typer from zabbix_cli.app import Example from zabbix_cli.app import app from zabbix_cli.output.console import exit_err from zabbix_cli.output.console import info from zabbix_cli.output.console import success from zabbix_cli.output.formatting.grammar import pluralize as p from zabbix_cli.output.render import render_result from zabbix_cli.utils.args import parse_list_arg if TYPE_CHECKING: from zabbix_cli.pyzabbix.types import Host from zabbix_cli.pyzabbix.types import HostGroup from zabbix_cli.pyzabbix.types import Template from zabbix_cli.pyzabbix.types import TemplateGroup HELP_PANEL = "Template" # FIXME: use parse_hostgroup_args from utils.args def _handle_hostnames_args( hostnames_or_ids: str, strict: bool = False, ) -> List[Host]: host_args = [h.strip() for h in hostnames_or_ids.split(",")] if not host_args: exit_err("At least one host name/ID is required.") hosts = app.state.client.get_hosts(*host_args, search=True) if not hosts: exit_err(f"No hosts found matching {hostnames_or_ids}") if strict and len(hosts) != len(host_args): exit_err(f"Found {len(hosts)} hosts, expected {len(host_args)}") return hosts def _handle_hostgroups_args( hgroup_names_or_ids: str, strict: bool = False, select_templates: bool = False, ) -> List[HostGroup]: hg_args = [h.strip() for h in hgroup_names_or_ids.split(",")] if not hg_args: exit_err("At least one host group name/ID is required.") hostgroups = app.state.client.get_hostgroups( *hg_args, search=True, select_templates=select_templates ) if not hostgroups: exit_err(f"No host groups found matching {hgroup_names_or_ids}") if strict and len(hostgroups) != len(hg_args): exit_err(f"Found {len(hostgroups)} host groups, expected {len(hostgroups)}") return hostgroups def _handle_templategroup_args( tgroup_names_or_ids: str, strict: bool = False, select_templates: bool = False, ) -> List[TemplateGroup]: tg_args = parse_list_arg(tgroup_names_or_ids) if not tg_args: exit_err("At least one template group name/ID is required.") templategroups = app.state.client.get_templategroups( *tg_args, search=True, select_templates=select_templates ) if not templategroups: exit_err(f"No template groups found matching {tgroup_names_or_ids}") if strict and len(templategroups) != len(tg_args): exit_err( f"Found {len(templategroups)} template groups, expected {len(templategroups)}" ) return templategroups def _handle_template_arg( template_names_or_ids: str, strict: bool = False, select_hosts: bool = False, ) -> List[Template]: template_args = parse_list_arg(template_names_or_ids) if not template_args: exit_err("At least one template name/ID is required.") templates = app.state.client.get_templates( *template_args, select_hosts=select_hosts ) if not templates: exit_err(f"No templates found matching {template_names_or_ids}") if strict and len(templates) != len(template_args): exit_err(f"Found {len(templates)} templates, expected {len(template_args)}") return templates ARG_TEMPLATE_NAMES_OR_IDS = typer.Argument( help="Template names or IDs. Comma-separated. Supports wildcards.", show_default=False, ) ARG_HOSTNAMES_OR_IDS = typer.Argument( help="Hostnames or IDs. Comma-separated. Supports wildcards.", show_default=False, ) ARG_GROUP_NAMES_OR_IDS = typer.Argument( help="Host/template group names or IDs. Comma-separated. Supports wildcards.", show_default=False, ) @app.command( "link_template_to_host", rich_help_panel=HELP_PANEL, examples=[ Example( "Link one template to one host", "link_template_to_host 'Apache by HTTP' foo.example.com", ), Example( "Link many templates to many hosts", "link_template_to_host 'Apache by HTTP,HAProxy by Zabbix agent' foo.example.com,bar.example.com", ), Example( "Link one template to all hosts", "link_template_to_host 'Apache by HTTP' '*'", ), Example( "Link many templates to all hosts", "link_template_to_host 'Apache by HTTP,HAProxy by Zabbix agent' '*'", ), Example( "Link all templates to all hosts [red](use with caution!)[/red]", "link_template_to_host '*' '*'", ), ], ) def link_template_to_host( ctx: typer.Context, template_names_or_ids: str = ARG_TEMPLATE_NAMES_OR_IDS, hostnames_or_ids: str = ARG_HOSTNAMES_OR_IDS, strict: bool = typer.Option( False, "--strict", help="Fail if any hosts or templates aren't found. Should not be used in conjunction with wildcards.", ), dryrun: bool = typer.Option( False, "--dryrun", help="Preview changes.", ), ) -> None: """Link templates to hosts.""" from zabbix_cli.commands.results.template import LinkTemplateToHostResult from zabbix_cli.models import AggregateResult templates = _handle_template_arg(template_names_or_ids, strict, select_hosts=True) hosts = _handle_hostnames_args(hostnames_or_ids, strict) if not dryrun: with app.state.console.status("Linking templates..."): app.state.client.link_templates_to_hosts(templates, hosts) result: List[LinkTemplateToHostResult] = [] for host in hosts: r = LinkTemplateToHostResult.from_result(templates, host, "Link") if not r.templates: continue result.append(r) total_templates = len(set(chain.from_iterable((r.templates) for r in result))) total_hosts = len(result) render_result(AggregateResult(result=result)) base_msg = f"{p('template', total_templates)} to {p('host', total_hosts)}" if dryrun: info(f"Would link {base_msg}.") else: success(f"Linked {base_msg}.") @app.command( "unlink_template_from_host", rich_help_panel=HELP_PANEL, examples=[ Example( "Unlink a template from a host", "unlink_template_from_host 'Apache by HTTP' foo.example.com", ), Example( "Unlink many templates from many hosts", "unlink_template_from_host 'Apache by HTTP,HAProxy by Zabbix agent' foo.example.com,bar.example.com", ), Example( "Unlink one template from all hosts", "unlink_template_from_host 'Apache by HTTP' '*'", ), Example( "Unlink templates starting with 'Apache' from hosts starting with 'Web'", "unlink_template_from_host 'Apache*' 'Web-*'", ), Example( "Unlink template from host without clearing items and triggers", "unlink_template_from_host --no-clear 'Apache by HTTP' foo.example.com", ), ], ) def unlink_template_from_host( ctx: typer.Context, template_names_or_ids: str = ARG_TEMPLATE_NAMES_OR_IDS, hostnames_or_ids: str = ARG_HOSTNAMES_OR_IDS, strict: bool = typer.Option( False, "--strict", help="Fail if any hosts or templates aren't found. Should not be used in conjunction with wildcards.", ), dryrun: bool = typer.Option( False, "--dryrun", help="Preview changes.", ), clear: bool = typer.Option( True, "--clear/--no-clear", help="Unlink and clear templates." ), ) -> None: """Unlink templates from hosts. Unlinks and clears by default. Use `--no-clear` to unlink without clearing. """ from zabbix_cli.commands.results.template import UnlinkTemplateFromHostResult from zabbix_cli.models import AggregateResult templates = _handle_template_arg(template_names_or_ids, strict, select_hosts=True) hosts = _handle_hostnames_args(hostnames_or_ids, strict) action = "Unlink and clear" if clear else "Unlink" if not dryrun: with app.state.console.status("Unlinking templates..."): app.state.client.unlink_templates_from_hosts(templates, hosts) # Only show hosts with matching templates to unlink result: List[UnlinkTemplateFromHostResult] = [] for host in hosts: r = UnlinkTemplateFromHostResult.from_result(templates, host, action) if not r.templates: continue result.append(r) total_templates = len(set(chain.from_iterable((r.templates) for r in result))) total_hosts = len(result) render_result(AggregateResult(result=result)) base_msg = f"{p('template', total_templates)} from {p('host', total_hosts)}" if dryrun: info(f"Would {action.lower()} {base_msg}.") else: action_success = "Unlinked and cleared" if clear else "Unlinked" success(f"{action_success} {base_msg}.") @app.command( "link_template_to_template", rich_help_panel=HELP_PANEL, examples=[ Example( "Link one template to one template", "link_template_to_template 'Apache by HTTP' foo_template", ), Example( "Link many templates to many templates", "link_template_to_template 'Apache by HTTP,HAProxy by Zabbix agent' foo_template,bar_template", ), Example( "Link all templates starting with 'Apache' to a template", "link_template_to_template 'Apache*' foo_template", ), Example( "Link all templates containing 'HTTP' to a subset of templates", "link_template_to_template '*HTTP*' 'Webserver-*'", ), ], ) def link_template_to_template( ctx: typer.Context, source: str = ARG_TEMPLATE_NAMES_OR_IDS, dest: str = ARG_TEMPLATE_NAMES_OR_IDS, strict: bool = typer.Option( False, "--strict", help="Fail if any templates aren't found. Should not be used in conjunction with wildcards.", ), dryrun: bool = typer.Option( False, "--dryrun", help="Do not actually link templates, just show what would be done.", ), ) -> None: """Link templates to templates. [b]NOTE:[/] Destination templates are the ones that are ultimately modified. Source templates remain unchanged. """ from zabbix_cli.commands.results.template import LinkTemplateResult # TODO: add existing link checking just like in `link_template_to_host` & `unlink_template_from_host` # so we only print the ones that are actually linked source_templates = _handle_template_arg(source, strict) dest_templates = _handle_template_arg(dest, strict) if not dryrun: with app.state.console.status("Linking templates..."): app.state.client.link_templates(source_templates, dest_templates) render_result( LinkTemplateResult.from_result(source_templates, dest_templates, "Link") ) base_msg = f"{p('template', len(source_templates))} to {p('template', len(dest_templates))}" if dryrun: info(f"Would link {base_msg}.") else: success(f"Linked {base_msg}.") @app.command( "unlink_template_from_template", rich_help_panel=HELP_PANEL, examples=[ Example( "Unlink one template from one template", "unlink_template_from_template 'Apache by HTTP' foo_template", ), Example( "Unlink many templates from many templates", "unlink_template_from_template 'Apache by HTTP,HAProxy by Zabbix agent' foo_template,bar_template", ), Example( "Unlink all templates starting with 'Apache' from a template", "unlink_template_from_template 'Apache*' foo_template", ), Example( "Unlink all templates containing 'HTTP' from a subset of templates", "unlink_template_from_template '*HTTP*' 'Web-*'", ), Example( "Unlink a template without clearing items and triggers", "unlink_template_from_template --no-clear foo_template bar_template", ), ], ) def unlink_template_from_template( ctx: typer.Context, source: str = ARG_TEMPLATE_NAMES_OR_IDS, dest: str = ARG_TEMPLATE_NAMES_OR_IDS, strict: bool = typer.Option( False, "--strict", help="Fail if any templates aren't found. Should not be used in conjunction with wildcards.", ), clear: bool = typer.Option( True, "--clear/--no-clear", help="Unlink and clear templates.", ), dryrun: bool = typer.Option( False, "--dryrun", help="Preview changes.", ), ) -> None: """Unlink templates from templates. Unlinks and clears by default. Use `--no-clear` to unlink without clearing. [b]NOTE:[/] Destination templates are the ones that are ultimately modified. Source templates remain unchanged. """ from zabbix_cli.commands.results.template import LinkTemplateResult source_templates = _handle_template_arg(source, strict) dest_templates = _handle_template_arg(dest, strict) if not dryrun: with app.state.console.status("Unlinking templates..."): app.state.client.unlink_templates( source_templates, dest_templates, clear=clear ) action = "Unlink and clear" if clear else "Unlink" render_result( LinkTemplateResult.from_result( source_templates, dest_templates, action, ) ) base_msg = f"{p('template', len(source_templates))} from {p('template', len(dest_templates))}" if dryrun: info(f"Would {action.lower()} {base_msg}.") else: action_success = "Unlinked and cleared" if clear else "Unlinked" success(f"{action_success} {base_msg}.") # Changed in V3: Changed name to reflect introduction of template groups in >=6.2 @app.command("add_template_to_group", rich_help_panel=HELP_PANEL) @app.command( # old name for backwards compatibility "link_template_to_hostgroup", hidden=True, deprecated=True, rich_help_panel=HELP_PANEL, help="DEPRECATED: Use add_template_to_group instead.", ) def add_template_to_group( ctx: typer.Context, template_names_or_ids: str = ARG_TEMPLATE_NAMES_OR_IDS, group_names_or_ids: str = ARG_GROUP_NAMES_OR_IDS, strict: bool = typer.Option( False, "--strict", help="Fail if any host groups or templates aren't found. Should not be used in conjunction with wildcards.", ), ) -> None: """Add templates to groups. [bold]NOTE:[/] Group arguments are interpreted as template groups in >= 6.2, otherwise as host groups. """ from zabbix_cli.commands.results.template import TemplateGroupResult groups: Union[List[HostGroup], List[TemplateGroup]] if app.state.client.version.release >= (6, 2, 0): groups = _handle_templategroup_args(group_names_or_ids, strict) else: groups = _handle_hostgroups_args(group_names_or_ids, strict) templates = _handle_template_arg(template_names_or_ids, strict) with app.state.console.status("Adding templates..."): app.state.client.link_templates_to_groups(templates, groups) render_result(TemplateGroupResult.from_result(templates, groups)) success(f"Added {len(templates)} templates to {len(groups)} groups.") # Changed in V3: Changed name to reflect introduction of template groups in >=6.2 @app.command( "remove_template_from_group", rich_help_panel=HELP_PANEL, examples=[ Example( "Remove one template from one group", "remove_template_from_group 'Apache by HTTP' foo_group", ), Example( "Remove many templates from many groups", "remove_template_from_group 'Apache by HTTP,HAProxy by Zabbix agent' foo_group,bar_group", ), Example( "Remove all templates starting with 'Apache' from a group", "remove_template_from_group 'Apache*' foo_group", ), Example( "Remove all templates containing 'HTTP' from all groups", "remove_template_from_group '*HTTP*' '*'", ), ], ) @app.command( # old name for backwards compatibility "unlink_template_from_hostgroup", hidden=True, deprecated=True, rich_help_panel=HELP_PANEL, help="Use `remove_template_from_group` instead.", ) def remove_template_from_group( ctx: typer.Context, template_names_or_ids: str = ARG_TEMPLATE_NAMES_OR_IDS, group_names_or_ids: str = ARG_GROUP_NAMES_OR_IDS, strict: bool = typer.Option( False, "--strict", help="Fail if any host groups or templates aren't found. Should not be used in conjunction with wildcards.", ), dryrun: bool = typer.Option( False, "--dryrun", help="Preview changes.", ), # TODO: add toggle for NOT clearing when unlinking? ) -> None: """Remove templates from groups.""" from zabbix_cli.commands.results.template import RemoveTemplateFromGroupResult from zabbix_cli.models import AggregateResult groups: Union[List[HostGroup], List[TemplateGroup]] if app.state.client.version.release >= (6, 2, 0): groups = _handle_templategroup_args( group_names_or_ids, strict=strict, select_templates=True ) else: groups = _handle_hostgroups_args( group_names_or_ids, strict=strict, select_templates=True ) templates = _handle_template_arg(template_names_or_ids, strict=strict) if not dryrun: with app.state.console.status("Removing templates from groups..."): # LEGACY: This used to also pass the templateids to templateids_clear, # which would unlink and clear all the templates from each other # This was a mistake, and has been removed in V3. # Users should use `unlink_templates_from_templates` for that. app.state.client.remove_templates_from_groups( templates, groups, ) result: List[RemoveTemplateFromGroupResult] = [] for group in groups: r = RemoveTemplateFromGroupResult.from_result(templates, group) if not r.templates: continue result.append(r) total_templates = len(set(chain.from_iterable((r.templates) for r in result))) total_groups = len(result) render_result(AggregateResult(result=result, empty_ok=True)) base_msg = f"{p('template', total_templates)} from {p('group', total_groups)}" if dryrun: info(f"Would remove {base_msg}.") else: success(f"Removed {base_msg}.") @app.command("show_template", rich_help_panel=HELP_PANEL) def show_template( ctx: typer.Context, template_name: str = typer.Argument( help="Template name or ID. Names support wildcards.", show_default=False, ), ) -> None: """Show a template.""" template = app.state.client.get_template( template_name, select_hosts=True, select_templates=True, select_parent_templates=True, ) render_result(template) @app.command("show_templates", rich_help_panel=HELP_PANEL) def show_templates( ctx: typer.Context, templates: Optional[str] = typer.Argument( None, help="Template name(s) or ID(s). Comma-separated. Supports wildcards.", show_default=False, ), ) -> None: """Show all templates. Shows all templates by default. The template name can be a pattern containing wildcards. Names and IDs cannot be mixed. """ from zabbix_cli.models import AggregateResult template_names_or_ids = parse_list_arg(templates) tpls = app.state.client.get_templates( *template_names_or_ids, select_hosts=True, select_templates=True, select_parent_templates=True, ) render_result(AggregateResult(result=tpls)) @app.command( "show_items", rich_help_panel=HELP_PANEL, examples=[ Example( "Show items for a template", "show_items 'Apache by HTTP'", ), ], ) def show_items( ctx: typer.Context, template_name: str = typer.Argument( help="Template name or ID. Supports wildcards.", show_default=False, ), ) -> None: """Show a template's items.""" from zabbix_cli.models import AggregateResult template = app.state.client.get_template(template_name) items = app.state.client.get_items(templates=[template]) # NOTE: __title__ is ignored by Pydantic when used as a kwarg # when instantiating a model. We either need to subclass AggregateResult # or set it after instantiation. # Ideally, we would rename the field to `table_title` and make it an actual field # but that clashes with our existing `__