pax_global_header00006660000000000000000000000064147535707350014532gustar00rootroot0000000000000052 comment=50213b1db08dbd2ad90ba36d5f7549507390e33a url-matcher-0.6.0/000077500000000000000000000000001475357073500137605ustar00rootroot00000000000000url-matcher-0.6.0/.codecov.yml000066400000000000000000000001201475357073500161740ustar00rootroot00000000000000comment: layout: "header, diff, tree" coverage: status: project: false url-matcher-0.6.0/.git-blame-ignore-revs000066400000000000000000000002131475357073500200540ustar00rootroot00000000000000# Contains commits to be ignored due to linting # https://github.com/zytedata/url-matcher/pull/5 47197448e5ac7e4d9dc9f764231a6915e7fb2295 url-matcher-0.6.0/.github/000077500000000000000000000000001475357073500153205ustar00rootroot00000000000000url-matcher-0.6.0/.github/workflows/000077500000000000000000000000001475357073500173555ustar00rootroot00000000000000url-matcher-0.6.0/.github/workflows/publish.yml000066400000000000000000000015741475357073500215550ustar00rootroot00000000000000# This workflows will upload a Python Package using Twine when a release is created # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries name: publish on: push: tags: - "[0-9]+.[0-9]+.[0-9]+" jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.13' - name: Install dependencies run: | python -m pip install --upgrade pip pip install setuptools wheel twine - name: Build and publish if: startsWith(github.ref, 'refs/tags') env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} run: | python setup.py sdist bdist_wheel twine upload dist/* url-matcher-0.6.0/.github/workflows/test.yml000066400000000000000000000033431475357073500210620ustar00rootroot00000000000000# This workflow will install Python dependencies, run tests and lint with a variety of Python versions # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions name: tox on: push: branches: [ main ] pull_request: branches: [ main ] jobs: test: runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: - python-version: '3.9' toxenv: min - python-version: '3.9' - python-version: '3.10' - python-version: '3.11' - python-version: '3.12' - python-version: '3.13' steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install tox - name: tox run: | tox -e ${{ matrix.toxenv || 'py' }} - name: coverage uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} check: runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: ['3.12'] # Keep in sync with .readthedocs.yml tox-job: ["mypy", "docs", "pre-commit", "twinecheck"] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install tox - name: tox run: | tox -e ${{ matrix.tox-job }} url-matcher-0.6.0/.gitignore000066400000000000000000000003471475357073500157540ustar00rootroot00000000000000# Python *.pyc *.pyo /build/ /dist/ *.egg-info # Mac OS *.DS_Store # IDE /.idea/ .mypy_cache/ .cache/ .tox/ .pytest_cache/ .coverage /coverage.xml htmlcov/ .ipynb_checkpoints docs/_build/ docs/_autosummary/ __pycache__/ _build/ url-matcher-0.6.0/.pre-commit-config.yaml000066400000000000000000000002161475357073500202400ustar00rootroot00000000000000repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.9.6 hooks: - id: ruff args: [ --fix ] - id: ruff-format url-matcher-0.6.0/.readthedocs.yml000066400000000000000000000003621475357073500170470ustar00rootroot00000000000000version: 2 formats: all sphinx: configuration: docs/conf.py build: os: ubuntu-22.04 tools: python: "3.12" # Keep in sync with .github/workflows/tests.yml python: install: - requirements: docs/requirements.txt - path: . url-matcher-0.6.0/CHANGELOG.rst000066400000000000000000000025071475357073500160050ustar00rootroot00000000000000========= Changelog ========= 0.6.0 (2025-02-14) ------------------ * Dropped Python 3.8 support. * Added Python 3.13 support. * Improved type hints and added ``py.typed``. * CI improvements. 0.5.0 (2024-04-15) ------------------ * Added the ``include_universal`` argument to :meth:`.URLMatcher.match` and :meth:`.URLMatcher.match_all`. It can be set to ``False`` to skip universal matchers. * Added the :meth:`.URLMatcher.match_universal` method that returns only identifiers of universal matchers. * Added ``.readthedocs.yml``. 0.4.0 (2024-04-03) ------------------ * Added official support for Python 3.12. * Added the :meth:`.URLMatcher.match_all` method that returns all matching identifiers. * Adding a :class:`~.Patterns` instance with several patterns for the same domain to a :class:`~.URLMatcher` no longer creates multiple identical :class:`~.matcher.PatternsMatcher` instances. * CI improvements. 0.3.0 (2023-09-21) ------------------ * Drop Python 3.7 support, make Python 3.11 support official. * Support tldextract >= 3.6, make the requirement of tldextract >= 1.2 explicit. 0.2.0 (2022-02-01) ------------------ * Update :class:`~.Patterns` to be **frozen** so instances can easily be deduped based on its hash uniqueness. * Remove Python 3.6 support 0.1.0 (2021-11-19) ------------------ * Initial release url-matcher-0.6.0/CONTRIBUTING.rst000066400000000000000000000015311475357073500164210ustar00rootroot00000000000000============ Contributing ============ ``url-matcher`` is an open-source project. Your contribution is very welcome! Issue Tracker ============= If you have a bug report, a new feature proposal or simply would like to make a question, please check our issue tracker on Github: https://github.com/zytedata/url-matcher/issues Source code =========== Our source code is hosted on Github: https://github.com/zytedata/url-matcher Before opening a pull request, it might be worth checking current and previous issues. Some code changes might also require some discussion before being accepted so it might be worth opening a new issue before implementing huge or breaking changes. Testing ======= We use tox_ to run tests with different Python versions:: tox The command above also runs type checks; we use mypy. .. _tox: https://tox.readthedocs.io url-matcher-0.6.0/LICENSE000066400000000000000000000027451475357073500147750ustar00rootroot00000000000000Copyright (c) Zyte Group Ltd All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of Zyte nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. url-matcher-0.6.0/MAINTAINING.rst000066400000000000000000000023751475357073500162570ustar00rootroot00000000000000Maintaining =========== How to release a new version ---------------------------- Make sure to install bump2version_, a maintained fork of bumpversion_: .. code-block:: shell pip install --upgrade bump2version Then follow this checklist: * Update changelog in a separate commit * Execute the ``bumpversion`` command * Push latest changes including tags The changelog is kept under the ``CHANGES.rst`` file. It should be updated in a separate commit to master. After changelog is merged to master, you can check which changes are needed to update the version executing: .. code-block:: shell bumpversion minor --dry-run --verbose When you're ready, you can remove the flags and execute: .. code-block:: shell bumpversion minor In this example, we're bumping our minor version, but you may use any of the following: * patch (for bug fixes) * minor (new features that keep compatibility) * major (introduces breaking changes) Finally, you can push the changes to the remote repository. Make sure to include git tags. .. code-block:: shell git push origin `git describe --tags` The PyPI release will be handled by Github actions. .. _bump2version: https://github.com/c4urself/bump2version .. _bumpversion: https://github.com/peritus/bumpversion url-matcher-0.6.0/README.rst000066400000000000000000000056561475357073500154630ustar00rootroot00000000000000=========== url-matcher =========== .. image:: https://img.shields.io/pypi/v/url-matcher.svg :target: https://pypi.python.org/pypi/url-matcher :alt: PyPI Version .. image:: https://img.shields.io/pypi/pyversions/url-matcher.svg :target: https://pypi.python.org/pypi/url-matcher :alt: Supported Python Versions .. image:: https://github.com/zytedata/url-matcher/workflows/tox/badge.svg :target: https://github.com/zytedata/url-matcher/actions :alt: Build Status .. image:: https://codecov.io/github/zytedata/url-matcher/coverage.svg?branch=main :target: https://codecov.io/gh/zytedata/url-matcher :alt: Coverage report URL matching library that relates URLs with resources. Rules are defined using simple pattern definitions. It is simpler and faster than using regular expressions if the rules involves many domains. To illustrate it with an example, imagine that you have several proxy servers and you want to route requests to the right one. You could define the following rules: * ``site1.com`` →︎ ``us_proxy`` * ``site2.com/uk`` →︎ ``uk_proxy`` * ``site2.com/ie`` →︎ ``ie_proxy`` All URLs from ``site1.com`` should use the US proxy. The situation for ``site2.com`` URLs are different: if the path starts with ``/uk``, then use the UK proxy, otherwise use the IE proxy. This library allows to create a matcher that can be used to match URLs with the right proxy using these rules. Have a look to https://github.com/zytedata/url-matcher/blob/main/url_matcher/example.py for an example of usage. The following files are useful to understand the pattern, the set of patterns and how they behave: * https://github.com/zytedata/url-matcher/blob/main/tests/fixtures/single_patterns.json * https://github.com/zytedata/url-matcher/blob/main/tests/fixtures/patterns.json The full documentation can be found at https://url-matcher.readthedocs.io/ License is BSD 3-clause. * Documentation: https://url-matcher.readthedocs.io/ * Source code: https://github.com/zytedata/url-matcher * Issue tracker: https://github.com/zytedata/url-matcher/issues Developing ********** Setup your local Python environment via: 1. ``pip install -r requirements-dev.txt`` 2. ``pre-commit install`` Now everytime you perform a ``git commit``, these tools will run against the staged files: * ``black`` * ``isort`` * ``flake8`` * ``mypy`` You can also directly invoke ``pre-commit run --all-files`` to run them without performing a commit. Using sphinx-autobuild ~~~~~~~~~~~~~~~~~~~~~~ When working on documentation, it is convenient to use sphinx-autobuild. First, run ``pip install -r docs/requirements.txt sphinx-autobuild``. Then run :: sphinx-autobuild docs docs/_build/html and then open http://127.0.0.1:8000/ in a browser, to see the current version of docs. A process would be running in a background, watching for docs changes; when docs are changed, a build is started, and the web page is refreshed automatically when the build is finished. url-matcher-0.6.0/docs/000077500000000000000000000000001475357073500147105ustar00rootroot00000000000000url-matcher-0.6.0/docs/Makefile000066400000000000000000000011041475357073500163440ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build SOURCEDIR = . 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)url-matcher-0.6.0/docs/_templates/000077500000000000000000000000001475357073500170455ustar00rootroot00000000000000url-matcher-0.6.0/docs/_templates/custom-class-template.rst000066400000000000000000000013071475357073500240260ustar00rootroot00000000000000.. Template based in the original one, with some changes proposed on https://stackoverflow.com/a/62613202/3887420 {{ fullname | escape | underline}} .. currentmodule:: {{ module }} .. autoclass:: {{ objname }} :members: :show-inheritance: {% block methods %} .. automethod:: __init__ {% if methods %} .. rubric:: {{ _('Methods') }} .. autosummary:: {% for item in methods %} ~{{ name }}.{{ item }} {%- endfor %} {% endif %} {% endblock %} {% block attributes %} {% if attributes %} .. rubric:: {{ _('Attributes') }} .. autosummary:: {% for item in attributes %} ~{{ name }}.{{ item }} {%- endfor %} {% endif %} {% endblock %} url-matcher-0.6.0/docs/_templates/custom-module-template.rst000066400000000000000000000024761475357073500242160ustar00rootroot00000000000000.. Template based in the original one, with some changes proposed on https://stackoverflow.com/a/62613202/3887420 {{ fullname | escape | underline}} .. automodule:: {{ fullname }} {% block attributes %} {% if attributes %} .. rubric:: {{ _('Module Attributes') }} .. autosummary:: :toctree: {% for item in attributes %} {{ item }} {%- endfor %} {% endif %} {% endblock %} {% block functions %} {% if functions %} .. rubric:: {{ _('Functions') }} .. autosummary:: :toctree: {% for item in functions %} {{ item }} {%- endfor %} {% endif %} {% endblock %} {% block classes %} {% if classes %} .. rubric:: {{ _('Classes') }} .. autosummary:: :toctree: :template: custom-class-template.rst {% for item in classes %} {{ item }} {%- endfor %} {% endif %} {% endblock %} {% block exceptions %} {% if exceptions %} .. rubric:: {{ _('Exceptions') }} .. autosummary:: :toctree: {% for item in exceptions %} {{ item }} {%- endfor %} {% endif %} {% endblock %} {% block modules %} {% if modules %} .. rubric:: Modules .. autosummary:: :toctree: :template: custom-module-template.rst :recursive: {% for item in modules %} {{ item }} {%- endfor %} {% endif %} {% endblock %} url-matcher-0.6.0/docs/api_reference.rst000066400000000000000000000003211475357073500202250ustar00rootroot00000000000000============= API Reference ============= .. Based on ideas found on https://stackoverflow.com/a/62613202/3887420 Module ``url_matcher`` ====================== .. automodule:: url_matcher :members: url-matcher-0.6.0/docs/changelog.rst000066400000000000000000000000701475357073500173660ustar00rootroot00000000000000.. include:: ../CHANGELOG.rst .. toctree:: :hidden: url-matcher-0.6.0/docs/conf.py000066400000000000000000000134451475357073500162160ustar00rootroot00000000000000# Configuration file for the Sphinx documentation builder. # # This file does only contain a selection of the most common options. For a # full list see the documentation: # http://www.sphinx-doc.org/en/master/config # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent)) from url_matcher import __version__ # -- Project information ----------------------------------------------------- project = "url-matcher" copyright = "2021, Zyte" author = "Zyte" # The short X.Y version version = "" # The full version, including alpha/beta/rc tags release = __version__.__version__ # -- General configuration --------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx.ext.ifconfig", "sphinx.ext.viewcode", "sphinx.ext.githubpages", "sphinx.ext.autosummary", ] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = {".rst": "restructuredtext"} # The master toctree document. master_doc = "index" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # The name of the Pygments (syntax highlighting) style to use. pygments_style = None # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = "sphinx_rtd_theme" # 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 = {} # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". # html_static_path = ['_static'] # Custom sidebar templates, must be a dictionary that maps document names # to template names. # # The default sidebars (for documents that don't match any pattern) are # defined by theme itself. Builtin themes are using these templates by # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', # 'searchbox.html']``. # # html_sidebars = {} # -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. htmlhelp_basename = "url-matcher-doc" # -- Options for LaTeX output ------------------------------------------------ latex_elements: dict = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ( master_doc, "url-matcher.tex", "url-matcher Documentation", "Zyte", "manual", ), ] # -- Options for manual page output ------------------------------------------ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [(master_doc, "url-matcher", "url-matcher Documentation", [author], 1)] # -- Options for Texinfo output ---------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( master_doc, "url-matcher", "url-matcher Documentation", author, "url-matcher", "URL matching rules library to connect URLs with resources", "Miscellaneous", ), ] # -- Options for Epub output ------------------------------------------------- # Bibliographic Dublin Core info. epub_title = project # The unique identifier of the text. This can be a ISBN number # or the project homepage. # # epub_identifier = '' # A unique identification for the text. # # epub_uid = '' # A list of files that should not be packed into the epub file. epub_exclude_files = ["search.html"] # -- Extension configuration ------------------------------------------------- # -- Options for intersphinx extension --------------------------------------- intersphinx_mapping = { "python": ( "https://docs.python.org/3", None, ) } autodoc_default_options = { "special-members": "__init__,__call__", "undoc-members": True, } add_module_names = False autosummary_generate = True url-matcher-0.6.0/docs/contributing.rst000066400000000000000000000000721475357073500201500ustar00rootroot00000000000000.. include:: ../CONTRIBUTING.rst .. toctree:: :hidden:url-matcher-0.6.0/docs/index.rst000066400000000000000000000007751475357073500165620ustar00rootroot00000000000000============================== url-matcher documentation ============================== URL matching library that relates URLs with resources. Rules are defined using simple pattern definitions. It is simpler and faster than using regular expressions if the rules involves many domains. :ref:`license` is BSD 3-clause. .. toctree:: :caption: Getting started :maxdepth: 1 intro.rst .. toctree:: :caption: Documentation :maxdepth: 1 api_reference contributing changelog license url-matcher-0.6.0/docs/intro.rst000066400000000000000000000134251475357073500166020ustar00rootroot00000000000000.. _`intro`: ============ Introduction ============ Let's start with an example. Imagine that you have several proxy servers and you want to route requests to the right one. You could define the following rules: * ``site1.com`` →︎ ``us_proxy`` * ``site2.com/uk`` →︎ ``uk_proxy`` * ``site2.com/ie`` →︎ ``ie_proxy`` All URLs from ``site1.com`` should use the US proxy. The situation for ``site2.com`` URLs are different: if the path starts with ``/uk``, then the UK proxy should be used whereas if the path starts with ``/ie`` then the IE proxy should be used instead. This library allows to create a matcher that can be used to match URLs with the right proxy using these rules. Let see how the library can handle this situation: .. code-block:: python from url_matcher import URLMatcher, Patterns matcher = URLMatcher() matcher.add_or_update("us_proxy", Patterns(["site1.com"])) matcher.add_or_update("uk_proxy", Patterns(["site2.com/uk"])) matcher.add_or_update("ie_proxy", Patterns(["site2.com/ie"])) proxy = matcher.match("http://site1.com/articles/article1") # proxy is "us_proxy" here proxy = matcher.match("http://site2.com/uk/a_page") # proxy is "uk_proxy" here proxy = matcher.match("https://www.site2.com/ie/a_page") # proxy is "ie_proxy" here proxy = matcher.match("http://example.com/a_differnt_page") # proxy is None here As can be seen the the class :class:`url_matcher.URLMatcher` is handy to handle this use case. .. note:: Relative URLs are not supported in the ``match`` method. Patterns, include and exclude ============================= A pattern is a URL that describes a set of URLs. For example, the pattern ``example.com`` describes any URL whose domain is ``example.com`` or any of its subdomains. A single pattern is sometimes not enough to describe which URLs to match. This is why we can define instead a set of patterns that are matched against. There is then a list of positive patterns (``include``) and a list of negative ones (``exclude``). **A URL is a match** if it matches **at least one** of the patterns in ``include`` and **none** of the patterns in ``exclude``. This is an example of a rule using such a set of patterns: .. code-block:: python patterns = Patterns(include=["example.com", "example.org"], exclude=["*.jpg|", "*.jpeg|"]) matcher.add_or_update("proxy_1", patterns)) Patterns ======== A pattern is a URL that describes a set of URLs. It itself is just a URL. The following diagram summarizes its different parts and what do they mean. .. image:: patterns/patterns-cheatsheet.png :alt: Patterns Cheatsheet .. note:: Matching is always **case-insensitive**. The best way to understand how the patterns work is to look at some examples: Basic patterns -------------- .. csv-table:: :file: patterns/basic_patterns.csv :widths: 30, 70 :header-rows: 1 Domain patterns --------------- .. csv-table:: :file: patterns/domain_patterns.csv :widths: 30, 70 :header-rows: 1 .. note:: Rules above only differ by the ``/`` character and this is enough to change the matching behaviour. The general rule is that the pattern matches the domain or any of the subdomains only if the pattern does not contain a path, a query or a fragment. Otherwise, only URLs with the exact same domain after removing ``www.`` will match the pattern. Path patterns ------------- A URL matches if the pattern path is a prefix of it. Besides, the following modifier characters can be used: * The ``*`` character matches any number of characters. * Use the ``|`` character at the end of the pattern path if a exact path matching is required. .. csv-table:: :file: patterns/path_patterns.csv :widths: 30, 70 :header-rows: 1 Query patterns -------------- It serves to match URLs that have some specific parameters in the URL. The order of parameters in the query string is irrelevant. The wildcard char ``*`` can be used for values. If a parameter is repeated in the pattern it will match if any of the values provided is matched .. csv-table:: :file: patterns/query_patterns.csv :widths: 30, 70 :header-rows: 1 Fragment patterns ----------------- It works exactly like the path. Rules conflict resolution ========================= Sometimes several rules can match the same URL. We have then a conflict. By default the library will prioritize the most specific rule. For example, if a URL is matching both a rule with a pattern ``example.com`` and another with the pattern ``example.com/articles`` then the later one will be final match because it is more specific. Alternatively, it is possible to control manually the order of rules by using the ``priority`` parameter of the :class:`url_matcher.Patterns`. In case of conflict, the rule with the highest priority will be chosen. The full criteria applied to resolve a conflict between rules are: 1. universality (rules with non universal include patterns are prioritized over rules with universal ones) 2. priority (the highest wins) 3. specificity (the most specific include patterns for the concerning domain wins) 4. the rule id (the rule with the highest id wins) Efficiency ========== Internally, the library clusters the rules by the top level domain of their include patterns. This is done to speed up the matching because it reduces the space of possible rules that can match a URL. The drawback is that the rules with ``include`` patterns that do not belong to any top level domain are not supported. In fact, an error is raised. An exception were done for the universal matching pattern. It is the only cross-top-level-domain ``include`` pattern that is allowed. The rationale is that is can be convenient to define defaults (e.g. to define the default proxy to use if no other rule matches). url-matcher-0.6.0/docs/license.rst000066400000000000000000000001011475357073500170540ustar00rootroot00000000000000.. _`license`: ======= License ======= .. include:: ../LICENSE url-matcher-0.6.0/docs/patterns/000077500000000000000000000000001475357073500165505ustar00rootroot00000000000000url-matcher-0.6.0/docs/patterns/basic_patterns.csv000066400000000000000000000012401475357073500222630ustar00rootroot00000000000000Pattern, Behaviour The empty string, Universal pattern. Match any URL ``example.com``, " Match any URL whose domain is ``example.com`` or any of its subdomains. | **Match:** * ``http://example.com/anything?id=24`` * ``https://www.example.com/page#with_fragment`` **Don't match:** * ``http://myexample.com`` " ``example.com/articles/``, " Match any URL whose domain is ``example.com`` or ``www.example.com`` and path starts by ``/articles/``. | **Match:** * ``http://www.example.com/articles/article1`` * ``https://example.com/articles/another_article?id=23`` **Don't match:** * ``http://example.com/articles`` * ``http://shop.example.com/articles/article1`` "url-matcher-0.6.0/docs/patterns/domain_patterns.csv000066400000000000000000000011241475357073500224520ustar00rootroot00000000000000Pattern, Behaviour ``shop.example.com``, " Match any URL whose domain is ``shop.example.com`` or any of its subdomains. | **Match:** * ``https://shop.example.com/foo?id=34#fragment`` * ``http://uk.shop.example.com/foo?id=34`` **Don't match:** * ``http://myshop.example.com`` " ``shop.example.com/``, " Match any URL whose domain is ``shop.example.com`` or ``www.shop.example.com``. | **Match:** * ``https://shop.example.com/foo?id=34#fragment`` * ``http://www.shop.example.com/foo?id=34`` **Don't match:** * ``http://myshop.example.com`` * ``http://uk.shop.example.com/foo?id=34`` "url-matcher-0.6.0/docs/patterns/path_patterns.csv000066400000000000000000000016221475357073500221420ustar00rootroot00000000000000Pattern, Behaviour ``/articles/``, " Match any URL whose path starts by ``/articles/``. | **Match:** * ``http://example.com/articles/an_article?id=23#main`` * ``https://foo.com/articles/`` **Don't match:** * ``https://foo.com/articles`` " ``example.com/index.html|``, " Match any URL whose domain is ``example.com`` or ``www.example.com`` and path is exactly ``/index.html`` | **Match:** * ``http://example.com/index.html?id=24`` * ``https://www.example.com/index.html#main`` **Don't match:** * ``http://shop.example.com/index.html`` * ``http://shop.example.com/index.html_2`` " ``/images/*.jpg|``, " Match any URL whose path starts by ``/images/`` and whose path ends by ``.jpg`` | **Match:** * ``http://example.com/images/foo.jpg`` * ``https://example.org/images/other/subpath/FOO.JPG?id=23`` **Don't match:** * ``http://example.com/images/foo.jpeg`` * ``http://example.com/images/foo.jpg_2`` "url-matcher-0.6.0/docs/patterns/patterns-cheatsheet.png000066400000000000000000002016061475357073500232360ustar00rootroot00000000000000‰PNG  IHDRØ,8¶WsRGB®Îé _tEXtmxfile%3Cmxfile%20host%3D%22app.diagrams.net%22%20modified%3D%222021-11-22T11%3A09%3A14.653Z%22%20agent%3D%225.0%20(Macintosh%3B%20Intel%20Mac%20OS%20X%2010_15_7)%20AppleWebKit%2F537.36%20(KHTML%2C%20like%20Gecko)%20Chrome%2F96.0.4664.45%20Safari%2F537.36%22%20etag%3D%22G5crxP-wRdDt0zO1ULS2%22%20version%3D%2215.8.3%22%20type%3D%22google%22%3E%3Cdiagram%20id%3D%2221cbwoHLaFD1zpfFUvyJ%22%20name%3D%22Page-1%22%3E7Vxtc5s4EP41nnuZCYN498e8NO3Nzc30JtPe3UcFZFtXQFTItXO%2F%2FiSQACGc4LQ4iW1%2FSGAFC%2BzzrLRaVszc62z7nsJi9QdJUDpz7GQ7c29mjuMBwP8KwUMtcEBYC5YUJ7XIbgV3%2BD9UC4GSrnGCSimrRYyQlOFCF8Ykz1HMNBmklGz0wxYkTTRBAZfIENzFMDWlf%2BGErWpp5Nut%2FAPCy5W6MrBlSwbVwVJQrmBCNh2R%2B27mXlNCWL2Vba9RKmyn2%2BV2R2tzYxTlbMwJoVvED5urT5%2FKKPj3N%2FLh%2Fe%2F3ny%2Bklm8wXcsHljfLHpQF%2BH0XYjNe0%2FThisL4C%2BLXu9qsMEN3BYxF24Zjz2UrlqV8D%2FBNStZ5gpJmj0GGSc53L%2BY2FyxIzq5JSmh1DTeoflxeMkq%2BoKEWeaOIMrTdaQHQ2JXzEZEMMfrAD1EnBKEV%2BvVZko4gtBvRpsVXYbbqQOtFljwQSk4tmyu0Zucb0vJ7oOAYKKwY4wa9FGqdW7SFWZEiKyYZ3ysEsZxbRvgfikqypgICLqYw4yfXShx3QeEyE4bp48ntx3SsdKPnJEcCIZymPRFM8VJAGHO1iMuvBBqY%2B8qlbMhwkojLDJKjZYQigPR2AdlOQvwI2BWaCvPANgAPfN9E3LUnwts9Ja9zAtvqOZ0zt8b5HHDBRBB4pwRBMworAIA7zvyBN5H5%2FVMyvx%2F4lh32XCCwvLE%2BEFrAmQiH4JRwCCKOg6fj4PGQwBuFw9yz%2FKkCgNCA4S5eoQy%2B%2FsEbgIkHb8%2BM2eZzc%2FweAGyy4Tsy0LohGcT5GS3HM8b6lwZrboD1sQqgTx0qdw5eF1Bqxt9B6s81kopPGirf4%2BNWMG9%2Frww4M4lw%2B2Ymn1NjFwRmzPHSgLkjsj5LbrFi5%2FPLbBu8V4fb%2B9rFc%2FRxgk8SbdttfyAwbAT8ASOFtjWfB%2B0vmork5qA%2Fc4JUkLksYK4ZL%2Fi6Fkm9q7im1CVvpMv7n4EtFMwcfgN2b%2FsXsSP4ZgsqXixghtOH%2BkyuDmZF1ei6nvAdlH5DgvlGi66krAguVABQbHtt9b2KxpzQDKZ680YaWLR7IqEpG1PEuO9d8AeOcb4cPF84%2BIX01cvqlAXTGzF3xFwqtzs3VjUyCvNywVUq5dz31QEbQhP92t3TE1wWKZRGw3mKO2cuUgJZV6NCiG8txf%2BCogUWujLI4pXQL7HlXKnhrY%2Fb1Z89MRn6of3NIy69O4p2Ne9qkvAd75oPJMCccCJvcsyx%2FuxNR%2BNNKE94G1lUxBWxth2vIK2tP%2BxVT0B%2FhvElYFSwiKcXGDYdVntg1ZssFgPnhtcdsGsNutZ9OXB2%2FzfEm9qU%2BX1ZPIb4cY2lMpD13F5AamYXm%2FT7YcbaEdF%2BBbCai6n33MILEkx5nF9nbkuyFvIuGAWimN%2BlmI%2Fd3MP4yz0nxMdWdlUQnLPqgfyrmX%2FDJVyHuNh187p%2Bn%2FmYmgNWfjAFxBJDMzoCA1MPfyrEgJmzOnePR9M9bnCaxJCKEOkcFr1B%2FL4nLPr1HBSdLmu0oAimKdmg5OhiIy0WcmzLeyoWcsMDxkLuiMqbqTOfINJfvKg0r5aLsQ5ZjqSqCzpWQVsYC69q82E%2FPJEvg7jXksZ%2FhC%2BPVJYN1DFFA8H9dKn8oUTaqQf3O0Brip9GRfeTldy4I0rPpu6D%2FFB%2Fh%2BhHpk1UN3WoGlizEzqHP0cT%2FnDNSEx1EoLK%2FCc5sohu5K3FP96o%2BEdL9ngHDHCcoQDn7EVH4kXn1MHbxu%2BcOjiz5jmssZtsATcBVd17eaSDZ7NScGByBVSBz2FG0xHLFE5udrUDxL1enbjeZPMIdwRmU0%2Bvhor%2BBkLDyLe4ddrfQKA4WeLAMZcclDBD9aVU3cZp5nz8x3spM10XChwPid1QLeKpd0s7UJPuZ1bcHjTn441I06E8uRQL1luvSWC5apZQDa6t2sfUTy2tQomxEv7J%2Fq5jz6HiXCWjKIUMf9PVDxlZXuGjIFnH5YLe4jmVrlIq6kXQ8qwWKlNRpK8E9nqBA4N0iZihp4K8eervYMGISowzC3axoF9oA%2Fr1F2NZ4PRKz%2BfOYVkwIv%2F7cixAW8z%2BFtosz4vk%2Fj98%2F8K2bNeVgputvF6189DZ6QwVN%2FYepOJPLz9ksHvUe0nyeaC3rM0HzyOf5%2BiznCg4LPlGRMevgXxB5HTId9rc89Wct%2BGe%2Fzzu%2BaGuKDpwxzdisflr4F7kuOeOr5nK%2Bo7GGcf2LTWC7cu%2FIHAtR3%2Fx2P0OwoFoCMxXJIlcvm3DauUCW6F%2Buc5ms7EEVut7dazeTvg5dINLZDD66CbNT60A73VWwBv6wAIIDlklAcyQqwc5ZmUXXrGDxfqVnDTJEJEN%2F1ovSJYnTfiJpTeNeRi9AshPO3X7BGCOPpUK%2FKFU%2B9Bq23CyvK1nJiSr4jPx8b%2FLk%2FcwtweYmUz2o6GaQRBMB9jQKoV%2BOJemuCh3mbODHiyL2jEWeCsM%2FMyAbhjjLgsm%2F%2BBJZHpSYOISTASKPybReHqgAFGJPBEw4jOMzUc86yC1%2FRKq%2B%2B5%2F%3C%2Fdiagram%3E%3C%2Fmxfile%3Eô+” IDATx^ì� œEÙÿŸ§gv“pa!Š(¢D9”Co_/ŒxæØ®Þ$FA�s/�$ÓÕ»!ˆ _Dñå�¢‚' ¢ˆç‹a‘#$ @vw¦Ÿÿç7Våí 3»³;³»³Ù§>?’ÙîªêoõQ¿ªç`Ò¢”€PJ@ (% ”€PJ@ 4L€®A+PJ@ (% ”€PJ@ (% H¶ÞJ@ (% ”€PJ@ (% š@@v jJ@ (% ”€PJ@ (% T`ë= ”€PJ@ (% ”€PJ  T`7¢V¡”€PJ@ (% ”€PJ@¶ÞJ@ (% ”€PJ@ (% š@@v jJ@ (% ”€PJ@ (% T`ë= ”€PJ@ (% ”€PJ  T`7¢V¡”€PJ@ (% ”€PJ@¶ÞJ@ (% ”€PJ@ (% š@@v jJ@ (% ”€PJ@ (% T`ë= ”€PJ@ (% ”€PJ  T`7¢V¡”€PJ@ (% ”€PJ@¶ÞJ@ (% ”€PJ@ (% š@@v jJ@ (% ”€PJ@ (% T`ë= ˆhÞ¼y¹™3gn—Ëåþe­¬eÞ¼yíDTZ³fMIÁ)% ”€PJ@ (% ²T`Oüýp$�S£¯�øî�®ƘWçr¹GW®\ùÇÑÕÐØY]]]/#¢­ …¯ë©) ÷1ó÷˜yaÇ—V;gÙ²eÓúûû72ó·ã8~w=õê1J@ (% ”€PJ@ L*°'~¬!°o®Ò�ÑdØBD7Zkß8ˆ�1¿!¢—ZkgÔÓ¾Ø"²(I’UÕÎÁîõ¬Y³þÄÌßŒãø”zêÕc”€PJ@ (% ”€˜:T`OüX·²ÀÆý¡\³twwç»»»a.½ÙqƘqØÝÝÝAwwwZÙÁ¡vµ>WØÃ^ûÄß:Ú% ”€PJ@ (% Z‰€ ì‰�¦ lcŒ!¢3ˆèyDô­!¢“áWlŒù�GD?µÖ~—>þüíÛÛÛo%¢n­}, Ã73óGˆèh"*ѯDäkÙ�Ý0 eæËˆèED´QD® ‚às¥Ri]?%¢ÝܹÿØsõêÕ³¨Ýnð_ˆè"2�™O ¢i"òÍb±xr{{û*yê`æïŠÈqÞ7: ùA|Òý}"º›™¿/"ç¸ëÄîõ+\{kqn’$?©Õç8Žo÷›ˆÎ%¢Cˆã‚rS>ŸŸ·bÅŠcÚˆèwÌüßqŸnŒù-‘c™¹@DûÑDt™µöD½K–,Ù¹X,bW �á‰èif>ÐZû承µJ@ (% ”€PJ@ 4ƒ€ ìfPl¬Ž¦ ì0 Ïeæ3!x‰èJ˜H;±ø+kíÁN þâ[DÞŸ$ÉUQ]-"ï%¢³­µç.^¼x¯R©t„­ˆ\ÉÌOÑñD´]¯- ?6ÆìODw¸c.cæ9Dô´;00ðòööö“‰B} "´2pØüùó§···?“Awƒ ÓÝoÖ?‘ý Ö™ù¬8ŽÏC0²Y³fm ¢mˆè‡"r;3¿‹ˆ^ÌÌ8áûy"Z쎱Ìüeˆø!ú¼K[[Û«áƒíÚ†Ÿ5Dý[\—[kOÈôùZkí»Â0ü 3/sçÜ+"·0óûÁ kÖÚËŽ?þø­g̘ñ ~cæ[\?òíÔkÂÞØ-¦g+% ”€PJ@ (%0T`�å¡ÛhŠÀîêêÚ5MÓµµÓ¦MÛuùòåýhÖ�{ 3Çñ/:;;_ÁÝÃ̉v¡ïîèèØ¦ÖQ�*"ŸMÓôˆžžž[PGEï섟a­=ßósw9;î�ÏÑéDô^kíšáLij»T*íÞÛÛû¿]]]»§iú7ÔÇÌûÅq|÷¢E‹žŸËåþND?Àn/‚—¥iz�ˆœ—$ÉY8¶««k§4M†€�ãøןÍ|°‡ë³ˆ<ã6î�·ËþýÃZ»G-�í|²ß3ù(Š‘ÛˆÈZk£0 ÏfæOÑ™ÖZ0B�LÓ~÷U`Oü¨=PJ@ (% ”€PÍ" »Y$G_OSvEljÈå{Dôß�4M_ÏÌçû`'>!L?íŽ)–J¥Aàf.�—.]:»T*½DD^HD�"r¸ßå6Æ �ÕSÖÚíý9Ë–-›¹qãÆÃ˜ù^kí#ØåÝu_�;ïkís3¿¡½?Zk¼¬¿»»»s}}}/'¢ŠÈQÌÜED¿°ÖZC`Ùgy¹ØçZkÏδý;"ÚËZÛ6„À>fæ8þÝ}}}hëfkíQQýìòùü¶03ÏÔû"Ú]öè=S (% ”€PJ@ ´Ø?"MØÆøVÃ÷ºVùŠ÷ ÎìÌæýN«?Éù ü<ÁÂ&ÙgAc·XD~–$É«k5V¯À‘o%I2¯B`ÿÄZë} ± ŸØ†á̼Ôõ §úþUØ~‡{¨>g|°CkmO¦?Ø ßg(�íwà«]ƒ1æD´c¥�6Æ Jü«T`Oü¨=PJ@ (% ”€PÍ" »Y$G_OSvE'‰È…"ò%fN*»ÓÖÖöè%—\ŸhVoÎ�Ó4}qOOOÙ4ÛÓKD ‰èª4M¿ÜÞÞþ»4M÷NÓôWØÏ<óÌ—f̘Aû€µöùa¾M±X<(‚û …ƒõ lg^}l½Û_ïÿ!"˜‰ŸšËåî( �8~[µlç=dŸK¥Ò+°ƒ]™¦ËE#R`çóùç­X±Bº\ܵ— Œ1ØŸ[e»ªðým¤g*% ”€PJ@ (%0ÑT`Oôü'ZuÃy°£(z�ˆÜäM“ýeEQÔ%"1óûâ8þNÆGøf>GDVf}°�1<†�í­|`²(ŠÎD×.B3ÆdŸÓ4 �†áf~3¿üÑß(Šö‘»Ô{â>í�PJ@ (% ”€h&ØÍ¤Y£®0 ÷ ‚àD»* Ø•-MØðK6Æüžˆ^BD×c[DÞNDHõT>Ÿß-—Ë ö÷÷c§z—LDp˜*¿Æ0ó>Û0©&¢·ÑÇÑaì6 |¸­­ QÇ¿Š¨Ü.€ž�Á˜Ïçwq)­ Ô‰¼k×]wýFe®jïÏ<Òì(ЉL¸¯‘‹‰h?fþ,!JøDt”µö/Æìr¿AD>*"WAðž¡ú<88xä lôïN—² Ì ôÁ ‘Æ5ÈÙ8<Ú„PJ@ (% ”€/*°Ç�´�íEí l˜&#÷õu.³oæž4M�ëééù­1æËDôf¾"ŽããpÀÂ… ;òù<œåEd/fžæê@](Èq½�ÒˆèÌüñ8Ž/2Æ@Ô"t¹ ‚73Ÿ\(~�‡ax!3Ÿ„ÿ®4�ÆoË–-›Ößß�tbWYk‘Úª\²æÕ™ß6ù`»ó°ã�\Õå³x—kÝh­}£1~Ýð%Ï»ÝãïÕgc À®œ^+Ó6‚—í lßgfþvÇïöiº‚ Ø fñµ®!Š¢w‹ˆÅ®¿;æF,rQ‡µvÇq¸µ % ”€PJ@ (% Æ�€ ìq€†á|f¾4‚Œá¶¿Ûy¡‰hm’$0Eqqé¼ÄZ b�†Ãpofþ›µöiTßæ­·Þzn¿ÏFÈö�ÍŸ?û|>?­§§çáw`˜�– Q¸×­[wÛš5kp¸1ÏŸ°Öb—˜\0·�;::ÖúôáúÜì~.^¼xÖÀÀÀÌ'Ÿ|ríŽ;î8ó±Ç{rÍš5%ü ‘Ñ�nv›ZŸPJ@ (% ”€PC@ö8p7ÆtÑ9ÖÚj¼›e">W¢MŒ”€1ædï"¢S­µHŸ†\Ù Dd�o­*òûH›Óã•€PJ@ (% ”€˜@*°Ç¾1æ£Îôx¿*Í©À‡1˜¨&°Óž¦é}.¥Lâû�ÿõ½ù|þàj;ÿÕWmW (% ”€PJ@ (�ƨÀnŒ_3ÎV�Ý Š-\Ç’%K�Âìhf~Kš¦%"ºµ­­í;*®[xдkJ@ (% ”€PJ`T`�Z“OQ�Ýd Z�PJ@ (% ”€PJ`"¨Àžê›·©{âÇ@{ ”€PJ@ (% ”€h˜€ ì†6\v­‚ÕZ”€PJ@ (% ”€PJ`P�=ƃ†!›]”ËåTIÑ5Æ­kõJ@ (% ”€PJ@ (%0^T`�1é®®®#Ó4½9‚× Ý‘cÞZ½PJ@ (% ”€PJ`¢¨ÀcòÃäÀãÖµz% ”€PJ@ (% ”€/*°Ç˜´ ì1¬Õ+% ”€PJ@ (% Z„€ ì1ˆ(Š.‘í­µóǸ)­^ (% ”€PJ@ (% &�€ ì „¯M+% ”€PJ@ (% ”À–C@ö–3–z%J@ (% ”€PJ@ (%0�T`O |mZ (% ”€PJ@ (% ¶*°·œ±Ô+QJ@ (% ”€PJ@ (� $ { á‡a¸]DDK’ä®1lJ«VJ@ (% ”€PJ@ (� & {  ÃùÌ|i/( æ´j% ”€PJ@ (% ”€˜`*°Çp4öÂÕª•€PJ@ (% ”€P-F@öˆ1f5½ÓZ»ý6£U+% ”€PJ@ (% ”@ P�=†ƒEÑŨ>Žã�Ža3ZµPJ@ (% ”€PJ@ ´Ø-0Ú% ”€PJ@ (% ”€˜üT`Oþ1Ô+PJ@ (% ”€PJ@ (�  »A» ”€PJ@ (% ”€P“Ÿ€ ìÉ?†zJ@ (% ”€PDGÑ95@¼V)% ƃ€ ì1¢†á~Dt},I’»Æ¨™-µZ| o®rq?&"ý@n©£®×¥”€PJ 1:hŒŸž­”@¨ÀnÄjUhì†ÀnÈ%K–l³qãF^µjÕ¿¢¡'+% ”€PõhÙùÃñÇ¿u.—k[½zõ†z.¤ÑcŒ1m¥RiÛ9sælèîîN­¯Ù磨ÓZ;Øìºµ>%0ÑT`�ѨÀnlË} .\¸mûµ··ÿiÅŠÿ¬çêŒ1$¢¬µ;Ös¼£”À¤! f¨“f¨´£SŒ@ËÍ<ÿ(о'"o²Ö–…åXcÌR"ú*3ÇñícÝÞHë7Æü/ Xk÷ìììÜ�ˆ^P,o[½zõÆ‘ÖÕèñÕÚïêêzmU(~ÝhýzþÔ# {ŒÆ< Ãk™ywk-LŵŒŒ@Ë} �10Mÿ�d­…éÿ°Eö°ˆô%0Y ´Ü;j²‚Ô~+�&hÙgSöæ#mŒù�Øû‡aøYf>5‚} …~×R­}cÌoˆè¥ÖÚãÚml‹  {Œ†;ØÌ¼}Ç£&¶äj'ìÙÝÝT3¥N`Ï›7¯}Íš5ÙA©صêÉ@vwwç»»»‹#9G�UJ é&ì5š+Y¼xñ¬b±øü 6üžˆJD”«|_�¦ÞV8gÞ¼y¹™3gn—Ëåþ5„©)Ï›7¯mîܹÅV4•mŽ[PÆäÙœ?þô:wV1¯–j<› °kÖ]­½¡v°Ýœ¦ÙUûZïýÐÈœÄós'°�/�]k6†{DcV/w=®õ ¨Àný1šŠ=ÕÒó "Zæþw½Šˆî¬ŠÎ‘ÏÑóˆèf^Çñ=\c BŽ#¢ˆh3ÿTD¾d­½Õó"ZADÛÑFf¾)ŽãÿÂjÖ¬Yˉè"šND÷ÑÕŸÁ$Î l"ú ¡~´ý3,Žã«3mïBD½.€[žˆn ‚à”B¡ð3ƒƒ>x3£ûÑ¿Eäf^l­}Ç„a¸�™;Ó4=³§§;íZ”€;£zG�]wj×EÑ©"òYÁÌsEä"z»µ¶%¿ÿƘWçr¹GW®\ ›aK†ocæï¹wú¥5„Æ[‰è:"ê²ÖÆÃVªLfM{6!ûúú.$¢…D´ ¾ÿDô�b±x¢�­bŒùí("½Ìü9fþ-æ .ìÈçó¸%¢§™ù;"ò|":Ê›ˆ»9ê?ÜɈè>Ì;’$I2ó˜Q_†E1"ú�f­½¤r€œ?3æ#ï‚Y3ÝBDXPûxÖDÜó!"Â;óŽ"3ÿBDXkÿRïœÇEQôn‰ˆè ¨ó"zŸµöw]¨o%=—ˆÐ&æO÷Ñ{­µ÷ cÌuÌ< "Q§›G­#¢/ZkËï¬líoÙ²e37nÜx3¿ fè˜ÑMAœ](4Æ€Ûfí£ŸDô ×þZ9.I’Ÿc†œ¯EQt ˆ|[DNcæÿ"¢cEd¯$IîŸÌ•ö}äZò;òËÐ3¶0£ú@†aøf†ÀFy@DþÁ̇eØü�™ï¼d‹ëׯŸ¾fÍš’ûଆ°&¢¯1óN"òA÷ÑÀGa":Ë}Hîdæ«ã8þœ1æÜowÑ/ñqqÉs­µg;�ýb×>Žù7½ÿNÓtNOOÏZc ü³ÿá>,·‰Èo™mO‘×'IrS¦?‘_2ó;ˆè%ø 'I‚�>T_BÄú¡&™[Ø=¢—£&’À¨ÞQÑacL=æ—¹\Î~Š™ße­Ýc"ú3\›Æì¨Ýh­}ãpÇâï^`‹È¢$IVU;¢�ˆ®p‹›×ÔS¯3i 4íÙ„$¢·2ó-"‚ùÉèuDt§µó|{aFì…ëù¹\î Åbq­•79qì3 ½À6ÆÀ·÷ ·ðQ†¹Éó˜ùÓq—S�¹çÁÆÌ||Çß«�Ì|ä~f¾KD ËÅ ì(Š–‰ÈWÜüèk®-l`á`޵ö±:ç$Þesª«D‹˜Û¬µÖÎÁn{{û3™>⹄0}3=b­…èÆµ}•ˆE伟ˆó¥k± `­ÅÿoV˜ã•ÇÑ |<ÿ¯$¢£ý8º ŠÍÚw #‹ÝŠeæ/‹È#ÃÍ׺ººŽLÓ4›ç� …�Ÿ´O”v|TT`� ›ž4ÆFõ�̼|ñržï^àß"¢cˆè‡ÖZ¼P±òz5>>"ò²$Iî5Æ`Ç÷ð���m½ XEVDBÿaª4�¢è�yÓmÖZì” ¢†‹ÅõDÔg­}¾Øîƒøuן3ˆ+©ÇÄq|�1;×Xß´³†á‹™»7¿°ÖêëÉår³W®\‰úñaú>íd­=ÿ^´hÑóÛÚÚ^Ðßß綾^úè��V¯¦:�Q½£ê„V�Ia=Ç”›3Æ`¢ûÇV‹2„;NM�íÌQaâ¾É¬µŠÀ®›M�㡇M.My6»ºº^™¦é¯ ®ã8>Â#Ãp 3¿‡™ßÇñw¼À‘¯µµµ-]±bÅ¿£(:SDÎ%¢ ­µ'ãÜÎÎ΃ƒ øî!°Ã0|/ë™ùÛq¿Ç ÊøŒ3þ„1?Éì°££cU5ßW"º»££cgA‡Åû?CäcóôÓOÿaÆŒ�¡�b±¸Sf‹ô 3¯ˆãxi�sÄ¡�UêÊBØ«A\ëþ�x��¦éÞ===ps/X>mÚ´í–/_þdö¶ªÇD¼�9Þ�÷Xk!¬ý{–~±£ñzØd6[iÅàr“ëqž¼½U�=yÇnKîù¨>�þåÁ«½yµ1æ<"‚¨�o­…™v¼ÏXPË u(ÓÎÎÎçAðS·+ο–J¥7A°V0ÖÚ—c`.ý"úœµ»IX€D?®ò¦®•}Θ~CD¦33vÀ¦‰È7‹ÅâÉííí«)Ù™Ÿ~¦•Þ7: ùA|Òý&±w3ó÷Eäœ4MÁýE4aô÷Ÿ{bÑ3 ÃC™ïïa·MD® ‚às˜¼z�MD4‡Æå¦|>?‚Ç�)3‰¢²;‘ˆËÌ"Ú‡ˆžp;g'úk^²dÉÎÅb»oر{ ×èÌ}´Öâ�¬¥õ4åÙ̈äeþÙpó„wÑ·‰è|kí^`A°›ß¥4Æ b÷"òŠ$Iîʈ:ìvïQEÑ×a)'" ‚ ìæÊ9ØÉ‚àUˆ^íö¦]ßj¸3óœS­µp…+—(ŠÎ‘OB`3sà ®ƒ »?æÓýýý¸÷ïÀ‚½1fØ9If'ç}ó•ìuúìÊÅ c úvJû …;³×2�=Ú9ÞâÅ‹_\,÷df¸Úuã]ë]eêØÃ²ñ›™¯ˆãn}Z¦(Øc0ðaÎgæ^;^ùÇà2&²ÊQ} ½À.•J»÷ööbu˜OA,g}�*¶ŸP:iÝ0™š^K`‡ax&–þ#X㣇]èçZk·÷÷/_9=I’Ïc†ÌM¹~ýúiÛl³Ívmmmk¼y¹« ~äðåVslj¼Sµí©J`Tï¨JX‹/Þ«T*aá “Õ«àþážõ‡Ö¯_?ǹ±<Ë &¤C™˜Apq©T‚£qu_ÁYp-ÁKcÌ ÝÂA°g[[ÛCýýýÃâbv›íwóÏœE ìÜü@D�Ac7f>+ŽãóŒlÖ¬YÈÿ _ÖŠÈí0UÇ3_À̧i ‘ŒþÂ]ÖGpù�ø…‹MQDðÞ�ã\s6 ìÒÖÖöjø`»¶#¢‹hçrkí ?íÅq*LMá6t 3Ã.AåÅX·›³NìÂD×s^”�Y¸eý¦<›Æ˜P_ÕÚƒûñ·þÞªiÚƒ�âí*Óqeƒœy^‹"3¿5Žãï;�}½µæãU‹·Ê«LÇeŒù0-ÇïišîÅ̰¤{V&gå²ÞZ»k=s ìëëÃ.6žUÿÜþӟòïùV’$ó|§�1Ÿ†»]µ´a#Ø#�ã¹8:ð;Gœò;ÊýÿH6Þ—ÃÎ×vØa‡Ca"Î̉ã¾ÝZ¦(Øc0ðš»a¨£ú@úIS>Ÿžßá­öB¯Ø.#üž`âô½ÇüϳgÏþL¼†ØÁÆ‹óÃÞÔÛ_qggçËPÍG{Vì �}�û°aÇ íÍJ.—“l ŸE‹Í‚àè ŽñþUƒƒƒ;©IxÃ÷›V FJ`Tï¨ÊF2¾"@Žq;ÖˆéP{ÕÌPG`búŒˆÜ‘$ ü�±èˆ\¼e�íÚ*çÊuAŠ`Š@J›L[+ûœØ~¢ÛÕÕµ{š¦æð÷Ü/Žã»á¶’ËåþND?Àn/rʦiŠ “ç%I‚˜ÔÕÕë�‡³;]•>Ø.Ò0vÕ�D�!×gŽ<�ˆDä'°áóºvËÝ.ûS𗄯y-�ÍÌߌãœÅ'B &k­�Â0ÄÂ질èLk-ÚC½¥ ì‘>1ãw|SžM?Ÿ‘ÿJ’>¼åEÑ Xèñ÷q �ýWÓ*•J;ôöö>îÏu»Ã³!¼½Ï4LÍE¤Z@¿Ä´�mŒ�FT91Æ\AD€ ø=_'"Ë“$Á3^..’àîöÊ‘ÌIX�™�HÓôÌBl£ýýý߇‰¸{¾ŽÍ\SöHæxííí?w;ô°|A Ø[{{{Ëj‡�pV†CÎ×2;Ø*°Çï™oÉ–T`�Á°DQt±ˆ`b´içr šÙ’«Õr4[D0„Pùã⡆ax+^¾µvE Dd3'qc—æÏŸ¿}{{;üŸË“ºjy°«l˜�í›5™Z¶lÙ´þþ~Dœ\×ÑÑq`__&Ž7[kü¬\2;è›Ìá·äB¯M ´�Q½£*¯!#°o°Öb×µ\ºººðî¹ÕO†«™¡ŽÀÄtH�íÞ'å÷�kþÏ/­•0#°e­=83y†_ôf¦¬Æ¤‚ÿ÷ËÜqÜÝÝ�ëëë{9DGIN IDATä(fîòñ&pL��:žÊ~O]TàÃ\ÐÊ—;�].™éréîåü]}¤ñÍv°³;i.b4Ú»ö¨(ŠÊþ¢ù|~[˜™gêÅ÷bwÝÁn±'òÿºÓ”gÓ³™orå·× ï&ä0%GÄlXŸÑ’%Kæ‹E, •}°½¹´wËÜ_°byk[[Ûî—\rɺzvE]"²2øÔ=KØIßÁ™ˆ?æÁþ– pèvw¯È¼k†œ“ ¸›1Ö%¯²ÖbG¸!SÏ9ƒƒƒŸo��¦i3#Öͦ÷ƒ1QÖa%3}„{X6*°[ö½0îS�=È�1?FµÖZï 6­lÑUŽê9�Çñÿc Š·‘ãé;‚ÈEó䓦M›¶¢¿¿Ñ-av¢]>þøã·Ìš5 %q|Šþ�t`.šè'‘b¢�EÑ[œ¿õ:—JQBá_Ó­µz1�ó4M”Ëå�jçr¬¤‹ÅÙVbŒÁ Îû�µöÊ-úîЋSO`Tï¨ZÛ¥æù¸ÿû‚ žÓÖÖöˆ7±¬1‰/ûyÖB‘11V`g0aº€±V½C˜b¢ý“ìw¯B`s†0ÇŽ9Ì·Q \ñß倎•Ûïp‹ÈÏü|e¿2>Ø¡µ¶'#T6ù»ÖÚÁΚšfÚ._ƒ1q.v¬Òîûq1câoCíAMy6Ý}þOç6€ ¿Dʆ©1v—w‡¸¬öl†a¸3ÃÇøßÌŒx<Ë0©†¥ZY`» X)ÌmäØõ % ê$0ªwTeÝ~»ÊÎü°ÿßä8ŽO¯!°Ëië01N`÷¦Ñ^¬ÃÔ;¿ØÍ}VÉ0‚yuÖüsH�mŒA¾\ôfâHvG¡PxĉpX=K`;?hˆpä.—±á  î/•J¯Àveš.ǬPª–ÀΚšºoæk0Æ`|n•ìªÂ»ÎûF{My6ÑÍ®®®W¤iŠ@¨;eº ¡öºŒ;ºð¼l›½4oå–ù »×Ø}‹÷Ívb Q¸áûïË—J%ãM˱ƒÍÌ›&«†Ð¹8`>â뮘‡–ç?.×w]0@_ âÍ O›RK 7'q.·fb”ëbæ/Æq|гÂC½WYkÛ \|Lœì|ÌÿÍŃÀ33órDóÊël`އ®˜OÁ¼׊ED¸ú•Ý=ªµoŒ�ÿ86,ò>jüpl2s¼Í‚ã�ým¯-´Ø­6"ÚhÚ²^œH³víÚÃÚÛÛÿîý·1¹Ûj«­ž¿Ë.»ÜçScÀ×°­­íñlŠ ¼˜ƒ ˜Q(ðqUY¸pá¶¹\y5áÓtòQVVEѾ.PI_±X¼Û§ÙUƒz’P�hÊ;*c"þÀúõë÷@@3tÊÇŽ@î$I®¨!°Ëyë01λ¼àˆtA"ÒODðIÖ»Šå�;“ñ°$I�⦜ iñ²™kÖŸ5c^{Lš¦c!°+Ó19�°¯ˆ@$©v#OÏØžÛ”gÓwѹ¼²‚ ¸·P(À­¬®â\àzöP’$¿Ï¦—óÀ�ÙíüîÌÌ¿Iý•�p>ѦiÊ6l¸}Íš5Õ:ŠyL©Tzb$ì¼óÎ˜ßø _›¯sN²·ËºâÓ`=T˜A˜?óÌ33fÌè[¾|9ÞEM+~Qn``àN|2ƒƒƒ3|PÜjí»Å„�;::Öúy`=lšÖq­hÒP�=i‡n‹îxS?�[4)½8% &‚@SÞQQı³d™ùeÎ]ä¡���=�¦ªšÀ�‰iM�mŒ)ï”C0¶µµ!MÖ`?ÁÔôÐ8Žôl³2Úì(ЉL¸¯XìÀŒV<°ÂîÞQÖÚ¿¸ Kˆ޵뮻~£¯¯;O؉ƒ{‚Ž!àÙ)ès>ŸßeppðÈ1ØÞÌ×ïBè£]ìªÀžˆ§®¾6›òlÖ×”¥”€¨N@¶Þ­H@?�­8*Ú'% <�¦¼£2ûn˜#gLïqîÞ­¥ªj�&¦ˆæ{[ÇG óƘïÑÛ]š.ˆë—0óñq#…\VÊÁà7ÚÑѱkåîV-óOŒ©¦¶;¦™ÈU].ð=wi°Ñ�ÖÚ7fÜ]È™g?eŒ�‡ëM¹ ê83Ÿìò¿Ñ‘+Ýc\¤}]@)L—µ6®fjêØl¶ EÑ»EÄbÝ5}£s ê°Öî¨�CKhʳْW¦�RJ`ÒP�=i†jJuT?�Sj¸õb•À¤#ДwTF`~ýúõ§ï°Ã >ìMë¡ÒLÓzÚkô˜®®®]…{ݺu·yVçÿø„w�AF†|>?­§§�›Ê.;[o½õÜ ~Ÿ�êÝhj�¿xñâY3Ÿ|òɵ;î¸ãÌÇ{ìI—“>؈Œ~ôXµ­õ6D )ÏfC=Г•€˜òT`7ùp9°O´ÖÎjrÕS©:ý@N¥ÑÖkU“�@SÞQY�m­E--BÀãdžj­ý‚Ë•]NÏHDç[kÏh‘®j76'ДgS¡*% ! »zUÎuûŸ[¯ÉÕO•êô9UFZ¯S LNMyG©ÀnÝÁÇN{š¦È�숈Œ Kð¿¾7ŸÏ<»è­K§¥{Ö”g³¥¯P;§”@ËP�Ýä! ÃðZfÞÝZ»_“«žJÕér*�¶^«˜|šòŽ‚)t{{{3ÿ¤Z@±É‡eË걋<|43¿%MSDx¿µ­­í;*®[zœ›òl¶ôj甀hy*°›ô5>œµ%0RúlŽ”˜¯”@Ó ¨Àn2RcÌGEdC’$«›\õTªN?�Si´õZ•Àä# ï¨É7fÚã©A@ŸÍ©1Îz•J ¥ ¨Ànéᙲ�Óä”z½p%0)è;jR “vr Ðgs º^²h5*°[mD´?  H½”€heúŽjåÑѾmÑ�š.MÓÃp‘===e·¼LÑgs‹}½8%09¨Àžã4ÕzÙRH|ÌK¥Ò¶sæÌÙÐÝÝ�Nô`twwçûúúØZ;8Ñ}©Õ~ÌxÞ¼ymsçÎ-¶Óìu,[¶lf©TJGÈçnذ¡tùå—?Õc3.ŒçÍ›—#¢œÏiŒë6ÆlÁ¹\î¡K.¹d] °hvZêÕì‹Óú”@+0ÆìBD}Dô6kíõ*°[y´´oJ`jP�=5ǽկº¥&¯Æ˜¥DôUf>(ŽãÛ'ž1æ�Dô\kíöÝ—!ö�ÌŒ1o%¢ëˆ¨ËZ/\¸pÛ ökooÿÓŠ+þ9‘×eŒù—ˆÜ�$É«GÚœKDw´B�ÃJÆ#½–z�Ãð+̼Œˆ^d­ýKEGˆÈ‰(/"M’äËõÖ5‰Žk©wÔ$â¦]U èêêzYš¦÷ÑË­µøÿlÑg³aÂZ�P�P�Ý(ÁÌùš»i0[êÙj;Š¢kDdgkí¡M#Þ䊆cfŒ�x½‚™?Çñ5Ƙ×Ñ�ˆè$kíEMîΈªÛ‚öfŒGa‡ax2v‡ …�1ðø!"º SÝ#¨n²ÚRï¨ÉMû©šA ³³óH,ÈÑ¥ÖÚ'T`7ƒªÖ¡”@3 ¨Àn"Í(Š.‘ù­¼³ØÄË˪Zjò:”Xœ7o^ûš5k`ª-ÍÒÝÝŒ—ùt³ÛN`W²F`ÃÔ9X³f òÐÖ[FsN¹îJ� “üîîîb= µƒ½lÙ²iË—/ﯧžƒ÷øXÜu_w¶¿Æ˜ÿ!¢7Xkƒfõ fèsçÎåzÇa„üFsxK½£FszŽØB è³¹…¬^–˜LT`7q´4vÓ`6å¹hÑ¢ÙA|;kèYš¦?OÓô«½½½�×ê)|‡‰h9½‹ˆ¶"¢[ˆè÷Dôñ¬‰¸1;t|ÁŠÌü YàwëŒ1ß!¢m‰èj":•ˆæÑOˆ(‚†sÿ¿̉ƒ xvþÐ'øðnܸñJDóˆh:ý�ˆYkobü†<Çs3oÇñ¾c̈¨×Õ}£É ¢;‰è×öŸ™yQÇ?õç9ShìÒîCDw×ð鬉xWW×Niš^NDGÁdšˆž`æOÇqŒ]zéìì<<‚+EäÓA¼Ý�Å£Øý‘ë™ù "Ú—ˆÖ1óâ8þœo? Ã73óGˆèh܃Dô+ùZ’$«pL–qÇß1Æ|·˜ˆËÌ×oìþ\f­=ç`¡åÁ<‰™�síþ[D®aæÅÖÚ§«qGjB"Z\,_›Ïç/sA q­÷3óq«ò<<̈þfÏ#¢µ"rZ’$¸ær‰¢è@ÁŽø\÷î¸ÜØ´·Íè*jÊ;jtMëYJ@ A@ŸM½=”€˜p*°›8*°›³ádE‡¸É{GE¯ú˜ù=qÿ¢†X(ï¾9qp—ˆ¼×çvE)_�ø… �8`f±�ÒÖÚÇœŸô‹Ý¹hkW'"|u¾}ôógÞß׿dø'C _CD¯têNk-Ä#vXáƒÝa­ÝvþüùÓÛÛÛŸÉ\ ÄDÿ›‰èk-D÷³ vgÍšµ�ˆ¶!¢ŠÈíÌŒE…3óqŸ^oÝ~Çr(f•ÃðmÌü=7ù�å¸ßÉÌW‹Å$—Ë=ì„c�™;DäÝ©išîÜÓÓƒ¿mV° 2Ü9Ƙÿ%¢­­µ;ú“Ã0|/Údæ÷Åq|µØàBÌ|‹ˆüÕ-\à§—Zkïëêêzeš¦¿ru`Áã@ø »ÿ>ØŽßZ·x‘�Å’cÝqŸ·Öžê9¸ópÿÀwãï îÜ‚Ëô ^U(~½xñâ½J¥î‘¢ˆ\ÉÌOÑñX¤‚൅BáÇYÆq2¾Ò¨û^¹…™ßï6`ys™[8‚ ý‰ˆü’™ßAD/‘Þ$I:«ÝKYlXð`LÝ5Û ¾V(~Vy^Åó� E�ÑwÜ󭵄a¸3c‘\° €ñ…¸g™ùè8ŽÁjL LPQq•ž½†ßQcÒq­T (}6õPJ` ¨ÀnâÀ;‚crÛÄj§bU  �1Øu~i x°Öî]ù·Œhº»££c˜gc Äþ �ýôÓOÿaÆŒ�áÜb±¸ÓªU«Ô ¢â#aæq/õ‚™?Çñ7Ü1´Û‰È)I’|Ñý†º øf¸C,ßc­…°.' !¨±»^S`§iºwOOv^±ó÷S9|Ú´iÛ-_¾âk³âƒÄˆÈyI’@Ü’Ûm}¢;¼Y�]«îÁÁÁ—8¡Y“YµÀp•â¯ÒDÜíÎb7½�$ ¹ÝÞo‹È‡“$�¨Ý¬ÔsÎHv¯öâ03¾ßŒãøØ0 oef¤‰y£ßM5ÆœNDŸ�8…ÀŽ¢èL9—™ÏŠãøŸ¸F¦}6§ÚM¡×«Z�€ ìíRcy°3B£&JfžWiºjŒ�:&ÝÖÚÏû“£(:_D> � ³VJf¾fÌþ˜v÷÷÷ÃܤôÂZ»é3Æ ùAì–1÷¾‰ˆ^×ÑÑÑæüKËÇ/^¼øÅÅbqOf†yp7vn}]Õv°½(öý1Æ ÿ§A°¡PÀ.`µŸÖ\__ßˉè…"r3wÑ/@Í ì¡êNÓô˜á˜�F`GQ´·ˆÜë:ýUìæ¯_¿þgÙTP•TÏ9#Øk­µ›h–.]ºÃàà Cîïèèxi__üÁ‹Ó¼Ÿ¼KŸü²Àö1—˽dåÊ•°:(—0 W‚³ˆ`"¸­ÛÉ‡Ùø9ø»3=‡;Á•ÖÚºßö‘»ˆè"kíI®*^ºtéìR©„æb ‹*Dt¶µöÜ!ö¦hø™>ß —c Ä4¬°;~Q>Ÿ¿aÅŠÿê�Ô€ÀÞÃ/¹k|�ˆÜÄ̧ÁÞ–a©‘mß‹‚¬_¿>?B¿üº_­aÞÈÌ3­µ×8I'ñuÓÔ•@ó8ס;Dä„$I`TYôÙln­I (�QP�=JpzÚ˜hè†áÙÌü©¡z?ã$Iàû¹©DQt5LÂ+Óqc> ¿lüž¦é^ÌüõjÑ®�1Ø}^o­í¨–JËóK쬨0Æl&°�1p¾ÝðIEñÁµ†Ø0‡O’¾ÊåbŒÁµ�5Dj1ÃðfF:­²94ýÛý÷f{¨º!â‡c6��ÎDQ´»´Ø=õ,DäÚ¶¶¶µrTwN5�EÑ "rY…‰ø¯¬µðmÞTŒ1°@ |>¿O±X„¹wYHW³)M—1yZgyë\E'‰È…"?gødc;´Öb‡–‡¥iz«ˆœž$ üüÁb3�½dÉ’�‹Åâ•D„èë¾øñR`—J¥Ý{{{a*ïïH+_Ë‚ žÓÖÖ†]ð×dê}ÀG{Ï^«ÿïöf©æ\dy˜äŸ."«‚ @º¶­µo¬`\vã¨å*P­�#ýÍq€»ÇojœÛÐ;j¤ýÑã•€ø�0 ?ˆopš¦öôôÜQ…‹>›z³(%0áT`Oøhšý�tA—†KõT6µ­˜¸ÃÏ3ò&ª‚ K€Xø4_'"Ë“$A€©rY¸paG>ŸG ²Û`ÞíöY__c̯‰èµöºuë¶r»àE¤"«··¾¨ð•=l˜ì²ér¦ÏC lØ Bå95—ËÝQ(q»†¸†ìvͺÑÏá˜�V`ûkéêêz…ˆ¼UDBçÇ^6gêÉ©uN5�í-*ö?³&ÈÈÓ�Ïçajo±X<Äý÷ÖÚçW¹ÊbÕ[1‹Å™Þ•ÇFQôuù Ì»™®ß‘E>8YVhÖØÆeƒ?òUiš~¹½½ýw0ãwæúC ì|>ÿ¼ì®´1f“Àö×âÁ1>ÁàààN—^z)°mVØ•ÏÒŠ•63#wöS°°ÖîUñœÂÿAþš¥|oa�Ä�šž¢%†!ÜnÎ$¢ç ÞI³ç�ô�A‰¨ËŒ¼÷Þ{ae30D�¼hÑ¢YO>ù俇9®‘n unÃí/Y²d›�7rö7V�ÍÖ;ožäÖ¬Aö NmoãÖh“£>Ö’6l(]~ùåø>V-ˆ±3sæÌ´µµ …°¶T©óÙh©>�¦3*°GC­Ê9óçÏß~õêÕå.- hhòºhÑ¢—år¹{†êE©TÚ§··Áµ6•(Š`¶»²2 “1ñœ‰øciš"’ñ߬µ{ø“ÝÎó^x�F`?øàƒ'03„Ó¹ÖÚ³Q·ó7[‡HÖÍØ~·^DK’äçh+ãƒ^i"^S`#¸×pÌF#°] 9˜¹cAæÊðÁ~®ÛÕÄîò³Lwë9Çó[DÏΘ³1>ÈT9Ë ÑL0²UÖÚE>ü¨/¹äŒ�ßqG�9o"¾õ‘ÞZ{­»WÐbv)•J; Âý(6¬%©{+øm»öË>ßÙˆ×ØGõõõ!(ÌÅ˦é(abwÿ„¬Ozö¹ ��…g1°ÝÀÀÀ,ÿnÍÜÏÞ ¿uFVAC﨑5µùÑ£˜ 6<‰n¤¿ˆبˆÐI|ƒ#0ÂÓë™Ä�°Ê¦†!Þ«s«ÅQq�Lȳ‰4ŒýýýÈ‚ñí8Žßí]Ë*]„*Þ�*úǬµRÓ@ÕQQ† ·_m®SGÓ rÀr)ÝuÇåÅØ† ™½Û_=ãÖPc ž%"Ø/÷¹rLÜy¨{Ó5ˆÈ—\´ôEð}�6mÚÛ±"?\Ýõ0«l߃TTHI†œÆ½ØÙŪ+R›•#D‡ax!3û€^å*\Ú¬7ÕÊÉ<Ü9Δéµvs}ÂbÂ#:·mî†HßÏÉ‘ý®$IÐg‚ ÖìÙ³aí³u_pOàù‡uCÙ7ÛM±øâÇ“àÓ>88xüêÕ«7f8”Ódá—¿¢ù“~éÅ:Æ(I’�c° �¾x_}¸,`æóŒ™?."¬¶‰±ÂÙ {h/k"î¢tÃõû‚gàÖZüþ¬RC`•u…¨2þèÛf&âÞ÷Ü_7o¿ýö1ó²ŠóO¶Ö^X­/Íø ‹¥"rw’$ߢ¾¦¼£FÚ_�Ä��ÀÖI¼ÙKb¤÷h ?!Ïfåu×#"†¸@ÙÝÝ�`špá©Z`jëƒlÁóüªuŒT`ãýSiÊ^9ש£O5ûSq U�;è 9´T¢.æòb;Ê—D¨ç7¿ár•¡J­¾Õ+°ÝõÃb¬æ˜ ׇFÿ^‡À.Çx©•Ês¿åË—÷Ó�zÇh¸Ë©ZO=ÏÆp÷w÷üøFÃ>&W�Ý$¬š»I ÿSMÓ>�Ƙ6f.§B®goN;Toý9išò† n¯åÕÕÕµ{©T‚9úßwÞyçû\ð†AÀ²X,400p§7�E ­ÁÁÁÙàT 7ô³päçÞ}ݺu·ùë4Æ "õ5üÛª6[/³áú ¦mmm�ûÔb���»A°73O ‚àþl4îZuÕsÚÌ÷ööÂZ fÁ$‚™»ì²Ë]ÕÆüDdf¾ÇG†¯¬ ©¹¶ÞzëýJ¥Ò f¾k$\‡ãå„·Xkáî€�>‚×�DþÓÃ�_ëïªæúõ‹Å»ÇÛ¿.Û/pìÌü¯§Ÿ~ú·Cùž�öz³ç¹]ó¬µ>ø_µj›öŽj¤ÏõLTt?$a�ÄWÁ3Ü$¾‘{vÎmʳiŒ�ëÔ6ÖZdÙðqV°8û¨µvüæ¾Õ÷bÑÔY½ýŽ™ÿ;ŽãÓ«=›.`ê‰øæÑ�Ì|9¬£²&ân�‹­/¢ºˆ\Á缫U†sƒ ø¤ˆ¼É™Ê )-aA9輑Îò4fF¦“cEd¯$IýÊñq‹zXà?ÁY¸!»ÇÕŸq©LË‹¥.E%6"°èû� Œyµ¯Ï¥Ñ„‚‡BøüJD¾æc� ÕïÊ>‡%d< IDATp@Ù’ VuÛ1ÓÇn¿½ìZWµ Ç ' '°�1rÁgwAß™Ö� ¬µ‰¢èÝ"3õ3ý"¹³ <–™?‘IÓŠ4”‹ƒ 8s7Î+`‘…q&¢åóùhÅŠîY..£, ±à _j�ø:ÈV³Y€Uïâ`AD�0ó×â8>Å¥`…%ÚQn±ÁU‘¹båÅŸ÷]Žˆ0N§Yk/ÉB5Æà>xgš¦ó`MéÿæâÁ‚nÒ–Ö1ÖåÌ<ÞºV¡Ì¼5ÒÃfê|a\Ëuº>‚?~CpWlZÜÁ)>�*P|ðÁ“˜d‘�çß"r 3/nd>4Úw– ìÑ’«8Ï¿\˜†ÎoR•S¹š¦| §2@½v% FG Ûñ±Ï梯RSSÞQ:‰ÿY�Ä—'¸“b?º§j\ÏjÖ³ù-"BÊ—"ˆÏ6á®äEW ¤h``àkp©"¢k­µïªØ·"¤ó„µÓ»|/°} 2ˆ8—Ùb2&@€ ìòÔSOýkÖ¬YˆõƒÅ¿bÓ€™Q|©/€°ïêê:2MSX§ùòb}¤i ×$ÓšíW%cL9kDAT¡êr¬ÌÓ�‹c�Í¢œ…"MÓ9===k�e,Ôp]W23†bz» ^[(~\«ß• ×nö׿ҕ"t3�Ï+!^ŸU`5/'Ü 2«šˆ»Ø.pƒËØ×°€€˜$NÏIÓt[çÚW>ßÕWÄéýñÝoXˆ(gΈ¢¨œ’¿ã"tÁDŸÈçó/�ÈÎÄÄÁaW!Þ�w¬–Á9‹9¸?B\^q988xe{{;Üí0fÙ°ÚÃâp>o­…•§_dð !À��ãN6W?ꈭµHéZ.Nœ#Ðï,ÙµT* 9ÖUž d5Á7wG_g†Èèsu&6þ†”¡pc¼MD~Ë̈3]D^Ÿ$ÉMîº|Dä—Ìü"BÓÞ$I�w\‹ ì&áÃp>v2ñ¢hR•S¹š¦| §2@½v% FGÀ-–þ<è­JMMyGctÿŸ šNâÿç¡å'ñ£{ªšsVggç‘íííyôÑG"êvSžMŸ ñ(°“ç³?¸+)»6cà>¶4ŸÏïòôÓOo¨%°�¨A0Ì|©TznooïãnQézìèf6vÍ‘#“$ù‰/Ÿ!"È÷Að‡4M‘ùã¼$IÎÂßÝîäÃpŸÂ`F¨b§õ7nrö_9JQ¡/è2‹ÀçYÜ®ýz"êC /°� C S<ÏgÑy>#KE§"å&2gôôôÜ‚c¢(z�ˆÀíè kíùÕúÝè]ÓÕÕõ²áx¹þVØÏ<óÌ?f̘QŽT_,wò–\ƈ5ÄàYÇñR—’s[kí¶Æì²"E' ,·GÐ@—!ærkí N�>Ï[àÀ0 Cf¶>…l†·"{ ½1³ƒ‹û÷óR„zV> h¯* ¿Ž¢¨ü”™ÏŠãø<«º3fÀ n¨Õíâ—»ÒÑѱªšëÁüùó§···Ã}ä)\—ǽEä^«§ž±¥Àö™S¼[!˜•ƒô!^2ßø{1—ËÍ^¹r%îQÜ‹ßwYGÊ–¬ãYT`�'mm«^Mù@ÖÛ˜§”€ðÜDãbkídž Ò”w”NâËm�ÄO²IüD½-| NÚ¬)mEšòl.^¼xV©Tz»ÍÖÚwc b Ól�D¤pc ‚’–3ĉ�ª;Ø.`)ó­µ0‘./*3þ½›Ä Ž�8Û¸qãaÌ|¯µ»�ÜÝÝ�ëëëƒéú Eä(fÆnbYdø:™ùŠ8Ža*ëM�‡m¿r\½P“$I’yGîÏ̳â8þ¡5Ù´‰Q½ID`ö{ZÇŸsçñÒ¥Kg—J%ì( ­SD÷Y/ªõ»I÷Ù�¼œ«*°á†Ô—>¸ïOF0ÃTûÀ0 Wb J¥Òžù|þ`æÖ°Jy3ï+"ÿÍÌïÌår¿*‹ÑZg}P®6MÓ­™ùÇZºë®»Ñ××ÿûbGGÇ4/váWÜ×ׇ{¤n�í…ze4{ßg¿˜ã¾{�Xk‘жf1Æ|V ™ »ßOxK„zÆz”»l€Eš\.‡ç¬\D‹THÛ‰8>Óo&"ì _”ÏçoȦ$mÒýTw5*°ëF¥Ž#�¦| DZ¿Ú”P[øðçr¹9qÿbˆKjÊ;J'ñÿ·Û¥“x¾.ŽcøÌ–K+Oâ'êQwñr¨¹kSžM'¼�Ùây0Ûu¢BvÌuÛÚÚ^888ø3~®C lA:ÆÞÊt\Þt¿çr¹Þ4M†ÀJ’äÕ5#ÆÆ̼ԛ—;“l˜ŒW ì�Äq ŸYìô-®}¤F¬l3“¢±¼Z­OÙ «þï• K–,Ù¹X,"²5|g}�)9ú}¶µöÜŒÀÞÔï&ÜgÃòrã\U`‹ÒwbWþ$k-ü•7c DÞzkm‡1&ô°ÂébæÃEäm®Ì|'~‘ƒ˜yÑ3Ï<³ÍV[mu€ˆ”­j”ûòùüŠÅ"Sž%¤‡‹�P¹ƒív×g!@n¶=œˆ—$ÉY?ô¡¸‡aø·°Æó43ß��”bÖÚƒ],ÜÜæÇmv+çóù§6nܸ!ŸÏÃO}³{!ÓnÝ;ØÞ X,ÎÌ+õ.Þl¿^�íx•Íæƒ x}š¦ÈRókí™îoÃŽu=Ûsö32>ØXÐ@`³l†“2»\.'Ù¸‹-šÁÑAã|Ûippp§K/½ôѺ^4M:Hv@""q¡P@„?-Ø?þöíííð�ø˜µÑ«•¦} u@”€Pc@ iï(�Ä—#Óê$~MâÇàyª«Jˆf.ÄqŒT–µJÓžÍ0 ÷s»�ˆz]6Ýîìì|i¿w�oìèèØ&¼C ìR©TÌårðyÝL0c¼ Q¾O‡À5Æ@¸ìP*•v€Ÿ¶,Ÿ'¢S rݱïõ¢Ï깃½hÑ¢ÕÓ~%Ð(ŠˆÈ*fNâ86ø»›ÃA¨üÃZ»G�Û‹£­|&ï<–;Š¢«!°†âå×2ߘ¦)#+ro—‹1æDtEVxc´ìuø»·ÆÉ˜,ã·S’$ùb&µêfæßˆvaßnkíIn§zï#�zýxŒÐD| 3¿Aõ¬µ×ºK€95vñ÷ÚvÙ,‘à]>òç;÷°v¬«l,8ì³~ýúüš5k`�þaï‹ãøjcÌ]ÞÁþ…B m°ò™Ößß?â tttØ×ׇ`n7gã§d¬0^í£�×õ²iÂA*°›1Š¢‹EäÄaÌ–šÐÒä¯Â¯lVøkT^XÓ>�“Ÿ˜^�P-H iï(�Äë$~²Mâ'êyO÷�Œ˜BP§m�Š+I˜‡CD”óÃ�ðD¨¡¢ˆÿ?Ð ‘¨!Z4¬ìŸœØ0ýFà´;°cîž�‚ˆÕð;/•Jã=hWD°A�€ÏºÈÊ0)>*D¶F&„ÍL­�1ö_9®ngöDû†@ ‚àØ9Gän"ú¤µö³õl/th &ðDôV"BZ'DÑþæÀÀÀ‡§M›6·Z¿¹×¢(Z4/ ¾fq¤�""øÝ#¨Ùe¥Ri3_ æÅbq×U«V•š¹he“üR©´;Rªcüxâçräyw,ÒzaG)Þ.‚àÅ"rî)f>ȦD}G¶ÓÒ4� 3wßÕ½ƒí¬ îFÀ5f^X*• ‚`‰‹¿ÊZ»Èõiƒá˜»T®>ÅiyߟSÏX···¬"MW „D±üw¼ÓÝe�EÑ[œ¿õ:Ìsב�hè'[k/ôb ¶išþ(—ËÍuþðùb±8{¼Ó�ªÀîNªãïš»Hîc šoOE�íü-û'"_ý#4y�„Ï↠JCåIFÚŽ™3g¾ ­­­¨V'“w¬ÇªçƘn¹;I¤Nª4M`g'ì:‰×Iüd˜Ä�Õóפz›ýl–w™™¹3Žãr¾å(Š®‘w1ó‡â8†`ö»i}j&cL9ú·.µ`Á‚ç´µµÁ¼â´\¼é¹«nçâ¡\œ™O†´Û±C ®C2u|é?ù BéF9>²è’µb½\êlÿYCEÌÛ±;‹èØ(ë 0ã8F¥šÀö~º³Þë\ŽlÔ�üÞ ˜ù|"z3ØY3rXŸ|ÀZ‹ßǵ¨ÀnnØõCôQ@Ç+HIý=kî‘XiLJ£X,Þ†˜7ó© šÛšÖæ^ìØU@TÏZ|(]/÷¬µoÙÒÉc^�ËåÍú'mé×ÜÈõ¹´YGfóqÖ¨O'ñ:‰/ßSqßÈ36ç6õÙlr9Š¢—¦iÚ1}úô_/_¾~¶Ï*H£´õÖ[Ï…)úŠ+à—½YéêêÚ•ˆv_·nÝm>]™sH Uö�­Qêj¿Ú¹¨?‚…Báw£eâvSÅZ‹:°cŠ d{33L°Ÿm½Ã�ׯMUôT*½ éxwÞyçûº»»!Ü*V*•ö+•JO͘1÷Cµ±‘}˜ùžÊœà#iÜÝShk3ß5Ì}2’ª«;š±ãÁÁÁ|oo¯ßVÝ .Ü6—ËíODmµ®#Š¢}Ó4Ý‹™ûŠÅâÝã½sí;­»áÛhS‚öOYk»›P�VAÔÊȺÆ' ÃÏ2ó©A샒 캰5tPÑ5ß/"ˆdz³ˆœ‰À4 58 Nv~U7h\Éøt»1Ìüó8Ž Ób+¿£êšDë$¾¹÷”Nâ›Ë³�ÚZùÙlà²ôT% &ØM-˜=A°aˆ¨ØMheJUÑJH<#Xi­Zº»»Ÿ£0{@=»Ö¹->ÒCò¨·ïcqíuì“\ô×£‘¿³J_ŸumcÑÏz5ã8Ø#£èx•} '±ÀÙEëÑJ`˜(÷�I€F»¨”@ P�Ý‚ƒ¢]š˜l˜uAáµ’™‘·;Ÿ•çóùãW¬XñO?6ÎÔý8D9t>I?‘/¹´tÑé�¬C Š���‹}"BtPDÝA.Ò4�×ÓÓ󇑌» žq†ó‹B¾^‰8�™Ï†�1æ,¸ÃApÒx n˜Öäóùß‹H’$ɧñ› br!½ß™N߇ëÀø{EŠÈ·Eä4fFŽVYYÁÌo‚àøì¢RE§Š‚K|¨§§çGÕ®g(n8Þófà‹JDèãóˆh-ÚG®F_gEG¸ /ðµAÿîøª&âažËÌËóG˜~sˆ0Zym{%IrÿpýD?:;;‚ @DH�‚`%—Aðö4MïJ’ä¬ÎÎÎà ¸RD>�ßEäMD„諊x=3ãzöu÷Ïâ8.¼qàï?ä …ßÔmAœâ£`càÿ¶ þIˆªë|Žž ¢Ë¬µ'vvv¾ ‚Ÿ:Ÿ&˜²ýs```Oïo5’{mª»páÂ=òùü_DäýI’Àïr¨ÒJ‹€Seˆô:§0ç¾q”µv¶>›SøFÐKW“„€ ìI2PS¬›2yu࣋±‚IöK\Òú‡Ö¯_?)|îJ#"ú3ïä‚U@È Úæû`ND/F´Oˆž���8�í‡òz"Ú~ÚDô�µ¶£Þ1Ž¢Âê+ÈÊÌß‘×8‘·)i†=z’Ï矷bÅŠ näÌårès6r䯉9BpyD-3:Žãs|Ô÷Lß`æ3™1Žc,2xQˆhšÏyæ™g¶¯hl8nðÁò9]•`„Åo®[N‘IK‚Ã0FºH’øwÕèšQ!ÈÇÉNÐ^/"×är¹¿"jiöÚ‚ 84MÓ£‰hu­ñE?ÝÂÒH™ù"‚À#>@GÙ¿; ÷!/¦«�D°xƒè©¾ü[D~ÀÌ`>Ý%qAU0fÓ!¬Eä·ÌŒ`(ÓEäõI’܆áWÜ‚ꂸG ,’`Ñf~7¤iz®‹Ì‰1Ç‚‚Þ _ª–Æ LÈ;ªñnk J`rp߆;­µï›œW ½VJ`*P�=•F»®5Š¢K%usú ™¼f6„ò 3yý¾áR”£%c°;{øÀÀÀ¶~G0Š"‹h�>ÅB-q7›Ò5DQ„�ïçM›¶]­€'Ùas¾ÜÈ#^,‹Þ3æ|ð§Ì|˜O÷Q�ÀÃð½Ì|µ�zŠvà—9cÆ äë|®oúôé; !yÒG ºå¬Y³Ê�W|P—Ã�K®µÖ¾«Ú­V7/°}IÔcŒ9�ˆ>ƒ< q_†á­¸V"z£µöÆì1#É™Y<Øtm®®aÇ×ÿíC‚ 𹑷‚V•11æƒaⱈBÚÑѱ-¥dR‰”sÃc°s½)N­µ°L@ôÌ3󉨜ëÔ l4NɈ~k­�ܵÀ½A}°›ÿî›�wTó/CkT“ƒ€sß8ÛZ‹÷¬% ”@KP�Ýàð êªÐ”?ì36*š�ÉkF`oaº««ë°4Mo‘åI’ waù¹1Æ âžD´§ˆœ sí4Méééùå{³œ�Æ¤ØøXû …;‡£˜QZk±#[.~wx$;Š¢¯cç]D>rSúrv²±£JD[¹Ü”•»Õ0KŽ˜¹ìÏ쯗™ßÇñ÷k\ǰܜÀÞÃZÛæëˆ¢èuH‚\�»ì²ËúúúJX`èèè˜æýß»»»ó}}}Ø�­;?¤¿+wâë_c Úú»µã_.>�]¥ÀöÖ8Æ™¶ÿ„ˆ®´Ö–St ⥈ÜEDYkO2ÆœÀÎæ¹uâê|ù¤ßùîþ Ãæçß`æ�Äq _ïrñfÓC lc üzaÆ]67Æ@˜Á‡¼j�Xfæ§�ÀÞ¬½ŒÐ‡Ïï|c Lšw\¿~ý60£¯Va=ÜœÀ~®µv{_RMÑ-¸¶¶¶¶¯»\“ÏÒõ9ó¦Ø�]ymCŽoH!_êë­µ0ïÞTœðþ&⡵¶Ç�“_¬9=I’ Ü=�Ø7ƤCÝëׯŸ¶ýöÛ&â¥Ri÷ÞÞÞÿͰ‚ ÞÄFƒœ ÷D�úï*°G�NOT##àR¡í–¦éœžžäJÖ¢”€hi*°Í�]?@D%¢s†É�� 'dòêw°E¤7I)+c ü°ÿÀÌÄq|º1‚~ʧÑ÷üñ?Ïž=û"rîp»2vE#ØÆøð^'"ŸK’íû>Î#¢o%°3»§^`ÿ½�™ß)"0?®,AðÊjÛq�XŸÁAišÞÃÌIǦÖQ7'°wÈæ!Î ìR©ôÕ|>�Ü¡XkŸïÛZ¸paG>Ÿp”;Ø› ìáúùÄOüfÖ¬YÈWy¿µv¯Ì Ï6„÷f&â"²(I’UŽÙ¦Å‚;ØØµF`3pÛ¬är9ANk¿ƒ�õ±wõ«À®ÿ•´Ù‘îýDu¦[œ�wÔ(/MOSJ@ (% Æ‘€ ìacþæ&õó¬j‹?= ÃùÌ|qvw²ÆEOÈä5c"þÀúõë÷ð;±ÆD³>KDŽcæ; ¶|ÊZûJßï<û…D„TDËFp¯rñ~»^`GQôÔëpD6Ç1^Ìgv°?OD§`¡ "‚5vçßÚÖÖ¶{©TÚg�]æ”6Ô.¼_¤ŽÛp¢ÔíToÓÖÖ¶ã%—\‚^¸¶";ñM{ýÜ€€b³V¯^�ÿÆ|fÞMØ0ß7ë6àvpîGà5kíþ*°›ÿºAl4>!ï¨æ_µÖ¨”€PJ@ 4›€ ì‰:ÌOÕ¹ëÑ`kSæô ™¼VDÿ.Yf~™ˆ 'îC{¬^½ºßƒ]JD ?žˆþAä¢oc€Nš6mÚŠ���¥È¹ 1‹´LBñáv°} °4MèééùmµwVÿŸ½/“£(ûßž™Ý„;€�,‡ ‡\ry+ˆÇ‡Ç'|OÐÛÕ›`Dñ@ñ`ñ¾4’LWï&ATÔxà(*Ÿ "~x‚˜„˰€`²;3]ÿç7Oõü{{çè�c�Ù·ž‡‡lO�¿ª®®_½<‡_[^f>jæÈ#ØÿcŒùúÇ̰©Þ›ˆÚ ©,ÁîïïßÛqœMöYž™¯5Ƭ$¢S‰è­õ‹k©Q£ ü„aˆ &¤MZëýê¬P8kˆÛèè(Æ\S‚m 6Ô÷?KD·ãr Ã]˜ŽÁvj•`Ãþ:e?�ìç·23.36RKlÏó^fí­·Úµ÷(3cî&¢wi­/šÁ†4¼hŒØgŸ}®¨³}Îì* jÕû¡aóñ˜LË•¢_’EA@¦!Ø-NH©Tz$H�$µ�i9¼Æöí6ìÔt‘î€+­õïð‡ëºo²±‡AèÊä’™ß�ÐU6´ÒÑDô"‚Wíy̼¦§§çÜÑÑQØiMk�pJ夔úÁKöñ¨_)õ"‚s±òßÕà\¹råÂb±ˆ „׊Ò�ˆè¿"‚­”‚“°ïÑi6BB}†™/Œ‡é²¡ÄÞ)Jß*•Jjxxøa×u_À̰yCˆ§/&û£”!†*óyZkHÄk¦”¸!œÔ8‚9™#¢÷i­? /æ»ï¾ûZxm�5<*ën­5ÖÏ„¤”zHqdƒ]kliúÙ××wç}÷݇‹ ÌÖ HìE6<Û×J&Rç·k—!X;Ïff8”+�Ïúûû�vçö¸í¿ëºç@Û#>cL>‚¶žr˜.ÇqöÍçóP��ÖÓ8q×uѧsñc6›ÝyÍš5eïï’Æ#`/›î�{¯o€Ñ´ìQ2o‚À\DÀuÝeÌü€Ö¡% ‚€ 0ã‚=ã§hNvpZ¯1‚ýé‘‘‘ó÷Øc�ã …Âq'RÑl 4Ö¦M›žÓÓÓsOg!®vØa‡'/Z´è.H ¡Ö»mÛ¶¾ùóçoY½z5lv&« üHooïÞ�Âv)¥ 鈸çÖ±±±£ǹ!"Ø1µ§1fß piò5!Y2þ4f^ÈÌœŒG|¥Ô5�xg2™Ý×®]‹ðauSÜÕý>00°�1ý¾#N2Ó–¯—¯Q?|ðAxùΠݳÏ>{¨ª àÙƘwA {KiÙ²e;g2™ãˆ(ÇÌ·i­ÿÕL…K–,Ù-›Íö Á;¹¤êïÀq6&¹ÒZã©Qš–=ªQ§äwA €_ fþ©ïûß(Ý8N“ tB°»g.»i$Órx�l­õyÓ¨R 1·Õ’ÂÖêSÿ3«ìN�añâÅ=»í¶Û!Ì )}YbÛ©¶fb½‘¦A$ ‡j¹çy—Û°g'A€P\’º�iÙ£ºN™ P\¤†a-±~ß÷a $I��€ì?EÝÑA×u¡¾{q&“YšBB:-‡×™@°¡�Ï篯%mž);r4Õè0 Šl±»cÁ6…çyPÇ¿Úfƒ£5¨ØCU¼lß>'@˜Ûƒœ–=jnC.£Ÿ‹xžwª1æf~‰ïû?�‹ȘA`ö! »…9ƒ“§R©ôd‘V5q1°QÙ´^¡JÛÓÓ3ÀÌ¿ô}ÿ†Æ£š99¬ éR"úI-ÛívöV)ÛãDzÙ솹j׋Ð`¹\î4cÌ �1ÿdæëÄF°�«lF×5-{ÔŒFD:'t¥ÔÙDôEcÌSƒ @$I‚€ Ìx„`·0E;=x“ˆ�=m;ýh$§ Ìq´xÌA IDAT„`Ïñ ßV­ZµËØØØ¾£££ß°a…JA`Æ# »…)‚�@k Uñ4iV^Ó Dò‚ÀÌE@)µ¿1NΘ¤—âY±GM–`CezÆ Û“3æy޹Ƙ‹˜ùE¾ïÿ,ÍŒ:ƒƒƒa½¼µò4"ØiêN¶»xñ➤š|R‚�¢^œ™LŠñWÍ'ìÈIA@º!ØMN¢ÄÀNœçy— ·ïûPO“fÅá5Í@$� Ì\¢ØMx)n륔RDô~"ÚŸˆî#¢�Dô®ÈîT)õ]fvŒ1ß$¢Û|›Œ1ï ‚à+ÂJ©·Ñ9DtÝÊÌ—c¾POE|pp0»eË–‹ˆhíDD ØW‹ÅsÖ­[÷o×u?õl"Ú•ˆdæ/ù¾ÿîä¬*¥>ED/fæOc>HDGÚüçû¾?åw]÷HÇqÞgŒù/«r~;3_eŒ¹ Ã}ÇùíKDE"ºllì�\.÷iô�™û�1Ûôt"z”™ƒj}‰÷Íó¼cÌGm[èÿ73™ÌGÖ¬YsD°ÕëºîiÌü6"z‘í×�Ƙ/A°myžw‚1æ;˜f~%½ÎóÔ þï‹ì™»HÏA@h'B°›@sÉ’%»õôôŒÀi—ÖºL%µ�¶^ÛÚ3©Lº×uÏaæKšðRܶ=ÊØXb 5ìÉ6§7j­Ÿ °•R°E>ÔÿCW"Zjÿ~²Öú^¥Ô;‰è³ žDô=":ÃfªG°•R? ¢—3óuÆH§ŸGD§‚ k­�ó<ïLcÌ»ˆè"ú¡1æÛ±LÙïc^aŸ]ÃÌÐòzúÀÌoñ}ÿKp”¶`Áøá‘ÿ™1æffF?a+þ ÌE†Á�‰h+áBa••žƒä#á*ê¯ÆƘ3ã— ‰>•%ï(ƒË fƼ=�ˆnÒZŸìzõ®X±â©¥R öçE¨É3ócDt.Ç9%ŸÏÿ>YÂ0¼6Öö½Žã<;ŸÏo‚Ý5Û… D�ÔÁN ÕøŒðŒ](îIé´«ÉVæl±¶^ç,‚2pA@hˆ€ëºk™ùÌ&¼·e�Ø' ÃM “½½½û¬^½zÔjËW3ó³}ß¿!"ØÆA`óœODcæWc~d i¶T*í oÛP‰^°`Èø‹jì���“Â0¼äÚ÷ýçG€¹®»‘™_Ã̧û¾ÿÝHEÜqœgäóùßVÖó¼2Á6Æäƒ X�<1rú Özï���£Â0¼å  å¦���½Â0| Þ‡Z*âÌüß÷1_(Wî;”£´ÖÉ>�uÖY;Ο?ÿ_xî8Γóù<.%pYq3ŸËåö»ÀJÆkÖëyÞyƘO†aøü¡¡¡ëP‡çy¯1Æ@ËàýZë�Çv‘™Ÿåû>Ú˜�D‚Ýð•¬dPJáÂ%ÛHC!}�’S�©C@öÔa--¥G -‡×ôÍINA@˜‹´à¥¸-{”•_ÎÌß ¢ÏDs†!T­?ÎÌô}ÿ£–`?Ek�‹òxžwª1’â÷Ñ Æ˜_B*¬µ†s9EįÁö<ïÆHŒWi­¿•SJÁ·ÈwˆèãZë÷O†`3ó‘¾ïÿ1V×oˆèD´£Öú?Dăƒƒ™-[¶MDc^ÈÌ È7h­ŸmI0ìœ'89ƒt]ký¿6°@è±kµÖ/L®ßÿ–Öú51L(•J‡çr¹ë …0†d¼Q½|öÙgï^*•3ÆDDPU‡¤ÿCZë�D8Ç/ª½OB°Óï2J©+Œ1§A°WúR’S�™�€ì™1Ò‹ñ´åð*  ‚€ Ð!Ú²G)¥` ÛëZé Zës,ÁÞ[k½[Œ¸>—ˆ Ö I6$ÀÃI"I�kl¥Ô— îì8ÎÓòùübuC�ú÷Dt¹ÖúÍ“!Ø###½qgbJ© Dô":Xkýw×u!™„5ÔÄ‘·ÿnH°K¥ÒÃÃÃÿˆõDü—ZkÌǸ¤”z#}%º¤¨p¤"^¯Þ•+W.,‹PÝ?%VGÔç$Á~›ïû«kM¦ìôo£çy7„a¸=‚8îé+�œ‚€ L#B°§ü¹Ð4b`c^5É�d[¯íÂW)•+•J;ï·ß~�4òŠÛ®6ëÕc�qäi*ÚœlÍ`Æ™Lf¨·N¶=É/L1mÙ£"âjŒùv%Ç�ËåºôÒK·Z‚½‡Öñ¨Ë ±¢#‚í8b]‹ˆ´ÖÚ‹å±ÝP‹`GÓóÊ `‹]Nžç½ÙsY¤Ê=‚�Édv_»v-|”Dý,Ûx—J¥=2™Ì Dôc"‚šøy™Læ¨n+¥ Ö.º®;Ûª’W%Øžç½Ò[ôq˜¬\¹r¿b±xð¶mÛ~;oÞ<�ý ñµãõ*¥à  ྆áç{zzþ†áV=]v‡^<¥Ô}Ìü#ß÷#_jIªA ýÁnS¨Ïcî ‚à¶&ŠÏ©"MÄÀ>m9¼¶ h¥¤-_dækÙÖµ«­4õT“f¥)7•yÒ`‚�ÉdZ»v-8Q2dÎTöWÚ&‰@[ö¨HÍ;©æl=__Ì̯‡ t#‚†áÆL&ó"ºWkýä¹ýšõh}~ŸHŽQ)ÕOD3¯ñ}û\9¹®{3ƒd—‰÷d¶1æuA@å�V®\¹S±XÙ¾_k½Ÿçy_7ƼÖóœ(–vL•»¡{2!؈Òî$&ønƒ‹€b±xa ‚½�ˆ²D´Ct©S­‚=É'mv¥4*®ÒZ¿'mÉ'‚ÀLA@v3¡”úŠUSKk¢º®.bc`«µFؘ´©-‡×´�5Ê—†,6ª£�¿{ž÷mcÌÂÈ^±�u·«®4˜5ŠuÛ®¾H=‚@h×ÅJ)Ø+Ý�bcþ›ˆÞLDOd³Ù}׬Yóx#‚ ò¬”ú©uhµïuÌ »g„ΪéEZ#===÷Û\ï`f„Ÿz�Œƒ˜bß6“!ØÖ:B…Áƒ6ì»Rë<­õ§=Ï[nŒ"¢+�1ˆÀq,3’ˆæ¡="z¡ÖúoJ)�ZxíØgŸ}®Ø¼yó%�ˆpµ9ޝY<Ðæë‰ªãe©w­K½¸;º€ª¹1æ×�Æ<¶lçÇÆÆÞÚÛÛ{$¼ˆ#”—¨ˆwàm“*A@˜eÁnb”R#Ì|Ù$â:7ÑJwiò2¢]‡WHb^í8N9ÔM†¿ ‚j”“JõÈ"<õnܸê�°lkt¦J%½Ýmµƒ`·»Om�©lÖ#àº.ÂC½Íqœ³ãN¹R¬m{”•´B�vÏQº# Ã3‡††`…é§">00ðœ0 áðë}ZëO.]ºôI¹\ê× ´å„ØÒÌ|a”§ÚØž†!¼�Ç�Iý¡ºþ˶ÿ"ú\/âD„ºãi»1ŽÏCšV­ZÕ;::ŠpVCõñs øÌ¼c Ã{ÑíÌ|åP…L[wZÌ¢~ô÷÷è8ίˆh_`õѱ±±Cr¹Ü§mÈxè…º*ˆÂ£�êÅCµ(¥€9ì"áüª›79Žóî|>éÒ„¤”Blà×e2™EêèJ)x6ÞÇqœçåóù{ìœá W�–Žå�bÏ©�`6Š](öÙšd[Þ£í±ëº‡ØX×›Z0?bÏóðoÞ¼y¿]½z5b67LÖ¯âoïï8Î�Ñ;Ѱ`,C$-ÖZ3Boc[´hÑõƒƒƒxÇÇ%„'#¢¶nÝzSäM)ïÜ�j­Ë¡µ–,Y²[6›íz`2ý¨–—½½½nݺõ–�7–š©¯¿¿ÿhÇqŒÖªË¸PÅœ�xßÖ;z3ÕJA@�.D@ö$'ñ¯™ùV0­õ•“,.ÙÓ!ÐòáÕó¼Ÿ!üKµæ˜ùç¾ïŸZí7¥$@/!¢?3óm°ŒòEÛó¼UƘ/€üc Ž¹?ì­jä~8 FqkmÙ@æb<Æ3¤gAí08,‚¤ªìÈdH^N‚Ú'ݪµ>Îæ�ÍrŸÖzg«â uÊ(­³¤ÿ4"*Çž­6ÎÅ‹g,XðˆõÞ ¬nff\*ÊÌŸð}ÿü´u§Á,Þ÷ª£ÊÆîEÌ_` ©Bæ ÝGD×#0þ0ÆœÁW”R Àÿ´*¥7c~ÏÌoÂ߯˜ApMr¼®ëºÌ¬#õM{ùõU¤­µomEÿÍÌß ÃðêNæ÷}ÿÒ½’«“ØË¿s@›h§å=ª‰6gt‘8ÁžÑ•Î ‚@·#€Ëyª¥²‰§$A Ó4s°ètŸf|ýlß¾ý‘ 6€ Hj?-^]×})¼�Ö뤶A€LWRÌÙÎí}}}ÇA=Û:¨KB…ñÄÿüç?ÿ7þü²„¥X,îµnݺãßIgAÁfæ7ú¾I:ò`½ìjŒyw�ªâêB|Øùöo�å;´Ö Ö夔‚£êr \[÷‚ ϶CCCÿ‡<žçý qZ{{{w­&Å8* CxòýhDK|îç:ß÷Ÿ'صê. ‡Yoº51«å®– v<–llN|­õ@Ì£o™£ß®ëâR—'Iñyµavà‘ö¾ï¿2ò�Œ< Ô ¼žç½ ª£ ò¹\îgÅb±cùqQÐþWFjœ,®ëndæÃµÖGM¶ìLsÄØDÿÛ^DvÛ!�³">=3_�ìsx+€`ÿ®JŽ•’�N# »ÓKýÍ ÐÁVJ!.ìÇ4ü~­5lè*)“¶ì�'úÁó¼�cÞ‚ÍÌeDÖ¢X'´ì¶Î;Ùr�Û—%ÁÁ®W7H<Ôè“íÄ1›,Á.•J Cj_N1GF§(¥ÂzïÅÈÈHodÓ™˜h ÀóïÁP=gæ_c`‡�u‚g?·öŸ¸l@›Í?‡ßín:ö¨ j ¾$ ‚@“Ø=Â%x“ÕI±¹‡€ì4çs5$«ì´»­+6ö«´Ö¯¶)ÇØÁŽy×­Ùœã8ÏM:ÅŠ¤˜Ìüjß÷aÿ<¨ô¾$Ò›æcVGžq‘)f×{Ô»k„Õù-wÕ"Ø[·nÝÁJÁáy·‚‡‡‡ïu]÷:f~N ö7|ß]¬Ïu ¶R væP‘‡šøy™Læ–|>ÿ R Ñ1† ÁF(šZu£Ÿ¸›¨‡Ùd v½X·6|›Å=.—‡�ÉdLäÄ,9ñ±5mü's°ƒÿ}ÌÓò Ö—¡.yhîHþ”ï�dA`N!`�Xn1Æ(¸ŸŸSƒ—Á¶ !ØíB² õÌÕ�¬B°'¹x\×]’ÉdîÉçóâ(¡vJ© °Uœd lÔÚÁFJ)Ø=#æiµô5­õ’?xž7`ŒYkŒ‚Ä1"ذ“Þêˆÿ+ û‰žcŸËÉóW"âÝ ÁÞ¼yó›™Þ±?¢µþ�<™o…¯vìHZoŒyNp&Ry’µ§'Á®G°‰è„F˜µ™`ßFDÇÄUßmèŸ?§È\rnòkË–-£öyÖqœá)ÙvÄà…=ö±¾ïߎw:ÿ$·É.‚Àœ@ ¿¿ÿxf†£I%ŽdçÄ”wb�-lDV¨!ÞÙJœ®nÈØ4ãª6!)ÆZ³íF;EÝ�X#¯Sö$!VJÝjŒùG§O²èœËÞd làÔ2Á¶äôr":3ü—µÖgU› ¥È,l|wbæ÷BÂi%´åü‘´RêjÄyEø(ÄC/•Jû1óz�àb±¸B€5C°7mÚ´Ÿã8°†Sµ³™¡n † fÄñ~ÖÐÐÐojØ`OJ‚íyÞrcÌÁ±×%°EfæOZï܈}û±±±Í===ÛH°¬!f5ð†J=âÒì³Ï>WlÞ¼ùx¯'Áö<ïeÖÞz«1ýE/˜£wi­/ªõ¢EN˜¬*ønÈ{ö8ì³ãe;•ßuÝs˜*èïÖZ{I‚€ ‚€ К"Ø z›7o>—™qn„ÿ›Ç�1ßfæQ(¾I„7½Â3ÏF˜é5Æ|£X,¾«§§g� �Šð®ßƒcU„EŰmhTœa BèÔ»Œ1Ÿk¤Éáºîiˆ’b#ÎÀ¬ðFD¸ ‚QeʃFãJ®”‚ùÜ‹™ùSÆ8Â=‘i˜ù|ß÷!*§zm·’µ=Kaúj‚=Iì%vzÀ਱ŸµÖKÒ—*çl Á¶êcÌ3ðof¾1’PÖ!a�ÈÂ.: ñ€Íê‹Dôv8(ÓZÿÎzÿBlÅêÙND/ÓZ_‹g56l~“6Ø?Ŧ99s]÷ ØÌm|gT…ú ®ŽØÜÛám?V¬XñÔR©t—V|•™#"…vuçàÓh\Õ¦,&`ÀÏ×0óÝÆ Þâûþ—µMD ð©�¬S°Œ:Þ„ìI@l_ê"z‡HžlcÌ=A`ÚLjÁžL£Q^x®fæÂ0äGyäæjN³�áÚJ¥ÒQ0X¸pá]�Ô‰ÒöÅÆd>qllìÖ(Bh …ùqç_ië«—o``ñ¹غuëMÑ8•RY)o9Yš”³d]x§²ÙlïÐФà©Ó²eËvÎd2·�¹º ±ÇS–Œs¥Ôw¬ùç<€ ³����“Ã0¼6"�UºÞ,ÁF¸ÏC£h"¨W)u•�PrÂdÛF[qV´¦…S´åË—?gG"ú‘Öú4×uá,öëQèP´}ÖYgí8þü¿ ÊØØØÎ6l€Àc\ò<„»Zâ³Ï>{÷R©i;$ýƘçч´Ö±—�Æ5®Þˆ`3ó‘¾ïðœl4h…îhÕæë¶mËT%ØõB²vÊ‚ÝÄ,BêI›(.E# »1F’CšD@)5ÂÌ}ßWMV!ÅA ÍÀü ’7f¾Jký™6W/Õu 6BÍ‘ã×*ÃjŠ`/]ºôI¹\’ßÄê¼—™ßa#Ë´Þtœ©›%ž‚­”º&}µ¦ˆ™_îû>.Æ¥•+W.,‹_µjèÑoQÈÕ2ÁN1® ÍF;úÔ:/†*ýÁÙlö‰Fm×#ØuB²–Íg{‚=Ûg°;û/»;çUF%Ì”Rß ¢¿h­ß?#:$�¨¶Âg $’gˆqYµ°Ú¤«Y‹ˆ5E°£ö–/_¾»ã8/rçÕÆ˜×ây¡PØ+—Ë¡ÞfÛ6"ب÷%Ì|º1ªêÉtoäh-þƒR Ç–ÁO†ŸïééùC†G؈0e‚Ýh\ëׯ(ÙXD°ãjå–,ÿ€ˆ^^*•öÈd2¸kØv#/âQÛ6_ÅßÎl„`ÏöìÎþ ÁîÎy•Q ‚€ Uˆ¢DÄÃG T‚@Lš`ÛМ›àXVký¦¨M×u/ƒ'pÇqžkŒyw“áMìO#²HÒù©Rêk ´¹\î€K/½![Ç%j4KD;DÞÈ=Ïû€1ÄúC}}}Û²eKÝqåóù¸Jz¹þ˜ öë‚ À…4Yÿ@ðCu¿Öz¿FmGä^v+Xеh!6ª‚-KKA@˜C¸®ë2³fæC}߇s'I‚@3Lš`£‘ˆLÑ[Ã0üy&“9Òƒp¯Ùb±¸{.—¹n6¼i]‚Ýßß¿·ã8 ÂHˆlr­1f%� /ÞZëWÂ󼯃ôù1D~EÞ‰¼ˆ˜266öÖl6ûY.¬ê¸Ö­[÷ïdÝ1/âp¬vm&"�v`{žÖúÓiÚ†tÜñI…dmfÒgZ‘`ObF\×-‡›jÂ+ö$Z鎬-ÄÀ‚ÝK@F!‚€ ¤F¶µÌüÌÑÑÑ׋Ÿ›Ô°IƉ4E°­—lØ`?-V%Bµ¾Qk½±•ð¦ÕÔŸ•Rl´gU߯Œ…‰Åão•J%5<<üpµ‰VJ¡¯PÛÞßþ¾Ý³”™?ND2ó;Çùa©Tª9®Äýû6LØ�ˆè¿bu#,7bnÃáYö}ßÿ\³!Ygû‚=‰´Žq.ó}1‘%ÕA1°�1·Ao�“M"Áž,b’_A@A )‚Á_a>•™·‹ÅÛ“Þv…7­A˜s øÌ¼�™ÿ˜ÏçÊH98í IDAT«aêïï?Úq£µþAZ‡lG ~ud»Ýh\ñF" 6É!L¬1æ°E‹]_-mš¶› ÉÚpà38ƒìIL޽�ºPk=8‰bs2k‹XÍj‚�Ø…™L&×Ì üªU«vyä‘GJ—_~ù3aá,^¼¸‡ˆJ7n,MuV¬X± T*�Vsì1Õ}ÁÇjñâŹ#�<²888NCûÒd›PJ­!¢c´ÖÏiS•R� ‚À `¥¼ÑuJ�]èEK{ †1㛈ìßÙÚA!Ø)'ÆuÝc™ùV¨^ˆŠxcРêå8Î/òùü/çž�cVìV6&¥lan©ã³ 8›+bÕ¡¶3ów|ßÿŸæjIWjÙ²e;;ŽslOOÏ_Ö¬Ysÿ’%Kæõôôlƒ ‘ïû¯KWKçr)¥`×5¬­µß¹–¤æN# ”ºŽˆæi­Oìt[R¿ ‚@û€©&3¯wçÀ:Ò]!Ø-BÞÊ9¶Å¦»¦¸ì”SioÍ®uç”&IcÊ–$ÛlwrÖÊÆ4“6¤× ,ø‹%¹ïîäÊTJ�BD?'¢sµÖÏ@‚ý\"úJ,f'á�º;ˆ€Rê"ÒZ¿«ƒÍHÕ‚€ mF E l´‚ý¹Mã¬!©*0óQ¾ïJÀj!ØÍá&¥:‹ÀL�`C%ØiF5:I°³ÕìVªAX�`C¢¼zõêÑN@�S¡ú Ò¾qãF8ö€�P%¥!Ømì#ö½qí·‚édæ·•v¤lë@S"›Í>¯¨Zë/´^£Ô í@ÀuÝ×Ã!3¿Å÷ý?¶£N©£ûp]÷Jf>Vk}@÷�NFÔMÁî¦Ùìž±LÁVJGDpb·j¤Dt7-×Z_ x•RW3ó޾ï??‚[)õ"¶ù~lf^aŒù(íAD[™ùBß÷WGå<Ï{¾1æë±Ž,`úḊ8œK„aˆ0/D¨"z”™?ìûþÅ ‰VÊü7"ºÂ3φbè5Æ|£X,¾«§§g�1 ‹Ìü=cÌ™Q¬D×u�tç}öwôñvf¾Êsò(¥àlãÌü-ß÷ÏWJ½‡ˆVc^ÇÌyÛïG‰è2­5Â8ÔLJ©·Ñ'ˆh‘íË 0·ÐZÿM)õF;ö]‰*é×ŒŽŽ.†Š8Á£&¼g¾ÚzÖ¼3 ÃÅCCCÿ5æºî³™Ø‰ò�„g³YªæÈãyúŒ¾¿™ˆ>fû}í¤µ>y–-[Ö—Ífo$¢‡´ÖXQÌÇ;�1ß„'O¨¥1ó{|ßÿ.ÈþæÍ›Ïeæ3aÏKD�c¾�9�lÆíÜ\DDo°kà.cÌçà‚³{^ÕÙ5’åË—Ÿ”ÉdÞÏÌë0�³«÷Ò[A {PJ½߈B¡°BûtïHed­ `UÄwÓZãì$I˜±Áž±S3§;6-{ùòå»g2™@mÂ>c l�³a.z@)õ"ÚQk½gŒà!á×™ùõ¾ï=?Yî5Æ\ÅÌ Y �eÞ���“Â0¡Cú�@DÛ¿ l«&�¸ˆ ¿ Ù÷ì‘‘ïÓZëó"UêØj¹šˆ …Ë$ëcŽ%¢}+Ñ÷ý�.^¼8³`Á¨ÊîDD?3ÆÜÌÌgÑ¡Ìü êXÝWj­Ïp]÷ ̼ÊÖ ây]l\K´Ö—U[µ–àBZ,¾„pö"dx?":Œˆ>HD¸¨¸XŽŽŽ~Þì¨ÊÑ;AD÷i­ûðƒçy§c®Á¿a+NDÄ„ÄED6›= $Ûó¼�cÞgç—?#"Œ¤ýp­õ]žç½Ùõÿ`Kü#»kϳ…™¿íû~Þ^l ¢_c~Ã̯Â8Œ1ÃAô£?J©ßì|ïCßlÈ Œ$Ìé7\/‚@ ¥.Ÿß ¯ÅŒ ³ÙÈfû ÎÀþ·#š‚íºîiV‚›‚`:Òßßÿ<Çq@ŽÞÁ×&C°! ‚iÄ7< CH÷‚œ»®û¿Ì /Æ/ÕZÿIJ󭄵L°=Ïû€1æ#)Fx(Ÿ?þ_ Îår{>ñÄODD´T*0<<ü�X[ �Çú¾ûòåËŸœÉd %ÿ‘Öú´���£Â0¼ö @nÑGHË`æë ¡¯E°­Möë!A÷<ïcÌMD¤µÖ^r9Úþþ«Ìô‹Å½¢pJ)�Ð’gß÷Ï®¥"në+^K¨eŒy^ooﮫW¯~ÌÎÇþƘ§Aðgäq]×efá#Øwe³Ù—¬Y³æŸ®ë¾‰™¿lt—zž÷ecÌ›l{åK¥Ô‰èìl6»¨P(œ� ØÂeD&“Ù}íÚµ#vþ®"¢½´Ö'¸®]ºTÄÅæîIccc;oذ ’A@˜ó(¥ )t˜Öú¨9† ³!Ø)§ÐuÝs˜ùZk¨¬Jªƒ€R ÞÖÿÑd ìi#ØžçaŒ¹Ó äêÛ###¿Þ¸qãX4ÜÉìÞÞÞyq›éˆ f³Ù�‹Å"¼…ûúúz#ÛgØònÙ²öÉe‚‘ðL&sØÚµkAèÊÉuݵÌ<`Œ9¹P(Üh ö�ZëgÆú ãµÖ{Çž¡î?Å0<88˜Ù²e Ô¤2ƼõÑ Zëg×!Ø'ú¾3ê�õùZ­5ÔØÇ¥HRÏÌ?ð}ÿ•Ñ�G6:: õrxL?¡Á¾Ik}Rl p\òÇqŽsç¾b±xm²Ò÷r¶0 wdæ_c~Ás#‚mmû A' ªçßÓZ¿J)…‹…ßB]D^Ó•RGˆ2­õ!®ë¾"A°A¦O#¢»ˆèâl6{5ˆ{Ôψ°cÞî8ίc \I¶ã8ÏÈçó�pKA`Î#àºî¥ÐR ‚@œPÍùÕ ³!Ø)ç°Å¸Î)[éŽlmÀjZ$Ø@ß󼥯˜OBig£hŒ¹2—Ë-]³fÍãÕv¤^œPÿ/­5l˜+ÉuÝ�Ìüf~¶1æúˆHÇóÄ�œ)¥¶Ñ­õüxÏóÎ5Æ\{êB¡ð-lØ A»ñr²sP&ê±gq‚Í®ë~‚™Ï¶jâÈö¸ýw]‚IÊëµý“—½ƒ'Æ ë¨{×"ØÉqÅÈ2T¯w0Æü²Î[s—Öú𘻬ë÷ï¡®UrKÔq¹ OÈår …1óg}ßw’`/]ºôI¹\n#½ Öþ½‘—q¥. ޝÕ7f~¹ïû é’A@A@º!Ø)&sÉ’%»õôô@ ôB­õ`Š"s6K «w´à„bÚv4qO7ƼÜã‚„E*ÐÕ¶R ŽÌà8iœ 6$Õ å1BÕoß¾ç¼yó 6}¯ÖúÉÑïÖÑÖæˆx+¥ÊjÈÅbq—Hµy#éh†Ï/‹7U‹݈`[Çl?&"¨‰Ÿ—ÉdnÉçó*¥@Â!5®)ÁÎf³ûÇ¥µÕÚŠ�¹lÇlŒYÁÛªŒµ,¡NãEÜŽ½lOÍÌ'càQ$*êp`6.e³Ù'ÐÏZÛó¼ �1‚*9Ð…aø”L&s./ –õ|cÌs‚ ¸>I°£†`·ï8΋Çyµµý&8éÉårP? 3ŸnŒ©hÄÉxä mÎnÓ0pÏónÃp»Hɦ|iR�PJÁíÅccc 6lØ?*’�‹€ìS#1°S€d³X‚ µØ ù|6¿Í¤i!ØÖ×§C1’tö÷÷ïí8¼Q—U°•R tOÉÚ^¬”Á;>I°£¿@,<Ð&­õ~VR½ì¨/½ôÒ­–kCc½7 Ã]˜¡¾àÕ»¬ÚÝßß´ã8·ÛÐ\ËJ¥ÒCŽã¬$"8[§µ^ÙI[Çcð0^N�$Øžç-7Æ ! – ² Õxx‡·òŽ��mŽBeŽˆO†`Û¾À³9l›áÔì²R©´B^¡­b±¸Ïºuë¶(¥žf¥Ñ·³ÑÑÑ_W»8ˆ«ˆÃ\)õy"ÂÁ«ù'Ç9Ôs.p„”yjlÛ7ØÂïWE�.?€ Æ�|I vD¦á$- ßg2™#�1ðôž-‹�jïà8ÖRž™¯5Æ`îN%¢k´Ö/ní‘Ò“E`pp°gË–-£Æ˜wA[~I‚€ ³ë@!ºE’ 0£‚=£§gÎvnZ¶%RPA«$xÕ†ó+¨ôÆâ%ïk3€Œ~ Ž·:‚oØ0]/"¢_ÙÐSQ]ÓZƒhÂdí¾ûîk­ zôû÷ˆH{˶Ӟç�iŒEx©r, gÁ õªU«z­÷kZk„+§FÛ–ClïgÅê騕™—ÑOz{{ÿu#Äœ~EaºÇÙ7ŸÏCŠ^³­8~J)„4ÃØ*mÙxÕ/‹â‹ã~B)ûèC‰è¯½½½GÕâXÃÛúñZëßá‚¡··w(æM£Ï+}ßnÀ¢\&é,Îþ‰2âz÷û¾� `þmcÌq§hJ©(dWÙÃøŠ+žZ*•`ƒ�‹�(!¼ÛµÖx�€“Ã0„SD„g‹Ò·J¥’†ƒ5ISˆ@¤Má8Ιù|þ+SØ´4%uPJÁ?ÇIZkDÖ�$TE  jYA`Ê‚=ePKC“@`Ú6úØßß¿¯ã8G0s¯ã8Ž{ðŽÆ€Ãz¡PÈÿµÞ¸à Ä®§§çÖ¸=v¬ž}Œ1Ocæ;â¤5^'Â;í¸ãŽÇ–J¥ùÌ|›Öºöª R{":`ëÖ­7EÞÒ•RAjÞÎvÐW`V*•ŽB¸°… Þ588B:.!O.—{!¸&3¾•+W.,•JÀè‰ùóçÿ6î½}2õL6¯çyÇ„aøTfÞR,o�ÛÊ£.¥Ýa~2ó[0›˜l×$ÏóžeŒy)3ÿÚ÷ýŸ @‚€ 03PJ}û¤Öúà™Ñ#éÅLD*âŽãüB4IgâìHŸ’Á–5ÑV@�Ú@"¦•`·©LA@j"€ÐžÌ|74¥&A@º!ØÝ0‹3h øPZég%µ­Kk‚€ ­ 懭 'e§ !Øu�Ò˜~Y Vé±’œ‚€ 0u(¥Ž#¢[Œ1o‚àò©kYZA@h‰�Ý*‚R~:‚-»-ën``à€0 �ûņ iK¥R‰ -"àºî›˜ùËaž044tK‹ÕIqA@�)D@)…3å•Zë%Sج4%´„€ì:ð¹®»„™ß>66v²�Æ–Ö™A`Zp]÷#Ìü"z’Öú_ÓÒ iTÆ! ¦² Ò" ¦ši‘’|3 !Ø3i6¤/‚€ mEÀuÝ�Ì|¤Öúˆ¶V,• ‚@ÓˆéFÓÐÍ©‚°¿fæ[�1Kƒ Ø0§/ƒ�ÕÁžÕÓ'�A@¨‡€çygcöÕZR��™�€˜nÌŒy˜é½X²dÉn===';Žs[>Ÿ¿g¦÷Wú'DÁ–µÐ\×}A&“ù‡l€m�S*A@º1ÝèÚ©•� ‚ Á–eÐÄF¦-0J%‚€ ‚@×# ¦]?Å2@A`N# »Îô»®{-]&võß‘ÈF†ˆÎÐZ_9§ß(¼ ‚€ ÔE@L7d�‚@7# »ÆìÚ°Sw‹c…ÆË_b`7ÆHr‚ÀÔ#ÐßßrOOÏßzè¡6nÜ86õ=�A@šEœL&óˆ˜6‹ ”›.„`×&Ø'‡ax­ã8§äóù_L×͆v•Ro'¢‹ÇÆÆH8³Ù0cÒGA`n `C-®/•J‡ ÿunŒZF)‚@w óCfþ¼ïû8gJf B°kLUt03Æ<=‚ÛfÍŒNCG!í'¢ä"bÀ—&A &J©A"º ··w×Õ«W?&P ‚€ ÌÄ¿Ïì™+ééx„`×'ØK´Ö'Ë¢A@˜}(¥|"Z¬µÞ}öõ^z,t'bºÑ�óÚîQ‰ùa»•ú¦!ØS‰¶´%‚€ 0e(¥¾GD µÖ'MY£Ò� ÔEÀuÝUÌü…l6»hÍš5÷ \‚@5„`˺˜ÍÁžÍ³'}A@¨‰$eøqhhHühÈ:f®ëjfvµÖr�!s2»!æ‡3qV¤Oi�Í--R’OA@A %\×½�™ÓZ¿´¥Š¤° 3!Ø3tb¤[‚€ ‚€ t®ë>î8Îå¾ï¯è¶±ÉxA@B°e‚€ ‚€ L bº1%0K#‚€ 0�ÁžFð¥iA@A@A@�îA@v÷Ì¥ŒDA@A@A`‚=�àKÓ‚€ ‚€ ‚€ ݃€ìî™K‰ ‚€ ‚€ ‚À4" {Á—¦A@A@A@º�®!Ø‹/îY°`Á~˜šl6;¶fÍšvÏ45?¥ÔÅb1ƒÖ­[÷ïj5¥ÉÓ|¤d+¬Zµª÷‰'žè±ëz»ÖºÐJ}R¶= :÷Þ{íßÿþ÷èÆ�ÇÚSóÔÖ²dÉ’Ýzzzö@«…Bá±õë×?ÔÎÌ´½¥[æ­�s$uUG`åÊ•û …cÇÙ±T*ýßüùóÿ´zõêQÁK˜ ºåÛÖYgí˜Ëåœyóæ™5kÖ<a=/ áå—_þD5LW­ZµË¶mÛŽpç©DÔ†áUCCC›¦iCh�®!ØJ©ÅDô�­u׌­•IVJý‹ˆÊèÞÞÞ]W¯^ýX²¾4yZ郔m¥Ôo‰èD[Ã¥Zë·6_›”l®ë.cæaÔgŒÉA0+㹺®;ÄÌË-.7j­ŸÙ.ŒPÏLÛ[ºeÞÚ9GR×ÿG‡þùóç�ˆÞŒ»ú$6Ì|�1f©Öúo‚› ÐIºäÛÏJ©ÿÑ<"*j­sÀ �[¶lÁeޱǵÖ;W9—¾—ˆ>ÎÌ‹}ßÿf'q—º�v!Ð5$´�{``à¤0 _jÏ›ƒ X—¼¿¿oÇq”}ŽŒŒ|f¦I±<Ï;Æs›í㟵ָ—Òäi×b“z&�€Rêf":~¶¹É�|f—PJ]CD§Ú^¾BkýÙÖã4{T' vµ½%ÍÞÚIgüurüRwmìÚ¼*º�®‡•1FAž‚@§è†oÿÀÀÀÓÃ0ü0Âå”ïûÏÇ¿í»v£Åîj­õËâ8º®ë2³Nb+»S«mêëUJ½r?ÛòW»ñÒRv•u¥”ú�oºOkÝ—Ìæºî ˜ùÑóB¡°W»Õ+[]ò®ë~’™Ï³õ|Hký‘*ãh˜§Õ~Hùæè†�l󣟙%¡öLD‘J[‘ˆv˜‰ªûiö¨NìjûOš½µS³>[æ­Sã—zk#`ׯ"Ú5–k;ÝÈÌÿ6ÆKDûÆk0ÆÁ�‚« Ð ºáÛ¯”z?}Ôâó.­õEø·ëºaæà߯˜³ƒ XÇP)õs":%öìn"ú»1æ]ADB£NÀ.uNJ)5ÅÌoô}ÿŠ)jzÊš‚ÝÅ[)õ"ÚßnbO ‚àÏÉá¦É3e«Qš€@7|d»mZ=Ï{�1f#ÆÅÌ?ð}ÿ•3qŒÓM°«í-ÓI°g˼ÍĵÔí}ò<ï38¼ÇÆù1­u™DI)u6}1öèN­õQÝŽ�Œozè†o¿Rê7Dô †áCCCÿ‡+¥@’�Á¿K¥ÒÃÃÃ8«ÆßµÊÙ•ˆnÒZŸ4=³ ­v !Ø�B¶õ¶SE<Í!0ÍáµÃL]¥Rê "ú«-°Ik]v—ØÄæIÝ dìÝð‘í0ÓX©çyß6Æœ�.cÞÁצ±;5›N³GuJ‚]kÿI³·v ËÙ2o�¿Ô[kZŠýz¹Ö6Ø’çy_7Ƽ6úalllþ†  é–$´�Ùþí·Ž‡#§€;ë•+WîT,#‡»j­÷®r6…6É"û�]ÁÛÚ ®T6íÁžö)Hß�F{pp0»uëÖL/ i�i¯�z��&îU±QþÉüîºîû˜ùã¶ÌEZëøí|ùqšüñÞõë×á^SkcÅŠ ²Ùl©š¾ILăƒƒø/¬WëÁÁAô7uŸ±N¶lÙ²-r€”ÍfwNóCµ¯¯o{£>Õëïd×|š=ªÁF›O<ñDa²k¾ÖÞ’fo­…ú2oÞ¼].\ø¯ÉâØì¼¥ÝšX£ŠÀÁÖÎ;ïÌiÖS­ö°¦7mÚô¤ùóç?’æ[׎~§©ýB¾FóÖŽ½$Mây–/_~p&“ùKô¬žê·çyÿeŒ¹:Êë8Î3òù<Q6LíøÞ£Ž½öÚë?�plØëh*ÍœØw§Gk gU-§´ïT;¾µ-w¶�Lvîf÷¿<ýýý/tçgÈÃÌßð}ÿuø·çyÿcŒù–-»^k½,Y�RªB°‰h‚6I£iI»Æ&;'�Úm×ïiûߦw¤mgúÉôGv»VËÔS�`{žw„1æƒDô "ÚÉvã^"ºž™¿æûþwã]³jb‡Z¯ÍåÛ3›¾g7‰+¡ÑbŒy5A"üôXl$°Ëü›Öú\ Ã5CCC¿o´ˆ’{ ½ÎSwoõ}}²^ô[©1žôã6°0wù_ÇqÞ—ÏçLÑŸªófßËÏÙ}áß÷ö/f¾Ôƒ÷i'cLU J;Þ7¥Ô‰Î^ M}£ðü>>FGG?ÜHBj "Ô™ñ.Ãâ>f¾…™?”Ïço­†‘Ýß+¸;ŽsN>ŸÇÞ>!�}öÙ{ …¸ÃÏåZk\ª•“µ·,«r:Žsq>Ÿÿ…Ý'àÓä`<ß¶mÛNÉ�<­î%¨·•¹ð<ïUÆ|ÛË©^’Äû�oÉË£}µÝß{‹)L¼p^ÀÞ_0‘gs„;ºÛ3/Ù„ïù]82oÛ¶íÌZaÛj­Û™ú¼km°™ùEƘ襬…¿«µŠ~TJ!vö8G&‰‚—#\,M¸q‹å«8ESJáV¼|°`æ÷c^HDÑFW­O7…aøÊø‚ŒgRJý‰ˆpH-§Z�•+W.,‹QxTk½[²±4yž†!¼#Ç T²ª¢1æœ*N*àä)J×h­_œ,<00pr†×Æž?^*•ž<<<üpì¥D?Pß„°)U@ÌÁÁXW¸\©3÷!îm=g…J© ÑØãž„•R°ÍÄEÒ�ˆíÔ<ôÑVÇq^VKúeß½¦Ö¼R —[©ö¨$Á&"¼ï®3/X«'Ös4SmoI³·ÆÕríæ³D„C~½„�ù.4êeª5oö$ºÌÃeçñƘ[b Ø»q9Í-µã}³Äö× B\m÷c^\Í2[‚ޱ×Ý�1Ÿ›7oÞùI©vB•Äø¹ù|ýš�ªHzÇùõˆïUÌü¶0 �‰…€+××øhÇ^ÒŽ¹p]÷4\ª¢̼¹žOÏópñ‚K¼r[±|ïßIDx¥"3?ß÷ý’';'v=\Ÿbo.ûŸX´hÑ«’Òô©üÖ6&íïJ)8â‚C®r2Æ,¯ö=Ž~�‡û³ë¦Bí{ÙÖ¹K†‚Œ›»à»^/¼¢R ‚¤2™©å3¤̩ì“Dô¤Ø¾„}zÄâ^á¢(R‡]vùÒ Vмˆ7³oã¬Õ÷ ýØ+ C|ãã¶d—q±€3RÙΜ™Ç­�ézGìÅL½s/ºû ='^We?‰ûuÂ.ˆ£h*Õ¦mÜ™Êó<\´×5© Ãpa-”ö}ž)ùº–`§˜™O�$Ù��Ƙ/13nqS^ãÜ´ý!¢GÇÆÆV»AJK°•Rˆ•¼Ú¶©µÖ^•¥nž”/b¥Z\ ø¾ÿ©è� IH$›`¯j=·B£ §ÛnD§ù¾�Í«œì¡Ò³J=ö§­öÿ•²±:&ÄJLlhi§cSƒ —¨Ï•5„I‰HÚÆp(õ}?š·r±FÛ’M‚Zä2Ù<ˆÝi¤~É‚–ô@3".%‹²á½HòÌf³Å%Ù¸!7ÆT i…m$Á‹7ýh6›Ý7^G‚�b¬— k¥JÁ¿j­©Q!bz"Ž|¹Ÿ¹\nÏK/½´¼;M¢µs¬ïû� WR«k¾‚�¶ßÛ³Ùì¡‘V@²Pµý'ÍÞAùÒÆÚÄBí¶ž.‘ª®cÌ»ƒ ¨EFjÎ[â †¦&Ô'Øízß”RÉ H´�C(Þ£ä{PtgŸä;›ðÒ>î�©q)ú3­õ‹â;E°qH«FÒ"‚ÝŽ½¤]s‘vÑÛ‹#H¡¢oÑÝZë§DåÛù½OjäÅúˆ‹sD1H^zc�’Ô>ˆ?Í L—¬vVrŸŒHPœ0E]òµÖЪ¤©üÖ¦�»Fùð"aÄi¤�T­¬ëºÐ {Žým{__ßΑ‰Y'殓»]ïQò|Úó*¿šNõÊÕ Ø ÷ív͉]'8�V;«Ôìz‚�ª¦Î£QåIÿ)}žÖúß‹¸ó¹U”³ÜÛ××w .⬶æ¹frgïfΧi;3•ùæÁ†4óÒ‘‘‘{öÜsOH± žðúd¨sAð\ü ÕÀl6;¿X,BÝ.Šq½5›ÍF7UÑ Ü‚b±u¨—3Ÿ˜ÉdضmÛöHVãƒ{3Öqœï—J¥yƘW@…/NäŒ1 ‚êÄãÒ$6n ËêNŽãœU½*uÕ̳lÙ²¾l6»9^ÆJà¿Ô××÷À¦M›Žr*!IIדµÖ ÌåTåeÚžËåö�‘H+äênAôÇÛMJ Œ1Ÿš7oÔkËÎ3¬ª1THËálú¡Öf•Tã£ÿ¸1f “Éü4 C¨øój’Y¨¯ÂZaæCÂ0|33Ç�à\«µ†vB9Õ"Øè»ã8ß\´hÑm÷ÝwÔÒq›W‘†à€=66¶(®šÖˆ`+¥Æa5Ÿ0 û}ôÑßí²Ë.;9ŽólfþL‚]k]y’k£Ö߮뮄êlìw¨ ¾sttôjôÙÖ¡åQQµÃ˜¡"�2Ö–ë*.¹†ŠþZH©í-:TLa*Q!WÌ@u7j·Á†VÔ³ …Â=½½½Ï„ÙE•‹\ôà¢ìó¸Ì²ªÿxß+}rç¸j*µžç=˃÷éV­õqQŸjlÔÿafþ¶ã8ÿ*•J/1ÆÀYK\³c‚‡ÔV×<Þ "JµGÕx/ É}G&“¹ò¡‡zx÷Ýw?Úƒ½®¢=ÃÌô}? ¿2nÉ(¥&ì-iöÖèÅó¼åƘŠfý8 cn†Ï…þþþ}3™ÌƘ/Ä.f³Ù§T#ýõæ­ ÁŽªÄÜýØUík}߇ ¾-¿oI5cx¦fæµP}Œö´b±í\‚Fi‘IJ“AÌ�1oÊårWÇ%K–ìÖÛÛûc ÔX+‡ÁdÙìøš€dõ&ÌãÈÈÈg`Ïßê^Ò®¹H»÷Y•`¨ìÆß�¾ïc=”S;¿÷‰º ¡>sÑ¢E#iñÀÀÀ>Ø[1çQû�.hc�0'žç-…*k,ß5Žã¼):ìZÕñ—13"(D— ÛµÖóãuOå·6íü¥É÷l�üµükXS‰ŠiDRÃ¥s×I‚ÝŽ=Í®œ�v·Xã|XþÎ!þµýãO¬×èÛ µÊe;.Þs¹ü›Àì $¶|‰„užËåÊçŽb±x?¾MìÛmRTÓìûäyÞ÷qf�­'˜à\†á�ÇÙ’ßXhÜJ¶”»#çQt"a÷ŽG�Cm¿P(àì[Èf³à/o�;p„–�Öæž•� {\Ö"+ eÄl6ûf†¦_Eà†á󇆆®Ã%^6›]hç²ÂpŽÌd2ÇÆÆL+–iÞó©ÌÓíû…Zë¸úq[Ïó~eí&Ê'U­Ó8âIã@¨Ê÷ñL&³ÿÚµk#¢^nßr`-H÷KÚ,Û lõ�`ƒ91«¬8‰… nVwLªp5ÊãºîFf~Ml![‹äÂô<ï Æ˜¯FÏ™ù¾ïGñÃ#¬+—ñ€™¿ãûþÿ(¥p©WA¹ollì)IÉ}ü£•­ö‚$HÉ„�¡ÊGš$í¬”R#—ÐN� Ú9ƒŠs¤î?î�Qƒ`¿¿Ú|U‘6^ªµ®¬ëlkO ³�r‚ú×Ã?|zÒ!•õÚ {¹²M>>0Û¶mÛm2v.pÀ4þ|ØÚG‡õb©T:|xx8òT_®xÕªU½£££°]«Øîj­�¶˜AÝ·¢¾oŒy{ ºã’í/¤òñÃlÅ�@5‚mŒ9?i‡¦”z9ý Qý;¬¯ƒÊc¥lÀ+vÅÕªÈìºîpÑb VbzÚ±ÅUÄ£ùxvRmÓ^"À»l:bçmœD×ü `ǵS(öJª¿Wy/¶ …ý“ùìœÀþ2š×´Ö¸$—í-�öVëÈ ë,’Ðm* ÇUSÛw]÷MÌ\±ƒ$¢ ÚFóVã ve6›=+éC ]ï›RêÜ7Yà*&EI,•Rñ=&)-…£-ا—ßçZ*ÂÖI\åýñl6»([‡ 6^¯ÒZÿ$>¶vì%íš‹$æÕþ®¶ÑeZkØ1Æ÷‘ø!Ï›úÞ'¼/£jhUúÍ Ø.FfhGRQÞ„�«Î‰ÝÃâ¦%UMËÊ�Iجg³Ùýã[Sù­u]÷]¸DN3�µò„a¸.‚Àùü‡DùâvÃñ²U.‡ŽÖZƒFæ#‘çì¶Í]§v§Þ#¥ì­ÏðeAÙ1`bO› !�­Û¸“³ ßìÉìÛízŸ<Ï;Áƒ‹Â(Ý122òôäy«ŠÙc#qÔ×±ó¨ÕL�ô;"¼Ik]¹€�ú¥”jy/i×\¤Ø÷ 6 ›t8Ù‹Ò·´Öp˜9n?O^¨7û½·—"¸„‹öÏŠ�w•¹�_`Ã�üQTR‚]uNìZ¨\èÔrì‡|ýýý:Žó÷X3‚‰gUvǾµqm™FsYë÷HcÐj)À(JUýÆ$0gRÔ©¹ëÁîÔ{¤”‚„?25Øè­é.ð�-ŽŒŒÌ«�"áE< Á®·oÃÙqËï“RjÜ¥f†Ö’¶zž÷å„vI=l¬·N¾#qsКZ­èDbŽpæ†#ß²s`»GŒSgæ##ͫľƒ}s,öl‚)‰ìfw¬i(WÅÆâ¥É›ó¨[I;PÇqŒÛ.5:Ú�HÃÃkâƒ[5ul᮪B⪵“�3~CÈÌÿíûþ÷“åëåI:ïhd‘<,Á{µÖÒøøG>îÁwÂpªÝnE™ðÂßyç�eM‹Z›±Ui‡§÷Hõ¨!ÁÉV«ÏuÝ1ó…QûµÆ_E�®üA¹$Án+Y))n%Îc¼oõvü#µçȆµÆz�´ý‹$�Ôèë­³ø�´ý8îX+Ìœ€c*·•…B὎ãìà8NÜÛü*­5.Yj¦¸'j¨’EjÿUv=2[9×Ã(q ø¢Ö:’T—ûg=o–¥ð4Ÿ´ÓNìjÒâÄ;×”§nÞÆ5ßp�J†GFFzkÍkb�V•p5Úí­‰uöx__ß®õB%Ã&%÷ŸFó–$ØÌ|–ïûq©x|«[yß”R0E‰›×ÜeŒY._†Œ«"¹Ÿ°çÆ×Z"î,¤ _”`דÌW¤[Íì%¸ Ÿª½¯Š†ÑÕ}}}¯¨¶&Ûù½‡�:æðÈ#�4µÖ¿�‚í€HÛ£Á®9'v½ðâÅ‹ËáÔ6n܈‡Ö¢ÕPÂXÅd‡ˆêìN~kÛI°1î¸C0ü�ôzŸpàˆ,œœvbî:E°;ñ%ö”[´Ö'(×ua®9P¬zyí[“%ØõömÔÙŽ9‰÷‰™¿âû>"UMÉ}µ‘Šx'ß‘„íu£=ï@ü"aÜY:.ÁŽ›×V!!íÆ¥d\+íTö8Kó}ÿŠzçÁÙø[תˆ—J¥C’ê«Ñõ÷÷?Ïqœ²M�Mk­!E,§F‡@»Y4<¼&>¸—ǽäÖX�ñ[¿+µÖgLfQYõJH⣠$¨Q¥ÁÛb9câ^!«uç°˜Úì8u xæ:Ž—îèëë;6M\O|ÜÇÆÆžaŒ� ÷ÁÌ|°1êqQHª¨ÉF»¦ú["~oQk�›¸ i2»‘WÄ*uUÖc-‚�T{‚�S†u½3Ú¸èÑaì.­5pK•”Rp:Iqªª׫(ùÎ%/µj¼�Ýq‰w‚ /ÒUCÖ)¥â’éªjúö��¿{vÜËl u§¸Šxš�X\òú¸Ö>&¤×|Ã=*A°köÃîy‰Â¨@ý5ÙçF{Kš½5±Î&|”“-]ºôI¹\®¦+y�i4oI‚�Éd[»vmÅì"j¯�ï[ÿÑŽãŒslgÛ)Û}#”Ž1æªZ!ã\×�ÏCͽ,±ÿÆU.+Ò„NìZÞŠíhi/iç\ÔÛ³<Ï;ÆÏÆQ‚zès`Zc¿Š«ˆ·í{o÷»gÛoÝAøî1VÌLlêìzs’ÔJ™�¦ 7Ù0{øf$<Õ#Ø�þÖ¾& Cô±é”Éd~Es°å+á,“ÏóÎ�û{)•J{Ä#ž$;Ñ®¹ëÁîÔ{äºî9Ì ?*ãTƒNë^°O–`×Ú·«-Š&ç \:E©ªÄ9±çÆ d= vGߥÌß`‡´ÕŸO5“õ/„��å¿t�“æF— SÜ |FvÓ[ÖÔ¬"Áî©õ!œ‚]ËqYâ…Œ{–½#²]M‹fÂîáj­5⸎K�ò(¥ðqùí} ˜dUuÿ9¯ª»@`”µ„ ²((¨QÀ�E�Ä#£˜a¦ë¾žÁ‰â5nc4šÄÐ�¡ß}=à . jÔh4‰Š;QCÄ ˆ3„˜©®zçÿýŠûêûÍ«­»ª»ºêÜï›ofºß»ïÞß¹Ûïž­iª„fí‘—Åqü‰ì3Îï®æD?M’ä??`^ÝμA.êÁ³Z`ÒŠ`ßd­=6¯Ž ÁnH8:!ØÍòª¢ ÆË‚ŸdZêÖ�vÖ­Ý1â=×Ô¢"gÜøôcÖÚzp�v¾�\âD§Ïæ™òæ|Ó75¯_vä¥éjd­âlfž”(3÷Zl˜DÖ"µã@—Í[žÑ`ï¸,§oüæ§ñš³^uiÌwJ°Î ´?Cìv›­Ö7Ö›ºßc࿟®°ÖÖòÉ7)s>Y_|cLS¹e v£ÀFÝžoí¤+Á¥|} …‡ü¸™mí™NõÉ�쬿¦ˆ4LÓÕÌl=c-ÒñZÒmY4[™9�èî�htéáÆv�`wc¿/•Jg»KÑv3D4%Ø­\ Ðg& ë£×´JýæáÖŒ`÷t¯mgïéäg"‹6é%œsT†¬|ÉZ›{Vêµì:IÓå›�û—,ÝšGN3íŸÍ`ÝP‹ŸâÒ¡âB Š›ZŠ.†‘ÔÍ—ýs2‹tJ°­Û¾ì"L’~̵â»Ù4Y?êgŠìžÎ‘Œk^'Ó¡ö¬‰”!ØÿEÑU¨›h`5ØÍHÍìfidjcÔƒ €Òô°›7¨}SyeÇõ(çéó­žÉ˜åv<ùæ6ðÅÎ%+þG³7Æ „ƒ(R‰¤·t­vü‘ÆøÞ×r¢æié<³W™ˆ7Ô‚§uå˜Õ}õ›ìçŠH-pÈ<Ë�ÖÚÛ}׿mlæŸ×¨¾ 9k‰‰›ˆ–æA®Ë4K°›ù�vƒ`¯]»ö°B¡€”s(¹ÚéÌa<7ØVf\OúyØÇÇÇ ©GÇ|§»i>ÕV»ÕÚâdÚŠ`û&ý-×̬Ɯ׎Ü: Ø]ŸoÎ|ÁÎq·Yk1;)�Q‘Ñò·kwß%¡N:!ØÙ8 ؾŒ7Åq\w�ig=Êq hç5ÿ™¶Ö>?àeÖ1ÅZ‹´qµ×›í÷ÆÄA�ÆF„ÿ:"Bš°Ô×uAÛÍXPdSbúm@ZB\§>ñ»f»§{m§‚oçùL0ËJ¹\~(‚¯fã‚4Š�±²ë„`cêqr2»+kZ6hp;gž¹ÂZûrÿgÝ&Ø •I6ÀY£˜A™>Ôcδ Ø=›#.sK®ÅMrO/3{·Àe™3ŽLZ5ØÞw�f5ØýF°‘*Ì�Ý`ƒFª4%XGf¸.¯(HfmsÌ»Ñk癬ÿÍYøZ ~ddä›i®Ìd›MÜûÝ-ãããÇdá÷9QñcÜ"~–™¿*"?»)»2©}ú‰`7L÷‘b`ŒA�¸ZÐ!9!Žãšib#‚]*•ËÌÈI�¤WÙÍß¾‘¼‚ ¸kjj*õ‡j%V´®é­þ|´Ns%5´0I“9DÔMÚ›`gÚ]h­=? X†`7Ë¥]{5c.W¿pèò˜_4‚ÝÎÚâÆs+‚]g"òÏq×ÝUòi6à3?/Š¢š‰g;rk—`÷r¾átûí·?£P(œ!"R•^ú]Þ966và¦M›~7Ÿ‹ØŒeÀ‡¬µÐPvälbbâ´ êA:@°´–ôR>àMûn¾¶9k€o">ïý¾ÁÛ ÌüY"ºûÞøøø­¸�ËøWΛ`»5A%}óoœ' Yüb?Þ¹sçÍ.ã#ˆÁŽÒ2P;·ÚÖ³¢(údöò}fffßl—Å’]‡g ˜û×2�¤Á»5�Œ1ÈP‚1�âçJGÄú4cN£Ÿã�õÙ@–Ý$ØÝ�IÖ÷Þ�Œž·/9…R=[ÐRl·ÆûÁeq)—M¯ÛðüÇÌEâÕ.E•`·<*Ïy@5ر>Ø- sfCo¹Aû’ Ãðtù2~Ö(ø@;Ï”J¥0óë\Ými[ ¹óý¹ƒ0'½—; ωÄ^b­õóãÖë Ãðm"‚œÊ(}E°™y·tM>ÙÀrDäß"÷j-x›oâ—“z¢¡q+ù´ó{cŒùÓTÓ‰TMår¹–û¥P(¼7I’'%IRO™×,FBúžoÞë ›`ûæµA<%õ×óqË9ó£Êçá›9×5jÙè£ ó‹F°ÛY[ÜÝŠ`ûãìsÖZhw–l:Ÿôµ#·v öbÎ7˜éŽŒŒ Ï7²ÔL*Q˜ùŒ(о†á{Eä-îÇMýæñÌÚµkV( yLË«­µµ¼²�h°�1s¢Ñ.€`/h-¹ë®»Ê+W®ôS!õdíó÷ãVA”–>Áž÷~ïg+@8"zJ^šL|³T*}‹™Ÿê;o‚†á+EäroŒ@«øy�Î\pµïzÏ.Ávsá´vö°Fψȅqω_âï=DT[‡2 ¹–‹%» výBËßG{±¦eÖ¦úyÍ· h'CN7 v·d’ñnènæÖ‚9J“%&ØPÄÔR´"VOEϘï|Q‚ÝrJ°Äk1vÓƒÐÄÄĉA\“НYzž<ûAÄùš¶óLFÂ'çþÍc³›·uëÖU­VýÔ`}E°á·[,�ܼy3Lîæ”¬_£O¶ÜÆ”«Áv 翊ÈóÓ å$tϾLDê·ôÌüâ(Š@NÚ*ÙÔ@Ìüæ(Š„lNq¦Â�Eá¶®Éɘ©"ÓÊ<××Þ‹Ed}Z¹˜_L‚†¡oÚ¾ÕZ»6°,ÁÎÊÑǃ‹»ÐZ[› ]ó‹I°[®?®­4Ø�S�ï§”ÊâîÌöpè© óvåÖ ÁÎäŒÅ÷rs€¶˜oʆü ))Úh­­§ÌY×}íS-½ÖÚµkU( -MKÃÞ›�?Jƒ!ŠìøøøAé¼ËæŠGð¡(ŠvËaŒ�?0ü‚ëe¾»kI—dÑÖÚ×îCÙ<ØóÙïsÒ@5Ë|ð—D„L i™7ÁÎ^l5ÊžáR¾!§pºçãÛK¦ÁîÁFªÉ$Ip¹•ìiÈ^‚’›�c1egŒÙDD°(©•J¥²7Ò×ùcÕ]ÀàüP3GÉìnΣLñz¬’Ìeç­ÖZÄ hZºE°»)“làF‘Ës,<–”`g-�ší©‡A€=5/sö%Ø­FîÜß+Á~�9쌹1~˜µþIõ’�DyAÅr6\$yÏš˜r©Tú Ÿê ~VÖÚ“³¢tÁ*ÒTI÷§Aª2mÉÕô¶óLú½3Õ)öÛƒƒ²ˆøÑ¤ß`­ý@ú\‰¾Ùù[×ÒälŽs’Ñcþ“ˆžåê«$Ird6â¸ó_F.P?HÐn¦í™tD½¾1|‘ˆ|&#¿¼Ã|-¿éGVO’ä¤ééiø4ÕJ 6Ò§Üâ}côéÙ±Z*•@´p8Nýë൷eß]PÁ�;]X'¢(ºÒ«Ä©ìêm³$‡ù}÷Ý÷"fžôêy}Eÿìú2ç@W†‘‘‘çf/2ä¯Î �±È»¨ƒ™ëš¿œ¹é§éJ=g>à‡a¾\D>潿³X,îŸbÐÍ1ßÎÕî¼@{9ëpmñ]9v[[Ý¡ãØ7�Þí2È�¤I5 ¨«žÚ¼ÓLn�ì¬95Ík¾ùs„·Z­>qË–-¾Ok:÷çÌßÊǃœ¡g{ã(/<‡aø)©§{̳nÊD™�PÍ�ÙLjÒŠÍ Ä6_‚�s~Ö’®­}ٹ쭽ßA¬÷ÿ�Zkt±aéÖ~ŸI-xóÌÌÌqY�SÎ%Úuµµv޹t³ýÃïˆ1±êM©ß±ÿŒ òõ­ ¹Æ™æ SSSðí¬•v×”n-•JÏgæšUÖ|K_õÛïÉßÏêâWß0ÅÔbÉ. à "‚|äµâ.ÿÌó•Eš5\R—ü†çh°»6�Â0|‰ˆ|Ê}¯~í»´ µ[mé–L&&&Ž ‚—•i¹Ö˜SSSõX8Na‚ÀsRÙ-¥‰xÎžŠ³ôŸLOO×ãþ8œpi†ø%5sr?�{ûTír-/]©?Ö:‰"ND-ÓÎw~/å{J°D?k"þ4G|ꉈ�jàJkíen !?òýÞ¡¤A£~`­­ù0çm¸®ÂEäkÌ ÈdòYî ‚àQ©i�?8Œ1ÈÏZK€’r3Æ€ÀÔ/5Ò,¶óLZ¯1f?h]3*䑽MDŽeæ—Ñ^ûnœ™™9! úá"ø‚üù‡²97ÝnÆé�ÿÌiÖÚš¯®1}ª‘3Wpøû’Ë sq�oÿ&½þ $Að½”$.榆aÁFÛ ÁF”ÖŸ2ó1pÈôýËÖÚggdÞPƒí0ÂŇŸ*ã‡o˜¡îÿ<_ËíÍÄq� ÄW "Ì}Xn¤Ñ“ñ\Ý_»¹uB°»5ß2QT‹uíBI¹WDžÎ̘kiÀKñ\¿@ÝB”M_iŒñóͧíA0-¸tÀ_¯~ÙáËdkûB×ÔÑ•µÏïSfíõóØ~4Š¢W6zÖ� ßÛ´£ýÞƒ5Ö�Òý¹&ÊÌü �Lü±Ÿ~ 牷AðÍ4€e»;'ç7êü¾ˆ|"‚EäôÌxôû‡sÁçfffb\´»¦tƒ`7“ÇB×(•ÞØØØ>6˜WÿbÉÎÍÿºË•k öšo‹È!ÌŒ<Æ»e&ÈË…Þ­ydŒÁý¹®-g¦�Ë|åf—�™¹‡¾¥ëÎùÖÚZ^í´t²nwY&s,\{ntçeÌËÜüKI°ÑF?FJŠ!.eˆ±î‘&¢s2c¦ 3}§ËlpªTaˆO\ÃÌw …WåY{.t>/ÅûJ°D}ÁÎñmJe3ç–¥�yRÝ4&C°±ù5KÅRûFȦÁâ݈`×£jµzxmHËgüo–J¥3˜ùÓ-Rv¤¯ü!I’c¦§§Ó<� RHåQ? 2snμR©4Ç?‡ÀJ¥r(Ì�œy#4'õµ“‡î—BÓ™Á¸.‹ÅÜôs6Hdî!ÕëÏ-Åbñ„ìÂÒê€ä|Œ>Ù"¥‹Û{­µoÍâØÁv õœ›ó&‹ÆûÑÖZh½ý�Ú0ãk…Þñx;ùu,ÁöMY[¥éÉšˆ»àDMç;3ïvhïæ˜f­Ö¨vçêjB°Û^[Ú][Ã0|¿ˆ´Ê��®™Ÿ¿ÿþûϾüòËïsí|3ÿn%·Nj¨¯[ó-çÐ×lÿ¯ …c/¹ä¬ÿþ\z©ˆà2­å¾âÈ8./çÌGTæL8q‘—{Yé}¦ûaúÿ…ìn¬%Ý’E#à3A�:%ØóÞï]Î]Ä-i%×[™ù™àdóŽãÖM- rö€e,ér¡Á%¾ˆ@[ꎡÉ>pjjêÎv×”~'Ø.Ö?VL= w8‹);c \9p±Û¬àl„s\-~JÁîÖ<òÝ›|“u/ æÄÙ ¾¼ÆwSƒÝM™ ~Ê{ì±UDþ¼îX+1SÂ]¿pp{Ô43§îf=µ¨LÛ™c™Û¬ ß�™™yjV¡Ðe‚ýö¼ˆæI’´ôÑo�}ßüz`v†Ô4�~� (–GJ³&8Nbs¶3½Åáß¿µÊ%Ø"òAüšÕ'|™×¦ÚÛ‹N= ðð“ÊÜdæú·´óLÞ÷œÿ 瓌X,˜[‹Åâ;7oÞ|‡7‘_%"5M¿+;fffoX!“ׯÔoΜ/4,g4hõÅbñÌÍ›7ÿ* Ãw‰È;üïZkÇÝa®îÓÛÌŸ;ܤaP«lÔU?(L–`ãà‘$ Ìq]|¨R©¼-ëCåH0Çm4N sƆaˆ¼Üðãmt(»›™ME�çnÅ“5ß^ÇqÝdÜ¡T*�ÌÌHÝ�§EÁ£—‹Å¿öÇ„ÿ~››Ô-A<{jj*Í?]¯"ãë•çvQ6cÖ0 }&Òè°BɸJÌÙ ³föu"òjf†&jŽ©˜{o'3ŸðÁÛ4÷µ__·Æ<êlµF…aØÖ¼pcÑßksc>kK;k«kû³Ü-{†5Èò,v:‘›Ëõtu<ðÀ^)Qo°ÞÔ~Ü…ù6‚HàÌÜÐÿÚõï›"b¬µ~Œ‰zÓ&''O’䋞èn͆paj,Î]Z!—¯5OëÚ)"o^µjÕ¦íÛ·#ðUm�I’äßeÇ¥ëi¹Vus-é†,ÉÙ'ØØçÅ_HßÏD™^Ð~oŒyáâÔ·ª7—s»víšØ¶mÛ.c Æo}�Îì¶ö7¿añò"ª“Ì)ˆDÎ(yéÁ¸Ý5¥{m³9Ú�ß•J¥Ë˜ùUi]Í Þ8X4ÙµX‡®. ëªÕ*\DÞçÖ“ñƒVù-dMËd*¸ÉZ{,êv䶦xé$‚u& X=ëAÚÞN×ínͧôûaþ©ˆÀ‚Æ?Óá<|“ˆ|dýõ#I’gLOOÃÚ/Ý?ÚÚw»=Gœ;Ò¹úÖ§þ0@@¿¿_µjÕ;óÎ&¥áÛ¬µˆ©’[Œ1P¤Àò%÷¢Û�üoµz”`wcåZuÀÔ°Z­-"cæ{víÚõC?ºàr˜>©P(ì+"ùqš:»áÆqüfäÇ9Ëù =”ˆ~LD𛆿OÝ$­Ÿàq¾‡O‘™y_hC˜ùç³³³ŸÌ#…½h;òã!"XБ_$Iò…l^—Ãi­fa&Ýè`Ú‹6z‹çqÏ”æ±0#:Œˆ~ËÌ×3ó7òÜæÓ>7^Ÿàäô8™‘_:Syøuµ F¡P81I’ÇApg’$?‘MáÞ¢ˆ<™™ $â¿™ùº…IW;ܤ²,Á¶Ö>kÃŽ;^ |˜yœ™q‰vc¡PøJ;&PÝóÍÖ¨ÅÂ'û�vÖV¼³aƱÙÙÙÇ'ImÙ˜¹*"×2óµ£££76Št¼ýêÆ|[³fÍøÈÈÈÓ‰èÑ"³î}ˆZæŸAp]^J¸lßlòöÛo?ªP(<1I’'Að0˜›ApíÎ�;¯Gîâvñp—WO‘'23ÖÙŽŒŒ\ÛÎxm÷yÏ-t-é†,Ò~¼ÛíýÞÅ´€©8\"éÖØ[FGG?›ž1Ò3ÈöíÛOÄø ‚à·ÕjõÛí®Áy}Æ¥�ˆœ*"�cf¸þÜV­V¿ã¼òT­VŸÊÌ+�›{ÕªUÿ•w(_(®ËñýE–¯[·îH¬‘"+”Ÿ!fBÇp»ê¨ôÃ<ê¨Á<Ü ™¸@¿GŠHÇ1\DjgøŒ›R•�µ@ê é]}ç�;î¸ã8ìDôd—éçG÷ÜsÏç›EïjC\e¬ ë)fÞ9;;{kžn/¾»uŒ{1Àêôyn§uèóË�¬;%ØË¯'ÚâvÈ#Øí¾«Ï)ŠÀòG@÷ûå/Cí�"Ð(š‚ 8¿O’änk-|¯sK†ÏXÕJ^”wEzðP‚ÝCë†ÛCpû¸j%Ø},œ5M v�€Õj�e‚€î÷ËDPÚLE`drÅWÊåòþyÖA.¸(Ü6Ò¹™}æÑ}e™! »‡Ó ·‡àöqÕJ°ûX8=jšì«Õ*ËÝï—‰ ´™ŠÀ<0Æd³[\Šp~Ð6熘ÈrQ+Ì\OU:�Ïê+Ë%Ø=žn¸=·�«V‚ÝÇÂéQÓ”`÷X­VX&è~¿L¥ÍTæ�€ó«FÀ6ÄÉH ‚Ç~CDçq‚ŽËT}«K¯XžÇ'õ•eŽÀÀì0 kyò¢(zm¿È¤ß6ÜR©„üÒ—ÅqŒ¨Üʲy‡ã8>µ_0„v(Á^¾RÄaæ¯[k7v2G”`/_™kˇ�¼½obbâf~§ˆ¼kzzAG;*ý¶ßwÔx}X˜'�Α �OólNO_[·nÝQÕjõú6ÓׂŒŸ’—±§�\æ•/‚mŒ©mˆÖÚSúe|¹0õ¸ÕBy—µö‚¥l›K€vÔɽSƒpuW*.5Òg]­H§¶_w¿ µõ �ùÎc RX½Þµëjk-R£hQ�>D‡¸ìÞBÁÕI’œ:O‚�´4}³ß÷!ìÚ¤D o.¡› �Oý ÒK‰È˜y}¢}'M—Ëåwg³õkŸú©]�ÆS?µ±Ý¶(Án©xn¾äaº®]PÚB@çH[0éCŠÀ²F {Y¢�Wæ‰À°l&^³fÍÁ£££äRùþ_¥RÙ>==]Ëû­e~(Ážn=}«5Ø=íð<*Wò0Ðô•¡B@çÈP‰[;;¤(ÁRÁk·»ŽÀì®c©>èº:(–µªÁ¢­äaˆ„­]�:G曾¤,+”`/+qicû%Ø},œeØ4%Ø}(4Õ`·Š’‡ÖéÃ�€Î‘á–¿ö~8P‚=rÖ^ö%ؽÇx˜¾ »¥­»µP”<´ÆHŸntŽ ·üµ÷Ã�€ìá�³ö²÷(Áî=ÆÃô%Ø}(m%Ø­…¢ä¡5FúÄp# sd¸å¯½”`‡œµ—½G@ vï1¦/(ÁîCi—J¥Ï0óaÖÚú°yKÞ¤R©t<3_GDgZk?ƒcžGDŸ‚àiSSSß^òFj�%DÀó"úwyiÇŸÒ9²„ÂÐO+=DÀóM"ÚÛZûøô3“““«’$Aà ÖÚ‹zøy­ZÜ\:ÀZ{”ß©uëÖQ­V®ói`D½( Ãð?D\nÎxZ”�wù#ƒä¬y®\.¯Ü¶mÛo»ŒÓ²¯®T*­fæKEä„8ޝG‡&''H’äÿˆè­µoZö�Ô( @ÀáÞ‰ˆÆ­µ;tŽ,L}UèSŒ1�$¢[ˆèbkí«ýfcðóï[k_Þ§Í×f)}ƒ@:—DäãqÿE¶aƘ"ú’Χ¾Y_7dýúõ‡V*•Û�§¾n|Nã†`ONNž$É­Dô.k-ȶ�0 /‘×XkçÈÜó"z˜µöhLfŒ1?gæ;£(:)sèÖ92ÌCû>PcþŠˆ>ÈÌgGQt¥ß¹0 §EäÙÖÚêÓÚE xséœ(Š>’ýD†ß‘Cu>õü¬Òƒ ÏM"rnÇÛ–{†`C)‰átkp¥Úkfþ`E¯õë�˜˜xvÿADßH’drzzúÇÝú®Ö£,&''�M’Úë““$9uzzúk:G–ƒä´�Š@gcþ†ˆþŽˆ¦­µ¥ìÛÆ˜ƒ‰h»î‡�áªOé\‘MqãÒj·R*•þ˜™ÿ•ˆngæ Q]3|Hi�ÛA Ã׉ÈDäÛq?­�wúý™�"ØÛógøU}&‚󧦦~ÑïBèeû&''OI’äj"º¡\.Ÿ’g>ïÝm‚`Ãèèè6mÚ´«—íÒº�¥F`Æ cårùy"K¬Ck­Ík—Α¥––~_˜?ëÖ­;*I’7ŠÈZ"ºvÏ=÷|ú\ð@ƒ¹þN"Ú("µý0Š¢OÏÿËú¦"0XdæÒ�ÖÚ›õ0�mâžy]¥R¹rëÖ­¸ÄÒ¢Ðúõë�©T*PüÁ=� …ÂÑ—\rÉOš�#ØŽd×ü±ño�6 A½¾žú‚àZõÁó—"¿ëSˆèÞr¹|x3ßôR©tf¯‘Óˆè"Bг¯2ó¢(úY«ïéï�å€Àš5kŽ( §3óéD„?û1óW“$¹(ŽãiÖ�#ËAÂÚFEàAÂ0¬Íñ$I0ߟLD ¢‹‚ ¸hjjê×ÍpZ½zõA###ïafòÛ˜ù?«ÕêGŠÅâÏZ½«ø+ƒ†@Î\¢<‹ÈFý.•J'3óë‰è%DTÅùRD¾!"_˜žžþ¯AÃKûÓ`ù "z*~#$ì’`C@«W¯Þwtt·"«‰È÷§º^D~Á/DdÞÚmGÜivvö†ùUCûFFFjLƒ ØWDÒA6ßñu¼ˆì‹—©Æ?ID–Ëåmí¶1 ÃsE¦s¾êýDtýFD~Ã̵ƒ¸Ï·±úž0ó}"rþN’ä¾ ð÷ ½ÜY³fÍx�( û&I²g{ŠÈžÌŒ¿÷P<Ò­}@žEdfÞÿ&¢ý‰h¯Òk˜9Ž¢èÒN>¤s¤´–äÙj:ƒ øæ!æ#3ƒÍ{Í?å”SŠGqÄ#0Óù—™‡+–¤·ÃýÑ¢›×¸(Û_DÒyŽ¿Ó‚½þKAlŠ¢èG�À511qZ¡P8[DÎ&¢‡ºw¡ùþ3ï‘ßá3ÿŽˆðGKsrçæìììÏ·nÝú¿ obbâ�B¡p(aíß+I’½°?2ó^"¢s³5¸íÌ%D¿"‚+¦¦¦~غʹOcí<"Â…¾‡2Ûà|‰3f_f®$Iòû 0÷ïÖƒ_Äq|óB>ã¹çãi¡÷úý�%Ø>p.,H(ÒU�ˆž!Þ Âš™·%IòÁFD ewS�òBÉt^[o ¢ßâò€™a&}šŽk>;÷Üs÷/‹'Apb’$'a£"¢ôþý�ùÔ=¤ï ²ý×Dä{Ì|­µ~ï»¤Š‘§¸?OfæGþ¤›Ñ�Â×q·wÑ}Nk…¿Ó‹�¯ÑwFFF®½øâ‹ïî¸Vï�# AoÉÞý3c^‹q`­ýŸ¼–LLLÃÌO�æ™0™y|ÉZ­n„´aµùíÿa曓$ùº[ko\(|k×®=¬P(`=>gÁŸ‡1óÞŽx§/ôSCû>Lò‰è[�™;¿äî‘hbbâ”  {:�T2´Àu¯ã˜K5¢ˆ #ù=!›"ëãbò¶=÷Üó«�\+:i†1fŸ$IN`æ'ÑÓ‚ 8Ð) üs&þ½\ ÈöµÌ|“ˆ\“$É·lÙËÐÜ211qbÏ%"¤ ”ñ¼hã©_ÉPì^€ßfÔ[­VwããæÔi¶ÏÑvZô×4é õ Á ¿X¬®‚ M'ö‹…hVzÑ?­s~cöK’šÒý …BªQ�6åDg’œjP¯bæ‹¢(úFú%cÌÅîÿ¸)ÆáyY…¿¡ '"XÜ/"÷ *ï·ÖâgZEàÁ8#˜˜‡Åb±fÅà4›Ðrž."8”£T`2Œ?ÖZÌ·Z)•Jïgæ7àßXÏ™z~år$cÞ Ë”tb.V«Õû·mÛ†K-Š€"Ð�&s–F§ys5\FD°ÖÖ/HÂ0œL’äõÌü("ºYDpžº D~£sS‡ßb!«¦G>ò‘{�ŒŒì]©Tö‚`of>TDpwþvüàfþ"]EÑW¼3¸Ì¢‘ÅGÇób n‘¾£»K@;2 støK'T«ÕK™Ú˜i_Ø®™v—š¤Õôg�uVaŸ}ö�Éá©"ò|":.�fkŒIã\."ŸŒãøs}Ö|mŽ"0àŒˆNuÎušO�ì�Ƙ�Ì�/ ¢�4ÒpÚ E ÏÈÌÍs\ó@²7†aøy¢V‹È§³Ùú¬+ÚE ½°=’™_ND/%¢c‰è½ÖÚ·–J¥�áç:žw (Áî²lK¥LÐXMœÖúÌ…˜jw¹yZ]Ÿ ÿš .&¢þ~ý‡µAu´(ŠÀ" [½"¢cˆèDôQ":ßZ{á"|^?¡( p�±Þ�ô‰:7u˜,wÖ­[w”nDô "ú!µ™î5Ë]°MÚ¯»ÂõRc!€Ù©SSSsòêöà“Zå2EÀ7GÍËÁ¼L»¥ÍV– “““$I× \v‘µV÷Åe#=mè #`Œ�5 Liunº°¼ð='¢ë^3à‚vÝÓƒD�älŒ�ŸõaJ°{ð€T»nݺ•ÕjÑÞª{@„ªÝXvLNN>?I’ÕCü²�6xÀX¿~ý¡•J~ÖJ°\ÖÃÐ=ä¯V«?Ññ<øÒV‚Ý#c¶Ñ_*ÁîÀTm†Ÿ‘3•`�Pµ+Ëc r#�«{Ù‰N<àc®Cö�›.è!鞎çá´ìÉÙ“¦ ”`÷àª6 ׉ÈJ°H¨Ú•e‡@©Tº„™'õ¿ìD§ pŒ1„ϪÎÍô�t/u{Ðñ<ØW‚Ý#ù¦~ØJ°{ðUërz^­{€„ª]Yv¤‘üõгìD§ ptn¸€‡¬{:ž‡CàJ°{$g%Ø=v«U‚=€BÕ.-;ôгìD¦ tn‰ ‡¤›:ž‡CÐJ°{$g%Ø=v«U‚=€BÕ.-;ôгìD¦ tn‰ ‡¤›:ž‡CÐJ°{$g%Ø=v«U‚=€BÕ.-;ôгìD¦ tn‰ ‡¤›:ž‡CÐJ°{$ç”`‹È q_ߣÏhµ€€ì¢vaÙ# ‡že/BíÀ€" ss@;¤ÝÒñ<‚W‚Ý#9¯^½zßÑÑÑÕÖÚ {ô ­v€À‚k­Ý8@]Ò®(ËcŒ±ÖÚe×pm°"0àcηÖ^0àÝÔî :ž_ÐJ°_ÆÚCE@PE@PE@P�E@@ ö"€¬ŸPE@PE@PE@|”`¾Œµ‡Š€" (Š€" (Š€" (‹€€ìEY?¡(Š€" (Š€" (Š€"0ø(Á|kE@PE@PE@P%Ø‹²~BPE@PE@PE`ðP‚=ø2Ö*Š€" (Š€" (Š€" ,CG°×¯_P¥R9†™÷fæŸtÐA?Ú¸qcÒ ÖëÖ­[Y­VwYkïïä=}vy!°qãÆâM7Ýø­~ìc[ét¼,s¢øí�IDATV¯Ï:ë¬Q"ª^uÕUÕÅú¦~Gè'ܘӤnÎÙõë×ïµsçNÞºuëïû©ßÚE Àº}ûöÇÑ#™ù¦ƒ>ø'ýºö#~Ú¦î"°qãÆà¦›n*6«õª«®*w÷«ZÛ°"04£ÙÙÙK™ù¥aßÍÌ&Š¢O·3V¯^ý�ÑÑјùQ½¬�wô™å‰€1æ‡DôØœÖÿ„ˆ>f­}I?ônÆ c»víÚÉÌÿEÑKú¡MÚE`1˜œœ33óœ~8(@s·råÊŸ¹‹Ÿ7Ž´'Š@{xûv"ú¬{k„ˆŽ"¢gâÿÌügí^¦:B� ´ÿ°Öþ‰ìöä O 7Æ\>}›ˆŽ$¢[™y«ˆü/=†ˆ&qAED7 …/¹ä’™áFK{¿˜xûFfþqöÛ"ËÔW-f›–â[¥Réï™ùMA755E’– 0ÛƒCÒ½333÷MhÃ0œ‘Kˆ(²Öbñ¯˜“Q°qãÆJúÃ<‚�ç–Êìi)¿ÝƒñØWUzì=}w€sÏ=wÿ‘‘‘«¡Ý‘ׯqüÁlÃ1Nzu3¨2ï«a¢�é<‚}¥µöl¿Ya¾HD>ÃÌߌ¢è™&c̵D1Æ4%Ø:ûDøÚŒ¾AÀ³…ˆÖÑãããçøç§5kÖ<´X,bï|¢ˆüsǯïqÃÎíW«ïCR‚ÍÌk¢(º´ƒ&î6Ž–óÚ¯»É/àÑ¡ Øk×®=¬P(ü‚ˆn?Ì'ÃnÁÿ0}×Z[3Y³fÍx±Xü=Õi-¿."Sq_‘l"ú ÝCDFDûÑMI’œ5==]¿+•J'3ófgf 3Œ¯‹ÅpóæÍwà;ÆhYJDWÑ›ˆèPhE‰(įÝߨûÚ ^455õk¼·aƽwîÜùf~>Ì<ˆè6"úrïHŸYÀ˜ÐW�6~=99yl’$7ÑÝ©¹¨ó7û€;\ìED�ùÇ+•ÊkRŸÍ^ÉÜ]"ý�™?EÑßLLL<=‚�1ókDd‚ˆNucùk•Jå%êCªÃ|ÐhF°Ý¥Ø�جµXg©T*�ÁÌEDÏ""\¢~WD>ÇñÖ‰‰‰? ‚àDS:üîŽr¹üèÑÑÑœîïˆèµDô"ÚÁÌçGQ„u\‹"0´œwÞyŸ��ý ö¾J¥r@Þ>S*•Ždf¸ZTÆÇÇÇvìØqªˆlcæWGQ”Zžà|ôï2µqg&œ‹ÞšÎ;"ºŠˆÞ`­�u¿‡Yú~"²…™ÿZJ�›× ÖZœ—jåœsÎÙsÅŠØ¿b­ýÓ¡Ø�u¼]‚mŒÉŽ£DQô‚R©ôØ Þ""ÏuûÀ Ìüo"òNo ŽˆÈ˜ù/Ü™ëK8ûãØ˜$É ï½÷Þ;W®\ùsœ Eä!Ì �ù˜ˆ|¢R©¼att¨¿ÂÌŸ‘W¦u;KEœ1_\\TÅ¥û=ê†Òð@"úKÇOàÖøçÖÚ�1›ˆgBXaÞMDÿ”rŸ!=ïîPlXc� ê Ë###_Û¼yó²»…Dvø³ŠžÁ`.2ó3wíÚõ=ø`{ï}Á™>�èî°ÖŽãwaž."_Æ¿QÍ:õ{‹ÅâÑ ÙΟfT(×Ñ*·q¤Õãg('‰È·ã8~šÛD>ODÏÃæ@Dð²;$^g­}BÏGÍ�| ÁvrøàÅbñ¡KƘš\ %‘¯ÑÓ‰èt"ªË¥W2÷/~¬µg–J¥ç3ó¿:QÝKD!¢SÜeüÇ_1$bÔn -4Ø©ïÝW¬µÏZ·nÝQÕjëgEDpõ;œ»±îA€Ë¨%IònwщCÈ'q·ID0§K×ìk‰ûCÍüŠ¢<ËÛ/YkÏ(•JÎÌWú�l�Buï_.—a K)?Á·R‹Ú,?QñÅìCC°'ã{ìq†ˆà67FÐ:×Jz8Jb455³òZA‘;gggâð÷3÷Ÿ‰èü žÁŽJ¥‚¨µ·»®ZI’ìéˆzM�’-?j¡1æ¿áŸÁ!©¹·1šðÓÇÇÇGœ?SMnëÖ­;²R©<š™ODqy0 gjÔdžFŸãƒí� ÜÔ?+‚EĈ4^¬µyϼ˜ˆ`Áð^kí[{%ó&»NÜ8¾Zl½ˆY¬Q¤ßY,ZDG°¥¿Ž¢òú²Þyç=¬Z­-"�„Ù.I‰èÖZÌe¬¹ÛZ‹ø5¿í0 Ÿ+"_dæ7GQô‹Õ_ýŽ"Ðo„a¸AD>„󈵊ŠÜ’\fþcÁ9¬©; ÃWŠÈåâIDïO+M’äÙÐR§Z¾”eÎO ðw93ñã]„óûœy8´àZ†?È™ˆÔ2Cø¥Z­Â�o{Þ8rÏñÆ� iê99�™»ékíÉÞekM¡â�§ç¯2n©5Ë o¯©i™½ŸÁõá§ÖÚcÃ0üˆˆ¼q‚ @Á´¼šì ž²sçθ,Gsb�cþ‘ˆÞ~255u�ìÅðÃB°1)ðgN¾kçgó øõÜk­Ý×¢ý�”ȈÈ'ã8>+S†0Ez 6 "ÚCDàKݨÀïçG¶Äw½ …HæOô¿Ÿ%ØÆøv¼Ï3'Oƒ°)Áîâ¼iƒ`ÿ|gܸ�ÿ9Ù¨ŒÆ˜ãˆæp—#:e¯dÞ„`—¬µÓÞø‚‰Ñ J°»8P´ª¾@ ™‰x¶�N«3S\´¦æÞ0ÿkE°ç¬Ù“““§$IrµˆüMÇX—µ(C‰€³„ïôÖZ¸Öå–Ô¯Z­>¼P(À�*�`c-ÃDÜYXŠ4*²Ö¾Æ£cRk?oßû\9ŠÅâÁ³³³§032ɼ>Š"(F´ šˆgÇ—J¥÷13¬4°O ¤{FJ°a’}.ܬµpAªc xÆû|‚�åî2÷ëÖZ¸ò¥ïÕ vª|k$*f~Þ®]»¾ ‚�S÷ßÑÛÁO¢(úo%Ø‹3à‡‚`c Uøk":3ëk€H€Û·oG°²}àK[©T®#¢G …‡ù)$Â0|3ß³sçÎ[òò`û¡þ©ú>íò¿X,Þ·yóæ_ååTu>s�O°ï¾ûî=víÚ3ß�^•$É·¶lÙr[©Tú&3?U5ØÝ›8Ͷ `‡[ÄZ ŸR©ô!fÆíý â8†/v­„aø*¹,5+ê•Ìì¿6%ØÝ"ZS!Ð Áö#'IòÁÑÑÑâð7�6öœ<ØJ°ûhhS–c ü=¡¤ØY,÷÷ãÜÀEND¾ÁA.èÜÜgŒ�¢bÁvç²j”0 Ã×!p:!mf¶“###w]|ñÅw7!ØPJ|Ú?fFʽ3fgg¸ôÒK¡ÙÖ2$,„`{—G7ŠÈ› …µSSSwc@‚aÑzr©TBp³×Ñó­µpIM‰r�xûÛ¥T}™÷ ,¢šl\\=î†yÚw;.—ËI?1Æ(Á^‚1>;õ] ¢/f#Fz~©¦±Âs½Œ™ÏJÍ ½€eQ¹\~m+‚=::zã®]»Aº%3Õœ#!3CC[k_7²õë_ÿúUÌŒ4ï¶Ö¾ÃÝŽíá¢>D v÷fQ#‚mŒA.lD…<8'ÆDeŒ5>Š¢Znu”R©t¢D¦Ä»W2W‚Ý=¹kMË� 6âX ¯ýi„Ö0 ßæÜEQýöÉ×`ÃÃSó¹@ú.y_GŠn¶öJÍ4æC¶n¿ýöCƒ ø_·QœÇÌ h6 ˆ6I’“¦§§Aâµ,�`ƒ$—E¤@Dˆ�´>E?�‹ÛÀ‘~ iÕÎgf¤üAta˜ŒÞf­=>›½’y¥R¹ÞÅøŒE\5Ø úú²A ‚�–້ .Òk-'/4 årùÕÐnc@Äi|rÕªUß¾};Ò0*Á^6£BºØ¸àLˆ_3Z „¹,öC¸ã¡ì˜™™9ôª«®ª:ò=ã~²|»ˆÀï§iõ� æGD„Ëí/@‹-"/t‚÷‹ÅC\@³\‚íÈx� ¹9B®iõ{p,ñ÷H°×ŠÜí>#"Ññ̌Ծˆþ�T¹§=ðÀw¬X±¢–…Ï1óWEd½·s¢ˆwªÁž˜˜80‚4KÅ3Ã- uÃÅâËÖÚg§Š–œºçh°S‹"‚fýB¤ïZbÑ ä燂`CrnÑGbyßç¿‚ÿòEããã¯O5Ía¾DD°øBÃ�‚(àRuÙ† ÆœvzŽ�‘1yQ¦þÓÿƒ�>666� ÞÈÙÉÌëÓ÷ ÈVžv-˜VäÌ™#¯óÚwµK×…üv;³þG9r¡SÁö¿Ë„¯1óg£(šòáøÈyx€÷sܘŸn­Å _Ö˜s@7Æ,Xæcccûb\¦&�1HãSõÕ·i{ò¾µPê'�ž#P*•pà�‹OSÿO7ó—®(p¹9“ü2õÏôLþȹ!5לù›f™ ¢·h>Ñž‹Y?° pù°a ˆËh¿à¼…=¹©¯«T*ÏD®ìLtg<ƒQˆü¿2MÓeŒÁ\ÅœÅÜMË�I’¼rzzqN°¿"HìQÖÚZDe¿xšrìá{§–+ËNmb—0ÆÀU.£sÎEÙêóÆ‘;ûã¬�Ôpµ——Òwmê.è¢�#¾G:Nqĸ<©W¬XqWÑÔDßs–H©Ÿ–OU«U³eË–{ñ“0 ß%"°x­ñc ‚zâ y¸çX]v j­—õÆÂÄÄòÊáôa…BáI’üÐZ‹ÙsÊÆ�‹;vì€I÷oÒhÞóÁ ÁtªÕêñÕjõ¾+V|/M/1ŸºüwÖ¯_¿W¥Rùãr¹|ݶmÛ�Ú«–2`vvvÂþ/´~}~`Ülß¾‘IÁM~$úùÕøÿßR™/A}_˜‹ÀÄÄR쉵‡ pÈæ1Ì|kº/@ÃV,Ǧ§§tI‹" ´‰"v'Iò¸B¡pXµZýe?À¼2ÆÀZä333§C‹�êð,3?f×®]?ܶmHp^Áü|´#ß·ÇqŒ¬m•0 /"׋Ȧ8Ža]¨E褌#¢Ãï¾ûîï_uÕUe7vAXï-—Ë( –Ëe¤Ö¥+V`oÁx‡¦î N£kŒAÇ1óAÌü£ùž1AÆxà�ñ+Vlï/éÌaèö€ËS»§(Š€" (Š€"ð q¯nß¾ýSDôâ$IŽ˜žž®åÖ¢t/øíËåòKpQä2ÉÀuák퓺ù=­«¿P‚ÝßòÑÖ)Š€" (Š€" ÌcÌ«‰îs([­µ0çÕ¢tý™Zžè*¿Ó¹ ÂéIq#¾“–!A@ ö�Z»©(Š€" (ŠÀ0!àb£¼‚™¯@Úaê»öuñpî¥'‹Èó˜y•ˆ\S,?‡Ô¼‹ßýâR" {)Ñ×o+Š€" (Š€" (Š€"  J°F”ÚE@PE@PE@P�¥D@ öR¢¯ßVE@PE@PE@”`Œ(µ#Š€" (Š€" (Š€" (K‰€ì¥D_¿­(Š€" (Š€" (Š€"00(ÁQjGE@PE@PE@P–%ØK‰¾~[PE@PE@PE``P‚=0¢ÔŽ(Š€" (Š€" (Š€" ,%J°—}ý¶" (Š€" (Š€" (ŠÀÀ  {`D©QE@PE@PE@XJ”`/%úúmE@PE@PE@P��A@ öÀˆR;¢(Š€" (Š€" (Š€"°”(Á^JôõÛŠ€" (Š€" (Š€" (ƒ€ì�¥vDPE@PE@PE`)øä3þØ&êIEND®B`‚url-matcher-0.6.0/docs/patterns/query_patterns.csv000066400000000000000000000017201475357073500223520ustar00rootroot00000000000000Pattern, Behaviour ``/product|?id=34``," Match any URL whose path is ``/product`` and contains the query parameter ``id`` with the value 34 | **Match:** * ``http://example.com/product?cat=shoes&id=34`` **Don't match:** * ``http://example.com/product?id=12`` * ``http://example.com/product/other?id=34`` " ``/product|?id=*``," Match any URL whose path is ``/product`` and contains the query parameter ``id`` with any value | **Match:** * ``http://example.com/product?cat=shoes&id=34`` * ``https://example.com/product?id=12&cat=clothes`` * ``https://example.com/product?id=`` **Don't match:** * ``http://example.com/product?cat=shoes`` * ``http://example.com/product?cat=shoes&ids=34`` " ``?cat=shoes&cat=pants``,"Match any URL containing the query parameters ``cat`` with the values ``shoes`` or ``pants`` | **Match:** * ``http://example.com/product?cat=shoes&id=34`` * ``http://example.org/p?cat=pants`` **Don't match:** * ``http://example.org/p?cat=pant`` "url-matcher-0.6.0/docs/requirements.txt000066400000000000000000000000461475357073500201740ustar00rootroot00000000000000Sphinx >= 3.1 sphinx-rtd-theme >= 0.4 url-matcher-0.6.0/pyproject.toml000066400000000000000000000054101475357073500166740ustar00rootroot00000000000000[tool.bumpversion] current_version = "0.6.0" commit = true tag = true tag_name = "{new_version}" [[tool.bumpversion.files]] filename = "setup.py" [[tool.bumpversion.files]] filename = "url_matcher/__version__.py" [[tool.mypy.overrides]] module = "tests.*" allow_untyped_defs = true check_untyped_defs = true [tool.ruff] line-length = 120 [tool.ruff.lint] extend-select = [ # flake8-bugbear "B", # flake8-comprehensions "C4", # pydocstyle "D", # flake8-future-annotations "FA", # flynt "FLY", # refurb "FURB", # isort "I", # flake8-implicit-str-concat "ISC", # flake8-logging "LOG", # Perflint "PERF", # pygrep-hooks "PGH", # flake8-pie "PIE", # pylint "PL", # flake8-pytest-style "PT", # flake8-use-pathlib "PTH", # flake8-pyi "PYI", # flake8-quotes "Q", # flake8-return "RET", # flake8-raise "RSE", # Ruff-specific rules "RUF", # flake8-bandit "S", # flake8-simplify "SIM", # flake8-slots "SLOT", # flake8-debugger "T10", # flake8-type-checking "TC", # pyupgrade "UP", # pycodestyle warnings "W", # flake8-2020 "YTT", ] ignore = [ # Missing docstring in public module "D100", # Missing docstring in public class "D101", # Missing docstring in public method "D102", # Missing docstring in public function "D103", # Missing docstring in public package "D104", # Missing docstring in magic method "D105", # Missing docstring in __init__ "D107", # One-line docstring should fit on one line with quotes "D200", # No blank lines allowed after function docstring "D202", # 1 blank line required between summary line and description "D205", # Multi-line docstring closing quotes should be on a separate line "D209", # First line should end with a period "D400", # First line should be in imperative mood; try rephrasing "D401", # First line should not be the function's "signature" "D402", # Too many return statements "PLR0911", # Too many branches "PLR0912", # Too many arguments in function definition "PLR0913", # Too many statements "PLR0915", # Magic value used in comparison "PLR2004", # String contains ambiguous {}. "RUF001", # Docstring contains ambiguous {}. "RUF002", # Comment contains ambiguous {}. "RUF003", # Mutable class attributes should be annotated with `typing.ClassVar` "RUF012", # Use of `assert` detected "S101", ] [tool.ruff.lint.per-file-ignores] "url_matcher/__init__.py" = ["F401", "F403"] "url_matcher/example.py" = ["PT", "S"] [tool.ruff.lint.pydocstyle] convention = "pep257" url-matcher-0.6.0/requirements-dev.txt000066400000000000000000000000221475357073500200120ustar00rootroot00000000000000pre-commit pytest url-matcher-0.6.0/setup.py000066400000000000000000000023271475357073500154760ustar00rootroot00000000000000from pathlib import Path from setuptools import find_packages, setup setup( name="url-matcher", version="0.6.0", description="URL matching rules library to connect URLs with resources", long_description=Path("README.rst").read_text(encoding="utf-8"), long_description_content_type="text/x-rst", author="Zyte Group Ltd", author_email="info@zyte.com", url="https://github.com/zytedata/url-matcher", packages=find_packages( exclude=[ "tests", ] ), package_data={ "url_matcher": ["py.typed"], }, include_package_data=True, python_requires=">=3.9", install_requires=[ "tldextract>=1.2", ], classifiers=[ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", ], ) url-matcher-0.6.0/tests/000077500000000000000000000000001475357073500151225ustar00rootroot00000000000000url-matcher-0.6.0/tests/__init__.py000066400000000000000000000000001475357073500172210ustar00rootroot00000000000000url-matcher-0.6.0/tests/fixtures/000077500000000000000000000000001475357073500167735ustar00rootroot00000000000000url-matcher-0.6.0/tests/fixtures/patterns.json000066400000000000000000000036761475357073500215420ustar00rootroot00000000000000[ { "description": "Universal matcher by using an empty include pattern", "patterns": { "include": [], "exclude": ["/index.html|"] }, "match": [ "https://books.toscrape.com/catalogue/join_902/index.html", "https://books.toscrape.com/catalogue/arena_587/index.html" ], "no_match": [ "https://books.toscrape.com/index.html" ] }, { "description": "Match products in books.toscrape.com", "patterns": { "include": [ "books.toscrape.com/catalogue/" ], "exclude": [ "/catalogue/category/" ] }, "match": [ "https://books.toscrape.com/catalogue/join_902/index.html", "https://books.toscrape.com/catalogue/arena_587/index.html" ], "no_match": [ "https://books.toscrape.com/index.html", "https://books.toscrape.com/catalogue/category/books/mystery_3/index.html" ] }, { "description": "Match product lists in books.toscrape.com", "patterns": { "include": [ "books.toscrape.com/catalogue/category/", "books.toscrape.com/|", "books.toscrape.com/index.html|" ] }, "match": [ "https://books.toscrape.com/catalogue/category/books/mystery_3/index.html", "https://books.toscrape.com/index.html", "https://books.toscrape.com/" ], "no_match": [ "https://books.toscrape.com/catalogue/join_902/index.html" ] }, { "description": "All Goodreads individual photo pages but excluding those from users", "patterns": { "include": [ "goodreads.com/photo/?photo=*" ], "exclude": [ "/photo/user/" ] }, "match": [ "https://www.goodreads.com/photo/author/2900145.Tracy_Ames?page=1&photo=113390", "https://www.goodreads.com/photo/group/30626-goodreads-italia?photo=925633" ], "no_match": [ "https://www.goodreads.com/photo/user/44137919-mikee-readwithmikee?photo=1762284" ] } ]url-matcher-0.6.0/tests/fixtures/patterns_corner_cases.json000066400000000000000000000027401475357073500242570ustar00rootroot00000000000000[ { "description": "Universal matcher by using an empty include pattern", "patterns": { "include": [ "" ], "exclude": [] }, "match": [ "https://books.toscrape.com/catalogue/join_902/index.html", "http://whatever:443/some?more=things#everything", "https://example.com/index.html" ], "no_match": [ ] }, { "description": "Universal matcher by using an empty include pattern and some excludes", "patterns": { "include": [ "" ], "exclude": [ "/index.html|" ] }, "match": [ "https://books.toscrape.com/catalogue/join_902/index.html", "http://whatever:443/some?more=things#everything" ], "no_match": [ "https://example.com/index.html" ] }, { "description": "Universal matcher with everything excluded", "patterns": { "include": [ "" ], "exclude": [ "" ] }, "match": [ ], "no_match": [ "https://example.com/index.html", "https://books.toscrape.com/catalogue/join_902/index.html", "http://whatever:443/some?more=things#everything" ] }, { "description": "Using IPs", "patterns": { "include": [ "127.0.0.1:80/ipv4", "[::1]:80/ipv6" ] }, "match": [ "http://127.0.0.1/ipv4/something_else", "http://[::1]/ipv6/something_else" ], "no_match": [ "https://example.com/ipv4/something_else" ] } ]url-matcher-0.6.0/tests/fixtures/rules.json000066400000000000000000000061711475357073500210250ustar00rootroot00000000000000[ { "description": "Priority is respected", "rules": [ [ "1", { "include": ["example.com/path"], "exclude": [], "priority": 500 }], [ "2", { "include": ["example.com"], "exclude": [], "priority": 600 }], [ "2", { "include": ["example.com"], "exclude": [], "priority": 550 }], [ "3", { "include": ["example.com"], "exclude": [], "priority": 400 }] ], "cases": [ [ "http://example.com/path", "2" ] ] }, { "description": "Use the most specific includes to resolve conflicts", "rules": [ ["1", { "include": ["example.com", "blog.example.org/path?id=12#fragment"] }], ["2", { "include": ["blog.example.com", "blog.example.org/path?id=12"] }], ["3", { "include": ["blog.example.com/path", "blog.example.org/path"] }], ["4", { "include": ["blog.example.com/path?query=23", "blog.example.org"] }], ["5", { "include": ["blog.example.com/path?query=23#fragment", "example.org"] }] ], "cases": [ ["http://example.com", "1"], ["http://example.com/other", "1"], ["http://blog.example.com", "2"], ["http://blog.example.com/other", "2"], ["http://blog.example.com/path/without_query", "3"], ["http://blog.example.com/path/with_query?query=23", "4"], ["http://blog.example.com/path/with_query?query=23#fragment", "5"], ["http://blog.example.org/path/subpath?id=12#fragment_subsegment", "1"], ["http://blog.example.org/path?id=12", "2"], ["http://blog.example.org/path", "3"], ["http://blog.example.org", "4"], ["http://example.org", "5"], ["http://myexample.org", null] ] }, { "description": "Id is respected on conflicts", "rules": [ ["3", { "include": ["example.com"], "exclude": ["/something"] }], ["1", { "include": ["example.com"] }], ["2", { "include": ["example.com"] }] ], "cases": [ ["http://example.com", "3"], ["http://example.org", null] ] }, { "description": "Id is respected on conflicts", "rules": [ ["3", { "include": ["example.com"], "exclude": ["/something"] }], ["1", { "include": ["example.com"] }], ["2", { "include": ["example.com"] }] ], "cases": [ ["http://example.com", "3"], ["http://example.org", null] ] }, { "description": "Domain rules have always priority over universal ones", "rules": [ ["1", { "include": ["example.com"], "priority": 500 }], ["2", { "include": ["example.com"], "priority": 500 }], ["3", { "include": [], "priority": 600 }], ["4", { "include": [""], "priority": 600 }] ], "cases": [ ["http://example.com", "2"], ["http://other", "4"] ] } ]url-matcher-0.6.0/tests/fixtures/single_patterns.json000066400000000000000000000140161475357073500230710ustar00rootroot00000000000000[ { "description": "Universal matching", "pattern": "", "match": [ "http://www.example.com/", "https://EXAMPLE.COM/product1", "http://products.example.com/path?id=23¶m=#hash", "http://otherexample.com/" ], "no_match": [] }, { "description": "Strict domain", "pattern": "example.com/", "match": [ "http://www.example.com/", "https://EXAMPLE.COM/product1" ], "no_match": [ "http://products.example.com/", "http://otherexample.com/" ] }, { "description": "Domain and subdomains", "pattern": "example.com", "match": [ "http://www.example.com/", "http://product.example.COM/product1", "https://example.com/product2", "https://example.com/product?id=2344#main_section" ], "no_match": [ "http://otherexample.com/" ] }, { "description": "Strict domain + root path", "pattern": "example.com/|", "match": [ "http://www.example.com/", "https://example.com/", "https://example.com/?id=23" ], "no_match": [ "http://example.com/product", "http://otherexample.com/", "https://uk.example.com/", "http://shop/example.com/" ] }, { "description": "Strict domain + strict path", "pattern": "example.com/index.htm|", "match": [ "http://www.example.com/index.htm", "https://example.com/index.htm?color=blue" ], "no_match": [ "http://example.com/index.html", "http://shop.example.com/index.htm" ] }, { "description": "Strict schema", "pattern": "http://example.com", "match": [ "http://www.example.com/", "http://www.example.com/product1", "HTTP://example.com/product1", "http://example.com/product2", "http://shop.example.com/product2" ], "no_match": [ "https://www.example.com/" ] }, { "description": "Path subsection 1", "pattern": "/product/", "match": [ "http://www.example.com/product/pen", "http://other-example.com/PRODUCT/", "https://example.com/product/rubber?size=xl" ], "no_match": [ "https://www.example.com/product", "http://www.example.com/shop/product/pen" ] }, { "description": "Path subsection 2", "pattern": "/product/*", "match": [ "http://www.example.com/product/pen", "http://other-example.com/PRODUCT/", "https://example.com/product/rubber?size=xl" ], "no_match": [ "https://www.example.com/product" ] }, { "description": "Path subsection 3", "pattern": "/product/*|", "match": [ "http://www.example.com/product/pen", "http://other-example.com/PRODUCT/", "https://example.com/product/rubber?size=xl" ], "no_match": [ "https://www.example.com/product" ] }, { "description": "match extension", "pattern": "/*.jpg|", "match": [ "http://www.example.com/my/image.jpg", "http://anything.com/my/image.JPG", "https://example.com/other-image.jpg#big" ], "no_match": [ "http://www.example.com/my/image.jpeg", "http://www.example.com/my/image/jpg_2" ] }, { "description": "Wildcards in the path", "pattern": "example.com/category/*/product/", "match": [ "http://example.com/category/bread/product/loaf", "http://www.example.com/category/bread/product/loaf", "https://example.com/category/bread/product/loaf?id=23#main" ], "no_match": [ "http://example.com/product/loaf", "http://shop.example.com/product/loaf", "http://fr.example.com/category/bread/product/loaf" ] }, { "description": "Must have query param", "pattern": "example.com/product?id=*", "match": [ "http://www.example.com/product?location=US&id=23", "http://www.example.com/PRODUCT?ID=23#main_section" ], "no_match": [ "http://uk.example.com/product?location=US&id=23", "http://www.example.com/product?location=US" ] }, { "description": "Wildcard in param value", "pattern": "example.com?product_id=*&category=shoes", "match": [ "http://www.example.com/product?category=shoes&location=US&product_id=23", "http://example.com/product?category=SHOES&product_id=23#main_section" ], "no_match": [ "http://www.example.com/product?product_id=23&category=clothes", "http://www.example.com/product?category=shoes", "http://shop.example.com/product?category=SHOES&product_id=23#main_section" ] }, { "description": "Multiple possible values for a param", "pattern": "example.com?param=1¶m=2", "match": [ "http://www.example.com?param=1&otherparam=3", "http://www.EXAMPLE.com?PARAM=2" ], "no_match": [ "http://example.com?param=3" ] }, { "description": "Wildcard in fragment", "pattern": "example.com#product_section_*", "match": [ "http://www.example.com/product_1?location=US#product_section_main", "http://example.com#product_section_main" ], "no_match": [ "http://www.example.com/product_1?location=US#product_description", "http://www.example.com/product_1?location=US", "http://uk.example.com#product_section_main" ] }, { "description": "Mix path wildcard with param string", "pattern": "shop.example.com/*/product/?category=shoes", "match": [ "https://shop.example.com/US/product/nike_air?sid=423&category=shoes" ], "no_match": [ "https://example.com/US/product/nike_air?sid=423&category=shoes", "https://shop.example.com/US/product/loaf?sid=424&category=bread", "https://shop.example.com/US/product/nike_air?sid=423", "https://shop.example.com/product/nike_air?sid=423&category=shoes", "https://my.shop.example.com/US/product/nike_air?sid=423&category=shoes" ] }, { "description": "Empty schema, but double slash", "pattern": "//example.com/product", "match": [ "http://another.page//example.com/product" ], "no_match": [ "https://example.com/product" ] } ]url-matcher-0.6.0/tests/fixtures/single_patterns_corner_cases.json000066400000000000000000000130011475357073500256100ustar00rootroot00000000000000[ { "description": "Double slash at the start", "pattern": "//strange_path/subpath", "match": [ "http://example.com//strange_path/subpath/something_else" ], "no_match": [ "http://strange_path/subpath" ] }, { "description": "Wildcard not allowed in query params", "pattern": "example.com?id_gen_*&category=shoes", "match": [ "http://example.com/prod?id_gen_=&category=shoes" ], "no_match": [ "http://www.example.com/product?category=shoes&id_gen_men=23", "http://www.example.com/product?product_id=23&category=clothes", "http://www.example.com/product?category=shoes", "http://shop.example.com/prod?id_gen_women=45&category=shoes" ] }, { "description": "Wildcard not allowed in query params (II)", "pattern": "example.com/path?*_id=34", "match": [ "https://example.com/path?_id=34" ], "no_match": [ "https://example.com/path?my_id=34" ] }, { "description": "Wildcard not allowed in query params (III)", "pattern": "example.com/path?*_id_*=*", "match": [ "https://example.com/path?_id_=34" ], "no_match": [ "https://example.com/path?_id_product=34", "https://example.com/path?my_id_product=34" ] }, { "description": "Single wildcard in query parameter", "pattern": "example.com/path?*", "match": [ "https://example.com/path", "https://example.com/path?id=34", "https://example.com/path?=34" ], "no_match": [ ] }, { "description": "Mix values and wildcards in query params", "pattern": "example.com?id=32&id=A*", "match": [ "http://www.example.com/product?otm=2&id=32", "http://example.com/prod?id=A45&category=shoes", "http://www.example.com/product?otm=2&id=32&id=a43", "http://www.example.com/product?&id=44&id=32" ], "no_match": [ "http://www.example.com/product?&id=44" ] }, { "description": "Empty query", "pattern": "example.com/path?", "match": [ "https://example.com/path", "https://example.com/path?id=34", "https://example.com/path?=34" ], "no_match": [ ] }, { "description": "Port provided", "pattern": "example.com:123", "match": [ "http://www.example.com:123/product?category=shoes&id_gen_men=23", "https://example.com:123/product?category=shoes&id_gen_men=23" ], "no_match": [ "http://www.example.com/product?category=shoes&id_gen_men=23" ] }, { "description": "Port 80 ignored", "pattern": "example.com:80", "match": [ "http://www.example.com:80/product?category=shoes&id_gen_men=23", "http://example.com/product?category=shoes&id_gen_men=23" ], "no_match": [ "https://www.example.com/product?category=shoes&id_gen_men=23", "https://www.example.com:80/product?category=shoes&id_gen_men=23", "http://www.example.com:123/product?category=shoes&id_gen_men=23" ] }, { "description": "Port 80 ignored (II)", "pattern": "http://example.com:80", "match": [ "http://www.example.com:80/product?category=shoes&id_gen_men=23", "http://example.com/product?category=shoes&id_gen_men=23" ], "no_match": [ "https://www.example.com/product?category=shoes&id_gen_men=23", "https://www.example.com:80/product?category=shoes&id_gen_men=23", "http://www.example.com:123/product?category=shoes&id_gen_men=23" ] }, { "description": "Port 443 ignored", "pattern": "example.com:443", "match": [ "https://example.com:443/product?category=shoes&id_gen_men=23", "https://example.com/product?category=shoes&id_gen_men=23" ], "no_match": [ "http://example.com/product?category=shoes&id_gen_men=23", "http://example.com:443/product?category=shoes&id_gen_men=23", "https://example.com:80/product?category=shoes&id_gen_men=23" ] }, { "description": "Port 443 ignored (II)", "pattern": "https://example.com:443", "match": [ "https://example.com:443/product?category=shoes&id_gen_men=23", "https://example.com/product?category=shoes&id_gen_men=23" ], "no_match": [ "http://example.com/product?category=shoes&id_gen_men=23", "http://example.com:443/product?category=shoes&id_gen_men=23", "https://example.com:80/product?category=shoes&id_gen_men=23" ] }, { "description": "Messing with http, https, 80 and 443", "pattern": "https://example.com:80", "match": [ "https://www.example.com:80/product?category=shoes&id_gen_men=23" ], "no_match": [ "http://example.com/product?category=shoes&id_gen_men=23", "https://www.example.com/product?category=shoes&id_gen_men=23", "http://www.example.com:80/product?category=shoes&id_gen_men=23" ] }, { "description": "Messing with http, https, 80 and 443 (II)", "pattern": "http://example.com:443", "match": [ "http://www.example.com:443/product?category=shoes&id_gen_men=23" ], "no_match": [ "https://example.com:443/product?category=shoes&id_gen_men=23", "https://www.example.com/product?category=shoes&id_gen_men=23", "http://www.example.com/product?category=shoes&id_gen_men=23" ] }, { "description": "IPs also works", "pattern": "127.0.0.1", "match": [ "http://127.0.0.1:80/product?category=shoes&id_gen_men=23", "https://127.0.0.1/product?category=shoes&id_gen_men=23" ], "no_match": [ "http://example.com/product?category=shoes&id_gen_men=23" ] } ]url-matcher-0.6.0/tests/test_matcher.py000066400000000000000000000156571475357073500201740ustar00rootroot00000000000000import pytest from url_matcher import Patterns, URLMatcher from url_matcher.matcher import IncludePatternsWithoutDomainError from .util import load_json_fixture PATTERNS_FIXTURE = load_json_fixture("patterns") CORNER_CASES_FIXTURE = load_json_fixture("patterns_corner_cases") RULES_FIXTURE = load_json_fixture("rules") @pytest.mark.parametrize( ("patterns", "match", "no_match"), [(row["patterns"], row["match"], row["no_match"]) for row in PATTERNS_FIXTURE], ids=[row["description"] for row in PATTERNS_FIXTURE], ) def test_matcher_single_rule(patterns, match, no_match): matcher = URLMatcher() matcher.add_or_update(23, Patterns(**patterns)) for url in match: assert matcher.match(url) == 23 assert list(matcher.match_all(url)) == [23] for url in no_match: assert not matcher.match(url) assert list(matcher.match_all(url)) == [] @pytest.mark.parametrize( ("patterns", "match", "no_match"), [(row["patterns"], row["match"], row["no_match"]) for row in CORNER_CASES_FIXTURE], ids=[row["description"] for row in CORNER_CASES_FIXTURE], ) def test_matcher_single_rule_corner_cases(patterns, match, no_match): matcher = URLMatcher() matcher.add_or_update(23, Patterns(**patterns)) for url in match: assert matcher.match(url) == 23 assert list(matcher.match_all(url)) == [23] for url in no_match: assert not matcher.match(url) assert list(matcher.match_all(url)) == [] @pytest.mark.parametrize( ("rules", "cases"), [(row["rules"], row["cases"]) for row in RULES_FIXTURE], ids=[row["description"] for row in RULES_FIXTURE], ) def test_matcher_rules(rules, cases): matcher = URLMatcher() for id, patterns in rules: matcher.add_or_update(id, Patterns(**patterns)) for url, id in cases: assert matcher.match(url) == id def test_matcher_init(): rules = { 1: Patterns(["example.com"]), 2: Patterns(["other.com"]), } matcher1 = URLMatcher(rules) matcher2 = URLMatcher(rules.items()) for matcher in [matcher1, matcher2]: assert matcher.match("http://example.com") == 1 assert matcher.match("http://other.com") == 2 assert matcher.match("http://non-match") is None def test_matcher_add_remove_get(): matcher = URLMatcher() patterns = Patterns(["example.com"]) matcher.add_or_update(1, patterns) assert matcher.match("http://example.com") == 1 assert matcher.get(1) is patterns assert list(matcher.match_universal()) == [] patterns_3 = Patterns(["example.com/articles"]) matcher.add_or_update(3, patterns_3) assert matcher.match("http://example.com/articles") == 3 assert matcher.get(3) is patterns_3 # Testing update patterns = Patterns(["example.com/products"]) matcher.add_or_update(1, patterns) assert matcher.match("http://example.com") is None assert matcher.match("http://example.com/products") == 1 assert matcher.get(1) is patterns # Testing universal patterns univ_patterns = Patterns([""]) matcher.add_or_update(2, univ_patterns) assert matcher.match("http://example.com") == 2 assert matcher.match("http://example.com/products") == 1 assert matcher.get(2) is univ_patterns assert list(matcher.match_universal()) == [2] # Removing a universal pattern matcher.remove(2) assert matcher.match("http://example.com") is None assert matcher.match("http://example.com/products") == 1 assert matcher.get(2) is None assert list(matcher.match_universal()) == [] # Removing regular patterns matcher.remove(3) assert matcher.match("http://example.com/products") == 1 assert matcher.match("http://example.com/articles") is None assert matcher.get(3) is None matcher.remove(1) assert matcher.match("http://example.com/products") is None assert matcher.get(1) is None assert len(matcher.matchers_by_domain) == 0 assert len(matcher.patterns) == 0 # Wrong patterns with pytest.raises(IncludePatternsWithoutDomainError): matcher.add_or_update(1, Patterns(["/no_domain_pattern"])) # Patterns with the same domain shouldn't produce multiple matchers patterns = Patterns(["example.com/products", "example.com/brands"]) matcher.add_or_update(1, patterns) assert len(matcher.matchers_by_domain) == 1 assert len(matcher.matchers_by_domain["example.com"]) == 1 assert len(matcher.patterns) == 1 assert matcher.match("http://example.com") is None assert matcher.match("http://example.com/products") == 1 assert matcher.match("http://example.com/brands") == 1 assert len(list(matcher.match_all("http://example.com/products"))) == 1 def test_dedupe_unique_patterns(): p = [ Patterns(["example.com"]), Patterns(include=["example.com"], exclude=None, priority=500), ] assert len(set(p)) == 1 p.append(Patterns(["example.com"], priority=1)) assert len(set(p)) == 2 def test_patterns_immutability(): p = Patterns(["example.com"]) with pytest.raises(AttributeError): p.priority = 1 # type: ignore[misc] def test_match_all(): matcher = URLMatcher() matcher.add_or_update(1, Patterns(include=["example.com"])) matcher.add_or_update(2, Patterns(include=["foo.example.com"])) matcher.add_or_update(3, Patterns(include=["bar.example.com/products"])) matcher.add_or_update(4, Patterns(include=["bar.example.com"])) assert list(matcher.match_all("http://example.com")) == [1] assert list(matcher.match_all("http://foo.example.com")) == [2, 1] assert list(matcher.match_all("http://bar.example.com")) == [4, 1] assert list(matcher.match_all("http://example.com/products")) == [1] assert list(matcher.match_all("http://foo.example.com/products")) == [2, 1] assert list(matcher.match_all("http://bar.example.com/products")) == [3, 4, 1] def test_match_all_include_universal(): matcher = URLMatcher() matcher.add_or_update(1, Patterns(include=["example.com"])) matcher.add_or_update(2, Patterns(include=[])) matcher.add_or_update(3, Patterns(include=["foo.example.com"])) matcher.add_or_update(4, Patterns(include=[""])) assert list(matcher.match_all("http://example.com")) == [1, 4, 2] assert list(matcher.match_all("http://example.com", include_universal=False)) == [1] assert list(matcher.match_all("http://foo.example.com")) == [3, 1, 4, 2] assert list(matcher.match_all("http://foo.example.com", include_universal=False)) == [3, 1] assert list(matcher.match_all("http://example.net")) == [4, 2] assert list(matcher.match_all("http://example.net", include_universal=False)) == [] def test_match_universal(): matcher = URLMatcher() matcher.add_or_update(1, Patterns(include=["example.com"])) matcher.add_or_update(2, Patterns(include=[])) matcher.add_or_update(3, Patterns(include=["foo.example.com"])) matcher.add_or_update(4, Patterns(include=[""])) assert list(matcher.match_universal()) == [4, 2] url-matcher-0.6.0/tests/test_patterns.py000066400000000000000000000025131475357073500203740ustar00rootroot00000000000000import pytest from url_matcher.patterns import PatternMatcher from .util import load_json_fixture PATTERNS_FIXTURE = load_json_fixture("single_patterns") CORNER_CASES_FIXTURE = load_json_fixture("single_patterns_corner_cases") @pytest.mark.parametrize( ("pattern", "match", "no_match"), [(row["pattern"], row["match"], row["no_match"]) for row in PATTERNS_FIXTURE], ids=[row["description"] for row in PATTERNS_FIXTURE], ) def test_single_patterns(pattern, match, no_match): matcher = PatternMatcher(pattern) for url in match: assert matcher.match(url) for url in no_match: assert not matcher.match(url) @pytest.mark.parametrize( ("pattern", "match", "no_match"), [(row["pattern"], row["match"], row["no_match"]) for row in CORNER_CASES_FIXTURE], ids=[row["description"] for row in CORNER_CASES_FIXTURE], ) def test_single_patterns_corner_cases(pattern, match, no_match): matcher = PatternMatcher(pattern) for url in match: assert matcher.match(url) for url in no_match: assert not matcher.match(url) def test_pattern_matcher_warning(): with pytest.warns(SyntaxWarning): matcher = PatternMatcher("example.com/path?*_id=34") assert matcher.match("http://example.com/path?_id=34") assert not matcher.match("http://example.com/path?a_id=34") url-matcher-0.6.0/tests/util.py000066400000000000000000000004271475357073500164540ustar00rootroot00000000000000from __future__ import annotations import json from pathlib import Path from typing import Any, cast def load_json_fixture(name: str) -> list[dict[str, Any]]: return cast(list[dict[str, Any]], json.loads((Path(__file__).parent / "fixtures" / f"{name}.json").read_text())) url-matcher-0.6.0/tox.ini000066400000000000000000000016661475357073500153040ustar00rootroot00000000000000[tox] envlist = min,py39,py310,py311,py312,py313,mypy,docs [testenv] deps = pytest pytest-cov commands = py.test \ --cov-report=term --cov-report=html --cov-report=xml --cov=url_matcher \ --cov-report=term-missing --doctest-modules \ {posargs:url_matcher tests} [testenv:min] basepython = python3.9 deps = {[testenv]deps} tldextract==1.2 [testenv:mypy] deps = mypy==1.15.0 pytest==8.3.3 commands = mypy --strict url_matcher tests [docs] changedir = docs deps = -rdocs/requirements.txt [testenv:docs] basepython = python3 changedir = {[docs]changedir} deps = {[docs]deps} commands = sphinx-build -W -b html . {envtmpdir}/html [testenv:pre-commit] deps = pre-commit commands = pre-commit run --all-files --show-diff-on-failure [testenv:twinecheck] basepython = python3 deps = twine==5.1.1 build==1.2.2 commands = python -m build --sdist twine check dist/* url-matcher-0.6.0/url_matcher/000077500000000000000000000000001475357073500162655ustar00rootroot00000000000000url-matcher-0.6.0/url_matcher/__init__.py000066400000000000000000000001201475357073500203670ustar00rootroot00000000000000__all__ = ["Patterns", "URLMatcher"] from .matcher import Patterns, URLMatcher url-matcher-0.6.0/url_matcher/__version__.py000066400000000000000000000000261475357073500211160ustar00rootroot00000000000000__version__ = "0.6.0" url-matcher-0.6.0/url_matcher/example.py000066400000000000000000000065721475357073500203040ustar00rootroot00000000000000""" Example of usage of the URLMatcher library """ from __future__ import annotations import dataclasses import random import time from collections import Counter from url_matcher import Patterns, URLMatcher from url_matcher.matcher import IncludePatternsWithoutDomainError matcher = URLMatcher() # Let's add a rule for books to scrape product patterns = Patterns(include=["books.toscrape.com/catalogue/"], exclude=["/catalogue/category/"]) matcher.add_or_update("books product", patterns) # Now a rule for product list in books to scrape patterns = Patterns( include=["books.toscrape.com/catalogue/category/", "books.toscrape.com/|", "books.toscrape.com/index.html|"] ) matcher.add_or_update("books productList", patterns) # Let's try it url = "https://books.toscrape.com/catalogue/soumission_998/index.html" assert matcher.match(url) == "books product" url = "https://books.toscrape.com/catalogue/category/books/fiction_10/index.html" assert matcher.match(url) == "books productList" url = "https://amazon.com" assert not matcher.match(url) # Adding a pattern without domain fails try: matcher.add_or_update("won't work", Patterns(["/path"])) raise AssertionError except IncludePatternsWithoutDomainError: ... # But the empty pattern works. It matches anything assert URLMatcher({"Anything": Patterns([""])}).match("http://anything") # Now let's see that priorities are working. They are applied only if several # rules match the URL. patterns = Patterns(["priority.com"]) matcher.add_or_update("low priority", dataclasses.replace(patterns, priority=200)) matcher.add_or_update("high priority", dataclasses.replace(patterns, priority=300)) assert matcher.match("http://priority.com") == "high priority" # Let's invert the priorities matcher.add_or_update("low priority", dataclasses.replace(patterns, priority=300)) matcher.add_or_update("high priority", dataclasses.replace(patterns, priority=200)) assert matcher.match("http://priority.com") == "low priority" # Let's check the speed creating patterns for many domains and matching # urls for these domains. def add_patterns(domain: int) -> None: patterns = Patterns(include=[f"{domain}/catalogue/?param=book"], exclude=["/catalogue/category/"]) matcher.add_or_update(f"{domain} product", patterns) patterns = Patterns(include=[f"{domain}/catalogue/category/?param=book_list", f"{domain}/", f"{domain}/index.html"]) matcher.add_or_update(f"{domain} productList", patterns) N_DOMAINS = 500 N_URLS = 300 URLS = [ "https://books.toscrape.com/catalogue/soumission_998/index.html?param=book&p1=23&p2=45", "https://books.toscrape.com/catalogue/category/books/fiction_10/index.html?param=book_list&p5=23&p6=45", ] # Adding the patterns for idx in range(N_DOMAINS): add_patterns(idx) urls = [] for _ in range(N_URLS): url = random.choice(URLS) domain = random.randint(0, N_DOMAINS - 1) url = url.replace("books.toscrape.com", f"{domain}") urls.append((domain, url)) # Let's try to match the urls start = time.perf_counter() counter: Counter[bool] = Counter() for domain, url in urls: match = matcher.match(url) counter[bool(match)] += 1 assert match and f"{domain}" in match end = time.perf_counter() # It took in my machine ~ 0.04 millis per URL print(f"{((end - start) / N_URLS) * 1000:.3f} milliseconds per URL. Total {end - start} seconds to match {N_URLS} URLs") print("Everything worked fine!") url-matcher-0.6.0/url_matcher/matcher.py000066400000000000000000000215661475357073500202740ustar00rootroot00000000000000""" The matcher module contains the UrlMatcher class. """ from __future__ import annotations from collections.abc import Iterable, Iterator, Mapping from dataclasses import dataclass, field from itertools import chain from typing import Any from url_matcher.patterns import PatternMatcher, get_pattern_domain, hierarchical_str from url_matcher.util import get_domain @dataclass(init=False, frozen=True) class Patterns: include: tuple[str, ...] exclude: tuple[str, ...] priority: int def __init__(self, include: list[str], exclude: list[str] | None = None, priority: int = 500): # The initialization is manually set so that we can support an API of # accepting and returning lists. However, tuples are being used underneath # that class so that the attributes are truly immutable, in addition to # being frozen=True. # Using lists are far less likely to have human typing mistakes compared to # tuples since the trailing `,` char can easily be missed out. For # example: # * ("element") is not the same as ("element",) which is a tuple. # Lastly, the manner of how we set the attribute values below is in line # with how Python's own `dataclasses` library assign attributes to frozen # classes. Here's a reference: # * https://github.com/python/cpython/blob/v3.10.2/Lib/dataclasses.py#L1117-L1120 object.__setattr__(self, "include", tuple(include)) object.__setattr__(self, "exclude", tuple(exclude or [])) object.__setattr__(self, "priority", priority) def get_domains(self) -> list[str]: domains = [get_pattern_domain(pattern) for pattern in self.include] # remove duplicate domains preserving the order return list(dict.fromkeys(domain for domain in domains if domain)) def get_includes_without_domain(self) -> list[str]: return [pattern for pattern in self.include if get_pattern_domain(pattern) is None] def all_includes_have_domain(self) -> bool: """Return true if all the include patterns have a domain""" return not self.get_includes_without_domain() def is_universal_pattern(self) -> bool: """Return true if there are no include patterns or they are empty. A universal pattern matches any domain""" return not any(pattern for pattern in self.include) def get_includes_for(self, domain: str) -> list[str]: return [pattern for pattern in self.include if get_pattern_domain(pattern) == domain] @dataclass class PatternsMatcher: identifier: Any patterns: Patterns include_matchers: list[PatternMatcher] = field(init=False) exclude_matchers: list[PatternMatcher] = field(init=False) def __post_init__(self) -> None: self.include_matchers = [PatternMatcher(pattern) for pattern in self.patterns.include] self.exclude_matchers = [PatternMatcher(pattern) for pattern in self.patterns.exclude] def match(self, url: str) -> bool: if self.include_matchers: for include in self.include_matchers: if include.match(url): break else: return False return not any(exclude.match(url) for exclude in self.exclude_matchers) class IncludePatternsWithoutDomainError(ValueError): def __init__(self, *args: Any, identifier: Any, patterns: Patterns, wrong_patterns: list[str]): super().__init__(*args) self.id = identifier self.patterns = patterns self.wrong_patterns = wrong_patterns class URLMatcher: def __init__(self, data: Mapping[Any, Patterns] | Iterable[tuple[Any, Patterns]] | None = None): """ A class that matches URLs against a list of patterns, returning the identifier of the rule that matched the URL. Example usage:: matcher = URLMatcher() matcher.add_or_update(1, Patterns(include=["example.com/product"])) matcher.add_or_update(2, Patterns(include=["other.com"])) assert matcher.match("http://example.com/product/a_product.html") == 1 assert matcher.match("http://other.com/a_different_page") == 2 :param data: A map or a list of tuples with identifier, patterns pairs to initialize the object from """ self.matchers_by_domain: dict[str, list[PatternsMatcher]] = {} self.matchers_universal: list[PatternsMatcher] = [] self.patterns: dict[Any, Patterns] = {} if data: items = data.items() if isinstance(data, Mapping) else data for identifier, patterns in items: self.add_or_update(identifier, patterns) def add_or_update(self, identifier: Any, patterns: Patterns) -> None: if not patterns.all_includes_have_domain() and not patterns.is_universal_pattern(): wrong_patterns = [p for p in patterns.get_includes_without_domain() if p] raise IncludePatternsWithoutDomainError( f"All include patterns must belong to a domain " f"but the patterns {wrong_patterns} doesn't. " f"For example, the include pattern '/product/* " f"is invalid whereas the pattern 'example.com/product/*' isn't. " f"The only exception is the empty pattern which matches everything " f"and it is allowed. " f"identifier: {identifier}.", identifier=identifier, patterns=patterns, wrong_patterns=wrong_patterns, ) if identifier in self.patterns: self.remove(identifier) self.patterns[identifier] = patterns matcher = PatternsMatcher(identifier, patterns) for domain in patterns.get_domains(): self._add_matcher(domain, matcher) if patterns.is_universal_pattern(): self._add_matcher("", matcher) def remove(self, identifier: Any) -> None: patterns = self.patterns.get(identifier) if not patterns: return del self.patterns[identifier] for domain in patterns.get_domains(): self._del_matcher(domain, identifier) if patterns.is_universal_pattern(): self._del_matcher("", identifier) def get(self, identifier: Any) -> Patterns | None: return self.patterns.get(identifier) def match(self, url: str, *, include_universal: bool = True) -> Any | None: return next(self.match_all(url, include_universal=include_universal), None) def match_all(self, url: str, *, include_universal: bool = True) -> Iterator[Any]: domain = get_domain(url) matchers: Iterable[PatternsMatcher] = self.matchers_by_domain.get(domain) or [] if include_universal: matchers = chain(matchers, self.matchers_universal) for matcher in matchers: if matcher.match(url): yield matcher.identifier def match_universal(self) -> Iterator[Any]: return (m.identifier for m in self.matchers_universal) def _sort_domain(self, domain: str) -> None: """ Sort all the rules within a domain so that the matching can be done in sequence: the first rule matching wins. A total ordering is defined. This is ensured by using including the identifier in the sorting criteria Sorting criteria: * Priority (descending) * Sorted list of includes for this domain (descending) * Rule identifier (descending) """ def sort_key(matcher: PatternsMatcher) -> tuple[int, list[str], Any]: sorted_includes = sorted(map(hierarchical_str, matcher.patterns.get_includes_for(domain))) return (matcher.patterns.priority, sorted_includes, matcher.identifier) self.matchers_by_domain[domain].sort(key=sort_key, reverse=True) self.matchers_universal.sort(key=sort_key, reverse=True) def _del_matcher(self, domain: str, identifier: Any) -> None: matchers = self.matchers_by_domain[domain] for idx in range(len(matchers)): if matchers[idx].identifier == identifier: del matchers[idx] break if not matchers: del self.matchers_by_domain[domain] for idx in range(len(self.matchers_universal)): if self.matchers_universal[idx].identifier == identifier: del self.matchers_universal[idx] break def _add_matcher(self, domain: str, matcher: PatternsMatcher) -> None: # FIXME: This can be made much more efficient if we insert the data directly in order instead of resorting. # The bisect module could be used for this purpose. # I'm leaving it for the future as insertion time is not critical. self.matchers_by_domain.setdefault(domain, []).append(matcher) if domain == "": self.matchers_universal.append(matcher) self._sort_domain(domain) url-matcher-0.6.0/url_matcher/patterns.py000066400000000000000000000223731475357073500205060ustar00rootroot00000000000000""" Utilities to parse patterns and match URLs using them. """ from __future__ import annotations import ipaddress import re import warnings from functools import lru_cache from re import Pattern from typing import NamedTuple from urllib.parse import parse_qs, urlparse from url_matcher.util import get_domain def get_pattern_domain(pattern: str) -> str | None: """ Returns the domain of the pattern if any. >>> get_pattern_domain("") >>> get_pattern_domain("/") >>> get_pattern_domain("dom") 'dom' >>> get_pattern_domain("DOM") 'dom' >>> get_pattern_domain("dom:80") 'dom' >>> get_pattern_domain("http://dom:80") 'dom' >>> get_pattern_domain("http://dom/a_path") 'dom' """ parsed = pattern_parse(pattern) if parsed.netloc: return get_domain(f"//{parsed.netloc}").lower() return None def pattern_to_url(pattern: str) -> str: """ Required for urlparse to recognize the domain in patterns like example.com/path >>> pattern_to_url("example.com/") '//example.com/' >>> pattern_to_url("example.com") '//example.com' >>> pattern_to_url("https://example.com") 'https://example.com' >>> pattern_to_url("MySchema4+.-://example.com") 'MySchema4+.-://example.com' >>> pattern_to_url("//example.com") '////example.com' """ # As defined in https://datatracker.ietf.org/doc/html/rfc3986#section-3.1 has_scheme = re.search(r"^([a-zA-Z][a-zA-Z0-9.+-]*:)?//", pattern) if not has_scheme: pattern = f"//{pattern}" elif pattern.startswith("//"): # This is required because urlparse("//example.com").netloc == "//example.com" # but instead we want it to be parsed into the the path. We achieve it by appending # two more slashes pattern = f"//{pattern}" return pattern class ParseTuple(NamedTuple): scheme: str netloc: str path: str query: str fragment: str @lru_cache(30) def pattern_parse(pattern: str) -> ParseTuple: """ Parses the pattern to a named tuple (scheme, netloc, path, query, fragment) >>> pattern_parse("example.com") ParseTuple(scheme='', netloc='example.com', path='', query='', fragment='') >>> pattern_parse("//example.com/path;this_is_also_path") ParseTuple(scheme='', netloc='', path='//example.com/path;this_is_also_path', query='', fragment='') """ pattern = pattern_to_url(pattern) return _urlparse(pattern) def _urlparse(url: str) -> ParseTuple: """ Returns a named tuple (scheme, netloc, path, query, fragment) where path and params are joined together into path and some other elements are normalized. >>> _urlparse("scheme://example.com/path;params?query=23#fragment") ParseTuple(scheme='scheme', netloc='example.com', path='/path;params', query='query=23', fragment='fragment') >>> _urlparse("http://example.com:80/path") ParseTuple(scheme='http', netloc='example.com', path='/path', query='', fragment='') """ scheme, netloc, path, params, query, fragment = urlparse(url) path = _join_path_and_params(path, params) scheme, netloc = normalize_netloc_and_schema(scheme, netloc) return ParseTuple(scheme, netloc, path, query, fragment) def _wildcard_re_escape(text: str) -> str: return re.escape(text).replace("\\*", ".*") def _join_path_and_params(path: str, params: str) -> str: if params: return f"{path};{params}" return path def normalize_netloc_and_schema(schema: str, netloc: str) -> tuple[str, str]: """ Removes 80 or 443 port when obvious. Deduces http or https when the port is provided >>> normalize_netloc_and_schema("http", "example.com:80") ('http', 'example.com') >>> normalize_netloc_and_schema("http", "example.com:80") ('http', 'example.com') >>> normalize_netloc_and_schema("http", "example.com:443") ('http', 'example.com:443') >>> normalize_netloc_and_schema("https", "example.com:443") ('https', 'example.com') >>> normalize_netloc_and_schema("", "example.com:80") ('http', 'example.com') >>> normalize_netloc_and_schema("", "example.com:443") ('https', 'example.com') """ schema = schema.lower() domain, port = split_domain_port(netloc) if (port == "80" and schema in ("http", "")) or (port == "443" and schema in ("https", "")): return "http" if port == "80" else "https", domain return schema, netloc def hierarchical_str(pattern: str) -> str: """ Rewrites the given pattern in a string that is useful to sort patterns from more general to more concrete. For example, the pattern "example.com" is more general than "blog.example.com" which is more general than "blog.example.com/post/1" >>> hierarchical_str("http://blog.example.com/path?query=23#fragment") 'com.example.blog/pathquery=23fragment' >>> hierarchical_str("http://blog.example.com:1234") 'com.example.blog' >>> hierarchical_str("http://127.0.0.1:80/path") '127.0.0.1/path' """ parsed = pattern_parse(pattern) netloc = parsed.netloc if ":" in parsed.netloc: netloc, _ = split_domain_port(parsed.netloc) try: ipaddress.ip_address(netloc) is_ip = True except ValueError: is_ip = False if not is_ip: # Reversing the domain so that higher levels are before # e.g. blog.example.com -> com.example.blog netloc = ".".join(reversed(netloc.split("."))) return "".join((netloc, *parsed[2:])) def split_domain_port(netloc: str) -> tuple[str, str | None]: """ Splits the netloc into domain and port. >>> split_domain_port("example.com") ('example.com', None) >>> split_domain_port("example.com:80") ('example.com', '80') """ segments = netloc.split(":") if len(segments) > 1: return ":".join(segments[:-1]), segments[-1] return netloc, None class PatternMatcher: def __init__(self, pattern: str): # Parsing and validation self.pattern = pattern self.parsed = pattern_parse(pattern) self.domain = get_pattern_domain(pattern) self.netloc_re: Pattern[str] | None = None self.path_re: Pattern[str] | None = None self.fragment_re: Pattern[str] | None = None self.query_re_dict: dict[str, Pattern[str]] | None = None self._build_regexes() def _build_regexes(self) -> None: """ Builds the compiled regexes that can be used to match the pattern. """ pscheme, pnetloc, ppath, pquery, pfragment = self.parsed if pnetloc: netloc_re = re.escape(pnetloc) if not any((ppath, pquery, pfragment)): # Also match subdomains if there is no path, query or fragment in the pattern netloc_re = rf"(?:.*\.)?{netloc_re}" netloc_re = f"^(?:www.)?{netloc_re}$" self.netloc_re = re.compile(netloc_re, re.IGNORECASE) if ppath: self.path_re = self._path_or_fragment_re(ppath) if pfragment: self.fragment_re = self._path_or_fragment_re(pfragment) if pquery: pkvs = parse_qs(pquery, keep_blank_values=True) query_re_dict = {} for pparam, values in pkvs.items(): pparam = pparam.lower() # noqa: PLW2901 if "*" in pparam: warnings.warn( f"Wildcard expansion is only allowed for the values in the query parameter. Pattern: '{self.pattern}'", SyntaxWarning, stacklevel=3, ) pparam = pparam.replace("*", "") # noqa: PLW2901 if not pparam: continue param_re = rf"^(?:{'|'.join([_wildcard_re_escape(value) for value in values])})$" query_re_dict[pparam] = re.compile(param_re, re.IGNORECASE) self.query_re_dict = query_re_dict or None def match(self, url: str) -> bool: """ Return True if the url matches the pattern. """ parsed = _urlparse(url) if self.parsed.scheme and parsed.scheme != self.parsed.scheme: return False if self.netloc_re and not self.netloc_re.match(parsed.netloc): return False if self.path_re and not self.path_re.match(parsed.path): return False if self.fragment_re and not self.fragment_re.match(parsed.fragment): return False if self.query_re_dict: kvs = parse_qs(parsed.query, keep_blank_values=True) kvs = {k.lower(): v for k, v in kvs.items()} # All params must be present in the URL for param, param_re in self.query_re_dict.items(): if param not in kvs: return False if not any(param_re.match(value) for value in (kvs[param])): return False return True @staticmethod def _path_or_fragment_re(path_or_fragment: str) -> Pattern[str]: """Wildcard expansion + end of line character""" re_str = _wildcard_re_escape(path_or_fragment) if re_str.endswith(r"\|"): # case where the match must be exact re_str = re_str[:-2] else: re_str += r".*" re_str = rf"^{re_str}$" return re.compile(re_str, re.IGNORECASE) url-matcher-0.6.0/url_matcher/py.typed000066400000000000000000000000001475357073500177520ustar00rootroot00000000000000url-matcher-0.6.0/url_matcher/util.py000066400000000000000000000012641475357073500176170ustar00rootroot00000000000000from __future__ import annotations from functools import lru_cache from urllib.parse import urlparse from tldextract import tldextract @lru_cache(100) def get_domain(url: str) -> str: """ Return the domain without any subdomain >>> get_domain("http://blog.example.com") 'example.com' >>> get_domain("http://www.example.com") 'example.com' >>> get_domain("http://deeper.blog.example.co.uk") 'example.co.uk' >>> get_domain("http://127.0.0.1") '127.0.0.1' """ parts = tldextract.extract(url) return ".".join(part for part in (parts.domain, parts.suffix) if part) def is_absolute(url: str) -> bool: return bool(urlparse(url).netloc)