pax_global_header00006660000000000000000000000064146156045530014523gustar00rootroot0000000000000052 comment=b373f384b132fd573a8723009a7c365a436509a9 scooby-0.10.0/000077500000000000000000000000001461560455300130775ustar00rootroot00000000000000scooby-0.10.0/.flake8000066400000000000000000000010521461560455300142500ustar00rootroot00000000000000[flake8] exclude = .git,__pycache__,build,dist ignore = # whitespace before ':' E203, # line break before binary operator W503, # line length too long E501, # do not assign a lambda expression, use a def E731, # too many leading '#' for block comment E266, # ambiguous variable name E741, # module level import not at top of file E402, # Quotes (temporary) Q0, # bare excepts (temporary) B001, E722, # Ignore "black" formatting errors, since black is already checked BLK100 scooby-0.10.0/.github/000077500000000000000000000000001461560455300144375ustar00rootroot00000000000000scooby-0.10.0/.github/workflows/000077500000000000000000000000001461560455300164745ustar00rootroot00000000000000scooby-0.10.0/.github/workflows/lint.yml000066400000000000000000000004041461560455300201630ustar00rootroot00000000000000name: Linting on: pull_request: push: branches: - main workflow_dispatch: jobs: stylecheck: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 - uses: pre-commit/action@v3.0.0 scooby-0.10.0/.github/workflows/pythonpackage.yml000066400000000000000000000026551461560455300220640ustar00rootroot00000000000000# 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: Tests on: push: tags: - "*" branches: - main pull_request: workflow_dispatch: schedule: - cron: "0 0 1 * *" jobs: test: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: python-version: [3.8, 3.9, "3.10", "3.11"] os: [ubuntu-latest, macOS-latest, windows-latest] steps: - name: Checkout uses: actions/checkout@v2 with: fetch-depth: 100 persist-credentials: false - name: Fetch git tags run: git fetch origin 'refs/tags/*:refs/tags/*' - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 with: python-version: ${{ matrix.python-version }} - name: Build Wheel and Install run: pip install -e . - name: Test Report Generation with No Dependencies run: python -c "import scooby;scooby.doo();" - name: Install testing requirements run: pip install -r requirements_test.txt - name: Test Entire API run: make apitest - name: Doctests run: make doctest - uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml verbose: true scooby-0.10.0/.github/workflows/release.yml000066400000000000000000000021301461560455300206330ustar00rootroot00000000000000name: Package Release on: push: tags: "*" release: types: [released] jobs: publish: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 with: fetch-depth: 100 persist-credentials: false - name: Fetch git tags run: git fetch origin 'refs/tags/*:refs/tags/*' - name: Set up Python uses: actions/setup-python@v2 with: python-version: "3.9" - name: Install dependencies run: | python -m pip install --upgrade pip pip install setuptools setuptools_scm wheel twine - name: Build and Install Wheel run: | python setup.py bdist_wheel pip install dist/scooby*.whl - name: Test Report Generation with No Dependencies run: python -c "import scooby;scooby.Report();" - name: Build and Publish to PyPI env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} run: | python setup.py sdist twine upload --skip-existing dist/scooby* scooby-0.10.0/.gitignore000066400000000000000000000006341461560455300150720ustar00rootroot00000000000000# Compiled source # ################### *.pyc *.pyd *.so *.o # Pip generated folders # ######################### *.egg-info/ build/ dist/ scooby/version.py # Mac .DS_Store # testing coverage and cache .pytest_cache/ tests/.coverage tests/htmlcov/ tests/dummy_module/ # windows test.bat # Jupyter notebook cache **/.ipynb_checkpoints # Code coverage results htmlcov .coverage coverage.xml *,cover .tox venvscooby-0.10.0/.pre-commit-config.yaml000066400000000000000000000015761461560455300173710ustar00rootroot00000000000000repos: - repo: https://github.com/psf/black rev: 22.3.0 hooks: - id: black - repo: https://github.com/pycqa/isort rev: 5.12.0 hooks: - id: isort - repo: https://github.com/PyCQA/flake8 rev: 3.9.2 hooks: - id: flake8 additional_dependencies: [ "flake8-black==0.3.2", "flake8-isort==4.1.1", "flake8-quotes==3.3.1", ] - repo: https://github.com/codespell-project/codespell rev: v2.1.0 hooks: - id: codespell args: [ "doc examples examples_flask pyvista tests", "*.py *.rst *.md", ] - repo: https://github.com/pycqa/pydocstyle rev: 6.1.1 hooks: - id: pydocstyle additional_dependencies: [toml==0.10.2] files: ^scooby/ - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.3.0 hooks: - id: check-merge-conflict - id: debug-statements # - id: no-commit-to-branch # args: [--branch, main] scooby-0.10.0/LICENSE000066400000000000000000000021041461560455300141010ustar00rootroot00000000000000MIT License Copyright (c) 2019 Dieter Werthmüller & Bane Sullivan Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. scooby-0.10.0/MANIFEST.in000066400000000000000000000004201461560455300146310ustar00rootroot00000000000000prune assets prune tests prune .github exclude codecov.yml exclude Makefile exclude MANIFEST.in exclude pyproject.toml exclude requirements_style.txt exclude requirements_test.txt exclude requirements.txt exclude .flake8 exclude .gitignore exclude .pre-commit-config.yaml scooby-0.10.0/Makefile000066400000000000000000000016761461560455300145510ustar00rootroot00000000000000# Simple makefile to simplify repetitive build env management tasks under posix CODESPELL_DIRS ?= ./ CODESPELL_SKIP ?= "*.pyc,*.txt,*.gif,*.png,*.jpg,*.ply,*.vtk,*.vti,*.js,*.html,*.doctree,*.ttf,*.woff,*.woff2,*.eot,*.mp4,*.inv,*.pickle,*.ipynb,flycheck*,./.git/*,./.hypothesis/*,*.yml,./doc/_build/*,./doc/images/*,./dist/*,*~,.hypothesis*,./doc/examples/*,*.mypy_cache/*,*cover,./tests/tinypages/_build/*,*/_autosummary/*" stylecheck: codespell lint test: apitest doctest codespell: @echo "Running codespell" @codespell $(CODESPELL_DIRS) -S $(CODESPELL_SKIP) pydocstyle: @echo "Running pydocstyle" @pydocstyle scooby --match='(?!coverage).*.py' lint: @echo "Linting with flake8" flake8 --ignore=E501,W503,D10,E123,E203 scooby tests doctest: @echo "Running module doctesting" pytest -v --doctest-modules scooby apitest: @echo "Running full API tests" pytest -v --cov scooby --cov-report xml format: @echo "Formatting" black . isort . scooby-0.10.0/README.md000066400000000000000000000345371461560455300143720ustar00rootroot00000000000000# 🐶🕵️ Scooby [![Downloads](https://img.shields.io/pypi/dm/scooby.svg?label=PyPI%20downloads)](https://pypi.org/project/scooby/) [![Tests](https://github.com/banesullivan/scooby/actions/workflows/pythonpackage.yml/badge.svg)](https://github.com/banesullivan/scooby/actions/workflows/pythonpackage.yml) [![PyPI Status](https://img.shields.io/pypi/v/scooby.svg?logo=python&logoColor=white)](https://pypi.org/project/scooby/) [![Conda Status](https://img.shields.io/conda/vn/conda-forge/scooby.svg)](https://anaconda.org/conda-forge/scooby) [![codecov](https://codecov.io/gh/banesullivan/scooby/branch/main/graph/badge.svg?token=eJqZ700tqH)](https://codecov.io/gh/banesullivan/scooby) *Great Dane turned Python environment detective* This is a lightweight tool for easily reporting your Python environment's package versions and hardware resources. Install from [PyPI](https://pypi.org/project/scooby/) ```bash pip install scooby ``` or from [conda-forge](https://anaconda.org/conda-forge/scooby/) ```bash conda install -c conda-forge scooby ``` ![Jupyter Notebook Formatting](https://github.com/banesullivan/scooby/raw/main/assets/jupyter.png) Scooby has HTML formatting for Jupyter notebooks and rich text formatting for just about every other environment. We designed this module to be lightweight such that it could easily be added as a dependency to Python projects for environment reporting when debugging. Simply add scooby to your dependencies and implement a function to have scooby report on the aspects of the environment you care most about. If scooby is unable to detect aspects of an environment that you'd like to know, please share this with us as a feature requests or pull requests. The scooby reporting is derived from the versioning-scripts created by [Dieter Werthmüller](https://github.com/prisae) for [empymod](https://empymod.github.io), [emg3d](https://empymod.github.io), and the [SimPEG](https://github.com/simpeg/) framework. It was heavily inspired by `ipynbtools.py` from [qutip](https://github.com/qutip) and [`watermark.py`](https://github.com/rasbt/watermark). This package has been altered to create a lightweight implementation so that it can easily be used as an environment reporting tool in any Python library with minimal impact. ## Usage ### Generating Reports Reports are rendered as html-tables in Jupyter notebooks as shown in the screenshot above, and otherwise as plain text lists. If you do not output the `Report` object either at the end of a notebook cell or it is generated somewhere in a vanilla Python script, you may have to print the `Report` object: `print(scooby.Report())`, but note that this will only output the plain text representation of the script. ```py >>> import scooby >>> scooby.Report() ``` ``` -------------------------------------------------------------------------------- Date: Wed Feb 12 15:35:43 2020 W. Europe Standard Time OS : Windows CPU(s) : 16 Machine : AMD64 Architecture : 64bit RAM : 31.9 GiB Environment : IPython Python 3.7.6 | packaged by conda-forge | (default, Jan 7 2020, 21:48:41) [MSC v.1916 64 bit (AMD64)] numpy : 1.18.1 scipy : 1.3.1 IPython : 7.12.0 matplotlib : 3.0.3 scooby : 0.5.0 Intel(R) Math Kernel Library Version 2019.0.4 Product Build 20190411 for Intel(R) 64 architecture applications -------------------------------------------------------------------------------- ``` For all the Scooby-Doo fans out there, `doo` is an alias for `Report` so you can oh-so satisfyingly do: ```py >>> import scooby >>> scooby.doo() ``` ``` -------------------------------------------------------------------------------- Date: Thu Nov 25 09:47:50 2021 MST OS : Darwin CPU(s) : 12 Machine : x86_64 Architecture : 64bit RAM : 32.0 GiB Environment : Python File system : apfs Python 3.8.12 | packaged by conda-forge | (default, Oct 12 2021, 21:50:38) [Clang 11.1.0 ] numpy : 1.21.4 scipy : 1.7.3 IPython : 7.29.0 matplotlib : 3.5.0 scooby : 0.5.8 -------------------------------------------------------------------------------- ``` Or better yet: ```py from scooby import doo as doobiedoo ``` On top of the default (optional) packages you can provide additional packages, either as strings or give already imported packages: ```py >>> import pyvista >>> import scooby >>> scooby.Report(additional=[pyvista, 'vtk', 'no_version', 'does_not_exist']) ``` ``` -------------------------------------------------------------------------------- Date: Wed Feb 12 16:15:15 2020 W. Europe Standard Time OS : Windows CPU(s) : 16 Machine : AMD64 Architecture : 64bit RAM : 31.9 GiB Environment : IPython Python 3.7.6 | packaged by conda-forge | (default, Jan 7 2020, 21:48:41) [MSC v.1916 64 bit (AMD64)] pyvista : 0.23.1 vtk : 8.1.2 no_version : Version unknown does_not_exist : Could not import numpy : 1.18.1 scipy : 1.3.1 IPython : 7.12.0 matplotlib : 3.0.3 scooby : 0.5.0 Intel(R) Math Kernel Library Version 2019.0.4 Product Build 20190411 for Intel(R) 64 architecture applications -------------------------------------------------------------------------------- ``` Furthermore, scooby reports if a package could not be imported or if the version of a package could not be determined. Other useful parameters are - `ncol`: number of columns in the html-table; - `text_width`: text width of the plain-text version; - `sort`: list is sorted alphabetically if True. Besides `additional` there are two more lists, `core` and `optional`, which can be used to provide package names. However, they are mostly useful for package maintainers wanting to use scooby to create their reporting system (see below). ### Implementing scooby in your project You can easily generate a custom `Report` instance using scooby within your project: ```py class Report(scooby.Report): def __init__(self, additional=None, ncol=3, text_width=80, sort=False): """Initiate a scooby.Report instance.""" # Mandatory packages. core = ['yourpackage', 'your_core_packages', 'e.g.', 'numpy', 'scooby'] # Optional packages. optional = ['your_optional_packages', 'e.g.', 'matplotlib'] scooby.Report.__init__(self, additional=additional, core=core, optional=optional, ncol=ncol, text_width=text_width, sort=sort) ``` This makes it particularly easy for a user of your project to quickly generate a report on all of the relevant package versions and environment details when sumbitting a bug. ```py >>> import your_package >>> your_package.Report() ``` The packages on the `core`-list are the mandatory ones for your project, while the `optional`-list can be used for optional packages. Keep the `additional`-list free to allow your users to add packages to the list. #### Implementing as a soft dependency If you would like to implement scooby, but are hesitant to add another dependency to your package, here is an easy way how you can use scooby as a soft dependency. Instead of `import scooby` use the following snippet: ```py # Make scooby a soft dependency: try: from scooby import Report as ScoobyReport except ImportError: class ScoobyReport: def __init__(self, *args, **kwargs): message = ( '\n *ERROR*: `Report` requires `scooby`.' '\n Install it via `pip install scooby` or' '\n `conda install -c conda-forge scooby`.\n' ) raise ImportError(message) ``` and then create your own `Report` class same as above, ```py class Report(ScoobyReport): def __init__(self, additional=None, ncol=3, text_width=80, sort=False): """Initiate a scooby.Report instance.""" # Mandatory packages. core = ['yourpackage', 'your_core_packages', 'e.g.', 'numpy', 'scooby'] # Optional packages. optional = ['your_optional_packages', 'e.g.', 'matplotlib'] scooby.Report.__init__(self, additional=additional, core=core, optional=optional, ncol=ncol, text_width=text_width, sort=sort) ``` If a user has scooby installed, all works as expected. If scooby is not installed, it will raise the following exception: ```py >>> import your_package >>> your_package.Report() *ERROR*: `Report` requires `scooby` Install it via `pip install scooby` or `conda install -c conda-forge scooby`. ``` ### Autogenerate Reports for any Packages Scooby can automatically generate a Report for any package and its distribution requirements with the `AutoReport` class: ```py >>> import scooby >>> scooby.AutoReport('matplotlib') ``` ``` -------------------------------------------------------------------------------- Date: Fri Oct 20 16:49:34 2023 PDT OS : Darwin CPU(s) : 8 Machine : arm64 Architecture : 64bit RAM : 16.0 GiB Environment : Python File system : apfs Python 3.11.3 | packaged by conda-forge | (main, Apr 6 2023, 08:58:31) [Clang 14.0.6 ] matplotlib : 3.7.1 contourpy : 1.0.7 cycler : 0.11.0 fonttools : 4.39.4 kiwisolver : 1.4.4 numpy : 1.24.3 packaging : 23.1 pillow : 9.5.0 pyparsing : 3.0.9 python-dateutil : 2.8.2 -------------------------------------------------------------------------------- ``` ### Solving Mysteries Are you struggling with the mystery of whether or not code is being executed in IPython, Jupyter, or normal Python? Try using some of scooby's investigative functions to solve these kinds of mysteries: ```py import scooby if scooby.in_ipykernel(): # Do Jupyter/IPyKernel stuff elif scooby.in_ipython(): # Do IPython stuff else: # Do normal, boring Python stuff ``` ### How does scooby get version numbers? A couple of locations are checked, and we are happy to implement more if needed, just open an issue! Currently, it looks in the following places: - `__version__` - `version` - lookup `VERSION_ATTRIBUTES` in the scooby knowledge base - lookup `VERSION_METHODS` in the scooby knowledge base `VERSION_ATTRIBUTES` is a dictionary of attributes for known python packages with a non-standard place for the version. You can add other known places via: ```py scooby.knowledge.VERSION_ATTRIBUTES['a_module'] = 'Awesome_version_location' ``` Similarly, `VERSION_METHODS` is a dictionary for methods to retrieve the version, and you can similarly add your methods which will get the version of a package. ### Using scooby to get version information. If you are only interested in the version of a single package then you can use scooby as well. A few examples: ```py >>> import scooby, numpy >>> scooby.get_version(numpy) ('numpy', '1.16.4') >>> scooby.get_version('no_version') ('no_version', 'Version unknown') >>> scooby.get_version('does_not_exist') ('does_not_exist', 'Could not import') ``` Note that modules can be provided as already loaded ones or as strings. ### Tracking Imports in a Session Scooby has the ability to track all imported modules during a Python session such that *any* imported, non-standard lib package that is used in the session is reported by a `TrackedReport`. For instance, start a session by importing scooby and enabling tracking with the `track_imports()` function. Then *all* subsequent packages that are imported during the session will be tracked and scooby can report their versions. Once you are ready to generate a `Report`, instantiate a `TrackedReport` object. In the following example, we import a constant from `scipy` which will report the versions of `scipy` and `numpy` as both packages are loaded in the session (note that `numpy` is internally loaded by `scipy`). ```py >>> import scooby >>> scooby.track_imports() >>> from scipy.constants import mu_0 # a float value >>> scooby.TrackedReport() ``` ``` -------------------------------------------------------------------------------- Date: Thu Apr 16 15:33:11 2020 MDT OS : Linux CPU(s) : 8 Machine : x86_64 Architecture : 64bit RAM : 62.7 GiB Environment : IPython Python 3.7.7 (default, Mar 10 2020, 15:16:38) [GCC 7.5.0] scooby : 0.5.2 numpy : 1.18.1 scipy : 1.4.1 -------------------------------------------------------------------------------- ``` ## Command-Line Interface Scooby comes with a command-line interface. Simply typing ```bash scooby ``` in a terminal will display the default report. You can also use the CLI to show the scooby Report of another package if that package has implemented a Report class as suggested above, using `packagename.Report()`. As an example, to print the report of pyvista you can run ```bash scooby -r pyvista ``` which will show the Report implemented in PyVista. The CLI can also generate a report based on the dependencies of a package's distribution where that package hasn't implemented a Report class. For example, we can generate a Report for `matplotlib` and its dependencies: ```bash $ scooby -r matplotlib -------------------------------------------------------------------------------- Date: Fri Oct 20 17:03:45 2023 PDT OS : Darwin CPU(s) : 8 Machine : arm64 Architecture : 64bit RAM : 16.0 GiB Environment : Python File system : apfs Python 3.11.3 | packaged by conda-forge | (main, Apr 6 2023, 08:58:31) [Clang 14.0.6 ] matplotlib : 3.7.1 contourpy : 1.0.7 cycler : 0.11.0 fonttools : 4.39.4 kiwisolver : 1.4.4 numpy : 1.24.3 packaging : 23.1 pillow : 9.5.0 pyparsing : 3.0.9 python-dateutil : 2.8.2 importlib-resources : 5.12.0 -------------------------------------------------------------------------------- ``` Simply type ```bash scooby --help ``` to see all the possibilities. ## Optional Requirements The following is a list of optional requirements and their purpose: - `psutil`: report total RAM in GiB - `mkl-services`: report Intel(R) Math Kernel Library version scooby-0.10.0/assets/000077500000000000000000000000001461560455300144015ustar00rootroot00000000000000scooby-0.10.0/assets/jupyter.png000066400000000000000000004774641461560455300166370ustar00rootroot00000000000000PNG  IHDR Z pHYsod OiCCPPhotoshop ICC profilexڝSgTS=BKKoR RB&*! J!QEEȠQ, !{kּ> H3Q5 B.@ $pd!s#~<<+"x M0B\t8K@zB@F&S`cbP-`'{[! eDh;VEX0fK9-0IWfH  0Q){`##xFW<+*x<$9E[-qWW.(I+6aa@.y24x6_-"bbϫp@t~,/;m%h^ uf@Wp~<5j>{-]cK'Xto(hw?G%fIq^D$.Tʳ?D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9(xoʿܔĹdff-[n ڴ VE/(ۻCɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG499?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3- cHRMz%u0`:o_Ft_IDATxwlWY2sk* TTbADJiCbW J/J @s)3*?s{onPԄ'ɹ3v{ֳ"1{]$IsT@=zѣGW Bl{ro'o8=!4d=zѣG+:p@M ܜJ)QJ!9GUU}ѣG=z\Aэ{+A$ۣG=zqF7:݆ݚ+`E1'Z~A=zѣRʹ!@km==_|1Ʉ,=zѣG-s9e<@PkE)t:eϞ=$I'=zѣGW08r饗gum1"Ioh r yr+ilvh*ebݶ9!`ccc=v D\_lǃ,$ 1_Xm%= wٖ(?hח^n;w?/6]y- 2-MWXn?:rs[w-f 7!XhK?߆CⷬX=zѣGքL&[H.l ]&a%D_7 ~WHvna[OBT7?Ƕڸ]{8< 3hJ#B qH# 2GW#48#D4GIO < xvնq=/GzrfN WqCc4HtA!i4JH ݜ < {qGdc|\' ~T^X <Ȁ"2ZH\E, B{$vmv.dQG=z{Rj.r(e9:qN|CHH{FPU!ܷ݇ua"*ċr "o [6 Nq^T! GuR(pΑ$ Jvd{""A#,AjjpVœ 2J˥ЩQsռ;ym "JBk  In/fC5 ӼŅ T"NH:< Q? C4%Ӎ IH3dTC*Cs4\v8.u$wbO%>a m@&!l+GY(\8! :#d$*|K=!H WpmPCӔTUM1&mG<<At\P7^tt=zѣenrl鍪V(!kK04 ҥX$݄@ !d$F!Bt3cu$ cm>uDFh{V.xyP48Jޑg1CQ1Lć@KAXT^hn^.kOjIr-nd1rUl_)dȒFz4ڷ*da2ZG7'۩YrBH ZB [fEtSP.Dљ<3it2C*'8b{y;>AG:G nѣG=z8 g+B$F$f94B+!lqdpܿo.wmS_-A 6A;FG?1W~|?Ί @m:Ej'uTir`3yҟ>fʬ)A"#+9;bgBJFhWJo5ZNiZxX N4fTLeEelXU^߹ɮ;/je\ Q>Z5D;I(ƨO}o*]*2,Ā7% =zѣG&@"JE%?Rsp;wݟʯTJh$ [n![Hpʲ`88}oVyõUNw!:X)z+9#^'EhhŁ6bm'h1gMM6HSt&i҆/ATjBbkG4B }sh(3d-$MKTrF*Dzzcp"XA7!5 -JgƋ:aQFB}=R47в$BS:E%HaFKtʰu#`Ik<5&U2o$:`D%ӪA fL9Q  pU {ѣG884lo'Fq2صu wi/oy6'@N24;`@ sdRE#ܚ71Yx*#uDo駟S|\蔺D/q-v!*ξ%vί~),YQ 4C&D 45/z x̽^ՋprO7<_y7`.yoy2wwmo݂X7 \<uwpχ*$uc{<1/y8!y{ws0LyⳞS$po^/sـqNHAB1{5n}g~rpXVx ,\K/! a{9k4R6.$o.L ؽgw篱17۳㖏/~Mh~F7s$o4^ors{k=zѣG#ޘ媍jZ!8Ѕ,א^:d2SGذU=*kiI!775İ>]G6Djx*R%<2(/¢mKm=Cԧ׽_- #*7%9>/ ^/|s~md%}o䴧?||_F'c9 xśUpyƬ/!nr"BR )}5t4LJ*6NswSdU~#q%id.#kv#!g=8M9+_g%y<+\v}Sbk0M{=Nn!'r$qLLY?-pE+=NT'u֬wq)*">>mϟFT>*SXϟ><ǯ܊fp77  |CfԈ[K_(9~Oou RGC?0O ع{R3L0k6r=zѣH/[VBFch5FK-uBisFpu_G:t*o=m .`.U*GQHNfk Ic^eKd^%IB c:֑gCc'%KوbYf7i 0  {QHv.-s'RۊfuդiNz0H<⁏M;1NmJj(=Ý$RlAؘl*D ͣnpÛrqdzG?Ng?-H5^BeY XH,fP !<rw\i$9dz9']D45/E=zޖ%/ jGiJT{,-(kKn[SQΏ椱oDL *Zt m& )Љ2tKP-ۉ `|Hp#- 8p+ PV%L"H (P !&COb,M  Ɠ|ӟs?82!0.B) &L(ŒO~LL={gYwhA.8z aw urHkz7MokR"*GT"HѣG=z8*-mAadYk CEy` )ꊁ ;@[2Ίb 0T{8`iCŘ`ɰܠ,׈v2^'JX?p=- ״)o=Il)]$/4"hd N8L(lk+ dh$c !Y} hG#烟{W?;⮜|ݛ0aY}9kpWgGv әc % F99l'< u* Pي47OtۦK}=kj@s^P4r'x].8{xHzh4 xCBc!}W=±'+_ފJj9^z̧r(( /׾û?Qn~_ĕUmWʗċ_xc:T%FHSѡӥm IآG=zن^$r 0-$!xyģFD6%72Lig` M@(An "V>j ?O}Nx&sN}㐉5.׸ umI 7RH63M&cXy*fTg2& ʼn)xgwei;x0'x5^Khv֊ P)Rg2B-L폸#]dj|xs_C$˵y2g ͘bJH(|85=)޼ͯ^ GO~!;z^R4Ԫ-n ~OsWߍ N fMScQ0Kq![^zIwm>"("Qm66e$i'ipu,XbXm$xr`tՀ9ځ Fh9d( dRc VXnF,KXQnV4SfZVH‘eu]t%$; uB1ڄ2(㌱!ZK ڤ$i i`=HKRꂢq ufEA* EOP⽧kU0l̀(e('l`RLعk<pUD/fJ]4'آͨ/}P!!EFAr[uv^ѣG=~f{;c1.ȓ$pE3WzDSIm/c :hDkC$] _&5{I|#@":ĠXZjm$TIP Umb$Uѐ#PjA$3\v.TM6DLKK/126[(qrP"Jvȕ"Zb_ڰt٬!xOQx<9H(ʢf2U=CIŞeѠy|ci@ "|cV* TՔA`JP)*)xa5OѨX,:Gtp"7qm I.6ۣG=z#;7ɐ&)Mu&c!P!<{,5PcD 2";CБ, 'fIT1脐Qm+UJDH!AoTȢ%HtZ} "**-ґhAP40J͗jˎ]+X')X$#K4j'gdY6̜mR4u6)F+C|~> 8$*EyH&P5%4xmE3mo8N` GSՈ6^(O@K[aF5eYDD) ZHɀ34mZ蘯:hTJ(OȂ +* zdzѣG]$[TH2 w(D%zcCS =rQL!/ȅ},/Kɻ7C:Q8$K)%ܣG=zB|&bpJ6- GnC<z|8FޭĘmL[>`paLJa Ȋ:THPƠe14c$uNSKJN:v8DljQj`S6%rx҂(8G$cXFtJhw()<eUQfL'Ì':IJPfDhCDOxo}̪^r>/a(̒iMy)56rCP V Nئ&%<C)j *Al#(`'3P6C05ԍ rjkz_?uk] S`83)7[3a6H=F%OѣG=zbOy2)|K ;R6lBmPsCMK458gL6xg4p4d9U*Wᜃ}oGFKK9bc>8 /xK2{wfh4eE/0+KTbZ4?׿{_=طQLiOz"o|AI&EW<9lǸ!Ԗٹ 4.+qIˌK±|ﱴs4%o/25A?w&ʏHTunt wbۯTpEs^KL9F7g%r;ܕI]s:7>:w,Wq55Å0Y@\~x>ns )ؕb{%KW)C VGG=z q;:Nuο5W_;pӞpɏޜ=8 ؚ*^v6Wnԕ#1^p ]~f^v65f(jҰs*+{5˯|kbnw~ 1R<,w}l? {Gqя ?~'1#gR;(8]7坧p3~|㼋ҧ?-qO~Ǭsp9Lb3>7:U8"!!؆-=HSojBѣG="J'`-ܧ_\> #[BpjCIk:^y;c00O(h)5/}32ҒŜӞO{je=Wz~Ĺ:syxCξ!Oyħ>ȇH$1澿y7c7^z!_=Llȗzu"f`\0/p;܌cv759\N f~cgpM#n 'sݗo|kk4{?IF79~.򝯟>~Gbg⌯~KA" U1_q 9|)LVAyCf1l6ae`Xs&cVwy) RM-i#45c0vw 5$DQ$=zCz`+ؒnMwE0 ۾U7/2zohV"..R]կ S> B=q?x+^2d\|ޅ{CThK*9;p]``0zY&.E`%_}QXiѰ+|U@Տ?w}k^\uN.CS֭ d>v)bTG=zOz2?}{Y*71-8+ /+l2 \F=|ۛ||=OS5OJaP^m`Qv]ZWnz#x 5uoGr -2Uv(l\zDYpfk {yCOȀ4 /x#Ş :Jd 9 svpя'>AOw䴧swKS ̀A0( 58%Sq_^wĵI9oAT+ի^s^,+P Ee[*0`1aww`0h `ꜹ\@3*|DU"\(\ {YbD#ji&GѣG==dSɕ3<qH˵˄ K](yO# e1CG'?ܽQ>fǮݼRk#h~wdݬ x׽ P//fNL:Ƅ'>*H׸/݌ ei^rޚ\BQZ2w]ȇ?dԳ=zIQnLC!$ 5>M5/|3y#…\ >Fٹ ':K@1gojm|_NJF+˿zw"eEjFMB@HT2fc/{s/^;0LV52hrI?w~x8ĄY tjiD{|Gq'blM2 #7ѣGWv\V^.kU?-:kgpZ9ՆtJ>Q;K <&qMAJ &eV$YpBC%Ɠd9ddAnrLJ5o@Ǩ M2[}?~7l2,P%iMrF:SkCA^GergSрYȇul(.=|^ŮtӠfV1YFaJ3-qfI MMDؘMOh:Ёƭ"e?.zaX ,Jqk[ YQZ ,Ih#6ѣG=8묳8S"YRneeD]^\%IB7`Sz<$4MReY*RH)ef uq>TUpDY54֣=A65i&6U}yS$Ba*R\NQK&4,iMXDjHr*0Bk49GA)eU0al@) qx JLgjl$B3_rT-E$Ż8y9xyl6E(GQW R06%pF * |\N=nHDg hj AC=z-oFk{OX,AkIRB)RJBF#Tv-`b6ai8bReLY=3jgY2!,k 憵rk]v ^Ws&aJd&̰>`' J1)e2RuBdib8Hqf1CP4Cb:5LghjF i)fS@gZNQ) F9e]4ioWP3U u qjnݧkeAfe8AnGpe(I+ .d_=zѣ2 k ZH!Ij|.$bFoHfeIT*', %$$ţA'8Α'):LPE94kjCL˒ [O&I>؅0,u$EOS@#`-B#C BE%JFR|Cf2R5Z?H:4x.CI.  j[`ú 2HLPh&,1MPBkf'Mr, ^4hcYj7".NH5cLd.VuTEЇ-ѣG= ml p!D 8|n] i9$߿=!,9ND(2 UU9y`"MA4ayK %ٌ9HIF'5K!R*\e--#8)c̤)R!(k׸J>cYYI5ZT$:Fz7y:ػc'h@X"t ֣G=z*0GDRu4e1up3/eUUB@JIYW4AsD*\]lM,d.QJ_!84R5EQ[@kB=e:emc9U=_0 xO"B8np^иj֐4y2%)kKP F#m$h5Y: ,gFXF;BZ#}#@$EYI'I2f/H|UriYL )gSTΡ$JCh}=zg}m̶m;LlAhkJ̼s1 jzһ FGB*s7#@x`z-Tje4 me@>'@LۼNz Nj&q%Q؋zEUT@@ADhH < ]y\`bykA@.i=`7g^aeY\kb2"'S/<%xOoѣG=|!GםG)5'=݆1Iɯ$IҪ$`xma{sVȭ0t$qaMZ.l,v`"ZyLL?6M WF+dX ѝGb#Xn}$҆m*QJ1Lؽ{|+c-3YH[dB7t"C fu+r6ƅB蝘s []+L`6业A֙!]2ގv's.UvёңbGx4pGۗ<ʶ菙#e/lG*k˯iv]tׄFᯟHO^ϣa'[~Wv$p>a=ړzyGe?gWWJvΧFk%Bև:_RGB9-(B;AH"O*#XlBD BRY /MR|Ѱ_iښ-xV)<1Ʉ>4-AOz!+׽.HCh?j%$ܦvk;v옟Bz/0Wb}/^m6 "G bph|fzÒ!xW(?tEtH/#Q+.=Z=^ߓcBii5ڹ-n{BT^}5˺9ü;#埿dw1nU ٔ/ t W(i3ܹă4XX*xh 73?'ډP#Aq}ɫuӆ /%&z!I;q4Me_YhG;>2~a^~a0<+5m/.dDyZ&% AXRBӦmJQ"FB=eO|;rndGڙw NƔSw{8qs?|ϽmPL!L[M߾W>L5}^Gs+Q@>q}˷;ĬwMS].{])%Zkʲsi6=HV;X \1{{X0ۮwG!"t(C`,9i؏=)8۪knͷ2oaZ$i6ݱe!QYO{-4Ip|[ߪ ֡X[ ^À>:`]y2'FQWZmdtXV "|ۆu @jMmyO+&>7C JB5>:i][FilllǑ8-:,dZκ?Cwt[[_ġV"$?tc-1w{{h8g֫W5,^GI* ~EB];t +׼Q*r>q ҂LhiDs=x yK]>WrӨ. ͠} 8fwk8MOhBCvc9/ZEP <~ܫ ƲlF] C6&F1L ZKj4*nEe-)*jmTo}T{m_umż+s򻶶JU,sBT$IFQD֑mc4 uMe9Q_F%B 2,w5,gi$LsUxs ɲb:EJh`:Z*dAt:ź@>?m'{K% R+c^@HQ km|8BdX=J$jA;=z\HS6 C"%|w`d2mqdP7 D7_B6`r𲛍BE;9e"օ1 ԎfR1J1NU{MZ"  & U]U!D( b4Y1s/2LSa4+ ґNEE_1o\4 oj ,,Rj||لwT ù<焫#eY)Y!kʲHvij(Ȳ$;lҎy9ʲܒSr>N\(P\VXhQznX__I=u]ω~]ss{NԴNMܹ,if^/ Gh!);us6xe"dYBU5 'd(!M&s9@1+[MTVV杬3Վ&TU5'dN~pyij7Wۥjí6NO틢{Ƙ抌1:F)rV02яZj`<m$- ?4M*zQ$I2ru:?l?1)-HɬhP:ơ|) D1gޛ1/lƌTx -YI2 7Pzפ:ǻC{=zzI|#lNm;TP $(IԸ_9a/ " U%(Y̲<1,d,lllA ^ ֚(R·4;"ӑnEQD0IR2)r$$&S{m$BcJ *.HAp8,yںf45a@tǬZB8d`jNr RMdYV-l:Yw,CRu"io<F[|geYcyS5yT|8"rBv dPNg$iJEU!╜LciJ xx0D1!P*ʪc4a,()U`[Ϫ1(:k-;V"!\PЋV!C%Z2dyyzb<#o"EJuQ:xg خ:anGF:[f'ȟWPq$ DwVHeJ1Mޑh#Fb]Mݔ8kM坬o# H`Կ`z\Ho.ߌ:{%d~v`ZpWɷ~t;ow^tlj:*ڀ\7_:#Z[!}@.Fp],ҷ1z_͐U55ɳ]0G8=\/[FG_*=E9vd /ߌ[¬t/nVBtCM̃vGʲKh' G:٘a#tQ1:t>pvƜuC{! ĺ46CȀ OM$weJ kHMuvֽD s:Ydf:+&A@H҆ T6֑m6N穮kfYmOw^hzsL&4B#C)t:NEHj6DpKQ ʛ&>l:wnN=aw\wLaii40K)ֵ~l7MtmLUźL&YZ^n;xrHUGFT;?bv[n;\u'MS\Rhݽ P1ZZ5v>buVfh a䡓J{t;b2GFѼ3#\J)_qƏoZ5ut:my9VI % MM*ˊ, =0S?:o.nr_45/=0!eD85FexÓ,Ix#[P`Z'>YpS rL6@ j rj94^uQ]=) O8Dؚ7* \Ghɺ`7 %8@*ye=~ёntG\[lBp Qپ 6fSvXa2ʪΈmje嬌~!ڹt%:ג:=bca46L&ў%&3#=J/l )i#*8t z'WWA1=퉏塯x)װt=St WGލnK ϐ[G7k󹹊/-|׿GxqlkId*Ǭ!c'v`A=F%ή-9/siDw}HNvv|6oVqMIԐ aTB)'gij,q8;pMŋ_lvv=4#rpM8{xk i"h*KS0!y+^hi{N?lDYɖATVQF"c~#^^7S JRl2^0`8q W?<Ӛ 5j 5ٹk'/xhZs5N9 v0d8\{666HDAC '>1VvOU`)f#xeK_7L Y'zlpuE",M1CyN8H# ~7ؿ?W58nN?o@kPB t?gVMA'nҞcw` !6S|@Uy;`8xi43,XB'^\z R*S("&$QXT!(T`X08 *XtNh08'zРCD1|@GKG`qS)M-@֓Yষ(GI?w=λp7z7ߓT#hkqKap2GBQ 1L4NOq='-V78\t| [(e(\UUyHD{PQ^l,e#iCB%y(M|3feeKKevaw{.Snt ǟpM~$#G+ A4 e1 PE?ۏܒEhj3ZOunwG'=}}|#౏{"ey%k~7LXg|$36JG6_9Le9f waYWKJbJ6j <>4tZ-dW$yLS`FI#if Ђ(m *xJP $*IᅧTF㜉>'VFy  ibi٬$Kyʷ|6icAYФl -μexQbT+J9h%U HMaAr 2H|A$*5Zg֩D1M(J*qPX =$G3 5SkH/aEZcɓex0PⰒ`xW143u~|w.f *4Z$jp"hDzBJ[xH>fu8G$b'=+0WwgUA!IB+-Lt#'e$:Bwoxyɓώ|e(KGf 11d5Oy:/~yQc!u}fT'6"*${ʢ6N>tm^ 4\C|{Iˤ,ykݣvdq)ȧH,w )+HeEpg0^fk 3C Ш# SVspES^җDZc̱Ǟld _wIfyϘav-"U5Y B*G=;`e 1g1~/ sKI7䓟w>U;y.ѩ<繯xh%B}&9_>C@X^c/m-I~ R R ;g[ ]nzcO};7:$׿ɍN4ߐ*ǿ\z~1㴧?3"ɴdum-Y"$#o}틜sxCƥ|募kл?A .mxx^>Qo}~|G6Ys Ȕw;Y]ICk #]j'>y/XcaVq93K}`)Nnsa00rl]ck pN1 ?st ڹ̫8^CNջߋr UWppoc;SA@ 簲c7pDZs2'-]4:p܁,|dO$Ʉ4ϱ~nyi?zm3 n9?.WpO䓟,2X~CckWu1?kz)?3vkq4yH渽W}xuH!ZHÙ_ v\ 0gkVEM͍n N=婌NG?k }=vF^V82! ACXxD]`z.o`rp?zCuˮ7d~8x\ERv,cYi"vʅ?!=fen^oe2|8' 1jp? 4,^AO9:)0!05" |I4/z!O|?p^+$^6KP8u>4<`c{66Wg\E~+?xC_ 04fHY-DSt+٧($Q!(r]`Lj ɧ uBDPci`K9'|Cv8C1чǍ(CpHvo-}kgn_n'͛w./Wؐ`dEAc-uS ::VO\&X2jNs[_~M+:ql?Ώ/z9-$?K:" Q5hߕ2~'DPI2r`X$wF%AQ-[cì(IRDËe DmeoG025 OYr\K{=(fj+wn]Fg_85\~or8G>ić tknF tl`p< =%Кe|ɬL3fx^B4||'g7κjя}:w/o7\CID|&A#Evc'K$:I7p W'wrG8O_2eTLLCpx[þ>53]zDT871 ):O}V5 9/߰ip+OycJ 1v4-[ JOqu evկ|-4yw=.8#x5_"-`~ȏϻPk1;5gN."m}5)Oy)oaw'x :SqF\wOq,gܔyƪs/Ǝ=yalr{弳Oy<^7\J1+k?+iwV8{VjJLYOe{a6aWc+n >/ 00-v 0Bp$mUz4s:^WM4&hxj S`twz%Wޫ9갇~};?Ϻ3H"𚓟B[[ۉ :wzW]}=?&e<*Ywx߽AuzxO4q95SS'עzt|yduㆻnG_FpfVwՈo)zH#Oֻ7sn:咳/$M<_;\óAX^VqݍQs[ܰW\w]+l 8qTs71?MͽΟ{6ٲ0bȻ~NbρNov+Jz D+lM)(MUC dLբtoFBڔٚv@)AD`DkXmoFV:y gUkyg|tuf_wdu^WHGQ5;xuMpMw [0ށzʙ* 㭷s #:|p;=ðj*Q{MU-=t.M x9nR>y۝T[O8nOqwJ[Hc[ VZʠ<3ӂy6O ym#@&Aa!A!Cl"mڧ?;N|wsg.}Ygsֹ~ȃ?!aj7TU5)tcjN)H rs[05X(T )pD (?86gحq'ɪbO~r/{ {ټzCxRls߿:6{/wܾ(YŪTuߊ# M] z??oOf4Cc9}"jfC׆f< @·*Ӽ@:0 xStNO`j1 'qGfֳR3D>,pG6^F#ʪG.<#HGWn嚟)0f5<'",Ò42ѤE1 uYQ M _A.K9_@ ?x_㎂Q|,trym"QE_gn.Z.! %E@ ZzCH%VUA.2~ޱN_ A%]p*6JB%R60G$+V{XznwcYn>s.valh`\7H3? 33LB+ò^<{y[YHi<﹛a0? D枪u G\U v4f=1b'R-@1hu1LPm/~*4 ;+:bkK3Rď}O? eǺAG yX(GdIԐyZpKƃ ͸-2ffh c1IH j4fal46-ƂLxp{n}i_> U@S$(zmxSlV`("Uqh ~mD*$ȶ#~%`ۣtL{lGY]!mgħ?pNl{E2?![>};r>~f: _t>8֣F{G@B:Sx*KxLagfOqIq-71p|̣ z'NTjǾ͸a {Y+Jbl _(ID YWH#?OviF R YFUcF cLNw?ʗΔfpwz;ۆMa:-D "p3`Tu7gv;& VYԪNP҂8hEik2#B`< |1-$%<7: WCRK!UcNy/ q 6 =:vuoThi&_~WC3Z_ysD^cEλ o}cpsݷw]:[cɀ[ױφ]X ?h/Ba Fn>qq@ts јN1,ݱx!όG'`$E`k]ci> a;W|?>q7S?Hj*_vgWaxAҗ 4w+ak{yч J(\Lp՗?71JAox_kPf ~Hڑ$$UP%?tg8a#.'Mp@c M2MHb0>3]~|WӾMn.bPsÕ{xKΰys{͐q}yqxLmIWqGʆw5x@mkNy($B2ǭGTu<4; J"C DռB9º=U٠UȻu13kg8gu0k8/"޶4+C(0VTZWx`ZRh#ϖo+% ;Y%e1|8cJQU 4>g`z9X{)a|3|c xړd {Oʫ_{?ݝcݍ#y"U5Ǔ4f3[﹕W_|#czVe;N{e8ࠃg52ߕq*a׿oXYEkq7xpqN}/I$a-n7y˟Nk;敯blc~C>=(p /?]fgv;ٙ]N;?+I "AT+w%vC]!ޣ'ҰM~LPP 0m,dk-nVV^ A+0^|30"!u85䳻3ax#O){ܩ$c-e<#qa-91я'?9O#6Gyr#XTi,VfY׼_i%9〃`sj+x{dH:5"TR!vU R?$ 5?5\z1Qnb(bMܴLwN mX9'=GE2[1-n'1LBcr|Px-!4t;r-2 cW t?Fn lMJ:TF"@e(S DIaj|`e>t샩KJ Šk PU%R͒u e}v7p iy˝[< Ij3:W\C}=1xSDـT 1Hc:#p<'2Jt]ź Hy cu0Uy*Ii<"FM8 QAu?jNӾ|>T`Hp:CK0)j kg}֬&]Y` :) [ٰJO!(\vk|N&P u5'{(5Yξ2c/rj =sȣ1eFo|A\O} =$.GviF˗(VXr 4?ݛNG4$>(ŗN}+pOoG{4TVReIv_fj29E{7VZrm%@@Hlx <53IǑf]Phh4RD]O's(WNؔuNgG=hw!6dc+7LW#JaRL+aa&Ym3ImQ[dm(vAN/KxQ99tMްa+7}$R@S$SdiA3Q30=8jG bHU55C HHD_^MM]a xL=*8/W#HP8ϦJ13;YX@IO瘦"E抅S3[L`1$]gBL# TQ$Sԃ"PVUg5uI&5;g UMvxt⪚)%RQ!F dfg$n\QOj!F7a`#38xwwa$TN%TM@e{8\mj˨XC^De ]l43D3+*^bE )ANj3c{ Vxk㮻Z̟{Ujw‡E^j؆hi@EI_h-CHz$0yAH[ʸ!H;Mӧ+U1OOg40!- |jZ*)L% FԈNZP4! hcDiRY]TYyZGI-R2k `9S $P (:M ˸q P9"$ghI]0zOR-NhM ),,h꒩~AmJF(D5#@m#ofzv@IF68PkR#xĆ }t ґڂd*2%f0{TF{=P1"ФP dh1I c@8\%, s2yAGJrL1)nK= eY⑸k+z:"1*i=FGa"ISyKtH1P@(e4(2ԣ%EA&RzDH:[$ys4g0tB0u޶>7:Co \HdKc\LmT嵀TX) ^,d+^7TQ# ut 0$2䢃0 5~za"Kb4Qq%yb+ HPts(\tf+eM @R(>`Cd(i ~ KyMC!r$'6C*L#d[%MsAC4 ))"P& JJE`1gD+4yApJhA+55ݩu# 2Aɜ+:M֕T"51tdJh*)p."$‰><N07D'=g4^@BILT`z] ~A#,AybWmuJq#ZkLi)ZS Vjn.puEJcq-$IdDJDmt7 ֍Yw(RK J5.vH;)!/SLHA3ʺK^8 ǤiNpLVߏb!+$9#RTƌ@؜Dg cNㅻ |]&%"CԚᄧTeIH 4D&-kWa涒 j`h(Q;riiDbmm44M7CTa*N*72pI-:$RC<2ݟŒ IT^ x0 DS[C*,nf7gIx@N/=C׉CJ\Mx$ -5D%*I9و*KmLu;تN24 ΐ>\jo/Gt=re+nQ!Rh5d+^/< ~%Am3R{)\Gر}"=IpALiB!F:D0dI)5!rM2j5eemW3XHw(SgޑHO7QRi iluF#&ZIpX弔!yNPcB;g1X+Hu4Аfr4@IAȲB|!XޘɃX/]p1ms2LMk M8iM, q7Qx%R4Κh-{ּDklӴ˸ɒ_!$4q9DTXgR1!je^w䗫 m෥mw6ee7P}KoEkS`ٻ >vx*M/sysu]4M]v%7M,S$kXUG%@X“ Sn-HfI2 a- $Y{ڲBG߾B#|,E):ε?pJw<6HJ"KBKYz_cI2_i["r -/-s>Tr) uUumk@Ƥ׏+FH`#Mdy=aeפ}^mS2Xq2~?2ddI 2[>^Ӡ ^xwد n" U`$Bwc"oS#`Ͻ4/$8d-FD| [;l߳Y>}3qGכe\Xx]Yėħ~RZB+$vi1?"~%\.[">ηk0|b;-a<*y#%!N2~Dk !hKBdTsL 0&;^ xowz1~ɜb$? !<ܸq" տ]yv_t6t4MD߭H)WQK&']8oXMb1ɚ$@" I&QWk R-UD.b)'\Lޓ\v[e܎* DyHlf- W/zݺ5/m`vЊ_PE) rk-]" JJ; 56?ww~-K(8y=ae#$KŽhքΧqX.'sl_nɤ'DbϧT:kr}}K' q< զmR,'BUɻn߳-}qrPЯIT!l{zh Ik`#X*վ^|$V%2Ȩ@%$Ak챓m8l$ ?sq/!HXߐ_09H- "_7Ĺm-~SoW;06Rܗ2~YM\[[ BضCwk#B8s9>a-O^ɶhQ,!7Y,mog²ݩ|Um*#;'{ZDm&_msD Ee i]Dda+n"RkDDۅ@A,KP cȖmBq"RD 2m+ٞ$"j鶇q|ti68V]FXtM[E!*l#c!q-"Ë@1!e8PmӍ"VnXLe*L*/v5,a_~] J}@JvB!et,Ho{ QL5x^a! =<閆gD*T6Ezז"Bz&%X=p(lB y!&]-V&XRWp&@: D_Ze@d$9Z3+mû,s+vx;a[$&_eһ bie7凖m.qCa)|w؃al˧?%Iվt>r&]j-BETh9sG,i.e*&9JIec6Z9=Qߥ}K_QCB11nE;8XKE^}i4%I'MvLhm\pzw2_>h[h"3k-r1l_f"CƯam%-MӐ)-ykۓJmC~ _tȗS]!@R mr&BXcc Vzkiͦ0nM\Gz l~ MKHʯc},rEo{_uyߌQU6i ^عPz ]n_zK 0MiDzm7,KCBMH]VaXF7 ; Yː/4/c%P Λ eFXE4$K/b5I4lKqӋSqIbmאʷo4Nris9CLw_sd% CYlڴiZbJ#o%D2ZN6u%œW &|RdQ2\7X+ZKm;pc<,'b9lb6BIoߨ뮻,qk7-'%e¥,ȶŷIۤ(e -g{Ryp5DzԐ4čoݳȰX9ARt{*͖X>/텘JEiCMrOXzwe>ŘeQ#`[na<߿X"ȅ6>mfU$UXtEg/+(docr@YlblDnt86ZJ JcR/۬&qz(\źsZl?P&0J{۟v~ I%ו?$/gI))h9ِCXOA b}mHrm>Q* @p $>xDc,~mLvQa'J;cK䤤*JDe22D"(_lQRpm2R'ZuIdsmqO  H!VU$,J76ZΌz;Ѷ5\EՊmIf~1 ?L}ߥ =²4+Bq.~qDш|4»">vrV+5,a V&16%RɹIôCI(,{?۾-߳h+;du;=l^~BwW_Z u]/&tN۲9[[֑AnQb0PBo,koK41IbR zئBljZ7q؎ްc.no!M4O(DƘ_1aѶf l-pĶb@Mh",8bKVC:eQ#ڳ}r8KH0faJm>>BE}ҦD\p8/ =Z!D{ڪeMoU,UKY:yh6@%IòʜRKpm1&m~XW&'(kC K^Tm„oUXڧthdĚYIzҬ$MN0+HvdY`0 dYRZ>bʻ%`XTpn*bXj;C^p2~gƦM4MtL&a]3!ϺHXo%Hb` Q> )bw@exnQY<''XDЭI) A Ƒ$ >xY$4Is#\65ַ2nN^ivc2KڭM{~aHd@4U,_d~9Ty#VӆhMl%K|(P*4a1MPEk54fjvf8eS"g0uA$Hm'Ij}L0&ؖDێzGt4 "%nʈ+-ʵ2xB-BakDIm>&u]geYF(vJ%JQuINNo 5M,*E AL)B(ަ_@vR4 Ie1nqKqZ#ڽ cm 8\tl\v%r7"$Hƴ}&Q//SJu5BKiaIDFM~w`]IzWX~=UU4 ι(碍 J; NSx/1uV _o$~T I";ƁM}QJŊ@Ck]DsH cBj| "x#r(IRMؘ؈6di@'ͳ<ͨm?& HuHM"?yΕ1ٸ,5X!HڍBJpAKV)%Rx<$/ԍk nFc EqJ_kP$;]TMMt:6?XBGkُ?B6 JAH S)3᫊t]  3ziJ$4N05nd*JC$J98dG>qhl+&1r4.$->д=<w%yAxJoc*|SN*E@ؙ!t)& ji{O$"II8 =nk-B,k)IT c-ucu×JڟvBz=㐹xQJ:1mMiw4 [G/9 6B0??O=Y=woSu P 9,?-&8)ZN~$_dNDR0|_?B( 9ϊJÎx`1B)y֡yӛ!m]w £ek_=c>/qBJF$!cMlݺN E֟/} ABv"Akͪ5TU+VCͽIF[!T/DzCet<^D}/oz+8)U7_CR!xɫR?U;עL*\=X%o{߿"s>`c1{wl|t^q?[IzW!tp4 UUq'> . .8}q,EjoֈV% L M$mR.q&{@+)&Zb)}Xŷ:wb3IJn>o5Ջ\Ec^q_+ C=\r饤)Ƿ|&_<<6Z nݣXYPDpIұvnF,'<~x)V`L!pWLe6@ÌFRQYCh h=Td9ALI\]Q5 ,,,x'|"EEZpHoj%[9f93%<\LvfY|_7M[+$}Oek=`j=?h.K#g>R'i^>`89Gˬỳx~<},)sj?OJq#Nz]rOz a?׼u;tZ<*X SsKwhH+PiNs'#\}e<4??? aw^>Ê+~r#ȔUWq7o~>dKH#U(f1SCt #8{ظi"_ZLݠdtfmrf"=M!5J.x|,oJذDžg}-sأڙW#)}1'$.[G>|^-א۝|c Mi%ƫgsg={W_uS^O(ƫxNk+!i-zp/VL4uMws 7p"\}uo>O2jOk?J߇ -{hEX'>k) |CS׽y0ttŽJ*ks͏t [G*L3@Jr;Iz^w%zʆ>ߞF.>N=ͦIyC`ϭ i*f=9O$i@g5qYd]/%OS>oƳ֖Õqlj ES ғgnuVexE*yozOyp%W>.>ko9iyq.Gx|b咋Υ}}(0)[(K?>qMװ宻x_5.:,v0?=|AUlX *j2ߞ%%7d1U]>.?oy |K dTRr:x79(]qO/gC@C!h)2Kh3l"<:\U?}vdY;^`][xߞŸEL4kxX j[zv84aaa)umXn5]gk8GrG?S"׈2 {t В53\|޹T8_?x.afqmwg>eX_#i}ɬ]/IxSN 5pUWs)堇<?!HǞQ:Nx^x![qqR5y~,Qpii )<ǝKg9`=r5W~=X$Z'?1 ATgJh'áڄx4~iC}]rqdfO:4o|2p%тl++"a,iG1 \rgk)>я(:"dF #t_XҒjJһ2~w&.lZ꺦,Ku̬zu ![Yf {89ꨣx;k N <:j@P1dճ<1cWpa'ZG?))И12ћMh=xVڌ(~)T)ͅ^E7:9`ݨ}ͣO~27p v9e=pǭks~*?1=^G'дRinE( R<:6Ƅ@pQnߛ&@U;ƣ =y⼠b?);"9|.R:p 7uZEm$46 EPsݱFInkx;'I&/aXb$b̖Ϩ{[R0=ݏOSf̮<'q/DRmo5OVѹ8edNѝb[FdvqzUl:\i!1xwqܣֻ7S͏r4bhF> `J4eS355C@2V:'O(vi'4× "30COyQGq dFiLy!4$9;];g-`˖yv[yզ-!|3_fuC燘j+:vQP(].s=wr#}nHIQ(66fD jb4Dቕwe2$ !>qI0~z.?.nQP(G~7nDp8;ADpTnxqy <5w :A=ˮc=0#@im%Y7J=Tg4FAha]rbA=ﺇ;o^{gγ46 na^?կz], u;ҕ߸#DKˆC:-B]zS0A;\r !PB*R-u2(IRtz]} R2?BI&7)qIi |?|r)ѡES:-n; !ڍJI US-Y{Seh(^ؗ;n=v9[練2Ke8$Ե͏. >U^ W_Ʌ|G,SfIdB*|?\p>g,kCף5>4- JSLH5CՐ185{E qfy N'6.kJ;ӟ^w=^-O퇈_~ Q5m/~7Hs ]eu|`v~0Ayv$հ&Qq`9 }@9zy=Ğ"ə_ IT}n}1\@)TUy:$+ mE鉦"A"PuQ\s5|SP5wq:?x1xz׻FENO;>N#:!r}{i)\t#Tu_<9 $wg.LkV'R 0t!$=ټx32 gӫ׿7}G?n;;7 ֯8?ttSsJ]lh*im(ߍlIeYr-ԦӺ5J"lٲ=jS-ʊi!OcC5B{OB\[8 &u!RhN/\+$i!˸Q5SYN SLШ$a[8C2TePQy.rNwݍoGy_ N)\gjjA$!jЪ2Fs `Ȼjyءp.[n{?oی)#ꦡ>$=(i gS":2߉LK{Wsg+[`jiɪ<)χK# 9Il@%%y;c9B'ħ̘ xen */vz-/3ݜ/~F[Bӟ{[£? r0`J2 ^N" Ox9K_h9)O M!x7yF!T@ɋ4y$7$\1`^ }{7 ,K& E|ഩ :,qbJ'(`ˊz([-G}ZE˒N,Ȍе.:͒XFxvZ\1c[ f|BUM+&wNpfͱ9> [;٬Ҙd/{;4+=B ZPY (xOjA_MLGc7遊K *0ҷ TL\`L"& vm˴ ]qwԀӘ% )R7\uGվN|cfA(up\[iƑr /rIrAYW,"11$BdZU]Qk -f}#$5E}4Kh)!s'`)5V6)BA(36wRRLlb bPWk0 eQ&,'`) R&y!2⒤/}گ:]ZUM]{&/Wo/~\aK-|ݩh𞺪biS)33P:Ԍ#&qڥin7!S;p$duM2L^:Ş`g{f41ҋRlLYqoZ41K$x(Ec/{6fea ĖK~'wJ egH㋍*5ob}>$P%;H6pbXQR:i?1>b G4) -)F{15Giif?ܘZQ3b_ .djw-)j{ߥ&1O$LO)>(IP ͵%$~ԘKDߥ5kZ "]"qN'!BpA;.cKw$~fjH6˂`z\uUGyuGsUe{¼EBZCyth!Yy8iDpc0!]Kߥ7X&h@I2NzJU զр(CУu.BZQ|DdŪ@0-ܨ[UzH~_˰jif3; ُZ}>\aSSq'KTn- %.qCQ5]-(&"آxuvBpT"hqdvL GzRBk;bUEHaLJIDZz8,V3›z !I.%ƺ5AT5{֔l-'vײ&|uSDDV켃2-W~"1xޯ7ѱwUw*!>fѰoojTU`e 3Utx_$D"E7ȕZa$Hj$YUNu t$'❩ PnBRy _!pE鈰&^Dj) LCmUQ,`'ill#O)!}8 "\{,<1>Up1}H?29 ߏmw׬D-8]ej0DGŢMj64%ā CvDAb$2"1X@"1k=&N1ļ$C *Mc!=`$'&:ܕ&Db.#dPɈ [ymBu1j]'UWGDP.bL|Dq~6}`ERH+I/!ňMɨgfH SWDiz{%=SŤXNbDUF$WdM5{R*BjNUI|$+I})d#!>a;Iq|ODT ӹ(@Wıno+ J7S)k-UUwhe8OV^SZ!XQ>CY tD*hױk^ PMQwj btBeP>h Q Ѐ3c54QlNnе|fŁ{'鵌(5Nc Rs+)lV7Rj6dLicZdGA1d_OlGCD d m^2))Ǻ\Y%+gA\{}ĆhND];u"kpo107:jD(&M_S:}6z41sǬ9)Wٻ֮] DAWׂ]cLRSIqWw+r;萝q]b sHfvJ8)hkN%. FHU6M@k8U>|7r4N΍"bkȘ\'ZB*rSE3NGZ gLk,^7yRUUK0NaNj˺dXUU*~O42^;}ՍH^t]w (;XlD FjG!xij@D):/QtĊO L'(bl|>3Bi) '$.~4L'gFWlUc3VԀVq:eC1o3"Ӱ0QtV߸Sy~LSQ߃j u+uhdXb66EQPSB"%5ؚ޷}(-0U٘Q 7lGGɕ+lD 5l̪Ho4u=QTs0HD-3Q0&5ūMyQrCrTd9&A)݂yJ4D4d00QQ/Jjs3N*RkWWϿ-1h';; 0}jX%`2$]Fuz@Mqq27uF>!ٞhs"66euG̃W*ETVZJSl7V0J1FM L@ER *T> D D-4su#ĈT( ](͞}eX!)h7kĒ7jVj*ƌUV}nDkMZL aԸI ydOC/@ypr}{#ke6q9`nXCdkɁ} [Ot8~{p{pE2h1&띶kE=f_@dFnp]rYX\U3@{t}{;dfık;K{9d!}<:ĬH\WJrB]^4scINo]CU5cSUMj2ړ(l+%׫++4ܬ8"XS`αCB1,u?1FEQ\#f'0jXtd}׌Vrؘ۟^_MrEp|;bfQ%6S(ւFhM xs\RXu)ICbFv}}j){GȔ#a]^Wk_SASUEz*'9mXCcW-`s74^pjN$~"?ir=HA*yu>aƤ6TV#B \ )bJPZ_x5Նll(ݐ\^!mp+̑'(CSZ׏YV%4Bd⾠jFn~ |Vr+'dCf^!hd^!U?yEt,ɚʑv$fCE1ۛBƌZW-{ƵF#cK9&!hMqvIv 7߳@OE!x䚶Y=.a ncb:+bd.%YwBlXZ0EB&m$j5&fL3 !>s{2HH/|A:a2G?$m1hT"ꮖ,TZCY)?zb(m^+uzOBш6aT62?Cz&Gt6f Fܥ&Y6q*rY(D|u͌u5?Y Ǟ*mnP\F)7-`Z^-f4r6TmKI"'$)chʬ sq{t.kB*ʲi>=!& k| kik5I_*]k Cc\t=*zXQNhiRaQXnuyiTѶKl^,mHFvGV%4ELBH4!RQI]o~X vd4e;S$9Y!eakX\XUl&5: 0E}Cm{AdV^F, &2+Ey}TyDYUGPPx\ל_)*v7LFjM4~- +hz_jm` E DM4V:FT(G45f3nz[dI@y4KIjT8ȅ | 4fC`b`=V{|VhI {6S&b֡9IuF4.﷔rKo&dlV:cI188q$kW7l:*aP̤g+rԮRs;%F ,M*憀fU豚CY%)]++=jȩ}:1xH߷j(opU3&FL Z\ 7 AYRg &358)7˄|Ж[^&n(Z]ôk X>ؼM\ h)&VYKbƎZ,_I.!oJQR4eSUEen 6%Kz "YOq1CcPIqUL%Қ6  fD "yʔEqiڌXٴ7UӵU[>ovvvZXmQ1. a * QVrALs\AXIр)2lBo*um!/δ6|i'lWN~sHʵg~A`5h9;tE ya5*jj`(i%W5Ѭ8k̙*O^:sO\A&^8T&`89@h.j@D0(}*1˂&EDPRC5>$D~}k4͸BtY۶x<ɴﳹMhZi^B\S0(BեF'SC4) *}5b4ai+|U%U7JT1GjF;H[xj(Jca0H'r!$OKTT-*yi<0*J0h8!*ڠ7'Ѫ'[.4k&h>Rt5lyM*ôՔC͕t y&b4#4ֻFEM0ese(SEbDIV<BUثlnB*ԛ1+:{θ_Vu"D2Fy$)aI& > 7HCk~B9A1݇q} v}%ݫsAAPK NPeV0Lx1|jҢ y'$LmV50xJGp_ \G6x\E2ؽmf8J9ƇDpmoHeu;J m_.eVKT3\@Ȅ HS(B}V\$?a.' ᴶ>Uɉ[$zj L@ggba;4堿֤TUɁM*՞*$]󥭈ڤ@(^"HXL&`bx(Eֺj=T m[ʲ֏!@$IDk-]кDs0qh29OTiqh歫A a֙k$e(mG UQ\ \Ak>-qg`SbsZ bstDJ`OǜeC7mhH^˜L cC;e%_51x)R2UZ)q_ر{~%9荫IAhHXؘ͑%CDf F\ͣ*W};4Zz$႑%/]UUCoL}OIf&ˎenɼ/"1!}cR&.IC!ska~zP_ í]X[7ϛAD?ݻG]W o9i4kzǚuzLs@u2795gHJ/gfeV?&W62yDʺ6WpRpd)3B7(dHQw$qj^lιQ3Rĵ#6[-;q zv*V=Q>ߋ2uet`25qy`78~yz&zfUX D:IcM(O$'DIJnb+?k&qtun 4Z+ *cR7( j-!jޯXt]w¶>z4e 9˓`t ҌɼۄyJTń^%'nJdDP .,mLA2ҤaNLYMÚR:uUYW4 a5bbLW.P1Ezո81&~3C' D<>I%Bu${/RwH"8fi , 2vy-)Wd$@*j8JЋi*8LLHM\ˮƤ52 vR oRم V''0ɝ1SY1PCz`Yu9F%u*GH()$Z(fԃ9X!PW9wp@ȵ[>hy ri+oDBF+ScoDi31iy[eGOZ{Ll#c6 4l3U$g|F_6(1"U0*t%oH-F#X>E{٫ʒl# F$֪Ot c_<  SԀEf8C!AU, %kC\`6J3GԤI8i¹KY"V5Ihe~$χL 8MAqpӡd!6%}aUd͒iW-TU1ƱNid_ 75JI$RbNZI-d~HGLMb47q C cJMCĤʇI+iI'pE=&WS \Ձk& )PI(5Li VeEy ق##bKrebpb$&9qeUJFeh˓35&7R;2ʻX#E)F76W&價_k3kJU?_;,o(mP%Oa/0P%]LD asKNɖ}0 |>2$Đc-0dg1*IƇ]D$SnM&j@Y} 6(+|De\)SI#=jR(9X2 \U@o ::?G) gz0I6-jB ]/*{Wrhb[GrEdTEo$A DUL\2=(tj5b$RС jx)v &eciL IYKBbj$ر1Z!~T'C>~CTPl)D1E1S_cu+Ufv=mI |wتM 8W =TB)bK HGg,b;nQê #T:\-l]Ktf}g a*!b:RI]B="&Tli=V`P,'ίϴ2Rtb)gņ(KFцSKWLŔL z J Jaoz44Dceh#{ ڷj`зNÆj k!5UsZBv:SDOU[9X:qD) &څ1X@Zr]Ew1hJEm ?yЇ7٤vw'%}ZC4ic!1'to/}h:b\LVyֳg> ~ at8",h|N9)2Dǿx؉TUE.6}KQLQIԼm[(J˼Y""`:& Ͳ0N9qR*(M| gpxsʏ<quEts(ෞk|~XV ?tZ\z4Xz~W7zWW×XiƸ$>ikFࠑ{|<۾;]rgy;KgQ[zÑGi^Ї񦷾@Y//ӝz?]Hμ/|o}Yç0{ȩjȪwDuݜ8sx>&kiԧ>~.׿8E섍I_}w?>w,t6e]ږ^xoz#7KNbV=ĥX-.'^yīvv*˼87>!|곗^^oVYgR440à.BdV*ჼ=| 8[3n&Uɲw ]0+{LywW79rds|PK甮aV)ЭdBsM (PSh[\ኣ}͡#[,cL1JRtF 5%6t:(akVPWɬp2@@a-Yb!%^-^%3ĞI]q6[[X4ĖF y:ҵvR%ڳ65~x_П80`76X4KB8G?y6>zoٚ ozw3m|GNwz~6˳xk_|;?~9!~槞=vwq;(bO=[=Ӟtvwwy/_S?=Lg%}=}"_>w {??r݆;{2,S^эsjW~[!zşe^G/铒CU>t=,QvUw_"",KbL)] 9uU?O}B m^m>G?aL! 6w `wR9Fwy\rJ= Y; ic:%g<t{'_;^/D;NwIOA0<繿prc]o_s<~x+1&g:Bs嶈1ж ֤q/N?s<XKCOo|#%Ooϟ/ckZhy/"/zK;^7 <)v~Ozgy.g񖷾yˏsߝG=nɥΩ&EQJlmmp{p\eӱ1 g?o{[ntQp>J,gLx>?7]|/.л׾\s_ɇp?E /y-|}q9Anx2;4vsrxe-].b0nr6]bO~ /$.]9n7Ç%Al|pRaMs`c˴|cǐskMoy3QmckG7yw~w~C6skxwNw./yٻx7}ei=Zh.Ͽ U~F5]`7X,qJ߰1}WKy5]nuTI,ox.Ք(Y,7?׾MދWy߽?xq[Hqo|?wWx^?vCar}5O~ǯ_j~/`qB.<)?^zp (eay_/敯y5_Ewޛy#y|m8;vݕ4媊%5=`g+QhN i۾?Wx6q܃෹yw~;;/|> 3>{qq_̧9֕\I.G8Rև>v#\uރZK{OXI%w~\.]{>Ot}eq,tWux?tCwǭo뛿ͭ#UqSOfg{Z,m0035Foe69<[OG>ʹސ.{{!$.C=Đ5#8W{Җ=z~7-oJ|w}ǎm[gh(qEMi*X ~Oqxo-o_xw~9gnEsϦ 'x<:s8oo|[jv}͠7]C9݄|퍎=gߘ&G6'>-o}[+|4tm߃978C!rddĵ+6;?trb,L&F bq&7?5q}4 8wKeg^v97#Vˆ?yKx#j2ַ9}c8dzr[[gSX( I&QQT 3}{I$ˮGGY,9ǷI7Uz<~ùgr[r<,8Hvc'PiԭnuY y]86)}DUY-ˮБ- /Ӟ΂28DUtg f;k ;www9t>thpMO]mp|nw;P5u]o}](W&!\[)'K$jckVgx~ٌ>^~{S=5\v`&| hsq&pIļp,eYrg,\uEɃUʈԦza2ࢋӻy7>}'v'?edRqEE=R[nuP/EڜaewF_s.G0>͗kEJCgA* kcs ~=;-Lm9 (yxϻ^K w}MNَDSOI p[߃uwanvs b<ޡ$5_Gl x}OUtO:rsַw?x~6|`03soCLy@U, ld(-Ee xzso?o&78=b҄:Q,7͸q xehIŲmL&4G=x>UUQW k@. nr؇?B@=cV5]| [O#f% a˷C0]4 TUŲyd ߭hZЫ}mZeE4Ds˯9̧?{|y{>ȑox#󎷽گg/}BQ݂sai7T:4J3_P2WoؤKNDRGzQ,KqT (7|.R,bMξQ`GX\~W|{66¡7^>BR L6Ks1gw^_w. gki8"AWu !K2vhXPJZ|~z0>򖷼nl>ee#կm/-x-(ǟ!g[s61u5[qܒ hnr6rtnpG<|/O_ʭNx~;`?c\&,+.ߊ3mW6 f] J1Q,}LZViV躀B6<9~txO4t2;|>iՎ;͂z/2G+\Ie˂7魜y Msyx.}2L7X+.(7y͋׼Ўƃ/N6^%[K]HAs8-^b2$c҈=97>>>1Dx }EDSˡa<,mNbO3UXcٌQJaC]brr&tjq iS;Ni؇<=JbheeQnP%#;2RFpz&eMVEuqr:%'ڎbZbE) v(VzLr2cz:4xBsٴ*8tseqTz7Y PI8O"B\AؕDR*8J\1;-fJm +fNPE: m`+9|0vq)īa酪tTt, rmVtMҕd b YE'lm4̪kF:Qu="lZ{8IF48@UNrHԎжhtۧՐFx c)wp5}ڢ`iuu !lk8Lȏ12jý}o^Mϻ5GzD+8%Q"M/[u>O,-ƸdGk=ݧ^x[ovcWE5łCQ5łlX5FMB{˖11J>btT,{CAĦ1}ݜP))7NcwgFU-IF/v966w {''?~z:!hOUIpM[ʲ`'Iv G-±cq_4W2xb`>3V-1 t[A cfL\Ѡ2m',Jr@ҍPԖ,xCDW҄=LiCqcKu4ID#,yw j$aDgL'@Z| #x1\v1w@f%qu%`m){vvvqF}4.'>R ~b@w-=8㎦ akZTi0q o+Z5mlUM|} &]qiG%[)ysSo;h7.":8_PEdyP\qtF{ㄝ.]iܻ1'"ɔº bX-x>MERq_(n^6` b~h$`>IGV[D  :._UXj2c}{x,EQaePifZT, .9F11pd뱦̒3 DllN9~r& [ \~eL&*uF/t]*SWU529S` PSuSBlUXC4Mr[cQSvO9&4ծ`_ms:n",4IU%ƕlx;qC¦m麎YPOfD`{D-BUNh#ت-tZPL;lUih$)y*+h:c'W `vh4 @"1X=^T@{=LN3(-kphZID!#*6KF3&uhfNa4tӵR%t=uYѵUov߷v]Q"̷cmv5-R2'ѭ ˖E1JD Q|`6[|G]"RmKe c y}F5oqV0 [d{$zҡFqM߰3:A't%N żْiҐ8~8eY"FM:ܿݘnQ3:]RB{ʄCzzJX yCŲ{J/4}82q}G[(-']N=!b|>OCg3vvOp1V: A{p\3f\]Q5_~9ٔbRsq&$EgK$Bi{&ؘm.öh\KOeQS[7KX.P%G(g6QN' Y#*CDh$F_;vwwz1BH%|E>ˉ`2`e>shk]X즁v p'O%IVY.焠oqB}`s6cg"b:怅t?m[3l`{?Nt=u]S%EFӍe]"`R;8Mϡα|ْdL&(ec GQ}D(Le{_Ru[Ђ&p$٦L;sjwY,>v L'%}PU[QfwѭCج04 iEA۶eFv!Ǐ3N=!FjR'=c{ڂJBibR ]OpEAxqi'*]H>NLRJmC{i4UAB@cqI]V$NSKȯvT5Q6PU) ԥ^&ؘ|CZJ)@'lmmb>5}290)+|ӧrt̷w>5:f6hzOYԄ &uTs3,9Ƅhwףc@u۶18Y4szfij P%1\Cr*ˏ,k cSG%q2fˎeVW؝3]viδ͒C)HY.>0j|N7h{ODA,UF.tF)r^666Fc8L8~8TeI4YW2-'8KR՛}AYt]Y̙ tTbN פ:+*bl(bQX0NPM]eX4s%}iGP(c vbAav`,e5+},jD`g,hێ-&.,iHYMjKw̗ b} DB {=ΜECߧFڦiIb58g>>sgC״8ciMRP6rBFz߿>"5i9oMc{wX%t-XKC}(,oSqj zAL'5heIg})XtoLKU0%`>U&jF'DyrTEA鄲n̲nSXA)]zsX[kθ#|J&\awew762(+4bϬX.h1Lg5MK-X:pCD:b]ٜio:Fɴ kKPzP k qy/2h}T{p\WoZSP,ЈxMAq{:vwq.BJT?g0&%Q2뺤{[X,tB;OaU3JiԠfYt;mn)8z(ǎ`D3e3Zt:'vZ꺢PUҁBGOwHTZNyg()mێ#Q)MOqP L` ~I!)a?(BZY& ز)* K"mu5%Tu0OQ5ls:ĥ}V\A3Z;V2>%[AfxGǐ`+f0T\.mN|hXB@DAuW) KU]fN,̦mWGqKKW&<$}Ӣcb*v]l6뺌73N3ރDJqCsmJd7~ldk*ˎZK^'늄„>lF1d(@0 r8Lt 1KJH_pUIK̍v1&5RRUB4ݫX k /gߌIMUUcSWݦV5i9IqEIAcLEQ49YӬ H{hh8xc*Bj1(&7u TQ/I_CD+˼~Nu#v}qB7@gS#bj@A1R`]IЀ2#IaDDS71lR4`\Iw)B a2}*5 9r:샪U._|G@bH{X$Bh+ 1zejnv! l1EAŊ҇qiS=n"ơ>) cPc1c!#+АtԇJ:%aX?bE`"ݠj"#-'8C9$d:ƒOMk7A=oM'AE :_Pj,A i B U0"1QXιn=&`:tzB$gTAk;RJq-7W=%9f@*KSnumcvkg-/ef0l_79Al&8AHB jYb:߳}ߟ2k{'^o<Ş[M5F4LZk?p[ʺh#H>M^,1iKYf߾6 Q=he=]ZNÜ`lA:;mug5)e'ud:ɒ=O\U}X+F~i7I~k0:Llֽj Ԉɻ۠b1#H~!xHl:o&>jVs ޯ0SqqDV;\k?lTg$hFZk}R9HW9C[0 C;|oyW8dg!mT{P|611AS_/>]Ѡ\)(D֨S5s+er#RJC&Q}ɯEV JKps0u}R "{ǿ^jzf$cL2SׂWdeG{CZiJ̽6yh.B]tz^wX*XJ,Ľ+ullztZgi8~%[ %?O+41Qz\=xVg)JSm}I*&J`w8NC6я~,qqYg){\˱i>seFrei51g6ErMZ~NEˀFUjHC#/Q̿v į>$JHn* ؠtp>J$tW8+!rJqk9,!!/ceE*ՓS=vxúyz\9pDV|aK@|ݿ4%iN+NA署c|svu 8@z-ö)f#Kb-oF9͔X噸fLe1e(S\ zYM3YJݷچ'&ad3eShuTYUѓg4vW2ejwڝt AP5ٟx\+1M7HtOW[[Yֳ?C){QqHY [xcl QaQ,B+XyrW,W`sƵ=Z~5'KͰUծ^ TXg(:$OUQ#BjDݏ~%Wޠ|"O_ʂ 9 ~싫 kwЛ>ZeMVI P3,L^'TKa9`CL?Ea__XXݕu8u3,dr feNiALn*U2|eZ,S!vsņk5w_)Qv&{6cl^̨bڶFVu?䍺r#'k8fy*/{P'5gR:j29m3ֹ\)!Ȋ^ y5'l-=UQKeMIͺ+QR(!'9Ei>]mVVAOsq}+4zC'rG*+yP&qw`Q1wϋ$sRO=D&ߛuL֟7ߙaGBdD{ي[kIr:`}ep=>mx9힅W^霏UͲ۱z+{}mf7hl4kV.pGg9]2*j>!ٻDŽpVjyf"dx^q[__=v%u9V&Ji:Ԇ2z /u'vp_ / LNW%dmA:34@N')x'=v@lfWB.Ie+'e{^KN ZbVC&O>w@}6.gDDCFJ4+z0`Dm!Ϣ![MD,5"n%MA0Q9ҠHiȁN[N21*$tt Oi8 "$Aru2=Zv^C6ˀ$c^-A  w} Ў0m!x>/[=ĤW&]ڱ>:-JKdIҗF| dU }DcLY= C36gLP3;AAz44UO6*Fi ^!0!eiLJd:r d,om}DbX4b #BUAOId5y$$q/WESrҦ=QҞ>cЧcJڂ*feHWyLk݇@  RazwFL$,Ojv}~|\ FS~.:^i2Ҟ2e5|:GtyG̪+_=>cX/@h~!wݚYIר _:H32'# Ut9ѐaHB>!Ȩz)\ ĎvYd4{ߏkHNH\qA{p:W(uO)W^aҚTE:Xs+k߃qhÞ ~儬341#ntL֙dOA.`MZi1XDl hVrRM`b 39]S6ӘZO:%h]16Z%xQ$!1b5c>>lX0eQ.q)\qEnt2.#U5B4 Y6J_|R5"3ɁvhD8gGel/d, J[r]!BIzkj#*fՐt-O,2eSd NjG۴ՋEvaxh.F7]3߽a1wS`UljF'Fvm^4b_41}-AԣO .)k \5( m)0MKĔY+ʔ I\i'`Ţ(W |mۯzYk5nB@5z 2@w ĮM=OQqEv!Hg\6)!U0,*ERfkUԧDOk8,= t9)D1%k۬Gy^;j&( 6 AӾ!!k R3Km0VaWf,,OXM8$j^qXQ"?<;E׿ݘUDPe&#M"]I i{Z4,A qJb}T@Q=eӋX m ށ#I`! (MCRoYHlqt E' }S8_aGU:U€< m#Ƿ(MWGc1N[QG 0 U !v]q+HL Li麞)neL4 VMS0%66%ۀ? Q&_:hT÷-!X⒖oKOQL'*]X[]k&5<vƉ\=EťR` ڜbMzUO8lj 1$O+((!JSPBhOcP:d7;2KFe*2{K8gEU]Ij-,#6x02SS5H S EC dfڄuЇ"&}h1E\;֪*2'8-tac+ -M > 0x]MW)xLɔeo̴LA d2w5KFB_"V0NzZt$%,g‚xOIeK0RbņlPeQ6f3KUR-q>KT,[6G?ih5 i6K5@cK?]%6FTz&KY7C8zZr耰 J3gTޮx>5 nQvqQ4$ DD\9 !(bL۪SP/&*5[lmqi*&!na- b2!W9i*F#EIi\% 4STЇ> j%̈Aip` TU1P/ 6E&WKH-K;@i,.+* Cg 6bܔC\Or-g#LD+уXT]`(`2* %1P:bhɉsx,=.)(J3)F4&\ZQ) }z-bHM'Eәj0V?"Y^,WRl Rtq(ҽ/mOۄ*+b)!D\Ѐ0H*6PZ>I^[dyvq]ѭ6`0]+PiQ tDYjBqb:mhkFW [Sۤ T9i=bpx+!k 8[,=>Hb8Y-Ewd : Dq9?  lCu² #{Z(uiMA] -Gw5.椷 *Aqp\{8@{v4tubS^Jv=GK@)AaB dnJ}@]T\*kaʦr! b_)=8 B98%в z:Sa`p-*R@aQtΰkl6u6|TՄe* PPԙJBTHJMPuX'hr +sdF׬@ `:`'4B@ +hKTZ*Jx)A' 5vNz}ʀS462<10-KCFM P?kՂ XrcxҘ$!4&`NK WU6}xBLN3qv,A]3a qӫK^.Jb%?)LhC/*kJ6sg25,Ŋ4ृt!`LiՇkQ,mH'+xQi2ElAׅqܲm;stH߰J)*,iӽ#E,eMvXQR'nmP`` c-)ABttL e;'@Ҡ,bdZZ*9v=-h[fFi-6@f(JYp*1P|RA2P#"1"m.ލݻD?ءm Nh.׮6 ]}iYդ2JY.p66jPC0-LD[ Zc^sq38uZ}mXLnBc;9l~g⻿ھRq) m}).%^@@3c>zuzd~*(&3:B-9opQ0+-[VacƼ?K ,kC366_> Lrr[qa.0Nb\rE75b6yӛޕbSOMb;O} |*gŎsSt4 JH( ح`k+؝(!H Jw =CNv=~(A)-oxquV+J9gD/}r; VE4"9U%0o8c>N-c(CP'UIHSQ!* '?:" IiqmW]+x (FC1MXc5H㬷,z0 Nl(^^#6xs/)p^|'+|֔Yz߷ =ZJ`cŋdm=E,^֥x -䵯9]Zs78-x2jO:\Xs'1'~}4Ih$i͚1:丵oIҌ ew[pۂ)N;nfl 7^'>%X rb5NІ32F+nu ]Ny 1@wLԍnUˇ J2ћtK tD~z9!ZkR6#~ ss=b8Ϣ[L1ݞ`uoGtK9 N]xݰ}z4-մSDjmhwPIV_{.Wo?K0n~ {瓕W/Wt;}'M8L;9'pȋ_EYt^( wYU{?ShVu FK@I V_oy lG\*ĶꀳaS||W@l,tsNWY9 c!t);—Sނ3_Zj{\ͯy {|_o''~Lʲė9E4auj{w eSF_pAJ\ ;n;hƊSϿ.a6; D͘x l=GI .)oO|** ycw227@[l lMM\PHF"EKޖٜEK[F1ot6ڽ7\Ƕ[=yN9$QҴk|/,ڼu/ .`Y1: Y~X>^{]9<!$W%g=h12g:pM@/s{ϪkoBC+]5tA,\s !.+Ypa%b4e JhC_[p 8T`7f٣\}GaiD19PN9.Er^ks;rŗܷ@kS<-o#j&!Þǯ8UNr-7s_ļ9Mg I'~Ţ?wm@k\tQ>wgt`= \;n3K?H-wn).4{{'; oxm x;'Dzw~DޥJ z!TpM=/8P.G-f6ݩ|w}>glT`Zkoy<,pɆItfV.XF|Zr- a齷λDV Gp2NOsÍ7Jզ9L|sW1?a%twYt-=o =*F%믻ޛy&xa{:>s1o5}7b`I;htWeJޥl=78`ut>k͂"K6h5i-[ƛ cx`*'"6 s7*:M `"'(ҴT, J,~]qӍW0P䕇ij[OTLXiFt9Xwm9k(V{+j8PգɊmmhOWyri*M*ֆkӖ/n.𻫯c]wg?c?*>xkC ?.8<w|peܟ 7_u/9`/n27Oc>|K'0Y1H DFQN;vi]`0C@՟^QVq*$:ߡYDO񆷽D4 n7P]ėӜ3}+SЇ>Ǥ4IT&#K_.OgU#$#]Lcz*ی[XZD!uKro5,]=;oI` c{:|+w^=/ cԀNxrOO&{l^e6 @) ØTh\~1lԤE=W_nU c}7hj}\}f9&_-; X뮪9WRpZAD[k2;oۮfVPU:/r iӢf(3?_ż5gUopfyq5>^qk.h^SkFhs0 4;l_wTMXKDŢ&csXp5vMrӥnĭq,yuo8m G}3˟b~d 6O=wU$lK;tʋr(3 Df _& PLWmt6SPe_;&x'1h*GslɆlFs7Z%w]϶lȈatd=-,y_칎NTInJ0p9s)qٿ ʁp?e*E_Z:R9Z>g㹣q|thU$!+ Iܚ!1g= ƌ4y"5 qniPx᥯xYHӴf!v/-A*jFSYJBkmhO8NY.o;%>o.==KJkX͎#މɹ矋I2gޙ+*,@\l7x<)WyԽU.z@_&/}1R Ŕr~\9g?Eޭ&8?pn#HI{"1¡/}%WVYR;xMXsު$r .<ǻ@ƛ… Yuj`7""#B]>Ä$BtY?9 :1AQ8"t,Mlm01$ݭښ%]FFF0vmG;M^rF`|n+qԤ]&;rE1gl6l.JvfZ6uzz9rET*•%4. H>9>|5e>UKb(%c[9Qe -_2+ߧ8cA+n(* * P " ?xvz3*Vpa{MYs`!*pvtkzKw-7\=zPpK7(naUXkt G휔n".EiE̥r.}ڛ Z"rˮԎ0p[ފ\#IlUVQ8Wb,dޚK.-dr*U ݛ%Sۆ/xR`gDj 03Z8G1)}Ӝ|O !>] x0|O=昏~k-~.ϼF%Y-/j4ep|򳟥5 ;wJN9tFU"#QzX(~p:%$f.-?P@8ũT ޓXU/{ o/#u X+(#*Ыt;6ѲS15x5Yi@ w8iZk-4+aݵ֡EYtN8}լJ];=L4HnCX/3hpP~/-drY*kn*"KL{=9v^C~sEڪU7[oؐ^k-Rmq5ק+H $l6LfwYlƕjPնM槡Yw<=V[Ɵxgdt.eh?꨷N(fG5t:Z+6[]9i`UGiGQ D _.M J,'ut8jhD#ׂwق)4<|UhNggRxVv~=οJԁ**Ц!?C+9K0F8;t2[]tR[\?I6r}(e֬tРH>U5Qڒ2b]e9V:zeyPx/*Շsh *2>o ':T]/vKoC|ȷrٿ'P: :8";Φߣ^\g?~vX=vHM@YO$FcI Mi5S^}x wh0@K'8inR'!Kj$&Bs߽Xr=,^lB%Vي< /%M/ }m!z۹ƻms~G[սtQT*}9!wmhOXq,wZya6_ʎ;@k<-|^P0sB3j$-hgRknѤp+ȳ6HtD}~q9ph `#[s)) j\seyqjEg,:^!;RQ$ !N܇Ѫ{(>+csUd}$Ơf6c,^vy8/H>wʗ+!ꛮǪӖxH8I)\{W^f4e99M /yyނP zMe۝A/p Wڜٌ@ !_'wp Ul<~ eQr~cz5Uj;V\x..yM_`q4ƒUBt f W4Lʷ?uO毿 84p0mFcs5-//:/[ ıe, lvYm-YsqgAh*üMZ=}wFkɅ.= q%,hB|atZjx ! OferwHa8 6)n}M򒷾_'" "ADUX{!2%"mq".9/ qC>vwwW(rLB2gpY,"o~%sIWh$[*"{ϑyD22 r?TZd"hC[ȃKDHqlrd)"RHޙ'l4Y^ɤ+2PdMi$,YYZT$Rz"Rȱ\|ֹHQL-M6* #;xtH/I0Z4GW?("N=5S&"3k ŗɞ Y$2Pb6| /=SavgĽ"2e7%7lrU D\!""%HIlYJ䁎Ȕt&m(`el7\%{?k:9طs鈈-:XN;RY""tE"X7 DƷU"KEd"/;;KI$ĕq_ċ[*'}$]]xÄ,튜LCg{(H('hZ_ݓ|3/plīi[|I b& i2/~!:h]*K>ȗ!eM;GȘRf͒BLd\&S" edbaʇd]7&6U"Wg' }R#7/K"e)g4W+eVT7I)쬟ˎxLJ!x׽RDr)ߒ&RDȧ?R̷,KEĻB>KdVN7dLo/)[l$$i]S"z`]!9)~t03%"aJ>wʧNLH)@N! aJ YcUH:{}̪&?9TyK)"(Ez]yګik.h-OK^%+7"wF)Xd%" "wcW^CE|)EڥT^(}>rq'j[ fD=,Y*/S3PDyHrT)NJrH\w7MDd75eNH.".L^-f_HW.L?jɃ˼E䓟|-./s[!'Db!oq1r߽_~YK/NB9?*6##e`c(8MGD : QE)`uH4H𰚗27Vm@%X5J{O+Z\~ޅgӇJBkZM$@փVSFl@N"1t22o d)qY#Q 7 <1,H U)#ڙc4_V"@.i4sSJg^¦ǦV1q$qdT>@pHTcU#`&UVO#]*q=,%E+k"TMhDl^߰)͐xT%kh4|=w'yK^]csRU:\E:yTVF@*UjW^VFx 'GJeT?%/&Ҩn:WIXDj@QuBC ׅ=ZN=,fUP UR??G~oI?BMFsVM6BѨ-JZ{#1 Sl j]1 +6j4yjN-|Iڄ ;MCQj o( =ojAIYMX4hE_r%-C9mMY+3*ѰRcui6H510~sU\hSA^$(VϡA7x;N6^o=AA<Xts S$u6B<4Pb)*/hV%It 6 %Y!d"Ncj] lEYa R%>Izלb};yX)kћh9c[R`ߟRJ~=7 mhj7hhjOצjpڙgIU+#߉*麲[9XPeI%~P y<EA+s1hk}Że_4XJ^ԇSR,JTK(~&Hu!|u)L$/)wkūW25籱(bl/VfjO?T] :F$J9:7FLյg+2oa֬}4I["DxӇehаEW %eꤖn`PX#`eaqڨR Նpe](QֈLR +00М {͋j1%\w lV[TJ&3PD`"`GQxtpT Om벳k6U4ģ`SP]VB9P.gVMvRdĪb髖7ߨAIc>JF+Bjg±_]{+kR|֧rjVGB3L3&̚T?Vf>"6Џ#Ofiuԃ 0JE܈c )߃3N3 BE4C[LG|㌝<^˯C?}/y1`a+o@usW܌13ͼϙ2jFgr<]i-<_X>TXq^ FX_+ݗҏ/տ_p|{__G<^@*YAըdGcf~is+=[JsE_o(A^Uo09dОtJ?To _8lƨգnGHy?͊c܃zߩx!3PySv/ǾG񟵚9F++>W=|0_<]=|Y_=pҋߣZϗG>?C kT?Pǽz<>q~q3+YLw_sqc^cܟ^iT`7=Ym6 mhCІ6tz6 mhCІ6; mhCІ6 ޡ mhCІ6 mmhCІ6 mhCwhCІ6 mhC:"듿;Zc97~Y?D>uxYñ;ϡ͕{bhO4\[̻ yzH~@nn<χRxI ^Z;ϱqLe|͔eF1t,o3^1(lH(cp?-"eIDQsoy,kV 7ppEOQxM Pxc8tzFĦZ ZN'e9x}G9R_>UWO1Ã̤Z;O !<"$Id8HObt:Ak=H ɿ~ܫfʽiN10EEQ  lQh4V@bEe`]yZ#"y/Y~e`o/b8$A@O!}zܡI?No5<QeY#fT8!ñ+?C1v)˒<ω_zhcLʎ[p+N;3E0ldx2X$MCx?f6!nx>fn8k AqЯJ ?`p&(~rkre(zoj}Ǩ(asӓ0Yt_Y;SB,AeehmEQǬ̡;?=ua:B>W;CD8_;DՓ1沑@P4F@Ix+yϪ6 Paﭟ#^~ UQ.Pc"BB\ʭ83+ԍ.D ,3&hTTce{?1cwٕ8뫚+9P=/Kџ a AԌyZm[gmW1.x$ uT/0z=Z#n0g@$T1AzD 5ﻞQ57sc, 00L?r^XlUHJЪS?!z0Z7W q(e^g 7xsR +'F@j>XkT9g3<̵nRcoKLja%ȈHUBOI"$pEII5 Wd( kc$(JXpΑq@0S.ʀR"/ AAPDQ2KĄ2)*C$FJ`0PmC(ђa("bBYcrWm&"S! Z#tYHD\DeLb:@hJffret4E,,hQ xtbXE-zD *(t4 |uZSJTkj /I\P1#X'PHDR"Jb $!#PPRk pJ[T0hաJPaƁ㏣Gs΄<^M #<‡H+,$X&Sh 5&R|Z9bCAśJ 38hT0a1)L%-C NbPYVU 9AZ4ZiPCMЊBy$8+1#њ,/tp!#Qą#:kT J< bc QABjt5 :uLpK4 QPOPUaDcp|;ԑ0lUm}g,2, ʺ٦l295]G)^ai4Lu&/z4 ʢ:GF8Wg$nđ!%>CK"!I"yӮ*(A(2FLaq i٣ifykpՕWNӴ SṷD!r)h8tzOp Fbɱl3B16k "CdyoF:wpv m YEl%%dNہi h|uHZ0l9c${Ґ2g'IZ2S#h6$*hrtg'z3K'cl|.oʋ~./td(}բpe~v)t;k93֛DC/8ڡDa| uX;@O_̈́!G!R0'XqJЃr/K.cy/ 8RlUUW%M<؈`2"X Q`h%2dtŲ- *!W";$#,06'=@3tsuԃ'pU~J ݈Y>O߇[oQ#,**o}; -YuWϫ^,2_v`.Mp/O?^Ͻ}*|A<Ԟ&KR>`\IxSW[*ȒWO]zhCGd?^(@ J`:|O٦a&I"|s{v6L,^*¼yP5nsMfr1ǰ֚ӈY&^f>1OPL&:Yλ͎M֞`ֈonpPm=\u\M33vچ-ᩂ _ $8GEI˜ +Yً7ohpEC<#00t&#{oxxk7Żavm{vmGn\p;1M$k0+8ֈ("B8?x٬2A{b8($/j>̖lAP)i+&+J?\PsYwy\K%61sn:)os5OL+zp( VDԧS:CWnr(Uu `@,Ql.F[e-rmm O΅}#sOIÜwb.6~8B"JcYgqŧ7>?-w܆\ɘVw۝^c]h3;m)/>v<66w7,Le]tl( iDklvRSf,o7.9JFCD6ĚkI+ОMxDzQ1q&niM"cɲ,!2xNNoY{uu㵑pB2f]k}y{_ƛ0113o_/ornNwX4pG''mtjpMz ׿#zD(eK1½%+`r$Q}w/秝3@31)"l6qYu9 Vd9}tBk|t p4@=~lhc@UT 6s_8oss:XSt:T5aIq](Y]FX}5( GgK\|Ylv3xϻJ3ߛeK"Xr6[eK~>fcϽvs]w PLx+@2:6^7\z1]*k&U.H s8yPdz*(˜u /{[`()mDoDs$IU ,gMG7o4Z j4 Ϲ?v(sui"iB(K(S&2N*& ޡ̄x`U"n>sZcu2•FZc 1mb*=-!NL)BDGM0ij *Lo~y.Ox;F1Fp5%c:5 Qd($ʝJ),0g*LNO˦@]b&"$bhۼ5o㞻/Îr-w$)}`Q#EtE4la&HtaDpM%#tr87ߒ#.zpЊ4Sⓒ"ݒDH;i-kߚ,YK-4Mi-馛xeװnېu|&?$2B\ xv5J8e6|t\w|'z3B#I. |K#2(LPbi}~oyrdzE~6jU;OX,YBl*I#v}K9+.w?0 }80Ô.0o5\X `NjmI[(qxצa"*t]DZkhcf͝c5:iDj䡇p0ל!D6U!dSmR&qaXyHz(AʩyB娣gxQ$(I"x:.ےƐX):]7:W4jjޡ o9QJ 8WhP͑y*":XE UUN:8NH꫰J)T# l>l2W-{XIl46 }'am c@iOh-f֬YH#6iTq9R"091E}yǚ~>_pFi͑Q WS4kcFtoWiW^Ƌ^">5F|-1јTcCF2/Kr=gxځ6sRF .Jp VupGQ2A{f3f$hDnu + _*z* P;$68m"FZ1$;J2Ƃ䟟جQ%YoJ_ "On'Ƀ(ś4Wy;b͆E"$eյe[ϗ曓6(@PM\y贉cKl-0$@rL?Ny@Y*n]8nIJ]BgqWa`||q=DОj#!n =ץp=%MSRL/c`띟O:͊; :it}Xb\ru:ؚ02#j5k̫IAP2tz6eë7(T05hQ(\NH&AJW11(s@",ߋ6R  Bp&IZRՙlц"$!U&UfחB`0ovyfHɲ+gTɚYg$irss掻@(uT ?a8ggvwoFVIIώoCR. Ŋl#je nhZU1$DIAi"xo瞅6. 򊗾McCFňI8' /}kc+1ZHAG1x` n9'XZ:' C(xI1|Q"t{ĢPAc}L>OӎfGD#_`ƕB1B+I&Slѷp"~uiN(`W|}@b(!1^&⁻b$DbCAP: rS5 ӊ-an7WN| :9ئ%; ^57؊_757ͽtk]i`BmZF E)ޡJIe+.ڛ4#d)(Dz%K~yYsٌۋi4NYIDE<6RDm }5t6ߊ̛==9_w s)cl@ *xkbO_E+oR~퉉"`2f/8Eh@&x(iDYig 21 e5YQ!h8I\0kd5-)h4\џ'2 R: /s/+MIXʹ@a lvU ʢ4Y4t6XZUR ),H!x\iir; o>0tvh e 'MF'Pf9&6O >_>C_+.@l5-ڈAC4Ka BɤEӠ4fLW߾]>ϳH'V MsEr~#%,4J qq@;Y4MB7,H@^h\N-0:bфn P2( ,:YN&A:si}uF +$ 5NՅPUfMh6!nR,FʌCSVM_ůT_&5Kjdc-z~ FkFMD鑦-2cyihF;Mx>AQTշvyTR >Ucv::EE{ё FBN.KiOZͲNtu̔ 5MWH'rb%61^,Z]+yP&)Z8l[Mi¿%U 7p5Ϩ 3CGZtթ]cD2.EBL !FQ(lx-(]f3p"#$8&'-Zn1:`>)`SOz2>֢ۙėmՄ\k~@Y( yI-fZő@{O #Έl c e Iܠ^*,HKbG*5Y%1iAF؈UeY4GiJE $" ַ#:fD(i#&m&QY#ZEdI16B |(Q!W`tYb-THPXqBtRP"ZS K=1qH,AW\]3,b yz'c S(Ui(bK |QVZ~/<0:Ki$ ye4F QQDB딸1 TRf9iPt V)Nr9Rt5ʂìTkc-iC˰eH,pĻi%1ͱ1.%2p!gRclEVdD& JB0I*(LyHEKJ ְ YL0uz }^"@=QQƒ0VW)bFj1!M[ BtŕR(r(^ZW~WC|NhFKA+E+LGVj@J|骠Da)#6F)0Qz^]*Q85JYm)9Lv{c-^4l3-JH{HX*wȺ<#RB)SC̈7=EQH -@Oظl *&N1ڽ&U0JDh4Eg:l8C𥧛O"]5VN*H]M,LОХ~Ąj4q.dE@DB y:cs:FD$ fBޡFkBy1:> d0^F܈Vk`5%_L,[HAP5#J 4F q9\fQ ^I51e3t9,` cl|QY6"VYmK6NLpgXy6ֳI\`U5 MLL):ZpeA19QP*MIL9B3:$˺4AETL{FF,]2͚ -vPRjIO+q*YCIJ'NZ `v aHTWBH)D6Z *kqP=|(ltl*N)MD+h&-J(rǬquiE(+5E{D U(kP֠ +I(}z̕Z\^`2uH3IImDDPAhZ#c ' cS4˙36FBKӞZF7+F :4IQU󳬨|\s?E*e~T(`nр&.,w4 LyBSBۭxy'W# 2PH,VbjiU. *fb`4&Jbr]++Ft=ZΡ Dc"E"AJin>xIJ hkqlԠQI26:$"+!RDi*ʴah^ˊ^UV!6#8e)DX@ڠsmg4lpbRU=B 4!xh,#(K·)] k5MXy)h4(LJP1\{9-Ć(2|,1V{̨KIa0B)ړ6$$FF,dyK= Z;&p¡JFnj &串0_+ 6 ##K(kZqNgrnu8Iqy(`]rt M畔p={&VLg]Td60^ƤnK,#ijj2A0Rg1S':C'|#;x-"U)KSa$Aj$iv;j D 4M) #B@i! !8e(JC)3örFJ`eAתU$glIȽ= J5!8/tc(Q4M&&IS˜٣ʒ9͑* 3==]B[s*Ua}R@ȇ~`fzm@k,CI:Z a*tGh e1:2B˫HLKl\?zl֑bښ*=`rC ^J#*xUVnw*P(@L3>:k|3$ Qy}FUd FtMqAيXտuha@W[ o>걗9O(ևR.Ykq=_Buς[(:QJ!$lh?T{ZBTU% @z>U'J땦ՊTyg]_tk׏{߻O*O Z;pxsO;˷<;Z[8PVlp43L^19f@-䑿[g2az[YILn(4$״"c҃v,JTL+gYB^%fwt( u^?dښ<E1p.A,wtgRׇ [ZY5M5s/uh[zJשQ?W('5M("0Ӈc<eLgwʱRRnvc]wОqTypzzd8q?<.룼寑P Nf\cQbяeP7O4J5S)fnG3ȧ5s( 5`@V(VY}ש<]y,<">{OYSqo[pw ʲawhĐǩo(˨NfUJndcި~I[I-vZrDl Ufv ;~=[9J,i%@|1>~~/#Ya->uOI?)C܇9$I2oj\o_Ǫ7!$@ox+캴Z^U}V_qMLw忇75:r ^G$O dZX:RO h̊U5h͌3To$lU̲1"gyN'`B,WBǼ?yb9P2PR[$C{(~=TajG ) `QryOu5Oac犢XkIԇCAVX[^["a9f^,$G8>Sd9I& ήyL:hv0on!~:&SɭVknʢdttɩ6I@)GjʰPe.s&eYW08PM,ˈc YQI*{0 BkԪJbyt'+|& ާ|3fy57_mych< O@ >K I\^,=IetdY*ǰh ۾*xzz4 Q0F199MZ2o@)hFQ5KaCSV-0ZS`@iJMH|@Ȏ PflЇ;8:6ЙeZ=xh+J7IA#K!O`,wph0)VثV+Mtm& hmyfsdg8N*k&졿1EA'H eY˸Um̊ Ƭw? 4 `h} }Aēq*MJZ4ejr((#_z .WNe&^[+e.GuE %n*I /z3 gڝ^@DU VdZ$(d @{ 0:2#«'{G3s8IBeE1{l_ʯ185t:PZ#['{1Ќj_^D7 mhCwhQHӔAvI҈Ng)E6JkGGi4Z9] R4,/ ȳ =cEyZKUS/h4ȲZ.-|A^z)HRCdSx(<. ⪔ (bVoJ.J8NHQ3Çи~6,@g<"SdDc"}8wF1;Fcwŕ*tNG Dʈs0&`N |9'06`猍s,! ;uWQ ߹{=N=I'^Uֻi.R^9GѠ7%}i@S$ԇPPkM@h-YnRZIZE yN%z1:FuzG?9DcHRRM1 I̓CհJ?x!ЗթB'0GӿeOdÆ 1z@6ۤy :=hjHPjJZVI)ta]!E ˈ~ ]tsslͫDJǣ(<(^_~-{JkJS#1Ew%OD˯ݢ(zredY^gGhi=( ZR%KcBͻH7c&i RMG'jv:#e+%pLpct:Я4 l޼ \,I1uBd*V2k "IȲ*W?KO;#<xfzj:1,^ .-[`dU¹Z{\҂~*~_\;Xh1>|6S*[ /ѩZߟf9{R8a'/Xl.En+Yu, Q Vf+vfMVƏĂKDoG/i=g^-y%Ƿ<ϩjini6_ /fIZǕ; IlvC`$̖hJcJ?V&w9s&B(Ou?wY˯눂W_oAMctQwt<ټysn=y#et$2T{-1\^J [kBCa+ZyA0dYF50&LK t1v؞czC( cKi >N_ W*L_z wSNh{n{`eZ'\4yޓ6ZjֲvQTt:T**xi4lٲ%,h C4K$HUj4c5…8ȋŋ7?a?:p㖠 3 ]kߺ;>/+zJڭ.Z:7sγt[=Y%K׼Fw'M~C{N gۡ-!kYx!/˙Li?n$F1:FǨ;:PEBkHdo7x=ݏ\Η?dxs13ʇ?ix[;jZL~:oԨe.gEPO;~\˛n叿Z|#387_~&Ӧ[ E;whLsW^BW 9!Fz aG+:;ݨ73KYt1&L@ 0DS?SO球!D߇;~A <J7{t˖fNQ(ýJq]7^+N8J/qX<9_Osw?\<2!~r׽{/+?|]߹?v5[6pm_ou{{O<Ҵx晕2c&'a\~D |NdЮ6`A …wAJ}n<0 tHkuK[b\D+}!7]~s4g6ͻp6n";:FuzG?DIs7RƎc񊗽z3m S>W-&LIcJtz{nXy_ƂE !|r!ozۙN6i{^?ބuR$G4aRAѥRB '|"~ Fx T'#:PIJۍX!z !49S.8gLPSB7Px{py3tI,n\ I<-Լgs3W>qۑSn%'(i8Q1+$N}-Ʋ> tC>ġG gr哼2n8&LYz5 TxkϦșZgq'!}^}& C;֬^ w~'zN T1Mp.hkm/[,b6L(KwAn Xs=rR>sMnC3:yҥKr!Q~QCؐ)Qct: +NzVYZhеkV=1ˡƪg;va0RRdL~;(!CCCA4M(yY82ucU7t;Aj,kJnZ7VxPZI9DT $BH ev$BU4iuЍ|%M /jsxK}.tEbqŕP5oppGfSmVXAEרe}8 Y]̷|rUHQE sfMk1%sKHw@m3{[ofӖ~Ҥ) ;EfKsogδTDPbۻf?RfԩAf-b)0&QRR-QR %|#_^tJi{@zA&+Xd2pxGmwt1􎎿1nocBڿ͗B$R%:0n5]. _[oa; GV %{ a8֭[GJRW8q-#V5,Z&I<լBl7q(Ooٸy6Zd|@+rEoƒW$U{3" YOt<1~Rb5}AO23Oa ()?^V:CLENӴcZ/Wˎ` C r̚9,l6޳pBJ 3}8qRxJ,| ZmgyjiibsCi nj?EzV,X=U+f} 6Z#5 sD% A>{CG<#T QEO+wرxRQ9R+!+6.w_HK9YzzpI'B;o׾qI/0/}iS) zoxz8RC~?/:(w̙'SWv0G"`Sd b#᳟,PT5ꏭRG% 2xW}Y`SpH.w8؃Q>2.{}8߻Jv~"89X߸Էy+e~z3͎K'(P"XPWB(?7Jvd]ί g魴d?%p҉Բ%qqXISU93p.xo-x[U_#_%L6'Wc;oνڏO_r cfȎgnS2kgfLقA0؂}%lL k>\yU8iW 'ur]U^ rОsXpk芔pM\կ`9 8`/w9cv/$1:ޟ@l5lٲ1ڞi۠.1&1nоvg >} nQQJ=ImF7wK4"& [iDڅ_8Zz p r&ߞV` E%x dEnjO<^59մ~|gƸIl*Ɛ[ɘ6xN EwhU(N(;XBWaB`F%i s2)-@P.pNU"'T`_@ʨ6@ q]ގ#^C5慣lPmtxrwԕ8T:lnщGA2>6T]SPK` TȋZtBXJ0N1D3Sl :mRRH7SMnN~翄l? C7ɩ rgpyNk-J :,)PAa T Xz|BPXOP1Bh^mmTf+FHhR9PU$& T*)hq 6wZ$:4ᝠ]˄qѿy JVRPBLA:{HZiZ!m$:$6҄n,Rr_PKk<54 <y!C0g6V[cP:E K-HB#喴Z'<CD8,NEgŀTY4 )#y: 3s:t;jP 9t,c} ;9}}} IĦR :&Fu= /$8q$(C4#BǶ-_]{m %vlhfտَzxvd5<o\X F&@) GPImi*'W4::FH?QbŊһya?g*4#( O"G[\jp(ogKH>" F8+)yXSx4FE|킔E`i (Y#%VHluH)lR8A+VfsI-B&:%;dMXY[E|^]B|mVB[?ylyެpHQNpv ^ZxGcJ&Ȝ+PAV3È VheU+A;7bLcm  KYh{p`xms3O- hz~K:(;:FOH?$C3(j'b9FVT^ m<={ b^ ?)ʉ gAx^!,8=5\ ?[x68^Dy5$Q(>7-wuGa-@ ͕<}@${GDϳ !zw[Y|hKK/C+mK_ qXbCP4 .ҍ@oX '= Fc/"Q{8H$Ico-ſ6ήml /<+P<7Q_!G j/‹h |Dy!@xzHmT!CBM ?OZ' ,BD=چ;"bTJb²;2ǻ܅DiP=C)i6:FN$DPo[;#5|qɈ9<+J7\/DNX#=áoJC,<לVs#ziXZ; F2:>"E~6 T-?Ozg{/8Y/\¢SVfMRjm,Roc v׳; O=TF{j)P YR]zNG/B F ZmӉϻGZ(kaǼ5Wrm#<Ԇ1:FuzIP.)YЕq9euІ4V #ܰS%3i{qyGhFvݔ Ợ #Qz]Q]IzW nNG4aGt{L.\_SDrO"JdX@R4 G騕}kZμ{Zm?;"*2\I.XpͲ=rhz:(\kٛǑ 6jǒ8p"uR3jx3ctQjtvEQ'l4އpm)6'{GELP5LJ0$z`{"Z!@Fn2G"C!Ș*>Rw|9i<T/&8ٱYLI) $[Ub3.{Vzj&=nFoSTFFc:KCVlsEjo5m[8}ﰷ ~x98t(q GnD/ɨL`=Z a""*ؼe/s*̅pX|&.FdZRI3)*VSu8 YF.e$A8^/ָ2uavYҽx{>ttx;J>Pr͈FvEG)vPzCڸHQ>^Ģ`x5 (LyC)՛J} Gؼ͈lgpyyÎx /x_xcds{,TAI ad7~=JP@SBo@Idknba-#sFA}NPy :%|t x s7ևݺ~^<F z#E<}l6׻7WyM[3-\)wm0|}?`U*fϞTjַB#Ɉ{y=p=DLo.̈u({zdsꥋI)Ooͭ-ڔTBɏ-^hPB6#ex-3bⶺ|^z_пya[lqGB\h)f鰎 ojo3Hr8-"@ćeшxG.>?+&b"]ox[ SY2<]:qi /KIm<Ȁ" ho08*W^RzėN Aɱ^!P Dw) C63SգWdxsls.S!.bpgozrou4mW*֞ K.PH (KpQIC9U3\Ķ(7GIF%m:;}W&|A o9t(l9V9;#TD C@%‘`BۓHD ߿6Ns{Ñ283&Aʞ΄RbVW5r/J{+\ RvAY#ƍaC#З\\rڀH?ŶV >׏q\V3hzÿz`j_ȞjH+<W}So܈{;?,|q=_Y~E|{iP9VkMAs<RLji[ZY]la;M h<uz cBVŚ5k0'X5N8̣]$^M8iH rGi6^¢HLh`faU>E0Q *"Y D%F: E6ʗ !}Y6Ԇj(&kAU9hY +JG{BhQ5@"й*Ir@Ur:8SqH:/@yp>ݑ8H %(eM4e%vD3i.Fq [lFVΟde^Apz53z YQExQ+b/ G hNɂǦ)'6ؿE:X"11HLգ16& x{җEb C O*o 0zm|{]U5 ؂ {!wl)BK^jR5_ Ί'Ei ->*:Tl(dMNDSvD(Cr.< ڋ(I p# GץǗ?S"Dl$Ax@2iгw" z!CḈ.B L*fEpcPeq=4\\w?yHQ訫/z~$Q H1N?*šS*6E!xЬ^,ޅD`uc˴28bX"#ד>ڊ'6%H<l ap=Kz&n?..HMm1~X4P Bi}LğXK&Ed| )$#Yr8K"܃ɸ4/fz%CA"~!H9t#xVĵi$Jć1vQt 6N)es9/2$$XBG/ۈy1u#^PpTbDzc[g ExB0S(\uOF.""zv83|09NyY>$7Xh*F /'\ROpi}zMXҨS; tVG90.j ~VD$lŗ Fy="8"?4Np~֭msȨ ո/3.̗_pƔn1HӇ.Ҧ\p睳liya$K$5apq&2a(('Jm{z>).f3 ¢;\q.L'hNVއTh%{?R^:ak( $91yJMa䔦150O&T <"+*J\إoS3("Ov[{);zQ/$ΗlLR=TC2zQ>=|Ŗ*l@2:?2|MJ:y@y1  {VP*Y @{2"6(6E/4dtNK8eP/} svu8NHÉܕ!UFI)[RY%"갹*B`]H":X:K F?EmA{bw MD<28HnT|@˜pg^!Q=DЊ#Q6h-iUR_bmI4 %§ "b, {*!YB/jt  Ӯ8H}8"z1!bZ9IKᐲ@\5"ljIpp+N8z\p?N&aMROgb:Sv0~DaI#72drr]ܔΗaD2ai=d4 t xog)${NW&CJ6ec'JV!ܚ&m0'ᄈȜ ˞ԕrlGcE/sV9vDZ~oc  6h@q˕Ê4h…4UP.\E4}|G!=w+\Բ/."?ABN| =. h]yLQq,ͰӒ(,muTK"ס|7!=k\lߓ h t!Xpizcq^M߳)ilN#>ʡ"j,phQW0Q# ?/<:"E:uvkp"ֹj|$zOir,NVT`GSҍ,D h^ּBE-ܣh[F@utm@\\2"^ֆ2BXi=Bxau͗r N/w2{L6Gp3'Yo"Ɣz2l<$Y}u6Gw(xPĵ귺`6ޝL<=@̇5cpe{oϘ @=6"1u!J;ݔ{!br{!ѫǔuqx{ٓEt1U*{Sت9\cH'vDm\B&fQ"B#hbHuM/1u)jlab `}^G(: xdGj@%4p֗ Nh B箘FB(<48DBlieU=l]@b,NZ(/L@m(SJQ݀CT[Mu+^z*}萔Q"Z<)^Zw5|]ZX2}kcU D!M!jaT+t$5s Q8T@$Xt<z( )"0jS$ =m*2ObSZ^p:TDspĄ¤8uu&"׽h'HEe+K"ķI(0TB@''}W 5 DJQyItv٦"P*TaTô ahPI j;s"(UJTS7i8o\a= ~8hJqX!4f6BDGtxΤa~8T'gB#e 0kDV%"+09"> A/J៏QY%PVX 2NE~2_YcGSVOPF%"*=˘e ى/œ"D2"VZ7۰gw9{us녜6uzNoلD|Kv$zUKS wIz A[[. JkD a\*"Rąed2t1vՠjP.D.‚["VU˘62V߻Xe9uVF#"KnjD2$r(S! Uf5ʴ/gB*H_tb#Q.1^/*r%r+~ֶ^#/FNjR|)5(J4BƬbZ)SAdo,!mʆ V!Mm^X ʕ]/mOCyS=YȷV\g qBbH0JRj"a\|PjBgeipA#,j+ !zSJK+1,H \_aBFDqT*uBR'uLDPHISmdC "6ZS6p+(IX6GDuB(J1l4M)YEA'Ǹ:3IR8P-0-Jkr9\ QhbW%**p@]\!3IU:|^z:lMP$֒$ ZPB5$I]ZMѥRе,BW)Zc!<4$Q%aPޓY;{7dƻTZm)ɭt$X#UR1-` QW%pZѸB#( dB9"\Ba]ϑ2 ]s/1xN$(D ]Ji)IA 1THZ6 JhT4;ATipiCKu)ph|FWm@ -,ֵqcuJLHHj A:PfT-2 t,]+@Wrwŋ@((e"'U.eb:I#u HX#UT U,qF8Tund\BWy Ԓ $ױNQN+g30N=eO. ?A.R P' CQt6߽j>نUW_ͬY8!Z -qE1zXHSUqNQ65ƐVkx`#K K(#/:|ZKZP.x߇5MlXN<= o{绰S.Z ")8Kd㰦Ka†(bMAV4m 6jxXkM7oa 9#PBjXIN8$_@%KpENmjA),xLIZSPlhLT*c@Jj{.xv3{VRL! Yڇw:R+82\K|ݧȬq[^OXug.3Σ?`?UW_͔iS6}>X6nSźu[8|#ʔiN$ԓ p>׌|c 4@&W!,eڌY̘5ʏp-}} ڭ.*0VPxWV{z##Ik;R8- ZJEN>stT*J)st::1$u&֮Sd!:Z-3ɜ3s18QmJ[j)y{}v<:ooyy3!P{pM(S]G*䦋Ԃ|kTBw?9: hkceݙ>c r(֮EhHGd>}{/-X)leYp4dYJ* {O$7oԓ^J&HpyN9]'tZ)^rKؼI+chnflw !3O.W M*6< .9w鳙3{=^~X#(r!m[UW\ɬәd~ߒUЉ[|#;/QyDFWc}Ew6J#T$S6:g9rSU4 =[zvnYJwy;g#- S8:!P"di7㽠Rk`6t" ,Rb!Q48o)rKfF&%$z4]d: %ts,h4u Bsxio5yr%b;hwUR8PPiJRZF{O<  |Zk)m)HEB%d5$RZE78^qg ۍCFE~{)4 Yn#U"T+  k*# $ K,iaAjcd>6m)+ ϧZ ί~Y,^ֳ`|O/^Ԙ}ʱ59i6f#8eN>-4v?&}^9xu cHX25J>ǘ&ctPMM0edI4MiuEݼ;rw3G=kw)V5-vVFecH ɖD*y[l"Ϸ+4`; lF6iRVa IPc:v~gpL?,I:$ :8rcV3,o[I368 mCEWcMxFiY+`=,as&j,rļGi:b{/]ŽӦ2yh[Ptmu42P6[d*'.d|r9D2$Ew:$oilni4*li>6eܘ t\BҷB߳nv&~ /cwq?&},^՞rg/\ƳO_^GQD8o3axͻ'Aw~ѓ:z٫>ͪ\RMM'5*ۙ_ax|Wqɼ@PѬ4BuzMyHbtidBe8j 1aXi,Ed1d)0]AnZ(U蒥" / *ӮalV>(.G 9nT4):MNZpg>֚8CclFdi=W*)ilW^s>ﻕ+w7~eyɣ?ŒǗԢ{g:?ȫ0$e Ο xeq$YM`޲gy'>=&SJIRѫn42NH2$'o $(<4 N84IC7n.4IXܼV)D39Eޤ K7/=dy,]Wp| K_1a9~?$j)21a~,˯: /rM)Е8Qin^Kc8H1ŻRkƏ@l6QJQO22ZljInAN)2QO2 RcAP`* J(T+U VslKZcբwxg;n" ܦ4":|ɪ*:JQ@ q|[$%MS2ཧR7n!S 04OQTkuCmtخVoiۼw7}k#o-}:͍U?ZEȂ6[<4,êьAuz)!!5m OF>>0wl>,[O;Pg̝; οMGAVXΙgGkɒ8#3got:!vdVHn;o_N"S,gMgwCO?)/:w=̘;Wq|{Lc军B_#%ђO>ͬ0mμm@DsZ%Icni(ɲZs Ϭ笗ɦh6T|+3{&sf`sk>Os߽wjsi}=s~IU5+OMoy3r0_gͯ~J;DQ75? ;**f>N9E#xY;n&?̳ L'vG(5v]ɂ0}~zAyNAr=wzڧ#fǩ9(ZԪ)>t?o}y=sA $t Gq / KOxw4 ի9H;LΝwݦ/|D :n7288n~/9 Cbo *Qt;$v`3Qi'wG3Hj)JgVAE)/:r2CctZ42vQB+j} 6[w/6?KxG}fr##:u7siw-˿ML;{7o5xlٸ`euo@;ް_t3M7׾˖q܋Nߢ(l ֺTjYJݢRh_vO~V{W}r*-;hG?AnfP ;-[xo2sL^W⽥R E%y}L7{ϙ_jH1>i3sTn_4@xta E5z90>0kLfL}݇Ԋ׽l^KO{)&k֬Ce|شaI%Af Y 2c f^te J){o֬YÖ-[HӔNxhZt;oTlsۭwF-#т[z g]t♿qyQ}:.ևt.Z9y睙3k67v Dd^7qٯd39Ù=myC烮W]왓wY[o+_tK-祧KȤӦ`ڴir$IFE(0#<]NCcu|+sqDZU|C{Ekg ?V>)/z1oz[αb29f9}ޗ0! V\ɓwaaY7i앯˾ʹ)מXSU|p'2c >20DQn/@$6l`sW\m>^{3gL1O<`yxj\kA[ʥxI'3e4;hJƃ>[q<_y]8Kyl o}C&C9r=8%NbD!m%MpƳdRvW<5E2{4l>Ug>y;7!nVxq?8}̚2]wg&fxM|g?ү~-$Mjz*i7\_;_jx;~[>'D枢Yp?qc)MfΚ翛3ZFa3Ը<R |v%7RjAZC ƎiШWy;%/~$ 4eEyء|SM- |3<(?4y#<+8?_k-XY:xغV =UK8UeSs}KkS%cb_}+lify-7E?.4_p$jn?n/eᒧ8C{+ ŃwW%3GFe ͩȨTⓂ4QEGd}#'[8sދZP4+]Gm5Y:g_ڳeZv6`k0i,xd A]dN k![#eGr8i{|a#t P40u Jf:e:n ^+Hm:YNX;Ppm0iXYd yvbι|?f邇yt}|wԧ>#ܗ?\unN8ƌ߁1אv!l(1s~1^o&SJ};X"A^86kp_oo]#䒯}[nvK^0n;ՠ#Ll#LxV-^Nw2Ad媵hE|`pp0:Td:a9;v,=߾,0m2j;ޏpw*TEk#h!9,Z4ӧOQ<"~똾\1nO3=񒳎/}Ϭw+{'sfL| d+V,AUXVRN>Z&eKAy|9sxlSwë"O|{?`hH~#טh9}ǹ|l2` :!n8/:tQ+ JS,\ɮ'PiĢlǽxo-J5Hy3{L2k+LGhAw#\_Yz&s-|+;ߡmcɯ~O~p%BOp~ͪeqMC}tV-W>+WcI+5j# L5sgo\A-?uu>˖·s>]Ē/_̥_}='?sf#u;#R!s.ws ŗ|y{uzE+/Jgx[7 ի'9͡A*iGyI;Oey;}  U\|yd׽ԓTtK Z±x*.r1g,[o|Rri<,^'W_=fݧA-st!y[/uo>%W񶷼^x.ցxElCq\y ~Z.xo+u,.Y8-f=́A} *: E3=`pҩ'aLN@ FZŦ 5{/pKOckivڤY<Wyv1pt]~뛘5}&Sv̜= /}$@ =+#UVu-p{A5xř/;nop}g!ħ>k^F,]wޑ JnKpqǠ>sbڮHHj4P_. /]BR2ze 4a`?B!^z)Lc_}B`⎻PՎNs w~GUW_]ǖ.-'n䝶xC6/sO*,C;3Re3i!=Y-ƒhϓ˖˔]"Epox#CƎ爣s;@o;֒vZ mOz&ɾégΚV~z1_ڶg>i:C ziT+(B ݼcsKU%$B;0nZk1{Ϲ4ƌ5z-> 7"d ĉqL2;8M[6Sdݜt$^|s{ -8={&}xE'l+U9SI*J 3es/SU:H(c֣uSz֯\O*zY|9oxO/]Ț+tv1gRxY1y 8j SN㋗|-p}cӞ 7o;;e7#-2[Edt&NZ͜q}?a;L%RRk^HZm:#;HetC۰Y(h 0o2r Z**/9x.ⷡs'?.f1q%ɇOnûSPӚDf[3<'p<_7y+௷ÿ, â%+x+_JӉUu)$5$!I眽梔;ءpƣtB1͛7SS;s-W-缝4Qo-o9-XQg9fݙ0fMU*ɴ=vghhlz E%Os=͝0mx3&tMzͲ/ytݭbrO:`w%aB)c?H0}$qv3jJ@gmsQGۿ>Ġ?E.Giu;7qM# 'd*g?'"CM=(.i ]'W]ƥhT4 tm(Ra,V߲G]s[5;3d;Sj ZZl[^K.a=A,ImvH3`|-ҳǴ)<, RHt$[ZmdFYݡZU$Rut]*ZB*;:VnCC D'p k?!e* ij̥Ro-b~jc{6IxI&:U2"W([$Ռ r!14I7r|a֢8C4-+Wס]$ Q1Th͚IcXRD#|/;_X o?R.xpQn8AZ(P$质VR$"psZ!V:#GcœK7o/\ܽû 4=^c١+p {PsNy-<%UҬ)RJvmk׮Gu EA! Xhs̡nR_etXh!HB6I5&YCVI5=qx[iSۗ2nڅp ?IJU Xr%glA 1^S CdY:n8Ug0|.{Vpw1}NB:x(! PTTDQ yQ* ]SP6RН5-k]Ϻ O$APiwwY2>3GA#kB?BzȍnE1њvMΓd)SNSq! BJN9,xr1-a'Yx>^Qg<:A.Yk>~.E*'c;VV !=4nSV&'oɒ[PӇ5Mno=tJfMţ>JVOߞqGv;–Oq*TRcŪ9{*cAt 9v`k3sYdUPĤ 6q&҅T]NTYl;a򜁁!dR'A9nKZeܸq %mFU=j0}Tu͛Yz1Ym^QOP>#wn86lڈRzI&$4iѨ1fv&;ERȬF+7Բ [Ec?UV14B%{OQX{d,8cHBV]w&" 7ж)TSIg9|S\,߹jBIOkݟ+#ë:<< 7UzE7*X`Xtw~a]IBOR-ZWeۥc%} R)qE(lϟW^7q1{l\E:P j e䝰XrgI rZjBsŜ31 ( qNz!XqVULxRi1RjڦU>BgVA%c:=S˖xl/…c'dqЍɳ+0y`s﵃ʘ3w=YzEal9*wC9kO=wj&/2KE ٟHR &&ƶ3ydRai-.#RjVI-Q6t\fVEp~ò Y2.Z[F*5$)t]wFjRt;d^ێzm׮c;DӴ2!M8t,Cg%,\KWcO0k M˖ŋ+?$󖁁?Bixf6oZMO184𹧑X|)T萦 L!Ghu|_}}/ >OqYdKIjmSI3)y2mi7HxjB&U16ǚ.ӨW= /?^ϻ/xp+<sR*M)雤ɋg;vaS&ݵTyT ]lACmfq> w=N&L0\λNwb`3W_ē+pv'5]R$"%C,[ ל~4BQײFc" {n zTZ*#72J[O=0khhC%S>Z*:8B HUL7, [Y+L}ŷSp+˩GǷt9`!/jf%_7gSv+Kxv=2<cm˪Zk9n5UT%A hy4&^ 䣈 }KL"blh{ĘO"Z Un1^{sϽU%&yw}>Un9s1~suҁ! DQ4͌񊦚1CR-7}.5q0 ?(XadL MOy*?½||~t-s[#Cj>#o)TN)`$0 է)]%t\w]ywЄ\a311kjBě~,1.~ƓHKevIT#ff*@#*~~wo6{\w!J ]2/}ARhx;q' y_ (u|샿qM?S<>U-J[7&4KJGB% *VX\K57xwy/ooY}gV3j7P Ymךiӯ|׷QsH{C<[Z#=~k}_䳞<_~n{M+vƇDR`'~Ѣdžf4f%Z].~?}V*oI|;.H ! 6DqM]fC—~?o$c.~g{ Q+Ż]^ƅY*0r¯;W{{H Q#2[nO?'=3~W-9pDmìYWu]YIpQ{b˿w|;;G̬~791.4/簊8=`Znz /=,F7(S~+GT1bqCd?»|'Y~~ꡟocV<_qן"UG~ * TCS,J}߫J+Z3<8ij=3m9lpCāg=o{;{q#7_y'=@W-&%'+ ndhUQU50")O{?~00:ǿw'=a9xֳ>OKK}*cqgBJ988`.(V݈Y7Uŧ?:^m^u;p-O/=9/Y<̢}k0X-HtP?s/sn|FC#aGRM?ٟ[no?~?bLcB*WI3l#>S'|_yQ7sM7;Q-x[·V_y^`M!;>v|T|՗]yoUbLb[oL%/B vQ4^yS|LUCF vHzC??c||%{_GK{o}O}g=W~n?z櫿ϼVhs\Cyxo]7cڽbyӟ.|es3q(ew;Xnv}_Mᅳs3˗3>S𼗼V>!uK6r3~ԭೞT/կ~-ƶ-tFa_9nrrͼ'o/r'qm7/2~+Zl M6ʲ\fW+7*n{cݯ##Xc0Z-c8". )d4.^`>knO!0{?k_mxoz͝w8I*8g wcxӞ]!/V_5ǁD17g=[}+“y7s7q>Hg萶]_Ɨ$KrxC8<ܧ0IB=t>ZEL3gtN;R{}+_a7Hp1.d;p|o;{y_L@"dP:#|{{>:VO# ݃9a f=nchR3fH>$h1bm SQ:RUu=JiCO-\o,V,8)4=;hGH_E}Zۡ~UŃ}kw!53̲rM]]rRD!_:P[_ ڠFDX #feGJTJ!a5Cԕ W%H#Un%@;#[g3zG;1i@2LcĄ \TT|SZs۞#zV!-^Sfw0~^qV8R:ZaMMӎ5tB8z\$T-rKke J>888ket0nu]ӻudy#J%*1rlfP#\,UYG4*j"I86-bwa?A1:gW@ S7ۗ`4G\wuPq$tJT&=щ |*=667j-}hz6 XS qgfQ,]Xfp Vf׽y+?anڰK XO=K^ef9>gg!&O sjhV cBrFbB\UnIڙs4v4&D4K$yTnΜ}l=GKdF >qCc qa?]'8O+h+^COڭm~15^,ZՈKDD > 7|Ps<ܕ(*8^909=O45:`l/{{Ï?c\7ӥ8W_Paavp:s:{8r''tr`.CLz`<Ȟ?Bk!;8SmO j8-T-QGb =;?Ԛc},]bQ5\p/{!JM%M74u[w;=u]Cت1sϟcࣈ;eA٘oFlݦFrȔYEcsp7~73S&iFk뮻_e^ 1gggq"#OT1iAo@ȸ<@a~:L`y<0="Bcmg ˣ>|㜣6нQU9sh$wqG(e871f?xbv6WU$5>EC[4m;{3 +o|Jbo7#HyN8bi`[,`2q޳ZuTE̯M0 Y !0. CmvX9AkάF#5C?3ٜFÒzѱCTrvs;]$1UuKBp,[|6UUM]ʪ*ǡ}kz }R.VJa&`1k9::Q ڬdےcJ'a& J: ]Z"0Hc,~" (a$rayDHA`Hn֊h"uY)\p;;;R(Q$ӯ:d6 C =!,w5n_t@=cչsXr`D5 JaZ+aL.|s`:zCH j[UFqyvjFG۝tU$RH\,_2}Vs;{aJlTS Bɧpαc\v4{12co!8zS55A4nbG!ȍ\w9/,*I%M(ƈm*G(b44 >woC*ScRnEaU7מ·%s4 9vvvr#t]6ּ& )0Hg5(kXG9yGU[1:Z]!Q躞A[.bfsÈTёԌW̍{f[F,!1YVK2彠DDjD+bХ럓D=_ .ѭVYFgࡪ Qڐb$۶|䞏qpxXtlLÀtg(18FAZ4m 9 "ĺ|êa5䎫) ߛܢl27Q8NAwvNUvHgC" ̚=H5Bj"GJGfu \XSqRbS6IW^jĤ,@ ^<+{эH̚9ǫ%(**f$*t![0x=2H̚ }|\.#Z^`6 k+LeqnmbE[e 64ø=*E麁{lmr;I]gPiewDzmh{C'JDNv! Sv/gGU1~XNd9yi }gA=m[PYpCVpUʈ@G30v) }ckBrbuCӴyO.kELJQGk;VeųdŻ5tJ6W bqz~Ԩ\ ,hʊFnA~d@L)rSmN<=ł#9s9I9^bu.'Ȑ!0kIrBTٙt]GDZ+r-.+E)rY4n= CA=]">YI ì]LʖI-1rI"=%B) 0.^\WO߫ p`Y]Ҋ,mÜlnf0u"đMӲ\0mwqc`MSHJVL>\]%x0")R55'DU5h1HLDQB;pZp֘"7rױ_0DgGG=~ITDiil۰%TN=\}"$8>857HBѻv9>3k > lŋRY0,xhw9>ݹ=1vw+.9Y^$1p((R!H>xf~ph$J%mB;.;FjMq,jCb<'8 JUMR 1yf:p. 1W<1й1#)a]ad ;;ڼwL$]ďU}26# *`lrfwg;Gt>Fs1FjJY3GDPJ:R C?f1\J&C]W@UUE+ 60W +ؖ\OmVERb:F"(ƬMju84u[ZQU9-D(b kH :w>`ޓ KZT5_w=E)ȯ-hF)E2 }GӴغPjRL$vgAׯ[Lk3D!B7,;;hP0x+V!T(ш,TZ.8h[/R[bwVt}Gs١u..Hlj ҹ](]$vw15w$ -΢Eb5 e%{Ȭ#", &2#0Ҵ F+~ x+9:ZwFTVl!"5ǘV<>"$#58b+ Uc!w}GUYڶE9?(JZ+BdjXI (a`gg0:hjm?8`5U"cfaBRbtq]O )j|tX,I1 R>P 5|VHD{фG غǬ#C9eww4H1N^w(-\{^< yz8Fb٬HLkBLU)E(-HH I.V\< 0d0 T;88H ?n Xm,!F}4]Ǚ|IIp>\vСM,ǭjq'9΍~y] #hZ $!QՆ$PJV 9+T<$c bs>f% MGk! X sØl%#nVӏmdOGB|BQQRk~D} {1*D*;ޱ^dV0+((, M7dp DB8Fkȼ'V x10%ZgZc>#9lmi-G[ct>ͬe$!RșJK0w.x3[ӈ1dհ|@7;Fm FpVFv0͡b͌'[XvCyª[b*Ű0FMwҡkQj|\h,?>` v"1xiK`9tx7-2cG0=VȊ~ 먪txCmKUUxC1 1+h U]کKYЏ=n4UrRِ11 #)erܹsPUy̚9id;P55 8::5{@@`uEkpT1CslI`ʵǎݝVh9tMUWh%4UF@*j FNKaH EIerwH DT(RCO;o1@4X++*ՐF\ƞ(ΉCF Cݶ9*ܲEv05]‚%2_Ӷ-{PkJ}gfr@] vY魴臑J+bJ5D1(jkPڑif ـmSQr<8zتIh-XmeT %.rS##K$6(`tl6iZV%ue98@UD%if(eAT.DhyLThSƐsVGrC12:sy-H¼m9XXntcTe5DC|Ś1V<1Dlrqقab1duxݫWKACTHJcBd4ԖnR&?қb,Z"jF*Ɉ_JYp-*iUlyK(VPbhY|;:HJqr!jea;s9@e, |d)xZrvh H!2!e?@2RFr}GH6|5)Mf')+EsG4bqt͌#r&56UBrv( *&mrYd47DRL )mER֖1+$^̮Gh-C@6<1@3ksV3o۬`%ޡMErNX4)0Xa:BirE J#4!y͌"X\>rRZȼ#X  tŠԳQÏs{{K]U cDTN$V”hgRϮ]JubFΈX Q,Yuf%J mH);WUFb X3flg2c5C?b5vְ<:kjU7E"rB=F5{* m,>zBI%D@ !%BĘ%\Z)ĘKPYIފ]BB~:w N'* *|tr>|w\6FR %h٨JR90lsH$0H.W0 tӹ,Ycգ*a)|Rޫ1$U-ċ|SZ:+ $$[J)%xr* oW>G*1v'ĭ!dvP֏HRRUB$+H@B b$02} +(>X}ϐB$*T)U#7_ӠNK7C,r8T:N?)t*g"/)Q :ShBI@gYI@1M1S E9PcӀdq ql"T–NP5Rx>ᑏ|gm\f|6`8ef l[k2mj ӖRg:S*K2C' ߏٟz\Ge$J$ Zb*׹}2CC#%+:[u&' ܨ!hɧ4MȮSa); NONuw%{eZY7i4^QַΣ^Ԣm9ѫPY+)RA}L={rĉkws dU EB"r-b(_Nҕ˪ORx[dnӮ#޲~ȕ愥2dDZNy)0IᱵBRrJESDt~(+>^Ο2mnreW gLRqFdm4cʿl "iM"&pO򍊲=\'YHtJItE٨Tҳ:փ^UzH,Hqŭ(gIȦ"tFV-l KMIX5Bv-dkm*!lx >%2.Bg.!kjR)%k'A7tiqBPR~Oeq[KZ2m㉙Sn:͂:'$O ,԰旴6&ӡM.k)Ԍt Cj ²|֨H,մ^D)F\zo6eZq5O|#/J(QDu?ΦI/N/ѡ5t8#SrR(׷x^>!Ebٳ~@ĢK6wIgςYcF~ 7"d$zLRADXrQ FFD?E"AeE'DAZgDcT 1f/CL4[G$$EL(Rk\!nZe?Y ]+W޿3i҄pkMIP)S9U2[`v (`Tk%N'}(Zy7JfNHr PMvMHWR͝DM%8x.˽0JmM; [melFڝ}{J*m%g,'xQeKSg8YHfSh6#3)5'6]sVHu¹FJ'i~ >I᧌m}gk*r3;} kR^\uBf-ٲIW^$tJ(63ێ5iN*ܲ{ d~0Ԅfd(5QT?rtmT^cUդ}O=P2%NUeFN7>9zRm|-jHnQdhö|"a;BTQEMZOkQsi:5ZI8e+TQʘlN*(zo6|N'F|*A:fNnₜ܂ޤK^B)C4 >QطlyMʓlg)mY0& *QT'`4Y z:nPVImi+ak3ƔxJ/פKl#xZ'm/_(@Dn Rn -#@2=OSG/QJ^@.wMEk$Q Ƥ%-ӄMz 幤PF]¥3g7dr^JgNݭ}w*TОkROU$(YOHr<@c nO[ 3ͽ5Nۿ)m|(E~BYLFo]h}e7[RrJabOSLZ/45RhNhfBY.GF$?E1/^IvBt$ I60n"UZ΅2RJ|lM.8I*)Ks.'<\)Km%/mɈ4>6/hjc!8@)L 7BU^C(YS}䈠 R; Gx(P5eY >IY[t~nO?m| Ӧ1D5xU ٗ')-~"k&2?JxCޫĦlS k m2Z:%iزvǫIcZruoŞNrzra J3Y[<1 E d"^re,ko}Y(bsT=9k9eL,"IuD$nLkka)yKMތu'.[{xrȭ${DmN.Nڄ$@ /TRhEP*B׹>==m9nܿ'ORVȍpHHt ~iboZ/q-'ol%sj>> :!ir q%tT MP!}l -ԧx9q+񸪀0QکʴJ' ')~<03D'r۸S[I[їNx xSlu*eIJ7"&#嬌A PC1̦ E_9UB&N|>6h}]q\r@ւgFXD j#dƦ*VPE)W~7<2jۯ !<),׆r\2>E) XJsw\Hryoʷ3rȺNx SpMuoX.f" ILRnkdXeb۸Ki%kkxbB%)X灕W'9LF6ȾLb.1.XԴ%n['D)HWZgt:ت1!nf6ֳ1*׿)$וU|YR9, (Fb; ޺z}֕I&u bGmH'jYK$so!Z<<@#<"*>,K49sO+Uo SRO7r_Lp\BTkw݆B{\xHoJ kON@\wj;D4^RJS'vreOQ"PIIu~`o5ʳFԙ?>32VWI BMISJ>y\V '\]CJA s"gѧT u"*WT&g\n+eϗhۿKsjJ/3{߱ }_)N ˝O, IиֳNPK=^Hu98^m$LmYg!W q\'dn-.W#uY9Cyd/8F7>r'~2F|.eg?)Y|W\CAs㔗G@e[-YJ( None: """Act upon CLI inputs.""" # Quick exit if only scooby version. if args_dict.pop('version'): print(f"scooby v{scooby.__version__}") return report = args_dict.pop('report') no_opt = args_dict.pop('no_opt') packages = args_dict.pop('packages') if no_opt is None: if report is None: no_opt = False else: no_opt = True # Report of another package. if report: try: module = importlib.import_module(report) except ImportError: pass else: try: print(module.Report()) return except AttributeError: pass try: dist_deps = get_distribution_dependencies(report) packages = [report, *dist_deps, *packages] except PackageNotFoundError: print( f"Package `{report}` has no Report class and `importlib` could not be used to autogenerate one.", file=sys.stderr, ) sys.exit(1) # Collect input. inp = {'additional': packages, 'sort': args_dict['sort']} # Define optional as empty list if no-opt. if no_opt: inp['optional'] = [] # Print the report. print(Report(**inp)) if __name__ == "__main__": main() scooby-0.10.0/scooby/knowledge.py000066400000000000000000000127201461560455300167300ustar00rootroot00000000000000""" The knowledge base. Knowledge ========= It contains, for instance, known odd locations of version information for particular modules (``VERSION_ATTRIBUTES``, ``VERSION_METHODS``) """ import os import sys import sysconfig from typing import Callable, Dict, List, Literal, Set, Tuple, Union PACKAGE_ALIASES = { 'vtkmodules': 'vtk', 'vtkmodules.all': 'vtk', } # Define unusual version locations VERSION_ATTRIBUTES = { 'PyQt5': 'Qt.PYQT_VERSION_STR', 'sip': 'SIP_VERSION_STR', } def get_pyqt5_version() -> str: """Return the PyQt5 version.""" try: from PyQt5.Qt import PYQT_VERSION_STR except ImportError: return 'Version unknown' return PYQT_VERSION_STR VERSION_METHODS: Dict[str, Callable[[], str]] = { 'PyQt5': get_pyqt5_version, } # Check the environments def in_ipython() -> bool: """Check if we are in a IPython environment. Returns ------- bool : True ``True`` when in an IPython environment. """ try: __IPYTHON__ return True except NameError: return False def in_ipykernel() -> bool: """Check if in a ipykernel (most likely Jupyter) environment. Warning ------- There is no way to tell if the code is being executed in a notebook (Jupyter Notebook or Jupyter Lab) or a kernel is used but executed in a QtConsole, or in an IPython console, or any other frontend GUI. However, if `in_ipykernel` returns True, you are most likely in a Jupyter Notebook/Lab, just keep it in mind that there are other possibilities. Returns ------- bool : True if using an ipykernel """ ipykernel = False if in_ipython(): try: ipykernel: bool = type(get_ipython()).__module__.startswith('ipykernel.') except NameError: pass return ipykernel def get_standard_lib_modules() -> Set[str]: """Return a set of the names of all modules in the standard library.""" site_path = sysconfig.get_path('stdlib') if getattr(sys, 'frozen', False): # within pyinstaller lib_path = os.path.join(site_path, '..') if os.path.isdir(lib_path): names = os.listdir(lib_path) else: names = [] stdlib_pkgs = {name[:-3] for name in names if name.endswith(".py")} else: names = os.listdir(site_path) stdlib_pkgs = set([name if not name.endswith(".py") else name[:-3] for name in names]) stdlib_pkgs = { "python", "sys", "__builtin__", "__builtins__", "builtins", "session", "math", "itertools", "binascii", "array", "atexit", "fcntl", "errno", "gc", "time", "unicodedata", "mmap", }.union(stdlib_pkgs) return stdlib_pkgs def version_tuple(v: str) -> Tuple[int, ...]: """Convert a version string to a tuple containing ints. Non-numeric version strings will be converted to 0. For example: ``'0.28.0dev0'`` will be converted to ``'0.28.0'`` Returns ------- ver_tuple : tuple Length 3 tuple representing the major, minor, and patch version. """ split_v = v.split(".") while len(split_v) < 3: split_v.append('0') if len(split_v) > 3: raise ValueError('Version strings containing more than three parts ' 'cannot be parsed') vals: List[int] = [] for item in split_v: if item.isnumeric(): vals.append(int(item)) else: vals.append(0) return tuple(vals) def meets_version(version: str, meets: str) -> bool: """Check if a version string meets a minimum version. This is a simplified way to compare version strings. For a more robust tool, please check out the ``packaging`` library: https://github.com/pypa/packaging Parameters ---------- version : str Version string. For example ``'0.25.1'``. meets : str Version string. For example ``'0.25.2'``. Returns ------- newer : bool True if version ``version`` is greater or equal to version ``meets``. Examples -------- >>> meets_version('0.25.1', '0.25.2') False >>> meets_version('0.26.0', '0.25.2') True """ va = version_tuple(version) vb = version_tuple(meets) if len(va) != len(vb): raise AssertionError("Versions are not comparable.") for i in range(len(va)): if va[i] > vb[i]: return True elif va[i] < vb[i]: return False # Arrived here if same version return True def get_filesystem_type() -> Union[str, Literal[False]]: """Get the type of the file system at the path of the scooby package.""" try: import psutil # lazy-load see PR#85 except ImportError: psutil = False from pathlib import Path # lazy-load see PR#85 import platform # lazy-load see PR#85 # Skip Windows due to https://github.com/banesullivan/scooby/issues/75 fs_type: Union[str, Literal[False]] if psutil and platform.system() != 'Windows': # Code by https://stackoverflow.com/a/35291824/10504481 my_path = str(Path(__file__).resolve()) best_match = "" fs_type = "" for part in psutil.disk_partitions(): if my_path.startswith(part.mountpoint) and len(best_match) < len(part.mountpoint): fs_type = part.fstype best_match = part.mountpoint else: fs_type = False return fs_type scooby-0.10.0/scooby/py.typed000066400000000000000000000000001461560455300160620ustar00rootroot00000000000000scooby-0.10.0/scooby/report.py000066400000000000000000000437321461560455300162730ustar00rootroot00000000000000"""The main module containing the `Report` class.""" import importlib from importlib.metadata import PackageNotFoundError, distribution, version as importlib_version import sys import time from types import ModuleType from typing import Any, Dict, List, Literal, Optional, Tuple, Union, cast from .knowledge import ( PACKAGE_ALIASES, VERSION_ATTRIBUTES, VERSION_METHODS, get_filesystem_type, in_ipykernel, in_ipython, ) MODULE_NOT_FOUND = 'Module not found' MODULE_TROUBLE = 'Trouble importing' VERSION_NOT_FOUND = 'Version unknown' # Info classes class PlatformInfo: """Internal helper class to access details about the computer platform.""" def __init__(self): """Initialize.""" self._mkl_info: Optional[str] # for typing purpose self._filesystem: Union[str, Literal[False]] @property def system(self) -> str: """Return the system/OS name. E.g. ``'Linux (name version)'``, ``'Windows'``, or ``'Darwin'``. An empty string is returned if the value cannot be determined. """ s = platform().system() if s == 'Linux': try: s += ( f' ({platform().freedesktop_os_release()["NAME"]} ' + f'{platform().freedesktop_os_release()["VERSION_ID"]})' ) except Exception: pass elif s == 'Windows': try: release, version, csd, ptype = platform().win32_ver() s += f' ({release} {version} {csd} {ptype})' except Exception: pass elif s == 'Darwin': try: release, _, _ = platform().mac_ver() s += f' (macOS {release})' except Exception: pass elif s == 'Java': # TODO: parse platform().java_ver() pass return s @property def platform(self) -> str: """Return the platform.""" return platform().platform() @property def machine(self) -> str: """Return the machine type, e.g. 'i386'. An empty string is returned if the value cannot be determined. """ return platform().machine() @property def architecture(self) -> str: """Return the bit architecture used for the executable.""" return platform().architecture()[0] @property def cpu_count(self) -> int: """Return the number of CPUs in the system.""" if not hasattr(self, '_cpu_count'): import multiprocessing # lazy-load see PR#85 self._cpu_count = multiprocessing.cpu_count() return self._cpu_count @property def total_ram(self) -> str: """Return total RAM info. If not available, returns 'unknown'. """ if not hasattr(self, '_total_ram'): try: import psutil # lazy-load see PR#85 tmem = psutil.virtual_memory().total self._total_ram = '{:.1f} GiB'.format(tmem / (1024.0**3)) except ImportError: self._total_ram = 'unknown' return self._total_ram @property def mkl_info(self) -> Optional[str]: """Return MKL info. If not available, returns 'unknown'. """ if not hasattr(self, '_mkl_info'): try: import mkl # lazy-load see PR#85 mkl.get_version_string() except (ImportError, AttributeError): mkl = False try: import numexpr # lazy-load see PR#85 except ImportError: numexpr = False # Get mkl info from numexpr or mkl, if available if mkl: self._mkl_info = cast(str, mkl.get_version_string()) elif numexpr: self._mkl_info = cast(str, numexpr.get_vml_version()) else: self._mkl_info = None return self._mkl_info @property def date(self) -> str: """Return the date formatted as a string.""" return time.strftime('%a %b %d %H:%M:%S %Y %Z') @property def filesystem(self) -> Union[str, Literal[False]]: """Get the type of the file system at the path of the scooby package.""" if not hasattr(self, '_filesystem'): self._filesystem = get_filesystem_type() return self._filesystem class PythonInfo: """Internal helper class to access Python info and package versions.""" def __init__( self, additional: Optional[List[Union[str, ModuleType]]], core: Optional[List[Union[str, ModuleType]]], optional: Optional[List[Union[str, ModuleType]]], sort: bool, ): """Initialize python info.""" self._packages: Dict[str, Any] = {} # Holds name of packages and their version self._sort = sort # Add packages in the following order: self._add_packages(additional) # Provided by the user self._add_packages(core) # Provided by a module dev self._add_packages(optional, optional=True) # Optional packages def _add_packages( self, packages: Optional[List[Union[str, ModuleType]]], optional: bool = False ): """Add all packages to list; optional ones only if available.""" # Ensure arguments are a list if isinstance(packages, (str, ModuleType)): pckgs: List[Union[str, ModuleType]] = [ packages, ] elif packages is None or len(packages) < 1: pckgs = list() else: pckgs = list(packages) # Loop over packages for pckg in pckgs: name, version = get_version(pckg) if not (version == MODULE_NOT_FOUND and optional): self._packages[name] = version @property def sys_version(self) -> str: """Return the system version.""" return sys.version @property def python_environment(self) -> Literal['Jupyter', 'IPython', 'Python']: """Return the python environment.""" if in_ipykernel(): return 'Jupyter' elif in_ipython(): return 'IPython' return 'Python' @property def packages(self) -> Dict[str, Any]: """Return versions of all packages. Includes available and unavailable/unknown. """ pckg_dict = dict(self._packages) if self._sort: packages: Dict[str, Any] = {} for name in sorted(pckg_dict.keys(), key=lambda x: x.lower()): packages[name] = pckg_dict[name] pckg_dict = packages return pckg_dict # The main Report instance class Report(PlatformInfo, PythonInfo): """Have Scooby report the active Python environment. Displays the system information when a ``__repr__`` method is called (through outputting or printing). Parameters ---------- additional : list(ModuleType), list(str) List of packages or package names to add to output information. core : list(ModuleType), list(str) The core packages to list first. optional : list(ModuleType), list(str) A list of packages to list if they are available. If not available, no warnings or error will be thrown. Defaults to ``['numpy', 'scipy', 'IPython', 'matplotlib', 'scooby']`` ncol : int, optional Number of package-columns in html table (no effect in text-version); Defaults to 3. text_width : int, optional The text width for non-HTML display modes. sort : bool, optional Sort the packages when the report is shown. extra_meta : tuple(tuple(str, str), ...), optional Additional two component pairs of meta information to display. max_width : int, optional Max-width of html-table. By default None. """ def __init__( self, additional: Optional[List[Union[str, ModuleType]]] = None, core: Optional[List[Union[str, ModuleType]]] = None, optional: Optional[List[Union[str, ModuleType]]] = None, ncol: int = 4, text_width: int = 80, sort: bool = False, extra_meta: Optional[Union[Tuple[Tuple[str, str], ...], List[Tuple[str, str]]]] = None, max_width: Optional[int] = None, ) -> None: """Initialize report.""" # Set default optional packages to investigate if optional is None: optional = ['numpy', 'scipy', 'IPython', 'matplotlib', 'scooby'] PythonInfo.__init__(self, additional=additional, core=core, optional=optional, sort=sort) self.ncol = int(ncol) self.text_width = int(text_width) self.max_width = max_width if extra_meta is not None: if not isinstance(extra_meta, (list, tuple)): raise TypeError("`extra_meta` must be a list/tuple of " "key-value pairs.") if len(extra_meta) == 2 and isinstance(extra_meta[0], str): extra_meta = [extra_meta] for meta in extra_meta: if not isinstance(meta, (list, tuple)) or len(meta) != 2: raise TypeError("Each chunk of meta info must have two values.") else: extra_meta = [] self._extra_meta = extra_meta def __repr__(self) -> str: """Return Plain-text version information.""" import textwrap # lazy-load see PR#85 # Width for text-version text = '\n' + self.text_width * '-' + '\n' # Date and time info as title date_text = ' Date: ' mult = 0 indent = len(date_text) for txt in textwrap.wrap(self.date, self.text_width - indent): date_text += ' ' * mult + txt + '\n' mult = indent text += date_text + '\n' # Get length of longest package: min of 18 and max of 40 if self._packages: row_width = min(40, max(18, len(max(self._packages.keys(), key=len)))) else: row_width = 18 # Platform/OS details repr_dict = self.to_dict() for key in ['OS', 'CPU(s)', 'Machine', 'Architecture', 'RAM', 'Environment', 'File system']: if key in repr_dict: text += f'{key:>{row_width}} : {repr_dict[key]}\n' for key, value in self._extra_meta: text += f'{key:>{row_width}} : {value}\n' # Python details text += '\n' for txt in textwrap.wrap('Python ' + self.sys_version, self.text_width - 4): text += ' ' + txt + '\n' if self._packages: text += '\n' # Loop over packages for name, version in self._packages.items(): text += f'{name:>{row_width}} : {version}\n' # MKL details if self.mkl_info: text += '\n' for txt in textwrap.wrap(self.mkl_info, self.text_width - 4): text += ' ' + txt + '\n' # Finish text += self.text_width * '-' return text def _repr_html_(self) -> str: """Return HTML-rendered version information.""" # Define html-styles border = "border: 1px solid;'" def colspan(html: str, txt: str, ncol: int, nrow: int) -> str: r"""Print txt in a row spanning whole table.""" html += " \n" html += " {txt}\n" html += " \n" return html def cols(html: str, version: str, name: str, ncol: int, i: int) -> Tuple[str, int]: r"""Print package information in two cells.""" # Check if we have to start a new row if i > 0 and i % ncol == 0: html += " \n" html += " \n" align = "left" if ncol == 1 else "right" html += f" %s\n" % version return html, i + 1 # Start html-table html = "\n" # Date and time info as title html = colspan(html, self.date, self.ncol, 0) # Platform/OS details html += " \n" repr_dict = self.to_dict() i = 0 for key in ['OS', 'CPU(s)', 'Machine', 'Architecture', 'RAM', 'Environment', "File system"]: if key in repr_dict: html, i = cols(html, repr_dict[key], key, self.ncol, i) for meta in self._extra_meta: html, i = cols(html, meta[1], meta[0], self.ncol, i) # Finish row html += " \n" # Python details html = colspan(html, 'Python ' + self.sys_version, self.ncol, 1) html += " \n" # Loop over packages i = 0 # Reset count for rows. for name, version in self.packages.items(): html, i = cols(html, version, name, self.ncol, i) # Fill up the row while i % self.ncol != 0: html += " \n" html += " \n" i += 1 # Finish row html += " \n" # MKL details if self.mkl_info: html = colspan(html, self.mkl_info, self.ncol, 2) # Finish html += "
" return html def to_dict(self) -> Dict[str, str]: """Return report as dict for storage.""" out: Dict[str, str] = {} # Date and time info out['Date'] = self.date # Platform/OS details out['OS'] = self.system out['CPU(s)'] = str(self.cpu_count) out['Machine'] = self.machine out['Architecture'] = self.architecture if self.filesystem: out['File system'] = self.filesystem if self.total_ram != 'unknown': out['RAM'] = self.total_ram out['Environment'] = self.python_environment for meta in self._extra_meta: out[meta[1]] = meta[0] # Python details out['Python'] = self.sys_version # Loop over packages for name, version in self._packages.items(): out[name] = version # MKL details if self.mkl_info: out['MKL'] = self.mkl_info return out class AutoReport(Report): """Auto-generate a scooby.Report for a package. This will generate a report based on the distribution requirements of the package. """ def __init__(self, module, additional=None, ncol=3, text_width=80, sort=False): """Initialize.""" if not isinstance(module, (str, ModuleType)): raise TypeError("Cannot generate report for type " "({})".format(type(module))) if isinstance(module, ModuleType): module = module.__name__ # Autogenerate from distribution requirements core = [module, *get_distribution_dependencies(module)] Report.__init__( self, additional=additional, core=core, optional=[], ncol=ncol, text_width=text_width, sort=sort, ) # This functionaliy might also be of interest on its own. def get_version(module: Union[str, ModuleType]) -> Tuple[str, Optional[str]]: """Get the version of ``module`` by passing the package or it's name. Parameters ---------- module : str or module Name of a module to import or the module itself. Returns ------- name : str Package name version : str or None Version of module. """ # module is (1) a module or (2) a string. if not isinstance(module, (str, ModuleType)): raise TypeError("Cannot fetch version from type " "({})".format(type(module))) # module is module; get name if isinstance(module, ModuleType): name = module.__name__ else: name = module module = None # Check aliased names if name in PACKAGE_ALIASES: name = PACKAGE_ALIASES[name] # try importlib.metadata before loading the module try: return name, importlib_version(name) except PackageNotFoundError: module = None # importlib could not find the package, try to load it if module is None: try: module = importlib.import_module(name) except ImportError: return name, MODULE_NOT_FOUND except Exception: return name, MODULE_TROUBLE # Try common version names on loaded module for v_string in ('__version__', 'version'): try: return name, getattr(module, v_string) except AttributeError: pass # Try the VERSION_ATTRIBUTES library try: attr = VERSION_ATTRIBUTES[name] return name, getattr(module, attr) except (KeyError, AttributeError): pass # Try the VERSION_METHODS library try: method = VERSION_METHODS[name] return name, method() except (KeyError, ImportError): pass # If still not found, return VERSION_NOT_FOUND return name, VERSION_NOT_FOUND def platform() -> ModuleType: """Return platform as lazy load; see PR#85.""" import platform return platform def get_distribution_dependencies(dist_name: str): """Get the dependencies of a specified package distribution. Parameters ---------- dist_name : str Name of the package distribution. Returns ------- dependencies : list List of dependency names. """ try: dist = distribution(dist_name) except PackageNotFoundError: raise PackageNotFoundError(f"Package `{dist_name}` has no distribution.") return [pkg.split()[0] for pkg in dist.requires] scooby-0.10.0/scooby/tracker.py000066400000000000000000000060631461560455300164070ustar00rootroot00000000000000"""Track imports.""" from types import ModuleType from typing import List, Mapping, Optional, Sequence, Set, Union from scooby.knowledge import get_standard_lib_modules from scooby.report import Report TRACKING_SUPPORTED = False SUPPORT_MESSAGE = ( "Tracking is not supported for this version of Python. " "Try using a modern version of Python." ) try: import builtins CLASSIC_IMPORT = builtins.__import__ TRACKING_SUPPORTED = True except (ImportError, AttributeError): pass # The variable we track all imports in TRACKED_IMPORTS: List[Union[str, ModuleType]] = ["scooby"] MODULES_TO_IGNORE = { "pyMKL", "mkl", "vtkmodules", "mpl_toolkits", } STDLIB_PKGS: Optional[Set[str]] = None def _criterion(name: str): if ( len(name) > 0 and name not in STDLIB_PKGS and not name.startswith("_") and name not in MODULES_TO_IGNORE ): return True return False if TRACKING_SUPPORTED: def scooby_import( name: str, globals: Optional[Mapping[str, object]] = None, locals: Optional[Mapping[str, object]] = None, fromlist: Sequence[str] = (), level: int = 0, ) -> ModuleType: """Override of the import method to track package names.""" m = CLASSIC_IMPORT(name, globals=globals, locals=locals, fromlist=fromlist, level=level) name = name.split(".")[0] if level == 0 and _criterion(name): TRACKED_IMPORTS.append(name) return m def track_imports() -> None: """Track all imported modules for the remainder of this session.""" if not TRACKING_SUPPORTED: raise RuntimeError(SUPPORT_MESSAGE) global STDLIB_PKGS STDLIB_PKGS = get_standard_lib_modules() builtins.__import__ = scooby_import return def untrack_imports() -> None: """Stop tracking imports and return to the builtin import method. This will also clear the tracked imports. """ if not TRACKING_SUPPORTED: raise RuntimeError(SUPPORT_MESSAGE) builtins.__import__ = CLASSIC_IMPORT TRACKED_IMPORTS.clear() TRACKED_IMPORTS.append("scooby") return class TrackedReport(Report): """A class to inspect the active environment and generate a report. Generates a report based on all imported modules. Simply pass the ``globals()`` dictionary. """ def __init__( self, additional: Optional[List[Union[str, ModuleType]]] = None, ncol: int = 3, text_width: int = 80, sort: bool = False, ): """Initialize.""" if not TRACKING_SUPPORTED: raise RuntimeError(SUPPORT_MESSAGE) if len(TRACKED_IMPORTS) < 2: raise RuntimeError( "There are no tracked imports, please use " "`scooby.track_imports()` before running your " "code." ) Report.__init__( self, additional=additional, core=TRACKED_IMPORTS, ncol=ncol, text_width=text_width, sort=sort, optional=[], ) scooby-0.10.0/setup.py000066400000000000000000000023111461560455300146060ustar00rootroot00000000000000# coding=utf-8 import io import os import setuptools with io.open("README.md", "r", encoding="utf-8") as f: long_description = f.read() setuptools.setup( name="scooby", author="Dieter Werthmüller, Bane Sullivan, Alex Kaszynski, and contributors", author_email="info@pyvista.org", description="A Great Dane turned Python environment detective", long_description=long_description, long_description_content_type="text/markdown", url="https://github.com/banesullivan/scooby", packages=setuptools.find_packages(), entry_points={ "console_scripts": [ "scooby=scooby.__main__:main", ], }, classifiers=[ "Programming Language :: Python", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Intended Audience :: Science/Research", "Natural Language :: English", ], python_requires=">=3.8", extras_require={ "cpu": ["psutil", "mkl"], }, use_scm_version={ "root": ".", "relative_to": __file__, "write_to": os.path.join("scooby", "version.py"), }, setup_requires=["setuptools_scm"], package_data={"scooby": ["py.typed"]}, ) scooby-0.10.0/tests/000077500000000000000000000000001461560455300142415ustar00rootroot00000000000000scooby-0.10.0/tests/test_scooby.py000066400000000000000000000207711461560455300171570ustar00rootroot00000000000000import os import re import subprocess import sys from bs4 import BeautifulSoup import numpy import pytest import scooby # Write a package `dummy_module` without version number. ppath = os.path.join("tests", "dummy_module") try: os.mkdir(ppath) except FileExistsError: pass with open(os.path.join(ppath, "__init__.py"), "w") as f: f.write("info = 'Package without __version__ number.'\n") sys.path.append('tests') def test_report(): report = scooby.Report() text = str(report) assert len(text) > 0 assert len(report.packages) > 0 for pkg, vers in report.packages.items(): assert isinstance(pkg, str) assert isinstance(vers, str) report = scooby.Report(core='numpy') assert 'numpy' in report.packages html = report._repr_html_() assert len(html) > 0 # Same as what is printed in Travis build log report = scooby.Report(additional=['pytest', 'foo']) report = scooby.Report( additional=[ 'foo', ] ) report = scooby.Report( additional=[ pytest, ] ) report = scooby.Report(additional=pytest) report = scooby.Report(additional=['collections', 'foo', 'aaa'], sort=True) def test_dict(): report = scooby.Report(['no_version', 'does_not_exist']) for key, value in report.to_dict().items(): if key != 'MKL': assert key in report.__repr__() assert value[:10] in report.__repr__() def test_inheritence_example(): class Report(scooby.Report): def __init__(self, additional=None, ncol=3, text_width=80, sort=False): """Initiate a scooby.Report instance.""" # Mandatory packages. core = ['psutil', 'mkl', 'numpy', 'scooby'] # Optional packages. optional = [ 'your_optional_packages', 'e.g.', 'matplotlib', 'foo', ] scooby.Report.__init__( self, additional=additional, core=core, optional=optional, ncol=ncol, text_width=text_width, sort=sort, ) report = Report(['pytest']) assert 'psutil' in report.packages assert 'mkl' in report.packages assert 'numpy' in report.packages def test_ipy(): scooby.in_ipykernel() def test_get_version(): name, version = scooby.get_version(numpy) assert version == numpy.__version__ assert name == "numpy" # Package that was no version given by owner; gets 0.1.0 from setup/pip name, version = scooby.get_version("no_version") assert version == "0.1.0" assert name == "no_version" # Dummy module without version (not installed properly) name, version = scooby.get_version("dummy_module") assert version == scooby.report.VERSION_NOT_FOUND assert name == "dummy_module" name, version = scooby.get_version("does_not_exist") assert version == scooby.report.MODULE_NOT_FOUND assert name == "does_not_exist" def test_plain_vs_html(): report = scooby.Report() text_html = BeautifulSoup(report._repr_html_(), features="html.parser").get_text() text_plain = report.__repr__() text_plain = " ".join(re.findall("[a-zA-Z1-9]+", text_plain)) text_html = " ".join(re.findall("[a-zA-Z1-9]+", text_html)) # Plain text currently starts with `Date :`; # we should remove that, or add it to the html version too. assert text_html[20:] == text_plain[25:] def test_extra_meta(): report = scooby.Report(extra_meta=("key", "value")) assert "key : value" in report.__repr__() report = scooby.Report(extra_meta=(("key", "value"),)) assert "key : value" in report.__repr__() report = scooby.Report(extra_meta=(("key", "value"), ("another", "one"))) assert "key : value" in report.__repr__() assert "another : one" in report.__repr__() with pytest.raises(TypeError): report = scooby.Report(extra_meta=(("key", "value"), "foo")) with pytest.raises(TypeError): report = scooby.Report(extra_meta="foo") with pytest.raises(TypeError): report = scooby.Report(extra_meta="for") @pytest.mark.skipif(sys.version_info.major < 3, reason="Tracking not supported on Python 2.") def test_tracking(): scooby.track_imports() from scipy.constants import mu_0 # noqa ; a float value report = scooby.TrackedReport() scooby.untrack_imports() import dummy_module # noqa import no_version # noqa assert "numpy" in report.packages assert "scipy" in report.packages assert "no_version" not in report.packages assert "dummy_module" not in report.packages assert "pytest" not in report.packages assert "mu_0" not in report.packages def test_version_compare(): assert scooby.meets_version('2', '1') assert not scooby.meets_version('1', '2') assert scooby.meets_version('1', '1') assert scooby.meets_version('0.1', '0.1') assert scooby.meets_version('0.1.0', '0.1.0') assert scooby.meets_version('1.0', '0.9') assert not scooby.meets_version('0.9', '1.0') assert scooby.meets_version('0.2.5', '0.1.8') assert not scooby.meets_version('0.1.8', '0.2.5') assert not scooby.meets_version('0.25.1', '0.25.2') assert scooby.meets_version('0.26.0', '0.25.2') assert scooby.meets_version('0.25.2', '0.25.2') assert not scooby.meets_version('0.25.2', '0.26') assert scooby.meets_version('0.28.0dev0', '0.25.2') with pytest.raises(ValueError): scooby.meets_version('0.25.2.0', '0.26') def test_import_os_error(): # pyvips requires libvips, etc., to be installed # We don't have this on CI, so this should throw an error on import # Make sure scooby can handle it. with pytest.raises(OSError): import pyvips # noqa assert scooby.Report(['pyvips']) @pytest.mark.skipif(not sys.platform.startswith('linux'), reason="Not Linux.") def test_import_time(): # Relevant for packages which provide a CLI: # How long does it take to import? cmd = ["time", "-f", "%U", "python", "-c", "import scooby"] # Run it twice, just in case. subprocess.run(cmd) subprocess.run(cmd) # Capture it out = subprocess.run(cmd, capture_output=True) # Currently we check t < 0.2 s. assert float(out.stderr.decode("utf-8")[:-1]) < 0.2 @pytest.mark.script_launch_mode('subprocess') def test_cli(script_runner): # help for inp in ['--help', '-h']: ret = script_runner.run(['scooby', inp]) assert ret.success assert "Great Dane turned Python environment detective" in ret.stdout def rep_comp(inp): # Exclude time to avoid errors. # Exclude scooby-version, because if run locally without having scooby # installed it will be "unknown" for the __main__ one. out = inp.split('OS :')[1] if 'scooby' in inp: out = out.split('scooby :')[0] else: # As the endings are different. out = out.split('--------')[0] return out # default: scooby-Report ret = script_runner.run(['scooby']) assert ret.success assert rep_comp(scooby.Report().__repr__()) == rep_comp(ret.stdout) # default: scooby-Report with sort and no-opt ret = script_runner.run(['scooby', 'numpy', '--no-opt', '--sort']) assert ret.success test = scooby.Report('numpy', optional=[], sort=True).__repr__() print(rep_comp(test)) print(rep_comp(ret.stdout)) assert rep_comp(test) == rep_comp(ret.stdout) # version -- VIA scooby/__main__.py by calling the folder scooby. ret = script_runner.run(['python', 'scooby', '--version']) assert ret.success assert "scooby v" in ret.stdout # version -- VIA scooby/__main__.py by calling the file. ret = script_runner.run(['python', os.path.join('scooby', '__main__.py'), '--version']) assert ret.success assert "scooby v" in ret.stdout # default: scooby-Report for matplotlibe ret = script_runner.run(['scooby', '--report', 'pytest']) assert ret.success assert "pytest" in ret.stdout assert "iniconfig" in ret.stdout # handle error -- no distribution ret = script_runner.run(['scooby', '--report', 'pathlib']) assert not ret.success assert "importlib" in ret.stderr # handle error -- not found ret = script_runner.run(['scooby', '--report', 'foobar']) assert not ret.success assert "no Report" in ret.stderr def test_auto_report(): report = scooby.AutoReport('pytest') assert 'pytest' in report.packages assert 'iniconfig' in report.packages