pax_global_header00006660000000000000000000000064150737656710014533gustar00rootroot0000000000000052 comment=617976e1706dc9a72ea40e529d4b1dcc433d4e22 chango-0.6.0/000077500000000000000000000000001507376567100127755ustar00rootroot00000000000000chango-0.6.0/.github/000077500000000000000000000000001507376567100143355ustar00rootroot00000000000000chango-0.6.0/.github/dependabot.yml000066400000000000000000000004211507376567100171620ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "pip" directory: "/" schedule: interval: "quarterly" # Updates the dependencies of the GitHub Actions workflows - package-ecosystem: "github-actions" directory: "/" schedule: interval: "quarterly" chango-0.6.0/.github/workflows/000077500000000000000000000000001507376567100163725ustar00rootroot00000000000000chango-0.6.0/.github/workflows/create_chango_fragment.yml000066400000000000000000000011301507376567100235550ustar00rootroot00000000000000name: Create Chango Fragment on: pull_request: types: - opened - reopened - synchronize - labeled - unlabeled jobs: create-chango-fragment: permissions: # Give the default GITHUB_TOKEN write permission to commit and push the # added or changed files to the repository. contents: write name: create-chango-fragment runs-on: ubuntu-latest steps: # Create the new fragment - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false - uses: ./ chango-0.6.0/.github/workflows/doc_build_test.yml000066400000000000000000000016601507376567100221030ustar00rootroot00000000000000name: Test Documentation Build on: pull_request: paths: - src/** - '**.rst' - '.github/workflows/doc_build_test.yml' push: branches: - main jobs: test-sphinx-build: name: test-sphinx-build runs-on: ubuntu-latest steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false - name: Set up Python uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: python-version: 3.12 cache: 'pip' cache-dependency-path: | **/requirements*.txt pyproject.toml - name: Install dependencies run: | python -W ignore -m pip install --upgrade pip python -W ignore -m pip install . --group docs - name: Build docs run: sphinx-build docs/source docs/build/html -W --keep-going -j auto chango-0.6.0/.github/workflows/gha_security.yml000066400000000000000000000016351507376567100216100ustar00rootroot00000000000000name: GitHub Actions Security Analysis on: push: branches: - master pull_request: jobs: zizmor: name: Security Analysis with zizmor runs-on: ubuntu-latest permissions: contents: read security-events: write steps: - name: Checkout repository uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false - name: Install the latest version of uv uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 - name: Run zizmor run: uvx zizmor --persona=pedantic --format sarif . > results.sarif env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload SARIF file uses: github/codeql-action/upload-sarif@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5 with: sarif_file: results.sarif category: zizmor chango-0.6.0/.github/workflows/release_pypi.yml000066400000000000000000000102021507376567100215710ustar00rootroot00000000000000name: Publish to PyPI on: # manually trigger the workflow workflow_dispatch: jobs: build: name: Build Distribution runs-on: ubuntu-latest outputs: TAG: ${{ steps.get_tag.outputs.TAG }} steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false - name: Set up Python uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: python-version: "3.x" - name: Install pypa/build run: >- python3 -m pip install build --user - name: Build a binary wheel and a source tarball run: python3 -m build - name: Store the distribution packages uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: python-package-distributions path: dist/ - name: Get Tag Name id: get_tag run: | pip install . TAG=$(python -c "from chango import __version__; print(__version__)") echo "TAG=$TAG" >> $GITHUB_OUTPUT publish-to-pypi: name: Publish to PyPI needs: - build runs-on: ubuntu-latest environment: name: release_pypi url: https://pypi.org/p/chango permissions: id-token: write # IMPORTANT: mandatory for trusted publishing steps: - name: Download all the dists uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 with: name: python-package-distributions path: dist/ - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 compute-signatures: name: Compute SHA1 Sums and Sign with Sigstore runs-on: ubuntu-latest needs: - publish-to-pypi permissions: id-token: write # IMPORTANT: mandatory for sigstore steps: - name: Download all the dists uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 with: name: python-package-distributions path: dist/ - name: Compute SHA1 Sums run: | # Compute SHA1 sum of the distribution packages and save it to a file with the same name, # but with .sha1 extension for file in dist/*; do sha1sum $file > $file.sha1 done - name: Sign the dists with Sigstore uses: sigstore/gh-action-sigstore-python@f7ad0af51a5648d09a20d00370f0a91c3bdf8f84 # v3.0.1 with: inputs: >- ./dist/*.tar.gz ./dist/*.whl - name: Store the distribution packages and signatures uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: python-package-distributions-and-signatures path: dist/ github-release: name: Upload to GitHub Release needs: - build - compute-signatures runs-on: ubuntu-latest permissions: contents: write # IMPORTANT: mandatory for making GitHub Releases steps: - name: Download all the dists uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 with: name: python-package-distributions-and-signatures path: dist/ - name: Create GitHub Release env: GITHUB_TOKEN: ${{ github.token }} TAG: ${{ needs.build.outputs.TAG }} # Create a tag and a GitHub Release. The description can be changed later, as for now # we don't define it through this workflow. run: >- gh release create "$TAG" --repo '${{ github.repository }}' --generate-notes - name: Upload artifact signatures to GitHub Release env: GITHUB_TOKEN: ${{ github.token }} TAG: ${{ needs.build.outputs.TAG }} # Upload to GitHub Release using the `gh` CLI. # `dist/` contains the built packages, and the # sigstore-produced signatures and certificates. run: >- gh release upload "$TAG" dist/** --repo '${{ github.repository }}' chango-0.6.0/.github/workflows/release_test_pypi.yml000066400000000000000000000104061507376567100226360ustar00rootroot00000000000000name: Publish to Test PyPI on: # manually trigger the workflow workflow_dispatch: jobs: build: name: Build Distribution runs-on: ubuntu-latest outputs: TAG: ${{ steps.get_tag.outputs.TAG }} steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false - name: Set up Python uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: python-version: "3.x" - name: Install pypa/build run: >- python3 -m pip install build --user - name: Build a binary wheel and a source tarball run: python3 -m build - name: Store the distribution packages uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: python-package-distributions path: dist/ - name: Get Tag Name id: get_tag run: | pip install . TAG=$(python -c "from chango import __version__; print(__version__)") echo "TAG=$TAG" >> $GITHUB_OUTPUT publish-to-test-pypi: name: Publish to Test PyPI needs: - build runs-on: ubuntu-latest environment: name: release_test_pypi url: https://test.pypi.org/p/chango permissions: id-token: write # IMPORTANT: mandatory for trusted publishing steps: - name: Download all the dists uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 with: name: python-package-distributions path: dist/ - name: Publish to Test PyPI uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 with: repository-url: https://test.pypi.org/legacy/ compute-signatures: name: Compute SHA1 Sums and Sign with Sigstore runs-on: ubuntu-latest needs: - publish-to-test-pypi permissions: id-token: write # IMPORTANT: mandatory for sigstore steps: - name: Download all the dists uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 with: name: python-package-distributions path: dist/ - name: Compute SHA1 Sums run: | # Compute SHA1 sum of the distribution packages and save it to a file with the same name, # but with .sha1 extension for file in dist/*; do sha1sum $file > $file.sha1 done - name: Sign the dists with Sigstore uses: sigstore/gh-action-sigstore-python@f7ad0af51a5648d09a20d00370f0a91c3bdf8f84 # v3.0.1 with: inputs: >- ./dist/*.tar.gz ./dist/*.whl - name: Store the distribution packages and signatures uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: python-package-distributions-and-signatures path: dist/ github-test-release: name: Upload to GitHub Release Draft needs: - build - compute-signatures runs-on: ubuntu-latest permissions: contents: write # IMPORTANT: mandatory for making GitHub Releases steps: - name: Download all the dists uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 with: name: python-package-distributions-and-signatures path: dist/ - name: Create GitHub Release env: GITHUB_TOKEN: ${{ github.token }} TAG: ${{ needs.build.outputs.TAG }} # Create a GitHub Release *draft*. The description can be changed later, as for now # we don't define it through this workflow. run: >- gh release create "$TAG" --repo '${{ github.repository }}' --generate-notes --draft - name: Upload artifact signatures to GitHub Release env: GITHUB_TOKEN: ${{ github.token }} TAG: ${{ needs.build.outputs.TAG }} # Upload to GitHub Release using the `gh` CLI. # `dist/` contains the built packages, and the # sigstore-produced signatures and certificates. run: >- gh release upload "$TAG" dist/** --repo '${{ github.repository }}' chango-0.6.0/.github/workflows/type_completeness_test.yml000066400000000000000000000007261507376567100237230ustar00rootroot00000000000000name: Check Type Completeness on: pull_request: paths: - src/** - pyproject.toml - .github/workflows/type_completeness_test.yml push: branches: - main jobs: test-type-completeness: name: test-type-completeness runs-on: ubuntu-latest steps: - uses: Bibo-Joshi/pyright-type-completeness@c85a67ff3c66f51dcbb2d06bfcf4fe83a57d69cc # v1.0.1 with: package-name: chango python-version: 3.12 chango-0.6.0/.github/workflows/unit_tests.yml000066400000000000000000000036731507376567100213270ustar00rootroot00000000000000name: Unit Tests on: pull_request: paths: - src/** - tests/** - pyproject.toml - requirements-dev.txt - '.github/workflows/unit_tests.yml' push: branches: - main jobs: pytest: name: pytest runs-on: ${{matrix.os}} strategy: matrix: python-version: ['3.12', '3.13'] os: [ubuntu-latest, windows-latest, macos-latest] fail-fast: False steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: python-version: ${{ matrix.python-version }} cache: 'pip' cache-dependency-path: | **/requirements*.txt pyproject.toml - name: Install dependencies run: | python -W ignore -m pip install --upgrade pip python -W ignore -m pip install . --group tests - name: Test with pytest run: | pytest --cov -v --junit-xml=.test_junit.xml - name: Test Summary id: test_summary uses: test-summary/action@31493c76ec9e7aa675f1585d3ed6f1da69269a86 # v2.4 if: always() # always run, even if tests fail with: paths: | .test_junit.xml - name: Submit coverage uses: codecov/codecov-action@fdcc8476540edceab3de004e990f80d881c6cc00 # v5.5.0 with: env_vars: OS,PYTHON name: ${{ matrix.os }}-${{ matrix.python-version }} fail_ci_if_error: true token: ${{ secrets.CODECOV_TOKEN }} - name: Upload test results to Codecov uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f # v1.1.1 if: ${{ !cancelled() }} with: files: .test_junit.xml token: ${{ secrets.CODECOV_TOKEN }} chango-0.6.0/.gitignore000066400000000000000000000061231507376567100147670ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ docs/_uild/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control #poetry.lock # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. #pdm.lock # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it # in version control. # https://pdm.fming.dev/latest/usage/project/#working-with-version-control .pdm.toml .pdm-python .pdm-build/ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # PyCharm # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. .idea/ tmp/chango-0.6.0/.pre-commit-config.yaml000066400000000000000000000026311507376567100172600ustar00rootroot00000000000000ci: autoupdate_commit_msg: 'Bump `pre-commit` Dependency Versions' autoupdate_schedule: quarterly repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.13.3 hooks: # Run the formatter. - id: ruff-format # Run the linter. - id: ruff args: - --fix - --unsafe-fixes # Does not have support for PEP 695 generics yet - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.18.2 hooks: - id: mypy name: mypy files: ^(?!(tests|changes)).*\.py$ additional_dependencies: - . - pydantic-settings~=2.3 - shortuuid~=1.0 - sphinx~=8.0 - tomlkit~=0.13 - typer~=0.12 - types-Pygments - types-PyYAML # pyright doesn't work well with pre-commit.ci - see # - https://github.com/RobertCraigie/pyright-python/pull/300 # - https://github.com/RobertCraigie/pyright-python/issues/231 # - repo: https://github.com/RobertCraigie/pyright-python # rev: v1.1.382.post0 # hooks: # - id: pyright # name: pyright # files: ^(?!(tests)).*\.py$ # additional_dependencies: # - pydantic-settings~=2.3 # - shortuuid~=1.0 # - tomlkit~=0.13 # - typer~=0.12 - repo: https://github.com/asottile/pyupgrade rev: v3.20.0 hooks: - id: pyupgrade args: - --py312-plus chango-0.6.0/.readthedocs.yaml000066400000000000000000000005211507376567100162220ustar00rootroot00000000000000version: 2 build: os: ubuntu-24.04 tools: python: "3.12" jobs: install: - pip install -U pip - pip install .[all] --group 'all' # install all the dependency groups sphinx: configuration: docs/source/conf.py python: install: - method: pip path: . - requirements: docs/requirements-docs.txt chango-0.6.0/LICENSE.txt000066400000000000000000000021161507376567100146200ustar00rootroot00000000000000MIT License Copyright (c) 2024-present Hinrich Mahler Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. chango-0.6.0/README.rst000066400000000000000000000126341507376567100144720ustar00rootroot00000000000000``chango`` ========== .. image:: https://img.shields.io/pypi/v/chango.svg :target: https://pypi.org/project/chango/ :alt: PyPi Package Version .. image:: https://img.shields.io/pypi/pyversions/chango.svg :target: https://pypi.org/project/chango/ :alt: Supported Python versions .. image:: https://readthedocs.org/projects/chango/badge/?version=stable :target: https://chango.readthedocs.io/ :alt: Documentation Status .. image:: https://img.shields.io/pypi/l/chango.svg :target: https://mit-license.org/ :alt: MIT License .. image:: https://github.com/Bibo-Joshi/chango/actions/workflows/unit_tests.yml/badge.svg?branch=main :target: https://github.com/Bibo-Joshi/chango/ :alt: Github Actions workflow .. image:: https://codecov.io/gh/Bibo-Joshi/chango/graph/badge.svg?token=H1HUA2FDR3 :target: https://codecov.io/gh/Bibo-Joshi/chango :alt: Code coverage .. image:: https://results.pre-commit.ci/badge/github/Bibo-Joshi/chango/main.svg :target: https://results.pre-commit.ci/latest/github/Bibo-Joshi/chango/main :alt: pre-commit.ci status .. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json :target: https://github.com/astral-sh/ruff :alt: Ruff Introduction ------------ ``chango`` is a changelog generation tool. Changes are described alongside the code changes in the codebase. ``chango`` extracts these changes and generates a changelog. ``chango`` consists of both a command line interface and a Python API. All aspects of the data formats, storage, and rendering are customizable via an interface class approach. Installing ---------- You can install or upgrade ``chango`` via .. code:: shell pipx install chango --upgrade Note that installation via `pipx `_ is recommended since ``chango`` should not interfere with your projects dependencies. Motivation ---------- Informative and helpful changelogs (or release notes) are an essential part of software development. They are a core part of the communication between developers and users. At the same time, creating and maintaining changelogs can be a tedious and error-prone task, especially since this is often done only when a new release is prepared. ``chango`` aims to make the process of maintaining changelogs as easy as possible. This is achieved roughly by two means: 1. **Shifting the creation of changelogs to the time of development**: Changes are described alongside the code changes in the codebase. This reduces the chance to forget about crucial details in the changes that should be mentioned in the changelog. It also ensures that the changelog undergoes the same review process as the code changes. 2. **Automating the generation of changelogs**: ``chango`` extracts the changes from the codebase and generates a changelog. At release time, the changelog is thus already up-to-date and requires close to zero manual work. Inspiration ~~~~~~~~~~~ This package is heavily inspired by the `towncrier `_ and `reno `_ packages. Both packages are excellent tools for changelog generation but are rather specialized in their use cases. ``chango`` aims to be more flexible and customizable than these packages. Quick Start ----------- A minimal setup of using ``chango`` for your project consists of the following steps. Building a ``ChanGo`` instance ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ``chango`` is designed to be highly customizable. Store the following code in a file named ``chango.py`` in the root of your project. .. code:: python from chango.concrete import ( CommentChangeNote, CommentVersionNote, DirectoryChanGo, DirectoryVersionScanner, HeaderVersionHistory, ) chango_instance = DirectoryChanGo( change_note_type=CommentChangeNote, version_note_type=CommentVersionNote, version_history_type=HeaderVersionHistory, scanner=DirectoryVersionScanner( base_directory="changes", unreleased_directory="unreleased" ), ) Create the directories ``changes`` and ``changes/unreleased`` in the root of your project. The ``chango_instance`` is the object that the CLI will use to interact with the changelog. It contains information about the data type of individual changes, versions and the history of versions. It also has instructions on how the individual changes are stored and how they are extracted from the codebase. All these aspects can be customized by providing different classes to the ``DirectoryChanGo`` constructor or using a different implementation of the ``ChanGo`` interface. Configuring ``pyproject.toml`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ We still need to make the ``chango`` CLI aware of the ``chango_instance``. This is done by adding the following lines to your ``pyproject.toml`` file. .. code:: toml [tool.chango] sys_path = "." chango_instance = { name= "chango_instance", module = "chango" } This instructs the CLI to import the ``chango_instance`` from the file ``chango.py`` in the root of your project. Adding Changes ~~~~~~~~~~~~~~~ Now the ``chango`` CLI is ready to be used. Go ahead and add a change to the ``changes/unreleased`` directory. It's as simple als calling .. code:: shell chango new short-slug-for-the-change For more information on how to use ``chango``, please refer to the `documentation `_. chango-0.6.0/action.js000066400000000000000000000056171507376567100146210ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2024-present Hinrich Mahler * * SPDX-License-Identifier: MIT */ module.exports = async ({github, context, core, query_issue_types}) => { const pullRequest = context.payload.pull_request; if (!pullRequest) { // This action only runs on pull_request events. Skip with debug message but // do not fail the action. console.log('No pull request found in the context. Not fetching additional data.'); return; } const use_query_issue_types = query_issue_types === 'true'; // Fetch the linked issues const data = await github.graphql(` query { repository(owner:"${context.repo.owner}", name:"${context.repo.repo}") { pullRequest(number:${context.payload.pull_request.number}) { closingIssuesReferences(first: 100) { nodes { number title ${use_query_issue_types ? 'issueType { name }' : ''} labels(first: 100) { nodes { name id } } } } } } } `, { headers: { "GraphQL-Features": "issue_types" } }); const closingIssues = data.repository.pullRequest.closingIssuesReferences.nodes.map(node => { return { number: node.number, title: node.title, labels: node.labels.nodes.map(label => label.name), issueType: node.issueType ? node.issueType.name : null }; }); // Fetch the parent PR const response = await github.graphql(` query { search( type: ISSUE, query: "org:${context.repo.owner} repo:${context.repo.repo} is:pr head:${context.payload.pull_request.base.ref}" first: 100 ) { issueCount nodes { ... on PullRequest { number title url state author { login } } } } } `); const prs = response.search.nodes; const parentPR = prs.length > 0 ? prs[0] : null; if (parentPR) { parentPR.author_login = parentPR.author.login; delete parentPR.author; } // Combine the linked issues and the parent PR to a single JSON object const linkedIssuesAndParentPR = { linked_issues: closingIssues, parent_pull_request: parentPR }; core.setOutput('data', JSON.stringify(linkedIssuesAndParentPR)); };chango-0.6.0/action.yml000066400000000000000000000175021507376567100150020ustar00rootroot00000000000000name: chango Action description: Composite Action for automatically creating a chango fragment for a PR. branding: icon: book-open color: gray-dark inputs: python-version: description: Python version to use. See actions/setup-python for more information. Default is 3.x required: false default: 3.x commit-and-push: description: Whether to commit and push the changes to the PR branch. Default is true. required: false default: true pyproject-toml: description: Path to the ``pyproject.toml`` file. Takes the same input as ``chango.config.get_chango_instance``. required: false # Using python naming here ensures that we can directly use this value below. default: None data: description: Additional JSON data to pass to the parameter ``data`` of ``chango.abc.ChanGo.build_from_github_event()``. Defaults to an instance of ``chango.action.ChanGoActionData``. required: false default: None github-token: description: GitHub Token or Personal Access Token (PAT) used to authenticate with GitHub. Defaults to the ``GITHUB_TOKEN``. required: false query-issue-types: description: Whether to query the issue types of the linked issues. Can only be used on organizations with issue types enabled. In this case, an organization scoped PAT is required. required: false default: false runs: using: composite steps: - name: Checkout action repository uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 env: ACTION_REPO: ${{ github.action_repository }} ACTION_REF: ${{ github.action_ref }} with: repository: ${{ env.ACTION_REPO }} ref: ${{ env.ACTION_REF }} path: ./.chango-action - name: Checkout target repository uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: # setting repo is required for PRs coming from forks repository: ${{ github.event.pull_request.head.repo.full_name }} ref: ${{ github.head_ref }} path: ./target-repo token: ${{ inputs.github-token || github.token }} - name: Set up Python id: setup-python uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: python-version: ${{ inputs.python-version }} cache: 'pip' cache-dependency-path: | **/requirements*.txt pyproject.toml - name: Install chango id: install-chango shell: bash run: | python -W ignore -m pip install ./.chango-action - name: Fetch Linked Issues & Parent PR (Custom GitHub Token) id: fetch-linked-issues-custom-token uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: github_token: ${{ inputs.github-token }} query_issue_types: ${{ inputs.query-issue-types }} if: ${{ env.github_token != null && env.github_token != '' }} with: github-token: ${{ env.github_token }} script: | const script = require('./.chango-action/action.js'); const query_issue_types = `${process.env.query_issue_types}`; await script({github, context, core, query_issue_types}); - name: Fetch Linked Issues & Parent PR (Default GitHub Token) id: fetch-linked-issues-default-token uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: github_token: ${{ inputs.github-token }} query_issue_types: ${{ inputs.query-issue-types }} if: ${{ env.github_token == null || env.github_token == '' }} with: script: | const script = require('./.chango-action/action.js'); const query_issue_types = `${process.env.query_issue_types}`; await script({github, context, core, query_issue_types}); - name: Create chango fragment id: create-chango-fragment uses: jannekem/run-python-script-action@bbfca66c612a28f3eeca0ae40e1f810265e2ea68 # v1.7 env: CUSTOM_OUTPUT: ${{ steps.fetch-linked-issues-custom-token.outputs.data }} DEFAULT_OUTPUT: ${{ steps.fetch-linked-issues-default-token.outputs.data }} with: script: | import base64 import subprocess import os from chango.config import get_chango_instance from chango.action import ChanGoActionData os.chdir('./target-repo') chango_instance = get_chango_instance(${{ inputs.pyproject-toml }}) null = None false = False true = True # Merge additionally fetched data into the data passed by the user output = ( ${{ toJson(env.CUSTOM_OUTPUT) }} or ${{ toJson(env.DEFAULT_OUTPUT) }} ) data = ( ${{ inputs.data }} or ChanGoActionData.model_validate_json(output) ) change_note = chango_instance.build_github_event_change_note( event=${{ toJson(github.event) }}, data=data ) should_commit = False if change_note is not None: path = chango_instance.write_change_note(change_note, version=None) last_commiter = subprocess.run( ["git", "log", "-1", "--format=%ce", "--", path], capture_output=True, text=True, check=True ).stdout.strip() # Do not override manual changes if last_commiter in ("github-actions[bot]", ""): should_commit = True set_output("should_commit", "true" if should_commit else "false") if should_commit: # b64 encoding avoits problems with new lines in the output encoded_content = base64.b64encode(change_note.to_string().encode()).decode() set_output("change_note_content", encoded_content) set_output("change_note_path", path.relative_to(os.getcwd()).as_posix()) else: set_output("change_note_content", "") set_output("change_note_path", "") - name: Commit and Push id: commit-and-push if: ${{ inputs.commit-and-push == 'true' && steps.create-chango-fragment.outputs.should_commit == 'true' }} uses: stefanzweifel/git-auto-commit-action@778341af668090896ca464160c2def5d1d1a3eb0 # v6.0.1 with: commit_message: "Add chango fragment for PR #${{ github.event.pull_request.number }}" repository: ./target-repo - name: Set Job Summary id: job-summary # Run only if the commit & push step failed. # We use this for PRs coming from forks. Here we can't easily push back to the PR branch or # create PR reviews, so we fail the job and set the job summary if: ${{ failure() && steps.create-chango-fragment.outputs.should_commit == 'true' }} env: CHANGE_NOTE_PATH: ${{ steps.create-chango-fragment.outputs.change_note_path }} CHANGE_NOTE_CONTENT: ${{ steps.create-chango-fragment.outputs.change_note_content }} uses: jannekem/run-python-script-action@bbfca66c612a28f3eeca0ae40e1f810265e2ea68 # v1.7 with: script: | import base64 from pathlib import Path file_path = Path("${{ env.CHANGE_NOTE_PATH }}") encoded_content = "${{ env.CHANGE_NOTE_CONTENT }}" decoded_content = base64.b64decode(encoded_content).decode() text = f""" Please create a [chango](https://chango.readthedocs.io/stable/) fragment for this PR. I suggest adding the following content to `{file_path}` in the repository: ```{file_path.suffix[1:]} {decoded_content} ``` """ set_summary(text) error("Chango fragment should be updated. See the job summary for details.") chango-0.6.0/changes/000077500000000000000000000000001507376567100144055ustar00rootroot00000000000000chango-0.6.0/changes/0.3.0_2025-02-04/000077500000000000000000000000001507376567100161535ustar00rootroot00000000000000chango-0.6.0/changes/0.3.0_2025-02-04/0064.TcBysgmjU9HuXcCNtAmXnp.toml000066400000000000000000000010021507376567100235320ustar00rootroot00000000000000features = "Add Classes :class:`~chango.concrete.BackwardCompatibleVersionScanner` and :class:`~chango.concrete.BackwardCompatibleChanGo` allowing to easily transition between change not formats" breaking = "Adapt expected exceptions in :class:`~chango.abc.ChanGo` and :class:`~chango.abc.VersionScanner` to use :class:`~chango.error.ChanGoError`" internal = "Switch to using :class:`~chango.concrete.sections.GitHubSectionChangeNote`" [[pull_requests]] uid = "64" author_uids = ["Bibo-Joshi"] closes_threads = [] chango-0.6.0/changes/0.3.0_2025-02-04/0066.GHbC3w7f2XSbFDmuVnDd6v.toml000066400000000000000000000001611507376567100232150ustar00rootroot00000000000000internal = "Bump Version to 0.3.0" [[pull_requests]] uid = "66" author_uids = ["Bibo-Joshi"] closes_threads = [] chango-0.6.0/changes/0.3.1_2025-02-06/000077500000000000000000000000001507376567100161565ustar00rootroot00000000000000chango-0.6.0/changes/0.3.1_2025-02-06/0068.GRvFbeQfEDdmzxGJPiz2w4.toml000066400000000000000000000001721507376567100234260ustar00rootroot00000000000000bugfixes = "Fix Faulty Build Configuration" [[pull_requests]] uid = "68" author_uids = ["Bibo-Joshi"] closes_threads = [] chango-0.6.0/changes/0.3.1_2025-02-06/0069.iKyCZ4ajCBQ6AFsCqPxWkA.toml000066400000000000000000000001611507376567100232430ustar00rootroot00000000000000internal = "Bump Version to 0.3.1" [[pull_requests]] uid = "69" author_uids = ["Bibo-Joshi"] closes_threads = [] chango-0.6.0/changes/0.3.2_2025-02-07/000077500000000000000000000000001507376567100161605ustar00rootroot00000000000000chango-0.6.0/changes/0.3.2_2025-02-07/0070.SoA9kPB4HQsbXfQtzbm4VS.toml000066400000000000000000000001641507376567100233260ustar00rootroot00000000000000internal = "Remove Debugging Content" [[pull_requests]] uid = "70" author_uids = ["Bibo-Joshi"] closes_threads = [] chango-0.6.0/changes/0.3.2_2025-02-07/0072.DzvfYPwDzGFU6SMMuSt6Ds.toml000066400000000000000000000002361507376567100233720ustar00rootroot00000000000000bugfixes = "Fix Link Returned by ``GitHubSectionChangeNot.get_thread_url``" [[pull_requests]] uid = "72" author_uids = ["Bibo-Joshi"] closes_threads = ["71"] chango-0.6.0/changes/0.3.2_2025-02-07/0073.RE4dFqzYC3aR9qvmVZv9og.toml000066400000000000000000000001611507376567100233660ustar00rootroot00000000000000internal = "Bump Version to 0.3.2" [[pull_requests]] uid = "73" author_uids = ["Bibo-Joshi"] closes_threads = [] chango-0.6.0/changes/0.4.0_2025-03-09/000077500000000000000000000000001507376567100161625ustar00rootroot00000000000000chango-0.6.0/changes/0.4.0_2025-03-09/0074.SSDViauEeBT7QHKRH6mFpc.toml000066400000000000000000000002361507376567100232100ustar00rootroot00000000000000internal = "Bump stefanzweifel/git-auto-commit-action from 5.0.1 to 5.1.0" [[pull_requests]] uid = "74" author_uids = ["dependabot[bot]"] closes_threads = [] chango-0.6.0/changes/0.4.0_2025-03-09/0075.JvzRqLCRaUSopaa8N8hgGM.toml000066400000000000000000000002201507376567100233730ustar00rootroot00000000000000internal = "Bump codecov/codecov-action from 5.3.1 to 5.4.0" [[pull_requests]] uid = "75" author_uids = ["dependabot[bot]"] closes_threads = [] chango-0.6.0/changes/0.4.0_2025-03-09/0076.eoGQRL4t4dv9NiRcsMcJNb.toml000066400000000000000000000002271507376567100233340ustar00rootroot00000000000000internal = "Bump pypa/gh-action-pypi-publish from 1.12.3 to 1.12.4" [[pull_requests]] uid = "76" author_uids = ["dependabot[bot]"] closes_threads = [] chango-0.6.0/changes/0.4.0_2025-03-09/0077.KHDp4yxBonQGJMNkv8VUCk.toml000066400000000000000000000002211507376567100233170ustar00rootroot00000000000000internal = "Bump github/codeql-action from 3.28.8 to 3.28.10" [[pull_requests]] uid = "77" author_uids = ["dependabot[bot]"] closes_threads = [] chango-0.6.0/changes/0.4.0_2025-03-09/0078.ivKibdXUti2zxb2YPvDZVE.toml000066400000000000000000000002141507376567100235130ustar00rootroot00000000000000internal = "Bump astral-sh/setup-uv from 5.1.0 to 5.3.0" [[pull_requests]] uid = "78" author_uids = ["dependabot[bot]"] closes_threads = [] chango-0.6.0/changes/0.4.0_2025-03-09/0079.Es29xTh5JD8tpRzfAsfXLp.toml000066400000000000000000000001751507376567100233650ustar00rootroot00000000000000features = "Add Workaround for PRs from Forks" [[pull_requests]] uid = "79" author_uids = ["Bibo-Joshi"] closes_threads = [] chango-0.6.0/changes/0.4.0_2025-03-09/0080.dyisXUTQu334rBnzvcFw86.toml000066400000000000000000000002431507376567100234150ustar00rootroot00000000000000features = "Check for Changes in ``DirectoryChanGo.build_github_event_change_note``" [[pull_requests]] uid = "80" author_uids = ["Bibo-Joshi"] closes_threads = [] chango-0.6.0/changes/0.4.0_2025-03-09/0081.eBzeTtXcD5VayQakSxYf4K.toml000066400000000000000000000001731507376567100234560ustar00rootroot00000000000000documentation = "Documentation Improvements" [[pull_requests]] uid = "81" author_uids = ["Bibo-Joshi"] closes_threads = [] chango-0.6.0/changes/0.4.0_2025-03-09/0082.YBQvoAgA35sMVTEkBg85jz.toml000066400000000000000000000001611507376567100232120ustar00rootroot00000000000000internal = "Bump Version to 0.4.0" [[pull_requests]] uid = "82" author_uids = ["Bibo-Joshi"] closes_threads = [] chango-0.6.0/changes/0.5.0_2025-08-02/000077500000000000000000000000001507376567100161615ustar00rootroot00000000000000chango-0.6.0/changes/0.5.0_2025-08-02/0083.5hTSdpXcfJSq8NGehybcyE.toml000066400000000000000000000002161507376567100234620ustar00rootroot00000000000000internal = "Bump actions/setup-python from 5.4.0 to 5.5.0" [[pull_requests]] uid = "83" author_uids = ["dependabot[bot]"] closes_threads = [] chango-0.6.0/changes/0.5.0_2025-08-02/0084.Q2soqnTqBjpdDCcosyRLJw.toml000066400000000000000000000002251507376567100236030ustar00rootroot00000000000000internal = "Bump codecov/test-results-action from 1.0.2 to 1.1.0" [[pull_requests]] uid = "84" author_uids = ["dependabot[bot]"] closes_threads = [] chango-0.6.0/changes/0.5.0_2025-08-02/0085.DnuHKLjMEHRjt9YJsGkdJ8.toml000066400000000000000000000002221507376567100232650ustar00rootroot00000000000000internal = "Bump github/codeql-action from 3.28.10 to 3.28.13" [[pull_requests]] uid = "85" author_uids = ["dependabot[bot]"] closes_threads = [] chango-0.6.0/changes/0.5.0_2025-08-02/0086.jSgwmus5gumz56ApSeCgSb.toml000066400000000000000000000002231507376567100235610ustar00rootroot00000000000000internal = "Bump actions/download-artifact from 4.1.8 to 4.2.1" [[pull_requests]] uid = "86" author_uids = ["dependabot[bot]"] closes_threads = [] chango-0.6.0/changes/0.5.0_2025-08-02/0087.U8AxZuKzxyCEHeizBWsVJq.toml000066400000000000000000000002211507376567100235250ustar00rootroot00000000000000internal = "Bump actions/upload-artifact from 4.6.0 to 4.6.2" [[pull_requests]] uid = "87" author_uids = ["dependabot[bot]"] closes_threads = [] chango-0.6.0/changes/0.5.0_2025-08-02/0088.2fQMEbF5EypoUZEGvJfVzH.toml000066400000000000000000000002111507376567100233030ustar00rootroot00000000000000internal = "Bump `pre-commit` Dependency Versions" [[pull_requests]] uid = "88" author_uids = ["pre-commit-ci[bot]"] closes_threads = [] chango-0.6.0/changes/0.5.0_2025-08-02/0089.mZsvnLwQtAfi89UWuUWJwa.toml000066400000000000000000000002161507376567100235610ustar00rootroot00000000000000internal = "Bump actions/setup-python from 5.5.0 to 5.6.0" [[pull_requests]] uid = "89" author_uids = ["dependabot[bot]"] closes_threads = [] chango-0.6.0/changes/0.5.0_2025-08-02/0090.ghFvSABn3u3QQ8zSYvmN5v.toml000066400000000000000000000002231507376567100233270ustar00rootroot00000000000000internal = "Bump actions/download-artifact from 4.2.1 to 4.3.0" [[pull_requests]] uid = "90" author_uids = ["dependabot[bot]"] closes_threads = [] chango-0.6.0/changes/0.5.0_2025-08-02/0091.4R9M49m56xiQcZo7NESjKi.toml000066400000000000000000000002361507376567100231000ustar00rootroot00000000000000internal = "Bump stefanzweifel/git-auto-commit-action from 5.1.0 to 5.2.0" [[pull_requests]] uid = "91" author_uids = ["dependabot[bot]"] closes_threads = [] chango-0.6.0/changes/0.5.0_2025-08-02/0092.dMTV9TeXd6WFBD3FqW6xHC.toml000066400000000000000000000002221507376567100230560ustar00rootroot00000000000000internal = "Bump github/codeql-action from 3.28.13 to 3.28.16" [[pull_requests]] uid = "92" author_uids = ["dependabot[bot]"] closes_threads = [] chango-0.6.0/changes/0.5.0_2025-08-02/0093.RT9fNQwRmEYWqKaTGXUkC5.toml000066400000000000000000000002141507376567100232530ustar00rootroot00000000000000internal = "Bump astral-sh/setup-uv from 5.3.0 to 6.0.1" [[pull_requests]] uid = "93" author_uids = ["dependabot[bot]"] closes_threads = [] chango-0.6.0/changes/0.5.0_2025-08-02/0094.HqLh2XekPzCa6CtfiaemH2.toml000066400000000000000000000002201507376567100233600ustar00rootroot00000000000000internal = "Bump codecov/codecov-action from 5.4.0 to 5.4.3" [[pull_requests]] uid = "94" author_uids = ["dependabot[bot]"] closes_threads = [] chango-0.6.0/changes/0.5.0_2025-08-02/0096.Y82ibb5y77Xa38xdk6QVC5.toml000066400000000000000000000002151507376567100230440ustar00rootroot00000000000000internal = "Bump github/codeql-action from 3.28.16 to 3.28.18" [[pull_requests]] uid = "96" author_uids = ["dependabot"] closes_threads = [] chango-0.6.0/changes/0.5.0_2025-08-02/0097.ZgS9STPa6TvPf3fwHnLsWk.toml000066400000000000000000000002251507376567100233740ustar00rootroot00000000000000internal = "Bump codecov/test-results-action from 1.1.0 to 1.1.1" [[pull_requests]] uid = "97" author_uids = ["dependabot[bot]"] closes_threads = [] chango-0.6.0/changes/0.5.0_2025-08-02/0098.BYNC7m8Dd3MhwhBsNCFvFD.toml000066400000000000000000000001561507376567100231410ustar00rootroot00000000000000 internal = "Adapt Test Suite to ``click`` Update" [[pull_requests]] uid = "98" author_uids = ["Bibo-Joshi"] chango-0.6.0/changes/0.5.0_2025-08-02/0099.GZdExxNVgV6VgKDSkPM54k.toml000066400000000000000000000001541507376567100232510ustar00rootroot00000000000000internal = "Implement PEP 735 Dependency Groups" [[pull_requests]] uid = "99" author_uids = ["Bibo-Joshi"] chango-0.6.0/changes/0.5.0_2025-08-02/0100.SRGBCyydFKXjpASJkwcTE9.toml000066400000000000000000000002321507376567100233100ustar00rootroot00000000000000internal = "Bump stefanzweifel/git-auto-commit-action from 5.2.0 to 6.0.1" [[pull_requests]] uid = "100" author_uids = ["dependabot"] closes_threads = [] chango-0.6.0/changes/0.5.0_2025-08-02/0101.5K88bbhxxSfVEQtqdpKEhT.toml000066400000000000000000000002051507376567100234000ustar00rootroot00000000000000internal = "Bump `pre-commit` Dependency Versions" [[pull_requests]] uid = "101" author_uids = ["pre-commit-ci"] closes_threads = [] chango-0.6.0/changes/0.5.0_2025-08-02/0102.azst9tBkpw7KRsJaVxiksm.toml000066400000000000000000000002261507376567100237050ustar00rootroot00000000000000internal = "Update furo requirement from ~=2024.8 to >=2024.8,<2026.0" [[pull_requests]] uid = "102" author_uids = ["dependabot"] closes_threads = [] chango-0.6.0/changes/0.5.0_2025-08-02/0103.TD8M4FqYfurPdeVfrLZB8H.toml000066400000000000000000000002101507376567100232450ustar00rootroot00000000000000internal = "Bump astral-sh/setup-uv from 6.0.1 to 6.4.3" [[pull_requests]] uid = "103" author_uids = ["dependabot"] closes_threads = [] chango-0.6.0/changes/0.5.0_2025-08-02/0104.CMrnZnJSf5s3nG4EtFdUSL.toml000066400000000000000000000002141507376567100232430ustar00rootroot00000000000000internal = "Bump github/codeql-action from 3.29.2 to 3.29.5" [[pull_requests]] uid = "104" author_uids = ["dependabot"] closes_threads = [] chango-0.6.0/changes/0.5.0_2025-08-02/0105.8uQ6mE8uwaGMVs59uFthEg.toml000066400000000000000000000002301507376567100233050ustar00rootroot00000000000000internal = "Bump sigstore/gh-action-sigstore-python from 3.0.0 to 3.0.1" [[pull_requests]] uid = "105" author_uids = ["dependabot"] closes_threads = [] chango-0.6.0/changes/0.5.0_2025-08-02/0108.nQN2S5vGw2tSVZB5TMbd4m.toml000066400000000000000000000002371507376567100231710ustar00rootroot00000000000000features = "Use ``git add`` in ``Chango.write_change_note`` if available" [[pull_requests]] uid = "108" author_uids = ["Bibo-Joshi"] closes_threads = ["106"] chango-0.6.0/changes/0.5.0_2025-08-02/0110.hgqm4UVHANHMSHpjFvUhmk.toml000066400000000000000000000002331507376567100234110ustar00rootroot00000000000000features = "Allow Several ``author_uids`` in ``sections.PullRequest``" [[pull_requests]] uid = "110" author_uids = ["Bibo-Joshi"] closes_threads = ["107"] chango-0.6.0/changes/0.6.0_2025-10-15/000077500000000000000000000000001507376567100161575ustar00rootroot00000000000000chango-0.6.0/changes/0.6.0_2025-10-15/0112.cgiNSBBq9pMfuYMjdPQxxi.toml000066400000000000000000000002131507376567100235210ustar00rootroot00000000000000internal = "Bump actions/checkout from 4.2.2 to 5.0.0" [[pull_requests]] uid = "112" author_uids = ["dependabot[bot]"] closes_threads = [] chango-0.6.0/changes/0.6.0_2025-10-15/0113.SVjGfdA6khFqpsWSG7vGoQ.toml000066400000000000000000000002211507376567100233660ustar00rootroot00000000000000internal = "Bump codecov/codecov-action from 5.4.3 to 5.5.0" [[pull_requests]] uid = "113" author_uids = ["dependabot[bot]"] closes_threads = [] chango-0.6.0/changes/0.6.0_2025-10-15/0114.ZCwTQtJH9E5DEqmfYoytsw.toml000066400000000000000000000002151507376567100234530ustar00rootroot00000000000000internal = "Bump astral-sh/setup-uv from 6.4.3 to 6.6.0" [[pull_requests]] uid = "114" author_uids = ["dependabot[bot]"] closes_threads = [] chango-0.6.0/changes/0.6.0_2025-10-15/0115.oUe2zCXhhXjxU5moKHBTzM.toml000066400000000000000000000002221507376567100234340ustar00rootroot00000000000000internal = "Bump github/codeql-action from 3.29.7 to 3.29.11" [[pull_requests]] uid = "115" author_uids = ["dependabot[bot]"] closes_threads = [] chango-0.6.0/changes/0.6.0_2025-10-15/0116.Eh78BRbWmwHorv6iU5Hpmt.toml000066400000000000000000000002241507376567100233750ustar00rootroot00000000000000internal = "Bump actions/download-artifact from 4.3.0 to 5.0.0" [[pull_requests]] uid = "116" author_uids = ["dependabot[bot]"] closes_threads = [] chango-0.6.0/changes/0.6.0_2025-10-15/0117.awekHXqm5tgCL8uCNDA2TH.toml000066400000000000000000000002301507376567100232430ustar00rootroot00000000000000internal = "Bump pypa/gh-action-pypi-publish from 1.12.4 to 1.13.0" [[pull_requests]] uid = "117" author_uids = ["dependabot[bot]"] closes_threads = [] chango-0.6.0/changes/0.6.0_2025-10-15/0118.9Bi9Ai5kfgiRFdwEFkcggc.toml000066400000000000000000000002171507376567100233550ustar00rootroot00000000000000internal = "Bump actions/setup-python from 5.6.0 to 6.0.0" [[pull_requests]] uid = "118" author_uids = ["dependabot[bot]"] closes_threads = [] chango-0.6.0/changes/0.6.0_2025-10-15/0119.eBhPtNjV43TeX9qyP29LSC.toml000066400000000000000000000002221507376567100231510ustar00rootroot00000000000000internal = "Bump github/codeql-action from 3.29.11 to 3.30.5" [[pull_requests]] uid = "119" author_uids = ["dependabot[bot]"] closes_threads = [] chango-0.6.0/changes/0.6.0_2025-10-15/0120.T9jW2fhjasWm3LvMRWwCMo.toml000066400000000000000000000002151507376567100233730ustar00rootroot00000000000000internal = "Bump astral-sh/setup-uv from 6.6.0 to 6.8.0" [[pull_requests]] uid = "120" author_uids = ["dependabot[bot]"] closes_threads = [] chango-0.6.0/changes/0.6.0_2025-10-15/0121.JF9pcVdYUvDYWS9rcFD7k7.toml000066400000000000000000000002201507376567100231710ustar00rootroot00000000000000internal = "Bump actions/github-script from 7.0.1 to 8.0.0" [[pull_requests]] uid = "121" author_uids = ["dependabot[bot]"] closes_threads = [] chango-0.6.0/changes/0.6.0_2025-10-15/0122.oSTgnyvMerJfmnevV8ShrA.toml000066400000000000000000000002121507376567100236510ustar00rootroot00000000000000internal = "Bump `pre-commit` Dependency Versions" [[pull_requests]] uid = "122" author_uids = ["pre-commit-ci[bot]"] closes_threads = [] chango-0.6.0/changes/0.6.0_2025-10-15/0123.e5oWFfVz2QMhkrmRUf22bs.toml000066400000000000000000000002251507376567100233620ustar00rootroot00000000000000features = "Use ``github-token`` also for target repository checkout" [[pull_requests]] uid = "123" author_uids = ["Bibo-Joshi"] closes_threads = [] chango-0.6.0/changes/0.6.0_2025-10-15/0128.8x66cSy8XzoJikGdhs9C5N.toml000066400000000000000000000002041507376567100232670ustar00rootroot00000000000000internal = "Update Dependabot Schedule to Quarterly" [[pull_requests]] uid = "128" author_uids = ["Bibo-Joshi"] closes_threads = [] chango-0.6.0/changes/0.6.0_2025-10-15/0129.L2Zsan69cg5qNusos2xj9G.toml000066400000000000000000000002331507376567100233700ustar00rootroot00000000000000documentation = "Skip Inherited Members for ``chango.config.ChanGoConfig``" [[pull_requests]] uid = "129" author_uids = ["Bibo-Joshi"] closes_threads = [] chango-0.6.0/changes/0.6.0_2025-10-15/0130.bvGQNqJvcFSBcAAa2fMmwB.toml000066400000000000000000000001361507376567100233130ustar00rootroot00000000000000internal = "Bump Version to 0.6.0" [[pull_requests]] uid = "130" author_uids = ["Bibo-Joshi"] chango-0.6.0/changes/config.py000066400000000000000000000044431507376567100162310ustar00rootroot00000000000000# noqa: INP001 # SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT from chango.concrete import ( BackwardCompatibleChanGo, BackwardCompatibleVersionScanner, CommentChangeNote, CommentVersionNote, DirectoryChanGo, DirectoryVersionScanner, HeaderVersionHistory, ) from chango.concrete.sections import GitHubSectionChangeNote, Section, SectionVersionNote from chango.constants import MarkupLanguage class RstChangeNote(CommentChangeNote): MARKUP = MarkupLanguage.RESTRUCTUREDTEXT legacy_version_scanner = DirectoryVersionScanner( base_directory="./legacy", unreleased_directory="unreleased" ) legacy_chango_instance = DirectoryChanGo( change_note_type=RstChangeNote, version_note_type=CommentVersionNote, version_history_type=HeaderVersionHistory, scanner=legacy_version_scanner, ) new_version_scanner = DirectoryVersionScanner( base_directory=".", unreleased_directory="unreleased" ) ChangoSectionChangeNote = GitHubSectionChangeNote.with_sections( [ Section(uid="highlights", title="Highlights", sort_order=0), Section(uid="breaking", title="Breaking Changes", sort_order=1), Section(uid="security", title="Security Changes", sort_order=2), Section(uid="deprecations", title="Deprecations", sort_order=3), Section(uid="features", title="New Features", sort_order=4), Section(uid="bugfixes", title="Bug Fixes", sort_order=5), Section(uid="dependencies", title="Dependencies", sort_order=6), Section(uid="other", title="Other Changes", sort_order=7), Section(uid="documentation", title="Documentation", sort_order=8), Section(uid="internal", title="Internal Changes", sort_order=9), ] ) ChangoSectionChangeNote.OWNER = "Bibo-Joshi" ChangoSectionChangeNote.REPOSITORY = "chango" new_chango_instance = DirectoryChanGo( change_note_type=ChangoSectionChangeNote, version_note_type=SectionVersionNote, version_history_type=HeaderVersionHistory, scanner=new_version_scanner, ) version_scanner = BackwardCompatibleVersionScanner( scanners=(new_version_scanner, legacy_version_scanner) ) chango_instance = BackwardCompatibleChanGo( main_instance=new_chango_instance, legacy_instances=(legacy_chango_instance,) ) chango-0.6.0/changes/legacy/000077500000000000000000000000001507376567100156515ustar00rootroot00000000000000chango-0.6.0/changes/legacy/0.0.1_2024-12-13/000077500000000000000000000000001507376567100174155ustar00rootroot00000000000000chango-0.6.0/changes/legacy/0.0.1_2024-12-13/0000-initial-commit.BZybp4GJjwK4Ab4qNqTFSg.rst000066400000000000000000000000161507376567100272650ustar00rootroot00000000000000Initial Commitchango-0.6.0/changes/legacy/0.0.1_2024-12-13/0001-initial-functionality.m3V6JBPdTfWyHuTwHkfHBq.rst000066400000000000000000000001211507376567100307360ustar00rootroot00000000000000Add Initial Functionality \(`#1 `_\)chango-0.6.0/changes/legacy/0.0.1_2024-12-13/0002-setup-rtd.o3viqPUxkoMFovcBHqeDxu.rst000066400000000000000000000001301507376567100266520ustar00rootroot00000000000000Set Up Initial ReadTheDocs Build \(`#2 `_\)0004-typing-chango-and-chango-abc.Tmar95x5Tr6ZGSbEQvEXtW.rst000066400000000000000000000001561507376567100316050ustar00rootroot00000000000000chango-0.6.0/changes/legacy/0.0.1_2024-12-13Add Types to Docstrings: ``chango`` and ``chango.abc`` \(`#4 `_\)chango-0.6.0/changes/legacy/0.0.1_2024-12-13/0005-typing-chango-concrete.jMidPdt4DUaF5gVZkh66z7.rst000066400000000000000000000001441507376567100307110ustar00rootroot00000000000000Add Types To Docstrings: ``chango.concrete`` \(`#5 `_\)chango-0.6.0/changes/legacy/0.0.1_2024-12-13/0006-sphinx-cli-docs.UofHZUusZXEa5VaK4KxAUJ.rst000066400000000000000000000001271507376567100273730ustar00rootroot00000000000000Add CLI to Sphinx Documentation \(`#6 `_\)chango-0.6.0/changes/legacy/0.0.1_2024-12-13/0007-unified-cli-docs.VjtnK2BYdHuTWmsddkUKt9.rst000066400000000000000000000001731507376567100276760ustar00rootroot00000000000000Set Up Unified Formatting for CLI Help Texts and HTML Documentation \(`#7 `_\)chango-0.6.0/changes/legacy/0.0.1_2024-12-13/0008-test-chango-package.7HvwBs9ek8NcC84mVvzZuY.rst000066400000000000000000000001571507376567100302570ustar00rootroot00000000000000Add Unit Tests for Direct Members of ``chango`` Package \(`#8 `_\)chango-0.6.0/changes/legacy/0.0.1_2024-12-13/0009-pytest-xdist-randomly.BChKtaJJzFKSq9JwhD7ERf.rst000066400000000000000000000001541507376567100306310ustar00rootroot00000000000000Make Use of ``pytest-xdist`` and ``pytest-randomly`` \(`#9 `_\)chango-0.6.0/changes/legacy/0.0.1_2024-12-13/0010-cache-dependencies.HVwwgMwYamt7HArepQB4Zy.rst000066400000000000000000000001221507376567100302500ustar00rootroot00000000000000Cache Dependencies in CI \(`#10 `_\)chango-0.6.0/changes/legacy/0.0.1_2024-12-13/0011-py-313.cuLHnxtTfopTMve4pBSYsx.rst000066400000000000000000000001301507376567100256620ustar00rootroot00000000000000Add Python 3.13 to Test Matrix \(`#11 `_\)chango-0.6.0/changes/legacy/0.0.1_2024-12-13/0012-code-coverage.88KDR4L6pPagMkmtEvoruy.rst000066400000000000000000000001341507376567100271700ustar00rootroot00000000000000Improve Coverage of Existing Tests \(`#12 `_\)chango-0.6.0/changes/legacy/0.0.1_2024-12-13/0013-test-changenote.925fSLA2VHU9GrGsWB74mR.rst000066400000000000000000000001461507376567100270710ustar00rootroot00000000000000Add Unit Tests for ``chango.abc.ChangeNote`` \(`#13 `_\)chango-0.6.0/changes/legacy/0.0.1_2024-12-13/0014-code-coverage.cC2TZxdTdu2teHCpuPeWdT.rst000066400000000000000000000001301507376567100272220ustar00rootroot00000000000000Improve Coverage Configuration \(`#14 `_\)chango-0.6.0/changes/legacy/0.0.1_2024-12-13/0015-concrete-cn-to-bytes.GYHPs3M85VyzXJu4GwFAEU.rst000066400000000000000000000001371507376567100301700ustar00rootroot00000000000000Make ``ChangeNote.to_bytes`` Concrete \(`#15 `_\)chango-0.6.0/changes/legacy/0.0.1_2024-12-13/0016-test-versionnote.mu2DEyxvUykkB8UufSSKCr.rst000066400000000000000000000001471507376567100301330ustar00rootroot00000000000000Add Unit Tests for ``chango.abc.VersionNote`` \(`#16 `_\)chango-0.6.0/changes/legacy/0.0.1_2024-12-13/0017-test-versionhistory.asPigJEkfxZN7iV7LFM4qf.rst000066400000000000000000000001521507376567100305070ustar00rootroot00000000000000Add Unit Tests for ``chango.abc.VersionHistory`` \(`#17 `_\)0018-test-comment-change-version-note.TcBN6GG7frXrSunUdHdDQR.rst000066400000000000000000000001741507376567100325740ustar00rootroot00000000000000chango-0.6.0/changes/legacy/0.0.1_2024-12-13Add Unit Tests for ``chango.concrete.Comment(Change|Version)Note`` \(`#18 `_\)0019-test-headerversionhistory.SRcgTedALXS4E4NJT4E6hY.rst000066400000000000000000000002051507376567100313040ustar00rootroot00000000000000chango-0.6.0/changes/legacy/0.0.1_2024-12-13Remove Redundant Logic From and Add Unit Tests for ``HeaderVersionHistory`` \(`#19 `_\)chango-0.6.0/changes/legacy/0.0.1_2024-12-13/0020-test-dirversionscanner.66Psx4iBQs599MJDAyjzsf.rst000066400000000000000000000001501507376567100310160ustar00rootroot00000000000000Add Unit Tests for ``DirectoryVersionScanner`` \(`#20 `_\)chango-0.6.0/changes/legacy/0.0.1_2024-12-13/0021-test-versionscanner.HdpQp3ZYr4jtGguXYDbYGp.rst000066400000000000000000000001371507376567100305430ustar00rootroot00000000000000Add Unit Tests for ``VersionScanner`` \(`#21 `_\)chango-0.6.0/changes/legacy/0.0.1_2024-12-13/0022-test-dirchango.9y3LnV8tcXMLgMf7vLLt93.rst000066400000000000000000000001401507376567100272000ustar00rootroot00000000000000Add Unit Tests for ``DirectoryChanGo`` \(`#22 `_\)chango-0.6.0/changes/legacy/0.0.1_2024-12-13/0023-remove-ccn-to-bytes.CjkEXbz8E2cs77FeGGUkYs.rst000066400000000000000000000001511507376567100301170ustar00rootroot00000000000000Remove Redundant ``CommentChangeNote.to_bytes`` \(`#23 `_\)chango-0.6.0/changes/legacy/0.0.1_2024-12-13/0024-test-chango.dBYXtMzVdxvJWob9zJFHMH.rst000066400000000000000000000001271507376567100267400ustar00rootroot00000000000000Add Unit Tests for ``ChanGo`` \(`#24 `_\)chango-0.6.0/changes/legacy/0.0.1_2024-12-13/0025-cli-testing-setup.KPbusABWcd9LXBVuGHdHrn.rst000066400000000000000000000001331507376567100300010ustar00rootroot00000000000000Initial Setup for Testing the CLI \(`#25 `_\)chango-0.6.0/changes/legacy/0.0.1_2024-12-13/0026-setup-readme-changelog.hk6pyRauQPuiZU8BL74eir.rst000066400000000000000000000001421507376567100310200ustar00rootroot00000000000000Initial Readme Content and Changelog Setup (`#26 `_)chango-0.6.0/changes/legacy/0.0.1_2024-12-13/0027.GgXyTboGwEdpssmQ6BzBBv.rst000066400000000000000000000001641507376567100246310ustar00rootroot00000000000000Create Change Notes from GitHub Events and Add GitHub Action (`#27 `_)chango-0.6.0/changes/legacy/0.0.1_2024-12-13/0028.bbYDowUgFJLq29mAH5yHWW.rst000066400000000000000000000001141507376567100243370ustar00rootroot00000000000000Add Sphinx Extension (`#28 `_)chango-0.6.0/changes/legacy/0.0.1_2024-12-13/0029.4M2mRZnKZV2uuAhV7nUavP.rst000066400000000000000000000001331507376567100243750ustar00rootroot00000000000000Add Public ``chango.config`` Module (`#29 `_)chango-0.6.0/changes/legacy/0.0.1_2024-12-13/0033.PuD8cwmt8yXbRRYSwou4Cw.rst000066400000000000000000000002231507376567100246070ustar00rootroot00000000000000Make Parameter ``base_directory`` of ``DirectoryVersionScanner`` Relative to Calling Module (`#33 `_)chango-0.6.0/changes/legacy/0.0.1_2024-12-13/0034.P2N42T4MjVPDtppCnX982X.rst000066400000000000000000000001401507376567100240750ustar00rootroot00000000000000Add ``VersionScanner.invalidate_caches`` (`#34 `_)chango-0.6.0/changes/legacy/0.0.1_2024-12-13/0035.8K3pz7w7SVxmxXBU5kkFxx.rst000066400000000000000000000001161507376567100245050ustar00rootroot00000000000000Increase Code Coverage (`#35 `_)chango-0.6.0/changes/legacy/0.0.1_2024-12-13/0036.W5nhsW85RpikMLLXPTjqak.rst000066400000000000000000000001161507376567100245020ustar00rootroot00000000000000Add Unit Tests for CLI (`#36 `_)chango-0.6.0/changes/legacy/0.0.1_2024-12-13/0037.hfNakUZfHjCbfW2bKcYy8s.rst000066400000000000000000000001571507376567100245410ustar00rootroot00000000000000Add Dependabot and Security Scanning for GitHub Actions (`#37 `_)chango-0.6.0/changes/legacy/0.0.1_2024-12-13/0038.5PGfDhN6AHYn652rJL9zsG.rst000066400000000000000000000001511507376567100241200ustar00rootroot00000000000000Update ``sphinx`` requirement from ~=8.0 to ~=8.1 (`#38 `_)chango-0.6.0/changes/legacy/0.0.1_2024-12-13/0039.Ndsowkz79YjKqQbSw5igEo.rst000066400000000000000000000001341507376567100246430ustar00rootroot00000000000000Add Workflows for Automated Releases (`#39 `_)chango-0.6.0/changes/legacy/0.0.1_2024-12-13/0041.a4SKb8Y96PJnwn5w4p5q6m.rst000066400000000000000000000001151507376567100242410ustar00rootroot00000000000000Bump Version to 0.0.1 (`#41 `_)chango-0.6.0/changes/legacy/0.1.0_2024-12-15/000077500000000000000000000000001507376567100174175ustar00rootroot00000000000000chango-0.6.0/changes/legacy/0.1.0_2024-12-15/0043.7ahWkgTKUq9oGCdTEFWZDA.rst000066400000000000000000000001511507376567100243030ustar00rootroot00000000000000Use ``git mv`` in ``ChanGo.release`` if Available (`#43 `_)chango-0.6.0/changes/legacy/0.1.0_2024-12-15/0044.2VnTznJa6NGhrRXPybAeYL.rst000066400000000000000000000001541507376567100244600ustar00rootroot00000000000000Set up Action Metadata for Publishing on Marketplace (`#44 `_)chango-0.6.0/changes/legacy/0.1.0_2024-12-15/0045.JMWfFMSUzojm9ryKootyES.rst000066400000000000000000000001151507376567100246550ustar00rootroot00000000000000Bump Version to 0.1.0 (`#45 `_)chango-0.6.0/changes/legacy/0.2.0_2025-02-03/000077500000000000000000000000001507376567100174155ustar00rootroot00000000000000chango-0.6.0/changes/legacy/0.2.0_2025-02-03/0046.5AzP9qas7MKFJbG7NMx7xY.rst000066400000000000000000000001601507376567100242430ustar00rootroot00000000000000FIx Sorting Direction in ``HeaderVersionHistory.render`` (`#46 `_)chango-0.6.0/changes/legacy/0.2.0_2025-02-03/0047.P4PDLHhrhTpFLoEGpqF6mn.rst000066400000000000000000000001301507376567100244000ustar00rootroot00000000000000Add ``chango.concrete.sections`` (`#47 `_)chango-0.6.0/changes/legacy/0.2.0_2025-02-03/0050.NqketU7yMJMv2bqENaJ9Lm.rst000066400000000000000000000002401507376567100244340ustar00rootroot00000000000000Implement ``GitHubSectionChangeNote.build_from_github_event`` And Several Adaptions on GitHub Automation (`#50 `_)chango-0.6.0/changes/legacy/0.2.0_2025-02-03/0052.CRdp9a3jnNZ88sZhC6cLQZ.rst000066400000000000000000000001431507376567100242670ustar00rootroot00000000000000Bump astral-sh/setup-uv from 4.2.0 to 5.1.0 (`#52 `_)chango-0.6.0/changes/legacy/0.2.0_2025-02-03/0053.Je97JS6nDpaxGyUBm7bMcN.rst000066400000000000000000000001501507376567100243200ustar00rootroot00000000000000Bump actions/upload-artifact from 4.4.3 to 4.5.0 (`#53 `_)chango-0.6.0/changes/legacy/0.2.0_2025-02-03/0054.TrsKmDWRLnDCBqdnEpDR35.rst000066400000000000000000000001471507376567100243460ustar00rootroot00000000000000Bump github/codeql-action from 3.27.9 to 3.28.0 (`#54 `_)chango-0.6.0/changes/legacy/0.2.0_2025-02-03/0055.CJgKvRKxsqqaii4poWJkUf.rst000066400000000000000000000001471507376567100246740ustar00rootroot00000000000000Bump codecov/codecov-action from 5.1.1 to 5.1.2 (`#55 `_)chango-0.6.0/changes/legacy/0.2.0_2025-02-03/0056.KTxZvJ4ULXw5x6jiRh6XdS.rst000066400000000000000000000001351507376567100244300ustar00rootroot00000000000000Bump `pre-commit` Dependency Versions (`#56 `_)chango-0.6.0/changes/legacy/0.2.0_2025-02-03/0057.SdJy36cMqNSkWby4biMDz6.rst000066400000000000000000000001631507376567100244220ustar00rootroot00000000000000Don't Enforce One Required Section in ``SectionChangeNote`` (`#57 `_)chango-0.6.0/changes/legacy/0.2.0_2025-02-03/0058.VAr8EJ6caZfdNxfAJA2PBs.rst000066400000000000000000000001601507376567100242530ustar00rootroot00000000000000Fix Nesting Bug in Parsing ``rich`` to ``sphinx`` Syntax (`#58 `_)chango-0.6.0/changes/legacy/0.2.0_2025-02-03/0059.6EttYDTKaRc6tYy8rSm3zM.rst000066400000000000000000000001471507376567100244510ustar00rootroot00000000000000Bump github/codeql-action from 3.28.0 to 3.28.8 (`#59 `_)chango-0.6.0/changes/legacy/0.2.0_2025-02-03/0060.bx5A2GKKajABEoSvTYKswM.rst000066400000000000000000000001541507376567100243430ustar00rootroot00000000000000Bump codecov/test-results-action from 1.0.1 to 1.0.2 (`#60 `_)chango-0.6.0/changes/legacy/0.2.0_2025-02-03/0061.fjkUvLFx93J4dDUtDK2K43.rst000066400000000000000000000001451507376567100241510ustar00rootroot00000000000000Bump actions/setup-python from 5.3.0 to 5.4.0 (`#61 `_)chango-0.6.0/changes/legacy/0.2.0_2025-02-03/0062.EwgB3Gi4tDUzaeg3WGVVFT.rst000066400000000000000000000001501507376567100243170ustar00rootroot00000000000000Bump actions/upload-artifact from 4.5.0 to 4.6.0 (`#62 `_)chango-0.6.0/changes/legacy/0.2.0_2025-02-03/0063.jyzznhjJCNU6ryKCue8gEX.rst000066400000000000000000000001471507376567100246330ustar00rootroot00000000000000Bump codecov/codecov-action from 5.1.2 to 5.3.1 (`#63 `_)chango-0.6.0/changes/legacy/0.2.0_2025-02-03/0065.4PfpUNtA3vTvkKtGF9rrUL.rst000066400000000000000000000001151507376567100244460ustar00rootroot00000000000000Bump Version to 0.2.0 (`#65 `_)chango-0.6.0/changes/legacy/unreleased/000077500000000000000000000000001507376567100200005ustar00rootroot00000000000000chango-0.6.0/changes/legacy/unreleased/.gitkeep000066400000000000000000000000001507376567100214170ustar00rootroot00000000000000chango-0.6.0/changes/unreleased/000077500000000000000000000000001507376567100165345ustar00rootroot00000000000000chango-0.6.0/changes/unreleased/.gitkeep000066400000000000000000000000001507376567100201530ustar00rootroot00000000000000chango-0.6.0/codecov.yml000066400000000000000000000001061507376567100151370ustar00rootroot00000000000000comment: false coverage: status: project: true patch: true chango-0.6.0/docs/000077500000000000000000000000001507376567100137255ustar00rootroot00000000000000chango-0.6.0/docs/Makefile000066400000000000000000000011761507376567100153720ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = source BUILDDIR = build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) chango-0.6.0/docs/__init__.py000066400000000000000000000001601507376567100160330ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT chango-0.6.0/docs/auxil/000077500000000000000000000000001507376567100150475ustar00rootroot00000000000000chango-0.6.0/docs/auxil/__init__.py000066400000000000000000000001601507376567100171550ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT chango-0.6.0/docs/auxil/rich_to_rst.py000066400000000000000000000104351507376567100177430ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT from rich.errors import StyleSyntaxError from rich.style import Style from rich.text import Span, Text class RichConverter: """Minimal effort implementation of converting text with python-rich formatting options to rST formatted text.""" def __init__(self, rich_text: str) -> None: self.rich_text = rich_text self.spans: list[Span] = [] self.plain_text: str = "" def parse_rich_text(self) -> None: """Reads the rich-formatted text. Parses it to plain text + a list of formatting entities. Those are then stored in :attr:`spans` and :attr:`plain_text`. """ text = Text.from_markup(self.rich_text) # Extract plain text self.plain_text = text.plain # Extract formatting data self.spans.extend(text.spans) @staticmethod def _process_rich_style(plain_text: str, style: Style) -> str: """Given a text and a rich Style object, return the corresponding rst string. If the style is not known, the plain text is returned unchanged. """ rst_text = plain_text if style.italic: rst_text = f"*{rst_text}*" if style.bold: rst_text = f"**{rst_text}**" if style.link: if not style.link.startswith("https://chango.readthedocs.io/"): rst_text = f"`{rst_text} <{style.link}>`_" else: try: reference = style.link.rsplit("#", 1)[1] except IndexError: reference = style.link.rsplit("/", 1)[1].removesuffix(".html") rst_text = f":attr:`{reference}`" return rst_text @staticmethod def _process_rich_string(plain_text: str, style: str) -> str: """Given a text and a rich style in string representation, return the corresponding rst string. If the style is not known, the plain text is returned unchanged. """ rst_text = plain_text if style == "code": rst_text = f"``{rst_text}``" return rst_text @classmethod def process_rich_span(cls, plain_text: str, style: str | Style) -> str: """Given a plain text and a rich style in string representation, return the corresponding rst formatted string. """ if isinstance(style, str): try: style_obj: Style | str = Style.parse(style) except StyleSyntaxError: style_obj = style else: style_obj = style if isinstance(style_obj, Style): return cls._process_rich_style(plain_text, style_obj) return cls._process_rich_string(plain_text, style_obj) @classmethod def _render_rst_text(cls, plain_text: str, spans: list[Span], offset: int = 0) -> str: # Implementation heavily borrows from # https://github.com/python-telegram-bot/python-telegram-bot/blob/… # …5ab82a9c2b09286b66777ad0345b1abf3dedf131/telegram/_message.py#L4487-L4574 rst_text = "" last_offset = 0 sorted_spans = sorted(spans, key=lambda s: s.start) parsed_spans = [] for span in sorted_spans: if span in parsed_spans: continue span_start = span.start - offset span_end = span.end - offset span_text = plain_text[span_start:span_end] nested_spans = [ s for s in sorted_spans if s.start >= span_start and s.end <= span_end and s != span ] parsed_spans.extend(nested_spans) if nested_spans: span_text = cls._render_rst_text( plain_text=span_text, spans=nested_spans, offset=span_start ) insert = cls.process_rich_span(span_text, span.style) rst_text += plain_text[last_offset:span_start] + insert last_offset = span_start + (span.end - span.start) rst_text += plain_text[last_offset:] return rst_text def render_rst_text(self) -> str: """Render :attr:`plain_text` with rst formatting based on :attr:`spans`.""" return self._render_rst_text(self.plain_text, self.spans) chango-0.6.0/docs/generate_files.py000066400000000000000000000052261507376567100172600ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT import importlib import logging from pathlib import Path from types import ModuleType from jinja2 import Template logger = logging.getLogger(__name__) CLASS_TEMPLATE = Template( """{{ class_name }} {{ "=" * class_name|length }} .. autoclass:: {{ fqn }} :members: :show-inheritance: """ ) MODULE_TEMPLATE = Template( """{{ module_name }} {{ "=" * module_name|length }} .. automodule:: {{ fqn }} .. toctree:: :titlesonly: {% for child in children -%} {{ child }} {% endfor -%} """ ) def write_text(file_path: Path, content: str) -> None: if file_path.exists(): logger.debug("File %s already exists, skipping", file_path) return file_path.write_text(content, encoding="utf-8") def write_class_rst_file(class_name: str, fqn: str, output_dir: Path) -> Path: file_name = f"{fqn.lower()}.rst" file_path = output_dir / file_name write_text(file_path, CLASS_TEMPLATE.render(class_name=class_name, fqn=fqn)) return file_path def write_module_rst_file( module_name: str, fqn: str, children: list[Path], output_dir: Path ) -> Path: file_name = f"{fqn.lower()}.rst" file_path = output_dir / file_name write_text( file_path, MODULE_TEMPLATE.render( module_name=module_name, fqn=fqn, children=sorted(child.stem for child in children) ), ) return file_path def create_rst_files(module_name: str, base_path: Path) -> list[Path]: try: module = importlib.import_module(module_name) except ImportError: logger.error("Could not import module %s", module_name) return [] if not hasattr(module, "__all__"): return [] created_files: list[Path] = [] for name in module.__all__: fqn = f"{module_name}.{name}" try: member = getattr(module, name) except AttributeError: continue if not isinstance(member, type | ModuleType): # Rest is covered on module level continue if isinstance(member, type): created_files.append(write_class_rst_file(name, fqn, base_path)) else: sub_files = create_rst_files(fqn, base_path) created_files.append(write_module_rst_file(name, fqn, sub_files, base_path)) write_module_rst_file(module_name, module_name, created_files, base_path) return created_files def main() -> None: output_dir = Path(__file__).parent / "source" output_dir.mkdir(exist_ok=True) create_rst_files("chango", output_dir) if __name__ == "__main__": main() chango-0.6.0/docs/make.bat000066400000000000000000000014011507376567100153260ustar00rootroot00000000000000@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=source set BUILDDIR=build %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.https://www.sphinx-doc.org/ exit /b 1 ) if "%1" == "" goto help %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end popd chango-0.6.0/docs/source/000077500000000000000000000000001507376567100152255ustar00rootroot00000000000000chango-0.6.0/docs/source/changes.rst000066400000000000000000000000261507376567100173650ustar00rootroot00000000000000.. chango:: Changelog chango-0.6.0/docs/source/chango.abc.changenote.rst000066400000000000000000000001401507376567100220470ustar00rootroot00000000000000ChangeNote ========== .. autoclass:: chango.abc.ChangeNote :members: :show-inheritance:chango-0.6.0/docs/source/chango.abc.chango.rst000066400000000000000000000001241507376567100211750ustar00rootroot00000000000000ChanGo ====== .. autoclass:: chango.abc.ChanGo :members: :show-inheritance:chango-0.6.0/docs/source/chango.abc.rst000066400000000000000000000003171507376567100177430ustar00rootroot00000000000000abc === .. automodule:: chango.abc .. toctree:: :titlesonly: chango.abc.changenote chango.abc.chango chango.abc.versionhistory chango.abc.versionnote chango.abc.versionscanner chango-0.6.0/docs/source/chango.abc.versionhistory.rst000066400000000000000000000001541507376567100230500ustar00rootroot00000000000000VersionHistory ============== .. autoclass:: chango.abc.VersionHistory :members: :show-inheritance:chango-0.6.0/docs/source/chango.abc.versionnote.rst000066400000000000000000000001431507376567100223120ustar00rootroot00000000000000VersionNote =========== .. autoclass:: chango.abc.VersionNote :members: :show-inheritance:chango-0.6.0/docs/source/chango.abc.versionscanner.rst000066400000000000000000000001541507376567100230000ustar00rootroot00000000000000VersionScanner ============== .. autoclass:: chango.abc.VersionScanner :members: :show-inheritance:chango-0.6.0/docs/source/chango.action.rst000066400000000000000000000000731507376567100204720ustar00rootroot00000000000000action ====== .. automodule:: chango.action :members: chango-0.6.0/docs/source/chango.changenoteinfo.rst000066400000000000000000000001501507376567100222000ustar00rootroot00000000000000ChangeNoteInfo ============== .. autoclass:: chango.ChangeNoteInfo :members: :show-inheritance:chango-0.6.0/docs/source/chango.concrete.backwardcompatiblechango.rst000066400000000000000000000002201507376567100260060ustar00rootroot00000000000000BackwardCompatibleChanGo ========================= .. autoclass:: chango.concrete.BackwardCompatibleChanGo :members: :show-inheritance:chango-0.6.0/docs/source/chango.concrete.backwardcompatibleversionscanner.rst000066400000000000000000000002471507376567100276170ustar00rootroot00000000000000BackwardCompatibleVersionScanner ================================ .. autoclass:: chango.concrete.BackwardCompatibleVersionScanner :members: :show-inheritance:chango-0.6.0/docs/source/chango.concrete.commentchangenote.rst000066400000000000000000000001721507376567100245140ustar00rootroot00000000000000CommentChangeNote ================= .. autoclass:: chango.concrete.CommentChangeNote :members: :show-inheritance:chango-0.6.0/docs/source/chango.concrete.commentversionnote.rst000066400000000000000000000001751507376567100247570ustar00rootroot00000000000000CommentVersionNote ================== .. autoclass:: chango.concrete.CommentVersionNote :members: :show-inheritance:chango-0.6.0/docs/source/chango.concrete.directorychango.rst000066400000000000000000000001641507376567100242030ustar00rootroot00000000000000DirectoryChanGo =============== .. autoclass:: chango.concrete.DirectoryChanGo :members: :show-inheritance:chango-0.6.0/docs/source/chango.concrete.directoryversionscanner.rst000066400000000000000000000002141507376567100257770ustar00rootroot00000000000000DirectoryVersionScanner ======================= .. autoclass:: chango.concrete.DirectoryVersionScanner :members: :show-inheritance:chango-0.6.0/docs/source/chango.concrete.headerversionhistory.rst000066400000000000000000000002031507376567100252710ustar00rootroot00000000000000HeaderVersionHistory ==================== .. autoclass:: chango.concrete.HeaderVersionHistory :members: :show-inheritance:chango-0.6.0/docs/source/chango.concrete.rst000066400000000000000000000006341507376567100210220ustar00rootroot00000000000000concrete ======== .. automodule:: chango.concrete .. toctree:: :titlesonly: chango.concrete.backwardcompatiblechango chango.concrete.backwardcompatibleversionscanner chango.concrete.commentchangenote chango.concrete.commentversionnote chango.concrete.directorychango chango.concrete.directoryversionscanner chango.concrete.headerversionhistory chango.concrete.sections chango-0.6.0/docs/source/chango.concrete.sections.rst000066400000000000000000000001401507376567100226400ustar00rootroot00000000000000sections ======== .. automodule:: chango.concrete.sections :members: :show-inheritance:chango-0.6.0/docs/source/chango.config.rst000066400000000000000000000001661507376567100204650ustar00rootroot00000000000000config ====== .. automodule:: chango.config :members: :inherited-members: TomlSettings, FrozenModel, BaseModel chango-0.6.0/docs/source/chango.constants.rst000066400000000000000000000001031507376567100212230ustar00rootroot00000000000000constants ========= .. automodule:: chango.constants :members: chango-0.6.0/docs/source/chango.error.rst000066400000000000000000000000701507376567100203430ustar00rootroot00000000000000error ===== .. automodule:: chango.error :members: chango-0.6.0/docs/source/chango.helpers.rst000066400000000000000000000000751507376567100206610ustar00rootroot00000000000000helpers ======= .. automodule:: chango.helpers :members: chango-0.6.0/docs/source/chango.rst000066400000000000000000000002641507376567100172200ustar00rootroot00000000000000chango ====== .. automodule:: chango :members: __version__ .. toctree:: :titlesonly: chango.changenoteinfo chango.version chango.abc chango.concrete chango-0.6.0/docs/source/chango.sphinx_ext.rst000066400000000000000000000001061507376567100214030ustar00rootroot00000000000000sphinx_ext ========== .. automodule:: chango.sphinx_ext :members: chango-0.6.0/docs/source/chango.version.rst000066400000000000000000000001231507376567100206760ustar00rootroot00000000000000Version ======= .. autoclass:: chango.Version :members: :show-inheritance:chango-0.6.0/docs/source/chango_auxil.rst000066400000000000000000000002661507376567100204240ustar00rootroot00000000000000Auxiliary modules ================= .. toctree:: :titlesonly: chango.action chango.config chango.constants chango.error chango.helpers chango.sphinx_extchango-0.6.0/docs/source/cli.rst000066400000000000000000000001321507376567100165220ustar00rootroot00000000000000CLI === .. click:: chango._cli:_typer_click_object :prog: chango :nested: full chango-0.6.0/docs/source/conf.py000066400000000000000000000134731507376567100165340ustar00rootroot00000000000000import os import re import sys import tomllib import typing from pathlib import Path import click from docutils.nodes import Node, reference from sphinx.addnodes import pending_xref from sphinx.application import Sphinx from sphinx.environment import BuildEnvironment sys.path.insert(0, str(Path("../..").resolve().absolute())) from chango import __version__ pyproject_toml = tomllib.loads(Path("../../pyproject.toml").read_text()) project = pyproject_toml["project"]["name"] version = __version__ release = __version__ documentation_summary = pyproject_toml["project"]["description"] author = pyproject_toml["project"]["authors"][0]["name"] copyright = "2024, Hinrich Mahler" # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration extensions = [ "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx.ext.napoleon", "sphinx_click", "sphinx_copybutton", "sphinx_paramlinks", "chango.sphinx_ext", ] html_theme = "furo" intersphinx_mapping = { "python": ("https://docs.python.org/3", None), "pydantic": ("https://docs.pydantic.dev/latest/", None), "sphinx": ("https://www.sphinx-doc.org/en/master/", None), } nitpicky = True # paramlinks options paramlinks_hyperlink_param = "name" # Use "Example:" instead of ".. admonition:: Example" napoleon_use_admonition_for_examples = True # Don't copy the ">>>" part of interactive python examples copybutton_only_copy_prompt_lines = False # Configuration for the chango sphinx directive chango_pyproject_toml_path = Path(__file__).parent.parent.parent # Don't show type hints in the signature - that just makes it hardly readable # and we document the types anyway autodoc_typehints = "none" autodoc_member_order = "alphabetical" autodoc_inherit_docstrings = False html_static_path = ["../../logo"] # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the documentation. html_theme_options = { "light_logo": "chango_light_mode_1024.png", "dark_logo": "chango_dark_mode_1024.png", "navigation_with_keys": True, "footer_icons": [ { # Github logo "name": "GitHub", "url": "https://github.com/Bibo-Joshi/chango", "html": ( '' "" ), "class": "", } ], } # The name for this set of Sphinx documents. If None, it defaults to # " documentation". html_title = f"chango {version}" # Furo's default permalink icon is `#` which doesn't look great imo. html_permalinks_icon = "¶" # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. html_favicon = "../../logo/chango_icon.ico" # Due to Sphinx behaviour, these imports only work when imported here, not at top of module. from docs.auxil.rich_to_rst import RichConverter # noqa: E402 def convert_rich_to_rst(_: Sphinx, __: click.Context, lines: list[str]) -> None: converter = RichConverter("\n".join(lines)) converter.parse_rich_text() lines[:] = converter.render_rst_text().split("\n") _TYPE_VAR_PATTERN = re.compile(r"typing\.([A-Z][a-zA-Z]*)") def missing_reference( _: Sphinx, __: BuildEnvironment, node: pending_xref, contnode: Node ) -> None | Node: """Here we redirect links to type variables. Sphinx tries to link TypeVar T to typing.T which does not exist. We instead link to typing.TypeVar. """ if not (match := _TYPE_VAR_PATTERN.match(node["reftarget"])): # Sort out everything that is obviously not a TypeVar return None name = match.group(1) if hasattr(typing, name): # We don't want to change valid links that exist return None link_node = reference(refuri="https://docs.python.org/3/library/typing.html#typing.TypeVar") link_node.append(contnode.deepcopy()) return link_node def config_inited(_: Sphinx, __: dict[str, str]) -> None: # for usage in _cli.__init__ os.environ["SPHINX_BUILD"] = "True" def setup(app: Sphinx) -> None: app.connect("config-inited", config_inited) app.connect("missing-reference", missing_reference) # We use the hooks defined by sphinx-click to convert python-rich syntax to rst # See also https://sphinx-click.readthedocs.io/en/latest/usage/#events. # This is a relatively sane way to have nice rendering both in the CLI and in the HTML # documentation. Doing conversion for computing the documentation is preferable to doing # conversions for the CLI as the latter would impact the CLI performance for event in [ "sphinx-click-process-description", "sphinx-click-process-usage", "sphinx-click-process-options", "sphinx-click-process-arguments", "sphinx-click-process-envvars", "sphinx-click-process-epilog", ]: app.connect(event, convert_rich_to_rst) chango-0.6.0/docs/source/github_action.rst000066400000000000000000000053541507376567100206050ustar00rootroot00000000000000.. _action: GitHub Actions ============== When using ``chango`` in your project, you will want to ensure that each change adds a change note. When hosted on `GitHub `_, you can use `GitHub Actions `_ to support this process and automatically create a template change note for each new change. ``chango`` defines the following methods to help you with this process: * :meth:`chango.abc.ChanGo.build_github_event_change_note` * :meth:`chango.abc.ChangeNote.build_from_github_event` Going even further, ``chango`` provides a composite `GitHub Action `_ that does the heavy lifting for you. You can configure it for example as follows: .. code:: yaml name: Create Chango Change Note on: pull_request: branches: - main types: - opened - reopened jobs: create-chango-fragment: permissions: # Give the default GITHUB_TOKEN write permission to commit and push the # added chango note to the PR branch. contents: write name: create-chango-fragment runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: Bibo-Joshi/chango@ with: # Optional: Specify a Python version to use python-version: '3.13' This will automatically use your ``chango`` configuration to create a change note for each new change. Inputs ------ The following inputs can be used to configure the action using the ``with`` keyword. .. list-table:: :width: 95% :align: left :header-rows: 1 * - Name - Description - Required - Default * - python-version - The Python version to use. - No - 3.x * - commit-and-push - Whether to commit and push the change note to the PR branch. - No - true * - pyproject-toml - Path to the ``pyproject.toml`` file. Takes the same input as :func:`chango.config.get_chango_instance`. - No - :obj:`None` * - data - Additional JSON data to pass to the parameter :paramref:`~chango.abc.ChanGo.build_github_event_change_note.data` of :meth:`chango.abc.ChanGo.build_github_event_change_note`. - No - An instance of :class:`chango.action.ChanGoActionData` * - github-token: - GitHub Token or Personal Access Token (PAT) used to authenticate with GitHub. - No - ``GITHUB_TOKEN`` * - query-issue-types: - Whether to query the issue types of the linked issues. Can only be used on organizations with issue types enabled. In this case, an organization scoped PAT is required. - No - :obj:`False` chango-0.6.0/docs/source/index.rst000066400000000000000000000007701507376567100170720ustar00rootroot00000000000000.. include:: ../../README.rst .. toctree:: :hidden: :caption: Guides github_action.rst sphinx_ext.rst .. toctree:: :hidden: :caption: API Reference chango chango_auxil cli .. toctree:: :hidden: :caption: Project changes Source Code PyPI Bug Tracker .. toctree:: :hidden: :caption: Indices and Tables genindex modindex chango-0.6.0/docs/source/sphinx_ext.rst000066400000000000000000000113721507376567100201540ustar00rootroot00000000000000.. _sphinx_ext: Sphinx Extension ================ In addition to the CLI, ``chango`` provides a Sphinx extension that can be used to automatically render change notes in your project documentation. Setup ----- To enable the extension, simply add ``'chango.sphinx_ext'`` to your ``extensions`` list in your ``conf.py`` file. .. code-block:: python extensions = [ ... 'chango.sphinx_ext', ] To specify the path to the ``pyproject.toml`` file, you can use the ``chango_pyproject_toml`` configuration option in your ``conf.py`` file. .. code-block:: python chango_pyproject_toml = 'path/to/pyproject.toml' This is useful to ensure that ``chango`` can find the correct configuration file independent of the current working directory. Now, you can use the ``chango`` directive in your documentation to render change notes. .. code-block:: rst .. chango:: Headline :start_from: 1.0.0 This will render a list of change notes starting from version 1.0.0 up to the latest (unreleased) version. Configuration ------------- The following configuration options are available: .. confval:: chango_pyproject_toml :type: ``pathlib.Path`` | ``str`` | ``None`` :default: ``None`` Path to the ``pyproject.toml`` file. Takes the same input as :func:`chango.config.get_chango_instance`. .. rst:directive:: chango The ``chango`` directive renders the version history of your project. If the directive has a body, it will be used as the headline for the change notes with ``=`` above and below the headline, which is the default reStructuredText syntax for a headline. .. admonition:: Example .. code-block:: rst .. chango:: Headline :start_from: "1.0.0" Renders as .. code-block:: rst ======== Headline ======== ... The options that are valid for the ``chango`` directive are the same as the options for :meth:`~chango.abc.ChanGo.load_version_history`. If your implementation of :class:`~chango.abc.ChanGo` has additional options, you can pass them as keyword arguments to the directive. ``chango`` will inspect the signature of the method and configure the options accordingly. .. important:: Since the options will be interpreted as keyword arguments for :meth:`~chango.abc.ChanGo.load_version_history`, by default, each option is required to have a value. .. tip:: The values be interpreted as JSON string and will be loaded using :func:`json.loads`. Since you can only specify strings as options in reStructuredText, it may be necessary to use custom validator functions to convert the strings to the correct types. Custom validators can be specified by using :class:`typing.Annotated` in the signature of the method. Validators should have the following signature: .. code-block:: python def validator(value: str | None) -> Any: ... .. admonition:: Example .. code-block:: python from collections.abc import Sequence from typing import Annotated def sequence_validator(value: str | None) -> Sequence[int]: if value is None: raise ValueError('Value must not be None') return tuple(map(int, value.split(','))) def flag_validator(value: str | None) -> bool: if value is not None: raise ValueError('Flag options must not have a value') return True class MyChanGo(ChanGo): def load_version_history( self, start_from: str | None = None, end_at: str | None = None, custom_option_1: dict[str, str] | None = None, custom_option_2: Annotated[Sequence[int], sequence_validator] = (1, 2, 3), custom_option_3: Annotated[bool, flag_validator] = False, ): ... With this signature, you can use the following directive: .. code-block:: rst .. chango:: :custom_option_1: {"key": "value"} :custom_option_2: 4,5,6 :custom_option_3: The following options are available by default: Keyword Arguments: ``:start_from:`` (:obj:`str`, optional): The version to start from. Passed to parameter :paramref:`~chango.abc.ChanGo.load_version_history` of :meth:`~chango.abc.ChanGo.load_version_history`. Defaults to ``None``. ``:end_at:`` (:obj:`str`, optional): The version to end at. Passed to parameter :paramref:`~chango.abc.ChanGo.load_version_history` of :meth:`~chango.abc.ChanGo.load_version_history`. Defaults to ``None``.chango-0.6.0/logo/000077500000000000000000000000001507376567100137355ustar00rootroot00000000000000chango-0.6.0/logo/chango_dark_mode.svg000066400000000000000000000445761507376567100177420ustar00rootroot00000000000000 chango-0.6.0/logo/chango_dark_mode_1024.png000066400000000000000000003232021507376567100203570ustar00rootroot00000000000000‰PNG  IHDR#;‚b• pHYs]]6µîtEXtSoftwarewww.inkscape.org›î< IDATxœìÝyœÍõþð×ç{ÎlÈŒ¥È’%FFfΘ„’,‰bl!´ÝTr»‰r Q¡}QºÙ—A¡lY†,c™™ ÍÌ9g4]cEw Ã`ŒYÎ÷óû£ô“³œóýœåõüK3g>ï×£åóþlBJ """""""r­ŒŒŒêEEEwI!ÚJ)ÃP RÖ€ÕÿøH'‘@„Ø¥I¹3""â¸3ê 6ˆˆˆˆˆˆˆœ/>>ÞÒšö ”²»n Ê1Ôn ,®y¢¼yØ """"""r’Ý»wW ˆ†½Ü ĉÃJ`µIˆ·ÂÃÃ÷•õ‡Ù """"""ª«Õ"…ˆ0@ü>épqÉbH9Ýáp¼•[Úb€ˆˆˆˆˆˆ¨Œ¬VkˆÔ´^BʺÂõ“þ¿@¶”òŸ‹eM©>ÏÑõíÙ³§F@@@o)eq?Õ™þ0ÝßÏ殺°°Âk}ˆ """"""¢«HII©i2™úBˆþ:Á}&ý!…/ölÓ¦ÍÙ«}† """"""¢ËìÙ³§†_``?¶÷wƒ›NúKؽY³fçJú&DDDDDDäóöfdÔö+,ì÷ÇJ&Õ™ÊC›sOŸîÙ±cÇâ+¿gVˆˆˆˆˆˆˆHµ”””ššŸßƒBÊ~@wáñsdt ùÀ¨¿};ˆˆˆˆˆˆÈWØíöz:ð¤ìà.šêL® €‡#""–ÿåkl‘7³Z­ …}%Ð@;Bu&—“òT‘¿ÿí­Ã²/}Éã·7])==½e±®÷@_ésKßBÔð+,üÀ?¿ÄDDDDDDä´ÔôôHÍáˆB<,ÛTrRâŽðððT€;ˆˆˆˆˆˆÈCeddø_,.î,¤ì£½5 6„—¹ÿ$tàu}î """"""’’’RÉÏϯ‹”r€zVÉÍK]où w‘[KKK»&S/!e³Ù|Ÿ.e êLÄ,L¦'¼Ë¹´´´š¦u“@´Ð´nÒ·÷—ƒ²€wy€ˆˆˆˆˆˆÜHMOoeÒõ^lí·¨äE²&w‘ ÈËËë(¥ì !¢5 —¨]Â$5­DDDDDDd˜ŒŒŒê………]„Ñ^â'„êX^OèzK6ˆˆˆˆˆˆÈ¥¬VkC!ÄýˆÐ Bøq¥ß`šÆ9›f·Û[ëºÞBô‚-8áWLÊ&lQ…ÅgeŸ>Ý^Ó´h < .·ö»•›ø •KZZÚš¦õüã,ÿý*©Îä‰ŠŠŠ „€ÙìÒ5ú<î """""¢RKKK Bô„½„¦µ•€Iu&O”™™‰‰‰HLLDff&¶nÛæê€kG'""""""Ï–‘‘á_PPÐá­ý=…¦5VÉ麛͆¸¸8ìØ¾ÙÙÙ~Ïb± råÊ®ŽPÀýÅž={jøûûw¾ôTŸÐ´`/»Ë'ýÛ¶nÅo¿ýVâç:uêäò,ø… """"""‚Ýn¿Ý!eODû´`⤿ì.Ÿôo‹Ã‰'®ùy!ºÞ¿ËsI!³@DDDDDäƒâããÍÕªUk«=Ð@3ÞÙ_>—Oúã¶lÁÉ“'Ký³‘­Záæ›ovaºßI)SÙ """""ò)))5M&S'!DtHµj½$ÌIù8|ÿý÷؇íÛ·#''§\ãtïÖÍÉÉJ¦ly³´´´a2õRF›Ìæ;hÜÚ_>ÅÅÅØ»wþ3gÎTh<ÜoÀöÐu """"""oŸ•|út{hÚ}è+4-R‚“þò¹|{ÿæM›Ê½Ò_’Ž;"8$Äiã]ÃáÈÈÈ£ly8»Ý~“”²§z†÷CÓ\þ¦œ7så¤ÿr½ûôqɸ#e,°@DDDDDäìvûí: R¶ ©ÎäÉ ‘œœŒ‰‰Ø¾};NŸ>íÒzµjÕBÛ¶m]Zã)åz€ """"""°bÅ Shhh;詽%p›êLžîÂ… HÚµ Û·oGbb"òòò «Ý«wohš!=›¹¹¹ñDDDDDDn+==½šÃá¸OÝ444Z!àyþ ÈÍÍÅÎÄD$&&b×®]¸pá‚á„èmH- ,ëØ±c1À‘[IMMmb2™zÉß·÷·‡fNø+&;;»víBbB’’’P\\¬4Oë;ïD½úõ ©%¤üêÒ¯Ù """""RÌf³µRBôÔL¦(Nø+îØ±cHLH@\\l6¤tŸ«}ŒºüOˆŸ,I—þ‘ """"""ãiV«µ­¢Ÿúh !¸µ¿t]Çþÿþ ‰‰Ø¼y3²ŽQ©DUªTA§N ©%¥ü—a€ˆˆˆˆˆÈ—.ñ0@ý!DNø+æòçú¶mÝŠß~ûMu¤ëzðÁhH-ép,ºüŸÙ """""r‘ø¬¬ÀêgÏvÕ¥ìÙ44´nRÉÓååå!)) Û·oÇ·;wâüùóª#•‰aÛÿ]­Zµúñò/°@DDDDDäD»wï®Ð@ß!Ðʪ3yºœœ$íÚ…¸¸8|÷Ýw(,,T©\š6mŠæaa†Ô@Ì•_c€ˆˆˆˆˆ¨‚’““ƒ*W®|Ÿ”r@@``?pÒ_aû÷ïGB|<âðßt«KüÊ«oß¾F•:¸âÊ/²@DDDDDT—Oúƒ*Uê«KYEu&Ové<ÿÎÄDlß¾GUÉ©ð`†ÔÀ²fÍš»òël•Ò¥3ýœô;G~~>öìÙƒ¸¸8$&$àܹ¿ÍY½F÷îÝlH-)åß¶ÿl]Óå“þ Ü :“'ûßÿþ‡¤¤$$&$ 99EEEª#bÀÀF•Ê´X,É%}ƒ """""¢+€ÿ³@DDDDDô;Íf³Ý`€†@ˆšð±‰ª³ ÍjEBB¶oÛ†_ýUu$¥~øa£J;Ž/®öM6ˆˆˆˆˆÈ§Ùl¶RÊâqTçñT§NÂÎ;±31ÉÉÉÈÏÏWÉ-T©RÝxÀ˜bBlˆŠŠúßÕ¾Íùœ}ûöÕ/Öõ~ò ‘Bu$tèÐ!$&&"1!v»º®«ŽävzFG#((ÈZ(ñò¿KØ """""Ÿ‘‘Q½ ¸¸§ƒ”]pÖ_F—oíß±};²³³UGr{=ôQ¥~Õ‹Š6^ël‘×JNN êùÇöþnðSÉÓœ>}»¾ý‰‰‰HJJB^^žêH£UTš4ibH- ,ŒŠŠºæ“ l‘WY±b…)44´‹ªT©€Êª3yníwŽýûUJŠßoÿ¿&ákÏ/‘wJIO¿Íäp ‚Oh¨:'áÖ~ç Áæ-[àïïoD¹8KDÄý×ûw‘DzZ­!BˆxÜÜÁÛüJ‹[û]«o¿~FMþ!€™¥ùDDDDDäQV¬XajÒ¤I'MÓ‡ý%`Ìë^àò­ý6› ÜyùßÿŠ‹‹×—æƒl‘G°Ùl-t౦¡¡ÿP‹S×ë»|kÿömÛð믿ªŽäÚÝuêÖ­kL1)ç\ïò¿KØ """""·•‘‘Q½¨¨¨¿<+€VÜß}§Nžü}•?1ß}÷.^¼¨:’Ï1ðò¿bMÓæ•öÃl‘[¹´Å_˜Là e~œø_Û±cǘ€¸¸8ÞÚ¯X:upO‡F•‹ ?VÚ³@DDDDDnÁjµ6ðHÓfÍž„”·€çÓ¯J×uØl6ìLLÄŽ;••¥:ýað!Ð4ÍZ˜U¦ÏóÒ"""""R%>++0äÌ™hhÚ0HÙû¯âܹsHLLDB|6›7oFvv¶ê8äAžzúiaÐ#˜RÎŠŠŠºàŒ¡Ø """"òr©©©ÍM&Óð€ÀÀ§¤_êwöìYÄÅÅa}l,¬V«ê8äêÕ¯ûî3욌bMÓf:k06ˆˆˆˆˆ¼ÐÁƒΟ?ßšö/ÍdjçËkýEEEHJJ†õ뢢"Ցȃ=õÔSÐ4ÍZX~ÌYã±@DDDDäERRRn6›ÍK`„¨ëËÛü:„ ë×cíÚµÈÉÉQ‡¼@­ZµÐ£GÃê9ãé¿Ë±@DDDDäRÓÓ£Lº>Òd6’€Ÿê<ªœ8qq[¶`ݺuØ¿¿ê8äeüqøùôÇKˆï[…‡çÌ!Ù """"òPñYYÁgϺ>Z"ÜW×ú ˜ˆõ±±Øµk‡êHä…‚ƒƒÑ§o_ÃêIàcgÉ‘‡IMMm¢™LÏ…OQ·‘»™}ûöaÍêÕØ´iòòòTÇ!/÷È# R¥JF•ûE/*ZéìAÙ """"ò ZZZZga2 ÓL¦~Lª©pîÜ9lÙ²+–/ç2LåÊ•ñð A†Õ“ÀÇQQQN¿­’ """""7¶{÷$0RhZ˜/^ê§ë:öîÝ‹ØØXl‹CAAêHäc €ªU«U.§R`àW Ì‘²Ûíͤ”Ï>%ʪó¨ðÛo¿aÆ øzÕ*;洗ЈÊÄßßCyİz˜Ö¬Y³s®› """""÷¡Ùíö:ð¤ìÀç÷!)) Ö¯ÇöíÛy¡)×»OÜxãF•» ëú4W Αbv»ý&)哸'€ªó¨uäÖ­[‡µk×"''Gu"€¦ixì±ÇŒ,9;22ò„«g€ˆˆˆˆH‘´´´дqø€ÕyŒvñâElÞ¼«V­BºÝ®:Ñß<ðÀ¨_¿¾Qå 5!œþôßåØ """"2XJJÊ-f³y´¦iÃ$¨:Ѳ²²°níZ|ýõ×ÈÍÍU‡¨Dš¦áégž1¬ž¾wéeläÒÄßl6ûÜÄ¿°° Xµr%öìÙ郯géÙ³'6lhT9À®.‘‹ùòÄÿèÑ£X»f V¯^3gΨŽCT*f³Ï f\A!VE„‡gºº DDDDD.â«ÿ¢¢"ÄÇÇsµŸõ”aõ$°¹UË–)FÔb€ˆˆˆˆÈI|qâ_\\ŒíÛ·cÕʕػw/WûÉãõë×uêÔ1® ®¿kT)Á? DDDDDsiâÀg&þ§NžÄºØX,_¶ ÙÙÙªã9…@bccqÓM7Ur·%"¢­QŸ€ˆˆˆˆ¨œ|qÅÿÇŒ ,Yº›7mBqq±ê8DN5pÀ#'ÿÀ[†w•™¯­øçååaÓ¦MXöÕW8pà€ê8D.„ØõëQ£F £JfX""Zâ÷' ÁDDDDD¥”––Öš6Îd6?)Õy\-++ ë֮ŪU«pöìYÕqˆ\jРAFNþ!€É0pòpÑuùÒŠ¿®ëعs'¾\º”Oø‘ϨT©ÖoØ€jÕªUòKDD8 npÑUøÒ^êG¾ì‘G5rò)Ä0xòpÑßøÒŠÿ÷ßåË–aÇŽ¼Ô|Ò 7Ü€õ6 jÕª†Ô“@zdD„ Ü@DDDDô_Yñ/,(À–-[ðù_à@f¦ê8DJ=öØc†Mþ@HùLþÿ¨ÍDDDDäÛÒÒÒhšö¼|ÅÿØÏ?ã믿Æ×_ÜÜ\Õqˆ” Ɔo¾AåÊ• ©'ÔȈˆ;~ÿ¥ñ¸€ˆˆˆˆ|VjjjÍlž 4íé¥7ÖuIIIøê«¯œ”]W²ðHä–ž:Ô°É?hÀP4ù¸€ˆˆˆˆ|Ýn¯¬ëúËb,€Õy\!//›6mÂ’Å‹qäÈÕqˆÜN:u°zõjøóŸ¤DDD´†Â€Wv9‰ˆˆˆˆ®BØl¶G$ð>„¨£:Œ+deeaùòåX³z5òóóUÇ!r[#FŒ0lò!&@áä`€ˆˆˆˆ|DjjjÍdš «ê,®PTT„çŸ{vïV…Èí………¡[÷îÆ"¹Uxø&ã –Œ """"òjñYY!gÎŒÕL&¯ÝîëÖ­ã䟨”FŽ !„aõ¤ÃñºaÅ® """"òZ6›­S5`†â6ÕY\©¸¸ bbTÇ ò÷vìˆ;Û´1²ä®ÈÈÈ­F¼6ˆˆˆˆÈëìÍȨí_TôSÅëÖ­ÃñãÇUÇ r{š¦aĈÆ•r‚±¯Ž """"ò+V¬0…††þÓxKÁªó¡¸¸1ó竎Aäú=ôn½õV#Kî°X,ñF¼6ˆˆˆˆÈ+¤§§·l:W†îíUm}l,Wÿ‰J¡R¥Jþ쳆ÖÔ„xÃЂ×Áy´””?³Ùü’&Á‹/ù+‰®ëX°`êDáÿøjÔ¬idÉ ááá;,x=l‘DzZ­w™Ìæ¹SE…ØØXüôÓOªcòóóCQQ‘ê¥rã7âÑÇ ½Dw§³ÿ—°@DDDDg÷îÝUß„ÏÐTçQÁáp`Þ¼yªcÐ4 Íš5ÃwÞ‰;ï¼ÍÃÂpèÐ!üsøp«Žw]Ï=÷‚‚‚Œ,¹Èb±X,Xl‘G±Z­=‚‚fH)oQE¥o¾ùÇ~þYu òbµkׯ]wÝ…víÚ¡uëÖ ð{óiÑÂ…˜1c‡â”×פIôêÝÛ°z¸X\\<Ѱ‚eÀy„}ûöÕr8BˆÇ ¥ê8J麎ù\ý''3™LhÙ²%:Ü{/Ú¶iƒÛš7‡â/ŸIKKû#(JYv/¾ø"4ÍÐBS£¢¢ÜòlDDDDäö¬Vë1€¡7x¹«o6lÀÑ£GUÇ /P§N´k×mÚ¶E»víP¥J•?—››‹Ù³gcÙW_A×uƒS–_ëÖ­q×ÝwYò´ŸŸß‡F, 6ˆˆˆˆÈmÙíöFº”³!DWÕYÜ…®ë<ûO忀¨V­p×Ýw£ýÝw£a£F×ü¼®ëøzÕ*|öÙg8{ö¬A)CÓ4üûå— ­)€7ÃÂÂr -Zl‘;Òl6ÛÓ˜ ä%IµqãF®þS™Ô¯_wß}7înßwÜqKõs{öìÁÔ©SñcF†‹ºÆÀjdɬʕ+Ï0²`Y±@DDDDn%55µ‰f6Ïp¯ê,îF×uÌ›;Wu rs°X,hÓ¶-Ú¶iƒæae{%3ëÈ̘1qqq.JèzÁÁÁ>|¸±E¥ߤI“c‹– DDDDäâããÍÕªUm2™&J)K·Déc6oÞŒ¬¬,Õ1È 5lØðÏUþ¨V­àPæ1Nœ8™3gbíš5uο$£FúóÕƒØ,ËWF,6ˆˆˆˆH¹ôôô–!ժ͗@kÕYÜ•®ë˜3gŽêä&._åïØ±#]ç,ÿµäçç㫯¾Âüyó——çÄ”j4 3ôÙ?€”/pû® DDDD¤LJJŠŸÙl~I“ø«ÎãζlÞŒ¬#GTÇ …êÕ«‡6mÚ MÛ¶¸ûî»Q©R¥ çp8°fÍÌœ9§NžtRJµ4Møqã }öO›#-8/Á)aµZ[kfó| ´T¥,ŠŠŠ°iÓ&4lÐ-Ãà ©©ë:æòì¿Ï À­[£}ûöh÷ݨW¿¾SÆ•RbÛÖ­˜>c†×5•z÷郖- ýOŠ.¤kdÁŠ`€ˆˆˆˆ uiÕB¼)?ÕyJKJ‰¸¸8|öÙg8öóϘcà„<..‡6¬©S¿~}´¿ç´¿ûnDÝqÊq–ÿZ’ví´éÓ=öfÿk©Zµ*^xác‹J¹Øb±X-Z~l‘aR÷í 7™ÍŸK Bu–²HMIÁÔ©S‘žžþç×B›65¤¶®ë˜Ë³ÿ^KæÍ›£sçÎèÔ¹37nì’:V«Ÿ}öRSR\2¾;ø×óÏ#ÄØ‹ÿò¥”¯Y°¢Ø """"—»tÿLàÜ%M:uê>ùä|³a¤”~½víÚ†Ý0¾uëV:tÈZd MÓŽ®]»¢s—.¨]»¶Ëj:t³gÍòè'ýJ£iÓ¦x衇Œ-*å‘‘‘G-Z1l‘K¥ìÛwkpµj $pê,¥%¥Ä†õëñÑG!77÷oß 5,Ǽyó ©E®€6mڠý÷¢S§N¨^½ºKëýòË/ˆ™?«W¯öø'ý®G±cÇÂd2VS?›4íCà : DDDDä*Âf³=c>PYu˜Ò:™‰·Þz v»ýªŸijP`ÛÖ­8™iH-r¾àà`´¿çÜ{ï½N¹µ¿4~ýõWÌ3kÖ¬Ãápy=wУgO´ŠŠ2´¦rtxD„ǽ™È9Õjm!btR¥´.\¸€Y3gbéÒ¥×855àü¿”ÒЋÉ9jÕª…ûî»:wFdd¤aÏÑåää fþ|¬X¹…†Ôt•+Wƨ‘#.û­ÅbYitQg`€ˆˆˆˆœ*ÍnJñ €Tg)­;vàƒ÷ßGvvv©>ßÌ€Û·oç꿇¨Q£î»ï>Üß­,‹¡oП9s ,ÀòåË‘ŸŸoX]wñìðá¨Q³¦‘%Rן ¯ûI7Ä9EJJJM“Ù++°Znîûb¡:Oi;v o½ù&vïÞ]®Ÿwõ RJÌçÍÿnãÒíý]»vŦä•þ IDAT}]»"00PIŽ .`Ù²eX¸`Ξ=«$ƒ;8p "## ­)€×£¢¢NZÔÉØ """¢rIOOo¬ëK$ÐRu–Òp8ø|Ñ"Ìž=¸$ÍÕ&$$ ##Ã¥5èÚüüüpÏ=÷ ûàž{îQ6é~Ÿø¹t)¾øâ‹2ïVñVµkׯó#F]6£¸¸ØãÏå°@DDDDe¥Yíöòø«S™™™˜4q¢S&ÖM\ܘ;gŽKǧ«kܸ1zFG£wïÞ¨^½ºÒ,ùùùXýõ׈Y°§Nzô¢³Ó;•+û²¨^ŒŠŠ*2´¨ °@DDDD¥f·ÛéR~ànÕYJãâÅ‹˜1c–,^ ]×2f“&Mœ2NI¸úo¼›nº =zô@Ïèh4nÜXu\¼x+V¬À ““£:ŽÛéÖ½;:vìhhM |m‰ˆØbhQa€ˆˆˆˆJ%Ínï/¤œ šê,¥‘š’‚É“'ãèÑ£N³fÍš qÚxWâê¿1üpo‡èѳ'Ú·o“ɤ:.^¼ˆ¯W­ÂÂ… qâÄ ÕqÜRpp0^~ùeCk à¢&Ä¿ -êBlÑ5%''UªTé=¼ :Kiäææâã)S )ûT·+WÿñÃ?¸l|_'„€ÅbAtt4ºÞ?ªT©¢:€ß·ú/_¾Ÿ/ZÄÿëýï£FF—ý0<<üˆÑE]… """"ºªÔôô¨ J•¾”€k¾;ɶmÛðî»ïºìÌ´+/œ3ÛãïsKuëÖEÏèhôìÑõê×WçO—nõÿâóÏqúôiÕqÜ^»vímtÙ£Bˆ÷.êJlQI„ÕnA“òxÀE999øøã±aýz—ÖqÕ€;wîäê¿iš†Ö­[ã¡þýÑ¥Khš¦:ÒŸ.\¸€5«Wór¿2 Ä«¯¾jx])Ä áááy†v!6ˆˆˆˆè/ìvûM:°R>¨:KiÄÅÅá·ßÆ™3g\^«©‹Žpõß9jÕª…|~µk×Vç/þœøÇÄàÔ©Sªãx”=ÿ¼ñ»7¤\±ÎØ¢®ÇýÉjµv…‹Ü¬:Ëõœ:yï¼ó¶oßnH=MÓаQ#§»ëÛo±oß>§ë+Üyµòòò°|ùr,Z¸¹¹¹ªãxœ-Z`È!F—=+„etQ#°@DDDDÈÈÈð/,.~BŒ T繞¸¸8¼ýÖ[†N¨êÕ«‡   §;oÞ<§é Üyµøÿ‰ÿ pöìYÕq<’Éd„×^3¼©#€ñÇ -j6ˆˆˆˆ|\ZZZa2} )Û©Îr=ÙÙÙ˜®·2›Í¸·cG<ôÐChÓ¦Û­ö¿¿B±dÉ|¹t)Ο?¯:ŽG{òÉ'qÛm·]voffæÌˆˆ£ë‚ """"f³Ùú M›)«©Îr=ë֭Ç| lRu« ÎÿÏž5Ëécz£Úµk£ÿþèÓ§jÔ¬©:N‰N:…Å_|+V /Ï«îS¢a£Fxæ™gŒ.[,€g à0º°QØ """òA ÈËËû@/¨Îr=999xëÍ7±cÇ¥9œ}`rr2ìv»SÇô6ÍÃÂ0dð`tà˜Íî9u9uê–,^Œ/¿ü/^TÇ+˜L&Lž<þÆ–rj„Å’flQc¹çŸ"""""r›Í*e,ª³\‘7ü_³Ÿœ7w®SÇó~~~èØ±#}ì1„‡‡«ŽsU¿üò /^ŒU«V¡° @u¯òÌ3Ï eË–Æâ'?¿IÆ5DDDD>$Ínï/€y‚Ug¹–sçÎáÓ©S±jÕ*ÕQþ¸å–[œ6ÞîÝ»‘ššê´ñ¼AõêÕÑ»wo<úðC¬Y³Fu”¿iÔ¨‘ÓÆÚ³{7ÒÒ¼ú˜ñu™L&tê܃F«V­Tǹ¦}ûöaÞܹHLL„”Ru¯6rÔ(4lØÐè²g¥”#.ª DDDD^Ìf³õ5 àæ[þ­V+&ŒãÇÝóéíF;m¬™3g:m,OS¹re<Ô¿?ŒÚµk«ŽsM©))˜;w.¾ûî;ÕQ|B»víððÃ^Wã-‘‘¿^X6ˆˆˆˆ¼PFF†AQÑ<¯:˵cö¬Yˆ‰‰qëmÕ´à»ï¾ƒÕjuÊXž¤F2d ˆn¸AuœkJJJÂüyóxGƒ‚ƒƒ1qÒ$!Œ.½'""b†ÑEUb€ˆˆˆÈËìÛ·¯~±®/@;ÕY®åرc˜0~릛n˜±c ¯+T½¸xŠá…ÝDDDD.%%¥¦Él^à~ÕY®%>>“'MÂéÓ§UG)g¼°ëÛo½vk¹¦ièÔ©ž:-Z¸õC8{ö,–-[†/—.õ¸ß‡ÞFI“'#8ØðûI 4àñȨ¨"£ »6ˆˆˆˆŒ±cÇâ@f¦ê(åVѱoß>'¥QÏl6£[÷îxöÙgQ¿~}Õq®é—_~ÁâÅ‹ñõªU(((P‡þаQ#Œ5JEéݘb±XTÔv ly˜ƒœ?~„xZu–«‘RbÙ²eøä“OPèᯆhH)1ÛKVÿ/­ø?ýôÓ¨[×m7œ8€11ؼy³[?/é‹üðî»ïªx¢@O 0Àataw‘±Ûíõt)WBˆ6ª³\Í™3g0qâD$ÄÇ«ŽRaBˆ moOLH@FF†ó) iºté‚çGŒÀ-·Ü¢:Î5ýøãXƒ­[·BJ©:•àߣGã¶Ûn3¼®Æ[""|vëÿ%lyˆ´´´{…¦-PKu–«Ù³{7&L˜€'N¨ŽâµjÕBåÊ•Ëõ³RJÌž=ÛɉŒc2™Ð£G<óÌ3¨çæ[ý“’’3>RRRTG¡kèÚµ+ h|a!’îß?52"ÂøÚn† """"÷'¬vû BÓ>à§:LIæÎ‹¹sæxՖ늼üщiŒqiÅÿ_Ï? ¨ŽsURJ$&&bþ¼y^û‚7©W¿>^ã Ãë à¢îp<íë[ÿ/a€ˆˆˆÈeddT),,\!ú«Îr5ÇÇØ1c¼ê¢»KÊ{þßÏþkš†|Æ së­þ‡›6nDLL >¬:•‚¿¿?>øàT©REEùñ‘‘‘ž}Ç‰Ø """rSéé麾BÜ®:ËÕl‹Ã¤I“pþüyÕQ\¢¼/ìØ±û÷ïwr×B Ë}÷á_ÿú—[?çWXP€µk×bÑ¢E8~ü¸ê8T/æÍ›_XˆäÌýû?àÖÿ?±@DDDä†Òìö.B×—Aˆª³”¤°°S§NÅ—K—ªŽâR7.óÏxÒêxx8F½ø""##UG¹ª .`ÍêÕX´h~ûí7Õq¨ŒîëÚ?ü°ŠÒ ëOpëÿ_±@DDDäfl6Û0LƒnyÞÿèÑ£óÊ+³Â]å9°mëVdffº ó4jÔÿ|î9tíÚUu”«ÊËËÃòå˱háBäææªŽCåP¯~}¼¡àÜ?ो倒ânŒ """"7qðàÁ€¼¼¼éxJu–«ñö-ÿ— FõêÕËô3RJÌ™;×E‰*®V­ZxfØ0ôíÛš¦©ŽS¢œœ,_¾K/ö‰ßgÞÊl6ã·ßVuîMDD„ç>ÁáBl¹´´´…É´À½ª³”¤° S?ýÔë·ü_®Œ ߦ¤¸‡`€ˆˆˆH aµZ߀¯ªÃ\©° ï¿ÿ>¾þúkÕQ”iP†€®ë˜3gŽ Ó”^»víðâ‹/¢ih¨ê(%JIIÁüy󜜬: 9™ŸŸ>š2EÉn¤øûù©éáýï’:DDDD±Z­wAˆ¯PKu–+9Ì;sçÌáylõêÕƒÙ\º¿*oܸYGޏ8Ñßiš†|/Ž…5k^ÿz¬V+fÍœ‰Ýœø{½Ûn» &LPS\Ê•‹e¡šâž‡ """"¤ÙíO !fp»%ÚœœŒ7Ž+´—)íù]×1oî\§ù»Ûo¿¯¼òŠªgÖ®Éf³!fþ|$&&ªŽB¨Q£¦~ú©’ç%%ðs€¿ÿ³†ö`l¹ÐŠ+LMBCßÀÕYJòý÷ßcì˜18uНf]®´ €ß|ƒ¬¬,׆¹LíÚµ1êÅqÿý÷C÷º;’[ý}ÙlÆû|€Zµ”ljÒ…”‡……å¨(î©Ø """r‘ýû÷ßÐ$4t‰¢Ug)ɪU«ð޻¸Xu·sK)º®cÞ¼y¤1xð`<ýÌ3¨T©’!5KËn·cþ¼y\ñ÷A/½ô¢¢¢Ô—òM‹Å¯¦¸çb€ˆˆˆÈ¬VkS!Ä:ܦ:Ë•.\¸€É“&aóæÍª£¸­ÒìØ°aƒËWÿ…èÖ½;FŽ‰Úµk»´VYÙívÌž5 IIIª£ÑÑÑ}ú@Ó4—Ô(}ûöaö¬YøöÛoUG!…j×®?þXÕ““:¤|4Âb9®¢¸7`€ˆˆˆÈ âããÍÕªU›"Tg¹’” .Ä´Ï>ã¥Ð Aƒk^°W\\ì’›ÿ…x°GŒ=ÕªUsúøåu 3sçÎÅÖ­[!¥T‡ ÀGS¦({vRã#-–8%ŽDDDD”‘‘Q½ZµjË%ÐEu–+={ã_}•«¶ep½ c׭ñcÇœZ³yXƇ–-[:uÜŠø1#³fÍÂÎ;9ñ'!0ùÍ7Ñ¢E UÖGFD¼¯ª¸·`€ˆˆˆ¨¬VkS  ™ê,W:xð ^|ñEûùgÕQ<ʵ.,,,Äœ9sœV«jÕªø×óÏ£ÿþn³ÝÿÈ‘#˜>m¶oßΉ?ýéégžÁý÷߯ªüÑ‚‚àoÈ b€ˆˆˆ¨œl6[7ñ€ÕY®´cǼ6aòòòTGñ8×j¬\±ÙÙÙ®!„@tt4FŽ…êÕ«WxØ’ãý÷ÞÃÊ•+UGñ*õê×ÿÛ×bæÏÇ… ¥_ˆìСÆŽ‡›o¾Ù™ÑÊeïÞ½øì?ÿAzzºê(ä!ž>½{÷V@Ê)‘«Ôðnl]…Ýnšö€Jª³\.77¯¼ü2öìÙ£:Š×©Å€'N`ÅŠ¥úÙ5kbÔÈ‘èíŠherðàAÌ™=q¼’Ê o¿~ÊžûûÖ3gÎŒSÀÛ±@DDDT«Õú„˜ @ýuí—9tèF…c?ÿ¬:ŠWª{E`ÎìÙ(¸ÎÓxš¦á¡þýñ /”x ‘Ž=ŠéÓ¦aëÖ­R*ÍBž¥C‡?~¼Êû!åÃ;väy&b€ˆˆˆè 6›m „x€PårI»va̘1|²Í…ê_vàÈ‘#X½zõ5?ߤI¼öúëwu´kúí·ß0göl¬Y³‡Ciò<-Z´À{ï¿“ɤ*B¤Œ¶X,¼ÉÔÅØ """úÊ+LM›6ý BüSu–+}¾h>ýôS躮:Š×ª\¹2BBBþüç§L¹êd: Æ ÃãO<¡ô’¿¼¼<ÄÄÄ`ÉâÅ×Ý©@T’ `Úôé R¡X 102"‪¾„ """" hú9€ª³\®°°o½ù&bccUGñz7Þx㟿þî»ïðí·ß–ø¹¶mÛbüøñ%^h]×±zõjÌœ1§NR–ƒ<[š51}ÆŒ¿4¾ 'åÈȈˆmêø6ˆˆˆÈçY­Ök!DÕY.wæÌü{ôh¤¤¤¨ŽâjÖ¬ à÷ÉõÇS¦üíûÁÁÁñ èׯ„Pw:d÷îݘ2e df*Ë@ž¯J•*˜6mêÖ­«,ƒb"-–Êø 6ˆˆˆÈ§¥¤¤Ül2›7ˆPårû÷ïǨ‘#‘­:ŠÏþctá‚8pàÿw# !ЫW/¼øÒKVÄ'Œ¤¤$eÈ;â³iÓpÛm·)Ë ~~nwÜÊÛ±@DDD>+55µ¹ÉÏo¤¼Eu–ËmÛ¶ ¯M˜€üü|ÕQ|ʹsç°iãFLŸ>ýϯ5lØ^{ QQQÊr:u 3gÌÀêÕ«yU˜ŸŸ¦L™‹Å¢2ÆèúCaaa…*Cø"ÁçAˆˆˆÈÙíö6º”ëÔTå)%æÍ›‡™3fð 7.më—RÂßßOŠ¡C‡Âßß_Iž‚‚,Y¼111ÈËËS’¼‹¦ixçÝwÑ­[7•1ÎiBܾOe_ÅDDDäsÒìö^BÊ/TRå’‚L~óMlX¿^uŸu©éÒ2<'NDãÆ•åØºu+>:ÇW’¼ã^}Uõä_ÀNþÕa€ˆˆˆ|ŠÕjý‡b.ÜèïA'NœÀ¨‘#‘‘‘¡:ŠO ¿žƒ†¦iJ2¤¥¥áã)S°oçGä\#FŒ@ÿþý•fÀ+–ˆv9r›ÿñ¹šÕj!ÞPãJ1óçsò¯X«V­ðúo AƒJêgggcÚ´iøfÃÿ §öì³xrèPµ!¤œi±üýy 2DDDäõV¬Xa !†«ÎR’]»v©Žà³ªT©‚Q/¾¨ìi¿sçÎaAL –,]ŠÂ‚Ãë“÷{ä‘GðÏ*¾l_ˆoΜ>ÍÿÝ/$"""¯vðàÁ€ó.|)¨ÎR’ãÇ£gªcø¤{ŽÚµk^»¸¸k׮ŌéÓ‘““cx}ò ƒ ˜±cÕ†â{³¹SXXØyµAà"""òbéééÕŠu}­îQåj¸úo¼êÕ«ã•1c”]†–´k>üðCdee)©O¾¡_¿~xeÌÕ1›5­''ÿîƒ """òJiiiu„…᪳\K†êÚµ+ƽú*ªU«fxí_ýŸ}ö_z —ëѳ'ÆO˜ äXËeN àÛo¿ýW•!è¯Ø """¯“––MÛ ¾ê,×RTT„½{÷ªŽájÖ¬‰q¯¾ŠÎ;^»   ,À PÀsþäbݺuäI“”½dñ‡|©ë½,‘‘™*CÐß±@DDD^%Õno«iZ,€šª³\OZj*.\¸ :†WBàÁ=ðòË/#88Øðú øðƒpüøqÃk“ïéÖ½;Þ~ûm˜L&•1rHddd²ÊT26ˆˆˆÈkX­ÖÞš_R¥4’’’TGðjõêÕÃk¯½†;Û´1¼öO?ý„?øß~û­áµÉ7uà¼ýöÛªWþ)GZ,–5jCÐÕ°@DDD^Áf³ …³áA¿IJæ™+hš†G}Ï=÷ ­}áÂÌ3K–,AQQ‘¡µÉwõéÓ¯½þºòÉ¿&GX,Ó•† kò˜ÿA]Íf#÷Tç(‹“'OâàªcxzõêaÒ¤Ihexí„„¼÷î»ÈÎÎ6¼6ù.w™üX1Quº66ˆˆˆÈc­X±ÂÔ$4tºžU¥¬víÚ)¥ê^CÓ4 <#FŒ0|ÕÿÀxÿ½÷’’bh]¢þ`ܸqÊ'ÿØàç7ô÷_’;c€ˆˆˆ¤ìa±|«:UDDDävÒìöABÊÅLª³8Sff&N<©:†[»»}{Lš85jÖ4¬æŽ;ðþ{ïá×_5¬&ÑÕ4oÞŸM›†5j¨Ž…r€Åb‰W„œƒ """r+i6ÛcX/›üÀ®o¹€v5xá…0xÈ! ©yòäI|ðþûˆ‹‹3¤Ñõ´mÛM™bè—×P,€AËÕAÈyØ """·‘f·?ä­“€çÿ¯&,, o¿ý66jdH=]×±zõj|òñÇÈËË3¤&ÑõôîÝ^{ f³[LÑŠ¥YÂÃW«BÎå¿»ˆˆˆˆìv{g!åxéä?//6›Mu ·¢ižxâ üó¹çàççgHÍ™™xó­·n·Rèz„öì³>|¸ê(—I!G†‡¯R„œ """RÎf³Ý)ñìÝw|Í×ÿð×ùÜìˆIÅÞA$w¸!BT¬ªÚ{S£ŠÖ^µ÷Þê«CU­-míVS;HîHAU­ˆ„L÷ÞÏùýaüP#ã~î¹ã<<"ɽçýâÉý¼?g`WÖY¤ræÌFÖ1¬FÙ²e1oÞ<ÔU«-R/77›6mÂ×7Â`0X¤&ǽ‰L&ÃÔiÓйsgÖQžÈ¥=T ÅnÖA8iðÇqÇqLéõú ìPŒu)ñéÿÿ¯c§N˜0a‚ÅÖ9Ÿå@Óÿ4h€ùó磔¯¯äµ²²²°jåJüôÓOü®?g•6j„E‹¡xñ⬣<ë6ÞQªTçYáØà Žã8Žã$§Õjû€¥¬s\ˆÇÇŒû÷ï?÷y“É$YÍ'NH6¶µprrÂà!C0tèP‚ y½Ó§OcÎìÙü®?g•!xÿý÷ññÈ‘ùÿP—@é» ¥òÖA8vx€ã8Žã8Iibcß&„l ýð¯qæÌŒ;YYYÿùš(QàÎ;¸víš$c[‹J•*aÑâŨ]»¶äµ²²²°zÕ*ìÚµ‹ßõ笒»»;fÏ™ƒwÞa¾ÍÉ‹ÎRQl£R©î±±ÅÇqÇI&&&¦¶ “íàÊ2GTTFõʵøF‰ö~ü_Ûví0eÊxxxH^ëLTæÌ™ƒÛ·oK^‹ã £bÅŠX¾|9j°Žò¥ô•ÉîË(½O)}`4ï«ÕêûÌÿêšã8«´cÇYõ€€ï Â2ǹsç0zÔ(äää¼ñ±Rl¨×é^ºß€­"„ oß¾9jœ%­•——‡ÿýïغe‹$ÍŽ3___,Y²uÕjÖQ^D 0^!—¯b„³>¼ÀqŒÄÆÆ– ”V¡”V*P‰RZÀ[ Ä€_€_ äÉKòxê#€']æ'Ó!)•¢½ë„ÿŸºûè=!Ï×yœƒ<“‹ˆâÓ«}™“´:Àm7\§ÀM¸I¹.Šâ¼¼¼õë×O1|ŽãX¨°@;–âââò}ñHÓ8aGëÿK”(¹óæ!<<\òZW®\Á´©S‘ y-Ž+¬,^¼Ø"G^P(¨P*¿c„³N¼ÀqÒëõUDQ ¤„T%@•ñè}ÞO÷ìųrPéñ[ø“¿/¥ .®®ÐêtÙ®à¥ô%ä"¡ô¢ÉdJP«Õi,Ãs—n<>b™áúõë=j>|˜ï爬-·—õÿõêÕÂ… áçç'iJ)~úé',_¶,ß޳4Bzöê…qãÆÁÉÉê.¥2 ÐE¡Tb„³^V÷]Ëq¶H¯×{¢”*( §€œrÞ „í¹W¶Ã@‚žþ›òdÁ7â!ºœœ}hhh:ÛÈÇ=K£×w%ÀR–îÞ½‹a~ˆû÷ïèy&£Ñ¬9R’“qéÒ%³Žii–<Ë<%9³gÏÆñãÇ%­ÃqEQªT)Ì›?aaa¬£¼LÅ6J•êë œuã Ž+ +W®¸fff†PBJÕ D €§¯Žø¿Ù•P†MŸ,Apus£Z­öoBˆVt D“I§R©®³ËqŽH«Õ6$„|‹g~ZZjj*† †ÄÄÄ?×ÜëÌOž:eÓ;Ö{zzbΜ9hÞ¢…äµþøýwÌ›7ii|²g½BCC±`Ákœò  ´­J¥ºÌ:gýx€ãÞàÒ¥K^Y¹¹ ¢Ø˜R„¸=¹CÍ1C@H5 T#@P ´:Ý œ€(Jé“É¥V«“Y‡å8{¦Õjk€=x4“‡‰¬¬,Œ>ÿ\»V¨çͼÀI^ÿ_¥J¬\µ •+W–´NVV-Z„½¿ý&iŽ+ '''Œ9Ò:wù@#N‚Ð588øë,œmà Ž{A\\\iƒ(†QlL iL…Èüw<Ε @+ ´zº„@«½ Aˆ¢@” ˆòððÐV¯^ýõg‚q—/ÑÑѾ2'§½˜ÝËËÍŘѣqáÂ…B!š± Š"NÙh yóæ˜3w.<==%­‡)S¦àæ’ÖḢ¨X±",\ˆ   ÖQ^Š_ˆFãÈ`µÚÀ: g;x€sxñññÅòòòZ€Öš¨ùäî¾õõy¹B!¤(­F€Þ"€Ì¬¬l­NJÿ™–Í:&ÇÙšS§N¹»{xìPƒU“Ʉɓ'ãܹ¢-{5™q Àùóçmn:» øè£0pÐ IïrŠ¢ˆÍ›7cýÿþ£ö]ðôô„Lö¨G_¼xñç¾–“C^Þsµ333‹\“s ;wÆø àááÁ:Ê˘@Èx•\¾†uÎöðçbcc«Š¢Ø‚>:¦ª%qe‰³(w $À,w£N§Ó‰Àï2BN †£üäŽ{#âîéù(mÈ*¥sçÌAddd‘Ç2ç [ÛýßÛÛ‹/–|c³äädLŸ6 QQQEÇËË .Dƒ àìì\àçF<|øyyyÈÉÉAvv6r²³‘žžŽŒŒ ¤gd ==ýÑÇß?ýøñײ²²Šôwà¬W‰%0sÖ,DDD°Žò*™!½årù¯¬ƒp¶‰78‡íáääÔŒRúÞã;ý•Yg⬊ÔP‹”Bæä”§ÕëO8(år¹€ywã8§Õj—‚Þ,3¬Zµ ¿üò‹YÆ2ç&€'lhú­Zµ°|Å ”+WNÒ:ÇŽì™3ñàAÑ—)gdd`ô¨Q@Æ Ñ , *•*ßÍ''§ÿÌ((QŸk ¤§§#55)))¸—”„””$=óž7 lCÃF0wÎkÝè® \.?Ï:g»ˆ-ïPËq¯WM&Šm)¥­  ÜXgâlV€Ã8è,“ ºË:DZ¤ÓéRàk–6}ý5Ö®]k¶ñêªÕظqc‘ÇIKKC³¦MÍ~ª€Ú´m‹3fÀÕUºIpyyyX½z5¶ÿ½¤§"¸¹¹!((uÕj¨T*Èår«šº››‹{II¸—œŒääd$ß»‡äš÷ïß/ðñ•œy¸¹¹aô˜1èÑ£‡Unô÷Øq*ŠU*Õ=ÖA8ÛÆœ]‰­jÅ.ë<œ]¢t8 Šâ/*•* |vç@´Zm#òfK§víÚ…óç›õ‚R©TbÓ7ßyœƒâ“É“‹HBNNN?~΢’ °ŸRú« ûär9ŸçÉÙ-FS‰B€Ò¬2ü‰qãÆ™ý{°\Žo¿ý¶ÈãÌš9ÓlˤP¢D ,[¾jµZÒ:öïÇüùó­j껟Ÿä›Ár9k׆‹„³¤——‡ÿýׯ_ǿׯãúõë¸ví®_¿nsO²æâêŠFŒ@ß~ý ë8¯BAé\¥R9›uÎ~ðg“4M%BWtPü¢Ÿ³™8({\÷ò¹œœÝÐëõž"¥'(Xe8þ<† Œœœ³]§Nlݶ­HcPJñNË–HNN6S*óªY³&V­^2eÊHV#/7Ë–/ÇÎ;$«a.NNN¨U«‚årȃƒ!W(P¶lYÖ± --5 —/#!!—€«W¯"¸G‚‚‚0wÞ™"žÿ&ÕªUÃÎ]»Š4ÆÁƒm¦DEçááyóç£Y³f’Õ0™Løßºuøæ›ofs:___B©R=Úd08NN¶}"%%qqqÐiµ8…K/ÚÄIùáææ†¡~ˆXóZ0`²B¡XÅ:gßx€³QQQÅ]ÜÝ;’Gwû[°ÎÃq B~£&Ó—*•ê<:a€ã¬†N§SQà8&ç©QJ1cÆ ìýí7ÉkU®R?ÿüs¡ŸŸ••…ˆ&M`4͘ªðÊW¨€U«V¡zõê’ÕHLLĤI“«×KVÃxxx *• ¥jµÅŠc«H²²²‡¨Ó§¡Ñhpþüy›\6Ð(<Ó§O·… ’BzÉåò#¬ƒpö78æbbcÕ‚(ЀmÿÆä¸B¢À ߉2ÙuPÐUÖy8îl|¼¿“Áp†Xeøßºuøê«¯,R«B… øå×_ ýü?ÿüãÆŽ5c¢Â Å’¥Káíí-YÈÈHÌš9éé|EÓ‹œœœP»vm( ¨CB V«áååÅ:V‘deeA§ÓAƒ˜˜ÄÅÅYõæ‚%K–Ä„‰ѺukÖQÞˆÇD£±‡Z­¾Ã: çx€c"66¶„ÉdB†`‡ã¬ˆHH²YìâÇ r,Dþó›OZZ$m¸ÊÄÏ?ý„¹sçZ¬^¹råðÛÞ½…~þ‚ùó±sçN3&*œÞ}ú`ܸq’mpf4±vÍlݺÕa¦ü•  DýÐPÔ¯_J¥®6vüà‹òrs‡èèhDEEA¯×[Å AЩS'Œ=Å‹gçM(YžzÿþÔˆˆë˜:Ä9Þà,*:6¶–ÌdBúðd‡ã¬\¶‰¢ø¹J¥Ò±Ã9­N·@_VõÏž=‹FŒ°è…¿¿?ö8Pèç·mÓ·nÝ2c¢‚qqqÁ”©SѱcGÉj$&&bÊ'Ÿ@«ÕJVÃÈd2Ô¬‰¡¡mÐuëÖ…³³3ëXE’““N‡¨Ó§q:* /\°xƒ¨F@¦OŸ¹\nѺ…”N€ …â'ÖA8ÇÜEètºp LÐüø>Ž+0Dø’²Ï तÑé>!À"Võ0hà@‹ï&ÿÖ[oáà¡C…zîµk×йS'3'Ê?___¬X¹RÒ Ÿ“'Obê”)HKã“››§§'BBBP¿~}Ô •tßKINNÆéS§püøqœÚï \E±‹J¥Šg…sL¼ÀI&::ÚÃÉÉ©Fd‡ãìIJÙDÈêà`Û=l›³Jz½¾HénL¶Ê¾{÷.ú÷뇤¤$‹×.åë‹ßÿ½PÏݺu+V,_næDùS§N¬\µ o½õ–$ãSJ±qãF|¶~½Ýì oíüýýѰaC4 Ghh(<=m{¤Éd‚F£ÁñcÇpôèQ³çÙ¼E Lœ8¥K—6Û˜Û*2Œ7ò9–x€3;½^_žRú¥ôRŠu޳SÀ_”uW.]ÚÝ­[7ë@œmÓëõA"¥'0Ù­,++ Äå„åáããƒ?## õÜFŒÀÉ“'Í(Ú´iƒ™3gÂE¢õä™™™˜1}:" ùï³³3T*5j„Fáá¨V­ëHEvóæMüyä>Œ¸¸¸B-¨]»6ÆO˜µZ-ABI¤Sàc•B±…uŽã Îl´Z­’2™]Øöb6•••eówÔ5|š““³144”oÉÍØ™3gJ¹¸ºžP…E}£ÑˆQ#GâÔ©S,ÊŠ/Ž¿Ž-ðórrrФIäåæJêåAÀèÑ£ÑÀÉj\½zãÇÃõë×%«Áœ¿¿?ÂÃÃÑ(<õëׇ‡“:Í&11‡ÆáC‡ûÆÇûúúâã‘#Ñ®];“‰JF€Ó‚ ô þ›uŽx€3ƒ˜¸8¹ ŠÓAiWðõýf'Š"ÒÒÒ–šŠÔÇ臘"-- Y™™ÈÍËCVf&rrr——‡ôôtäåå!'7™0 xøðásc>yìë‚ðôcBÈsGyyyÁÅÅż¼PÌÓÅŠCñâÅQ¬X1x+†bß¼½½áïï___899™ÿ‡{Q€ïBVÊåòK¬Ãp6CÐét{)ð.«sæÌÁîŸfUÀ£uØÇOœ(ðóN?Ž?þX‚D/çåå…Å‹£a£F’Õ8xð æÎ™óŸßœuqqqJ¥BDÓ¦ˆˆˆ°…³î_+!!?lߎ}ûö!''繯UªT }úôA»öíáææÆ(a‰Xg4'¨ÕjöG$pÜc¼ÀšF£Q™l¿ð/J)RRRpçöm$Þ½‹ÄÄDܹs‰wîàþýûHKKCêã }[ÿ*J–,‰Ò¥KÃÏÏþeÊà-??¼Uº4*Uª„Ê•+?m6pf!ØGEqJ¥*Ü¢fÎahµÚ… d «ú[¶lÁÊ+X•ÊÝÝ' 1aéÒ¥øþ»ï$Hô_•«TÁêÕ«Q©R%IÆ7™LX³z5¶lá³”m !µj×F³fÍaÓ ¦§§cßÞ½¸|ù2¼¼¼P?4 4°™;þ@Å~*•ê/ÖY8îE¼À˜F£ a&Ë»E¶$%%W®\ÁåË—qõêUܼq‰wïânb¢Uœ™k-Jùú¢Jåʨ\¹2*Vª„ªUª RåÊ([¶¬MýÒ·Bg°X¡PìÁ£ÆÇ=¥Õj;‚ŸÀ¨‰{âøqŒ5Ê*6—sqqAÔ™3~^Ç,2M¾aÆX¼dÉs³±Ì)%%“'MBtt´$ãs–U¡B4mÖ M›6…\.ç¿G-‰2B> ~À: ǽ opù¦Õj>¾KÔ–uk$Š"._¾ŒóçÏãÊ•+¸zå ššÊ:šMóôôDõêÕP³&P³fMT¯^îîÙJ¯B–ÆM|*":.€gx³¨íÚ5ôï×™™™,Êÿ‡L&ù^üÞºu mÛ´‘(ÑÿëÓ§Æ/ÙE\¬^ &09}“^©R¥Ð¤I4mÖ ¡¡¡pvæÛ4I$„LQÊåkXá¸×á ît:]8&ƒ_ø?Ç`0 >>111ÐÄÄ@£ÑXÍ Y{'*T¨€š5k¢N: BíÀ@ÞÈŸë d•É`Ø V«ù__,Ï`8  ‹úiiiè߯þý÷_å_Š‚¦@ÏÙñãX¸p¡D‰í?uêTtìÔI²;~üË–-ã3Ò„··7š5oŽw[µBH½z|f€¹rŽPÚG¡P°9Ƅ㠀7¸WÒh4!„%hÆ:‹5ÈÉÉÁ…  Õjuú4´Z-r-¸ë3÷z‚  råʨˆÀÚµ¡R©P³V-þâæÕîR`u^NÎz~r€Ã!Z½þPÚEqQ1jÔ(œ8~œEù׊Ñh@HþWCŒ3ItDž··7–¯XIÆÏËÍÅâ%KðóO?I2>gý¼½½Ñ¢E ´iÛJ¥²@ßûÜS"–‰Fã >»Ž³¼ÀýGLLLuA&[ xs?Q‡cG"** ñññ0™øQë¶ÄÓÓJ• jµjµuêÔL&cËÚ< ÀJ77·55kÖÌ`†“žV«BV±ª¿dñblß¾Uù×:söl¾§G D4i"ÉNù5jÔÀê5kP¶lY³ ·o߯„ñãqáÂIÆçlO™2eЪU+¼Ûº5jÖ¬É:Ž-É À£Ñø)ŸUÇÙ ÞàžŠ‹‹+mÅ™ ô¹@,-- §O±cÇpâÄ ¾~ßθ»»C©T¢îã†@PP_ ùÿ’ °œ²N.—g±ÃI#F¯o PzŒ~Æïܹ æÏgQ:_NGEÁÕÕ5_=sæ >:Ôì"""°`áBÉÎw?uê¦|ò ÒÒÒ$Ÿ³}ÕªUCçÎѦm[x{3Ù"ÄÝ¡„Ì †¯øLÎÚñ‡ÈþqóIO J§f{arcÙÄ IDAT+v9!ÇDZcÇ Óé¬b7jÎ2ÜÜÜ  AXX6lˆ*Uª°Žd ’°äáÇŸ………e³Ùϙ3gJ¹¸¹Å€ÒŠ,êŸ;wÇ ƒÑhdQ>_Nœ<™ï ïÕ«VaóæÍf«MÁÀAƒðÑGI¶tióæÍX»f ÿ=Çå‹‹«+Z4oŽN;C­Vó%ùAéU*3Trùvü"‹³J¼ààôz};‘ÒÕª²Îb)&“ QQQøóÈ?~‰‰‰¬#qVÂßßa ¢aXꇆ¢xñâ¬#±t+>|¸–7ìÑêõ?ƒÒ,Šß¼yýúöµúYUÇŽG±bÅòõØîÝ»ãr‚yöûrquÅìٳѺuk³Œ÷¢ÜÜ\Ì™=û÷ï—d|ÎþUªT :wF»víP²dIÖq¬!ç¨É4A¥RýÅ: ǽˆ7”N§SQBVÒ&¬³X‚(Š8wî:„ßÿÝê_„rì ‚€àà`4‰ˆ@ÓˆTvÐÙ¸!³¾íÖ­ßÃFétºÉXÌ¢vVVôï«W¯²(_ ‘‘‘ðöñyãã’’’ðn«V0Çk(???¬\µ AAAEëe1nìX¾ÞŸ3 ggg´iÓý à³æò?J?Q*•—Ygá¸'xÀÁhµZBÈ |À®wCEZ­‡ÂáßGJr2ëHœ «T©š6mŠ&ËåŽwº! г”JåÖQ¸‚Ñh4aDþƒuÿ¢(bì˜18zô¨¥KÊGŽäëîæîÝ»1göì"× ÄªÕ«ñÖ[oy¬—‰ŽŽÆÄ ðàÁIÆç— ˆˆˆÀÇ#GòFÀ›l™!—Ë“X‡á8ÞpD§Óõ¡Àr¥Y‡‘ ¥q±±8xð >Œ¤$þs–3¿’%K>šд)4hàh F‚ÒIJ¥ò,ë Ü›éõú·DJ5¤ÙNþ Ö¬^o¾ù†EéB9üûïðõõ}ãã&MœˆÃ‡©V«V­0gîÜ|o:XP?üð–/[fÕ{.p¶O&“¡K—.;nÜÜÜXDZv(0U¥P| €oÄÁ1Ã@§ÓP`€–¬³HåæÍ›Ø³{7öîÝ‹;wî°ŽÃ9âÅ‹£Y³fx§U+„††:ÒÌ€ß@é8>­Ñª­N÷ €¶,Šïýí7LŸ>EéB;pð J—~}Üd2¡YÓ¦HOO/T B†!C†H²©Z^^-Z„Ý?ÿlö±9îUªW¯Ž_íè{çä b¥Ãx#c…7ìX||¼‹Á`˜F)°ÃcýòrsñÇ‘#ØýóÏ8{ö¬YÖbr\Q”(Q-Z¶D«V­ R©¡` ÀçÎÎγï³Ã=O£Ó'f}Y\¬^!|€¼Ü\å mßþý(S¦Ìk£Õj1ðý÷ 5¾»»;æÍŸæÍ›êùorïÞ=Œ?±z½$ãsÜ«T«V ŸñE¾fÐp#€O]œgf²Ã9Þ°S:NET¬³˜Û¥K—°û矱oß¾Bßá8©ùùùáÝwßE»öíQ£F Öq¤ö„ÌI½ÿ|¾±ˆ‰U ¢x€‹¥k§$'£gÏžH¶Á}W~ýõW”¯PáµY¿~=6|ùeÇö÷÷ÇšµkPØx¯«×cüøñ¸wïž$ãsÜËxxx`ðàÁèÛ·/\$ZÎbçnSB¦¨äòoYáoØ™èèhÁÉiFÃŽ6ùËÈÈÀþýû±{÷n\ˆg‡ã ¤víÚhß¾=Z·n¯ÆmX%dŒJ.ÿƒuG_,Ï`ˆ`ñΓÑhćC‡"&&ÆÒ¥Íb÷ž=¨T©ÒkÓ·Oœ?¾@ã* ¬X¹¥J•*J¼WÚ½{7-\ˆ¼¼lúàÕ+W®àûï¾ÃÞ½{ù4Ρ•.];wFÇNìî Š2ÙGê  «¬³Ø£èèhg™“Óqõ-];33}ûôÁõë×-]Úì¾ûî;Ô |é×(¥hÙ¢RRR^;F‰%°rÕ*(•Òôå8€Ù³g#''G’ñ9xtbE§Î1`À»û}dSùÎd0ŒP«Õi¬£pöƒ7lPtttE™“Ó7š²ÎRX¢(âøñãønÛ6œ9s†áÇqÏÉdˆhÚ}úôJeWyäQ`Uš·÷ìˆÊ•ùÕ‹iuº%&Yº.¥ãÆŽEdd¤¥KKbËÖ­ zé×У{÷×>¿J•*Xûé§(_¾¼Ù³‰¢ˆÏÖ¯ÇÆùïLN2>>>èÙ³'zöìiï›ÖÚBþ…(öV*•'XGáìoØ^ß“Pú9oÖY Ãd2aß¾}ØøÕWvq·ˆã¤ˆ¾ýú¡eË–ö´i`Âã©ü´3Ðjµ-AÈ‚¥k½q#>ýôSK—•Ìæo¿…\.é׾ݼ«V­zåsCCC±lùrxyy™=×Ç1cút9rÄìcs”)Sýû÷GÇNàæfÓKí•„ÌTÊåKäRŽ{ Þ°§Nrw÷ô\J?`¥0ŒF#ì߯ø…?ÇJ)__tíÚ½ìé® !ßœœÆ× ä›~’^¯‹Rª£€¿¥kŸ‰ŠÂðáà ´#¾µûzÓ¦Wκ>lNŸ>ýÒ¯uìÔ Ó¦M“¤Iw÷î]Œ;–ËI¢zõê0`ÞmÝÚžšÌv‹G ŠýT*ß„+4Þ°ѱ±µd&Ó äå·%¬X^^öìÞM›6áÎ;¬ãpœÍswwGÛvíЯ_?T¨PusH#À$…B±ÿ…T0D£Óí!@;KNJJBÏ=ðàÁK—–Ô†¯¾BHHÈ>Ÿ—›‹·ß~û?ûÔ‚€1cÆ _iN]Ôh4˜0~<îß¿/Éøœãª[·.Þ8ááá’œRÁI*Y d\.ÿ•uÎ6ñ€•Ójµïƒuçî‹!""B’,»wïÆÂ `0$Ÿs<„¼Ý¤ …BÁ:ŽU¸pájÔ¨a‹³(YžzÿþÔˆˆ#ë0œm±¹ïvG_,Ï`ø „ôe¥ ²³³±sçNlÞ¼)ÉɬãXTã·ßÆÞß~cƒs¢(âÀþý8xà7nŒAƒÛú º– ä¼N§››°¼[·n&Ö¬Ylll0€…,j¯_¿Þ./þÀôŠå §Ozîã·Þz kÖ®E­ZµÌžAE¬Z¹[·n5ûØœcruuEÛ¶mÑ·o_T®R…u«`4±fõjlÛ¶ å˗Lj#ðN«V‹o¥RX”Nôññ =߃/¥ã ‚ϰBz½>H¤t'€š¬³ä—ÑhÄ®]»ðå_8ìTÅõŸ}†dzŽÁ90µZÁC† ,,Œu”¢:a„!êàà‹¬ƒX£Ç âhŸúqòÄ Œ9Ò®Öý?ëÓuëþŸÏ÷îÕ .\ðhcÎU«WKr4Zff&&OžŒ“'øfß\Ñ•,YÝ»wG÷=P¢D Öq¬Fbb"&Mš„X½þ¹Ïàã‘#ѸqcFÉ í(í®T*O²ÂÙÞ°2:®6†¦ü9rk׬qèÍýJ–,‰Ã¿ÿŽ·7FVVë8œƒ İáÃmñEÌSÈ¡„ÌM½ŸÞø<­N÷5€–®k¯ëþŸµzÍ4iÒä¹Ïåää ¼Q#˜L&´jÕ sæÎ…«««Ùkÿûï¿=z4þ¹vÍìcsŽ¥J•*èÛ¯Ú¶i ¾WmÙ±cÇ0cút¤¥¥½ò1*• #G²µcxó(0A¥PØÏ±,œdxÀJDFF:ùøø,!ãØÄn,±±±Xµr%4 ë(Ì…‡‡ãÓuë0dð`DGG³ŽÃq€`¹Ç·í„œ3ÒÏxD£×w!fˆY”ÉdÂСCcç?ßV®Z…¦M›>÷9N‡ï¿aÆსC%Ù0-** “&NäûÈpER¿~}ôí×oì÷&“ ÿ[·ß|ó ò{íŽGŽDÍš63! ä;*—ËùÝ(î•lf¡‹=;sæL)Ÿ%ö °‹ÿ›7n`ò¤Iп?¿ø¬NP@i[ÝbÎÎÅêõ1|8 ˆ³gϲŽS8”†8‰¢F§ÓM†ƒÿÎÒh4e‰(~Á¢ögŸ}f÷ÿ šþ»õăð¿õë1ôÃ%¹¨Ú¾};>1‚_üs…âää„÷Þ{ßoߎ/¾ü7æÿ/HJJÂÐ>À¦M›ò}ñÇGï^½0eÊܾm#§îQÚ[Å“¦ë(œõâ3Óét* ü 2ë,o’–šŠ 6àÇ仿àɺјèh <˜uŽ{©zõêaøˆ¶6­ñY‡ 0P¡PÜb„¢Õë¥ïYºðÉ“'1òãívÝÿ³–,]ŠwÞyÇ"µŒF#–,^Œ;->¡ƒ³^^^èÒ¥ zöê…Ò¥K³ŽcµN:…iS§yé’‹«+úôîÁC†ÀÓÓ&Vé&SAè¢ >Ê:g}x€!­Vû>!ä3 ¸±Îò:&“ ßÿ=¾üâ ddd°Žcuœù×_ððð€ÑhDD“&|Ϊ…‡‡cô˜1¨^½:ë(Gi „a*¹Ü¡®š´ZíG„µ¨{÷î¡g³¹ëÂE‹ÐºukÉ뤦¦bâ„ 8wîœäµ8ûR±bEôêÕ íÚ··• Q&DQÄ矎_}eÖæeÉ’%1|øptêÜ2™ÌlãJ9>P(üHî9¼À† Óé–R`<ë oƒÅ‹áòåˬ£X­zõêáË ž~}ШQ#[:ªŽ‰ÄÄDLŸ6MÒ=™ªU«†O¦LAHHˆd5Ì„‚ÒÅJ¥r:ûŸÆÅå ÿ baz½ÞS«Óí²ö‹ÿ””̘1CæÿoñÜÇá6¼ó:ç8DQÄž={С}{¬]»Ög÷¼Ÿg0œÓjµJÖA¤é$ÅͰðÅ?|î ëþŸe’x™ÃñãÇñþ€üâŸË777téÒ;vîÄgŸŽÆó‹ÿ78tèztï.ù†ÌW¯^ÅÐ>ÀìY³–š*i­"" dŠV¯ÿ5**ª8ë0œuà3,H§Ó•£À¯¬v.¥{öìÁª•+ù†Dù :„R¾¾O?—‘‘-Z /7—a26\]]Q®\9øúúÂÅÅîîîpusƒ«‹ <<=á”ÏérÙÙÙ0 0™LO—SdeeÁd2!//ÙÙÙÈÌÌDVV²³³‘““#å_Ë!x{{cÈ {÷îpqqa'ßC) T*ÿÇ:‹´zý|P:ÍÒuÏ;‡‡uˆuÿÏš5k:vê$ÉØßmÛ†+V8Ü¿)WpþþþèÞ£:wî oooÖqlBVV–,^Œ_ýÕâµK”(ñãÇK>{È 4&£±Z­¾Ã:ÇoXHL\œ\Å_AiEÖY^åÖ­[˜7w.¢¢¢XG±ÂñnÝ—åN›:ûöícÈ2\]]Q«V-!((•*U‚™2(Q¢“<”RdddàáÇÈÉÎFFf&ÒÒÒ½¥¦"5- éiiHMKCêƒHMMEJJ RRR ´#°#(_¾<ÆŽ‡fÍš±ŽR0„ìqqrh7‹ÕµZmCr€E𦥦¢{÷îHJJ²dY«0múttíÚÕ¬cšL&,^¼;wì0븜ýQ(èݧš7onõëË­I¬^©Ó¦áæLsÔ ÅôéÓQ¡B¦9Þà–(“½W7(HÏ:ÇoX€&6¶5Åx±Îò2¢(bû÷ßcݺuÈÎÎfǦ|µq#Ôjõ>9!=zô°»‹Ë²eË",, 5kÂËË NNNðöö†ÏÓ÷®®®¬cæ›ÑhDrr2îÞ½‹{IIHJJÂݤ$ܺu W¯^Åÿ…é%Ç‚9‚ú¡¡˜8q¢MmHÐ[¡Pg¥¨ôz½§H©@ KÖ¥”bÜØ±ˆŒŒ´dY«PµjUüoýzøûû›mÌŒŒ Lœ07Ö¹WrvvÆ;#Þ}ú 00u›b4ñõÆøòË/­æwµ»»;ÆŒ‹nݺYóqŒ¨ tä'8.Þ˜N§Ge°Òýnß¾™3fH¾VÊÕU«±qãÆW~}„ øã÷ß-˜È:”òõE¹²eQ®\¹GoåË£ZÕª¨Z­šÍíXl0pýúuüý÷߸rå ´ ôz=rdy‡L&C·nÝ0løp[š†j$Àt…B±€Íþ‚Óét_QÀâgŠþðÃX¼h‘¥Ë2‚+W¢xqó-‘½yãFŽ…®]3Û˜œýðóóC—.]Ð¥kWø>³ŒËN‡ùóæáÊ•+¬£¼TXXfÏ™ƒ·Þz‹u”—"@ŽHH_•\¾‹uÎòx@:D«ÕÎ!³Xy•={ö`ÙÒ¥üȺBðððÀwßJ•*½ò1Žv|V~øûû£jµj¨^½:ªV­ŠêÕ«£J•*ðð°øþf…f0pþüyDŸ;‡:­Öîÿy{{cøˆèÚµ«-MKýM&ýƒƒƒ‹vø3Z­¶Ùm麗/_Fß¾}nÿ’÷Þ{³fÏ6ëÞ1ÑÑ7nÒÒÒÌ6&gÔj5zô쉦M›ÂÉɉu›“‘‘µkÖà§Ÿ~²úý4Š/ŽO¦L±ÈÑ¢…DAéd¥R¹Œuβx@ÑÑÑÎ2'§¯ôeåeîß¿ysç:äOspqqÁü вeË7>öܹs3z´Ý_ !eË–EµjÕž¾ jµj6q±i2™pñâEÄDGãtTÎ;g·P5jÔÀ'S¦ nݺ¬£ä×ß!Ýäry ë ùWÚh2Åð³dÝÜÜ\ôéÝW¯^µdYæ† ‚}dÖ©º¿üò æÏ›ƒÁ`¶19Ûæáá÷Ú´A=ljY•µ9xà–-_Ž”ädÖQ ¤eË–˜6}ºõΤ#d™R.Ÿ ž5Ç o˜™^¯÷Eq±Êv_dd$æÍËïJRù °dÉ’­Ó»qã>™<ñññ&³?nnn¨U«êÔ©ƒÀ:uP§NT¬XÑš×Ôxt‚ATTŽ=ŠcÇŽáÞ½{¬#™!í۷ǘ±cáããÃ:N~ä‚ÉJ¹| ë ù@´:Ý/,¾•ô‚ùó±sçNK—eF&“aÚ´ièÔ¹³ÙÆEë>ý›6m2Û˜œm«\¥ ztíÚ¡X±b¬ãج[·naáÂ…8yâë(…æïï%K—B.—³ŽòRØòàÁƒAFÖY8éñ€ÅÇÇ—Ì3~Ðu–åää`É’%ØýóϬ£Ø$ 4:v,Ô4QQñçŸbãW_áÂ… $t ÅŠ{Ú ¨ˆ:AAfݰËÜ(¥¸xñ"Ž=Š£GâÂ… V?e1¿¼½½1fÌtèØÑê›2m&—Ë­v:ŽF¯N(]oéºüñ&Œoé²Ì¸ººbÉ’%ha¶1³³³1}Ú49rÄlcr¶IDDD {¨_¿¾­ü|´JØ´i¾Û¶Í.öÞ‘Édøèãñþûï[ç÷!{Šyxô¨^½ºíÿcs¯Åf¢Õj+ƒj²Îò¢+W®`ò¤Iøûï¿YG±)®®®hÒ¤ Ú´m‹F™m:úÅ‹±oï^>|‰‰‰fÓ‘•*U †ªn][íI)ÉÉ8|ø08½^o§D(•JL6 5jXt³úÂ!ä5™ºªT*«›ŽWMf2iXô6abb"ztïŽôôtK–eÆËË «×¬1ë2–¤¤$Œ=š7wœŸŸ:tè€.]»ZucÚ üøãØðå—v¹FÃF0oÞ<”,Y’u”ÿ¢tvvv—°°0~,˜ã 3Ðh4D(Ë:Ë‹víÚ…eK—ÚEçTj‚  zõêP©ThŽzõêÁÍÍMÒšÿý7N:V‹óçÏãöíÛ’ÖsÎÎÎ Dݺu¡ª[uëÖµÊÓnß¾ƒ`ÿ¸œÀ:N‘899¡wŸ>6lÜÝÝYÇy“tJÈ kÚù822ÒɧD‰ê[²®(Šø`ÈÄÄØÌ Eâçç‡ÿ­_oÖfÕ…øxŒ3IIIf“³2™ á£S§Nhܸ1Á*|²”RÿüsK—e¢bÅŠXÿÙg(W®œÙÆüã?0cútdg;Þ2AP¬X1BàååàÑ-òøg««‹Ë+gb=|øÆgÎlÏÌÈx:*//999ÈÎζêM+T¨€Ž;¢}‡ü?39{ö,V¯ZåPû%Éd2Œ=ýú÷gå¿9g2Z«ÕjÛÚq‘ËÞ(FBá «šÃÉ“'Ûô…„¥øùù¡F@ÀÓ5åuêÀÏÏ¢›oÃd2!áÒ%ècc‹X½ÿþû¯E38ooo„7nŒ¦M›¢Q£F’Ïò((N‡];wâСC69s‡‚:`ÜøñO/ ¬Ø ôP(Ìn5=þ=r €EÏÓétŒ<›ЪU+Lš<Ù:7;ú&P:_©TÎ`±[ºZ­v™`©zOÌš9Ó!.`[¶l‰ù êä–—IKKÃøqãm–ñlE5P³&ªT®ŒÊ•+£z¨P¡‚Õ`I0g IDAT-¥*¨¬¬,ܹsIwïâAj*ÒRS‘ššŠÔ´4PJ¡T(Ð , ¥J•bÕ®œ={_mØ€3gΰŽbuŠ/Ž… ¢Qx8ë(/J ¢Ø”7ìoV«m Bvð`å‰ÄÄDŒ7ΡÖM±ŒaÇ£aŸòO×®…N§“ gn‚ $$­[·F³æÍ­æ®Ozz:~ýõWl߾ݦ–øøøø`òäÉx·ukÖQÞd¯‹³sÿÀÀÀûRÒjµ AÈQæ9^$Ÿ~?|'N´dI&ºuïŽO>ùÄl©7oÜÀÈ‘#ñÏ?ÿ˜e<[çâêŠjU«¢N: Fpp0*W®lóMN:'OœÀ†  ÕjYG±j‚ àã‘#1pà@ÖQžC€‹F£±™Z­¾Ã: Wt¼P:®€Ý°šEÃg¢¢0yòd¤¦¦²Žâ0zö쉱ãÆåë®’(Šøâ‹/ðÕ† v¿VÔ^¹¸¸ aÆx·uk4iÒÄ*ö EGþøßnÙ‚X½žuœ|kÕª¦N›f5 •Wø[&ƒƒƒc¥*í!srÒ°èÙ‰IIIèÞ­›]«õ¬¡~ˆáÇ›m<½^1£GãÁƒfÓyzz"((*• êȃƒáb¥G²r–A)Å_ý… _~ÉoRPûöí1}Æ 8;;³Žò,¾ÀNð@>étºp `ç‰QJñí·ßbíš5üÂ’æ-Z`Ù²eoÜhåŠزe‹…RqRóðð@Ó¦MñÞ{ï¡AX˜UÜíÒh4øvóf=zÔ&~”.]sçÎ5Ë’ eÒJ¥r‡ƒktºO ð±c¿ ¥Ç CTT”%ËZܘ±c1`À³wøðaLŸ>Ýæ–ÞXA‚µr…Â*¨œô :„Í›7Ûü1·,Õ­[+V®„ë(ϺL€¦,7Ï劎7òA«Õ6z¼á_1ÖY€GëýçÌ™ƒ}ûö±ŽâÐ&OžŒž½z½òëGŘѣÁÿÙ'´kß:t0ëÑb…uýúulݲ¿üú«Õ_¬BЧOŒ9ÒšïRPºX©TN‡÷ÐétM)ð‹n!¾uëV¬X¾Ü’%-Š‚‰'¢WïÞfsÓ¦MøtíZþ3ÜLœQ§N¨CB V«¡P(àáa5«)93HKKî;±}ûvÜ»wu»P¾B¬]»UªTaåY gç&ütÛÅo ÓéT_¬•`îß¿qcÇòµäVÀËË {÷í{éQg¢(¢KçÎ|½¨x²_@‡ŽÑ¢ysæ´÷ïßÇ÷ß}‡ï¿ÿÞêO—¨^½:/Y‚jÕª±ŽòJ8 Bïààà"ÏÿÖëõž¢(ê@ˆEÿ—/_Fß>}ìvãQA0eêTtíÚÕ,ã™L&,\°?ýô“YÆã^N&“!°N„¨Õ¨W¿>T*Ÿ!`£®_¿ŽmÛ¶á×_~ANNë8vÇËË K—-Cƒ XGyV\^nnDýúõSXá Ž7^C£Ñ(ˆ •õwõêUŒ5 ·nñY7ÖâÃaÃ0lذÿ|þàÁƒødòd‰8–¼¼¼ÐºuktéÚL³¤¥¦bó·ßâ‡íÛñðáC¦Y^ÇÍÍ “&MB§ÎYGy+!äry\QÑjµ@Ès…ʼÜ\ôíÛ—/_¶dY‹³fÏFûöíÍ2^VV&Mœˆ“'Oše<.ÿ\\\ P(ŠÐ hˬ¸W;sæ ¶mÝŠãÇÛÄ4[&“É0}úttìÔ‰u”giAiS¥RÉ7"³1¼ð z½¾¦Hé_J³Î§OŸÆ¤‰‘‘‘Á: ÷Œ’%Kâà¡Cprrzîóƒ„F£a”гµÑ»W/¼Ûºõ¾?,)55Û·oÇÖ-[¬zF@óæÍ1sÖ,kÞ 0“ò¾J.ßU˜'?>Aæ ,<õù²eضm›%KZŒ ˜3gÚ¶kg–ñîÝ»‡Q#GââÅ‹f+#´A4 EíÀ@Ö‘8ˆbŸüN{Ôêt[ô‘8ÕsÒRSѽ{w$%%Y²¬E¸¸¸`é²ehÒ¤‰YÆû믿0uÊ«^*Ã=‚5j ´A4jÔ¡Ö}ªˆM;þ<~üá8xÐê7˜u$Mš4Áâ%K¬iߌŸS<èad„{3ÞxFTTTqW7·H*ÖY fÍœ‰ýû÷³ŽòìÝwTT×¾ðɨˆ`KÄ5±1 F£1v£F± ņ¢"¶( R,Š TP”޽ÇX£FE£ÆK° ¢ ™óþHn^Š…2çì)¿ÏZo­wu8ßïÍÊö>»¨R¥ ~8v R©0nìXüüóÏŒ[M&—Ë1ÚÕ;wf6ƒÿÛo¿aåŠ8zô(“üéÒ¥ .Z¤±[8à*Çqƒ¬­­yßç233ó€è§ÉÍðñÁ¡C‡ÄŽœ±±1–-_ŽŽ;ªåyÛãâLû—µP50ÄÎvvv¨Q£ë::¥¨°Düޏt©BGŸµ²¶FXX˜&]m#—@ƒK GÊÊÊ2**.Þ  ë.¯^½Â4ooüôÓO¬«RÚ¸q#Z·iƒW¯^¡k—.ùf•hž bÔÈ‘ø¦_?1ép!=¡¡¡ÈÊÊb’ÿ>ü1‚ƒƒ5yïïK%—ËSÞö—ééé–RƒKù,™½{÷Âoöl1#EQ©R%¬X¹íÚµ«ð³T*–…†êìùº¬mÛ¶êèˆnݺ1=_EÝ¿‰ HMMÅóçt®›6hРÖDF¢vmص ðübÝû¤chàEff€¡¬‹<}ú^^^¸¢¿Œ“w›0aÜ&LÀÙ³g1ÁÍu¢e,--áìâT®\Yô|•J…}{÷bÕªUxøð¡èùïcddŸ3ÔvÅ›T0_.—à_o= E8NÔâ>„ƒ½½Î+“ɰ*<¶¶_ ÷úõkÌöõűcÇ*^ŒˆB&“¡ÿþêèˆF±®£ST*~:s;vì ÓüµT:u¹v-êիǺÊxÞËÆÆ&‚u òn4@¡P¬Ç1?RóÞ½{ðpwÇÝ»wYW!eÔñË/¨±jÕ*Öuˆ–’ÉdpppÀÈQ£`nn.z~AAbbb°):ZãöC÷ë×~sæ R¥J¬«¼Küs3³Q]4(…Bá Žõõ2Ïóðpw×¹ÕcUªTAÄêÕhÕªU…Ÿ•óä &Mš¤‘+^ÈÕ«WNNNøvà@Èd2ÖutÊ‹/––†„øxú½SXXZbÍš5hÒ„ùf â9ÎÑÖÚ:‘uòvz? P*gƒçX÷¸~íÜÝÝ‘““ú )333üpì|¦OÇ‘#GX×!ZÎÔÔŽŽŽ9j“½}Ož°®CÔÈÌÌ «ÂÃÕ2YZQPÀýq=à Ö]ÈéõÀŸohb!ò½Ìÿ–••wwäææ²¬A*(%5î& ;;›u¢#LMMáèä„Q#GÂŒÁD@zz:póæMѳßE&“aî¼yèÝ»7ë*ïrîr@w1Cïܹ'GGú…¾J•*ˆ\»ÍÕ0øW(˜2y2ýœÕ`ÿ[æï4l4hÀºŽN),,ÄÁƒ‘˜¥Rɺ©©)–¯X¡–³RÔà|%—Ë/³.BþIo' EGŽãŽðÓû3”J%<=<——DzQƒé>> f]ƒè ™L§aÃ0bøpÑ'Š‹‹±eóflذA£—NÆÁÛÛ†††¬«0WRR×Ñ£qñâEÖUÔ¦J•*X‰-ZTøYGŽßìÙ(¤+Ì4’••œœœ0pÐ Zæ¯f¿þú+’’’°{×.;„¼›‘±1BBBðÕW_±®¸+帎ÖÖÖ÷Xw!ÿO/' EpÜY5YöHOOǤ‰5n¯-)ŸÖ­[ãÂ… ¬k&“É0räH 1¦¦¦¢fß¿K—,Á‰š³š¯U«V ÖœÓY¿~=VGèÎyK•+WÆê5kÔ²Œ•®ùÓLÿ[æï4l¾üòKZæ¯FE……8tè’’’‘‘ÁºaÄÐÐÁ!!èÒ¥ ë* 024üªyóæô¶SCèÝÀÙ³g«™˜œä¦dΜ9ï©S5ꩉDB¿dQXXZb‚›©T*jö‘#G¤1·˜™™!00¿ü’u&®\¹‚‘#FàÍ›7¬«¨…L&Ú5kÐÊÚºBÏáy+V¬À–Í›ÕÔŒ¨ƒ©©) '''Í=ËCKݺu I‰‰Ø½{7mu!þ˜ F×®]YW8nïõ_~ùÖÁÁ¡„u¢g Ò&M›¦èDzÇñãÇá3}:ŠŠŠXÖ „h¹ ÀkâDôèÑCÔÜüü|¬^½Ûãâ4bÒK"‘`ì¸qpssÓ«7‰E……pvvƯ¿þʺŠZÈd2¬^³ÖüaþüùØ¿oŸšš‘в²²‚£“ÈäªS]UTXˆÃGŽ )1‘V ’·244ÄÒ  tëÖu€çClll|X× z6 P*#Àó,;œ9sS¦LAíE$„¨IË–-1eêT´iÓFÔÜÌÌLÌ›;¿ýö›¨¹ïÒ¶m[,Y²––¬«ˆbYh(bbbX×P‹J•*aUxx…ÿÎÏχÏôé8sæŒšš‘Š°±±³‹ zôè¡W“sB»sçÒRS‘ššŠgÏž±®C4œD"A@@úôí˺ xÀÝV.dÝCßéÍ@FfæDcÙáÜÙ³˜4iDDD÷îÝá=m>þøcÑ2óóó±,4)))qe`­Zµ¢× )==ãÇÓˆejjŠðˆØÚÚVè9ÙÙÙ˜èå…7n¨©)|ûí·6l-óW£¢¢"ùÛÛ~Mø~K´‡–,Y‚={²®R,á¸>ÖÖÖGYÑgz1 P(:‚ã~`ĪÅ àåé‰×¯_³ª@ÑFÆÆ5j¾ûî;˜˜ˆwÉÉÉ“'±ðûïñøñcÑ2ßÅÈȾ¾¾4x0ë*‚ÈÏÏÇPt~sæÐ¹: |öÙgpqqAßo¾ý6]uëÖ-ìÚ¹)))ñý“è&ccc„GD mÛ¶¬«¼~íÚ7t3€øtz@¡T®Ïc•óæM|çêJƒBSR©ððôD•*UDÉÌyò .ÄñãÇEÉ{ŸZµj!tÙ2´hÑ‚u• ÉË˃½Ö/sW×àÓ¦M[¹Rë'C´ ÇqøòË/1jôhM<è„üü|8p)Éɸtéë:DOÈd2¬‰Œd~^,”Ëåó™–ÐC:;™™9žÖ²ÊÏÎΆëèÑÈÎÎfUBþÁÒÒ>3fà믿-399ÁAA(((-ómŒŒ1gÎ 0€iŠX0>ÒÒÒXרccc¬ CûöíËý •J…à  lß¾]ÍÈû¢OŸ>9j7n̺ŽN¸¨T"9%À«W¯X×!z¨jÕªX·~=>ýôS–5xžã†ÚZ['²,¡otr@©T¶WñüŒYäçææâ;WWܼy“Ežžž0á—þ{÷îÁwÖ,æ^cî¼yèׯÓïóã?bÊäɬk”›D"ÁÂE‹*ôÏ8;;îîî¸}ë–›‘¿³±±ÁhWWtîÜY´kCu‘J¥ÂéÓ§‘’œŒãÇãÍ›7¬+Rjr¹‘k×2½Î“¦ÛÊåLÆrúDg&222ÚrÉ)F,ò###±62’E4!„¨Eƒ† ±ðûïÑÊÚZð¬ââb¬Zµ ±11ÌOq>|8¦z{C"‘0íño¹¹¹°·³Ã“'OXW)Žã0wî\ 2¤Üϸyó&<Üݵzûƒ&kß¾=Æ6mÚ°®¢Õ²³³‘ššŠ´ÔT:ü™hµÎ;cÙòå,ÏÉyžïjccC{½¤éééfRƒ ±È?xð fÍœÉü—XB©(‰D‚Áƒcº(oΞ= ???ä0ävìØAÁÁÉdL{üÝl__ìÛ·ur›:u*FŽU|ù2¼<=é>t´oßžžž¢Löé*•J…óçÏ#)1GEI ]eNtC¿~ý°ÈߟÙj ¸[\XhÛ®]»&ô€NL(Šmà¸a,²3331~üx²ˆ'„A4jÔ -B‹-Ïzüø1fûúâçŸ<ë}>ýôS„­Z…š5k2í?üð¼§Ne]£Ü&Mž WW×rýéÓ§1}Ú4¼~ýZ­ô›D"A=0fìXÖ×~iµììl$''#-5=b]‡A|7f &NœÈ²Â¹|è<Ahý@†RéÎñüjÙ÷ïßLjáÃñìÙ3ñ„"(©T WWWŒwsƒ¡¡¡ Y*• ë׭úuë R©ÍzŸš5kbeX>ûì3fž?{;;äähçËñnnpww/÷×ïß·óæÍCqq±[é/©TŠ>}ûb̘1¬¯úÒZ*• 'ŽGRRN:Åô{!b™7o^…¶pUxËåòåÌ è0­žÈÈÈh.‘HÒy@ôÓ*òòò0jäHܼySìhBU“¦MˆÆ žuúÔ)Ìž=¹¹¹‚g½‹©©)V†…¡mÛ¶LògÍš…û÷3É®¨Q£FaJV.l‹Cpp0 °Ô@*•¢ÿþ;nê֭˺ŽVÊÎÎFJJ RSRèm?Ñ;XŽ/¾ø‚U…b Ç}emm}–U]¥µéé醃Ÿ8 µØÙ*• S&OƉ'ÄŽ&„&ŒŒ1aÂŒ5JðÃò²³³á3}:Ó[LLL¶j>ÿüsQsµùÔGGGÌòõ-÷×GGG#låJ56ÒOÿ[êï5q"êիǺŽÖ¡½ý„ü?™L†èèh4iÚ”U…Û…òöíÛ³½;XÇhí€B©ôÏû±È Bܶm,¢ !„©N:aÁ÷ßÃÂÂBМÂÂB`ç΂æ¼Oݺu±k÷nÑò^¼x»!C´òÔÿAƒaÞüùå:4J¥RañâÅHLH ™þJ¥èûÍ7?~<¬¬¬X×Ñ:ô¶Ÿ·«S§bbb`aiɦÏo±±±)ÿ‰²ä?´ràÂÅ‹m$*ÕÂnJ}‹½{÷Âoöl±c !DcXXXàûï¿Ç—: ž•˜€àà` žõo#FŽ„···hyóçÍc:áQ^}ûö…@@¹V†¼yósüüpàÀšé‰Dò×ÀŸÞø— íí'¤tš7oŽQQ¢Üô6à$—Ëw0 ×AZ7žžn*50È úZ”k×®aÔÈ‘(((;šB4 Çqp6 S&O†‘±± Y/^„Ïôé¢Þ_»vm$%'ÃÔÔT”¼Ó§OÃÓÃC”,uêÞ½;‚‚ƒËugtQQføøàÇ ™î“H$èÛ·/ÆŽ‡ °®£U²³³‘ššŠÔ”Q¿¯¢ÍzôèàV×>/yóFÞ¦M›ßX„ë­›P(«Áqå?^¸œrss1ÜÅ÷îÝ;šB4V“&M°xÉ|òÉ'‚æ<}ú¾³fáܹs‚æ ¬Â#"СCÁ³ ??ööøý÷ßEÉS—N:aÙòååº!âõë×ðž:?ýô“ÍtÇqèÞ½;<<=ѨQ#Öu´†J¥Â‰'œ”„“'OÒÛ~BÊÁÝÝãÝÜØ„sÜakë¯AWV˜VM(Š^à¸DzR©T˜8q"NŸ:%f,!„hÌöóÀÍ)))Áª°0lÙ²Bþìªèöeµ80ñññ¢å©C»ví¶jŒË±ú#??½¼‘‘!@3ÝÖ±cGxzy¡y󿬫hœœ$''#)1‘ÞöRAÇ!tÙ2tëÖMžŸjcc³‚M¸îК €ôôt3©Áe‹Ž 6ˆK!ZeРA˜åë[®AaY9|óæÍëW¯ÔþìÞ}ú`ñâÅ¢-q¼žŽqãÆiÕÛÈVÖÖˆŒŒ,×öˆÜÜ\xzxàòåË4Ó]¶¶¶ðòòBë6mXWÑ/^Äö¸8>|˜É"„è*™L†M›7‹r5ð¿q@ÇqŸ[[[³»&HhÍ€B¡XŽ+vîñãÇ1eòdAß6íebbBgBò7Mš4APp°à{’¯_»†)S¦àÁƒj{fÛ¶m!øÆÿb¨ƒ~ûM{¶46iÚ6l@ÕªUËüµ999pwwÇõk×h¦›š5kOOOQÜÔE……8pð ¶ÇÅ!++‹uBtV]++ÄÆÆÂÌÌŒE|FÉ›7íÛ´iSÌ"\hÅ@FFFN"ù"/ýøð!‡Enn®˜±DK4lØãÇGHHrrrX×!DcÈd2Ì™3}úö4çéÓ§˜>mšZ–’·hÑk×­ƒL&SC³ÒY¾|9¶lÞ,Z^EÕ«WQQQåº êáÇ˜àæ†Û·o«¿˜jР<==Ñ£gOVni•ììlÄÇÇ#%9ÏŸ?g]‡½Ð¾}{D¬^]®C`+ŒçgÚØØ‰¬Ê~gÈΜ9S‰“H6@äÁII fûúÒàŸ¼hÕÒ]BÄŸŸ___ ¨°P°œêÕ«#ríZ 8°BÏiÔ¨Â#"Dü_ºt ±11¢åUT­Zµ¹vm¹ÿ÷îÝØᆪÁ)XZZÂoÎ$&%¡g¯^4øÿ€ŸþÓ¼½Ñï›oEƒBDtöìY„¯ZÅ&œãfddÐa(åDˆpX IDAT¤ñ+ E08nºØ¹«W¯ÆúuëÄŽ%Z†¶ò~Íš5CHh(>úè#As6mÚ„°•+˼]«AÆX¿~=,Ë1°-¯ââb8†7nˆ–YæææˆŠŠBƒ† Ëüµ·oÝ‚››=z$@3Ý!“É0jÔ( 1•*Ub]G£ãàˆÅÕ«WY×!D¯q‡åË—£K×®,ÂÏØX[w@oâÊH£' … 8ß1Tééé¯e‡2Bˆ¦233Ãâ%K¿VïÈáØíçWêUõë×Çú P£F A{ý›6M0Ëd2¬_¿ÍÊqêü¯¿þŠ nnxòä‰Ítƒww÷r­®Ð'yyyØ™–†˜˜dgg³®CùS•*U°-.uëÖ=›&ÙÊ化!h/8v옙¹ùYh-fîÓ§Oá8t(ýÂRF´žòN‰'M¨Q£]Ö|þüyxOм¼¼÷~ŽÅ›àà ñæÍQsËÃÄÄ«W£uë²ÿ¾té<=<ðâÅ ši?Žãðõ×_ÃËË u­¬X×Ñh·oÝÂÖ­[±{÷nZqGˆ†jÖ¼96mÚ###±£_•H¥ÖmZ¶üUì`m¦±gT«^ÝGìÁ?Ïó˜;g þËÁÅÅ…uBˆS©TX¹bfÍœ)Èõ}ÿóùçŸ#*:ú½oõY þU*.\¨ƒCCC„„„”kð!=ÜÜhðÿ­[·FLl,–,]Jƒÿ÷8wö,&zyaÈ!HLL¤Á?!ìJVBBBXD›JKJV³Öf9žž^<ï'vn||-v¬ÖûüóÏ1rÔ(´kßžu"sssÖˆ9xð F…»wï –Ñ¤IDEG¿upÅjð;¶oÇ¥Kšu±D"A@``¹®ž;sæ <==‘ŸŸ/@3ífee…lŒŠB‹-X×ÑH*• „³³3ÜÜÜpòäIº†™-‘½{÷²ˆþ:33s‹`m¥‘[Je*x¾bÇ:—Ñ;wàäèH3Ìe`ai Œ9R©*• 111ˆ‰¡U:ÄÑÑ)©©‚žæNôOÕªUˆNÞoþäÉx¸»ãúõëØþ³³³aog§ñcŽã0oÞ< <¸Ì_{ìØ1Ìœ1EEE4Ó^U«VÅØqãàääCCQ4Ò………صs'6oÙ‚{NB„%“É·};¬D^ÝÄw¥×ÌÚÚZ³Èj›ÈÌÌìÍûÅÌ,))ëèѸxñ¢˜±:£C‡ˆÀ¤I“pêäIÖuˆš-_±ÛãâpöìYÖUˆŽ‘H$ðððÀwcÆv.À‹/0ÑË /^¾d6ø€)“'ãÇd’]ÞÞÞ1rd™¿nÿ¾}˜;w®Vlo‹ì0ÁÍ fÕª±®£‘^¼xøøxÄmÛ†§OŸ²®CQƒV­Z!*:¢ærÀB¹\>_ÔP-¥Q7nÜ0ÎËÏWh*fîúõë±:"BÌH3uêT,_¾œu ¢f––سgŽ=ŠÙ¾¾¬ëÕ·o_,X°FÆÆ‚<ÿõë×xõê,,,yþ‡>t>>>L²ËbÜøñððð(ó×¥¦¤`Ñ¢EtsÎßtéÚS¦LAƒ XWÑH>DlL ’““=„ÂÆ¸qãàáé)vl!x¾•Íu±ƒµFMddfÎã€ïÅ̼rå FŽAo-*ÈÒÒ’–ýë333ø S§Nàyþ‹!%%…öcA´jÕ Ë—/×¹«Ð^¾|‰!ƒkü÷G§aÃ0sæÌ2Ýö¸8Ñ÷…?}òÉ'˜1c‰ó<@tTÒÒÒP\\̺!D ‰ë7l(×A²Âó;mllDÝF®4f ==½žÔÀ €L¬Ì¢ÂB8;;ã×_éæB FhÛ¶-¾èÐݺuC•*Uþñ÷·oÝÂ?ü€ŒŒ œ9s†&ΈZÕ®]+V®Ä§Ÿ~ʺŠÚøû#11‘u÷ê?`.\XæmÑQQ ¨•v©Zµ*&L˜€¡ŽŽJ¥¬ëhœ»wïbㆠسgýÜ DOÔ©S;âãÿó»¤àx¾¿ÍqCµ‹ÆÜ •JWAÄÁ?DFFÒàŸ¿yüø1233q!=ׯ]ûÇßâôéÓ8qâΞ=K¿ÄµËÎΆëèÑ8zô(ë*j‘‘‘äädÖ5Þ«[·nX°`A™ÿ«#"hð?ÞrÙÙÙ!5- Üiðÿ/·nÝÂ?? 8iiiôsƒ=òûï¿#00EôÊc·o›°Ö±à‚RÙGÂóûÄ̼zõ*†»¸ ¤¤DÌXB´Š‡§'Ƈ‚‚¸8;ãæÍ›¬+= Æá€B+**‚“£#nݺźÊ;}þù爀‘‘Q™¾nùò娲y³@­´‡­­-|fÌ@³fÍXWÑ8ׯ_džõëqøða:‚=Œž½z‰Êq~6ÖÖLf´ó €„„i“¦MZŠ•YRR‚ÇãÊ•+bE¢• ±wß>ìÙ³+èG"²o¾ùóçÏìp@!­ŒDdd$ëïôÙgŸaÃÆÉJ¿ðŽçya{\œ€Í4_Íš51eêTôéÓGk'¨„róæMD®YƒÃ‡Ó¹„€……’’’ľ å¯R5·µµ½#f¨¶`¾ iÓ¦c!âàbbbhðOH)ãèÑ£8rø0ë*DíÝ»nnnÈ}þœu•2¹}뢢¢X×x§ºVVˆ(Óà_¥R!Àß_¯ÿ=z4RÓÒзo_üÿÍÝ»w1ÇÏöö8tè þ !ÉÉÉÁÒ¥KÅŽ5å$±CµÓ¿üòK•‚‚‚kŽQQˆß±………¬ëB´Ì Aƒ0ÁqCyþK›Óâ†j6fg¨x~ DüçææÒ•E„¢Å 1cÆ =\oéÒ¥9ø—H$ð÷/Ó࿸¸3||ôrð߬yslÞ²3fÌ ÁÿŸ  fËüBÊ%-- .\5“ç¸%¢j&+233myàgˆ8±háB¿™BHé8::bÆÌ™H˜Ÿe 8~ü8&OšÄºÆ[ùÍ™{{ûR¾°°Ó¦Mé“'l¥yªV­ŠÉS¦`ðàÁ´ÜÿO%%%HKKÃÚÈHÿüs„GDÀÈȨTŸùò%<=û sçÍCóæÍYWÑ/^¼À¦èhlݺ•®|%„ˆF"‘ &6VÔïÅ<ǵµ¶Þ(Z †sÇ‹üö?<<œÿ„¢rž<Á¸±c‘.â옘ü7oÞËW¬(õàÿÙ³g7v¬^ þ+UªlݶÿøãšÇm[·âÛMƒBˆ¨T*‚ƒ‚ æËh˜—žžn(Z †m 33Ó€Xy7nÜ@rR’Xq„B{ùò%<<S(ߊ¨_pìØ1ƒjææW44èO*• NŽŽ¸~ýºq„B4Øà!Càçç©Tª–çýþûïp°·G~~¾ZžWQ[µ :t(ÕçïÝ»·ñãñàÁ›±S½zuÌœ5 _ý5ë*Ìñ<}ûöaÅòåxüø1ë:„òVfffسwo©·°Uü$—ËK÷ƒS ¾ÀÌÜ|Dü@jj* þ !„R’“1uʼ~ýZ-Ïó_´HcÿÇaî¼y¥üß¹scÇŒÑéÁ¿~ýœœLƒׯ]ØテßìÙ4ø'„h´ÜÜ\lÙ²E´<øB¡Pt-PÃ= á€Ò_D\A¯^½ÂšÕ«ÅŠ#„¢Nœ8±cÆ ''§BÏIKKÓ¨íeîîîøöÛÒ­b¼}ëÆ‹‡ ÜŠêÕ«#tÙ2øÀ¬Z5Öu˜*,,Ddd$\\\‘‘Áº!„”JlL ž>}*ZÇq¢Q5 ™™™öZ™ñw›6m“'OÄŠ#„¢%²²²0zôhüöÛoåúú'Ož`Yh¨š[•ßà!C0nüøR}öúµk3fŒÎ¾îÝ»7’““ѽ{wÖU˜;}êì† ÁÚÈH³®C!¥öêÕ+lŠŽ-ú(•ÊÖ¢j!'8žçý|þ?äää 6&F¬8B!ZæÞÝ»5r$.*•eþÚ¼xñB€VeשS'øù•îÇë•+W0nÜ8Qߪˆ¥Zµj Æ’¥Kõþ­Γ'˜5k<==qÿþ}Öu!¤\vÄÇ‹ºRçyÑÂ4ˆ` …b 8ÎZ¨çÿ[TT”ÚöxBÑMÏŸ?ÇØ±cqèСRMjj*Ž;&\©2hÖ¼9–•êPÃ+YYpŸ0A'ïyïÔ©âЫW/ÖU˜ây»wí‚ìßϺ!„THQa!Ö¯['Z8(Š&¢jÁnPdfžÐN‡ÿËãÇ1 ŠG!DËI$̘9ŽŽŽïý܃0ÔÁA#þûøã±yófXXZ~ð³ …^žžÑ[*W®Œ)S§ÂÎÎŽuæîܹƒœ?žuBQ‰D‚¤äd4hÐ@”<ˆ”ËåiAVdddtHƒXIƒB!¥¦R©°dñb„……á]á*• óæÎÕˆA´™™ÂÃÃK5ø?þ<<ÜÝ5¢·:uèÐÉ))z?ø/,,Dxx8ìíìhðOÑ9*• Ö¯-FeeeU-P³@"™&ÈsßâÞÝ»HKK+ŽBˆ‰ŽŠ‚^½zõŸ¿Û‹ôôt­þÉÈØËW¬@ƒ† ?øÙ3gÎ`¢——Nm‰326ÆtD¬^5j°®ÃÔO?ý{{{lܰoÞ¼a]‡B±ÿ~1¯¬­T\\ŒQ#GâÞÝ»ýÙµk×Á°Õ$ üýakkûÁÏž:yS¦LÑ©q7Fll,\\\Àqë:Ì<}ú~³gÃ}„ü{J!º¨¤¤1[¶ˆ–Çó¼¾O“¨ý¿¨J¥š.ÄsßææÍ›Ø·w¯Q„BtØ70|øpìÛ·¹¹¹˜>mŠ4` í=mz–â »ãÇÃÛÛ[#:«ÇqæìŒ­[·¢I½;Ÿéìß!ƒc/ý¾CÑ#©©©xöì™8a÷Iff¦Þœ*k Î‡)•ÊšÇ æXÁÿZ·nT*•Hi„¢ß$‰NÏÍÍÍÅl__T®\yyy¬ë`øðápqqùàç~øáÌœ1Cgî}·°´ÄÂ… ѱcGÖU˜zöìq¸ 7VBˆ®(((@\\<<¥RÙZÅó¢\–œ››‹oúöo?!„#vëV wqÑ©kæ4…D"Á²eËÐ¥k×÷~.55‹.ÔúÁíÚµ±ÈßmÛ¶e]…©Ÿ~ú ß/X€ììlÖU!Dc5l؉IIܧ&§mäò/ÅbE-ÿU^¯ÿ¯^½B@@<ÜÝiðO!pëÖ-ütæŒXq …Xa,Txàܹsऎ2òêÕ+ÄÅʼnE!ä_~úé'ÖtÒ Aƒ0jÔ¨÷~&!>þþZ=ø—ÉdX´h–,]ŠªU«²®ÃLzz:†:8 1!êX…I!ú >>^´,Žã¾-ŒƒŠ>ÀÈÄd8Ïó¢–””„ÜÜ\1¢ÑHfff077‡™™ªU«333þõ÷•+WþkyÔëׯQPXˆü¼<¼zý…xùò%rrrðôéS­H6èßõkÓ¦ fûù½÷3Ûãâ¤ÕƒÅO?ýAÁÁ¨W¯ë*ÌcÕªUØKÿ["€*Uª@"‘ J•*à8î­ÿùò%xžGaa!­À"zíĉøý÷ßQ§NÁ³xÀ)==}Z›6mtãŽÝ©ðxÞU =>¨¨¨±11bDÂLµjÕP¿~}Ôµ²B=++XÕ«+++|ôÑG¨V­šÚö>•”” ''>DNN²³³qûÖ-ܼy¿þú+]?Eˆ¬¬¬úI¼‹Å²ÐP­ü; ŠéÓ¦ÁH¯¼sç|}}q%+‹u" š5k¢~ýú¨Y«ª››ÃÂÒÕ«W‡¹¹9,,,`aaÈd2H¥Ò2?¿¨°/^¾Ä‹/þú¿—/_âÁƒxpÿ>oÅó<„;wîEˆ`ÌÍÍÑ®}{|Ѿ=Úµo>úˆu¥2yúô)”J%ÈÌÌÄ¥K—ðæÍÖµÑ:ˆ@ûöíßù™6 <<\ÄVêõÙgŸ!(8VVV¬«0“——Ø¿ŸuR•+W†µµ5ZY[úU+´lÕJ+ϰàywîÜAff&Nž<‰3§O#??Ÿu-BÊ, 0ß|óà9PÀó|›ç‚‡‰¬ÜJ¥R¦âùÿ.xêäIxyy CˆZI$´iÓ:uBû/¾@Ó¦MÿºK]äååáÌ™38yâNž]U¬êÔ©ƒþ ÿþz}-ei<}ú;wîDܶm4™O4Š©©)<ˆÊ•+ žÅólåòE‚‰¨\ …Â$@ŸÈÏÏGﯿ¦CJˆFùä“Oà0t(úõë'Ê7]QXXˆ}ûö!~Ç\¹r…uBD#“ɰ%&5úÏßñ<Ðlݺ•A³Š‘Éd˜¿`zõêź 3………XŠ„„Z©¨ÁLMMѳgO øö[´nÝZmWêê‹ââbìß¿1[¶àúõë¬ë˜3w.ìììĈºa#—7 3ßäË7™© WJLLD€¿¿Ð1„”JË–-1fìXtéÒE§ócA©Tb{\>ŒââbÖuŒD"Aè²eèÚµëþŽçy-]ŠíÛ·‹_¬‚š5k†   ÔÕã%ÿ¿þú+|gÍ¢‘«S§œœœ0xÈT©R…u­Çó<Μ>Í[¶àÜÙ³¬ë=gkk‹¨hQŽ£ƒ„ã¾°¶¶Ö™éË< T*[ªxþ¢@}þÁÅÙYYYbDòNmÛ¶ÅØqãÞ{e)Ÿœ'OœœŒ„„<~ü˜uBÔÎÝÝãÝÜþóç<Ï#00‰ ZUŒ££#¼§MÓë%ÿ»wíB@@ XW!o!—Ëá2|8ºwﮓ'øk‚ŒŒ ¬\±™™™¬«=ÅqvîÚ…ºuë Æó+mll¦$Ž2Odff.æYõùËÕ«W1ÌÉIèBÞ©u›6˜4iärÁ»è½ââb¤¥¥!:* <`]‡µèÑ£‚CBþ³hša‰ IDATbH¥R!ÀßÉÉÉŒš•©©)æÍŸÞ½{³®ÂLQQ‚ƒ‚˜˜Èº ùôèÙÇGË–-Y×ÑÇŽê°0ºý‡0ñ®Ivܱ‘ËBG¶”y@‘™y@caêü¿ÀÀ@$ÄÇ CÈXYYaò”)èÑ£ë*zçÍ›7سg6nØ€»wï²®CH¹5iÒ›6o†©©é?þ\¥Raá÷ß#--Q³ò©W¯B—-CãÆ‚ÿø×X¿ÿþ;¦O›F+5Œ¡¡!„1cÆ víÚ¬ëè%•J…;wbmd$²³³Y×!zÄÊÊ i;wв5W%‘´mݪUºàA"(Ó€B¡°ÇeØÀ‡êôêÙ/_¾:Š¿T­Z£]]áââ¢×K[5J¥Â‘#G°fõjܺu‹uBÊÄÌÌ ±±±ÿÙÏó<´îíñW_}…€À@½ÞC}îÜ9øÎšE÷£kôîÓÜÜôú, MRXXˆèèhDmÜHçûÑlÞ²ÖÖÖÂq\€µõჄW¦cPyŽå¨ÅCÒàŸˆ†ã8ØÙÙaç®]puu¥Á¿H$èÕ«“’HouˆÖJ¥o=O¥RaÑ¢EZ5ø—H$pwwÇʰ0½ü«T*¬Y³î&Ðà_CH$ 0)))ð÷÷§Á¿166Æ„ °uëVqd„صk—8AŽÙ~~8}êë* ü{ôèOO4hЀuò<ÏcÏîÝ ÅóçÏY×!:¬jÕª8tø°(/ð$÷™µµõ/‚ ¬Ô+233[ˆ1øôèΟ?/t Ñs‰#GBbR þµ€¡¡!œ]\¶s'†¥S‰F0`À[ÿZ5øoÒ¤ ¶o߮׃ÿK—.ÁÉɉÿ¢mÛ¶ˆÛ¾AÁÁ4ø×Ç¡ÿ€HLJBçÎY×!:ìÅ‹8qâ„(Y*•j (A+Ë€Á‚µø›}ûöA¥R‰EôÔ'Ÿ|‚˜ØXL:ÆÆÆ¬ë2¨^½:fÏž„ÄDú…‚h”–-[bΜn üß›mºê¯k׮شy³^/«ŽÇw®®t˜™¨[·.BBC±~Ã4mÚ”uRX±r%fùúÒï\D0»EÚÀqœ(ãa¡•z €"3ó€Ï…­ upÀõë×…Ž!zjÐàÁ˜9s&LLLXW!jpêäI,^¼÷ïßg]…è1KKKlݶ 5kÖüëÏxžGPP¶ÇÅ1lVzÇÁõ»ïàéé ‰¤LÇéŒ×¯_ÃÑ"ìÝ»—u½'“É0f̸ Nçòè›7oÂwÖ,\»vu¢c qäèQ1Ϋá%WÏÚÚúžÐAB*ÕOy¥RY@»à—_~¡Á?„L&C@` æÏŸOƒòe§NHLJ‚««+ X×!zÈÈȡ˖ýgð¢5ƒ#ccbâĉz;ø¿}û6FŽAƒÆ$  „´´4¸~÷ þuL£F‹áÇ‹rmÑÅÅÅbmàJ€b ©T?éU*Õ7¥ýlE:tH袇>ýôSlݶ ß|ó ë*D&&&˜4y2¶ÅÅA.—³®CôŒÏŒÿ9ízYh(¶mÝʨQÙÔ¨Q7nDß¾}YWaæÇcÇ0ÜÅ7nÜ`]E¯µhѱ±±˜¿`,,-Y×!122´éÓ SSSÖuˆ9zô¨(9Ïký6€Ò ê%QFN‡i€¨Ù€°eËÔ¯_Ÿu"°&Mš *:~sæèí•eD\ý €½½ý?þlÙ²eˆeÔ¨lZ´hØ­[ѲeKÖU˜àyÑQQðööF~~>ë:z«R¥J˜6}:¶ÄÄ Yóæ¬ë‘ôèÙóóFêÖe]…èˆÓ§N¡°°PŒ¨®éééfb åƒééé†àù^B¹~íîܹ#t щ“&OÆÂE‹`D‡Îè ‰D{{{¤¤¤ G¬ëÖ¤iSøùùýãÏV®X˜-[5*›=z`ÃÆÿغ OŠ á7{6ÂÂÂèàa†:~ù%“’0|øp½Ý~¢Ïš4i‚ØØX´§Û˜ˆ¼~ý§OŸ#ÊP*•jõ59ünk``Ð @5¡‹:|Xè¢'*Uª„ฺº²®B±°´DHh(-Z„Ê•+³®CtLåÊ•úóD°iÓ&v¥Ê`Ĉ ÖÛóP?~Œ1cÆ`ß¾}¬«è-sssø ""}ôë:„!³jÕ±z5\þu…*!å!Ö6H$‚¿Ò'xÞþÀš jP³fMDEG£{÷ ÐÀ$$&¢]»v¬«Áq.Z«¿]“Žè¨(†­JG"‘`–¯/¼§MÓÛ·­—/_†‹³3.]ºÄºŠÞê׿?’SRЯ_?ÖUˆ†J¥˜îãƒiÓ§ÓဤBŽÿø#Þ¼y#|Hãc¡|ð@EfæO]›sçÎ 8PÈ¢êZYaíÚµºö6ါ²Àq·9žÿã¸ß<â8î1ÏóEÇå們‘Qe(**’T/á¸ê•ª:Ïq >€<ÇÕÏ7Ѐ^ü¤åyqÛ¶!,,L¬ýaDG¹ººbÒäÉýçÕ«Wcýºu •Ž©©)/Y‚Î;³®Â̾}ûðý‚ô=€‘Zµjaþ‚èСë*DƒíÞµ ,@II ë*DK­‰ŒÄ_|!|Ï7´±±¹-|ú½÷Þ¬ôôt3©à×ÿ?~\è¢ã6lˆµëÖ¡F¬«TD€óàù3‰ä'å¸gôÙßþÿ'ïû`VVVå7oÞ´P­Àó6:h…|_ÐFÇÁÙÅ:vÄ??dee±®D´PÛ¶máéåõ×^õë×3lT:–––[µ Íš5c]… •J…ˆˆDGEáC/=ˆ0z÷éƒÙ³g£jÕª¬«ˆ!wÀ}÷yžÏð—Ë©T¹ryžÏã8îµJ*-IIÉ ©TúÎï›7oLxƒJR•ª*#žç«0…DRƒçù: €<Ï7Ç5„7w ¥ÿ€¨jf†>>4YGÊåèÑ£âLH$=l>HýÞ» C©ü–ãù4¡K¸¹¹áÜÙ³BÇõé§ŸbMd$ÌÍÍYW)+€<¿ã¸}×®];ãààÀtÊ[©TÊT*Õ纂ãzh›xóæ Ö¬YCƒR&5jÔ@Üöí°°°ÏóX±b¶lÞ̺Ö5lث׬AíÚµYWa"??~~~øñØ1ÖUôR•*Uà;{¶®^3YŽ»ž¿ÀdüÚ¾}û,Kýùs¼9Çqø?î+ÿ Zøs<##“'MÂË—/YW!ZÆÒÒc«[¼\î(tˆÞ; P(V€ã&¿ójŸŸ®]ºˆ³_ƒèœ–-["bõj­z«À鶈—Ëå÷Y÷yŸ³gÏV566îÆqÜø¼eÐ §Oœ9sðìÙ³˜è5‰D‚uë×£M›6àyAK—bûöí¬k}P«V­°jÕ*˜Uü_tÿþ}L2ׯ_g]E/µk×ß/\¨k“Ox`·Øchhx°yóæy¬ }ÈÅ‹ÍKJJúr7äÏ #ÖJ+++ nãÇ#/Oãÿ1 ³yËX[[ óÄF.¯@ë®’yÿ €ÌL%÷Ç’`Á>t>>>BFõÉ'Ÿ`cTÌÌ´â*·ภ%Û¦U««¬Ë”GBB‚´ñgŸ})Q©ìxÀ€%ëNõèÑ#øÎš… .°®B4؈‘#áíí’’øûû#5%…u¥êر#BBCQ©R%ÖU˜HOOÇôiÓðüùsÖUôŽ‘±1¼¼¼àââ¢+‡M^Ï'¨$’]­­­ÏA ÙÿŸŒŒŒ‰dÏqcÁóZ±'H¡PÀÃݯ_¿f]…h· 0aÂÁsTIÛÖ­Z¥ ¤fïœ8w‘±ñc|HØ‚ùó‘–&ø.¢c>þøcDoÚ¤ù{þ9îgžçêÈdñ7Ö™ÍlYYYFÅÅÅýyÀ@háòÂÿ)))ÁêÕ«iKy«úõë#>>……˜áンZ°]­wŸ>X´h YWa")) K/¦•… 4mÚhܸ1ë*õ@½QUu»UU;QBzSà˜è<7{ù•WðôÓO‹Ž!ÀO?ýħ¢Äó)ä=·Ü`³Ù:‚¦«š¦¡k—.rB¯ä’=z eÚ4¢«®´÷ÊJKG·k×Nÿs8!NJ_!ÀEçq•ÝnÇÈ‘#qþœaÖ/$?¢( &O™‚?ûéVº½{÷â­ÁƒqN~rŒñ‰‰èÙ³§è(•B] 0YUÕ/Eg1Š}ûöU/¹zõq‡®aÞÊËË1pÀ¹ð'ÝÕ—_}…F1­A/bUÕP?ˆoùÍLéÀºðáÇåÍ¿ä’ÖQQ˜$00ïΞ®]»ŠŽÂ¥‹-BRRœN]¿eøEQ0dèPL{ç£Ül]$ÀØ@³Ùªªê:rd¼t¿ªªêJÈS TÝÍ›7GBÓË$ƒÛž™ÉåÔ“Éļ{Þ›~·™™YóU ›Š¢›#߇»BÚ[-–1qqqò(°Z­š¦=Dc"sôïß"#H:tüøq\¼ÈáôiE1Ì6€Þá322Ì ¤%ë¢{þ™u É Æ'®õŸÒƒÔéüƒÅbÙ'&€t Ôj±,'@4tyô¢Éd„‰ñ怢£H>„‚‰'â‰'ž¨ôµçÏŸÇÀŸŸǯ+--ÅÐ!Cx“ä²={öàÕW^Á¹sçDGñaaaXºt©Þ‡©ƒÒ1/¶·X,»D‡‘nÔ¦M›ŸµŠŠN„}†2›Í˜8i’®°|E`PÞß/~C)åÕОGo¸á;¤F˜¸žŸŸóçuqzˆ¤3]»uskÀ•7`/!äáØØØÿ Ý‘ªª'TUíA é Ptž[0`åÉ+FŽ…§þò—J_WXXˆAâèÑ£.}}ii)† ŠÔÇn›¬¬,¼ùÆ(((Åo4iÒ«V¯†ªª¢£Ü¥ß+„´±Z­3;wî¬Ûù0þ...îh`@@GÂZ‹bbbðÜóÏ‹*ï7ÊJKÝú%Š#'‡G™XE¼á†OªÄl¾ŸuÁŸåÓé1|øpQåóL&SgUUe¯©ÎÅZ,«BTòè,·òLïÞH9AL×Q%÷úo OŸ>•¾®¼¼Ç «ôžùÒÒR 6 Û¶m«tMoÚ²e þ6hŠŠŠ„æð'1 V­^¦M›ŠŽr;ÅhµZ;Ëî|8¶mÝêÖõžÚ¸q#F ŽÒRyè /]ºtÁ²eËP³fMÑQng‡BH›XU] €Š#¹Îjµ^¢”v-¢~pp0FŽ)¢´_9uêºvë&:†KöìÙ§ÓɼNYY™Ð)殺¹W•yÀ¡Cò8uéF¡¡¡xíµ×ø¦ô<µZ­Gø—˜9k‚ôÙ­TA€©—.^ì(Ÿú—Õj½d6™z@Èäï®Ýº!&†ù\s¿vìèQtéÒEt —”””ðzmåQÄS7.ÒšuÁƒ².!Ì€D7TNM¦^ªªîá]Xòž6mÚü|µ¸¸=€¥¢³Ü,22ï¯Ze˜ö8I¬‡~S’’Üš!ñ]FÒæÍóJ޲ÒRŒ1[8-¬]»S§N…¦i\êù;EQ0jôhŒ=Z¯óJh„<¤ªêd¹×ßø~í®ì&âtB† Æ»¬_9vü8¢¢¢*:ŠKöìÞ;!:¦ò?׿û÷±,æt:ñß#GX– ¦aÆx¦woîu)0$6&æ{î…%¯‹/±ªê›x:;.°Q£FXùÞ{ˆŒŒEÒ±¸¸8ÌHM…Ùl®ôµû÷ïÇøñã½z]VV†‘#Fà‡~ðÚkÞÊ’Å‹1kæLP*»»y BêÌ™nÍ—àkbÛX,ú:–Bòˆªª' î“=ÛÄÅáá‡æ]Öo?v Š¢ }{c ¿ÏËËãQ&ŠGOý¶àp8¨Ê²Ø±cÇPVVƲ„d0ýú÷wëC¯'(°ä×=…’QUõ“_·èêx¨ÚµkcÙò刱b[˜ÄYË–-1/-Í­VìóçÎaè!(.öþQèeee5r$¾ÿÞû뤔RÌ~÷],^,߆y ÃÒeËеkWÑQnåz©ªÚ7**êŠè0’÷©ªºG#äyÜ»:Þ2D¯Ý.†wìØ/í;tœÄ5œZñ(â©ß¾#4MkÁº˜Üÿ/]¯N:xêÉ'¹Ö$@VP@À®E%nÚ´is zHHG¤‹Îr½5j`ñâÅh×®è(’Ž4hÐó,@HHH¥¯-ýuhßéÓ§$ûEYYFåÕEMÓðöÛocÍš5^{MéÎêׯ÷Þ{.B ˤ(qªªnEb«Åò5Þâ]·yóæèñ§?ñ.ëNž< MÓÐÁ àÑqVÃn· ™j^×/‰E°.ößÿÊ#Ö¥ÿéÛ·/ïãÒŠ!}¢¢¢dŠkÑ¢E©ªªC(!/ðþ£Q7#==wî,:Ф¡¡¡X°p¡[ƒ")¥˜ùÄ É$WÜwß}xÕ*„7k&:Êï`MqqñC111òéŸøµûò=Þuûõí Bï²>¯¬¬ gΜAÆ 1侮°gΰ?ô‹Ò’yý¶@ iκØñcÜg€x•ÔÛàQ&œGOüo¥MX*++Cþ¯IŒè©§žB§Dƒ DG1¼‡z 6äWÒqªªžàWPÒ›Î;W¨ª:”¾  Ttà×3ЇÇ ¿qý $ Ô¿¼èælEEE>l ¹>4ûŠŠ $ŒãÒ"ÀÕ«W1tȶH•óÜsÏaæ¬Y¼çëÜM9%dªª¯É–éšXU]J?çYÓÝ÷_éö®í© Áý÷ß/8ÍÝåqXPæÕ=¥@Æ‘#UÔeYèÚ¤H£ ÂÐaÃжm[aFj*ÂÃÃEÇ2´gŸ}–g¹íV«Už5%¬VëûTQþà‚è,×¼þúë3fŒPäã}ì1 :Ô­k5MCâøñ8|ø°—S¹çÚ"ÀæÍ›oû5%%%xkð`üô“<Ò—7 ÀØqãôuä¥çB‹µX‰Ž"éÓéŽ?»wïÎ{û©ÏË¿n¨ž»[Ûx:rø0œN'ÓÔ( 5.\h €é§O–Gy[@@ºuïŽäädlÜ´ ýû÷ÿí÷bbbðÏO>ÁÊ÷ÞC¿~ýPÛ Îþ¬q“&\÷ `$ã­üðC;v ŸnØ :ŽaµŽŠÂ½÷rÚCȇrÒ°t7ñññ%V«õ9P:Ot–kìÔ ,@HHˆè(’Ìf3fΚ…ûîsï4 ‚K—0bøp{9™÷9NŒ;/ýõ¯ÈÍÍÇ/T­Ziiixøá‡EG¹^)(íoµZG÷îÝ›í&[ɧX,–]xŸG-EQð¿‡Q>¯¬¬ EEE¿ý»ªªÓ¸†Ç€f„«g |ðKæwß}§ë'1z׃ßS ÍIÈt^Å$ÃÓ¬Vë0úKLj.¾ÁÛÄÅaé²e ErÓ¸ñãѾ}{·®u:HHHÀñãǽœŠ§Ó‰cÇŽ‰ŽáBBB°`áB·ÿ~1r”v±Z­«D‘ŒI«¨˜€Ë)Ý»uãQÆo\¼ð¿9ŽF˜p–Ã=)Ñ4ý/PBê³.dÄ-×;tè2å4c·Bøµ)òϸ˜˜½|ŠI¾"VU§SBú(¢¢¢°tÙ2„……‰Ž"UR¿~ýЫW/·¯OKKCff¦I¾"44K–.ÕÛ´í# !¬Vë6ÑA$㊋‹;E.OîŽFõêÕy”ò ×o0BÀù ìž0ʦGÀùóçY—`JÓ4lÛ&¶¹«UëÖ¨[—ù_³_hÚL>…$_k±¬&@/ºè»ŽŒŒÄÒ¥KQ³fMÑQ$uéÒÅíãþ`ãÆX³zµI¾¢víÚX¶l™®ÎÚ¦À®ò€€x‹Å²OtÉøL&ÓLe¬ë(Š‚víÚ±.ã7.\·pß}÷¡Z5]ÀÇ ”]÷ËJ™®Rhš†‚‚–%¸CÿÜשS'^¥¶[­Ö¼ŠI¾GUÕ/ ð]ñ‰%rÀ"##‘œ’âöI'@Ò”)r«™ô;õêÕÃò+):Êo°¹ìêÕGˆŠ2v‹§¤ÑÑÑǰ†G­:ð(ã®ïP‹E`š»»Èa”Ö`_Ä}¿l˜.\ºtIÞ<û9n ”.âSHòeªªnלÎ?€£¢³@DD–.[æö4y‰½š5kbî¼y¨Zµª[×b䈆ú'ñÕ¨Q#¬X¹ááᢣ\ïƒŠŠŠíÛ·¿,:ˆä[!syÔéÏ£Œ_¸råÊ ÿ®÷m×/X°B]qR²²²0ÝdÊ£ÕBÒ¯ÐÐPDGGó(u¡¤¤äc…$ßצM›ŸååÐÅi-Z´ÀÒeËP»vmÑQ¤›`Ö»ï¢A÷ŽýÕ4 qô¨.Ö›$ ÇŠ•+ѨQ#ÑQ®—jUÕ¾qqqº˜—"ù‹Å²óNÎÆ£qc懠ù…¢›t>°¤„ý¬IB©¾Ô@X‘ þ­cÇŽn·ÄVÖÇÇÇs™ +ù‡¸¸¸£f“é T›7oÞü—E€:ºÞZæwÆŽ‡6mÚ¸}ý²¥Kñý÷ß{1‘ä ""#±|Å Ô«§›YR%d¨UU “S$ßD yGè˜e|ÞõÇÀý÷ßB˜ÞZzDÓ4”•–²-Bˆ¾·B˜˜æÑj!é—'Œ+ƒPú—B’_‰ŽŽ>c2™:@Ç€Üwß}X&tãù^ðhâÿ÷ߥK—z1‘ä Zµj…eK—ê¦ã‡W)!ÏÅZ,i¢³H¾hÚß0¾CZ·jź„_¸rÓ@µjÕÜîˆã¥äêUÖ%ôÝ ( óÉR—}` ä>N{NX­VùMb"&&æb@@@wlš5k†åË–¡Ž\ª}ûö5j”Û×=zåŒé­[·Æâ%KªŸ#@/išöX¬ÅòÑA$ÿ`µZ/` ë:­Z·f]Â/ß´@WKoå*û€Ö<¡€ñþ(’CüVHHš·hÁ¼þ @~Š–˜‰ŠŠºr14ôqPú¹è,Þ¬V¬\ÉïxMé5Âô3`2™Üº¾¸¸#GŒ@aa¡—“IFÖªU+,Z´¡¡ºyx”J‰ýNtÉ¿PB¾f]#**J×­êFqsDFDHâ: îMæD!„0+}«•!É?X,.ûÿ|Å£ˆäß:‡‡_ÍËËëà}ÑY iÓ¦Xºl™\à,$$éóç#ÌÍ'´”R$M™‚x9™dd÷ß?–.[¦§'ÿ§ÐÅjµÚD‘ü‰³®Q­Z54’ƒ=v«¡z²@ç CœÚÿ‹/††Êö‰‹Þ½{;­ªú*‹Î÷Þ{/–-_®§Aa>‚””4kÖÌí×X³z56nÜèÅT’ÑEGGcÑâŨ^½ºè(×ìwVTtPUuè ’Љ‰Ép–u9Àsåee¿ûµwp8 Àœ‘‘af]Ä]Š0Ÿ ;üWdË–<Êü§sx8ó¥±cÇÂétz1•dd‹‹-BµjÕDG¹f(í{RtI"„0_hÚ´)ë>¯¼¼üw¿¦( î½÷^i\Ãc ¢¢"˜y7)PæáäÿT¥J4jÔˆy'!Û˜‘¤Û°Z,Ó@é`è`exx8/^Œ5t}ü¬!5lØÉÉÉnÏ4©¨¨ÀØ„y,®ôUU±pÑ"„„èfXôF…?Z­ÖK¢ƒHh7iº„Ï»Õ4Öñ|…«ì· ¼¼\¿àpLÜàŸš7oÎ| ®V1›w1-"IwaµZ€Ò—TˆÎ‰9sæ ((HtŸQµjUÌ;×£ál©©©°Ûí^L%Yll,,\¨Ÿ›J?¿ú¤Åb‘Ø$ÝP(Íb]£‰\ðØíôüg[z‹m ÞàÞ1A(d€ÄDóæÍ™× @VTTûïbIº «Õú!žP*:K›¸8¤Îœ ³Y·óg eÒ¤IM4þêË/±~Ý:/&’Œ¬M›6˜¿`~nþ÷òòòzÉY:’Þ¨ªz‚§YÖ AíÚµY–ðy·]Ðñö ÃV¼ŠŠ /hógEW®°.!é§o|y<‘¤ªªn @ÂwÿÃþ€‘£F‰Žax/½ôëÑÃíëóòòœœìÅD’‘Y,¤¥§#8X7[CXUõµÞ½{ËÁ’.QàgÖ5ôܪnd9lvåPÃd2q9Ý\fË¿TŸÃ±d”yD‘¤+ªª~ J{(åùçŸG»víDÇ0¬¸¸8 6Ìíë¯\¹‚Q#Gr6$éßý÷߯«¶¼mUU]Ì/‘¤Û¢t/ëz~RmdzþsåÑàt:uÜÀc€\ðKõ8Ifv3/"I•dµZ·jŠÒÀ‘9JJJpèÐ!‘ «^½zH9&“{?¿)¥˜ú'EQP—ý€¢¸¸8yw#éVEEÅnrJDíŸss±råJ¥ /aìXÄÄĸ}ýÎ;1?=Ý‹‰$£º÷Þ{±dÉÔ¬YSt ”¡V«u¦è ’ä*J)ó|Ö­Ÿÿÿ¢N:ÜrTÕØï|2é| ÓÞŒÛM†”|[õêÕÙ·ý"{k%]3™Í @ikÞuËÊÊ0iÒ$89ìqó5O?ý4zõêåöõçÏø±c埽„úõëcÑâŨ­À”Cb-–4ÑA$©2òòòŽ`ú†&„éç=BÈ÷±Kwtû[ÉP[‚ÝÁ£À©ó²@ò:Âað%U”RæE$ÉE‡£%–‹¨••…¥K—Š(mh˜4i’G¯15) ‡öR"Ɉƒ‚0wÞbzöì):ÆQú0Q”v»}0w|JCÌ;ä€ûªßaŸÕªU¡pØ\Y<îOM:nƒW0 'ü•5¢i\& JÒíØl¶B©Í÷9ÌŸ/¢´¡µiÓ<ØíëN'Æ&$È® ?7pà@†GrV•é6›í«¬¬,}Žä–¤; „0¿ƒÔñ½šîݩ͟¢Ï.?ïT– %%%<Êèsc‘ä8D ù÷ 7—/_FBB‚|]Iµkׯô3<Úë¹`þ|yÚ‚Ÿ{±O¼ñ曢cT!=Lfs¶Ýno':Š$U†FÓËÈ“\(SÚ€}IúŸ¬¬¬“¦} ”wí .`ÜØ±ÐñÏ]ª_¿>’““=úaŸœœŒ¼ýû½˜J2’ÈÈHÌž3A>yŸÜ§°¨hÓöíÛk‹"I·¥iLøÈÏÔª}ç·=vð˜ ÷!€¥, ép߇ÄÞùóçy”iÈ£ˆ$]c2™’@i[Þu5MCâøñrò|%™Íf¤¦¦",,Ìí×øÇúõøêË/½˜J2’úõë#}þ|]±ò<˜ép8ZŠÎ"I·DÓ>………,_ÞçվˀI‡ <:Ôu=Ó^m³ìðKEEE­ö]öÿ:àïÇRÆ[ôxôƒÄǹsçx”ñ£Og’@ AiSÞEÏž=‹ñãÆANjȺԴiSL™2Åíë5MÃøqãpZ~(ôKŠ¢`êÛo#..NtªJÿ/ÛáxFtI€_‡T6aYãŒÜ^ç¶»µÿ€¦ÃÓsxlpšLú]ãHA5DÇÐû•5Mkżˆä×l6[?òïºN§cpáÂÞ¥ -((³fÍòhÏöâÅ‹ñã?z1•d$Æ ã>*:†H„Ò²÷Î$/),,l€é&rÙà¾zõî¾;£¼œé¼y·ðØ`ÖñÓ„0_àñ‡¬uëÖEëÖ­EÇÐ…£ÿý/ó”÷7øJÒ]8Žf $MDíÅ‹a×®]"JÚØqãc…xl¨¨¨Ðo¥”é!U«VeYBWZ·nè˜Ñ1tá‡ÈEf X €{KÏöÌL¬\¹’wYÃ{òÉ'ñÔSO¹}ýÉ“'1!1Qn¹ðS]»uèQò~÷:”Î̶ÛßDòk÷³.pòäIÖ%|–K ’T-4 @{~¥J™Ÿ}̺„nܘhæ‹•†ðß#Gx”‰ÉÊÊòŸ¿`7aaa@i<ïº/^Ä„ äMh%EDD`ì¸qn__QQqcÇ¢  À‹©$£ˆ‰‰Arr2 †lÇ‚*I”6,_¿àÒ%C«}Vƒ†w?‘[[x¼×›œNývB˜Ú©v—ã!|‰Õj…ÕjÕåÄKÞŽ9Ê~ð‡Ùl63ýá ù›ÍÖ„$ò®K)Å„ 䇑J ÁÌY³P¥J·_ãÝY³àp8¼˜J2Šððp¤§§{ô÷ÇçQ:Èn·¯ÍÊÊò¯ÁN’PYYYxeýyy,_ÞçÕ¯÷ôØÀc €ÓlÖí“EØ/x0ŒÉHBCC‹Ð°0´mÛVtáJJJx­Ú™GÉ?dffÖ!kÀxèЭ¬^½Û¶nå]Öð&O™‚{ï½×íë7mÚ„>úÈ‹‰$£¨U«ÒÓÓ&:ŠîQày“Éô™ìº“x!qª±¬±ÿ~–/ïóº°@A|ƒÝž#¬B.³.âÉ4f#éܹóoOþ»uë&8>äææ2¯A ‘Ø’×¥¸wÝ={ö`Áüù¼ËÞ‹}ú {÷în_ìØ1LMJòb"É(‚‚‚0gî\4nÂô„1ßBH“ÙüíöíÛï~ö—$yHa]cßÞ½¬Kø¬êÕ«»´xZTTÄ!Måð˜O§(ŠþþÃ¥@Óä/ùãÿøÛ?wéÚAAÓèÃîݻ١4ÞápøÇ*“Ä”Ýn„p^XXˆ„1ct¹J®g1 † æöõ¥¥¥5r$®\¹âÅT’(Š‚i+E$ã†vÿÉÊʺû£?Iò¡´+ëyr €ÛÂÃÃïú5𦡏¸˜}˜Jâ±å+00P¿ \føA@«V­ß±ãoÿ^«V-ôêÕK`"}ؓãL €.< I¾+++«)‹¨ýöÔ©8q℈҆ŠÔÔT¸¿%9uÆ Ùþé§FŒ‰.]ŒñcãèÑ£ÈÎÎãF„XL&Ó999Ü»¥$ÿ}fY£¬¬ ‡bY§ÝëÂ@QQ‘.‡sX¨hÑ¢E)ë"îR\b]¤Fh(ëBbÒäÉ 7 ”äæærùÆ×€§™‘|™b2›ßÀ}#ð?þñlÚ´‰wYCS))). º¯¾üŸ|ò‰SIFñ‹/¢OŸ>¢c¸äÈ‘#xõ•WðÊË/cÌèÑ(,d~p“ëi®iÚÖììlUtÉ™LÏ€ñ,‡Ã²2ÝnÓÖ½pfïèµÃŽÃ€>ÿÃÅ¥ fÍš¬KÓ A,^²­[·þÝï…„„`ÅÊ•ˆç~’˜nñY]¥ôϹ¹¹ì I¾Èæp$€Ã^ÛÍ•-ºZ´¼NöÛ´uÛþJ…¢œc]Ä×EªªHœ0Ÿ~öbccoûµuêÔÁÂE‹°lùrüéOBu?™‡p½|Þ`k–:Ì÷ŠI¾';;[¥Sx×-++ÃøqãPZªÛ1]zà0`à@·¯/))ÁèQ£PRRâÅT’DGGcÚ;ïp9ÅÏ?‘ IDATÿÙSçÏŸÇ Aƒ~w$èñãÇÑ¿¬[·NP²[ª B6îr8Dò ‡£1€?°®³cûvÖ%|š+[®èu€ý@]/˜Š’¯8L‹øÂ@í:uvíÚ¡k·n¨[·n¥®oÛ¶-Ú¶m‹ŠŠ ìØ±?nÛ†={ö`ïÞ½ºŽáM?ýôžáæuMë à_Ì I>#+++à×ÖîÝ#óçÏ—ûÏ+©N:xÇø””¹çÓ5nÜóÒÒ¸ ~òTQQÞ<Ç»åï—••áiÓpðÀ$Œ«—…ÒϲŽ~±‹-22Ò¥¯»6ÃDO!b<@ÑyÀµ§þù,‹øÂà—³,wïÞ´´4üå/Á‹/¼€o¿ýö®×íÝ»o Œ?õèY3g";;Ûï>üÿÈë–Ò7»~ä×~¹ €ûgȹ)99ùùLßr}N§Nðò+¯¸}}QQF%ç-øB’’’ÐÆƒ…#žæÌ™ƒÿû¿ÿ«ôuû÷ïGß—^¾}û¤r[ÿ²òòçææ÷ $¡ÀÖE(¥øæ›oX—ñi®.œÕá@ppðïNnc@.„„„xÜ:¯G?ÿü3F ŽIwxš÷é† èûÒKزe‹Ï¶ø»"Ã……/‰²ÙlâUL2¦²ŠŠI Ä»î矎oä‘•Ò A$''{ôÃ:iÊ=zÔ‹©$#wÓ S=àÑ™N Ñõ(ãziMcâ‹/¾ÀŠåË÷ë‡ÉÉÉr€¼¼<9|˜O1EͧdD»rrâ@iïºÇCªlA¯”€€ÌHMEhX˜Û¯ñÑGa“\tñ;½zõÂ+¯¾*:†K2220Ó Ç^¹r4[¶lñB*¯i¡QºÅáp<$:ˆ¤oëׯ7`*Z7näQƧ¹Ü Ã€Ú<¶¦Sªÿ8úPýúõY—jùŠ¿ûK>oÞ<8Ÿ°`$Ü>„Sú°Ãáhϧ˜d$R4í}l'RÞÄét"11EEºîÓáÇ#&&Æíë÷ìÙƒ9³g{1‘d|ãEÇpInn.ÆçµmW¯^Ű¡CñÅ_xåõ¼¤ŽFé&»ÝþWÑA$ýŠŒŒì J[³®#Ûÿ=× A—‡ªêr vmöEÑßøu~é äÖgÍxQý X—ª¬´Ÿ}öÙoÿž——‡]YYéϧpTÀ¹î’þ]¹re2€hÞu—-[‡ÃÁ»¬¡uëÞ/¼ø¢Û×_¾|cFFY™œAæOZ¶l‰ÔÔT˜L&ÑQîêĉòÖ[())ñêë:NL™<ÿX¿Þ«¯ë¡ ¬¶ÛíIsz¤›deeSB¦ð¨åp8púôi¥|–« óš¦áüùóŒÓTa(¥Ì®{BB)óÍ‘õêÖe]B¸ë÷örÜón<·Pà1»Ýþ—b’!8Ž6 d”€ºX¾lﲆִiSL™2Åíë)¥˜4iNž<é½P’îÕ¯_iééz9ïŽ 0xð`fŽ5MCJJ V®XÁäõÝD(0Éæp|üã?VFÒÅlžJï~¨¼|©¯îCб¸6BéìÙ³¨¨¨`œ¦òêqèJ'”êz•I'!Ì|½öíÛ‡cÇ~i¦Ø¼y³à4út}—k”nÅ$]Ë8r¤ŠFépžú_RR‚‰&È­@•„©©ÝÄ­^½ßedx/”¤{!!!˜—–†ºxØP^^Ž1£GsYOOOÇŒ3ô5„˜ÒÞUƒƒ¿q8úÿKbÎápD`ZEEEø×¿þÅ£”OS]\¸vO¤7 6äQFÿ[ˆÓÉ~ÀÇg\óý÷ßãܹsØ¿¿è(ºôùçŸóŠHi¼Ífëͧ˜¤gaI¢x×þÎ;rú|%;­Zµrûz›Í†ùéé^L$éÙlƬwßuy(•H׺S¶oßέæGÿ;¦&%éíøáŽ¥YÙÙÙñ¢ƒHBÒÅà´8ÿÅçŸËY< @KO8îÇ N³Yÿ±±±§0Ý(Ù¨Q#–/¯9öìÞ­¯Õv¹páß's„ÌÞ·o_u~%½Ùåpt0’wÝo6mÂçŸλ¬¡=ñÄxê/qûú‹/blB‚.[%v&Lœˆ:ˆŽá’ôôt|-à ä§Ÿ~ªÇ™‰¢dØl¶¿‰"‰a·ÛÿàAµ(¥X·nR>­U«V rékêt€Ã=)-¬Víë"žP~ýÿ€ã, 5lØ,Kè‚ÃáÀîÝ»EÇеO>ù„g¹Æ%%%“x”ôãÀA ¥+p–ŸŸäädž% ¯E‹Mn×4 &LÀ™3ºž»#yÙo¾‰'Ÿ|Rt —|ºaÞ[¹RXýÍ›7ãoƒéí h ™os8Ö:ýo¼Æn·ßOT^õvlßŽÃ¼Ž£öamÚ´qùkõ¸ $$uêÔa]ætçð𫬋xB¹îŸ™ö©*Šâ]§NÂÎ;EÇеÌÌL¾mA„ ³Ûí±ü JzqåÊ•dpný×4 'NDAAϲ†‚™3g¢J•*n¿ÆŠ+°mëV/¦’ô®çãcÀ€¢c¸dë–-ºXܹs'¼ù& .]åF”¾¨iÚ¶]»vµEbïǬJp)Ÿþ{Ç<àò×êq @DDa{ þË´€\¿À|Y¬é½÷².¡ ò¸¯;Ó4 «×¬áYÒL5Gޏw!Nvvv[2œwݵ|€í™™¼ËÚ„ Þ¬™Û×ïØ±‹-òb"IïxàLž<™ù9oØ»w/t3 t÷îÝxõÕW‘ŸŸ/:ʱ(&ÓŽl‡ãÏ¢£HlÏp?¯zÄ·òt.™L&Xc]ž¦Ç€Ìkh†Z $u±¦M¹œð!œÎíèÒgŸ}†óçÎñ,yXAÁ4ž%q222ÌDQ–‚sëÿÁƒ1Áž% ïÙgŸÅc=z¸}ýùóç1~Ü8ù¾ëGš5k†Yヒ€®‡z¸%??Ç Ó[Û=<ˆþýúéqHi¡ôS›Ã‘¼~ýz®ïßÙvûKàÚº³dñbù3 ¢î¿ßåzNŸ>­»÷=ˆä°à"žøm€ÌÇÖûË€twe¥¥øè£x—šÝwQ‰¿°°°á¸nû(++øqãPVZʳ¬¡EFFbÄH÷ç3jš†ÄñãqŽïb¢$Pí:u°`áBÔ¨QCt”»***ÂàÁƒqú´>‡AŸ:u ¯¼ü2öíÛ':ÊÍ(MŒˆŒüWVVóͺ?‡£½,åYóàÁƒòhn/©Lû^óçÊn‰àpZŒÂážÚS¿-˜ aö^?Ù ¹fݺu¼W¢(feeÉ•(æp8š)¼ëΛ7yòøO—Õ¨Q³çÌA‹Ó„oeÑÂ…È”Û-üFÕªU‘ž–† ˆŽrW1b„îßΟ?×_{ ÙÙÙ¢£ÜJwS@@Ö¯'¹Hg·Ûi”~B®Û1˧ÿ^Ó®];—¿öÀ “¸‡‚-ØÑ4ígæE<ôÛÀ•+Wà—Ó˜‰àÓv!ÄåË—±îãy—½ÇðO9Àw9)] ˜gÍ;w⣿ÿgIC#„ iêTÃnÛº+NT—øRÓgÌ@ë(®3=ÝB)ÅÔ¤$ÃÌ),,Ä ±uËÑQ~Ò¦ ¥?dÛí“222Ì¢ãHîÉÊÊ ¦Àì`¿N^^þ#Ÿþ{Epp0b+±ÿÿ€;êׯjÕª1¯c6›÷2/â¡ßâããK@Ó£ÃÂÂpÏ=÷°,!Ìûï¿ÏZ:¥mà äÄ0d·ÛÿJ€GyÖ,**”ɓå†JèÛ·/:wîìöõgΜAbb¢ü3÷# cÇâøƒè.Yºd ¾øâ Ñ1*åêÕ«6l¾úòKÑQnÅL€¤°š5¿ËÉɹOt©r²²²Lfó:®÷{IzZšü9á%íÚµ«ÔqîzÜÉ¡ýŸ§cbb.2/ä!å¦gÞ«&»¤ë]¾|YÔ¹Èým6Û…%6rsskQ`6ïºsfÏÆ‰'x—5¬ØØX¼5dˆÛ×WTT aÌ\ÒÛ1f3}ûõóÏ>+:†K¾üâ ,Y²Dt ·TTT`âĉX¾|¹è(·ÓÑ©i6›ÍÖOtÉeŠÉl~@OÞ…øáüðüËú¬N=äò×–——ãÈ‘#츉Ç=(tßþܼ@)óI0r@ºÙG}„3gÎð/LÈd›Íö:ÿ ¥ååÓpm1Ú¶m>ùäž% ­V­Z˜>cL&÷‡{§¥¥Án·{1•¤gÝ»wÇСCEÇpÉÎ;1uêTPJEGq¥ æÏÇÔ©SusláMªƒ÷mǺÜÜÜZ¢ÃHwf·ÛçèûnYYf¦¦ò.ë³!èÔ©“Ë_äða”——3L䞨û9œ Ü°@ݬ ò˜¾(Kii)/^,¦8! ív{/1Å%o±Ùl€ëbNaa!¦&%úÃ>OŠ¢ eÚ4Ô­[×í×ÈÈÈÀkÖx1•¤gV«ÉÉÉP”››õçðáÃ9b„.?ôºcÃ'Ÿ`È!º<Æ @iï²òr{¶ÃÑUtéÖìvûT ¼%¢öšÕ«uy½QEDD ^½z.ýžÜ\†iÜg‰‰a^ƒºœ¨z³~ªJ¬ r:Q2˜Ï?û 999"J›)°Îf³q_¡–¼F!éøý–&¦RSSÅt®Ôëo¼Üæ}âÄ Lž4I.¸ø‰¦M›bÎܹôà”^.\¸€·ÆåË—EGñªm[·âµW_ÅÙ³gEG¹Æ„ÒMv»}ÞôÿÅØíö$ LQûÌ™3X±b…ˆÒ>ëáJÎìÙ-æóü5hеë°?UÔLˆñªV­š€é§«ðfÍ ñ]âKÓ4LKI5¬ÅBÞ·Ûíω(.yÆn·¿ =ÏšøÒ`C¾DŠÇo¼áöõeee3z´ÏÝ`I·†ù  ,,Lt”»*--Ű¡C}vÈÞ½{Ñ÷¥—ô|œ!¡À+EE™6›Í*:ŒØŽT LUÖ¬Y())UÞ'uïÞ½R_ïÐá@ ‡§ÿJËÊÊtpÓ@Ë–- fY0 ­Z¶dYB2¨½{÷Š‘1ßÍgF2 … "??_TyÛíöwQ!¤ÛËÍÍ­•““S3''§¦Ãá1›ÍÓp5}útœ?žgIà @êÌ™=Éݸq#>þøc/¦’ôJQ¤¤¤ÀÂლ7Ì›7›¿ùFt .Š‹‹1bøp¬^µJt”; !ãkdÛl¶E‡ñ'æl»}9(&*C~~>ÞyçQå}V×nÝ*õõ¹¹¹ºY›m·oÎÊÉi%:/±Ùl)Á?¯9‚¹s戌ೞøóŸ+õõš¦!{óÛÈJ‹mÓ†G™‹q11ûxò†ß-X­Ö#˜nÂ&„È9Ò#11Q²jJÿa³Ùfee‹ãKÖ¯_oÊv8^ȶÛí¥Ÿ ‹èL·“™™‰Ï>ýTt C ÁÌY³PµjU·_㫯¾Þ$ñ‹ääd†Dò’ŸŸ!o½%»R~uüøqôë×› 0t]Lšf·Ùí3233kˆÎcp$Ûn B6pýpxŠ‹‹1jäH9õŸEQðøãWêš}{÷êrF ØÆ'éyÓí~â2bÐ&Vwø$q8X¶l™è¿ d ÉlÞe³ÙÅ(ÙvûK‘‘{ ¥@׫%%%x{êTyþ¼ !˜2e ÂÃÃÝ~C‡!%9Ù{¡$Ý oÖ sæÎ5ÄÑÀEEEòÖ["‡ÔêRII ÆŒAZZš.Ý$À˜ *Uòìvûëׯ7‰d4;rsëÛìöÏ 0 ·˜#ÆÓµ}ÿÃguèÐõëׯÔ5Ûwì`”Æ}\*ÚÿÜf€rXhû€¼’înÙҥزe‹è×´![ívûùÁÁ=ÙÙÙÝlvûN¬ÐBtWÌŸ?ßgÏøö¶>}ú [%]¯¤¤cF–Osü@í:u0þ|Clt:HHHÀ¾}†éîäŠRŠ÷V®Ä!CPXX(:Ž+êR`IDDÄ®ììlcLÔl‡ã™€òò•{,ÌÈkÖ`ãÆ¢cø¬??ùd¥¯Ù¾]Cðc,T©ÂådPC ¡¸å€Âa **Ê£QÉ?hš†ÄñãqLü<€k(ðvDdä÷»ví2Ä ¬8Ž6Ùvû×DQ6A‡ûûoÇápࣿÿ]t CˆÅÐaž5mÚ4ù4Ç#=- 5Å%Ó§OÇVý,DëÖÖ-[ð×>}t?à7„Xˆ¢l²9›dwßíÙíöF6»ýcBézuDç€;v`Þ¼y¢cø¬ÐÐP<Ò¹s¥®©¨¨€-;›M tŒçQ¦¢J•*Æ_ ”f‚ñ>³Ù «Õʲ„ä#._¾Œ‘#F ¸¸Xt”ëuTL&‡ÝnOÝ··k×®Ö6‡cFéN<*:Oe”——#iÊ#´µ W»vmÌHM…Ùì~Gè§6àË/¾ðb*IL&¦Ï˜ÖQQ¢£¸dõªUøÇúõ¢cÆÑ£GÑ·o_|þù碣¸ŽÒn d»ÍáØ”­ŠŽ£f›Ã1”¹žçšÓ§Oc¬ÄÉÔc=Vé­Y999zûœèøàƒ<Êd·lÙÒíO×ÜrÀjµ^Ǻx\\ë’ÈËËÔɓõ¶»*¦GDFþ˜““£ë}ì¼Ùl¶›Ýþb2í¥½Ñ™*kÍêÕ8tèèºwí†îž{îqû5òöïÇôéÓ½˜JÒ«ñ‰‰x衇DÇpÉæo¾‘OÝpõêULž4 III(+-Çu”v#в+Ûnÿh×îÝì7 ëÉv8ž©Y³f( @7C ð·AƒpáÂÑQ|Ú“n´ÿïÐaûí:uЪûÃ?(ðó"^vû±»”þĺx\Û¶¬KH>dÓ¦M˜;w®è·ò€SÓ²lGrnnn5ÑaR²srzØìö/AÈ^}p§÷;uê–/_.:†! ~ë-´õོ¸¸cPj¤É-¯¿þ:zõê%:†Krss1qâDÙäO7l@ß¾}õ´…Ï žSœN›ÍfûÌáp´ˆ'‡Ãñ„ÍnßE(]O]›XZZŠáÆɅyÆZ´háV‡Öwß}Ç g:ÆÇƒöÏŸ¥ß3/âe·ýpN…ùLtt4BBBX—‘|ÈêU«°fÍÑ1n%”&–•—¶9C322„NÇå)33³†ÝnÃf·çMû?=aÐÿk¦¥¤ÈAt.èܹ3úõëçÑkLMJ‘ǽ”HÒ«ž?Žƒ‰Žá’'Nà­Áƒå{€ìÛ·/¾ð6oÞ,:JeògÒŸìvûÎl‡£¯¯þ\ÏÍÍ ´Ùl½mv{¦Féçt·?WÓ4ŒMH@¶÷˜ûš§þò—J_sîÜ9üüóÏ Òx¦cÇŽ<Êв²2à ‰¹í‡tMQ2X7›Íh×®ë2’™3{¶ž÷ ×¥sÃjÖ̶ÙlOÂà7·“™™YÃf³õ±9ŸV©Rå –0Ʀ޻شi“žNžÐ­&MšàíädV×?þøcüûßÿöb*IÚ¶m‹I“&qyã©¢ÿgï¾Ã¢8þ?€¿÷8Š ŠŠ…ˆ Êb4FE5öнE» XÀФ+bﻉ ±Kì£b ¨ˆ¨Ò¹Û߉ù™o,Àíîìózž<‰In>ï…Ù™ÏäæbæŒt‹1‡Þ½{‡Ùžžðõõ•ä¢ Ø1,»ÅÈØ8%!)iv|||ùÏ;‰H|||ý„„„ÅEÅÅ‚aöíypPΞ=K:†ÆÓÖÖF¯^½Êü¹¸¸8±Ñ…L&CÛo¾á¿ËÞlÓ¦Í+þ që“«™v-[>HHLLPÏí;tÀ™3gø,Ai–eáçç‡ê&&h'LwÏòh †9”ðÀJ¥R¹ÉÎÎî-éPêHJJªÉ²lw–e‡èééug=°,¿ÝB–››‹ÁÁ¤cˆž®®.V„„ råòŸx¹“œŒÐSQbdѤ ÂÂᣣC:ÊcæŒHII!E#=rܹƒeË—£qcI^¢S,ÌÈd ?³ZZSþøãä!C$ÓîîÝ»U œX`,#“ÙC/)¢££ñã?’ŽQ!tqt„±±q™?'¾ð-Z´(×KY± ó+ïExð¥íL¿á¯s¼¼éСŸÃSêýƒZPp0:uêD:Χ1L#aZr¹_bbâQ–e—Âjáõë×Mär¹½ èÌñ×~ £Q“þ­Zµ ¤cˆÞ¼ùóѤI“r>''ÞÞÞ(**â0%65jÔ@dd¤Z EBaYþ~~¸víé(íÁƒ5jfLŸŽ#GJbWÈGè€aœ•ÊÉ¢I“§‰‰‰ûìµ¶¶¾@tM#âããëÈd²¾,Ëög¦3 r):bbb°níZÒ1*Œ#F”ù3………¸|‰÷¶qe&P÷@&“äó¹- .`Þç9 Lï~¦ÊEGG+BB$ÓUúoJ0̆e¯ª& Jeâ[cã[ææ¤ÅÇÇ×g¦%Ë0V ÃXeZ@‚ÝûË+%%Æ¥M¿¾`РAX°pa¹?ϲ,f{zâôéÓ¦¢Ä¦råÊØ¸i,,,HG)•511ˆ‰‰!£Béܹ3.Z###ÒQ¸ò”.©€'k1Ìc†aëèè<æûа””Ýììl3---3Ø1@–e›3@K¼_¼—ã5 Q IDAT˜èèh:ùP³fͰ«;-âââ0cút©gëÖ­°Rð~™G¡Œaª+Š\¾ qí³;†ùMˆ7}ß¶oO¨r)**‚§‡‚‚ƒáàà@:NiieÛ±@;†e™ Foß*_HðŒÒX†yà% ¼‘±ìk–eß(•Ê×:::9EEEïìììŠ?WdïÞ½ZÍš5«ZTT¤'—Ë«0 SK¥R™A&«•ª€º ÃÔcæŒLfüý„ ²s\B ¡“ÿ/hni‰9sæ¨5ÆŽ;èä_Ãikk#$$D2“ÿ£GŽ`Íš5¤cT8gΜAÒÍ›X¸p!ìííIÇáB]ü~¦­bY€e‘_P€„ÄÄ< ®^âï?–Ía¦@–ŠaJ•*›a%˲ZÌ_Û#Œ€eY£~\ƒe°lu0aêjÈ´´À`þþ.¹ÿV­\IoâØðr¼ý ÊÞ †††hѲ¥¥~—âäøÂø»@]>C\¹|®®®|– 4œ\.‡¿¿?zôìI:ŠÐXYÿu0,  À8Û'çÏŸÇ´©SIÇ5CCCìØ¹uë–ÿÛÁͤ$L˜0%%%&£Ä„aøûû£wŸ>¤£”Êõë×áîæ†ââÏ®§R<8h<==éÍP¢¢¢°qÃÒ1*###?qºººeú\II º::âí[qµ¸êÙ³'–,]Ê!†ñ±Q(–ð_ˆ{¥™ œä;„]ëÖ044ä» ¥ÁJJJ0þ|lÚ¸‘t¡1Œÿþ£6 ˜þý×tò_JJ¥’6£û™L†€À@µ&ÿo³²àííM'ÿnÊ”)’™ü?~ôžtò/ÀðaÃè5o˜J¥Bpp0ü0dÈ2OþàÒ¥K¢›ü@×nݩð¬$Ïÿ¥˜$0ïw4iii‰»‘% ,Ë"22þ~~P*%Ó”—"lß¾}xDï¡ÿ,gggµ¶²,‹  ==ÃT”Ø899ÁyâDÒ1Jåõëט6mš(^+ª´´4LtvFÈŠ(( Ö‡" °°Þ^^عcé(ŽŽ®.† ^®Ï?~œã4ê300@{afX[[K¶kìd2ÙI¼Ï¦ºtéÂw ª‚8pà¦O›†Ü\IË¡”››‹˜Õ«Iǵo¾ùnîîj±yófœ;wŽ£D”uìØsçÍ#£T 1sæL¤¥¥‘ŽBý•J…íÛ·cˆ“®\¹B:%€¬¬,LrqÁ©S§HG©úöéƒêÕ«—ùsE……8+ÂkÜííí˵›¡~oý(­/.XYY½ÃðþU¸]»vÐ××ç» UA\¼x£G¦Í%©ÏÚ»w/²²²¾ü/VP¦¦¦Xºld²òŸ(¹qãV­\Éa*Jl,--±<(ZZZ¤£|‘J¥‚¯¯/n&%‘ŽB}FZZÜ\]áç燜^èS¥¦¦bÜØ±H¢¿‰Éd3vl¹>{îüyQ¾hëþÝw‚Ôaæ¨ …xRÚ§:Þèèê µeƒª ?z„1£Gãè‘#¤£P"TXXˆíÛ·“Ž!Z:ºº U늮ׯ_cÞܹôHŽ«W¯"£¢P©R%ÒQJ%2"¿ÆÆ’ŽA•˲8xàDßk ß~û £FŽÄŸþI:J…Õ©S'Ô¯_¿\Ÿ=!Òíÿß¶k'D©bF¥’ìù ” 2@ÿËŽŽŽB”¡*üü|øúúÂßÏE……¤ãP"rèÐ!¼ÊÌ$C´æÍ›KKËr^¥RÁgþ|dddp˜ŠCCCD­\Y®í£$ìÛ·[¶l!ƒ*£—/_b¶§'¦MŠ´ÔTÒq(5©T*¬Z¹³fΤ»;›èâR®Ïegg#..Žã4êsèÜ:Âlÿ?occ#éí£¥ZP(Wðþ¤ÜÑÞ^2o(i9pàÆŽ‹””ÒQ(‘Øýã¤#ˆ–Ó!0`€Zc¬_·—.]â(%6:ººˆ€¹¹9é(¥rñÂ,âZ(Š7çÏŸ‡““Ö®YCô%*++ S&OÆúõëñ¥kÈ)~uèС܋üÇŽC¡v¨û? Hzû?Pú#*¼ï£Ö×ׇƒƒße¨ êîÝ»5r$¶lÙ•J²};*¼ÜÜ\<|øP­1içÿO°R(àíí­ÖW._Æš5k8JD‰L&C`@lllHG)•{÷îÁÛÛ›EÑ………X½z5œœœpñÂÒq¨2¸xñ"† J†Eb’«k¹?{ðÀ“p£J•*h'Ìö¨´´ RˆG¥îìÄ2ÌA>ƒ¼×³W/!ÊPTQQÂÃÂà[PP€ãÇŽqœH}†††hÛ¶­ µdÀ~A ñ¬L—;³,KPçÒ¥K>>ôaOC™››#<<\¨ë•Ô¶mëVìݳ‡t ŠŒŒ bàÀ8zäýº$ääd|?n~øá¼yó†tê#zõîÆ—ûó»víâ0 w(HFCÞþe\øûÎÃ3|o//Œ5 7oÞ$‡ú¸»»—ûóñññ¸“œÌa"n´lÙB•Ó˜•å2ïåcFÿø~ýûCKKKˆRõ§OŸÆÀ²b^¿~M:ŽFz›•…Í›7£_ß¾ ùâ™L'''µjþñÇ´Ñßd2—,Q{·UTT9JE‰I¥J•…ºuë’ŽR*7oÞ„½‚ú>„¯ôï}{÷¢¨¨ˆt$úÁKll,ýÞ*rÆ C:uÊýù;wr˜†; ¤üamm/H1”y (?/ðæC&&&èб#ße(ê“ ±}ûvôéÝ‘‘‘xKßprâÎ;X¼hº÷"ÂÃK}-P‡Ôúæõ¾6õWWWtèÐA­1~;{Û¶nå(%&ZZZXKKKÒQJåéÓ§˜9cmG}RZZÑ»W/lٲ峻ͨO»ÿ>-\ˆAôˆ…dÁeÒ¤r>==gNŸæ07ôõõñ@ÝÿU€8Ï?”S™Ú¶m›Í‚´€êLE}N~~>6m܈޽{cõêÕÈÎÎ&IrŠ‹‹qìØ1Œ;#GŒÀO?ýT憋C‡U;Ç“'OÔCtêÔ ]\ÔãÙ³gX¸p!}룡æÎ›‡ŽY„ÏÎÎÆ´©Sén-ªT233†ïºwG`@<ü•Þ¼cYΟ‡»›† ŠŸþ™6Y”É“'£J•*åþü»v‰òM÷îÝa`` H-𣠅”çá-!!a8  T*ѳG¼|ù’ïRUjzzzèѳ'†‚æy;F‚J¥BB|}8éÿõ×_‘™™ÉɸNC†prýØËR7ÐTúúú Ukò¡!!tò¯¡z÷éƒ)S¦ŽQ*,ËÂÏÏNþ)µ°,‹+—/ãÊå˨V­zõê…þ¨Õ-]Ê”J%.\¸€#‡ã·ß~£=$ÎsölµžŸ<(ÊÉ£F™üÃ0µý(çHHJÚ –ÇqžÿHOOGŸÞ½E¹õ„¢Þ«Zµ*ì;u‚C§Nø¶}{TªT‰t$ÁäææâúõëøýâEN'ýïéèêâĉ0â  ùwÝ»—ºç€¦aË–/G÷îÝÕçØ±c˜?oG©(1iÓ¶-V®\)Ô}Êj[ƒ˜˜Ò1( Õ¢E ôîÝŽ]»¢fÍš¤ãðJ¥RáÆ8{æ ŽŸ8W§ÈèÖ­‚‚ƒËýù¢¢"ôëÛ/^¼à07êÕ«‡ºuëþÿÿØÈÈU«Våý{OQQr²³‘ùêž={†çÏžáéß~þü9RSS‘››ËkJ<š4i‚#F¨5ÆÁD9ù ÔÖx[›û“PÅ„¤ÖW$°AÌC9®,«áÇӭ§”F(..Frr2’““ÿóÏ P³fMÃÈÈÕªW‡±‘Ñ_?66F•*UþÙ¢[õï+]äÚÚÿî9À²ÈÉÉAqI òóó‘ŸŸââbäää  ?/22ùò%ÒÓÓñòåK¼ÈÈ(ó•|Báâê¿U«^]”ÛÙø¢¯¯°°0µPlÛº§~ý•£T”Xhkk#$$¤£”ÊÛ¬,LŸ6M” ©¨Š+//÷îÝýϼ¤200@•*U`hhCCCT©Z2†A¥J•þùž®¯¯ÿŸ…‚÷ß¿ °¨…(**Bvv6²³³‘““ƒììlð÷HIŠL&ƒï‚j-úçççcíÚµ¦âŽ­­-Z¶l)H-ØÕ®]»|AŠ L­…Bñ(!1ñ79ÊóI½zõBdD=£Di´ÜÜ\¦9víŠI®®jQRR‚9ÞÞô:( 4xð`¸¸¸ŽQj—/_ÆÏ?ÿL:†$µhÑ‚Ó>*E‰“L&âŋÕ~q²*:999¥â֨ѣk¸É2Œ8Ï@pDíŸEsó;9ÈòEÕML$õÆ‚¢¨òiÚ´)4hÀù¸–-Zp>¦ØX4i???0 £Ö8‘¸qãG©(±èر#æÍŸO:F™´mÛ³fÍRû×tEdI(ªB>blmmÕãîݻؿoG‰¸ehhˆ~ýú U._Î0Û„*F'Ë( °@¹ï,‹±cÇŠþºŠ¢ÔÓ­[7^ÆmÔ¨ªV­ÊËØb`dd„ððpèëë«5Nll,¶oßÎQ*J,Z¶l‰åAAj5‡"eì¸qXüÃ’ÌNJµjÕиqc|Ó®é(Eñ¨aƘ>}ºZc°,‹åË–A¥Rq”Š[ƒœþÝðšO ³ÇÊÊJÝí¤p2“¶¶¶¾ †‰ãb¬/177‡½½½¥(Š"„¯ÆdZZZ°ïÔ‰—±I“Ëå F:uÔçÉ“'øañb°¬ kº”@ÌÌÌ!Üúõë‡ÐPÎ{ƒh*Èd2XYYÁÔÔ”tŠ¢x —Ëáçï¯ö×ÅcÇŽ!>>ž£TÜÒÖÖVûZò`•Ê5‚#„»Wé*Õ*ÎÆú‚qß/T)Š¢&—Ëy=«¯nc<±òööÆ×_­Öyyyð˜5‹Þ­aLLL³f ªkÀ-:uBtt4*W®L:ŠèuîÒÀ_7>8tæý²&Š¢˜|ÈA"J,*W®ŒUÑÑjï “VvvX¿aª ô@(E5kÖDÛvRõïߟöP ( ÓªU+N^Š®_·êâÃ0#àÕ¢…,F g %, XÇDW5;\S%Nzzz¼Ž¯£«‹aÇóZCH­ìì0gεÇÙµs'N?ÎA"J,tttŽ&MšŽÂ¹¦M›bó¦M¨W¯é(¢ä>8zô(©øÑÉÁáÞL c{…BqN°‚„qÛào¬LÌǸóõ×_ÃÎÎN¨rE àîR§uëÖ˜W._¤^Y 44m>¸×»¼ 0vìXÜ¿wƒd”4lØ7m‚¡¡!é(¢rðÀHêˆ:ªU« >ü“»@òòòŽCŠºó7EQÿ¯{÷îX¶|9'»»üýüpàÀRñƒaìÞ½[ÈÛ]e½yÓÄÁÁ¡BuBæå¨Š‹waë®å&ì5EñH¥RáÎ;‚ÕÓÕÕExx8Z‰ð8QãÆ±}ÇN&ÿ@'ÿÄÔÔ«¢£éäÿ#„à+ SAvE¼~ýË—/Ǽyó>úFÿÝ»w7v,öîÙC'ÿ%Mš4Á¢Å‹9™ü_¹rä »vöjW–]QÑ&ÿ vvvÅ¢øÿ#õðõ×_ UŽ¢(žÝ¾uKÐz•*UÂêèhôîÝ[кŸÓ§o_lÛ¾æææœŒ·oï^=r„“±(ò ±rÕ*N΄jª.]º ::š“³òRqâøqìÛûßݬAË—#%%…@"Š¢Ê£FˆˆŒäd7d~~>üýüD}ÜG&“ÁÕÕU¸‚,ûJ&“ ¶[]Lx[€JººkdòYãCÓg̨°Í(JÓÔ²eKôà ;6EQ䥥¦«=~üxlݶ ‚Öe}úöÅþ8iö÷^VV¼fÏFQQgcRähiiaùòå°±¤Ï®ÚŽ= ×I“0fôhzôh…é…@QR'“ɰtÙ24oÞœ“ñ~ÿýwü¸k'cñEGWÓ¦M²ä;eqñ*! Š ¯  Å0Q2ø®óÞ´éÓ+Ìy?ŠÒd¤Wª---±cçN¸»»ÃÀÀ€÷zmÚ´ÁÖmÛàïïÏéyn•J…ùóæ!==³1]} IDAT)r†:98ŽR*¿üò .X€’’<}úã¿ÿ D²˜ššbÓæÍhÕª‘úB;õë¯ÿüõÙ3g&¡(ª,¼¼¼`ooÏÉXoÞ¼Á D½õF‰ºuë VVÚÙÙ ¶K]lx_P(¹ Ø]µkׯ¨Q£„*'zæææ‚L^(ŠkÄ¿aikkc’«+9‚1cÆðrÅZëÖ­½z5Ö¬]‹–-[r>~ÌêÕøý÷ß9—"cÚôé8hé¥râÄ ,ðõýכ眜¸»¹áâÅ‹D2U­Z«W¯†££#‘úBŠ‹‹CQQRSSqÿþ}Òq(Š*…‘£Faøˆœ÷Ã? 3SÜóÜjÕªÁYØëÜs† ² Øð¾ ì„€»œQ­Z5¡Ê‰Z‹–-ѬY3Ò1(ªÌT*ñ€÷Œáá鉣¿ü‚)S§ª}&¯R¥J0p öìÙƒuë×£]»v%ý·Ó§Ocýúõ¼ŒM o‚³3ÆO:F©œ={>óçtÛyAAfÎ˜ØØXÉþÚjŒaÆ©/”ÜÜ\$%%á²H¯7¥(êß:98ÀÓÓ“³ñöíÝ‹ßΞål<¾Lž2EЗ• £P(›—Š‘ÖâÅ‹y/R«V­âôôt Ó÷bttt` ¯sçÎ QNÔ&Œ5jÐ7€”$Mœ8‘h3¾ÿ¥¯¯V­ZaäÈ‘h֬Ћ‹ñâÅ‹R]©edd„.]ºÀÍÝ .„££#ªW¯Î[Ö‡bú´iôÜ¿†>|8<8|0äÓÕ«Wááá’Ïü¾P©T8uêjÖ¨ÁÙ9ײ`:v„¶\Ž«W¯ ^_( 4@JJ þøãÒQ(ŠúŒæ––ˆŒŒ„ŽŽ'ã=~ôžžž()÷ wMš`Á‚‚5qg€•J5¢víÚ9‚)F¨7lIII*–}@6¼J¥C‡ ÁÇ…('JõêÕÃþýû‘_P€Aâõëפ#QT©1 ƒËW®@[[›t”Ï*..FüHLLDÆË—xùò%À²022Bu4lØ …fff‚eÊÉÉÁ˜Ñ£ñäÉÁjRüéÛ·/~ðó“Ä-7ÉÉɘäâ‚ÜÜÒ5Vfžž=z4ÏÉ>íðáÃðûáÑ?(—G—.]š–†û÷BQÔ'˜ššbÛöí011ád¼ââbŒ3F 1kÖ mÛ¶‚Õc€(kkëé‚)Á !)i6XV°{¨ÎŸ?iS§ UN4är9:vìÔû{ÒñàÁ„‡…á÷߇R©$œ¢¾¬fÍš8qò$é’£R©0sÆ ºJC8v튠  Èd‚œØSËãG0~üxdee•ù³S§Nú è¿\¼p^^^ÈËË#–µkׯ«W¯èN Š)ccclܸæ p6fXX¶nÿõö:uBxD„%óÀÂÚÚú©EÅHÐ'ŠüÜÜUž U¯C‡øæ›o„*G\åÊ•1tèP+ààáá!‰‡'.¼{÷{öìÁÀ1sæLüùçŸÿü³û÷îa’‹ †‚C‡!??Ÿ`RŠú2ºPv§N† HÇ 8ЪU+„„„ˆþ ¼~ýînnj_5¹nÝ:„††kþÙ¤Ilß¾—Û8(Š¢>¤¯¯•«V¡iÓ¦œùúõkøþÏÍ+b5tØ0¡ŸóÞÉ&DÈ‚b&øÌø­¡¡ »,š4“““PåD¡¤¤¿= w77 ;;nnn¸zõª$¾(P´­@»w¸ðàÁIÜõK}™¥¥%"£¢ §§G:ʽ{÷S&Oæ¬ßĶ­[±léRb¿Ž«›˜`ý† èÞ½;‘úEi>]]„……A¡Pp6¦J¥Â¼¹sÿêC$r†††˜4i’°EY6¤¢wþÿà ææ`˜ !kNŸ15k Ò{PTž={†“'Oâðáô %)zzz¼]§‰²³³1kæL;¿\™››#2*JÐ+‘Ê«°°3fÌà¼ÑÔž={àëãC¬_®®.–-_Žé3fH¢ñ"EQÒ!—Ë„67¾[¹r%®\¹Âé˜|™9k …,ùZ©T† YPìˆìבËWHªžd®OâZ\\.^¼H:E•IûöíiŸŠRR©T˜?>RSSIG¡ÔTÏÌ k×­ãõzH®”””ÀÛË 7®_çeü_~ù |}‰-0 ƒñãÇ# 0:ººD2P¥Yd2ЩS'NÇýíìYlÞ´‰Ó1ùbgg‡þýû [”e—ÛÙÙ½¶¨¸Y°´´,bo!k~÷Ýwø¶}{!KŠÂí[·ðÇ;¤cPT™ôìÙ“tÉXµj.œ?O:¥¦Zµj!&&5jÔ å‹X–ÅâÅ‹ÇkcÇŽaΜ9(..æµÎçôêÕ k×®EµjÕˆe (Jú†Á‚ ð]œŽ›–šŠ9þ§££ß „ÞY•–ŸŸ%dA) ÖÏÚÚú œ²¦¯¯o…{«˜žžN·ÿS’R§NtîÒ…t IˆÅ¦IÇ ÔdllŒèÕ«Q·n]ÒQJ%,, G¤Ö©_ŬY³PXX(H½±¶¶ÆÎ]»8mÖEQTÅ2sæL 8Ó1‹ áí휜NÇ勳³³ð žYvn»víhçóÿAº=¾ÁºÒÕ®]ÎÎÎB•£(ªFŽUanîPÇ­[·àëë+‰UêÓªT©‚èիѰaCÒQJeË–-ضu« 5/œ?Ó§£  @кªU«6l܈ö:Ë@Q”4M:cÇã|Ü%K–àŽDvù6hÐã'Lºl‚Í.¡‹JѧlkkëxØ!dͱãÆIæA‹¢* 0€t ÑËÌÌ„§‡Š¾¥Ô§§§‡ðˆ4kÖŒt”Rùå—_N¤öåË—1Ùݹ¹¹Dê}}ŠŠŠÂô3è"%EQ¥2yÊ8OœÈù¸ÀO?ýÄù¸|`óæÏüZ[V¥ôE³”ˆá;Ø<‚}G×ÖÖ†¯/íìKQ"4løpIt?'©  Ó§OGF½ÍFÊtttV­Z‘ŽR*çÎâ… ‰î8‰‡»»;Ñí®ï›Ó¯UE}–‡‡\\\8÷Nr2–/_Îù¸|4h¾þúk¡ËµµµýUè¢RA|ÀÚÚú)„ Y³U«VèOß2R”¨T©RãxØ"§IX–Å p'9™tJ r¹˃‚Жãk ø’””o//”””Ž‚›IIp4 YYYDs8::bÛöí0oЀhТćax{{cÌØ±œ™™‰Y³fIf`u̘9Sè²%2†™+tQ)!¾ÚÚÚAž YsæŒ066²$EQŸ1nÜ8T­Z•t Q[»f bccIÇ Ô ¥¥…%K—ÂÁÁt”Ryøð!¦O›Fôüýÿºsç\&NÄ«W¯ˆæhР¶oߎÎ;ÍAQ”xÈd2Ì›7#FŽä|ìÂÂBÌœ1/^¼à|l¾xyy¡J•*B—R(·„.*%¢X°´´|Ç2Ì!kaþüùB–¤(êªW¯Ž‘£F‘Ž!j±±±X³f é”ÞßÝ­[7ÒQJ%==S&OÆÛ·â»>9%%Î& ==h„„†bêÔ©´/EUpr¹þ2t(çc¿ßxûömÎÇæ‹½½=¾ûî;¡Ë¾Ëú ]TjDóÝÊV¡ØÄ7„¬Ùµ[7ôêÕKÈ’E}Äĉ+ÜeqóæM,”È=¿ÔÇÉd2øùû£GÏž¤£”ÊÛ¬,L™<™øûsžaáÂ…ÂfY²gÄ$@4 T`o¡‹Î›?¦¦¦B—¥(êoõêÕàÁƒIÇ­´ÔTÌœ1CT[°©²aó}|лwoÒQJ¥°°3gÎÄÇIGù¢gÏžáûï¿GJJ é(ø¶}{ü¸{7Z´hA: EQÒ××GDd$ìííy?66ëÖ®åel¾Ì÷ñAu¡ËÆß¿£ÐE¥HL °U(Ne½¯±råÊX´x1½€¢™9ktttHÇ¥W™™pwwÇëׯIG¡ÊI&“aáÂ…,‘E®’’xÍž„„ÒQJíUf&\'MÂÝ»wIG©©)ÖoØ@¯3¥¨ ¢º‰ ÖoØÀ[S×›7oÂ××WR; @â¨ËÓ‡ ¢º°‰järù,o„¬ùÍ7ß`(çu(Šú<;;;8::’Ž!J¹¹¹˜6mñíÍTù1 ƒ9sçbÀÀ¤£” ˲ð÷ǹsçHG)³×¯_ÃeâD$%%‘Ž===,Z¼+V¬ ÑüŠ¢(˜™™aÓ¦MhÞ¼9/ã¿|ù³==%ÓñêÖ­‹Ù^^$Jo±¶¶>O¢°‰n eË–/X@ðî|3gÍBýúõ….KQ–L&ƒ§§'é¢TRRo//ܹs‡tªœ†ÁÜyó$µ¸¼uëVüôÓO¤c”[NN&»»ãúõ뤣»vÅŽ;ÑÜÒ’tŠ¢8fkk‹mÛ·ÃÌÌŒ—ñsss1uÊdddð2>d2—,Ð¥_ËfŽÐE¥Lt `km½ÀE!kêééÁßßZZZB–¥¨ «WïÞôÁø#T*,X€‹ýHqèý›)Mþ W¯^¨ÇÓìPÞ?4_¼pt½!ܲe FER”†èÖ­bbbxkúYTXˆY3gâÞ½{¼ŒÏgggX[[ _˜eç* 鬔ˆ€(¨d ã  XÈ¢V Æ/dIŠªôôô0mÚ4Ò1DG©TÂÇÇÇ#…*'†a0þ| 6Œt”2«Q£bbbP«V-ÒQÔRPP€Y³fáôéÓ¤£´µµ1ÛË á022"‡¢¨rzãDzåË¡£«ËK ¥R‰¹sçâêÕ«¼ŒÏ—-Z`’«+‰ÒWlll6(,eb]€B¡¸ L躓\]ѬY3¡ËRT…2aÂÔ¬Y“t QÉÈÈÀdww:ù—0™Lß à4dé(åV·n]ÄÄÄ Zµj¤£¨¥¨¨Þ^^8&¢ßOöööøq÷n´²³#…¢¨2ÒÑÕEà’%˜:u*d2~¦O,ËÂßÏgΜáe|¾èéé! 0r¹\èÒ%`YW*¡ Kh@YRò€GBÖÔÖÖF@` ôôô„,KQF½zõ0nÜ8Ò1D#%%èÛ§®\¹B:UN2™ .Ä AƒHGQ›yƒˆ^½ZòwÚ+•Jøúø`ßÞ½¤£ü£V­ZX·n&¹ºò6‰ (Š[&&&ذazöìÉk°°0Iöañðð€¹¹9‰Ò¡666Ò¹²FD±_+q#)©‡Œe_Â?|ø0.X tYŠÒx¡aaèܹ3éD¥¥¦âÌ™38zô¨(®.£Ô#“ɰø‡зo_ÒQ8uÿÞ=¸»»ãÕ«W¤£¨m’«+ÜÝÝIÇø—øøx,\¸i©©¤£Põ -[¶ÄŠÞFmÚ¸‘‘‘¼ÖàC—.]°"$„D“ûùyyÖíڵ˺°&ý$$%íË ¾§ò‡Å‹qèÐ!¡ËR”Æj×®¢W¯&CpùùùHLLÄï¿ÿޏ¸8<~$èÆ&ŠG2™ ~þþèÝ»7é(¼xüø1Ü\]ñâÅ ÒQÔ6xð`Ì÷ñÕ›÷üü|„‡…aïÞ½’ºç›¢*‚!C‡böìÙÐÑÑáµÎþýû ¹¯_}õvì܉ʕ+ ]še¦›­BqJèšB W““Mµ‹‹ï´{Naa!ÆŒƒûëÂIQb$—˱wï^˜7h@: ïÞ¾}‹„øx\¿q ññHNN†R©$‹â˜L&C`` zð¼-”´§OŸÂÍÕiii¤£¨­sçÎXºltyjàU^—/_ÆâE‹žžN: õV n&%‘ŽA DOO¾ ²°{òäIÌ›;*•´Ž±ëééaë¶m°°°¼6 ¬±µ¶v¼°‘ÄÄ'&º1€à¯ŸïµN?INþ† ‘Ú2†é§P()®A¤´€øøøúŒL–@ðeûeK—b÷îÝB—¥(àçï¯1 Ò®^½ŠŸþ§O¢“þ ¨R¥JG›¶mIG!B©T" ‡$Em¦¦¦X† ’Žòo³²°tÙ2œ8~œt” 뫯¾Bß~ý0vÌèèêâñ£GX·n.\¸€·oéËGMb``__Þ»ü¿÷Ë/¿`¯¯$w ¶hÑ7mâ½/ÂǰÀF[kkgÁ k ­Å‹“ÎPjµk×~ûâŋןI´iÛ/\ÀË—/….MQ’¦P(à=g‰±œúíìY,ðõņ pïÞ=“ŽD ¬råÊX »Ö­IG!F&“ÁÁÁ2†Áõë×IÇQË»wïpâøqضjSSSÒqþEOO]»v…EãÆˆ§‹kРìííѹK˜Ö® ¨\¥ òòò T*‘žžN¿h+++¬Ž‰ õvïÞINþ ±fíZ Ú’í½4†e˜šš(®i$µàoLBBÂQ0Œà]—ž>}Š‘#F ;;[èÒ%I2™ Û¶o‡¥¥%é(å–››‹€€?&øm¤”ˆ"zõjIÿZæÚáÇáïç'ù‰žž–-[†N¤£|Tnn.V®\‰=»wKrÒ e Ã`á¢Eèׯ\\\pCâ‹^ÔÿÓÖֆˤI˜0a´´´©¹vͬ–èMH2™ QQQø¶}{åU ÐÕÚÚú ‰âšHR;ÞËÌÌŒS±ìxzBÖ­Zµ*5j„“'NH"a°“L:F¹=þÎÎθvõ*é(AÕML°vÝ:4mÚ”txËW >é M›6…âââPP Ý—2%%%8yò$*ééÁÚÆ†tœÿÐÑÑA‡бcGܹs™t'¢ îݽ‹&M›bÝÚµ¤£PiÒ¤ V®Z…îÝ» r-¨J¥Âòe˰yófÞkñÅÕÕ "R›¢¬­­¥¹r"R’\¨Y³fvzzz*Fð™Eýúõ¡§§‡K—. ]š¢$¥Zµj Ýu[¥õðáCLrqÁS ¸úŒ*?SSS¬[·N,çÄ߀e»äç‡jëèt`N:Pí:uÐÕÑ—.]BVVé8寲,.]º„/^ }ûö‚½,‹5j`àÀ024DBB‚äw^HÅ»wïPPP€‡’ŽB©I.—ÃyâD¢fÍš‚Ô|÷î<<<$½‹°S§N˜ïãCä('ü‘——7ÄÌ̬DðâL’ `jjz3=#£ +¡kÛØØ ýùsܽ{WèÒ%sç̓B¡ £\’““1Éů^½"…"ÈÜÜëÖ¯G½zõHG€,°ìw666WÍÌÌJÞfeí/))é  .é`U Ñ»OüqçÒ$¾`öÇ >>ìí¡§'è&ÃRaVVVèÓ·/ž?ŽGtR*ˆÔÔTzüB⬬¬‰=z¶À—––7WW$%& RÍš5CdT‘¦Š _ëÖ­Ÿ(®É¤Øàׯ_7Ô’Ë@à-Hqq1Ü\]qãÆ ¡KSg†Aƒ 8³akk‹ 7J²ñßý{÷àââB»}:=FUxµjÕÂþÄòæ´Ô^¼xç ðôéSÒQ(‚úöí‹E‹‹¢¤+•Ê.­Zµ*Õê\BB‚Ã0q, š í=z„éÓ§#-5•tµ 1^^^‚n!.¯””„…†ââÅ‹¤£P” ªV­ 777 6Lð¯ãùùùðõñÁéÓ§­Ë///Œ5ŠTyVÆ0ý ÅaR*X€[·nÕ*Q*ãÔ&QçŽ&Qš¢D#(8ݺu#£L^¿~ ç ðøñcÒQ(‚F OOQô­`€t•Jåhkk›\–ÏݼyÓJ©TžÃTç+[Y½ÍÊ‚§§'®kÀýémÚ´ÁÒeËP­Z5ÒQJåâÅ‹ EJJ é(Å+¹\ŽÁƒÃÍÝFFF‚×OKMÅìÙ³5¢9ø°aÃ0wÞ<’Âl¬­=H¨4fâ“’–=ý Àßû÷ï'Qš¢ˆk×®¢WKëšÖ·oßÂÅÅ÷ïÝ#…"„aL:œIGy/ ,ÛÅÆÆæ~y>œ˜˜Ø†bTå8W¹cI` :D:ŠÚjÖ¬‰åAA°±±!¥TT*<ˆÕÑÑôVJã0 ƒnݺaê´i033#’áÌ™3X´p!rr¤¿[½}‡ˆŒŒ$¶Ó‰.ikkw²´´”îÙ1‰Ð¨ˆOL\À~$j—””ÀcÖ,œ;wŽDyŠ"FGG{÷íÃW_}E:J©åææÂÍÕ·n}òx5¥áär9-Z„>}û’Ž`Tö¯mÿj½²½qó¦L¥: @T¯ª·nÙ‚ˆˆÉ—“Ëå˜9kF‘Û"[fyyyØýãØºu+²²²HÇ¡(µµnÝ3fÎDË–-‰ÔW*•ˆŠŒÄÖ­[5¢ç†E“&Ø´iÉ#œoÀ²­lll“ P‘hÜYbbâQèA¢xaa!ÜÝÜO¢Æ/¯2‰µ IDAT¬¬,ÌñöÖˆ]8•*U‚¯/z÷îM:J™åååáÇ]»°mÛ6º@‰R›¶máT­Z58x†††¤£”Ê™3g0ÛÓScVqìÚ¢™œ1Àu•JÕS¨Ÿ¤¤¤,Ëþ¢9÷ðÞÑ#Gàçï¢ÂBÒQÔÖ°aC¢y󿤣”Kqq1N?Ž;và?þ ‡ª`tuuѧOŒ7N4M…ÏŸ?Å‹kÔ˜®®.VEGݽÉ,ËÚÛØØ\%¢Óô\¿~ÝPK.¿€Øþ–›IIpuuE~~>©ŹÀ%KЫW/Ò1JåÖ­[˜äâBVP#FŽÄìÙ³‰]môg*ééõoÚ´© +ÃW®\©®£§w,ÛNȺ¥q÷î]xÌš¥}´´´0ÑÅ“&MÓ¯¹2»“œŒ;wâø±cP*•¤ãPÌÄăœ0|øp‰d‡Vaa!"""ðã®]Ñåÿ=¹\ŽÐ°0tìØ‘h˜`mm½‰hˆ Lãàú͛ʹTªßûªréÒ%LŸ6 ÅÅŤ"Pg¾þúk¬Y» ÃŽòEi©©7n^¿~M: %0---xÏ™ƒ¡C‡’Žò¡#ùyyCÛµkGd5*%%E÷]nîV¢úI4«/X)Í›Ìòzúô)öïÛ‡ýû÷#;;›tJƒ4·´ÄÈ#УgOÈårÒqþqûömøúøàñãǤ£pJ&“aÉ’%ø®‘‹Ò>´ÊÆÚz*éY…X€ÄÄÄî,p±¯0'އ]I§$MWWûöí#Ú1¶´^½z…ïÇCZZé(”À*W®Œ!!hK¸qÔ¿0ÌNeqñ÷vvvDW‚÷îÝ«eѤI€)$s|LII ‚ƒ‚°gÏÒQ8a``€Ù^^0`é(jËÍÍÅÁƒ±ÿ~<~ÄKÏJª¨R¥ z÷éƒÁƒ“n>÷J¥ë׭úuë4îYa,\¸$šƒÎéjkwµ´´,"¤‚«0 ˜˜8•¢Hf8yò$|æÏGI mv)5:ººqFU]Ó¦MÃggÒ1¾(//.'"99™tJ`uëÖEDd$5jD:Ê? ÊÚÚzÑ|ÓMLLœÃKˆn+ÏÁ°lÙ2iÆ3bçα`áB“ŽÂ‰ÄÄDüôÓO8yârssIÇ¡$ÀÆÆƒF·îÝEÓ‹åCOž<Á__ܼy“tÎ1 ///Œ9’l–}PTTÔ¶M›6¯È¡*Ô$$%­ËN&™áÔ©S˜;g]˜®Ýºáâ… ÈËË#…˜Æc×?Šj«ÞÇ(•J̘1Ο'…Øÿµwçñ1]ÿÇßçÎDĄľ K"™%Aj)¡B­mil-ª¨¶ÔZJUíúµ«.tת5–ª}mJma’™ÉbIEì‰ ÉÌ=¿?¨Ÿ%”,sîÌ|ž‡‡ÕÜ×ãû«$÷sÏ='P«ÅüùóQªT)Ñ)÷q`†A§#º#71KÆù/4¢[e±XðÑÈ‘¸rÅ96ˆ.]¦ Ƈ-ZˆN)0wîÜÁÎ;ðÇàÈ‘#Nõ®4É¿J•*¡]»vh×¾=jÔ¨!:'W²,cÕÊ•X°`Sî$IÆ~ò Þxã Ñ)7$ÆšhµÚ8Ñ!ĪZuêüÎ…î^‰Ñ£FÑžä‹ °ç¯¿°fÍÑ)BH’„Ÿ/†V«òŸ>Ÿ>Ýi–“g׿•W0iÒ$%=]âeÐéæˆyšè¸8­d³ýÀGtË£®\¹‚‘#G"ÖbR`Z´hÇŒAùòŠ:•1ßÎ;‡ 6`ÃàÂ… ¢sˆ ^^^kÝíÚµƒ^¯Wô^A‰'N`Ê”)NùÔ¸û}Û¤I“СcGÑ)6pþª^¯ß$:„Üår0™LÞŒ±¢CÚ³gF}ô‘Ó,qtFnnn Åë;£Q£FÈÉÉÁÆ °î÷ßêÒgÑóÍ71jÔ(ÑÿiÅòå˜1c†è bG’$aРAèûÎ;JúfÓ Îûëõú_D‡<‹¸¸¸òV›m-€Æ¢[•Ï?ÿ¿¯[':¥Àh4|ðÁèÞ£‡CŸY–a2™°{×.ìÚµ /^D ™»»;š7oŽvíÚ¡q“&pssôTwîÜÁwß~‹%K–8íj\µZéŸް°0Ñ)àÀƒN'ôlò0—wÏD–9? ¬ÈŽ}ÿ#GÒ»å ¦V«Ñ¬Y3Lš<ÿüó&N˜€ÄÄDÑYvU©R%D¬^bÅŠ‰NyªýûöaÈ!N·yy2FƒiÓ§£yóæ¢S”Í{Ó Õ®ò<î°ÀÛ¢[r³jÕ*Ìš9Ó©¾a÷ó÷Çøñãáçç':¥PpÎqôèQìÚµ »wírº]Õ]™$I¨ß Úµk‡V­ZA£QÜ[D¹:pà¦O›æÔ›qwǬY³Ð¬Y3Ñ)`À"N÷¾èò0—€ÅbyIæ|'€"";8€áÆ!‹†Šöþûïcß¾}°¸Ø“øú›oи±â >ääÉ“èÓ»7mHåBªT­Šùóç+j³?™ è¬Óé¶‹É+“Å2œÏ Ýò¨èèhŒ5 ×®9ÏR’$¡Gø`Ð ÅYóëäÉ“÷W?~\tyNjµÁÁÁmÑ/¿ü2Ê–ú í¹¤¦¦bÎìÙØ¼y³è”Båááyóç+âlH:qâõððpz*£0.=³ÙÜ—?‰îˆ:tC‡Å;wD§'¨S§!˲è»êб#¦L™":ã©ÒÒÒÐë­·pþüyÑ)ÄN†„`æÌ™ðòòò 4.Ëí ÃÑ!ùm±¼"q¾€¢þ€Ë—/cô¨Q0›Í¢S T… ðñ˜1 b/^ÄÁƒu袢¢œj¨ãL4 š4i‚-Z IÓ¦(^¼¸è¤çÂ9Çúõë1Þ<¤§§‹Î)T ¾üAAA¢Sà°Íj vݳÌå`2›g-º#::Ç Ã7D§(UªÖ®] /ooÑ)O”ï¾ “É$:…ØI÷=ðÑGA¥RÔê3\–Û §9wÒl6×㜯cŠZbV«sçÎÅòeËD§¸_|#FŒ@­ÚµE§Ø çIIIˆŠŠÂ¡ƒa4]úÄѪU«†_Dhh(4h øwúŸÄd2aÎìÙˆ‹sþç½¼¼ðõ7ß ^½z¢S Ñfµ6¾*:„äŽw1³Ùü z‰9yò$D›æEøßŒhÓ¦èŒ'âœcüøñØ´q£èbîîîøôÓO•°£ñC`´Z­ƒƒƒnë󨨨ÒEÜÝWÝ’›m[·bòäÉNw³(I:½ú* „2eʈα;›Í‹Å‚ÃQQˆE||<ÒÒÒDg9­ÒeÊ ¤aC4 AHH*T¨ :)_Ο?/æÏÇÎ;]âhʲeËâ믿VÊÐð²M¥jpRty2Üc4ÝT*Õz0ÖVtË¥K—0xÐ $%%‰N!.¬yh(æÏŸ/:ã©~üá|õÕW¢3ˆT©R³çÌA:uD§*y–‘‘øË–-s™¶jÕ® (ehsC–¤–AFÑ!äéhð£ÑXLåæ¶œ7ÝróæM 6 ÑFú;DìÏÓÓkÖ®E¹råD§<Ñ®;1jÔ(—˜î»º&M›bÚ´iJ{ß ø1--í½ÐÐPçÙ–þ)Ìfó»ø €âÖgffbÒĉرc‡è”BQ®\9 þðC´oßÞéŽ Ì+Y–qúôiÄÇÇãøñãH>}ÉÉɸpá}]x@ñâÅ€zõêÝý9 ÀéV•ȲŒµkÖ`áÂ…HMMc75¬ٳ•rC68ï ×ëó“°“¡À#bbbÊ2IÚ –è–ì¬,ŒûôSìtÒohˆrûôS¼ñÆ¢3ž(!!ýÞy‡6Ítr’$¡ÿ€8p Ònz8Æêtº¢Cì-&&¦“¤•J‰nÉÍòeË0þ|§}úW·n] 1 6¢XYYYHNN¾ûãôiœNNÆ™{¿v毞žž¨^½:jÔ¨5j zðõõE•*UÀWhöïÛ‡yóæ¹ÜªÙ×;wƸq㔲Žœ¿¡×ë׋!φ¹ˆ‰‰ñ$íe@UÑ-²,cÖ¬YX±|¹èâ"6lˆEß~«Øo._¾Œ·Þ|W®\B Q‰%0uÚ4¼ôÒK¢S•Å{Û Õ®"ŠÉdªIZÎyxýÑ„Œþøcœ;{VtJ¡©_¿>Þ{ÿ}‹Nq(iii¸zå .^º„«W®àòå˸|ù2®Üû½kW¯*ö ²··7Ê–-‹òåˣ̽ŸË•+‡*Uª Fu$_Aˆ:t‹-BLLŒè»bŒaðàÁx§_?Ñ)ÿâ è§Óé~Bž žÀb±Ô‘9ÿ @yÑ-ðóÏ?ãË hY)T«""P©R%Ñ)¹ÊÊÊÂ;}û"!Ái6Z'¹ð÷÷ÇŒ™3Q¥JÑ)Jå’ôº!0pèÑ:TÂÝÝ} ë$º%7™™™˜2y2¶mÛ&:¥P5hÐï½÷‚hP`¬V+222‘‘›7nàfF233‘‘‘ÌŒŒ»¿¾÷Ï»ïÿ{½{»ü7ΨtéÒ˜8iš6m*:%76Œ¢ž_DD„Ê·víi  @yŸTܼyÿûüslÞ¼YtŠÝ”/_={öDç.]àéé):‡‡\½z+W¬ÀêÕ«iØ»CNœ4 aaa¢SrïæÈÝÿ“82<'“ÉÔŒmPBtË¿¢ÂèÑ£‘žž.:…8FƒˆÕ«Q±bEÑ)áœcäˆøóÏ?E§Ö¬Y3L˜8¥J)bÕG¥ónz½~‡èGf6›»rà'Ñ-O²mÛ6|>}ºK}íÔh4èÜ¥ zöì‰ *ˆÎ!.îøñãXúÛoغu+rrrDç(‚fÍžZµj‰Nyætº@7ÿNy,Éòv(ät8wîF ŽÄÄDÑ)ÄŒÿì3tîÜYtF®-Z„o-A »»;FŒ‰ððpE®8pÂ&I¯â ¢ãâ´’Íö;€¢[žäòå˘ðÙg8xð è»R«Õ Cï>}P·n]Ñ9Ä…dggãÏ?ÿÄÚ5k%:GQZ·nÏ&L€F£°¹)½óïthG‹%H–åí`¬´è–ݺu 'LÀŽôàŠü·fÍšá‹ Dgäj÷îÝøhäHÐç'çQ·n]LÿüsÔ¨¡Ð{AÆ6Ûrrz»Îã`;ˆŠŠ*]¤hÑ༕è–'áœcåÊ•XðŸ}û¶è»3 èÒ¥ ÂÂÂhŸRhOœÀºßÇæM›\jÕͳ(R¤FŒ‰nݺ‰Ny ftº1¢;HÁ¢@>(q%¬Zµ sæÌA¶“Ÿ{Lò®dÉ’ˆX½¥K+f~ußÉ“'ѧwodfÒ¦ëÎ@’$ôêÕ ƒVæFcÓõZígœ{x"##ÕÞÞÞ³ÁØPÑ-OsîÜ9Lž4Éew÷òòB‡йKÔ¬YStqغe Ö­[‡„„Ñ9ŠT¹reÌœ5 þþþ¢SÀñ:nªèRðhO&“IÆv(#ºåA‰'Nàã?ÆéÓ§E§š3w.Z¶l):ã1éééxë­·pîìYÑ)¤T­Z“&O†Á`’+Ü0@§Óý&ºÅÄX,=ç?@!Gêæ†sŽ5«Wcþüù.=„ Bç.]Öª­ Ï…sŽèèh¬[·;wì@=Œz¢WڶŸq㔸1'gÀHÚ×yÑ DGGû©TªÝPÔŽ:·o߯Ì3è¨@òW_}'MñY–1xÐ 8p@t É'I’е[7 :E‹ó$ç¸,¿n0Žˆq%f³ÙÀu|D·<ÍÅ‹1eòdìß¿_tŠP´*€< Y–a±X°sÇìÚµ /^¤hcÇŽEûD§äÆÆîÆB  HLLŒ?“¤mªˆnyÔ®;1iÒ$ܼySt ¬råÊX¹j•ò6˜ðÍ7ßàûï¾Aò©R¥J˜0q"6l(:åÉû‹ÛláƒáŠèWWÞ*Ë+ÁysÑ-ÿeÇŽ˜6u*½³  fÍškÝíÚµCµjÕDçÁdY†ÙlÆŽ;°kçN\¾|Yt’C¨W¯>ÿßÿPµjUÑ)aÀ™±n­öÑ-¤pÑ Æj*µz;€:¢[uîìYŒ3ñññ¢Sˆ ’$áû~@PPè”Çìûûo 2²L¯`;*ÆÂÃÃ1tØ0+¦ØÞç m6ÛÐàà`:sJ ÈÈHµw©RÓÁùGy$Ä¿®\¹‚iS§â¯¿þ¢Œ1 ¬uk´nÝåË—DìD–eDGGßÒõêUÑIC­V£ÿþè?`T*•èœÜÜ`Àk:ŽÎ^v4(` ¥²­Ö༑è–GåääàË/¿ÄoK–Ðîê.¨oß¾2Ty{p¥¤¤ gô„ÍU­ZŸŽ¯ì§þ€•36Ò Õ*óè e±X:Êœÿ À[tËÙ¶u+fÏžM7=$ :­[·FXXJ—QÔvH¤\»zÀþýûqðàA¤¥¥‰Nr8¾¾¾˜Œýû÷ãÈ‘#¸}û¶è,‡çëë‹ñŸ}­V+:å‰8°W-I¯Ò²C€ÂÅbÌæÏð±è'Ù¿?¦LžL;¶:±Ùsæàå—_ñ˜6`üøñ¢3ÈsÒh44hºuï®ô§{œ3ÓÓÒ> µŠŽ!Ï&ÆbyŸq>€¢—”woœV®X¯¿þÚ¥ |V¾¾¾hÔ¸1B6D V‹%JˆNrI°X,w˜Í°X,ôßorwwǻヒÞ}ú@­V‹Îy2Æ"®—(Ñ;´zõ;¢SˆýÑÀÌfó;X@Yë¯ïÉÌÌÄüyó°fÍZ àdºwïŽÇŒñ˜Ä'лwoܹC_wIhh(>3*(êÄÓÜÜàŒ½cÐj׈!ÏÏb±Éœ¯à+ºåY\¹r³fÎÄŽ;D§8 ƪU«†€€ ^@êÔ©£äeÒI–eüsæ ,±±0›L°X,8uê½¾RH5j„O>ùU¸Ãÿƒ°@§Ó @ÿ!¸(؉ÙlnÍŠyG:„I“&!%%Et )uëÖÅ/¿þª¸o¨222ðfÏžøçŸD§gäããƒÑ£G£q“&¢SžEŒM¥ 8):„äÝñãÇ‹ßÎÊZÎ{ŠnyVû÷íìY³œœ,:Å!¹¹¹¡N:÷‡¾µjÁÇÇE‹æRSS‘”˜ˆÄ¤$$%%!ñÄ œ:uŠ–óÛAéÒ¥1ò£Ð¶m[Ñ)ÿ%œÔëõ‹E‡±h`G÷žj¬PEt˓ܺu _ÌŸˆˆZ àÀ¼¼¼°ä·ßwÎ,ç‰Ý»w‹N!ÏÀÃà À[½z)nÉ\q¾ðº·÷ZÒèøàÑÉÌÌD÷îÝqîìYÑ)ä Ê–-‹Aƒ£cÇŽñM ΀/ÝÜÜFùûûg‹Ž!öÛ–Éò*Šny»vîļyópþüyÑ)„<(R¤zöì‰þ@£ÑˆÎyœ3 :Ý' ÍþÈ#h XLLLs&IÊŠnù/‡ÆŒÿý'OÒÞZJÔ§O >\tF®>ýôSlÚ¸QtÉ…‡‡úôéƒÞ}úÀÃÃ!^³€Ë2ò#xl IDATc}‚´Ú­¢CˆýÅÅÅ•·Úl?è ºåydgeaÅŠøé§ŸžNãq/·j…aÆ¡JÅnáõ¨œ±^­öÑ!D™h ‹¥Š ¬çŠ_skµZ±|ùr|»h« ½zõˆ‘#Egäjë–-;v¬è òI’ÐéÕW1hÐ ”)SFtÎóØÁeùmƒÁ@Ç•¸¸‹¥7ãükž¢[žGFF/^ŒeK—Ò’oB¬nݺøhÔ(Eî©ô'¸,¿n0D‡å¢€BD&'-™žþz‰nyW¯^Åüùó±yÓ&:-@°¶mÛbÚôé`Œ‰NyLJJ ºu튌Œ Ñ)äÍCC1hÐ ÔªUKtÊ3cÀu:Ý,ÐrFrÉdªà0ÖLtËóºvõ*¾ûî;¬]»V«Ut!äžJ•*aà{ï¡C‡ŽòJÜ¿ÖówôzýuÑ!DÙh ,Ìl6æÀtñ'&&óæÍC¬Å":Å%Õ¨QËW¬ÈuS"Ñl6ú÷ë“É$:…Ü‚Aƒ#0Ða6SÿWœ¬R½@ŸhÈc"""Tµk×þˆ“¡ð=ursîìY|óÍ7ضm@ˆ@åÊ•CÿþýñÚë¯;ÆÑ·ÿ/ Œ}¬×j¿B (&6¶-³Ù–€±Ò¢[žÕŸþ‰¯¾ü§NâR~^¼z½^tF®-Z„o-Aèõz <Ø‘vöÿ—ÌYînnŸÑFä¿X,– ™ó%üE·äÅñãÇñõW_áï¿ÿ¦•u„ØQÉ’%ñN¿~Wä•§âü¤$I]µZm´èâ8h PqqqU­6Û E·<+Y–±aÃ,Z¸/^ãô4h€ï¾ÿ^tF®L&ú÷ëGGþ æçï>øM›6’§¹,÷5 ‰!Ž#29¹¨WzúDŒ Ý“GÅ÷ß}‡ÈÈHRˆJ”(Þ½{£GÏž(V¬˜èœçƵ²ÕúñGž ,22Ríííý)y%¸»ÓñÊU«ðÓ?âúuz ©°Lž2;vñ˜ÌÌLt GJ íÑ&ŠN§CÿõÆŸøÞ£hÑêÔ©sSt qL111:HÒO Ý’W‰‰‰øñ‡°cÇz5€¤ÑhгgOôêÝÅ‹“·ÁØX½V»w¿fò\hà,KG™óÅJ‰ny™™™øeñb,]º·nÝãT$I®ݻáíí-:å1Ó§OGĪU¢3\RÆ ÑÀ4hÐ@tJ^]`À»:ŽÎŒ$ù©.Y²äHLà`ëzÿ_òéÓøáDZuËZUEH>”.Soöì‰ð®]áééP‡‡<(N%I=cE‡ÇEWÕ*ËKÀysÑ-ÏëÚµkøáûï±víZdgÓk¼¡fÍšX³v­èŒÇ˜L&ô{çzZeGŒ14iÒý €N§“wŒE¨˜&:…8‹Å sþ#€†¢[òãÜÙ³øñ§Ÿ°yÓ&úZJÈs¨Rµ*úôîN:¡ˆ£½ãÿÿ8¾Ôh4£}}}³DÇÇFro§ã±˜|·155«V­ÂÒß~£cáò©}‡˜:uªèŒ‡äää {·n´¤¸¹¹!44½ûôA@@€èœü8ÎßÓëõ›D‡ç¡ªU«Ö06 €‡èžüHMMÅúõë±|Ù2\¹rEt!ŠU»vmôêÕ mÛµƒJ¥“çÁy_½^¿Ctq4p@&“©[ÀWtK^¤_¿Žå+V`ÅòåHO§}Kòbè°axûí·EgÒjµô——8³Ù\Àçè%º¥ Ý¾}Û¶nźuë`±ÐBâÜÝÝѲeKtìÔ !!!Îð*ÜÃ8·È’40H«=(:…¸8)gØàA²,cïÞ½X¾l¢¢¢\z“£Vaa˜5k–è ÀÞ½{1äÃEg8œ"EŠ yóæhß¡š6mêèÇå&^bì}Úä8*‹ÅÒQæ|>€š¢[ ÃÅ‹±eËüñÇH¦.Q ?thßíÛ·‡—··èœÂ Æ&^OMjC\ œ˜ÑhôR«Õs8ðœ`5À¿þÝähó¦M¸uë–è»ëݧ†.:0pà@D:$:Ã!H’½Á€¶mÛ¢M›6(^Ü©Vÿë›bËÉù"88˜ÞÝ!-))É=33s> ŒMW ÁÑ„lÞ¼Û¶mÕ+WDçV©R%tèØ;t@•ªÿ6ëÓlâ²<È`0œB\ \€ÙlnÃoàdO2222°eóflذ±±±¢sìfÔ¨Qèù曢3…ï¾+:CñкM´nÝåË—SXd,vS©>  3=‰S‰‹‹+oµZ§‚±¾œn¹Î¿dYƑDZyófDFF"==]tq•*UÂË­Z¡U«V t¦ ýr“·éõúÑ!ĵÑÀE$$$ɶZßçS8É9)ÿ/ùôiü±a6oÚ„K—œûþcöìÙx¹U+!×NLLÄ®]»°}Û6zïÿ)jÕª…6mÚ M›6ÎþsYj0ˆ!¤0EGGû©Tª¹xEtKa³Ùl0ˆŒŒDäŸâÂ… ¢“ˆ©Zµ*Zµj…VaaðWБƅè6+ZtZ:unŠŽ!„.Æb±TáœOwÆ Ž€»O0Ìf36m܈-[¶8å+¿.Y‚ÀÀ@»\+-- GŽÁ¡ƒ±oß>\¼xÑ.×u4’$¡NݺhÖ¬Ú´iƒ5jˆN²‡ÎØXƒV»}!!.Ãb±t”ey{At‹½œš4nŒ&M›ÂÇÇGt’½çÃõzý>Ñ!„<Š.Ìh4º©ÜÜ>pÖ×tîìYlظ›7m¹sçDçä™$Iˆ:|¸@wONNÆýû±oÿ~Á;w ìc;›*U«¢IãÆhÔ¸1BBBP´hQÑIödðSŽ›Û„þþ´„ ¥²rrF3`Ñ=ö–““‹Å‚CâàÁƒˆ‡,Ë¢³ˆ ÕªUC“¦MÑ´IׯwwwÑI"¤0`’N§ûýe ŠDâÔçç&>>Û·oÇŽíÛî½ÆråÊaÛöíùú²,#>.»wïÆ®]»pöìÙªs>¨_¿>7i‚&Mš ªó¿ÏŸ;Ævªè:»mòbbbÊB’F2`—¼ë€7nàpT<ˆ#F#Î$'Ó*2'V¢D  4jÜM7v…=ož†Þó'ƒä>³ÙÜš ÔÝbœsÄÅÅaÇöíØ±c‡C¼ß€%¿ýöÜÿž,Ë0ÅÄ`Û¶møóÏ?騧'(R¤P¿AÔ¯_:EŠ%Ò>‰±±Z­v¯èBAtt´¯¤VOçÝH¢{D»~ý:Ìf3Ì&L&â•%:‹äQ™2e ½Á€à  ¼àë IrùÿÌsÀØbµ$M  '*Ä!Ѐ<Äh4º©Õ꾘 ¢è{áœÃb±`ûöíØ¹c._¾,:)W/¿ü2fÏ™óÌþøñãØrï|gGpØ›››Ûc7ü.ºdñ!ˆeœO¡£ŠÉ³Ù\36œ¿Àev;û/9998zôèý@BB}mR°ªU«"((AÁÁ0 ®» .w2[Y§×ëEÇò}ÉÉÉ8wîœÝ¿+]±bÅàS½:ªûø FÍš¨^½:ªW¯WßÛ&/öóOõz}¤èB  Ès1›Íõ8çÓÁX'Ñ-¢Y­VDØ»w/:„“'Oúê€O>ùá]»"99k×®Åë×#==½P¯©$*• U«Uƒ¯¯/|_x/¼ð|kÕBµjÕh#¢çcâŒM5hµë@Oü ±;‹Å Ëò'`¬+hP nܸ .àâ… HIIÁ… páâE\¼p.\Àµk×D'(777”/_åÊ—GÅŠQ±B”/_UªVEõêÕQ¡BÑ‰ŽŽƒó­œóƒá/Ñ1„$<¹÷^ãÿ4Ý¢7oÞ„ÉdB¬Å‚ΞÅùsçp"1±@w< Á7pôèÑû˜JT´hQT«V Õ||àãペ5kâ…^@5èÉEþ’›¦Õj7‚Þñ'D8“ÉT Œð7Ñ=Î,;;©©©¸zõ*RSS‘––†«W® 5-íîï_¹‚ÔÔT¤§§ãÖ­[¸}û¶ÝÝÝÝáíí^^ðöò‚—·7¼½½áåå…’%K¢|¹r(_¡*V¬ˆÒ¥Kƒ1Ú_²XÁyĽ³èB  H¾X,––20œ·Ý¢DIIIèÖµ+mf” FƒJ•*¡R¥JwoöÿýáãƒòåËÓ76‡ØÄey6=Å D™âââªZm¶Á()º‡ÜÝèæÍ›ÈÌÌÄ­[·îÿ¸qãnݺ«õá·¦l6[®¯!¸)÷¢Eïÿ\ÄÍ E=<àææ¢E¡vsC‰%àííM›ÒŠuŒý,³µZíiÑ1„&a2™ôŒ±ô^ããš½ônÞ¼):ÃîŠ-ŠJ•*¡råÊýüïÚ]¸Ðe3`%€´S1!Ž!29¹¨×]çðÝCˆ ¸É€ŸeYža0RDÇb4 Êl6×ãÀݨE÷ˆÆ9ÇKM›:åæDnnnwoê¹±ÿ÷f¿T©R¢]U*8ÿÖf³}|At !$O$‹ÅÒÞÆù0´Cˆ:ƾòpwÿ¶N:®÷”†¸4Ba2™ª3Ɔó»Ë=D÷ˆrîìYtìØQtF¾+V Ðjµð­U ¾¾¾¨V­Ôj—Ÿñ(Éq0¶P~ÐjµÎ7q"ÄEÅÄÄè˜$} \øë)!d8ÿâúõëëèôâªh@ •Ñh¬¨R©F€±÷xŠî±·­[¶`ìØ±¢3òÄÏÏ¡¡¡hÒ´)êÖ­KGê)“ëç_ÒñD„87‹ÅRŽsÞ—ýøŠî!Ä\c‹ç u:Ý Ñ1„ˆFb ¥rrr>àÀ*Šî±—ñãÇcㆠ¢3žY… ðÚk¯¡Ó«¯¢bE—ù“#ºÄÅå…ƒáŒèBˆ]±‹¥%8À€×ÐÑ(„äÎÌ€ocKie!ÿÄ®Šdgg¿Ɔh º§0åääàå–-b@ƒÁ€·ûöEÓ¦M!I’è’;ÀNÎØ÷îjõþþþÙ¢ƒ!bÅÄÄ”e*Up>@mÑ=„ˆÆ€;œóuŒ±ot:Ýߢ{Q"abbbA’†2 3œðüã½{÷bȇŠÎx*½^·ûöEóæÍE§'»Î–$é:šˆòÌd25‡$ 篃ö .†eàµ$­ LÝCˆ’Ñ€g4+ªÕê~œ±༚螂¢ÔåÿŒ14oÞý @½zõDçÜeó ’$ýšššº…6*"„<«C‡•pwwŒ½‰»'Ð.ÄYçKl*Õ¯ÁÇDÇâ(h@”DЉ‰iÉTªwï=ÁpØ-æ333Ñêå—qçÎÑ)÷I’„°Ö­Ñ¿øúÒþQJÄ#gl ·Ù– †+¢{!Ž-**ª´»»{ôÐÝDH~0àglƒ,¡9!yC¢H&“©:€·ÁX/5ç<·Õ«WcÚÔ©¢3î ÁÐaÃàçç':…<Šó“¤°D«ÕCqNf³¹6ç¼ç½•4&ŽÄ `7glUöíÛ!!!7DâÈh@”ŽY,–¦²,÷cá¼D=‹^o½…¸¸8ÑÐjµøpÈÔ¯__t ycÿ0Î#dY^a0ŽˆÎ!„¸–èØØ`&˯1à5¢{ÉE6ÛÉ8_íææ¶Þßß?Ut!΂Äa8pÀ£¨Fó*»; h …¾×˜””„ð7ÞÚàçç‡Áƒ£q“&B;ÈCR°šs¾J¯×ï@Ÿ| !Âãâ^PÉòëàü5ÐQ0Dˆ{Ëû·qÎ×0Î7èõú뢛qF4 Éh4V”Ôêž x€Atσ¦M›†ÕB®]½zuôë×íÚ·§ãü”á8~W1¶N«ÕFnú ! WÞjµv‚$½Æ8oÉ¢¢›ˆÓËç[¹$­)æî¾©N:Ê?;™GâðÌfsmèÆ€n„nkóæM´iÝ·oß¶ëuK•*…AƒãÕW_…J¥È…®‚8ÆÖÉVëïAAAGEBH^$$$xfgg7gwWܵæ@]ÑMÄ9p –1¶U¶ªÕê¿ýýý³E7âJh@œŠÙl®Ç9cÝÔ±÷õ—üú+æÎk·ë©Õj„wíŠ÷ßÅ‹·ÛuÉC2D2`€M:î¼è B)h‡*¸eg¿ÆZè ¢è&â02ÁØm99낃ƒÿDˆ+£qZ‹%H–å`ì#{\O–e¼Ú©Î;gË!$$cÆŒAõ5ìr=rŸÌã|+€Í‰‰‰ÃÃÃm¢£!ÄŽ$“É I »÷ªÀ‹4¢£ˆbØÀX ã|§,Ë[ÓÓÓ÷Ñq}„( ˆS‹±X:1Î×ÛãZ‘‘‘>lX¡_ÇËË ‚Î;ƒ1:ÒÙNNØÉÛ›m§Á`HD!J¡ª]»v]M4å@3>‚³ˆýX`–*Æö1Æþ LEÉ ˆS3›Í;9ð²=®5pà@D:T¨× ÃØO>AÉ’% õ:gìeÀn«Õº‹–+BÈó1ÆÅ½ Ér4çMpwÚÖ9ÜdÀ>ÎØ^ÆùFsØ××7Kt!äÙЀ8­ØØØ@›,[ìq­S§Ná.]PXŸÊ–-‹‰“&¡qãÆ…òñ]œÌxÆØ^Èò>I’öhµZû¼ÇA!.âøñãų²²ô6ÀÀ€ Îyü¨E·‘§Êc±à<šF‡Oœ8a¡Wßq\ôI—8-«,³×ùåË–ÚÍ«°0|:n¼¼½ åã» TDqΈR©Th©"!„®{Ç»í½÷™œ\´dzºV‚`cAà¼.Oa¡.Œw8`çFHR4ãÜhµZゃƒsüs:NT"!¤Ð â”bbbÊJ’ô=Î0.¬£ÿ4 †.]ºèÇu1™`ÌÂ8?Ì9¥×ëEGBy"SM’¤:œ1?ι»{ª?€r¢ãœD6€DãœçŒƒ,[ÒÓÓãi³>Bœ­ ÎI’Þ³ÇÍ?¬[»¶Àoþu:¦N›†*UªèÇur—˜8ÆL2c¦SÇŽ%Ò2EBq(Ü`0œÁݽX¶?øJegg×eŒÕP@uùÞÏ ¨@eïX…» à88? I:Î8?f³ÙŽÝ¸q#™nô q]´€8„„„"Ù99ɰÃÅ}ô_ww¼÷Þ{èÓ§$‰öJz‚ëÀX<€xn³Å˲|At!„1ŒF£›››[Y–}po(IªÎ9¯À8¯È+Dzpž!ÁmœáŒçgÁù?Œ±œ³Ùlg9çg‚ƒƒo‰Ž$„(­ N'+'§³ÃÍ?üõ×_vóïëë‹©Ó¦¡N:òñ\&8? ÆN1à$çüc,‰1–@ôByÔ½÷ÔOßûñ$R\\\YÎy9›ÍV1V@9ÎyEÎX)ÆXq%çžüî>Å”à…Â=Á ·9ÊkŒ±Tpž ίA’R!ËW¹$¥2Y¾&«Õ©<++%88øj!öBœ ˆÓ‘€¡öZײbÅŠ| ÆÞ|óM :nnnPå0.8ÎOB’N1ÎOrÎO©Õê“—DÇBq:ò½¯/—Ä>Ï¿h4‹¹¹¹y2Æ|¸Ò—ü_çŒ}`Ðé–‹!„B!„ˆGâ°Ü­ÖJOÞ’¯à¬zàééÒ¥1yòd4nÒÄWÎÆv2ÎßÖkµçE§B!„B”ÄaqÎ+ö5®^½Š¿"#-Z´À„ àåí]Ø—Í3Ü0Q§ÕÎ`ù!„B!ÄAЀ8,ÎXðÂÝÏnÝÚµðòöÆðáÃѾ}ûB½V0xS§ÓÅ‹!„B!„(ã…|EHa1›Í9°¨0¯qñâE”*YEÜÝ ó2ùÅð¥F£íëë›%:†B!„¢L´€8,Îy+ÔkT¨P¡P?~¸ 1ö–V«Ý-:„B!„¢l’èBò!St€`‘6«5˜nþ !„B!ςđ] ˆ ŒML!„B!„ Wˆ“Óëõ¦ì¬¬VÙûÚ Xb³ZCèæŸB!„¢´€¸ ³ÙÜ™³QØ«8¿ÆnÐé–êu!„B!ä9Ѐ¸”¤¤$÷›·n dœàSÀþæ1Æ~Öjµ™ü± !„B!$_h@\Rdd¤ºdÉ’/Ë@W¼ T> cFeÆ6F ¿P„B!„E¢qyF£ÑM­V‡È€AüÀy0V€7€4ܽ±¿ÎÏ@’Ž3ÎV«uWppð¡ñ„B!„òŒþMh7¼›Þá`IEND®B`‚chango-0.6.0/logo/chango_dark_mode_512.png000066400000000000000000001426141507376567100203060ustar00rootroot00000000000000‰PNG  IHDR‘ª pHYs ® ®c¾‰tEXtSoftwarewww.inkscape.org›î< IDATxœìÝg@TÇÚðÿœ]X@ `£ QvQÑXcר@1vÞØ»Q{4Fcì½%ö;*`]Ø]c‹Üè@){æý`y-”ݳgÌïKvžó\¯rž3gæB)Ã0 Ã0¶!..Îçù6”Òf ¤€¸ÈG5¡ôÏ”””cÍ›7ÏÍ/aÃ0 ÃX7­V[‘ºçƒ@H#=†= „,H}òdM^…+†aÆ ]½zµR.ÏÒ` pCEó:]Ÿ:uêÜ~û‹¬`†a+¡ºzµš$77„tP)t GHK¹\óú ¬`†a Òh4µt¤@'ŸA¼›þû’ ÐX¡PÜXÀ0 Ã0æFÔju]BH‚x›í€êæÍ›ÁÁÁ:V0 Ã0Œéqæ3èH€nª[*JÈ?¹|-+†aÆâããí_äæ¶à€ PÚ@9KçôJrÉ%*±€a†aD¢R©œ¤RiÛWSûñr¾õ¡4Pjé†aÆ–]Žw·ËÍížï$‘J[QÀÉÒ9é¡ +†aÆ@qqqUu”v¥Áv¯÷èS-Þ7Bj²€a†a ±wï^‰··wÃW‹øaÆ•û&âÉÖ0 Ã0LT*UÎήáùN ¤-gKçô¾ÌÌLĨTpttD½Çà›`†a˜WÞLí%Ri3PjgmSû©))ˆˆˆ@ø‰¸œœlÜ´É HeÃ0 SlEFFJÝÜÜš¼šÚï îÏ/Hjj*Μ>ððp\¸p999o¾çêê ???ƒâà>+†a˜bE­V»RŽkG€N®nní)àf]Ïø/%''#""'Ož„êÊètºˆÒ]þþþ+†a+_*+7·#úÒ°Ê›þëE|§OŸ†J¥Bnn®É¯éîîŽÆ<Ž—H6¿þ5+†a«ñj~çWÓûm `uÛõxžÇ¿þBÔéÓ?~ÿý·ÙsêÖ gX=D¸:¾¾ª×¿gÃ0 cQ.\p,Q¢D+Ji0éF(µºãt3333§O#22Rô÷ù†H$è*dúØøöoXÀ0 ØÝíÛ·em(¥ÁŽNN]yJ­î ÿý.\À騨ÚïZR‹-P¶lYC‡e󹹿¼ýV0 Ã0æÂiµÚæ<¥}ñòH]WK'ô¶·§öOŸ>¿®_7¨)¹t6x Âüýý½ý5V0 Ã0&¥Õj+RJûP`€ª–ÎçmYYY¸xñ"μºé'''[:¥UªT õêÕ3xÏóëÞÿ+†aÑE&$8¸¦¤tÇ ¥-XMžäädDEE!22—/_FvV–¥SÒ[/¿ÒùﺟŸßé÷¿Ì †aF41qqþžïï ô!¥¬¥AÏßÿȈDFFâêÕ«V9µ_tîÜÙàq”ÒµÈãV0 Ã0F‰­@$’`Jé`ðµ†[ëÛïóí·o-:vê„>úÈÐaRŽÛ‘×7XÀ0 Ãìõ*~èG8.”J-=ÇŸ•…XµQQQ?~=*| éÑ£‡áƒ(Ýåëëû4¯o±€a†Ñ[ÌÕ«r¢Ó}M€ÞJY:ŸÔÔTDGG¿|§aÒÖ»–ôÙgŸÁËËðà y‰äƒÅ¯±€a†)ÐíÛ·eÏŸ?ï ŽÂQÚÊÒù¤¥¥áÔÉ“8rô(®\¾ žç-’ÉõìÕKȰè·;ÿ½ Ã0LžTW¯V“ðüPPú)cÉ}¯·ë:x‘‘‘VÓ”Ç>ùä4jÔÈàq”Õ}Ÿ Ã0ÌÛ¸ØØØD""¡4€ÄR‰dggãÂ… GÄ©SÈÈȰT*Õ»OƒûþHJýè£=}€ Ã0 .ÇÇ»Ûçä  ÀpÂq•-õ´¯ÓépéâE=zxþü¹Eò°...èÔ©“á YÓÜÓóEAaÃ0L1¦ÑhSBÆØQÚ•Zèä=žç¡ÑhŽãÇŽYô kÓ«W/89|6RVŽTº¾°±€a¦˜Q©T.R©ôK ŒPËROûwïÞÅàðáÃHJJ²HÖÌÉÉIØâ?Bv×óñI,ìc¬`†)&bccëŽ)yyów´Déééˆ8u Dtt´Mvä3—îÁÁpqq1x¬Ðçs¬`†)Ââããí³³³»€ã†޳Ⱦ×Sü‡ÄáÇ‘™™i‰4lн½=úöíkø@JOËŠ}>Ê †a˜"H£Ñ|ÌÃð5)o‰iþÄÄD9raûöáþýûf¿¾-ëÜ¥ Ê–-kð8BÈ2}?Ë †a˜"äÕa|h®Ë2ºt邊+>Òeþþþ‚¦fXÀ0 c%^ßø)0fºñó<óçÏcïž=8{ö,x^ï]dŒHìííñŸ¯¿24U"‘,z]V0 ÃX˜Z­v¥„Œ&À8˜éÆŸžžŽ£Gb玸˶ðYT`` ÜÝÝ Gž¾¾O…^— Ã0™ààš–6„L&€á§¾póæMüº{7>Œ/ <.ž1'''¡OÿO²_¼üô°€aÆ"4M[WJWƒÃ7} »V,_Ž˜½Î‰a̤ÿþ(S¦Œá ù1 ͘k¶º“aÆ|âããKeçä|@Ð’o!xžG·nÝX·>+S¶lYøã8:|2ó#G‡ª5jÔxfÌõÙ Ã0ŒyX­¶¡t1|Â=z”Ýü­Ðˆ#„ÜüJ0öæ°€aÆä47Ö ¹¹¯Íó<~þÉà&qŒ‰U©R:w2ôÇqkÄÈ Ã0&¢R©ì¤Ré³8X"‡ãÇŽáöíÛ–¸´M+W®ê „“~ýõWÑãOœ8‰Äàq˜-—ËE9n‘ Ã0& V«I¤Òõ¨e©xžÇÆ-uy›b/“Á¿N|Ö¨5j„*UªàØÑ£Xôã¢_+ 72ôÆÓ§OE›ÎaÃ0Œˆ^íé_@€X2—ððpüý÷ß–LÁªUªT 5B£ÆQ·n]88¼œ¤IHHÀСC}é’è×ä8ã'L6˜ÒæÍ›çŠ• +†aD£Õ¶ãÙH=]ÅÅó<6oÚdé4¬ŠL&ƒR©D@ƒh€š>>ï|?%%Û¶nÅÎ;‘m’ºwïŽ5j<ŽJå1saÃ0Œ‘T*•‹D*ý3ã־œ8q·nݲtW¹re4nÜ7†:°—É>øLzz:¶nÝŠ;v ##Ãd¹¸ººbÄÈ‘‚ÆRJ¿ ê¾}V0 Ã!F«m'‘J7ÂOý (]º4œ Çó<6nØ v:6ÁÁÁõêÕC£ÆÑ¨Q£ØÉÎÎÆž={°yÓ&¤¤¤˜<·Q£GÃÅÅð†óS*ω+†aP«Õ® d) ;vrr2Ö¬Yƒ?À©ˆƒÇŸœ1zÌAc)!¡~ …É'°€a¦‘‘‘RWW×é„ã¦0¼u[T*æÍ‹„„„<¿ïmàv±S'O‰§ÿªU«¢u›6h×®<==ÅÐh4øiófœ>}ZÜä 4~üx”*UÊàqˆñ“Ë6AJo°€a&Z­¶Š«›ÛŸ‰7%%‹/Æ¡ƒQЉ¬Õ«W×;&¥lxåùòåÑ¡C|ѱ#ªU~Brll,Ö¯[‡Kzâ[½zõÐUØÂ?*!d^ä”ÞÁ †a˜<Äjµý ¥«”3nxx8¾Ÿ?OŸ>-ðsvvvøä“OôŽ{êÔ)Ü´±§{™ Íš6Å;¢qãÆ‚z㿦Õj±yÓ&‹?ñ¿fooÐÐP½'¾;ärù¤õV0 üE¥R•‘H¥ ÐU̸<À·sæàâÅ‹z}¾råÊzß)¥6³ïŸ¿:uЩS'´nÝ%J”0*žZ­Æúuëôþs5—aÇóJ!CŸñ<"v>yaÃ0Ì+±ZmK‰TºÀÇbÅ|kŸ¾ ™ŒŒ4ë¶6!œÑ¦MôìÕË Wù±¶'þ·y{{£_¿~‚ÆRB¦ûùùý+rJybÃ0Å^||¼}NNÎ|L€ˆÛûHJJÂömÛ°oß>dffŠÓ’Z´h!´×?@È.…\&nF†aÃ0ER¬FÓÖe?ÚÇ1cút\¾|YŒpù²³³+ð‰òä‰&=ñ¯zõê2t(Z¶l)tZû÷ïÝÖ-[ðçŸ";;[”˜–V¡BÌš=[èðdªÓ1AXÀ0L‘)uqs›G€)bÅúHÐx SúùY|Ñ+†)2.ÇÇ»»º¹ý‘¶øét:¬^µ [¶l}¡_~ ÚŽÛ·o‹z=wwwüçë¯(Úÿ¯¿þÂŽ;päðað¼IO´µˆ1cÇÂ××Wèð …¢Sÿ¯±€a˜"A«ÕÖ±öå}bb"&OžŒ8­VŒpzËo Ïóؼi“h×qqqÁ€ѧwoaÍkò‹-?ÿlµ}úÅдiSôéÓGÐX Ü“ÙÙ9%ÁXÀ0ŒÍÓh4})°"½ïˆˆÀ¬™3ñìÙ31Â$¿àø±c¸uë–Ññe2z÷郯¾ú ÎÎÎFÇ£”"** ?mÞŒ¸¸8£ãY3Ì7Oè¢HžPÚßÇÇç‰Øy Å †alVdd¤ÔÕÍm€‘bÄËÉÉÁòe˰k×.³Mù¿/¯3äyžÇú ŒŠËq:wé‚áÇ£\¹rFÅ^¾9vì~þé'Ñ_KX#;;;,üáÁïýAé\¥R)jRFbÃ06)>>¾”››Û ´#^bb"B¦LF£#œ Çá“|XŒÔÌÆÃÃ<ÀÞ½{¼l ŒQ£G£dIaË ²³²°sçNlÞ¼ù¢‚ù%K–ĪU«Œé›@ 0¸NíÚæ=6R¬`Æ¢âââÜryþšëÑ£G˜0~¼MžJW®\9,^¼ÙÙÙðòöÆŒéÓá+— ŠE)ű£G±bÅ /^¼#5‹øøã±~ÃTÊã4@}ܿ˗/ÇÉ'ØÊþBH$,X¸Ð¨›?T¹99a£7€Í0 c1ZmŽÒ?”#^ûv혘(F(³+Q¢FŒž½z½ÙhˆgÏžáçŸ~ÂÎ]»•e‚ ‹BfΚ…®]»ænŽÝgõ||ló/Ý+l€a³R«Õ]8B~à(F¼[·nÙìÍ¿iÓ¦6MÐ4N‡½{÷bÝÚµ?ÆØVB:mšq7Js×ÞÖoþ+†1#F3„¬‡ˆ?{.XÑi~úruuÅä)Sо}{AãcT*,X¸·Šña=†"„`â7ß {÷îÆ„ÉÐY.—ß)-‹bÃ0f¡Ñh¦P`ØqÏÙXкukL …›››Ác=z„eË–áð¡Cì=¿!˜4izõîmLú*”JÛú WV0 cR{÷î•xy{¯0DìØ™™™PÛÈ6¥Ë”ÁŒéÓѬysƒÇæää`çÎØ¸aƒUžbhÍ8ŽÃŒ3Ð50Ш8§P(ÂDJË*°€a“Q©TNÕ½½wèdŠøÑÑÑ6ÑÙ®uëÖ6 ®®®ŽŽÆ âÎ;&Ȭhã8³çÌA§NÆýõ£À<¥B±J¤´¬+†1‰èèèÒö‚R㎬+€µ¿ÿ/UªBCCµñ}øð!V®\‰Cš ³¢ã8|;w®1Çú°B©PÌ)-«Â †aD§V«=e2ÙJ駦¼Ž5¿ÿoÕº5B¼ëÏÍÍÅÖ-[°qãFd±m}‚ØËdX¸p!š xÝòB¶*äòñ¢$e…XÀ0Œ¨bcc}Ç…S ‚)¯óÏ?ÿà¾ñëììŒ±ãÆ¡[·nÕh4˜7w.nß¾m‚ÌŠggg,]¶ þþþÆ¢ô—[7oVÊå¶qš”¬`F4Z­¶á¸„ŸY«§óVøô߬ys̘>¥ËÖßèùóçX±böýö›Íœ^h^ŸêW½zuãr@—›;À–Žö‚ ÈB÷©”Òîæ¸ÞùsçÌq½888`ôèÑèݧÁc£¢¢ðýüù6{†µ¨Zµ*V¯YwwãþúQà˜³“Ó—Õ«WÏ)5«Å †aŒ¦Õj+J€cÔL7ÿììl¨T*s\ªP¾¾¾˜÷ÝwøäÃŽ4HNNÆÂ… qòÄ eV|øÊåX±b… ]o£À±~ E±X|Á †aŒ¢R©ÊHì쎋q¨¾bcc-¾^*•bÈС£Ë&†aŠ%Ž“J·À¨ŸÀGÁ×_§OŸê=æáÇÙ&çææ†å+V`jh¨A7ÿ„„|5p –.]Ênþ"pwwÇO?ÿ,ÊÍŸ»Sž>íãïï_ìnþ›`FµF³‚_ã·ß~Ã÷óçƒçyètúï¶²Äô½zõ0ï»ïP®\9½ÇPJ†Å?þˆÌÌLfW|Èår,Y²Äàm–y"dB.  Øî»dÃ0Q«Õ“AÈHcblß¾K—,ys¢!€9÷ÿK$ >ƒ 2è=óÿû_Ìš9Æ„Ù/AAA˜{{{£cQ`¡Ÿ\"BZ6Å„Z­vͱ·wpÐ需çyGǽ;™““agg—E)Í&„¤gddèÒ,’0c•4Mò½116mÚ„Õ«Þ=W…׳Ðétˆ¾tɘËëÍÃÃß/X…B¡÷J)víÚ…•+V°6¾"qrrBè´iF÷ôEB†ùÉå›ÄfëX`£"œŸ=ó$”z  G©;¥´,qP–e^ýÞ€]NÞþ1˽ßqL"îõ×(…ÌÁj&ÀSOð”ò@2¡ô¿”{„Ò{<ÏßKusû»¹§g±Ù>Si4šÆ¶ÂˆµCÖ¯ÇÚµk?øºNÏîwZ­Ïž=zy½µlÕ ³fÍ‚³³³Þc’““1cút\2SRT¯^?,Z„*Uªˆ.“ÒÓO.ÿCŒ`E+¬œV«-A)­@I9(õ!^®ïµZ¥@H¾¿7‚@Ye)¼š²}ýk €p\SSujæo ÄB®Jã8Ž‹—H$7|||¬ÿ¼V¦@Æ›Rú;ÑõÛ{vïÞçÍÐÿ€©»ÿÙÙÙá›I“УGƒÆEDDàÛ9s’’b¢ÌŠŸÎ;¼à²O@ig?…ÂzÚGZVX™«W¯VÊÍÍm  iÀo?q‰sS7 /x½. t<Ïç¨5š[ ä¥ôxþ¼L&S±¢ÀvhµÚr”Òà ¤´ÐGŽÁ¢~È÷ûú¾0åé¥K—Æ¢„ŸŸŸÞc233ñã¢E 3Y^Ńƒ¦††¢sçÎb…¼­ã¸Nþ¾¾‰°¨`€…ÅÆÆVà8®>Ð@e+¾É aÀ”ú ‡œœœjæ ÎBοxñâ|ýúõ[:QæC*•ÊI"•þBª qöìYÌœ1£ÀCnôyðäÉÜøË4?ÃkÕª…ÅK– |ùòz¹ÐÐP$$$äûØÛÛéD‰7_{‘™‰œœ—»Î222››‹YYl‹ €Úµkcî¼yðôô+ddvVVwöó%o¬0³ÈÈH©››[JiÒžpœ‚EêŽ_ 8hLÆ”RØËdT­Ñ\§ÀBéQŽãÂåryº¥ódÀI¤Ò„P«Õ˜ôÍ7ÈÍÍ-ðsúÌ\8Þ$'åuêÔ Ó§O‡½ž­dyžÇÖ­[±vÍš77ò¼p___|Ö¨êÕ«‡  >9++ iiixöìÒÒÒ^þúÕÓÒÒðøÉ$%%áÉãÇHJJÂãÇ ýsµR©C† Á Áƒ!‘HÄ Jé&N7¢~ýúÅr¿>X`‘ ®))mÁq=\ÝÜÚSÀ­ˆ=å‹ð!€ÊSš¥Öh¢(!‡xŽ;ä_»öK'Xi4šï ëæMŒ=/^¾64W@ìýÿ‰&L0迤¤$LŸ6 —/_.ô³<Ïãøñã8~ü8 B… ¨[·.êøûC.—ÃÓÓä­Ÿ2™ eË–EÙ²eõÎçñãÇxòä ’“’””„ûÿûîß»‡{÷îáþýûHK³þM²tN6ìI9ôäÉ“ÓÍ›7/=VL­V÷!Û!pv*11ýûõCrr²^ŸŸ1s&‚‚‚òý>ÏóhÕ²¥A âêêŠ-B½zõôsáÂL -gggøÊåûú¾ü¯\Ž’%KŠûµÔÔTÜ¿÷ïÝÃÍ[·ð×õ눷ŠÅŠÇ¡OŸ>5j”Þ³/zH¥}”Jå!±e¬™F£iLA‚¸X:Ÿ"(‰¿êÙUG.¿hédŠ"­V@)|õªÆ`ÏŸ?Ç ¯¾Â­[·ô3mÚ4tÎ÷û×®]C_žÔ òé§ŸbÉÒ¥ððð(üÃxY|¬[»›7o6É+ˆ×8Žƒ§§'䯊…R‰*Uª¼3K –àúõë¸øëסÕhðüùsѯ“Ï*U0cÆ Ô©SG´˜ˆ#”vS*•úÿÅ+æX ‚Ëññîv¹¹_RJÀ×Òù„üC)ý…~R(7-NQ R©<$Rée Ÿ››‹Ñ£FáâEÃj³©Sñå—ùwί€¡Ú¶k‡Ù³gë½µìÉ“'˜jðÿ±”(Qµk׆ÒÏ~~~ðS*Å|Z~ƒçyܽ{jµ±±±P]¹‚ÄÄDѯ#•Jѯ_? >\”Ž~oÙÉ2”­2 +„#±qqí¥CAé`ë),ív‚çwûùùé7ï̼C¥R9I¥ÒÓð2žRŠÙ³fá? ï³2iÒ¤ßÅ0À¨¶ºÇaÌØ±0`€Þc.]º„i¡¡xüØzÛËdð­]õë×Gý€øúúŠ·hî= wïBƒ+W®àâ… F¿6ð÷÷ÇôéÓá)NSŸ×²(!ãýärã«ÃbˆŠLHppIKëA(‚—{ôë’ BPnƒŸŸß K'cCˆZ­Þ Bz `ÌSú„‰ѯ_¿<¿—––†Ï›7<ýîââ‚ï,@ÆúZÌó<6nØ€ 6˜tÊ_ NNN¨S§ê Aƒðòò2ÉuxžÇõë×qþÜ9œ;wqqqzÿÙ¸¸¸`ܸqèÒµ«Ø¯3îSžïáççwAÌ Å +ô[™2 „ü€«¥óaôr™²ÊÙÉé×êÕ«³MÖˆÕhfà[¡ãþù'fΜ ¡?OÆŽ‡æù½ãÇcÊäÉ‚âzyyaÉ’%¨X©’^ŸOKKÃôiÓpæÌA׳44mÚM›5CݺuÅžf#-- —.]BTd$"##‘žþáÌ;Çq Ĉ‘#QªT)q ôN7Øßßÿ‘¸‹VB÷©„ç§艗Ý©©©HIIAJJ RSR’šŠôçÏ‘•gÏž!';™™™ÈÍÍEFFFžq$R)J89x9ÝX²dI”,Q%áìì WWW”+Wîîî™à]¤‰$ƒÒR©t]íÚµïY:k£Õj;ñ”€ÀÿÑÑÑ5rd{â 3zôh <8ÏïÍž5 08f«Ö­1gÎ8½úû\˜›7obâĉ¸¯hüqrrBƒ ФiS4iÒ¥K näX ììlœ9s{~ýW®\ÇqhÙªlЉLJÈD6å/VäC£ÑÔ0=`Äá'bÊÍÍEBBîܹƒ;wîàÁƒH|ð‰‰‰xøð¡Q?€…pqq‡‡ªV­ŠêÕ«£jµj¨V­*T¨`ÐÑ©f” Jç8î{¹\céd¬A\\\UÏ_à&dü½{÷Яo_¤¦¦•Lj‘#ñõ×_ðuJ)Ú¶i£÷vB „`èС2t¨ÞS·¼¹sõêY`‹8Žƒš6k†¦M›¢F&¹NjJ M²P€†ò|o??¿xS/ŽX𞸸8_N7„ÁÂ7þ¤¤$ÄÆÄ 66jµÿý·ÙoòB8::¢J•*¨^½:¼¼½¡ËñiÍš°³³³tj¯Q‡0W¡PD[:K¹pá‚££“Ó9ú7¿ËóçÏÑ¿_?ܽ{×è\† †¡Ã†}ðõ›7oâKæqppÀœo¿E›6môú|nn.ÿø#vïÞ­÷5Šwww´lÙmÛµƒ¯¯Õo\¢ tEÉ’%§°Wyâb+×_ÑjµyJçèB,rãOIIÁ¹sçpñâE¨ccqÿþ}K¤a´ÌÌLÄÇÇ#>þÿ u™L†ZµjA¡TB©TB!—ÃÅÕbK)€Žè«Ñ#”ÎU*•Åî”0'§Uxóçy!!!¢ÜüüOäë¯õj»[ÓÇË–-C¹råôºžV«ÅÄ ðè[Cö¶Zµj!¨[7´mÛ%Þ:ÀÈ ¤€Eºœœeþþþy/`b Vl €½{÷J¼½½ÿC9ô?ÌH”RÄÅÅáø±c8qâ>|h®K[5{™ uýýѤI4nÜXïUÛ¢£ô4ÏqSŠr—AFãà¼ÐNÀìY³DÍiàÀ;nÜ;_KOOÇçÍ›úÚ«uëÖøvî\½›û8pó¿ûÙÙìDêü899¡cÇŽøjÐ ¸»»[:·Ý¥Ó•JåvÅãiÉ„Še V«• dý)>>ÇŽCøñãxðà¹.k³<==ѤI|Þ¢”J¥IÚ¡€àWJéT¥R™`Î ›šZ­v!*UŽÇÐ!CD¿yöëß&Lxçk‘‘‘ÿ^Qð6Bþóõ×>|¸^?t:–,Y‚];woqagg‡.]»bìØ±¢ŸS`¤XP:J©TêÿŽˆù@±ZpáÂGGGÇ) d*Ól}Ë“'OpèàAìß¿_´w¥ÅEBB°}ûvT¨P:t@ûPµª û–¡z‚ F³.77w¦¿¿¿qËÜ­!d-xóOLLÄÄ Lòäœ×€‚Nÿ“Éd˜3gÚ¶k§WüÔÔTL™<—.]œcq”““ƒ°}ûP£F tïÞÝÒé¼Í„œÕh4;rss'°~›€­¶Gé¢ö¡|Ïó8þ<~ß¿QQQEæ¼nkQ³fMtøâ ´k×eÊ”1×e“)!³RŸ<ÙhË'j4š!X/dlvv6}õ®]»&vZ€ž={bJHÈ;_û¢Cüûï¿|¶L™2Xºlj×®­Wì;wî`ܸqEf¿9ÕÀ˜1cP«V-K§RdŒR({,ˆ­)ò€J¥r’H¥K 1åuRSS†=¿þj’C4˜wq‡úõë£[·nø¼E “õC!×Áóß(•Êæ¿˜¸4M- DCàB×ïçÏÇž=¦ûùÜ£BCCßü>áî]~ð¹O?ýË–/Gùòú-Û‰ŠŒÄ´iÓòìTÇä/ C‡ ƒŸŸ M"A€í¹¹¹£‹Êl9éWZ­¶6'•î‚ Oè»{÷.ví܉ƒÙ&"Öˆçy\¼x/^DùòåÑ=8AAAâ·}¥5AÈ!µFsP*‘Œ°•®‚‘ ®ÀN¼ù9rĤ7àß{pá‡íÝ[¶l‰¹óæéµ[„RŠÍ›7cíš5Åfg±!ø¬Q#üç?ÿR©´t:£@?‰]3µZ=@©TFZ:[PTgˆZ«J0IK*•J…Ÿ6oÆ… ÷?gÄeooÖmÚ gÏžzO! „̼uãÆªààà¼7±[ µV»”Ž2öï¿ÿF¿¾}óm-–®˜õÖ΂ ãÇ#""ÀËÓ Áƒ1räH½û½xñ³fÎÄñãÇM–oQbgg‡víÚ¡ÿ€¨^½º¥Ó%ÀÊ%JLf½ Vä €ØØØ D"Ù J[™"þ¥K—°aýzÄİN²Ö¬víÚèÕ»7Ú¶mkÚׄ\à€!r¹üªé."œZ­î Bö ›‘‘¾}ú˜ek§Nðíܹ^Îî´øüs¤¦¦Â^&ìY³Ð¡C½â$&&bü¸qøë¯¿L™n‘P²dItïÞ½z÷Ö»‚Ñp„ôµÖ›ÖÀ*¶ ¥V«»ŽÓ˜âæîìY è߯5ûÍ¿fÍšf½^QpõêUL E`×®8pà€écRÚ§4V£Ñ,×jµVÕ9E«ÕV ¸ÙÏܹsͶ{åíiú[·n!55¥K—Ʀõ¾ùÇÄÄ OïÞìæ_wwwL˜0GÃØqã,zó¿páÆ FcŠð JéåX­¶¿)‚E¢Ø»w¯D­V/zõ¤#êÒðk×®á?ƒcÔ¨QÐjµb†Ö[ûöíõnr¼ëÞ½{˜=k:wê„];w";Ë$3‚R ŒyU|nŠ jïÞ½ØB·{÷n=rDì´ò¥{«¸|ù2¼¼½±}ÇøÊåz ð¡CñäÉS¥hó¼¼½1}Æ øãôëßߢþxžÇºuë0jäHDEEaà€6t¨èÅ¥[b5š{÷îµÚÓ\-Åæ_ܸqÃ933sé,fÜû÷ïcÕÊ•8~ü¸ÅßñoÜ´ «V®4U•\¬¸»»ã«AƒÐµkWS•nïÕZíTP:_ÈØ«W¯bð Afí”צM,üá/èÙ:77‹~øÁä‹mYƒ ÐÀ4lØÐÒ©=z„©!!¸råÊßã8í۷ǨѣEï@HcRŽëåëëûTÔÀ6̦ €˜˜˜êœDr€~§è!==ëׯÇî_~±Š“÷d2""#±uëV¬_·ÎÒéåÊ•ÃÐaÃеkWS]|Mòò‡Mœ)‚D£ÑøQà"4»JMIA¯^½ÌÞ­²eË–øqñbƒÆ¤¤¤`Ò7ßäy#)îìe2´kÛ½ûô1ÙÑ¿BD_º„ÐÐP<~ü¸ÀÏ9::bðàÁè׿¿Ø…úmŽ@¶.à%›} ÑhÚrÉeˆxóˆˆ@·  lß¶Í*nþаaC8::¢I“&–N¥HIJJÂÜo¿EÏ/¿ÌsË™jéxþ¢F£jŠàù¹pá‚#v@ÀÍŸRŠéÓ§[¤Uu~§æçöíÛèÛ§»ù¿§|ùò5jŽ=Š9ß~k57ÿœœ,_¶ Ç/ôæ¼ÅãÇñðáC“o3+L£Æ1aÂSµþ]—›ûµ9Ú•j4šå#dì®;±hÑ"±SÒK³æÍ±lÙ2½>{þüyLž4‰5÷yKôìÙ-ÌÕË wï"44ׯ_£yóæ˜"æk B&+åòÅ h‹lªP©Tv©t€bÅü}ÿ~üøãVùÃäã?Æþ߇àè‘#˜:uªY®íââgggØÛÛ£\¹r([¶,ÜÝÝQ¦lYxV®ŒjÕª¡´ˆ­xÓÓÓqïÞ=ܽ{wîÜAœV ­VkÖæJ‰Aݺaøðápss;üÿ@i_S6(Q«Õ­AÈ1ŸœtëæMôí×ÏT‹$ äé鉕«V¡bÅŠ…~vïž=X¸p¡Á3E‘L&C‡/¾@Ïž=áíímét> Óé°cǬ[»V”ÇÎÎΘ6mšÞç?è…ÒùJ¥r:Û¹ŠÈf €Û·oËž§§ÿàÃþ ¤¦¦bÞ¼y8.F8Ñ•(Q«V¯þ #×ìY³pàÀ eõ.T¯^U«UƒWõêðòöƧŸ~*ÚŽ…œœ\»v ±11¸xébT*³œ­P¢D 2}úöûiJJç)•Êo!òQ¦jµÚ•¢%€Áç(gee¡OïÞ¸s玘)饎¿?–,Y—?Çó<–,^Œì$?xxx Ç—_"°kW¸¸ºZ:<ÅÅÅaÞܹ¸yó¦è±;vê„ñv1²5åÉ“ÿØò9BÙD_2;7w¿Xûû/_¾ŒÓ§ãáÇb„•½L†Ö­[cøðáøøãóüÌñãDZuËÄ›éu€!$ ªV«†Zµj¡–jÕª/ooH¥Æ¿­IOOÇ…óçqæÌœ9sOŸšv1o50cæLS„rPÂqýÅ\«Ñì&À—BÆšºÏ~Zµnï¾û®ÐE^:QQQfÊÌ:Õ¯__öì‰æÍ››jáªÑ?z„µk×bÿþý&mÁ\±bEÌŸ?_ïm¢…¢ôÌÌÌž 6Ì' m°ú >>¾TNNÎ! 406Vnn.Ö¬Yƒ­[¶XMpB*Uª„Zµj¡Y³fhܤ‰Þ•í¿ÿþ‹ˆˆÄÆÄàÆ¸ÿ¾‰³Æ^&ƒB.G½zõP·^=øúú]èt:\ºt GADDž?.R¶ïâ8½zõˆ‘#áä$¨•~Þù‡P¬P(¢ «Ñô#À6!c£¢¢0~Ü8³ou îÑ!!!…ÞÈ’’’0fôhܸqÃL™Y777tìÔ ¨RŤ™%##[·nÅömÛ™iž{¨D"ÁÐaÃ0xð`q "B¢t99]ŠÓaBV]hµÚŠÑÑÑF-´1GGGøùù¡I“&h٪ʖ-kT¼ì¬,œ9saû÷ãâ… &)îÜÝÝ:mš¨»1ð‚R:F©TnãêÕ«•ruº8Ï¡çáÑ£Gèlò™”÷ 6 C‡ +ôsׯ_Ǹ±c‘””d†¬^ªR¥ Z´l‰ *ÀÅÅ%K–|ó=™LÙ{³Ù99oÞo§¥¥xyAVVRRRðôéS¤<}Š””<~üøÍ z•Åq4lˆÀ®]Ѭyó7k€¬QvVÂöïǦõZÝo Íš7Çwß}'Ö+ åùÖ~~~Éb³vV[hµÚ*<χƒjÆÆ:{ö,¦†„˜ì)Q,åË—G³fÍйK—§322ðÇ8~ü84ÕÌfÁq”J%Z·n-J1ðï¿ÿ",, ~ÿ‰¿è¾M›6˜CMøøø zõê¢7Œ¢”âÉãÇHJNFRR’“’üèž>} ¹¯/š4ibµïö_ËÌÌÄo¿ý†m[·šäß™¡ªU«†eË–¡b%ƒ—À|ˆRmvvv‹úõë[¦¢1#«,bcc}Ç ß¡ßù ”bË–-Xµr¥ÍÝ$Û¶m‹3g~PÕFGG#têT‹UÛ¦Äq Å›bÀ˜å¹¹¹Ƕ­[Eo/úÑGaúŒhݺµxA)ÕJ$’@__ß¿õ«Õ'”®r¹;v`ñæÛegg‡ùó磕fÛ·oDz¥K­â߬T*EõêÕñiÍšP(ð÷÷G%1n26*==¿îÞ;v˜}æ¨0...øá‡P? ÀèXˆ!”¶T*•)"¤fµ¬®¸zõj¥\ž? J?1&NVVæÌž#fìg.¶·Û£/_côêÙÓâ{æÍã8Ô­WÝ»wÇçŸnÔšèèhlÛ¶ çÏõ]w×®]1iòd1×ÄøñãÍÖ4Ç”¶mÛöf¥ë´ÐP>|ØÂ™_éҥѥkWêµWA=899aÅÊ•ð÷÷/ðsÿþû/ÆŒm‘>¦P¥J•7AݺußYHh t:N:…Û·[ìT1´hÑßÿ=ìe2cC³·³kçããc½SYE!ôAá IDAT_2'''ÜØ­~§OŸÆäI“U„n’nnn8qò$N<‰I“&Y:«Q¶lYôêÝ=zô¼ú7N«ÅâÅ‹E;e±Aƒønþ|”*UJ”xÔ 4P©T&¼þBLLLMN"Q(ü¨¼÷DEEaÜØ±båV ’%Kbõš5²O;N«Å¸qãŠä1¾vvvhÓ¶-Æ'ö¢Q“xôèÂöíþ}û̺ó”êÖ­‹eË—¿C€%œ:Zò„OS°xpûömÙ󌌃Æ6ù9räfΘa–Nqæöó–-8pà~ß¿ßÒ©X>ú={õBïÞ½ í&—J)ÂÃñrÅ Qú(”+W .„ŸŸŸÑ±^yD€ …""22Rêêæv@}Cƒ¤¤¤ {÷îf™sqqÁšµkáSÈk‘ãÇcÆŒEnV«|ùòèŒÀÀ@”.]ÚÒé*F¥Âž={pòäÉ"ùóóÓO?ÅªÕ«Åøÿb§R¡è‡"Ô6Ø¢ÀÞ½{%^ÞÞ{ad{ß={ö`á‚V±jؾ™4 ÿüSôÕìE‰““º£¿~‚ž¶²³³ñË/¿`ÃúõF/²”J¥3f úöëgp_‡|ä`¥Ô„Ì`ÊäÉ8~¼Ðµ…FsuuÅú  íMÿóO?aåÊ•fo@dJuêÔAÏ^½¬ò@ž÷eddàð¡CسgnݺeétL®R¥JX»n]¾ÝUõFÈwJ¹|º8YYžE µZ½„|cLŒM›6aõªUb¥d•Ú´iƒˆˆ«9¢ØšÙËdìÚ¿úJÐÉa‰‰‰XôÃ8uê”ѹ´hÑs¾ýVÌwÀ:ßYŽ=бrÈ—‹‹ ÖoØPà´<Ïcá‚i=l ö2:´ož½zYÍÑ»‰Gؾ}8zô¨U€fJeÊ”ÁºõëQ­šq­e(!ÿñ“Ë7‹”–EY¬0¦})ðrêvéҥؾMp›!“ÉŠÔºs°—ÉЧwo þϽÿ;}ú4.X€ÿýר<*Uª„E?þh±›Ã£Gн[7¤¦š¶»©>7ÿ/^`jH"##Mš‹9¸»»#88AݺÁÕÊ›ö¤§§ãð¡CØ¿¿Mt 5¥Ò¥Kcã¦MƶUÎ!@'…BqL¬¼,Å"@L\œ?Çóg `!Ók ,ÀîÝ»EÌŠ)ŠJ•*…á#F ((Èàú™™™X¾löìÙcÔTµL&CHHºŠr¥AÆŽ#Ún‡ü¸¸¸`íºu¨Y3ÿŽÝ)))3f âlxU9ðrš¿W¯^øÜ¦ùã´Z„……áØ±cfï¨hÍÊ”)ƒ›6Û¿áåù&~~~⬠¶³æc \à!4ÆêU«°iÓ&³bŠ:///L˜8 ¾ÑäÒ¥K˜=kÊ!((SBBDo-›ŸßÿsfÏ6é5>úè#¬[·®À>÷ïÝÃÈ‘#ñÏ?ÿ˜4S‘H$hݦ ú÷ï_`‘c ÒÒÒpøÐ!„……‹wûB•+W›6o6¶«ã}4P(ÿ+/s3k™ààšš«˜_Ûµs'-Z$bVLq€‰ß|///ƒÆ¥§§cé’%Ø·oŸQׯéãƒ%K–ZŸ`ˆ‡"¸{w<{fºþ%%K–ÄÚuëP»ví|?síÚ5Œ=Ú&·ù9::¢C‡è?`>ùĨƤ&w=>ûöíÃáÇÙÓ¾žÊ—/M›6{~@tÉ%šÚêö@sD­Õ{4†……aÞܹEjå0c~‰ý À°¡C n‰9³g#%Ex‹ð2eÊ`ñ’%…RŠaC‡":Ú蓆óåì쌵ëÖxhÕéÓ§2eŠÍÝJ—./{öD=m-5—””üùçŸØf¶ÎŽE‡‡6mÞŒ *fµR¡%VNæd¶@­Õ~J?º;v ¡S§Ù­~Œùyzzbæ¬YïÙøð!BCC£R ¾¶½½=¦„„ ((HpŒüìþå,\¸Pô¸¯•(Qk×­ƒ¯¯o¾Ÿ9øçŸ˜={¶Mµ­\¹2úõïN;ŠÑ=Î$(¥¸rå ÂÂÂpêäI³çP”U¬T [·n5®¥}•JåNñ²2³Z­¶Oé‚^~ž={ãÇ+’M*Ëâ8=zôÀè1c :Ô‡çylذ7l0ª(íÞ½;&O™"Ú™ïÿüó¾ìÑãÍõbsppÀêÕ«Q§€ö¾Û·mÃÒ¥Kmf¦®víÚøjÐ 4oÞ\´VÎb{üøñ›§}[]KaÍjÕª…›6ÁÑQðºôLPú™R©T‹™—©™¼Ðjµ%xJUxÀÏ­›71pàÀbqc9˜>c>ûì3ƒÆ]¹rÓBCjZ§N,Y²Äè3àyžÇW𬻽L†åË—ç»’RŠ•+VàçŸ6ÉõŦP(0dÈ|Ö¨‘¥SÉÏ󈎎þ?öÎ3,ª«kÃÏ>3€‚ X’¨ ¨Q#‘™Aì=£‰1ÆÞ[,+(öŠ‚ˆŠ]ì=5QcLÞX’Œ8ƒ56ŒJDdÎþ~X> 3pÎì3æ¾®÷Ç뙽ö2ÂìuÖ^ëYØ»w/Ž;fב˜-Z`ñ’%¦tw\Öçå5ðóó“¶ç–!’Z­v†ŠY›žžŽ~}ûšÜ‹mÇNaéÔ©&„†ÂÍÍ­ÐkÒÓÓ1uêTüþÛo¢÷õôôIJåËQµjUÑ66n܈إKE¯7†ƒƒ¢-BË–-ó}Îó<æÌžo¿ýV’ýYRÏÏþ#F0™/÷SSqàÀìÛ·‰<µÂÓ¥kWLŸ>]¼B¾S)]`#rÁ’j®¡TÔpï¼¼<øû#Á„{V;vÄðî»ï"""ÂhšûMX\ ”+WëÖ¯ÕŸ|õêUôéÝ[’;a™L†ÈÈH´ý8ÿq¹99˜8q¢Õ ü4lØ#üý œNh xžG||üË·}ûu§å€@€x„ŒQ)ÒD⌑,Ðjµ•)¥Z"jƒ9Ç–Ú±ó&ÇaĈ>b„ {áß~û S&OÆÃ‡Eí»6. 4´†çy|5d³©†¯ÂqfÏ™ƒŽ;æû<++ £CB¬:PoÒ¤ †Ár@3â»ï¾ÃþýûíoûV!ÓgÌÀ—_~)ÖDü”Jåß,ý’©N­Õ%@1‹÷ìÞˆˆÖ>Ù±#???DÌŸwÞy§Ðknݺ… ãÇãâÅ‹‚öú¼S'Ì™3G¨‹’UýB0mÚ4t1Щ™™‰‘#GZ­ºŸŸŸBF–¬ÝR,ö·}ëG.—#nÝ:¨T*Që)p–ÏËkìççgÕ…’&„D‰Y{æÌØ)ìX eÊ”Á¬Ù³ ÞçGNNæÌžï¿ÿ¾PŸ¯_¿>V¬\)X%ðîÝ»èÞ­›$ƒ]BCCÑ·_¿|Ÿ¥§§#(0Ð*µå?øàŒ5ÊêŠû²²²pðàAìÚ¹7nܰ´;v  |ùòعkÊ‹˜. dŠJ¡°ê7Yæ€Z­®J8îo‚'°¤¥¥¡Gf™Yn ”/_ 6Ä?ü`iWŠ=„ôíۣnjԲ‡U+Wm‰«^½:6nÚ$¨ððÁÁÁ&b„¿?ó}v?5¸rå ó}MÁÓÓAAAø¤}{«j绕œŒ]»váÛo¿-vøl•J…¸uë —ËÅ,Ês\“z>>V{?Æ<Ðhµ|.t¥cÇŒÁñãÇ™úcë”-[Ö&eT‹*u¼½-H9ìÈ?búŒÈÍg¢c¥J•°~ÃQÒÀßÿ=¦N™"x]AôêÕ “ÂÃó}–’’‚\¿~ù¾b)_¾zôì‰ys犮ì¶%J•*…GYÚ «G.—#4, ={ö,ôš›7o"0 ·oß6éð€Éáá8|ø°¨µ†hÞ¼9/Y’ï[ôÍ›7á?b„ÉY “ÉЭ[7¡Œ‰âI¬ÈHOÇŽ;ñõ®]ÈȰ;…ÄÉÉ ›·lAíÚ¢ôìòdWÏÇÇ'‘µ_¦Âì¢Ì©dÉùqøçææ"|Ò$«<üàêꊒ%Kš"iS4oÑ•+ þg,väååa~DæÎ™Sh…¶*Uª`ã¦MhÞ¼¹I‡ÿÉ“'™þ*• ££ó=ü¯^½Š¡_}e‡ãƱëë¯>y²Uþÿý÷EGãÓO?ÅÚ5kì‡%''“ÃÃó½Æ+ò<ž_€0vËd˜d4Mr €` ÅEÑÑØ¶m›É>H‰»»;ÒÒÒ,í†äxzz"zÑ"ܺu óæÎµ×___,ŒŽF¹r¢$/‘••…îݺ1=ŒkÖª…õë×ÃÕÕõ­g/^D`@€Åþ«V­Š±ãÆ¡U«VõãÉÉÉØ´q#:dÈSŒ0`Æ/j-z)•ÊÝŒ]2 “3ÇŽ“ƒ8ˆ8üÿúë/lßný”,ýåg<==Ñ£gOxU­ •J…N:™6«¡V«Ñ¿_?³´Ä-‹ezø{xzbÕÊ•ùþçÎÈáÃ-úóïêêŠqãÇcÏ7ßXÅáùÒ%Lš4 _vîŒ}ûöÙÿbÆöíÛE‹^Q`‘N§Ü'%&g´Z­?V ]—››‹^={ZU5±`øðá¸páNžèÚ¥‹½½ÌN±ÂËË ËW¬¥¯°uëVÄ,ZÄÄ)S¦ {oýùwß}‡Ù³f™õŽÛÑуư¡CżA1ƒRŠü±K—Z…ÎÛ¢D‰Ø·¿Mú|d°Z ¿ ‹¸ !s!ðð€Õ«WÛ;ÅŽëׯcРA8Ÿ”$hÝ¿ÿþ‹U+W2ñadpp¾‡ÿîÝ»1kæL³þuëÖÅÎ]»dÑÃÿܹs=ßóùóç›|ø{xz"fñâ×J)EG›íðW(صkX0埙™‰¨¨( 6Ì~øÛ‘”eË–‰É*9prùh)ü) …þ­Ôétè/tƒÓññLR™vì58€1cƼ&·ûÝwßá—Ÿ6Én¹rå°jÕª×f9ð<ˆˆ³ Þrtr¸ñã±qÓ&xU«&ù~†øá‡ÐåË/±sÇû ˆÉ9Ÿ”„cÇŽ ^G€ ¥Ù{T0…xJÇpbœRŠ¥K— vÊŽâÂï¿ý†¾}úàÂ… ¸qãFE™d¯dÉ’ˆ]¶ì5}žç1göl|³g©îÈûï¿m[·bÀ€{ë¿uë‚1eòdÜ¿ß">Ø)žl§p[Š“Ë²ö¥0ªàôéÓ圮(%Äø/?ÿŒ &ˆóÌŽ âàà`Ö¢UG''¸—)ƒ””Ñ6d2/Y‚-Z¼ü3žç1}út|è 7 BAï>}0fôh‹µöQJ±{÷n,]²Äbs ìØY‡ ZC€ J¥Ò€Y+S ¢;::…ÀÃÖ¯_/Ø!;v,MÙ²e1hÐ ³î™›“cÒá'N|íð×ëõ˜:eŠä‡¹òå±|Å „……Yìð¿sçüý±`þ|ûáoÇ¢lÚ¸Qð | ÖéÚHàŽQ 2L¨á?þøÃ,óÑíØa‰ƒƒºu놟~Š÷ßßÒîš>}û¢GÏž/ÿ^^&‡‡ãðáÃ’îÛºukìÙ³M›6•tCPJ±oß>ôèÞ§OŸ¶ˆvì¼Ê©S§pñâEá ) bïq ¼ÐjµŸPàˆPÃÆEBB‚hÇìØ±$ï¿ÿ>®\¹bi7 E³æÍûòÎ=//“&NÄ/¿ü"Ùž%K–Äø Э[7Éö(ˆ””Ìž5 üñ‡Å|°c'?:v숹óæ ]–G/¥Rù¯>åGã€)³²´Z­ýð·„¼ûX±"J—.2eÊ L™2 „¼üLnn.žÜÈÌ=zã'L€»»»er<ë´‹”Æ«ü74HBBBE™\þ¹P£ìwÿ’ãè䟺uáíí oooÔ¬U ž¢î`õz=’oÞÄ?ÿüƒ+W®@§ÓA«Õ¾ÖžfÇú([¶,b—-ƒ‹‹ €gÁÝ„ñãqòäIÉöìÖ­BÃÂÞ2YYYX°`inÞ´ˆhš¤ÈårÔ«W-Z¶DÓ¦MQ­Z5Q½žfôhÄÇÇK²WíÚµ…ªU«Jb¿ (¥Ø²e V,_^¤®ª8ŽCã&MбcG´hÑ®®®óE¯×ã÷ßÇpâøñ"õßÙ’,Yº­Zµ¶ˆ%*…b¬4½±•¡@§Ó5â)$á—““ƒöŸ|b²Œ©g2ª}ûöE›¶máàà`iw^#99ûöîÅþýûíÿÖ`À€7~<àñãÇ‚¿þúK’½º÷èÐ ,ÖÞw?5S§N-Rj¢UªTA—®]ѱcGT¨PÁÒî¼Å½{÷°cûv|óÍ7xô葥ݱiZ¶l‰¥Âý§ÏËóðóó“< 3¨µÚE'ÄØ0cút&ŽW5j„aÇ£~ýú–v¥@rrrpøðaìØ±—/]²´;Å‚&Mš`ùŠà8YYY5 gÏže¾‹‹ ¦MŸŽöíÛ3·]Xâãã19<<°˜,iبúõë‡æÍ›[t0RaÉÊÊÂŽíÛ±yóf{=Här9ŽüôÓk²Ü…'äÓz ŹõC§Ñjo¨,ÄØ€þýqîÜ9&Ž7>øàŒ7 6´´+‚¡”âøñãˆ[»III–v§Èâéé‰mÛ·ÃÍÍ YYY­VË|ŸªU«bñ’%¨f¡*J)Ö­[‡Õ«VÙ|Ý Çqh×®††š5kZÚQ¤§§c]\¾þúë|‡WÙ1Nhh(úöë'tÙZ•Ré/…?¯’o NLlIxþ¸C—.]B¯W„HìŽÒ¥Kc̘1ø¢sg›x+(ˆ“'ObYl,._¾liWŠ...ؼe jÔ¨¬¬, FÃ|ŸæÍ›#bþ|‹ÝGgdd`ÚÔ©’v2˜BÚ´iÿ€›=øßäòå˘7w®$AgQ¦Ž·7vìØ!tYÊåK—*÷èÑC/…O/È÷Ä!”öjèG‰ÇŠ"íÚµÃÞ}ûðe—.Eâð€-Z`××_cÚôé(_¾¼¥Ý)p‡¹óæ¡FÈÎÎÆ¨à`æ‡?!_ Š¥±±;üÏ'%¡oŸ>6øûùùaûŽˆ^´¨ÈþP³fMlظ“'OF‰%,íŽÍp>) ÿüóÐeïÖªU«‰þ¼J~§¥…¡”âÇ%¿®(2¸¸¸ bþ|D-\ˆråÊYÚæp‡®]»â»0xðà×DJì' ­[·ÆãÇ1*8jµš©ý’%KbAd$Fe±@tïÞ½°´;6ƒInè"+¯ñÖ€N§«ËSš(ĈV«Å`3O±U,ÝRe ._º„¹sçBgoÌG}„E11xòä F3Wج\¹2/^Œšµj1µ[Xrss1oî\8pÀ"û³ÀÉÉ Ã‡Ç€áè(hbºMóôéSDEFâ›o¾±´+VWµjØ¿¿Ðe×UJ¥¤…8o…ûzJ—ýÚÓÿ…ããví°ióæbuøϤc7nÚ„ðÉ“áììliwl†*Uª`öœ9ÈÉÉAȨQÌ¥R‰íÛ·[ì𿟚ŠáÆÙôáß AìÞ³C‡ +V‡?ðlpÖ”©S1eêT«kU¶6®_»†k×® ]æuöìYISIoøDˆžçqôèQvQ††¨¨(sÞ¥R^ügÜk.'^Àqzö쉯¿þ*•ÊÜÛÛ%J”@ô¢EËå‚3gÎ0µß¦M¬Y»¥Ë”aj·°œ?ýúõ³Ù¬››fÌœ‰5k×¢J•*–vÇ¢tïÞ+V®|)Im'Ä ç"ry; \ùû¯^œ:uª¤³³ó ú”JLLÄÀ$q®(@Áرc1`à@©¶ ’(ð+œp™RzM¥R¥[¤V«+pW'Ä£Ôõ(Ȥrô<ÏcóæÍXµr¥]qÌsçÍC›6m0fôhæcn{ö쉉“&Yì¾ÿ§Ÿ~ÂŒéÓmv@M“&M0kölsŠødáÙïx2¡4@:!ä1dp@.¥4@帒„ç9 çÆQ*£„”P”Vð!žýŽK¢êtþüyYdtX#²àJ©ì$…?À€V«mOAÕ|k׬ÁªU«˜;V „ <<üµ9팠 ä¥t½Œ# …â?F“’’Êæäåµ'”vЀ¤eüÿý7&†…áßÍ6ýÒ&èܹ3BØ‹üB0jÔ(ÁJXA)ŪU«°..Î&g…899a̘1èÕ»·Ô:ýzPú;€ïe2Ùá .$±j»xñ¢kvNN{BiÁ8?±Ñ IDAT¸~ý:† †û©©,Í !øáða¼÷Þ{B–eêóòÊI¥ øZ Ñj#„ 10xÐ {_¨ÆŒ‹Al‹#Sl Àz¥R)©ôÞž={d5kÖì`0ù€$œ™™™˜5s¦¤³ëm www,^²óæÎeª¥ —Ë1sæLtü\ðpO&<~üS§LÁÿþ÷?‹ìo*uêÔÁ¼ˆIÅ‘À²zýN__ß{’môœ„„„ò2™l  "+»W®\ÁðaÞn4 Y,™>}:ºtí*h GHK…B!Ioìë€Nw ”zrAff&>jÝz½¤Z6É Aƒ0f,³yY dIÎãÇQ5zÈÊha9}út9''§A”Ñ T’ Ï;v &&¦Ø+•/_ÙÙÙÈÎÎffÓÙÙ‹bbиqcf6…pÿþ}Œ Áßÿm‘ýM¥W¯^7~¼$E~xÂ;Àó±¾¾¾y“ºråŠÓ£G†‚Þaaó|R† Æôç¸(ðÉ'Ÿ 2*JÐÌQ*•’hì¿ Ž;&/ãЕ?=ŠÐÐP)ü²iš6k†eË–±¸cÕXOy~–¯¯¯Å¤äry/Ji(Q°¶¯Ñh0aÂ{ú!%K–ÄÒØX4hÐÀ"ûßJNÆÈ‘#mr„¯‹‹ ¦M›†ö:Ha>”.å8n%«+Ó"0BBFƤðð"3(ɸ¸¸`åªUP*•ù>_¸p!âââððáCøû3-Ì£”bñâÅXms÷¾¥J•ÂÒØX 2„e‹ßJHw•RÙÞ×××&çdû*{)Ï· º+ÁÕÕsçͳÿ?'11Qp,áyI*x9àY A¯®‰‰‚Æi1mÚ4ñ_”þÃëõMU*•MU(Šs¾Je+JH€G,löêÕ ££áä$‰vI‘ÂÅÅ«V­‚B‘}æªU«°cûö—ÿ?338Çà*çyÌ›;[6o6Ù–¹©\¹2¶lÝŠæÍ›³2IAH(õöU(ö²2j)|}}Oñz}S¢ûR}}}ѽ{w†^™‡5j0·™‘‘ëׯ [$⊾0pàîî^û¼Y|iúõï///±Ë¯>utl^¯^½+ ]²$ÔW¡X%ã8%«lÀKÙÚÒ¥Y˜+’888`QL | þ»wïÆÚ5kÞúóA€)}^^¦L™‚½{mï¬ûðñyËvýý„Ü$@[•B1šïù…R¯^½+”ç›Ál/=ï¼Ã¤ËÐløøøHò½#BûýS§N•dí„ÚBþs¥¨œW¦Q¶lY :Tìò!Ÿ5ðö¾ËÒ'kÀÇÇ窯RÙ”NƒÚ¥R‰M›7£råÊ ¼+Zp‡¹óæ¡Q£ü_þøãDEF\ÿèÑ#"Q„.nN&Œo“ÁZµj…¸uëXŽäÞ¦úT¡T*eeКðõõ½'ç¸öH³ÞÅÅ#ƒƒY»%)©©©hÛ¶-s»:áây2æƒ8ày^Ð`gžçq3YÔÏ@‘cøðáb«þs8B¾T(YûdEð*•*Šò|+7L5æåå…M›7ãý÷ßgàZÑ!lâD|òIþ3¼®^½Š‰aaŠu=zôAAA‚‚€ììlŒ5 Çä¯5ЫW/Ä,^Œ’%™¼T¥ —J©àççg{m¨[·n2x¾(UÌøù矣fÍš¬Ý’ŒÛ·o£i³fÌíž¿ ¼ƒZÔeídzÁ„ îܹcs¾RP®\9Á²Ž/ @ˆTòŽÖ†¯¯ï)Çù‚R“ç¾–/_ëÖ¯GݺÌl’á#F,>MOOÇèýì3áÑ ¥:¥R¹Gl??¿ ‡Ï@ÈSìB0nÜ8 1‚•kVÇq˜¿`ÁH½^°ÐP1oòøñcܹs‡¹])yÑ-âãcò  J'ª”J???ëýR²,”çùiBùùùÙÄ5À‹V†äµÅ"&“.æ…Ýå8A)¥’O»eåM›6¼†²&V¿U¼½½sU EQ¦Ú ‚@¯¬‹1cÇ¢E‹Ÿ›RôWÔ¨\¹26nÜÈB/"—U*•É?—E__ߟê •Ëå¨o¡QÕBx˜™ @‚ %EðBHþé?‘p¥…,ÈÈȰê·s©qtt48eÍÙyyy_KáO‚ª”ʉ t,Lì@`` #·,O—®]VM³(ú+*T¯^6n„‡§§©¦2AéçJ¥r ¿ŠÏÇ ¢±õJkâáÃg KJáßûFÉÌÌ<Ó” GA¥ìRÜÿÛ …Bpï?öu…0V¨Tª%À¤Z‰þþ6%8bˆúõëc²‘v?–E¶N­Zµ°~Óõæ p—­T*•à·âŒƒƒÃÙBÖ|ø!óÖvæ<|~P±bE”*UŠ™]žç‘›+¬‹”ÂÎ(u² óy:¤¸"² Èþz&¥R¹”ö`RõðáÃ1jÔ(F^™OOODGGC.—çû\Š¢?[¥fÍšX½f Ê”)cª©ëy2Ys¥R)|h{1ÇÛÛû9&dM­Úµ™W׳æE»¡QÏyòø±Ð%l3 ÄMÈ‚ì¬,–ûÛ~~~B—ääåå É_–¨Tª=”㾄À7Š7ùjèP„ŒÍÈ+óñ¢â¿´M¯×cbX˜$E¶F5°fíZƒÝ¸$—ÉZúÕ­û ¿Š)‚!999¡zõêRù„WßÒYŸ<ôy`› €  @V¶IßÇ6Ou¡ó¡ ùÓÏϯxÿG‰¯ÏaŽLº>2dˆM]B0köl£_Œ‹cbð×_™Ñ+ë¤zõê¬ÿ$ÊóÕ­[׺+­"B×ÃÚ€W5û«1öõ‰ÀcÀž($nnnbRŒEr4¨¹P('9BÚ0©÷tøðáèß¿?#¯¤eð!FERŽ9‚íö¢?xU«†µkך<;„ ú¼¼V¾¾¾·¹VlÉupxš^°))úW//¦¶…^ßBD Ÿ1Œ= Q?¨}ŠÙ³fAo¥?¿Ó¦OGíÚµ >ß¾};~úé'3zd}xxz"nÝ:}þ»ôz}÷÷ßßÞ?)”A­)î T)x3#Ç2 öyáÊEE¡@ ä4ž?ÔjuÂó;äß_ËbcqþüyÆ^±¡Oß¾FGÕªÕj,Y¼ØŒY•+WF\\‹áÛÒÒÒØuý¥ƒ}ïËzY%K–|íÿ3ÍO¦0…5ßÕHMFºàÌ–=‰F£B>³öÞ½{˜:eŠUyxx`öœ9ñ?~Œ ãÇ#«˜ nB0mút´jÝÚÛ9mj­v:aßÄv BÔØoí?ëÞœb–ý2¡”yðHÈ‚%J°ÜߦH~—\>!!lT1A§ÓÕ!sŬåyS§NŃÖ×éè舨¨(£z¸r劽².ѹsgsnI0K£Õn8vìXñ}»aÏó‚Ú5îß¿/•+LxçÖS–™9ÁBØ^~½yb.„þ‡’‚‡ÂGÃê.vÞ"!!Á™Rº*_°zÕ*œŽgìBÃÂPÇÈö®]»pèàA3zd]tíÚÃGŒ°ÔöƒË”-ûͱë׋ï;¼„|ø¿”‰Ü`ƒÉª“F°† €M’Þ’[·n ^Ãó¼BWŠ,r¹|>³öÏ?ÿÄúõëY»Ä„Ž;¢{÷îŸët:Ä,ZdF¬‹-Z`ò”)–u‚ÒÎe>ü1>>Þ^¼ku…|ø®•o*O²Ìp_l !LAlæ  kWË·ï^¹rE¸p!ö ¨uº/( fmjjªÕÞûרQS¦N5ø<==ÃÂðôiñ§ûðÃe­`”¶r*Qâç„„“' W(!†Û[òáÆõëy†·¤§Yç Ïl3­ æˆÀÀ€Ò¥K£Ý'ŸÀ‹]?°(²²²ðï¿Â&Ò …Dî)*J׉YËó<&‡‡[å]¢³³3FGü½áyS¦LÁÝ»‚äÓ‹ Xk±Ì¢ÈNœ;wκÕ[!:®ýw»xé’Tî˜ Çqo]°Ãst|ÓÉô-£” º°D C‡Édøì³Ï̾÷›\ºxQè’FjµZºK¤¢áäòDýwZ³f þúë/Æ.±aÚ´iF…l6lØ€?~ÿÝŒY¥K—ƲåËŠ!Y Jëäñüo ‰‰¢®£Š+<Ï,tˆïT³Q©R¥·é‡BÛÁ â`DY ÙÙÙ„ë×®½õìò¥K4p ®åóL N d«F£™ »jà[èt: úŽËÍÍÅü!•KLò  D‰‚u(Ç1¾„·¤¥ââÅ‹ |yàþûï¿9r$Ͳ¿!ŽŸ8!x !Ä?))ÉüwVJbb¢(.fíÍ›7±páBÖ.1᫯¾BóæÍ >ÿã÷ß­V«@j>úè#„††2µ©×ë11, III?sçÎ <:ÎÔ홡Ñé¶Ùƒ^‡B¼Sà_áÔ°¸¢‘ ™L–o/«@ŒÀGi&“Í_Ø#„ óÔ¼€Rúò sݺux"t"ŸüñûïÈ˦Ç@÷rss‡Iä’M‘àÇó›N%åååarx8?~ÌÞ1iبƒ‚ >OII±Úš©ñQ(0ÁÁo<±`þ|üöÛo~.###†ÇñcÇLߔҾe22~¶w÷‡àäòpÔ³vÍêÕøûï¿Y»d2ï¼óæÏŸoð€ËËËÃİ0«®x– OOO,]ºô­/SSÙ¸q#¾ùæ›B>''ãÇÇwß}Çbûf„ãþÔétæ-B²B!Ñ ÊHOÇ/ÿûŸD±!¿"Õ[ÉÉÌøwߘ1PˆÀš½‚à|||ÒzªT©K „çy|»¿U}yîÚ¹S̲ò”i¬}±%4Š¢4_ÕjµUŽø•Ë刌ŠBÙ²†‡?ÆÆÆB«ÕšÑ+ë L™2X¾bÜÝräDZ,6Vð:½^Y3gbëÖ­,ܨÎSú§Z§ëØ-¢Ñh>!„®Ûÿí·ÈÍÉ‘Â%fä\5½–ä%"€ìçç53ž½®P*( P½zu–> FQ;3Μ9ƒË"¬|öܹb)|ìØ19Ùƒ~=zdµR¿!£GC¥R|~ìØ1lcsàØNNNX‹*Uª0µ{öìYLŸ>]´&;¥1‹aùòå,Üq!”îÐjµK‹ÛäOFãB¶B`Q¤^¯ÇžÝ»%òŠ „øÖ{;I™_¡©XDœ£Lßþ è$³DpçγïY;wí³ÌQ¦×””Tе?ÖN™²eÇ0|Ra~Dnßfzýń֭[£ÿþŸß¹s3gÌ`:@Äà8ó"" P°u“““1aüxäææšlkýºu˜7o“ ’!2¹üWµZmÞô¨…8výz BÈ7 §½ pðàA«ü]~•ªU«æ›Ñc™¨Y«– ÏS ™ÙæÏá€b¸„6ŒI›'~øþ{Q×ø 77w….Y- U@éL1k9‚~ø±G¦S¥JÌ7Ïà¨êÜÜ\L?ÂÇHÛ<ãÇGÛ¶m™Ú|ðà‚™*±}³g&‡‡³ÄÔŒpÜYµZ-X×Ö(óða,ü„®ËËËÃúu¢F~˜?¿üÿj×®^ebßP‡A:§ Ë € î®®’ÎH¶rrrÄßI2P­ÕŠš|g‹Èd²e\„®{ðàÌŸ/G¦Q¢D DGGÃÅÅð_iQt´Ñö´¢JÿþýÑ·[Íœ'Ož`tHˆ¨‘ÜqäÈ„Œ…¬,&+ïŽûI£ÕÆÕVAµV; ”³vß¾}’ü²¦UëÖoý¥7nÜ`bßËËK°’-0½á€çùóBZâÀùúë¯EOr#À FÓ—±KV‡F£ù„ˆ’CŽˆˆ°ªâÏ„Ožl4…wäÈì¶ò{N)ø¸];Œ7Ž©ÍÓ¥ýúóÏ?1xÐ VWÀØ2g4¨+/kE­Ó…@TDž‘žŽ•+¬?ñéììŒF ¾õçwïÞe$¢Žˆ™6”RÉ€$œ3œŸFrq$7'K–,»œ!›4Mg–>YIII¥(!Â˵ñìýåçŸY»d2]ºvÅ_Žg®_¿Ž9³g›Ñ#ë@¥RaÞܹÌ{ýÇĘEõñÊ•+0`Ë6Ó !§´ZíXü¶ÕfÑh4A„RÑ_vË—/·‰ë°&M›æ«v«cØÅc¬hØóþgüüü2 °ÂÐÜüÖÌ‘|K²X ä뢪3þôéÓ™BçƒÖ›úÿàƒ0iâDƒÏsrrÊìMÁV¨Zµ*–,YÂ\&çVh„ IDAT|×®]ضmS›Æ¸ŸšŠaC‡â—_~ab%(£ÑjOhµÚ™µf8Y‘2ÈgΜÁ¾}û{% å“þžuŸ°BDpS¥R1O…¾• º¬ñ(ÒÌ›7ÙÙÙb—;m.jÏž=‚†iX :ÎE­ÓuÓètk5ZíYVûF«ý‡cÄØ³ÆÔ¿««+.\hô[0>._¾lF¯,OÙ²e±|Å ”.ÃVãêĉXÅÔfaxòä ÂBC±eóf–f›Q@­ÑéæÚXm§Öj€µyø?zôÓ§M³ÊÞ7‘ÉdhÞ¢E¾ÏÔj5“=ÜÜÜà“ÍßàÕ@#da¥J•DIUn%'#::Ú4#”†ÖªUëÐéӧ˱ñJz«ktºµ<¥÷¥ß Ú¯ÞfÆŒ¸jz¯hEžÒ­vcBBÂÛá¨IHH¨¨ÕjcóôúËF`ª~¶`þ|¦}Þ,ðóóCð¨QŸggg#44Ô*U™“°‰óm—2…¬¬,„Œ…ÔÔT¦vMáüùóèÛ§ûѵ”~F­V«]g-Bñññnn9 ¶)¶´Z-¢­tjg~¸»»£eË–ù>S3¼ÿoÔ¸±Ð%™/^”DGüeàëë{€ ËKU>ZÉŬ¬,Œ;lî¯ËäòsZ­¶= c¦ Óé<4ÍB™\~…£ b’_Aüñûï8zô(k³&Q®|y,ˆŒ„Lfø&cîœ9L%BmAƒ¡W¯^Lmæåå!t«¬¡ÈÈÈÀ¨à`ÄÅűVu”Q`(á¸ËZ­6V£Ñx±4^XŽ]¿^B­Ó:•(ñ7( ‚‰] ·’“1~Ü8VKf¡cÇŽppÈÿ}æÌ™3Löpww‡·Ð@Bâ{ôè¡gâÀ¼ù,èž¡víÚpuueèNÑàÆ aõFèA5Zí~NgRD.”„„‡³:]­Vû Oé52€³{åææ"Ê_ÆÉdˆŒŒ4:þú›o¾ÁáÇÍè•åiß¾=BFfn7""§NIRëÄžç±rÅ Œ=ZŠUg Œ!—5Í­Vk–·«‹/ºj4šP÷ŒŒk„Ò•L.æxðà‚‚‚pÿþ}šCéÿÿþûYkh“&M„ßÿó¼$÷ÿÀ%DÐF2™ Íš7gëQA§Ó!,4”eü%Oéßj­v¯F£i'ÕàµZ]A­ÓuWkµëerù]ŽÒÃè@.Å~/ضu+3•-VŒ 6( M›5¼†ã86=©ù@^ý‹={¶'“ jüñða„‡‡3w¬¨Ð´Y3,މaÞ ” &„$Bîäå奿ååý×°aÃCï‹/ºfgg{R™¬ Çó>êRB| P"Û}Ä’’’‚®]ºXEÑ× Z¶l‰%K—ÔùÏÊÊBÿ~ýpýúuó:fAªU«†›6å[%m GŤ‰m¢MìU!èÚµ+&„†¢D I;ûôNRà"!äxþ&€ÿx¹<…æä¤;::fÞwu}ÜÚËëIBB‚³£££S^^^I¹\^N¯×—£÷GiU T!ÕAiˆå.ˆÌÌLJªØ( .ÄÇíòoÌ6t(LÞƒã8ýùg£cÃó!#=-­|ëÖ­óLv È‘ Ñhµwº¿ïáÇhÛ¦ òò$ñ¯HШQ#,Љ1ªϘ§îxBr¥Ù@w 8g¼ÌæLAL ÃO?ýdi7^âááí;vÀÍÍ-ßç”R„N˜ÀL,Æ(W¾<¶lÙ‚J•ØÖª©ÕjXýlxc¼ÿþûˆˆˆ<Ý­(‘‘žŽÀ  œ·ÁÙ+VÄÁC‡ò­óyðàÚ}ü1“àÔÇÇ[ާÀ>_¥²›É›àÍ< hìš››|íÅ€F‰Ç!C’’b®-TP”Ö¡€ßóÉ]ÕŸ«òYÍáúôi«:üœµp¡ÁÃvîØQ¬ggg,‹e~øß¼yãÆŽµéÃxÖÚÀ¬[·®X¾ݺu ƒ¶ÉÃúôéc°È÷رcÌ2Sí dŒA(ý‘Éæxë"rH¨‘–­Z±ñ¦sùÒ%ôï×g¤’Šk׬±´ ¯>iêÔ©cðybb¢)³l™L†‘‘¢†—#==£‚ƒ­NíQ,¹99X±|9z÷êF¦šM£Õj1hà@›½ sqqA—®] >g%HFA»O>¼N¯×aâ€Þ œäò#…ä­[µ2xWjçÿIMMň#°qۻ‹/2¹[cE·nÝðe—.Ÿg¤§cbX˜Mµ6™Ê¤ðp´0 *–ÜœŒ37oÞdj×øçŸðÕ!˜;g233-펤ìÚ¹Ç Ã,íŠhzôèR¥Jåûì~j*NŸ>Íd¼÷žà:Ë¿ýüü$ý%y+ðöö~à„#žžP(Ìœ*ÊèõzÄÆÆbÄðáø÷_Aó—lŠøøøÇ$ïØ¾ÝLÞŒB0#C~(¥˜6mšÍHš²à«¡Cѽ{w¦6)¥˜>cF‘~K¦”bïÞ½èÒ¥ > Àİ0”uw7ø¹¬¬,üxDÒìV¡)W®¢££áèèhð3›6mÂÉ“'Íè•eùì³ÏÌÜîòåËqäGI¯5­†û©©˜6múôî-UË Ùùõ×_ѽ{wüle‚]bè׿¿ÁV<ÏcßÞ½Löá8ü±àu”Ò¯™8`„ü›yyþ€PCíÛ·‡ûV·"MVVæÎ™ƒaC‡Z¥ú™Ôj5††$&&âóÏ?7ÚúoÅ_r¹‘QQF[MHÀŠåËÍè•eiРfΚÅüZoÿ¾}ذ~=S›¶À¥K—02(þþþ6[(—œœŒQ£0nìXÜ·"™f±¸¹¹¡ÿþŸŸ8q¢À fa©ß Þ}÷]a‹(ýG¥RIž&Ë7ðõõ½AD!†J•*…Ö}ÄÆ«bÆÙ³gѧwoÄ,Z„ŒŒ K»#ˆsçÎ!88_ òš\f×nÆ;W¬¥rìØ±FÅ~ýúõChh(}½ZŒû©©ˆŠŠB÷nÝŠTlРAFUl÷ìÙÃl¯®Fꊌ ùÛ?`DݶS`c_|ñE±Iï±F¯×cëÖ­Ø¿?úõïþýû,N±4OŸ>ÅÑ£G±k×.$êto=W©T¨^½ºQ/^”ʽBóé§Ÿ¢o¿~Ÿó<Éáá¸wïž½²*TÀò+˜Ë{_¾|¡&˹7¡”âç£GñóÑ£ðõõÅÀAƒÐ²eKæÊЦòï¿ÿbÇŽØ·wo‘rU¹reô3òö+92’¤.]º4>jÓFð:Jén&€Á€çù­„ãæAÀ<÷ÆãwÞÁÿýÇĹâÈ£G°fõjlß¶ ¾ø=zô@µjÕ,íxž‡F£ÁÏ?ÿŒŸŽ1ªóÝ­€·¬Ç« ¦V­Z˜>c†ÑϬ]»ñññfòȲ¸¸¸ vÙ21•ÊFIMMEȨQÈÊÊbj·( V«¡V«QµjUôìÙíÛ·G9#s'¤F¯×ã÷ßÇwß~Ë´ÿÝÚ?a‚ÑëêÝ{ö0û»wüüs£µE¸äëë+Éô¿7yS ð54Z킚׮YƒU«V™ê—çB R©ðÑG¡õGÁÓÓÓl{§§§#áÌÄŸ>c¿þZ¨7a777ütôhõ Íš6µ˜ô¯››¶ïØÃsOþøãŒ .²_‚¯"—˱46M›6ej÷ñãÇúÕW8þOKKÃç;2ûnÚ½gÁ!d¦J¡˜ÅÄ‚¶2h4ý@È6!ÓÒÒði‡ȱ‚¯¢ˆ‡§'>>ðQ(ðÁ J•*Bµ¥ó%#=—¯\Á•+Wpåòeèt:\¹rEp‡Bÿþý1~„?×´I<~üX¬»¢á8Ë–-3:”ãÎ;èÓ»·ÍÕcˆeÆŒFõÄÀó<ÆŽƒ'uÛyŽ““”J% êx{ÃÃÕ+W6)(ÈÊÊÂíÛ·ñï¿ÿâÂ… øûÜ9èt:<|ø¡çÖ‹ƒƒvïÙ///ƒŸ‰‰‰ÁÖ-[˜ì§T*±ióf¡ËxÊóÕ}}}Í2Íè„·Çï+é유ÐÓ?ÜÝÝѱcG쳑É^¶Æ­ädÜJNÆ?ü¿b³‹‹ *Uª„råÊ¡lÙ²(ãî'GG¸>—³•qÇ!//™™™ÐëõHOKCjj*RRR’’‚G™ì!=zö,Ôg]]]-„ŒmôðÏÍÍÅ„ñã‹Íá?Âߟùá‹-²ÊÃßÁÁÁ&z×srrpúôé·„h\\\àîî777¸ºº¢dÉ’/ 6‘›› àY–ùè233‘ùð!ÒÒÒŠý5Ìà!CŒþ©©©Øý5»Ú;c]† ÀO*3þ@@“&Mk´Ú}†1Ú¯ìß¿¿Hõ·[3YYY¸|ù²Å[ 7nŒ*Uªê³+V4{­ÈgŸ}†AƒýLTd$’l´UK(Ÿwê„€€æv¿Ù³ÇªDž^ÅG¡Àµ«W‘––fiWD‘••Uìr1Ô®]Ç7ú™¸µk™e®=<<Цm[ÁëxBÌÚ'[`é)GÈF¡F«W¯Ž&ŒïíX?B†]xø¡„ž¼Í‡~X`Ñß°—‘ø‡µÓ¨Q#̘1C o…¢ÅlÆð®SÇì?{v,‹££#æÌk´µõòåËL÷ûõë'¦³ãž“\.XƒÇ ôP¡Pœ jXLúÃŽmÓ¢eËB¶¹‘4¼¬XP`!^Td$3­777|ѹ³àuØâííËĉBR¸…Ò¥B 7nܵŠñ|ìↃƒÊ xëkب‘Qõ=V8::"&&Æè^™™™˜0aB±(\õôôIJåË%¯0÷ðôÄÆM›PÓŠ¾¼¼¼àW¿>ÚµkgggK»cÇ ¨T* 2¢÷GŽyMÄÌTº÷è!æç‹'„Ä1s¢*pttÜ @ÐBü%¸_´cp'¨æC.—£ÿ€zôŒ)S§ÂÇÈ *J)¦MŠ[ÉÉ’ûbiÊ•+‡•+W¢\¹rfÙ¯|ùòX¿~=êÕ«g–ýŒáææ†iÓ§C&“¡t™2˜={6ÜžÉÚ)š¸»»cAd¤ÑT|vv6–,^ÌlOgggqÙoJ) ³«£*ðööÎ¥€àæþ>úµk×î•›#''·nÝ´¦wïÞ’Š >_0¤jýúõ8~ü¸d>X ...X¶|9<̨#<ëöX¹jZY íîââ‚F!|òdÏËË 'Nœ@jW˜²TªT ›4´¦\¹røàƒðóÑ£LT÷är9ÆŒ £Ÿ»{÷.‚-¢E`N8ŽÃ¼ˆ‹¼¿éGëÖ­ñíÝwXTWúð﹃‚±Ò\1*°,˜bO²jbÃA;v4öŠ%vQl‰‰FŒŠ-AcVÅ;œBÄ€ 5¢ "úÌœßF&«ÈÌœ;ûyž<ûÄûž³›ÌÜ÷ž{ÎûC¥â¿Êiii)®\¹‚3gÎ`ûöí¸œ™‰€€€{²®_Lj#°~ýz¤¦¤àæÍ›•¦á“­kÛ¶-¦N›Vî —’’D…üü|fã~þùçzm„~ŽP:ÑÍÍÍ,Ý¡ô:§@Y @¯_ia ò0´äkÛ¶m±6.ÎèeØúõëãÛ-[ðÙö”••aRt´Õž×Gtt4:wîÌ4¦F£1¨Æ!ãÆÃøñãy9~XžcÇŽaRt4€gåo'Ož,”(¶A5ÂÂE‹Þx/..7n°«·S­Z5C÷4ý©ÕjÙµÔ“^ €T*½Jõ.ñ‚-Zè{™ÀÊó…jÙ²%v%$ ½´ØÞ¡àû;мg¼—.Yb5íX18"ýú÷g³¤¤cǎżyó ^±ùlà@,X°€yËá7Q*•¸”žŽS§N!33Ó¤c øW³V-Ä®YóÆ.ªRS±í»ï˜ŽnØ ¥ËÍVšRïJ„ÙÐsÆO˜`-/-¡³ž­ÊÍÍ5ªúc:u°jõjlذ¡B £££#>8Dtt4ªV­úÆk$&2}ïg©>ùäŒ|Ã}ctTNŸ:…}{÷bÂøñìÒµ+Ö¬]k’†7/K:v¬Rlú¬l°jÕ*¸»»—û¹'Ož`Ú´iL›|yzzb@9mÅËq§¨¨h³‰ Üf@¯£T«wÒÞú^7sÆ $&&ê=+"‘«V­ÂèÑ£+E—7sH½pY¢wõêU]Ý¿¡ýû›ìFc.!:`éÒ¥L«üét:L2åµ?œÍš5úuëàâêjPü;wî jÔ(\½zÕ˜iVˆ““JJJ ÑhxKÀ?B¦NŠO{¿ùy4113gÌ`:¾\.Ç×›7²§åNQaa£   ³îB6èA.—§Ø­ïuuêÔycU&>hµZ(.\@rr2t:Μ>”ÔTáæÏ˜¹§ðJ¥¥¥?~¼Íßü[·n//fzó§”bá‚å>5]ºt ‘‘‘xðàAc¸»»cËÖ­åvid¥  @¸ùÛ1cÇVèæŸ••…Å‹1›ã8LŒŽ6lC+¥1æ¾ù& ÓjgÐû›4hÐ Ô3q1(**Âöøx|ýõר³gÍ/›ƒ!øL!fþ|¤©Õ枯¤R)V¬\ 1ã³ìkÖ¬ÁîÝoÎõ/_¾ŒˆÁƒ‘““cÐ8NNNˆE¿~ý º^PùDòÆîžÀ³ßþ艙wQìÕ«|}}õ¾Ž·ªU«öÓÉÈà àÙ¨ïuööö˜2uª¡Ã-~Û6³mËD":1>nÆB||<~üѤ ¶L®iÓ¦ˆ]³¦B› õñÝÖ­ØüuÅ»“fee!bð`dgg4Çq˜üŘ2u*D"‘A1•À0bĈ }vÞܹ¸rå ÓñkÕª…ÑcÆzù4‹x5j—}¶ ðHßë‚‚‚Ð¥kWc†6Ø£GzOWPtì777sOãoÎ;‡ËÍRaÓd6lˆ¸uë Þ„÷:À j¤ÿù矎›7o}»fPžWðJ½ûôÁ„‰+ôÙ;wâСCÌç=i’¡ß9…\*g=C•Èåò{ t!×Nœ0ù–À<8ŽCxx¸¹§ñ7Þº…/&O¶éênžžžX¿aƒAµÇËsúÔ)Ìž=Ûà#wïÞÅàðp\»vÍà9a[|¼ÐQTð7Ÿ ˆ)S¦Tè½»J¥Â²¥K™Ï¡]»vèÔ©“A×r„ŒÇèùbô9!±X¼€Þë+5kÕ˜±c^`ºvëfQ?Ô;v¬M¯ö¸»»cã¦MÌ[*§©ÕˆŽŽ6z£Üýû÷1x022 opæåå…-[·¢«™V –eè°a®"™ñãÆ¡¬ŒmGGGÃ_a’ ‘HN2‘ŒN|}}KA©AÿôêÕË$;üÛÛ3/8c ­V‹)_|a’#eæâî¾þLã^¿~QQQÌú#<|øC"#Ú€éàà€ "::ZØPIB0vÜ8 «`Iù‚‚Œ3yyyÌç2nüø7z!_°ž±˜T ‘Éd 䨾×B0{ölá]Ÿë߯Ÿ¡_æ(¥˜?>Nž´¨$›)¾nþ9991|8óU“ÇcøðáHII1*Nè€ظi“Åí3ðËÎÎ3f̨ÐnàYÍŠ)_|Ë—/3ŸKHH>ýôSÃ.¦t¹¿¿¿áïÄx¬T˜N£N½+üÔ®]S¦La5 ¹¸¸ |ð`sOã…uqqØ¿oŸ¹§Á¾nþ°vͦ}Ñ_VPP€Q#GþOA}`ÇŽéÐÍÄÍÙÙkÖ®EÏ^½*|ÍÒ¥Kyy¨Q£fΚeèåYÇŰœ+nü&îîîywrr8¼¹ã?øøø ++ WÕðkTTZ¶liîi€RŠÕ«VaóæÍæž oø¼ù@‹–-qöìYÞÚvkµZ9rÞÁÛÛÛà8èܹ3\\\ðÛo¿Ùô&ÏÊÌÓÓ6n„¿¿…¯Ù´q#¾áá7€‚E‹£iÓ¦† ´T*ýƒí¬Ø`ÚǾJ•ÅÒ ¹vÊ”)³”,x3ooo³myüø1âããÑ«gO|ûí·f Ÿø¾ùÏž¶âÖ­C“&Mx£¬¬ “'M2º.!ýCC±eëV4š{Ù©TŠ­ß}§W¢¸k×.ÄÅÅñ2ŸO{÷6¸È¾“ÉdGO‰ƒz”G­V·ÓQú+½ë#¦¥¥apx8ó›ö6nÚdò§ÿ²²2¤©Õ8—œŒäsçpñâE›4ÅÍÿeOŸ>Ÿ±c~g_BFÅäõQII ÖÄÆbûöí6SÚÛÞÞÞà.‹ÖŒ‚ÐÐPŒ;V¯VÑ?:ļÃßsÍ|}ñí7ßVa“ÒÇùJ$’\æc„yJ•j€!†\ûýöíøòË/ÏHÀRç.]°ˆq]í×ÉÊÊ™ӧqîÜ9¤¦¦¢°°Ð$ãZ‚zõêaÃÆ&_+-)ÁÔ©S‘””Äë8ýú÷Gtt4“®…RS1kÖ,üùçŸ ff^ÇǦM›*UÏ‚êÕ«cÎܹè çþŽC‡aÆôé¼<¸¸¸ ~ûvxzz€Ò0™L¶…í¬Øâ%P«ÕN:Jr}tt4ޱØU“JÍÉÉ {÷íc~þüe¥¥¥HJJ®;¡T*yÇ’5nÜëÖ­CÍZµÌ2¾N§Ã‚˜ìÙ³‡×q:vìˆù11‹ÅFÇ*,,ÄÊ•+±;!Áà"Fæäàà€ððp„ „ßΟǼyóxÛ˜iIüüü°øË/õ^åÚ·w/æÏŸÏË“?ÇqXµz5Ú¶mkXB~I$=ØÎŠ=^P*•m@ȯô>¼[PP€¡¡¸qã3cܸqXÁ#9†ÈÈÈ@ôĉ¸uëocX:¥p)J(à`L6`ýúõ¼ÞP[´h¥Ë–1;¬T*± &†—£`|‹ÅX‹¹sæÜSÁZˆÅbDDD`Px¸Þ,·ÇÇcéÒ¥¼ý{9dH…{ ¼BÕéüår¹Å·å-•Jµ?Ⱥ~ÐÑø IDATÄ“'OXOK` oooìܵ‹i»Ù—¥¤¤`ì˜1Ì»vY“V­ZaÅÊ•ptt4& ¥Àèt—8ŽK46 8|ø0fΘÁë{éúõëcÕêÕ¨_¿>“xÛ¶mÆõëQ\¬÷éd³rss³ù'Ìš=5Òë:N‡åË–!>ž¿rúíÛ·ÇŠ•+ }5E ð±T*=Àz^|`z àŸ4ÍLbÐn¢ bñ—_ Õ¿xDAÇŽáàP±ûÃä/¾àíæ!5Q£FUê›H‡ˆ5úæJ£äRi¬\.?J)íÚv¿¬S§Nøêë¯Q‹Ç×7nÜÀÀÏ>Ãùäd&ñìì솽ûöáÝwõ>™lV¶|ówppÀø ðí–-zßü 0fôh^oþMš4ÁÂE‹ŒÙ—g-7€a€WñððÐåæäœ¥À ßÖù—zõêÁÅÕ§Œ, "x=Ji…Î~wîÜŸ‡…ñ2‡´´4Œ9’Y ZkÔµkW,\´UŒ{®#@¤L&{ѦÛÍÍ-#''ç:€0àdÎsuêÔAçΑ’’Â[­€’’üüóϨQ£†A}Ö_ÅÙÙ»tAË–-qùÊÜ¿gñ«²6‰‚îÝ»cùŠhÓ¦M…êù¿ìöíÛ6t(¯{‚êÔ©ƒ›6Ü\‹h5šÞVsŒ×êÖ­››““sÀ¿ ¹ÞÏÏóòpñâEÆ3@~~þ?ãèèˆU«WÃÉɉùø>lž>}Ê<¶µèݧfΚeìj—”–Édßþó¿pssSßÉÉÉ%@w‘T«V Ý»wǵkו•eÄT_O§Óáä‰xðàÞyçf+€èÙ³'êyy!==½R¯4™Z@` –.]оýú¡Zµjz_ŸœœŒ‘#Fðº'ÂÑÑëÖ¯Gƒ QDuº®Vu …÷ÜÜÜwsrêr}pp0222„Mf2jÔ(^š6e]¿Ž!‘‘6ݵ¯<„DEEaô˜1z?ýƒ–.—J·¾îînn)%âF%UªTAÇNPVV¥Bah˜7JOOǙӧĬm8!o7i‚O{÷†‹‹ 233+õªß|||0uÚ4Œ;µk×ÖûzN‡¯6mœ9sxýç$‰°déRƒCe2ÙÏ §e¼n|Ùñ¬,×GÎÀÀ$ ¤¤#FŒÀ…ÔTÆ3”§iÓ¦ØÏ|/Æýû÷ñùÀ6¿ÓùuÄb1æÌ™ƒ.Æ·ºÕ L*•n«È‡•ju(Ýû8€¹óæ¡”ÇÍ...˜cøq¬r#!![¾ý<`¿²jæë‹ˆˆtèÐÁàwéòó1uÚ4œ9}šñìþŽã8Ì;Ý?üÐàX/•J‡3œ–ɘ,•Jõ6R”Ò?}úÔèりã8Û¶mC3FïcŸ+((@ÄàÁøã‹,Í;,_¾F)•Ê0² €Ñ;:ÓÓÓ1aüx^7®q‡ÏÃÂ0|øp½*ÄUTqq1öìÙƒï·oÇíÛ·™Ç¯,är9"""Œ^-¼šŠiÓ¦™d3äS¦ o߾Ƅø­š“S;«,ÝhÒjõ§„Ò]0pòáÇ4ˆ·w‚ÿ÷ÙÀ?~<Ó˜cFÆ™3g˜ÆµžžžX³f ‹öO©N×S.—ë݆ii]‰N· €þ/eÿ!??“'MÂùóç U®Æc~L Þ~ûm^âët:œm233ÌîÍ…‡côèÑÆ„(ãé"‘Hޱš“9˜%@”*ÕwàîÝ»ˆŒŒÄŸ6X1. ·nÝÂ=3YZ‡àà`¦1ãââ°iãÆ7н÷Þ{ˆY° Â5^‡wµ"Qç??5‹y©Õj/Jé~ ý>öïß…  ´´”E¸×jÕº5¦OŸŽzõêñ:Nqq1’’’ðã? %%ÅfÂÙÙïðºwïŽÀÀ@& ˜F£ÁÖ-[°aÃÞÿy.""#G2*†I¥Ò Œ¦d6æJžm |üø( 24FNN†DFâæÍ›,§fVµjÕÂò+pïÞ=Ìœ1Ã,Ç•ºwïŽù11Lcr¹AŒ òàÁ?~'~ý©©©6UW jÕªFH‡ a²Äÿ²§OŸ"võjìÞ½ÛdI!ÑÑÑèjl ™DÒ€MdfM@­Vûé(=Àà;΃5j.]ºÄpf•Óüùó:óO·o߯ÀÏ>C^^³˜Ö AƒX±b‹Í~!¿ŠééïïÿÐø`åS¨Õý¥›½ùC«ÕbMl,¶lÙÂûÊ!]»vEÔèÑps3zKƒ^´Z-ÒÒÒp>9ÉÉɸxñ"¯}øàííV­[#88­[µbú¤ÿœN§Ãþýû‡&|¸á8Ó§OGÏ^F/pÎwqù CƒÖÕ\¢fO@­V·ÖQzFìH.((@ôĉ8{ö,ÙU.AAAˆ[·ŽY¼‚‚|>p ®^½Ê,¦5 AÌ‚l*'Rº;ßÕõ3Sþè(•J8îPú/ñ~ýõWÌš9Ó$ŸÄöö0`ÌKåÊŠÐjµ¸|ù2.^¼ˆ‹/âRz:®_¿n1IØÞMš4AóæÍ!‘HвeK^û<À¹sç°|Ù2“wh¬Zµ*.Z„£âP ­¬¤äÝV­ZÙTÁ‹Hà‚ZÝ…£ôC×h4˜={6°š^ ÃÑÑ»àééÉ$žN§CTTï…<, !‘C†`èСÆ4ù”.”ÉdÓ˜üKªP(<ÇíЊE¼ììlLŠŽ6YIï·Þz C† A¯^½xyšÕ—N§Ã;wp#+ ×®_Ç­›7‘“›‹ÜÜ\ÜËÍE^^ÓåpBjÔ¬‰:µkÃËË 6D#oïgÿÙ¨‘IöLÏJ}¯‰5K?—ZµjaõêÕ,ê˜\.«R¥}K__›ëÒd1 ¨TªžH`pÙ9J)bccñÍæÍ gfû¦M›†O{÷foé’%¼ví²4o½õæÌ‹víÚ±§¥„DÉ%vË1¸råŠý“‚‚8„³ˆWVV†åË—cÇ÷ß³W!uêÔÁ ððg‰€q–x¥ÑhððáC<}úôo=~üøÅgž>}ú"I°³³{Ñ5Ò^,F5gg899¡Zµj¨Q£jÖ¬i²›ü«dffbÃúõøå—_̲ñ·Q£Fˆ]³îîîÆ†ú”¶“ÉdY ¦eq,*…J5Œq0r#Rbb"æÏŸÏk™R[„µqqÌÎU'&&bæŒLbYƒV­[#&&†Õ2êSÊq}äþþ‡XcA¡V&”.ƒÊ””„˜ùóñð!ï[^¨[·.…‡£g±"`«222°iãF;vÌl'~‚Û´ÁâÅ‹ j<ô9h/•JMSœÀ ,.•J5Š«adðÇ`ÜØ±6Ý_ÛXÕªUCÂîÝÌ6N]ºt ƒÂÂ,æ}'ŸD"""#1dÈ6KþÀm|$•Jùë²c ¥RÙ„ì W—WÈˢ$%%±Wao½õzôèÐxï]YPJqþüylÇÉ“'Ívã'„ ,, £¢¢X|ïëD¢÷YÕÛ°T™€R©Œ!ëadÓ’û÷ïcÂøñP«múŸ£ÁXîú¿ÿ>„†ò~þÛxyyaÁÂ…ð÷÷gªˆD=üüü,¶²•Z­öÒQº@KV1<ˆ//þÛR·)ØÛÛã£?ÆþóÔ¯_ߤcÛŠ‚‚ütð âããÍÞ©µzõê˜Ãäîr×Éßß?ÁÔ,šÅ& P«J7ÂÈ$ ´´K—.E®]ŒffÞ{ï=,[¾œI¬ÒÒRDFDTŠD«K×®˜>}:»]æ„l/*(ˆ ²øÞ´ðZJ?góÞ½{˜3gN›a£Çq Æ'Ÿ|‚öíÛ3ïzi‹”J%öïÛ‡#GŽ °°ÐÜÓAÓ¦M±tÙ26˜ ¹ î™LfÚã fbÑ (•ÊP²Fl |.éèQÌ;×äO–è­·ÞÂî={Xԥ̙=û÷ïgËRÕ¨Q“¿ø:ubRK€iR©t1«€¦¢R©†P`-í €}{÷bÙ²ef+ªS»vmôèÑ=zö„‡‡‡Yæ`©nß¾#GŽà‡~@Öõëæž€gÉ[hh(¢¢¢Xíë¸Îò¾D"±Œÿ&`ñ ¨Tª¾ØÀèÊwïÞÅÔ)S PXÜkV“Z²d >èØ‘I¬Ý»w#fþ|&±,UÇŽ1eêT–ó¨N××Ðn~–@¡P|@8.@V1ïÞ½‹Y³fá¼»ñq‰D‚Ž;¢sçΨYI÷ dggãø/¿àÈ‘#P©TUÆ»f­Z˜3{6Ú´mË*äïeUªt´Å£~届T*Õ»ØÀÅØXZ­ë×­ÃæÍ›+E=ïúð£0oÞ<&±.¥§#lÐ ›=mQ·n]L›>Õñ¾ç~׊D=ýü¬¾B’B¡ð „|BÚ³ŠI)ž}û°zÕ*“*ÇqhÙ²%:vê„6mÚ˜¼Ê )•–” õœ={gÏœÁ•+L[N0Ó¥kWL:ÎÎάBþ"â¸OLQiÓÒXM*•ª¹8D&-ÀÒÒÒ0{Ö,\»vE8«Ð AÄoßþâ ±1=z„Ðþým²-3!½zõÂØqãX'úÿ¸Àw„á‰ÄfŠÇ'$$ˆ7n<„Ì€‘ûu^öðáC,_¾°˜§Oooo#(8Fww4§ââb¤_¼•Z””¤¦¤XôéÚµkcÒ¤IÌV.„lÛÙ ñõõ­\JþbU ðâ‰ã‘°ˆWZZŠ 6`Ë·ßB«Õ²i±ÄööØöÝwhüöÛFÇÒét3z´Y*|ñ­qãÆ˜4y2Z´hÁ2lÆJ¥R›í‡¬T*»ØBj²Œ›ššŠ11—¨‹D"øøøÀßßÍýüмysx{{[äFÂ’’\»z—¯\Á¥K— V©™™ FcÇqèÝ»7FEE±LÆ)(+“ÉæÀ •6-…Õ%žž^£¬¬lÞg³gÍ2y­jSš3w.>þøc&±6mÚ„¸µk™Ä²®®®1b>ùôSVçúŸK§:]o¹\žÎ2¨%JMMý—ÝN ¼Ã2®F£A®]ذaƒÙ_ ”ÇÎÎ^^^hذ!êׯú ÀÃõjÕB:u˜®&ýSII ²³³‘}û6îܹƒììlܼu W._Æ­[·¬òugÓ¦M1mútøùù± [B€©TºePkd• ?~ÜÎÕÕu)Ã*¦F£ÁŽ;°aýz<}ú”UX‹>x0¢¢¢˜ÄJMMÅÈH«üAy;;;ôíÛC†eÞú„lထ¶´äÿ&éééâRf>(†¯àQ~>Ö­[‡Ý»w[劽½=êÔ©WWW899ÁÙÙNÕª¡š“œþç•‚££#Š‹‹¡ÓéPPPV‹ÂÂB"??ù!ÿáCäååÙTKâš5kbÄÈ‘èÑ£ëdü6(í#“Éΰ j­¬6xN©T†BÖQ€Ù˸÷ïcÕªU8`AïѳW/̘1ƒI©ßÇ£oŸ>6S]±mÛ¶˜0q"4hÀ:ôP%“ɶ°l- Eá¸o4`ûÚµkX‹_~ù…uh‰Åb„††""2’nŽ¿p„ô“H$¶_©¬‚¬>µZ ö±j_úœJ¥ÂâÅ‹q)ÝzWnC:tÀòåË™eÑ'NDÒQ«=¹ö‚L&ðáÃѺuköÁ 9«ÓhXæ6jJNN®nïà°À>â§©ÕX½z5RRRø/0ŽãЭ{w 6ŒYGÒ—PÄj4š‰e¬ƒ[3›H 55Õ³³ÛF€÷XÆ¥”âðáÈ[»7oÞdšwØ·?³£KûöîÅܹs™Ä2©TŠaÇãw˜¾¢~®„L—I$ËØÆûFT*U/ ¬Ú/;sæ 6¬__)*QÚB:vìˆaÇ£aÆ| ñ”†Éd2Û®Rf ›IþB”jõhPº@–5 öíÝ‹7âþýû,Có¦sçÎX´˜M‘¹Û·o£÷§Ÿ¢¨Èâ«Õ¾’T*ÅÐaÃÄ׿ƒÒÏd2™’¯¬R©t!‹ÁÓjœONÆÆ‘ššÊ×8ŽÃ{ï¿ÈÈH¼ÍàTÒ+ +qodk @¥Rµ¥„ij~%EEEرcâ·mÃX‡gjêÔ©èݧ“XãÆŽÅñãÇ™Ä2¥–-[bPx8Ÿ7~ (ýR,Ï©¬g‰õ¥R©>¤ÀzÌ×zŸ»pá¶nÙ‚“'OÚÌfU[ ‹ñá‡âó°0üë_ÌžŸ+%ÀÌÌÌÌ¥½{÷¶¾¢&d“ ðì¨`iYÙ×z𿤤û÷ïÇÖ-[,¶ζøx4oÞÜè8gΜÁÈ#ÌÈ4Э[7ôëß7æs¨ß@éá©_©©©.œÝbD‚ñI—ݸqÛã㑘˜hµ«W¶ÀÕÕ={õBhh(ßm˜/‚ÒÿßÉŠ±Ùà9…Z=Pº³"î/Óh4øùÐ!|óÍ7W¨äøñãpqu5*¥}z÷¶Ø² /óðð@Ÿ¾}Ñ£G¸¸]1º<)0ýJffœð„aœ jõ;¥ëÈøçÑ£Gøñ‡°wï^deeñ9”à%þþþèÓ§:uêĪaÏëh °ò¡‹Ëô ó9-±ù~ÿý÷ºeZmzñ5¥ÉÉÉØ¹cNœ8aöeG'''œ:}Úè8gNŸÆÈ‘#̈"‘ÁÁÁèѳ':tèÀúÌð«°‰FøùùÝâ{ J„S©TX€q!†¿£”âÂ… س{7’Ž³ÙæT½zutêܽzöD3__S ©$ÀP©TzÞƒÙ’J‘<§R©þCUØôÀ};wî !!ûöîE~~>ŸC½V“&M°cçN£ã :ÉfìÌö:þþþèÖ­:wé²C_y²8BFK$’DS V) Âq‹ `|ÑŠ7xòä ’’’pè§Ÿ’’bö¤Ýš=OÄ?üè#t áûiÿ¹'”éW22Ö +q†©T ü–žîVE£YJûƒç™Ò’;v ‰àÜÙ³&ýiÛ¶-b׬1øúË™™8þë¯Uî×ËË ÝºwG·nÝP¿~}S û„,̯^}…°´h*•ª%d%(åmçæ?Ý»w‡Æ/ÇŽA¡PÉ@ˆD"´lÙïðÞ{ï=Ô¨ÁësÕË(v‰™(‘Hþ4Õ ¶¨Ò%Ï)Šp\,üM1^nn.~úé'$þø£Iö üûßÿÆì9s*üù¢¢"œONÆ©S§pêÔ)‹¨ôÇqšûù¡}ûöh×®š4ibÊáµ ds™ÝÌÊÖ#ÜB…ZÝ—‹ù8ÍSžüü|œ8qÇùçÏŸ·©»ÆrttD«V­Ò¡ÞíÐÁè=Fz#$:ÝX™LfüûMAåM€¿ú Ô¨1”ÎÀë®±—]¼x?ýôŽ9‚Ü\~ªRV¤öÿÝ»wqâÄ œ8q¿ýö›E¼uvvFPPÚµk‡6mÛšjyÿo(pŒP:AØIl~éééâ²²²0s(À¦¢•´Z-Ôj5’““qîìY\¼xÑ*:è±B‚ƒƒܦ är9ªTaZb¥¢² 0'33óka¹ŸJ<÷[zº›]YY >`²^ž:J¥GÆÑ£G™ŠŽŽFè€ÿóç9998˜ˆÿ>ŒË™™ÌÆ3”‹‹ ¤R)äËåðóó3_;ÕgO³d2ÙO晀àu222œ‹JJƃÒñày£`yŠ‹‹qñâE( ¨”J¤¥¥YtwB}‰D"4kÖ r¹üÅwÒÕÔOù—B–rÀêÊÔPËT„à% …—ˆD @é¿M=¶N§ƒB¡@RRNž<‰?o·É|ÑâÅèܹ3€g{’ŽCâ?"99Ù¬ï7ÝÝÝ!—Ë!“É €···)vî—Bé©Tz•¸7¸5HMM­egg7#8›{>?þøÈÈÈÀÕ«W‘}û¶Åï#‰DðööF³fÍÐÌ×Íš5C“&Mþ§#¡™éÑIDATr Ì<-c]¥©à¸ „ÒÔ*Uª¤øúúæ™{Rþ € )Uªo„ésÍ;wða÷îªÞºukÌ™;uëÖ5p†¼)N€4Ji¥ô¼H$J–¶$==½ZIIISp\3Bi3JHB©©¶€È À%dPJ3Äb±Z¸ÙW>væž@eñûï¿×Ð_ßëvîØñÆ›¿ØÞÆ ÃçŸnîÎzE®+ÐéÒ)Ç©9JÓ4M¦p>>ørÉ4lØÐ¨ùUP€Û¸C€›¸N(½ÊqÜU­V{U.—g›bµù«ÁM>ž½öz’––æZZZê,‹_ùM)£”>‹ÅáDŒÀPB`W®\±!z—ð;xð`¹m|»ø!¦M›†ªU«5¿×(°“ò÷ì&ŸåïïÿÁßп¾kÂ÷MÀ+!0§OŸö!z¿˜ß¹sç+ÿÜÞÞ“'OFÏ^½ŒžÛ+”øÖN$šéçç—ÃÇ@ 0?!0BFê{ÉùääWžûoذ!¾\²>>>L¦ö’RPú5¥t¾°”/¶OHx¦P(ZŽk©ïußÿýÿüY¯^½0iòdس-ê£!Û8`ŽD*½Î2°@ ,—ðŒã¸Ïô=h™'N¼ø{'''L›>]»ve;9JÔŠD“ýýÿ`X –NHxF.ú^óýöí/Žþ5óõÅâÅ‹Q¯^=–ÓºJGËd²ƒ,ƒ Àz…€x”žž..-++Pá<………èÜ© ЯŒ7Uª0ë.¬¡À¢G..14(fT ÖGXàQii©‡¾í÷!‰°rÕ*´oßžåt²ð™L*=Å2¨@ ¬“ðH'Õä*PÂ÷eU«VÅž½{Q³fMv¡ô+±X<î¯Je@ ¯ø¤R©šSàw3N!Ÿƒ¥Ré^3ÎA HXàÇqÙZŽB=¬ •ã¸>þþþ×L=¶@ ,ŸY;ÇØº¿Êy^1õ¸ø®°°°póÁë+<£ÀI46ÑpAi„T&K0Ñx@ °R ÏD„|k¢¡:­6P&Üü@PBÀ3‰DrÀa>Ç ÀwZ¦m@@€É_7À: ¯L€#d´ŽÒÕ‡~B *“Hþ·q€@ åVL@"‘d`0ýŠ”OÁÒR.Üü@`!0©Tº‹2€ÆÈP”««99I$’ sAå#21…Bñá¸­Ü ¸ü%ä3¹D’Äz^@ ¨\„“ËåGµÀ E¹†ÅØ`'É…›¿@ XVÌèüùó5Å=t¥2nœÜp”ª!‡ªT©rX¨ã/–þ·ªÚ¾¡á¬TIEND®B`‚chango-0.6.0/logo/chango_icon.ico000066400000000000000000000064421507376567100167060ustar00rootroot00000000000000  ( 2 € ÐÐiYÅ€ƒk4 8k‡ˆl6 ]ÌûóT„ýî«:A­Â —³Ò®; †öÿþÿÈ+¨ÿÿæg xÌyZ‡§¬”u¢ÙYpøÿÿÿÿÿ€éÿôx}ËZ—ïÿýÿÿò¦ŒÚB4àÿûÿÿùúÈ9‚“õÿõp:[Âñðÿý÷üÿÿµ¤¹—ÿÿÿú¦LCWný¾¬þÿïWFîûúÍiA[ÃÿüžÓNÞþüÿ£ Éÿ©ÈÿÿåGkó¤"ÌÿÔ¿ûîïøHIìó›áÿÿÖ:Hxÿï¬ýþÿó:¤ÿÞžóÿÿÇ'gÿ÷Ö¯îûþÿr_éÿÿͨÓÿÇ1žÿûôŸ½ÿÿÿàP ïÿÿþê¾0/ÂÿÙ]pòÿÿÿk^õÿÿÿñÀ±ÙüÿþÿÞÍËÎTñúÍ°Êøÿÿÿß' ¨ÿþþÿÿüýÿÿþÌhdÔ·ê›1C¨ïÿÿÿÿÿüy&Æÿÿú÷ÿÿý樇›×¥ñåˆ+4ƒÔüÿý›&ªøãÏÿëÃ¥£m"ÐÑ‹äóµP,kΉ OžÊÚΫr(I⑇Ðÿýà| \tЭҾ¿Û "Æ÷¦‹ÙÿÿÿÈ0IÙÿ­w—àþÿÉ%.µ·¯¸{šÞÿ© EWõöƨËôe hÅ÷ÿøÿÏFÏóü|J˜³øÿÿÿðàÀ€€€€ÀÀÀÀ€€€Ààø?ÿÿ€ÿÿàÿÿðÿÿøÿÿþÿÿÿ€ÿÿÿàchango-0.6.0/logo/chango_icon.svg000066400000000000000000000320601507376567100167260ustar00rootroot00000000000000 chango-0.6.0/logo/chango_light_mode.svg000066400000000000000000000330551507376567100201160ustar00rootroot00000000000000 chango-0.6.0/logo/chango_light_mode_1024.png000066400000000000000000001646151507376567100205600ustar00rootroot00000000000000‰PNG  IHDRU?•û pHYsYY"#÷tEXtSoftwarewww.inkscape.org›î< IDATxœìÝyÜíÕØÇñÏuršçy”QH)!‘!©4H!¢‰©T2zÌ$¡BDÉ=M† i Ov¡þO™ÇÛÎ ·H’$I’Ô_æ(úwÖãí÷+Ú I’$IR˜£èßXw=;3ïw@’$I’¤–Šˆ y¤½ÿ‰“|Ìn$I’$©E"b#)úךâãnVÉ̇í$I’$©²9ÚûwÖéâ£OË̇œ$I’$©‚9ŠþW3ùöþ±œ:ûNH’$I’Ô#sý¯eêíýcùEf^5ûoœ$I’$©!1 Ø”Rôï¬ÒÃáOyT”$I’$©{"b>à9Ô)úg»X93ï™ý;$I’$Iš¢ˆXØ‚Rôo,Y7gÎYüƒ’$I’$MJD, lÛym,R7Ñ£œ2÷Ü I’$IÒ8EÄã­(«ü/¦×M4¢ë'å\¿’$I’$""Ö¤ü»Pô‹º‰ÆtÊÜÅ?Ø I’$IÒ£tNîßRô¿xrÝDò0°zfþcîoØ I’$Iz±ð`G``Ū&ï‡#ÿà€$I’$iHEÄR”“û·¶–¨›¨+søßln$I’$ Î~þí;¯ç1X ã·«f挑¾9H¿QI’$I’#"žB9Ào[`£ÊqšôÅyÿ`€$I’$iÀDÄ‚Àf”Öþ€Uê&ê‰ÖÉÌ?Íë vH’$I’ú^D,¼ˆRôï,^7QÏýp´âœ$I’$õ©Î~þí(­ý›Óë&ªê¤±ÞàI’$IR_ˆˆiÀ†>ÕØ I’$I}&"¼ˆrjÿËåë&RÃnVÏ̧ò;$I’$©̱ҿ ðJ`…º‰ÔCŸ™jñvH’$IRkÍUôï ¬X7‘*xˆ²úËTd€$I’$µÈ\Eÿ.ÀJu©²3ºQüƒ’$I’TÝ\EÿÎÀÊu©ENèÖƒœ$I’¤ ""€çPZûwV©›H-tIfþª[s@’$I’z("Ö£ý¯Ö®Gíö‰n>ÌC%I’$©a± e•๕ã¨?Ü<)3îÖí$I’¤DÄ’Àö”¢k`¾º‰ÔgŽïfñvH’$IR×DÄÀ–”¢'`ẉԧnÖÈ̺ùP;$I’$i æ8ÁwàUÀâui|¤ÛÅ?Ø I’$I“O^O)úWªGƒãNà ™ùïn?ØI’$I§ˆXšr˜ß>ÀF•ãh0ÐDñvH’$IÒ¨æØ×¿;ðr`zÝD`÷RöþßÞÄÃí$I’¤DÄFÀë€Ý€å*ÇÑpø\SÅ?Ø I’$Iÿ+SNððôÊq4\fkgæ_›ÀI’$IC-"¶£¬öo…u’êør“Å?Ø I’$iHEÄs€=]Å*ÇÑp› ¬›™×79ˆ3[’$I’†FD,Eiñß[üÕßlºø;$I’$ ¸ˆ˜¼ˆrußÀüuI±QfþºéAì$I’4"bUà5À¾À•ãHórn/Šp@’$IÒ‰ˆ€í)úm ÌW7‘4¦cz5’$I’ú^D¬ ¼x#°\å8Òxý<3/éÕ`NH’$IêK±°°ðÌÊq¤Éø`/ó@I’$I}%"Ö£´øï,U9Ž4YWÏÌåvH’$Ij½ˆXØŽRôoQ9ŽÔ ïîeñvH’$Ij±9ööï ,]9ŽÔ-?ÏÌM{=¨’$I’ZeŽ“ü]í× zOAí$I’Ô ñ$à ”Cý–©GjÊ%™ù¼Û I’$©š¹Vû_ DÝDRãÞ]k`'$I’$õ\D<xe¿«ýçgæ…µw €$I’¤žˆˆiÀ‹(«ý¯æ«›Hê¹çdæ/j n€$I’¤FEÄ’”•þ·kTŽ#ÕrnÍâì$I’Ԉ؈²Ú¿;°På8RmÏÊÌËj°@’$IR×Ìq¨ß@Ïï9—ZêÛµ‹°@’$IRDÄZÀÞÀžÀ²•ãHm’À†™yeí vH’$Išõ“ÆåÌ6ÿ`€$I’¤ Šˆe)+ýûâ¡~ÒhfëgæÕµƒ€’$I’Æ)"žA)ú_ ,\9ŽÔÎhKñvH’$IEDÌì@ióߢr©ŸÌž’™×Ö2›’$I’#"V¡¬öï¬P9ŽÔ¾Ü¦âì$I’4‡ˆØxð*`zå8R¿ºxRfÞT;Èœì$I’†\D,¼8xzå8Ò øHÛŠ°@’$IZ±K‡2('÷cñvH’$I+"VþŒš˜À¥tZû3óÊÉ<$"ÞJ™„Z²{ñªºXg<[ÚÈI’$ip†Å¿ÆgÎUþïdæ¿&û ˆ`gà8`îÄkwõkñvH’$I)"VnªE­4ø-¶~à×ãÝË?šˆx1ð!`é>«…~<33gÖ2YvH’$I Šˆõ€2ó}ÿz´›)‡÷ý€rbÿÝÝzpDl@9ào«n=³exK?ÿà€$I’ÔˆˆØ8ØX¢Çc¯ìÓË1ÕJ¬òÏ)"ž¼Ø˜¯›Ïn™/gæÏj‡˜*'$I’¤.Šˆ5£W\Ÿ™ÿéqŒw ÷xLµÃ=”þ³ó¦²—4±ð^`¿®¼›raßôÿ£$I’¤žˆˆE÷QN=ŸŽoMêõ)äXxS/ÇTu·çß.È̇š("–Ž ü36,L™™·ÖÑ NH’$ISÛŸVáÛ¿íqWÿ‡Ã”¶þï?Í̇›¬3Áµ?¥Ý¿§[Z*»øtíÝâ€$I’4I±2ðq`—QÞÖ³€ˆXØ·Wã©ç®Î¾›™WôbÀˆ˜ŸÒæÿ~`…^ŒÙ2oÉ̵Ct‹’$IÒEÄã(«¡ãíW5Ÿè¿íáxjÖLà”¢ÿ¬Ì¼©WGÄtà ”}þ«ôjÜ–9#3R;D7E—”$I’ZD<8 Xoÿ°t·O^Igoö 8Ðïfç_¾“™wõrðÎäÖk(…ÿš½»eþ¬›™«¤›ì$I’Æ!"–Žö¦ócWõ¢øï8‹ÿ~5¸Rô+3ïèu€ˆ˜Ø x°N¯Ço¡£­ø'$I’¤1EÄŽÀ ÀÊühOöÿwöþïß‹±Ô5³€ŸQŠþofæ-5BDÄ4ÊïÖ­‘¡…®>Z;Dœ$I’æ!"VŽvŸä#zu€«ÿýcöA~§fæ µBDDÛGÖÊÑB ì×äUŠ59 I’$Í¥SíNY\f jüÀˆXWÿÛnvÑÿ•̼¾v˜ˆØ8بv–:%3/¨¢)NH’$Isˆˆ'Ÿ^0ÅGÍþ0å@c{;cßD Þû3p:å$ùkj‡ˆˆm#±ðŸ—Û(7i ,'$I’$uµßÑÀ"]xäŸ3óþ.q?CÒIã€$I’†Rguô³Àš Ñôþÿƒpõ¿×î¾F9ÁÿWµÃÌ-"^ ¼Wü'êÈÌüsí½-Ù–"I’$õDgåüx`o êI™y]îüþ‚½0ø ppf›ZügëLf}xví,}è·À&™9£v^°@’$IC£s úIÀ* u?åø¦ˆÅÓþœLiñ¿­v˜‘DÄË(+þî793€=†¥ø'$I’4:§åìÓ£!¯ÎÌ™M<¸³úÿ¶&ž-¢œâÿ9ZtŠÿÜ<Õ¿kŽÉÌ+k‡è%'$I’4Ð"b;à34¿ê?§ß7øì·K5øüaô7à+À ™ù·Úaæ¥SøÿðÌÚYÀcj‡è5'$I’4"byàÃÀî†o䀈XWÿ»eöÞþÏg5Õ±1UÀ¶”Vÿ+dz€½2óÚAzÍ I’$ œˆx5ð `ÙJšêx+°tCÏ7Ÿ¾˜™·Ô3/1 Ø‘Rø¯_9ΠùXfþ¬vˆ¼@’$I£³ê¥pªiÕÌü{7ØYý¿'&cð]ʵçgæ¬Êyæ)"ævŽžZ9Î ºØ 3﫤;$I’4"b'Ê^ÿå*G¹³ÛÅÇ[°øŸ¨Û)-þ'fæ?j‡MDL^ ¬S9Πšì9¬Å?8 I’¤>×9ÿxzwÂÿXº¾ÿ?"¥\ý§ñ¹88½íÅ^D̼ x°vå8ƒî£™yqí59 I’¤¾[_V­eMxõ;Únp.åì‡Ö^á7[D,ìB»þùT×PÎSjNH’$©ïDÄ”+¼¢rœ¹uõÀN¡øön>sÀÜ| øxfÞX9˘:Ý{‡+UŽ3,^Ÿ™÷×R›’$Iê+±9ðE`ÚYæá]~Þ[põ$ןNÍÌÿÔ3–ˆXšr…ãÀR•ã ›c2ó²Ú!ÚÀ[$I’Ô"bAà(EÔ´ÊqF³lfÞÑuV‹o¤Þu†m“Ày”6ÿóÚÞæ+o«gýxVfΨ¤ ì$IRëEÄúÀWhÿµh·u«øïx ÿ§ÉÌkj‡ˆXx°7°På8ÃêAJë¿Å‡’$Ij­ˆ˜FY=ý °@å8ãѵâ4"£7Ìþ |øXÛ¯ñ›-"Ö¤ìïߘ¿nš¡÷ÞÌlâPξå€$I’Z)"V¤ìõߪv– ¸º‹Ïz+°LŸ×On§\ã÷É̼³v˜ñˆˆ§‡»aÕ©¢müS’$I­»'K×Î2A]鈈Å)Ãæ/ÀÇ“3ó¾ÊYÆ%"žìLûn¤Vw»gæÌÚAÚÆ I’$µF§ðý°Oí,“ôÇ.=çmôßäÇT\EY­ýjf>\;ÌxDÄf”VÿmkgÑc¼93ÿ¯vˆ6òI’$µBD¼ø2ðøÊQ¦bõÌüëTKPNþ†«â~—™çÕ23)vÞ lX9ŽFö•Ì|míme€$I’ªŠˆéÀQ”ÕÔ6_ï7–ÿ7uá9oc°‹ÿ¾ Û/w³GÄüÀî”=þëTŽ£yû?`ÿÚ!ÚÌ I’$U«S®÷{ní,]pÍTï¥ï¬þØ¥ý½ú.eÅ¿_ ÿM€#€í¨G“÷éÌ<»vˆ~S<¨T’$Iš§ˆx p°Bí, ûcf®7™FÄ2ÀÀbÝÔçïí£ëü^H)üy2jX\ <;3¨¤Ÿôó=«’$Ij©ˆx\D üÁ/þajûÿ¡ÿŠÿË-2s«~(þ#b‹ˆøpÿƒà^`7‹ÿ‰s €$I’º*"V¥ô÷¼ÚYzhRÕÿý»œ¥IŽ>Ÿ™³j‡MDL^¼ظru×~™9ÕC7‡’’$Iêš!jùŸÛd‹‘CéÕÿ;€ÏÌk‡MDL§êwð¤ÊqÔ}ŸËÌSk‡èWž I’¤)‹ˆÇïÞÃpn3Ý83¯˜È"bYàÚ=pð)à˜Ì¼»v˜ÑDÄ”küÞ ¬V9Žšqeßÿýµƒô+;$I’4%ñxàkÀ¦µ³T’Àµ“øÜa´·ø8…r²ÿ͵Ì&"ÞHùßs¥ÊqÔœ»ZüO’$Iš´ˆxðúû »©úkf®>‘DÄò”ÕÿEš‰4i œ ‘™“™Ô虈X8r†Â•ã¨Y ìä•Sg€$I’&¬ÓòÿAÊöa¿G}²«ÿm+þ–™Õ2šˆX‘Rø,\9ŽzãcÿÝá€$I’&¤sÊÿ×Þ–ÿ¹]7‘7w Ø75”e2þ’™çÔ2šˆx"eâdw`þÊqÔ;?¯bP8 I’¤q‹ˆR®ø[±v–™Ð¥ˆmÃÊõ½À‡)üµödÿˆxðNÊ>ë—ár eßÿŒÚA…ÿI’$iL”vÿ£ù*Çi›qoè¬þïÓ`–ñHàtàÐ̼¥r–y²ðz3(Åÿßk$þ‹$I’¤QEÄ2”‚q«ÚYZj"ï¤îêÿeÀÛ2ó3Œ*"VŽÀÂؽ-3/®bÐx €$I’æ)"6ΞP9J[=,’™3ÇzcD¬üX¨ñTõwJQ}Z¶´èþû TŽ£ºNËÌ×Õ1ˆœQ“$IÒˆ"â­À‡ðÀµÑüi<ÅÇ;é}ñp<ð¡Ì¼¯ÇcK§Õÿ``zÍ,j…Ë©¿Mf`9 I’¤G‰ˆÏP 2n\ûÿ;«ÿ{5œenßÈÌ¿ôxÜq‰ˆÇ㊿qeßÿµƒ *'$I’ô_±p°~í,}âúq¾ï]ônõÿ àÀ̼¤GãMH§ðŸ}¸ŸÝ%šm°SfÞX;È s@’$IDÄÖÀW€¥jgé#cvDÄÊ”b·iwN˜À¶„ž‰ˆÕ€C(íÝ VŽ£öykf^X;Ä s@’$iÈEÄ4à=À{i•ãô›ñÜðnš]ýŸ| 8*3ïipœI‰ˆU(+þ{a«¿FvbfžT;Ä0ðI’¤!‹_v¬¥_-—™ÿœ×7;«Þ×Ó\á{1°fþ®¡çOZD,O9Õÿ­Ô¹ù@ýáb`‹Ì|¨va`€$IÒŠˆ§Qöû¯];KŸºk´â¿ã]4Süß œÞ¶ký:Ž­þÛŸ-þ{Ç I’¤!¯¾,R;Kµý?"Ö¤û{ÿg'ïÉÌ»»üì)‰ˆå(­þoÂí`‡Ì¼£vaâ€$IÒéì÷?8 ˆÊqúÝXIwïµÿ9°_fþ¶‹Ïœ²ˆXŒÒê0°Xå8ê3×dæj6NH’$ ‰N¡v*ðòÚYÄ<;"âIÀ«»4ÎÀûOeæ¬.=sÊ"b~`à(`źiÔgÊÌïÕ1Œœ$I±6p°^í,d´€ÿæ›âó8R,uÖ@ÏtºHvŽÖ¬Gýçs™ùÉÚ!†•’$I."¶ΖªeÀŒØ1õ[~Ci÷ÿÅŸÓU±ðaàéµ³¨/ýØ¿vˆaæ=¯’$I,"ÎÅâ¿ÛøÓ<¾÷&¾ÂÀ›ÛTüGÄ #âÀ°ø×äüØ53®d˜Ù I’4€"bà$Êmußß3󾹿϶žÄóf·û’™·M5\·DÄS÷»Ô΢¾öʉÿÿ©dØ9 I’4`"beàl`“ÚYØ óøúÿLâY¿Þ”™?›Bž®Šˆu€;ãmšš/ËÌ›j‘[$I’JDl@¹.Îâ¿Yžû ñRàùxÆý”ô7nKñ«DÄg?PVý-þ53€]Úvuå0³@’$i@DÄ.À—€…+G™ óãu!°ofŽv“@ÏDÄÒÀ¡À[…*ÇÑ`H`ŸÌ<¯v= I’¤>A)>ß+¶½ò¨-ñ àYãøÜmÀÁ™yz#©&("Þ,Q9ŽË™ù¥Ú!ôhNH’$õ±N÷eʽìêÿvDÄtà˜1Þ?û¿ƒ2óŸMNæ7G+ÕM£tRf[;„Ë I’¤>Î6¨eÝ8ǯ÷Öå½×Rùûi£‰Æ)"vŽÖªEé[ÀþµChd‘™µ3H’$i‚"bSà,`…ÚY†Ð `Ì̈X ø°ôï{88&3ìeÀ‘DÄ3›Õ΢u1°ef>P;ˆFf€$IRŸ‰ˆW§àamµÜœ¬¢½‡‘‹ÿK(‡ü]Ý»X#‹ˆU)9÷Â[ÀÔœß;Xü·›ÿ$Iê1-"޾†ÅMÿˆˆyl«ó]”Bûùµ‹ÿˆX,"Ž®öÁŸýÕœë—dæ]µƒhtvH’$õˆX ø*°mí,báˆXrþÂüs|ý«”Cþn­«ˆˆù€=÷ã5ïo”â¿ê?÷Ï$Ij¹ˆXøð”ÚYÀ,àn`©Îßßì׆ûÎ#bKà#ÀSkgÑPø'¥Û嵃h|l’$Ij±ˆØø%ÿm2RüϤÛO«]üGÄS"âÀyXü«7þ lcñß_Ü I’ÔR±7ði`zí,zŒß{fæe5CDÄòÀQ”süÙ^½òåÀ¿_Õ¢‰q €$IR EÄ+(÷i«]þ‡rµßCµBDÄ‚ÀÀ;ÅkåÐPzØ13Ï­Dç,¡$IR;íP;€ã·”Uÿ_× ì ¬Q+‡†ÖLàuÿýË3$I’Z¦S佤vý×ýÀáÀÆ•‹ÿM€‹o`ñ¯Þ›¼>3¿^;ˆ&ÏI’¤öÙX©vðS`ïÌüS­±eÅg jåÐP›ì•™_©DSc€$IRûlU;€¸‡²ÇþŵŠÿˆX("Ž®vÁâ_u$p@f~±vM’$IíóÒÚ†Ü÷7eæßjˆˆô/Ÿ IDATO¨•Aê8(3O¬BÝá-’$I-‹wàÕ5Üž™Ÿ« "ž|ئV©#)ÅÿÇkQ÷Ø I’Ô.[`ñ_ÙÀ[2ó¶ƒGÄÂÀ¡”è‘AšCoÏÌOÔ¢îr@’$©]lÿï­[€ý3ó¬Z"b;ààñµ2HsHàÀÌüdí ê>”$Ij'zç`ÝZÅD¬ßÁâ_í”N‹ÿe€$IRKDÄ“ñз^¸Ø73¿ScðˆX8 ØW{$ðVülþG’$©=\ýoÞé”"ç®^Ó€=€c€å{=¾4ŠY”I±Ï×¢f9 I’Ô[Õ0Àn£\íwvÁ#âÀ§€MkŒ/b&°gf~¹v5Ïk%I’Z "¤\ÿ·pí,èL`¿Ìüg¯Žˆ¥÷ûóõz|i »Õ<S½e€$IR;¼‹ÿn» x[fžÖë;íþ¯>,Ûëñ¥q¸xEfžW;ˆzÇ I’¤vpÿw}›Òòk¯ŽˆMOôzliœî¶ËÌ‹jQoy  $IR;8Ðw»g掽.þ#béˆøp ÿj¯;-,þ‡“gH’$U«­cœì•™ëõÀ± pžî¯v»Ø*3¯ªDuØ I’Tß6µô¹{€}­{]üGÄÚqð ,þÕn×϶ønž I’TŸíÿ“÷#ʪO;("b:pp$°`/Ç–&ár`›Ì¼½vÕåI’¤Š"âqÀ?%jgé3÷ï>”™³z9pDl|X·—ãJ“t°cfÞS;ˆê³@’$©®M±øŸ¨ŸQú»¡—ƒFÄò”ký^ÛËq¥)ø:ðºÌ|¨vµƒgH’$ÕµUí}dðàùŠÿ×Wcñ¯þñ àÕÿš“’$Iu90>7PVýÖËA#b J»¿ç4¨_$ðþÌ<²vµgH’$U+P®åŠÚYZî4àÍ™yo¯ŒˆiÀ^”–ÿE{5®4E{d浃¨ì$Iªç¥Xüæ6Ê ÿßíå ±>p2°I/Ç•¦èNàå™yqí j/Ï$IªÇ¶òyûðô^ÿ±`D ü ‹õ—çZük,n$Iª Ób~+°lí,-óoàÀÌ<¥—ƒv®ö;xb/Ç•ºà”•ÿ[kQûÙ I’TÇÆXüÏí—ÀF½,þ#báˆ8–rWºÅ¿úÍ7€Yük¼œ$IªÃÓÿñ0pð¼Ì¼¾WƒFÄó+Ãðçbõ—¤ü;óªÌ¼¿võ”$IšCD<¸*›ß'¹eÃÏï×P®÷»¼WFÄ”Óý߈‡0ªÿܼ>3ϬDýÇ™NI’¤ŽˆØ ¸˜¯áq–žÕä} Ï÷¸øßø°'ÿê?7›[ük²œ$I"â`à{Àâ4ß%ù’ŒÑf·/ËÌ}3óÞ^ KGÄ©À¹Àj½Sê²ËM2óWµƒ¨ ó<’$IDÄtàD`¯9¾ÜhÃ}ýßù”–ÿ[z5`D¼ø °b¯Æ”ºì4`ŸÌ| võ7;$IÒЊˆ…sxtñÍÿŒ4ŒûÿŽ^Ú«â?"–ˆˆÏgcñ¯þ48<3_gñ¯n°@’$ ¥ˆX ø.ðÜ¾ÝØÏHñ4†¯ý¯À«3óÒ^ [_VíÕ˜R—ÝA9åÿüÚA48ì$IC'"V.aäâš]$¶öÿ3õ{UüGÄ¢qðC,þÕ¿®žiñ¯ns@’$ •ˆX ¸Xo”·5yÀ°LÜGÙ³¼kfÞÝ‹#âù”Âi_<á_ýëkÀs3óÆÚA4xœ$IC£³òÿ`Í1ÞÚÈ@D,<¯‰g·Ìï(«—'÷b°ˆX(">Âøþ¿•Úê!à€Ì|u¯nÇÐðñ I’4"bJ¸Ö8ÞÞTÀ zv[|883ïïÅ`ñLàËÀº½OjÈMÀ.™ùËÚA4Øì$I/"V~¬=Î45°UCÏmƒ»)–í׋â?"‡QÎr°øW?û ¥cÆâ_³@’$ ´ˆX޲çÿIøXS?# êþÿK)§üÿµƒEÄÚ”{ÑŸÝ‹ñ¤†Ì>•™³j‡Ñp°@’$ ¬ˆXø+þ¡€NÑ:Þ„~1“RÀ¼ ‡Åÿ^Ào°øW» Ø&3ßgñ¯^²@’$ ¤ˆXø6°Ñ$>ÞÄ€Akÿ¿Ø-3Ò‹Á:';ôb<©A¯ÍÌ›kÑð±@’$ œˆ˜Ò"þÂI> €ÑýxV‹ÿ-ßbñ¯þ68 ØÒâ_µØ I’JDðY`ç)<¦«±°y7ŸYÑiÀ¾=:èo!àXà šOjÐß(çd\\;ˆ†›’$iÐì9Ågtûg¤ç‹vù™½ö ð–Ìü|/‹ˆ €¯OîÅxRƒ¾ ¼!3ï¨Dr €$IñNà.<ªÛ[ú½ýÿþ/þ£Øøÿêo÷;Xü«-ì$I¡s:üÑ]zœø)ðÊ̼­é:ý}xÙ5¸’rÒúÀ}¯Ïîîüz F^ [’òÿÿÀtJçÆ‚ÀB_/Þù{i4WPú»¦viNNH’¤¾;'ѽ}â]û)"VžÒ­çõØÇ€C3óᦊˆ-€S•ºð¸S&¾ÙÄÄEDÌO™XXjŽ_/Ñùû¥e:¯e;¯Ù_[¤ÛyÔ*³(ÛÞ—™Õ#Í-2³vI’¤I‹ˆßèâc_œ™tãA±7ð¹n<«‡îöÎ̯5=PDL>¼ƒîoOüø_à<à—™ù@—ǘÎõ”Ë«+«tþº2°bç¯+ËÓÌmjÎÿ¯ËÌ‹j‘æÅ I’Ô·"â™Àźüè-3óGÝxPD|Ø©Ïê‘?¯ÈÌ«š("Ö¾ lÒôX———fæ¿z4ö„t®²\ž2°er`ödÁì¿®ÖùžçzÕw*ðÖ̼{ÌwJ9 I’úRD¬ \DYMí¶­3ó‡S}HD<ø'¥5¼üxMfÞÕô@±eÛÆâM5ŠYÀïK_¿ÈÌë*æ™°Îv„Õ€'ÌñZX£óëUð Å&ÝB¹ó;µƒHãá€$Iê;±ðsJ¡Ó„m3óûS}HD<¸° yzá“ÀA™9³ÉA"bÊé·69ÎÜ\F™¸‚Ò%pgÝH“×™ X•rÅzÀš×Ó(]š¼3ý2󟵃Hãå!€’$©¯DÄ¢”{µ›*þ¡{?#½´KÏiÒýÀž=Úï¿6ð `æǚ‚Å-:/€™q5eÂéç”Nk³OVÑ:ÑÝÐy}wÎïEÄÊÀúÀÓ :]ÏËí”Âÿ›µƒHe€$Iê}Ñç0õkâÆ²Sfž5Õ‡DÄåÀF]ÈÓ”›€3óЦŠˆ/P·å¿[î¤Lü’2)ðË̼§n¤îˆˆ…(ÝÏ¢œÏðdœ˜í[”â¿ñk1¥&8 I’úFDœ¼¹Cíš™gNå±p3íÝ})e¢ãÖ&é´ 8 Éq*›\M™˜Ý)pM¿t Œ%"ãÑÏ¢œ-0LnÈÌoÕ"M…’$©/DÄ;({Ç{a·Ì[;¥ø£“ù`D|xc—óŒ×ÅÀΙy[“ƒDÄ뀓€…šG“v?åfóïdæ+çéšÎÙ/v^,2Ï¢´û¿Ãv +'¤–‰ˆ%€€E;¯Å…;_ŸŸ‘ÿ°[¤ó½‘ÜMùCJËàì=ƒ÷R®»¹›rÎÝÀÝ™ùðT/’4{ãϦnëül‡eæñýPDp°J÷#éӔ΅Æþû Ÿönj 5âZà\JwÀEƒ²ê‹ÛûR®œ—Y”ßÿ»šîŒ‘ÚÎ ©½k+Ët^ËR³Yf®¯ÍþõÒ”â~áyçá^à:_ÿ¸ø+ðàï”to¤ƒˆ$õ^DlHY½žÈj^“ŽÈÌc&ú¡ˆX¸²<£yxKfžÜä ±ðM`“&ÇQãî¡LœM9; ÑÛ!z%"6ö^Í#?OÝ œ|<3¯­•Mj'¤IˆˆÅ({WVë¼æüûUiçݶMIÊÄÀß(××t^×eæ¿*f“Ôr±ð+Ê?Ûâ=™ùÁ‰~("Žk ϼܼ"3/irˆxå°¿^žÌ®æ=\|›²UàæÊy¦¬Ó°eÄUƒÒí u‹Ò("b`=`}à©ÀÓ:¯åkæêC·P&®í¼~\™™·WM%©ºNKùÔ¿2onGfæQýPDüxQyFr°Cfþ¥©:[Ž¡[3ÔœY”k¿ |;3¯«œGRœ::wN? x>eæx}` `ZÍ\î”VÙ+ßvþz}fάšJRÏDÄ—€××Î1‚fæ{&òˆX¸ƒyŸÇÒMç¯ÍÌÿ45@§Ûí`ç¦ÆP«]M™ øºûæ¥Áñ¸Ú¤Z:‡ím<¯óÚ˜ÞüЦG¬Üym=Ç×«(«—¿¤»%=""¡Å?LnµûE4ÿçHÇïÎÌYc½y²"âÉÀYÀºM¡Ö[¯ó:""®¦l9#3¯¯KÒTØ ¡+R ýÍ(«üëãê~¿¸ÎdL x®€ÔÇ"bà;´·­üøÌ1ðAà0 šGcp)e2àLÿ–úƒ(±*¥àߨ‚ö\'¥f%e¯â…×E™yKÝH’FKSºyÖ®e ŸÈÌÇûæˆX‡rÈi® ö÷÷†žOD,C)ä¶hj ´ÀwÏç5¹=EÒÔx€úZDL6¶¶ž«Ã(€§t^ûÁWâÎï¼~ÐäAY’Æ'"|ƒöÿ0ñ-b[ý–I9Ø#3ïkèùDÄú”ÃÞšê^Ðà›¼¢óú{DœœÔä ’&ÇõÎÕ|ÛÛ/–©›H}à~JgÀÿRV&®®œGJñi:“t}à3™9î¬q.ÝHà}”Ûûa-"v¾ˆsê¾™À“ïgæÃ•óH õ‰Î}[¯¤ÿ ×M¤>wðÊ5Zdæ•óH/"Þ œX;Çœœ™ûŒç± åú¿nýÙt/ðúÌüV—ž÷º‡c眚÷7à㔯î©FfN¨µ:‡ømI)ú·«›Hê?”ŠïPV(G8ñBà8ÅçÌSDlœN»¯_ÔpùðªÌ¼·viX8 j"b`w`OàÉ•ãH£™\B™ øffÞV9Ôz;åÏ¡ÛÌÏÈÌÝÆzSD¬üe ãÌÏÌMᣊˆÞÅÄo7š”À6™Ù­-4’Æà5€ê©Î„[QŠþmé¯ý ^Ó€çw^ŸŒˆŸPîË>Ó=ŒÒ<}þ-þæçû¶šÂ÷¯ÉÌïMᣊˆE/;55†4I¿Þš™?­D&v¨'"bMàÀÀ*uÓH]s?på‡ë 2sVÝ8R;DÄŽÀ·èïÓåÏÊÌ1‹æˆ8›rfÍDÝ@9ìï“øì¸tþìý6ð´¦Æ&áàÀç¼Pê='Ô˜Î)þÛûPöë祱ü²·ö”̼®v©–ˆX¸ X¢v–):'3G-ì;×ÞÎį?^Þäv¢ˆØ8X®©1¤ ºøð©Ì¼¯viX¹L]«DÄÑ”;_¿lÅ¿ß*ÀaÀµqIDìÕi½•†FçŸù³éÿâÆ·MrS&þ{ý ð†‹ÿƒ€ó±øW;ÜÜ|Ž/]<+3¯l"DDLˆO3x7-¨}.§œè¿vf~43難m"b¡ˆ8™²%b‘Η·¡LÖµÓ!%µ–C*"VˆÓ)ï°þ³]ÞyIm¶p.åíœP/EÄ.À~µsôȼ:žÂ#?ôؼ©ÕшX†Ò™öæ&ž/uÌ¢œ+±yf>³s¢ÿõCµQD¬CÙ&»×ßžF™<¹:"NŠˆUzN''†LD,Gׯaì;Œ‡É¯(·Ôéx_å<Òd]¼73/¬Dƒ#"Nö¯£Çþš™«÷zЈ88öÿÙ»ïxËÆ³ã¿kô(%"‰žFˆD ‚轄hAÑAˆ‰ð¾‚ˆ¢^%!Ñ{ï¢EI´è½3Æ0s½Rýé~¡ìu€‡$í.)Z|†ÊD €–ô-à`‘ÜYè>Ûßý7$­\)OS¼|L¨Gð$ÀTÅ–˜˜ÊémÒÙâ?Ú~/w˜Po’vÎ#£7mO×ÍHZ8“F†Ð©›# ¢…_ÿ5³v&O¡ìû€mlß™áÞ¡ÇÄ@Ã+4¿ö$ ´BÿýÒö!£ÿ†¤Iç€óDÊæàŸÀC¤$ϯC÷€wm0›Hš˜‚4»>ª¢î´¤ÞÚSS“& ¦$=úßSÓÓ3ÿÝ««“ñ°pZl cST¡¿˜Þn;Øö4ݺxQìï`bR4tn8i—Îmß;LSGpNVÉe$p4°í -®„б˜h0I‹‘ÎfΓ;Kƒ æ°ýꘟ´©MN[}ÜOÚzz'iËÛlÌšªŸŠms£&¦f¾ | ˜˜µøõˤI‡ð±;lß‘;H¨I3“V£z½wõ£¶KoÃ'ià`˲¯zÆËÀñÀ±¶_ʦÉ$­ü™zý¼{ØÒv¥] T¬ž@Úš+s˜í_Œí’¦ž"­:·ÕÒ Â0Òêþpà5àõâã%RÛš'‹§›\=XÒ ¤¾Úóßæ&Mô*“ªïeû…ÜaB^’—“ŠHö²»€5l¿\æE‹÷•ó€eʼnè÷¶ùŸÝä÷â:(jâü„zît2©ˆãäÚ%&¦hëuiàæuRKœqΞKú%i‹fHF/Ž |ôaû­œÁ¢xXXŠÔFïÛô^®wI‹‡Çƒey\lXöC·¤¯×þZ™× ­÷>iÒèhÛ·åÓtÅYÿ‡ÒŒcž›Æß}(SL4„¤‰€Ý€ýI3ÇikÛþÇø¾¨X »X©’TÍö&ð0iB`Ô¯÷Ù~-kªHšŠÔBs `q`I`²¬¡ªó,éüái¹ƒ„jIZ¸•Þ~9 Ø®ì"j’–ΧŽP/’|޲ý|î0mP´5=X1w–~ ü‰T³*&èÀÅ@Hú éM`±ÜYZbéÜó‰}ùâbËæéÀª]MÕ^O“&î#m«ý§ígóFêIŸ!M¬¬Ì•7Q%.v°ýßÜAB÷Išš´½ø+¹³d2’ôp}hÙ–´%iÐ…zÄyÌ! IDAT¸–ôýraTó/GQØù—¤¢Ù9*ü—å>àG¶É$4[LÔX±Mià¤jèaàþ lmûÞþ¼¨ø»ø9©rzôR¸—Hwúul…ëJÒ7H«‘v ´µï{¤c‡Øž;LèI§“¶Åö¢÷MlŸ[æE‹d“vï…0>ïzޱýïÜaÚDÒR¤ •¶Ì~—´KéŒÜABsÅ@Mg’O% 2ÂÀÝJê~ñ@Úž[÷+>æ()[Hž&MŒú¸» mp$MK:"²:©…P'ˆþCzà¸>wP>I›½h0é8Ø5e^´x¯8X³Ìë†Öyˆ48=ÝöàÜaÚDÒÜÀÀrgé’SH»ô†æš'&jHÒ’À_èíÊäex„T8çô²·K;6$ ˜³Ìë mÉ}¸ ¸¸Ùöãy#_ÑÞkàGÀZ´«ƒ„3€Ýl¿’;L(GqÄìÚõ½ÚW¯«–Ý?]Ò,À…¤b¢!Œið7घT-_ÑÆô·ÀOiïî¼Q6°ý`î ¡Yb FŠí‚{ûÒþZÝðp;p3pY•g—%}XXXˆÔ¥!þË÷"pSñq­í‡3ç'ISvlDÚЖ"‚o{' d7Mȯ˜°º…ôs«×< ¬dû±2/*iaàÀÌe^7´Â¿HE&ϰýfî0mSìºÙø½ulö=`GÛ'çš#&jBÒH«kËåÎÒ&­ _C:C~GÙýš¢ü}X´4»ãYàJà àÛodÎ3VÅ1µI;F–¥-o¶,{ª#é`Ra¬^ó°²íʼ¨¤õ€ÿ¦(óº ô:ð60Nª±0A:ëÞW“óÉ?ËÏ&R'¦)þû3ÀTÔ·Àâ`à,àdÛwæÓFÅD斤ų™2ÇÉéDÒD@t 5 iEÒùË^þÁÕ©ÁŒV]žTL.Ë6ñ¢Çó"¤öq‹ß"vTiiRèàÛOfÎ3V’¾HÚš¸%0[æ8õé¡ë0Û#r‡ }'iYà*`Pî,»XÃö[e]°8¶7ð?€ÊºnÍ™tÌá àߤ㯯çúYPL´NL;ÆÇçFûïIò³¿v«-ã­ÀÉÀ_méÒ=zZ±kv}`?àk™ãÔÅÀ:Ñ62LHLdTüðú ðkzï!¬›^®&­ _lûõnÜDÒÄ|²ø[ÓsmsðwÒdÀ¿r‡Sñïe`kR7&OÝlaûÜA„Iúi;r¯Õ™¹ˆt^ö½².(i2Ò¶î^ë ð,°¤í§sˆâï ³“ÞÇg-~õÑ×¶qçgE%ÿî)ž½6&MºÅÀÿÓ^Ö·}cî ¡¾b “b¦ú ҃螀ËÓóËX™(Vú·'mçþü@¯*ñÅÎය]—ô%` Ò®€Y2ÇéÔpRÅåßEËÀz“ôWÒÊY/9ت̾êE·ž¿“ZöªH»ï)>ž´=2kªG4g%혴k`FRÇ—I€»I5iî˲Hšø °'Ñ…iB> ì=2wPO1¤ù€ó¯äÎÒc' Pþ¯“‰Is‘¶;oD;Îp÷ªgHg2ϲ}î0£“4i7ÉvůMÜNü ðSÛwå>MÒ¦¤sê½ä÷ÀžeNü…_/¾ZÖ5[d(éXÀ=¤-ÉwÿŽcB¡EM¥­Hþzm×Ò@lu˜b b’~DÚ.ØKJëæ:`cÛ/öõÅßÛ ôf«¬6{˜'žÈft’æv%m-nZIç¡WæŠkI³÷“ ¨õ»Û>´Ì‹JZ‚´ò?}™×m¹Á¤·‘º¸Ülûݼ‘BUý·v#jd Ä-ÀÚ¶_Í$ÔGLT¤8³t0é>ä÷"°P_ª@Kúð§îG ™ÝAš 8»f%¾ìHzš.sœþºØ$WaÎð±bwÉõôÎvõ‘Àv¶O(ó¢Ådð©4oR®n>$m¿ÔQä&ÛýéZª8Z³°31ÉV–'IÅOÊ$ÔCLT@ÒtÀ¹¤ö_¡>n–ß eÑÓùf`ÒÊR…Ü> míý3pY]V°‹ÕŸ’Šštþñ]`Û'åÒË$ý øßÜ9*2‚T”²Ô£’ö&ý6ñhNÝî%MÜ@š(­SC¨?I_'½¿mJjïÊõ6©ê¹ƒ„üb ËŠs‚•Jëj[ÛÇíEk§»€«jäEÒyéSl?š; |´’»°°@æ8ýq!iPöZî ½¦˜È¼…úöJ/Ó‡À¦¶Ï*ë‚EŸñãHp¡#IÇUn í\¹ÉöY…®(Z’î¬JtÄê¶Iò±«µÇÅ@IZøÍÛ¶ÛKž¾:¶ªå’V%­‡`ÒN“óêÐ×¹˜ Zø-ðí¼iúìyRýré’¦$mµþzî,lhûü².(é³ÀyÀòe]3td©¨àÕÅÇ­¶‡å:UTôß4ðŸ?sœ^ti" sö¨˜èI›“V bëxýmnûÔ1SÒuÀÒ•§ u÷6p pŒíÇr‡)&~HêP1_æ8}1‚T ðãá£û$l“;GÞÖµ}qY”4iø›e]3”æ=Ò¤ì¨ ûÚÔz°­$Í@úyô3R[ÅÏù¤ ù˜HëA1P²âa|ßâ#4Ãõ¶—ý7$ÍJ*šÛѸ¸†Ôâü܃ÙâgÏêÀþ4cGÀ ¤‡çsi+I«°m?³þ°–í+˺ ¤ùIÇ÷¢íX3¼Fêðs5p•í'3ç £‘ôR1Û-‰óýur;©8`Íë11P¢¢Wé¤Õ¸Ðæ´ýԨߴpP¶D¡ižŽþœ»p•¤A¤­•¿¦þÛ¾_6³}iî m#i&à_Àçsgé²!¤Øëʺ ¤µ€3‰J“=ÁÇ»®³ýzæ<=§x/Z ØX…öOD6ÕÃÀ*¶ŸÉ$T'&JRTú¿XlÔÿt;°HÆ<¡™†§‡å.X ÜŒTµ¼Î[- ìeûƒÜaÚ Ø r1©¨V›½¬fûæ².(igàPb÷W› öŽÈ½S«ÏÛ“ZùÍ•9Nè›çI“äª%4 p90Oî,¡cWÛ^>zóz˜(o¤Ð`#¿‡Ø¾=g¢ÜÀ/¨÷Šæ­¤EÏåÒt’¶!Õ i³7I¬w”q±bÂìÒjeh‡ç€ã“l¿”;LÛIZ€t¶C`ŠÌqBÿ½¬mûúÜAB÷ÅÀIú&p0Kî,a@Þ¦·=DÒºÀ¹¹…Ö¸8¸ÄàJú2pðc껺ù*©.ÀU¹ƒ4•¤¹€û€©rgé¢×mßSÆÅ$MœEª¡šmTm–c€‹l˜9O«Iš X4ð_4sœ0pÀõm_”;H论>6‚¤ÅI÷1øo¾É€…‹ÿ^,gÐ:ß.´YÑþ¨r¶Ÿ³½ð]R¾:š¸LÒoŠó£¡ŠUìÿ£ÝƒÿW€eJü‘ôï!ÿÍö©µÙܶW°}A þ»GÒl’~w˜†ÙvסyXº¬sª’æ#UÁ^ Œë…,vf±½“íGrj+%+Jú;©Èâ^¤IÛÐ.“gGÉBKÅ€HÚ„Ô<Έ·ËßuIç Ú¼‚êáYà@àdÛÃs4 °3©mé”92LÀ3¤íˆ¥œón3Ißî$ífj£H+ÿ¥×”´ pðÙ2®*eà àHà Û#3çi5IÓ?!õûZÞ4¡Bö°}Hî ¡|1ÐO’¶"WŠÝíó"°ð`î ¡§¹C„rÅ ¶$íHª(ní43°Zî¡çÌ  <*ië5l?mûÀZ¤ ‰:™8RÒÙ’¦Î¦¦ö£½ƒÿ—€eKüïœI þ›ä1ÒN¥/ÛÞ1ÿÝ#iIÛIº¸ø)1øïu¿’tDÑ^6´Dìè£â¡á÷¹s„®»(fòz’Ô³úì[[‹¶û’¸'©úþð`]ÛåRE1Úhç‘´—Iƒÿ‡z¡¢¨äÀŽNª`àJÒΤËc›wIZˆ´Úÿ#âd»?;åìfÊ} é7¤–Ð~#‰¡î~iûš7/ ¤KýŠÊ ¶±}fî ¹íëîæÊ¥ ^! þ<Ù#i à à‡Nºm0pi›ôë¢bGÕFÀÖD!ÌÐ7Ç;Ä$@óÅÀmNöÊ#„г® ⹿ê[þ~ ü˜¶êûOÀqÀζßÏ$IÇ‘VíÚæUÒàÀõXŠNÿ ~YᓎN±ývî0m&iÒÏ 8Vúë8`û˜h¶˜Iÿ ü*wŽBÏ3p°§íÿV}sI_N¤~½ÒïÖËñg’›¤˶Ë|4øp«?I³—ßèµB×Üü8ßöˆÜaÚJÒäÀ¤Õþå3Ç Íw2°uÍi®˜Iûÿ“;G!ŒféaùÛƒ«¼q±`;R-”:…zØÄö¥¹ƒT¥XÕ~€T¸´M^–+c·K±Êy ð…§ eÂÇÕüÿ“;L›Iú6iµ#`šÌqB»œD:Ž“ c!i7 ú^†êê`à̪·áIú éáý{UÞwF¿ì…m‰’þ ¬Ÿ;GÉ^–·}ß@/$ieà\¢˜YݼoûÜaÚª(äºiàÿÝÌqB»lÛ ï»mc´©êlÛ¶U†ÚçRUÞ{ª¼©¤‰€ÝHÅQëÔNí<`sÛïæÒ-’~œ•;GÉÞ þïè…$mNj×[·½ìAàPà¬^®ÙÑm’¶$VûCµŽ°½Kî¡b`4ŃÃIDøBsŒ$õ5ßÍö+UÞXÒ7I»ëTAú`­6n-–4#i0õùÜYJô6°‚í»z!I{øuq p0pq¬v‡¤iH­û¶¡^?‡Co9Àö>¹C„¾‹ €‚¤M€S‰Á¡™Þ~gûên*iÒâ¾{Wuß xØÈö¹ƒ”IÒÀZ¹s”è-ÒàÿŸ¹ˆ¤‰I-+·,%UˆáÀÙÀa9:—ô IK¾ß×>“9N{Û>0wˆÐ71HZ•Ô&¨.¯!„Щ{IÕy4¨ê/I‹v|­ÊûŽÇ`oà6¬>JÚ˜Ô˾-ÞV´}ç@.Rœw>Xµ”T¡SïÚƒiûùÜaÚHÒ À¦¤ÿÜ™ã„06;ÛþcîaÂz~@Òwk©WUëBˆ‘¤ãL»Û~§ª›Jš8‚Ôjª.Î~j{Hî ’43iëÿt¹³”äÒàÿŽ\DÒ瀋©WAÊ^ó*p ðGÛoæÓ6’‘¾¿7)>¦È›(„ñ2°íãs ã×Ó’¾ ÜL»ÎS†Â(/?·}^•7•´)p4õ©Âþ/`mÛÿͤ’ÎÖΣ$CUlß8‹Hú"p90_)©B=IjIz‚í÷r‡i›bÒoS`+`®ÌqBè‘À†¶ÏÉ$Œ[ÏN·³åÎB]v1°½íg«º¡¤¯“λª{NÀÀl_•;HHÚŒTŸ¦ ÞV³}Ý@."éÀÀ¬¥¤ ýñ/REÿ¿TYk¤«ýË’vP­Et²Í5XÃö•¹ƒ„±ëÉ €bÛàÀ¼¹³„BEÞ!‰?ÖöÈ*n(irÒ`aû*î×{Ø>,w¾(&ª>—;K †“º3\6‹Çö.f(%Uè«ëƒÚVX³$Í üØøbæ8!”å`™ªÛ‡¾é¹ €âôJ`ÉÜYB!ƒ›€Ím?QÕ %ýT“ .Ù3­ê¾uYÒÅÀj¹s”àC`=ÛÈE$­üú-i;“v0Ðz á“$MFZåß’´ê¨B½ ,aûñÜAÂ'õÔ€$!õL !„^5„Զ*äKš 8 X¬ŠûõÁ=Àšu­X.é'À)¹s”`ðcÛgä"’6 u™˜´”Ta|F’&ZˆV~å*ޝlE:ß»XB/x‚4 ðRî ác½6°©_u!¸øIU³óE¿ö}€_S¯H“wç2ºmý7°írIÛGQï™65ðß×ö¿s‡i‹bµMÒÙþååMBå¾oû­ÜABÒ3’6"õP޼!„ð±ÁÀî¤jÞUíX‘´š;S÷›€wIÅ/É>Ú©v)°rî,d`ÛÇ ä"’ö~[J¢0.ÃI…&nj§Œ:* ¡nM¬ö‡p5°ªír =2 é{À5Àä¹³„BM] lYU§I_ u XªŠûMÀ`gÛÊDÒ¤z M·›íC;}qQýHàgåE cJú^;Äös¹Ã´A±ÚÿCÒÀ)bÑ)„ÑýÙö¹C„˜4p0cî,!„Pso“Úþ¥Š›Gv¡ÊûÙþm®›Kš…´Uò³¹2”ä7¶ÿ§ÓKš”´Cdƒò"…Ñ Ž'­ø¿œ;L«ý[›«ý!ŒÏÞ¶Ì¢×µz@Ò4ÀÍÀ|¹³„BƒœK:»ýf7“ôàÿ¨ÇÀw_Ûû縱¤ËhþÖÿÃmïÚé‹%M œGóÿêhÔVÿýl¿9KãU? Îö‡Ð6±}fî ½¬µÅöÁ iG ¥B¨Ú“Àƶo«âf’æÎ¾^Åý&`&Û¯TyCI›’&Ašì¶wîôÅ’¦'Õ?øny‘0 88Èö‹¹Ã4¤¯‘ý±ÚBg†ËÙ¾5w^Õæ €¨øBó!ð?¤v`#º}³b×Ö©ÀÚݾ×x\c{ù*oXÔCx˜®Êû–ì`ÛN I®æ)5UoœHøÇŠÿgû×& ü—&VûC¨×€Em?‘;H/j値5€¿-ƒB¡ ·z¹?ÕíUð LÔíûa0¿íG«¼©¤ó€uª¼gÉN'µ“ÙÉ‹%ÍF*Ô;W©©z×û¤â~Ú~>w˜&+VûGíZR!”ë?¤I€·sé5­›ôUàN`ÚÜYB¡EÞ¶³}V7“´pÕ>tïaû÷ÞIë’j.4Õy¤6Ší)Þ³¯f-5UoútŒdÛÏäÓT±ÚB¥.Öìt9t¦U’¦n¾™;K!´Ô‰ÀN¶‡uûFEUüó¨æLø¹ÀnaïDqæý!`¦ªîY²‹€u:íë,i^Ò¶ÿ/”šª÷ŒÎ!°¬t÷J›HšØØœXí¡JÙÞ+wˆ^Òš €bÛèÙÀú¹³„BËݬoû±nߨX;’´×-÷‹ÛÒÅ{|ФӀMª¼g‰®Vït"HÒ‚ÀåDµºøµíûsi¢âÙq9``uª?vBH6²}vî ½¢M¿þ;Ghœ‘D­ˆ:ñ°¥íJ¶¯KÚŽ40qÉ—~XÂöÓ%_w¼$­JÚúØD·+Ø~·“Kú©ÚÚ>6ÕµÀ¯lßž;HIšØ”4ðÿFæ8!Jz/¾7w^Њ I‹7“äÎç*àËÀܹƒ„ÐPv³ý~·oTÔ8‡òªå¿ |ßöJº^ŸÝf©ò¾%yXÚö¼XÒ²¤½S–šªwÜAø_“;HIúð3RQ¿©3Ç !|Ò3ÀÂU·áíEŸ4-p0Gî,¡qÞ6&õ;˜*oœë.Òùù'»}£¢hÜ… |Õîm`YÛ÷ W†ìaû÷ý}Q±ÃãDRÛµ0v#€“€}£À_ßHú6°;°>QØ/„^v3°t·; õŠÆLHÚ8#wŽ–¹‚´}îë™s„ê,kû:IkåzÒ;ÀF¶/éæM$ ²=²›÷èC†MIíïšâw¶Õß5N#M*‡±»Ø­Š¢˜m i)`/`Eb²:„üÆöÿäјôeà_Àçrgi™Á¤3ºñæÚ~ïûØ> @Ò€c‰I€ÇÒJ󡹃t‹¤€3äÎÒGÇØþY_$i2à`Íò#µÂcÀ®¶£ànHZÔÖsÉÜYBµó!°¤í¨e5@µŸ(¶P^Nš!ôÝ;Ày¤žì—Ù:æHšXTˆjñjã…ÀŸílϤl’N6É£Î6ëïŽ IŸ!Õ‰÷çO{—t¼îˆha5a’V% üÍ%„PkOß)ꘅ5a`+Rû¤Bÿ$õ—>Âö;£>!i1`g``ê<ñBàF`Û¯Mð+¢Xż2wŽ>ºôç߯"°’¦.!VjÇdÒ„Êž¶_ȦΊž5Iÿ3Ç !4Ç©¶£ÞÌÔz@Ò‡€isg ¡žæ·ý¦¤MH­ Õ $´V%Å«P¬Šÿ ˜+w–>¸XÉö°þ¼HÒ´ÀeÄjí˜þ ìdû¶ÜAêLÒ ``à[™ã„ši8ZÕ¹º?üÿ‰ü‡P–Y€_Jšøõÿ÷zÇœÀ­Å6à¦Û—f þÿü ƒÁÿt¤¶±1øÿØ+ÀVÀ"1ø·¢MäûIu#bðBèÔqÅdtè@m’ÖÖÎ#„–Ù€´e÷󹃄0†i€ %í;H§$ÍOj³YwO+Û~«?/’4#p-±]{”¤…НÛ>)w׉:“´i‡Ä߀y3Ç !4ß—€Öî¶ZôYÒÖÿ/åÎB l“;Dãq$°K“TŶæ›Årg™€×€%l?ÒŸIš ¸ ˜¯+©šçn`{ÛwæRg’– C\*w–B+­bûòÜ!š¦®;%ÿ!tKôêu·pޤ)r釩ÿà0iå¿¿ƒÿ/7ƒ€×IÛý¿ƒÿq“´„¤ë€›ˆÁ¡{Ž+ŠÒ†~¨ÝIË’ÎFoúBèm·‘Ω¿š;Èøí4¦Êe<ÞV³}M^$i6àšQ× ›F’ÚVîiûõÜaêJÒ‚ÀÀJ¹³„zƱ¶·Ï¢Ij5 i2Ra¢¯åÎB¡žVµýhî ã"é"`õÜ9Æc$°¡ísúó"Is’ÎüÏÖ•TÍq/i»ÿí¹ƒÔ•¤¹H[ý7 pBÕ ,»²ú®nG~A þC!|l.R‡€ÅsIëSïÁ?¤Ötýü´í¿—ÿ,ƒÿ±“ôyIGÿ&/‹Á¡jƒ€£ŠZ<¡j³@Ò,¤7)sg !„P;ï?µý—ÜAFiHÁÚýmœuÇ© IDATïÛŸHúiÛÿ»©.~fû™ÜAêHÒ”ÀÀÞ¤!„Û¶ÿœ;DÔi¦äpbðBaì&Îô‹ÜAFó{ê=ø?¾ƒÁÿ·H+ÿ½:øXËö1øÿ4I“HúéhÎAÄà?„PJš6wˆ&¨Å€¤€urç!„Pkþ é’²n5–´$©|]ü¬?/( ¸] |¾+‰êmp0íäSG’ÖþÌ”9N!ŒéóÀ~¹C4Aö#’&5H!„&9ƒt$àƒªo\¼oÝ ÌSõ½ûèzRoäa}}A1ø¿˜®[¡jì_À6qÎìŠïÀïçÎB0XÐöý¹ƒÔYvìJ þC!ôÏË2õÿÝ›úþÖîçàqÒʯ þß#­E‘¿±ô%IÇwƒÿB3Lü)÷.ÁºËº@ÒG¨wïäBõu'°ºíW«¸™¤¹I«ÿ“Uq¿~zXÂöK}}¤e€‹è½<×[Ùþoî u#i*`OÒÍ™ã„B'6´}vîu•{ÀÿƒÿBû.p³¤Ù»}£¢ÅÐÉÔsðÿ2°R?ÿ+—Ð[ƒÿ·­åcðÿIJ6~E þCÍuHÑ­$ŒE¶ Ió›åº!„ÖøpkQÁ¾›vëò=:1˜tæÿ‰¾¾@ÒêÀ?è­AÞEÀ7mŸèÜjFÒBÀ­À©Àò¦ !„û2°sîu•퀤«€å³Ü<„B½ üÀö e_XÒœ¤bqu[Qø€tâʾ¾@ÒÚÀÙÀ¤]KU/¯?·ý—ÜAêFÒŒÀÀäßBez ˜Ëö¹ƒÔM–ö’V#ÿ!„ÊõYà I–yÑ¢˜ÐñÔoðoRõúþ þ×þJï þÏ%µö‹Áÿh$ ’´)©­ßVÄà?„Ð>Ó»çQG•ï41©í_]+(‡Bh¶‘Àn¶/ãb’¶&MÔ;¶÷ïëKÚ8…T%¹í^¶µ}aî u#i à`¾ÜYB¡Ë†_±ýbî u’cÆwKbðB¡{‡IúcQ¸¯cE·šƒË‰Uª“û9øß‚t¾»ÿçóÅàÿ“$}NÒˆÁ¡7|†TÔ4Œ¦ÒE¿æÇ€™*»i!„^v.°©ía¼XÒÅÀjåF°Ë€5mØ—/–´p4Ðö¾È¯ÛÙ>?w:)ްlLŸ9N!Tm8ð ÛOæRUïØ…ü‡B¨ÎzÀ•’>×ßg¤ë6ø¿X¿ƒÿéÁÿyÀ¼1øÿ¤¢3ÆÍÀ‰Äà?„Л&~›;DT¶@ÒtÀIEšB!„*= ¬jûé¾|±¤YIUÿëôžõ°˜í—úòÅ’ö ­ú¶ÙëÀ¶ÏΤNŠþ×û?&Î'„rÌoû¡ÜAê Ê¿¤^R!„zÇ<Àí’˜ÐuN¡^ïYo«Äàÿ.#=ÐÅà4’V~A þCRý›>×Íi»JvHš x‚úµP !„Ð[ëØ¾j\_Pl›/¥ƒ@I†+Ú¾©/_,é`ïîFÊêàç¶OͤN$ÍHú¾Ý8w–B¨! Û¾;wܪÚ°71ø!„ßÔÀ%’¶Û'%Íü®ÚHã5’Tݯƒÿ?ÐîÁÿ¤UÿSs©“¢ÅãÃÄà?„ÆEÀž¹CÔA×wHšx˜¼«7 !„úçD`GÛïHš ¸X0kªOÚÕöw#•Þ'ùn£áÀ¯?Ø™;L]Hš8X)o’Bh„ÀܶË$§*vüšü‡B¨Ÿ­€k%ÍVüïC©×àÿˆ~ þ¢½ƒÿïÚþ} þ%;’þlbðB}3°{î¹uu€¤¹€“tí&!„ÂÀ .%µ ¬‹ó€ &4à-ÿÇÛT’ªZ##€_Ù–;L]Hšø3°tæ(!„ÐDïsØ~1w\º½`ObðBMóxî›’z þo6éÃàéCÿÏ+ØþE þ“bÕà~bðBšŒöî˜ë“®í(z(?LÚ•„Bè–m€cH[åBµµýúø¾HÒDÀÉÀf•¤ªÖÙÀv¶ßʤ.ŠzJ'+æÎB-ð0«í·sÉ¡›;ö ÿ!„Ð4w§’ÎÇj½¬ÑÇÁÿ©´oðÿ.°¹í cðÿ1I? ÿ!„P–i€ír‡È¥+;$}x‚(þBMñé¼õá¶?´°?°@Î`=âC`ÛWï‹$M œü¨’TÕù'°Q¯Wf¤™€Õsg !„z‰T  çŽ™ukÀîÄà?„šä‹Àï«%M"ià"bð_•û0øŸø íüÁÿÇ$mHªðƒÿBèŽ/оt}RúI3OŸ)õÂ!„ª²%©²|q­Æ‘¶Ç[HÒ¤¤³ñkW©/›Nh⣗ÏPÇëäÎB=à1à½Öb¶;v ÿ!„Ðd»ƒÿª\ì:¾/(ÿçÒ®ÁÿÅÀü1øÿ˜¤uH«þ1ø!„j|X)wˆª•º@Òg€§J»h!„ÐNß³ýθ¾@ÒdÀ߀Õ*KÕ]ï“&˜þänµ!jIÓGåÎB=èbÛkäQ¥‰K¾ÞæÄà?„qyxx­øx½øxx³øüÐ1¾~(0Eñ1íÿý`Ò¦Y€™(ÿßt¡;^!Uüßà àÚ³:ñ°¾í{r© IK“Š:~9s”P/#Iσ·IEBÃ!À{Àè…˦âÓ»¶>7ÚÇt¤ç†§­"ivÛOåR•Òvm‰æ,å‚!4“Iç‰$=ì>^|<<ÛÍ3F’‘&¾DÚÒ4/0wñëœDO÷êâ}`9Û·Œë Šuÿ–¯,Uw lÕ«=—ÇTtsø-°'ñ³¹× ž!=/°«ícr© I+ÿGjËšå=àÒjÿÕ¶GdÎÓhE°ÝHÏ Ñ^04Åö¶Í¢›:­ÿB“¸øí;s‡iIß~FÚú7Eæ8!ÔÅŸlï8¶OHš‘Tkä[ÕF*UlùM±(ò;`W¢mZÓ< œTÇV¾M'i.Ò¿‹ŸŸÉ›&„ ºÍö÷r‡è¦N\,_^œºb© ß½^°§ÛŠíÌ[ÛsdŽBN×+Ùþ`ÌOÛï¾YyªòÄ–ÿÑHšôg²PÞ$¡ŸnŽ.ˆÕþî+v=íIZ0˜Àw2Ç ¡jO‘Šþ½6æ'$ÍLüÏ]u¨’ 'mù*ÿI«ÎŠGá³fø€Ô’ópÛwåÓ‹$Í ìlB õ´¿í}s‡è– Þ·àëCè¦ËùmoƒÿêÙiû|Ra³58rzÅ`­q þ¿\Osÿ/KÇà?‘4HÒ~ÀÅÄà¿ †¿æ°½Q þó±ýŒíÍùIG3C¨›\Z©£’&ž!Šÿ…úyØÍö¹ƒ„O’´2iG@ meàG¶ÏóÅàÿ:à«•§*ÇMÀú¶_Ê¤Š­Ìg+æÎ&h(p,ðûháWO’–ŽæË%„Ñ|ßöM¹CtC§+ø«ƒÿP/¯ÛߎÁ=Ù¾ÜöÀr¤ÁDmsà8ÿ_®¥¹ƒÿ#åbðŸ•Íï&ÿu7 ø#0—íÝbð__Å kA`Ò„Mu°IîÝÒé€óI-=B¨ƒsI-;>µå6Ô—¤åIÅ—š\-„Q®V³˜¤Ï“VþçÉ’j`†‘~¶ž’;H]HÚšT-~ÒÜYÂ8 N%á}>s–ÐOÅn©#æÎzÞÛÀ̶ßˤlýž(ª|¿LÖ•D!ôÝ3À¶¶/Ë$tFÒ$¤Ž¿¦Ï'„NýXtÌöaEµÿëhæ™ÿÿëØ¾/w:4%p<°qî,aœF§þÏäFÒº¤‰€ØqrZßö¹¹C”­“#ëƒÿ×HÒY±ycðßl¶?°}$ðàÀû™#…Ð_o“Šþ9øÿòó+Å¡ÅlŸHêpkî,¡çLB ëÞõwÀF@k{"†ÚúØ XÑö˹Äî³}°péï?„ºù+©§øG$MOš¨lZ+«×l•;H]®#U$gžzI*ð÷UÛ¿µ=,sžPÛOK“~îö¿‚ykÝ@¿ŠJºŸt'„ªN™;Kè ÃÏYë§Éú¼@Ò׈Á¨ÖµÀ‚1øïm¶–¶ÞÊ'„ÁÀºc þ?\Eóÿ‹Åà?‘ôcàzbð_7À_‘ŽÆà?PTe_œT"„n›X-wˆ2õçÀº]K§@¢ Û#mOª¨~aî<¡gØÂöFýÆhƒÿ²¥êÌ‘¤î=_LUÒ â¼ÿéÀä¹ó„O¸˜ßöïlÇq°ðÛ÷ß%× ¡ÛZu  ?ët-Eìb{›x³c²ý’í[ïäÎzΣ÷–4-p°`¾Hý6ØÙöÏmÈ&7ISÿ ÷õñ°°¬íGs‡ õdû5`EàO¹³„Ö[EÒ¹C”¥O5Šêÿw?NèqƒIçý/àW†ž'ivR1¨¥² ½âfÒ`äIŸ®$­@5ÅPRÛÂäR’æ$í(úfî,áÎv´ýRî ¡9$íHî ݳV[Þ?ûºà]M¼,ƒÿÐW¶Ÿ"µ ÜtF4„nyØ`´Áÿ4Àå4kðÿ2°t[^JÒ2ÀÄà¿N^&=`¯ƒÿÐ_¶&íéùM¡kZs  ¯­*|jçÒÊZ÷ ýRÔ8X¸7wžÐJ’v&½­ü_,š5Uÿjõw5°XÖDýs=°x±c¦§IšDÒÑÀ±À$¹óÞ¶´½–íWs‡ Ígû`U¢NP(ßt´äØi_v¬H¼Q†îx øþèµCè”ílïIÚ±ôZî<¡.4#©5éBYõÏÀ*¶{¾}¦¤éI;7¶Ï%|äVàÛŪm¥±}°1 Ê׊c}™ˆíÿ¡^–‹þÓ¡l¶/¾C*ÚB§^%­LZÒÌíÈòFê—“õl÷üVXIówj†„ü>ö!-<‘;Lh§âXéjÀÜYB«¬’;@Æ; i-ù?jåM`¥ü‡n±ý° pq$ tf;Û/Kú6p;0Oî@ýp°U´ùI«·såÎø°˜íâû3t›í›µˆš¡_EÐ3†«Ù~ wÐn¶?´½q$ ôßM¶ÿ&i ÒN’Ysê#»ÚÞË}éñÛr’v&µù›&w–ÀñÀ¶ïÎ$ôÛWëÃsg ­±Bî5¡ €å*IzÅp`Û·åzG ,é&à$`ÊÜaúè`3ۇ璛¤A’§ïÝŽB÷¼ ll{[ÛïåzíK€‰¡ŸÐø $]JåÙÜö©¹C„ÞT´nùðóÜYB(Ù0Òyÿ‹sÉMÒäÀ餿ßÀúQì7Ô¤#sç÷0C“1sf\Ò¤Dû¿PžÃcðr*ŽìLZˆU¨ÐC5cðÿQ›Æ+‰Á]œN:ïƒÿP ¶ŽÎ#4Þ´À¹C Äø¶Æ-LUUÐjW¿Ì"ÛžÎ%„¬aûªÜAr“4p ±pQÀmloj;*°‡ºù9pIî¡ñ} `|ËT–"´ÙãÀ¶?Ì$„Qlß ,J0„ÐDoËÛ¾6wÜ$-DªôÿÜYÿ¶}Bî !ŒM±m{càáÜYB£­˜;À@Œo`‰ÊR„¶¬eûÜAB“í—H}ÁãA54Íë¤Áÿí¹ƒäV´ù»˜)s”g’ÿæÂøØ~Xƒè:·ˆ¤Æv˜ë€¤‰HGBˆm?”;Dãb{¸ím€mˆA¡^–Vj é§À4§SC[ö´ýcÛïæB_Øþ/° ©}jý5 °tî×€ùˆ¾¹a`ε}Rî!ôE±]uyb5 ÔÛKÀR¶ÿÕÉ‹%Í\rž,”ü/p2é!,äó°²íƒs ¡¿l_š;Gh¬ÆÖ×À╦mó,°mî!ô‡í›H;Ÿ¢bu¨£W:­¨.i#à¿’6)7VµŠE§¿Ê%ð(°¸í«s aö"Õ ¡¿{\~\ß«4Eh“ ãÜh¢bKàÀ͹³„0šQgþ;:[-éçÀÀäÀ)’~\f¸ªHú,p)ÐÈü-s!°P´ø MW©Þ˜TX5„þ˜OR#;æk`±JS„69ÌvTVeûuÒ¶®³sg xXqÛþ÷ŽTüÖDÀ©MÛ éKÀMÀr¹³ô8kÛœ;Le°ý$°Uî¡q&΢Ÿš40{õQB <ì—;De{°p`î,¡§½M:_}O'/–t ðë±|j"ÒN€FLHš‹´+g¾ÜYzÜPàG¶÷±=2w˜Êdû<à/¹s„Æiä¢ùØv,ÀÇ+!ô•ílÍ$„28Ù›´*ðaî<¡ç Vµ}g'/–´;°çx¾dÔ$À¦\¿*’æ%­üÏž9J¯{XÂö9¹ƒ„ÐE»Ž\…ÐW­š¡¿N·}Uî!”­èfñCÒêWU ¬nûÖN^,i  /UÙGMlÖÉ}ºMÒÂÀõ@+º4ØÝÀ"¶ïÍ$„n²ý ð‹Ü9zØÑ¹t`QI[8 €P†×ˆ˜¡Ål_Dª Å-C· ~hûÆN^,iàxú¾“oðgI?éä~Ý"i)à`úÜYzÜ%ÀÒ¶_Ê$„ŠœDg‹<ž:zïËhૹCôWL„2ìo;ú§‡V+Vc¿<—;Kh­À&¶¯èäÅÅ ùLÒÊ~ N–´y'÷-›¤Õ€Ë€©sgéqÇkÙ~7wªbÛ¤VÖ±ë¯z_¦™˜w à’¦¾’)Kh¦GI !´ží‡HmRÿ;KhQuT::c]Êû0Y‡÷œ$é§¾¾’6.¦È™£ÇØÓövE‹´zŠí'ˆ"À9ÌFúùß´"£‹æÐ_c(úgÛäBUl? , Ü–;Kh•=mŸØÉ %M \ÄÀ·ËN,jT®¸ï™À$9îxØÈv_jH„Ðf‡/äÑcæ*Ž5­x³wßÌ’"4Õ ¶ÿž;DU³ý:°<é|lu°íßwòBI%Mà—ap‚¤-Kº^ŸHÚ8‘þ_åyXÑv·à†P*ÛC€ßæÎÑcæ4¸ïçãáq§sÖ~»÷û=笵×ú|8BÒŽ¥’Æ’/6+ÊÊ8 ØÒ]|Ìzò[\ `X–(~ô@&]xíTÕ.7â¶uº."ÎϬ©"â0`[` ­ÝÖi“&ÿ÷ òäbÛü/€×UšªÇË?´ÓÀH³§“ŠgZ>߈ˆ‹ãKf6E–ƒsç‹DÄ=ÀßòFéË|’΢W]\¸¸*wˆûAîfMÇÆgŽbÍñiÛÿÍ%ÆØX£¢<ƒpxQ|°¿'J³¿Ö©<•õj©èäÿåbÖB'®•Q¿Å&ùyÛv,—;@¯º¸p&pFîõ(àþÀf=ˆˆÓ€ÏäÎbÙ°MD\4è’> |¹ºHp˜¤Ïöü„Tcè7ä]¼uã-"⇹ƒ˜µQD<‹o‚ ÃÂÅn7hß@kŽtmàZà|Ò$õ¯™³tÑ11.w³¶ˆˆs5'rg±¬¾¿ôÉ’Þ]až²|_Òç¦û@iNÒùÙUkOeSó°nDœ’;ˆYˉwöÕm&`Áâç“v.µEk:L^°­ÆÇëEÄ„¢²òj¤í:¾ûV ¸×¹Yß"â2Òäç‘ÜY,‹FÄAƒ>YÒÀ¯€Y«‹T ‡JúüT Í œ¼oh©lr«EDÛ5ND"îȬ"â:`\ tÔœGê 1¢]Þ©4·E¯€C$íò²?HÝ ~¬4ôT6Ñ}ÀÊquî frbî#`Ò:Ì–¢‹çЫ‰;æfÉd@Kßî’ô½âîšH:ø;ð šûª-ü‚gVBDÜ ¬ Ü;‹ Å-ÀfQ¦5í¡4ÿîùÄE€/ü÷7¤W+dKe·¿äbÖ1g‘:ºX}šäçmZ˜_Ò¹CôbâÀbÓ|Tó½øw«Å8RÁµ;@Ò{€Uihön¤E­:èZà}áãFfõñ®ØzMºpðP® XYÒÌÀi¼ôü£Y/.Ö(^W̬F1ô=gõ˜²_·©À¹ôbâ@—>l,¼.wˆ9¿h«hf‹ˆ§€u€ßçÎb¥û–ãûÀ;«c#å7¤c'Oçb6BÜZ³>m^X0w€^L\˜ü/ºÍf,þ³jøî¿Y"â`}Ü[¸Í.v(3€¤í€í«‰c#äT`ã¢0™™ ê3ßd¿¾Ÿ#ÈZµ amèOJÌj〠Hí…¬]î%MÀþp"iEàðê"Ùˆ8Ø¢Ìמ™ ¦è²q[î5錄(>#Ý)K¿ZqS½‹;¬:×ËͬfÅQ›M€_åÎb={øpDükÐжy¿f®,•‚#m"âÅÜAÌF˜wÔc¬¤Y&û½¶hEûyï°iñö³!*îä}8%w›® MÀntIc€“éV'«ßÁÀNE!23˧-“Ò6šs²__™%EÿfË 3Hš ˜=wk¤‹r05ñ<ðQàç¹³Ø4íS¦âáàýd±ÑñíˆØµhefy]—;@‡Í5Ù¯oÊ’¢íXÀÛÿmÊ^®ÎÂlÛz?œ”;‹MÑéÀ7Ë i à Õı±OD|-w3û¯;Ií²­z“/Ü<“#HŸZ³0ù 3€ÝRÈ,Ÿbàãx'@ÓÜ|¢ÌXIoŽª.’€ý#b¯Ü!ÌìŠ÷¶§k›—,Ÿ‰þœ)K?¼`­æsMf™ox[‘ŠÄY~“Šþ ¼8*i^R¡G½³^í»åafSäcõ˜|ÀŸ†ž¢^°Vó€YDÄ ¤šî×óÀ&ñ÷A4p"ðÚÊRY×}="öÉÂ̦êúÜ:jJqÚ°0ã:4Žlj|þ߬!ŠÂ€ÎÈe„í—–co`í ²ØhØ3"¾•;„™M“wÔ£­ Ђ]^°)y¸'w3ûŸb`3Üž3‡"â°2HZp7ëÕî±oîf6]wÏåÑAc§ð{7=Å`¼`­ôg·2kžˆl\–;˹ء̒^¨’DÖu»EÄwr‡0³é+jõÜ;G½l "þ <š!K¿Z±0¥" 6ÚÚ²Âf6r"b°.Þv8 ç‘4+p20we©¬ËöŒˆýs‡0³¾Ü‘;@Mi¤Ö‹M׊€9r‡°ÆiC› ³‘OÎ’ÿ5w–{Ø""î*9΀·VǺïKÞöoÖJm˜”¶Í”j@;[¦–½1|À¦Ä f þ™;KG}="Î/3€¤O[V”Ǻm׈ønîf66LJÛf¦©ü~þ®ÜÏGlJÊÞñ2³!(ZÒ­<“;KÇœ”:ƒ-iEà{ÕıŽÛ#"ÎÂÌæÃÓ†9Ê ¹LØäßQ4kˆ¸8"wŽù °e™B¨’æN¥Û-»½"b¿Ü!̬”6Ü•îŠ6ü]·bÀGlRE„Û™˜µ„¤€Õsçèˆ'€ #âÉA(þ=N^]Y*ëªý"bŸÜ!̬´ûrè ©-·a·…¬uîÍÀÌú²-°|îÀÖñ—’ãì¬QA붃#bÜ!̬¼ˆxxáØúò ð|îÓÑŠ€Ùs‡°Fñ*¦YKHz5àÂaÕØ/"~UfIk_«(u×À—r‡0³J=œ;À((Žç=˜;Çt´b`jUm45ý›ÊÌI~‚ ¹Vá<àe´8p-xã·¬~|¶L 3k¤Grè˜i½F6}®ÒŠ.cs‡°FùOîfÖ“æÑwˆ@Ò,À/WT–ʺèD`{OþÍ:É ÕzaöÀÐR ¦ñ7¼`“ó€YÃIz#p`îð,°iD>:if“{uîâ€šÍ |FÒ:É;̆HÒlÀéøøMY“Îý¼¨-i ©ƒBmrÖŽˆ§s1³FZ$w€y¨‡Ç4yÀG¬uüÁ×l¸~¼)wˆ–›|<"þQrœïï« uËCÀZñHî fÖ<’æÁÝbªÔK €&·²oÅ€Q^x*w€Z°¸ ff5“´-°uîðq~™$mL*üg6©§u+(*ifݵXîÓËbþLµ§œîœÜhF`ñÜ!̺NÒÃrçè€ßû”@ÒRÀOU’Ⱥâà#qMî fÖh^¨Öß{xÌØÚS ® £zžíyà˜Ü!j™Ü̺LÒ¬¤Bs³åÎÒrÿ$eøÍVÒìÀi¸ƒ½ÜNqvîfÖx.X§"âÑ×ä­¨ðDî™Ü\J³‹Häâ³z}Ÿû/ë`óˆx¸ä8®Á`S²OD•;„™µÂsè^îþÌ\kŠrZ±`T®.ªùÞœ;H-;€YWIÚ Ø>wŽØ-"þPfIÛ[U”Ǻã8`ïÜ!̬5–Ë CîíñqM®Wæ€;«øñ̬)šÉ;Ìj éÕÀ‘¹stÀYÀÁeôfàÐjâX‡\ ìßÂifùI°|îrOkò€Æ××Õ€§€ ŠŸŸ–3HC½IR“Ûk˜µŽ¤™€Sysgi¹;O”™ Iš ø%®Á`/u °AD4þÛ™5ÆâÀܹCtHŽ<“;ÀôŒêÀ9ñ,@DÜ Üž9OÓÌ,•;„YÇì ¬˜;DË= lÿ)9ÎQø5Î^ê~`ˆx7hÃ÷Ùˆømîý´©ŽÑŠÀ÷ßIZ(o*³ÑSÔöY9w޹'"Æ÷ð¸&ï˜@:®Øh30“ÞçIwùßLéñ'Ò6žŸ/1[S-Wœ±1³Áí¬š;DË=EºCûô Uš–¨,•uÁþñ£Ü!úQt¹Xx’ßþpƒ¤äIe6²VæÈ¢Cz­ÉÖäãÚÐE¦Ë \CúþºˆØazÅ}"âþˆøðzà+À´ —cMÆ«äaÖV’VĽī°CDÜVrŒ/®"ŒuÆÉÀî¹CôCÒ+sE§ðÇ ‘v´êÿɬå¼À_­^š¼ ñÛÿÆŽL -´Ù‹ÀmÀUÀ•Àï"¢×V/wJZxi•ïíÀ›WV’¸ù>œ“;„YÛHš“´›h¦ÜYZîȈøy™$½د¢<Ö ¶nÃ]š‰ŠygËNãa3ûGvèq+­™ λnªusó@I3DÄ‹À£¹ƒTàzÒ›ú«û*÷iàÉâÇçHÇ FÅ깘µÔaÀks‡h¹ë/”@Ò|ÀIx!Æþç¯À[·¤1¤¯ã•z|ÊÖÀ9EÝ 3«¤Y€wçÎÑ17õø¸&×9às—®,ꑘYµ6Í cþÏõøØ&/<‘;@/&.ÜŸ5Eý¾ \$i®)=@Ò¢¤¢_fb¶¦Û0w³6(zqŸH³Ï¦µÁÁñë2Hz?°O5q¬&ˆ+s釤O_/9ÌÀ’V« ’™ñßöþ|\­?õñØ&/<”;@/&.Tu^¾éÞKjøÅVÑ“e†ž¨ùÖ, š™Ù´íIz±Á]NÉÊì’$`œ±’DÖ_ŠˆÓs‡è‡¤5¨n7ÑÀÙ’ûúÚRT£mW'¾ð? Üo¹¥6(~ì··î¨Ú>w³&‘ô*à¨Ü9Zî~`‹2wi‹ö®§’Z¿š=¬­8‡9‰# ùš;GgjÍl:$-¸£Fõ®ê㱯­-Ey”=Ê83DDVÌGÉÄB€ïÊš¢=V”´|îfM IÀ1¸ p/&ÿe'j߯Ý,yØ0"þ’;H?$}Ø6Óå·N*Î5›Ù´íH:FcÕê§Ek“þãr‡èŤ[¿þ–-EKHZÔ#×zó©ÜÌâ3 ÿn]×|-".-3€¤õ€/W”ÇÚ-€m#â’ÜAú!iKà›™clüZÒ¬™s˜5–¤Ù­sçè ‡Ûûxüëê R;sèÕ(/ì;@Ë|lbñD³Q%ià€Ü9Zî7”ü;”ôàX|7Æ’¯EÄÏs‡è‡¤GÓŒ¯áµ€s‹B„för[óäÑA—;Ñ{Õä€?åЫIþš-E>Lÿ!6‰y€­r‡0Ë¥(Ôu©0¦ æ`«>ßð_¢Ø®| >‚aÉѱoîýôFàt I[ïßü^Ò|¹ƒ˜5IQ,Óí3ëqQ¯,(›¼sûæÜz5ê;^™;@ }Ƀl„}xGî-ö°YDWQk·[€DÄ ¹ƒôª8F÷+à5¹³LÃ2À•’†Õ’Ь±Š»ÿ{æÎÑQýmmòkRÐÒÿZÓ3ײúJîfÃ$iàxÀ»_wJD^fI¯#ý;4áÌ´åõ`͈xÍFÙG€7åÑQöy°Éÿˆ'r‡èÕŠ€~ª0Úèz›¤Õs‡0¢ivᙦû+°]™$ÍBš<Í]I"k³'€u"⟹ƒôé0`Ü!úð RM€µs1Ë¡X´ûFîöû>ÿæZRT£5Ûÿá¥; E[,»}Š^èf&éC¸cHã€M#âÉ’ã ¼µ‚<ÖnÏ“¾žZõaKÒWhY­‚Âl¤[æb–Áæ¤#1V½Îéó9M>Ъ9ôä ­zCµ¬Þ…»(XÇI𛿴éj«Ï–¬Iú°}Ey¬ÝvŒˆós‡è‡¤Í€ýrç(a&à8I»åb6,E½ŽoçÎÑa7DÄý½>XÒÀ‚5æ)«Ushï°2¾íŽÖqßË¢ÅމˆŸ–@Ò²À‘å±vûVDü$wˆ~Hz7p/ÿ¼Õ6ö“t¤¤1¹Ã˜ ÁWiv±Î¶ûmŸoòÝhù@«Â[vË[çaVIëá¯ï2n>[f€âÌ©À•$²6û-;‹[­<˜%w– í@:àïIë,IK_Σãú]hòùÿÿjµÆK"âARK³^í]|H7ë I¯~œ;G‹=I:§]¶³ÌÀ+Ècív1°mŸÕ¢³*^CÎæË¥ëKZ(w³š Ìš;D‡=\ÝçsV¬#HE®Žˆ ¹CôcJ[Ò¼ Àú±(ðµÜ!Ì*öÀn·SD”ê*#é“ÀVå±öº Ø("ÆçÒ+IcI+–Ê¥Fo®(Žè˜u†¤âWu;3"^ìó9ïª%I5®Ì _SZ¸aè)¬ívõ‡ë I›’úþÚ`ˆË i9RË4mkGÄc¹ƒôªèŽs °jî,C°8p¹¤Õr1«‚¤YÃsç§õó`I ’^ošªßÝ ÙMi uÿ–ÝXà·´¶+ÞdŽÈ£Å®v-3€¤9Içþ½ýr´= ¬÷äÒ§}€æ1DóçHr«Të‚}¥s‡è¸ÿ¿ïó9M¾ûÀU¹CôË V•÷îlmwÝ<³; ›U°UûÇt{ë´Mß‹Àqmî ý´5£y$n&àHI‡¸3µ•¤;çÎ1ÎàsÂ;kIR;#â‘Ü!úõ²€ˆ¸—´íά_}:ÍZGÒ&ÀF¹s´Ô‹ÀG#â¾2ƒHÚؼšHÖbŸˆ³r‡è‡¤UqáÐ]€3%Í•;ˆY?$Í KûÛu¶Á/xN“w\–;À ¦ö…î]6ˆ€£r‡0ëWQ±Ûçþ·wDô»¥ï%$½8¤¢<Ö^ûEÄ‘¹CôCÒHgZgÊ¥Ö&ÕXwˆÒ÷ÝIc÷Ö¥*—ä0¨iU»líÿ”5Æ$R?dkIïÜ»z0ÇFD©‚Ÿ’^œ Œ­&’µÐ¤É©îÃ&é3Àsçh™k$5ùüI;Ëc„¼œ0ÀóÞ Ì^q–*•*~œ“¬nGIò «5J±õhÜòg7;• 8÷ðšJY=O:BrSî ý´©b¸õo!àBI_,^̆NÒÊÀA¹sŒ˜s"bóMÞþwDÜ™;Ä ¦õá÷¤ÞÎfeÌœ^Üí3kŠ=€×çÑB›Dĸ’ã|X·‚<ÖN|*"ÎϤ’Þ œ̘;K‹Í|ø¥¤¹r‡±ÑRý;·ì¶AŠÿA³újgØ4S]ˆˆÇ†˜ÅºkIàÔâ,YV’–¾’;G °UDÜQfb 𷫉d-µwD—;D?Šóëg‘ŠÜZy×z‡  ‹¤€sqËßa{øM¿O*ßU}œÊ´vû?LûëÅÃa#aUàø¢r²YŶÓ#p¿ùAg”@Òü¤;¨î2º~ûäÑ¢[ÈÙÀ"¹³tÌë+$m“;ˆu›¤9H“P·¨¾#âùž·Í­4¸ wˆ2¦7kõökœça#mKà¹C´ÐÅÀže˜¤mš'Q£ë\àÓ¹Cô£hg{ °\î,5+ðÓ¢U wWXå$ÍDÚö¿Bî,#jÐíÿëUš¢Z×UÐ9«é-\ <3Œ 62>/ÉÛ¯mèŠ:æÎÑB÷›GÄ %ÇÙøPy¬n6«àëhØÖÌblM:ðÖÜA¬;Š]Gáïá\®ˆ›û}’¤µjÈS•³s(kš ñ,pѲØèøŽ¤Oæa#ç;À¹C´ÌóÀG"â_e‘ô`¯j"Y ýX'"žÌ¤’¾Œ[…ÓÒ¤#Ÿw—«Èw­r‡aG ø¼•€ùª R±n/έ=…?–´}î 6$½ð¢Sÿv‹ˆ?”@ÒBÀÏqåôQõ°vD<;H?$mBZ4´ášøpVQ3ĬoJ¾;Ë{„TógMÞþÿp]îeõ²pNí)lÍ)©T?q³é)ΞJo¯wö?¿)3@±ïç¤þß6zž6Œˆ[s釤wÇã׌œÖn”´Fî Ö.Åî‘ï»äÎ2âŽ.Ñ2¸É ¿ˆÈ¢¬é¾¹EÄÀ߆ÅF€Ã%}>wë´ïÈ¢en>YÁ›ÜÞ¸è⨚@jyIî ý´$p©8åõ*àI?4[î0Ö|ÅäÿûÀι³Œ¸òDIKËV§R­ßþ½¯n{€ÕEÀ÷$í‘;ˆu¤Ù}sçh™§M"â‰2ƒwîü}=ºv‹ˆ“s‡è‡¤yIî\+¤9ìDÚ Ðäžà–Y1ù?ølî,Æ™qï€ÏݼÒ$ÕGG:äõºð«ZS˜Á·%U´\2«Ê—qÛ¹~í·”@Ò¢À‹5@o IDAT x õ¨:<"ZÕqCÒXàt`™ÜYlŠ^\&é[E[7³ÿ*¾&~BZ,²üÊüHe)ªwnD<•;DÔËÏâç€ ²XÝÎ#µŠ*u÷ÑLÒ"¤­ì³çÎÒ"GDÄgÊ P,â]¼·šHÖ2gGÄ‹¹ƒôª¸sx°eî,Ö“m#â†ÜA,?Is’jÖ¸Íl3\+ òDIË“¾¿›j‹ˆ´°a£ôtw¦x#?³æ,fkVùÍÄZï[xòß«/V0ξxò?ª®>Ú¦Éa/<ùo“·WKÚWÒ,¹ÃX>’^\†'ÿMrP‰ç6ùîÿ8à7¹CT¥§’Ö¦#…¬ ­´µª€”5CQÈëvÀGJzó0ðŽgö´é°{xž;€wGÄùƒôCÒ'€cñ×l[ÝlW¶]©µ¤7¿ËÅþë`™A‹XwKVžª§EÄ&¹CT¥Ÿó™Þ–mò0ð{I»/ fýø žü÷êEÒb[ÙÉÿkðDjT= ¬ÕÂÉÿû£ð×l›- \"épIsåcÃ!iàxòß4—ضÍüœ’;@•z^ˆˆçð®1À~À™’^‘;ŒµCqöëÜ9Zd÷ˆ¸ ÌEµ“ŸŽžqÀúqGî ý´ ©èߨÜY¬´€Ï·Ijòb+IÒI‘&csæÎc/ñé&À >VQŽ:µ–fÓ¶.p[Yvf΢%N¥Üy½‰ÞYÁ8Ö.w\™;H?$-@ú07oî,V©W'Iú]ÑKÜ:DÒBÀï]ñ®&:0"Æ òDI3Ó쀳#âéÜ!ªÔïÀoÇêb6‹“Š~»¸Ûhö2EÇ’­rçh‰?“*i÷Vf*$mì\M$k™]"âŒÜ!ú!iVRŠ&o5µr>ÜT´ œ-w+OÒjÀuÀ*¹³Øý øQ‰ço¼²¢,uèÔöès 8pZMY̦g °p¤år‡±jHZ¼Xع¬‚zk‹T‘«ã6*ÛÏVÒkŸâ»1£èÀˆ8x[ãX»\JZ@š;H?$}øRî–ÍJÀ’~&éÕ¹ÃØÔIšUÒ¾À€¥rç±éÚ¿äùøml>:,¬§An¸I:ørõqÌ2øp@D<›;ŒM™¤9HÀ¿Ì>Ùÿ%"–-9þšÀ9eÆç딼IÚœtWÍFËmÀ{"¢Uµ€$­œ‰[ƒZ2øiâòHî0ö?’> |T÷Éšï>`©A?{»xï­2T…éâÜbа&™ø&ps1 ´‘4“¤Ïw{ñòÉ?Àñ\j Æè²»€U0ù_øq5‘¬EÖnáäyÒ‘Oþm¢YI ÑwJÚ[Ò\¹:I¯“ô[à×xòß&{—œ¯Cs'ÿ?ëâäÜ é:¼ýÓšétà qoî £LÒ` àëÀë§ñÐñÀbñPÉëÝ,SfŒ{xwDÜTf¢‚úUÀ›+Iemñ°JD\Ÿ;H?$-\I³?`Z~ÿ&Õ‡øAÏú6™¤y€¯_Äí{Ûæ/À›"âÅAtÍ>ÿ¿|Dü)wˆ:”9sñ“ÊR˜Uk#RåßÝŠ ‹ ‘¤1’¶&m>žiOþ~YÁäN|VpZv(;ù/Ž'ÿ£æ`ÓNþç~ƒ'ÿ6}¯î–´‡$6­™¤9$íIÚ™¶;žü·Ñ×JNþßL³'ÿ×tuòå~Fº«dÖD³û÷HújQ Îj$iNIŸ'mõ?x]O=¢‚Ë¿•f‘ÉéЈ8±ì ’¶ ë±Ñ²sDœ›;D?Šs¥'oÉÅZeàÛÀ}’•ä¶s“4VÒö¤î?ßæÍÉsi·m»T¤FGçP§H:øDuqÌjs/ðÀqñ|î0]"i)`GÒä°ß³”Šˆå+ÈðyRá {©Ë€ÕÊ~ÍKz#iëÿ”ê7Xwí{åÑ/IG^“ÌÊ|?"n˦Í$Í|ø °Pæ8VÎàqí HšŸô¹¼©7çž^OäR—²wÌ| ÀÚâÕÀQÀm’¶–äíf%HšYÒæ’.$Û…þ'ÿPÍÝ€%*§Kî6«`ò?© Ž'ÿ£å8`ïÜ!ú%iW<ù·jÌJš´þYÒù’Ö•äf}´lÑöwâMOþÛï„2“ÿÂö4wòpJ—'ÿP~€Hç|݇ÝÚæaà§À#â®ÜaÚ@ÒLÀjÀ¦ÀÀ+Jù©½ÊSd;¥ÈeÉxàñDzI:øxùHÖ"¿#µ‹lÕn)I‘«N³'ÿ·v}ò%wHzðÒV)³6û'p&©íÅ1>sž,$-¼Ÿôæ½õé Rû˜[«LÒ­À²UŒÕÇEÄÖe‘´©†€ËŒŽû€•"⟹ƒôCÒ¤¢T æÎb#éAàç¤Ý'WEÙÖ-"iy`}`sà ™ãX½þ ,Ï :@q£í6¦ß*§]#âàÜ!êVz@ÒQ¤­>f]ñén÷À¹ñXæ<µ)Šó¬Bšô€ô&^÷ªýÅñª“ô| àzà½1®Ì ’^ \¼¦’TÖ“¾vnɤ’æ.Ç €Ö ÷§‘î€_2ç©T±Ûá}À‡Iÿų²aZ="~_fIN¨(OÆ‹ŽÂñžª–n,Ǭ‘&V,¯®,þ»µLÿÓ\$Í ¼XŽÔ"kR_÷aoÓÛ,"N­j0I·KU5^KýxGDÜSf¢ÈÕÙ¤ 6ÆkFÄE¹ƒôCÒXà\ÒÂ¥YÓ<\\ \ ÜÒ£5ó’>+¼xoñßÜYCY'EÄe(>[ÜB³kOŽˆÍs‡†J$] ¬\É`fÍ÷$p-iaààNR_Û»ÊlªJQ„çÕ¤Éý›7ËK¹kÜ,^å!I·ËT5^ ½¬–HÒÞ@ëZ¿ÙÀØ2"~–;H¿$ l•;‡YÆ·wwOþ_D<;ì@Å–ìW‘v{-Qü8ñ¿e€Å†Éçq`Ùˆx Ì ’¶ޝ&RmVŠˆ+s‡†* —†ltÌIºë4ù§ ’î#U8}x„ÔqàÒÝ€‰?GzQ¼ONíBÅô\¤–)³×,, ,J*‚·HñóW‘Š6ÕQ5ÜyˆÑ^øbE“ÿµ€¯WÇÚcÏ–Nþ÷À“k—±¤»éo™ÒJzx xt’'þü)ÒÅÏŸŸìçcHŸ íè›g’¡ç&Õò™·øýy'ûµ‹Ú´ìYÁä,ðÍŠòÔå£2ù‡jwŒ!Mz\ÛlpÏ“î ˜›nUµ~t÷¿Ò"c’N6«rÌ9:">UvI‹“ÎýWÑáÁÚáȈØ1wˆ~IÚ8W/ãR­¿›ÙÔ\Jj)\ª–…¤Ï߯&Rm6‰ˆÓr‡–Ê&ñðƒªÆ3Q³ñÒÕù.MþΨ©ÂxÙ¶4mu9𙲃Hš…TÁÚ“Ññà³¹CôKÒŠÀqxò_ÖÀ͹C˜Yc=lWÁä`Ïj"Õæ.R°‘QõäâHÒÙh3³)9²¦qoªiÜ&»ظ¢v•ßÞQÁ8Ö×›·­©¤…IYÜv¸¼ëH…éÌ̦äëñ· Æù*ÍoÑú½¶½–Ué@Ñ*íè*Ç4³Îx¨«Êø55ÛTãH“ÿ•¨hËSúµÆÝÀzñtî x8+wˆŽ¸†ÔÙÆÌlrW‡–DÒ’À—ÊÇ©ÕcÀ1¹C [Û‹¿ÇÿŠ“˜™Mtr+¬atv°mD”^ôôàÇå#YK<¬UÅÂQÅëÇÀ¹³tÀ¤#DêSof¥= lUÑçµCH¬›ìÇñTîÃVù@DÜK*Îcf6©ßÖ5pñFun]ã7Ì~qRÙA$Íüo§ÏŽˆÛs)#’¯»àÉë ®‰ˆI ffíZÅû„¤Õõ+ÈS§çÃs‡È¡®c‘îR™™Az‘ýcÍ×8½æñ›à,*hÓ'I¤~¼¯-ÈÚày`Óˆ¨û{ph"âPà“¤jöÖŸ_ORØkdª^›ÙtM»‹¶‡•S»“#â¹CäPË@DÜœ_ÇØfÖJ÷áÌñoIgã»êVàãe+òvÖ«`k¾>µíÀÉ%"Ž6¦Ûß÷uøÕ$?? ï¤03xødTÓþóÀÒŒS·ƒsÈ¥Îcߪql3k—:Zÿ½DD’µÀIÀs‡–ˆ¸x/pWî, õ"ðÕˆ8gò?(z}o”Þadf­s ðÕ*’ôàãUŒU³‹#¢®¶Ô­ jŽzLãÒùÀêµ^ÄÌšî`®ˆ¨ý¬®¤ÅIçå»PÝþ˜ˆØ¶Š$|¶Š±¬ñ~¬ãs¶bûé¹Àr¹³4ÈeÀÅÎÌ©’´°7ðQ`Ìr™Y^ÿÞw—HÒÌÀõÀJ§ªß*Åα‘U÷€½†p 3k¶1À›‡q¡ˆ¸øÎ0®U³óOW1¤ÍñäTü‰Tñä&ÿñ° iÒ;Êï›Þä "­€­H;†ê.Þjfù|²ŠÉá´còÁ¨Oþa;$|¨ö ™Y“í¥ÛËô¢X‰¾xû0®WƒK5«Ø1!éÀUÀì¥SYÓÝ ¼§ŠBNm'iVà`ÝÜY†ààfàZÒ÷úUÔ 4ðVàÀ ¤Ïr>FdÖ~GÄ®U $魤מ™ª¯f+÷² ÚuÃØ®`féCäPDÄsÀ¦Àcúf…®Ö¯hò?©Â·'ÿÝ÷/Ò¢ÑÈOþŠïŸ éngIÍ,¼Xx}±RJ1Æê¤¿ÇMŠ=ù7k¿‹¨îÜÿLÀOhÇäÿwžü'CÙ é\`¡\ÌÌšèúˆêyI뿦=çYÏ6‹ˆgÊ$I¤-¼—NeM÷$ðˆ¸.w¦)¾¾ |!w–!{šT á×Àé½¾¦æ7 - ®ͺæ>àñPƒIÚ“öTÔwD\‘;D s`%àC¹˜™5ÑóÀÜÃ(8)I#ÝÖŽ§A |*"^¨b0I»U1–5Úx`Ý*:Et™¤ÝoÊ%ƒ‡Hÿï‡Å4>ôIÚ´X²Ä°‚™ÙP=¼/"®®b0Io þ›¹ŠñjvnD¬•;DS íq±ârÞ°®gf3ð–a_4"~lLÞûº)žv‰ˆm*œü¿nB´i›líÉÿôEÄ~¤×sgÉ`àPàISüÜ'髤ãBžü›u×NNþg$mýoÃäR‡+ ûŽØÞC¾ž™5ËŠ9.?6žÊqýiø©Í¡U X´A;™ö{°Áí¿È¢-"âhÒYö¡îBjM€]&ÿMIïöe4wG˜Šƒ#â§Ž·3ð® Ç«ÓÙqUîM2Ô€ˆ¸’tÍÌFÓ²¹.gïîÉ•a2'o©ò<š¤1¤ÉÿBUiu@D|/wˆ~H#i™œ"âפÂvæÌ‘ÑžEqÐIíCóH™Ùà~|¥ªÁ$½Žöœû|úer¼à•´åÕÌFÏks^<"n&Cø éM!‡{µ#bˈøwÅcï¬\ñ˜Ö<Ç»åÑI³g×KÚ,g–ˆ¸œô}roΙ¼øÈÄ_HZ ø@¾8fV³›FD%ÇŸŠ Ç“º´ÁIqmîM3ô€ˆø+ðÃa_×Ìáu¹DÄã±°&p×/ý$°ðƈ8§êÁ%mÂèU:EçÛM«˜[Ó½äÏ"}ÏÍ œ$éK93EÄ­¤A7çÌ‘É&“ü|ƒl)̬nëEÄ“Žùu`¥ Ç«Ó8`÷Ü!šhh]^rQé•À߀y‡~ñæ{ž´5qÁÜAÌjð"0KUÅîÊ’4ø°'°pM—üøvD<\Ç$- \ÌYÇøÖW«GÄÓ¹ƒôJÒ,À™¤m÷“Û7"ör¤—47éhâûsæ²ç…"âQIWОs¼fÖ»g€U«<û.é=À%ÀŒUY³oGÄ×r‡h¢,g¾Šm¯m9;2l·WæaV“IÝ!"ÆGÄH;¾@Z˜¬Ê}¤•çÅ"b—'ÿs¿Â“ÿ®» X§e“ÿ±¤¯Í)MþötÄÔ*ÓCD2f5³Z½lQñänRí¢¶Lþ ‹´)ÈYôåpàÎŒ×oª‹€ r‡0«QãÞ<"♢ ÚÒ¤ÉÀ™ Ö.ìQà`]`ɈøN çüÿK’Hgñ²W´¡¸øPD<–;H¯ŠQ'’¾Ÿ¦eGàIÙ#â9` R«¼Q±°.þgÖEŸˆ3+óÀâY§¯W|ô¡S²øïÅÓ™ÕS³hž ­Æÿ¸ƒöØ0ëÇ<Å]·F“4?°°©HÖ”Ž<\ \\ü!"†VäTÒ^¸ºm×ýX9"îˤWÅÂÔ‘Àö}<í7À¦ñl=©z#éˤ»F]o‰w-i¡sŸÜA̬RûEÄU(éc¤ݶ¸x{DLȤ©².Hº xoÖÍÀAñIŸfΚʬZãÙ›R ’f&Õæxð0ðHqç0WžõIÛ«}¯»ÞwäÒIß!uüé×À‡ss(>ìþ›3GÍžÎ#íV2³n8غÊ"±’'M¨ç®jÌ!X5".ʢɚ°°"éÌ{×WÛ§äEà/¤='FÄ5“þ¡¤%‘ÎO.L޻׬mn‹ˆ7äÑvE/õ«ðkB—ýxDÜ’;H?Š;è”â2`݈x¢¢H‘´ðK`žœ9jö ÞihÖ§©òKq”ëà=U9¿Žˆ s‡hºìwŽ"âjà¹sdò©½¤­Ä“{ x x¼ø¹YÛÝž;@ÛMRôÏ“ÿîzX«…“ÿí(_tieà|IY»EĤsò­Ú}Ñ'Oþͺá\RÑ¿ªwWîI»&ÿã¯äÑÙwHz ©úý¬¹³d4Ø3"´1i ¢?ä[—ì{åÑVEµô_ëçÎbµGšü_’;H?Šš>'Q]‘ÏH…©h¼m‹OÞ—3‡™ÙT\JzÏx¦ÊA%}8Så¸5;8"vÍ¢ ± iOÜðEàí¤wYÕ`…ˆ¸6wˆ¶’´7à”îl¿Í¤’V΢úš5>V"¶v\UÚÌšàÒÿª'ÿcH»¹Ú4ùø‚'ÿ½kÌ@DŒ'õnÆ–„|Ö µ3ëšÓª¬L;JŠ¢'Р×l«TÛGÄI¹ƒôCÒ›€s€9j¼ÌR¤E€%j¼FO"âP` k—3yVˆ×0ö¤Z,mr^Dü œP#lŒ¢SÊ[€ sg1³¡¹x×&ÿsgÓÞÉÿSÀ–žü×£• EoñmVÐ2ë+r‡h*I³KÚ•t‡xñÌq¬z—‘îü·qÛÿËDÄyÀ椳—]±ðKI3ç2©ˆ¸XøÝúû6³—;Ÿ4ùÿk)^ç~ ¬Xçujöň¸#wˆ®j倉$mœš;‡Ùˆ›¼3"®Í¤i$-l l…·ùvÕ%À:]ÜýR´¨<hÔó’.6lâbMqüâDàÕ¹³˜Yå¾ìZ÷mI3'×yšÎ¢ËZ½ éP`çÜ9ÌFØÑñ©Ü!š (6öv`Ò›ï›ó&²š]¬×ÅÉÿD’6Nu缤I;6Ìdr’æ¶ÉÅÌ*1Ø)"Ž­ûBÅgÛÕ}­Ý ¼-"þ;H—“‚K IDATua`,iûe›·¹˜µÕH…ÿÊ$IK+“*{¯,”5 ËïGÄ3¹ƒÔ­è}:Ý* x7©Uãßr™Ië‘>Èûõ¤]žfÊÂãN`“ˆ¸q“ôà«Ã¸VMžV‰ˆ+réºÖ/Àÿ·wßavUÕÇ¿ B/AB/"½ HU ½H“Þ¤#]EiB@Œ¢¡ A~`!J =Ò!ˆ„ IÖï}†Œ03™™Ü{Ö)ïçyî3q’ÜóJfæÞ½ÎÚk6UûI`Áè,"5s‚»×æDެº¾*iÁ¿yöX*4”D¸ØÕÝks$­™mÜ ôŽÎÒ@ÿ%mߤ=fÖèOšÇ åðg`×èRƒ€ƒÜ}l3³3€óó¸VäîG‡¨ƒJÌl'à. 0çýŠTÜp`íªOhÍÞ„oì |í寻AÀ^î>):HÞ²™÷KFgi Iwèî‹Ò3Û ¸X(:‹ÌÐ7€›Ñ¿Uµ†zþÜsZd™Ù€óò¸VÝIšÏR…iÁU¦•h})“­Ü½’ÇW™Ù"¤»n{›³Æ&’‚¸8ÀÝ?ÅÌ–&ÖˆÎÒ@Ÿ‡ºûŸ¢ƒtÄÌ%m Ø%:‹tè)`=àGÀÙ±Q$ÈëÀþî>$¯ šÙ™ÀOòº^“¼¬ëîc¢ƒÔEÕ ½€Im¹"Ò<·ºû·¢C4R69wWà`;ª5ù\fÞïï¸ûÔè ÑÌlÒL€¯Ggi Nq÷‹¢ƒtÆÌ.ˆÎ"Ÿy¸ƒtÇ÷3›…ô:r°!0g`6ÉÏÍÀyµü˜ÙÙÀóº^“L6ÉkN‚$³Dh¤¬y_ ¶ÉDr089:D£˜Y/3; Ü ì„ÿò¿®$Ý!®ýâ {ƒ»pCt–2àB3»0›õQHî~ét‘;£³o£Ü}0Œ4›j©¸$Õõ ©8¼wŽûý-;­ì‹HE-þsV©€Vf¶iHS¥ "ñcw?7:D#˜ÙRÀÀ¦ÑY¤°~ œ¦}‰_”-”ÏNÎÒ`ד >S¢ƒtÆÌ¾I:[üËÑY€›Ý}ïlfà hëX< âîÿÎë‚f6p p@^×l¢ËÜýøèuTÉTfOŒHÑüX£ ÐÌlSRÛ¦†5IGÎv÷s¢C™ü˜=8J# vw÷¢ƒtÆÌæ%í7?u.Á:À@`åè ÒT“€3K²Ž\dßïóºf=ô«óLHU.©Š¿ot‘ ÙÁÝÿbf™ÙÀ-Të\siNu÷ £ƒ”…™mFš P¥‚Ú«ÀÎîþBt1³µ«H{Î%ÎUÀ‘Ñ!¤©ž Ýõ‘çE³¬wkçyÝ&yØÀÝÿ¤®*[0³¹€¿£D‘F¸ÍÝ÷Œ1³Ìl_à:`¶è,RHS£Ü}@t²1³åHÇ$®¥Æ{ù˜ÀVÙð¹#HÛ24$0Æx`þèÒŸNx¸,ïy0f¶ðg`ñ<¯Û$ãÍÜýÙè uV這-<,E$ 6áy?Vs÷·šðܹ1³½IAÚ›)í™B:æï–è eef½›€m£³4P p¢»÷Òf¶p1ê€i”{H…á7ò¾pö¾åZ`®¼¯Ý-À.Uè$-»ÊÉs÷QÀ7I•;‘ªj!U‡!M%n´s+°øßø#ZüKû>!µ{kñ?Ü}é$Ë£³4P/àr3»";n¸ÐÜý=wßèè.›HϽìãî;æ½øÏ&ýŸET\…Å?ÀñZüCå îþp Û ‘œõ¾,,ÜàçN:wº´Ì¬鮤Úþ¥=’†Ý¤ ܽÅÝŽ&'«â(à/fVŠözwøð]`Tp‘2ù”ÔE³Š»ß”÷ÅÍlNR·â9¤#J«àWî~etI*¿ -3;ƒ´7N¤ª&ÐØÁv|ÝÝÿÙÀçÌU6íÿ^`žè,RHïÛ¸ûðè Udf[7S­=é#HÝ"¹ý5³²éá§ßGÃOE:s/iËÏ‹϶ðÜlqý&ì–÷ìéX­ föR7€ˆÌØÝý è=efë½£³H!½BZü¿¤ÊÌlÒÀ¢³4ÐhÒpÀ£ƒt‡™-œG:C¼] "]ô2p’»Š æq'ðå¨ Mð°¹»D¦«c`ào@ßè,"7–ÔþVÊcZÌlMÒ) }¢³H!= lWÖ¯ï²1³>ÀmÀÑYh*pð3/Ù›)3[¸ˆjý{ˆôÄûÀ¹À€È3éÍìP ?ÕÙïð°¡»¿DþWíª¿î>ØÐ‘ÎYÖÅ‘™­ÜÿÒ¾-Êúõ]Fî>ظ&:KÍJº›~—™}):Lw¸û0wߨ x!8ŽH„OH ÿÜýŠ¨Å¿™ÍefWWS­ÅÿǤ­RZüPí:Z™Ù ÀÃÀ¢ÑYD è_ÀFeܯef_&}oëèOiÏ À·Ý}Jtº2³“ ¨ÖMˆ×€=ÝýÉè Ýef³{‘Î8_-8ŽH³M~üÔÝß‹ ’­EnÖŽÌÑSI{þöSHçªôâÛ-îþ °50&:‹HÁLŽ,éâ¿7p7ZüKû.Ôâ?–»_ìŒÎÒ@ËCÌìøè ÝåîÓÜ} °&° i{ŒHÕL!¼š»S€ÅÿÎÀToñï¤÷ZüXm îþ°+01:‹H\æîâCtWvlÎ]¤7±"mMŽs÷ãÝ]ÇÁ€»ß l ”f’~Ì\jfךYéZy³BÀ `]`_´5@ªa2p%©Õÿ w52Œ™õ2³_’†ý•jëPáî¿‹!«í€¶²*Üí¤³ÔEêìM`õ²MkÍZXo$µ±Š´5 8ÀÝo‹"_df IyUò iKÀ+ÑAz*û¹º7ikÀªÁqDºëc`p‰»¿ÀÌ– ý¼Û,:K“\äî'G‡«u@«¬â}©mE¤ÎŽ)Ûâ?s!ZüË!ó§ÅA¹û‡Àö¤ïá*Y ø—™í¤§²Ž€5€}€¡Á‘Dºâ}RÑêËî~RÿÛORÝÅÿïS¢CHר  3;¸8:‡HÛÜ}ÏèÝef'—DçÂy ØÑÝGD‘®1³ý€ßsGgi°ëIÅÕ±ÑAf–™mœ@:=`Öà8"m~\ëî…ÙÚkfs“ œG§Ynöv÷–è Ò5*|Ž™ü :‡HÎÆ“ã”ê¸3û©NÝLÒÖãÀ7uÌ_ù˜ÙZÀ¤¡zUò&pˆ»ÿ=:H#d­Ì‡Ç Ç‘úrÒ±®€Û‹6¼ØÌÖ' \9:KÝGz½DºN€Ï13®"½°‰ÔÅ1î~Etˆî0³Í€û9£³H¡ÜLZhæt™-LúwÜ28J£M#u+ý°*o–Íl>àà8`…Ø4R#ãë€þî>2:Ìç™Y/à‡À™T{¾ØÀÎî>):ˆt í0³YI?XöÎ"’ƒÇ¾ešŽnf«PÍ ºÒsçgº^ØJ/{}1ésÕÿw#:ÌŒdïKŽ~ Ì'ÿvr÷O¢ƒÈÌQ  ²*ö•¤#<¤\œô‚r7p¾»6³I­‰ëSØíq`kw0:HW™Ù¥ÀñÑ9¤0îöq÷1ÑA$f6;p©Í¶Šž'me¤™Ìl%à[ÀîÀׂãH¼÷I7hn(Ó×~vbÉ`ƒè,9yØ¥,7¤sÒÒÙ@©£€þÑY¤ÛŒ´wt^`¶ìs Ž-ªÚYÓÝqMÉÿG¢Å¿Lw)°ƒÿõâîSÜýHR`Jtž&XlfW™ÙÑašÅÝGºûyî¾.°pi¨k¥f!H§Æ×ÛK¸û1eYü›ÙÜfvð/ê³ø¿Ôm§ÅE¨ ²N€KÐB¤¬î~ü9:H°QÀêeY<™Y?à¯Tû(éšÉÀ÷ÜýÚè ËÌ6!m X,:K“¼œèäÅÌv%ul‰~æW͇À=¤ïÛ{Ëx¦™mKê^.:KŽnö/ã¿—tL€0³ Ik)—ÒòÝ£ƒÛÓÝo‹Ñf¶ ð(©kCêí=Ò°¿RÜ%’æ3³%›¾ÑYšè^à(w-:HžÌlA`G`k`+`‰ØDÒC¯“†/ß<äî-±qzÆÌ!KZ·ãÁ¯¾ëîS£ƒHc©ÐCfv>pFté¶)ÀìÑ!Ýîî{D‡è 3ë<¬E vu÷·£ƒH±˜Ùl¤c·Ž‹ÎÒD€_†Örø–™­F*ô¶zÇ&’8ðiѧ»?œg¦d?_ŽÎ&m­“Kﻊ•¤ÀL0³cIß š¥ e0XÍÝGE™‘lØ×ý¤7zRow÷‰ÑA¤¸ÌlÒ±½óFgi¢w3ëܽ¶ûåͬ°©°°10Gh¨z{‹4 îAàÁ2¼Çè 3Û ø%°Jt–g»û9Ñ!¤yT˜If¶+p0Wt‘8Ìݯ‰Ñföàðèª8ÓÝ/ˆ"å`f+“ö«®¥É^Nq÷{¢ƒAVXX7{l¬ Ì™«Â>p÷aÁyÊÌV%uÕñÈá©À±îþëè Ò\*4€™mA,§}ÊRTú•¡•ËÌN´è«·w½Üý‘è R.ÙyóW“Ž™«º»SÝ}Dt¢1³yIÅ€õ³ÇÀW"3•Ô§¤ã)†fáUì@1³Å€sC©gñh°¯»ßDšO€1³5H“Ê—ŠÎ"ò9€¯ºû¿£ƒÌHÖQsÚVSgƒI‹ÿ÷¢ƒHy™Ù‰¤Bâl3ú³%×B:‹ülwÿot˜"ËŽV\X•ÔÖÝúëå¨þ×IWL^ž#-ôŸž®úö+3› 84×k¾à8Q> ó÷Xtɇ dfK“ŠUo?”r9ÙÝ/Š1#f¶iñ7Ot ó+Rkó§ÑA¤üÌl#` °Lt–Œ#íW¾ÌÝ?ŠS&Ù ·åHÅ€•I§, ,ô‰K×c€I[I^Fd×ë4í=Û:²?põøÑ‘WíÝ}dtÉ ff_"M?Ý,:‹0ؼè/êÙùÏO šºú„tÔÐÑA¤Z²×ä«Ý¢³äd4鸲ËUh 3›“ôÚ´8Ó‹K‹ ‘N$˜¿ÍLj"öTRh,énî(Òp¾QÀÛÀ;¤­Uo×ýëÂÌf%-üÏ${êì`7uÕ M½X\Λ—XuÜý¥è 1³9€¿“&9Kýfi 33àÒòºL‹ \ô¯û‚/oÙåÞÙcìãì¤â@«y˜~$ñ¬Ÿû½i¤Å|[ãI‹|²ß—}n0®®ÇCvG¶ðßø°Rpœ"øðwŸDò§@“d?h~ œEjë$w¿8:ÄŒ˜Ùï€Ã¢sHˆßÇU}©ƒ™} ¸ X!:KŽZ —»ûÇÑaDòff³ûþu<Òïó8 8¯ ƒ¡¥9Th23;¸ ˜3:‹ÔÊ`³¢Oê5³£þÑ9$wGºû ÑA¤^Ìl~à2ààè,9û€é*Håe ÿ½H‹ÝUƒãÅàÛî~st‰¥@ÌlàÒP‘f›¬]ô.f¶%p?š¾\7O{ýëSªÍÌö çŠÎ’³H_»ûÑaD-›ê ©Wwü§{ØÝÝŸŠ"ñTȉ™- ÜlE*ïDw¿4:DgÌlÒ1C Gg‘\ý‘tçBt3[ø-°st–“›Ÿiþ†TAöýü=àhêWØ›‘‡€o¹ûûÑA¤TÈQ6ììJàÐè,RY¦þ¶õßÌæÖ‰Î"¹ îî·Gi+x$p!0wpœÓH']äîD‡é.3[‹t·_¦V”é.Nw÷–è R*0³cIÇôôŠÎ"•24õ¿°­ÕÙ›íHy¤q÷w¢ƒˆtÄÌV"u¨l%У¤ÅÂE."‹dûûw -ü¿§¨>&Þu¼®|Á,ÑêÈÝ/¶!íÅi”yñŸ9-þëb2p2°ÿRtÙÏÎM€s€ºÞ)Û¸ifG›YÄyö"2³>fv<0„ÿyX_‹éˆ:™Ù²¤á€kEg‘Òû°u‘ïÚ˜Ùö¤ìY£³HÓ½ìïîOGé.3Û¸X9:K°Iÿ¸û‹Ña¤ž²»ýýHǘ#6Qá]¥Y;Ò‚™ÙœÀÀ±€Ç‘r|ÕÝߌÒ‘¬½öq`è,Òt¾çîŸDé©ìµùtà褀aÀàOZXHÌlIàà`Ùà8e0‰´×ÿWÑA¤øT(3Û¸X0:‹”ÎþE>OÝÌæ#í-]=:‹4Õ(àwD¤Q²c×¢¡¥­Æ’NèïîÏE‡‘jɆeïl:»ê`?w&:ˆ”ƒ ’vÐ7:‹”ÆÍî¾wtˆŽd­{w^Ð¥º®¾ïî㢃ˆ4Z¶(98 u´ràŸ¤®€ÛÝ}rp))3›´—O`wt#¬;èœêî“¢ÃHy¨P0fÖ‹4„èt4¤Q:÷.°¦»¤#föCà§Ñ9¤iÞ!M¾':ˆH³™Ù:¤b×ÚÑY æàà&`p‘gÑH1˜ÙìÀV¤Eÿ®hÑßïßÖë¯ô„ ef[‘öÒ.E ÉíÜýþè ɾ†ïC-|UõàxwD$/ÙÝÊÓI:sü‹Þez1à1×›LÉd4[“ý»_ŠMTjs÷ÿD‘rR ÀÌlÒ4Ïí¢³Háôw÷c£CtÄÌ–" Z$:‹4Ü»À‘Úë/uffk¿6ŒÎR`oŠÝ}XtÉŸ™-DZôïì ôŽMTzãIÛí®Ž"å¦@Á™™'ç¡» ’ 6(ê$æìÙ?IgJKuL#í÷=]{ýE>›qr8ð3tÂÉŒ¼Bx0TÛª)ÛÆº1°-° °.ÚÎÚ(u÷7¢ƒHù©Pf¶&iïázÑY$ÔÒâxtŽ˜Ù¯€ã¢sHC íõ8:ˆHј٢ÀEÀþÑYJâ}à¯À=À}*(–›™-ËôÿVÀü±‰*çÒ¶£+´¥FE€É*«§?æŽ#1s÷k¢CtÄÌöF熙Lº»ù3wŸF¤È²¹'W+Eg)‘`(ð éç(P\YWê*¤»ü››+††ª¶{€£Ýýõè R-*”™­FêÐÞÃzù“»¢#f¶*ð0otiˆÁ¤»þ/F)‹lÐÙéÙcÎà8e4< < <êîãc#Õ—™Í l@:žzãì¡á}Í7Š4d÷–è RM*””™Í |Ÿtdà\Áq¤ù^Ös÷£ƒ´'{“ð8°Zt™iïgתÝP¤gÌlER7@¿è,%74÷fi°ì0àyu$5ž™ÍCz _“´w¿oök䓟iÀo€3´5FšI€’3³•IÝ}£³HÓL6r÷g¢ƒtÄÌnö‰Î!3¥ø5p–Žöi 3Ûø%ð•à(U2xxŠT1ûøš»·D+ƒl;éŠÀ¤þÀWeÑÀ¾HÏG¸û£ÑA¤úT¨€lññÀO¹ƒãHc9pˆ»ÿ!:HGÌìXà²è2SŽu÷g£ƒˆT™ÍI:Íçt´Eª™¦¯2½ 0²õ×î>:2XÞÌlv`iRá©õ±°*é.¿æHÇà\àbwÿ4:ŒÔƒ bf+W&±J5üÄÝÏŠÑ3Ûˆt䟎¨,§wSÔî/Ò\f¶i¨æ€Ç©› ¤Ÿw£²ïïd?û|ºŸ²-w}€E€…€Å™¾È_XXÝÍ/ƒÛ“Ýýµè R/*T™í\BzòìWÔ…™™õžF­­e4 ¸8ßÝ?Š#R'f¶ð+`£è,ò1Àxà#`06ûõø6Çeñ@Û»¶S³ÏµÕ’ý=€Þ¤…ù|@/R׿¤‘s‘Šéód¿îCZà/œ}\(ûœ†K–ßSÀ÷ÝýÑA¤žT¨(3››Ônx z±(£ÁÀ6î>):HGÌìz`¿èÒ-ÜH0ôft‘ºÊŽSÛø9én­ˆTß{À™¤!»Ó¢ÃH}©Pqf¶<éNßNÑY¤Ëv(òY3;(ì\i×`à$wDD’lòú ¤b}ïà8"Ò꺓BQ &Ìl'ҟ壳H§¶.êqf¶©}mþè,Ò%/§¹ûÑAD¤}fÖ‡Ôµw êÚ©’[SµÏ_ŠD€É&ŸL:ã[§ÏKÀ¦îþAtŽdÇ F{WË`p0@“…EÊÁÌ–Î&í‘rzø»Ž"òy*Ô™-CÚw¸7šD\“€õÝýùè 1³Ÿö¯Iq.®p÷ ÑaD¤ûÌlÒѾ»£×i‘2 œåî÷Féˆ 5ffëŽ$Ú6:‹p¦»Ÿ¢3f¶9ð7`Öè,Ò®ñÀÅÀ%îþù)Ô"RBf¶>p>Ð/:‹ˆtê9à,à΢žÞ$ÒJÁ̾N*l¥¦ÆËy0Œ™}‰täß—£³ÈLú¿p÷ÑÑaD¤ñÌlR÷ÕvÑYDäŒ$mÛ¹I“ý¥,f‰ ñÜýïÀÆÀÀˆà8u4°È‹ÿÌUhñ_4‘¶ò,ëî§iñ/R]îþˆ»ol "é)"q^Vw÷µø—2Q€ü3›…T¸X68N]lçî÷E‡èˆ™íÜC>3ø5éŽÿ‡ÑaD$f¶&éèÀýж,‘<½Bêºû»OŠ#Ò*H»²Ž!KÔ'8N•µó»ûÄè íÉZÿ_‹Î"¼OÚãe :FD$f¶:ðC`/Ti¦GI7Çén¿” Ò)3ë  ,§Šžw÷5£CtÄÌ®&µ¸IœWK«5Õ_DÚcf+^§æM#Rü¸ÀÝŠ#Ò(*H—˜ÙÜÀaÀÉh/x#ýÑÝŠÑ3ÛxAåààÏî>5:ŒˆŸ™-Nêà[:8ŽHYµ¶>þÂÝŸ#Òh(]âîÜýr`àÛÀ‹Á‘ªâåèø9Züç­¸ØÈÝ7u÷Û´ø‘®r÷±îþ `yÒ|€¡Á‘DÊd p°¢» Å¿T•:¤G²a»gëÇ)³ÜýúèŸgf;wG稑1À5ÀåîþFt©Ž¬›ëDà›hN€H{ž®nÔV;©d¦eo.NvŠÎRB}ÝýÑèŸgfEç¨aÀàzwÿ$:ŒˆT—™- |%6H¸)ÀÀw :ŒHžT†1³ãHÇΧ,w÷÷¢C´•ý;‰ÎQa€«Üý_ÑaD¤^ÌlV`Ò¬€íQW€ÔËÀoHƒuß#Ai83[œôÆâp`‰à8E6˜× öMhf·{F稠瀫ëÜ}lt3[ øiÈïRÁqDše i[ãï{4[GêNi3›4'àh`óà8E4ÜÝ׈Ñ–™-ŒDw„e4piÑ?,:ŒˆH{²®€íƒIÛùæŒM$Òÿ®#ííF¤(T\˜ÙÊÀQ¤3åuFq2ÈÝw‰Ñ–™]£ä¦þÜâîƒóˆˆt™™õ& ü°Ð+6‘H·Œn®u÷§£Ãˆ‘ ’«ìŒâCHÅ€cÓ„»ÜÝ‹Ñ*û·y hzê ` 0ÐÝGE‡™YÙ–¾}HG ®G¤#Iýþܯ‘Ω !ÌÌ€-Ií†{PÏEçéî~At3[8‡T˜‘®{Žé‹þW£Ãˆˆ4KÖÉ·/é5»PÛפ–>þÜFÚׯ“tDºH gfó’ÞP lÌ›(7¸ûõQÏŽ„ÚØ ø:jó쪑ÀMÀMî><:ŒˆHÞÌly`WÒV¾hnŒäc0ˆ´è¿O[ìDzF)”l"ñþ¤‰i¬ÓNÆIDATÄUß"°¥»ÿ3¯‹™Ù\À&@¿ìñ5Àòº~ɽ@ÚS8HÃüDD¦3³>ÀޤáÛSÏŽ>iž1¤ þ·Úû'ç)=¤²-}I]{½c5ÅŠîþJ³žÜÌfÖ¶¶!ý÷œ£Y׫˜ÉÀß?“ýïç)<3›Ø6{ô–‹M$%äÀÓÀ_{Gݽ%6’Hµ¨ …—ݹÞ8ˆ´˜­J«áÜn_3³EIÿ¶Ë>.ÒÈ篸7Io6îþÏÝ? Î#"RjÙV­³Ç7€bIA}üé5ø^w/8H¥© ¥bf “æìlNy‹£Ý}¡F<‘™­ìì¬Úú»jðÓßpŒÎ#"RYf6+é5ªµ °êJ««`p?éNÿšÜ/’¤´²»Ý­Å€Í(×ðÀgÝ}­žþe3û*éŒæÝ€Õ–ªÚZ€'Úûÿ©B""1ÌlÒÑ‚}Iói6F]kU5‘tTîCÀ`R[ÿDZ‘DêK©„ì¬â=IÅ€¾¿p»ïØ¿`f_!Á´:‚©+¦2}Áÿ`°ÚúEDŠ+ëhk-ôVE]me4x„´Ø u÷)±‘D¤• R9ÙI­Å€(曇ߺûá3úCf67°7éT„¾óÿKQ|< q÷ñ±‘DD¤§Ìl>`-Ò@Ûµ³«³Gæ’ÿ1x–ÔÒßúx^-ý"Å¥€TZV Ø1{lÌ›è3¸ûéý¦™­ˆ†&ud$i¡ÿ(iÑ?\o8DDªÍÌf#uÁµ¾ ¬,™«&&ϺëZû/hJ¿H¹¨ µ‘&ðuÒYÅ;ËÆ9ÓÝÏkû 3›´§ÿ(Ò€CI¦/“Þp|öp÷±¡©DD¤0ÌìKÀʤm+gU€åÙ£•ÑGÀKÙcDöñ`¤û"å§€Ô–™­Iê Ø‰´U ÏŽs÷˳K‡ßË1C}Lz“ñ©¥ðIàíÝ‘žÈ:–V$þ¿œ=–¾BzÝ-úÜ f˜H:þöMR‘ý²E¿»¿LDšKÀÌ6%&°)ð5š»Çð4àÒP¿í(ïq†=5x<Ÿ=†¯¹~(‰ˆHN²î»¥™^XŠ´`QRq`‘ìÑ'*cLFooog[ÿ÷[îþ߸x"I‘vdç/Mº;°?éî¼tÏTÒ›—Iûõ[/o¸û´Àl"""]– ÚææzófùH³{æËþ÷\mžff<‹ècàÓ6?¦ößOÎ>~HZà·>>hû9q+"Q@d̬0˜':Kýx={¼ö¹_¿ªcDDDDDŠ£Wt‘¢s÷3›@= 2}Qÿ:Óù¯¯»û'A¹DDDDD¤›T™3›ƒÔâWU£I÷FZô_o}¸û¸¸X"""""ÒH*ˆÌØZ¤}{U0 <œ=þåîÿ‰$"""""yP@dÆ6Ž0“Æww÷ªm_DDDD¤žT™±]¢ôÐ0`ð'wŸFDDDDDbé‘N˜ÙÂÀ»”§XÖ ~îîãȈˆˆˆHq”eQ#e7Êñ}2¸ ø©»Œ#"""""ÅS†…H¤}¢ÌÀà·À…îþvt).m這-¼Xt–v|\\âîÿ#"""""ŧ‘ŽJñÿŸ×g»û¨è0"""""Rêi‡™Í¼,¥Õ4à6à wÿwt)uˆ´o[бøwàvàGî>":Œˆˆˆˆˆ”— "í;*:ððwDDDDDDÊO[D>ÇÌ–F³EœâˆˆˆˆˆTPÔG¤ÈŽ&æ{c4p°¡ÿ"""""ÒhêiÃÌæÞÈñ²Ÿ’Žôû‰»Ëñº"""""R#š ò¿&ßÅÿàw>ÇkŠˆˆˆˆH ©@$cf VÍárã€ýÝ}jבšS€ÈtýÈgñð=w7‡k‰ˆˆˆˆˆ(ÒÖqM~þ À îþM-þEDDDD$oÚ B.Gÿ=äî/7éùEDDDDD:¥‘äšóýМlªÅ¿ˆˆˆˆˆDR€Ô^vôßÛ@ï?õàw²ÁÏ+"""""ÒmêIGÿ5rñïÀ`=-þEDDDD¤(Ô µÖ„£ÿÞs÷{ô|"""""" ¡c¥î¶¦q‹ÿAÀ!îþaƒžODDDDD¤a´@êî„Ô ur,/þïÖÑâ_DDDDDªHR f67éîþ‚íüv ðàB×7„ˆˆˆˆˆT”NºØöÿo“ZþÎ9ˆˆˆˆˆH®´@êâÈv>×Úò¯Å¿ˆˆˆˆˆTž RyÙÑë¶ùÔ$Òi»¸û1©DDDDDDò¥-Rßkóë€ýÜý™¨0""""""4P*ÍÌú†ÿÍôNs÷I±©DDDDDDò§©º€¡¤…ÿcÑaDDDDDD¢ü?ä³ß“ãyIEND®B`‚chango-0.6.0/logo/chango_light_mode_512.png000066400000000000000000000631611507376567100204730ustar00rootroot00000000000000‰PNG  IHDRŒ¬âÄ pHYs , , ÝbßtEXtSoftwarewww.inkscape.org›î< IDATxœìuØ]ÅÑÀ“ECð ÁK -îî®ÁÝBqŠS @[Š--NRàÃZ ÅŠ» „‘ùþ˜ó›7×ïž³çÜ;¿ç™'o®ìÎ=»çìîì쌨*Žã8Žã´6"ÒØX˜{šÈú8Žã8Ž“"Ò𷶦OÞ:UÜà8Žã8­ƒˆLl 6ú”øØr>pÇqœ‚#"3›Ûë½+|ü}`aßpÇqœ""³`+ümõž5~õoªªnpÇqœ‚ "ó[›kõ.ä'Uuˆ[Çq'LjÈüÀæØJe@š(îߪ:êŸ98Žã8Ž“2"ò lÀßX&`ÑWýX‡o8Žã8N\D¤°<°5æÈ·@ Õ| PÕÀ-Žã8Ž…$0Ϻ˜÷þ¦À)Wy}ÇànpÇqœÌ‘Y°#`º «_BU_ëø[Çq'E’ýüM±•þJ@·j<ÒyðŸ8Žã8NPDd`E~ ¿;o\øk×| ÀqÇqšDDúkc«üÍãj4ÃyTu|çÝà8Žã8 " ð“ß@¸•劮ƒ?¸ÀqÇqjBDºc{ø›b«üÅâjT“…Tõîo¸ÀqÇqÊ "3bþ†@¿¸ÕÍ¿J þàÇqÇ™™ð·$ߦýZ˜Êù¯ßpÇqÚžNûù!âíç…0óÿ¤RoºÀqÇi;’лKaƒþöÀÏâj” ç—üÁ-Žã8N› "}€U°A;Ò½“QØÑ¿oË}À-Žã8NË""3cûù[ë}ãj”×TüÁ-Žã8N‹‘¬ô7vÃýžq5ÊSÕ·+}È-Žã8Ná‘ÞÀz˜ßÖÀ´q5ŠÊ½Õð €ã8ŽSP:æÙؘ!®F¹áüZ>ä[Žã8NaH¼÷WÆVúÛ³ÇÕ(w¼,©5 înpÇqrO’Rw×D抬Nž9­–ÁÜà8Žã䙨sæ[ ®6…à=àg•ÎþwÆ-Žã8Nn‘^ر½Ý€€îq5*gÖ:øƒ[Çqœ "Ë`ƒþÎ@ÿÈê‘aÀüª:®Ö/¸Àqlj‚ˆÌ9òí ü*²:Eç¬zp €ã8Ž“!Éѽµ€ÁX¶½"gÚË ßó©ê÷õ|É-Žã8NêˆÈbÀîØj¶Èê´Ô;øƒ[Çqœ”‘i°Õþ²‘ÕiUFUõ›z¿èÇq'("²(¶Òߘ9²:­Îy þàÇq'"Ò˸7X¸µßbžÿ#ù²[Çqœ†‘À.ÀAÀÜ‘Õi7Îitð·8Žã8u’Äã_[ío…/&cð5°€ª~×hÞhŽã8NMˆÈlØÞþ~Àü‘ÕiwÎjfð·8Žã8UH¢ô Æñô‰¬Ž_a«ÿQÍâÇqg*DdzlÀ?øEduœ)9­ÙÁÜà8Žãt"9Âw ´g†Èê8S3XHUÇ6[[ÇqÚœNN}‡›àGøòÌïB þàÇqœ¶FD–îÄúŠÀkÀ’õ¤ü­D·…8Žã8…å÷øà_Ž5øƒ[ÇqÚYx_ ‡Tuíz£;Žã´/§àã@˜ ºPoxÇqœ6DD~‰EñsšãKà&àì|~Ü ª/„.ÔO8Žã´''âÞþ0xøW"¯Ë—³¤Pß8lrŸ8Žã´ÉÞÿÖ±õ(C€û°ÿ?!xE¤pðkÒ³¨Ÿ§ªŸ¤Q°;:Žã´"r3°]l=rÌxàqlÀ¿OU_ïü¦ˆôÂýãYSÔã3àgªú}…»Àq§‘_ƒbë‘C>"ð±Uþè®H&팘NG¥5øƒOÇq ‡ˆL×ÄÀp"îðð6àß§ªoTú°ˆlü øeº<ü=Í | Àq§ ˆÈ²À™Àýªzzß_ ‹&×®€Ï»°AÿÁZ&Q"²p:°zʺuf"°´ª¾šf%npÇÉ9"Ò8KÐ#ÀY ÕŽ«ÿ·;yVU'×ò¥d²ô'`Ëu+ÇÅiþàÇqœ\#"Ûbžæ³uzy€ª­³œ…7îÕË+o·wÕ{~^DæÆ&J{g‘ü%°¨ªŽL»"·8Žã䙸ؠË[_Õ;ø'œDëþ < Ü ÜªªŸÖ[€ˆÌ ìô«^]›Åà>pÇÉ"2 p¦·o‰¼Ò@™‹;4©ZyôoVÕ) ÓÀ?èNµ†x¸:«Ê|à8Ž“Ddàr`© {¹¢§uVÿob¡woVÕw-DDæà§ì,öÀ`Íp_Þ'Žã8‘‘¾Àï€#©>P¿^åý®e/ìØ jya$p pðD3ƒ¤ˆÌ Li K,NUÕ7³¬Ð'Žã8‘€‹©=°L]Ì¡­ˆÏúIÀCØ ÿUÓLa9ø^ÎȺR?à8Ž$ŽüùÀ®u|M~ªúmu,„™Ì‹4x¸¸QU¿l¶0™8 Ý›·l¢³Šª>“uÅEêŽã8-A²ê¿ ˜»Î¯~ZëàŸpÅxÎþ»&ÿ ±."³¿fh¶¼¹0ÆànpÇÉ ™‹ä7¸Á"þ­ªÖXׂÀ[ä{ð6ð7àJUý*Dø>O3Þ%òÜ1ÇqZ†$–üeÀ\MS1^}~K>Ÿñ?ÿ.QÕÇC*"0'Ê}iC•›"ŠyýGü!ŸÃq§e°êïLM€"2°K€úB2¸ ø‹ª U¨ˆ ÄVûƒ‰À§^.QÕûc*àÇqœ”‘±Uÿ€@EÖj8è¨Îfy ¸¸IUÇ…*TDŽÅ-ÆÁ»˜cbTÜÀq'0"2-p.fŽÉLÕœ“Âo=×]“±¬{ç©êÃ! ‘å°í-°ÄHEc"°šª>[·8Žã$‰æw°hà¢k=p"ñÿ XûÓUµ…ªˆÈªXä¾MC–Óò0øƒ[Çq‚ "‚í÷§a~¯z ¢çÿ÷ØþþÙ!÷÷Dd],/ÂÊ!ËÄ‹ÀŠªúClEÀ-Žã8M“¤½X+ÅjjYQg}î8¶¿ÿUªPé Ž– UndÆ»äeðŸ8Žã4…ˆ .fN¹ªŠY˜ì<ÿ‡gcÿèP…ŠHì7Cø-”Øz[¤Y|à8ŽÓ"2Êw¯Œª¬6xd±úÿ 8¸(äùuéìãŸ/T¹9âNàÂØJtÅ}Çqê$ñD¿X(Ãjû©êÈ2ú,‚MÒ:÷5ðg,lí¨P…ŠÈ ÀþØ9þÙC•›3>– ¹E ·8ŽãÔHâèw(–¹-ËsöŸ•üN$Áfê?7ðÀ? æ0yÐ/T¹9d°}ð €ã8NM$«Õ+€m#T_6`²÷¿Càú&`1úORÕa¡ M2óu¬øó§?ÇÆJôS >pÇ©‚ˆ, Ü,I…J!€O!ܳ\±8ýÇ«ê»ÊìNtÅ ×Û ÷`Á r‹OÇq* "afð^Õx³Ô‹"²°] :ŽQÕç•×®÷8`{Š®·>v ‘Ö8M|à8ŽS‚Ää%v=6ï”yýw4?°¾®ª÷4YÎ$“ß›SÌp½Í0Û÷ÿ&¶"Õð €ã8NDd)Ì䟥—%Þîú‚ˆüœæVÿßcGúNSÕñM”ÓY§%0‡ÄA´ßÀßÁAyÞ÷ïŒOÇq:!"¿Æönó²Wý]G¼“€n ”7 Û{‚ª~Ù”f "ò+àdŠ› '©ê±•¨à8ŽˆH/à/X@š<ñ¼ª.×ùùð õOSÕ—B(–ìñ ìÜ€.­ÆãÀ:y õ[ ·8ŽÓö$±üo–­K JyãŸB}î'À‘ªzk…SÿIÀÖ´÷Š¿ƒ­‹4øƒOÇis’4³ÿ ¿‘è¦ØÿOÌí[×øÝ°}þ?ªê˜fIVü¿¶ÁWüŒ¶RÕᱩŸ8ŽÓ¶ˆÈþÀydÕ¯^ºžø#µ­ºÄÒ¦r ¬Yø=vœÏþŸP`U}1¶"àÇqÚŽd¿ÿ"`ŸØºÔÀ€ÄZ±i•ÏŽE »¶ÙŠEdVà,ˆOÌ8yåwªzSl%ÅÇi+Dd.l¿ÅغÔÈ qøEäQ`µ2Ÿ›€e'<¥ÙL}"Òsî;èÛLY-Ì•ªZ„ dYÜà8NÛ "+cûýsÆÖ¥F†wü7¡üàÿ(p€ªV \é‹ úGÓÚIzšå_À¯c+Ñ,¾—ã8N[ ";bûâEü>‘nØÞWFbæùµšüE¤‡ˆ ÆNœ†þ•xØAU'ÆV¤YÜà8NK“¤ð=)‘¢ñqòïÎÀ’]Þ»sòk8˜Orm§ 7ZNñ°±ª~[‘øÀqœ–%‰ç°ql]äcé‰EÙëà`U½¿™‚Ed ìˆà2Í”ÓFŒ6PÕOc+ ßp§%‘§(îàÀç `ÌÉï `‰fYXDnÆÿZ lÚ¬EÞðSŽã´"²f"ï[—&yXxØOU_i´ ™ âsÐ#ˆvíÁxlð0¶"¡ñ €ã8-…ˆì\Hk rßÇcIf&7R@²…p p0s@ÝÚ‰À U½3¶"iàÇqZY ;× < VÕ-@D¶Â¶ ÜÁ¯~&»ªê±I ÷p§•Ø(¶ì…9œ}ÔH"²Œˆ<üüA1GË–üÁO8ŽÓZl[&¹ èóy#_NB÷þ ›@ø¯qŽTÕËb+‘6¾à8NK "sC)fzÚ/£ߟ Ú;Ö7KHÅÚ ŽPÕsc+’np§UXŸbþׇ©ê×|9ñ{¸øUP­ÚVÕ¿ÄV$+|à8N«P4óÿØ>óí|YDæÄüv¡˜Ÿ<Ñvƒ?ø€ã8-@bÌ[—¹ÛëÿªÞ/ŠHì,ÿïB+Ö†LöQÕ«c+’5np§X–b þÃUõÖF¾,"ë? ªUû2ØMUoŠ­H |à8N+PóÿíÀ¯IÞ#"³ç;ת}ìÔèL+àÇqZ b+P‘À1+K²õíŠ{÷‡f$°…ª¶JШ†pÇq ˆôÃLëÝcëR‚{±ýåºÏõ‹È¢À¥ÀÁµjo>6RÕ×b+á8NÑYü þcð$2u þ"ÒCDŽ^Âÿм¬âƒ¿á[Žã¼™ÿŸvQÕ·ëý¢ˆ¬Š­ú\+çi`³FN^´*np§°${äy™Lþ¬\ïà/"3‰ÈeX"#üÃs°–þSâÇqŠÌÀ€ØJaÇÉ«÷‹"²!p90wh¥8øm£é”[·8Ž“ Ddç$ O=äaõ%ðËzÿdÕÿ7à>|ðOƒ1Àªz¬þ¥ñ €ã8QãÀuÔïÌs0ØFU÷QÕQõ|1Yõ¿ 쑆bC€UUõ–ØŠä?è8N4’ÿeÀÞÉK}Tu\ßøè•’z•xØQU?¨çK"2p°/¿?-ž¶VÕa±É;np' "Ò¸†Ÿ¨Ï/im²ü;ýVi`ð_x Œþiq%æìçƒ ¸ ã8™“$´¹ضË[õldmþŠï{¨ž/%«þ³}ð?-Æ`9®Ž­H‘ð €ã8™’ þÿ6/ñv=€,ãÿß ìYï12Y¸˜?­€·€m=¸Oýø€ã8™‘ìù_CéÁjœˆÈÂÀ‚¡ôªÀà7Àæõ þ"2ˆœ<‚þir°œþáÇq²ä`Ç ï×jÈbõÿ°]&ÿù° «¥¢•–ÉïXU=?¶"EÆ'Žãd‚ˆœ‚ÅǯD­Ï¤´÷ÿŸÇ<ɇÔó%Ùø 0C*Z9¯;«ê˱):>p'uDä`àw5|´ª@Dzk6«S®×z@Df.vªòÑ‹€QÀw˜SàôÉ{=i“¿{%÷ë"3uù;188¾ž¶qÊãqÇI•dU| µù-Xíxˆ¬ÜB·.LŽRÕóêù’ˆ¬‚9ú ¬ñ+c€'€‡yNU'ÔSg§ºgfÇÂ!ÏÌÌ‘ü;g"ˆ+!${¨êñi%|à8NjˆÈFÀ@¿²ˆª¾[¥Ìs€Ã›Õ­ _Û«êjýBâÐxM ^SÕ‰iT$"sË$²tòï\iÔU'_¿VÕÛc+ÒªøÀqœàˆÈ¼XHÖz’%*é‘y€OšÑ­ 7{«êèZ¿ "«7‘mÂÑÀsØdà àIU™Ve"26XXXè›V}]PlÕ¤ª~Qm‰OÇ Jâ÷8°x__²’w·ˆì‹åh–IÀ ÀZãCPD8øñ¨'cÞða‚Çê=±P"ÒX› ¬¬J:'Þö«÷è¥Ó>p'É@q/°NƒE,£ªÿ«PþmÀÖ –ÝÁH`'U½¯Ö/ˆÈ,تtã&ëN“÷€ÿÿ éGЕ$ï°Ób“‚ÞMùp&pª{øg‡OÇ B²Bþ°{Ŭ ªÏ–),8O3Nko[Vs4ìRïòXèâyš¨7kx› <<¢ªcÓªLDúòÓd`C`á:¾þ¶ê=Õœ øÀqœ ˆÈɘG|3¬¬ªO•)5àÑ&ʾØMUGÕú \@ñÑÅŽÞ Ü§ªï§Y™ˆ,l V¡ôÐÏ€ß׫êä4õqJ】@™èÌÚéïé°UQo,xÈôü´GY.XȈN—È·‰|‡­´>†zjM§"²;¶úo6ÛÝjªúx™:NÅŒzQàà÷uì÷÷Æ"úíÕ@}Eàl2p'æ?0)­Š’S{aiç†×ã|é„Ç'9CDf–À¨Ån˜™#’Zã±Ùú[X>ó×ySUÇDÒÉÉ I œÿf•¼¦ª>R¦ž°cjõ0[õÿ³Ö/$±üÿ9½µ_aÙïîOk« Ù"š%Mß§>|‘dOs)Ì£v ìáV¤}ÆÉÀ‡Ø¤à àØñ¤¡Qµr2#9î÷,….ë– Æ#"³ŸSŸ…áS,‹ß‹µ~ADÖÅŽøÍRG=­Äà>àïÀ=iú 8ññ @†$¹–Å‚‡¬¬L}ARŠÂ‡Ø1°Ž3˯û_ë!"Óbí¼dÀb7PÕ©ÂüŠÈ®Àµu”ó,æì÷y-îtÄï4Õ¯•…müx@Uˆ¬Ÿ¤LbÒ_;>´0[\¢ð-6x []¼\ë^¬“O’óV`›ÀEo\êxžˆÜ@õD;Ü ìYëêUD¦ÃŽø5{¼°•Ž]£+TõíØÊ8að @ $æÊA‰¬Jü !yã3ÌéàAw*<þK±™ªÞÝ¥®nÀT7Ë7âì7?¶÷ýËtmG›È_üÓ-{ÅÆ'H…l líç»±6Æcñâïî®– Ήˆl‹­²›õø/ÅVªzG—ú–Ç’äTb,–-î–Z+‘51+F»î÷7ËkØdëÖØŠ8QKzN§ "ÒSD‰È½˜ƒÒ¥ÀÚøà_½°-’ó÷Eä9$±¢89CD–®&ÁJß;VùÎP`:ÿý±”Â>ø7ÎâÀ-"rBlEœÆpÓtˆÈϽ]±óøN8–Oäy¸¸Ý· â“X¹î ݤ0¥&Tøüÿ€-TõÓZ ‘˜ùz¿ts¦ä,VÂ…±qÃ'5""½0óþ~˜÷¾“.Ó`N“£Eävà윲ï;fL²0oÊUM1‘~X&ºRÜì^ëäPDfÅÎ÷¯Þ”†ÎÀUÀiª23£“1¾P™]DN>¼`}ðÏži]°ï‰È‘"2sdÚã©nŠAW Àz%^SàT`Û:ÿ_aét}ðoœ€¿ «êþ>øŸ”AD–‘«“‰…Ï™’ù³€!"ryò`wRDDÖ!ÿRtµJ®Ýåÿã€]Uõ„:<ýañ(æ  _;òvÏ-èká€.ˆÈJ"rð–Õ¬èI@Z•¾À>ÀK"ò¼ˆì–ìï:‘9€ëÉαµë3i­N… º¡–‚Ä8;±0m ýÚ‰aرʪzt­~Nqð @‚ˆ¬ž8= lJz^Îåðس ¶=󡈖¤&uš$™PÝJ¶Ö¯-I™E’ÿ¾ ,£ª5eL‚ûÜœŽ?çêåU``>U=YUGTù|ª$ýÀI¶¿1Dd5y;‹¾n$5¾ÂL”Ns Î>Hü|Õ×§a¬²¤³¥aäßÿV­Õôœ÷yØ*°n­Î±ˆ¥¿RÕkb‡þ‘n"ò;àù—ˆÔ›Ê©BÛNDd¹ø×¨öù”y1' ³c{–‰ÈÉ"2cl…Іˆl¡ê΀5 °à@ß×òeY x˨éTg2– p%U]GUïËC˜îäÈé=ØDwì(èó"r‹ˆ,RñËNÍ´Ý@Df‘‹±t¶Û’½©¿/OÅV¢™s^{_DNð‰@mˆÈ‚Ø‘¿÷Fg'ÀóUõÐZ}ŠÈØ*¶ómÔËhìüþBªº™ª>[¡Ddul˧ë©ÁžÙ¯‹È¥,¬yÚf "ÓˆÈÀ{Àþä+ÂX>õ‰±iQúÀ|~#"=c+”WD¤7vV>ÖdéG €ª¾QË’ˆœ—‘¯û:|  Ì«ª‡¨ê‡±ê 1ù‡= +íûO Þ‘ã’>ë4@[LDd9ì ðÙÀô‘ÕéÊàaUýx8².]QL¿´Æä¤ðglá™ßJs>aÓûÖK]§ D¤?6œŽ:-ÃCX®’Uõ Uý&¶B‘¹?Qû$núäó¯ˆH×ã¢N ´ôlYD¦Çò{H~ãóß¡ª’¿/&[GDÞÁr§? ¼ || |§ª“Ê}1YEw8ÙMƒ­gê"3vù{nlf?醓­ÆBÀm"ò(p¸ª¾Q—Ü "[ -Û纒ì߃µ§35c°#œ©ê«±•)G§áR Ñà^ ŠÈuÀa±O-‰–͘dúºlòÊÌãöMé¼ÁOGŸ²`"0 ˦ö)–XeHòï{XƯ÷T5¨ Y¹ý<‘¥€Õ’¿³f2ö<¾Ï9‹ÈlßµD5®ö©Åû<¹¿o£ñA£•ù[L\™çÁ0‰1q.°CÀb‡`¢ XfËÒr€äìò)À1ä{‹c2p¨ª^ÔùEYø7ùz°ý¼…MN^Å(ŸWÕÏBV""³a5°Y®ìÆg§«êø ëNçÿ¦Žº—%NªÅ]DöÀVŒîËñŠí_Ü•ç|IÛ‹Ñ0S ULÎNîd]uJ¡ª-#˜)èYìfȳ<Š¥/-÷;æ®À¥ØºV’¡ÀÀ‰˜Çî,ÛsQà̳{BF¿éM`õØ}9ãûæèˆ}h"¶ê¯EOÁrÄî÷y’QÀ%ÀÏc÷£Ûp,æI׿ì”CôßWi €ˆì…90M[—ŒÆÒ–ÞܦªÔò¥$¢Ý†ÀXŠÜ…ɯ/CïcÎŒ©êÐ…&ç‚;bÁiÒ´î(p%p´æØ„Y šc5=ØQUï¨öAé\ l—¶RáE¬^¯æ@œkDd!ÌaoÙ/ý8XU¯Î°ÎÂPø @’.ôRì|hÞøÛ_¾xYU ÷›<ü–Ä2™í,LÃôy › Ü‹¥òm:ºXâ1¼=– àgÍ–WaØ6Í-)Ö$Râ ˜¥%k¾¶Ðöj“­¡;S×jjþ‡ ¶__cJgºN§z$Ò±™›¸÷ÃbÌÒEjå[ìYr¥Äq59«–F=f®ŽK°{Ù·:Qè €ˆü{0 Œ¬J-ü€ „··«ê—•>œ¬þ·¶ÁöÃû¤®aúŒÄºÞJ¸ÉÀêØÃeÒKÜtp€¶X4¹ Û‹ÍšÏTõåj‘_`–³ùS×jj&`~!Ï/h`ŸøÑñwVìtÌ€DæÄ¶çÄò0cw ²M§ß×?霱uŠ)¯36ÙWs |2?ÖŽ}?4x]ºaÇŲn×—ÙjÔïìôÃFå l¿yÐ?v{gЗ6ÔŸš•ÉXž‰}£¶alêèl“‡HìŽZïôÌ>y‘ј—óòúÎF„?: K—Û#ö½Q絈qäïyj ±È’·ç ï…ì#ÏaçÝ×úÄnÿ@}¨/–Oåí\ãfå2 {ìk­-c+Pc‡[žƒÎ’ÖCb@ò;Ï>y”ÿa[#M=@Í?‰|–‚œ5–ÆgÜvO3Õ Û\ØD!v_KSÆb—ö €+O`Ì)òë\ËrÐ+öõÒ¦±¨¡Ó­Më›ÅÂþ&å@—<Ë0̳xú&úSwìøàЀz$ç{Š@oÌý˜!öõμ}c+P¥óm‰ÍšcwŽ´åÖä·ÆÖ£(ò5¶ׯ‰¾5~vt &çÓ-²ßWÿ0m zmBkOðÇ—c9?¢÷ƒ:úËÌØ–ä{9¸†YÉóÔà§ÒJ] p7òïJ>ÀN6ÄÖ£hò¶¿ÚðM Ì‹yö‡Òéq’-¼–_!ËÕÛƒÔ°]C1<ý•±|$…rĶ[¯$ÿaÈÓ’·±Û!³öŽ­@™Nx­q̯ñýÿÆe4p0k}nsìŒp}¾Ö‰}%¿k:²]Åýè[E'Á¬/±ûMò0”ª0ŽeXœ…—rpýò C€c·K&m[ñ¤t—bÊHÌG !‡Ì ýL¬J'b!P»E¾Ÿ.Îðú?DõÁ¿À+v_ )fþ_Æ~~ÖÙ7VÆâo„Úk%ù˜?v¥Þb+Ð¥Cž˜ƒ†w)¾¼O§ø ôÃå°¬€!t¹˜9Òý´ÙYҦʞ?0=–ê:vÿ%…3ócù^ËÁõË»|Ì»ÍRí±èÔ1ÈAƒ»´–< ¬Ô`ìù„Ø;ÿX&ãûiFl€Êâ:?No,¦ý 9è!äeÌG)—Ÿe®ÿ2XÒ4_í×'ÑÂ>ÑH:ç.´ö—x2¸˜¯Á¾¹2ðn=Æ»fxO]“Ñõ}‚êƒÿB®aly‹%Qˆ3üÀL˜£eZa±ÛEÞ%g޽ÁúHt,×}«z»äG¾ÃêÞ“ÇÒ¹^@‡ÉXôÀTÄ0‡Æ,®i-+ÿU(và˜ X Þ¥b?+kl{Áb§ÜŒËÁõky ˜#vûï/‘;ëÚ´Ç9—üÈÀb ö×Ýsfý>šˆaPEÇY°t»i_ÇÿQ%Âþ¶¨güÇa“¾Eb?¤kl÷~Øjß÷öӓר!ªe‘$f‡]žâ>\Š-c±”¥Ó4Ðo%Ìq©w_¤p_Ý’Áõ{‰*l[¯™áºÊW˜3rîû°(})“³ñÜ®ò ŠäXµEê¸óba]c7¦K{Ë 4¡ s¼(@ýß›¼¯vÈàš½A•ÀKÀa/ŽÇ—˜Gÿt±Ê5´óÀo±Ó.±¯[;ÊõĤj_ŠÐyûbæÃØèâ¢Ø*õ4;ØÑdýƒÜWsb«×4¯Õ»À\tàÔ´i=2 8‚ÂG}PÛj,qM-+­&§ÅîAúUÆX€›sÐx..]åàç ôé…ãe}R“÷Öÿ¥|}>¦ÂI ,ÉÒ¥9hÇZe(f©¨¸(¶ðÓjÿƒ\3—)eÿØý£éþ•qg>!æâRNF{5Я§nh²î¯h0j °}Ê×å3*¤<zbûбۯ ôŽýð­p=}µ_ ™l»¿4Õ×2ìÔ[àgý]Š!7Ò@jPl`iô½wƒ÷UÒõ§ù’ – í0v›U“¡X°±Üæ}ÇWûE”Ñ,Óã}.£Ž½8æð»±\\j•weèë«`+æz꺰‰{ëê¯Á7À’êž‹ÿ»­*Éà8rlêOúÌÍøj¿¨ò)ëM½ïeй§§5¢€¥%>ÛϯŒ~C¿Àjwt½ƒë§øÛG«T¨{&à©´Q9ƒ%vŠ’‡¡†¶ëìŠå }­\š—{ˆœø«¡~˜AG¿:“W‹'ó™¾å.`–:ûýtTwÌ{ŠW¦˜ßAZ“Ç€*Ô=+ù=É3¸Œœ†nÅÌü'“M°&—l唨ý«îþ˜rgß.ââB>¤Î½>Ì3þ¼2å=ÌØÄ½õç”~ç$`§ õμžƒöè*9ýP-sÝ–Å" zÀžÖ•IÀf±ûZ]ý2Å?/¶‡»Q*ÉóX¶§Øz¸C¾5p/Ì”°ÍþËcÈiüÆ+Ô;¶ß»ºÊ#4௑úÃÕ¼ù·ÅÂOǾF.ÙÈ`áØ}¯æ>šbÇ8QI†`³òµhϨ„Ïä@‡"ÊdàÔ¹ßì„™Ö›ü{^v·“*Ô»0Ù¥®U>  YêU;¹ðv®‘Köò29>m2E_Mé8.P‹üx1zÄu€á9У¨rU2ᕸ/Ö¢‰Á?)ãÄ”~Ïê\œ|íYž1¹:ËE9=ø$×È%®œ»?ÖÔgS¸ –£8Nmi™Qó*Höx¥±¼ñßæ@·"ÊkÀ‚0?#¯×SÆ¢,Cú!†k•IÀUÀœ±œ]®ÑŒØùý/sp\ò!©pŠ&/úF艧£Ì»|\Lì‹H‰­S‘åk`Ý ™nÀc)è7УL+#sp5ùíËÄ~`v¹>³ÊÑ5rÉ—¼GÞsL¾!Šbúw±¨hî‘F&§<ؘ‚ÞQæ"°:ùÞ5 uœ›ìk@?à4,VBìëã’o¹$v­$’t覑€W±}0'ÿ|‚ÔpÂq6p´†º©DdìèÝô‹}XSUG–¨o,†AÌ{y"p!æ˜8*¢?""Ób{üGa“Ç©†«ê¿b+RŠ€{‚æ8Åå&`OUª@¹ Ø4Ty˜ir5UV¢®°$4}ÖW/Ï`™Ö^Œ¨ÃˆHOl»ì,ãÔÃgX>ïb+Ò•n! ‘íðÁßqvþ%"3…(LDv$ìà?X¿Ìà¿ ðOâ þßb)zWÉÃà/"ÝDd[Ìúr>ø;1;½“;š¶ˆÈ À›X„0ÇqŒW1Óß§ "ý70g³ŒÖPÕWKÔ5Ë‚Ø#P]õr#pD©‰I DdSlŸñغ8-Á,’è›±éL À©øàï8]YxFD–l¢Œ?nðl^fðßÛºˆ1ø¿l¨ª;çað‘EEä,ÿƒþN(z`>-ù¢IoØ%h¿³ô..õHǪ»Þ{+d¦¿ñØ [ªžÝ‰sOIJõå"˜Ð3óOÈAŸqi]Ù:v_Ÿ¢ß7yÓ܃ êâ’wlWÇ}2ÓßdÊ$÷3eŽ‚¬ä `…Ø¿äôÀ<û¿ÎA?qi}y—2q7bHÃ["²:°I£ßwœ6¢p“ˆ^ãçÿ̨îãTõÆ®/ŠÈAÀ_ ä\#Ó¥Uõ™ ë-‰ˆ¬‡<˜9²:N{°°gl%~¤‰™³g¸rq©_ΧB"!,”v(“ü¥eê8³ dù»_–½âI~ÿìXjÞØ}Á¥=å3 Oìû@UiôÚ*ÑÅ¥¨r %ö¾Þ„ ¥}нDYG뜀9 GÏŽvž?ïiÊ]Z_}?44ÀbÈ¿‘ƒ èâRdyè×åÞ:;PÙOQ"Ä/prÆ¿ñUr¿ø9éäRpqiD†Sg6Ñ4¤‘ý¿=€Åøžã8?±Ž ŠÈšÀo”ûvÜoLçEä4à¤åׂË©ê ÕYé-"ÀÒ~¯SÇéÄ,˜nTê $"}0/Æ©iä8íÅ8Ìéïloº†+«ê{_‘³€#›,»V†a¡£Ç>‘•€«E"«â8¥ø X@Uˆ¥@½€=ñÁßi]žPgol¼ÙÁ,°YçÁ_ŒsÉnðÿ?à—±ÿdÕfò÷ÁßÉ+€]b*Pó@D¦Á²`9N«1 ûºðp\UB=´ÓÑ:Ì Xõ~­ª[¨êð ê+‹ˆ, < t©‹ãÔÀQ"’åQÜ)¨§â)éá81é…y‡o ¬Y—F8YUoéøO2ø_ ”AÝÿÃý.Í ®²ˆHù=æù‹˜º8Nü ØŠåø,åz*""³aþÅÔ#C†c¾§0gÕ·Tuh¨Â“Éã¢X$Ê€ €C•ïTåBU=$ëJ«N§šç²Q§­™ŒÝÜ·÷v=Ê•&Éà±°1°ž5ïŒVÑ$µoâ {-æ§““±8þ'©êÄë©JÃÿ`Ž˜z¤Œ/a ×î^PÕÉY* "‹aσ€_eYwò-0·ª~Ÿi­5Dк’øQ“ZYÞŽÄŽ Õ©Í—ÀöŸäàú¸L)“°@?mÕ›4¦Yç—À9è—=³È>A–ò6p0_ìëÝåÚ/‰å±™ƒkÔª²_æíZ¥Ñû£spaZQþlH…Ä0±;%²>v¾;FÚX—©å„NíÓ¸-åúæÊA_\³DƾþiÈXl¡µrìë\C;LŽmAžn­&ÏfÞžUû°\”V’ÉØ;ñÑë¼ñçǼ¾¿ËÁulW¹•Ÿ¶ízw¦ÜWÿH‰„BúÞv-Úï>Nf}h“À~ÀÐ\ÇV’Å2mÇ ,˜×yì Ò*òæ´ýæmòÆï…®uS`¶ò20mÒ½°½á´êúØ2}­p^®}hùØèûh£iã± ±¯k+ÈŸ2m¿ »F.F+ÈçÀ$+·V`&l…8&׸Õå+`þäº÷ÄœÂÒªëàç9è_°IsìkR>~M ü%Úk^ÒµHµ‹|L†Û•ô¯9¸E–ÉØ±¬~Y5f¤Ì#Û}Ò‘ ØYHßì_ú+°6ðE®}(í›÷Š}m3h»­1§ÑØ×¼È²VfíU¦{`«ŽØ¢¨2Ø$ö͘ñ¿4ðt®}«É!îÉÛSªc2p‘÷û±mÇã°6±¯{™\B÷ø›lÇ9çàúU®È¬­Ê4àÆ9¸E•ǰóœÑoÄ7~7àÜ? ”\\×4½ýG;ä ïÌ€6‰}ÍCÉÃXfÄè÷e¤öìxs«Læ²”ád4/— h‡2¯;•¹3ß|[‘¨êdU½ 7|[l} ÎËÀþIŸ1Ójh>VUÕ¿§Pv͈ȂX¬ÍbꈑÀ`ì9ðJleb¡ÆÀ¦Ø5qjg2JJ6Õ@Dú`Yќڙ„™jÐÈQÒò€ª~®ªƒ°\×~ó×ÏH`¶ÿCòwhÞÀο˜BÙ5#"kÏ?©G nÃ(/×dÜî¨ê¿°ÐÂÄÖ¥`l•E%¥,kcÁœÚø3¡^[‘¼¡ª7`QŒ­KP,™ÏGÀuØøÐ<†­ü?I¡ìš‘°€Xýcꀯ€mTuª~[™¼¡ªï«mki€-“dW©Rj°AÚ•¶〭Uõ±É+ÉvÈúÀ‘ØŠÖ©ÌY˜7þM¤³w°¾ªŽH¡ìš‘"r1ðÌ¿¡ÈÜ ,¡ªÿŒ­HžI&Fk`[=Nuæ–M»Ÿ4Î`;U½'¶"y'Ù<Û׊ºêÌ9ïƼýÓ0û_„õÙq)”]"Òóß?–ý†MUuXleŠ€ªŽÄ²7>[—‚°qÚL1‘ùEÒ®´˜ 쥪wÅV¤H¨êÓÀRX gjþƒ¿ }ã+p¬ª¬g”댈,„]+–x çýWßë¯UýËòjl] ÀziWÐÕ°~Ú¶'ªêõ±•("ªú °=Ú`”Sö'|öÉÀ¾‰Gv4DdEàI`¡˜zà*`EU};¶"EEU¿Æ,Ccë’sV‘Ó¬À'õs pZl%ŠL²%ðGl"ðml}Z˜ÉÀ>ªzeL%DdKà¿À¬1õh’1Àªº·ªŽ­LÑQÕπͱëê”fR¶–u¬’fe-ÀÀžnö ƒªÞòÕTx&{«êßb*!"ÿúÄÔ£IÞÅy][‘VBU_öŒ­GÎIuàÇ €ˆ fO³²‚3ØQU}ÆÄ”º¶ÿí„¡ÃGåêRoŠÈì"²sš ˆH79 ¸èžf])óolð-¶"­ˆªÞ‚PsJ³Nš…w¶,ŸfE-À±íÙ+MÇ ± 7NsL¬T%W«‰£ïãÀu"rH ˆH/,zá‘i”Ÿ!ç`9=¢™lÇ"_:S³Hrr&:OVH«’àil%㤄ªþìŠ%¥q£cð¿¶Ô›"2£~!,Vûy"rhHDdZàn`ûåfÌ8`7U=RU'ÅV¦ÕIŽ¥îŽÇ )…+¥U¸[ª3ó¢võ”IœÆ3§vÛó¿®Ô›"2+ð–·ýÇ—sEä° ˆÈLIë†(/_ë–»ŽN:¨êËÀÙ±õh‚'±‰c¬˜R¹6‘îX:Wgj.ñý¿lQÕ‹€m÷¶®£*˜ýû`«òEK½M~ÓLå"2ð)®V2à},?±iSþ€µA™¸3¥²S· ú¦UI ü>¶툪ގyÀ~[—pviq*’xâWSÝÂ÷g9¼‘Ê“­…G%ù~NxX)‰[ïD 9^ùÛØz4È@ ­¬šË'‹ôàtL<ú_iÎJ‚V8HVb«ãC*q-pt…÷O¦ö„BçˆÈõTžD÷{”ÒÖ…¢p°¶ª­ˆÃ­Às±•h€€—€4²ÁN,œB¹?NŠ|ó¦ÅH,Y‰U}X O'ZŠ{±@?%ãRˆÈŽXÄÅz8[DjòÞ‘űÁ`uä‰ËA~¼7$}ùøØz4H?Ì’”K¤Qh–€¯2¨#$Iާ9‘QÕ°IÀ±uÉOÛªjIÏiY¸Û㯗³Dä¨J‘¥°=ÿ9(?/œ ìçžþùBUÀVÓEc 曋§Qh–[çePG(&ÇVÂù UŠ¥}!¶.9à `³r«ÖÄãÿŸ4}ïL)¹µ "Ëb›fi¢üØœ ªGyTÏÜRÒ§%ç Ä'S1 ¸‹ŸÅHsG2à89BU¿Â"c=[—ˆ 6,ç›""Ó`ù*æ-õ~œ!"Çt)EàAÌÜYD&«ê©±q*r3ųžÅRE‡& @òÀHËŒ7üÏHÎÑŸ\ŒO©¾P\[§4ɶÌXˆÖvc,°…ª©ð™³5ÖyzÇ$@DVîRÍP–"±„>ÅVÄ©L²µusl=êd^UˆM‚—œè J7`6Ò‹ÕÝØ xZDÃö-wz¥T_†a™Ëœœ’˜¾7ÇÌÜí‚bƒ×óå> "»A#û%œ."—÷Ó§P~ŒÇ|&<ÀOq(ZÊõ޽O¦PvïNå£Ù8ñÌœL›A]Ír«;åŸ$tðvï!Ñ(''‰SJ""K—¥Xÿ>ãþ-Å÷XLÿ;b+âÔÅ3ëpǶX€¡ ÌjŠꉂLÔöZ}U÷w,JZIDdšwúkUF멪g›,‰ƒfZNui0sòïS˜Å.4s‡.0Ë @ðý‹øŽöv0+É$`OÌפyKí[î¬ÿ4Ø^é|™jU †kªêÓ±qæþØ ÔA?€${ä›)”?[賜‡Ó²S ’IÀ^XÈÛVâS`Ë$Dj9ÎÖÎHŸ"ñ °º§ð.pbLWÄÖ¥ NUÕ²GÝé¯,ŸaI}ŠšRÖ)M‘Âw©5…²Ý"ŸUÙku B2 ØôÒs¦É}ÀIåÞLÒ‚ÞH±ð¤ÁçØàïfÿÖ£H)š»¨ê—Ø ”ø EüÁÑB$“€Ý°µ(¼ìœè^ŽS€õ2Ò§( Ãÿ" Ní¼[:èT/´%ªgàò|Љ"u2§’p¢;a¦á¼3Ø&9BTÙ8.;• ÁÀ:ªúVlEœÔ(Ò³¹ó ´Þݪ¤þ}`¸ 5™•Bh¦À¾ªúr¹7EdàZRx˜áØàïi¢[›Ï±¤rE óýùaŠe+°wÕOµ¡ˉL’<ã¯ä¿Ÿ§ª7–{39[üŠ›/ ¾Âÿ×c+â¤K²%öMl=j¤³`xà²S™¤•¨hŒŠ­€œ}ÉœG€£«|æ/ÀRèR¾ÖUÕ4<­|R”X-%Sv7A*7)~ …‘_çÆÖ£ ŸÛ'¾ %‘½±(‡Žñ Û¿ìv‰Ó’Åð}§¿COZRIìc\lœ0ˆH_,>~ðc3 RÕ/Ê} Éðç¹ëb°a¥INË2>¶5ÒyP €onh.~[‰*ªªÏ”{SDúaûþy÷_ÈŠqÀªú\lEœ(”µ’åŒÑþmð-€ñ @ ";b9òÌuªzi¹7E¤–âxþìTÊ5Tõ¡ØŠ8Ñ(Ê p[n0‚Yp²EDʬ9áu`ÿ*Ÿù-°IºöVÕ;c+âD¥Ï牪Úy«â;lòŠJÂÂ-?1sõ8y%9*w3ùŽkñ=¶ï?ºÜDd=àäÌ4Ê?¿QÕkc+áD'Ï÷uWÿ¨ªR€ @ðB J‚Å8å9‡ü•\)bˆÌ‹Åùw«œqªªž[ 'L[–rù©LÆ„.´ ¸  ˆÈ¶À±õ¨ÂŪzS¹7E¤'p 0Kv*åšk€c+áä†",ÐJ…¹o¯Ë|Й"t0§ "² pyl=ªðpx•Ïœ¬.Eà!ÌZüçéÌ[ø´Äk!'ÁÃ!w£Ë¾EÆ|±î®Ì[§>D¤¶ï?cl]*0 öSö³ˆìBuÇÀvá%ì¸ß±qrÃ\c[lhÊå·œàjRøQ ²Dlœº9 X&¶P`wU-›gBD'ÿ'²âc`Uõ°ÜNgÆV FÒ¶¤âPÖ#9î^‹Xg~[§vDdkà ØzTáLU½«Ü›"2p+ùŽX˜#€U5íU”S<~[IÛ •-€X€ÏgÛ#Õß•9D¤ûLmˆÌ\I 1ò(pB•Ï\ ü,]òÎx`kOëë”aÉØ ÔÈGÿ“ø.„ܺ>VÇ´\’¤y¼’üDás+@ÎI¼åÿÌ[— |쨪eÏ‹È~ÀNÙ©”[ØKU]°ˆô‘õ’´ÐNqÉûñ^°ÕùÛ]^ëOØEJ*€Ð9‹«¡ÀµÀªú °öÐŒMQfšíÌiÀò±•¨À$`§J¦lY óúwà·ªzcèBEdÌAô~àŸ"’ç £S†¤Ýаð¾ªvM(7kà:‚/”»a¦ø´ù;Úó[`Uݽ³—¯ªÞ9zl \¼Äð^#BN$Qò~[*œ¤ªÿ-÷¦ˆÌˆ÷÷$?p©ªžºÐdŰYòÒ–Àó"²H躜ÔYbœx½Äk¡cz¤bÈb0óŒ,󙉨—üãtÀ‰‰ÙÉ"Ò;5’gs…¢W e KÞ¹‡ôœ8ÏvíòÚ‚Àã"’gë‘35ëÄV F ;ÈÂëvq솼 øLDÎILtˆÈšÀ{˜¹î `9 WzueZlÆéä˰óÀyåc`×į¥$"r°Mv*å–°ì~!㤠"¿-óö¬ÀEdãëò{ IDATÐõ:áI,9[ÅÖ£FJ9°†Þ¸¼Ì,é‰EE;@DfîæËXrø:gˆÈÞÀÖ±õ¨Àx`[Uý¦Ü’•çYÙ©”[>6UÕàÈDdgàU>6-p§ˆìº~'8+óÄV¢F^-ñZh @©c†McÐÁ¡IÞóÝÉW$·­E¤{Nmˆ,DþæWÕçʽ)"3cûþí¾½ÔqÖ?xÒYƒÚ†NüMDŽ ­‡”b+P##)m}¬<• ÀÄÉ8° æ “'f6Œ­„ó£'÷õä;ØMªzq¹73æÕäÇ‹ñÀ–ªúfè‚Ed1,žH=Û†œ&"öc‚ùCDú;ÇÖ£Fž(³õÒéTIa»¾[²7$tÁ5²°l¤º+18¶`Ùàòœ çMª÷•£øÉ½]Q`U}4tÁIð®{~ ñÌ0MÕO:Y²·iÖ<^æõA¾†WÊ'Ò(Ý’ƒÏÊkdòiÝXDв÷Ô’ˆÈÊØ±Ñ¼ò=0¨Ò^¶ˆ¬ œšJ¹å8Uý{èBEdZà.š¿;p«ˆøÑÌXdŒ­GL5‘éëH%JfÇ VÎ#Õ[iÈÿyó–%‰‘5Öyå€J¡kEdV,bažC\®ªg„.4ñº;1‚-‰È Êsg3Š”m<ð|‰×#ì‘å—–õ#±-yfßÄyËÉž €…c+Q‹UõºrovœB®ŠÈݤ—æø|Âû­üGDB{o;5’¬þOŠ­GXD½ùcëR‹¨ê»]_‘Û g¡šÌ¤ª©DDUG/@žéKõÀ"N8þL~ÿÉÀnUÿ9hïÁÿ쬃ÿ ØÖJÚƒ?Ø1凓 ‡“ GS¬ÁÿRƒBÈÓ/¥1øÃ”7Ò‹iTÐì!"«ÄV¢Õ‘È·éÿtU½§Ü›I𨱣]‡õ+tÁ"2?p'Ð'tÙè‹82Ã:ÛY8&¶urg©EdA†-2`YSÐyðDZ•.ñ$Aé‘dÈ»,¶ø/ð»*Ÿ9X3}UrËdì¬ÿc¡ N¶(ï!|dµZèœ%"õXé\×kÉvr‚’`ÕÀõ¤66wž¤6Ëh–NŽ­D óg`îØJ”a(°S%g6Ù8.;•rɱªzsèB“‰÷mرª˜ìÜø©8aù-aMæY0s.ÅjëÊdð,ælà”æè$°‹dðÜ+¶e˜ˆy²—ÝÏ‘¹ëÈf_:¯\¬ªÁ%GÂ.Ö ]vƒl‹Ò—'žJ2:MLž‹,KkJ½‘Œ!c˜ŸÝ™®«ß¨ÌÜÀ-Ƀßiž3ȯéÿàœ*Ÿù–Ъ]yØ1¥³þ»ßm·¾Àõ"r? #9ò÷,ùZÑxLUß+óÞª„øï€eME× €;Vgu lö7§6Dd`ߨz”á}`OUÕr‘Í#²S)w|l¦ª£Cœ¤ö½‚°Ò48øorüÓ©‘N'fòœè«WUxoƒ€õLÂbФ†t~Æ%¡o¿ x&™ü^U‹²27$¡rŸVŒ­K ÆaAl^*÷üâd+ Í×À*ªúvè‚“ã`OE Ãý6a¼/¶"E@DþŠ9U‘QÀœ¥&¾ÉÄf(á¬ϨjªÏÈ),ªú n¨•ß%!IúÙ|þUü{·Ð¾ƒÇYÿ4ÿY±ã~EüÁŽ'Þ“l í([¦ˆÈéwðsþ+gõZ•°[ÿ XVIJy.—‹mìLÍ9"rBl%ŠD’m-¯)roPÕ+«|ælÂe + 죪åòŸ7L’Š÷ò›!´‚m ¼ "KÅV&ˆÈ)^°ŸÎLÆ’P•c›Àõ¥>@U§`ìFw©]N%ÙNq©,X¦¿ØíUJ^úVÑ}PôŒ)G¦Ô'ºaV•Ø¿/”ŒÃFù3ÁÚW0‡ÚØíÒ¬ÜYå7 X×0 {êmSæÇ¼•ƒ‹]4¹èûf˳`•&ä ­ºÊ·XÒ§Jº/”|.¶®±ä¢ûÅ9ø}iÈÃÀÏbßw‘ïù^س1v[„5+üÎ×õ—,Ú§\ðߨŸGDdÞØŠä˜“ÉŸƒ©b!lË%õè0Oß Ì™VùâNàÐ4 ‘ÁX˜Vd à%9YDzÅV&kŸŽû±gcÑyQU®ð~è¤Q·.¯4ef3«¶UTù Ø$öÌ;o‚åÇž”ƒöé*ªA÷Ks g,y†*[#Mô‰ ȧE( y X#ö}˜áý¾"aMâ±e» ¿µ;ðqÀº†ݲh§r€'°œÞNýôî‘KDdºØÊ¤ˆô‘Zò^ïKþÂåÞO•0¤IÄ·ÁÙ¨“;:ÎúOEš„Ó½…üY„ÒbQà!¹ª•ãˆHw9x„üùª——± EåØiù½MˇK…YÍÉÄŸu]ÞV=Oi†ß?î¨òÙn˜SKìöè,ý«è½vî7¶®1ä+`‘”úÎ\I¿‰ýcÉ÷ÀïécßÇÛuaàñ\ßвE•ß}_àúVˬÍ*ü¨°c±/~ÑeðW`æØ7h ›¼;°6¹éøƒª|'´ƒL³2XºŠÎ}Ws k ‹úI£ÿL¼ƒß˜ù8èû¾n²Mû`š±9¸¦¡åY*œæÀŽ­†ÜÚü€ŒÌÿªJµ†}$ Ð*2Ø;ËÆ |“ °-ðF—ßõ9U`XªÜØ×¿³ì^Ãï½:zÆIÀ¶)õ¡î˜ƒqìߘ7y ŽÕ3ö}Þ@{î|”ƒk˜–lX圸¾3mÃ*?n¯4@«ÉkØyòBœÆ<ߦüÑÐ?ÔPÆ­9¸îRõx íÝïH±/]”ƒß—gù ”3Sìû¾J;övÞÌÁ5KSþ]å:ôÆv¡ê›Ìi[VùÓ£sЭ(/b³çÞ±oèíÞ Ërw1ð]…ß0˜§†ò^ÌÁõV̹µâ* ‹UЮ}þ‚ûÔorðûŠ"ßç Å~tiÃÙcOspÒ–ñT‰áì¸Î{2oÓýª4F+Ë×X”¬ˆ¸=ôÄ2žGíÇwþYcÙŸçà:檢ç ÀÛ9Ð5†ÜNJ‘Ç€-ÉçÐ"ÈS˜n¶HÏ…i±­¿ÿ£}Žl*pf•ëÒ ;²Î­²nß)²–"9®órÅ9¡ø¸x ËõNZ‰ÈLØjw5`MlÅß·ÎbÖWÕj¨ëkâ&x™¬­bØ‹ˆ`[¡ãyg°ë“Æq¿å°ˆxõö-gJ&`ñ“Àëª:)t%I²«%±|ë`¦îvb(¶úUî"²psÀ:‡óªê„€eV¥ê@DÄ:‚“-߯ï%ò¶çôvLk,ð]×A –±nìØÕ`ìhÛâ4nõì&©ÚDd(óìóªzI¥ˆÈ‘ÀYé“'ÞÇÒ]°ˆÌ<õA',£°g‡Øsaö¼m!ŒÃž]™³öõfÂ&æ€ù€XÈëž©jžvQÕʽ™¤ý}{–†âdU=%`yµQ£hâ›d\*Ëd²Ý»>ìÿÛ;ó°=‡«ÿŽ%!"±­¥Ôª*´ö]ƒZZUªª(j«jÅÚªZK­ýˆ¨Ú¢Z­¢Ti¤H?ÕT­©]ÄJ$i"‘7çûãÜo$‘÷}Ÿeî™yžûü®ë\\¼Ï̹gæž™{æ,u#þ3a»\Qƒ~ÛQ­ãÍNy›ò|ý—žÈ໓÷È/>…KZé2áÏþWí:à‚Ü…Ý:ù"Ä;f \WÇß-K‘x»?í’"*ÛÍT'"]'3°'e]3}»bʙǤVÂɆ‰ÀQÝýˆ, ü0p½×—qW 5mŠcÞîò ;Õb´ªN®ãïÿZš&]3 PÔ嚈,†-þU;¢îÀŽ9ÿ¯¬ Tõ ,šhÎü³·q°«Â7{ø›“±«’P(æñ‘†:Žôú`ë©h\ÒK]Á*0„˜VàÓ€A5èÕ9Ê‘£c1b‘îrõØ _›Z—ôrS cùØq}ÈzïHqô_ïjÂ~ à€ÕŒªN ^ŠiUÕǺû#ÙóM¯g_çQPÕ˱ë€Y±ê¬‘‡Tõaµ4з§VÆIÊxl£Ú`aC’Öð¸ÎÝ|ÌÒ4õnÍ%­œÒÀ—àçˆó%XKdÂtà¨]åÊT_À.ØÉLê6Pà>æ `…eð¼Ï}REyج†ñ»k uß—òë_UiäE>=ƒNsI+=—u1v./Y¯kè!Ä2Øä© Ú0¶”è§Žþß3´Šýì»±¹kƒnô[ 8¸ s¯KÝg.åË15ŒÛ¾”“ï ZÖ¿®¤‘üìc§NuÙ¤Áß Á\Ëà:àp-ÞØn¸ Ø $råAà-!pL=¨êØËåk‘«îƒù½/¬("™÷Š@PkëbþÝ«DÕÐIÁMªúóþî\ìþ?$VÕô¨ îäÿ‡ô;7—t2‡–`ñÄC&™ ¥†äJ˜K`ê¶‹-O˦þÒX V§ëäR1d,óœúxŠâjÉS@߯êv”su¹Uê÷PUiôîGØ,H.­';5±¬Œ  Ã8`›ëÜKð‘ºÝbÊx`•’ñ>Mþ~,J`ª¶yšâJ³ HÝW.ñäßÀš5ŒÑ%±ˆ§¡ë¿;õÂß)\ ªS°¯.§º¬ÓèUu"°3pf„S/À0`#í&¾'"²"ðkªât°«ª¾º`Ùx^Dº šÒÅØ Ë}‘‚õ€Ï‹È@`ÇD:8ñé €5®†¿=&æ¹.˜ƒeT̃&vð‹“öoA©j·TrV /ÉUÿ¥6ÿÚ÷1C¿nÓt.PþbÀÈ Ú+¦L6/éËcæ?ý;£Éò†'j§+°À.©ûË%Žt`ÁÁj—{PŽWÈðÔ_ýó=g“/ï—2èÔNÙ»N­GUäœÀ Ër˜_þ(æ?ªŸüø°|å^œA[Å”ÀŽ%-þ·Rçå4‘Ê c=4A[=YŒ­Ô}æGNªq<®„]„® °rêE?Ø h¬Ô÷gÏß)t9x”j&v‰-5½LMŒ«å€%š,ãÐ Ú)¦Ìö,©?V^é¦îëhÒÍ8’¸ïîllƒ™ºß\Ê—KjƒÜY’ß½À÷$ Ù,Àì¨$³€w±,x`ÇÈï'Ô§J<]fáª:YU±@D¶Ä®ªBp°ª¿S‘X.úÕºù³ƒ€›DdñFëQÕaØÑë{–Q'‹’6Mµ‡_Ç×ø·ßv/A‡ñ¤Œùß¾ÊðÒ“ÌöÄ¿ücÉ `éÔ»×nÆã*TëËn¡Œ¶ì‡eÌ«U—Û€ÞMÖ¹>ðbíêÒúòj<™Â<…f•¤Ç>©çÅ…>s IbàÄ]Åèn©äÖÔ·›±¸ðpmSN(±-G5 ÏÝÀ’MÖ½À(uÛº´®Üôªq¼­¼Z’·§ž»’W¨ê»¤O¬Rµèn)¹4µÝ0 Ø,µªª†.TDnvhàç»w‰HßFëWsŒÙ8N½üûêî1 U1ÖoÂ<’B3Ú ¥!ðÃݤßõ¹”+¤Þµv3þNÈ }bÊ…%¶åôû+Ð?€.Çã×{.µËŸ¨#Pðãu9>õ¼Øí³ž4Ö"|¾d—¼ä ©mcoªåz%5„?n°-Cºä–  ÓöÀ›´»KÞò{ê°Aö§¼,cHœ€«Çç/aò’Á p)GîM=`»sëP­4ÕÃhÂヌ¶<¦}V  Ûª¤ ì’·Œ¯cõ`]È8ë<”AÛÄ’«)oñß‹¡Q¦þ/kÒwà úÄ%­ÌΤŽë0,ÉÏßKÔéA2?úï” ^ ¢ª/cYN{08=µó""}°û¾-Rë‰áÀªZV«±¸üeò à/"²^³©êoM±4¾N5™ ¨ªgj±²÷D¨ªsì”Á{ÀAªÚQRùA)e ªWw”U¾•‹‹M]ˆÈ:À=XFÁ*p5pT‰‹?ªz3p4öS&«÷‹ÈFͤª/[Q¾ÞN^L>¯ª#jýˆ,\ ìVšV–þåËKÉG´€×HLäÒ¸¼ ôM}T, ì‡-üeYíæ(Ã)éØ¿‹v>‚8ÞïÛÔ{Ò#s‰#Ok50F.+Y¯©çɺÛ$„² îÃÛÊòµ$za÷Ò?Â"ûUÉůSJ»óï¡í÷ÆÂ=—ý|3€½ê½<ð« ú­ YXÆ*Ê/¨ÃÇž±ñÃ’õú|(ÕÝ.‘&”“28.õËÈÈ ÏêÀw±l\Ó2xþ”r ÿyúb{,ÑVÙÏ9»Þ©û~ÀÛôaH9x.=RÉ4,ÙU#ãᜒu› ¬Ÿê]mê]‰4™p{ƒÈ¥v™|*ÒØø–D¦Š_ù “sRO Eß "Þ±ú™u_¸5ƒ¾lVÞ~,¬EWL­SlyŠæ"ln¹(‚~INIƒ¼''“e—2L.µÉyÆDÌP4õ³æ"s€SRO ôÑ'±‘žÿ*êäR£þ{a©XS÷m³r2庮å(çK4Ðï‹b³ÊÖñ¢ÔïhSïGäÉdC`JË¥{y’Óý+fð¬¹Hptê ¡‹¾êO¼<# :xýûçÓÚ¶H¯d CLyølƒýÝ»F,[ÇQÞ°F·L&{âG½±å êKëúõ’ÇÀ xúæyep@êÉ ‡>[ Ë£=žÖ-á6FgÐß.]ËLà \X± tOFÐóoT“¼×‰&“ªemK-ïbSjùÛQ””d¦èûþuèR™|)õDPGÿCœ/éÉÀN%è/XÈã3è{—ùåwÀ:MôíÖÀ¿#èù6 ¸!æ()'’á 8—ùe°A‰}¾Õ4bêJ¦RÂ"áÝÝ™8axgaUÊx†ÞÀ‰‘žÃ¥{ù°}“ýy8凲֢ŽmR¿ƒÁÞƒ„“H/,¨KêÁçò¡\Pb/Ýï¦~Æ\d&Wª£?מÔV¿¢$kì:ê<Üí4…¼ Dî®X\ÿk"é;‡Ì¯êên¿Ä“H_,ÈKêèb–ÒeM²‹¿Éàs‘§€ÕS¿üúuyê³-iFž¦D·T,ÂùÀ2í.χѤ–ü±ˆz7õ;|Ü'WÀ&‘e0(«.{”ØÇ?Ëàùr‘‘À2©ß»€}»pq¤¶› |£äçéܸ·Rxy 8€™ò€}‰¨ªSÎHý®•2ÞS+Pt檴‡¯n«ÊM%ö­|~(×½R¿o%õó!Ä ¬À”¼‰Â\ɾ‡Ç.iVæ`W½{ ²eÑ/×D~†Ò®FSKræéØÄ±àt™_&+•Ô§ûaþí©Ÿ1J‰Þ9°ñ’½BJ, ÌW€û©Vªfe2p!Ý9í€q‘Ÿã*Úø½M®Àü)ª‘Ñ«#£ç<¬¤¾ÜŽ8V¹¹Ë,àÐÔïVÄwxeâùÚÏÁ™%#=ÛºX\yÏpÚuŒÆî÷ëNØÓM»/ü„ø×àº"g‘¢³ADÖÃîI?žZ—’˜\Ž5ýK™œŠQXNí ƒ@DÖ– Yn 2‹~_jEb""½€K€£"Uù"ðmU½7Fe"²(° fÁ¾'°tŒz3æìZæFU²`Ù¸Û|ÅäJàU¹Þ¨d·€¹ ÈHÌ6 Ý˜ \>ž‡Ýi¥`°‘ª>²PY[üc¿°¹ñ–êöåÔŠ¤BDŽÂ6½"Uy#p‚ª¾©>Dd `0vMð%̨¹ ¼ÜÜ ª„.¼˜GÎÅ‚7ÅägÀÐF9’å@DÖîÃRĶŠE“Z1¡ßWÕ‡,PDÇbÆï²Üä&àpUžZ‘ԈȖÀÍÀj‘ªœ œ\­ª‘ê@D¶Â6ƒM1/‰vá ,«ëmÀ#e,""À˜'FŠùñlU==A½IÈv "«Ä’9áxøœª~²P²Ì£8UUÏO­HNˆÈ ÀuÀn«}8IUï‰Xç|ˆH?`Ìf3`c,öI«0;‰ ŒTÕ×ʬLD¶Æl:6+³ž.P,çy êNFÖYø=°}j]Ú„`+Uý{ÈBEäà‚e¶“°$JQî¡[ Yû2ŠYÖÇâØF`lÄ:Ja;ð)ìd`Mq v:û˜{A¦`›¦1XÚá1ªúbŒŠEdMÌj_Ò´ÃûÀ!ªzs‚º“’ý@Dz×cI<œæ¸PU‡„,PDöÄ6i1'öœx»ï)µ"¹#";#0oXÌÆæ³sì#Y Û¬…?¯ | Û¬Œ%Ðꇅ½m”÷°Mêìþþ ,ÆÁ3À3eÝ/ ùp*f,Ú;vý“€/«êèDõ'¥%60÷ âBàøÔº´0OcñçßU ˆ¬ütÆŒ©¹3<›‘Z‘V¡˜øG`Çã1ù€7A­ÕcPxWôǼgþë„E07¼Nfa‘ßSÕw¢)Y"²2p ¶ð7³©i–€ÝCB·-³èDD¾…¹Ñ¥Ú1¶*³±£ÿ1¡ ,®gþŽÅä®ï`†~¿K­H+R‡Ÿü€x^|€Ù$œ«ª/D®»²ˆÈjXtÅ£€>‰Õ짪ë‘”–ÛÀ\ËâßbÇdNmœ¥ª?UXq"s°{¨2[ˆ‡±ûþ–ûŠÌ Ù ¸òÙÕÏî.VÕ?'¨¿ˆÈ&Àwý±“‹”(p)pbh#èV¤%70×CàVÌ ÆéžGÍCxù)pR¨òZ„àGØf*ª‹Y;SmŸŽÝ§r›{3b½µÝƒ¿Ä p Þ »²Ý&±:LÁ¢rú©]AËn`nŽKÃSë’13MUõÉPŠÈXЕ*ñ–‰îÔŠ´+"²)ðKÌR>ã€áÀ5ªúFB=Z’Â&è0à`ÒÆ9Y±ÀWTõ¹ÔŠäDKo:‘ý±ÐU5DëŽSUõ'¡ +ŽóF“Öx'&Š-J'¨ê»‰ui{ŠMý±»â”^%`W\À{ýT kDdy`o,#äÖiµùŠêžè¹>J[lDä“X¶A$rå!`ÛPÇÕExÎG€5B”׌ŽLL¦ªAa~ ¬X°qðkàæ2ÂÞ¶""2[ô÷v$ψ‡obÉÎþ˜Z‘\i› ̽w:BúÀ©ùæòäÈ«0ú»Ø#Dy™3ø9pšªNK­LU‘>ÀYØ=r. Ì Xhã[€Ç«/æ†èìZȶä÷ãVló^i+ÿžh« @'"²ð ,°FU9DU¯ U˜ˆüv4Ûî<‡¹÷=˜ZÇ‘Ï`Ǹ[¦ÖeÞîþ„]¼XŸ ùX¶vƲ®”V£š˜Œ]×›ûÚ™¶ÜÀܯ‡s€ã° Uâ:Uýf¨ÂDd0–ä'ç³ÌÂMåA}ò£8úp.y¦™ž<‰¹ˆ>ŒÅÇxºU¼EŠ˜Ÿ¶Àý-h^®Ç²øµÕF¬LÚvÐIq—ø ª“žv,°E¨£kYø'0 Dy™r;6qxP˜Ì) IþÒiÀãXÎç€g±Ð»ãRù Q×ÄNG7Àý hí¬«ÏßVÕ‘©i5Ú~ "Kb‰HN–H¬N™¼…-þAÔéMïǾÚ‘±À÷Ty‰˜…k9SU_J­Œ“Y Ër2fç´—7xæòð À!…OÆâZ·ÂFà0U½&DAŵÈXà“!ÊKÀ$ì¨ÿ2?êw:)®Ä6÷) 9Ý£XzæsUutjeª€oº ð—=8 X:±:ݱ™ªŽ QˆœŽùX·cY!Gx§+Šp¶»bW;ãáÂsa*pp¹ªŽM­L•ð @ pŽŽ!ÏÈXBÄ»‘þÀ+´NFÅìªâÒÞN5‘ÀÀ7q;Tü ˹q½ªNI­Lñ @ˆH/,óÕ±ä“|ŠªöQˆ ÎQVÉ<‡}-\«ª¯¤VÆimD¤7–Õîp,«]Õ†Çf ðìýõ|‰ñ @ˆÈ&À_–L¨Êãª:¨ÙB ?êÈ×íï-̨g^Dd`àÀƉÕi':€û°à=·ªêôÄú8¾hYs—;˜4§PÕ/6úãÂr`?`Ÿ`Z…a:pp#ð'ØçÄDDÖÇÞ‰/›àöõ2 Ús ¶è¿•Vgaø ÅâÁÀAÀj‘ª½RU®ç"òi`÷B¶&Ÿ<ë`6wF¹AŸ“"²°ðE`[ÒžúåÌTàÏØû{›ªNJ¬Ó¾L‘¶t'ìz`o,9GYœ®ªg÷ Oo`{lòÚË– X„¯;;Uõ‰Äú8N·®²Ûƒ1O‚AT×n@±Èw2:U–C§1|P"…áà`ìˆ}/,{WH ¸Óß ;‘Ø•|Üg£€ý+ÁieЍƒ[²5°9–•¯™¥ï|GûûÛÚø Å—øÎؽâîÀŠŠÝMUïž§ŽO‡bFL9¸,NǾðÄ&¿©ê´´*9NyˆÈ¢Xf±ӰԼ­Dð ¶à?RÈ£ªúŸ¤Z9Añ @Šk‚M±´Ä{`E#FF[c/æW±8)Ý'`yæ•çBæ(pœV¥ˆ'2X¯øçÚÀê˜çMˆF™ ŒžÅü§;ÿÝ­õÛßd@‘‡à³…ì€]Ô½Åobe>› Œ^,äyìð±Áˆ§Š¡Š×>VÈìoeìÝ^ »>\èËüFˆ‹ÿ}&vâÖùÏYØâ>i™Œ¹Ô¾Š½Ç¯ªê»¥>œ“5¾ÈŒât`:éò¼¼\ÈK|¸Ø¿Œww<Çqœö '0ÇèôŠPÏ{Ì¿÷ 0NUß‹P·ã8Ž“ßäÇv”td p+0é÷óŽã8ÕÅ7ùq@ e¾ŽÅùæ†=Žã8¸ @V>Åo.ÒØ8à\,ñÆÌ@e:Žã8m€Ÿäž„Yüß..ò…ßqÇY¾È‹ƒ›üýdà§À¥~Ôï8Žãt‡_d‚ˆl4 6p0ÔýzÇqœZð€|8®Áßý8FUÇ„TÆqÇioü Š0¡¯}êøÙDà$ÌÀÏ;ÑqÇ© ?ȃéoñ¿ËøzIú8Žã8mŽŸ$¦Èö<°f >8 ¸Ä¿úÇqœfð€ôìEm‹ÿà U}¶d}Çqœ °HjŽíáÿ+p°•/þŽã8N(ü !"òiÌõ¯«Øÿ“€oªêñ´rÇqª€_¤åxº^üÇ_SÕqõqÇq*‚Ÿ$BD–^ã£Öÿ \ œ¨ªDWÌqÇ©~Ž#ùèâ?;ò¿+>Žã8N…ð€®/kÌóŸpß~Çq'ðáâ¯À%À`_üÇqœXø@:ãþ¿…ùöß“RÇq§zø@dDdð(08PUßL¬’ã8ŽSA|9x¸ÌÃù:Žã8©ø¼'å‡pßVIEND®B`‚chango-0.6.0/pyproject.toml000066400000000000000000000062171507376567100157170ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "chango" dynamic = ["version"] description = 'CHANgelog GOvernor for Your Project' readme = "README.rst" requires-python = ">=3.12" license = "MIT" keywords = ["releasenotes", "changelog", "development", "versioning"] authors = [ { name = "Hinrich Mahler", email = "chango@mahlerhome.de" }, ] classifiers = [ "Development Status :: 4 - Beta", "Programming Language :: Python", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Intended Audience :: Developers", "Operating System :: OS Independent", "License :: OSI Approved :: MIT License", "Topic :: Software Development :: Documentation", "Topic :: Software Development :: Libraries :: Python Modules", "Framework :: Sphinx :: Extension", ] dependencies = [ "pydantic-settings~=2.3", "shortuuid~=1.0", "sphinx~=8.1", "tomlkit~=0.13", "typer~=0.12", ] [dependency-groups] docs = [ "furo>=2024.8,<2026.0", "jinja2~=3.1", "sphinx~=8.1", "sphinx-click~=6.0", "sphinx-copybutton~=0.5", "sphinx-paramlinks~=0.6", ] tests = [ "build", "pre-commit", "pytest~=8.3", "pytest-cov", "pytest-randomly", ] all = ["pre-commit", { include-group = "docs" }, { include-group = "tests" }] [project.urls] Documentation = "https://chango.readthedocs.io/" Issues = "https://github.com/Bibo-Joshi/chango/issues" Source = "https://github.com/Bibo-Joshi/chango" # CLI [project.scripts] chango = "chango._cli:app" # HATCH: [tool.hatch.version] path = "src/chango/__about__.py" # See also https://github.com/pypa/hatch/issues/1230 for discussion # the source distribution will include most of the files in the root directory [tool.hatch.build.targets.sdist] exclude = [".venv*", ".github"] # the wheel will only include the src/chango package [tool.hatch.build.targets.wheel] packages = ["src/chango"] # RUFF: [tool.ruff] line-length = 99 show-fixes = true [tool.ruff.lint] preview = true explicit-preview-rules = true select = ["E", "F", "I", "PL", "UP", "RUF", "PTH", "C4", "B", "PIE", "SIM", "RET", "RSE", "G", "ISC", "PT", "ASYNC", "TCH", "SLOT", "PERF", "PYI", "FLY", "AIR", "RUF022", "RUF023", "Q", "INP", "W", "YTT", "DTZ", "ARG"] ignore = ["ISC001", "DTZ011"] [tool.ruff.lint.isort] split-on-trailing-comma = false [tool.ruff.lint.per-file-ignores] "docs/**.py" = ["INP001"] "tests/**.py" = ["PLR0913", "ARG005"] "tests/data/**.py" = ["INP001"] [tool.ruff.format] docstring-code-format = true skip-magic-trailing-comma = true # MYPY: [tool.mypy] warn_unused_ignores = true warn_unused_configs = true disallow_untyped_defs = true disallow_incomplete_defs = true disallow_untyped_decorators = true show_error_codes = true python_version = "3.12" # PYTEST: [tool.pytest.ini_options] pythonpath= ["src", "tests"] # COVERAGE: [tool.coverage.run] source_pkgs = ["chango"] branch = true parallel = true [tool.coverage.report] exclude_also = [ "@overload", "@abstractmethod", "if TYPE_CHECKING:", "class .*\\bProtocol\\):", "if __name__ == .__main__.:" ] # CHANGO [tool.chango] sys_path = "changes" chango_instance = { name= "chango_instance", module = "config" } chango-0.6.0/src/000077500000000000000000000000001507376567100135645ustar00rootroot00000000000000chango-0.6.0/src/chango/000077500000000000000000000000001507376567100150235ustar00rootroot00000000000000chango-0.6.0/src/chango/__about__.py000066400000000000000000000002051507376567100173000ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT __version__ = "0.6.0" chango-0.6.0/src/chango/__init__.py000066400000000000000000000010371507376567100171350ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT __all__ = [ "ChangeNoteInfo", "Version", "__version__", "abc", "action", "concrete", "config", "constants", "error", "helpers", ] from . import __about__, abc, action, concrete, config, constants, error, helpers from ._changenoteinfo import ChangeNoteInfo from ._version import Version #: :obj:`str`: The version of the ``chango`` library as string __version__: str = __about__.__version__ chango-0.6.0/src/chango/__main__.py000066400000000000000000000003251507376567100171150ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT from ._cli import app # pragma: no cover if __name__ == "__main__": # pragma: no cover app() chango-0.6.0/src/chango/_changenoteinfo.py000066400000000000000000000020451507376567100205240ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT from dataclasses import dataclass from pathlib import Path from ._version import Version @dataclass(frozen=True) class ChangeNoteInfo: """Objects of this type represents metadata about a change note. Args: uid (:obj:`str`): Unique identifier of this change note. version (:class:`~chango.Version` | :obj:`None`): The version the change note belongs to. May be :obj:`None` if the change note is not yet released. file_path (:class:`pathlib.Path`): The file path this change note is stored at. Attributes: uid (:obj:`str`): Unique identifier of this change note. version (:class:`~chango.Version` | :obj:`None`): The version the change note belongs to. May be :obj:`None` if the change note is not yet released. file_path (:class:`pathlib.Path`): The file path this change note is stored at. """ uid: str version: Version | None file_path: Path chango-0.6.0/src/chango/_cli/000077500000000000000000000000001507376567100157315ustar00rootroot00000000000000chango-0.6.0/src/chango/_cli/__init__.py000066400000000000000000000021431507376567100200420ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT __all__ = ["app"] import os from typing import Annotated import typer from .. import __version__ from .config import app as config_app from .edit import edit from .new import new from .release import release from .report import app as report_app app = typer.Typer( help="CLI for chango - CHANgelog GOvernor for Your Project", rich_markup_mode="rich" ) def version_callback(value: bool) -> None: if value: typer.echo(__version__) raise typer.Exit @app.callback() def main( _version: Annotated[ bool, typer.Option("--version", callback=version_callback, help="Show the version and exit."), ] = False, ) -> None: pass app.add_typer(config_app, name="config") app.command()(edit) app.command()(new) app.command()(release) app.add_typer(report_app, name="report") if os.getenv("SPHINX_BUILD") == "True": # pragma: no cover # See https://github.com/fastapi/typer/issues/200#issuecomment-795873331 _typer_click_object = typer.main.get_command(app) chango-0.6.0/src/chango/_cli/config.py000066400000000000000000000046471507376567100175630ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT __all__ = ["app"] from pathlib import Path from typing import Annotated import tomlkit import typer from pydantic import ValidationError from rich import print as rprint from rich.markdown import Markdown from .._utils.config import get_pyproject_toml_path from ..config import ChanGoConfig app = typer.Typer(help="Show or verify the configuration of the chango CLI.") _PATH_ANNOTATION = Annotated[ Path | None, typer.Option( help=( "The path to the [code]pyproject.toml[/code] file. " "Input behavior as for [code]chango.config.ChanGoConfig.load[/code]." ) ), ] @app.callback(rich_help_panel="Meta Functionality") def callback(context: typer.Context, path: _PATH_ANNOTATION = None) -> None: effective_path = get_pyproject_toml_path(path) if not effective_path.exists(): raise typer.BadParameter(f"File not found: {effective_path}") context.obj = {"path": effective_path} try: toml_data = tomlkit.load(context.obj["path"].open("rb")) except Exception as exc: raise typer.BadParameter(f"Failed to parse the configuration file: {exc}") from exc try: context.obj["data"] = toml_data["tool"]["chango"] # type: ignore[index] except KeyError as exc: raise typer.BadParameter( "No configuration found for chango in the configuration file." ) from exc @app.command() def show(context: typer.Context) -> None: """Show the configuration.""" string = f""" Showing the configuration of the chango CLI as configured in ``{context.obj["path"]}``. ```toml {tomlkit.dumps(context.obj["data"])} ``` """ rprint(Markdown(string)) @app.command() def validate(context: typer.Context) -> None: """Validate the configuration.""" try: config = ChanGoConfig.load(context.obj["path"]) except ValidationError as exc: raise typer.BadParameter( f"Validation of config file at {context.obj['path']} failed:\n{exc}" ) from exc try: config.import_chango_instance() except ImportError as exc: raise typer.BadParameter( f"Config file at {context.obj['path']} is valid " f"but importing the ChanGo instance failed:\n{exc}" ) from exc rprint(f"The configuration in [code]{context.obj['path']}[/code] is valid.") chango-0.6.0/src/chango/_cli/edit.py000066400000000000000000000010651507376567100172320ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT __all__ = ["edit"] from typing import Annotated import typer from chango.config import get_chango_instance def edit( uid: Annotated[ str, typer.Argument( help="The unique identifier of the change note to edit.", show_default=False ), ], ) -> None: """Edit an existing change note in the default editor.""" typer.launch(get_chango_instance().scanner.lookup_change_note(uid).file_path.as_posix()) chango-0.6.0/src/chango/_cli/new.py000066400000000000000000000015741507376567100171030ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT __all__ = ["new"] from typing import Annotated import typer from chango.config import get_chango_instance def new( slug: Annotated[ str, typer.Option("--slug", "-s", help="The slug of the change note.", show_default=False) ], edit: Annotated[ bool, typer.Option( "--edit/--no-edit", "-e/-ne", help="Whether to open the change note in the default editor.", ), ] = True, ) -> None: """Create a new change note.""" change_note = get_chango_instance().build_template_change_note(slug=slug) path = get_chango_instance().write_change_note(change_note, version=None) typer.echo(f"Created new change note {change_note.file_name}") if edit: typer.launch(path.as_posix()) chango-0.6.0/src/chango/_cli/release.py000066400000000000000000000017341507376567100177300ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT __all__ = ["release"] import datetime as dtm from typing import Annotated import typer from chango import Version from chango.config import get_chango_instance from .utils.types import date as date_callback def _today() -> dtm.date: return dtm.date.today() def release( uid: Annotated[ str, typer.Option(help="The unique identifier of the version release.", show_default=False) ], date: Annotated[ dtm.date, typer.Option( help="The date of the version release. Defaults to today.", parser=date_callback, default_factory=_today, ), ], ) -> None: """Release the unreleased changes to a new version.""" if get_chango_instance().release(Version(uid, date)): typer.echo(f"Released version {uid} on {date}") else: typer.echo("No unreleased changes found.") chango-0.6.0/src/chango/_cli/report.py000066400000000000000000000027071507376567100176240ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT __all__ = ["app"] from typing import Annotated import typer from chango.config import get_chango_instance from ..constants import MarkupLanguage from .utils.types import MARKUP, OUTPUT_FILE app = typer.Typer(help="Generate reports for one or multiple versions.") @app.command() def version( uid: Annotated[ str, typer.Option( help=( "The unique identifier of the version to report on. Leave empty for unreleased " "changes." ), show_default=False, ), ], markup: MARKUP = MarkupLanguage.MARKDOWN, output: OUTPUT_FILE = None, ) -> None: """Print a report of the change notes for a specific version.""" version_note = get_chango_instance().load_version_note(uid) text = version_note.render(markup=markup) if output: output.write_text(text) typer.echo(f"Report written to {output}") else: typer.echo(text) @app.command() def history(markup: MARKUP = MarkupLanguage.MARKDOWN, output: OUTPUT_FILE = None) -> None: """Print a report of the version history.""" version_history = get_chango_instance().load_version_history() text = version_history.render(markup=markup) if output: output.write_text(text) typer.echo(f"Report written to {output}") else: typer.echo(text) chango-0.6.0/src/chango/_cli/utils/000077500000000000000000000000001507376567100170715ustar00rootroot00000000000000chango-0.6.0/src/chango/_cli/utils/__init__.py000066400000000000000000000001601507376567100211770ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT chango-0.6.0/src/chango/_cli/utils/types.py000066400000000000000000000021601507376567100206060ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT import datetime as dtm from pathlib import Path from typing import Annotated import typer from chango.constants import MarkupLanguage def markup_callback(value: str) -> MarkupLanguage: try: return MarkupLanguage.from_string(value) except ValueError as exc: raise typer.BadParameter(str(exc)) from exc def date(value: str | dtm.date) -> dtm.date: if isinstance(value, dtm.date): return value try: return dtm.date.fromisoformat(value) except ValueError as exc: raise typer.BadParameter(str(exc)) from exc MARKUP = Annotated[ str, typer.Option( ..., "-m", "--markup", help="The markup language to use for the report.", callback=markup_callback, ), ] OUTPUT_FILE = Annotated[ Path | None, typer.Option( ..., "-o", "--output", help="The file to write to. If not specified, the output is printed to the console.", dir_okay=False, writable=True, ), ] chango-0.6.0/src/chango/_utils/000077500000000000000000000000001507376567100163225ustar00rootroot00000000000000chango-0.6.0/src/chango/_utils/__init__.py000066400000000000000000000001601507376567100204300ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT chango-0.6.0/src/chango/_utils/config.py000066400000000000000000000053351507376567100201470ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT import contextlib import functools import sys from collections.abc import Callable, Generator, Iterator from contextlib import contextmanager from pathlib import Path from typing import ClassVar, override from pydantic import BaseModel, ConfigDict from pydantic_settings import ( BaseSettings, PydanticBaseSettingsSource, PyprojectTomlConfigSettingsSource, ) from chango._utils.types import PathLike class FrozenModel(BaseModel): """A frozen Pydantic model.""" model_config: ClassVar[ConfigDict] = ConfigDict( frozen=True, arbitrary_types_allowed=True, extra="forbid" ) @classmethod @contextmanager def _unfrozen(cls) -> Generator[None, None, None]: original_frozen = cls.model_config["frozen"] cls.model_config["frozen"] = False try: yield finally: cls.model_config["frozen"] = original_frozen class TomlSettings(BaseSettings): """Example loading values from the table used by default.""" _SOURCE_FACTORY: Callable[[type[BaseSettings]], PyprojectTomlConfigSettingsSource] = ( functools.partial(PyprojectTomlConfigSettingsSource) ) @classmethod @contextmanager def _with_path(cls, path: Path) -> Generator[None, None, None]: """Temporarily load from a different path.""" cls._SOURCE_FACTORY = functools.partial(PyprojectTomlConfigSettingsSource, toml_file=path) try: yield finally: cls._SOURCE_FACTORY = functools.partial(PyprojectTomlConfigSettingsSource) @classmethod @override def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return init_settings, cls._SOURCE_FACTORY(settings_cls) @contextlib.contextmanager def add_sys_path(path: Path | None) -> Iterator[None]: """Temporarily add the given path to `sys.path`.""" if path is None: yield return effective_path = path.resolve() try: sys.path.insert(0, str(effective_path)) yield finally: sys.path.remove(str(effective_path)) def get_pyproject_toml_path(path: PathLike | None) -> Path: """Get the path to the pyproject.toml file.""" effective_path = Path.cwd() if path is None else Path(path).resolve() if not effective_path.is_file(): effective_path = effective_path / "pyproject.toml" return effective_path chango-0.6.0/src/chango/_utils/filename.py000066400000000000000000000020021507376567100204460ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT from dataclasses import dataclass, field from typing import ClassVar, Self import shortuuid from ..error import ValidationError _short_uuid = shortuuid.ShortUUID() def random_uid() -> str: return _short_uuid.uuid() @dataclass(frozen=True) class FileName: slug: str uid: str = field(default_factory=random_uid) SEPARATOR: ClassVar[str] = "." def __post_init__(self) -> None: if self.SEPARATOR in self.slug: raise ValidationError(f"slug must not contain {self.SEPARATOR!r}") @classmethod def from_string(cls, string: str) -> Self: try: slug, uid, _ = string.split(cls.SEPARATOR) except ValueError as exc: raise ValidationError(f"invalid filename: {string!r}") from exc return cls(slug, uid) def to_string(self, extension: str) -> str: return self.SEPARATOR.join([self.slug, self.uid, extension]) chango-0.6.0/src/chango/_utils/files.py000066400000000000000000000040361507376567100200010ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT import subprocess from pathlib import Path UTF8 = "utf-8" class _GitHelper: # Alternatives to using subprocess would be using pygit2 or dulwich. # However, that would add rather heavy dependencies for a very small part of this library. # Let's keep it in mind for a possible future improvement. def __init__(self) -> None: self.git_available: bool | None = None @staticmethod def _git_move(src: Path, dst: Path) -> None: subprocess.check_call(["git", "mv", str(src), str(dst)]) @staticmethod def _git_add(path: Path) -> None: """Add a file to the git index.""" subprocess.check_call(["git", "add", str(path)]) @staticmethod def _pathlib_move(src: Path, dst: Path) -> None: src.rename(dst) def move(self, src: Path, dst: Path) -> None: if self.git_available is None: # We use try-except instead of first checking if git is available # because that way we can avoid calling git twice. try: self._git_move(src, dst) self.git_available = True except subprocess.CalledProcessError: self.git_available = False self._pathlib_move(src, dst) elif self.git_available: self._git_move(src, dst) else: self._pathlib_move(src, dst) def add(self, path: Path) -> None: if self.git_available is False: return if self.git_available: self._git_add(path) else: try: self._git_add(path) self.git_available = True except subprocess.CalledProcessError: self.git_available = False _GIT_HELPER = _GitHelper() def move_file(src: Path, dst: Path) -> None: _GIT_HELPER.move(src, dst) def try_git_add(path: Path) -> None: """Add a file to the git index if git is available.""" _GIT_HELPER.add(path) chango-0.6.0/src/chango/_utils/strings.py000066400000000000000000000006111507376567100203630ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT def indent_multiline(text: str, indent: int = 2, newlines: int = 1) -> str: """Indent all lines of a multi-line string except the first one.""" return (newlines * "\n").join( line if i == 0 else " " * indent + line for i, line in enumerate(text.splitlines()) ) chango-0.6.0/src/chango/_utils/types.py000066400000000000000000000006071507376567100200430ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT from pathlib import Path from typing import TYPE_CHECKING, Union from .._version import Version if TYPE_CHECKING: from ..abc._changenote import ChangeNote VersionUID = str | None VUIDInput = Version | str | None CNUIDInput = Union["ChangeNote", str] PathLike = str | Path chango-0.6.0/src/chango/_version.py000066400000000000000000000012151507376567100172200ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT import datetime as dtm from dataclasses import dataclass @dataclass(frozen=True) class Version: """Objects of this type represent a released version of a software project. Args: uid (:obj:`str`): Unique identifier / version number of this version. date (:class:`datetime.date`): Release date of this version. Attributes: uid (:obj:`str`): Unique identifier / version number of this version. date (:class:`datetime.date`): Release date of this version. """ uid: str date: dtm.date chango-0.6.0/src/chango/abc/000077500000000000000000000000001507376567100155505ustar00rootroot00000000000000chango-0.6.0/src/chango/abc/__init__.py000066400000000000000000000007651507376567100176710ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT """This module contains abstract base classes defining the interfaces that the chango package is build on. """ __all__ = ["ChanGo", "ChangeNote", "VersionHistory", "VersionNote", "VersionScanner"] from ._changenote import ChangeNote from ._chango import ChanGo from ._versionhistory import VersionHistory from ._versionnote import VersionNote from ._versionscanner import VersionScanner chango-0.6.0/src/chango/abc/_changenote.py000066400000000000000000000175661507376567100204130ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT import abc from pathlib import Path from typing import Any, Self from .._utils.filename import FileName from .._utils.files import UTF8 from .._utils.types import PathLike from ..action import ChanGoActionData class ChangeNote(abc.ABC): """Abstract base class for a change note describing a single change in a software project. Args: slug (:obj:`str`): A short, human-readable identifier for the change note. uid (:obj:`str`): A unique identifier for the change note. If not provided, a random identifier will be generated. Should be 8 characters long and consist of lowercase letters and digits. """ def __init__(self, slug: str, uid: str | None = None): self._file_name = FileName(slug=slug, uid=uid) if uid else FileName(slug=slug) @property def slug(self) -> str: """:obj:`str`: The short, human-readable identifier for the change note.""" return self._file_name.slug @property def uid(self) -> str: """:obj:`str`: The unique identifier for the change note.""" return self._file_name.uid @property @abc.abstractmethod def file_extension(self) -> str: """:obj:`str`: The file extension to use when writing the change note to a file. The extension must *not* include the leading dot. """ def update_uid(self, uid: str) -> None: """Update the UID of the change note. Use with caution. Args: uid (:obj:`str`): The new UID to use. """ self._file_name = FileName(slug=self.slug, uid=uid) @classmethod @abc.abstractmethod def build_template(cls, slug: str, uid: str | None = None) -> Self: """Build a template change note for the concrete change note type. Tip: This will be used to create a new change note in the CLI. Args: slug (:obj:`str`): The slug to use for the change note. uid (:obj:`str`): The unique identifier for the change note or :obj:`None` to generate a random one. Returns: The :class:`ChangeNote` object. """ @classmethod def build_from_github_event( cls, event: dict[str, Any], data: dict[str, Any] | ChanGoActionData | None = None ) -> Self: """Build a change note from a GitHub event. Important: This is an optional method and by default raises a :class:`NotImplementedError`. Implement this method if you want to automatically create change notes based on GitHub events. Tip: This method is useful for automatically creating change note drafts in GitHub actions to ensure that each pull request has documented changes. .. seealso:: :ref:`action` Args: event (Dict[:obj:`str`, :obj:`~typing.Any`]): The GitHub event data. This should be one of the `events that trigger workflows `_. The event is represented as a JSON dictionary. data (Dict[:obj:`str`, :obj:`~typing.Any`] | :class:`chango.action.ChanGoActionData`, \ optional): Additional data that may be required to build the change note. Returns: :class:`CNT `: The change note or :obj:`None`. Raises: NotImplementedError: If the method is not implemented. .. _ettw: https://docs.github.com/en/actions/writing-workflows/\ choosing-when-your-workflow-runs/events-that-trigger-workflows """ raise NotImplementedError @property def file_name(self) -> str: """The file name to use when writing the change note to a file.""" return self._file_name.to_string(self.file_extension) @classmethod def from_file(cls, file_path: PathLike, encoding: str = UTF8) -> Self: """ Read a change note from the specified file. Tip: This convenience method calls :meth:`from_bytes` internally. Args: file_path (:class:`pathlib.Path` | :obj:`str`): The path to the file to read from. encoding (:obj:`str`): The encoding to use for reading. Returns: :class:`ChangeNote`: The :class:`ChangeNote` object. Raises: :class:`chango.error.ValidationError`: If the data is not a valid change note file. """ path = Path(file_path) file_name = FileName.from_string(path.name) return cls.from_bytes( slug=file_name.slug, uid=file_name.uid, data=path.read_bytes(), encoding=encoding ) @classmethod def from_bytes(cls, slug: str, uid: str, data: bytes, encoding: str = UTF8) -> Self: """ Read a change note from the specified byte data. The data will be the raw binary content of a change note file. Tip: This convenience method calls :meth:`from_string` internally. Args: slug (:obj:`str`): The slug of the change note. uid (:obj:`str`): The UID of the change note. data (:obj:`bytes`): The bytes to read from. encoding (:obj:`str`): The encoding to use for reading. Returns: :class:`ChangeNote`: The :class:`ChangeNote` object. Raises: :class:`chango.error.ValidationError`: If the data is not a valid change note file. """ return cls.from_string(slug=slug, uid=uid, string=data.decode(encoding)) @classmethod @abc.abstractmethod def from_string(cls, slug: str, uid: str, string: str) -> Self: """Read a change note from the specified string data. The implementation must be able to handle the case where the string is not a valid change note and raise an :exc:`~chango.error.ValidationError` in that case. Args: slug (:obj:`str`): The slug of the change note. uid (:obj:`str`): The UID of the change note. string (:obj:`str`): The string to read from. Returns: :class:`ChangeNote`: The :class:`ChangeNote` object. Raises: :class:`chango.error.ValidationError`: If the string is not a valid change note. """ def to_bytes(self, encoding: str = UTF8) -> bytes: """Write the change note to bytes. This binary data should be suitable for writing to a file and reading back in with :meth:`from_bytes`. Tip: This convenience method calls :meth:`to_string` internally. Args: encoding (:obj:`str`): The encoding to use. Returns: :obj:`bytes`: The bytes data. """ return self.to_string(encoding).encode(encoding) @abc.abstractmethod def to_string(self, encoding: str = UTF8) -> str: """Write the change note to a string. This string should be suitable for writing to a file and reading back in with :meth:`from_string`. Args: encoding (:obj:`str`): The encoding to use for writing. Returns: :obj:`str`: The string data. """ def to_file(self, directory: PathLike | None = None, encoding: str = UTF8) -> Path: """Write the change note to the directory. Hint: The file name will always be the :attr:`~chango.abc.ChangeNote.file_name`. Args: directory: Optional. The directory to write the file to. If not provided, the file will be written to the current working directory. encoding (:obj:`str`): The encoding to use for writing. Returns: :class:`pathlib.Path`: The path to the file that was written. """ path = Path(directory) if directory else Path.cwd() write_path = path / self.file_name write_path.write_bytes(self.to_bytes(encoding=encoding)) return write_path chango-0.6.0/src/chango/abc/_chango.py000066400000000000000000000234651507376567100175320ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT import abc from pathlib import Path from typing import TYPE_CHECKING, Any, Optional from .._utils.files import UTF8, move_file, try_git_add from .._utils.types import VUIDInput from ..action import ChanGoActionData from ._changenote import ChangeNote from ._versionhistory import VersionHistory from ._versionnote import VersionNote from ._versionscanner import VersionScanner if TYPE_CHECKING: from .. import Version class ChanGo[VST: VersionScanner, VHT: VersionHistory, VNT: VersionNote, CNT: ChangeNote](abc.ABC): """Abstract base class for loading :class:`~chango.abc.ChangeNote`, :class:`~chango.abc.VersionNote` and :class:`~chango.abc.VersionHistory` objects as well as writing :class:`~chango.abc.ChangeNote` objects. This class holds the main interface for interacting with the version history and change notes. """ @property @abc.abstractmethod def scanner(self) -> VST: """:class:`VST `: The :class:`~chango.abc.VersionScanner` used by this instance. """ @abc.abstractmethod def build_template_change_note(self, slug: str, uid: str | None = None) -> CNT: """Build a template change note for the concrete change note type. Tip: This will be used to create a new change note in the CLI. Args: slug (:obj:`str`): The slug to use for the change note. uid (:obj:`str`, optional): The unique identifier for the change note or :obj:`None` to generate a random one. Returns: :class:`CNT `:The :class:`~chango.abc.ChangeNote` object. """ @abc.abstractmethod def build_version_note(self, version: Optional["Version"]) -> VNT: """Build a new empty version note. Args: version (:class:`~chango.Version` | :obj:`None`): The version of the software project this note is for. May be :obj:`None` if the version is not yet released. Returns: :class:`VNT `: The :class:`~chango.abc.VersionNote` object. """ @abc.abstractmethod def build_version_history(self) -> VHT: """:class:`VHT `: Build a new empty version history.""" @abc.abstractmethod def load_change_note(self, uid: str) -> CNT: """Load a change note with the given identifier. Args: uid (:obj:`str`): The unique identifier or file name of the change note to load. Returns: :class:`CNT `: The :class:`~chango.abc.ChangeNote` object. Raises: ~chango.error.ChanGoError: If the change note with the given identifier is not available. """ @abc.abstractmethod def get_write_directory(self, change_note: CNT | str, version: VUIDInput) -> Path: """Determine the directory to write a change note to. Important: * It should be ensured that the directory exists. * The :paramref:`version` does *not* need to be already available. In that case, it's expected that :paramref:`version` is of type :class:`~chango.Version`. Args: change_note (:class:`CNT ` | :obj:`str`): The change note to write or its UID. version (:class:`~chango.Version` | :obj:`str` | :obj:`None`): The version the change note belongs to. Maybe be :obj:`None` if the change note is not yet released. Returns: :class:`pathlib.Path`: The directory to write the change note to. Raises: ~chango.error.ChanGoError: If the :paramref:`version` is a :obj:`str` but not yet available. """ def build_github_event_change_note( self, event: dict[str, Any], data: dict[str, Any] | ChanGoActionData | None = None ) -> CNT | None: """Build a change note from a GitHub event. Important: This is an optional method and by default raises a :class:`NotImplementedError`. Implement this method if you want to automatically create change notes based on GitHub events. Tip: This method is useful for automatically creating change note drafts in GitHub actions to ensure that each pull request has documented changes. .. seealso:: :ref:`action` Args: event (Dict[:obj:`str`, :obj:`~typing.Any`]): The GitHub event data. This should be one of the `events that trigger workflows `_. The event is represented as a JSON dictionary. data (Dict[:obj:`str`, :obj:`~typing.Any`] | :class:`chango.action.ChanGoActionData`, \ optional): Additional data that may be required to build the change note. Returns: :class:`CNT ` | :obj:`None`: The change note or :obj:`None` if no change note should be created (e.g., if a change note is already available) for the change. Raises: NotImplementedError: If the method is not implemented. .. _ettw: https://docs.github.com/en/actions/writing-workflows/\ choosing-when-your-workflow-runs/events-that-trigger-workflows """ raise NotImplementedError def write_change_note( self, change_note: CNT, version: VUIDInput, encoding: str = UTF8 ) -> Path: """Write a change note to disk. Important: The :paramref:`version` does *not* need to be already available. In that case, it's expected that :paramref:`version` is of type :class:`~chango.Version`. Tip: This method calls :meth:`chango.abc.VersionScanner.invalidate_caches` after writing the change note to disk. Args: change_note (:class:`CNT ` | :obj:`str`): The change note to write. version (:class:`~chango.Version` | :obj:`str` | :obj:`None`): The version the change note belongs to. Maybe be :obj:`None` if the change note is not yet released. encoding (:obj:`str`): The encoding to use for writing. Returns: :class:`pathlib.Path`: The file path the change note was written to. Raises: ~chango.error.ChanGoError: If the :paramref:`version` is a :obj:`str` but not yet available. """ path = change_note.to_file( directory=self.get_write_directory(change_note=change_note, version=version), encoding=encoding, ) try_git_add(path) self.scanner.invalidate_caches() return path def load_version_note(self, version: VUIDInput) -> VNT: """Load a version note. Args: version (:class:`~chango.Version` | :obj:`str` | :obj:`None`): The version of the version note to load or the corresponding uid. May be :obj:`None` if the version is not yet released. Returns: :class:`VNT `: The loaded :class:`~chango.abc.VersionNote`. Raises: ~chango.error.ChanGoError: If the version is not available. """ changes = self.scanner.get_changes(version) version_obj = self.scanner.get_version(version) if isinstance(version, str) else version version_note = self.build_version_note(version=version_obj) for change in changes: version_note.add_change_note(self.load_change_note(change)) return version_note def load_version_history(self, start_from: VUIDInput = None, end_at: VUIDInput = None) -> VHT: """Load the version history. Important: By default, unreleased changes are included in the returned version history, if available. Args: start_from (:class:`~chango.Version` | :obj:`str`, optional): The version to start from. If :obj:`None`, start from the earliest available version. end_at (:class:`~chango.Version` | :obj:`str`, optional): The version to end at. If :obj:`None`, end at the latest available version, *including* unreleased changes. Returns: :class:`VHT `: The loaded version :class:`~chango.abc.VersionHistory`. """ version_history = self.build_version_history() if not end_at and self.scanner.has_unreleased_changes(): version_history.add_version_note(self.load_version_note(None)) for version in self.scanner.get_available_versions(start_from=start_from, end_at=end_at): version_history.add_version_note(self.load_version_note(version)) return version_history def release(self, version: "Version") -> bool: """Release a version. This calls :meth:`get_write_directory` for all unreleased change notes and moves the file if necessary. Tip: This method calls :meth:`chango.abc.VersionScanner.invalidate_caches` after releasing the version. Args: version (:class:`~chango.Version`): The version to release. Returns: :obj:`bool`: Whether a release was performed. If no unreleased changes are available, this method returns :obj:`False`. """ if not self.scanner.has_unreleased_changes(): return False for uid in self.scanner.get_changes(None): change_info = self.scanner.lookup_change_note(uid) write_dir = self.get_write_directory(uid, version) if change_info.file_path.parent != write_dir: move_file(change_info.file_path, write_dir / change_info.file_path.name) self.scanner.invalidate_caches() return True chango-0.6.0/src/chango/abc/_versionhistory.py000066400000000000000000000061731507376567100213770ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT import abc import warnings from collections.abc import Iterator, MutableMapping from .._utils.types import VersionUID, VUIDInput from ..abc._versionnote import VersionNote from ..helpers import ensure_uid class VersionHistory[VNT: VersionNote](MutableMapping[VersionUID, VNT], abc.ABC): """Abstract base class for a version history describing the versions in a software project over several versions. Hint: Objects of this class can be used as :class:`~collections.abc.MutableMapping`, where the keys are the unique identifiers of the versions and the values are the version notes themselves. """ def __init__(self) -> None: self._version_notes: dict[VersionUID, VNT] = {} def __delitem__(self, key: VUIDInput, /) -> None: del self._version_notes[ensure_uid(key)] def __getitem__(self, key: VUIDInput, /) -> VNT: return self._version_notes[ensure_uid(key)] def __iter__(self) -> Iterator[VersionUID]: return iter(self._version_notes) def __len__(self) -> int: return len(self._version_notes) def __setitem__(self, key: VUIDInput, value: VNT, /) -> None: if ensure_uid(key) != value.uid: warnings.warn( f"Key {key!r} does not match version note UID {value.uid!r}. " "Using the version UID as key.", stacklevel=2, ) self._version_notes[value.uid] = value # type: ignore[index] def add_version_note(self, version_note: VNT) -> None: """Add a version note to the version note. Args: version_note (:class:`VNT `): The :class:`~chango.abc.VersionNote` note to add. """ self[version_note.uid] = version_note # type: ignore[index] def remove_version_note(self, version_note: VNT) -> None: """Remove a version note from the version note. Args: version_note (:class:`VNT `): The :class:`~chango.abc.VersionNote` note to remove. """ del self[version_note.uid] # type: ignore[arg-type] @abc.abstractmethod def render(self, markup: str) -> str: """Render the version note as a string. Must include information about all change notes contained in the version note. Hint: * Make use of :meth:`chango.abc.VersionNote.render` to render the change notes. * The change notes should be rendered in reverse chronological order. This needs to be handled by the implementation and can be achieved either by applying appropriate sorting the :attr:`~chango.abc.VersionNote.uid` or by sorting by :attr:`~chango.abc.VersionNote.date` if available. Args: markup (:obj:`str`): The markup language to use for rendering. If the markup language is not supported, an :exc:`~chango.error.UnsupportedMarkupError` should be raised. Returns: :obj:`str`: The rendered version note. """ chango-0.6.0/src/chango/abc/_versionnote.py000066400000000000000000000112211507376567100206310ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT import abc import datetime as dtm import warnings from collections.abc import Iterator, MutableMapping from typing import TYPE_CHECKING, overload from .._utils.filename import FileName from ..abc._changenote import ChangeNote from ..error import ValidationError if TYPE_CHECKING: from chango import Version class VersionNote[CNT: ChangeNote, V: (Version, None)](MutableMapping[str, CNT], abc.ABC): """Abstract base class for a version note describing the set of changes in a software project for a single version. Hint: Objects of this class can be used as :class:`~collections.abc.MutableMapping`, where the keys are the unique identifiers (or file names) of the change notes and the values are the change notes themselves. Warning: To ensure that the changes in this version are displayed in the correct order, the change notes should be added in the order they were made. Manual reordering of the change notes may interfere with the order in which they are displayed. Args: version (:class:`~chango.Version` | :obj:`None`): The version of the software project this note is for or May be :obj:`None` if the version is not yet released. Attributes: version: (:class:`~chango.Version` | :obj:`None`): The version of the software project this note is for or May be :obj:`None` if the version is not yet released. """ def __init__(self, version: V) -> None: self.version: V = version self._change_notes: dict[str, CNT] = {} def __delitem__(self, key: str, /) -> None: try: del self._change_notes[key] except KeyError: try: del self._change_notes[FileName.from_string(key).uid] except ValidationError: raise KeyError(key) from None def __getitem__(self, key: str, /) -> CNT: try: return self._change_notes[key] except KeyError: try: return self._change_notes[FileName.from_string(key).uid] except ValidationError: raise KeyError(key) from None def __iter__(self) -> Iterator[str]: return iter(self._change_notes) def __len__(self) -> int: return len(self._change_notes) def __setitem__(self, key: str, value: CNT, /) -> None: if key != value.uid: warnings.warn( f"Key {key!r} does not match change note UID {value.uid!r}. Using the UID as key.", stacklevel=2, ) self._change_notes[value.uid] = value @overload def uid(self: "VersionNote[CNT, Version]") -> str: ... @overload def uid(self: "VersionNote[CNT, None]") -> None: ... @property def uid(self) -> str | None: """Convenience property for the version UID. Returns: :obj:`str` | :obj:`None`: The UID of :attr:`version` if available, :obj:`None` otherwise. """ if self.version is None: return None return self.version.uid @overload def date(self: "VersionNote[CNT, Version]") -> dtm.date: ... @overload def date(self: "VersionNote[CNT, None]") -> None: ... @property def date(self) -> dtm.date | None: """Convenience property for the version UID. Returns: :class:`datetime.date` | :obj:`None`: The release date of :attr:`version` if available, :obj:`None` otherwise. """ if self.version is None: return None return self.version.date def add_change_note(self, change_note: CNT) -> None: """Add a change note to the version note. Args: change_note (:class:`CNT `): The :class:`~chango.abc.ChangeNote` note to add. """ self[change_note.uid] = change_note def remove_change_note(self, change_note: CNT) -> None: """Remove a change note from the version note. Args: change_note (:class:`CNT `): The :class:`~chango.abc.ChangeNote` note to remove. """ del self[change_note.uid] @abc.abstractmethod def render(self, markup: str) -> str: """Render the version note as a string. Args: markup (:obj:`str`): The markup language to use for rendering. If the markup language is not supported, an :exc:`~chango.error.UnsupportedMarkupError` should be raised. Returns: :obj:`str`: The rendered version note. """ chango-0.6.0/src/chango/abc/_versionscanner.py000066400000000000000000000132311507376567100213200ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT import abc from collections.abc import Collection, Iterator from .._changenoteinfo import ChangeNoteInfo from .._utils.types import VUIDInput from .._version import Version from ..error import ChanGoError class VersionScanner(Collection[Version]): """Abstract base class for a version scanner that can list available versions. Hint: Objects of this class can be used as :class:`~collections.abc.Collection` of versions as returned by the :meth:`get_available_versions` method. """ def __contains__(self, x: object, /) -> bool: if x is None: return self.has_unreleased_changes() if not isinstance(x, str | Version): return False return self.is_available(x) def __iter__(self) -> Iterator[Version]: return iter(self.get_available_versions()) def __len__(self) -> int: return len(self.get_available_versions()) @abc.abstractmethod def is_available(self, uid: VUIDInput) -> bool: """Check if the version with the given identifier is available. Tip: :obj:`None` may be passed for convenience, but it's recommended that an implementation calls :meth:`has_unreleased_changes` internally. Args: uid (:class:`~chango.Version` | :obj:`str` | :obj:`None`): The version identifier to check. """ @abc.abstractmethod def has_unreleased_changes(self) -> bool: """Check if there are changes in the repository that are not yet released in a version. Returns: :obj:`bool`: :obj:`True` if there are unreleased changes, :obj:`False` otherwise. """ @abc.abstractmethod def get_latest_version(self) -> Version: """Get the latest version Returns: :class:`~chango.Version`: The latest version Raises: ~chango.error.ChanGoError: If no versions are available. """ @abc.abstractmethod def get_available_versions( self, start_from: VUIDInput = None, end_at: VUIDInput = None ) -> tuple[Version, ...]: """Get the available versions. Important: Unreleased changes must *not* be included in the returned version identifiers. Args: start_from (:class:`~chango.Version` | :obj:`str`, optional): The version identifier to start from. If :obj:`None`, start from the earliest available version. end_at (:class:`~chango.Version` | :obj:`str`, optional): The version identifier to end at. If :obj:`None`, end at the latest available version, *excluding* unreleased changes. Returns: Tuple[:class:`~chango.Version`]: The available versions. """ @abc.abstractmethod def lookup_change_note(self, uid: str) -> ChangeNoteInfo: """Lookup a change note with the given identifier. Args: uid (:obj:`str`): The unique identifier or file name of the change note to lookup Returns: :class:`chango.ChangeNoteInfo`: The metadata about the change note specifying the file path and version it belongs to. Raises: ~chango.error.ChanGoError: If the change note with the given identifier is not available. """ def get_version(self, uid: str) -> Version: """Get the version with the given identifier. Hint: The default implementation calls :meth:`get_available_versions`. Implementations may override this method to provide a more efficient way to get the version. Args: uid (:obj:`str`): The version identifier to get the version for. Returns: :class:`~chango.Version`: The version. Raises: ~chango.error.ChanGoError: If the version with the given identifier is not available. """ try: return next(version for version in self if version.uid == uid) except StopIteration as exc: raise ChanGoError(f"Version '{uid}' not available.") from exc @abc.abstractmethod def get_changes(self, uid: VUIDInput) -> tuple[str, ...]: """Get the changes either for a given version identifier or all available. Hint: To easily extract the UIDs from the change files, :meth:`chango.helpers.change_uid_from_file` can be used. Important: The returned UIDs must be in the order in which the changes were made. Args: uid (:class:`~chango.Version` | :obj:`str` | :obj:`None`): The version identifier to get the change files for. If :obj:`None`, get the change files for unreleased changes must be returned. Returns: Tuple[:obj:`str`]: UIDs of the changes corresponding to the version identifier. Raises: ~chango.error.ChanGoError: If the version with the given identifier is not available. """ def invalidate_caches(self) -> None: """Invalidate any internal caches that may be used by the implementation. Important: * This method is not required to do anything if the implementation does not use any caches. By default, it does nothing. * This method is called by :meth:`chango.abc.ChanGo.release` and :meth:`chango.abc.ChanGo.write_change_note` after the respective operation has been completed. This gives the implementation the opportunity to clear any caches that may have been affected by the operation. """ chango-0.6.0/src/chango/action.py000066400000000000000000000063331507376567100166570ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT """This module contains functionality required when using chango in :ref:`action`.""" from typing import ClassVar, Literal from pydantic import AnyHttpUrl, BaseModel, ConfigDict __all__ = ["ChanGoActionData", "LinkedIssue", "ParentPullRequest"] class _FrozenModel(BaseModel): model_config: ClassVar[ConfigDict] = ConfigDict(frozen=True) class ParentPullRequest(_FrozenModel): """Data structure for a pull request associated with the target branch of the current pull request. Args: number (:obj:`int`): The pull request number. title (:obj:`str`): The title of the pull request. url (:obj:`str`): The URL of the pull request. state (:obj:`str`): The state of the pull request. Possible values are ``open``, ``closed``, and ``merged``. Attributes: number (:obj:`int`): The pull request number. author_login (:obj:`str`): The login of the author of the pull request. title (:obj:`str`): The title of the pull request. url (:obj:`str`): The URL of the pull request. state (:obj:`str`): The state of the pull request. Possible values are ``OPEN``, ``CLOSED``, and ``MERGED``. """ number: int author_login: str title: str url: AnyHttpUrl state: Literal["OPEN", "CLOSED", "MERGED"] class LinkedIssue(_FrozenModel): """Data structure for an issue linked in a GitHub pull request. Args: number (:obj:`int`): The issue number. title (:obj:`str`): The title of the issue. labels (tuple[:obj:`str`], optional): The labels of the issue. issue_type (:obj:`str`, optional): The type of the issue. Attributes: number (:obj:`int`): The issue number. title (:obj:`str`): The title of the issue. labels (tuple[:obj:`str`]): Optional. The labels of the issue. issue_type (:obj:`str`): Optional. The type of the issue. """ number: int title: str labels: tuple[str, ...] | None issue_type: str | None = None class ChanGoActionData(_FrozenModel): """Data structure for the additional information that the ``chango`` action automatically provides in addition to the GitHub event payload. Args: parent_pull_request (:class:`ParentPullRequest` | :obj:`None`): If there is a pull request associated with the target branch of the current pull request, this field contains information about it. linked_issues (tuple[:class:`LinkedIssue`], optional): Information about linked issues, i.e., issues that will be closed when the current pull request is merged. Attributes: parent_pull_request (:class:`ParentPullRequest`): Optional. If there is a pull request associated with the target branch of the current pull request, this field contains information about it. linked_issues (tuple[:class:`LinkedIssue`]): Optional. Information about linked issues, i.e., issues that will be closed when the current pull request is merged. """ parent_pull_request: ParentPullRequest | None linked_issues: tuple[LinkedIssue, ...] | None chango-0.6.0/src/chango/concrete/000077500000000000000000000000001507376567100166255ustar00rootroot00000000000000chango-0.6.0/src/chango/concrete/__init__.py000066400000000000000000000016371507376567100207450ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT """This module contains implementations of the interface classes defined in the :mod:`~chango.abc` module that are shipped with this package.""" __all__ = [ "BackwardCompatibleChanGo", "BackwardCompatibleVersionScanner", "CommentChangeNote", "CommentVersionNote", "DirectoryChanGo", "DirectoryVersionScanner", "HeaderVersionHistory", "sections", ] from . import sections from ._backwardcompatiblechango import BackwardCompatibleChanGo from ._backwardcompatibleversionscanner import BackwardCompatibleVersionScanner from ._commentchangenote import CommentChangeNote from ._commentversionnote import CommentVersionNote from ._directorychango import DirectoryChanGo from ._directoryversionscanner import DirectoryVersionScanner from ._headerversionhistory import HeaderVersionHistory chango-0.6.0/src/chango/concrete/_backwardcompatiblechango.py000066400000000000000000000107251507376567100243410ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT import contextlib from collections.abc import Collection from pathlib import Path from typing import TYPE_CHECKING, Any, Optional, override from .._utils.types import VUIDInput from ..abc import ChangeNote, ChanGo, VersionHistory, VersionNote from ..action import ChanGoActionData from ..error import ChanGoError from ._backwardcompatibleversionscanner import BackwardCompatibleVersionScanner if TYPE_CHECKING: from .. import Version class BackwardCompatibleChanGo[VHT: VersionHistory, VNT: VersionNote, CNT: ChangeNote]( ChanGo["BackwardCompatibleVersionScanner", VHT, VNT, CNT] ): """An Implementation of the :class:`~chango.abc.ChanGo` interface that wraps multiple other implementations of :class:`~chango.abc.ChanGo`. The purpose of this class is to ease transition between different version note formats in a project. Args: main_instance(:class:`~chango.abc.ChanGo`): The :class:`~chango.abc.ChanGo` instance that should be used for new version notes. legacy_instances(Collection[:class:`~chango.abc.ChanGo`]): A collection of :class:`~chango.abc.ChanGo` instances that should be used for loading older version notes. """ def __init__( self, main_instance: "ChanGo[Any,VHT, VNT, CNT]", legacy_instances: Collection[ChanGo[Any, Any, Any, Any]], ): self._main_instance = main_instance self._legacy_instances = tuple(legacy_instances) self._scanner = BackwardCompatibleVersionScanner( (main_instance.scanner, *(chango.scanner for chango in self._legacy_instances)) ) @property @override def scanner(self) -> "BackwardCompatibleVersionScanner": """The :class:`~chango.concrete.BackwardCompatibleVersionScanner` instance that is used by this :class:`BackwardCompatibleChanGo`. Hint: The scanner is a composite of the scanners of :paramref:`~BackwardCompatibleChanGo.main_instance` and :paramref:`~BackwardCompatibleChanGo.legacy_instance`. """ return self._scanner @override def build_template_change_note(self, slug: str, uid: str | None = None) -> CNT: """Calls :meth:`~chango.abc.ChanGo.build_template_change_note` on :paramref:`~BackwardCompatibleChanGo.main_instance`. """ return self._main_instance.build_template_change_note(slug, uid) @override def build_version_note(self, version: Optional["Version"]) -> VNT: """Calls :meth:`~chango.abc.ChanGo.build_version_note` on :paramref:`~BackwardCompatibleChanGo.main_instance` or one of the legacy instances depending on the result of :meth:`~chango.abc.VersionScanner.is_available`. """ for chango in (self._main_instance, *self._legacy_instances): if chango.scanner.is_available(version): return chango.build_version_note(version) raise ChanGoError(f"Version {version} not found") @override def build_version_history(self) -> VHT: """Calls :meth:`~chango.abc.ChanGo.build_version_history` on :paramref:`~BackwardCompatibleChanGo.main_instance`. """ return self._main_instance.build_version_history() @override def load_change_note(self, uid: str) -> CNT: """Load a change note with the given identifier. Tries to load the change note from the main chango first and then from the legacy changos. """ for chango in (self._main_instance, *self._legacy_instances): with contextlib.suppress(ChanGoError): return chango.load_change_note(uid) raise ChanGoError(f"Change note with uid {uid} not found") @override def get_write_directory(self, change_note: CNT | str, version: VUIDInput) -> Path: """Calls :meth:`~chango.abc.ChanGo.get_write_directory` on :paramref:`~BackwardCompatibleChanGo.main_instance`. """ return self._main_instance.get_write_directory(change_note, version) def build_github_event_change_note( self, event: dict[str, Any], data: dict[str, Any] | ChanGoActionData | None = None ) -> CNT | None: """Calls :meth:`~chango.abc.ChanGo.build_github_event_change_note` on :paramref:`~BackwardCompatibleChanGo.main_instance`. """ return self._main_instance.build_github_event_change_note(event, data) chango-0.6.0/src/chango/concrete/_backwardcompatibleversionscanner.py000066400000000000000000000070231507376567100261360ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT # # SPDX-License-Identifier: MIT # # SPDX-License-Identifier: MIT import contextlib from collections.abc import Collection from typing import override from .._changenoteinfo import ChangeNoteInfo from .._utils.types import VUIDInput from .._version import Version from ..abc import VersionScanner from ..error import ChanGoError class BackwardCompatibleVersionScanner(VersionScanner): """An Implementation of the :class:`~chango.abc.VersionScanner` interface that wraps multiple other implementations of :class:`~chango.abc.VersionScanner`. The purpose of this class is to ease transition between different version note formats in a project. Warning: This assumes that the versions available for each of the scanners are mutually exclusive, i.e. no two scanners can return the same version. Tip: Use together with :class:`~chango.concrete.BackwardCompatibleChanGo`. Args: scanners (Collection[:class:`~chango.abc.VersionScanner`]): The scanners to wrap. """ def __init__(self, scanners: Collection[VersionScanner]): self._scanners = tuple(scanners) @override def is_available(self, uid: VUIDInput) -> bool: return any(scanner.is_available(uid) for scanner in self._scanners) @override def has_unreleased_changes(self) -> bool: return any(scanner.has_unreleased_changes() for scanner in self._scanners) @override def get_latest_version(self) -> Version: """Implementation of :meth:`chango.abc.VersionScanner.get_latest_version`. Important: The newest version is determined by the date of the version, not the order in which the scanners were passed to the constructor. Returns: :class:`~chango.Version`: The latest version """ versions = [] for scanner in self._scanners: with contextlib.suppress(ChanGoError): versions.append(scanner.get_latest_version()) if not versions: raise ChanGoError("No versions available.") return max(versions, key=lambda v: v.date) @override def get_available_versions( self, start_from: VUIDInput = None, end_at: VUIDInput = None ) -> tuple[Version, ...]: return tuple( version for scanner in self._scanners for version in scanner.get_available_versions(start_from, end_at) ) @override def lookup_change_note(self, uid: str) -> ChangeNoteInfo: """Lookup a change note with the given identifier. Args: uid (:obj:`str`): The unique identifier or file name of the change note to lookup Returns: :class:`chango.ChangeNoteInfo`: The metadata about the change note specifying the file path and version it belongs to. """ for scanner in self._scanners: with contextlib.suppress(ChanGoError): return scanner.lookup_change_note(uid) raise ChanGoError(f"Change note '{uid}' not available.") @override def get_changes(self, uid: VUIDInput) -> tuple[str, ...]: for scanner in self._scanners: with contextlib.suppress(ChanGoError): return scanner.get_changes(uid) raise ChanGoError(f"Version '{uid}' not available.") @override def invalidate_caches(self) -> None: for scanner in self._scanners: scanner.invalidate_caches() chango-0.6.0/src/chango/concrete/_commentchangenote.py000066400000000000000000000062231507376567100230370ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT from typing import Any, ClassVar, Self, override from .._utils.files import UTF8 from ..abc import ChangeNote from ..action import ChanGoActionData from ..constants import MarkupLanguage class CommentChangeNote(ChangeNote): """A simple change note that consists of a single comment. May be multi-line. Args: comment (:obj:`str`): The comment text. Attributes: comment (:obj:`str`): The comment text. """ MARKUP: ClassVar[str] = MarkupLanguage.TEXT """:obj:`str`: The markup language used in the comment. Will also be used as file extension. """ @override def __init__(self, slug: str, comment: str, uid: str | None = None): super().__init__(slug=slug, uid=uid) self.comment: str = comment @property @override def file_extension(self) -> str: return self.MARKUP @classmethod @override def from_string(cls, slug: str, uid: str, string: str) -> Self: return cls(slug=slug, comment=string, uid=uid) @override def to_string(self, encoding: str = UTF8) -> str: return self.comment @classmethod @override def build_template(cls, slug: str, uid: str | None = None) -> Self: return cls(slug=slug, comment="example comment", uid=uid) @classmethod @override def build_from_github_event( cls, event: dict[str, Any], data: dict[str, Any] | ChanGoActionData | None = None ) -> Self: """Implementation of :meth:`~chango.abc.ChangeNote.build_from_github_event`. Considers only events of type ``pull_request`` and ``pull_request_target``. Uses the pull request number as slug and the pull request title as comment. Currently only supports :attr:`~chango.constants.MarkupLanguage.TEXT`, :attr:`~chango.constants.MarkupLanguage.MARKDOWN`, :attr:`~chango.constants.MarkupLanguage.RESTRUCTUREDTEXT` and :attr:`~chango.constants.MarkupLanguage.HTML`. Caution: Does not consider any formatting in the pull request title! Raises: ValueError: If required data is missing or not in the expected format or if :attr:`MARKUP` is not supported. """ try: pull_request = event["pull_request"] pr_number = pull_request["number"] html_url = pull_request["html_url"] except KeyError as exc: raise ValueError("Unable to extract required data from event.") from exc match cls.MARKUP: case MarkupLanguage.TEXT: link = f"({html_url})" case MarkupLanguage.MARKDOWN: link = f"([#{pr_number}]({html_url}))" case MarkupLanguage.RESTRUCTUREDTEXT: link = f"(`#{pr_number} <{html_url}>`_)" case MarkupLanguage.HTML: link = f'(#{pr_number})' case _: raise ValueError(f"Unsupported markup language: {cls.MARKUP}") return cls(slug=f"{pr_number:04}", comment=f"{pull_request['title']} {link}") chango-0.6.0/src/chango/concrete/_commentversionnote.py000066400000000000000000000043251507376567100233000ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT from typing import TYPE_CHECKING, override from .._utils.strings import indent_multiline from ..abc import VersionNote from ..concrete import CommentChangeNote from ..constants import MarkupLanguage from ..error import UnsupportedMarkupError if TYPE_CHECKING: from chango import Version class CommentVersionNote[V: (Version, None)](VersionNote[CommentChangeNote, V]): """A simple version note implementation that works with :class:`~chango.concrete.CommentChangeNote`. """ @override def render(self, markup: str) -> str: """Render the version note to a string by listing all contained change notes separated by a newline. For markup languages Markdown, HTML and reStructuredText, the change notes will be rendered as unordered lists. Args: markup (:obj:`str`): The markup language to use for rendering. Raises: :exc:`~chango.error.UnsupportedMarkupError`: If the ``markup`` parameter does not coincide with :attr:`chango.concrete.CommentChangeNote.MARKUP` Returns: :obj:`str`: The rendered version note. """ try: markup = MarkupLanguage.from_string(markup) except ValueError as exc: raise UnsupportedMarkupError(markup) from exc match markup: case MarkupLanguage.MARKDOWN: return "\n".join( f"- {indent_multiline(note.comment, indent=4, newlines=2)}" for note in self.values() ) case MarkupLanguage.HTML: return ( "
    \n" + "\n".join( f"
  • {note.comment.replace('\n', '
    ')}
  • " for note in self.values() ) + "\n
" ) case MarkupLanguage.RESTRUCTUREDTEXT: return "\n".join( f"- {indent_multiline(note.comment, newlines=2)}" for note in self.values() ) case _: return "\n\n".join(note.comment for note in self.values()) chango-0.6.0/src/chango/concrete/_directorychango.py000066400000000000000000000205201507376567100225210ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT from pathlib import Path from typing import TYPE_CHECKING, Any, Optional, override from .._utils.types import VUIDInput from ..abc import ChangeNote, ChanGo, VersionHistory, VersionNote from ..action import ChanGoActionData from ..error import ChanGoError from ._directoryversionscanner import DirectoryVersionScanner from .sections import SectionChangeNote, SectionVersionNote if TYPE_CHECKING: from chango import Version class DirectoryChanGo[VHT: VersionHistory, VNT: VersionNote, CNT: ChangeNote]( ChanGo[DirectoryVersionScanner, VHT, VNT, CNT] ): """Implementation of the :class:`~chango.abc.ChanGo` interface that works with :class:`~chango.concrete.DirectoryVersionScanner` and assumes that change notes are stored in subdirectories named after the version identifier. Args: change_note_type (:class:`type`): The type of change notes to load. Must be a subclass of :class:`~chango.abc.ChangeNote`. version_note_type (:class:`type`): The type of version notes to load. Must be a subclass of :class:`~chango.abc.VersionNote`. version_history_type (:class:`type`): The type of version histories to load. Must be a subclass of :class:`~chango.abc.VersionHistory`. scanner (:class:`~chango.concrete.DirectoryVersionScanner`): The version scanner to use. directory_format (:obj:`str`, optional): Reverse of :paramref:`~chango.concrete.DirectoryVersionScannerdirectory_pattern`. Must be a string that can be used with :meth:`str.format` and contain at least one named field ``uid`` for the version identifier and optionally a second named field ``date`` for the date of the version release in ISO format. The default value is compatible with the default value of :paramref:`~chango.concrete.DirectoryVersionScannerdirectory_pattern`. Attributes: directory_format (:obj:`str`): The format string used to create version directories. """ def __init__( self: "DirectoryChanGo[VHT, VNT, CNT]", change_note_type: type[CNT], version_note_type: type[VNT], version_history_type: type[VHT], scanner: DirectoryVersionScanner, directory_format: str = "{uid}_{date}", ): self._scanner: DirectoryVersionScanner = scanner self.directory_format: str = directory_format self.change_note_type: type[CNT] = change_note_type self.version_note_type: type[VNT] = version_note_type self.version_history_type: type[VHT] = version_history_type @property @override def scanner(self) -> DirectoryVersionScanner: return self._scanner @override def build_template_change_note(self, slug: str, uid: str | None = None) -> CNT: return self.change_note_type.build_template(slug=slug, uid=uid) @override def build_version_note(self, version: Optional["Version"]) -> VNT: """Implementation of :meth:`~chango.abc.ChanGo.build_version_note`. Includes special handling for :class:`~chango.concrete.sections.SectionVersionNote`, which has the required argument :paramref:`~chango.concrete.sections.SectionVersionNote.section_change_note_type`. """ if issubclass(self.version_note_type, SectionVersionNote): return self.version_note_type( section_change_note_type=self.change_note_type, version=version ) return self.version_note_type(version=version) @override def build_version_history(self) -> VHT: return self.version_history_type() @override def load_change_note(self, uid: str) -> CNT: return self.change_note_type.from_file(self.scanner.lookup_change_note(uid).file_path) @override def get_write_directory(self, change_note: CNT | str, version: VUIDInput) -> Path: if version is None: directory = self.scanner.unreleased_directory else: if isinstance(version, str): try: version_obj = self.scanner.get_version(version) except ChanGoError as exc: raise ChanGoError( f"Version '{version}' not available yet. To get the write directory for a " "new version, pass the version as `change.Version` object." ) from exc else: version_obj = version directory = self.scanner.base_directory / self.directory_format.format( uid=version_obj.uid, date=version_obj.date.isoformat() ) directory.mkdir(parents=True, exist_ok=True) return directory @override def build_github_event_change_note( self, event: dict[str, Any], data: dict[str, Any] | ChanGoActionData | None = None ) -> CNT | None: """Implementation of :meth:`~chango.abc.ChanGo.build_github_event_change_note`. Important: By default, this will always call :meth:`chango.abc.ChangeNote.build_from_github_event` and does not check if a new change note is necessary. The only exception is when :paramref:`~DirectoryChanGo.change_note_type` is a subclass of :class:`~chango.concrete.sections.SectionChangeNote`: * If there already is a change note for the current pull request, it is updated with the new information. If nothing changed, returns :obj:`None`. * If the ``data`` parameter is an instance of :class:`~chango.action.ChanGoActionData` with a parent pull request, then this method will try to find an existing *unreleased* change note for the parent pull request and append the new information to it. """ change_note = self.change_note_type.build_from_github_event(event=event, data=data) if not isinstance(change_note, SectionChangeNote): return change_note # Special handling for SectionChangeNote orig_data = change_note.to_bytes() was_modified = False existing_change_notes = self.load_version_note(None).values() # First check if we can override any existing change notes pr_ids = [pr.uid for pr in change_note.pull_requests] for existing_change_note in existing_change_notes: if not any( uid in pr_ids for uid in (pr.uid for pr in existing_change_note.pull_requests) ): continue change_note.update_uid(existing_change_note.uid) orig_data = existing_change_note.to_bytes() was_modified = True # Handle Parent PRs if not isinstance(data, ChanGoActionData) or not data.parent_pull_request: if was_modified and (orig_data == change_note.to_bytes()): return None return change_note parent_pr = data.parent_pull_request # load all unreleased change notes and find the one for the parent pull request for existing_change_note in existing_change_notes: if str(parent_pr.number) not in (pr.uid for pr in existing_change_note.pull_requests): continue # Combine the PRs on existing and new change notes. Override with new PRs if necessary existing_prs = {pr.uid: pr for pr in existing_change_note.pull_requests} existing_prs.update({pr.uid: pr for pr in change_note.pull_requests}) existing_change_note.pull_requests = tuple(existing_prs.values()) for section_name in change_note.SECTIONS: if not (new_value := getattr(change_note, section_name)): continue if not (existing_value := getattr(existing_change_note, section_name)): setattr(existing_change_note, section_name, new_value) else: setattr(existing_change_note, section_name, f"{existing_value}\n{new_value}") # change_note must have at least one section specified, so we now that here we have # at least one change that we report return existing_change_note # If we get here, then we didn't find an existing change note for the parent PR # This we need to return the new change note return change_note chango-0.6.0/src/chango/concrete/_directoryversionscanner.py000066400000000000000000000215321507376567100243250ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT import contextlib import datetime as dtm import inspect import itertools import re from pathlib import Path from typing import NamedTuple, override from .._changenoteinfo import ChangeNoteInfo from .._utils.filename import FileName from .._utils.types import PathLike, VUIDInput from .._version import Version from ..abc import VersionScanner from ..error import ChanGoError, ValidationError from ..helpers import ensure_uid _DEFAULT_PATTERN = re.compile(r"(?P[^_]+)_(?P[\d-]+)") class _VersionInfo(NamedTuple): date: dtm.date directory: Path class _FileInfo(NamedTuple): uid: str file: Path def _make_relative_to(base: Path, path: Path) -> Path: if path.is_absolute(): return path.resolve().absolute() return (base / path).resolve().absolute() class DirectoryVersionScanner(VersionScanner): """Implementation of a version scanner that assumes that change notes are stored in subdirectories named after the version identifier. Args: base_directory (:obj:`str` | :class:`~pathlib.Path`): The base directory to scan for version directories. Important: If the path is relative, it will be resolved relative to the directory of the calling module. .. admonition:: Example If you build your :class:`DirectoryVersionScanner` within ``/home/user/project/chango.py``, passing ``base_directory="changes"`` will resolve to ``/home/user/project/changes``. unreleased_directory (:obj:`str` | :class:`~pathlib.Path`): The directory that contains unreleased changes. Important: If :meth:`pathlib.Path.is_dir` returns :obj:`False` for this directory, it will be assumed to be a subdirectory of the :paramref:`base_directory`. directory_pattern (:obj:`str` | :obj:`re.Pattern`, optional): The pattern to match version directories against. Must contain one named group ``uid`` for the version identifier and a second named group for the ``date`` for the date of the version release in ISO format. Attributes: base_directory (:class:`~pathlib.Path`): The base directory to scan for version directories. directory_pattern (:obj:`re.Pattern`): The pattern to match version directories against. unreleased_directory (:class:`~pathlib.Path`): The directory that contains unreleased changes. """ def __init__( self, base_directory: PathLike, unreleased_directory: PathLike, directory_pattern: str | re.Pattern[str] = _DEFAULT_PATTERN, ): self.directory_pattern: re.Pattern[str] = re.compile(directory_pattern) caller_dir = Path(inspect.stack()[1].filename).resolve().absolute().parent self.base_directory: Path = _make_relative_to(caller_dir, Path(base_directory)) if not self.base_directory.is_dir(): raise ValueError(f"Base directory '{self.base_directory}' does not exist.") if (path := Path(unreleased_directory)).is_dir(): self.unreleased_directory: Path = path.resolve().absolute() else: self.unreleased_directory = self.base_directory / unreleased_directory if not self.unreleased_directory.is_dir(): raise ValueError( f"Unreleased directory '{self.unreleased_directory}' does not exist." ) self.__available_versions: dict[str, _VersionInfo] | None = None @property def _available_versions(self) -> dict[str, _VersionInfo]: # Simple Cache for the available versions if self.__available_versions is not None: return self.__available_versions self.__available_versions = {} for directory in self.base_directory.iterdir(): if not directory.is_dir() or not ( match := self.directory_pattern.match(directory.name) ): continue uid = match.group("uid") date = dtm.date.fromisoformat(match.group("date")) self.__available_versions[uid] = _VersionInfo(date, directory) return self.__available_versions def _get_available_version(self, uid: str) -> Version: try: return Version(uid=uid, date=self._available_versions[uid].date) except KeyError as exc: raise ChanGoError(f"Version '{uid}' not available.") from exc def invalidate_caches(self) -> None: self.__available_versions = None @override def is_available(self, uid: VUIDInput) -> bool: if uid is None: return self.has_unreleased_changes() if (version := self._available_versions.get(ensure_uid(uid))) is None: return False if isinstance(uid, Version): return version.date == uid.date return True @override def has_unreleased_changes(self) -> bool: """Implementation of :meth:`chango.abc.VersionScanner.has_unreleased_changes`. Checks if :attr:`unreleased_directory` contains any files. Returns: :obj:`bool`: :obj:`True` if there are unreleased changes, :obj:`False` otherwise. """ return bool(self._get_file_names(None)) @override def get_latest_version(self) -> Version: """Implementation of :meth:`chango.abc.VersionScanner.get_latest_version`. Important: In case of multiple releases on the same day, lexicographical comparison of the version identifiers is employed. Returns: :class:`~chango.Version`: The latest version """ if not self._available_versions: raise ChanGoError("No versions available.") return self._get_available_version( max( self._available_versions, key=lambda uid: (self._available_versions[uid].date, uid) ) ) @override def get_available_versions( self, start_from: VUIDInput = None, end_at: VUIDInput = None ) -> tuple[Version, ...]: """Implementation of :meth:`chango.abc.VersionScanner.get_available_versions`. Important: Limiting the version range by :paramref:`~chango.abc.VersionScanner.get_available_versions.start_from` and :paramref:`~chango.abc.VersionScanner.get_available_versions.end_at` is based on lexicographical comparison of the version identifiers. Returns: Tuple[:class:`~chango.Version`]: The available versions within the specified range. """ start = ensure_uid(start_from) end = ensure_uid(end_at) return tuple( self._get_available_version(uid) for uid in self._available_versions if (start is None or uid >= start) and (end is None or uid <= end) ) def _get_file_names(self, uid: VUIDInput) -> tuple[_FileInfo, ...]: try: directory = ( self._available_versions[ensure_uid(uid)].directory if uid else self.unreleased_directory ) except KeyError as exc: raise ChanGoError(f"Version '{uid}' not available.") from exc out = [] # Sorting is an undocumented implementation detail for now! for change in sorted(directory.iterdir()): if not change.is_file(): continue with contextlib.suppress(ValidationError): name = FileName.from_string(change.name) out.append(_FileInfo(name.uid, change)) return tuple(out) @override def lookup_change_note(self, uid: str) -> ChangeNoteInfo: try: version, file_name = next( ( self._get_available_version(version_uid) if version_uid else None, file_info.file.name, ) for version_uid in itertools.chain(self._available_versions, (None,)) for file_info in self._get_file_names(version_uid) if uid == file_info.uid ) directory = ( self._available_versions[version.uid].directory if version else self.unreleased_directory ) except StopIteration as exc: raise ChanGoError(f"Change note '{uid}' not found in any version.") from exc return ChangeNoteInfo(uid, version, directory / file_name) @override def get_version(self, uid: str) -> Version: return self._get_available_version(uid) @override def get_changes(self, uid: VUIDInput) -> tuple[str, ...]: return tuple(file_info.uid for file_info in self._get_file_names(uid)) chango-0.6.0/src/chango/concrete/_headerversionhistory.py000066400000000000000000000054341507376567100236240ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT import string from typing import override from ..abc import VersionHistory, VersionNote from ..constants import MarkupLanguage from ..error import UnsupportedMarkupError class HeaderVersionHistory[VNT: VersionNote](VersionHistory[VNT]): """A simple version history implementation that renders version notes by prefixing them with the version UID as header, followed by the release date if available. """ @override def render(self, markup: str) -> str: """Does the rendering. Tip: Version notes are automatically sorted by release date before rendering. If unreleased changes are present, they are rendered first. Important: Currently, only Markdown, HTML and reStructuredText are supported as markup languages. Args: markup (:obj:`str`): The markup language to use for rendering. Returns: :obj:`str`: The rendered version history. Raises: :exc:`~chango.error.UnsupportedMarkupError`: If the ``markup`` parameter does not coincide with :attr:`~chango.constants.MarkupLanguage.MARKDOWN`, :attr:`~chango.constants.MarkupLanguage.HTML`, or :attr:`~chango.constants.MarkupLanguage.RESTRUCTUREDTEXT` """ released_notes = list(filter(lambda note: note.version, self.values())) changes = sorted( released_notes, key=lambda note: note.date, # type: ignore[arg-type,return-value] reverse=True, ) match markup: case MarkupLanguage.MARKDOWN: tpl_str = "# $uid\n*$date*\n\n$comment" case MarkupLanguage.HTML: tpl_str = "

$uid

\n$date\n\n$comment" case MarkupLanguage.RESTRUCTUREDTEXT: tpl_str = "$uid\n$rst_underline\n*$date*\n\n$comment" case _: raise UnsupportedMarkupError( f"Got unsupported markup '{markup}', can only render Markdown, HTML, " f"and reStructuredText" ) if None in self: changes.insert(0, self[None]) template = string.Template(tpl_str) return "\n\n".join( template.substitute( uid=note.uid or "Unreleased", # type: ignore[truthy-function] rst_underline="=" * len(note.uid or "Unreleased"), # type: ignore[truthy-function,arg-type] date=( "unknown" if (note.date is None) else note.date.isoformat() # type: ignore[attr-defined] ), comment=note.render(markup), ) for note in changes ) chango-0.6.0/src/chango/concrete/sections/000077500000000000000000000000001507376567100204545ustar00rootroot00000000000000chango-0.6.0/src/chango/concrete/sections/__init__.py000066400000000000000000000026111507376567100225650ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT """This module contains an implementation of :class:`~chango.abc.ChangeNote` that consists of multiple sections and includes references to pull requests that are related to the change. The main class is :class:`SectionChangeNote`, while :class:`Section` and :class:`PullRequest` are used to define the sections and pull requests, respectively. Example: To create a change note with two sections, one required and one optional, use .. code-block:: python from chango.concrete.sections import GitHubSectionChangeNote, Section class MySectionChangeNote( GitHubSectionChangeNote.with_sections( [ Section(uid="required_section", title="Required Section", is_required=True), Section(uid="optional_section", title="Optional Section"), ] ) ): OWNER = "my-username" REPOSITORY = "my-repo" """ __all__ = [ "GitHubSectionChangeNote", "PullRequest", "Section", "SectionChangeNote", "SectionVersionNote", ] from ._githubsectionchangenote import GitHubSectionChangeNote from ._pullrequest import PullRequest from ._section import Section from ._sectionchangenote import SectionChangeNote from ._sectionversionnote import SectionVersionNote chango-0.6.0/src/chango/concrete/sections/_githubsectionchangenote.py000066400000000000000000000144571507376567100261030ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT # # SPDX-License-Identifier: MIT from collections.abc import Collection from typing import Any, ClassVar, Self, override from ...action import ChanGoActionData from ._pullrequest import PullRequest from ._sectionchangenote import SectionChangeNote class GitHubSectionChangeNote(SectionChangeNote): """Specialization of :class:`~chango.concrete.sections.SectionChangeNote` for projects hosted on GitHub. Example: .. code-block:: python from chango.concrete.sections import GitHubSectionChangeNote, Section class MySectionChangeNote( GitHubSectionChangeNote.with_sections( [ Section(uid="req_section", title="Required Section", is_required=True), Section(uid="opt_section", title="Optional Section"), ] ) ): OWNER = "my-username" REPOSITORY = "my-repo" """ OWNER: ClassVar[str | None] = None """:obj:`str`: The owner of the repository on GitHub. This must be set as a class variable.""" REPOSITORY: ClassVar[str | None] = None """:obj:`str`: The name of the repository on GitHub. This must be set as a class variable.""" @classmethod def _get_owner(cls) -> str: if cls.OWNER is None: raise ValueError("OWNER must be set as class variable.") return cls.OWNER @classmethod def _get_repository(cls) -> str: if cls.REPOSITORY is None: raise ValueError("REPOSITORY must be set as class variable.") return cls.REPOSITORY @classmethod @override def get_pull_request_url(cls, pr_uid: str) -> str: """Implementation of :meth:`SectionChangeNote.get_pull_request_url` based on :attr:`OWNER` and :attr:`REPOSITORY`. """ return f"https://github.com/{cls._get_owner()}/{cls._get_repository()}/pull/{pr_uid}" @classmethod @override def get_thread_url(cls, thread_uid: str) -> str: """Implementation of :meth:`SectionChangeNote.get_pull_request_url` based on :attr:`OWNER` and :attr:`REPOSITORY`. """ return f"https://github.com/{cls._get_owner()}/{cls._get_repository()}/issues/{thread_uid}" @classmethod @override def get_author_url(cls, author_uid: str) -> str: """Get the URL of an author with the given UID. Args: author_uid (:obj:`str`): The UID of an author as defined in :attr:`chango.concrete.sections.PullRequest.author_uids`. Returns: :obj:`str`: The URL of the author. """ return f"https://github.com/{author_uid}" @classmethod def get_sections( cls, labels: Collection[str], # noqa: ARG003 issue_types: Collection[str] | None, # noqa: ARG003 ) -> set[str]: """Determine appropriate sections based on the labels of a pull request as well as the labels and types of the issues closed by the pull request. If this class has required sections, they are all returned. Otherwise, the first section in the order of :attr:`~chango.concrete.sections.Section.sort_order` is returned. Tip: This method can be overridden to provide custom logic for determining the section based on the labels and issue types. Args: labels (Collection[:obj:`str`]): The combined set of labels of the pull request and the issues closed by the pull request. issue_types (Collection[:obj:`str`]): The types of the issues closed by the pull request. Caution: Since issue types are currently in `public preview `_, this set may be empty. Returns: Set[:obj:`str`]: The UIDs of the sections. """ sorted_sections = sorted( (section for section in cls.SECTIONS.values()), key=lambda section: section.sort_order ) required_sections = {section.uid for section in sorted_sections if section.is_required} return required_sections or {sorted_sections[0].uid} @classmethod @override def build_from_github_event( cls, event: dict[str, Any], data: dict[str, Any] | ChanGoActionData | None = None ) -> Self: """Implementation of :meth:`chango.abc.ChangeNote.build_from_github_event`. This writes the pull request title to the sections determined by :meth:`get_sections`. Uses the pull request number as slug. Caution: * Does not consider any formatting in the pull request title! * Considers the ``data`` argument only if it is an instance of :class:`~chango.action.ChanGoActionData`. Raises: ValueError: If required data is missing or not in the expected format. """ try: pull_request = event["pull_request"] pr_number = pull_request["number"] pr_title = pull_request["title"] pr_labels = {label["name"] for label in pull_request.get("labels", [])} author_uid = pull_request["user"]["login"] except (KeyError, TypeError) as exc: raise ValueError("Unable to extract required data from event.") from exc issue_types: set[str] = set() closes_threads: set[int] = set() labels = pr_labels if isinstance(data, ChanGoActionData) and data.linked_issues: for issue in data.linked_issues: closes_threads.add(issue.number) if issue.labels: labels.update(issue.labels) if issue.issue_type: issue_types.add(issue.issue_type) sections = cls.get_sections(labels, issue_types) return cls( slug=f"{pr_number:04}", # type: ignore[call-arg] pull_requests=( PullRequest( uid=str(pr_number), author_uids=(author_uid,), closes_threads=tuple(map(str, closes_threads)), ), ), **dict.fromkeys(sections, pr_title), ) chango-0.6.0/src/chango/concrete/sections/_pullrequest.py000066400000000000000000000040511507376567100235520ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT from typing import Annotated, Any import pydantic as pydt from pydantic import BeforeValidator def _validate_author_uid(value: str | tuple[str, ...]) -> tuple[str, ...]: if isinstance(value, str): return (value,) return value class PullRequest(pydt.BaseModel): """Simple data structure to represent a pull/merge request. Args: uid (:obj:`str`): The unique identifier for the pull request. For example, the pull request number. author_uids (:obj:`str` | tuple[:obj:`str`, ...]): The unique identifier of the author(s) of the pull request. For example, the author's username. closes_threads (tuple[:obj:`str`], optional): The threads that are closed by this pull request. Attributes: uid (:obj:`str`): The unique identifier for the pull request. author_uids (tuple[:obj:`str`, ...]): The unique identifier of the author(s) of the pull request. closes_threads (tuple[:obj:`str`]): The threads that are closed by this pull request. May be empty. """ uid: str author_uids: Annotated[tuple[str, ...], BeforeValidator(_validate_author_uid)] closes_threads: tuple[str, ...] = pydt.Field(default_factory=tuple) @pydt.model_validator(mode="before") def unify_author_ids(cls, data: Any) -> Any: # for backwards compatibility, we allow both `author_uid` and `author_uids` if not isinstance(data, dict): # This cause is here only due to pydantics documentation example. # in practice, this should never happen. return data # pragma: no cover author_uid = data.pop("author_uid", None) author_uids = data.pop("author_uids", None) if author_uid is not None and author_uids is not None: raise ValueError("author_uid and author_uids are mutually exclusive") data["author_uids"] = author_uid or author_uids return data chango-0.6.0/src/chango/concrete/sections/_section.py000066400000000000000000000030671507376567100226370ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT from typing import ClassVar import pydantic as pydt class Section(pydt.BaseModel): """Configuration for a section in a :class:`SectionChangeNote`. Args: uid (:obj:`str`): The unique identifier for the section. This is used as the field name in the change note. title (:obj:`str`): The title of the section. is_required (:obj:`bool`, optional): Whether the section is required. Defaults to :obj:`False`. Tip: At least one section must be required. render_pr_details (:obj:`bool`, optional): Whether to include details about the pull requests related to the change in the rendering for this section. Defaults to :obj:`True`. sort_order (:obj:`int`, optional): The sort order of the section. Defaults to ``0``. Attributes: uid (:obj:`str`): The unique identifier for the section. title (:obj:`str`): The title of the section. is_required (:obj:`bool`): Whether the section is required. render_pr_details (:obj:`bool`, optional): Whether to include details about the pull requests related to the change in the rendering for this section. sort_order (:obj:`int`): The sort order of the section. """ model_config: ClassVar[pydt.ConfigDict] = pydt.ConfigDict(frozen=True) uid: str title: str is_required: bool = False render_pr_details: bool = True sort_order: int = 0 chango-0.6.0/src/chango/concrete/sections/_sectionchangenote.py000066400000000000000000000202751507376567100246730ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT # # SPDX-License-Identifier: MIT import abc import tomllib from collections.abc import Collection from typing import Any, ClassVar, Final, Self, override import pydantic as pydt import tomlkit from chango._utils.files import UTF8 from chango.abc import ChangeNote from chango.concrete.sections._pullrequest import PullRequest from chango.concrete.sections._section import Section from chango.constants import MarkupLanguage from chango.error import ValidationError class SectionChangeNote(pydt.BaseModel, ChangeNote, abc.ABC): """A change note that consists of multiple sections and includes references to pull requests that are related to the change. Uses the `toml `_ format for specifying the content. Important: * This class does not contain any specified sections by default and must not be instantiated directly. Use :meth:`with_sections` to create a suitable subclass with the desired sections. * Even though this class is in the :mod:`~chango.concrete` module, it is still an abstract base class and must be subclassed to be used. However, only the methods :meth:`get_pull_request_url`, :meth:`get_thread_url`, and :meth:`get_author_url` must be implemented in the subclass. A concrete subclass is provided in :class:`~chango.concrete.sections.GitHubSectionChangeNote`. Args: pull_requests (tuple[:class:`PullRequest`], optional): The pull requests that are related to the change. Attributes: pull_requests (tuple[:class:`~chango.concrete.sections.PullRequest`]): The pull requests that are related to the change """ MARKUP: ClassVar[str] = MarkupLanguage.RESTRUCTUREDTEXT """:obj:`str`: The markup language used in the sections. """ SECTIONS: Final[dict[str, Section]] = pydt.Field(default_factory=dict) """dict[:obj:`str`, :class:`Section`]: The sections of the change note. Maps the UID of the section to the :class:`Section` object containing the configuration for the section """ _BUILT_BY_WITH_SECTIONS: Final[bool] = pydt.PrivateAttr(default=False) """Internal attribute to check if the class was built by the :meth:`with_sections` class method. """ pull_requests: tuple[PullRequest, ...] = pydt.Field(default_factory=tuple) def __init__(self, slug: str, *args: Any, uid: str | None = None, **kwargs: Any) -> None: # Mixing pydantic with non-pydantic base classes is a bit tricky. # Unfortunately, we have to call the __init__ methods of both classes manually and also # don't get an overly nice signature. However, this class should rarely be instantiated # directly, so this should be acceptable. super().__init__(*args, **kwargs) ChangeNote.__init__(self, slug=slug, uid=uid) if not any(getattr(self, name) for name in self.SECTIONS): raise ValidationError("At least one section must be specified") @pydt.model_validator(mode="after") def _validate_section_configuration(self) -> Self: # Runs after the pydantic validation. Adds some additional checks in case someone is # trying to manually build subclasses of SectionChangeNote. if not self._BUILT_BY_WITH_SECTIONS: raise TypeError( "SectionChangeNote must not be subclassed manually. Please use" "SectionChangeNote.with_sections to create a suitable subclass." ) return self @classmethod def with_sections(cls, sections: Collection[Section], name: str | None = None) -> type[Self]: """Create a new subclass of :class:`SectionChangeNote` with the given sections. Args: sections (Collection[:class:`Section`]): The sections to include in the change note. Tip: All sections may be optional, but at least one section must be specified on instantiation. That is, a change note without content in any section is not allowed. name (:obj:`str`, optional): The name of the new class. Defaults to ``DynamicSectionChangeNote``. Returns: type[:class:`SectionChangeNote`]: The new subclass of :class:`SectionChangeNote`. """ # This also covers the case of `sections` being an empty collection if not sections: raise ValueError("Class must have at least one section") config_fields = { section.uid: (str, pydt.Field(...)) if section.is_required else (str | None, None) for section in sections } doc_insert = ", ".join(f'"{x}"' for x in (section.title for section in sections)) dynamic_model = pydt.create_model( # type: ignore[call-overload] name or "DynamicSectionChangeNote", __base__=cls, __doc__=f"SectionChangeNote with sections {doc_insert}", **config_fields, ) dynamic_model.SECTIONS = {section.uid: section for section in sections} dynamic_model._BUILT_BY_WITH_SECTIONS = True return dynamic_model @property @override def file_extension(self) -> str: return "toml" @classmethod @override def from_string(cls, slug: str, uid: str, string: str) -> Self: """Implementation of :meth:`~chango.abc.ChangeNote.from_string`. Args: slug (:obj:`str`): The slug of the change note. uid (:obj:`str`): The UID of the change note. string (:obj:`str`): The ``toml`` string to read from. Returns: :class:`~chango.abc.ChangeNote`: The :class:`~chango.abc.ChangeNote` object. Raises: :class:`chango.error.ValidationError`: If the string is not a valid change note. """ try: return cls(slug=slug, uid=uid, **tomllib.loads(string)) except (tomllib.TOMLDecodeError, pydt.ValidationError) as exc: raise ValidationError(f"Invalid TOML data: {exc}") from exc @override def to_string(self, encoding: str = UTF8) -> str: return tomlkit.dumps( {key: value for key, value in self.model_dump().items() if value is not None} ) @classmethod @override def build_template(cls, slug: str, uid: str | None = None) -> Self: required_sections = { field_name: "Required Section Content" if field_info.is_required() else "Optional Section Content" for field_name, field_info in cls.model_fields.items() if field_name != "pull_requests" } pull_requests = ( PullRequest( uid="pr-number-1", closes_threads=("thread1", "thread2"), author_uids=("author1",) ), PullRequest(uid="pr-number-2", closes_threads=("thread3",), author_uids=("author2",)), ) return cls(slug=slug, uid=uid, pull_requests=pull_requests, **required_sections) @classmethod @abc.abstractmethod def get_pull_request_url(cls, pr_uid: str) -> str: """Get the URL of the pull request with the given UID. Args: pr_uid (:obj:`str`): The UID of the pull request as defined in :attr:`chango.concrete.sections.PullRequest.uid`. Returns: :obj:`str`: The URL of the pull request. """ @classmethod @abc.abstractmethod def get_thread_url(cls, thread_uid: str) -> str: """Get the URL of the thread with the given UID. Args: thread_uid (:obj:`str`): The UID of the thread as defined in :attr:`chango.concrete.sections.PullRequest.closes_threads`. Returns: :obj:`str`: The URL of the thread. """ @classmethod @abc.abstractmethod def get_author_url(cls, author_uid: str) -> str: """Get the URL of an author with the given UID. Args: author_uid (:obj:`str`): The UID of an author as defined in :attr:`chango.concrete.sections.PullRequest.author_uids`. Returns: :obj:`str`: The URL of the author. """ chango-0.6.0/src/chango/concrete/sections/_sectionversionnote.py000066400000000000000000000113001507376567100251200ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT from collections import defaultdict from typing import TYPE_CHECKING, override from ..._utils.strings import indent_multiline from ...abc import VersionNote from ...constants import MarkupLanguage from ...error import UnsupportedMarkupError from ._pullrequest import PullRequest from ._sectionchangenote import SectionChangeNote if TYPE_CHECKING: from chango import Version class SectionVersionNote[V: (Version, None), SCN: SectionChangeNote](VersionNote[SCN, V]): """An implementation of :class:`~chango.abc.VersionNote` that works with :class:`~chango.concrete.sections.SectionChangeNote`. Important: Currently, only :attr:`~chango.constants.MarkupLanguage.RESTRUCTUREDTEXT` is supported. Args: section_change_note_type (\ type[:class:`~chango.concrete.sections.SectionChangeNote`]): The type of the section change note to use. Hint: It will not be possible to add change notes of a different type to this version note. """ def __init__(self, version: V, section_change_note_type: type[SCN]) -> None: super().__init__(version) # type: ignore[arg-type] if section_change_note_type.MARKUP != MarkupLanguage.RESTRUCTUREDTEXT: raise UnsupportedMarkupError( "This version note currently only supports reStructuredText markup." ) self._section_change_note_type = section_change_note_type self._sorted_sections = dict( sorted(section_change_note_type.SECTIONS.items(), key=lambda x: x[1].sort_order) ) @override def __setitem__(self, key: str, value: SCN, /) -> None: if not isinstance(value, self._section_change_note_type): raise TypeError( f"Expected a {self._section_change_note_type} instance, got {type(value)}" ) super().__setitem__(key, value) def _render_pr(self, pr: PullRequest) -> str: pr_url = self._section_change_note_type.get_pull_request_url(pr.uid) author_links = [ f"`@{author_uid} <{self._section_change_note_type.get_author_url(author_uid)}>`_" for author_uid in pr.author_uids ] thread_links = [ f"`#{thread_uid} <{self._section_change_note_type.get_thread_url(thread_uid)}>`_" for thread_uid in pr.closes_threads ] base = f"`#{pr.uid} <{pr_url}>`_ by {', '.join(author_links)}" if not thread_links: return base return f"{base} closes {', '.join(thread_links)}" def _render_section_entry( self, content: str, pull_requests: tuple[PullRequest, ...] | None = None ) -> str: indented_content = f"- {indent_multiline(content, newlines=2)}" if not pull_requests: return indented_content pr_details = "; ".join(self._render_pr(pr) for pr in pull_requests) if "\n" not in content: return f"{indented_content} ({pr_details})" return f"{indented_content}\n\n ({pr_details})" @override def render(self, markup: str) -> str: """Render the version note to a string by listing all contained change notes. Aggregates the content of all change notes for each section and renders them in the order defined by :attr:`~chango.concrete.sections.Section.sort_order`. Important: Currently, only :attr:`~chango.constants.MarkupLanguage.RESTRUCTUREDTEXT` is supported. """ try: markup = MarkupLanguage.from_string(markup) except ValueError as exc: raise UnsupportedMarkupError(markup) from exc if markup != MarkupLanguage.RESTRUCTUREDTEXT: raise UnsupportedMarkupError(markup) section_contents: dict[str, str] = defaultdict(str) for change_note in self.values(): for section_uid, section in self._sorted_sections.items(): if section_content := getattr(change_note, section_uid): section_contents[section_uid] = "\n".join( ( section_contents[section_uid], self._render_section_entry( section_content, change_note.pull_requests if section.render_pr_details else None, ), ) ) return "\n\n".join( f"{section.title}\n{'-' * len(section.title)}\n{content}" for uid, section in self._sorted_sections.items() if (content := section_contents[uid]) ) chango-0.6.0/src/chango/config/000077500000000000000000000000001507376567100162705ustar00rootroot00000000000000chango-0.6.0/src/chango/config/__init__.py000066400000000000000000000021501507376567100203770ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT """This module provides the functionality to load the configuration for the ChanGo CLI.""" __all__ = ["ChanGoConfig", "ChanGoInstanceConfig", "get_chango_instance"] import functools from typing import Any from chango.abc import ChanGo from .._utils.types import PathLike from ._models import ChanGoConfig, ChanGoInstanceConfig @functools.lru_cache(maxsize=256) def get_chango_instance(path: PathLike | None = None) -> ChanGo[Any, Any, Any, Any]: """Get the :class:`~chango.abc.ChanGo` instance specified in the configuration file. Uses LRU caching to avoid reloading the configuration file multiple times. Args: path (:class:`~pathlib.Path` | :obj:`str` | :obj:`None`, optional): The path to the configuration file as passed to :meth:`ChanGoConfig.load`. Returns: :class:`~chango.abc.ChanGo`: The instance of the :class:`~chango.abc.ChanGo` class specified in the configuration file. """ return ChanGoConfig.load(path).import_chango_instance() chango-0.6.0/src/chango/config/_models.py000066400000000000000000000135061507376567100202710ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT import importlib from pathlib import Path from typing import Annotated, Any, ClassVar, Self, cast from pydantic import Field from pydantic_settings import SettingsConfigDict from chango.abc import ChanGo from .._utils.config import FrozenModel, TomlSettings, add_sys_path, get_pyproject_toml_path from .._utils.types import PathLike __all__ = ["ChanGoConfig", "ChanGoInstanceConfig"] class ChanGoInstanceConfig(FrozenModel): """Data structure for specifying how the :class:`~chango.abc.ChanGo` should be imported for the CLI. Args: name (:obj:`str`): The name of the object to import. module (:obj:`str`): The module to import the object from as passed to :func:`importlib.import_module`. package (:obj:`str` | :obj:`None`, optional): The module to import the object from as passed to :func:`importlib.import_module`. Attributes: name (:obj:`str`): The name of the object to import. module (:obj:`str`): The module to import the object from as passed to :func:`importlib.import_module`. package (:obj:`str` | :obj:`None`): The module to import the object from as passed to :func:`importlib.import_module`. """ name: str module: Annotated[str, Field(examples=["my_config_module"])] package: str | None = None class ChanGoConfig(FrozenModel, TomlSettings): """Data structure for the ChanGos CLI configuration in the ``pyproject.toml`` file. Tip: Rather than manually creating an instance of this class, use :meth:`load` to load the configuration from the ``pyproject.toml`` file. Important: The attributes of :attr:`chango_instance` will be passed to :func:`importlib.import_module` to import the user defined :class:`~chango.abc.ChanGo` instance. For this to work, the module must be findable by Python, which may depend on your current working directory and the Python path. It can help to set :paramref:`sys_path` accordingly. Please evaluate the security implications of this before setting it. Keyword Args: sys_path (:class:`~pathlib.Path`, optional): A path to *temporarily* add to the system path before importing the module. Example: To add the current working directory to the system path, set this to ``.``. Caution: Since this class is usually loaded via :meth:`load`, the path is resolved relative to the ``pyproject.toml`` file path. If the path is absolute, it will be used as is. When instantiating this class manually, the path is resolved relative to the current working directory. chango_instance (:class:`~chango.config.ChanGoInstanceConfig`): Specification of how the :class:`~chango.abc.ChanGo` instance to use in the CLI is imported. Attributes: sys_path (:class:`~pathlib.Path` | None): The path to *temporarily* add to the system path before importing the module. If the path is not absolute, it will considered as relative to the current working directory. chango_instance (:class:`~chango.config.ChanGoInstanceConfig`): The instance of :class:`~chango.abc.ChanGo` to use in the CLI. """ model_config: ClassVar[SettingsConfigDict] = SettingsConfigDict( pyproject_toml_table_header=("tool", "chango"), extra="ignore" ) sys_path: Path | None = None chango_instance: Annotated[ ChanGoInstanceConfig, Field(examples=[ChanGoInstanceConfig(name="chango_instance", module="my_config_module")]), ] @classmethod def load(cls, path: PathLike | None = None) -> Self: """Load the :class:`~chango.config.ChanGoConfig` from the ``pyproject.toml`` file. Tip: If the specification of :attr:`sys_path` is relative, it will be resolved relative to the :paramref:`path` parameter by this method. Keyword Args: path (:class:`~pathlib.Path` | None): The path to the ``pyproject.toml`` file. The path resolution works as follows: * If ``path`` is ``None``, the current working directory is used. * If ``path`` is absolute, it is used as is. Relative paths are resolved relative to the current working directory. * If the path does not point to a file, it is assumed to be a directory and the file name ``pyproject.toml`` is appended. Returns: :class:`~chango.config.ChanGoConfig`: The loaded configuration. """ pyproject_toml_path = get_pyproject_toml_path(path) with cls._with_path(get_pyproject_toml_path(path)): obj = cls() # type: ignore[call-arg] if obj.sys_path is None or obj.sys_path.is_absolute(): return obj with obj._unfrozen(): obj.sys_path = (pyproject_toml_path.parent / obj.sys_path).resolve() return obj def import_chango_instance(self) -> ChanGo[Any, Any, Any, Any]: """Import the :class:`~chango.abc.ChanGo` instance specified in :attr:`chango_instance`. This considers the :attr:`sys_path` attribute to temporarily add a path to the system path. Returns: :class:`~chango.abc.ChanGo`: The imported :class:`~chango.abc.ChanGo` instance. """ with add_sys_path(self.sys_path): return cast( "ChanGo", getattr( importlib.import_module( self.chango_instance.module, self.chango_instance.package ), self.chango_instance.name, ), ) chango-0.6.0/src/chango/constants.py000066400000000000000000000060641507376567100174170ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT """This module contains constants used throughout the :mod:`chango` package.""" __all__ = ["MarkupLanguage"] import contextlib from collections.abc import Mapping from enum import StrEnum class MarkupLanguage(StrEnum): """Commonly known markup languages""" ASCIIDOC = "asciidoc" """The `AsciiDoc `_ markup language""" CREOLE = "creole" """The `Creole `_ markup language""" HTML = "html" """The `HyperText Markup Language `_""" MARKDOWN = "markdown" """The `Markdown `_ markup language""" MEDIAWIKI = "mediawiki" """The `MediaWiki `_ markup language""" ORG = "org" """The `Org-mode `_ markup language""" POD = "pod" """The `Plain Old Documentation `_ markup language""" RDOC = "rdoc" """The `RDoc `_ markup language""" RESTRUCTUREDTEXT = "rst" """The `reStructuredText `_ markup language""" TEXTILE = "textile" """The `Textile `_ markup language""" TEXT = "txt" """Plain text""" @classmethod def from_string( cls, string: str, mapping: Mapping[str, "MarkupLanguage"] | None = None ) -> "MarkupLanguage": """Get the markup language enum member from a string by comparing against the members of this enum as well as commonly used file extensions. Case-insensitive. Leading dots are ignored. Args: string (:obj:`str`): The string to look up. mapping (:class:`~collections.abc.Mapping` [:obj:`str`, :class:`MarkupLanguage`] | \ :obj:`None`): A mapping of file extensions to markup languages. If not provided, the default mapping will be used. Returns: :class:`MarkupLanguage`: The markup language enum member. Raises: ValueError: If the file extension can not be resolved to a markup language. """ lookup = string.lower().lstrip(".") with contextlib.suppress(ValueError): return cls(lookup) with contextlib.suppress(KeyError): return cls[lookup.upper()] effective_mapping = mapping or { "adoc": cls.ASCIIDOC, "htm": cls.HTML, "md": cls.MARKDOWN, "mkd": cls.MARKDOWN, "mdwn": cls.MARKDOWN, "mdown": cls.MARKDOWN, "mdtxt": cls.MARKDOWN, "mdtext": cls.MARKDOWN, "mediawiki": cls.MEDIAWIKI, "org": cls.ORG, "pod": cls.POD, "rdoc": cls.RDOC, "text": cls.TEXT, } with contextlib.suppress(KeyError): return effective_mapping[lookup] raise ValueError(f"File extension `{string}` not found in mapping.") chango-0.6.0/src/chango/error.py000066400000000000000000000011031507376567100165210ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT """This module contains error classes specific to the :mod:`chango` package.""" __all__ = ["ChanGoError", "UnsupportedMarkupError", "ValidationError"] class ChanGoError(Exception): """Base class for all exceptions defined by the chango package.""" class ValidationError(ChanGoError): """Exception raised when a validation error occurs.""" class UnsupportedMarkupError(ChanGoError): """Exception raised when an unsupported markup is encountered.""" chango-0.6.0/src/chango/helpers.py000066400000000000000000000032741507376567100170450ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT from pathlib import Path from typing import Protocol, overload from ._utils.filename import FileName from ._utils.types import PathLike __all__ = ["change_uid_from_file", "ensure_uid"] def change_uid_from_file(file: PathLike) -> str: """Get the change note identifier from a file name or path. Args: file (:obj:`str` | :class:`pathlib.Path`): The file name or path to get the identifier from. Returns: :obj:`str`: The :attr:`~chango.abc.ChangeNote.uid` of the change note. """ if isinstance(file, Path): return change_uid_from_file(file.name) return FileName.from_string(file).uid class _UIDProtocol(Protocol): uid: str class _UIDPropProtocol(Protocol): @property def uid(self) -> str: ... @overload def ensure_uid(obj: _UIDProtocol | _UIDPropProtocol) -> str: ... @overload def ensure_uid(obj: None) -> None: ... @overload def ensure_uid(obj: str) -> str: ... def ensure_uid(obj: _UIDProtocol | _UIDPropProtocol | str | None) -> str | None: """Extract the unique identifier of an object. Input of type :obj:`str` and :obj:`None` is returned unchanged. Args: obj (:obj:`str` | :obj:`None` | has ``uid``): An object that either has a string attribute ``uid`` (e.g. :class:`~chango.abc.ChangeNote` or :class:`~chango.Version`), is a :obj:`str` or is :obj:`None`. Returns: :obj:`str` | :obj:`None`: The extracted UID if available and :obj:`None` else. """ if obj is None: return None if isinstance(obj, str): return obj return obj.uid chango-0.6.0/src/chango/py.typed000066400000000000000000000000001507376567100165100ustar00rootroot00000000000000chango-0.6.0/src/chango/sphinx_ext/000077500000000000000000000000001507376567100172145ustar00rootroot00000000000000chango-0.6.0/src/chango/sphinx_ext/__init__.py000066400000000000000000000034101507376567100213230ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT """This module contains functionality that allows automatically rendering changelogs in `Sphinx `_ documentation using ``chango``. .. seealso:: :ref:`sphinx_ext` """ import typing from pathlib import Path from types import NoneType from sphinx.application import Sphinx from chango import __version__ __all__ = ["setup"] from ._util import directive_factory def setup(app: Sphinx) -> dict[str, typing.Any]: """Sets up the ``chango`` Sphinx extension. This currently does two things: 1. Adds the ``chango`` directive to Sphinx, which allows you to include changelogs in your documentation. 2. Adds a configuration value ``chango_pyproject_toml_path`` to the Sphinx configuration, which allows you to specify the path to the ``pyproject.toml`` file that contains the chango configuration. Args: app (:class:`sphinx.application.Sphinx`): The Sphinx application object. Returns: dict[:class:`str`, :class:`typing.Any`]: A dictionary containing metadata about the extension. """ app.add_config_value( "chango_pyproject_toml_path", None, rebuild="env", # Path & PurePath do not work well with how sphinx handles config value type checks types=(str, type(Path.cwd()), NoneType), description=( "Path to the pyproject.toml file to use for the chango configuration. Takes " "the same inputs as `chango.config.ChanGoConfig.load`." ), ) app.add_directive("chango", directive_factory(app)) return {"version": __version__, "parallel_read_safe": True, "parallel_write_safe": True} chango-0.6.0/src/chango/sphinx_ext/_util.py000066400000000000000000000100001507376567100206710ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT import json import typing from docutils.nodes import Node from sphinx.application import Sphinx from sphinx.util.docutils import SphinxDirective from chango._utils.types import PathLike from chango.config import get_chango_instance from chango.constants import MarkupLanguage class JsonValidator: """Validator that interprets the input as JSON data and loads it accordingly. The value must be a JSON-loadable value, not None. """ def __init__(self, option_name: str) -> None: self.option_name = option_name def __call__(self, var: str | None) -> str | int | float | bool | dict | list | None: if var is None: raise ValueError( f"Option '{self.option_name}' must be a JSON-loadable value, not None" ) try: return json.loads(var) except json.JSONDecodeError as exc: raise ValueError( f"Option '{self.option_name}' must be a JSON-loadable value, not {var!r}" ) from exc def __repr__(self) -> str: # pragma: no cover """So far only used for debugging.""" return "" def parse_function(func: typing.Callable) -> dict[str, typing.Callable[[str | None], typing.Any]]: """Parse a function's signature and annotations to create a dictionary of validators. Custom validators may be defined using the `typing.Annotated` type. Defaults are not - options are interpreted as kwargs and are always expected to carry a value. Example: >>> from typing import Annotated >>> from collections.abc import Sequence >>> >>> def custom_validator(x: str | None) -> Sequence[float]: ... return tuple(map(float, x.split(","))) if x is not None else (1.0, 2.0, 3.0) >>> >>> def foo( >>> arg1: str, >>> arg2: int = 42, >>> arg3: Annotated[Sequence[float], custom_validator] = (1, 2, 3), >>> ) -> None: ... pass >>> >>> parse_function(foo) {'arg1': , 'arg2': , \ 'arg3': .custom_validator at 0x000001FB6F1D7100>} """ # To get custom validator, we need to evaluate the annotations to detect `typing.Annotated` annotations = typing.get_type_hints(func, include_extras=True, localns=locals()) return { name: typing.get_args(annotation)[1] if typing.get_origin(annotation) is typing.Annotated else JsonValidator(name) for name, annotation in annotations.items() # The return value is not a parameter if name != "return" } def directive_factory(app: Sphinx) -> type[SphinxDirective]: """Create a directive class that uses the chango instance from the Sphinx app config. This approach is necessary because the `option_spec` attribute of a directive class can not be dynamically set. """ if not isinstance(app.config.chango_pyproject_toml_path, PathLike | None): raise TypeError( f"Expected 'chango_pyproject_toml_path' to be a string or Path, " f"but got {type(app.config.chango_pyproject_toml_path)}" ) chango_instance = get_chango_instance(app.config.chango_pyproject_toml_path) class ChangoDirective(SphinxDirective): has_content = True option_spec = parse_function( # type: ignore[assignment] chango_instance.load_version_history ) def run(self) -> list[Node]: title = " ".join(self.content) text = chango_instance.load_version_history(**self.options).render( MarkupLanguage.RESTRUCTUREDTEXT ) if title: decoration = len(title) * "=" text = f"{decoration}\n{title}\n{decoration}\n\n{text}" return self.parse_text_to_nodes(text, allow_section_headings=True) return ChangoDirective chango-0.6.0/tests/000077500000000000000000000000001507376567100141375ustar00rootroot00000000000000chango-0.6.0/tests/__init__.py000066400000000000000000000001561507376567100162520ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT chango-0.6.0/tests/abc/000077500000000000000000000000001507376567100146645ustar00rootroot00000000000000chango-0.6.0/tests/abc/__init__.py000066400000000000000000000001601507376567100167720ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT chango-0.6.0/tests/abc/test_changenote.py000066400000000000000000000126321507376567100204140ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT from pathlib import Path import pytest import shortuuid from chango.abc import ChangeNote from chango.concrete import CommentChangeNote from chango.error import ValidationError from tests.auxil.files import data_path UTF_8_PATH = data_path("comment-change-note.uid.txt") UTF_16_PATH = data_path("comment-change-note-utf16.uid.txt") @pytest.fixture(params=["string", "path"]) def utf_8_path(request): if request.param == "string": return str(UTF_8_PATH) return UTF_8_PATH @pytest.fixture(params=["string", "path"]) def utf_16_path(request): if request.param == "string": return str(UTF_16_PATH) return UTF_16_PATH class TestChangeNote: """Since ChangeNote is an abstract base class, we are testing with CommentChangeNote as a simple implementation. Note that we do *not* test abstract methods, as that is the responsibility of the concrete implementations. """ change_note = CommentChangeNote(slug="slug", comment="this is a comment", uid="uid") def test_init(self): assert self.change_note.slug == "slug" assert self.change_note.uid == "uid" change_note = CommentChangeNote(slug="slug", comment="this is a comment") assert change_note.slug == "slug" assert isinstance(change_note.uid, str) assert len(change_note.uid) == len(shortuuid.ShortUUID().uuid()) def test_init_invalid_slug(self): with pytest.raises(ValidationError, match="slug must not contain"): CommentChangeNote(slug="slug.with.dot", comment="this is a comment") def test_file_name(self): assert self.change_note.file_name == "slug.uid.txt" def test_from_file(self, utf_8_path): change_note = CommentChangeNote.from_file(utf_8_path) assert change_note.slug == "comment-change-note" assert change_note.uid == "uid" def test_from_file_encoding(self, utf_16_path): change_note = CommentChangeNote.from_file(utf_16_path, encoding="utf-16") assert change_note.slug == "comment-change-note-utf16" assert change_note.uid == "uid" assert change_note.comment == "this is an utf-16 comment ð›™ðŒ¢ð‘" with pytest.raises(UnicodeDecodeError): CommentChangeNote.from_file(utf_16_path, encoding="utf-8") def test_from_bytes(self): change_note = CommentChangeNote.from_bytes( slug="slug", uid="uid", data=UTF_8_PATH.read_bytes() ) assert change_note.slug == "slug" assert change_note.uid == "uid" assert change_note.comment == "this is a comment" def test_from_bytes_encoding(self): change_note = CommentChangeNote.from_bytes( slug="slug", uid="uid", data=UTF_16_PATH.read_bytes(), encoding="utf-16" ) assert change_note.slug == "slug" assert change_note.uid == "uid" assert change_note.comment == "this is an utf-16 comment ð›™ðŒ¢ð‘" with pytest.raises(UnicodeDecodeError): CommentChangeNote.from_bytes( slug="slug", uid="uid", data=UTF_16_PATH.read_bytes(), encoding="utf-8" ) def test_to_bytes(self): assert self.change_note.to_bytes() == b"this is a comment" def test_to_bytes_encoding(self): change_note = CommentChangeNote(slug="slug", comment="this is a comment ð›™ðŒ¢ð‘", uid="uid") assert change_note.to_bytes(encoding="utf-16") == "this is a comment ð›™ðŒ¢ð‘".encode("utf-16") @pytest.mark.parametrize("directory", [None, "custom"]) def test_to_file(self, tmp_path, directory): path = None expected_dir = tmp_path if directory == "custom" else Path.cwd() try: path = self.change_note.to_file(directory=tmp_path if directory == "custom" else None) assert path == expected_dir / "slug.uid.txt" assert path.read_text() == "this is a comment" finally: if path: path.unlink() @pytest.mark.parametrize("directory", [None, "custom"]) def test_to_file_encoding(self, tmp_path, directory): path = None expected_dir = tmp_path if directory == "custom" else Path.cwd() change_note = CommentChangeNote(slug="slug", comment="this is a comment ð›™ðŒ¢ð‘", uid="uid") try: path = change_note.to_file( directory=tmp_path if directory == "custom" else None, encoding="utf-16" ) assert path == expected_dir / "slug.uid.txt" assert path.read_text(encoding="utf-16") == "this is a comment ð›™ðŒ¢ð‘" with pytest.raises(UnicodeDecodeError): path.read_text(encoding="utf-8") finally: if path: path.unlink() def test_build_from_github_event(self, monkeypatch): monkeypatch.setattr( self.change_note, "build_from_github_event", ChangeNote.build_from_github_event ) with pytest.raises(NotImplementedError): self.change_note.build_from_github_event({}) def test_update_uid(self): change_note = CommentChangeNote(slug="slug", comment="this is a comment", uid="uid") assert change_note.uid == "uid" change_note.update_uid("abc") assert change_note.uid == "abc" assert change_note.slug == "slug" assert change_note.file_name == "slug.abc.txt" chango-0.6.0/tests/abc/test_chango.py000066400000000000000000000263271507376567100175460ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT import datetime as dtm import functools import shutil import subprocess from pathlib import Path import pytest import chango as chango_module from chango import Version from chango.abc import ChanGo from chango.concrete import ( CommentChangeNote, CommentVersionNote, DirectoryChanGo, DirectoryVersionScanner, HeaderVersionHistory, ) from chango.error import ChanGoError from chango.helpers import ensure_uid from tests.auxil.files import data_path @pytest.fixture def scanner() -> DirectoryVersionScanner: return DirectoryVersionScanner(TestChanGo.DATA_ROOT, "unreleased") @pytest.fixture def chango(scanner) -> DirectoryChanGo: return DirectoryChanGo( change_note_type=CommentChangeNote, version_note_type=CommentVersionNote, version_history_type=HeaderVersionHistory, scanner=scanner, ) @pytest.fixture def chango_no_unreleased() -> DirectoryChanGo: return DirectoryChanGo( change_note_type=CommentChangeNote, version_note_type=CommentVersionNote, version_history_type=HeaderVersionHistory, scanner=DirectoryVersionScanner(TestChanGo.DATA_ROOT, "no-unreleased"), ) @pytest.fixture def cache_invalidation_tracker(): class Tracker: def __init__(self): self.invalidate_caches = False self.super_call = None def __call__(self, *args, **kwargs): self.invalidate_caches = True if self.super_call: self.super_call(*args, **kwargs) @property def was_called(self): return self.invalidate_caches def set_super(self, super_call): self.super_call = super_call return self return Tracker() class TestChanGo: """Since Chango is an abstract base class, we are testing with DirectoryChanGo as a simple implementation. Note that we do *not* test abstract methods, as that is the responsibility of the concrete implementations. """ DATA_ROOT = data_path("directoryversionscanner") @pytest.mark.parametrize( "version", [ None, "1.1", Version("1.1", dtm.date(2024, 1, 1)), "1.2", Version("1.2", dtm.date(2024, 1, 2)), Version("new-version", dtm.date(2024, 1, 17)), ], ) @pytest.mark.parametrize("encoding", ["utf-8", "utf-16"]) @pytest.mark.parametrize( "has_git", [pytest.param(True, id="with-git"), pytest.param(False, id="without-git")] ) def test_write_change_note( self, chango, version, monkeypatch, encoding, cache_invalidation_tracker, has_git ): # Unfortunately, testing the git-available part is not easily possible without using # some of the internal utils and also not with directly running git. This is because # a) the availability of git is cached and there is no public interface to reset it # b) mocking subprocess before the module is imported is not easily possible # c) actually running `git add` is hard to reset # Since `chango._utils.files` is not part of the public API, we settle for testing # with the private interfaces. chango_module._utils.files._GIT_HELPER.git_available = None def check_call(args, *_, **__): assert args[:2] == ["git", "add"] if not has_git: raise subprocess.CalledProcessError(1, "git add") monkeypatch.setattr("chango._utils.files.subprocess.check_call", check_call) if version is None: expected_path = chango.scanner.unreleased_directory else: version_uid = ensure_uid(version) if version_uid == "new-version": expected_path = chango.scanner.base_directory / "new-version_2024-01-17" else: day = int(version_uid.split(".")[-1]) expected_path = chango.scanner.base_directory / f"{version_uid}_2024-01-0{day}" existed = expected_path.is_dir() def to_file(*_, **kwargs): assert kwargs.get("encoding") == encoding assert kwargs.get("directory") == expected_path note = chango.build_template_change_note("this-is-a-new-slug") monkeypatch.setattr(note, "to_file", to_file) monkeypatch.setattr(chango.scanner, "invalidate_caches", cache_invalidation_tracker) for _ in range(3): # run multiple times to cover all paths in _GIT_HELPER try: chango.write_change_note(note, version, encoding=encoding) assert cache_invalidation_tracker.was_called finally: if not existed and expected_path.is_dir(): shutil.rmtree(expected_path) def test_write_change_note_new_string_version(self, chango): note = chango.build_template_change_note("this-is-a-new-slug") with pytest.raises(ChanGoError, match="'new-version-uid' not available"): chango.write_change_note(note, "new-version-uid") def test_load_version_note_unavailable(self, chango): with pytest.raises(ChanGoError, match=r"Version '1.4' not available."): chango.load_version_note("1.4") @pytest.mark.parametrize( "version", [ None, "1.1", Version("1.1", dtm.date(2024, 1, 1)), "1.2", Version("1.2", dtm.date(2024, 1, 2)), ], ) def test_load_version_note(self, chango, version): version_note = chango.load_version_note(version) version_uid = ensure_uid(version) expected_uids = { f"uid_{(version_uid or 'ur').replace('.', '-')}_{idx}" for idx in range(3) } assert version_note.uid == version_uid assert version_note.date == ( dtm.date(2024, 1, int(version_uid.split(".")[-1])) if version else None ) assert set(version_note) == expected_uids @pytest.mark.parametrize( ("start_from", "end_at"), [(None, None), (None, "1.2"), ("1.3", None), ("1.2", "1.3"), ("1.3", "1.3")], ) def test_load_version_history(self, chango, start_from, end_at): lower_idx = int(start_from.split(".")[-1]) if start_from else 1 upper_idx = int(end_at.split(".")[-1]) + 1 if end_at else 4 versions = { Version(f"1.{idx}", dtm.date(2024, 1, idx)) for idx in range(lower_idx, upper_idx) } if not end_at: versions |= {Version("1.3.1", dtm.date(2024, 1, 3)), None} version_history = chango.load_version_history(start_from, end_at) assert set(version_history) == set(map(ensure_uid, versions)) for version in versions: assert version_history[ensure_uid(version)].date == (version.date if version else None) assert version_history[ensure_uid(version)].version == version def test_release_no_unreleased_changes( self, chango_no_unreleased: ChanGo, monkeypatch, cache_invalidation_tracker ): monkeypatch.setattr( chango_no_unreleased.scanner, "invalidate_caches", cache_invalidation_tracker ) version = Version("1.4", dtm.date(2024, 1, 4)) assert not chango_no_unreleased.release(version) assert not chango_no_unreleased.scanner.is_available(version) assert not cache_invalidation_tracker.was_called @pytest.mark.parametrize( "has_git", [pytest.param(True, id="with-git"), pytest.param(False, id="without-git")] ) def test_release(self, chango, cache_invalidation_tracker, monkeypatch, has_git): # Unfortunately, testing the git-available part is not easily possible without using # some of the internal utils and also not with directly running git. This is because # a) the availability of git is cached and there is no public interface to reset it # b) mocking subprocess before the module is imported is not easily possible # c) actually running `git mv` is harder to reset than just using the pathlib move # Since `chango._utils.files` is not part of the public API, we settle for testing # with the private interfaces. chango_module._utils.files._GIT_HELPER.git_available = None def check_call(args, *_, **__): assert args[:2] == ["git", "mv"] if not has_git: raise subprocess.CalledProcessError(1, "git mv") source, destination = args[2], args[3] Path(source).rename(destination) monkeypatch.setattr("chango._utils.files.subprocess.check_call", check_call) version = Version("1.4", dtm.date(2024, 1, 4)) expected_path = chango.scanner.base_directory / "1.4_2024-01-04" expected_files = { (file.name, file.read_bytes()) for file in (self.DATA_ROOT / "unreleased").iterdir() if file.name != "not-a-change-note.txt" } monkeypatch.setattr( chango.scanner, "invalidate_caches", cache_invalidation_tracker.set_super(chango.scanner.invalidate_caches), ) try: assert chango.release(version) assert cache_invalidation_tracker.was_called assert chango.scanner.is_available(version) assert chango.scanner.get_version(version.uid) == version assert expected_path.is_dir() assert { (file.name, file.read_bytes()) for file in expected_path.iterdir() } == expected_files finally: for file_name, file_content in expected_files: (self.DATA_ROOT / "unreleased" / file_name).write_bytes(file_content) if expected_path.is_dir(): shutil.rmtree(expected_path) def test_release_same_directory(self, chango, monkeypatch, cache_invalidation_tracker): def get_write_directory(*_, **__): return chango.scanner.unreleased_directory monkeypatch.setattr(chango, "get_write_directory", get_write_directory) monkeypatch.setattr( chango.scanner, "invalidate_caches", cache_invalidation_tracker.set_super(chango.scanner.invalidate_caches), ) version = Version("1.4", dtm.date(2024, 1, 4)) expected_files = { (file.name, file.read_bytes()) for file in (self.DATA_ROOT / "unreleased").iterdir() if file.name != "not-a-change-note.txt" } try: assert chango.release(version) assert cache_invalidation_tracker.was_called for file_name, file_content in expected_files: assert (self.DATA_ROOT / "unreleased" / file_name).read_bytes() == file_content except Exception: for file_name, file_content in expected_files: (self.DATA_ROOT / "unreleased" / file_name).write_bytes(file_content) def test_build_github_event_change_note(self, chango, monkeypatch): monkeypatch.setattr( chango, "build_github_event_change_note", functools.partial(ChanGo.build_github_event_change_note, chango), ) with pytest.raises(NotImplementedError): chango.build_github_event_change_note({}) chango-0.6.0/tests/abc/test_versionhistory.py000066400000000000000000000061141507376567100214060ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT import datetime as dtm import pytest from chango import Version from chango.concrete import CommentChangeNote, CommentVersionNote, HeaderVersionHistory class TestVersionHistory: """Since VersionHistory is an abstract base class, we are testing with CommentVersionHistory as a simple implementation. Note that we do *not* test abstract methods, as that is the responsibility of the concrete implementations. """ @pytest.fixture(autouse=True) def setup(self): self.version_notes = [] for i in range(5): version_note = CommentVersionNote( version=Version(f"1.0.{i}", date=dtm.date(2024, 1, i + 1)) ) for j in range(5): version_note.add_change_note( CommentChangeNote( slug=f"1-0-{i}-slug-{j}", uid=f"1-0-{i}-uid-{j}", comment=f"1-0-{i}-comment-{j}", ) ) self.version_notes.append(version_note) self.version_note = self.version_notes[0] self.version_history = HeaderVersionHistory() @pytest.fixture(params=["uid", "Version", "None"]) def key(self, request): if request.param == "uid": return self.version_note.uid if request.param == "Version": return self.version_note.version return None def test_set_get_del_item(self, key): version_note = self.version_note if key else CommentVersionNote(None) self.version_history[key] = version_note assert self.version_history[key] == version_note del self.version_history[key] with pytest.raises(KeyError): self.version_history[key] with pytest.raises(KeyError): del self.version_history[self.version_note.uid] def test_setitem_warning(self): with pytest.warns( UserWarning, match="Key 'non-matching-key' does not match version note UID" ): self.version_history["non-matching-key"] = self.version_note def test_add_remove_note(self, key): if key is None: pytest.skip("Not relevant for None key") self.version_history.add_version_note(self.version_note) assert self.version_history[key] == self.version_note self.version_history.remove_version_note(self.version_note) with pytest.raises(KeyError): self.version_history.remove_version_note(self.version_note) def test_iter(self): for version_note in self.version_notes: self.version_history.add_version_note(version_note) for i, key in enumerate(self.version_history): assert self.version_history[key] is self.version_notes[i] def test_len(self): assert len(self.version_history) == 0 for version_note in self.version_notes: self.version_history.add_version_note(version_note) assert len(self.version_history) == len(self.version_notes) chango-0.6.0/tests/abc/test_versionnote.py000066400000000000000000000062741507376567100206610ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT import datetime as dtm import pytest from chango import Version from chango.concrete import CommentChangeNote, CommentVersionNote class TestVersionNote: """Since VersionNote is an abstract base class, we are testing with CommentVersionNote as a simple implementation. Note that we do *not* test abstract methods, as that is the responsibility of the concrete implementations. """ @pytest.fixture( autouse=True, params=[None, Version(uid="1.0.0", date=dtm.date(2024, 1, 1))], ids=["None", "Version"], ) def setup(self, request): # This is the next best thing to parametrizing __init__ that I could find # in reasonable time version = request.param self.version = version self.version_note = CommentVersionNote(version=version) self.change_notes = [ CommentChangeNote(slug=f"slug-{i}", uid=f"uid-{i}", comment=f"comment-{i}") for i in range(5) ] @property def change_note(self) -> CommentChangeNote: return self.change_notes[0] def test_init(self): assert self.version_note.version == self.version assert len(self.version_note) == 0 def test_uid(self): assert self.version_note.uid == (self.version.uid if self.version else None) def test_date(self): assert self.version_note.date == (self.version.date if self.version else None) @pytest.mark.parametrize("key_type", ["uid", "filename"]) def test_set_get_del_item(self, key_type): key = self.change_note.uid if key_type == "uid" else self.change_note.file_name self.version_note[key] = self.change_note assert self.version_note[key] == self.change_note del self.version_note[key] with pytest.raises(KeyError): self.version_note[key] with pytest.raises(KeyError): del self.version_note[self.change_note.uid] def test_setitem_warning(self): with pytest.warns( UserWarning, match="Key 'non-matching-key' does not match change note UID" ): self.version_note["non-matching-key"] = self.change_note @pytest.mark.parametrize("key_type", ["uid", "filename"]) def test_add_remove_note(self, key_type): key = self.change_note.uid if key_type == "uid" else self.change_note.file_name self.version_note.add_change_note(self.change_note) assert self.version_note[key] == self.change_note self.version_note.remove_change_note(self.change_note) with pytest.raises(KeyError): self.version_note.remove_change_note(self.change_note) def test_iter(self): for change_note in self.change_notes: self.version_note.add_change_note(change_note) for i, key in enumerate(self.version_note): assert self.version_note[key] is self.change_notes[i] def test_len(self): assert len(self.version_note) == 0 for change_note in self.change_notes: self.version_note.add_change_note(change_note) assert len(self.version_note) == len(self.change_notes) chango-0.6.0/tests/abc/test_versionscanner.py000066400000000000000000000065261507376567100213450ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT import datetime as dtm import pytest from chango import Version from chango.abc import VersionScanner from chango.concrete import DirectoryVersionScanner from chango.error import ChanGoError from tests.auxil.files import data_path @pytest.fixture def scanner(monkeypatch) -> DirectoryVersionScanner: # DVS overrides get_version, but we want to test the base implementation monkeypatch.setattr(DirectoryVersionScanner, "get_version", VersionScanner.get_version) return DirectoryVersionScanner(TestVersionScanner.DATA_ROOT, "unreleased") @pytest.fixture def scanner_no_unreleased(monkeypatch) -> DirectoryVersionScanner: # DVS overrides get_version, but we want to test the base implementation monkeypatch.setattr(DirectoryVersionScanner, "get_version", VersionScanner.get_version) return DirectoryVersionScanner(TestVersionScanner.DATA_ROOT, "no-unreleased") class TestVersionScanner: """Since VersionScanner is an abstract base class, we are testing with DirectoryVersionScanner as a simple implementation. Note that we do *not* test abstract methods, as that is the responsibility of the concrete implementations. """ DATA_ROOT = data_path("directoryversionscanner") @pytest.mark.parametrize( ("version", "expected"), [ ("1.1", True), (Version("1.1", dtm.date(2024, 1, 1)), True), (Version("1.1", dtm.date(2024, 5, 1)), False), ("1.2", True), (Version("1.2", dtm.date(2024, 1, 2)), True), (Version("1.2", dtm.date(2024, 5, 1)), False), ("1.3", True), (Version("1.3", dtm.date(2024, 1, 3)), True), (Version("1.3", dtm.date(2024, 5, 1)), False), ("1.3.1", True), (Version("1.3.1", dtm.date(2024, 1, 3)), True), (Version("1.3.1", dtm.date(2024, 5, 1)), False), (None, True), ("1.4", False), ("1.0", False), (object(), False), (dtm.date(2024, 1, 1), False), ], ) def test_contains(self, scanner, version, expected): assert (version in scanner) == expected def test_contains_no_unreleased_changes(self, scanner_no_unreleased): assert None not in scanner_no_unreleased def test_iter(self, scanner): assert set(scanner) == { Version("1.1", dtm.date(2024, 1, 1)), Version("1.2", dtm.date(2024, 1, 2)), Version("1.3", dtm.date(2024, 1, 3)), Version("1.3.1", dtm.date(2024, 1, 3)), } def test_len(self, scanner): assert len(scanner) == len(scanner.get_available_versions()) @pytest.mark.parametrize("idx", [1, 2, 3]) def test_get_version(self, scanner, idx): version = scanner.get_version(f"1.{idx}") assert version.uid == f"1.{idx}" assert version.date == dtm.date(2024, 1, idx) def test_get_version_not_found(self, scanner): with pytest.raises(ChanGoError, match="not available"): scanner.get_version("1.4") def test_invalidates_caches(self, scanner): # This does nothing, but we want to test that it doesn't raise an error scanner.invalidate_caches = VersionScanner.invalidate_caches scanner.invalidate_caches(scanner) chango-0.6.0/tests/auxil/000077500000000000000000000000001507376567100152615ustar00rootroot00000000000000chango-0.6.0/tests/auxil/__init__.py000066400000000000000000000001601507376567100173670ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT chango-0.6.0/tests/auxil/files.py000066400000000000000000000014131507376567100167340ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT import os from contextlib import contextmanager from pathlib import Path from chango._utils.types import PathLike PROJECT_ROOT_PATH = Path(__file__).parent.parent.parent.resolve() TEST_DATA_PATH = PROJECT_ROOT_PATH / "tests" / "data" def data_path(filename: PathLike) -> Path: return TEST_DATA_PATH / filename def path_to_python_string(path: Path, output_type: type[str] | type[Path]) -> str: if output_type is str: return f"'{path.as_posix()}'" return f"Path(r'{path}')" @contextmanager def temporary_chdir(path: Path): current_dir = Path.cwd() try: os.chdir(path) yield finally: os.chdir(current_dir) chango-0.6.0/tests/cli/000077500000000000000000000000001507376567100147065ustar00rootroot00000000000000chango-0.6.0/tests/cli/__init__.py000066400000000000000000000000001507376567100170050ustar00rootroot00000000000000chango-0.6.0/tests/cli/conftest.py000066400000000000000000000045771507376567100171220ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT import traceback import unittest.mock from collections.abc import Mapping, Sequence from typing import IO, Any, Literal import pytest from click.testing import Result from typer import Typer from typer.testing import CliRunner import chango from chango._cli import app as chango_app class CLIResult: def __init__(self, result: Result) -> None: self._result = result def __getattr__(self, name: str) -> Any: # only called if attribute is not found in self return getattr(self._result, name) def check_exit_code(self, expected_code: int = 0) -> Literal[True]: text = f"stdout:\n{self.stdout}" if self.exception: text += f"\nexception:\n{self.exception}" text += f"\ntraceback:\n{'\n'.join(traceback.format_exception(*self.exc_info))}" if self.exit_code != expected_code: raise AssertionError( f"Expected exit code {expected_code}, got {self.exit_code}\n{text}" ) return True class ReuseCliRunner(CliRunner): def __init__(self, app: Typer, *args: Any, **kwargs: Any) -> None: self.app = app # For easier testing, disable rich markup mode self.app.rich_markup_mode = None super().__init__(*args, **kwargs) def invoke( self, args: str | Sequence[str] | None = None, input: bytes | str | IO[Any] | None = None, env: Mapping[str, str] | None = None, catch_exceptions: bool = True, color: bool = False, **extra: Any, ) -> CLIResult: return CLIResult( super().invoke( self.app, args=args, input=input, env=env, catch_exceptions=catch_exceptions, color=color, **extra, ) ) @pytest.fixture(scope="session") def cli(): return ReuseCliRunner(chango_app) @pytest.fixture def mock_chango_instance(monkeypatch): chango_config = unittest.mock.MagicMock() monkeypatch.setattr(chango.config.ChanGoConfig, "load", lambda *_, **__: chango_config) yield chango_config.import_chango_instance() # This is required to ensure that each test gets a new instance of the mock chango.config.get_chango_instance.cache_clear() chango-0.6.0/tests/cli/test_config.py000066400000000000000000000073411507376567100175710ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT from contextlib import nullcontext from pathlib import Path import pytest from click import UsageError from tests.auxil.files import PROJECT_ROOT_PATH, data_path, temporary_chdir from tests.cli.conftest import ReuseCliRunner class TestConfig: @pytest.mark.parametrize( "path", [ None, data_path("config/pyproject.toml"), data_path("config/pyproject.toml").relative_to(Path.cwd(), walk_up=True), data_path("config"), ], ids=["None", "absolute", "relative", "directory"], ) def test_show_path_selection(self, cli: ReuseCliRunner, path): with temporary_chdir(data_path("config")) if path is None else nullcontext(): args = ["config", "--path", str(path), "show"] if path else ["config", "show"] result = cli.invoke(args) assert result.check_exit_code(0) assert str(data_path("config/pyproject.toml")) in result.stdout assert 'sys_path = "/abs/sys_path"' in result.stdout assert ( 'chango_instance = { name= "name", module = "module", package = "package" }' in result.stdout ) def test_show_path_not_found(self, cli: ReuseCliRunner): path = Path("nonexistent").absolute() result = cli.invoke(["config", "--path", str(path), "show"]) assert result.check_exit_code(UsageError.exit_code) assert f"File not found: {path!s}" in result.stderr def test_show_invalid_toml(self, cli: ReuseCliRunner, tmp_path): path = tmp_path / "pyproject.toml" path.write_text("invalid toml") with temporary_chdir(tmp_path): result = cli.invoke(["config", "--path", str(path), "show"]) assert result.check_exit_code(UsageError.exit_code) assert "Failed to parse the configuration file" in result.stderr def test_show_no_chango_config(self, cli: ReuseCliRunner, tmp_path): path = tmp_path / "pyproject.toml" path.write_text("[tool.other]") with temporary_chdir(tmp_path): result = cli.invoke(["config", "--path", str(path), "show"]) assert result.check_exit_code(UsageError.exit_code) assert "No configuration found for chango" in result.stderr def test_validate_invalid_chango_config(self, cli: ReuseCliRunner, tmp_path): path = tmp_path / "pyproject.toml" path.write_text("[tool.chango]\nsys_path = 42") with temporary_chdir(tmp_path): result = cli.invoke(["config", "--path", str(path), "validate"]) assert result.check_exit_code(UsageError.exit_code) assert f"Validation of config file at {path!s} failed:" in result.stderr def test_validate_import_error(self, cli: ReuseCliRunner, tmp_path): path = tmp_path / "pyproject.toml" path.write_text( "[tool.chango]\nsys_path = 'sys_path'\nchango_instance = { name= 'name', module = " "'module', package = 'package' }" ) with temporary_chdir(tmp_path): result = cli.invoke(["config", "--path", str(path), "validate"]) assert result.check_exit_code(UsageError.exit_code) assert "importing the ChanGo instance failed" in result.stderr def test_validate_success(self, cli: ReuseCliRunner): with temporary_chdir(PROJECT_ROOT_PATH): result = cli.invoke(["config", "validate"]) assert result.check_exit_code(0) assert "The configuration in" in result.stdout assert str(PROJECT_ROOT_PATH / "pyproject.toml") in result.stdout assert "valid" in result.stdout chango-0.6.0/tests/cli/test_edit.py000066400000000000000000000015331507376567100172460ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT from pathlib import Path from unittest.mock import MagicMock from tests.cli.conftest import ReuseCliRunner class TestEdit: def test_release_no_unreleased(self, cli: ReuseCliRunner, mock_chango_instance, monkeypatch): launch_mock = MagicMock() monkeypatch.setattr("typer.launch", launch_mock) test_path = Path("this/is/a/test/path") mock_chango_instance.scanner.lookup_change_note.return_value.file_path = test_path result = cli.invoke(args=["edit", "some_uid"]) assert result.check_exit_code() assert result.stdout == "" mock_chango_instance.scanner.lookup_change_note.assert_called_once_with("some_uid") launch_mock.assert_called_once_with(test_path.as_posix()) chango-0.6.0/tests/cli/test_main.py000066400000000000000000000006251507376567100172460ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT from chango.__about__ import __version__ from tests.cli.conftest import ReuseCliRunner class TestMain: def test_version(self, cli: ReuseCliRunner): result = cli.invoke(args=["--version"]) assert result.check_exit_code() assert result.stdout == __version__ + "\n" chango-0.6.0/tests/cli/test_new.py000066400000000000000000000032061507376567100171110ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT from pathlib import Path from unittest.mock import MagicMock import pytest from tests.cli.conftest import ReuseCliRunner class TestNew: @pytest.mark.parametrize("slug_option", ["--slug", "-s"], ids=["LongSlug", "ShortSlug"]) @pytest.mark.parametrize( ("edit", "edit_option"), [(True, "--edit"), (True, "-e"), (False, "--no-edit"), (False, "-ne"), (None, None)], ) def test_new( self, cli: ReuseCliRunner, mock_chango_instance, monkeypatch, edit, slug_option, edit_option, ): launch_mock = MagicMock() monkeypatch.setattr("typer.launch", launch_mock) test_path = Path("this/is/a/test/path") change_note = mock_chango_instance.build_template_change_note.return_value change_note.file_name = "expected_file_name" mock_chango_instance.write_change_note.return_value = test_path args = ["new", slug_option, "some_uid"] if edit_option is not None: args.append(edit_option) result = cli.invoke(args=args) assert result.check_exit_code() assert result.stdout == "Created new change note expected_file_name\n" mock_chango_instance.build_template_change_note.assert_called_once_with(slug="some_uid") mock_chango_instance.write_change_note.assert_called_once_with(change_note, version=None) if edit in (True, None): launch_mock.assert_called_once_with(test_path.as_posix()) else: launch_mock.assert_not_called() chango-0.6.0/tests/cli/test_release.py000066400000000000000000000034561507376567100177470ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT import datetime as dtm import pytest from click import UsageError from chango import Version from tests.cli.conftest import ReuseCliRunner class TestRelease: @pytest.mark.parametrize("has_unreleased", [True, False]) def test_release_basic(self, cli: ReuseCliRunner, mock_chango_instance, has_unreleased): mock_chango_instance.release.return_value = has_unreleased result = cli.invoke(args=["release", "--uid", "1.0.0", "--date", "2024-01-01"]) assert result.check_exit_code() assert result.stdout == ( "Released version 1.0.0 on 2024-01-01\n" if has_unreleased else "No unreleased changes found.\n" ) assert len(mock_chango_instance.release.call_args_list) == 1 assert mock_chango_instance.release.call_args_list[0].args == ( Version("1.0.0", dtm.date(2024, 1, 1)), ) def test_release_default_date(self, cli: ReuseCliRunner, mock_chango_instance): result = cli.invoke(args=["release", "--uid", "1.0.0"]) assert result.check_exit_code() assert result.stdout == f"Released version 1.0.0 on {dtm.date.today()}\n" assert len(mock_chango_instance.release.call_args_list) == 1 assert mock_chango_instance.release.call_args_list[0].args == ( Version("1.0.0", dtm.date.today()), ) def test_release_invalid_date(self, cli: ReuseCliRunner, mock_chango_instance): result = cli.invoke(args=["release", "--uid", "1.0.0", "--date", "invalid"]) assert result.check_exit_code(UsageError.exit_code) assert "Invalid value for '--date'" in result.stderr assert len(mock_chango_instance.release.call_args_list) == 0 chango-0.6.0/tests/cli/test_report.py000066400000000000000000000113041507376567100176310ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT from pathlib import Path import pytest from click import UsageError from chango.constants import MarkupLanguage from tests.cli.conftest import ReuseCliRunner class TestReport: @pytest.mark.parametrize( "markup", [MarkupLanguage.MARKDOWN, MarkupLanguage.HTML, None], ids=["Markdown", "HTML", "DefaultMarkup"], ) @pytest.mark.parametrize( "markup_option", ["--markup", "-m"], ids=["LongMarkup", "ShortMarkup"] ) @pytest.mark.parametrize( ("output", "output_option"), [(None, None), (True, "--output"), (True, "-o")] ) def test_report_version( self, cli: ReuseCliRunner, mock_chango_instance, markup, markup_option, output, output_option, tmp_path: Path, ): file_path = tmp_path / "output_file" version_note = mock_chango_instance.load_version_note.return_value version_note.render.return_value = "expected_render_output" args = ["report", "version", "--uid", "1.2.3"] if markup is not None: args.extend([markup_option, markup.value]) if output is True: args.extend([output_option, file_path.as_posix()]) result = cli.invoke(args=args) assert result.check_exit_code() if output is not None: assert result.stdout == f"Report written to {file_path}\n" else: assert result.stdout == "expected_render_output\n" mock_chango_instance.load_version_note.assert_called_once_with("1.2.3") version_note.render.assert_called_once_with(markup=markup or MarkupLanguage.MARKDOWN) if output is True: assert file_path.read_text() == "expected_render_output" @pytest.mark.parametrize( "markup", [MarkupLanguage.MARKDOWN, MarkupLanguage.HTML, None], ids=["Markdown", "HTML", "DefaultMarkup"], ) @pytest.mark.parametrize( "markup_option", ["--markup", "-m"], ids=["LongMarkup", "ShortMarkup"] ) @pytest.mark.parametrize( ("output", "output_option"), [(None, None), (True, "--output"), (True, "-o")] ) def test_report_history( self, cli: ReuseCliRunner, mock_chango_instance, markup, markup_option, output, output_option, tmp_path: Path, ): file_path = tmp_path / "output_file" version_history = mock_chango_instance.load_version_history.return_value version_history.render.return_value = "expected_render_output" args = ["report", "history"] if markup is not None: args.extend([markup_option, markup.value]) if output is True: args.extend([output_option, file_path.as_posix()]) result = cli.invoke(args=args) assert result.check_exit_code() if output is not None: assert result.stdout == f"Report written to {file_path}\n" else: assert result.stdout == "expected_render_output\n" mock_chango_instance.load_version_history.assert_called_once_with() version_history.render.assert_called_once_with(markup=markup or MarkupLanguage.MARKDOWN) if output is True: assert file_path.read_text() == "expected_render_output" @pytest.mark.parametrize("invalidity_type", ["dir", "non_writable"]) @pytest.mark.parametrize("subcommand", ["version", "history"], ids=["Version", "History"]) def test_report_invalid_file( self, cli: ReuseCliRunner, mock_chango_instance, tmp_path: Path, invalidity_type, subcommand, ): file_path = tmp_path / "output_file" if invalidity_type == "dir": output_path = tmp_path elif invalidity_type == "non_writable": file_path.touch() file_path.chmod(0o444) output_path = file_path args = ["report", subcommand, "--uid", "1.2.3", "--output", output_path.as_posix()] result = cli.invoke(args=args) assert result.check_exit_code(UsageError.exit_code) mock_chango_instance.load_version_note.assert_not_called() @pytest.mark.parametrize("subcommand", ["version", "history"], ids=["Version", "History"]) def test_report_invalid_markup(self, cli: ReuseCliRunner, mock_chango_instance, subcommand): args = ["report", subcommand, "--markup", "invalid_markup"] if subcommand == "version": args.extend(["--uid", "1.2.3"]) result = cli.invoke(args=args) assert result.check_exit_code(UsageError.exit_code) mock_chango_instance.load_version_note.assert_not_called() chango-0.6.0/tests/concrete/000077500000000000000000000000001507376567100157415ustar00rootroot00000000000000chango-0.6.0/tests/concrete/__init__.py000066400000000000000000000001601507376567100200470ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT chango-0.6.0/tests/concrete/sections/000077500000000000000000000000001507376567100175705ustar00rootroot00000000000000chango-0.6.0/tests/concrete/sections/__init__.py000066400000000000000000000001601507376567100216760ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT chango-0.6.0/tests/concrete/sections/test_githubsectionchangenote.py000066400000000000000000000165461507376567100261200ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT import pytest from chango.action import ChanGoActionData, LinkedIssue from chango.concrete.sections import GitHubSectionChangeNote, PullRequest, Section class DummyChangNote( GitHubSectionChangeNote.with_sections([Section(uid="req", title="Req", is_required=True)]) ): OWNER = "my-username" REPOSITORY = "my-repo" class DummyChangNoteNoOwner( GitHubSectionChangeNote.with_sections([Section(uid="req", title="Req", is_required=True)]) ): REPOSITORY = "my-repo" class DummyChangNoteNoRepository( GitHubSectionChangeNote.with_sections([Section(uid="req", title="Req", is_required=True)]) ): OWNER = "my-username" class FromGitHubEvent( GitHubSectionChangeNote.with_sections( [ Section(uid="opt_0", title="Opt", is_required=False, sort_order=0), Section(uid="req_1", title="Req", is_required=True, sort_order=1), Section(uid="req_0", title="Req", is_required=True, sort_order=0), ] ) ): pass class TestGitHubSectionChangeNote: """Since TestSectionChangeNote is an abstract base class, we are testing with GitHubTestSectionChangeNote as a simple implementation. Note that we do *not* test abstract methods, as that is the responsibility of the concrete implementations. """ def test_class_variables(self): assert DummyChangNote.OWNER == "my-username" assert DummyChangNote.REPOSITORY == "my-repo" def test_get_pull_request_url(self): assert ( DummyChangNote.get_pull_request_url("123") == "https://github.com/my-username/my-repo/pull/123" ) def test_get_pull_request_url_invalid(self): with pytest.raises(ValueError, match=r"OWNER must be set as class variable."): DummyChangNoteNoOwner.get_pull_request_url("123") with pytest.raises(ValueError, match=r"REPOSITORY must be set as class variable."): DummyChangNoteNoRepository.get_pull_request_url("123") def test_get_thread_url(self): assert ( DummyChangNote.get_thread_url("123") == "https://github.com/my-username/my-repo/issues/123" ) def test_get_thread_url_invalid(self): with pytest.raises(ValueError, match=r"OWNER must be set as class variable."): DummyChangNoteNoOwner.get_thread_url("123") with pytest.raises(ValueError, match=r"REPOSITORY must be set as class variable."): DummyChangNoteNoRepository.get_thread_url("123") def test_get_author_url(self): assert DummyChangNote.get_author_url("123") == "https://github.com/123" def test_get_sections_has_required(self): assert FromGitHubEvent.get_sections(None, None) == {"req_0", "req_1"} def test_get_sections_no_required(self): NoRequired = GitHubSectionChangeNote.with_sections( [ Section(uid="opt_0", title="Opt", is_required=False, sort_order=5), Section(uid="opt_1", title="Opt", is_required=False, sort_order=42), Section(uid="opt_2", title="Opt", is_required=False, sort_order=-3), ] ) assert NoRequired.get_sections(None, None) == {"opt_2"} def test_build_from_github_event_missing_data(self): with pytest.raises(ValueError, match="required data"): FromGitHubEvent.build_from_github_event({}) def test_build_from_github_event_basic(self): event_data = { "pull_request": { "html_url": "https://example.com/pull/42", "number": 42, "title": "example title", "user": {"login": "author_uid"}, "labels": [{"name": "label1"}, {"name": "label2"}], } } change_note = FromGitHubEvent.build_from_github_event(event_data) assert change_note.req_0 == "example title" assert change_note.pull_requests == ( PullRequest(uid="42", author_uids=("author_uid",), closes_threads=()), ) assert change_note.slug == "0042" def test_build_from_github_event_custom_get_sections(self): class CustomGetSections(FromGitHubEvent): @classmethod def get_sections(cls, event_data, sections): # noqa: ARG003 return {"opt_0", "req_1", "req_0"} event_data = { "pull_request": { "html_url": "https://example.com/pull/42", "number": 42, "title": "example title", "user": {"login": "author_uid"}, "labels": [{"name": "label1"}, {"name": "label2"}], } } change_note = CustomGetSections.build_from_github_event(event_data) assert change_note.req_0 == "example title" assert change_note.req_1 == "example title" assert change_note.opt_0 == "example title" assert change_note.pull_requests == ( PullRequest(uid="42", author_uids=("author_uid",), closes_threads=()), ) assert change_note.slug == "0042" def test_build_from_github_event_chango_action_data_no_linked_issues(self, monkeypatch): received_data = {} def get_sections(labels, issue_types): received_data["labels"] = labels received_data["issue_types"] = issue_types return {"req_0", "req_1"} monkeypatch.setattr(FromGitHubEvent, "get_sections", get_sections) event_data = { "pull_request": { "html_url": "https://example.com/pull/42", "number": 42, "title": "example title", "user": {"login": "author_uid"}, "labels": [], } } data = ChanGoActionData(linked_issues=None, parent_pull_request=None) FromGitHubEvent.build_from_github_event(event_data, data) assert received_data["labels"] == set() assert received_data["issue_types"] == set() def test_build_from_github_event_chango_action_data(self, monkeypatch): received_data = {} def get_sections(labels, issue_types): received_data["labels"] = labels received_data["issue_types"] = issue_types return {"req_0", "req_1"} monkeypatch.setattr(FromGitHubEvent, "get_sections", get_sections) event_data = { "pull_request": { "html_url": "https://example.com/pull/42", "number": 42, "title": "example title", "user": {"login": "author_uid"}, "labels": [{"name": "pr_label1"}, {"name": "pr_label2"}], } } data = ChanGoActionData( linked_issues=( LinkedIssue(number=1, title="issue_title", labels=None, issue_type="issue_type"), LinkedIssue( number=2, title="issue_title", labels=("issue_label1", "issue_label2"), issue_type=None, ), ), parent_pull_request=None, ) FromGitHubEvent.build_from_github_event(event_data, data) assert received_data["labels"] == { "pr_label1", "pr_label2", "issue_label1", "issue_label2", } assert received_data["issue_types"] == {"issue_type"} chango-0.6.0/tests/concrete/sections/test_pullrequest.py000066400000000000000000000025401507376567100235670ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT import pytest from pydantic import ValidationError from chango.concrete.sections import PullRequest class TestPullRequest: def test_init_required_args(self): pr = PullRequest(uid="uid1", author_uids=("author1",)) assert pr.uid == "uid1" assert pr.author_uids == ("author1",) assert pr.closes_threads == () def test_init_all_args(self): pr = PullRequest(uid="uid2", author_uids=("author2",), closes_threads=("thread3",)) assert pr.uid == "uid2" assert pr.author_uids == ("author2",) assert pr.closes_threads == ("thread3",) def test_init_legacy_author_uid(self): pr = PullRequest(uid="uid3", author_uid="author3") assert pr.uid == "uid3" assert pr.author_uids == ("author3",) assert pr.closes_threads == () def test_init_mutually_exclusive_author_fields(self): with pytest.raises( ValidationError, match="author_uid and author_uids are mutually exclusive" ): PullRequest(uid="uid4", author_uid="author4", author_uids=("author5",)) def test_init_invalid_author_type(self): with pytest.raises(ValidationError, match="should be a valid tuple"): PullRequest(uid="uid5", author_uids=42) chango-0.6.0/tests/concrete/sections/test_section.py000066400000000000000000000015371507376567100226530ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT from chango.concrete.sections import Section class TestSection: def test_init_required_args(self): section = Section(uid="uid1", title="Title 1") assert section.uid == "uid1" assert section.title == "Title 1" assert section.is_required is False assert section.render_pr_details is True assert section.sort_order == 0 def test_init_all_args(self): section = Section( uid="uid2", title="Title 2", is_required=True, render_pr_details=False, sort_order=1 ) assert section.uid == "uid2" assert section.title == "Title 2" assert section.is_required is True assert section.render_pr_details is False assert section.sort_order == 1 chango-0.6.0/tests/concrete/sections/test_sectionchangenote.py000066400000000000000000000136151507376567100247070ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT import pytest import shortuuid from chango.concrete.sections import ( GitHubSectionChangeNote, PullRequest, Section, SectionChangeNote, ) from chango.constants import MarkupLanguage from chango.error import ValidationError class DummyChangNote( GitHubSectionChangeNote.with_sections( [ Section(uid="req_section", title="Required Section", is_required=True), Section(uid="opt_section", title="Optional Section"), ] ) ): OWNER = "my-username" REPOSITORY = "my-repo" @pytest.fixture def section_change_note(): return DummyChangNote( slug="slug", req_section="req ð›™ðŒ¢ð‘", opt_section="opt ð›™ðŒ¢ð‘", pull_requests=( PullRequest(uid="uid1", closes_threads=("thread1",), author_uids=("author1",)), PullRequest(uid="uid2", closes_threads=("thread2",), author_uids=("author2",)), ), ) class TestSectionChangeNote: """Since TestSectionChangeNote is an abstract base class, we are testing with GitHubTestSectionChangeNote as a simple implementation. Note that we do *not* test abstract methods, as that is the responsibility of the concrete implementations. """ sections = ( Section(uid="req_section", title="Required Section", is_required=True), Section(uid="opt_section", title="Optional Section"), ) def test_manual_subclass(self): class SubClass(SectionChangeNote): @classmethod def get_pull_request_url(cls, pr_uid: str) -> str: pass @classmethod def get_thread_url(cls, thread_uid: str) -> str: pass @classmethod def get_author_url(cls, author_uid: str) -> str: pass with pytest.raises(TypeError, match="SectionChangeNote must not be subclassed manually"): SubClass(slug="slug") @pytest.mark.parametrize("name", [None, "CustomName"]) def test_with_sections(self, name): cls = SectionChangeNote.with_sections(self.sections, name=name) assert cls.SECTIONS["req_section"] is self.sections[0] assert cls.SECTIONS["opt_section"] is self.sections[1] assert cls.__name__ == (name or "DynamicSectionChangeNote") def test_with_sections_empty_sequence(self): with pytest.raises(ValueError, match="Class must have at least one section"): SectionChangeNote.with_sections([]) def test_empty_init(self): cls = GitHubSectionChangeNote.with_sections( [Section(uid=f"opt_{i}", title=f"Optional {i}") for i in range(10)] ) with pytest.raises(ValidationError, match="At least one section must be specified"): cls(slug="slug", opt_0="", opt_1=None) def test_constants(self, section_change_note): assert section_change_note.file_extension == "toml" assert section_change_note.MARKUP == MarkupLanguage.RESTRUCTUREDTEXT @pytest.mark.parametrize("has_prs", [True, False]) def test_from_string(self, section_change_note, has_prs): string = """ req_section = '''Required section. With multiple lines.''' opt_section = "Optional Section." """ if has_prs: string += """ [[pull_requests]] uid = "uid1" closes_threads = ["thread1", "thread2"] author_uids = ["author1"] [[pull_requests]] uid = "uid2" closes_threads = ["thread3"] author_uid = ["author2"] """ change_note = section_change_note.from_string("slug", "uid", string) assert change_note.slug == "slug" assert change_note.uid == "uid" assert change_note.req_section == "Required section.\nWith multiple lines." assert change_note.opt_section == "Optional Section." if has_prs: assert len(change_note.pull_requests) == 2 # noqa: PLR2004 assert change_note.pull_requests[0].uid == "uid1" assert change_note.pull_requests[0].closes_threads == ("thread1", "thread2") assert change_note.pull_requests[0].author_uids == ("author1",) assert change_note.pull_requests[1].uid == "uid2" assert change_note.pull_requests[1].closes_threads == ("thread3",) assert change_note.pull_requests[1].author_uids == ("author2",) else: assert change_note.pull_requests == () def test_from_string_invalid(self, section_change_note): with pytest.raises(ValidationError, match="Invalid TOML data"): section_change_note.from_string("slug", "uid", "invalid toml") @pytest.mark.parametrize("encoding", ["utf-8", "utf-16"]) def test_to_string(self, section_change_note, encoding): string = section_change_note.to_string(encoding=encoding) assert ( string == """req_section = "req ð›™ðŒ¢ð‘" opt_section = "opt ð›™ðŒ¢ð‘" [[pull_requests]] uid = "uid1" author_uids = ["author1"] closes_threads = ["thread1"] [[pull_requests]] uid = "uid2" author_uids = ["author2"] closes_threads = ["thread2"] """ ) @pytest.mark.parametrize("uid", ["uid1", None]) def test_build_template(self, uid): change_note = DummyChangNote.build_template("slug", uid) assert change_note.req_section == "Required Section Content" assert change_note.opt_section == "Optional Section Content" assert change_note.pull_requests == ( PullRequest( uid="pr-number-1", closes_threads=("thread1", "thread2"), author_uids=("author1",) ), PullRequest(uid="pr-number-2", closes_threads=("thread3",), author_uids=("author2",)), ) assert change_note.slug == "slug" if uid: assert change_note.uid == "uid1" else: assert isinstance(change_note.uid, str) assert len(change_note.uid) == len(shortuuid.ShortUUID().uuid()) chango-0.6.0/tests/concrete/sections/test_sectionversionnote.py000066400000000000000000000075011507376567100251440ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT import pytest from chango.concrete.sections import ( GitHubSectionChangeNote, PullRequest, Section, SectionChangeNote, SectionVersionNote, ) from chango.error import UnsupportedMarkupError class DummySectionChangeNote( GitHubSectionChangeNote.with_sections( [ Section(uid="req_section", title="Required Section", is_required=True, sort_order=10), Section(uid="opt_section", title="Optional Section", render_pr_details=False), ] ) ): OWNER = "my-username" REPOSITORY = "my-repo" OtherSectionChangeNote = GitHubSectionChangeNote.with_sections( [Section(uid="req_section", title="Required Section", is_required=True)] ) @pytest.fixture def section_version_note(): return SectionVersionNote(None, DummySectionChangeNote) class TestSectionVersionNote: def test_init_invalid_markup(self): class InvalidSectionChangeNote(SectionChangeNote): MARKUP = "invalid" with pytest.raises(UnsupportedMarkupError, match="only supports reStructuredText"): SectionVersionNote(None, InvalidSectionChangeNote) def test_setitem_invalid_type(self, section_version_note): other_section_change_note = OtherSectionChangeNote(slug="slug", req_section="req_section") with pytest.raises(TypeError, match="Expected a"): section_version_note["key"] = other_section_change_note with pytest.raises(TypeError, match="Expected a"): section_version_note.add_change_note(other_section_change_note) def test_render_unknown_markup(self, section_version_note): with pytest.raises(UnsupportedMarkupError, match="unknown"): section_version_note.render("unknown") def test_render_unsupported_markup(self, section_version_note): with pytest.raises(UnsupportedMarkupError, match="markdown"): section_version_note.render("markdown") def test_render(self, section_version_note): section_version_note.add_change_note( DummySectionChangeNote( slug="slug1", uid="uid1", req_section="change note 1 req.\nWith multiple lines.", opt_section="change note 1 opt.", pull_requests=[ PullRequest( uid="pr1", closes_threads=("thread1", "thread2"), author_uids=("author1",) ), PullRequest(uid="pr2", author_uids=("author2",)), ], ) ) section_version_note.add_change_note( DummySectionChangeNote(slug="slug2", uid="uid2", req_section="change note 2 req.") ) section_version_note.add_change_note( DummySectionChangeNote( slug="slug3", uid="uid3", req_section="change note 3 req.", pull_requests=[PullRequest(uid="pr_a", author_uids=("author_b", "author_c"))], ) ) assert ( section_version_note.render("rst") == """\ Optional Section ---------------- - change note 1 opt. Required Section ---------------- - change note 1 req. With multiple lines. (`#pr1 `_ by `@author1 \ `_ closes `#thread1 \ `_, `#thread2 \ `_; `#pr2 \ `_ by `@author2 `_) - change note 2 req. - change note 3 req. (`#pr_a `_ by \ `@author_b `_, `@author_c `_)\ """ ) chango-0.6.0/tests/concrete/test_backwardcompatiblechango.py000066400000000000000000000212341507376567100243520ustar00rootroot00000000000000import inspect from unittest.mock import MagicMock import pytest from chango.concrete import BackwardCompatibleChanGo, BackwardCompatibleVersionScanner from chango.error import ChanGoError class TestBackwardCompatibleChanGo: @staticmethod def build_mocks( *, chango: tuple[str, list[object]] | None = None, scanner: tuple[str, list[object]] | None = None, ) -> tuple[MagicMock, list[MagicMock]]: if not chango and not scanner: raise ValueError("At least one of 'chango' or 'scanner' must be provided.") if chango and scanner and len(chango[1]) != len(scanner[1]): raise ValueError( "The number of chango instances must match the number of scanner instances." ) if (effective_length := len(chango[1] if chango else scanner[1])) < 2: # noqa PLR2004 raise ValueError("At least two instances must be provided.") def setup_mock(mock, method_name, result_): method = getattr(mock, method_name) if isinstance(result_, Exception) or ( inspect.isclass(result_) and issubclass(result_, Exception) ): method.side_effect = result_ else: method.return_value = result_ instances = [MagicMock() for _ in range(effective_length)] if chango: for instance, result in zip(instances, chango[1], strict=False): setup_mock(instance, chango[0], result) if scanner: for instance, result in zip(instances, scanner[1], strict=False): setup_mock(instance.scanner, scanner[0], result) return instances[0], instances[1:] def test_scanner(self): chango = BackwardCompatibleChanGo(MagicMock(), [MagicMock(), MagicMock()]) assert isinstance(chango.scanner, BackwardCompatibleVersionScanner) def test_build_template_change_note(self): expected_template = object() main_instance, legacy_instances = self.build_mocks( chango=("build_template_change_note", [expected_template, RuntimeError, RuntimeError]), scanner=None, ) chango = BackwardCompatibleChanGo(main_instance, legacy_instances) assert chango.build_template_change_note("slug") is expected_template main_instance.build_template_change_note.assert_called_once_with("slug", None) for legacy_instance in legacy_instances: assert not legacy_instance.build_template_change_note.called assert chango.build_template_change_note("slug", "uid") is expected_template main_instance.build_template_change_note.assert_called_with("slug", "uid") for legacy_instance in legacy_instances: assert not legacy_instance.build_template_change_note.called @pytest.mark.parametrize( ("is_available", "version_note", "expected"), [ ( [True, False, False], ["main-version-note", ChanGoError, ChanGoError], "main-version-note", ), ( [False, True, False], [ChanGoError, "legacy-version-note-1", ChanGoError], "legacy-version-note-1", ), ( [False, False, True], [ChanGoError, ChanGoError, "legacy-version-note-2"], "legacy-version-note-2", ), ], ) @pytest.mark.parametrize("version", ["version", None]) def test_build_version_note(self, is_available, version_note, version, expected): main_instance, legacy_instances = self.build_mocks( chango=("build_version_note", version_note), scanner=("is_available", is_available) ) chango = BackwardCompatibleChanGo(main_instance, legacy_instances) assert chango.build_version_note(version) == expected has_returned = False for instance, was_available in zip( [main_instance, *legacy_instances], is_available, strict=False ): if not has_returned: instance.scanner.is_available.assert_called_once_with(version) if was_available: instance.build_version_note.assert_called_once_with(version) else: assert not instance.build_version_note.called has_returned = has_returned or was_available else: assert not instance.scanner.is_available.called assert not instance.build_version_note.called def test_build_version_note_not_found(self): main_instance, legacy_instances = self.build_mocks( chango=("build_version_note", [ChanGoError, ChanGoError, ChanGoError]), scanner=("is_available", [False, False, False]), ) chango = BackwardCompatibleChanGo(main_instance, legacy_instances) with pytest.raises(ChanGoError): chango.build_version_note("version") for instance in [main_instance, *legacy_instances]: instance.scanner.is_available.assert_called_once_with("version") instance.build_version_note.assert_not_called() def test_build_version_history(self): expected_history = object() main_instance, legacy_instances = self.build_mocks( chango=("build_version_history", [expected_history, RuntimeError, RuntimeError]), scanner=None, ) chango = BackwardCompatibleChanGo(main_instance, legacy_instances) assert chango.build_version_history() is expected_history main_instance.build_version_history.assert_called_once_with() for legacy_instance in legacy_instances: assert not legacy_instance.build_version_history.called @pytest.mark.parametrize( ("results", "expected"), [ (["main-change-note", ChanGoError, ChanGoError], "main-change-note"), ([ChanGoError, "legacy-change-note-1", ChanGoError], "legacy-change-note-1"), ([ChanGoError, ChanGoError, "legacy-change-note-2"], "legacy-change-note-2"), ], ) def test_load_change_note(self, results, expected): main_instance, legacy_instances = self.build_mocks( chango=("load_change_note", [expected, ChanGoError, ChanGoError]), scanner=None ) chango = BackwardCompatibleChanGo(main_instance, legacy_instances) assert chango.load_change_note("uid") == expected has_returned = False for instance, result in zip([main_instance, *legacy_instances], results, strict=False): if not has_returned: instance.load_change_note.assert_called_once_with("uid") has_returned = has_returned or not isinstance(result, ChanGoError) else: assert not instance.load_change_note.called def test_load_change_note_not_found(self): main_instance, legacy_instances = self.build_mocks( chango=("load_change_note", [ChanGoError, ChanGoError, ChanGoError]), scanner=None ) chango = BackwardCompatibleChanGo(main_instance, legacy_instances) with pytest.raises(ChanGoError): chango.load_change_note("uid") for instance in [main_instance, *legacy_instances]: instance.load_change_note.assert_called_once_with("uid") def test_get_write_directory(self): expected_directory = object() main_instance, legacy_instances = self.build_mocks( chango=("get_write_directory", [expected_directory, RuntimeError, RuntimeError]), scanner=None, ) chango = BackwardCompatibleChanGo(main_instance, legacy_instances) assert chango.get_write_directory("change_note", "version") is expected_directory main_instance.get_write_directory.assert_called_once_with("change_note", "version") for legacy_instance in legacy_instances: assert not legacy_instance.get_write_directory.called def test_build_github_event_change_note(self): expected = object() main_instance, legacy_instances = self.build_mocks( chango=("build_github_event_change_note", [expected, RuntimeError, RuntimeError]), scanner=None, ) chango = BackwardCompatibleChanGo(main_instance, legacy_instances) call_args = [[("event", "data"), ("event", "data")], [("event",), ("event", None)]] for args, expected_args in call_args: assert chango.build_github_event_change_note(*args) is expected main_instance.build_github_event_change_note.assert_called_with(*expected_args) for legacy_instance in legacy_instances: assert not legacy_instance.build_github_event_change_note.called chango-0.6.0/tests/concrete/test_backwardcompatibleversionscanner.py000066400000000000000000000165221507376567100261560ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT import datetime as dtm from unittest.mock import MagicMock import pytest from chango import Version from chango.concrete import BackwardCompatibleVersionScanner from chango.error import ChanGoError class TestBackwardCompatibleVersionScanner: @staticmethod def build_mock_scanners(method_name: str, expected_results: list[object]) -> list[MagicMock]: mocks = [MagicMock() for _ in range(len(expected_results))] for mock, result in zip(mocks, expected_results, strict=False): method = getattr(mock, method_name) if isinstance(result, Exception): method.side_effect = result else: method.return_value = result return mocks @pytest.mark.parametrize( ("results", "expected"), [ ([True, False], True), ([False, False], False), ([False, True], True), ([True, True], True), ], ) @pytest.mark.parametrize("version", ["1.0.0", "2.0.0", None]) def test_is_available(self, results, expected, version): scanners = self.build_mock_scanners("is_available", results) scanner = BackwardCompatibleVersionScanner(scanners) assert scanner.is_available(version) == expected was_true = False for scanner, result in zip(scanners, results, strict=False): if not was_true: scanner.is_available.assert_called_once_with(version) was_true = was_true or result else: assert not scanner.is_available.called @pytest.mark.parametrize( ("results", "expected"), [ ([True, False], True), ([False, False], False), ([False, True], True), ([True, True], True), ], ) def test_has_unreleased_changes(self, results, expected): scanners = self.build_mock_scanners("has_unreleased_changes", results) scanner = BackwardCompatibleVersionScanner(scanners) assert scanner.has_unreleased_changes() == expected was_true = False for scanner, result in zip(scanners, results, strict=False): if not was_true: scanner.has_unreleased_changes.assert_called_once_with() was_true = was_true or result else: assert not scanner.has_unreleased_changes.called @pytest.mark.parametrize( ("results", "expected"), [ ( [ Version("1.0.0", date=dtm.date(2025, 1, 1)), Version("1.0.0", date=dtm.date(2025, 1, 10)), ], Version("1.0.0", date=dtm.date(2025, 1, 10)), ), ( [ Version("1.0.0", date=dtm.date(2025, 1, 10)), Version("1.0.0", date=dtm.date(2025, 1, 1)), ], Version("1.0.0", date=dtm.date(2025, 1, 10)), ), ( [ ChanGoError("No versions available."), Version("1.0.0", date=dtm.date(2025, 1, 10)), ], Version("1.0.0", date=dtm.date(2025, 1, 10)), ), ( [ChanGoError("No versions available."), ChanGoError("No versions available.")], ChanGoError("No versions available."), ), ], ) def test_get_latest_version(self, results, expected): scanners = self.build_mock_scanners("get_latest_version", results) scanner = BackwardCompatibleVersionScanner(scanners) if isinstance(expected, ChanGoError): with pytest.raises(ChanGoError, match=r"No versions available."): scanner.get_latest_version() else: assert scanner.get_latest_version() == expected for scanner in scanners: scanner.get_latest_version.assert_called_once_with() @pytest.mark.parametrize( ("results", "expected"), [ ( [ (Version("1.0.0", dtm.date.today()), Version("1.0.1", dtm.date.today())), (Version("2.0.0", dtm.date.today()), Version("2.0.1", dtm.date.today())), ], ( Version("1.0.0", dtm.date.today()), Version("1.0.1", dtm.date.today()), Version("2.0.0", dtm.date.today()), Version("2.0.1", dtm.date.today()), ), ), ( [(Version("1.0.0", dtm.date.today()), Version("1.0.1", dtm.date.today())), []], (Version("1.0.0", dtm.date.today()), Version("1.0.1", dtm.date.today())), ), ], ) def test_get_available_versions(self, results, expected): scanners = self.build_mock_scanners("get_available_versions", results) scanner = BackwardCompatibleVersionScanner(scanners) start_from = object() end_at = object() assert scanner.get_available_versions(start_from=start_from, end_at=end_at) == expected for scanner in scanners: scanner.get_available_versions.assert_called_once_with(start_from, end_at) @pytest.mark.parametrize( ("results", "expected"), [ ( [ ChanGoError("Change note '1.0.0' not available."), ChanGoError("Change note '1.0.0' not available."), ], ChanGoError("Change note '1.0.0' not available."), ), ( [ "ReturnValueA", ChanGoError("Change note '1.0.0' not available."), "ReturnValueB", ], "ReturnValueA", ), ( [ ChanGoError("Change note '1.0.0' not available."), "ReturnValueA", "ReturnValueB", ], "ReturnValueA", ), ], ) @pytest.mark.parametrize("method_name", ["lookup_change_note", "get_changes"]) def test_lookup_change_note_get_changes(self, results, expected, method_name): scanners = self.build_mock_scanners(method_name, results) scanner = BackwardCompatibleVersionScanner(scanners) method_object = getattr(scanner, method_name) uid = object() if isinstance(expected, ChanGoError): with pytest.raises(ChanGoError, match=r"not available."): method_object(uid) else: assert method_object(uid) == expected has_returned = False for scanner, result in zip(scanners, results, strict=False): if not has_returned: getattr(scanner, method_name).assert_called_once_with(uid) has_returned = has_returned or not isinstance(result, ChanGoError) else: assert not getattr(scanner, method_name).called def test_invalidate_caches(self): scanners = self.build_mock_scanners("invalidate_caches", [None, None]) scanner = BackwardCompatibleVersionScanner(scanners) scanner.invalidate_caches() for scanner in scanners: scanner.invalidate_caches.assert_called_once_with() chango-0.6.0/tests/concrete/test_commentchangenote.py000066400000000000000000000070531507376567100230550ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT import pytest from chango.concrete import CommentChangeNote from chango.constants import MarkupLanguage class TestCommentChangeNote: change_note = CommentChangeNote(slug="slug", comment="comment ð›™ðŒ¢ð‘") def test_init(self): assert self.change_note.comment == "comment ð›™ðŒ¢ð‘" def test_file_extension(self): assert self.change_note.file_extension == CommentChangeNote.MARKUP def test_from_string(self): change_note = CommentChangeNote.from_string("slug", "uid", "comment") assert change_note.comment == "comment" @pytest.mark.parametrize("encoding", ["utf-8", "utf-16"]) def test_to_bytes(self, encoding): assert self.change_note.to_bytes(encoding) == ( b"comment \xf0\x9d\x9b\x99\xf0\x9d\x8c\xa2\xf0\x91\x81\x8d" if encoding == "utf-8" else ( b"\xff\xfec\x00o\x00m\x00m\x00e\x00n\x00t\x00 " b'\x005\xd8\xd9\xde4\xd8"\xdf\x04\xd8M\xdc' ) ) @pytest.mark.parametrize("encoding", ["utf-8", "utf-16"]) def test_to_string(self, encoding): assert self.change_note.to_string(encoding=encoding) == "comment ð›™ðŒ¢ð‘" def test_build_template(self): change_note = CommentChangeNote.build_template("slug", "uid") assert change_note.comment == "example comment" assert change_note.slug == "slug" assert change_note.uid == "uid" def test_build_template_no_uid(self): change_note = CommentChangeNote.build_template("slug") assert change_note.comment == "example comment" assert change_note.slug == "slug" assert isinstance(change_note.uid, str) assert len(change_note.uid) > 0 def test_build_from_github_event_missing_data(self): with pytest.raises(ValueError, match="required data"): CommentChangeNote.build_from_github_event({}) def test_build_from_github_event_unsupported_language(self, monkeypatch): monkeypatch.setattr(CommentChangeNote, "MARKUP", "unsupported markup language") with pytest.raises(ValueError, match="unsupported markup language"): CommentChangeNote.build_from_github_event( { "pull_request": { "html_url": "https://example.com/pull/42", "number": 42, "title": "example title", } } ) @pytest.mark.parametrize( ("language", "expected"), [ (MarkupLanguage.TEXT, "example title (https://example.com/pull/42)"), (MarkupLanguage.MARKDOWN, "example title ([#42](https://example.com/pull/42))"), ( MarkupLanguage.RESTRUCTUREDTEXT, "example title (`#42 `_)", ), (MarkupLanguage.HTML, 'example title (#42)'), ], ) def test_build_from_github_event(self, language, expected, monkeypatch): monkeypatch.setattr(CommentChangeNote, "MARKUP", language) event_data = { "pull_request": { "html_url": "https://example.com/pull/42", "number": 42, "title": "example title", } } change_note = CommentChangeNote.build_from_github_event(event_data) assert change_note.comment == expected assert change_note.slug == "0042" chango-0.6.0/tests/concrete/test_commentversionnote.py000066400000000000000000000040321507376567100233070ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT import datetime as dtm import pytest from chango import Version from chango.concrete import CommentChangeNote, CommentVersionNote from chango.error import UnsupportedMarkupError class TestCommentVersionNote: comments = ("comment 1", "a\nmulti-line\ncomment 2", "comment 3") change_notes = tuple( CommentChangeNote(slug=f"slug-{i}", comment=comment) for i, comment in enumerate(comments) ) @pytest.fixture( autouse=True, params=[None, Version(uid="1.0.0", date=dtm.date(2024, 1, 1))], ids=["None", "Version"], ) def setup(self, request): # This is the next best thing to parametrizing __init__ that I could find # in reasonable time version = request.param self.version = version self.version_note = CommentVersionNote(version=version) for change_note in self.change_notes: self.version_note.add_change_note(change_note) def test_unsupported_markup(self): with pytest.raises(UnsupportedMarkupError): self.version_note.render("unsupported markup") def test_markdown(self): rendered = self.version_note.render("markdown") assert ( rendered == """- comment 1 - a multi-line comment 2 - comment 3""" ) def test_html(self): rendered = self.version_note.render("html") assert ( rendered == """
  • comment 1
  • a
    multi-line
    comment 2
  • comment 3
""" ) def test_restructuredtext(self): rendered = self.version_note.render("restructuredtext") assert ( rendered == """- comment 1 - a multi-line comment 2 - comment 3""" ) def test_fallback(self): rendered = self.version_note.render("text") assert ( rendered == """comment 1 a multi-line comment 2 comment 3""" ) chango-0.6.0/tests/concrete/test_directorychango.py000066400000000000000000000265041507376567100225450ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT import datetime as dtm import pytest import shortuuid from chango import Version from chango.action import ChanGoActionData, ParentPullRequest from chango.concrete import ( CommentChangeNote, CommentVersionNote, DirectoryChanGo, DirectoryVersionScanner, HeaderVersionHistory, ) from chango.concrete.sections import ( GitHubSectionChangeNote, PullRequest, Section, SectionVersionNote, ) from chango.error import ChanGoError from chango.helpers import ensure_uid from tests.auxil.files import data_path @pytest.fixture(scope="module") def scanner() -> DirectoryVersionScanner: return DirectoryVersionScanner(TestDirectoryChango.DATA_ROOT, "unreleased") @pytest.fixture(scope="module") def chango(scanner) -> DirectoryChanGo: return DirectoryChanGo( change_note_type=CommentChangeNote, version_note_type=CommentVersionNote, version_history_type=HeaderVersionHistory, scanner=scanner, ) DummySectionChangeNote = GitHubSectionChangeNote.with_sections( [ Section(uid="req_0", title="req_0", is_required=True), Section(uid="opt_0", title="opt_0", is_required=False), Section(uid="opt_1", title="opt_1", is_required=False), ] ) @pytest.fixture(scope="module") def section_scanner() -> DirectoryVersionScanner: return DirectoryVersionScanner(TestDirectoryChango.SECTION_DATA_ROOT, "unreleased") @pytest.fixture(scope="module") def section_chango(section_scanner) -> DirectoryChanGo: return DirectoryChanGo( change_note_type=DummySectionChangeNote, version_note_type=SectionVersionNote, version_history_type=HeaderVersionHistory, scanner=section_scanner, ) class TestDirectoryChango: DATA_ROOT = data_path("directoryversionscanner") SECTION_DATA_ROOT = data_path("directoryversionscanner-sections") def test_init_basic(self, chango, scanner): assert chango.directory_format == "{uid}_{date}" assert chango.change_note_type == CommentChangeNote assert chango.version_note_type == CommentVersionNote assert chango.version_history_type == HeaderVersionHistory assert chango.scanner is scanner def test_init_custom_format(self, scanner): chango = DirectoryChanGo( change_note_type=CommentChangeNote, version_note_type=CommentVersionNote, version_history_type=HeaderVersionHistory, scanner=scanner, directory_format="{uid} custom {date}", ) assert chango.directory_format == "{uid} custom {date}" @pytest.mark.parametrize("uid", [None, "uid"]) def test_build_template_change_note(self, chango, uid): note = chango.build_template_change_note("slug", uid) assert isinstance(note, CommentChangeNote) assert note.slug == "slug" if uid is not None: assert note.uid == uid else: assert isinstance(note.uid, str) assert len(note.uid) == len(shortuuid.ShortUUID().uuid()) class TestBuildGitHubEeventChangeNote: def test_basic(self, chango): event = { "pull_request": { "html_url": "https://example.com/pull/42", "number": 42, "title": "example title", } } note = chango.build_github_event_change_note(event) assert isinstance(note, CommentChangeNote) assert isinstance(note.uid, str) assert len(note.uid) == len(shortuuid.ShortUUID().uuid()) @pytest.mark.parametrize( "data", [ pytest.param(None, id="None"), pytest.param({"some": "dict"}, id="dict"), pytest.param( ChanGoActionData(parent_pull_request=None, linked_issues=None), id="ChanGoActionData without parent PR", ), ], ) def test_section_change_note_early_exit(self, chango, data): event = { "pull_request": { "html_url": "https://example.com/pull/42", "number": 42, "title": "example title", } } note = chango.build_github_event_change_note(event, data) assert isinstance(note, CommentChangeNote) assert isinstance(note.uid, str) assert len(note.uid) == len(shortuuid.ShortUUID().uuid()) def test_section_change_note_no_existing(self, section_chango): event = { "pull_request": { "html_url": "https://example.com/pull/42", "number": 33, "title": "example title", "user": {"login": "author"}, } } data = ChanGoActionData( parent_pull_request=ParentPullRequest( url="https://example.com/pull/43", number=50, title="example title", state="OPEN", author_login="author", ), linked_issues=None, ) note = section_chango.build_github_event_change_note(event, data) assert isinstance(note, DummySectionChangeNote) assert note.slug == "0033" assert note.pull_requests == ( PullRequest(uid="33", author_uids=("author",), closes_threads=()), ) assert note.req_0 == "example title" @pytest.mark.parametrize("modifies", [True, False]) def test_section_change_note_with_existing(self, section_chango, modifies): event = { "pull_request": { "html_url": "https://example.com/pull/42", "number": 101, "title": "example title" if modifies else "req_0", "user": {"login": "author"}, } } note = section_chango.build_github_event_change_note(event, None) if not modifies: assert note is None return assert isinstance(note, DummySectionChangeNote) assert note.slug == "0101" assert note.uid == "abcuc4HVWKycYccXAcM3xc" assert note.pull_requests == ( PullRequest(uid="101", author_uids=("author",), closes_threads=()), ) assert note.req_0 == "example title" def test_section_change_note_with_existing_parent(self, section_chango, monkeypatch): def get_sections(*args, **kwargs): # noqa: ARG001 return {"req_0", "opt_0"} monkeypatch.setattr(DummySectionChangeNote, "get_sections", get_sections) event = { "pull_request": { "html_url": "https://example.com/pull/42", "number": 45, "title": "example title", "user": {"login": "author"}, } } data = ChanGoActionData( parent_pull_request=ParentPullRequest( url="https://example.com/pull/43", number=43, title="example title", state="OPEN", author_login="author", ), linked_issues=None, ) note = section_chango.build_github_event_change_note(event, data) assert isinstance(note, DummySectionChangeNote) assert note.slug == "0043" assert note.pull_requests == ( PullRequest( uid="43", author_uids=("parent_author",), closes_threads=("existing_thread1", "existing_thread2"), ), PullRequest(uid="45", author_uids=("author",), closes_threads=()), ) assert note.req_0 == "existing_req_0\nexample title" assert note.opt_0 == "example title" assert note.opt_1 == "existing_opt_1" def test_build_version_note_version(self, chango): version = Version("uid", dtm.date.today()) note = chango.build_version_note(version) assert isinstance(note, CommentVersionNote) assert note.version == version def test_build_version_note_none(self, chango): note = chango.build_version_note(None) assert isinstance(note, CommentVersionNote) assert note.version is None def test_build_version_note_sections(self, section_chango): note = section_chango.build_version_note(None) assert isinstance(note, SectionVersionNote) assert note.version is None def test_build_version_history(self, chango): history = chango.build_version_history() assert isinstance(history, HeaderVersionHistory) @pytest.mark.parametrize("idx", [1, 2, 3]) def test_load_change_note(self, chango, idx): uid = f"uid_1-{idx}_0" change_note = chango.load_change_note(uid) assert isinstance(change_note, CommentChangeNote) assert change_note.uid == uid assert change_note.slug == "comment-change-note" path = ( self.DATA_ROOT / f"1.{idx}_2024-01-0{idx}" / f"comment-change-note.uid_1-{idx}_0.txt" ) assert change_note.comment == path.read_text() @pytest.mark.parametrize( "version", [ None, "1.1", Version("1.1", dtm.date(2024, 1, 1)), "1.2", Version("1.2", dtm.date(2024, 1, 2)), ], ) @pytest.mark.parametrize( "change_note", ["str_change_note", CommentChangeNote.build_template("slug", "uid")] ) def test_get_write_directory(self, chango, version, change_note): if version is None: expected_path = chango.scanner.unreleased_directory else: version_uid = ensure_uid(version) day = int(version_uid.split(".")[-1]) expected_path = chango.scanner.base_directory / f"{version_uid}_2024-01-0{day}" path_existed = expected_path.is_dir() try: path = chango.get_write_directory(change_note, version) assert path.is_dir() finally: # Clean up if (expected_path is not None) and (not path_existed): expected_path.rmdir() @pytest.mark.parametrize( "change_note", ["str_change_note", CommentChangeNote.build_template("slug", "uid")] ) def test_get_write_directory_new_version(self, chango, change_note): version = Version("new-version", dtm.date(2024, 1, 17)) expected_path = chango.scanner.base_directory / "new-version_2024-01-17" try: path = chango.get_write_directory(change_note, version) assert path.is_dir() assert path == expected_path finally: # Clean up expected_path.rmdir() @pytest.mark.parametrize( "change_note", ["str_change_note", CommentChangeNote.build_template("slug", "uid")] ) def test_get_write_directory_new_str_version(self, chango, change_note): with pytest.raises(ChanGoError, match=r"'new-version' not available."): chango.get_write_directory(change_note, "new-version") chango-0.6.0/tests/concrete/test_directoryversionscanner.py000066400000000000000000000165241507376567100243460ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT import datetime as dtm from pathlib import Path import pytest from chango import Version from chango.concrete import DirectoryVersionScanner from chango.error import ChanGoError from tests.auxil.files import data_path @pytest.fixture def scanner() -> DirectoryVersionScanner: return DirectoryVersionScanner(TestDirectoryVersionScanner.DATA_ROOT, "unreleased") @pytest.fixture def scanner_no_unreleased() -> DirectoryVersionScanner: return DirectoryVersionScanner(TestDirectoryVersionScanner.DATA_ROOT, "no-unreleased") class TestDirectoryVersionScanner: DATA_ROOT = data_path("directoryversionscanner") def test_init_basic(self): scanner = DirectoryVersionScanner(self.DATA_ROOT, "unreleased") assert scanner.base_directory == self.DATA_ROOT assert scanner.unreleased_directory == self.DATA_ROOT / "unreleased" scanner = DirectoryVersionScanner(self.DATA_ROOT, self.DATA_ROOT / "unreleased") assert scanner.base_directory == self.DATA_ROOT assert scanner.unreleased_directory == self.DATA_ROOT / "unreleased" def test_init_relative(self): scanner = DirectoryVersionScanner("../data/directoryversionscanner", "unreleased") assert scanner.base_directory == self.DATA_ROOT assert scanner.unreleased_directory == self.DATA_ROOT / "unreleased" def test_base_directory_not_exists(self): with pytest.raises(ValueError, match="does not exist"): DirectoryVersionScanner("does_not_exist", "unreleased") def test_unreleased_directory_not_exists(self): with pytest.raises(ValueError, match="does not exist"): DirectoryVersionScanner(self.DATA_ROOT, "does_not_exist") @pytest.mark.parametrize( ("version", "expected"), [ ("1.1", True), (Version("1.1", dtm.date(2024, 1, 1)), True), (Version("1.1", dtm.date(2024, 5, 1)), False), ("1.2", True), (Version("1.2", dtm.date(2024, 1, 2)), True), (Version("1.2", dtm.date(2024, 5, 1)), False), ("1.3", True), (Version("1.3", dtm.date(2024, 1, 3)), True), (Version("1.3", dtm.date(2024, 5, 1)), False), ("1.3.1", True), (Version("1.3.1", dtm.date(2024, 1, 3)), True), (Version("1.3.1", dtm.date(2024, 5, 1)), False), (None, True), ("1.4", False), ("1.0", False), ], ) def test_is_available(self, scanner, version, expected): assert scanner.is_available(version) == expected def test_is_available_no_unreleased(self, scanner_no_unreleased): assert scanner_no_unreleased.is_available(None) is False def test_has_unreleased_changes(self, scanner, scanner_no_unreleased): assert scanner.has_unreleased_changes() is True assert scanner_no_unreleased.has_unreleased_changes() is False def test_get_latest_version(self, scanner): # It's important that we get 1.3.1 here and not 1.3, since both are released # on the same day such that lexicographical sorting kicks in! latest = scanner.get_latest_version() assert latest.uid == "1.3.1" assert latest.date == dtm.date(2024, 1, 3) def test_get_latest_version_nothing_released(self): scanner = DirectoryVersionScanner(self.DATA_ROOT / "no-released", "unreleased") with pytest.raises(ChanGoError, match="No versions available"): scanner.get_latest_version() def test_get_available_versions(self, scanner): assert set(scanner.get_available_versions()) == { Version("1.1", dtm.date(2024, 1, 1)), Version("1.2", dtm.date(2024, 1, 2)), Version("1.3", dtm.date(2024, 1, 3)), Version("1.3.1", dtm.date(2024, 1, 3)), } def test_get_available_versions_start_from(self, scanner): assert set(scanner.get_available_versions(start_from="1.2")) == { Version("1.2", dtm.date(2024, 1, 2)), Version("1.3", dtm.date(2024, 1, 3)), Version("1.3.1", dtm.date(2024, 1, 3)), } def test_get_available_versions_end_at(self, scanner): assert set(scanner.get_available_versions(end_at="1.2")) == { Version("1.1", dtm.date(2024, 1, 1)), Version("1.2", dtm.date(2024, 1, 2)), } @pytest.mark.parametrize("idx", [1, 2, 3]) def test_lookup_change_note(self, scanner, idx): change_note = scanner.lookup_change_note(f"uid_1-{idx}_0") assert change_note.uid == f"uid_1-{idx}_0" assert change_note.version == Version(f"1.{idx}", dtm.date(2024, 1, idx)) assert ( change_note.file_path == self.DATA_ROOT / f"1.{idx}_2024-01-0{idx}" / f"comment-change-note.uid_1-{idx}_0.txt" ) def test_lookup_change_note_unreleased(self, scanner): change_note = scanner.lookup_change_note("uid_ur_0") assert change_note.uid == "uid_ur_0" assert change_note.version is None assert ( change_note.file_path == self.DATA_ROOT / "unreleased" / "comment-change-note.uid_ur_0.txt" ) def test_lookup_change_note_not_found(self, scanner): with pytest.raises(ChanGoError, match="not found in any version"): scanner.lookup_change_note("unknown_uid") @pytest.mark.parametrize("idx", [1, 2, 3]) def test_get_version(self, scanner, idx): version = scanner.get_version(f"1.{idx}") assert version.uid == f"1.{idx}" assert version.date == dtm.date(2024, 1, idx) def test_get_version_not_found(self, scanner): with pytest.raises(ChanGoError, match="not available"): scanner.get_version("1.4") @pytest.mark.parametrize( "version", [ "1.1", Version("1.1", dtm.date(2024, 1, 1)), "1.2", Version("1.2", dtm.date(2024, 1, 2)), "1.3", Version("1.3", dtm.date(2024, 1, 3)), "1.3.1", Version("1.3.1", dtm.date(2024, 1, 3)), None, ], ) def test_get_changes(self, scanner, version): changes = set(scanner.get_changes(version)) uid = version.uid if isinstance(version, Version) else (version or "ur") assert changes == {f"uid_{uid.replace('.', '-')}_{idx}" for idx in range(3)} def test_get_changes_not_found(self, scanner): with pytest.raises(ChanGoError, match="not available"): scanner.get_changes("1.4") def test_invalidate_caches(self, scanner): original_versions = { Version("1.1", dtm.date(2024, 1, 1)), Version("1.2", dtm.date(2024, 1, 2)), Version("1.3", dtm.date(2024, 1, 3)), Version("1.3.1", dtm.date(2024, 1, 3)), } assert set(scanner.get_available_versions()) == original_versions new_directory = Path(self.DATA_ROOT / "1.4_2024-01-04") try: new_directory.mkdir() assert set(scanner.get_available_versions()) == original_versions scanner.invalidate_caches() assert set(scanner.get_available_versions()) == original_versions | { Version("1.4", dtm.date(2024, 1, 4)) } finally: new_directory.rmdir() chango-0.6.0/tests/concrete/test_headerversionhistory.py000066400000000000000000000056601507376567100236410ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT import datetime as dtm from pathlib import Path import pytest from chango import Version from chango.concrete import CommentChangeNote, CommentVersionNote, HeaderVersionHistory from chango.constants import MarkupLanguage from chango.error import UnsupportedMarkupError from tests.auxil.files import data_path class TestHeaderVersionHistory: comments = ("comment 1", "a\nmulti-line\ncomment 2", "comment 3") @staticmethod def get_expected_file(unreleased_changes: bool, markup: MarkupLanguage) -> Path: unreleased_prefix = "with-unreleased" if unreleased_changes else "without-unreleased" file_name = f"{unreleased_prefix}.{markup}" return data_path(Path("headerversionhistory") / file_name) @classmethod def get_expected_string(cls, unreleased_changes: bool, markup: MarkupLanguage) -> str: return cls.get_expected_file(unreleased_changes, markup).read_text() def get_version_notes(self, unreleased_changes: bool) -> list[CommentVersionNote]: version_notes = [] for j, version_number in enumerate(range(3)): version = Version(uid=f"1.0.{version_number}", date=dtm.date(2024, 1, 1 + j)) version_note = CommentVersionNote(version=version) version_notes.append(version_note) for i, comment in enumerate(self.comments): change_note = CommentChangeNote(slug=f"slug-{i}", comment=comment) version_note.add_change_note(change_note) if not unreleased_changes: return version_notes unreleased_version_note = CommentVersionNote(version=None) version_notes.append(unreleased_version_note) for i, comment in enumerate(self.comments): change_note = CommentChangeNote(slug=f"slug-{i}", comment=comment) unreleased_version_note.add_change_note(change_note) return version_notes @pytest.mark.parametrize( "markup", [MarkupLanguage.MARKDOWN, MarkupLanguage.HTML, MarkupLanguage.RESTRUCTUREDTEXT] ) @pytest.mark.parametrize( "unreleased_changes", [False, True], ids=["without-unreleased", "with-unreleased"] ) def test_expected_output(self, unreleased_changes: bool, markup: MarkupLanguage): version_notes = self.get_version_notes(unreleased_changes=unreleased_changes) version_history = HeaderVersionHistory() for version_note in version_notes: version_history.add_version_note(version_note) expected = self.get_expected_string(unreleased_changes=unreleased_changes, markup=markup) assert version_history.render(markup) == expected def test_unsupported_markup(self): version_history = HeaderVersionHistory() with pytest.raises(UnsupportedMarkupError, match="Got unsupported markup 'unsupported'"): version_history.render("unsupported") chango-0.6.0/tests/config/000077500000000000000000000000001507376567100154045ustar00rootroot00000000000000chango-0.6.0/tests/config/__init__.py000066400000000000000000000001601507376567100175120ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT chango-0.6.0/tests/config/test_changoconfig.py000066400000000000000000000121311507376567100214400ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT import sys from collections.abc import Collection from contextlib import nullcontext from pathlib import Path from string import Template from types import SimpleNamespace import pytest from chango.concrete import BackwardCompatibleChanGo from chango.config import ChanGoConfig, ChanGoInstanceConfig from tests.auxil.files import PROJECT_ROOT_PATH, data_path, temporary_chdir class TestChanGoInstanceConfig: chango_instance = ChanGoInstanceConfig(name="name", module="module", package="package") def test_init(self, tmp_path): # To ensure that there is pyproject.toml in the current directory that would interfere # with the test. with temporary_chdir(tmp_path): config = ChanGoConfig(sys_path=Path("sys_path"), chango_instance=self.chango_instance) assert config.sys_path == Path("sys_path") assert config.chango_instance is self.chango_instance def test_init_required(self, tmp_path): # To ensure that there is pyproject.toml in the current directory that would interfere # with the test. with temporary_chdir(tmp_path): config = ChanGoConfig(chango_instance=self.chango_instance) assert config.sys_path is None assert config.chango_instance is self.chango_instance @pytest.mark.parametrize( "path", [ None, data_path("config/pyproject.toml"), data_path("config/pyproject.toml").as_posix(), data_path("config/pyproject.toml").relative_to(Path.cwd(), walk_up=True), data_path("config/pyproject.toml").relative_to(Path.cwd(), walk_up=True).as_posix(), data_path("config"), data_path("config").as_posix(), ], ids=[ "None", "absolute", "absolute-string", "relative", "relative-string", "directory", "directory-string", ], ) def test_load_path_input(self, path): with temporary_chdir(data_path("config")) if path is None else nullcontext(): config = ChanGoConfig.load(path) assert config.sys_path == Path("/abs/sys_path").absolute() assert config.chango_instance == self.chango_instance @pytest.mark.parametrize( ("sys_path", "expected"), [ (None, None), (Path("/abs/sys_path").absolute(), Path("/abs/sys_path").absolute()), (Path("relative"), data_path("config/relative").absolute()), (Path("../relative"), data_path("relative").absolute()), ], ids=["None", "absolute", "relative", "relative-parent"], ) def test_load_sys_path_output(self, sys_path, expected): tmp_file = data_path("config/tmp.toml") try: sys_path_entry = f"sys_path = '{sys_path}'" if sys_path else "" template = Template( data_path("config/pyproject.toml.template").read_text(encoding="utf-8") ) tmp_file.write_text( template.substitute(sys_path_entry=sys_path_entry), encoding="utf-8" ) config = ChanGoConfig.load(tmp_file) assert config.sys_path == expected finally: tmp_file.unlink() def test_import_chango_instance_basic(self): # Testing with importlib is a bit of a hassle, and also we don't want to test # the actual import, but the import logic. So here we just test that the # config of the chango repo itself is imported correctly. with temporary_chdir(PROJECT_ROOT_PATH): config = ChanGoConfig.load() chango_instance = config.import_chango_instance() assert isinstance(chango_instance, BackwardCompatibleChanGo) @pytest.mark.parametrize( ("sys_path", "expected"), [ (None, None), (Path("/abs/sys_path").absolute(), Path("/abs/sys_path").absolute()), (Path("relative"), Path.cwd() / "relative"), ], ids=["None", "relative", "relative-parent"], ) def test_import_chango_instance_sys_path_input(self, monkeypatch, sys_path, expected): original_sys_path = sys.path.copy() # Here we test that the sys_path is added to the system path correctly. def is_in_sys_path(system_path: Collection[str], search_path: Path): sys_paths = map(Path, system_path) return any(search_path == path for path in sys_paths) def import_module(*_, **__): return SimpleNamespace(name=sys.path.copy()) monkeypatch.setattr("importlib.import_module", import_module) reported_path = ChanGoConfig( sys_path=sys_path, chango_instance=self.chango_instance ).import_chango_instance() if expected is not None: assert is_in_sys_path(reported_path, expected) else: assert reported_path == original_sys_path # Check that everything was restored correctly assert sys.path == original_sys_path chango-0.6.0/tests/config/test_changoinstanceconfig.py000066400000000000000000000012051507376567100231650ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT from chango.config import ChanGoInstanceConfig class TestChanGoInstanceConfig: def test_init(self): config = ChanGoInstanceConfig(name="name", module="module", package="package") assert config.name == "name" assert config.module == "module" assert config.package == "package" def test_init_required(self): config = ChanGoInstanceConfig(name="name", module="module") assert config.name == "name" assert config.module == "module" assert config.package is None chango-0.6.0/tests/config/test_config.py000066400000000000000000000020471507376567100202650ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT import os from contextlib import contextmanager from pathlib import Path from types import SimpleNamespace from chango.config import ChanGoConfig, get_chango_instance @contextmanager def temporary_chdir(path: Path): current_dir = Path.cwd() try: os.chdir(path) yield finally: os.chdir(current_dir) class TestConfigModule: def test_get_chango_instance_caching(self, monkeypatch): call_count = 0 def import_chango_instance(): nonlocal call_count call_count += 1 return call_count def load(*_, **__): return SimpleNamespace(import_chango_instance=import_chango_instance) monkeypatch.setattr(ChanGoConfig, "load", load) for _ in range(10): assert get_chango_instance() == 1, "The ChanGo instance should be cached!" # to ensure that other tests still pass get_chango_instance.cache_clear() chango-0.6.0/tests/conftest.py000066400000000000000000000004211507376567100163330ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT # INFO: # Best reference for how use sphinx testing so far is # https://github.com/sphinx-doc/sphinx/issues/7008 pytest_plugins = ["sphinx.testing.fixtures"] chango-0.6.0/tests/data/000077500000000000000000000000001507376567100150505ustar00rootroot00000000000000chango-0.6.0/tests/data/comment-change-note-utf16.uid.txt000066400000000000000000000001021507376567100231550ustar00rootroot00000000000000ÿþthis is an utf-16 comment 5ØÙÞ4Ø"ߨMÜchango-0.6.0/tests/data/comment-change-note.uid.txt000066400000000000000000000000211507376567100222120ustar00rootroot00000000000000this is a commentchango-0.6.0/tests/data/config/000077500000000000000000000000001507376567100163155ustar00rootroot00000000000000chango-0.6.0/tests/data/config/pyproject.toml000066400000000000000000000001641507376567100212320ustar00rootroot00000000000000[tool.chango] sys_path = "/abs/sys_path" chango_instance = { name= "name", module = "module", package = "package" } chango-0.6.0/tests/data/config/pyproject.toml.template000066400000000000000000000001511507376567100230400ustar00rootroot00000000000000[tool.chango] $sys_path_entry chango_instance = { name= "name", module = "module", package = "package" } chango-0.6.0/tests/data/directoryversionscanner-sections/000077500000000000000000000000001507376567100236615ustar00rootroot00000000000000chango-0.6.0/tests/data/directoryversionscanner-sections/unreleased/000077500000000000000000000000001507376567100260105ustar00rootroot00000000000000chango-0.6.0/tests/data/directoryversionscanner-sections/unreleased/0043.Zhpuc4HVWKycYccXAcM3xc.toml000066400000000000000000000003641507376567100332740ustar00rootroot00000000000000req_0 = "existing_req_0" opt_1 = "existing_opt_1" [[pull_requests]] uid = "43" author_uids = ["parent_author"] closes_threads = ["existing_thread1", "existing_thread2"] [[pull_requests]] uid = "45" author_uids = ["unknown"] closes_threads = [] chango-0.6.0/tests/data/directoryversionscanner-sections/unreleased/0101.abcuc4HVWKycYccXAcM3xc.toml000066400000000000000000000001061507376567100332450ustar00rootroot00000000000000req_0 = "req_0" [[pull_requests]] uid = "101" author_uids = ["author"]chango-0.6.0/tests/data/directoryversionscanner/000077500000000000000000000000001507376567100220345ustar00rootroot00000000000000chango-0.6.0/tests/data/directoryversionscanner/1.1_2024-01-01/000077500000000000000000000000001507376567100234365ustar00rootroot00000000000000chango-0.6.0/tests/data/directoryversionscanner/1.1_2024-01-01/comment-change-note.uid_1-1_0.txt000066400000000000000000000000211507376567100313750ustar00rootroot00000000000000this is a commentchango-0.6.0/tests/data/directoryversionscanner/1.1_2024-01-01/comment-change-note.uid_1-1_1.txt000066400000000000000000000000211507376567100313760ustar00rootroot00000000000000this is a commentchango-0.6.0/tests/data/directoryversionscanner/1.1_2024-01-01/comment-change-note.uid_1-1_2.txt000066400000000000000000000000211507376567100313770ustar00rootroot00000000000000this is a commentchango-0.6.0/tests/data/directoryversionscanner/1.1_2024-01-01/not-a-change-note.txt000066400000000000000000000000001507376567100273710ustar00rootroot00000000000000chango-0.6.0/tests/data/directoryversionscanner/1.1_2024-01-01/subdirectory-to-ignore/000077500000000000000000000000001507376567100300555ustar00rootroot00000000000000chango-0.6.0/tests/data/directoryversionscanner/1.1_2024-01-01/subdirectory-to-ignore/.gitkeep000066400000000000000000000000001507376567100314740ustar00rootroot00000000000000chango-0.6.0/tests/data/directoryversionscanner/1.2_2024-01-02/000077500000000000000000000000001507376567100234405ustar00rootroot00000000000000chango-0.6.0/tests/data/directoryversionscanner/1.2_2024-01-02/comment-change-note.uid_1-2_0.txt000066400000000000000000000000211507376567100314000ustar00rootroot00000000000000this is a commentchango-0.6.0/tests/data/directoryversionscanner/1.2_2024-01-02/comment-change-note.uid_1-2_1.txt000066400000000000000000000000211507376567100314010ustar00rootroot00000000000000this is a commentchango-0.6.0/tests/data/directoryversionscanner/1.2_2024-01-02/comment-change-note.uid_1-2_2.txt000066400000000000000000000000211507376567100314020ustar00rootroot00000000000000this is a commentchango-0.6.0/tests/data/directoryversionscanner/1.2_2024-01-02/not-a-change-note.txt000066400000000000000000000000001507376567100273730ustar00rootroot00000000000000chango-0.6.0/tests/data/directoryversionscanner/1.3.1_2024-01-03/000077500000000000000000000000001507376567100236015ustar00rootroot00000000000000chango-0.6.0/tests/data/directoryversionscanner/1.3.1_2024-01-03/comment-change-note.uid_1-3-1_0.txt000066400000000000000000000000211507376567100317000ustar00rootroot00000000000000this is a commentchango-0.6.0/tests/data/directoryversionscanner/1.3.1_2024-01-03/comment-change-note.uid_1-3-1_1.txt000066400000000000000000000000211507376567100317010ustar00rootroot00000000000000this is a commentchango-0.6.0/tests/data/directoryversionscanner/1.3.1_2024-01-03/comment-change-note.uid_1-3-1_2.txt000066400000000000000000000000211507376567100317020ustar00rootroot00000000000000this is a commentchango-0.6.0/tests/data/directoryversionscanner/1.3_2024-01-03/000077500000000000000000000000001507376567100234425ustar00rootroot00000000000000chango-0.6.0/tests/data/directoryversionscanner/1.3_2024-01-03/comment-change-note.uid_1-3_0.txt000066400000000000000000000000211507376567100314030ustar00rootroot00000000000000this is a commentchango-0.6.0/tests/data/directoryversionscanner/1.3_2024-01-03/comment-change-note.uid_1-3_1.txt000066400000000000000000000000211507376567100314040ustar00rootroot00000000000000this is a commentchango-0.6.0/tests/data/directoryversionscanner/1.3_2024-01-03/comment-change-note.uid_1-3_2.txt000066400000000000000000000000211507376567100314050ustar00rootroot00000000000000this is a commentchango-0.6.0/tests/data/directoryversionscanner/1.3_2024-01-03/not-a-change-note.txt000066400000000000000000000000001507376567100273750ustar00rootroot00000000000000chango-0.6.0/tests/data/directoryversionscanner/no-released/000077500000000000000000000000001507376567100242325ustar00rootroot00000000000000chango-0.6.0/tests/data/directoryversionscanner/no-released/unreleased/000077500000000000000000000000001507376567100263615ustar00rootroot00000000000000chango-0.6.0/tests/data/directoryversionscanner/no-released/unreleased/.gitkeep000066400000000000000000000000001507376567100300000ustar00rootroot00000000000000chango-0.6.0/tests/data/directoryversionscanner/no-unreleased/000077500000000000000000000000001507376567100245755ustar00rootroot00000000000000chango-0.6.0/tests/data/directoryversionscanner/no-unreleased/not-a-change-note.txt000066400000000000000000000000001507376567100305300ustar00rootroot00000000000000chango-0.6.0/tests/data/directoryversionscanner/unreleased/000077500000000000000000000000001507376567100241635ustar00rootroot00000000000000chango-0.6.0/tests/data/directoryversionscanner/unreleased/comment-change-note.uid_ur_0.txt000066400000000000000000000000211507376567100322520ustar00rootroot00000000000000this is a commentchango-0.6.0/tests/data/directoryversionscanner/unreleased/comment-change-note.uid_ur_1.txt000066400000000000000000000000211507376567100322530ustar00rootroot00000000000000this is a commentchango-0.6.0/tests/data/directoryversionscanner/unreleased/comment-change-note.uid_ur_2.txt000066400000000000000000000000211507376567100322540ustar00rootroot00000000000000this is a commentchango-0.6.0/tests/data/directoryversionscanner/unreleased/not-a-change-note.txt000066400000000000000000000000001507376567100301160ustar00rootroot00000000000000chango-0.6.0/tests/data/headerversionhistory/000077500000000000000000000000001507376567100213305ustar00rootroot00000000000000chango-0.6.0/tests/data/headerversionhistory/with-unreleased.html000066400000000000000000000007501507376567100253200ustar00rootroot00000000000000

Unreleased

unknown
  • comment 1
  • a
    multi-line
    comment 2
  • comment 3

1.0.2

2024-01-03
  • comment 1
  • a
    multi-line
    comment 2
  • comment 3

1.0.1

2024-01-02
  • comment 1
  • a
    multi-line
    comment 2
  • comment 3

1.0.0

2024-01-01
  • comment 1
  • a
    multi-line
    comment 2
  • comment 3
chango-0.6.0/tests/data/headerversionhistory/with-unreleased.markdown000066400000000000000000000005101507376567100261700ustar00rootroot00000000000000# Unreleased *unknown* - comment 1 - a multi-line comment 2 - comment 3 # 1.0.2 *2024-01-03* - comment 1 - a multi-line comment 2 - comment 3 # 1.0.1 *2024-01-02* - comment 1 - a multi-line comment 2 - comment 3 # 1.0.0 *2024-01-01* - comment 1 - a multi-line comment 2 - comment 3chango-0.6.0/tests/data/headerversionhistory/with-unreleased.rst000066400000000000000000000005151507376567100251630ustar00rootroot00000000000000Unreleased ========== *unknown* - comment 1 - a multi-line comment 2 - comment 3 1.0.2 ===== *2024-01-03* - comment 1 - a multi-line comment 2 - comment 3 1.0.1 ===== *2024-01-02* - comment 1 - a multi-line comment 2 - comment 3 1.0.0 ===== *2024-01-01* - comment 1 - a multi-line comment 2 - comment 3chango-0.6.0/tests/data/headerversionhistory/without-unreleased.html000066400000000000000000000005541507376567100260520ustar00rootroot00000000000000

1.0.2

2024-01-03
  • comment 1
  • a
    multi-line
    comment 2
  • comment 3

1.0.1

2024-01-02
  • comment 1
  • a
    multi-line
    comment 2
  • comment 3

1.0.0

2024-01-01
  • comment 1
  • a
    multi-line
    comment 2
  • comment 3
chango-0.6.0/tests/data/headerversionhistory/without-unreleased.markdown000066400000000000000000000003641507376567100267270ustar00rootroot00000000000000# 1.0.2 *2024-01-03* - comment 1 - a multi-line comment 2 - comment 3 # 1.0.1 *2024-01-02* - comment 1 - a multi-line comment 2 - comment 3 # 1.0.0 *2024-01-01* - comment 1 - a multi-line comment 2 - comment 3chango-0.6.0/tests/data/headerversionhistory/without-unreleased.rst000066400000000000000000000003641507376567100257150ustar00rootroot000000000000001.0.2 ===== *2024-01-03* - comment 1 - a multi-line comment 2 - comment 3 1.0.1 ===== *2024-01-02* - comment 1 - a multi-line comment 2 - comment 3 1.0.0 ===== *2024-01-01* - comment 1 - a multi-line comment 2 - comment 3chango-0.6.0/tests/data/sphinx_ext/000077500000000000000000000000001507376567100172415ustar00rootroot00000000000000chango-0.6.0/tests/data/sphinx_ext/conf_value.py.template000066400000000000000000000002221507376567100235420ustar00rootroot00000000000000from pathlib import Path extensions = [ "chango.sphinx_ext", ] # Configuration for the chango sphinx directive $chango_pyproject_toml_path chango-0.6.0/tests/data/sphinx_ext/index.rst.template000066400000000000000000000000131507376567100227060ustar00rootroot00000000000000$directive chango-0.6.0/tests/data/sphinx_ext/test-root/000077500000000000000000000000001507376567100212015ustar00rootroot00000000000000chango-0.6.0/tests/data/sphinx_ext/test-root/conf.py000066400000000000000000000002601507376567100224760ustar00rootroot00000000000000from pathlib import Path extensions = ["chango.sphinx_ext"] # Configuration for the chango sphinx directive chango_pyproject_toml_path = Path(__file__).parent.parent.parent chango-0.6.0/tests/data/sphinx_ext/test-root/index.rst000066400000000000000000000000131507376567100230340ustar00rootroot00000000000000.. chango::chango-0.6.0/tests/sphinx_ext/000077500000000000000000000000001507376567100163305ustar00rootroot00000000000000chango-0.6.0/tests/sphinx_ext/__init__.py000066400000000000000000000001601507376567100204360ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT chango-0.6.0/tests/sphinx_ext/conftest.py000066400000000000000000000053451507376567100205360ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT from pathlib import Path from types import SimpleNamespace import pytest from chango._utils.types import PathLike from chango.config import ChanGoInstanceConfig, get_chango_instance from tests.auxil.files import TEST_DATA_PATH # INFO: # Best reference for how use sphinx testing so far is # https://github.com/sphinx-doc/sphinx/issues/7008 @pytest.fixture(scope="session") def rootdir(): return TEST_DATA_PATH / "sphinx_ext" class MockStorage: def __init__(self): self.loaded_config: MockCGConfig | None = None def invalidate_storage(self): self.loaded_config = None def set_current_config(self, config: "MockCGConfig"): self.loaded_config = config def get(self) -> "MockCGConfig": if self.loaded_config is None: raise RuntimeError("No config loaded") return self.loaded_config @property def rendered_content(self): return self.loaded_config.chango.version_history.RENDERED_CONTENT CG_CONFIG_STORAGE = MockStorage() class MockVersionHistory(SimpleNamespace): RENDERED_CONTENT = "This is the rendered version history" def __init__(self): super().__init__() self.received_kwargs = None self.received_args = None def render(self, *args, **kwargs) -> str: self.received_args = args self.received_kwargs = kwargs return self.RENDERED_CONTENT class MockChanGo(SimpleNamespace): def __init__(self): super().__init__() self.received_kwargs = None self.received_args = None self.version_history = MockVersionHistory() def load_version_history( self, *args, start_from: str | None = None, end_at: str | None = None, **kwargs ) -> MockVersionHistory: self.received_args = args self.received_kwargs = kwargs | {"start_from": start_from, "end_at": end_at} return self.version_history class MockCGConfig(SimpleNamespace): def __init__(self, **kwargs): super().__init__(**kwargs) self.chango = MockChanGo() @classmethod def load(cls, path: PathLike | None): out = cls( sys_path=None if path is None else Path(path), chango_instance=ChanGoInstanceConfig(name="name", module="module"), ) CG_CONFIG_STORAGE.set_current_config(out) return out def import_chango_instance(self) -> MockChanGo: return self.chango @pytest.fixture(autouse=True) def cg_config_mock(monkeypatch): monkeypatch.setattr("chango.config.ChanGoConfig", MockCGConfig) yield CG_CONFIG_STORAGE get_chango_instance.cache_clear() CG_CONFIG_STORAGE.invalidate_storage() chango-0.6.0/tests/sphinx_ext/test_sphinx_ext.py000066400000000000000000000270241507376567100221370ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT import logging from collections.abc import Callable, Sequence from pathlib import Path from string import Template from typing import Annotated import pytest import shortuuid from _pytest.tmpdir import TempPathFactory from sphinx.testing.util import SphinxTestApp from sphinx.util.logging import pending_warnings from chango import __version__ from chango._utils.types import PathLike from chango.constants import MarkupLanguage from tests.auxil.files import data_path, path_to_python_string from tests.sphinx_ext.conftest import MockChanGo, MockStorage MAKE_APP_TYPE = Callable[..., SphinxTestApp] class SphinxBuildError(RuntimeError): pass class TestSphinxExt: SPHINX_EXT_TEST_ROOT = data_path("sphinx_ext") @staticmethod def assert_successful_build(app: SphinxTestApp): with pending_warnings() as mem_handler: app.build() for record in mem_handler.buffer: if record.levelno == logging.ERROR: raise SphinxBuildError(record.getMessage()) @classmethod def create_template( cls, *, tmp_path_factory: TempPathFactory, conf_value_insert: str | None = None, directive_insert: str = ".. chango::", ) -> Path: uid = shortuuid.uuid() src_dir = f"test-{uid}" tmp_dir = tmp_path_factory.mktemp(src_dir) conf_template = Template( (cls.SPHINX_EXT_TEST_ROOT / "conf_value.py.template").read_text(encoding="utf-8") ) index_template = Template( (cls.SPHINX_EXT_TEST_ROOT / "index.rst.template").read_text(encoding="utf-8") ) (tmp_dir / "conf.py").write_text( conf_template.substitute(chango_pyproject_toml_path=conf_value_insert), encoding="utf-8", ) (tmp_dir / "index.rst").write_text( index_template.substitute(directive=directive_insert), encoding="utf-8" ) return tmp_dir @staticmethod def compute_chango_pyproject_toml_path_insert( path: str | Path, path_representation: PathLike ) -> str: if path == "explicit_none": return "chango_pyproject_toml_path = None" if path is None: return "" return f"chango_pyproject_toml_path = {path_to_python_string(path, path_representation)}" @pytest.mark.parametrize( "path", [ None, "explicit_none", data_path("config/pyproject.toml"), data_path("config/pyproject.toml").relative_to(Path.cwd(), walk_up=True), data_path("config"), ], ids=["None", "explicit_none", "absolute", "relative", "directory"], ) @pytest.mark.parametrize("path_representation", [str, Path]) def test_chango_pyproject_toml_path_valid( self, path, path_representation, make_app: MAKE_APP_TYPE, tmp_path_factory: TempPathFactory ): app = make_app( srcdir=self.create_template( conf_value_insert=self.compute_chango_pyproject_toml_path_insert( path, path_representation ), tmp_path_factory=tmp_path_factory, ) ) if path in ("explicit_none", None): assert app.config.chango_pyproject_toml_path is None else: assert ( app.config.chango_pyproject_toml_path == path.as_posix() if path_representation is str else path ) @pytest.mark.parametrize("path", [1, {"key": "value"}, [1, 2, 3]], ids=["int", "dict", "list"]) def test_chango_pyproject_toml_path_invalid( self, path, make_app: MAKE_APP_TYPE, tmp_path_factory: TempPathFactory ): insert = f"chango_pyproject_toml_path = {path!r}" with pytest.raises( TypeError, match="Expected 'chango_pyproject_toml_path' to be a string or Path" ): make_app( srcdir=self.create_template( conf_value_insert=insert, tmp_path_factory=tmp_path_factory ) ) def test_metadata(self, app: SphinxTestApp): assert app.extensions["chango.sphinx_ext"].version == __version__ assert app.extensions["chango.sphinx_ext"].parallel_read_safe is True assert app.extensions["chango.sphinx_ext"].parallel_write_safe is True @pytest.mark.parametrize( "path", [ None, "explicit_none", data_path("config/pyproject.toml"), data_path("config/pyproject.toml").relative_to(Path.cwd(), walk_up=True), data_path("config"), ], ids=["None", "explicit_none", "absolute", "relative", "directory"], ) @pytest.mark.parametrize("path_representation", [str, Path]) def test_directive_chango_instance_loading( self, make_app: MAKE_APP_TYPE, path, path_representation, tmp_path_factory: TempPathFactory, cg_config_mock, ): app = make_app( srcdir=self.create_template( conf_value_insert=self.compute_chango_pyproject_toml_path_insert( path, path_representation ), tmp_path_factory=tmp_path_factory, ) ) self.assert_successful_build(app) received_sys_path = cg_config_mock.get().sys_path if path in ("explicit_none", None): assert received_sys_path is None else: assert received_sys_path == path @pytest.mark.parametrize( "headline", [None, "This is a headline"], ids=["no_headline", "headline"] ) def test_directive_rendering_basic( self, cg_config_mock, make_app: MAKE_APP_TYPE, tmp_path_factory: TempPathFactory, headline ): directive = f".. chango:: {headline}" if headline else ".. chango::" app = make_app( srcdir=self.create_template( directive_insert=directive, tmp_path_factory=tmp_path_factory ) ) self.assert_successful_build(app) index = app.outdir.joinpath("index.html") assert index.exists() content = index.read_text(encoding="utf-8") assert cg_config_mock.rendered_content in content if headline: assert f"

{headline}" in content else: assert f"

{headline}" not in content def test_directive_rendering_passed_markup_language(self, cg_config_mock, app: SphinxTestApp): self.assert_successful_build(app) received_args = cg_config_mock.get().chango.version_history.received_args received_kwargs = cg_config_mock.get().chango.version_history.received_kwargs assert received_args == (MarkupLanguage.RESTRUCTUREDTEXT,) assert received_kwargs == {} def test_argument_passing_basic( self, cg_config_mock: MockStorage, make_app: MAKE_APP_TYPE, tmp_path_factory: TempPathFactory, ): directive = """ .. chango:: :start_from: "start_from" :end_at: "end_at" """ app = make_app( srcdir=self.create_template( directive_insert=directive, tmp_path_factory=tmp_path_factory ) ) self.assert_successful_build(app) received_kwargs = cg_config_mock.get().chango.received_kwargs assert received_kwargs == {"start_from": "start_from", "end_at": "end_at"} def test_argument_passing_unknown_option( self, make_app: MAKE_APP_TYPE, tmp_path_factory: TempPathFactory ): directive = """ .. chango:: :unknown: """ app = make_app( srcdir=self.create_template( directive_insert=directive, tmp_path_factory=tmp_path_factory ) ) with pytest.raises(SphinxBuildError, match='unknown option: "unknown"'): self.assert_successful_build(app) def test_argument_passing_json_data( self, cg_config_mock: MockStorage, make_app: MAKE_APP_TYPE, tmp_path_factory: TempPathFactory, ): directive = """ .. chango:: :start_from: {"key": "value"} :end_at: [false, true, null] """ app = make_app( srcdir=self.create_template( directive_insert=directive, tmp_path_factory=tmp_path_factory ) ) self.assert_successful_build(app) received_kwargs = cg_config_mock.get().chango.received_kwargs assert received_kwargs == {"start_from": {"key": "value"}, "end_at": [False, True, None]} def test_argument_passing_invalid_json_data( self, make_app: MAKE_APP_TYPE, tmp_path_factory: TempPathFactory ): directive = """ .. chango:: :start_from: {"key": "value} """ app = make_app( srcdir=self.create_template( directive_insert=directive, tmp_path_factory=tmp_path_factory ) ) with pytest.raises(SphinxBuildError, match="must be a JSON-loadable value"): self.assert_successful_build(app) def test_argument_passing_missing_value( self, make_app: MAKE_APP_TYPE, tmp_path_factory: TempPathFactory ): directive = """ .. chango:: :start_from: """ app = make_app( srcdir=self.create_template( directive_insert=directive, tmp_path_factory=tmp_path_factory ) ) with pytest.raises(SphinxBuildError, match="must be a JSON-loadable value"): self.assert_successful_build(app) def test_argument_passing_custom_signature( self, cg_config_mock: MockStorage, make_app: MAKE_APP_TYPE, tmp_path_factory: TempPathFactory, monkeypatch, ): validator_kwargs = {} def sequence_validator(value: str | None) -> Sequence[int]: validator_kwargs["sequence_validator"] = value return tuple(map(int, value.split(","))) def flag_validator(value: str | None) -> bool: validator_kwargs["flag_validator"] = value return True original_load_version_history = MockChanGo.load_version_history def load_version_history( *args, start_from: str | None = None, end_at: str | None = None, json_dict_arg: dict[str, str] | None = None, sequence_arg: Annotated[Sequence[int], sequence_validator] = (1, 2, 3), flag_arg: Annotated[bool, flag_validator] = False, ): return original_load_version_history( *args, start_from=start_from, end_at=end_at, json_dict_arg=json_dict_arg, sequence_arg=sequence_arg, flag_arg=flag_arg, ) monkeypatch.setattr(MockChanGo, "load_version_history", load_version_history) directive = """ .. chango:: :json_dict_arg: {"key": "value"} :sequence_arg: 1,2,3 :flag_arg: """ app = make_app( srcdir=self.create_template( directive_insert=directive, tmp_path_factory=tmp_path_factory ) ) self.assert_successful_build(app) received_kwargs = cg_config_mock.get().chango.received_kwargs assert received_kwargs == { "json_dict_arg": {"key": "value"}, "sequence_arg": (1, 2, 3), "flag_arg": True, "start_from": None, "end_at": None, } assert validator_kwargs == {"sequence_validator": "1,2,3", "flag_validator": None} chango-0.6.0/tests/test_action.py000066400000000000000000000065451507376567100170370ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT import pytest from pydantic import ValidationError from chango.action import ChanGoActionData, LinkedIssue, ParentPullRequest @pytest.fixture(scope="module") def parent_pull_request(): return ParentPullRequest( number=1, author_login="author", title="title", url="http://example.com", state="OPEN" ) @pytest.fixture(scope="module") def linked_issue(): return LinkedIssue(number=1, title="title", labels=["label1", "label2"], issue_type="type") class TestParentPullRequest: def test_init_basic(self, parent_pull_request): assert parent_pull_request.number == 1 assert parent_pull_request.author_login == "author" assert parent_pull_request.title == "title" assert str(parent_pull_request.url) == "http://example.com/" assert parent_pull_request.state == "OPEN" def test_frozen(self, parent_pull_request): with pytest.raises(ValidationError, match="frozen"): parent_pull_request.number = 2 def test_invalid_url(self): with pytest.raises(ValidationError, match=r"input_value='example.com'"): ParentPullRequest( number=1, author_login="author", title="title", url="example.com", state="open" ) def test_invalid_state(self): with pytest.raises(ValidationError, match="input_value='invalid'"): ParentPullRequest( number=1, author_login="author", title="title", url="http://example.com", state="invalid", ) class TestLinkedIssue: def test_init_basic(self, linked_issue): assert linked_issue.number == 1 assert linked_issue.title == "title" assert linked_issue.labels == ("label1", "label2") assert linked_issue.issue_type == "type" def test_init_no_optional(self): linked_issue = LinkedIssue(number=1, title="title", labels=None) assert linked_issue.labels is None assert linked_issue.issue_type is None def test_frozen(self, linked_issue): with pytest.raises(ValidationError, match="frozen"): linked_issue.number = 2 def test_init_none_labels(self): linked_issue = LinkedIssue(number=1, title="title", labels=None, issue_type=None) assert linked_issue.labels is None assert linked_issue.issue_type is None class TestChanGoActionData: def test_init_basic(self, parent_pull_request, linked_issue): data = ChanGoActionData( parent_pull_request=parent_pull_request, linked_issues=[linked_issue] ) assert data.parent_pull_request == parent_pull_request assert data.linked_issues == (linked_issue,) def test_init_none(self): data = ChanGoActionData(parent_pull_request=None, linked_issues=None) assert data.parent_pull_request is None assert data.linked_issues is None def test_frozen(self, parent_pull_request, linked_issue): data = ChanGoActionData( parent_pull_request=parent_pull_request, linked_issues=[linked_issue] ) with pytest.raises(ValidationError, match="frozen"): data.parent_pull_request = None with pytest.raises(ValidationError, match="frozen"): data.linked_issues = None chango-0.6.0/tests/test_build.py000066400000000000000000000006611507376567100166520ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT import os import shutil from pathlib import Path import pytest # To make the tests agnostic of the cwd @pytest.fixture(autouse=True) def _change_test_dir(request, monkeypatch): monkeypatch.chdir(request.config.rootdir) def test_build(): assert os.system("python -m build") == 0 shutil.rmtree(Path("dist")) chango-0.6.0/tests/test_changenoteinfo.py000066400000000000000000000016001507376567100205340ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT from pathlib import Path import pytest from chango import ChangeNoteInfo, Version class TestChangeNoteInfo: def test_init(self): v = Version("1.2.3", None) cni = ChangeNoteInfo(uid="123", version=v, file_path=Path("/path/to/file")) assert cni.uid == "123" assert cni.version == v assert cni.file_path == Path("/path/to/file") def test_frozen(self): v = Version("1.2.3", None) cni = ChangeNoteInfo(uid="123", version=v, file_path=Path("/path/to/file")) with pytest.raises(AttributeError): cni.uid = "124" with pytest.raises(AttributeError): cni.version = Version("1.2.4", None) with pytest.raises(AttributeError): cni.file_path = Path("/new/path/to/file") chango-0.6.0/tests/test_constants.py000066400000000000000000000047441507376567100175750ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT import pytest from chango.constants import MarkupLanguage class TestMarkupLanguage: def test_str_instance(self): for member in MarkupLanguage: assert isinstance(member, str) @pytest.mark.parametrize("leading_dot", [True, False]) def test_from_string_members(self, leading_dot): for member in MarkupLanguage: string = member.value if leading_dot: string = "." + string assert MarkupLanguage.from_string(string) == member @pytest.mark.parametrize("leading_dot", [True, False]) def test_from_string_default_mapping(self, leading_dot): default_mapping = { "adoc": MarkupLanguage.ASCIIDOC, "htm": MarkupLanguage.HTML, "md": MarkupLanguage.MARKDOWN, "mkd": MarkupLanguage.MARKDOWN, "mdwn": MarkupLanguage.MARKDOWN, "mdown": MarkupLanguage.MARKDOWN, "mdtxt": MarkupLanguage.MARKDOWN, "mdtext": MarkupLanguage.MARKDOWN, "mediawiki": MarkupLanguage.MEDIAWIKI, "org": MarkupLanguage.ORG, "pod": MarkupLanguage.POD, "rdoc": MarkupLanguage.RDOC, "text": MarkupLanguage.TEXT, } for ext, member in default_mapping.items(): string = ext if leading_dot: string = "." + string assert MarkupLanguage.from_string(string) == member @pytest.mark.parametrize("leading_dot", [True, False]) def test_from_string_custom_mapping(self, leading_dot): custom_mapping = { "abc": MarkupLanguage.TEXT, "def": MarkupLanguage.HTML, "ghi": MarkupLanguage.MARKDOWN, } for ext, member in custom_mapping.items(): string = ext if leading_dot: string = "." + string assert MarkupLanguage.from_string(string, custom_mapping) == member @pytest.mark.parametrize("leading_dot", [True, False]) @pytest.mark.parametrize( "mapping", [None, {"abc": MarkupLanguage.TEXT}], ids=["default", "custom"] ) def test_from_string_invalid(self, leading_dot, mapping): string = "invalid" if leading_dot: string = "." + string with pytest.raises(ValueError, match=f"File extension `{string}` not found"): MarkupLanguage.from_string(string, mapping) chango-0.6.0/tests/test_helpers.py000066400000000000000000000015141507376567100172130ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT from pathlib import Path from chango.helpers import change_uid_from_file, ensure_uid class TestHelpers: def test_ensure_uid_none(self): assert ensure_uid(None) is None def test_ensure_uid_str(self): assert ensure_uid("uid") == "uid" def test_ensure_uid_obj(self): class Obj: uid = "uid" assert ensure_uid(Obj()) == "uid" def test_ensure_uid_obj_prop(self): class Obj: @property def uid(self): return "uid" assert ensure_uid(Obj()) == "uid" def test_change_uid_from_file(self): assert change_uid_from_file("slug.uid.md") == "uid" assert change_uid_from_file(Path("slug.uid.md")) == "uid" chango-0.6.0/tests/test_version.py000066400000000000000000000011161507376567100172340ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024-present Hinrich Mahler # # SPDX-License-Identifier: MIT import datetime as dtm import pytest from chango import Version class TestVersion: def test_init(self): v = Version("1.2.3", dtm.date(2024, 1, 1)) assert v.uid == "1.2.3" assert v.date == dtm.date(2024, 1, 1) def test_frozen(self): v = Version("1.2.3", dtm.date(2024, 1, 1)) with pytest.raises(AttributeError): v.uid = "1.2.4" with pytest.raises(AttributeError): v.date = dtm.date(2024, 1, 2)