pax_global_header00006660000000000000000000000064147517415540014527gustar00rootroot0000000000000052 comment=5ba87609ee7525abe26d83685b288bd7a4818ede stravalib-2.2/000077500000000000000000000000001475174155400133615ustar00rootroot00000000000000stravalib-2.2/.all-contributorsrc000066400000000000000000000037171475174155400172220ustar00rootroot00000000000000{ "projectName": "stravalib", "projectOwner": "stravalib", "files": [ "README.md" ], "imageSize": 100, "commit": false, "commitConvention": "angular", "contributorsSortAlphabetically": true, "contributorsPerLine": 7, "skipCi": true, "repoType": "github", "repoHost": "https://github.com", "commitType": "docs", "contributors": [ { "login": "lwasser", "name": "Leah Wasser", "avatar_url": "https://avatars.githubusercontent.com/u/7649194?v=4", "profile": "http://www.leahwasser.com", "contributions": [ "code", "review", "doc" ] }, { "login": "yotam5", "name": "Yotam", "avatar_url": "https://avatars.githubusercontent.com/u/69643410?v=4", "profile": "https://github.com/yotam5", "contributions": [ "doc" ] }, { "login": "jsamoocha", "name": "Jonatan Samoocha", "avatar_url": "https://avatars.githubusercontent.com/u/1788027?v=4", "profile": "https://vortza.com", "contributions": [ "code", "review", "doc", "maintenance" ] }, { "login": "enadeau", "name": "Γ‰mile Nadeau", "avatar_url": "https://avatars.githubusercontent.com/u/12940089?v=4", "profile": "https://github.com/enadeau", "contributions": [ "code", "review", "doc", "maintenance" ] }, { "login": "jlelong", "name": "Jerome Lelong", "avatar_url": "https://avatars.githubusercontent.com/u/2910140?v=4", "profile": "http://www-ljk.imag.fr/membres/Jerome.Lelong/", "contributions": [ "bug" ] }, { "login": "djcunningham0", "name": "Danny Cunningham", "avatar_url": "https://avatars.githubusercontent.com/u/38900370?v=4", "profile": "https://towardsdatascience.com/@djcunningham0", "contributions": [ "doc", "ideas" ] } ] } stravalib-2.2/.codecov.yml000066400000000000000000000003561475174155400156100ustar00rootroot00000000000000# Set non-blocking status checks coverage: status: project: default: # Allows 10% drop from previous base commit threshold: 10% informational: true patch: default: informational: true stravalib-2.2/.coveragerc000066400000000000000000000001051475174155400154760ustar00rootroot00000000000000[run] branch = True omit = */tests/* */_version_generated.py stravalib-2.2/.github/000077500000000000000000000000001475174155400147215ustar00rootroot00000000000000stravalib-2.2/.github/.dependabot.yml000066400000000000000000000010161475174155400176250ustar00rootroot00000000000000# To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file version: 2 updates: - package-ecosystem: "pip" # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "monthly" stravalib-2.2/.github/ISSUE_TEMPLATE/000077500000000000000000000000001475174155400171045ustar00rootroot00000000000000stravalib-2.2/.github/ISSUE_TEMPLATE/bug-report.yml000066400000000000000000000057051475174155400217240ustar00rootroot00000000000000name: 🐞 Bug Report description: Report incorrect behavior in the stravalib library title: "BUG: " labels: [Bug, help-wanted] body: - type: checkboxes id: checks attributes: label: Stravalib version checks options: - label: > I have tested this in a new clean environment with only stravalib and core python files. required: true - label: > I have checked that this issue has not already been reported. required: true - label: > I have confirmed this bug exists on the [latest version](https://pypi.org/project/stravalib/) of stravalib. required: true - label: > I have confirmed this bug exists on the [main branch](https://github.com/stravalib/stravalib) of stravalib. - type: dropdown id: operating-system attributes: label: What operating system are you seeing the problem on? multiple: true options: - Windows - Mac - Linux - type: textarea id: python-version attributes: label: What version of python or you running? description: > Example: 3.10.12 placeholder: > Enter the version here. ... render: python validations: required: true - type: textarea id: example attributes: label: Reproducible Example description: > Please follow [this guide](https://matthewrocklin.com/blog/work/2018/02/28/minimal-bug-reports) on how to provide a minimal, copy-pastable example. placeholder: > from stravalib.client import Client # Your code here (please don't include token values / secrets here!) ... render: python validations: required: true - type: textarea id: problem attributes: label: Issue Description description: > Please provide a description of the issue shown in the reproducible example. validations: required: true - type: textarea id: expected-behavior attributes: label: Expected Behavior description: > Please describe or show a code example of the expected behavior. validations: required: true - type: textarea id: version attributes: label: Your environment description: > Please paste the output of ``pip freeze`` or the associated conda envt ``conda env export > environment.yml`` here value: >
Replace this line with the output of pip or conda list.
validations: required: true - type: checkboxes id: terms attributes: label: Code of Conduct description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/stravalib/stravalib/blob/main/CODE_OF_CONDUCT.md) options: - label: I agree to follow this project's Code of Conduct required: true stravalib-2.2/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000003411475174155400210720ustar00rootroot00000000000000blank_issues_enabled: true contact_links: - name: Stravalib community support url: https://github.com/orgs/stravalib/discussions/categories/q-a about: Please ask general questions about stravalib in our discussions stravalib-2.2/.github/ISSUE_TEMPLATE/documentation-issue.yml000066400000000000000000000032341475174155400236300ustar00rootroot00000000000000name: πŸ“ Documentation Improvement description: Report wrong or missing documentation title: "DOC: " labels: [documentation] body: - type: checkboxes attributes: label: Stravalib version checks options: - label: > I have checked that the issue still exists on the latest version of the docs [here](https://stravalib.readthedocs.io/en/latest/) required: true - type: textarea id: location attributes: label: Location of the documentation description: > Please provide the location of the documentation, e.g. "stravalib.Client" or the URL of the documentation, e.g. "https://stravalib.readthedocs.io/en/latest/reference/client.htmll" placeholder: https://stravalib.readthedocs.io/en/latest/reference/client.html validations: required: true - type: textarea id: problem attributes: label: Documentation problem description: > Please provide a description of what documentation you believe needs to be fixed/improved validations: required: true - type: textarea id: suggested-fix attributes: label: Suggested fix for documentation description: > Please explain how the suggested fix improves the stravalib docs. validations: required: true - type: checkboxes id: terms attributes: label: Code of Conduct description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/stravalib/stravalib/blob/main/CODE_OF_CONDUCT.md) options: - label: I agree to follow this project's Code of Conduct required: true stravalib-2.2/.github/ISSUE_TEMPLATE/feature-request.yml000066400000000000000000000040701475174155400227510ustar00rootroot00000000000000name: Feature Request description: Suggest an idea for stravalib title: "ENH: " labels: [feature-request] body: - type: checkboxes id: checks attributes: label: Feature Type description: > Please check what type of feature request you would like to propose. options: - label: > Adding new functionality to stravalib - label: > Changing existing functionality in stravalib - label: > Removing existing functionality in stravalib - type: textarea id: description attributes: label: Problem Description description: > Please describe what problem the feature would solve. placeholder: > I wish I could use stravalib to ... validations: required: true - type: textarea id: feature attributes: label: Feature Description description: > Please describe how the new feature would be implemented. Please use pseudo-code if relevant. placeholder: > Your text here - pseudo-code demonstrated what this would look like when used encouraged validations: required: true - type: textarea id: alternative attributes: label: Alternative Solutions description: > Please describe any alternative solution (existing functionality, 3rd party package, etc.) that would satisfy the feature request. placeholder: > Your text here. validations: required: true - type: textarea id: context attributes: label: Additional Context description: > Please provide any relevant GitHub issues, code examples or references that help describe and support the feature request. - type: checkboxes id: terms attributes: label: Code of Conduct description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/stravalib/stravalib/blob/main/CODE_OF_CONDUCT.md) options: - label: I agree to follow this project's Code of Conduct required: true stravalib-2.2/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000040051475174155400205210ustar00rootroot00000000000000closes #issue-this-closes-here _The text above will ensure the issue will be closed when this PR is merged_ ## Description ## Type of change Select the statement best describes this pull request. - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] This change requires a documentation update - [ ] This is a documentation update - [ ] This is a infrastructure update (docs, ci, etc) - [ ] Other (please describe) ## Does your PR include tests If you are fixing a bug or adding a feature, we appreciate (but do not require) tests to support whatever fix of feature you're implementing. - [ ] Yes - [ ] No, i'd like some help with tests - [ ] This change doesn't require tests ## Did you include your contribution to the change log? - [ ] Yes, `changelog.md` is up-to-date. _If you are having a hard time getting the tests to run correctly feel free to ping one of the maintainers here!_ stravalib-2.2/.github/PULL_REQUEST_TEMPLATE/000077500000000000000000000000001475174155400202005ustar00rootroot00000000000000stravalib-2.2/.github/PULL_REQUEST_TEMPLATE/release-pull-request-template.md000066400000000000000000000031351475174155400264150ustar00rootroot00000000000000# Stravalib Release Pull Request Template Please use this template when you are preparing to make a release to stravalib * [An overview of our release workflow can be found in our documentation.](https://stravalib.readthedocs.io/en/latest/contributing/build-release-guide.html) * [Before making a release be sure to check out test PyPI](https://test.pypi.org/project/stravalib/) to ensure that the build is working properly. ## Release checklist - [ ] Be sure to clearly specify what version you are bumping to in the PR title: Example: Bump to version x.x - [ ] Add the version of this release to our changelog - [ ] Organize items changes under the new version in groups as follows: Added, Fixed and Changed - [ ] Add all contributors to the release below those sections - [ ] Wait for a maintainer to approve the pull request. Then merge! You are now ready to create the release. The changelog should look something like this: ``` ## Unreleased ## New version here: e.g. v1.0.0 ### Added * Add: Add an option to mute Strava activity on update (@ollyajoke, #227) ### Fixed * Fix: add new attributes for bikes according to updates to Strava API to fix warning message (@huaminghuangtw, #239) ### Changed * Change: Improved unknown time zone handling (@jsamoocha, #242) ### Contributors to this release @jsamoocha, @yihong0618, @tirkarthi, @huaminghuangtw, @ollyajoke, @lwasser ``` Once this PR is merged you are ready to - [ ] Create a tagged release on GitHub using the same version that you merged in the changelog added here - [ ] When you publish that release, the GitHub action to push to PyPI will be invoked. stravalib-2.2/.github/dependabot.yml000066400000000000000000000007061475174155400175540ustar00rootroot00000000000000# To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file version: 2 updates: - package-ecosystem: "pip" directory: "/" schedule: interval: "monthly" stravalib-2.2/.github/workflows/000077500000000000000000000000001475174155400167565ustar00rootroot00000000000000stravalib-2.2/.github/workflows/build-docs.yml000066400000000000000000000026701475174155400215330ustar00rootroot00000000000000name: Build Documentation on: pull_request: push: branches: - main jobs: build-doc: runs-on: ubuntu-latest env: PYTHON-VERSION: "3.11" steps: - uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON-VERSION }} - name: Upgrade pip & install nox run: | # install pip=>20.1 to use "pip cache dir" python3 -m pip install --upgrade pip pip install nox - name: Set Variables id: set_variables shell: bash run: | echo "PY=$(python -c 'import hashlib, sys;print(hashlib.sha256(sys.version.encode()+sys.executable.encode()).hexdigest())')" >> $GITHUB_OUTPUT echo "PIP_CACHE=$(pip cache dir)" >> $GITHUB_OUTPUT - name: Cache dependencies uses: actions/cache@v4 with: path: ${{ steps.set_variables.outputs.PIP_CACHE }} key: ${{ runner.os }}-pip-${{ steps.set_variables.outputs.PY }} - name: Install dependencies run: | pip install nox - name: Build docs & linkcheck run: | # Build html and link check nox -s docs - name: Print doc link failures in the output.txt file if: success() || failure() run: | cat docs/_build/linkcheck/output.txt | while read line do echo -e "$line \n" done stravalib-2.2/.github/workflows/build-test.yml000066400000000000000000000041601475174155400215560ustar00rootroot00000000000000name: Pytest unit/integration on: pull_request: push: branches: - main # Use bash by default in all jobs defaults: run: shell: bash jobs: build-test: name: Test Run (${{ matrix.python-version }}, ${{ matrix.os }}) runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: ["ubuntu-latest", "macos-latest", "windows-latest"] python-version: ["3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 with: # fetch more than the last single commit to help scm generate proper version fetch-depth: 20 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Set Variables id: set_variables shell: bash run: | echo "PY=$(python -c 'import hashlib, sys;print(hashlib.sha256(sys.version.encode()+sys.executable.encode()).hexdigest())')" >> $GITHUB_OUTPUT echo "PIP_CACHE=$(pip cache dir)" >> $GITHUB_OUTPUT - name: Cache dependencies uses: actions/cache@v4 with: path: ${{ steps.set_variables.outputs.PIP_CACHE }} key: ${{ runner.os }}-pip-${{ steps.set_variables.outputs.PY }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install nox - name: List installed packages run: pip list # Need tags for setuptools_scm to provide a proper version - name: Fetch git tags run: git fetch origin 'refs/tags/*:refs/tags/*' # For now i made a separate tests run so we can see each version # build via github actions. There may be a better way to do this. - name: Run tests with pytest & nox run: | nox -s tests-${{ matrix.python-version }} - name: Upload coverage to Codecov if: ${{ matrix.os == 'ubuntu-latest' && matrix.python-version == '3.11'}} uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} verbose: true files: ./coverage.xml stravalib-2.2/.github/workflows/check-strava-api.yml000066400000000000000000000017431475174155400226300ustar00rootroot00000000000000name: Check Strava API on: schedule: - cron: "0 0 * * *" workflow_dispatch: jobs: update-model: name: Update Model runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Fetch API run: curl https://developers.strava.com/swagger/swagger.json > src/stravalib/tests/resources/strava_swagger.json - name: Fetch API Schema uses: stravalib/strava_swagger2pydantic@v1 with: model_file: "src/stravalib/strava_model.py" - name: Create Pull Request uses: peter-evans/create-pull-request@v6 with: add-paths: | src/stravalib commit-message: Strava API Change branch: api-change delete-branch: true title: "[CHANGE] Strava API Change" body: | There were changes in the Strava API: [Please edit this comment to indicate what has changed] - [ ] The changelog is updated (only when necessary) stravalib-2.2/.github/workflows/push-pypi.yml000066400000000000000000000024101475174155400214340ustar00rootroot00000000000000name: Publish to PyPI on: release: types: [published] push: branches: - main jobs: build-publish: runs-on: ubuntu-latest # Only run build action on base repo - not forks if: github.repository_owner == 'stravalib' steps: - name: Checkout uses: actions/checkout@v4 with: # So scm can view previous commits fetch-depth: 100 # Need the tags so that setuptools-scm can form a valid version number - name: Fetch git tags run: git fetch origin 'refs/tags/*:refs/tags/*' - name: Setup Python uses: actions/setup-python@v5 with: python-version: "3.10" - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install build twine pip list - name: Build package run: | python -m build echo "" echo "Generated files:" ls -lh dist/ - name: Check the archives run: twine check dist/* - name: Publish package to PyPI # Only publish to real PyPI on release if: github.event_name == 'release' uses: pypa/gh-action-pypi-publish@release/v1 with: password: ${{ secrets.PYPI_TOKEN }} stravalib-2.2/.github/workflows/type-check.yml000066400000000000000000000022671475174155400215440ustar00rootroot00000000000000name: Mypy static type checking on: pull_request: push: branches: - main # Use bash by default in all jobs defaults: run: shell: bash jobs: mypy: name: Run mypy runs-on: ubuntu-latest strategy: fail-fast: false steps: - uses: actions/checkout@v4 - name: Set up Python 3.11 uses: actions/setup-python@v5 with: python-version: "3.11" - name: Set Variables id: set_variables shell: bash run: | echo "PY=$(python -c 'import hashlib, sys;print(hashlib.sha256(sys.version.encode()+sys.executable.encode()).hexdigest())')" >> $GITHUB_OUTPUT echo "PIP_CACHE=$(pip cache dir)" >> $GITHUB_OUTPUT - name: Cache dependencies uses: actions/cache@v4 with: path: ${{ steps.set_variables.outputs.PIP_CACHE }} key: ubuntu-latest-pip-${{ steps.set_variables.outputs.PY }} - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install nox - name: List installed packages run: python -m pip freeze - name: Run tests with mypy run: | nox -s mypy stravalib-2.2/.gitignore000066400000000000000000000010641475174155400153520ustar00rootroot00000000000000env*/ _build/ build/ dist/ *.pyc .coverage .ipynb_checkpoints # This will remove our ability to add CI in the .github dir #.* .DS_Store .idea/ *.egg-info/ *.egg/ docs/_build/ test.ini src/stravalib/_version_generated.py #*~ examples/strava-oauth/settings.cfg # Code cov .coverage .nox .vscode/* docs/_build/* .env # So people don't mistakenly delete and submit a pr with these stravalib/tests/test.ini-example stravalib/tests/test.ini stravalib/docs/api .vscode/ stravalib-2023/ .nox/* # Ignore Sphinx auto-generated API stubs # docs/reference/api coverage.xml stravalib-2.2/.pre-commit-config.yaml000066400000000000000000000045751475174155400176550ustar00rootroot00000000000000# pre-commit is a tool that you run locally # to perform a predefined set of tasks manually and/or # automatically before git commits are made. # Here we are using pre-commit with the precommit.ci bot to implement # code fixes automagically in pr's. You will still want to install pre-commit # to run locally # Config reference: https://pre-commit.com/#pre-commit-configyaml---top-level # To run on a pr, add a comment with the text "pre-commit.ci run" # Common tasks # - Manually update hooks: pre-commit autoupdate # - Run on all files: pre-commit run --all-files # - Register git hooks: pre-commit install --install-hooks # - Run a specific hook on all files: pre-commit run codespell --all-files ci: autofix_prs: false autofix_commit_msg: | '[pre-commit.ci πŸ€–] Apply code format tools to PR' autoupdate_commit_msg: "[pre-commit.ci] pre-commit autoupdate" # Update hook versions every quarter (so we don't get hit with weekly update pr's) autoupdate_schedule: quarterly autoupdate_branch: "" default_stages: [pre-commit] repos: # Misc commit checks - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 # ref: https://github.com/pre-commit/pre-commit-hooks#hooks-available hooks: # Autoformat: Makes sure files end in a newline and only a newline. - id: end-of-file-fixer # Lint: Check for files with names that would conflict on a # case-insensitive filesystem like MacOS HFS+ or Windows FAT. - id: check-case-conflict - id: trailing-whitespace # Linting code using ruff - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.8.6 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] # Black for auto code formatting - repo: https://github.com/psf/black rev: 24.10.0 hooks: - id: black entry: bash -c 'black "$@"; git add -u' -- language_version: python3.10 args: ["--line-length=79"] - repo: https://github.com/adamchainz/blacken-docs rev: "1.19.1" hooks: - id: blacken-docs - repo: https://github.com/codespell-project/codespell rev: v2.3.0 hooks: - id: codespell files: ^.*\.(py|md|rst)$ exclude: > (?x)^( src/stravalib/strava_model.py| src/stravalib/tests/resources/strava_swagger.json )$ additional_dependencies: - tomli stravalib-2.2/.readthedocs.yml000066400000000000000000000003571475174155400164540ustar00rootroot00000000000000version: 2 sphinx: builder: html configuration: docs/conf.py fail_on_warning: false build: os: ubuntu-22.04 tools: python: "3.10" python: install: - method: pip path: . extra_requirements: - docs stravalib-2.2/CODE_OF_CONDUCT.md000066400000000000000000000125701475174155400161650ustar00rootroot00000000000000 # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socioeconomic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement by emailing leah at pyopensci.org . All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations]. [homepage]: https://www.contributor-covenant.org [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html [Mozilla CoC]: https://github.com/mozilla/diversity [FAQ]: https://www.contributor-covenant.org/faq [translations]: https://www.contributor-covenant.org/translations stravalib-2.2/CONTRIBUTING.md000066400000000000000000000174121475174155400156170ustar00rootroot00000000000000# How to Contribute to Stravalib **Thank you for considering contributing to stravalib!** This is a community-driven project. It's people like you that make it useful and successful. We welcome contributions of all kinds. Below are some of the ways that you can contribute to `stravalib`: - Submit bug reports and feature requests - Write tutorials or examples - Fix typos and improve the documentation - Submit code fixes [Please read our development guide](https://stravalib.readthedocs.io/en/latest/contributing/development-guide.html) if you are interested in submitting a pull request to suggest changes to our code or documentation. ## Contribution ground rules The `stravalib` maintainers work on `stravalib` in their free time because they love the package's contribution to the Python ecosystem. As such we value contributions and respectful interactions with `stravalib` users. **Please be considerate and respectful of others** in all of your communications in this repository. Everyone must abide by our [Code of Conduct](https://github.com/stravalib/stravalib/blob/main/CODE_OF_CONDUCT.md). Please read it carefully. Our goal is to maintain a diverse community of `stravalib` users and contributors that's pleasant for everyone. ## How to start contributing to stravalib If you are thinking about submitting a change to stravalib documentation or code, please start by [submitting an issue in our GitHub repository](https://github.com/stravalib/stravalib/issues/). We will use that issue to communicate with you about: 1. Whether the change is in scope for the project 2. Any obstacles that you need help with or clarification on. ### Want to submit a pull request with changes to our code or documentation? We welcome changes to stravalib through pull requests. Before you submit a pull request please be sure to: 1. Read this document fully 2. Submit an issue about the change as discussed below and 3. [Read our development guide](https://stravalib.readthedocs.io/en/latest/contributing/development-guide.html) ### How to report a bug in stravalib or typo in our documentation Found a bug? Or a typo in our documentation? We want to know about it! To report a bug or a documentation issue, please submit an issue on GitHub. If you submit a bug report, **please add as much detail as you can about the bug**. Remember: the more information we have, the easier it will be for us to solve your problem. ### Documentation fixes If you're browsing the documentation and notice a typo or something that could be improved, please let us know by creating an issue. We also welcome you to submit a documentation fix directly using a pull request to our repository. ## An overview of our stravalib git / GitHub workflow We follow the [git pull request workflow](https://www.asmeurer.com/git-workflow/) to make changes to our codebase. Every change made goes through a pull request, even our own so that our [continuous integration](https://en.wikipedia.org/wiki/Continuous_integration) services can check that the code is up to standards and passes all of our tests. This workflow ensures that our _main_ branch us always stable. ### General guidelines for pull requests (PRs): - **Open an issue first** describing what you want to do. If an issue already matches your PR, leave a comment there instead to let us know what you plan to do. Be as specific as you can in the issue. - Each pull request should contain a **small** and logical collection of changes directly related to your opened issue. - Larger changes should be broken down into smaller components and integrated separately. - Bug fixes should be submitted in separate PRs. - Describe what your pull request changes and _why_ this is a good thing (or refer to the issue you opened if it contains that information). Be as specific as you can. - Do not commit changes to files irrelevant to your feature or bugfix (eg: `.gitignore`, IDE project files, etc). - Write descriptive commit messages that describe what your change is. - Be willing to accept feedback and to work on your code through a pull request. We don't want to break other users' code, so care must be taken not to introduce bugs. - Be aware that the pull request review process is not immediate. The time that it takes to review a pull request is generally proportional to the size of the pull request. Larger pull requests may take longer to review and merge. ### Testing your code Automated testing ensures that our code is as free of bugs as possible. It also lets us know immediately if a change breaks any other part of the code. All of our test code and data are stored in the `tests` directory within the `stravalib` package directory. We use the [pytest](https://docs.pytest.org/en/latest/) framework to run the test suite. If you submit a code fix or enhancement and know how to write tests, please include tests for your code in your pr. This helps us ensure that your change doesn't break any of the existing functionality. Tests also help us be confident that we won't break your code in the future. If you're **new to testing**, please review existing test files for examples of how to create tests. **Don't let the tests keep you from submitting your contribution!** If you're unsure how to do this or are having trouble, submit your pull request anyway. We will help you create the tests and sort out any problems during the code review. You can learn more about [how to run our test suite in the development guide, here](https://stravalib.readthedocs.io/en/latest/contributing/development-guide.html#about-the-stravalib-test-suite). ### Test coverage We use `codecov`, implemented through the `pytest-cov` extension to sphinx to track `stravalib`'s test % coverage. When you submit a pull request, you will see how that pull request affects our package's total test coverage. ### Documentation Our documentation is in the `doc` folder. We use [sphinx](https://www.sphinx-doc.org/) to build the web pages from these sources. You can learn more about [building and contributing to our documentation here.](https://stravalib.readthedocs.io/en/latest/contributing/development-guide.html#documentation) ### Code Review and issue response timeline After you've submitted a pull request or an issue, you should expect to see a response from a maintainer within a week or so, depending on how busy the maintainers are at that time. If you submit a pull request, we may suggest some changes, improvements, or alternative approaches. Some things that will increase the chance that your pull request is accepted quickly: - Write a good and detailed description of what the pull request does. - Write tests for the code you wrote/modified. - Readable code is better than clever code (even with comments). - Write documentation for your code (docstrings) and leave comments explaining the _reason_ behind non-obvious things. - Include an example of new features in the gallery or tutorials. - Follow the [numpy style guide](https://numpydoc.readthedocs.io/en/latest/format.html) for documentation and docstrings. - Run the automatic code formatter and style checks. All pull requests are automatically tested using workflows in GitHub Actions. Our GitHub actions: - run the test suite - test the documentation build (which includes API documentation created from docstrings) - run [code format and syntax tests for code formatters](https://stravalib.readthedocs.io/en/latest/contributing/development-guide.html#code-format-and-syntax) When you submit a pull request, you will see whether the tests ran or failed. You will also see the resulting % code coverage based on your pull request. Please try to ensure that all tests pass (Green checks) in your pull request before requesting a review from maintainers. If you have any trouble with the GitHub action tests, please leave a comment in the Pull Request or open an Issue. We will do our best to help you. stravalib-2.2/LICENSE.txt000066400000000000000000000261361475174155400152140ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. stravalib-2.2/MANIFEST.in000066400000000000000000000001371475174155400151200ustar00rootroot00000000000000recursive-include docs * recursive-include src/stravalib/tests/ * recursive-exclude examples * stravalib-2.2/README.md000066400000000000000000000200111475174155400146320ustar00rootroot00000000000000# Welcome to stravalib [![All Contributors](https://img.shields.io/github/all-contributors/stravalib/stravalib?color=ee8449&style=flat-square)](#contributors) [![DOI](https://zenodo.org/badge/8828908.svg)](https://zenodo.org/badge/latestdoi/8828908) ![PyPI](https://img.shields.io/pypi/v/stravalib?style=plastic) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/stravalib?style=plastic) [![Documentation Status](https://readthedocs.org/projects/stravalib/badge/?version=latest)](https://stravalib.readthedocs.io/en/latest/?badge=latest) ![Package Tests Status](https://github.com/stravalib/stravalib/actions/workflows/build-test.yml/badge.svg) ![PyPI - Downloads](https://img.shields.io/pypi/dm/stravalib?style=plastic) [![codecov](https://codecov.io/gh/stravalib/stravalib/branch/main/graph/badge.svg?token=sHbFJn7epy)](https://codecov.io/gh/stravalib/stravalib) The **stravalib** Python package provides easy-to-use tools for accessing and downloading Strava data from the Strava V3 web service. Stravalib provides a Client class that supports: - Authenticating with stravalib - Accessing and downloading Strava activity, club, and profile data - Making changes to account activities It also provides support for working with date/time/temporal attributes and quantities through the [Python Pint library](https://pypi.org/project/Pint/). ## Dependencies - Python 3.9+ - [Setuptools](https://pypi.org/project/setuptools/) for building stravalib - Other Python libraries (installed automatically when using pip): - [requests](https://pypi.org/project/requests/), - [pytz](https://pypi.org/project/pytz/) - [pint](https://pypi.org/project/pint/) - [arrow](https://pypi.org/project/arrow/), - [pydantic 2.x](https://pypi.org/project/pydantic/) ## Installation stravalib is available on [PyPI](https://pypi.org/project/stravalib/) and can be installed using `pip`: `pip install stravalib` ## Get started using Stravalib Most of the methods that you will use with stravalib are in the `stravalib.client.Client` class. You may be interested in the following tutorials to get started 1. [How to create a Strava app.](https://stravalib.readthedocs.io/en/latest/get-started/authenticate-with-strava.html#authenticate-with-the-strava-api-using-stravalib) 1. [How to authenticate with Strava using stravalib.](https://stravalib.readthedocs.io/en/latest/get-started/authenticate-with-strava.html) 1. [How to get activities using stravalib.](https://stravalib.readthedocs.io/en/latest/get-started/activities.html) 2. [Athlete data using stravalib](https://stravalib.readthedocs.io/en/latest/get-started/athletes.html) 3. [Unit conversion and stravalib](https://stravalib.readthedocs.io/en/latest/get-started/activities.html#stravalib-offers-unit-conversion-helpers) We welcome contributions to our tutorials and get started documentation if you are a stravalib user and want to contribute! ## How to Contribute to Stravalib ### Contributing quickstart Ready to contribute? Here's how to set up Stravalib for local development. 1. Fork the repository on GitHub To create your own copy of the repository on GitHub, navigate to the `stravalib/stravalib ` repository and click the **Fork** button in the top-right corner of the page. 2. Clone your fork locally Use `git clone` to get a local copy of your stravalib repository on your local filesystem: ```console git clone git@github.com:your_name_here/stravalib.git cd stravalib/ ``` 3. Set up your fork for local development Read through our [development guide](https://stravalib.readthedocs.io/en/latest/contributing/development-guide.html) to learn how to: * [Run our test suite](https://stravalib.readthedocs.io/en/latest/contributing/development-guide.html#about-the-stravalib-test-suite) * [Build our docs](https://stravalib.readthedocs.io/en/latest/contributing/development-guide.html#documentation) * [Lint and format our code](https://stravalib.readthedocs.io/en/latest/contributing/development-guide.html#code-format-and-syntax) ### Building from source To build the project locally and install in editable mode: 1. access the project root directory 2. run: ```bash $ pip install -e . ``` ### Pull Requests and tests Please add tests that cover any changes that you make to stravalib. Adding tests will greatly reduce the effort of reviewing and merging your Pull Request. [Read more about our test suite here.](https://stravalib.readthedocs.io/en/latest/contributing/development-guide.html#about-the-stravalib-test-suite). We developed a [mock fixture](https://stravalib.readthedocs.io/en/latest/contributing/development-guide.html#tests-the-stravalib-mock-fixture) that ensures that when tests are run, they are not hitting the Strava API. ## Still reading? The [published sphinx documentation](https://stravalib.readthedocs.io/) provides much more.
Danny Cunningham
Danny Cunningham

πŸ“– πŸ€”
Jerome Lelong
Jerome Lelong

πŸ›
Jonatan Samoocha
Jonatan Samoocha

πŸ’» πŸ‘€ πŸ“– 🚧
Leah Wasser
Leah Wasser

πŸ’» πŸ‘€ πŸ“–
Yotam
Yotam

πŸ“–
Γ‰mile Nadeau
Γ‰mile Nadeau

πŸ’» πŸ‘€ πŸ“– 🚧
stravalib-2.2/changelog.md000066400000000000000000000444761475174155400156510ustar00rootroot00000000000000# Change Log ## Unreleased ## v2.2.0 ### Added - Add: `SummaryAthlete` return to exchange_code_for_token (@lwasser, #552) - Add: Add more information about how our mock fixture works (@lwasser, #292) - Add(feature): Automatic token refresh :sparkles: built into stravalib (@lwasser, @jsamoocha, #585) ### Fixed - Fix: Docs should be linked rather than repeating information (@lwasser, #613) - Fix: Update `segment_efforts` api and return warnings (@lwasser, #321) ### Contributors to this release @jsamoocha, @lwasser ## v2.1.0 ### Added - Add: update activities doc page and migrate to myst (@lwasser, #508) - Add: `get_athlete_zones` method on client (@enadeau, #508) - Add: tutorial on authenticating with Strava + stravalib + update get-started & docs fixes (@lwasser, #317) - Add: inheritance diagrams & explain overrides (@lwasser, #531) - Add: update examples to stravalib 2.x (@lwasser, #581) - Add: rename RST files to MD (@lwasser, #589) ### Fixed - Fix: some overrides moved from `DetailedActivity` to `SummaryActivity` (@enadeau, #570) - Fix: ensures `ActivityType` instances can be compared to str (@jsamoocha, #583) - Fix: moved several undocumented attributes from `DetailedActivity` to `SummaryActivity` (@jsamoocha, #594) - Fix: broken link (@lwasser, #597) - Fix: do not create PDF files in CI (@lwasser, #563) - Fix: banner URL to relative path (@lwasser, #565) - Fix: missing comma in all-contributors JSON config (@lwasser, #573) - Fix: tiny update to streaming data documentation (@lwasser, #588) - Fix: summary activity undocumented attributes (@jsamoocha, #595) - Fix: update pre-commit configuration (@lwasser, #591) ### Changed - Change: Strava API update (@github-actions, #592) ### Contributors to this release @yotam5 made their first contribution to stravalib! @jsamoocha, @lwasser, @enadeau, @yotam5 ## v2.0.0 ### Major Changes in This Release 1. **Breaking:** Added support for Pydantic 2.x; 1.x behavior is no longer supported. Refer to [Pydantic’s V2 migration guide](https://docs.pydantic.dev/latest/migration/) if you use extensions of Stravalib model classes or Pydantic’s serialization mechanisms (`parse_obj()`, `dict()`, `json()`). 2. Removed deprecated (de-)serialization methods `deserialize()`, `from_dict()`, and `to_dict()`. Use [Pydantic’s serialization mechanisms](https://docs.pydantic.dev/latest/concepts/serialization/). 3. Renamed `unithelper` module to `unit_helper`. Helper functions like `feet()` and `miles()` now return a Pint `Quantity` object. 4. Introduced new types for distances, velocities, durations, and time zones. `activity.distance` now returns a `Distance` type. Retrieve distance in meters with `activity.distance` or as a `Quantity` using `activity.distance.quantity`. Please see the migration guide in our docs for more details on the changes. ### Added - Add: `naive_datetime()` test to the `pydantic-v2 branch`. (@bmeares, #522) - Add: custom types that pass static type checks (@jsamoocha, #534) - Add: Strava type hierarchy for Activity type (@jsamoocha, #505) - Add: Athlete stats (@jsamoocha, #507) - Add: Summary segments (@jsamoocha, #509) - Add: SummarySegmentEffort superclass (@jsamoocha, #518) - Add: test for latlon values (@lwasser, #516) - Add: correct type returns following clubs/{id} & clubs/{id}/activities spec (@lwasser, #519) ### Fixed - Fix: Update enhanced types doc (@jsamoocha, #546) - Fix: Replace invariant with covariant container types (@lwasser, @jsamoocha, #510) - Fix: generated model using Pydantic v2 (@jsamoocha, #495) - Fix: Gear and Athlete type hierarchies (+ naive_datetime typing fixes) (@jsamoocha, #526) - Fix: docstrings in model.py (@lwasser, #484) - Fix: return types -- athlete clubs endpoint (@lwasser, #517) - Fix: Updates for activities/id endpoint πŸ™ˆ (@lwasser, #520) - Fix: Activities/{id}/zones & Laps (@lwasser, #524) - Fix: Rename `unithelper.py` --> `unit_helper.py` (@lwasser, #535) - Fix: Update & check photos endpoint (@lwasser) - Fix: Photos endpoint cleanup (@lwasser, #540) - Fix: Activity Comment check / cleanup (@lwasser, #541) - Fix: Migrate to covariant types to fix typing (@lwasser, #530) - Fix: Typing upgrade to Python 3.10+ (@lwasser, #547) - Fix(docs): Cleanup API docs & add migration guide (@jsamoocha, @lwasser, #537) - Fix(docs): Clean up reference docs (@lwasser, #537, #545) ### Removed - Remove: backward compatibility mixin (@jsamoocha, #503) - Remove: deprecated client methods (@lwasser, #514) ### Contributors to this release @jsamoocha, @lwasser, @bmeares - @bmeares made their first contribution in https://github.com/stravalib/stravalib/pull/522 :sparkles: ## v1.7 ### Added - Add: Strava API change - Route objects have a new waypoints attribute (@bot, #480) ### Fixed - Fix: Docs - add contributing section to top bar for easier discovery and a few small syntax fixes in the docs (@lwasser) - Fix: `Manifest.in` file - remove example dir (@lwasser, #307) - Fix: Codecov report wasn't generating correctly (@lwasser, #469) - Fix: Add sport type to create_activity and create type validator method. NOTE: this fix contains a breaking change in `Client.create_activity()` activity_type is now an optional keyword argument rather than a required positional argument (@lwasser, #279) - Fix: Fixes the Strava API update bot (@jsamoocha, #477) - Fix: Cleanup dependencies to only use `pyproject.toml` (@lwasser, #466) - Fix: use `parse_obj` rather than deserialize internally where possible (@lwasser, #358) ## Removed - Remove: functional test suite from stravalib (@lwasser, #457) - Remove: `client.delete_activity` method is no longer supported by Strava (@lwasser, #238) - Remove/Add: Drop Python 3.9, add Python 3.12 (@lwasser, #487) ### Breaking Changes If you have been using the `client.delete_activity` method then your code will no longer work as this method was removed due to being deprecated by Strava. We also are dropping support for Python 3.9 (end of life / no more security fixes in October 2024 and now only getting security fixes) in this release and adding support for 3.12. ### Contributors to this release @jsamoocha, @lwasser, stravalib bot :) ## v1.6 ### Added - Add: Support for Strava's new read rate limits (@jsamoocha, #446) - Add: Improved handling of unexpected activity types (@jsamoocha, #454) ### Fixed - Fix: Forgot to update model CI build to follow new src layout (@lwasser, #438) - Fix: Type annotation on field that are of type timedelta are now correct (@enadeau, #440) - Fix: Correct type for ActivityPhoto sizes attribute (@jsamoocha, #444) - Fix: codespell config - ignore resources dir in tests (@lwasser, #445) - Fix: Ignore documentation autogenerated api stubs (@lwasser, #447) ### Breaking Changes A potentially breaking change is that the RateLimiter's `__call__()` method now has an additional `method` argument representing the HTTP method used for that request. Existing custom rate limiters from users must be updated to this change. ### Contributors to this release @jsamoocha @endeau, @lwasser, ## v1.5 ### Added - Stravalib now includes types annotation, the package is PEP 561 compatible (@enadeau, #423) - Add: Add nox to run tests, build docs, build package wheel/sdist(@lwasser, #395, #397) - Type annotation for all files in the library (@enadeau, #384, #415) - Add blacken-docs and codespell to pre-commit & apply on docs (@lwasser, #391) ### Fixed - Allow parsing of activity with segment of type other that Run and Ride (@JohnScolaro, #434) ### Changed - Infra: Replace flake8 and isort by ruff (@enadeau, #430) ### Removed - Remove python 3.8 support following NEP-29 (@enadeau, #416) ### Contributors to this release @endeau, @lwasser, @JohnScolaro ## v1.4 ### Fixed - Apply flake8 and numpy docstrings to limiter & protocol (@lwasser, #326) - Update client's stream method to warn when using unofficial parameters (@enadeau, #385) - Fix docstring in SleepingRateLimitRule (@enadeau) - Fix: rename `SubscriptionCallback.validate` -> `SubscriptionCallback.validate_token` to avoid conflict with `pydantic.BaseModel` (@lwasser, #394) - Fix: docstrings in model.py, documentation errors, findfonts warning suppression by removing opengraph (temporarily), typing updates (@lwasser, #387) - Fix: read the docs is breaking due to pydantic json warnings, also update python version on build and sync pr previews (@lwasser, #412) - Fix: update master to main in all builds (@lwasser) ### Added - Type annotation to client file (@enadeau, #384) - Add: issue templates for easier debugging / guide users (@lwasser, #408) - Fix: read the docs is breaking due to pydantic json warnings, also update python version on build and sync pr previews (@lwasser, #412) - Fix: update master to main in all builds (@lwasser) ### Contributors to this release @endeau, @lwasser, @jsamoocha ## v1.3.3 ### Fixed - Fix: pins pydantic to v1 in pyproject.toml dependencies (@jsamoocha, #382) ## v1.3.2 ### Added - Add: type checking to limiter, protocol and exc file (@enadeau , #374) ### Fixed - Fix: two minor mistakes in documentation (@enadeau , #375) - Fix: pins pydantic to v1.10.6 (@lwasser, #380) ### Contributors to this release @enadeau, @lwasser - Fix two minor mistakes in documentation (@enadeau , #375) - Add type checking to limiter, protocol and exc file (@enadeau , #374) - Apply flake8 and numpy docstrings to all modules limiter & protocol (@lwasser, #326) ## v1.3.1 ### Added - Add: Add field override in class Segment to support all activity types (@solorisx, #368) ### Fixed - Fix: Bumps Flask version in example code (@jsamoocha, #366) ### Contributors to this release @solorisx, @jsamoocha ## v1.3.0 ### Added - Add: Adds RPE to activity model (@jsamoocha, #355) - Add: support sport_type in client.update_activitiy() (@think-nice-things, #360) ### Fixed - Fix: Move to numpy style docstrings & add black (@lwasser, #365) ### Deprecated - The `activity_type` parameter in the client method `update_activity()` is deprecated and should be replaced by `sport_type`. ### Contributors to this release @jsamoocha, @lwasser, @think-nice-things ## v1.3.0rc0 ### Added - Adds Strava API changes, and datamodel-code-generator bug fix (@jsamoocha, #333) - Add: Replace full legacy model with extensions from the generated pydantic model (@jsamoocha, #324) - Add: Add support for lazy loading related entities (@jsamoocha, #322) - Add: Add support for nested model attributes(@jsamoocha, #316) - Add: replaces implementations for the classes Club, Gear, ActivityTotals, AthleteStats, and Athlete by the generated Pydantic model & backwards compatibility (@jsamoocha, #315) - Add: Workflow for updating strava model when the API changes (@jsamoocha, #302) - Add: `pydantic_autodoc` to sphinx build and reconfigure api structure - p1 (@lwasser, #326) ### Fixed - Fix: Corrects attribute lookup for enum values (@jsamoocha,#329) ### Deprecated - The `BaseEntity` methods `deserialize()`, `from_dict()`, and `to_dict()` are deprecated and will raise a `DeprecationWarning` when they're used. They should be replaced by the pydantic methods `parse_obj()` and `dict()` or `json()`. ### Removed - The complete `attributes` module - All the abstract entity types (e.g. `IdentifiableEntity`, `LoadableEntity`) from the `model` module - Constants used for activity types such as `Activity.RIDE` - `HeartrateActivityZone`, `PowerActivityZone`, `PaceActivityZone` as subtypes of `BaseActivityZone` (the latter is retained) - Everything related to segment leaderboards as this is not supported by Strava anymore ### Contributors to this release @jsamoocha, @lwasser, @oliverkurth ## v1.2.0 ### Added - Add: Upload photo to activity (@gitexel, #318) - Add: Support uploading `activity_file` object with type `bytes` (@gitexel, #308) - Add: Pre-commit hook + instructions and configure precommit.ci bot (@lwasser, #293) ### Fixed - Fix: Internal warnings should be ignored in tests (@jsamoocha, #319) - Fix: `setuptools_scm` bug when installing stravalib remotely via GitHub (@lwasser, #331) - Fix: fix LatLon unmarshal from string type (@oliverkurth, #334) - Fix: allows arithmetic and comparison between multiple quantities (@jsamoocha, #335) ### Contributors to this release @oliverkurth, @gitexel, @jsamoocha, @lwasser ## v1.1.0 ### Added - Add: Development & build/release guide to documentation, edit button to documentation theme, pr template for release (@lwasser, #289) - Add: Integration tests for /routes/{id} and /segments/starred (GET) (@jsamoocha, #250 (partial)) - Add: Add integration tests for all POST/PUT client methods (@jsamoocha, #250 (partial)) - Add: code cov to test suite (@lwasser, #262) - Add: add code of conduct to the repo, update contributing guide + readme badges (@lwasser, #269, #274) - Add: pull request templates for regular pr and release (@lwasser, #294) - Add: Support for python 3.11 ### Fixed - Fix: Move docs to `furo` theme, add `myst` support for markdown, include CONTRIBUTING.md in documentation, enhance intro documentation page and add linkcheck to docs build (@lwasser, #276) - Fix: deprecated set-output command in actions build (@yihong0618, #272) - Fix: Add readthedocs config file to ensure build installs using pip (@lwasser, #270) ### Changed - Change: Replace `units` dependency by `pint` (@jsamoocha, #281) ### Removed - Remove: Support for python 3.7 ### Contributors to this release @lwasser, @yihong0618, @jsamoocha ## v1.0.0 ### Added - Add: Add an option to mute Strava activity on update (@ollyajoke, #227) - Add Update make to build and serve docs and also run current tests (@lwasser,#263) - Add: Move package to build / `setuptools_scm` for version / remove setup.py and add CI push to pypi (@lwasser, #259) ### Fixed - Fix: add new attributes for bikes according to updates to Strava API to fix warning message (@huaminghuangtw, #239) - Fix: Minor bug in PyPI push and also streamlined action build (@lwasser, #265) - Fix: `get_athlete` w new attrs for shoes given strava updates to API (@lwasser, #220) - Fix: Refactor deprecated unittest aliases for Python 3.11 compatibility (@tirkarthi, #223) - Patch: Update readme and fix broken links in docs (@lwasser, #229) ### Changed - Change: Improved unknown time zone handling (@jsamoocha, #242) - Change: Refactor test suite and implement Ci for tests (@jsamoocha, #246) - Change: Remove support for python 2.x (@yihong0618, #254) - Change: Overhaul of documentation, fix links and CI build (@lwasser, #222) ### Contributors to this release @jsamoocha, @yihong0618, @tirkarthi, @huaminghuangtw, @ollyajoke, @lwasser ## 0.10.4 - Fix to unicode regression (@hozn, #217) ## 0.10.3 - Fixes IndexErrors when deserializing empty lists as GPS locations (@hozn, #216) - Fix a few fields in Activity model (@hozn, #201, #214, #207) - deal with tzname without offset and timedelta in string format (@hozn, #195) - Update to docs and repr (@hozn, #200, #205, #206) - Now webhooks use the same domain as the rest of API. (@hozn, #204) - Setting rate_limit_requests=False in Client causes error (@hozn, #157) ## 0.10.2 - More fixes to new new authorization scopes (@hozn, #168) - Added an example oauth app and some small docs updates. - Changed missing-attribute warnings to be debug-level logs. ## 0.10.1 - Fixes of authorization_url / new scopes for new oauth (@hozn, #163, #165) ## 0.10.0 - Implementation of Strava's new auth. (@hozn, #162, #163) ## 0.9.4 - Version bump for dup file upload to pypi. :-[ ## 0.9.3 - Fix mutable parameter defaults in rate-limiter util functions (@hozn, #155) - Add the missing subscription_permissions attr to Athlete (@hozn, #156) ## 0.9.2 - Fix for pip 0.10.0 (@paulte, #149, #150) ## 0.9.1 - Auto-configure the rate limits (not just usage) from response headers. (@hozn, #142) ## 0.9.0 - More API changes to reflect the big privacy changes from Strava. (@hozn, #139, #140) - Fix to kom_type attribute (@hozn, #138) ## 0.8.0 - Fixes to segment leaderboard models for Strava's API BREAKING CHANGE (@hozn, #137) (See https://groups.google.com/forum/#!topic/strava-api/SsL2ytxtZng) - Return ObjectNotFound and AccessUnauthorized HTTPError subclasses for 404 and 401 errors respectively (@hozn, #134) - Return None when there are no activity streams (@hozn, #118) ## 0.7.0 - Updated Activity for new attributes (@hozn, #115, #122) - New segment attributes (@JohnnyLChang, #106) - Streams for a route (@drixselecta, #101) - Activity Uploader improvements (@bwalks, #119) - Added to_dict() method to model objects (@hozn, #127) - Added get_athlete_starred_segments (@wjazdbitu, #117) - Fixed glitches in activity.laps (@hozn, #112) - Fixed bug in club.members (@hozn, #110) ## 0.6.6 - Fix for delete_activity (@jonderwaater, #99) ## 0.6.5 - Updated ActivityPhoto model to support native photos and reverted get_activity_photos behavior for backwards compatibility (@hozn, #98) - Added missing Club attributes (MMI) (@hozn, #97) ## 0.6.4 - Added support for undocumented inclusion of laps in activity details. (@hozn, #96) - Added missing parameter for get_activity_photos (@hozn, #94) - Added missing activyt pr_count attribute (@Wilm0r, #95) - add "starred" property on SegmentExplorerResult (@mdarmetko, #92) ## 0.6.3 - Fixed update_activity to include description (@hozn, #91) ## 0.6.2 - More Python3 bugfixes ## 0.6.1 - Python3 bugfixes (@Tafkas, @martinogden) - Added delete_activity - added context_entries parameter to get_segment_leaderboard method (@jedman) ## 0.6.0 - Use (require) more modern pip/setuptools. - Full Python 3 support (using Six). (@hozn, #69) - Webhooks support (thanks to loisaidasam) (@hozn, #77) - explore_segments bugfix (@hozn, #71) - General updates to model/attribs (@hozn, #64, #73, etc.) ## 0.5.0 - Renamed `Activity.photos` property to `full_photos` due to new conflict with Strava API (@hozn, #45) ## 0.4.0 - Supporting new/removed attribs in Strava API (@hozn, #41, #42) - Added support for joining/leaving clubs (@hozn, #43) - Respect time zones in datetime objects being converted to epochs. (@hozn, #44) ## 0.3.0 - Activity streams data (Ghis) - Friends/followers model attributes (Ghis) - Support for photos (Ghis) - Updates for new Strava exposed API attributes (@hozn) ## 0.2.2 - Fixed the \_resolve_url to not assume running on **nix** system. ## 0.2.1 - Changed Activity.gear to be a full entity attribute (Strava API changed) ## 0.2.0 - Added core functionality for Strava API v3. - Mostly redesigned codebase based on drastic changes in v3 API. - Dropped support for API v1, v2 and the "scrape" module. ## 0.1.0 - First proof-of-concept (very alpha) release. stravalib-2.2/docs/000077500000000000000000000000001475174155400143115ustar00rootroot00000000000000stravalib-2.2/docs/_static/000077500000000000000000000000001475174155400157375ustar00rootroot00000000000000stravalib-2.2/docs/_static/keepme000066400000000000000000000000001475174155400171160ustar00rootroot00000000000000stravalib-2.2/docs/_static/stravalib.css000066400000000000000000000007461475174155400204470ustar00rootroot00000000000000html, body { font-size: 1.02rem; } .admonition { margin-top: 40px; margin-bottom: 40px; } h1 { margin-top: 50px; margin-bottom: 40px; } h2 { margin-top: 60px; } h3 { margin-top: 40px} figcaption .caption-text { text-align: left!important; } figure { margin-top: 60px!important; margin-bottom: 60px!important; } figcaption { font-size: .9em; font-weight: bold; } .admonition p { font-size: 1.1em; font-weight: bold; } stravalib-2.2/docs/conf.py000066400000000000000000000156771475174155400156300ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Configuration file for the Sphinx documentation builder. # # This file does only contain a selection of the most common options. For a # full list see the documentation: # http://www.sphinx-doc.org/en/master/config # -- Path setup -------------------------------------------------------------- import datetime # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. import os import sys import stravalib sys.path.insert(0, os.path.abspath("../")) sys.path.insert(0, os.path.abspath("../src")) # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # -- Project information ----------------------------------------------------- # General project info # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. project = "stravalib" # Automagically create the year vs hand code copyright = ( f"{datetime.date.today().year}, The {project} Developers" # noqa: A001 ) # Grab the package version from the version attr if len(stravalib.__version__.split(".")) > 3: version = "dev" else: version = stravalib.__version__ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ "sphinx.ext.napoleon", # Numpy style doc support "sphinx_remove_toctrees", # Remove api generated stubs from doctree "sphinxcontrib.autodoc_pydantic", # Add json schema display to pydantic models "sphinx.ext.autosummary", # Generate API stubs for each class "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx.ext.ifconfig", "sphinx.ext.viewcode", "sphinx_copybutton", "myst_nb", "sphinx_design", "sphinx_inline_tabs", "sphinxcontrib.mermaid", ] remove_from_toctrees = ["docs/reference/api/*"] # https://autodoc-pydantic.readthedocs.io/en/stable/users/installation.html autodoc_pydantic_model_show_json = True autodoc_pydantic_settings_show_json = False autosummary_generate = True # Colon fence for card support in md myst_enable_extensions = ["colon_fence"] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The main toctree document. master_doc = "index" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [ "_build", "stravalib/tests", "stravalib/tests/functional", "stravalib/tests/unit", "stravalib/tests/resources", "stravalib/docs/reference/api", ] # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # The title in the left hand corner of the docs html_title = "Stravalib Docs" # Theme and css html_theme = "pydata_sphinx_theme" # Link to our repo for easy PR/ editing html_theme_options = { "header_links_before_dropdown": 5, "use_edit_page_button": True, "show_toc_level": 1, # "navbar_align": "left", # [left, content, right] For testing that the navbar items align properly "github_url": "https://github.com/stravalib/stravalib", "footer_start": ["copyright"], "announcement": "Stravalib 2.x is out πŸš€! Check out our migration guide for tips on changes from Stravalib V1!", } html_context = { "github_user": "stravalib", "github_repo": "stravalib", "github_version": "main", } html_static_path = ["_static"] # html_css_files = ["stravalib.css"] # Instagram always throws 429 so ignore it linkcheck_ignore = [r"https://www.instagram.com/accounts/login/"] # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] # Intersphinx configuration intersphinx_mapping = { "Python": ("http://docs.python.org/", None), } # Default to using the order defined in source. autodoc_default_options = { "member-order": "alphabetical", "members": True, "undoc-members": True, "inherited-members": True, } # Here we globally customize what methods and attrs are included in the docs. # there is no good way to do this (that I can find) for an entire inherited # class int_dir = dir(int) methods_to_skip = [ member for member in int_dir if not (member.startswith("__") and member.endswith("__")) ] def skip_member(app, what, name, obj, skip, options): """ Determine whether a member should be skipped during Sphinx documentation generation. This function is used as a callback to the `autodoc-skip-member` event in Sphinx. It allows you to programmatically decide whether a particular member (such as a method or attribute) should be included in the documentation. Parameters ---------- app : `sphinx.application.Sphinx` The Sphinx application object. what : str The type of the object which the member belongs to (e.g., 'module', 'class', 'exception', 'function', 'method', 'attribute'). name : str The name of the member. obj : object The member object itself. skip : bool A boolean indicating if autodoc will skip this member if the user-defined callback does not override the decision. options : object The options given to the directive: an object with attributes `inherited_members`, `undoc_members`, `show_inheritance`, and `noindex` that are `True` if the flag option of the same name was given to the auto directive. Returns ------- bool True if the member should be skipped, False otherwise. """ # Skip methods defined above if name in methods_to_skip: return True # Skip special methods if name.startswith("__") and name.endswith("__"): return True # Otherwise, do not skip return skip def setup(app): """ Connect the `skip_member` function to the `autodoc-skip-member` event in Sphinx. This function is used to set up the Sphinx extension by connecting the `skip_member` function to the `autodoc-skip-member` event. This allows the `skip_member` function to control which members are included or excluded from the generated documentation. Parameters ---------- app : `sphinx.application.Sphinx` The Sphinx application object. """ app.connect("autodoc-skip-member", skip_member) stravalib-2.2/docs/contributing/000077500000000000000000000000001475174155400170205ustar00rootroot00000000000000stravalib-2.2/docs/contributing/build-release-guide.md000066400000000000000000000107671475174155400231650ustar00rootroot00000000000000# Stravalib build and release guide This page outlines the build structure and release workflow for stravalib. ## Stravalib packaging overview For packaging we use `setuptools` for packaging and the `build` package to create a wheel and distribution for pushing to PyPI. ## Package versioning To keep track of stravalib versioning, we use `setuptools_scm`. Setuptools_scm is a behind the scenes tool that uses the most current tag in the repository to determine what version of the package is being built. `setuptools_scm` creates a `_version_generated.py` file upon build using that tag. ```{warning} If you build the package locally, the `_version_generated.py` file should NEVER be committed to version control. It should be ignored via our `.gitignore` file ``` If you wish to build stravalib locally to check out the .whl and source distribution (SDist): ``` make build ``` When you run `make build`, it will do a few things 1. it will create a `dist` directory with the wheel and the package SDist tarball. You can see the version of `stravalib` in the name of those files: ```bash dist/ stravalib-1.0.0.post27-py3-non-any.whl stravalib-1.0.0.post27.tar.gz ``` 2. `make build` also invokes `setuptools_scm` to create a `_version_generated.py` file in the stravalib package directory: ```bash stravalib/ stravalib/ _version_generated,py ``` ## Our PyPI release workflow The entire release workflow is automated and can be completed on fully in the GitHub.com interface if you wish. We follow [semantic version](https://semver.org/) best practices for our release workflow as follows: - MAJOR version when you make incompatible API changes - MINOR version when you add functionality in a backwards compatible manner - PATCH version when you make backwards compatible bug fixes ### How to make a release to PyPI ```{note} The build workflow explained below will run and push to test PyPI on every merge to the main branch of stravalib. Thus before you create a pull request to initiate a new release, please check out stravalib on [test pypi](https://pypi.org/project/stravalib/) to: 1. Make sure that the README file and other elements are rendering properly 2. You can also install the package from test PyPI as an additional check! ``` To make a release: - βœ”οΈ 1. Determine with the other maintainers what release version we are moving to. This can be done in an issue. - βœ”οΈ 2. Create a new **pull request** using the release pull request template that does the following: - Organizes the changelog.md unreleased items into added, fixed and changed sections - Lists contributors to this release using GitHub handles - Adds the version number of that specific release. Below you can see an example of what these changelog changes looked like when we bumped to version 1.0 of stravalib. _(Some of the original change log content is removed to keep this page shorter)_ ``` ## Unreleased ## v1.0.0 ### Added * Add: Add an option to mute Strava activity on update (@ollyajoke, #227) * Add Update make to build and serve docs and also run current tests (@lwasser,#263) * Add: Move package to build / `setuptools_scm` for version / remove setup.py and add CI push to pypi (@lwasser, #259) ### Fixed * Fix: add new attributes for bikes according to updates to Strava API to fix warning message (@huaminghuangtw, #239) * Fix: Refactor deprecated unittest aliases for Python 3.11 compatibility (@tirkarthi, #223) * Patch: Update readme and fix broken links in docs (@lwasser, #229) ### Changed * Change: Refactor test suite and implement Ci for tests (@jsamoocha, #246) * Change: Remove support for python 2.x (@yihong0618, #254) ### Contributors to this release @jsamoocha, @yihong0618, @tirkarthi, @huaminghuangtw, @ollyajoke, @lwasser ``` - βœ”οΈ 3. Once another maintainer approves the pull request, you can merge it. You are now ready to make the actual release. - βœ”οΈ 4. In GitHub.com go to `Releases` and prepare a new release. When you create that release you can specify the tag for this release. Use `v` in the tag number to maintain consistency with previous releases. This is the ONLY manual step in the release workflow. Be sure to create the correct tag number: example `v1.0.1` for a patch version. Copy the updated changelog information into the body of the release. - βœ”οΈ 5. Now hit `publish release`. When you publish the release, a GitHub action will be enabled that will: 1. build the wheel and SDist and 2. publish the distribution to PyPI Congratulations! You've just created a release of stravalib! stravalib-2.2/docs/contributing/development-guide.md000066400000000000000000000551321475174155400227650ustar00rootroot00000000000000# Development Guide for Contributing to Stravalib ```{note} * Please make sure that you've read our [contributing guide](how-to-contribute.md) before reading this guide. * If you are looking for information on our package build structure and release workflow, please see our build and [release guide](build-release-guide) ``` The steps to get started with contributing to stravalib are below. To begin, fork and clone the [stravalib GitHub repository](https://github.com/stravalib/stravalib). ## Fork and clone the stravalib repository ### 1. Fork the repository on GitHub To create your own copy of the stravalib repository on GitHub, navigate to the [stravalib/stravalib](https://github.com/stravalib/stravalib) repository and click the **Fork** button in the top-right corner of the page. ### 2. Clone your fork locally Next, use `git clone` to create a local copy of your stravalib forked repository on your local filesystem: ```bash $ git clone git@github.com:your_name_here/stravalib.git $ cd stravalib/ ``` Once you have cloned your forked repository locally, you are ready to create a development environment. ## Setup a local development environment We suggest you create a virtual environment on your computer to work on `stravalib`. ::::{tab-set} ::: {tab-item} venv Follow these instructions if you prefer using `venv` to create virtual environments. To begin, create a new virtual environment in the project directory. This will create a local environment directory called `stravalib_env`: ```bash $ python -m venv stravalib_env ``` Next, activate the environment. On macOS and Linux: ```bash $ source stravalib_dev_env/bin/activate ``` On Windows: ```bash $ .\stravalib_dev_env\Scripts\activate ``` ::: ::: {tab-item} Conda If you prefer Conda for environment management, use the instructions below. Anaconda and Miniconda are two commonly-used conda Python distributions. If you are unsure of which distribution to use, [we suggest miniconda](https://docs.conda.io/en/latest/miniconda.html) as it is a lighter-weight installation. To begin, create a new `conda` environment called `stravalib_dev`. ```bash $ conda env create -f environment.yml ``` Next, activate the environment. ``` $ conda activate stravalib_dev ``` ::: :::: Once you have a virtual environment created, you are ready to install stravalib's package dependencies and the `stravalib` package in editable mode (`-e`). Editable mode allows you to update the package and test those updates in real-time. ```bash # Install the package in editable model and all requirements $ pip install -e ".[build, tests, docs]" ``` :::{note} If you only want to install dependencies for building and testing the package (and exclude the docs requirements), you can run: `pip install -e ".[build, tests]"` Quotes around `".[build, tests]"` are required for some shells such as `zsh` but not for all shells. ::: (ci_api_updates)= ## Architecture Overview ![Stravalib Architecture](../images/stravalib_architecture.png) Stravalib contains the following main components: At the core, a (pydantic) domain model is generated and updated by a bot via pull requests. This model reflects the officially published API specification by Strava and is stored in the module `strava_model.py`. This file should never be edited manually. Instead, the stravalib bot will suggest changes to the model through pull requests that can then be merged by stravalib maintainers. The module `model.py` contains classes that inherit from the official Strava domain model in `strava_model.py`. This module supports custom typing, unit conversion, (de-)serialization behavior, and support for undocumented Strava features. The module `protocol.py` manages the sending of HTTP requests to Strava and handling the received responses (including rate limiting). It is used by methods in `client.py` to de-serialize raw response data into the domain entities in `model.py`. ## Python support We loosely follow the [Numpy guidelines defined in NEP 29](https://numpy.org/neps/nep-0029-deprecation_policy.html) for Python version support. However, in some cases, we may decide to support older versions of Python, following community demand. ## Code style, linting & typing We use several tools to maintain consistent code formatting and adhere to the [Python Enhancement Protocol (PEP) 8](https://peps.python.org/pep-0008/) standards, which outline best practices for Python code readability and structure. Below are the primary tools configured for this project: - [black](https://black.readthedocs.io/en/stable/): An auto-formatter that enforces consistent code style. Although Black’s default line length is 88 characters, we configure it to 79 characters to better align with [PEP 8 line width guidelines](https://peps.python.org/pep-0008/#maximum-line-length). - [ruff](https://github.com/charliermarsh/ruff): A fast, all-in-one Python linter that covers many functions formerly provided by separate tools like `flake8` and `isort`. Ruff performs both linting and import sorting, identifying unused imports, variables, and other PEP 8 inconsistencies. - [codespell](https://github.com/codespell-project/codespell): A spelling checker for code comments and documentation, helping to catch typos in Python, Markdown, and RST files. - [blacken-docs](https://github.com/adamchainz/blacken-docs): A tool for applying Black’s formatting to Python code blocks, ensuring consistent code style in documentation. ### Pre-commit Hook Setup For local development, we use [`pre-commit`](https://pre-commit.com/), which automatically runs each code format and linting tool configured in the `pre-commit-config.yaml` file. Once installed, `pre-commit` will execute each tool in the configuration file every time you make a commit. With pre-commit hooks setup, here’s what happens when you make a new commit to our codebase: 1. **black**: Automatically formats code to meet style guidelines. If the formatting is incorrect, `black` will reformat it for you. 2. **ruff**: Runs linting checks and fixes minor issues automatically, including sorting imports. 3. **codespell**: Identifies typos in code comments and documentation. You will need to fix these manually. 4. **blacken-docs**: Blacken docs will format any code snippets provided in our documentation to match Black's guidelines above If issues are found that cannot be automatically corrected, you’ll see a list of errors that need to be addressed before proceeding with your commit. ### Setup and run the pre-commit hooks The configuration for all of the pre-commit hooks is found in the **.pre-commit-config.yaml** file. To set up our pre-commit hooks locally: 1. First, make sure that pre-commit is installed. You can install pre-commit using `pip` or `pipx`. ```bash $ pip install pre-commit ``` Next, install all of the hooks into your stravalib development environment. ```bash $ pre-commit install ``` :::{tip} You can run all pre-commit hooks locally without a commit by using: ```bash $ pre-commit run --all-files ``` You can also run a single hook using the following: ``` # Only run ruff # pre-commit run ruff ``` ::: ### Pre-commit.ci bot We use the `https://pre-commit.ci` bot, in addition to pre-commit in our local build to manage pull requests. The configuration for this bot can be found in the ci: section of the `pre-commit-config.yaml` file. This bot can run all of the code format hooks on every pull request if it's set to do so. Currently, we have the bot set to run only when it's asked to run on a PR. To call the bot on a pull request, add the text: `pre-commit.ci run` as a single-line comment in the pull request. The bot will automatically run all of the hooks that it is configured to run. ```{tip} If you have an open Pull Request but you need to make some changes locally, and the bot has already run on your pull request and added a commit, you can force push to the pull request to avoid multiple bot commits. To do this: * Do not pull down any changes from the pull request, * Commit your changes locally, When you are ready to push your local changes use: `git push origin branch-name-here --force` If you have not yet pulled down pre-commit bot's changes, this will force the branch to be in the same commit state as your local branch. ``` ### Typing using mypy We use [mypy](https://mypy.readthedocs.io/) to ensure proper typing throughout our library. To run `mypy` across Python versions, use: `nox -s mypy` Similar to running tests, if you are missing a version of Python, `nox` will skip that run and continue to the next version. ```bash ❯ nox -s mypy nox > Running session mypy-3.10 nox > Missing interpreters will error by default on CI systems. nox > Session mypy-3.10 skipped: Python interpreter 3.10 not found. nox > Running session mypy-3.11 ``` ## Code format and syntax If you are contributing code to `stravalib`, please be sure to follow PEP 8 syntax best practices. ### Docstrings **All docstrings** should follow the [numpy style guide](https://numpydoc.readthedocs.io/en/latest/format.html#docstring-standard). All functions/classes/methods should have docstrings with a full description of all arguments and return values. ```{warning} This also will be updated once we implement a code styler While the maximum line length for code is automatically set by *Black*, docstrings must be formatted manually. To play nicely with Jupyter and IPython, **limit docstrings to 79 characters** per line. ``` ## About the stravalib test suite Stravalib has a set of unit and integration tests that can be run locally and that also run in our CI infrastructure using GitHub Actions. To avoid direct API calls which require authentication, when running our test suite, we have a mock fixture and and infrastructure setup. ### Unit and integration test suite We have set up the test suite to run on the stravalib package as installed. Thus, when running your tests, it is critical that you have a stravalib development environment setup and activated with the stravalib package installed from your fork using pip `pip install .` You can run the tests using make as specified below. Note that when you run the tests this way, they will run in a temporary environment to ensure that they are running against the installed version of the package that you are working on. To run the test suite across all Python versions that we support use: ``` nox -s tests ``` `nox -s tests` does a few things: 1. It creates a temporary directory called `tmp-test-dir-stravalib` in which your tests are run. We create this test directory to ensure that tests are being run against the installed version of stravalib (with the most recent local development changes as installed) rather than the flat files located in the GitHub repository. 2. It runs the tests and provides output (see below) 3. Finally it removes the temporary directory To run tests for a specific Python version use: `nox -s tests-python-version-here`. For example, the command below runs our tests on Python 3.10 only. ```bash nox -s tests-3.10 ``` ### Test code coverage We use [pytest-cov](https://pytest-cov.readthedocs.io/en/latest/) to calculate test coverage. When you run `nox -s tests` pytest-cov will provide you with coverage outputs locally. You can ignore the returned values for any files in the `test` directory. Example output from `nox -s test`: ```bash pytest --cov stravalib stravalib/tests/unit stravalib/tests/integration =================================================== test session starts =================================================== platform darwin -- Python 3.8.13, pytest-7.2.0, pluggy-1.0.0 rootdir: .../stravalib plugins: cov-4.0.0 collected 105 items stravalib/tests/unit/test_attributes.py ............... [ 14%] stravalib/tests/unit/test_client_utils.py ....... [ 20%] stravalib/tests/unit/test_limiter.py ............. [ 33%] stravalib/tests/unit/test_model.py ....... [ 40%] stravalib/tests/integration/test_client.py ............................................................... [100%] ---------- coverage: platform darwin, python 3.8.13-final-0 ---------- Name Stmts Miss Cover ---------------------------------------------------------------------------- stravalib/__init__.py 2 0 100% stravalib/_version.py 2 0 100% stravalib/_version_generated.py 2 0 100% stravalib/attributes.py 170 19 89% stravalib/client.py 439 180 59% stravalib/exc.py 34 3 91% stravalib/model.py 709 126 82% stravalib/protocol.py 130 39 70% stravalib/unit_helper.py 16 1 94% stravalib/util/__init__.py 0 0 100% stravalib/util/limiter.py 122 27 78% ---------------------------------------------------------------------------- TOTAL ``` ### Code coverage reporting on pull requests with codecov We use an integration with [codecov.io](https://about.codecov.io) to report test coverage changes on every pull request. This report will appear in your pull request once all of the GitHub action checks have run. ```{note} The actual code coverage report is uploaded on the GitHub action run on `ubuntu` and `Python 3.11`. When that step in the actions completes, the report will be processed and returned to the pull request. ``` ## Tests & the stravalib mock fixture To run integration tests that ensure stravalib is interacting with API data correctly, Stravalib uses a mock object accessed through a `pytest` fixture `stravalib.tests.integration.strava_api_stub.StravaAPIMock` that is based on `responses.RequestsMock`. This fixture adds a mock that prevents requests from being made to the Strava API. Instead, it creates responses using the endpoint provided and the `swagger.json` file that is found both online and within the `stravalib/src/stravalib/tests/resources/` directory that are based on examples from the published Strava API documentation. :::{tip} Example usages of this fixture can be found in the {py:mod}`stravalib.tests.integration.test_client` module. ::: :::{mermaid} flowchart TD A["fab:fa-strava Stravalib Test Suite"] --> C["**mock_strava_api fixture**
(defined in conftest)"] C -- Creates instance of --> D["**StravaAPIMock**
strava_api_stub.py module
Inherits from responses.RequestsMock"] D -- Returns fake response data using: --> G["**swagger.json**
(local or online)"] style C color:#FFFFFF, stroke:#00C853, fill:#AA00FF style G color:#FFFFFF, fill:#d35400, stroke:#AA00FF style A color:#FFFFFF, fill:#d35400, stroke:#AA00FF ::: ### How the mock fixture works The `stravalib` test suite is supported by the {py:class}`stravalib.tests.integration.strava_api_stub.StravaAPIMock` mock API object, which is used in most client GET method tests through a `pytest` fixture. The Strava API mock object: 1. **Matches Endpoints**: Attempts to match the endpoint being tested with a corresponding path in `swagger.json`, using either an online or local copy. This mock expects a relative URL that aligns with a path in the `swagger.json` file (e.g., `/activities/{Id}`) and includes the appropriate HTTP method and status code. 2. **Provides Example Responses**: Retrieves the example JSON response associated with the matched endpoint in `swagger.json` and uses it as the mock response body. The example response can be customized by using the `response_update` parameter, which accepts a dictionary of values to override fields in the default response. If the response is a JSON array, the `n_results` argument can specify how many objects to return. If the object can find an endpoint match, it then returns the example JSON response (or the updated response if you use the update parameter) to use in the test. :::{tip} The `swagger.json` file is an API specification document describing the available endpoints in the Strava API, including methods, parameters, and expected responses for each endpoint. It defines the API structure in JSON format and includes example responses for testing. This file is used in `stravalib`'s tests to mock API interactions and validate the expected structure and content of responses. The mock API object checks if `swagger.json` is accessible online; if not, it uses a local version located in the `tests/resources` directory within `stravalib`. ::: ### Mock fixture features To call the mock fixture in a test, you 1. Create a new test and add the `mock_strava_api` fixture as an input to the test function. The test below will try to access the `/athlete/activities` Strava endpoint which returns an athlete's activities. Here, the fixture will bypass trying to access the real online API. And instead, will find the `/athlete/activities` endpoint in the Strava online or local `swagger.json` file. When you call {py:func}`stravalib.client.Client.get_activities()`, the mocked endpoint will return the sample data provided in the `swagger.json` file. ```python def test_example(mock_strava_api, client): """An example test""" mock_strava_api.get("/athlete/activities") activity_list = list(client.get_activities()) assert len(activity_list) == 4 ``` The mock fixture object provides parameters that allow you to modify a test. Sometimes you may want to update the default example return data in the swagger.json file. This might happen if you want to intentionally "break" a test to ensure that the client call responds appropriately. To modify the returned sample data use the `response_update` parameter. Below you update the response id key to be another value. ```python def test_example_test(mock_strava_api, client): """An example test""" mock_strava_api.get( "/athlete/activities", response_update={"id": 12345}, ) activity_list = list(client.get_activities()) ``` You can also specify the number of results that you'd like to see in the mock output using the `n_results` parameters. ```python def test_example_test(mock_strava_api, client): """An example test""" mock_strava_api.get( "/athlete/activities", response_update={"id": 12345}, n_results=4, ) activity_list = list(client.get_activities()) ``` :::{tip} Stravalib uses lazily loaded entities when returning results from endpoint such as activities that may include multiple response objects in the return. As such, a mocked call to {py:func}`stravalib.client.Client.get_activities` will not actually initiate a get response until you try to access the first object returned in the {py:class}`stravalib.client.BatchedResultsIterator` object. ::: ## Documentation `Stravalib` documentation is created using `sphinx` and the [`pydata_sphinx_theme`](https://pydata-sphinx-theme.readthedocs.io/en/stable/index.html) theme. `Stravalib` documentation is hosted on [ReadtheDocs](https://readthedocs.org). The final online build that you see on readthedocs happens on the readthedocs website. Our continuous integration GitHub action only tests that the documentation builds correctly. It also tests for broken links. The readthedocs build is configured using the `.readthedocs.yml` file rather than from within the readthedocs interface as recommended by the readthedocs website. The badge below (also on our `README.md` file) tells you whether the readthedocs build is passing or failing. [![Documentation Status](https://readthedocs.org/projects/stravalib/badge/?version=latest)](https://stravalib.readthedocs.io/en/latest/?badge=latest) Currently [@hozn](https://www.github.com/hozn), [@lwasser](https://www.github.com/lwasser) and [@jsamoocha](https://www.github.com/jsamoocha) have access to the readthedocs `stravalib` documentation build Online documentation will be updated on all merges to the main branch of `stravalib`. ### Build documentation locally To build the documentation, first activate your stravalib development environment which has all of the packages required to build the documentation. Then, use the command: ```bash $ nox -s docs ``` This command: - Builds documentation - Builds `stravalib` API reference documentation using docstrings within the package - Checks for broken links After running `nox -s docs` you can view the built documentation in a web browser locally by opening the following file on your computer: ``` /your-path-to-stravalib-dir/stravalib/docs/_build/html/index.html ``` You can also view any broken links in the output.txt file located here: `/your-path-to-stravalib-dir/stravalib/docs/_build/linkcheck/output.txt` ### Build locally with a live server We use `sphinx-autobuild` to build the documentation in a live web server. This allows you to see your edits automatically as you are working on the text files of the documentation. To run the live server use: ```bash $ nox -s docs-live ``` ```{note} There is a quirk with autobuild where included files such as the CHANGELOG will not update live in your local rendered build until you update content on a file without included content. ``` ### Stravalib API Documentation The API reference can be found [here](reference). The *autodoc* sphinx extension will automatically create pages for each function/class/module listed there. You can reference classes, functions, and modules from anywhere (including docstrings) using * {py:func}\`package.module.function\`, * {py:func}\`package.module.Class.method\`, * {py:class}\`package.module.class\`, or * {py:mod}\`package.module\`. Sphinx will create a link to the automatically generated page for that function/class/module. ### About the documentation CI build Once you create a pull request, GitHub actions will build the docs and check for any syntax or url errors. Once the PR is approved and merged into the main branch of the `stravalib/stravalib` repository, the docs will build and be [available at the readthedocs website](https://stravalib.readthedocs.io/en/latest/). ### Cleanup of documentation and package build files To clean up all documentation build folders and files, run the following command from the root of the `stravalib` directory: ```bash $ nox -s clean-docs ``` To clean up build files such as the package **.whl**, and other temporary files created when building `stravalib` distributions and running tests, run: ```bash $ nox -s clean_build ``` stravalib-2.2/docs/contributing/how-to-contribute.md000066400000000000000000000000441475174155400227310ustar00rootroot00000000000000 ```{include} ../../CONTRIBUTING.md stravalib-2.2/docs/contributing/inheritance.md000066400000000000000000000074331475174155400216420ustar00rootroot00000000000000# Stravalib's Inheritance Patterns Stravalib's API is built directly from Strava's `swagger.json` example response data. We use the Pydantic `BaseModel` combined with `datacodegen` to build our base model objects (found in `strava_model.py`), which we then enhance and update to align with the data actually returned by the Strava API. This page will help you understand Stravalib's inheritance patterns. In our experience maintaining Stravalib, the Strava API online specification and the `swagger.json` file don't always align perfectly with the data actually returned by Strava. As a result, you will notice that we frequently overwrite data types to match the actual data we see returned from the Strava API for each endpoint. [More information on this process can be found here.](ci_api_updates) Below, we present the inheritance schema for Stravalib. ## Inheritance overview At a high level, there are two modules: * {py:mod}`stravalib.model` and * {py:mod}`stravalib.strava_model`. `strava_model` is generated automatically using a CI build from the Strava API. Stravalib uses the model.py module to do a few things: 1. It supports inheritance of the {py:class}`stravalib.model.BoundClientEntity`, which supports API calls for lazily loaded properties, 2. it allows us to override {py:mod}`stravalib.strava_model` attributes that have typed attributes that don't align with the actual API responses, and 3. allows us to add attributes that are found in the returned data but not documented or found in the swagger.json response. The full inheritance pattern, which includes inheritance from both `strava_model` and `pydantic.BaseModel`, is below. ### Inheritance diagram showing the relationship between strava_model.py and model.py :::{mermaid} classDiagram direction BT %% Activities in model namespace `model.MetaActivity` --|> `model.BoundClientEntity` `model.SummaryActivity` --|> `model.MetaActivity` `model.DetailedActivity` --|> `model.SummaryActivity` %% Activities in strava_model namespace `strava_model.SummaryActivity` --|> `strava_model.MetaActivity` `strava_model.DetailedActivity` --|> `strava_model.SummaryActivity` %% Define inheritance relationships between model and strava_model `model.MetaActivity` --|> `strava_model.MetaActivity` `model.SummaryActivity` --|> `strava_model.SummaryActivity` `model.DetailedActivity` --|> `strava_model.DetailedActivity` ::: ## Model object inheritance patterns The {py:mod}`stravalib.model` module contains core objects that inherit from and modify objects in {py:mod}`strava_model.py`. The `strava_model.py` file is generated directly from the Strava `swagger.json` API response. Our main client class provides methods for making API `GET` and `PUT` requests. To support these requests and enable lazily loaded operations, all Strava model meta-level objects inherit from `BoundClientEntity`, which stores the token credentials needed for authenticated API calls. This ensures that summary and detailed-level objects can also support lazily loaded operations. ### Inheritance diagram showing the relationship between Detailed, Summary and Meta classes and BoundClientEntity :::{mermaid} classDiagram direction BT %% Activities MetaActivity --|> BoundClientEntity SummaryActivity --|> MetaActivity DetailedActivity --|> SummaryActivity %% Athletes MetaAthlete --|> BoundClientEntity SummaryAthlete --|> MetaAthlete DetailedAthlete --|> SummaryAthlete %% Clubs MetaClub --|> BoundClientEntity SummaryClub --|> MetaClub DetailedClub --|> SummaryClub ::: stravalib-2.2/docs/contributing/intro.md000066400000000000000000000023431475174155400204770ustar00rootroot00000000000000# Contribute to stravalib We welcome contributions of all kinds to stravalib! Below are some resources to help you get started. ::::{grid} 1 1 1 2 :class-container: text-center :gutter: 3 :::{grid-item-card} :link: how-to-contribute :link-type: doc ✨ **Contributing Guide** ✨ ^^^ Contributing guidelines for stravalib. ::: :::{grid-item-card} :link: development-guide :link-type: doc ✨ **Development Guide** ✨ ^^^ Learn about our development infrastructure and workflows. ::: :::{grid-item-card} :link: build-release-guide :link-type: doc ✨ **Build & Release Guide** ✨ ^^^ Learn about our build and release workflow. We use version control based versioning. ::: :::{grid-item-card} :link: resources-for-new-contributors :link-type: doc ✨ **Contributor Resources** ✨ ^^^ If you are new to contributing to open source software, these resources will help you get started. ::: :::{toctree} :hidden: :caption: Contributing :maxdepth: 2 Contributing Guide <../contributing/how-to-contribute> Development Guide <../contributing/development-guide> Inheritance <../contributing/inheritance> Build & Release Guide <../contributing/build-release-guide> New Contributor Resources <../contributing/resources-for-new-contributors> ::: stravalib-2.2/docs/contributing/resources-for-new-contributors.md000066400000000000000000000025101475174155400254600ustar00rootroot00000000000000# Contributor resources ## Contributing Code **Is this your first contribution?** Please take a look at these resources to learn about git and pull requests (don't hesitate to ask questions: * [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/). * Aaron Meurer's [tutorial on the git workflow](https://www.asmeurer.com/git-workflow/) * [How to Contribute to an Open Source Project on GitHub](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github) If you're new to working with git, GitHub, and the Unix Shell, we recommend starting with the [Software Carpentry](https://software-carpentry.org/) lessons, which are available in English and Spanish: * [Version Control with Git](https://swcarpentry.github.io/git-novice/) / spanish: [Control de versiones con Git](https://swcarpentry.github.io/git-novice-es/) * [The Unix Shell](https://swcarpentry.github.io/shell-novice/) / spanish: [La Terminal de Unix](https://swcarpentry.github.io/shell-novice-es/) ## Additional contribution resources For more information on contributing to open source projects, checkout: * [GitHub's contribution guide](https://docs.github.com/en) is a great starting point if you are new to version control. * The [Zen of Scientific Software Maintenance](https://jrleeman.github.io/ScientificSoftwareMaintenance/) stravalib-2.2/docs/get-started/000077500000000000000000000000001475174155400165345ustar00rootroot00000000000000stravalib-2.2/docs/get-started/activities.md000066400000000000000000000210311475174155400212170ustar00rootroot00000000000000(activities)= # Get Strava activity data This page overviews working with your Strava activity data using the stravalib Python library. ## Retrieve an activity To access data for a given activity, use the `client.get_activity` method and provide the `activity_id`. The `client.get_activity` method returns a {py:class}`stravalib.model.DetailedActivity` object. :::{note} All of the commands below require you first to authenticate using a client object containing a token. `from stravalib.client import Client` ::: ```{code-block} pycon # This command assumes that you have already authenticated the client object. >>> activity = client.get_activity(1234) >>> type(activity) ``` The `DetailedActivity` object has many properties such as type, distance, and elapsed time. ```{code-block} pycon # Get the activity type >>> activity = client.get_activity(207650614) >>> print(f"type = {activity.type}") # Output root='Hike' ``` ```pycon # Get activity distance >>> print(f"The distance is: {a.distance}") The distance is: 1234 ``` ## Stravalib offers unit conversion helpers stravalib uses the [python Pint library](https://pypi.org/project/Pint/) to facilitate working with the values in the API that have associated units (e.g. distance, speed). You can use the pint library directly or through the `stravalib.unithelper` module for shortcuts You can convert the distance value to another unit if you import stravalib's `stravalib.unit_helper` module. ```python from stravalib import unit_helper unit_helper.feet(activity.distance) # Output: unit_helper.miles(activity.distance) # unit_helper.kilometers(activity.distance) # ``` Similarly, you can access elapsed time and convert it to a `timedelta` object. ```python activity.elapsed_time # Output: 2273 activity.elapsed_time.timedelta # Output: ``` ### DetailedActivity iterator objects Some items returned by stravalib will be returned as a {py:class}`BatchedResultsIterator` object. A `BatchedResultsIterator` object contains a list of items associated with an activity - for example, a list of comments or kudos. If an attribute contains a discrete value, you can access the item's value as an attribute like this: ```{code-block} pycon >>> activity.comment_count 3 ``` However if it's a `BatchedResultsIterator`, you will see this: ```{code-block} pycon >>> activity.comments ``` You can access each comment or item within a `BatchedResultsIterator` object using a Python loop or a list comprehension: ```{code-block} pycon >>> for comment in activity.comments: >>> print(f"Comment by: {comment.athlete.firstname}, {comment.text}") Comment by: YourFriendsNameHere: Not the pool! ``` ## Get Strava activity streams {py:func}`stravalib.client.Client.get_activity_streams` returns a dictionary containing time-series data associated with your activity. You can specify the stream variables that you want to be returned by providing a list of accepted types to the `types` parameter. The type options for streaming data can be found here: {py:class}`stravalib.strava_model.StreamType`. The data returned from this request is a dictionary object that looks something like this: ```python streams """ # Output dict: {'latlng': Stream(...), 'distance': Stream(...), 'altitude': Stream(...)} """ ``` The Python dictionary's key represent the stream type: ```python if "altitude" in streams.keys(): print(streams["altitude"].data) ``` :::{tip} The resolution of the streaming data refers to the number of data points returned for your activity. Low resolution means fewer points. Low-resolution data returns a smaller dataset; this data will be faster to download. Alternatively, high-resolution data will return a larger dataset and is slower to download. However, the output spatial data will look more "smooth" as more points are associated with the activity path. ::: :::{warning} Collecting streaming data is API (and memory) intensive! ::: ### Full-resolution data When accessing streaming data, if you don't set a resolution value it will default to `None`. In this case, Strava will return the full-resolution representation of your data. :::{warning} The `resolution` parameter for Strava data streams is undocumented and could (and has) changed at any time. ::: ### Low-resolution data request ```python # Request desired stream types types = ["latlng", "altitude"] streams = client.get_activity_streams( activity_id=123456, types=types, resolution="low", series_type="distance", ) print(type(streams)) # Output # dict print(len(streams_low["latlng"].data)) # Output: This will return the lowest resolution data # 100 ``` ### Medium resolution data request ```python # Request desired stream types types = ["latlng", "altitude"] streams = client.get_activity_streams( activity_id=123456, types=types, resolution="medium", series_type="distance", ) print(len(streams_med["latlng"].data)) # Output: notice there are more data points compared to a low resolution request # 983 ``` ### High resolution data request ```python # Request desired stream types types = ["latlng", "altitude"] streams = client.get_activity_streams( activity_id=123456, types=types, resolution="high", series_type="distance", ) print(len(streams_high["latlng"].data)) # Output: notice there are more data points compared to both low and medium resolution. This is the max resolution possible. # 1729 ``` :::{note} If your activity is short, the number of data points returned for medium vs. low-resolution data may not be significantly different. ::: ### Access activity zones Additionally, you can retrieve activity zones using: {py:func}`stravalib.client.Client.get_activity_zones`; activity laps can be retrieved with {py:func}`stravalib.client.Client.get_activity_laps`. ### Access photos for an activity To get photos for an associated activity, you can use`client.get_activity_photos(activity_id, max_resolution)`. Here, max_resolution is the maximum resolution of photos that you want to collect in pixels. ```python photos = client.get_activity_photos(id_w_photos, 2000) photos # Expected Output: # ``` The photos endpoint returns a `BatchedResultsIterator` object that you can loop through to access photo metadata and URLs for downloading the photos. ```python for i, photo in enumerate(photos): print("photo") ``` You can access photo attributes like this: ```python photo.default True photo.sizes {"2000": [2048, 1261]} photo.urls {"2000": "https://dgtzuqphqg23d.cloudfront.net/url-is-here.jpg"} ``` ## Get a list of Strava activities You can access multiple activities using the {py:func}`stravalib.client.Client.get_activities` method. This method will return a `BatchedResultsIterator` that you can loop through. :::{note} The activities `batchedResultsIterator` object stores data that allows stravalib to access activity data when you iterate through the object. This approach limits the API requests made up front to Strava. ::: Below, you request activities that were recorded after Jan 1, 2024. ```python activities = client.get_activities(after="2024-01-01", limit=5) print(activities) # Expected output: # ``` Using the limit parameter will limit the number of activities Stravalib will retrieve. Above, you retrieve the first 5 activities. ```python for i, activity in enumerate(activities): print(i) """ 0 1 2 3 4 print(f"I found {i+1} activities for you.") I found 5 activities for you. """ ``` :::{tip} To get activities starting with the oldest first, specify a value for the `after=` parameter when calling `client.get_activities`. Use the `before=` parameter to get the last 5 activities. ::: ## Get club member activities You can also use stravalib to access activities associated with a club. To do this, use {py:func}`stravalib.client.Client.get_club_activities`. stravalib-2.2/docs/get-started/athletes.md000066400000000000000000000043101475174155400206650ustar00rootroot00000000000000(athletes)= # Athletes This page is designed to mirror the documentation structure at [Strava API Athletes](https://developers.strava.com/docs/reference/#api-Athletes) and describe the methods for working with athlete data in the Strava API. ## Retrieve Current Athlete This is the simplest request. It is provided by the {py:fun}`stravalib.client.Client.get_athlete` when called with no parameters. ```python athlete = client.get_athlete() print("Hello, {}".format(athlete.firstname)) ``` See the {py:class}`stravalib.model.Athlete` class for details on what is returned. For this method, a full detailed-level attribute set is returned. ## Retrieve Another Athlete A variation on the above request, this is provided by the `stravalib.client.Client.get_athlete` when called with an athlete ID. ```python athlete = client.get_athlete(227615) print("Hello, {}".format(athlete.firstname)) ``` See the {py:class}`stravalib.model.Athlete` class for details. Only a summary-level subset of attributes is returned when fetching information about another athlete. ## Update Current Athlete (This is not yet implemented by stravalib.) This page is designed to mirror the structure of the documentation at https://developers.strava.com/docs/reference/#api-Athletes and describe the methods for working with athlete data in the Strava API. ## Retrieve current athlete This is the simplest request. It is provided by the {py:fun}`stravalib.client.Client.get_athlete` when called with no parameters. ```python athlete = client.get_athlete() print("Hello, {}".format(athlete.firstname)) ``` See the {py:class}`stravalib.model.Athlete` class for details on what is returned. For this method, a full detailed-level attribute set is returned. ## Retrieve Another Athlete A variation on the above request, this is provided by the {py:fun}`stravalib.client.Client.get_athlete` when called with an athlete ID. ```python athlete = client.get_athlete(227615) print("Hello, {}".format(athlete.firstname)) ``` See the {py:class}`stravalib.model.Athlete` class for details. only summary-level subset of attributes is returned when fetching information about another athlete. ## Update Current Athlete (This is not yet implemented by stravalib.) stravalib-2.2/docs/get-started/authenticate-with-strava.md000066400000000000000000000125221475174155400240050ustar00rootroot00000000000000(authenticate)= # Authenticate with the Strava API using stravalib To retrieve data from the Strava API, you must first authenticate with Strava. This page provides you with the information required to set up authentication with Strava. ## Step 1: Create an application in your Strava account First, create a new application in your Strava account. To do this: 1. Login to your Strava account 2. Go to settings --> My API Application 3. Create a new application in your account :::{figure} ../images/strava-api-create-application.png --- alt: "Screenshot of the Strava API create application page" name: strava-api-create-application --- The above Strava documentation page image shows the Strava API create application page. The website value in this image is "localhost." This value can be any value if you authenticate to gain local access to your Strava data. But if you plan to build a web application, you should place the URL of your application in that field. ::: :::{admonition} More resources on setting up a Strava app :class: tip * If you want a more technical overview, see the [official Strava documentation](https://developers.strava.com/docs/getting-started/#account) * [A helpful tutorial about setting up a Strava app](https://medium.com/analytics-vidhya/accessing-user-data-via-the-strava-api-using-stravalib-d5bee7fdde17) ::: ## Requesting Authorization You are ready to authenticate once you have set up your Strava API app. The {py:class}`stravalib.client.Client` class contains the {py:func}`stravalib.client.Client.authorization_url` method that builds an authorization URL. This URL can be clicked on by a user or used locally to grant your application access to Strava account data. :::{figure} ../images/strava_api_values.png --- alt: "Screenshot of the Strava API create application page" name: strava-api-create-application --- An image from the Strava documentation page shows what your API page will look like after you have created your app above. For the step below, you will need the Client ID value provided in your app. ::: ```python from stravalib import Client client = Client() url = client.authorization_url( client_id=REPLACE_WITH_YOUR_CLIENT_ID, redirect_uri="http://myapp.example.com/authorization", ) ``` Note that you can use localhost or 127.0.0.1 as the redirect host for local development. ```python url = client.authorization_url( client_id=REPLACE_WITH_YOUR_CLIENT_ID, redirect_uri="http://127.0.0.1:5000/authorization", ) print(url) # Output: # 'https://www.strava.com/oauth/authorize?client_id=YOURCLIENTIDHERE&redirect_uri=http%3A%2F%2F127.0.0.1%3A5000%2Fauthorization&approval_prompt=auto&scope=read%2Cactivity%3Aread&response_type=code' ``` Once you have the URL value, you can display it in your web application to allow athletes to authorize your application to read their data. If you are trying to authenticate locally, paste the URL into your browser to support exchanging the temporary code Strava provides for a temporary access token. If you are using `localhost,` the URL looks something like this: ``` http://127.0.0.1:5000/authorization?state=&code=234234235874c642aaaca70a702f4494965b3bf003&scope=read,activity:read ``` While the above URL, a local development URL, may look like a broken link in your browser, it is correct. Notice the `code=longstringhere` in the URL. Next, you will exchange that code value for a token. The token is what you will use to access your (or a user's if they authenticate using your app) Strava data. To exchange the code for a token: 1. Return to your Strava app on the Strava website and copy the **Client Secret** value. 2. Use the **Client Secret** value with the `client_id` value in `client.exchange_code_for_token()`. ```python token_response = client.exchange_code_for_token( client_id=MY_STRAVA_CLIENT_ID, client_secret=MY_STRAVA_CLIENT_SECRET, code=code ) # The token response above contains both an access_token and a refresh token. access_token = token_response["access_token"] refresh_token = token_response["refresh_token"] # You'll need this in 6 hours ``` The resulting `access_token` is valid until the specified expiration time; for Strava, this time is 6 hours, specified as unix epoch seconds. You can see the expiration time by looking at the `expires_at` field of the returned token. Alternatively, you or a user can explicitly revoke application access. You can store this token value to access the account data in the future without requiring re-authorization. However, you must refresh the token after the 6-hour expiration period. Once you have an access token, you can begin to interact with the Strava API to access user data for the authenticated account. ```python from stravalib import Client client = Client(access_token=STORED_ACCESS_TOKEN) client.get_athlete() # Get current athlete details ``` To refresh the token, you call the {py:func}`stravalib.client.Client.refresh_access_token` method. ```python from stravalib import Client client = Client() token_response = client.refresh_access_token( client_id=MY_STRAVA_CLIENT_ID, client_secret=MY_STRAVA_CLIENT_SECRET, refresh_token=last_refresh_token, ) new_access_token = token_response["access_token"] ``` :::{note} See the [strava-oauth directory](https://github.com/stravalib/stravalib/tree/main/examples/strava-oauth) for an example of a Flask application that fetches a Strava authentication token. ::: stravalib-2.2/docs/get-started/how-to-get-strava-data-python.md000066400000000000000000000375431475174155400246100ustar00rootroot00000000000000# How to get your data from the Strava API using Python and stravalib In this tutorial, you will learn how to set up your Strava API application using **Stravalib** and Python to access data from Strava's V3 REST API. After setting up authentication, you'll also learn how to refresh your Strava token after it expires. ## Steps to set up Strava API authentication Authentication involves 4 steps: 1. **Create a Developer App**: First, you will create a free developer application or "app" in your Strava account. You will use the credentials created by this app to connect to the Strava API. 2. **Set Up Authentication**: Next, you'll configure your authentication to your Strava data by setting up permissions, known as "scopes." Scopes determine what your app can accessβ€”-whether it's just reading data or making changes to the data online on behalf of you or a user. 3. **Log In and Authorize**: When you log in to Strava and approve the scope permissions created in Step 2, Strava will provide a code. This code is used to request an access token from Strava that you can then use to make data requests. 4. **Use and Refresh the Access Token**: The access token created above allows you to interact with your Strava data for up to 6 hours. After the - hour time period, you can refresh your token using {py:func}`stravalib.Client.refresh_token()` as often as needed to continue accessing the API. You only need to complete steps 1-3 once. Once you have a `refresh_token` value, you can continue to refresh your token whenever it expires, following Step 4 above. :::{note} This example workflow is a local workflow that doesn't rely on a web application tool like Flask. If you want to use Flask, we have a demo folder that provides a basic setup in the `examples/strava-oath` directory of the stravalib GitHub repository. ::: ```python # You will use this to log in to your Strava account import webbrowser import json from stravalib.client import Client # Open the secrets file and store the client ID and client secret as objects, separated by a comma # Read below to learn how to set up the app that provides you with the client ID # and the client secret client_id, client_secret = open("client_secrets.txt").read().strip().split(",") # Create a client object client = Client() # Define your scope (this is read-only - see below for a "write" example which # allows you to update activities and publish new activities to your Strava account). # read_all allows read access for both private and public activities request_scope = ["read_all", "profile:read_all", "activity:read_all"] # Create a localhost URL for authorization (for local development) redirect_url = "http://127.0.0.1:5000/authorization" # Create authorization url; your app client_id required to authorize url = client.authorization_url( client_id=client_id, redirect_uri=redirect_url, scope=request_scope, ) # Open the URL in a web browser webbrowser.open(url) print( """You will see a url that looks like this. """, """http://127.0.0.1:5000/authorization?state=&code=12323423423423423423423550&scope=read,activity:read_all,profile:read_all,read_all")""", """Copy the values between code= and & in the url that you see in the browser. """, ) # Using input allows you to copy the code into your Python console # (or Jupyter Notebook) code = input("Please enter the code that you received: ") print( f"Great! Your code is {code}\n" "Next, I will exchange that code for a token.\n" "I only have to do this once." ) # Exchange the code returned from Strava for an access token token_response = client.exchange_code_for_token( client_id=client_id, client_secret=client_secret, code=code ) token_response # Example output of token_response # {'access_token': 'value-here-123123123', 'refresh_token': # '123123123', # 'expires_at': 1673665980} # Get current athlete details athlete = client.get_athlete() # Print athlete name :) If this works, your connection is successful! print(f"Hi, {athlete.firstname} Welcome to stravalib!") # You are now successfully authenticated! ``` Below you will get a detailed overview of the above steps. You will also learn how to refresh your access token after the 6-hour window has expired. ## Step 1: create a developer app in your Strava account To begin, you need to create a free developer application in your Strava account. [Click here and follow Strava's steps to create a developer application.](https://developers.strava.com/docs/getting-started/#account) The figures below are from the Strava setup link provided above. They show you: 1. The Strava application setup screen and 2. The Strava application, once it's been set up with the client ID and access token values. :::{figure-md} fig-target :class: myclass Image showing the settings for the application that you will create in your Strava account Figure from the Strava documentation showing the application settings. You can name your application whatever you wish. You can add any website URL if you are grabbing your data locally. For this tutorial, use `localhost` for the Authorization Callback Domain if you are only using this locally. Otherwise, you likely have a website URL that you will enter here. ::: :::{figure-md} fig-target :class: myclass Image showing the application and application client secret, access token and refresh token. Figure from the Strava documentation showing the application settings. This screenshot shows the final application you should have once you follow the steps above. Notice that you can see the access token scope and expiration time and date on the app page. These are values that you will work with below. Copy the **Client ID** and **Client secret** to a file (see below for more). ::: :::{important} Remember to store your client ID and client secret values somewhere safe. Do not EVER commit secrets or token values to `.git` or push it to GitHub (unless you have encrypted it)! ::: ### Save your secret and access token values in a text file Notice that there is a client secret (hidden in the screenshot) and a token in the first screenshot above. Next, you will copy both the secret and the token to use (and reuse) in your code. Do the following: * Create a `client_secrets.txt` file. * Add the client secret and access token values on a single line separated by a comma in the format that you see below: `secret_value_here, access_token_value_here` :::{note} The format described above is not required for a Strava workflow. This is just an example of how to store relevant information that you will need to reuse if you intent to update the data in your workflow regularly. If you are creating a web application, you should store this information securely somewhere in your application's database or web infrastructure. ::: ## Step 2: Setup Strava authentication Once you have created your `client_secrets.txt` file, you are ready to setup authentication using **Python**. At the top of your code, import the {py:class}`stravalib.client.Client` class. In this tutorial, you will also import `webbrowser` to launch a web browser from your code to login to Strava. `webbrowser` behaves similarly to a web application. ```python # Use webbrowser to launch a browser from Python import webbrowser import json from stravalib.client import Client ``` Next, read the `client_id` and `access_token` from the `client_secrets.txt` file that you created earlier. Then, create a **stravalib** `Client()` object. Below, the `client_id` and `access_token` are read from the client_secrets.txt file. The `.strip()` method removes any extra spaces, and `.split(",")` separates the two values. You then create a `Client()` object from stravalib, which will be used to interact with the Strava API and manage your data. ```python # Read the client_id and access_token from the secrets file client_id, access_token = open("client_secrets.txt").read().strip().split(",") # Create a stravalib Client() object client = Client() ``` The `Client()` object is what you'll use to interact with Strava. It stores your authentication details and provides methods to: * Retrieve different types of data from Strava, such as activities and club data. * Modify activity and club data on Strava (if you have a token with write permissions). You will learn more about read vs. write token scopes below. ## Step 3: Login and authorize Strava to interact with your code Now that your developer app is created, it's time to authenticate with Strava. 1. You have the client secret and token from your Strava account's developer app, and 2. You have the `Client()` object from **stravalib**. ### Create a URL to Authenticate and Define the Token Scope Next, you'll define the scope of your authentication (what permissions you need) and set up a redirect URL. 1. **Define the Scope**: The scope determines what permissions your app will have when interacting with Strava. If you only need to **read** your data, a read-only scope is enough. However, if you want to **write** data (e.g., upload or create activities), you'll need to request write permissions. 2. **Set a Redirect URL**: After you log in to Strava and approve the scope permissions, Strava will redirect you to a specific URL. This is the `redirect_url` you need to define. If you're just authenticating on your local machine to access your own data, you can use something simple like `localhost` for the `redirect_url`. ```{tip} If you are building an app, then your redirect URL might be the URL of your app. ``` ### Define the Scope for Strava API access When working with Strava's API, you must define your application's permissions, or **scope**. The scope determines what your app can access or modify. If you only want to **download** your data, you can use **read-only** permissions. #### Example scope values for read-only access The example below shows a Python list that defines read-only access. This scope allows your application to access a user's profile and activity data without modifying anything: ```python # Read-only scope values request_scope = ["read_all", "profile:read_all", "activity:read_all"] ``` #### Example scope values for read and write access You must include a "write" scope if you need to modify and upload data to Strava (e.g., creating or modifying activities). Here’s an example of a scope that includes both read and write permissions: ```python # Read and write scope values request_scope = ["read_all", "profile:read_all", "activity:write", "activity:read_all"] ``` The `activity:write` scope allows your app to upload or modify activity data to Strava. :::{tip} [Learn more about request scope options from the Strava documentation here.](https://developers.strava.com/docs/authentication/#details-about-requesting-access) ::: In this tutorial, you will limit your scope to **read-only** as you are only looking at data in this tutorial rather than modifying it. You also set the `redirect_url` to **localhost** (to be opened on your computer locally) URL: ```python # Create a localhost URL redirect_url = "http://127.0.0.1:5000/authorization" # Define a read-only scope request_scope = ["read_all", "profile:read_all", "activity:read_all"] # Create an authorization URL using stravalib url = client.authorization_url( client_id=client_id, redirect_uri=redirect_url, scope=request_scope, ) ``` Your URL will look something like this: ```python print(url) # 'https://www.strava.com/oauth/authorize?client_id=123456&redirect_uri=http%3A%2F%2F127.0.0.1%3A5000%2Fauthorization&approval_prompt=auto&response_type=code&scope=read_all%2Cprofile%3Aread_all%2Cactivity%3Aread_all' ``` ### Authenticate with Strava using Python Now that you've created your Strava authentication URL, you're ready to use it to get permission to access data from the Strava API. In this section, you'll use Python's `webbrowser.open` method to: 1. Open the URL directly from your Python code in a web browser. 2. Authenticate with Strava (login and allow your app to access the developer app you created earlier). 3. Strava will redirect you to a "page not found" screen with a long URL after authentication. This might look like an error, but it's correct! The URL contains the code that you need to complete authentication. The URL will look something like this: ```text http://127.0.0.1:5000/authorization?state=&code=xxxxxxxxxxx&scope=read ``` 4. Copy the value between **code** and the **&** symbol. The `xxxxxxxxxxx` in the example represents the code you'll need to authenticate with the API. Now you're ready to use this code to authenticate! ```python # Open the url that you created above in a web browser webbrowser.open(url) print( """You will see a URL that looks like this: http://127.0.0.1:5000/authorization?state=&code=12323423423423423423423550&scope=read,activity:read_all,profile:read_all,read_all Copy the values between code= and & in the URL that you see in the browser.""" ) # Using input allows you to copy the code into your Python console (or Jupyter Notebook) code = input("Please enter the code that you received: ") print( f"Great! Your code is {code}\n" "Next, I will exchange that code for a token.\n" "I only have to do this once." ) ``` You only need to get a code from Strava once. Next, you exchange the code for an access token. After this, you can refresh the token as often as needed to continue accessing your data. In the example below, you'll exchange the code for a token using {py:func}`stravalib.client.Client.exchange_code_for_token()`. The token response contains both an access token (valid for 6 hours) and a refresh token, which you will use to get a new access token once the current one expires. You can save the token response as a `.json` file so that you can reuse the refresh token later without going through the authentication process again. ```python token_response = client.exchange_code_for_token( client_id=client_id, client_secret=client_secret, code=code ) # Save the token response as a JSON file with open(json_path, "w") as f: json.dump(token_response, f) print("Token saved - hooray!") # Access and refresh tokens access_token = token_response["access_token"] refresh_token = token_response["refresh_token"] # Use this after 6 hours ``` ## Step 4: Use and refresh your Strava API token Strava provides you with an access token that is good for 6 hours. You can refresh the token as needed if you wish to grab more data after that 6-hour window has ended. You can refresh the token using the {py:func}`stravalib.client.Client.refresh_access_token()` method. Earlier, you saved the `refresh_token` and `expires_at` values to a JSON file. You can use the refresh token stored in the JSON file to refresh your token for another 6 hours of use as many times as needed. This might be most useful if you try to process your data again--for example, the next day and you don't have the refresh token value stored and accessible to Python. ```python # Open the token JSON file that you saved earlier with open(json_path, "r") as f: token_response_refresh = json.load(f) print(token_response_refresh) # Output: # {'access_token': 'ab0667a99d17b7c278d9f730f733ad09016306cf', # 'refresh_token': '9f8d5689c93e83c7b0c69a8585010d4762e8b2ac', # 'expires_at': 1726560054} ``` ```python refresh_response = client.refresh_access_token( client_id=client_id, # Stored in the secrets.txt file above client_secret=client_secret, refresh_token=refresh_token, # Stored in your JSON file ) # Check that the refresh worked client.get_athlete() # View the newly refreshed token print(client.access_token) ``` ## Wrap up You now know how to: 1. Setup an app within your Strava account 2. Connect that app to your Python code using stravalib 3. Request and refresh a token that allows you to request and update data in your Strava account stravalib-2.2/docs/get-started/index.md000066400000000000000000000033471475174155400201740ustar00rootroot00000000000000# Get Started Using Stravalib ```{toctree} :hidden: :caption: Get Started Install Stravalib Overview Authentication Activities Athletes ``` ```{toctree} :hidden: :caption: Tutorials Authenticate with Strava ``` ## Install stravalib (install)= The package is available on PyPI to be installed using `pip`. ```bash $ pip install stravalib ``` ## Using Stravalib In order to make use of this library, you will need to have access keys for one or more Strava users. [This is a nice tutorial that has information about setting up a free app within Strava](https://medium.com/analytics-vidhya/accessing-user-data-via-the-strava-api-using-stravalib-d5bee7fdde17). These access keys can be fetched by using helper methods provided by the `Client` class. See `auth` for more details. ## Stravalib get started tutorials ::::{grid} 1 1 1 2 :class-container: text-center :gutter: 3 :::{grid-item-card} :link: authenticate-with-strava :link-type: doc ✨ **Authenticate with Strava** ✨ ^^^ To begin using stravalib you will need to first authenticate with a Strava application connected to your account or one that you have access to. Learn how to do that here. ::: :::{grid-item-card} :link: activities :link-type: doc ✨ **Work with Strava activity data** ✨ ^^^ Once you have authenticated, you can begin to access your data on Strava. Here ou will learn how to work with activity data. ::: :::{grid-item-card} :link: athletes :link-type: doc ✨ **Work with Strava athlete / social data** ✨ ^^^ The API also gives you access to your athlete account information including friends, followers and more. Learn how to work with that data here. ::: :::: stravalib-2.2/docs/get-started/overview.rst000066400000000000000000000121061475174155400211340ustar00rootroot00000000000000.. _overview: Usage Overview ************** The :class:`stravalib.client.Client` class exposes methods that loosely correspond with the REST methods exposed by the Strava API. Retrieving Single Entities ========================== The simplest case are the client methods that return single entities. The entity object types are instances of :mod:`stravalib.model` classes. For example:: client = Client(access_token=JOHNS_ACCESS_TOKEN) athlete = client.get_athlete() # Get John's full athlete record print("Hello, {}. I know your email is {}".format(athlete.firstname, athlete.email)) # "Hello, John. I know your email is john@example.com" Retrieving Entity Result Sets ============================= A number of Strava API endpoints return paged results. The stravalib library abstracts over the paging to provide an iterator that will iterate over the entire resultset, fetching 200-page result sets under the hood. This capability is provided by the :class:`stravalib.client.BatchedResultsIterator` class. If you only wish to fetch a few objects, you can specify a limit in the method call or set the limit on the resulting iterator.:: activities = client.get_activities(limit=10) assert len(list(activities)) == 10 # or: activities = client.get_activities() activities.limit = 10 assert len(list(activities)) == 10 Note that setting the limit on the iterator is the only option when you are using the collection attributes on entities.:: activity = client.get_activity(activity_id) comments = activity.comments comments.limit = 1 assert len(list(comments)) == 1 Attribute Types and Units ========================= Many of the attributes in the Strava API are either temporal (or interval) types or quantities that have implicit units associated with them. In both cases, richer python types than the simple string or numeric values that the Strava REST API returns can be accessed as follows: Date/Time Types --------------- The date+time responses are encoded as python native :class:`datetime.datetime` objects.:: a = client.get_activity(96089609) print(a.start_date) # 2013-11-17 16:00:00+00:00 Date values which have no time component are encoded as python native :class:`datetime.date` objects.:: me = client.get_athlete() print(me.dateofbirth) # 2010-12-26 Interval/duration values are given in seconds by default. You can use the ``timedelta()`` accessor to get :class:`datetime.timedelta` objects, which allows them to be added to datetime objects, etc.:: a = client.get_activity(96089609) print(a.elapsed_time) # 38700 print(a.elapsed_time.timedelta()) # 10:45:00 Quantities and Units -------------------- Typically the units for quantity attributes returned by the Strava REST API are not what people would actually want to see (e.g. meters-per-second instead of kilometers-per-hour or miles-per-hour). To facilitate working with these quantities, stravalib makes use of the `pint library `_. You can use the ``quantity()`` accessor to turn the "plain" ``int`` or ``float`` values into a :class:`pint.Quantity` object.:: activity = client.get_activity(96089609) print(activity.distance.quantity()) # 22530.80 meter Hmmm, meters. Well, here in the US we like to see miles. While you can certainly do this using the Pint package, stravalib provides a preconfigured set of common units to simplify matters.:: from stravalib import unit_helper activity = client.get_activity(96089609) print(unit_helper.miles(activity.distance)) # 14.00 mi Of course, if you want to do something besides display those values, you'll likely want a number. You can directly access the 'magnitude' attribute of the :class:`pint.Quantity` instance.:: activity = client.get_activity(96089609) print(unit_helper.miles(activity.distance).magnitude) # 13.9999900581 Rate Limits =========== Strava imposes rate limits on the usage of its API. This means that the number of requests sent to Strava have an upper limit per 15 minutes and per day. These limits are not fixed but depend on the "size" of the client app. Strava _may_ choose to adjust rate limits for apps as they grow. [Learn more about rate limits here.](https://developers.strava.com/docs/rate-limits/) You can see the limits set for your app at [your account's settings.](https://www.strava.com/settings/api) When initializing a `stravalib.Client` instance, the default rate limiter allows requests until the short - or daily limits are reached. Once limits are reached. the client object will wait until the end of the 15-minute or day period. In case you want to configure the limiter to throttle requests (i.e., making sure the time between requests for the remaining period is evenly spread), you can initialize the client object as:: from stravalib.util.limiter import DefaultRateLimiter client = stravalib.Client( my_access_token, rate_limiter=DefaultRateLimiter(priority='medium') ) The ``low`` priority complies with the daily limit. The ``medium`` priority ensures that requests are throttled to comply with the 15-minute limit. stravalib-2.2/docs/images/000077500000000000000000000000001475174155400155565ustar00rootroot00000000000000stravalib-2.2/docs/images/strava-api-create-application.png000066400000000000000000003170121475174155400241010ustar00rootroot00000000000000‰PNG  IHDR_"φ­ ΑiCCPICC ProfileH‰•—P“Ω€ο§7ZB€„ή‘N)‘ήE%$„CB@°‘"ΰZPAEΡUΧΘZQl‹bTtAe],ΨPy?π»οΝ{oή™9sΏ99χ”;χfΞΝ‹3`%2EΩ’/F\|70@P€ sΈR1+,, 2½ώ]>thb½k9λί―’ΜγKΉ@a'σ€άL„O!ϊ’+–d€Ϊ‹Ψυs³Εά†0M‚ˆpΟ§Nρπ'O2LϊDEx#LOζp$©ˆ‘ΓMEβ=Άρ„"„Ε»sΒΗΆΘΜ\<Α½›$%Nκίb&Λcr8©ržκeRπ>B©8ƒ“χΗ–Μ Ωt#DΙI@²ͺ gΦ“Ύ8HΞ’δΠiς&ύ'Y ˆžfΤ;ašyŸ ωތΰiNϊ±εq²ΩQΣΜ—ϊFN³dq„¨ϊ:]„AΠ¨‚ήB_`L†i°lρ™0 ‚£ΰp*œηΓ…πFΈ†Β πEψ|ξƒ_Α£(€"‘θ(]”%ЉςF…’P)( jͺU†ͺFΥ‘šQν¨»¨>Τ0κ3‹¦’hK΄+:ζ’³Π+ΠΠθΓθtϊ.Ί=‚ώŽ‘`41ζ ‡IΕδbŠ0e˜ƒ˜Σ˜Λ˜ϋ˜AΜ,KΗc°Ψxlv)vv7ΆΫ‚νΔ`Gq8œ:Ξη† ΕqpΩΈ"άNάQάάά ξž„ΧΑΫβύπ x~5Ύ ?FP"\‘!°‰p€ΠLΈE$Œ•‰ΖD7b1ΈŠXN¬#^&φί‘H$=’3)œ$$ΚIΗIWIύ€Οd²Ω›œH–‘7’‘[ΘΘο(ŠΕ“’@Ι¦l€ΤP.QžP>)P¬Ψ <…• • w^+ YŠ σΛO*ήRV"()y+q”V(U*QκVU¦*Ϋ(‡*g*oP>’|Mω… NΕHΕW…§R¨²_ε’ΚEΥ§zSΉΤ5ΤΤΛΤA–fLcΣh%΄c΄ڈͺŠͺ½jŒκΥJΥsͺ}t݈ΦgΠ7ΡOΠ»θ_fiΝbΝβΟZ?«n֝YΥf«yͺρՊΥκΥξ«}Qg¨ϋͺ§«oQoT¬Φ0ΣΧΘΥΨ£qYcx6mΆλlξμβΩ'f?Τ„5Ν4#4—jξΧΌ©9ͺ₯­ε―%ΦΪ©uIkX›ν©¦½MϋΌφUΗ]G¨³Mη‚ΞK†*ƒΕΘ`”3Ϊ#ΊšΊΊ2έ}ΊΊczΖzΡz«υκυλυ™ϊ)ϊΫτ[υG t ζ,3¨5xhH0d wΆ~426Š5ZgΤhτΒX͘mœo\kάkB1ρ0Ι2©6ΉgŠ5eš¦›ξ6½m›9˜ Μ*Νn™ΓζŽζBσέζ g ‘E΅E·%Ω’e™cYkΩoE· ΆZmΥhυzŽΑœ„9[ζ΄Οωnν`a}ΐϊ‘ŠM Νj›f›·ΆfΆ\ΫJΫ{v;?»•vMvoμΝνωφ{μ{¨σΦ9΄:|str”8Φ998%9νrκf˜aΜ Μ«Ξg/η•Ξg?»8Ίd»œpωΣΥ5έυˆλ‹ΉΖsωsΜpΣsγΈνsλsgΈ'ΉοuοσΠυΰxT{<υΤχδyτ|Ξ2e₯±Ž²^{Y{IΌN{}τvρ^ξέβƒςρχ)φιπUρφ­π}β§η—κWλ7βοΰΏΤΏ%°% ›­Εζ²kΨ#NΛΫ‚ΘA‘AAOƒΝ‚%ΑΝσΰyσΆΞλ 1 …4†‚PvθΦΠΗaΖaYaΏ„cΓΓΒ+ßEΨD,‹h€F.Š<ω!Κ+jSΤ£h“hYtkŒbLbLMΜΗXŸΨΨΎΈ9qΛγnΔkΔ γ›p 1 FηϋΞί>0Ρ!±(±kρ‚% -ΤX˜±πά"ΕEœE'“0I±IG’ΎrB9՜Ρdvςδ7wχΟ“·7Δwγ—ςŸ§Έ₯”¦ΌHuKݚ:$π” †…ήΒ α›΄€΄ͺ΄ι‘ι‡Η3b3κ3ρ™I™gD*’tQΫbνΕKwŠΝΕEβΎ,—¬νY#’ ΙA)$] mΚ¦!ƒΡM™‰l­¬?Η=§2ηSnLξΙ%ΚKDKnζ™ε­Ο{žο—ΣRτRξΦeΊΛV-λ_ΞZΎo΄"yEλJύ•…+ ό ―"J_υλjλΥ₯«ί―‰]Σ\¨UXP8°Φmm‘B‘€¨{λΊͺΠ?θXo·~ηϊοΕΌβλ%Φ%e%_7p7\ΡζΗςΗ7¦lμΨδΈiΟfμfΡζ-[—*—ζ—l·΅ac[ρΆχΫmΏVf_V΅ƒΈCΆ£―<ΈΌi§ΑΞΝ;ΏV*ξWzUΦοά΅~ΧΗέΌέwφxξ©«ͺ*©ϊ²WΈ·gŸΎ†j£κ²ύΨύ9ϋŸˆ9Πώσ§šƒK~;$:Τw8βp[SMΝΝ#›jαZYνΠΡΔ£·ωkͺ³¬ΫWO―/9ŽΛŽΏό9ιηA'ZO2O֝2<΅λ4υtqԐΧ0(hμkŠoκ<x¦΅Ω΅ωτ/VΏ:«{ΆςœκΉMη‰η Ϗ_ΘΏ0Ϊ"nΎ˜zq uQλ£Kq—ξ΅…·u\Ί|υŠί•Kν¬φ Wݞ½ζrνΜuζυΖŽ7n:ά<ύ«Γ―§;;n9έjΊν|»Ήsnηω;w.ήυΉ{εϋލϋ!χ;»’»zΊ»ϋzx=/dΰΌϊ]ϊϋΧΑΒg”geΟužΧΌ°}qvΘoθφΛω/_‰_ ύ‘όΗΧ&―OύιωηΝ‘Έ‘Α7’7γo7ΌSwθ½ύϋΦΡ°Ρ'2?Œ},ώ€ώιπgζηφ/±_žε~Ε}-fϊ­ω{ΠχήρΜρq1GΒ™PˆΒ))Ό=„Μ ρPo@œ?5OO 4υ 0Iΰ?ρΤΜ=)ŽΤ!KX ώˆ-ΐxbœExb$Šς°\);Ϋ©Xdd²Δ|§€o’ρρ±έγγί Ε"σMKΦΤ?!ΪΘ7E.ΰή·w5}/"αX=­½Ά pHYs%%IR$πiTXtXML:com.adobe.xmp 1092 1214 1 M0β@IDATxμ½`Εφ?~ ©$@ ”„ή«tjATŠ‚€Š»(φŠυΩ}ΔbŸ^DT:Jο½$€χžϋ|ΞήΉ,Χ€ΐσ‹ΟorwwΚ™3gfw>{Ξ™Y‚8Α‘€#GŽ 8p$ΰHΰŒHΐχŒΤβTβHΐ‘€#GŽ 8p$ pΐ—3 8p$ΰHΐ‘€#GgPψ:ƒΒvͺr$ΰHΐ‘€#GŽ 8ΰΛŽ 8p$ΰHΐ‘€#3(|Aa;U9p$ΰHΐ‘€#GŽπεŒGŽ 8p$ΰHΐ‘ΐ”€ΎΞ °ͺ 8p$ΰHΐ‘€#GψrΖ€#GŽ 8p$ΰHΰ Jΐ_gPΨNUŽ 8p$ΰHΐ‘€#GŽNWό8|||τοtιόΥε _€ϋΏΖΫ_έV‡ž#GŽ όσ$ΰƒ‰ΚωΌΠ?―ίώvŽ»λoηΓ›νρUΕΩӝsGŽ 8p$p&%π_iΎ8©™‰†γΝ‡Dζ8QΎγ•?•x‹'ΦσίΥeΪfκφυ=3ΪΚΚJ•λ?Suš6οhϊνα‰ΐkΫΆν²iσ&iΦ΄™4iΨ3ŽG篌§œL0Ϊ-ςΙσΒΒBωυΧeΚOΧn]₯ZhθεΝπε 8p$ΰHΐ‘@U8eΝ—™ΰφο? oN™"AART\$Χ]w­4LHπLrœ ΦύMή`ͺT―I°šάzλΝT/§gxΪ»wŸLyλ- 4π‘o/5jΔzx:Ω ½“ΝWεϋςΛΘ―Λ–ITd”ddfΘ9ƒΞ‘ϋŸ2?€CY|ψαtΩΊm›ΛΰσΟ“Ύ}Ο–Γ‡KΟ^d玍_/YV,["uκΤώΫx%Ÿ _Σ§$cǎΡλ¦N“+.ϋ·ρ₯L8?Ž 8p$ΰHΐ&SΦ|`’›—+O=ω„ΔΔΦ•Œτ2bΔπcΐ—©#==]^ύχΛΈ N;ΙM7έ¨I†ŽΙχί ­άάyϊ©'A*…rε•—γ{J€ ­}ϋφΛφΫ%ΐί_BΫ·oηΡϊœΑ?ΙlκΛΝΝ•=ϋ‚¬Xώ3D$POŽμέ³Ozχξω—ƒΥ?aι˜d‚θ•«VΛk―Ύ’ρ­ZΆΠcvvŽ―ŽΊΘͺ•Λ$;;ϋŒ―²²2π΄JΚΛΚ₯Ό’\Z4o.΅jΥR€Eζvνή­<ςgηΞ]zώ­qυTθœ8p$ΰHΐ‘€#?‘ΐ)ƒ/΅η¨Ÿ―ŸDΖΤΕΔΧH–,9 ώ~~UVεπΒpV—Ξ‡ X'½'C‚{Ό&š`ϋρΞkΚψ‚'Ώΐ©C)..ωX2Ϊ8)=΅Η›:W―^-C‡^¬ι_–¦M›jΌ1•zΣ=^}φxžW%Koh–””Hχn½]Ξ$YΊτg_”‘ΖαεcΗJΚα©Δ˜§ό¨,Hƒ†O%€Ζ1xΧ―‘ξŸͺςTg/γœ;p$ΰHΐ‘€#o œ:ψrS ΓuNF¦P Αΰž»ά©φƒ5©•P”—[“₯•ίš­œ>˜ α€…ΐ<œΠ>ΜDΘλͺΞ+**„œpωΗPQšž|žLyNΈŒ+//󀙀€ΟDlς‘M— ΤμΤ­z¬‰]#ρcΟKpβχρρEΉ€*y5式όόσ/šTΠxθπ©_K―W\₯ΰΛ΄ί”gύόc`šI7ύΑv1ΞΞ'σVV²ŒUŽu3ΫVQ 9βΪΘΡ€=ζδy~A!‹{ˎ5R.Ύψb²cΝΘφ:ΚΖG O$bΟc?g³>6ΝΟΟWǁw~¦_xᒁρ·tιβηoυ½‘ABByεj[]Z'ΛΫeΑs0cm6/ v^XŽωL0}exδυρΚ™2ΞΡ‘€#GŽ xKΰ΄Α—7‘“½6“Ωš5kδ½χ¦JDdΈ$''Λ%0[Ν;OfΟ™+Τl$4h ½zυ”~ύϊz&NΦΑ‰“&οΎϋ^V―^#‡SRTκΘ嗏ΑdΝζ„(˜°σc&ΝU0Ÿ}Γ²kΧn),(8b'%&Κ V³"i9rΎloΙΦ-[₯abυsϊύχuςΘ#Jll¬ϊ¬q~Oδƒš—Π‹ˆˆPΎ θ/έΰθmŸπνό˜s3ΡgffΚΜ/Ώ–κ0α֍―­ΪšgŸ{I&5‘oΎω&έR­Ϊ±NγŸ}φόιΦJaQ‘\zΙpiΪ¬™|ϊΙ§²`~x%Ι Aƒ€M›Φ ³wο^ycς5₯RƒuγMγeσ¦ΝςŌ™’ž–.q΅γ€s§rξΉηIxx<ηŽ ¦<(―½>Y5aΕΕΕ0'ορ V(οέ»χΘχί/Λ–­€|„…‡K½zυδΌsΟ9F6€N9₯§gȜ9sδηŸ•Μ¬L­4:*Jzφμ)ηœ3>ƒΥ5nφμ9ςυ7ίJψΝΛ/„†eΪΤε“O>ƒoΧτc{™1γKΘa…ϊύu†©{Ψ°‘ .ωι'™3{μά΅K|QwνΪ΅₯yσf2xπ`©[ΧΦ†={d2δE€)γoΈόύ"3f~©c0 όέ§·\tΡ…θŸjž:”QηΗ‘€#GŽ O§* :aΨ΄y3t‘]{P9βϊν·ί5š¨cŽ .Ττvν;ΉΞ?ˆ+//_ΣηΝ«ρ,;δΒ‘;οœθΉΧ ηAzύΚ+―Ί iΠ2όIIIq v‰¦ΕΥIpΕΤ¨η)wο½χ»Ίuοεj¬•«IΣV.€ -͎λwήsη«ζΙΟΊ‘_Ρk˜Ξ4/|„τΪ/°Ί«]ϋΞ–­ΪΉš5o­qMšΆtiΎ-[ΆΊjΥn¨ρΥ"jιΡ’§j%ΧΏύš‡o-γύcd΅dΙOZžνΉσ‰#iiΎύΊZ·ι ρkΦΥ’&?ιέƒž:'NΌΗ5jΤΟ5‰ηό‡fyͺ]·n'ΎcΗ.I“žπ\Χ¬Uίπ§ΧγΖ]γ‚―ž–+ƒμοΎη>΄΅¦Αω^γ­ώg[­~ΪΌe‹§žεΛ—{θΦ«ΟΎ΄dβγ₯η/Ύψ²Λ΄……ΰ£εκΤΉ›¦Ηxς~Ξξ;ΠuπΠ!₯άsΟkz§Ξ]΅oΪ΄νθ²Ζ‹ΈfΟ™£y}t’‡Ζ“O=ν2gί=ςΘcšV=6ή“‡}MΓ£γ]Jƒ?λΦ­χδιΡ³λρǟτ\7Ljꊯ›¨ΧοΎΗS±§œsβHΐ‘€#GŽN$3³f6 hΠΰηφλsv9pΰ ΌπΒsΠr ”+ΌJςr‹€UλrvίώrΫm·@{²ά^ϋrζηΐ9RPX"Ι‰υ΅Lύδ™gž‚–₯H5;4[Y&6‘•+WΚ5Χ\₯&D€-ΉφΪλU»ΐΝI’τθΡ[FΎL³£££‘ΕΉUŸΧGς‘Yπ$:: «9o€vn˜š±¨ιyš°Τ΄l0“6­šΚλoLLϊβ\C0Q«†lήόωΎ«:1Ή—, I9΄ΎLέ€4lgχι%λ qcXφ«Υ~jˆL…ΦNΔ_Ίƒχ3Ώ†ζg:4d—Κ%—Œ’=»·IΧn=₯MΫrήyηΚώύϋ΅Ν’"‘ΐLu“<όπƒΰΏ\}υ΅Wmσ—λϋοΏνΡL«ŒώcθσW·^#ΤΣYκBφΤ 1Psxυ5γ%ΉQ3•·΄`χέ€\™…Ι€ηΘ„ ·CΛ5WσXΛΣOKVψΪ»V’Y’`i\PP τιΣO-œ+οΎσžζ§–tΨπKΤ<Κ* `νΣ»‡\rι(‘¦ŒZBρ߀ꖏΒΧ_#=φˆΆ13= cβzωΧΏž“횩¬κԊΖXθ.\lΑ`5)BεΕ>θ‘ΰ^.=šΊ‰„ΦΆoΏςά³R-$ΛΝ ΟΰHΐ‘€#GŽͺ”ΐ‰YUi§ͺωZΰ₯ωΚΟ·4_ ,ΰŒθκsv?\&άεΒΔ­UΒΟΙU«N’ ώVΠπZoΌYγwCC"ΎΥU»σ’λ–[ow₯¦¦j5oΤ΅jέΗφF[Ίφμ±4_Ψz@5a½{χuMyλm—ΡDΑ|ι:πW‡Žg)/ση/π4ω›oΎΡΈδFΝ]7άp“jΌ`²t˜G5Ϊ—!^μᛉ&3¦aΟ)-cκΤ ό˜λ””Tε»}‡ΞJsύϊ šΕΠ!oηΚΙΙ1E΅¬ΡZQΣG9N™ς–‡Η/Ύ˜‘q½ϋτΥγΤ©Σ΄μϊυ–&‡|S3xvίΝ›-Υή}ϋ\£.㊫ytO![«?ξšxχ4_[·nEίΤVΩ…ΗΉΆΰšαέw- #5EΑa΅\kϋMγασηzυ΅Χ•ς{ΡΕΓ΄μΏA眯ZΡΑ\θΪ±c‡ζηΐ’ζ§vλ⋇{΄Ž0ΣB&ηΊΪΈ5ƒ0j9=ΎπΒ‹žzž…¦Œ&bWc΄Ή‹[SK ehλδΈ9šφ‚ΖoΨ°AιtιέEνζ­o‡άΈ_ύΥUš/#O?ύL˘~Υ ηΗ‘€#GŽ T!{Ν§―Ψ„Z&7άpφδͺ‘š¦Ž;Θ³Ο<¦[4Jͺ+λ6lΠψ#G ZΘt―¬,’ΏNjΦ¬©iτmzΰώ{dύΊ5’~DFS4|ψpψ6ύ.³g―{>Ρ?λ0VΓefe©Ώ΅' τ53Αςγ•΅2Ž~^ΖΉ“²f[Ήb΅jͺX š³κFΝlήΌMRRS ΙcŽθ ½&­uΏ―–œœ\ΉaόM½•CηΝU«ΕLsηό 0λi~SN/π“••- Ήš»Ρͺε!\₯IZΫΆa›Ώ(εω¦ς[·¬—‰wMΐ*Ε&ͺ­©,ξΑ–rxΆΦO«&^ϋSmNY™ε¬ΟΌ«ΰ‡3 ,ύi±<ϋτCΆMνΚoψ°aΣ°άΐCπ-λ€ςfŸχνWΠJ})Ÿ}ϊ1|κŸ+M5hΎξEτΣ‚ΙΠ³`C}ξlήh¦_Lϋ ³"+†τx`Ω†6sLDV― αlα¨mKJJ’{οΉ Ζ₯ͺΙδΦ †΅΄Ή©Β­KθΖΠ₯K}Ω%²qΣ½>tθ°Ν˜Σ ηΗ‘€#GŽ T!Ϊαώ(Ά:zf―Η{2rcO–,μ5tΨ©{N–\}Φ΄I½¦cψ–­»°²-ΓŽrσςΰ=DΈͺΝ^†&)ΛddρΔI%€―9ςϋΊureJjšlΪψ›$7ΖQ؈•Α*£§JΓ:#όr/ψ²Vcrο(š΄Ϊ΅iφ·εž{&š¬z„FΚ†Θφm% €±aB‚›§£ €;ω\ΈpΚΑqΎDWXΒ Χ.]4ƒ­& θθ}εy€U¨\§ŽζΖ!Ÿi€έ£{7ysςλ¨q ˜( Σ₯₯\‘ ‰‰ 5ήΠ#Ÿ;u…œK5>??ϝ‡γώT–W(_μ»}ϋφIu˜iχο1{‚ϊqq΅δ‰'&ydA>ɁΥάyσε'΄}Χέ’†½αΦoΪ)ω%Ώ;ΙΜΜΆγ)wϋ­Ύ%S•. `±ΣFofΉMΙύϋφ€™5N― o &θu(ϊς"O|ΡδMͺΗιŸνdZ±œ¬CZΞΠΡ ηΗ‘€#GŽ œ@§ Ύτ»~~Αžύ½J‘`πž„r°ρ)·3ΰ„eΆ”ΠHόΐ§>ZΖgΘLž>ξ­'xžšιήΐšd „¨ύ0Ϋ"x—1<`.Φπν·ίɐ!˜*υ$ό‘c’Οΐ6 έc2όαΒ"FΪό|MTdˆngΡΈq’tξά^`CSτƍ?ΉυA0vρˆ_l—ΰHƒ<Γ„%}ς…΄lΥ\·kψϊλοδί―ΌδΙή’e[ΥΞ4n{~}%W^qΉΣ>θf4―ΩΓS'&ŽΫ5”–Y`Κ€ΆˆΖζ‘Υ+ω°ΓχZͺ=_8ͺ BΝTD7ό†υΏΥ­*θθO`IPˆ•|nΰβ£ϋ~‘½έδ—“>eΆwχV5/ξΨ±WRSφH­Έ«€OΰS]~Ζ~V7m’ž=zxΪ+’' Ιz˜. όϊ ˜ ¦ηΠπe`{‡ΊΪ4T«+;cΏΚ―Q£dε•όΑJΝ…=zφΦ|Υͺa» ΪzVυκ%ˆ¦άλΧ―ζ‘φB0Žλl/ιΣά‹•ΘKyψ`χώήΊMΑp0ΜΕ>pŸ$$$ΐΌ'kΦ¬°ν„ΕνX1"D¨ΑΠύ13Ηk„F‡„C›ΈfήSμ+ςFΝ*C1Lύ'λV°1ώ$π³9Α‘€#GŽ ό©ώ¨–ω“"fr§―ΥYguΔD»O:ŸΥM»ρnΓΊnΊΚ  ‡ίΨ{εε—€Sϋ²sϋfιΪε,kυ™Φa͜4 rηtγΚ7N€f)$LKΛ€I²žϊ[EFβ³;τJ?²OfΞόΦΗ2‡αŸτφΫοκήXΤ\—*-ξuδH¦ϊ₯ήƒ•ύ<ΠpΒύ+ kΥͺ‘tν?¦άοjύhO&3>99I2Σ¨Πχί &*š«h"₯yλω^ΤΥzO<ρ€ΗoΚDαdOΡΤ£Ÿ—’λ―»R6aΟ­σΏ—…σΏ“%‹g HλΦ-Υ―‹ε~όq Z^OπS§NœΌή;2Α|%«πιΗIΊwο Ω™‡<ζE£Ι’Ω1:&^ΰˆ―2  dΑQ_iΘEΗΦ8‰ΡkX|(ΨπΡMBWά }ϊtv³utŠ (dˆΑΑ7lά,χήχ6}ν M/–¦ΆYHVΣβΣgΐA=Nξ©?Ρ'Ÿ~†M?ίΧ²ύ ’ϋοΏWΟ͏Ρz8pώgΫT,ωq!Ά`θ/͚YŸθ1yyΌ;ΉΟψβ3έ:bƌ€]«  "ˆ«Χ@†ΈBξ™x‹ψ£_ή~ηiΦ’‘ΣmŸ>}”€€ΉυλΥX‚Jσ₯[Χ‚½Δδƒχί•³±υΗ’EσεΡΗ&馨¬‡ŸŽ Ha0`†ς TμΡΖ 2Xξ‚#ΎpθpͺŒΏρflι0N7qτψ3Ψΐu°Μšυ<ϋΤÐ[ˆŽνΠΆ6¬%ΙI‰Ί£?7αε˜ΐΚL‘_ i σ0ψx— VΈΚΛ―ό[κplΐΙXqhϋx€_b"Ά°xώ,2ΈSΈ½Ι5Χί œ§Z/°)λœyK€mkKώ£F^ΒjάΪΛ ₯Q•vν¨, ›£CGΛ:?Ž 8p$ΰHΰΈΐ„ό_…O>ω”*ύkΩΊ«f\}΄*Ίυ“tL+-ΒGr­Ηl`6_εΆάJ†N£ΖΝ]dz=ξͺk°1kž‡GhC¬|~Q.nΚ­(LΉQΨh΄}‡³΄ώπ¨:Ψjb–{ηέχ4· υδgΉΟuΑ΄§qΨ­ήSΟ]»=ω°”žs£Q8bkhω4."ͺΆ'ι…†Χτ\o― 0z΄ŸΟ˜9Sσ‘vΣf­=[f”c“Z*Ο­Π~iΎΞιϝ7Oi=όπ£zΝ-#ƌ½Bi9°”!―Ή½ƒ fλn«Π»O?φ«<ΜW3‹[:X4b\άl–ΫkL°m€kΆ­Ψ‚MU™ΧȎ[O˜πω_hφ^sᛐž:Œ‡αΩΔ Τ[_σ˜>·xφrs‘ εΧ>Έ΄ išΝhgΝ²6”}βΙ§kmiadΰ 8p$ΰHΐ‘ΐρ$pZš/LRͺιΰ›Θ‘—κ§y>„‰qεŠUHΩ°«“Ί@C+W_u₯\zι]’ΟUƒF[ΐY‹!#3KΈ΅·= lΞάωΊqfm¬Œ»ϊͺ+δͺ«Ζ針ΡΝίΌysΥrΌϊκλ²rΥiœ\_"Β[ΘΝ7‡ζ¨™<πΰCϊ‰:G›ν"Ǝ-aψόΛ{οOUΣ```sιΨ‘½^΄h>ƒ3K΅eζ…Τφ4l˜ ώIο£ ·‹¨W/Zl& -?{΄qγ&λSD[·Βy>Ϊ_Υ” ΠΪΊKA£‘GNZ?l?Νz«W―ΕηΞ…FL΅^άrίWd0ν₯OΦυ׏μπZD~κ¨_ίΎͺύΑtˈ{οΉS5:Τύώ;·SIl˜ O>ρΨ1ŸΦ±«g¨ Όοή{ΐΓyϋ­χ$>P\₯HΝ"·œHHHPhΚ‹‰μ«κύŒω Dάyk¨4hPί³ ω­=ψ#4iSρ™Ÿ5R?‘16Ž­ŽΕ ϋ-Π<Σ…”s<>I΄βΧς:>UD0MΨόχxl=Βm'}t’4¨_Wjΐlh7+Ž»ς hδόeΡβňχA₯{όΊͺc£\j\9"ΰλE™“/ΚxΪΤΰάžόη«―΅―2°’²n|ΪΠ@½f:6Tνκι3.Z –Œ›μRΓθ­ύŠ€ί#7θ₯VŽ‹+œΰHΐ‘€#GŽNF>˜” :™όΘΓβPΡoˆίάΛΝΛΕΔƒ 6ΪγγeςqΒε$JΠΣ ‚&ΗvνΪΘ‡Σ>PΪψ΄Ž'Jkχ£@Ο^9Mg¬« +ωθ'Sξ!Eή(šΧΌ'XNΊ¬‹GϊF™U„J?,‹cα¬~υό€j`ηsΛΉΫ΄Ωδ5GΖ³^œͺ£Όw½&/†Gη“Ϋ“O=%LzA\eY‚cεΊk―Ρ|Ί2ψ^ΜιCΥ²eK5ΝffεΚ%σα3VGΏSΘοerwˆˆHm‡w{ύμ;ΚΑήςΕ8¦ΩΛQnΨDV ±Ύ$Oή}jςσΘ•ά ‚`‡ 1ŽνΟΐ?πFΙΎaω ¬0₯Y/ΫΜ`ψεΉ©‹η€‹O(i}τc}¦L> -8žΜXbάρκ1ω£#GŽ 8¨J§­ω2Δ8™ΙŠ+΅ ήΑ€€‚@ί ϊq%X (²‡ͺΚ2Ž“=WΔ™`ςqφLcΰζ«uαΜmγ @3qφ#Σ9!›IٞΖsS'W~ςΟLš=ΜSf™Έ½y$mΐ¬!©‡³<[;0_<>2nσ1ψΰŸp«mŽΒ§q °ΰ’ωg‚7οήυ3ίρΪ`ΐ i°μςf9ODͺφρ  Uυ§½LUrd½USŽi€K­›=˜:Mάͺ’oΚ―“ξ 8p$ΰHΐ‘@U¨z–ͺ*η βΜde&V3‰±ΣLΊDy…₯QX³f…ΤƒYΙδ14NTφxyντνηΗΛoβνyνηφt{›L“nη™iŒ7i&ο_q΄σP€Υ—©‡χ*Ynΐ`ηΓΞ7!%£#Ί«,;Ή―Enλ»•,G αΧ^Ži§ =ςD θΖZžzμtνyMΌ‰3ΧήG{Ίi·=Ξ;ΏΉΆη1ε˜Ζx{šΙο 8p$ΰHΐ‘ΐ_-Ώ|¦ΜδeŽ&Ύͺ#Ν’7ίr;>†©»SΛΐp2e ½SΙ{ͺ΄Mζx’ΊN”fΚGSόlΟ=ψT΅WΙάwΜLsΝ£‰£Iρ>¬ΪTmh ΣM{ΉΏβά’}r”N—‡3]ξδZγδr$ΰHΐ‘€#GUKΰΏφωͺšμŸΗR’žVn­ΘιN ^ΣsΨύšNΆ•vM”#σ“•š“Ο‘€#GŽ όuψΫΐΧ_Χ‡’#GŽ 8p$ΰHΰŸ#ΛΦχΟαΧαΤ‘€#GŽ 8p$π–€ΎώΡέη0οHΐ‘€#GŽ όΣ$ΰ€―Z9ό:p$ΰHΐ‘€#Gh 8ΰλέ}σŽ 8p$ΰHΐ‘ΐ?Mψϊ§υ˜Γ―#GŽ 8p$π–€ΎώΡέη0οHΐ‘€#GŽ όΣ$ΰ€―Z9ό:p$ΰHΐ‘€#Gh 8ΰλέ}σŽ 8p$ΰHΐ‘ΐ?M§ύy!~'P?Ϊgk±υg[O«Œ4y¬οώ™+ΟχŸaνΎŽ/σΫ€ž UŸ˜oτYί΄>ΨΜ2<σ”E’‡¦ΖZ©&Ÿ‘lΡr—Βgw4Έ3ρϋˆδF¬μΈΑJ;š•clη†Ύ›Ž¦Ί«tGUy`Φΐξ•_«αžΌFžˆΏΰδD˜ώ Θ;$ 8p$ΰHΐ‘ΐ38-πUYQ!ΫΧΜ“ς²© ¨¬¨” Δι" TΡ€|…s€·‚‚BΙΜΚ–²r €[£¦U“μœ\)--_Π―?KΛK₯RΚ₯Œ.*ͺ”ΜμIΟ.”B|_:0¨Ί†D!όTŠδ䁿δe§#Ν_κΥk @Θ? HςσsρmΔR0V)ώώβοοƒ|Y’rh―δHέϊI :εΰ‘TB‚CΕΧΟ_‚ƒBδΐž]’Ÿ›%5jΤ@σ*€Ό’D|}+ΕηG$&:JB‚ΔΗU*A.‰°0 υ—Π°h —°ZM•.Ϋο>|πKΏ@œσθλ#ψ)’1Οό|Pw¬Z΅JRŽ‘ΤΤ#.”[6β·οΨ)ΕΕΕΘ°¨rgQœπΰ|μ£ςς2RA λV‚jw"Α•KύzuεΧ_³Ύ ©TœGŽ 8p$ΰHΰt%pZΰ‹ *7'KΚJ‹P――5―cr―ˆ±€—kR7`Μ€|•—JYQΎ”ΨψΈΚ‘Ι -ΜώH++-ς²R)-!@ςQ0ελλ'%}ε•`ΘZRκ'eoΕΕe¨ί7OBͺ(„KVNž’hΐΟΎCωRT\ˆτ0‰ΔΗΌ€₯θG©«U ‘ΜΜ )-.’¨π0 HΩ΄cŸψζ|μ•I^^ΎςV·nΌϊ•Ι―Λ·Θ‘Τ,ιr– ΡW²³3T!Αώ’v$Uβbs%2"T‚ό*$4π¬$P*ΐkeiDΰ@+Ύώe>εhšŸψ‘}<Ί|ˆ–\RQV!~~.ρ".ςE΄¨{£ΜЋЀ ?}‘-ψΐΆŸŸŸddηΘ¦λeη=OώRVa•°ŠωH9d$Ω)G@ΚOjΖΧ’ ΐJ2.~δάυϊξNI—AH3ύθ|HΓ9:p$ΰHΐ‘€#S—ΐi/NδεψΓn@sWyΉκP€Επ#:&ΰžωΰ―εK₯Έ΄ ښ€―P‰‰‰’(δ)(Θ—ΖI $ΐ(ΐΟWΨ'{χ† €ŠΚ$4Π_2R3% ­Π.΅kEJ4@šx:|ψ04WfΥΒœδεeKυhπ pSY‘Ί™7η6R>Π€‘ΰ=$ΘOω£φ€ΖΧmhΣΐwΈ‡X±>Ÿ―ςDQ.ΠnK’ό%HS|0J °νΑ Όp‘ΐ*22ZκΖS[ˆς–Π”oΓιB³V#:RVXX5-OΰΜ2¬Α<’έ‘V~Ο4π2χ0`;!/gšS½ΫWύ†ηθHΰe pŠQ‹Šυ€ΤgΧρξΙJΎ8ςŸϋωv&εfψδΰG”ΐι/ŒLΰ.hQ¨%1· Φ Pπά4E2hMO ΥAy‚ŸR‚&ΜΨAζ"ˆ «@©!(π…V¦ vΕ’Š£ΎO€24§εζεΐœι’Τ΄4βiœœˆr.))Ά™ΚΚΚ ΩρƒV 6Π"ΰ«mΟAΩ=ϋ‘ξiΡͺ΅ΔΔΦ@tΜΥ€4[JbrC˜+7&Ώ@˜C@ΏšΈ`~-*,ΘΕΟfG*5Λ–£ ~ΰ šΊrΤ_^!₯ΰ?ω]8ϊS΅E;)΄]°^ΐA¨²ΌeωPρ₯p¬Tfω}QnμʟΗRh ‹Ρ.j Α¬–£VŒ˜…5΅…4₯2ΎΌ ˜Κ‘ύi^~4 “Ώ+Ÿ΅@ύ―ρζπγHΰ ˜ϋs ›`βΝ΅9ΰΓgέƒPά^!ά£©OωY#e`dΑs{`όρ˜iO?ύŒΜ›7_‹ΩσΪΛςœςΘΖXxσΝ)rΓψ›δόξ {^τϊ±Σ5IήeΌ―½σyΣ0ω–pγƍrΫmw`œεiQ“ίδ3τΌΗK7ε7mΪ,oL~σ”dmΚfeeΙK/½ΘΜSβΙ›GοkCŸtoΊωVيρΐ`βy4ηήeνΧφ<οΏ?UΦ¬]«ΙS¦Ό-Σ§lΟͺτμωIΔ…wZ9\^~εί²gΟΝjy4ηή4Nt}’rf 9’¦ςΞ…(CUυœˆŽ©Ώͺr&νd'SΟ‰hUΕγτ>,*•Ÿ6ΩΏn“ωkvˁŒό*ŸW%x^ώ²υ°dδ#ς8QGΣψΪΙ?ΓΓΤ\yϊ«5xώZ/€&ώh‰cΟXλc½¬Ÿ|œJΠϊAƒJ†•[Ιδω›,ˆ:^;Ό46τ~Ϊ°_&Lϋ–,Σ‹τρ~GΧπiΚ½:kΌmψtG²'βιΟ msT^Μ…Χρx|2ۉʝLΊWU§}yΪΰλθ ³OΪΠdQ›PΕIœf.λh˜ϋڝFηrš™‡τ,š„o•ΠΆ”βΊι,Γ:OΠ¦ΰΝ>_ԌΑΑȌ4¨Ε²nͺ LΚΕ’› -Π(+Θ)Οaκd θHOΟ@=ΘητBŸrψYUΰ戈ˆ”€Δ$©[·>ΜnΠh‘M[·m—ω‹—B«U‚zό%''Gλ!¨£Žιv%ΕΠ8‘έ •ΈA θ/–…™™ΩΘ“%iΠΖ₯bΒNKKG$=#’–nΕNI#}ͺ€ΐ‘>55 Ύei8GZ Σq~8t΅ ”5λΰή€'άξzΞkφu„μτΪJ3ύgΚXύ„<φ2ž²šrfΨo½υŽLzό)y;v[o»SžαeΉkβ} €(Χ‡'=η;δέjƒαΤήN“fβLs€Ω›r`0yΝ9―νεΎώζ[yϊΩ—δςΛΗΜ‡yΚ0Ÿ)kΟ―ΌθΪγμτ {yž›x“ΧL΄«W―–ΙoΎεαάcάZγœu:¦<―MœαG{Ί=ήδMK;"/Ύτ†ηΑΝό &έΠ5tμiEπOœ0αv½ΟμρUεeϊι„`΄Ω›'CίπθMΧΞ3Σ8̘ω%ξ΅ΝΚC>[ CΣ§ά?†Σ?,wΗνIΖ*Ÿ[ FG3Ω~Όλ7ΧΜbΞΝψΝƒ/(εΝgCUωO¦~ΣCΏ*ZΗ‹3eN¦žγΡ`ΌαΑδ1Όο8œ%=_Z ½>]+oH•;ζn“zOΟ“ωΏνΥ¬h’g¬nά›.έοš+sVοv“aO?+ΞΚΝεΉο~χπηVμ‡oσΡ>$•γS³Rf£^ΦO> }½8ΑΟs7ΚR$΁ ιy2cΓa«>Dρd;M8*s+ρφt“ΟΠ£lα"c‚ϋ–6—ž£©‚ι&©‹™&}ΉFΦμ°ξζεB1›’Dϋ ρU•eyΆπDιšΙφ£ΌΰΪΞ‡9Χ΄ΙΖFǜSV鞨_M©Σ?žžΩQλ£: ;ΥRΦ FAP„ |ΐσh&#sΝ8 \σƒIΞ¦=ϊp<Έ<%dΑ9͎]nR(i™ιπEMŽ0θ©ΝhEΕ¨c)"2«bH₯_ΤDΐ—+“+Š υλλD@°T†rϋχνΧΊš6i§ϋD:ΐͺ€ZDD„IΟRΣdq L–Πjqf@^yy”„G„αO.5jΘhE­§φŠRh΅°ͺ²@Jqψ€a°C+TR)aEUš3¬xΘ ΐ L–ηšI>KjžΜA˜ΌEˆ Κ°³Μ·T}±"°Κ‘υr₯©YΘ„`˜²hŠ•‚?Κ€ΌζŸύάΔ!»ΚΥLΌ>“>s―ΏώͺΚ!“Iίώƒ°κςiΧ­šL™Ύ}ϋιέ££Ρ|€\jΛ"##•MΆΓ<Έ –˜Ζ8{γwϋν·)^—ΒwΪTζΛΜΜT€e Αφok“ηžy\ΊuνͺyX†υΗ‹©›}Cωάq&]j\9ζΗ2€μ‘Αώγ VH‹γ›yoς2ύΰΑC²pα"Ήϊͺ+•^‹ΝεΙ'ΧsςOZ¬Ÿγ2«ŠCCC YεΑŒj§Θy«*pLΆnΩTΑHAA–­Jž|‘αjΫθθh­›΄XGχΉf<λ€ `ΩFΚ„Ό²oxNω1ΫCωσšωŒ©ι~ζ™§<ωX啍(ΌονςRBnZ¦Ό΅ «’‘·Vχ²­ W^qΉgL˜Ό€O 7γ Oδ™ηԊ²=†gιέ§“‡έ&Ψ.‘ΌΩŸ€c=ρΜs·•ε˜ν%mζ©ΐu6dV Ο Σ&Φiψ ΝκΥ«cEv°tλΡ[γIΗ;0?εc=#Ž©“ρ¬rg[ΨΗ|Ισή}ΕtςΛxσΞΈγέ€oϊ™ωX/_Šωgx {D\=bbbάm΄ξΥ‚’rΉψύεr~|„,y–TΓJvΒ‘™K·Κ€7—ΛφG"%Ήvϊƒ”E¬; QνcdΦζTΉ¨k#,‚²ΪC³Π˜Aϋ/͜WˆRreΦΆ4Ήυ± γ»…A^~R€y€n/ΥΓƒ™UιπhιRΞδu6ꭎϊΙGϋ€ZτπΥl΄`˜yŒΤH‘'ϊΣ-eɎ4©i½€Rύ0ϊ γήΙ/Φω!"ΤΊWΜ8 ¦±ξ(XepΠΐ±ƒ˜η|% 2.?=ZΤ•ΞMλ`>ƒBυqG{ H£‹‘‘‘Λ ΅w±!Κ7σ‘}ΟnL‘ΞuπΌj§σΛυZͺό˜N> ‘2«e7˜d|)”Ϊ^ΠΝΑ 9“ΞΎ3υ2ƒΙKΩ² αX θ~9b:λb;¨i¬Œη ώŒl¨@4ε‚(cΤ­,²,ιfδI0όΐ=eη!IύeΑq§AŽ5)RΛC¦!)ήxζ[}@- Ωgγ¬?¦qKf>DŽ.d!9ύ#[|PρfΤΦ{κα„b™1•·¦‰ˆlαΖ€?όΎ²±"3B Α€ΖX5L*xΨΠDγΑYΫDΠΤX&ΖΈZ5°:2[ΣȊ‘Ι’™2//WWpςaΖƒŒΠ©V NφΡ΅€V\]LVp•xHΐˆ‡EκΣ‡&Ά±(‡―ΗΒΒ<€―l),Κ Μ‡ω1Z4δ/Α L8ύγqŒ€ΐŠ­(p“c‚ζV‘txΗβκD5©βΘs:Βσ‘Θ €η |³δOσ-eθ2θφ~tpιΉ¦[Φτ i΄b8"¦ΐΎg3ψ0/**±Ζx±OœμiΊtΤe’ΤΈ΅Όχώ*Ά‹“Λ€ΗŸώΞ•~ύΟ‘Η{q–iΒΘΑ4νž{ξ—Ε‹λε“'ΛΏύͺ<ŠόC.*έ~}¦‘ή·ίΝ’χήϋ@F -kž‚ˆ;ξΈS.Έΰb0πτ'|ƒ©2lψ₯¦έYςΛ―(Πb={ς©t>«§ŒΈd”Œ5FφξέΛj4ζ{σ‚΅&Έqγ–#FJΟ^ύδε—­χ·M>RSS΅άο]Ϋφgι½ΘφΑΘQVΫxmxδ;φ ΩqΩ²e2θœσες+Τ6³φΐrδ™/./-Zu€Œ†£?ήT€lς²>A›ewΙΰ .’sΟ‚±1IΗι<ςΘcςδΒΈσ_°ΥO–―X‘εXW ΪNω0μΨ±CeΝ>©Ϋ ΉόϋΥΧ4~5LμμjΡMxsΚ[2yςε•ύ8xπ…2 ύΨω¬2kΦlΝΖϊΉ(θšk—ƒΞƒ6vœΜž3Gb0֌| =sœ3g.δήWƌΉγm ότΣRMβσνρ'žΤ±?ρξ{εΌσ/D{.”;wi:ιšιιιZ–ν1ξ—^z™ςKKΐ„ wUyO¬€lΏαFνs–%0?ώfΈ¬TRΊΗ$ϋοΆΫ'( c‚y\»ύ°lH+”‰C;(π"OœΜFτh"£[א―VXό<€a2}~]Š|=²½Μ/‘ {ΰμοΝΫ οΜέ`.ε0Ά,όΚIx™ mΥE³ΆŠC§ηηΙΦƒX”…‡t κzoϊβΥEσμh•ΒΡ?εΒτνΣ2WT`/°hΌMΗIΓ€z¨Q‚$&ΦΓκΛ†ψKδΔ°A]iP·ŽΤ―# κΥ–†υγρWW’ b‚4IJ”f’π—,͚4Ώš5kHήJ©a Η–*›'Ό)τ²7ηˆη[ei4θΨ o“ŸρΜΛΐΑJ_= άΊ‰jΚ™ύ1όθΔ₯ν@?#˜‰ŒmŸ3ϋ{hΒδ㏧ΛίΝ€hœ¬_Ώ^σM†5Csη|―ωvνΪ₯GρcθσϊΠ!ψdΰ훁ΪΠΫn»U.ΊpˆΜžυŒ=RnΉυv,&NΌKϊχ냉λ2yϋ­)ͺ)z“/Ηβη_|"3g|*[€L(½¦O…V%R:Ϊ΄n ΆH>ωψC©Ž7ϊys~Q#G¨Y•_Έ`ΆΌψάrρΕ)°#X|σΝ·ε_Ο<°τL™όŠ >|π{HΞ;ο\yσυΰίXSλMJJΒώuω²rυ:ŒQjΪ²€eΛ2dΘ`ωμӏdΪ΄w19•9sη’5εω‰Η“^={Κ·ίόGξΏo’τ=ϋlΤ›«cΐ.ξ#χύw_Kξ›ιN•Yί%ξΈ]Ύός?JkΡ’Ε2ϊ²QςυW_Θί#={v—›!3ΰ}]Œ—j”(§+ΌF’1ž?ϋμωβσδ΅Χ§Θτ>’xάϋ@Ω©4ΧoΨ(›6¬υ\oΪ΄Iγ~¦ΖΘ;¬Zσ;ο δ„ ·‘οΎ—_|VޜςŽTΩΫ΄h5†γδλ|*3ΎψT_d>μcΌ€Y/49Ca!·ΤŒ―ΈΟŸ7[ώσŸ/δ·ίΦy9ŸγΖ])η €m?ώzlCs–ϊ²¨Μ3lκ΄ιrξ95ίΚe δΆ[oQ_ΖΆmΪΘwί~%[ΆlΦϊ؏7έϊτξέSŸS-[Ά“λ½ZσL›ϊŽφύ–-[4ο½χήMxˆ|τΡ‡δΗ—@+ΌΣS'3™{fέΊurΞ9ƒδωηž‘O?ύX|π^ιΥ«§πή`?Ργύχ?ΤΊfΟϊVΪΆm-Ο=χ‚ΦΓη‚ qximΦ΄±,[ΎάDΙLŒ…ΑƒΟΣ±σΰƒλKπ1χΔK/k^Ύ€ξΪΉΗSƒDvνή«ΟeFf@ƒχΩη3eβ]wΚK/>―|1ήΤΎ]C“’%Z' ΰ‹σ È6udήξ,Έ¦XΧ«·–x¬:οΥ¬Ž\ΠΈ†ό°vŸζγO΄1yπΙ54ζά"n`‡†2½²ϊτΞ»©·4‰―š•²:₯š₯™qC/YwuWy`αY±ι€’ΰŠqŒ€ΎG}ƒΗjύρP 00η‘l₯„yΗ."ϋ)Οt΄ηŽ ΪJΟψHΉͺc]yυꞈCϋ1/.ή”.­λFΙχ·φ•o†΄‘Ÿo]B ?Yw— ωξΖή2όmM+gα£ΖΐϊΎέ.Ρ‘²ύ‘69N²ΡŽ­E°:!­]γΪ²τΦ³ε΅«{Θ€‘εΛάRΤuΉœ›# ο([ν*γfm“%λ­±r²fb­θ$|O2ί²ρZŽˆoJcΖYΫe|“1jL¦[“=ŽΨ[AW‚"·=ΰ=$@š%~εΝθBήr¬zdj|s.h*ΰ ―ίDΓ‰*Ύωeeg‚6Ν—xθ₯‚ΥΪΠ‚Α ž¦GͺŒ«aΫͺλΉΩhͺKδν‘©3-=Uύ$" ‹«U [JD"š) ϊνΪH£€Ιƒ£ώμ•‚ν)RS³ u(FΒα'Φ΅κΠj`sWξ -@[:΅€Yσ&ΊusL‰°a=}h΅€‰³Yr²$7L€΄€WΌ4ˆ‡σ?ΑWΌΤ «S»–ԁVN\,ޱW3·HΘ„κψΔ΅Π3©πεΩLCΔk'ΊŸŒγŸy˜“Ητ6‘Ι\ύΝGσ(sΡFŽfΝΫB2[„DHηNδΒ‹†y΄Nχάσˆ ˆ‘ΫH@sΟƒO`<`o97 Σ°`τ•ρΧ+8zν΅ΧΥΔI³Πΰ cL₯λ$HS/Ν>Q؊„&:޽Χ^}EΚ8fκΦ­+?xΏ<>ι1KΧ 6Γδ|…4hΠ@Α25³­ΪtaC/V >pΰ©Ÿ(_t‘j3 ͺ7i­~„–Σ?ό@'HNΘ€‘˜ΤL7Χ%ο4ΙŒΤ3!*2emxiΩΊƒŒ»ς ₯ΫΊU+‡―TkΗΌ”ΙψoPꑦ²σꚷh§~‰†–9RΦ 7άpΔΖΖH³fMeΦμΩςξ{S΅Ύύφ;•εBsUOΊοΎωJvp€€ΙΔύˆ?LΕK~΄@Ν…ΙΈ>xŠ<τπ“ύλςh°Φ―[/χέ ¬‚_Γ‚‹|ϋιΉzŸXΠΒ+ƒφkΈLξ»΄oϊτξ-ί|=šβΝΚ~7}Ωη3 ­ϋύάNΝ€WBNρu“­ηrσεKMP8§F{ύϊΨCoξσhωκ?3€GξϊΜβ†ΡS§M“]»θΈ3ϊ2ιΤΉ;΄Ž΅.Ž7s=ϊΘCrυΥW«φ‘ΟΎsΟ»/‚Ϊ‡O=υŒ,ˆeΨ `•˜PKy[‹‘1q ΠiZŒ‹«­Z²u}H§Mϋ@ξΊs‚ΤΒψ#Ώύφ[eΛfάψ@IDATλD‰αΗ΄™2|κ©§΅Ο)F.α’‚―'žxTš6mͺ2>l˜¬^³Φ#g&σŽ3ciOυž{ιΕ7„c™Οζ7ί|ΰΙλžx|’ή{ΤΦσ~³šΓ δα™§—φνΫιΈ6ΐΥ<ZjΓόgžN&žτΒa9ΨPaβ>ZΉ“t­ͺwΛΊςΨΖ#0=YΪ 5ωαΩn αgŒΫBCM€;ϊDΥ‰²LράbI"εځ-%ζ³V ±rk—x9n-n1–g[2‘A›΄1Mz‘^†‰ΰγγ{υœ“o0ό’Θƒ ,Sυ‘ωȐ΄ΕOb`:ŒΈa ‚ {“κ2€s’DΑάx~§D ―,9…r΅ι;³δž‹ΪI|υj’P#\½Έ<·ξˆ£œϊ`Ε…Κuη΄–dl₯δv“Οψ@ΌŒγ„¦ΆΪΡΥΤμ7σ§­rgb΄ŒλίBλνΥͺžΜ½­Ÿ‚]Π6υcdCF‘Ξε5#CΠ_‰…˜'w^ΨNj£/›Τ‰–‰η’mP'Ϊ΄Ηί²Š€OΫ:6"ΒCεuδλ΅ϋ=u»ΩΠλΏβΗ’ΤiPβNσ•ψ£Φ‹ gG2X>ΰ¨9α$Ο8ό@iΑ½ΑψςEƒ™^IpUΙΑŽŽBΌnΛ!Σ,Ι·δR¨_ ςρ¦ΐS„•-EX%ΘY%&/²’R—δeηd—J>v»/.σ“ΰjuxhRF³αh>’0aΦ]ΆeΛW„ΚU‹τ δΫ-' nπZ#6ΰ*Eς0ΩΡw₯υaUž#Β«α-$›©i>SuT4}< α‘UT”'©‡J­Qβ ΪXέXΘΑqc‡ϋ0LΨP*©θπ_ °ιޱ*'ΘΗ~pxΊ‘.<λζ€&ΟΖU@«Η V©₯)GyTΎ)±,-#ϋΓέΪ#ΆF3£¦³liξSφ¨ωβMΙσΉΰf‰}άBކε„Μ‡8Η%Αi ψ(½ σ ΅ L£6ς†k,³•έί„ν³Ϊi& %¨1εx‘v–A_.Ψh^ιάΉ›šUnj&η‰MuB" ‹xf :‘ šΎlΩo‰‰υ­>G>j "αGΘώfž)o½…–vm[*ˆΪ0Γ’ƒΌδ‰yŽXށš†blάm+₯Γ j vμΨ­ι,WZa4"P―7°a:_fΞ9w°²5uΕTηŠβ"½'ωRτLa+°"•YΤα#.UΰΛςτ•dΫsqOvοΡG«‘ή­I@ˆ¦Mjc‚'ήt#LǏ+β’”ƍ“œGVz‘אξΚΰη%σTσX'Ύ%θ΄~Η^š•mΦϋ}AsΪ•WŒΡx#Ÿ~ύzb³:vκβ_$’Ο το«t ―mΪ΄–,ν ε‚ύMΰ³Ϋ χƒ£mΫ6²hαJ}fωσΞ-=€Τ ¨Ώλξ‰z M¬{χ'ϋžόš`1ή€33F˜Ω™―>φUœΆΑ ρQFω·}˜D;WU@SΣτ™rsΔ’½Y˜Όq_€Ιο»Rεμ6 τ>6/\Κh+Gξη"λ,Γύ `Ε%νBό$Φ"8J ΚlΌπ™ρϋΞ#¨―Pžž·Eb–ξνι²πPž<Ύκ׈Π΅·›$μ”@¨Δ0ΌΤθd ‘ -FΈoΈ·Κ°υΗ2rŠΖHœshz$x©Ύ}Θη η°¨ζL7cgΖΟΫeβο)’}ο@FkΨ“’->_-;sJ€^DψmŠ#ΫIΙ΅O«–—fΛ-U ΜΓgLŸZΥ0—[ΗH€Mϊ}›`΅ίϊ5qzΉlψγ%ΖzΡͺ†—\‹Φτ…εͺ%{€²A˜{ιΣ• _5©` Ω‘ύ”E$ΐ’nξ β°ZA­ σηο’Ν[ ƒ―cBu5wRΎžΑw S§qΪΰ‹½FπΔ#;’’Κ• cΰ `,6<τέςα AL£­Wo>~ΊηγJϋԎΡ,X@’WP,i™yxΛΔV pφ‚'π* a‚―Bψ[aχx_kλ:ΒЁm*`Ε ^ &ΊPΌarO/ΎQl%ΑδAΣ‚ λ|8ΝσaΕ7g–­€Ÿ5`ξΥz[Άj‘ΊRlήΚΙ ,4š¨ΊzΣ”••κ6Ύ½ΰΪZΕIΩΐ©ή§oY.ά.8ιc ”rΟ/Ÿp,…ΌΈ‘)Ν―Xx­?{DΠΙύΑ~ψ‘ΐ ΄xδC†€σΦ†<‘Η­gϊε{seλLiZΧxr‚Μ‡qάO{δΓ>‰ZdιΖƒ΅f5y°}I‘ls|ζνσ•{|UΓ‘Vh½¨°@iY₯μ)±ό·xΝΆγΦRΰΕkJζ¨t¬sζ±>ΎX΅WιQOΐLFS&Ÿίω³7Λ²Ν‡|‘`κ4/R‡0Η©Μ‰~πEίΫƒφ£‰@=!ψγsŽ’_Ÿ²b‰pkΚ²¨αCϋbΨÏ*l‘w>/L 9S7ΨF=λαŸ6β»M²wΒΩΠΌYΪ6ζόΫu4ήƒI2§yπ‡»}ξV1δ•ώhnM—Uς–”z@Λ΅?Σς«’c?cL-±ν‘d@PΨdeΥk5ςΣσƒ<˜ŸwΐφFiQr§δΙψ ²>[W}ΎI~½³§tiR[^όΎJ!Σ†Τ‚ΑŒΊ/-OZBkΗp(-ς²άLh6Š•Χ즋˜N¬Aπmx9f2ΓŽνΥS V†΄ *2GG8ή¬ΐJœύτώ`ΔτΡ’Ζ*ol>τ½JΓΚAώqο*ވ|πqρ»Ϊ"ώ ό?φοί―[.p/ζ₯]œτωP§š½Iγ¦xH·“6mZIBƒϊΊzˆ>Ί*Z&>ΰ© c~>œΈˆu€Ή’oάg‹›½ζζfγνϊ jp‚(Œh ‡…/ωaB,Ζ^^θ¬(j}H²y°š‡&»‚Αz€Y,J3FγŒfΓΠ&ŸfΠ4λΑƒ_Vτ·n˜›αn˜qOΤ|`‚·jFYρΙ'Ÿ’ηŸQΈΉπeψa= Σ&σ€gy:E“5GFFϊ²°Χš»Zώ¦,͌7έt‹:ΤΣaœfp:-?τΠΓjJ"/T}Ϋ₯Θ²τε2Χ9pά·‡½ΆW\κ{΄sΗ&Ο”c᝷§œmχΟp#Ν–-[Κ†u«…Ξΰ8ΤH\|ΡErΥUWj^ςΖ{Α²³-GZ{Ο ™2ε-μ—— ί€­κ;Ds*ΑM¦#G^*›7oQ`ΐν8FΓ‘›Œϋτ l|Œ·F©³χ«p4§ί厝;εΪλo–GΊGϋE_„ yž0αάΣ­•.gu–ΈOztοͺΆ‘½GnΗΒɜΌ΅jΣ ‹'~Δ½’ j˜Οτ9ΟMωKF ΗΒ€‘πίϊ]σ΄iΚ.•­[ƒν’\|ρτΣφΤΐ³!9Ή‘‡f-h―Έόrέ“.Zϊ>ώDV,[ͺώv¬+7ΧκΦΝ€(Υ\/XΈP>ύd:•– π.u)4Sύ€^έxi Y1΄nέZΦY-3fΜΔ(„Yv›\0d¨š ©i=ζrέΧ‹{ΞνΪ΅[0DV―γi'i˜6χλw6@έ}²tιΟκΫGMάd€ΊΞ;3ϊΨ“—Χ,——k«ŒD0τh"Ώσ‡tJ·nέ4νxχΔΓ?’€‘}Ότ§…nςδ ΄kνšε:ŽH€ΐ<Λ” ϋΗL„mΦ”ϋΊΑ}cςRY ͺlΌ˜sςˆ=«.θ’Œς"£ά%wχJΠͺ/ΰΓΥ«ϋ.;»©ΌΉ6Uρ›ΗGΙ#KχΒ™> ŽΧ…ς!Γ VLπΑΈžΉ-S6䙐bKŒ£o]iL0|Σ!}2L~£ϊ4ΡzY?ωΈ·gCΉzΡN!½’ε–…;e/@Α>˜.ί_ΌUβρ 04βπ©ΌOאύπqc Μ‹mu1.ʊb<―jbβ¨Δ(υρ:(|τ«ίδΦ5aδ7‘+δ ςz?‡AyA sνoύφ―2uPc‘Ζ,4¨McΛlΙ󳋢K˜D•O¬…‰ρΗMΨG ζ^Κ£Ψ-§«»$Ș/7Γα?E² ϊ`ώFh'‹₯URœζ-Dή£cΫ዇ωί;°xΘγͺ9Ϋuo·L7-Ι,–fκ;tI–Βvι6‘ύya¦Πxέ9 ύ`™ό 5n9rΧ·₯ 4dYMλΕHV‘Ύ5g½δΐΒΆ+5GFaαΑ+w©ΌΦCSϊ<pε*ƒ_o>OφΪΊγO6·'Ÿϋ“6μEv ‘Ύ1‘‘lΌ‰£vŠxσΗ ŽΤ‘½/Π~•s5 '¦r‰‚Ήƒίδφ|££0iNβNσ11±πƒΦ‰oΪQhάΧ'($\ƒαπ^ι/G0@χμO“ά|Ύcβ †οπΤ%&$JBBCL¬e0SB•Ο·­VΝ)˜+ƒ,Ϊ+~Ÿ1 uTΐWΜ€*o+α05–be"ω‹ …Ssœœ£‘‚)”  [HΔΖβ£ή˜ωy#°G='­":5bTUƒ 8ΈVΜ@c€ν&ƒ©πύG_˜^ύhK±ς‘ωωY#‚°JΤΟ—ώ±ΓΥ‰`‘“&eΝο:BΡ§η ¨Π/V°ϊΓ}aA’™Px3‘Lΰ9γ—ω鹝ʓ|ώθ3˜Ψ0Qž@ΝΝΗ 7BcΔ ./ΌpΖNˆΤ‡oέ€ΗΥ4οvΦ†Ω٘"£ρaτ`L„&ഁϞyc€ΟΗϋλ‘G‰yΔ%#˜ Ψ_ύn˜N@Π¬©5‰ςšό5i’μiσ΄lΡΤsΝzΞΨ c΅T}~ώυ―η€kχώA[Ήθ’!rεΈ«‘ΧzwβΔΜMUiŽγΚ3Ύl΄kΫBΝRœδ6lΨΏ©‡°`^.Žθ‚ƒAπσaΰ6'Qξ­9xMš­Z5ΓΓΨŒ3ς"Θ€‰pΤθ±²sΗ8³Ώ¬~kΜΣ·οΩؐτ#Ήtδ‡:ͺ%~κΙΗUKΖ—‘>½ΞqΖΆRωπ#“ %…{.KnΈώ*¬Ζ₯U±o{τθŽUн¨1’!|όKύxνέw,Σ‘}}U‡&ςΡ‡οŞp€aΓpδή'ί6mΪ„E΅ΏXž<ΏσΞ»rαΕ—¨6šͺ‰οΑ½f₯S6ΖΉμ˜ΡBΗvτ$ΐbjiΆθžπβ‹/ΙΧ_ƒ‘wλύΈiβε³―OŸϊŒδ‹ίΜ™_ΚΥΧή$M›$Ό”;°"’W~ψΣ­[W=§Ω•γ„χy,ΐθΊυkδξ»ο“?ό«HΘ­·ŽWŸDfώΧ3OΛύ<¨+ 6L€ίγν[¦™`ΐ3Η WJ’O>³rsςΤAŸZEΚ…šF~ρΓCŽU §†ttμΠRΞ:«3Μλ΅”Ζρξ‰;ξΈMΙ€se£€SϋVrήΉηΘ΅Χέ€4‹:·ϋ°σ`κζ‘]‡ξ–†w”XlζΩωύζΆίαΰ=[OμΉΉ—DΒjύžti‚ϋ¨|Τδ…λ€Έ(ΉΈquY½υτiΧPnkw@:OY*aF»¨UmΉ*™[ˆhi§τk[”―ύ$›oι©[΄soυ`ε€oζ ³u…‰γq&μ •TΫZuJ@BΣ©I‰˜·]vΚ’aέΛ\leΡ«σΪΒ”zVb,ήα­η;i\rV’\φρ*Ήαύ_δ›;b<ό²ΰ4nΩ‰Κρ bxŽς“ΎΔJHΠ+ ξξΎ¨½¦±ξs°“§qˆ₯Γ{+Ȏaσn˜H1ίώΌ9EΎν€c.―ƒΆΎq]Ήηό–2ι]°κ³{­0ι—+{3 0ΞΡ&T}ί€¦ϋ³5²7g™Όu}o‰όΓ †ώνδƒœ"9χ£UίπΓΪ4Y5Ύ«ΔΑχŠŠ™ΑπΛ2[}0?yj„―ΖxφIL‚Ow­'_@s9ώΛuΊςtιυ]ԍΫjΌ1Έ‰tϋp•τ@ω šΦ”K:ΔyξεηΖv“VXύ8hζ:Ή>!J=§ΉΌΈ`«βŒ p_™;‹άΣκ7Xνz€τr,’ήφ§ηΛΔ_φΚξjΞ4ƒPSOοζ_χSθΚΣΩ{νΩϊ1j‚ϊΗ”Π”hΡ‡€~HE6Τπš‚3ώY>Jό,L‹bε%XΨW=W εψ£©Δ7?΅>4½ψ…HDt `―j+π·Bή@?€=Π- br Κεπ‘Ω{ M³π–€@«Δν,φξέ/9ΠZ΄Ε[4<5hΑ˜ ς ςdϋΆ­’Ψ ^jΖDKa>v‘‡Ή‘Ξv|SΨ³w7>3t@αή€lύΐ•$x=Γ€{ξ”γm9 ΐͺ[Vΐ PΔՐ|nP;…I €X(4e~i•Έyΐ{h˜DΤl Υγ°ΟW4€΅‘+χψςEΫ8‘Πο‹{ΊΠGNϋ‘πΑΰͺK~ΆΘΗ…•š)ϋeΛΖυΰ‘RVZ–όᓉΟεaεΤjh³­Iτψηλ~¨RƒΒψκΡΥQεΑtσdγΝLmwΣoΥ¬‰|<}š˜œΒPωΛ²r˜4sβ6·Ζ–χ~AτΟ#G΅>ΜΛɏτΨ~{ }«έόX{™¦³>‚UŽeN†,g₯[pΪie£Βnš]L0e °³Σ3q&‘_U{©Ή£Ω‚Ϊ[ε•}κ~θ²<5LԜ?ςJΪ†7¦sε#Ӎɐ|0σ°^†ͺκΥόP~L§L¦xNΣ.ύ<Ι΅3°F~†©‡yΗ½Η8ƍωΡΠ1ς0ςaΦCpl—+σnήΌYš7oSmŠjφIŸνf^φ#Ϋγ-3ζc5όŒ¬ΣŒ'%ΰυΓt#Σ/Μ’cKολώρξ CΖȏ4$΅”Ωί}‘ŒωΙ«ι'ο{Β”#ͺε5·Ν‘¨]cD4ψT+a“~ήΈ_Χ‹Ε~d«w€JΗ·~•œGΟ³WͺΕaؐν#mHŸ–>»άMΪι­™εK“ξΎ^ThΩqΉr‹ΐ‹Z.Ϊ¬Ω‰(³΄^Ό™Λ‘‚dG[i“Έ±Έ’‹φVώ…C£Δ”uϊβfV_­h½bjΤ†_κΡΑάƒ+ ΖΪΉ“Π0—΅ζaeI₯«¦ψbcβΊJεPή!© ‡@|³εv ΟƒΝ=Ύή !S>΄s£€Z!κΟ-ΗfšτAaΕ―¨T“ΕM]i%β†.ψ{ΡΞ‹|€σΑ €s~Ϋ‘oFτ‘£KƒΚ•ςΥ›ΗŠΣrΒ»ΑŠ₯–‹tό&L0yy]•<μm6eμG;ί€E Ι?;mSΖΞ³½=v™Ψλ$ ςE-”w φ+œΜ{ήΌΩλ3rε=Μ}¨a£Vˆ.vήyn—ϋ‰xg^‹ΎuδF›ΝβjJ o:ρΕ,ζ ˆF’7 5€Θ¦(™`/Œ³η7yNΟ4CƒΌ’~ ¬4Α€ρšυS1θoΥ― ΛdtΝoέ‹&˜68β™o|y3˜QϋΧUςI>jΡwΚμΐ‹up΅€ τ6IڐP(/ψg]Z|ςάΞ?―­gžύ1l…’Εl RΛ½Υτ·ΙMŸ4μΐwΡΦTιύΖry(΅θ"γΎί.o›μ^€CΐmπFv#%πbύG₯Ε«“ ΰ(~[ωά λϋjΈςΞΎ,§oK}ΚFьZ ͺπPhrBρπŽTPT ˆ0šύᣕ)iXaXS$Π„Ύ]ς­*~εQ%ΠJeθwύ‘σΘΰ7‘ o)ΎxΣ‚ωΞKŒ}±²cΜΗ>i†xk(ΖΫxΓλΗΑ δΊς%6ΊDͺωdI„/V8FϋHέθP©ƒ7†h9|°i&:?ψa5&WpΓ³ΠίZ‘ςTΔωKj\‰—\Ψ‚ΣkψΘξƒXP‘*™9xKς‹¦ ΫZIU Ζ 2δσ@}0―|4Ζ₯Ρ”ϊ†K°j-œο]›/ŽΤ ŸξŠnΰΕ7;Ύ%°¨δήT”5ΓAΡ;Χ0_"#NΟ(nκ šπeγNΊ@‹J‹ΙŠιΟ8₯ΗΨ^ΰ„Δ`Ι…"8ϊFώWΆζ²žͺhΫy·§›σγ₯3Ύͺ<φόvΞςUΙΣΠ­*Ν›ζ©δυ.{2Χv™ίΤη]ΦΔΫyφΞc——ρ/½πΐ­₯U%ˆbœΙoΚ›#γνυράδ­­Ÿ±Κ±sηN&»ηhςΨ˚ΔcΣX·I©ϊθΝƒ)_uξγΗr{—ΩΨn€kΧnϊB`o‹‘iηΧΔŠφ4w²GΣF}δι3Κj·w'’w ΘΜ[lφts~"z§šfxΥ6§~>ΝυξΝ\•CιδωTψf]UUCΊ'βƒuhzε•žWyΓgM8nαάΧ—Ά‡o›,©ΰ!oZΦx8Ϊn^“Ώ‡u”ώΝjΛzμϋ…΅²ό†.»ϋ3h»™‰ηξ†πeΕγΧΚ‘—υΟi/ςφ±χš՝ouΞέΣ“Gš  Κ $’".ž““ύ,{± ΖϋžΧ^c―YƒY/`lΐkφzρ#'ΐƒ ƒE”5šΡΜhςτtOητuΏίοTWO«™‘ A·fξwS…SηήώκύΟ©SGqΚθ? Σd0T}ό• ΄κ/Ν²:»ϊkά_yn˜ζ\pZΚΔηƒτ%ŽΦ$Nο>Sλy†‡,O₯eγ+'(5ι{Υ pΐ^ΠOͺΖzŽΠCΠΨγ£€œ„/XΛφΐ.5AΦ§ρώ=©]/ΗR29v˜_} ιI›VAW+ @ί\N=-n˜ŸjLjf,@¬ΐ4Νΰ“5ΐΣ/L\SΗμ‚f@ΡΊξ٘a3ΑύριŽtξϊŽtίΜƒ}υι@lΨώQόΡκΣςΥ+S qk:mxΆbζ\ΦΫM( c£Έ T¦V6AW+Qg˜A&τρΧ‰~Ύ0υ²SslW<ύΉ·!‡>€uŒϋsησε·Ι«ε/ιΒqo]s]θ¨Kϋ:&ΣΚ¦c©oΠΨhj"S#τ2•¬fιΉ NΣoλHu€RΈ;H6fIΖκ°Σ‚ZϊΫ\€4ΔΔ&J–//ξq€]ŸΗ‘ZΛ―Ν‚;ξ…U­_ή‹|±œ=ξeNlξBδ©>* TX¬ψΞΕςχΈψώzξwιGp?jχW>ϋ*ΰ* λήφύώ’³ͺχ§GωοΜqοΗλSΙ^ώN-ν˜w2χγΥΎ΄άK_vότmΫh‘EŒqw}Δβδ©ο’½ύCτ‹ΔMflP₯ΩN3 ~a`©˜₯7Ξ,‘&€L§!,{6 ˆ¨Ψ5LœQ&όΠoΜE“γP΄οΐ^BEμ T‡_U ¦Ί&|Όa™¦§šS @₯av85L§ξΦΡ΄~ν4Λχ$λ'So3rs―½Ž¨χ>œΉB ΰUί ³€qΡΡI&NtržΑΉ^ΗΔzΒBπΔ¬X ω¦\„L₯­™Τ…γ_‡21Έ3ζJκ3πΎA&V`€ 5\ž†ΕͺuR¦‡ϊ]Τ―MΏ¦KΣ":΄ 4ˆ‰S8榣½kξΛ™½/Nω̎“8DΟ}yΗλ0Άς€_’s¨+Ώy΄—"³·ς/W―ε4ΚζΟΛυj_i ΐB άW_β?  kaΏύώ‰οδ…«γJχ“ξνίΩΒςέO’ήc΅K_~A<Še4[ΙlΙFΙLιο ΖIΨ¬ ΄ςΜGYΑAŒτ!’έΦO«>sυot|·VψqqcŒΰ„γ˜ WPΏ3|FYWqΗϋ‚¦Άw`Ζ£LgΧ2̜†‚`ΆO=Kϋ°@φΨΰ°\MiνςΩ΄vΕTΪ²‘>­Z Kύ©†«πΤ€\=Α-ιk…ωo¬ί).ΦZˆf^ω―•[ΜPάΗ²BΓLΐό «6R`¦ ͺ™Nσ‘‰ŽΤ™B6l˜1ο‚Yke©Ψ΅>’,Ύb¬nπEό―™|Χ:™E–f™¬―;λ…IU+΄ @ƒ!sψαό`„:BηIͺEzlΡ·¨Μ ΦΥ³`ln_ς•}ΉŸσδό^«R₯J•* T¨4pο4°$πe“ϊ!Ιb9eΊ0[:ά†Β;yWsˆSʌ$6! ΘjΑΤ&ΫeΉŽ ™`\FL•γ„9ΨΎcgjdmΨΈ™•\2Θ?ΔΚjΗ_ͺ‹XKΜ°θfj΅‘ꧏ¦™αύ©~”΅Ο:&9›W€m›ΪYZhΠ4Κ’.±0•š˜ε8,8ΓaŸ €8Ά3P5ƒƒ}‡>Ymk`ζΞΪΆ C”zϊΫ8˜–­<35v/OυGvRΡιD“©Vͺ6ΐžtœp™Θ{kΥCό―d48k/ nx_}e˜xNu¦Ά\}w,·4Cΐ™ Ο±dPCψ`a²„YCq€EX2ΐaΐ‘h+›:²>Ό ˜ΤtδZp +ηΥΎ@₯J•* Tx`4°$πώaai Ξ‘ !  %%ΠύΘ~ή““Ρr¬ΫεTΦ„° ΦJS€ζ=˜ Ε \jηf"9σκ”υιgžŽψ“˜ιΰ―F;D6žHƒGXXϋΰ45Δ φ«šΣγΉ>­]u4υφ0“±Λ,šΥq~Ά)Φ·Γn΅€Ž»Ά[¬}ΞτDλKΝΛa:W§Ϊ(Ž©ƒ΅ϋ1G2a`%¦ΝNΪc†β²΅i˜Ÿ]Ν+Σ2d›b–γ8SΫ»WΣN_šθί «5ΒΔΕz̑膨φ=°q̜]ψ—υ©/ςΟ7Ρ?Μ¦L_ΧΤ ˆ› ΐg6ύατ9“-4">JqΕόΔ ¬’κά­=Ξƒ―(ΰ•’$y°ΝƒΈΗΞ‡9—<.fΜ|I£λ|Φ’­ΪW¨4Pi @₯JχBK_Η²Vϊ ˜πƒ2ԁT51ςΐϋ^-Ύ^2]:Σ»Ÿ$3ϋz πΉrΥͺπχ:rδ0έ€Υ‚ύq©‘ƒ‡X@χΜ³Ρ–ΎώεΟ§υOKλ7$oΈα¦tΰP:ΐ2.£ύϋ_ˆ?ΆΪ™žpΑϊtΡ#V§εΛdΖU0VψbQ%OΣΰ*B=¬O퀫q@Ξl=KŸ`bll_“¦΄74Έ‡°†„@5D²˜ž Mb"HœΖϋvΐt$υ]žfƐ¬ΝX#ύ(lV=Ρ{ˆRL02ΐ‹¦MP¦ Πׁ_[G;¬ϋϊ_Gά―£}»3"‡YeΥ`κYNκY½ΐft}ω€±Π-Ι…Η}5ΐ™2'Ωπ%ž‡S"§@OτSΕ;sP¦δsο6—Υ[qί+1IΒBUͺ4Pi @₯J•ξ ,|9,Êΰ—4Cά+™/AΦ(‘ލl?#6J„kƒ―/g0.Αh흍°Mψl­]».uv³ΐ4ω .κν&Ψ¨.œψξl„`cη€ιXΰϊ‹$_ŸΊζΊτΥ―~{žΓ©~jθPZ‰OΥsž²)=λ [X:’…AYμΪp˜ωŒΡθažRγΚΤΌκΒΤΨ³™k̊œ> :#ζW/˜P[GwG Π8|= ιίΈ SjΖLι,†Τ Ϋ½3€Μ~Z„sDvX±ζφ4=I/fG 'A”_'XΎŽό-˜ λˆ7VΗΪ Λې‹PυΗ€΄›wmO7μ32Ÿ’.|ςs¦mgI‚ασ…)ΐ₯¦ΨZΈ–²ΘΔ]>ΈΦ΁iγXφ τ$Ϋ,™ŠCl¬`€0t-―₯o9ι W('+ω]Κh )+Y₯J•* T¨4PiΰΎΡΐΐγτ Λ¨  Ηz—ͺ‘Υ2Ψͺ‘dNτ‘2°j ,N3q»Z§Π<v"Ψ@ƒΧb Έw΄ΜdΝϊY?­£³+m<ύ,Φ€;ώωŸ9]Γυi ΗϊfLuγSΓDmI—\Έ*=‘mλ)8Υwΐx5LΊΔŒΕaj5Μ{-8΄Q›ψ_z|5ΥΖa̎2c‘‰Φ?‹3ΛθAX.C:Ha˜™˜εΝ1Ύf_MΘ;+ "aU ¨‹cOΘ.0"rΆBύ°4Ξ挰…0Uθ₯·3ΡΆ–FΒdyd`8]σ΅Ο¦ν·νLΟxήΟ₯Η<υݚΪ1ώΖmu¬ki“.=Το‹NΜ%“G~žΨΝ›ιΒ]ŽΝGβrΏ|‚6ΨάΥn†ύπΚ>Φœ<7χΥ"Uͺ4Pi @₯J•Ύ4EνάΉ“ΩG:Ή §v°¬ˆKΰnΒΨ^Ω S뀑œΰLVF³£k#zμŒIύ»Ζ'XžV§ ϊ –š"Ψ –ςpi˜ƒ‡ΐ~αP―Wήλυ,@]Ηz[gmκIOzΜΪtφβŒ5ΓΉt°$x’½Y̎^k ΔD]έ@š₯άμ‹ˆNξK“‡}8βΓTˆi²ΡΕ΄‘ΪκpΌ7ϊ„Μž8gPS#ΖΧ4ΑU κΌΔπΗ²?ω7ΰ$V˜™Ή7‹_Y#L—Αb±n†ιpZdΙΦ¨Ω‘ M˜,Χ°φΦΉz‰_ŸφLΧ_y9λυΊSžώL˜AΦ<£E z%ξ ΞΠ,AΩΡΜdY·›`kN`™eq˜Ί)ω γ᳈8lτΗkΗ49Χ―Ξ+ T¨4Pi @₯₯i`IΰˁέHφF―—₯Ѝφ‚.le±^ ƒϊ\šg]800r{δ‚άψ‚4ΛiΎOΈ QΰμHί‘0©e±ΨΓψ€MŒ dΖX*¨ΐpΦ©-ιΒ3V€ΣΦ΄°&Ζ?ƒ—Š?œ2hœ,A{Χ2 ι,α!¦†wΰOΨΐ\YODϋϊΔ‚·š)ΐF=¦½Ά©Q¦B3³nδΩQόa›ˆγΐluškΗ¬Ι ©˜6]7KΔcl.Ω­1ΒJ(K£¬fΉ?‹υα€f@θŸϊ4fJβˆΨΪΡ€o‹~2Λv9Ν/υ¬ξbΟρ|ςπψ㘻Œώι‡Ω4 [ΖINŠπyΈ™|±‘βq·ϊ¨4Pi @₯J•–’₯/ΐΣ©§ž’&πρr]AA€ΐ*ΐ•f6ŽMΈ/χdΐ0ϊy]Ÿ°Έ%¨ƒϋ.K΄ΌwEΊεΦ[πΙ:šφ3³θνβFΎf+¨γ1g¬I8m1΅π©jdf νΞ¬ζš5Pd€ϊ:–0šH3:ύ;“ΐ₯†oΰ~σιθα±t¬ŸUΫ&α£#˜<œ?FXͺhŒΕ<Η)ο"ΦϊW΅Γ€-'j/φ½ΛZYx»5υβγΥΩήΘŽ˜Scup™Ž[«)_ˏ"/ϊŒΨl˜‹½ΞB- Λ:ΔQχ’2Υi₯J•* T¨4pο4°$πe“ί8;mŒ ’ψλ@Gώ²&r7x}’›‰\Ο±3$½¦Ή² ³₯ΜΞ ŽοL7έts:xπΨv­1Φ>Μ΄bώΫάۜΆΑθιε4Ρv ViπΥΜBΥΑ€YΠM¦ œΥδΊ‡2<0Z“˜όΦΑ##€»Ρ΄sχpΪ½w$ν=0†³?ρΛΖe˜!e»fIQβΐ5Σu±οD–žήz’ζΜ•0λWw¦υk:XΊ¨-­”ufBμ#X&Γ`€Ξ)ꛀIΑ¬κ΅)‘‹oχv6₯5Λ;Συwφ§IfFξάqKϊΗΟόSκ^Ή"=ϊQ ²$1ΙfπΑ’Ξγ)#¬θη1—ηNζA©}‚ν#`GθΎ„ ±Ύ’=ΐ—Z¨R₯J•* TΈo5°dπ%¬Π‘>|ΎbTΟΐ*Ηυς833šγdΗ4a8 ˆ r\*Α—¦Θ}ϋφ₯ο]yeΊόςo`φ›H§m> §Αt睇 I1θcΆ!vΔ3π“ZΓϊ‰0@ΝψYΥp¬Ÿ*ξ ubAΪpͺ$)5­)’γλ›J{`ΈΏc ]{γΑtێ‘΄χπt:JΘV‡Yq–(σΦ ‰ΠΦNσ΄5ƒsύπιcm_±ώρΤ~λXZΫu,m\ݜ6―mOηm^–NƒΙ:΅—Ω—°dDΌH­κΨ ί‘ΈaŠœ†‰›ΔۈμΛ™uΉvy_ΪνdƒžιΖk―M_όΣϊυλXi >h„ z+ ²c_ΐνόήΛ‘Εό3©Ž|$•ύγ,˜AŸ‘€Ω»²i‘[ ΣόΑάy΅«4Pi @₯J•–¬%ƒ―ΊμH%žqΈœΕηfΚ°ε8(ΐ*9ΘC‘IF1ΘΟ1_Œφšΐ<ψήχWΏς%œμ—₯G]pA:οάsλ?^sυUι–›n ¦l¬Χ©Λ2Μ~¬‰(P!.V3Ρδ[ρ;`ŽP2ζ 31Λ¬Εα!fοšHΧlοO—ίx(έ,Ϋ΅–K)υ§παΒ,€W™‹[K†Ι3ϊˆω §.T«b@ΩΠδύρ4Ζ’E;ρΫwΗxΊyΐnΗ`:ο΄ΞτΘΝ½i Œ˜ έJδm‘5X'\ΛHŽ0[Yšhjx<­Z·.]zρσΣ­G&ηΎώνΤΧwCϊΞ·Ύ‘žτΈG₯u+žˆ£+I2•S@«F3+()ŸΕ˜ϋΘϊ.ΰΚτ}ηΈ|mNaρLτI³ά<σ•ΡΨΒj«γJ•* T¨4Pi`‰Xψbδ6~V=a4;fͺDΈΕ9 ΣyΜόcη δ’™Α|ή')Žξ1Σ vλφΫuΧ]•„‹]gγh^—V.gαξφιτ +Σ5§τ¦λύ~Ϊ‚YoE7 ΘͺΪW§ϊ6‚™ή ~ΐH(£Έi”½‚Yͺ–bJΰ5ά (K—]s }υκ½ιΦDν’~¬!§`pVkš*g0ΞΞ^ΑΎaͺ€ž ‰©ΉΖ}˜'ύQ”‘MdiρρŸE†R?LΪξώcι¦;GΣ#ρK»pλͺtζ©–Ε'¬“c3e›)d{„, Ηχ1Bu¬άΈ9υΨKVb’­=σ’τιΟ~2}λΫ_I_ϋΏ€Σ7ž’zΞ~m6±ρθ(/Σθl’Šί–Κ@eŸΌΜRΆcΝg2ŸWf"eσ$‡θV€,f©-?±yV.WϋJ•* T¨4Pi`ΙXψ²9FθπρZΎŠŽα2]ˆΕu™š<–^ΌX!:ώ ΰΖ™v]„Txδ# mo\Ώ>Ξ±£Gπ‘ΒΤ@ZΏvejΉθ‘iύͺξ΄b–΅gˆˆίΤ–Z—©π3˜Ύdu˜Ρ°ς₯™ILyΌκqžŸIΧάx8ύΫϋ7·M‡1ρMΞ&)#Q€,―uΔ!KPεuX‘T@Ž~k²a&ΥΩΜ(@›¦ŸυΚš›˜˜NC{'ΐΑ\‡&Σ!"θŸChŒ+ZS±Ηšpβohΐt‰<šλ™αΩΩ»6Ν„65v€Η>φβ΄n]oΪ°nYϊΔ‡?•žϊ€K§_„=Λ”#:ΔζMiBv@—>w1[‘c;¨™Ψ Ι£/>;;]€ΥβύάM« `ΥG₯J•* T¨4p―5°dπ¬Š,φ1ΐ/”&Μ]sςΐnΖ@0|Ζ¨3ψj°J£τμ lΕ)§œυ/l Sγˆq­¦‰œOΧφ–΄ŽˆψέμλŽή‘κJ-έ­©yYO±όΑG[ Ž θšavβΘ0‘·κZΑ‘ZϊώυGWΎ…/Ω-#ipζ\AAd³’`D†HιζWHΧ2±#s}Λo<,Λ#υΏ²„¬’σgˆ^―Yv–),žφ β\γ±t””Γ“irΫΚ΄©§™Έdΐ!&.L`zΓτΨΠ‘Ί1·jV4DΫΆlM/zΑ X<Ό%⟍³\Qgg{ίΜ.Ϊ>vΐYφ*P6يΑJ"₯Zw‹PΚH^ΛΔsqούrΞρ₯xΘ?tυ§ϊBθ†.|οΛίίυί—²VuU¨4Pi ΐ}«{Ύ²< τŽω‹S΄d<^œG Πdθt?σӊYΜ08Ϊ "¬{ 05`šœΔtΦΚ’΄969žΪX:¨₯Σcƒp§‘ε„:S+ΐdΊ Ν°,Π$ΛόμžHί½υhϊΒ·χ₯lg)΄υNhcœYΆγX<(ήUξ Τr_;lΔΛπ†Cn˜?jCΞ™†V Χ 3ΗΣb”MοŸLυ‡Σlx£Ύ`νi l φkŒ ±‰ε‰ΪΊz’οΦ"#8Λ"ά‚Ώψ₯°‚=ΜeQ%τeΒ&εxΉ/½ΘP2 “»Š/α$La^₯|)ω(ύ.{uοfr"ΔC!“ͺΟ¬Ό_χ‡\>Γϋ³~eΎ?λwN,ΦΣΙί:¬κ¬4Pi @₯“k`ΙΰΛ™qy&ca΄2ToΏψ=.{E(Ηά ?%cwΝjΟcpιΎ΅΅5r<kΜ ±%`ΐ ˜:€3Έ £@£8ΨΟΰh/F˜œMαCΛΤ4ӘFAZίΏώ@ϊάwχ²nβd!˜Μ₯©³X€ …[””΅€…}*Χυ-³‚lΘ³’ ΑμO~λ‹EKψ”Έ΅Ύ5M͎ΡGΜ£‘e’Ÿ%’c³«¦ΖX8–Jš%ά¦Ζ–rMΕfΝφ֊€NzSsk'ΐ“v4šh'¨<εc―ε@©9ŒGΎ'Ψ Πσ ˜€jŽχR@“Ÿ‘}4ž™[]gΟ<ˍ>xŸ ζηwλΎ’Fέ ξμ»ν ΓΌ c–ι©χUQνΨΖύUΏXΏ›©΄ηρΒλžW©@₯J• ,™ΰλ}ώ ήc“_τ SΌΆψΈQ/B5XVί€€I„ΠLΎ„XώΦ™λΝ Α­Ν΄γ~Α„5 «sγ8‹TλƒέšlHύD₯Ώκ–£ιWLΧή1‘ϊ‰1Z£Ζ"ΥΏΟb.Α`U¨…ς.”?ί?ήKΗ8g‚ cS‘α_e΄6£{MΝΤ³’dsΕ9θΫqt:]·k(ύ`7ω{ΣΡ±β‰1‹±₯°F,3ΐh&'@“ΦΐlNύΗFF'0if]{έ΄Pυ$€ … œ*¨uσx‚Ι “sAUhΑάQ‡ Z§}—…2~[s³›+4aξ%’Ω >HφιΖoJW^yU:Δjφ{αϋvό=ρω–wζΈ° ―•cίƒΒ¦]ύ ι5―ωΟιškBο{ίί¦'<αΉιΆΫΆΗωρ2ΉnΟ'―Yίβ{ε™Έ/rούώtρΕ?zύ₯}Ϋ\xΌP†ξΎ}ϋΣχΏodΎ=σνΩsgθο벚J=ξγGΝ’>•ϋ σFΑeΛyΩ—2ξ«Ti @₯J'Φΐ’ΑΧΒκJ%ε/έόλ>ƒγ §δρ‹ήΑ΄ΰΚ?t˜ΒυΫZ ₯ΰ90AΞ²ΗL7νŒCš‘k"žΧYΰ€`΄χ‘ΑTg±οΙ4ψΊ‘†Ÿ»|OΊζŽΡD ώ4J؈‰™Vό«ΪΓΙK`΄!³&°(r–cχεΈΘ\φ%o°Oασ–ΛKF!5ΐ °„˜Λ5ŽΎ ¨Κ;ƒΧ¬¬,Ψ0l^oή;œΌυpΪKdύΙ†ξΤ΅ŠΌ„Ξ ži̐5f?"  €kΑ―Mfp‚λ5”‘g2*Ydκ^ΊeΏπx„'ΜΩ₯―,+ —ώΆ?­@ۊΜm.„ΞyΉMΏ΄\½ =ΰ© β»vνJω?Nzνk;}ς“rd “E*ΟΕ³…Η₯όΒkεΨ~•ϋ2Qϋ·οMLj―f:οΌs―ϊ«Roο²87ίρrω.eΛήϋ ߛ썿ς+― πheζ³ώΧΎφG«Ώ”±άβγΈ°θγ»ί½"=ζ1²BΒ'ησ{π•―\–.ΊθΒtΗ»βzƒYξ»φΙ ^_άζΒΎ–{Qή+eΚ½’Ώδ©φ•* T¨4ΐœΊ{£„ςek¨_΄ω šAΔ2k5M\.wnnΡ‚}ΡΗΘ ©Ν˜!=%xΈζμΘY|¬fˆνU›ΐ!Σžυψx± 5ώ^˜δŽΰ`ωΝ}ιŠΫFSΈeœ˜\3 mΐЁ5,σP% u–Αβ}8>Θe`Sΐ₯5(§yΡ–Wήϊ0Ÿš*ΐF™ +!{§ UX¦ω΄φKP4ˆΫSipd&-[5•V―Y‘Ϋ—ƒΰ`η™Τ';Ψ€Ώ›ρ0\"il  ΙLΚ©i|ΪJϋyeγΟΐAΥ •²)sH=χ|ς€Z¦ŽωΆ£œnεΊύ’ 3ΗvΊα†ŸMιτΣΟ…ΊŽψg}iŊΘ–`Yκπ‘ΤέέΕΦM@ή;ιGΎrλBvίKΩ2ΩΎ΅kΧ¦~όϊΉJ˜Ίν›ύŽgDM‚—sΞ9;-[–Α—χhXκΚϊ֏­³³#ŽΥΰφΞ;χςŒΖ’ΜΊuk£Nσξέ»/ύέίύuzΕ+^’Ξ8γ €yc€ 3Ο<σ.υ²fιώύζκ_›:XΫΣ$k©όmmmδοI»wο €½yσisr›λxς9Ύπ…/OŸω αIΆmM—<υ)qSŸΚΗ<ζ’ω2>ϋ‘Ϋά؟NkV―lφF^Cΐ¨SΫπ ΨΤχrBΏΨΧ]»vΗϋ²iΣΖω†ΥΓΰΰ`θHζΤ%ΘdOΥΑCαšt@IDAT΄:¨4Pi ΐƒ¬₯ƒ/=†ρŸοΦJ~αšΚΎd(_Β„‚³Ή0΅‹lOL•`Œv_c -΄ΘΖak¦ °?²V­­©q²+MβΔUbqlΣ'qŸ©oIΧν>Ύ€Ή±―ΖlΆ3c °ΰƒλ{#ŒO#Χpš a‘‘φMϊd)—2Ί…i.ϊθ}ϋ*X„]rv£ƒ52McΗ€ŠXώH¦Θl¬—~[τ*5ΣQu2£PPcpƏ«E΄ΫWu¦½GFSηr`a3ƒ9ΐr 9?†1γ±%ΌΆΆO?­…ΊuΣμώq bM™Υπ<χΕkΡ/Ϊ·.Gi`·ΊΜg*:¨€yό`%۞€ωΌμ²O\πˆtώωHΏχ{oM·ήΊ³ΰŠkǎιWυάΌ0ϋί‰tξΉΫ ^π₯WΌςε**ύ韾+@ΜΕ?>ύΕ_ό  ’‹Π&η¦Χ½ξ5αΧ₯Ύ|ΉJW?τ‘¦/|α+ι/ςΎmaϋ?ηKίϋή΅ρ̟όδΗΐŒύZ:λ¬3\½ϋέοYϊ& δ0!BΞI―|εKη₯ιρΣι-o}WϊίψMκz¬έ?€wΏϋΟΗ>φΙτιO>ύΥ_½3κΧμ©)ς;ίΉ `5“GPέWΏϊτ¨G]Klύ—ςzδ=g֐>ϊΡJ§m:5ύά ž—~ι—~qL•gδ;!P_³fUϊ«χόMΪΌε΄΄qγΖMccψΞ½#W^y5²Ό7}λ[W«"€ΩyτιWŸxqΊύφθζ·ΡαsYuβ@τν"B½όβ/Ύ*}υ«_OC†Ν[6 χKΣΟώμσ’ιoϋ»ι=΄χο\ ˆλL?σ3O­Y³&Ϊ|0ί£’›j_i @₯‡‚2κXŠ$|YFeIC3εΛ@gσΦ•`f cx$€Ώ›c@ΚfGςbškl“%κHcCD—?6μν{ϊΣ·oΪ—v±6f³ρ¦V) VΠx4΅φ¨Ψ`m=˜7;0kΆ„S#ŒT1u:ε±iΚY ŸωϋΪ2.ŽEΕΠνcxmv£ΨTG@ΥϊιΤΖΒίΈG@$E5C6αΜή΅<5w―zflθuΐγ²?Αy.βbSΖόΐh„z'ZŸO<•}fΈšΌ›²oΰK.ύΌ ψ*…#E0gΘΰ⬐rϋΫg@”`zv§ψϋ/ ž(y,¬Σ @Πχγ}Qσ­Z΅<}ωΛ—±_‰ωπΟb9¦7Ώωι꫞˜υiηΞέα υΞwώΟτœη<3}ρ‹—Ν›ζ2σuΌkΦ)+$s£ΤΫήφ˜ Ύτφ·azΗ;ώ`π;ιΏύ·7λφνo'νΪ}gϊγ?~SϊϊאtNϊ³?{o˜'=ιβτκ_~&Η[ΣσŸ\Ν―X4,K&ζΔ[ίϊ§°K{[ήςίŠo†ι:”ώθήžŽν¦LΦκΚ+― 6ιοxsZI?ωΙ’ΫCθ’+Oό»°žyζΡΦίόΝϋcζ¬Z†‡ΗβšωώιŸ>ϊϊψΗwϊΘGήεήώ’O=οpww'Λ|}0yΰρΥΑΎ½ιMoE·§(ŸΩ·­ιSŸϊ§”¬ŸΩοώξΣ† §€ύΧO€w½λ-θβ[ιΓώ˜MQ7BU©@₯J•BKfΎx Tψw²T~aηύρ/ίrέ/dƒ¬:p8Ύ—€‘/χ)"±^Αeu`ŽˆίUψΗχΙhνT=ΐbΩί»qΊGφ¦nœλgX+’5|dΨdž_Έ‘γΧΤBΈ†φ4YO ¨ΞmΣ šνt' hΡ―κ8Ζ½,Ÿl>)#ιOΞyς7λ‡0?q#€_U[‘­μΕLuwvu¦nLZυ€Α¦–ξΤΪ±Œ˜±~Ε¬ŠΜ₯!ΩAΤC=0P^FQ@Q6χΚ}|;.ωθ„² Χ «ε@_ΟμKA\³&PWϊO±Τe•Α†1 ӈWπ€l¦λ―Ώ>½ΣNΫƒ³ΖλBΨšο¦—ΌδE±΅‘Gης‹/~Lϊ…_xUŸλη?΅΄}ϋνΑ : gςŸώΣ₯ΑVmΫΆ-Μ—7ήxs0BPSιͺm«[Α—&Άϋ·+Σ{ίϋ‡ιιOZδϋΰίνš_όβrύ©˜ά†Β(lό=z4mέΊ%=φq₯ΧύΖkUφξόΘ“Ÿ›ŒisΪyΗιk_»°υΫι™Ο|FΤ―ωρKί »#Ωo™¬M›Φ§—ΏόeρƒαΆΫnO?ψΑΗYχt8ς/όπyυ«ΧίxVϊρ5ιяΎ€ΗΙ2Vψτεχ9₯7½ι !£k¨±˜½&E}ήFGG‚a„½zfzΡ‹^τ²ΛΎŽ «^υŠΠρ7Ώω-tsgΘ¦œ»v€νΫϊ΄_›7o0^f^}ημsy¦ 孎+ T¨4πpΣΐΑ_δώ 8β)™°T’_΄ S9Ο{ξy?ΚQ‹ƒ< G|οϋΕm Qd  BΕ|‘cbδ^}€/|žš{Rί±Γ8λή’ΆοοO7ξHc8%$C α&š \*¨’nY©YΐO^―1ϋPe2θ FŸΘΜ†`ώ§έψΗώ‡“Hˆ t"ϋΰ€Ρ ¬UΝΕ«½w–^lŒ|©£ _š|i&YΚΘ­₯­3ΕΞΆ¦Ω2ΆͺHH›…x‚IξΨ¦η$uV6εΉ‘ΩAΨ̏ΐ+ƒ/@—°ανά΅xŠ”Μιxo£Αθηά­lgμ‡μΠε—Φλότ‘}?¨φ=‡I7έtσ<ψ V¨€¨•+Wΰ“Υ`B–Ιη»rεr˜&5žΛ6Žgƒ>jΑŠΙ²CC·Βψ¬sλΣX’ξ2FΦ΅|ω²pWNε7 nLΖm3•ηf½&λΫΏz€eίk6¬,ξ˜j™ΙΤ9AΕ$θά·οθ|qqώΓ<£ρόŸϋܟIΏυ[―ΒΜω©¨γΖuσνϊӟMŸύμΏΐhυQίκ¨Λw&Λg8ŒΡ΄zυΚ¨Uͺο—~gφK™υIσ9y¬?άΆmλΣΝ7ίγυΝ­›7oŠΌ>Γ2qa^Δκ @₯Jc d+Φ ;S"¬[Ό 4ϋ…\ Ή];Ήy#―Ξθ²ϊΆdSμLΎLFΓ§!–πiLc8Ρ785΅,KwΟ¦ΫΑbu¬H=,Ο33ƒ9 ωΪ‘ŠΪ  š1cΦ±Ή–γc45=Ιΰ†―’μEΆeτŸΊ‹ŸΣYΨ?†cFLR@’ —ΎŒœΐn  7ΣΧ¬\°aš υ…θF¬o$lDwWWκΕ€ΣڔΛa:‰ξί‰£8H33i °arD!˜ΠM]ςκt.mδUu‘ήB—ξ‹Ήλ LPέ”ΣI e-HΛ©Χ(OYϋh>χΗ·%Ώ"σ,ε Θ΄sη0_7γ²bΕςp_·nM€œο~χ{Q΅2wmǎΦΌΈgΟ^Σh€¬θcώƒ\ίe4“8pˆrθΗω„β&φΏ9Ηƍηγgv[άφϊ7Ώyyϊ—ΎœŽιKŸψΔίΗώ=οΡ—λ8Σ?šΊPG֝uaŒž_@Ε;ξκίΊυΡσa-lDŸΆ5kΞ‰{/eΌgRήΦΦ“vκκκφ”υ΅―ύ•xΖ²[ΦΥΒlV'όɟόeLJψΔ'>˜ήχΎw@ςo0Δ₯ '½δηqό]Q―y_ύφττΐΆ}–νy˜r?Pώ?ψ{ύjψ­­^oΦƒ%«Ti @₯‡·Nών}z‰/^Ύ|³±ζΔ™σvώ’f8›Οΐ² σσΐ\"γQωžΑH&0rΠ1E °$x™ε‹^^hΖkε†­iˆΕ²wν=–†κ{Σ©ΜμͺJcwμ$τΔόΌ„.iŠ2“3Υωάχ‚'͘Α¦1ΛΛ$Λ ψ k#`Ι$2ϋ šδ4ύY?l ²š9―ΡF  @—_=1³t3<δnΕ΄ΈcωZ€"³σΦt/g‘πε©δ4&G‡kΩ4[pΣ§,dkQqmgMrφL~†£½n!sQv@ωοςψ œAi*χKrSΝά½ΘρΐYŒ»υΕ/^‰μΏan||ίߟ^ϊίOώο—Ηΰ.HPμΨ±+ύωŸΏ;L}ŸόafΪΩgŸ€R“cooO8„?ϋΩΟLΧ^ϋόΏ~@ψŠWΗύi&u˜ Ϋ%ρύs&ε–-›Σ³žυ€Υί2Ι€—½μ7 ρόIπaύΫ·ίΞΒπΧa’ό  ―›Ί`;IΎ_«VmΓΑώsα‹υ΄§]Οt€nύ[·n₯ώ'R?ΒΖ­Œχζ#ω$&Ώ'Α&m VΙϊ•©$(μάydξ½-Wσήφ††n›ζšXνΧ~³ηŸσΖ;9†žϊq†ε 7άϊΨΎ}gτΟΧΚwξθΡ‘½Τ>Ι$‘yx§όϋ)ΧΆnέ‚Ό?Ηd€OsνlΙΏώλχγwΆgχί£ λ₯X•* T¨4π°Χΐ’i Vς}"-ζϋΗοd?“ςΛ™½ίΖε‹ή½ ƒŒo*w©0€ΰhήπŠQγGUcM©φk¦g}oΔΏu%f’i5ΏΒΫ`·ZVΊλ;3qǝfˆ[U3|  πξέz&·r-ΜρnΔ‘γ‡ycMGΪ ³)ΧκŒε+7[Ό8ϋ3sq†γ­Ω:esk7H«Φ¬M«Vk[†Ήg μΓژdΰ ˆ–ϋΐΐy·h%#GΔ20S+¦ΌWΎΨψpo2Ÿƒ²πλqœ£ΧΜ}€†x0Q°Tυ<Κͺώ5κ“υ›ΏωσΜ<}ΎiΓ!<ιIO ‘?–ΜΧΑƒGΈφ„9ςΏΩ[ίϊί‰§uξ`˜εέX•žϋάηΰώoαΥ―~~c/Œzx§.ΌπΙαOε±3ΟάzΣΤφ{Ώχ_Σ%—<VηŸΒ1ώ7ίψϋΑZ:«Q¬?ϋ³wc ½%œω5iκΰnR†7ύαƒOΨ1€δ—Bξ.ΨΟ3ΟάυλLϊΧVzφ³ŸNύ“ϊ'’­7ΌαυQΏΟM_+Γh”'ͺ π9ΟΉ0X§hdΑ‡ν>ύιϝΏ§>ŸρŒ§§—Ύτ„‹θ @΅fΝjΜ‘―‹I:φϋΞ_|ρc|ωnΛʝώ±Π½Uϋ<4{z"Ξωˆj* ψG0τG‚.ώ*f¦ώΪ―ύJ€±όL£hυQi @₯‡½ oeΌTΤ|竟(σ₯Ionˆ·¦Β"-¬Υ&J3^ˆΥYΖπšb°―ƒ%ŠYƒ 2=,£Σ#488˜ϊφΑvM€iBP —©“›‹JSˆρυδmK«ΊΎέ‡eίΌ%νλ›L½ .;wܜϊφά°˜MΣ5"Μ³d°^5LpψL‰:kΘF h^“,†‘@OSd^’gNώΘΒζΜϊFΚ O¦¨4‘Β– ² €–&Dςq«ΰ¨γ¬Χ[Ά₯-›OMm„Ρΐ){MΟjβ+B—˜Iίλ»ΊΣl3ΐ­gDδo`@#²5Sm Λ)Υ7aζGNβGσύο}/˜“›oΎ9τΥΞ‚δ#–«Ή&¨5/Ϊ-μΓωž½:Σc 3Έ52Ψ-W£l$™97GC:rψPΊθ1gfέ§b %=@ΎKμΚ&Ί0ω|4 BτύzΝk~“° ―L—^ϊ(rdwυa2ίώααΫt'τ· ΡAΏ°Xζ©Ρ–Ο»€oΫ΅ύl²ΜΏQ<Χ!έχΧψ_‚Ύ’τλŒ-¨ς]—5Sζ…y2“«^aoOPΏεœYι߈²•²^· υp’ςE–xwΡO‘ές–ΝrM!—“.²μκA°₯JΚm^ΟΝg=εά6Κ³P&―{οχlΫe™¬S•vΛύ"c΅―4Pi ΐΓYK6; ˜τs5Εΰν—pΩT¨cΉΙk³›Ώο5ΏΤ5[˜β š½_Ψˆ`ΐŒ™εŒ1^ΝpΜMξ1 Z–Ό³ n΅1fnu/KνΛκΣΆ₯ή½‡ΣΎΔf©‘³7ήΜ8ΰSKKsΰ•…:Ι6².ˆΞ”»εΆ[‚kΔάJX4! ΦΆθη4ώs3“‚Y %@― ˜:³]Y/κΡά58: “LΉTvϋϋύο="°+dy θό‘d¦Œ‚_ςxΏδ‰‹wσQς-¬»\s―ή|BεΪΒͺNtmα}KžΥΏ8ο}uξkΌ°½{[οƒΡ‡{+sUΎ@₯J†– Ύ~aΛόΒ2ΒΑXƒΔΘa˜JnϊΩΈ΄Λ/uΝ›p:ε;˜ζ₯‰2X#"y:XΪ_φΖ)2@fkL‹ΟˆI‚ QA‹ΘΖr6/0©kΜΜ‘λzΡRβΒTNΞ”€YGΙ`—f˜ΡHeŸΈ&cΦh y  `β*ΰFΰKΘ±μ‚ηeˍQnΤ…2δINεH0˜υΰZŒ­.Μ“΄/ROgœ~:±Γš™qw§ιΎ4„―‘ύP ΞtIRχ$¬_8M‡―k<²žf3LέΨθ8μ#kN…?tP„xνΥ‘I_¨ /|τΌdεzΉΰωΩgŸUNcΏ8Ο]nήΝΙΙΚeQ²ζΟ”R΄aΞ²Oavœ£Ά”CΰdΒ`­£η±@*μ€4σΔΦ‚ΣτTS΄•­;Pmy¬γ~φυrΐ<ΈΑ¬κyξ[fΑr£κR"8p‹Tφs§μΤ»ϊhλθΔΑ~<-_±2]τΨΗ₯S_£­«Ί2έrλΝDσgqcΩ?šΊrmΏ œΓ‡‰5ˆ³υ‹Lχγ%@SŸ³¬PW?‘ΖΙΧαδΫ[τγβCθΓgz2ΠUΔ¬@WΡD΅―4Pi @₯[χ |LψΕƒuWwF ΗφΘΪ £…Μ€€M˜t΄/€%–/ ΛΨY†_Τ422L°Λ#i“#vΔΟAٲŜιq€/}œόvh·οe’JζI–$ΛeYεΠεήΐ­„Ž\‰·c²u=ΧμΦWJ†«™ϊzYζPή'&Η£—MŠφȞ“Ύ]™‘fΖQ1σΘzΙVΝ¦Q|ΌΐSΒY:xkV4@3ΧΒΌI esƒ~oγ؍\wOšYEΐYbͺ TcclΞ0=;†™²=ϊLšLη“‹:¨€ήΏOTΫU;•* TxΈi Œ™·~?Πύ]2ψŠQ<ΐ8Ό”Α1ω΄ππ²η"’KΐγΫΝ€“@Β±™3 |/θΙΐ(›5;NŒ³`qsGάkΐ DL‚emΦίI€ό$ψ1πŽρšC3kU3ψ¦3E*gΕ™œi(lτΤ&&qf§Β0Ρ1σΡΨϋΣ8ΆΟp,3Τΐl“2”Ω’ζ‹”U1§K˜u΅L€Μ†~†ΤkhΓtL@ΚΐQp5ŽΏΫ$ŽεΣΞκC0eC°` ™žΪd‘- &Ϋ‚Γ}8‘£[δW—2y£c£aͺά{η^"ρw*D€πo+'χσ^YnΈα–9}Ν)θ~n³ͺΎ@₯JK ψΛΰ˜tξΉg9π°ΤΓΤι{ΎxP‚ˆCPσ—MpΑ©Γ#jρΙsΔƒwΠ5ξ’ΐFζΛ—!ƒ͈‚&Ν€sŒ~^ζ—ωimk)ω:Ϊ»^’ ΒpΩοj.fy3λfΫ6™ϋ ‰0―y˜AW ³-3£%Ψ‚Ιš.¬Wξƒ@Np₯9qv6Οœς(pRNΗΊ₯ΎZ“°LΞJTήμf>λTΞ.ΞB²9Ά‹n5„΄qM™³άQ7`PέΘV­dŠΏL–ύ²~AM±Ι\Ν=+€€ΐV'{―„©4t)°$ΌΕZqΊ_±’M”rqδeΛ–GœΖ5χD*οΚYgm νy•* T¨4PiΰΎΧ€γˆdΖΝ7ogŒ©Ύkο{ ί΅Ζ{Ύr€τφ WΊψ•ΑΣ½€£œ‹61’sμCχYΐ„€ β%¨PEβžΐ@3ωtT—ȌO₯ΞeΜξc‘jfξ bbΑjΗ‹ΉΠ:Šlξ½Ξό€‘Ύ *;ΨΓ«!|₯€€ZlΧ™—ΣkšΨΊ”Πε>δAgTΥΌλκ‰γΕ?M’φΣ†‚Α—}P@ΗhΣΆUD–Ο²tppΝΔΪ‘Ήί(λTW–f¦KyaϊΏζڈ1fΝώώ£ΌζΧ¬ŒVŠ\^FΩ­ π h€Ρ!2ζ¨srΠhˆΰ_O\²+ "e.Υ[•* T¨4PiΰώΣ@ώυgw•ξo ,|…`&Π‡€ΰξR>%ΕξB‘Θ} 8ζβ-ΈcvžΜW# G››ZΘμ³φFƒαΉ}ϋν ΚM©—₯MVΎ%‚\ζ‘5±Ι:ΔρϋψψcPΟfΌμ“%»d?ς D„Q$Οτ[0ΡΝΞΆΐ˜Χ‡Ζ£οš)¦zΐWπ #͏2iΝ͘χfF@Ÿΐ)ΣdŸΒμJ§›WoMš CΖ£R!N"P΅κkZΏ7Ω9@[°fΘ;91†Žš¬w Qξ?ϊ‘ΓX­ .υΐ―Θθ…r‘΅0~κΙγz˜RΥ―ϊgSYd k\ΖμΝlϋΰ/ΑνβχI«Ti @₯Jχ±όυ;·JχΏ– ΎΚ θ˜½pp,Ηeo1?tξυ@2–7Χ]“τ§εZZˆχ0™Ρ‡IFΏ)Γ]›ξ0Nα{χμM+Χc±κ5ψAuxXD›<ΑQ‡Η›€”#dlΑ2ε=ΪΧΦδJŒφ“ΡHLΞψFσθ `,_ό»Κq8ΫΟ9ν+§ Md£ͺkF5Ξ—qΉdš ~ 04Y—ςͺΛ5g+ό³(/Γ–νπκɢ٘ύi0ΪΖ`…2Sg\³ωμΏ@ΣΊυ§¦M7€ΞŽŽΤ ΥΩί_4Ω΄*c—ƒ­Ξλ<–YΣ΄ι?ύΖˆΆ/θ 3―ΒU©@₯J•* TΈΧXψ |’AL Ϊ3χ?%9Έ Όό—AΞܞψCφΘΊ)Ξzœ˜e…FΔ3Η«°6ΉΐςίϊzΪ»woΔΉ2?…`¨ζfιΑ"Ώ¨ >ςš@{h/ƒΫ|)e¨Μη’A‚'#f}ϊ{9sQΐ )Ψ¨‡ŒΎΤτ›šΑdW|Ό2CΦχGϋ*L₯‚°˜₯σt/,UΜp ₯IιhKݏٔθRAš'™Œ°§Ώ9–žώ¬§G€Ωέ»v₯+Ύύ} Ο¦f7._ΎΏ­^VXkΆΕlHύΤ2θSmv\ύlή•ΙŒΤΈ~aσšαV•* T¨4Pi @₯%k`iΰ‹ζ:e;Qλ„ζς-Ύ8Μn’Μ—!4ΩE|/ςΚ`FΑͺΔσZ‰‰­£kE„GΠ§ bv€¦C ό›€c|6? bτ₯ό)G]aΝζΕXt@yV†bΠχIΏ)Α“ ”α\žG–ΜΨY‚/gT6³fc6?  d¦„q²_dDΑ€@ͺοΘΡtpΩaΨή­‹θμmέΜΞlACSP/ω-fύ““Ωδg}φΑdύ²^DDe6εD:tpψ’Kwε9}λ–6;pl ν?°?Φ]“ΆnΫ²DυπαΓ€±ΔBλKW_uI„™θ…s[3ζR=2rϊ±ϊ΄ζٜœT©@₯J•* TΈ—X2ψ²]ΡB ΅€•ϋ χ›Šq―ͺ㰍›€λ` ZD/­°B±Ά#`‘₯Ή5΅d£ZΈΎmΫιi &P[½j5Ύ`k0­-K}GL2P09€λ  ΌC2bΨ$™Μλ?A’³¦ Uλε‡Σ”Σ”˜ΖΉ95ΣuμΧΡ>@˜Β³!f EύΌbέD‚œ °z77ndαλΣ¬|βi!ϋ ΐΥB˜+Ϋι“@¬±ί0δΣ„i첑‘‘θGC?>Y’_ΞΎ4†ΨΚξiΎ^γ˜!οΌsμ‡—ƒm†ΥjC'=iΝΪ΅ι¬³Ο 7xl Π:˜:k@ξΪ}²N c Tχ¦Sן@ΜY‘Ν-.FmίͺTi @₯J•* ά[ ,|θG+”³@gρ 8‹ ΗΕ,9ΰΆ„)ΉŒŽκd1iΤA&Mt“˜έŒnŸ€6 κ,¬LkκξιΖ|w&©9]τθ ΓGΘGά*ςλ\ήέΥEό»4α̎–a"Ν²55βΤξLΏμΫ…$°UΆe¨Λz’^ 0R¦θZ Ά θΌ Ž2υΣΘΦ»¦Ψπ2>™uΚXΝR‡&ΝFfkN± χΠπhjcy’ϊfϊ† ³FΤϋΜΫtέpτ5ΐτkΓaΎ£½+η›γ„”Πχ n Ξ u˜Eρ…ƒ km1ΰ«μ]kάr> SΙδIeΤjΊlijKmα7wΑτ₯.Λ$ΫΏ@LdΈό›—§vΪο;ڟ~φg6χψc¬Ž* T¨4Pi @₯%j`ιΰ  £GΈ*3Ys¨C ›tdL€‘ }9\d·eS°M3γ\-Μ@Œυƒš™±ΧDΠΣ B+8 Psγ4ζ δ]Ά¬)™  @Ζ •1·€'€1}•’=Μ„΅ ΐˆ2Όf-²LΆ©yM@β-ƒ€fp£ι9²‘vΫb†„0Ÿ~X³€3£ο;Ÿ<€!ΪKΘ"Ψr£›©‘εΜ3;ͺ†&ΔΔ’@”₯TΆG©gœε€S©‡‰u-θ„z띳ӀΆ"υcrCdδΧ‡M€ˆ;²«c5ΜͺΘplΰλ3 Fpd¨@FƒRcΤ―ϊΥ©ͺ―ΑζΑηE>§>u†ΊX³ͺ;­[Ϋ›.xδ91Ι ΕΉjoΈαΖ` }~&Aηb†3ίyh|xŸεΎ’3ƒoύπό‰€ή}N(υdυΌ_ŽηDΊΛ”Ώ§ϊξRθ$' Ϋ)υž$kuΉ@₯J•d ,| d!4y/Όb€bΐA?ξε^f°eGYΩ™%4Ν}\σ^”P4¦0£uαCψθΘ@€Y~€ n|•”‘V›δœYy t·(˜H°‘E3MΧhςϊ0Yv†P ¦X΄ 50ΡL¨ϊ)›3sςΐ[3~WS€QDa£rλo5M{¦ZώXϊK‰”ΜΠ©―sςΜqΓρΐ¦Θ?:‰ΖF™,P7 ΨbΒ@³Ρϊk©½ΕvΡΝΜ}³mύ„‘cu〫yV?³Ύ£}iΟ;8H’'„–% ”›B‘S欞iŒκΞXaS2sΔƒq3όE#¦ΚΝ›6¦6€³Ξ<3uα _τ²§‡d:‘|κιDΧΤXvaωΒNVοΒΌ OΦή=Υw²r ―Ÿ¨“Ι·°\u\i @₯JΌ–Ύ2ΠΘ,_π™ΘΏς=79ΙžεKœy2Y‘0Ϋa Άˆ;Bα{EY–IVΖ­œ;£Πu 5ΥiRΤΌΧΦ–ΞΓ,ηLΘp΄́Ιδَ`*f樞rΜξ³=Άˆ{…lΚα%0ΓΩΫl†ω’ΚΣ|((‹ό‚½`Ο7―λ%6*ƒ`Ύ–c• Ότ݊¨ς䝙ΌΥ½_ΖFl 5=/2²Ν°”ΡτΔL©ΦVϊlO= (Α„Qg@r–ύΘθDΪ‡Ι°­½5ϊˆ6τžϋε³πΌ"Ωw‚U€iΦt}u‰§€n3¦OΫkV‘?IΏΎγ|/Ι^·„Tΐ‹&ΩoΌ1]pΑ1α»ίύnψξ­[·.ή“ςΜKW^uοWczδ#“6J= 6Κϋ₯|W_}u„Ω΄iΣ λ+υžloŸ}Η‹ΘyίΛωΙΚUΧ+ T¨4PiΰΑΡΐ’ΐ—’ .d«Dζ—Ή>Δ9ΓΌclsyrή|qΦΕAQf²ΎR@© Ώ-A–³οŒ…εl½’ΟZΕx‚/D4‰?Ψ‘CG«pDIΐ¦Ώ–¦·`«mF‚³:+ƒ–€—Žσ²aΩ‡ωL‚Ϋrp˜ ώΘΞ¦Y3οgΩΝο@P‡ϊ‘„ ‚©7ΏωΝρξ ˆ °λ{ε»t„Α?χs?—^ώς—§Ώϋ»ΏKΟxΖ3R_EŽ…ΰI™JΚοZώγ΅ψ‡H_ωΚWB6λχώYg•žόη§Ν›7Ο½›ΉwWu-ΌοyI e)Χͺ}₯J•* ,MK_ ή 2eΛƒƒ:Ϋά€™€Iρ…ησγG>ˆŸAΣΑΘΊdΈœ•WΒΘ<9€"‘Ÿ@’ζά< Ν„€ƒ&Κ^˜ŸνΫw€ρ±ρΤ xӌ¦Βz'Κ,—e ρ0Κ,Fε 3xC°‘ΦρΑΨσ`΄7xΡ!ΐ` ζΝΎ:fg=d@θ eΏV% ξτΑš†ζJύ8±ΟβψίΡήMŸPDdΑ’ˆXOνΜθ€ξ±ρIΜ‘ΔΓL(lbkcRA3}obζgΘί8 U£a*ΠχŒ&Iς[gy^§Q6‘βžϋ|Κ‘ςΓδ‰'‘ίIμθνβWφ“œ8 τšΧΌ&τ/ωΪΧΎ–~χw7½ύοO§žzj<+ŸOeŸŸ―@ϋψσ/}8}φΩρ\½ΆuλΦR/|Ξ–΅ξ·Όε-±χž²˜|—ήωΞw¦g=λYιq{\0‹Ώ5  οa²|IE–²/Χ‹Μ>RŸ―Μάi§–^φ²—Έ“΅΄ί―xΕ+_υ_§σΟ??ϊeωω² ŽKύeoΎ*U¨4Pi ΐύ§%‚/Ηργ Jπ±pАc‰αΪ<ωhnΟΏΦtŠŸp2[.7Ω.λ2ƒƒˆΐΛε„ΌςΐIΞεz ΌΪΪΪ ¬»H}.―³{ϝl{Β|&™Δa](“‘ζ [X ΡΊΖFuΨgέFΪ6~—XIy\Ά‡Ϋ‘7ŸΫ“γrξ7η8™er0׿ʁ3χQΰ(ˆδ€Ξ-ύՎψ΄ζjίή#„ΖX—Φώ!"Ξ+'ζH3Κ¨ΙΔ9+!/-D{>2D›2{UΙΥ©Νθς,³€€Σzγ‘-LΘ lΚoό‘8β,?Kn’K’fU& Πήμ ‹•2ς>Τ“LΧ™ψ©•dh/}ιK„ίΈ3ΐ—ο–,κ5Χ\“v±ΐŠ+£υ¨QRήcί»+―Ό2νή½;sΞ9σΐ(λ.)έ-·ά’Ύχ½ο₯U«V₯Η>φ±Α²ΩξφνΫΓά©gœqF%ή5A’[IΦιϋhΪ±cG€ΩΩΩυ6₯Θ$ϋ¦L¬ξ°mΫΆ`ςό±b²ε駟η~\tΡEρχτήχΎ7½γοˆ:½Ύ‡Ώk―½6B—lΩ²%€™Œœuψ.{*Μ¦‚Xϋδuσ˜ΗXΌJ•* T¨4ph@΄°Δδ@.€’+ΐΕA=ƒ¨”/s‰_κξψΚf(M7:w{ξΐ·΅c玴χΞ½ρ«ή@ΰ$€:Δ@pm“Ψ$ ¬³ςnΊωζtƒž@Mp~0Θ& 3ψͺμRښY.ͺ1γ #λ%ƒΤ “$θ@ܐΣ6υΣΐΙy]$rΙχτ'2©ωτMk φΞ{šΠξιπ©“ό±ώtˆ¨σϋοΌƒpϋΣΰ±#iˆmpΰ0 ϊ‘i°#ƒ3Κρ:Ξ¦Pε2’~Βi6ΞμM¦Y¦&XΏad˜6ξνΉdΑ,0+pœ£4›SΦ2³ά ί―άηl6”Κ0E(H™ ΜϋPO™bzSV’¬ϊυλCtߏ·½νmΑ„ /Ώόςτ«Ώϊ«iηΝρ̍©φρι]οzWδάη>fΑN@ο‘›Αl?ωΙO¦Ο|ζ3²>ψΑ«εϋiϊμg?›ώύί=ŽσσχΛζ@ίK“η&λλbςΘΏψΕτρœηVǚ62γΤΏ—;ξΈ#ύ/ύR=Οΰώ ύνίώνά»•λ(@ίφ ˜{ξsŸΣς¦λ».]zι₯ι?ψAœ«‡χΌη=Α.ΫΊϊ…_ψ…gφEΦL=|ωΛ_9£PυQi @₯JχZKdΎ2˜b(bΠΤ2ǜ NΜ _fΏ\ΈƒŠ Δ͈υ½š1 Κ<9 ͺlό·Œƒˆ{ο9Pyέ_ύeπ2βΌα Ηp?―ΫwξN‡ϊϊ8Η΄ 6Ε¬CAQoοςπ+IΓ„v0π¨u70+zeΜlGΖMΩ&Ο”²<¬”ΑΤk “ƒ’y½o½ξK~†Θ•Αΐ¨«V¬L½έΔά">Ψδ8k3’wl”ψ^ΘΡ  ’Ρ"Μ* ! ‹Γp…―YθEί3Α!Že’4‹Ϊ:ΝΛ0‘;2δ’ά£έ’ζe>~)ξΗ)e¬W»#’Su˜—ύIqΈχ ‚Ϋψν€ΠZΌ/2\ozΣ›‚R‚-Αˆ CχK^ς’τ—ω—ιƒόωήΐD°ρ‘}(rΚ)‘:M–_ψΒΠy~Ζ²²2Z―}νkγΪγψτλΏώλQ―ΎVΦ«ΩΫδ³1YΦ΄ψάkϊNΚ>ύΦoύVμŸύμgΨ’A“}ϊΐ>~k?σ?oφTξ?υ©O fΞwla½ε9kΞ”•σι}ο{_ϊε_ώετͺW½*Οφ΄§Ερ“Ÿόδ`Άμ³ϊ°/Φ'γυ;Ώσ;ιΌσΞ‹όΥG₯J•* ά7Xψr8Ρ |bί'#­3sOfΖ$ΓΘ*ΘΒΔΒΠ€ vL„:Ρ * „ŠλR)Ν)ϊ¬8@Zόws€+¦ΗbdXΜ7Ț„ακ?6Δ’9Yλq(Ψ0@Œ&ɘρH>Ω’0Νςfρ·2Δ…υkΞ3Ž—l”m ξτν)3ΕΚ`iŸl3€ ΗQ–χ&σΠ&γ`=Δ"«z(χ½εΣ4,_CZ³ΊV-"Ψγ¦`ΔΤ:Dμ―±fϊήΚRJN8X–ΊˆΨζUΐ‘²-_+Vš€”ΉσΊμ^ ,Ψ4Œ™νΗL¦+‡ϊye©σ%˘rWΤ™“Žœ‚ρΑτ!ϋIHφ§ƒΐ°OΉδ)ρ\<8ΘR €4ΛiRΣ]€δσςΩ<ε)OIς'`Yμ9ΟyNδχϊ|/Ύψβτ―ϊ―σ*p΅ /ΌpΎ M~Φοϋj²ήήdΎΠI|,gΕό§ιT¦ΞφMw ΣζΝ›Σ·ΏύνωχTΣ©¦TΝ’sρ„-X‡ο‚ŒŸ ΠλofςΊ&oΩ±ΫoΏ=̏ξ_τ’Ε;jnΟ|ζ3cΒAyίOΨHu±@₯J•~, , |9‚·ΐΌœ=(8|eΐ’‡w„FΙδtvv₯Ξξ6~ιβo5ΠίΣ3@ΕAAΠ½ƒΚ0`hG{νέΚ $θ,δ5Ml£c½― Rn 31Α΄ϋ#δΤ¨ŽPΈΟ„ Άer`ŽΌ:οg#Œ`M™&<φΝΌeΛ€Κz2Γε`₯XeP7o9Άϋ‘ίWš!p+»:κ`ΏšˆMN΅† uŒS:τΧΖ™8€© +ƒΛΊξc#1ΒdΕ―π[XA’re3b }gBŠ‚+΄“™/{ gύΘ½{ˆς±ςQ˜-Λ2Ψjφ΄? I9U29%=ιIOŠwF“ήήπ†xwό`*ύœΊ}―Bšͺίχ}(ωΛ»`žγzZšŽ,o½ΚPκσ)υϊN5ΣφΎ}ϋβέΚk ιRΜ‡…™ΣD\dσσΨχσΠ‘C¬X°?tbΏμƒύ4•όκΒ{Φ« ω½ΞοͺΗ–)Q°ϊ¨4Pi @₯{­₯―Ήfm¨ΙQ’όežY}Ÿd ] dšΔΨdΈŽρeP‘ΑˆI³–ΐΚ­0GšŽˆi)†0,Ύ^“ύ3ε~yGέε-_?~μωObq]žύΪΌysψ4ύΜΟόL<_ϋ£©g]|υσςω ";+;ZQΩ/ΤΕ]‘Ζ»œΟeτΪΙ/«<ί=ΩΤ'<α ι’K.™Οζe.©€*Λ™ό{΄šG ‹fŸμ‹!-μ›yτzήσž―‚Ξ[o½5β“YŸοτW\1γD²—φ«}₯J•* όθ8ώνύ£—‰œuΏΔ\ΐΉ•u;»Ίγ—ΆΰΛ/χQL}}GΓFΖ+γƒ@p"،γ`"ЈJΰ2Η¨ ΈΜηυ<`d†iΐ5€ΙqˆA1‡”ύ‘\9°d@%#ΧݍΞόCƒΚd]yΆ€ %ΐys~ΑRφ7S²̫͚Iι§Ι>™§„ Θ† ΆΝ$Έ3―2›2€3 ,¬‘&Œ©EΥι0ƒΰ‘Υ½iݚΥ,‘ΤƒΟΑQργͺu庍[666 °Ε)$‚¨ŽO £qŽ‘Λΰ―2`0‹F©7 … ―³VG<0³–Α2Γ­Θggηΰ7ΌGqΑϊcΊEyΐšχR’!I ˆZž3ίώφ·‡ί—ύΡΌχ‹Ώψ‹0ύθG‡ιΞϋϊ[™Ξ=χάτ§ϊ§ιŸψD'JSέ‡?όažΡ²xφζ˜ϋ,LΎ/εšΜkyώ%ϊ4οΛβ$ΘY|έkn¦—Ύτ₯1I@œ ’γΌρΜt†\Ω³of‰/2Y—]vYϊηώηπσœizΑ ^εŒQζ,Ο―ύλιΆΫnKxΔ#βΎ‘*.…Qσu–¨ώqΞuve•* T¨4PiΰΎΣΐ’Α—>_.γδl²ε½+Β\ΣήΩ3ώ††FpυΓv °d’E„Ibd γƒ26–}­Κ 〃ΏτŒδΔ άΌ&HΣΏkfHgy¦OΑMμƒd2\M±Υ˜ψߊŒ‚ΓΡΡα¨'@¦K}š2°΄e”QXkΝ–3¬Ιapω?ΓC>Ϊ™³Μh˜ίΩ‘šh•Ή½½ v«1˜ΊˆJOήZM`„‰‡­£­;­θYλW‚fßKΡ•j 686 ζxF σ*&Χ‰ρaφš[a•Š/;'0“Ή@œ€RT‡Œe£ΟœΘxEom¬$ ½βrζ Œ…ρΒdN"°ίΎΟΖ%ϋθG? Κη­μ/|α γέ7ޘ€ΜΝc}ΦLϊΎ°ΥM`¦σ½ϊ +}ŒΥG₯J•* ά+ ΤρΕΌpψύ‘*ΣIύŠΛ>GΨ„>K#ρͺV₯UΜ¬·j“’Ξ½ƒƒCΑŠΑ».ΑΑcˆAΨf5ϋ"cΕ†…νp@πΗΝ/–.ŸX›m`6ΦΡtΏ–Q|½&aΖρΩrmŘΡΘ`ηugŸΉΜΛν·έJh‡#κAΏ«Μf=e0μ…"Ϊ/Ρ•a%Žk%ϋΠΔ,NK­ˆ€’#š ˜ΡŒι­ό-MκDΰ§l] zWυ€σΟ;+=ιΙ‡#Έ€Ι[epφ‘δhσϊΥΑΒ ŽΒ4΅€CΏ#τ/˜Ψϊ|YOwOΟ%&A―hJ’Οψa²vφYόr–W Kξ'qέ½@yΩςΥιΏώΑې-ƒ ? Ιg㍷βT~Ζ@ΎϋfΝοVž‰{· σ3)χΤ›Œ«? 3€Κ}AŒ¦F”ε}3 e•ξ©ίR―RΠγuλ60'|œ¨œχNtύDΧό(>iε=±ΌνΩέ/”mαυ7λ°?šΛ{b=·άzKΎΝ›7{I j[Ξμ¬R₯J?έψqΏsΊ΅qφnΙΜWbπ_Ι¨ς£ Gφ po2r»ζΓΖH tf~ιΧ\Π  o’ƒƒu ¬¦ΤθΤBΎ:ΠΛv9π8˜Ι0t2pœ΅§`^ψ07ΰ8ÁOKΣ[ώRD”?³#-o$|\ρ‡¦K—ϊ1―€ά, J „H!—ͺ[Ezχ™ L4 σr1ϊb=φ‡«Ρ†w,τόφΩΎ‘•ΑY‘š‡857`εκ_†ΐΐδΔ&(ΤlΚ –LX β}o7zθaBΒ8 0GŽdχ”υИmGτΫΎ«—ΰ·ζX¬lRE>ϊQπyΚ«ω5Λmο¨ΐ,φΝc`ρŸ„δ{³™sίμJξˆηΎ Ξ*4θΎ€CFͺ€ΠΤσ…ΐ§ά_xm1θ:QžrΝύΒ²εϊΒkE>ί[7“ΧJNΦήΒ<ζ-η²r…™+u{ŽwΔLO—QOL6νcϋXΔ9[άfQ}T¨4Pi ΐ’4°4π%Ύ 9Πιά>H‡>|md₯―[`v&YΪg“ €ΛYˆtetγ .fψŞ΅cϊ °Τΰi gΤrώB'”…λ4_·&Μ=έψ”ΥγΠ¨(νΒGΰ ΙFr@8ΐκ8‚!rΖW8ω“ΓhφΩωpͺ+Ι œa­v‘)Ξͺ­ldŒΌ6Δτ}―Α<3™Ο-ϋ|5˜tΐ¨φ> .u¦Χωί±_0zγθ9]*π₯ο–&ΗfEg-ͺΠβP_OtώφVΓΫΓ„₯C~?:iλπ‘Ά΄?²~b‘5`’νθΘΡϋ5g…:Ɍ Σδ$ώwBϋΟϊKяˆ¦ο9)k?†…•K?Ι©€”‡rΎx”{ξή/Η ο?ΠΗE†…ς–kw'Λβ<εόdυhzΥΑώk_ϋZμ59~ΰƒHgœ~F4SΚί]›Υ½J•* TΈg , | 2ζ€Χ€, •τΘ,ΰΒΈ_γψb α ?L`ΣIbNι˜/Έq6ή$L–€JVΕxUšΪΔ7Τ7>¦sω,ΥXͺaξςWώ|_Vΰ+‘κa*œ‘8Β6<2˜D€{γaˆ M!hΔίjσ ζG}­?»vξ @ΈŒxYέQ9‰Ÿ΅PΤ€ o ˆ`²‰Νb‘@Kˆ qξ4CpSά§γάΐ¦‚IY%— j'ŽY³rτI'zψ2ξ(q¦ΰMα7Φӝ™Cttυ¦1²]{γmitϋn|ˆZ1―JΫΆlI«V.ΗΡπ}0Ύ1AΎέP@IDATΝκ¬δ7›ϊwΦζ4‘υΕ^ΑΡ/i‰DCZό4¦{χtΦΙ}%ΟΙκρΊΛΉωμO–οξwΥ^₯J•~Ϊ4°4πΕX< ΰυηK€#»%»€ωP¦Η˜YΞξ 6e†q¨±€OΓp6χΛ]Sž λθπ >Mƒψ΅Ο€žΆ΄rν†Τ‹Ω§§Ηυ1Ήf!5κ˜„ρΡ§L2‰Ρκj˜τ@!“¦BΑ“g.7ΰ9% ˜œ\ HΪ£i Œ5Bδy—,h4α7% Ιώ[†pȏ]Πλ!εž¬(—φ°Q†Φh8i22ό„ϊ˜ $†ΰ΄~Ώ6f=/·Y̊υ΄ΧŒΩQg}u)ΜΕΰGΫF«·|C#7~ꨑkζΥ1»–ρ豝0_ƒI$Η}ϋwp*±uKzΔΉη€MNI=L†pφf ΜamΞΤZγΉψΌ@Δρ¬θςe°}¦^―OΏ θ*ΐ«œτχΌκa₯J•8 , |!Ÿ&;Φ§¦Ηƒα π27K1ΎΈ7ώgΊ€N‰”.€qΖήp3ρWdk₯Ϊ΄q}°4-ƒΥ«V8rΘ·-cxS"hλD3u2Οα  τ{Ρ—J“žΐ%՟ Ψ Ψ”ι“fήΪ_p}šϊ‰Ή°°`‘Y–ψš)Ώμΰ›Τ„ιΟσVD]rHVάήή™Ϊ:ΪSfVύήΒ'lι―cσŒφP“Ÿ]fΛ¨c_3oεˆmvπy 3’eV―ZΞή:ΉcŠ˜eΣ΅ dB†LŠόΣdiŸyκRMwξή“vοڝz`›tόǜΈ}ϋmιΞ]w€Ν§m€ۜNΫ°“εrt‹¨…ΐ­M-ψ¨0•Q“­ΟMέzœκXœΎUιᑁΊJoŸ—λΥΎ@₯J•–%/A„ώ\ύ}ύ―Qœί3¨qF`Μ\h ‚¦§ΐai\°vŠ» κDp‡e9zτXΊω¦›a»ϊYgŸ›NίΆ5­cΖδrœΚeg] }Ε6ϊD΅3{°΅½#›8B gLβcF3_M`Δq49r}άι7NύΘՊΣ=³§e¦¨»™λ=i„Y‰²YΝ-8ΒÌ΅’WfKφ§ €§IΞκ-§YSGμWl*‚ΌκOεL9τΐΚΎS™9*ŒΚΒηΝ0}€­λnΈ1Μ¨ξ‹˜h²‰gqvΪΊντ΄uΛΨ-ΐR['ϊtamc…F^2sθΓٟλΦ­M<ς<ΦΈά(cF(zkΖηlV 3ξ!v¦ZΪ»ϋŽΉ‘Cυš5kΣ Β¨ Πe˜:k0k5žcCΜ‘Ίhͺϋ*U¨4Pi @₯Jχ–ΎD;‡χΑZ Δ€απ ƒ~^b'œ…Ί.αϊ…σ=μ˜3#ΰίΐ9οά'Œ—ΐQEΣ21œ΅°&€~TΜžΤρž)Μ쁝 +L’°]ϊQ(*d™1ς< Υ7‹Y†0qSΣ0HuάƒωLΙ*ι_ΦΩ!#Τ€³p˜%­8Ήk†lΉQH8Θc.Τ―ΜΆνrΙΈ ²<Ύn‚LΟΝkyε–Τ£εm· €m1ƒ±P©žvμΨ+vy:ν΄Νι‚ Ξ'ζ9ι”SΧ U#g' S€μΩ&Ψ›nΌž€˜w€SaΦ)h σ)ǍȬ9uίΆaB}ΘjΌ%έ²}Wx]Λd†υ§nΧά†ƒΎ °\G?5ΥV©@₯J•* TΈo4°$πΕxŽI-/b-qΆ‘Ί@D/+ρ–¦*Αα#†Γ̈œΒ±Ό 0Υ‹W3ε֟Ί>mέΊφ¦|²dT & '}Ψ‘6@—Ξδ/M3ΞΚ#Ο@bfiΫ…3πςz-‘ ΙkP·ΐί(Ξά Xy—˜ €‹2k†i(Η? ‘NμΤΒ10 )'Ϊ˜.w€0Ρι›(jΉ ‹Υ4§„£Œ>_ &Κ-| †²HΎœV :55υ‘όΠόG0AΜp_\0CpΫl‹„¨ράoqBVT”‡ΐ7 1Η*σIύTeş"5P$u\9IΕDΖψ^Όd[Ώ ύGW•ΰι!πCvuθ₯F²Πf~JMˆίU ΗxFΰg8Αζία{φwܚ³ύΩ$q‰qΨk0MRa Ωbœ4šFIηΚBΉΘΪ1ϊjigΈ d“Z7\3†X9:Γ8fέΊw“ŸΗ%Kφγœ’Ε•™$΄₯EΊΘ€γHDθŒβυΦ"ΞZ:%j"k¨/ω[ιΣ§‡’Χ@σv0 Cΐ¨G87ςέk©~¨3ω"#!ΑΙ‡?ΡV„Š(+)Dτυl - &vD]OΑΚΕhmθΜΞJbBCΠδΘΐͺ$Kϊ Μθaˆ@_NS5U œμΥ J‘ΙY΅zδ!š>I΅LŽ (ΕA 2v’χγ!αR"Β; ό8-ξ‰`§ΈΝλJ„ΜΨ’‡Ύ \@Xš?KK¨IsΪ4Κ£O₯ќˆ:τυ’‰”š9M*‹$‘}pT…Eh%Π~‘«Ρ/ŽΪ.j±Έͺ”xβΤ_ F }ζbσ%Ε­°+(·εΒδˆ… _ι-3$+Ω·¬Fd¨βF‚¨Ν l~δρš ΒB±`!ΔZ±ŽΪΚ)cG«Ζλ›oηΰEΓ!ŸΞώ…ϊ|ŠA¦#"ZΒ―δ"4Ή±qά “μ%Π08[+†€!` ‡@έΘC δ·`+›5kVK6΄#άώ§S‡~K)Ι Πz%©? Ι ‰‰΅5‘ΠΘ0©ž“¬ΰCU‘’IBBI–@ θďhμPVι*ΒE‹I>6–ζ*<ζq…#5?4)rc#棞8(Ω6‰iΙλΰ¨5·Z²¨°QαsΖ\t‡dΉΧ#+ΊΊ”ΙΛΡƒ’7’'άQiΪ¦«’čmπC™,Α™X#L‹‘/Έo%ΙFM„ώ+φρΡXΫ…Ί@?A€ ΰοU\€νœP5!!I}θR`Ξe dIΊΈh€F―Etνβšςρ/1>wπ=j$4m%Ψΐz‘š ·¬aΊ’WΟX•Ί¦εΰŸ‰εY2 Cΐ0 ½G xVέγΪ$Τ’¬X½R6"dDYQtκΤQzφθI=~Cp(‡Œ¬…ΎRΤXUΰœΞνΤ‚Ρ ή9υ9-j£@IHxH`TKΖvHœ™OTγ΅< ΡͺΘ.ΐPCː΄QCΒe9$c$ hΕ=ρ‚8MΌG¦βV,2Λ• †ˆmͺΉR‰V ίΥ{¬J εeκ%ΏTeβ.ϊθΘ ₯!Cο‘U²ίΘ[aζ[±jtκΤfΎ(%΄hŸ{^rJΚ'quD”ΖWbΔ‘³±ˆ –μF ΖΪ +Σ[A— 8Wb‘aβΨƒ“šyAό”ζΑτΫ±]+hΐFA#Y ‹~X"ΙIρR“qιA‡€)Ή›ϊφ)^Α‚μά0 Cΐ0κ„@Θ[Ϊ†hμλΧcck˜α3°‡`Χ.Ίκ. ή•pΊ/ΖjDj·θŒ;x Ž IΤτ„€ΰΘ’Θ³€Β χ™O'wψCΡο‹χπΩ„½s6e“Ί ¬«οΘ9 IΝ”ŽmπΪ›¬j‡a«εΠ8±ΈrΘTδR7Dς娝«ΰΔ²'θ+ 0IP5±Β™v›_Κΰ‹eγρδ’80Ώ>k‹–dIŸΎ=aήkς…Έcp°'1’_š†ΖΐΉ[i ‡ψ†…@£ΕΊ;°R DBƒX„Υ–?,Z¨qΎ±yFΊTdtόpΰMΒ¦>fz­h² τ1‹†“~Οn™rΪΙcdκŽ7dΩς ~,_ŠΥ“Ι’œΨ^ƒΩz,9*K†€!`†€!PwhCΫγδ‰ +h`Πν[@ΒΤD•–ήRύ†θH_b@ξAβΕ8`QτIΒG·©Α¬OE³[(΄;JΖ@šΤ§Κ±%DΊͺ&Gϊ•Ec/BnO΄~έ:Dv/·1Z‡„Θ‘"Π" ϋιI˜σΕB†V aAshς#Ρ i“d‹fNΐTΗzδ»D"δ?Ž2‘έIGπHΪ %Π7­Kξζψ[@.Ρx%Μ€ΐf%bn­X»^ αw†5žUŸR¬¨€ί ©qFM5ˆΔ¦Q칟¦ oAηϊ%ΌYKΛ¬/Ώ”eYΛdSvΆΖψβCŽβjR”‘χ›άΝXl4Lšπ) ƒ9³WχΞrΖι§"E&β q#π ΝαΠ€EλGαͺ†€!`†@]¨³ζ‹Œ")9EΪΆm£«η’±Ώ!8ˆ’%L$"JπεΝΤΌ€”‘ν c: Έ‰ηZ DΖΕέςš,nν³!&Φ¬^η"Γ+Α©’ŽŽt9‚UM!BSΤ<ΡA €^ι·’=΄Η>;Η|ή₯―•#lJΘ@’τ #xXωΤ: \$]Š<’9’9’4\)ρCυJ΄Ο+šTs uPPcI)Κd­X#=Ίw•1Qb₯€λ Υ܊kͺ¨½Ϊ‘2‡Ά¬ν΄©thώ~«%©ΜZŠ=ρL’S-5-MZ¦₯‚D9ηωP¬Œ¬„f+ &`Ζj+)ގ@ͺq½k¦œ '|n€ΞΨe\ΐPѐΙξZ2 Cΐ0 }G`―Θ—'3l–Ϋ‡θ+3;KΨΚD„ϋ RΕHρŒζ^ί%ε(Om™‡j†h>"'r€Δ „d…H-¨₯0έ1š±MπoΪ¦0˜”@3δI$I••$vξCΝ²U†3στ¨tv†$Ι±ͺ1‘!Ÿ€LMl$^ͺέrZ­°‡²ε  ŽZŸ™JξH¬ ¦X―)γ˜,•ύcX”qΜt-#ΐ$»=Žm‰X^ά‡1ŠΪ>¬>,žΤ0R©ΐύΣA‘y3 Δ-•‹oHv) ύ" άΆ%O#Ϋ―Y½ ¦ΘVΒύΣ22t!Wk–κJptͺ«UΉd·ΜŽrϊ)cεKhΟ"Aς"€}υͺpςΨΡ0 Cΐ0κ„ΐ^‘/ί'⑃ϊitvjV¨m‘Y›i3ΙΓDΠ?‰γ•$+ $ TVQΓDγ£#7ˆ­rΒ€’Όγγ~)‰Α55>%0 ΟΙ‘\8ωW"‰OεnͺΝΈ[ŒFνWBΪ?Cψˆ’ –’?Y lΞΉΪ’δ$ ύTBFR„>²οJh¨±‚<|ρ†~(ŸΔG)ςT›†#ϋΗQz#…ΆK‰e2„Œc„ ηV1Β,KΩθΧ–Ό\Ιݜ#];΅:ς4}§L±%†όσ`9ΖK‡ό0„–ΰΚK―‘ C_IκΚ° ΅ uΘ6 γ” l¬ SνκΛq^.ke]τFΗ<ά–ΧΤtEhuΛμδ’ο£ύR¬ΘLLJVΘ*– Cΐ0 C`ί¨ω")θΨΆ5φ?„ζFύ£Θ#H@JΈMύΈPŽδ‡Δσ%D(³>V`3θ½ ͺΪ›‘ ~ΰy…K8ŒƒΤmGό©b8€‡†E‚d dϋΈ],‹K%^Έ‘Žχτ£Σ:}€HJΤw ­Q‹¦Z(žγžξ·ˆ~@š“‘γ œ{ΙTd%wΞ±i‹#ΞΕ9Vž{M‘ω₯"”βΈ3ξ ΐ1s,₯ň4ΏE‰d ΆβxuŒμΚ°Yzg±½Phΰ|ψŽ™wxPMξλFζθΙχьA,0ΗR<“phωμΒαCηβφsLH­Τ­–ΆmΟΗBŠνΨά{+6φ^²aΎ};ιΠ)Sz₯dκst½·oCΐ0 Cΐ¨+u"_lŒ«μΚα/€&1ΪΎH”tΰHB€Ϊ"œ“ˆρœÍ8’ΒP+† δ]]\RK2ΐkšϊHv¨+ΑκΙr―(τ˜Z&’Ϗؖ&ζ!“u5Β=ςΉΚfϋhύΰGΙ Νl‡u(€η(Γ?NμK΅|–cςGΚvΙ΅―Ew&‡εpSϋ„ ”H‡ϊ|Ÿ νk΄fͺ&‹}QΉ$a”ξ°„ΝVρe―”‘•Ν…Γ'.6> Ρπ±›g3φ7(羘‘Π„…ΕF β΅ φ—mπλڌMΌ7"Ί=CVlؐ-±’t+φ©,+₯ζ-L’’Ώ—ΜΜΞrK―Γ«ž!{bΙ0 Cΐ0κ†@έΘˆ Š#=ώHNαH‰“':ώΊŠ¨€>y­UVίQΗΕ ©Κ:Θ!” €(‘š@&-’$m”ο|»œluΨG-–wΪ/–ηΚI艔΄ ˆŸ?g9Κ`οιFΏ6υ£|6爣ο«ΖΩBMjŽ$Y#)ς™AGŠΖ’ρβ‡2ΪZ °6 ζρ&―5rϊH'wΥv©l˜DΥ„ΚqΓ‘eΩŒRQ“GlΠSvX%ΦαΨ—19­€΅ΙαJΐψάs  2&ϋ>’L φΫ\Ώaƒ|ύέwς=Β\¬GœΆ­0η2¬‡ίϋ’&γΠp8ƒ n݊U‘›·ι˜ΩmK†€!`†€!°oԍ|‘MjΎBΰ―D*AbΔMœy$ ΰ*Eu5 ȁ#<Rθ/ɏnίγΌηQΖi₯H%œ6Κ‘B ΨΏζ8ξΑ€kΗ9Θ;Tα 'lΗωj±„+ΔΎ0Ÿύ!AγΗ“0ηŒO&¨ƒΨ~@—ζH%j™XŽ<°%Χε‡Η r¨acΎi?(!8ΰ'§γ†‘(£ζIΘgŸ(Ÿ&STv}¦6P9ςΕΦI€8x·’Αkγ%­VL¦·ƒ)ΨmηΔMΆt–ώlμIόΔβR€oŸ`~Ρ β‹½)ωμhΞ€ƒˆqβΚKšzΉQχ.Μ‘Y2 Cΐ0 ½@ ΞδΛ*G6؞'5<χD£Š<`bχD„αXΆΪŠZζQ†σYβ=žWb» %/ )I‰ƒXU%0‡iR"δΪR­07˜j .υz'ΡrζQjΫΘ‰ΌζŒdǍGωŽφ•KΝ’*+ y‚yPύΔΨˆiRΎδΞuμΘ¬ΐ}ŽίΛεΡ‘Rp‘θ¨(νλΰ±,V#SϋΗώcGG8Ξ+ΉΒ9ϋ©β΄ œS.ΩŽ.Ύ]ν»K‹VΠˆ°γ‚€ ¦ΒΒB%VΙ ’Œ­ŸΈYv|ˆZz+ιΠ‘ƒ 8@6εε`oΞR)Dό―νπω*@δύΤΫSθfl!‡}8ύ8tΌφe†€!`uF Ξ䋁>Ήu5Jτις¦EOΒx䇕*M―ΥΜ†CΝ }žhr#Ρb†APn‘Z$\·lΩqͺ
d+κ4`*Ϊ%2W–&9:Υ»ϊN&o«\mΛ)»*υ–ΦΖ»ε³(Ϋ›ύT&Κ(‘ƒΜ΅Κ:Τ,±ΎŸ‹!F Ε{$NŽ€:­ όΡ§‹r΄"΄ˆ L©ΨŸ1Δ¨›ŽkZφ™Έ°S*ΫΙr}Y’ϋ@² L‹ΙΠv%ΑάΘ ΅uΈ§ζΦ­Ϋδ‹/gŸl»tξάE†=!,bPυс„³ˆΖΒ‰–‰ bΛEν ϊΜηΑ… βΚψbκΘΟρX2 Cΐ0 }B ΞδΛ›σH0H9±^‰J€py3_0ωR’–B2’ζ2Τ$‘Qσ€’/r'‘C;‹ΔŒD"%)I@Ύrsς@Xί5LΎCyHσYm1ρRΌβά=Τ£ ΙέΦ2n4”Βμΐ‘d$π‘LέΌ[΅iJn„―ͺδϋβ$i£,†zjγη‘Ξυ‘ PάΛ1‘š(A† A―hψo)ξϊfH.‰)θ₯nΕ§ϊ5ψ¬b|–.†&k{~BXδI!λ‹°`„YΙ1ΑΣM»Ρ/Œ+}βfέ Α…Υ«|N,ƒgƒ%“JπΛͺ1Ϊ‰!`†€!`ԁ:“/Nβš0y{ '}N$*ώ>'rO\\>}»Έ*Οω…ΉXUŽύΠQΓβL’4‘TTρ ±Π~₯ΘΕYΊM‘χ˒鏲θ?V3›#Zo$ZαŒa’Γώ°/€DŽHΤ`\<˜ Ήe}1$άsδ ΓςC¦EP΅WŒU¦ςΨMŽZ%Ž“η>±Ύb€J\Wΐ>“τ°; uΡ¦ukIΕΨΈνOόΪ*J δͺ_V”‘OrDβVŽ-Έυ·nŠOΞ€W,ϊK”Ψ&c¦!j=΄Βr¨•€v’α5Ό»TžPԈˆˆFθ <7˜:qΫ‘>`©¦Mv¨€¦GvΒ’!`†€!`μu&_J΄Π΄wpηΌΜU~ AβB|Ÿ”tdT’/~ΨΌ#C”γΘζz–₯Ή’ΎNΤ0A^ΝkΠ~‘b0Ό‚γ7Έν8Ώέ™Ά[Eͺσ”α¦ϊ‘ωvAH˜ο5;‘ΰΕΖΖHΏΎύp„ΦŒ‘ςΉςqGΪcG;ˆ>„pW=―Hƒž۝‘-Ÿί ±Ζ Cΐ0 }D ΞδK© &dN€J<Πj€xN2 2ΑΔkϊ")‰ΰ=’ξ“ΝM•Ά >“r°2^‘Lπšη”A?―τΤ–’˜˜ ›w*œ{Ψ‘#tbgD{d#‘Š~ρ χΩW½Α[ξœΧό°:ΑkyΚr}r Θπ²ΐDͺ#ψ»U.δPF  ‹rhWσ΄τ€ YΨl›fΕ0iΧ¦€¦¦:RRΙ~αŽβ1-Γ'"2\Γm”–—@£UŒ6ΰc 3f„’±R˜V·tεζmQ—'ΕΤp‘Μ±ΝΖJMM—\Ι(χ ƒR†Ž!. ΖAΜ8¦Ξθvƒ'm}ί„ΰ―+V¬Tνχύμ₯‹’qŸγ9ώσLΗμσΥΗ0π,ˆ…O¬γSpΎΟσχύ3d~p9ί—ίΩ½ΰΊ»+ηοΫΡ0 C ι"PgςEβ@A–@+7ρ9ς³ο3Δ‹D€GNBτι’FˆϋR;C’ÐX:iBΙXˆ…»GθΉ"·j‘’,ιιιΨd{+³‘qς5CŒώξ“’-Θα}?ϊΙχΧɘ\HP Ž‹ΪžΐΌ¬¦Oφ τ²΄­@C”‘ς΅ŒΛtν@ΗBΗl”Σ6pΑέγ’€m›Φͺυ«Έ―~^8W7ΚΡ5•x” .n(߁ύ4AΎ‚Ždά“d/11 ζ\Δγ §€€p’ΘpĞmECγΕVΌV²Z΄rPμIΛ{μ?j~δ‘B0y\gΟώZ&O~\¦MϋAaσ°8!IƎ!W]u™ Π}doΩ?>ηκϊϊωδ1b˜Όύφ4yκ©ΏΛ·ίώ-εη??Y.Έΰ\³dΆhΎH¬0ƒ@DθprSr₯fB|ͺΜW(‚sώ#?Q_/j¨°ΒΤ©ͺ€ΉΛΕΙ‚<œsλ Ž²2h‰’Έ0Dr΅=Ώ€¦ΈHυQrδ†Δ…‘+"@B₯QβIŽ8‡¬°o`‡HsΌf‡·HΝ< 㑉€‹§Ϊchβ(Ei%ι"ΩR‡<’&šκ4χΚΡϊ΄Q‚?e»jr€‰ρΗ0ΎΒ"?-@‘|8υ££αΛΕH^θk cm…`Slhϊ"α―΅½°T–.[&Ω0% Έjϋφ₯]ϋ2)V‘Ω!)p$‘δ1²Λαη……•±jvdδ\lΔ-ˆ@δbcγ·’’2© ‹Β RhΗ`–,/ΓͺΗ$@τY;‰ΜmXΑ™’B?6χS%†Δ˜γτα9Z·n%>ϊj˜X'ζι];Β™­δ먣FΘώσͺ>γQ£NΠ‘Όώ‡(“MX~_εͺUνίΏ‡Μžύœ~ϊi Z εCzΚΩgŸ!™™™±cGΆ?βYΊ]–/_.ο½χ•ΌρΖ32|ψ0•ΉhΡbyξΉWdεΚUϊ,6oή"£F+ΏύΕJΪXhΰΐώΠ’έ䍲oΎ™#G=;dͺ ϋΣ ϋ2 Cΐh’Τ™|qς£§ΌΞј 1#κδF‚ήβΗqNβΑΔςό(ΑdI”!Κu^§fKiψ20ΑQsFc\EsΒ…Π|8—o‚9(Ώ¨+œo Ι ›η€ΕΎΈ£#S>O;€/*Nܞ4±^p‰/§γβM^³ I©Kέ‚ΠeϊBΑGŒ}V2 βFβΐ±Σχ‡¦/ά ΕxΛ *Γ>Ž(’Ψ‘Μrμ·Ψ’£γ γ9ƒοW~ήf¬ςλ&#?IZ¦΅Βύ5/’tj47@t‹d„νtοΪE»А<Ιω’ŸR16Τ ίZšPhΌZe$£!’·}+4h €$˜¨†}5Ήhΰ@$ΟΝ@(ŽΥ«7b₯&H*Ι ±εΦIkΧ…ŸV[ΰ“γίeϊτOA8K@JΫhŸ8Ÿ΅n‘„#Ÿ/Οω hVΜΜl/_|ρ•ΌφΪ›Jπzχξ‘ώ`$Ξά[3 <θ‡Ε”šΪRƒόϊ½2yΩ­V+½ΟvΪ΅k 2΅JM’$€ΉXB?0ΆΗί=ϋέ£Gw˜‡Λ‚‹PΎ|ΡΎ9»P #eθoP%Ϊ—!`†@SE`ŸΘ΅#œ0όG΅B@ŠZ-O΄<ω"€Μc"Ν"Ή Ε€N*ΓzΘΑβ·£4$"α aJ\ΰp2n‘ΓI°’8Τc’¬κδL„ξš7ΨF­€ν‘@ίύ„ΗkŸH>H`|Χ+Gκ΄ „zΙz 4βeΛδ9'`~H8f&/£εMžP~©“7ΙW9βxUV"FWΖεγuιΪUzχν ⃉š*ΤΡPeΕ ωΠ rK"δCΫη0QΰJHΕ­ŒJ°υP6œΑ—―\-Λ–e dȐΑ‡β˜ 2£Žϋe₯ΕΪΗ ΰP Νϋ’ζΧχq$n]Ίt†?U”|όρ§»7 @ H̞{ξyΉμ²ί©#=W^yΉόγ–ίόζWΣrΉϊκ•ό²Ÿόέ‘ψ”o–₯\nη4kΦχςKOͺι’fΨ Φλ=:ίσ3ώ"5Ozθ!κδΏr%ύΛ•L“\‰tΠπƒͺΏ“¬¬e2xpg­Λv’£#5Ÿ}`ϋLΠt S±ψψ8τ©&Η~zϏY/μΛ0 C Ι"PgςεγqβD£€#0±qr#9β‡χ˜8*9Γ NrΖɎ g΄ƒφ…ω4½Ρ0Ί’«ρH€œŸ΅Gά.(LM:κ[ΤΫ$wRS'Ϊς‰ωό°}N"‘ύ ς“žΟγ‘m:zD±$6lƒ«2!ΆJ¦kŸ’vΖOςΜχψπ^…-“@,Π΅{>xglŸΨEF! Ϊ`½ΆmΫI—ž=$˜‚νͺΑ!$r”E£le"Τ#XHx,:γ*ԈθC{8|՞~Ώΰ;™7δcε²e‹‘]Z3[ινOFzͺξρ²XZ Β Σ¨ΗFΨ€_τι:昑ςςΛ…Ζ._;μP˜Θ+―Ό!W\qτ&tZ­¦)š¬™p€D½Ψrμξρ|έΊ πλzŽνΏT9t’ρΕ—ul\±8iγ2fΜ rΝ5WΒ?kΈάvΫ#0K>"ύϊυQί¬˜[όψΜh2<γŒαςμ³Vν#5qωΟλ03-™™@KQ!όχΤ$Ο^ΈηΘϊC†!o½υžj ‡ ; ε3υ>+– Cΐ0š>aAΪΫa’”lXω4œ\œ='gžϋδΟ«ξc–S β#ωb]_AΙ & %ψζ–Bεe 2Κ-w’ΫJT[³ΞϞܱ:Ϋζ‡ΪνΪv€ΛχGNqŽ&ωΖ«œσcc8V=•ι‹Q†0Gκ<±σGcxΝ>±.―ydBUhύ°2Ξν}ϋτDΈ l›ηvFυ'!cάθθXVεκΞ “sΒ)ΔBλSΜ©ρ’ίc©…³φζΪJ― hΒ*θχ†~Σaμ qΛ§„¨(BŠ|˜oseΡ’”@2€k.Β1,[ΆšœΥΘ ρ‹Ρ>ΔΔΔbαπc„jR«©Η/>;βESν!‡ Π~’t½ύφj‚;v”\rΙEj&d8‡Ž3°2ρ ]M8|ψPτYτ‰ΝΎ-°wfnNŽ|όΙηˆ Φ„ͺ/ό―ϊ¨ζμ…^Q3 ύ³HΜ¨]#!:ρΔαͺi₯¦νΌσΞQηj^?ώ8]ΩZΘμMΩpΞ_–.]&£G” /<Žφ‰Β .ΐBήUš-ΒΕqq5%wΨ°![N:ιρš3ή³d†€!Πτ€$ΐφb¬t:ώfϊkR\ˆˆιΚ©ωρ’Έ²‘+N&~Bα=ž+AΒΓΝi€žΘ§O©}‘v€„pŠ‚ζ₯€ε Χ Š•π‘ycΪς%œ’I*¨`ςν;„Ε3Χ&ΙϋΓU}₯Uu”ψ‘OΎ.ϋΖsj*τΚWΙζ ™γaY%P˜α=Αb^΅ ··%ΗΚώ>ωϋ`{>ΡΤόΓβΕX πίb¬¨|PΊwοεYΦ’!`†@ΣG zVΨΛ±2(')ϊ‘`0qΒqηΤΉ<ζ3ΟO,45²|ΉUΛΕϊ$€ ψɌ~LŒIΕ‰εΉ…N!ό½Φ`\΅z3Ή4J”ΕϋΒΥ~<ϊδOΩ=ίφCΛ ΣχЍΓ/mς0;1Υβ΄Λ²^ϊάε»±»ΆH΄˜|ͺΪggωQΡQΠ.9ί­ςR†y@@Uφ‘!5:t Pœš©υ …Ή°D‹hq1I%72ΗΕMͺ9υ„ΣŒ…Ηh8„J»HΤgοcΊ!.9QŽFθƒ-yyΐuκθBÝ€… %ςω—ίΚχ‹–Θ‘‡τ“cŽ>JHΎD"nŠ9ŽΤργσω;σχΉυ”OΑ€‹y,£–ϋUy.Έ©Οζσ"ώNΓ5RFŽ ΝUoψ‡ΝQl/Ίθ‚Ώ3>g2Ÿ|_(ƒΏGŸ|ώ«S^“ωŸ³ΰv,β€ύV‰Λ°Ό%Cΐ0 ζ@Ι— Μχδ4ƒΡοΘO0œHψ!‘a“–Α‘ΪΓΚ™υf "εqε]( ’ ΧF!V»­^΅Z6¬_BΉΘ§l~<ω’_ΟΩ7ν_@–;§G}ί7ΆQSΙs™z<κ?=Q“cx@ΗιΫσν0mπΓρϊφ˜ΒΙ{επ«Bh NΠΘc3šχβx3>.^Ϊ΄n•tmthIi`G0ΦhΔμΥ{#}ΎΨA˜άψΈρΗ`5(λ© ŠΔ‡š-š*ιΰ VΖKιΡ­3βX AΈ„w±)w1ˆ ΐΛ ’Σ=œϊ·@#”΅l• <>_@ΑΡFˆjΰδq ~^sv₯φ}ά͟ΚσrYFŸ*:&Ψπϊƒδ!>ΪθΡ£t?ɝ…„Ώ³v‚ϋHηϊ>ψP#άsλοͺž°/Cΐ0 &…@Ι')DΤ„IΛ³Νh₯xξ'&WηΈοB6€Ωΰ?%*ΈΙ£ξqHqΈ¦-ρΌBJ²r`RΝFA~šsΌi²K‹ψψ]ξΟ²N’Σ,°,΅jš<»b œ«ΟŽl‡]U‚ ^ͺ ”§¦…+ΉεξΓ¨οM κ F’BB…Ά\\“λ³ ϋBm©ΗJςEJ™––A9cca’eΏP–8“/‘e( F!Τ>κ?§ωc›Υ§y t[λ¬ηΪωΆCΰΠΎa}ŽΌαgJ!„ώjθ_xd¬ΓηΞλθˆήw#90ί»#(»»Ώ«^ο¬ŒνΕOpΪQΪYύΰ:Αη½zυ~|Ϊ™<ώ†€!`M:“/Nΰš¨-ΐ‡Χ~β„B__†Χ>‘8T;nk-\γ.N•Έ€ΨθDΟ,ήVYΥZ$j{BΓθ\0$q,™<²}Κζ9I˜kΧ9½ϋΎ‘0όΚΰEV‰•„M‚Η•€ŽlαšyΛΊVΥ'jπ8^η7Δ»ώ–‹#@$slΣ“Cί7ϊEΒη+ρ#|£ΰ§–’ά±€’₯βBE!‚?Η€n oh'Τ(ηΧM σ΅q/šW4gξdYΚ Yƒ‹%υ΅γ"ΘπθP‰‹ΗV<ƒ$7/_Ύϊϋ†GcΣν8‚O 7μ.ΆŒ-ΘΔgό λ»/l‹m2Υ>ίΧΆƒε6δ˜φ΅ίVί0š. ύŽmΊHξΩΘκLΎΈšŽjHθ,Ο€š«ιΠ s¦}½G₯aR£5\κ₯%aQ:Aށ*aΠςp⧉’„A/‹ΰΰŸ΅b•Κϋ©/:c“ΨΠo§ΒΈR’Mi†γž€;θΨ~€ιD"zΠ΅]ϊ†mh’?*MΫcaža¨Šθ #αrX:lyξ>Ž{=EG—\q—Η'€υ‘Ιs’Βpβg°Υ̎ΰ5\6lΚ“₯+ΦH9ž'΅_eΨσ1΄p‡ϊΫ‘ΘδIJ°&ρ@φΗΪ6 C © ΰ•Me<au&_t‚©aδt>8M˜Μ©Abςέλ'OύGr…LnYΓπ`nπGŽjv@Κ資rtd`QŠ«Dθ…¨˜riέ³œ8*^ύ ΡΣ–M8’AΝ΅Rό8ίͺš€ΑχKsA(x]υ!™ χ“=5HšιΕθ8Π]mŽγbσΎmζ»sοζΫσεh:δpΉ=Qϋ6i©>Ρκ“RRΰ›Εψ\ˆgΖ§’ΈΰG/ꬴ1ŠWa45βBqs]πmγ χ˜G­u]¬’ίΌ£ν‡ ‘b¬Όƒ^P±κ₯³ =r0|Β°Š€—€š~i\ψŽg| ύ²³sΠΏΰt―¬}Cΐ0;!π=Ž@LΓTέΥ£±¦±τΏN‘&H( ¨A’F¦fς₯fnυ•/Ώ·εH&hrΫ]½κ–ώ3bAΣ ‹‚τ#4λw 5°9γFιτ=SζζΨ’žΣ‘?!>ΖqΎϊνN₯s뜜ΝΠfhτxXK†€!`ϋŽ]„h%ZΏ~#\_Z  w›}jv‹@Uτe‚f β| ‚ŸjWϋ`Tνi .Zƒ˜μ©€Zε‚εω[5δͺzΚqW8έmΝZejH­΅S9ώ6‰3Ϋ©«C;θΈ³ήέ—S%φxΦΤx‘xuλΦE:s>lυ€ 5 C Y"’ϋΛ.Y’₯ΰr_[n–€4ΐ λHΎHά€]=u7@oΩΔΆ[Υ―ͺ“έχo/Šξ^»ΊG₯vVhOjξ25eψgά£υ? 7‘ΤHΥβ\Qκσƒϋh熀!`uG€οVΎcωε;—δΛή΅uΗsOjχΪ’!°Kδ€t§7Ή¦Ζλ@φe—Ω Cΐ09|·ςΛw-ίΉΆ°©ώ¨‘―ϊΗΨZΨ'ΰόo>^ϋ„ U6 C`Opοښ‘=©geφ#_{™Υ0 Cΐ0 C Ξωͺ3tVΡ0 Cΐ0 ½GΐΘΧήcf5 Cΐ0 Cΐ¨3FΎκ U4 Cΐ0 C`ο¨s¨‰½oΚjυ‹Γb‡Λΰ [!YΏ˜›tCΐhDΰ©ϋ£Λ ZŽd#κ|ΣκͺiΎšΦσlΆ£!ιβ~ž111UέΏ2Ž$,˜˜ν PžΤω#eΙΫD­!`Τ7 %-ίΞ™#_~ω₯ž[ΠκϊF}Χς|ν»ΣH ©ŠŠŠ’Υ«WΛ[o½%S¦L‘>ψ@ςσσ«φωΜΚΚ’{ξΉ΄—)QΪcώOέ#$|aέ|σΝςζ›oΊ ΚQ§A kΤ ΎF‘uΣ0š0|7ρΡ‚‚?~Ό\~ωε’››«°Φxo5a Ά‘59ςΕRe­ΝƒΫΟnυ‡Ο›―O>ωD233eμΨ±ςςΛ/ΛρΗ―η$d|ιδδδΘm·έ¦)ΧάJ‰ΙΏxx€φвjίσZ-ͺλyŸ)22R”½|ωrωΓ¨Ασš‰“'*χYί’!` Žήo|gρΣW_}UζΝ›§€ϊχUƒχΗ”&504\Dx˜ΔFET}b"Γ%ϊθ#5^ψΫίʌΟ>ΣΝjΩφ>¨&ΟnΈΑ^t»}²Vΐ0φ|ΠέbκΤ©rάqΗι{ŠοNš™Χ£{wη„ο'€ύΥ°ΙωIšωΒ6Φ4ΝZ²^~υβ|Ω³₯”Vξωω%‚5ςα/“ΎR₯΄ΌBIQ&βPhΔ*P†¬Ÿ?P&ωΓTm+*X;D―yŸε}b9–£~?ΈΗΌPδρCσgp £lό«D9Wm²•¨ΗΆƒKΫωξ ™βK…*t’!/j©˜OΝSǎε²Λ.Σίρ^°`δεει gύϊυrΨa‡Ι°aΓdΤ¨QςΒ /H=δ±Η“νΫ·«–lθΠ‘ͺ9#»ϋξ»εέwί•]»JBB‚,Z΄HΆlΩ’ΔλυΧ_Ώ… J»vνTϋΆm‘¨ >\΅l7ήx£3š-‹ŠŠΜΉ»‡kχ C`Ώ!ΐyιβ‹/–K/½΄†Lζϋ‰‡οHK ‡@“!_²Pό˜~90]9g¨Πλ¦dλΎ©ίΘ-―Ο“/9F" γδœ_\&ωE₯’-QaR’E²TςΡΠ–ε+JŽ‹RΡyωΕJXž‰„ΎC¬“»½HΏΌι xε m$ƒa'XP@IDAT‘Ε•\#Ÿ€Œν~ΙfΤ#=k‘ΐz‚>”Ήςφ½{ψ²ΰ°ΤZ 2ΨUƒηορεBRξΥκΏ…F*==]M’—\r‰ΠdΙDMΛlƒI’Ϊ΄σΟ?_ε2Dξή{ο•N8AːΰqZ}q‘nFF†‘ ‰—_mI2ψή{ο)ω"ιc0`@•ΓΏfΨ—!`†@³D Ι‘/Μ΅²ƒΔ‡qLR“cδδC;ΚψηΏΦi‘ΡQςΪWKεΚ—Hf$4' =“Ζυ—!=ΪȜ•›dΒ[ίΙαZΚsΧΙβ’ryν”Ύ’½­Hnb…,+*“w Fφ‘HΘ_»9_ξ~}|•/ΕΠfι,7œ2P"  ;vςt™ŒΊGφl«?,jδ.œ2W¦ώ)Ρ?υ[™Ί2OΙΧιSδΪ“H ˆI`0‰h–ΏΚ=4‰5I$5Τ2y2Δ£']$gLό-΄oί^I“7C&&&ΒDY¬χg̘!ηŸwΎ$$&HΫΆmu΅δƒ0R΅k-[ΆTβελ2ί?'ϊxEGE©Φ-DΛΉG-Ԝ]wέuς)Ό%''«IΤΧΣ†νΛ0 zF€οœ'žxBέ0θΑχ͎|Gέpύυͺύbž₯†C I9ά+lP~TpΒ’Αpk~©|τύZ929Jbc’δΫ¬ rφkσεΥ3Κ›—'ΏΤNŽ|αkΩ‚ΗΏ—δIQI™Ό}ΡyθȎ2φ©―duNΎ|tρynTOΉςύ,ΙΪΈU'ή›_ύF5//_0L¦œ7T–ζΚ„7ζJR|Œœ–™"ΣΎ[«}a?>Z°NF·K’‰±ςΘ;σdΓφbωπ£δƒKŽ’εy…ςχκ ΪpOΏ‘·Δ—΅T$_4nάΈQMΤlRE3ΰυx±pyuHQ~AΎΎtxŸ/#’·¨¨h½OΣγ­ΎU}Αήyη‘oVq‘sšχeyd]&ž'’<]) 2HΝXοή½επΓWχΙ§Ÿͺ―λΨ .5;7 ϊF€οΎ›Ζ§ώͺΟ=χœ<όσ2mΪ4Νγ‹|Φ~§ΥwΏš»ό&EΎΘΫ#ΒCε₯₯›εάg?“_?3C~υψtynI=^Ζš)’+§ήY:€'ΘθC:Š€DK!L–λ υzaεVΉfLι’‘(½Ϊ₯ΘΝcϋΙ} ²eL–§κ$w.Ϊ$Ϋ K₯ ΈTnžΏQ~~X'»RωΛΌ rκΐφ1‘Sϋ·•?~»VI_( 4χ_εŽŸ/ j₯Ž8βυέΊπΒ ε»οΎSŸ­Y³f))c™ΈΈ8]˜·9―†dj¬ΌfŒ7ψ’bήgp”§³~dT€ζ± Ύœ‚Sόΐ|‰Ν•tΆ'Ρc>ύΒ~χ»ίΙ駟!0ΊΓ©Υϋ£Λ±sCΐ0κΎK±θ¨K—.ςK/U5υδ“OJώύU館Ίi'υŽ@“2;RAgφ^-’ε’#:JIYΉŒ{ε;yρΤ>r̊$B9Ε2wΓvΉς₯YR _Ω€–1j*€VRd˜ž—•\΅CzFΡ? ώX₯ψΛ'ΰg\IΉ>`α±’ ?―ΒβrΝKƊΚXδ­Λ+^ν[J(|»–oΘΣπψ³BϊvL“[‹€OLΈ<=sΉ<=k%|ΖD’Πρ‹a²,Q3<ηγ_οΏ±7ΐ— Ι‰’»λ»τeB§ψ₯K—κ5ύΊ˜¨!£ΟλψΔ•Šτ#9£Σό)§œ"wάq‡œyζ™ΒP4!²`$EΠdΕGGΘ’ [₯eΐCžŽρLe5TΰR"`δͺΖ ςΒ2ΙΕjΚτ€X A½-8'KŽ”8Θ›thyϋυͺQ›8 ΅Δ#ΏΈ΄LΎœ\‰Θ0^ VΩ4A²–cβωδΙ“•ΠQ6WYaŸqs¨ήg;‡Ϊ0&šΩ&λY2 C ‘໇D‹hrε5ί[ό£V^7τ“¨n―I™9,mνΠZΡΤWsβθC;3>„Ό:s ό~BddŸΆrη—kdζΒ5RˆrΣΏ[-CωX6l)ԐΛΚ(!p² u1ϋΙρ*)«Ά-δμIr[seb‹-Z›'wΑYz˜4[&Δΐ|U!Ητm/7ΟY'ΧΟZ##ϋ·—JjΪΰTkŸ ™ψώBYŽz›A'Ύώ­ά Η}ΜΞUνΨɞ#ΐ  «8PAΌψ²ρΩQσE’œx•dΙ-jΠXŸςψα}>y_.Έ.ΙUνHψt৆¬ν>όπΓΚ‚G:ϊ“ΈQ¦%Cΐ04-–`Uφ!x?λςwΙq– …£Ž7C–BλΕ ͺ$`”Γ2ύΏ¨BLΩIΌ.φ‡Ιbu2}ΧΝXρH‚HMΓSGΰΧJΡWL ό*ΙΜ‚?ΐQ΄“j™κπS=`R3ΖD—™Ψc°† C`ΰŽ\p”•΅«΅{¨[ΕV΅bu@ ΙiΎΈΪ:ͺi+b8•,“d©„*ΧρΡ‘κcUbΖž@Ε^~’'وΖuΥ_ ¨Οkή§¬ς€¦#δ ΄*°z±:ώ2Aδjφ‡2},/’.^3Š>IžΧΈ°ž₯ƍ})˜HψνΉ6ξgi½7 C`#ΠδΘ΅U$B΅cQqΓ―+ΚQΑ“£'^Ύ~0ρb^π΅―η΅[$R>ΟΧίY|™ŸͺηλΫ±q"`ΎσΉY― CΐhšωΪhJΊ‚n:”±§$]uIu­W—Ά¬Ž!`†€!`Π&fΙ8ˆ`$ϊk2β[Χ Cΐh”Έwm΅ rΔΣFΎφΦςn`xˆHψζ"š<Νx΅MΒ»©n· Cΐ0φΎ[ωŽε»–ο\Ύ{-Υ/ΝΖμXΏ0šτύ_τKOO•U«Φh„xΎ,lΓώFΪδ†@sG€d«ΎΧ―ί(:΄S8ό;ΈΉcS_γ7ςU_Țά}Bΐ/JHIIΦΏΖ–,Ι’Φ­3°}S¬†ωΨ'αVΩ0 C@ ©‘/―ΤΤΒw.“λ…}νwš\œ―ύސ TDœ―`θσςΆHvvŽώuΆ³Υ¬Αeνά0 C`OΰnjeπΔkOkZΉΊ#`δ«ξΨ5«šš|y°ύvAώڎ†€!`ϋ†€ωxν~u©mfΗΊ fuο`/‰‡ή4 f€€Η6ƒ‘C΄ΥŽΕc°Nμσ?ΨBvί0 Ί#`οΨΊcW—šFΎκ‚šΥ1 Cΐ0 C Žωͺ#pVΝ0 Cΐ0 Ί `δ«.¨YCΐ0 Cΐ0κˆ€‘―:gΥ Cΐ0 Cΐ¨ MŽ|Ρi°ΎλSv]žΥ1 Cΐ0 Ζ‡@“#_εεΔYϊ£}ΉŒ–&w^ύ°˜]σήΞ7rfŒ©Ϊ²½\_ΏZͺo§¦¬=m+XŽ†€!`†@ΣA Ι/j₯ΚΛΛεώ&Κ©γN•―ΏώZ£φϊ½Š‡²\xx„nQγ SXXhΥF’αααzξορQSFTT„Μšυ•uΜ1²bΕ ‰ˆpϋ Rndd$d†Χ p¬Η<–c›^^p[Ό\u,†€!`†@ΣF I/’£ˆˆpYΉr…όω֛䓏?wήy[·’!αbΪΎm›ƒHνM›²₯€€D‰οq_«νΫ·+9ΫΌy³lCY£ΰώ€εζΝ™₯uyWTT„vWJ^^ž)ΦρD/''GΦ]#εИyyΑm­_Ώ^rss«κ·g熀!`†€!Π4hδΛi–Df=[.χή{Ώά}χν²1{£’ž²²2Ήξ†λδ/·ύEnΊωΎ}+9‚σeι₯ͺϋχs– .8Oyd²΄i“&cƎ‘?žώ#­T(΄\>A©%3Ώœ©ZΆnέ:aΣηvςΪk―)ρ*((‡ž$νΪ΅•Ν;Θε—^΅eά?λ9΄uξΉΏ‘ΏύmΌtκΤVŽ?αωζ›―΅Ÿ^;ζΫ°£!`†€!`4==ω’΅[Π^=ύΜ3rώyηΙ)§œͺOκϋωσA†œΩš‡œ C–·ήzO^›ςyμρΗ€¬¬\Ν‚oΌ1UύΉ>ωδ ‰•‹.Ή²M5 XΐgŒZ¬œœ­rΜ1Γ€c‡0GΞ‘ργο”ωŸŸΛκΥ«εσΟgΘυΧ_-ϊΧΏεΣΟfΚ?ρŒL˜xŸΆ%И½ύφλ’œœŒ~Ό+ Μ•—^zIΫ&‰΄d†€!`M&@ΎœΙqω²e2γΣι-ΤLeͺ*--“3Οϊ…œvΪι2rδHωύeWΚ—3gͺ 1<άi΄Ξ<σ,1bˆόξwΏ“•Λ Ν‚τΡͺ­‘’ŸΦΊuλτ—qΘήa‡ ‹.ΊX²²V)©ϊμ³2`ΰa2zτhzδ`hάξ”ΏσdΛ–-UζΗ_όβ—2jΤ θΣ/%wsΠ™ίΘWΣώŸΝFg†€!`FOΎHXΰΖ₯¦;hβύχΛΙ§œ&qΠ^ύί‹/+I’¦ͺ¦GjΉ<‘RGό€¦) Π ΊWAQ?™ψτθΩώ_m ‘ͺ¬HyrE²ΆmΫJR‹6ςΨcΙ_ΜΖρQιΪ΅£lέΊU†sβϋΞ䉝^„_114›„³.†€!` Lhά‰δ‰Δ…dFύΈ02ζ3Ο“°Ξ;K2HIΛ’HΡlΙλΓ"τ͊OHP’ERδ5^Z _>hͺo‡eΨvFF†ΚσuΌσΌoΧ$^”ΗςΨ ~jΞWΜ“0φΧ >ΑΛΪΟgΧ΅νŽ!`†€!pπ Πθ5_Jr—ŸΜ8bD‘“xπΔMFB¬} ΎοΫΰqgνxRP»Ξςƒε5–σ©ω"Ž΅±uΟ’:ί^§uά=ͺ{[~χ­„!`†€!°g4zΝ—ζO/– Φ0՞Θ=ιςdΙΛάΩqgνΤ–ηλν*ίί·γξπΔ‹1ΧV¬X©¦d>―.]ΊHΛ–-Tw$xτΡ'° !I~ϋΫ T›Ι ψ¬ό3 ~HΞΦ]‡OΒ έ[Ξ:λŒͺNψ²Uvb†€!`ΤM†|ν?ωϊ –ΧLΌφη»“αο³Ό—Γ<'ͺfž/kΗΊ#ΰqž=ϋk™<ωq™6νsΔ^ΛΓ"…$;v1ό^ „lφμo±΅΅j0έφN5ΤψΌό3γ–PοΎϋ±ΔΕΕU2ίKί¦ΏΆ£!`†€!°ΏhΤ«w†Ÿdkί§ΦΓί£&lo7΅φ3λQ―ωa Φ¨(η@_»M»kj©ξΊλ^읹Ϋ?έ%‹,Ο>;{bn–Ϋn»Kw!ˆŠŠΒ1Xτ-‘x……E²jΥjΙΟΟΧΖ)+;;[5^Μΰο E‹$―X}Ž ˜»zυ-럫^Ψ—!`†€!P49ςΕ‰Φ;ΎΧΖ«Ό’Όj5ββΕ?ΘƍχŠ€EDDj}nd4QΛB³ΧwίΝW"Ζ6Ω‡]₯ŸΈ΅«*Ν:«―fΛ’EΛa<!BN•nέΊBλ5ZΞ;οWΐ=_Φ¬Y«Ο€Οƒ»„…†Ιχί/;v@ΰέo;ή›0αΉρΖ[°ϋA‘–gθn¦~ύΙρǟ&W\q­<υΤ3MΣχ^Ϊ¬’ ή0 C`―h2δ‹„‡dˆ{+ž{ήΉˆύ΅R―ιHOmώσ­ςΡG"? Z”Gdξά98―†€‘Аo'DΩΤv͜ωΉœqζςΏW\ŽmΞ”ιΣ?‚Ζ‹qΖVΚ-·ά¬ζ―PLώ*' ΛkΪZ‰‹#˜HΔήxγe9䐁χQRεΣ§7; Έσ@NλuΪΚΔΔΔ€ϋV΅eO=υ δv–{ξyΟψ«ΰjvn†€!`μwš”Ο8–”–•j$ϋ1ΨW‘Nَ”…Γ\΅XξŸx― >b°‚ψη?v4εU1Β³‹“>γq5“'u4M]rιοΰΨύˆτλΫO‰².\ΈTϊυλ'O?ύŒΦ+Cϋž °Ύσ? UςFΩάw’DŽm—cYK5 ΣόΆmΫ%%%I1γ]βΙgB‚ΛΨjLžΨκ…~Υτχb±ηΆOώΌ°°Xzφμ ΪΉΞ\ yΣ§-Ϋ"9κ¨αZ^ Ϋ—!`†€!°ŸhRδ‹Ψp"tψ‘2eΚ9ωδS0q§€HUΚηŸΟPθ” a~α… 59DŽb—&ύόœ₯8ξ>‘‘|7*iε ¨Αδ3άΊu|ΈΦκŽΌf~νDΒΕδŽ5M‰EEΕΊu«*_=>Η€€Θέ^ΥFmyvm†€!`μͺmnϋCΪA £›g5b„vΨaςε—3‘a ƒ_ΠΥhLšτ¨Nβ°,ʚ΅kτ ”ϋR¬ήsχxωΓώ ·ύυ6\Oν 7δΞμ”)Ύι:iίΎ•ά{ού2ύγ8o•wΙ*gι₯κ/Τ―_yβρ'@ΔξPbpΔα‡c’O—W^yYrsrδΕ{I&NΈ_ώς—Ϋ”<ΠTϊcΝΝAδξ‚Η€K—ΞpŒ=2©5$Ρ’ΧsΟ=/}ϊτ—~X¬Δ9xN±V­ϊΘςε+u$jΩΩ›j˜&βΤ)Ÿ~_LtμίΌy«†―`– Cΐ0 ϊB IjΎΈςπδ“OFx‚Ιz€Φ‹d,ϋ(^΅J±δ 9?Ι2δΐΘ‘ΗCσ+C‘ kΫΆ’/š9‘σΓςΧ^sœpό(™?;ωγM”nΠ‚MœxΏNκ~«"šSS“`š‘ϋHRλEΣζ'Ÿ~*33εύχίS2χΩ§Κ’%‹αήΎb¦bW?π#Ž8\Ž9f¨Όόςα Ÿηx(κ€ΜΎ!W]u΅τξέK}νh&φfάμRΠ‘CšΌχή‡ Ά•J²6mΚΕsi‰k§ε3]΅j<πΐ$Έήcρ>žy©τοίW»Bςη5g»κ›ε†€!`uAΐ©vκRσ ­Ca^^όyza« x˜?—3fΘp˜ψv`ΒυI'a\πHν“#YΨΏ±²Bύ‰|9Ir ΅Ϊ΄)p"\π™φφ»ςΓ’%0SΞ•¨θ(LόeͺΑŠŽŽ€6f™άzλ­ΨGςNμ£šφ+Ϋ‘œΡœφΡτΟ€GžΈη·gηΥρΧΈΗζ7^‹ΠϐeΛ–Λ}χM’9sΎ“_ϊLΉα†k4\Ÿ]›6­”\‘€‘]sΝουΉM}νM±φrτΡΓ`ϊWΏ/>O:܏s’dfv’ηŸI‰Φ]wέ,ƒͺπρ²_‘!`†@}!Π5_’!θŒ}ΪΈΣu½υΦΏb’m‡Υm³” L%K2VRR‚j:Μ%%ΕzΝ+j@Έ:rŊ2jμiςՌ0™wΛF¨Šέ ²>e…π!„›»E»α:˜¬{>ζεmS―A‡ ΈGŽΛq²ž„yΞ²ΉΉΉ’»9WZ€΄Π•Ž;Α[}›<ςS‰h·Ψ'{φmσζΝ*;υ+aβdΩΰvό8ΆγάXΫcαΙ―χ3_~weχ΄œο‡ Cΐ0 }A Ι‘/‚ΑΙ6xB >η}^ϋ ٟ;₯G΅ζΓΧayŸ˜GRF"G"E’ΖDYΎMξλπΘz,Ο{$pL¬ΛλΖ’ςΥX°²~†€!`?…@΅-ζ§J5²{ΑΔioΟ9Τΰ:ΑC'Yς„‹ωΑδΙΧα‘ΑAkσ*_ΦΧχΧΑςν|ΧWΓlΧψΨCΐ0φ{Ηξ z{_·I’―½‡aΟkμ ¨MΌ‚₯οIύΰςvξπΈΡLkΙ0 C`!@‹ŒΗξ?©&ι§hRδλ@2χΩφO=ΰ¦r//o ₯ζΐW― Crζα¦26‡!`.@ŠΐͺςT]9~ΰϊΡΌZntδΛ“²tž3ρH?,²wjF|™Ϊ2ΈNν{{rνεΛ n›~]ή·«ΆΌΰ:΅οΩυO#ΐθσ99›±S@†LδjEK†€!`ϋŽέd 5θ4 2n©ώhTδ‹D‡«Ή:‘ΎSήyyόΡpclF7§SΌwhχδΘ;Θ‡ΨxwΦ6ΉW22NΫg€|ί?’-&OΊΈΊ’e*QήO™,ͺnS»GΌ‰5^$^έΊuΡΏΞψ-†€!`μ?’£S<»dιΈŒθίΑϋ―“Œ@Ψ_‚3φτΌΌΌa'AώΈmΠςΛεΊλ•h?zχξ W˜ξΝxσΝ7Ι»ο½+3Ώψ[Δ Πΐ¦Τ‚‘l‘ψά~Η_eϊτιrΤQGW‘6?VjΝ(Ÿ‰G^{ςδσvΦ6ΙίΒ… t?Hn4mΪ;ρ½ΈIsp‰Ι“–ώσŸΨŒ{€nS¬ n',Μυ#Έ<χe‚ϋ©mΐ/šmHbθΗΜ-€ZβΕWc±Cݚ2 C I#ΐ9‰ΚΖΙΜΝΝΓn!-ͺζ&=π8ΈF±½ Ι˜^uΥUJŽ6n܈K¨¬[·„κH ?ΚSO>­€λιgžͺ‚”?¦7ί|CώσςΙΫ’§€ˆ7\KEEEU?4*jΦψ!ΰ²vΫΩΩAΰB%??_ ηŸ<ωδSΊδwά^E’’"δ“O>‘—_}E7ίφ+«:‡“`3)΅g+' aτρΗXž}a?›S"6τρbΤ`Βڜ0°±†€!PίψΉŽοZΎsωξ΅TΏ4 ςEH–ΚΚΚεΡGΕΎ~η€ˆ*2Τ}ςΙͺ‹‹‹ΔvBΗHΞ¦ύρDcΟΕΕ‹cSζ—εΟ>­Z°j8w(Ρ‘£αΫoΏ%“ž$Œe΅zυjΉβΚ+”XQ6ӏΫ&YsΔι£ιΛΠ‘C±eM˜ <o‹’/šΧ­Ϋ χMΈO~h’n5IζΜ‘ŽΤE`oΘyrsΗL.ΉδbΉϊš«αXΎ&Τ"Ήθβ‹dωςε zŸš53΄ζ“¨‰4―ζσΌm€†€!p pοΪζ4Ώ(€Eω"+§&θˆ#ސŽΫ€m—plDrž‘‘!G9D5CΫΆΙ£="γƍ’/V©ά-}.Ώό₯}ϋ 5›΄α¦L&’ N%S¦L‘O?ύ\žyζi9yμXΘMSEV»mš:ιzDωˆαΓTVII₯ΌπΒ rφYg,Ε(Azβ‰ΗεόσΞ“ξέ{ΘϊυλΩ*>5ΨˆŽΫm†΃Πφ³πmκ&£©νŒŸγyT>›ρ₯ΌωΦ[rόρ'hΩoK†€!`†€!Π8hδ‹Π’,ΡlG₯Ο‘·RΔ™G’ΕtΟψ»eΰ€2|ψ-7}:6ΑnΧΎV#€¬Ό „,Zβ΅>ρHӝ Ÿ~κiέ„;55UN>ωTΘtfGΚ­έΆ'PTΝ•¨6ŠD+Ώ _N9e45"ί|σ΅3Ο<‹"΄ ΅a^›Ζ<ςΏςς hΌ.—Γ  ςvΆ,]šGσm ‘§I‡φνAπ†Θƒ<ˆ} “ͺΜ‘¬oΙ0 Cΐ0†|Z’ —άI˜Mq“™,άgρΊλΧλ‚‚"Ήιζ[°γfyζΩΙσΟ=§&ΌόγοͺΙς$Θ1QbFΉnϋ κ0Ζj΅θ:CΣζ‹/ώŸΜš=KξΉ{<΄a± d₯θΟ#²uΫ6ωχΏ-$fλ ωzZ΅œœœζO’Hϊ₯‘Π‘X²_μ5raa!κοΔΦ""άΖίeϋ6 Cΐ0 ΖŠ@£"_dšλ4‘PΝ™σ­\sΝ΅J¬ΆΓ,IRυπ€Irκ)§HλV­»€oMο1ΡwŠφtdӟn‘·ήzW.Z$\Ή͍°kšYΗ΅νœΓ`ϊ|γ7δΩΏ]nλJšΆmέ¦r/Ώμ2˜Ο‡ω2¦ŽZ―c‡J ½ΟΙVdD€Lžό ΜŸ?cͺΤΕiΠΎ₯ΆL–?όύYˆΌirνuΧ©Σ>‰™―ΟώX2 Cΐ0 Ζ…@£ŠσEhIX’’’$6&DF`’[!?<ώ`‡Λ_ώr«Ζ…"ΑΊZ―aΓ†hyπ+h½ΦΘάΉsΤΏ«& “wήy[Ίtι"£O:AΪ΄i+7ίr“ €ω’&Hš5«Ιšk;.6€M4ώΤ“O=)­ΰwvί}χΚ¬¦LLJ’[τgτg—h9ϊ’qΥγI'V }Θ|’ιςτŸ%S§Ύ&&Nr˜AǏ4v[εαΙ“εŽΫο@ψŠžπϋšΏ―7ε,˜1YΗ’!`†€!°W`₯u…j…œ Šˆ½’a…χ !Π’όX½³Ρ¬QTΔm^L"ω`·in€Œ#*?’₯ΘΘ(\WΗ"γ=ΦsfŚ ?†x ?˜ Ϊʐ…8 Θ¨ o;<<" +―n 5FΥ―ŒΘψώπœ$.Έmφ766 Ϊ­ιˆ6 Δρ6·-$6::A[ T†Y@ί+Ρw‘%ι ,˜€ΨK\ ΪПλ ~1ξ€ΨγΩ`·† CΐhpΞ‰‰‰ΡM >bΔ΅ϊ»ΰpžΚΚZθ=σaψA:„F§ω"Ž$1LœŒiϊ ―rΣ|~ρ‡R“dνΚdΗU‹”ε&χκgU}]%RO‚Ϋζ²άππ¨šm3ΣkΜxNm“Ο+—Π$H_Œ€΄h‘?τ΄΄4%ˆee₯ZžυI,IzH ΩOŽΛ’!`†€!°§pξαRPPΛΚxXŒ–β% –š`λΞʳrϋŽ@£$_Α$fW$igΠΧ Ύ_›ΠPγLœ‚ΛΦ–Q»npΩΰσΪυψΧWT8ωϋ+©ςν²m–w²Ϊ?­ΆŒ`ωvn†€!`μΜ'\m?uκTyυΥW΅Γ+]xα…JΎvZΗ2λšͺ‘zmͺρίρͺπ―šO™j·[ϋΊ>Ϊ7™†€!`MώΡ ΛKVV–œ…8”>]tΡEδ›V™=U"ψΊvάw₯ζkί‡}πH0mΦΑσ,¬'†€!`45ψG<γYRλuάqΗIλΦ­Υ²’››«y=ΊwwNψ i–FιpίpπXKsΈχHΨΡ0 Ζ…Θ§o—wχ½g>­/$hό˜Γ½G¦ώfv¬Œ­Cΐ0 Cΐ0ͺ0³cvb†€!`4=¨Υzβ‰'°ϋΝfΗ‘C‡Κ Χ_O‡γ­Ζoz(\#2ςup=λ!`†€!°ίP‡{¬t7nœ\{ν΅5δ2μDb\2†$͏–3;6ΦΦ’!`†€!Π PλUŠ ͺάΕε₯—^ͺjϋΙ'ŸΤ0G ΈZΫ¬ͺΤ¦ωͺ7hM°!`†€!p @†mνƌ#?ϋΩΟ4Θκ駟A½‚ή5Λ.ωj–έm†€!Π\ φ‹AΌγββδΖoD€οiΩ²eν…š Λ8λLΎπ,α w° ΓϊQŸπY[2 CΐhΌθ*πν:»ͺpκ.ΑΉ™άσ¬ωβdν’²Έ[Λ ‰€°†DΫΪ2 C ΐ‹ΌkMμ₯^οΉΘ:‘/Š·ηΆη [ICΐ0 Cΐ0<ΆΪΡ#aGCΐ0 Cΐ0#_ ²5a†€!`†€GΐΘ—Gކ€!`†€!`4FΎdkb_€ƒh₯-«έ­!`{‚€{ΧΪςφ=Αj_ΛωΪW­~½!ΐν."##€°°P—Ds› K†€!`ϋΎ[v‚οZΎsm«‘ύ‹οΞ€Υy΅γΞ„Yž!°ΏΰΛ€ΣΣSeΥͺ5―/…ΚΚΚύΥ„Ι1 Cΐ$[₯₯e²~ύFιΠ‘bβίΑPύ €MP?ؚΤύ„ΐΪ΅λ$'g³΄n!±±±ψ ΝΤβϋ ZcΝš©ρ"ρJMm!mΫΆiζˆ4Μπ|5 ΞΦΚ>"—·E²³sτ―3μ­°¬Ί!`†€C D­ ΄2€€$( „€‘―ښΩ?p2K†€!`ϋσρΪXξ©$#_{Š”•; ˜Α…ί7 &Ž€½cφωjXΌ­5Cΐ0 CΐhζX¨‰fώ°α†€!`†@Γ"`δ«aρΆΦ Cΐ0 C ™#`δ«™lψ†€!`†€!Π°ωjXΌ­5Cΐ0 Cΐhζωjζ?Ύ!`†€!`4,FΎokΝ0 Cΐ0š9FΎšωΐ†o†€!` ‹€‘―†ΕΫZ3 Cΐ0 fŽ€‘―fώ°α†€!`†@Γ"`δ«aρΆΦ Cΐ0 C ™#`δ«™lψ†€!`†€!Π°ωjXΌ­5Cΐ0 Cΐhζωjζ?Ύ!`†€!`4,FΎokΝ0 Cΐ0š9FΎšωΐ†ΏwμΨ±Cψ±d†€!`Τ#_uEΞκ5••;€²²²F[$?΅σx];―F₯ύt"όX2 Cΐ0κŠ€‘―Ί"gυΠΠ ­ω3%ω©ΗλΪy;λ ‰[EEΕ^k―ΌΆ+/o‹¬_ΏAEϋΌ΅cy†€!`†ΐ¨9«νͺ”ε h³ζΟ^ζΝϋNŠŠŠ΄$<‹ύ ωžό”——W•+))©*ΗϋΎŒΧŠQή₯—^!ί|3GΛ튈±λx$|lηξ»&ηŸ‰dgoR XEEe™σmQ°―ηΪ˜}†€!`@ΐΘ—ύ Zv€όόλ_ΟˈgJVΦ2νηΆmΫε»ώ&Χ\s“¬[·^σΦ]'—]v<πΐδ*Σ£7z‘'FωωωςΤSΘΦ­[΅nXX˜’(ίƒα΅k^‡ΛG ’±cO”ΨΨX-ϊ駟ΙΥWί K—fUΙaY_ΟkΛχνΨΡ0 C ω!`δ«ω=σF1bOvŽ=φ(Υ,mΪ΄Iϋ½jΥ*)--“θθα9Svv6>ΉrάqGKLLŒζmΫΆM/^"+V¬”²²2!Ιb s?y_nΩ²εBΩ$IΑiγΖlY°`‘π~iiiΥύ£>ZN?}œΔΕΕ ΅lΛ—―I“ώ#$€ΤŒ±ί”Ε{¬»dΙRΙΟ/Π<ή³d†€!`„†ΐΑˆ€'1mΫΆ•=ZΓΤΈXŽ=φΩ°aƒδζζόΔ¨6μΘ#‡ o#ξ―•nέΊθPfΜψB{μ)™9s€€ΔΛθΡΗΑΤx‘΄nέ©BΪΆν R΄Dήxγ-™:uš ΠKΞ9ηrI£Ts6eΚkςόσ‘εΛΦHxDΈuΤ`Ήα†k%==M&N|·rί}wΛ+―L‘—_ž*ΏύΟdόψ‰ςφΫοβx‡¬\ΉJzh²|πΑ °2hξ“ύίίIΏ~}«ΘΩΑˆΉυΙ0 C a0ΝWΓΰl­μ%^Υ‘C{%G4;R›4gΞ<ιΥ«» Ψ_Ο™·pα"3f°dff*»α†?Iϋφm@†^YΊS>ώψsωη?ŸΧPnΧmευΧί–Γ?T&Lψ+Q%|Ή&ΚΪuλdυκ50k>€6zΘ”ΧžΙϊ+4lkΰw6OλΣΗ‹.¦‘#…Άν(­ΥrΦY§ΛΉηώJ   {·Uςμ³Λ«―ώCΛN˜π lΩ²Ε4`І}†€!ΠΌ0ςΥΌŸA;zOΎ₯wοžJͺ–,Ι‚cύh²FΙρΗ+³gΟ•¬₯ΛΤlΨ­[gΥL-^ΌXrrΆ€|΅W’ RΦ^Ύόr–KDD„|ρω΅ε΄ΣΖΙ‰'Ž’qγN–O>ωNΦΓ‡,<< fΚr₯­0%UmΩO<ŒφFͺω’υύͺΚnέΊJŸ>½δΝ7Ώ–Γ„~φR­ΧΒ…‹₯K—L%ZtθoέΊϊ:_M”ά̏νΟΞ:f†@ƒ `fǁ٩ ήτ8pΰœχδ­·ή–Ν›·€Lu’²¨¨Hyλνi IδΤSΗh$M:΅Υ‘ό™’­Μ̎¦M9VL+q*-[¦δΘχ‰ςDΦ ύΔH’&NΌC¦Lω― 6Nzφμϋγδβ‹/”ξέ»U­lDMτkί>Q‰έ·wπQUΩ?©”$„Π‹τή€Y@@)μλ*φ‚\Φ?mνXY]±/θŠ±"vΑU,HSPzGšτήKH›ωα/c2a&σ»ŸΟδΝ{·ϋ½άηžχ‚ π|akBήττύΪFo’Ν_“?I€H€’•ΕW΄Ξ|ŒΫ‰―5NrεR5~‘Ѝ*ϊ©jO6oήΨ^1°zυκڈ ΚΎύv²ΖhύŸn v΅kˆΓφdZZY ΒΟΤ²­eήΌωΛΥΙςηΝ[`q`.ΏmΫΦΊuKy챇d€Ι*’^“ςεΛΙέwχ51ηΌr¨Œοˆ£wή,τΏ†ΕŸυιs‡εoέΊU…έ.©P‘‚υη­oψƒH€H ͺP|EΥtGΦ`Hψͺ^½ͺ §₯rφΩ]%%%ΕD Π‘οͺ§«†ΤͺUΛοΤ…vΦ€ω‘‚-Gx ήxcˆΤ­[[ž{ξ ¨OKK‘±cΤς1ϊtdI œ\cΘj`C‹;λ¬Λ5@ώzυ¦] 9Άtι:©R₯Š=1™‘±ίΔ•[πΎ!}ωεhσx΅iΣJE]{ωαηjWm©X±‚Œω±Ζ€­’Χ^ ΙΙIVž?H€H€’—@\?MΡ;|Ž<œ 8ρ…8«M›6ۏW^y™Τ¬YΣΜΖKP§NύMΪ΅;I:w>έ„Y‹Νμ5#G~fω'ΤFnΉεF)[Ά¬Ύ~Lžό³>ύx³Ζ†mQ/Ωχ&ΜϊτΉ]ρλ›w¬^½4l² »χT4­Π'!/Σ€ϊKν5θoέοάΉ“yίRSSυ΅±ϊž―ePί­[ Φo¬[œ{υIΚ―4θ¬T\QΕ\/έΆlh27pfOΫH€H€BG FΟ—…Ž/[.$\Η/:uο_xςηΈξM(‹ψ/%ˆ.'xπΦό,}οW‰%¬ψ]»ΝCζΨθM‘φ7ΤΒMΰΆ=…έžwωH€Š/G‚Ηˆ%ΰ„ΝΚ•+eƌ‡ΔκΈ”Χ5—WGgߊ+τOw, 4™™‘)Ώώϊ«μή½Ϋym  £/nα"CaOA|M M!ˆP_:q4ϋ 'ZζΜ™#ƒίl[Έζ>$<\Σ¦M“5kΦXvvΆL:U6nάΈζκyΑΫƒΘsΧ\9ΧOπΉσv >\–-[ζŠΙςΛεΩgŸ΅~υ‹kG—œw.ΈmWΦ•sηή£ΛsmΈσΰΆάu]YWGŒcΫΆm2eΚΩ·o_ Έ+γκΈ wGožχ{pYœ»ςψCκ WΞqφlΨ°ΑζσθMΌκ{―Ήrξ’%KτΉΟΛΥ/Κ»rήΊ¨γΞƒ=o]Χ†;Ί2y]Χ·L^mΊ|W>―£kG& γK€βλψςgο…H dΙ’R±REqž,,Ξψ`!BΒυηŸ^–/_nηΘ‹‹‹“‡~XΦ]kΧPΦΥσ½ήWΖ]sε\?ξ Ίk»vν’-[ΆHΛ–-­όψύχί₯{χξ’””dΧPΟΫ6Ξ] “»Ž2ΎຎΚP+8?ΐΓk§kΟ]YW&Ηη_°·oί.wί}·ύ^”u}:;ptΙΥ Ξsm;6(οΚΊφPF΄)οuΧ.Ž.ζ푇²yτ^w}ΈϊkΧ‘\9Χ„ψgŸ}θBe]9מ+οΞƒhΫυεκ](œ\YΧΛΟ―M\wyNπΡ΅‰# ΐρ%ΧOΣρ5½“@ή°˜lΪ΄E*V,y•t‹Ξ’%KeέΊuR·n]ωρΗeφμΩ’˜˜(εΛ——ύϋχΛ?ό «W―xIΣΣmQύωηŸeσζΝv ©N:‚νK#Τ7nœΜœ9ΣΊ­T©’±¨νάΉS&Nœ(γǏ7α–––Qhύ‘Ύ³ ۍπ¨\rΙ%6\‡'¬[·nΦχάΉsε„N0›6mΪ$Ώός‹ ?΄0aΒ«·xρb³½\Ήrf }ϊtΑψq/` °Ά#oόOγ[Ηξ:˜-X°@οΡMΦ7l™?Ύ$$$ζχ 7ΆΒΡΆ˜'MšdνΑNόcgΪr χ'xΐFπwν!mΒˆDάŸΰ“œœly˜ΤΙΚΚV6ceΥͺUΖ=3+ΣώMΐ΄ώΡ x ΰ~,Θο\o~?:ό/ΠΡqc­0$o1Uƒ ²Ε‹σuΧ^g" Iˆ',Šψ±‚#<:X”‘°ΐήύςβ‹/Ϊβ…-ΙnΈΑ„ςχξέ+/Όπ‚Œ=ΪU,’W^y₯m!•W^1oqηeΐ’|ςΙ'<4θ φ4jΤΘΖγ?n‚ υ±πžyζ™²tιRœš ΌοΎϋlΡ„ψzύχν:~|ςΙ'ςΟώΣΖƒsˆ ΨΑτΜ3ΟΚ#a{W―^ςΥW_Ω9~Ό=όm[¨ρέ+>ϊθ#kΒβ§^½z&2Pν`ρ†ν(<ό±άsΟ=2rδHێ„XΐΦdƒ Œ%Ψ?ύτΣςξ»ο’ ³#FŒ°οΰτδ“O΄yΝ5ΧΘ‡~h" γC( ;]Β<`aμΪ³ge}ρΕrγ7Ϊυn©pπ΅iΣΖ§ TΰΪ7ί|γ;όσ}ΊΐϋΤ³ΰλ₯‹OEšε£υ€ϊUοOE‘OoΛ‡ύψΗ?|κ‰ ΄§‹ΏO·Λμ\mŸŠŸz‘μόΥW_υ]qΕ>Av޲ΊΐΪwff―ŠŸ Ÿ ;Wo—ε6Μ7δ­!vTΔΨwŒω'ϊT‘αӘ-«§’ΠςCυ ω.Ύψbeθ΄νΚ©Έπ]ύυ>)– ;ϊυληSραSλͺθΌΝφ©χ)pŽϊ^x‘•Ώ=zψTPYΎ ί₯—^κSaΰΫΌe³O=:ΉΪκΩ³§O…‹•uvΊ£Š‡λHΕ–ο’‹. °ΔuυHψan1Ξ~Μ§zΘ|*β¬ 0ωε—]s>Μ μQΡΈφΐψΎόςK;W‘η;ϋμ³sυ§βΨwΛ-·XΎώΐξΙQ£Fκ« φuθΠΑ7χŒΕ,N;ν΄oTRΑlŒρύϋŸO…Ύ±Β9κ½τK|ά+ηž{O=ΐΘφ©7ΡΧωŒΞΎ―ΏώΪΞρCΕ oπΰΑs~!G  Ώs]yž@|ΡH<φB‘' Ώ8€uλΦR΅jUϋ=ΌN͚53Οz‡' ετŸMΰ»»†λπv¨ψΚŸuI'™'[c΅kΧ–† Zό˜.τζκΤ©ͺjt•OΞ:λ,ϋξ~`kήliΊIϋφνν[RΨ’Ϋ°~ƒmγ­_ΏήΌ?κφœŠσX΄kΧΞΚb{[lπΖΑ‹-U]Θ΄mΫΦΆ―Ύϊj+Ϋ’E Q‘`žlAώUΫΒ ΌAπjaœ5kΦ΄²Ξ―•Š σ|ΑKο˜:8‘,Ό…HψŽ |lŸ’.£,ϊ†· ΫΊ*Ϋ_π0a[c†—[~`qκ©§š' mβ;<…Ÿ›#gς½ΙΝ˜`{ž:xύ°έ†qΒ&°‚ φ οR₯J™· σTΏ~}9γŒ3MΒ~wŸ olWͺ²ϊΰk˜K7~τ ώπ"vžrΚ)ζQCy°@nQL1Ζ5j_³¦M}γ l†‡΄qγƁλ*νžB]°ΒΦ5ΌvΰŠ˜ΕΣO?έ<‹¨Ϋa Άƒ‘°ύY―~=©­χ¬K΅jΥ Δ8Ίk<’ -Š―’εΝήBH‹[·’«όmgςccrοΎ£>βzηΪ‚XA`<Ά¦°Έc› 1Cˆσ0`€tμΨQξΈγ[μ\'DWα…Ψ$,ވI‚`AB?XΰƒU²TI‹ίΑΉ#-Χ„@IDATz+lΫ"ε‚σ/°²Xt! .°₯uβ‰'šΐ8p ΕL‘ςέβ :Ά`Υ£eφck*9)YšκŸW°Ώz-ΫdΨ*„PΒ6,Dƒ›uΗά?fΜyν΅Χ’1`3„‰z(mk’qsΨΦE‚=φ˜€”I‘† ZίNθδѝ]ςΞ1˜Caϋβη˜Kυxšν+VυlYΌλ1ΰrΣM7Y9Ψοw7lίb»BbOzΊ>ΑΓ1tlpβ~{$τkH(ƒ„{Αε»Άp6{Ϋƒύΰ†φPρ^ξ>rmaϋePmαγ„κγƒs—άΈsI€ŠžΕWΡ3g!"ΰ]ΔΌ]_wη“mρ32&xi vΰΒB O ³2eΚΨ’…"¨gϞQ½_=^η-Bόσ\‘-xΟΠ·σ:α„ Δβ–ΰi‚·ρ``G;uκϊ½f¨‘7NƒΙaD Δ lΔ:U«VMP¨ˆ&ΝˆΐjΔdaπ†ΐcχΑ˜—¨sηΞV?œp0Α’ 5ςπ=ˆYϊτΣO-φ ύ»…έUvqg’@°9―ϊM>πΚˆ·ήzΛΪ‚Η}aΡt ž,lΛa1w’[A>[f[a(ΫͺU+Ρψ/NΨ^teΡ­!$άSŽj}ϋφ5OμΗΨ δp½yσζΦ}mέ~Βvž„7 OΒ„ώ! nΧΐ|xΌ̎„ ΆE‘ΰυΓbΡ„ΰt0…πpΆαž8φ‡p―·ί~[BiΫ–E;°,}τQιΠ±C@(ΑΛqŠΊθΑη_κΟA5»Ž:πό!Aπ`~P^!pΐα „ΗmiL”<υΤSV3œήyp>#0w.N΄•QΖ>KJrŠs!λž°Dƒ`Œ'ίyη»gΰICpΎΖ·™ Sˆ.<`€6q_A˜BΌΉ­E[ˆ;$x#!”5&Λμ;ΌJžD$o”Η9ψΰΙVόCl"α>ΜΚτo«Ϋύμ9τώ›pex$(Z|ΥDΡςfoG@‹_A{†ΐ‚ ±€χΏ{·ύO Αsε^3oΌ dˆΒβolˆΌ“ %ΔΔžrΓ" οκΐƒ„όžκωBμ „ΌR°₯kΧΧ„ψ₯Kώͺ―˜ΠνMŒ qgυ*α΅ 8‡]πZ & bβ‚ .0‘‚-ΊΪ*(\YΨ 1ϋ qpΩe—™mX`Γu,Ξ;Ϊ‡ ‚ΰ€χeπδbΑ`ΌCπ€Ζ‘‘ˆ'δ “’“ŒΔ™O½D°’υœθƒ°ϋόσΟΝsO<‹πή`ύβƒΧxάpύ f3Ζο8ΰ‰Nbl‘bμ§k˜π;σ¬3-ή [p`χζ›ošΘ [±„ζcξέ»· σ–¨γψΰƒχΝσ†§2±] ―ζγΜ;Ϊ_(ˆGΔ―AT’τ ρ„X;ˆ+°6t˜υ‡±"ξΌ02άWM0o[˜χή{―σαΓ}‹yθ†WOΣβήBό!žTΕύζx"υΐθΪk―΅χΕAτ’|0—lΗλ%°ύ ‘ V!ώΠ. x ΰ~,θο\o=~?r1 ϋΰ³ΫG^Ÿ5H d°@̟ΏX½ MŒ#”ΗβοΆ PήyΒΰ‘p ΌXθ ƒλ6TXΈαQHW–yπΘ αŸ 5$4ƒƒ…Ρ%―)°β B1MπΔ8o–kΟ[Ο΅›°¨:;σ»ξΌ§kea/x―χƒ2θ"Ε%œΓ³…q RπΨΡςQφΑNocπ A8 g_°mήφαCYˆ]؏:}΄γ΅}"}x―Γ~΄™Χάa[σΫ\Χ?X"β m"Αϋ ΊΌόπͺˆ>}ϊΨXQmΊ‘σάsΟΩ-<œ€#ΖβϊC{ψŽΎά5a?ΈΊφ\ΪxUqηwο‘κ ¬χΒ9’›π (ά‹Gς;—ΠŽžΐΑιθΫ`M X`άν ^`°aQΗΗ%\ΓB‹„Ό5XHKbqΣ’wρΓw,|ψη9;°Θa›K_Ν`eπνΒγα…δ„Ύ»zψξ’ΧNw ΗΰλX€ƒΚ‘ύΰ²Αύ Œ ¨γ9O‰»ζ΅ΥyΑν»ςhž&—Ό}ΰZ°m8wŒ`BΉΰ9ΜΥώλ¨Zή4—\9.ΉkξQηΆέ5Χ?8η9„φΌό ϊΰέBr"ίqο άπ|B0’}ΗΓυ‰²hΟ%gŽ^ήςψ&πΐ"α uΌΆΉkΑσΜΣ*σ @‘ η«Hq³³#!Κ…y3Ψδ]ΐ—oΆjΌ‹a°ν£\pB><8π–Έ‹5Ζ…kαžάψς[^ΧƒΗ„6 R.―zΈVΠΊyυs8ϋΥ~p{ΑmΉ|lkcN±5λ-γς1ˆΫΒV!«»<ήΌΞ½νM~^uxE ”ΏsΥo4ζQ|Eγ¬GȘΗ/‚#Y##Ν xZ°hbΫ¨0’σ^xΫΚλš7Ÿί#‡ΐ‘ξ•ΒΎ—"‡ -%8Ζ|Žσ£ŽΝ‚nwN^νδuνpν0?< ξ^9\~xŽŠV‘ „š=_‘&ΜφI€H€H€HΐC€βΛƒ_I€H€H€H Τ(ΎBM˜νΠγZΖlH€ˆ―ΌώZ†Λη±pP|GΆB$@$@$@"ΐ€ϋab!   (_…Γ‘­   @P| ‘   @α ψ*Žl…H€H€H€ D€β«@˜XˆH€H€H€ ‡ΕWαpd+$@$@$@$P _ΒΔB$@$@$@$P8(Ύ ‡#[!    ψ*&"   Β!@ρU8Ω    ˆΕW0±    Š―ΒαΘVH€H€H€H @(Ύ „‰…H€H€H€H pP|GΆB$@$@$@"@ρU L,m|>ŸΰͺκφCeχ±΄[XcεΌΛψX—H€ J F‘…n…)¨,G ““c₯bbbŸP%ό‹aσ‘2;Πn°Θq¬ά1P_H€H€Ž zΎŽ vvz4bccˆˆΒώ?ΪΛΞΞ<―έ»wΛκΥkŽΖΜCΦqv‡ͺ}t>Ž•γε„—λFr¦λsΫΆν²nέϊ£jέ΅±~YΉre‘ΟQΕJ$@$p”(ΎŽ«-£ωσΘ΄iΣeγΖM`Xœένύξ¬τ^sίααrή΄ΉsηI―^—™3gY•Χ_,ν۟'ΏΎΔΞΦρχƒσΰ„kh/8Χ ;Ώp|σΝ!rΪioίυ>½ί½6Έ~Χ]'Σ§ΟŒ ŸΕ‹—-[ΆZΡ`ακΪrGo{ψŽλyΙεΉz8ΊδΉ2θ3++KϊχNnΌρv›?δyΫυΦqνxΉΉkŸώ₯ΤͺU+0O(ƒδκ{νπ^ώŽs& 8^(ΎŽyφ[ n1ύγ?δοΏWzχΎ[FŽeuύBΒί Ύγƒδύξκ{―Ήο(ξςα‰χH‹gΚε—ί(ψΗ}2bΔ»²gΟλ6͘άXUΧqrΆβ/'ŸάV.ΈΰIJ*m{YyλδΕ͍½NΪςπÏI… ¬ χΓΥΗΡو_ŽT©RE°ύ΅}ϋ6Ν―¦m–0³ Db%..ΞΞ!š6m"eΛϊΕς!ΠΦ―ί`‹{ΥͺU%99) `ΰΩΑ6εΎ}ϋ¬NΥͺULˆa‘_³f­ ϊ†\uUiΨ°‘$$ΔΛI'΅•Fεjοή½Ά-‡:¨Ÿ””dΆdffš·¨T©RZ>U·έV™'­NΪΦ‡ςό(Y²„²I‘[n½N^~ωY'μ‹/FΛ³ΟΎ"h릛zZi“={Πη:γT½z5)Y²d`LAΖ 'œP=ΐuσ⁆q}λΦmRΉr%λoΓ†R§Nmιε IOίoγœaNRSΛόiΞΐ:''on`vσΝ•€R₯JθΚƏΆ0†ΜΜ,λΆ"Α~τ αW±b»/φξέ'΅kΧ–ΔΔ„ΐ8­0 @ ψ*BΨμκΘ `‘ίΏ?C~όqΌ΄jΥBZΆl!όη3Ί•ΆD·Λ[ƒΛ–-—Ϋnϋ?7΅…Ȑ₯Y³ϊrΙ%ΘUW_ χ–όϋί/˜8ν΄vςΚ+U“"'žΨLώφ·^&*όήΌ1β=ωίΎ—W_}^_}Ϋξ|λ­aςλ―³t -G:u:Y=c·KγƍL\½φΪ@ωώϋ‰ςΗ›δ”SšΚΥWχP{.—O>ωLž~ζυBυΥΆ^W―έΗςΪk/ΚϋΟ>#ωΟkΫƒΨŠόε—ι*¬rδΤS[Λ-·τ”Φ­[™€ΈλϋΤήζ*$βδ½χ>•Ϊ΅ͺΛ_.9_·ρnψ“³_ M›Ό¨ΌN΄O£F Uψτ—―Ώ+]»v‘Ϊ΅kɜ9s΅7dόψ_UŒΔ«8:M½‹½₯nέ:κά!Γ†½­εΏ“U«6ͺ@*-Χ_Ήτμy½@Θ1Σμυςθέϋ6i€±φύ›zΪQoί΅ςΫoΣU,‘>!ώWζΝ[¨υώ£cΪ Χ^{›άpΓž9«'ωΛrέuΧ(›ΟεΙ'·Χ^{]Η½UT=₯ή»–Vm£/„ε+VΘΈqΣTlέ-gžΩ ΝΫφcϞ© [aγή·/]…Γ rε•WΨ–ΩοΏ/UτμRα–w‚ΟοΡў¬/lYΞ™³Κςε+T€,•kΉΔX‘(Άi§Na}ϋ·cχΫ6+ΆO:©œώy―5U=[3g.Tρvo€Η°α±ρC\εO?Ν—O?}U=Y™yΨ–υηωε ?ΜΩι§·Ο5gί~;A½cσ­ΏvνNQΟ—Ÿ[λΦ­­όΔ7ˆΕ©SgjΩYrα…ηΫxwνΪ%ηœs _l“BδV¨P^½WHΉrεlΛyΠ Ο%#3#olΌJ$@E@€β« ³‹#'qροΠ€IΏ¨Χ«₯m"X‹φƍ›eΑ‚…ρ΅kΧσ 9…79Ή”Π»' +T(§ž±ŠfLΉriΊ(—3oβ€œηλ€^±2ˆ;B‚Ϋ΅k±zΣN°s΄‡ν@—¦L™*C†Όmm•+WΦ‚τa§?ˆsBΚΘπ/ψ~q0 ν­[7ΧΆΖ\›5jœ bqΩ‘†ΈΆ σκΕΖϊbΒΦέθΓΥ;x?YœΖΏa,ˆ;ι€zΆέΆkΧN0ρ²yσ} ρyλ§aΓz][ ΚoΫΆΌσΞ{2zτ7κUϊ‡œqFw5]t[ς‰W/β²e3ςερˆ¬ΤόjfψΉtΠΣ£s³ϋOs–”TRYϋzpΌάΡ΅žΰ€{cεΚ &έύβŸίσξω_ŽΝsrr²U‡ΐτωVHl ώŽ'$@EO€ΏŠž9{,·HΓC3wξΫ.*_ΎœΘW­ZY½eeΚ”_­%§””$‹Α‚Œ΄jΥ]ΔχjΉ4ΫΪΓb½nέ½ΎΚς„Ύ~ύF­—lσ^€NNΘΑϋU³fK{eςp}βΔI2vμw&^>όp”D,ΧpσΪ¬_ΏYΫπσς‹°„€ψr ρ[HhΏ^½6ΧZΰbΪ*WnjyQς`oΙ’yίΙ/ΊJ˜· e!†ΖŸ ήͺ9ϊ”e#¬UMl\ΉE½ˆ§Κ»οΥνΧ·MX!ζ [›b?οΌsu[οmΧ\ΣC|π~‹υ*Y²”Τ¨qb.“&MVOγΧ6Fx‘²²ό[ŒN„*Υΐ80ΏeΛ–ωӜνή½Οlr ±OΛ—oˆ wGΨ¦MC}``¬Š₯Y&žf̘­Ϋ ;4fνq)]⩦\ziWλOtb›qψπw¬qγΖλS…χh|ΩCφŠl―B<‘œŸΗiA<ξΡ-Μ.&Ψόž/k*ΧΨοΌ€ΘΐόΟYι%ν)SδϋΉ5–Ο>J½\ικy;ΛΖ»cΗV›χwξάAƒοG«8―ežΑaΓήΡmΘSυ©&:Ρ§ίv΄θέA―£*’ @Ρˆλ§©h»do$ph&_Ψόδ“Ου Ά&;t±Šϋ‘π΄^1kΦ\υ2΅ΆλΓ‡Ώ―q?έM}ϊι—zLΥ§oΆ€w,ΐίƒΕ uο~ΆzhώgυΟ;οL}*ρ «7―OŸΎΠΪΐ–žά»wn·u²Χΰ΅4ώϋŸT`ΝΣ'.Φ π>φΎ)lq._ΎάΆθ°έˆΐvΌ’‘[·Ξκ½ͺlOΫ•―,¨‡―θ₯³Ύ/VΟά§kϋυιΜ&ΦίΨ±?XϋνΪiœ˜ηγΥˆ%›5kΆm―zκ©&ΜΏ}ϋz$gZŽΈΑ΄iΣƒŒ‡p 6ύίυ6OΚ‚a³fMΥϋ·^aO“6lX_™έjž1lί•+WڞŽ>ό³νΡGΡ­Žφ: Ό§lηΞιŽgόW―Y£cψ].Ίθ\{ AŸΣυαˆΖ³Οξ¦cί­O0Ž8²³sΝYοή7ιœu0›Λ”I•rε“”ΫJσXžuV7‹ΏΫ²eƒŠΥΚ¦²‰λ„„³žΠƍJίΎ}T€Χ3ρŒ—ςΒC†y„¨Ε½ηNtΈ1αˆ€|Œέε‘.'Άr1.ΗΣε»ΊΑ<Π7>Ž₯³c@xΟ7gΤ7lαΒΓ‰­Lx½σ‚2°L Ψάv.£O$wδg›β ("άv,"ΠμζΘ @|ΰiΐΌ’waΗ‚Ž-Θ=.Ά’ξε€n‡θΨΎ}‡zOΦκbμXNxΉ2qΪWœ§/,ξή„r°Η½ρy.Žx>HθΟ΄ ΚyΗ’Wϋ¨Qη’kΧ½u‘\ίΥΑΡΛΖ{έ΅‡khη‡Κΰ…₯ξ₯₯(_ΊωυοΪ(Ȝ9{½cχ +¬ΪqeœήkψξνηωΩ†<& (*τ|iφSθάΒ ―ώ#ήxΧQxE:ΕωΒ…‹Μ3ΥΈqc2ξ‘άΆ«οΧ%ΗώΒΞόΪwύ„βx¨>]ϊ… N.?―Όΰ²8Gy”=ܜεUχP׎ԎC΅Ε< 5Š―Pfϋ$@$@$@$ΰ!ΐWMx`πkd€ΧΫXΞϋ‘Χ(Sx(Ȝ…‡₯΄‚H€ Ÿ=_…Ο”-’   @ΎrGη[Œ$p|Ξ£u|¬b―$@$P< &Σ=i]ί±7rˆBέώ!ΊfVˆ `n9Ώ!†ΜζI Δθ?τΠa0HšP<δδδΨ@bbb¦ΠΕ•·»ŸΕβ:N7>I€Β‡=_α3΄δ0bcc,’…ρ†μμl ^€cΒa³Ρή¬”ύϋχΆμΡΘΚΚφC‘ΐ|λ#εϊ~ώΣΉs7_ΰƒόP€ΰΎΧ‡wŒnœ¨γl=\ύPδ―[·NΆoίaMO;B16ΆI$p@\?MOωΒ“μ… ΙΚ•«$>>^’““l‘τ 1|wΙ-\.ηXψqŽΟΝ»δε—_•E‹~—–-O4±ας]ξθΪΒy~}ΐ>,ΰ3f̐¦M›HλΦm€I“&ωŠ»ΰv\ΑΧaς‘7nάxiΣ¦΅tλv¦Τ]+0&WeƒΫpΧς+ƒ|$ΧΗΆmΫeΑ‚²lΩrY»v­ρNIIρ:πΣΫ–Λ@ΏΏΎDžyζYe#υλΧ“I“~–ηž υκΥ•Š+Θ°aoΛW_}­ŒKRRR€Χf΄‡φ]Αyσύ=£Κ,Y²Tž~ϊ9‰‹‹΅ώ\}”rmΈ²»wοΡ1.’›6Ι&ύlήΌE222₯tιRZ?.ΐΒίƒ³ΗζΪΒ™λΧΌί]žšf)―:< ύϊ rέu·ΘςεΛ₯K—3rέ“(›W[ώΦω“H ΔGšΑ΄7Ί`ΡΑΒoΟί~―μΪ΅GΏώrιΣ§w`Aυ.lŽŽχΎ2\6lΨ(ϋ[/IM-£ ν~ων·R»vΝΐ’ ρ„δϊtGo[ξΚy―cΑFͺP‘‚<τΠcR«V-;wmΪIΠΧΪρΆΑ…z2―Ό2H.Ώό―rϊι­v΅jUεŸ|XͺT©’«5o}Χ. x―{Ώ{Λ œ;‡ΐ}ν΅AςυΧγU|m”R₯JΛœ"wήΩ[:vμ`εm υpέ³gΌτ ˆ­pYΆnέ*ƒ½*7ήx½/^ό»L›6;pξψΈώέν{ϋpΧέџoMlΪ½{·φύΊœtRkΛπΦχ—<ψ¦>}ξ‘””ςσΟ ­Ξ[ͺhn!Χ^{•Š·zv-jR yνp½ύΜχηΊswΔUWאπŠ‹.κ.Υ«W·ωΗ5ΗΗo0‘ _Εa£` σζΝ—%€Aƒf2sζlΩ²e‹”/_ήFΎsηNΩ±c§ž—SΟEiZXΈ*W$πδ̞=ΧΌfλΧ―—2eR4/Ξ»””d]c‹ρž={UŒΥ2O[$±½·vν:Ω·oŸynΚ•+g}"γΖMΆ@£5kΦJBB‚Š’ͺrΛ-7lΫΌy³nκ6›.άΎΏ7'>>NΫͺX|Χ­[―[MΫΝλ‚…ž=€5kΦΘώσœzj[Ω½»΅Ω[£F ΉνΆ›΅ŸΚVc,ˆ(Ο ϊ©U«¦ŠͺRf;u7Vδ½πΒΛ*ΊVΚ“Oή§"¦­¬^½F½U#δώϋϋΙπαƒΤ›Uίϊέ©ΌΧoΨ`ί«V­’"Ζοƒ=͚57Θτ₯V@D”,YRΚ•+kω{χξ5ΆΥ«WΛe„xBLa~ΐΔk'ΰ>–O>ω\γΟQfδ³ΟΖ*οΩV~τθ―νήxύυηUΠ½g^΄Χ_ΛΌ³ρρ φ †›cΨ›––"?Ož"W^ΩCh+χι§_˜½°Ÿ‰H 2 Πσ™σV»Εޞ]ˆ°%X³f υψ΄Υ@ξ_Τq™yό"L·”a[WXΔ±uΧ²εiζqƒW«C‡Σ€wοGτs³yΉΠŽ[˜±Ε §N~HαΑλe—]bqL°wΠ ‘Ί=Ά Ν™g0ά±c‡ςMΥ-A―)'XΰuB‚§θΉηžQ΅ΩDK™2ΊεV₯’` Ι?ggΒ/Œ-λ@~¬ ˆςψγ©'³₯z…šlυ‘MΌ6mΪ˜Η ma{6..Ζƍ§g̘­bλt›0@~υD!xφwθΠΐ„έ_ŒΦψ©nΌΕκ¬ρΝo³³h355ΙDξ‡_.^xΊŠΈKUΐ—³νοΎoσΑΆmΫNγΫ’Es۞1β+έ¦½Kγτ:™ΐ0`ˆmG’_xψΰέƒwۏ*”Χ\¬VQ»CR’SΜ‡_XΓ¦‰ͺ'΄zφΊ¨°k.&LΆΊ.ΞЍ‡G Θ!@ρ9sU–ΊE±VZ:΄΄νŸ€€{³Gκ-ζ A,ΚŠμ—Έή ]N¬Ž[¬ΠΆ#!Ξ;T­ZV¨€Ψ#‘mΆ„…ρ;XγuAF*[Ά¬Š΅rζ}€ˆAΫπ¨AxSϊΎά―R@|}χ=dεŽ;n7‘€ςπθ τ¦ΜŸΏΘΌoUͺTT1ιA>!"±πcΫ+?έΨlMΧΚώz‰ϊν *r¬!>Ϋ΅«/ί}χ½=Πΰ<+_~ω•Ύ6£±ŠΔ&ŽnΌρQݚl£ΫiΚ³Ο>ΨsΆakρ`B?θϟΠζ–-Ϋ,WΰD0;Xbb 9r”nΡΝΧX½υϋ;_wŽΖ>MPž"©*1οΨκ…0E[Ψ"ώψγOΝkV²d)mw½z OΡzg©@g푟`~Έζf_­Λ1£§Β**θj™ΨΖύΆqγf œGάkΦ¬7QŽώύμΆαΏΚθΓώ{ ωYDΟέ›o3»‡―>ΐ0Xπ΄-ξ'¦πŸ‚`[έύ…Άΐ!8v1‘ DzΎ"kΎ’ΖZ·ΐ 6θ›o¦jόӁ­·mΫΆ©'ιA ŸhΤ•+W6οΒηŸe[:X€,,’K›7oΔH]}υΊ°¦Ι^υ‚Α«δϊς/€’‹e†=Φ₯K'LuK±Ύ4jΤ@cyF©'&ΝbΆ°p§§ο7Aη<˜΄±}ϋβ‹²¨—γωφΫq*VZΫλ2ΰ-ΗvϊήΈqƒŽeœΖ}¬ΫŽ΅Τ£γ΅‚x,]Ί„=l€ν-l:[±e…˜΅.]N/ o­[—„#Ί6gvρΕέu+τ e—­ήΖφκϊCγ—Ύ”+ΈN· O΄:uΜλ5cΖLω駉ζΕ©_ΏΆu€ρ/Y²%ΠΎ_8¬ πΫΤΤέή¬ή΄ΥφξΆ)Sfκ טΈF}¨„1€Ζί»χν6 ~Φ`ϊζiκή½›|_nžΆ₯“4ό~Ϋv|ς©η䚫{¨(ic‚m‘ύσΞk£Β#ΟQ§NgμElV³f- γ/Χ\σWw=Œ#b«πjΑƒ‡λΣ‡-5ΰώlΝhιΪk―1Ω„ “μa…&MZœψƍ›τd—šΠINNΆ§V±ΝŒ~“‡2˜œΕ©H«nζΜ™ksŽ2ζƘw€ /<_Ÿ&ΝRύ ΝΙϊjŠšϊ„γ ϊΔΙΖ :ujξΤλΨρ¬€ΧΒκœsΊΨΨ°mxλ­7ιS±ολ‹h­σy¦ήδΧ©Σ¬>ξ£N¨fWτωhΣ¦c -&Ϋϊυήo(ΗD$yψ·#oΞ’Ζb,”9X,±yΆρρζα=KXμ±πBδΐλ€-I—Pν‘-lο  4ΧΆk |\Ϊ­±_ϋ5 ρR,ς\X`a’³Ω΅ώά‚οΚ ςqŽ>Γ†…‹*έN/^,ύϊυ“μμl;wύΊ²ξθΪπΆλςΌu\>Žπδ­Y³F¦M›ζίRυΥUγ‘H€H  P|Eΰ€ΡδΌ ”,YR*WΘ„pΗΘmGζ΅%ιΝχz—œπqu\9Χx₯J•€tιvZ’D ©S§Žύ]4\pύΊ²ξθΪpφΈλ/ΧΏΛwbpωςεςβ‹/ϊ·uWε™H€H€"—cΎ"wξhyœŸdeeΒ5gφY΄x‘@ 5oή\κΧ―ΘΗ—ωσηΛ‚ Μ«Τ΄iSiΦ¬™εCmΪ΄Q~ϋmšlάΈQͺU«&mΫΆ•rεΚ™ψA?Nᘘ˜(λΧ―7oΨΝ;­/τηR~Ά8α…:Ώώϊ«¬Z΅J μN9ε©P‘‚yσΰυͺZ΅ͺ|φΩgΊuk©U«–Ω™H€H€"=_‘7g΄8_Ή=B#FŒ»οΉ[φοί/ˆ»ωζ›eΒ„ Ϊ_}υ•ά~ϋν²yσfY»v­\{ν΅&pPρ\gŸ}ŽόπΓ’””$Ÿ|ς‰τοί_φμΩcž-'ΌP«5«ΧΘΐM¨mΪ΄I.Ώόr™:u*²M(½σΞ;²eόψρΦΪ|α…dτθΡRΆlY7nœ\uΥU²aΓΙΘȐ-[ΆHff¦§§§[›όA$@$ΉθωŠάΉ£ε"cqWπ! ρ_C‡•† ZΙFΩφ…νΊδδdC’»‘1±1ζΝ‚G ΚΕiΕΗϋ dηdK\lœ•AϋσζΝ“GyΔιL_¦L™€°Κ«τ ζ’  GyΩ‚€};Ψτδ“OΚδΙ“ε»οΎ“Hǎ₯W―^χεŽL$@$@‘O€β+ςη#8@ΘΙ“΄΄4Y΄h‘μΫ·/πT"β<Κ"Ξ ηUͺT1α΅}ϋv«Σ²eKΫ2μΪ΅«τιΣΗZΗΦb²\r"Ο{|Ν‰%τ—Ÿ-+V4!qwρΕΫ±^={φ΄νPΨ€vΌΒΞ‰1Χ7$@$@‘E€χ‘5_΄φΰΝςψσBΨφƒg qTΨ‚όύχίeΠ A½{wΰAlΆώΧ΅|ΕrYΊt©`ΫoπΰΑΆΑ„W<ΰƒmC΄³pαBm0ΑyΟπbž6or‚ Χς³₯sηΞrΒ 'ΨΣ–ηŸΎŒŸ0^ ρΔΌeHπ¦Νœ9Ӟ€thŸ‰H€H 2 ΔιΛ!ϋE¦ι΄ΊΈ€ΐΨ΄i‹Š₯ςώw\bΐπ:α5 [·n5ΆςZ΅jeμ_ύ΅ότΣOφ x”AƒΐyΌfβΣO?΅' ρް»οΎΫΆρj‰ΉsηΪӏΏύφ›½βb±]π”! ±ZuλΦ΅€yHNxᘚšΉBΈΰΎ#ήΚ%w Gˆ+Ď!αά+zΌί½ύΊvΌΧ j >?½uρΎ1&  Θ'@ρωsΘδCΒ Β ηξš;Η1―|WΞ›‡²G›ςjΟ]C›ψξϊrη/Wωξ»Λγ‘H€H ςP|EޜΡβ#$p8Αr¨όCε‘VόPν*•—4φ°  @ΡΰӎEϜ=’   D1Š―(ž|H€H€H θ P|=sφH$@$@$Ε(Ύ’xς9t   ’'@ρUτΜΩγΐ‹ 4?`,J$@GIΏkρ;—)τψ΄c資c “γ“,}‘i|ΌΎ8•Ώό$}9x?Tι1eU 8HΒ+++K_8Νχ€Ίo_‘cΛ–‘€ύ/LTφ’e+%&Gί^―o€g‰Ε‹XγβΕ—±_OΤyΝί•Ό-H€Ž•€ώ_Ξ§ΒΛούβμŽηακσΟ ŽσυπδΔΔJΦϊU’>κΏ’=w‚ψ²aŽͺ/N=~ΖqΟζε-}\σNRς²Ϋ$ΎJ ριTι+βΉ`w$PŒ @|α―{0…–ΕWhω²υ£%p`[-gσ:Ωσθ%’³dŠΔT¬+’“q΄-z±‰βΫ΄LbλŸ*IO|&±ͺqq$@$EΈνE“QC=Ο΄ο“·${Ύ ―ϊνΕ·›n³%FΤ0BblΝφΚδgΩχΙ`IΊναtΑFI€H€BG€β+tlΩςQ@“ώ­ΓŒ Ι^0UbͺTΩΏ Gέb±ͺ¨,ΐlΐ(&‚ΤΟ¬X“ƒ! bJ€»Εtb#{Xž`OΔxεμΡžk‘=ΈB°^Y€‰ΕΏΉζΘΗ‘ΰ‘H€ΕWΈΟν#  (V(ΎŠΥtΗΑp;-οY…§ l˜H€H€"ΕW€Νν%  ˆh_=}4žH€H€H P|EڌΡ^   ˆ&ΐWMDττΡψ?Π7βΫkΰρ*|ο«)Χρws=&ψ§&ςΏ 1V±%DβυΟϋΰ%°™»΄(γςηΕ  ΌP|εE…Χ"”€‘gξΙΪ IίόŸ¬γΐ»ΑTeνΦΌυϊ·yDτsΔIEVL‚ΘξΉβΣζE_­S­žώΐ?‘Γ°„5Aλfνυ ™H€H€’–ΕWΤN}qΈ /_¦Δ”o,’ΤAdΧ&ρνX‘b«€Ύ+]bΚΤ)ΣN_ΦΊ[|[—κΰ΅δiΙΩτ”Έν υ u‘Ψ²$g» ³_~ŒχnIi¦gZώ  (~(ΎŠίœrDΑœ ΧuKΡ·CuSΝF[­Ž$ͺ‡*ρ υDHqz-&΅œμ½γ4‰;ηVIΊγυ–©KS‰ŠΥΥ v@Εj;Ώ—½ή”R=n±|_¦Ζœ•I“ΔŠΥ$ΆΜ‹²χω^ίόdup―ω$ΆR5I,_Yφ½ΌRJφzWJž΅ΏήΎ=jO})₯)$ϋ_ΎVbͺ·ρoGZ ώ  (N\pKqΗBΉ Δ+T‰iψ–/λ€ˆŠKύγΎ”ΧU’Μ9S­Tb‹“%ΆAuIθςW^>υfνό¬μΊο/κ‘Ϊκo)Gδͺͺƒ*S2fό,{ί~Qv΄N”}Ώiωqu‰”,'»οΏHr6―WΡ#ϋ'•ΧΗKLΉ:R’ΛEV.}Μϋ²£B²€eη%NW\Mέ†άΏξ5_όI$@$P¬P|«ιŒςΑ@d9‘…νEάέxŠ1Χ]ξ ψYεμΦ§G<&ΎU›$ύέηΥ³₯σqρΧζJ‰MM΅B™σgHζJΦΨΟ%cΚ8»ζΣφcΚ6‘ŒWοΜ‰_I\νf’τξΟ’xJΓπΆ©@ΛYφu@θ!N,η=mΎ^KέŠΤ ~M m:HΚΌ…’Ψϊ4;-SVb«6_ΖJ΅=—α–Ο$@$@‘O€ΫŽ‘?‡@d©n’l TcJh`}Ίž«ςmC6Ԑ~‡Ές±sΙΒy–ΔTͺ¨ο,Ÿc§˜€2ͺ}όž'ί>}?βτ“΅ΎzΑb΄?ίΆ©RͺR’σ…vΝ—ΎWρ5j 6ιΣ–(k G ‹)­1`ψομU!ζKO—ΜE܏\ŽΎ#ΒΟo³Ώ2’  ό―uq™Ι¨‡Š<™¨;u9»ύO#ΖΧm"ρ]Ÿ(όAƒλ/–ψFΔ Ϋ†Ύ ͺ}βόΒ*65Mβ;]+Ύι›$±ϋΊΝˆ§$}’=χGΙΩγo+‘as‰kyΆV·A4ΥX,M>M1U:JBK}w˜¦ύγ'ΫKiΌΦ7ΩΉ½³ ή7KώcL }‚Rw ³—Οχ‹.`ι#_“UjʞGO—¬Y“%kαLΗνHΕΧQΏ‰@·<  „%zΎΒrZhΤΘVTu}ΣΓΔ1RβΤ3ΤK•*Iw='9Wί)±εΥϋTZέVš2±mxΐetk/ι¦{$»{‰«ZΣ.e―ϋC²g“¬¦gI <¨O1&=ϋ…κ•B[H1 €Ώg£ψφͺ§+­’$4k-IΏMΔ–§ZΎmΖλӐ[Χ«· .8}Ϊ±έ·h¦ ­’Ήx$4i%₯oλ'ρν/ψΊ$NςsΆnŒΟ­bRE κœ5Α$@$@Ε€=_Ε`9%€·Ψ§΅’¬οȞ‘/¨ˆΩ¨^¬’W£Ύ ―œ]ΫeίΘJζg˜3΄<ήΡ…Γ– ’Ή`¦ΔU―£g1’½~•μ}σ ‘ς‰’υΥC’ώυ&žbSΛλΦci}ΔXΗΪ―Ÿ έ¦\/ιŸΧWJlSQVYΫv”ΜωΏ© S™βkλΆšώΏQ’³m“κ1%«ΥX$΅₯>ρx‡d©ƒ(„ΐƒπΚ^΅TφΌςO}{ώbυ|ι–ξυ0”‰H€H ΨˆΡΐaώΧΊΨLgρΞ{.ίzέ',‘n-‚Ί‘ VτmσΎυ3%¦b=‰kvŽΔV©₯O•œ…“$g₯Ύ ΅fρ-˜$%ϊPJwΉδl\#»ώ~‚Ύάτ<υ–•“œ₯ίi¬˜ξ –i‘Ϊ)K_²Ί@b랭Αυ%gΝ\ύC1΅.VQ₯lΧZυlΝΦx1}Wυ¦βΫτ‡–/±΅ΞS§―’ΨΆDρzT‘•ΦXb*¨πJί%>υ˜ΙžEϊ€d†>Uy‰Ύ‚BŸšά±Mr½oCŒ±Ύα-σΊθ<γΖί“άΏF·=;IΚσϋ·J=ΩόJ$@$ήΈνήσCλŽ„žΜΪ£’€΅zžvIΦΟύAχ–Oͺ₯oΧλx{Ό:½μMχΪvLR²Ύ›λTfcT/ιVbΙΊ*Ό*j;Ί©―zˆ©ΨJE•Ζa­UΑTB·%Sšλχο5―€z§Κκk#ZκŸΪ’1b#t+²‚Š΄υύ_Ώψ½V‰ΪŽΖmY΅Λ·κkt¨±œβ7Ti₯Aϋ+F«,Γ^Ί/₯›šwL°…šŸπ:,K$@$–(ΎΒrZhΤΡPo‘ Ρ->ϋ#Φπ!psΧυ­τRIΏΞŸ.ϋSυΝς›Χι’ Ά$υJYY-—½_ΏcG^ΒΩϊ€cb^RκYΛΡνDˆ0{ŒRΫ…P?#“¬ύp"ηhέD\XΐS‡2j”¬η`6δaΈΖ/$@$@αH€ΫŽα8+Qo“; κ'ΎΙ)’1σK‰©_G_±MΙDϋV›Š¬’iϊg“HBΧS<οψŠv.Q†H€"ˆ_²A“U¦β΅ ›•³iμ~ψ|ΙYthb½k9λί―’ΜγKΉ@a'σ€άL„O!ϊ’+–d€Ϊ‹Ψυs³Εά†0M‚ˆpΟ§Nρπ'O2LϊDEx#LOζp$©ˆ‘ΓMEβ=Άρ„"„Ε»sΒΗΆΘΜ\<Α½›$%Nκίb&Λcr8©ržκeRπ>B©8ƒ“χΗ–Μ Ωt#DΙI@²ͺ gΦ“Ύ8HΞ’δΠiς&ύ'Y ˆžfΤ;ašyŸ ωތΰiNϊ±εq²ΩQΣΜ—ϊFN³dq„¨ϊ:]„AΠ¨‚ήB_`L†i°lρ™0 ‚£ΰp*œηΓ…πFΈ†Β πEψ|ξƒ_Α£(€"‘θ(]”%ЉςF…’P)( jͺU†ͺFΥ‘šQν¨»¨>Τ0κ3‹¦’hK΄+:ζ’³Π+ΠΠθΓθtϊ.Ί=‚ώŽ‘`41ζ ‡IΕδbŠ0e˜ƒ˜Σ˜Λ˜ϋ˜AΜ,KΗc°Ψxlv)vv7ΆΫ‚νΔ`Gq8œ:Ξη† ΕqpΩΈ"άNάQάάά ξž„ΧΑΫβύπ x~5Ύ ?FP"\‘!°‰p€ΠLΈE$Œ•‰ΖD7b1ΈŠXN¬#^&φί‘H$=’3)œ$$ΚIΗIWIύ€Οd²Ω›œH–‘7’‘[ΘΘο(ŠΕ“’@Ι¦l€ΤP.QžP>)P¬Ψ <…• • w^+ YŠ σΛO*ήRV"()y+q”V(U*QκVU¦*Ϋ(‡*g*oP>’|Mω… NΕHΕW…§R¨²_ε’ΚEΥ§zSΉΤ5ΤΤΛΤA–fLcΣh%΄c΄ڈͺŠͺ½jŒκΥJΥsͺ}t݈ΦgΠ7ΡOΠ»θ_fiΝbΝβΟZ?«n֝YΥf«yͺρՊΥκΥξ«}Qg¨ϋͺ§«oQoT¬Φ0ΣΧΘΥΨ£qYcx6mΆλlξμβΩ'f?Τ„5Ν4#4—jξΧΌ©9ͺ₯­ε―%ΦΪ©uIkX›ν©¦½MϋΌφUΗ]G¨³Mη‚ΞK†*ƒΕΘ`”3Ϊ#ΊšΊΊ2έ}ΊΊczΖzΡz«υκυλυ™ϊ)ϊΫτ[υG t ζ,3¨5xhH0d wΆ~426Š5ZgΤhτΒX͘mœo\kάkB1ρ0Ι2©6ΉgŠ5eš¦›ξ6½m›9˜ Μ*Νn™ΓζŽζBσέζ g ‘E΅E·%Ω’e™cYkΩoE· ΆZmΥhυzŽΑœ„9[ζ΄Οωnν`a}ΐϊ‘ŠM Νj›f›·ΆfΆ\ΫJΫ{v;?»•vMvoμΝνωφ{μ{¨σΦ9΄:|str”8Φ998%9νrκf˜aΜ Μ«Ξg/η•Ξg?»8Ίd»œpωΣΥ5έυˆλ‹ΉΖsωsΜpΣsγΈνsλsgΈ'ΉοuοσΠυΰxT{<υΤχδyτ|Ξ2e₯±Ž²^{Y{IΌN{}τvρ^ξέβƒςρχ)φιπUρφ­π}β§η—κWλ7βοΰΏΤΏ%°% ›­Εζ²kΨ#NΛΫ‚ΘA‘AAOƒΝ‚%ΑΝσΰyσΆΞλ 1 …4†‚PvθΦΠΗaΖaYaΏ„cΓΓΒ+ßEΨD,‹h€F.Š<ω!Κ+jSΤ£h“hYtkŒbLbLMΜΗXŸΨΨΎΈ9qΛγnΔkΔ γ›p 1 FηϋΞί>0Ρ!±(±kρ‚% -ΤX˜±πά"ΕEœE'“0I±IG’ΎrB9՜Ρdvςδ7wχΟ“·7Δwγ—ςŸ§Έ₯”¦ΌHuKݚ:$π” †…ήΒ α›΄€΄ͺ΄ι‘ι‡Η3b3κ3ρ™I™gD*’tQΫbνΕKwŠΝΕEβΎ,—¬νY#’ ΙA)$] mΚ¦!ƒΡM™‰l­¬?Η=§2ηSnLξΙ%ΚKDKnζ™ε­Ο{žο—ΣRτRξΦeΊΛV-λ_ΞZΎo΄"yEλJύ•…+ ό ―"J_υλjλΥ₯«ί―‰]Σ\¨UXP8°Φmm‘B‘€¨{λΊͺΠ?θXo·~ηϊοΕΌβλ%Φ%e%_7p7\ΡζΗςΗ7¦lμΨδΈiΟfμfΡζ-[—*—ζ—l·΅ac[ρΆχΫmΏVf_V΅ƒΈCΆ£―<ΈΌi§ΑΞΝ;ΏV*ξWzUΦοά΅~ΧΗέΌέwφxξ©«ͺ*©ϊ²WΈ·gŸΎ†j£κ²ύΨύ9ϋŸˆ9Πώσ§šƒK~;$:Τw8βp[SMΝΝ#›jαZYνΠΡΔ£·ωkͺ³¬ΫWO―/9ŽΛŽΏό9ιηA'ZO2O֝2<΅λ4υtqԐΧ0(hμkŠoκ<x¦΅Ω΅ωτ/VΏ:«{ΆςœκΉMη‰η Ϗ_ΘΏ0Ϊ"nΎ˜zq uQλ£Kq—ξ΅…·u\Ί|υŠί•Kν¬φ Wݞ½ζrνΜuζυΖŽ7n:ά<ύ«Γ―§;;n9έjΊν|»Ήsnηω;w.ήυΉ{εϋލϋ!χ;»’»zΊ»ϋzx=/dΰΌϊ]ϊϋΧΑΒg”geΟužΧΌ°}qvΘoθφΛω/_‰_ ύ‘όΗΧ&―OύιωηΝ‘Έ‘Α7’7γo7ΌSwθ½ύϋΦΡ°Ρ'2?Œ},ώ€ώιπgζηφ/±_žε~Ε}-fϊ­ω{ΠχήρΜρq1GΒ™PˆΒ))Ό=„Μ ρPo@œ?5OO 4υ 0Iΰ?ρΤΜ=)ŽΤ!KX ώˆ-ΐxbœExb$Šς°\);Ϋ©Xdd²Δ|§€o’ρρ±έγγί Ε"σMKΦΤ?!ΪΘ7E.ΰή·w5}/"αX=­½Ά pHYs%%IR$πiTXtXML:com.adobe.xmp 1104 1424 1 …”#@IDATxμ]œVΕ½Ϋ,έ]K§tI X(JΨμ° [ΔΕV@°‹€»»»kσϋŸsίΞΗΫΟ]X–EξόvΏW“gή̜ΉsηΎ°œ˜3 Cΐ0 Cΐ0 E άp0 Cΐ0 Cΐ0 CA>„…†€!`†€!`bΩ^Cΐ0 Cΐ0 Cΐ‡€dvj†€!`†€!`AΆwΐ0 Cΐ0 Cΐπ!`Ω††€!`†€!`Fν0 Cΐ0 Cΐ0|Aφa§†€!`†€!`†€d{ Cΐ0 Cΐ0 F}`Ψ©!`†€!`†€!`ΩήCΐ0 Cΐ0 Cΐ‡€dvj†€!`†€!`D†ΐ‰B hΤ<„…y©„Ή“•¨Εk†€!`†ΐΏD $Ζc12’<½lœςdŠΠ¦2Γ Π©_©Γα#=?uJj95 Cΐ0 "'™ ƒ@¦ΰ?όš))πC’y’Θ¦’wδ3μωLχ 9=Ir κlΛ–­²gΟILLΘ¨()?Ώ*T(HžOOd}Iμ¦!`†€!`d#NA&ι JˆΨΏWRvοτξ₯$KXΌž+H„O „aψηΗμ˜fx„Hr‘£ ίψυσ4ωN'ŠΤη‰3’π’%%’dyxBž“‘g> u‘Q‘w^3Ÿι†#.aH8( “~“˜–qy,>mrΗzε— /[ΆL¦Ο˜%kΦ¬Aή+γγ%GLŒδΝ›GΚ”.-υκΥ•Š+hRώpǚΆ…3 Cΐ0 Cΰx"prr*‰$ΉKœ9QϊI’—ΐΐεΙ Δ‹Δ–“π‚%$’J‰lΠF’*א°œy$qώt9ψ^/ +X €31σX€μφKxρ*’γς›εΐ}qN J@Α‹€†Εζ–°BΘCΩY₯–DV¨ŠτψD7HξύY Ζσ€Ω}O‰<³«δΌβκ!yγj9πF/‘(~‘Dއozα2Β<=Ώξž‹/3afτ›.ΒΡαδ/ΛρΒ€+N§#¦~,νά0²>ρleŒδƒKΚ–υ²ΐ“’τγ@‘BΈ•³ΈH$Ι#]¬Θξ•’²}ͺ$ΟψZ>Ÿ¬{ŽΔήό΄vοδ1c$¬Κ2 $υΌgζ7<qΖK v7γIž£φm@š )$§ι9ήN ݍόε‡ΧφχKŽnwHDρ²A›&˜ͺ‹„IŒ‰’2g¬$ξέ Ιν/–ˆ’₯4LXΉϊ’ψΑ#VΎΘύNHΝΘΑO“%ͺv#•˜;bμβŒ}¨$ς΅„U,)δ-"ρ‰^D½jΟKJ’Δψ©P4ϊΝyύ}Έ‚žΕΞΝHŽΗŒ§€ΈMλVR\Ω4$‚μͺU«eτ˜±2ώ’!Uo YνΤ™·τξKޏKηp~Ž%Ν£ “]ςq΄ω¦γ]fγ ΕΜ]KNΕ0™ΕιXΚζ&‹§¦aEœ ŝΟψο0sώ2{τOFώm\™MΣό§"Y+Af£› μΫ-{λ*Ιs–°²Νΐ@χA¨ ip$ˆqΨ^2XiͺT₯‡‹„΄uͺ„AΨuή’8zόεή>b›Ιuj ΌΎ„UΎJrέφμλs…€ωB*›† ;ΑΪαdž—ΐΖι ¨Ή%Χσs‘:Q.„$³CσTφφΉIR–Ž•ΐΆ5sσ»ΫεV-yνrΩ{7€Όyκ#>”1:·O’Ψ§FHLΫ qε!ΫΔςΊ²·Gψ­‡t€Mό.›$9&9ΪuψηM•}=@˟)²υ/Ιύϊb‰(WΩ+Ο?Κ‘AŽϋ;XvΰΛ—―/ΏϊFJ”(..ΎPςCߘΞλ€]±<ςΎsηNώέHΩΈq£\vΩ₯R©bEυ:χΜ"Β€€$IΐΙ .‘‘‘Π’‰Π2―τ.\„ͺL‘5ͺ§‰aΕIΒΦ­[₯AΌ'ΑΉ|lήΌY–-[.Mš4βq²sTI²ώ΅ΎXΟQšλ½kΡΡΡZŸGιόω dΧ]¬YΣ ƒ9ΜΆmΫ.Σ¦M—ƍαΟ—eοm†Λ’‘ν†ΙΆ£¨Oύ[7}ϊ )X° ”/_ξ΄Α4=Μά{ϊΜέwΗΠηΗr}<γ:–τ-Œ!έΘz 2‰χ³$O9 bw’ΡT©g`Υt‘νΈ„DYΐΓς€`„ƒΐ’μm‰ι3L"Š—„Ÿ‚ΉˆυxL$9B¦ηά ’-Ψ,¦DAΎ*IΥ‘ΥO΄έ ‹Θ¨‘uθF#a₯šHΚ†ΏεΐΗ/Jξϋ^q¦ώ0ύ#νTι1UF’Η~*a•@\>aΨνέΊ£D+-₯+HΤ…/IΒ§P›(βΏ[ΒJ”’ψ‘ύ%ΊaK”€25ΏρΏ …΄œ0 δoΟj ―}–Δ4nƒpΠMŽ’υ\ΌH’ΐA‘ƒΏ~-Ήn~ΨΓ‹·³ΐ‘Τ’ r€‹ˆJŽIŽyƒ'Ÿϋ«†χωœώΎϊϊ™9c¦T¬PAύΘμΊό0Ÿ/Όπͺ-ZXβ!‘OŽ₯K—’‹.ꨀǟ ΞΉΑΔ‘x^»{Ώ{φσΟΏiΌ$ΘιΕ1{φlωξ»δ½χή –›ώiwρ1Ο.Nf%\7ͺzO—Ύχ ―Aj“8‡—g>η€`Ŋ•roΚθΡ?#m—‡μrtυ7uκ4yτΡΎR©R²PrvΰΐAνA6oΩ&}ϊ<*΅kΧΥ χώ± ‘xρΪαύλ―ΏΛΤ©3΄ώ‰‹Γ~ˆt<§^ύ9ηt’jd—/Ώ_ϊs˜3œ‹η~ηχ㿟Ξ]ωΆ›}ΈΏ ~ύ:«Χ݊₯Sž;LXwz$„χ½χ>ν„ΩωOΟoθ3^;ι?£0τ—ݝΛ;σΉwο>6Hޱ*tpο%ίΩ;Αf](P@ββΚΛϊυdυκΥ:Iv82}s†€!™m–‚€ΞP’%qΒ‘’:ΔοBς Ώή’wKτ­ ο[E’WΝ—”³%yΪ%‰ΡΆ‹δhίIgνε:½ͺnΦΓ1€8©lΐ_ ή OέΠΗ΄!D Γ ηχγ?χϋΗύΐΊι–rVEZβΖ’ $yό[’tΩmY©{~/t\ψ?ό=‘"“΄ωΜ!υI5Rb»έώbΪ_*‰#ξE|ά|‡Όε*!)3”ψIJŽ—2ƒ’Όf™$~]β /žθ<X1_rάφG’Sβό’τΛV…ψVΌ$xD’:t–ΘςΘ/Iυ –"»}λΦm²fνZΥ9¦Z#zαϋqχι/|y„[§Τ"EŠ7Ÿχγ~š(Ζ˝wήyŸΣΕΖΖJ±bEΣ`ύ9¬xΞΧΧ9—NhΏ—7ηΫ;¦†ΦκΥ+§‰;m¨μsεΚZ΅jy΅σ’+gN‘€ώύχIοήw+žρΨ §™&‘p.½²3>w»UλΛ…qΈ»4έ‘ρΥ­[GvCŸ>O`‘KIMΗωqG7ύΈψxξw~?ώϋΩρœν&>>AΫ ΛΜΌ“<ηD=„bΰΚε°`yάyθQŸα§xρ’Ϊ.xΝπΔ,Τ―Ζ‹ή›χέ3ϊwΞί=wyq~²λΡε“}Σ·ί•―Ώ޽ΉA”χKωςeδφΫo‘Κ•+iφΣ›`σ+3ΟŸs.n>ηmNFΪ·oƒΆR^-Z,?ώ΄Œυ£p5~ήΕλ=σϊ"η‡Gϋνω94±ττύρΈΈήωqαά΅ ¬B k 2J€€5° ’ΣπXΆb―΅‘£₯CLσφ*e•fm΄ό)[ŸTisβ·ΧKΞ۟Υ{‘«Kž/WrΤΡπa°‘ΌqμώJN°nΊƒZF`Χ\‰l~—Δ^έip°DZ”ΘΕΔ*‘•$¨uθ}<βζ·4ώIΪα}γ:98δEIY5Z$t€IjRξd,‘G²Ώ~Θ+GŒΏ@žΏ°Š Α`!ώ…šHXɚ"O’Ϋ\=δ2(_œDuzzΥ=%¬ €ΘP/ +U~^ΑΖ»Άž·€Δφ-ΤP=’s²ϊ½k$ΌΑ9έΈα°91ώϋΑPΏΐ9U5θPtjbΔς•Dήϊ½2xOμο];eΧξέ ςόbF©ΊΞ•’Ϋ 9»–9+Ζ˜š+˜Fύ‘^½Ί*EξΡγ!XΨ¨$Y$ύΣ¦N—;v`*'5”άΉ=bDb=eΚTUQ DœKξ₯J•Τ"sΠXΉr₯Lž{φH@Χ`2]^ρ¦zF¨>A…γΫΞή½{uUŠRš†$ήΕ‹ΣηλΦ­—h[ε1Ιώλ―IΑ:ζ‰Ρ₯K—iη;ΞϊΟc ?ΩkΧΆώXΎϊjΈτνϋ¨Ύw\>ό;Ήζš[δΛ/£)£x†N°]Yτ]|ΌN{ξMFψξ±o©V­RΠύϊύ«'ό8 ρXσ㎼Ικσ‡wΟάΡΥ―»fΉ»½ηζss†@V"u,Κc’(Zy_XŒwNI'I-Hίή1½vΏόν;Iœ3­7‡Δ^rδy―GœIpaώ-’D9¨,€l/+α…aeΔSΒOΠ‘X ‹κ·8ύB½ώ.Ό`Qφΐ]ၽΎΊT9·ϊ‡_ψͺΣT’/ΏGΛΑˆ#=b„ ]”"Ω₯‡ς Ύ@ό~Iψγ’(Wς/Zφ‘Ά‚ΈBŠμ\LϋK$Œc<€ι*ι-*)Σ&H"v ¬]$|ύ$Β”ƒ%Gδ„ξσ&‰Ήδ.”'ŸF‘8oΊ$ύŠεω‚ βΙP‘ƒΔ:¬€Θ#—€ ½{Δφ:Χα%&Bέ’<œΉϋ%νžηΝ—W„™:H§2.£ψŽζ>jD‰υXιAlΩ²€ƒ5T*ΙϋΠΛ–)ƒw¦›7ί oΌρΆϊ§δμέwΚοV"4gφ\£†:03,Ιο€I“eΰΐ΄l\ΎΏωζ;AŒΦς±’Ν… —ΘsΟ½„%Ξυ²vνz9ό+dτθ±ϊœψ ό‰άv[/H»Κͺ•k€K—λe 67rπΰςkοήΚσΟχΧΑ’Λύ@W¬X‘α—/_}ΪΛ4~ͺQDΑυΎ}ϋ呇—?ώ ~ΒdȐΟυœ“–'#7wξ\ωΰƒdβΔ‰JZθds€ID½ς7Ϊ>ž΄<£8ŽΧ}–ΨΈ:£D“ŽΔΘ9NΞ>ϋ:ŒgAJ(Φϋ1™ιΥλΰρjΌ8‘c]}τΡ'zNΫέ<π€ΌςΚk:ω!QkΥͺ»όϊλoϊ|σζ-ΐϊ.ΤρΖ{ο} ?ό„όωηhM«_ΏQο1ύωη_δβ‹―†#±{φΩ! μ¬+Χ4ςΤŸμ„Ή?_ĝd’Žωv¬ΦA½ztΒΖηC‡—_|RΠΌ²iΣfyκΙηΣ1cΖ#ŽDΉηž‡;Ύ{^έ{Θ 3λοέw?¦‘: Ήζš›‚ν‹*νΫ]?Ο©ͺ 5συΪkoΚ“H'™Ρ£ΗΙuΧέͺ˜»ψ™―P—°vοΐ’%K₯ςKOΛ™g6Σ‰…·έv+&] ΄ύ3Δ‰“5φ'Γ†}'Ÿώ₯,Y²D‹¦q‘>8Ή>|ήλΑVs›τΐ県€€°'τΩΊ<πή΄ω/ΎψJΎωf(Ϊΐ2ή:βΎ nΔf}ώςΛoθC>Σ ΎkHVίNLωŒύَ;1IŸ 6ρYό_½z’ώO?ύ\'4ή™cǎΣ|³ll3‡«?†1gœ(²Fό£ΉχΘhXt L·ΑšΓΌ‘ –C‹ΪVŠN—ΪΔ__”ΔΟ_TέΪ°β ΏuΊHt»Λ$ͺf} ΛsgΜΩιΠΨ•œ’h†:z‘_J™œ45ΥΏzuqψΓωt<έ픍«―V½*t§ϋ«Τƒ[χξ—λΉ“hοΒͺΜ¬YsTϊM©\«V5ε‘GP‰Ïω=©mΘσ;ΐζΧες—^zδ|]†υŁoζΜ™ šσ”Πq#[5 )]€ΑzŒ~†σΈΈ8(puuΛ4O„cόa©νΩ՝?Ν9b€KœάuΧνR«VMΝι+―μͺRΘPΌΚ—//Ήrε%~zζϋΘzΌΰ‚σ΄ΞyRΘ‘CG`sήυhΤQ]•r2]JβZ΄8S½φjzΥϊμΥλ!θ«ΥοΛ/Ώ)οΌσ‚κ-σ9%¨?ύ8*¨Nΐ{~—1gώ"@Bׯ߈IΖ‹˜ €€Έ†ΛΆ­Ϋ₯σePώζ¦MkΪ·tbǍ· ’AƒήΥ~8₯ΰέ¨…wύς.—iqI/Ήδj9λ¬6Šχ87fΜ8Ύεςα‡οͺžw·n—ƒ8Ύqχνϋ€bΧ°aœά‰:ZΗ\©™0a2γgΪv―θήUÌωζΝιΎ—ΩkΧnV­Z…ώ¦Άψ ήη$ϊΗ>}ΧΝΕΌΟw“?NΨkΥͺ’ΌNϊυ{„v0T¦ͺιδž’όΗ{ ΨVNξ6œ,χίί['qŒΣοόΧ$ΥχέΧG|πνΟzθέwtUdΣ¦-ΈήFj‘ˆ{"8y:τM9οΌst’2`ΐ@ω쳑ΐόzΤΗDων·ί!εŸ"―Ώώ"V γύŸ~τvτ_—@·ΊΪε­Θg/ΉβŠn:ρeψι3fΛ…h³!xΈτ;11ώ1λ6rϋ±±σΣ7’g l˜θ,c.ΌV’~z –)&C’ΪD₯Ÿ*Ρ ’°Βœƒ–δ6q§${M’Ύ}M"Ϊv–œwΎΰI’Sγ fqfθψμpϐ¬2G”€lZ# ΣΖΑ?%ΜΘΦ9’ψννV$F7".HlΓJ`ΐ(]N“ ƒ„#ppŸ$ ‡~pQ|•“ά’ $ΥJώ‘y@όŸ#$η•wiΈ˜³.†žρέHR0–'G1IY;UdΕwP­¨…ϋBGη‡τx‹δΈσ•†3`Βά©’τ;*ŸͺΖA)87 ς₯ΘΕλ!ή'$©=t‘+ PŠμ&šςρ‘”ˆ:™œν³³υ–τRγsϊ[‹Ž=ΒζΟη-§η7+οq©Χε½rεJΊμΞ₯cJΗΈ_¨P~•ή–(Q„§ ΘΒΛrΥU]±a¬’Ϊtvy₯4«iSZ9@ύ‘œœ<ΤͺU– vǁωάs[Ι1ο5nάPή~ϋ}•ΞlΪ΄ δ¬aσ9Υ;’£?RςLβGRΒ%}Ju註HλtŒŸΊΕ\Ž₯cfΜ ‹!+9ζsΑvνΪbΰz;Xfυμϋ‘ZEΛ–-ΡS`Ήa€FΏ`0EU5gΓ† ‘JHΜaη‹"ΛO)+_Ύ΄&ΞΌsB@Ό(iO/–…αθˆM‘B€}U½ζ'5”Ϊ;η—XοΪ΅'HΔωΌxρβϊΕΘψψƒŠΙLΓ†˜§ΊΪΨ@ΨuK©]z.»bNŒrηΞ₯οΉΫ,Κχ­dIt†©{χn˜Ψ½"={>ˆ”χ?ΔΗΉrΕΚ™°{NΗΈͺT© rάL'hn’θ&<Ӧ͐Λ/ο€δ˜υALHΐŸx’Ÿ†eψrεJI Aηh₯tι*‘dύp‚‹7$m’άtΣυιͺe¬]Ή9Y­Z΅’–—εb{6…ςr_ƒίmήΌ ωeLμaΑ;Ύoί^Uη"Aζ‡ΠΙύυΧί†Ύͺ;ήε2κί1¦DzΓ† ΣοΙO?}‰>­’z!η* Υ†8$f‘“Η―Ώ¦yρβŘ ΑΔ~HP_šRΰΡ£'jXFH²ώΜ3Jǎηiό4σyΣMwHΫΆm΄Ÿόδ“΄‚‡φIžό ”ΫΠδAƒ!ΤOJ'C‡*$ΜkC„“K’η}+ϋ/Ή_™ ΅ŠβΟqΓ i…ε¬() ~’ύ£³_ΕZώΑwΓJ‚SΉ—.Ga ,ψK’oyY"JΕy$’•„©γa•c¨'=NΐΖCš…Ϋ7V0Jα’^J‘K@Š<¬‡$ÜΏ§RδKί”„χο„τ( 8ύFCχ‚’aΖ±wΉD4ΎH’΅ς™Nω‘5-‚cΠΊέs%,7 U© !ω ‰Ό³/Ξ=I·Αρύu„ˆϊ”TGΰ>u©―ΛNΫuόώTέ}ϊ[μˆ)Y sρωύŸ¨s惝½;’°Μœ9ξš$uwy€/ΘM]HHΛ©‡ε (ΖIΚ½ͺcΙ%γΧ^{z|•υυ9ΰΡΉ2ω±ΰO©%ό糨¨hD(m¦Κ 'ώη1ˆƒ%οχή S…P›¨©7 u>]ZτΓsG. '2t”@£θšGΎτaΘOΉrε0˜uTρqγƁЬUύΩζΝ›cΠT—C‚œτK–e¦cω¨ήπΤSύ0ΙΩΔ‹j"Žθ…f˜d3χΞ†ϊγ5λ„ϝΣ:`Fθ½QΐΪ«7{7\˜ΠcvĜy§ΥŽΆm[‡f7ˆ5I\•*•@ά>C›)τΗͺ Ήζ* «Ύn²Aά;<§Z‹#„Ξ/q‡*Ϋ)ΌΒyΦsΨ†ιφΓ¦<ŸQ?œyεΎ|ω€[7J¬5€ϊ ύΙ^Xσ½ Ν!³οεŸX#Nΐ7§δ˜χψŽΧͺUK .CsςΑ ½rŸ?^UΧςbO›WθHΠccs¨Ž<υδωž²νL:Λmš6㠝<²-ΡΡ"FΫΆΝ”»ΌtbήΌƒ6\χrlήΌUλ—ϊιnBΏvνfLόWͺ™ΕZ©ΰ+>•+<Έψ4!ϋ1²¬%Θ,;:―ΘΚ΅%Ο+?HΒδ±’0f>άρ›VOSύ^_ ΛS£HΙ2ώÊ5“”ΕεΰΓ%gΧێ3<Θ₯½1…% RΏΠΗ|Rϊ›Έ—™a…EŠωIδω7Kμe7zι“0Ψ έγw?{JiΉbΧ‰lΡS›VHΚΚ1*Μ‚;A\Gώ―ΉGΓΗ΄½H‡ί Ι/1‰¬Jœ)ΙbzΨLΈi»Dί 6ΎθG—0{Š$ύ1PΒβ"_FjžσKTύž’4υ[„Τ’η°’"ίO’Ξι œk)ήθE5Žγύγ:θ:uΐ%K—©ž\ώόΫAfg΍N£FQ}ΠFPa`§ŸU Ubμk7ΐrYwΦ¬…Πυ€Rƒ $μXΎX!Ϋ€ u#Fό$;”jœwήΉϊΟM|=zά«eηR!ΛΓ:#GΒ΄hΡ%΅$Κt”oΔj' άτ8oήBΥΗσ$a"° lΥͺ΅ΊQiΜΉπ»;έ‡(† Ώ|ωJ%d<§cψOGΜ+T(―K―\p>ˆŠΧμIv]ωΥc?ά$ΥͺU+);΅3°nέΊΊiΠ©+d,Kn{εL›”+·»K=SbϋΩgρZΆlπςκˆ0ωΓπάOTΌ4<,§SρπΞ]*‡ŽήΔ+YλrΓ†Ν:qδ&3Φί£)Sf'b‡B₯=Λn˜{€Ν£ŠΟΉ©‘ΊΏύϊέ#o½5m©―Jr‰οΚ•λ”ΐ-ZDq ιœ9σ‘ΆrŽFΓφο&Ÿ+VЏŽΟN )έ€“€J 0ϋ'Χ‡πΛLέm7ΉrΟωŒŽX8$ŸT…hΤθΠδή+!ΌΌλ΄Ώ OΜ–,%L6xΝώ瑇ξΦ>ƒΧ11QΘλ‘xxΟυ!œR½‹Ξυη|ΖΥ*VΥΘonάΎ}‡¦Ες<όp€3άΓί<Όώϊ»ΊFaDΙ’ψXVκώΜY@ΦdŽkήx@T.»¨4οPcύW€‘ ‚ vΜ‡ŠHu€u”DόdΛΎΩσΐ`Ιqξεj £+™Vͺτj•AZ@ZAl)}޽’‡$-Ιι=\ΒjUΔ³=FΧ†ϊCOIn‡―λAΝΝ…Ρ—Ύ#ρ@‚ρ•=Œ£Loο‰hI’΄Πbγ‘{ό‘7χ₯`@ΒΎuΆ~έ/ηmΚή§·`’1’dκ]bΰǘΣη ΘύoδόΈAгύ3›5‘qθœω—τά8Ζn6،5Fv€$€ ₯ΟYι˜κβ’drp‘Δ–z‘?ώ –ϊ©₯ ϊ‘$˜&Φ¬Y«ƒΕ—_~ Θ.°IrΊu»Nξν}'–Λ›λͺO8λ dπς€)οq0‘#'Ξ…Žε`ιΤιBΔ©κΠΉsG•zU¬XI₯½οΏ‘t’ >οΤι<ΕkιεHs»n(δ’ώΔ‰“ cό›Zΰ`όLRW=xŽ*ηžΫ]- P―zρβ%0‘φ1$}eƒως|¦K"ί€qciΨ ˆήΣχ}’ο[oRuΰΎŽιƒšuη€’.δ9‘XΈp‘ͺ@8Ό4¨§^H”½:c“¨ΊŠ Λ#ρ€ χν/΅~LΠ{ϊ0υ9γβΒ₯KCΉ+t:Εϋυ€nϋ~δOAηΒdtΜN˜³ά|ηΨnHzxνTŊT}θρΗϋa’x»ΠjΘυΧߊ ά·Aέμ"E Κ AŸΘ 7\«νdĈ‘ž*tτΤ\(₯¦~2WGΰ}γDξ‰'žΗŠN/υ›ς怏z?υλΧ…Žώk°τπ5o-ΫΆm“W_}η-‘ηκ­ 9ΏιO&Φ-ΕΕ•—3ζλD–mΤ‘_ζχε—ϋ£O*‘ΊΊΌόi&ΌηHσ'Ÿ|†M©·ΓNΌ­“²#~ ϊwιιCόΈtˆγνΪυrτCήjύPMi<₯ɜάΣΟ!η΅C^³/Ÿ2e†bΟI7ω-[ΆFΓ.\HυšΟ>»]pούμή½G%Χ$ε\Iσ ξΎ›‚‡qΠYξJ―ζ ,E k ²Χͺ΅€Ύ( ύzKΤƒΟ υq#γͺͺ…‰¨3šˆœwΉ$-œ!ϋiΜ‹u+Ω‘ͺ@ικρvJŽw‘*Ω¬«$M46ΠΈ€@XԜ(Ž V0Ǐt`΄Γ§’χ@χψ-¨O 3P»Ηθ`6̐θ˞Q5 ~ψ#δ?°y1€ΣΠ‹`pθxJΑ―ι©₯ ω·„αCx ΊdNΔH₯Η»$ϊžCγDH“όPΒ*€JωYnŒΩ1gƒ°Ηζ‘˜ “ύc‡@ύωƒt9¬6BώπŒ$ )r•380’μͺ΅U«–0HΞΙ—ψHΕ qϊΩιΌyς¨Z—π¨VA)Ι1±Ÿ°ΛΏ#6?QoŽƒnhη}Ό«šiΣΎθ»οΎ―DŠƒΑΓψΓκ¬9Cΰ$ ΅ŸšN-ΰΑ‘ŸΘΑη0 Wo΅ŠρP†'¬έE"ͺbΦ\¦€ ω$yαIόρ)<ƒ/%ΕΠΙ μ˜-Qmο•\χ<2‰A›€2›λφήΧ=-\PΝlƒίs’\wτσό2νΓω‡46κœϋ%WηεΰΟ_ΙΑgΊ Ώ†§_ΡγWψHŠ“–Hξώ+”ό2Ίψ1?ȁG/€ΏTJιρΞY’ϋ HΛV’5οvΰρ‹R%ΜΠ3ζζ½έ³αg©D”©¨~|5@βί½Ν“"c μ_!αΟ—άOωιN8 ρmͺ†κ T&Pv–/ς¬{%wO υΣχfHΨ)E†ΰΨ2K’Ξ~Prέύ ρΘ€&x~όΠςε+d¬¬^³V-p@%i#Q.]Ί”P­‚’cvδ²Š$³3ηΏ·LNBŽέ#;‘°p’Ύ/U ΨΉs"Arδ‡’J>§δΕ©B07xωΙ$ΓΊΨmcΊΤ¦:βHGΏ<§dšώœ:ƒg¦My€k2³ώ™¦°όΔM#ΕΙ8)bX~–νTtΐ‰ΛΙ²Πyχ<;ΣΌ>ZΌ\xm§χΞ€θ;ΜψH0άsΎΫ¬χNxΟ) σ–ΘΥ\ 6Kς}§c=]~Ω Aw©TŸιΉ°κ!›ώj7iϋbΔ6AU.₯σύs˜σέ&ιεΊΎ}Ÿ…ώKϊήQ%‰ο{hϋ ŒΛ9―ύνQΥ δΥϋ”PΗ|R…‹„>½6κ?»\;ΜΨΉ±νΛ/‡’_Ι©j %K‡Uˆ›‚6¨G Λ«:‚{GiV+[wάq›ξ‘ΔyΫΆΑΙύΈqTzΟ=Ο<σφVΤƒ9Δςχί“‘Φ7˜¬<£υAU–>Z!Ξάθ{Λ-7κqιe° ς&τω ξ=ΰgΧi2’αωΞsDσp’Π¦ΩσΠ₯ψRήLαζ=ίΐzH»=/9oΈŸ#³ζ-°o·μy€Kͺ?HυœΏξ/JΞλοΥ’₯l^'{g₯1!ΐζ¬₯S$ησΏKt³vϊxπαJŠs$ʜ¨ΣΏl|žžΰΑŸ7‡ €ΧšNt*ŒŸd.eΫf݈Ψ2&ή B©nŠΛ}†„εEvΚ ΡΣ/7qƒ^$6žQε`Α‰Όδ‰ο™ Rk^Ÿ©ρκ†>~rZ%Ȉ‡‚J™3rάκŸaa‹9 zP±ΧΓJ@A8¨/Φ…•hŠΟ9Ώ)ρ-:jS¦ύ*aUš@₯»x‘όƒO=:1~¬„ω8π$ΝUβ ‘†ΝγRψŠΩHXπΐ§³#ΚT’π’₯ ‹ό t‘o†™;θ&7ο"Qυši4” Ǐό›±Q›ωΩι-Σ$²γ Η οpaΜ7\t&ίφ:θp–œ‘GdΊΘQUα—XgsYaθ‘ρ?Τρ9₯K$Εt”$‡£ΎhO“χ]ϊ0 ~B^wνςα]VxνΓ+ ―Cύ8Ώ½8οβrXωγOοYFiψηφ_>?VΌώ &ξ½Ήηžͺo9jΤX%-Tuα§­)Qv~ώM:Ω9¬+υδ x Rc¨…ΑΉϋ^+ƒ{FΏtξ:=Ώξžσγγξ9?ΩωΘΌΊΌ³τJ‡σOBJ*ύxϊεuA#θxν'Σώs\.}©ζεœK›ώθθ΄cˆ?<Ÿ?φX_έXΧΉs'%Ί|³pg«Ϊε…ρsՍώ{ξœ—U]ϊκΩ~ ,D λ$ΘΎB%-ž#ΏϋP’ώμΙ1@ŸHθ…qV bG3fI˜ΕξΩ •̌»=,±ΧφΖ§˜ΡθΩi’‘Ήc2Ύ>·χΞr*ˆ&A‹€ύΩMΫ@¨‘‚p·§‚ Ι§†ΛΨoψ§βFόϊB>Ϊ*₯ U^§Qπ‡ Γε»–)‹ΔΫελ%ϊH…―ƒT8$ΤUήσπε’²θft y%°jƒD]ΧGrέφ˜Ζ›Όe½μ½Ώ½–-œ/ό!ΡMΟϋρ—ύ·tπš(_β*„-&²a“δώ*Uj§TέβΤcΒτρ²ζ–Θsiψ_ 8ΛH`ΕΙυα‰ͺΥyƒΓM4ΥγσÎ-Τ±stΞu|”–Pέbς”irτl©ƒθž9Ώv4N5Bίa΅όT+ΧΡδχt)ηΡ`’ΏΔΝ9Ÿιξeζψo°wιg6m—Φ–-[υ«“³gΟƒZD μ'7Ϊ:φ6ΪcrΈψ½’g~b”,̏!p,d=Aζ{OŽ’–΄|‘$.˜%Ι³FαΛΨιΊ{$¬ψη¦Όθ"ˆ“ΘšgJTΣ w Ξpυ=’?°΄v± σŒ0ѐΎ3ΏϋΆKδ­%φ*„QπƒNϊ‰λXI’Χ­[§†μ)A0gό%‘Χ…2© ,3O]λ#•ΕžgŒλΨj9C„^g‹=1²YOYφP)&Τ0‹”²g2Τ*¨š€Νia‡ΕΪωορ?ΰ#!υ}ͺ4ψœδΥ·ι#x?Sώ=Β©y…Ž•ίρ σβ:}FΙ(τ€λύ$6Γ©>ˆ$o€‰;όGΧo‘ρ3.Υ/Φ …/f¦γ“Δϊžx§$ςόΔΆs$Ιΐ<Œ_ά;\8η?‹Φyf1ΰ–œ!`'φεtnl ½>IZ”†ΐ CΰδdWœT‰μIύe‘ώ¬Λš³λH³oKΝ0 Cΐ02Fΰδδ4ωΒΜΣ›|ΊKΙi6S 8”ΉqF΄Π'"r‹Σ0 Cΐ0 C A' 17/ †§=7<ν8no“Ed†€!`†ΐ±"ΐfζ Cΐ0 Cΐ0 C #Θφ*†€!`†€!`>Œ ϋΐ°SCΐ0 Cΐ0 Cΐ²½†€!`†€!`†€#Θ>0μΤ0 Cΐ0 Cΐ0‚lο€!`†€!`†€!ΰCΐ² ;5 Cΐ0 Cΐ0Œ Ϋ;`†€!`†€!`ψ0‚μΓN Cΐ0 Cΐ0 #Θφ†€!`†€!`>Œ ϋΐ°SCΐ0 Cΐ0 Cΐ²½†€!`†€!`†€Θ””€οN Cΐ0 Cΐ0 ΣΘ-[ΆŸήXι Cΐ0 Cΐ0 aρρρ&Bφb§†€!`†€!`§7aΙΙΙFOοwΐJo†€!`†€!ΰC 2,,Μwi§†€!`†€!`†ΐ鍀δΣ»ώ­τ†€!`†€!`!˜™·@μ0 Cΐ0 CΰτFΐςι]VzCΐ0 Cΐ0 Œ ‡b—†€!`†€!`§7FOοϊ·†€!`†€!`„ `9»4 Cΐ0 Cΐ8½0‚|zΧΏ•ή0 Cΐ0 C #Θ!€Ψ₯!`†€!`†ΐ鍀δΣ»ώ­τ†€!`†€!`!AΔ. Cΐ0 Cΐ0NoŒ Ÿήυo₯7 Cΐ0 CΐAΐr vi†€!`†€!pz#`ωτ+½!`†€!`†@FC±KCΐ0 Cΐ0 Σ#Θ§wύ[ι Cΐ0 Cΐ0B0‚ˆ]†€!`†€!`œήA>½λίJo†€!`†€!‚€δ@μ0 Cΐ0 CΰτFΐςι]VzCΐ0 Cΐ0 Œ ‡b—†€!`†€!`§7FOοϊ·†€!`†€!`„ `9»4 Cΐ0 Cΐ8½0‚|zΧΏ•ή0 Cΐ0 C #Θ!€Ψ₯!`†€!`†ΐ鍀δΣ»ώ­τ†€!`†€!`!AΔ. Cΐ0 Cΐ0No"¦ψ)))Ž&ˆω5 aaanσR$vj†€!`d;2MIŽηΝ[€9,Ueižκxm’ds†€!`†€!­8*eHͺ›rΆOΛ\6Fΐ[…ΙΖ΄¬†€!`†€"pTΩ03 Cΐ0 Cΐ0ώλ•ŠΕ +Ÿ!`†ΐ·w†%β>ώ7Ρ;g* ;#Θ? mœRψ ƒ?γΗJό³?Ύμ6Hg”ΟΜ–;#άόevηŽ„Ή£»zL?Nšόχ$.ύΈ[$1½2ϊλ“ΟύΧΔίέ ­‹Sύ:»΅·SOΛΏ!@Œ Ϋ{`œ&8rp$βv4pœ*σΏΝg(ΡΚ,FĜ.=̏5ΞΜ€}"γΞLϊYαηpetοϊ¦M›dώό.΅jΥ–‚ όηH2'³fΝ’mΫ·KρbΕQΚYΏ₯aόη0‚όŸ―b+ !pHrΆyσfY·~½DEF₯QσsEŠ•R%KT4[·hρbINN>DAIF+W,ΡΡΡGί‰ςœ˜˜(‹/‘δ”Τ|’³¦Ϊ₯P!NrηΝ!irDkοή½²|Ε ;‚tc’c”ˆ(PΔ,B‹EγHΊ‹sϞ=²‚q¦~8†v²£’’€jΥ*AΏΗŠΙκΥkdϋŽν‰τΙΡ©Yΐϊ*Šz.y”υ|¬y8‘αˆα²εΛeίΎ}όX₯`'yςδΡχqυκΥrΡΕ—Ι¬™S4+wκ,Ύ+E Ξ°ΎOdž&n–/Τ₯7Ι’ŸAƒˍ7ήτ>lΨpιΤιβl_Ζ`†νΔΘ !fΚ‘s ̞=?ΐ£9Cΐ8zNf‰Υ 6LΉDlžβzDΏ€Η[n½="™©BΉΈfϞ“GDšΈ@Η αˆa¦β<ž\>ηΝ›šΏ°`>Γc λωό‘I;Ώ‘ωpχ§M›–6G0‡]θ1gށ&M[nΏύŽΐ?όΉN“†‹sκTgl0Ξ&M›8 ώ»„„„ΐUW]ŒΣŸΏwχ ΈτCΛz*]σ]½φΊ}eτ0$¦Ξ½ϋξ}ήύفΞΡσ#FκγSχn¬Y³εΚhΨ¨i u›vjΥjš·hΨ½{·–Σωs˜ΨΡ02€I1z˜3N""Ό&ίΈ~ Ω»―œJΪ(ι}oΐ;rΟέ=€zυj‚Aυ°L'Ι5j΄ΒΦ’eK©Σ0”RF!Ύ#ΘY³n—Ο)S§jz-[΅‘ύϋχλ9Λ<ρ―ςϋοJΫΆm[^/³^‰š·h"ρρρ*γύˆpHˆS ‹nWοƒ]»vΙ‡ƒΏ’wήyKΊt½Bž{ΆŸJ7ιΗ9—Ώζ- ₯ά»vν‘Ε‹ΉΗG}tυΆlΩr2d°œyfKI@ΌL“iQ‚ύΩηίJ―žχHΉreƒχ:‘l wξ\R’dy”₯΄Jκ'Œ£εdφX潐.Σ%Δ'¨ύ~žοΗ{Jη°Χ‹lφΓΌ―Y³V<€|:CS)]Ί΄ΔΖΖ¦ΙνΑƒQ˜p‰‰‰Α»}σάwPΣψ³ Cΐ8z\λ;ϊΒ0NA<‚–ΗA4δ!*tcǍΣγαΘƒ#[[Άl‘}"5kΥ•={φj\.>’½“ν\>χρ½”-_9M>χνΫ/gΤ©*Ÿ}ρ¬‡Κ ΓΙ±l,§+λ–mΫeσζmψίͺ—qδΘ‘C Kjq’EkωκΛ’ΥΩ²jΥj%f‘ι0Nώ±dσHYΘπΉ«·iΣ§«–†υλςJu‹­›W«Ύ*=d’ΈOvύ‘JΚφ{»ΠwŽX΄jΩB³>nάh5κw=―W·Cλ ;•‘¬ž½zCΥ¦ͺ΄nsޝR₯J‘ΚeπλΝΚΚ•+'wέy­pr°rΥZ™2e’\ΦωR)T¨ ϊuο„^؏!`&A>*ΈΜ³!πί@€Α‘„ύJΥj΅δ“O>•Λ/λ ύΩ‚ϊ,½Α•axΪ΄ι2sΖdiΪ¬…μέλIκΌ8ΣM—F(jιΕνό€ζpώ]8Ρεs1t€‡ ύF79S%lά¬E—œœ’ΆΩ³¦0Ξ–R₯JeXζΠx7I γͺRΉ‚JfIWvc’0dŒvνzψΫ±Kš5«''Ž“Ύ}Ÿ–·ί~γΊΩŒOη‘)ΊFW^JGŽό^J”ŠΣς2ΤƒNJJ—¨…δηŸ‘σΟ?/(UύGd©7g¨σΧƒΛ·σΓgώηξΎ;χψPΆ€€»ΌRšή¨Q#?~‚@u!\p>ˆfeυοτΑ]ήά1΄Lξώ‘ΚζόΉ£+«;Ίϋp`ΏZ˜ŽΙΓύ<&Ήrε€49Δy@/T>ψΰ=l¦Ί„ΉY1ϋmφa,σ’%KεΛ/>9l&‘ B"Ώ}Η)X Ÿn«U£’ͺ}τκΥS*Uͺˆη«Σ8 CsεβσP?Ϊ—"@θ}Ζ‘ή=ή?\|ώη—pt°Χ^{L6MζΟ[ Χ\s•‹V.lš›vaœb°-π?11A4¬/―Ύϊ²άvΫ­Ίν',–k7cƌΥR†₯~ΠΒ ΜθlŽ#όριΩσniέϊ,…$% κΧ…Ή­Ϊ/ΈvδŽ4§vλ­“E‹KžΌΉΥϟόQ ξmt0wƒΌK/τθβZΈp‘όϊˏΛJ Άo[']Ί\†Ά.oΏυ†δ¨^I‰K a+ΤEf*A+ύkHkω…t:ΓLSoαHœ4¨/ύϋΏ&χάs·4jάL1)^’ΌLG:$ΛάT₯h¨χγΚKU—αߍ«¬ΔiΝͺω’ εͺ«Π‰Ι"IkΑΒ₯ε‡₯xGξ]\Ο™3[UΪ΄i§˜?Mž}ϊyπΑϋaWxΎ 0P†|>TΆoYν‚ηrEχEŠΡώ˜‰MδsΞ…01WLλbέΊ rήΉδ-ΤΓͺU«π>|€ „_ΛŠε ƒρΥ©ΫPΊ\~©\}υURδΫ•3θα0'Lσλ―Ώ‘› YmΥͺ­pΒ0κΟί±)σw¬xerο-;vξ”ΎA>βKωρ‡‘ˆΉlω*ω εΚ+―υλk™όωqη$?ύτ βωBΎύζΔγ©ω#<ϋœσ₯{·.ΐb]Ήα;Δρ 8€%τ¦ΫžΥ…m’?^%υ [©B Ή«G/¬δG~‘?ώόSΞΒζRκΟ_vY7LΦΒ5’ΧjΥͺȐOΗ]—7ΖΛ3ςε—_Ιw#~ysgπVKΐΌ3ΜΔuB]•bξή NΐΨΫ΅;[ΓύρΗ―ς'ς\UπήΐΰΧ«wόοVιΪ΅‹n2τη'Mβvad3w~ψςΣ‘Sr1sζL½fsΟv’3ͺQ£Ίΐ Oπžz:̏ ϋχί“±DΩTϊΏφΊΌτR9‚Ξ­Ϋ•jΫΥ ΰ.wνΒςΪϝΏσξ»θ8fK74`vT$Η”ΡmάΈQ~Δ`β—Όθ\X·{nGCΰTA€ο0₯jάFIYdͺ…‹qγΗkά;Ξ ΎηΌή‰αΗ*ΥͺŸ‘* ΌO©›ί/‰3Ϋ;]£F υΘΝr”ΚQgrȐ!₯[©χ]ϋqG©oΎωR"£"eηΞέ²nέFl°«’v•ΐŸŽFΞ#Π&ό₯OχAθYΣU}fJwιΈλ?>!^rζ)&?A/Χ/ιVGϊKφη‡ηLΫ•₯N34–™½FΚΔ υ§sι-^ΌHF޽Υ"©Rν$τ•M`ε‘HbsYΈ`Žζ7|i<θCYΆl™fΓΥ•?OΤΡ¦Ϋƒόξι’”έJ2Ή‘³f͚ςϊλύ₯b\)HΓ›λfΖTzx7$ρwΊ υ»JΣrqσΈrΕ"•>’Δ­XΎHb ԘIv½-ει§ϋBʘ_γ℆“ ΗGyXΚUl 0Υ—&>^3:w“—½x?YΧtξΟ‰λk)pθ K#$β .Α䦉ͺΔ΄iΫd°4;³₯δΟ—G^}₯Ώ4lΠο' ¬{ΖΓΊίU–{zφa½T¦L!υλΧ”V˜v8ϋ\ιΠα\i +* 6‘εΛWΚυΧ_‡4»…=ŒΟaEι}¨cό*YF[₯£Νg="ν­ΫwBΪΏ[ί­Ν[Ά«Τά½.o<ς©π|ζ™~Θ―hέ‘“μΆΔ$‚uΈaΓ&ΉγŽIγ¦­dςδΙZ.Σ£T™ŽB¨sO—„zϊϊ›ouΒΕ²±ώˆ©ϋPΛΫ§o?²,ώ85ϋ1²!ǝ »2rΖNGIƒΰΰL™hQOΚ@³IsΠψ‘”…ƒwzξŽ;$_}υ™Μ›3…ΥΧΤ+Γ0.¦ΙΑœq³A±c&9§sχ`ΰœ8q²JκΧ―§ωͺ«Δ Ώ«ϊγόΛUόΩQmέΊνθTΡ~ μΥ*ψe±J+θF;ΆΉΚUjΘ§Ÿ~νΔ?Ήmϊτςχ€ρ*έ"وΖRxυκU”,ϋ—Τ9Ι€«Y³†΄kŽZ >.ΝPΡ-ΗGόΞ΅GGΪrb9Ÿ* ‹Ξ‘«μόΈ…σηλ?wω$Y6μ;©R΅¦«ΦΙuΧί(Ε‹,*7έ|«,YΆFƒV«RNΎχxρFh_δhΞQwa(εcΔ–Π‘‹:.Ξa2q’§oΝkφQtΥ«ΧΠc[Hύθ(1&Q§sύ° ―7Sά=nc½V¬\]7fώοŽ{„’]½₯Λ bIρ"©γ2=u­'ώυ7TVz*a¦ΐ’ ‰ΘQQ‰F‹Ν¨κKίswo)\(nφ\±’*Ϋ‚ρ1$YeKε—‹:uQaE(¦©ΩMχ,ήQWfwο ΟΧ]+g΅;O&Mž‘y§n:λiγΖΝ2–/FώC‰'Γ7hΨP­’peρB:–ρpœyϊιgυ=’̒^ŽcΗό)Ώύϊ3τά–™³=ιxlliΡ²ή#‘¦I@ΊδdoΌγD½Ηϊ?Lƒ>·mΫ‘·όοh,ΪW$X·ΉrΕB5㟠ß|2D'¬Ÿ ›κD•:κcP>J'Lš₯u£˜ηΝ“ &Β= LΫ₯‡Suœτρ].W:ο_aκ3Z‡Μ7'΅μK¨γΟ‰ 'ߜl<χμ3μE`Ώ†ΐ)€ΐ #Θ< ‹ƒΊΒ7Txƒ!;7vά”Θ–.WΘΣM|ξΉηε3¨58G=¬N:ΛΌyσυ–kœΌ`δ΅§bαι?]{ΝՐ&Ώ€~Ήα¦χ½χc“ΜCθΤλ ?5ΚΞ‹;Θ;œ}žΖ{Η= Ο΅YγΊχήϋd :IΞnοΌ«‡Ζ=pΰϋXξσςΓ΄’R;.&0qβ$˜ήi/W\y5–ΓΪκ5ο‡vhΌgΞΘpΠγG=ΖΞ—^›Ԑ•‚XŠώMfβΣ΅tξ½fΫ₯‘X††Ά­Ά“ysgb™Λ­g΅•%‹ηι «ψ“:’RΗ΄S§‹@tηzƒrͺμ―Ώ&i܌—iΈAxμXJ―s©TΫ鎢mΣ*ψ<'.Ώσ 0nά(Δ€ νάΎKόT•}Η9gwύ{6)asjΣSΝ£1/™r Έ.=ηŸΧξΝΊΡ9©%ϋ;κR“$e6υœΑΣb~I²† N*Θ²έ°a³tΎ¬‹T¬XACΦΔη‡sε+αIΝΡ–*]AFŒόAν3ΌΛsh2ψΜ βK’\Π1ύ ‚„XθγΚ9eJ‘ :vΐ§‹Κ”Ι•τRΊΈsη.Ώ¦*Ι¦>ϋ!δƒ0)Θώ4)ΊΠ1PΥX υΌ;«ΠOo…ΚE;©\©‚ΖΗwω’P£dΙβ²bΩωόσ/EwgLΣI>]0Ύw¬——_ξ/kV-‘ZΥ+λ I>ΛS!œ<σμs§O?½DYΈh₯bΔ —”n_~9€δP aOI™₯₯'t_xώ9νπ(eήΏΟ[’Γ¨Δ€‘Σε™g6“ηŸλ}΅ο°TΥW―Χ]§y΄¨–μ,ϋ#@ ν¬9 ₯qγ†:πΟ7 ›±ΌΑ•“X‚$<-]ΊLήΐzΓF΅U ‰χ:v<‡Šπ4Hxξo±gB%Κ9’£J•kΘx¨?pΠ€s톫2T‹¨Z½‚¦ΙΑœͺ5kΦRiϋ½υGXƎ§Ο88Sχ™v­ΪzδΥθψ! ΨE‹—SςO©WfQ”RΙ γqaxdΏ΄fΝZyεΥΧ₯FΝ:ΨTuP‰ω†u+„fΈ‘ ?JŒεθΓŽ*m@7•’’uk—ΛΉηžά<Ε tWv½RσΥH$LuΏA_ΈdΙMΤΕ“^X&βX²D1H'Ι‹/½¬Ίέ~0&ε†Γ<ΰθpOU)&ύξΩ³OΚW¨ͺϊΌά΄EηγlzΝ \… Πp?άSƎύ]>ϊπ}ωφΫ―ΤLέΪυ[τb}R·Ίφυεƒ?V)²ί!¨εσI΅Ίώύ_V‰)UIψŽιΣ&Λ³ΨPΝ瑐Η{D~νΉΰόv―Ά©^―ΫS3κ&./ߍψ^Ονωϋο 2ωοΏtΒςďKϋφν`Vο|yύ΅Wε©>}5 J‘Ή‘Ξ©!6hΠzΛ_ΚK/>}5`egpB·pΡZyτΡ‡ul6τk΄ΩFΦ½οz‘ΞΫ>QbΜΥ¦[ΆL)™8ώWy*,Χ@ΐτςφνΠa ϊΣ”œσ-…‰ %Μ£G ΖZLŸ›Ή9•{Ύύφk΅’B]ηΆ­› I2' | Μέι Ϊ― Ζg'†@vFΰ„dΞΨ))Ήψ’‹δ₯WήRύ,A]%J”ΚC/n_ͺ^7΄¬‡Ί`t\†yςρUθη—ΐ¦MŽ θƝw Ηά κ}ŸzBΓ²ρsy―rΙ’%u©§χ½}•ΜR›Gzχξ©*$Όά]Λ%?Ϊ~u;q#°DEI‚sNίkζΜYrΞΉ±ϋ»’ͺoT­ZEŠ—ͺ¨ϊqτ{ΈAΖΕeGC » @se›7¬”Ό°L@3gtπκCσΕžW»ͺz3υΗ}H„KΈ[·οΐ’t©‹/`ΞQηχΒ‹.Q)!x‰Β@ɁwΩς΄:°œθRzWlΆsJ¨/»μln+Ρ“ΤΞΉφ·yστ3C•œ’SšziηΛ₯B…Έ`πΈΈςΨ°Χ{6)y£…‡Ο>ύ«]KՏ›„œ0'LΟωγ‘$€KΛ”ΨυΖΚΤ’₯«A¨bτ>—ΑιZ·n₯Z/ŽΓ#H΄ŽA—LΡTξX·N½G HΚIΤφοΩ¬–,\ΈΙΨΣAw8lYN’ΗΏ'MG} _βλ‰~΄°ΖΙw‡*iοΏϊεI*| …A~G|7 ͺ4+4ώ¬`ψΗ†9σ–B7υFΥ3¦Ε ζ‘–J.Έ £|2ψ]%k\α`ϊT}ΰ¦27Fπή±8° MΤ(XoL›ϊ·;w‘wέ₯m‚iπΏlΩ²ςψγΘΪ5ΛP§+eΞάEξηŸ~ O8†Qσή{εΝ7ί’?όH:ΒΦ4γ &TƒΈe£σV@Y†ά ή[τaπ>'8TQς€ήTNC½FBݏŽ„Σ՟ήHηΗαCK#οτ9¬΅4RΙvlŽX]ΝιΧοI©U«–ζ~ί₯—t‚šΘ3: "ξόκ`lž’AύόΠdψΞPΎxΡ\χ— θj©υΔrΦ]Kžzςqlβ\«ού’HoίΊ&Θ\Cγ΅kC » πOe₯γ”36Έ-ρk@-š7Aƒ†ΫŸξΧWMωPβατΩΙήΫσv™:e*τ!+ΙGƒ?—QxΛslDώΞ›dvΒ„1XΆ‚OH{J`–ˏPΨΉ‚>R\ΚͺsFE5―Γημ¨H†›5k© 5..N—όΨIτ7^f:J–gΝ™/=ό¨¦ΝNΎAέZΪρρΉ?ŸΌ6gdkRω&7ΩΤ―W„²Ju©nAGU±bE†Ο±,\ΚέΖ2σT•.-{Ιiζ&]‚§I2:JΌΧ¬Y£&)XΈΜ½•ΧU1’»‹–ιςΉ“όό'ΌάkρνΠαψΨKmτe‰°Θ°ωσΡχz›I.Z’£Τœύ]€ΌΓΎ‘šκςχyώlQκθξέa1Χ ο‘<―^.Όπ|™°‘.Ο“l’ΔΡmέΊE~:Kl‰ΩΑ}[tσΙXh|΄ΠpΡΕ—BΥgŽN˜\ήΦ―[―ρ~ΤΓ~8©γ†ο"ΕΚͺͺ'2λ!αΏώϊ7UνΘ½o.]ŽcTΉX΅j ή£Z―₯ €Α«ͺο3ΙezŽm‹žX>bβm~τtΔS©―ΎΫώ°L›x…–ΡΥŸ‡>σ‡ηΉΛχZΌ;·!ΟUΛ«δxήΩ /κ$1;ΗΈ\y;vμ¨>#A/WΊ˜Žέ|Η¨ΏοβεsΦ’₯«δΊλnŽΏξ9γδϋΧύŠ«δ·ίΗΐd_ρ`ž5γ0gdgNAFϋPΙ;Σλ»F—ohβŽ;{θr Ώ3ΟA Š‰¬·ί~W₯ΎνΫ΅κΞ…ŠμάΉIοωηŸMƒ«kΰΪΨ} φs¬ ΪτTβtό²UώT"ΝσΜ―sŒƒžCη‘Ί£;oήψσΖs†Ω Κ–)ΙdΤS΅Ήΐv4²F9@°s¦kΈ1’—Τφθ€I“τŸΉMzΌAέ<.ωžsNWlβϋT;Gz5@κΙ*OηΨ9ΉŠχ8@ΈtyŸjΟυ»ΊdOΒζζλ˜ύη”W`ηκkS5ϊID|ήr–+Γ3:ΞΪ9ΠΡΥƒ”ν’K.n δΧ™.\$=`›rΐ€·t O/ΏΠ~ l‹ΐ‘Ύm›6šKJ½Hž~ώΆR1Θσ]’F ³ΪΦ¦Nύ[zaσ₯ΛtnpΤ‹7`’uλv…Œ;ƒf‰+W›θ&`Y{ƒΪΈ₯5Ÿ­ΔρQ½βρ'žΤ–QΊxB’^²ΣΟ† λ՞.?υΜΙtJ|Šmš…δG.ΨFιHlΈαͺόQί’aK/ͺ›¦/yDκAuΔί―ς0ŽV­Ϋ(Q"—‘ΖOόHΔΉqiΞμιXήn,3`ξ’dίεΣΝ1Ÿ:29 :Χ§rrCυeΛ–₯φcœπ{ω£ω΅Bσι}g²ς/ψ%A¦Υwϟ)Z!ΨΆ}-V,ΓΊζ—ίΈ—ΐΰzλ©B…©ΌϊσWwοή bUI'_ώ΄xξ0*Υ:J€9Ω‘s:͌/”>ͺ‡#ό ŠΤΡάήvθψζΘαIΊ9Ω)WVVςκs/Ώi##ήψϋ‡s~ΉβB•?ͺκ8ΗI7$zRxΆOΒ=Χγ±$M!©mw·₯~œ…γ™3]JμςRUKh]jωŠΥ:vzͺ†όLωΑP―Α8ΈB’n|¨#ͺ‹ˆ°}ύ#Έέ0²='Œ SBSΊt)0(ΊιΖλΡ1ΐ‡C<Ϋ \βdC€c‡Θ™*7ό 8»`θύτΓQZqΘqο άΌ ‰@~|-Ιοn½υΥΏκ|fΞψ°HΥ°ΡξY„σv2ΗΕ•WiΆ C•§ΜrΠ`:u˜ΗΓFμύχ?¬ή—―\-O<φL)UΣηnΐ ϋ1Nψα :κ suζ­·ΓFͺΧN'L˜ fίxύ5Hό₯„Υ=1vEgfϋf»½Λπ_ΰ3ΘεΚ•Rέά‰S 7όΙά–M«€rΕ²AβΣ¦uk&3OGͺΈ9hωήΖ«Tισ9zΜΩΉ‹_Uσ$Ÿ$±ωςδ”’E κδ˜α›si$Θ.οξ~π˜J@HΪhΖ+#Gϋ·άί[.ΌπΒ IΛ γΜ(’ ξ»ςJΟ—_«“ Έω¦τf̜ )&7•Eiω 7`–/S\ 'ΓsΣVε*5eθΠοΤΏnΘ/ζ₯η""™ϊJο9*Yϋϊΰ³TBδΤθΠΛρΜα€`:7™ρM}2 ’ιLŸύ΅Kž.RκŒΚŸQΔ8μΤιb)Q*NνSύn=tš—.ρΤ†\ψ˜\E₯r…T•—Έ{˜Ι#1Λ,α<„?#χt{jKŽ‚"mo.Τ<―ŒœΏν„ϊρž₯3«υhΧ†@6DΰΈdGiν‘)μ(Ίλ.].—KaNΚ›QŠZhΡ’EPZ@lΈ|φΰƒc0)ͺPω₯‹‡ͺgΓD“sέ:Ώ@‡*pΎΧͺy ”ύ‘G’»ξΊC?—ŒιψŒƒη›oΌ–F2rσΝ7; ΆΠ…k:P3LσζΝeτθίU™Λ€$φζ SΤ±KΎ]t‘~݌·*VͺNΚκ5k%OώΪ–·nέ.mΪvΠΝy™-3ΫΫbCld’㊌#ΧsζΜ~ΪzϊtοƒB$΄ZpFAΧΆ—ϋμάΌ ™£NˆιŸχΎ€Φ₯π·CŒΘΣk¦NhxΈG ¨—Z‘R5|nΎ˜Φ=ΈQ+4]JK)u¦U‡ώ―ώ’d_W U Έq)oή|W>Έ”’šΞα¦s o=oΞŒΰ„€„ˆε-/―n” –‘±WL¦Κΐ™y’%‰1cώΠ;±ΟfΌ‘Ž«h δU ”RS’λw~IΎΧ¬]ΐ)N φσNŸέ‹Φ#J Ku5~T‚φͺcc=„‹ΛΥ·3“G3›‚ΠQxA§ιλΩΡύuœDΠVρ¦M[πΦHysΘϊ5ΣU‚BΛΗ{Δ‹εLBύ ζ“δ˜ζίθ‡nhΌϊκλ°Ωυ)TΈ~„‡y_Ίt©t»ϊV©V v—F~8§ ώ³^2 ‚x™:χ‰gΆ7'…_=ξτΚG{φμ–MP ʝ;'AH5qŠq›ϋŽΕyXEޏ% cœ Ž;Avωdƒtƒ ο±Σsδ˜ΧώηΤM~φΩπu¦W΅ασŒ0;€ΓΉΠηLΧΕε:ΧΠψ)%φ;pΞωσΙ{Œ‹ε 5 wν:t½a?†ΐ)Œwγσ“Α;`‚K§«W―ΓWΑfγ«i₯΅ΝήρƒτΦ׎T\7Ή‹‹“nΌϋΎ‡€ΈœΐFΆΏ'O‘ξΨΜ;yΚ•Ύ1.š›{κ©ΎϊA^©}Ή|¬‘ψ!Ύι/I8ΓρC›·νRΙ1ύωŸSΊHΑΌΠΧεF±)2ΓΟS/Xˆ/B5,=bΛςμAΞκ¬ψγυŸ»4ώgζœρ1ξ?₯Ρ8έjζwαRLhrηΠηώ4XήΔΔdɟ—«lyυ9ΛOG %$Θιε“Έ”(VDΎωμ/ZΗ`:μI΄yά„>ό§Ÿ~‡)½ zΟ‘Θ|ωR…JΛSιwž<Ή”D.\Έ@WθΞ,σ° Άπ5Z',$v9±1ŽΞι+(ΟΡ;/ ϋό°Ž2yκ,ΌΛω‚ΊκΛ—―ΐΧζ<λŒ›ι7–σ₯—_ͺ¨P(ΒΙTμ~}ϋΐŠΡΝΗͺΥ̘>Oξ»οXΎx48Arωd\χl„…§ *±vχCΜ%’VΗ9Qα„…Ψ:jX²„·ΩΛa ™8†Ύ‹M›6Ω±΅j~Œ+΄κ1`ΐ@ύl³Λ‡‹zђŘήΫΞCε}LΒή~λ lΖ[¬y£5:ΦυEφ WEΈΒθڍΛο”©SΥ/ ‹£+•wΛύ:Όυ½X“nΦΈθ‡y;;εžΣD]ι2•tBΗχ²φό‚½'z&ξœ?nΰ£>|Έι—ι,Z²VΐBTυΎσ―φcόΗ8©™eͺFT¨PαΈ“cύ1½γΥΐg\ώ<ΪΉ!p²pƒ8M«Ρq$™‘4–:•ό2Ψ½χέ/ΡNιŽ₯-Υ«KsŒΉUEŸ“¦)Θ7ήxG?ηKL«΄nptΦ+Ό―ώ„EΗVλ4ΕΕ―ηΡm•*•Υ*N₯JaF2υ県σύμΩΉδ.‡ͺfT©V zΉΓ±‰k{°iC|„Žd"τXπ M"½kWO³fΝ–Υ+«€Ÿj6n…e‘3urΑύ•+WJSή*•+«ΆmΫ6°ΨQYλ€ρSšΘρ3ΤίΓWυθdc3, }ϊιgjzŽοΑΎ}ϋWZ½hά€yΠκEh|ιf<›ΚKSο7kΦLΟI8U.~ωe,lζ?«.Όχ? Θα†QnΈ«Z%NκΤͺαΞ…ΊΛ­€ν”Ψμ—'_qω_Ξ£ϊW4ι‡qύϊλorΓ-½T—ŸεΚΘ1?΄@BU RhWί?bΐ―Μ²=:u‰ΓΕΑg”Έ_sM7ύϊ!MΩqE…Φaξ½!]΅aάά˜Ι‰Η"ύ`†•υΘ 'P’ΌSΞ;ο\τC«ͺ₯iχ '• Lvl€3:֎οΏV1VC «pm―2ˆTχ€4ΝŒΞΦ/'²$‚l―Ξfςιό–+WφμœUΥΗ‡]RΊ»»»;ιR@,₯lRQR Q)‘Α? (%!΅twwΓΎόζ1o/έeva—³Ÿχξ½§Οχξ;oήά9sX˜jG6νςhl‘ωD:δν~›y­B] “– ͺ ταΐ4zΤHή2Ψύˆ^ XΧΖvΜΉ%]΄/Δ!δf:Ϊo°P­₯Λ—ŠΠ†τˆΐ}‡ΐ…mΉoςX!όœ>yHvΟ“tωqsxuŽ…V·ώΛ/²™ο o·Xσ‰°˜έμ)O.)qx3y؞Έ(o"Σ§Οή,©5f―$εΚW兝oPΡb%ec δ…iΞΡΓϋ>“u±3ίήϋϊ3˜dό2cŽό jάΈoΥό‚lΣωYΥs”ƒ— Z·,²υΏ(B3Ÿ~WΈ'x¨°ζΜγ}mc—Kx±Ά ΜW_m-žg>!―σ₯ύ„Πή¦Mc:ΞebĈιΡΨ-[φCLD-k'ΰ;ύ~ξτGyG?π I>L?ŒŸΖ‚)ο†ΖΧŸήf Έ§Ξ±βs,ς‘­*¬E†K6h !ddτ—_f±›ΌΟυ±VQΚaαd‘ΒΘΕυδΜ•ƒ7*@›6o[bh–‘ν„ΰ…ν„—³„Ω·Ήo1΅Φ…#ϊνsΦ,™)mΊ΄ά―hT±RUΪΊm;·λ/šj˜t ],ς[·ΡρJΤπ•Ξj€žΈqcΙxτ^ή—αήώΟρξ ΰ€~`α6κ^Ήbχ½xπΈ‹±¦Γ†Žαu1?²½|z±Α‡°ŠΕkX€7eΚTyϊ€:αώ³vν—ΨOψ ι/μΊ‹+Hλ6lο -y§Α?ώMiΣgΝ0l—q}ω³ Ÿ?ΏόΟ£_ŊΉ=9Ap‡©β±π2kwlί,‚Έ–AΏτŸ!pΣ€qζΛ—¦M›N+Ωk ž’ >p-R΄0}3j‚la½τŸ•\1₯Α|~Μ:ΜξYϋΛ"wπp§»:τ7¨ΰN‹Η}rΜ Agͺ‹7O…@ΐ'ι©4o#π$ ¨Π ΐηx—Ι;ΧNy„+τCΏμŠ)"Z7lΪΑΒΒ]φω½…7hΒ ΣJ~Ν‡2π2€pρ"―€?u–·a?_’ΘoZ‹ R¦Ξ,›*@8Β—8ΎΤ‘QΔctΥϊj~-ο}T!6±3x {„cΗNˆ†ξ8οŠV½z5±{FΎ d oMβΨ±―…{”°σgεvw©;Λώsχο=θјUχΓβ΅_u’Gΰ\ω΅kΧΡ•‹ΗDP:Ο?^V»J΄}Y³d•bc§›€属9s^Ζ·έo“lƒ œdΎϋφξνƒGA{φŸbw—KΕfΒ6ρΐ}Δ¦,[ΒOΐύεΗ•jιc0·gχ!Κ’9 ώϊK6uΙ$[Cψď”ΑhN‘ΙΟ“3+oΞτ½μ€ͺχυA[{‰7ͺ8ΗγC';mϋ_βAύέƒςB›>uΚOΤΊu[μ>&ΠZ—+_œ7+),?ϊ`±†ΝPVψG4ΒMξieΡ,ώ|„5^§X0{E4`ΐ0NΥvγ >¨Ί4­P‘BβΏ6]Ί4"πΕd-ΫΏ«ΧΣώύϋewO‰Π,fʘ^Έ\9³±τ† ή} μcφΤΙ‚κL'Υ υ~°lΨ°‘ͺT}^|ΎcˆΔ‰Π+―ԍ°Ž)°6UЁΦžPO|ΦRB MΔu@_§NmΡHή_>ΊUΕ‹£}»Φ°πš3χwΪ⷏² ž*Mf*V€}ςIoͺΗ»/Bht VχΧE”"URϊΊ6λΧ_XΓ9MΆΛήΎc/:q€$IK%‹ wί}S4Ηπδ¬R₯JI•«Tγd‰₯0νpώίαΗ\ώE˜kvπ>)ΪSν κ€€›6mZΆ£ώ†wP|…hύJΛ–―”EŒšGΊŸή‡Ηκ{μγ΅?8Β‡ψ_ ηIω™3ε’ΗDMΙ\J/Μ¦oΛ8`Cœ„]λ=Ÿ>­0NΛ[0'wψυGŸPώϋυύ\4ύ‹Ω›Η!~b³ό€ˆΛ6ΞHGΐbΓͺ•Ψ½{‘ΐ•œ9²RjnQ‚Φ‡γΛΌφξέ{dӟω ώ Ώϊ“sόxL‘*U(W’^ΠGLu’$I"}Ρ:P_¬X±d{n0Őxl+ντ…<P.―/ͺP±²Œ{°Ρg F 2ˆΖΖ€OH0=Ζ‡ΑΟokwr2ySΠ’Œ€α3„>@γƒ// ώTp815@xE^ΌœΒ‡3κsjφ†ΌΞ64]ΫιΕ ³το+6­πxαζίUΛY3ΉBvξC~Νλl+°sŒ /οΰέοtη5ΖͺΆ₯8°ΐώ¦){œ/ϋΗ©χΕΠWŒ7€ΜP6°ϋ†>AψϊοΏ5T²d ωΡ€aλΧm έή‘ϊ ”Η"FlA σ x―ΐΆΒ°]E@pυ>ξάΉ“rε*Ιζ9 ΅±ΟϋT­Jǚa]ΠH’>,D`ψ°GΠϊδβήξβΑωΏμώ_Ώ‹ζ$ ―ήOggέ`rμΨρ{γrοclιΨΞ8 βΪ¦σΫYΜGNŸ>-Τ`Β’’w Δ’;„ΐώWϋl9λΣ2Έη8όία^#Ÿχ-ΪρώΏτξ34κGXθ†fχγψ‘‘Ž ιύΠ{‡ϊ4h_τGτ\ »G•±8#Qώ_Qzgύ0F L ΰ‹―|qλ—epωƒϊrt–q nΫόόhθˆοεΡ3ΑžT―QK㣜SψpΦΨyhΖXyΔ‘=o‘Β™ύ.έ™7€ηSgXτ%$χΝ{,ΰ€Β4Œx9ƒ¦…δώΑŒC/Pv±x9CpυυƒMΛ‡τΒ9&0ɐ!½Ό΄η10‘ιΞ: PΓ½š3θ8₯OZ&°{Žv‹wΆs½θΞρΤGΝlΌσj_υή8Σ΅/ΞΈΰΞv‚+kiFΰi0ωiίkί<γ uZΉr₯hΊ°λΨ°αίRΚδnA…oΪΈŽ5ʟ{Νλϊ3Ž&Β /(ΠΩQάQ*H©Π…<ήχΝϋΪYχyHκσ.VΧΞ~:Η¦υ#―ΐ„Fgœ‡Υ8œ}rΦ‹sο4Δ…$hΉ ΖΘ5?rέ!iίςΘDΐδΘt·¬―F ΐ—0ΎρΨΊNέ&Ό Κ½ΰ ~X±)„1Ψ:³lΫΟ6»ΗH4Ό(ΡUxE`‘‰βΖg;_Φ’kPA ΧΞsMπΘ6ΞɐΫ] Χχ\μϋžd8λpž?XOψΗ ύΗιƒ–ΥcXυ8,λC]aY_XΡκ1‰@¨dϋPE€Ϋg}‰L’ΪgGΗ‹cέ:ΟΣΎ}ϋΕ ΌάaΣ ¬φgΩb*P¨8 τΫ!ΗσΨ¬F¦ϋϊ,φZ„}Ρε+7θκε“b―Ϊ±κ$όΊpζ0mζ{Ος6έΈršςζΙεΡΆ†Ά^ΛoŒ€o‘]’ ‚&> Fΐ„Œ„DhRωlΘ <ΉT8Βqεͺhοžνχ ƎύA|.§L™‚…γ›Τϋ2ΪΕ'³—φν;ΘΒ-όΟΒrΑB­uΤIX„ωφΫ]<‹7oήΌqΟr€Vϊ‰4FΐC ^,όΩ/’Ÿ,‹MZώόydaΪ}žα h#η͟OWΨOnή„ ~Όψβ)Ϋ!C0FPaϊΖ醦Zdν8„]§™…Ζ‡τΦυ…΄]ΛgŒ€x‘Π c&ΚΙΎ:Mƒό(¨­LT& δ;χ0†¨£EΖ=ΗΚχλ`gΎƒ>‰RMγƒ9,ζiΐ} ΛΦυ…eί¬.#`Œ€7PΘ(M„γΗΡ"xwΐ@T!ΰv«ΰ8ͺŒγTAXˆƒPl‚1HX0FΐˆF ”rΐ]DˆυΗDtNα0’χ5¬ϋ§‚°ΓΊ~«Ο#`Œ@X°aIΣκ2Fΐ#`Œ€ˆτL@Žτ·Π`Œ€0Fΐ#–L@KšV—0Fΐ#`Œ@€'`r€Ώ…6#`Œ€0Fΐ°$`rX΄ΊŒ€0Fΐ#`"=#ύ-΄#`Œ€0Fΐ„%Γ’¦ΥeŒ€0Fΐ#ι ˜€ιo‘ ΐ#`Œ€0F , ˜€–4­.#`Œ€0FΐHO άdμζοο$ €=ξΞbaQG΄#`Œ€0Fΐ(I Τ[M‡„_l)άΆ²>>/›kΪ^HϊfyŒ€0Fΐ#`Œ@p_Jυͺ]…ΥΣ§OΣ­‘7nxεp_nίΎƒvμΨhZH"ΡΞϊυθΤ©S"ˆγΪpιwΊ+Θt” ¬³žΐΞ+X\`e-Ξ#`Œ€0FΰιC‚\²d ΪΈq£ŒΠiqαΒΚ“'7=&Τ‚¨ š‡¦’E‹Π䟧<@yXνΡ`kdΤsΥpγZγτ¨iœΫ“φ@#ΑD¨ζ\λΓΨϋψ‚φξέ'₯4>˜*,Ι#`Œ€0Fΰ)sYΗαέWN7oΎ™0‡PΑpνΪu’–"Er‰»yσζφΚ·nέ’;wξhuW­ZE₯J—£eK‘³gΟή§E†€ŠςgΜ₯Ϋ·oί'(# λΩ³ηθΪ΅k’†8τMΫσηΟΣ•+WΈΝ3‘»wοJ]Ύ  Τ…€s=’?ˆΧϊ]»Nƒ‡Žm7ς()`oFΐ#`Œ€0Š@Έ Θ(“₯ΜΜήΝ)„d¬σXhN—1; •nαuΰΐ/θg‡&ψ₯KT―^ΪΆΝO`©ͺB,κ±c€/φ§΄ιΪun[Ο?.€Š•ͺQΓFM¨\ωΚ΄rεJ©+4Ϋ ^iD 6‘μΉ I»θ+™FŽϊŽ&OώYϊpλΦMξοiΟ8Π—Ν[όvς νΩ»WηΟ_@Ÿ~ܝβΔ‰sŸιƒ Κ ϊ‹ώ”’GNEЦι3gψ°”ύσΟ…Τ§O?ͺX±%I’„5lΘvΠ›(i€΄yσfʚ=/΅kΧVŠ/F»wο¦<Ήs³Άz­\±”ήyϋ-Ξ›„²eΛFߏEoΌϋ‘Τ“Η#F 9Χ·D x„έcΗOQ§N―ΛΈR₯JɚδN΄jΥjɚ2eJξ«/%K–Œbί“ΦaG#`Œ€0FΐˆE \άΌaˆΠζžfΰœ9sRΉ²ΕiΓ†TΌxqκΧ·˜@ μΓf  $ n]:ΡΪ5k)[Φl4nΒZόχŠΖ?ΝΏ(BΌ\πzυ ΪτΥ«WΣΥ«W=BΆζ΅£0Fΐ#`Œ@Δ!ndΨμ¦K—V΄ΌργΗ§φνΪP$‰©@·ΉCμΨ± nή LΒήΈvνZμb Ϋ$•xΥΐͺφψδΙ“΄nέzχ·$B6gΨ τρ'ΌΨn9k«[ΣόIέ{~H βΗeAψ-YΊ”Ν/Kh“{υώ¦2“φο?LγΗO rεʊΠϊσδ‰Όξ3jάΈ)e!Ήc‡ΆΤ’E )WΈpa2dΥ|‘,‡κΏ\Ϊ΄mώΰά‘pΑΌ5bdόiΣΘΨ`ρ͈ΑT»Ns:Ş-ztΐ„δ{άμ`Œ€0FΐˆF  ŸχoADa—λη·‹½@δϋά ²y’‘ίflF€f ΠκB7Ig;`˜:¨ όνΘQtδπ0 Ÿ§η ΚC« αΣ; >€ΗŠK’ΰ—ξβΔO@1cΉσ« :°hBzάΈq%Ώ¦αxžνœcς‚<5΅Π4d„ 4^ύ0³ΐ9„t΄‡sΌΠΌœ}½~έ½(?,D=ψΝg(κ²#`Œ€ˆΒMƒμ1TΑ*γΪ™ΫδΎdΏΓCΔLιN‘Χj7μΎΊέYΚB0…Χ­ }ΐΉ[λœΤ“†MΓ1IβΔ’†7-«ΗD‰yœγq ΒΘΰμQήc°`Œ€0FΐˆM άlC2l4Ÿ―^•7ΩΛ‹σ²z„ΤXvQΏ ΆšiΞ4Σ£¦!ŸΖιΡ™†τ”Τ²!mΓς#`Œ€0Fΐ<>pΣ ‡€k*4ΒGqΪ΅₯„HFCRGPyVGp鏚T_4>Έz5€0Fΐ#`ž.§* cθU³Šs"Ÿξ?„΅nŒ€0Fΐ¨Nΰ© ΘΈ&GυCΏ0Fΐ#`"§jƒq0XOŒ€0Fΐ#`Œ€›€ ΘφŸ`Œ€0Fΐ#`L@vΐ°S#`Œ€0Fΐ#`²ύ#`Œ€0Fΐ0μΤ#`Œ€0Fΐ˜€lFΐ#`Œ€0FΐA Τ²ω)vΠ³S# φΩ ,ΛjŒ€0Fΰ)₯dέΉs‡’G.›{<Ε~[ΣF R€pŒΟor©ϊm}ςtγ$mΩόΔ+‰ΐΖ+p.kŒΐγ…€ξήυ§;φvΏ{Ό†­΄ˆJX>&ΏόyΨ|aΑBΒ^`±ΖR$JG)ο§3₯αΨΰ€x,‘]δλλC9sf3 ςc!·ΒQ‘€jwξάΓΓ·_—QρΰacV‘ξΦ­[΄yσ:}ϊňƒςζΝK©S§~ 8ς#„T8Τϊ¨ˆ#‚KCώ ΪΊV^σ…εQΫAU…2”Ε σ2ΕΉs§ξέί—&€ΌΞόήηz-™oΪ―€t΄υ @ξ(Ž§Ϊ―΅kΧRρβΕ©b₯jT€H!šτΣ ͺPΎόv8₯L™’Ξž=Kοvω”:Θθι@IDATvμ ½Ρ~jy=j<2iœv_ΣpŒΞ?X4>|˜&L˜D=z| Β·ΖΫсgƒ@¨%]LŒ€=ϋμ„žYT)‘BΨθΡchΧέtζΜ6t0͜1Mή *Πή½ϋ<‚.μΩOŸ>CW―^•8UZ?~œΖOόYβaΞƒzρΊ}ϋ6ΧyVŽΪ– ‚8B0‡ζGΔi:βΟ?OW\α³Aϋξέ»χμκ‰ΛŸ“6юی₯άΒ&κCή° Ϊ/ŒΒρΈqγiΡίΰ―с}[)Q’Τ±Σ›\Œ˜1¨jΥ"ΐ^Ώ~ΗqAβΑ@λAϊαβEON0Œmαψ^—wΉΎ*Έߎ•q£.ά“[·άe$Γ½²7oή”Ά4ΎFΐD‘#Η°¬—FΐΘAΒΒ±cΗ¨wοž4dπ Jš4©GΠ¬S§6ύόσTΎnJΎ 6P΅j5©aΓ&”>S֚N–ψ]MΟΧlD™3¦₯J•Ÿ§ώ[#ρ+V¬ k³ζ-Y ]™V­ϊWβ!$=z”Z·nK/Τ~‘^}΅-M™:Ίtι*:8HmΪ΄“ΆΚW¨JΓ‡#¦ ¨υΎρΖ[τyŸΎ”TC£™,YRΚW (›œ–2Ι’%γ|>”*U*ΙΏnύzz‘Φ‹”-[6±ΗΝ™3W”ŒXΨ₯X±bΤ)“ Ά΄)R$—Χ»οΎMΖΘi±$Ο%ΡοsgQΌxρX«„Ύ;ŠͺΦjH―½Φ^κ―\₯:5hΠ@ΪΖ[υκΟSΝZυιέwί‘˜1cΡΨοΗΣwcΎ‘t^E›λΙό˜'ΗOœ ‚ςέW ρ·h}}}ε˜0iz֎·’ψργSβΔ‰Y ό‘hΞQ°C‡Χ8‹΅Γ—Θ—Χtλφπ@ڍ›7h›Ό”)S—b23ώ="ηΙ“'“S, Δ}I˜0! τ-ZΌ„ .LΫwμ LRSώόω%ΏήOΉ°7#`"<#ό-²#ΔO€όΆ‘‹/‰0 B•S°‚π7mΪtϊδΣ~”'wJ˜(!mέΌŽOϋ ’»lΛΦΓ"@nj“.^ΈH[Άn§ž,ήΈq“π˜lC[”βΔ‰-v·yςb›έDΔzjςαΆ±—)K.©λ₯ΛTŽM2ž{ξ9±!†ΐ !0uψ›Z‰Ξ~ζΚ•‹₯KEϋφν“rΧ―ί K&{3F R09Rά&λ€0Ο*3€Οΐ’Ψ‚g†5ͺ{΄­ΞΎdΝdύ—λqœ/5mΪDς-ZTΧΫw‰z7Ÿh"B8Fˆ?UͺX–ƏϋΑΜοp‡7hβmΫFΡ¦ζΜ™S=Ζ έvΘyόρiω?‹%?4Θy!Ϋ±Σ—E@UύΗ ύ„ ΨΉΣλΔ¦bόφ[YPŽ#‚«ŽS*zŒ76σηΟ'ξΫΊΎΧEά»‘J΄±gΟ±Ω†=·/p€ λ ώώwE{Žώ–-[†σN‘F ·Š}²ΦΕΖRTϋށuώSαρψ#GVZ²tΫd―£?μ)Ω΅Οeν́ˆNΐιEτ;dύ3Fΰ™&α T"Φϊλ,ͺY³-Y²”.± „Ψ―ΎL={tgˆoPdb‘Ω¬Y³yΨrΖ‚ρΒ?ζΡƍ›„„θŸ&M … Awύz^ΰχ|-ΪΎ}‡˜aτθΡ‹ZρβΌ΅kΧΡ’E‹ιS^€V€h ‚η…Ω³S…ŠU‰νwΕΛΫ(Σkή€o‡|.uCsͺšXη ͺX±‚xwθ7ΰkΦVW•$Œ/¬‚ «ΕΨƒΕ+ ΣΫoΏ+‹ζΰΡcΫ6?Κ[°<±½°0ψς•«χ5 N―θ±Θ°W―žm3ΚBˆv†β‘Β;v^¨Έ”ώωgΉp@=0MiΦ΄1=_­*kӊi Κ«—g]vnŒ@Δ&`δˆ}¬wFΐD!υκΥ₯_~™AM›·₯\92Σ±§¨Hα‚"Βv΄?ώ8Ž*W{‘Κ–.B΅kΥd{ΰŽ,wξ\Τ§o?Ά-$ή*J•*IKΩ‡rΟ²ύm"Ϊwΰ›τdM§[cά§Οg”kςκςήT΅JEκΡ½ 6Βc60aόχτρ'ŸSγΖMι,»‚λΨ‘-5oήLξ„ρ΄iΣxξŽ 3f€—λΦ’Sl5kIKQPΔfatδ·ίˆWŠr«SήάΩθοΕkiΔ°Ο©]Ϋ6ntίθ”7{¬ΪΡ„ ²IJ aΆhρbκΠρm͞&ͺ³yΔήDΝ3ΐ;vœ8ZLŽ©ΩΆ;Φ½MH2fΜ@ί~Λ6Ωμφ ?@ͺ±PŒ 6Λ*”—} d‡εψ₯{3F ά Dγ_½!ϊi_Ψ~~»Ψž,‡g χήYFΰ"`Ÿ‘gθf†ΓP0« ν0άΎΑ‹ΐT(Τt˜: ΐ&šNΔ«`‡z M…6SCΫΛ^d±]œ{Bώαž,sζLžϊ6mΪ,;ΡΑώζ¨ο<—Ιζjjx”ΗKΝ9΄·oί‘—λΏB^oΟξΦκxμwQ&,ƒΆ‡:αΗ›‚$Ož\lͺ΅§ΰ£}D- aWY#¬‘_ΫΨΎ};Oζ!φ|!›•8ϋ‰όŒ€ˆLƒ9ξ“υgœ€ Α&γ,YάΪW\{ YŒ5¨ ηΜηdQ„GΝƒ#„Ό•+W‰ξ€I“Δ7C«–-iό„ χ ΗθWφώ Aϋ‚ς*(ͺ–~[ΆjGιΣ₯‘Š+J” Ό€ΌΕ4Ά™F@aVW‰δ7ύΡ€kδΕ"DΌPFΣυ( χή‹ƒοe0€Ώε–μ—yΒ„‰&ίcf#Y „›^A=ZΒd‚Ι―Π­ΧYζQκq–G‘ν‹³;7#`䇲t% s›—4=°4”AΊwš³Œ¦cώoΝΪΊe+ω°ΐ[°@ή²Ήp°eQΏwΠϊ8@[Έ₯K‹ :χΞΦΧ:6Τλ=₯eC[NλΥ1‚εςε+D3]’Dq9jšζ΅£0‘‡@ΈΘα5)WopiΑݎG-\–f#`r`T,ξY!ΰ=—z_?+γ΄q#5„Ή‰…NŠp\Ώ*P Ώ<.τΖ‰UΤψΕ‹EΌΣ½―΅ήγǏΣΑƒ‡δΡάυč²fΛJqψ‘€ζAYœ#₯p¦Γ6o*o±Ί‘νο>μY-ι!­Cγ7-§νz_k>ƒKΣt­ΗYNΣp ,ύaυzΧeΧFΐD]ϊ40wυΤοa„0‘<^8lnzX‘=,•adΏυίy\Αυh}^ΝD‰KŒύqF P6H# „Ή€¬c†ƒv„Ν›/& ΰΰsΫ›">7υ—·$πόkb5q`‘sη·hβ„q4}ϊΟ΄mΛJŸ> :L²κδ|εΚ:ώΌΔ© ‰GάZ'V=_Ώ~]VaΓ7蠁}¨ ΫΝι o”ΑJnσΈF_ΡO¬nFΈtιoQzYΞ5ΧWΩ?ςγu™ϋ‘+Ξ%#Ώ!ύ8}ϊŒ¬€Ζ΅υ+ŠΎbU6ς9Σ΅μNΓJwMCίp~—ϋ‡rΞ4­ΫŽFΐ#`Œ€0'n2Όd)3ΣδŸgˆOtΒ+Ιy,4§Λ˜…`·»μ{»i€ΰY―^qϊŽ8§π¬B*VO#~A_mΥ’Ύ6N„]”ί|;’·H­B/Υ­Oo½υ*S2―Ψ%C(1γWšχΏΩ4ή\qŽηοƎ-ζGŽ‘|yσŠCωΦ-°p܌ƌ%»Y5šVZEΣ§Mf§ύSι9φΪ­ΫR„ό+»\‘έreˊΆ{oε ˜ΏΝžA³#~8‰?xπ ύοχίθΧi?P»vmε•Œύuξά‘(Μ‘?ζΝΗϋ~μ;uC3I Π€ξώ>|˜ΝVJJΩΏL£‘ί ₯:΅kΣVτ―^½Fως£ΧΪ·₯ίηΞf ϋχT«Φ ΌcΦvι· ηraoFΐ#`Œ€0AσEzΪ’Ώ?o»Ι‚lέ—^’jΧ!01ϋόeΖLΦΚΎDiΨωύŸώ-Ω‹-BΗNž!le ‘šΟO?ξ.ζj:€#‚/nνzϊλ―Ώωά‡vμάEotκH‹/‘ταߌ’·Xψ„“|˜PT©R…*WDϊχeΤ—·N­F Τ—Όϊ-4v™‚?KΉέΊχ£ kώ"μ…Π΅kΩ–υ‹/Έ}dϊ$eΑΊ-ϋM&ι0kȐ9kur=‰©Φ 5)UšΜT·ξKβ?΄zυηE« ³ŽΜ™3σξKΓEΫ sŒdΙ’RξΌ…ιάΩsRΧ©S§Y{ύ)/^Μ%Χ―4h@Ν[΄•sΨs7dΝρΛ/Χ“kμΰ„01ΩΌyŎŸ˜Š+*NσS₯JM›4cσʝ;·ΗC Ϊ›0Fΐ#`Œ@ΒM@†6ττ™³”3gNΦ²§ 6RρβΕ©_ί>l{{š5›;Θηžf8A‚Τ­K'Z»f-e˚ΖM˜B‹v›¨€¬#€PΌbΕRήΪ³4kœoRκ4©hέΊuμ»³ˆhr“$NH“yλΤΉΏΟ#h±cΕŽΕ‹οڈ°ΜΛέΨξ9™˜f¨VGΈj›Œ£ δΛΒ所Ih^α€ΎHΡ’bœ˜ΟσεΛΘ‚r:τ1Mͺ䞝™P0Kζτl³¬½&ΡB³υ±DL6>|εɝƒ&JHΫ·mν7ύΉPœ8±=Qwςδnψ'ΨT€hΡΒ’τ=oޟ΄«jV­0΄Δ°yφaν9FκΪN"ή‘j«ίΆEv/ΞC<ϊυλVΛ¦.˜5―Ά±—}Vθ₯!ΐ:욱p/kΙ[4oFkΧea·¨h’ύΆνπτΥ—ΫSm9Κ£Ÿΰˆ*UJϊcΑŸrζ%»vνfΟ=‡ςζ/J£G}λΩ ? Π3 Fΐ#`Œ€0!'pΏτςrΝ  ΥΚ–,Q‚vνΪ-f-ΩΦiΊHΧyΨ Βa©1›*@Uι ½Ι.4@˜D>±εκ[ov’7ίz8HWY«:qβ$zλνw5;k’oxΞυ Ρ_lΡϊEΏξτΡGŸ‘C‡ΔΣΔΰΑC©Y³”.]:VoΡ΅kΧ΅¨ΡφΕ —Mσε‘_΅θZŸφct¦‘€Y0Fΐ#`Œ€p³AIσξ  >_½ͺ,`˚5λci;‘ρYŸ€0F *΅€!٘€0Fΐ#`’£Ξ½Ά‘#`Œ€0Fΐ„€€ Θ!€dYŒ€0Fΐ#`’P.‹:`l€Fΐ¨@ΆρNϋxx\±Mm’·1#ƒ£ciFΐg˜γΐb†‡nC3FΐKΐL,‚Εc‰Fΐg“€ Αη/\ εΛ—ΣΌyσθŸ–ΣεΛ—=δσηΟӐ!ΓθάΉsΑ©iV*ξΝoˆόόΆΣΘQ£οm†ΓήΎΝγ‘"²£0‘€ Θ‘π¦Y—€0C@…γ]»vSΩr•¨Σοί/¦.ο}@*V£}ϋφKυW―^£χή{—pDΜτBγNŸ>Eƒ‡Œ”#yMPtφfŒ@$#`&‘μ†Yw€0C@…γ«W―R“¦-©UΛfΤυ½.#F Ί}ϋ6 όβKʚ5 έΉs‡bƌA%K•“]TΡζΝ›79.¦GPFhηλ δΛΕ›Iω²@}UΚ'L˜Pςj›Σo+kŒ€x’Lƒό$i[[Fΐ§L@5Ί›6m¦ λWӝ:Šp ABς›ί GW\αφάΉ ²ƒ*Μ,7iF'NœπŒ`ςδŸiάΈρrzoήΊEγΖO ―4¦‚…K‘Γθƍ!ΩSΠNŒ€0œ@Έi1YκD ϊNœ‹τέ»ΫwΤχΈuDtnΦ?#`"γǏSΫΆ―Q‚ €Γ>>>2ο%Nœ˜Ϊ΄i-qΘ+VL9‡ύοΏθξ]ΉΖL/\½"Χηύο7ͺZ₯ύ4i>}šςδΙM©S₯’&MK{3FΐDα’AVAΎ βε:#2$τUϋŽ£ ΆΣ­#"ΫϊfŒΐ³M@η!h„S€H~ί`‘†9Nή±jƒWΫi–hœ? Ο…zMbz=Ί[Οrχξ]IθΨρuJ–,)εΝ‹,X@cΎϋž…κ»ž94 ΄#`".0U8>}ϊ ­^ύ­Y³FŽΫwμ`MΓΥϋ&ΙRΗNoΡ£Η„Κκ+(dΞtΔΧ­[G'ώt_gΎϋBx±sΧ.ιχΉϋΏgΟι›~8«A[ΞΰέΆ¦ƒE›ΆνiΦμί$»ζΣtgzyτΗΰς—ŽnWφaiΑ₯;ϋgηFΐD\ϊ9N“&5ωmί!E^˜S1ΗA) !`v 8Σ4η|ˆ²5jΦ¦8qβˆ@Œβqό-yQκ“vξ=BDψRΈvνšτC51(_¨p jΡ’9α‹ ω0¬?sζ¬uΌHC<Ζqύ”"•i‡!Œ«`cBYδρn e‚«W˝:uΪ³ΨF²7#`"%“ςεΛG=zτ’ϊ ΡΆm~2Χμέ»^½#νάΉ‹`‹ŒΉρςε«"$'J”nάΌEσό!s0žυκΥƒbŎ-’EsŒσcΑyǎT³f κπz;6ΕπυΜ―‘šuځ(G άdf°;ƒržDkΦ¨A&N’ή~" 9BM›΅‘ όρ§ψδlυj[ͺZ­†8­GόŠ+¨gΟή²ZΊOζ §1cƊΐ‹tƒ*δβ«ͺ *A/Υm@ΥͺΧ”‰‚^»φ―Σ’E‹QDΪ―^£;`β‘iwξ²ΠΜePgΊtιθ»±cΩ‘ώJ‰Γ*οώύΠ›o½C/ΧoD·X`>rδ(»MjA7£ΒEJΡ€Ÿ&Kή 6+Z°ŸHΖ5›M,П•+WQEφ9Ϊ¬yK*_Ύ²\£ΐ₯K—θέ.οΡΧ_‘ΜYσΣO?ύ,υ¬Zυ/ϋ(­ϊ@~Ύέ»χ€#Ύ₯ΝߒjΟΏ@­Ϋ΄£γΗOH;(όηΒ…T±R5jΨ¨ •“Άάγ@κEšφΧVχŸ<6mυjk*SΆβ}šzΝcG#`"ύή»wOjΦ¬1ε+\‘κΥ{…Š”¬H%ЧoΏqo@Ρ¨t©’"άbŸ1}oς ΥSWž~9θ+~έ™]τΟ? &’¦<§Υ©SŸΚυ֍\p¬·Fΐΰ‰2DPΧζΝ~.ƒ ,KςβΕ‹]UͺΦπδΧr~Ϋ·ΓΎΒuρβE ¨.ςMζ:ώ‚λΔ‰“ΟΒ™‹RΧ―ΏΞr5n\κβݝ$mΤθ1.֎ΊΨ=‘\³ )ιΌ”‹WcΛωΦ­Ϋ\΅jΧuωωωΉΨM‘kΨ°α܏κ’6eκTW‹–­₯~DΜών7kO<ΧhWC³f-]‹-K9vzγMΧπa#δόλΑC€,Ό»Nž<εΊ|ωŠ«Nz.֞ΈX ξΪ½{·€£οK—.s•*]ήuΰΐarπΰ!IgmΊ‹\Χί/’k^5ξbν3ŸΗwuνΪΝ΅sηN©ϋΰΑƒ’ΎΤ+Ρ£G₯ώ*Uͺ»Z΄xΥ΅ŸλgΑΨυrύW\_~ω•€mήμζ΅dΙRα7mϊt© eΡξ‡w?Ž;ξb-9§EsΝ™;WκYΌd‰λΕ—κ»Ξ;/ΧN^aoΑιg(ΨJ,Ρ„ηηχΒ…‹.ή4ΔuϊΜOνHΗ σ€3οUžŸ0# σώΏωΙ—œσ|™γε‚ίœε5ΎFΐˆL ά4ΘΎS„Ψ±π8..kI/³fΩ—²eI%ρ>lΏ†ΐ‚₯h”_~ΉMβ^xΗ/%M‘‰Ϊ΅mΓ‹>’PωiζΜ_Ωίζ$)ƒΗwΊ’:oή<4wΞ―σ Ψ gΛ–MΞKΎ εΛ‹Ι4«S§NgΫθΦ’eε ά£mεΖν$fmρ 6ω‡.ό‹zυώFό†^|©ŠŠΠΡ£ΗP™2ed%ψΎ}ϋθχίgS₯J•Ψvο%J”Hl–›3Wς§L™R΄0©Ψεϊ»~Γzz‘Φ‹?˜DδΜ™ƒžK˜–un_€δ‹5ήν(GŽ/^\Ϊ°q#ΥδόΩy<š?qςŒ΄•σ#μέˆ:uz2ρΈS₯JIίθH­Y+i‹-‘ΟϋτemuαΧ¨aCΪΈq“τqύ† τ|υZχυ#~’τ\οVqώ#n ΪǏ]Y¦J+²ΝβLΦΉέBι=•Fμ́HEŸ_ΜuψLgϞ’έ3UC<ρrn ‚ψηxζa€ΓΔ Aηaδ‰?ΎΗ}œΦ%™μΝ#I„›dŒ#^@ρˆξβEΨΣ^₯δΙ“±ŸΜ37nΆ·½ΕΧΙYhΫH_|ω΅z΅ Υ―_™zφψ€Š+Ζ¦w¨Z•²"CX†0 Asη=R7&i1‰ΰ+Τ9π‹/XΈ]EY³d¦‹l)S±Ώ…­sλΦm‰΅ΛβΫsζμΏx1ήOrΒoθsμ8±Ω΄c{ήΈ&Ά·ΉX€eν―ή’‡›< °‹FψϊλΑΌjϋ’τ‘Ώ~¨H‘"sΤ‹#Β’·lέN={}Θυί€XΌUω…(f¬Xb.R _&^όp{«WώRΕ pΉXR_ςδIΉMχ•D°{&˜· Νύϋχ³ΰ^A’Α_d λ“'Nέ{ξλG……Ι—Ό lΫ°\œύ§I›*T(D}>„ξΊ$ƒ½#i `ξDΠyηˆΣx\;β5oHσ•ΟY―#`" ,Μ{ζΦ<¨fW3fόJ:tb5Φ=εžΙΈ`Α‚τσδI4zΤ%šϋϋοTœmα` ·AΣ¦Ξ‘Iοx΄'Nž œ9²J―1aϋ²π0cΖ ΦΒnmnςdΙhέΊυΤώ΅ŽžI½I“†ΌΠn‘ΞέΊΌζρκœΔ!ȟ:yŠΊty‡jΤ¨.e5mα<šO4ϊ₯Q~‹7.ΕI† ϊΒ£9m°.ΆC>¬ΰœΏRΕ²4~άΕ ϋ³Ά&†! σΧ–'-A‚ψ䇆\!tΗ‘όžμr’ ³fΝB›6oa[ΐz’‰lκBY2e–*eJ—Ό―¨B4~Τdg­ΘoGΠ ^LΉœmΑ+²ΩΟo»ψ8Υ=χ·jWFΐD6:Ώ…€ί!Ι’όπ#κΧ―uλΦUΖλο—φ8ΚBa,ΪΝ~†3fΝ#¦ρβΕ§T)έ¦Θθ+šΤΛΌύιxq=΄eΛVjPΏ>΅iέJκΉ}ϋkˆέ^Α2,ω° χoX“κ„ τz^4Χ΅[/ͺϋ‹R‚žwΈΙ«΅Ρ?Εx!ŸΦƒ6ε`"QͺX>Y ˆ-ZΩƘστΧ_n―¨λ"ϋΥP„]½a·)˜oΐγΕϊυ¨r•κ΄wο^’Α%@<&*\ΈΘω«T­IπΟ aφψρSG­ύ½Μύ@¨R₯ }ϊΙΗΔ6Θμγ"±ν5εΝ“‡Ξž;KE‹₯)?OΊ―xα |TΓ{FώΕ€\μΨqdG,Τ§ ±#`Œ€0FΐQšYΌ θ7Κ6&, „9…³θμcĈΙεjD=ΠΤΖ‹O„l€ΖEK—ίΩ_ε― Qc‚ΙJ‚ψ ΨΦ9@ψGZ`υ"&XT‰…<ψαcαΡΰž„ζ3τh­X)#`Œ€0Fΰq „›Ω)¬9;©Β1β `ͺ‡xΨς⅁υΰx˜} #VG#h~Ταl ρΈN–,©δΓ›Ά±—=MτξυύΉhmί²Ϊ“Ψ‰– , qΊr[ΣΡ΄c€ξήHΣ8όπi¨ y\$αή[hς;™hέh?Ϋeλ5ΪΡσΐϊ‘iЎγ₯ΧZN"μΝ#`Œ€0ρp΄AœTPΒβρ‚ˆ‚j• *Hߌr_ΊζχnEλΧ:ηp1ΧΌySΪ²a%©Ϋ5Νο]Ο£\k]Ξ1h\`υišζΧλΐς"NΣCšίYΚj9΅‡Υ\9gύvnŒ€0Fΐg…@Έi·ΰa.EŠςBΞτΰΪpζS!/mΪ4lwλήΦΪ[H Π¦9ΫIΩπΞ―}xX;A₯―υΪΡ#`Œ€0ρ' {Γ…pζΦ|B£μ0ήυΞψλ€zξΉiγz-#`Œ€0Fΐo^@F‡!ά†…<λΗcο»6Fΐ#`Œ€ πΔmšΆ3#`Œ€0Fΐ#ρ˜€ρξ‰υΘ#`Œ€0Fΰ)0ω)Β·¦€0Fΐ#`"#ή=±#`Œ€0Fΐςρ‰Fώώ.Š;e͚•βΗ‹'še§ΠιοοΟω|hΜΉ΄wο^κε]·Φ€#8λΌΑ₯iž‡Qοή½ϋhώ}tλφmJŸ.=εϟOΪsφνam•ξ¬Γ»/ήiΞ:”ΧΊuλhΫΆνΤͺU Oqg>O€#© ΰ3―σR`sž¦?©£ΞChσŽΞ=Έ~’}|gί‚λ—ζσ~J¨ρ‚w:βΠ‡ΰκF FΐD.anƒ¬“δΆm[©T©’4xΘ0ϊzπPβΪS…ŠUiχξέLξ:αΰ=F Α Ύ΄ η„€q:™–ζ©8„'Σ§BΩ²e₯qγ&όyPωiΐΐ/θ6 Λ¨?€mΦL¨ύ|!8Ί£ύΧ‘Œ3^λΠ6‘vβΔ š7oΎδΣΌšO―=‰vbŒ@€# σζF}α3ρOs@:o£θ“φG\#„g?΅nοv5^Ϋwφ-(v(£ω€γχޜρڎw:uΜΞΆωμ܁ΘG Μ5Ȋ“Mυ΅iΒ„q#ztΊuλ :ŒΊ½ίƒfϚ!ΏΈ1™Dη΄³ηΞQάΈq©NΪtχξ]©βζΝ›ƒ…eL<PκE<Β9.ηγλK‰&”xΜvύϊuΊvν%J”ˆ|9P>°:Ρϊ Z‘uλΧSγƍhυκΥT’D Iϋΰƒn”!CzJ›& ό-=_Χ―ίΰΆzΪBf΄A:f̘tγ―QβΔ‰=ep=xθ(ͺRΉkΦ³ά—φν;tρβJ’$‰Œ}:αΕ‰‡ž{.ΗΝcIš4©τ o`sωςe;ΔγΪΙΔ“ΡNŒ€ˆπτ³‹Ο–­[ιψ±γŸΘεΜ‘“2eΚθιδΙ?SŽΩ©xρβ4kφl:°€η)2‘́… ΅ύΐΚ\½z•vξάIΉsη¦8qβπω.Ίΐsšπρρ₯)RPΪ΄i}ις•+ς€/“ιΏΜ ²ε*Sύϊ ©FΝΪδηη'iνΪΏN‹-φΤ‰‰―zZtμΨ1‰C?U Ÿ8ρ'ϊϊλΑαiιΣ§£υλ7Pόψ <ω§M›NeΚU€ζΝ[Ρ‹/½L›6m’΄K—.QΧnοΣθ1ίQΫv―QωςUθύΊ‹ !Άσ›oS–Li¨{χž¬a"‚y©_ΏΤκΥΦτζ[οHŸwμΨAυ^~…š5kIeΚV€I“&³ΰ}GΪΐXοάΉ-ηψ’σέX*Yͺ5iڜš6m!LUH–LφfŒ@€  B~Hχνן ,HsζώN“'O‘Μ™3ΡμΩΏyΖ1ΑŸς4 >Ρπc?ΰ)œΦ£*½4s ζ―>}ϊ±ΰyZ΅œO:EE‹%2`ΓΓ!CFΠΗF%KW’W[·e3»ύR·–“Κσ u‘ΏP|ϊιgΤͺυλbjΦ³χΗԌηa|Gθx~ύόγ!?i[@ΓGŒ€4iRsήmž>aΌΘ»ŸTδΜ™ƒηϋβ<ζ3CΔÌ-}ϊτ΄πΟΏθŸε+)ož.iS§Mc-ΛΟT΄H13xύυΧhΓ†ςx­L™2œ6‘Ž?αΞ;u:΅kΧZϊΓ“·Δαο$]g³__·ΩλW$ }tζ›ΚΪγ)S§RαΒξΗzΝ›7£R₯ΛΣV~ Σ ’Τͺe J™2eΚ”‰ΖŸ@ΫX›2eJ1λH–,Ŏ[4Χ08 kfŠˆy4ί ε’6ακΚσθUnσπαΓτΪk©|ΉΒΔ?ΐeξvΟϞjεδΚΥλ|tO@wψ©–>}ΓΌ”κέ™ mΕdUΆlκπz;Ά+›KγΗύ@:Ά•xά•<ΉrΆgFΐ"6΄αΗB64& 5Μ³[—ΧDHGΌφω11Φω%šΜZk\k?-Z$š`ΔeϞ•Ž9*yt‘ίΊueΑ!Ζ!Z5,Θ„Ί1nΤ‡τ˜χhIcΑG Π,oάΌMβ΄ώ“όε—“ΫE@V,R”s~«Y£ύ»rOϊ»x‘Jz£σ["¨λΨ$£½#‘ θpεκž|yΑXlι―ΞA8ͺ6Ω{ B1o ` GΧοSͺT©ΔVΈxρbtpQ:ΰ Χέ[>αR 3~ˆ#`±Τη˜—$!„o(9RΗΒbΝ¦σ'2nίΎƒ}5„Ύa/b~ŽΨPNΚ_g oχξοS†τhξœYτΟ…τϋΠ¨Ρc<υ6‚FŒψFμ”oέΊM‰%ΈoΑ62bΝΘΟS¦Ρ·#Gσ‚?Έ.½χΕνθΈπΔΤ‚0ρpϋTϋ»ό展N 8:'uœίΰEΞ€8]t†xLθ+W­¦† ΫxsI}ΡΛΔ“νΛ¨7?ΚSsˆ¦MSΛ-dqΕωσθΗΗΙΒ ­‹)Φ³ΙEΧn½¨ξ½GgΞ~iΰΑΒχΠ‘ΓιδΙ“2Ib_ΥͺU=BuΓWπbΈ&΄qγFΡ.ΐ΄γίUΛxt>њ_εGβ[C¬cLπ§Οœ£]-Β=ανZd ωΩης¦ kE Ηκκ-[ΆΛυκQϋφm$ ΄ΡΧ^“σ… RλΫ²yFbΡx&r­ΠŽFΐDx˜ °xωάιCβYΧόt> 곍tMΓbζ5¬½eΫZz·KWϊμσΎTσ…:²ˆσL6xΰ Χ½'Z˜[}Έ:o’ύ‡ν›s>Ε"γΌyσx«#4ιPR Όψb€₯ukVΡ)ΦΊ2Tβa–Φ°QφFΤPgxώ1l0}π~OI‡η‰ϋ3“šβυθμΉ³’ΐ`ΫbyΒ§υΓnΔπ!τϋάΩμςs /Έ.'^1τ>‘²€Y[ͺΆ7#`žαfbΝhΆ¬™=:XaβΦΙŒ΄lςΰ 0/Hœ(±' nΞ6lΐφ· Ω–-―ΔΓά’Ε³ιυŽo²‡ˆ±Tύωj4π‹/ΩζΧ½r»B…ς4ύ·{­%MœͺZ±A†Ϋ3΄#γA_ w>¨Τ9Ρα}Ϊ΄iEΣς›,όπγD±m;tψρͺU«–τ₯J•Κ4eΚTΆΣ{]AγΒm±°œ,YR˜K•,"ΪgΙΜoΠaF€Fψ›α_s] Ε>.δ`‘γΐϊΊuλκΩλCρΜqόψ)v›7‘jΦ¬!u€WͺT)εΌd‰’΄4ί2jΤΈ©h| Ήόυ—ΒγqŽQ Ψ›0’€~VaBP΅Z ‚-mφμΩε±ΎvήqςεΝKεΚ•ε'QX?αͺQVηXNoαI?α₯uT¬hʘ1ƒ<αΓ|½—½½Ηvά³gΝ‘ηXC}νΪuΩ< F9ηΧdμ΅CmΏ΅>;#Ή DγΉ[βzΘ8ψW1{ΨEyςδVπ}H5a–¬έju¦γω`ŸΛ>‹ωΡY‘Pνx€u‘󁡧ι₯=lΐ!-ΦωΦ/K{ν3φ#΄Γ‹?Ycχ”Ό“/ς…7ηm¬E€ΠˆΜό”B7U ?¬ω“/½0―@ΐ|‚r¨GΞΐΚΒ$ιήΒ”ȏv0ο‘.η…Ύh½ˆ”ΉQ:Δ›³N~jƞ;N‹»7xFBpφ};rτ(=η9qΉ©ιθ“ζΣ8ŒI™hˆ;ΚεοάΉ+fpΚe4  ΤηΌ/šfG#`"'H+ ‡·NvZΞϋZγνhΒ‹€ ΘαEφΩ7°Ή*°Έΐ(xησΎ¬LXΕ‘-„°Ž΅AΥο£σάY.€}zάςΪ¦€ˆ|ΒΝΔ"’‘PmNx! #Ϊ8¬?FΐD-ξΉ c8C:ιΌ§ΔBZNσλ|©Χ‘9†Ά­ΠԍΌZ?ϊ¨qXοt-ΤQλςn#¨όoŒΐ³C ΚΘΈe‘ŸΫl#1F 2`ΉΓ£Ωπͺχ(γœ²ή£” IC’'ΈΆ·|pu[š0“@ΈωAŽ˜Γ΅^#`Œ€0Fΐΰ ˜€<K5Fΐ#`Œ€ˆbL@Žb7ά†kŒ€0Fΐ#<ƒηc©Fΐ#`Œ€0QŒ€ ΘQμ†Ϋp€0Fΐ#`‚'`rπ|,Υ#`Œ€0F Š΅€lξn’Ψˆ 7ΜΨg'ΜPZEFΐ#`Β•@(ύ »d{Ql§ιtœ=΄Κΐ3@Β1Ά¬ΥΝž!ΩŒ€0Fΐ<³B! G£»wύiǎ=τφ0§λ‹{Εή{‰½χή£ΪR1«΄(-JmU»vΡͺQJΥ–ΆTν]₯U{«±…BήόΞ—ϋyωš’ψkr.yοέsο=χήίίοwξ½ργc ϋX‹‰vLˆv°ΠΓΓ‡=dέΟΆΰC΄7J*Š€" (Š@Έ<AΆ(A‚ψT¨PR rΈXͺPˆcA>vΜ—σθΫe„@i‚" (Š€"π π­'δ8~ό§v]~ΊͺMPώΏΰΕR­Ηίs ΅+Š€" (‘Aΰ©™ϊGVΝ£ό½wώ‰JE@P§&Θ/b'΄MŠ€" (Š€" (Š@t!π”.ΡU­κQη jPE@P'# ωΙiEΰ?ά;^ ™40£†E@PE@ˆ%Θc£)Š@¬A³h€OŸ6ΦτG;’(Š€" Δ$Jc]Υ­Ό@`ώr Š€" (Š€"πdtή“1Š€" (Š€" (q%ΘqθdkWE@PE@PžŒ€δ'c€9E@PE@PβJγΠΙΦ*Š€" (Š€" <€‡i₯μ+‡a=‚Ω?ΉiQΛ^ύϫ\K+Š€" (Š€" ό?ˆ‚ r 2!5i1έιwύ1έ?Υ―(Š€" (Š€"3D;A6ψΚ:yς$aj)¬ΰ•*u*Κ™3'Ή₯Hρ―ž Œ#€T?J6rW’‘όQI’Σ§OΣΕ‹—ΈώψlΙ~H©S§¦όωσS’D‰Δ²mt>IM^£ίUft yOŸ>C&L’‘C‡PφμΩΒ΅€]Ψ»–G|όψ TΆlYͺW―³½&Ÿk{ŒΗ₯?.Ν”Χ½" (Š€" (ŠQŒδΓ‡Q­Z΅ΘΛ«-ί &!Μ§ύθΛ/fR£F ϋπHd†~υυ7TΘݝ*W©μ$‘H3ιPbΟo?‘?Ό”ϊφνCmΫ½NΑw‚ιηŸΧRχwΪΣGcFSΪ΄i™΄?ςό$]φt£ίΘLέ&8ΙOš4‰μMAΤM0εΜή”7ιΨƒΘ›2Θ‡`ΟgΚJBθ&Όt“/Ό4{Y=VE@PE@p νΩ BVΏAϊφΫ―(aΒtχή=Ϊ²e 5jؐ6mΪL5kΦpΎ;wξΠ­[·ΔΚ›8qbQrŸΆmΫΞηδωL ή€{BtβG€ΪΤπαš>}υθρސθ«W―Rσζ^τÏ?R··ίv’Ο7nHX˜ ‰|πΰ”I˜0!]»v’'ONhΪΩυλם2F„6E¨uaΒDϊγν4gξ<κΦν&†w™‡ΠδΙS₯ΚΥ©Aƒ¦Τ¦m{ϊηŸœ„O †nΰΦΑφe‰‘2d αΓ‡χί/ΘζΘQ£©n½FT§nC9r4έΈ(iΫΆm£wήιIcƌεεyΣs~΄wο^nΗ»4iκ―A­Ϋ΄§Π΄O>₯5λ‘G‘όπ»”G›`ΉφχΏ*ρ5kΦR•ͺ5©Γor] „ψ#αζΝ›4nάͺS―!5oαEοφθ%–v€υο?_$6αPΘυμٟS•j΅©Uλ6ΤύtβΔ ι7Ϊά·_šύωzσ­.Tσ 8ˆ$έίߟ-ιύ©ιK―P£ΖΝ€ΟxΑ0$Y*Ѝ" (Š€" (Š€#Θ¨$–QT„rεΚ–Νλι“Γ .ΠσΎ€…ί~I«WύFϊυ¦¦ΝΌεή½ί£Υ«QΗ7:0)ž(Ϋ]»vΣί;wΣΚΛhύϊΥ”7O&¨ΣE―+αƒίqβΔI$ V_„C‡QΟΒrό9ΚsηΞΣΪ5+hΝκβ/ύε—_IΪ=wξlςπ(LgΞόCξξΨ2{‡ζΝ›C₯J•”φ—(QŒJ”(Α~Νωhσ¦΅Τη½ξτz‡ΞR›5kkσ₯K—©aΓ4γ³OhΥΚετ~ŸχhƌY’„ϊΫ…?Πϊ΅«hٟXώΨφ‡€={N¬Εˆ,Yςuο>ˆ~Xτ ύψΓ"*[¦4½ΪΌ΅ΰ„ΎΝωβ{ρΉž:εcZ±b)Mš86lΨ(z,XHŒυϊu«ι—_ΣΎύhώύ’†~jPE@PE@‹@ŒΉX„­ζ‘;A$I)}Ζάtυκ5Κ—/―Γ{μ~‹nήΌy)Mj7±~fΛ–]’‹;›››νJ•*Rωςεč%K– E‹~ ϋχˆ‡½Ξ€l±ήΉs'pΟ¦N Χ ƒrώϋ4xψDϊuΙΧτ€Λ²9•^}υωλέ»«‰G5kΧ§-š;UΒzέ°QSͺ[·ŽΘΰGύσ/ΏR“ƍΕBŽψ'ŸΞ&τ 9sd’|ρCέ$Ž?.ƒM=H„ΛΘ‘Γ{Ω|’<==„DCβκζ–‚ ~"Dι£q“™πώL… ’ψ;οtc«πh:zτ)R„ξέΉG^oO™3;κόςΛ―ΙΫϋˆδMΞn’<`² {Aϊε§Ε"ΗνΤ (Š€" (Š€"%Θ zΖJ K2ٍΧικ•3”%Kf!ΕΣ¦}Jί-ϊ‘Šρ xœž:uJπY p“ωE@YΈ. ύp8π=Iy™\Ý7oNύ·%–Υ­ό%~½ΑΑw)OžάBa>ώ<•)ξNσζΕ. B}‹Pžο…ZΌ-Κ”1ƒs(Aόψο2ώπΑCJΟΎΎ Ϋβ7Ν$;C†΄ΞώΒ΅~3f€}ϋφΡ„‰“ΩΕ’ϋAΧ’!ƒQΆΧ]‹~όq1½Ρ± y9C£FΌΟέΔ­γrΰWŒΊ`‰G}υκU`όnΫSaΟ<μΚβπέFžδΙ“Ρ=‘]Ϋ6Lχ‰š½μEΧί’q£‡Pϋφν(U*ΰόoίm)€E@PE@Pβ01Hγ ‘3ξ fΏdΙΟΤ₯K7q™ψmΕ ™ νμΉs”-Ƙš­r•Ίβλ8'LNC-° s“?žBY³f‘Ο¦Β/mdέργ&Ως?:“ •½zvcΒΩύ‘0τdsρ㒧tιRBAΖoί"ώ€Ι/ˆ'ά6πq‘ UΒq~δ1ΑN8Ρn“WŒο~K³gςεΏρτme„άgΙ’…ZΆ|Mώΰη<Š}’?ž2•Ζ~4†ΛttΝΙ“%“€ˆΑκŒΑxλΦνΰ©δά€M Δόθ%ν0x'γ²]Ίt¦·ήz“|}OΘ¬" ؝;Ώiš­{E@PE@P1ψέ’½ϋΟq:~άW\†ύ>ϊh4@λ+M€3 ΰ:}χέχtαό Ž9ΘΘύςΛ2Ί|ω²Νψ°δ2‘ρjτ;nΌΈ₯dΜ”‘ςεΟΛψ‰Zέ(Š@0Ο<ΫL0ΝΒ³Ϊw>oΌ½½iγΖΝ2;fΒΑtŽ~ψseMϋύkžE ο… !’2HΓX#GŽςsμ>»€e“vαYbΧχ8=Ž*=#‘Σ\ΛΊΖMވδ&=Ίφx]¬'8 N`74 ~.RΔ3ŒsΞ_β/’… ΉΩΰ+ePΠνΠ1-FJt–Ο1°Γ q{€ξΤΌ:,¦΅‡£Gς9ΞN0ИΆ έ.·ηcΉ* ζ=~OόoΣ¬υήΤ­ŽeL•Œ26ρ‘ΰLΩp’\rjTˆ[$Α!2]ΖΝ~εΚUt–ήIϊWΐ;<;α5 σ=•2‡ΌζᏼˆƒPšόˆƒ,C?βαΘMΧt£ {XΊaqE0m3m·—s•ΉΖ‘Λ΄Ε›z }ΐB"Θcκ1{τί΅ φφ#,ΚΘgτbo―K2ρΖή6”E˜‘―¦^‰θζΉ!ܟζzn ӊ"€ΉwΦ­_Ο_ŒJΡυΐ ΰ—Ο7ίμΔ_snQεJ•( ―’yθΠaκΣοκϋ~Ožο<§§άIU«VCœΜύktΊ6ιφtΈ‚%OžB9γΕΧ5¦wΔΈŠ„‰’ΠιSgd¬A²dΙyφŸςBލ.S·«~{Ίkβ¦,ςΪγ86εqŒ•PοπXœόuΞ.GZtLWyσζ-ž’sž[ ΪΜ93νή½[ζ“ώϋ„ΨΒ}/]Ί΄΄gΟ^:Νγ\Φ―ίHiΩPq‡]Σ`˜9|Ψ[¦=Μ/5IdjP‡γΤ©S¬kωωεζZœ793Κ t`œ ΎμeȐžΞπK /^ό“<£―\Ή"/C˜=θ‚SŽςώBͺΝoEtΰ`°Εε‚+ΖΔA_^ΌΊ—ΛEiS`eWs=9΄ƒ›―«Ž–˜²ΡΡ.Υ‘όΧˆ1‹ˆ€17)Ν1nJ{\".ι摌΄θΈ‰Γ«ΫΘLύΡ±7:ν}42³/Ν^wdσΩΛ˜γ¨”5:t―(ž;'™τΤ―Wη_I7h†B5jΥcό1=R^ψK—τ”―Qp)›<ž.uθοϊυhΥκ5<Εhjς>|„€δ˜BtοΎύς…JεJτώϋ½YO<š5{“ή Θ_τ^jˆŠ/FO3~ό$—r“vό½“Z΅j)ƒ±2&LζΉθYΎ‹Ϊ΄ρβΉτKECο©@[ο² cΐ­`J•< %OμψYη¦’GΚΔLϊ}n3€Ζ—ΨGΧ2"·οή§ »!”Ξ n‘·”{χω\σ@sτ!„γ/kWβΘΞ CΡ‹P7Š@,Cΐq΅Ÿ;…Χ4ΧxDε"#Rέ‘Ρ™<«ηqivέ‘Νg/cŽ£RΦθΠ½" ­Z΅šΊtνζ$Η ŽPΌτηeQ!`bqλΦmωb„EΪςͺŸWxUKά‡°.ΏΔ+[Άiϋ:•ηΥCW­Z%°‚4ύ}ϊιtκ?`5nς²¬~ K&Ζsΐ"‹Υ< "δ[B7ηqωΒ)!ͺFrẂŽ€Υ;1pΧΎ‚(¬°hΒΪuλxeΠΊΤ«5U­V‹§ΚόCδ°Φvz³3}Μ3 ›Ε†Ύύvχ‘ηoC­[·ck«Ÿθ7~δ?e%T DyG0Ζ‰DqƒΟθ³Ο &oρeJM¨„;άю=.€7Mš΄RSv}Έxω Λ}dΞx|ύ9s°eُ19Ξ–-‹X€ρ΅.{ŽμμͺβCǎως˜”μςr+2τagΈ Κ‚4bΪLŒ9ζγ+_‰²gwΈd€J•š2fΚ r+ώ2[“Α:Šέg,Ά9G'%―Ο·’ϋΈ5τσŸΎ’—Š,‰γΣ’mΎΤrϊFς˜²‘ϊ|υ]c’Œ6<ΰ±>ίlς¦|γΧPσ™[¨κΔ5΄ϋψEI›Ύb}ΉώSOω[iΞZGΒξσ·Ρ†=ŽλŒ’ΜΊQbΟέ‚‹°Σ(Š@B€oǎ<Ÿω+sc$Θ π€\ό!@nˆΠΓ‡hΣ–lmKΰT[±-ϊv&5i˜}½Ε’xδΘž•¦0/OοOΏύΆŠ­—ίΠ‡l•Ο3λΐ9{φ >μC&wxΰςΫbΑ6ŸιM=)SΊρ*3eπ3ˆlŊ…°ƒš0gξΞDAlήΏ/͟%½χ^O^Pθ 5¨_Ÿ6oήΒ>½Eiύ† T½Z5ž’ς"»w%₯… ΎαΥMη2™<Γ$1+a%Π^—ι#A Ώψbυzο}^νσ;PύϋοžγςUι•W^vZΙM[M{’²?Ν–bX/^"‹<œcfΈ:`Ε 7Κ\σˆƒΤb@σ }ef%X}±Κ)¬χ & !CϊΛΛGŽ9dΌϊ›• 8f‚5cTΰNp<ΈŽΞ;Κ<ψ8Ο°Έ£κΌς+\ΰJ–*IΧx\ κyd² +ΖΫ`̈±Μ?kAŽρNs+8„ͺ.ΨMKšzPσJξ΄aίκ·φΥ-™‹’π\ό›ή‘*Χƒhζ•ΨJ|Ÿά§m¦ς;ORΫZž΄vοizγ'oΪΡ« ȞŽ–2±.;cŒ}‰ΚεΝ@νV‘·κ3NΧoӜTχF0uα8¬ΝσόSŸ†nψ¬gQΛ½θ(A~ΡϐΆOP^@„Ο2ι1ξ¦a†<ƒ09ˆ+ΎŠ9SΩηΥMΖ!8p@¦dΔ\θ8›%KVž›Ό ύ͟εA―²l̘rŒ―½Φ‚ήξΦCH¬ΤπoYC]‘sη·duΝeΏ.§*Uͺsς}š3g.uκΤQˆΩδ)ŸΡWσf9Wymήό κΕDsΛζίy.φ1B°‘Χ«eK*Δ. ή/^€<ω<Ξ]½IuJζ¦}ό‡pσN‰O―Χς ¬iSˆl^ν|tπB ³γ -hξAε :fgκT· ψσ νσ9O%ά³ΉΫϋ) Έ―ίUz³`ς»u—Ξϊί€ΐΫw)G’„T0«Γ2/=0έ(/J_‘ΝPX‹‹x8VγDk±&ˆˆMK4$Δ1';f˜ΉtFŒ-³&ΐz Χ 3³M|Ά2cΰ˜ ΠŸΒνΡt•ίΥeς8φhƒcq&LŸ‰ΏΡ£FˆpƒυΙ“gsπτπ LΌΪ§}Q,hΤ―_7ρ#>sζ “κΚ’νΡ7ΎΞwορj€Ή³;ϋ +(ΉΑw>·πA†?u«Vm™|;ϊ R ˆ˜ ΐ˜ƒΫΟŽέάR9v•Γ ¦,Κ£πΣFΐ±9‡8vwwLεi—£¬ρ#v•Wf_e£ί‡'—ŒΟΈΑΛ―δIΡ±•iμςƒ”σγMT4E"šΨƒκ•Κ#mΘ•4!ϋ ?ΘξΖΗ!όε―V{Sχtβ e\ ³ΈΡ vΑΐ ΎŠi’Ρα3ώ΄ϋΜUj[&'Ήr“ΎB<υu.ž11G#” ?γIΤb/<J_ψS€ TCzπΙ|δθ±l‘τβOτŽΉΚΡ>XΧΫ@τc’e· RΗ£:y-Ν«jΞp~fΉ4$Ύ΄¨ΗΦbΑΐΕ’lΘ΄I0ν:Η+’ξΪ΅›5jΔ„;‘ΦϊυλΡΫckο–ƒ~T‹]tdHjΚΐS»ω1ΆΓk\ι4IΙο[™9`°ώΈDmŠ;hA—b™iγ?ςφΏMνk¦ΤΙΣ”-Ύtuφ¬”Εxκ8ΖοQWE¦E Ά πΘά[z€ύPE ¨W―.ω_ δFΙ8|ΞίΉs“Ξ”Ž] `Ε =Gΐͺ›wΔW΅hΡ"δ}h-Yς“Xρ<ΚM_zU¦ Cށ7eΎΪΠ‚Bά0}xHͺT©hλΦ­²ΰ„!?)\&fϞΝξB|‘χσΟgς =w!΅cǎsYAτSρsFω:ujσΜΓΔ -]ΆŒŠxz >‰τΏ†ψ΅b—…Φ­[ΙœΛ ΩpλθπF'qo€>Έ#ό²τWρυum+£+ΨI°Ρ‰ϊ"’‡ΧΘ’CŽ:ΓΣ‘ά΄χiχ†ΠŸ ΈM‰F―€uμOœ”g«ΘWŠ„άwΎXΐω-ž}B˜oh(wσžΓͺί±bnjσΪΕσ‚ξWλΣ©€`*V ‹δ.Y 3 έ|†’±₯d»@Žt΄ύά ϊιμuςΘν˜9ΌΎ>m_4Ώ"π’" δυΜh»Eΰ…Bd‹ύό½}9ŠWυlH9yΖ‚ν»Ρ‚ ڊF'b‹k‘‚ω₯ύ U+—ekί}φΙΝΐ3@ΰD‡Π·ί.δ•@ΟRϞݨL™’7Oξ\b­•ˆθIΘ₯^Θzυ|—:tμΜƒΏ.Π”)“ΕŠkΪ•%KžΣχ0υξΣ—Ύ˜χeɜ‰φ8F‹™ŒWΰ)ήή}χ­ :jδpI+R€­]»Ž Κώ»)x@Z /v²‰²dΝBηyŽaY)Tr:6΅xκ8ιsλφ”“W½Ν$yάΨ1Ξň°jwžΑ’œ;gΆX©…θΓτ©!Jbš+ƒ-¨™ŸΪ.;D₯Χ£­A!τ£W1*˜--’FiS·™Κ’ρ—ŒΜ)~Έa|Υό5ZΈ‹<“%€&Τ»ΊU’ιΣχΚ‘žjδp£¦E„95O!ΧΡ##ήΉOyΊ8=•YέΗFbl%½Ψ–φIˆ  ―+ιEΑ£¬δaA ξΚΜδΤ,οŽVβ\γξΘof.0Δi<2fQ0ώ–gXAͺπΙ2»[Εέ»wE·ˆ0Λƒ± .,šΩ0ώΈφv» jOsΜƒH©R¦’Δ‘ξφώΈκΒ<ΘX…ςΜ>©7h τ™•P\χΡ‹ζ2ΎΑƒη°οp2Ά$#`š·ϋ˜Ο˜-ΐζΊƒ―1Ξu"φ}7α6ΟJ|Ÿ₯ K¦‘~/䁸_˜ω₯<Λ1G²E Ά# 9ΆŸaνί ƒH†δζtD©!vBiA†`Ȉ‘»ξ]ΛFΆτΈ–}’nΧ2α•·ΛμΗeΓ« 2{νενΗe5}Έβ쏨&ΉZωšθόETNεŠ@\Aΰ©],μ7S\Iϋ©DzοDŠ/†s.AFL02hoς™²&Q~»yWΞθ2yPΦΘμΗ&iι&͞ΗΑ”3ωΓΣ…2H7y]uh<κ[Ωe]μ½Y¬ΕΡΕV–#]6κ­V Šΐ‹ΐSdΗ'@|F3ΕΏ‹ΪBEΰ~΄π©<̈™³΄QD ²d$Όjž΅ldΚ=)Ογ—φ,ύxZ}αΥ‘²Η#ΰ »ΟQjTΚF€SεŠ@l@ΰ)2¦zHGϊ²¬±‘ϋΪEΰω λ V.ƒO©˜yžO΅Z‹" (Š€" <OA-™Σ²P‘2C-ΘΟ€Ά‰³ΐŠ ς±cΎŒΎ]ΖΩ A;(Š€"πŸ@ΰ)2ϊOΘ1FYkP§Cΐ1Β_>h>]AΝ­(Š€" (ρ§fΊj9~ηG+‹E轋N¦vEPE V#πΤ9V£‘SE@PE@ˆσ(AŽσ—€ (Š€" (Š€"`G@ ² =VE@PE@ˆσ(AŽσ—€ (Š€" (Š€"`G@ ² =VE@PE@ˆσ(AŽσ—€ (Š€" (Š€"`G@ ² =VE@PE@ˆσ(AŽσ—€ (Š€" (Š€"`G@ ² =VE@PE@ˆσ<εRΣ‘Η «†…·rXΌxρΟ;˜φΈ.“‘όYΫ}¦φγgΥ§εθBΰαC+ΊT©E@PE V##ΩCC]4ις˜Š›ϊ\Ϋ‘<*ν°Χa?ŽŠN-«D\λW―Hςαύ4ͺΝΧςŠ€" (ŠΐsE Ϊ ²!WψΣΙ“')~όxbIYΌw/„άέ P¦L™œ2δG0d5nGÞfκωυΧεδγγCύϊυ%oο#΄yσκΦ­+Χλπ1ωΝ;Ηα/^LTΉΏΏ?={ŽJ”(ξl2Ψλz\ƏŸ@eΛ–₯zυκJΆ;wτΟfR+―–T΅j4ψκύ^O*T¨P„}ŽHDrτcΨπΤ«η»‘Φ ]υιIiφvθρά_ιΣ§ύo6^[­(Š€" }ϊτ4~όΨpυ’ΟK4/Π‹Ύ€ΰ#˜Ά™z‘υ&OžœR€H!yt£(Š€" (Š@\E Ζf±xΘ„0H=P4ό$‚4D¬±οΌΣƒ‰bjϊnΡ"ϊλ―Ώθ΅–­Ι½PI<δ‘α»pαuξό65iϊ2•,U‘Fγ${+W­’1užΏ»wο9ν ήoΌρ¦ΐ9sη:“@οqμΜ9C:½E-ΉΥͺΧ‘©Σ>’ωΟ?~Τ₯kwͺV΅ux£-X°PςΏ~‚[…ΟqͺZ«1₯O—†jΦ¬K+V¬d«t΅i۞Ž;&ύΈΜD·ΤΈq3ͺX©:5ΪΩG_ίΤΌ…½ΪΌ%εΘνIΣ§&ϊ7U`WˆjΥͺr»Π}Ά Βϊ ύζ[]ΉΞš4yς$&™Χ¨q“WΕbέ©SGš:e΅½}1wyxxroˆ._ΎBΧ―_§ *ΠΚ_Ώgβ?”Ϋ»„²dΙ"yΓd±aĈQτλZ²δ±’·k†ΈJ 8€01mΤ°>΅mΫF^ΰ³\¦Lͺ\Ή}6}ͺψ2―ψm)e͚U^0τf½R?lΨJΔ.,?.^Dχ™”χ0Hˆύ¨‘#˜˜ΝϋbU©ό%ύπύB:Ζdn Κ—£bŊ1oSΧ.oςΛC:pΰ½ίw U©R…ςδΙ-D$Zƒ" (Š€" (q ˜#ΘρβΣ•‹§θ‹/ζ3½/dξϋ"O›:Yά.ϋ_cΛοHΚ–-›ό½ΤμUjߍ_œ€χzΏ/ΦUwνΪUΘΪΝ›βo܏­°πΛE€uΪα—+Ρ7 ˜°cΰ ΘυςεΏQυκ՘lί’dΙ’“?ξ‡eVΰΦΣ²₯?IΣ§OGσηΝ&―6ΩͺάQH/Θn¦L™)i$¦δΙ“9-™Ψ/t$ΦrXS§NI‰ΩŠŽΊgΝόŒNŸ>ΝιY€+~[F—.]”ςΓ‡ Λp`ΰMiSγ&͜ύϘ1ή-€τΒŸ6MjΦ›Xτ~ώω,Ά|CY2g½Γ†αŒ"τΑΑβ’’5Gjםψ—+WŽ^~₯…”CfΈšœ:uJπ,^Ό8mXΏΪΩ%Η§nE@PE Ž!c~­5kΥ£9sgSBΆΞXjα3œ6mjiE@<)fƒ_.ςΊΉ₯7ΔαzρΑΠa”„¨eΝ–•ΆόΎ† κ‹$αΟ·μH »MΐQ_Σ¦MΨνΰ]ϊlΖLΆ–V¦Τ)έ$#|tkΤ¬%n΄m…ϋGΪ4nb]† &Y_sΛ:Ϊ-ΎΠ|ˆtYΈo Ÿpyπ,ZŠgH/rδ͐!½όA°zΝ>|4“οŒ”σ¬\ρ+υιέ34οvWqΜ‚A Αlν6z‹°ήtι nHK“&-εΞWXκŒΟ/*Ήrd+>”Α9EŠδb9Gό >?3gΞ¦ΒE+Rή\Yhβ„ΡΤ°aCι;5(Š€" (Š€"Χˆ1‚ β +kβP’hξΎΔηpηΑDΜ!Ad$dΣ₯}χέ"ςβ©Σ@ZǍ›@wxpŸ#¦XΔυ>έ €Z<5ȁp’έ]ήξA#‡ ’r ²{φόMΫ·o .¬ΊΑάn‹Λ;Ϊb9šFάfiX‘3dΘ@=zτ’qcΗH+\-ZΌΦš–-[Ξx„Ύ§pϋοr7lάHί/Z $z’&MFΫΆmaξ?˜4³5™;W θ…ϋEΧ·»ΣΗOawΛ„ωžGωˆ>ό-ξL’Ρg΄ΑξάΉΓ:β >˜iδη_~‘Ί2fΜ$Ω _ƒ" (Š€" (q#Θp™ˆΟ’ 1μΧoy.,σΓ*ι^ %JθpCψ9³gck³c*6Δa…΅dmγΖM4rτ8ͺ[―!­Y³–ΖO˜(iΘ+uΊt·XXσηΛ±;ΡC{@RP?7ά+fΜ%d€„rώόΉ<³ƒ΅jΥ†-Φm©ΫΫo²n[)w ΜςKφ?ό(²,Y3;} QΏ§Ga‘cƒϊ‹υtZ™a₯M“&γW^υβx¨cΗμۜ™~ώωκά₯Οpρ’X΅ϋΌίOΪ =\8cΖ,ͺS§mέΊUάQ<= 9υb0°jιΥZttw§>}z£¨_Βξπ&GŽμGϋ κO“&Nα4ZRcΆ¨ϋν*Γ Ÿ ¨υX`Ѝ" (Š€" Δ1β1Q|dΆ}Lηa½υφφ!OΟ‚Nrϊ˜μb‘Ÿ]—L rψƒeΣ#‹k~Ό ppE@€ΫCπ`JΗηΠdX˜AH1Ξαυα/ΌA{ȏrΘ‡€N;)κF€£NμΨMξ!ΖΥΒ€‘,Ϊςvβ}Ιv΄ ~Β\λA:ϊ`'›ΧΩ­‚…,CŸ °£<^Π”7ύG̍φ£­hsxzኁω•M0m²c‚ aΕG›  nθ+ζBΦ3ΰœ>Ν=3­P­Š€" (Š€"π$bŒ ?©β§I·“Σ§)χ,yΓ«+†hŠΐ%ΘΑ“¦MVE #`ζ–?xπ “˜‚€BL‡{KŽωޏωψHYδΏ|ω2:uϊ_ς³gΟωσηEŽΌ(ƒό'Nœ$0r€=z”0Ÿ=ςΨ ³]=Ρ@„Ήλ2Ώ?φώ7ƒiΜ/{θJ c%UϋJ΅ŽŽ­)k—ι±" %Α!2@ΰfΏrε*/mœήωPˆL9Ν£(τ+Aˆ>φοίΟdτ͝;ςζΝC.\ Μ™3Σξέ»ω·ΚŸΎώΚ–-\ηΥVΣΚ ₯§OŸ¦υλ7RZ^Ρτ/<•,YR!‡’Γήή”$qn c§S§N±=δηwVdΙ’%§«W―?ό#:°`Τ­[AΌBkz:Γ2ιΕ‹bΙΈώ+”W‡=qς$]pΚQή_Vt5‹4±β(‡GDœ ZΛ«…ϊ°yΔπ…_ΏV­^CiR§&οΓGhΠ ώ"_΄θΪ»oΏ¬†Z₯r%zύή¬'͚=‡Iο δz_jˆŠ/F<€ργ'ΡΝ[7iΗί;©U«–TΆl–?  &SΰΝ@–ο’6mΌ¨T©RφnDωχξύp+˜R%OBΙ;~ΦΉ©δ‘21“ώ„ΌβμCIϐ:Ήƒ‡cDnί½OAwC([RJΐ…@’οέηΥiy΅Uτ!„γuΗBXRB¨Aˆ₯θΥKO¬vKPΨŒ@ζ,™ι>CŸLήβσΧ͌]XAš=.€7Mš΄"Ο‘=;]Ό|…ε>liΞDnnn”0aBΚ‘3>νG‡˜gΛ–E,ΐI’$‘μ9²Σ‘#>tμ˜/εΰγD‰qZr±JagΈ ʝ"Uͺ””)SFvίπ•/­°#€J•š2fΚ rΆjg碁ΠFG€%aΫ‘sTqβZςϊ|+Ή[C?ι+rΈTdIŸmσ₯–Σ7’Η”Τη«?θi΄αΑC‹ΎΩδMωΖ―‘ζ3·PΥ‰khχρ‹’6}Ε>ϊrύ!§ž󷜡Ž8„έηo£ {N9YE 6"πΤδθΊΉc#˜Ϊ'Eΰqθ½σ8t4Mx:N³₯VγΕ‹—Π«―ΎΒ~TΈp!quxΏΟ{΄aγFͺ]»–ΔAšiΠ€ΎL¨‹Υχ₯Λ”4iR&Ι hȐώt‡]&rδΘAΧ]£$I’RV&ΰCχ#,Ÿ.]:q§ΈF\GηΞ)=Λ0`οή½{μήq‘ͺW―&δΊd©’tνκ5ι ά.Dž4[ŽY~-@, ΫQ ΗΰΩ·‚C¨κ‚έ΄€©5―δNφ‘~kQέ’Ή(I’„΄ικͺr=ˆfΎQ‰­ΔχΙ}Ϊf*Ώσ$΅­εIkχž¦7~ς¦½ͺPμιh)λ²3ΆQΐΨ—¨\ή ΤnΕz«~1Ίtύ6Ν9@uoSŽΓΪ<Οο:υiθιθBτπύ¨ΐ‘eAΰ) ²Εoμχε­Ÿa4(Š@δ9Ζ½κωΉBšKP"D iΣ&‘i“ΠκΞ|―½ΦBΘlΚ”)©Z΅ͺ"Ηο•—WKŒw…ύ•σR… εςΝ›‹_ρ7¨X±bN=͚5£γǏ³…8εϟOδπeΟ??νέ»—-Ζ™ΨΟ8»Έ$”(Qœπ+6H:5κ)Ζ_Ζ aεΞJ’x  γΣΙσΧιάΥ›T§dnΪΗ7ορ(£ψτz-ʚ6…ΘζΥΞG/Κρ7;ΞΠ‚ζTΎ`V‰wͺ[„όy†φωœ§ξYιάνύΘάΧο*½Y0ωέΊKgύoRΰν»”#IB*˜Υa™ΧO7±§ ΘόI†ύŽŽυΏ$εΗ±πjΠ.Ε°φ<δO‘°8±ν'ΖκQŊ@\Aδ δΨlΗ±›[ !ΗrΈAΰΟ”E~ά“ΉrεΨp — »»»;e±=?δ•ΩWΑθ7ΗαΙ%γ3nπ<ακ(y’Dt¬ke»ό εόxM‘ˆ¦4φ z₯ςHr%MΘ>Θ œ΅ΈρqžC,Ωs=˜Ί§sgψ('`2έ8‹έ` ꫘&>γO»Ο\₯Άer™+7ιΠι++ξ\ =%bbŽF(AvΒ«± § Θ–ά@… P r,»΄;1~D`A†?#ͺΔ|…Zƒ"Λ0ΔΜNRΡeΘAPρgH­]nޱG@δ5ΗrπΉΡοšr΄{ό™‘ά€?λUΐhUέ#ΎκVƒ>ckο–ƒ~T‹]tdHjʐ*ωΉψ›'^J§IJ~ΧΨΚΜƒνφΗε jSάA ΊΛLψ‘·mj_£0₯Nž˜¦lρ₯{¬³g₯<(FP:ŽOβΊQbOAΡνxBŽνΨ†φEˆI0 H­Η1‰°κŽ‹„χ{δJR .vβjdΨG—<ΌΆ@Dr€=K0κs·)¬ίieSOͺU"7e+EBž«9ΤΒlρμφχq”»yοTΩ±bnj87Κ™žςgK+>Θ§‚©X,’^²@fzkΚοτVΙΜBΆ²ΕxϋΉt†IωΜά$ODΈ=KŸ΄Œ"π’!π”_TΜ;θ‹Φm"πb# χΞ‹}~΄uŠΐCLsep£5σSΫe‡¨τϊc΄5(„~τ*F™πž’FiS·™~%γ{™S$–(ά0Ύj~‡-άEžΙR΄οκV‰r€w“τB9SnΤ΄ˆƒ0§ζ)δ:zd€ΐ;χ)#O‡`3”K\7Š@lB€έ‰"Ηx1’ΧΫΫ‡<= ²«Ε#Ÿ¦Ψ†φEˆIτŠItU·"wΐ\Ζ7xπ\φN:2¦y»ωŒΩςk5|ρ“Ÿˆgξ0α6ΟJ|Ÿ₯ K¦‘~/䁸_˜ω₯<Λ1G²E Ά#πΤδΨˆφOPE@ψ― Β›œg•ΐβ Δρω/q’GDiφ…=δ[°”M$ƒύμeqŒπΈςŽΊUb/JcοΉΥž)Š€" Δr@†d—;ΚΎΗΖZό€nΛ0B[Y>ŒtΩ'ιΦtE 6  96œEνƒ" (Š@œEΐAvŸ­ϋQ)ϋl5j)E࿁€:ύ7Ξ“ΆRPE@PEΰ9! ω9­Υ(Š€" (Š€" ό7P‹ΖyV*Β[,HS{ήυE΅hoD~₯‘υ-HGLΘαϋ*!ρ«+YLΤ«:E@ˆi” Η4Βͺ_ˆc˜EΜhzΣύˆδ&=ΊχΟ»Ύ¨Άί΄7ͺzžGy_†0γG΅" b Š€" όWP‚ό_=sΪnEΰDΰξέ»΄gο^r/P€2dΘΰœr Mέ·o?₯J•ŠςεΛλ°:†ΆY,£b΅δς+{θΠ!J:5εΜ™3³θ²@IDATL;B«uΆ!<Σi> ύŒΟ½{!²κζš‡EY¬ΚΜF=<=»πκΖζŸΝ˜I―ΌάŒςδΙσΤ}αι58ΩχXZψ³Υ©V‘lT,WzΊ}ο>>γO7oί“ΥΨrgL ήL?ν8A)y>ήϊ₯r‡Ϋ»Ξ¨?νϚfΪ„ςα]HœnS>’½«ήΗιzRρrυηŸΡ?όC­ZyET­ΚEΰ)ˆ1‚Œ›ΦάΈα΅'ͺŸ?~£±ˆd&ξE fH’$ ύπΓbQž9σ3ge‡{S©R%ιΐƒ"³ί«Έ—μ2ύtjέ&Ρa―ί5 M02³7εž5Νθ±wοΑύ4gΞτί9Jΐdη·ί–RΕJΥ)GŽltγΖMš5σS!ΘφΊρ 1‚Ύ>½‡RκΥ™ ³+‹υΔ{4­k}q΄Γ7Όt,:yu·φ£){ΞR·zžΔ‹SΌΝvšsκ:•J•„φ^ ’•νJS£y¨hΆ4Tψσνt)_FΚΔ«…§Σήg96:Γkϋ“"Σg{£2slκ5q“f/‡γπ‚kYG^£ΟμΓK³η3ι~~~τϋο[• ‡ΈΚg@ Frx7ΆkΫ’ς91"ύFξZ—ΖE ζ0χίCQ¦L™¨•ΧkT³fM!Λύϋ’Ο?ŸΓ€΅¨4δ.ΰϊuJ–4%OžΜΩ8XC&t<š 3$$„%J$r€γ8((Hδi€ΉΩ˜6μΩ³—J•.OΫΩͺvζΜ?L–L䑐ϊΈώdIΉ~Η²Ή¦,ς€`£ )S²E”σΫΣnάΈ,bvM»Ξ}‚ξtι…)wοή=Ί~ύ“ά””4iR)oί$Nœ˜fΜ˜Ξ„7έΊu“jΧm@3g|"/φώ£ίwοή£΄iΣ96:jΤ,ο\α4λ@}hκBρΒrλΦ-^f%Tƒ-κ¦Ϋ·oSZ—vύ ΗS7ϊΠΗ5σQb^‰mνn_šs؟.l($ψۍήΤψ—ƒt―xN*”3=uΘ™Š6μ=Cmjz5ΡΆ·ŸkΧ θ›λω ΌIάgΌώ+͎₯)‡o`‚=0KX;€#οΖφ:_#)R€s>‘QΐJšh7+΄9i2ΎφωαqΧϊ‚sžVϊiϊŽΊpήЏτιΣ“›[ Ή^#ͺ_εŠ€"πtDϋ,ζζ½rε ύύχNΉΓk‘#GιθΡcα%=Vfτ_Ίt‰6oήB+W€mΫΆΛƒΜώ3Jα…Η₯!DεΒΣ₯2E #€ϋ€,cƌ΄ό·ί¨V­ZroΨ°‘V­ΪJ­[·ˆ>L―6oImΫΎN•«Τ oΏ]ΐKβ:ˆ1ŽΏώϊ'”xŽ΄i۞°Ώs'˜zφz>ώx*εΛ_ŒΎϋn‘3Ÿ90/ήί-ϊžFF-_{•ΆlΩ"ΙΈŸΡF<{ZΌζE-[Ά&χΒ%EΘ π‰ϊΝ·ΊΠ«―Ά€bΕΛΡgŸΝr‰΄›7o¨Ρc¨n½FT§nC9r΄ΘLΪΈρ¨N½†ΤΌ…[{ΡεΛ—Eη_ν  ›P‡7:JW\ešfβΚՁΊsη“Θβ…„wφμΟ©J΅ΪΤͺujΧώ :qβ€³_wƒο φμΨρ7y)E'Ož’tΤk4plάδ%:tθ°Θoά€ΎύϊΣl~qAŸ«±ξ1‘Ώ.ν6Ο?³?-ˆ~ΉDžΉ3Jω’²Ο :BŽ!Ȝš‰?·n―ΛJ_ν;GzŒ’+˜σxυκUκΫ·?5}ιjΤΈ5š₯.œ«ρ&R: ψœ4’=ί#όf xYψ7–'$ DυύΎύhρ’Ÿ¨K—n¬χ%9o~gΟJ:ΚγkΘK\gΎ~ΛW¨ΚΧφjgΧ8@Λ–ύκ<Hΐ=°}ϋŸτΞ;=i̘±BhOψϊ 6|DΈΧχ?žB•*W§ šΚωΓυi°Δο_ΕJΥ¨IΣWhάΈ tΜη8α ŽE@ˆ&ψa©ΐ?`ցήφ όC#Ι[ΆόŽΗ€υηŸJr~PΘ1[g$ν½χϊ8eΣiLy&ΔRΎ^ύΖΦϋ}ϋ[E‹—±ΚW¨b>}Z²"ŸΙk/kdfo°OfOΧcE ͺDφŠj=Οςζ>bλ›Υ±Σ[ΦC?΄Šρύɟ~₯YLωήMl}ρΕ| ρύΘ½ΌbΕJIŸ6νSkκΤOœ]8ώ‚•3wAλόωσ[89oJ«oί~–ΟqλκΥkΞ|80uŸ>}Ft\·ψ%έ*]Ά’|Wςϊϋ_ε΄€[³ΉόUIΗsŠΙ†€W«^Λb’e1!·Ψ‡Yτ,\ψ€Mœ8ΙκΪ΅›ΕΦ? z^οΠњ6ΝΡΦ₯K—ZEŠ•‘4&˜!‹‰’rοžθψuωrΡ±iσfλ₯fΝ9_€ΔM›1Ηl΅΄Š-eνέ·Oς`Γ/¬'­uτθQ©ƒ_¬₯Κ[l=”grqΖψΨ1nσa©Ι˜”έ·ΏΔχσž‰‘υ㏋­άy [L -Τ“ qFkΰ ΑΦΕ‹—¬S§NIή%K~’²ζYΞΦR‰o;rΞJ=|ΉuϋnˆΔν›mήη,κ±ΨZΆγ„SόηΡσVΒaΛ­;‘ωCΥ8ӟυΐΰτι§Σ­Ξ]ή .\Όh½ςκk~w&NœlΥͺ]ŸϋtΪϊη?« ηkϊ’ΆhΡχάΟda°,VΌ¬\_ΐ₯€{«₯Wk똏…λ΅_Φk-[KYœϋτs[?ό‹ΰΉwο^ΑμπαΓ’>kΦl+YΚ,|½Km58nή²Eς.^²Δςσ;+ε§N›&m3ΧTΫ5…ί9~‰γί΅3|―\·lυμΥ[τž<ι8WΈΖpmΨ°QtOšτ±€λFP’ެ€‘ ‘ύq7ƒ?Άm“vθ‡Γœ~“ΆnέzIϋθ£±ςπΐΓΔ€™ΖΰaŽYΜC;ό α!ndΘϋα°α,Oι|0‘ρ#ˆLΤ‡AΆ8˜$Ω³₯Fφ&ύI/a kDx ‘½‡£β?‘dξe___ΉWΩ²κΌW7mΪ,ΔΞδA‡~ωe©“€°eΟρ1αΒ… V©2¬ . Ι”5Ÿε}δˆI³7χ6[­w{τ’4άίž%,8(ρ 7Zl] σ’όψqλ₯ΛNBΜ–U§ή³gΟZ'NœΊsη-d­Z½ΖΊrω ?;ό₯έx!¬Y³FϊΊsη0Ο<{ sύS™Hƒδ›`οΏ‘™φίΊ† γ™Δ`!A&ο;w¬I3Y»vο[†™¬jU©ZΣΪ΅Λ!CΒ'Ÿ|jυλ7€ϋw‰1Όΐ„ΡA¬ώώϋo zρΜ4δωΏϊκk‹­δ8t†yγήSVώΡ+­ϋ‘F³ί’_zΊh­ΨyRʘ~ςγ—‘αΏY—;žΏό8Ž–`τ>g.γRΝ:pπ σsJΑΪ½{³>Ά ³η u—ϋ\­zmΖrƒ3 X₯³φμΩŠ Yl…w¦Ÿγσ†σ όώΪ±Cς²%WβΈ.[΅nk}ωεW’Ώeμα,‹s7ς΅Η_œixα˞«€΅ΪεšJž:›gdΔ3Χ07ϋzυIω₯K—Y^­Ϊ:uα`μΨρΦ°a#ΒΘ4’(ώ@Œψ σΓ„ύΎP†ΜyiαwK荨@όβ?Ώ.|ςΛ‘Ϋ]FlγsΡxώ4™?~jίΎŠΚg²Φ­Ϋρg£¨D‰βς‰Κ|VBY~Λ¦-šK^ξΊψ‹½ί§7-R„]-‚δ3ΏuӐ†ςgΞ+δηwž†̟x[Iψ?-$|’;pΘ—ΖŽB]:Ώ%zζΜύ‚nπ'Ζ³ηΞ[dθηŸ~_CΤcΪ λFPΒEΐΈ9δΛ—O\ͺTδΌw‚οSΙβE$Ξ?ώβNεS§Ξ<ξηxΈJe˚AΚ 3%ΉŸMAܟLϊθΧ_—³ϋΦqκΗχ8σ:β½_ά°J•,IpE([¦€Έ/ <ξλ<γΒΡ£G¨VϊμΗι&r<Γ²gΟ.iLn){ΆΜτεό―θ›oHέ Ω·wŸΎβ»[»vmœψ#½Ρ± y9C£Gφ₯nέΊΚLΛ—-‘)S?‘lΩ‹Rυκ%iτ¨αΌ―.z#³A;ο±[€ρ·F»ΰ#[§FYq-ƒŽ‡Rοή½(QL”9sfQ‹|ΧΨO– 3υ0H\ ’±ΟkΓFMΓDά‹<‹`ŸξΔΞf ΟθππOΐNy`‹<,Zφ{|/Qϋ’™©qΩΌG>œΒ‘η‘Ώ)ŠάQQ&T₯]»Άm€Ν^φ’kΧoΡG£Ρ[ovL Μ&>»¨ύΑμ)πΗ1ά/€I+u,λΥ«ΐΎΌ·DV°PqgY`Ÿžύ²=όᑃ©l^y₯™Θρπ‚9όxxy΅9.Όσn/žφ¨)½ρF&Θ~δξξNΉsη’*UͺΠW_KΦgΘ6ΔV*T¨αΗ³2c Κ ‘Tόΐ»ΉΉ9Ϋ ΊQH!`([ ω“&IJϋ–{Ω Δƒ―nžά9%dl1sζΗ΄g{χ;όCEJΈpΊ@Μ}|ŽΣχ‹~£υλς"X¬x1ϊhμ$z»kyΩέςΗ_βσlκΗΛ4HqϊτhΣ†΅\MΞ—JˆψΕ‹™Xήb³η€Mt•fb‚gκΔ³Ζ ΊΓσό•‹}•GΣ”)ΣΨίt”π™<oΔρΔ_Φ¨Fδν}„@hL»M?Μ^ό‘C#θWβ$‰Ε7"€‘ή Ώο¦±9žOΎ'N³λvΡ[B :vd― tγAd Τ£AƒJ›žq q xnn)X£!°€³U„?4%!OΫv#„_*ΰdΜY όM* ε•SzZnήΉΗφi‹R%5όΡωrδ~Ά­ω-™ο₯3½υΦ›δλ{‚ύΪ½(%?―ΫΆmΛύ? αςζΝ#ƒ:1ΨξμΩslˆq<Σαg`°\·nM˜ΰΐηΨΖ:@=»ΓπKΦ>JΓS^灝 — Ω³f8޲ϋη}„! Ϊ―MQ$2Ύ& ϊvο?ξ5… ‰“>ζ΄l όΛwίy›―WΒbYέiΧΞ]T ϊςλElΙY)=‹λB€Ξ’E=ι»?˜ΛπΘ‘CεGΈvνZτΑPώ”ΘΦŒΎΞΗΆΕυ—ωΊuλ8ί!ΐxzœΥ+—SΎ½%ν>ΟGš&Mj9ΖT>˜“Tƒ" <έσ‹;JƒΘa:ͺC‡2aJ?.^Β_p.Λ¬ 6 *UͺLC‡ηιQˆg hB]Ίv“ϋ;ί‹εΛ§„lA΅<#@"@”V­ZΓϘQ’ŒOϐ'NœˆΎ˜7ΦςΛtεΚ•y‹#4hπ\OL<ό„¬W«ZUΚ|ύΥ<1b”’ /³x*5oώͺ€u§;“ηiτςΛ-(OM—+WN9bΈ€Υ―_Άρ,^­ΪP[½ρœ6μ]!UύψΩλ½ΎLΔsΣΙSˆ ΘBΌp¬΅hsΕς₯(!=ΰR6“§‰kΥΊ=eʘž?ϋ§§Ÿ–,·’ εK39tΜ‹c£Ζ/ϋ©²1βeZΎό7κάε™Sωβ…KlΥ!Δ?] *U(Ζ€iο`Ω΄΄!w¦TTTΐ½_“₯,¦οΓΛΤ€ƒyφ•…l9Λ_?»Œ<ΎΗOΠ‚…ίQ»vmΩJŸX[Iΰ ¬ΒΩ³e5QΩw{»«ω°Χ”γεζ]ΎήzφμC΅Ων§b…ςlͺ) Έ‰δΝ“‡Ώ’l >}ˆΥΌIγ†<φ\ηW†0•hDPž ~ΨD*`°ΐΣΜb -½ΪˆξuλΧ[οΌΫΣΒhp3xfνΪ΅2"œί%›πTC<Ψe­υFΗN΁ ¦q&Fη+ΰ)ƒfLφ§y F@Fτς4<|œΨΒΐ 0xƒRψ3›δΓΐZ·i'ƒlΗ`ΏοΎsŒZ7uš|ΊW’‚@dΤρ’•ΕZτΫsO1Yζ™ όΓ h3iΘk\‹ϋω Ο‹ΐΆ\CŸ)Ά$$ˆg€©Γԏ™L0iˆ£~ϋσކY*n‚= ³[ΨΣL&ο2S„=)@Ÿ½Ÿvύ˜eϊΜμM~ζ¨Στ­ΖΔlΚ™|LΎδ9iβfoτ.ήzΤκ0k“Λώ^ΘλV°c@΅Ιw™ζΡ eΦρσŽ™FΜLa FCηƒ­1Λƒ ¦ ˆγΪg;±§Ήb‰όΓ€ žΚˆςδ)-Ÿ;ρ™1oή<ό©΄ρΘb°—‹NovfŸ­ΝςV} ρˆfβQνς©Κψqαν<ΌzQFƒ" <°šΑΟΣX#qοαk¬ΙpC@& ya©3 xΐˆόΠgŽE`Ϋ@.ι‘O[’θ†ί­©ΓԏΟάՏ/[mΓςΥh_xiιΣ§ “fςΐ] ~£πW†ΎΗ΄ΡήOΣf”Γ˜ΤmdΨ#˜όFΜQ'Κ‘ο˜ŸΪ ƒyM9ΔπήŽz—ΟOγŒε₯ΨΌA‰Ζ§IY»!_Λ „Μ¬ž‡ dM+υ›―…H‹€~α’4#GΫMή§ι‡)cτ ndz\εˆ›rΟ^Ώ£–δμO<­ͺN}†ψ‚βimκnQΩέζ{όθ9ξΠ=[S—ιβFfφ‘MΡFΐKΩΎύ{δ· q”·λ…,<ݐ?KO—‘™½iƒ«~€ΫϋηšqE@ˆρψ{Ό9#T?fβ>διY0ŒE(’κ‘Σί$b?T«±y»–t&Οx36‚3gΡYΏ³2½[Dz‘ΗδηO2Βƒι@œM=Ψ›‡F cΠ†±I&ήΐςŒφ€£-Π‹Άΐ‚Œ²ζέδΧ½"UžφŠj}Z^ˆn Žn½ͺOP ³ γ”λ§UCŽ‚=S)7‘>ύΤ1rιv"ŒΈ †ψbO΄ψC@~ΘLy|ς4ΑΘ°7„iφvΪΫhΚι^PE€Ÿ―q|υ4ε8Π]ν’" D€@Œω GP_±!΅ ₯υκΧᩝNΘ‚!†Θ†Ιl‹ΨI0ςšόFެζΨ€ΫeH³ΛmͺυPPE # δ8Ÿ|νΊ"`C Ζ,ΘΆ:"<4DVΰ¦MšH>Cv#,dK0$Ψ&ϊΧaDy"’K E@P »‘ΏgΏi‘Mw-ϋ€rΑQ9»ά^Φ΅^{šύψΡJΗT―φ4ΫΣν:νrΧ2εs}9rm»kΊ«^{~{&ί“M>ϋύpΥυ8=υΫU‡½=+AF“pΜΙΦώIR©" (Š€"`ΐo¦λο₯‘!9FŒ q(={ΊΙχ8½&λήθΆλƒ ΑUŸkوβF§+)5r³(έUξZΟ“Κ›t{Ÿ\uΨγαε72δ3Ηv}FfΧcŽMšι‡‰›}DzL~£ΗΎ7eν2=ށ;AFӞυŠΈ[š’(Š€"ΐ>‚0DΆί†0˜=Ο#L~8œZ΅ς’Ε? ‘„>“Ηθ~\½K3εNμ]Ϋώ€ςΘςΤ)^ϋ‘‰Ο³C• lΌ‰i#1 κ‘#ή”„gŒ*]ͺ΄L…gτ‚Daωm^3€gs*(«0švωπjXE0Q’ΔT‚WμΓlSF―Ιcί›4 |ί³g/―0x‹ *Δ‹~ε—lc„Ur±²©§ ΣΊ»°«rΨΌgο^Ίtρ’,»]²dIΑΚ€cP=V1Όrε2OΉ—R–Η”y(·Ÿ§ΕRυθ+Ζα\:¬«―šθ!ΛΙca°½ϋώ­Σb†,;Ώ{χnΑ*gΜΌΰL i£©ί4ΨΔ±’ζ^nο}^²hΡ’<JgŸqN.\ΈHy‘LPq7™ςϊ ί½g―ΐ™Uϊoς?ž8@XΜΜΣΓS¦ΆEY¬–yθΠ!ξη£ιΡτ aeP£ΓΤ₯ϋˆψΏϊ Gά,MQE@P"F?τζΗήLğ&„W.1―€gδvkœ‘™:ǟiκ /-ΌφΈζ³—Ερ΄iŸ0:(E€NϊίΊ•ςηΛGΫΆm§ΕK~ς„"MζeΥσζΝC+yeΙΙO#^ K–Ν6m>ΞΨΛ« U¬XAΖώ8΄a ΤBLn7oήJί°˜2gσΰ5 |œύ4ωΜύ€N^θDVΌ>b ­[·ά `›%›―ο ~αx“W όTγO›φ™Τϋ9―όgp0ϊΜ:ΡοιΣ?£ςΌ/V©,]Ί΄,ωδΣpυƒσ’?ΤΊu;ΒμZX ΈT©’²Ά‚©Ÿ/DάΔ/\Έ@΅jΧ§Y³ηΠeΛ „ϊΠαΓV΄g?ZΌΔ`₯Ε?ώΨ&­Z΅Zځt{Δ―]»Ζ«q§reΛΝ;ε ΐΩ³g§U«ΧΠΊ΅(_ΎΌ|l“tΌ€Lœ4•ΖŽ› “?žΚλCLΌχο? y"Β\u+Rίx"΅’^€”i&E " χP<ιΪεA€Ι“SoPPP˜ΥqŸΉXˆόaoV›Γ1“­|ΗVIηJ…&/["eE;&0R/ΚbECΤmd¨«ΪWi4y°7ωpΜKdˊ’F/dhgΉ U¬U«W#v%?Žc…Dώ·V¬\)ιΨLœ4Ικήύ]‰£|ΑΒΕ¬E‹Ύ—8/ˆeuμτ–5uΪ'g‹«”GzΝZu­Ώή)ςS§«Ρώ±m›Δ±™ε—VΣ¦/;1²·ι&Ξ‹rYMš4c<«7ώϊλr‹d°‘MV.ΘΜκΥk(«ζ"Νθΐ1‚ΑbΧ]ΞώωGδǏϋJόΰΑCίΈq“Δq˜Κ*»+V8pΑ*„8%hCΩς•,ΆnK>sNwνΪ~&΅’o䏭-ΌœνΩΉΣΡ???IvΑλεWš;―§aΓFXύϊΰόŽλ`ΞάΉV½ϊ€<ϊT½FmkφμΟ₯,6τuκΦΘ‹Υ‰RX'NΆ:v|ΛϊωηŸ%0̘5Ÿ5oή|‰cσΝ7ίςy¬η\±yπ‡λaξά/¬ζ-Z:―UWΌ%“nΒEop‘ <2KMGJ™fRβ zΕΑ“]Ž1πCΏπ»EV‘’₯­Ϊuκ[-½Z[†Lu§‡υŁ|ƒ`͟₯΄eƒΠ >ΒͺTΉ:U³vμΨ!i νΪΏΑδΕAωΐσΡX‹-°V«Vm…Μ‘Ά,Zuκ6° *f-\ψ“Dy9b½ΤμUI+R΄”΅`ΑΒ0АΨΙLΐ<‹”΄Κ”©h½Φ²΅β…τ^½z[E‹•β>Υ³ό/r ‚όϋο[Γ,s½tΩ2«AΓ&‡}ϋφ3ΉJf©tΆ4[©εtΆ†Zϋφν“γ²e+ZύεθϋζΝ[¬2G0ν}5J6ηΉgSjΫΆ Α£VF}(ΗvΨ4xπ φξqMβΠVƒ—DθOΘΰ’€O°Af C”_3)Š@@pο¨@LτHxYš5oaύπÏRχ10“°–―X!ζ°σ„‰žc>ωΤb ²γε ,β΄‘Ν—kΫ6‡νlΝZu¬΅kΆ Θ»‡Ν&Fƒ-kΗ%ϊηŸ—Zι2d΅Μy1y±=Ε6ΟΈξμΨΫΣu?δ„RƒlΙήΉŠ1pΝ₯(ςVΟ"#‘χ^Šΐ« €g΄duj>ϊ¨”˜2@‹ωΫΪuμ†kύ0?^Vg=qβ$1a7_·nί¦8lf‘M>iԈΌ2xρηόΜ΄lωrϊλΟ-μ·6«4λ!»3Ο7|:gJκlnF//*˜7›Έϋϊμ³&Œ―ΎϊšκΧ―+m©XΉ5gΝ`•*•YK˜XΚM Sο@›g½πUΛ6Κ4vΜxJ˜0Ύσ“>ΪΙΆΣ”5kVΡNš²F֏ ˆFrΓ†βbνΒ… )rdJ›& ϋΞ@Ÿ~Ϊ”FŽΕΰήtυκ5*_Ύ<-]ϊ‹ΤΛ„Τ©MΌqγΧωwr²ίγ!C†_|ΞroS£FpΟ6JLG0vΝ΅i9ŸތJ—ώH\ΒΑ½>ύ3™s δ…F»rεiέΊ₯’ιΆ·}2ύB^ƒ}ρβΕ¨I“O©V­š„}˜&,ύy1Λv˜π €hŽ™ΔŠ/ησŒAΗNέ¨ύ—_ˆfί΄ 2―\σsΚΕ1‚;ω?/aωγΖJ:|AgΙ’™† AI’$aŽγτόΩsJš4‰hβ½½³Sίή_ –ΐeΘΰΎT―~ΪώΧ&Ρθ~ΛΪύΞ]ΊŠ >œ0a29s–fΜΖ>›cΘWˆJ_ΣH‡ΉJΟ―ΊώΓTε‰Ώ?Ÿ#c61\OΥͺU₯_–-#Φ:σ€AŠΐΎ°qӗν:R«VmΈŽ˜blωΕƒβΕ‹ϋβ<’6 !A  ?gϋγ|AΐχcHΔkE@n@)=“σηνϋlπ α5HΝwίM±0§hέΊ-ύψΓjήΌ©˜X g° Œ:vRʞ„<ΰ%6‡w&)ŽGcΚ”)( /˜’ΩΣ§M¦ξ=zŠ•«ι†ίMͺQ£ }πΑBlΊtκΐ„ΉΝδΟίǎŸ’Ω³ηPφc‹ €ψΉoΫ¦΅nӎΨϋ,ŸmŸ‹ yΒψ²Υ†Σ5nʟΟ/±½σ°„έ–μ•kΦϊMš<™?£ϋIdry]4΄f2ςƒ„€€βΟΎ4S/Άl’ D“μe0i<Ψ§`sʞ5d’ڍrφv ½3`[m¨ρؚφ˜t”7qΐštΨcς ‚I3ω₯\7&‚™΄G\οEΦΞƎ‹΅Ϊ έ–3ε±5小Ε5Ψ€A섁ŸIC=xA@?Cξέ»OlJΐvΙIxΉuσζMΡ’Γ–«ώ!ΨΣ±¬Q·;;\wς!C»,Δ›kρ†p‰•QVΡ3i&Ÿγœ\v€ΰ“nΆ&Ά&@Ρ~ΤΨΉQΆ§£Œ=Ό,ώvοΪ~( ςQώδ“ΡνEφ§ύUB‹ΗC‡|™ {)A-xš_pAΐNPLβ\‰IΓœ¦‡ŒΦaΚ›­ΙcΧ6Ήζ3ωΝ6Έt“/°­»ςξβ\Λ»Λγ.Ξ]9ΔΩϋ’rr\]e؏±`―Ηφ<8*Έζu=ͺ¬kš»²φ8ϋΎ)λ.Ξ€ιφΏG Τ―s8‘E τθ½zΜ΄„"†™ϋ Ηφ8³ς†ˆ@©6rψPJ ΎˆΕK«½\`uΩγ\wυ"ΝΔ£ŒΙk/o7νr—nδ&Γ΅ŒύΨ”±Λ0qφ|ϋΘcΚ Η!-‡ό¦lHΛ‘LPΑ]{L~Σ.S'βC[oPςM=!έΊΆΗ΅-Α₯‡΄Νχο!j‚όο5MkRE@P‚Fΐ{.Χ8ϋ1όμ"€XΉϋδn—ΤΎ]¦=_`ρφBGŽψΊM I$κΩ½{]½zUˆ8Žέ…ΐβ‘i₯•ζSE@PE@x;‚ h@€ ΘO{χξ€μζ·nέ’,Y2Σ€I“%¨ΑkνΩsη(Ožά4wήόΐ²JΌΡb›r&3ŽνZn“nΆξLYέ*Š€" (Š€" Ό½„9A6PEŒQvW¬\%$ζ†|ξάΉK'N$q?ώ‡½²ΏΏ?=}ϊԈϋΗvΫΆmT°Π‡΄yΣςσσsj‘QΚ"$ƒ΄dμ™:ƒ“¨<„₯KG Τ οάΉC\ŸΎωf εΘα-€ΦlXκ©S§ΣΑƒhΑΒE΄sΧ.*[¦ŒΫΫ·oΣW=Ώ¦bŊъε+鈯―Τ=hΰJ•*%~β²–9KΆ\,οεΝ›GL@:wνAmΫϚηfԁ Ά§gŠdέYXw7χΫ\m¦" (Š€"πŸ!n“τΐ}a+Œ‡rγƍhρOKhΩ²eΤͺu[J˜0!»{ΘΪάΘB:Ρϋ²eΛΠ–ί·ζΝ›ι£R²Mςϋ ˆ62Ψ¬β>uνΪ™Κ—/K΅λΤ’ΡcƜ9σe2΄Κ;ΆΙynJ~”Ώqγ&νΫ»ƒβΖ‰Γ‚ˆάΝq~’ސnŸƒG„(γΈ|ωrτη›ιςε£'NljΩͺm£ΑF Š€" (Š€" (o'αFabaΌPȟŸŽ=&f Τ$‘fΌI "KζΜ‘ΙlΩ²u¨NνΙ„>hM0DyξΌθ«ξx^Q*Q’8*XκΧ«GK~ZHΎυμΩ‹Ν8v‹§Šτιίέ―ί^Œδ(₯ϐžFŽ*eόŸψSω •Ωζ8;΅nσ%>}–σ3 d™ί²ωwΦLΧ{δΔ‰ӈαίJ=¦"HE@PE@PήJήc"ίOpΫζγs”ψπOΑ‘όϟ=§ΘlF€j@h#GvK:“g˜D"<~ΒD:ξΌx“p'εAΈΝD;{ž§OŸ‘Ί>k€«Σμο§‹k6?Ώ’ 6θ<ωΝiω²₯"ηή½{œ_DΩ 0Μ.ΠF€™φΩλΣ}E ΄„φ ­|Ν―(Š€" (aƒ@ΈiαN &€drŒ8{ϊεΛ—™KcΖ8Μ"n'«8F™,Dz±0 ί²™νŸύ…Ψ&L蘌b ’όœ‰ϊ¦λΕΤ"f̘ Η¦^Lά3ΑΔ™cέ*Š€" (Š€" Ό½„› rH ρDq.]¦A»‹C^†YΡ£G§½ϋv‹yd›x£†{Ή½ϋφΘ±»t“ε\ΛJύQE@PE@x«7‹’jH)ΚΈ‡TŽζS^wΤΔβu?CΪ>E@PEΐΐͺAF@ŽAŠaV䲂 Α₯UVΣE@PE@Pήn7κύϋ ’lΧ"ΏjΥfB^`r‚K¬œΖ+Š€" (Š€" Όύόηδ·bν‘" (Š€" (Šΐ›„€δ7ιli[E@PE@PΒ%Θα±V (Š€" (Š€"π&! ωM:[ΪVE@PE@PpG@ rΈC¬(Š€" (Š€" ΌI(A~“Ξ–ΆUPE Ήν„ΫPΧ€ΈΧ)ή΅}z¬(―‘&ΘaιŽνυA[’„?zο„?ΖZΓ»ƒ€!ΖpΫi'Ύ&χ›k<βπ‡<†(c?$ρ@e\σ›x€Ω叴#όσαί¨UλPήBιΩ’§OŸR€H‘œƒΛΫƒφB_πΰĽÏΨπ­H₯+οƟύ({φμkTΔ?zτˆNœ8IY³f λΦ-Ίrυ*eτςr’„όW9ξώύ”6mšρηϟyΙ“'—xC€!;NœΨ”0aBg(@‰%€(Q’Π₯K—θΐƒ’Ξ’%3₯J•šξή½K7oή 7 ™N–,Χ™•Ξ1‘~ψΰΧω•,Y‚<<<(oήξ+φ!ōOβ=ΩταςΥkδθ1U©Z‘bƌ)Ο3Ο”ž¬A>Ηϋ©P‘’ύΕ½šΒ3Ν›ΏΏœ>£:΅k΄ΎΡ ςΧ―ί$u£,ž‡±cΗ’Δ‰ΡNΦRnj“Ν,&±cΗ‘D‰Ž]»)βΉ¬1»xUŒ&xλα ΤzΙ~Šρ=:ρθ-“‘jJ/šδ€Q"Πό­ΗιχcWiΛΝGΤ m\κU+―αgόEkξ¦ΓΤeσ)Κ5"Αl\͜”'CR»b/Ε‹…>+ΓδŸ+j=} εJZ”σ–f1}+ύ/gr*›/‘±“Ο@IDAT|3DϊUϋ€εΧ PdΦ}αΤ (‘F@οPC¦@8͚bh.\DΥ«W£sηΞQ¦LιβΕ‹Τ‘ύ—΄nύz1wΐ1Hν;w¨[—ŽL¨‰ΦχΚ•«-Z4&Ζ©GΞb"αιιI7nά ¨Q£Q2&ΰ=Ίw‚?~|&έχΕΌ"3ΧΡ΄icJΐq˜°ӎK—.SΡ’E„\ηΜ•“nψݐv£n‰ζΑšcŽΏqS4―jbaΘρ½GOθΓ9»hQ₯ΜLŠ3ΠΊ½g¨Σ_ϊ(g*Š9mπ{HάΊO>)ΔZβ§”aΤFΚΏγ$Υ+‘…Φμ9MŸ,φ‘ΏΪ~@ιSΔ§Ÿ·§Όγ·ΝA•)_Ϊ„TΕa!ΘWn= )'nG·Q3&ΜΠ6O;w‹Ϊ—sΨwλ; —¨&Όα„š ΏαύΥζ+Š€" ΌTb;bG°˜„uφ¨V­šBf1QH‘%/§΅k'γ]cΫα΄T @~g|Ν5θμΩ³bŸl&ϋ!±J•*tμΨ1ΦG€tιή—όργΗγύt΄gΟΦ'– y#‡·όA‹ ’Bxbό%J0^„…ΑX@DŠ@'/ή’ ~w©TΞΤ΄—ξ>ΌO°‹hX"3%‹C⦕|Ÿ\Ί#ϋίu†ζΤΘLω½’Ιρ§e₯.ΫΞΠή£)G†dtαΑ>ΊΓόψ9?j═Ξέ{Lη―ί₯;“gΤHδ•Μ‘™+Έ4BΧ%Θ―ΡΙΠ¦(Š€" „ P3cμ#ΰϋ1cΖrμ3ό™²Θ-pͺT©€<φ !φ3dΘπx”5vΔφόˆ/ΜΆΚFΎΩw/_ς‡›ΝuE™|›¦AΛPΚα([ŒΘ4’Bf*+΄!U΄HlƒΡYKLή‚ΙΒ³ϋΦ#ϊ"Ύƒ8ΓFsŒ*$I·ΩV9^Œ¨T0:svρ£zyR™kwιΰιkτWά4}ŠΜčP‚μ„Wwή2” Ώe'T»£ό€(ΰAιϊ° ,>,Ϋ2‚?{pΧ{Ί}ί”um»=ΟΏ±¬ ‘ιO`2Β#¨ ~ΌfΗ§qαL]v’ŠΊo Cvνρf[δ1Χ€=`ρFΎ)+BψρξχΐβMΉ—έ[x–ςbσˆ™-ŠΡ8Φφn:pŽΚ|·“χˆC Ω΅Ϋ9&Γφ`Ž0΅.wάhtξk™9`²~ΏzŸκz;hA³μIhύώsδsύ5(–‰β°MςˆMΗɟeΆ)”ΕΨ>™ρ Ηs,•θ"π! SP#ΰ΅ZEΰmBdΒNLί‹7ιa±E½¨Ηώη-ξκ1B^όύΧΑή~ΧύΧ‘}v|@~A¨€ά‡a’–i#β%ν_‚XΉ΄%°xΣN{Δ…E<κt''°x{B³oύ…›(r•τΫGcoIaJ‰ϋΞΨΛ9bοv§9(wΧ™TΥΈ`jjπΣaΪyμ2έΌ˜f=D§x"_φτI%=gϊ$Τsγς`M1ΘvzΟψτΗ…Ϋ΄ψό-ʜ: ηΠ΄]σ*o ͺA~SΞ”ΆSxΝ0δΎa<$‹2Ď[΄q Π¦Α'-Nΐ$)σPG7ά‘ˆΰΊgΚ»–…[―«WΠ{LNή ₯{_&`™6BΆky“†Ε#~ψαGΪ»o?υλΫ[lKMšΩšΆΉΚ0ρξδ#n.»˘1»όΚλV»hΚ?{ώŒφ±K1'Bμ"FŒ(ωE«Μ}ΚΜ~x Ά(cΗ ΠŒ?ͺU­Bi€qβodcT»C’ndAc8nυ*‘59eO•€ψ?•ΟπwψSΊδρ(u’XBšu‚b1a+“+΅ΫφyΊ}9ΜωO•0&Ν)žŽκ-=HΉΧϊ–ϋOhAνμδΕηββΝϋT>žΓu›©Εƒ'ξ%‰Ea†1³ΖC*?w'eρˆDOψάξlQˆ<Δ”τŒž ¨˜gLͺ”ΥA˜γ° ΉΖ™ѝ‡O)»‹CΰΫ\ƒ"πΦ"ΐζD<ϊ† ΐՍΟQΚ’Ε‹m•ώΆi AQΝ’(ŒΐΫvaθΐƒϊργΗT©R5*[4uκΨAϊ‰1bσζΝμf«9sΖiγi.SΦΏΚvĈ‘Τ‘λ­[―‘xΨ³η“u/š0~œΈΫrχΩΫ {h¬οgS―>iώά”3G^|Βρπ··Ι΅½φςiφr ?ω”jΧͺA•+WvF»ΛOmΫΆ£³μš,"ύεΛ¦‚…Š’§gržΧc“Οlέ₯›UΨΆ½œŸΪπJgX‚8^Όx"DδrϞ½4tp*\Θ1Α ν4ŸΔαςΛήή›ά―HάV΄ΑNόεβƍkΊΖ‹OΔεΥΣ‘_Ώ.εμ}7ΡΧργΗrωˆμα.•ό¨,όΡBxΡFγμώύϋόBβΟ}ˆλμd+žί‰Θ10vpcΜπ\Π6ƒ³ι?κ.ΐ'{_@~{Ώ ί¬Β6rύQ^ό}ŠΒkΝγ4εΠuΊ·œΰΩλ}¨Β’δ2¦L@RΖ¦u{ΞPέβ™!BC8!€s½JΰΑœ;œ³(‘&ΒH³/μ!š1)Y&ϋΩΛb!¨ςŽϊ«Ό½θkΰΫ{n΅gŠ@Έ#Bˆ2\_MaBΪ©s7&f–ψ¦­Z­"•+WVΪ°m۟T¦lEjΠΰϊΰΓβ΄lΩr‰‰kΡ’mηεMΨΉs'5ω¬Ήž={Ž>oρM˜0‘βπJg7m’xΤiΒs֘’ w0Ehόι'΄δηεB?oή|Κ_ ύ―v]ͺσq}ρ™‹Όύϊ`mνjš>}Υ«Χ€@@Χ­[O:v¦.]»Sκχ½Ω5ΨU&φw¨U«6T΅j *τOˆb“NΘρνΘω+UFε+T‘Ύ}ϋΙ’Δ¨7Z΄¨Ό$2›np=•ΉlΡb₯δiφ>ΰΔ•ΕΙβ>vYcήI“&ΣEJrϋλR}ΖρΔ‰“(&rσβΐα―ΏΆS–¬ΉθδΙSrΌrε*ΡFΧεώU¨XYΜa€>uμΤ™&MžΒx7c―%©kΧn„—C’‘Ο΄σ"OθZrε>eIνX#'ΫͺνζΠ#_’8Ρψ8&nαΈVφd4sορ˜yΒ9W,·b3b/‹Φ…΄,ςjPήzxπ Q`­Š΅Ώ…­E@=oλ=ΔΔLΐΈsηU²T«wοΎVΚΤ^Ϋ%KόΙ“§πάΆ–.]j1ω²ώψc›οάΉK *jmΪ΄Iφρ³yσ+oήΒr|ꔣμ7ƒ‡XlkΜεoKΌ©C†|k±ΩπαC«C‡ŽVη.έ$Ž ―Τwόψ ‹5₯ΦΈqγ­j5jYύύ-ΦJ[Ν?š={ŽΕΆΤ’ηŸ—Jώ5k~³x‘)ΣΎ}G«_Ώώka­σηΟ[yς²-Z,ωGŽm5mφΉΕδΪΊtω²U΅ZMιΏξΥNJ'…΅wο^λ6ΧΕζ VΡb%ݎ£¦O¨#kΆ\/,ςρΓŸΫΟ:rδˆΕšjkψπ‘VŽ\ω₯N\Wήήy,_ί£‚9°Ζ{χν“Ύμγ-zkΑ‚…Vκ΄™€―¨'b”DVΧnέ­Λ—―0Ύ¬MΏXs/2ž=wœ_^±ΝŠΣ{™ΕŸδ%ήώ³Υη/·ΆΠZϊΧ gτΆ#­H½–Y_δ!Ζ™;Š€" ΌΞ„›‰wΪ©y0ox;}“ήP‘‘1š)Σέ*Š@@pOγ^‰+& <ˆςεΛK3gΞ’I{ΘΉoί^ͺWΏ‘,Ό€γB… ˜1γhυκ_)OžάΌ"YάγΒ{<²ΐξ$ICŸ5iΒK ;fΞ#ή>Žΐ’wΏ!tΰΐ!Ξo‘ŸŸkAοpΎCVZΐ+­goάΈqD;Z€H1Η8uj eτς’θΌ°C<1₯@~±ώ²=•.ύιΒ… 4rδpΡΜBΓμΑ«’΅nυ9kΜ§Q͚5€άΎύιΔΙ“δ•Α‹~^²HΚαη4yΒP^D"‡ΔU©R™ΛΝshΔ1NΪϋβ(PΫ ‹1c'²f{1OψΛ(YZΆlA]Ύϊ†9Byrη&Ο”ΙiΥͺΥ΄pΡOΔ/‚+2nΪΈ‰:uκBI“&•βpnΜ:B‡¦œ9s3GΤ¨aJ’$±ΘΕyσᴚ|δϋpΈIωSB6­ˆ·˜8‹½τώSΧ胱[iE“ΌT!oZgŸb±=μSΎ6ξ<τ RHE@xC‚lύόόΐsϋ@x=Ρr> ^Οζi«Χs―{{g§\Ήσ ω2»ΜKϊbς‚±§Εjcσζύ &‘"EβqΑδcΒm9Μ,6ΧH—&Ϋΰ΄―ό;7&=‘šΥ*Π§lV•@BYƒJ)yΩ`˜A€πb2ώ›qΐΦ6ΑuκΤ£glkŒ_²OŸ:\_αd?>Ϋ)›ΰΗΆΕ‰˜€Oœ8™ry‡[³”ŸΙ&ςΦ«ϋ1YΌ­R΅6έΈuΎιߝ2鄝ς£Η(A‚ψF”α8qb;M?œ Aμ m6φΩ₯Šε’’ΟΉνΪ΅₯ΘΡ3ΩM"οΖΝ›B˜Y›.XxπΛ@Ής•ΨŒ#2·έ’,Ω3Ά―(„tœ#wηs³Μ©ΒyBΨ}ό 5Θ™DΘ1Ž‘ρÁυΠ²u”tΔ½ˆΠ" (―-aN Ύvν:Ϋΐd-΄KEe[<Ψ)ΖbmΙT°`°r`ϋ iޠ䕆ͺμu˜ͺ‘)βO–”9s&yhΨσΨχM~wqF6Άξϊ‡2₯IΈΚ ¬Œ=ήΟουκέ‡ΪΆi%'#ÞΗΘ·oƒJ*Ν.CχίmpD`[Z3’2aΫ²εw94“ΝŽρεI}ιΔΞφαΓGξ'OŸ™“Μ§Μ΅gδΉnAsηΞΕː€‘#GΡΨqγD“ qxΡmPΏ“βΪBhq/>xπ’DuΓΘQ"¨ι† C`ΌΈρθΪ•³Τ»χΧ”&Γmϊ⍀‰p͚5₯Ο>kBǏŸ š΅>–Iˆ8~ο½2ώIFω A{δ…Π~΄ΆΑHΡ_·y θΠ΄?qš_ ώ`OC‡)_bδ{xΫbΗ’˜ό2PΆliκΦ­«s,Αd<ΰrώΦ~c‘!°¬#p›έ…Hμαφφ‚ΐγ9q–ό—*δΙ™Ž•›Ή.ω’‹wYs‹#bG3b5jΤT&¨`m{y±VϋψC0eεΰŏIΓα¨Qchώ’­Š=9vfί]škYslΚ`b>‚l"ΨΫaφM^΄eΠ7CLš1iφΊMΆψ3i¦nw[ΧΊμe ηΝ£Ω4?Eγ`PeM[μuΪe›x“Ο€™c“[EΐkΧόάΛΉrε€ysgΛΔβAΠgσP6Ÿ8QfΘγˆόζε †Χ]cΩTy@#O‚ h0Ϋ‚z±}%κ3ωΰbΛΤyςp}ρIi&i¦­Θƒ: U7εApEΧGžάήN·dΈS§NMlΫFΓ†-[Ιξί~‘Ε3PΎE‹ΟιμΩσLάͺ|/^ŒR³¦!RΔH”Γ;kΧμ•cΏp»†ϊ`bπ㏠ΨλΓ!…₯K—¦οgΟαΊPmφb1zτXκά©½xΕ@‰%’¨Qώv;ηέƒέ³ΕA’σ€»Έ»μ~­zυZlw\›žςΈS„ϊυκΞ»Xs\‡Κ—―L03m2BbΆ›ŽbsilήΩ²Θ’&’ΑΝ0,˜?EzaV‚,7aΜιC:uΩλΗNZΌhΎψlΖύY n€~»‹ΫΌωβΙ†βrCš6kI΅Ω¬€cΗΤ΅KGJ–,Ϊ§T¨@n§· ΤγαΝΩw#˜{>uβΨδ=2Όΰ'ρfΘάqς:Mέs‘ύ"₯‡$ΣFί«τEφ€l–αχ±a‹q ξΒΛ¦YΙEzP²MωΐΆrƒ’\yxŠΑu―AP0_(7+ΩFžΐ7΄ζΧ2ΐ"ξ(k?0Ιδ&ΫŁ vf—PG|}™τ^§&ŸΦ§v_~)Ÿ1 oΰς(³zυZ~Μ£ωσ`‡ώσ)mš””/^4h M™<• λcΆ³»EkΧ[Δ©“'8νςaBήMκΊ|ω*O κΟ°ςB ϋφλ/Ÿ<$υ̜1Mκ¦ύ§OŸfί’εθΟmd‚Λ—ώp­!6ΕxΑΗΛ/\ΰΩ±·Χƒ|}G‚iΓ’ί}iΩΑK4«Eq‰ΗΟΦ*ϋ39ŽΑ~xMΎk·PβΑkιXΫ"”>Ym]ΊtΩbM’5œλCΈtι’•7_a ξ€Π7ΘβU±Ψ-Τ€ΏέBε+d-\ΈHς/_±RΪLh'°@Χ―ί ιί&Η[·ώΑξ—.[όYμUϊιλλλLG­B…‹XΌb™ΕEΑ']†,–Ο‹Ύ²«Φmڊ *6Η°κΤ­Οξ§zK]»vνYΣ§Ο°XΓlmίΎ]ŽωE@ .jΝΰφΈͺ*Yͺ,csZ Vr ?Α"{(XAo`ΧkΕ~lߏAΎkΗΑΥλ.Ώ‰3[» wqφτΠξCž«LΧc#Σ5>ΈcS.¨-W/α>»lk8y“uπτ596.ΰLYSל >Φ„•ϋ$ΪΔ™³ ›t<ε1žΏξΙx/‰!ψΑύ±mΎΟΟ1ΰώ&ΨΫ‰> .Χgςβyƒ4„+VX]»v—}ύQWG  Κ% ·εRΧnΪALrY‰ΞŸΏΐvΘ ι‡~”I-Ÿπ>bf‘5?:wηύ(Œ?ύ΅ύ²ΣΝβ υΐ§ΟψΌ“ΡŠπΰ@μΣTV›BžJ•+QΏ#δS#ά=φƟqσˆf'idςy dΚ”Iβκ/“{P6Έpις5κί―7₯αΟΕ­Z}Αφ§΅gωόIΪ'˜6`Β~vc5–έBm³Έ…jυEsωΜ[½FuZςΣ~Ϋ_ΐŸŽ½E°>l4γα˜Ψ-(\Ψ±²2υμΩƒqy.Z h­+T¬"«’!Mά_1­M–,©h½ 9ŠΗZ,LμΑdΙΙ“'ςrΏgΩΣ1»½Λʚ5+}Υ£»L¨Jζ™žκΧ―Ο»Θ<±)±W)٘θsŠ΅Ϋ°sφζφ[»Z4H ©–y5ΌΫ˜k…‡,Βγΐμ#Νμ‡%ZF¦½nzL~w2‚J ©όΰς…¦“ΧΘΔ±i7β\ΣMΎ ΆFٝ퉧φ‘SžΡ γ¬BkdΧ,œΑf{ό·¦6¨:Bšf˜Ί 4˜Ά±M;.«rόe¬};|…Œ-Ο•ρΌΈΜβE?³ηΌJbϊšΗ=ŒΥΠ¦σK?Mš2%ŒΗγgbΫ1‰!{~έK–H_±|₯|αΔdoΎ(^QΠΖ~™ΐ_&ΛIσρ…‘Ό΄x%η<ηπžUfϞΛ+K¦€―ΏώJΎFβYΤΐ@vΡ·F0­\©uθΠN< `ςκ„ hϊŒΩμZ0₯cΫφ‘ίvL‰―΄νΪw’gNΥ*•(›Η˜―!ΕRσ)Š@ΰ„« ς³‡Wθ76}ψyι2bΝ+ύώϋοτΏΥ’Φ`΅§J—ηΙ3υ©Gž΄hρRω€ŠD (ψ„hφ±ε·gώu|²Δ1μAJMΰ‹”$q|Ι‹O…χn§>}ϋΛ ©s±GΖg[|ςΔ*WΩ²θ> γσ©=˜‡κΒ>>W"άd“Š„I<Ε-TΫ/;°iΔ—ŒΓz!λO9ΟΉsΔyQƒgξάμ“TϊθψΜi>ӚΈ5kΦπ dEι“ΖM¨W―>΄rΕ/ςβpQί±ώώŽϊΡ–GόΐK > σ’βηΥ<$γς¬όΤοg’4ΜXOε™”Ϋΰxˆϋ1’;ΙϊwS'Ι G¦l)[φ<ΔZ 9?¨Wƒ"ZpmšϋΖ΅l`ρω^φ8¨ΊC*3(A₯…T~pω^ΆSΫW ΒX8ς s#ΛUͺ™˜gΓc‹Υο°)ΪΪίVΣ’% iοήύό·Oͺ‚ύωš5λxEΗω΄”ύRΓΣ ”0K–όΜ“.ΫӏσΏ§?Ξσ²κ5>{u˜΅όϊλzY ²ΗWέhΩ/KDρΠ‘Cg)‹ρ΄X‰ Τ¬iZΎμgϊ~ΦwT‘By6ς‘tΰΊ ?w> ̘‹DΜM™Κγi¦Μωp^Μ Η3Ύ|鲘"ώΊz…x~β―y"‹Ώz²’eυKΩ„p5₯M“Fμψ‘^’DqκΧ·­^΅Œ=Έδ§νΔZ λ" Ό2α¦A†Ά³t™ τνoώρ@„- ΄£ΰjΧώŸΥoΨ 4ΒΔωK—μ;FΔa–4΄œ&€ΪgˆC…yA€³2‘›4qΌΣ–Π>QrΝΰΨCΓψ^E}†@šΊ±5ε°E ωΖlςλWΞQο>μŠ5Ξ¨Dš]Όαc€ΔŒϊ?όPΪ†ψ³gϠƍˆx‡k<ΗΔ τƒWσbνH9YX³@€Á5oόΟn3 /˜ ˆCχ°Ζό¦s!hIΜ<"ilΒ!Zd;ΠΖά“'O.ΆΜ½zυ\imγ W ’lς½hˆnEΰ-Fγλλ’²VυΰAη-K~Z(MΓσ₯KχΑ΄kϋ‚[>„aΓΎ•―hπ™=aβφp²<ΐ’+;φ§#G|)[Ά¬tόΨ!š;g/ϊ’AΚΆgβ™‚ΗΑΛ—Gώ:θwνn€/“u>'K₯gΙ’…}q7–―qF“‹gƒ[Ÿσ3 ΤGε¨V͚"Ο»a#ΖΡ΄)γΩ'7+€8oυκΥ¨~γ–Lΰ[Θ3Δύyό؟ηΦδ`mς,) M5&]š‰’%K–mΊ™ΐ-™τGP^ pΣ γmω.O(1ƒΆΠN"±|/ž“8ξΨ±“΅Θݘ7Ϊv½m@+ŽΑ,eJOj·P¬Ε0n‘zφμEί?[ςΜώX›7o&³Μ‘ρ6tΈΔΓ,‚kΏL°Ε$»uλΧΣσηΘ$δΖ&[·n ½h“Ή=Όμ/kMόΕό‚—₯αΓGΠ^°«‚α“ήΧόƒ8H;ΌpΨnψoEΛ”­@?-Y"uαS$‚œ?{έWEΰ_FήCš|ΪHhIœ<=?A”,˜Θ˜Ρ+Ήσ+$ΎΑμ"{φl’@‰ΔW40‡3_±ˆKκ”ΙiΪτ™ΤΆγKγ’Ε?Q³OλHyŒΥψbX\%ώ ΪQVnτL‘BΔρΌφ“Kφ’>ϋΝ3ΙΤ©[E@xyΒMƒ…WgJŸ.m€ΒhmaCΌqΓΟΤΌEk±γ-ΓKΊό­Ψρ’+°5Ζ2¬&`°)kΣΊΥoΨ„}w^’1£GΚΐrh΄­p„Z\Όewa °ϋ:sζ<{}hα\‚5MκT„6"ςgΔ K°&Ϊρα-³“’ UΟΙ₯¨wάΨμ¦ͺ>“Π+b/ά―ooDy2Ÿ|n+Țγϊ Kνβ₯ώ‘Ÿ°YDΛ–Ÿ³¦Ό”«Γ•fΘG{1ϋt Ÿ?k֊½RΌΟfjί‘“SΛ›šϋ2~όD*UͺρD?ΆŸ+Κ^62:΅ήύϊφa/ߊg ˜a”-󑬺…~ΰ“bζL^OΟr Μ»uλΜΆ{}hζΜοιΨρSŒεΚ“7/Š:λ—ύQEΰ_@γž xŽ4cΟ@Ÿ½X ₯zΪ²U½zυx‘φΔ^E¦M#_Φ@š1&]ΊχεKΎ’!`μ©όν·ΏxŒt,ΊrΤw?+aΨ[ <³πυν°Ο^qw‹ΝηΌ2εpω2‰q; ²Ε›-ν6ip%ΈkŸ/{%saΠ'Œχhž/ί²βc?άφΰclοΥ»Ÿ΄ KƒΓΣΖpψΘΖ Š€"FπM’ΐo©!ςba„!Ώ™ lβ\·μWΧς»ξ'Ρ<0ˆπΏsί΅ βρ˜eŒΌ&01–:!Λ΄ƒ}ψZ¬50Q2Se‘Xp•…ΎΨσ»λk6Œ\Μ‚fM9”νργΗΕ{†‰D{ίϐΥιΕ}΄χΛδc7MΞΩ¨ί5feΜ±E쁡Ωυκ5”4vΚl6ΆΕ.Fel†χ£lήydμΎ{χž•3W>‹ύe[Ό¨•<;ΊtνfΥͺUGΚΒϋ?z-6δηΠ}λΘ_‹ηXΏΎUߺ՚6mF€±ΧŒΓΏύΆΦͺΚm΄?Ÿ†|;Τͺ[·x+‚GaΓ†[θBϋ-ž;#ž‹NžCz^ΐf˜”…₯ύP^πΚωe²νΞ/“Ǐ 9sη±r=Η£Θ8k`Α8š"y2s(ΫŸ7ηΥfGQΥͺ5εΛ)<\τεEiZ΅ό‚ΏzΆ'v1JψY²dq±£fΕ₯M“F|·οΨE΄ζ+”£)S¦Š”Υ (―Ž@˜/š&½ Ωϊ·Κ„¦&―»ΆΉΖαδςτι3ΌδξŸl£|“»΄TΝ.@`MΊ‘iίΊΚ²§a?¨²&ΫΠ’[SοΛ”E X“τN,’η[ψ·€¦ ΄˜1‹£`^Fόψ›cϋ ³ ΜΑ0‹ Ν Ϊ+U₯9μεΒ ILYΨχΝ1Άvy8Ip•…2&Ξl“\z`ε4^PB†@Έ™X„¬ϊw3R nf€3Ϋ—`ίM΅ΧŠ€"π#`ΖKŒŸ86qf4hˆ`ς·wίnςδ‰ΚξΖfw²%σKόΈ“eβΜΦ΄ΑU<νύsMΧcE@x5” Ώ~/]ƒ›Νφ₯…iAE@PήQ‚?C›†y-f…SΐΪς/{ ^Άž Κ½l[΄œ" 87?Θ °" (Š€"π¦!s Š€" (AΦk@PE@x€1·P@EΰέF@ ς»}ώ΅χŠ€" (Š€" (.(AvDE@PE@Pήm” ΏΫη_{―(Š€" (Š€"ΰ‚€d@τPPE@PEΰέF ΤYέΚΌΫŒφώεΠ{ηε±Σ’Š€" (ŠΐΏ‰@(ύ ;–ΡΔ’—ΖAωΏΩX­KxS9Ζ΄Ότΐ›Ϊm·" (Š€"πΞ  ‚ŒυηŸσœΗ)B¬ΰσΞ`€U^ζΗΌœ­%KΪςς―,O(Š€" (Š@ψ! ‚lQĈx}ϊτ€δπ;!*ωνDΐh}}sυνςν<ΛΪ+E@P·Pdtω=!ΗκHύm9ύڏΌXͺφψίD\λRE@P^POSΫγ—ZK)zοθ5 (Š€" Ό„š ΏέV*Š€" (Š€" (/‡@(M,^-₯(=˜$¨APE@P‚G@ rπiEΰGζ~~7Ε“€J•«Σ oΣΤ©ίΡθΡcœεW―^M ₯ΊυP…Š•₯†―Yσ}πaqjτI©γ?ΆI9C BtGPE@PEΰ΅E ΜmMOŸ2]«₯LιI •­[·’$ΊΒΦΑƒϊΠΦί‚\΄hQjΣΆ#ΥͺUSμ•]ΊtE ‘φΚθM?iDqγΖ";ώtŽI6Βή½ϋXϋϋ)U¨Pž΅Κq¨xρb4tθ01›@ϊ†›¨cΗ.”4iRΊq㆐ߋ珑k¨£y8&"Βϊϊu?ͺ^½͜9M4έ(«APE@PEΰΝ@ ά4Θ0§ΈΖD1cƌταωD›/_>8 ?ΑΓ&ρExa£ ΪA;wμ€τισ€Ήω΄aέJAšc;A†YF‹­hτ¨BžQΟ† ™μΉ0½θΫ·?ύΑž)½Ÿ–n³ΉFJΟδ"λργGT nρ^ϋfδM‘"=ζ°Ύξw]&"3LA ωMΗϋ―?·Kω~7hΗΝԹK7±C‡©†Εfq™Pοaσo‡g’έžjΤ(A_υθ.ζRXE@PE@Pލ ƒϋb2[„οQγƍΨμa ΑcD«Φm)aΒ„¬•}HQ’Dvjˆaž0aΒ$Jž<9}TκCJ—ξ}Ψ5 H³!ΞΉΩlς‘ή»ο-_ώ3%βzvνΪMΥk~,ιQ£Fc‚»G&‚#\ΌxQΜ+°Ÿ"y ZΈΰ'κΪ₯³Σ,&Ρ=’#Yl•Λq;»wοκlχƒ˜hG™9sζ€ysgΣ€‰whΩςεΝ8Ϊ”$Ig~€?Š€" (Š€" (―-dŸaΤT˜X/ςη§£GQύϊυ¨aƒzRΜ$=Ddɜ™"2.[ΆΥ©ύ?ΡςΪ'ηΩ›u’ύ+ΓΗςΙ“§D=jτhŠ%!ϋC~_Μ!"± h§ANg}?›βΗsLΒ˚5 9ΌΎηΈΫ·oΣΦXόq6™H"βσδΙΓ“τVΛ€<ΨAOŸ>ƒ†~;DL=‘dΙΤ£G7Ρ"Γ|cΣ¦ΝTΆ\%–u‹Nž:Eži2Ρvk3f,Jš$©Θ4$^τGP^{πΥ cύΌοΖ›ΊΓ»ž·IΎΑ ηΛέyzΥτ· «7΅/ζΪΫoΏ?ΝΎ»σo/σΆξ|‚ΒΑ€aλ.ΨeΈΓρUΣέΥωΊΗ…›9 Ϋ{z²ωŸŒX±bQΣΟ>₯ργρΒ!ή‚ ψ€›7/ζ +°ΧˆΙ¬yΝ#ρξΘe̘1ιΟν»©Sη.βJν†ί-φ…μMGnc—mΡ©bŊl^ρ'{‘(OΉrf§όLΞΉΙ› AΪΙ&_ΆλHί1ω-\° Χ7Ž?.dDωάΉs4rδhjΞf;Ά#Ψ(C+#GΦL― ζΝ[±iF2ΊΜΆΝφ‘D‰±7xΤ―W7~ hΜΪο΄tλΦmΪΌy χ1±j=ύQ^0ΩΏNΩ[lμqaΉoδ»χΒ²žW•Ε1F)XŽIΪmϊoΆίf&M2ϋφ6™Έ€ΫΊ₯»8WξξIw_—‘Χ~~qόΆΣ_ϋ5Ž>›xΣWΌμιfί.ΓΔΩe½lΊiΓ›Ά}Ap\Α΄nΫ||޲—Σ$!¨"ΘΫήΘl€j 56$Ι3L θγ'L€σηΞΣ7ί T4Κ9|;² ¬‘iN(ςάΌy‹‰yLŠ5ͺcv`_|χξ]™¨‡‹¦ ΘwεΚΦJŸ€\Ήr³7¨’ή ΩξΪ΅‹).ωαŠΆΣ(gπΏŒ?L΄ΧiΟ£ϋο&Έ.Cs½›(ύw½6γΗ΅kΧe€yΎ„α>N›6άΣφΦΩ‡N3†!έΔΫγ‚Š7iΘκ—.]–HMyΣΣ§ΟΠΰ!Cιλž=ψ=9=η1_Κ\λ3ǐ 8†+ΝΌyσŠ—σ!Ή¦ΣS[w pγ'μ_"σ€iwΑΙ3Ϋ#8η™ΈΛVq˜}όψ ™ΐ=»7%K–Tϊlϊζλλ+ι±Ω'ΌE=@:p=|Ψ‡’²&7ύqγΖ ξΐΠΘGΊΑάgΚ•fς΅ ͺΌ»4wqXΌ»Ί‘Χ]_άε5qΑΙ.έΘ1[{pްΐXŽή.(ήΰ±κ!Oτ7νΔψκεεE ΔτόΉΆΑ£NΘΑ±λ½bδ›ΌζΨ΄3¨m`e‹‡¬ \λB^΄¦žpY Ε”u0οtur°o~:wφœ(τΰΌΙ”‡ά—½OPmΈtινί@Μjs°ΉibVΪε#Ÿ=ΨΣf̜E9XyŠvαά‚—!²Mή_~YΖΦG©S§Žό,=Μ6ρœ΄ζΞό‘97φΆΆο~t ,w(βα£ΨcCΓ ‘Ε±€σ B[_|κ;S‡»ΖlMΧs€2HßIs''¨8w²Mώ fΞϊžΆΩ|π»ζ… gδ™­‰wνŸIl‹rφ~9φόφt{|`ϋΘ9p§š6mκ΅»LžGώǏύynS3ζ hȐaςΧ]gΊpρ’νŸiδΪیxp―m«=ή^Φδ j‹ϋl€›ϋ eμ² VfkO JΎi?ΜE?oΡ’ϊυD0 -R€uaG Ν¦ύsζΜ₯œό|λΦ?xl+AC‡ ο_&έ~Ÿ΄iΊϋ2π’Šωc?ώΈfΚC^™ςΠY&γH3ύ2}Α±i;β0.ZΜsΤ\–,&?ʚφIΰΌD|1 ΞFŽžΰ”οšΧ”y₯-7&D΅ΏΦώύ>Άa˜pŠ(^ρΞβImΦ‰'δΨΔΏJ=ΑΙpM7Ημ.Ξβ·kόψ ΦΒE‹¬sηΞ£MΘkώ\ΫXΌk>=~χ{θέC1όzΜ(ΎqγF«LيY²Η_œ¬I“&[Y³ε²x₯LgΞ_‹η#8γ°Γ Yύϊ ±>^’,Ζ:gΖ”±Χ]Ά|%g€Ω―Π)‡(‹ϊξέ»oρ-ΩΗΑ²nάΈι<Ζϊ`κ`’o%J’ΪβΥBqΘΓΔQϊcΪ‡8{0νΆd—5nΩnIš»ΡΗ’Ά‹­E›[­ΎΫd%μ·Ίqχ‘…‘=η_­νΎ%ί³c½]ήΛξ›vœ={ΟV‹η’8E±[N‹½ Ι1?@%}Γ† rΜΪ5+CΖμΏΌΘ1pΚ”9‡Εftrό˜±mόιgΦΨ±γδΨΤ#όc0D<Ξ£ύZΈΑXΪρ½,­t¦Aώp.νXγό˜sjΚ?ε8Τεz‘<ΚβϊΓy32›6mn-X°@’Œ|Θγ/#Ξv ΡΤaʚ-d ―ΉΦ}Σn“Η¦½ˆs½Ύ‡:Œ΄“έ£JΣ.δAp'ρ}cURΝ;nΌU£ζœυρW«B…Κ‚ ς!τ·Rφ_a>η'Η}—μΆΆr(=yfQ₯ΦFσΦ²:ΟώΓQ0  V ,΄Ψ+’Hyaσ²oΘ~΅r0ζO™8™°τ—_¬Š«Κ!Ο-aάβπCϊΆIΆXΓf₯χΚκ$ƒθσηŽ§eΛ–[ν;t²X mňάΒωιΖy(R΄„•5{.!r¦~&Σ¦M·€Ν$ΧΣwίM·X³g­_ΏAκ‘_»v­³nώdΜD£ž“xcžΌ­ΜYsΚ=~ωςIγ/ϋ|.ϊΘ>rΙReeΧ/θ&ϋψ μšrfαΞNΡN€‡›Β\Ψ!l{Έd³·εuiSΈtT…ώλ(AώΧ!U…ζΑ ‚\’ddA 1¬Οž=NJίS4QxΧ­ΧPzX{Μ ΙΓD3Ύ ²τl6!ιϋφν“ς v3y3Ρ½%ιfΜΑκΖ kΕ3.ž:uΪJα™A΄*Π>EΆϊτιΗΪΎkΦ &θx }υΥΧ4BμžRŽA,ͺUŸ΅jυjΩΗƒ³Pα"ΦιΣ§eΜ™AYhšπ°ŽI>Ξ0 πž“W­œWZ;΄ ΟΟ<‘»z'ΏD|ύ‹uεΦ9ώ“΅Η²ΨYŽΓϊ/C† ΅Ύε-΄ž©2X½ϋτu’%hŠ›ώ…TkΞνŸό’Š +p(Vό£ΟφΟoefΒ ‚ύά,Yς³ΰ²ΓξA…Θ‚LυλΧ_΄ΈΌͺ«•'_!kΑΒ…R–W•όΌ"«hN§L™*ΗxΉBψδ“&LzWΚ>~pxeΚ.η/Aρ¦m77ϋΨ—²hB!&k3fΜ”φνέ»WW\Π47mϊΉ\―ΈFΠoΓ!CΎ•k_iqmγεΑ+ζ". IDATώŒ;sζŒΤ±yσfΉΦ­[/ΗΠH­#τΧΜ… ,Ά]•}ϋυš1†ζZcŠ”ΠκΨ±“uδˆ/kMX{ω>@Yƒ1ˆ!ŽQ‚ΉήΝq8W q’ΡνΫwπ‹^&«Ώ°”◁(1’ω6χ ʚςΈ_=b& ώ^ΩεΈWͺΫξ•oΏ*mΫ½{hίGŒiεεsŒΐ+λZΩ³ηvj§M(H‚άgάfΰŠsˆ—φzeMžδ‹””}3ΖwMIζPό„› 2w8ΔΆ#°- ’·Β‘Ρή–Χ₯M‘μ‚fWW@ΛΩoXΏ†Z΅jKL°θ^]sτ˜ρ΄zωB™ϋ―ρdΆd2ζΎΗ“Ž;*ύωμ³&4dπ ‰sώΌhβΝ;)V΄Θ5rDIβ©4²]Άγ$•ϋqνϋ΄%ŽΓuqH‡1xΟ’ΌΈB»)yƒϋf=βΙӝ)UΚT΄μ—%΄eΣoμ~Mœ4YŠ3A“œp`ž-˜ŸΒ6Iη‡φ‹y$§cξΘύϋΕ~S2IYΗΏόPΫ/ΫΛDG`ΗZkφx4œ]“–•‰ΩΡ<¨ΥΝٍθ\±!_²d)Ν›?ŸrζΜ!§>ύ΄1%χLΟi<Α‘ζΌχήߏ`œW^‘mdν$#ϋO¬Μ#«Ά&MšŒjΧΛI‹VΕγIΰ§N{rx†ZΏξW^=6O.&“ γΕ‹+“ΡoΜ‹Α*΅ΘŸ*U*Ι‹ΙnxΖ™λ.JΛ•―Μ«Ο¦—•i3fτ’„IΣY=ΆI“¦βεvq\ƒ†Ÿˆέ)lξ;vμ`…Ω3§Žπυ}Xξ—ΘQ"ΡgM?γEΓΌΈMμ5 1I'&˜E°]μΧ_χ–y=86η [œ#΄“ίŜmε9:{ϊ•.σύ΄h!ύ΅υWΒuΛ/’_.π{Ψx&Oό½Βο€a„η…_€iάΈρ»;σώίwgί2ໆύ»ΐΉΏίΞΜ»υάοέ·ο{ηsnz.Ξ>ϋ,ޘΉΡΡΤβ†λΖΡGr³ΦΝω1z]ΟΌϋ†’με’[¬Z»ΩέΔUA(\΅;'5?ŽΰP“ϋ8΅ΑPMIπ}’φcǞ©S'Έ¨=]Ίtvrψ7M5ސ°d$J–h_Ό£’¬Ό§<ώ ήΎψ(τnίΊa+ςO λEX^ZF¦ΫkI²U΅SgC’L™Œ"D3Ÿ §vνΪbφ½Ώ¦£υΡn³'mΠτW_weΒKm{?‘ŠΔΘ‰œvΓ,ίι4TRZ‚Δ€n % ±" C‘Gω|CjΡpύ8²ξœ&9Ζ€ύœ3ي•«qN«Φ a­›ϋρΓW“>*9_L’C“;O:ΨΆynΉuΙ·6£Šsη"Œƒ‡ό=~;{6Ίυ„φ‡΅ΰΝ Œ=Κ‘λ(aŠΖΊΥœ§σ!βα‡ΖΰγNB<Ιχ¬»oγƒΤχτq‘ΫωiΡβΟqνu7Ψ0²qΨοπj§(ν }Ə?ƒNμΗ9Ό>”ςς6α#nΘ»Γμ¨ΡγOg7aΦΉCK7wΥα‘š6υ|ξ9π&2‡β:vέ6γV»ϊ"Γ~Š]{ΚσΛΰJ‘DIhΤΎβ:ežxβpβΊ“ψλ’ΘΞn\ϋk…ΧΏŸhΧΜΆΩώ!WoM³›TŸΓͺΛmg9ρφσάuΖc­ΉΔΔ$³^%fΝϊΆο(p˜°vuXΫϋ~w/Mg£kΟAθΠΆ~1sFŠžΣθυ»sώΥς‡5₯©S'» ΄ΫΕπ£έ~ S¦œΟΘΦ „ηκψ’©y³θΌ°€oΏ\‘Ώjs€7Iw‘Ό’I†γyNωφ’šˆσ́;—Š$›΄6—-[ΙηŸλ²ύλβ„aCܚW¦¬½ξFwmvhί+VΔ`^ΣJϊ"ύ€υ 5₯kλ‚i“k΅¦όΆ΅ω6‚\”¬Ž!`2θf4lH zœ›σ£s~ο~k·Oyjσυ.C ύ€Χ―©mλȐ‘ExλŸοVίυ[%ύ‘9rΉfχ8£~UΗΏŠ@Š΄κΎίήύ¨ϊΑQβ}Χ₯·Š(Ήπω–ˆΕ7ΏDΝ€›TT‘θΊI)zΟ?^y™d~žš;#Έ—Ÿ/όF€ž8΅m!i¦4r΄ΟDζύχΚ!θΡ–šiεV W€z£Q’›‰•ΑUύVΒJ˜΅kΧ―½>―†>’iΗΦ§η>‰‡ΌίiQUQ‘¬ΖŒ<Α΅Ιižƒ}0ί(΄V —Β‘*5¦ΆzT λ[ZD‘ϋ4ξO u›΄ήž}ξφυO4"ΙνΥ«§+φNJ­λτε•FqΛ–ΝΌ.‡ϊΕξ;‘κΊΩ%³κ V­!5jLΏ_Uυ΅Ηξ7$υΐ\ͺ±‰;zΫ°σΪς7εW“xw±’AFdΎQγVΈϋξ™ŽXͺO­#ύι­ΔžΞ©Φϋžόωο2μc+tθΠήUQ˜»«Ό―σΪAΦ¨ožΑ?Σ«/©ΝqΗ qυ³XξSsρΰ΅ΏN‘£wΟΞnnzύrιg ) Οc/Χ'Mp°μ«5N+ ΌκjŸϊrΈzώuρφόψ¦ξϋ.ο|F'™2ωϋ$ϋΣάCά³Ο>‡yσήre‚ίοK±Έ |Œ‹†Q—5ε:έΛǞQίK#+2 Cΰ`E@7KΕQΧ+u%i$ǏŸˆ‡η<βŽwώƒφœk. εŠeŸWίpύ‘‡γ c΅«?|ψpnOΏλœ΄«sΔLboΞΊ©~Ίp©Ϋy”φ’n#‘έH₯[Ώ!ίυ)Ύ±™›$±±;ΦG‰zl_e₯eξ΅΄ΚJ©τoτΙ4xο½·Ήθ|ρϊ'4ΟθοΖ’yˆβ—&Sγ΅Kͺ"Ν³PTVΪ WSƒΌlΓvδm‰†xσeΨ²½…”9…―Ψ•D*φGςρΙϊΫΛ/1΄ΤŸάΉZΝ›φ%—^I³‡_ΉaτΪυ¨£ƒ6Ž?Ε͝:u Ξ«ΊwμΨSh6πλ{ξq&Š΅;zτh\yΕeξΖ«yh,b‘=Lψ©uλVΈψβ‹ρΐbcτζ1†υ 7άD‹?Έv¨i=kβD,X°ΠΕπΥ[‡λ–Q{½νAΣ‹;ξΈ›―μΧQƒ·3<[Λά¦Ž,υμΩ Ÿ-ψΟ=χΌ#™ZcƝκ6­!c”ЁΝ=H5kΦάΙ}7₯fπšwˆ¬)]xΡΕ\s‘‹ΎφίU;ξγ)Β+Σ‘Χ_ƒx”Ίu1bΔjό–Ή~€i3z$Ž?~†?žζ>ΡqGPc«υ­MΈ€E€ν-Ǝϋž‹ι-΄Žjω«ΤKζ }ϋφv»ΣžqΖiξΔΗΫ ΆΫ‡Φξφ…ΥύΠthΨ°anW[αA§0’ΑΙΤΈŽt-cI«©νΫω¦ΐ?‘¬Qγ΅BΎ’DόkEvι‹}l/(tηH&Ÿ-όˆ&pΙσή|“‹ύη$ϊ€ζ_gz;£uΣ­{7τοΫέiΊ₯aΥ‡)S/tcԘ“]xHaΆϋ9₯S"΅τΡ0“NH~ψΧ™LU΄–W­^νdΥCΰy睋SO=ΩU=’±Ώσ―wAί„ωφθύχίg˜G½qνΚ'©4θθ:]'cΨvϊΥWΉφ°ίqǝΈ•ζ"΄§©Y>Mn¦ΣΤιnj±ΣœœΡλ(Šο™Ζc€³άZVlkνlΌrωηV=όKνΟMσΎθGW!³ΚD8I»­€:zΫΉ¦ΪΦiMΉNφυΑAj•x‚λ%ŠE­·J†ΐA€€]C ϋ$’9©]ρh_ιKx£ty΄Οδχ fVΈ|9‡5mήΦ7ξTηΤ¦π`ΤδΉΊͺ3qβΩt 9ήE:Pζ_ϊWη…?–υϋυδQƒηκϊώΨ|π—ΫͺSNwN4γN>ΥkžΫήE-Ә±§Έ(Œqκ~ϊη-―>H =Ζπ₯γΛ'K­59¨ΝŸ?ί+Šoΰξ·†~χ»ϋu·ςζΡK^ε3ιΔ₯¨£FucψN\Ό!Ή6ΡO†ς*―τzέωŠχΙς¨WΉ WεxΈΰο­OWΊΊ|΅μΎηΌΊΠ»γΉέoΏw°>όώδ(%½‘£Ζxν:tσ~ΞπR΄©¬α+:B4rŒ7ŒΞx mηGπΫΛΩNJ'ΏNή½χώΆΪΉΙ―γωΕ=9m)ωyrˆόρ%—ΩoΈ‹r §1šΈ::'=τ0ΙΊzΓ‡tŽF§Ÿqf΅·ΎίδΥχˆn½q£,οΛ.χtn•΄žhͺΰζ_}]¨AΪ]»2EY4hˆ ¦(r"ΥxJ‹/aTΈ'9§=Λ‘Œ`‘°h:Η ΟεαΟΓVԊcΕb,ΧYwŽωΜ3άΕΑJ‘5΄nδ¨δ―[EPQ”‰±γNqλ[Ž«J;~pό„‰»„pυΫ(Ϊƒϊ9RςσέAΥ‡Ÿ'§SΪ`;GCΏ\N˜i[Ή1›d·ρβ‰]ϊπη€π{Ξ<«Φ׊Δό(3yΎŸ£c˜Ÿ–/_ξζΓ"—υCΒ**Μ ΓGxw1ξͺŸNχ^g$₯ΨλŒvΪ.ξ:U€#ω@Ρ<„‘’ΕAƒŽ­>§Oπœκ:Vš:ν"oφμϋάo^ώ·œhΆα΅mίΥ;•‘=τƒmΥλAδ'ηBEΈ‘Όϋ[Τ9ΤΗ·NΧI›]―υObL‡Ι«Ό#yB8hSΞλ0~Π—ΣζC‡ϋŸΕν$O5žώ3ΞžΊrQB <Ζ;jΠ`οΪk―χIDΞΘJεοΖ›nvΏ΅&δθ*ŒόΎk³¦\γZ~ΤΫNzϋ"ζVnjπŸ‘ν€wœt'ύι5yl"q‡~Ύ΄ΜΙiOš&i@τ Q‰΄&J»YωζΌa:»Ίšvΰtψανϋί*IS’qτΚVΏ%ςύ׌5klύ9ω(›/‹ϊ₯Ίk―WαJ²Η•ŒΖΦs…όΰ=ōϋΨ‹±nk1?s`u^1Ν)’isGΉ”€eξ0kή>oΊ·ΙήυΟξ§_ΝMZΨΤ”Τj­¦Κ€—)‰+η+]ΩgeE'%‚ίž7WΎς]λ^ηJ+[ζͺ>tNΥΖeξ·W±4gΗ™3θ8Ά¬€GζΈΧκ\ψLό ΞώψΟόδΛSΣX{+σΫλ[υΤ~©ΨώλL»vλL5ΈBΞΔvcΏ C`oθΖ@ΫL|±τ+#Θ{ΚΚ<>Inπ‚6`ύ{ΡD3yόψ;ϋ‚wx½ϊόΣ…ίxΪwc«aΊ)z ΄k[Άd Ψκύφ'N&(Β―&­Ω·ουΐk)σ%ί–φΐ›ΑŸΔξΎΑucXνσϊXS΅'ΘΌ¬ƒŒΣΨςρ[W’/ŽŽ±ρχ.»•‡.tZ„C§6ΓϊΙ·£‡ξb°™ΚΈΝDeͺζnd―φ‹@S‡ϊUmΠͺ5U‚Qδ8x r6^Έ6r[CΰFΐγ.hπb ’~Hƒa“7 Cΐ0κL₯99F@ΓςpŽMΔοΪμSχΦε;—Ε0 Cΐ0 }!Pg‚,³ iŽ96‹}αk冀³¨po[μz±Υ`†€!`D£Ϊ’𐆀!`†€!`†@ύ#`Ήώ1Ά Cΐ0 Cΐ0 Œ @'ΛD5 Cΐ0 Cΐ¨Œ Χ?Ζ6‚!`†€!`†ΐ„€δθd™¨†€!`†€!`υ€δϊΗΨF0 Cΐ0 Cΐ8€0‚|,Υ0 Cΐ0 C ώ¨W‚¬Νφ”άΖ {*¬eώžϊχϋŽύŽνϏͫιχžϊ―©nνσ΄ΝpΜVΓά†Έ^R}υ[/ΒZ§†€!`†€!`4κΎQHmdχ<γ“ψ—Œpy<·%u )dρ ψΙνF*Šω½kYm†P›„Δ4DΒεˆT–ρpgρΜˆ R%/Rp8TuμqμT Ν±Kvi·s\`Ό«§:^€’E;ϋίY―ΏΨo€S6/βWbT&v]F΅οhώn# ɞν—sΠάχμ΅—Ζj6L"‘ϊyδk˜³5© Cΐ0 oΐώ'Θ$₯ΈDo]„«fέΫ’h¦Fͺ€ŒΈό―?OD“ΓΊU»:N‚Χ/ψ/ZiY½&IΈ₯/Ιψ¦₯£’ Š’7¦δ™-{p„Ά¬ZΰΖnά²' dΉΛΫ9z”Δ–—nΑΦΟ>GV§ζHLΞfΏIφ8v< kގ%(β3AG޽eσb¬/:ηψcό[wsj±‹K@αΆ%XΙωwh4Κ聈‘䝧χύ₯΅±eΛ6­οC›Ά!`†€!PKφ;Aφβ¨9.Z„ΘΡW trΎ:Y…_#߈[Ts›jjPCEKP8ω7H αλ3Hlεˆ-œ¦9*Ή4Έˆξ$ΦΡ|Q\`R3]:ύDΎZˆΤοA qo ’Μ“γ”_|"YŠD"Ψπε"δύγtlήqα5¨8σ.#ϋλΙ{²Ž΄¬ΡδQΎ`ωf·‚β § ι½—΄ρ=D›Ί-ΆE°AJΐφϊζ"’Ϋ qWžδVΉκ‹΄βR^ρ ²~ς2Ϊ΄Eψβ!ΘΈj2‡†χ‹Α[5^rΥF0H 3 Ζˆ’ϊ¨lzpc –iŒ„ψl,.@aΩtlΦ ή¦O|ςmθ9~ΒOήƒΘ[3α59jΑƒ\«G§Χ/JW•§ίAΝ«†qUfιΐF @VœΝ'&K†€!`†€!°Oφ;AΠ|!Ό ΘΎ|$Z… ‰ρ(Ώλ,$tνO¦Ε‹PΪωτšv°y ήψ9*‹>G\J;’?™L’V–αq ‚‰Y;Ι'§ˆOE$οSx£nBο‰SP°ό ”ΌseΕΣΤ’DΉΊMš& „κΩxNρ{“°ρ΄s±νζ hΊy ڎTT θιλΜH"A–v8Jƒ i(κs$_0GŒ8y› ό¦‘Π£Ός’Kšk„6!!΅ ΚKV‘2΄Ι]ŒK¦ΉGηXJsŽ$€΄ΰƒΐRζU 9½;‘r-~X'»tΓ™πˆ „šτ0ΙL?*KσP\Ό†bΗ!)£—Ο£‰δͺ,έ„PΙzΔ%Ω'΅ίρ)}ςdέϊ ²:tBθϊώˆ#ΧOζƒA"(γwχρ¬&.)\F9Y~9]7—ςŽΌλ€dΫRφ†€τ„„&)U˜μsY… {b< D5! Cΐ0 ούN§" ‹„£&ι†`Gn:ekœŽςυ$o—œ rB„Κh‘˜ƒeιÐΈ}:Ζo—Šςς2,J‚φ‰•Θ.ώ•$Κ¬*9 τ’Ό‘c5RZ†Β£/‚·ψAx Τΐ’ΨE¨-{ϋ5„ξ…@ξ‘H:q*Zœσcl?οη¨}:’d‹YI†*ο.‰Ϊ_’ΰςΆΉΘ8ΌŸ+iΤ»? Ϊζ ω’‘8˜†e©‘΅μUlιtΠ²β–<…Ž)$ΏMz"?‰uριΘXφ&Άυ8^b ύs;!!SvΟ΄™.+E\{ ιŸ`ω³χ£w`Σ:!ωGXΩqΟGεŽ-Θ\τ2Ϊ–ψ”–Θ_ρ)V7= έG‘b[>2—ώ;τΒj’ΰμ֝в]gΌ™~"Ίg£β΅›ροy/’gxR›υΑΆΥΑΚΈl ϋΉˆ”#eα 萛‚”F‡aErlD\Ι(ν5‘­³βU΄jέ™³ΥYŠβΎ Lv`†€!`†ΐAŒΐώ'Θ‹ά8G1ώl”“‹ν'݈πσΣΧ’7ΚΊ¬*ςIƒYxE_ ί/ζa[ήF”]ή)Ήmj}έρ8ΌΏ?ƒΠo¦ ΠώHͺBιμGΝr°l=Κ{ ¦mso„ήz‰ύ† aψxTΌς ΅ΤιnL½N–Fb)Ν¨Ή-Ήΰ€3ΦΆΘ­χœθXΩΈή)·!₯YB/<„ΤS©₯>ξxΈΡκF#oAΏI—"oΑGθ2p0β“P°mΦάs’ώό šέυš7ΫΏώ=zφF0)[Φή…ό™W wυΧԊ“„sήα•΄ΏώΩ,3ξ„9 …ύ ЧΝΖQ“¦"‘šΎ@BΦ.ό[gNCvώT^<ǜq₯ϊε˜λTvAtΏχU$я8ώ‘—zζT¬μ…Α?ύ5Κ~y%Κ^}ΫΟΌ GNΉ)IIξΌδ­\ŽM3/EΛw_CΗ'ζ ¬EkΒ•΄ΣnƒΟΫκΧ^BΩμΣ‘œ}8ϊ²Ρ6’ΌηEc%†€!`†€!p°! αώO4 $&’rkΌu+Ρhψ)(#ϊlXˆΐΨKœ’ŠŠ%Ÿ ˜Ls‰₯Μ_Ί™=ϋ ’Λρ¨\² 'NDrb*ήzfR»4ƒIζ•kib0r9b2BΣΞvΩHλΣ‘,― š\Ny[q Ύ|ψς‰ωXžΣqMθhWZB-pUͺώαg°M<Κ·PΓ=d‚ΫPϊ«‹,άξŽΛ·ͺœui’Ϊ8 ν?Ξ@ρ½Χ"³QΪ]z3*(f€6Ο©™MΫͺΚf]‰G‰¦΄9Ξ½μvP<Ž_50M!‚ ‰HLIF x+"Η~έ§ό‰Δ«dΖT„ψpΠ¦O?΄Έΰ65B—qg ²ψclΌβ$lωΛ\΄κέ™Wά‡Πu#f~Έ¬ewώαWƒ@fSFψHt€7Τ Ίώΰ*4*)DΙν’τρYhΡ±rςK”NΨw“Φm‘ϊε]=ήOΡ~Τiv;ζ0<9²·d†€!`†ΐ!„€(ίώOΥ ΗΚώω2;tFxπ4”Ό"νΨ‘πΎ^‚π¦šw Ϋ$!d­XM?Ώ 'PρL­η‡ο"EvΞ$Œ‘Ξ!€χκ‹ό\Ό™δ2₯Τ—s2CΩx š²¦:¬Ή$ωόD(νt9ωfΚ @£bΰ?σ‘Ϊ(ƒΪΩ`ΰ±Μ?!ΚOMm τΆ6½‘όΧ?±œ|utWdΕ'Β[K›…ξCΘ]γ°1£%š|DM05ΌI—Ά@&‰¨'hρΝΨ±u ΰ%Ž$―-CΣυ£ˆœuΛχ―AnΡ&dw;ŽˆόηχμΝ؜؝›οΖqS`•όΫΎfyFe6αΈ‹·y«·.©1²Ώx%σhΒό£\'O„ΣtI$XΔ8ΜΉϊΙˆΨEVΏΠΎ Cΐ0 Cΐ8xΦΛΤHͺdiβi6TzΎό₯g M'ΚήgΌ_ Jr+‡5TlD u.*ޜΛΠdr‡Bω’i†ρ5CU0ς‚Τ˜$—¨(De#Eu,Β4Ρ(9n$ЦGΡπq(yτN$₯¦!aμOΞ€ ‘f4©Π؁΄A0Δ\ ‹ΆΪMBΉq5ΆOώ΄»]‘΄Σg!ΪΪ2:―8Ε7οΎΛή}f"]PyΪςb7―άIaύY—cγΤːsϊΉ$’yη=xμW ΆϊΙL¬p.vά{rNG[ι%ˆœ$WΥΨϊJCh>εσžαF"A4»ύ%|Υγ{4ΝxGύμNδ CΣ²e¦­$±‹½ζΘΌμHƒ#ν]’φΊq6ςO»…άœΖ"Ρ~3š£βυί!Hγθ–ΧΝΖΧέΗGεAyΎ^LΗCΆ¦΄“ΓΗBJ>Ι¦<~Y2 Cΐ0 CΰPB 4ΘdUμΥcœαMιάεξ_’CΨO‘@;άςWC #!•!Θ?ρ―`Z[T~ό ΏXŒ †…+yϋEΖ.cA•sžΜ+"Ϋ>Cκ±4ΧHOGΙίζ"0šά­ΝP kŸ=ŒΘ¦[‘uδ ΣΜ—€Qr8‚­mυ4Rqδ~Αςε+―2―•ΌŒaΌΘΊp<ΙdόβΞξΖHγwΎ;N:ι{h2κ|T|ω‘3οHΞ[…ΣowΡ&€q.|θ6ͺq9 γ"μ;£dŽΉύgfΩΈEP{N‚―qƒ’Zγ d€<J3“χ€’ΉG#gόT4ϋΥӌΎA'G:/–ύρNΦY‹ΚγΖ’λΔ©θ̘΁kαΡyPΆœξ»aθwι΅(nœoεgΔ>DΊΪr/Bρcχ ‹aξ†Μz’ρ’Χ¬@Ρμ«μLyUObυ€ΦΪ@% !Žf„,ΰ^¨Μ’!`†€!`‡δ?΅c@aΪυ.ώψS΄ώύεˆ/^AΗ±΄(ωόNdUjjΣ“˜!ΦΌ+HK¨ Ξ!Μ‚·}Y΄Λ‘@ΒZΈ₯Ελ°μ…θΒΠhM33―8—ζ½Ψs}5&·„€·’}CΉm]LvIgΑ0#7γxŸ“•{Τθn’οUs‘2r< §φE0‡6Π)ι/y‚CPΞ”Nό#Œμmύ‚˜4£ΜΝαm#&Τb{ω‹hqc$‰ pdΙσœ±β8Ÿ5ˆλq6 ~*>ΞzΗrLΚ]Bf_ς%χξΓρΊΓΫ²^h›Ϋ0ΕΫΞωρΑΐΫΒ~›t@\§‘쇙,y$βǝϋΪ‚ ΄7ΦyrOΤpgΆgψ=bW^Νσq°οo‡q T‘²Q{¬ύαoΠσΘΎΞLθΫuf­ Cΐ0 C ΎŽu?'jdIθPH!qMΘ"ι$™1wžC’Μ&X§ΖΙ"e λΦbτΈοz4Ξ‘ί£³œ&ΦT’βhRŸ$l&iKlαΘ3XF;ζ²Ν$u_±Œ„3ο=*CΣωGXMŽΥϋ‘κΦΫF,Cnθας$mœ½­ŸFΙ}=ωœ‡šΪ0qϋloΝ>ςVskgjYβ—Ω’Q6gθ:v™LUl œΧn¦&6ΰΚ™$ΞKLβΞςΤξόΰ\5ΞΫΣ„p©–Ή™/ΣΧεU~<‡ςΠτ"»7συΐζά9/όωάθοŒΐαΗωkŽi|˜P[Lβ›Ψ6ŠΥΖχ9±– G3φKgΒʏΊiΛ™m%OΑΧ<T}Η7fqΰΞ^žΪΚkjξ]πsΓΫ‡!`†€!`5υ@…—'I1H@₯εΥI&χ“ζo’1%•“τFJΙ!IP“Φ­AρΛsQρηιŒ4Am-5nŽΔΉΚϊ`Ÿ"ƒ"΅Nsμθ‰ΜͺoΧ?ΗJjΙBζΕ’\ΏΊϊωΣwlΉ~‹΄ΊψkU2ϊmTŸA­vKD–ώ₯ΟΟqαΤ€•εžΣŠυΉe3r©έύχ«(-ΪN²Ύ‘ZάμŽ$SsφΙ~,.nKViΪYȁ₯|"ϊV’ ΄h” 7^B†2]Ύλ›ΞށF,χ·Νv$Ώjκ—ψ‹d»δΧс{H†ΔMI€Ϋ΅ΥqΥΨΐ> Cΐ0 Cΐ84¨‹ΊGFι-·ˆάŒ|3#gΊbvβŒbλYύΧ• ‰°—Ώ&ν("I5‰δ\d&QΊΨb9Ν)\ a#™υbψfbΡΐO‰g†€!`μŠ@=iwdΟGzΝOϋή΄KΝ°’LHΫΩΙ±&!-kb&γhβQ!9cΙ―΄°•4§θHΕ9‰s…H~lΉ:°d†€!`†€!ΠΠψŽ ²ΰI¦)Λ~ ¦Ή…#Ώ5I[5gΒPSΉε†€!`†€!`4t CC‡Μδ3 Cΐ0 Cΐ8˜0‚|0Ÿ]››!`†€!`†@0‚\gΘ¬!`†€!`†ΐΑŒ€δƒωμΪά Cΐ0 Cΐ0κŒ€δ:Cf Cΐ0 Cΐ0fκΕ‚1]=ί—;Γ1hƒ%CΐΨά†\׌βh[2 Cΐ0 @ r \†@εFFψmΐgU»―5ψiš€†ΐwˆΙq raΖΟΆd†€!` :δHZ *Βΐ‹γ6ΙΆFƒ?Α&`C@ @rά‘Τμ† ŒΙ`†€!`ϋ@ ΩC$>«ΞΎ€ΩVμW+6Ύ€ηyςŠ>XΪ5τ €,Γ0 Cΐh ό„“4W‘ΔNIENDB`‚stravalib-2.2/docs/images/stravalib_architecture.png000066400000000000000000002313211475174155400230170ustar00rootroot00000000000000‰PNG  IHDRΌ―FΌbKGD ½§“ IDATxœμέy˜ΥuέψΧφ}dίTάχΚDQPK-3΅¬μ{[Ώ΄ΫΌSΌ΅Ό[,5+3—ά—4EΕ}aSQDYDeΦYΞόώGα3‡›ΗγςωœΟς:ΓuyΝΣΟωΌO¦€€$`[ςͺ{j.Ρ@’h I4$H$‰F’D#I’€$Ρ@’h I4$H$‰F’D#I’€$Ρ@’h I4$H$‰F’D#I’€$Ρ@’h I4$H$‰F’D#I’€$Ρ@’h I4$H$‰F’D#I’€$Ρ@’h I4$H$‰F’D#I’€$Ρ@’h I4$H$‰F’D#I’€$Ρ@’h I4$H$‰F’D#I’€$Ρ@’h I4$H$‰F’D#I’€$Ρ@’h I4$H$‰F’D#I’€$Ρ@’h I4$H$‰F’D#I’€$Ρ@’h I4$H$‰F’D#I’€$Ρ@’h I4$H$‰F’D#I’€$Ρ@’h I4$H$‰F’D#I’€$Ρ@’h I4$H$‰F’D#I’€$Ρ@’h I4$H$‰F’D#I’€$Ρ@’h I4$H$‰F’D#I’€$Ρ@’h I4$H$‰F’D#I’€$Ρ@’h I4$H$‰F’D#I’€$Ρ@’h I4$H$‰F’D#I’€$Ρ@’h I4$H$‰F’D#I’€$Ρ@’h I4$H$‰F’D#I’€$Ρ@’h I4$H$‰F’D#I’€$Ρ@’h I4$H$‰F’D#I’€$Ρ@’h I4$H$‰F’D#I’€$Ρ@’h I4$H$‰F’D#I’€$Ρ@’h I4$H$‰F’D#I’€$Ρ@’h I4$H$‰F’D#°s¬~?¦ό•Υ=;D4;AIIάUάψνύΗΘWχ4δN4;Αœ—c±ώ£{c,|£Ί§ w’¨j›bόΝQ°."bΣκy}lΩXέ3#ΡT΅9SβεF”DDd"¦>3Ÿ―ζ‘Θ•hͺΤζ 1ξζ(.wk±pSŒϊSlΎ™ΘhͺԜ)1ϋΉOmΙDΌ;%ήx¦š`‡ˆF κΕ¨?Δ†?»}ΣΪω‡(*¨Ž™Ψ!’¨"%%ρϊΣ1η…mΌ”‰xϋ…xγ…()Ωεc°CD#PE6­‹±‰Β ‰—‹b̍±qέ. €&€*2utΌ>ξγES·ιΝ'γgwα@TΡT…•KbτŸ"ŠΆ·OvsŒωKlN݊ &@U˜τh,žώω»½;5¦ίωΣPeD#°ΓςWΕΘk£d»·K¬‹Η…Ύ³ΰ C4;&[Oύ-ς—Utω“γΥq–Qψ’ΐŽY³"žϋG…n3–ΚΗS·Ε†5;s&ͺŒhv@II;žΉΣ“5–h*iΙμxώξMέZIQ#SGΖΌi9Ž@UΐV>\cnˆ’’oΩ5NΌ0ςκFDdςβ”‹£Y»OUΈ!ώω±~uއ°ΓD#πiΕΕρΤίbύ‡ΉŸGœ{}²aCγΰαQ’Ιρ|3ŸŒ™Οε: ;J4ŸΆdNΌtδšxΡΆw|εœΘ”ϋ£nύzQ΄κ”γ ‹7ΗγΧΕ†΅Ήΐ@y%ρΤίbΥœŽAί‹N½?»½CοrqξΏx,œΚρXvŒhώ­€$–Ξ‹ΙE&Η…k’Γρ•oG^Οn―S7Ύς­hέ3ΗΣoŽ17FΑζ`ˆFΰί²E1ώ–Θ?ΗΓλ6Œ‘—DΛέΆύj‹έβψF¦^Ž'_63&?%ΉΦ,ΉΐΏ-›Οέ‘λχ"ξqt|鱝§!:=Ίμ›γΙ3%1ζΖXσAއ+ΡDDDQa<ω·Ψœλ—[4hΗ_ ooŸΆγ˜οFlυαΥ zχ•˜6ΦΝF€]L4±xVΌςpφϋr 8ζσw;μ”θ20ΧkΕψ›#e‡ ΡDd³1φζX½8ΗΓ3 bΨO£qσΟί³e»8ω'usΌΠ{―Ε‹ζx,9Pλ•”Δ‚ΧbϊγΉ}ς3ϋŽ>F¦·)3™8μ΄θ~ΐφ}܎lAŒklZŸΛ±δD4@­W\Oίω+r<ΌAσςγhΠ¨’ϋΧo§ύ<VΰΆδ6½VΌτOO6μ2’j½%³cΒ½9.šZ±ΠΨηΛ•;jΑqΰiΉ\."2%1ξΟ±jYއPI’j·ΒΝ1ώoQ°6ΗΓ[vŒώ_δUς7Šz βΈσ£Iβ?Χ’™ρΚ(7v Ρ΅Ϋό1υα-‰8β¬θsp.Ηφά/φV‘Η ·‘(žΉ=VηϊyZ*C4@-VΈ%ώυλX·<ΗΓΫνΓ.Ž:9-…ZΏQ »(κ6ΛρK^ξΝρX*C4@-6ν‰xλωΝ«Ǟ-rύˆiDtΩ3==ΗίF²ρΤm±~uξW bD#ΤV›7Δγˆ-ω9ήΊ{|ι[‘W'χ2™8ώϋΡ|χ_ωNΌψ€'v6Ρ΅Υδ‘1wbn_—™zq’Uϋ‘ρqΨι9~=dI6žΊ9>zoGg`»D#ΤJ«WΔS·F^·ιΊŒCs^Ζ¦œL&Ύϊhή)ΗΓ—½“ŽlN_@ňF¨•f<σ'ηxl6βΈΔn«f’ΞύβKηDnwΖώ9J r:8Ž‹#Ώžcζm­n½~Q4hγαKތQ¨šIΨΡ΅ΟŠ…±quD&J’rΤ©φˆ―ˆFΉ~UΖ65oGžΩΚΟSQ’S«rΚΙι‹•€/΄v]γŒ_Ζ»―GΑζŠ>šΙD^^΄κύΏ}¬Δ΅Š ?~ζpλ KJ’€$J²Q\Gž gFΆ8Ϊw‹&Ν#―Ώ’d"Š‹cc~|°8^Ό?:τˆM"“·νkED”DID^^δΥ­‚§1jL‰…ͺ –ͺόο%©Ϋ–lqΌ=%ζ½…[>ο΄%‘ΙΔώ'DΧ‘ΙTϊs―Ω’xoVΜEEŸlέϊΡ΅ ψJΤ©WΙΛΤRξ4@­Uω»m•:"“…[’Έ¨’ίεψξŒθΌgΤ­_ι©"ο͊’’¨S e‹??b(Η3ΐΞ‘ΙΔξ=£N…υϊΥρα’\.΄ne|ΈΈSuθS‰©j=Ρμ4­;FϋžέΉΈ0ζ½%•όΕβΒxgJ%>4ΫrχθΨ§Κ–~¨D#°ΣΤ­έχ©ΔΣƒ+—ΔΚ₯•»Δš•ΈΝX·^τάΏ’— "D#°sνΦ-Zu¨Δώ ¦GQaEw.)‰EoDΑ¦Šξί’}΄λ^‰aΐΞ•Ι‹Ύ‡D½†έωΌψ轊ξΌjI,ŸW‰Iz Wt"B4;]λŽΡΆsEw.ΙΖ‚iQ\›%%±`FE—BΝd’MηhΣ₯’cπo’ΨΙκ֏ξϋUβIΒUΛ*τ˜β†U±lnEΟ™Ι‹n’~…oxπo’ΨωΪtͺΔ2ͺEρΞΛ%ΫΫ§Έ(ζL©ΔR«­:Dη~έ€rD#°σΥ­½¨ΔΝΖ•KγΓν>ΩΈvE¬XPΡ³Υ©=φ‹Œ_{r῞ΐ.Ρ¦S΄νς9χΛ[0-ύΌbIΌ;# 7WτT-ΫGϋέ€Oΐ.‘Ι‹ήE½FέύΙeTW.­ΔmΖL^τ9ΈΛ·πi’ΨUΪtŽέΊVtη’lΌϋZlύB,|= *|›±m—hΣ©’;°Ρμ*uκFJ.£ϊΡVΛ¨_Kίθςς’ϋ>n3μΡμB­;E‡>™ ν\Ts_ώΤ©ΕE1gbd‹+zΉ–’cίJ @9’Ψ…κԍžϋG^…Y΅,>XψΙΧΌ_ΉES{`ΡT€δ?£ΐΥ¦cμΦ-J*ΌŒκόi?ΑX’…―§—TέJ«έ£]χ <Ρμb™θsPΤ―π2ͺ+ލ%oEDΌΏ –ΏSα‹”.ΦΪ —(G4»\λNΡ[EwΞdbώ΄Θ_oOŽ’Š΅[ΧhmΡT€* €].―Nτά?κΤ«θώΦΔΜgcΥ Ÿ?/z ŒϊM¨’¨-w½+±Šw+ρ]­:Δξ½r €­‰F :Τ©½¬Δ2ͺ™Š}KGDΤ­=¬Δώl—hͺIΛφΡΎg%–Q­θi;DϋξU|N€ZL4Υ%½ІMͺτ”yΡηΰ¨[Ώ*Ο P»‰F ϊ΄ξϊTε Ϋu6M¨J’¨>™Όθ±_4¨’›™LτΨΟmF€ͺ%€jΥ¬Mtέ»jNΥ¦K΄οY5§ΰίD#P­ςκDUπdcέϊΡk‹¦T9ΡT·ΖΝ£ϋΎ‘Ω±_KZuˆvέ«fʍ@ Π₯4kϋα₯‹¦Φ©Wuπ1ΡΤMZDΧώΉ~Έtχ^ΡΊc•ΐΗD#P3tά#š΅ΙεΐΌ:Ρs‹¦μ$’¨7ϋζr`‡>±[Χͺž€‰F Ζθ²w4nQΉC5‹^μœiˆ@ R―Atί7ς*σϋIϋžΡrχ6’¨QΊμM+ΌŒjύF•ŽL*Ιd€š€Q³θΎod*ΆŒj—½’y۝<@m'€fχ^ΊΩΨ qτyuvώ@΅šhj˜FΝ£ηΐ()Ωή>™Ltή+šΆΪU3Τ^’¨a2™θ²χηaγ–Ρ}Ÿ]5@­&€š§nύθΎodΏ¨d2Ρ©o%ΦΛ`ˆF Fκ²W4Kda£fΡ΅EΛ`LjF FjΨ4z άvvMZξςj)ΡΤT»χŠζ»}vcƒΖΡm@ς“«T5ΑjͺFΝ>»Œj&]ϋG£fΥ7@­#€¬σžŸz²±IΛθ6 ϊ¦¨D#PƒΥ©=ώϋΓ¨™Ψ½O4ρ݌»”hjΆ.{F‹vMZDχ},š °‹‰F f«ί8ϊm:EΏ#,š °λeJΚ?\Pe‹cΣΊhΨ,κΤ­ξQjΡ@’§$H$‰F’D#I’€$Ρ@’h I4$H$‰F’D#Iu«{ :½4q„I“–.[Φ¬i³ή½zž:μδV­ZUχPΫ–ŸŸψc6lpΪπαε·Ώ>sζ›³ή*Ώ₯yσζ{ξΡ·w―^e[–.[φό‹/rЁε7P’j―KG\1vό“;tθά©γ{K–Ž{κ©›φχ~Ν‡VΊΓΈ'ŸšρΖσί?­ή9K=1nό5Χ]ϋτΠ§χ'νχ℉·έ~ΗΦϋ?iθ/‘Ιd"bήόΧ\wύ^φsΡPY>ž ΅Τ¨'ƌδηwμΘGφΧ›FώσΑ‡ξΉ»MλV—όόςυ6”ξσΪλ―?ςΨΘꝳΜΘQ£;vΨ="F³υ«άߌ—'ΟxyςΤ‰/=|ί½Η|ωKύθγ£vω˜iD#ΤR“_~₯Q£Fηž}vι½ΈˆθΣ»Χ7N?}ύ† sή~;"6lάXPPωωω………₯[ΆlΩλΦε/~oIΩ©Š‹‹ί|λ­•«V•?ϊ Κβ³T6›ΝΟΟ/((ΨώΫ4oώόY³gŸχν³ϋυν;fάψl6ϋ™ςκδ•jPΏ~ί>½―Ίό²ˆ˜ςς+•ψ‰°-’j©’β’M›6½ωΦ§žόΪ©§ά{Ην½{φŒˆσΎΑ#=ΆeΛ–#Ž9Άτ–έ7Ο9ο7Χ^χ§›ώϊΥ!'ήΠC±aγƟώΟϏ<ζΨožsήWŽ?aθ©§=τΘΏJOυσ+<ϊΨγΦ­Λ/;ωγOŒ9β˜c§Ο˜±ύ·iδθ'κΥ«wό AƒτΑ‡N6mϋο°°(//oΣζMΉόh(G4@-5θ˜c"β’Λ.ύnxiβ€υλΧGD“ƍτί»eΛ–qνo~5ψΈA κΧψΎ{tlιQ/M˜xƒrςI' 7ώυζ—&NΊπ‡<;nΜCχάΥΎ]»_]σ»?ϊ("N>qHQQΡs/ΎPvΕqO>Ωaχέ>πΐνΈ΅βββΡcΖuΔα-[΄|ά ˆ5fμvήZ6›½η²Ωμ>ύϋWŏ  V³ΤRƒΎzΜΥW^qλίοΈϋΎϋοΎοώΌΌΌΎ½{Ÿ<τΔ“O<±yσfΡ΅K—V-[F&Σ·Oο²£>ό裑|°Gχξ₯¬W―ήwΞωφYgžmΫ΄ωφYߜ:mϊ» νΦΆν—Ž:²yσfO=σμ°‘C#bυκΥ/O}υ»ηž“——·ύ·uΒ€Ι+W­:δ„ˆθΨ‘Γ€ώ{?ύμ³#.ύYΓ† ΛφΉζΪλ›6i[ ΆΌ1σΝ΅λΦuνω›gž±³~|΅†h€ΪkΨΠ‘Γ†]΄xρ΄ι―½:ύ΅g_xώwΧαΞ{ξ½οΞ;Άo±χž{–cD\όγEΔβχ–,xχέ₯Λ–5:"’€$"κΧ―?xΠ G΅~Γ†¦Mš<υμsΕΕΕΓ†žψΉnmδθэ7ګߞ­\GzθΜ7g=ϋό C_ΆΟϊ ²Ωβ?θΐϊυν{Φ7ΞlΈρύ€Pke³ΩL&“ΙdΊuνΪ­kΧS‡ΫΌyσŸzsιΗ¨ΫΪξνΫ—γΤiΣoΊεΦi―½Φ΄iΣέ»uλΪuΞάΉe―žt␇ωΧ /½tβΰΑγž|jϋuιάΉ"–·fνΪ^šPXX8ψδa巏3Ά|4^}ε½zτΘνGΐvˆF¨Φ¬]{τ±Ηuζ—ώτⲍ 6Όΰόο>ψπ#ο.\TΑσόςΧΏΙf³zΰΎο?œ8yςψ§Ÿ.{uίΊvιςΤ3Ο|ΐΣgΜΈjΔε<°Ό1γΖ^~ι%]:w)ΫxΗ]wO~ωεVlΫ¦Meή7•f!¨ZΆhΡn·έžρ₯-εΎ#"Φεητθή­"'™=ηνE‹ŸύΝo”†_DΜxcζgφ9ωΔ!'M~τρΗ4hpάWΏZρˌ=Ίc‡έO?ν΄Γ=€μŸΣN–Νfǎ²‚ο€œ‰F¨₯.ώρ–.[vξωίqΒΔeΛ—/\΄hΜΈρηπΏ7nTφ±Ο6­[<χΒ‹kΦέϊ ½{υlέΊΥ“O?³tΩ²₯Λ–ύγξ{ξΉοώˆXΉjUqρǏ2€ °πΆΫοτΥc7nTρKΝ}gήμ9o<ΈμΛ$K}ιΘ#6lΈύ5T¨’j©!ƒΕ#ή_±βΒ‹.|ςπ“Ώvϊ\ρΏ­[·ϊλΨ―oί}Žως—Ίtξτύχ%O>ύΜΦg¨W―ήO.ΌpΡβΕ' ;ε„a§<σάσήsWο^½.qEΩχ(vμ°ϋϋάRP0ό€‘•:°ΤΘΡ£#bθ '|ζ5:ϊΘ#ζΌύφό ͺπgΐΦ2%‰eΚ€Ϊ`KAΑΒE‹–,]Ϊ¨a£]:wξΤiλ}ή_±b·ΆmλΤ©³Ν3d³Ω™³fuιΤΉuλV₯\ΈhqΧ.λΦύœ₯r>€]I4δγ©$‰F’D#π)oΞ™·vέϊꞒ™:cΦΤ³ͺ{ €j#€Oy`δψΕΛή―ξ)jω‹–Μ_΄€Ί§¨6’€$Ρ@’h I4$H$‰F’D#I’€$Ρ@’h I4$H$‰F’D#I’€$Ρ@’h I4$H$‰F’D#I’€$Ρ@’h I4$H$‰F’D#I’€$Ρ@’h I4T·Ί¨E&Myωέ… Ώ~κ)υλΧ/Ώ}ΦμΩ3^γΈcΏΊ[ΫΆΟ½πβ²εΛSg8όΠC.ZΌύztοώϊΜ™oΞz«όφζΝ›οΉGίή½zνψ»jΡ°λŒ3vτΨ±'Ÿ8δ3Ρ8εεWώψ—›φΪ³ίnmΫ>ψΘ#“&OIαWW]ωΔψρΫί‘Gχξ/N˜xΫνwlύκπ“†ώβŠ™LfGήP«ˆF€šεΧώΎΈ¨ΈτίoΊυΦ»ξ½οή;nοΥ³gι– κ7θΨνοPvͺGξΏ―WΟQXT΄hΡβ›n½υ±Q£χΫwίS‡Όλήπη™F€š₯Aύϊ7*ύ§^½zΡ°aΓ²-uκΤωάΚN•W'―Tƒϊυϋφι}Υε—EΔ”—_©·|‰F€Ϊ’°°(//oΣζMΥ=πEβγ©»ΪΔ)S7j\~Λ»‹νμ‹f³Ω{x ›ΝξΣΏΞΎπŸD4μj?»lΔΉΠ5Χ^ί΄I“ˆΨR°ε™o]·k—Ξί<σŒ]suΰ?ƒhΨΥξΌνΦΖM>u§ρ±ΗGέϋΐƒU~‘υ6d³/™sЁτλΫχ¬oœΩ€mκ» IDATqγνPžhΨΥzχκΩ¬Y³ς[ΪΆi³3.tυ•WτκΡcgœ¨=,„@’h I4$HΚ”””Tχ @ 2⚿œ1μψύzWχ 5Ε#ΗGΔ™ΓŽ―ξAͺ‡;$‰F’D#I’€$Ρ@’h I4$H$‰F’D#I’€$Ρ@’h I4$H$‰F’D#I’€$Ρ@’h I4$H$‰F’D#I’€$Ρ@’h I4$H$‰F’D#I’€$Ρ@’h ©nuP£5iά¨ό·lΩ2kΦ¬>ψ ??ΏΊFϊβjΠ A«V­φή{οΦ­[Wχ,@E‰F€ν9iΠΡ±zυκ»ξΊλΡG8qbQQQuυ…Χ·oί“N:ιΌσΞΫ{ォ{ΰsψx*ΐφlάΈρͺ«κάΉσW\Ρ΅kΧ;οΌsφμΩλΦ­+‘ς6oήΌdΙ’±cΗ>|δΘ‘ύϋχ6lΨΌyσͺϋ/؞LIIIuΟΤ #ωΛÎΠ―wuR#<ϊθ£?ωΙOΦ¬Y3bĈ .Έ Y³fΥ=ΡŽ’’’qγΖύμg?{ηw.Ίθ’+―Ό²aΓ†Υ=° ξ4lCIIΙe—]vΪi§}υ«_;wξ%—\’«V&“9α„^{ν΅k―½φζ›o>ζ˜c>ψΰƒκ ΨΡπY›6mϊϊΧΏ~έuΧέqΗ·ί~{ϋφν«{’XuλΦ½πΒ 'Ožόα‡rΘ!³fΝͺύŸ’ΝfΟ>ϋμηž{ξ駟>ηœsͺ{œZ‘_Ώ~S¦LιάΉσ Aƒή{ο½κψΡπ)#FŒxόρΗ~ψᣎ:ͺΊg©EΪ΄i3f̘Άm۞|ςΙλΧ――ξq€OˆF€Oόλ_ϊνo{Ϋm·}ε+_©ξYjf͚=ώψγΛ–-ϋώχΏ_έ³ŸΫΈqγE]tξΉηϊTjuιή½ϋwάqύχ?όσΥ= π1Ρπ±kΉfυκΥΏϊΥ―ͺ{ZmȐ!C‡½πΒ ‹ŠŠͺ{ B4”Z½zυ΅Χ^{ΕWtθΠ‘Ίg©νΏώϊΉsη>πΐΥ=!JέuΧ]uκΤΉΰ‚ ͺ{’OŸ>Γ‡Ώε–[ͺ{ B4”zτΡG‡ή¬Y³κ„ˆˆo}λ[“&MZ±bEuˆF€ˆΝ›7Oš4iπΰΑΥ=;φΨcλΤ©c9¨ D#@̞=»°°pΰΐΥ=kάΈρ{μ1sζΜκΛ—/ˆ.]ΊTχ |’sηΞ₯/@υ±aΓ†ˆh€IuΒ'š6mΊ~ύϊκž%%%‘Ιdͺ{>‘ΙdJ^€κ%H$‰F’D#Iu«{j»όόόǟΣ°aƒΣ†/Ώύυ™3ߜυVω-Ν›7ίsΎ½{υ*Ϋ²tΩ²η_|鐃,Ώρ3^š8iΒ€IK—-kΦ΄Yο^=Ovr«V­ͺό]TĚ5kώtΣ_/ΏτguκΤ©i³νΈω ŒκιχƒοWχ T1Ρ@5{bάψk»>"φι? OοOΪοΕ o»ύŽ­χ~Π_\1’t•Λyσ\sέυ{ΩΟSΡxιˆ+Ǝ²c‡;u|oΙqO=uσίώ~Γο―9β°Γvλٞ?όωΖF•cšmΗυθή}άSOο΅ηž_>ϊ¨κž€ͺ$¨f#GξΨaχeΛί5fΜΕ?ώΡg^}δώϋzυμ…EE‹-ΎιΦ[5zΏ}χ=uΨɟ{ζQOŒ;ώΙ ΞξΏ½Θ|gήό]|ρ%?ΏόΙ'F5έ΅ίΘχζ¬Y£ΗŽσΨΏjΰlU"//ο{ηϋΫk―=τΰƒ6lXέγPe<Σ@uš7ώ¬Ω³ΟϋφΩύϊφ3n|6›ύΜyuςJ5¨_ΏoŸήW]~YDLyω•Šœ|ςΛ―4jΤθά³Ο.ϋς½>½{}γτΣΧoΨ0ην·K·•^tνΊuoΜ|sΣ¦M[Ÿ§¨¨hΞάΉΛί›Wω𣏦Όςʚ΅k·?̍·άϊ壏jί]gΫ°qㆍΛί΄iS~~~ω‘ςσσΛΈŠ‹‹ί|λ­•«Vmσκλ7lX΄xqjΆνΏšzϋ6nά²eKD¬[—Ώψ½%qό c7nΪόΘc#S§ΰ‹ΘFͺΣΘΡOΤ«WοψAƒ6lΨxÍ™:mΪ!΄ύ ‹ςςς6mήFΪm­¨ΈhΣ¦MoΎυΦΑPΆρk§ž²~ϋuιά©τϋvΔwΞωφΌωσ'LšœΝfΤ―ιτk§|ςtε½~~ΙχλΫwλIV―^=iς”ί\ύ‹ŠΟvω•W½2υΥ Ο>——η|οsή~ϋαϋξνΫ§wDόγž{oΊεΦ—žyͺIγΖ6nόί_^=iς”ΘμΪ₯σ·Ο:λτΣN-=η–-[~uΝοbL6›mίέ]πƒλψ§οœ{ΞΉί:+υjΗn}"Žά·ΝsΞΈίΎ­[΅ΊϋΎϋΏvΚπKzqύϊυ8παG;λΜ3*ςΐ‚;T›βββΡcΖuΔα-[΄|ά ˆ( °”l6{Οd³Ω}ϊχ―ΘωsLD\rΩεΏΓ /Mœ΄~ύϊˆhΈρ€ώ{·lΩ²l·;οΉ·qγΖήsΧ}wήqЁώςΧΏ™φΪk₯/έϋΐƒΧ\wύΰAƒξΏλχήq{‡»χ‚Ύ3o~ι«Χ\wύγΖ_φ³Kžύψ―qΥ₯Λ.½όŠmNRzH·.]*>ΫQGΎ~Γ†9oύˆόόόΉοΌS§M+=όεW¦ξ?pΏ&Gč½ω₯‰“.όαώσΠ=w΅oΧξWΧόξÏ>*έσΧΏϋύγOŒωɏ.όη}χόδGυ‡?ίΈ.?Ώδί·(·ωj””TδνGΔK&ήΰC§œ|IC†”nιή½Ϋό ή_±’"A|!ΈΣ@΅™0iςΚU«†9!":vθ0 ήO?ϋμˆKVώ‰Έk½ΎτΏ-[ޘωζΪuλΊvιό͊έΘτΥcΎςŠ[~Ηέχέχ}χηεευνέϋδ‘'ž|β‰Ν›7+Ϋ­}»vW_ωΏ κ׏ˆίύκꆝςχάuΐΐ7nϊλm·ν·Ο>W_ωq ώρΪί}μq?ϊθΟ/ωοχ–,ylΤ话zΚ™_ZD |όκ5k½αK–.νά©Σg&ygώόˆθάΉsΕg;ςπΓ#βΥιΣχΪ³ίτ3κΦ­»χ^{Ύ:mϊYgž±₯ `Ζλ―_ψΓ JOU―^½οœσν›{mΫ΄ωφYߜ:mϊ» νΦΆν{K–ŒύΔΉg«τΎβ}ϊδeς.ρρΫΩώ«Ϋϋ₯[>ό裑|°Gχξeο«G·n1ύ΅C_‘Ώ#j>Ρ@΅9ztγƍφκ·ηG+WFΔ‡:σΝYΟ>BωήXΏaC6[\ϊοx@ΏΎ}ΟϊΖ™₯wΨ*bΨΠ‘Γ†]΄xρ΄ι―½:ύ΅g_xώwΧαΞ{ξ½οΞ;vkΫΆtŸ}χPZŒΡ΄iΣ=ϋυ[πξ»1oώόuλςχμ·Η+―N+;a—ΝfΞz+"fΟy;›Ν8p`ΩKgyFκc™οΜ›Χ¬Y³–-ZT|ΆφνΪυνΣ{Ϊk―•FΰΎϊqΨawάuwIIΙλ―Ώ±₯ ΰ¨#Ž(=OικA‹ί[²ΰέw—.[6rΤθˆ(½[8ηνΉΩlφ°C.»θΡGYφοΫuϋoΏΤή{ξYΎ#’{·nρΑΏοsπ@4P=Φ¬]ϋΒK Ÿ<¬όφQcΖ–Ζ«―Ό’WΉ]"›Νf2™L&Σ­kΧn]»ž:|ΨζΝ›όΧ›Koξ•­ΤΪ΅ά§F#’c‡έߚ3;"–.[zlδc₯φo];wŽˆeΛ—GDϊΝφξ’E»΅mSΩَ:βˆ‡υhIIΙ«Σ§ωθ£=ψΰnόΛάyσ^~υΥN;φμΡ½τTS§MΏι–[§½φZΣ¦M{tοΦ­kΧ9sη–ΎTΊzMω;Ÿ5ͺ[·nE^έώΫ/΅{ϋφŸy§νΫ·‹ˆ‚-[*ςcΰ A4P=ƌ_XXxω₯—tιόI³έqΧέ“_~ω£•+ΫΆi³c+bΝΪ΅G{άYgžqιO/.ΫΨ°aΓ Ξξƒ?ςξΒEeW―YSώΐ•«Vuνά%"Z·n?ϋιΕ_?υ”­Οί’E‹ˆX²tiιΚ4±~Γ†…‹υμή£qγFŸΩΉ~½ϊΛ–Ώ_RRRΊVjg;ςπΓώώ;gΌώƜ·ηώμ’‹ϊνΡ·yσf―N›ώςΤ©GqxفΏόυo²ΩμΏΈ―τΛ*'Nž<ώι§K_jΥͺUDΌ»pa§ŽK·Ό·dIQQQE^έώΫOY±βƒˆθΩ3ΗΞ ²ΥcδθΡ;μ~ϊi§~θ!eœvΚ°l6;vό“;~ώ–-Z΄Ϋm·η_|iKAAωνλςσ ztοVΆε7f–}wE~~ώ΄ι―υθΡ="zυμ™——χ℉e{nΩ²ε[ίωξ7ί{φΫ#Κ­LΉω–oχέΒ’Β­‡ιέ«ηζΝ›W|πA₯fΫoŸ}š6mzΫ¨_Ώώ€ώ{ηεε|ΐΟ½ψβ›³ή*ϋlκμ9o/ZΌψμo~£΄#bΖ3ΛNXzͺ)―L-Ϋςμ /VπΥνΏύ”ΕKή‹ˆΎ½{ogΎXD#Υ`ξ;σfΟy{ΘΰΑeίRXκKGΩ°aΓν―‘ZqψGK—-;χόοΏ8aβ²εΛ.Z4fάψσψ_7* Ψ9sηώζχΧ-~oΙΒE‹.ΉμςΒ’’οwnD΄mΣζ§}”)#GΞΟΟqΒΔϊΙEo͞sΜ—ΎύϊφύςΡG=φψ¨§žyφύ+ξ蟏<6ς˜/©Eσζ[Orΐώ#bα’OnoVdΆ:uκ~θ!&MΪoŸ}κΥ«‡|Π+S_­W―^ΩuτξΥ³uλVO>ύΜeΛ–.[φ»οΉηΎϋ#bεͺUΕΕΕύχΪλˆΓ{θ‘]{Γߘωζ?ξΉχΦΏύ½l†νΏΊύ·Ÿςή’₯ 6άz) ΎΈ|<€j0rτθˆzΒ ŸΩή¨Q££<βΙ§Ÿ™Ώ`ΑŽ_eΘΰγ·όι/7]xΡ'ŸΠο_βͺςί¦8lθΠΧίxγΑ‡Žˆ¦MšόbΔε₯«ΉDΔO.ό―‚‚‚+~qυquDtλΪυΏ»f―=ϋ•Ύϊλ_\uΩ•Wύτ~^ϊΗ/}ΤU—_ΎΝI8°N: ->τΰƒ+5ΫQGώδΣΟ”%β!x@ƒ J·Τ«Wο'^ψη›ώzΒ°S"bίΌηŸ\rι₯#hΥͺε‘όΫϋεο»ώΑ‡ΉϋΎϋ»uνzνosΑ~\Ά’Π6_-{¬qϋo›.ZΤ§w―ο–ΰ?C¦δίίΕ‘Ά,\΄hΙ₯6κΪ₯σgn‚νsΠ!ηŸwξί—,]ϊΑφνΣ»iΣ¦Ÿ9ΓΚU«ζΎ3―Q£†φή»N:Ÿyu͚5‹ί[‘ΓξeΛ±nΣΟ.±n}ώΝϊcΕg«Έl6;sΦ¬.:—>ˆ˜Νf.Zά΅Kη²όΛf³›7oiάΈΡϋ+V7τδ;vλΐ}χ-xωWόΚΰ~ημύzWδν—·yσζcN8ρβθk§ ΟνT£‡zθŒ3Ξπ{Qrϊι§GΔC=Tέƒ@mηN#αΤ―ΏGŸ>{τι³ύέ:wκ”jΆ6­[—^ŠΟhΩ²eΛ–-?wŒ|ο»§Ÿuφ’Ε‹»uνZΩΩ>W^^ήΎ”cιΪͺkΦ=σμsΎvΚπσΟ;·tyžQOŒΙd2}ϋτΩΞ«Ν[΄*ςνΏύςbL«–-‡ =qί5Šh€]‘WηœυΝ[~ϋ―~qΥ.»hΛ-φκΧο¦[o›>γυ}τŸ;oήΣΟ>wρTϊρΤΤ«ο,]σΉgήZqqρνwή5β~Vϊψ%1eΔ59cΨρeΟ4Φži¬<Σ5„;$‰F’D#I’€$Ρ@’h I4$H$‰F’D#I’€$Ρ@’h I4$H$‰F’D#I’€$Ρ@’h I4$H$‰F’D#I’€$Ρ@’h I4$H$‰F’D#I’€$Ρ@’h I4$H$‰F’D#I’€$Ρ@’h I4$H$‰F’D#I’€$Ρ@’h I4$H$‰F’D#I’€$Ρ@’h I4$H$‰F’D#I’€$Ρ@’h I4$H$‰F’D#I’€$Ρ@’h¨νFϋ℉Υ=PC‰F€Ÿ½ϋ―£:σώΎηΜά¦^,χήmlά€0¦„ ΄ 8$$lv³ΩΝ²$!! Ιnψ₯Z(B5n@Œ+ΕΖƒ»-YΆ%]]έ{gζœχχΗ•eIφ½–lcΩχσΜσ «9sζΜπ<’Ύ> ΰΨυς«³ξώω½Ÿτ]ώtΟ<ϋά'}8N!4P$!’t:έΡ iνχή{jζ³݊Ž‘L&£ΡhG·ˆΚΚʈ¨ΊΊϊπ« ‚ΐZKD΅uuΛWΌŸL&›ŸM44d’i]]|Σζ-Mί―©©Y΄dΙφ<―yaΟσ‰(ϋΎŸ»p“ΥΥ /©­έTξ Ϋϋ8‰††ϊϊϊζ…­΅ρxΌν•ηV]]]ZZzDͺ€Γαtt:ވ#ˆhŊ½{χ>Μͺ&L>ν 7\ΏfνΪ7Όe­ ‡B·ύχ7.Ώτ’ΜΩkn˜1~ά‰₯%%=ςθε—^rΫ7ώ+νyχόόή'Ÿ™™)Π­ΌόŽosκ”)D4γζ―|Έz5vφ§ΏσΝΫόάe9 Q2™όαέχ<βKD€”:qμ˜oέϊί#† #’άΪγάσσ{Ÿ{αΕY/<ί­Όύμs—LΏθ³]TXX0lθύΩO‰θχϊ υλΫ·€Έ˜˜‡ RTX˜»πζ-[f>Λ/»τκ+.οΡ½ϋ΄σΟ›qύΏlΪΌyKeeξ ωq&MœPΡ­Ϋ¬Ω³3ΕφμΩ³tΩΫΣΞ?O©#πζ“O>ΩΏ±cΗ~Up˜ˆˆΎπ…/¬]»φ₯—^:όͺN;& eΎΞΟΟ9bΔΊυλ›ΞŽ9rΰ€™―ί_΅ΚZ{Δ‰Mgc±θΑƒΧ¨0wα>\m­4~|ΣΩk―Ύκ… ϊτξέ»΄ύq”RΣΞ?oΩ;οξή½‡ˆ^›;ΟZ{αωη·₯ΞάξΏώ3f~UpψˆˆF=}ϊτΫn»-‚Γ¬ͺ_ίΎΝ?φκΩ£>±oΑ˜έ»7}½uλ6":aτ¨ζε+Ί•ΧΤΤμ_mξΒ[·m#’AΆχΒΓyœΜΥΧζΞ%’Y³η 2xΨΠ!m©3·»οΎΫσΌ[nΉεπ«€Γ‡ΠΠθ—Ώόεš5k~χ»ίf={Z†±]»wχλΣχ€%KJJˆhύ†ΝΏΉκƒ{φμΡήΒEEED΄₯²²ιT}"ρώͺU Ιvέ₯]3l萑CϚ=§¦¦fρ₯]pA[*ΜmΣ¦Mχή{οwήYQQqψ΅ΐαChh4xπΰ―ύλίύξw?όπΓΓ©gως™m*ˆ(/{ϋ°δ°!CˆhΡ’%Mί©άΊ΅zΧqšΛ—»πΘΓ‰hΙ²eMgύ»ί_7γ&?πΫu—φ>ΞE\°dΩ²'g>KD^pΈcS}ߟ1cFΏ~ύώνίών0«€#‘`Ÿ;οΌsδΘ‘Σ¦MΫΉsη!WςαGύδgχnΪΌeΓƍ·~ϋv?Ύ4γΖ–5rΔi“'?=σΩΩσζ%“Ιυ6|ηο‡C‘»6S ¬΄ΤσΌΉσ_―©­Ν]xΔ°agž1eζsΟϚ=gϋŽ>ρχ§f>{φ™S‹ z—ζ~τ±‹/Ώςύ•+Ϋψ8œχωύτ©“OͺθΦν_ZΖΧΎφ΅Ε‹?φΨcλfUp€`ŸF€}"‘ΘΜ™3O9ε”Λ.»μ…^(,,<„J>{ΡEο-_ώψ“OQ~^ήχΎsϋ€ώύ³ώρχξό֝wύη­·e>Vtλφ§ίύ¦)}}ζΤη^xα?ώϋΦΜ>Ή ψ{w}ϋΞ»ΎρΝoe>žyΖ”»nΏ½-winOMΝ†Sιt§Gχξ'Mœ°xι²Ο]rI{_T+?όαπ‡?̜9σΔO<Μͺΰβ#²4tίΉηΧW}φΌ1#Žΐr&Η―•+Wž{ξΉeeeΟ?ό€½+ΆΡΨ“NωβŒΏφ―·l©¬¬ͺΪ9lθόόόƒ^΅uΫΆυ6τκΩ³Ώ~ϋoY±}ǎnεεZλΆ©©Ω΄yKϞ=Ί•—·λ.‡ό8?ψΙέsζ͟υΒσŽsˆΎου«_ύΛ_ώrί}χaύ€c zZ=zτ’E‹.ΎψβSN9εώϋοŸ6mΪ!T§wο>½{·±p―ž={υμ™νlσWZΈΈΈΈΈΈψξ’[ΆΗI44Ότκ¬kΊςγ† nΊι¦Ε‹?σΜ3Σ§O?΄Jΰ“ƒ9Π·oί7ήxγœsΞΉπΒ §OŸώρΗwt‹ŽEρί·^7γ&"ϊ—k>—744άqΗ£FΪΊuλ›oΎ‰ΔplBh8°όόόGydξάΉ7n=zτW\ράsΟ544δΎκ‹§5ςθ΄π(Θύ8Zι Ο?οω§ώ^Ԟɟ"²dΙ’Ϋn»mΐ€ΏϊΥ―~ψΓ._ΎσŽY˜Σ-`Nγώ‚ xμ±Η~ϋί/X°@k=|ψπή½{Ϊ9]\*•ΪΉsηͺU«κκκϊχο?cƌ[nΉϋ1γ „ΖvμΨ1oήΌχή{oǎρxΌ£›sp‘ΌΒ’ξ}Ά­[ΥΡ i‰DJJJF5yςδ±mΫ%:Βh«ξέ»_uΥUW]uUG7€­V|Έζρg_ωΏ'žθθ†ΐq s +„FΘ ‘²Bh€¬ +„FΘ ‘²Bh€¬ +„FΘ ‘²Bh€¬ +„FΘ ‘²Bh€¬ +„FΘ ‘²Bh€¬ +„FΘ ‘²Bh€¬ +„FΘ ‘²Bh€¬ +„FΘ ‘²Bh€¬ +„FΘ ‘²Bh€¬ +„FΘ ‘²Bh€¬ +„FΘ ‘²Bh€¬ +§£p”€ΣήΞέ5΅ρϊtΪ ΣΡΝ9¦­Ω°9™Lwt+ΰ•―¨ϋ IDAT¨άQED‹ίYΩΡ 92G‡Γ‘’‚όn₯Εαp¨£›ΠU 4@''"[ΆU­ίTΉ»¦Ž‰"±X(RΏsρ,%tt+ΰπ,už›ΦKy{κΦ¬Ϋ$D₯Ε…ϋυξΣ³‚™;Ί]~e@gV½»ζέU%κJΚΛ‡]X\¬΄ξθFλf߳͞wŸ!CwtCΰ`wνŠ«†ŽΥΡ 9’¬1u55Υ;vΌ½όƒΥλ6Ž5¬Ό΄Έ£Π™!4@ησΊΥ[Άν()+3it$νθΐ‘‘΄..++.+K5$7­[σΖ’wϊτμ>~Μp$πΙ@h€N(™J/XΆ<™τ†S\ZΪΡΝ€OD$v˜šέ»Χ}ΈzώΒ·O86 wt£:!¬ž M]}bή‚₯‘ΡΖ#1tzΕ₯₯£'ŒŒΜ[°΄>ΡΡΝθ„ SI{ή‚₯ΛέHtΤψqαH€£›GC85~œ‰.XΊ<νyέ€Ξ‘:cνΒeο[‘‘£Gλμλ£ώό'?8κδGΌ?wm?πη›»κH·>Z;ΓNMΜo-]n φΤ8’ σψπγ u‰Δπ±c\ΧΝVΖK§̟§΅žχΪ¬OΊ=ίΞmίϊ――}w€ Ηq‡žpBΌ!ωᚍέ€N‘:‰DCrΝ†Ν}ϊŒFc9нυζΙTςͺλnΨΈqύϊ΅kŽZσΰ(ˆFc}ϊX³as}’‘£ΫΠy`υTθ$–°&Vτκ™»ΨμY/3φ’Ο]ρΨC̟=kΰΰ!ΝΟ>ωΨίή|}M>mŠXΫτ}Ο=ύδ’E 6oΪ4bτθI'O>oΪED”hHάσύ;Ίζϊ… ή\ψΟ7΅ΦLμg?wύοOόώςε֚۾ώΥ;ς³X4Άdα[O?ρθ–Ν›zρeWN˜tR¦ς»Ύ}λ}ΆΊͺκΩ§ώƒŸώ‚ˆ_}ρω7ζΝρ±½ͺΊο A̜£Xm͞e‹uΞΉ…EΕcƍŸ7η΅ζgψ›ϋώό»_4xά„IO>ϊ·ηž~²ιΤ_ώπΫώόϋ={έxσW"αθ/οωΡμW^""‹ίZπ£ο}gύΪ5—|ξŠΑC†ώφWΏxκρGˆhτΨKJJ ‹ŠNϊΤ©Žv^{υε;Ύω Οχψόuαpδ»σŸo̝“©|εςε>όΧ_ί½έ{hGλ_[ΆtρΧ\7υμO/[ΊθΗw}χΘΏ/€Ξ‹™ϋ΄½ͺ+©)θi€Ξ`γ–m‘hτ l̟3[DNŸz61υμϋ~ω³WΎ?bτ D΄«zηΜ§žΈα‹_ΎϊΊˆθΚΟ_{Σ΅W‰HζΒχWΌwΕΥΧ^7γ‹Dtφ§Ο{ω»ο/οœσ.Ȝ-++ώ=χ*₯.Όδ²ΐ<ψΐΕ—^~ξωΎυζλΙ†δεW_›N₯ώςΫwΖΩη|λŽΡΕ—]ώΓ;ΎυπžrΦٍν_ΏφO=ή«OŸυkΧlέ²ω¦―|υ΄)S‰hΒ€“zΰOΎηΉ‘Π'ρή:₯βH4ΊiΛΆF 9xi8τ4@g°­jWiyωA‹½φκΛγ'N*.)!’ΙgLUJΝ›ΣΈΞ’E ML›ώΩΜΗX^ώιgžΥtαύφOΧΝψ’οy[6o|}ΞμΊΊ:?π›Ξž6υ,₯₯N9σœD}|ΫΆ­Νο»ϊΓvοή5eκ9‰†Dζ˜p§6nX·{χLΙ§žΡ«O"*)-ӎσΐ~χΖά9 ‰ϊ'Lόω―~‹ΔΠ^%εε[«vut+: τ4ΐqΟσύD’‘Ο ƒt)lέ²ω£VF’Ρk.›ήτΝΧηΜΎω«‘”ΪΎmk8),*n:UVΆ/…ωhυί{ψΝys1}ϋ pœΏ@{υκ½ολή½‰¨fϞ~ύμ»uε"ϊαίjΥ€ϊxΌ΄΄Œˆϊ ”ωNqIΙ]?ώιƒώÏξΊ])uΒΨq—_}νΙ“OmΛ{€&…Ε%Ϋ6oφ|?”}-eh#„F8ξΕλˆ(–—kΡT"šύκˎλήψΕ―4Ν{άΈaέ‹ΟΝ\ώξΫγ&LŠF£ιTjwuuSεϊuk3_XkΏo—–ύδχ ><}ν+_dΪ7yσχυ:ΦΗγDΤ­’’ω­c±ύΰ§Ώ0pPσοgc+'2ω€S&oή΄aιΒ…―Όψ»Ύ}λoώόΰ€AƒΫτ.€ˆφώ@¨―o(-)κθΆχ0<Ž{žηQ޽3ζΌφκΔ“NΉδς+?ϋΉ+2ΗM_ώWν8σfΟ"’~ύъχήm*Ώβέw2_¬\ρ^Վν7}ω_ǎ‹ΖLlήΈNHšJύθΓ}W½χγΊέ{΄XΔ΅oΏώD΄qύΊnέ3ΗΚεΛϋ«_°jύ‹xω»οάυν[­΅}ϋ ΈτΚ«ΏwχΟ­΅k>Zέώ·Π₯e~ €›ύƒ2„F8ξYk‰hΦά+Wl«άrΦ9ŸiώΝΌό‚ρ&½9nΰϋ'O>΅wί~>όΐΦ-[ίκρGjkk2Εϊθ†BK.π=―rσ¦ίυt*N§šκy~ζΣo½ω†ηyΛ–,~κρG/ωά•™)Žωω[+7o«άΰ ‰'όΒs37mܐN₯^|nζoοϋeqI©Ϊ―ΝC†}{ι’ϋπΫ]Υ;}Ο›;λef4tθyQ]Gζ‚5φ %ΰ 0<Ί„Ω―ΎŽD>uΪι­Ύϊ™g/]ΌpΩ’E§œzϊwΏγοέ~Ϋ½B)5hΘΠkoψΒμY―QaQρΥΧέπΤ£{ζο)­―όόu'œ8ώΏω՟~sί•Χ]OD\tρ½w Q_ο†B“OŸrύM7g*?γμO/\πΟΧ\ρχΌςo~χξάqσυŸΧZ‹Θg.ΈπK·|mvΖςς―Ή~ΖƒώΓS?βΈ΅φ†/}eΠ`„Fθ0ά΄˜8ΐqͺr[ΥβwWž2uκαWe‚`νڏ£ΡhŸΎύ[mω˜N₯6n\?pΰΰΜZ¦»««Γ±¨ ‚+§ŸοδΤ)g¬[σq―Ύ}cΡX« λβu%%{l«άR΅cG―>}ΊUtΟьݻͺ7oΪΔΜύϊΘ¬υz4ύζΎί~ζόΟ ŠY”ΑšΧΎϊς«ϊο·ttC:ΐ’ωσO7Ίwϊƒ€œΠΣ°vœaΓGπT8i~*³^N]]mζ£RjΘ°α¬°)1QΟή}zφξsΠf”–•—–|€£s +„F€C‹ΖnΏλG#Fκθ†|R0<ΰΠ9;嬳;Ί‡kΧ]­™¬«­ΫY΅³ιcqq±Βι]B#@W·xα’υλΦ7ΞΒ·.|«ρkGλΏxγΡo#0< «:t`AuVά·ΏP(t΄ΫΗ „F€nࠁΪuˆψη¬ q€Ua λ@hθκ΄£ €Τώ*ΠΣ―ί£ί$8v 4 6T¬mυM₯Ԑ‘C+ ti@}ϊυ …Γ­Ύ)Φ6΄CΪΗ„F ₯ԐaCtΛͺ‘pΈWο^Υ$8F 4ΡΠaCL³ͺZλa#†p’#t)ψMDD={υΚΛ‹5}4Ζ 6€ΫΗ„F "b’aΓ‡7u-ζηηUτθΡ±M€cB#4:|ˆ΅–ˆ΄VΓGŒ8ΠΎΠε 4@£ςςςΒ’B"2ΖΑΨT "„FhnΔΘDTRR\VVΪΡm€cB#μ3tθ"6rxG7Ž°OQqQEξ™θ@ΠΚ駟ZXXΨΡ­€cB#΄Π£'vΪ€} +„FΘ ‘²Bh€¬ +„FΘ ‘²Bh€¬ +„FΘ ‘²Bh€¬ +„FΘ ‘²Bh€¬ +„FΘ ‘²Bh€¬ +„FΘΚιθ Љˆ¬ͺ˜„ˆ…ˆ‰˜D‘3g>*%DFH˜,ϋ€5‰31‘ΘήΓR d„„H„ˆΘσȘ½7J2ek)ηό¨„ΈΩ•Z3iMJ‘fЍ}‰IlγυΜd„Hežˆ4 ±YkEη~uL€Ψ‘–֍:°#ψδXJ&ηΌϊςόΧ^Yρξ;Χ―««­±Φvt£ˆˆάP(/ΏπGίό·ŽnΘΑ)₯ ‹Šϋ4fάψ©Ÿ>ομϜ‰F;ΊQ@„ΠpdonTLNcHb/ @$ž’m»mƒG–Τž=A"Χ509‘PΤδ₯m:€ΣΎg£C{C§Σ&„ˆD„ˆκ”c‰yίνφQ$Zl³οKσbŽ1ΝγΦ¬΄rεΈΚaŠš„V¬]rΦ³bfΛ,Δ’_ιΦ½Π!λJΠ·GΈ Δ1­’!&«Ψ'iΜΎy-mMŒIΌφΎŸέύπ_ώX―7ιδSNŸrυυ3ŠKJ•Βxφ±ΦΦμΩ½~ݚe‹>ϊΧΏδ^χ…/ύϋ­ί,(,κθ¦tuΠ%Α­L.b"m)¨M»Ν=‰Υ Υ5΄m§SSηξaΟhΦNM^Z«(QŒ„™]&EΒ"DB"iβ$³b&fΦ™ (B"„‰8Σk({S)±(QΚκL|Λnή£%ŽΫbκ scW&‘°ςlo"²Δ”ΙŠΜ™―ΙOϋšFkνγ=pχ·‹Θ-ωίW_?£[EχŽnT'±³jΗcήΗϋώχρ‡ψζχ~tտ܈ЁXψ³ΰψQΉ­jρ»+O™:υ(άKˆRUνΦoτ7orVWσ–ΈGΪρ­υ¬΅ŠD)QJ”ˆˆφbfΚ %kEˆ3]ƒ–Δ²%!+–ˆ”RJ5ώR‘² PeQΚΈΜ”ι•ΩΫc)ΎVVνλλΪΫAΘBnJeF Y₯„Y₯ˆH “Ν€W±"Ƙ 3ΖΦΡΚuέ~ΊΊWΌ’‚τΘAαΑ½c…nž8,ε΄QΎ¨.ΨΑXW[σελZπϊΌnΎε·ίYT\Ρ-κ„jkφάϋ£ούυΏ=υŒ3πγ…EΕνΊ|Ρόω'έ»gΕ'Τ<€=§X‚€λβφύ5΄|ƒZS%Ϋ’VΎCLΔlIXϋ–΄rI”½‰Q› uQ&¬‘dΖy‘8djΚM(™“iΆfoh䦀Ψ8‹R©¦ςen6>•Θ±DfίGΫlH)“Έl83ε‘…ΘŠ±D6Σ›IΝ$b…H‘£ˆΕJg•*ώhθ΄χφΆ_irxo3qΈΫ·;—a'χ£iΣ΅ώ’ΨΈnνυŸ»8‘¨Ηό·ΖŒ›ΠΡΝ鴊ŠKΎ³½βΪλg\yιτ3O{π©ηϊάэ芺֏x€C`ΘVVy+V4,_­Φνˆ&t4‘Γι°ά κω&V€ΒΦ•4)Ό_e Ψ4υ2“R,d…¬(EBD™ŽΎ¦m˜Iˆ”QMaOHHˆ› ±Νη16ογΣF5_§ω$D!ς΅w‚d&Φ‘Κ,Ω£Œ£Œ&±b…Ιj²Š±žλZv1yμtίQΗ5΅΄f“?{َ!ƒΜΔ‰e'Ο/Tβ™,£ΡΖuk§ŸuZŸ~ύŸxqVχž½:Ί9ί˜q^˜ΦŒ+/~ΦiΟΟύ'r#ΐΡ‡α©pάϋ䆧ŠΠξZσΤ*^ϊφφdCΨχb–C’”°6ΒVYWY§1%6υαQγΔGΡ{g²‰ˆ₯̊€DF±a½οNΌ/‘φ©y,l9P›«ίd΅6PUϋ­¨Ί· ‹§[­­“·j‰(μkΧh!&a²Y«,‘%«}’Ω0[%,Ld$`NDσxbŸΨe'†RŽ+$Άν«§§κjk¦ŸyZ^~ώ“/Ο‰εεutsΊ†DβςσΟNΤΧ??οŸm§Šα©G z@„|C«ΧΔg-¨œ]=B€‡v|σΙ$•ΈœΙŠl,© ±S0Έ,‘ΝLGdΧ4&9nJ~Ό7F ΫΩͺΥέΤaΘ-3cΣΕf-έχ_[{ΐΘ&ΔDŽ˜ž""RiΓ™¦³e²€-ΉΨY™GΚ°ς™|&kK’΄DκγzΩϋ²gCέ‰cδ3S‹Ίˆ"›τ;ν_ΦΪ›―½2‘¨βΕYHŒGY,/οώ'žΉpκδ›―½ς‘η^ΖΊ8GS§ύ±pΘDhW\f-ͺ}γέ=I)θz8⇕Љ%K*°€…BΚfΆm"Kl„ s d‰­€¨„(Σ ΨΈΰ*qcΖs¬ ΆΩ [0Τb¦b‹³œYj΅Ω•Νdf7Φ—f¦S’%E{Χ8•ΖM%Ο(_±ΓΦΧ–]Ϋ8†Ά6š§D) iVΖjhςΩ$΅2FGW§ς6/«^[Usώi=Η‹†ρ‚Ξ9ιρ‡xλω˜F₯vˆξ={έΔ3MόψC|ώ†/ttsΊ„F€˜i嚚ΏΟ«ϊxWABχ³‡ƒZߊ'&€8€IΉΔ†Θs-ka"ΆΔD.‰ΫX‘xΚ'"Κ¬žΊw ΥΜ0TΛ-ϊIZ…½@·ΨΎε\Εjqa3‘ PYςš0YeˆLcΪ;ŽT‘cΒB"ΆD"YKΖ°Ii2 RBšΔq¬ΈJG2’$‘Lƒ+mω†δ–Ϋ¦Œ‹\|nχ²B6&π’vξΓq,‹Ηλ~ϊ½οΞψΚW±ςM3n_ώΧ»οΌύΒK.kοbͺpΘ:Ϗr€ΓΧΘάUΑ―_t>Ψή'T(8~ ϋ1mcD\ΡڊΆF[« iΛDd˜ ‹°²ΒVΨ !#dˎeNJc­cΆΆΆΎΆΎ²FšΙΜ\άwΨ 5;„Ιov͏@/Λα³1$†Δ°έ{ΓΖp`8°δIζ_( ΖY–šΙar q@dΘφŒJY6œX6a1Ežη&MyU~―W–Ή<³gG »œ―wi>Π€ΨγΣ}?ύ‰1ζ?ΏυݎnHWχ_ίΎCDώίΟοιθ†t!Ό€η,‰?τΚͺ„“ΦQb­­rM„Ιar˜4“bΚμ΄(LčinoΤciupγ&ΤμͺΖƒp΄‘ˆΨ¦ƒ¨ΕaI,g?šέΧΛΦ²±l2ι—8Σ%Κ™# 2$†¬!#œ|k‰-‘Υb΅΅ΦlZ‡\ΌμΓδCOo^Si­*Œ8Α'ψ?μ(J%“ε_ϊχ―c?ΖWT\ς₯ϊίξS*™μθΆtDD ύΕͺ—η§jλ‹ŒφΕέCN[­¬w*ήΆΓ–Ι(1l|m=E Ρ5R΄θ£ΰί°²Κ iΟU!7ΞyυεϊxέΥΧΟθθ†ΡΥΧψΧΥΝυJG7 «@h zŸ_Y΄ϋυυ΅~‘ΡV\Ε"JΜ~‹ΤM›[π8Θ΅ΝΗ΅Ν;P[ —mμ©$ O©z«•Λ7GώςlΝ–€Ή3lΑ1΅WΖM:Ή[EχŽnu«θ>nΙσf½άΡ θ* «SLsή5//φvϊ‘:J©˜#㠘M>³§tόkn=x΄Ωq|ΙSΩ*hmH[_Iš) ΗρUΎξ΅jKτι«wTKΔΕΗχrͺο½½lβ)ŸκθVΐ>N>εύχήνθVtX=Ί4!š½xΧ³sc΅~…D“ΔΦ7†­£ŒbV€β–Sb£m­­e¦ΚΡg(DΆYaζEΫή…ΨϊŽι‘YΣbέόƒˆ φ8UΦ IHlΪΡiKV2›y0!ciΑ;ι ζκ«ΚΊε§Σ~Ψ·‹©nήΈαšA7ut+`ŸAƒ‡>ω·‡:Ί]Ερϊ³ΰπ ΣΊͺΰωΧqί X[‰’D•h-€˜˜¬+ΤΦΔΨΎ[Σ~ γ΄yxκkCv­J*Ωw°΄όH¬,)!ΗJȚ BΦsΔS”fςj₯dΙGvφβνυΓΗσ(Υϊx6x8¦Ηλj;Ί]z λͺKΨ§goΩΪPdW ‘Υ,šY˜=fCaΆΡCOڎψwΤΥ!O‰δ–]™–D΄0‰f%dIŒcUˆq:ˆΌφΖϊ=‹O:"½ "­υΑΛΑΡ’΅‚Ξ°Ζΐq=ΠEK\ZξGΚ·dYΗZΧΧzŽ€₯‰EΘ1tθQα€k΄w‡cœΡ6νΎΆb—Δe ±8,šD5T§ ΈͺημY‰=»;ΝCt-ΠE­έβΏ΅ά#§Βh&²JŒbŸΩg ˆXH“h!&–άΩ/g,lsll}“rŒ*"i΅=FΛ£εZŸmY²ω-³7NHŒ2F‘eeY–Ψ’$ΚWuϋ’KΦlV―ΏQœ―†ΠEax*tEIK]’VΤ–E% ‰ 2D™‰œfΛψDd[ŽΝ1ͺ³uΨ"sΰr™[΅½ΪΆ@%"Φf.«€Ε@ΧVMmΎάSξΝDZ,„]‚bKΔVΤζιU•œV.krEŎ=ΐ6Y1·ξ–ΜVΆZB§Ε-ZL€lUIλΛΪπrm{=‰¦mΗ(&q4bP(M~M²`՚δ€^ξ!ξ} Gz K`ΆŽ²,΄b·%n½P hΛ’φύ6lO ·*Žψ1k”ΓΦ±AZ{V(Η€ΒVͺy¦r$E†ΒŽeς˜Ερ₯ΒΤΐδΦ₯Dž[_ QeBJkεb’~2έΰ”$ˆ<qœCΜ[qmƒpƒ“rH “Š(β@t­¨@lŒ$Βδ7ojσδΥ*νς$Ζύ.lω―Μ’γS y^  ₯8¨ΐ²QŠtg­Y·ΣۚŽ”»mlt$„Fθ˜(0κνuA\Ρ H$šbRμ9Ζz• EτΩo©›¬b,¬DΆ¬–"L!e-YΓ)//δ.Άύ{;ƒzF‡χ)νQ-Œ²v) JψfW΅Ω²έω`[νϋ[vm©ί+uU1)Ÿ8₯|GD1ϋ’Y,G\"²–Œ“8Šχ…F>Œ•_ΫΡ£ΨΊ`Λ’½žh`„΄―]CšΘ²£•‰‘IlΨμFh8N 4@—ΐ€)^»1nI ³΅–˜©qΞΥ[–3XYr•U₯˜<_Υ+JλΤΘ<9ixω…'ΔJKU^ž ¦€­CΔ€=«σ Ԙ>Sό‚­uΡ7WΤΎΉ"QYGΖ*LjJjγ:FyΔi’$±#B$Q²!ΆanΉ©d»–9ε£>Tˆˆ…I˜XD“±$U;wΗγ%”οoOtEΠ%0Q˜x‚΅Γ$™υL3™Š™„[l‡Ρj¬jΦ`cˆEyZXϋ%†¬­Ν {§^:lLΟX$κ‘N“2^ ύ@³bcI‘Λ*ίr*νΦ=B±aS»ήπβ²ϊ·–šd’ φ΅€YY‰ff‰°EdΫ>΅ω©£Ÿ‰2ϋcZbK’ˆ$πΗzVΩΊ„_½ΫPOΠpμCh€.ΑZͺΪlί™π’…₯‹0‘Ξ1k1wΪ œ8YεyŽΥΡ”Ϋ-O.˜RzΙ§ΚϋΈ šΆοWΤ48λ6Υ―ό¨nΛφ]5q?\‡Š £ε=ό1Γ"γ{—χ ›BΥpFowd―ξ/τΨωΜ¬Κ=ρή’΅PX()Κ§IIg IDAT΄²Ž"bφˆlσ5Ϊ5kρΠs#ηήU1{je!²DFYq™Y΄o9… ·V‘Œ<&V€ά K,m©_B:U֊o„…ΨˆU†…₯EOc«{ϋυννcΨ*7BμJ2Ρ'?yΝ΄§ΉF¬³»><σ½ΛήΩ±n'EV…D‘VŠ Σ6|mιφ!ε5gPqڈβήeΆ\Υ]zJI·‚όŸΩ²Ύ†Ι*’°vM` ±',"QšΘŠDν%δXνfΏiœ­»3›}έ:{rα²"l‰ +ΡΔJΘW.'=ήΎΓχ|EŽ>„€£ ‘ΊΦ”π•) ΘρX,“°X!!nέΫΨ6š ύ€•­+՝?₯dκ ‘"Nω6όΑVzzΦΆ—W±eŠΛD‡ρΤΊΪΣ*,±”η›Š₯[ΤΫk ?N_yvΡψnΎ±g ισΤ―^ήU“ˆ*.L{†΅gΩΆDBΛ'³?HΫIΫΗ” ‹°!2,š¬C€DQl$Ԑ £‰σπ£ΊciGuΒr^`)̊3½‹,,ΜΒLφΠn*λ„ΘζληM.ΊδΌ‚“06τΦϋ ΌTΉj[Ύκ£2ΖSΟΚs]²J[°DTΘ•όtHωαβΩv―}|σ5S{L›T\θ֟71Ά6ˆΌπΪφx}DI+J ‰ q‹Ρ©νΣΖُ<ߞ­.ΛΔ]Eβb߈²­‘xCPƟ"Η:ό€€.Α7΄½:n€ΐZbRLΒ™0sx ±8Š΄©Φ=8or‘–t½DΧl ώόόšΝρ^A8ΟMΧΕςκzυρτ-ά°zΗΞP}ͺΤ:Qq)°†%©Ι §œ»#ήναΆ–»gަ°±N ­Ϋΰ,Y’pT1i!—L˜Θ’φ­΅ΉSΝΉjN[Ccλ:Y83ΡΣ‡Ϋ<ޚ6εOΜ΅ΛΦ”{Ϊ­#ΗJνΧΟ¦ORy‘uΦ/μ‘_2ζ\ž>Ϊ<ϊΪΗ―nΨ“_δεμ)‹₯ςΘ±{ΨK9~ÎΒ?>mΎ}sΈ·ιΡ[­RϋμZ§6-Hιό†£­ημ±,LϋiφpΣΩ'U«`άΌk±Υ3·κ“T-β]«Y•Ν{EZ”TlI˜$D£@”DMΪ„tJ©΄‡ŽF€γκθ ρ8%“Z„DˆY17οZΜΥΙ–νœˆQ~(5ndyq^Ύ₯Όu•©·WV¦Ε·2zμˆpD…$θ·»Ύ΄ŽTΚ‘ξ½ nψάΈ―~nδΔ¨Σ£ͺ  %L*ίνΡ‘Z'¬7nK­X₯#1gΒψΑyQf"&ΛLΔ96Μ΄Vš9€WΥn­ξ(Φ:ί¦=›LG§…p8 Khh°".³"!km&έevl=J²UΦiyŽφε1‘¨T±γωΛ>ΨSέ`8撐 ” •Ee½dΪyιΝΚΌU½)ς™Κ#φό±%w\1μ_&DϊΊΫl°;ν‡ˆΚΩ[V»‚ψάχw₯Ε²F Λ+.°Nch”ƁžΒ‡ εP6ί URlqλΛΎ9¬ ₯pt 4@—`ƒ’"VLjo†9p(Κ|φvε5•ιWšΪ“ΨH2­¬Is,M~@ie­–hˆςΩ5β¦·μρxfΝύuωςu‰€UXϋ#zΪ¦ηύΗMeŸ.vwΈi£%δsΊA»«Ά»{ β ΄˜ϊυŒHΪgΆFYa›Fš+4Š•fΗ!gΏvΙsŸV0Mΰ8€ΦΠ%ˆ%+VŽΦ 2݌ͺqOˆμ—Άψ3‹ΨΜ(Ρ~έt”|ΕzΧZ»C<6l” +ΗΣ+JΨ²r|SΊbMϊ7­ϋΜ)%S&wοΡΝqςΣcG¦+zυΈpΟs vV¦σSμ‡Uή–fWm}ίββ uΛ,ΔJ2s+³`jρ,ΉΦ@=Θ Λzβ`έ›rΰUX›7…ο€…pŽθi€.ΑZρΌ€„iκφ²""d„¬ΨZ]»oœͺ‘h₯˜YkΝΜέ uˆSΜo ϊ ΟR(°€˜5‰ηSƒ›6Φ6ζ!Χ N·uύνδ‘5sΧΦο$VΪTμ_ρ©>§ ͺωa’Ρ)kύΊDŠEE][QβƜˆˆX6†EhΏα²ϋaήw΄κidΥΦCΘ6?šΛsOefafk-‰Θ~―ŽAθi€ƒEho?]γ¬Δ½sΫά'ΝG¨’£Y˜ QΪ’&€¬γˆ£•Ob,[O‰eΒ:ˆ‰uG5ysoOšΆμŠ…KδRμhΦ–ŒQ’lTq@¬…mΐOY«Ž€#1Ρικz?IΒ£­%W‚Q’ΘZΧX—$€$ν’ΥΦυ•'L–­ˆ,ήΆ'η~2=Θ©μφΫΤƒ•mσk€„Π]‚‰eα¦ΐΨl %Ιώ‘&WMϋΖ^ŠoΨ²+ΜΪeε°#ΒV+₯-YQΦ5Ž2lY¬χ'­’œŽˆW]QšΌκœ~gνQ\dΘ­5ζÍu„CΚΆ&δ…"ωĊΔ© bμ°₯¦ωЇ΄ΪήCΨ’m΍-X"{ΟG B#t B,Δl­¨€ˆ\±L,€΅pf7‹œaLˆΘ1gβ”eΡ"υυ†D1ΩΌBUXH& ΄δ9>y.²i–PŠΟUΦWa£ τξigN?³ΌOa²ΤM EΆΤΏςΞW—Δ=Ϋ3ͺmΪς+BEнz²;kR&­΄!νY«8 nΡ—ΨrΌ*“•‰²|ΜτΈΆ8—½;ѢΐΝz‰[tmΆlΨ¦”›ιηm>•±Άΐq‘Ί+lI+Xν³ˆkYD ‘Άd™D˜²ΞΪ›Δ„2!‰˜‰­e‘ •ρDJεΕReΕΊOΉ·2ι…l±"RΖF­2žh')Κ·RlSγ”]rAxμΐ [Θ7&RοG?ήΈηΙWή_Ά©{•WκDMD΄bέJΌ>ΡBR‰εnΪι‡2&Ĝ`²Ύr%nΞ™€-V)mž₯εͺ«"-–όQ*G‘m9ΕQ©fa―ε«k΅zλήύ0›ήdσv Ξ M8Zθ γ1³G›M;Ν†]2*Οi5ΆWΟ΅k­G I4ΪΦ­—ΩSP‘·σ²sJ§œζτ-·1±NCΙΞ:ziωŽ—ίZRYέΗ„’Κ œ]I?―ΐδν»£ ΄Δ’Ϊ]λlήZ/n©Qž0±hm©]#TswΣW.ξS5'φή|Yο'žΪΌcێu…ƒ$Ό{s;/›ή^Λ‹D 5?‡upŽΠ…Hγ Ό}«†R«―|Uφσ,,–I„ΘSαͺ΄ϋΔkk·WWMτΠΎ=ς£Ž_šoΛ††Fκאv“–λ= »ι°ΟW*$ω,:MΞΪjž9Γ¬E©έ‰q£ s4bmβšƒΜ‰Γ κQT\ΰ†C6ε{Uρ`λvιͺΪ·7lΫΥP$¦Ψ—#iMέ¬³,Δ$ΒΦ²&fbfJζΧ‰δ©xa¬fά eΓτΡ/Φ-ζ„,§ŒμLΙ†Ιχ>JΏϋξš†γaΧJ½+¦=˜Ά½SŽΠuΘ~_4Κ‰r&mI‘X²–§Uš’)[ϊ{»Y<ω€!η+λίˍ©TTΕ‹ΓnE(Ο 4DΒ?ϋ6ΌΉΚΞ[ΎqΞ;ΫΧU€L‘ŽΈ‘xuΎ»ν¬SЧΦoTίHž²rŒpΪ³ρΎι“ϋNΫoέž΅4ρβ’Ίδ8ρ!ΆJ‰a2–Ε°1ΔDŠI[VIͺν©;gLή%gŽ6 μhΆΦ†uΐ†|²½˜ΗŒ\2)ΊiRω‹o­{ρύΚΝ^A,RN–{ΊαpήϋΎw‰œpBh€.Esχ£εXTY%,ΔbΩ¬,‡”ͺ0žLn™_Ο7·Υλ„zς˜HŸ2₯­IyΌ+ek½πžz»}{έΚεΌhεΦνWΫEΚΔdκ" Κσ?}vs§ζuω Yα4λ€Gq/β„œJ…ٍh{bo/\ή«πρΉU•»εT;6Μβ( ˜…X ±%MΔ–D‹Χ;Όϋ ΣzŸbYyΜ¨ p8)ͺΆ>dη(EtΨρ#:Ω7\QΦ§x ΎήΦ†έyα†B― qDζ Ά|“ŒΎF€γB#t%›J»Ζ[f_“φ.Kcι³wηq–VΥ½πΧZ{?ΟsΖ:§ζκy¨žfADePŒ³Ύ1‰ζ&ט“ο½Ζά ήΌQΝ« qŒCEA”‹ scΣMΟsUuΧxΖηΩ{­χSΥTUwέM7Φϊ~Ξ§8Γ3§ψ@zν½6 ’±μMβCοS–μNοyΌϊΘ³{Lz^ΆΨš3π얝_½ι‰έΓ‹FηβAH9™W… šΨϋ8ϊZŠjΊ²γŒS[39W“€7αυ›έcΏή΅~gο`9(¦r+ZμΩ§Μ\9 ΩZ&Ύτμ&Œζ|Ηύ=ύhA @ρIkήδ²Βηtd|‚žϊcxrΧΠCk{6lαz΅˜Ήσ «Vwœ>·΅#r­Ζ_yA‡/D7ίXN†“ƒ­TωςibTJ©ƒ†F₯”RΣ" ²Ž!γη4 0ήϋρϋ€UΗεF/Μ‚ Μ@bΕ‘XB€P%(S’.s." -P₯΅+η’hΛ`&I΅BΈ/4b“J€‚,θlΰR‘―_ύΦσO-DQ}„γΗΆήtwο/~… Ά’ν"Δΐ‡O―©πΑgΟ:Ε_qαŒsVvΆ¦Μ›Ο ΘΡΧo¨ƒ¨!‚gfAΒD+ω vΕλ]}^h<Υ9Z»uπϊΫ6?±-” BsFf—Ύg]έ·φΜ₯…rε‚峑τΏυτ6ί“ύι-΅½"ˆ("ή{’Ι«6NX}qbρpŠ"νρiΧͺ”RκΨΣΠ¨”Rjzρ" ΐ"Δςbž™ §59>  ‹ Čβ0νΛΰS$h=†)6³²™7Ύφ”Ϋj½IS2&θ#‚D — λΙ©ΛΣoΉ(MP―"?τLWnή±‘―έ…CqδΓtœuL[ηS<»mέξm=oΞ\qzkAκ—žžyβٞϋΦŒ kœ5RKSω”ξΤ§₯ΘΦΘάΏnΧ7~τΤΦώΞZΨEXA(W=zΣ^uΑƒλJƒ}ΓοΉΌpΙY]Ε°vΩyι][Χl|Ά…Θˆ LZՏ€l;ξ6Ε>J)₯~&e‘RJ)υ[i,’4ƚdΕΒ#8Τxθ½€€ ``Ζ„!\–8‚‚‚LX’zpώŠKNΛeάΎ°P³"‘7‘9+½α⦎άJΖ­ο ΎλΎm»Ζf"J§Έ)Ε6‚Š…!Ɓœ ζ₯ΈgoΗ7~ΌuCŸ# [CΉόfF‚ ZΚ„’ŠX T»όœ9σZΠCψόϊ7nίΆo± a’5₯ζ΄o\ •²Aό|oόŸχm}z³ggδλ—\ΠlaφƐ1ζ%nΘDSl©•RκD‘‘Q)₯Τt1E˜9’ƒL|Γ!$ @ !Cθ ˆΡΤ‰jc(Φ(¨E΅ZTI  ΄7\υ¦μY+wΆšυιΔ{_,›b?¦MRœι»5e%5ΰΓ?ΈcΣφ’…nH"^j@Cˆe;΄taΟG―.\{NΎ=o· Ύ{ηΆR L–Ν,ΜΩQvq"θΩ G†mψ9Ν©Χ,Οδ±N΅ΰΑŸοΨ±+οaWSaIΒJEXjΆ…!Y=£ςΗonyηω-©Βΰ“»†npG₯’ ˜N^>kvΡ{?iΤξAnλ‘„F͌J)u’Πα©J)₯¦‰ΡΪ–l`δΔΑͺšΨόf4=@„FUA O¦Φmΰ€ƒΐ2HM†»ΪΜΗ><οξϋxhο–}iv@ΛwΕςΆΞfΔΈykoωΡuύƒ~5zŸΈZ=¬ ₯R1,Ÿ“ύƒ·ΟxΝ¬Lu„wa²ϋ »~›ίΈ§rΚ<κHεVw—Χ¬Ωd|)Š!Άυ$ΐE‹Ϋƒ)ςλ7άχθφΔΜι·1³˜iσδγdδΤς§Χ-Y=Ομ«ψ†ͺ?ΚάdΟϋ.]Τ”²9λW-μΨϋ$ε­GςΩ2€ΘΤλ•h4TJ©Ÿ†F₯”RΣΒX\ΑΡ'γΓMŒ’xψλ8r0ξ…qγΗπHPͺfj©Φ‘ζ ƒ€ί³―TΚ··ζύŒT靗΄½αΤΤσλλΫv†$œ9oe˜ΆξΩΌ«iς³[vv₯{φ΅—!°4rφ_/›Ώ’–Œ ΨΈαl=N?΅SVuGi©Ώ~E=7@!‡‚ Τsbˆ›–Κaΰ½ψgλ#[mG‹-vχά–κ@%Ώ΅†βΝ%ŸΈ²eεlŽN₯}s₯”«7UlπΤ–‘Ε3Š‘€VΝέϋVKΌ°"Cdϋ!‰¬ΛΥ™Oy¦h„"Ϊ G)₯N•RJM €(€,H‚€βΜρ©‡‘0 ‹ σΨγ[Ϊ2όΆ·ž³lFWΞJ‘-ΥέIB o‹pΡΞ>γ™[άχΡk;ΜΧύτσ#vσHΆ…?όξωK;PΜP©ΦvΟ½;Χ}WRθ¬$>gΜΘ)‹δ5έΛ"ŸJ9rl8CΞ•ΆμG.hΛωχ_±ψ5έζω­Γτ½5]3 ΊzEχΌ’ *{«…[oίρθΒ©&Α}ΓϋΚΔ-`³ω|*qΙ‰03“°€Ν„rτ·OλJ)uΠΠ¨”RjzΙs'ΑŽWhl„UΔ ΅>ώΜΰ¦νk~ύό“WD³Ϋ9S,a‡E,zΙXκ[ΠiΟYbηEϋf―hm/ΤlaφŠ™r₯’½ε[nύιπpœMlΰ\Ϊ;Λ”8`#5DB’Θ%ΧΨ d Ί8ψ=s‹xή²Ž…·°9_lšeν©³ΐ"ο¬Πξήtχ½Γ53/!‹βΘ3ψΖχ„ˆΡ‰ŠΑF‘…δ€²¬RJ©ί2•RJM" ‚£U' H4σΡ‚€ˆ$(^XΠΔΤΝΜϋRzΓϊΩυ• ‹‹fεΪ›[R©T-.Ϟ™ΧiCΛ…Θ¦l°£―gΝζzΧ’B*τη,“Ά” Wκ·ή3rσ}΅½Ύ&Qδ½ΟDΆ%ZtΓ}ΥόΪνΥ4B¦&8`½Ο-€ζΟmaͺΞ,$»ΎώΚSΟχΜ>=“ύy˚ΊHF†βΜM?ΫrΛΟjS"γ@Rθ›rΐη‡FFͺ±γΐ €#`?ΚtΚ{7ρ#œͺ”R' J)₯¦±œΨΘ*Β‡Y"ΓΓ/D8gO„ ZBL DPέ„ZEkϋχύΊgΨJΏ5;,!ΦηŠο{Λ’4ΥVΟΔΕάϊ‘κΏάϊ‚Ήβ΄KVH(iŸ˜[ξήsΣΟ’­>ϐΊ ‘ښNvδB¨‚Kψ>χo7?…kC‘±Nήα’χtΞƒt·@*4›αί~ς\&΅θ’Υ3RAΒμ‡ͺΑ ήzΧπ€ο’(πI 92Rš?{š2Έόξ=I-aO1 ! 1:BΔ‰apόΛIΉpβ–νI€”RκUGC£RJ©iaΒ€Ϊί‘tυ”£[ΌP@< &@Ζ.6΅Ψ'gI}ΎΩ₯λ1$!@:Ι±ηΚ‹;: %qνΫJ/lο L](Ξ:Bδ-{vnοΝΘ»l6sρ9ΏΪρά@ά΅³άυ΅[φ>·u >ςx2’t&Ζ\ |:ŒΉ˜­½νωΕ\x·m¨γ²d<³E&ΐψ²j…ZeTJ©½τ&J)₯Τo ”Ρ'γΗ©&Td"Ά† `lŒ¬C¬YˆCŽ#vΖ#bS_9ϊι£=YS{νͺ淜a;ι‰Pj;†ΫΏsέωp‘Ÿ›!S"¬œ²Ι–pΗΫ^_8΄Άš“~ςhyO™ λ”  eΑ£7{jφGmΩη²I8rώκ–χœ=―}dkž°§\ΌαޏΦχϊ"fo Eq’•mŸsr!2Rsω»ή=0LΖCΒΜμ@DFύΘh[Ϊ#}μΏJ)₯^έ΄¨”RJ7˜!#²% d$ PG1›Šυ†Α₯½Λ$Ψ'q›9]ξΚσ«W_8'oηkO¬/ίρΠΎA)¦,°‡°.ΜΕ} €s IDATϊλ έλͺ—œ”i‹β]4kΡ·ο_¨fΚιVbδς1:G Ή¨οWv\rAg>5(ΜΟ¬‘{ξΑ΄ΐ°γ„ΗώΖYπ¨ξ*₯”:hhTJ)5-€qΒ""Œ€z΄L*x0γ‘8Ψ‘LQ&I$ή&‰qe Ό±lbG‚Μ@hGrΰz+_½!^ώα-‹:[ &χ‹–ΌvΏϋ—›žΪΎsOμF o‘Ϋ‹;Wv/»ςœ₯+@Φ8ορΩmrύm{zͺmˆ³1‹&Γ(&3ˆθwο-~οΆαSCW˜ΥVHΣΫή8{ξόΚ=OΌπΨϊm•΄  S΅ΘφŸ²Έ~ω™­«—ωΠτ04ύr+}ωξΝOΧrAX  ‚XBD¬Š0‰‘ΙχgŠ_Γρ_S)₯Τ±¦‘Q)₯ΤtΑ2ΊΞƒ Ρς‚Ά’‰Ξ€ε&@βΐybC(,Q±΅1N“ΘxΙφφΒWnΪςφ+ΊVΟKe3|κ*^½|Ξξ9»†]Ώ3©¬ν̝ڑK0ΐ §~½΅φυ>χτdκ°X`F±Θ‹Τ avσφΚ—ώsέ‡>xράY˜Β‘ΣNσ«Nž³―AίΦ]δΠ5·„sΫ e •XRΫzΒ―ώηSk·gλ.uD±[ΑΠΈnμΖLu/ΗίρΟ_bG₯”R―•RJ©cζΐ<‰€cΛBΎψށQiŽχ<ΩΌ±gΫ΅—ΟΌπδbWVRA2§#˜έ²XACb\q_{nπ?^ΏacΤ™Β˜%9ΤͺSχνnήπΥ‡ΉbΙ…'[Πe­›Σ޳[ ζ1πˆ€PγμP-ϋΘΣ}·ώτΙΝ»2† ŸφAmυυ ΩaD)₯Τo–†F₯”Rκ˜™PIkΌΣx6qωΖ—ή7Joοq_ϋξ¦gŸ.=oήβ™©Φ „˜X dΧΈ?‰΅eΟ}Οξ~ψ™ςp­Λ­ΐ"\F{ΘΆ>‚¦jΫ·φVΏφν=kŸτηœΤΊͺ;l+ E!BdŠcι―Ή_ο¨ώδ k6 TΣ„MΑb©4ε—>Μ2ωεΥz•RJύζhhTJ)₯Ž‹ύ‘q45ΚΨK™0–uRΈJqb¨T£s=ύ\W—ŸΩžnΙ7e£()•χμ©nχsΎΒsΔΌΨβȘ€RΗ}€%2>_«ΒύχWyxCk›λœΆwe£” ͺΙΠήξA·±§6μ²1v">e‡lΘΘΉC|ΉFBͺΞ!‡§jfTJ©„†F₯”RΣ!"cDδΐ ΨT)Ϋ€ƒ8οΔ°Ko)«,Ββž¦ΐΧ³lc|‚ι:zwς3ΫCD!JŽ₯#‘£%#5‚±'FfδqΕMi4;E“ςyΗƒ–%W†F’ eγ7V¬Ϋ„#ŽΘζœX΄!zGθŽYbB’™p3'Νέwν“ξ$σ„9D/υ… :₯Q)₯N•RJ©)§ή½Ό™σ2Ί82zΟ€HΞ7U @z’}–@ •l~“¬αŒg₯LX!JH"„ b2šηΖ±:'—±L‘Cdh- @6¬ΓΘ0’η\€μ F€™D|pθ;€€“j‰‡{F§{*₯”z΅ΣΠ¨”Rjš:&₯Ε—D„8ί8΅u@œͺLœͺ`¬ΆFi¬6₯ϊ;ZMs!‰!²Ϋ†+½»z Ξ! ‰τHe ²"(K-.ΧΉ.ΰ-A$b#ΰFΖRd|ύΖO=V5[₯”R―•RJMGGm¦ή'ŽΫ$"D€8޽0 „Qh {‰ΘrήH) ­ZΨ|ζŠφ“—GΝΤ”£$Ζ βJΝmέ―έR~θ©Ύ ;₯Ώ0eq$‹@‡8*Fƒ‰5I{K8oFg[Z³6…H«'•jo ή΄cph$Ž™  ZΖ‰#PΗ:E@4‡sλ&Σh©”R' J)₯¦ A`†±α”chŽ4»ΰ‘μΒdκ±Ο.“ΆρN„- €@AA&1³ZšΈxζΕgαŒfΞΫZ >4,˜)u.Κ.λξ8ωδφΗΦΖ7d`ΗήXγ I£ρ"ˆˆ-pΊžm*ΤΊ¦fuΩ¦”‹Δο- >€Ίε3™b&_,ΆμΪ;²aσξςΘ BD`%hόalYΖΡΔ»ΏAμ!‡§NJΡ0ξ(:6U)₯N•RJM (} Όχ˜r‚@ ‘€ΰ)λ~H0Ύ} jM €IqRFŒ΄TMfSo«Κ.œ›kΞξάCΚεkδΈ­n0±‰γΡA™]E\ξ”ξΊ|ΙΕ«A΄/&¨Έ4Έάΰ@›T9kΪ ]Y š™Ο,Κιg¦OΚΥΏςγ͏οHUaAΝxkFBζΐٜ‡’—–Ϊ*΄ƒžω©ζUΛΫ:Z}ˆ©“Η ‚αΤj¦ΔΎ=,¦0Μr=“.Ο›ο»[=½―§?ε€­&djDΔΖTH@ έnΖηD߁aμv°V•Rκ„ ‘Q)₯Τ΄$" ’ΩΤΡEyς;‡‡‘€βHά7P1…|fF»ΏπΤΩΫo―H¦΅?΅e/p€b3dPΔθœνκ ώΫ;4;b-φΕvΗw=Ψχό3{ϊκΐ6Γs ~ε’7œ½paW}§œζ>ΠΌtδ?v>Ώ³δŒpθ€Œ%!Lͺ!”‹Mώ΄Ή|ΎΞPε ΪΫηvνμιχ½ΓC1ΤSΖ·dφ΅Šέ³;[Šy/₯¦BΣκUΏfチ^‘Τh\Δ#«΄δn6ξΈ4’χTku(₯”z•ΠΠ¨”RjΊ’ΡS„FUνŝsApFΘYΓΉm»zŸΪR™1#rεάE3žhΩωάPRΚF‡…I–|-”ΌoΝΚΥo*žŸ€Έ^©₯ο{¦η†6?·-rΥ6†ΠgDκΓ#Ϋ‡žίVzlΝ3oΎ΄νΒ3l{rκΌβ»XπΩλw&Β`Ι!X ‚\*¦λ+η6·λ ;‡vγΑgΧτ•ΚΉΔη<ΠBΥ»ν•ϊξΎ‘έ}Υe‹Š3ΊRd]kKfΩββSΟlNf bc­ ŒL•Γ}2ΊƒŒώC42*₯Τ βPsε•RJ©ίf2Ze8¬Ž8²£ƒ*_ϊ %ΘH>]N²χ>³«§Xɜ<ΏpυωMΤG>‚‚ΰ‘b‡β™β}§,†Χ’6†‡}τΓϋ·|αkΫ ьZTLbB[©€²CΆω™=Ρυ7oΌγ}q½₯Ιΰ+ ž”Ka ˜‡&ASg“8o₯Ί°=;Ώ%±—ΩΌ΅τψ―{{«©2fk`ΕX"“`n;β°­·L<³yΓφ~4C<£#Z03@H@œχ^²N;j,eΚΈI‡»―RJ©ί, J)₯¦iτ†7ίat€δaΤ»&§Αύ©s#KDT3¦Tγπαgjχ>Ά/N‚l.~ύEιk/nžKεBμ¬$ž81y¦|1ΟoΌ kNn lΜ#ΫΏωΐΖu•ζjΤ{bŽI*‘DWͺψTΝ§Zv,ΈαŽςšη VZZ»"ΧΥ Ζ”Π3:`ƒ"Hζ΅ηšƒ€Έ­wY·Ύ:\.:ΫZEλ¬gS¨AbƒͺPmΟoΪ·kwΥǘ"XΊ #²ˆœ˜FΛ† klŒ‹‚S— /ώA]œC)₯N•RJM €„D4ξ5"ŽfΙ Ψw|Plμ7ϊ ‰rƒ|DT;(ΦTγή1°fKέQ=WΌζ ω\ΤΊ’5‰ά.€½‚Ξ -\ΠΉt~s:αώ’άω‹uΫΣ1ΑKλ!xCiη›\’G c<&Υ_qӏ{ΠR2§Cfw€ ‹ρBb€’N:Š©–|DΜuO[wμ.‘—ŒηΘ"!“χ‰ηš7.!ˆ1©Ϊ[ϊOS!›n.fAœ!faA!½‡—…ΗΧρ]&MUJ)υj€‘Q)₯Τ΄`­έŸGΛαδC>ΰΠ!K5ς 0ΪΥΟ_½ωωυ=©ӝ-υ·_Ϊτ§\yιΩΝ3󽦢ΑlZ4'Ϋ’5Δ-ύ;iΛ $³³ΛΖύy·]o\ΖBΥ7SSƒ#ΉΪvΛ{j&xf{ΌioΒj!S[Ά¨ ]5jš†S²7δΑ€‹y1@Sξέ5Π# ½³ΜΔ†8 ΎΩ°΅P#ˆ=ƒ9½«§:4β‰HZŠYKΰ“Ί°§Ρm#2ύS"0[C)₯Τ«6ΒQJ)5-cΖ¦ΥρTkfΤδξ©γM5%2”ayb Ÿ|ψψ³~ηŠΧ, ’Tβω…Ο?k_YvοNFϊ†–Ο/ZστsΠ³7Λ>έ’­\rj~Εό–;ξΫςμ‚.π°j_sζŒ}}ε>QΩμΚ=8όΤΞ‘“WtY[ν^€£j&uΝΙζ Ή&Œ±5Ο 'Β}U¬²8$@` Ξ =xφδ@$@ŠΣށ‘φΦ0Αr!א$  ‚""‡3Βχΐ{DD:‡X―Y?Π’9ϋ5KΊ›4M»χβŽ>b,€ύγΊ½Τ’—όbC!@`!'Ύ–ΛXm„£”R'Σ¨”RjZ ΜlΟ†† xa‘'" p+ΤΏ“¦GΛ’²P5PBπΆblήy¨u|χΦώζŽμ™ΛšςΑήן.Z±κ‘gχ=ϋΔΝ›K΅Έ˜Ιζ:[π5g/=οδζωω8€R£ϋΦ}ωΖ5ύ₯N €Fβˆ‘˜AΠ2`©R_»nGSΖΝΡJ2³Γ4΅,ΨΊ—wυT‡J!KhM˜Y32³g¦RιΔ  7l-?ώτj=l $m4±MΛ££z§μ‚#QΐϋG±Š€€Aρ>ιlk C·”RκXΡΠ¨”RjZ ‚Άf›Ο₯ͺήyŒΡX;[7γΨΟ`ܟ Μ€ 9oj‘`Δ!’GαΨΧBΆl‘/έϊB-ιΎtΉΝEΥ%ΝvφΉ3<©½V—½±aΠd1Ϋ  JHxί¦κ—oί°©?[0ιΔΥΙD$Q-’1 Ψ°RγGνpN{* z*ͺ,˜•ΥQ β¦4{'![ ‚αΐ–1Žs½ςΜs=UΛVR£λ†ŒΞfahKπ sBΗέlΤ"έSΙ‚χ(ΐmΦΤV8J)υκ§‘Q)₯Τt‘Ν˜bSΎΤ_&1£]?‘±„D#ϋ3Ž“ΦξπΑ@-¨RΪ8E>H μM≁έγΫ±τ‡†ΟΘ]wΩYΩb6πΠδ½·‰χ$)%@€Ϋξέω΅ϋ†7Ζ3Πd$Z,BŒ(hŒ0Κθ·#2ιήα⯞žε–.LG9¨d žυυ˜Zλ6…2PΧ£MλwΏ°Υ•\ͺBή‡ΰƒΦbε%J΄/~Ž„Θ,„’ Γζ–γ[ΪUJ)u¬hhTJ)5- ψΊΥEΨΧ3k(μ»'JR·@ύ.ι&SE¬Εa§žΩhπΕΌΒ cνςΥ(_Ζ6τF|ΪΈΖη!©ΥΤίί;tΓσϟsrΫΩ«[f6₯:‹…}΅šτΧqΟ ύrMυΎΗφlο‹Ω²P'q±D(ά*Aœ€tHT{κΠ·ΙzomφΜBgk&m’\6;I‰m96I»ghΗφ½ε’!pQSlͺγΎσώ[z/>iy’t5]³\ Π€Kq΅Κ‡Υ°ΪzRΆtF»ŒΦ)•RJ½ΊihTJ)5]δrα’%-oςΟΆQϊ°@1ΌΚFI—IΜΝ•ΫχlyπΎM)ΧήeBIœτŒψ}₯x jͺœq& hΔ“°4Ι€ˆx¬VK₯­–#ΓΉ΄ ¬8;RgΟΧΩ; , ςaˆΠΐžHΙg,$³geσωπ¨οRJ©W’†F₯”RΣ„€RΈ`^άVƒ‘Ψ HˆX<‚‘’ΗuύΖ†€-{@@©z«ρξRύωή:’ΰ„XL)PA‹Œβ x'4Ύι€K%F!€”g©Η3”œˆ°X‘@XΌαXΠΒXίΤ#ƈ€Ž°XB’$4<0~s6o-3*₯Τ‰@C£RJ©ιΒ /˜›žΡμ+})ΓXg4)  Jt˜ΉhrwΠγ †M@^$φ"HdsΞδXD‰%v^H ˆ@Q< ˆΌΈŽΕ€Λ3^1!‡H`…Θ°!ΰAœ#2€lτΏ9άΠx@“XtVj’"Bγςωpη’E]hXάΛΏUJ)₯Ž; J)₯¦ίΡlWtgwο«V$¨1  ƒG‹ŽOƒZZ /,`˜Ρx‰lΖxaOΒ , qκ+C!$@μ˜„KF"ˆƒαΡ³ψ—³ͺ³'AπVƒg hŠΚsfeΌmS)₯Τ+IC£RJ©ιQΔt'/Λ?ώμP= dαΖ2ΗΦeIDί3frC  ˆ€ˆσP@$0H3AdΐƜCĈ–°‡ΈOΨΨF< ``ζ 7@€„ލ]Ν€F8œ4‚ ΰPΨ³―υ.^™ξhMy~‰ˆ«”RκUBC£RJ©ιA³†Vtg»ŠΥΎέ0b,ΎœbΪ±ΉΊI£:9ΧΘm@ @‚8šδΌ‘2ƒΩSΗ–R<(Fd‡Β/ΞZΔΖάE 4c‡iΌ<άFA“Ώˆ©FγRB(蜫7εϊ/ΏdbͺκhκΕ:”RJ½JhhTJ)5]ˆΨ$ΑY]tΖ©λz†A‚‘Σ ―tχΤ©ηF Ϋύε=‘ΖΣΡ ’Gπ4qυI ½xθή©θkF’ ‚Š4φ00TFS*ςᇻI_„)’·„ P‚δ–―—.nχL+RJ)υκ§‘Q)₯Τ4NΘ ¦ƒψ΄S:o{ Ί·B^Ρ§krΨΕ΄ceͺΑ«ˆ€# Πώ S E„‘N@_bΆ₯:DA6 ˆ‚„‚žDFO!~6ƐmΊr()γO Ζ1Έ³Οš•Ν™γ0X)₯Τρ’‘Q)₯Τ΄ΐˆ1 cΘvΟ’sΟLοΎΏ\vB XA|qlηθ\;A/θ%šp¬ Ρ¬Qš›¬ίxbΦ:τlΓ*cWβAφŸEX¨₯1„F덀“4"^° ΐΨ|ΕF’„‰Yqcͺa€b°ΡΎ„„A‡Μ$Ζω37Ÿ΅zž&F₯”:±hhTJ)5-8φΰE G­Ω‘7_’»ϋ‘Ύ²’,9ά…ΠDb‘±Θ € 3:@ΆŒ?Τ„μ7Ϊ'fά"πbh<’ έΈΓ Xχσ‹3˜ΖEp,‡1‘:⍎3htI›ΘΩ8τΑ§uΎτ7 C"Œΰ ™„Θ£a€Θ… Tλax›«ΧœΏ %«S•Rκσžχ―”RJ½2XΠ 2@ΝcΥεηu΄_~Α‚,τr\‹Αzd‰GηΙyL™EBτYςY™ΒXγΣqoLȍGηΐcζΖΗ𰇱ЏΛ‚(„ŒB!Z„Bž΄ΌιτΣΫ΄χRJp44*₯”šφξ°δ, \ύΊω§Μ zJ%½Gρ™Ι0Š!ŸE—‘ύarR!ξ8D²—pD':—‡ΐˆ €τŒ=HB$@‚1³p:ˆ±37ς«η2ΊΜΖΛrχ]?yτα‡ΟοΊσŽΗyψ7{=J©iBC£RJ©ι5@ˆt΄šw^±΄5;€ ,Δ`Όδ<9¦Π!Š9°xθΊ#Œίζ¨/U„Η?ΖM/<Θ1'^ΐTγ< 5εAD#`E˜«©U.Ώ8»zIšOΨٌΦZο_ιVIΊα[ίόι·5žϋλ_ΉλΞΫ_ώ1οϋΩέ_ώΒη^ώq^aή{kuš•R― J)₯¦a`/Θcέ@ΛͺeωΛ/ιΚ.1Œ(!{›8pŒuΑ*@ Αqk~ƒvΑΩρGz½G›ύπ€° ΘΧ#"<Ξ€Oy’‰9ΉΡƒ‡@…Εy#lΕl2ΰWvσ›.nMρ‰[fΜ7F†‡~ΣW1ΑŸώΕΈξ]ο{ωΗynΝΣwή~ΫΛ?Ξ+lxh0ίTψM_…RΣ…†F₯”RΣ‚l Γ|6ΉφMΛ.>©˜ςCθ*θΔHΚbV€cΐ*P°>q§C&Ζc{½GΗ=ΰΕςγ9φ UΣ)>}7z·Ž­Ξ$ˆ9ΆR «ρΌφώi³›3ΞOθ'tb™;ΑΖ λ_Ι3φοΫϋδ㏠:©:ω”…‹Η;·qΓϊޞ=“Ά,WΚq½  IDAT•jeύΪηJ##ϋ?ͺT+Iœ@Ή4β’δΐ³xη‰022όόskjΥκψ}+εψ™Ή\IβψπΏζQΫ΄aύΌ…έ―ΐ‰”R έS•RJM[†Δ›‘§·_ΤΉ§·gέΆαΊ ½DŒδΉ7ZŒ"παp¬Τ6Ε§‡|9ή‘—"“βω±= ‰  ‚r`Ρ€σqmqgφέo_Ό`f»ΘΛ ά7uυ)§>ρθ#―ΜΉjΥκΏ|ξξΎλ'@DΛW­ώθύxχ’%“6ϋΰο\»tŊOώΥί6^ήςƒΏvύ—αpΖ¬ΩώΙO-[ΉͺρΡG>πž³Ο=ΟsλMίo$ΐ«ή~έΐŸύΡG7Ύ°Ήβ?ϊψ'xλU“ΞrΕ%ηΏγ]οέ²yγ―ω%3‡aψόρ7]ωVψΧώάέ?½σΫ?Έ΅₯΅­±ρ?Ίυ‹Ÿϋ‡σω9ω΄ΣΟ½yя=Ίκ€“χY”R ZiTJ)5]‘0’•ψŒEΑοΎ{Ωό™`ό`ˆ€ή‡ΐ‘€0/υΚCVη&φR&] βρ’•Fχ‰ΟI“(‘ Ψ ³Ct..7ε‚kί2γό3Ϊ’Δ$|'FΈπυo|κWφυφΌηϊΧ/~ώž»οϊÏύ·o}–?ϋδ§φμΪυ™Ojκ]nωΑϊΕΟ_xΡλΏpύΧώωΛ_ιθμόΔΗώpΛ¦ϋ7ΈσφΫΦώϊΉόβ—υί>ϋάσnώώ λΧ­€OώΥΎπ’Χ‡aψε―}λ‚‹/9θΑoΊα»ιLζ_Ύςώ·―ž|κι_ψΗΟ<ϋτ“pΙ₯—3σƒχί·Λϋώ³ΆφŽΥ§œz ξΒ”z{φ<ωΨ#―{ΓeΗϋDJ© J)₯¦©˜±šXfc°oywψΑχž>³#νk J6"'π ΚγdβπT!l¬΄αHΧύ5לvρ…MhΐΛ η‹/½,—oϊή|ύxŸhχΞwέyΗεoyΫ•W]ΣήΡyΡλ/}ϋοΌgΧΞν»wν<Τ.ΥJυΫίψʊU'ύιK–._Ίbε§ώξXδΗ?Ίe6™Lφορ +V4Aχ{?τaΨΌq#̜=§P(β‚ξEω|ΣAίΦήρ§ρ?v/^ΊlΕΤ_ησM7~η[°ϊ”S[ΫΪψωΟ› <ϋΤ“½ώR’γώλΎα[ίh*/zÏχ‰”R :±Ν™3g=Ώvνώ—K—/OgcΝ€Ύ¦>ΚςU«Β0l<Οds‹–,έΆu ΡE―Ώτ‡7ώηΰΐ@±ΉωΑϋοc拏ψχ/ώΣ»?ψ»©tϊxŸK)Υ ‘Q)₯Τt†,Pλ“φ\ΐgž”B³δ[7?χδΊz­^π°©‘‡rΪ‚πθΖ6N ϊ)η4N<Ω„η‚γސ‰y9‹uζΎO;q‹Ÿ"•Fίk+€!‘Π3cGς‹ζΕο|σ’‹ΞEΔς[‘ώψΟςϋίύΦηώξ―?ύŸ?~gιιΩ sηΟ?ό]φμή ?ΉύΆ»ξΌcόϋ3gΞή<—ΝοNΤθtΈ–Νœ5gόΛΞΦ?ίx~Ι₯—ύΰ{ίyπώŸ_ρΦ«ψω=σv/θ^tψW~t>ϋ·ŸFΔ?ϊ³Ώ8ή'RJν§‘Q)₯”ΗΑp]B“œ±2θμ:ελ7¬ΏWϋ†]†­IΠsT1 –%J=Ib ΆβŒ'λύ`<ώΑίϋΘω(<ϋΤίΌώ˟ωΒ—u½ ₯^I'ό₯”Rκ˜`ΐΊ·qs[ι#οYϊΌkΞ’9ΓA2Β1 ₯kε(ŽΗδ ΐ2…Žg·4εl½©ZέΌ<Δcj“—Γ8΄—θΔ3ρPžσ^2Fπδ=9OΞ“x$ο=%qΪ']ιΫ.jω³?8ύτ₯yπψ[–{ο^{ώ…|ΗU=»w§S,Z²žyκρύο|λ«ΧΙG>μόAΦΓh˜;=ςπƒϋί‰λυ}τΓίόκυΗδ’ž_³fεȚ§žš=oήώO/Ήτ²gžzβΗ·ίΗ{ljΟξ]|ΗU―=Βλήϋγz"₯Τ$•RJ©QΰcΗ]ΦΧΝόΘ»WŸ:ίfέ K&Ζ°fL%ΐͺ•„D6™ΔN]f<κΔψ²ΎΕQ™ϊ Ξ°3>1œπ„– s“ψ“˜œΫ3―Ψϋ‘λfΏΪ₯KζΆWγTΜΡ+φ}_IDtύwnΜfs|ΗU•rωxœ’{Ρ’³Ο=ο;nΕΟονλνΉν‡?ΈσφΫΞ9‚Cu©€––Φ·\}νγ=ςrGΉ4ςθΓύΟ?ψ λž?ηΌ ηŒΝΝ-I?ό‹kBήόύ~χ=ο\ΏφΉύlά°ώKόΉ];vμΨΎυοώκ$ήύΞ{ίΏΣΧ]ςωξ7Ύzκιg΄Ά΅ΏŒ―ώ*εςίqU6›»ώ;7Ύ½v”RγιπT₯”Rj‚„‘λž»’eωΗΞxθ‰=7ήgž[·ΣΦYH"dοC"0ΰ€½cd8θB‹2©΅Μθ€ΟΡ-'OyωΠ!S¦šρΨ(!ΞΧ<ΰb'/)‰ˆˆHDˆX zBτ‘Έ}ΨΩ»€N”Μl’k.\τ†Kζt΄ǐL΅nεoƒ¦Bρ?nΊνΚ‹Ξ½φ²‹Ώ~γ͝3fσS|β“Ÿϊ‡Ώύτί|κ//Ο>χΌύΩ_N½Λ‡~ο£qφοζ³0kΞάυΏ?³xι²Γ9έkΟΏΰώτǟώδ'λ4 ξΨΎ΅V―οίΰ —]±vΝ³Ίε&Θf²ς‰Ώœ=ηΕJc{GηI§œϊτ“O\ώζCŽ}ω5ΖΫΆώθή› Εγw"₯ΤAα+ωwŸJ)₯Τρ°swο£O=wΦ…ΫΓ„B”°―μΩύθgοόŎνC­΅`NΥ#S5 m’OνX“ώ―:)ϋˆ8Ε‚‹FΈΓΌζ£jt―ύIΈŒΙB’‘$eͺμ.+ΛV΄½ϋM§/벑L™fΛlέ΄ρ}ΧΌ₯\.}ύΖ›ΣόΖα‘Α];vttu΅΄Άζ.ύ›7nL₯’₯ΛWsd cφυφ΄΄ΆΈΧeΎφοy>ό»wνάΧΧ·pΡ’L67i›/|φ<όΐύίώΑ­Ζ—jΔ³O=Ρ¨1ώΗM·Ν[Ψ}ψ;>rί}gž²r֌ŽγqUJM+ZiTJ)₯Žœη4ΙάBιƒW-ΎθœΕ?|χ=OlίΎ—‡k&‘`Scήΰ‘ΊŒ«Y}GΥaχg`I%Eπ€.NS-%Ε\ιΤ“š^wαŠΕ‹Š…Θ'±q|ψ§ύm0oaχ~ώΰοΏηΊ7_ψΪχήGώτ“Ÿ:ζλp4ŠGZRknni>£εθNΧήΡ9υ3fΚ1sցοWͺ•Ÿμξ·^σφ㑇>ϋ·Ÿώζυ_>η‚ΧύΫ·oΠ£RΏ)•RJ© qtΩŠ  €Ξρ ηtΎύM]O?ίθΣ½Ο<_έ³O“¨ΒcγBυΖ— Ί§8ΰσX„ΡI5ΙI—έ˜3ΆΏeNΞK:Š£ΜΰΚΕΩSW΄{ΖκΕ³²θ™$©&Tγ#+jύvh*Ώsλ7|λŸωΤ'oΉρ{ώ£]χήttvύ¦―λυιO~b׎pυ;ήylάΫ³η†o}γίΏψOˆψ™/|ωΊχ~@η1*υ€ΓS•RJπgx*!ΐ‘¦ " €A ‚cIkλμ9Γ΅xGoθΕΝ{ό¦Ν½=}ƒΓε$vXOΨ3ŠΨξqtεŠΖπΤΙ—0aSž0ΦsοΚ†)ϊ’6ζΈΟE³ €&μ9GGΙβΨ…‰b„$a>—BcΜvZ΅zΦς₯™Ω3°%ΛθCIB¬ˆTψΣŽ]Μ¦ώœ@F†‡Ύψ~ζ;_ΚπΠΰ)gœyϊYg/μ^\(6ιΡW§{ξώιΌ »»τΣ;oΏ­£³sωΚΥ™LζεŸΛ{?8ΠΏyΣ†ΗωεSΏz΄©PόΩ»Ο°(Ώγg—ή{•&MΕή{7v5φΔh)–¨±Εk,‰&Άcμc/ˆ½w±aG₯Ψ) ,μςΌΨ<+`]ΚχsωbwζΜ9Ώ™9r3³³} ϊfΤ…ώv nOŠ ‘PβAh”ό—s€‘#‘!”™™ͺΰφί !‘œ—33 ebrZRŠ">9C–ͺLISf($™J‰Hg8Ιλ•™ίΕBIfΆσ.k Μ֏ςΏ3? ͺB£P=“GϊΝ%κ:²τ ‘)$B©+U#C‰‰±ž"C.•Hμ,uMMMuώΓ¬Χ>™™ eφ|(•ˆ|?βXjB£JjJΚ‘ϋά=δrΔ£‡ ―β …Ά‹Κ…½£³βYΜ»ϊʐ·!•JΝ-,έΛ{VZ­i«ΆΝ[·504|› @QαφT@™w8Q'e¦ΘΠv2Ez«$RS#SaσΏn@ϋ…HW4α$ –šΈ¨bhdτA§.tz‡O-ϋƒ‚…mΪΆΦv!JξhDhhDhhDhhDhhΔΣSΕΡ‰S§Ož>efjζνεωaηNVVVΩpΧΆνκΤoX$elΩ΄ήΟ―b₯*Uσmy08ΘάΜΌv½ϊοh”s§OήΌqύΡΓŽŽ+Vœυ{δƒχν±²ΆU§^Ξ ίζ€l\·:°JՊ• ΎIrR⁠}†tθ\ˆEžϋΠ 4Š1&6όΨ‰“©©©7oίώmι²Φ;Ÿ:s¦ Ϋώ³aνρΓ‡Šͺ’5+Ήt‘ -7­ω{žοb”„„WS'Œ™4vΤΞmΔΕΕ ή?kڏzuΫΏw·ΊΝΪΏVοϋον±C—,œ―^υ6dΥς%Χ„ΌΡ&‡/]΄ΰןg=zVΐM²œu_Ε‘PΌμΪ³wίώΰΑƒξΫ±mΕ’Ε;ώΩ΄yνk«QcΗ'%'k»:-˜6aμΉΣ§>ϋς«χ\ΈtΕ?»‚._ικξ±`φτ‹ηΟͺڌόaΒΗ½ϋ«^‡ήΈΊow!γλΫ;°oƒ££βΰώ}ά$[ΑYχPΕΛ™s獌Œ>νΧO"‘¨–ψx{υκΡ#)9ωφ;Y[>ρβμωσρ―^εڏ,Ev+τzRbbΞUŠŒŒ°ϋwŸ=ΙuΓYJDψ#₯R™½Γ䀴ΤTυ[₯R™œ”˜‘žiG 7J6ΗΌ~5€ί€A=zχ“J₯B©Tκ[ΑΔθ±R©tίΞνͺf•ͺTυτφBΘRdιςt!DΞΪr= ά©¨'C―_M–εΪ= »{ηVχ^}½Ό} ΞΉw±/_„\Ίπϊ”ε,X½/j ―β―\ΎxξtΉ<λςdY²<-MΥΙέ[‘ΉžkΐΫγ3€β%C‘‘’’rγζΝΪ5k¨vϋ°kυͺU]]Κ©ή¦€€ό4kφ½ϋ„R©ΤίΟ―QΣ–Y{˜;}Κ‘C …’Gο~Ÿ}ω•zνφ-›W._¬ Nε\ƌŸδPI΅*].Ÿ?{ϊ±Γ•J₯©™Ω§ƒΎΜZΨ€ή=jΦͺ3jΒ$Υۘ¨ΘΟϊτ9vB«ΆνsξE‘GΙf떍ζ–]»χΜΆάΧ?`ζΌ…2™μΏΪzu«P±βψΙΣG}σUΨ½;BˆΪ·ώfΔθφ»ζ}@ςέ©Ψ—/†}υωνΠB]=½_νΪύcMΥΨ·WWO―qσ–2™lε²ΕΧ\Z½¦jUjJΚoση ͺSV©ςWߍπςφΝY°z_„rΉ|ιΒω{wνPubmcϋέχcԟΟςiίΊ κθθμψχU@νΪύγ/Ώ–ΗρWΕK«ζΝ…£ΖyΑ/'NNJJB˜W`ii©j3{ήό=AϋǍΌ{ηŒ)“cbžnωg£Ί‡G?{φlά”ι³, ¬²yύU,Blί²yι’Mš΅\Έ|ε―KVΨ;8Œ6Tύι»_ζΞ:~τπ€/†,_½aΨχcΧύWΦ«pW„£</οιi`h˜sU•κ5κ5l”mαψΙӚ΄h©――ΏdεšΖΝ[δ{@ς΅sλ33³σ.]΅ΆF­:Λ~ϋε…σΉΆT(‡ƒƒjΧ­gnnΡ€yKρΏw¨.]΄ΰπΑΰ‘ΓΎ_σΟφQγ'ΕDE͚2ISΑ―·Z8?8hοΘ±Άμ^²r»Gω©Η†?z n°oχΞ[7Cη.Z²tΥΪΊ nϋgΣέ;· Έk€βJ# xiΥ’ω΄I—ωךυΦ¬ί •J}½½;uhί©}{ss3!Δγ'OΆοΪένΓ=»wB΄kΫζQΔγεώιδ\Nafn1eζ\#c#!„©©ΩןvΎ—O…YΚΪU+*V 9v‚j¬I3ζτθΨvον_}72&:κȁύφθΥ½W_!„›»‡TGgκ„1oZŽ+KNrtrV/ τ`ÚUYΫ ψ|ˆƒ£“ϊ­³‹«……₯HΚ{y«j: ٝ ~§Ξš§ΊUψϋq?ξτΑΦMλjΤͺ³εΕsgγβb›·ώ@αΰθT‘bΐ©γGΎ>ΚΐΠ0:ςIπΎ=tκ±λGBˆf-[ΏŠcρΒθ¨Θ\ V‰‰Ž Ϊ³«m‡Nͺ˞¦ff?ώ4»[ΗΦλώkμ€iͺ6ΖΖ&3η.TνZΏΟ>?{κδΓ°0ί ώΩ5@ΕNη:wθqιrΘΕΛ!‡3ΑίkΧ­ϋ/;[Ϋ[·ο(•ΚšΥͺ½nί±““«—*1 !ΌΌ½U)B‘J\Οb’…αΓ’½}}―†\Voλμ\ξφ­[BˆΠkW•Je£fΝΥ«κΤ«―ϊα)ΒQt€R!DFz†zIΊ<=ξεKΥλΨ—±#uϋΈOΦИ+M€ jΦ©«ώp©™™Ή§·Ο“Ǐsmy h·‘±‘―_lμK!DΝZuξά =}ςx³–­οί½«T*_Ÿ².έztιΦ#ο‘οήΎ₯T*«T}}—²‘±‘Gy―π‡ΥK*ψϋ«wΝΩΩUρ\Γ‡H…Fh/J₯R"‘H$w77w7·»tNMM]΄d©κΒγˆoΏ‰ŠŽBx–/―©KKkυk©T"„ΘΜΜBΔDG !‚vο ή·'k{gg!„κ‰5Y·ΥΡΥ54ΘεΎPyΊ<ΧεE8ŠΉ…₯ΉΉEΜΣΧΟΫ·Βμ_~W½^χΚΥ+ΠT^VšHN9wΚΜά"λ[77³gNεά0!αΥΩΣ§2Σ?ωΈkΦε‡φοkΦ²υΣ§ΡB7‚T«φ4&Zαλ?— mlmξgy’©‰™ϊuή»(4B# ‰υͺqΛΦ}z~ϋΟεB/o!Δυ+―ο)½rρβΦfωόωSυΫΠkWήΕ(Ωτμϋ‰"#cες%ΩΎΎβΪ•Λ§OΟcΓΚw§_½ͺϊU|ά“Ηα+UΞΩρ =ŽŽν;X£VmυΏ:vV*•ΗπφυB\»’ε”ύΉ|ψΟσ>eε=½„!—/¨—ΔDGΕΖΎΜ΅ΐ»Ch/#Ύύ&2*κΣA_?y**:ϊQxψή ύƒ† 566jΧΆΒΟΧ·iγFΫwξ:pθpΜΣ§6μο`ffžwΟΦΦ6>μvιΒΉA{’“ϟ9=q̈{wnΧoΨXQ‘b@Z΅ƒφμ:vψ`Rb⍫WΆn^―«§§ήάΗί?τΖυƒA{ΓΓnϋgΣή,ίG_„£dS³vέF͚ΪΏoβθαǏ τΰϊՐU,0jx`ΥjΉnbee.—Ÿ9y"λΧ!j’οN= »·paχ†έyϊT]έn=ϋδhsώέ;ΝZΆΙvI³N½††ƒƒΌΌ}λ6hΌgχΙ£Gž?{Ίsλ–}»wΦoΤXuΚ4μSΑ―fνΊϋwν°ο”ρ£ΥίӘ‡|wͺWΏOϞ>5δ³~B33σι?/Θz³¨JpΠ!DσΦm³-742ͺ]―ώ‰#‡Γ==~œιS~š4N΅ͺnƒ†ΓFΛ·ΰQγœ3}Κ΄ ?¨ήΪΨΪΝώ巜wκ9)J!„ŽH€" α€b(M.ώ$2ΘΠΘΝΥΕ₯\φ "„ˆxόΔΙΙQͺ£wόμεjuλκ€σΈΈΨ‡aa††όttt²ΚΜΜ|ώ*>ΞΗΧΟΠ(—Ϋ=‰‘J%vφοt”œ2ӟ<Žxως₯·κΓ~y{ώμ©΅mΆq5Ιw§b’£’““<Κ{°CM^ΕG=ybοθhmc[π‚ŸΖD?Žwpt,ηβVˆηΩ"«ύAΑBˆ6msωsC)#OK 9{ΆIέκΦVω·'B# Δ“§§ο9x²Bε@ΥChhRvBc|lܝλΧΪ·l¨―ωφoğλ%žΎžž‰‰qB|œΆ P\$ΔΕ™˜“"Ah”Nφ6q/^h» ΕBfffάΛΞφ6Ϊ.(%€ΐέΕ)5%%>6VΫ…Ό?λV―‹x‘ν*€βθU\\jJŠ›‹“Ά J B# 4075q΄·}όΰAΩω¬ώ«W ςτ|Ύš(ƒ233?xΰhoknj’νZ€R‚Π(%ύ½ΣRRžEEi»Ϊτ4**5%₯²Ÿ—Ά JB# ”016ςςp}ώ(%E¦νZhGJŠ,2ό‘·‡«©‰±ΆkJB# τπσρ071Ήsνz:χmeOFFϊ½7̌όΌέ΅] Pͺ₯‡ŽTZ·F%©Dά U(2΄]€χG‘ΘΈ{#TdfΦ«¨£££νr€R…Π(U τυλΧ LOMΉr%-5UΫεxRSo†\IOM©_3Π@__Ϋε₯ ‘Pژ›š4­_SWGz9€L} P6ΕΗΖ†^ΡΥ‘4­_“'¦ο‚Ά  θ4©[=δϊ;Χ―[ΩX»yzi»(E,U–ρΰ~άΛX'‡j•+θrW*πn₯“ŽN­ͺΛ»9_ΉyχϊΕ VΆ66Ž––R~­J8₯Bρ*>ώεӘΈ/MLΥ©fkm©ν’€ŒΠ(Νl­-[4¨υ$ϊΩÈΘ{‘‘! MŒυυτ₯Ί₯αΐθΘ'™ιiΪ%IRB‚βήΝ›Ϊ.€”rΉϋ€Ÿž !lmlϊχι}α减Βνlmέ\]­,-…Dβλγ-„ΙR–όρGΥΐΐi“&ͺΆύuξύ[Άή²mΫΨQίGΗΔΔΖΖ}ΠΊuy!ΔgŸτΧΧΧ740ΘYj“F ΝΝΝ:άΉC!D\\άΉ ~ϊ‰TšύοΤyΤ¦ι8̞;ίΤΔD‘&O»vύΖ«„7W—ή=?~‹C›»³611Q½~ώβωƒ‡lmlώZΎTWWWΰXmί΅λ“Ύ}ztϋHακβ’žž>~ς”‚Œ[賐νl@Ρ"4 9s”¦΅U+«£ΒΤΤΤίΟοΑΓ‡ΦΞήwξάV*•­[ΆP·lά°AΦ G|ϋ"βρ“FFEνΨ΅[!23sw?,,!!Ρί―Βω‹—Τ ]]Κ]½)„prtt)Wnγ?[βγ_5iά°N͚}{υΜ΅l}}ύΆ­ZmΫΉ+)9ΩΤΔδΐα# …’s‡φ9[Ό6΅€δd₯R‘z]«f ?_ί>½zšη±Iᘘ˜XX˜«ίΖΏzξΒEΥ7|δ}¬n„ήT(Ν›4Q―ͺQ½zΗ-ͺ³E‹Π€v˜™š !²-W=HΖ<ΛχpΈΉΊfmΰμδxσφ-!Δ‹/…ΦVΦκUΊΊΊF†―?wwαεΕΛ–_ 155-οαξξζvϋξέ\‹‰ŒŠBlέΎc»*Ό©‡vqBH₯U,[½nύž}A{‚‚€RijUǍεει™³«ŽνΫmώwλ±'Ϊ·m| z΅ͺ..9›Ό6΅i“&z•/Ÿw5ynΉ, οΎώJ}'ͺ"Y&λΩο“₯¬θΥ£»T*ΝϋXEEG !ll^ŸGϋ<ώ@΅Ξ"< P„h‡κ>Μ«ΧoT ΘΊόφέ»‰ΔΖΪF½$.>>kƒ—±±n.B3ss!Δ­;·U«Β>L–ΙΤ-§Ξ˜©T*·n\―ΊDvκΜ™ύζZŒ΅΅•bτΘέ?μšk{;»ο‡}7ς»oοέ;|μ؟«ώώ~μψm›6δlY₯re7WΧ‡ΧQσς•+“'ŒΟ΅Γ‚ΧV8ŸDUW&ΖΖ λΧ[·qStLL9g缏•………"*:¦œ³³jITt΄¦Ϋf«³Ο!„€vΤ­SGqδψρ¬‰"*:ϊΪυώώζζ――4^»v]έ&11ρεςε=„ξξBˆ‹—.«[ž;AύϊΦν;αύzχR₯2!Δ•kσYyyzJ₯γ'O©—€₯₯υύlΰoK— !B^νΤ­Η“ΘH‰Dβλγ=xΠΐφmΫ„GDhΚBΪ·;uϊΜΆ; Z·h‘³ΑΥV@Φ––OŸ=UΏ Ήrε-;ΜΚΒB‘ϊΎ“Ό•§‡‡"λ—s„\ΉZΐ:‹φ,@Q!4 ΞNŽŸ}ό…‹#nj=~ςԍΠΠΝnπΕΰŒŒŒ‘ΓΎΝΪςφέ»3žρψΙ£ππQγΖ§gd|>ΰS!„——WύΊuΆξΨtΰ@BBβε+«Χ­ΣΣΣSmενεimm|πPdTTdTΤͺ5kΧί „x«P(„6ΦΦrΉόΘ±γρ―^ΩΪΨτκΡύτΩ³;vοNLL<~ςΤΠaΓoήΊ­ϊl^•Κ•e2Ω/Ώύ§P(]ΏqπΘ‘jU5έuΩ‘];yzϊ+jΥ’Ή±±QΞωΦVC^ΫΉ{OΨΓ‡k7lάR€ίt―z.Nl\Ό"οcXΉRνZ5—­ψsΑƒ/cc?1{ώόΦω6g!λΩ,ΒΑν©hΡ°―‡ !ώΩΊνΠΡ£ͺ%圝ΞϋΉF΅jY›uξΠακ΅kͺο`462ϊό³AI)ιBˆΗ‘1ύϋ~χλθq„ΊΊΊΣ&Mό}ιrΥVzzzΓΎώzΡβ%tξ*„¨RΉς¦΅«‡3fΒD++ΛΊ΅k7oΪdηž=ί}?jΒcz|τα°―‡Κες‰S¦MΣ„ξnn ζΜθο'„J₯ΓΏύzΪΜYMΫ΄ΥΡΡQ(~ΎΎšξ;B8;9Φ¬^νΒ₯Λ]:vΘ΅AΎ΅β`~3dπΠΠ S¦J$’ΐJ•fNς͈‘…θ'WvvΆBˆώέZ»f !DΗJ1sΚ”±“&;^‘««;jψ°™?Ο-`…> ΩΞfQν8!$™y>¦ Όk™™™αΡ1Oύ|}¬¬¬²­ ¬UgΠ€OΏύjΘk6‡\ 5·΄ΤΥΣΟΆ}bbB¦2cϊΈff¦Ω6W*•ΧCC]ΛΉ¨>/§T*…GΈΉΊ¨Ύ=Bστ©­­ŽŽŽκνΛΨΨ»χξVP/TIHH {π ώΥ+''G_o?ώZωΦVQΡ1R©ΔΡΑα-k+ˆ<Ž•βIddll\_©TZ£~ΓΑƒ~υ嬳Πg!ΫΩΜiγŽύBˆžΫΌιΞ(ΛΈ€–I$wwΥσΠ€~ˆ¨ηΉό­W"±΄΄ ¨31 !€Ri•Κ•³Ύυ,ο‘΅AΆάbcm]―NξϊΜΝΝͺU­’w‘o$ίΪ AύL χ c%„p)WΞ₯\9!DzzzΞ΅yΧYθ³π~2€²†Ο4P2x—w504Θu•R©¬RΡη=Χ(#k];u ¨θ/„J₯ώ>:ΉέjhhΰιVξ½—†‘J₯];uτχσΣv!PHܞ @±6eβυλ@Ÿσ!7²5ΠΡ‘V ¨πφŸ0Δ;’££“υ$@‰Γ0”ξΞζ¦ΖΩ*Κ@ξMΌ3„FJ ‰U*θθόΟίf¦&.Ξοοι/€²†Π@Iθο£P(…ψο)ͺ::κ•ύ$Ϊ­ Pͺ(Iœμl¬,…ψ/'*Κ@_ν–(ݍ”0U|Տ½±΅Άr°³Φn=€Π@ S%ΐ7S©BH₯j•+h»@)Gh „±Ά΄pt°•‘©TϊσάTΐ»Eh δ©ΰ—)„³£½•…ΉΆk”r„FJžΚώή‰€Z%ξMΌs„FJ3S/w—J~ήΪ.Pϊιj»”i …ςι‹—OŸΗΖ'$Κd©ι™™™Ϊ. J}ύΗΟi»Š·₯#•κκιš›šX[Z8ΩΫXYr·-;„FhGzFΖέ°π‡£22¦fζ&VφŽ:ΊzΎ Κ…R©HO—%'?ŠŒΉφΘΜΤ€‚—»«³ƒΆλΌFhΔϋ–™™zη23ΣΡΕΥΞΡQO__ΫE΄/911&2ς΅[""«ψϋXZ˜i»"€„FΌgιιη―„>ηΰμμβα‘£Λ όΗΔΜΜΛΟΟΙΕ%<,μθ™K}ΛϋzΊk»(‘οQ²,εΜΕkς E@υj&¦όύ cSS*Ub"#oή KL’U«TA*εΉ} M„FΌ'Ι²”cg.ιTΤΣ7Πv9€bΝ±\9C#£ϋ·nΙειukT–π‘wΠώt‡χ!==γΜΕkz†ώUͺaimνXωYlόυ[χ΅] ”i„FΌs™™™ηCnΘ3Ύ•€::Ϊ.G›Φϊs`ߏσmΦ½C›λVΏ‡z ˜353χͺΰχ "ςaD€Άk€²‹Πˆw."2ζylΌO₯€7ΊΖ8u˜±#Ύ}wUJk;['W··ΓRΣ΄] ”Q„FΌ[7ο>p,WŽ'ί ΗΕέMΟ@Ζν0meΒΑ»u',\‘Μ,ηρ™ιOcb‚χξ:qτ°\.―\₯ڐo‡›˜ώ2gƍkΧ”JŘaC'ΝόyΞ΄ItθόβΩ³[™6gΎ­­έΞ­[.œ;ύ8"Β/  fνzmΪuBόΆ`nZjκΘ±ԝϛω“έ§ƒΎTddδΊIήƏΦSΧθΘ'G›˜švϊ°{•κΥ—ό2?τΖU{§_ X)PΥrϋ–ΝϋχξŠ‰Žts+ί«€Ί ͺ;Ω²qέΙγG…υ4ΚT*³φαμ™­›7@ρ'‘J]Κ{ή υςp±²0Χv9Pζp₯οB‘|ψ8ΚΡΕEΣχ1*22ƎόφΕσέ{χmΌε₯‹ηfLž(„¬beemnaQ«n}]έΠkΧ6¬ύϋχ_ηΩ;8κ¬\Ύd՟˝œ?ύb°‘Ρ‚ΩΣνί'„°΅·?΄ηΕσgͺΟΖΔΪSΞΕE‘i“Ό]Ώ²lΡ/ϋχμΫ ασηΟfN8|ΘOŸΕ΄nΧ16φε?|―P(„-_ςΗβ…u4ϊvδ.ξξ“Ǎ:r0XΥΓ‹ύΉτwOO―ͺΥknΩ°nηΦ-κΞύψΓHyΊΌ{―Ύ†G?qδp^3#γό™ΣΣ§LxvΏΛGέ½Ό}–,œο¦υB;Ηc‡F>ŽP΅ŒŽ|rψΰ~G'η|wJ k[[S³α|²΄€+x‡žΎx™‘‘°stΤΤ "όQΤ“ΗmΠ¨‰’zΝΪkV­H—Λ[΅mζδρYJ·ž}T-Γ†­X³ΙΩΕEqγϊΥξ=ϋτ0HΡΌe›ΧάΈv΅E›š΅hυχKϜ<Ρ±λGBˆΣǏκ4h,Mςέ##γ%­‘J₯υ7:πCΓy‹– !άΛ{ώ4qμ“'&Ζ&[ΩΨχӁ½ϊB4kΩ:-5εί6mΡ*φε‹νnώdΠ—=ϋ~"„θΡ«Οΐ>gff !RSW.ω­qσͺΛƒ>μφӏcΧϊ³Q³ζyΧccc;uφ<©TΪΎΛ‡ʌυ«WuκΪ­QΣf‹wϊδρξ½ϊ !NŸ<«§Χ Qγ|χJ{'LjψΪFxψ±‹wθισXS3s=}}M ¬¬mttuW-_zβΘaYrR•κ5ζ.\’kϋzυ«£βΧ%+ϊ”.—?y~όπ‘„„„τŒt!„ƒ£SΕJ§OS5;qμH½ŒŒσΨ$_΅λΥWύvβΰΰ$„¨Χ°‘jΉ«››βYLΜΥ+—Σες΅j«7©^«nlμΛηϞ^8wV‘‘ΡcgΥrcΣ†M›©^ίΉ}+6φe£&-’eΙͺΥkΥ τ 6φeήυ4hLύΫR£¦-’“£££Μ-,«Υ¨uϊΔqΥςΣ'ŽΧ¨U‡‘(e,ml Ε‹ΈWΪ.Κ4βŠ{•hba‘GK+«Ι3ζ¬ώsωτΙγ₯Ri₯ΐͺέzφ©]―~Ξ–nžžκΧχοήωgγΪ“G( W7έ,χΎ6kΩjΙ’_’“ΣRΣn…^οΩwnΎ›δΝΪΪFυB*•!Λekπ4*J__ίΛ§‚zIΉr.Bˆ—/žΗDGš[XͺWΩΨΨͺ^DE>BότγΨl½%%&ͺGΜ•s–œΛ•BΔΗΕΉΉ{4mΡjή¬Ÿbc_J$’[‘ΧΏcwJ }ƒW―νm¬΄] ”-„FΌC))©ΦοMU©U§^­:υG<Ίxφμώ½»'΅ψΟΥž^šΪ+•Κ©~°΄Ά™9‘w… ΖFΖί$ΥΪΖΝZ,YΈΰάΩΣI ‰ζ–5jΧΙw“ΌIς» ΚΠΠ(===))ΡΒςΏ_b"Β !¬mlŒŒRSc_Ό°Άύ/+>|πί£Œ…ΣζΜχ(ο™΅·Ό£BžώϊiRb’ΒΞή^QΏQγ_ηι=yBH$Ίzzu4*Θή@Ιbhl”œ’ͺν* ΜαφTΌCιš£rνJΘδq£”J₯«›GΧ=§Μš«T*οί½“Η&‘Χ―>{3πΛ―«V362Vdd<)2UkΝ-,«Χͺ}ϊΔ±“ΗŽ4mΡRGG'ίMή’«»Gffζυ+WΤKnί 5·°΄³wps//„Έ~υυͺλWBώΫΚΝ]ώπ½ƒκ_θ΅kKΞΟ7£†έ½ύΊ·«Wtυτ„Ζ&¦΅κΤ=}βΨ™ΗjΧ«―Ί)Jέττ}ΈP„x‡233σΎ ηνγsωβ…Ώ–/yωβyΊ\~δ@D"ρτρB˜ššEE>ŽŽ|’όί―©pχ(―§―αμιtΉ<ςqČΙRΣ^αΉY«6ΞΉqνJ‹Vm ΈΙΫ¨Q»Ž«›ΗΖu?‰Q*•Ν9qδP珺K₯Ϊυκ—suΫ°vUΤ“'ιιnZκUΌj«ς^ή5jΥή³s{Dψ£΄ΤΤ½;·/Y΄ΐΚ:ηΣ6ύ{Τ·_©ίξΪΎυΜΙrΉό…σnΪΠε£κMš4ou%δRΘε‹Ν[Ά-’]€bG"Ι,šΏψή‘ΪdlbΪ»€7­οΧ½ΛGZ―ύ{ε'ŸφτςB4nή2%%u@οξΙΙIY71·°μΩχ“½;ΆuiΫ|πgύ<<½>κΫSǎXΌHΥ ~ƒΖB§r.ΎώάδmH₯ρSJKMϋδγΫ4›8zDλv{φι/„ΠΡΡ™8uFZjκg}ΊwjέτΘΑΰ>Ÿ|&‘ό—’Gώ0ΡΞΞξ‹ώ½>lΧς·?ΧkΠπσ!ίζμqDψυ«!κ·tθ4oΦ΄Ξ­›N?ΊVέΊύ~‘^U·~C]]]3S³:Ή}((I&²Γ;³mίŠΦφvy7‹}ωβqD„D"qsχ°΄zύxEFFBb‚••uΞMRSΓΓ–/ο₯zΤjμ‹ΖF&Ζ&yŒRˆM N©T>~ž_ήΣΫΤμ[ͺΘΘ »gddδβκNŒjΡ‘Ož=}κμβbgοχ ―ztl;aκΜϊ?ΈΟΩΥ5η=¨zukά¬Ε€/†Όύ@1tοζM#IνjΪ.€ΫΈcΏ’gη6Ϊ.@IΒƒp }Φ6ΆΦXΡ¬ttusMŒBCCί ώ―{°Νeσ‚lςέ—smlni1mφό|ϋT“J₯ξξε…{.«ttu³Ž›S9§r.H5–·o…œΛ/œ;σμiL»N]ή¨7 o„F”iΏ.ϋSΫ%„„WΓ† zρόyΧξ=UΟΕŠ ‘(ŒŒΗOžξW±b«Ίχκ[΅Z7½h δ‹Π” Ίzzš5״ꃝίs=(#xz*@#B#@#B#@#B#@#B#@#B#@#B#@#B#@#B#@#B#@#B#@#B#@#B#@#B#@#]mH|\읛‘―βγεiiΪ₯δ113³³·χυ«¨o` νZ΄ƒωσ6˜?̟·Αό€R€ΠˆbνέЍ«:ΈoΟƒ{w΅]K‰§««[«^ƒΆ»tοΣΟΒJΫεΌ̟"ΔόΑΫ(ƒσJIff¦Άk@©΅mίŠΦφv…ΨφQΨύ)cΏή³ΛΓΛ»]§υ›4σ¨dmcΛίͺ !))1&*ςϊ•£ΑAA»v(”Š!ΓΎjψ(#ccm—φ0Šσ‡ωσ6Švώά»yΣHGR»Z@‘ΧYvlά±_Ρ³sm $!4β*\hLKM?cκςί~ρτς?}v³Vm$Ι;ͺ° JJJ\³bΩ―³¦›[ZN™³ΰƒN]΄]QcώΌSΜΌ·Ÿ?„Ζ·GhP<ΕΛ‹ηΟΊ·kΉzΕ²gόΌΜ₯ζ­Ϋς[Ρ2552μϋΧn7lΪόσήέfM_šώrΔόyט?x₯{ώ@)ΖgQŒάΉϊΙGttuw=ενλ§νrJ3;{‡ωK¬Χ¨Ι¨―Ώ|pξΒ« Œ΄]ΤΫbώΌ7ΜΌR9 tγJ#Š‹¨'{vhγTΞeχΡΣόΖφ~tοΣΣξΰSǎ~;¨ΏR©Τv9o…ωσώ1π6JΣό€RΠˆb!E&ϋΌww ΛΏ·μ°²ΆΡv9eHώΪΌνΐΎ=?OύQΫ΅σG[˜?x₯cώ@Y@hD±πύΠϟD„―ΩΆΫάΒRΫ΅”9΅λ7œ³hι’Ή³φξΨ¦νZ ‰ω£EΜΌR0 , 4BϋΜ8Ί}σΖΛVΊΊ{h»–2ͺ{Ÿώ=ϊ~2ÿ™LΫ΅Ό1ζΦ1π6Jτό€2‚Π-S(Ώήͺ]‡ζm>Πv-eΪΈi3^½ZΌΰgmςf˜?Εσo£„Ξ(;Π²νlΌwηΦ€™s΅]HYgkgݘqK~™ϋ*>NΫ΅ΌζO1ΑόΑΫ(‘σΚB#΄l͊em;v.ον£νB ϊ ϊRGͺ³eύZmς˜?Εσo£$Ξ(;Π¦gOc.;Σ­W?m!„055kΫ±σΎ%ζq̟b…ωƒ·Qβζ”)„FhΣ™γΗ€:: ›6Χv!ψO“Vm.œ=-OKΣv!Βό)n˜?x%kώ@™Bh„6έΊqΝΫ§‚‘±±Ά Α*W­–‘ž~ξmmR ̟β†ωƒ·Q²ζ”)„FhΣӘhgmWΧœΚΉ!žΖΔh»aώ7ΜΌ’5 L!4B›RRdF&&Ϊ―™˜˜ !’΅]H0ŠζήFɚ?P¦‘M™™™‰DΫUΰ5ΥιΘΜΜΤv!Βό)n˜?x%kώ@™BhhDhhDhhDhhDhhDhhDhhDhhDhhDhhDhhDhhDhhDhhDhhDhhDhhDhhDhhDhhDhhDhhDhhDhhDhhDhhDhhDhhDhhDhhDhhDhhDhhDhhDhhDhhDhhDhhDhhDhhDhΔ{~h IDAThDh@Ρ‹Ÿ:c¦B‘Θuνͺ΅λ.‡\)D·a,^Ά<ΧU'Nžωσά―‡;qŸ«ώŽ‹‹+DjΫwν>yϊtAZξΪ»οψΙSoΊΥ»¨xG(z ύ¦―――££“λΪΕΛ–Ÿ=Ύέ–χπ:pπθρΩ–™0qθ°αΗNœLMM½yϋφoK—΅ξΨωΤ™3…BeΙς?ΆοΪ]–+ώZ΅mΗΞ7έ*AΑf͝WˆJ€wDWΫ ΄ΉΊ{_Πήν[‹Όg©Tϊω€Og͝[·v-CCCΥΒ]{φξΫΜZIš\>{ξΌ-ΫΆ«:΄³΅ύqάM5R½ύ¨g轢ιθθ¬ί΄YΞϋφκ9zΔpMG( 4 (έ»&„pwuU/™2cfπΑCί ύjϋζM“ƍ[ΆbejjͺzνΊ›fΟ›ίΆU« «W­ϋk₯““γΐΑCThβααφΰAΜΣ§ͺ·­š7BŒ7ώηΏœ8u:))Iabl\ΉR€₯₯eG9qςΤ†M›»vκΨ±]»lΓύΆdι‰S§Ώ2ψpΠήΝkW;ΨΫOŸ=ηω‹yTθθΰ°hώ<υΏŽνΫ !κΧ­›o‡sgNoΫΊ•Ύώ–υλΪ΄j™­ΫΩsηνΨ½gΪ€‰'ά²~—§ηπΡ?„=x nποφΧnάXυΗ²­Χ7mάh톍‘·nεQ'P„F₯{aaBΥΫΘ¨¨=ϋ‚ϊφκ9 ?Ος­Z4ŸπΓuc™,eΙT œ6ib€ΏεJΏΞύY™™ΉeΫΆ<†(οξ.„P?΅U‹ζΣ&M451Y³~ΓΠaΓΆhΥ£OΏ΅6ͺοΟ,Θ(Ο_ΌXχ_γFͺθο—m8==½Ο>ιί§ηΗΆ66~*τοΣ;33σα£π<*466j¨‘ꟍ΅Uπƒ΅jT3rxΎΊΉΊZYZ ‰ΔΧΗΫΒάμ955uΡ’₯kΦoX³~ÈoΏΙ{GM{tαεΕΛ–_ 155-οαξξζvϋξέ‚Š΄΄΄οFŽ’ΙdΛ_€NΡ…ξ0**ZQ) bΦ…φvΆ7oέVΏ53}ύœU©T"„Θ,XΈς@h@QΧӏŠŽΙΜΜT=ΛΤΚΚJqλΞmg'GUƒ°‡U€BX[[ !FΡύΓ9»ͺ[»φΉγGs.ϊτ™ΒΣ³Ό"ώΥ«Ζ-[χιωρ˜‘#Τ ΈiΛΏͺ{>σ%_SgΜT*•[7χφςBœ:sfΑƒΩpβ”i·ξάωuξΟ^žžoί‘κH>|ξRξυΣ}nήΊντV‘~~¬ .’θπ™F%o/ΟΤΤԧϞ©ήϊωϊ !.^Ί¬npξόυk/OO©Tzόδ)υ’΄΄΄ΎŸ όmι²<†ˆxςXαλν-„°΄°°·³;zόDΪ~DBb’\./οα^θQTnέΎΡ―w/UΐB\Ήv=ί­„Λ\tΰΐwCΏj¨a‘t¨Ϊίs^½Θ¨¨/_V ,Θζ@‘P”jT―&„xώίsb*W ¨_·ΞΦ;ƒHHHΌreυΊuzzzͺ΅Ά66½zt?}φμŽέ»Ÿ<5tΨπ›·n7o$!?‰444T_pSέ€ϊι /ŽŸ<ύ(<|oΠώAC†΅kΫ¦Π£¨x{yZ[[<΅jΝΪ΅λ7!^ΖΖ* M[;qβχeΛ]]\lmmwοΫ§ώ§T*σνΠΖΪZ.—9v<ώΥ«¬}VτχkP―ήΦν;=š’’ςπΡ£ “§θλ·OΎ{Ό nO@QͺY­šŽŽΞ£πˆΊ΅k«–Μ˜2eδcG› „ΠΥ՝6iβοK—«Ϋϋz¨\.Ÿ8eΪD1MαξζΆ`Ξ윏0ΝκQxΈ·—ϊi:νΪΆI“ΛώΎψλα―οP­\)`Ζ”Ιͺλœ…EEOOoΨΧ_/ZΌδƒΞ]…U*Wή΄vυ°QcΖL˜hee©ήΗlΞ]Έ˜™™ωψΙ“ρ“&g]ήΆU«|;lή΄ΙΞ={Ύϋ~”κ{³n>cΚ€±“&υίg;ννμV,]log—ο^oCΒ½ΞxwΆν;βγ_ΡΪ^γ²/ϋ},„XΆfΣ{, ω(g’³tυƎuΧv!ωcώCΜΌ|ηΟ½›7t$΅«ΌΟͺJ™;φ !zvnσ=nBBRβ…Ώͺ—¨ΎR".>’ŸŸ‘‘QΞM^ΖΖή½wίΘΘ°r@€ŽŽN§¦¦6 ύˆoΏιΦ΅KΦεirω£ππ'‘‘F†Fn.Y?ψWˆQ²Q*•ΧCC]ΛΉ¨>©T*…GΈΉΊ¨ˆϊ¦ςν0ζιS;[Ϋ\‹ŒŠŽ~ψθ‘³““»››:9οWPΔΎό|`>ύΒ#"άέάTK$‰gy!<4mbcm]―NξWν²ΩΉg―•₯eην³-7ΠΧ―ΰγSΑΗ'm >J6R©΄JεΚYίz–χ(D?ο0§Ή:;99;9½Νθΐα/(b^εΛ§χς?Wζίτ )Š•―;j€ϊS‘ή5B#Šή—ŸrvvNOO/Ϊn#£’ϋτόΈaύϊEΫ-€άͺεK>ύ|0‘Q”’’’ΗŸξέΏ€ίԁ}{…χοˢꍊ,ΈBw›š’"„042*κŠή‰ΌηOJϊΤJ–%ϊELLτ„©3Χoέ5ηΧί7ο š>χΧΤ΄΄ι“ΖΗΕΕνpWjζOIŸ!jyόπΙCΆϊ XaΦf…ϋT²ζ”)„Fh“₯΅υΛ/4­•κθH₯R©Tjee]³vέ_αTΞεο?—§ΘR²6Sdd„έΏϋμiLΦ…²YΊ<]‘œ”˜‘žώΊ±Bqηφ͜ΏUWͺRΥΣΫGS%Ή‘–"K‰€T*s]ϋθaΨέ;·Ίχκλεν{δ`pΦfšŠT­Ίz=)1QSIy¦qίσά •ΈΨ—B+kλΌΗ-&ςž?y(ιSλŸukβγβΎ9Ίa“¦ͺ%Ίzz5jΥξ7`P\\μΡƒrv%KNJKMUΏU*•9g"κΙγΠλW“eɚ Ξ[©™?%}†¨δρΓG-φ勐K^εQΏΊΒdYrΆΉ‘šHιryΦfΩz₯ΘdΙIšΆΚͺdΝ(SΈ=Ϊδ]ΑΣκU™™™‰$ίΖ†FF]>κ±dαό“Η·jΫ^΅pϋ–Ν+—/V].p*η2fό$Ώ€JBˆQί|vοŽβ£φ­Ώ1Ί}η²ΩόY?]>wN–"B8»Έ|Τ£wϋΞ]Uύ θΥ­BŊγ'OΟ9¦!„ιrωόΩӏ>¨T*MΝΜ>τeΞΝμΫ««§ΧΈyK™LΆrΩβkW.W­^S΅*g‘Bˆ EΖάιSŽ: P(„=zχϋμΛ―r= ίχΌχ"«Ϋ7C…ήός=ΕΑ͟<”Έ©΅wΧWχΖΝZdλ°Σ‡έœΛΉΨΪΫεk@ο5kΥ5a’κmLTδg}zŒ;A½Ώ±/_ ϋκσΫ‘7„ΊzzΏΪ΅ϋΗoz$Kλό)q3D%>BˆΤ””ίζΟ9$„J₯ώ•*υέ/oߜυ«+œ3mςΕσg7mίkjf¦κδΰώ}σgύ4sώΒj5j©›eλαΞ­Πƒϋχ­έ²ΓΪΖV΅ΥΎ];͟3{ΑoUͺΧΘZpɚ?P¦p₯ΪT£vΔΔ„«—/°}ΕJ•…‘«ήnί²yι’Mš΅\Έ|ε―KVΨ;8Œ6τΡƒ0!ΔψΙӚ΄h©――ΏdεšΖΝ[!V―Xvαμ™ώƒΎ\Ώmχο+VΩΪΪύΆΰηΨ—ω\§Κc!Δ/sg?zxΐC–―ή0μϋ±λώ+λΕ!„B‘8T»n=ss‹&Ν[нI,g‘BˆG?{φlά”ι³, ¬²yύΥ―_/,g·yοEV§Žφτρ΅΄*ιΣω“‡4΅^Ε{ωϊδŒ::::uκ7πςφ-ΔΨΉu‹™™ΩŒy —Z[£VeΏύrιΒω7ν€ϟ4CTςώα#„XΊhΑαƒΑC‡}ΏζŸν£ΖOŠ‰Šš5eRυ«΅jΣN‘‘qζΤqυ’c‡‚ν«TϋŸμ—­‡­?P*•§ŽS78~τ­}εͺΥ²νrɚ?P¦‘Mώ•ΛΉΊύ{χWeωqό:‡ ‚μ " Š{ο=0³ά«a–M›šiΓά•VZίΖ·,¦iš~Ν½χ65Pœ Ω’€ Ώ?NΏλ7^ΟGpξsέΧύ9§Owη}ξϋάχŽrRΘΡΩY‘”t[‘•™υλŠš·l=ύέχš6mήbξGŸh wlέ$„pχτjΨΠV¨Tύό­­m„ΖΖ&γŸ8bΜ8{{Ώ&MGΌ°°0ϊΦ­R6Wϊ&βγξέ=rΜψ±=ιέΘ§gί~―Ύ9³Ψ gOŸJIIξ4DαβκΦ΄y‹γGκ>Ϋ•,RamΣpώ’%=zυiΫΎγ”Χ¦ !"#"*TX±iK\”F£Ω±εAC)ηΏΕU΄Ja@­•/„pqqΣUΥ΄Yσ‹—ΆοΨΙ§±ί[οΝV«ΥΧ­Π u» ¨C΄JίωΔΗΖμΩΉ}Θ°Žνδμo`ΠΨǞŒ‹Ž‹}ΰ~I«Kχ ¬­: }x/5%δ―sQ«υY’Ψ ­ΪΆspt:zhΏn­Πΰ~ƒŠ­epύυ §§BI*•jόΔ§W._6νέΩ––eŽΧ!ΌΌΌ…Q7"ο§§ϋœώK7ΐέέγκ•+\χΉ)― !βbbnEέLˆΫ»s»B–²Ή7qιΒyFΣ«_έS]Ίu/φ1hοm–Mš%'ίBtμΤεΪεK'Žι70HΆQ? ΛΏ―ακζ.„Έ_‘Β*=ψΠήέQΧ#Η?υ΄¬ΆΪ¦’ύS j-ν/Ύς ς+χJe:vιͺ;timmγλί$ζͺ•Sέξκ­w>aa¦uλŽυ3nĘq₯Ώ &¦¦}ϊά½c[Ff†•₯Υ±Γ‡ ‚zΈτ΅ΤjuΏA-5%ΕΦΞξψ‘ͦ ΑΕ†\@½Bh„Β&½0eωΧ_|½τγ³η—982<\Ρ€Ys!DB|ΌbΧΆ-{΄ŸΐώŸ»»ηΧ½Όκ§ε‘ηƒ-­x5jδαεVϊζJί„φΆΆœIedllnf{˜–vοΤ‰γωyy“Ə,Ίϊώέ;K E'T«UBˆΒ.+τΪΛ98??Γχίτπ#M[Θj«…*Τ?₯0 Φrpt²°΄HŒ‹+9maaαΛώλζξ‘ϋ½œLn^ρkXΫ4,ϊΠΫΫηΤΙγ₯ORTοκQŽObbΌΒΫΗ§τ2J8xΘφΝœ>q¬ΐΑ‡μkΩ¦­›Ηƒ_uQ‚Ϊ°vυρ#‡†yτΠ_ΏΖ~ώEh@ύAh„œί|wφβωψ΄·Oγοέ½]Ρ΄Y ΒΦήNρkΣ6’<ϊbΙbF³lΕ―>ύ„goŸΖj΅ϊt‘ƒ!Ή99S_~ώ—Ώ/ΉnDΨ΅Ψθ[£ΖNΠ~hB\ΎZfa₯oΒΟΏ‰"4δŸ“ΗBΞώλŠ{wmwqu:|T‡Nu ytΈF£9ό ›"”_…^{yG„]ύtαά_›ζΫ€2ΧPQV9ϋ§ΧZO<ύlfVζΟί}Sτ& Ή99Ώ­όΩΨΔ€ίΐA%·hΫΠ6))Qχπ…bBϟΧΝv/5%&:J{ι—ς¨σύcpRζΞΗ? @q!δœn•U?~?mΚσωeG4h𐳧Oνή±ΥΤΜ¬WŸώeŽΧτΠ…ΏvlΫ"„(vnͺAχΤ„F(ΟΨΔδϋ5Ώ_ψλάΫ―λλη[ώψcύΊ?Φ―[σΛOsί}λ«Ο>i,pκΫ±moο0lԘsgNοέ΅=γ~ϊŸ'OΜ~ϋΝπkW»χμ­`ggŸ—›{ςΨΡ΄΄{>}mνμŽ:—·aνκMΏ―B€$'kοlρ@₯o’iσ:uή΅}λαϋξ§§_<²ρχ5Ζ&&ΪuoDFD„]λ7pp±#‡]Ίυ037Χ^ζΎX‘εΗ*τΪΛœš’ότ؍ύό§Ύσ~ωk¨=dύS ƒn-!Dηnέ=΄cλζΩ3§Ψ·ϋΪΥΛϋwοœ9νΥ !ΑΟ<’—·OΙ-6 Όt1tίQQ7ώXΏnG‰―߈ rΙβ‘7"#>ύp‘±ρ˜ O”ηΝ¬“ύcΠRžŸ@Χ=χlίvμΠΑ€Ϋ‰[6nΨΉmKχ^½΅ΗKί/υ’——χΫͺ=ϋτ“–,9Cίƒ Χ¬ψ±]‡ŽŽάΖΠϋκ NOE­Π4°ΕW?­š<~€·Oγ7ޞ₯]Ές§εΪ?Μ-,ZΆjσά”Wƒ† 555Υ­5ω…—sss—.ϊ`©B/ο9 7ω{|uλΥ{οξσgΝΤήjlς‹―¬XΎμι £…-Z}ύÊοΏ»xαœ†vΆν:t’Vϊ&f̚χαάY‹ζΟBOχύU?ώ]σž]Ϋ…ύƒ*6‘Ή…Eηnݏ<uσz#ίbE–«Ψk—NOO{fάΘόΌΌ•·κy-=°JaΠ­₯υƌw=ΌΌϋυݍ1ζ/Z₯{nξιη^ »ryΙ’…*•ͺYσ–oΏ?oξ»oπΨΔ§O8>eςD!„΅΅Ν‡Ÿ~ξζξQζ;YχϊG¨„wH9w>3gΝύδΓωΜύ; wνΡsκŒχX±y\\][΅i{!$8hΘPYρ%gprviέΆέωΰΏ†<ςΟ ·•κŸB‘ί}Y• *y  ͺlέ{ΔΫΟί©άΏŒZΉ|ΩϋΣ_ŸπΤ3}ώuΡγ*₯KIIΎinnΦ4°…‘‘Q±g“n'Ϊ;8j—k4škW.ΉΉ{ΪΪΩiΖDίςππ42.γΫ“R6QXX}+κ^jJ“€fζ₯ύ¨E‹¬ςΏφŽŽΊωτ˜αΙΙwΧnΫ]?QΉώ)Eνo­Όάܘθ[)))^ήήNΞ.eΎ’Δ„΅ZUΚΘ„ψΈŒŒϋ>ύΚӍτOνο2₯έK‹‰qvu΅wp,₯ώΚ)6Γ—K?>yτΘ―6k_uεϊηΪ… ­ΜΫ·jVιͺ°vσn!Δ„αΕ/` ₯ 4’ν9rΪΞΙΩ£Q£ς―²wΗΆWžy’m‡NKΏύΑ«‘O΅•q`χΞi/Nvqs_±~“»§WΩ+ϊ§ΖΠ?¨Μ¬Μ'G>zμ€g_zτOθΉ³^.ŽΝ|«­ΊΠ ψM#ͺQCk«ŒϋιZeΠÏlΪw$1>Ύo‡–Ÿ.œ›•™Yφ:¨ α“Ζ ›8κ‘žύϊ±χpωΔ/θŸA ’ζϚ9ν₯η…£ΖMΠ§4Ω™6Φ ͺ­RΐƒQœμνRS ‹\ΰ±<š·j³Ο·η,όρ›―Ίϊ~ψώ;!ηΞpH\Y™™{Άo}αΙq};ΆŠΉukΓύύy΅UƒΊφρ‹ώ©&τ*ΝHmΤ«oΏ§žyvϊ”ητιŸ{©©…B8ΩΫVSNOE5ΚΚΞΩ}θ€³@{g§²G—p'ιφΚεΛΦό96ϊ–΅΅M@σφfζζe―‰»Ÿ–¦)(θΨ΅ϋΔη^6zœqY?¦2tτOU‘θ}TaD\Ύ$ςσzwm_εEΦ+œž  ¨^§Ξ…¦gεΆhίNŸI.‡ž?χηιπ«—ο₯€dηdWUmUΛΔΤ,/7Gι*¬Ak'g—ζ­Ϊtλέ§<—K©cθ=Ρ?τ>ͺͺ²3³BϞiί:ΠΛ½ή5aΥ"4¨B#ͺWΪύŒΗΞψ6mκθR—7Ÿ““spΑ‡.~{ <θ裞τOXhh~nΞ€žTάrC?„F•ΐoQ½lX5φrΎ~½  _ιZͺQdΔυ›7’²³kιQΤrττQϊ'599%9Ήm‹#(‚ΠˆjΠX₯WΦαΓΪaΧΒ4Mdx€…ΐ Ρ?ΠGοŸœμμλW―yΊΉ8r P‘ΥΞΤΔ€[‡Φι©©ΡΧ―+]K΅ΘΜȈ‹·j`‘t-0<ττQηϋ§   μβE Σv­š*] Τ_„FΤ»†Φν[5‰‰ΉySιZͺ^xX„‰‰i―^=γcγ*v_J€ώ>κvδηη] ΥδηuοΠΪΨΘHιr ώ"4’†xΊΉ΄kΩ4ώΦ­ˆ+W4Όsc-αηολγλcfnQgΟC5‘ :ά?YY™—ώ ΞΟΝιΡΉ­…Ή™ε@½FhDΝρρrοή©M<pX IDATZJςεΰΰτ{χ”.§j€έ»wϋφν&M›¨Υj?ίπ°Ίy†ͺ ύ}ΤΥώ),,LŒ½lifΪ―{›VJWυ‘5ΚΙΑ_χŽVζ¦—CB"―\ΙΞΜRΊ"}]»niiιξα.„hΰ'))99Yι’`0θθ£ξυOaaajrςΕsηn]ΏξλεΡ³K[3SS₯‹c₯ @½ceiΡ£S›ψΫw.\‰8ζOΫ†vŽ llΜ-,ŒŒ ξrκ‘α‘MόΥ*•ΒΝέέͺAƒˆπΘΞ]μ• †ώ>κFh4šόΌΌΜŒΜ΄Τ””;w²³²άœ{vleei‘ti€Ώ‘ 7gGW'‡Δ€»Ρq‰q·’ςς ς.Ž™YΩΙΙΙΞφ O>¬]ΐΒ,4δ‚&;Σΐ²/”@@u―¬¬,½\yΊZs>*Τ2„F(F₯RΉ:;Ί:; !2³²32³ςςς …!έΛρδΩ 6Φ ϊφθ¨ϋˆΦΨΫ}ΓΆ}Ύ^ξNŽvJVC@@u¦Τj΅©‰±u+S₯k<‘΅‚₯…Ή₯…ΉUTL‘7£γΪ·jζικ¬[θακ|θΔΩΈΫIm[rK1”†ώ>θ@MβB8@%EEΗ₯¦₯·lRlyλΐ&‘WΒ5…†tΘ5ώ>θ@M"4•tαJΈ«³£³cρkN΄iΡ4ύ~ζΝ[±ŠTCA@τ &qz*PΝΕ«Ÿ}·JQXX¨ΡhŒŒŒ΄ΟͺTͺ WΒ}y*Z#j/ϊϊ 5ŒΠTFVVvΟΞνt’ξ^ΈΤ§›n‰…Ή™uΑ0Π?Πύ¨a„F 2XYφξΪ^χ0τjΔ…ΛaE—₯  ϊPΓψM#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€@u‰‹[½v]Dddy―]ΏαπΡ£U΅ιM[·;qByŽ?±τ‹/:¬T¨(ϊϊ UˆΠT—ˆΘλ/ύ,δBhyΌrΥξ½ϋ«jΣί~Ώ|ΣΦmΪΏwνΩ»xΙJLrώ•©ΣώχΗ¦ΫIIUUʏώ>θ@2VΊUoαœΩΦΦΦΪΏƒΟŸίΈyΛ;oM―θ$ΑηΟ !6όΆΪέΝ­ŠλCνF@τΤ=ijΘύŒŒœœ!DFfζΕK—8,#3σ|hθŸΝΟΟΏŸPrνΜiiι·’c„Ϊ·kΠDϋTnnž"===//O·JjjκΉΰΰ””Y΅™YYBFσΐ­θ¦:}ζΜ•«Χrss‹½ήμμl!DffVXxDaa‘nωΕK—ξgdΘ6’tΛιŸJ tΛι¨Ž45dτ„Ηϋφξedd΄fέοڏAO>6aζ›Σtςσσg͝·cχž‚‚!ΔδIOM}υέ³«ΧϋΟΧΥ~lςςτ\΄`~λV-΅O=>ι™vmΫΨΫΩ­Zσۘ‘#ޞώζΓ#F΅jΩbΙ’žyα₯«Χ !zτψώ;o=κ―ΰ~χέΉΰm #}dΦ;o›™š«vςKS‚CΞ !ΊχνμΣ“ήxεε’[ΙΙΝύxΙ lβδθ8η½wϊτκ₯{½Ϋ·³°°Ψ΄u[nn“£γβž4ΘΜΤtΓšΥƒ Τh4oΎσNa‘ψcέo;6mœϊκ+[ΆοΨ±kwΙR?˜;gάθQBˆ kV?1aό·ςρ’₯›·m_8wφ±ύϋ6¬Yνηλ;mζ;‘Χ―λ&Ω΅wίΝ¨¨/–|2ούYΉΉΉ/½φϊΎƒ‡ζ½?λ—ΎoΡ<π“Ο>ΟΜΜͺ²7· θ}Π?τTGšceeυύΧ_[ZZ!^~ρ…CGކ…G΄ Τ>Ϋ°aΓ―?Lϋ¬΅Νψ‰O] oΦ΄iffΦ·Λ—·mέzαάΩΪ‘_,ω΄χΐ  όρ΄K’ξάΩΌ~]cŸb[τφς²³΅*U@!Dl\\rrʐ  νΘΙ“ž255573+Yͺ—§§ƒ½½’‰ΏŸJ₯*Ή•ΨΈΈ›·Œ1|ψ#!ll¬σι'= όξ‡Ÿ>ωθνxkλ_ώ™ΉΉΉβδ©Σ»χνϋhΑΌ–Ν› !7φ½9σΒ##Ϊ΄jU%οm}@Π?ϊ θ¨4B#PsZ΅hύL&„πφτB$ωP³€&Ίg=άέ…ρρρBˆˆΘΘ΄΄τΐfM<{N7ΨΛΣ#τeέÁ%?±•δζκκια±vύ†ΤΤ{}zχμ±γ“M(ύE·rρςeFΣ©Cέ³––ώ~~EΎιo ύΔ&„pttP©TΝώ~θΰ „Έ{7Ήό[ύCθƒώ‘ @Ν±n`­ϋ[­V !t—gBΨΫΩ?πΩΨΈ8!ΔΖM›uW±ΧςφτΤύνκβRžΤjυŠεί­\½fϋΞ]ΫwνR«ΥΪ΅}oζ ?_ίς¬^t+qqρBˆ–-šΰμδxωΚUέC;[»b[76fŸSyτύ£ϊ‡ώ€Jc ΤέyVβίΧJ>[”½½bζτ7ǎ© ΞNNoM}cϊ―‡GD8|ψΗΏΌυξ¬?ΦύVΡyμμμ„7nFyzxθ^ΎrΥΝΝUχPφŠ„%^=ΚFθΠ?•@θΠ?PQ\¨νό|}Υjυ‘cΗuKrrržœόμΧΛΎ«θTΑηΟ3.&6V₯R4ριΉg‡>48κΦ-ν• +$ΐί_qϊΜέ’ΨΈΈ;wοΆmέΊ’S‘ZΡ?Πύ„F φstpxlάΨ§NmήΆ-==ύΘ±γ―LvωΚΥώ}ϊ”gu{ϋάάάƒ‡€ή»Χ¦U«ΜΜΜ|ύίδδ”‚‚‚ ‘χ<ΨMk΅ΊΒ»‚ζΝztλΆqΣζύ‡eeeέΈyσύy ΜLM'=ωDΕ_"ͺύ}Π?Αι©€A˜ϊκ+ΉΉΉ³η/œ- !y{ώΙΗΝ›•gέώ}ϋlΩΎύ·fhο“6νυW.ZάwπCFFFΝζ½?«rU}4ξ»sηM›ρΆφ‘³“ΣΛΎqvrͺάl¨>ττAT%Ψ ’B―F¬ΫΌϋƒ·_){¨ξ&'‡…GXX˜·jΡΒΘΘ¨Bλ&$&:9:jΧJKKΌ~=υή=77ΧJ|Ν_T\|ό›7έέάy{λ9U½EΠ?ϊ θŸ Y»y·bΒπΑJΐp€0φφέΊtάΊE/ττA@}ΖΧr)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€Œ•.BˆŒΜ¬Μ¬μάΌ|! •₯2’Sο !bn+] ύ}z¨ΥjScλV¦&&JΧx0B#SXX˜t7&.1ρNr^^Ύεθ%ω^šβΟΰKJƒD@u¦¬¬,ݝΌ=έlX)] ΰ_PFόν;Ddff5΄³uχnΤΐΖΖάΒΒΘΨX₯R)]ZeD„GވŠι§…ΐ Ρ?Π‡‘χO‘F“———™‘™–šp'όF΄›³c«@+K ₯KόΠˆš–‘™r)μφdGgη€–­Μ,ψXυ—J­653353³΅·σφυMMNŽΎ~}ίΡ?ύ}Όš5ρ1RsρP‘5*ιnΚιΰ‹¦fζΝΫΆ΅nΨPιr΅‹­½}C;»ΫqqΧ£nήINνΪ‘₯™©©E@}Ηx¨97£γNœ9ocgίΌ];#ΰT*•‹‡Gσvν2srž8—v?C銠Ύ#4’†ΔΔ'_Όζζνν¨ζt#@©,,,[΄ogljvόϐ¬μ₯Λ€zΟξ¨ )χ ½ζζιιιγSέΫϊuŏΟ>9ΎΜacΌvυΚκ.PiΖΖ&M[΅R›œ8w!Ώ @ιr ώ"4’ΪεζειΉ'<9I1ξ±'ž}b|aa‘"';ϋ§oΏξέ€φπΰ°Qc>˜σξ―+~μΥ―ιυ888.ψx©Z­:bTΎ&ΝΚΓFŽιΥ·ί7_,=qμΘΨǞBœ8vΔΨΔ€G―ήUρ>Κ`niaηθxγV¬—»τ[H@5αH#ͺQ|β΅‘‘ƒCιΓ:wλ½ ‡‹‹›’[Ο^Ϊε^ήήBˆΫ ηCώΚΛΝνΠ©³n•φΊ&'ίMΊxζτ©‚όό‡]niΥ gί~ΪΏ―]½’œ|·WŸ™ΪΪwκuσzrςέλιΡ§Ÿξ¦ ½ϊΘΈŸgΣΠΆ]‡N'ŽΡ.?qτH‡N]¬XWδύTžƒ‹KrjZNN…@½Γ‘FT£€δΫ†ͺ²ξΚhowͺT«UBWwbγβLMMύš4Υ-ρππBά½“”gfnnΣΠVχ”ƒƒ£φΈΨ!Δsή-6ΫύτtέΘ½HξBˆΤ”οF>} ZΊψƒδδ»*•κΚ₯Π·fΝ)ύuͺPC[[•IΙ©žnΞJΧυ ‘Υθ^ϊ};§²Ο#*3Uš›[δεεέΏŸήΠΦN»δVΤM!„½ƒ£……ENvvς;φŽgΕΧ#΅XZZ !~ς™Oγέκ£τΔ(„ΘΝΛΣύ}?=]αδμ,„θή«χKMN;*T*c“=z•ωUEmddne™–~_ fqz*ͺQvv©™™ώσx5ς),, Ρ-Ήzε’MC['gοF…‘ηy*4$ψο΅Ό !’n\wrvΡώsιΒ…oΏό¬ΜŒvυŸΩΞ‡›˜ΈΈΊ !,­tκυΔΡΓ'ξά­»₯…₯ώ/ P~¦¦¦Ωœž 5ŽΠˆjTPP Vι?O‡Ξ]ΌΌ}Φώ%1!A£Ρœ9}ςθΑύΓGU«Υ»uχπςώνΧq11ωyy[·ζή½ΏoδΥΨΟΏC§ΞΫ·lΊu3';{Η–Mί~υΉ­½ΊDh\ϋλ/3^Yχp릍'ΝΝΝ=wζΟ­ϋmΔθqΊUϊτ|.ψ―³ύ>€λTˆΪΘ(Ώ @ι* ήατTT―*Ή–Z­ž΅ΰƒηΌ?iόHSΣΌάάGFŒžπΔSB##£Ω >š?λνΙOŒU«ΥΎώMž˜4yήέΪ§Ώ3{ρΒ9/<υ˜‘‘QaaaА‘ΟOy½δόΡ·’BΟλydΨΕ 3ξί715νΦ³ΧSΟΎ {ͺkχžΖΖΖζ]Ίu―ŠW¨•(TΊ¨PΨ–½‡u[Z5Ψuψ€ξa#_έCŸΖ~ίύ²:::*-5΅±―λ.[κγλχΓͺ΅‘‘αž^T*Υγ“&kŸ²wtόδ‹oβccn'&Ί{z:9σΛυΫvλώžρޜούsU›6ν:NyύΝλαξ^^ΕΞA537·³³οέo€‘1ν ^ΰƒ/ †Z­nΤ¨±hτ€§ŒŒšJοιζαιζαYΡmω4-ΉόΜι“·6’B³†‹Π”KZΪ½©Sž»“”4rμνuq€ϊ€ΠόΓΒrΦΌ›5oώΐ§Ζ>φdΫv*zΠ0h„FΰΖ&&½ϊυ—=5δ‘α5\ 8nΉ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)c₯ Κv9τόΉ?O‡]Ήt/%%'7GιrΐΤΜΒΪΖφΕ‰γ•.δΑ4°vrviήͺM·ή}œœ]”.§¦Ρ?z’θ}Τσώ€ΊAUXX¨t ¨³ώΨy°I`s{g§Κ­~'ιφΚεΛΦό96ϊ–΅΅MΣ-νμνΝΜΝ«ΆΘϊ #==>.."μͺ¦  C—nŸ{qψ˜ρΖΖuό;#ϊ§ͺΠ?τ>ͺΆΒ/_Ά0RunΧ’j‹¬WΦnή-„˜0|°…0$uόϊ0Pωyy?}ϋυg‹š™™ŸψτΠ‘£[·λ R©”Λ°eef;t`Γo«ή|ιΩo>ϋτƒ₯ιΦ«―EU ϊ§:Π?JΧeΨκO@Δ‘FT£ΚiΌz~ΚSΗDG½τΖτW§ΏmaiYMεΥ[7"Βη½3}ίΞν#ΖMψδ«ο¬4PΊ’ͺDT7ϊϊΠ³8¨?Ž4¨.„ƒΪeοŽm#φvqs;tξβŒΩσωΔVϋ7ωeΓ–U·;x`δ >q1ΡJWTeθŸ@@uΈ #4’YΉ|Ω³F;aΝζ^|”.§Žλ?xΘΆΓ' ςσ‡φιvνΚ%₯Λ©τOM’ ΊΧ?P·Q[μέ±mΦ›―MŸ5χ“―Ώ361QΊœzΑ«‘Ο¦G}|ύ&v'ιΆεθ…ώ©yττQ—ϊκκF@}@h„ςςσς^x|\λφ>ώς[₯k©lνμW¬ίt#2β?‹?PΊ–Κ ”E@†ή?PO‘ΌΏύ*&:jι·?pV˜RόšΝœ³ΰ»―>Ώ¦t-F(Žώ> Ί ž 4Baw’nΎθƒ—ή˜Ξ•'”5ρΉύόΌ7CιB*†ώ©%θθΓ@ϋκB#φΛχߚ™™½:ύm₯ ©οŒg}°xοŽm†u%Cϊ§–  ν¨?PRaaαΊU+ΖO|šϋ‘Υύ‚jδλ·nε ₯ )/ϊ§V‘ ƒλ¨WP•‹b£o 9ZιB „*•jθπQ{wnSΊς’jϊϊ0Έώ€z…Π%;}ΚΪΪ¦u»J‚ΏuοΣοzxXjJ²…” ύSΫΠ?Π‡aυΤ+„F()όΪ•&ΝU*•…ΰo͚·BD\»ͺt!εBΤ6ττaXύυ ‘JJΉ{ΧΡΙIι*πG'!Dςέ»JR.τOmC@†Υ?P―‘€άΌ\S33₯«ΐ?΄:r²³•.€\θŸΪ†ώ> « ^!4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)c₯ (—B!Ξ^Έ|ύVŒ…°[± ήJWΐΐp€ ΎπφpυrwQΊ †#ΐ0¨„θΨΊyηv-”.κŽ4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ΚXι…eάOί»k§™ΉΩG†—>rλsvqι½ηŸέ·g—΅Mηnέ«‘F€άάά£χ‡‡]ŽΊikοΠΨΟΘ£Γ­,­tΚί3kW―lέ¦mσ–­«³^( όϋŸRμΩΉέΞήΎS—nU2 €’8%©TͺΒΒBek8°wΟ²―>βΣΕ7―G{κπώ}ί~ω™ξαϊί~=r`Ώlžu«~Ω½}KyΆXlΪZEϋ―C₯R)]HΉΤ†ώ‘‰™6εωO?ZpδΰΌΌό“GπΝWΟ?1ώβωݘςχ̊�\mΕVϊ§’JΩ”’Ψ>δןΨ³s[™kV;χB†Υ?P―‘$+«YΚΦ°wηvWW!ΔΎέ;‹=uιβωΫΚυ™ΎBͺiΪ*q~Ί’…”KmθŸΚΘ̘φΚ ρο/X΄fγΦOΎψοο[w}Έδ‹μœœηΞJIIVΊΐκBTT)ϋŸRΫ‡Lηύρ?UζZE‡ΥΞ½aυΤ+„F(ΙΩΕ5.&FΑnވ »veμcOϊωά·G£ΡθžΚΜΚΜΛΝBdάOΟΟΛ+ΊVfVζ•K‘χΣΣKŸΌ ??2"μvbB±u8νΧΌ„ΈX!„³‹‹²e”“βύ#³~υͺΤ””Χ§ΟμΩ§―v‰±‰I‡N'>σ\JJς‘}{K’™q?';[χP£Ρ”lςၯ<;ι©q#Ÿy|μΥKK™V6Έζ…†›˜ψ4Sͺ€ QΌdvlέμιΥ¨wΏΕ–5fώ’%­Ϋ΅+ΉΚ3ϋrΙΗΊ‡ q±£‡άΏG·$ω/??ω‰qΣ_}iό°!¬_WMΕλƒώ©Rφ?Bˆμ¬¬%Ξ|Τ£οΎωϊ„αOν₯Ȉ0ρ }Θ3Y΄`Άβ“…σΖ=ϊPΡο³φνή9zhΠΕΠσE‡›aΩŸydpςέ;Ί΅vnέύ~ωύO_|ϋƒ³‹ΛΜ©―h³TrΪRΧΌCI'ΙIDAT{vuκΪέΤΜL‘­W”²ύ#“žž–v/Υ/ IɟfuιήΓΟ? ΣnΩΈΑΪΪϊ£₯_.[ρk‡N]Ύϋϊ?ηΞόYυV%ϊ§όJί!–}υω}{^™ϊΦͺυ›f̚›·xώ\!Ω5i όpA~ώΙγGtKοίγμβΪ¦]‡’ÊΝ0 hˆF£9~δ°nΐ‘Cϋœ[΅}ΐ·ΥΚ°ϊκB#”δμβΪ‘K·­ύU‘­Ÿ=}*%%ΉΠ!„‹«[Σζ-Ž9¨;EΠέΣ«aC[‘R5φσ·Άώϋ76Φ6 η/Z£WŸΆν;Nymš"2"’Ψ΄Y™YΏψ‘yΛΦΣί}? i`Σζ-ζ~τ‰¦°pΗΦM%§-}p »?}χΆ-C†¬ωMW޲ύ#“/„pqq«Ϊi›6kΎ`ρφ;ω4φ{λ½Ωj΅zγΊΥU» =Ρ?Rϊώ'>6fΟΞνC†xtδh'g—~ƒΖ>φd\lt|\μwMZ]Ίχh`m}τΠνΓ{©)!0xˆZύ―Χ›‘UΫvŽNGνΧ­άo`P±΅ͺ›ΑυΤ+„F(lβs/ξΪΊωFDxΝozοm–Mš%'ίMNΎΫ±S—¬Μ¬ǎ”²ŠŸΏΏ…₯…φoW7w!Δν„ψbc’nDήOOχ8ό—φŸπ«Wέέ=^ΉRrΒ n«~ψ@S0ζρ'k~Σ•¦`ΘΨΩΫ !ς ς«vڎ]Ίκ]Z[Ϋψϊ7‰‰ŽΪMθ‰ώ©χ?aa¦uλŽυ3nϋcnξ₯ΜibjΪ§ΐΏΞžΡώ²ρΨαCA=\z%j΅Ίίΐ ‹Ξ§¦€!Ž9¬Ρhϊ¬ο+¬ Cμ¨?ΈO#6bμ„eY:οιΏl¨ΡKω₯₯έ;uβx~^ή€ρϊb{ξύΙΦ²΅΅Χύ­V«Δ_#Ύ¨„ψx!Δm[φμά^tΉ»»gΙ +4ΈZ%έNόβ㏦L}«‘­] oZJυO),,-γβJ>UXXψγ²ΊΉ{ ^Ζα”άΌβΧ ±ΆiXτ‘··Ο©“Ηυ,΅ Ρ?Rζώ'11^ανγSΡ™²}σ§Oλ?pπαϋZΆiλζQφΞd@ΠCΦ>~δΠΠα#:ΰγλΧΨΟΏ’›Φ‡φΤ„F(ΜΘΘhΑ§Ÿyhΐέ;ϋRcΫ=ΈwO~^ή«Σήrσπ-\ΏfΥ_gLNΎkoοπΐ΅Κs1[{;!ΔK―M{x؈ͺ\­ΝyΟ¦aΓ—§ΝPΆŒŠRͺJηνέ8ψμ™ττ΄bg†όuvΓΪՏOš\ζ ρ±Ε3gB|lΡ‡aΧjoΥPKΠ?RζώGϋA|\œ.ΌedfΔ܊ςφn¬;Ωα[΄rχτ:vθ@ΫΆ.^™:σ½ςΤΣΨΟίΗΧοθ‘½ϊφ;|θρβ*Γ@ϋκNO…ςΊυκ;bά„i/NŽŽΊYcέ»k»‹«λΠα£:tκ¬ϋgΘ£Γ5Ναέ‘όΌ}«ΥκΣEŽεζδL}ωω_~ό^ΟΑΥgύκ•ΏϊΛΌ?³°΄¬ΙνV Eϊ§tO<ύlfVζΟί}Sτ& Ή99Ώ­όΩΨΔ€ίΐA%W±mh›””¨{xιBH±‘ηΟλf»—šΥΌe«j¨½2蟊*sγ „ΈrN·ΚͺΏŸ6εωό‚βwa)iΠΰ!gOŸΪ½c«©™Y―>ύΛY€ ‡.„ό΅cΫ!D Ÿ›jΠύυ‘΅Β’.χτn4qδ#iχRk`s7"#"Βυ8ΈΨ‘Γ.έz˜™›οΫ³KϋΠΞΞ>/7χδ±£Eo’V&{{‡a£Ζœ;szονχΣ%WixιbθΎ];’’nό±~ݎ7^Ώώε’Ε7"#nDF|ϊα#cγ1ž¨‰Sϊ§’Κ³ρσθΪ£ηžνێ:˜t;qΛΖ ;·mιή«·φΨu黦ώACςςς~[΅’gŸ~²Γ’%gθ;`Paaαš?ΆλΠΡΑΡ©Š_³\訍¨F*•ͺPΙίYXZ._³ώή½ΤIc†§$ί­ξΒφμΪ.„θτP±ε杻u ΏuσΊ’[―ήnσgΝ6eςΔ)“'^»rωΓO?/ύš(5ƒώ©„rξfΚۺ}ϋζΎ7qμˆoΎXΪCΗ©3ώ>Χ΄τ]“‹«k«6msssƒ† •ΥPr'g—ΦmΫεζζy€ζN•―L–γW€*¦*y ͺlΫwΜΣΗΗΩέ½œγ―]Ή4iτ0#cγ_6l=χwNΊhοΰhddTΡSR’oDFš››5 lQrυbΣ–>Έš¬_½rΖ«/=όΘ—?¬4·(ν‡R‘φO^nnLτ­””/oo'g—2Η'&$¨ΥͺRF&ΔΗedάχiμWcMR ϊ§€έK‹‰qvu΅wp,φT₯wM²Ύ\ϊρΙ£G~έ°ΩΘΈ&wPΉώΉzαΌ½΅UΫ•ΉΩ) ¨FOœ5·nΨΘΟ―ό«άIΊ=yό¨π«WfΞY0ρΉkδ³K=”t;qќχ~υ—W§ΏύφΌΚsƒ@Τ ϊ§ξΙΜΚ|rτπα£ΗNzφ…κή–>ύrκT“ΖžM{W_y€’ŒζΝ›§t ¨³Rο₯ίK»οδVk€Ί'σώύ¨ΘΘτ{χš4πm€t9B#j\aaα­Ψ„KΧk ]==]]MMM•.  Όϋι鉱±woίΆ³΅iΨΔΆ!η€@­@h„2ςςσΓ"£nFΗηεη7°ΆΆ²±±°°026Q©” Pƒ4M~n^ffFZjjNvΆ΅΅USίF^ξ.JΧψ‘J*(Π$ήΉ{;)9%-=#3;??Ÿ†€zΕH­661Άi`eoΫΠΝΕΑ‘Š#4€Έ@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4€)B#@ŠΠ"4ώ―ύ:δo=ΑeΡ’F–4°€€%,i`I#KXΐ’F–4°€€%,i`I#KXΐ’F–4°*ΓΦΈ9\§IENDB`‚stravalib-2.2/docs/index.md000066400000000000000000000056371475174155400157550ustar00rootroot00000000000000# Welcome to the Stravalib Documentation! ::::{grid} 2 :reverse: :::{grid-item} :columns: 12 :class: sd-fs-3 Stravalib is an open source Python package that makes it easier for you to access the Strava v3 REST API using the Python programming language. ```{only} html ![GitHub release (latest by date)](https://img.shields.io/github/v/release/stravalib/stravalib?color=purple&display_name=tag&style=plastic) [![](https://img.shields.io/github/stars/stravalib/stravalib?style=social)](https://github.com/pyopensci/contributing-guide) [![DOI](https://zenodo.org/badge/8828908.svg)](https://zenodo.org/badge/latestdoi/8828908) ``` ::: :::: ::::{grid} 1 1 1 2 :class-container: text-center :gutter: 3 :::{grid-item-card} :link: get-started/index :link-type: doc ✨ **Get Started Using Stravalib** ✨ ^^^ New to Stravalib? This section is for you! ::: :::{grid-item-card} :link: contributing/how-to-contribute :link-type: doc ✨ **Want to contribute?** ✨ ^^^ We welcome contributions of all kinds to stravalib. Learn more about the many ways that you can contribute. ::: :::{grid-item-card} :link: reference :link-type: doc ✨ **Package Code (API) Documentation** ✨ ^^^ Documentation for every method and class available to you in the stravalib package. ::: :::: ## About the stravalib Python package **stravalib** is a Python library for interacting with [version 3](https://developers.strava.com/docs/reference/) of the [Strava](https://www.strava.com) API. Our goal is to expose the entire user-facing Strava V3 API. The **stravalib** Python package provides easy-to-use tools for accessing and downloading Strava data from the Strava V3 web service. Stravalib provides a `Client` class that supports: * Authenticating with stravalib * Accessing and downloading Strava activity, club and profile data * Making changes to account activities It also provides support for working with date/time/temporal attributes and quantities through the [Python Pint library](https://pypi.org/project/Pint/). ## Why use stravalib? There are numerous reasons to use stravalib in your workflows: * Stravalib returns your data in structured Python dictionaries with associated data types that make it easier to work with the data in Python. * Relationships can be traversed on model objects to pull in related content "seamlessly". * dates, times and durations are imported as Python objects making it easier to convert and work with this data. * Stravalib provides built-in support for rate-limiting * and more intelligent error handling. :::{toctree} :hidden: :maxdepth: 2 🏠 Home ::: :::{toctree} :hidden: :caption: Get Started Get Started ::: :::{toctree} :hidden: :caption: API Documentation :maxdepth: 2 Code/API Reference ::: :::{toctree} :hidden: :caption: Contribute :maxdepth: 2 Contribute ::: :::{toctree} :hidden: :caption: What's New What's new ::: stravalib-2.2/docs/reference.rst000066400000000000000000000010131475174155400167740ustar00rootroot00000000000000.. _reference: API Reference ============= Welcome to the API documentation for the stravalib package. Below the main modules are listed. .. warning:: Only public facing methods are documented here. .. toctree:: :maxdepth: 2 Client Model Strava Model Unit helper Protocol Limiter (Util Submodule) Exceptions Module Details -------------- stravalib-2.2/docs/reference/000077500000000000000000000000001475174155400162475ustar00rootroot00000000000000stravalib-2.2/docs/reference/client.rst000066400000000000000000000050161475174155400202610ustar00rootroot00000000000000============ Client ============ .. currentmodule:: stravalib.client The ``Client`` object class for interacting with the Strava v3 API. While you can create this object without an access token, you will likely want to create an access token to authenticate and access most of the Strava data accessible via the API. Main Classes ------------- .. autosummary:: :toctree: api/ Client BatchedResultsIterator ActivityUploader General methods and attributes ------------------------------- .. autosummary:: :toctree: api/ Client.authorization_url Client.exchange_code_for_token Client.refresh_access_token Client.deauthorize Athlete methods ----------------- .. autosummary:: :toctree: api/ Client.get_activities Client.get_athlete Client.update_athlete Client.get_athlete_koms Client.get_athlete_stats Client.get_athlete_clubs Client.get_gear Club related methods -------------------- .. autosummary:: :toctree: api/ Client.join_club Client.leave_club Client.get_club Client.get_club_members Client.get_club_activities Activity related methods ------------------------- .. autosummary:: :toctree: api/ Client.get_activity Client.create_activity Client.update_activity Client.upload_activity Client.get_activity_zones Client.get_activity_comments Client.get_activity_kudos Client.get_activity_photos Client.get_activity_laps Client._validate_activity_type Segment related methods ------------------------- .. autosummary:: :toctree: api/ Client.get_segment_effort Client.get_segment Client.get_starred_segments Client.get_athlete_starred_segments Client.get_segment_efforts Client.explore_segments Stream related methods ------------------------- .. autosummary:: :toctree: api/ Client.get_activity_streams Client.get_effort_streams Client.get_segment_streams Route related methods ---------------------- .. autosummary:: :toctree: api/ Client.get_routes Client.get_route Client.get_route_streams Subscription related methods ----------------------------- .. autosummary:: :toctree: api/ Client.create_subscription Client.handle_subscription_callback Client.handle_subscription_update Client.list_subscriptions Client.delete_subscription Activity Uploader Constructor ----------------------------- .. autosummary:: :toctree: api/ ActivityUploader ActivityUploader methods --------------------------- .. autosummary:: :toctree: api/ ActivityUploader.update_from_response stravalib-2.2/docs/reference/exceptions.rst000066400000000000000000000001241475174155400211570ustar00rootroot00000000000000.. automodule:: stravalib.exc :members: :undoc-members: :show-inheritance: stravalib-2.2/docs/reference/model.rst000066400000000000000000000033521475174155400201040ustar00rootroot00000000000000============ Model ============ .. currentmodule:: stravalib.model Athletes ------------------------ .. autosummary:: :toctree: api/ :recursive: AthleteStats MetaAthlete SummaryAthlete DetailedAthlete AthletePrEffort AthleteSegmentStats Activities ------------------------- .. autosummary:: :toctree: api/ :recursive: MetaActivity SummaryActivity DetailedActivity ClubActivity ActivityTotals Lap Split RelaxedActivityType RelaxedSportType ActivityZone TimedZoneDistribution Activity Photos ------------------------- Note: the activity photo classes are defined differently in the Strava spec. This section will likely need to be updated. These endpoints are not well documented. .. autosummary:: :toctree: api/ ActivityPhotoPrimary ActivityPhoto Clubs ------------- .. autosummary:: :toctree: api/ MetaClub SummaryClub DetailedClub Routes and Segments ------------------------- .. autosummary:: :toctree: api/ LatLon SummarySegment Segment SegmentExplorerResult Map Route Stream Segment Efforts ----------------------------- .. autosummary:: :toctree: api/ Split BaseEffort BestEffort SegmentEffort SegmentEffortAchievement SummarySegmentEffort Webhook Subscriptions ------------------------- .. autosummary:: :toctree: api/ Subscription SubscriptionCallback SubscriptionUpdate .. _custom-types-anchor: Custom Types -------------------------- .. autosummary:: :toctree: api/ Distance Duration Timezone Velocity Stravalib helpers ----------------- .. autosummary:: :toctree: api/ BoundClientEntity naive_datetime lazy_property check_valid_location stravalib-2.2/docs/reference/protocol.rst000066400000000000000000000004751475174155400206500ustar00rootroot00000000000000================= Protocol Module ================= .. currentmodule:: stravalib.model This module contains only one class. This class handles interactions with the Strava V3 API including steps in the authentication process. .. automodule:: stravalib.protocol :members: :undoc-members: :show-inheritance: stravalib-2.2/docs/reference/strava_model.rst000066400000000000000000000005301475174155400214570ustar00rootroot00000000000000============================ Strava Model Module ============================ .. currentmodule:: stravalib.strava_model The `strava-model` module contains the officially documented Strava API entities. This module is generated by a bot and should not be edited manually. .. automodule:: stravalib.strava_model :members: :undoc-members: stravalib-2.2/docs/reference/unit_helper.rst000066400000000000000000000005711475174155400213220ustar00rootroot00000000000000============================ Unit Helper Module ============================ .. currentmodule:: stravalib.unit_helper The `unit_helper` module provides helper utility to convert various units. Summary Functions ------------------ .. autosummary:: :toctree: api/ c2f Summary Classes ---------------- .. autosummary:: :toctree: api/ _Quantity UnitConverter stravalib-2.2/docs/reference/utilities.rst000066400000000000000000000012221475174155400210110ustar00rootroot00000000000000====================================================== Limiter - in Utility Submodule: Functions and Classes ====================================================== .. currentmodule:: stravalib.util.limiter This module provides a mixture of helpers to support rate limiting and also functions for conversion? Summary Functions ------------------------------- .. autosummary:: :toctree: api/ get_rates_from_response_headers get_seconds_until_next_quarter get_seconds_until_next_day Summary Classes ------------------------------- .. autosummary:: :toctree: api/ SleepingRateLimitRule RateLimiter DefaultRateLimiter RequestRate stravalib-2.2/docs/whats-new/000077500000000000000000000000001475174155400162265ustar00rootroot00000000000000stravalib-2.2/docs/whats-new/changelog.md000066400000000000000000000000401475174155400204710ustar00rootroot00000000000000```{include} ../../changelog.md stravalib-2.2/docs/whats-new/stravalib-2.md000066400000000000000000000055521475174155400207050ustar00rootroot00000000000000# Stravalib V2 Migration Guide :::{toctree} :hidden: :caption: What's New What's New Changelog ::: Stravalib V2 includes several breaking changes. The sections below provide a guide to help you migrate your code to this latest release. ## Pydantic V2 Stravalib now uses Pydantic V2. If your code uses extensions of Stravalib model classes or uses Pydantic’s (de-) serialization mechanisms such as `parse_obj()`, `dict()`, or `json()`, please consider going through [Pydantic’s V2 migration guide](https://docs.pydantic.dev/latest/migration/) first. ## Legacy (de-)Serialization The already deprecated (de-)serialization methods `deserialize()`, `from_dict()`, and `to_dict()` are no longer supported. Instead, please use [Pydantic’s serialization mechanisms](https://docs.pydantic.dev/latest/concepts/serialization/). ## Strava Meta/Summary/Detailed Type Hierarchy Many model classes were changed to reflect the type hierarchy defined in the Strava API. This means that classes such as `Activity` are now split into `MetaActivity`, `SummaryActivity`, and `DetailedActivity`. As `MetaActivity` has fewer attributes than the former `Activity` class, this may lead to `AttributeError`s. However, these missing attributes would have a `None` value, as the Strava API never returned them. Code that uses checks on `resource_state` attributes or checks attributes on being `None` to determine the detail level of a response object should now rely on the type itself. Since many class names from Stravalib V1 have been replaced, any `isinstance()` checks on Stravalib model types may now (silently) fail. ## Unit Conversion The former `unithelper` module is renamed to `unit_helper` for naming consistency. The deprecated compatibility with the `units` package is no longer supported. The helper functions in `unit_helper` such as `feet()` or `miles()` now return a Pint `Quantity` object. ## Custom Types Stravalib V2 introduces new types for distances, velocities, durations, and time zones. When accessed from a model object. For example, `activity.distance` now returns a `Distance` type, which is an extension of `float`. The β€œplain” distance (in meters, the Strava default) can now be retrieved as a float by `activity.distance`. However, this distance can also be received as a `Quantity` with an explicit unit using `activity.distance.quantity`. This behavior differs from Stravalib V1, where `activity.distance` would immediately return a Quantity-like object. Detailed documentation about these new custom types can be [found here.](custom-types-anchor) ## Other The remaining unrelated [breaking](reference) changes are: - The `ActivityKudos` class has been removed. Stravalib now uses the new `SummaryAthlete` class in its place. - `Athlete.is_authenticated_athlete()` is removed. - The `Bike` and `Shoe` subtypes of `Gear` are replaced by `strava_model.SummaryGear`. stravalib-2.2/examples/000077500000000000000000000000001475174155400151775ustar00rootroot00000000000000stravalib-2.2/examples/strava-oauth/000077500000000000000000000000001475174155400176155ustar00rootroot00000000000000stravalib-2.2/examples/strava-oauth/README.md000066400000000000000000000014711475174155400210770ustar00rootroot00000000000000This is an example Flask application showing how you can use stravalib to help with getting access tokens. ## Create A Virtualenv with venv We'll assume you're using Python 3. ```bash $ cd /path/to/stravalib $ python3 -m venv env $ source env/bin/activate (env) $ pip install -e ".[tests, build]" (env) $ pip install -r examples/strava-oauth/requirements.txt ``` ## Create a Config File Create a file -- for example `settings.cfg` that contains your client id and secret in it: ```bash (env) $ cd examples/strava-oauth/ (env) $ vi settings.cfg ``` ```python STRAVA_CLIENT_ID = 123 STRAVA_CLIENT_SECRET = "deadbeefdeadbeefdeadbeef" ``` ## Run your flask server Run the Flask server, specifying the path to this file in your `APP_SETTINGS` environment var: ``` (env) $ APP_SETTINGS=settings.cfg python3 server.py ``` stravalib-2.2/examples/strava-oauth/requirements.txt000066400000000000000000000000061475174155400230750ustar00rootroot00000000000000flask stravalib-2.2/examples/strava-oauth/server.py000066400000000000000000000025531475174155400215020ustar00rootroot00000000000000#!flask/bin/python from flask import Flask, render_template, request, url_for from stravalib import Client app = Flask(__name__) app.config.from_envvar("APP_SETTINGS") @app.route("/") def login(): c = Client() url = c.authorization_url( client_id=app.config["STRAVA_CLIENT_ID"], redirect_uri=url_for(".logged_in", _external=True), approval_prompt="auto", ) return render_template("login.html", authorize_url=url) @app.route("/strava-oauth") def logged_in(): """ Method called by Strava (redirect) that includes parameters. - state - code - error """ error = request.args.get("error") state = request.args.get("state") if error: return render_template("login_error.html", error=error) else: code = request.args.get("code") client = Client() access_token = client.exchange_code_for_token( client_id=app.config["STRAVA_CLIENT_ID"], client_secret=app.config["STRAVA_CLIENT_SECRET"], code=code, ) # Probably here you'd want to store this somewhere -- e.g. in a database. strava_athlete = client.get_athlete() return render_template( "login_results.html", athlete=strava_athlete, access_token=access_token, ) if __name__ == "__main__": app.run(debug=True) stravalib-2.2/examples/strava-oauth/static/000077500000000000000000000000001475174155400211045ustar00rootroot00000000000000stravalib-2.2/examples/strava-oauth/static/ConnectWithStrava.png000066400000000000000000000052031475174155400252200ustar00rootroot00000000000000‰PNG  IHDR+šp&`tEXtSoftwareAdobe ImageReadyqΙe< %IDATxΪμ\MlTUΎoZMˆγ‚V+ˆ &ކΊ$Y [Šξ”€Xu§”­?Lΐ•Rde„΄;]ˆ%°2Jqa $š &JgΌί™{nΟ»οΎy3χ¦ηK†o^ί½οάsΎsΞχξ41ξ«°?†ŒB‘X/˜νΏTΏ $!œΆ―Νj'…bέaήΎ^c‚H)|£ΆQ(Φ=^9$–P!Μi₯ P(\ε0X³PRP(ΰ‚ †΅…B‘¨©  E%…B‘Δ P(V1lP++J ΑGΏ0Ɏ=ji…B‰‘ ½WK+J Ž!0A(ŠuN !hΥ P¬τW]-„DΡΈ>£VWtη[O=gΜΰIΆΠΧ§MγφοΖܹ՘CΜχι^δΈv ŒEΧΉ8ΉΆˆ!―uY4ονέέΑA0ι@JL₯­±tδ–kΰ€έΰΚsχ¨©½υqζIWrhΌωΖCύά Zo¬Mΐ/ Ϋ]{ύΖ峦>5Ώΰ΅Γγ¦aƒ½qωŒ©}x%υqύδλt\Ϊ¦6y5=σ'ˆτpŸ &»^―οΓ}΅χνΟRλ|$ΟΘυχφVœX < ±Ž[άϊ§οxV_“A;ςͺ'ΓϊΉ‰JΖθϋvΑΦ”ΦύpӍ·oωΐ1n֞HαΨΧ₯d%ƒ¨θžrχΊπΖΣΩ'rΦχpܟΟνZΙyγσΎS?iΐ† “eΐDιC‘ΠXΉΦR1ΕHYΪ:Xξηk…¬ƒvš5»o›c¦œ½¬dƒJ‘MRHeεŠZ™ΪθXΖο’ύGIΨΚαόDΆzbΫΘχL8O5ύUDτω2νκ/ί9Ž·EUU p κۜ“Pu€l`ΛY̍IΛ—jŠ%«„nZƒNΫΒLε‡l ­>刞Ž‘Zώ•ιγsϊύXα΅‚pόνιδGD!Ζ@P£­ +ΖڌΓγBC‹BΥBͺ _–ͺ‘TbˆU Έ)2‚0leZX[°pύƒƒ~qθoτeλ~ρ1χύcΝωcqΎ%,.;?ebw}d%8#;$ΞE†ΰ~›ϋK:=οΘ+ΝςΠ~Q)•ΥPεΐ±v6YΗzLšζιΘσΗ΅h{,yσ“EbδyΜΝA†YηΔη©{Δύ8ϋPΖς€ΰ€ύ]3x-ewΚ¨n\σΰnΌΕ³οC›u¬a7άύ#;Ϋωq‹(νkm=±I;ε‘Ÿ«χ›<ΏΥBX5$;€+V΄Υ‚\oρƒUE Xτ¬Xcϋ€ΰ3fψ²ϋ|ΩP†Y¬lΙ‚™ΖΟΜŽg°Ο^sαΘστ»ΰΞΑBG#’°Α=€?/ΘVv¬:lƒωqλ#‚™ Ώνœ"Σ“Κ ‚—΅gf^ψ,§Oράπ“‰χΑטυs„-R6sηΰΪ‰ ώΤΈ8ŸΗ½1“φi 7Ω¬¨ς€ Β Α˜ΈΎ'ζ[vaΞ-4‹°Z(ͺ2-ͺŸοϘΎ.Η―Ώ UCiC¬’ŒΗμ,VΫ=c'sΨ*Ζq*W8’, r"•Ÿ ˆΓ.‰dœqΐπ±ΉςΉX0vj‰ pηTDŽΈ¦ Bp8•Οώξ<ŸέΕ܈8“Cό²G$ΗΥ sΒ8"Σ?{;n ž δ αmΞ‹υοpj™Yœ‹K6σ¦±C›ΕZ— βΙσAdφvΧΐJ―ΟΛ>^N΅ΠRkοΝΖIνΕ|=a9΄†ˆ!Σ3έΉ•r"&‰¨³œB·œq\Φ‘€wd›'e>?h b½0]S:7 LμT^Άχ™Ω•έœ±iž6ΰ¨ G»α4ί21α‰>9―TOՎαhuΓ-Χb@LG³yͺ:C+ΧΫ s·₯³Η`ρ%§{ΫY\eΒVc§+υ"Ύ©Ι–Υ‚ jμ±ΘΥ4\Λڊ\’Υψjh%ZU ζρ'šΖόω;cώώΣί 18ΞΈχ—1ύ[’X΅­…2μ‚fΪ:P4P8¨’‘Ε ‘@μαnΎaγ,‡ce*W&‘>7֎H'ΛS'ŽϋεG{»,ݟ_Όo7¦φΘ‰—’ˆ€ZΌ‡i_R'7G۝hΉGx4/¬λ2‘½ͺ" Example Strava Login

Login using Strava

Authenticate using your Strava account.

stravalib-2.2/examples/strava-oauth/templates/login_error.html000066400000000000000000000007331475174155400250250ustar00rootroot00000000000000 Example Strava Login Error

Authorization Failed

There was an error authorizing the application to access your Strava account.

Error code: {{ error }}

stravalib-2.2/examples/strava-oauth/templates/login_results.html000066400000000000000000000007201475174155400253710ustar00rootroot00000000000000 Example Strava Login Results

Authorization Results

Hi, {{ athlete.firstname }}. Your access token:

{{ access_token|tojson(indent=2) }}

stravalib-2.2/noxfile.py000066400000000000000000000113301475174155400153750ustar00rootroot00000000000000import os import pathlib import shutil from glob import glob import nox nox.options.reuse_existing_virtualenvs = False # Sphinx output and source directories BUILD_DIR = "_build" OUTPUT_DIR = pathlib.Path(BUILD_DIR, "html") SOURCE_DIR = pathlib.Path("docs") # Sphinx build commands SPHINX_BUILD = "sphinx-build" SPHINX_AUTO_BUILD = "sphinx-autobuild" # Sphinx parameters used to build the guide BUILD_PARAMETERS = ["-b", "html"] # Sphinx parameters used to test the build of the guide TEST_PARAMETERS = ["-W", "--keep-going", "-E", "-a"] # Sphinx-autobuild ignore and include parameters AUTOBUILD_IGNORE = [ "_build", ".nox", "build_assets", "tmp", ] AUTOBUILD_INCLUDE = [pathlib.Path("_static", "pyos.css")] # Build docs build_command = ["-b", "html", "docs/", "docs/_build/html"] @nox.session(name="docs-test", python="3.11") def docs_test(session): """A session that builds the docs statically and returns any errors that it finds.""" session.install(".[docs]") session.run( SPHINX_BUILD, *BUILD_PARAMETERS, *TEST_PARAMETERS, SOURCE_DIR, OUTPUT_DIR, *session.posargs, ) @nox.session(name="docs", python="3.11") def docs(session): session.install(".[docs]") cmd = ["sphinx-build"] cmd.extend(build_command + session.posargs) session.run(*cmd) @nox.session(name="docs-live", python="3.11") def docs_live(session): session.install(".[docs]") AUTOBUILD_IGNORE = [ "_build", "build_assets", "tmp", ] cmd = ["sphinx-autobuild"] for folder in AUTOBUILD_IGNORE: cmd.extend(["--ignore", f"*/{folder}/*"]) cmd.extend(build_command + session.posargs) session.run(*cmd) ####### RUN TESTS ######## # Use this for venv envs nox -s test @nox.session(python=["3.10", "3.11", "3.12"]) def tests(session): """Install requirements in a venv and run tests.""" session.install(".[tests]") session.run( "pytest", "--cov=src/stravalib", "--cov-report=xml:coverage.xml", "--cov-report=term", "src/stravalib/tests/unit/", "src/stravalib/tests/integration/", ) # Use this for venv envs nox -s mypy @nox.session(python="3.11") def mypy(session): session.install(".[lint]") session.run( "mypy", ) @nox.session(name="docs-clean") def clean_docs(session): """ Clean out the docs directory used in the live build. """ dirs_to_clean = [ pathlib.Path("docs", "_build"), pathlib.Path("docs", "reference", "api"), ] for dir_path in dirs_to_clean: print(f"Cleaning directory: {dir_path}") dir_contents = dir_path.glob("*") for content in dir_contents: print(f"cleaning content from the {dir_path}") if content.is_dir(): shutil.rmtree(content) else: os.remove(content) # Clean the API doc stubs (autogenerated) api_path = pathlib.Path("docs", "reference", "api") api_contents = api_path.glob("*") for content in api_contents: print(f"cleaning out API stub docs {api_path}") os.remove(content) @nox.session() def build(session): """Build the package's SDist and wheel using PyPA build and setuptools / setuptools_scm""" session.install(".[build]") session.run("python", "-m", "build") @nox.session() def install_wheel(session): """If you have several wheels in your dist/ directory this will try to install each one. so be sure to clean things out before running.""" wheel_files = glob(os.path.join("dist", "*.whl")) print(wheel_files) if wheel_files: most_recent_wheel = max(wheel_files, key=os.path.getmtime) print("Installing:", most_recent_wheel) session.install(most_recent_wheel) else: print("No wheel files found matching the pattern: *.whl") @nox.session() def clean_build(session): """Clean out the dist/ directory and also clean out other remnant files such as .coverage, etc.""" dirs_remove = [ "__pycache__", ".mypy_cache", "build", "dist", ] files_remove = [ "*.pyc", "*.orig", ".coverage", "MANIFEST", "*.egg-info", ".cache", ".pytest_cache", "src/stravalib/_version_generated.py", ] for pattern in files_remove: matches = glob(pattern, recursive=True) print("searching for", matches) for match in matches: if os.path.isfile(match): os.remove(match) print(f"Removed file: {match}") for a_dir in dirs_remove: if os.path.isdir(a_dir): shutil.rmtree(a_dir) print(f"Removed directory: {a_dir}") stravalib-2.2/pyproject.toml000066400000000000000000000067111475174155400163020ustar00rootroot00000000000000[build-system] requires = ["setuptools>=45", "setuptools_scm[toml]>=6.2"] build-backend = "setuptools.build_meta" [tool.setuptools] package-dir = { "" = "src" } [tool.setuptools_scm] # Make sure setuptools uses version of last created tag - this allows us to specify bump version_scheme = "post-release" # Make sure scm doesn't use local scheme version for push to pypi # (so there isn't a + in the version) local_scheme = "no-local-version" write_to = "src/stravalib/_version_generated.py" write_to_template = '__version__ = "v{version}"' [tool.setuptools.package-data] "stravalib" = ["py.typed"] [project] name = "stravalib" description = "A Python package that makes it easy to access and download data from the Strava V3 REST API." keywords = ["strava", "running", "cycling", "athletes"] readme = "README.md" dynamic = ["version"] license = { text = "Apache 2.0 License" } authors = [{ name = "Hans Lellelid", email = "hans@xmpl.org" }] maintainers = [ { name = "Leah Wasser" }, { name = "Hans Lellelid" }, { name = "Jonatan Samoocha" }, { name = "Yihong" }, { name = "Γ‰mile Nadeau" }, ] requires-python = ">=3.10" classifiers = [ "Development Status :: 5 - Production/Stable", "Programming Language :: Python", "Intended Audience :: Science/Research", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Operating System :: OS Independent", "Topic :: Scientific/Engineering", "Topic :: Software Development :: Libraries", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", ] dependencies = ["pint", "pytz", "arrow", "requests", "pydantic>=2.0"] [project.urls] documentation = "https://stravalib.readthedocs.io" repository = "https://github.com/stravalib/stravalib" changelog = "https://github.com/stravalib/stravalib/blob/main/changelog.md" [project.optional-dependencies] build = ["build"] tests = [ "pytest", "pytest-cov", # Run tests in parallel "pytest-xdist", "responses", "pytest-mock" ] docs = [ "sphinx", "pydata-sphinx-theme", "autodoc_pydantic", "sphinx_remove_toctrees", "myst-nb", "sphinx-autobuild", "sphinx-inline-tabs", "sphinx_copybutton", "sphinx_design", "sphinxext-opengraph", # Needed for sphinx open graph "matplotlib", "sphinxcontrib-mermaid", ] lint = [ "pre-commit", "mypy", "types-requests", "types-pytz", "types-Flask", "ruff" ] [tool.black] line-length = 79 # When editing the config for black in this file, be sure to make # the same edits in the repo stravalib/strava_swagger2pydantic [tool.isort] profile = "black" [tool.pytest.ini_options] filterwarnings = [ "ignore::FutureWarning:stravalib.*", "ignore::DeprecationWarning:stravalib.*", ] [tool.mypy] python_version = "3.10" follow_imports = "silent" warn_redundant_casts = true warn_unused_ignores = true disallow_any_generics = true check_untyped_defs = true no_implicit_reexport = true warn_unreachable = true disallow_untyped_defs = true files = ["src/stravalib/"] plugins = ["pydantic.mypy"] exclude = ["src/stravalib/tests/"] [tool.pydantic-mypy] init_forbid_extra = true init_typed = true warn_required_dynamic_aliases = true warn_untyped_fields = true [tool.codespell] skip = './docs/_build, ./build, ./src/stravalib/tests/resources' [tool.ruff] lint.select = [ "F", # pyflakes "I", # isort ] lint.ignore = ["F841"] line-length = 79 stravalib-2.2/src/000077500000000000000000000000001475174155400141505ustar00rootroot00000000000000stravalib-2.2/src/stravalib/000077500000000000000000000000001475174155400161375ustar00rootroot00000000000000stravalib-2.2/src/stravalib/__init__.py000066400000000000000000000002431475174155400202470ustar00rootroot00000000000000from stravalib.client import Client __all__ = ["Client"] try: from ._version_generated import __version__ except ImportError: __version__ = "unreleased" stravalib-2.2/src/stravalib/client.py000066400000000000000000002411661475174155400200010ustar00rootroot00000000000000""" Client ============== Provides the main interface classes for the Strava version 3 REST API. """ from __future__ import annotations import calendar import collections import functools import logging import time from collections.abc import Iterable from datetime import datetime, timedelta from io import BytesIO from typing import ( TYPE_CHECKING, Any, Deque, Generic, Literal, NoReturn, Protocol, Tuple, TypeVar, cast, ) import arrow import pint import pytz from pydantic import BaseModel from requests import Session from stravalib import exc, model, strava_model, unit_helper from stravalib.exc import ( ActivityPhotoUploadNotSupported, warn_attribute_unofficial, warn_method_unofficial, warn_param_deprecation, warn_param_unofficial, warn_param_unsupported, ) from stravalib.model import SummaryAthlete from stravalib.protocol import AccessInfo, ApiV3, Scope from stravalib.util import limiter if TYPE_CHECKING: from _typeshed import SupportsRead ActivityType = str SportType = str StreamType = str PhotoMetadata = Any class Client: """Main client class for interacting with the exposed Strava v3 API methods. This class can be instantiated without an access_token when performing authentication; however, most methods will require a valid access token. """ def __init__( self, access_token: str | None = None, rate_limit_requests: bool = True, rate_limiter: limiter.RateLimiter | None = None, requests_session: Session | None = None, token_expires: int | None = None, refresh_token: str | None = None, ) -> None: """ Initialize a new client object. Parameters ---------- access_token : str The token that provides access to a specific Strava account. If empty, assume that this account is not yet authenticated. rate_limit_requests : bool Whether to apply a rate limiter to the requests. (default True) rate_limiter : callable A :class:`stravalib.util.limiter.RateLimiter` object to use. If not specified (and rate_limit_requests is True), then :class:`stravalib.util.limiter.DefaultRateLimiter` will be used. requests_session : requests.Session() object (Optional) pass request session object. token_expires : int epoch timestamp -- seconds since jan 1 1970 (Epoch timestamp) This represents the date and time that the token will expire. It is used to automatically check and refresh the token in the client method on all API requests. refresh_token : str """ self.log = logging.getLogger( "{0.__module__}.{0.__name__}".format(self.__class__) ) if rate_limit_requests: if not rate_limiter: rate_limiter = limiter.DefaultRateLimiter() elif rate_limiter: raise ValueError( "Cannot specify rate_limiter object when rate_limit_requests is" " False" ) self.protocol = ApiV3( access_token=access_token, refresh_token=refresh_token, token_expires=token_expires, requests_session=requests_session, rate_limiter=rate_limiter, ) @property def access_token(self) -> str | None: """The currently configured authorization token.""" return self.protocol.access_token @access_token.setter def access_token(self, token_value: str | None) -> None: """Set the currently configured authorization token. Parameters ---------- access_token : str User's access token Returns ------- """ self.protocol.access_token = token_value @property def token_expires(self) -> int | None: """The currently configured authorization token.""" return self.protocol.token_expires @token_expires.setter def token_expires(self, expires_value: int) -> None: """Used to set and update the refresh token. Parameters ---------- expires_value : int Current token expiration time (epoch seconds) Returns ------- """ self.protocol.token_expires = expires_value @property def refresh_token(self) -> str | None: """The currently configured authorization token.""" return self.protocol.refresh_token @refresh_token.setter def refresh_token(self, refresh_value: str) -> None: """Used to set and update the refresh token. Parameters ---------- refresh_value : str Current token refresh value. Returns ------- None Updates the `refresh_token` attribute in the Client class. """ self.protocol.refresh_token = refresh_value def authorization_url( self, client_id: int, redirect_uri: str, approval_prompt: Literal["auto", "force"] = "auto", scope: list[Scope] | Scope | None = None, state: str | None = None, ) -> str: """Get the URL needed to authorize your application to access a Strava user's information. See https://developers.strava.com/docs/authentication/ Parameters ---------- client_id : int The numeric developer client id. redirect_uri : str The URL that Strava will redirect to after successful (or failed) authorization. approval_prompt : str, default='auto' Whether to prompt for approval even if approval already granted to app. Choices are 'auto' or 'force'. scope : list[str], default = None The access scope required. Omit to imply "read" and "activity:read" Valid values are 'read', 'read_all', 'profile:read_all', 'profile:write', 'activity:read', 'activity:read_all', 'activity:write'. state : str, default=None An arbitrary variable that will be returned to your application in the redirect URI. Returns ------- str: A string containing the url required to authorize with the Strava API. """ return self.protocol.authorization_url( client_id=client_id, redirect_uri=redirect_uri, approval_prompt=approval_prompt, scope=scope, state=state, ) def exchange_code_for_token( self, client_id: int, client_secret: str, code: str, return_athlete: bool = False, ) -> AccessInfo | Tuple[AccessInfo, SummaryAthlete | None]: """Exchange the temporary authorization code (returned with redirect from Strava authorization URL) for a short-lived access token and a refresh token (used to obtain the next access token later on). Parameters ---------- client_id : int The numeric developer client id. client_secret : str The developer client secret code : str The temporary authorization code return_athlete : bool (default = False) Whether to return a SummaryAthlete object (or not) This parameter is currently undocumented and could change at any time. Returns ------- AccessInfo TypedDictionary containing the access_token, refresh_token and expires_at (number of seconds since Epoch when the provided access token will expire) tuple Contains: - the `AccessInfo` typed dict containing token values - a `SummaryAthlete` object representing the authenticated user. Notes ----- Strava by default returns `SummaryAthlete` information during this exchange. However this return is currently undocumented and could change at any time. """ access_info, athlete_data = self.protocol.exchange_code_for_token( client_id=client_id, client_secret=client_secret, code=code, return_athlete=return_athlete, ) # Return both access_info and athlete if requested. Athlete will be None if Strava # doesn't return it in their end point response. if return_athlete: if athlete_data: summary_athlete = SummaryAthlete.model_validate(athlete_data) else: summary_athlete = None return access_info, summary_athlete else: return access_info def refresh_access_token( self, client_id: int, client_secret: str, refresh_token: str ) -> AccessInfo: """Exchanges the previous refresh token for a short-lived access token and a new refresh token (used to obtain the next access token later on). Parameters ---------- client_id : int The numeric developer client id. client_secret : str The developer client secret refresh_token : str The refresh token obtained from a previous authorization request Returns ------- dict: Dictionary containing the access_token, refresh_token and expires_at (number of seconds since Epoch when the provided access token will expire) """ return self.protocol.refresh_access_token( client_id=client_id, client_secret=client_secret, refresh_token=refresh_token, ) def deauthorize(self) -> None: """Deauthorize the application. This causes the application to be removed from the athlete's "My Apps" settings page. https://developers.strava.com/docs/authentication/#deauthorization """ self.protocol.post("oauth/deauthorize") def _utc_datetime_to_epoch(self, activity_datetime: str | datetime) -> int: """Convert the specified datetime value to a unix epoch timestamp (seconds since epoch). Parameters ---------- activity_datetime : str A string which may contain tzinfo (offset) or a datetime object (naive datetime will be considered to be UTC). Returns ------- datetime value in univ epoch time stamp format (seconds since epoch) """ if isinstance(activity_datetime, str): activity_datetime = arrow.get(activity_datetime).datetime assert isinstance(activity_datetime, datetime) if activity_datetime.tzinfo: activity_datetime = activity_datetime.astimezone(pytz.utc) return calendar.timegm(activity_datetime.timetuple()) def get_activities( self, before: datetime | str | None = None, after: datetime | str | None = None, limit: int | None = None, ) -> BatchedResultsIterator[model.SummaryActivity]: """Get activities for authenticated user sorted by newest first. https://developers.strava.com/docs/reference/#api-Activities-getLoggedInAthleteActivities Parameters ---------- before : datetime.datetime or str or None, default=None Result will start with activities whose start date is before specified date. (UTC) after : datetime.datetime or str or None, default=None Result will start with activities whose start date is after specified value. (UTC) limit : int or None, default=None Maximum number of activities to return. Returns ------- :class:`stravalib.model.BatchedResultsIterator` An iterator of :class:`stravalib.model.SummaryActivity` objects. """ before_epoch = self._utc_datetime_to_epoch(before) if before else None after_epoch = self._utc_datetime_to_epoch(after) if after else None params = dict(before=before_epoch, after=after_epoch) result_fetcher = functools.partial( self.protocol.get, "/athlete/activities", check_for_errors=True, **params, ) return BatchedResultsIterator( entity=model.SummaryActivity, bind_client=self, result_fetcher=result_fetcher, limit=limit, ) def get_athlete(self) -> model.DetailedAthlete: """Gets the specified athlete; if athlete_id is None then retrieves a detail-level representation of currently authenticated athlete; otherwise summary-level representation returned of athlete. Parameters ---------- Returns ------- class:`stravalib.model.DetailedAthlete` The :class:`stravalib.model.DetailedAthlete` model object. Notes ----- See: https://developers.strava.com/docs/reference/#api-Athletes https://developers.strava.com/docs/reference/#api-Athletes-getLoggedInAthlete """ raw = self.protocol.get("/athlete") return model.DetailedAthlete.model_validate( {**raw, **{"bound_client": self}} ) def update_athlete( self, city: str | None = None, state: str | None = None, country: str | None = None, sex: str | None = None, weight: float | None = None, ) -> model.DetailedAthlete: """Updates the properties of the authorized athlete. https://developers.strava.com/docs/reference/#api-Athletes-updateLoggedInAthlete Parameters ---------- city : str, default=None City the athlete lives in .. deprecated:: 1.0 This param is not supported by the Strava API and may be removed in the future. state : str, default=None State the athlete lives in .. deprecated:: 1.0 This param is not supported by the Strava API and may be removed in the future. country : str, default=None Country the athlete lives in .. deprecated:: 1.0 This param is not supported by the Strava API and may be removed in the future. sex : str, default=None Sex of the athlete .. deprecated:: 1.0 This param is not supported by the Strava API and may be removed in the future. weight : float, default=None Weight of the athlete in kg (float) """ params: dict[str, Any] = { "city": city, "state": state, "country": country, "sex": sex, } params = {k: v for (k, v) in params.items() if v is not None} for p in params.keys(): if p != "weight": warn_param_unsupported(p) if weight is not None: params["weight"] = float(weight) raw_athlete = self.protocol.put("/athlete", **params) return model.DetailedAthlete.model_validate( {**raw_athlete, **{"bound_client": self}} ) def get_athlete_zones(self) -> strava_model.Zones: """ Get the authenticated athlete's heart rate and power zones Parameters ---------- Returns ------- class:`stravalib.model.Zones` The athlete zones model object. """ raw = self.protocol.get("/athlete/zones") return strava_model.Zones.model_validate(raw) def get_athlete_koms( self, athlete_id: int, limit: int | None = None ) -> BatchedResultsIterator[model.SegmentEffort]: """Gets Q/KOMs/CRs for specified athlete. KOMs are returned as `stravalib.model.SegmentEffort` objects. Parameters ---------- athlete_id : int The ID of the athlete. limit : int Maximum number of KOM segment efforts to return (default unlimited). Returns ------- class:`BatchedResultsIterator` An iterator of :class:`stravalib.model.SegmentEffort` objects. """ result_fetcher = functools.partial( self.protocol.get, "/athletes/{id}/koms", id=athlete_id ) return BatchedResultsIterator( entity=model.SegmentEffort, bind_client=self, result_fetcher=result_fetcher, limit=limit, ) def get_athlete_stats( self, athlete_id: int | None = None ) -> model.AthleteStats: """Returns Statistics for the athlete. athlete_id must be the id of the authenticated athlete or left blank. If it is left blank two requests will be made - first to get the authenticated athlete's id and second to get the Stats. https://developers.strava.com/docs/reference/#api-Athletes-getStats Parameters ---------- athlete_id : int, default=None Strava ID value for the athlete. Returns ------- :class:`stravalib.model.AthleteStats` A model containing the Stats Notes ----- Note that this will return the stats for public activities only, regardless of the scopes of the current access token. """ if athlete_id is None: athlete_id = self.get_athlete().id raw = self.protocol.get("/athletes/{id}/stats", id=athlete_id) # TODO: Better error handling - this will return a 401 if this athlete # is not the authenticated athlete. return model.AthleteStats.model_validate(raw) def get_athlete_clubs( self, limit: int | None = None ) -> BatchedResultsIterator[model.SummaryClub]: """List the clubs for the currently authenticated athlete. https://developers.strava.com/docs/reference/#api-Clubs-getLoggedInAthleteClubs limit : int or None, default=None Maximum number of club objects to return. Returns ------- :class:`stravalib.model.BatchedResultsIterator` An iterator of :class:`stravalib.model.SummaryClub` objects """ result_fetcher = functools.partial(self.protocol.get, "/athlete/clubs") return BatchedResultsIterator( entity=model.SummaryClub, bind_client=self, result_fetcher=result_fetcher, limit=limit, ) def join_club(self, club_id: int) -> None: """Joins the club on behalf of authenticated athlete. (Access token with write permissions required.) Parameters ---------- club_id : int The numeric ID of the club to join. Returns ------- No actual return. This implements a post action that allows the athlete to join a club via an API. """ self.protocol.post("clubs/{id}/join", id=club_id) def leave_club(self, club_id: int) -> None: """Leave club on behalf of authenticated user. (Access token with write permissions required.) Parameters ---------- club_id : int Returns ------- No actual return. This implements a post action that allows the athlete to leave a club via an API. """ self.protocol.post("clubs/{id}/leave", id=club_id) def get_club(self, club_id: int) -> model.DetailedClub: """Return a specific club object. https://developers.strava.com/docs/reference/#api-Clubs-getClubById Parameters ---------- club_id : int The ID of the club to fetch. Returns ------- class: `model.DetailedClub` object containing the club data. Notes ------ https://developers.strava.com/docs/reference/#api-Clubs-getClubById """ raw = self.protocol.get("/clubs/{id}", id=club_id) return model.DetailedClub.model_validate( {**raw, **{"bound_client": self}} ) def get_club_members( self, club_id: int, limit: int | None = None ) -> BatchedResultsIterator[strava_model.ClubAthlete]: """Gets the member objects for specified club ID. https://developers.strava.com/docs/reference/#api-Clubs-getClubMembersById Parameters ---------- club_id : int The numeric ID for the club. limit : int Maximum number of athletes to return. (default unlimited) Returns ------- class:`BatchedResultsIterator` An iterator of :class:`stravalib.model.Athlete` objects. """ result_fetcher = functools.partial( self.protocol.get, "/clubs/{id}/members", id=club_id ) return BatchedResultsIterator( entity=strava_model.ClubAthlete, bind_client=self, result_fetcher=result_fetcher, limit=limit, ) def get_club_activities( self, club_id: int, limit: int | None = None ) -> BatchedResultsIterator[model.ClubActivity]: """Gets the activities associated with specified club. https://developers.strava.com/docs/reference/#api-Clubs-getClubActivitiesById Parameters ---------- club_id : int The numeric ID for the club. limit : int Maximum number of activities to return. (default unlimited) Returns ------- class:`BatchedResultsIterator` An iterator of :class:`stravalib.model.ClubActivity` objects. """ result_fetcher = functools.partial( self.protocol.get, "/clubs/{id}/activities", id=club_id ) return BatchedResultsIterator( entity=model.ClubActivity, bind_client=self, result_fetcher=result_fetcher, limit=limit, ) def get_club_admins( self, club_id: int, limit: int | None = None ) -> BatchedResultsIterator[model.SummaryAthlete]: """Returns a list of the administrators of a given club. https://developers.strava.com/docs/reference/#api-Clubs-getClubAdminsById Parameters ---------- club_id : int The numeric ID for the club. limit : int Maximum number of admins to return. (default unlimited) Returns ------- class:`BatchedResultsIterator` An iterator of :class:`stravalib.model.SummaryAthlete` objects. """ result_fetcher = functools.partial( self.protocol.get, "/clubs/{id}/admins", id=club_id ) return BatchedResultsIterator( entity=model.SummaryAthlete, bind_client=self, result_fetcher=result_fetcher, limit=limit, ) def get_activity( self, activity_id: int, include_all_efforts: bool = False ) -> model.DetailedActivity: """Gets specified activity. Will be detail-level activity return if owned by authenticated user; otherwise it will be summary-level. Parameters ---------- activity_id : int The ID of activity to fetch. include_all_efforts : bool, default=False Whether to include segment efforts - only available to the owner of the activity. Returns ------- :class:`model.DetailedActivity` A `DetailedActivity` object containing the requested activity data. Notes ------ https://developers.strava.com/docs/reference/#api-Activities-getActivityById """ raw = self.protocol.get( "/activities/{id}", id=activity_id, include_all_efforts=include_all_efforts, ) return model.DetailedActivity.model_validate( {**raw, **{"bound_client": self}} ) def _validate_activity_type( self, params: dict[str, Any], activity_type: str | None, sport_type: str | None, ) -> dict[str, Any]: """Validate activity type and sport type values. Parameters ---------- params : dict A dictionary of activity values used to update or create an activity. activity_type : str, default=None The activity type (case-insensitive). Deprecated. Prefer to use sport_type. In a request where both type and sport_type are present, this field will be ignored. See https://developers.strava.com/docs/reference/#api-models-UpdatableActivity. For possible values see: :class:`stravalib.model.DetailedActivity.TYPES` sport_type : str, default=None For possible values (case-sensitive) see: :class:`stravalib.model.DetailedActivity.SPORT_TYPES` Returns ------- dict Dictionary containing overlapping parameter values between the two methods Raises ------ ValueError If the activity_type or sport_type is invalid. """ # Check for sport_type first. If provided, it is favored over # activity_type / type if sport_type is not None: if not sport_type in model.DetailedActivity.SPORT_TYPES: raise ValueError( f"Invalid activity type: {sport_type}. Possible values: {model.DetailedActivity.SPORT_TYPES!r}" ) params["sport_type"] = sport_type return params # Handle activity type throwing warning given sport type is preferred # This also ONLY adds activity type if the user provides it over # sport_type. elif activity_type is not None: if not activity_type.lower() in [ t.lower() for t in model.DetailedActivity.TYPES ]: raise ValueError( f"Invalid activity type: {activity_type}. Possible values: {model.DetailedActivity.TYPES!r}" ) params["type"] = activity_type.lower() warn_param_deprecation( "activity_type", "sport_type", "https://developers.strava.com/docs/reference/#api-models-UpdatableActivity", ) return params def create_activity( self, name: str, start_date_local: datetime | str, elapsed_time: int | timedelta, sport_type: SportType | None = None, activity_type: ActivityType | None = None, description: str | None = None, distance: pint.Quantity | float | None = None, trainer: bool | None = None, commute: bool | None = None, ) -> model.DetailedActivity: """Create a new manual activity. If you would like to create an activity from an uploaded GPS file, see the :meth:`stravalib.client.Client.upload_activity` method instead. Parameters ---------- name : str The name of the activity. activity_type : str, default=None The activity type (case-insensitive). Deprecated. Prefer to use sport_type. In a request where both type and sport_type are present, this field will be ignored. See https://developers.strava.com/docs/reference/#api-models-UpdatableActivity. For possible values see: :class:`stravalib.model.DetailedActivity.TYPES` sport_type : str, default=None For possible values (case-sensitive) see: For possible values see: :class:`stravalib.model.DetailedActivity.SPORT_TYPES` start_date_local : class:`datetime.datetime` or string in ISO8601 format Local date/time of activity start. (TZ info will be ignored) elapsed_time : class:`datetime.timedelta` or int (seconds) The time in seconds or a :class:`datetime.timedelta` object. description : str, default=None The description for the activity. distance : :class:`pint.Quantity` or float (meters), default=None The distance in meters (float) or a :class:`pint.Quantity` instance. trainer : bool Whether this activity was completed using a trainer (or not) commute : bool Whether the activity is a commute or not. Notes ----- See: https://developers.strava.com/docs/reference/#api-Uploads-createUpload See: https://developers.strava.com/docs/reference/#api-Activities-createActivity """ # Strava API requires either sport_type or activity_type to be defined # to create an activity. Check for that here. if sport_type is None and activity_type is None: raise ValueError( "Either 'sport_type' or 'activity_type' must be provided to create an activity." ) if isinstance(elapsed_time, timedelta): elapsed_time = elapsed_time.seconds if isinstance(distance, pint.Quantity): distance = unit_helper.meters(distance).magnitude if isinstance(start_date_local, datetime): start_date_local = start_date_local.strftime("%Y-%m-%dT%H:%M:%SZ") params: dict[str, Any] = dict( name=name, start_date_local=start_date_local, elapsed_time=elapsed_time, ) update_params = { "description": description, "distance": distance, "commute": commute, "trainer": trainer, } for key, value in update_params.items(): if value is not None: params[key] = value params = self._validate_activity_type( params, activity_type, sport_type ) raw_activity = self.protocol.post("/activities", **params) return model.DetailedActivity.model_validate( {**raw_activity, **{"bound_client": self}} ) def update_activity( self, activity_id: int, name: str | None = None, activity_type: ActivityType | None = None, sport_type: SportType | None = None, description: str | None = None, private: bool | None = None, commute: bool | None = None, trainer: bool | None = None, gear_id: int | None = None, device_name: str | None = None, hide_from_home: bool | None = None, ) -> model.DetailedActivity: """Updates the properties of a specific activity. Parameters ---------- activity_id : int The ID of the activity to update. name : str, default=None The name of the activity. activity_type : str, default=None The activity type (case-insensitive). Deprecated. Prefer to use sport_type. In a request where both type and sport_type are present, this field will be ignored. See https://developers.strava.com/docs/reference/#api-models-UpdatableActivity. For possible values see: :class:`stravalib.model.DetailedActivity.TYPES` sport_type : str, default=None For possible values see: :class:`stravalib.model.DetailedActivity.SPORT_TYPES` private : bool, default=None Whether the activity is private. .. deprecated:: 1.0 This param is not supported by the Strava API and may be removed in the future. commute : bool, default=None Whether the activity is a commute. trainer : bool, default=None Whether this is a trainer activity. gear_id : int, default=None Alphanumeric ID of gear (bike, shoes) used on this activity. description : str, default=None Description for the activity. device_name : str, default=None Device name for the activity .. deprecated:: 1.0 This param is not supported by the Strava API and may be removed in the future. hide_from_home : bool, default=None Whether the activity is muted (hidden from Home and Club feeds). Returns ------- Updates the activity in the selected Strava account Notes ------ See: https://developers.strava.com/docs/reference/#api-Activities-updateActivityById """ # Convert the kwargs into a params dict params: dict[str, Any] = {} if name is not None: params["name"] = name if private is not None: warn_param_unsupported("private") params["private"] = int(private) if commute is not None: params["commute"] = int(commute) if trainer is not None: params["trainer"] = int(trainer) if gear_id is not None: params["gear_id"] = gear_id if description is not None: params["description"] = description if device_name is not None: warn_param_unsupported("device_name") params["device_name"] = device_name if hide_from_home is not None: params["hide_from_home"] = int(hide_from_home) # Validate sport and activity types params = self._validate_activity_type( params, activity_type, sport_type ) raw_activity = self.protocol.put( "/activities/{activity_id}", activity_id=activity_id, **params ) return model.DetailedActivity.model_validate( {**raw_activity, **{"bound_client": self}} ) def upload_activity( self, activity_file: SupportsRead[str | bytes], data_type: Literal["fit", "fit.gz", "tcx", "tcx.gz", "gpx", "gpx.gz"], name: str | None = None, description: str | None = None, activity_type: ActivityType | None = None, private: bool | None = None, external_id: str | None = None, trainer: bool | None = None, commute: bool | None = None, ) -> ActivityUploader: """Uploads a GPS file (tcx, gpx) to create a new activity for current athlete. https://developers.strava.com/docs/reference/#api-Uploads-createUpload Parameters ---------- activity_file : TextIOWrapper, str or bytes The file object to upload or file contents. data_type : str File format for upload. Possible values: fit, fit.gz, tcx, tcx.gz, gpx, gpx.gz name : str, optional, default=None If not provided, will be populated using start date and location, if available description : str, optional, default=None The description for the activity activity_type : str, optional Case-insensitive type of activity. possible values: ride, run, swim, workout, hike, walk, nordicski, alpineski, backcountryski, iceskate, inlineskate, kitesurf, rollerski, windsurf, workout, snowboard, snowshoe Type detected from file overrides, uses athlete's default type if not specified WARNING - This param is supported (as of 2022-11-15), but not documented and may be removed in the future. private : bool, optional, default=None Set to True to mark the resulting activity as private, 'view_private' permissions will be necessary to view the activity. .. deprecated:: 1.0 This param is not supported by the Strava API and may be removed in the future. external_id : str, optional, default=None An arbitrary unique identifier may be specified which will be included in status responses. trainer : bool, optional, default=None Whether the resulting activity should be marked as having been performed on a trainer. commute : bool, optional, default=None Whether the resulting activity should be tagged as a commute. """ if not hasattr(activity_file, "read"): if isinstance(activity_file, str): activity_file = BytesIO(activity_file.encode("utf-8")) elif isinstance(activity_file, bytes): activity_file = BytesIO(activity_file) else: raise TypeError( "Invalid type specified for activity_file: {}".format( type(activity_file) ) ) valid_data_types = ("fit", "fit.gz", "tcx", "tcx.gz", "gpx", "gpx.gz") if data_type not in valid_data_types: raise ValueError( f"Invalid data type {data_type}. Possible values {valid_data_types!r}" ) params: dict[str, Any] = {"data_type": data_type} if name is not None: params["name"] = name if description is not None: params["description"] = description if activity_type is not None: if not activity_type.lower() in [ t.lower() for t in model.DetailedActivity.TYPES ]: raise ValueError( f"Invalid activity type: {activity_type}. Possible values: {model.DetailedActivity.TYPES!r}" ) warn_param_unofficial("activity_type") params["activity_type"] = activity_type.lower() if private is not None: warn_param_unsupported("private") params["private"] = int(private) if external_id is not None: params["external_id"] = external_id if trainer is not None: params["trainer"] = int(trainer) if commute is not None: params["commute"] = int(commute) initial_response = self.protocol.post( "/uploads", files={"file": activity_file}, check_for_errors=False, **params, ) return ActivityUploader(self, response=initial_response) def get_activity_zones(self, activity_id: int) -> list[model.ActivityZone]: """Gets activity zones for activity. Activity zones relate to a users effort (heartrate and power). https://developers.strava.com/docs/reference/#api-Activities-getZonesByActivityId Parameters ---------- activity_id : int The activity for which to get zones. Returns ------- :class:`list` A list of :class:`stravalib.model.ActivityZone` objects. Notes ----- Activity zones require a Strava premium account. The Strava API has the return value for activity zones incorrectly typed as `int` when it can return either `int` or `float`. """ zones = self.protocol.get("/activities/{id}/zones", id=activity_id) return [ model.ActivityZone.model_validate({**z, **{"bound_client": self}}) for z in zones ] def get_activity_comments( self, activity_id: int, markdown: bool = False, limit: int | None = None, ) -> BatchedResultsIterator[strava_model.Comment]: """Gets the comments for an activity. https://developers.strava.com/docs/reference/#api-Activities-getCommentsByActivityId Parameters ---------- activity_id : int The activity for which to fetch comments. markdown : bool Whether to include markdown in comments (default is false/filterout) limit : int Max rows to return (default unlimited). Returns ------- class:`BatchedResultsIterator` An iterator of :class:`stravalib.strava_model.Comment` objects. """ result_fetcher = functools.partial( self.protocol.get, "/activities/{id}/comments", id=activity_id, markdown=int(markdown), ) return BatchedResultsIterator( entity=strava_model.Comment, bind_client=self, result_fetcher=result_fetcher, limit=limit, ) def get_activity_kudos( self, activity_id: int, limit: int | None = None ) -> BatchedResultsIterator[model.SummaryAthlete]: """Gets the kudos for an activity. https://developers.strava.com/docs/reference/#api-Activities-getKudoersByActivityId Parameters ---------- activity_id : int The activity for which to fetch kudos. limit : int Max rows to return (default unlimited). Returns ------- class:`BatchedResultsIterator` An iterator of :class:`stravalib.model.ActivityKudos` objects. """ result_fetcher = functools.partial( self.protocol.get, "/activities/{id}/kudos", id=activity_id ) return BatchedResultsIterator( entity=model.SummaryAthlete, bind_client=self, result_fetcher=result_fetcher, limit=limit, ) def get_activity_photos( self, activity_id: int, size: int | None = None, only_instagram: bool = False, ) -> BatchedResultsIterator[model.ActivityPhoto]: """Gets the photos from an activity. https://developers.strava.com/docs/reference/#api-Activities Parameters ---------- activity_id : int The activity for which to fetch photos. size : int, default=None the requested size of the activity's photos. URLs for the photos will be returned that best match the requested size. If not included, the smallest size is returned only_instagram : bool, default=False Parameter to preserve legacy behavior of only returning Instagram photos. Returns ------- class:`BatchedResultsIterator` An iterator of :class:`stravalib.model.ActivityPhoto` objects. """ params: dict[str, Any] = {} if not only_instagram: params["photo_sources"] = "true" if size is not None: params["size"] = size result_fetcher = functools.partial( self.protocol.get, "/activities/{id}/photos", id=activity_id, **params, ) return BatchedResultsIterator( entity=model.ActivityPhoto, bind_client=self, result_fetcher=result_fetcher, ) def get_activity_laps( self, activity_id: int ) -> BatchedResultsIterator[model.Lap]: """Gets the laps from an activity. https://developers.strava.com/docs/reference/#api-Activities-getLapsByActivityId Parameters ---------- activity_id : int The activity for which to fetch laps. Returns ------- class:`BatchedResultsIterator` An iterator of :class:`stravalib.model.Lap` objects. """ result_fetcher = functools.partial( self.protocol.get, "/activities/{id}/laps", id=activity_id ) return BatchedResultsIterator( entity=model.Lap, bind_client=self, result_fetcher=result_fetcher, ) def get_gear(self, gear_id: str) -> strava_model.DetailedGear: """Get details for an item of gear. https://developers.strava.com/docs/reference/#api-Gears Parameters ---------- gear_id : str The gear id. Returns ------- class:`stravalib.strava_model.DetailedGear` """ return strava_model.DetailedGear.model_validate( self.protocol.get("/gear/{id}", id=gear_id) ) def get_segment_effort(self, effort_id: int) -> model.SegmentEffort: """Return a specific segment effort by ID. Parameters ---------- effort_id : int The id of associated effort to fetch. Returns ------- class:`stravalib.model.SegmentEffort` The specified effort on a segment. Notes ------ https://developers.strava.com/docs/reference/#api-SegmentEfforts """ return model.SegmentEffort.model_validate( self.protocol.get("/segment_efforts/{id}", id=effort_id) ) def get_segment(self, segment_id: int) -> model.Segment: """Gets a specific segment by ID. https://developers.strava.com/docs/reference/#api-SegmentEfforts-getSegmentEffortById Parameters ---------- segment_id : int The segment to fetch. Returns ------- class:`stravalib.model.Segment` A segment object. """ return model.Segment.model_validate( { **self.protocol.get("/segments/{id}", id=segment_id), **{"bound_client": self}, } ) def get_starred_segments( self, limit: int | None = None ) -> BatchedResultsIterator[model.SummarySegment]: """Returns a summary representation of the segments starred by the authenticated user. Pagination is supported. https://developers.strava.com/docs/reference/#api-Segments-getLoggedInAthleteStarredSegments Parameters ---------- limit : int, optional, default=None Limit number of starred segments returned. Returns ------- class:`BatchedResultsIterator` An iterator of :class:`stravalib.model.SummarySegment` starred by authenticated user. """ params = {} if limit is not None: params["limit"] = limit result_fetcher = functools.partial( self.protocol.get, "/segments/starred" ) return BatchedResultsIterator( entity=model.SummarySegment, bind_client=self, result_fetcher=result_fetcher, limit=limit, ) def get_athlete_starred_segments( self, athlete_id: int, limit: int | None = None ) -> BatchedResultsIterator[model.Segment]: """Returns a summary representation of the segments starred by the specified athlete. Pagination is supported. https://developers.strava.com/docs/reference/#api-Segments-getLoggedInAthleteStarredSegments Parameters ---------- athlete_id : int The ID of the athlete. limit : int, optional, default=None Limit number of starred segments returned. Returns ------- class:`BatchedResultsIterator` An iterator of :class:`stravalib.model.Segment` starred by authenticated user. """ result_fetcher = functools.partial( self.protocol.get, "/athletes/{id}/segments/starred", id=athlete_id ) return BatchedResultsIterator( entity=model.Segment, bind_client=self, result_fetcher=result_fetcher, limit=limit, ) # Next TODO: add tests to deprecated items def get_segment_efforts( self, segment_id: int, athlete_id: int | None = None, start_date_local: datetime | str | None = None, end_date_local: datetime | str | None = None, limit: int | None = None, ) -> BatchedResultsIterator[model.BaseEffort]: """Gets all efforts on a particular segment sorted by start_date_local Returns an array of segment effort summary representations sorted by `start_date_local` ascending or by elapsed_time if an athlete_id is provided. NOTES: * The `athlete_id` sorting only works if you do NOT provide start and end dates. * If no date filtering parameters are provided, all efforts for the segment will be returned. Date range filtering is accomplished using an inclusive start and end time. `start_date_local` and `end_date_local` must be sent together. For open ended ranges pick dates significantly in the past or future. The filtering is done over local time for the segment, so there is no need for timezone conversion. For example, all efforts on Jan. 1st, 2024 for a segment in San Francisco, CA can be fetched using `2024-01-01T00:00:00Z` and `2024-01-01T23:59:59Z`. This endpoint requires a Strava subscription. Parameters ---------- segment_id : int ID for the segment of interest start_date_local : `datetime.datetime` or `str`, optional, default=None Efforts before this date will be excluded. Either as ISO8601 or datetime object end_date_local : `datetime.datetime` or str, optional, default=None Efforts after this date will be excluded. Either as ISO8601 or datetime object limit : int, default=None, optional limit number of efforts. This parameter has been removed by Strava and will be removed in stravalib in the near future. .. deprecated:: This param is not supported by the Strava API and may be removed in the future. athlete_id : int, default=None Strava ID for the athlete. Used to sort efforts by duration. Can only be used if start and end date are not provided. .. deprecated:: This param is not supported by the Strava API and may be removed in the future. Returns ------- class:`BatchedResultsIterator` An iterator of :class:`stravalib.model.SegmentEffort` efforts on a segment. Notes ------ https://developers.strava.com/docs/reference/#api-SegmentEfforts-getEffortsBySegmentId """ params: dict[str, Any] = {"segment_id": segment_id} if athlete_id is not None: warn_param_unsupported("athlete_id") params["athlete_id"] = athlete_id if start_date_local: if isinstance(start_date_local, str): start_date_local = arrow.get(start_date_local).naive params["start_date_local"] = start_date_local.strftime( "%Y-%m-%dT%H:%M:%SZ" ) if end_date_local: if isinstance(end_date_local, str): end_date_local = arrow.get(end_date_local).naive params["end_date_local"] = end_date_local.strftime( "%Y-%m-%dT%H:%M:%SZ" ) if limit is not None: warn_param_deprecation("limit", "date ranges") params["limit"] = limit result_fetcher = functools.partial( self.protocol.get, "/segment_efforts", **params, ) return BatchedResultsIterator( entity=model.BaseEffort, bind_client=self, result_fetcher=result_fetcher, limit=limit, ) def explore_segments( self, bounds: ( tuple[float, float, float, float] | tuple[tuple[float, float], tuple[float, float]] ), activity_type: ActivityType | None = None, min_cat: int | None = None, max_cat: int | None = None, ) -> list[model.SegmentExplorerResult]: """Returns an array of up to 10 segments. https://developers.strava.com/docs/reference/#api-Segments-exploreSegments Parameters ---------- bounds : tuple of 4 floats or tuple of 2 (lat,lon) tuples list of bounding box corners lat/lon [sw.lat, sw.lng, ne.lat, ne.lng] (south,west,north,east) activity_type : str optional, default is riding) 'running' or 'riding' min_cat : int, optional, default=None Minimum climb category filter max_cat : int, optional, default=None Maximum climb category filter Returns ------- :class:`list` An list of :class:`stravalib.model.Segment`. """ if len(bounds) == 2: bounds = ( bounds[0][0], bounds[0][1], bounds[1][0], bounds[1][1], ) elif len(bounds) != 4: raise ValueError( "Invalid bounds specified: {0!r}. Must be tuple of 4 float " "values or tuple of 2 (lat,lon) tuples." ) params: dict[str, Any] = {"bounds": ",".join(str(b) for b in bounds)} valid_activity_types = ("riding", "running") if activity_type is not None: if activity_type not in ("riding", "running"): raise ValueError( "Invalid activity type: {}. Possible values: {!r}".format( activity_type, valid_activity_types ) ) params["activity_type"] = activity_type if min_cat is not None: params["min_cat"] = min_cat if max_cat is not None: params["max_cat"] = max_cat raw = self.protocol.get("/segments/explore", **params) return [ model.SegmentExplorerResult.model_validate( {**v, **{"bound_client": self}} ) for v in raw["segments"] ] def _get_streams( self, stream_url: str, types: list[StreamType] | None = None, resolution: Literal["low", "medium", "high"] | None = None, series_type: Literal["time", "distance"] | None = None, ) -> dict[StreamType, model.Stream]: """ Generic method to retrieve stream data for activity, effort or segment. https://developers.strava.com/docs/reference/#api-Streams-getActivityStreams Streams represent the raw spatial data for the uploaded file. External applications may only access this information for activities owned by the authenticated athlete. Streams are available in 11 different types. If the stream is not available for a particular activity it will be left out of the request results. Streams types are: time, latlng, distance, altitude, velocity_smooth, heartrate, cadence, watts, temp, moving, grade_smooth Parameters ---------- stream_url : str Resource locator for the streams types : list[str], optional, default=None A list of the types of streams to fetch. resolution : str, optional, default=None Indicates desired number of data points. 'low' (100), 'medium' (1000) or 'high' (10000). The default of `None` will return full resolution data which in some cases (e.g. for long activities) will return more points / full data resolution. .. deprecated:: This param is not officially supported by the Strava API and may be removed in the future. series_type : str, optional Relevant only if using resolution either 'time' or 'distance'. Used to index the streams if the stream is being reduced. .. deprecated:: This param is not officially supported by the Strava API and may be removed in the future. Returns ------- py:class:`dict` An dictionary of :class:`stravalib.model.Stream` from the activity """ extra_params: dict[str, Any] = {} if resolution is not None: warn_param_unofficial("resolution") extra_params["resolution"] = resolution if series_type is not None: warn_param_unofficial("series_type") extra_params["series_type"] = series_type if not types: types = strava_model.StreamType.model_json_schema()["enum"] assert types is not None invalid_types = set(types).difference( strava_model.StreamType.model_json_schema()["enum"] ) if invalid_types: raise ValueError( f"Types {invalid_types} not supported by StravaApi" ) types_arg = ",".join(types) response = self.protocol.get( stream_url, keys=types_arg, key_by_type=True, **extra_params ) return { stream_type: model.Stream.model_validate(stream) for stream_type, stream in response.items() } def get_activity_streams( self, activity_id: int, types: list[StreamType] | None = None, resolution: Literal["low", "medium", "high"] | None = None, series_type: Literal["time", "distance"] | None = None, ) -> dict[StreamType, model.Stream]: """Returns a stream for an activity. https://developers.strava.com/docs/reference/#api-Streams-getActivityStreams Streams represent the raw spatial data for the uploaded file. External applications may only access this information for activities owned by the authenticated athlete. Streams are available in 11 different types. If the stream is not available for a particular activity it will be left out of the request results. Streams types are: time, latlng, distance, altitude, velocity_smooth, heartrate, cadence, watts, temp, moving, grade_smooth Parameters ---------- activity_id : int The ID of activity. types : list[str], optional, default=None A list of the types of streams to fetch. resolution : str, optional Indicates desired number of data points. 'low' (100), 'medium' (1000) or 'high' (10000). .. deprecated:: This param is not officially supported by the Strava API and may be removed in the future. series_type : str, optional Relevant only if using resolution either 'time' or 'distance'. Used to index the streams if the stream is being reduced. .. deprecated:: This param is not officially supported by the Strava API and may be removed in the future. Returns ------- :class:`dict` A dictionary of :class:`stravalib.model.Stream` from the activity """ return self._get_streams( f"/activities/{activity_id}/streams", types=types, resolution=resolution, series_type=series_type, ) def get_effort_streams( self, effort_id: int, types: list[StreamType] | None = None, resolution: Literal["low", "medium", "high"] | None = None, series_type: Literal["time", "distance"] | None = None, ) -> dict[StreamType, model.Stream]: """Returns an streams for an effort. https://developers.strava.com/docs/reference/#api-Streams-getSegmentEffortStreams Streams represent the raw data of the uploaded file. External applications may only access this information for activities owned by the authenticated athlete. Streams are available in 11 different types. If the stream is not available for a particular activity it will be left out of the request results. Streams types are: time, latlng, distance, altitude, velocity_smooth, heartrate, cadence, watts, temp, moving, grade_smooth Parameters ---------- effort_id : int The ID of effort. types : list, optional, default=None A list of the the types of streams to fetch. resolution : str, optional Indicates desired number of data points. 'low' (100), 'medium' (1000) or 'high' (10000). .. deprecated:: This param is not officially supported by the Strava API and may be removed in the future. series_type : str, optional Relevant only if using resolution either 'time' or 'distance'. Used to index the streams if the stream is being reduced. .. deprecated:: This param is not officially supported by the Strava API and may be removed in the future. Returns ------- :class:`dict` An dictionary of :class:`stravalib.model.Stream` from the effort. """ return self._get_streams( f"/segment_efforts/{effort_id}/streams", types=types, resolution=resolution, series_type=series_type, ) def get_segment_streams( self, segment_id: int, types: list[StreamType] | None = None, resolution: Literal["low", "medium", "high"] | None = None, series_type: Literal["time", "distance"] | None = None, ) -> dict[StreamType, model.Stream]: """Returns streams for a segment. https://developers.strava.com/docs/reference/#api-Streams-getSegmentStreams Streams represent the raw data of the uploaded file. External applications may only access this information for activities owned by the authenticated athlete. Streams are available in 11 different types. If the stream is not available for a particular activity it will be left out of the request results. Streams types are: time, latlng, distance, altitude, velocity_smooth, heartrate, cadence, watts, temp, moving, grade_smooth Parameters ---------- segment_id : int The ID of segment. types : list, optional, default=None A list of the the types of streams to fetch. resolution : str, optional Indicates desired number of data points. 'low' (100), 'medium' (1000) or 'high' (10000). .. deprecated:: This param is not officially supported by the Strava API and may be removed in the future. series_type : str, optional Relevant only if using resolution either 'time' or 'distance'. Used to index the streams if the stream is being reduced. .. deprecated:: This param is not officially supported by the Strava API and may be removed in the future. Returns ------- :class:`dict` An dictionary of :class:`stravalib.model.Stream` from the effort. """ return self._get_streams( f"/segments/{segment_id}/streams", types=types, resolution=resolution, series_type=series_type, ) def get_routes( self, athlete_id: int | None = None, limit: int | None = None ) -> BatchedResultsIterator[model.Route]: """Gets the routes list for an authenticated user. https://developers.strava.com/docs/reference/#api-Routes-getRoutesByAthleteId Parameters ---------- athlete_id : int, default=None Strava athlete ID limit : int, default=unlimited Max rows to return. Returns ------- class:`BatchedResultsIterator` An iterator of :class:`stravalib.model.Route` objects. """ if athlete_id is None: athlete_id = self.get_athlete().id result_fetcher = functools.partial( self.protocol.get, f"/athletes/{athlete_id}/routes" ) return BatchedResultsIterator( entity=model.Route, bind_client=self, result_fetcher=result_fetcher, limit=limit, ) def get_route(self, route_id: int) -> model.Route: """Gets specified route. Will be detail-level if owned by authenticated user; otherwise summary-level. https://developers.strava.com/docs/reference/#api-Routes-getRouteById Parameters ---------- route_id : int The ID of route to fetch. Returns ------- class: `model.Route` A model.Route object containing the route data. """ raw = self.protocol.get("/routes/{id}", id=route_id) return model.Route.model_validate({**raw, **{"bound_client": self}}) def get_route_streams( self, route_id: int ) -> dict[StreamType, model.Stream]: """Returns streams for a route. Streams represent the raw data of the saved route. External applications may access this information for all public routes and for the private routes of the authenticated athlete. The 3 available route stream types `distance`, `altitude` and `latlng` are always returned. See: https://developers.strava.com/docs/reference/#api-Streams-getRouteStreams Parameters ---------- route_id : int The ID of activity. Returns ------- :class:`dict` A dictionary of :class:`stravalib.model.Stream` from the route. """ result_fetcher = functools.partial( self.protocol.get, f"/routes/{route_id}/streams/" ) streams = BatchedResultsIterator( entity=model.Stream, bind_client=self, result_fetcher=result_fetcher, ) # Pack streams into dictionary return {cast(StreamType, i.type): i for i in streams} # TODO: removed old link to create a subscription but can't find new equiv # in current strava docs def create_subscription( self, client_id: int, client_secret: str, callback_url: str, verify_token: str = model.Subscription.VERIFY_TOKEN_DEFAULT, ) -> model.Subscription: """Creates a webhook event subscription. Parameters ---------- client_id : int application's ID, obtained during registration client_secret : str application's secret, obtained during registration callback_url : str callback URL where Strava will first send a GET request to validate, then subsequently send POST requests with updates verify_token : str a token you can use to verify Strava's GET callback request (Default value = model.Subscription.VERIFY_TOKEN_DEFAULT) Returns ------- class:`stravalib.model.Subscription` Notes ----- `verify_token` is set to a default in the event that the author doesn't want to specify one. The application must have permission to make use of the webhook API. Access can be requested by contacting developers -at- strava.com. An instance of :class:`stravalib.model.Subscription`. """ params: dict[str, Any] = dict( client_id=client_id, client_secret=client_secret, callback_url=callback_url, verify_token=verify_token, ) raw = self.protocol.post("/push_subscriptions", **params) return model.Subscription.model_validate( {**raw, **{"bound_client": self}} ) def handle_subscription_callback( self, raw: dict[str, Any], verify_token: str = model.Subscription.VERIFY_TOKEN_DEFAULT, ) -> dict[str, str]: """Validate callback request and return valid response with challenge. Parameters ---------- raw : dict The raw JSON response which will be serialized to a Python dict. verify_token : default=model.Subscription.VERIFY_TOKEN_DEFAULT Returns ------- dict[str, str] The JSON response expected by Strava to the challenge request. """ callback = model.SubscriptionCallback.model_validate(raw) callback.validate_token(verify_token) assert callback.hub_challenge is not None response_raw = {"hub.challenge": callback.hub_challenge} return response_raw def handle_subscription_update( self, raw: dict[str, Any] ) -> model.SubscriptionUpdate: """Converts a raw subscription update into a model. Parameters ---------- raw : dict The raw json response deserialized into a dict. Returns ------- class:`stravalib.model.SubscriptionUpdate` The subscription update model object. """ return model.SubscriptionUpdate.model_validate( {**raw, **{"bound_client": self}} ) def list_subscriptions( self, client_id: int, client_secret: str ) -> BatchedResultsIterator[model.Subscription]: """List current webhook event subscriptions in place for the current application. Parameters ---------- client_id : int application's ID, obtained during registration client_secret : str application's secret, obtained during registration Returns ------- class:`BatchedResultsIterator` An iterator of :class:`stravalib.model.Subscription` objects. """ result_fetcher = functools.partial( self.protocol.get, "/push_subscriptions", client_id=client_id, client_secret=client_secret, ) return BatchedResultsIterator( entity=model.Subscription, bind_client=self, result_fetcher=result_fetcher, ) def delete_subscription( self, subscription_id: int, client_id: int, client_secret: str ) -> None: """Unsubscribe from webhook events for an existing subscription. Parameters ---------- subscription_id : int ID of subscription to remove. client_id : int application's ID, obtained during registration client_secret : str application's secret, obtained during registration Returns ------- Deletes the specific subscription using the subscription ID """ self.protocol.delete( "/push_subscriptions/{id}", id=subscription_id, client_id=client_id, client_secret=client_secret, ) # Expects a 204 response if all goes well. T = TypeVar("T", bound=BaseModel) class ResultFetcher(Protocol): def __call__(self, *, page: int, per_page: int) -> Iterable[Any]: ... class BatchedResultsIterator(Generic[T]): """An iterator that enables iterating over requests that return paged results.""" # Number of results returned in a batch. We maximize this to minimize # requests to server (rate limiting) default_per_page = 200 def __init__( self, entity: type[T], result_fetcher: ResultFetcher, bind_client: Client | None = None, limit: int | None = None, per_page: int | None = None, ): """ Parameters ---------- entity : type The class for the model entity. result_fetcher: callable The callable that will return another batch of results. bind_client: :class:`stravalib.client.Client` The client object to pass to the entities for supporting further fetching of objects. limit: int The maximum number of rides to return. per_page: int How many rows to fetch per page (default is 200). """ self.log = logging.getLogger( "{0.__module__}.{0.__name__}".format(self.__class__) ) self.entity = entity self.bind_client = bind_client self.result_fetcher = result_fetcher self.limit = limit if per_page is not None: self.per_page = per_page else: self.per_page = BatchedResultsIterator.default_per_page self._buffer: None | Deque[T] self.reset() def __repr__(self) -> str: return "<{} entity={}>".format( self.__class__.__name__, self.entity.__name__ ) def reset(self) -> None: self._counter = 0 self._buffer = None self._page = 1 self._all_results_fetched = False def _fill_buffer(self) -> None: """Fills the internal buffer from Strava API.""" # If we cannot fetch anymore from the server then we're done here. if self._all_results_fetched: self._eof() raw_results = self.result_fetcher( page=self._page, per_page=self.per_page ) entities = [] for raw in raw_results: new_entity = self.entity.model_validate( {**raw, **{"bound_client": self.bind_client}} ) entities.append(new_entity) self._buffer = collections.deque(entities) self.log.debug( "Requested page {} (got: {} items)".format( self._page, len(self._buffer) ) ) if len(self._buffer) < self.per_page: self._all_results_fetched = True self._page += 1 def __iter__(self) -> BatchedResultsIterator[T]: return self def _eof(self) -> NoReturn: """ """ self.reset() raise StopIteration def __next__(self) -> T: return self.next() def next(self) -> T: if self.limit and self._counter >= self.limit: self._eof() if not self._buffer: self._fill_buffer() assert self._buffer is not None try: result = self._buffer.popleft() except IndexError: self._eof() else: self._counter += 1 return result class ActivityUploader: """ The "future" object that holds information about an activity file upload and can wait for upload to finish, etc. """ def __init__( self, client: Client, response: dict[str, Any], raise_exc: bool = True ) -> None: """ Initializes the instance with the given client, response, and optional exception flag. Parameters ---------- client: `stravalib.client.Client` The :class:`stravalib.client.Client` object that is handling the upload. response: Dict[str,Any] The initial upload response. raise_exc: bool Whether to raise an exception if the response indicates an error state. (default True) """ self.client = client self.response = response self.update_from_response(response, raise_exc=raise_exc) self._photo_metadata @property def photo_metadata(self) -> PhotoMetadata: """Photo metadata for the activity upload response, if any. it contains a pre-signed uri for uploading the photo. Notes ----- This is only available after the upload has completed. This metadata is only available for partner apps. If you have a regular / non partner related Strava app / account it will not work. """ warn_attribute_unofficial("photo_metadata") return self._photo_metadata @photo_metadata.setter def photo_metadata(self, value: PhotoMetadata) -> PhotoMetadata: """ Parameters ---------- value : list of dictionaries or none Contains an optional list of dictionaries with photo metadata or a value of `None`. Returns ------- Updates the `_photo_metadata` value in the object """ self._photo_metadata = value def update_from_response( self, response: dict[str, Any], raise_exc: bool = True ) -> None: """Updates internal state of object. Parameters ---------- response : :class:`dict` The response object (dict). raise_exc : bool Raises ------ stravalib.exc.ActivityUploadFailed If the response indicates an error and raise_exc is True. Whether to raise an exception if the response indicates an error state. (default True) Returns ------- """ self.upload_id = response.get("id") self.external_id = response.get("external_id") self.activity_id = response.get("activity_id") self.status = response.get("status") or response.get("message") # Undocumented field, it contains pre-signed uri to upload photo to self._photo_metadata = response.get("photo_metadata") if response.get("error"): self.error = response.get("error") elif response.get("errors"): # This appears to be an undocumented API; this is a temp hack self.error = str(response.get("errors")) else: self.error = None if raise_exc: self.raise_for_error() @property def is_processing(self) -> bool: """ """ return self.activity_id is None and self.error is None @property def is_error(self) -> bool: """ """ return self.error is not None @property def is_complete(self) -> bool: """ """ return self.activity_id is not None def raise_for_error(self) -> None: """ """ # FIXME: We need better handling of the actual responses, once those are # more accurately documented. if self.error: raise exc.ActivityUploadFailed(self.error) elif self.status == "The created activity has been deleted.": raise exc.CreatedActivityDeleted(self.status) def poll(self) -> None: """Update internal state from polling strava.com. Raises ------- class: `stravalib.exc.ActivityUploadFailed` If poll returns an error. """ response = self.client.protocol.get( "/uploads/{upload_id}", upload_id=self.upload_id, check_for_errors=False, ) self.update_from_response(response) def wait( self, timeout: float | None = None, poll_interval: float = 1.0 ) -> model.DetailedActivity: """Wait for the upload to complete or to err out. Will return the resulting Activity or raise an exception if the upload fails. Parameters ---------- timeout : float, default=None The max seconds to wait. Will raise TimeoutExceeded exception if this time passes without success or error response. poll_interval : float, default=1.0 (seconds) How long to wait between upload checks. Strava recommends 1s minimum. Returns ------- class:`stravalib.model.DetailedActivity` Raises ------ stravalib.exc.TimeoutExceeded If a timeout was specified and activity is still processing after timeout has elapsed. stravalib.exc.ActivityUploadFailed If the poll returns an error. The uploaded Activity object (fetched from server) """ start = time.time() while self.activity_id is None: self.poll() time.sleep(poll_interval) if timeout and (time.time() - start) > timeout: raise exc.TimeoutExceeded() # If we got this far, we must have an activity! return self.client.get_activity(self.activity_id) def upload_photo( self, photo: SupportsRead[bytes], timeout: float | None = None ) -> None: """Uploads a photo to the activity. Parameters ---------- photo : bytes The file-like object to upload. timeout : float, default=None The max seconds to wait. Will raise TimeoutExceeded Notes ----- In order to upload a photo, the activity must be uploaded and processed. The ability to add photos to activity is currently limited to partner apps & devices such as Zwift, Peloton, Tempo Move, etc... Given that the ability isn't in the public API, neither are the docs """ warn_method_unofficial("upload_photo") try: if not isinstance(photo, bytes): raise TypeError("Photo must be bytes type") self.poll() if self.is_processing: raise ValueError("Activity upload not complete") if not self.photo_metadata: raise ActivityPhotoUploadNotSupported( "Photo upload not supported" ) photos_data: list[dict[Any, Any]] = [ photo_data for photo_data in self.photo_metadata if photo_data and photo_data.get("method") == "PUT" and photo_data.get("header", {}).get("Content-Type") == "image/jpeg" ] if not photos_data: raise ActivityPhotoUploadNotSupported( "Photo upload not supported" ) if photos_data: response = self.client.protocol.rsession.put( url=photos_data[0]["uri"], data=photo, headers=photos_data[0]["header"], timeout=timeout, ) response.raise_for_status() except Exception as error: raise exc.ActivityPhotoUploadFailed(error) stravalib-2.2/src/stravalib/exc.py000066400000000000000000000110241475174155400172660ustar00rootroot00000000000000""" Exceptions & Error Handling ============================ Exceptions and error handling for stravalib. These are classes designed to capture and handle various errors encountered when interacting with the Strava API. """ from __future__ import annotations import logging import warnings import requests.exceptions class AuthError(RuntimeError): pass class LoginFailed(AuthError): pass class LoginRequired(AuthError): """ Login is required to perform specified action. """ class UnboundEntity(RuntimeError): """ Exception used to indicate that a model Entity is not bound to client instances. """ class Fault(requests.exceptions.HTTPError): """ Container for exceptions raised by the remote server. """ class ObjectNotFound(Fault): """ When we get a 404 back from an API call. """ class AccessUnauthorized(Fault): """ When we get a 401 back from an API call. """ class RateLimitExceeded(RuntimeError): """ Exception raised when the client rate limit has been exceeded. https://developers.strava.com/docs/rate-limits/ """ def __init__( self, msg: str, timeout: float | None = None, limit: float | None = None, ) -> None: super().__init__() self.limit = limit self.timeout = timeout class RateLimitTimeout(RateLimitExceeded): """ Exception raised when the client rate limit has been exceeded and the time to clear the limit (timeout) has not yet been reached https://developers.strava.com/docs/rate-limits/ """ class ActivityUploadFailed(RuntimeError): pass class ErrorProcessingActivity(ActivityUploadFailed): pass class CreatedActivityDeleted(ActivityUploadFailed): pass class ActivityPhotoUploadFailed(RuntimeError): pass class ActivityPhotoUploadNotSupported(ActivityPhotoUploadFailed): pass class TimeoutExceeded(RuntimeError): pass class NotAuthenticatedAthlete(AuthError): """ Exception when trying to access data which requires an authenticated athlete """ pass # Warnings configuration and helper functions warnings.simplefilter("default") logging.captureWarnings(True) def warn_method_deprecation( klass: type, method_name: str, alternative: str, alt_url: str | None = None, ) -> None: alt_support_msg = ( f" See {alt_url} for more information." if alt_url else "" ) warnings.warn( f'The method "{method_name}" of class "{klass}" is deprecated and will be ' f'removed in the future. Instead, you can use "{alternative}".{alt_support_msg}', DeprecationWarning, stacklevel=3, ) def warn_param_unsupported(param_name: str) -> None: warnings.warn( f'The "{param_name}" parameter is unsupported by the Strava API. It has no ' "effect and may lead to errors in the future.", DeprecationWarning, stacklevel=3, ) def warn_param_deprecation( param_name: str, alternative: str, alt_url: str | None = None ) -> None: alt_support_msg = ( f" See {alt_url} for more information." if alt_url else "" ) warnings.warn( f'The "{param_name}" parameter is deprecated and will be removed' f'in the future. Instead, you can use "{alternative}".{alt_support_msg}', DeprecationWarning, stacklevel=3, ) def warn_param_unofficial(param_name: str) -> None: warnings.warn( f'The "{param_name}" parameter is undocumented in the Strava API. Its use ' "may lead to unexpected behavior or errors in the future.", FutureWarning, stacklevel=3, ) def warn_attribute_unofficial(attr_name: str) -> None: warnings.warn( f'The "{attr_name}" parameter is undocumented in the Strava API. Its use ' "may lead to unexpected behavior or errors in the future.", FutureWarning, stacklevel=3, ) def warn_method_unofficial(method_name: str) -> None: warnings.warn( f'The "{method_name}" method is undocumented in the Strava API. Its use ' "may lead to unexpected behavior or errors in the future.", FutureWarning, stacklevel=3, ) def warn_units_deprecated() -> None: warnings.warn( "You are using a Quantity object or attributes from the units library, which is " "deprecated. Support for these types will be removed in the future. Instead, " "please use Quantity objects from the Pint package (https://pint.readthedocs.io).", DeprecationWarning, stacklevel=3, ) stravalib-2.2/src/stravalib/model.py000066400000000000000000001266721475174155400176270ustar00rootroot00000000000000""" ============================== Model Functions and Classes ============================== This module contains entity classes that represent the various Strava datatypes, such as `Activity`, `Gear`, and more. These entities inherit fields from superclasses in `strava_model.py`, which is generated from the official Strava API specification. The classes in this module add behavior such as type enrichment, unit conversion, and lazy loading related entities from the API. """ from __future__ import annotations import logging from abc import ABC, abstractmethod from collections.abc import Callable, Mapping, Sequence from datetime import date, datetime, timedelta from functools import wraps from typing import ( TYPE_CHECKING, Annotated, Any, ClassVar, Generic, Literal, TypeVar, Union, get_args, ) import pytz from dateutil import parser from dateutil.parser import ParserError from pydantic import ( AliasChoices, BaseModel, Field, GetCoreSchemaHandler, GetJsonSchemaHandler, field_validator, model_validator, ) from pydantic.json_schema import JsonSchemaValue from pydantic_core import core_schema from pytz import UnknownTimeZoneError from stravalib import exc, strava_model from stravalib.strava_model import ( ActivityType, BaseStream, Comment, ExplorerSegment, LatLng, PolylineMap, SportType, SummaryGear, ) from stravalib.unit_helper import _Quantity if TYPE_CHECKING: from stravalib.client import BatchedResultsIterator LOGGER = logging.getLogger(__name__) T = TypeVar("T") U = TypeVar("U", bound="BoundClientEntity") # Create alias for this type so docs are more readable AllDateTypes = Union[ datetime, str, bytes, int, float, ] # Could naive_datetime also be run in a validator? def naive_datetime(value: AllDateTypes | None) -> datetime | None: """Utility helper that parses a datetime value provided in JSON, string, int or other formats and returns a datetime.datetime object. Parameters ---------- value : str, int A value representing a date/time that may be presented in string, int, deserialized or other format. Returns ------- datetime.datetime A datetime object representing the datetime input value. Notes ----- Valid str, following formats work (from pydantic docs): YYYY-MM-DD[T]HH:MM[:SS[.ffffff]][Z or [Β±]HH[:]MM] """ if value is None: return None if isinstance(value, (int, float)): # If epoch is given, we have to assume it's UTC. When using the # regular fromtimestamp(), epoch will be interpreted as in the # _local_ timezone. dt = datetime.utcfromtimestamp(value) return dt.replace(tzinfo=None) elif isinstance(value, str): try: dt = parser.parse(value) return dt.replace(tzinfo=None) except ParserError: # Maybe timestamp was passed as string, e.g '1714305600'? try: dt = datetime.utcfromtimestamp(float(value)) return dt.replace(tzinfo=None) except ValueError: LOGGER.error(f"Invalid datetime value: {value}") raise elif isinstance(value, datetime): return value.replace(tzinfo=None) else: raise ValueError(f"Unsupported value type: {type(value)}") def lazy_property(fn: Callable[[U], T]) -> T | None: """ Should be used to decorate the functions that return a lazily loaded entity (collection), e.g., the members of a club. Parameters ---------- fn : Callable The input function that returns the lazily loaded entity (collection). Returns ------- property A lazily loaded property. Raises ------ exc.UnboundEntity If the object is unbound (self.bound_client is None) or has a None ID (self.id is None). Notes ----- Assumes that fn (like a regular getter property) has as single argument a reference to self, and uses one of the (bound) client methods to retrieve an entity (collection) by self.id. """ @wraps(fn) def wrapper(obj: U) -> T | None: try: if obj.bound_client is None: raise exc.UnboundEntity( f"Unable to fetch objects for unbound {obj.__class__} entity." ) if obj.id is None: # type: ignore[attr-defined] LOGGER.warning( f"Cannot retrieve {obj.__class__}.{fn.__name__}, self.id is None" ) return None return fn(obj) except AttributeError as e: raise exc.UnboundEntity( f"Unable to fetch objects for unbound {obj.__class__} entity: {e}" ) return property(wrapper) # type: ignore[return-value] # Custom validators for some edge cases: # This method checks a list of floats - ie a stream not just a single lat/lon def check_valid_location( location: Sequence[float] | str | None, ) -> list[float] | None: """ Validate a list of location xy values. Converts a list of floating point values stored as strings to floats and returns either a list of floats or None if no location data is found. This function is used to validate LatLon object inputs Parameters ---------- location : List of floats Either a List of x,y floating point values or strings or None (The legacy serialized format is str) Returns -------- List or None Either returns a List of floating point values representing location x,y data or None if empty list is returned from the API. Raises ------ AttributeError If empty list is returned, raises AttributeError """ # Legacy serialized form is str, so in case of attempting to de-serialize # from local storage: if isinstance(location, str): try: return [float(l) for l in location.split(",")] except AttributeError: # Location for activities without GPS may be returned as empty list return None # Because this could be any Sequence type, explicitly return list elif location: return list(location) else: return None # Custom types: BaseType = TypeVar("BaseType") CustomType = TypeVar("CustomType") class _CustomTypeAnnotation(Generic[BaseType, CustomType], ABC): _type: type[CustomType] @classmethod @abstractmethod def validate(cls, value: BaseType) -> CustomType: ... @classmethod @abstractmethod def get_core_schema(cls) -> Mapping[str, Any]: ... @classmethod def __get_pydantic_core_schema__( cls, _source_type: BaseType, _handler: GetCoreSchemaHandler, ) -> core_schema.CoreSchema: from_schema = core_schema.chain_schema( [ cls.get_core_schema(), core_schema.no_info_plain_validator_function(cls.validate), ] ) return core_schema.json_or_python_schema( json_schema=from_schema, python_schema=core_schema.union_schema( [core_schema.is_instance_schema(cls._type), from_schema] ), serialization=core_schema.plain_serializer_function_ser_schema( lambda v: v ), ) @classmethod def __get_pydantic_json_schema__( cls, _core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler, ) -> JsonSchemaValue: return handler(cls.get_core_schema()) class _CustomIntAnnotation(_CustomTypeAnnotation[int, Any], ABC): @classmethod def get_core_schema(cls) -> Mapping[str, Any]: return core_schema.int_schema() class _CustomFloatAnnotation(_CustomTypeAnnotation[float, Any], ABC): @classmethod def get_core_schema(cls) -> Mapping[str, Any]: return core_schema.float_schema() class _CustomStrAnnotation(_CustomTypeAnnotation[str, Any], ABC): @classmethod def get_core_schema(cls) -> Mapping[str, Any]: return core_schema.str_schema() class Duration(int): """ A class that calculates the duration or time elapsed for an activity, activity segment, or activity lap. This class extends the built-in `int` class to represent durations in seconds and provides a method to convert this duration to a `datetime.timedelta` object. Methods ------- timedelta() -> timedelta Converts the duration to a `datetime.timedelta` object. Examples -------- >>> activity = client.get_activity(11416949675) >>> activity.elapsed_time 3214 >>> activity.elapsed_time.timedelta() datetime.timedelta(seconds=3214) """ def timedelta(self) -> timedelta: """ Converts the duration to a `datetime.timedelta` object. Returns ------- timedelta A `datetime.timedelta` object representing the duration in seconds. """ return timedelta(seconds=float(self)) class _DurationAnnotation(_CustomIntAnnotation): _type = Duration @classmethod def validate(cls, value: int) -> Duration: return Duration(value) class Timezone(str): """A class to represent and manipulate time zone information. This class extends the built-in `str` class to include a method for converting the time zone string to a pytz time zone object. Methods ------- timezone() -> pytz._UTCclass | pytz.tzinfo.StaticTzInfo | pytz.tzinfo.DstTzInfo | None Converts the time zone string to a pytz time zone object. Examples -------- >>> activity = client.get_activity(11416949675) >>> activity.timezone '(GMT+01:00) Europe/Amsterdam' >>> activity.timezone.timezone() """ def timezone( self, ) -> ( pytz._UTCclass | pytz.tzinfo.StaticTzInfo | pytz.tzinfo.DstTzInfo | None ): """ Converts the time zone string to a pytz time zone object. This method attempts to convert the time zone string, which can either be in the format "(GMT-08:00) America/Los_Angeles" or "America/Los_Angeles", to a corresponding pytz time zone object. Returns ------- pytz._UTCclass, pytz.tzinfo.StaticTzInfo, `pytz.tzinfo.DstTzInfo`, or `None`The corresponding `pytz` time zone object if the time zone string is recognized, otherwise `None`. Raises ------ `UnknownTimeZoneError` If the time zone string is not recognized. """ if " " in self: # (GMT-08:00) America/Los_Angeles tzname = self.split(" ", 1)[1] else: # America/Los_Angeles tzname = self try: return pytz.timezone(tzname) except UnknownTimeZoneError as e: LOGGER.warning( f"Encountered unknown time zone {tzname}, returning None" ) return None class _TimezoneAnnotation(_CustomStrAnnotation): _type = Timezone @classmethod def validate(cls, value: str) -> Timezone: return Timezone(value) class Distance(_Quantity): """A class for representing distances as quantities (using meter as unit, which is the implicit default of Strava). These quantities can then be converted to other units such as feet or meters using the `stravalib.unit_helper` module. Examples ---------- Once you have distance in meters, you can then use the unit_helper module to convert to other units such as feet >>> from stravalib import unit_helper >>> activity = client.get_activity(11416949675) >>> activity.distance 8055.9 >>> activity.distance.quantity() >>> unit_helper.feet(activity.distance) >>> unit_helper.miles(activity.distance) >>> unit_helper.miles(activity.distance).magnitude 5.00570419 """ unit = "meters" class _DistanceAnnotation(_CustomFloatAnnotation): _type = Distance @classmethod def validate(cls, value: float) -> Distance: return Distance(value) class Velocity(_Quantity): """ A class for representing velocities as quantities (using meters per second as unit, which is the implicit default of Strava). These quantities can then be converted to other units such as km/h or mph using the `stravalib.unit_helper` module. Examples -------- >>> from stravalib import unit_helper >>> activity = client.get_activity(11854593990) >>> activity.average_speed 3.53 >>> activity.average_speed.quantity() >>> unit_helper.miles_per_hour(activity.average_speed) >>> unit_helper.miles_per_hour(activity.average_speed).magnitude 7.896385110952039 """ unit = "meters/second" class _VelocityAnnotation(_CustomFloatAnnotation): _type = Velocity @classmethod def validate(cls, value: float) -> Velocity: return Velocity(value) DurationType = Annotated[Duration, _DurationAnnotation] DistanceType = Annotated[Distance, _DistanceAnnotation] VelocityType = Annotated[Velocity, _VelocityAnnotation] TimezoneType = Annotated[Timezone, _TimezoneAnnotation] class BoundClientEntity(BaseModel): """A class that bounds the Client object to the model.""" # Using Any as type here to prevent catch-22 between circular import and # Pydantic forward-referencing issues "resolved" by PEP-8 violations. # See e.g. https://github.com/pydantic/pydantic/issues/1873 bound_client: Any | None = Field(None, exclude=True) class RelaxedActivityType(ActivityType): """This object supports allowing an array of Literal values to be used for Activity Type. by default, the generated `strava_model` module only allows a Literal that includes types: `Ride` and `Run`. """ @model_validator(mode="before") def check_activity_type(cls, values: str) -> str: """Pydantic validator that checks whether an activity type value is valid prior to populating the model. If the available activity type is not valid, it assigns the value to be "Workout". Parameters ---------- values : str A dictionary containing an activity type key value pair. Returns ------- str A dictionary with a validated activity type value assigned. """ if values not in get_args( ActivityType.model_fields["root"].annotation ): LOGGER.warning( f'Unexpected activity type. Given={values}, replacing by "Workout"' ) values = "Workout" return values def __eq__(self, other: Any) -> bool: if isinstance(other, RelaxedActivityType): return other.root == self.root elif isinstance(other, str): return other == self.root return False class RelaxedSportType(SportType): """A class that extends the list of Literal values allowed for Sport Types that are defined in the generated `strava_model` module. """ @model_validator(mode="before") def check_sport_type(cls, values: str) -> str: """Pydantic validator that checks whether a sport type value is valid prior to populating the model. If the existing sport type is not valid, it assigns the value to be "Workout". Parameters ---------- values : str A dictionary containing an sport type key value pair. Returns ------- str A str containing the validated sport type. """ if values not in SportType.__annotations__["root"]: LOGGER.warning( f'Unexpected sport type. Given={values}, replacing by "Workout"' ) values = "Workout" return values def __eq__(self, other: Any) -> bool: if isinstance(other, RelaxedSportType): return other.root == self.root elif isinstance(other, str): return other == self.root return False class LatLon(LatLng): """Stores lat / lon values or None.""" @model_validator(mode="before") def check_valid_latlng(cls, values: Sequence[float]) -> list[float] | None: """Validate that Strava returned an actual lat/lon rather than an empty list. If list is empty, return None Parameters ---------- values: List The list of lat/lon values returned by Strava. This list will be empty if there was no GPS associated with the activity. Returns ------- List or None list of lat/lon values or None """ # Strava sometimes returns empty list in case of activities without GPS # Explicitly return a list to make mypy happy if values: return list(values) else: return None @property def lat(self) -> float: """The latitude value of an x,y coordinate. Returns ------- float The latitude value. """ return self.root[0] @property def lon(self) -> float: """ The longitude value of an x,y coordinate. Returns ------- float The longitude value. """ return self.root[1] class MetaClub(strava_model.MetaClub, BoundClientEntity): """ Represents an identifiable club with lazily loaded properties to obtain this club's members and activities. """ @lazy_property def members(self) -> BatchedResultsIterator[strava_model.ClubAthlete]: """ Lazy property to retrieve club members stored as Athlete objects. Returns ------- List A list of club members stored as Athlete objects. """ assert self.bound_client is not None, "Bound client is not set." return self.bound_client.get_club_members(self.id) @lazy_property def activities(self) -> BatchedResultsIterator[ClubActivity]: """ Lazy property to retrieve club activities. Returns ------- Iterator An iterator of Activity objects representing club activities. """ assert self.bound_client is not None, "Bound client is not set." return self.bound_client.get_club_activities(self.id) class SummaryClub(MetaClub, strava_model.SummaryClub): """ Represents a single club with detailed information about the club including the club's location, activity types, etc. See Also -------- DetailedClub : A class representing a club's detailed information. Notes ----- Clubs are the only object that can have multiple valid `activity_types`. Activities only have one. Endpoint docs are found here: https://developers.strava.com/docs/reference/#api-models-SummaryClub """ # Undocumented attributes: profile: str | None = None description: str | None = None club_type: str | None = None class DetailedClub(SummaryClub, strava_model.DetailedClub): """The detailed club object contains all of the club data available to the authenticated Athlete.""" ... class ActivityTotals(strava_model.ActivityTotal): """Contains a set of total values for an activity including elapsed time, moving time, distance and elevation gain. """ # Attribute overrides for custom types: distance: DistanceType | None = None elevation_gain: DistanceType | None = None elapsed_time: DurationType | None = None moving_time: DurationType | None = None class AthleteStats(strava_model.ActivityStats): """ Summary totals for rides, runs and swims, as shown in an athlete's public profile. Non-public activities are not counted for these totals. """ # Field overrides from superclass for type extensions: recent_ride_totals: ActivityTotals | None = None recent_run_totals: ActivityTotals | None = None recent_swim_totals: ActivityTotals | None = None ytd_ride_totals: ActivityTotals | None = None ytd_run_totals: ActivityTotals | None = None ytd_swim_totals: ActivityTotals | None = None all_ride_totals: ActivityTotals | None = None all_run_totals: ActivityTotals | None = None all_swim_totals: ActivityTotals | None = None biggest_ride_distance: DistanceType | None = None biggest_climb_elevation_gain: DistanceType | None = None class MetaAthlete(strava_model.MetaAthlete, BoundClientEntity): """ Represents an identifiable athlete with lazily loaded property to obtain this athlete's summary stats. """ # Undocumented resource_state: int | None = None @lazy_property def stats(self) -> AthleteStats: """ Grabs statistics for an (authenticated) athlete. Returns ------- Associated :class:`stravalib.model.AthleteStats` Raises ------ `stravalib.exc.NotAuthenticatedAthlete` exception if authentication is missing. """ assert self.bound_client is not None, "Bound client is not set." return self.bound_client.get_athlete_stats(self.id) class SummaryAthlete(MetaAthlete, strava_model.SummaryAthlete): """The Summary Athlete object. This is redefined here to inherit the `BoundClient` which allows API access for lazy methods.""" ... class DetailedAthlete(SummaryAthlete, strava_model.DetailedAthlete): """Represents high level athlete information including their name, email, clubs they belong to, bikes, shoes, etc. Notes ------ Also provides access to detailed athlete stats upon request. Many attributes in this object are undocumented by Strava and could be modified at any time. """ # Field overrides from superclass for type extensions: clubs: Sequence[SummaryClub] | None = None # Undocumented attributes: athlete_type: Literal["cyclist", "runner"] | None = None friend: str | None = None follower: str | None = None approve_followers: bool | None = None badge_type_id: int | None = None mutual_friend_count: int | None = None date_preference: str | None = None email: str | None = None super_user: bool | None = None email_language: str | None = None max_heartrate: float | None = None username: str | None = None description: str | None = None instagram_username: str | None = None offer_in_app_payment: bool | None = None global_privacy: bool | None = None receive_newsletter: bool | None = None email_kom_lost: bool | None = None dateofbirth: date | None = None facebook_sharing_enabled: bool | None = None profile_original: str | None = None premium_expiration_date: int | None = None email_send_follower_notices: bool | None = None plan: str | None = None agreed_to_terms: str | None = None follower_request_count: int | None = None email_facebook_twitter_friend_joins: bool | None = None receive_kudos_emails: bool | None = None receive_follower_feed_emails: bool | None = None receive_comment_emails: bool | None = None sample_race_distance: int | None = None sample_race_time: int | None = None membership: str | None = None admin: bool | None = None """Indicates if the athlete has admin privileges.""" owner: bool | None = None subscription_permissions: Sequence[bool] | None = None @field_validator("athlete_type", mode="before") def to_str_representation( cls, raw_type: int ) -> Literal["cyclist", "runner"] | None: """Replaces legacy 'ChoicesAttribute' class. Parameters ---------- raw_type : int The raw integer representing the athlete type. Returns ------- str The string representation of the athlete type. """ if raw_type == 0: return "cyclist" elif raw_type == 1: return "runner" else: LOGGER.warning(f"Unknown athlete type value: {raw_type}") return None class ActivityPhotoPrimary(strava_model.Primary): """ Represents the primary photo for an activity. Attributes ---------- use_primary_photo : bool, optional Indicates whether the photo is used as the primary photo. Notes ----- https://developers.strava.com/docs/reference/#api-models-PhotosSummary_primary This object is not returned in strava_model, but it is documented in the Strava API V3 spec. The Spec documentation deviates from what is actually returned. This model represents what is actually returned. """ # Strava_model has this defined as id but unique_id is the correct attr name # it's also defined as an int but should be a string unique_id: str | None media_type: int | None = None class PhotosSummary(strava_model.PhotosSummary): """ Represents the metadata of photos returned with the activity. Not to be confused with the fully loaded photos for an activity. Attributes ---------- primary : ActivityPhoto_primary, optional The primary photo for the activity. use_primary_photo : bool, optional Indicates whether the primary photo is used. Not currently documented by Strava. Notes ----- Undocumented attributes could be changed by Strava at any time. THe API spec specifies that this object returns count and ActivityPhoto_primary https://developers.strava.com/docs/reference/#api-models-PhotosSummary """ primary: ActivityPhotoPrimary | None = None # Undocumented by strava use_primary_photo: bool | None = None class ActivityPhoto(BaseModel): """A full photo record attached to an activity. Notes ----- Warning: this entity is undocumented by Strava and there is no official endpoint to retrieve it """ activity_id: int | None = None activity_name: str | None = None athlete_id: int | None = None caption: str | None = None created_at: datetime | None = None created_at_local: datetime | None = None default_photo: bool | None = None location: LatLon | None = None post_id: int | None = None sizes: dict[str, Sequence[int]] | None = None source: int | None = None type: int | None = None unique_id: str | None = None uploaded_at: datetime | None = None urls: dict[str, str] | None = None # TODO: i don't see these in the actual returns but they are used below ref: str | None = None uid: str | None = None _naive_local = field_validator("created_at_local")(naive_datetime) _check_latlng = field_validator("location", mode="before")( check_valid_location ) def __repr__(self) -> str: """Return a string representation of the instance. This representation varies according to the source of the photo (native, from Instagram, or an unknown source. This representation includes the class name, photo type, and a key identifier.""" if self.source == 1: photo_type = "native" idfield = "unique_id" idval = self.unique_id elif self.source == 2: photo_type = "instagram" idfield = "uid" idval = self.uid else: photo_type = "(no type)" idfield = "id" idval = self.uid return "<{clz} {type} {idfield}={id}>".format( clz=self.__class__.__name__, type=photo_type, idfield=idfield, id=idval, ) class Lap( strava_model.Lap, BoundClientEntity, ): """An object that represents an Activity lap. This object inherits the BoundClient to support API access through lazy methods. """ # Field overrides from superclass for type extensions: activity: MetaActivity | None = None athlete: MetaAthlete | None = None distance: DistanceType | None = None total_elevation_gain: DistanceType | None = None elapsed_time: DurationType | None = None moving_time: DurationType | None = None average_speed: VelocityType | None = None max_speed: VelocityType | None = None # Undocumented attributes: average_watts: float | None = None average_heartrate: float | None = None max_heartrate: float | None = None device_watts: bool | None = None _naive_local = field_validator("start_date_local")(naive_datetime) class Map(PolylineMap): """Pass through object. Inherits from PolyLineMap""" pass class Split( strava_model.Split, ): """ A split -- may be metric or standard units (which has no bearing on the units used in this object, just the binning of values). """ # Attribute overrides for type extensions: distance: DistanceType | None = None elevation_difference: DistanceType | None = None elapsed_time: DurationType | None = None moving_time: DurationType | None = None average_speed: VelocityType | None = None # Undocumented attributes: average_heartrate: float | None = None average_grade_adjusted_speed: VelocityType | None = None class SegmentExplorerResult( ExplorerSegment, BoundClientEntity, ): """Represents a segment result from the segment explorer feature. Notes ----- These do not represent full segment objects. The segment object can be fetched using the 'segment' property of this object. """ # Field overrides from superclass for type extensions: elev_difference: DistanceType | None = None distance: DistanceType | None = None start_latlng: LatLon | None = None end_latlng: LatLon | None = None # Undocumented attributes: starred: bool | None = None _check_latlng = field_validator( "start_latlng", "end_latlng", mode="before" )(check_valid_location) @lazy_property def segment(self) -> Segment: """Returns the associated (full) :class:`Segment` object. This property is lazy-loaded. Segment objects will be fetched from the bound client only when explicitly accessed for the first time. Returns ------- Segment object or None The associated Segment object, if available; otherwise, returns None. """ assert self.bound_client is not None return self.bound_client.get_segment(self.id) class AthletePrEffort( strava_model.SummaryPRSegmentEffort, ): """An object that holds athlete PR effort attributes.""" # Override fields from superclass to match actual responses by Strava API: activity_id: int | None = Field( validation_alias=AliasChoices("pr_activity_id", "activity_id"), default=None, ) elapsed_time: int | None = Field( validation_alias=AliasChoices("pr_elapsed_time", "elapsed_time"), default=None, ) # Attribute overrides for type extensions: pr_elapsed_time: DurationType | None = None # Undocumented attributes: distance: DistanceType | None = None start_date: datetime | None = None start_date_local: datetime | None = None is_kom: bool | None = None _naive_local = field_validator("start_date_local")(naive_datetime) class SummarySegment(strava_model.SummarySegment, BoundClientEntity): """Contains summary information for a specific segment Notes ----- Several attributes represent overrides from the superclass to support accurate typing. """ # Field overrides from superclass for type extensions: start_latlng: LatLon | None = None end_latlng: LatLon | None = None athlete_pr_effort: AthletePrEffort | None = None # Ignore because the spec is incorrectly typed - Optional[Literal["Ride", "Run"]] activity_type: RelaxedActivityType | None = None # type: ignore[assignment] athlete_segment_stats: AthleteSegmentStats | None = ( None # Actually, this is only part of a (detailed) segment response ) _latlng_check = field_validator( "start_latlng", "end_latlng", mode="before" )(check_valid_location) class Segment(SummarySegment, strava_model.DetailedSegment): """ Represents a single Strava segment. """ # Field overrides from superclass for type extensions: map: Map | None = None distance: DistanceType | None = None elevation_high: DistanceType | None = None elevation_low: DistanceType | None = None total_elevation_gain: DistanceType | None = None # Undocumented attributes: start_latitude: float | None = None end_latitude: float | None = None start_longitude: float | None = None end_longitude: float | None = None starred: bool | None = None pr_time: timedelta | None = None starred_date: datetime | None = None elevation_profile: str | None = None class SegmentEffortAchievement(BaseModel): """ An undocumented structure being returned for segment efforts. Notes ----- Undocumented Strava elements can change at any time without notice. """ rank: int | None = None """ Rank in segment (either overall leader board, or pr rank) """ type: str | None = None """ The type of achievement -- e.g. 'year_pr' or 'overall' """ type_id: int | None = None """ Numeric ID for type of achievement? (6 = year_pr, 2 = overall ??? other?) """ effort_count: int | None = None class SummarySegmentEffort(strava_model.SummarySegmentEffort): """Returns summary information for a segment in an activity.""" # Override superclass fields to match actual Strava API responses activity_id: int | None = Field( validation_alias=AliasChoices("pr_activity_id", "activity_id"), default=None, ) elapsed_time: int | None = Field( validation_alias=AliasChoices("pr_elapsed_time", "elapsed_time"), default=None, ) _naive_local = field_validator("start_date_local")(naive_datetime) class BaseEffort( SummarySegmentEffort, strava_model.DetailedSegmentEffort, ): """Base class for a best effort or segment effort.""" # Field overrides from superclass for type extensions: segment: SummarySegment | None = None activity: MetaActivity | None = None athlete: MetaAthlete | None = None elapsed_time: DurationType | None = None moving_time: DurationType | None = None distance: DistanceType | None = None class BestEffort(BaseEffort): """Class representing a best effort (e.g. best time for 5k)""" pass class SegmentEffort(BaseEffort): """Class representing a best effort on a particular segment.""" achievements: Sequence[SegmentEffortAchievement] | None = None class AthleteSegmentStats( SummarySegmentEffort, ): """ A structure being returned for segment stats for current athlete. """ # Attribute overrides for type extensions: distance: DistanceType | None = None elapsed_time: DurationType | None = None # Undocumented attributes: effort_count: int | None = None pr_date: date | None = None class MetaActivity(strava_model.MetaActivity, BoundClientEntity): """ Represents an identifiable activity with lazily loaded properties to collect this activity's comments, zones, kudos and photos. """ @lazy_property def comments(self) -> BatchedResultsIterator[Comment]: """Retrieves comments for a specific activity id.""" assert self.bound_client is not None return self.bound_client.get_activity_comments(self.id) @lazy_property def zones(self) -> list[ActivityZone]: """Retrieve a list of zones for an activity. Returns ------- list A list of :class:`stravalib.model.ActivityZone` objects. """ assert self.bound_client is not None return self.bound_client.get_activity_zones(self.id) @lazy_property def kudos(self) -> BatchedResultsIterator[SummaryAthlete]: """Retrieves the kudos provided for a specific activity.""" assert self.bound_client is not None return self.bound_client.get_activity_kudos(self.id) @lazy_property def full_photos(self) -> BatchedResultsIterator[ActivityPhoto]: """Retrieves activity photos for a specific activity by id.""" assert self.bound_client is not None return self.bound_client.get_activity_photos( self.id, only_instagram=False ) class SummaryActivity(MetaActivity, strava_model.SummaryActivity): """The Activity object that contains high level summary activity for an activity. Notes ----- In the case that the Strava spec is misaligned with the data actually returned, we override attributes as needed. """ # field overrides from superclass for type extensions: athlete: MetaAthlete | None = None average_speed: VelocityType | None = None distance: DistanceType | None = None elapsed_time: DurationType | None = None # These force validator to run on lat/lon start_latlng: LatLon | None = None end_latlng: LatLon | None = None map: Map | None = None max_speed: VelocityType | None = None moving_time: DurationType | None = None type: RelaxedActivityType | None = None sport_type: RelaxedSportType | None = None timezone: TimezoneType | None = None total_elevation_gain: DistanceType | None = None # Undocumented attributes: utc_offset: float | None = None location_city: str | None = None location_state: str | None = None location_country: str | None = None pr_count: int | None = None suffer_score: int | None = None has_heartrate: bool | None = None average_heartrate: float | None = None max_heartrate: int | None = None average_cadence: float | None = None from_accepted_tag: bool | None = None visibility: str | None = None _latlng_check = field_validator( "start_latlng", "end_latlng", mode="before" )(check_valid_location) class DetailedActivity( SummaryActivity, strava_model.DetailedActivity, ): """ Represents an activity (ride, run, etc.). """ # Field overrides from superclass for type extensions: gear: SummaryGear | None = None best_efforts: Sequence[BestEffort] | None = None # TODO: returning empty Sequence should be DetailedSegmentEffort object # TODO: test on activity with actual segments segment_efforts: Sequence[SegmentEffort] | None = None # TODO: Returns Split object - check returns for that object splits_metric: Sequence[Split] | None = None splits_standard: Sequence[Split] | None = None photos: PhotosSummary | None = None laps: Sequence[Lap] | None = None # Added for backward compatibility # TODO maybe deprecate? TYPES: ClassVar[tuple[Any, ...]] = get_args( ActivityType.model_fields["root"].annotation ) SPORT_TYPES: ClassVar[tuple[Any, ...]] = get_args( SportType.model_fields["root"].annotation ) # Undocumented attributes: guid: str | None = None start_latitude: float | None = None start_longitude: float | None = None average_temp: int | None = None instagram_primary_photo: str | None = None partner_logo_url: str | None = None partner_brand_tag: str | None = None segment_leaderboard_opt_out: bool | None = None perceived_exertion: int | None = None prefer_perceived_exertion: bool | None = None private_note: str | None = None _naive_local = field_validator("start_date_local")(naive_datetime) class ClubActivity(strava_model.ClubActivity): """Represents an activity returned from a club. Notes ----- The Strava API specification suggests that this should return a `MetaAthlete` Object for the athlete associated with this activity. However, the object spec is missing what is actually returned, i.e., resource_state, first name, and last initial. This object matches the actual return data, not the spec. """ # Intentional class override as spec returns metaAthlete object # (which only contains id) athlete: strava_model.ClubAthlete | None = None # type: ignore[assignment] class TimedZoneDistribution(strava_model.TimedZoneRange): """ A single distribution bucket object, used for activity zones. Notes ----- Min/max value types are overridden. The Strava API incorrectly types zones as being `int`. However it can return `int` or `float` (floats are returned for pace, ints for heartrate and power). """ # Type overrides to support pace values returned as ints min: float | None = None # type: ignore[assignment] max: float | None = None # type: ignore[assignment] class ActivityZone( strava_model.ActivityZone, BoundClientEntity, ): """ Base class for activity zones. A collection of :class:`stravalib.strava_model.TimedZoneDistribution` objects. """ # Field overrides from superclass for type extensions: distribution_buckets: Sequence[TimedZoneDistribution] | None = None # type: ignore[assignment] # strava_model only contains heartrate and power (ints), but also returns pace (float) type: Literal["heartrate", "power", "pace"] | None = None # type: ignore[assignment] class Stream(BaseStream): """Stream of readings from the activity, effort or segment.""" type: str | None = None # Not using the typed subclasses from the generated model # for backward compatibility: data: Sequence[Any] | None = None class Route( strava_model.Route, BoundClientEntity, ): """Represents a spatial route created by an athlete.""" # Superclass field overrides for using extended types distance: DistanceType | None = None elevation_gain: DistanceType | None = None athlete: SummaryAthlete | None = None map: Map | None = None segments: Sequence[SummarySegment] | None = None class Subscription(BaseModel): """ Represents a Webhook Event Subscription. """ OBJECT_TYPE_ACTIVITY: ClassVar[str] = "activity" ASPECT_TYPE_CREATE: ClassVar[str] = "create" VERIFY_TOKEN_DEFAULT: ClassVar[str] = "STRAVA" id: int | None = None application_id: int | None = None object_type: str | None = None aspect_type: str | None = None callback_url: str | None = None created_at: datetime | None = None updated_at: datetime | None = None class SubscriptionCallback(BaseModel): """ Represents a Webhook Event Subscription Callback. """ hub_mode: str | None = Field(None, alias="hub.mode") hub_verify_token: str | None = Field(None, alias="hub.verify_token") hub_challenge: str | None = Field(None, alias="hub.challenge") def validate_token( self, verify_token: str = Subscription.VERIFY_TOKEN_DEFAULT ) -> None: """ Validate the subscription's hub_verify_token against the provided token. Parameters ---------- verify_token : str The token to verify subscription If not provided, it uses the default `VERIFY_TOKEN_DEFAULT`. Returns ------- None None if an assertion error is not raised Raises ------ AssertionError If the hub_verify_token does not match the provided verify_token. """ assert self.hub_verify_token == verify_token class SubscriptionUpdate(BaseModel): """ Represents a Webhook Event Subscription Update. """ subscription_id: int | None = None owner_id: int | None = None object_id: int | None = None object_type: str | None = None aspect_type: str | None = None event_time: datetime | None = None updates: dict[str, Any] | None = None stravalib-2.2/src/stravalib/protocol.py000066400000000000000000000541401475174155400203560ustar00rootroot00000000000000"""Protocol ============== Low-level classes for interacting directly with the Strava API webservers. """ from __future__ import annotations import abc import functools import logging import os import time from collections.abc import Callable from typing import TYPE_CHECKING, Any, Literal, TypedDict from urllib.parse import urlencode, urljoin, urlunsplit import requests from stravalib import exc if TYPE_CHECKING: from _typeshed import SupportsRead Scope = Literal[ "read", "read_all", "profile:read_all", "profile:write", "activity:read", "activity:read_all", "activity:write", ] RequestMethod = Literal["GET", "POST", "PUT", "DELETE"] class AccessInfo(TypedDict): """Dictionary containing token exchange response from Strava.""" access_token: str """A short live token the access Strava API""" refresh_token: str """The refresh token for this user, to be used to get the next access token for this user. Please expect that this value can change anytime you retrieve a new access token. Once a new refresh token code has been returned, the older code will no longer work. """ expires_at: int """The number of seconds since the epoch when the provided access token will expire""" class ApiV3(metaclass=abc.ABCMeta): """This class is responsible for performing the HTTP requests, rate limiting, and error handling.""" server = "www.strava.com" api_base = "/api/v3" def __init__( self, access_token: str | None = None, requests_session: requests.Session | None = None, rate_limiter: ( Callable[[dict[str, str], RequestMethod], None] | None ) = None, token_expires: int | None = None, refresh_token: str | None = None, client_id: int | None = None, client_secret: str | None = None, ): """Initialize this protocol client, optionally providing a (shared) :class:`requests.Session` object. Parameters ---------- access_token : str The token that provides access to a specific Strava account. requests_session : :class:`requests.Session` An existing :class:`requests.Session` object to use. rate_limiter : Callable[[dict[str, str], RequestMethod], None], optional A callable used to enforce rate-limiting for API requests. The callable accepts a dict of headers and the HTTP request method as arguments. Defaults to None. token_expires: int Epoch time in seconds when the token expires refresh_token: str Refresh token used to re-authenticate with Strava client_id: int client id for the Strava APP pulled from the users envt client_secret: str client secret for the Strava app pulled from the users envt """ self.log = logging.getLogger( "{0.__module__}.{0.__name__}".format(self.__class__) ) self.access_token: str | None = access_token self.token_expires: int | None = token_expires self.refresh_token: str | None = refresh_token self.client_id: int | None = None self.client_secret: str | None = None if requests_session: self.rsession: requests.Session = requests_session else: self.rsession = requests.Session() self.rate_limiter = rate_limiter or ( lambda _request_params, _method: None ) # Check for credentials when initializing self._check_credentials() def _check_credentials(self) -> None: """Gets Strava client_id and secret credentials from user's environment. If the user environment is populated with both values and client_id is a proper int, it returns a tuple with both values. Otherwise it returns None. Returns ------- None If the client_id and secret are available it populates self with both variables to support automatic token refresh. """ # Default both to None; set if they are available in the correct format client_id: int | None = None client_secret: str | None = None client_id_str = os.environ.get("STRAVA_CLIENT_ID") client_secret = os.environ.get("STRAVA_CLIENT_SECRET") # Make sure client_id exists and can be cast to int if client_id_str: try: # Make sure client_id is a valid int client_id = int(client_id_str) except ValueError: logging.error("STRAVA_CLIENT_ID must be a valid integer.") else: logging.error( "Please make sure your STRAVA_CLIENT_ID is set in your environment." ) if client_id and client_secret: self.client_id = client_id self.client_secret = client_secret else: logging.warning( "STRAVA_CLIENT_ID and STRAVA_CLIENT_SECRET not found in your " " environment. Please refresh your access_token manually." " Or add STRAVA_CLIENT_ID and STRAVA_CLIENT_SECRET to your environment." ) return None def _request( self, url: str, params: dict[str, Any] | None = None, files: dict[str, SupportsRead[str | bytes]] | None = None, method: RequestMethod = "GET", check_for_errors: bool = True, ) -> Any: """Perform the underlying request, returning the parsed JSON results. Before running, this method will check to make sure that your access_token is valid. If it isn't, it will refresh it and then make the requested Strava API call. Parameters ---------- url : str The request URL. params : Dict[str,Any] Request parameters files : Dict[str,file] Dictionary of file name to file-like objects. method : str The request method (GET/POST/etc.) check_for_errors : bool Whether to raise an error or not. Returns ------- Dict[str, Any] The parsed JSON response. """ # Only refresh token if we know the users' environment is setup if "/oauth/token" not in url and self.client_id and self.client_secret: self.refresh_expired_token() url = self.resolve_url(url) self.log.info( "{method} {url!r} with params {params!r}".format( method=method, url=url, params=params ) ) if params is None: params = {} if self.access_token: params["access_token"] = self.access_token methods = { "GET": self.rsession.get, "POST": functools.partial(self.rsession.post, files=files), "PUT": self.rsession.put, "DELETE": self.rsession.delete, } try: requester = methods[method.upper()] except KeyError: raise ValueError( "Invalid/unsupported request method specified: {}".format( method ) ) raw = requester(url, params=params) # type: ignore[operator] # Rate limits are taken from HTTP response headers # https://developers.strava.com/docs/rate-limits/ self.rate_limiter(raw.headers, method) if check_for_errors: self._handle_protocol_error(raw) # 204 = No content if raw.status_code in [204]: resp = {} else: resp = raw.json() return resp def _token_expired(self) -> bool: """Checks if a token has expired or not. Returns True if it's expired. Returns ------- bool True if token has expired, otherwise returns false """ if self.token_expires: if time.time() > self.token_expires: print("Your token has expired; Refreshing it now.") return True else: return False else: logging.warning( "Please make sure you've set client.token_expires if you" " want automatic token refresh to work" ) return False def refresh_expired_token(self) -> None: """Checks to see if a token has expired and auto refreshes it if the user has setup their environment with the client secret information. Returns ------- None If all is setup properly, updates and resets the access_token attribute. Otherwise it logs a warning """ if not self.refresh_token: logging.warning( "Please set client.refresh_token if you want to use" "the auto token-refresh feature" ) return # Token is not yet expired, move on if not self._token_expired(): return # This should never be false, BUT mypy wants a reminder that these values # are populated assert self.client_id is not None, "client_id is required but is None." assert ( self.client_secret is not None ), "client_secret is required but is None." # If the token is expired AND the refresh token exists if self._token_expired() and self.refresh_token: self.refresh_access_token( client_id=self.client_id, client_secret=self.client_secret, refresh_token=self.refresh_token, ) return def authorization_url( self, client_id: int, redirect_uri: str, approval_prompt: Literal["auto", "force"] = "auto", scope: list[Scope] | Scope | None = None, state: str | None = None, ) -> str: """Get the URL needed to authorize your application to access a Strava user's information. See https://developers.strava.com/docs/authentication/ Parameters ---------- client_id : int The numeric developer client id. redirect_uri : str The URL that Strava will redirect to after successful (or failed) authorization. approval_prompt : str Whether to prompt for approval even if approval already granted to app. Choices are 'auto' or 'force'. (Default is 'auto') scope : list[str] The access scope required. Omit to imply "read" and "activity:read" Valid values are 'read', 'read_all', 'profile:read_all', 'profile:write', 'activity:read', 'activity:read_all', 'activity:write'. state : str An arbitrary variable that will be returned to your application in the redirect URI. Returns ------- str The URL to use for authorization link. """ assert approval_prompt in ("auto", "force") if scope is None: scope = ["read", "activity:read"] elif isinstance(scope, (str, bytes)): scope = [scope] unsupported = set(scope) - { "read", "read_all", "profile:read_all", "profile:write", "activity:read", "activity:read_all", "activity:write", } assert not unsupported, "Unsupported scope value(s): {}".format( unsupported ) params = { "client_id": client_id, "redirect_uri": redirect_uri, "approval_prompt": approval_prompt, "scope": ",".join(scope), "response_type": "code", } if state is not None: params["state"] = state return urlunsplit( ("https", self.server, "/oauth/authorize", urlencode(params), "") ) def exchange_code_for_token( self, client_id: int, client_secret: str, code: str, return_athlete: bool = False, ) -> tuple[AccessInfo, dict[str, Any] | None]: """Exchange the temporary authorization code (returned with redirect from Strava authorization URL) for a short-lived access token and a refresh token (used to obtain the next access token later on). Parameters ---------- client_id : int The numeric developer client id. client_secret : str The developer client secret code : str The temporary authorization code return_athlete : bool (optional, default=False) Whether to return the `SummaryAthlete` object with the token response. This behavior is currently undocumented and could change at any time. Default is `False`. Returns ------- dict Dictionary containing the access_token, refresh_token and expires_at (number of seconds since Epoch when the provided access token will expire) """ # The method returns: No rates present in response headers response = self._request( f"https://{self.server}/oauth/token", params={ "client_id": client_id, "client_secret": client_secret, "code": code, "grant_type": "authorization_code", }, method="POST", ) access_info: AccessInfo = { "access_token": response["access_token"], "refresh_token": response["refresh_token"], "expires_at": response["expires_at"], } self.access_token = response["access_token"] if return_athlete: # This will be None if Strava removes undocumented athlete response # from the undocumented endpoint return return access_info, response.get("athlete") else: return access_info, None def refresh_access_token( self, client_id: int, client_secret: str, refresh_token: str, ) -> AccessInfo: """Exchanges the previous refresh token for a short-lived access token and a new refresh token (used to obtain the next access token later on) Parameters ---------- client_id : int The numeric developer client id. client_secret : str The developer client secret refresh_token : str The refresh token obtain from a previous authorization request Returns ------- dict Dictionary containing the access_token, refresh_token and expires_at (number of seconds since Epoch when the provided access token will expire) Notes ----- This method is user facing. Here, we don't populate client_id and client_secret from self; A user can call this method and refresh the token manually with those values. """ response = self._request( f"https://{self.server}/oauth/token", params={ "client_id": client_id, "client_secret": client_secret, "refresh_token": refresh_token, "grant_type": "refresh_token", }, method="POST", ) access_info: AccessInfo = { "access_token": response["access_token"], "refresh_token": response["refresh_token"], "expires_at": response["expires_at"], } self.access_token = response["access_token"] # Update expires_at and refresh to support automatic refresh self.refresh_token = response["refresh_token"] self.token_expires = response["expires_at"] return access_info def resolve_url(self, url: str) -> str: """ Parameters ---------- url : str url string to be be accessed / resolved Returns ------- str A string representing the full properly formatted (https) url. """ if not url.startswith("http"): url = urljoin( f"https://{self.server}", self.api_base + "/" + url.strip("/"), ) return url def _handle_protocol_error( self, response: requests.Response ) -> requests.Response: """Parses the raw response from the server, raising a :class:`stravalib.exc.Fault` if the server returned an error. Parameters ---------- response The response object. Raises ------ Fault If the response contains an error. """ error_str = None try: json_response = response.json() except ValueError: pass else: if "message" in json_response or "errors" in json_response: error_str = "{}: {}".format( json_response.get("message", "Undefined error"), json_response.get("errors"), ) # Special subclasses for some errors if response.status_code == 404: msg = f"{response.reason}: {error_str}" raise exc.ObjectNotFound(msg, response=response) elif response.status_code == 401: msg = f"{response.reason}: {error_str}" raise exc.AccessUnauthorized(msg, response=response) elif 400 <= response.status_code < 500: msg = "{} Client Error: {} [{}]".format( response.status_code, response.reason, error_str, ) raise exc.Fault(msg, response=response) elif 500 <= response.status_code < 600: msg = "{} Server Error: {} [{}]".format( response.status_code, response.reason, error_str, ) raise exc.Fault(msg, response=response) elif error_str: msg = error_str raise exc.Fault(msg, response=response) return response def _extract_referenced_vars(self, s: str) -> list[str]: """Utility method to find the referenced format variables in a string. (Assumes string.format() format vars.) Parameters ---------- s The string that contains format variables. (e.g. "{foo}-text") Returns ------- list The list of referenced variable names. (e.g. ['foo']) """ d: dict[str, int] = {} while True: try: s.format(**d) except KeyError as exc: # exc.args[0] contains the name of the key that was not found; # 0 is used because it appears to work with all types of # placeholders. d[exc.args[0]] = 0 else: break return list(d.keys()) def get( self, url: str, check_for_errors: bool = True, **kwargs: Any ) -> Any: """Performs a generic GET request for specified params, returning the response. Parameters ---------- url : str String representing the url to retrieve check-for_errors: bool (default = True) Flag used to raise an error (or not) Returns ------- dict Performs the request and returns a JSON object deserialized as dict """ referenced = self._extract_referenced_vars(url) url = url.format(**kwargs) params = {k: v for k, v in kwargs.items() if k not in referenced} return self._request( url, params=params, check_for_errors=check_for_errors ) def post( self, url: str, files: dict[str, SupportsRead[str | bytes]] | None = None, check_for_errors: bool = True, **kwargs: Any, ) -> Any: """Performs a generic POST request for specified params, returning the response. Parameters ---------- url : str Url string to be requested. files: dict Dictionary of file name to file-like objects. Used by _requests check_for_errors: bool Whether to raise an error (or not) Returns ------- Deserialized request output. """ referenced = self._extract_referenced_vars(url) url = url.format(**kwargs) params = {k: v for k, v in kwargs.items() if k not in referenced} return self._request( url, params=params, files=files, method="POST", check_for_errors=check_for_errors, ) def put( self, url: str, check_for_errors: bool = True, **kwargs: Any ) -> Any: """Performs a generic PUT request for specified params, returning the response. Parameters ---------- url : str String representing url to access. check_for_errors: bool Whether to raise an error (or not) Returns ------- Replaces current online content with new content. """ referenced = self._extract_referenced_vars(url) url = url.format(**kwargs) params = {k: v for k, v in kwargs.items() if k not in referenced} return self._request( url, params=params, method="PUT", check_for_errors=check_for_errors ) def delete( self, url: str, check_for_errors: bool = True, **kwargs: Any ) -> Any: """Performs a generic DELETE request for specified params, returning the response. Parameters ---------- url : str String representing url to access. check_for_errors: bool Whether to raise an error (or not) Returns ------- Deletes specified current online content. """ referenced = self._extract_referenced_vars(url) url = url.format(**kwargs) params = {k: v for k, v in kwargs.items() if k not in referenced} return self._request( url, params=params, method="DELETE", check_for_errors=check_for_errors, ) stravalib-2.2/src/stravalib/py.typed000066400000000000000000000000001475174155400176240ustar00rootroot00000000000000stravalib-2.2/src/stravalib/strava_model.py000066400000000000000000001133141475174155400211740ustar00rootroot00000000000000# generated by datamodel-codegen: # filename: from __future__ import annotations from collections.abc import Mapping, Sequence from datetime import datetime from typing import Literal from pydantic import BaseModel, Field, RootModel class ActivityTotal(BaseModel): """ A roll-up of metrics pertaining to a set of activities. Values are in seconds and meters. """ achievement_count: int | None = None """ The total number of achievements of the considered activities. """ count: int | None = None """ The number of activities considered in this total. """ distance: float | None = None """ The total distance covered by the considered activities. """ elapsed_time: int | None = None """ The total elapsed time of the considered activities. """ elevation_gain: float | None = None """ The total elevation gain of the considered activities. """ moving_time: int | None = None """ The total moving time of the considered activities. """ class ActivityType( RootModel[ Literal[ "AlpineSki", "BackcountrySki", "Canoeing", "Crossfit", "EBikeRide", "Elliptical", "Golf", "Handcycle", "Hike", "IceSkate", "InlineSkate", "Kayaking", "Kitesurf", "NordicSki", "Ride", "RockClimbing", "RollerSki", "Rowing", "Run", "Sail", "Skateboard", "Snowboard", "Snowshoe", "Soccer", "StairStepper", "StandUpPaddling", "Surfing", "Swim", "Velomobile", "VirtualRide", "VirtualRun", "Walk", "WeightTraining", "Wheelchair", "Windsurf", "Workout", "Yoga", ] ] ): root: Literal[ "AlpineSki", "BackcountrySki", "Canoeing", "Crossfit", "EBikeRide", "Elliptical", "Golf", "Handcycle", "Hike", "IceSkate", "InlineSkate", "Kayaking", "Kitesurf", "NordicSki", "Ride", "RockClimbing", "RollerSki", "Rowing", "Run", "Sail", "Skateboard", "Snowboard", "Snowshoe", "Soccer", "StairStepper", "StandUpPaddling", "Surfing", "Swim", "Velomobile", "VirtualRide", "VirtualRun", "Walk", "WeightTraining", "Wheelchair", "Windsurf", "Workout", "Yoga", ] """ An enumeration of the types an activity may have. Note that this enumeration does not include new sport types (e.g. MountainBikeRide, EMountainBikeRide), activities with these sport types will have the corresponding activity type (e.g. Ride for MountainBikeRide, EBikeRide for EMountainBikeRide) """ class BaseStream(BaseModel): original_size: int | None = None """ The number of data points in this stream """ resolution: Literal["low", "medium", "high"] | None = None """ The level of detail (sampling) in which this stream was returned """ series_type: Literal["distance", "time"] | None = None """ The base series used in the case the stream was downsampled """ class CadenceStream(BaseStream): data: Sequence[int] | None = None """ The sequence of cadence values for this stream, in rotations per minute """ class ClubAthlete(BaseModel): admin: bool | None = None """ Whether the athlete is a club admin. """ firstname: str | None = None """ The athlete's first name. """ lastname: str | None = None """ The athlete's last initial. """ member: str | None = None """ The athlete's member status. """ owner: bool | None = None """ Whether the athlete is club owner. """ resource_state: int | None = None """ Resource state, indicates level of detail. Possible values: 1 -> "meta", 2 -> "summary", 3 -> "detail" """ class DistanceStream(BaseStream): data: Sequence[float] | None = None """ The sequence of distance values for this stream, in meters """ class Error(BaseModel): code: str | None = None """ The code associated with this error. """ field: str | None = None """ The specific field or aspect of the resource associated with this error. """ resource: str | None = None """ The type of resource associated with this error. """ class Fault(BaseModel): """ Encapsulates the errors that may be returned from the API. """ errors: Sequence[Error] | None = None """ The set of specific errors associated with this fault, if any. """ message: str | None = None """ The message of the fault. """ class HeartrateStream(BaseStream): data: Sequence[int] | None = None """ The sequence of heart rate values for this stream, in beats per minute """ class LatLng(RootModel[Sequence[float]]): """ A pair of latitude/longitude coordinates, represented as an array of 2 floating point numbers. """ root: Sequence[float] = Field(..., max_length=2, min_length=2) """ A pair of latitude/longitude coordinates, represented as an array of 2 floating point numbers. """ class LatLngStream(BaseStream): data: Sequence[LatLng] | None = None """ The sequence of lat/long values for this stream """ class MembershipApplication(BaseModel): active: bool | None = None """ Whether the membership is currently active """ membership: Literal["member", "pending"] | None = None """ The membership status of this application """ success: bool | None = None """ Whether the application for membership was successfully submitted """ class MetaActivity(BaseModel): id: int | None = None """ The unique identifier of the activity """ class MetaAthlete(BaseModel): id: int | None = None """ The unique identifier of the athlete """ class MetaClub(BaseModel): id: int | None = None """ The club's unique identifier. """ name: str | None = None """ The club's name. """ resource_state: int | None = None """ Resource state, indicates level of detail. Possible values: 1 -> "meta", 2 -> "summary", 3 -> "detail" """ class MovingStream(BaseStream): data: Sequence[bool] | None = None """ The sequence of moving values for this stream, as boolean values """ class Primary(BaseModel): id: int | None = None source: int | None = None unique_id: str | None = None urls: Mapping[str, str] | None = None class PhotosSummary(BaseModel): count: int | None = None """ The number of photos """ primary: Primary | None = None class PolylineMap(BaseModel): id: str | None = None """ The identifier of the map """ polyline: str | None = None """ The polyline of the map, only returned on detailed representation of an object """ summary_polyline: str | None = None """ The summary polyline of the map """ class PowerStream(BaseStream): data: Sequence[int] | None = None """ The sequence of power values for this stream, in watts """ class SmoothGradeStream(BaseStream): data: Sequence[float] | None = None """ The sequence of grade values for this stream, as percents of a grade """ class SmoothVelocityStream(BaseStream): data: Sequence[float] | None = None """ The sequence of velocity values for this stream, in meters per second """ class Split(BaseModel): average_speed: float | None = None """ The average speed of this split, in meters per second """ distance: float | None = None """ The distance of this split, in meters """ elapsed_time: int | None = None """ The elapsed time of this split, in seconds """ elevation_difference: float | None = None """ The elevation difference of this split, in meters """ moving_time: int | None = None """ The moving time of this split, in seconds """ pace_zone: int | None = None """ The pacing zone of this split """ split: int | None = None """ N/A """ class SportType( RootModel[ Literal[ "AlpineSki", "BackcountrySki", "Badminton", "Canoeing", "Crossfit", "EBikeRide", "Elliptical", "EMountainBikeRide", "Golf", "GravelRide", "Handcycle", "HighIntensityIntervalTraining", "Hike", "IceSkate", "InlineSkate", "Kayaking", "Kitesurf", "MountainBikeRide", "NordicSki", "Pickleball", "Pilates", "Racquetball", "Ride", "RockClimbing", "RollerSki", "Rowing", "Run", "Sail", "Skateboard", "Snowboard", "Snowshoe", "Soccer", "Squash", "StairStepper", "StandUpPaddling", "Surfing", "Swim", "TableTennis", "Tennis", "TrailRun", "Velomobile", "VirtualRide", "VirtualRow", "VirtualRun", "Walk", "WeightTraining", "Wheelchair", "Windsurf", "Workout", "Yoga", ] ] ): root: Literal[ "AlpineSki", "BackcountrySki", "Badminton", "Canoeing", "Crossfit", "EBikeRide", "Elliptical", "EMountainBikeRide", "Golf", "GravelRide", "Handcycle", "HighIntensityIntervalTraining", "Hike", "IceSkate", "InlineSkate", "Kayaking", "Kitesurf", "MountainBikeRide", "NordicSki", "Pickleball", "Pilates", "Racquetball", "Ride", "RockClimbing", "RollerSki", "Rowing", "Run", "Sail", "Skateboard", "Snowboard", "Snowshoe", "Soccer", "Squash", "StairStepper", "StandUpPaddling", "Surfing", "Swim", "TableTennis", "Tennis", "TrailRun", "Velomobile", "VirtualRide", "VirtualRow", "VirtualRun", "Walk", "WeightTraining", "Wheelchair", "Windsurf", "Workout", "Yoga", ] """ An enumeration of the sport types an activity may have. Distinct from ActivityType in that it has new types (e.g. MountainBikeRide) """ class StreamType( RootModel[ Literal[ "time", "distance", "latlng", "altitude", "velocity_smooth", "heartrate", "cadence", "watts", "temp", "moving", "grade_smooth", ] ] ): root: Literal[ "time", "distance", "latlng", "altitude", "velocity_smooth", "heartrate", "cadence", "watts", "temp", "moving", "grade_smooth", ] """ An enumeration of the supported types of streams. """ class SummaryActivity(MetaActivity): achievement_count: int | None = None """ The number of achievements gained during this activity """ athlete: MetaAthlete | None = None athlete_count: int | None = Field(None, ge=1) """ The number of athletes for taking part in a group activity """ average_speed: float | None = None """ The activity's average speed, in meters per second """ average_watts: float | None = None """ Average power output in watts during this activity. Rides only """ comment_count: int | None = None """ The number of comments for this activity """ commute: bool | None = None """ Whether this activity is a commute """ device_watts: bool | None = None """ Whether the watts are from a power meter, false if estimated """ distance: float | None = None """ The activity's distance, in meters """ elapsed_time: int | None = None """ The activity's elapsed time, in seconds """ elev_high: float | None = None """ The activity's highest elevation, in meters """ elev_low: float | None = None """ The activity's lowest elevation, in meters """ end_latlng: LatLng | None = None external_id: str | None = None """ The identifier provided at upload time """ flagged: bool | None = None """ Whether this activity is flagged """ gear_id: str | None = None """ The id of the gear for the activity """ has_kudoed: bool | None = None """ Whether the logged-in athlete has kudoed this activity """ hide_from_home: bool | None = None """ Whether the activity is muted """ kilojoules: float | None = None """ The total work done in kilojoules during this activity. Rides only """ kudos_count: int | None = None """ The number of kudos given for this activity """ manual: bool | None = None """ Whether this activity was created manually """ map: PolylineMap | None = None max_speed: float | None = None """ The activity's max speed, in meters per second """ max_watts: int | None = None """ Rides with power meter data only """ moving_time: int | None = None """ The activity's moving time, in seconds """ name: str | None = None """ The name of the activity """ photo_count: int | None = None """ The number of Instagram photos for this activity """ private: bool | None = None """ Whether this activity is private """ sport_type: SportType | None = None start_date: datetime | None = None """ The time at which the activity was started. """ start_date_local: datetime | None = None """ The time at which the activity was started in the local timezone. """ start_latlng: LatLng | None = None timezone: str | None = None """ The timezone of the activity """ total_elevation_gain: float | None = None """ The activity's total elevation gain. """ total_photo_count: int | None = None """ The number of Instagram and Strava photos for this activity """ trainer: bool | None = None """ Whether this activity was recorded on a training machine """ type: ActivityType | None = None """ Deprecated. Prefer to use sport_type """ upload_id: int | None = None """ The identifier of the upload that resulted in this activity """ upload_id_str: str | None = None """ The unique identifier of the upload in string format """ weighted_average_watts: int | None = None """ Similar to Normalized Power. Rides with power meter data only """ workout_type: int | None = None """ The activity's workout type """ class SummaryAthlete(MetaAthlete): city: str | None = None """ The athlete's city. """ country: str | None = None """ The athlete's country. """ created_at: datetime | None = None """ The time at which the athlete was created. """ firstname: str | None = None """ The athlete's first name. """ lastname: str | None = None """ The athlete's last name. """ premium: bool | None = None """ Deprecated. Use summit field instead. Whether the athlete has any Summit subscription. """ profile: str | None = None """ URL to a 124x124 pixel profile picture. """ profile_medium: str | None = None """ URL to a 62x62 pixel profile picture. """ resource_state: int | None = None """ Resource state, indicates level of detail. Possible values: 1 -> "meta", 2 -> "summary", 3 -> "detail" """ sex: Literal["M", "F"] | None = None """ The athlete's sex. """ state: str | None = None """ The athlete's state or geographical region. """ summit: bool | None = None """ Whether the athlete has any Summit subscription. """ updated_at: datetime | None = None """ The time at which the athlete was last updated. """ class SummaryClub(MetaClub): activity_types: Sequence[ActivityType] | None = None """ The activity types that count for a club. This takes precedence over sport_type. """ city: str | None = None """ The club's city. """ country: str | None = None """ The club's country. """ cover_photo: str | None = None """ URL to a ~1185x580 pixel cover photo. """ cover_photo_small: str | None = None """ URL to a ~360x176 pixel cover photo. """ featured: bool | None = None """ Whether the club is featured or not. """ member_count: int | None = None """ The club's member count. """ private: bool | None = None """ Whether the club is private. """ profile_medium: str | None = None """ URL to a 60x60 pixel profile picture. """ sport_type: Literal["cycling", "running", "triathlon", "other"] | None = ( None ) """ Deprecated. Prefer to use activity_types. """ state: str | None = None """ The club's state or geographical region. """ url: str | None = None """ The club's vanity URL. """ verified: bool | None = None """ Whether the club is verified or not. """ class SummaryGear(BaseModel): distance: float | None = None """ The distance logged with this gear. """ id: str | None = None """ The gear's unique identifier. """ name: str | None = None """ The gear's name. """ primary: bool | None = None """ Whether this gear's is the owner's default one. """ resource_state: int | None = None """ Resource state, indicates level of detail. Possible values: 2 -> "summary", 3 -> "detail" """ class SummaryPRSegmentEffort(BaseModel): effort_count: int | None = None """ Number of efforts by the authenticated athlete on this segment. """ pr_activity_id: int | None = None """ The unique identifier of the activity related to the PR effort. """ pr_date: datetime | None = None """ The time at which the PR effort was started. """ pr_elapsed_time: int | None = None """ The elapsed time ot the PR effort. """ class SummarySegmentEffort(BaseModel): activity_id: int | None = None """ The unique identifier of the activity related to this effort """ distance: float | None = None """ The effort's distance in meters """ elapsed_time: int | None = None """ The effort's elapsed time """ id: int | None = None """ The unique identifier of this effort """ is_kom: bool | None = None """ Whether this effort is the current best on the leaderboard """ start_date: datetime | None = None """ The time at which the effort was started. """ start_date_local: datetime | None = None """ The time at which the effort was started in the local timezone. """ class TemperatureStream(BaseStream): data: Sequence[int] | None = None """ The sequence of temperature values for this stream, in celsius degrees """ class TimeStream(BaseStream): data: Sequence[int] | None = None """ The sequence of time values for this stream, in seconds """ class UpdatableActivity(BaseModel): commute: bool | None = None """ Whether this activity is a commute """ description: str | None = None """ The description of the activity """ gear_id: str | None = None """ Identifier for the gear associated with the activity. β€˜none’ clears gear from activity """ hide_from_home: bool | None = None """ Whether this activity is muted """ name: str | None = None """ The name of the activity """ sport_type: SportType | None = None trainer: bool | None = None """ Whether this activity was recorded on a training machine """ type: ActivityType | None = None """ Deprecated. Prefer to use sport_type. In a request where both type and sport_type are present, this field will be ignored """ class Upload(BaseModel): activity_id: int | None = None """ The identifier of the activity this upload resulted into """ error: str | None = None """ The error associated with this upload """ external_id: str | None = None """ The external identifier of the upload """ id: int | None = None """ The unique identifier of the upload """ id_str: str | None = None """ The unique identifier of the upload in string format """ status: str | None = None """ The status of this upload """ class Waypoint(BaseModel): categories: Sequence[str] | None = Field(None, min_length=0) """ Categories that the waypoint belongs to """ description: str | None = None """ A description of the waypoint (optional) """ distance_into_route: int | None = None """ The number meters along the route that the waypoint is located """ latlng: LatLng | None = None """ The location along the route that the waypoint is closest to """ target_latlng: LatLng | None = None """ A location off of the route that the waypoint is (optional) """ title: str | None = None """ A title for the waypoint """ class ZoneRange(BaseModel): max: int | None = None """ The maximum value in the range. """ min: int | None = None """ The minimum value in the range. """ class ZoneRanges(RootModel[Sequence[ZoneRange]]): root: Sequence[ZoneRange] class ActivityStats(BaseModel): """ A set of rolled-up statistics and totals for an athlete """ all_ride_totals: ActivityTotal | None = None """ The all time ride stats for the athlete. """ all_run_totals: ActivityTotal | None = None """ The all time run stats for the athlete. """ all_swim_totals: ActivityTotal | None = None """ The all time swim stats for the athlete. """ biggest_climb_elevation_gain: float | None = None """ The highest climb ridden by the athlete. """ biggest_ride_distance: float | None = None """ The longest distance ridden by the athlete. """ recent_ride_totals: ActivityTotal | None = None """ The recent (last 4 weeks) ride stats for the athlete. """ recent_run_totals: ActivityTotal | None = None """ The recent (last 4 weeks) run stats for the athlete. """ recent_swim_totals: ActivityTotal | None = None """ The recent (last 4 weeks) swim stats for the athlete. """ ytd_ride_totals: ActivityTotal | None = None """ The year to date ride stats for the athlete. """ ytd_run_totals: ActivityTotal | None = None """ The year to date run stats for the athlete. """ ytd_swim_totals: ActivityTotal | None = None """ The year to date swim stats for the athlete. """ class AltitudeStream(BaseStream): data: Sequence[float] | None = None """ The sequence of altitude values for this stream, in meters """ class ClubActivity(BaseModel): athlete: MetaAthlete | None = None distance: float | None = None """ The activity's distance, in meters """ elapsed_time: int | None = None """ The activity's elapsed time, in seconds """ moving_time: int | None = None """ The activity's moving time, in seconds """ name: str | None = None """ The name of the activity """ sport_type: SportType | None = None total_elevation_gain: float | None = None """ The activity's total elevation gain. """ type: ActivityType | None = None """ Deprecated. Prefer to use sport_type """ workout_type: int | None = None """ The activity's workout type """ class ClubAnnouncement(BaseModel): athlete: SummaryAthlete | None = None club_id: int | None = None """ The unique identifier of the club this announcements was made in. """ created_at: datetime | None = None """ The time at which this announcement was created. """ id: int | None = None """ The unique identifier of this announcement. """ message: str | None = None """ The content of this announcement """ class Comment(BaseModel): activity_id: int | None = None """ The identifier of the activity this comment is related to """ athlete: SummaryAthlete | None = None created_at: datetime | None = None """ The time at which this comment was created. """ id: int | None = None """ The unique identifier of this comment """ text: str | None = None """ The content of the comment """ class DetailedAthlete(SummaryAthlete): bikes: Sequence[SummaryGear] | None = None """ The athlete's bikes. """ clubs: Sequence[SummaryClub] | None = None """ The athlete's clubs. """ follower_count: int | None = None """ The athlete's follower count. """ friend_count: int | None = None """ The athlete's friend count. """ ftp: int | None = None """ The athlete's FTP (Functional Threshold Power). """ measurement_preference: Literal["feet", "meters"] | None = None """ The athlete's preferred unit system. """ shoes: Sequence[SummaryGear] | None = None """ The athlete's shoes. """ weight: float | None = None """ The athlete's weight. """ class DetailedClub(SummaryClub): admin: bool | None = None """ Whether the currently logged-in athlete is an administrator of this club. """ following_count: int | None = None """ The number of athletes in the club that the logged-in athlete follows. """ membership: Literal["member", "pending"] | None = None """ The membership status of the logged-in athlete. """ owner: bool | None = None """ Whether the currently logged-in athlete is the owner of this club. """ class DetailedGear(SummaryGear): brand_name: str | None = None """ The gear's brand name. """ description: str | None = None """ The gear's description. """ frame_type: int | None = None """ The gear's frame type (bike only). """ model_name: str | None = None """ The gear's model name. """ class ExplorerSegment(BaseModel): avg_grade: float | None = None """ The segment's average grade, in percents """ climb_category: int | None = Field(None, ge=0, le=5) """ The category of the climb [0, 5]. Higher is harder ie. 5 is Hors catΓ©gorie, 0 is uncategorized in climb_category. If climb_category = 5, climb_category_desc = HC. If climb_category = 2, climb_category_desc = 3. """ climb_category_desc: Literal["NC", "4", "3", "2", "1", "HC"] | None = None """ The description for the category of the climb """ distance: float | None = None """ The segment's distance, in meters """ elev_difference: float | None = None """ The segments's evelation difference, in meters """ end_latlng: LatLng | None = None id: int | None = None """ The unique identifier of this segment """ name: str | None = None """ The name of this segment """ points: str | None = None """ The polyline of the segment """ start_latlng: LatLng | None = None class HeartRateZoneRanges(BaseModel): custom_zones: bool | None = None """ Whether the athlete has set their own custom heart rate zones """ zones: ZoneRanges | None = None class Lap(BaseModel): activity: MetaActivity | None = None athlete: MetaAthlete | None = None average_cadence: float | None = None """ The lap's average cadence """ average_speed: float | None = None """ The lap's average speed """ distance: float | None = None """ The lap's distance, in meters """ elapsed_time: int | None = None """ The lap's elapsed time, in seconds """ end_index: int | None = None """ The end index of this effort in its activity's stream """ id: int | None = None """ The unique identifier of this lap """ lap_index: int | None = None """ The index of this lap in the activity it belongs to """ max_speed: float | None = None """ The maximum speed of this lat, in meters per second """ moving_time: int | None = None """ The lap's moving time, in seconds """ name: str | None = None """ The name of the lap """ pace_zone: int | None = None """ The athlete's pace zone during this lap """ split: int | None = None start_date: datetime | None = None """ The time at which the lap was started. """ start_date_local: datetime | None = None """ The time at which the lap was started in the local timezone. """ start_index: int | None = None """ The start index of this effort in its activity's stream """ total_elevation_gain: float | None = None """ The elevation gain of this lap, in meters """ class PowerZoneRanges(BaseModel): zones: ZoneRanges | None = None class StreamSet(BaseModel): altitude: AltitudeStream | None = None cadence: CadenceStream | None = None distance: DistanceStream | None = None grade_smooth: SmoothGradeStream | None = None heartrate: HeartrateStream | None = None latlng: LatLngStream | None = None moving: MovingStream | None = None temp: TemperatureStream | None = None time: TimeStream | None = None velocity_smooth: SmoothVelocityStream | None = None watts: PowerStream | None = None class SummarySegment(BaseModel): activity_type: Literal["Ride", "Run"] | None = None athlete_pr_effort: SummaryPRSegmentEffort | None = None athlete_segment_stats: SummarySegmentEffort | None = None average_grade: float | None = None """ The segment's average grade, in percents """ city: str | None = None """ The segments's city. """ climb_category: int | None = None """ The category of the climb [0, 5]. Higher is harder ie. 5 is Hors catΓ©gorie, 0 is uncategorized in climb_category. """ country: str | None = None """ The segment's country. """ distance: float | None = None """ The segment's distance, in meters """ elevation_high: float | None = None """ The segments's highest elevation, in meters """ elevation_low: float | None = None """ The segments's lowest elevation, in meters """ end_latlng: LatLng | None = None id: int | None = None """ The unique identifier of this segment """ maximum_grade: float | None = None """ The segments's maximum grade, in percents """ name: str | None = None """ The name of this segment """ private: bool | None = None """ Whether this segment is private. """ start_latlng: LatLng | None = None state: str | None = None """ The segments's state or geographical region. """ class TimedZoneRange(ZoneRange): """ A union type representing the time spent in a given zone. """ time: int | None = None """ The number of seconds spent in this zone """ class Zones(BaseModel): heart_rate: HeartRateZoneRanges | None = None power: PowerZoneRanges | None = None class DetailedSegment(SummarySegment): athlete_count: int | None = None """ The number of unique athletes who have an effort for this segment """ created_at: datetime | None = None """ The time at which the segment was created. """ effort_count: int | None = None """ The total number of efforts for this segment """ hazardous: bool | None = None """ Whether this segment is considered hazardous """ map: PolylineMap | None = None star_count: int | None = None """ The number of stars for this segment """ total_elevation_gain: float | None = None """ The segment's total elevation gain. """ updated_at: datetime | None = None """ The time at which the segment was last updated. """ class DetailedSegmentEffort(SummarySegmentEffort): activity: MetaActivity | None = None athlete: MetaAthlete | None = None average_cadence: float | None = None """ The effort's average cadence """ average_heartrate: float | None = None """ The heart heart rate of the athlete during this effort """ average_watts: float | None = None """ The average wattage of this effort """ device_watts: bool | None = None """ For riding efforts, whether the wattage was reported by a dedicated recording device """ end_index: int | None = None """ The end index of this effort in its activity's stream """ hidden: bool | None = None """ Whether this effort should be hidden when viewed within an activity """ kom_rank: int | None = Field(None, ge=1, le=10) """ The rank of the effort on the global leaderboard if it belongs in the top 10 at the time of upload """ max_heartrate: float | None = None """ The maximum heart rate of the athlete during this effort """ moving_time: int | None = None """ The effort's moving time """ name: str | None = None """ The name of the segment on which this effort was performed """ pr_rank: int | None = Field(None, ge=1, le=3) """ The rank of the effort on the athlete's leaderboard if it belongs in the top 3 at the time of upload """ segment: SummarySegment | None = None start_index: int | None = None """ The start index of this effort in its activity's stream """ class ExplorerResponse(BaseModel): segments: Sequence[ExplorerSegment] | None = None """ The set of segments matching an explorer request """ class Route(BaseModel): athlete: SummaryAthlete | None = None created_at: datetime | None = None """ The time at which the route was created """ description: str | None = None """ The description of the route """ distance: float | None = None """ The route's distance, in meters """ elevation_gain: float | None = None """ The route's elevation gain. """ estimated_moving_time: int | None = None """ Estimated time in seconds for the authenticated athlete to complete route """ id: int | None = None """ The unique identifier of this route """ id_str: str | None = None """ The unique identifier of the route in string format """ map: PolylineMap | None = None name: str | None = None """ The name of this route """ private: bool | None = None """ Whether this route is private """ segments: Sequence[SummarySegment] | None = None """ The segments traversed by this route """ starred: bool | None = None """ Whether this route is starred by the logged-in athlete """ sub_type: int | None = None """ This route's sub-type (1 for road, 2 for mountain bike, 3 for cross, 4 for trail, 5 for mixed) """ timestamp: int | None = None """ An epoch timestamp of when the route was created """ type: int | None = None """ This route's type (1 for ride, 2 for runs) """ updated_at: datetime | None = None """ The time at which the route was last updated """ waypoints: Sequence[Waypoint] | None = Field(None, min_length=0) """ The custom waypoints along this route """ class TimedZoneDistribution(RootModel[Sequence[TimedZoneRange]]): """ Stores the exclusive ranges representing zones and the time spent in each. """ root: Sequence[TimedZoneRange] """ Stores the exclusive ranges representing zones and the time spent in each. """ class ActivityZone(BaseModel): custom_zones: bool | None = None distribution_buckets: TimedZoneDistribution | None = None max: int | None = None points: int | None = None score: int | None = None sensor_based: bool | None = None type: Literal["heartrate", "power"] | None = None class DetailedActivity(SummaryActivity): best_efforts: Sequence[DetailedSegmentEffort] | None = None calories: float | None = None """ The number of kilocalories consumed during this activity """ description: str | None = None """ The description of the activity """ device_name: str | None = None """ The name of the device used to record the activity """ embed_token: str | None = None """ The token used to embed a Strava activity """ gear: SummaryGear | None = None laps: Sequence[Lap] | None = None photos: PhotosSummary | None = None segment_efforts: Sequence[DetailedSegmentEffort] | None = None splits_metric: Sequence[Split] | None = None """ The splits of this activity in metric units (for runs) """ splits_standard: Sequence[Split] | None = None """ The splits of this activity in imperial units (for runs) """ stravalib-2.2/src/stravalib/tests/000077500000000000000000000000001475174155400173015ustar00rootroot00000000000000stravalib-2.2/src/stravalib/tests/__init__.py000066400000000000000000000005241475174155400214130ustar00rootroot00000000000000import os.path # Removing unittest - exception for python < 3.0 as we no longer support it from unittest import TestCase TESTS_DIR = os.path.dirname(__file__) RESOURCES_DIR = os.path.join(TESTS_DIR, "resources") class TestBase(TestCase): def setUp(self): super().setUp() def tearDown(self): super().tearDown() stravalib-2.2/src/stravalib/tests/auth_responder.py000066400000000000000000000111621475174155400226760ustar00rootroot00000000000000#!/usr/bin/env python """ A basic authorization server. Run this with your Strava Client ID and Client Secret and access from your browser (because the Strava OAuth page uses javascript) in order to get a resulting access token. That access token can then be used to initialize a Client that can read (and/or write) data from the Strava API. You must run this from a virtualenv that has stravalib installed. Example Usage: (env) shell$ python -m stravalib.tests.auth_responder --port=8000 --client-id=123 --client-secret=deadbeefdeadbeefdeadbeefdeadbeefdeadbeef Then connect in your browser to http://localhost:8000/ The redirected response (from Strava) will deliver a code that can be exchanged for a token. The access token will be presented in the browser after the exchange. Save this value into your config (e.g. into your test.ini) to run functional tests. """ import argparse import logging import threading from http.server import BaseHTTPRequestHandler, HTTPServer from urllib import parse as urlparse from stravalib import Client class StravaAuthHTTPServer(HTTPServer): def __init__( self, server_address, RequestHandlerClass, client_id, client_secret, bind_and_activate=True, ): HTTPServer.__init__( self, server_address, RequestHandlerClass, bind_and_activate=bind_and_activate, ) self.logger = logging.getLogger("auth_server.http") self.client_id = client_id self.client_secret = client_secret self.listening_event = threading.Event() def serve_forever(self, *args, **kwargs): self.listening_event.set() return HTTPServer.serve_forever(self, *args, **kwargs) class RequestHandler(BaseHTTPRequestHandler): def do_GET(self): request_path = self.path parsed_path = urlparse.urlparse(request_path) client = Client() if request_path.startswith("/authorization"): self.send_response(200) self.send_header(b"Content-type", b"text/plain") self.end_headers() self.wfile.write(b"Authorization Handler\n\n") code = urlparse.parse_qs(parsed_path.query).get("code") if code: code = code[0] token_response = client.exchange_code_for_token( client_id=self.server.client_id, client_secret=self.server.client_secret, code=code, ) access_token = token_response["access_token"] self.server.logger.info( "Exchanged code {} for access token {}".format( code, access_token ) ) self.wfile.write(b"Access Token: {}\n".format(access_token)) else: self.server.logger.error("No code param received.") self.wfile.write(b"ERROR: No code param received.\n") else: url = client.authorization_url( client_id=self.server.client_id, redirect_uri="http://localhost:{}/authorization".format( self.server.server_port ), ) self.send_response(302) self.send_header("Content-type", "text/plain") self.send_header(b"Location", bytes(url)) self.end_headers() self.wfile.write(b"Redirect to URL: {}\n".format(url)) def main(port, client_id, client_secret): logging.basicConfig( level=logging.INFO, format="%(levelname)-8s %(message)s" ) logger = logging.getLogger("auth_responder") logger.info("Listening on localhost:%s" % port) server = StravaAuthHTTPServer( ("", port), RequestHandler, client_id=client_id, client_secret=client_secret, ) server.serve_forever() if __name__ == "__main__": parser = argparse.ArgumentParser( description="Run a local web server to receive authorization responses from Strava." ) parser.add_argument( "-p", "--port", help="Which port to bind to", action="store", type=int, default=8000, ) parser.add_argument( "--client-id", help="Strava API Client ID", action="store", required=True, ) parser.add_argument( "--client-secret", help="Strava API Client Secret", action="store", required=True, ) args = parser.parse_args() main( port=args.port, client_id=args.client_id, client_secret=args.client_secret, ) stravalib-2.2/src/stravalib/tests/conftest.py000066400000000000000000000022041475174155400214760ustar00rootroot00000000000000import os import warnings from unittest.mock import patch import pytest from stravalib import Client from stravalib.protocol import ApiV3 from stravalib.tests.integration.strava_api_stub import StravaAPIMock warnings.simplefilter("always") @pytest.fixture def mock_strava_api(): with StravaAPIMock() as api_mock: api_mock.add_passthru("https://developers.strava.com/swagger") yield api_mock @pytest.fixture def client(): return Client() @pytest.fixture def mock_strava_env(): """Fixture that mocks Strava environment variables.""" with patch.dict( os.environ, {"STRAVA_CLIENT_ID": "12345", "STRAVA_CLIENT_SECRET": "123ghp234"}, clear=True, # Ensures other environment variables are not leaked ): yield @pytest.fixture def apiv3_instance(mock_strava_env): """Fixture to create an ApiV3 instance for testing. Takes the mock_strava_env which sets up environment variables""" mock_strava_env return ApiV3( access_token="dummy_access_token", client_id=123456, client_secret="clientfoosecret", refresh_token="refresh_value123", ) stravalib-2.2/src/stravalib/tests/integration/000077500000000000000000000000001475174155400216245ustar00rootroot00000000000000stravalib-2.2/src/stravalib/tests/integration/__init__.py000066400000000000000000000000001475174155400237230ustar00rootroot00000000000000stravalib-2.2/src/stravalib/tests/integration/strava_api_stub.py000066400000000000000000000154271475174155400253750ustar00rootroot00000000000000import json import logging import os import re from functools import lru_cache, wraps from typing import Any, Callable, Dict, Optional import requests from responses import BaseResponse, RequestsMock from stravalib.protocol import ApiV3 from stravalib.tests import RESOURCES_DIR LOGGER = logging.getLogger(__name__) @lru_cache(maxsize=1) def _get_strava_api_paths(): use_local = False try: strava_api_swagger_response = requests.get( "https://developers.strava.com/swagger/swagger.json" ) if strava_api_swagger_response.status_code != 200: use_local = True except requests.exceptions.ConnectionError: use_local = True if use_local: LOGGER.warning( "Failed to retrieve recent swagger API definition from Strava, using " "(potentially stale) version from local resources" ) with open( os.path.join(RESOURCES_DIR, "strava_swagger.json"), "r" ) as swagger_file: return json.load(swagger_file)["paths"] else: return strava_api_swagger_response.json()["paths"] def _api_method_adapter(api_method: Callable) -> Callable: """ Decorator for mock registration methods of `responses.RequestsMock` """ @wraps(api_method) def method_wrapper( *args, response_update: Dict[str, Any] = None, n_results: Optional[int] = None, **kwargs, ) -> BaseResponse: """ Wrapper for mock registration methods of `responses.RequestsMock` Parameters ---------- response_update: Dict used to update any JSON object defined as an example response in the API's swagger.json n_results: The number of example objects to be returned by the mock response (in case of an array response). *args: Url and/or other positional arguments that would otherwise be provided to the `responses` registration methods. Note that url is expected to be relative, matching one of the paths defined in the Strava API's swagger.json. **kwargs Keyword arguments that would otherwise be provided to the `responses` registration methods. Returns ------- A mocked response registration object This wrapper expects a relative url that matches one of the paths in the swagger.json of Strava's API, e.g., `/activities/{Id}`. For the given HTTP method and status code, the corresponding example JSON response is retrieved from the swagger.json, and used in the mock registration as the response body. This response can be updated with the optional `response_update` argument (e.g. to set specific fields). If the response is a JSON array, the `n_results` argument specifies how many objects are in the response. Alternatively, a complete response can be specified using the `json` or `body` keywords, as is usually done using the `responses` library. In that case, the response updating - and multiplication mechanisms above are ignored and the given response is returned as-is. """ # get url from args/kwargs try: relative_url = args[0] except IndexError: try: relative_url = kwargs.pop("url") except KeyError: raise ValueError( "Expecting url either as first positional argument or keyword argument" ) # match url with swagger path path_info = _get_strava_api_paths()[relative_url] # find default response in swagger if no json response is provided in the kwargs if "json" not in kwargs: http_method = api_method.args[0].lower() response_status = kwargs.get("status", 200) try: method_responses = path_info[http_method] except KeyError: raise ValueError( f"Endpoint {relative_url} has no support for method {http_method}" ) try: response = method_responses["responses"][str(response_status)][ "examples" ]["application/json"] if isinstance(response, list) and n_results is not None: # Make sure response has n_results items response = (response * (n_results // len(response) + 1))[ :n_results ] elif n_results is not None: # Force single response in example into result list (not all examples provide lists) LOGGER.warning("Forcing example single response into list") response = [{**response, **response_update}] * n_results except KeyError: LOGGER.warning( f"There are no known example responses for HTTP status {response_status}, " f"using empty response. You may want to provide a full json response " f'using the "json" keyword argument.' ) response = {} # update fields if necessary if response_update is not None: if isinstance(response, list): response = [ {**item, **response_update} for item in response ] else: response.update(response_update) kwargs.update({"json": response}) # replace named parameters in url by wildcards matching_url = re.sub(r"\{\w+\}", r"\\w+", relative_url) return api_method( re.compile( ApiV3().resolve_url(matching_url) ), # replaces url from args[0] *args[1:], **kwargs, ) return method_wrapper class StravaAPIMock(RequestsMock): """ A stub/mock for the Strava API. This is a thin wrapper around `responses.RequestsMock`. It intercepts calls for registering mock responses and replaces these by a decorated method that supports convenience arguments that are not in the `responses` package. The methods `delete`, `get`, `head`, `options`, `patch`, `post`, and `put` are intercepted (and decorated), while the generic method `add` can be used to bypass the decoration and directly use the `responses` API. """ def __getattribute__(self, name): attr = super().__getattribute__(name) if name in [ "delete", "get", "head", "options", "patch", "post", "put", ]: return _api_method_adapter(attr) else: return attr stravalib-2.2/src/stravalib/tests/integration/test_client.py000066400000000000000000001062371475174155400245240ustar00rootroot00000000000000import datetime import json import os import warnings from unittest import mock from unittest.mock import patch import pytest import responses from responses import matchers from stravalib.client import ActivityUploader from stravalib.exc import ( AccessUnauthorized, ActivityPhotoUploadFailed, ) from stravalib.model import DetailedAthlete, SummaryAthlete, SummarySegment from stravalib.strava_model import SummaryActivity, Zones from stravalib.tests import RESOURCES_DIR from stravalib.unit_helper import miles warnings.simplefilter("always") @pytest.fixture def zone_response(): file_path = os.path.join(RESOURCES_DIR, "example_zone_response.json") with open(file_path, "r") as file: data = json.load(file) return data @pytest.fixture def default_call_kwargs(): """A fixture containing default input / call parameters for a create activity call to the strava API""" default_call_kwargs = { "name": "test", "start_date_local": "2022-01-01T09:00:00", "elapsed_time": 3600, } return default_call_kwargs @pytest.fixture def default_request_params(): """A fixture containing default request parameters for a create activity request to the strava API""" default_request_params = { "name": "test", "start_date_local": "2022-01-01T09:00:00", "elapsed_time": "3600", } return default_request_params @pytest.fixture def raw_exchange_response(): """Expected response from the protocol exchange_code_for_token method.""" return ( { "access_token": "123456", "refresh_token": "789sdf987", "expires_at": 1732417459, }, { "id": 10295934, "username": "foo_bar", "resource_state": 2, "firstname": "Foo", "lastname": "Bar", "bio": "A bio", "city": "City", "state": "State", "country": "Country", "sex": "F", "premium": True, "summit": True, }, ) def test_get_athlete(mock_strava_api, client): mock_strava_api.get("/athlete", response_update={"id": 42}) athlete = client.get_athlete() assert isinstance(athlete, DetailedAthlete) assert athlete.id == 42 assert athlete.measurement_preference == "feet" def test_get_athlete_zones(mock_strava_api, client): json_response = { "heart_rate": { "custom_zones": False, "zones": [ {"min": 0, "max": 123}, {"min": 123, "max": 153}, {"min": 153, "max": 169}, {"min": 169, "max": 184}, {"min": 184, "max": -1}, ], } } # We overwrite the response from the mock as the example in the swagger # specification is incorrect mock_strava_api.get("/athlete/zones", json=json_response) zones = client.get_athlete_zones() assert isinstance(zones, Zones) @pytest.mark.parametrize( "include_all_efforts,expected_url", ( (None, "/activities/42?include_all_efforts=False"), (False, "/activities/42?include_all_efforts=False"), (True, "/activities/42?include_all_efforts=True"), ), ) def test_get_activity( mock_strava_api, client, include_all_efforts, expected_url ): test_activity_id = 42 mock_strava_api.get( "/activities/{id}", response_update={"id": test_activity_id} ) if include_all_efforts is not None: activity = client.get_activity(test_activity_id, include_all_efforts) else: activity = client.get_activity(test_activity_id) assert mock_strava_api.calls[-1].request.url.endswith(expected_url) assert activity.id == test_activity_id def test_activity_with_segment_that_that_is_not_ride_or_run( mock_strava_api, client ): """ Make sure that activity that are not run are ride can still have segment. See issue https://github.com/stravalib/stravalib/issues/432 """ test_activity_id = 1907 mock_strava_api.get( "/activities/{id}", response_update={ "id": test_activity_id, "type": "Hike", "segment_efforts": [ { "id": 3125507031446243384, "resource_state": 2, "name": "Landmannalaugavegur Climb", "activity": { "id": 9634609164, "visibility": "followers_only", "resource_state": 1, }, "athlete": {"id": 69911568, "resource_state": 1}, "elapsed_time": 454, "moving_time": 426, "start_date": "2023-08-12T05:22:38Z", "start_date_local": "2023-08-12T05:22:38Z", "distance": 709.74, "start_index": 91, "end_index": 213, "average_cadence": 47.7, "device_watts": False, "segment": { "id": 837087, "resource_state": 2, "name": "Landmannalaugavegur Climb", "activity_type": "Hike", "distance": 709.74, "average_grade": 7.7, "maximum_grade": 427.6, "elevation_high": 624.4, "elevation_low": 570.0, "start_latlng": [ 63.99094129912555, -19.063721196725965, ], "end_latlng": [63.99059135466814, -19.075878812000155], "elevation_profile": None, "climb_category": 0, "city": None, "state": "SuΓ°urland", "country": "Iceland", "private": False, "hazardous": False, "starred": False, }, "pr_rank": None, "achievements": [], "visibility": "followers_only", "hidden": False, } ], }, ) activity = client.get_activity(test_activity_id) def test_get_activity_laps(mock_strava_api, client): mock_strava_api.get( "/activities/{id}/laps", response_update={"distance": 1000}, n_results=2, ) laps = list(client.get_activity_laps(42)) assert len(laps) == 2 assert laps[0].distance == 1000 def test_get_club_activities(mock_strava_api, client): mock_strava_api.get( "/clubs/{id}/activities", response_update={"distance": 1000}, n_results=2, ) activities = list(client.get_club_activities(42)) assert len(activities) == 2 assert activities[0].distance == 1000 def test_get_club_admins(mock_strava_api, client): mock_strava_api.get( "/clubs/{id}/admins", response_update={"firstname": "Jane"}, n_results=2, ) admins = list(client.get_club_admins(42)) assert isinstance(admins[0], SummaryAthlete) assert len(admins) == 2 assert admins[0].firstname == "Jane" def test_get_activity_zones(mock_strava_api, client, zone_response): """Returns an activities associated zone (related to heart rate and power) Notes ----- There is no example response for this endpoint in swagger.json so we created a sample json file with the return that we are currently seeing. Thus this method could break at any time if Strava changes the response output. """ mock_strava_api.get("/activities/{id}/zones", json=zone_response) # https://developers.strava.com/docs/reference/#api-models-integer activity_zones = client.get_activity_zones(42) assert len(activity_zones) == 2 assert activity_zones[0].type == "heartrate" assert activity_zones[0].sensor_based def test_get_activity_streams(mock_strava_api, client): query_params = {"keys": "distance", "key_by_type": True} mock_strava_api.get( "/activities/{id}/streams", json={ "distance": { "data": [1.0, 2.0, 3.0], "series_type": "distance", "original_size": 9, "resolution": "high", } }, match=[matchers.query_param_matcher(query_params)], ) streams = client.get_activity_streams(42, types=["distance"]) assert streams["distance"].data == [1.0, 2.0, 3.0] def test_get_effort_streams(mock_strava_api, client): query_params = {"keys": "distance", "key_by_type": True} mock_strava_api.get( "/segment_efforts/{id}/streams", json={ "distance": { "data": [1.0, 2.0, 3.0], "series_type": "distance", "original_size": 9, "resolution": "high", } }, match=[matchers.query_param_matcher(query_params)], ) streams = client.get_effort_streams(42, types=["distance"]) assert streams["distance"].data == [1.0, 2.0, 3.0] def test_get_segment_streams(mock_strava_api, client): query_params = {"keys": "distance", "key_by_type": True} mock_strava_api.get( "/segments/{id}/streams", json={ "distance": { "data": [1.0, 2.0, 3.0], "series_type": "distance", "original_size": 9, "resolution": "high", } }, match=[matchers.query_param_matcher(query_params)], ) streams = client.get_segment_streams(42, types=["distance"]) assert streams["distance"].data == [1.0, 2.0, 3.0] def test_get_activity_streams_no_type_specified(mock_strava_api, client): query_params = { "keys": "time,distance,latlng,altitude,velocity_smooth,heartrate,cadence,watts,temp,moving,grade_smooth", "key_by_type": True, } mock_strava_api.get( "/activities/{id}/streams", json={ "distance": { "data": [1.0, 2.0, 3.0], "original_size": 9, "resolution": "high", } }, match=[matchers.query_param_matcher(query_params)], ) streams = client.get_activity_streams(42) assert streams["distance"].data == [1.0, 2.0, 3.0] def test_get_activity_streams_invalid_type(mock_strava_api, client): with pytest.raises(ValueError): streams = client.get_activity_streams( 42, types=["distance", "hhjj", "npt"] ) def test_get_activity_streams_resolution_unofficial(mock_strava_api, client): mock_strava_api.get( "/activities/{id}/streams", json={ "distance": { "data": [1.0, 2.0, 3.0], "series_type": "distance", "original_size": 9, "resolution": "high", } }, ) with pytest.warns(FutureWarning): streams = client.get_activity_streams( 42, types=["distance"], resolution="high" ) def test_get_activity_streams_series_type_unofficial(mock_strava_api, client): mock_strava_api.get( "/activities/{id}/streams", json={ "distance": { "data": [1.0, 2.0, 3.0], "series_type": "distance", "original_size": 9, "resolution": "high", } }, ) with pytest.warns(FutureWarning): streams = client.get_activity_streams( 42, types=["distance"], series_type="distance" ) @pytest.mark.parametrize( "update_kwargs,expected_params,expected_warning,expected_exception", ( ({}, {}, None, None), ({"activity_type": "Run"}, {"type": "run"}, DeprecationWarning, None), ({"sport_type": "TrailRun"}, {"sport_type": "TrailRun"}, None, None), ( {"activity_type": "Run", "sport_type": "TrailRun"}, {"sport_type": "TrailRun"}, None, None, ), ({"private": True}, {"private": "1"}, DeprecationWarning, None), ({"commute": True}, {"commute": "1"}, None, None), ({"trainer": True}, {"trainer": "1"}, None, None), ({"gear_id": "fb42"}, {"gear_id": "fb42"}, None, None), ({"description": "foo"}, {"description": "foo"}, None, None), ( {"device_name": "foo"}, {"device_name": "foo"}, DeprecationWarning, None, ), ({"hide_from_home": False}, {"hide_from_home": "0"}, None, None), ( {"name": "My awesome activity"}, {"name": "My awesome activity"}, None, None, ), ), ) def test_update_activity( mock_strava_api, client, update_kwargs, expected_params, expected_warning, expected_exception, ): activity_id = 42 def _call_update_activity(): _ = client.update_activity(activity_id, **update_kwargs) assert mock_strava_api.calls[-1].request.params == expected_params if expected_exception: with pytest.raises(expected_exception): _call_update_activity() else: mock_strava_api.put("/activities/{id}", status=200) if expected_warning: with pytest.warns(expected_warning): _call_update_activity() else: _call_update_activity() @pytest.mark.parametrize( "activity_file_type,data_type,upload_kwargs,expected_params,expected_warning,expected_exception", ( ("file", "tcx", {}, {"data_type": "tcx"}, None, None), ("str", "tcx", {}, {"data_type": "tcx"}, None, None), ("bytes", "tcx", {}, {"data_type": "tcx"}, None, None), ("not_supported", "tcx", {}, {}, None, TypeError), ("file", "invalid", {}, {}, None, ValueError), ( "file", "tcx", {"name": "name"}, {"data_type": "tcx", "name": "name"}, None, None, ), ( "file", "tcx", {"description": "descr"}, {"data_type": "tcx", "description": "descr"}, None, None, ), ( "file", "tcx", {"activity_type": "run"}, {"data_type": "tcx", "activity_type": "run"}, FutureWarning, None, ), ( "file", "tcx", {"activity_type": "Run"}, {"data_type": "tcx", "activity_type": "run"}, FutureWarning, None, ), ("file", "tcx", {"activity_type": "sleep"}, None, None, ValueError), ( "file", "tcx", {"private": True}, {"data_type": "tcx", "private": "1"}, DeprecationWarning, None, ), ( "file", "tcx", {"external_id": 42}, {"data_type": "tcx", "external_id": "42"}, None, None, ), ( "file", "tcx", {"trainer": True}, {"data_type": "tcx", "trainer": "1"}, None, None, ), ( "file", "tcx", {"commute": False}, {"data_type": "tcx", "commute": "0"}, None, None, ), ), ) def test_upload_activity( mock_strava_api, client, activity_file_type, data_type, upload_kwargs, expected_params, expected_warning, expected_exception, ): init_upload_response = { "id": 1, "id_str": "abc", "external_id": "abc", "status": "default_status", "error": "", } def _call_and_assert(file): _ = client.upload_activity(file, data_type, **upload_kwargs) assert mock_strava_api.calls[-1].request.params == expected_params def _call_upload(file): if expected_exception: with pytest.raises(expected_exception): _call_and_assert(file) else: mock_strava_api.post( "/uploads", status=201, json=init_upload_response ) if expected_warning: with pytest.warns(expected_warning): _call_and_assert(file) else: _call_and_assert(file) with open(os.path.join(RESOURCES_DIR, "sample.tcx")) as f: if activity_file_type == "file": _call_upload(f) elif activity_file_type == "str": _call_upload(f.read()) elif activity_file_type == "bytes": _call_upload(f.read().encode("utf-8")) else: _call_upload({}) @pytest.mark.parametrize( "update_kwargs,expected_params,expected_warning,expected_exception", ( ({}, {}, None, None), ({"city": "foo"}, {"city": "foo"}, DeprecationWarning, None), ({"state": "foo"}, {"state": "foo"}, DeprecationWarning, None), ({"country": "foo"}, {"country": "foo"}, DeprecationWarning, None), ({"sex": "foo"}, {"sex": "foo"}, DeprecationWarning, None), ({"weight": "foo"}, {}, None, ValueError), ({"weight": "99.9"}, {"weight": "99.9"}, None, None), ({"weight": 99.9}, {"weight": "99.9"}, None, None), ({"weight": 99}, {"weight": "99.0"}, None, None), ), ) def test_update_athlete( mock_strava_api, client, update_kwargs, expected_params, expected_warning, expected_exception, ): def _call_and_assert(): _ = client.update_athlete(**update_kwargs) assert mock_strava_api.calls[-1].request.params == expected_params if expected_exception: with pytest.raises(expected_exception): _call_and_assert() else: mock_strava_api.put("/athlete", status=200) if expected_warning: with pytest.warns(expected_warning): _call_and_assert() else: _call_and_assert() @pytest.mark.parametrize( "extra_create_kwargs,extra_expected_params,expected_exception", ( ( { "sport_type": "TrailRun", "start_date_local": datetime.datetime(2022, 1, 1, 10, 0, 0), }, { "sport_type": "TrailRun", "start_date_local": "2022-01-01T10:00:00Z", }, None, ), ( { "sport_type": "TrailRun", "elapsed_time": datetime.timedelta(minutes=1), }, {"sport_type": "TrailRun", "elapsed_time": "60"}, None, ), ( {"sport_type": "TrailRun", "distance": 1000}, {"sport_type": "TrailRun", "distance": "1000"}, None, ), ( {"sport_type": "TrailRun", "distance": miles(1)}, {"sport_type": "TrailRun", "distance": "1609.344"}, None, ), ( {"sport_type": "TrailRun", "description": "foo"}, {"sport_type": "TrailRun", "description": "foo"}, None, ), ( {"description": "foo"}, {"description": "foo"}, ValueError, ), ), ) def test_create_activity( default_call_kwargs, default_request_params, mock_strava_api, client, extra_create_kwargs, extra_expected_params, expected_exception, ): """Test what happens when create activity receives valid and invalid sport type values and also what happens when a required API item is missing.""" call_kwargs = default_call_kwargs | extra_create_kwargs expected_params = default_request_params | extra_expected_params def _call_and_assert(): _ = client.create_activity(**call_kwargs) assert mock_strava_api.calls[-1].request.params == expected_params if expected_exception: with pytest.raises(expected_exception): _call_and_assert() else: mock_strava_api.post("/activities", status=201) _call_and_assert() def test_activity_uploader(mock_strava_api, client): test_activity_id = 42 init_upload_response = { "id": 1, "id_str": "abc", "external_id": "abc", "status": "default_status", "error": "", } mock_strava_api.post("/uploads", status=201, json=init_upload_response) mock_strava_api.get("/uploads/{uploadId}", json=init_upload_response) mock_strava_api.get("/uploads/{uploadId}", json=init_upload_response) mock_strava_api.get( "/uploads/{uploadId}", json={**init_upload_response, "activity_id": test_activity_id}, ) mock_strava_api.get( "/activities/{id}", response_update={"id": test_activity_id} ) with open(os.path.join(RESOURCES_DIR, "sample.tcx")) as activity_file: uploader = client.upload_activity(activity_file, data_type="tcx") assert uploader.is_processing activity = uploader.wait() assert uploader.is_complete assert activity.id == test_activity_id def test_get_route(mock_strava_api, client): with open( os.path.join(RESOURCES_DIR, "example_route_response.json"), "r" ) as route_response_fp: route_response = json.load(route_response_fp) mock_strava_api.get("/routes/{id}", status=200, json=route_response) route = client.get_route(42) assert route.name == "15k, no traffic" @responses.activate def test_create_subscription(mock_strava_api, client): responses.post( "https://www.strava.com/api/v3/push_subscriptions", json={ "application_id": 42, "object_type": "activity", "aspect_type": "create", "callback_url": "https://foobar.com", "created_at": 1674660406, }, status=200, ) created_subscription = client.create_subscription( 42, 42, "https://foobar.com" ) assert created_subscription.application_id == 42 @pytest.mark.parametrize( "raw,expected_verify_token,expected_response,expected_exception", ( ( {"hub.verify_token": "a", "hub.challenge": "b"}, "a", {"hub.challenge": "b"}, None, ), ( {"hub.verify_token": "foo", "hub.challenge": "b"}, "a", None, AssertionError, ), ), ) def test_handle_subscription_callback( client, raw, expected_verify_token, expected_response, expected_exception ): if expected_exception: with pytest.raises(expected_exception): client.handle_subscription_callback(raw, expected_verify_token) else: assert ( client.handle_subscription_callback(raw, expected_verify_token) == expected_response ) @pytest.mark.parametrize( "limit,n_raw_results,expected_n_segments", ( (None, 0, 0), (None, 10, 10), (10, 10, 10), (10, 20, 10), (10, 1, 1), (10, 0, 0), ), ) def test_get_starred_segments( mock_strava_api, client, limit, n_raw_results, expected_n_segments ): mock_strava_api.get( "/segments/starred", response_update={"name": "test_segment"}, n_results=n_raw_results, ) kwargs = {"limit": limit} if limit is not None else {} segment_list = list(client.get_starred_segments(**kwargs)) assert len(segment_list) == expected_n_segments if expected_n_segments > 0: assert isinstance(segment_list[0], SummarySegment) assert segment_list[0].name == "test_segment" def test_get_club(mock_strava_api, client): mock_strava_api.get("/clubs/{id}", response_update={"name": "foo"}) club = client.get_club(42) assert club.name == "foo" @pytest.mark.parametrize("n_clubs", (0, 2)) def test_get_athlete_clubs_iterator(mock_strava_api, client, n_clubs): """Test that athlete clubs returns the correct number of clubs""" # Create a mock instance with the club name update to my club mock_strava_api.get( "/athlete/clubs", response_update={"name": "myclub"}, n_results=n_clubs ) # Convert iterator to list clubs = list(client.get_athlete_clubs()) assert len(clubs) == n_clubs if clubs: assert clubs[0].name == "myclub" @pytest.mark.parametrize("n_members", (0, 2)) def test_get_club_members(mock_strava_api, client, n_members): mock_strava_api.get( "/clubs/{id}/members", response_update={"lastname": "Doe"}, n_results=n_members, ) members = list(client.get_club_members(42)) assert len(members) == n_members if members: assert members[0].lastname == "Doe" @pytest.mark.parametrize( "athlete_id,authenticated_athlete,expected_biggest_ride_distance,expected_exception", ( (42, True, 1000, None), (42, False, None, AccessUnauthorized), (None, True, 1000, None), ), ) def test_get_athlete_stats( mock_strava_api, client, athlete_id, authenticated_athlete, expected_biggest_ride_distance, expected_exception, ): if athlete_id is None: mock_strava_api.get("/athlete", response_update={"id": 42}) if authenticated_athlete: mock_strava_api.get( "/athletes/{id}/stats", response_update={ "biggest_ride_distance": expected_biggest_ride_distance }, ) else: mock_strava_api.get( "/athletes/{id}/stats", json=[ { "resource": "Athlete", "field": "access_token", "code": "invalid", } ], status=401, ) if expected_exception: with pytest.raises(expected_exception): client.get_athlete_stats(athlete_id) else: stats = client.get_athlete_stats(athlete_id) assert stats.biggest_ride_distance == expected_biggest_ride_distance def test_get_gear(mock_strava_api, client): mock_strava_api.get("/gear/{id}", response_update={"name": "foo_bike"}) assert client.get_gear(42).name == "foo_bike" @pytest.mark.parametrize( "limit,n_raw_results,expected_n_activities", ( (None, 10, 10), (None, 0, 0), (10, 10, 10), (10, 20, 10), (10, 1, 1), (10, 0, 0), ), ) def test_get_activities( mock_strava_api, client, limit, n_raw_results, expected_n_activities ): mock_strava_api.get( "/athlete/activities", response_update={"name": "test_activity"}, n_results=n_raw_results, ) kwargs = {"limit": limit} if limit is not None else {} activity_list = list(client.get_activities(**kwargs)) assert len(activity_list) == expected_n_activities if expected_n_activities > 0: assert isinstance(activity_list[0], SummaryActivity) assert activity_list[0].name == "test_activity" def test_get_activities_quantity_addition(mock_strava_api, client): mock_strava_api.get( "/athlete/activities", response_update={"distance": 1000.0}, n_results=2, ) act_list = list(client.get_activities(limit=2)) total_d = 0 total_d += act_list[0].distance total_d += act_list[1].distance assert total_d == 2000.0 def test_get_segment(mock_strava_api, client): mock_strava_api.get("/segments/{id}", response_update={"name": "foo"}) segment = client.get_segment(42) assert segment.name == "foo" def test_get_segment_effort(mock_strava_api, client): mock_strava_api.get( "/segment_efforts/{id}", response_update={"max_heartrate": 170} ) effort = client.get_segment_effort(42) assert effort.max_heartrate == 170 @pytest.mark.parametrize( "params, warning_type, warning_message", [ ( {"segment_id": 2345, "athlete_id": 12345}, DeprecationWarning, 'The "athlete_id" parameter is unsupported', ), ( {"segment_id": 2345, "limit": 10}, DeprecationWarning, 'The "limit" parameter is deprecated', ), ], ) def test_get_segment_efforts_warnings( params, warning_type, warning_message, mock_strava_api, client ): """Test that if provided with deprecated params, the user receives a warning.""" mock_strava_api.get( "/segment_efforts", response_update={"name": "Best Run Ever"}, n_results=1, ) with pytest.warns(warning_type, match=warning_message): a = client.get_segment_efforts(**params) next(a) def test_get_segment_efforts(client, mock_strava_api): """Test that endpoint returns data as expected.""" mock_strava_api.get("/segment_efforts", n_results=4) efforts = list( client.get_segment_efforts(segment_id=2345, athlete_id=12345) ) assert len(efforts) == 4 assert efforts[0].name == "Alpe d'Huez" def test_get_activities_paged(mock_strava_api, client): for i in range(1, 4): params = {"page": i, "per_page": 200} mock_strava_api.get( "/athlete/activities", response_update={"id": i}, n_results=(200 if i < 3 else 100), match=[matchers.query_param_matcher(params)], ) activity_list = list(client.get_activities()) assert len(activity_list) == 500 assert activity_list[0].id == 1 assert activity_list[400].id == 3 @responses.activate def test_upload_activity_photo_works(client): """ Test uploading an activity with a photo. """ strava_pre_signed_uri = "https://strava-photo-uploads-prod.s3-accelerate.amazonaws.com/12345.jpg" photo_bytes = b"photo_data" photo_metadata_header = { "Content-Type": "image/jpeg", "Expect": "100-continue", "Host": "strava-photo-uploads-prod.s3-accelerate.amazonaws.com", } activity_upload_response = { "id": 12345, "external_id": "external_id", "error": None, "status": "Your activity is ready.", "activity_id": 12345, "photo_metadata": [ { "uri": strava_pre_signed_uri, "header": photo_metadata_header, "method": "PUT", "max_size": 1600, } ], } with responses.RequestsMock( assert_all_requests_are_fired=True ) as _responses: _responses.add( responses.PUT, "https://strava-photo-uploads-prod.s3-accelerate.amazonaws.com/12345.jpg", status=200, ) _responses.add( responses.GET, "https://www.strava.com/api/v3/uploads/12345", status=200, json=activity_upload_response, ) activity_uploader = ActivityUploader( client, response=activity_upload_response ) activity_uploader.upload_photo(photo=photo_bytes) def test_upload_activity_photo_fail_type_error(client): activity_uploader = ActivityUploader(client, response={}) with pytest.raises(ActivityPhotoUploadFailed) as error: activity_uploader.upload_photo(photo="photo_str") assert str(error.value) == "Photo must be bytes type" @mock.patch("stravalib.client.ActivityUploader.poll") def test_upload_activity_photo_fail_activity_upload_not_complete(client): activity_upload_response = { "id": 1234578, "external_id": "external_id", "error": None, "status": "Your activity is being processed.", } activity_uploader = ActivityUploader( client, response=activity_upload_response ) with pytest.raises(ActivityPhotoUploadFailed) as error: activity_uploader.upload_photo(photo=b"photo_bytes") assert str(error.value) == "Activity upload not complete" @pytest.mark.parametrize("photo_metadata", (None, [], [{}])) @mock.patch("stravalib.client.ActivityUploader.poll") def test_upload_activity_photo_fail_not_supported(client, photo_metadata): activity_upload_response = { "id": 1234578, "external_id": "external_id", "error": None, "status": "Your activity is ready.", "activity_id": 1234578, "photo_metadata": photo_metadata, } activity_uploader = ActivityUploader( client, response=activity_upload_response ) with pytest.raises(ActivityPhotoUploadFailed) as error: activity_uploader.upload_photo(photo=b"photo_bytes") assert str(error.value) == "Photo upload not supported" def test_get_activity_comments(mock_strava_api, client): mock_strava_api.get( "/activities/{id}/comments", response_update={"text": "foo"}, n_results=2, ) comment_list = list( client.get_activity_comments(42) ) # no idea what the markdown param is supposed to do assert len(comment_list) == 2 assert comment_list[0].text == "foo" def test_explore_segments(mock_strava_api, client): # TODO parameterize test with multiple inputs # It is hard to patch the response for this one, since the # endpoint returns a nested list of segments. mock_strava_api.get("/segments/explore") segment_list = client.explore_segments((1, 2, 3, 4)) assert len(segment_list) == 1 assert segment_list[0].name == "Hawk Hill" def test_get_activity_kudos(mock_strava_api, client): mock_strava_api.get( "/activities/{id}/kudos", response_update={"lastname": "Doe"}, n_results=2, ) kudoer_list = list(client.get_activity_kudos(42)) assert isinstance(kudoer_list[0], SummaryAthlete) assert len(kudoer_list) == 2 assert kudoer_list[0].lastname == "Doe" @patch("stravalib.protocol.ApiV3.exchange_code_for_token") def test_exchange_code_for_token_athlete( mock_protocol_exchange_code, client, raw_exchange_response ): """If a user requests athlete data, then it should be returned as a SummaryAthlete object.""" mock_protocol_exchange_code.return_value = raw_exchange_response access_info, summary_athlete = client.exchange_code_for_token( client_id=123, client_secret="secret", code="temp_code", return_athlete=True, ) assert access_info["access_token"] == "123456" assert summary_athlete.firstname == "Foo" @patch("stravalib.protocol.ApiV3.exchange_code_for_token") def test_exchange_code_for_token_missing_athlete( mock_protocol_exchange_code, client, raw_exchange_response ): """If athlete=True but Strava modifies the api response and athlete return is None client should only return AccessInfo object""" mock_protocol_exchange_code.return_value = (raw_exchange_response[0], None) access_info, athlete = client.exchange_code_for_token( client_id=123, client_secret="secret", code="temp_code", return_athlete=True, ) assert athlete is None assert access_info["access_token"] == "123456" @patch("stravalib.protocol.ApiV3.exchange_code_for_token") def test_exchange_code_for_token_no_athlete( mock_protocol_exchange_code, client, raw_exchange_response ): """When athlete isn't True, only return AccessInfo Typed Dict.""" mock_protocol_exchange_code.return_value = raw_exchange_response access_info = client.exchange_code_for_token( client_id=123, client_secret="secret", code="temp_code", return_athlete=False, ) assert ( access_info["access_token"] == raw_exchange_response[0]["access_token"] ) assert isinstance(access_info, dict) stravalib-2.2/src/stravalib/tests/resources/000077500000000000000000000000001475174155400213135ustar00rootroot00000000000000stravalib-2.2/src/stravalib/tests/resources/activity-manual.3.json000066400000000000000000000025151475174155400254610ustar00rootroot00000000000000{ "achievement_count": 0, "athlete": { "id": 1513, "resource_state": 1 }, "athlete_count": 1, "average_speed": 0.5821912144702842, "calories": 2084.438266172827, "comment_count": 0, "commute": false, "distance": 22530.8, "elapsed_time": 38700, "end_latlng": null, "external_id": null, "flagged": false, "gear_id": "g69911", "guid": "7143b325a3e3d6b8f7fc207952a1d03a023b4b9d", "has_kudoed": false, "id": 96089609, "kudos_count": 0, "location_city": "El Dorado County, CA, USA", "location_state": "CA", "manual": true, "map": { "id": "a96089609", "polyline": null, "resource_state": 3, "summary_polyline": null }, "max_speed": 0.0, "moving_time": 38700, "name": "Mt. Tallac Summit Sunday", "photo_count": 0, "total_photo_count": 0, "private": false, "resource_state": 3, "segment_efforts": [], "start_date": "2013-11-17T16:00:00Z", "start_date_local": "2013-11-17T08:00:00Z", "start_latitude": 38.74263759999999, "start_latlng": [ 38.74263759999999, -120.4357631 ], "start_longitude": -120.4357631, "timezone": "(GMT-08:00) America/Los_Angeles", "total_elevation_gain": 0, "trainer": false, "type": "Hike", "upload_id": null } stravalib-2.2/src/stravalib/tests/resources/activity.3.json000066400000000000000000000565231475174155400242160ustar00rootroot00000000000000{ "achievement_count": 9, "athlete": { "id": 1513, "resource_state": 1 }, "athlete_count": 2, "average_speed": 2.0, "best_efforts": [ { "activity": { "id": 99895560 }, "athlete": { "id": 1513 }, "distance": 400, "elapsed_time": 134, "end_index": 204, "id": 464733311, "kom_rank": null, "moving_time": 136, "name": "400m", "pr_rank": null, "resource_state": 2, "segment": null, "start_date": "2013-12-12T19:40:29Z", "start_date_local": "2013-12-12T11:40:29Z", "start_index": 112 }, { "activity": { "id": 99895560 }, "athlete": { "id": 1513 }, "distance": 805, "elapsed_time": 279, "end_index": 299, "id": 464733312, "kom_rank": null, "moving_time": 281, "name": "1/2 mile", "pr_rank": 3, "resource_state": 2, "segment": null, "start_date": "2013-12-12T19:40:29Z", "start_date_local": "2013-12-12T11:40:29Z", "start_index": 112 }, { "activity": { "id": 99895560 }, "athlete": { "id": 1513 }, "distance": 1000, "elapsed_time": 355, "end_index": 340, "id": 464733313, "kom_rank": null, "moving_time": 356, "name": "1k", "pr_rank": 2, "resource_state": 2, "segment": null, "start_date": "2013-12-12T19:40:30Z", "start_date_local": "2013-12-12T11:40:30Z", "start_index": 113 }, { "activity": { "id": 99895560 }, "athlete": { "id": 1513 }, "distance": 1609, "elapsed_time": 656, "end_index": 478, "id": 464733314, "kom_rank": null, "moving_time": 602, "name": "1 mile", "pr_rank": 3, "resource_state": 2, "segment": null, "start_date": "2013-12-12T19:40:30Z", "start_date_local": "2013-12-12T11:40:30Z", "start_index": 113 }, { "activity": { "id": 99895560 }, "athlete": { "id": 1513 }, "distance": 3219, "elapsed_time": 1579, "end_index": 855, "id": 464733315, "kom_rank": null, "moving_time": 1382, "name": "2 miles", "pr_rank": 3, "resource_state": 2, "segment": null, "start_date": "2013-12-12T19:40:29Z", "start_date_local": "2013-12-12T11:40:29Z", "start_index": 112 }, { "activity": { "id": 99895560 }, "athlete": { "id": 1513 }, "distance": 5000, "elapsed_time": 2507, "end_index": 1158, "id": 464733316, "kom_rank": null, "moving_time": 2178, "name": "5k", "pr_rank": 2, "resource_state": 2, "segment": null, "start_date": "2013-12-12T19:36:41Z", "start_date_local": "2013-12-12T11:36:41Z", "start_index": 0 } ], "calories": 537.9, "comment_count": 1, "commute": false, "description": null, "distance": 5781.1, "elapsed_time": 3140, "end_latlng": [ 37.780877, -122.39548 ], "external_id": "620D5DDD-B836-4F6B-8760-F905B8C2895F", "flagged": false, "gear_id": "g69911", "guid": "620D5DDD-B836-4F6B-8760-F905B8C2895F", "has_kudoed": false, "id": 99895560, "kudos_count": 12, "location_city": "San Francisco", "location_state": "CA", "manual": false, "map": { "id": "a99895560", "polyline": "ubreFbk`jVTFJCLI^u@`@k@~@aAHUZg@b@c@P]hAyATa@d@e@PYh@_@p@cABGIMd@W\\[HOAK[a@@GPOl@u@PQXc@NO^o@V_AH[Be@CcAMm@Yk@I]EGCSWs@Oq@Ig@Wm@EYSeAOOIUMMICW@iAHOEW@OCoBLi@JS@_@C]BYFk@A]@UB[?_@Jg@X{@H[Dw@Cs@FMASIG?SBaBS{@@UI]@k@Gk@AQE[Cc@?SCS@s@A[Gi@?a@SDF?DiAOYKIFIG@NADWISA_BAcAD[IDAc@Fc@AOFO?[D[As@BSD[?a@Lq@JWNML[LYPo@n@]VqAl@ODgAj@CD{@Vm@^]d@UTMTi@t@QNQXQPKVg@d@o@z@[ZeA|AWVk@v@[\\ULGJQFU`@SHY`@UFWRAFa@NCBGf@QRc@PQr@KFK@SPHQJBLEDMBEF?BCHSFCHOf@[JSDA^]JO@GZ]D@BSFQ\\KBE?IDCR?VSDG@Of@c@?EHKFKXSNW\\YBM^Y?GNMBMh@m@f@{@RQLWdAmAf@i@PKHQRSLYVKVYPM\\GZONANMLG^Ih@a@j@M`@YJMLYf@_@D@PCXBX_@TKHMPBNAVEPIb@DZCJDRCNDNCNBVOXAVDPENAx@Jv@Bp@Jz@@z@LfAELDXAr@PDCECHZ@RIZARBXFPV\\BJ\\NVd@HBPL@NLPHDDLVHV`@DZBFPJFTLRDBJEBEXC|@j@NDJXPPBLLLNXVp@B^Lr@@b@J\\n@h@DBJ?DD?JOf@W`@CRKPGFQFa@XINERDXBQBDPARGVED@RRBFEA?PD?@DDGFAD@DDL@VJVTZ\\DJLDDL?LBFf@x@RTNHDt@Xh@PH@DQp@KRA^YlA[b@By@e@GEDLPv@^OBXCTGJG^q@AGIEYIGP", "resource_state": 3, "summary_polyline": "oyqeFz``jVvGsItDsIiEwQsXbBaVcByQz@wLjHkWzYzYw[vLcGbGg@hOz@?jClFjHvBf@hErIyBnFjCR" }, "max_speed": 8.3, "moving_time": 2892, "name": "Lunch Rover Shuffle-Walk-Yog with Todd", "photo_count": 0, "total_photo_count": 0, "private": false, "resource_state": 3, "segment_efforts": [ { "activity": { "id": 99895560 }, "athlete": { "id": 1513 }, "distance": 172.8, "elapsed_time": 108, "end_index": 113, "id": 2137250436, "kom_rank": null, "moving_time": 61, "name": "Sprint to catch light", "pr_rank": null, "resource_state": 2, "segment": { "activity_type": "Run", "average_grade": -1.8, "city": "San Francisco", "climb_category": 0, "distance": 124.35, "elevation_high": 6.8, "elevation_low": 4.5, "end_latitude": 37.7782023511827, "end_latlng": [ 37.7782023511827, -122.39171963185072 ], "end_longitude": -122.39171963185072, "id": 3866093, "maximum_grade": -0.0, "name": "Sprint to catch light", "pr_time": 108, "private": false, "resource_state": 2, "start_latitude": 37.778987400233746, "start_latlng": [ 37.778987400233746, -122.39271523430943 ], "start_longitude": -122.39271523430943, "state": "CA" }, "start_date": "2013-12-12T19:38:42Z", "start_date_local": "2013-12-12T11:38:42Z", "start_index": 76 }, { "activity": { "id": 99895560 }, "athlete": { "id": 1513 }, "distance": 773.2, "elapsed_time": 273, "end_index": 327, "id": 2137250444, "kom_rank": null, "moving_time": 273, "name": "AT&T 800m Dash", "pr_rank": 1, "resource_state": 2, "segment": { "activity_type": "Run", "average_grade": -0.0, "city": "San Francisco", "climb_category": 0, "distance": 785.3, "elevation_high": 4.1, "elevation_low": 0.0, "end_latitude": 37.782742, "end_latlng": [ 37.782742, -122.387922 ], "end_longitude": -122.387922, "id": 5666311, "maximum_grade": 18.1, "name": "AT&T 800m Dash", "pr_time": 273, "private": false, "resource_state": 2, "start_latitude": 37.777171, "start_latlng": [ 37.777171, -122.390329 ], "start_longitude": -122.390329, "state": "CA" }, "start_date": "2013-12-12T19:41:27Z", "start_date_local": "2013-12-12T11:41:27Z", "start_index": 151 }, { "activity": { "id": 99895560 }, "athlete": { "id": 1513 }, "distance": 257.5, "elapsed_time": 88, "end_index": 211, "id": 2137250440, "kom_rank": null, "moving_time": 88, "name": "AT&T Park Backside", "pr_rank": 1, "resource_state": 2, "segment": { "activity_type": "Run", "average_grade": 0.0, "city": "San Francisco", "climb_category": 0, "distance": 249.397, "elevation_high": 6.2, "elevation_low": 6.2, "end_latitude": 37.778352638706565, "end_latlng": [ 37.778352638706565, -122.38767142407596 ], "end_longitude": -122.38767142407596, "id": 1808371, "maximum_grade": 0.0, "name": "AT&T Park Backside", "pr_time": 88, "private": false, "resource_state": 2, "start_latitude": 37.777267936617136, "start_latlng": [ 37.777267936617136, -122.39012941718102 ], "start_longitude": -122.39012941718102, "state": "CA" }, "start_date": "2013-12-12T19:41:31Z", "start_date_local": "2013-12-12T11:41:31Z", "start_index": 155 }, { "activity": { "id": 99895560 }, "athlete": { "id": 1513 }, "distance": 1768.4, "elapsed_time": 741, "end_index": 562, "id": 2137250448, "kom_rank": null, "moving_time": 689, "name": "Mile By the Bay ", "pr_rank": null, "resource_state": 2, "segment": { "activity_type": "Run", "average_grade": -0.0, "city": "San Francisco", "climb_category": 0, "distance": 1689.36, "elevation_high": 4.2, "elevation_low": 3.0, "end_latitude": 37.79156947508454, "end_latlng": [ 37.79156947508454, -122.38967721350491 ], "end_longitude": -122.38967721350491, "id": 2552268, "maximum_grade": 0.6, "name": "Mile By the Bay ", "pr_time": 741, "private": false, "resource_state": 2, "start_latitude": 37.777588460594416, "start_latlng": [ 37.777588460594416, -122.38948199898005 ], "start_longitude": -122.38948199898005, "state": "CA" }, "start_date": "2013-12-12T19:41:52Z", "start_date_local": "2013-12-12T11:41:52Z", "start_index": 172 }, { "activity": { "id": 99895560 }, "athlete": { "id": 1513 }, "distance": 322.5, "elapsed_time": 115, "end_index": 288, "id": 2137250442, "kom_rank": null, "moving_time": 115, "name": "Pier 42 to Pier 40 Beer Dash", "pr_rank": 1, "resource_state": 2, "segment": { "activity_type": "Run", "average_grade": 0.2, "city": "San Francisco", "climb_category": 0, "distance": 318.09, "elevation_high": 4.4, "elevation_low": 3.5, "end_latitude": 37.781130066141486, "end_latlng": [ 37.781130066141486, -122.38788725808263 ], "end_longitude": -122.38788725808263, "id": 3394958, "maximum_grade": 0.9, "name": "Pier 42 to Pier 40 Beer Dash", "pr_time": 115, "private": false, "resource_state": 2, "start_latitude": 37.7783128246665, "start_latlng": [ 37.7783128246665, -122.38754871301353 ], "start_longitude": -122.38754871301353, "state": "CA" }, "start_date": "2013-12-12T19:42:59Z", "start_date_local": "2013-12-12T11:42:59Z", "start_index": 211 }, { "activity": { "id": 99895560 }, "athlete": { "id": 1513 }, "distance": 1678.8, "elapsed_time": 790, "end_index": 672, "id": 2137250456, "kom_rank": null, "moving_time": 714, "name": "AT&T Park to Ferry Building Scenic Mile", "pr_rank": null, "resource_state": 2, "segment": { "activity_type": "Run", "average_grade": -0.0, "city": "San Francisco", "climb_category": 0, "distance": 1596.4, "elevation_high": 3.7, "elevation_low": 2.8, "end_latitude": 37.7944156, "end_latlng": [ 37.7944156, -122.3931068 ], "end_longitude": -122.3931068, "id": 825637, "maximum_grade": 0.7, "name": "AT&T Park to Ferry Building Scenic Mile", "pr_time": 790, "private": false, "resource_state": 2, "start_latitude": 37.7814597, "start_latlng": [ 37.7814597, -122.3882274 ], "start_longitude": -122.3882274, "state": "CA" }, "start_date": "2013-12-12T19:45:08Z", "start_date_local": "2013-12-12T11:45:08Z", "start_index": 298 }, { "activity": { "id": 99895560 }, "athlete": { "id": 1513 }, "distance": 1755.4, "elapsed_time": 861, "end_index": 695, "id": 2137250458, "kom_rank": null, "moving_time": 781, "name": "south beach speed mile", "pr_rank": null, "resource_state": 2, "segment": { "activity_type": "Run", "average_grade": -0.0, "city": "San Francisco", "climb_category": 0, "distance": 1657.4, "elevation_high": 3.9, "elevation_low": 2.4, "end_latitude": 37.795105, "end_latlng": [ 37.795105, -122.393717 ], "end_longitude": -122.393717, "id": 4878224, "maximum_grade": 0.8, "name": "south beach speed mile", "pr_time": 861, "private": false, "resource_state": 2, "start_latitude": 37.78152, "start_latlng": [ 37.78152, -122.388018 ], "start_longitude": -122.388018, "state": "CA" }, "start_date": "2013-12-12T19:45:10Z", "start_date_local": "2013-12-12T11:45:10Z", "start_index": 299 }, { "activity": { "id": 99895560 }, "athlete": { "id": 1513 }, "distance": 1628.4, "elapsed_time": 770, "end_index": 672, "id": 2137250457, "kom_rank": null, "moving_time": 694, "name": "Townsend Mile", "pr_rank": null, "resource_state": 2, "segment": { "activity_type": "Run", "average_grade": 0.3, "city": "San Francisco", "climb_category": 0, "distance": 1600.1, "elevation_high": 4.2, "elevation_low": -0.4, "end_latitude": 37.794483, "end_latlng": [ 37.794483, -122.393248 ], "end_longitude": -122.393248, "id": 4850732, "maximum_grade": 21.2, "name": "Townsend Mile", "pr_time": 770, "private": false, "resource_state": 2, "start_latitude": 37.781929, "start_latlng": [ 37.781929, -122.388087 ], "start_longitude": -122.388087, "state": "CA" }, "start_date": "2013-12-12T19:45:28Z", "start_date_local": "2013-12-12T11:45:28Z", "start_index": 308 }, { "activity": { "id": 99895560 }, "athlete": { "id": 1513 }, "distance": 286.2, "elapsed_time": 160, "end_index": 665, "id": 2137250453, "kom_rank": null, "moving_time": 136, "name": "Gotta Catch That Ferry Home!", "pr_rank": null, "resource_state": 2, "segment": { "activity_type": "Run", "average_grade": 0.1, "city": "San Francisco", "climb_category": 0, "distance": 331.264, "elevation_high": 3.2, "elevation_low": 2.5, "end_latitude": 37.79453448954764, "end_latlng": [ 37.79453448954764, -122.3928627559192 ], "end_longitude": -122.3928627559192, "id": 790791, "maximum_grade": 1.6, "name": "Gotta Catch That Ferry Home!", "pr_time": 160, "private": false, "resource_state": 2, "start_latitude": 37.79226324524524, "start_latlng": [ 37.79226324524524, -122.3912170530505 ], "start_longitude": -122.3912170530505, "state": "CA" }, "start_date": "2013-12-12T19:55:23Z", "start_date_local": "2013-12-12T11:55:23Z", "start_index": 597 }, { "activity": { "id": 99895560 }, "athlete": { "id": 1513 }, "distance": 552.3, "elapsed_time": 245, "end_index": 1014, "id": 2137250459, "kom_rank": null, "moving_time": 245, "name": "Harrison to Brannan on Embarcadero", "pr_rank": 2, "resource_state": 2, "segment": { "activity_type": "Run", "average_grade": 0.1, "city": "San Francisco", "climb_category": 0, "distance": 542.2, "elevation_high": 3.9, "elevation_low": 3.0, "end_latitude": 37.784975, "end_latlng": [ 37.784975, -122.387799 ], "end_longitude": -122.387799, "id": 2856187, "maximum_grade": 3.6, "name": "Harrison to Brannan on Embarcadero", "pr_time": 219, "private": false, "resource_state": 2, "start_latitude": 37.789698, "start_latlng": [ 37.789698, -122.388244 ], "start_longitude": -122.388244, "state": "CA" }, "start_date": "2013-12-12T20:08:00Z", "start_date_local": "2013-12-12T12:08:00Z", "start_index": 892 } ], "splits_metric": [ { "distance": 1002.9, "elapsed_time": 395, "elevation_difference": -2.0, "moving_time": 369, "split": 1 }, { "distance": 1000.0, "elapsed_time": 429, "elevation_difference": -0.1, "moving_time": 429, "split": 2 }, { "distance": 998.7, "elapsed_time": 482, "elevation_difference": -0.5, "moving_time": 482, "split": 3 }, { "distance": 1000.2, "elapsed_time": 620, "elevation_difference": 0.6, "moving_time": 620, "split": 4 }, { "distance": 1000.3, "elapsed_time": 583, "elevation_difference": 7.5, "moving_time": 508, "split": 5 }, { "distance": 779.0, "elapsed_time": 631, "elevation_difference": -5.3, "moving_time": 487, "split": 6 } ], "splits_standard": [ { "distance": 1613.2, "elapsed_time": 622, "elevation_difference": -2.2, "moving_time": 596, "split": 1 }, { "distance": 1608.2, "elapsed_time": 893, "elevation_difference": -0.5, "moving_time": 893, "split": 2 }, { "distance": 1609.2, "elapsed_time": 926, "elevation_difference": 3.4, "moving_time": 851, "split": 3 }, { "distance": 950.5, "elapsed_time": 699, "elevation_difference": -0.5, "moving_time": 555, "split": 4 } ], "start_date": "2013-12-12T19:36:41Z", "start_date_local": "2013-12-12T11:36:41Z", "start_latitude": 37.781076, "start_latlng": [ 37.781076, -122.395536 ], "start_longitude": -122.395536, "timezone": "(GMT-08:00) America/Los_Angeles", "total_elevation_gain": 13.5, "trainer": false, "type": "Run", "upload_id": 109244893, "workout_type": null } stravalib-2.2/src/stravalib/tests/resources/athlete.2.json000066400000000000000000000037711475174155400240040ustar00rootroot00000000000000{ "all_ride_totals": { "count": 8, "distance": 548551, "elapsed_time": 81493, "elevation_gain": 9860, "moving_time": 81493 }, "all_run_totals": { "count": 3, "distance": 47052, "elapsed_time": 15976, "elevation_gain": 752, "moving_time": 15976 }, "approve_followers": true, "athlete_type": 0, "biggest_climb_elevation_gain": 3016.7999999999997, "biggest_ride_distance": 123285.0, "city": "San Francisco", "created_at": "2012-01-18T18:20:37Z", "date_preference": "%m/%d/%Y", "email_language": "en-US", "firstname": "John", "follower": "accepted", "follower_count": 0, "friend": null, "friend_count": 34, "id": 227615, "lastname": "Applestrava", "measurement_preference": "feet", "mutual_friend_count": 27, "premium": true, "profile": "http://dgalywyr863hv.cloudfront.net/pictures/athletes/227615/41555/3/large.jpg", "profile_medium": "http://dgalywyr863hv.cloudfront.net/pictures/athletes/227615/41555/3/medium.jpg", "profile_original": "http://dgalywyr863hv.cloudfront.net/pictures/athletes/227615/41555/3/original.jpg", "recent_ride_totals": { "achievement_count": 0, "count": 0, "distance": 0.0, "elapsed_time": 0, "elevation_gain": 0.0, "moving_time": 0 }, "recent_run_totals": { "achievement_count": 0, "count": 0, "distance": 0.0, "elapsed_time": 0, "elevation_gain": 0.0, "moving_time": 0 }, "resource_state": 3, "sex": "M", "state": "CA", "super_user": false, "updated_at": "2013-01-08T19:08:21Z", "ytd_ride_totals": { "count": 1, "distance": 32998, "elapsed_time": 4977, "elevation_gain": 362, "moving_time": 4977 }, "ytd_run_totals": { "count": 0, "distance": 0, "elapsed_time": 0, "elevation_gain": 0, "moving_time": 0 } } stravalib-2.2/src/stravalib/tests/resources/athlete.3.json000066400000000000000000000102151475174155400237740ustar00rootroot00000000000000{ "agreed_to_terms": true, "all_ride_totals": { "count": 266, "distance": 7401423, "elapsed_time": 1349820, "elevation_gain": 73649, "moving_time": 1348305 }, "all_run_totals": { "count": 5, "distance": 47045, "elapsed_time": 15925, "elevation_gain": 50, "moving_time": 15295 }, "approve_followers": true, "athlete_type": 1, "biggest_climb_elevation_gain": 474.6, "biggest_ride_distance": 152127.0, "bikes": [ { "distance": 1981531.2, "id": "b55763", "name": "Cannondale SuperSix HI-MOD 2 Red", "primary": false, "resource_state": 2 }, { "distance": 176435.9, "id": "b809124", "name": "LOOK 986", "primary": true, "resource_state": 2 }, { "distance": 191201.9, "id": "b58057", "name": "Niner One 9", "primary": false, "resource_state": 2 }, { "distance": 82744.3, "id": "b979998", "name": "Specialized Langster Pro", "primary": false, "resource_state": 2 } ], "city": "Nicasio", "clubs": [ { "id": 7, "name": "Team Roaring Mouse", "resource_state": 2 }, { "id": 1, "name": "Team Strava Cycling", "resource_state": 2 }, { "id": 34444, "name": "Team Strava Cyclocross", "resource_state": 2 } ], "created_at": "2009-09-22T18:24:06Z", "date_preference": "%m/%d/%Y", "dateofbirth": "1985-01-11", "description": "iOS Software Engineer at Strava.", "email": "jeff@strava.com", "email_facebook_twitter_friend_joins": false, "email_kom_lost": false, "email_language": "en-US", "email_send_follower_notices": false, "facebook_sharing_enabled": true, "firstname": "Jeff", "follower": null, "follower_count": 106, "follower_request_count": 0, "friend": null, "friend_count": 115, "ftp": null, "global_privacy": 1, "id": 1513, "instagram_username": "jeffremer", "lastname": "Remer", "max_heartrate": 198, "measurement_preference": "feet", "mutual_friend_count": 0, "offer_in_app_payment": false, "plan": "paid", "premium": true, "premium_expiration_date": 1390896000, "profile": "http://dgalywyr863hv.cloudfront.net/pictures/athletes/1513/692280/5/large.jpg", "profile_medium": "http://dgalywyr863hv.cloudfront.net/pictures/athletes/1513/692280/5/medium.jpg", "profile_original": "http://dgalywyr863hv.cloudfront.net/pictures/athletes/1513/692280/5/original.jpg", "receive_comment_emails": false, "receive_follower_feed_emails": true, "receive_kudos_emails": false, "receive_newsletter": true, "recent_ride_totals": { "achievement_count": 0, "count": 0, "distance": 0.0, "elapsed_time": 0, "elevation_gain": 0.0, "moving_time": 0 }, "recent_run_totals": { "achievement_count": 9, "count": 1, "distance": 5781.10009765625, "elapsed_time": 3140, "elevation_gain": 13.504196166992188, "moving_time": 2892 }, "resource_state": 3, "sample_race_distance": 10000, "sample_race_time": 5400, "sex": "M", "shoes": [ { "distance": 9045.8, "id": "g69911", "name": "Salomon XT Wings 2", "primary": true, "resource_state": 2 } ], "state": "California", "super_user": true, "updated_at": "2013-12-18T01:24:27Z", "username": "jeffremer", "weight": 75.2963, "ytd_ride_totals": { "count": 20, "distance": 585559, "elapsed_time": 126841, "elevation_gain": 8024, "moving_time": 126841 }, "ytd_run_totals": { "count": 1, "distance": 5782, "elapsed_time": 2892, "elevation_gain": 14, "moving_time": 2892 } } stravalib-2.2/src/stravalib/tests/resources/example_route_response.json000066400000000000000000000356731475174155400270130ustar00rootroot00000000000000{ "athlete": { "id": 42424242, "username": "foobar", "resource_state": 2, "firstname": "Foo", "lastname": "Bar", "bio": null, "city": null, "state": null, "country": null, "sex": "M", "premium": true, "summit": true, "created_at": "2014-03-22T12:54:16Z", "updated_at": "2023-01-13T09:20:30Z", "badge_type_id": 1, "weight": 65.0, "profile_medium": null, "profile": null, "friend": null, "follower": null }, "description": "", "distance": 15115.193361202424, "elevation_gain": 24.119343701388587, "id": 23895346, "id_str": "23895346", "map": { "id": "r23895346", "summary_polyline": "{~e}H}gz_@jAWb@WfB_BTG\\DhBiChBwCTUJa@?_@I]BKfAy@dFiDpHqDF?Ld@dBq@rCmBRQnAu@tCgAjEsCbEyCfRsK~MyFfDkBLM`JteBNaAvFwWrCkMfF_UPi@bF{TLo@t@sIlOhAxAN`Dn@vKrC~E`BfDf@l@d@bAdBvAdBnNpIw@vLg@rJYbDBNdErK`MvZdB~DjA`CnAbBhCrElHdHf@p@xBtBdBjBhInOrFtJbApCKnJgA`GMtAYG}BOuACiDHcCN_B@aMw@eLc@cOu@k@?eQjDq@ReAb@{Cl@}@FmDx@iNnCoAP}AFwBo@y@I{AUgImCkE{AaEoAkCk@_E_@cNS_AQcHwBmF{A}Bi@oAc@_Bs@_AQqBSg@Q_@WuCwCyBeCFUCKeAuAgCsCKUmFcb@QaDOg@PSFQHaAG_@mBqFu@qDe@cDTKBMHGfASHM\\uAh@[A?|@c@FI@YSqD@_@f@gB\\y@p@wBt@wCZo@Pg@ZoBP[@WJa@HMZMcBcG?g@aAmCeA}Bo@y@IP}@gBg@s@sClBeBp@Me@G?cBv@mExBaG~D_@Yc@e@oDa\\_JlEwCx@", "resource_state": 3, "polyline": "{~e}H}gz_@jAWb@WfB_BTG\\DhBiChBwCTUJa@?_@I]BKfAy@dFiDpHqDF?Ld@dBq@rCmBRQnAu@tCgAjEsCbEyCfRsK~MyFfDkBLM`JteBNaAvFwWrCkMfF_UPi@bF{TLo@t@sIlOhAxAN`Dn@vKrC~E`BfDf@l@d@bAdBvAdBnNpIw@vLg@rJYbDBNdErK`MvZdB~DjA`CnAbBhCrElHdHf@p@xBtBdBjBhInOrFtJbApCKnJgA`GMtAYG}BOuACiDHcCN_B@aMw@eLc@cOu@k@?eQjDq@ReAb@{Cl@}@FmDx@iNnCoAP}AFwBo@y@I{AUgImCkE{AaEoAkCk@_E_@cNS_AQcHwBmF{A}Bi@oAc@_Bs@_AQqBSg@Q_@WuCwCyBeCFUCKeAuAgCsCKUmFcb@QaDOg@PSFQHaAG_@mBqFu@qDe@cDTKBMHGfASHM\\uAh@[A?|@c@FI@YSqD@_@f@gB\\y@p@wBt@wCZo@Pg@ZoBP[@WJa@HMZMcBcG?g@aAmCeA}Bo@y@IP}@gBg@s@sClBeBp@Me@G?cBv@mExBaG~D_@Yc@e@oDa\\_JlEwCx@" }, "map_urls": { "url": "https://d3o5xota0a1fcr.cloudfront.net/v6/maps/Q3LT7TUSJGLDPHNKMKCD4VRX4ZZWENILSWXXN7GSWMW7FWCC3CAC46GWXZ6ISCRDS5LUL666K3J2NXUJUC5SRFK3STRQ====", "retina_url": "https://d3o5xota0a1fcr.cloudfront.net/v6/maps/ITVROAQQNZR62HQYXAJLIZAZMELM54C3OGMCXJ76YABIOJCGCNHBP5L4U5VQGRWKFKAJWEAZI7SED65RNRD2ACR5BFHA====" }, "name": "15k, no traffic", "private": false, "resource_state": 3, "starred": false, "sub_type": 1, "created_at": "2020-02-15T07:26:24Z", "updated_at": "2023-01-25T07:03:47Z", "timestamp": 1581751584, "type": 2, "estimated_moving_time": 4609, "segments": [ { "id": 5908898, "resource_state": 2, "name": "Paradijsweg", "activity_type": "Run", "distance": 1131.6, "average_grade": -0.2, "maximum_grade": 6.3, "elevation_high": 16.0, "elevation_low": 8.4, "start_latlng": [ 52.129719, 5.388209 ], "end_latlng": [ 52.120482, 5.394089 ], "elevation_profile": null, "climb_category": 0, "city": "Leusden", "state": "UT", "country": "The Netherlands", "private": false, "hazardous": false, "starred": false }, { "id": 5909010, "resource_state": 2, "name": "nimmerdor treek", "activity_type": "Run", "distance": 1908.1, "average_grade": -0.2, "maximum_grade": 5.4, "elevation_high": 17.8, "elevation_low": 8.5, "start_latlng": [ 52.136221, 5.382968 ], "end_latlng": [ 52.120943, 5.393756 ], "elevation_profile": null, "climb_category": 0, "city": "Amersfoort", "state": "UT", "country": "The Netherlands", "private": false, "hazardous": false, "starred": false }, { "id": 6038825, "resource_state": 2, "name": "Treekerweg", "activity_type": "Run", "distance": 790.2, "average_grade": 0.2, "maximum_grade": 2.5, "elevation_high": 9.9, "elevation_low": 8.6, "start_latlng": [ 52.113625, 5.393466 ], "end_latlng": [ 52.10719, 5.391807 ], "elevation_profile": null, "climb_category": 0, "city": "Leusden", "state": "Utrecht", "country": "Netherlands", "private": false, "hazardous": false, "starred": false }, { "id": 7845371, "resource_state": 2, "name": "Viaduct Nimmerdor A28 N-Z", "activity_type": "Run", "distance": 300.3, "average_grade": 0.3, "maximum_grade": 5.4, "elevation_high": 14.7, "elevation_low": 8.7, "start_latlng": [ 52.133993, 5.385814 ], "end_latlng": [ 52.131475, 5.387323 ], "elevation_profile": null, "climb_category": 0, "city": "Leusden", "state": "UT", "country": "The Netherlands", "private": false, "hazardous": false, "starred": false }, { "id": 7845381, "resource_state": 2, "name": "Viaduct Nimmerdor A28 Z-N", "activity_type": "Run", "distance": 333.0, "average_grade": -0.2, "maximum_grade": 4.7, "elevation_high": 15.2, "elevation_low": 8.6, "start_latlng": [ 52.131352, 5.387419 ], "end_latlng": [ 52.134134, 5.385679 ], "elevation_profile": null, "climb_category": 0, "city": "Leusden", "state": "UT", "country": "The Netherlands", "private": false, "hazardous": false, "starred": false }, { "id": 8566739, "resource_state": 2, "name": "Doornse Grindweg - fietspad", "activity_type": "Run", "distance": 2014.9, "average_grade": -0.0, "maximum_grade": 3.2, "elevation_high": 18.0, "elevation_low": 11.8, "start_latlng": [ 52.092348, 5.362292 ], "end_latlng": [ 52.110293, 5.36069 ], "elevation_profile": null, "climb_category": 0, "city": "Woudenberg", "state": "Utrecht", "country": "Netherlands", "private": false, "hazardous": false, "starred": false }, { "id": 9800119, "resource_state": 2, "name": "Kerkweg", "activity_type": "Run", "distance": 628.9, "average_grade": -0.9, "maximum_grade": 2.9, "elevation_high": 15.0, "elevation_low": 8.5, "start_latlng": [ 52.127515, 5.366518 ], "end_latlng": [ 52.129971, 5.374385 ], "elevation_profile": null, "climb_category": 0, "city": "Leusden", "state": "Utrecht", "country": "Netherlands", "private": false, "hazardous": false, "starred": false }, { "id": 11980735, "resource_state": 2, "name": "Treekerweg Paaltjes tot Paaltjes", "activity_type": "Run", "distance": 1844.2, "average_grade": 0.1, "maximum_grade": 5.0, "elevation_high": 19.7, "elevation_low": 10.8, "start_latlng": [ 52.102526, 5.388294 ], "end_latlng": [ 52.093437, 5.368515 ], "elevation_profile": null, "climb_category": 0, "city": "Leusden", "state": "Utrecht", "country": "Netherlands", "private": false, "hazardous": false, "starred": false }, { "id": 15380426, "resource_state": 2, "name": "Trekerweg", "activity_type": "Run", "distance": 212.4, "average_grade": 0.5, "maximum_grade": 1.4, "elevation_high": 17.0, "elevation_low": 16.0, "start_latlng": [ 52.093129, 5.368144 ], "end_latlng": [ 52.091816, 5.36605 ], "elevation_profile": null, "climb_category": 0, "city": "Leusden", "state": "Utrecht", "country": "Netherlands", "private": false, "hazardous": false, "starred": false }, { "id": 22491920, "resource_state": 2, "name": "Ooiervaarshorstlaan", "activity_type": "Run", "distance": 1149.7, "average_grade": -0.5, "maximum_grade": 2.6, "elevation_high": 17.2, "elevation_low": 7.9, "start_latlng": [ 52.118442, 5.378588 ], "end_latlng": [ 52.113753, 5.393267 ], "elevation_profile": null, "climb_category": 0, "city": "Leusden", "state": "Utrecht", "country": "Netherlands", "private": false, "hazardous": false, "starred": false }, { "id": 23698584, "resource_state": 2, "name": "400m Slagboom Keesomstraat tot aan viadcut ", "activity_type": "Run", "distance": 386.3, "average_grade": 0.1, "maximum_grade": 1.5, "elevation_high": 11.6, "elevation_low": 11.0, "start_latlng": [ 52.137145, 5.382523 ], "end_latlng": [ 52.134375, 5.385398 ], "elevation_profile": null, "climb_category": 0, "city": "Leusden", "state": "Utrecht", "country": "Netherlands", "private": false, "hazardous": false, "starred": false }, { "id": 30236892, "resource_state": 2, "name": "Sprinten naar het tankstation", "activity_type": "Run", "distance": 335.2, "average_grade": -0.1, "maximum_grade": 1.0, "elevation_high": 12.1, "elevation_low": 11.2, "start_latlng": [ 52.123585, 5.363975 ], "end_latlng": [ 52.126445, 5.365075 ], "elevation_profile": null, "climb_category": 0, "city": "Leusden", "state": "Utrecht", "country": "Netherlands", "private": false, "hazardous": false, "starred": false }, { "id": 30818657, "resource_state": 2, "name": "Mountainbikers ontwijken", "activity_type": "Run", "distance": 260.1, "average_grade": -0.1, "maximum_grade": 2.6, "elevation_high": 13.4, "elevation_low": 12.6, "start_latlng": [ 52.0999, 5.377475 ], "end_latlng": [ 52.098253, 5.374693 ], "elevation_profile": null, "climb_category": 0, "city": "Leusden", "state": "Utrecht", "country": "Netherlands", "private": false, "hazardous": false, "starred": false }, { "id": 31055810, "resource_state": 2, "name": "Wat een dooie boel hier", "activity_type": "Run", "distance": 179.1, "average_grade": -0.2, "maximum_grade": 0.9, "elevation_high": 11.5, "elevation_low": 10.9, "start_latlng": [ 52.12994, 5.375362 ], "end_latlng": [ 52.130797, 5.37817 ], "elevation_profile": null, "climb_category": 0, "city": "Leusden", "state": "Utrecht", "country": "Netherlands", "private": false, "hazardous": false, "starred": false }, { "id": 31099653, "resource_state": 2, "name": "Zwaaien naar de horeca", "activity_type": "Run", "distance": 438.7, "average_grade": 0.0, "maximum_grade": 0.6, "elevation_high": 12.9, "elevation_low": 12.5, "start_latlng": [ 52.11288, 5.360848 ], "end_latlng": [ 52.11667, 5.362385 ], "elevation_profile": null, "climb_category": 0, "city": "Leusden", "state": "Utrecht", "country": "Netherlands", "private": false, "hazardous": false, "starred": false }, { "id": 31263331, "resource_state": 2, "name": "Vals plat sprint", "activity_type": "Run", "distance": 389.7, "average_grade": -0.3, "maximum_grade": 1.2, "elevation_high": 13.3, "elevation_low": 11.9, "start_latlng": [ 52.117255, 5.36258 ], "end_latlng": [ 52.120722, 5.362687 ], "elevation_profile": null, "climb_category": 0, "city": "Leusden", "state": "Utrecht", "country": "Netherlands", "private": false, "hazardous": false, "starred": false } ] } stravalib-2.2/src/stravalib/tests/resources/example_zone_response.json000066400000000000000000000033301475174155400266110ustar00rootroot00000000000000[ { "score": 28.0, "distribution_buckets": [ { "min": 0, "max": 120, "time": 589 }, { "min": 120, "max": 149, "time": 2732 }, { "min": 149, "max": 164, "time": 0 }, { "min": 164, "max": 179, "time": 0 }, { "min": 179, "max": -1, "time": 0 } ], "type": "heartrate", "resource_state": 3, "sensor_based": true, "points": 0, "custom_zones": false }, { "score": 4, "distribution_buckets": [ { "max": 3.319925953425268, "min": 0, "time": 2042 }, { "max": 3.8569727988322966, "min": 3.319925953425268, "time": 285 }, { "max": 4.296374763256229, "min": 3.8569727988322966, "time": 870 }, { "max": 4.589309406205517, "min": 4.296374763256229, "time": 121 }, { "max": 4.882244049154806, "min": 4.589309406205517, "time": 3 }, { "max": -1, "min": 4.882244049154806, "time": 0 } ], "type": "pace", "resource_state": 3, "sensor_based": true } ] stravalib-2.2/src/stravalib/tests/resources/sample.tcx000066400000000000000000015617671475174155400233430ustar00rootroot00000000000000 2009-02-21T12:57:25Z 18.645.3164786.7650000 81 96 ActiveLocation -90.5360.000 70 -91.1810.000 76 -91.6090.000 81 -91.1230.000 87 -91.6780.000 90 52.1485144.500887 -91.7315.316 93 8168.5864664.87109411.1399992371 137 165 ActiveManual 52.1483264.500603 -90.79534.314 98 -90.98175.185 105 -91.408120.276 108 52.1474204.500992 -90.549187.704 108 52.1472764.501159 -90.845207.269 108 52.1469184.501408 -89.691250.563 112 -88.002351.016 116 -88.807365.969 118 -90.384404.661 114 52.1455114.501028 -89.606496.550 119 -88.306595.279 121 52.1450544.499456 -88.083617.242 121 52.1449594.499014 -86.080653.732 119 52.1448864.498988 -84.689662.360 115 52.1448734.499016 -83.897666.266 89 52.1448504.499045 -84.188669.897 89 52.1447624.499182 -84.003682.863 91 52.1445984.499638 -84.017719.079 99 52.1445754.499735 -84.371726.232 105 52.1444804.500130 -86.311755.320 107 52.1442724.500675 -88.188799.698 106 52.1439194.501586 -88.085873.543 108 52.1438544.501824 -87.567890.863 108 52.1435484.502766 -87.152964.460 109 52.1432324.503736 -86.6251039.606 115 52.1424524.506057 -86.5351220.746 118 52.1419814.507488 -84.7071331.908 118 52.1413594.509149 -85.2771465.209 119 52.1409604.510186 -82.6201549.229 121 52.1408764.510336 -82.7521563.096 121 52.1406774.511251 -82.8281629.651 128 52.1406464.511362 -82.4281637.967 128 52.1404724.511900 -81.9891679.624 123 52.1401004.513061 -81.5211769.248 118 52.1396854.514491 -83.5321877.682 117 52.1396474.514610 -83.4511886.705 117 52.1394974.515338 -82.4541939.348 115 52.1393254.515894 -82.8101981.955 107 52.1392564.516111 -82.9551998.707 106 52.1393334.516797 -81.4932048.878 108 52.1402464.518509 -81.1272204.151 110 52.1410864.520136 -81.3582349.582 110 52.1411424.520215 -81.1612357.843 110 52.1412044.520821 -80.9522405.136 108 52.1411094.521361 -79.7722448.595 105 52.1411534.521451 -79.9412456.532 106 52.1419724.523011 -79.4952596.904 105 52.1427074.524274 -77.7182716.174 100 52.1427474.524329 -77.6242722.406 99 52.1429054.524631 -76.8052749.359 93 52.1430824.525016 -77.2442782.134 93 52.1432544.525424 -77.0952816.088 95 52.1433614.525766 -76.7352842.259 100 52.1432654.526445 -77.5782894.013 103 52.1430324.526761 -77.2392927.763 110 52.1428694.527006 -76.9112952.496 118 52.1428164.527088 -77.1192960.720 118 52.1424104.527760 -75.3583025.138 124 52.1423114.527918 -75.5273040.530 128 52.1416614.529141 -75.8463151.371 126 52.1410564.530803 -75.5893283.686 125 52.1405774.532182 -75.0863392.252 126 52.1399924.533714 -73.6323515.988 124 52.1399214.534043 -73.1713539.803 124 52.1396054.534913 -71.4303609.093 124 52.1393684.535417 -71.3663654.894 119 52.1395264.536026 -71.3933701.070 120 52.1396464.537370 -71.2603794.403 126 52.1398234.539524 -72.5883943.419 123 52.1397974.540062 -72.6563980.079 128 52.1397744.540326 -72.3233998.659 132 52.1397534.540593 -72.2974017.021 127 52.1397424.540726 -72.2974026.297 127 52.1397104.541129 -72.4774054.110 120 52.1396994.541263 -72.1704063.380 120 52.1393534.543694 -71.6674234.552 117 52.1389784.545010 -71.1494333.926 122 52.1381674.547478 -70.9474525.310 126 52.1372314.549654 -70.3204707.600 127 52.1361244.551542 -69.9834886.213 129 52.1351174.552922 -69.4715032.781 125 52.1349134.553076 -70.6685057.787 130 52.1348424.553108 -70.1085066.005 132 52.1347084.553202 -70.1355082.358 135 52.1344894.553508 -68.6235114.800 130 52.1344514.553615 -68.4605123.256 129 52.1338324.554705 -68.2815225.490 128 52.1329404.556074 -68.2615361.803 127 52.1324784.556921 -67.5075439.812 128 52.1316154.558497 -66.7055584.483 130 52.1309924.559868 -66.5605701.334 132 52.1305214.561028 -66.8345796.571 127 52.1297844.563019 -64.9805955.758 134 52.1291864.565339 -65.3896128.216 133 52.1287694.567553 -63.4776286.723 132 52.1287194.567801 -64.2826304.605 131 52.1284504.569438 -63.7126421.055 131 52.1284444.569697 -63.4026438.795 129 52.1281164.572764 -62.8946651.950 129 52.1279744.574058 -62.3746741.989 136 52.1279444.574318 -61.8856760.140 143 52.1279294.574449 -61.7096769.260 142 52.1278754.575113 -61.7706815.066 137 52.1278004.576967 -62.3426942.394 132 52.1277954.577100 -62.1076951.250 131 52.1277004.579857 -60.2087140.495 129 52.1277964.582147 -60.3857297.937 133 52.1277174.582558 -60.1877327.771 130 52.1276834.582854 -60.0337348.758 124 52.1277084.582952 -59.9597356.007 124 52.1279944.583315 -59.0877396.836 124 52.1280824.583550 -59.7287415.630 125 52.1282684.584148 -60.0037461.580 133 52.1288264.586422 -58.9787629.251 128 52.1290624.587609 -57.8347714.833 124 52.1292354.588214 -58.2017767.653 123 52.1293944.588470 -58.2287792.925 124 52.1296764.589124 -57.9557847.519 129 52.1303614.590981 -56.3337996.001 125 52.1304574.591287 -56.2008019.518 120 52.1305144.591509 -55.4388036.204 117 52.1306204.591495 -56.5268050.202 117 52.1309514.591563 -56.3558090.327 119 52.1310084.591552 -56.1268096.652 119 52.1311314.591513 -56.2388110.682 119 52.1314524.591305 -55.7138149.136 125 52.1315704.591208 -55.3828163.963 132 52.1316404.591137 -55.0328172.935 138 52.1317144.591088 -55.6438181.800 138 52.1319924.590968 -55.1268216.721 129 52.1320584.591054 -55.2798226.029 129 52.1321004.591160 -55.1528234.732 129 52.1323594.591989 -56.0358298.142 132 52.1324654.592628 -56.6298343.645 134 52.1323304.593867 -55.2848430.406 129 52.1322064.595270 -55.1378527.706 127 52.1325494.595651 -55.5348580.963 128 52.1331284.595627 -55.7618645.298 129 52.1339124.594921 -57.0718745.694 133 52.1340404.594791 -56.6808762.483 132 52.1344934.594867 -53.6398817.478 128 52.1345384.594938 -53.7168824.747 127 52.1346164.595095 -53.3708838.153 124 52.1346634.595091 -52.9458843.869 120 52.1347694.594961 -54.6818858.627 119 52.1348624.594839 -56.6638871.998 120 52.1351924.594459 -55.0508917.017 122 52.1354164.594321 -54.6798943.991 121 52.1361154.593739 -56.0169032.830 125 52.1368214.592978 -55.5689127.252 127 52.1371614.592601 -55.6239172.967 133 52.1384854.591147 -55.4889350.871 137 52.1396734.589821 -54.3639511.209 137 52.1408284.588928 -53.7569653.528 137 52.1414094.588519 -53.8979724.176 135 52.1419874.588512 -52.5859791.201 133 52.1423684.588595 -53.5139835.681 133 52.1437534.587638 -53.08810003.095 133 52.1450324.586770 -52.50310157.333 134 52.1464744.585751 -52.35510332.382 140 52.1478724.584760 -52.21510502.026 138 52.1481544.584554 -51.78910536.479 143 52.1485884.584244 -51.30610589.218 150 52.1491854.583827 -51.22610661.515 145 52.1505994.582851 -50.53710832.204 142 52.1517504.582038 -49.83010972.026 140 52.1527074.581303 -49.52611089.981 137 52.1529304.581040 -49.25211120.575 133 52.1532414.580777 -47.63411159.383 125 52.1533194.580727 -46.32111168.721 125 52.1534174.581166 -47.65611203.977 124 52.1534444.581397 -48.76811220.134 128 52.1537094.583313 -47.86711354.520 131 52.1541194.586252 -47.76611560.832 130 52.1543514.587881 -46.38911675.203 125 52.1544464.588496 -45.55411718.564 120 52.1547654.588533 -47.87011753.168 120 52.1558034.588199 -47.64911871.195 125 52.1568104.587891 -47.76211985.109 130 52.1578584.587465 -47.89012105.388 131 52.1580834.587370 -47.95312131.218 136 52.1590714.586964 -47.63812244.801 135 52.1606254.586318 -46.20112423.354 134 52.1621644.585654 -45.56312600.551 135 52.1633024.585162 -45.74912731.604 134 52.1645234.584650 -45.25412871.901 136 52.1655344.584233 -44.28712987.870 131 52.1657724.584130 -43.62413015.224 129 52.1661124.584152 -45.11013053.855 124 52.1663474.584061 -47.83413080.844 121 52.1665054.583965 -45.92113099.423 121 52.1669554.583718 -42.71513155.394 120 52.1671364.584216 -43.40213195.809 121 52.1673934.585245 -43.56413271.835 126 52.1677334.585843 -43.19413328.191 130 52.1678814.586483 -44.14113375.556 135 52.1680834.587822 -44.22813470.075 139 52.1682044.589525 -43.77613588.248 132 52.1683724.590369 -42.76313648.878 130 52.1682384.590915 -43.24513694.887 129 52.1676634.591241 -43.57513762.995 131 52.1674904.591499 -42.28313789.280 133 52.1673874.591861 -43.09213816.646 133 52.1671614.592434 -43.68713863.891 133 52.1669154.593138 -43.41913919.670 131 52.1668514.594976 -42.25814047.076 134 52.1673514.598100 -41.95514268.001 133 52.1679184.600322 -42.07514434.267 133 52.1680694.601280 -42.01014502.413 133 52.1677674.602921 -40.58814620.167 132 52.1676664.603208 -39.78714642.685 130 52.1672214.603859 -40.52614709.779 135 52.1669314.604036 -39.77914744.140 141 52.1652044.604602 -39.54814940.486 139 52.1633434.605119 -38.39415150.557 144 52.1628144.605270 -38.53415210.343 143 52.1626104.605417 -39.79015235.368 143 52.1624334.606258 -40.67315298.759 144 52.1624494.606411 -41.86015309.126 145 52.1625284.606883 -43.51715342.560 146 52.1629184.609406 -42.38415520.937 143 52.1631174.610831 -42.21815620.906 149 52.1633504.612483 -42.86015736.878 144 52.1637974.614876 -41.54115908.483 143 52.1642354.617614 -41.96616102.087 142 52.1645084.619554 -40.96416237.964 139 52.1648964.621896 -40.51816404.387 140 52.1650614.623660 -39.44516526.797 142 52.1654354.626307 -39.89516713.072 141 52.1658614.629005 -40.10816903.646 141 52.1660304.629609 -39.52216949.201 143 52.1661984.630673 -39.57817024.523 144 52.1661854.631230 -39.91517062.930 143 52.1663914.632871 -39.08917178.076 143 52.1667384.635261 -36.78617346.430 143 52.1667794.635702 -36.06617376.756 143 52.1666194.636338 -36.05917426.408 141 52.1665994.636447 -35.99717434.186 142 52.1665194.637383 -34.35717498.943 147 52.1665184.637730 -33.91817522.617 147 52.1667474.639066 -30.63117617.783 152 52.1670104.640196 -32.87317700.602 149 52.1672154.640858 -33.89217751.395 146 52.1674964.641590 -37.78817813.033 143 52.1675864.642493 -38.35817875.652 138 52.1676684.643982 -38.70817978.230 137 52.1676784.644826 -38.38318036.039 142 52.1677294.647350 -38.37818208.799 146 52.1677814.650032 -37.13718392.488 144 52.1678304.652737 -37.62918577.602 142 52.1678944.655685 -36.91318779.328 141 52.1679524.658415 -37.24818966.369 141 52.1680014.661111 -36.82419150.881 143 52.1680674.664077 -35.99919353.980 142 52.1681604.666115 -35.33719493.820 140 52.1681564.666346 -35.14919509.535 140 52.1682504.666803 -34.83619543.484 136 -38.82719633.311 130 52.1680784.669408 -34.91019723.307 135 52.1680904.670219 -34.91719778.826 140 52.1681064.671060 -34.68219836.451 145 52.1681354.672517 -34.85419936.193 150 52.1682114.675636 -35.25120149.773 150 52.1682934.679149 -34.32820390.375 147 52.1683824.682223 -34.34320601.066 144 52.1684744.685515 -33.99020826.543 143 52.1685004.686094 -33.88820866.293 143 52.1688834.686988 -33.72420943.541 145 52.1707044.688346 -33.69521166.586 143 52.1723314.688941 -33.59721352.197 145 52.1739994.689558 -33.87521542.545 147 52.1750654.689953 -33.36921663.971 147 52.1753374.690134 -31.84321696.615 146 52.1754654.690517 -31.77221727.096 144 52.1753824.690927 -31.85221758.887 143 52.1746314.692364 -32.92521888.373 146 52.1734204.694546 -32.35922089.439 148 52.1725964.696052 -32.76822227.324 149 52.1722694.696651 -32.26922281.963 150 52.1720844.697531 -32.05622346.291 151 52.1717174.698148 -34.00122407.023 149 52.1713664.698379 -32.17222448.988 146 52.1711264.698693 -29.24522483.410 146 52.1710504.698832 -28.19822496.158 147 52.1709394.699056 -29.30322515.873 150 52.1708744.699217 -28.43022529.059 151 52.1708564.699615 -26.85922556.920 153 52.1708914.699828 -27.50222572.062 153 52.1707924.700253 -28.59822604.365 153 52.1703384.701001 -28.57522676.238 150 52.1701144.701345 -26.60222711.297 147 52.1700724.701458 -27.38622720.463 147 52.1693714.702647 -28.37522833.625 148 52.1692264.702880 -26.47622855.768 146 52.1691614.702976 -27.26522865.613 143 52.1689904.703266 -27.19022893.678 143 52.1687314.703701 -29.38422935.355 148 52.1683954.704272 -28.82622989.453 154 52.1681734.704659 -28.06223025.621 161 52.1678934.705142 -28.95323070.666 156 52.1671514.706455 -26.21423192.529 152 52.1670314.706663 -26.30323212.080 149 52.1668234.707043 -27.22423247.236 153 52.1662964.708034 -27.81423337.029 148 52.1656174.709496 -23.77823463.387 150 52.1655714.709668 -23.05323476.221 150 52.1654004.710301 -25.11823523.707 149 52.1653544.710969 -26.20023572.977 149 52.1653634.711101 -27.48923582.100 148 52.1653654.712149 -27.84323653.719 142 52.1654024.713994 -28.35223780.170 138 52.1654164.714601 -25.73423821.750 139 52.1655144.716644 -26.96323962.391 136 52.1656354.717630 -26.59724031.080 134 52.1657674.718210 -27.61624073.725 134 52.1659124.719448 -28.76424159.863 139 52.1659484.719797 -26.95624184.035 141 52.1660794.720983 -27.96724266.566 136 52.1661714.721854 -27.69524327.023 141 52.1664284.724416 -27.88124504.434 140 52.1665304.725416 -27.63424574.006 135 52.1666734.726996 -27.38124683.375 133 52.1667514.728093 -25.78924758.785 138 52.1669684.729780 -26.81524877.486 138 52.1670794.730191 -28.36424908.320 137 52.1675754.732232 -28.17825058.414 132 52.1682774.735112 -27.82325270.270 131 52.1690824.738405 -27.98525512.873 130 52.1699464.741952 -28.11125773.865 128 52.1706544.744844 -27.94525986.834 129 52.1713524.747726 -27.01026198.627 130 52.1721314.750941 -27.28626435.133 132 52.1728444.753843 -27.21926648.963 133 52.1736814.757295 -27.41726902.324 129 52.1743754.760138 -26.80127112.082 123 52.1746184.761152 -28.46327186.539 128 52.1751004.763142 -26.69627332.912 133 52.1758304.766142 -26.36727553.619 131 52.1762054.767669 -26.80327665.994 125 52.1763184.768125 -26.30927699.516 120 52.1765984.769235 -25.30527781.459 116 52.1766354.769870 -26.35527825.582 121 52.1766754.769962 -26.26727833.430 127 52.1768604.770378 -27.57627868.967 132 52.1775164.773066 -25.76428066.828 134 52.1777464.773980 -25.78728134.258 141 52.1777694.774080 -25.01228141.082 140 52.1777314.774263 -25.73828157.266 135 52.1778224.774853 -26.60428199.160 135 52.1780604.775840 -28.06228271.844 141 52.1786974.778355 -26.53928457.740 141 52.1790424.779603 -26.89528551.531 141 52.1794784.781279 -26.38728676.139 146 52.1800164.782840 -22.95928798.699 146 52.1800984.783031 -22.26628814.516 146 52.1803774.784271 -23.14628905.996 149 52.1804764.785079 -25.58228962.525 147 52.1810354.787386 -25.89729132.123 142 52.1815904.789653 -24.75629299.084 137 52.1822944.792493 -24.71729508.518 135 52.1830134.795474 -24.29629727.562 131 52.1837374.798352 -24.50829940.074 129 52.1845214.801539 -24.36730174.928 130 52.1852594.804547 -24.18830396.639 127 52.1855454.805833 -24.18630490.260 127 52.1854484.806882 -24.10630564.219 126 52.1850334.807486 -25.47830627.148 126 52.1834604.808431 -24.90330813.635 131 52.1822074.809195 -24.58030962.584 136 52.1810834.809891 -24.35931096.412 141 52.1795224.810867 -24.42831282.193 142 52.1778994.811876 -24.42331475.873 143 52.1763364.812852 -24.34931662.193 146 52.1754384.813403 -22.58731768.980 144 52.1752114.813694 -23.09631801.385 144 52.1743164.815482 -21.08931959.053 140 52.1736544.816823 -21.75432076.576 135 52.1725994.818952 -22.01232263.688 135 52.1716634.820852 -20.96032430.266 134 52.1711644.821879 -19.41732519.783 139 52.1703034.823609 -19.61832672.074 144 52.1698604.824575 -20.27732754.559 146 52.1697644.825065 -20.45332789.973 146 52.1696954.826171 -19.75232865.957 141 52.1695894.828107 -19.76832999.035 136 52.1693254.828612 -19.61933048.465 135 52.1679204.829028 -19.80833207.402 136 52.1665304.829448 -19.67633364.809 134 52.1654104.829765 -18.99033491.359 136 52.1648074.829945 -18.97733559.566 135 52.1646244.829696 -19.90033589.695 133 52.1645344.828449 -20.70333675.617 139 52.1643734.826317 -19.51333822.594 138 52.1642084.824265 -19.24033964.398 139 52.1639914.821497 -19.09234155.410 139 52.1636454.819694 -19.09134285.281 144 52.1635654.819382 -19.07934308.328 139 52.1632324.817972 -18.20134411.719 134 52.1627444.816026 -18.70734555.699 133 52.1622614.814001 -20.34434704.340 137 52.1618874.812426 -18.68234819.980 142 52.1611164.809564 -19.42235033.805 142 52.1605764.807727 -18.32435173.180 146 52.1605134.807295 -17.96935203.625 146 52.1601684.806108 -18.40935293.367 151 52.1599454.805334 -19.72935351.980 144 52.1597264.804578 -20.61935409.250 144 52.1591034.802395 -19.08435573.930 144 52.1583464.799758 -18.88935772.879 148 52.1580934.798748 -17.82835847.656 154 52.1578234.797875 -19.13035914.516 149 52.1576674.797335 -18.21535955.367 148 52.1570264.795065 -18.62136126.309 153 52.1566554.793787 -17.62736223.078 152 52.1565754.793592 -18.34036239.043 152 52.1561644.792064 -17.80336353.352 154 52.1556224.790229 -17.73536492.688 154 52.1551734.788858 -17.41136598.969 158 52.1548324.787813 -17.27236679.973 162 52.1546674.787433 -17.53936711.918 156 52.1545244.787028 -16.70036743.848 161 52.1538094.785525 -16.51936874.840 156 52.1535764.785063 -16.97736914.938 161 52.1534404.784798 -16.14436939.215 156 52.1532354.784390 -15.84736975.016 154 52.1531654.784164 -14.99736991.930 153 52.1529584.784003 -14.83137018.910 153 52.1527594.783773 -16.10837046.738 149 52.1521374.782561 -15.66537154.730 153 52.1511294.780609 -16.02937329.328 152 52.1503624.779400 -14.62037448.773 154 52.1496814.778564 -14.64937543.875 153 52.1496154.778510 -14.21337552.223 153 52.1486054.777459 -14.98737685.676 156 52.1472324.775999 -15.64737868.199 153 52.1465254.775243 -14.64037962.297 158 52.1464214.775118 -13.98637976.523 165 52.1463754.775061 -14.06937982.793 164 52.1462934.774954 -14.07137994.422 163 52.1456144.774324 -14.89138082.004 160 52.1448754.773533 -14.44738180.465 155 52.1434984.772117 -14.06038361.789 151 52.1422194.770757 -14.38638531.668 150 52.1409444.769346 -14.99338703.527 151 52.1397854.768219 -14.64738854.145 152 52.1388144.767651 -13.76138968.977 151 52.1383874.767451 -14.16139018.578 155 52.1378204.767188 -14.07539084.055 150 52.1364894.766504 -13.91639239.504 149 52.1351484.765840 -14.15739395.539 148 52.1342274.765132 -14.09739509.148 147 52.1332464.763986 -13.22739644.289 146 52.1331494.763808 -13.33039660.273 152 52.1330994.763722 -13.11939668.605 152 52.1325794.762756 -13.61239756.480 151 52.1323144.762186 -14.42739805.504 150 52.1319064.761242 -14.15239884.336 155 52.1317174.760770 -14.34939922.867 150 52.1314694.760137 -13.08839974.125 150 52.1315944.759827 -11.90240010.727 151 52.1327904.759651 -11.43540144.621 150 52.1338054.759484 -15.17740258.297 145 52.1349084.759225 -15.58240382.500 142 52.1361724.758527 -16.57840531.109 140 52.1375704.757653 -17.35340697.695 140 52.1390114.756740 -16.39240869.914 141 52.1405344.755801 -16.28941051.125 142 52.1419534.754894 -16.77841220.805 143 52.1434214.753995 -16.36941394.902 142 52.1445874.753272 -16.75041534.156 146 52.1452524.752847 -16.26041613.688 146 52.1457924.752519 -15.24741677.918 147 52.1470064.751747 -15.33241822.641 142 52.1479444.751171 -15.73141934.527 140 52.1494184.750245 -15.68042110.406 138 52.1505104.749562 -15.59242240.582 138 52.1519834.748641 -15.26942416.219 139 52.1534024.747750 -15.10342585.520 138 52.1550054.746752 -15.16042776.492 134 52.1564754.745838 -14.84242951.605 135 52.1578494.744985 -14.77743115.223 136 52.1593344.744057 -14.04443292.215 135 52.1607294.743177 -13.99043458.855 132 52.1619984.742389 -14.63943609.961 134 52.1633864.741532 -14.41743775.137 133 52.1649784.740549 -14.86043964.496 135 52.1662664.739749 -14.78744118.012 138 52.1674334.739013 -14.94244257.285 140 52.1686594.738245 -13.64844403.371 139 52.1688714.738113 -13.53644428.473 137 52.1689644.737550 -15.28644473.926 137 52.1688104.736928 -16.05744519.777 142 52.1683284.735026 -14.44944660.559 143 52.1679754.733634 -13.91544763.574 144 52.1677244.732635 -14.29744837.477 150 52.1672614.730732 -13.53744977.227 150 52.1669074.729329 -9.84845081.336 152 52.1668794.729179 -9.87745091.957 152 52.1668064.728150 -10.22445163.047 157 52.1667654.727596 -10.08145201.297 152 52.1667524.727484 -10.01745209.062 151 52.1666904.726923 -12.63345248.082 151 52.1665494.725541 -11.78845343.965 146 52.1663134.723182 -12.59645507.195 142 52.1661264.721340 -12.27445635.223 146 52.1660594.720700 -12.71445679.641 151 52.1658314.718732 -11.61045816.660 150 52.1657444.718019 -10.82245866.195 147 52.1655844.717064 -10.46445934.109 147 52.1654494.715591 -10.26846036.113 147 52.1653974.712938 -9.64546217.883 144 52.1653944.711074 -8.86946345.418 139 52.1653944.710975 -8.80646352.121 139 52.1654944.710481 -7.57646388.367 138 52.1656994.709470 -6.17846462.273 144 52.1658214.709060 -8.13646493.805 145 52.1658944.708841 -8.45246510.770 145 52.1660804.708453 -8.26546544.617 145 52.1666934.707448 -9.21546642.031 140 52.1668114.707152 -9.11046666.172 139 52.1678134.705366 -9.30746831.586 137 52.1686534.703917 -7.65046967.898 138 52.1689184.703430 -8.14447012.555 138 52.1696704.702231 -8.13047129.766 137 52.1700224.701613 -6.76847187.320 132 52.1700984.701475 -6.94347199.922 131 52.1701734.701242 -7.55047217.930 131 52.1705004.700774 -7.49947266.938 132 52.1711544.699627 -7.00347374.004 130 52.1711744.699114 -6.42047412.094 129 52.1711504.698662 -7.19347446.348 126 52.1712374.698524 -8.50847460.074 126 52.1714794.698283 -11.14147491.945 125 52.1716854.698124 -9.89547517.422 122 52.1718184.698028 -10.58247533.586 121 52.1721034.697448 -11.44347585.840 121 52.1722144.696826 -11.17047630.078 119 52.1723614.696462 -9.84847660.348 121 52.1726434.695954 -10.42447707.227 126 52.1729704.695345 -10.37247762.523 131 52.1735344.694340 -9.38447855.578 132 52.1745094.692559 -9.20948018.758 135 52.1753104.691099 -9.49248152.527 136 52.1753754.690954 -8.57948164.477 134 52.1754914.690600 -9.26848191.867 126 52.1754714.690450 -9.69748202.711 126 52.1754354.690280 -10.17348215.047 126 52.1752634.690001 -10.85048243.352 128 52.1750064.689857 -10.71148273.598 135 52.1738394.689427 -10.23948406.762 135 52.1725374.688952 -10.61248555.211 138 52.1712484.688466 -9.78548702.414 138 52.1701844.687925 -10.44848826.664 139 52.1692634.687174 -10.34348941.523 141 52.1687964.686788 -10.08548999.695 142 52.1686194.686477 -11.18149029.137 143 52.1685544.686284 -10.94749043.965 143 52.1684854.685858 -10.95349074.570 144 52.1684554.683848 -11.67649212.082 144 52.1683944.681897 -11.03749345.746 144 52.1683584.680961 -11.05749410.043 149 52.1683094.678411 -10.81549584.641 148 52.1682624.676073 -10.30749744.703 149 52.1682094.673757 -10.05249903.262 150 52.1681744.671863 -10.41350032.922 146 52.1681334.669941 -9.72150164.574 146 52.1681684.668236 -12.43750281.664 140 52.1681854.668010 -13.18850297.246 138 52.1682684.667075 -10.17450361.938 137 52.1683014.666693 -9.15050387.996 136 52.1682004.666227 -9.42150422.098 135 52.1681444.664729 -9.66550525.109 137 52.1681334.664196 -10.44450561.645 143 52.1681314.664090 -10.35750568.887 148 52.1681294.663985 -10.34550576.023 148 52.1681274.663881 -10.70250583.141 153 52.1681244.663669 -10.91950597.660 147 52.1681114.663256 -10.34950625.930 142 52.1680694.661579 -10.93650740.809 143 52.1680314.659893 -10.87050856.312 145 52.1680084.658832 -9.22550928.723 146 52.1680024.658398 -11.43850958.660 147 52.1679694.656830 -10.42751066.004 150 52.1679644.656617 -10.25951080.574 155 52.1679624.656404 -10.14551095.156 158 52.1679494.655777 -9.75051138.074 153 52.1679284.654434 -8.39451230.012 151 52.1679184.654113 -9.76451251.961 150 52.1679014.652892 -8.42651335.562 147 52.1678994.652683 -9.83651349.844 145 52.1678934.652477 -11.11951363.926 146 52.1678594.651140 -8.17851455.547 146 52.1678354.650061 -10.63351529.289 146 52.1678214.649544 -9.18151564.785 151 52.1677834.647847 -9.33851681.039 146 52.1677694.647095 -9.71551732.449 145 52.1677174.645078 -9.20051870.602 143 52.1677034.644353 -8.38651920.242 148 52.1676874.643302 -7.98551992.176 143 52.1675374.641851 -6.79152093.016 141 52.1675304.641647 -6.75752106.988 141 52.1675114.641454 -6.02152120.336 141 52.1673404.641039 -4.64452155.238 143 52.1672664.640960 -3.37452165.152 143 52.1672164.640776 -2.22152179.098 143 52.1671644.640503 -2.39452198.395 144 52.1670894.640321 -2.59852213.508 145 52.1668544.639403 0.03352281.957 150 52.1665454.637842 -2.49152394.453 150 52.1665264.637643 -2.26552408.152 150 52.1665374.636962 -3.91252454.809 149 52.1665644.636682 -3.69652474.293 144 52.1666104.636312 -3.89652500.039 141 52.1667144.636055 -3.18852521.621 134 52.1667634.636008 -2.83152527.668 133 52.1667484.635376 -5.46652572.562 131 52.1667034.634648 -6.95552623.336 130 52.1664584.633182 -7.76952726.801 133 52.1661944.631494 -7.46752845.898 138 52.1661784.631389 -6.30152853.359 138 52.1662044.630957 -6.57352883.324 139 52.1661914.630406 -7.25252921.246 139 52.1660634.629666 -7.08252973.891 141 52.1657474.628169 -6.16953082.691 143 52.1653764.625756 -7.41253252.848 144 52.1652354.624823 -7.15353318.578 149 52.1652024.624623 -7.61353332.441 152 52.1651274.624123 -7.37953368.000 147 52.1650794.623208 -5.78653431.348 146 52.1648734.621501 -5.96353551.410 146 52.1645524.619637 -6.88053683.918 146 52.1643074.617829 -6.42853810.645 144 52.1639744.615813 -7.30553953.344 143 52.1636144.613915 -7.22554089.320 143 52.1633744.612401 -7.02954196.559 144 52.1631194.610572 -6.36354324.961 146 52.1630414.609943 -6.87454368.922 152 52.1627844.608106 -7.01854497.836 149 52.1624744.606300 -4.25254626.086 147 52.1624644.606209 -3.45254632.281 147 52.1626934.605341 -1.20754700.566 149 52.1628724.605228 -1.03854721.930 151 52.1641794.604879 -0.20954869.668 152 52.1646714.604741 -1.36454925.262 147 52.1652954.604545 -1.46854995.914 142 52.1659704.604329 -1.19355072.480 137 52.1671604.603877 -1.77055208.625 134 52.1677624.602975 -2.85455300.949 131 52.1680974.601476 -4.11255410.355 132 52.1681134.601360 -4.14955418.312 132 52.1680334.600456 -4.15255481.445 132 52.1676234.599321 -5.49855571.699 134 52.1672864.597440 -4.41955705.805 136 52.1670454.596002 -5.19055807.746 141 52.1668274.594154 -4.08955937.043 142 52.1670804.592576 -3.95556049.645 144 52.1672624.592250 -3.36556079.938 144 52.1674464.591664 -4.12556125.316 144 52.1676894.591176 -3.01556169.434 145 52.1682554.590843 -2.72056236.492 142 52.1683834.590294 -4.40356282.828 142 52.1683544.590071 -4.57356298.406 142 52.1680964.588632 -4.14056400.805 138 52.1680964.588029 -4.29356442.297 140 52.1679264.586518 -3.73056548.066 139 52.1678404.585985 -3.05656584.906 138 52.1677414.585739 -2.18856605.207 137 52.1675434.585483 -3.21756633.926 138 52.1674544.585338 -3.91456648.379 139 52.1671444.584146 -3.93856736.969 140 52.1670934.583967 -4.40656750.172 140 52.1669334.583704 -2.91756776.293 139 52.1667104.583694 -1.74856801.344 139 52.1664794.583783 -1.43656827.758 139 52.1662124.583600 -3.66456861.805 139 52.1654934.582017 -4.62256996.648 140 52.1649304.579679 -3.62557168.555 144 52.1643804.578469 -2.75657272.457 146 52.1635014.577662 -3.01757385.652 145 52.1625974.577250 -3.05457490.188 148 52.1622414.577039 -2.68757532.340 149 52.1620694.576578 -0.93357570.473 150 52.1620304.576359 -1.17457586.070 147 52.1619074.576094 -2.56657609.926 146 52.1613564.575544 -2.22757682.035 143 52.1612124.575361 -2.74857702.391 143 52.1610374.574944 -3.57157737.094 143 52.1607274.573186 -3.74257862.391 144 52.1603694.570929 -3.24458021.988 148 52.1600604.568861 -2.67358167.594 148 52.1598194.567155 -3.00358287.543 147 52.1598244.565118 -1.79558427.641 146 52.1602074.563809 1.03858527.375 148 52.1603514.563512 1.81158553.391 148 52.1611554.562450 2.94358668.957 144 52.1619934.561484 1.80658783.414 141 52.1622194.561338 3.38258810.738 137 52.1624674.561092 2.88358843.598 135 52.1625774.560891 4.24458861.824 135 52.1627644.560561 3.10558894.566 135 52.1627614.560113 4.60758927.809 131 52.1630314.559535 5.10358977.656 136 52.1633484.558918 6.01659032.664 141 52.1640344.557538 5.04759154.078 140 52.1641144.557394 3.77359167.371 140 52.1643874.557286 4.96859200.039 137 52.1646664.557503 4.10859235.332 133 52.1648634.557817 3.53159266.086 126 52.1649524.558125 2.91759289.641 121 52.1648534.558425 1.38359313.840 116 52.1645704.558410 -0.40859346.387 114 52.1642414.558131 -1.41459387.551 116 52.1638794.557653 -3.25859439.488 116 52.1638394.557555 -2.98759447.766 116 52.1636964.557110 -2.47959482.141 118 52.1636764.556646 -2.92759514.020 118 52.1638674.556160 -1.73359556.781 118 52.1639294.555844 -1.99559583.906 117 52.1637644.555376 -3.78859621.531 122 52.1636964.555200 -3.38259635.824 126 52.1634274.554491 -2.74559692.816 131 52.1630794.553505 -1.90659770.613 136 52.1628404.552601 -1.91759838.047 141 52.1627094.551952 -0.81059884.504 144 52.1625674.551680 0.36759909.348 146 52.1623224.550890 -1.59659971.922 140 52.1622224.550647 -1.58359991.848 140 52.1620034.550265 -1.18960026.914 134 52.1619684.550206 -1.41160032.520 132 52.1619644.549822 -1.31360062.523 126 -1.60260073.609 124 52.1620084.549397 -1.18760097.016 123 52.1614714.548632 -0.81560176.594 127 52.1614004.548491 -0.41860188.969 127 52.1609544.548382 1.32360243.086 132 52.1605564.548139 1.47260292.590 135 52.1600504.546935 -0.01660392.871 136 52.1593874.544874 -0.92160551.926 134 52.1588774.543199 -0.55660679.871 133 52.1586604.542371 -0.76160741.395 140 52.1585914.542126 -0.53660759.719 135 52.1585704.542049 -0.96960765.504 134 52.1584884.541638 -0.88260795.719 132 52.1580674.540529 -1.72460885.262 138 52.1576744.539322 -1.25860978.625 138 52.1576204.538925 -0.19861006.480 138 52.1574864.538467 -0.24861041.398 139 52.1572464.537961 -1.61161085.227 139 52.1571504.537592 -1.11461112.738 139 52.1569474.536576 0.55761185.070 136 52.1569554.536391 1.40761197.637 131 52.1569544.536336 1.70761201.207 128 52.1568814.536036 0.24661221.133 127 52.1568614.535962 0.10461227.023 127 52.1567614.535730 0.08461246.539 127 52.1565864.535160 -0.29361290.488 132 52.1563504.534295 0.16761355.270 137 52.1559484.532809 -0.14161466.238 138 52.1558994.532643 -0.34961478.734 139 52.1558604.532489 0.62661489.664 138 52.1559174.532014 0.09261525.488 138 52.1558934.531939 -0.82261531.219 139 52.1552554.530295 0.78861664.508 140 52.1546904.528888 -0.02661779.469 141 52.1541464.527516 0.71361891.168 141 52.1537274.526355 0.82261982.797 137 52.1535914.525837 1.59862021.266 137 52.1534924.525575 0.99762043.000 137 52.1531834.524980 0.88862096.383 144 52.1530654.524692 1.15862120.027 139 52.1528494.524092 0.72662167.488 136 52.1525864.523569 1.73062215.418 134 52.1525594.523520 1.44462219.906 134 52.1522654.522428 3.63062302.027 137 52.1521794.521409 3.33262373.066 132 52.1524364.521165 3.36862413.348 131 52.1525324.521482 2.26162438.418 130 52.1525494.521673 2.40262451.051 130 52.1527654.521830 2.02962483.516 128 52.1538134.521397 1.17062604.395 125 52.1544264.520308 0.95262706.305 121 52.1549624.518958 0.99062816.590 123 52.1551894.517759 0.91562902.438 125 52.1553264.516648 2.71862979.586 124 52.1553594.516311 2.93763003.293 121 52.1553664.516065 2.08763020.230 122 52.1554514.514753 0.98663110.492 121 52.1552194.514222 1.22763161.836 119 52.1550804.514166 1.25163177.695 119 52.1548864.514089 1.45863200.371 121 52.1547224.514053 1.49963218.465 123 52.1545504.514269 3.41963250.922 121 52.1545664.514301 3.77263253.266 121 52.1546444.514393 4.26563263.984 121 52.1547474.514282 4.10263279.840 118 52.1547924.513436 4.00563338.270 122 52.1547454.513320 4.57763347.727 123 52.1546334.513111 2.97563369.039 122 52.1538574.512928 1.88463456.891 124 52.1531464.512836 1.48363536.145 126 52.1521734.512388 2.14963649.129 129 52.1516714.512063 2.15963709.297 130 52.1512604.511665 2.01763762.699 130 52.1503624.510933 2.13963875.027 128 52.1502144.510778 1.89563894.504 127 52.1500104.510465 2.45563926.457 127 52.1496684.510018 3.15963974.539 122 52.1496124.509778 3.36463995.266 117 52.1497064.509592 2.58064011.211 117 52.1500524.508905 2.06464072.137 118 52.1502214.508525 1.46464104.246 118 52.1502034.508107 1.03664134.801 119 52.1499554.507584 1.04264179.988 126 52.1493634.506292 0.89964290.266 127 52.1489124.505152 1.49864382.977 129 52.1486424.504251 1.60764451.074 130 52.1486554.504016 2.33064469.062 130 52.1492444.503194 2.08064555.297 126 52.1495914.502560 2.08664614.906 128 52.1495424.502316 2.44364632.984 129 52.1493174.501910 2.80364670.188 127 EDGE7053420061559625 2500 EDGE705 2500 Release EN006-B0625-00 stravalib-2.2/src/stravalib/tests/resources/strava_swagger.json000066400000000000000000003103661475174155400252360ustar00rootroot00000000000000{ "swagger": "2.0", "info": { "title": "Strava API v3", "description": "The [Swagger Playground](https://developers.strava.com/playground) is the easiest way to familiarize yourself with the Strava API by submitting HTTP requests and observing the responses before you write any client code. It will show what a response will look like with different endpoints depending on the authorization scope you receive from your athletes. To use the Playground, go to https://www.strava.com/settings/api and change your β€œAuthorization Callback Domain” to developers.strava.com. Please note, we only support Swagger 2.0. There is a known issue where you can only select one scope at a time. For more information, please check the section β€œclient code” at https://developers.strava.com/docs.", "version": "3.0.0" }, "host": "www.strava.com", "schemes": [ "https" ], "basePath": "/api/v3", "produces": [ "application/json" ], "securityDefinitions": { "strava_oauth": { "type": "oauth2", "flow": "accessCode", "authorizationUrl": "https://www.strava.com/api/v3/oauth/authorize", "tokenUrl": "https://www.strava.com/api/v3/oauth/token", "scopes": { "read": "Read public segments, public routes, public profile data, public posts, public events, club feeds, and leaderboards", "read_all": "Read private routes, private segments, and private events for the user", "profile:read_all": "Read all profile information even if the user has set their profile visibility to Followers or Only You", "profile:write": "Update the user's weight and Functional Threshold Power (FTP), and access to star or unstar segments on their behalf", "activity:read": "Read the user's activity data for activities that are visible to Everyone and Followers, excluding privacy zone data", "activity:read_all": "The same access as activity:read, plus privacy zone data and access to read the user's activities with visibility set to Only You", "activity:write": "Access to create manual activities and uploads, and access to edit any activities that are visible to the app, based on activity read access level" } } }, "security": [ { "strava_oauth": [ "public" ] } ], "parameters": { "page": { "name": "page", "in": "query", "description": "Page number. Defaults to 1.", "type": "integer" }, "perPage": { "name": "per_page", "in": "query", "description": "Number of items per page. Defaults to 30.", "type": "integer", "default": 30 } }, "paths": { "/athletes/{id}/stats": { "get": { "operationId": "getStats", "summary": "Get Athlete Stats", "description": "Returns the activity stats of an athlete. Only includes data from activities set to Everyone visibilty.", "parameters": [ { "name": "id", "in": "path", "description": "The identifier of the athlete. Must match the authenticated athlete.", "required": true, "type": "integer", "format": "int64" } ], "tags": [ "Athletes" ], "responses": { "200": { "description": "Activity stats of the athlete.", "schema": { "$ref": "https://developers.strava.com/swagger/activity_stats.json#/ActivityStats" } }, "default": { "description": "Unexpected error.", "schema": { "$ref": "https://developers.strava.com/swagger/fault.json#/Fault" } } } } }, "/athlete": { "get": { "operationId": "getLoggedInAthlete", "summary": "Get Authenticated Athlete", "description": "Returns the currently authenticated athlete. Tokens with profile:read_all scope will receive a detailed athlete representation; all others will receive a summary representation.", "tags": [ "Athletes" ], "responses": { "200": { "description": "Profile information for the authenticated athlete.", "schema": { "$ref": "https://developers.strava.com/swagger/athlete.json#/DetailedAthlete" }, "examples": { "application/json": { "id": 1234567890987654321, "username": "marianne_t", "resource_state": 3, "firstname": "Marianne", "lastname": "Teutenberg", "city": "San Francisco", "state": "CA", "country": "US", "sex": "F", "premium": true, "created_at": "2017-11-14T02:30:05Z", "updated_at": "2018-02-06T19:32:20Z", "badge_type_id": 4, "profile_medium": "https://xxxxxx.cloudfront.net/pictures/athletes/123456789/123456789/2/medium.jpg", "profile": "https://xxxxx.cloudfront.net/pictures/athletes/123456789/123456789/2/large.jpg", "friend": null, "follower": null, "follower_count": 5, "friend_count": 5, "mutual_friend_count": 0, "athlete_type": 1, "date_preference": "%m/%d/%Y", "measurement_preference": "feet", "clubs": [], "ftp": null, "weight": 0, "bikes": [ { "id": "b12345678987655", "primary": true, "name": "EMC", "resource_state": 2, "distance": 0 } ], "shoes": [ { "id": "g12345678987655", "primary": true, "name": "adidas", "resource_state": 2, "distance": 4904 } ] } } }, "default": { "description": "Unexpected error.", "schema": { "$ref": "https://developers.strava.com/swagger/fault.json#/Fault" } } } }, "put": { "operationId": "updateLoggedInAthlete", "summary": "Update Athlete", "description": "Update the currently authenticated athlete. Requires profile:write scope.", "consumes": [ "multipart/form-data" ], "tags": [ "Athletes" ], "parameters": [ { "name": "weight", "in": "path", "description": "The weight of the athlete in kilograms.", "required": true, "type": "number", "format": "float" } ], "responses": { "200": { "description": "Profile information for the authenticated athlete.", "schema": { "$ref": "https://developers.strava.com/swagger/athlete.json#/DetailedAthlete" }, "examples": { "application/json": { "id": 12345678987655098765444, "username": "marianne_v", "resource_state": 3, "firstname": "Marianne", "lastname": "V.", "city": "San Francisco", "state": "CA", "country": "US", "sex": "F", "premium": true, "created_at": "2017-11-14T02:30:05Z", "updated_at": "2018-02-06T19:32:20Z", "badge_type_id": 4, "profile_medium": "https://xxxxxx.cloudfront.net/pictures/athletes/1234567898765509876/1234567898765509876/2/medium.jpg", "profile": "https://xxxxx.cloudfront.net/pictures/athletes/1234567898765509876/1234567898765509876/2/large.jpg", "friend": null, "follower": null, "follower_count": 5, "friend_count": 5, "mutual_friend_count": 0, "athlete_type": 1, "date_preference": "%m/%d/%Y", "measurement_preference": "feet", "clubs": [], "ftp": null, "weight": 0, "bikes": [ { "id": "b1234567898765509876", "primary": true, "name": "EMC", "resource_state": 2, "distance": 0 } ], "shoes": [ { "id": "g1234567898765509876", "primary": true, "name": "adidas", "resource_state": 2, "distance": 4904 } ] } } }, "default": { "description": "Unexpected error.", "schema": { "$ref": "https://developers.strava.com/swagger/fault.json#/Fault" } } } } }, "/athlete/zones": { "get": { "operationId": "getLoggedInAthleteZones", "summary": "Get Zones", "description": "Returns the the authenticated athlete's heart rate and power zones. Requires profile:read_all.", "tags": [ "Athletes" ], "responses": { "200": { "description": "Heart rate and power zones.", "schema": { "$ref": "https://developers.strava.com/swagger/zones.json#/Zones" }, "examples": { "application/json": [ { "distribution_buckets": [ { "max": 0, "min": 0, "time": 1498 }, { "max": 50, "min": 0, "time": 62 }, { "max": 100, "min": 50, "time": 169 }, { "max": 150, "min": 100, "time": 536 }, { "max": 200, "min": 150, "time": 672 }, { "max": 250, "min": 200, "time": 821 }, { "max": 300, "min": 250, "time": 529 }, { "max": 350, "min": 300, "time": 251 }, { "max": 400, "min": 350, "time": 80 }, { "max": 450, "min": 400, "time": 81 }, { "max": -1, "min": 450, "time": 343 } ], "type": "power", "resource_state": 3, "sensor_based": true } ] } }, "default": { "description": "Unexpected error.", "schema": { "$ref": "https://developers.strava.com/swagger/fault.json#/Fault" } } } } }, "/segments/{id}": { "get": { "operationId": "getSegmentById", "summary": "Get Segment", "description": "Returns the specified segment. read_all scope required in order to retrieve athlete-specific segment information, or to retrieve private segments.", "parameters": [ { "name": "id", "in": "path", "description": "The identifier of the segment.", "required": true, "type": "integer", "format": "int64" } ], "tags": [ "Segments" ], "responses": { "200": { "description": "Representation of a segment.", "schema": { "$ref": "https://developers.strava.com/swagger/segment.json#/DetailedSegment" }, "examples": { "application/json": { "id": 229781, "resource_state": 3, "name": "Hawk Hill", "activity_type": "Ride", "distance": 2684.82, "average_grade": 5.7, "maximum_grade": 14.2, "elevation_high": 245.3, "elevation_low": 92.4, "start_latlng": [ 37.8331119, -122.4834356 ], "end_latlng": [ 37.8280722, -122.4981393 ], "climb_category": 1, "city": "San Francisco", "state": "CA", "country": "United States", "private": false, "hazardous": false, "starred": false, "created_at": "2009-09-21T20:29:41Z", "updated_at": "2018-02-15T09:04:18Z", "total_elevation_gain": 155.733, "map": { "id": "s229781", "polyline": "}g|eFnpqjVl@En@Md@HbAd@d@^h@Xx@VbARjBDh@OPQf@w@d@k@XKXDFPH\\EbGT`AV`@v@|@NTNb@?XOb@cAxAWLuE@eAFMBoAv@eBt@q@b@}@tAeAt@i@dAC`AFZj@dB?~@[h@MbAVn@b@b@\\d@Eh@Qb@_@d@eB|@c@h@WfBK|AMpA?VF\\\\t@f@t@h@j@|@b@hCb@b@XTd@Bl@GtA?jAL`ALp@Tr@RXd@Rx@Pn@^Zh@Tx@Zf@`@FTCzDy@f@Yx@m@n@Op@VJr@", "resource_state": 3 }, "effort_count": 309974, "athlete_count": 30623, "star_count": 2428, "athlete_segment_stats": { "pr_elapsed_time": 553, "pr_date": "1993-04-03", "effort_count": 2 } } } }, "default": { "description": "Unexpected error.", "schema": { "$ref": "https://developers.strava.com/swagger/fault.json#/Fault" } } } } }, "/segments/starred": { "get": { "operationId": "getLoggedInAthleteStarredSegments", "summary": "List Starred Segments", "description": "List of the authenticated athlete's starred segments. Private segments are filtered out unless requested by a token with read_all scope.", "parameters": [ { "$ref": "#/parameters/page" }, { "$ref": "#/parameters/perPage" } ], "tags": [ "Segments" ], "responses": { "200": { "description": "List of the authenticated athlete's starred segments.", "schema": { "type": "array", "items": { "$ref": "https://developers.strava.com/swagger/segment.json#/SummarySegment" } }, "examples": { "application/json": { "id": 229781, "resource_state": 3, "name": "Hawk Hill", "activity_type": "Ride", "distance": 2684.82, "average_grade": 5.7, "maximum_grade": 14.2, "elevation_high": 245.3, "elevation_low": 92.4, "start_latlng": [ 37.8331119, -122.4834356 ], "end_latlng": [ 37.8280722, -122.4981393 ], "climb_category": 1, "city": "San Francisco", "state": "CA", "country": "United States", "private": false, "hazardous": false, "starred": false, "created_at": "2009-09-21T20:29:41Z", "updated_at": "2018-02-15T09:04:18Z", "total_elevation_gain": 155.733, "map": { "id": "s229781", "polyline": "}g|eFnpqjVl@En@Md@HbAd@d@^h@Xx@VbARjBDh@OPQf@w@d@k@XKXDFPH\\EbGT`AV`@v@|@NTNb@?XOb@cAxAWLuE@eAFMBoAv@eBt@q@b@}@tAeAt@i@dAC`AFZj@dB?~@[h@MbAVn@b@b@\\d@Eh@Qb@_@d@eB|@c@h@WfBK|AMpA?VF\\\\t@f@t@h@j@|@b@hCb@b@XTd@Bl@GtA?jAL`ALp@Tr@RXd@Rx@Pn@^Zh@Tx@Zf@`@FTCzDy@f@Yx@m@n@Op@VJr@", "resource_state": 3 }, "effort_count": 309974, "athlete_count": 30623, "star_count": 2428, "athlete_segment_stats": { "pr_elapsed_time": 553, "pr_date": "1993-04-03", "effort_count": 2 } } } }, "default": { "description": "Unexpected error.", "schema": { "$ref": "https://developers.strava.com/swagger/fault.json#/Fault" } } } } }, "/segments/{id}/starred": { "put": { "operationId": "starSegment", "summary": "Star Segment", "description": "Stars/Unstars the given segment for the authenticated athlete. Requires profile:write scope.", "parameters": [ { "name": "id", "in": "path", "description": "The identifier of the segment to star.", "required": true, "type": "integer", "format": "int64" }, { "name": "starred", "in": "formData", "description": "If true, star the segment; if false, unstar the segment.", "type": "boolean", "required": true, "default": false } ], "tags": [ "Segments" ], "responses": { "200": { "description": "Representation of a segment.", "schema": { "$ref": "https://developers.strava.com/swagger/segment.json#/DetailedSegment" }, "examples": { "application/json": { "id": 229781, "resource_state": 3, "name": "Hawk Hill", "activity_type": "Ride", "distance": 2684.82, "average_grade": 5.7, "maximum_grade": 14.2, "elevation_high": 245.3, "elevation_low": 92.4, "start_latlng": [ 37.8331119, -122.4834356 ], "end_latlng": [ 37.8280722, -122.4981393 ], "climb_category": 1, "city": "San Francisco", "state": "CA", "country": "United States", "private": false, "hazardous": false, "starred": false, "created_at": "2009-09-21T20:29:41Z", "updated_at": "2018-02-15T09:04:18Z", "total_elevation_gain": 155.733, "map": { "id": "s229781", "polyline": "}g|eFnpqjVl@En@Md@HbAd@d@^h@Xx@VbARjBDh@OPQf@w@d@k@XKXDFPH\\EbGT`AV`@v@|@NTNb@?XOb@cAxAWLuE@eAFMBoAv@eBt@q@b@}@tAeAt@i@dAC`AFZj@dB?~@[h@MbAVn@b@b@\\d@Eh@Qb@_@d@eB|@c@h@WfBK|AMpA?VF\\\\t@f@t@h@j@|@b@hCb@b@XTd@Bl@GtA?jAL`ALp@Tr@RXd@Rx@Pn@^Zh@Tx@Zf@`@FTCzDy@f@Yx@m@n@Op@VJr@", "resource_state": 3 }, "effort_count": 309974, "athlete_count": 30623, "star_count": 2428, "athlete_segment_stats": { "pr_elapsed_time": 553, "pr_date": "1993-04-03", "effort_count": 2 } } } }, "default": { "description": "Unexpected error.", "schema": { "$ref": "https://developers.strava.com/swagger/fault.json#/Fault" } } } } }, "/segment_efforts": { "get": { "operationId": "getEffortsBySegmentId", "summary": "List Segment Efforts", "description": "Returns a set of the authenticated athlete's segment efforts for a given segment. Requires subscription.", "parameters": [ { "name": "segment_id", "in": "query", "description": "The identifier of the segment.", "required": true, "type": "integer" }, { "name": "start_date_local", "in": "query", "description": "ISO 8601 formatted date time.", "type": "string", "format": "date-time" }, { "name": "end_date_local", "in": "query", "description": "ISO 8601 formatted date time.", "type": "string", "format": "date-time" }, { "$ref": "#/parameters/perPage", "in": "query" } ], "tags": [ "SegmentEfforts" ], "responses": { "200": { "description": "List of segment efforts.", "schema": { "type": "array", "items": { "$ref": "https://developers.strava.com/swagger/segment_effort.json#/DetailedSegmentEffort" } }, "examples": { "application/json": [ { "id": 123456789, "resource_state": 2, "name": "Alpe d'Huez", "activity": { "id": 1234567890, "resource_state": 1 }, "athlete": { "id": 123445678689, "resource_state": 1 }, "elapsed_time": 1657, "moving_time": 1642, "start_date": "2007-09-15T08:15:29Z", "start_date_local": "2007-09-15T09:15:29Z", "distance": 6148.92, "start_index": 1102, "end_index": 1366, "device_watts": false, "average_watts": 220.2, "segment": { "id": 788127, "resource_state": 2, "name": "Alpe d'Huez", "activity_type": "Ride", "distance": 6297.46, "average_grade": 4.8, "maximum_grade": 16.3, "elevation_high": 416, "elevation_low": 104.6, "start_latlng": [ 52.98501000581467, -3.1869720001197367 ], "end_latlng": [ 53.02204074375785, -3.2039630001245737 ], "climb_category": 2, "city": "Le Bourg D'Oisans", "state": "RA", "country": "France", "private": false, "hazardous": false, "starred": false }, "kom_rank": null, "pr_rank": null, "achievements": [] } ] } }, "default": { "description": "Unexpected error.", "schema": { "$ref": "https://developers.strava.com/swagger/fault.json#/Fault" } } } } }, "/segments/explore": { "get": { "operationId": "exploreSegments", "summary": "Explore segments", "description": "Returns the top 10 segments matching a specified query.", "parameters": [ { "name": "bounds", "in": "query", "description": "The latitude and longitude for two points describing a rectangular boundary for the search: [southwest corner latitutde, southwest corner longitude, northeast corner latitude, northeast corner longitude]", "required": true, "type": "array", "items": { "type": "number", "format": "float" }, "collectionFormat": "csv", "minItems": 4, "maxItems": 4 }, { "name": "activity_type", "in": "query", "description": "Desired activity type.", "type": "string", "enum": [ "running", "riding" ] }, { "name": "min_cat", "in": "query", "description": "The minimum climbing category.", "type": "integer", "minimum": 0, "maximum": 5 }, { "name": "max_cat", "in": "query", "description": "The maximum climbing category.", "type": "integer", "minimum": 0, "maximum": 5 } ], "tags": [ "Segments" ], "responses": { "200": { "description": "List of matching segments.", "schema": { "$ref": "https://developers.strava.com/swagger/segment.json#/ExplorerResponse" }, "examples": { "application/json": { "segments": [ { "id": 229781, "resource_state": 2, "name": "Hawk Hill", "climb_category": 1, "climb_category_desc": "4", "avg_grade": 5.7, "start_latlng": [ 37.8331119, -122.4834356 ], "end_latlng": [ 37.8280722, -122.4981393 ], "elev_difference": 152.8, "distance": 2684.8, "points": "}g|eFnpqjVl@En@Md@HbAd@d@^h@Xx@VbARjBDh@OPQf@w@d@k@XKXDFPH\\EbGT`AV`@v@|@NTNb@?XOb@cAxAWLuE@eAFMBoAv@eBt@q@b@}@tAeAt@i@dAC`AFZj@dB?~@[h@MbAVn@b@b@\\d@Eh@Qb@_@d@eB|@c@h@WfBK|AMpA?VF\\\\t@f@t@h@j@|@b@hCb@b@XTd@Bl@GtA?jAL`ALp@Tr@RXd@Rx@Pn@^Zh@Tx@Zf@`@FTCzDy@f@Yx@m@n@Op@VJr@", "starred": false } ] } } }, "default": { "description": "Unexpected error.", "schema": { "$ref": "https://developers.strava.com/swagger/fault.json#/Fault" } } } } }, "/segment_efforts/{id}": { "get": { "operationId": "getSegmentEffortById", "summary": "Get Segment Effort", "description": "Returns a segment effort from an activity that is owned by the authenticated athlete. Requires subscription.", "parameters": [ { "name": "id", "in": "path", "description": "The identifier of the segment effort.", "required": true, "type": "integer", "format": "int64" } ], "tags": [ "SegmentEfforts" ], "responses": { "200": { "description": "Representation of a segment effort.", "schema": { "$ref": "https://developers.strava.com/swagger/segment_effort.json#/DetailedSegmentEffort" }, "examples": { "application/json": { "id": 1234556789, "resource_state": 3, "name": "Alpe d'Huez", "activity": { "id": 3454504, "resource_state": 1 }, "athlete": { "id": 54321, "resource_state": 1 }, "elapsed_time": 381, "moving_time": 340, "start_date": "2018-02-12T16:12:41Z", "start_date_local": "2018-02-12T08:12:41Z", "distance": 83, "start_index": 65, "end_index": 83, "segment": { "id": 63450, "resource_state": 2, "name": "Alpe d'Huez", "activity_type": "Run", "distance": 780.35, "average_grade": -0.5, "maximum_grade": 0, "elevation_high": 21, "elevation_low": 17.2, "start_latlng": [ 37.808407654682, -122.426682919323 ], "end_latlng": [ 37.808297909724, -122.421324329674 ], "climb_category": 0, "city": "San Francisco", "state": "CA", "country": "United States", "private": false, "hazardous": false, "starred": false }, "kom_rank": null, "pr_rank": null, "achievements": [], "athlete_segment_stats": { "pr_elapsed_time": 212, "pr_date": "2015-02-12", "effort_count": 149 } } } }, "default": { "description": "Unexpected error.", "schema": { "$ref": "https://developers.strava.com/swagger/fault.json#/Fault" } } } } }, "/activities": { "post": { "operationId": "createActivity", "summary": "Create an Activity", "description": "Creates a manual activity for an athlete, requires activity:write scope.", "parameters": [ { "name": "name", "in": "formData", "description": "The name of the activity.", "required": true, "type": "string" }, { "name": "type", "in": "formData", "description": "Type of activity. For example - Run, Ride etc.", "type": "string", "required": false }, { "name": "sport_type", "in": "formData", "description": "Sport type of activity. For example - Run, MountainBikeRide, Ride, etc.", "type": "string", "required": true }, { "name": "start_date_local", "in": "formData", "description": "ISO 8601 formatted date time.", "type": "string", "format": "date-time", "required": true }, { "name": "elapsed_time", "in": "formData", "description": "In seconds.", "type": "integer", "required": true }, { "name": "description", "in": "formData", "description": "Description of the activity.", "type": "string", "required": false }, { "name": "distance", "in": "formData", "description": "In meters.", "type": "number", "format": "float", "required": false }, { "name": "trainer", "in": "formData", "description": "Set to 1 to mark as a trainer activity.", "type": "integer", "required": false }, { "name": "commute", "in": "formData", "description": "Set to 1 to mark as commute.", "type": "integer", "required": false } ], "tags": [ "Activities" ], "responses": { "201": { "description": "The activity's detailed representation.", "schema": { "$ref": "https://developers.strava.com/swagger/activity.json#/DetailedActivity" }, "examples": { "application/json": { "id": 123456778928065, "resource_state": 3, "external_id": null, "upload_id": null, "athlete": { "id": 12343545645788, "resource_state": 1 }, "name": "Chill Day", "distance": 0, "moving_time": 18373, "elapsed_time": 18373, "total_elevation_gain": 0, "type": "Ride", "sport_type": "MountainBikeRide", "start_date": "2018-02-20T18:02:13Z", "start_date_local": "2018-02-20T10:02:13Z", "timezone": "(GMT-08:00) America/Los_Angeles", "utc_offset": -28800, "achievement_count": 0, "kudos_count": 0, "comment_count": 0, "athlete_count": 1, "photo_count": 0, "map": { "id": "a12345678908766", "polyline": null, "resource_state": 3 }, "trainer": false, "commute": false, "manual": true, "private": false, "flagged": false, "gear_id": "b453542543", "from_accepted_tag": null, "average_speed": 0, "max_speed": 0, "device_watts": false, "has_heartrate": false, "pr_count": 0, "total_photo_count": 0, "has_kudoed": false, "workout_type": null, "description": null, "calories": 0, "segment_efforts": [] } } }, "default": { "description": "Unexpected error.", "schema": { "$ref": "https://developers.strava.com/swagger/fault.json#/Fault" } } } } }, "/activities/{id}": { "get": { "operationId": "getActivityById", "summary": "Get Activity", "description": "Returns the given activity that is owned by the authenticated athlete. Requires activity:read for Everyone and Followers activities. Requires activity:read_all for Only Me activities.", "parameters": [ { "name": "id", "in": "path", "description": "The identifier of the activity.", "required": true, "type": "integer", "format": "int64" }, { "name": "include_all_efforts", "in": "query", "description": "To include all segments efforts.", "type": "boolean" } ], "tags": [ "Activities" ], "responses": { "200": { "description": "The activity's detailed representation.", "schema": { "$ref": "https://developers.strava.com/swagger/activity.json#/DetailedActivity" }, "examples": { "application/json": { "id": 12345678987654321, "resource_state": 3, "external_id": "garmin_push_12345678987654321", "upload_id": 98765432123456789, "athlete": { "id": 134815, "resource_state": 1 }, "name": "Happy Friday", "distance": 28099, "moving_time": 4207, "elapsed_time": 4410, "total_elevation_gain": 516, "type": "Ride", "sport_type": "MountainBikeRide", "start_date": "2018-02-16T14:52:54Z", "start_date_local": "2018-02-16T06:52:54Z", "timezone": "(GMT-08:00) America/Los_Angeles", "utc_offset": -28800, "start_latlng": [ 37.83, -122.26 ], "end_latlng": [ 37.83, -122.26 ], "achievement_count": 0, "kudos_count": 19, "comment_count": 0, "athlete_count": 1, "photo_count": 0, "map": { "id": "a1410355832", "polyline": "ki{eFvqfiVqAWQIGEEKAYJgBVqDJ{BHa@jAkNJw@Pw@V{APs@^aABQAOEQGKoJ_FuJkFqAo@{A}@sH{DiAs@Q]?WVy@`@oBt@_CB]KYMMkB{AQEI@WT{BlE{@zAQPI@ICsCqA_BcAeCmAaFmCqIoEcLeG}KcG}A}@cDaBiDsByAkAuBqBi@y@_@o@o@kB}BgIoA_EUkAMcACa@BeBBq@LaAJe@b@uA`@_AdBcD`@iAPq@RgALqAB{@EqAyAoOCy@AmCBmANqBLqAZkB\\iCPiBJwCCsASiCq@iD]eA]y@[i@w@mAa@i@k@g@kAw@i@Ya@Q]EWFMLa@~BYpAFNpA`Aj@n@X`@V`AHh@JfB@xAMvAGZGHIDIAWOEQNcC@sACYK[MSOMe@QKKKYOs@UYQISCQ?Q@WNo@r@OHGAGCKOQ_BU}@MQGG]Io@@c@FYNg@d@s@d@ODQAMOMaASs@_@a@SESAQDqBn@a@RO?KK?UBU\\kA@Y?WMo@Iy@GWQ_@WSSGg@AkABQB_Ap@_A^o@b@Q@o@IS@OHi@n@OFS?OI}@iAQMQGQC}@DOIIUK{@IUOMyBo@kASOKIQCa@L[|AgATWN[He@?QKw@FOPCh@Fx@l@TDLELKl@aAHIJEX@r@ZTDV@LENQVg@RkA@c@MeA?WFOPMf@Ej@Fj@@LGHKDM?_@_@iC?a@HKRIl@NT?FCHMFW?YEYGWQa@GYBiAIq@Gq@L_BHSHK|@WJETSLQZs@z@_A~@uA^U`@G\\CRB\\Tl@p@Th@JZ^bB`@lAHLXVLDP?LGFSKiDBo@d@wBVi@R]VYVE\\@`@Lh@Fh@CzAk@RSDQA]GYe@eAGWSiBAWBWBIJORK`@KPOPSTg@h@}Ad@o@F[E_@EGMKUGmAEYGMIMYKs@?a@J}@@_BD_@HQJMx@e@LKHKHWAo@UoAAWFmAH}@?w@C[YwAAc@HSNM|Ao@rA}@zAq@`@a@j@eAxAuBXQj@MXSR[b@gAFg@?YISOGaAHi@Xw@v@_@d@WRSFqARUHQJc@d@m@`A[VSFUBcAEU@WFULUPa@v@Y~@UrBc@dBI~@?l@P~ABt@N`HEjA]zAEp@@p@TrBCl@CTQb@k@dAg@jAU^KJYLK@k@A[Js@d@a@b@]RgBl@[FMAw@[]G]?m@D_@F]P[Vu@t@[TMF_@Do@E_@@q@P]PWZUZw@vAkAlAGJOj@IlAMd@OR{@p@a@d@sBpD]v@a@`Aa@n@]TODgBVk@Pe@^cBfBc@Rs@La@RSPm@|@wCpDS^Wp@QZML{@l@qBbCYd@k@lAIVCZBZNTr@`@RRHZANIZQPKDW@e@CaASU?I@YTKRQx@@\\VmALYRQLCL?v@P|@D\\GJEFKDM@OCa@COOYIGm@YMUCM@]JYr@uAx@kAt@}@jAeAPWbAkBj@s@bAiAz@oAj@m@VQlAc@VQ~@aA`Au@p@Q`AIv@MZORUV_@p@iB|AoCh@q@dAaANUNWH[N{AJ[^m@t@_Av@wA\\a@`@W`@In@Al@B^E`@Wl@u@\\[VQ\\K`@Eb@?R@dAZP@d@CRExAs@\\Yt@{@LG\\MjAATINOXo@d@kAl@_AHYBOCe@QiBCm@Fq@\\wADo@AyGEeBWuB@YHu@Tu@Lk@VcCTo@d@aA\\WJE`@G~@FP?VI\\U~@sANO`@SfAMj@U\\WjAsAXS`@UNENALBHFFL?^Ml@Uj@]b@q@RUJSPkChEc@XcAb@sA|@]PaA\\OJKNER?TDTNj@Jn@?p@OfC@ZR`B@VCV_@n@{@l@WbACv@OlABnAPl@LNNHbBBNBLFFJ@^GLg@x@i@|AMP[X}@XOJKPET?l@LhAFXp@fBDRCd@S\\_@Ps@PQ@}A]S?QDe@V]b@MR[fAKt@ErAF~CANILYDKGIKe@{@Yy@e@sB[gA[c@e@YUCU?WBUHUNQPq@`AiArAMV[^e@Zc@JQJKNMz@?r@Bb@PfAAfA@VVbADn@E`@KHSEe@SMAKDKFM\\^dDCh@m@LoAQ_@@MFOZLfBEl@QbASd@KLQBOAaAc@QAQ@QHc@v@ONMJOBOCg@c@]O[EMBKFGL?RHv@ARERGNe@h@{@h@WVGNDt@JLNFPFz@LdBf@f@PJNHPF`ADPJJJDl@I`@B^Tp@bALJNDNALIf@i@PGPCt@DNE`@Uv@[dAw@RITGRCtAARBPJLPJRZxB?VEX_@vAAR?RDNHJJBh@UnBm@h@IRDRJNNJPNbBFRJLLBLCzAmAd@Uf@Gf@?P@PFJNHPFTH`BDTHNJJJ@LG`@m@^YPER@RDPHNNJRLn@HRLN^VNPHTFX@\\UlDFb@FHh@NP@HKPsB?}ASkCQ{@[y@q@}@cA{@KOCQDa@t@{CFGJCf@Nl@ZtA~@r@p@`@h@rAxBd@rA\\fARdAPjANrB?f@AtBCd@QfBkAjJOlBChA?rBFrBNlBdAfKFzAC~@Iz@Mz@Sv@s@jBmAxBi@hAWt@Sv@Qx@O`BA`@?dAPfBVpAd@`BfBlFf@fBdA~Cr@pAz@fApBhBjAt@H?IL?FBFJLx@^lHvDvh@~XnElCbAd@pGhDbAb@nAr@`Ad@`GhDnBbAxCbBrWhNJJDPARGP_@t@Qh@]pAUtAoA`Ny@jJApBBNFLJFJBv@Hb@HBF?\\", "resource_state": 3, "summary_polyline": "ki{eFvqfiVsBmA`Feh@qg@iX`B}JeCcCqGjIq~@kf@cM{KeHeX`@_GdGkSeBiXtB}YuEkPwFyDeAzAe@pC~DfGc@bIOsGmCcEiD~@oBuEkFhBcBmDiEfAVuDiAuD}NnDaNiIlCyDD_CtJKv@wGhD]YyEzBo@g@uKxGmHpCGtEtI~AuLrHkAcAaIvEgH_EaDR_FpBuBg@sNxHqEtHgLoTpIiCzKNr[sB|Es\\`JyObYeMbGsMnPsAfDxAnD}DBu@bCx@{BbEEyAoD`AmChNoQzMoGhOwX|[yIzBeFKg[zAkIdU_LiHxK}HzEh@vM_BtBg@xGzDbCcF~GhArHaIfByAhLsDiJuC?_HbHd@nL_Cz@ZnEkDDy@hHwJLiCbIrNrIvN_EfAjDWlEnEiAfBxDlFkBfBtEfDaAzBvDKdFx@|@XgJmDsHhAgD`GfElEzOwBnYdBxXgGlSc@bGdHpW|HdJztBnhAgFxc@HnCvBdA" }, "trainer": false, "commute": false, "manual": false, "private": false, "flagged": false, "gear_id": "b12345678987654321", "from_accepted_tag": false, "average_speed": 6.679, "max_speed": 18.5, "average_cadence": 78.5, "average_temp": 4, "average_watts": 185.5, "weighted_average_watts": 230, "kilojoules": 780.5, "device_watts": true, "has_heartrate": false, "max_watts": 743, "elev_high": 446.6, "elev_low": 17.2, "pr_count": 0, "total_photo_count": 2, "has_kudoed": false, "workout_type": 10, "suffer_score": null, "description": "", "calories": 870.2, "segment_efforts": [ { "id": 12345678987654321, "resource_state": 2, "name": "Tunnel Rd.", "activity": { "id": 12345678987654321, "resource_state": 1 }, "athlete": { "id": 134815, "resource_state": 1 }, "elapsed_time": 2038, "moving_time": 2038, "start_date": "2018-02-16T14:56:25Z", "start_date_local": "2018-02-16T06:56:25Z", "distance": 9434.8, "start_index": 211, "end_index": 2246, "average_cadence": 78.6, "device_watts": true, "average_watts": 237.6, "segment": { "id": 673683, "resource_state": 2, "name": "Tunnel Rd.", "activity_type": "Ride", "distance": 9220.7, "average_grade": 4.2, "maximum_grade": 25.8, "elevation_high": 426.5, "elevation_low": 43.4, "start_latlng": [ 37.8346153, -122.2520872 ], "end_latlng": [ 37.8476261, -122.2008944 ], "climb_category": 3, "city": "Oakland", "state": "CA", "country": "United States", "private": false, "hazardous": false, "starred": false }, "kom_rank": null, "pr_rank": null, "achievements": [], "hidden": false } ], "splits_metric": [ { "distance": 1001.5, "elapsed_time": 141, "elevation_difference": 4.4, "moving_time": 141, "split": 1, "average_speed": 7.1, "pace_zone": 0 } ], "laps": [ { "id": 4479306946, "resource_state": 2, "name": "Lap 1", "activity": { "id": 1410355832, "resource_state": 1 }, "athlete": { "id": 134815, "resource_state": 1 }, "elapsed_time": 1573, "moving_time": 1569, "start_date": "2018-02-16T14:52:54Z", "start_date_local": "2018-02-16T06:52:54Z", "distance": 8046.72, "start_index": 0, "end_index": 1570, "total_elevation_gain": 276, "average_speed": 5.12, "max_speed": 9.5, "average_cadence": 78.6, "device_watts": true, "average_watts": 233.1, "lap_index": 1, "split": 1 } ], "gear": { "id": "b12345678987654321", "primary": true, "name": "Tarmac", "resource_state": 2, "distance": 32547610 }, "partner_brand_tag": null, "photos": { "primary": { "id": null, "unique_id": "3FDGKL3-204E-4867-9E8D-89FC79EAAE17", "urls": { "100": "https://dgtzuqphqg23d.cloudfront.net/Bv93zv5t_mr57v0wXFbY_JyvtucgmU5Ym6N9z_bKeUI-128x96.jpg", "600": "https://dgtzuqphqg23d.cloudfront.net/Bv93zv5t_mr57v0wXFbY_JyvtucgmU5Ym6N9z_bKeUI-768x576.jpg" }, "source": 1 }, "use_primary_photo": true, "count": 2 }, "highlighted_kudosers": [ { "destination_url": "strava://athletes/12345678987654321", "display_name": "Marianne V.", "avatar_url": "https://dgalywyr863hv.cloudfront.net/pictures/athletes/12345678987654321/12345678987654321/3/medium.jpg", "show_name": true } ], "hide_from_home": false, "device_name": "Garmin Edge 1030", "embed_token": "18e4615989b47dd4ff3dc711b0aa4502e4b311a9", "segment_leaderboard_opt_out": false, "leaderboard_opt_out": false } } }, "default": { "description": "Unexpected error.", "schema": { "$ref": "https://developers.strava.com/swagger/fault.json#/Fault" } } } }, "put": { "operationId": "updateActivityById", "summary": "Update Activity", "description": "Updates the given activity that is owned by the authenticated athlete. Requires activity:write. Also requires activity:read_all in order to update Only Me activities", "parameters": [ { "name": "id", "in": "path", "description": "The identifier of the activity.", "required": true, "type": "integer", "format": "int64" }, { "name": "body", "in": "body", "schema": { "$ref": "https://developers.strava.com/swagger/activity.json#/UpdatableActivity" } } ], "tags": [ "Activities" ], "responses": { "200": { "description": "The activity's detailed representation.", "schema": { "$ref": "https://developers.strava.com/swagger/activity.json#/DetailedActivity" }, "examples": { "application/json": { "id": 12345678987654321, "resource_state": 3, "external_id": "garmin_push_12345678987654321", "upload_id": 98765432123456789, "athlete": { "id": 134815, "resource_state": 1 }, "name": "Happy Friday", "distance": 28099, "moving_time": 4207, "elapsed_time": 4410, "total_elevation_gain": 516, "type": "Ride", "sport_type": "MountainBikeRide", "start_date": "2018-02-16T14:52:54Z", "start_date_local": "2018-02-16T06:52:54Z", "timezone": "(GMT-08:00) America/Los_Angeles", "utc_offset": -28800, "start_latlng": [ 37.83, -122.26 ], "end_latlng": [ 37.83, -122.26 ], "location_city": null, "location_state": null, "location_country": "United States", "achievement_count": 0, "kudos_count": 19, "comment_count": 0, "athlete_count": 1, "photo_count": 0, "map": { "id": "a1410355832", "polyline": "ki{eFvqfiVqAWQIGEEKAYJgBVqDJ{BHa@jAkNJw@Pw@V{APs@^aABQAOEQGKoJ_FuJkFqAo@{A}@sH{DiAs@Q]?WVy@`@oBt@_CB]KYMMkB{AQEI@WT{BlE{@zAQPI@ICsCqA_BcAeCmAaFmCqIoEcLeG}KcG}A}@cDaBiDsByAkAuBqBi@y@_@o@o@kB}BgIoA_EUkAMcACa@BeBBq@LaAJe@b@uA`@_AdBcD`@iAPq@RgALqAB{@EqAyAoOCy@AmCBmANqBLqAZkB\\iCPiBJwCCsASiCq@iD]eA]y@[i@w@mAa@i@k@g@kAw@i@Ya@Q]EWFMLa@~BYpAFNpA`Aj@n@X`@V`AHh@JfB@xAMvAGZGHIDIAWOEQNcC@sACYK[MSOMe@QKKKYOs@UYQISCQ?Q@WNo@r@OHGAGCKOQ_BU}@MQGG]Io@@c@FYNg@d@s@d@ODQAMOMaASs@_@a@SESAQDqBn@a@RO?KK?UBU\\kA@Y?WMo@Iy@GWQ_@WSSGg@AkABQB_Ap@_A^o@b@Q@o@IS@OHi@n@OFS?OI}@iAQMQGQC}@DOIIUK{@IUOMyBo@kASOKIQCa@L[|AgATWN[He@?QKw@FOPCh@Fx@l@TDLELKl@aAHIJEX@r@ZTDV@LENQVg@RkA@c@MeA?WFOPMf@Ej@Fj@@LGHKDM?_@_@iC?a@HKRIl@NT?FCHMFW?YEYGWQa@GYBiAIq@Gq@L_BHSHK|@WJETSLQZs@z@_A~@uA^U`@G\\CRB\\Tl@p@Th@JZ^bB`@lAHLXVLDP?LGFSKiDBo@d@wBVi@R]VYVE\\@`@Lh@Fh@CzAk@RSDQA]GYe@eAGWSiBAWBWBIJORK`@KPOPSTg@h@}Ad@o@F[E_@EGMKUGmAEYGMIMYKs@?a@J}@@_BD_@HQJMx@e@LKHKHWAo@UoAAWFmAH}@?w@C[YwAAc@HSNM|Ao@rA}@zAq@`@a@j@eAxAuBXQj@MXSR[b@gAFg@?YISOGaAHi@Xw@v@_@d@WRSFqARUHQJc@d@m@`A[VSFUBcAEU@WFULUPa@v@Y~@UrBc@dBI~@?l@P~ABt@N`HEjA]zAEp@@p@TrBCl@CTQb@k@dAg@jAU^KJYLK@k@A[Js@d@a@b@]RgBl@[FMAw@[]G]?m@D_@F]P[Vu@t@[TMF_@Do@E_@@q@P]PWZUZw@vAkAlAGJOj@IlAMd@OR{@p@a@d@sBpD]v@a@`Aa@n@]TODgBVk@Pe@^cBfBc@Rs@La@RSPm@|@wCpDS^Wp@QZML{@l@qBbCYd@k@lAIVCZBZNTr@`@RRHZANIZQPKDW@e@CaASU?I@YTKRQx@@\\VmALYRQLCL?v@P|@D\\GJEFKDM@OCa@COOYIGm@YMUCM@]JYr@uAx@kAt@}@jAeAPWbAkBj@s@bAiAz@oAj@m@VQlAc@VQ~@aA`Au@p@Q`AIv@MZORUV_@p@iB|AoCh@q@dAaANUNWH[N{AJ[^m@t@_Av@wA\\a@`@W`@In@Al@B^E`@Wl@u@\\[VQ\\K`@Eb@?R@dAZP@d@CRExAs@\\Yt@{@LG\\MjAATINOXo@d@kAl@_AHYBOCe@QiBCm@Fq@\\wADo@AyGEeBWuB@YHu@Tu@Lk@VcCTo@d@aA\\WJE`@G~@FP?VI\\U~@sANO`@SfAMj@U\\WjAsAXS`@UNENALBHFFL?^Ml@Uj@]b@q@RUJSPkChEc@XcAb@sA|@]PaA\\OJKNER?TDTNj@Jn@?p@OfC@ZR`B@VCV_@n@{@l@WbACv@OlABnAPl@LNNHbBBNBLFFJ@^GLg@x@i@|AMP[X}@XOJKPET?l@LhAFXp@fBDRCd@S\\_@Ps@PQ@}A]S?QDe@V]b@MR[fAKt@ErAF~CANILYDKGIKe@{@Yy@e@sB[gA[c@e@YUCU?WBUHUNQPq@`AiArAMV[^e@Zc@JQJKNMz@?r@Bb@PfAAfA@VVbADn@E`@KHSEe@SMAKDKFM\\^dDCh@m@LoAQ_@@MFOZLfBEl@QbASd@KLQBOAaAc@QAQ@QHc@v@ONMJOBOCg@c@]O[EMBKFGL?RHv@ARERGNe@h@{@h@WVGNDt@JLNFPFz@LdBf@f@PJNHPF`ADPJJJDl@I`@B^Tp@bALJNDNALIf@i@PGPCt@DNE`@Uv@[dAw@RITGRCtAARBPJLPJRZxB?VEX_@vAAR?RDNHJJBh@UnBm@h@IRDRJNNJPNbBFRJLLBLCzAmAd@Uf@Gf@?P@PFJNHPFTH`BDTHNJJJ@LG`@m@^YPER@RDPHNNJRLn@HRLN^VNPHTFX@\\UlDFb@FHh@NP@HKPsB?}ASkCQ{@[y@q@}@cA{@KOCQDa@t@{CFGJCf@Nl@ZtA~@r@p@`@h@rAxBd@rA\\fARdAPjANrB?f@AtBCd@QfBkAjJOlBChA?rBFrBNlBdAfKFzAC~@Iz@Mz@Sv@s@jBmAxBi@hAWt@Sv@Qx@O`BA`@?dAPfBVpAd@`BfBlFf@fBdA~Cr@pAz@fApBhBjAt@H?IL?FBFJLx@^lHvDvh@~XnElCbAd@pGhDbAb@nAr@`Ad@`GhDnBbAxCbBrWhNJJDPARGP_@t@Qh@]pAUtAoA`Ny@jJApBBNFLJFJBv@Hb@HBF?\\", "resource_state": 3, "summary_polyline": "ki{eFvqfiVsBmA`Feh@qg@iX`B}JeCcCqGjIq~@kf@cM{KeHeX`@_GdGkSeBiXtB}YuEkPwFyDeAzAe@pC~DfGc@bIOsGmCcEiD~@oBuEkFhBcBmDiEfAVuDiAuD}NnDaNiIlCyDD_CtJKv@wGhD]YyEzBo@g@uKxGmHpCGtEtI~AuLrHkAcAaIvEgH_EaDR_FpBuBg@sNxHqEtHgLoTpIiCzKNr[sB|Es\\`JyObYeMbGsMnPsAfDxAnD}DBu@bCx@{BbEEyAoD`AmChNoQzMoGhOwX|[yIzBeFKg[zAkIdU_LiHxK}HzEh@vM_BtBg@xGzDbCcF~GhArHaIfByAhLsDiJuC?_HbHd@nL_Cz@ZnEkDDy@hHwJLiCbIrNrIvN_EfAjDWlEnEiAfBxDlFkBfBtEfDaAzBvDKdFx@|@XgJmDsHhAgD`GfElEzOwBnYdBxXgGlSc@bGdHpW|HdJztBnhAgFxc@HnCvBdA" }, "trainer": false, "commute": false, "manual": false, "private": false, "flagged": false, "gear_id": "b12345678987654321", "from_accepted_tag": false, "average_speed": 6.679, "max_speed": 18.5, "average_cadence": 78.5, "average_temp": 4, "average_watts": 185.5, "weighted_average_watts": 230, "kilojoules": 780.5, "device_watts": true, "has_heartrate": false, "max_watts": 743, "elev_high": 446.6, "elev_low": 17.2, "pr_count": 0, "total_photo_count": 2, "has_kudoed": false, "workout_type": 10, "suffer_score": null, "description": "", "calories": 870.2, "segment_efforts": [ { "id": 12345678987654321, "resource_state": 2, "name": "Tunnel Rd.", "activity": { "id": 12345678987654321, "resource_state": 1 }, "athlete": { "id": 12345678987654321, "resource_state": 1 }, "elapsed_time": 2038, "moving_time": 2038, "start_date": "2018-02-16T14:56:25Z", "start_date_local": "2018-02-16T06:56:25Z", "distance": 9434.8, "start_index": 211, "end_index": 2246, "average_cadence": 78.6, "device_watts": true, "average_watts": 237.6, "segment": { "id": 673683, "resource_state": 2, "name": "Tunnel Rd.", "activity_type": "Ride", "distance": 9220.7, "average_grade": 4.2, "maximum_grade": 25.8, "elevation_high": 426.5, "elevation_low": 43.4, "start_latlng": [ 37.8346153, -122.2520872 ], "end_latlng": [ 37.8476261, -122.2008944 ], "climb_category": 3, "city": "Oakland", "state": "CA", "country": "United States", "private": false, "hazardous": false, "starred": false }, "kom_rank": null, "pr_rank": null, "achievements": [], "hidden": false } ], "splits_metric": [ { "distance": 1001.5, "elapsed_time": 141, "elevation_difference": 4.4, "moving_time": 141, "split": 1, "average_speed": 7.1, "pace_zone": 0 } ], "laps": [ { "id": 4479306946, "resource_state": 2, "name": "Lap 1", "activity": { "id": 1410355832, "resource_state": 1 }, "athlete": { "id": 134815, "resource_state": 1 }, "elapsed_time": 1573, "moving_time": 1569, "start_date": "2018-02-16T14:52:54Z", "start_date_local": "2018-02-16T06:52:54Z", "distance": 8046.72, "start_index": 0, "end_index": 1570, "total_elevation_gain": 276, "average_speed": 5.12, "max_speed": 9.5, "average_cadence": 78.6, "device_watts": true, "average_watts": 233.1, "lap_index": 1, "split": 1 } ], "gear": { "id": "b12345678987654321", "primary": true, "name": "Tarmac", "resource_state": 2, "distance": 32547610 }, "partner_brand_tag": null, "photos": { "primary": { "id": null, "unique_id": "3FDGKL3-204E-4867-9E8D-89FC79EAAE17", "urls": { "100": "https://dgtzuqphqg23d.cloudfront.net/Bv93zv5t_mr57v0wXFbY_JyvtucgmU5Ym6N9z_bKeUI-128x96.jpg", "600": "https://dgtzuqphqg23d.cloudfront.net/Bv93zv5t_mr57v0wXFbY_JyvtucgmU5Ym6N9z_bKeUI-768x576.jpg" }, "source": 1 }, "use_primary_photo": true, "count": 2 }, "highlighted_kudosers": [ { "destination_url": "strava://athletes/12345678987654321", "display_name": "Marianne V.", "avatar_url": "https://dgalywyr863hv.cloudfront.net/pictures/athletes/12345678987654321/12345678987654321/3/medium.jpg", "show_name": true } ], "hide_from_home": false, "device_name": "Garmin Edge 1030", "embed_token": "18e4615989b47dd4ff3dc711b0aa4502e4b311a9", "segment_leaderboard_opt_out": false, "leaderboard_opt_out": false } } }, "default": { "description": "Unexpected error.", "schema": { "$ref": "https://developers.strava.com/swagger/fault.json#/Fault" } } } } }, "/athlete/activities": { "get": { "operationId": "getLoggedInAthleteActivities", "summary": "List Athlete Activities", "description": "Returns the activities of an athlete for a specific identifier. Requires activity:read. Only Me activities will be filtered out unless requested by a token with activity:read_all.", "parameters": [ { "name": "before", "in": "query", "description": "An epoch timestamp to use for filtering activities that have taken place before a certain time.", "type": "integer" }, { "name": "after", "in": "query", "description": "An epoch timestamp to use for filtering activities that have taken place after a certain time.", "type": "integer" }, { "$ref": "#/parameters/page" }, { "$ref": "#/parameters/perPage" } ], "tags": [ "Activities" ], "responses": { "200": { "description": "The authenticated athlete's activities", "schema": { "type": "array", "items": { "$ref": "https://developers.strava.com/swagger/activity.json#/SummaryActivity" } }, "examples": { "application/json": [ { "resource_state": 2, "athlete": { "id": 134815, "resource_state": 1 }, "name": "Happy Friday", "distance": 24931.4, "moving_time": 4500, "elapsed_time": 4500, "total_elevation_gain": 0, "type": "Ride", "sport_type": "MountainBikeRide", "workout_type": null, "id": 154504250376823, "external_id": "garmin_push_12345678987654321", "upload_id": 987654321234567891234, "start_date": "2018-05-02T12:15:09Z", "start_date_local": "2018-05-02T05:15:09Z", "timezone": "(GMT-08:00) America/Los_Angeles", "utc_offset": -25200, "start_latlng": null, "end_latlng": null, "location_city": null, "location_state": null, "location_country": "United States", "achievement_count": 0, "kudos_count": 3, "comment_count": 1, "athlete_count": 1, "photo_count": 0, "map": { "id": "a12345678987654321", "summary_polyline": null, "resource_state": 2 }, "trainer": true, "commute": false, "manual": false, "private": false, "flagged": false, "gear_id": "b12345678987654321", "from_accepted_tag": false, "average_speed": 5.54, "max_speed": 11, "average_cadence": 67.1, "average_watts": 175.3, "weighted_average_watts": 210, "kilojoules": 788.7, "device_watts": true, "has_heartrate": true, "average_heartrate": 140.3, "max_heartrate": 178, "max_watts": 406, "pr_count": 0, "total_photo_count": 1, "has_kudoed": false, "suffer_score": 82 }, { "resource_state": 2, "athlete": { "id": 167560, "resource_state": 1 }, "name": "Bondcliff", "distance": 23676.5, "moving_time": 5400, "elapsed_time": 5400, "total_elevation_gain": 0, "type": "Ride", "sport_type": "MountainBikeRide", "workout_type": null, "id": 1234567809, "external_id": "garmin_push_12345678987654321", "upload_id": 1234567819, "start_date": "2018-04-30T12:35:51Z", "start_date_local": "2018-04-30T05:35:51Z", "timezone": "(GMT-08:00) America/Los_Angeles", "utc_offset": -25200, "start_latlng": null, "end_latlng": null, "location_city": null, "location_state": null, "location_country": "United States", "achievement_count": 0, "kudos_count": 4, "comment_count": 0, "athlete_count": 1, "photo_count": 0, "map": { "id": "a12345689", "summary_polyline": null, "resource_state": 2 }, "trainer": true, "commute": false, "manual": false, "private": false, "flagged": false, "gear_id": "b12345678912343", "from_accepted_tag": false, "average_speed": 4.385, "max_speed": 8.8, "average_cadence": 69.8, "average_watts": 200, "weighted_average_watts": 214, "kilojoules": 1080, "device_watts": true, "has_heartrate": true, "average_heartrate": 152.4, "max_heartrate": 183, "max_watts": 403, "pr_count": 0, "total_photo_count": 1, "has_kudoed": false, "suffer_score": 162 } ] } }, "default": { "description": "Unexpected error.", "schema": { "$ref": "https://developers.strava.com/swagger/fault.json#/Fault" } } } } }, "/activities/{id}/laps": { "get": { "operationId": "getLapsByActivityId", "summary": "List Activity Laps", "description": "Returns the laps of an activity identified by an identifier. Requires activity:read for Everyone and Followers activities. Requires activity:read_all for Only Me activities.", "parameters": [ { "name": "id", "in": "path", "description": "The identifier of the activity.", "required": true, "type": "integer", "format": "int64" } ], "tags": [ "Activities" ], "responses": { "200": { "description": "Activity Laps.", "schema": { "type": "array", "items": { "$ref": "https://developers.strava.com/swagger/lap.json#/Lap" } }, "examples": { "application/json": [ { "id": 12345678987654321, "resource_state": 2, "name": "Lap 1", "activity": { "id": 12345678987654321, "resource_state": 1 }, "athlete": { "id": 12345678987654321, "resource_state": 1 }, "elapsed_time": 1691, "moving_time": 1587, "start_date": "2018-02-08T14:13:37Z", "start_date_local": "2018-02-08T06:13:37Z", "distance": 8046.72, "start_index": 0, "end_index": 1590, "total_elevation_gain": 270, "average_speed": 4.76, "max_speed": 9.4, "average_cadence": 79, "device_watts": true, "average_watts": 228.2, "lap_index": 1, "split": 1 } ] } }, "default": { "description": "Unexpected error.", "schema": { "$ref": "https://developers.strava.com/swagger/fault.json#/Fault" } } } } }, "/activities/{id}/zones": { "get": { "operationId": "getZonesByActivityId", "summary": "Get Activity Zones", "description": "Summit Feature. Returns the zones of a given activity. Requires activity:read for Everyone and Followers activities. Requires activity:read_all for Only Me activities.", "parameters": [ { "name": "id", "in": "path", "description": "The identifier of the activity.", "required": true, "type": "integer", "format": "int64" } ], "tags": [ "Activities" ], "responses": { "200": { "description": "Activity Zones.", "schema": { "type": "array", "items": { "$ref": "https://developers.strava.com/swagger/zones.json#/ActivityZone" } } }, "default": { "description": "Unexpected error.", "schema": { "$ref": "https://developers.strava.com/swagger/fault.json#/Fault" } } } } }, "/activities/{id}/comments": { "get": { "operationId": "getCommentsByActivityId", "summary": "List Activity Comments", "description": "Returns the comments on the given activity. Requires activity:read for Everyone and Followers activities. Requires activity:read_all for Only Me activities.", "parameters": [ { "name": "id", "in": "path", "description": "The identifier of the activity.", "required": true, "type": "integer", "format": "int64" }, { "name": "page", "in": "query", "description": "Deprecated. Prefer to use after_cursor.", "type": "integer" }, { "name": "per_page", "in": "query", "description": "Deprecated. Prefer to use page_size.", "type": "integer", "default": 30 }, { "name": "page_size", "in": "query", "description": "Number of items per page. Defaults to 30.", "type": "integer", "default": 30 }, { "name": "after_cursor", "in": "query", "description": "Cursor of the last item in the previous page of results, used to request the subsequent page of results. When omitted, the first page of results is fetched.", "type": "string" } ], "tags": [ "Activities" ], "responses": { "200": { "description": "Comments.", "schema": { "type": "array", "items": { "$ref": "https://developers.strava.com/swagger/comment.json#/Comment" } }, "examples": { "application/json": [ { "id": 12345678987654321, "activity_id": 12345678987654321, "post_id": null, "resource_state": 2, "text": "Good job and keep the cat pictures coming!", "mentions_metadata": null, "created_at": "2018-02-08T19:25:39Z", "athlete": { "firstname": "Peter", "lastname": "S" }, "cursor": "abc123%20" } ] } }, "default": { "description": "Unexpected error.", "schema": { "$ref": "https://developers.strava.com/swagger/fault.json#/Fault" } } } } }, "/activities/{id}/kudos": { "get": { "operationId": "getKudoersByActivityId", "summary": "List Activity Kudoers", "description": "Returns the athletes who kudoed an activity identified by an identifier. Requires activity:read for Everyone and Followers activities. Requires activity:read_all for Only Me activities.", "parameters": [ { "name": "id", "in": "path", "description": "The identifier of the activity.", "required": true, "type": "integer", "format": "int64" }, { "$ref": "#/parameters/page" }, { "$ref": "#/parameters/perPage" } ], "tags": [ "Activities" ], "responses": { "200": { "description": "Comments.", "schema": { "type": "array", "items": { "$ref": "https://developers.strava.com/swagger/athlete.json#/SummaryAthlete" } }, "examples": { "application/json": [ { "firstname": "Peter", "lastname": "S" } ] } }, "default": { "description": "Unexpected error.", "schema": { "$ref": "https://developers.strava.com/swagger/fault.json#/Fault" } } } } }, "/clubs/{id}": { "get": { "operationId": "getClubById", "summary": "Get Club", "description": "Returns a given club using its identifier.", "parameters": [ { "name": "id", "in": "path", "description": "The identifier of the club.", "required": true, "type": "integer", "format": "int64" } ], "tags": [ "Clubs" ], "responses": { "200": { "description": "The detailed representation of a club.", "schema": { "$ref": "https://developers.strava.com/swagger/club.json#/DetailedClub" }, "examples": { "application/json": { "id": 1, "resource_state": 3, "name": "Team Strava Cycling", "profile_medium": "https://dgalywyr863hv.cloudfront.net/pictures/clubs/1/1582/4/medium.jpg", "profile": "https://dgalywyr863hv.cloudfront.net/pictures/clubs/1/1582/4/large.jpg", "cover_photo": "https://dgalywyr863hv.cloudfront.net/pictures/clubs/1/4328276/1/large.jpg", "cover_photo_small": "https://dgalywyr863hv.cloudfront.net/pictures/clubs/1/4328276/1/small.jpg", "sport_type": "cycling", "activity_types": ["Ride", "VirtualRide", "EBikeRide", "Velomobile", "Handcycle"], "city": "San Francisco", "state": "California", "country": "United States", "private": true, "member_count": 116, "featured": false, "verified": false, "url": "team-strava-bike", "membership": "member", "admin": false, "owner": false, "description": "Private club for Cyclists who work at Strava.", "club_type": "company", "post_count": 29, "owner_id": 759, "following_count": 107 } } }, "default": { "description": "Unexpected error.", "schema": { "$ref": "https://developers.strava.com/swagger/fault.json#/Fault" } } } } }, "/clubs/{id}/members": { "get": { "operationId": "getClubMembersById", "summary": "List Club Members", "description": "Returns a list of the athletes who are members of a given club.", "parameters": [ { "name": "id", "in": "path", "description": "The identifier of the club.", "required": true, "type": "integer", "format": "int64" }, { "$ref": "#/parameters/page" }, { "$ref": "#/parameters/perPage" } ], "tags": [ "Clubs" ], "responses": { "200": { "description": "A list of club athlete representations.", "schema": { "type": "array", "items": { "$ref": "https://developers.strava.com/swagger/athlete.json#/ClubAthlete" } }, "examples": { "application/json": [ { "resource_state": 2, "firstname": "Peter", "lastname": "S.", "membership": "member", "admin": false, "owner": false } ] } }, "default": { "description": "Unexpected error.", "schema": { "$ref": "https://developers.strava.com/swagger/fault.json#/Fault" } } } } }, "/clubs/{id}/admins": { "get": { "operationId": "getClubAdminsById", "summary": "List Club Administrators", "description": "Returns a list of the administrators of a given club.", "parameters": [ { "name": "id", "in": "path", "description": "The identifier of the club.", "required": true, "type": "integer", "format": "int64" }, { "$ref": "#/parameters/page" }, { "$ref": "#/parameters/perPage" } ], "tags": [ "Clubs" ], "responses": { "200": { "description": "A list of summary athlete representations.", "schema": { "type": "array", "items": { "$ref": "https://developers.strava.com/swagger/athlete.json#/SummaryAthlete" } }, "examples": { "application/json": [ { "resource_state": 2, "firstname": "Peter", "lastname": "S." } ] } }, "default": { "description": "Unexpected error.", "schema": { "$ref": "https://developers.strava.com/swagger/fault.json#/Fault" } } } } }, "/clubs/{id}/activities": { "get": { "operationId": "getClubActivitiesById", "summary": "List Club Activities", "description": "Retrieve recent activities from members of a specific club. The authenticated athlete must belong to the requested club in order to hit this endpoint. Pagination is supported. Athlete profile visibility is respected for all activities.", "parameters": [ { "name": "id", "in": "path", "description": "The identifier of the club.", "required": true, "type": "integer", "format": "int64" }, { "$ref": "#/parameters/page" }, { "$ref": "#/parameters/perPage" } ], "tags": [ "Clubs" ], "responses": { "200": { "description": "A list of activities.", "schema": { "type": "array", "items": { "$ref": "https://developers.strava.com/swagger/activity.json#/ClubActivity" } }, "examples": { "application/json": [ { "resource_state": 2, "athlete": { "resource_state": 2, "firstname": "Peter", "lastname": "S." }, "name": "World Championship", "distance": 2641.7, "moving_time": 577, "elapsed_time": 635, "total_elevation_gain": 8.8, "type": "Ride", "sport_type": "MountainBikeRide", "workout_type": null } ] } }, "default": { "description": "Unexpected error.", "schema": { "$ref": "https://developers.strava.com/swagger/fault.json#/Fault" } } } } }, "/athlete/clubs": { "get": { "operationId": "getLoggedInAthleteClubs", "summary": "List Athlete Clubs", "description": "Returns a list of the clubs whose membership includes the authenticated athlete.", "parameters": [ { "$ref": "#/parameters/page" }, { "$ref": "#/parameters/perPage" } ], "tags": [ "Clubs" ], "responses": { "200": { "description": "A list of summary club representations.", "schema": { "type": "array", "items": { "$ref": "https://developers.strava.com/swagger/club.json#/SummaryClub" } }, "examples": { "application/json": [ { "id": 231407, "resource_state": 2, "name": "The Strava Club", "profile_medium": "https://dgalywyr863hv.cloudfront.net/pictures/clubs/231407/5319085/1/medium.jpg", "profile": "https://dgalywyr863hv.cloudfront.net/pictures/clubs/231407/5319085/1/large.jpg", "cover_photo": "https://dgalywyr863hv.cloudfront.net/pictures/clubs/231407/5098428/4/large.jpg", "cover_photo_small": "https://dgalywyr863hv.cloudfront.net/pictures/clubs/231407/5098428/4/small.jpg", "sport_type": "other", "city": "San Francisco", "state": "California", "country": "United States", "private": false, "member_count": 93151, "featured": false, "verified": true, "url": "strava" } ] } }, "default": { "description": "Unexpected error.", "schema": { "$ref": "https://developers.strava.com/swagger/fault.json#/Fault" } } } } }, "/gear/{id}": { "get": { "operationId": "getGearById", "summary": "Get Equipment", "description": "Returns an equipment using its identifier.", "parameters": [ { "name": "id", "in": "path", "description": "The identifier of the gear.", "required": true, "type": "string" } ], "tags": [ "Gears" ], "responses": { "200": { "description": "A representation of the gear.", "schema": { "$ref": "https://developers.strava.com/swagger/gear.json#/DetailedGear" }, "examples": { "application/json": { "id": "b1231", "primary": false, "resource_state": 3, "distance": 388206, "brand_name": "BMC", "model_name": "Teammachine", "frame_type": 3, "description": "My Bike." } } }, "default": { "description": "Unexpected error.", "schema": { "$ref": "https://developers.strava.com/swagger/fault.json#/Fault" } } } } }, "/routes/{id}": { "get": { "operationId": "getRouteById", "summary": "Get Route", "description": "Returns a route using its identifier. Requires read_all scope for private routes.", "parameters": [ { "name": "id", "in": "path", "description": "The identifier of the route.", "required": true, "type": "integer", "format": "int64" } ], "tags": [ "Routes" ], "responses": { "200": { "description": "A representation of the route.", "schema": { "$ref": "https://developers.strava.com/swagger/route.json#/Route" } }, "default": { "description": "Unexpected error.", "schema": { "$ref": "https://developers.strava.com/swagger/fault.json#/Fault" } } } } }, "/athletes/{id}/routes": { "get": { "operationId": "getRoutesByAthleteId", "summary": "List Athlete Routes", "description": "Returns a list of the routes created by the authenticated athlete. Private routes are filtered out unless requested by a token with read_all scope.", "parameters": [ { "$ref": "#/parameters/page" }, { "$ref": "#/parameters/perPage" } ], "tags": [ "Routes" ], "responses": { "200": { "description": "A representation of the route.", "schema": { "type": "array", "items": { "$ref": "https://developers.strava.com/swagger/route.json#/Route" } } }, "default": { "description": "Unexpected error.", "schema": { "$ref": "https://developers.strava.com/swagger/fault.json#/Fault" } } } } }, "/routes/{id}/export_gpx": { "get": { "operationId": "getRouteAsGPX", "summary": "Export Route GPX", "description": "Returns a GPX file of the route. Requires read_all scope for private routes.", "parameters": [ { "name": "id", "in": "path", "description": "The identifier of the route.", "required": true, "type": "integer", "format": "int64" } ], "tags": [ "Routes" ], "responses": { "200": { "description": "A GPX file with the route.", "content": { "application/gpx+xml" : { "schema": { "type" : "string", "format": "binary" } } } }, "default": { "description": "Unexpected error.", "schema": { "$ref": "https://developers.strava.com/swagger/fault.json#/Fault" } } } } }, "/routes/{id}/export_tcx": { "get": { "operationId": "getRouteAsTCX", "summary": "Export Route TCX", "description": "Returns a TCX file of the route. Requires read_all scope for private routes.", "parameters": [ { "name": "id", "in": "path", "description": "The identifier of the route.", "required": true, "type": "integer", "format": "int64" } ], "tags": [ "Routes" ], "responses": { "200": { "description": "A TCX file with the route.", "content": { "application/tcx+xml" : { "schema": { "type" : "string", "format": "binary" } } } }, "default": { "description": "Unexpected error.", "schema": { "$ref": "https://developers.strava.com/swagger/fault.json#/Fault" } } } } }, "/uploads": { "post": { "operationId": "createUpload", "summary": "Upload Activity", "description": "Uploads a new data file to create an activity from. Requires activity:write scope.", "consumes": [ "multipart/form-data" ], "parameters": [ { "name": "file", "in": "formData", "type": "file", "description": "The uploaded file." }, { "name": "name", "in": "formData", "description": "The desired name of the resulting activity.", "type": "string" }, { "name": "description", "in": "formData", "description": "The desired description of the resulting activity.", "type": "string" }, { "name": "trainer", "in": "formData", "description": "Whether the resulting activity should be marked as having been performed on a trainer.", "type": "string" }, { "name": "commute", "in": "formData", "description": "Whether the resulting activity should be tagged as a commute.", "type": "string" }, { "name": "data_type", "in": "formData", "description": "The format of the uploaded file.", "type": "string", "enum": [ "fit", "fit.gz", "tcx", "tcx.gz", "gpx", "gpx.gz" ] }, { "name": "external_id", "in": "formData", "description": "The desired external identifier of the resulting activity.", "type": "string" } ], "tags": [ "Uploads" ], "responses": { "201": { "description": "A representation of the created upload.", "schema": { "$ref": "https://developers.strava.com/swagger/upload.json#/Upload" } }, "default": { "description": "Unexpected error.", "schema": { "$ref": "https://developers.strava.com/swagger/fault.json#/Fault" } } } } }, "/uploads/{uploadId}": { "get": { "operationId": "getUploadById", "summary": "Get Upload", "description": "Returns an upload for a given identifier. Requires activity:write scope.", "parameters": [ { "name": "uploadId", "in": "path", "description": "The identifier of the upload.", "required": true, "type": "integer", "format": "int64" } ], "tags": [ "Uploads" ], "responses": { "200": { "description": "Representation of the upload.", "schema": { "$ref": "https://developers.strava.com/swagger/upload.json#/Upload" } }, "default": { "description": "Unexpected error.", "schema": { "$ref": "https://developers.strava.com/swagger/fault.json#/Fault" } } } } }, "/activities/{id}/streams": { "get": { "operationId": "getActivityStreams", "summary": "Get Activity Streams", "description": "Returns the given activity's streams. Requires activity:read scope. Requires activity:read_all scope for Only Me activities.", "parameters": [ { "name": "id", "in": "path", "description": "The identifier of the activity.", "required": true, "type": "integer", "format": "int64" }, { "name": "keys", "in": "query", "description": "Desired stream types.", "required": true, "type": "array", "items": { "type": "string", "enum": [ "time", "distance", "latlng", "altitude", "velocity_smooth", "heartrate", "cadence", "watts", "temp", "moving", "grade_smooth" ] }, "collectionFormat": "csv", "minItems": 1 }, { "name": "key_by_type", "in": "query", "description": "Must be true.", "type": "boolean", "required": true, "default": true } ], "tags": [ "Streams" ], "responses": { "200": { "description": "The set of requested streams.", "schema": { "$ref": "https://developers.strava.com/swagger/stream.json#/StreamSet" }, "examples": { "application/json": [ { "type": "distance", "data": [ 2.9, 5.8, 8.5, 11.7, 15, 19, 23.2, 28, 32.8, 38.1, 43.8, 49.5 ], "series_type": "distance", "original_size": 12, "resolution": "high" } ] } }, "default": { "description": "Unexpected error.", "schema": { "$ref": "https://developers.strava.com/swagger/fault.json#/Fault" } } } } }, "/segment_efforts/{id}/streams": { "get": { "operationId": "getSegmentEffortStreams", "summary": "Get Segment Effort Streams", "description": "Returns a set of streams for a segment effort completed by the authenticated athlete. Requires read_all scope.", "parameters": [ { "name": "id", "in": "path", "description": "The identifier of the segment effort.", "required": true, "type": "integer", "format": "int64" }, { "name": "keys", "in": "query", "description": "The types of streams to return.", "required": true, "type": "array", "items": { "type": "string", "enum": [ "time", "distance", "latlng", "altitude", "velocity_smooth", "heartrate", "cadence", "watts", "temp", "moving", "grade_smooth" ] }, "collectionFormat": "csv", "minItems": 1 }, { "name": "key_by_type", "in": "query", "description": "Must be true.", "type": "boolean", "required": true, "default": true } ], "tags": [ "Streams" ], "responses": { "200": { "description": "The set of requested streams.", "schema": { "$ref": "https://developers.strava.com/swagger/stream.json#/StreamSet" }, "examples": { "application/json": [ { "type": "distance", "data": [ 904.5, 957.8, 963.1, 989.1, 1011.9, 1049.7, 1082.4, 1098.1, 1113.2, 1124.7, 1139.2, 1142.1, 1170.4, 1173 ], "series_type": "distance", "original_size": 14, "resolution": "high" } ] } }, "default": { "description": "Unexpected error.", "schema": { "$ref": "https://developers.strava.com/swagger/fault.json#/Fault" } } } } }, "/segments/{id}/streams": { "get": { "operationId": "getSegmentStreams", "summary": "Get Segment Streams", "description": "Returns the given segment's streams. Requires read_all scope for private segments.", "parameters": [ { "name": "id", "in": "path", "description": "The identifier of the segment.", "required": true, "type": "integer", "format": "int64" }, { "name": "keys", "in": "query", "description": "The types of streams to return.", "required": true, "type": "array", "items": { "type": "string", "enum": [ "distance", "latlng", "altitude" ] }, "collectionFormat": "csv", "minItems": 1 }, { "name": "key_by_type", "in": "query", "description": "Must be true.", "type": "boolean", "required": true, "default": true } ], "tags": [ "Streams" ], "responses": { "200": { "description": "The set of requested streams.", "schema": { "$ref": "https://developers.strava.com/swagger/stream.json#/StreamSet" }, "examples": { "application/json": [ { "type": "latlng", "data": [ [ 37.833112, -122.483436 ], [ 37.832964, -122.483406 ] ], "series_type": "distance", "original_size": 2, "resolution": "high" }, { "type": "distance", "data": [ 0, 16.8 ], "series_type": "distance", "original_size": 2, "resolution": "high" }, { "type": "altitude", "data": [ 92.4, 93.4 ], "series_type": "distance", "original_size": 2, "resolution": "high" } ] } }, "default": { "description": "Unexpected error.", "schema": { "$ref": "https://developers.strava.com/swagger/fault.json#/Fault" } } } } }, "/routes/{id}/streams": { "get": { "operationId": "getRouteStreams", "summary": "Get Route Streams", "description": "Returns the given route's streams. Requires read_all scope for private routes.", "parameters": [ { "name": "id", "in": "path", "description": "The identifier of the route.", "required": true, "type": "integer", "format": "int64" } ], "tags": [ "Streams" ], "responses": { "200": { "description": "The set of requested streams.", "schema": { "$ref": "https://developers.strava.com/swagger/stream.json#/StreamSet" }, "examples": { "application/json": [ { "type": "latlng", "data": [ [ 37.833112, -122.483436 ], [ 37.832964, -122.483406 ] ] }, { "type": "distance", "data": [ 0, 16.8 ] }, { "type": "altitude", "data": [ 92.4, 93.4 ] } ] } }, "default": { "description": "Unexpected error.", "schema": { "$ref": "https://developers.strava.com/swagger/fault.json#/Fault" } } } } } } } stravalib-2.2/src/stravalib/tests/test.ini-example000066400000000000000000000007361475174155400224200ustar00rootroot00000000000000[write_tests] # Add a Strava access token for the write/upload functional tests # (This token must have been created with the ability to write data -- i.e. # scope=write or scope=private,write.) # # You can use the stravalib.tests.auth_responder server to help fetch this (see that module's documentation). access_token = xxxxxxxxxxxxxxxx [activity_tests] # Some tests require an activity which is owned by the athlete who obtained the # access_token above. activity_id = stravalib-2.2/src/stravalib/tests/unit/000077500000000000000000000000001475174155400202605ustar00rootroot00000000000000stravalib-2.2/src/stravalib/tests/unit/__init__.py000066400000000000000000000000001475174155400223570ustar00rootroot00000000000000stravalib-2.2/src/stravalib/tests/unit/test_client.py000066400000000000000000000006771475174155400231610ustar00rootroot00000000000000import stravalib def test_client_protocol_initialization(): """Test that instantiating Client sets corresponding values in protocol.""" client = stravalib.Client( access_token="access_123", token_expires=123456, refresh_token="refresh_123", ) assert client.protocol.access_token == "access_123" assert client.protocol.token_expires == 123456 assert client.protocol.refresh_token == "refresh_123" stravalib-2.2/src/stravalib/tests/unit/test_client_utils.py000066400000000000000000000074651475174155400244030ustar00rootroot00000000000000import datetime import os from unittest import mock from urllib import parse as urlparse import pytz from stravalib.client import Client from stravalib.tests import RESOURCES_DIR, TestBase class ClientUtilsTest(TestBase): client = Client() def test_utc_datetime_to_epoch_utc_datetime_given_correct_epoch_returned( self, ): dt = pytz.utc.localize(datetime.datetime(2014, 1, 1, 0, 0, 0)) self.assertEqual(1388534400, self.client._utc_datetime_to_epoch(dt)) class ClientAuthorizationUrlTest(TestBase): client = Client() def get_url_param(self, url, key): """ >>> get_url_param("http://www.example.com/?key=1", "key") 1 """ return urlparse.parse_qs(urlparse.urlparse(url).query)[key][0] def test_incorrect_scope_raises(self): self.assertRaises( Exception, self.client.authorization_url, 1, "www.example.com", scope="wrong", ) self.assertRaises( Exception, self.client.authorization_url, 1, "www.example.com", scope=["wrong"], ) def test_correct_scope(self): url = self.client.authorization_url( 1, "www.example.com", scope="activity:write" ) self.assertEqual(self.get_url_param(url, "scope"), "activity:write") # Check also with two params url = self.client.authorization_url( 1, "www.example.com", scope=["activity:write", "activity:read_all"] ) self.assertEqual( self.get_url_param(url, "scope"), "activity:write,activity:read_all", ) def test_scope_may_be_list(self): url = self.client.authorization_url( 1, "www.example.com", scope=["activity:write", "activity:read_all"] ) self.assertEqual( self.get_url_param(url, "scope"), "activity:write,activity:read_all", ) def test_incorrect_approval_prompt_raises(self): self.assertRaises( Exception, self.client.authorization_url, 1, "www.example.com", approval_prompt="wrong", ) def test_state_param(self): url = self.client.authorization_url( 1, "www.example.com", state="my_state" ) self.assertEqual(self.get_url_param(url, "state"), "my_state") def test_params(self): url = self.client.authorization_url(1, "www.example.com") self.assertEqual(self.get_url_param(url, "client_id"), "1") self.assertEqual( self.get_url_param(url, "redirect_uri"), "www.example.com" ) self.assertEqual(self.get_url_param(url, "approval_prompt"), "auto") class TestClientUploadActivity(TestBase): client = Client() def test_upload_activity_file_with_different_types(self): """ Test uploading an activity with different activity_file object types. """ with ( mock.patch("stravalib.protocol.ApiV3.post", return_value={}), open(os.path.join(RESOURCES_DIR, "sample.tcx")) as fp, ): # test activity_file with type TextIOWrapper uploader = self.client.upload_activity(fp, data_type="tcx") self.assertTrue(uploader.is_processing) # test activity_file with type str uploader = self.client.upload_activity( fp.read(), data_type="tcx", activity_type="ride" ) self.assertTrue(uploader.is_processing) # test activity_file with type bytes uploader = self.client.upload_activity( fp.read().encode("utf-8"), data_type="tcx", activity_type="ride", ) self.assertTrue(uploader.is_processing) stravalib-2.2/src/stravalib/tests/unit/test_limiter.py000066400000000000000000000106101475174155400233340ustar00rootroot00000000000000import arrow import pytest from stravalib.util.limiter import ( RequestRate, SleepingRateLimitRule, get_rates_from_response_headers, get_seconds_until_next_day, get_seconds_until_next_quarter, ) # Example responses for Strava 3rd party apps that are not enrolled in the # 2023 developer program: fake_response_unenrolled = { "Status": "404 Not Found", "X-Request-Id": "a1a4a4973962ffa7e0f18d7c485fe741", "Content-Encoding": "gzip", "Content-Length": "104", "Connection": "keep-alive", "X-RateLimit-Limit": "600,30000", "X-UA-Compatible": "IE=Edge,chrome=1", "Cache-Control": "no-cache, private", "Date": "Tue, 14 Nov 2017 11:29:15 GMT", "X-FRAME-OPTIONS": "DENY", "Content-Type": "application/json; charset=UTF-8", "X-RateLimit-Usage": "4,67", } fake_response_unenrolled_limit_exceeded = fake_response_unenrolled | { "X-RateLimit-Usage": "601, 602" } fake_response_unenrolled_no_rates = { "Status": "200 OK", "X-Request-Id": "d465159561420f6e0239dc24429a7cf3", "Content-Encoding": "gzip", "Content-Length": "371", "Connection": "keep-alive", "X-UA-Compatible": "IE=Edge,chrome=1", "Cache-Control": "max-age=0, private, must-revalidate", "Date": "Tue, 14 Nov 2017 13:19:31 GMT", "X-FRAME-OPTIONS": "DENY", "Content-Type": "application/json; charset=UTF-8", } # Example responses for Strava 3rd party apps that are enrolled: fake_reponse_enrolled = fake_response_unenrolled | { "X-ReadRateLimit-Usage": "2,32", "X-ReadRateLimit-Limit": "300,15000", } fake_reponse_enrolled_read_limit_exceeded = fake_reponse_enrolled | { "X-ReadRateLimit-Usage": "301,302" } fake_reponse_enrolled_overall_limit_exceeded = fake_reponse_enrolled | { "X-RateLimit-Usage": "601,602" } @pytest.mark.parametrize( "headers,request_method,expected_rates", # rates = (short_usage,long_usage,short_limit,long_limit) ( (fake_response_unenrolled, "GET", (4, 67, 600, 30000)), (fake_response_unenrolled, "POST", (4, 67, 600, 30000)), (fake_response_unenrolled_no_rates, "GET", None), (fake_response_unenrolled_no_rates, "PUT", None), (fake_reponse_enrolled, "GET", (2, 32, 300, 15000)), (fake_reponse_enrolled, "PUT", (4, 67, 600, 30000)), ), ) def test_get_rates_from_response_headers( headers, request_method, expected_rates ): assert ( get_rates_from_response_headers(headers, request_method) == expected_rates ) @pytest.mark.parametrize( "timestamp,expected_seconds", ( (arrow.get(2017, 11, 1, 17, 14, 0, 0), 59), (arrow.get(2017, 11, 1, 17, 59, 0, 0), 59), (arrow.get(2017, 11, 1, 17, 59, 59, 999999), 0), (arrow.get(2017, 11, 1, 17, 0, 0, 1), 899), ), ) def test_get_seconds_until_next_quarter(timestamp, expected_seconds): assert get_seconds_until_next_quarter(timestamp) == expected_seconds @pytest.mark.parametrize( "timestamp,expected_seconds", ( (arrow.get(2017, 11, 1, 23, 59, 0, 0), 59), (arrow.get(2017, 11, 1, 0, 0, 0, 0), 86399), ), ) def test_get_seconds_until_next_day(timestamp, expected_seconds): assert get_seconds_until_next_day(timestamp) == expected_seconds def test_create_limiter_invalid_priority(): """Should raise ValueError in case of invalid priority""" with pytest.raises(ValueError): SleepingRateLimitRule(priority="foobar") @pytest.mark.parametrize( "priority,rates,seconds_until_short_limit,seconds_until_long_limit,expected_wait_time", ( ("high", RequestRate(42, 42, 100, 100), 60, 3600, 0), ("medium", RequestRate(1, 1, 11, 100), 10, 1000, 1), ("medium", RequestRate(1, 1, 11, 100), 5, 1000, 0.5), ("low", RequestRate(1, 1, 3, 11), 1, 10, 1), ("low", RequestRate(1, 1, 3, 11), 1, 5, 0.5), ("high", RequestRate(10, 10, 10, 100), 42, 1000, 42), ("high", RequestRate(999, 10, 10, 100), 42, 1000, 42), ("medium", RequestRate(10, 100, 10, 100), 42, 1000, 1000), ("low", RequestRate(10, 1001, 10, 100), 42, 1000, 1000), ), ) def test_get_wait_time( priority, rates, seconds_until_short_limit, seconds_until_long_limit, expected_wait_time, ): rule = SleepingRateLimitRule(priority=priority) assert ( rule._get_wait_time( rates, seconds_until_short_limit, seconds_until_long_limit ) == expected_wait_time ) stravalib-2.2/src/stravalib/tests/unit/test_model.py000066400000000000000000000307721475174155400230020ustar00rootroot00000000000000from datetime import datetime, timedelta, timezone import pytest import pytz import stravalib.unit_helper as uh from stravalib import model from stravalib.model import ( ActivityPhoto, ActivityTotals, AthletePrEffort, AthleteSegmentStats, AthleteStats, BaseEffort, DetailedActivity, Distance, Duration, Lap, LatLon, Route, Segment, SegmentEffort, SegmentExplorerResult, Split, SubscriptionCallback, SummaryActivity, SummarySegmentEffort, Timezone, Velocity, naive_datetime, ) from stravalib.tests import TestBase @pytest.mark.parametrize( "model_class,raw,expected_value", ( ( DetailedActivity, {"start_latlng": []}, None, ), ( DetailedActivity, {"end_latlng": []}, None, ), ( DetailedActivity, {"end_latlng": "5.4,4.3"}, LatLon([5.4, 4.3]), ), ( DetailedActivity, {"start_latlng": "5.4,4.3"}, LatLon([5.4, 4.3]), ), (DetailedActivity, {"start_latlng": []}, None), (Segment, {"start_latlng": []}, None), (SegmentExplorerResult, {"start_latlng": []}, None), (ActivityPhoto, {"location": []}, None), # TODO re-add this Activity test when custom types are implemented # (DetailedActivity, {"timezone": "foobar"}, None), ( DetailedActivity, {"start_date_local": "2023-01-17T11:06:07Z"}, datetime(2023, 1, 17, 11, 6, 7), ), ( BaseEffort, {"start_date_local": "2023-01-17T11:06:07Z"}, datetime(2023, 1, 17, 11, 6, 7), ), ( Lap, {"start_date_local": "2023-01-17T11:06:07Z"}, datetime(2023, 1, 17, 11, 6, 7), ), ( SummarySegmentEffort, {"start_date_local": "2023-01-17T11:06:07Z"}, datetime(2023, 1, 17, 11, 6, 7), ), ( SegmentEffort, {"start_date_local": "2023-01-17T11:06:07Z"}, datetime(2023, 1, 17, 11, 6, 7), ), ), ) def test_deserialization_edge_cases(model_class, raw, expected_value): obj = model_class.model_validate(raw) assert getattr(obj, list(raw.keys())[0]) == expected_value @pytest.mark.parametrize( "model_class,raw,parsed_attr,expected_parsed_attr_value", ( (SummarySegmentEffort, {"pr_activity_id": 42}, "activity_id", 42), (SummarySegmentEffort, {"activity_id": 42}, "activity_id", 42), ( SummarySegmentEffort, {"pr_elapsed_time": 42}, "elapsed_time", 42, ), ( SummarySegmentEffort, {"elapsed_time": 42}, "elapsed_time", 42, ), (AthletePrEffort, {"pr_activity_id": 42}, "activity_id", 42), (AthletePrEffort, {"activity_id": 42}, "activity_id", 42), ( AthletePrEffort, {"pr_elapsed_time": 42}, "elapsed_time", 42, ), ( AthletePrEffort, {"elapsed_time": 42}, "elapsed_time", 42, ), ), ) def test_strava_api_field_name_inconsistencies( model_class, raw, parsed_attr, expected_parsed_attr_value ): obj = model_class.model_validate(raw) assert getattr(obj, parsed_attr) == expected_parsed_attr_value def test_subscription_callback_field_names(): sub_callback_raw = { "hub.mode": "subscribe", "hub.verify_token": "STRAVA", "hub.challenge": "15f7d1a91c1f40f8a748fd134752feb3", } sub_callback = SubscriptionCallback.model_validate(sub_callback_raw) assert sub_callback.hub_mode == "subscribe" assert sub_callback.hub_verify_token == "STRAVA" @pytest.mark.parametrize( "klass,attr,given_type,expected_type", ( (DetailedActivity, "sport_type", "Run", "Run"), (DetailedActivity, "sport_type", "FooBar", "Workout"), (DetailedActivity, "type", "Run", "Run"), (DetailedActivity, "type", "FooBar", "Workout"), (Segment, "activity_type", "Run", "Run"), (Segment, "activity_type", "FooBar", "Workout"), ), ) def test_relaxed_activity_type_validation( klass, attr, given_type, expected_type ): obj = klass.model_validate({attr: given_type}) assert getattr(obj, attr) == expected_type @pytest.mark.parametrize( "a_attr,a_value,b_attr,b_value,expected_attr_equality", ( ("type", "Run", "type", "Run", True), ("type", "Run", "type", "Ride", False), ("type", "Run", "id", 42, False), ("sport_type", "Run", "sport_type", "Run", True), ("sport_type", "Run", "sport_type", "Ride", False), ("sport_type", "Run", "id", 42, False), ), ) def test_relaxed_activity_type_equality( a_attr, a_value, b_attr, b_value, expected_attr_equality ): a = SummaryActivity.model_validate({a_attr: a_value}) b = SummaryActivity.model_validate({b_attr: b_value}) assert (getattr(a, a_attr) == getattr(b, b_attr)) == expected_attr_equality @pytest.mark.parametrize( "model_type,attr,expected_base_type,expected_extended_type", ( (ActivityTotals, "distance", float, Distance), (ActivityTotals, "elevation_gain", float, Distance), (ActivityTotals, "elapsed_time", int, Duration), (ActivityTotals, "moving_time", int, Duration), (AthleteStats, "biggest_ride_distance", float, Distance), (AthleteStats, "biggest_climb_elevation_gain", float, Distance), (Lap, "distance", float, Distance), (Lap, "total_elevation_gain", float, Distance), (Lap, "average_speed", float, Velocity), (Lap, "max_speed", float, Velocity), (Lap, "elapsed_time", int, Duration), (Lap, "moving_time", int, Duration), (Split, "distance", float, Distance), (Split, "elevation_difference", float, Distance), (Split, "average_speed", float, Velocity), (Split, "average_grade_adjusted_speed", float, Velocity), (Split, "elapsed_time", int, Duration), (Split, "moving_time", int, Duration), (SegmentExplorerResult, "elev_difference", float, Distance), (SegmentExplorerResult, "distance", float, Distance), (AthleteSegmentStats, "distance", float, Distance), (AthleteSegmentStats, "elapsed_time", int, Duration), (AthletePrEffort, "distance", float, Distance), (AthletePrEffort, "pr_elapsed_time", int, Duration), (Segment, "distance", float, Distance), (Segment, "elevation_high", float, Distance), (Segment, "elevation_low", float, Distance), (Segment, "total_elevation_gain", float, Distance), (BaseEffort, "distance", float, Distance), (BaseEffort, "elapsed_time", int, Duration), (BaseEffort, "moving_time", int, Duration), (DetailedActivity, "distance", float, Distance), (DetailedActivity, "timezone", str, Timezone), (DetailedActivity, "total_elevation_gain", float, Distance), (DetailedActivity, "average_speed", float, Velocity), (DetailedActivity, "max_speed", float, Velocity), (DetailedActivity, "elapsed_time", int, Duration), (DetailedActivity, "moving_time", int, Duration), (Route, "distance", float, Distance), (Route, "elevation_gain", float, Distance), ), ) def test_extended_types( model_type, attr, expected_base_type, expected_extended_type ): test_value = "Europe/Amsterdam" if expected_base_type == str else 42 obj = model_type.model_validate(dict(**{attr: test_value})) assert isinstance(getattr(obj, attr), expected_base_type) assert isinstance(getattr(obj, attr), expected_extended_type) @pytest.mark.parametrize( "model_type,attr,extended_attr,expected_base_value,expected_extended_value", ( ( ActivityTotals, "elapsed_time", "timedelta", 42, timedelta(seconds=42), ), (ActivityTotals, "distance", "quantity", 42, uh.meters(42)), (Lap, "average_speed", "quantity", 42, uh.meters_per_second(42)), ), ) def test_extended_types_values( model_type, attr, extended_attr, expected_base_value, expected_extended_value, ): obj = model_type.model_validate(dict(**{attr: 42})) base_attr = getattr(obj, attr) extended_attr = getattr(base_attr, extended_attr)() assert base_attr == expected_base_value assert extended_attr == expected_extended_value @pytest.mark.parametrize( "arg,expected_value", ( ("Factory", None), ("(GMT+00:00) Factory", None), ("Europe/Amsterdam", pytz.timezone("Europe/Amsterdam")), ("(GMT+01:00) Europe/Amsterdam", pytz.timezone("Europe/Amsterdam")), ), ) def test_timezone(arg, expected_value): tz = Timezone(arg) assert tz.timezone() == expected_value class ModelTest(TestBase): def setUp(self): super(ModelTest, self).setUp() def test_entity_collections(self) -> None: """Test that club information parsed from the API in a dict format can be correctly ingested into the Athlete model. Notes ----- In Pydantic 2.x we use `model_validate` instead of `parse_object`. Model_Validate always returns a new model. In this test we instantiate a new instance a when calling `model_validate` aligning with Pydantic's immutability approach. """ d = { "clubs": [ {"resource_state": 2, "id": 7, "name": "Team Roaring Mouse"}, {"resource_state": 2, "id": 1, "name": "Team Strava Cycling"}, { "resource_state": 2, "id": 34444, "name": "Team Strava Cyclocross", }, ] } a = model.DetailedAthlete.model_validate(d) self.assertEqual(3, len(a.clubs)) self.assertEqual("Team Roaring Mouse", a.clubs[0].name) def test_subscription_deser(self): d = { "id": 1, "object_type": "activity", "aspect_type": "create", "callback_url": "http://you.com/callback/", "created_at": "2015-04-29T18:11:09.400558047-07:00", "updated_at": "2015-04-29T18:11:09.400558047-07:00", } sub = model.Subscription.model_validate(d) self.assertEqual(d["id"], sub.id) def test_subscription_update_deser(self): d = { "subscription_id": "1", "owner_id": 13408, "object_id": 12312312312, "object_type": "activity", "aspect_type": "create", "event_time": 1297286541, } subupd = model.SubscriptionUpdate.model_validate(d) self.assertEqual( "2011-02-09 21:22:21", subupd.event_time.strftime("%Y-%m-%d %H:%M:%S"), ) # Test cases for the naive_datetime function @pytest.mark.parametrize( "input_value, expected_output, exception", [ (0, datetime(1970, 1, 1), None), ("2024-04-28T12:00:00Z", datetime(2024, 4, 28, 12, 0), None), ( int(datetime(2022, 4, 28, 12, 0, tzinfo=timezone.utc).timestamp()), datetime(2022, 4, 28, 12, 0), None, ), ( str( int( datetime( 2022, 4, 28, 12, 0, tzinfo=timezone.utc ).timestamp() ) ), datetime(2022, 4, 28, 12, 0), None, ), ( datetime(2024, 4, 28, 12, 0, tzinfo=timezone.utc), datetime(2024, 4, 28, 12, 0), None, ), ( "April 28, 2024 12:00 PM UTC", datetime(2024, 4, 28, 12, 0), None, ), ("Foo", None, ValueError), ({"foo": 42}, None, ValueError), (None, None, None), ], ) def test_naive_datetime(input_value, expected_output, exception): """Make sure our datetime parses properly reformats dates in various formats. This test is important given the pydantic `parse_datetime` method was removed in 2.x # https://docs.pydantic.dev/1.10/usage/types/#datetime-types Values 1. date time as a formatted string 2. datetime as a UNIX or POSIX format """ if exception: with pytest.raises(exception): naive_datetime(input_value) else: assert naive_datetime(input_value) == expected_output stravalib-2.2/src/stravalib/tests/unit/test_protocol_ApiV3_token_refresh.py000066400000000000000000000140701475174155400274540ustar00rootroot00000000000000import logging import os import time from unittest.mock import MagicMock, patch import pytest import stravalib ###### _request method responsible for API call ###### @pytest.mark.parametrize( "url, should_call_refresh", [ ("/oauth/token", False), # Skips refreshing for /oauth/token ( "/api/some_other_endpoint", True, ), ], ) def test_request_skips_refresh_for_oauth_token( apiv3_instance, url, should_call_refresh ): """Test that _request skips refresh_expired_token when provided with a url that includes /oauth/token to avoid recursion""" apiv3_instance.refresh_expired_token = MagicMock() apiv3_instance.resolve_url = MagicMock(return_value="/oauth/token") apiv3_instance.rsession.get = MagicMock( return_value=MagicMock(status_code=204, headers={}) ) apiv3_instance._request(url) if should_call_refresh: apiv3_instance.refresh_expired_token.assert_called_once() else: apiv3_instance.refresh_expired_token.assert_not_called() ###### _check_credentials helper ###### class TestCheckCredentials: """Tests for the `_check_credentials` app setup helper method.""" @pytest.fixture(autouse=True) def setup(self, apiv3_instance): """Shared setup for all tests in this class.""" self.apiv3 = apiv3_instance def test_check_credentials_success(self): """Ensure that if credentials exist in the environment and client_id can be cast to a valid int that the method populates self. client ID and secret as expected.""" self.apiv3._check_credentials() assert isinstance(self.apiv3.client_id, int) @pytest.mark.parametrize( "env_vars, expected_log_message", [ ( { "STRAVA_CLIENT_ID": "not_an_int", "STRAVA_CLIENT_SECRET": "123ghp234", }, "STRAVA_CLIENT_ID must be a valid integer.", ), # Invalid client ID ( {"STRAVA_CLIENT_SECRET": "123ghp234"}, "Please make sure your STRAVA_CLIENT_ID is set", ), # Missing client ID ( {"STRAVA_CLIENT_ID": "12345"}, "STRAVA_CLIENT_ID and STRAVA_CLIENT_SECRET not found", ), # Missing secret ], ) def test_check_credentials_missing_vals( self, caplog, env_vars, expected_log_message ): """Test that if parts of all of the credentials are missing, the method logs the correct warning and returns `None`. Here we overwriting environment variables to see if _check_credentials works as expected with various user envt edge cases.""" with patch.dict(os.environ, env_vars, clear=True): with caplog.at_level(logging.WARNING): credentials = self.apiv3._check_credentials() assert expected_log_message in caplog.text assert credentials is None ###### refresh_expired_token method ###### @patch.object(stravalib.protocol.ApiV3, "refresh_access_token") @patch.object(stravalib.protocol.ApiV3, "_token_expired", return_value=True) def test_refresh_expired_token_calls_refresh_access_token( token_expired_mock, refresh_access_token_mock, apiv3_instance, ): """Test that `refresh_access_token` is called when the token is expired and the refresh token exists. This tests mocks refresh_access_token so it doesn't run, but checks to make sure it's called as it should be if all values are present. """ out = apiv3_instance.refresh_expired_token() refresh_access_token_mock.assert_called_once() # Should return none assert not out ###### _token_expired helper ###### @pytest.mark.parametrize( "token_expires, expected_result", [ (time.time() - 10, True), # Token expired (time.time() + 10, False), ], ) def test_token_expired(apiv3_instance, token_expires, expected_result): """Test the _token_expired method for both expired and valid tokens.""" apiv3_instance.token_expires = token_expires assert apiv3_instance._token_expired() == expected_result # caplog is a builtin fixture in pytest that captures logging outputs def test_token_expires_is_none_logs_warning(apiv3_instance, caplog): """Test that _token_expired logs a warning if token_expires is None.""" apiv3_instance.token_expires = None with caplog.at_level(logging.WARNING): assert not apiv3_instance._token_expired() assert ( "Please make sure you've set client.token_expires" in caplog.text ) class TestRefreshExpiredToken: """Tests for the `refresh_expired_token` method.""" @pytest.fixture(autouse=True) def setup(self, apiv3_instance): """ Shared setup for all tests in this class. Mocks `_token_expired` and `refresh_access_token` and sets up the environment variables. """ self.apiv3 = apiv3_instance self.apiv3._token_expired = MagicMock() self.apiv3.refresh_access_token = MagicMock() @pytest.mark.parametrize( "token_expired, expected_call", [(True, 1), (False, 0)] ) def test_refresh_expired_token_refresh_called( self, token_expired, expected_call ): """Test that `refresh_access_token` method is called when token is expired and not called when valid. """ self.apiv3._token_expired.return_value = token_expired self.apiv3.refresh_expired_token() assert self.apiv3.refresh_access_token.call_count == expected_call def test_refresh_expired_token_no_refresh(self, caplog): """Test that we fail gracefully and log an error if the refresh token isn't populated. """ self.apiv3.refresh_token = None self.apiv3._token_expired.return_value = True with caplog.at_level(logging.WARNING): self.apiv3.refresh_expired_token() assert ( "Please set client.refresh_token if you want to use" in caplog.text ) stravalib-2.2/src/stravalib/tests/unit/test_protocol_auth.py000066400000000000000000000035461475174155400245630ustar00rootroot00000000000000from unittest.mock import patch import pytest from stravalib.protocol import ApiV3 @pytest.fixture def mock_token_exchange_response(): """A fixture that mocks a token exchange response that includes the athlete info""" return { "access_token": "mock_access_token", "refresh_token": "mock_refresh_token", "expires_at": 1234567890, "athlete": { "id": 12345, "username": "mock_athlete", "firstname": "Mock", "lastname": "Athlete", }, } @pytest.fixture def mock_token_refresh_response(): """A fixture that mocks the response for a refreshed token.""" return { "access_token": "new_mock_access_token", "refresh_token": "new_mock_refresh_token", "expires_at": 9876543210, } @patch("stravalib.protocol.ApiV3._request") def test_exchange_code_for_token_athlete( mock_request, mock_token_exchange_response ): # Set _request to return the raw dictionary api_instance = ApiV3() mock_request.return_value = mock_token_exchange_response exchange_response, athlete = api_instance.exchange_code_for_token( client_id=123, client_secret="secret", code="auth_code", return_athlete=True, ) assert exchange_response["access_token"] == "mock_access_token" assert athlete["id"] == 12345 @patch("stravalib.protocol.ApiV3._request") def test_exchange_code_for_token_no_athlete( mock_request, mock_token_exchange_response ): api_instance = ApiV3() mock_request.return_value = mock_token_exchange_response access_info, athlete_info = api_instance.exchange_code_for_token( client_id=123, client_secret="secret", code="auth_code", ) assert athlete_info is None assert len(access_info) == 3 assert access_info["access_token"] == "mock_access_token" stravalib-2.2/src/stravalib/tests/unit/test_unithelper.py000066400000000000000000000012711475174155400240510ustar00rootroot00000000000000import pytest from stravalib import unit_helper as uh from stravalib.unit_helper import _Quantity class DistanceQuantity(_Quantity): unit = "feet" @pytest.mark.parametrize( "expression,expected_unitless_result", ((uh.ureg("feet") * 6, 1.83), (DistanceQuantity(6), 1.83), (2.0, 2.0)), ) def test_unit_converter(expression, expected_unitless_result): converter = uh.UnitConverter("meters") assert converter(expression).magnitude == pytest.approx( expected_unitless_result, abs=0.01 ) def test_arithmetic_comparison_support(): assert uh.meters(2) == uh.meters(2) assert uh.meters(2) > uh.meters(1) assert uh.meters(2) + uh.meters(1) == uh.meters(3) stravalib-2.2/src/stravalib/tests/unit/test_validate_activity_type.py000066400000000000000000000035171475174155400264450ustar00rootroot00000000000000import pytest @pytest.fixture def params(): """A fixture that creates a dictionary representing parameters associated with a Strava activity.""" params = { "name": "New Activity", "start_date_local": "2024-03-04T18:56:47Z", "elapsed_time": 9000, "description": "An activity description here.", "distance": 5700, } return params # TODO: potentially redo this so each expected and return type is a dict. # then simplify the logic below @pytest.mark.parametrize( "sport_type, activity_type, expected_result, expected_exception", ( ("TrailRun", None, {"sport_type": "TrailRun"}, None), ("funrun", None, None, ValueError), (None, "Run", {"type": "run"}, None), (None, "junoDog", None, ValueError), ("TrailRun", "Run", {"sport_type": "TrailRun"}, None), ), ) def test_validate_activity_type( params, client, activity_type, sport_type, expected_result, expected_exception, ): """Test the validation step for create and update activity. Create_activity should require with sport_type (or type but type is deprecated). As such we test to ensure that a request to create an activity that is missing sport_type returns a ValueError """ if expected_exception: # TODO: should we have a error message specific to this error here? # value = "output error message" with pytest.raises(expected_exception): client._validate_activity_type(params, activity_type, sport_type) else: out_params = client._validate_activity_type( params, activity_type, sport_type ) # This is a "merge" or dict union introduced in 3.9 # It combines the two dictionaries updating overlapping key(/val) pairs assert params | expected_result == out_params stravalib-2.2/src/stravalib/unit_helper.py000066400000000000000000000035741475174155400210400ustar00rootroot00000000000000""" Unit Helper ============== Helpers for converting Strava's units to something more practical. """ from typing import Any import pint from pint.facets.plain import PlainQuantity from stravalib.unit_registry import ureg class _Quantity(float): """ Subtype of float that can represent quantities by adding a unit """ unit: str """ The quantity's unit """ def quantity(self) -> pint.Quantity: """ Returns the base type (e.g., float) as a pint.Quantity by attaching the unit to it. """ return ureg.Quantity(self, self.unit) class UnitConverter: """ Callable that converts quantities or unitless numbers to quantities of its unit """ def __init__(self, unit: str) -> None: self.unit = unit def __call__( self, q: _Quantity | pint.Quantity | float ) -> PlainQuantity[Any]: if isinstance(q, pint.Quantity): return q.to(self.unit) elif isinstance(q, _Quantity): return q.quantity().to(self.unit) else: # unitless number: simply return a Quantity return ureg.Quantity(q, self.unit) meter = meters = UnitConverter("m") second = seconds = UnitConverter("s") hour = hours = UnitConverter("hour") foot = feet = UnitConverter("ft") mile = miles = UnitConverter("mi") kilometer = kilometers = UnitConverter("km") meters_per_second = UnitConverter("m/s") miles_per_hour = mph = UnitConverter("mi/hour") kilometers_per_hour = kph = UnitConverter("km/hour") kilogram = kilograms = kg = kgs = UnitConverter("kg") pound = pounds = lb = lbs = UnitConverter("lb") def c2f(celsius: float) -> float: """ Convert Celsius to Fahrenheit. Parameters ---------- celsius : Temperature in Celsius. Returns ------- float Temperature in Fahrenheit. """ return (9.0 / 5.0) * celsius + 32 stravalib-2.2/src/stravalib/unit_registry.py000066400000000000000000000001101475174155400214100ustar00rootroot00000000000000from pint import UnitRegistry ureg = UnitRegistry() Q_ = ureg.Quantity stravalib-2.2/src/stravalib/util/000077500000000000000000000000001475174155400171145ustar00rootroot00000000000000stravalib-2.2/src/stravalib/util/__init__.py000066400000000000000000000000001475174155400212130ustar00rootroot00000000000000stravalib-2.2/src/stravalib/util/limiter.py000066400000000000000000000176561475174155400211520ustar00rootroot00000000000000"""Utilities ============== Rate limiter classes. These are basically callables that when called register that a request was issued. Depending on how they are configured that may cause a pause or exception if a rate limit has been exceeded. It is up to the calling code to ensure that these callables are invoked with every (successful?) call to the backend API. TODO: There is probably a better way to hook these into the requests library directly From the Strava docs: Strava API usage is limited on a per-application basis using a short term, 15 minute, limit and a long term, daily, limit. The default rate limit allows 600 requests every 15 minutes, with up to 30,000 requests per day. This limit allows applications to make 40 requests per minute for about half the day. """ from __future__ import annotations import logging import time from collections.abc import Callable from logging import Logger from typing import Literal, NamedTuple import arrow from stravalib.protocol import RequestMethod class RequestRate(NamedTuple): """Tuple containing request usage and usage limit.""" short_usage: int """15-minute usage""" long_usage: int """Daily usage""" short_limit: int """15-minutes limit""" long_limit: int """Daily limit""" def get_rates_from_response_headers( headers: dict[str, str], method: RequestMethod ) -> RequestRate | None: """Returns a namedtuple with values for short - and long usage and limit rates found in provided HTTP response headers Parameters ---------- headers : dict HTTP response headers method : RequestMethod HTTP request method corresponding to the provided response headers Returns ------- Optional[RequestRate] namedtuple with request rates or None if no rate-limit headers present in response. """ usage_rates = limit_rates = [] if "X-ReadRateLimit-Usage" in headers and method == "GET": usage_rates = [ int(v) for v in headers["X-ReadRateLimit-Usage"].split(",") ] limit_rates = [ int(v) for v in headers["X-ReadRateLimit-Limit"].split(",") ] elif "X-RateLimit-Usage" in headers: usage_rates = [int(v) for v in headers["X-RateLimit-Usage"].split(",")] limit_rates = [int(v) for v in headers["X-RateLimit-Limit"].split(",")] if usage_rates and limit_rates: return RequestRate( short_usage=usage_rates[0], long_usage=usage_rates[1], short_limit=limit_rates[0], long_limit=limit_rates[1], ) else: return None def get_seconds_until_next_quarter( now: arrow.arrow.Arrow | None = None, ) -> int: """Returns the number of seconds until the next quarter of an hour. This is the short-term rate limit used by Strava. Parameters ---------- now : arrow.arrow.Arrow A (utc) timestamp Returns ------- int The number of seconds until the next quarter, as int """ if now is None: now = arrow.utcnow() return ( 899 - ( now - now.replace( minute=(now.minute // 15) * 15, second=0, microsecond=0 ) ).seconds ) def get_seconds_until_next_day(now: arrow.arrow.Arrow | None = None) -> int: """Returns the number of seconds until the next day (utc midnight). This is the long-term rate limit used by Strava. Parameters ---------- now : arrow.arrow.Arrow A (utc) timestamp Returns ------- Int The number of seconds until next day, as int """ if now is None: now = arrow.utcnow() return (now.ceil("day") - now).seconds class SleepingRateLimitRule: """A rate limit rule that can be prioritized and can dynamically adapt its limits based on API responses. Given its priority, it will enforce a variable "cool-down" period after each response. When rate limits are reached within their period, this limiter will wait until the end of that period. It will NOT raise any kind of exception in this case. """ def __init__( self, priority: Literal["low", "medium", "high"] = "high", ) -> None: """ Constructs a new SleepingRateLimitRule. Parameters ---------- priority : Literal["low", "medium", "high"] The priority for this rule. When 'low', the cool-down period after each request will be such that the long-term limits will not be exceeded. When 'medium', the cool-down period will be such that the short-term limits will not be exceeded. When 'high', there will be no cool-down period. """ if priority not in ["low", "medium", "high"]: raise ValueError( f'Invalid priority "{priority}", expecting one of "low", "medium" or "high"' ) self.log = logging.getLogger( "{0.__module__}.{0.__name__}".format(self.__class__) ) self.priority = priority def _get_wait_time( self, rates: RequestRate, seconds_until_short_limit: int, seconds_until_long_limit: int, ) -> float: """Calculate how much time user has until they can make another request""" if rates.long_usage >= rates.long_limit: self.log.warning("Long term API rate limit exceeded") return seconds_until_long_limit elif rates.short_usage >= rates.short_limit: self.log.warning("Short term API rate limit exceeded") return seconds_until_short_limit if self.priority == "high": return 0 elif self.priority == "medium": return seconds_until_short_limit / ( rates.short_limit - rates.short_usage ) elif self.priority == "low": return seconds_until_long_limit / ( rates.long_limit - rates.long_usage ) def __call__( self, response_headers: dict[str, str], method: RequestMethod ) -> None: """Determines wait time until a call can be made again""" rates = get_rates_from_response_headers(response_headers, method) self.log.debug(f"Throttling based on rates: {rates}") if rates: time.sleep( self._get_wait_time( rates, get_seconds_until_next_quarter(), get_seconds_until_next_day(), ) ) else: self.log.warning("No rates present in response headers") class RateLimiter: def __init__(self) -> None: self.log: Logger = logging.getLogger( "{0.__module__}.{0.__name__}".format(self.__class__) ) self.rules: list[Callable[[dict[str, str], RequestMethod], None]] = [] def __call__(self, args: dict[str, str], method: RequestMethod) -> None: """Register another request is being issued.""" for r in self.rules: r(args, method) class DefaultRateLimiter(RateLimiter): """Implements something similar to the default rate limit for Strava apps. See https://developers.strava.com/docs/rate-limits/ and https://communityhub.strava.com/t5/developer-knowledge-base/our-developer-program/ta-p/8849. Rate limits are enforced by throttling requests based on their method and client/app-specific limits imposed by Strava. """ def __init__( self, priority: Literal["low", "medium", "high"] = "high" ) -> None: """ Initializes the rate limiter based on the given priority. Parameters ---------- priority : Literal["low", "medium", "high"] The priority given to the requests. Default is "high" (i.e. no throttling). """ super().__init__() self.rules.append(SleepingRateLimitRule(priority=priority)) # TODO: This should be added to our documentation