pax_global_header00006660000000000000000000000064146301274300014512gustar00rootroot0000000000000052 comment=5b8659610502b0ff887061132a36a3ae1c606ef8 conda-package-handling-2.3.0/000077500000000000000000000000001463012743000157535ustar00rootroot00000000000000conda-package-handling-2.3.0/.authors.yml000066400000000000000000000065511463012743000202500ustar00rootroot00000000000000- name: Michael Sarahan email: msarahan@gmail.com github: msarahan alternate_emails: - msarahan@continuum.io - msarahan@anaconda.com - msarahan@gmail.com aliases: - Mike Sarahan - Michael Sarahan num_commits: 149 first_commit: 2019-01-03 20:00:40 - name: Nehal J Wani email: nehaljw.kkd1@gmail.com num_commits: 4 first_commit: 2019-05-01 08:25:27 github: nehaljwani - name: Jonathan J. Helmus email: jjhelmus@gmail.com num_commits: 10 first_commit: 2019-08-02 14:19:54 github: jjhelmus - name: Alan Du email: alanhdu@gmail.com num_commits: 4 first_commit: 2019-11-07 05:49:27 github: alanhdu - name: Ray Donnelly email: mingw.android@gmail.com num_commits: 14 first_commit: 2020-05-06 10:49:55 github: mingwandroid - name: Pure Software email: ossdev@puresoftware.com aliases: - ossdev07 num_commits: 1 first_commit: 2020-01-06 10:18:31 github: ossdev07 - name: John Lee email: johnleenimh@gmail.com aliases: - leej3 num_commits: 12 first_commit: 2021-02-23 12:20:16 github: leej3 - name: Matthew R. Becker email: beckermr@users.noreply.github.com num_commits: 2 first_commit: 2020-12-29 14:58:11 github: beckermr - name: Eli Uriegas email: seemethere101@gmail.com num_commits: 1 first_commit: 2021-03-14 08:41:18 github: seemethere - name: Daniel Bast email: 2790401+dbast@users.noreply.github.com num_commits: 3 first_commit: 2021-04-12 11:49:45 github: dbast - name: Conda Bot email: 18747875+conda-bot@users.noreply.github.com num_commits: 32 first_commit: 2022-01-17 20:22:29 github: conda-bot aliases: - conda-bot - Distro Bot - distro-bot@anaconda.com - conda bot alternate_emails: - ad-team+condabot@anaconda.com - 18747875+conda-bot@users.noreply.github.com - Distro Bot - distro-bot@anaconda.com - name: Cheng H. Lee email: clee@anaconda.com alternate_emails: - chenghlee@users.noreply.github.com num_commits: 4 first_commit: 2022-02-09 18:08:33 - name: Chris Burr email: chrisburr@users.noreply.github.com num_commits: 1 first_commit: 2022-02-17 10:23:53 - name: Daniel Holth email: dholth@anaconda.com num_commits: 26 first_commit: 2021-08-20 21:11:50 github: dholth - name: Vadim Zayakin email: 77290357+vz-x@users.noreply.github.com aliases: - vz-x num_commits: 1 first_commit: 2022-02-17 11:31:22 github: vz-x - name: Christopher Barber email: christopher.barber@analog.com num_commits: 2 first_commit: 2022-02-09 01:00:38 - name: Jannis Leidel email: jannis@leidel.info num_commits: 9 first_commit: 2021-09-17 21:51:27 github: jezdez - name: Tobias "Tobi" Koch email: tkoch@anaconda.com num_commits: 1 first_commit: 2022-03-31 19:54:23 - name: Marius van Niekerk email: marius.v.niekerk@gmail.com num_commits: 1 first_commit: 2022-07-19 15:55:18 github: mariusvniekerk - name: Ken Odegard email: kodegard@anaconda.com num_commits: 5 first_commit: 2022-06-15 15:11:13 github: kenodegard - name: pre-commit-ci[bot] email: 66853113+pre-commit-ci[bot]@users.noreply.github.com github: pre-commit-ci[bot] num_commits: 14 first_commit: 2023-01-20 04:55:56 - name: Justin Wood (Callek) email: callek@gmail.com num_commits: 1 first_commit: 2024-05-21 11:47:03 github: callek - name: jaimergp email: jaimergp@users.noreply.github.com num_commits: 2 first_commit: 2024-04-02 13:11:21 github: jaimergp conda-package-handling-2.3.0/.coveragerc000066400000000000000000000002211463012743000200670ustar00rootroot00000000000000[report] omit = setup.py src/conda_package_handling/__main__.py src/conda_package_handling/_version.py versioneer.py tests/* conda-package-handling-2.3.0/.gitattributes000066400000000000000000000001071463012743000206440ustar00rootroot00000000000000src/conda_package_handling/_version.py export-subst * text=auto eol=lf conda-package-handling-2.3.0/.github/000077500000000000000000000000001463012743000173135ustar00rootroot00000000000000conda-package-handling-2.3.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001463012743000214765ustar00rootroot00000000000000conda-package-handling-2.3.0/.github/ISSUE_TEMPLATE/0_bug.yml000066400000000000000000000053001463012743000232130ustar00rootroot00000000000000name: Bug Report description: Create a bug report. labels: - type::bug body: - type: markdown attributes: value: | Because processing new bug reports is time-consuming, we would like to ask you to fill out the following form to the best of your ability and as completely as possible. > [!NOTE] > Bug reports that are incomplete or missing information may be closed as inactionable. Since there are already a lot of open issues, please also take a moment to search existing ones to see if your bug has already been reported. If you find something related, please upvote that issue and provide additional details as necessary. 💐 Thank you for helping to make Conda better. We would be unable to improve Conda without our community! - type: checkboxes id: checks attributes: label: Checklist description: Please confirm and check all of the following options. options: - label: I added a descriptive title required: true - label: I searched open reports and couldn't find a duplicate required: true - type: textarea id: what attributes: label: What happened? description: What should have happened instead? Please provide as many details as possible. The more information provided, the more likely we are able to replicate your problem and offer a solution. validations: required: true - type: textarea id: info attributes: label: Conda Info description: | Let's collect some basic information about your conda install. Please run the following command from your command line and paste the output below. ```bash conda info ``` render: shell - type: textarea id: config attributes: label: Conda Config description: | Let's collect any customizations you may have for your conda install. Please run the following command from your command line and paste the output below. ```bash conda config --show-sources ``` render: shell - type: textarea id: list attributes: label: Conda list description: | The packages installed into your environment can offer clues as to the problem you are facing. Please activate the environment within which you are encountering this bug, run the following command from your command line, and paste the output below. ```bash conda list --show-channel-urls ``` render: shell - type: textarea id: context attributes: label: Additional Context description: Include any additional information (or screenshots) that you think would be valuable. conda-package-handling-2.3.0/.github/ISSUE_TEMPLATE/1_feature.yml000066400000000000000000000036131463012743000240770ustar00rootroot00000000000000name: Feature Request description: Create a feature request. labels: - type::feature body: - type: markdown attributes: value: | Because processing new feature requests is time-consuming, we would like to ask you to fill out the following form to the best of your ability and as completely as possible. > [!NOTE] > Feature requests that are incomplete or missing information may be closed as inactionable. Since there are already a lot of open issues, please also take a moment to search existing ones to see if your feature request has already been submitted. If you find something related, please upvote that issue and provide additional details as necessary. 💐 Thank you for helping to make Conda better. We would be unable to improve Conda without our community! - type: checkboxes id: checks attributes: label: Checklist description: Please confirm and check all of the following options. options: - label: I added a descriptive title required: true - label: I searched open requests and couldn't find a duplicate required: true - type: textarea id: idea attributes: label: What is the idea? description: Describe what the feature is and the desired state. validations: required: true - type: textarea id: why attributes: label: Why is this needed? description: Who would benefit from this feature? Why would this add value to them? What problem does this solve? - type: textarea id: what attributes: label: What should happen? description: What should be the user experience with the feature? Describe from a user perspective what they would do and see. - type: textarea id: context attributes: label: Additional Context description: Include any additional information that you think would be valuable. conda-package-handling-2.3.0/.github/ISSUE_TEMPLATE/2_documentation.yml000066400000000000000000000026511463012743000253170ustar00rootroot00000000000000name: Documentation description: Create a documentation related issue. labels: - type::documentation body: - type: markdown attributes: value: | > [!NOTE] > Documentation requests that are incomplete or missing information may be closed as inactionable. Since there are already a lot of open issues, please also take a moment to search existing ones to see if your bug has already been reported. If you find something related, please upvote that issue and provide additional details as necessary. 💐 Thank you for helping to make conda better. We would be unable to improve conda without our community! - type: checkboxes id: checks attributes: label: Checklist description: Please confirm and check all of the following options. options: - label: I added a descriptive title required: true - label: I searched open reports and couldn't find a duplicate required: true - type: textarea id: what attributes: label: What happened? description: Mention here any typos, broken links, or missing, incomplete, or outdated information, etc. that you have noticed in the conda docs or CLI help. validations: required: true - type: textarea id: context attributes: label: Additional Context description: Include any additional information (or screenshots) that you think would be valuable. conda-package-handling-2.3.0/.github/ISSUE_TEMPLATE/epic.yml000066400000000000000000000060741463012743000231500ustar00rootroot00000000000000name: Epic description: A collection of related tickets. labels: - epic body: - type: markdown attributes: value: | This form is intended for grouping and collecting together related tickets to better gauge the scope of a problem/feature. If you are attempting to report a bug, propose a new feature, or some other code change please use one of the other forms available. > [!NOTE] > Epics that are incomplete or missing information may be closed as inactionable. Since there are already a lot of open issues, please also take a moment to search existing ones to see if a similar epic has already been opened. If you find something related, please upvote that issue and provide additional details as necessary. 💐 Thank you for helping to make Conda better. We would be unable to improve Conda without our community! - type: checkboxes id: checks attributes: label: Checklist description: Please confirm and check all of the following options. options: - label: I added a descriptive title required: true - label: I searched open issues and couldn't find a duplicate required: true - type: textarea id: what attributes: label: What? description: >- What feature or problem will be addressed in this epic? placeholder: Please describe here. validations: required: true - type: textarea id: why attributes: label: Why? description: >- Why is the reported issue(s) a problem, or why is the proposed feature needed? (Research and spike issues can be linked here.) value: | - [ ] placeholder: Please describe here and/or link to relevant supporting issues. validations: required: true - type: textarea id: user_impact attributes: label: User impact description: >- In what specific way(s) will users benefit from this change? (e.g. use cases or performance improvements) placeholder: Please describe here. validations: required: true - type: textarea id: goals attributes: label: Goals description: >- What goal(s) should this epic accomplish? value: | - [ ] validations: required: true - type: textarea id: tasks attributes: label: Tasks description: >- What needs to be done to implement this change? value: | - [ ] validations: required: false - type: textarea id: blocked_by attributes: label: 'This epic is blocked by:' description: >- Epics and issues that block this epic. value: | - [ ] validations: required: false - type: textarea id: blocks attributes: label: 'This epic blocks:' description: >- Epics and issues that are blocked by this epic. value: | - [ ] validations: required: false conda-package-handling-2.3.0/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000027161463012743000231220ustar00rootroot00000000000000 ### Description ### Checklist - did you ... - [ ] Add a file to the `news` directory ([using the template](../blob/main/news/TEMPLATE)) for the next release's release notes? - [ ] Add / update necessary tests? - [ ] Add / update outdated documentation? conda-package-handling-2.3.0/.github/workflows/000077500000000000000000000000001463012743000213505ustar00rootroot00000000000000conda-package-handling-2.3.0/.github/workflows/cla.yml000066400000000000000000000021611463012743000226320ustar00rootroot00000000000000name: CLA on: issue_comment: types: - created pull_request_target: jobs: check: if: >- !github.event.repository.fork && ( github.event.issue.pull_request && github.event.comment.body == '@conda-bot check' || github.event_name == 'pull_request_target' ) runs-on: ubuntu-latest steps: - name: Check CLA uses: conda/actions/check-cla@976289d0cfd85139701b26ddd133abdd025a7b5f # v24.5.0 with: # [required] # A token with ability to comment, label, and modify the commit status # (`pull_request: write` and `statuses: write` for fine-grained PAT; `repo` for classic PAT) # (default: secrets.GITHUB_TOKEN) token: ${{ secrets.CLA_ACTION_TOKEN }} # [required] # Label to apply to contributor's PR once CLA is signed label: cla-signed # [required] # Token for opening signee PR in the provided `cla_repo` # (`pull_request: write` for fine-grained PAT; `repo` and `workflow` for classic PAT) cla_token: ${{ secrets.CLA_FORK_TOKEN }} conda-package-handling-2.3.0/.github/workflows/issues.yml000066400000000000000000000024651463012743000234150ustar00rootroot00000000000000name: Automate Issues on: # NOTE: github.event is issue_comment payload: # https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#issue_comment issue_comment: types: [created] env: FEEDBACK_LBL: pending::feedback SUPPORT_LBL: pending::support jobs: # NOTE: will update label if anyone responds, not just the author/reporter # TODO: create conda-issue-sorting team and modify this to toggle label based on # whether a non-issue-sorting engineer commented pending_support: # if [pending::feedback] and anyone responds if: >- !github.event.repository.fork && !github.event.issue.pull_request && contains(github.event.issue.labels.*.name, 'pending::feedback') runs-on: ubuntu-latest steps: # remove [pending::feedback] - uses: actions-ecosystem/action-remove-labels@2ce5d41b4b6aa8503e285553f75ed56e0a40bae0 # v1.3.0 with: labels: ${{ env.FEEDBACK_LBL }} github_token: ${{ secrets.PROJECT_TOKEN }} # add [pending::support], if still open - uses: actions-ecosystem/action-add-labels@18f1af5e3544586314bbe15c0273249c770b2daf # v1.1.3 if: github.event.issue.state == 'open' with: labels: ${{ env.SUPPORT_LBL }} github_token: ${{ secrets.PROJECT_TOKEN }} conda-package-handling-2.3.0/.github/workflows/labels.yml000066400000000000000000000027721463012743000233450ustar00rootroot00000000000000name: Sync Labels on: # NOTE: github.event is workflow_dispatch payload: # https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#workflow_dispatch workflow_dispatch: inputs: dryrun: description: 'dryrun: Preview changes to labels without editing them (true|false)' required: true type: boolean default: true jobs: sync: if: '!github.event.repository.fork' runs-on: ubuntu-latest env: GLOBAL: https://raw.githubusercontent.com/conda/infra/main/.github/global.yml LOCAL: .github/labels.yml steps: - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 - id: has_local uses: andstor/file-existence-action@076e0072799f4942c8bc574a82233e1e4d13e9d6 # v3.0.0 with: files: ${{ env.LOCAL }} - name: Global Only uses: EndBug/label-sync@52074158190acb45f3077f9099fea818aa43f97a # v2.3.3 if: steps.has_local.outputs.files_exists == 'false' with: config-file: ${{ env.GLOBAL }} delete-other-labels: true dry-run: ${{ github.event.inputs.dryrun }} - name: Global & Local uses: EndBug/label-sync@52074158190acb45f3077f9099fea818aa43f97a # v2.3.3 if: steps.has_local.outputs.files_exists == 'true' with: config-file: | ${{ env.GLOBAL }} ${{ env.LOCAL }} delete-other-labels: true dry-run: ${{ github.event.inputs.dryrun }} conda-package-handling-2.3.0/.github/workflows/lock.yml000066400000000000000000000037411463012743000230300ustar00rootroot00000000000000name: Lock on: # NOTE: github.event is workflow_dispatch payload: # https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#workflow_dispatch workflow_dispatch: schedule: - cron: 0 6 * * * permissions: issues: write pull-requests: write jobs: lock: if: '!github.event.repository.fork' runs-on: ubuntu-latest steps: - uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1 with: # Number of days of inactivity before a closed issue is locked issue-inactive-days: 365 # Do not lock issues created before a given timestamp, value must follow ISO 8601 exclude-issue-created-before: '' # Do not lock issues with these labels, value must be a comma separated list of labels or '' exclude-any-issue-labels: '' # Labels to add before locking an issue, value must be a comma separated list of labels or '' add-issue-labels: locked # Reason for locking an issue, value must be one of resolved, off-topic, too heated, spam or '' issue-lock-reason: resolved # Number of days of inactivity before a closed pull request is locked pr-inactive-days: 365 # Do not lock pull requests created before a given timestamp, value must follow ISO 8601 exclude-pr-created-before: '' # Do not lock pull requests with these labels, value must be a comma separated list of labels or '' exclude-any-pr-labels: '' # Labels to add before locking a pull request, value must be a comma separated list of labels or '' add-pr-labels: locked # Reason for locking a pull request, value must be one of resolved, off-topic, too heated, spam or '' pr-lock-reason: resolved # Limit locking to issues, pull requests or discussions, value must be a comma separated list of issues, prs, discussions or '' process-only: issues, prs conda-package-handling-2.3.0/.github/workflows/project.yml000066400000000000000000000011031463012743000235340ustar00rootroot00000000000000name: Add to Project on: issues: types: - opened pull_request_target: types: - opened jobs: add_to_project: if: '!github.event.repository.fork' runs-on: ubuntu-latest steps: - uses: actions/add-to-project@9bfe908f2eaa7ba10340b31e314148fcfe6a2458 # v1.0.1 with: # issues are added to the Planning project # PRs are added to the Review project project-url: https://github.com/orgs/conda/projects/${{ github.event_name == 'issues' && 2 || 16 }} github-token: ${{ secrets.PROJECT_TOKEN }} conda-package-handling-2.3.0/.github/workflows/sphinx.yml000066400000000000000000000021221463012743000234010ustar00rootroot00000000000000name: Sphinx on: push: branches: - main pull_request: branches: - main jobs: sphinx: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: "3.x" architecture: "x64" cache: "pip" - name: Build Documentation run: | pip install -e .[docs] pip install -r docs/requirements.txt # redundant? make html - name: Upload artifact uses: actions/upload-pages-artifact@v1 with: # Upload entire repository path: 'build/html' pages: runs-on: ubuntu-latest if: github.ref == 'refs/heads/main' needs: [sphinx] # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages permissions: contents: read pages: write id-token: write environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} steps: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v1 conda-package-handling-2.3.0/.github/workflows/stale.yml000066400000000000000000000072741463012743000232150ustar00rootroot00000000000000name: Stale on: # NOTE: github.event is workflow_dispatch payload: # https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#workflow_dispatch workflow_dispatch: inputs: dryrun: description: 'dryrun: Preview stale issues/prs without marking them (true|false)' required: true type: boolean default: true schedule: - cron: 0 4 * * * permissions: issues: write pull-requests: write jobs: stale: if: '!github.event.repository.fork' runs-on: ubuntu-latest strategy: matrix: include: - only-issue-labels: '' days-before-issue-stale: 365 days-before-issue-close: 30 # [type::support] issues have a more aggressive stale/close timeline - only-issue-labels: type::support days-before-issue-stale: 90 days-before-issue-close: 21 steps: - uses: conda/actions/read-yaml@976289d0cfd85139701b26ddd133abdd025a7b5f # v24.5.0 id: read_yaml with: path: https://raw.githubusercontent.com/conda/infra/main/.github/messages.yml - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0 id: stale with: # Only issues with these labels are checked whether they are stale only-issue-labels: ${{ matrix.only-issue-labels }} # Idle number of days before marking issues stale days-before-issue-stale: ${{ matrix.days-before-issue-stale }} # Idle number of days before closing stale issues/PRs days-before-issue-close: ${{ matrix.days-before-issue-close }} # Idle number of days before marking PRs stale days-before-pr-stale: 365 # Idle number of days before closing stale PRs days-before-pr-close: 30 # Comment on the staled issues stale-issue-message: ${{ fromJSON(steps.read_yaml.outputs.value)['stale-issue'] }} # Label to apply on staled issues stale-issue-label: stale # Label to apply on closed issues close-issue-label: stale::closed # Reason to use when closing issues close-issue-reason: not_planned # Comment on the staled PRs stale-pr-message: ${{ fromJSON(steps.read_yaml.outputs.value)['stale-pr'] }} # Label to apply on staled PRs stale-pr-label: stale # Label to apply on closed PRs close-pr-label: stale::closed # Remove stale label from issues/PRs on updates/comments remove-stale-when-updated: true # Add specified labels to issues/PRs when they become unstale labels-to-add-when-unstale: stale::recovered # Remove specified labels to issues/PRs when they become unstale labels-to-remove-when-unstale: stale,stale::closed # Max number of operations per run operations-per-run: ${{ secrets.STALE_OPERATIONS_PER_RUN || 100 }} # Dry-run debug-only: ${{ github.event.inputs.dryrun || false }} # Order to get issues/PRs ascending: true # Delete branch after closing a stale PR delete-branch: false # Issues with these labels will never be considered stale exempt-issue-labels: stale::recovered,epic # Issues with these labels will never be considered stale exempt-pr-labels: stale::recovered,epic # Exempt all issues/PRs with milestones from stale exempt-all-milestones: true # Assignees on issues/PRs exempted from stale exempt-assignees: mingwandroid - name: Print outputs run: echo ${{ join(steps.stale.outputs.*, ',') }} conda-package-handling-2.3.0/.github/workflows/tests.yml000066400000000000000000000025651463012743000232450ustar00rootroot00000000000000name: CI on: push: branches: - main pull_request: branches: - main jobs: test: name: Test on ${{ matrix.os }}, Python ${{ matrix.python-version }} runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, windows-latest] python-version: [3.8, 3.9, '3.10', 3.11, 3.12] steps: - uses: actions/checkout@v3 - name: Additional info about the build shell: bash run: | uname -a df -h ulimit -a # More info on options: https://github.com/conda-incubator/setup-miniconda - uses: conda-incubator/setup-miniconda@v2 with: python-version: ${{ matrix.python-version }} environment-file: devtools/conda-envs/test_env.yaml channels: defaults activate-environment: test_env auto-update-conda: false auto-activate-base: false show-channel-urls: true - name: Install package # conda setup requires this special shell shell: bash -l {0} run: | conda build -c conda conda.recipe # TODO: Re-enable codecov when we figure out how to grab coverage from the conda build # environment # - name: CodeCov # uses: codecov/codecov-action@v1 # with: # file: ./coverage.xml # flags: unittests # name: codecov-${{ matrix.os }}-py${{ matrix.python-version }} conda-package-handling-2.3.0/.gitignore000066400000000000000000000024421463012743000177450ustar00rootroot00000000000000# cythonized module src/conda_package_handling/archive_utils_cy.c # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ junit.xml rever/ # ignore .vscode .vscode conda-package-handling-2.3.0/.mailmap000066400000000000000000000067321463012743000174040ustar00rootroot00000000000000# This file was autogenerated by rever: https://regro.github.io/rever-docs/ # This prevent git from showing duplicates with various logging commands. # See the git documentation for more details. The syntax is: # # good-name bad-name # # You can skip bad-name if it is the same as good-name and is unique in the repo. # # This file is up-to-date if the command git log --format="%aN <%aE>" | sort -u # gives no duplicates. Alan Du Cheng H. Lee Cheng H. Lee Chris Burr Christopher Barber Conda Bot <18747875+conda-bot@users.noreply.github.com> conda-bot Conda Bot <18747875+conda-bot@users.noreply.github.com> conda-bot <18747875+conda-bot@users.noreply.github.com> Conda Bot <18747875+conda-bot@users.noreply.github.com> conda-bot Conda Bot <18747875+conda-bot@users.noreply.github.com> conda-bot Conda Bot <18747875+conda-bot@users.noreply.github.com> Distro Bot Conda Bot <18747875+conda-bot@users.noreply.github.com> Distro Bot <18747875+conda-bot@users.noreply.github.com> Conda Bot <18747875+conda-bot@users.noreply.github.com> Distro Bot Conda Bot <18747875+conda-bot@users.noreply.github.com> Distro Bot Conda Bot <18747875+conda-bot@users.noreply.github.com> distro-bot@anaconda.com Conda Bot <18747875+conda-bot@users.noreply.github.com> distro-bot@anaconda.com <18747875+conda-bot@users.noreply.github.com> Conda Bot <18747875+conda-bot@users.noreply.github.com> distro-bot@anaconda.com Conda Bot <18747875+conda-bot@users.noreply.github.com> distro-bot@anaconda.com Conda Bot <18747875+conda-bot@users.noreply.github.com> conda bot Conda Bot <18747875+conda-bot@users.noreply.github.com> conda bot <18747875+conda-bot@users.noreply.github.com> Conda Bot <18747875+conda-bot@users.noreply.github.com> conda bot Conda Bot <18747875+conda-bot@users.noreply.github.com> conda bot Daniel Bast <2790401+dbast@users.noreply.github.com> Daniel Holth Eli Uriegas Jannis Leidel John Lee leej3 Jonathan J. Helmus Justin Wood (Callek) Ken Odegard Marius van Niekerk Matthew R. Becker Michael Sarahan Mike Sarahan Michael Sarahan Mike Sarahan Michael Sarahan Mike Sarahan Michael Sarahan Michael Sarahan Michael Sarahan Michael Sarahan Michael Sarahan Michael Sarahan Nehal J Wani Pure Software ossdev07 Ray Donnelly Tobias "Tobi" Koch Vadim Zayakin <77290357+vz-x@users.noreply.github.com> vz-x <77290357+vz-x@users.noreply.github.com> jaimergp pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> conda-package-handling-2.3.0/.pre-commit-config.yaml000066400000000000000000000016541463012743000222420ustar00rootroot00000000000000# disable autofixing PRs, commenting "pre-commit.ci autofix" on a pull request triggers a autofix ci: autofix_prs: false repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.6.0 hooks: # standard end of line/end of file cleanup - id: mixed-line-ending - id: end-of-file-fixer - id: trailing-whitespace # ensure syntaxes are valid - id: check-toml - id: check-yaml exclude: ^(conda\.)?recipe/meta.yaml # catch git merge/rebase problems - id: check-merge-conflict - repo: https://github.com/asottile/pyupgrade rev: v3.15.2 hooks: - id: pyupgrade args: ["--py38-plus"] - repo: https://github.com/PyCQA/isort rev: 5.13.2 hooks: - id: isort - repo: https://github.com/psf/black rev: 24.4.2 hooks: - id: black - repo: https://github.com/PyCQA/flake8 rev: 7.0.0 hooks: - id: flake8 conda-package-handling-2.3.0/AUTHORS.md000066400000000000000000000007561463012743000174320ustar00rootroot00000000000000All of the people who have made at least one contribution to conda-package-handling. Authors are sorted alphabetically. * Alan Du * Cheng H. Lee * Chris Burr * Christopher Barber * Conda Bot * Daniel Bast * Daniel Holth * Eli Uriegas * Jannis Leidel * John Lee * Jonathan J. Helmus * Justin Wood (Callek) * Ken Odegard * Marius van Niekerk * Matthew R. Becker * Michael Sarahan * Nehal J Wani * Pure Software * Ray Donnelly * Tobias "Tobi" Koch * Vadim Zayakin * jaimergp * pre-commit-ci[bot] conda-package-handling-2.3.0/CHANGELOG.md000066400000000000000000000236121463012743000175700ustar00rootroot00000000000000[//]: # (current developments) ## 2.3.0 (2024-06-05) ### Enhancements * Add `cph list` to report artifact contents without prior extraction. (#236) * Added formal support for Python 3.10, 3.11, and 3.12. (#231) ### Bug fixes * Delay ``os.getcwd()`` call to body of ``CondaFormat_v2.create()`` when ``out_folder`` is not passed. (#205) ### Deprecations * Removed formal support for Python 3.7. (#231) ### Other * Remove MANIFEST.in, used for Python sdists, which referenced non-existent files. Source distributions appear correct without MANIFEST.in. (#163) * Add explicit `zstandard` dependency. ([#222](https://github.com/conda/conda-package-handling/issues/222)) ### Contributors * @conda-bot * @dholth * @callek made their first contribution in https://github.com/conda/conda-package-handling/pull/231 * @jaimergp made their first contribution in https://github.com/conda/conda-package-handling/pull/235 * @pre-commit-ci[bot] ## 2.2.0 (2023-07-28) ### Bug fixes * Respect umask when unpacking packages, by requiring `conda-package-streaming >= 0.9.0`. ### Docs * Include README.md in pypi metadata. (#215) ### Contributors * @conda-bot * @dbast * @dholth * @pre-commit-ci[bot] ## 2.1.0 (2023-05-04) ### Bug fixes * Include decompressed size when creating `.conda` archives with `CondaFormat_v2.create()`, to reduce memory usage on decompression. (#171) Transmuted archives (converted from `.tar.bz2`) do not contain the decompressed size. * Include LICENSE, not just LICENSE.txt in info/ section (#172) ### Contributors * @conda-bot * @dbast * @dholth * @pre-commit-ci[bot] ## 2.0.2 (2022-12-01) ### Bug fixes * Reduce memory usage when creating `.conda`. Allocate only one zstd comperssor when creating `.conda`. Lower default compression level to 19 from 22. (#168) ### Contributors * @dholth ## 2.0.1 (2022-11-18) ### Bug fixes * Require conda-package-streaming 0.7.0 for Windows c:\ vs C:\ check, pypy support ### Contributors * @dholth ## 2.0.0 (2022-11-17) ### Enhancements * Remove progress bars. * Based on conda-package-streaming instead of libarchive. * Requires the `python-zstandard` (`zstandard`) library. * Threadsafe `extract()` function. * More efficient `.conda` handling. ### Deprecations * Remove broken `verify` subcommand. * Remove support for `binsort` (was supposed to help with `tar.bz2` compression). (Use `.conda` instead.) ### Docs * Add sphinx documentation. ### Other * Reformat entire codebase with `black`, `isort`. (#132) ### Contributors * @conda-bot * @dholth * @jezdez * @kenodegard * @mariusvniekerk ## 1.9.0 (2022-09-06) ### Enhancements * Support setting the zstd compression level on the cli. (#114) ### Bug fixes * Include tested fix for "``info/`` sorts first in ``.tar.bz2``" feature, useful for streaming ``.tar.bz2``. (#102) * Fix extracting ``.conda`` given as relative path. (#116) * Gracefully handle missing subcommands. (#105) ### Contributors * @conda-bot * @jezdez * @dholth * @kenodegard made their first contribution in #112 * @mariusvniekerk made their first contribution in #114 ## 1.8.1 (2022-04-01) ### Bug fixes * Don't drop empty directories that happen to be prefixes of something else (#99) ### Contributors * @tobijk * @conda-bot * @chenghlee ## 1.8.0 (2022-03-12) ### Enhancements * Compute package hashes in threads. (#83) ### Bug fixes * Fix running from a read-only working directory (#44) * Fix symlinks to directories being incorrectly placed in the ``info`` tarball when transmuting ``.tar.bz2``- to ``.conda``-format packages (#84) * No longer generate emtpy metadata.json in v2 packages (#88) * Fix for TypeError in tarball.py. (#86) ### Deprecations * Remove Python 2 support. ### Other * Added project board, issue staleness, thread locking and label automation using GitHub action workflows to improve maintenance of GitHub project. More information can be found in the infra repo: https://github.com/conda/infra * Removed unused continuous integration platform config files. ### Contributors * @dholth * @conda-bot * @chenghlee * @analog-cbarber * @chrisburr * @vz-x * @jezdez ## 1.7.3 (2021-04-12) ### Enhancements * Python tar extraction now used as a fallback if libarchive fails ### Bug fixes * Fix binsort's mangling of symlinks * Fix #71, larger directories fail to extract using libarchive * When testing that exceptions are raised or archives containing abs paths, first check that such a "broken" archive was created during test setup... otherwise skip the test. * api.create now raises an error correctly if archive creation failed or extension is not supported. * Travis CI issue now resolved, mock added as dependency for conda test environments and system dependencies * Fixed bug where extract parser cli failed due to not having ``out_folder`` attribute. ### Contributors * @mingwandroid * @leej3 * @beckermr * @seemethere ## 1.7.2 (2020-10-16) ### Enhancements * add --force to transmute ### Bug fixes * Do not report symlinks as missing files * Fixes for --process and --out-folder #68 * --out-folder: Normalise, expand user-ify and ensure it ends with os.sep ### Contributors * @mingwandroid * @nehaljwani ## 1.6.0 (2019-09-20) ### Enhancements * add a "prefix" keyword argument to the api.extract function. When combined with dest_dir, the prefix is the base directory, and the dest_dir is the folder name. dest_dir alone as an abspath is both the base directory and the folder name. ### Bug fixes * provide a non-ProcessPoolExecutor path when number of processes is 1 * open files to be added to archives in binary mode. On Windows, the implicit default was text mode, which was corrupting newline data and putting in null characters. * extraction prefix defaults to the folder containing the specified archive. This is a behavior change from 1.3.x, which extracted into the CWD by default. ### Contributors * @msarahan * @jjhelmus ## 1.5.0 (2019-08-31) ### Contributors * @msarahan * @jjhelmus ## 1.4.1 (2019-08-04) ### Enhancements * several small error fixes from bad copypasta ### Contributors * @msarahan ## 1.4.0 (2019-08-02) ### Bug fixes * provide fallback to built-in tarfile if libarchive fails to import. Won't support new .conda format (obviously) * tmpdir created in output folder (defaults to cwd, but not always cwd) ### Contributors * @msarahan ## 1.3.11 (2019-07-11) ### Bug fixes * fix BadZipFile exception handling on py27 ### Contributors * @msarahan ## 1.3.10 (2019-06-24) ### Contributors * @msarahan ## 1.3.9 (2019-06-14) ### Bug fixes * put temporary files in CWD/.cph_tmp(random) instead of default temp dir. Hope that this fixes the permission problems seen on appveyor and azure. ### Contributors * @msarahan ## 1.3.8 (2019-06-13) ### Bug fixes * Write output files to output path directly, rather than any temporary. Hope that this fixes permission errors on appveyor/azure ### Contributors * @msarahan ## 1.3.7 (2019-06-12) ### Bug fixes * Don't print message for every skipped file that already exists. Don't even look at files that match the target conversion pattern. ### Contributors * @msarahan ## 1.3.6 (2019-06-12) ### Contributors ## 1.3.5 (2019-06-12) ### Bug fixes * fix recursion issue with TemporaryDirectory ### Contributors * @msarahan ## 1.3.4 (2019-06-11) ### Bug fixes * fix setup.cfg path issue with versioneer * try copying temporary artifact to final location instead of moving it, in hopes of avoiding permission errors ### Contributors * @msarahan ## 1.3.3 (2019-06-11) ### Bug fixes * add .gitattributes file to fix versioneer not working ### Contributors * @msarahan ## 1.3.2 (2019-06-11) ### Bug fixes * port rm_rf functionality from conda, to better handle permissions errors being observed on Azure and Appveyor windows hosts (but not on local machines) ### Contributors * @msarahan ## 1.3.1 (2019-06-11) ### Bug fixes * try to wrap tempdir cleanup so that it never exits violently. Add warning message. ### Contributors * @msarahan ## 1.3.0 (2019-06-10) ### Enhancements * add a cph-specific exception, so that downstream consumers of cph don't have to handle libarchive exceptions ### Contributors * @msarahan ## 1.2.0 (2019-06-08) ### Enhancements * add get_default_extracted_folder api function that returns the folder location where a file would be extracted to by default (no dest folder specified) * add --processes flag to cph t, to limit number of processes spawned. Defaults to number of CPUs if not set. ### Contributors * @msarahan ## 1.1.5 (2019-05-21) ### Bug fixes * generate symlink tests rather than including file layout, to avoid issues on win ### Contributors * @msarahan ## 1.1.4 (2019-05-21) ### Enhancements * moved conda_package_handling into src (src layout) ### Contributors * @msarahan ## 1.1.3 (2019-05-20) ### Bug fixes * improve tests of symlink and other file contents ### Contributors * @msarahan ## 1.1.2 (2019-05-20) ### Bug fixes * fix creation dropping symlinks and things that are not otherwise "files" ### Contributors * @msarahan ## 1.1.1 (2019-05-14) ### Bug fixes * fix path join bug, where an absolute path for out_fn was causing file writing problems ### Contributors * @msarahan ## 1.1.0 (2019-05-10) ### Bug fixes * simplify .conda package info, to work with conda/conda#8639 and conda/conda-build#3500 * add missing six dep * fix reference in cli.py to incorrect API function (how was this working?) * Wrap calls to shutil.move in try, because of windows permission errors observed on Appveyor ### Contributors * @msarahan * @nehaljwani ## 1.0.4 (2019-02-13) ### Enhancements * new api-only function, ``get_pkg_details`` that returns package size and checksum info in dictionary form * add version info output to the CLI ### Contributors * @msarahan ## 1.0.3 (2019-02-04) ### Bug fixes * fix support for python 2.7 ### Contributors * @msarahan ## 1.0.2 (2019-02-04) ### Contributors * @msarahan ## 1.0.1 (2019-02-04) ### Contributors conda-package-handling-2.3.0/CODE_OF_CONDUCT.md000066400000000000000000000024321463012743000205530ustar00rootroot00000000000000# Conda Organization Code of Conduct > [!NOTE] > Below is the short version of our CoC, see the long version [here](https://github.com/conda-incubator/governance/blob/main/CODE_OF_CONDUCT.md). # The Short Version Be kind to others. Do not insult or put down others. Behave professionally. Remember that harassment and sexist, racist, or exclusionary jokes are not appropriate for the conda Organization. All communication should be appropriate for a professional audience including people of many different backgrounds. Sexual language and imagery is not appropriate. The conda Organization is dedicated to providing a harassment-free community for everyone, regardless of gender, sexual orientation, gender identity and expression, disability, physical appearance, body size, race, or religion. We do not tolerate harassment of community members in any form. Thank you for helping make this a welcoming, friendly community for all. ## Report an Incident * Report a code of conduct incident [using a form](https://form.jotform.com/221527028480048). * Report a code of conduct incident via email: [conduct@conda.org](mailto:conduct@conda.org). * Contact [an individual committee member](#committee-membership) or [CoC event representative](#coc-representatives) to report an incident in confidence. conda-package-handling-2.3.0/HOW_WE_USE_GITHUB.md000066400000000000000000000546161463012743000211170ustar00rootroot00000000000000 [conda-org]: https://github.com/conda [sub-team]: https://github.com/conda-incubator/governance#sub-teams [project-planning]: https://github.com/orgs/conda/projects/2/views/11 [project-sorting]: https://github.com/orgs/conda/projects/2/views/11 [project-support]: https://github.com/orgs/conda/projects/2/views/12 [project-backlog]: https://github.com/orgs/conda/projects/2/views/13 [project-in-progress]: https://github.com/orgs/conda/projects/2/views/14 [docs-toc]: https://github.blog/changelog/2021-04-13-table-of-contents-support-in-markdown-files/ [docs-actions]: https://docs.github.com/en/actions [docs-saved-reply]: https://docs.github.com/en/get-started/writing-on-github/working-with-saved-replies/creating-a-saved-reply [docs-commit-signing]: https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits [infrastructure]: https://github.com/conda/infrastructure [workflow-sync]: https://github.com/conda/infrastructure/blob/main/.github/workflows/sync.yml [labels-global]: https://github.com/conda/infrastructure/blob/main/.github/global.yml [workflow-cla]: /.github/workflows/cla.yml [workflow-issues]: /.github/workflows/issues.yml [workflow-labels]: /.github/workflows/labels.yml [workflow-lock]: /.github/workflows/lock.yml [workflow-project]: /.github/workflows/project.yml [workflow-stale]: /.github/workflows/stale.yml [labels-local]: /.github/labels.yml [labels-page]: ../../labels # How We Use GitHub This document seeks to outline how we as a community use GitHub Issues to track bugs and feature requests while still catering to development practices & project management (_e.g._, release cycles, feature planning, priority sorting, etc.). **Topics:** - [What is "Issue Sorting"?](#what-is-issue-sorting) - [Issue Sorting Procedures](#issue-sorting-procedures) - [Commit Signing](#commit-signing) - [Types of Issues](#types-of-issues) - [Standard Issue](#standard-issue) - [Epics](#epics) - [Spikes](#spikes) - [Working on Issues](#working-on-issues) > [!NOTE] > This document is written in the style of an FAQ. For easier navigation, use [GitHub's table of contents feature][docs-toc]. ## What is "Issue Sorting"? > [!NOTE] > "Issue sorting" is similar to that of "triaging", but we've chosen to use different terminology because "triaging" is a word related to very weighty topics (_e.g._, injuries and war) and we would like to be sensitive to those connotations. Additionally, we are taking a more "fuzzy" approach to sorting (_e.g._, severities may not be assigned, etc.). "Issue Sorting" refers to the process of assessing the priority of incoming issues. Below is a high-level diagram of the flow of issues: ```mermaid flowchart LR subgraph flow_sorting [Issue Sorting] board_sorting{{Sorting}} board_support{{Support}} board_sorting<-->board_support end subgraph flow_refinement [Refinement] board_backlog{{Backlog}} board_backlog-- refine -->board_backlog end subgraph flow_progress [In Progress] board_progress{{In Progress}} end state_new(New Issues) state_closed(Closed) state_new-->board_sorting board_sorting-- investigated -->board_backlog board_sorting-- duplicates, off-topic -->state_closed board_support-- resolved, unresponsive -->state_closed board_backlog-- pending work -->board_progress board_backlog-- resolved, irrelevant -->state_closed board_progress-- resolved -->state_closed ``` ### Why sort issues? At the most basic "bird's eye view" level, sorted issues will fall into the category of four main priority levels: - Do now - Do sometime - Provide user support - Never do (_i.e._, close) At its core, sorting enables new issues to be placed into these four categories, which helps to ensure that they will be processed at a velocity similar to or exceeding the rate at which new issues are coming in. One of the benefits of actively sorting issues is to avoid engineer burnout and to make necessary work sustainable; this is done by eliminating a never-ending backlog that has not been reviewed by any maintainers. There will always be broad-scope design and architecture implementations that the maintainers will be interested in pursuing; by actively organizing issues, the sorting engineers will be able to more easily track and tackle both specific and big-picture goals. ### Who does the sorting? Sorting engineers are a conda governance [sub-team][sub-team]; they are a group of community members who are responsible for making decisions regarding closing issues and setting feature work priorities, among other sorting-related tasks. ### How do items show up for sorting? New issues that are opened in any of the repositories in the [conda GitHub organization][conda-org] will show up in the "Sorting" tab of the [Planning project][project-planning]. There are two [GitHub Actions][docs-actions] workflows utilized for this purpose; [`.github/workflows/issues.yml`][workflow-issues] and [`.github/workflows/project.yml`][workflow-project]. The GitHub Actions in the [`conda/infrastructure`][infrastructure] repository are viewed as canonical; the [`.github/workflows/sync.yml` workflow][workflow-sync] sends out any modifications to other `conda` repositories from there. ### What is done about the issues in the "Sorting" tab? Issues in the ["Sorting" tab of the project board][project-sorting] are considered ready for the following procedures: - Mitigation via short-term workarounds and fixes - Redirection to the correct project - Determining if support can be provided for errors and questions - Closing out of any duplicate/off-topic issues The sorting engineers on rotation are not seeking to _resolve_ issues that arise. Instead, the goal is to understand the issue and to determine whether it is legitimate, and then to collect as much relevant information as possible so that the maintainers can make an informed decision about the appropriate resolution schedule. Issues will remain in the ["Sorting" tab][project-sorting] as long as the issue is in an investigatory phase (_e.g._, querying the user for more details, asking the user to attempt other workarounds, other debugging efforts, etc.) and are likely to remain in this state the longest, but should still be progressing over the course of 1-2 weeks. For more information on the sorting process, see [Issue Sorting Procedures](#issue-sorting-procedures). ### When do items move out of the "Sorting" tab? Items move out of the ["Sorting" tab][project-sorting] once the investigatory phase described in [What is done about the issues in the "Sorting" tab?](#what-is-done-about-the-issues-in-the-sorting-tab) has concluded and the sorting engineer has enough information to make a decision about the appropriate resolution schedule for the issue. The additional tabs in the project board that the issues can be moved to include the following: - **"Support"** - Any issue in the ["Support" tab of the Planning board][project-support] is a request for support and is not a feature request or a bug report. Add the [`type::support`](https://github.com/conda/infrastructure/labels/type%3A%3Asupport) label to move an issue to this tab. - **"Backlog"** - The issue has revealed a bug or feature request. We have collected enough details to understand the problem/request and to reproduce it on our own. These issues have been moved into the [Backlog tab of the Planning board][project-backlog] at the end of the sorting rotation during Refinement. Add the [`backlog`](https://github.com/conda/infrastructure/labels/backlog) label to move an issue to this tab. - **"Closed"** - The issue was closed due to being a duplicate, being redirected to a different project, was a user error, a question that has been resolved, etc. ### Where do work issues go after being sorted? Once issues are deemed ready to be worked on, they will be moved to the ["Backlog" tab of the Planning board][project-backlog]. Once actively in progress, the issues will be moved to the ["In Progress" tab of the Planning board][project-in-progress] and then closed out once the work is complete. ### What is the purpose of having a "Backlog"? Issues are "backlogged" when they have been sorted but not yet earmarked for an upcoming release. ### What automation procedures are currently in place? Global automation procedures synced out from the [`conda/infrastructure`][infrastructure] repo include: - [Marking of issues and pull requests as stale][workflow-stale], resulting in: - issues marked as [`type::support`](https://github.com/conda/infrastructure/labels/type%3A%3Asupport) being labeled stale after 21 days of inactivity and being closed after 7 further days of inactivity (that is, closed after 30 inactive days total) - all other inactive issues (not labeled as [`type::support`](https://github.com/conda/infrastructure/labels/type%3A%3Asupport) being labeled stale after 365 days of inactivity and being closed after 30 further days of inactivity (that is, closed after an approximate total of 1 year and 1 month of inactivity) - all inactive pull requests being labeled stale after 365 days of inactivity and being closed after 30 further days of inactivity (that is, closed after an approximate total of 1 year and 1 month of inactivity) - [Locking of closed issues and pull requests with no further activity][workflow-lock] after 365 days - [Adding new issues and pull requests to the respective project boards][workflow-project] - [Indicating an issue is ready for the sorting engineer's attention][workflow-issues] by toggling [`pending::feedback`](https://github.com/conda/infrastructure/labels/pending%3A%3Afeedback) with [`pending::support`](https://github.com/conda/infrastructure/labels/pending%3A%3Asupport) after a contributor leaves a comment - [Verifying that contributors have signed the CLA][workflow-cla] before allowing pull requests to be merged; if the contributor hasn't signed the CLA previously, merging is be blocked until a manual review can be done - [Syncing out templates, labels, workflows, and documentation][workflow-sync] from [`conda/infrastructure`][infrastructure] to the other repositories ## Issue Sorting Procedures ### How are issues sorted? Issues in the ["Sorting" tab of the Planning board][project-sorting] are reviewed by issue sorting engineers, who take rotational sorting shifts. In the process of sorting issues, engineers label the issues and move them to the other tabs of the project board for further action. Issues that require input from multiple members of the sorting team will be brought up during refinement meetings in order to understand how those particular issues fit into the short- and long-term roadmap. These meetings enable the sorting engineers to get together to collectively prioritize issues, earmark feature requests for specific future releases (versus a more open-ended backlog), tag issues as ideal for first-time contributors, as well as whether or not to close/reject specific feature requests. ### How does labeling work? Labeling is a very important means for sorting engineers to keep track of the current state of an issue with regards to the asynchronous nature of communicating with users. Utilizing the proper labels helps to identify the severity of the issue as well as to quickly understand the current state of a discussion. Each label has an associated description that clarifies how the label should be used. Hover on the label to see its description. Label colors are used to distinguish labels by category. Generally speaking, labels with the same category are considered mutually exclusive, but in some cases labels sharing the same category can occur concurrently, as they indicate qualifiers as opposed to types. For example, we may have the following types, [`type::bug`](https://github.com/conda/infrastructure/labels/type%3A%3Abug), [`type::feature`](https://github.com/conda/infrastructure/labels/type%3A%3Afeature), and [`type::documentation`](https://github.com/conda/infrastructure/labels/type%3A%3Adocumentation), where for any one issue there would be _at most_ **one** of these to be defined (_i.e._ an issue should not be a bug _and_ a feature request at the same time). Alternatively, with issues involving specific operating systems (_i.e._, [`os::linux`](https://github.com/conda/infrastructure/labels/os%3A%3Alinux), [`os::macos`](https://github.com/conda/infrastructure/labels/os%3A%3Amacos), and [`os::windows`](https://github.com/conda/infrastructure/labels/os%3A%3Awindows)), an issue could be labeled with one or more, depending on the system(s) the issue occurs on. Please note that there are also automation policies in place that are affected by labeling. For example, if an issue is labeled as [`type::support`](https://github.com/conda/infrastructure/labels/type%3A%3Asupport), that issue will be marked [`stale`](https://github.com/conda/infrastructure/labels/stale) after 21 days of inactivity and auto-closed after seven more days without activity (30 inactive days total), which is earlier than issues without this label. See [What automation procedures are currently in place?](#what-automation-procedures-are-currently-in-place) for more details. ### What labels are required for each issue? At minimum, both `type` and `source` labels should be specified on each issue before moving it from the "Sorting" tab to the "Backlog" tab. All issues that are bugs should also be tagged with a `severity` label. The `type` labels are exclusive of each other: each sorted issue should have exactly one `type` label. These labels give high-level information on the issue's classification (_e.g._, bug, feature, tech debt, etc.) The `source` labels are exclusive of each other: each sorted issue should have exactly one `source` label. These labels give information on the sub-group to which the issue's author belongs (_e.g._, a partner, a frequent contributor, the wider community, etc.). Through these labels, maintainers gain insight into how well we're meeting the needs of various groups. The `severity` labels are exclusive of each other and, while required for the [`type::bug`](https://github.com/conda/infrastructure/labels/type%3A%bug) label, they can also be applied to other types to indicate demand or need. These labels help us to prioritize our work. Severity is not the only factor for work prioritization, but it is an important consideration. Please review the descriptions of the `type`, `source`, and `severity` labels on the [labels page][labels-page] prior to use. ### How are new labels defined? Labels are defined using a scoped syntax with an optional high-level category (_e.g._, `source`, `tag`, `type`, etc.) and a specific topic, much like the following: - `[topic]` - `[category::topic]` - `[category::topic-phrase]` This syntax helps with issue sorting enforcement, as it helps to ensure that sorted issues are, at minimum, categorized by type and source. There are a number of labels that have been defined for the different repositories. In order to create a streamlined sorting process, label terminologies are standardized using similar (if not the same) labels. ### How are new labels added? New **global** labels (_i.e._, labels that apply equally to all repositories within the conda GitHub organization) are added to [`conda/infrastructure`][infrastructure]'s [`.github/global.yml` file][labels-global]; new **local** labels (_i.e._, labels specific to particular repositories) are added to each repository's [`.github/labels.yml` file][labels-local]. All new labels should follow the labeling syntax described in ["How are new labels defined?"](#how-are-new-labels-defined). Global labels are combined with any local labels and these aggregated labels are used by the [`.github/workflows/labels.yml` workflow][workflow-labels] to synchronize the labels available for the repository. ### Are there any templates to use as responses for commonly-seen issues? Some of the same types of issues appear regularly (_e.g._, issues that are duplicates of others, issues that should be filed in the Anaconda issue tracker, errors that are due to a user's specific setup/environment, etc.). Below are some boilerplate responses for the most commonly-seen issues to be sorted:
Duplicate Issue

This is a duplicate of [link to primary issue]; please feel free to continue the discussion there.
> [!WARNING] > Apply the https://github.com/conda/infrastructure/labels/duplicate label to the issue being closed and https://github.com/conda/infrastructure/labels/duplicate%3A%3Aprimary to the original issue.
Requesting an Uninstall/Reinstall of conda

Please uninstall your current version of `conda` and reinstall the latest version.
Feel free to use either the [miniconda](https://docs.anaconda.com/free/miniconda/)
or [anaconda](https://www.anaconda.com/products/individual) installer,
whichever is more appropriate for your needs.
Redirect to Anaconda Issue Tracker

Thank you for filing this issue! Unfortunately, this is off-topic for this repo.
If you are still encountering this issue please reopen in the
[Anaconda issue tracker](https://github.com/ContinuumIO/anaconda-issues/issues)
where `conda` installer/package issues are addressed.
> [!WARNING] > Apply the https://github.com/conda/infrastructure/labels/off-topic label to these issues before closing them out.
Redirecting to Nucleus Forums

Unfortunately, this issue is outside the scope of support we offer via GitHub;
if you continue to experience the problems described here,
please post details to the [Nucleus forums](https://community.anaconda.cloud/).
> [!WARNING] > Apply the https://github.com/conda/infrastructure/labels/off-topic label to these issues before closing them out.
Slow solving of conda environment
Hi [@username],

Thanks for voicing your concern about the performance of the classic dependency solver. To fix this, our official recommendation is using the new default "conda-libmamba-solver" instead of the classic solver (more information about the "conda-libmamba-solver" can be found here: https://conda.github.io/conda-libmamba-solver/getting-started/).

In most cases "conda-libmamba-solver" should be significantly faster than the "classic" solver. We hope it provides you with a much better experience going forward.
In order to not have to manually type or copy/paste the above repeatedly, note that it's possible to add text for the most commonly-used responses via [GitHub's "Add Saved Reply" option][docs-saved-reply]. ## Commit Signing For all conda maintainers, we require commit signing and strongly recommend it for all others wishing to contribute to conda related projects. More information about how to set this up within GitHub can be found here: - [GitHub's signing commits docs][docs-commit-signing] ## Types of Issues ### Standard Issue TODO ### Epics TODO ### Spikes #### What is a spike? "Spike" is a term that is borrowed from extreme programming and agile development. They are used when the **outcome of an issue is unknown or even optional**. For example, when first coming across a problem that has not been solved before, a project may choose to either research the problem or create a prototype in order to better understand it. Additionally, spikes represent work that **may or may not actually be completed or implemented**. An example of this are prototypes created to explore possible solutions. Not all prototypes are implemented and the purpose of creating a prototype is often to explore the problem space more. For research-oriented tasks, the end result of this research may be that a feature request simply is not viable at the moment and would result in putting a stop to that work. Finally, spikes are usually **timeboxed**. However, given the open source/volunteer nature of our contributions, we do not enforce this for our contributors. When a timebox is set, this means that we are limiting how long we want someone to work on said spike. We do this to prevent contributors from falling into a rabbit hole they may never return from. Instead, we set a time limit to perform work on the spike and then have the assignee report back. If the tasks defined in the spike have not yet been completed, a decision is made on whether it makes sense to perform further work on the spike. #### When do I create a spike? A spike should be created when we do not have enough information to move forward with solving a problem. That simply means that, whenever we are dealing with unknowns or processes the project team has never encountered before, it may be useful for us to create a spike. In day-to-day work, this kind of situation may appear when new bug reports or feature requests come in that deal with problems or technologies that the project team is unfamiliar with. All issues that the project team has sufficient knowledge of should instead proceed as regular issues. #### When do I not create a spike? Below are some common scenarios where creating a spike is not appropriate: - Writing a technical specification for a feature we know how to implement - Design work that would go into drafting how an API is going to look and function - Any work that must be completed or is not optional ## Working on Issues ### How do I assign myself to an issue I am actively reviewing? If you do **not** have permissions, please indicate that you are working on an issue by leaving a comment. Someone who has permissions will assign you to the issue. If two weeks have passed without a pull request or an additional comment requesting information, you may be removed from the issue and the issue reassigned. If you are assigned to an issue but will not be able to continue work on it, please comment to indicate that you will no longer be working on it and press `unassign me` next to your username in the `Assignees` section of the issue page (top right). If you **do** have permissions, please assign yourself to the issue by pressing `assign myself` under the `Assignees` section of the issue page (top right). conda-package-handling-2.3.0/LICENSE000066400000000000000000000027411463012743000167640ustar00rootroot00000000000000BSD 3-Clause License Copyright (c) 2019, Conda All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. conda-package-handling-2.3.0/Makefile000066400000000000000000000012461463012743000174160ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = docs BUILDDIR = build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) style: isort --profile=black . black . conda-package-handling-2.3.0/README.md000066400000000000000000000013771463012743000172420ustar00rootroot00000000000000# conda-package-handling [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/conda/conda-package-handling/main.svg)](https://results.pre-commit.ci/latest/github/conda/conda-package-handling/main) Create and extract conda packages of various formats. `conda` and `conda-build` use `conda_package_handling.api` to create and extract conda packages. This package also provides the `cph` command line tool to extract, create, and convert between formats. See also [conda-package-streaming](https://conda.github.io/conda-package-streaming), an efficient library to read from new and old format .conda and .tar.bz2 conda packages. Full documentation at [https://conda.github.io/conda-package-handling/](https://conda.github.io/conda-package-handling/) conda-package-handling-2.3.0/RELEASE.md000066400000000000000000000466651463012743000173760ustar00rootroot00000000000000 [epic template]: https://github.com/conda/conda/issues/new?assignees=&labels=epic&template=epic.yml [compare]: https://github.com/conda/infrastructure/compare [new release]: https://github.com/conda/infrastructure/releases/new [infrastructure]: https://github.com/conda/infrastructure [rever docs]: https://regro.github.io/rever-docs [release docs]: https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes [merge conflicts]: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/addressing-merge-conflicts/about-merge-conflicts [Anaconda Recipes]: https://github.com/AnacondaRecipes/conda-feedstock [conda-forge]: https://github.com/conda-forge/conda-feedstock # Release Process > [!NOTE] > Throughout this document are references to the version number as `YY.M.[$patch_number]`, this should be replaced with the correct version number. Do **not** prefix the version with a lowercase `v`. ## 1. Open the release issue and cut a release branch. (do this ~1 week prior to release) > [!NOTE] > The new release branch should adhere to the naming convention of `YY.M.x` (make sure to put the `.x` at the end!). In the case of patch/hotfix releases, however, do NOT cut a new release branch; instead, use the previously-cut release branch with the appropriate `YY.M.x` version numbers. Use the issue template below to create the release issue. After creating the release issue, pin it for easy access.

Release Template

```markdown ### Summary Placeholder for `{{ repo.name }} YY.M.x` release. | Pilot | | |---|---| | Co-pilot | | ### Tasks [milestone]: {{ repo.url }}/milestone/ [process]: {{ repo.url }}/blob/main/RELEASE.md [releases]: {{ repo.url }}/releases [main]: https://github.com/AnacondaRecipes/{{ repo.name }}-feedstock [conda-forge]: https://github.com/conda-forge/{{ repo.name }}-feedstock [ReadTheDocs]: https://readthedocs.com/projects/continuumio-{{ repo.name }}/

The week before release week

- [ ] Create release branch (named `YY.M.x`) - [ ] Ensure release candidates are being successfully built (see `conda-canary/label/rc-{{ repo.name }}-YY.M.x`) - [ ] [Complete outstanding PRs][milestone] - [ ] Test release candidates

Release week

- [ ] Create release PR (see [release process][process]) - [ ] [Publish release][releases] - [ ] Merge `YY.M.x` back into `main` - [ ] Activate the `YY.M.x` branch on [ReadTheDocs][ReadTheDocs] - [ ] Feedstocks - [ ] Bump version & update dependencies/tests in [Anaconda, Inc.'s feedstock][main] - [ ] Bump version & update dependencies/tests in [conda-forge feedstock][conda-forge] - [ ] Hand off to the Anaconda packaging team - [ ] Announce release - Blog Post (optional) - [ ] conda.org (link to pull request) - Long form - [ ] Create release [announcement draft](https://github.com/conda/communications) - [ ] [Discourse](https://conda.discourse.group/) - [ ] [Matrix (conda/conda)](https://matrix.to/#/#conda_conda:gitter.im) (this auto posts from Discourse) - Summary - [ ] [Twitter](https://twitter.com/condaproject)
```
If a patch release is necessary, reopen the original release issue and append the following template to the release issue summary.

Patch Release Template

```markdown

Patch YY.M.N

- [ ] - [ ] Create release PR (see [release process][process]) - [ ] [Publish release][releases] - [ ] Merge `YY.M.x` back into `main` - [ ] Feedstocks - [ ] Bump version & update dependencies/tests in [Anaconda, Inc.'s feedstock][main] - [ ] Bump version & update dependencies/tests in [conda-forge feedstock][conda-forge] - [ ] Hand off to the Anaconda packaging team
```
> [!NOTE] > The [epic template][epic template] is perfect for this; remember to remove the **`epic`** label. ## 2. Alert various parties of the upcoming release. (do this ~1 week prior to release) Let various interested parties know about the upcoming release; at minimum, conda-forge maintainers should be informed. For major features, a blog post describing the new features should be prepared and posted once the release is completed (see the announcements section of the release issue). ## 3. Manually test canary build(s). ### Canary Builds for Manual Testing Once the release PRs are filed, successful canary builds will be available on `https://anaconda.org/conda-canary/conda/files?channel=rc-{{ repo.name }}-YY.M.x` for manual testing. > [!NOTE] > You do not need to apply the `build::review` label for release PRs; every commit to the release branch builds and uploads canary builds to the respective `rc-` label. ## 4. Ensure `rever.xsh` and `news/TEMPLATE` are up to date. These are synced from [`conda/infrastructure`][infrastructure].

5. Run rever. (ideally done on the Monday of release week)

Currently, there are only 2 activities we use rever for, (1) aggregating the authors and (2) updating the changelog. Aggregating the authors can be an error-prone process and also suffers from builtin race conditions (_i.e._, to generate an updated `.authors.yml` we need an updated `.mailmap` but to have an updated `.mailmap` we need an updated `.authors.yml`). This is why the following steps are very heavy-handed (and potentially repetitive) in running rever commands, undoing commits, squashing/reordering commits, etc. 1. Install [`rever`][rever docs] and activate the environment: ```bash $ conda create -n rever conda-forge::rever $ conda activate rever (rever) $ ``` 2. Clone and `cd` into the repository if you haven't done so already: ```bash (rever) $ git clone git@github.com:{{ repo.user }}/{{ repo.name }}.git (rever) $ cd conda ``` 2. Fetch the latest changes from the remote and checkout the release branch created a week ago: ```bash (rever) $ git fetch upstream (rever) $ git checkout YY.M.x ``` 2. Create a versioned branch, this is where rever will make its changes: ```bash (rever) $ git checkout -b changelog-YY.M.[$patch_number] ``` 2. Run `rever --activities authors `: > **Note:** > Include `--force` when re-running any rever commands for the same ``, otherwise, rever will skip the activity and no changes will be made (i.e., rever remembers if an activity has been run for a given version). ```bash (rever) $ rever --activities authors --force ``` - If rever finds that any of the authors are not correctly represented in `.authors.yml` it will produce an error. If the author that the error pertains to is: - **a new contributor**: the snippet suggested by rever should be added to the `.authors.yml` file. - **an existing contributor**, a result of using a new name/email combo: find the existing author in `.authors.yml` and add the new name/email combo to that author's `aliases` and `alterative_emails`. - Once you have successfully run `rever --activities authors` with no errors, review the commit made by rever. This commit will contain updates to one or more of the author files (`.authors.yml`, `.mailmap`, and `AUTHORS.md`). Due to the race condition between `.authors.yml` and `.mailmap`, we want to extract changes made to any of the following keys in `.authors.yml` and commit them separately from the other changes in the rever commit: - `name` - `email` - `github` - `aliases` - `alternate_emails` Other keys (e.g., `num_commits` and `first_commit`) do not need to be included in this separate commit as they will be overwritten by rever. - Here's a sample run where we undo the commit made by rever in order to commit the changes to `.authors.yml` separately: ```bash (rever) $ rever --activities authors --force YY.M.[$patch_number] # changes were made to .authors.yml as per the prior bullet (rever) $ git diff --name-only HEAD HEAD~1 .authors.yml .mailmap AUTHORS.md # undo commit (rever) $ git reset --soft HEAD~1 # undo changes made to everything except .authors.yml (rever) $ git restore --staged --worktree .mailmap AUTHORS.md ``` - Commit these changes to `.authors.yml`: ```bash (rever) $ git add . (rever) $ git commit -m "Update .authors.yml" ``` - Rerun `rever --activities authors --force ` and finally check that your `.mailmap` is correct by running: ```bash git shortlog -se ``` Compare this list with `AUTHORS.md`. If they have any discrepancies, additional modifications to `.authors.yml` is needed, so repeat the above steps as needed. - Once you are pleased with how the author's file looks, we want to undo the rever commit and commit the `.mailmap` changes separately: ```bash # undo commit (but preserve changes) (rever) $ git reset --soft HEAD~1 # undo changes made to everything except .mailmap (rever) $ git restore --staged --worktree .authors.yml AUTHORS.md ``` - Commit these changes to `.mailmap`: ```bash (rever) $ git add . (rever) $ git commit -m "Update .mailmap" ``` - Continue repeating the above processes until the `.authors.yml` and `.mailmap` are corrected to your liking. After completing this, you will have at most two commits on your release branch: ```bash (rever) $ git cherry -v + 86957814cf235879498ed7806029b8ff5f400034 Update .authors.yml + 3ec7491f2f58494a62f1491987d66f499f8113ad Update .mailmap ``` 4. Review news snippets (ensure they are all using the correct Markdown format, **not** reStructuredText) and add additional snippets for undocumented PRs/changes as necessary. > **Note:** > We've found it useful to name news snippets with the following format: `-`. > > We've also found that we like to include the PR #s inline with the text itself, e.g.: > > ```markdown > ## Enhancements > > * Add `win-arm64` as a known platform (subdir). (#11778) > ``` - You can utilize [GitHub's compare view][compare] to review what changes are to be included in this release. Make sure you compare the current release branch against the previous one (e.g., `24.5.x` would be compared against `24.3.x`) - Add a new news snippet for any PRs of importance that are missing. - Commit these changes to news snippets: ```bash (rever) $ git add . (rever) $ git commit -m "Update news" ``` - After completing this, you will have at most three commits on your release branch: ```bash (rever) $ git cherry -v + 86957814cf235879498ed7806029b8ff5f400034 Update .authors.yml + 3ec7491f2f58494a62f1491987d66f499f8113ad Update .mailmap + 432a9e1b41a3dec8f95a7556632f9a93fdf029fd Update news ``` 5. Run `rever --activities changelog`: > **Note:** > This has previously been a notoriously fickle step (likely due to incorrect regex patterns in the `rever.xsh` config file and missing `github` keys in `.authors.yml`) so beware of potential hiccups. If this fails, it's highly likely to be an innocent issue. ```bash (rever) $ rever --activities changelog --force ``` - Any necessary modifications to `.authors.yml`, `.mailmap`, or the news snippets themselves should be amended to the previous commits. - Once you have successfully run `rever --activities changelog` with no errors simply revert the last commit (see the next step for why): ```bash # undo commit (and discard changes) (rever) $ git reset --hard HEAD~1 ``` - After completing this, you will have at most three commits on your release branch: ```bash (rever) $ git cherry -v + 86957814cf235879498ed7806029b8ff5f400034 Update .authors.yml + 3ec7491f2f58494a62f1491987d66f499f8113ad Update .mailmap + 432a9e1b41a3dec8f95a7556632f9a93fdf029fd Update news ``` 6. Now that we have successfully run the activities separately, we wish to run both together. This will ensure that the contributor list, a side-effect of the authors activity, is included in the changelog activity. ```bash (rever) $ rever --force ``` - After completing this, you will have at most five commits on your release branch: ```bash (rever) $ git cherry -v + 86957814cf235879498ed7806029b8ff5f400034 Update .authors.yml + 3ec7491f2f58494a62f1491987d66f499f8113ad Update .mailmap + 432a9e1b41a3dec8f95a7556632f9a93fdf029fd Update news + a5c0db938893d2c12cab12a1f7eb3e646ed80373 Update authorship for YY.M.[$patch_number] + 5e95169d0df4bcdc2da9a6ba4a2561d90e49f75d Update CHANGELOG for YY.M.[$patch_number] ``` 7. Since rever does not include stats on first-time contributors, we will need to add this manually. - Use [GitHub's auto-generated release notes][new release] to get a list of all new contributors (and their first PR) and manually merge this list with the contributor list in `CHANGELOG.md`. See [GitHub docs][release docs] for how to auto-generate the release notes. - Commit these final changes: ```bash (rever) $ git add . (rever) $ git commit -m "Add first-time contributions" ``` - After completing this, you will have at most six commits on your release branch: ```bash (rever) $ git cherry -v + 86957814cf235879498ed7806029b8ff5f400034 Update .authors.yml + 3ec7491f2f58494a62f1491987d66f499f8113ad Update .mailmap + 432a9e1b41a3dec8f95a7556632f9a93fdf029fd Update news + a5c0db938893d2c12cab12a1f7eb3e646ed80373 Update authorship for YY.M.[$patch_number] + 5e95169d0df4bcdc2da9a6ba4a2561d90e49f75d Update CHANGELOG for YY.M.[$patch_number] + 93fdf029fd4cf235872c12cab12a1f7e8f95a755 Add first-time contributions ``` 8. Push this versioned branch. ```bash (rever) $ git push -u upstream changelog-YY.M.[$patch_number] ``` 9. Open the Release PR targing the `YY.M.x` branch.
GitHub PR Template ```markdown ## Description ✂️ snip snip ✂️ the making of a new release. Xref # ```
10. Update release issue to include a link to the release PR. 11. [Create][new release] the release and **SAVE AS A DRAFT** with the following values: > **Note:** > Only publish the release after the release PR is merged, until then always **save as draft**. | Field | Value | |---|---| | Choose a tag | `YY.M.[$patch_number]` | | Target | `YY.M.x` | | Body | copy/paste blurb from `CHANGELOG.md` |
## 6. Wait for review and approval of release PR. ## 7. Merge release PR and publish release. To publish the release, go to the project's release page (e.g., https://github.com/conda/conda/releases) and add the release notes from `CHANGELOG.md` to the draft release you created earlier. Then publish the release. > [!NOTE] > Release notes can be drafted and saved ahead of time. ## 8. Merge/cherry pick the release branch over to the `main` branch.
Internal process 1. From the main "< > Code" page of the repository, select the drop down menu next to the `main` branch button and then select "View all branches" at the very bottom. 2. Find the applicable `YY.M.x` branch and click the "New pull request" button. 3. "Base" should point to `main` while "Compare" should point to `YY.M.x`. 4. Ensure that all of the commits being pulled in look accurate, then select "Create pull request". > [!NOTE] > Make sure NOT to push the "Update Branch" button. If there are [merge conflicts][merge conflicts], create a temporary "connector branch" dedicated to fixing merge conflicts separately from the `YY.M.x` and `main` branches. 5. Review and merge the pull request the same as any code change pull request. > [!NOTE] > The commits from the release branch need to be retained in order to be able to compare individual commits; in other words, a "merge commit" is required when merging the resulting pull request vs. a "squash merge". Protected branches will require permissions to be temporarily relaxed in order to enable this action.
## 9. Open PRs to bump [Anaconda Recipes][Anaconda Recipes] and [conda-forge][conda-forge] feedstocks to use `YY.M.[$patch_number]`. > [!NOTE] > Conda-forge's PRs will be auto-created via the `regro-cf-autotick-bot`. Follow the instructions below if any changes need to be made to the recipe that were not automatically added (these instructions are only necessary for anyone who is _not_ a conda-forge feedstock maintainer, since maintainers can push changes directly to the autotick branch): > - Create a new branch based off of autotick's branch (autotick's branches usually use the `regro-cf-autotick-bot:XX.YY.[$patch_number]_[short hash]` syntax) > - Add any changes via commits to that new branch > - Open a new PR and push it against the `main` branch > > Make sure to include a comment on the original `autotick-bot` PR that a new pull request has been created, in order to avoid duplicating work! `regro-cf-autotick-bot` will close the auto-created PR once the new PR is merged. > > For more information about this process, please read the ["Pushing to regro-cf-autotick-bot branch" section of the conda-forge documentation](https://conda-forge.org/docs/maintainer/updating_pkgs.html#pushing-to-regro-cf-autotick-bot-branch). ## 10. Hand off to Anaconda's packaging team. > [!NOTE] > This step should NOT be done past Thursday morning EST; please start the process on a Monday, Tuesday, or Wednesday instead in order to avoid any potential debugging sessions over evenings or weekends.
Internal process 1. Open packaging request in #package_requests Slack channel, include links to the Release PR and feedstock PRs. 2. Message packaging team/PM to let them know that a release has occurred and that you are the release manager.
## 11. Continue championing and shepherding. Remember to make all relevant announcements and continue to update the release issue with the latest details as tasks are completed. conda-package-handling-2.3.0/conda.recipe/000077500000000000000000000000001463012743000203055ustar00rootroot00000000000000conda-package-handling-2.3.0/conda.recipe/meta.yaml000066400000000000000000000027161463012743000221250ustar00rootroot00000000000000{% set name = "conda-package-handling" %} {% set version_match = load_file_regex( load_file="src/conda_package_handling/__init__.py", regex_pattern='^__version__ = "(.+)"') %} {% set version = version_match[1] %} package: name: {{ name }} version: {{ version }} source: - path: .. build: number: 0 script: {{ PYTHON }} -m pip install . --no-deps -vv entry_points: - cph = conda_package_handling.cli:main # by skipping nooarch: python, tests run under build python requirements: host: - python - pip - wheel run: - python - zstandard >=0.15 - conda-package-streaming >=0.9.0 test: source_files: - tests requires: - mock - pytest - pytest-cov - pytest-mock imports: - conda_package_handling - conda_package_handling.api commands: - pytest -v --cov=conda_package_handling --color=yes tests/ about: home: https://github.com/conda/conda-package-handling dev_url: https://github.com/conda/conda-package-handling doc_url: https://conda.github.io/conda-package-handling/ license: BSD-3-Clause license_family: BSD license_file: LICENSE summary: Create and extract conda packages of various formats. description: | `conda` and `conda-build` use `conda_package_handling.api` to create and extract conda packages. This package also provides the `cph` command line tool to extract, create, and convert between formats. extra: recipe-maintainers: - dholth - jezdez conda-package-handling-2.3.0/devtools/000077500000000000000000000000001463012743000176125ustar00rootroot00000000000000conda-package-handling-2.3.0/devtools/conda-envs/000077500000000000000000000000001463012743000216475ustar00rootroot00000000000000conda-package-handling-2.3.0/devtools/conda-envs/test_env.yaml000066400000000000000000000001011463012743000243520ustar00rootroot00000000000000name: test_env dependencies: - anaconda-client - conda-build conda-package-handling-2.3.0/docs/000077500000000000000000000000001463012743000167035ustar00rootroot00000000000000conda-package-handling-2.3.0/docs/api.md000066400000000000000000000002121463012743000177710ustar00rootroot00000000000000api module ========== ```{eval-rst} .. automodule:: conda_package_handling.api :members: :undoc-members: :show-inheritance: ``` conda-package-handling-2.3.0/docs/cli.md000066400000000000000000000002051463012743000177710ustar00rootroot00000000000000cph utility =========== ```{eval-rst} .. argparse:: :module: conda_package_handling.cli :func: build_parser :prog: cph ``` conda-package-handling-2.3.0/docs/conf.py000066400000000000000000000035451463012743000202110ustar00rootroot00000000000000# Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # import os import sys sys.path.insert(0, os.path.abspath("..")) # -- Project information ----------------------------------------------------- project = "conda-package-handling" copyright = "Anaconda, Inc." author = "Anaconda, Inc." # -- General configuration --------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ "sphinx.ext.autodoc", "sphinxarg.ext", "myst_parser", ] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = [] # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = "furo" # 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"] conda-package-handling-2.3.0/docs/index.md000066400000000000000000000075651463012743000203510ustar00rootroot00000000000000# Welcome to conda-package-handling's documentation! `conda-package-handling` is a library and command line utility used to handle `.conda` and `.tar.bz2` [conda packages](https://docs.conda.io/projects/conda/en/latest/user-guide/concepts/packages.html). `conda` and `conda-build` use `conda_package_handling.api` to create and extract conda packages. This package also provides the `cph` command line tool to extract, create, and convert between formats. See also [conda-package-streaming](https://github.com/conda-incubator/conda-package-streaming) ## A new major version As of version 2.x, `conda-package-handling` provides a backwards-compatible wrapper around [`conda-package-streaming`](https://conda.github.io/conda-package-streaming/), plus additional package creation functionality not found in `conda-package-streaming`. `conda-package-handling` always expects to read and write to the filesystem. If you need a simpler API to extract or inspect conda packages, check out [`conda-package-streaming`](https://conda.github.io/conda-package-streaming/). Version 2.x is approximately two times faster extracting `.conda` packages, by extracting `.conda`'s embedded `.tar.zst` without first writing it to a temporary file. It uses [`python-zstandard`](https://github.com/indygreg/python-zstandard) and the Python standard library instead of a custom `libarchive` and so is easier to build. Extraction does not `chdir` to the output directory, and is thread-safe. Version 2.x creates `.conda` packages slightly differently as well. * `.conda`'s `info-` archive comes after the `pkg-` archive. * Inside `.conda`, the `info-` and `pkg-`'s ZIP metadata use a fixed timestamp, instead of the current time - can be seen with `python -m zipfile -l [filename].conda`. * `.conda`'s embedded `.tar.zst` strip `uid`/`gid`/`username`/`groupname` instead of preserving these from the filesystem. * Both `.conda` and `.tar.bz2` are created by Python's standard `zipfile` and `tarfile` instead of `libarchive`. No particular attention has been paid to archiving time which will be dominated by the compression algorithm, but this also avoids using a temporary `.tar.zst`. ## Overview There are two conda formats. The new conda format, described at [.conda file format](https://docs.conda.io/projects/conda/en/latest/user-guide/concepts/packages.html?highlight=format#conda-file-format), consists of an outer, uncompressed ZIP-format container, with 2 inner compressed .tar files. It is designed to have much faster metadata access and utilize more modern compression algorithms. The old conda format is a `.tar.bz2` archive. The cph command line tool can transmute (convert) the old package format to the new one, and vice versa. ``` cph transmute mkl-2018.0.3-1.tar.bz2 .conda ``` The new package format is an indexed, uncompressed zip file that contains two Zstandard-compressed tarfiles. The info metadata about packages is separated into its own tarfile from the rest of the package contents. By doing this, we can extract only the metadata, for speeding up operations like indexing. And, the Zstandard algorithm is much, much faster to decompress than bz2. Package creation is primarily something that conda-build uses, as cph only packages but does not create metadata that makes a conda package useful. ``` cph create /path/to/some/dir my-cute-archive.conda ``` This would not necessarily create a valid conda package, unless the directory being archived contained all the metadata in an "info" directory that a standard conda package needs. The .conda file it creates, however, uses all the nice new compression formats, though, and you could use cph on some other computer to extract it. ## Development Install this package and its test dependencies; run tests. ``` pip install -e ".[test]" pytest ``` ## Contents ```{toctree} :maxdepth: 2 modules ``` # Indices and tables - {ref}`genindex` - {ref}`modindex` - {ref}`search` conda-package-handling-2.3.0/docs/modules.md000066400000000000000000000001031463012743000206670ustar00rootroot00000000000000# conda_package_handling ```{toctree} :maxdepth: 4 cli api ``` conda-package-handling-2.3.0/docs/requirements.txt000066400000000000000000000002121463012743000221620ustar00rootroot00000000000000# used in sphinx documentation build conda-package-streaming>=0.9.0 # docs furo sphinx sphinx-argparse myst-parser mdit-py-plugins>=0.3.0 conda-package-handling-2.3.0/make.bat000066400000000000000000000013771463012743000173700ustar00rootroot00000000000000@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=docs set BUILDDIR=build %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.https://www.sphinx-doc.org/ exit /b 1 ) if "%1" == "" goto help %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end popd conda-package-handling-2.3.0/news/000077500000000000000000000000001463012743000167275ustar00rootroot00000000000000conda-package-handling-2.3.0/news/TEMPLATE000066400000000000000000000002221463012743000200610ustar00rootroot00000000000000### Enhancements * ### Bug fixes * ### Deprecations * ### Docs * ### Other * conda-package-handling-2.3.0/pyproject.toml000066400000000000000000000007251463012743000206730ustar00rootroot00000000000000[tool.black] # consider reverting to standard line length line-length = 99 [tool.isort] profile = "black" line_length = 99 [tool.pytest] norecursedirs = [".*", "*.egg*", "build", "dist", "conda.recipe"] addopts = [ "--junitxml=junit.xml", "--ignore setup.py", "--ignore run_test.py", "--cov-report term-missing", "--tb native", "--strict-markers", "--durations=20", ] markers = ["serial: execute test serially (to avoid race conditions)"] conda-package-handling-2.3.0/rever.xsh000066400000000000000000000015161463012743000176250ustar00rootroot00000000000000$ACTIVITIES = ["authors", "changelog"] # Basic settings $PROJECT = $GITHUB_REPO = $(basename $(git remote get-url origin)).split('.')[0].strip() $GITHUB_ORG = "conda" # Authors settings $AUTHORS_FILENAME = "AUTHORS.md" $AUTHORS_SORTBY = "alpha" # Changelog settings $CHANGELOG_FILENAME = "CHANGELOG.md" $CHANGELOG_PATTERN = r"\[//\]: # \(current developments\)" $CHANGELOG_HEADER = """[//]: # (current developments) ## $VERSION ($RELEASE_DATE) """ $CHANGELOG_CATEGORIES = [ "Enhancements", "Bug fixes", "Deprecations", "Docs", "Other", ] $CHANGELOG_CATEGORY_TITLE_FORMAT = "### {category}\n\n" $CHANGELOG_AUTHORS_TITLE = "Contributors" $CHANGELOG_AUTHORS_FORMAT = "* @{github}\n" try: # allow repository to customize synchronized-from-infa rever config from rever_overrides import * except ImportError: pass conda-package-handling-2.3.0/rever_overrides.xsh000066400000000000000000000004221463012743000217020ustar00rootroot00000000000000# override synced-from-infra rever.xsh $ACTIVITIES = ["version_bump", "authors", "changelog"] $VERSION_BUMP_PATTERNS = [ # These note where/how to find the version numbers ('src/conda_package_handling/__init__.py', r'__version__\s*=.*', '__version__ = "$VERSION"'), ] conda-package-handling-2.3.0/setup.cfg000066400000000000000000000002761463012743000176010ustar00rootroot00000000000000[flake8] max-line-length = 100 ignore = E122,E123,E126,E127,E128,E731,E722 exclude = build,src/conda_package_handling/_version.py,tests,conda.recipe,.git,versioneer.py,benchmarks,.asv,rever conda-package-handling-2.3.0/setup.py000066400000000000000000000024171463012743000174710ustar00rootroot00000000000000import importlib.util import pathlib from setuptools import find_packages, setup spec = importlib.util.spec_from_file_location( "conda_package_handling", pathlib.Path("src/conda_package_handling/__init__.py") ) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) version = module.__version__ setup( name="conda-package-handling", version=version, description="Create and extract conda packages of various formats.", long_description=open("README.md").read(), long_description_content_type="text/markdown", author="Anaconda, Inc.", author_email="conda@anaconda.com", url="https://github.com/conda/conda-package-handling", packages=find_packages("src", exclude=["tests"]), package_dir={"": "src"}, entry_points={"console_scripts": ["cph=conda_package_handling.cli:main"]}, keywords="conda-package-handling", classifiers=["Programming Language :: Python :: 3"], python_requires=">=3.8", install_requires=["conda-package-streaming >= 0.9.0"], extras_require={ "docs": [ "furo", "sphinx", "sphinx-argparse", "myst-parser", "mdit-py-plugins>=0.3.0", ], "test": ["mock", "pytest", "pytest-cov", "pytest-mock"], }, ) conda-package-handling-2.3.0/src/000077500000000000000000000000001463012743000165425ustar00rootroot00000000000000conda-package-handling-2.3.0/src/conda_package_handling/000077500000000000000000000000001463012743000231455ustar00rootroot00000000000000conda-package-handling-2.3.0/src/conda_package_handling/__init__.py000066400000000000000000000000261463012743000252540ustar00rootroot00000000000000__version__ = "2.3.0" conda-package-handling-2.3.0/src/conda_package_handling/__main__.py000066400000000000000000000000741463012743000252400ustar00rootroot00000000000000from conda_package_handling import cli cli.main(args=None) conda-package-handling-2.3.0/src/conda_package_handling/api.py000066400000000000000000000174571463012743000243060ustar00rootroot00000000000000from __future__ import annotations import functools as _functools import os as _os import warnings as _warnings from glob import glob as _glob # expose these two exceptions as part of the API. Everything else should feed into these. from .exceptions import ConversionError, InvalidArchiveError # NOQA from .interface import AbstractBaseFormat from .tarball import CondaTarBZ2 as _CondaTarBZ2 from .utils import filter_info_files from .utils import get_executor as _get_executor from .utils import rm_rf as _rm_rf SUPPORTED_EXTENSIONS: dict[str, type[AbstractBaseFormat]] = {".tar.bz2": _CondaTarBZ2} libarchive_enabled = False #: Old API meaning "can extract .conda" (now without libarchive) try: from .conda_fmt import ZSTD_COMPRESS_LEVEL, ZSTD_COMPRESS_THREADS from .conda_fmt import CondaFormat_v2 as _CondaFormat_v2 SUPPORTED_EXTENSIONS[".conda"] = _CondaFormat_v2 libarchive_enabled = True except ImportError: _warnings.warn("Install zstandard Python bindings for .conda support") THREADSAFE_EXTRACT = True #: Not present in conda-package-handling<2.0. def _collect_paths(prefix): dir_paths, file_paths = [], [] for dp, dn, filenames in _os.walk(prefix): for f in filenames: file_paths.append(_os.path.relpath(_os.path.join(dp, f), prefix)) dir_paths.extend(_os.path.relpath(_os.path.join(dp, _), prefix) for _ in dn) file_list = file_paths + [ dp for dp in dir_paths if not any(f.startswith(dp + _os.sep) for f in file_paths) ] return file_list def get_default_extracted_folder(in_file, abspath=True): dirname = None for ext in SUPPORTED_EXTENSIONS: if in_file.endswith(ext): dirname = in_file[: -len(ext)] if dirname and not abspath: dirname = _os.path.basename(dirname) return dirname def extract(fn, dest_dir=None, components=None, prefix=None): if dest_dir: if _os.path.isabs(dest_dir) and prefix: raise ValueError( "dest_dir and prefix both provided as abs paths. If providing both, " "prefix can be abspath, but dest dir must be relative (relative to " "prefix)" ) if not _os.path.isabs(dest_dir): dest_dir = _os.path.normpath(_os.path.join(prefix or _os.getcwd(), dest_dir)) else: dest_dir = _os.path.join( prefix or _os.path.dirname(fn), get_default_extracted_folder(fn, abspath=False), ) if not _os.path.isdir(dest_dir): _os.makedirs(dest_dir) for format in SUPPORTED_EXTENSIONS.values(): if format.supported(fn): format.extract(fn, dest_dir, components=components) break else: raise ValueError( "Didn't recognize extension for file '{}'. Supported extensions are: {}".format( fn, list(SUPPORTED_EXTENSIONS.keys()) ) ) def create(prefix, file_list, out_fn, out_folder=None, **kw): if not out_folder: out_folder = _os.getcwd() # simplify arguments to format.create() if _os.path.isabs(out_fn): out_folder = _os.path.dirname(out_fn) out_fn = _os.path.basename(out_fn) if file_list is None: file_list = _collect_paths(prefix) elif isinstance(file_list, str): try: with open(file_list) as f: data = f.readlines() file_list = [_.strip() for _ in data] except: raise out = None for format in SUPPORTED_EXTENSIONS.values(): if format.supported(out_fn): try: out = format.create(prefix, file_list, out_fn, out_folder, **kw) break except BaseException as err: # don't leave broken files around abs_out_fn = _os.path.join(out_folder, out_fn) if _os.path.isfile(abs_out_fn): _rm_rf(abs_out_fn) raise err else: raise ValueError( "Didn't recognize extension for file '{}'. Supported extensions are: {}".format( out_fn, list(SUPPORTED_EXTENSIONS.keys()) ) ) return out def _convert( fn, out_ext, out_folder, force=False, zstd_compress_level=None, zstd_compress_threads=None, **kw, ): # allow package to work in degraded mode when zstandard is not available import conda_package_streaming.transmute import zstandard basename = get_default_extracted_folder(fn, abspath=False) from .validate import validate_converted_files_match_streaming if not basename: return ( fn, "", "Input file %s doesn't have a supported extension (%s), skipping it" % (fn, SUPPORTED_EXTENSIONS), ) out_fn = str(_os.path.join(out_folder, basename + out_ext)) errors = "" if not _os.path.lexists(out_fn) or force: if force and _os.path.lexists(out_fn): _os.unlink(out_fn) if out_ext == ".conda": # ZSTD_COMPRESS_* constants are only defined if we have zstandard if zstd_compress_level is None: zstd_compress_level = ZSTD_COMPRESS_LEVEL if zstd_compress_threads is None: zstd_compress_threads = ZSTD_COMPRESS_THREADS def compressor(): return zstandard.ZstdCompressor( level=zstd_compress_level, threads=zstd_compress_threads ) def is_info(filename): return filter_info_files([filename], prefix=".") == [] transmute = _functools.partial( conda_package_streaming.transmute.transmute, fn, out_folder, compressor=compressor, is_info=is_info, ) else: transmute = _functools.partial( conda_package_streaming.transmute.transmute_tar_bz2, fn, out_folder ) try: transmute() result = validate_converted_files_match_streaming(out_fn, fn) _, missing_files, mismatching_sizes = result if missing_files or mismatching_sizes: errors = str(ConversionError(missing_files, mismatching_sizes)) except BaseException as e: # don't leave partial package around if _os.path.isfile(out_fn): _rm_rf(out_fn) if not isinstance(e, Exception): raise errors = str(e) return fn, out_fn, errors def transmute(in_file, out_ext, out_folder=None, processes=1, **kw): if not out_folder: out_folder = _os.path.dirname(in_file) or _os.getcwd() flist = set(_glob(in_file)) if in_file.endswith(".tar.bz2"): flist = flist - set(_glob(in_file.replace(".tar.bz2", out_ext))) elif in_file.endswith(".conda"): flist = flist - set(_glob(in_file.replace(".conda", out_ext))) failed_files = {} with _get_executor(processes) as executor: convert_f = _functools.partial(_convert, out_ext=out_ext, out_folder=out_folder, **kw) for fn, out_fn, errors in executor.map(convert_f, flist): if errors: failed_files[fn] = errors _rm_rf(out_fn) return failed_files def get_pkg_details(in_file): """For the new pkg format, we return the size and hashes of the inner pkg part of the file""" for format in SUPPORTED_EXTENSIONS.values(): if format.supported(in_file): return format.get_pkg_details(in_file) raise ValueError(f"Don't know what to do with file {in_file}") def list_contents(in_file, verbose=False): for format in SUPPORTED_EXTENSIONS.values(): if format.supported(in_file): return format.list_contents(in_file, verbose=verbose) raise ValueError(f"Don't know what to do with file {in_file}") conda-package-handling-2.3.0/src/conda_package_handling/cli.py000066400000000000000000000130031463012743000242630ustar00rootroot00000000000000import argparse import os import sys from pprint import pprint from . import __version__, api def parse_args(parse_this=None): parser = build_parser() return parser.parse_args(parse_this) def build_parser(): parser = argparse.ArgumentParser() parser.add_argument( "-V", "--version", action="version", help="Show the conda-package-handling version number and exit.", version=f"conda-package-handling {__version__}", ) sp = parser.add_subparsers(title="subcommands", dest="subcommand", required=True) extract_parser = sp.add_parser("extract", help="extract package contents", aliases=["x"]) extract_parser.add_argument("archive_path", help="path to archive to extract") extract_parser.add_argument( "--dest", help="destination folder to extract to. If not set, defaults to" " package filename minus extension in the same folder as the input archive." " May be relative path used in tandem with the --prefix flag.", ) extract_parser.add_argument( "--prefix", help="base directory to extract to. Use this to set the base" " directory, while allowing the folder name to be automatically determined " "by the input filename. An abspath --prefix with an unset --dest will " "achieve this.", ) extract_parser.add_argument( "--info", help="If the archive supports separate metadata, this" " flag extracts only the metadata in the info folder from the " "package. If the archive does not support separate metadata, this " "flag has no effect and all files are extracted.", action="store_true", ) create_parser = sp.add_parser("create", help="bundle files into a package", aliases=["c"]) create_parser.add_argument( "prefix", help="folder of files to bundle. Not strictly required to" " have conda package metadata, but if conda package metadata isn't " "present, you'll see a warning and your file will not work as a " "conda package", ) create_parser.add_argument( "out_fn", help="Filename of archive to be created. Extension determines package type.", ) create_parser.add_argument( "--file-list", help="Path to file containing one relative path per" " line that should be included in the archive. If not provided, " "lists all files in the prefix.", ) create_parser.add_argument("--out-folder", help="Folder to dump final archive to") convert_parser = sp.add_parser( "transmute", help="convert from one package type to another", aliases=["t"] ) convert_parser.add_argument( "in_file", help="existing file to convert from. Glob patterns accepted." ) convert_parser.add_argument( "out_ext", help="extension of file to convert to. Examples: .tar.bz2, .conda", ) convert_parser.add_argument("--out-folder", help="Folder to dump final archive to") convert_parser.add_argument( "--force", action="store_true", help="Force overwrite existing package" ) convert_parser.add_argument( "--processes", type=int, help="Max number of processes to use. If not set, defaults to 1.", ) convert_parser.add_argument( "--zstd-compression-level", help=( "When building v2 packages, set the compression level used by " "conda-package-handling. Defaults to 19." ), type=int, choices=range(1, 23), default=19, ) convert_parser.add_argument( "--zstd-compression-threads", help=( "When building v2 packages, set the compression threads used by " "conda-package-handling. Defaults to 1. -1=automatic." ), type=int, default=1, ) list_parser = sp.add_parser( "list", aliases=["l"], help="List package contents like `python -m tarfile --list ...` would do.", ) list_parser.add_argument("archive_path", help="path to archive to inspect") list_parser.add_argument( "-v", "--verbose", action="store_true", help="Report more details, similar to 'ls -l'. Otherwise, only the filenames are printed.", ) return parser def main(args=None): args = parse_args(args) if hasattr(args, "out_folder") and args.out_folder: args.out_folder = ( os.path.abspath(os.path.normpath(os.path.expanduser(args.out_folder))) + os.sep ) if args.subcommand in ("extract", "x"): if args.info: api.extract(args.archive_path, args.dest, components="info", prefix=args.prefix) else: api.extract(args.archive_path, args.dest, prefix=args.prefix) elif args.subcommand in ("create", "c"): api.create(args.prefix, args.file_list, args.out_fn, args.out_folder) elif args.subcommand in ("transmute", "t"): failed_files = api.transmute( args.in_file, args.out_ext, args.out_folder, args.processes or 1, force=args.force, zstd_compress_level=args.zstd_compression_level, zstd_compress_threads=args.zstd_compression_threads, ) if failed_files: print("failed files:") pprint(failed_files) sys.exit(1) elif args.subcommand in ("list", "l"): api.list_contents(args.archive_path, verbose=args.verbose) if __name__ == "__main__": # pragma: no cover main(args=None) conda-package-handling-2.3.0/src/conda_package_handling/conda_fmt.py000066400000000000000000000126331463012743000254560ustar00rootroot00000000000000""" The 'new' conda format, introduced in late 2018/early 2019. https://docs.conda.io/projects/conda/en/latest/user-guide/concepts/packages.html """ from __future__ import annotations import json import os import tarfile from typing import Callable from zipfile import ZIP_STORED, ZipFile import zstandard from . import utils from .interface import AbstractBaseFormat from .streaming import _extract, _list CONDA_PACKAGE_FORMAT_VERSION = 2 DEFAULT_COMPRESSION_TUPLE = (".tar.zst", "zstd", "zstd:compression-level=19") # increase to reduce speed and increase compression (22 = conda's default) ZSTD_COMPRESS_LEVEL = 19 # increase to reduce compression (slightly) and increase speed ZSTD_COMPRESS_THREADS = 1 class CondaFormat_v2(AbstractBaseFormat): """If there's another conda format or breaking changes, please create a new class and keep this one, so that handling of v2 stays working.""" @staticmethod def supported(fn): return fn.endswith(".conda") @staticmethod def extract(fn, dest_dir, **kw): components = utils.ensure_list(kw.get("components")) or ("info", "pkg") if not os.path.isabs(fn): fn = os.path.normpath(os.path.join(os.getcwd(), fn)) if not os.path.isdir(dest_dir): os.makedirs(dest_dir) _extract(str(fn), str(dest_dir), components=components) @staticmethod def extract_info(fn, dest_dir=None): return CondaFormat_v2.extract(fn, dest_dir, components=["info"]) @staticmethod def create( prefix, file_list, out_fn, out_folder=None, compressor: Callable[[], zstandard.ZstdCompressor] | None = None, compression_tuple=(None, None, None), ): if out_folder is None: out_folder = os.getcwd() if os.path.isabs(out_fn): out_folder = os.path.dirname(out_fn) out_fn = os.path.basename(out_fn) conda_pkg_fn = os.path.join(out_folder, out_fn) file_id = out_fn.replace(".conda", "") pkg_files = utils.filter_info_files(file_list, prefix) # preserve order pkg_files_set = set(pkg_files) info_files = list(f for f in file_list if f not in pkg_files_set) if compressor and (compression_tuple != (None, None, None)): raise ValueError("Supply one of compressor= or (deprecated) compression_tuple=") if compressor is None: compressor = lambda: zstandard.ZstdCompressor( level=ZSTD_COMPRESS_LEVEL, threads=ZSTD_COMPRESS_THREADS, ) # legacy libarchive-ish compatibility ext, comp_filter, filter_opts = compression_tuple if filter_opts and filter_opts.startswith("zstd:compression-level="): compressor = lambda: zstandard.ZstdCompressor( level=int(filter_opts.split("=", 1)[-1]), threads=ZSTD_COMPRESS_THREADS, ) class NullWriter: """ zstd uses less memory on extract if size is known. """ def __init__(self): self.size = 0 def write(self, bytes): self.size += len(bytes) return len(bytes) def tell(self): return self.size with ZipFile(conda_pkg_fn, "w", compression=ZIP_STORED) as conda_file, utils.tmp_chdir( prefix ): pkg_metadata = {"conda_pkg_format_version": CONDA_PACKAGE_FORMAT_VERSION} conda_file.writestr("metadata.json", json.dumps(pkg_metadata)) components_files = (f"pkg-{file_id}.tar.zst", pkg_files), ( f"info-{file_id}.tar.zst", info_files, ) # put the info last, for parity with updated transmute. compress = compressor() for component, files in components_files: # If size is known, the decompressor may be able to allocate less memory. # The compressor will error if size is not correct. with tarfile.TarFile(fileobj=NullWriter(), mode="w") as sizer: # type: ignore for file in files: sizer.add(file, filter=utils.anonymize_tarinfo) size = sizer.fileobj.size # type: ignore with conda_file.open(component, "w") as component_file: # only one stream_writer() per compressor() must be in use at a time component_stream = compress.stream_writer( component_file, size=size, closefd=False ) component_tar = tarfile.TarFile(fileobj=component_stream, mode="w") for file in files: component_tar.add(file, filter=utils.anonymize_tarinfo) component_tar.close() component_stream.close() return conda_pkg_fn @staticmethod def get_pkg_details(in_file): stat_result = os.stat(in_file) size = stat_result.st_size md5, sha256 = utils.checksums(in_file, ("md5", "sha256")) return {"size": size, "md5": md5, "sha256": sha256} @staticmethod def list_contents(fn, verbose=False, **kw): components = utils.ensure_list(kw.get("components")) or ("info", "pkg") if not os.path.isabs(fn): fn = os.path.abspath(fn) _list(fn, components=components, verbose=verbose) conda-package-handling-2.3.0/src/conda_package_handling/exceptions.py000066400000000000000000000035451463012743000257070ustar00rootroot00000000000000from errno import ENOENT class InvalidArchiveError(Exception): """Raised when libarchive can't open a file""" def __init__(self, fn, msg, *args, **kw): msg = ( "Error with archive %s. You probably need to delete and re-download " "or re-create this file. Message was:\n\n%s" % (fn, msg) ) self.errno = ENOENT super().__init__(msg) class ArchiveCreationError(Exception): """Raised when an archive fails during creation""" pass class CaseInsensitiveFileSystemError(InvalidArchiveError): def __init__(self, package_location, extract_location, **kwargs): message = """ Cannot extract package to a case-insensitive file system. Your install destination does not differentiate between upper and lowercase characters, and this breaks things. Try installing to a location that is case-sensitive. Windows drives are usually the culprit here - can you install to a native Unix drive, or turn on case sensitivity for this (Windows) location? package location: %(package_location)s extract location: %(extract_location)s """ self.package_location = package_location self.extract_location = extract_location super().__init__(package_location, message, **kwargs) class ConversionError(Exception): def __init__(self, missing_files, mismatching_sizes, *args, **kw): self.missing_files = missing_files self.mismatching_sizes = mismatching_sizes errors = "" if self.missing_files: errors = "Missing files in converted package: %s\n" % self.missing_files errors = ( errors + "Mismatching sizes (corruption) in converted package: %s" # noqa % self.mismatching_sizes ) super().__init__(errors, *args, **kw) conda-package-handling-2.3.0/src/conda_package_handling/interface.py000066400000000000000000000014341463012743000254610ustar00rootroot00000000000000from __future__ import annotations import abc class AbstractBaseFormat(metaclass=abc.ABCMeta): @staticmethod @abc.abstractmethod def supported(fn): # pragma: no cover return False @staticmethod @abc.abstractmethod def extract(fn, dest_dir, **kw): # pragma: no cover raise NotImplementedError @staticmethod @abc.abstractmethod def create(prefix, file_list, out_fn, out_folder: str | None = None, **kw): # pragma: no cover raise NotImplementedError @staticmethod @abc.abstractmethod def get_pkg_details(in_file): # pragma: no cover raise NotImplementedError @staticmethod @abc.abstractmethod def list_contents(in_file, verbose=False, **kw): # pragma: no cover raise NotImplementedError conda-package-handling-2.3.0/src/conda_package_handling/streaming.py000066400000000000000000000057451463012743000255230ustar00rootroot00000000000000""" Exception-compatible adapter from conda_package_streaming. """ from __future__ import annotations import io from contextlib import redirect_stdout from tarfile import TarError, TarFile, TarInfo from typing import Iterator from zipfile import BadZipFile from conda_package_streaming.extract import exceptions as cps_exceptions from conda_package_streaming.extract import extract_stream, package_streaming from . import exceptions def _stream_components( filename: str, components: list[str], dest_dir: str = "", ) -> Iterator[tuple[TarFile, TarInfo]]: if str(filename).endswith(".tar.bz2"): assert components == ["pkg"] try: with open(filename, "rb") as fileobj: for component in components: # will parse zipfile twice yield package_streaming.stream_conda_component( filename, fileobj, component=component ) except cps_exceptions.CaseInsensitiveFileSystemError as e: raise exceptions.CaseInsensitiveFileSystemError(filename, dest_dir) from e except (OSError, TarError, BadZipFile) as e: raise exceptions.InvalidArchiveError(filename, f"failed with error: {str(e)}") from e def _extract(filename: str, dest_dir: str, components: list[str]): """ Extract .conda or .tar.bz2 package to dest_dir. If it's a conda package, components may be ["pkg", "info"] If it's a .tar.bz2 package, components must equal ["pkg"] Internal. Skip directly to conda-package-streaming if you don't need exception compatibility. """ for stream in _stream_components(filename, components, dest_dir=dest_dir): try: extract_stream(stream, dest_dir) except cps_exceptions.CaseInsensitiveFileSystemError as e: raise exceptions.CaseInsensitiveFileSystemError(filename, dest_dir) from e except (OSError, TarError, BadZipFile) as e: raise exceptions.InvalidArchiveError(filename, f"failed with error: {str(e)}") from e def _list(filename: str, components: list[str], verbose=True): memfile = io.StringIO() for component in _stream_components(filename, components): for tar, _ in component: with redirect_stdout(memfile): tar.list(verbose=verbose) # next iteraton of for loop raises GeneratorExit in stream # see comments in conda_package_streaming.extract:extract_stream # and docstring in conda_package_streaming.package_streaming:stream_conda_info component.close() memfile.seek(0) if verbose: lines = sorted( memfile.readlines(), # verbose Tarfile.list() produces lines like: # ?rw-r--r-- 502/20 2342 2018-10-04 14:02:00 info/about.json # We only want the last part but we need to be mindful of paths containing spaces key=lambda line: line.split(None, 5)[-1], ) else: lines = sorted(memfile.readlines()) print("".join(lines), end="") conda-package-handling-2.3.0/src/conda_package_handling/tarball.py000066400000000000000000000061341463012743000251440ustar00rootroot00000000000000import logging import os import re import tarfile from . import streaming, utils from .interface import AbstractBaseFormat LOG = logging.getLogger(__name__) def _sort_file_order(prefix, files): """Sort by filesize, to optimize compression?""" info_slash = "info" + os.path.sep def order(f): # we don't care about empty files so send them back via 100000 fsize = os.lstat(os.path.join(prefix, f)).st_size or 100000 # info/* records will be False == 0, others will be 1. info_order = int(not f.startswith(info_slash)) if info_order: _, ext = os.path.splitext(f) # Strip any .dylib.* and .so.* and rename .dylib to .so ext = re.sub(r"(\.dylib|\.so).*$", r".so", ext) if not ext: # Files without extensions should be sorted by dirname info_order = 1 + hash(os.path.dirname(f)) % (10**8) else: info_order = 1 + abs(hash(ext)) % (10**8) return info_order, fsize files_list = list(sorted(files, key=order)) return files_list def _create_no_libarchive(fullpath, files): with tarfile.open(fullpath, "w:bz2") as t: for f in files: t.add(f, filter=utils.anonymize_tarinfo) def create_compressed_tarball( prefix, files, tmpdir, basename, ext, compression_filter, filter_opts="" ): tmp_path = os.path.join(tmpdir, basename) files = _sort_file_order(prefix, files) # add files in order of a) in info directory, b) increasing size so # we can access small manifest or json files without decompressing # possible large binary or data files fullpath = tmp_path + ext with utils.tmp_chdir(prefix): _create_no_libarchive(fullpath, files) return fullpath class CondaTarBZ2(AbstractBaseFormat): @staticmethod def supported(fn): return fn.endswith(".tar.bz2") @staticmethod def extract(fn, dest_dir, **kw): if not os.path.isdir(dest_dir): os.makedirs(dest_dir) if not os.path.isabs(fn): fn = os.path.normpath(os.path.join(os.getcwd(), fn)) streaming._extract(str(fn), str(dest_dir), components=["pkg"]) @staticmethod def create(prefix, file_list, out_fn, out_folder=None, **kw): if out_folder is None: out_folder = os.getcwd() if os.path.isabs(out_fn): out_folder = os.path.dirname(out_fn) out_file = create_compressed_tarball( prefix, file_list, out_folder, os.path.basename(out_fn).replace(".tar.bz2", ""), ".tar.bz2", "bzip2", ) return out_file @staticmethod def get_pkg_details(in_file): stat_result = os.stat(in_file) size = stat_result.st_size md5, sha256 = utils.checksums(in_file, ("md5", "sha256")) return {"size": size, "md5": md5, "sha256": sha256} @staticmethod def list_contents(fn, verbose=False, **kw): if not os.path.isabs(fn): fn = os.path.abspath(fn) streaming._list(str(fn), components=["pkg"], verbose=verbose) conda-package-handling-2.3.0/src/conda_package_handling/utils.py000066400000000000000000000405031463012743000246610ustar00rootroot00000000000000import contextlib import fnmatch import hashlib import logging import os import re import shutil import sys import warnings as _warnings from concurrent.futures import Executor, ProcessPoolExecutor, ThreadPoolExecutor from errno import EACCES, ENOENT, EPERM, EROFS from itertools import chain from os.path import ( abspath, basename, dirname, isdir, isfile, islink, join, lexists, normpath, split, ) from stat import S_IEXEC, S_IMODE, S_ISDIR, S_ISREG, S_IWRITE from subprocess import STDOUT, CalledProcessError, check_output, list2cmdline from tempfile import NamedTemporaryFile, mkdtemp on_win = sys.platform == "win32" log = logging.getLogger(__name__) CONDA_TEMP_EXTENSION = ".c~" def which(executable): from distutils.spawn import find_executable return find_executable(executable) def make_writable(path): try: mode = os.lstat(path).st_mode if S_ISDIR(mode): os.chmod(path, S_IMODE(mode) | S_IWRITE | S_IEXEC) elif S_ISREG(mode) or islink(path): os.chmod(path, S_IMODE(mode) | S_IWRITE) else: log.debug("path cannot be made writable: %s", path) return True except Exception as e: eno = getattr(e, "errno", None) if eno in (ENOENT,): log.debug("tried to make writable, but didn't exist: %s", path) raise elif eno in (EACCES, EPERM, EROFS): log.debug("tried make writable but failed: %s\n%r", path, e) return False else: log.warn("Error making path writable: %s\n%r", path, e) raise class DummyExecutor(Executor): def map(self, func, *iterables): for iterable in iterables: for thing in iterable: yield func(thing) def get_executor(processes): return DummyExecutor() if processes == 1 else ProcessPoolExecutor(max_workers=processes) def recursive_make_writable(path): # The need for this function was pointed out at # https://github.com/conda/conda/issues/3266#issuecomment-239241915 # Especially on windows, file removal will often fail because it is marked read-only if isdir(path): for root, dirs, files in os.walk(path): for path in chain.from_iterable((files, dirs)): try: make_writable(join(root, path)) except: pass else: try: make_writable(path) except: pass def quote_for_shell(arguments, shell=None): if not shell: shell = "cmd.exe" if on_win else "bash" if shell == "cmd.exe": return list2cmdline(arguments) else: # If any multiline argument gets mixed with any other argument (which is true if we've # arrived in this function) then we just quote it. This assumes something like: # ['python', '-c', 'a\nmultiline\nprogram\n'] # It may make sense to allow specifying a replacement character for '\n' too? e.g. ';' quoted = [] # This could all be replaced with some regex wizardry but that is less readable and # for code like this, readability is very important. for arg in arguments: if '"' in arg: quote = "'" elif "'" in arg: quote = '"' elif not any(_ in arg for _ in (" ", "\n")): quote = "" else: quote = '"' quoted.append(quote + arg + quote) return " ".join(quoted) def rmtree(path, *args, **kwargs): # subprocessing to delete large folders can be quite a bit faster path = normpath(path) if on_win: try: # the fastest way seems to be using DEL to recursively delete files # https://www.ghacks.net/2017/07/18/how-to-delete-large-folders-in-windows-super-fast/ # However, this is not entirely safe, as it can end up following symlinks to folders # https://superuser.com/a/306618/184799 # so, we stick with the slower, but hopefully safer way. Maybe if we figured out how # to scan for any possible symlinks, we could do the faster way. # out = check_output('DEL /F/Q/S *.* > NUL 2> NUL'.format(path), shell=True, # stderr=STDOUT, cwd=path) out = check_output(f'RD /S /Q "{path}" > NUL 2> NUL', shell=True, stderr=STDOUT) except: try: # Try to delete in Unicode name = None with NamedTemporaryFile(suffix=".bat", delete=False) as batch_file: batch_file.write(f"RD /S {quote_for_shell([path])}\n") batch_file.write("chcp 65001\n") batch_file.write(f"RD /S {quote_for_shell([path])}\n") batch_file.write("EXIT 0\n") name = batch_file.name # If the above is bugged we can end up deleting hard-drives, so we check # that 'path' appears in it. This is not bulletproof but it could save you (me). with open(name) as contents: content = contents.read() assert path in content comspec = os.environ["COMSPEC"] CREATE_NO_WINDOW = 0x08000000 # It is essential that we `pass stdout=None, stderr=None, stdin=None` here because # if we do not, then the standard console handles get attached and chcp affects the # parent process (and any which share those console handles!) out = check_output( [comspec, "/d", "/c", name], shell=False, stdout=None, stderr=None, stdin=None, creationflags=CREATE_NO_WINDOW, ) except CalledProcessError as e: if e.returncode != 5: log.error(f"Removing folder {path} the fast way failed. Output was: {out}") raise else: log.debug(f"removing dir contents the fast way failed. Output was: {out}") else: try: os.makedirs(".empty") except: pass # yes, this looks strange. See # https://unix.stackexchange.com/a/79656/34459 # https://web.archive.org/web/20130929001850/http://linuxnote.net/jianingy/en/linux/a-fast-way-to-remove-huge-number-of-files.html # NOQA rsync = which("rsync") if rsync and isdir(".empty"): try: out = check_output( [ rsync, "-a", "--force", "--delete", join(os.getcwd(), ".empty") + "/", path + "/", ], stderr=STDOUT, ) except CalledProcessError: log.debug(f"removing dir contents the fast way failed. Output was: {out}") shutil.rmtree(".empty") shutil.rmtree(path) def unlink_or_rename_to_trash(path): """If files are in use, especially on windows, we can't remove them. The fallback path is to rename them (but keep their folder the same), which maintains the file handle validity. See comments at: https://serverfault.com/a/503769 """ try: make_writable(path) os.unlink(path) except OSError: try: os.rename(path, path + ".conda_trash") except OSError: if on_win: # on windows, it is important to use the rename program, as just using python's # rename leads to permission errors when files are in use. with NamedTemporaryFile(suffix=".bat") as trash_script: with open(trash_script, "w") as f: f.write('@pushd "%1"\n') f.write("@REM Rename src to dest") f.write('@ren "%2" "%3" > NUL 2> NUL")') _dirname, _fn = split(path) dest_fn = path + ".conda_trash" counter = 1 while isfile(dest_fn): dest_fn = dest_fn.splitext[0] + f".conda_trash_{counter}" counter += 1 out = "< empty >" try: out = check_output( [ "cmd.exe", "/C", trash_script, _dirname, _fn, basename(dest_fn), ], stderr=STDOUT, ) except CalledProcessError: log.warn( "renaming file path {} to trash failed. Output was: {}".format( path, out ) ) log.warn( "Could not remove or rename {}. Please remove this file manually (you " "may need to reboot to free file handles)".format(path) ) def remove_empty_parent_paths(path): # recurse to clean up empty folders that were created to have a nested hierarchy parent_path = dirname(path) while isdir(parent_path) and not os.listdir(parent_path): rmdir(parent_path) parent_path = dirname(parent_path) def rm_rf(path, clean_empty_parents=False, *args, **kw): """ Completely delete path max_retries is the number of times to retry on failure. The default is 5. This only applies to deleting a directory. If removing path fails and trash is True, files will be moved to the trash directory. """ recursive_make_writable(path) try: path = abspath(path) if isdir(path) and not islink(path): rmdir(path) elif lexists(path): unlink_or_rename_to_trash(path) else: log.debug("rm_rf failed. Not a link, file, or directory: %s", path) finally: if lexists(path): log.info("rm_rf failed for %s", path) return False if isdir(path): delete_trash(path) if clean_empty_parents: remove_empty_parent_paths(path) return True # aliases that all do the same thing (legacy compat) try_rmdir_all_empty = move_to_trash = move_path_to_trash = rm_rf def delete_trash(prefix): if not prefix: prefix = sys.prefix exclude = {"envs"} for root, dirs, files in os.walk(prefix, topdown=True): dirs[:] = [d for d in dirs if d not in exclude] for fn in files: if fnmatch.fnmatch(fn, "*.conda_trash*") or fnmatch.fnmatch( fn, "*" + CONDA_TEMP_EXTENSION ): filename = join(root, fn) try: os.unlink(filename) remove_empty_parent_paths(filename) except OSError as e: log.debug("%r errno %d\nCannot unlink %s.", e, e.errno, filename) def rmdir(dirpath): if not isdir(dirpath): return try: rmtree(dirpath) # we don't really care about errors that much. We'll catch remaining files # with slower python logic. except: pass for root, dirs, files in os.walk(dirpath, topdown=False): for f in files: unlink_or_rename_to_trash(join(root, f)) # we have our own TemporaryDirectory class because it's faster and handles disk issues better. class TemporaryDirectory: """Create and return a temporary directory. This has the same behavior as mkdtemp but can be used as a context manager. For example: with TemporaryDirectory() as tmpdir: ... Upon exiting the context, the directory and everything contained in it are removed. """ # Handle mkdtemp raising an exception name = None _closed = False def __init__(self, suffix="", prefix=".cph_tmp", dir=os.getcwd()): self.name = mkdtemp(suffix, prefix, dir) def __repr__(self): return f"<{self.__class__.__name__} {self.name!r}>" def __enter__(self): return self.name def cleanup(self, _warn=False, _warnings=_warnings): if self.name and not self._closed: try: rm_rf(self.name) except: _warnings.warn( 'Conda-package-handling says: "I tried to clean up, ' "but I could not. There is a mess in %s that you might " 'want to clean up yourself. Sorry..."' % self.name ) self._closed = True if _warn and _warnings.warn: _warnings.warn( f"Implicitly cleaning up {self!r}", _warnings.ResourceWarning, ) def __exit__(self, exc, value, tb): self.cleanup() def __del__(self): # Issue a ResourceWarning if implicit cleanup needed self.cleanup(_warn=True) @contextlib.contextmanager def tmp_chdir(dest): curdir = os.getcwd() try: os.chdir(dest) yield finally: os.chdir(curdir) def ensure_list(arg): if isinstance(arg, str) or not hasattr(arg, "__iter__"): if arg is not None: arg = [arg] else: arg = [] return arg def filter_files( files_list, prefix, filter_patterns=( r"(.*[\\\\/])?\.git[\\\\/].*", r"(.*[\\\\/])?\.git$", r"(.*)?\.DS_Store.*", r".*\.la$", r"conda-meta.*", ), ): """Remove things like the .git directory from the list of files to be copied""" for pattern in filter_patterns: r = re.compile(pattern) files_list = set(files_list) - set(filter(r.match, files_list)) return [ f for f in files_list if # `islink` prevents symlinks to directories from being removed os.path.islink(os.path.join(prefix, f)) or not os.path.isdir(os.path.join(prefix, f)) ] def filter_info_files(files_list, prefix): return filter_files( files_list, prefix, filter_patterns=( "info[\\\\/]index\\.json", "info[\\\\/]files", "info[\\\\/]paths\\.json", "info[\\\\/]about\\.json", "info[\\\\/]has_prefix", "info[\\\\/]hash_input_files", # legacy, not used anymore "info[\\\\/]hash_input\\.json", "info[\\\\/]run_exports\\.yaml", # legacy "info[\\\\/]run_exports\\.json", # current "info[\\\\/]git", "info[\\\\/]recipe[\\\\/].*", "info[\\\\/]recipe_log.json", "info[\\\\/]recipe.tar", "info[\\\\/]test[\\\\/].*", "info[\\\\/]LICENSE.*", "info[\\\\/]requires", "info[\\\\/]meta", "info[\\\\/]platform", "info[\\\\/]no_link", "info[\\\\/]link\\.json", "info[\\\\/]icon\\.png", ), ) def _checksum(fd, algorithm, buffersize=65536): hash_impl = getattr(hashlib, algorithm) if not hash_impl: raise ValueError(f"Unrecognized hash algorithm: {algorithm}") else: hash_impl = hash_impl() for block in iter(lambda: fd.read(buffersize), b""): hash_impl.update(block) return hash_impl.hexdigest() def sha256_checksum(fd): return _checksum(fd, "sha256") def md5_checksum(fd): return _checksum(fd, "md5") def checksum(fn, algorithm, buffersize=1 << 18): """ Calculate a checksum for a filename (not an open file). """ with open(fn, "rb") as fd: return _checksum(fd, algorithm, buffersize) def checksums(fn, algorithms, buffersize=1 << 18): """ Calculate multiple checksums for a filename in parallel. """ with ThreadPoolExecutor(max_workers=len(algorithms)) as e: # take care not to share hash_impl between threads results = [e.submit(checksum, fn, algorithm, buffersize) for algorithm in algorithms] return [result.result() for result in results] def anonymize_tarinfo(tarinfo): """ Remove user id, name from tarinfo. """ # also remove timestamps? tarinfo.uid = 0 tarinfo.uname = "" tarinfo.gid = 0 tarinfo.gname = "" return tarinfo conda-package-handling-2.3.0/src/conda_package_handling/validate.py000066400000000000000000000071041463012743000253120ustar00rootroot00000000000000from __future__ import annotations import hashlib import os from itertools import chain from pathlib import Path from conda_package_streaming import package_streaming from .utils import TemporaryDirectory def validate_converted_files_match( src_file_or_folder, subject, reference_ext="" ): # pragma: nocover # No longer used by conda-package-handling from .api import extract with TemporaryDirectory() as tmpdir: assert tmpdir is not None if os.path.isdir(src_file_or_folder): src_folder = src_file_or_folder else: extract(src_file_or_folder + reference_ext, dest_dir=os.path.join(tmpdir, "src")) src_folder = os.path.join(tmpdir, "src") converted_folder = os.path.join(tmpdir, "converted") extract(subject, dest_dir=converted_folder) missing_files = set() mismatch_size = set() for root, dirs, files in os.walk(src_folder): for f in files: absfile = os.path.join(root, f) rp = os.path.relpath(absfile, src_folder) destpath = os.path.join(converted_folder, rp) if not os.path.islink(destpath): if not os.path.isfile(destpath): missing_files.add(rp) elif os.stat(absfile).st_size != os.stat(destpath).st_size: mismatch_size.add(rp) return src_file_or_folder, missing_files, mismatch_size def hash_fn(): return hashlib.blake2b() IGNORE_FIELDS = { "uid", "gid", "mtime", "uname", "gname", "chksum", } #: ignore if not strict def validate_converted_files_match_streaming( src: str | Path, reference: str | Path, *, strict=True ): """ Check that two .tar.bz2 or .conda files (either of src_file and reference_file can be either format) match exactly, down to the timestamps etc. Does not check outside of the info- and pkg- components of a .conda. (conda's metadata.json, which gives the version "2" of the format) If strict = True, also check for matching uid, gid, mtime, uname, gname. """ source_set = {} reference_set = {} ignore_fields = {"chksum"} if strict else IGNORE_FIELDS def get_fileset(filename: str | Path): fileset = {} components = ["info", "pkg"] if os.fspath(filename).endswith(".conda") else ["pkg"] with open(filename, "rb") as conda_file: for component in components: for tar, member in package_streaming.stream_conda_component( filename, conda_file, component ): info = {k: v for k, v in member.get_info().items() if k not in ignore_fields} if member.isfile(): hasher = hash_fn() fd = tar.extractfile(member) assert fd is not None for block in iter(lambda: fd.read(1 << 18), b""): # type: ignore hasher.update(block) info["digest"] = hasher.hexdigest() fileset[info["name"]] = info return fileset source_set = get_fileset(src) reference_set = get_fileset(reference) missing = [] mismatched = [] if source_set != reference_set: for file in chain(source_set, reference_set): if not (file in source_set and file in reference_set): missing.append(file) elif source_set[file] != reference_set[file]: mismatched.append(file) return src, missing, mismatched conda-package-handling-2.3.0/tests/000077500000000000000000000000001463012743000171155ustar00rootroot00000000000000conda-package-handling-2.3.0/tests/__init__.py000066400000000000000000000000001463012743000212140ustar00rootroot00000000000000conda-package-handling-2.3.0/tests/conftest.py000066400000000000000000000016021463012743000213130ustar00rootroot00000000000000import os import shutil import pytest @pytest.fixture(scope="function") def testing_workdir(tmpdir, request): """Create a workdir in a safe temporary folder; cd into dir above before test, cd out after :param tmpdir: py.test fixture, will be injected :param request: py.test fixture-related, will be injected (see pytest docs) """ saved_path = os.getcwd() tmpdir.chdir() # temporary folder for profiling output, if any tmpdir.mkdir("prof") def return_to_saved_path(): if os.path.isdir(os.path.join(saved_path, "prof")): profdir = tmpdir.join("prof") files = profdir.listdir("*.prof") if profdir.isdir() else [] for f in files: shutil.copy(str(f), os.path.join(saved_path, "prof", f.basename)) os.chdir(saved_path) request.addfinalizer(return_to_saved_path) return str(tmpdir) conda-package-handling-2.3.0/tests/data/000077500000000000000000000000001463012743000200265ustar00rootroot00000000000000conda-package-handling-2.3.0/tests/data/cph_test_data-0.0.1-0.tar.bz2000066400000000000000000000063211463012743000246250ustar00rootroot00000000000000BZh91AY&SYz5O@^;( H`n;^׽'wKw^e tX $DibOe=SjmF=M mM $&Ip̻_?kLZlg펬*FmMen8U~ԻoЕ:u^> ݑ\xϽ%H{\1a9wD3?Gh W-vz% 8qeCG+[~}4@4aepcꓝ47M;=_g3RM5P@sĥt5& lFaWnRH?5`){Xiam F7IHEQ!ˆ{k# m-s3T4k 4|80PEd@TAF"ԅ,RUHT|_7W^5?V!{Ӏ2L"W|;^{B^{~yRb p338vxBگ Ȕ *)'U(R0E%]|l&E4aVq@ZEnv n"C;)%72oW8m\-,zY45}86.b+æ6s/sҺag;SMh~p]Im7%#ˀ  ,ڣB#( U{#㺖]Y"LdoC A|+og6{o6!ϥ51/h54kHe}87un.qfؠ֡ L;a:3ԃ%ø̌!4NƂHI{QRĪix6ftHO B:r3V8ehL,+ V1T2hddLKp([cjc;\K:ir7 /B28 H`^^#rh5twT [7yY{mQ1dYd^pk)cw5گoI ǀh+E8`NtB|.y@S HڎjI+~i܎0m < 䪝l9E(.@f2BE@I em\g#NX8٪FF84YN#LD_ "LE.T=[/$$4n8x-^0ƥ1.`8 s)}$: 65UnAWg($xυ 07k-e}!H(͔ =?  'dcs8UB>T1|Ma0? g/0+Kq.,_̄eyA*\Mp_{ fqc 1 *JCOyq* 4si ', ꥱڇlIʂ($>Oʪe<9m75!͇(0iHNbD_: ȦN4wW!_6Niօv7ꌮ14":_:\  '[2~^]+JMV![-JY1mM 8I;I;-dO-#dˆŘMm6r{."<9Z[RZ)^mͰfzDjVCU8?$?;Ģ>^j=JE_!-%DˀEHKRcVXBPa$Ȳx<@Rm#2@I)mD)KDK~Z*Eeh` nD.y9bJ&Q vgL^1(F+i3*,lh!l,Fs|8 P2X6p2tLX̮>KVSbr$̇!13ߴ 0 p^oϒ_F@ vZ-a4W+i(GXVUf (ym)tY7AS.tЉJv!jXsvE|ft Ku=GݼӗHDٌ;"itwSh„RF1\+ī^@Ҿ eQ 4kݰ.wKKkSs,2:Q#%i_CBM:^kqV6و'{nfauOI-!YĨwh Jjconda-package-handling-2.3.0/tests/data/mock-2.0.0-py37_1000.conda000066400000000000000000003354151463012743000236730ustar00rootroot00000000000000PK2$N,ğ00!info-mock-2.0.0-py37_1000.tar.zst(/=-3Ga@I[C1GkjR`;=+NӞd ŠPN@ZW%5$d ^Kp,:-!lEMeP(0" oeM۴mò(%Ͼn[|׸ۼbMR79+u{_l m{xq댯]jodRF? ]JoՆiikcM(j)M643f[y/龺TCe_l챌c>|e5zl=6ms ҥr0Rʥ̄W kMqXJmS @{P?ŁLTO}r0:{Uns=`WibpQOU<_qdi >?=DtI7_]Ю~MQͤKlUNE"$֖%=Ag&v& Y֖dy0y::%ta{L6U[?Wo>**,|gz c]ys̀m(C73MQSdRyB|KTO-]tZ"׃fU'PZlMMm.ɚ.(=e t [Q%]& ʝ^M[ɩhSd|3vQlS-*O0 TU]oSL30}x)۟2~T9SUS &ā<¡O=r8)Z5XeC&wهeCvSc^gݳ:A{0j?jf=j!nUs-vdI67?aWmډ|MFgNw9kz໮Z!FiY>tQvjZkUfUY:~.uv'}+^׭.6]cѾm]g۞ >/޹d_W9ee:O)6ʆov^({:a1wO2e-fdhm* YY u1Ae $ hL)ZkK]+_ʔ*@6Z1 X X Y 31d_@4Ii2Ar%WRX$ˬʫO ` @XiLhXZmἰic3߆W=*/eG۵a*ss>s6)mSٶPވ-|uVŊU·Zleeop;k]ֲ=me^wQMmm]+|YuKW\ۗ٤5B6rw8 '4b(B#3C$LCIVSD,@1 P@6! JQ#q dU~C N%Ju&ytW~+'vzU*m8c-sAFYI 6O?hZ*կ=1G[ҕo- HYtiڎDm^8ۃwX=ni]Be&5쥴EP~ԎʖHTR@1O{OPSPoQBvW-I7F@<PC35C{xe!Fni+4\!Cٜ})*g*ͦ3)Ee!j UUD4z4lR#  ;.JMs7& >U5ېrqgB+ v.W}O%jh#µޖkQSh~WpzS/w`ŀ͆p6WT`IHZp?hT>(7 U$TOqML93(dg ~[I=B~T2цPؑQ͜k(ɒq\sP%E@FkrUnT~ksx+dhK.0KdžARA1m J~8Y\א{p*"Sj`Ns )f#h['[PF_(OV,2s'/B/lKd8 hr?ID%'XVKdSDvҢvxanhIb^%u\;FVea)`vCFejNs\ڊ1ǃ᭮w @~{#_SaO7yf2/6--:ܿlqtdsa:y:^ | ːfxƲ~؝PvKnzGsX|t72ZcՏ{[a0QF"lBzT03Vb"{cy= 6ɔcPaBP$^KulP &t\kRbe` DWS \i IiJgoNXNǖ(ǰ66C MD, f1irvd#S"M\aźr;;`X3nV$vH*> `H.~r/s9ѩ{a}}$}S\6cM.]ir淄UjU&_UIhR|YL5GchƓ[, t-N 2`%7u .}/8vi|CXxK: 1b56]pD&̓ s&ߴF{6! ½fQ{ $NvfowCS09) z בMqIYV#N o 1N(Nih qCXUh5d;cJ zۧn]&" hBVMn:)P 5d6J`͐0GA)IK"c I)_8xB8h(%@KB6ٍwAB+cTQaׇTH)T_cghM8/0hZ[ 4E3E)D1M I&ڳ0YB!Ѿ`\Dvd#})V0ˡ+ xeRb- PK2$N䍭Y(( pkg-mock-2.0.0-py37_1000.tar.zst(/˜UYq?3cday,'ҲUi[c "$<'zM$foa< $ )%m)e@ d |q(G sNGm,(8L D{q7a=wWDƩіRr{// %CMq:$< 'V'OJFIBpž]4Ѧ<-=cLFaEQ%*Qom]!uE7/0f-7n.غ~[/nYBz"U麵7ԛkR?N!NAh}NfMSihwu. u @c+bKV 2O{T 7ljZޣ0т"_,X^.:D7.uNžݏtE;uYwuS+`/+w+ḙNM}-n$7a j ,_ ϡ4i@f y0n:䫅?1aW"H"3$NHҬrCP': AF8+]M?X_GFx1yhYPi\㋻YP LOC8P\xjS 3=X(T͎hoDV}i%yH]'a$$w߇ֹ/a|+;Y "qgZg\͸w8\ػ1+jOJI4ElؕTɎU_-;Tfjfux\W;/c[QRn0t]yl?S能0|O%S%P*KaGv;Ko 6.]yh]l]]j2: yAn\\䙇5M=뺒!.w]ם֮4eB K&QakyIKyN vUqIySy>L+{]uø|tHw^ku6a/vI3Q\v?{I!XWN-=k_ ] ӤNZ%Xv]'D1 M[-5ODSנ^뺮&ܽқi´\4|oykvXC*DfgusSv]w{j2xktsx]zuzqu]dƃdQsscjܸL4Q! 3&cG &SHyԛk #W GgeuAcG jSmT&x#2sƟ$4IQ'-Hތn_[8~^8lEJ 2?*_ !Y\iZ:Z#J;-%XGYmT zN6PNN-Ӊ- Hx+/_A㯩І}sx/U ,xYhM+ESap[!@pss㪱荆 ]~ś!8h{^#ߖ&YI'-<\% W0[ >q8ѱllgO ވ" !D@gi1r"9Bѐ/.d0D4fSakkeU A7aڤ7ͮ֒ x(sw"R 0fZv(445ǕxN4Fc3k3g.zhH#&OMXO3tcW; v_ѺB` !dpxq3zomDjʈ ?6шmGc ڷn |Z"t+RF:9O+aX^hHi~QKlN2ZÝ\ƗKP*-qQQZ;j#,'{L  !Gxr&q Z~-3^|`G6_xW_JӣG8DA;wN=&@g[Upu_Q.0[>NwsiDgD*SsB 4e64H"&6^v:zN6<=`f(Н>~W&j.z MoYXtt**zL/t>LtYjS)}U'=usX DaeS5MvEԈO5{RRU;Ų!/.=(7:4ܬ]eDyWaRt-kŹJ,1p;t䠑"_,n;=6X%]$M}eټv4ɉҿ~8g?2(\Ep]_B +e ~">1 v"L q :s+N_!Z;$(++ F,^qx=G, Ka;ǻ<[ tg7|tsu'0"Ha)Mu/fUgV3]มɮ|ş&sp=,!LܱyqGl _1tﶄ:Kw/pu7vc!'3cx<cmJ]^Anu+rVfi4E Y)́{?=GE8u/uզ3,L ޻#MaԢ6=!;-Bzvy]ΤXư֎bTN&pNz\wYZ'A]ŨyGf%T R== t#I@@'B~D2.nD@/ y@'=}Ť+H@G@D_{yw]GzGqQ/6$Qg@ozO>2.mXA{1`=8;}:;#=G^v~S!t!= f.P>z΂YQt1)`P;]Sp."Pu p7!3/.VHלUky0(SKnu*}7`(w(IYrwcX8SavV촌[3 FC@'fńR\ɀJ Vɪ)UJU$Ujt=`0$Ssc]>DsQ),┎O^d?H;: ^`/1L4qP~q6q_~s N2*Q^TRFJ2r!L@t 1Wl$nMU $d7THVqB'XO;223> ;4C²zlh; +5YЇJd7eYW$HS>İQƉO YX(U /Y,7CB6?9L5Z"B نJK,a,ȗ|Uhi-J.:3W^2hzo,Gpn*;0M uٻ#ւ4tTg߮g7hػz2o5IH6;Bp ^ FF)6ly31 O" ũ8gRst 3f3OgFv׸ ̄ xd<[{ -V[y8@H"!6V*g#$%IP߂NL3?0Ɖ%!ͰJ0a%` #sGDwZ;1͜o"8HN0֡nɑA u4uT_#S U MLnY#,_DKEvXˆŸers~=T$ 8lc1Ċ̢onT#tGtu* (opȝlh|p0bve\>D+ؐ{X5`E|a1(dPߒeb2e݌gϢ%ÆX%r= Hl9ݍƧM8$~5Oo=3cC)bt0;]87.-,+S`PT')ifBE$BpQ! O?rUxadH8ܰƊ|7ǿ-b/izU^VL`S"B{HI -,庰 ͣ,&r MpĜ.IDʸVž? +_p.M\16_^rP]^;Lnzj$LĞ:Gpͤr8RٚT6hP=YX gɛ)eqb q47#kŶBa_+*I"YUgPeg,bRWpI(FW;62kc:jKa%H|ɯq.'Ұ+a}e/,xr~?+ (J=/aX)+XcCŧz ?⿍GX"Qj/(ѧ.S'ay>zlqk-L 9cX#.owhE`pհk4?ɠnTHL/ ?orȜx1VCnSaۿ4ռrg_N^Px&,V.DTz?@Hz;RU~F(9Tj!?3f5B|3½ظ\.WpGKy)8Ϯ[~gzD`Nf$ueƷgK _@v.AY ou>r!Llqʧ=*'@Kð "؋k](>$m17 E=~ʧP`Nt\54ZH0'f=.J "5l>.R(pt0h\Gm@IX(=PTPď>d~eJ(n 1QjH@4N߬G@3hKPݖ&}hWrDRp(A̘17TS)%T姒ȯfRP!?7gHm}A/JVoϳ* RW'DHf:uzfwYdn MnMB@ot6,KL0zg!LWMX ^nJRwSzJAcQu5Ia+*z>. eHB<؄jU5:BqCķq;W>";}ӏyGg[uW1t"ԥl#]&eݿG^^ w%g702,u9 `Ԅ_dڃ2/i-"*,\J\Y,⿃>i(;an@mpv"VA52,ܷaX/w JXn rYTZvһ{b SRV 2Q$ 1=<\1T'd^< _rp$%.(+ 훻8|(H֩_@BˆpC2 Z䜅T\/ u/ ^v, cmrft#ņZ#C)VR>Z%}4?ޯˡtŝ?]C쏳dIbǗc}c,HDUYCG%XFk*y#A¶\]]XR%ACNBh`L48Chm\)Q(ҳo&3lV8F6Ʋj/(BtحV٤H\_G?-9a cG|ewTDXSy!q8⌜7̪R+`ɀɺ]!n[? fzT;~@p{3uu߹f*v7$` 3H8p&`&jl"؀mi@8 t+#jb[3nm8ьz( .1xN t ~`>'sO-,ABG,@|OM$PnngĈ`70Eh>TDr8h{qZ2'Ͻ[wɵA22#t~X*;đ yB@#S :iUԠ. ȭ2q^X*Ripb,rw9DL0뉚訅E@~ې;PHN?0Yxlk)CԻ+o\gzf2[RL78L{ֶk 6Yaݰ(`B7a| Vߓ"XAjP.g)O%amH"Uwlep#*j0jlxM2}b~6J95țCbOXĦ_*؜dXvSzI弛g,rt`ؘ U&~*X1Q#!DF2_z)$( ²`geZQaYᕯVj;P:Tbd'ϟX=yfwϰ8pa#*ed [(jZc77fTFHsMi{&9g|$8k d~v` Gֆbr'/Nr p,xH^BK?!Kl)X?g=]b$ͷfE4Sc$JtJ b @D1) H m},?K޾ڔ[s+NۂYlJW֊PNw:@ ٴ bⶾFO$;`PI O,I_0c/.դ(ҖYPb F'UpȐLgR21.t/Rof eR5ՌԠCҚnHHkF==ّ+eү=kXyZMp.s?B Z j.F;-2Z~?UfkoǑzPn/'N;}RDD yU:< ⒨0J>|Dk2J@^CL_PQGb`]y:.Mϑѿ(< IWT>mB[.j)L__D[Jc\oèyP8z7E꿳Dw{ )+Ƒv<>>$ ;MĘsCPk31$Wm%p[./Gwxҍ1Em}?[3P4\ަo.`,g-,Wssg`CßGA0bI~ܮpRdJ\ӈ%^#"XUcY&8>Rq3NdFn=:j~tP&8 1*+3IԼ|lu5BG+M`/+G/'уSv_ l 댏"о[(`3uQoNep@>fIC< IOG'S"ն:Zf`7 V(WICS['WH?qѥjr}lo:ksZ^~28AR͸ V)>_N{hS kQ~O܁#vHy$oG=\ᮿDҠvO)=[VH!|]>)s EP BYA}=t5cDpqT#ܾaK&^TNMMv}ZzMgTTGl v$F ?[L˲ނ"_-P+A^Z#tՇy [. ǒS?MOv&& J7acarv< C2ӚH7ϸ$@xl/^/z$>wS2 \Od00 ɀ\8Ck(\-Jz CcU)|fW/W_zhH@bgFCޑEڙ1qqH7`(Xas`o3X5$Tr`oP,%͚e '`\B !υ/K A*2vg`h %x7MZy eQ"le.YH`&92,@8 l%gpeapf$͆30N1CȾ7:Ao+ti@?N3Ǐ WiÃӍN-^'ueġ,vLB t.qN zGZ>&XrdRAO DmGz&m%9*N~t" oWY0`=-Zv4"7.>龞 p>F10;&.+9e ~KO}t>)$$?Xo\Ik߭Y QII d$8e9߃d'cƷSB"PV)b0k#@+vjy0ܢ쯹Y?QFѕ(5@B^)4!{d1gƓih@&Ku7 Lm֡>I?2 pEqrk%E]3AڶrC.NuD EWWE]8DqWILռ kۮXMɥcD59X;I'|JV^*AB*86\ni,WFD>!2VÝ"#§`lrhK! Шtoר3DrClaĻdabhMJˈ`oQu z.uhܠ%2zX$YҤ孤0uV!'8ʶ᥂AA10]lD獅\ M~IB⬇BQw3xeh-@) ;P#{/EH-oD)|HCLrZM,Y3]Z+ɺ5x0/@1ЇUCv3>za J\ Pb3oR5҃ #Y9`[./u.vjG]j1pih)t+Q/Ȑ޻R}lզo0ybY}J3!0/FNŎر=݉:؃IQd! >5 @6/Wï^ W[0uަ.]8Ϝ>-pZ.jM\'. &33!ϯ8VǠ?ces♩b}G㳺'7ҚrKuV_@[&[C ѩՉ fFDL5NЌE8OLKSJ j.SZ{|K5g쪑R1r8`${ڕM~n ;O>J.TU4Y¤YjŇ)wE g B_ ٪*zAm)!{fb;n6EONHýۘ7vj3c>hhq7=1"Ѝˆ|q[Er{Vj JD~DzYYѨ{ xwR˸MM5`2ӗ(?{G_Ȅg @xqE\ Do.4wA:R5) Hx'ҕh4mC?So  @WpK~0 )M-,ӝr!뒘 1P}Mi`y/{}Ojˢ63_ύ1 TFI-.LB3#\ȜK"%k6tӴk]QS"1iP|c&v܁ssࢇBI"!e/YrV\K*2"\Xczd9Z<^'2m##uE݃ ~Q$zTC kv8d@VĄ[gꈏWq59EL'XA,a/.&½0i,*:ʕo"۲&X2w/R1>9_shRld^?˞ZNN]kc_ x_>b6MRAԝ}7`trTyD:Sٰ.hC쫚J +6T֠s{wq 35W`gݢb!qQB]'0>AWON 槲ABUM2؆4`宆FZj 'P {U-M2)|Tzv͏f-S˹zkǵ2 RT9J:CSˎJL oA0aypR,O&6c? ޔDAL1 !yN1SPrխ:Fc^LcbeV#V-kz-3N0*o:%a2! =<(r1)Tǝ4T [뛃"-3`${|?`[$jUGx{@$l4.)Ex;hbH5ث(m :aae†bvЩt@rQѿ2En䓸[ǪRKSF)t?[xC 2UqD3{_wnUjY7x*yӶG}!U=`[8q.:c_iyGq=?눴]X'קmlOr#|Ʋ_HxJ~ʬeϕ>&\'ky*I@KNAvv?93b pk/w"qyɃhb>/otyֵ1(4n/I]>TQX(,TGa9VN%#ʺjD49P@;کipE)@63bS bA0x`8>/ߴUaiv[4ߔ@rIFVk6*ų`IXz] |Dw~/E_c5 -*0keѼBVO|`8h䧢)FDj_: RxFrŝL% 6ݽ9r)AkqY m3szܔi ^WIfg}/lf6k88:r:ArMƔ6fujW1C^d!D6܅8g+>\pu 8`jBf@*F3Ԕ4ҷ-߈ ~W8{72Q 9K5dHm縷M ;E‹h& KMI*^g᫷ȍ'du,-g--@) %iyӤ%M3VeE}рoJs(WQZbFN;]/9iU7 ]tf\)'H/I ^"nrD "A@ \-$< U Bqa/&Q)3fԓpIKl%gvjGgeqe?i\ddüs9~hgs V滎SR2Gc97l\M愁$ JH~~xuQ_\ \tb"fF9|I !7Bgx(˦WG,\r3 2$]>k+"9Rh\2kp~z`~qxsՄ.n9#Z ͿLA%5MĵgZ֣,/M|,ec%Q^d^ qD%Ob V67~%21ꋜ 4+' _!N&_v!bGhd;IJLr?C_;C@6 JӆX6}Xkǔ]K2p/:Y(|IS^C 2W!h^ {/,juԭU"f%wHTJDU; ̋7I]HPӉT.!_vŴAu)A=c>\P*ݸӓ a¹g:vdTҮ+J))Ow۶"*n1hNё~aF8Cھ:: #3$Jm`Rh]m?nXu@:r,Ny44񿆰ҐSOyH1E>rؔw?r_ _6ؒv.nRJ,OV;ɚe h 8݉ee B-^>* Uܿ4#\b9@[>)@Gƹs4YHZ˒, ko–RKKs~9;:@qĭ0|{ p0&KTFsE 0n'PTbhllAp%f5s Nqԭb>?Hd"z =I4kdn_"/9Gۯ{Y.yJS&A9],w}%.KӷIkS9 K{h.(bK"5MBl S?ȱ[KI k6D#]G{1~,ܶsRAͳU.IwV)z8Up;C&8hV8:SR`qaj%-\?ؙMXwRZ,CXph!2Zhobʑ<f70`}~@;CEg@&r! nB#hFz_1T]ۚϸ _I)rDۊ~7>LuY'N7dp7u6RmT^>[ )$I9. XaL4$>b+&]PRF,T2MƯ)vhoȺw ѦݺY(7 36;Ve,3tVE%]~_8W)xofpTu~gݜq=ۻ]CxA=C(deb~- (`U"beyWPNE*c<fUoa|L E+eV9A/9U2/aL Z18 BDyػV6 8Y!0%Pf__M2vu m(Ǐ$%dԫB1k=K*ٍR&M`c)+ M&K nʠ(oINE˹gq%?2?~NW 8T5ސOQ亓B$17 A@++əkn]F[4e]w|] vWNs}pr=R =bu\qM>\)Lq;UM"ݴ& Tm!N2'Jts2lLbEd%bwvbN7s\HATG Ȕͧ0"ɎñԖ Rv+Kb||1\+K X\Q oLG jb^b}´\Б&\l}F304bCpa@dr%˫ P݃6s $љM@A-Rcx @z_rʪU{@D+ r6'.DW! Y EЫ9!ViSC|}$ \E7PQR=//CQ#³IHF)S_c`!b2t 0q \`k1L3U[цz͜JRe]N}~xǍ֣}/`Jog 'A _/!vi.-@ԃfY/YBߧkP`FB2oDxM-@\ Q)e'E>YteDp1sJr!x/n͗C[;Bki8{4C'F)g4yv"\W :gHDEG+ x4wM )&;o_xm.,,&] IƟa)K3֗U$G ^F!Y&N-mV\lnv'1q峊 {lɎlFrm*hKfAj({BC9oAjTĨ>Lٛ e Z ?TE6@JГg$,[B,Qp8 OTCpJ% lIu(B"S8cX=6Lڂ};ѝ$ u[j@3c+V\w88ZvlW&>JQ1nO`'^ Hb{pE:1 @x΍X a+Y@#t蔪:G;34싹 a50*Cxd`)rvTPIHV!4O,|caͷR!J ky:>gy֟+wNpiv߲-rqEi[q)Ц1)簤)f߸v;#s>g4~’+,\2v")%|ud8ָ8Ȅ]搡"?:/is0 p mXuf+ecqt+ Cj'Bƹ ɮaڇ 2 C9܌+J ] {>U%#n3#h0kr$!W᎚kW8~bΛ-VESyG؃ );[6B)U-Yf6G+tn0LdەsL;\G4l2B"ʫ-aT\J zXd/(u< 4bӾ67 pV]V H5%9r&=xk:4P鰄@G|}=.pJ(ԩJt60+U m%@8tK1Xz&f0OihvAe:^z %~ ph9&-X=?/ňGQ c]FcI bN#^ oYhV:)>ψZzC+(3*¹q6PSs!S^Z-\ $ ]0e$De[k?ʮ~䊛-wY-` Z>Y* ԠXeO5`ˋŶ϶U *k/};ڬzz. bD!rs@VAx(8*)]{}t< _}"8cŔz.A )HH5w\OS bkO' oR]22o~{M\G JL7SzX~8$M9rC8ϮSHgں((W ĤrS$Q[wޮ6hUtQ᪺rIܚ8IUMyfA.OO>~Yhj eT1<ֹ\@X2ɗ>PXhI#^Pb/E wDQ4NcO;=TL`(dMe@JPx*F [EHkҢC1.a"%b%^v2޻ء0d3cªI6%е\0pYXؔ.07Ŭ"tp<12ڐ&Y1pدCܶՊa#oT(#9Ctu]0'uڬˢ[Q0i]C7DH4F7r@$ |a=qu(1A0&PYnQrmœ1t}\ pW9i}m?՚2_p5[v< Љ.+TBuZ*vsCXυᎂE敍_Ş+4_sH|P/}%Kݜء;}Hi|s5ee$0bb<`AVFd<Fn'(.*Vi{`|#:6Y"pH i`ڔ~gBj:8" T2i{:L9k{WOKGY;+(/pQ(H9H?n)3k)q7X FݡfBԞȏtHp;槊瘹rq4=ԩ~믍ۇAw '}(rq/3U#˓V1CQu3,.o?|2oLLUn!rIOzdÁ;t ^bH nI l6N:٘1ko` iM_!\ܶ1!" %V~At&ld[ !qxPD#"r9*ʃi|!FL'rF Sۋ=c jᮌi.sCo{rsEWT$N--* P=w ܱwjd%_:e9EC*bu/lT R>2hq² ڪQ 7>Ecvpj']OD5!ϧ|=:Rݶa-νf9.D^#sX_ k~2NOk>buCCH*QAňa5u}#{ډ/_vnu}{1U&q8]|\ qgIN08qTb9!7>Q#*),5TA-o}u8|O80O C~k9[1tO"\cmd|"Й5Z"`t?&f!,q@wu/~4K[[˺3p s$,ĔC–Ώ$CSith!m 崰cJ 4@)/J;y_Fj>/Gcs&&'إ4À5^փQ' Z:I?E$95AR1Cß~`h`$EzuA3}\?se"+@[9VU8=*Hb׳Haг.Pñd(pFFi֐!vWXȹR{3B{ةzTrt`1QiЁMuLqTUJ^cKMp mlvQr&8~={]I/}3l!vub /DF,B~8p+GA8 q73tRo@HG&Ub(th⁗/bcq#ލ9as׋A+ť#V%ίe cIJĝ-[qӘAaaX~k͖#G׊s $/,FApE>@ D tY6,s$njxgj49V6 v3TԫWLd Z]V0sSFHx7*$c9g ^4)Н:I2"D;&ދi 3+:c*E'B"F74E a>dQ<9Gȼ{OV٫/u[w}vd5f8 sݗW~TM*۴ A/H*}7lMCm<*te.dz|Zo )@݀TL@}]ߝԲ\s_h؎v("<\)5a|JYz$oO C\=5JY8~ zT0 |ʞ%o`4c{ pVd}Ee0W]ꝁ ;_HruU.,sa:Al.J&,H:.8.W W wh\TxѪmwB"W|}?Nl+e@sRNYF+]qQ|2ML*Wf ;v Tظv{ồFЙeD6MdLLS[nBWkڙ2Y_}6חq/ rIӀ936*; KNNIm\Ot @U/:lnmƌ"60T0IR"6"Z^r]x N4Л] GL9~)NU7)B5;\XS )/⬈VVFTҘlB O^)C73,[02k uДODoo L^lK砢l*~40hׁΆC`DT=>4 sHvS Aś22x\ç5)zAQipl`R }4.sҔmGz(;{H =h #]BJPigLFSv PY7 ݹ' ΖP7OߦXX7a[hohFM0w-]kET C 2DDs1 _v*S?Z0Oy+-v ϳKtX d@-)lR@m*Mw,n)&L8k;Kj`T)µ!(?~OOqK 9dh^Xhf.遒wCRD ?Ǒ=7󬅨!:& %uaBHdDްmSyz̲)-J|8)gzukYZy(#0 v=X̚Hpb&5Vu3륌i{/ݧn4JW{A?Z# Qln?2" ^M5沣-I5H2EPJ zyo|ZERGn} riflp;i4'a%2(IR\s#Ot V&ˎְ:OnQ$S,XOO \G8{! }o*#q=J9:E;"P`iR@E$鸂ز#GVEbm#Rh C|LaD8@ t e{.R!~F$*nXA-4jᲊrvk4WP֔O0:X4k;zpfA8Sh>zK`7x_[Y‰1eoӢLD؋ T&HZfjUi*=h*Fv+KɜW1#J0y؜ 6pI٨QHyS. [~}U%/t2# =LXŵF<KMwn;< \N\!ښPj:,Sߖ.>[Z{;Q# @sx;!~[<#]^.>8mJTC|c0[^oT~(_Z0ohx]lu)"E+feʀ65zȇ3/bYRبK|&q"G[+0$ (V,̿>0,܁Jq<f>AH6WQ YDqQOKeĄ J@K"}}o2/IiUwی^j;iak& ~DZɂuQфj(,pU2tUZ o~Qꦿ W6P-+S]>{c4I64S dFKc|WT=&0z+%݉7| ]GE$k,2}ƞm"LNy:`r\1DQ8jko{as 2XKMD%|g"c`]3fx:Q2JWኚRI'',S8zW02Dv85Q3ع!BfX)-ͩ:4eۦٻa?8zT\9y=2}sRy$9ܗ(LKhMNOG6(BG" s1!L.Yw1w9 ߔ} 0T Hid>^)$r:n Lmen^is6<$@}AidE]<[|o->%W?'F'I$p>ei f 6|\Rl5SEGo [:WM\ybx?`t%iI rtnt/lqsin}yFA-ABU`R@~eZoAmh Zh:JPNY6>ΫF0Wu`jeC*1mCeeZV!Ji5y[CFXiW5 hܖG9}UcPxBY\4*xr."#;载'i~lٔC$OEsI vIˇ!dj Š[csBB8ɬ :g䷖ʒN%O2Ëi:gH, }_+=*6AyNN?V\Fjt/xoiWbc9/C"~4M|,S熯ԛ3$zZAԓ}|v$,9?ʙq 2W+>] ~ߩ 5J9|N JVBk2kC:g湑\b8]#)}Nj&0I$xh;Q{y@KI`&J4[,NϹHlFU)c%<[8=yes[WڂHSv\o`z5Ixs*c}HZX4TxγDz~WPKr/~!֨-e F]r+ݽ.wuHGwhKu2:Il|EiȎE1o#H2f'uUopR>sVbNZ@>!G۠kỂd8ҕ~XF0`\@&෠%:Q~tӶuiG]0j;&΢Gb>]0[B99ԁ`h>}dMr J2ߌYKrÞ{'q/”fZ/^NJ7QqQE S©5bTÈO6.߂81]5Tѳ>'($PhˍAB~Q-¯P杯t-srSn: n#% >QKϤAt(ełNuHtz:Q a#7,9{h͚LpCI٧LvƔiCQMnHj fQW(t9%zG?|ۣa>>:9)c1UKד=dT_==< xԅn@&$a GB.f ʦ+2_v^CKpvrxB"m`*2a4*J#Gk˗M!"q"bQ&_i,Y0赟'b]6O%-S;-$v|ShJs6vMq;v)xE.D'5)EFrslP ŕh BDk;x~ڨH֐웝e|̕I03@la[/pݗlH(+οbͿhGt|ytF 0KFϛ/ӓh+Pga{7PdUGݛe!y@ C'|l`m(ѫ l Ϲ8 Fh$e@u]:2T |l 3q(#G/~C[w/ ذ?} ~kOPITR/Yοܚ09$*C,7ʶ@A͛#[Ζ*qʁmIA.[k-Tgu^jd8_x*C~*a0QY' ؞\TH#G0 D3k(wXK@z{b $ІLa=/R6 >Z? csƎ={UD ˰?ЦWфUF~(LU[8{k_tttLXu1t|T?~*:wqh-`{"EdM۱VCF*%hh_|?ݷEl`ǚq[\ul#rVK\y_$vUxQQ'>=ogc}E1gV1KyW]-ĨBo zdja ]~B5]WeEC&!@t"?$SBwA[WC%/9Egڎz4 #+Y #^f`2.1/oI]2ʔLLKr?|@TzUYS֐R~lQQ;nJY2) `DD>[uѶVc(Y>qz ]X{T(fm  |S`)tawo%IcyYzleIIoITJQAA2g#Q|rC$) e߾E`KDjooh1kX8϶Xk=9Ww;IT |YA*ʄ1w%UI5J,Pxw2jsav&<MAMy;kŠ`O v%0CJF-hpq'w0Zcة RI*p)9܀'/኿@} 襡7ýs^ 6Oao ^*1 @O[4cIIx)+ +J >JzFA௠xW{| J{ʣ+NIQ?YU铴 Qʷ=g2RV蓯=A/,6TY޳h(jm+jO_TR_(H(ToC/io/2y tO/;*6HH>H Hå=zx#\Qd%$8< TG"*3KFEzDR"hx*!>> !#rXF*Hy;|#uC 9pC(Bc/B(@]|jKP(uyY@) xwRZjCm:iwGKkhOwP'!9inzB;Q9}I"8͠2V1h &KaU4J-;J*M+m\QH4 B5~|h ax0<_;vMnRW[Py('^j4=LW-Y:17V 8_S94Q sOBcZ=sE2\ 6p&*MՖ[よW>-7#sJh΋VHMZX~0N-7gи.X=b/Q*+21ϊedl㎄* 5 KED/{ /eeQ0Gɀ*H G<;o]X|4b2/;+$){s8̽bfnڎ͌e:E;4w0B!0Nt2g0|Q}BqdGDq}y)*rg-$ sJ dePB<: jx1cԱ2Lô臑"8LkY%hF0Aw[OproB@*bƖʒO":^6'֗Q!| x>3|*O߽?1NtRZf}kOjzh r{)@T}| E LMZ n]㥫)TR~RRx]j:_-c(;xV^K'Ԣ3ˑSE|G֝S7ó=%].!8LIgl8B]?߂P `3FEq"?%l02(`^X uqX.. {}&뿀`o]$n-k$a1濾EoArۿ*Hv}7zk\Tp5KD [ ./~A钻Kf DR5nuseԥ0Cf2s3Fk$ٱ$1\a)0P8tkGHI${gA9rzjOթ;NRozam , h=W H5óʢݛ ++wƮka$Ov@ r"մ;n"*RP .%{:vT5Q59}325.n@Ed?%~Гfw[@{1skiOUz$k "ywVhj`?0X^9?v/]|5|QZž(]ynQ;S–-\aƊhii#T(#*|@ =*d/#uQsxZjN=h8B)udV Kxs:J¡%H9NFiX[sB|,Bp KɺFkBBO9Nԑdw@xfԲSԬPj*pn*έjs `m,7P[?V5/e1&1, S\fZk#hUMF$jTJ:WfKo6FY?ƽ$<-QG}J%p$&-e8--Yfh+Iz>KѻHfE(Ù߀9P7s͆qKHg#Ydy̱S]=۠h/4XQڥZOiy~X(TKI'ω`%)^caWI>*./246rL0\.ix|U@o΀("5e-_> xZgZ 0m=qg!rW{wT]DEE0= e 0Mv򡣇J;amӨj{2?A"#mTga jQsSd_Ơ865{ x x&P '<n^>u7=>i oJC#Ww_O/~d^>OA }_2- !I A 臏FEuS7 DGa- Qsis׫?$όy`-+^retK;y y]CPfݶo׫L^:ܕ_5̬$:oeo6q|ٌqgH!jnaŔcSԵnekG[^~JRo&t ,*# = D ii i-A#]^b xfQD#B0KחHLRiËApkJ_G$bՌᠣ3N3 A[RS5Eѵ_e`]xA$d$vS+F0)01 &W#ZY"T4+k'<ܫXM[n3)3+_`..k X- aTHoP~#Y ucY%w=/&_YA͵sъTE ykDGu?'ㆣJG5Ǫz^te>$7caW.B ,x;=!P4w$;ԑm:Bv.R˰$/0B|;>QFҵ iɰ>bw~|l #0,,*mtTӥzcxHTxI7: `k܀ _x'=cXu<xQt l?e^K|oGǰb*BMԪjd \i^ C@<կ -TPn1*quEQlfmp~s*0B%ЫVW7b%sm^ũ\F)j Gs_A}ݠaP$$蒪4(SEDh.Ģ휺'ZMWKU•5]MU 5"O ߊmeN"mՎd^ǣMU֟&L[zStFТ;D{(~R Wj.|^Xk G9/nl].`BR8C X Uz UY|ud! l06#*ιG)W]:/KCÝ :c$$E'[﷡ R1HaZ `(n|WK49 44KB0ښv6opّ1K!7 CjM,+QFGx 2͆3d(d +Q9Z (mڶİe^}7C9Țޚ{O};cG_͍u;0n3/Vǥ5OabYG6v;5UBEmF%5g/l1|aO`ǚ'lx _I{%k{Z9I֊]uF;]%/ [[ɃYq/&/8NE|Mk.2;[pkTSE㧍[gNҳ#z`ck3`z %Vb"އT7,[Vg-l|<8u0hgf4DQSGFPs_vMH\OGȠH:`tSl/Sv%ߗvDBUuATão4 [-$Oe @ɖ&3QnbMKtDRu^AXr;P"kT2lj4,ZdjK'LCT%KnSCl[nj9r0҃-~G.bi.AH[oĥc4KHFzV xwp/cMG_f,M4-wk-juVӾH֎+۟0xg&F`0,AF5&U)n$ϜYٮ Rƴ|՘le)-6O^ 2N*Ųlϑ"AɣR=rE|"c`''*]vfL1fXTp 93X WTQYs;/(Z;.w%0sɊ.uhb  0pJ `k2p\9(ԞKEJ+u7_8x@_b,F4-J}O J7"c]Ĺ3\ZnL{끁R ;9ȱw4_#phKppT}@6=YE؎Q` `&յtk+/-W* 8鎱ANCPcmG P2KR0Z 'ϪU>9s44$֌0=C. y_K#Q,b"CA>=2 9Et(Bƾv#a]΄vψG(^u!OM坜ZLzDSN/g4i*}xL^mdcr74o%#S <TDPH%X!F$B3C .\>a]ʼni12D)<VPH(,חxSm?.W'shhD]5OŭrfͫP SL\֊ +_y߉[L̯]g1闗O(fxoNec_~*?eeZys$d' fF9r5EIpH9RP]y:tIYVFY M"qX!Jǎ!/6@ cF[SIdqb90Fd 1cl'!?;3~EJ;x-vg?HzXAB "<ƽ^jG4tM\eJAК'1>:sT|8GZ z5BޑTŃ| |D' PS Biq <,kfE)7D{Zx옔 8WW4pe6X,^AKڷB)Mi:IVQȮMot1OEo{GoN !{if|bHK"](TGKEXs;P 93ҘՌWSs󅔚}'k,7 _p yBsdyxCxn4P4/}6v696~O#{y?Kd93crYM.K80@ jFJ^둚qN_#cN#DqINĠH~2]dhF&W6RvWdLINgLݭG'&)&w-蛵ߥX|z&-ac-lKl{1z̄wC30{@ZG ']N[" ':l{u/Celx3I@N^oHt$BP5ضZ4mA9:y!!ji`aMVƮBfog+pOn|'uߵA4'u?9!~ *F8i8H_V0$2A]4HkPr誩pu$ɠS@kL=2Q0Vk󿥛.c[ o}9֝zvHtKb> ۆͬ}^b^>4wcw J!:;|f͹!k9(Cί (q}G&JY~Jq~J\|b\}FCCrzE(Ck=AK+fr#%o6.W0HLEBgJA*>Ih\@ar7J9Z ) (x-_8`DW}`D4\|%@H+1 "!EG]8Ro; fJ?Grb$p  6%ա/A; Or#B|筮`FIrbtYꡗZbD 0Gce0nd՞^]/ٮR΢9 b 6/#6/Z1T% ۂMY-tOCbΩN1zQޥhG`@dQ>#Fz=r> pna#Q܆4k`k( Y<ӈ"im\H|k~вAu̶vfz(ߝUKt;w0J Q49c@]G xLcp@3b Et +d%MΓse*.`'orGWfc 7f}+L.mjeeDB.rWEŌN3Ʀ#_h(+ ?Jp)5AkDZ70]O/T ~di*\{8+c[,f>hH61CSmu܇ӷw1hZ؅SpF~T%=t@I,@F&q1P8!q!P|{-ghLEЊ[!6#;;ʛ-aVkDah9vdx[a \)AnGBY pP ܌9J,1"!Ǘ['pžD_+*~[~/~mks[JVʫJh&)@Cʾ HK !TL#%_>)Gi\U2ЍuSsRǩq|PqP8(qn(aFh؅IQv83LNs?-Y(@D zoHST8M{ƛ J k$U˨;#|-QQ̃ 8iBǗɉR%b$"c*KӣD"F!Қ0f:xLVDa;ǽqN>[' Z V1>(3 Ϣs%g}Dtq<Нo\^& do61qIo*R lz k՗g \jMYD/K` [IZJ:&mOf>z'@_`x s@,ǟ.QUOODq lgnK(ҋAc%g쏥裂 R&9r%[փNɜ &j^ R:x$xh hlv`N(? =ϲ;x[H0\V]g߱(QBc is|렜a`uPkcϏɇf~F5;04}e%8,&-GX۵yHb`Ro;+m'`H8# 22%aNrͮs,}cH[sj_Aq+E=M$ǟ]Qˏ†PCA$lNGa-Xŕb#MQ=f1e ^;fs@ %pYIe-@b" (9yJ+tI;hCipx< M*.h D %l'R gIq0'..VHq^WU~HO[5Cj 0Kzg+~^s[5tׄWEZ ׻IN޿u3~ C' ]괢ֹTơUuA)_C -CSn}Znx ,\l!lbh~qܯq]  &5޿S=| Zu麎Fr8<=`1쫷>0} +hIw 4X›P!>6ӜX~J>כ[KK}"8򻽠5_qxR<&ZE}aA)Y^4"Lv@񆋵9+U$P.󈊬J[:#1b%o|5^W]FZJcI&|wh֩.} † .W(J^ "N83br F+del|t3UI@٢FxP91ҨNOt6 מ$midta|3ޞydQ_TUBBdCq;ɀsWe@:p6(üWg㬰q9OZY bA/NoԕXcz*pAfvqkaλ# :<3FgOZQHl,jr97uOL &+! A [:' wj6Z"1J'\(aW"6JHқ,zF=Rޱ7S!Ա$'FJ*v3?KWDsVO(`$_7L.@*$c8t +dex'ySuୡ{eiܐm~3# ȟn<ЭFx?ˋ8T6YczUvbQf6N9chV1agzŖuN^=DPI>n68_;y!௮ H' [2T dX)iy'͖/PKĀS*dGubGq_JRӡ %@rU:zM怆/9QpR4cdJSC[HALd2ɸa-_ľs8`5vM}-X[`oH˗:)H s'u⯠,%ZςA׊nvk͋=L]]S *.X2a~zk s2w2Q3ϽS0v'ǸGF1Rr̡Iˏ :}/fQ "vË vDF"cޜAzY:˾B'gl\\pbB(!b~kƸmP ₄/9m K/ݻ0XJSwPiQKBvG0pL"aA+-^POzPFE%OM3 3dIJ7@Q޷DsGkenȺ'#`uRc%5/Q'G~8K΃)wDU}f'`WHJvsޙJM!#[6FauCb#N+Ħ7홞O&?9ǀ}]JLٚB:jʊb q1tK~DE erG^EVWN 8\ϸ͋ km"!l@8}T[+g >Gz{Y^IbâB;Fp{8:pAEwo}LXiP%k'jK <EVP1%!$gnհڒ!e^OUJ鬮}d]¯FfĀfÐD|ݾ$:_MOcWߙD 7t]l)T<)F =ا,mYx]vfuNnjFuZ%˒1/>} ?>)IrCzΦI!ji;fVCJ-fR \$ mTҮQslˮ%a{g .$$uqԒa6`t Uܚ`]&DΆ0( uom6U.gxr@|iO BAkO0/{d1bІ:! I`LJ #gɔn`:/4Bv^nv V|tb̤F0RiZ>=`M%ظq!"1Mnf+x$krG F-<|;,CX ^gV[8BfJ3{ZDv wwΔ)N[!\6h+"*ͩ`"D"(—6"Rd Y;np Hb Z@B#UҼDsUBc57f9,>=ZrðP+lš7tsRN7f ȴ9(@5ۊ_sFEp#GR60I/iBI;Kҋɺ.5>׭18{/3iO[{%I|/_禈ZiR[,տq@֥!SeR_|c͔ZK}?{' ^Кb&AaQ5)QQrMkL)bjU;mGdN7ccDVi0Dl ҭѶ{خy kC<{Eu^%vct;?"flyt6~ P^&4팬r&|,'s]ǎA&0L ֞.h61E[%myjj\] Mug{͌":q03Uߙ:i_ŦS_͵0~,+?ͪ-~hc߉Em2oBYE/v'NaJrh9KɨDjV%_)4`c3da9XiB[Ί/Njs PTR@6B@ / R=>aV d5X0bXn%&J՚5,l>E1dr@ %C1}!Ֆ[S6V (s.y-||`.JY8H$C/oп ܡ)cfєqm3"&ՎS[" _ v([*P~BQ"DT.ʤ~@"HyLZaR‰U]> /Wdz0s]Rts=/[Gen,K3\R] @C [&԰XWOIJͪb/c8^aXcR t )JT1ɦ(#oN yĴUhPC_4J^/Kk2ّ516([3 7_%{_5IRO%gJmK(:tѿ>l'Ňٜ,GYS>l!lhHuc1Sx<r2\LlڦQD[4٨µRx]ܽaү"!m+h\ ]Y[i?_x=pv1&ՒֽJfT ,z&eKZBRF1jfJ7UM`ҙv^Lcqm)'[X/0m ž1P@j9ddkϚ Ѱ_n3 ql6xvFVWQa)t.k̗g ښinwŠ4.QHl#SUAqQg(36?Ƞ0qpiNA 7 uli;&5-[l:&syi]uTOW7j9yPF oT* QɳA i}݊f]B=]ŴYL 1sQM h -aT7mDZJEGXQ| beJ` _78UWිFnwTF(*N3iSfrSmQ{b&Arv]:&ji1]~T͚uW]4_YS)|Bq[~!Ξ271%fAgw.@*w,]ˬ65p0ڸǗ0 -MܠϠ"3|Yӫ6(h)z6h m: )ȞxݒmЛ)P1MANS͏ Ǥ0SA:*03Qdֳ9e;nΒ:ɧ #OV06ŧ{Sk z-%A]@ W҈ :iGGQ'^* >TC~|mpY"%))jpY'Zs{ KP.H90@ `HH4=m}`P=MhjĔ vf:3#' Cb@}LB75rWvAk شH - "9\QYTF"^]>X xmG3kʫ!7Ѻߩ8@.a@rK#v򄅖HUOˇI^Zb!ǜAK`iv p9ڪ"*ޑL5:wUP]R"m a&T?_on.S͝dg0wJ˷- ZӆS~s 7\N|:)g3lGi?k$A6rbD^+F'-EUwۋӍ;Aoj)  Kᅱ{J"6-a~"1k$=|GWJ~q_i {{/I&ڧ:l^kqjLxlI;!Ӎ';F8Bh[x?uث6]YRV=h.f;Ñ*ݠ$|@# <4b|ѹpa+ @Ag%vDҴt!`zBbCg<+Wl1ESGR>k1QfW C nܕUհ,KQ:'LӏH9WpW}a+xSsYM+uq^4k9QuGX6dXޘ t !-@^ Hsc“o>/[='Hh]Lߎ5jNJ?#uV̅4,]"a:\L#q Pܯو8q{έvACyQDNvbؘ{,0zvU55pKɝt ܹ$V6;GͷKiV&As(#uTQ],͌/IC̻q|Y1` m1>#&~ŇiuOnX3 >D/ʙ"cd5DDn_['"vt(+(%`0j6;KblndoyMc}]k8Nœ~Km⒇?Fh{M$[Dh?m5QY+}4L1Sy@0\g<bgeI`Mm.+,vz(^tP@3,% zGEy5Lj&H$ܲl]~_)]7(%v\1S|"0Lӡb cO+StO'-_zjI_q+_z/,GMx~<4 YU4\*N5BZ}Hl 11OA*i,@VB07ÿ /}4bz fW4ƲV1HDoΰwͱa9o"7. q]VR_Bߧu s>%{Dc}jSS`@O*sMG#(Dž<5S{I,zhFnfe(XTE6Ux,QnM_aoXp,Oo}`&~_Kem=(sv\YIK8GjnC~8eZ4&Gژ!ҟsx4_jK"nե_p P{76VР[~3{!p1~q1\04qTy=GG) J˥ b|0)XEJLcC |]ڦDZQKrʆx݁j㫏;yκ3ÑYC҆JоG6 ̡+k]R۶ Wqc,=wQb2hά baKVx Lc|5SEs jWyh'6qZ)'rAJW9Wa7⛞ܿ8SkY<>vgE=ʈ߭^(tX̣wA1IFT/"Gs(?Mz.߶f rhi Я֒bî÷,A?7<]oA2\ Vupy|[T@k^1£^AWwKgW /m픴ؠ*ULW4DK(k{ҏ:;E >Jc+a{Od1rk0]ag87Jyn*u ķmƀӣl=B'G iwMp4l+" h̢Ac2wZBe+R*wd?MuK,@Y;R3e@(y䥟Uѱ兮kmC-'WoP>, [O==Pj [e"l TcGۣRM WGK4dw[7Qi"R+d݁ MXCY-p@f˵r]O0cQe+N]ј0& 0\Lkh[eMrf\di/tkxh<%w0ptH+/ юM@Tnx-@jYȃJ/"ѳqa[PpAXMBΒ7ldxA5 lD$̓}07^;a>ղۅ>ՋS@wYiaF}{V܄RxSA"^l`$dY7_< }w1j2ek*Z2bq$RCѩX$XT^ @?Zdo$B1Z<]FBt䩰' )ȨܠW)4BV+ċs,^#S_N_WR [AvQ=I2ǴRKNOY5"+s/S>`NТSO֤θO_&WQ|oQ]ƹ)p]17&\,M\esO eϼP3!M2xxȃZ8&4̊7˴Q뢢8n Q)!6 ?}cV~kVzwB-XdgD؜HLB)sB/*sBмb2ړO D2eLmrJCvn͙_WrJ RWCE'fq:/ Yt Ia/\ׇߍE!끝nyT[>u`Ck:vg%KW_qq7uܨCQv-vaނ7#* }q WhGA26G1`-|.{ bH`ZiDΙ<9=Z A.n6mԉ3[y*X-lSѮuZQkt;1_۟ AiÚ =zi뤽BskZixg1;GGs@Wk!M.棓RuHTm!D~ر;o,TqBŜM %0P{YhqD؉aNx\V](osY¬grrFk_1V"Qa[XwG!0]U-9pBCmr(ָ#yS0G[:8 BZǕW#A6ybfy5FK.T]zۗi3q E;9/k^!TԙD4-_ٓ$ۚ#kEJ:8"8w0))ٯ(F+ T%z^x7Ƭ$C"(7AӣfZ+EQ#(8P _6TFoz_(='u=w9&O\+ẅ;ߣQlXԡ¥Z HOTw k0ش#\uz׭.<^h5]JZ'8 3T  ,9,Wo\Gn k> rqDrE9JWx= #_(PXd!XsT.pLk!YNIyvq+Ef:wrO h ̋nS~7ZvEqŸN"Kg2Cl Pқź3a~ ?RFLnQ8!˲n*55[)a#ijZwpbb%;BC^UpDl?W'Rʗ[Y$% Jɟ=klGD{aV) n U : }.yP .\4[۸#_\FMh u(Wp oC~1? hjiՅp)<K UirH`(n,ufTLN:JFBMC]xW4}s\ʆf 1gkU QR]A` >qxu dޢeڜLG#X>hrlT.U5kkEꉐVXQ7K$ Ԇ޹ ;^L$BFm{SVCNnS(4c)-^L͎#Jo$E9Iyнư {ӺMnWc1`UzF.Ӑ%"Ѵ%WMT(ΔAP dA_8qR6aݣ[ r& ڈ&[ .P؝h1Ga|VNB녣CțG?԰K0n,MEJ͝ 1sY0_m'^ I,#n%``B+f\h˼̍J!u8rFWHW iHϗESYYV8 RpSM}V(]5q103^; FѕUDk~.|BsQt@6|8赘x2NR﬜yZtU|(u$6vN&jE#p.!?'^4=R9QcD k33\s-Qц_q z?ԣ<. 'V/ TPBoDFKym;dBܠQPl2ħ.s'4$}9%?Ap0(>l >`p) g'PJfyV5{NFX~ߩ;gP 6$ Ue[mOPp)eF}l(8ㆫ ^&Tl>Y=iO7›՜K|?EW͹#? 0a)s 5߭ 2e<&;ą`9#⁋^ˬCVIMz-%':HIЦTE'Vf OJPE [Tgf ӳ#=`Ǿ=GKG&(Ց|Ҩ@=ipyq⭐FvPީe[$b_DVT/& &[Ы>C/(`ڀ>DMU#̜U!, }wy-A|Zc$?V/ ~\j8CA6?M8H$lD{ .+NPʱOg.ڣ:X^{M7|QS-ү7R*QfҤ!(nR6JXjbh]E)`4dIo6eI %5ź%9 @ y{e6InBS*/V7vm};c?`ؿ 3,DCu(!r7L4 !hO#4 nHoĒt; E>JfPgT&,uAjD #K/WH7dRT:aW2KE2 0yMnBe+uP%U.ؔcE$F(LtZTkG!5zpJЈyʣ2@rQS`o3_ȋ+2!\"Gu LhWx۶^MI #M`bS[k\<&_ZCUKh7cLV"樤z-3lJLN뙢u FmVabpz`jJ!_7OU=V!`0-EkFgBbX9T3S0 &uHKU_Wj_ݩmI,2CXȶ!U@U@D?B)DVai~>6:f0' hV$Ex%:In“[h]ZLǭA iCENRî?Pt4Oρ$Vp'eOphсGzİQބWBQ* T Cgrp(-$+~(D0 ) es Q:i P;=oqɂSI٦&=٥Hk9(wn A7Ŋu}1uZ|W'I3aF:0TtPe~>!s3uRГY~VX2td W-uΨRDNiJiΙ6d}fm3rP͍ ָ|\+G6Ӣ"h rv81 7V옐 E}#= ×RŠ|-wEYO,G|X ź^S4A$^c{>NQ3r-X!-Bkt)DyI"w}v@p"t<>p$]2"iCb-8ruXD0 ҝꟅȫ3O`-Ή\CTX$(Ł`C-Wz̟I %4N%{*3^jlpM! &p.of3%KQN%;ZF~gԮƗZEJpgOyl1 $jk vJⱃx킭)ŒV % u yMmՁݑNPkmKQL䃭'a5BaDgŐ˹NJ@ C2 B%Nuۨx0t[Sgtʘ_IR1aHx% O todbx;I20xjPy?v|sUE* 2 zU˱h%`4Iex3ޣJk"[?#}* sK5J7.}]ɻ_1"j\C(`t+\QdS9EscIjyA)u.έF~IJ+ip-&7cYˠ>V#H߇Ecu} ߭5];̈́S^Ő ^ԴvyC9{;ؖzJEv*zd/F5m|`@) Oj( hv1goUk!A񋚐k"^v)E~EHN8kdG+*1Ό[OgoA,u- ЃY5X}7Y\8b[V"Jp|$;;-GTNvoz´:ϻE UQ^юڂ^L*N='\;$Gm qPSPXAZnҵdg{DbOO~ZApdfcQAJ׽dwQaA9疗(: ,Z$^=!\ >Hu%Io=M`kjvXFC?γRe}kHp=!(RZM0LDgYjlW҇[.@?ͧ*搉)肙CP-T<J \ A0ĭE[MC-rzPQ:MNGiF!^љP)թ`? 1 iX Q.Ach);C;mӗІ0ߢЀJ-MJV% ` E%d]-stⴗa@Z i>к%ddGk9{?tՀf$IFL5S:s;VFcDꑊg*q{-S'~-麐?!Ι?X[lyFes&G`I;{؄P4ۑk|g9{b,q˗Q@`XmMY&5%I~P.pHX/_>ȝ Tde5(G30S|Ƹؽ&G-ys PU/᠏$ns&7C#^LUvSgeUTò#D c>Ù^Pn8 e4/P `B&Ǒ[kZܙ9*#OY+)RI5A0@G [Hx g(؎e-V`̾5N$xh"'@ &^yqQ|d\+N2cVM *XdWjn<*8iDd,hF؅_3#TZ\W987"0=K֣|2*%ӾJe<^1ƿɨ4$Ӽwen嗙G+EVjnp30j= 7hJ97؝%:h+ ,uEygY렺"V"'"(=gLԔ020W3+)nnS)7ֹh(/#0A?H=(6aAV[2j6BalI H☃wxG[ֈI3Mfe.̪M[-  N; ҺxYUK,VNk rI" 5SmͶ2%n79ERE ']厎Ņ:p"5ɊPđ@fa}qQ|ctHDI(^ +1|]E* 52\ 4D4?'/Ϳnr\#<#r[Ɔ`w:_^q1|b涌lH rHJ1Nky!]L9: 5 $ f 5Sg:-u8V ^zKCK ,j2Y zvyM^m ,].RljnxhcWdiW n}?ϊ{)Py/RܖD8P8BZJ6V؋^B /:۴@h5zab;ݰK4CWBz"YbleMJ"+SV431 6_9IrxΡ ҢPG׳OY Ǒw Ľ* NKKbjs]G!9t5P3\ݮ;h,q1+q6mu̮G.h5jEfP YZ/ MZ%*؆mpgQ{D6 ~w4#Ǹgi贚ݕ?QްDR)9nsmUpbm[>*1jh|$u*Ը|t ƫtb\x/ 1sL :ʱ '3X0:_2p@mɱ3?S9y"T»>@l1 8= M`xwiȮ uV.z:[6*탡1X"Ę{E foa:Mku9+ipeR"]g@ Cojr_wh-^HәYn2+ݓ 'h;u v͐ݛEd 7[ 2))? T+P7"‡FZxzi8Ś&d op6jNcu!*[{H;md%D9E3[A]~#~/= *0B%{RL;r6D0˼;Rd:ЇoyZ._s_9g'Mjrp:b`mj60\/wEC:0F~X3B0|z *j ZVy}s3LJjC30?^{jr%'`9p2$ĆP 9Mo4WpլC)-SN{42kҏܽMhtibf;ΖKۿ-61"WwB5StPzriWs +ŸaHIA3ms3e Ůo FxIr m`w"=UG"1b ;ýxDnYM ػㄸ` (Ypٷ#Gf,wX5#q+sAl/g+LHv3\["q;(efX]z*?{{]P~ d=@EM ]=ց"(jMhbVۑi: #Va30x [F4s\-0\fQȱ.S+ԏ2[J˪-魶җw$L6pYm%W8fPG$ _BN F*QaĄ5n@TMl G=/V;w)C[%UQJ#>c- C\h`Зwk 6{.If8D>`]l)?;8#K3[J i=6R-&M>󈭀FޡҚ"ZS?к J0D2pF1[|CTq>}G=dSGhR~8A6B)n3R\=4:G&5iEȣ`PUɄ[LH ZrW\b`oNE*S%&EHjݿ\>Ԓ)ș̂BA+yWEl3;Jh!>+:PeB\wh0ksn-m*֐АrېM{=-6:+|/~Ǟ =tJvg{<h,n;JUd#wo1u ?}Рu]}ɍ~ND~0I(QwRDϚS%&,x L(8i@^e㲰iz<Z gٽ7K!m [ל\B9/$|0bz;%>x)x%F^k|Nj?=#,'!xc>FX( ڥZHȰoWYQkQ6Pb*wfbmHݥ6S Qim8ss\Q?$T0]Wj wm r Bt}H,\q&>f:M 86q#_7)I\IМ 9lEEc5/+lۈY 3D.zk?썛*P^kZ(d-]puFw&FW-POj)$vCC)&Ѭ!IA,dDZ R" ԰I%T񢡒bcXU5\58X&wp|r F%X'E$Z[|rab90lUD+B<@:8 ;HGY8.hA39&'&nqXZ"Mre1Pk4",UGfcOAK׆e]S&ԩ0ܛEiJ>;s:Lm0GS/n=ϥKi B!?SͿi]|:=S9Ez7N-_MT/eӫs)Uyea)[bΧ>9\Nz,yzN5ǯwi败ic| Lw &[9~ޛznu95Ӝ^;M=_c;v?OnjM1XsX]uy*OeS-#Dtb]ė0˲.t)=}&J){3DžxCM-—e]˒,rwr%w:2M׮j u:}(ɛǚyaN ")Xa:EF}]{{ =JePD"psQ UhnJkUS8^s\^6&.m R~^k;Ki}ϛKUv=T#ٕu+eYY^RNizZߊkR9f*9PnyNygܾn1 K\Vn+.RJA`(I_M=M/jHfBQX`LPnf *>*3}UozO¸Sgוf^߶UJGCOtN{]UߗyLͲ a7^g|^?gMpڑ%z&eSy#^XK{QҝY԰i]o5gj^_킱!ԣe1*e}hj[{ߎ| Sֵߵm5=)E;X [|!- B G(hDRki ' WptJ4AUli_%נNԉjٜ9eOOԞIJ;%rRېOrwk:gZdS]ab#ibd414S/-ė3s/:gӟu4/[Y4KC{l.[hzH@ %q qHρ9&Bi Q@\L8b1ȉH }aN; Yq黽9ŋKRHN\ "cSAc dҧOPw7E27y΍#* S%,)c>t8=pM5&ŬRL//pR@oi [ N25[śn KGpēLTc<x 'VD.$O $l%GLTlb?s.*{6IƪjkHZ̂^M˴5I%|+")NŶ{JdɪN_6&l|&K @ |)|kXZ[k3 8a =wa s*5:x:rč幜AXܨmڻ1_y3p=c:%!!qEecǨ( 7%uK9wEzNT%rwb@y?psTz@4~a-ajwE'Ɯǂ$j_KC9Zo戞 W#7JUkN ?wi' l(`I&p/ ŒUڒ\ ƕ{IY80" ޷wA{&Fˤ]۪}&]Ƞd%EF e A}WE> a3jWC'q$Nf.ƧgsɇB;kZ#8oګwnܲ"t74X t[2{кm'R4e i*H/z"vtVIO&J`$fJǣ3TxL ^y#H 5 4xʇ,ҚSw} vb'an({m|LϏ'!5#l9㗶4CLA<;DVKX8}b)sk(W3[ە[0evijvɏbñHQ`thL[{]IqbRsĠ$|:ĚRw@nQ14D)h!* 0)#pMm@=,/iagxa2F&ɕ/h\̡4vF^)3#OZ_Q ̨A4n\n?{V2O+/McPceX? 戈GԄy*많2 !$#krTމW~Hg'[ U߱?5Who1J{BV?+n>ձq?a0`nxk2^|}kV߹cHQҰE]H|"k)c,(+m0_dI_#֧6|PkݕCN2\rnaIZ=&/~) ޹ݜ~t}OҼ )@|Qgـ-~쭡aSo1qKFF|AQ. +)ےKVeRB c#j$}M+.]%';(p;MOAXydD_{>n+7Uմ[tby3VJ -6.=4-\8I/G"q m%վ qb#Ŗv2<]mSbz^#muTL&vy{>=U}sZy=Uy#j/_s'[B} EZd#k k-3j9 vc~YV\kg#?)TA=L4xsp$>Ur:aiĩ KIk,,7k,\ס>9URsQ t>&oYIpCč"Zm["4X A !ړmD]yQ)6y;mႥ+(#جώv X{ [ML!۷t+o).[#҉]R3>G_'RP`?(M[v[+8U#`JZM0g0qI."CNWi|]}Oױ-Ӡ7]I;ʐ+V֜𩆲3V*yQ~ +K8TlouzCye64 ">74pdVaESg-hga,fF4m|H+4`)+Kƀ,#yg0|ڰhKRմ;uH̑x#H̳3Hz%e.'dɘQGkmxG?`LoCFRX%>27lBbzNHեvP Z*¿Muٲ[zW0f&"Fa|#:|StEh݂*AwMH݅* g(az͵K+'hu*ƌȼ [04 8nPL˻NG8`I^~;A:`%Չ爋mt zUOM@ڹmySx/'*M,'8b$ GPLO7jhqX7KA]y>ɍ_‹82D =COyxU;CkTU5s o fl o`dV*ÓxybvhǼ?@ZBH  K<5AI4%p5bE_ 2 L;ϢZgvؓ.Um ]OL@ m m+PK("$tg_?ɣHSu}b>J; 66\I*FZl8Ќ*$'K~$W[|"X$5QDB[ ac%MI߸.d#OG'uqD7{ۋLu~gx|yRDҳx=M UDF&c8,4*Q SQ(Z~+& &yO4 iDYiD쉗[n&y8d`sUlty4lӄumcj9"tVbᮃ!erlbӻ[|p`;xQfhR氆[漦5#CCoGbޡQK-rvfhYjty|I5E(1ueck_*_ӆ3N dדņXΫWvYE +:* :NKo=Z<{g'.\&{N(mp_25aWHG2IyΒ.Y DIG޳`t#2P,jHq Ht_F1u~3A0jas?a~Cr]nqR .ϓs!"jwT*&_M{2IaTDN8,[ayArX! Z RǴ! }S0:˺FӨxzj"6ZЋ5²2W7Ps߄ʎ#7X6.>,RB܊XRN훋rwoc>_rGeNvMdzp4L5BN~J7+[Ru'ar dӪ˱" ujZy~[rJ}MOgA;uTӪ-`BKfmTo֪')@eIzB6u6eKY9Gx-X+>Djh`f@VېrЩm x64vA)<,j% Fn^('=qbevޞ5vJB'YKOߞ$X7ljQzkӟKrhPqbB]6#ז]ph#b-d2hY?9,GU͎񣬇8y nϸ^ KKphb,DP&R#av7lc]S^SZ4HP7Stp4]qPz8z$>+V=,sL(GIy5i%Ц o lehsl9a㩋j柅UZmLJmi kќݨ)s :ۑEKpBaE۴xڀ>XH.WYS<-pX ,3/:¯}I[2BH!*= _.z&HKn]5YF.Axډͼp-R A9yL^Nޓ25$_BdcﵒlOâmjJ6I;93cǫCcDq(/A^ЁF\@"s̡K?KOniA'C˩8l"wnX/b٨o wǍq5fdcO`S03NO.vr S?7Kf C:u|,ur4+|!r1Ral'#JЬ rb643daM!_Ld)ΘEћ5Ö[v1 B[/_NMDi!ų(VC,Ƽ= tޥ|fHvv+zi`ѰA5QHO-]QD  J- ;[c' 8~bx,@¨Y6XW{b P(4;8p`|SL+چjv]oԩk_=V.|B%*pt@lFC[5|v$H뤬xuzEƦyr![{_Vb[=pkF DB=A0Fy ÆmĊpz"O2A)R\%#~8Zay | |Ww9o@#Bl#ayZ'׮9ؔxn ďPܝB/! $х3zEa^ͦp +#[خ Cg<ؑ4z&2>s0WEC?:5ꀫm{{`̆1"): Di#wZ4N3,\э Tu'ZHɇ&.v-ƱWeŔjKfX 1'.3^ hd Ai,pxW_?(o{'M9ًyYb>F=_mU Շ3Q?_(OJL,4҉[RTsؓVfC͝K(p[n`.Wu,?`lQ DBO1փE @/v<a6-@N:IΎW3s[܀  }/툒*3-*1 xj" C3vfKM.m1||{h8_OqK;<,-҆5kxvF=qe.sXw׍/ԝƁpP_t^, e5EmzÈ͸Ȏb"C>δ2 D'6-s%HwfJ]@Hl4Z>]P;geG.JÚ"L:K|+6 l( |v)1RWQݟ>O*C*G@W ikX{ 4MIG9³jDU5 :wӠjټ,Du%VnP8FLe 6UBuX#qc/lR~4~mel>F<6_+H^vj x'? eTmF[;/v1Q>0Xu p2ř[b uĻDjr?RNGG0[qu'c4_`Uv0`K2 P}~âT=:i+  BF|P.w*ЮuPrpU|yd F}ɣYu{Un  eFIp~|1yCF2jR NgSb.ׁ kCDUh3- ҠP~ƭ&qCrB..b 1 tת)(Jt Ε18hl2@iO%6Z*^gŘulx@ //r7L6QkB8#gw{^+A"ͱ1rIuo̱O$0uZlK在7nAZѾ->G!2i=Br|!CA 5?=@!v D}x/ɕ: o(򇾝J |v~P؆lxm¹ HWOoV$W:^uƠ0lfElC cޭ {D$PweJojO :-κLk8et ^_#E1/Fm=?1h?&NPAA)(R83~Ok*=!,޼RRr7<zOW0k4Npy/BC/)}DSs7Q% 7e1 o-[„P"&{!M%o~wuGf"IX5醤,UIs00\tc5]R=ߺl+4 8U&(F/_4k(M ![z|#!t_ǛqT2΂ D.7=IV`l5!g,ji`=!WY&a O A@?JBgH,1& E2ؚ $XIDV껕}v1tF=*MZ=#F\z\lO)@A9 η(cϗ" 6|Ø΅kY @ cg"eL~q!oE4qC|TMT :K NT6jZ ȀjH[%kڭ @QkȺv::'F$Aƨ3Tb(/jAlwd&}j&KXV7O:'qFipteSp'5ė5!~P5 O孿A%O:6ձˁs҂~BDeDߨ޻&5t kGGs΃1y%*R  ^VJK(<EeoU7 _3βk NFF?m9AVp]p7e!/ sx Xy2ޡ/#@8A ;^wYlypV"ן^疌J%^ [S %ӴiL=33$caWs =)U/L fueؚp-u]fT)be2 G,>eb3$ iX؇;|4ZζvvOYy݋hhWiC(81`bgD%)Mc]ϫN&ieIڷ>$a(u2}Bg(ۼoͫ;bʷ3M-Tz-x XNDy׸4>.+ #NCeTlυ?OB3WyN`#SJk9H#'-mO R46G3}kraTᗼbL:h&|mi&/SwG>z^ FP b,|<$i_@\#Ta}taN2T=*C? AN#ޫ;MtE-L F#s i ;qx)8D[2R q$q \m$HA<  wbMݷS+lqcʥAoB}!%[w.H[l(S>sqJlIP~{pDaKugI`di*;d!) 3FƼGH`[pWe+,Ɨ&=؈8Cb@A_@|&{u/sOO3H26rd6=|vs ƁC7++!BOqEQ4_) |PtAkԾS=^ C)tI:T b0pݯ^Eb>kᷖ _hӎ ʝZϭ3LPMo6CRyRGʰ2ˏܛSMByDt|@\9Ijېvį`midZfUi@/;c-LJ_ Co?&Tپz[P/vHf!EUMXzK0Q>:Qdj SYj @׉\%=<U`@ʯʈŴt4nyȭB+۾YԊyy3ӮQo6tI_z-C^|3%xr~bk6xX)S-qqjҾ(MeRNjb}Ub #։FI: AۇGg#y)ҵmWs%7}5z$-8 yy#XgoMV;6H6(щtBf*pi"$x3 (aԸ.ޮpnaRJO?BNj1]_=iN,2l$Nf xYYu:4791!ȴ8~$~3%*N:UKd ylP0!+0 ˘LRKJfcXolHPLL~y$\X86y$~rmZRW+%]MU6 \ENpwd }rX#䜻 \ *@iNFD`7?2c„rx_&Q0}ĥ/7"HD+uUy,N*+~r~<0߉;3 "oR:^O]j1Ka(xD(8X= }*xH>m\Tν*[{ DU23>-Ƞ҃G3(G|c,)t)WjbYIlҥsePi`bxVu%8DJ s)pV~lH@lwmE\]G$5y:WZrSs| Ä m0p!Kv  o 3d/@> [cin$Ir&p ) d~z:@:u,VoqIdF܁ E$YN'ˉ38Q\xGHe6PU("9pcKSBR YfAfK[@De5“b96=l XbώY<|4og*i8>\ڛ#ס:r3A(]V9̰2 S psd*[TD_-Bx69jRژV4̳SidO#>@"RȆS'bݟTWZϝ81U$ Vݯ3FFX6dZ ~\2heBc#x")G?wFRZczl8cQx 0FXiPye& ~HI\D]fwX-1#g*OD~X1 H_o')ڹ^_ %u 񽺔  4Mx RipNC(6$]xRpBάYh&K]"V I@ 8ylh  +[wLY^Ǒ`0\ ?XG`GT6εvQ-hz{F0Z;ցO!ՌYY&S[,"ŒOl7SθPLI7u|FSOV(}(cK^+\^+}CA8&(Hs2+Na!Bt#@OHW׊[jda?n=T|;nд% %0-ѝ']Ǚ7lASh P.r;\JJ}ksF/<"$gE ?erϡ16glA L*b 쌾x*ڮP dx|&PF$'vPܯXj*9mީ)=$Cec .onu{y?>o@"j Ϭ{SN!*Yˏt_o6Uq(oK+uz&/&ϾoB ,Z;`vWqpVRESNTFe/jFl%$uaJ0)9H.7e>ɱS&dq.1&b"5gTum9eM !I_c7qţG=P`TEt5T 0wb&n8  CzK[V--)< tIG X-ۻjkjc\XmA8{ Mh$kLoj,|E^*QN8 sj;`  /C°DHxHS ]&xEԟMFmsam ~:j_T6&Gm\%HTh~K!P~I[pKDՁn22dq\e,E8#@Aj9ny(8z{#YJW6|G$[׫ks!o,j"Tm.6{(^6174[oD,j~b67rea||Prirʐ&7QWDOj&4%` oxu a؝$VpmxI՟Y+=uqe^} 3+*Y1PZ pQuT BI[&x Rh/o690D˪7K1)/7ZdzT|Lg0c<:F&'Ty?4_S{ 8Xe}N3˲<AD#3v.2VF*+.X]`ARh9k}`D0.i*qrf FJ5XdpܒTS fHuӯhMMg[0P zih2hֵ5j|'{IJ"w * q³}ͅubY&chTZ?E*;3`tXp`Y\nu3. v/2MhǑT 8ҸU{aX p  ڥ??s[+8t6GtAc&h 'Dո7ZIYECtXFD~VZ<>+^ ) ;j|gMZHg*Rd=Q;I 2]/hXc NsFİ!y@jG'O/`WUSFQIZx8>*:_ya;ܡ\# ߕ&6k49d=w17@M2+R]/T^3YEQ&JDo<¤ʢˣ!@"B׳ <a@ A}U^?F 0"8)&D(GTN:@Wsvɦb0*kUԮ AyI4Pɉ9 9{1p!!{?(Ji2c ?*TPIk^.pjb&y0"izEH;J/0M%CO_CdD4P&QbY XDl)3*R'ęSW;3/۟h:3|)2<x+x8LyW@Ktl$btarX:jl껖V% 0iz!űjM>.ےBE6 /&exa({9UC=%cU\T`xx}мfXJ#11z65۾LsH@h&R5, 9H%I'#t >`Z}-Kzyi/[z#E&SZ#<U_ :: V49$p"(&:H; %hraukw)+\s_i$ BB&35ߧQ]x0QojZ zбAgmTwȉ+@b._kZ?s:n-Zk`!\,R:LЮ܋>~ F=I oE${PٳZX2;|ڀ/'D*0̧b:W$,* h}E{ (6ftJ L&(G_q2'6@׿DCdZ0bz{Uqܡor1,c7ހ *k_0٘x\Nʀo LvdWtgPU61l!ѿ3x _c?TxX% f\b? "*" e$LY]0A].GE3оDf-آ7dr߲RgUy uxCv|_䚗hwZ^6a@']/7Hdm@=&<i٫c DZ r\ `&d(8:;l%o? kz.*ZIt ~ىF(|A$Hg$<8CI O[VD|0}{s>%tATH\{'W.;S`0eR=|z] ^@O^(&m:}E!3?}oG03P߀ߎۺG5b70Z@x9l d`&AV܀sZ=@e0 ty-t ]3ПwۏY}f{h 9MC+lK@)S<"l0-?e?0ʙB{RV̆1 S+W`|$SatyCFg HirHymffƁ8Mh _(ghà1:8`MH*-+*a*I8c`B 9󎗅>w`vU4ժIܕ5B =|j!&7b^$a*dkLAn!H;OOuቖ|Z1 vi\NY*.>[*v4nU^tTcelFQam39,:jde`<3b4;ۂm; `"9Sw\J-Cp ֊8h-%eݞ>u>l2v>^S0A&<Ō4T"ȳxVvvnT^ȎiF+jdزl."o5v Q2rtW$7͚8{vD?uZ9ـi \&riV 0FϤ'&SOk[ yC kum1aJ`4tQ94w+Pye@ rKipZ鱁 .J.JJߧvYx71,W,̬X#bEo<. VhYY _#1o7 wvG\ggߋ%uaIMԚ';LRq$yE^ȺmiC^:y0{K.I.FWe HHwSORlZbQ'y'cK 8sMΑH~%1nϼu0[5;3Hԑ!;'ƜY$(IK$id>Ъ^1E Oe՝#&*K &VeJh=` L:(IPeoQ5.W&ڤªU=,VY*JVTFJ5aQIfQʮ ,FU0KèbE ,4b=PXk4kªhe#,SX4 Y˄صYZQ0. `ab6*JXM",.d+T.[aTqeYYox$ pO'u " 6o!|N܇0>{Hd꛹Nȷyܻ9Jf2~: ߶d+ؐ78Xq5F̨߮*?Ng!R&1H G:vHYUC͝(.|槱%ߪ 8Y|N5Qc|d$EMʪ1̟ NjviR\K5Fr5ԟY5JLEAXUfU\REXʲ$Jb*E*rYafeEiUXU#I!72x= BoNU\F9 q  VP?;S}LW揺gtx;BUVU̖v+_gbۙ; C Gmv: F}M 3;P#r]a7 ܑs辷K: OyGv$r/=uuՋK e\-¨te$@c@$\.y'TAĊCS‰)` aA8` !8hFѹ0Y|9L{GoԸpS\RwIҷ`\H!LYR[3w*xޭIxm7Lߨϙa]oiD|Dè3<{0? 0k#pz*\$&OZ$8 g љ8'j_UgP(+O+D!ˍR/QHgP!}M72?R#+Jݸu7b@bpBC)=eZ I " ڙqO?G;4ɛ[-MS%Y}ts.u Z(8{1G;n'k"J~|#+3a;fT;RF*}Glv=d5#嚪*}1'; B|۱ 9PbU.r|fbtNDg iY[k@HY"Âw~-^@bU[w)].Xn߳̍l?o'rZZџHW1$'9! yE֯Yl p\E[2':0e-~ag2xR64FwiBPGqܟY—Pʊ"h| wNr~m!d/ MxBhoq)h3~wU]T,aH4q7(9ߗ08;F#x#Ȯ X"d^Yީa&R_\B1ae*`p&WXQ m ʮ&nt_?IBHyp=[|S9wj"Yxd"jL_r.!Bcs)B9Ҁ<_\]fXafIO +TI}#29x'l\uݑ^( Ҕ Jf4t#4[zĒm.o@`sᜤ)wH>ƂӮ"sʈnXu y6YWjjqϷ_ k>&c/+n)aSehΟk]qao *3O$JAbM), 32Z̉lƢc^O˺qyv73OB &@ ]^)N.Ϩ!ÆAӡι 5Iy]+߯S8 HT6q\;9A?]G&O<:NJs+f-FUWmF/PVyg]4k|v(7C98Vn&"ᔀ3Uex͝VQbO[r- =D' y\iЫa"3#aB`P>ye鬉ǵ5v5^:9 QRT˲NX΋Bv̹[$͉srܩ">p# >֏8b$ŝ-'ˠ{SѸDcg5tTRQEMR!(*0>PU -*rJ"}[/I⹢3$lfÂL3 s&U2JK{A֕@]i=˹q HhDnA#:F03Ee1̡Zψv\:^Vs'k[V1doS)-EQ"nqH M"U7vP&gSgc: y zh-U#"cz"7zksDDjOFcw놫= _3$|/Fi׭8m[cZ> i8VT/˘υ=G lɨ~">AmFoѥ J:fxhDvKNR8ԉDq(p-ӥu+έt1Gn¶pu}@&+LV`6!񴑨^%DP~!rcATn\"ȣMD.JCZ6\Ums:] DZ~$>.$j+P-u_¼!phv;YpR1B~ i'oXN5`_W,Es>Bȷ'x&e (j|tB#xA14X^;L_L(:dmѧϨxi2Bޕϼ#`[2ψpk O#:[rqm-4%2m>Wޢ/.e*3kH>=6kno-ah-`дh\.[ sqf >6" QKggX{ hO2R]/K2"ElhO'I<ӏH|or)%,6y/* [cj C.lr5ԎkS!rVJ,gA㆐  z11RSub[Ӗ;B )+%uGW<r~>#,c/HmHecTb3]Rh,B^q94/v`hK#T-#12Be*F7 APkVސXgh&!`GQoIWț'e5h=sGDgt{ݞHV {7tD *Ty L]}&2THL4 vK^ѱ=8fˉϋ)f06b[}:+p4& |I>H_旆l~-g0"-!,@-s7VK+EڲYt@'^_NW6DRxGYd4{a韫>QPۦs)0~J¡V d th)=ذ dlL눔[PYa!gGqϥ tٸ> Bjx#F$ˋX .ߥQ7ہ[ݴ,О=D9=y0kx!trEl*$h9gV./,?ɋy\inaF}\zBdZo|01,ฌY!'XLoR]i3;1ZB׭{$ټmuۮ"@"t#AS?}k`_ 2HLD`jC I)*`O3^`!M`D.[*#M<|">_|/~#WȅƋU9sR֒*W*L2f0Z:|kh vbQ5YW wirS zxʝ-)PY\F_8KOAk ?5X".~qhOWPcԅE7aah$J%`U7>h{.=ml ǽ z4X-tQ{,lDˁVO+`CeS2 j ݄ks;"L!)T]5%옳 Dl˔Wl)RkPaMraX}JCAE*~xpL 6 c }&;v flVuNJ:jqa8lIoI;.TEzuCKiZt:4o7\{n9 fNFB&9_ZWŜ \_ nE}I1f#pF#?i, =XNՂrVJ]R)F0BM[U[}>+70&ˤsOoR:eED1Ч-q_h䙷)Ftf?ASw.zPJVP߀B>/Wqq &惼 Nu-ٱM0c-q?u9+#`^;IDqj3m&=W![1W҈ҲԘ .{F JP;!"x}A-{7_3"fNY]60|.!}p Mb!M=B۶Ϫi1 M~1{c{]|wM8ך@+Jvq7EӴxi{uBjJr3Z4ͨ'ܪٱPB*%O= VOBe04RзJ7\X\k?E*?@zδ6ų\(x_[Q_IzaVL&/]<>Q;%:j&-]֣,c1=\DPVb3S~oź]r91s opx z~kB⏞ 4٧_37Т4 =l%K*9 dM'p~ipb#_"ΊYT[Q5!L%k}XL2)=ZE%l%nMgb|U\ u[(O7vl.]\9؆5< 9R1 @Y- Ov9XM;~TWd]LVA^g}()#5)#UBF؃L%:0RVғ6[ȅ"(K|&}: Lf <eTIJ! . L%NL$us&4rҘۢ DʠψG%ngˡFx-Ob 2?(~uLAo =BD)-(<@Ƶ,S|2Zm'S:e];,/_/_nNz@Ly]?y.1~$=?@R a3$C$;SsC)H{B\v1jwW:s:n %b$j4JNܰHo"toƂK!H"˪l^*ʇ17Sv*ؓ8 ޹I\nN蝈ýo7%F'L?I']v`{) Gnr_Ym6t?$\idC4bj 9FڛlsՂꠀR6}(9^ 735ɟă˽/Bˎ8)VrPLHDKUSRM¬gDt5\C<ɵ*EF˒G(}0˟NV9%a.kA d_w㵡ޓ+A; T/\ۄK*.@J\k^f &΂A'ԩYpu&j [ΟA26o.`T[Y9:%x(4.K\.ůl~ݝbPG wc)}XoS'u9n)KIѸ) %˵o و0Q65/]x43;U.iۓO61Eԣc_Irީl|W<@v?RؼMBc5wkӍɳ=ʂKt|B(Pu:]&[5 m!}$UIb *$ fBzzw괦^[\x~8?fTKSԊ KFb>1/g _\KAȜM[7*v|m֯KL^Yx)}n~;!៑]S"N[t6A~1%n3QsuN. bHdj&* 0t /ٝB*O+6RAɇp΋JX*d{:rB_;`3x xL\{(i(~#iTH>wiGSaS-aQKJo}l8+ɩ83y 3ahY'!1q4ztoA^݌\I1ħ[%gk ҉] /)^cY4=y.VI'ɮ uƏi 8wf NTHY<܊ nz89i| aMע a$`4- alb$ʯdɮcx%E|He%F,׈p*e;d` 71,ܣ GhϦ)ʁ0 Ao=]CW^?.M**a^\upt-Dn@? =;׆9O40]3C*e6@  opv &SNp4;ҡxjK(%eNR;6ݠH>IAB&*% dqN%i?CWcg""R1`ZHiPYL88dh.ퟄM+[_nKH0*j[c .uhw (aDbEg"6(2 uQṂD3xI +[ U\[Axx[lG)7DS֑ȱ<ӝrtGxI>09%E}w\Or\A%I9H]/~ ɤi ac}xd W4$с+?t-cF #~ĉ?ɦNot! 31I|G4As 2%{ >% 8 =s9n_i:9rΌ;P+vxpl/* Kv0!mpdS_w<%"?;AفDTS0z(Dner:Aɖd$ :)ۀ;xdJQFl #x8! ߙM_Igq,yvGop.cT8L7 "EI8{_ډq% Ă5Qa@*FDsQSoQ_ܺ7v jHVM{3)uY1L-†]hQ UH2s/!\}XjQI6 DFQq@1'nSQzZQsA蘇)Gb,Ve,sy4ҫ7l|bv[oC,t[Ao;" WoosLCY >qҪo'o8F2"Ym!FJR"֗#+mɦʲyi;oy \#wg zs͞N ыKp K"%q]L0R 4,>[b@?,·tQ,O1M亀!iN҄~[l~0P[[ٕ¡Yl dk)]qN^NJ0,K9h%nv ]dՙ>RD׸lAyNFn{Ԅ&hbt~t0E#'8^͡v+d%Gp^!plO5 +#(rz*BѶ 5Ǧ'4JxOxk(RPH瀆57n;EZR-걡|oA' 3C1t<ה  :pGSpGE *mdNZ ^C\u ('լ9^LOfRϢ\bkq"ԐE[Mj@eQjI{ۤMV̂bo`#qJ d*J&w{f 9ڀ qQkw{$xsVAwʾqCZ^W93&\4x8.bjCL&5HtP=T,~\Piǚ4u=BɃ*edv-)塧F׏ֽM& ,ΆFOcR>i=a (P+w4F2`Ջq ]R2qmB RLj@5 E 5eI 'X둺g A6?̅Pܟ  d @(N 2GsbB4 '=ܤ`Cx0̈́ ND b?\{~$nOBJa+"MJ>"ΥT7?2cFڄu@eDddJ$fMD 26MPnP)Em|w˶ճF<Á%+!t穮|q+UYD.p%TFc*7}K韗|_Wēni `e"ڠ%ݒ4RO$.všImeTLJ<8{А4/CEWH`!x@y켶/`!5j ~PĿWi"B @JfWlL*rb!ۃMp.>TIĜ^5Upd姍 rb&o n믶zczH&.D{xTʝI,}c7;nqUc'ލ-^W[Qꉾؚ2nş-JFӠ;(I zE ;k|Y cw.w.Ʌ_#H0%k㐱^= Mw~5̥3w5Rv틩YڍKQUg+ [Q#J7eDm'wN8|сu\$STWvժ<78莨5YM5恀xLyʡ`n^┓Wz!78w>69ǟP<Yh7˲D[[ J/]˶|m`T0$&NCNؓGvu 灪3;.q4JxK}ھZf6&,V )c8Tnԡ(^<9%r(d7ć 5~A[W7 \'NrڏU^#Ce }_( 9n a2L w #<|1Ii g҄رPv*qUԔMO M p=Rb/AjNr@Lkg#+`B%hUǝhb0Ճ@GKp.-rxmbo4RGnxxѠcnq~ϭǬA%v ,g®Czkc𸷯cj[ʢ|Cj/ID%9X>ClHI\:5sq!2i~u89^d+&(N#ɔ!` H~pNFXa!.PضA<ГVy>DNycxi@ ϖv5/ذQ\WxVCk[-mwĈp$y K."Ρlu((ݽQOs"N0TKTsЏ%aB v"c4$k1hk1lxk TƯBOdz(@ayxeg!I PÉLgj׳>D4tb;WnRFec}peQ'^"SIgܩi$3;if9**2RGh ^6 o5']jjIG駱,IX 6&O~ZR2`AjE\jo{.X-FW~;ջZD#~XP`(iܡ~ĸX,_\hڥłZ =,XaUI]z3~ }~8k"xqZ-`5Xmz3N~ԅN/.\軮ukVFT~^@ v, ' ICOkr>_t83( nA668Qv3fAA 4Cכ_NjL_:{Wx>~VGH@)r~"S#B[zjJY }Yux 9f' !v+fN$iٕGƔeو忎M% FfI(pܞoMWZ4BRǕԚfi\;֑˓'bm`l8j-UǕ<3,r/5҈@`gry:9#u'aը Φۜt.9:qkixl؝ ɯ]hE,Z- 'yZZ``QKؤ r^-kA My%wg Zl xժy+])˛e&k6j| 0%s0ׂk POA"Nwy>7 jEO5|eu g\Y'%nl,T0s&2=^6ke,;ڞTw 夷A~2dwYI/;"Sl{ֆ鞦lm։YkOCxTAwm{-2e\r pU Ҭڠ5 b( nз(N DntjcPͻݰ)&j@WHK'N613X6 kAz"gH<W:fCRbDE\:ο{)V A`R |MZ1 )\5>b%aHpdL/8fa i-i Mk -(x:'~²@v7EXQwu.J@ǎӚM}OX"do655EXwˇsdMN562>@½}i_qzC !(eeJQka`- X>q:MpLH[v?וFO-lnTIfnr]$D+N3J$iC~ y6u{0Yd08t`Z%qQ T;TSr%toY+|*i3߂b܊}3L4= '&V9zmSЈIPIلJcr~xT/A^4Z!K&" U[ݩ߹}^ >d+DE)U$,aY&&zsܼ/m6Lrif{(huV”!tDBL& @c"'CNG /@pmF]fփ4OŹKy0 :B"k%*n7Gin&~?"a  )CCRN\?NdvwmV}p?;!'[kr)DM +۔ "yI͙P~VN`zKlE ^YcӖ+i9C[I p`9g6낐eB 4hFBVN('L2Kr)ybqvυz% )^׶ 6F #>%JGDp Yn!$^&RM9'߼ÆbOJb;ؒk-Ǻjbg-p(;tLvӢY_/;}zir5O2: /Zu4%8n4"R8a8-?gdVgwAkhf{N>hf9s>Sנͷ pBz?#^k۹|IG ! K<>Cr,}hoO6l%uwb%A'6T. QKPM)A#n'ϱ)Y>\erO|`Җ);%|Agk/ZTL<*}k׌G< KD`692 8) 8p<$85>S8]cc'["I{&OaJ 99bU9:|Ls# 5LJ>Od$ VW$r,H-!fHLAzG>`hp(_;5)ix?:^ANB)%Umoӊ̭V!1̣ͯ!t!LYS3 dWְ?} Um($ jhƨ{8K%WЉxu%>ӧnz%YROʠI7q.Aenr^ _4*nQ5mP7.бvʣ7 .rNH%+ 6?XW{ KDjj!hlhwUyp$(QȈgsD31<ܨ7s&* ڵTsv̼ƙs 5)\RPia/07X-0r>S'nct=-[v|lNȺ b,p}Ilzi6=J8$-7IX6{]TPj^om,% f{q-s/ 3q+ &(Pb _8dv?,ڈcnwմEv9y8dw&Cov0 Nu`;)˛%TX9~)yt?7;扥ԊXl_v`Ktt._=bx?q{pH/BaOQlkvLAT`Z覯߉XfAem C+J$n%jJGpF(Qٮ Uk[R8-5j<4ԟr-6՟kfu7߅Lap+ҳPC}֬;cpmm˘އN6.Lih.& ?Jtሱt na2BwOgt[綡&5rYEQ(b2֥ ٵa5V?:6?J5;sJڗLd@bdbL[pj?GiY>(5VŊ2hzaoyi.NjW CP4ƤgO>x RS%`̦G葛8:__̓ٺi6/2SGmc6DŽ=Mmq%7UJopZقDh2 9,[ >A#0-M栭*=L&eЙo5TL 4Q7 r xLylمc!r0D/U],7̪C9qWQYHӖ{\0 "9r`аܾk4mkwM]2"r x"A,X5 rĢ7~}m{HW6J[T8鎹: Av ك6t*,,mb]굫? ^+M~hg&rTg=2E;li;H7艔;{Nw%yS5r|)enz]}"f8-{UnwаQH䮛do#$Q < 0:x۟#/{6s`lݷ%|WxԂΩ=lFL$FQLe-,=iHLJ_ҐUĢ,h 5K/E=SƢfϳe0y11e2^7Z#ݺ=_'qFt|Thi:݃&ի.:j\O)ޱcZ{ X`xXoҡpGtZցdK;=Yπ&ya'ӵ]zZqE{/1G,wx+*x4@EGT&'T`MvjXZ1HZ?d-{Ҧiw1Hs$=EH^_Rl-QFqklviZ0wR(49\V[9!39y v}[oT=R8ҿMzU*SD|ݧ\ ˖8ԟ hv?@ynI&Ί8jVJOۢwR0k=AFO#[y&|Wj f乾ppeIمZYV6 sGʏě9ƍs= LocN4X*HwhQiۡ_.Q$R^1tW!\+e`L}ߣ?\ʒgS/Ar_@hw0t=Fk+!5c8Q lxW$.K!IPJdi".N8&o(P ${ +^1/C@U [aڣ@gR!O9V+caFK$<Bq$ʐ$?_҉/L}֫_ʏ[qpĘ,Be)|~%sӼBgU % ң |xQqoBC|) !mB ѬI7c[c C'!ô#<*vK Œ%6Qpq~A*SHz|W !RU6@tL&*GR+cE/#:bF`k%.tެ Aं傧<[Uk&*brKp-v" @F Sc )nQxά""Ģ$ɢI9_( ":KM_S.鮝Ox)%–x2?&!_VT2^v8<KFM[Oa4AS/nD@j(Cxԟ. ñ:Rj^U``0rg7/ǏeNAU uVTN%A%[@G넀4hA9QAT"~ۮx3"۔\J9AyBYRoٻ0;lR]3$#I. lE:M"|5S@\,xt^G`Ј'D4P~ ]xaDɆ9TZO/s@0< =@mY+8LI~l=6\'HSDFsO|'c&mәП$VaH؀_,tV :><)5F6|' @ۊh%mEjb m;xV,7~|gZa4ASʠOtiyxv#/g:(jpSNµ3-|Y/9;sbm"C0 n\{D+O6ؤe1L:=,V#as2GlƩ3VThOOW--w5j.tLnK5 l ?׽H~R)H.V0sa5ĩ/-{2 +^ aƌ)=ɵ#*`+065zd1P  h뙿͸t AWړXRü^-'-q'Zߪ,Nڐx{e5DT.9OƠMs4b 1w'Z/LM5ҋ=@̈́$NԒ~dKF~5" D|6Q D{"R ni36'"o M!4LXhQQuK`y {z$Q >>e6/W۝,.E*#WKDzaDIbqi\_=*ۮMWŴb! y-hz0z}#"swytJ8r9g}kiY Rb [>;'ϟu O%+N.mtW-舵 jS@Nc!^e}HF؍HUY_ JkN\uR$x:[I\H.Lj`(4LAϭ4L DjahvQ#pNٹocDkdp\`O|=X92iBzw+)h,]_OBlؗPNmhW?47:: G\gWz-ݥc,6Q''x#ʅ<&!A36aq6 %_y c^Ņ"\HKt 3uPK2$Nm metadata.json{"conda_pkg_format_version": 2}PK2$N,ğ00!info-mock-2.0.0-py37_1000.tar.zstPK2$N䍭Y(( opkg-mock-2.0.0-py37_1000.tar.zstPK2$Nm չmetadata.jsonPKconda-package-handling-2.3.0/tests/data/mock-2.0.0-py37_1000.tar.bz2000066400000000000000000003201201463012743000240540ustar00rootroot00000000000000BZh91AY&SYtL_m_>gڮWǏ@z*m@{| ǖۀ4C@dJ w;h<` ݺ$WE|톲{g5k8JW݃n +Nk>ռwy{ EsoO/y}xǽfh{ttCwﯵ}oM{=w{ȷmS޲{}o;{}Av]s]-pgۻltZWۺEpu^9u˭wᛵݷ7NAuhBpwwzwtXףûMǨE)7{y54C骭_OwOM<^P5(!*׷przj:x/a:Ft4:smր֛;m}}oy@/k@׷urmgyuk/JuJ<p;+DmSǽg k|W}O:{mJ:tbǦt::U2xD9l)x6vn+1X3iG]cUj{m^r3#z&wQtW}=GtWӣf0Lmi^x*/B ?H n]:oi0MJң .nx1,.E1' e%xe{f!nb]ct%+J<\)hJ䉄,ުoEE֪),Uʚ»*˰yI86<0@C+\&Uںםy)kKdw K=):=ub5U>V:ae|2,O+=]VXc)ž3&z^7u~F]9; z3;Kn}rq, @u^__{+KrnX!5ܫɬlhQ^r{KjL'Mr&w0K\w|uۣH~fqmT-6 yu~l?2>j<$k%zl/vCgi|ن;D F18}=k}ˌXe|xnC&/,,q|("! E"Bü<`uȉ$ʇniDeT@pFRߥ]u@v/YuōwBHMN P ~5 +-RY|}=YX4=2qZGE™$-V>P턝okfiI;]8  6\v 'QV_=ľ0FaFNVCa0M"QvȬ~wܢV1QEiTR{ !hFLy7ñLIh+/9A (^w0(pKYk=:xld˞dbu~=N8RKա02E~j03`Fz\@n/vh7b[&~v9?kKle3f x݃ExwƓ}l|Ƌj}k2(]_%֟c(H21쫇PB? 琂3HS(}C0@WP)lt YxLh8Xw&Htq{[)foʫ8* KW'.?q !!ŝHe4&N~zCVw0~DHąL?K}٧Q_ TS2/hcP2 F9|pd,c;g_DDP WR0fDō8^mST<ϬSdc6W􈦚]+4Zn>t(̈p?σPX2jfkB|S%_mv< Ae2d.Svgrۙ| '+vh. ce& b^k: }aBlCync#10\w4)`Q#21؝[86t5GgǖEU@rz^zؐe͡!|Hcgf*ܘɤDoW)̈#ZaUXɉ-Sw E m\RxĩBozxL{V`Me.(jqܴeTW_RΜqާ'JG6@&<Z"4 = VQnrc -ϲm,.|KIg8nwo/4Hn/\> '(,$pC'lӈZ EȺG֑At0|ޙRK]0dAsY3_?۪Xz8n{m/cq@q^5]hZSxp5$}ktZ玻η!JA%6Oh}1֤ )k6'T vGȸc:bdMfA AHޟ=|U rN+ߪ)yH'yp a#IwHry.2ʼnM%@O ?g0RvVzDU>Kx wa9%w>rP-e"(i])f&>@u}Fv|ԡ6`@GFju Ji5c7:NvE}wd9 swhV| 2060jG.9P|?_SU|_3㷐Olzr1x9\g#?zms]  _/=2{ҌIlj8;Wh>IoN κ9*! LɜqOn7CSɑ&}STzuaY^JBRBK,AezDlT )n[cgZi7= չݠ::&_e|t=<bջ_Z m&|5"[sv CR<wZe.ێ;}ǽ TUfDUdzԛIX6'XV櫶X0E{Y1?Gk羪3af@ ٴ,' m!1ǣ^}<X*@vUIljF; ~5cY 1ju_D=^ pO_DATU,5v Eۉ8֍ [oqǑ4 @WONEI4UpNl 'K{~ 4=Zmͮgh} < Na(_}Dt8c7Q\NcoouH+eW_r}8D_,YN%8b|kQyTTҝ{a=}gS~O毧e.J5={mn}';?o\1sҧ>-웟V~Z{Jvaá̇0f`̌FYsR`̈Ԃ0g e_6舭}S|;OVSg'@#u;K?w}_ӹX "sx 0 ) ^?[׬" ~bi+zh#@75;-Rbd (m*YV]$@3(;@YzdI P\= OJ$(H (sDE$K?oG"8LqfMenC.4|5beHmxٳ2tZ^.p5S=&5YE+PY+X髊j$l/𚸅E1+#͔8qUUQD]YX"ZRbL H C,3ڜ.l[Svl;OP\~I-gDjyՒ!s|pcՓzF˼HSj̍Е7,&dDzyeYs S:>fo6{激Q'iW}fr$K?{h]b:+d>郀Ń?qڪ+]WcREw;~.C S 6oy~^vxjK[d0.doXHq'Jx=bt<'tt/Ϸx! o;_/.6sYݜs}S7t>8䊓dKA/)ڐ,7wkf#BIv24;+ :12>ƻȬ$k9ľ<ȜT0&D'0Qyw_b ?Sػ|,pbw7U Vj"jcZ [,Q!Gf=T{q*~O\gjҊ@T훖kC0S=BBd. AquH:g.ɟq@0qCrC#{S]o|)/C%;C;$X+~{3LkSŞJ ?`{ؽGIf?7h^Kxɶt‹wyi+#\-mhIu&[&*Q.2+EG̵5cI̸5Y9ܫSjR轢u$1kPt뒻շ8ں"ESwc;̊8շkE\cU~]Ǹ-E- 1\\oxzsߖ=8N {QQ,NLe@v_‡',1r4Jޜ3aNbt|fNɅ"%xm9 of?f--⧨nn /Q,OQ6p]v6Vercc_)zb3Mx+^MP X4!rruQ6.hxi2*hs~d̫].Qx5U/1u՗m}7*Z((QtZZDq! b2_kmSX9SƷعerŭ  6+ Q_iĒ\@ATV02'Md&ҹB ejr U~.MʊHP}v" Y{~Yq,M40+ZiƊ4՚HT0ejIB F[u?؃tHh?iR0O3mģ^E+Ma|ᇼ{YoD@x9={:DOuU9U(UPF?Fσmn!B" "QTD P*SAIB J R R#H P,J4B!@SB &e(DJJ4TJ4 PE-P JRRLERP4D)LK+Tҡ.Рd#0DAJJ"0H4 P%#H*P!H#DJU(*+DL*A#@4$5-%AC4) SBM JSHJ%P41P4-- HH"P" $HRУP@% ̢P2UR%3R5MD4U,M*P4# *RR!BH4#E*R%R҄J1 #J,¤B"ĀP-A̡ %P,-"4!Hд(UR H44#E@%B+@H$J%!CBHHID DBDQ@R@ *-(!@RP(R 4 ґTII@āTP@P$IB1U+J4"P-*3 ұE)@PB4 !BJ 4 T-I+AJҌU51D@QKRQCD҉Id,>洆-64jL5Ӷ.=F15urhʎ-ɲ9fq2-kT6{30֕E8CU53ueD3<IS'LY*uM& %qydɔM0l+76FX +wnlй7!a1H{5E%.аi:) V5 ͊!TJ!iexTS܊s.B%BkNw [! $0UM[[ӺIȚ2* ,HS4!Z&5Q:baLk9m . $M<H9N`@]W5uYP'< 6z;ލ4w)2k.4VҲ$|4!ͲVC7\6Q8BPdMH TN*iFdHBof"J(5(d)@RJ TҀRR%1-%RУJ(P H* "P+BHVn Od%UCޮ?̉T& ,J ?"T!B*RP AW(=4 NeFB(@CDʅE"{#T%IhUJM3BJQ  fDBdhb!Q ^އwn F cMU> RPzqW3NbG7Q=(@׀, )m#G %W_,B0! JH(!Jdg؂ubKɜ`2b yWA(]0:#خǜh|2hu*)Q7*nS)cHjZU2SGe58ٓ :JV#&~D 3~W!Qh,$2`]4'YBV`adH$d)80M2NR_A7|~3mv°U0b(0_YtӬDDJ1V1-UQLZ0"iDe&b(EnT0BRx͚6!̪b!qM; #s9lhX2LLLښ(Xř 3LQQa5V8I=J DmUUUVz:3sk)':ҏ `exHCW E,/Q^lWMDd9(SxrK^L;<`$I2chM6 %4BËrTAi6VXRx mfj*V2-јe4&`4BhK&T)D4a%7A$ NH1 }DȏmJ7J!N$4)p`66g\Xi/:GR@RR*!@(4T@B jAB H Ҡ!!DM "(H PC!P/.ILQ`$s-&KmJR1(DQ2 DBLP{w'躥 T%5f*]eu:"eE d1569a(ǣ}||;[$ 'F:$*>%K W@Vap8^Ĵ5 0Oݓg9a\'3C!@e+81~IW]}WkP.͛Lao3C 1 TR$PY3. 9@PE'$+/[P0\Qv);\xw {~(>pa"L 2US^1`j &T)t@Y! YA$"5 cSjMcL#-h EpJLe -FԨc! dϤ=wY;GnKr\Q68F^٭KfAYx6f7{rΛc];SPD;|A(/cI DM? ? CRPT"XH:L$G6KɎjjz8> 9Tc&0/  u^N)mYY*k95ϷKrstҘ;m~'/a3ՙmo*DbZ0%W&sK( iÇPD̙RpF>} rdB wO spe5QGflh&B/aeLL<jQ>/RrǑ⿹1y0@qu#WA|ŗ/Ww5j!W6tq+G-W:Gur[-8c%x}*{z~5|6#~loBZwEVO*wL7:^V~ux<;[7ܾnێyu!z<6G@G^gힳ އ?k̺PD@X=wX"i  ^(Xr@ʪ".D̸\e%Y'GeUCB6&PS-"$czd,w(r'aCZ66:I1ӦcT +!* 5&ŗ3Fg3wGQӤCe1يs֣ m˓u.FJ7Cx8Vuݖe2yDءB#FҬm GN\L@"z]d>~ҟOgd/dѐjؘ)!˒HPh Tu*%PUjf!R 54uL`[. `cɿΩCPdw8CL44*iNNB L15]h,X`H,YFm"PF5*"\Jnr@յG޴wE[Gէ}L6ۉ#sal#W['Vd@ƈiMhWVp/܏5jaLIGmSV&"ce5n'È.A)c6fq*hԚc@BP$Q]};IdR2dSeH(Rn &udH ,*C" ɐQ UJ"VD` Ǣa?.L8Tȿyfq7K:qRh@ZSw5bĚiHBdžߓ5dٷWV׉ڈ6sdTTc>rwab[f#f98mޛ9F0xTqz2NBI,4flMAIbSxݘgFpX {)j'gDvC#KrL%o+gbeiKmn,g)Rm;UI mys Ze4b +9UN%8.i\[즌+x!ZamX9JT"eHuF6RS*ICU͑n;^zM*i'EO7śZeLKj["a9D9#>5/aaM<8lUá}Dkw1[y єL̻i՘ߣq2)Us8S084$b0$ ӡ (fg 0To-\7m )8&\̓iW;q#3ygQ*&vիpb"2|0(.B8`OB&,lЌɬUβ736jjv5򑓎q\ʫGM|MoM]b@HmT8{q4 91+7&#mg˩Ӹ-&bXdbE!4.&* bV:&NyVȹy0E$Ȧ#yfXb NB3U[Bwgk:$$WLv[VYJ0ZObUk2+PwV{('8W}aƵv}'hx)6[JdXF+q:,摬MK3vR"#Lm%1UJrPf12T8훡ӫB5pLa4 )L-ucO*qnUVfTd!C7[-h}J~XJާW2STDDK^(,imdXڕQbgsW5DbmMAK$/kBa x˖/4uc8~W<8{y1;~W3 :?]ue'[A $ͳg8o t 7:tY7 kb6v-qKFH>dDLqi vl/n)<kJmN-j0C 4ː()49Fϙ'4/LnL]0M0Si %&Z) jpӑ6A0P*kWDϣ@ql{C(A{J'[F+m=IuF6;Aol|T_]8yN5)@Y^3V]tW`<T6=i*q&K%I I= gKz$۶'e' `am &r:tjىgP'fRߕSiÖ@LV Ɣ+Q҉ܬbvgPBuz$4ː~lbEjtP&$.hfP7gJPbM*b(fXE8' 2aĐG == Grg68Q$b=!˿ɥc2'ֱA1|%>}ZX[U9{>T0-p>;}ʍb(u$^zĥvPtޗ[Qüv+yfp){^1oo&+K{o_4*5Sw+ZޖWuFs[oUnCpȇ{1{ĸ LrbX RQڒ1L);#QNeېU@-7)^mLZ,1xzL$Xq3{tnl,CֲFdh+8&6>N%B>l.;+H9(z80](+U^rM'ָM=fec)v3ZaRL)Xۥ!x`q dN_FeΎӹI89e@Аp)V,|TβP#:C)ӵ>eP:M:6O7knXc/NWN Yc@A!lJ@qHDde5;28(rR7t5 *ǬJm3W<.%: ꙂζIOl' vsh<Bya&(""HbE" c s U3Z{N0q4T@Yr [yKB2,EDEK0u7E 0U< au7WS3q$&cǝF` zƚwivsi],f8Qwth4 ;:,rJ)J\YCc"F0X "dF3KhX A)+f\)-c]h#6ҝ;: >t*ak.b#Ռ%W:q]7/sN${2!mȌF<r&}gCf|n6&**ۏ@Ns:gpGF\lȆU$2_BHr2P0Z2C (PieU RшJFƙ &AQ$냬I'XͤUA'M{Җ0 2O]d+})e{T{z'wJX/᳈*TNWa7``osUg8}3=kgY3}Kw]罾.4Vp/yTmcYidvRk'mc9t)}9X:*廒mɌ$ ]+$vuJIuy{O8.Tgy޶1z7/}q#rt<3wOؑ\qɣ]A h8`tdC=]XJFG9 jI!`fN5 MPT_,0ȳlmiz]ڋz2[4N_4=~><^@ @e@ F]T;߁5 լ2W'ބH9wfISO8b&78pZ+Lpnzyˇ(+[KUjG%Cv}+fA1=BI|}\3cՋf Ga-!4psvB~Glb1pE(p%E&ZF:HO+A)cUix-2GPpːUqrx%ȇ+ 'aTqK۬5IeF9/:W/dZDR0ʇGV*K+GgTitMq_ia8j\GK055&CZṦ skhHim4 9ގk D|ΛF#']X;EB ! 5 n:@Yt͌:&shbfV1R5S f;o StōQl9 0(vlgMAJӅU-tȸ1@پI0bbi V"f{'v| 4+D,Y!J7lʰC2jxx+dzx/l[ ;̫QH=fs|wNQwS!nv4wWt}in1 !Mmdeb PH KPD'9>ưzњ(pЯNyk1&Um\au-`76 =hZ#Հiw ,H5>>zv@gQDQ#1tQ@0H}Y\_{x?k^XcC}4p/?pV<=c~^i58{4;&om?/%S}ZvGxWC?ƼA' ~ R(5 %?nI-"JYj*F Dkj,uZsF""D3B5)ajml{SSZ?J.^K?԰%(eS9 -R8%hhJŠJ)I{-`.{ª?sH\UW(eWL8ڹUV1z,p/Կ(oOs9zs=M72~~US14uӿwbh$]Z--3f -r> ?F Q/7ӊ5alRĦـ,Bfr`AC vtlĀ["D IR:r8Ñ&$sehڑcTOYeiգe7gt<q/lɝj jZ*b C'F*l6u deR fKae,DRL %ȚxarV^\!=bduJ% J{)NCJ8ׄ>ᦺ.iCLEeN4^5[ Yvd6Nl5)cP$SHx%KV݆ed8Qzh5jqZK CQ)/Ga:jjSZ1$p[K"Iu*?TTT$~md((?Wg4* F =W^oËY9W0?71osq:fg*ly,];!FdTճp'$u#4>0/?| (PR,QeatQAQ4p?CF!QT?J'JJQOgZT1SX"`ʵ AcOۂ)}X~uT iY3jw~5XǠ*}φ8fSy4H:RfV:[=t?I.(GhOс23޿6hx¥~ wg,FI<00U$fCBDLvAcF3:b;4q2Y `8r>2]M" y…S-kİ݂m6DCA @+@Bz4)kNELS.wt^/aCZYG)I7MқpL@G~AHpvgx !"FhU eL"xqÛ2tNm%Hh )-=.BII@9(e T@!w_ryeuO=d㲼SÖ:yOT(yĠ MwX]zɫ00t.nb66J R(.g z7> ߜE鲀"*Q!';<"i4 4Zg"#9v:ViKoS" Ԉ $p'1 ̡=#f@JrQ!r"0SIky[[DGV [*'s3`,+C'Q D=HHI2,d@w)sCӗ7_}|X7uU5u]K<.O@~c?D&6ό}7_-O~rJ J4(` " 7~(%`XO|}Y+{ +nz5U[#61]he ,N%EArTKH0 URh-DT0Fa41҂t!"KRn?"^*0 4цz>8]=^"FRnF%je8aF1wWc$2"K,<$$C¬U\68 f%XƤ]y% Ul|a!-! -k`^?U/({CHY# HD2"# 4X#{I__b%׷UP|LZrLO-^,8fbF]GHx/ü >rQHQc 4abKDсeDsaki)mѴXaD-D.Z*%F)'|oL䆭"0&XޥEŒ,y8,41ZfV T `bbt5dXc 25WF 7EL($ 44I2 `Bѯ >*$:mkt&/)3k?6CK$ј.݄g}Aͳخ02/N1TDǮ_°:";W& hpIˆqF" P#npr;q̳89`$1t󖢂&b2LE.eIl'#-]C2RCP G[ ?<'d}N{z=} )\7BxwpJҌ X,+()h*IPM -SJSRPEjգYEQbE_lPPLh x!\TeJ`A@YA1QVCYO HyXz|?r~Oys/ˏ_Orop>}zl# qZfK:zMIiCb+η+c)&:ȶ:bdm,Dz_ J ௑|ʐ )(PRTWa)꤂EϤhJп;>upJo`3tY/XumW]4p\MtCLM.Ns"GzFvwub&";0=SwȅUAD{`^P/rE߁Գ<1G9azs]=y`4peu< l!ڔCR+b1%$iow3\ԻO1rLv_"j2EpdRng9=^Fވ#}G7ּ5/Dg ܞ ]:#K)_ZĠ/t,~ϗ7IǑ^D#85Ldn(>_[I9)KtDqY&KmC.8[<~ZCI v9c/q/GkbnIEUhc%dwyBI:j[׮I 4Vw73L?Zjy1{7Fbv|ޫMNn.d!/*S3ў$C 6C"*ж*!S>#]`D,d1}R wNKϓGgwO}t|tQ,3 >r3v֐Gg?+2E()[|=$Bl["24;h~cO]3u6xNbn;@d!@:|#cuQ/B}OK6tK@Dz4 ?iQM^X^ _[=CwXv! p>js:]Wxi61Qknӱ{@y %nC 0=xj)-u%MG* 0EyLA`pH1*d>;=L}d@ⱞ(tUSΫjTҧ3,'Jd8UDrNUd8L"* grpCmӧ_-D}Ze]ey(I ؁6ߏ̵G'>'Da#zH;!7ԙd V6wqLރ`cG9ml^#ݗ_ņwQ.nH/傃σ6fxͲuF8A|g8aB]|7(P@Lt$Z`MO6H3$`rPϬb&!RNB1h^ףZ{zBbZүaB4F|[ꀽ2nxXȼs ]r=vC1g\3RvI.lo"/)Eoi-UM=F4&0oV0֩F&6]2Υ{zWcT"Qv LBH6hFCd1׶Ye> F|>B_]g=͙:Lvdd)o^Az0(CKvb'])}5U> 벢–V)4FnjN830P4S)_ MHB+ }[0ЧQD wƶ,! C8Akgsm<x$'gGV" Ixh0e,ƗkYi:3ibF襆pm!8G |pnvX3 7fGBR$gn$Bl fb%G̈́u?سn=U(O@!u-OK ؄$qB:\Tmiixq\x$% / (ƮI޿Ssߗ /t;n Z^_5r[}}k6𾌨tXrb(})"EuIu1P|5Ko{$~[݇414ZzSul.eO iظ; Ńxjr_6[RoLsflB0D.\6v^:B]x }!sXb2m[N[;ou(S6TEx[Kc| dgn6r!3w=f :]]OwB.TȡUcQ\U:*ͽ~y2L乼O3K tzt>l*]_ڷq81~F"e^w݄3_ :gti xɇSP 2IF,:=>᫊qit 5=raHϹzo$h!Cq{xS cb룺0U,dQ5AeP TQ!rOx^Caȉ*}1!_ͳ?ȟq1EH`2?VLhu!TPz+D\t-"7)XRYdvbdPPT̰ 11-p, ,1(Mr(PDX2U\lW `mF*MN}%c_jVNv$12dvD.Ghh[,1|x?HO/5 S6TB.$=uA⥐' SDI%0ц/i/xIgpƸ~yIX aW9q.GE#q_:g$uqX;ʛC'3g|Id_yն1IUᲺ3م QOlt0̚, cQe! SV{'pC=EYP=dP=G߄4,dowZ &&hdY`QDACMH(6<2$dQzB<И)"3f c2"NETtx'ufܪwO 7ԶR;.X\Mxn[=W BQ@r^0 tCX ?7%[g lX^?R>:M#(ўH7Igٰxx[P:<#b%=]L9H밀ÐxQzf70㛛dIp|":t (3 Id)*=Ƃ YT ZJ) ( .`ï4>Eg PT%M"ɦ̘vp}"22ݿlP^'𿯎gFy:X OYQT}l~1)zsa'JBpB!.TU3?o>J w5;'};DJ#'ō/R6=]&}#K1 ϶QUFp0l BqK{I'tgW}9_9ud/ `cQH"ШXEL(/P8(v@;/W'L˯-% Z<0&CvhŅ/ JoD.!"B'8iQ E$ASBr\Jc=^w8 AT;(fĉN[H9 C# Zm_Cv0@&$nca~!=PQohgB-1~&UgKxh質LCPdJd$ו]6V[겮dHh'ڈ'>!8cUµ!|o;yXGb6{f\9trWYUjΏ  [8E X&!}h w{ї<' Jih(kPUIlCWy͈܈OE GbH XYNʅOޘXwgE:9#Vu}߅hbʃ| hy"@B@{@y!`pݣCZ ^7T LtRtB2d#CInc4sW[x쟪8^"+5Mtp2W sA'ꍣdkb4T x"RLREb @c8(Ѝÿ,qNfm"Zt2m43*)H3 v)fP[J 2fmJWFb%% !Bq]%&D@`OTk3;1A6J*;BDM)E(q"J@B)0?x|1 нc coHmāW?z)K3Ѕ U%iJoSL 2h( -)&HE0k%PleBH=.:o"rBH[!+@"zm( 7$, c 4r$ѬB ˀ0^TqI\ DV2x, 6mB 8Hbh:&É݂g&ERRh &0 IU*Ît SG)Q(╮Sb̗9c &68 r $L&)'ĝ1a+-g^-."4-vA^Pq+)?cu`)|$ ٮU1KRAܑi߄g)~_eϧTUNc)X:)),*ܘC*(涏9{z}{r;I>b!C ,O32#! Ĩ21J=m8gvvf/$m\n7㷖eb hhbč#!3ϸ N0!Sʩ}#ҪسÁ~ g 7J:rgjgI[iZgD)MK,k4:LElzopfܹ1Ӵر-?{C)eǍt:5!"w ~ tIMH*=k ~= ȉ~T2.ά\-͡F56vgzRu]Qτ;wt.q}0#wkwzlG㗥hi6>ǎ Gמ^G9Zq4ǤdT-I/I~ =r\/mڶ1: ؚؤ5[n^Wj +EbѪ1^04˟nqܧCc [A"F*X[ɑPz߬,Drւi nƎgUN Gv iX석K Voz<κGAqX.@D8iI i6!P0N_mN4vXNbD>cؚbj 1~521=$'I8t.-OK9 9PQF 3ƓmQCQc%EmQ-֞NÛ3 ,FP9grG5Fc,팘x3#4CѓvʠC7ۮ[dC8(ذUܳ8/h:8$en6 d'b(+8$= nAS:w2g͏E.!X!H1>Eb R),';s`!I:f6lPl~122C܈Pr5/ڨv >"*7i7 7aƃCրKG:\:M <c2*2 b\2D̢JTkl!&f v-7bN km#Y`Tψf_GCH9+u!%EsU=8%wliCba~Yo Dbxv.ӪCx7`}|!)dE& 6᭏2^Ĉʗ9$G#YYz 胱;m"w[8Û67[s"790@pX袌S.:2ww+ȷd]d64p`\g t!~A]lpͱr{rޮ6oz#uEw!0=T`o&Q$ғ*l-=,A"V$oV_|@Hat/S~zv]Ng J]Ôhv6ezt,=v|=+Dw6~OަIn1|z"WY h=]fl wyY/\  M} 1GБb((sI 77J9WQ닯XG61[TqxP {NhpzQlCDc.Y:\z#0'=kώ8g| Z)V;Ҹa _\42+qYLٙab톡i㶛$ۈqkW|˹X66su2Ad2YP`\ Ku8ѤC,C97C4y̐㰸VM^JTZK\ '̙c$9F7^1EREuA- //<~Jq<=0HI oc}_, LbƒFL`v IV+HpH-flBn\ {ui T;),3:hMHbrwc*O ցna0z#OAk4zKjרљ~ ESI`H&VeKyV]~er4c Kl?Kg|#aB"aO F`='&% k@ $8J¢(qZENwX)},pψeGg2+Ѳ$W 6aȗDc4UOSc4 E%~&bG bރ1pLaV@ ttah(a]l='߁ 44")(wZ; ,KJ k,[&a|X5D^&-\7_[9ĖJNqxcQ<__~r?A}NOZeNWHY9Igkm 2k+5&x| K*aGj>R;t$C3P>(H@K=9,غӘw*W c#q);sq:8p6D ɂ5իşwrYbۉVԽt#Phͪi7>n'F>w+!vLxX25"8UDC 0儼4Ap=ژ 1fC 2$HL7{ng_OxX8^CL޸8<$6}OV៧7\uIP޵MJĞ=~=8:atYQ9J}TS͙<-=xFj:qdY"Rc0࢏NI0` |aY(A@Կ)vhc0ZtΟ0IkbNܙ ,=hc$x02i޿ b.y:Y|0;7M:}DA_)L8he,2| VPY?Q!DRE1PET/}Ûnp6_dg! tuOA]@TK02`b `f$ hb;XT G|s5k#@JQ6 =RR3)fg|5 /9/.h2(nMQ],- |s|}sɄϷ鬑I"\|DO)aR=mVzw >{'Ht;>M`Ĭa|졤o|}WҲv9 BB~?s nx9UVuλirH/#U˺Rц6ڂIC_J zȠ=BH>c9- JQSÏS~ʱni3f/кٛVg֙cJ>TRU'\4ՉGfHx(5+LҪ|9@j'ǜBgF28r v6?/ ^ߥr0 Lݽr療ՍcŴ!:(I&w&׽ j@a~_ " Ɲuzyu[wS0 AQ:kKx&:fӶ x-b Dti+RQ2qȂ# {ieV5V8JV0c :5,"VC, #Wva.+6!kL[a%u$~Fi`ͳ"zg٥*-CyĀ64(%ɪ S^3P_.:g7xSV ?~i З fDj2ǿ xG = vö]S "K rh7t(w}O:/-frc`A+k߯KcC^NROllprp =n{L9|q.y3tnqiՑ:cs>A $2֫Xkv"ˮWZ2fs`BYMf3Md̄ $/J]OJJWI]zachkSvetIY_|yPC{8ҫgJ;;֫/G5Ee&iVnbNxgd_}w:{4Dz^ltF$"(~x;d³g(Svmr޸ IW$¹|X35Нt.Î\n2F]IG\0qu׵d"0" enL{ZDCP²C 1)[[SVN1 6\9 Ea:YbGcw)z 7Gw}10Niۼ"4|͕YIߛܺދѴ'MlNg㸃>#ׂ;b &Dp$r%&nmԝ.w z*k !O~`M=z:;fYEELRM(M.bXwUAcro>;ʢLnY̒Ѹ6T嵛$k,Z%*̢ $dBs<[az Ѐzf(ꔾ8MfwGd]v"A};׮ ԥem8ɣ\Kfu+wm|Y}"]9>|\/"P9=t 9HgDn>W=RAϮmurx:IEP\ːwʙeL)yfL!.2! bj8B#5ǯūݩozKNKn& 1^ɦޙBZ4էn7dsǏꏌˊKׁx̎} D$ؔ-~~WM7niQ}2LN@gع*"|PI G`ЎfXyY!ꐸa:.u@Uz|"}lкʄDv޺2WhA=_l싙Pd=7XRZSw82}N#3) J-A/Q=+"1TMUtB`!ERUn(0Q Qb9Ӫb70ᄢ_w`wO7)̢Qqnmǟ2/ P=TB|M^= z8m<[!+#Lp=uF&bk藚{H8,Jl.ݚ^zG(ZTVy̚=vx_iXIOG_!veyj/K {=߂6o`Yd=mB@jb(F\ʋb @хZ{lDc'2aӞ%aPii?;鬖+^RMYEWNR Α֫j tD8:;1\ܠioͲxخQK '* !5-T;q;5v:TݿoMNߩ֏ Ukǃ1ˇcko8wzˠX>gx}[Ԙ0=ϯks,/>M[ 3Dʝi20s1d`w}0|۳`t+9ɫK]a|5}zwQkB\y_]':wR rl~;6go^^m'f9m>KyǏ>Ƿ{qlV7>Q"\Y|wN#Ǝ Džc<5xuA:=\KӪ-D˴ݖT|cyisQY *_ti:۫r1RGpn&Un1c2%Ŗ~*%F_4t3<:GQ=6'_m]k}HCn&=U.N .zM~̜g{#Kc'c 9 4hq#>o\[=>qkRCnW~f_i^0cY]}ubVWIaf9N!F'Hfk*XR.kg-.xUU͚aL)v\# N{txMml;K%qynn$z2pWL Ιr<5}Ͻ3BVeJ8{ûU~^3' 6\X~^$LuZ\<6Wϗ.$ۨ3!k‐x]N#oݡ*_Gors.H<|[7z/]8wnP/jnb^Ru dxY5Zx%twm1-;s!sE/G27qoM :pgY)\Q LهDZcT(aB*b©zl:`a`d0gՉ7D>,trTĬ`;Ec=eI7Gfi^*W|5O=Rxzeguy薍%hE(=qvyUV=FHj 왕D͹i٨Li ɽĎo熎9* 2Lá[xў~L ?'9ϩNTM혽{2AU5rC O [Juu{Uaq󔄙%ÍdYexc˩o|;Lϳ/~lsn1Z=kǷS--4RuV(k"]Cɓ ؄EPjnpnq %[h3w/LK6i7]dO?WgA_{*J+C1g;Ix,઄ A'z/nNWsqM)m4 {:Džm)]aħؘ|7m&5HRhvQHf6g(#f0h cXOt84jUe/ +;Ύ{|:7C!JY)?C};} -Z貤Sr@LV Sc 4 DCk#VݫDԶz5o54'y˃6RJzgYǫnאqݱ:@O?0w ׂCJ1ulczdynI~//m2BQTD D;g HͶ%:Fmȋr)LQC,ʕ،[:0+'-jNRFƑY;^mR[&mwO}Aj|_ OIh7ʩ ˝s&a6A=#GpGBkVikB^݄*bD>Bad$=0vH/o;3hר:.)"n܉rC[3OӞ=05(o1D̂EG k# % 3ZUb0ZEADs[$ҡO,) i4#!#&g*RGc\YHtϺF[黿'kTڛށH3-DkW{;LH; 3QၝuLH &MZE>~;=W_iDRD^m1?r-8 arG?A%{f0ŁLy6[ 䮔:_*Ɇw`/f-̙`[ll8l!(]¼<,a_YZ28zv#qs=n#n<.zv)=UB143]}G`Q8I5YK6ITKph ry(rYGPdo9nI9xt2v.%OmF" Ą 3IR!B- \1 , y7U~Xm# 32+y? f_jiVQ'<Y4E~Sw]QXCe#zG:fHkC}{#٤z%A\\ΎQ {N"4B0P>|WoiJP[dětI>7 xmڞf Ջ{5ghf9P蚵p&XqP,S0e4m>.jLLhx lMi"jK8xyoK=:^!3Cͥ-J*j ,MvUHlcK1k!hlDY4rAmj4SYXH$X#Ԯ!@7 9Tr0Z U-sX@^MZ4\7wz-KuJ1G7\&h.DX0t`ão$L "E̦&8dX/q'Z% |&9&#{޶l r0$BѴ׮1Ur-+4|aT)tvsFbk= ߛXĤN9h!˓Io~b]ܝ5|1 77& ,E MP[uf%(1ƐӶmNsczb(b*esc' F{O6ZZi$7` jDa ,%74)ֲ"`x S} 4E\`TQ"3hYDlǗ ӹ ,KZ&i"+h-QJ*['G$>ňiA;)-/6 %A($b5W~m) VNe|rytJX1/,P|i| PVkRYNtU.RASq[/crpFR$͚>ӍyC4h(()iiF@HDRBu%a w4!肍 `p0`AaFQРc2AJr UOVEvmKxAcWЌ4vkۻ8T2w |zzW|XB""&B\.$뱁s fT "ll*2 wgC?u_޿y8" ɔwVt,|r8HygB(:/Ia ~ tv:6(RrNxLOJ?LF޿[8:Wϗ&J {_7~lgbiEӠaW ]GgayHk_zG@Fe '!a-XN< o߫aM\ߪLjo_y:K8!y>kW'< ~}=1lk?8дZ6ygwgo6d8_5kg' ϐ峈WH.Btc~\B0ß]+1~5\VLY1;mn~yԓh"Lct2r/L]:sֵnцztr2;kqC3vùQ]?Z0竁d*\e^a0O<~%-+PE֠jL19(#gKuxsoR&Il'%zٚRv9JA!isa/!Awq ;o[^O tPҪfmo,Wk)Vny^#^V|xjߝPi/-`޶ڏ鷮/e4 c 4F MuRNZ_B7?("HK,xW-_W`㪎0OIRߏ Yf̬Wl@Vy@!ib`U$WDߗ~Evχ;~Y4/fr:ug%TFDBR|B`r'"n4$/mū6QY"ʯנ4_#}v5{e`<~7Ex|JZ>XjXXOדy]c? H/,9oxs0~AWat~m}yιFqjٳI)7d;P:PʂF=W ~FcGm;Kb7:c|6gl2{ndӶ3?03 }plZ;i2)s{ɾp')ݦ0f'V_fvX$#Pp#lQԖwOKLҴWP6J #ɵC{{NaLk`>t e w~gl~7x*JV티.xCv[^)caA@i8qg,cGp>m97 b !p<||{>>lZj75{>\=qr?׭Ft+gKg~ϼ?:#3/S0]:Ӷl]83^33anw;Z'}qMk.nw=nc?7r0jLbem/'gE=h||w˳upR8]aU;da#x9gEUy{~9:+m}MtXK~_k6ۢXo+}F|wqZn6˃>{ |ߖi:mu|6--7"Wz{ٍo|Lw6fZ;7wgr;zYӮ1߭p[yw4-mcgp&L1{svGgj.|/V]]w[yſ5vvF-đplUirå5;H s`u@<9# m$d؀HeY_LFNoDɚ7}?]2hgN] 8Bk=FyE˼l"AWu׶B\?T@ (f/d XSܳU%SKz'3^s1taɳ|TKϱ:qtx?SJE։'RcEzS"Y"cfۼ囼rJ-ݧU^Аvyc#;`޸Ȇn`ɰW\gG:Wktz^)B:)iFLT"w5!ac~w0n#T\*U̯M*Y1<5t s' ~jpD^i( In2`ۤ/3`\YaBjpDIz QL2QHvCx%Eqc+M9كg7LEIos}(;3퓪(3 "GK@6KYVwA?/:`zm:vrwW5<]΅<d e0X&gjB L:wc"q)SY,Lx٩q;Az_hZxЙk}Q O JPz;z^\x-Ch4(4!wTķ=#hLP)⣵Scq0&m`vܝڂ2y M@+r^${-!Yi;$&wîP`D#St/帬4CZ>31 ͊cP Om;n>bsTӰx#n20w>wJ1**deyV`4ƝoeSgbX؈ȋ URC؃h=EuGt k}b[=Bŕ^S4GZEʥYPၼ͛+,ze7j-s,\7,JW#0fi!nuMX`.KIʑSAX@6 ReI+0qy ] mZdY! H<oʂłތs!~QXYWyE &^nixgjHfq =AP$hm*o# T@xqb`SCĪ&dB]!"!6FfѠdd%tTK54"y,G *|s 4 iw{ pEiV8#s[ >ϳQCb,#x$~8:x/ Pk+_~A;mWpLA0*G_{#(lD eyBy0딃 K#t:|=mbQ"YTWHHaDCG̬_FDӪHXWg|&"P70MdT؁DH1Yg4mj!u)/0AmcTX|X lL`'''~]SGd9?M䙶cȝpV)~3\iմx? !e(Cg$&*SLXP ]mV<\E dZ@ B0QgUo/}%ܥB'a񚏚H } ㉂)F@`)9f#dϐpQYD0h6fpS |WM'`c.A6*~TD"iJ;Q)&e8$Ҫdi p!4-H P6kLC62jƒ~j ]Q;S@]@P ]s B})黉eh sH4%)}yb]%?OB8X dg޼'g$`v2:ShH``HW]CtG)ZouM`eh$CQ;Aą0*E+PF+F $<\O@!`R//3F3O{t |? ZE? I3go4?HA1~ PH4@}t^m϶)t`: j=&IԞHPH0d)꣏}\|Li gBg=]}|5δwh媵XYkV [%ơiS aY/ "apRgaV/#!svZ!UW.m*鑢~A@dmY!MACcksx, ~6\ai׷dE+C~*\w1aSBQfK1`ubRk3ުvEHOqmS]9m2a&>h\Nr+)A/>?q"_DjR?lPL_`lk Hf2d1|H]Ū^yOET_My+.d_rC3pk.ނӬd 9h ќz3Ȟ'9k9ű2iJV`Ĝ; ^~6 ߭)zc*YuӨ41r*SZ6tܝw&&z3S+fLOF~iՠRIf۔:z9Z S,y'gUOA7Phr=x`p("W(._s !H_m>z?'!: 6sCsB4" 9($=g />cKoT+8?G޼(Xe$vϿ+!̠~Ew>)4#$P* :C LC3TPPef R$EsCHLܰ.Y+dmHFe K@8pt p ʐ(D\LY h]86zbX0QJٻQpBɍ-/S+jDX44Ihd{8^p=>ox4#n ydDD <'d I !}D)Jbchza>01*2$K'TBhG~~k{y QA~"?b!D!ƕzd(WUTJ?1F܌d7DĜIPRgP ,`ҰQba,cr#]d1xMbv79g1L&>u{}e3\0r&aLy]b$3i чkK2T^B˨8?$+$ܥI{UJAR?lcHBAwuT$a?^ ?C!h'OH:wK:R\MK$t\`Dʳa FV U/L sH)W[Ok}O^&?ĦvQJ 5 ^R A l2,Ihb!,  @5hX9$JbIWz.wV&KYsiuz $gŁ>t(!b!ݿf=oJ4,>,"rCK<-u*Wb]ȻrLcR^IŒI &x^ɺ_>bl{ӥzb@WƊb"0Mq |x3p'L*Uoe "7".ƊB c2X% B[,x]py m%AHG)?/`s.3>!:2  r1+pA}"+a5Ȁ gH:BLLP3X EmjRC!&Kzu5uuaӭ7g/hbA'(0rȗ&TU `nLG;*U??]Zs;E!kGO,ѝy# =olȮ-O$+b V( npoIMEsʮ }xJ"@,.a? Y^럏 Oy'7 )*DojX")h?#j`:`2JxjA)9&ŬD=$Ǟ*@A@LB.G?o[kHr ?3&ۚKOaT_Q~ɝڥ-N91dV? "@?⒧ބ9 #>Wˇ\ZRcⷚ_-Yv6y~WjkH撅AAqݢ PZ`H.>,j^KeC_ȿ dbTNkqK]qP3R|&[+$ڻ8YΕ4Ww*k^s sDe=5F4gQ?W|TMXOo/`¡eE1f?fJ!¶pネpg`bCh,>'泝mqr_~ĠaFc3x~*Txl#?,-f֭wMCr#`Ov*ӟ 5G *鋊M8 5CGGY.ȯ|Ws'EEh8?榍j ) a~tk!uhf% K?L8K'DɧE$ز;9}=?/-2LhoTT;\` Lo糨:;lri_j=Z{r(P )B~+J+"?$ l؍}sVTps,g%:&NQrybvƖJ,d}=VH;t0Ebpy) OKN׽|75NO90?u ?FmΎ3(WCm S~b#YN(M8+ٓ|Q1('PzWoۉTL?E2m9dxMYxysx .#ycNog@n& QJo3 q$(S gwW&Xxqg,gBBFT{1Y[*ٱ =yG=i,8Ù;ӚRlCq、 .L/R)h||w,i3(N|1Fۆᅑ#.9xFڑ d^l?Mm=k?X}}ޚޢ!D%g|*Ry2}gX,@*쪱VDTޔOߑBͦi6*D0tj&.kAq%|Ӳ΋g#1UGļ~_l -~J'@nݝgȻmMxw^)SB'5kY è\d֌&Hb6S1t$I(F3qtP= Eg߉؆kbLYwWxOF3ULbE+[׫N6 UmyeLv`pʒ|<(/̏;[ɞ=^/Y8͚: ~"P̝ 7xyvq$`w+Z 8I֮5UpRaR2tas˹U!z FhNF3r=vOD鰲*̢^ JQ;L.Oۮb DslgPzrl(9VJT21-t+aϽpz\nޞ*XRqpw%æRTpPLR}g}Z0@,$VdJ'Gr#䣚|`GgåI6\ z>zt\'qa<`5Q#}YVx]adߖǡ#{.)w@2#((U-P"IM3;gSrWϙ [bT9˖r܃ܿ}`+p&5JU)ޠ5crZw ay  [gk4DL$JEMGzs[a3y 6HJBJS_NάO@4m`թWԂhT#qI9ܤ+R3Sg.𤦫0 BW5XWd2jbEZ0el"cz)ҎI!N_kTÅm)5*fu+'̫:qUU.+v"_>UbKDwM@ b]~Bac~~/YԄk8$m`b{ f 1e- @@0e@C$9 r "09&I $:`2;9&I2@I =GNF$qø0;A AG, qFBFc@1DPt+Ql) H"D DJ Dq!1_\OvB綮C1?VP׷Px:_tYӽf Dt#>~ $0h>?g9ԆHa@['DшdZ*8u~t dB &2Y+lQOVc'\.0,ƽ}ti[% ʓOa+vK1Y*]>cĄ*0(QR1Q };V JTSJ>~ees`LLz*oވ1赡wW4D4xC'PJAԍ,HĪ$@L"?I>礀:W6vl+A&_*CIWb(e}B1}ɲr~|sP$6XSC:Nwth؅ tHi<$e1*m-ek= QŇ~oN<_$g%Ț2 %_5N`|B&G~XCB}3x nR q(HJH$`kЦUp|: rw5cV4 Ylfzq&gr ĶU * o`EQ TN/] f$؄fp3 ]=#U#ĕ2Z *EU1Mb vS p$HCXDj (Q>y?@9@ԑ%V&bI" *V "maHz PIE\n8aX:<$;1 C LҴwWvAaΐ0|YJ=Ed<d&"!=8JnNx$B|:q/WR;2Q'Bc:uw߈"%$%""ࠆ@@=^7 n>E2,車/E@sAG)kW% (kMt(1Ze6mFe |B:dq]\-uVXdR[A^as IjF6+`.0<'G_lx@D!H0d"Dv4MLHC704EQB⊰dPHX&*M%P,@DC•d>q4'-kl?8 ʷsZ4Da΁2]\ A( *IqdR, f: %tfp Ր&!HVPsH6lFrDي_zJ$AIʅ[b1B3jWC5ɟuŔeѐfPpX(Ec}-tǻ0' ( N5Fmb 0JdB CPf | 2ĝ ztTɸ u { vUʀH}$I1X]ߵT @!ٍˑMn`!U `Pv4{`C>)׳˽- @Ҟ'0/ߔ| L;Bca^ߝ!G}:0L_.gٓi9.lGNM(JA!}]kVJn1vs;8v9sow!uSV߄lܒ1 lCh>^sK~2\4X1Ws>gA{"Д0!J}YwMwmyӎlk6X`_wC}yc3,W:03[4EQ~b<ϛ}xB8$CޚwuAӢz_$aûiQ@HKp&U5;]?gs 5_uA Aw {\nFhLU*FXHB`Fe=G?'zM܃#KNY.hn{3<ͩ>hꆏ@&斔B@H^[}WR}5~g?׼&J{-"(m66}>9*Rb}Θ!7ۄڞn "Sك,APٍʉ7 +5jj\#*XH+GH5-{!MBSwԋ\DIgQljqQDsd#O9YA=:=''#%HQh˼Qm^[7IQ/*ӳ|f{!<e5g5dfqz,*zY1ШP H%b%>n_EFURPLjk+v0 z KE9 j{ CD"0BQ@?/Hd@,`r:Gpd iЅY_ ~+(,c*J,|Mt!0s0jet|:>tr䯗nV|v<Һ>/&/|e/Z/7 "Gp0U$7 zQ}l0&C"~Ef qlMkP%*>Êf?׋+[EKOV~{A:ttk$%JB% u0DLyq-,c&܌>?q 䀁< EQ$OVaVΑ HbHڌt*<}h6XPzJKR ZMϱ- ud~?@N4N10zqѲMaO˜,u0g֕授M蟹T#Z$ /ޛ\@E*d e(A !`(e4Ǽ)Ȑ'u> S Vc%?g^zTbMs_C(} gٽw_1ϰ@h0F"n"Nb˷}\6ݡ8IG{\3d#  ab[PZhR=@"l\3@$+^ﲞ;NF 10Hda uB2Nݾ{NgRxù,{_rd P+Ro"@Po8PE"N ALO=1+I"oP iK{;_ Du:%g!( "F2ELȬH !HeD01`Kfr!(%d!v; ÔCʑhƢ,bmYknVHSam7J"( W"31yyZ/'Zys:XLRQ[1}FO i6IpAuK" w[yDux,:%)H - ojFaD(6J-yu GOL>ͷONC|FA) x\}bf1!ƪՖ0ۆD$P-ix?^xOM:_8$77^&&Lhi&@ͭm?n^vy "QDaUvhS.C#l_֐R?_(<:BH",4`ib-3Q!& J"aX`bp¥*&YTP&@ ` " ar0*jZIh*@ JN>MKCA4A. K$ʚ_MB0ۗ%W% D :4RCCNT߽P7$Jɪ8ȁMJ}S8~̹!gwLаIc⢼i۲7[Í%5n ]߫w[G4:8T9PJ>&$Z#68uLOG>vLgSvMe1`5Fq8kCL\]` .cRPYl2)J#4"ōFE@DjZ0m&0VBG#–2ȡcl, &ST31 ༅%nNF7s?oæz{{?EPDMX&zʼqPXK ڴl;ɵPƴc*$p7l.I:#K;avB#[pЪE[-Lq:sNQA -+|S9yy t`Ab$Pq%|[~Cr>b_U+fCC*&R;{@)5j/n0_V!!w[qC!RRByh>a9XRb@i'Z^!!{wFx{tl b^&\nIiw,Dz5C$Ǒ4fّ"gš+46.pd>LPs|@3B 2E35( B)J)C֋=&4SB EM 훨lE>Lei6( BT5j.Y{"p# 2Ì&0#.նpRR$e3nh_1}xU.V0լVĉfܢ f7jv̺JZ +9:ߥ#-l!r4mSd߽P(T#9H5$3pP ɊٶФ돳^!ٝ偍wLBW2eQ0d3a>586QGG)|(moR^Ҹhgų 'B08tգŜ%h+h}JX7YGx0qTib<je2&ics-793edLpOFuELtAub|ndﶹv:ЈqK4 ah ϥV.+c^1f}ϻMg+=tɖ{ bP(E=?VmyrVwKSpB B4 wcoA/&`zcKl|/5efed,iRe_=Z+ՙJdk+ǫX=4`h B0&oYy|M m3>bv?3tb:NXeKCZ0 y 3̬m(PVEQ\  `"h2`ô)H D +B]}5D AaeQլ4je1EƒHŁTmjI T] GQ%1 rq,HmGMUFE Fs`, !{Xdyi Kxkv/Ӂ~B'o; 0UĢe_'% Fo`Pd+EU֌W۵X!Y& +oXu~7gƗ.P;YwøMōbS( H?S;22AI5R  0ꚭNaa-q ~#Jc$d$QB2r%2JM<1L2f&KE&CD7R$U%PV2̦`3C (0F4[ݻ[6n$Wt T bE 4 8+ j&#h1}蘨ϏRJ/hh= FD7l Ks1e݊&(\Dkc:c)0ѩ8qAz\88451-XP+C( I  e~ٖ!7.G!1Je$ɦhIJ8ci iê 򨫹ђAPd!ES4J|7"ȫ|~3BGlId3/鿇 p~JPqO4$`W!ħz),_ۥBhu{J[З5{t':}?k%! CR $mܲBxզ+q 6ohTŞB%92VR 19L ӭ M4\uX$~hI2OI}=$wF#)y EECۙqD o=d?z{d P}[3%8{ioxnWW#yAw|7:L{O~ڵ+jOS~eFTS@IհDzd ܾ' FX| cumPwA*fГxO lKޮ]ey~q&VMKBQui0uc[Kk{ɚ(kYWqi5B B<~ 4ã=nz5⁋.2(Y͸CJe 3.D(pZ.Á# ,$.o /k0+NK|N XauHSLO(p-bnqցg$e @GCXw:DB!L(ԟv7I˄"g̰/,g28̃D 6R6 R61#c #s664u ;@d4ZwOiûrCs i63]#e>sxMVޤ5A(y&er[ YyE 5(]VsBX@jbj0`n#d,hЈkmfL>M7!!3h>Fm nD͡hm"͂s:z“3`EV5oN^ H0Pɗd$,L8 gHbWAurRۣH $j>#sr2'uBE^2b+_ /1%w 7z)nDGg%4HH`!D PN6Ơ (%7NkJك?|s|'0WXxE5# {51(w4SVjiccmTgI ɤ{- d&- B܊dK)|lڌ-̉A:R3r-l(Cñ,[jd0Qm] 7vcmڬI$mY}kdzL2$햕LNȂcmWފ98y8m;D"jd^LDeഴ)ƃ]@ƵIʻv{Tx,[x8(& T0ї"ЩFLԥxͰk}4ͬ3i-{(2W 5 ref^3LڙYغ6Yke &uAɬl!6S&S0DDZ5ME}5K8تGH&)XfB2c_I'aa6$Uٗ `*J^|IlkW4[&pDXãP|@6I;VRNRmn!8Ъ !Wî7*{8Jl4l̒*hG\n tO]|h24Jx0񷎹WUCq8o `ՍU> TKލdqUtyW5 xh5{/Yܔ&m5(rE2Ud=淵=Ms#[y<2DQ簏%lq13)0pVaia"@E$J @c RҠU[ w{9mG_ˣzBI$dS_aپ|ov%=^7H3J95&bg#!-!m&MNp߃`J#Gheb5`f UWXcx[*Ú,RE fX!xgzB,L jfiؠc+KͶ6ZODJ20REb\#BSpZA(-!n/BiWG4`gĝnYWsw( NJSp``sr80C2|PG{ S$|$+8<?H՚IV+wn<$pR"D n9O^&NiUWR|ZDZon:gIopAH@X,AJ){[*)TC{tKP뗌*2 )@c0޲xd#@ۥ%:$B X vwFH!66 c3BKL؈g4"RU$udMws/u}'yAo5%` =@%4ckU(}L| бF( U>A⹇_rnU0 D\ C PiCA-F8!HdA @rU˻ք>l0QZ@0 ijEIcRu8f컔'-H\r XCsXw8xP"R&@֬7AO_F# Uƽq[^u;iɵJ"*MM);9HHMQ#0fcaƴ70"IhssPtHM E壟naCi`-`6{E$.UE驝!$Pʜ:fh  A@eTU740Chn6b֬m2d `BNZ7摢(ALHK(AlX "PaaRN4T/0$!. P,tBnJ$1HI(žsJweD01@b,1*d. @DLeKaPD2Q JXG6%4D  @ED԰3&#"މ* !d(X+!rF<yU"(PB(SN)1ZJ(n5*Bat(&dQwK/쌆Ji"\aaaziB{KxZܶƾL-T~EښܩycrndoѦ]Tie.Xodٸ(F&aףZI%TA$:ȳENܗ͢E&Bc;(! i' 7/jP}~;: 8Dq?@  7~I\;F_09[8P?]^/;'*`s{,3Xm8.WvYU.Y1_1!o_JJt59_躒(Exl'ąEoW)a(c dW['晚ɚDůҬ)c;*JjNU *,mZfkgjėe!\.+/maiKc+Bf Ɇl5;,.O8ϔ~P[l E%*Gɗe@ ENնC,$@E!HQE5Q!IDDD’QLC,#tQM%b)RHTݰDѭ@ ˑ$R҈R+,Be `L! R0LK B R T PDRIE FHF H#;$ a"b6}FjwGhuAm40d+BZOi=rЎĀ'zȩaOc_FY*eled˷|f1DžIJO@ @CM ip%i kzbBM Y5<-~b:n;WDb D5"ѝ>?[>t.1Pdq)_(xcf( {51~ > L $%DqZI8%(ZDil d3_?&q0tO4aAKDy9"of@ږcn;Ҥ; uGrt0L=}%"wb4rêW.]4D.(DBl"PFm]IlwY6uhp0ZY1vRm= '%Ŗ-f@@RCAdI ΓE.ԑMDO!kix%"HmU&Ұ1F%̨b$&ӗRd [pMvl*Ț:M$5`]X Bb̹k %rZ )/HJ/RؠmIuʄq0Q< :@`1J&x{3($&!<䦸0y4| u^H);Q&N5YgK:4YHpO <LQ-ׁJ $,hŊ=@ R $RD"V1"VQ(c#RIV **0 /$iէe6 % @L !с1N &aII$ `X @b =79DOyп :Lw$ӰE2&z`Öt͎R he0 #DxF Le"DJ("XZX 658.hd:^0xNP h"`E͍LJ2 Z(m T d)UIZ8g{y r19p*® nB$ 46J!S J*Y"bI$#=jϋGӜU36J?I; 2deGl/ A~0\`HS9GI2edR\z !vCx ?Ulp1GoG.Hy_Z&eCHɵ94JQ/S_P;޴{u> ,#V/J6{S/V&&KUDco,lʈ{uu\u:DPդ)(AI޷v'(1I?d:=RTS1"r[X Z'|lڏJ?< 0;N=3^:HZz}ɤ-Rј}Y^/7m2mI&ِᅛ83&226 7d,MTLH L},pJ,!n7Y,s cZ|Ctʅ"RaԽw?H}ǻᗡWHndЯ-)}'O5lKȇ<=e$:@0(\)L$^c@f$n9BƁ2Ԁky''1S:\R$@i$Š45p0GW݅@p HSa^t Sy2` q_C$ xA/&~.  fW> A̟&S*M1K/M((8l}#-˘,oEC-c (dL;e(IBPBiB69|pawfUhh! uL&HEB7V˥X[A*>3m*XWYj"+z^iX׷|# 9>!4<<@o_ :r!aw>y{n=gw4JTT'Gr.&a&bjSY'jyQ)Ղ3딓L,PSgQ{Ȥk(?s—GNg:-Q܁ 0I52*T3cT4@43BI4G)H"R%RZLO$i=!m]h{%&fSi:VGi6Q;!h"$9,! I2 Lz6-F0yM}( c10|>e;&'O{z<Љ˂ jmct H;vH ÀNWT&(o=̜$&*1HP2(rbUsJbG "H2>_=ө =f<8ӮE  "!AD@P19zɡxo3=({oZJZyPMPƏq{R'\"Qzl SCiXiY C򄐀B`p?<*ȃ%܅i $*NR2: yC4BBhRǼheN]e٩aV;sMk@͋&ZVU'ص)'Ɗ 9BraJgIADPKے+TU./P)-prj$"bD;ӎKzDYӯ>;lPWjS"-ѐ#!f,زx(@ˡl;`>& χ_!waI࿰EQ} }?Ta{3Y53zm9>4l6޼fӺ~KQ,q!UiN\>tA/#8ƾB+dB~E;jr0dNCLQfĄ~Q'Z0F:΄)wO_0B~*ބNT4B"R(9"!A>PTۂ-Bcտwscυn%- ދЎ;Em Y3 rd_^gO7q2>i CI"yc4uZ2Zs~(tmxϧj$)"F= ՞ƧW6~8O+JoLfYC6̠cKjj) bh6c޻yT"PЏ6|IHp/"o[mvM!|$=UEG+>*YsMZCyMh%,V% jFBCغ0C Ȉ2XhL:6j؎Hc Wԁ( H4 Шp%R A VCYP X<2 Hi b%nQȱDp"ڐ2QJ*Td!Pd"0!5hT*s X0|D`i-Vr¡$4HN(J 18@B|F ASiam#0)\D)ҎdP*dԈ9jETdDd%bc)d%jIBKl -@(!#W% \qI擵yyyھa!\̑".a(I{S80z <`+%x"Go1$1$ 1D4 0pn>^.!baO]~%g8\`0 #DRPTqx̴4d@ NxzN|^u&-@{%@@Hw L 4sS<йI78ixE (qY Q01Phb XDJ2 4 FB2,@$T!@k[0@2lq&$8bb$1B V*A)(Cu;^RWjQ @4σ4<Mn{̅h\IJB@n]=y'yZBHb0EH=稩fOZK?s #!VVKI}SESGCb{EķAf1 mv&2?8d/iznԝ8jvM˜.4%"y"H1) 1 CJc #E9R⇜5 v!$<.BJ F/*}rN)) fGt5GCH1AS`>uQP"/DH/γuzy?.zH&{"4T ~>hKSDߥP):ڃWP1!c K^!uXJ" %A&rd p0H*HP;`(%+SK1T4&+$e *URHt$YCԇ HdCgr@cdbn"bdIPo\-?a<1t *""e}>_ꗄ-rvNГ%[JA2il0ѨZSb0|vb#t RH]H1fؚLq?_ChSh'ɨSDRnDSZ/s24!JPHJ4: Fĩ)IfPlڌYDēB u%F#R"dJ@C\ 0pQ)pDcZ&JA~@.!̧Jb!p7tDtOך>yh7 U %!ͅ]RrrC;q<#he (Fg#m9>vfډV{ ɪ&" bGh@\# Ťh"R"*Y "b)@bJR)&P H"dHiV% &"BQ$ `*H( R!)a*H "Z`B )A JiH$j$J*hbQVe $UfP<2 4SDKPB )*I $pU ((C*D %!Dx_E߇{Y!LDAIab(W*:$/NUH@ wZȰDZ 0@@QA X 3A`N o DjH3ke7!lt&{(i]G $JEI,LQTĔ "C:NG_@&x9jJdT4Dp,* RKuFI`tT ]H<-"B QrJ YDSNt)Fl b,HGPg7Y.]*!i'WbnN$ 0V68 O%UZU[fu0wS閠(Ha!P5!D@xDݕޯ֩J`BUH4HB4DLCQ5$K+K \ $ J!H &hhœ12iTP2@U"&BASpLPy Ws7#;9<."UJF"|m7A` 8*h!ELϧIV0m$49R@]&;k,x8 E<0aPDaHK%8(@D?ف $9Ld lBL8N? Q98$0 y@6e !VC@7"?fD 1C1$(V!Zt#V"r;WxBϣ2B)BP;kr_KAG3d:J6XoR "n@=?h:⃄=ZZJ/ /#-M $[FCٳ8 g%/5fgQ3) %E8F 'eJQPH1&HU/gH >?c6ѷcR>cmUS(q5H M2I>XX60Aը,\;s~{CAvWؚiKfWaBBR{&gRIZq'aޔ!{W4|En퐜L_-fzuPT屍L\D!$fzvFfZhls Ud&ap1 Ab!dRN$([ċNv>/ ṭ e8j;CO@zrǫ ?W,}Pk<^nhx$DKU@؁$>N'>tf=EԄ[l 4n@s, Csgg<,0MՖlkH4ICz7;7Cqә(DDְ0CJ R %$͜u:(RA)ل 4eG-3=;܃pR`p7sa-2"3 _j rx82`:ؠX#Ag#ª/zj/j%`S/="TU~ ;@H%`w6.hlyh=crAg(ތq+Y`M_bISngd9' pxzͻb"R*' !I -05 2>; SS phad+T@HA'vxx^1_ƌ n}uȒIui*ie'HC$cbDDl?HRjӦFmB#{9骤 ljM D76Wv/9 H2\8JT~)l yxzcH Ҙ(+Є)i*˕ j{b,U%{}!!ÓSz eUIiT}OdVAdp"O/ ~F */@uꔠr뀵gDc[H߫"ix,D>Zuh:lͅnɮǬjja,2C rI: }ȶP`Q-r#ցutIkM3D^[6*80X[ "*6ڎ1=K,%>WS}ux_UqGEe9 5ʽP.ϛ2v -:n)Z4&ε,Ah,+9/[×ҝO:䋳4RPֆ->`H7NN 6CVBY厹JMz#21W# ~ >g7wԯrki6ϧ[oz6=IyXPgաx,d:nyj~|t ?Mpi2Pdrʑ_+ORijN{`ji@wXņ5:a 0bAVь%iT l kK 1)1mF8lmb6}=}Q˙!KVWOu[,՗Z-4)xfa]j0($Ճ 5A,S+`;4ƚ"ݠ+!ֆӼû'ݺ%znmgdMt@CޞcZ ܐ}|%P dm0-MFBf"MI"B 7h-bG[[_Ү_;)|ud[lYGWhh.kHw`<<)AJ0HΣqS4\N`NT 6#dm_ٓ en{r泱dL{§oYZTF bبri8(-!4U+^MmT3 k\!͠ť lӧECBcJF uAoM;;XvelO$z!55łeJܚ-` %>>c QВ(e [X4'hs{\urd &63@Tl]gr`n$`@ph%K KKqϝ{_HQ=z(; nr<镠Ba -Ў=RJٷb-C6 a0wٌ!&O3H) 2}tɶ+vf"=2,KYuGrN4ܘz[-凎lh$؝oriV0:J!<-YF|7'dB!4Fll`퀵"݅abi+Gi~#a=5کk'ԒXLc:JQ}R)nc)3yO4 v;HsROwnN>oVґF/,WV{~.$%|KD͘uIb:z_ƋYM4ёId6"+aY>RoGpϞRE5x E y= sçm&bZ"SS>bC|n ')Phc'vVetHH0H. H c`Az:i`ל.S'񧓨3(þ:!H0X '(QMeURw0HSv 0dCoHcxɶ~F,0_Dh؞ yV0̼޴,BD @'`?9|X$RmxD逇 L.+*P&3A;NYp<}{ԧk @Ȅ!!c1NC|'i.&42وlF@(d[k{gk vA IE\fP@yyItd.FžC kC9LVH>=uhh s],)C]ʌ[FYPBXThQDb0JLڷ&6&:?8;5Cƭ?̂?©:)Eg}~T>BiX /' 5DUaҾ;] %^u֡4)~lzlڈ( %HaW$Q ѻ+s8"F35! PPh?f?(d !\%#Ol!ad7/#˫4"h3/z)j肜+ ԙt>a}R}OӣGx TNs .:RU yvqG>'*+kn $?x' X'Lţdw63"f h`7-j9Xzl}#jCd2eJ/oȨ$(T VB"7R}#8w1,CɠY3!F7I:fO=Hؚ[<|)BjGd۩T!ڨ$n-{ ғg1r6C;_iwPzgo`}VݽUNgz>e<9]{R㼞y64{ Ii{;"sQBǵN_؆ůB63ٴL,D0DT{)Kf3׃E>&̳GcQ"Hj8ΗEP'’hq~'#qkidch͓%[`ka[\FLWV"E]-}0Wlê L ق`΍p|߯;m<92^2d=bm= *&N9fݓXr{4oY>cŮ_GC#FHcv;<7Yߒ47l@qU%f_|SF#q؎ .]Q)z [jG#":O 3laGgSdz"IZ=꘴enaV 17˦Hjֲ $xuHh'kn`[؄i(*o \< & q>d@P̦oN.Ck|Sיɣ=fh7>inoN-FL91~[\_3E0:ɉ,vABko#܄uiVA1TfT+u~Z6Z dsń!&6oЩg48ó [V"!Fn 6mF*7^X /G@S è"=a9zv݉SZG4,\6 M&6ē *!<'5h rX9BE֦!wv CA4REDF1.-K{ȍѕ%3?{࿻821[5B%=rRDgUM#偩D3H6i 0ŦX[c $ $􇡤\,[#-k;ɤ{IB$ԄB`a@ J"4S4/6e_\&i%2@&DD2S NH@+Hk$9 ɨKĘp aT5X,51f'0M1 %eb-ҌTQĴ[(-!ϻК eF9%L1ME-4E5415a8Ab}B `$XEIh))°daQLSIIIM Hđ%)xq`v)DGuf̫b1$ƄJ H hh"H""55+@D$ڔD-`Db,A,Du YD-T2Pް&O1T4V[}? ڌ To!DS@ ĝ!d#rҦ{-kOu :&uJ I!$n, D܁q@P@hI,(*LY*PG|c&"Wqj3k HL} ?: fN$} A4-.5h3Ys;ф4L"!UC[ƸPѧx6῭Izql[Gw cjQtO~XC%{f.?w1ԆGK8[3`=3M1T/u꛶G6q!  >=>az3jP%BB =@2gv 6؊ Bv(q&\H "3K2XNl `HGu^[MujMd療 nT+{5F0ipD }Y5Mc pJD`/;NtPƴwxil꫞mz9Ng{NƊBDJ1RD@@$@ +" }P[*h`IS4Yp#H*Ik'{tme<J.V&d5m%ȁJ5&$E"Q #CJ@RDJ%5T9MR, ALADHY PP "XPDSQ܀ hT=u@P!(*\7Hjo^鎝%X/C]yu"p?]0bk+-7eѴ17?@T*=mMVI 5FOمwN# ]m/Lb]E0lTH-6'VeZM~kxP#[zZc/涖"" me&"lH/YiM^E HP09L?N(rOZ5Q ٱcޚ m!^6pVXIΘ.Z-8k,6,65#LmR=1"2od6-51@㄄#HXWe,Nb[5'HnJrhm[tS I:[< CC:xv*UD6lٲT&,`r9 rM TfDai% ţ *l8:rӸi0T,D &S? $b+aagFhbqA&a6B )u/QYH$DdNI1kAb40TńM3X=!Ԓ%¥2Hm t3$AAAt ŽIl% Z2ӝpiȆƈL'S& ƠRH6i$PufI52`ҝeºl s1c6fTry,̣,F1woEl7j(ĦW*[S5 Vu&^*ZP$; r0f4l\k9}Jm G6ou#RL6.1hN&4#-9k9 WfJIB&weX0e䌆3bmG4f06R812mlM .*Ӵ4XU4%F:#@܈O-f⫬M$x汣uX RF %ٕ$8mW$(DxX-,^2*Ga֑!Iu(LDEQT1D HI@D@"1QT}&J-C2DHHPM(5 gtT7B/KŅ͢(8 @-~8T:|TD0]1P7# #42Md`@-MG1SaXhCڤF4ז Ȱ`VݎdML  "zj @j 0M* 'A&+Ë sɰ*]P31sz i]!.Pရ\Ҝ 8#3j6H(vIyLUHAP" igX)<01wtA)iLdpA 3ϸ2h$]יwA1$$>noQ0 I(@)&J;ώFDMP7D䲫#KO9 M|}" < = `4SB5{+:xM Ր<=jMu f-3V|:70JvKBBFDpDkE: (ID0bZB1 + Ht@E\s JF‰,5iVSjeUe148'%zUVB q+*M317QЖX6@(a5p҄CXk(cMՑN!Nc sQH饳SH[HfyhFk$ad iLMXpGTJ ڍ@k=UW8l4TGhnk.ԲOC(e'k !*(b*.uМ0tNl[ {(`7ddD" @LVICU 0? @( E=IQXxlUTp h }|46T&|?3Y{/h;4"h$a*Ў)U01CIp%O2Oͬ|=z[?3Mekmf)hLsi #ȼd׾f>~^.N?O\'[G'g_u'kÅOn׭J_U}-O}o>o_7aA~X<"X$o^vR堪{O\k =5^m!>)$Kⷪ@oyqWJ5__*{v޾l+mṄ޾\il^R.[9|g:&u=0SVj֟wȽ$>O}@'zdLhI!-;#^CyMl=bmvHy׮CN1qվ%}lL=C޷jyQ4zUQ}LE;_{doxYPz}wI!_Hy:El쒽Z>))!D8-WB3m =ǃA\A0VH1\@%JZZ\ \$3%EpB&gA" ܐdt&Sb%آA]N6˜H_ցPD)+JgOC=6{[ؓ$W @0d_B}TuI;XC ) i$PX  8`1զC "Hvkt,LG/wJ}Ũ)"]n`{TUߎݟ9 Pboքŀl'xV77.`#s<:* mr!S"]`Y`X9eu:8~cf*2`EDdeȈ* , UA"c؄4 MH ͹183aK&O Eh܃rC>!{ZX (VQ.!X4LG|2޸AJD@6}k~]qY$j52KZ^x `3#k'j= A*BE 6DW/"f!1M CČF:H}mt| &4Y 4P_!҆yϮxuO턇NM$ _C{?f yF\Eg?* B7jj`Y.Mѻeԛ*\LEi7/?CKu?~v{SW=EQS a+C T 3T)KM= ~D[x8,DuM#K H!(}xwʧK2-)fŨ%V|]!(Y]q{A h#('SNX37OmRk[}\O}N6tLӹ[1pj$pF0X5 \ Tr6z跠pf $FPqܢNAC![3཈(#@%͗BgD![AS$;f9:Gϊ@x WG11ܡhX0Eb taR!`)|lаÞOx@pHggvdrC[7 AV*3;}^􍕣6wM4$yvݯCfT 23%s2h) PdjI"9Y{x-oS^B0cWjdרb9={LQi$,ӴO!&К[ &VC5 ,I$ ڢys*۫irjc]/|Za!U_(M[שC#,qש9g^fe1lX ph*WUǽ¢.M QPUݣpA(†mkcՙLhMxM^+0`Xm=UV Qڍ%a崚|( iAK\OgųOpgnބx=j;ٟh$ 0JSxxBHc|'C#5>߳ev"Gk@/{}8y?9RtD>^j.ꝇAjPҡLH~BYB0yҳ7;6"ڐl-BTd#" )$C&v&u0{R5ᝤmZ ޢ)/6|&]C!rEN}cY![ܱ fz& qfθkEfcE4hF!O{q6*%qQoTϧ G+)9ld`c1QZ!j*da9'Z\ٻQoQ˾zjXc]#Mƛ2KxkT2%yvi &%ކ-[R/ SMwYbwtHjtzkF-mR+3ÃN$qvqS:&ͳLro-Ӭ~hw!mOζ5Y!nWnHoaЈG9+Fmt3$ݐ6cIR t67O\fnhAĦL2;V45v# ;ˈv_d5 yGkscΝ*ðe`v \{ Yۭ`ъ3 #xrIKBq2];{IRb4Ƹ)7;f(1i??Xpۃ$d!EݒM-#\YSS?% =k`1P/-\[ &' SC~PExdm5$i QB%A#N*bv&n#w :UT/ͬSJ(Wcm9N8BPzZ ~YO~b:Z'aSR!h0u(q3.rSl"޼e% ғ|si};Cox ĀBAbJa!hY.ӗfiی4诣Xͬ{Ӎ l_>3hBm$@vxUL/I]K U9ɰ?^)+fÏ;SU|,^(th,hrF#JȭL2'=oa;>!D/|_2VD3\H"RYӪ|Ϸ(W]%kV5La cD0|0#NOp񮑣(f&­_<:~~o?G?C@BsVYwLFa'X8c@\(GeHeDd8.|VQIaS~G5lc۬qΰ&P'Nd =#R~dۢq$}~J:z%[ XFcgЌm[Vg阨 DhTtu[E"txsQ|vv_xWc6[mηjQࣲ5;-ڿ/wv xȺ?rv{0SltyCd<|˶Ï(,,̽]ۍX^'3leQd,KL2h3pi.릎4z]7ΜY\䌟RP^ szjjL5ˆvE +?G?zO}SCu_9镶3}? Fâ눧lM##"j%b~\SEm~`"?%*>`ED;WGGsIӤ<~OgIs34hJGrqj_Oau8Ъ;+@fu`06F.D43-`6dIHh?&cfJHbdkəz,dzDgܳaΎiNen~L0g\1N~oMw*njAOk sFM㩛ĩwiKw/8Ik[qП2e]56 'S8_'d p!yG0l9tgZeaݸ[&n7{a̚A =hʵGl^ҪnA|ebOkTp (j4RC `BIevVJPQ}thxƪ?mե$xZ898?1r E>9蔟o^~[.MHkrUϽ֠h /]rY "#/qnJOZ4cJ2mCUBI~m׬0RќpoV FM*fu6F d8vkb4n `pt% $c&hC& Ec)IJP ۞8n8b8rXCcȈ3Cڨ7E+Ҽ:z"*y{1cj=+Fbǚ5^.:S!xi>Wlt/AAG;LԬ-Rehwp>eͬ8tǃJ]&mukZ\f)t[UyZ?/Ɛsf E*bUXfɞ80rG+-n:_Hgr+])3/xIߗ ^e&I0n06~>i0gI5!+zL\27" (Jy\~>cn 1Cpub"ĥBK1C:77 vJ( sG 8;t/ezg , .#ƜGxXjH>?*|GTS;:A.(쮠m{5xSԗ^kл9.-KZrˎ.ܧ].Cr_-VQn{W//_*Ս57Y屩_\5ǝhϡhJѾZ#^^ ZVukWYXus]~O<>#qsyvg[u׋άw I>  \2p pS YZ] f/E0%dIqL20]~/UŘlVF-w=4 "c$t&BL2Ȇ@KNGTMVix4@ @@z<*XnXdn;˦]w㶶 4#e1&B31I[ל^u<&-85d!ax_?b_|Νnt[й&|lS8xC44dk#]L0'cBSI.4cWO2ĠJ>̓;CCǹ&-Q<^"DX$3xܛɌa9i7xC be>&!BLΡk bb5N82ګ"3pI3P=w >4}+ JrqhmZ:x.rH{rQ/\s~q!ըOC&qK؅ .!O>?D6Ü4uŏE',&3-:2VGC&em? RB|ai4VB{{ :.HP>t1Fzxfk~JpBΜؿ0,@|NS.b!hcU2QE[8[nO)1rD +7:MM"ؕ5x|,٣,2Iiwx-L wW,jr!ILi'NY, E$[6%\o=%/N}m?uh%"r`3[7jF30ÅFV$dJdQ%AfP8hgG|rÇ,ܚ,.JJ̛2Bq6[ZkgݙFۚ6rk\ Tl!b,";kZU5PRڦHM2L ƤT 8aHLVT2,-Lf9TF6|E)AC4 ,7ǖFB[F6&6E]xQUD݆K:xzLlgM0KL>tuE:wc)!чW:ZtlQWN!d @^P/ qgw._~ ŧ7s!8UZ6NŻjc)q$TCB-U/(iDUv(Y.Y:tjpb@Q>o8\rf-y&SJT|`~(r".ǫYMQ'&췛5 EzQ8O=~>2Gg~C)E&f.h1ye!]B.Yj5" SYLْ2[0f`"ČviMBj6#N|SVa6>TƜeLۘ ѶB rVDVkFm;2|I $BNoIRŎPSv~',,5-3JQʁl=s`RjgNXcDz/rVn ޸U\5=yێ׮[~Iǁɇ7Zd4Im$ !* [-ꉚE\A(}nJ1#im{W(w#}|sk?Sx#9ӀjSlӤ9Z~c܈FbK"f?[O/)#2{q`7^{i+@4C[%#k@Zh鯞#[L6uoeIo;ðb=1fŹ0"{XV"%cʨL#FM8檌I1ö[5gk~2w7{ɄiK3 bin/hello-1.0 chmod 755 bin/hello-1.0 echo -n "" >share/terminfo/xterm.dat chmod 644 share/terminfo/xterm.dat echo -n "" >lib/alibrary/alib.lib echo -n "" >lib/python3.10/amodule.py # Tests for symlink... pushd bin >/dev/null ln -sn hello-1.0 hello # ...to file in same dir popd >/dev/null pushd libexec >/dev/null ln -sn ../bin/hello greetings # ...to file in another dir popd >/dev/null pushd share >/dev/null ln -sn terminfo termcap # ...to subdir in same dir popd >/dev/null pushd lib >/dev/null ln -sn alibrary alib # ...to subdir in same dir ln -sn python3.10 python3.1 # ...to subdir in same dir ln -sn ../share/terminfo terminfo # ...to subdir of another dir ln -sn libdangle.lib.1 libdangle.lib # ...dangling link popd >/dev/null conda-package-handling-2.3.0/tests/recipes/cph_test_data/meta.yaml000066400000000000000000000001121463012743000251550ustar00rootroot00000000000000package: name: cph_test_data version: 0.0.1 build: noarch: generic conda-package-handling-2.3.0/tests/test_api.py000066400000000000000000000451101463012743000213000ustar00rootroot00000000000000import json import os import pathlib import platform import shutil import sys import tarfile import time import zipfile from datetime import datetime from tempfile import TemporaryDirectory import pytest import conda_package_handling import conda_package_handling.tarball from conda_package_handling import api, exceptions this_dir = os.path.dirname(__file__) data_dir = os.path.join(this_dir, "data") version_file = pathlib.Path(this_dir).parent / "src" / "conda_package_handling" / "__init__.py" test_package_name = "mock-2.0.0-py37_1000" test_package_name_2 = "cph_test_data-0.0.1-0" @pytest.mark.skipif( bool(os.environ.get("GITHUB_ACTIONS", False)), reason="Fails on GitHub Actions" ) @pytest.mark.skipif(not version_file.exists(), reason=f"Could not find {version_file}") def test_correct_version(): """ Prevent accidentally running tests against a globally installed different version. """ assert conda_package_handling.__version__ in version_file.read_text() def test_api_extract_tarball_implicit_path(testing_workdir): tarfile = os.path.join(data_dir, test_package_name + ".tar.bz2") local_tarfile = os.path.join(testing_workdir, os.path.basename(tarfile)) shutil.copy2(tarfile, local_tarfile) api.extract(local_tarfile) assert os.path.isfile(os.path.join(testing_workdir, test_package_name, "info", "index.json")) def test_api_tarball_details(testing_workdir): tarfile = os.path.join(data_dir, test_package_name + ".tar.bz2") results = api.get_pkg_details(tarfile) assert results["size"] == 106576 assert results["md5"] == "0f9cce120a73803a70abb14bd4d4900b" assert results["sha256"] == "34c659b0fdc53d28ae721fd5717446fb8abebb1016794bd61e25937853f4c29c" def test_api_conda_v2_details(testing_workdir): condafile = os.path.join(data_dir, test_package_name + ".conda") results = api.get_pkg_details(condafile) assert results["size"] == 113421 assert results["sha256"] == "181ec44eb7b06ebb833eae845bcc466ad96474be1f33ee55cab7ac1b0fdbbfa3" assert results["md5"] == "23c226430e35a3bd994db6c36b9ac8ae" def test_api_extract_tarball_explicit_path(testing_workdir): tarfile = os.path.join(data_dir, test_package_name + ".tar.bz2") local_tarfile = os.path.join(testing_workdir, os.path.basename(tarfile)) shutil.copy2(tarfile, local_tarfile) api.extract(local_tarfile, "manual_path") assert os.path.isfile(os.path.join(testing_workdir, "manual_path", "info", "index.json")) def test_api_extract_conda_v2_implicit_path(testing_workdir): condafile = os.path.join(data_dir, test_package_name + ".conda") local_condafile = os.path.join(testing_workdir, os.path.basename(condafile)) shutil.copy2(condafile, local_condafile) api.extract(local_condafile) assert os.path.isfile(os.path.join(testing_workdir, test_package_name, "info", "index.json")) def test_api_extract_conda_v2_no_destdir_relative_path(testing_workdir): cwd = os.getcwd() os.chdir(testing_workdir) try: condafile = os.path.join(data_dir, test_package_name + ".conda") local_condafile = os.path.join(testing_workdir, os.path.basename(condafile)) shutil.copy2(condafile, local_condafile) condafile = os.path.basename(local_condafile) assert os.path.exists(condafile) # cli passes dest=None, prefix=None api.extract(condafile, None, prefix=None) finally: os.chdir(cwd) def test_api_extract_conda_v2_explicit_path(testing_workdir): condafile = os.path.join(data_dir, test_package_name + ".conda") local_condafile = os.path.join(testing_workdir, os.path.basename(condafile)) shutil.copy2(condafile, local_condafile) api.extract(condafile, "manual_path") assert os.path.isfile(os.path.join(testing_workdir, "manual_path", "info", "index.json")) def test_api_extract_conda_v2_explicit_path_prefix(testing_workdir): tarfile = os.path.join(data_dir, test_package_name + ".conda") api.extract(tarfile, prefix=os.path.join(testing_workdir, "folder")) assert os.path.isfile( os.path.join(testing_workdir, "folder", test_package_name, "info", "index.json") ) api.extract(tarfile, dest_dir="steve", prefix=os.path.join(testing_workdir, "folder")) assert os.path.isfile(os.path.join(testing_workdir, "folder", "steve", "info", "index.json")) def test_api_extract_dest_dir_and_prefix_both_abs_raises(): tarfile = os.path.join(data_dir, test_package_name + ".conda") with pytest.raises(ValueError): api.extract(tarfile, prefix=os.path.dirname(tarfile), dest_dir=os.path.dirname(tarfile)) def test_api_extract_info_conda_v2(testing_workdir): condafile = os.path.join(data_dir, test_package_name + ".conda") local_condafile = os.path.join(testing_workdir, os.path.basename(condafile)) shutil.copy2(condafile, local_condafile) api.extract(local_condafile, "manual_path", components="info") assert os.path.isfile(os.path.join(testing_workdir, "manual_path", "info", "index.json")) assert not os.path.isdir(os.path.join(testing_workdir, "manual_path", "lib")) def check_conda_v2_metadata(condafile): with zipfile.ZipFile(condafile) as zf: d = json.loads(zf.read("metadata.json")) assert d["conda_pkg_format_version"] == 2 def test_api_transmute_tarball_to_conda_v2(testing_workdir): tarfile = os.path.join(data_dir, test_package_name + ".tar.bz2") # lower compress level makes the test run much faster, even 15 is much # better than 22 errors = api.transmute(tarfile, ".conda", testing_workdir, zstd_compress_level=3) assert not errors condafile = os.path.join(testing_workdir, test_package_name + ".conda") assert os.path.isfile(condafile) check_conda_v2_metadata(condafile) def test_api_transmute_tarball_info_sorts_first(testing_workdir): test_packages = [test_package_name] test_packages_with_symlinks = [test_package_name_2] if sys.platform != "win32": test_packages += test_packages_with_symlinks for test_package in test_packages: test_file = os.path.join(data_dir, test_package + ".tar.bz2") # transmute/convert doesn't re-sort files; extract to folder. api.extract(test_file, testing_workdir) out_fn = os.path.join(testing_workdir, test_package + ".tar.bz2") out = api.create(testing_workdir, None, out_fn) assert out == out_fn # info must be first with tarfile.open(out_fn, "r:bz2") as repacked: info_seen = False not_info_seen = False for member in repacked: if member.name.startswith("info"): assert ( not_info_seen is False ), f"{test_package} package info/ must sort first, " f"but {[m.name for m in repacked.getmembers()]}" info_seen = True else: not_info_seen = True assert info_seen, "package had no info/ files" @pytest.mark.skipif(sys.platform == "win32", reason="windows and symlinks are not great") def test_api_transmute_to_conda_v2_contents(testing_workdir): def _walk(path): for entry in os.scandir(path): if entry.is_dir(follow_symlinks=False): yield from _walk(entry.path) continue yield entry tar_path = os.path.join(data_dir, test_package_name_2 + ".tar.bz2") conda_path = os.path.join(testing_workdir, test_package_name_2 + ".conda") api.transmute(tar_path, ".conda", testing_workdir, zstd_compress_level=3) # Verify original contents were all put in the right place pkg_tarbz2 = tarfile.open(tar_path, mode="r:bz2") info_items = [item for item in pkg_tarbz2.getmembers() if item.path.startswith("info/")] pkg_items = [item for item in pkg_tarbz2.getmembers() if not item.path.startswith("info/")] errors = [] for component, expected in (("info", info_items), ("pkg", pkg_items)): with TemporaryDirectory() as root: api.extract(conda_path, root, components=component) contents = { os.path.relpath(entry.path, root): { "is_symlink": entry.is_symlink(), "target": os.readlink(entry.path) if entry.is_symlink() else None, } for entry in _walk(root) } for item in expected: if item.path not in contents: errors.append(f"'{item.path}' not found in {component} contents") continue ct = contents.pop(item.path) if item.issym(): if not ct["is_symlink"] or ct["target"] != item.linkname: errors.append( f"{item.name} -> {item.linkname} incorrect in {component} contents" ) elif not item.isfile(): # Raise an exception rather than appending to `errors` # because getting to this point is an indication that our # test data (i.e., .tar.bz2 package) is corrupt, rather # than the `.transmute` function having problems (which is # what `errors` is meant to track). For context, conda # packages should only contain regular files and symlinks. raise ValueError(f"unexpected item '{item.path}' in test .tar.bz2") if contents: errors.append(f"extra files [{', '.join(contents)}] in {component} contents") assert not errors def test_api_transmute_conda_v2_to_tarball(testing_workdir): condafile = os.path.join(data_dir, test_package_name + ".conda") outfile = pathlib.Path(testing_workdir, test_package_name + ".tar.bz2") # one quiet=True in the test suite for coverage api.transmute(condafile, ".tar.bz2", testing_workdir, quiet=True) assert outfile.is_file() # test that no-force keeps file, and force overwrites file for force in False, True: mtime = outfile.stat().st_mtime time.sleep(2 if platform.platform() == "Windows" else 0) api.transmute(condafile, ".tar.bz2", testing_workdir, force=force) mtime2 = outfile.stat().st_mtime assert (mtime2 == mtime) != force def test_warning_when_bundling_no_metadata(testing_workdir): pass @pytest.mark.skipif(sys.platform == "win32", reason="windows and symlinks are not great") def test_create_package_with_uncommon_conditions_captures_all_content(testing_workdir): os.makedirs("src/a_folder") os.makedirs("src/empty_folder") os.makedirs("src/symlink_stuff") with open("src/a_folder/text_file", "w") as f: f.write("weee") open("src/empty_file", "w").close() os.link("src/a_folder/text_file", "src/a_folder/hardlink_to_text_file") os.symlink("../a_folder", "src/symlink_stuff/symlink_to_a") os.symlink("../empty_file", "src/symlink_stuff/symlink_to_empty_file") os.symlink("../a_folder/text_file", "src/symlink_stuff/symlink_to_text_file") with tarfile.open("pinkie.tar.bz2", "w:bz2") as tf: def add(source, target): tf.add(source, target, recursive=False) add("src/empty_folder", "empty_folder") add("src/empty_file", "empty_file") add("src/a_folder", "a_folder") add("src/a_folder/text_file", "a_folder/text_file") add("src/a_folder/hardlink_to_text_file", "a_folder/hardlink_to_text_file") add("src/symlink_stuff/symlink_to_a", "symlink_stuff/symlink_to_a") add( "src/symlink_stuff/symlink_to_empty_file", "symlink_stuff/symlink_to_empty_file", ) add( "src/symlink_stuff/symlink_to_text_file", "symlink_stuff/symlink_to_text_file", ) api.create("src", None, "thebrain.tar.bz2") # test against both archives created manually and those created by cph. # They should be equal in all ways. for fn in ("pinkie.tar.bz2", "thebrain.tar.bz2"): api.extract(fn) target_dir = fn[:-8] flist = [ "empty_folder", "empty_file", "a_folder/text_file", "a_folder/hardlink_to_text_file", "symlink_stuff/symlink_to_a", "symlink_stuff/symlink_to_text_file", "symlink_stuff/symlink_to_empty_file", ] # no symlinks on windows if sys.platform != "win32": # not directly included but checked symlink flist.append("symlink_stuff/symlink_to_a/text_file") missing_content = [] for f in flist: path_that_should_be_there = os.path.join(testing_workdir, target_dir, f) if not ( os.path.exists(path_that_should_be_there) or os.path.lexists(path_that_should_be_there) # noqa ): missing_content.append(f) if missing_content: print("missing files in output package") print(missing_content) sys.exit(1) # hardlinks should be preserved, but they're currently not with libarchive # hardlinked_file = os.path.join(testing_workdir, target_dir, 'a_folder/text_file') # stat = os.stat(hardlinked_file) # assert stat.st_nlink == 2 hardlinked_file = os.path.join(testing_workdir, target_dir, "empty_file") stat = os.stat(hardlinked_file) if sys.platform != "win32": assert stat.st_nlink == 1 @pytest.mark.skipif( datetime.now() <= datetime(2020, 12, 1), reason="Don't understand why this doesn't behave. Punt.", ) def test_secure_refusal_to_extract_abs_paths(testing_workdir): with tarfile.open("pinkie.tar.bz2", "w:bz2") as tf: open("thebrain", "w").close() tf.add(os.path.join(testing_workdir, "thebrain"), "/naughty/abs_path") try: tf.getmember("/naughty/abs_path") except KeyError: pytest.skip("Tar implementation does not generate unsafe paths in archive.") with pytest.raises(api.InvalidArchiveError): api.extract("pinkie.tar.bz2") def tests_secure_refusal_to_extract_dotdot(testing_workdir): with tarfile.open("pinkie.tar.bz2", "w:bz2") as tf: open("thebrain", "w").close() tf.add(os.path.join(testing_workdir, "thebrain"), "../naughty/abs_path") with pytest.raises(api.InvalidArchiveError): api.extract("pinkie.tar.bz2") def test_api_bad_filename(testing_workdir): with pytest.raises(ValueError): api.extract("pinkie.rar", testing_workdir) def test_details_bad_extension(): with pytest.raises(ValueError): # TODO this function should not exist api.get_pkg_details("pinkie.rar") def test_convert_bad_extension(testing_workdir): api._convert("pinkie.rar", ".conda", testing_workdir) def test_convert_keyerror(tmpdir, mocker): tarfile = os.path.join(data_dir, test_package_name + ".tar.bz2") mocker.patch( "conda_package_streaming.transmute.transmute", side_effect=KeyboardInterrupt(), ) # interrupted before ".conda" was created with pytest.raises(KeyboardInterrupt): api._convert(tarfile, ".conda", tmpdir) def create_file_and_raise(*args, **kwargs): out_fn = pathlib.Path(tmpdir, pathlib.Path(tarfile[: -len(".tar.bz2")] + ".conda").name) print("out fn", out_fn) out_fn.write_text("") raise KeyboardInterrupt() mocker.patch("conda_package_streaming.transmute.transmute", side_effect=create_file_and_raise) # interrupted after ".conda" was created with pytest.raises(KeyboardInterrupt): api._convert(tarfile, ".conda", tmpdir) def test_create_filelist(tmpdir, mocker): # another bad API, tested for coverage filelist = pathlib.Path(tmpdir, "filelist.txt") filelist.write_text("\n".join(["filelist.txt", "anotherfile"])) # when looking for filelist-not-found.txt with pytest.raises(FileNotFoundError): api.create(str(tmpdir), "filelist-not-found.txt", str(tmpdir / "newconda.conda")) # when adding anotherfile with pytest.raises(FileNotFoundError): api.create(str(tmpdir), str(filelist), str(tmpdir / "newconda.conda")) # unrecognized target extension with pytest.raises(ValueError): api.create(str(tmpdir), str(filelist), str(tmpdir / "newpackage.rar")) def create_file_and_raise(prefix, file_list, out_fn, *args, **kwargs): pathlib.Path(prefix, out_fn).write_text("") raise KeyboardInterrupt() mocker.patch( "conda_package_handling.conda_fmt.CondaFormat_v2.create", side_effect=create_file_and_raise, ) # failure inside inner create() with pytest.raises(KeyboardInterrupt): api.create(str(tmpdir), str(filelist), str(tmpdir / "newpackage.conda")) def test_api_transmute_fail_validation(tmpdir, mocker): package = os.path.join(data_dir, test_package_name + ".conda") # this code is only called for .conda -> .tar.bz2; a streaming validate for # .tar.bz2 -> .conda would be a good idea. mocker.patch( "conda_package_handling.validate.validate_converted_files_match_streaming", return_value=(str(package), {"missing-file.txt"}, {"mismatched-size.txt"}), ) errors = api.transmute(package, ".tar.bz2", tmpdir) assert errors def test_api_transmute_fail_validation_to_conda(tmpdir, mocker): package = os.path.join(data_dir, test_package_name + ".tar.bz2") mocker.patch( "conda_package_handling.validate.validate_converted_files_match_streaming", return_value=(str(package), {"missing-file.txt"}, {"mismatched-size.txt"}), ) errors = api.transmute(package, ".conda", tmpdir, zstd_compress_level=3) assert errors def test_api_transmute_fail_validation_2(tmpdir, mocker): package = os.path.join(data_dir, test_package_name + ".conda") tmptarfile = tmpdir / pathlib.Path(package).name shutil.copy(package, tmptarfile) mocker.patch( "conda_package_handling.validate.validate_converted_files_match_streaming", side_effect=Exception("not today"), ) # run with out_folder=None errors = api.transmute(str(tmptarfile), ".tar.bz2") assert errors def test_api_translates_exception(mocker, tmpdir): from conda_package_streaming.extract import exceptions as cps_exceptions tarfile = os.path.join(data_dir, test_package_name + ".tar.bz2") # translates their exception to our exception of the same name mocker.patch( "conda_package_streaming.package_streaming.stream_conda_component", side_effect=cps_exceptions.CaseInsensitiveFileSystemError(), ) # should this be exported from the api or inherit from InvalidArchiveError? with pytest.raises(exceptions.CaseInsensitiveFileSystemError): api.extract(tarfile, tmpdir) conda-package-handling-2.3.0/tests/test_cli.py000066400000000000000000000044221463012743000212770ustar00rootroot00000000000000import os from pathlib import Path import pytest import conda_package_handling.cli as cli from .test_api import data_dir, test_package_name def test_cli(tmpdir, mocker): """ Code coverage for the cli. """ for command in [ [ "x", str(Path(data_dir, test_package_name + ".tar.bz2")), f"--prefix={tmpdir}", ], [ "x", str(Path(data_dir, test_package_name + ".conda")), "--info", f"--prefix={tmpdir}", ], [ "c", str(Path(tmpdir, test_package_name)), ".tar.bz2", f"--out-folder={tmpdir}", ], ]: cli.main(args=command) # XXX difficult to get to this error handling code through the actual CLI; # for example, a .tar.bz2 that can't be extracted raises OSError instead of # returning errors. Designed for .tar.bz2 -> .conda conversions that somehow # omit files? mocker.patch( "conda_package_handling.api.transmute", return_value=set("that is why you fail".split()), ) with pytest.raises(SystemExit): command = [ "t", str(Path(data_dir, test_package_name + ".tar.bz2")), ".conda", f"--out-folder={tmpdir}", ] cli.main(args=command) def test_import_main(): """ e.g. python -m conda_package_handling """ with pytest.raises(SystemExit): import conda_package_handling.__main__ # noqa @pytest.mark.parametrize( "artifact,n_files", [("mock-2.0.0-py37_1000.conda", 43), ("mock-2.0.0-py37_1000.tar.bz2", 43)], ) def test_list(artifact, n_files, capsys): "Integration test to ensure `cph list` works correctly." cli.main(["list", os.path.relpath(os.path.join(data_dir, artifact), os.getcwd())]) stdout, stderr = capsys.readouterr() assert n_files == sum(bool(line.strip()) for line in stdout.splitlines()) # test verbose flag cli.main( [ "list", "--verbose", os.path.join(data_dir, artifact), ] ) stdout, stderr = capsys.readouterr() assert n_files == sum(bool(line.strip()) for line in stdout.splitlines()) with pytest.raises(ValueError): cli.main(["list", "setup.py"]) conda-package-handling-2.3.0/tests/test_degraded.py000066400000000000000000000035441463012743000222730ustar00rootroot00000000000000""" Test conda-package-handling can work in `.tar.bz2`-only mode if zstandard is not available. (Giving the user a chance to immediately install zstandard.) """ import importlib import subprocess import sys import warnings def test_degraded(): try: sys.modules["zstandard"] = None # type: ignore sys.modules["conda_package_streaming.transmute"] = None # type: ignore sys.modules["conda_package_handling.conda_fmt"] = None # type: ignore # this is only testing conda_package_handling's code, and does not test # that conda_package_streaming works without zstandard. with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") # Ensure warnings are sent import conda_package_handling.api importlib.reload(conda_package_handling.api) assert len(w) == 1 assert issubclass(w[-1].category, UserWarning) assert "zstandard" in str(w[-1].message) assert conda_package_handling.api.libarchive_enabled is False finally: sys.modules.pop("zstandard", None) sys.modules.pop("conda_package_handling.conda_fmt", None) sys.modules.pop("conda_package_streaming.transmute", None) with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") # Ensure warnings are sent import conda_package_handling.api importlib.reload(conda_package_handling.api) assert len(w) == 0 assert conda_package_handling.api.libarchive_enabled is True def test_degraded_subprocess(): """ More reliable way to mock 'zstandard not available' """ subprocess.check_call( [ sys.executable, "-c", "import sys; sys.modules['zstandard'] = None; import conda_package_handling.api", ] ) conda-package-handling-2.3.0/tests/test_interface.py000066400000000000000000000032451463012743000224720ustar00rootroot00000000000000""" Test format classes. (Some of their code is unreachable through api.py) """ import os from pathlib import Path import pytest from conda_package_handling.conda_fmt import CondaFormat_v2 from conda_package_handling.tarball import CondaTarBZ2 from .test_api import data_dir, test_package_name TEST_CONDA = Path(data_dir, test_package_name + ".conda") TEST_TARBZ = Path(data_dir, test_package_name + ".tar.bz2") def test_extract_create(tmpdir): for format, infile, outfile in ( (CondaFormat_v2, TEST_CONDA, "newmock.conda"), (CondaTarBZ2, TEST_TARBZ, "newmock.tar.bz2"), ): both_path = Path(tmpdir, f"mkdirs-{outfile.split('.', 1)[-1]}") # these old APIs don't guarantee Path-like's format.extract(infile, str(both_path)) assert sorted(os.listdir(both_path)) == sorted(["lib", "info"]) if format == CondaFormat_v2: info_path = Path(tmpdir, "info-only") format.extract_info(TEST_CONDA, str(info_path)) # type: ignore assert os.listdir(info_path) == ["info"] filelist = [str(p.relative_to(both_path)) for p in both_path.rglob("*")] format.create( both_path, filelist, tmpdir / outfile, # compression_tuple is for libarchive compatibility. Instead, pass # compressor=(compressor factory function) compression_tuple=(".tar.zst", "zstd", "zstd:compression-level=1"), ) assert (tmpdir / outfile).exists() with pytest.raises(ValueError): CondaFormat_v2.create( "", [], "", compressor=True, compression_tuple=("1", "2", "3") # type: ignore ) conda-package-handling-2.3.0/tests/test_utils.py000066400000000000000000000030621463012743000216670ustar00rootroot00000000000000import os import sys from errno import EACCES, ENOENT, EPERM, EROFS import pytest from conda_package_handling import utils def test_rm_rf_file(testing_workdir): with open("dummy", "w") as f: f.write("weeee") utils.rm_rf("dummy") with open("dummy", "w") as f: f.write("weeee") utils.rm_rf(os.path.join(testing_workdir, "dummy")) @pytest.mark.parametrize("errno", (ENOENT, EACCES, EPERM, EROFS)) def test_rename_to_trash(testing_workdir, mocker, errno): unlink = mocker.patch("os.unlink") unlink.side_effect = EnvironmentError(errno, "") with open("dummy", "w") as f: f.write("weeee") utils.unlink_or_rename_to_trash("dummy") assert os.path.isfile("dummy.conda_trash") # force a second error for the inner rename try (after unlink fails) if sys.platform == "win32": with open("dummy", "w") as f: f.write("weeee") mocker.patch("os.rename") unlink.side_effect = EnvironmentError(errno, "") utils.unlink_or_rename_to_trash("dummy") assert os.path.isfile("dummy.conda_trash") def test_delete_trash(testing_workdir, mocker): isdir = mocker.patch("conda_package_handling.utils.isdir") isdir.return_value = True lexists = mocker.patch("conda_package_handling.utils.lexists") lexists.return_value = False mocker.patch("conda_package_handling.utils.rmdir") os.makedirs("folder") with open("folder/dummy.conda_trash", "w") as f: f.write("weeee") utils.rm_rf("folder") assert not os.path.isfile("folder/dummy.conda_trash") conda-package-handling-2.3.0/tests/test_validate.py000066400000000000000000000033331463012743000223210ustar00rootroot00000000000000from pathlib import Path from conda_package_handling.validate import validate_converted_files_match_streaming from .test_api import data_dir, test_package_name, test_package_name_2 def test_validate_streaming(): assert validate_converted_files_match_streaming( Path(data_dir, test_package_name + ".conda"), Path(data_dir, test_package_name + ".tar.bz2"), strict=False, ) == (Path(data_dir, test_package_name + ".conda"), [], []) # old converted files don't match uname, gname, mtime assert validate_converted_files_match_streaming( Path(data_dir, test_package_name + ".conda"), Path(data_dir, test_package_name + ".tar.bz2"), strict=True, ) != (Path(data_dir, test_package_name + ".conda"), [], []) src, missing, mismatched = validate_converted_files_match_streaming( Path(data_dir, test_package_name_2 + ".tar.bz2"), Path(data_dir, test_package_name + ".conda"), strict=False, ) assert src == Path(data_dir, test_package_name_2 + ".tar.bz2") # not that critical exactly what mismatches; we are comparing separate packages assert len(missing) == 47 assert mismatched == [ "info/hash_input.json", "info/files", "info/index.json", "info/paths.json", "info/about.json", "info/git", "info/recipe/meta.yaml", "info/recipe/conda_build_config.yaml", "info/recipe/meta.yaml.template", "info/hash_input.json", "info/index.json", "info/files", "info/about.json", "info/paths.json", "info/git", "info/recipe/meta.yaml.template", "info/recipe/conda_build_config.yaml", "info/recipe/meta.yaml", ]