pax_global_header00006660000000000000000000000064147211134030014506gustar00rootroot0000000000000052 comment=15ae571c648700206974403bb8db384ee67ae425 organize-3.3.0/000077500000000000000000000000001472111340300133275ustar00rootroot00000000000000organize-3.3.0/.dockerignore000066400000000000000000000002041472111340300157770ustar00rootroot00000000000000# ignore everything * # except pyproject files !poetry.lock !pyproject.toml !README.md # and python scripts !**/*.py !**/py.typed organize-3.3.0/.editorconfig000066400000000000000000000005231472111340300160040ustar00rootroot00000000000000# EditorConfig is awesome: http://EditorConfig.org # top-most EditorConfig file root = true [*] end_of_line = lf insert_final_newline = true charset = utf-8 indent_style = space indent_size = 4 [{*.bat,Makefile}] indent_style = tab [*.rst] indent_size = 2 [{*.yml,*.yaml}] indent_size = 2 [*.md] indent_size = 2 indent_style = space organize-3.3.0/.github/000077500000000000000000000000001472111340300146675ustar00rootroot00000000000000organize-3.3.0/.github/FUNDING.yml000066400000000000000000000010701472111340300165020ustar00rootroot00000000000000# These are supported funding model platforms github: tfeldmann patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: tfeldmann tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username custom: ["paypal.me/tfeldmann42"] organize-3.3.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001472111340300170525ustar00rootroot00000000000000organize-3.3.0/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000006471472111340300215530ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve title: "" labels: bug assignees: "" --- **Describe the bug** A clear and concise description of what the bug is. **Screenshots** If applicable, add screenshots to help explain your problem. **Environment (please complete the following information):** - OS: - Output of `organize --version`: **Your config file** ```yaml # paste your config file here ``` organize-3.3.0/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000011401472111340300225730ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project title: '' labels: feature request assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. organize-3.3.0/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000010341472111340300204660ustar00rootroot00000000000000 ## Change Summary ## Related issue number ## Checklist - [ ] Tests for the changes exist and pass on CI - [ ] Documentation reflects the changes where applicable - [ ] Change is documented in CHANGELOG.md (if applicable) - [ ] My PR is ready to review organize-3.3.0/.github/dependabot.yml000066400000000000000000000002651472111340300175220ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: pip directory: / schedule: interval: monthly - package-ecosystem: github-actions directory: / schedule: interval: monthly organize-3.3.0/.github/workflows/000077500000000000000000000000001472111340300167245ustar00rootroot00000000000000organize-3.3.0/.github/workflows/publish-docker-image.yml000066400000000000000000000023271472111340300234460ustar00rootroot00000000000000# This workflow uses actions that are not certified by GitHub. # They are provided by a third-party and are governed by # separate terms of service, privacy policy, and support # documentation. # GitHub recommends pinning actions to a commit SHA. # To get a newer version, you will need to update the SHA. # You can also reference a tag or branch, but the action may change without warning. name: Publish Docker image on: release: types: [published] jobs: push_to_registry: name: Push Docker image to Docker Hub runs-on: ubuntu-latest steps: - name: Check out the repo uses: actions/checkout@v4 - name: Log in to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Extract metadata (tags, labels) for Docker id: meta uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 with: images: tfeldmann/organize - name: Build and push Docker image uses: docker/build-push-action@v5 with: push: true tags: tfeldmann/organize:latest labels: ${{ steps.meta.outputs.labels }} organize-3.3.0/.github/workflows/tests.yml000066400000000000000000000022621472111340300206130ustar00rootroot00000000000000name: tests on: push: paths-ignore: - "docs/**" - "*.md" branches: - main pull_request: branches: - main workflow_dispatch: # https://github.com/python-poetry/poetry/issues/8623 env: PYTHON_KEYRING_BACKEND: keyring.backends.null.Keyring jobs: build: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] python-version: ["3.9", "3.10", "3.11", "3.12"] fail-fast: false steps: - name: Checkout repo uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v5 with: python-version: "${{ matrix.python-version }}" - name: Setup Environment env: PYTHON_KEYRING_BACKEND: keyring.backends.null.Keyring run: | python3 -m pip install -U pip setuptools python3 -m pip install poetry==1.7.1 poetry install --with=dev - name: Version info run: | poetry run python main.py --version - name: Test with pytest run: | poetry run pytest - name: Check with MyPy run: | poetry run mypy . organize-3.3.0/.gitignore000066400000000000000000000041561472111340300153250ustar00rootroot00000000000000.configs .pytest_cache/ **/.ruff_cache/ # Created by https://www.gitignore.io/api/python ### Python ### # 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 # 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/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # 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/ # End of https://www.gitignore.io/api/python # Created by https://www.gitignore.io/api/visualstudiocode ### VisualStudioCode ### .vscode .history # End of https://www.gitignore.io/api/visualstudiocode .idea # Created by https://www.toptal.com/developers/gitignore/api/macos # Edit at https://www.toptal.com/developers/gitignore?templates=macos ### macOS ### # General .DS_Store .AppleDouble .LSOverride # Icon must end with two \r Icon # Thumbnails ._* # Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns .com.apple.timemachine.donotpresent # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk ### macOS Patch ### # iCloud generated files *.icloud # End of https://www.toptal.com/developers/gitignore/api/macos organize-3.3.0/.pre-commit-config.yaml000066400000000000000000000011251472111340300176070ustar00rootroot00000000000000repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: - id: check-yaml - id: end-of-file-fixer - id: mixed-line-ending args: - --fix=lf - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.8.0 hooks: # lint and fix - id: ruff args: [--fix] # sort imports - id: ruff args: [--select=I, --fix] # format - id: ruff-format - repo: https://github.com/python-poetry/poetry rev: "1.8.0" hooks: - id: poetry-check organize-3.3.0/.readthedocs.yml000066400000000000000000000007261472111340300164220ustar00rootroot00000000000000# .readthedocs.yaml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Set the version of Python and other tools you might need build: os: ubuntu-20.04 tools: python: "3.9" mkdocs: configuration: mkdocs.yml # Optionally declare the Python requirements required to build your docs python: install: - method: pip path: . extra_requirements: - docs organize-3.3.0/CHANGELOG.md000066400000000000000000000322231472111340300151420ustar00rootroot00000000000000# Changelog ## [Unreleased] ## v3.3.0 (2024-11-25) - Added a new conflict mode `deduplicate` which skips duplicate files and renames non-duplicates (thanks @TheExistingOne). - The `exif` filter now supports extracting metadata from non-image files such as EPUB or PDF files. - Loosen the pdfminer-six dependency version constraint for easier NixOS packaging. - Fixes encoding issues in windows (thanks @Alimektor). ## v3.2.5 (2024-07-09) - Fixes a bug where some location options did not accept yaml aliases (#390, thanks for reporting @zany130). ## v3.2.4 (2024-07-07) - Fixes a bug preventing organize from starting (thanks @feather42). - Fixes a bug where ignoring failing shell commands would not work (thanks @florianklumb). ## v3.2.3 (2024-03-28) - Improves the logic of finding and creating config files. - Fixes a bug where config files in XDG_CONFIG_HOME are not found (#371). - Fixes a bug in the `relative_path` parameter crashing on actions after moving files (#372). ## v3.2.2 (2024-03-04) - Fixes an problem where the `organize new` command fails to create a new config file. ## v3.2.1 (2024-02-23) - Files and folders are now handled in natural sort order (#354). ## v3.2.0 (2024-02-19) - Integrated `.docx`, `.pdf` and various raw text parsers into `filecontent` filter. - Removed `textract` and ~50 MB of dependencies as they are no longer required. - Full Python 3.12 support - Add support for piping in a config file from STDIN (`organize run --stdin < file.yml`) **Important:** You may have to adjust your `filecontent` regexes. The output should be a bit cleaner now. ## v3.1.2 (2024-02-16) - Fixes a validation error where correctly defined actions were not accepted in Python 3.12.2. ## v3.1.1 (2024-02-11) - Fixes the `organize show` command which broke in v3.1.0. ## v3.1.0 (2024-02-04) - Add a new output format `errorsonly` which only shows output if an error occured. - Fixes a bug where messages and paths containing brackets where not printed correctly (#348, thanks @kwbr!) ## v3.0.1 (2024-01-12) - Fixes a bug where Quicktime and mp4 files return no Exif data. (#313, @jleatham thanks for debugging!) - Fixes a bug where `exlude_dirs` are not correctly excluded. (#339) ## v3.0.0 (2024-01-05) - Supports `exiftool` in the `exif` filter by setting the `ORGANIZE_EXIFTOOL_PATH` env var. (thanks to @HernandoR for working on qtff support) - Fixes the `min_depth` location parameter. (thanks to @danielklim for working on this) ## v3.0.0a2 (2024-01-02) - Fix bug on first run of `organize new`. Create the organize config directory if it does not exist. (thanks @white-gecko) - Adds action `hardlink`. ## v3.0.0a1 (2024-01-01) - Default to UTF-8 encoding when reading and writing config files for windows users who don't use python in UTF-8 mode (env variable `PYTHONUTF8=1`). - `write`-action: Allow setting the text encoding. ## v3.0.0a0 (2023-12-31) - New action: `write` to write lines into a file. - New filter: `date_lastused` (macOS only). - You can now specify the timezone in all time based filters. - Removed hidden (deprecated) CLI option `--config-file`. - Lots of new tests and some bugfixes. - `exif` filter now supports the simplematch syntax. - Placeholder`{now}` must be `{now()}` now. - Multiple `path`s per location are now supported. - Locations now support a `min_depth` option - Duplicate filter: `detect_original_by` now supports `last_seen`. - New command line interface (added `new`, `show` and `list` commands). - `JSONL` output (`organize run --format=JSONL`) - `move` and `copy` now intelligently autodetect if you mean to move to a folder (This autodetection can be deactivated). - `copy` action: You can now specify whether you want to continue with the original or with the copy. - Completely removes the `pyfilesystem` dependency. - At least a 4x speed up. Often more than 10x. ## v2.4.3 (2023-10-14) - Modified filter: `exif`. Enabled datetime fields on exif data (Issue #266) (Thanks @FlorianFritz) - Support Exif data from Huawei and Honer phones (Thanks @HernandoR) ## v2.4.2 (2023-08-25) - Fix reading exif data for HEIC images (Issue #267) ## v2.4.1 (2023-08-25) - Fix unicode bug in logging (Issue #294) (Thanks @xdhmoore) - Updated dependencies (Thanks @gaby) - Removed support for python 3.7 (EOL - June 2023) (Thanks @gaby) ## v2.4.0 (2022-09-05) - New action: `write`. - New filter: `date_lastused` (macOS only). - Conflict resolution renaming now starts with 2 instead of 1. - Add support for FS urls as path to the config file and working dir (both in the CLI and ORGANIZE_CONFIG environment variable). - Removed hidden (deprecated) CLI option `--config-file`. - Lots of new tests and some bugfixes. ## v2.3.0 (2022-07-26) - New filter: `macos_tags` (macOS only). - Ignore broken symlinks (Issue #202) ## v2.2.0 (2022-03-31) - Tag support (#199) to run subsets of rules in your config. ## v2.1.2 (2022-02-13) - Hotfix for `filecontent` filter. ## v2.1.1 (2022-02-13) - `filecontent` filter: Fixes bug #188. - Bugfix for #185 and #184. ## v2.1.0 (2022-02-11) - Added filter `date_added` (macOS only) - `created` filter now supports gnu coreutils stat utility for birthtime detection - refactored time based filters into a common class ## v2.0.9 (2022-02-10) - `shell` shows a message when code is not run in simulation - `shell` add options `simulation_output` and `simulation_returncode` - fixes a bug where location options are applied to other locations as well - `created` filter now falls back to using the stat utility on linux systems where the birthtime is not included in `os.stat`. ## v2.0.8 (2022-02-09) - Bugfix `shell` for real. ## v2.0.7 (2022-02-09) - Bugfix for `shell`. ## v2.0.6 (2022-02-09) - Speed up moving files. - `shell` action: Run command through the user's shell. ## v2.0.5 (2022-02-08) - Fixed the migration message and docs URL ## v2.0.4 (2022-02-08) - exclude_dir, system_exclude_dirs, exclude_files, system_exclude_files, filter and filter_dirs now accept single strings. - Fixed a bug in the name filter ## v2.0.3 (2022-02-07) - Fixed typo: `system_exlude_files` ## v2.0.2 (2022-02-07) - Bugfix in env variable expansion in locations ## v2.0.1 (2022-02-07) - Small bugfix in `macos_tags` action. - Bugfix in the migration detection. ## v2.0.0 (2022-02-07) This is a huge update with lots of improvements. Please backup all your important stuff before running and use the simulate option! [**Migration Guide**](docs/updating-from-v1.md) ### what's new - You can now [target directories](docs/rules.md#targeting-directories) with your rules (copying, renaming, etc a whole folder) - [Organize inside or between (S)FTP, S3 Buckets, Zip archives and many more](docs/locations.md#remote-filesystems-and-archives) (list of [available filesystems](https://www.pyfilesystem.org/page/index-of-filesystems/)). - [`max_depth`](docs/locations.md#location-options) setting when recursing into subfolders - Respects your rule order - safer, less magic, less surprises. - (organize v1 tried to be clever. v2 now works your config file from top to bottom) - [Jinja2 template engine for placeholders](docs/rules.md#templates-and-placeholders). - Instant start. (does not need to gather all the files before starting) - [Filters can now be excluded](docs/filters.md#how-to-exclude-filters). - [Filter modes](docs/rules.md#rule-options): `all`, `any` and `none`. - [Rule names](docs/rules.md#rule-options). - new conflict resolution settings in [`move`](docs/actions.md#move), [`copy`](docs/actions.md#copy) and [`rename`](docs/actions.md#rename) action: - Options are `skip`, `overwrite`, `trash`, `rename_new` or `rename_existing` - You can now define a custom `rename_template`. - The [`duplicate`](docs/filters.md#duplicate) now supports several options on how to distinguish between original and duplicate file. - The [`python`](docs/actions.md#python) action can now be run in simulation. - The [`shell`](docs/actions.md#shell) action now returns stdout and errorcode. - Added filter [`empty`](docs/filters.md#empty) - find empty files and folders - Added filter [`hash`](docs/filters.md#hash) - generate file hashes - Added action [`symlink`](docs/actions.md#symlink) - generate symlinks - Added action [`confirm`](docs/actions.md#confirm) - asks for confirmation - Many small fixes and improvements! ### changed - The `timezone` keyword for [`lastmodified`](docs/filters.md#lastmodified) and [`created`](docs/filters.md#created) was removed. The timezone is now the local timezone by default. - The `filesize` filter was renamed to [`size`](docs/filters.md#size) and can now be used to get directory sizes as well. - The `filename` filter was renamed to [`name`](docs/filters.md#name) and can now be used to get directory names as well. - The [`size`](docs/filters.md#size) filter now returns multiple formats ### removed - Glob syntax is gone from folders ([no longer needed](docs/locations.md)) - `"!"` folder exclude syntax is gone ([no longer needed](docs/locations.md)) ## v1.10.1 (2021-04-21) - Action `macos_tags` now supports colors and placeholders. - Show full expanded path if folder is not found. ## v1.10.0 (2021-04-20) - Add filter `mimetype` - Add action `macos_tags` - Support [`simplematch`](https://github.com/tfeldmann/simplematch) syntax in `lename`-filter. - Updated dependencies - Because installing `textract` is quite hard on some platforms it is now an optional dendency. Install it with `pip install organize-tool[textract]` - This version needs python 3.6 minimum. Some dependencies that were simply backports (thlib2, typing) are removed. - Add timezones in created and last_modified filters (Thank you, @win0err!) ## v1.9.1 (2020-11-10) - Add {env} variable - Add {now} variable ## v1.9 (2020-06-12) - Add filter `Duplicate`. ## v1.8.2 (2020-04-03) - Fix a bug in the filename filter config parsing algorithm with digits-only filenames. ## v1.8.1 (2020-03-28) - Flatten filter and action lists to allow enhanced config file configuration (Thanks to @rawdamedia!) - Add support for multiline content filters (Thanks to @zor-el!) ## v1.8.0 (2020-03-04) - Added action `Delete`. - Added filter `FileContent`. - Python 3.4 is officially deprecated and no longer supported. - `--config-file` command line option now supports `~` for user folder and expansion oenvironment variables - Added `years`, `months`, `weeks` and `seconds` parameter to filter `created` and `stmodified` ## v1.7.0 (2019-11-26) - Added filter `Exif` to filter by image exif data. - Placeholder variable properties are now case insensitve. ## v1.6.2 (2019-11-22) - Fix `Rename` action (`'PosixPath' object has no attribute 'items'`). - Use type hints everywhere. ## v1.6.1 (2019-10-25) - Shows a warning for missing folders instead of raising an exception. ## v1.6 (2019-08-19) - Added filter: `Python` - Added filter: `FileSize` - The organize module can now be run directly: `python3 -m organize` - Various code simplifications and speedups. - Fixes an issue with globstring file exclusion. - Remove `clint` dependency as it is no longer maintained. - Added various integration tests - The "~~ SIMULATION ~~"-banner now takes up the whole terminal width ## v1.5.3 (2019-08-01) - Filename filter now supports lists. ## v1.5.2 (2019-07-29) - Environment variables in folder pathes are now expanded (syntax `$name` or `${name}` a additionally `%name%` on windows). F example this allows the usage of e.g. `%public/Desktop%` in windows. ## v1.5.1 (2019-07-23) - New filter "Created" to filter by creation date. - Fixes issue #39 where globstrings don't work most of the time. - Integration test for issue #39 - Support indented config files ## v1.5 (2019-07-17) - Fixes issue #31 where the {path} variable always resolves to the source path - Updated dependencies - Exclude changelog and readme from published wheel ## v1.4.5 (2019-07-03) - Filter and Actions names are now case-insensitive ## v1.4.4 (2019-07-02) - Fixes issues #36 with umlauts in config file on windows ## v1.4.3 (2019-06-05) - Use safe YAML loader to fix a deprecation warning. (Thanks mope1!) - Better error message if a folder does not exist. (Again thanks mope1!) - Fix example code in documentation for LastModified filter. - Custom config file locations (given by cmd line argument or environment variable). - `config --debug` now shows the full path to the config file. ## v1.4.2 (2018-11-14) - Fixes a bug with command line arguments in the `$EDITOR` environment viable. - Fixes a bug where an empty config wouldn't show the correct error message. - Fix binary wheel creation in setup.py by using environment markers ## v1.4.1 (2018-10-05) - A custom separator `counter_separator` can now be set in the actions Move, Cy and Rename. ## v1.4 (2018-09-21) - Fixes a bug where glob wildcards are not detected correctly - Adds support for excluding folders and files via glob syntax. - Makes sure that files are only handled once per rule. ## v1.3 (2018-07-06) - Glob support in folder configuration. - New variable {relative_path} is now available in actions. ## v1.2 (2018-03-19) - Shows the relative path to files in subfolders. ## v1.1 (2018-03-13) - Removes the colon from extension filter output so `{extension.lower}` now rurns `'png'` instead of `'.png'`. ## v1.0 (2018-03-13) - Initial release. organize-3.3.0/Dockerfile000066400000000000000000000013031472111340300153160ustar00rootroot00000000000000FROM python:3.11-slim as base ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \ PYTHONUNBUFFERED=1 \ VIRTUAL_ENV="/venv" ENV PATH="${VIRTUAL_ENV}/bin:$PATH" WORKDIR /app FROM base as pydeps RUN pip install "poetry==1.7.1" && \ python -m venv ${VIRTUAL_ENV} COPY pyproject.toml poetry.lock ./ RUN poetry install --only=main --no-interaction FROM base as final RUN apt update && \ apt install -y exiftool poppler-utils && \ rm -rf /var/lib/apt/lists/* ENV ORGANIZE_CONFIG=/config/config.yml \ ORGANIZE_EXIFTOOL_PATH=exiftool RUN mkdir /config && mkdir /data COPY --from=pydeps ${VIRTUAL_ENV} ${VIRTUAL_ENV} COPY ./organize ./organize ENTRYPOINT ["python", "-m", "organize"] CMD ["--help"] organize-3.3.0/LICENSE.txt000066400000000000000000000020651472111340300151550ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) Thomas Feldmann Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. organize-3.3.0/README.md000066400000000000000000000207071472111340300146140ustar00rootroot00000000000000

organize v3 is out

---

organize - The file management automation tool
Full documentation at Read the docs

## v3 is now available The new version should be *much* faster and fix a lot of bugs. It also comes with a some new actions, filters and options. If you encounter any other bugs or problems during the migration, please reach out! - [See the changelog](https://tfeldmann.github.io/organize/changelog/) - [Migration guide](https://tfeldmann.github.io/organize/migrating/#migrating-from-v2-to-v3) ## About Your desktop is a mess? You cannot find anything in your downloads and documents? Sorting and renaming all these files by hand is too tedious? Time to automate it once and benefit from it forever. **organize** is a command line, open-source alternative to apps like Hazel (macOS) or File Juggler (Windows). ### People use this for: - Sorting and tagging pictures into various folder structures based on EXIF data - Sorting and renaming PDF invoices based on file content - Removing incomplete downloads from their ~/Downloads - Cleaning up their ~/Desktop from unused files - Freeing up disk space by removing duplicates - Automating various business processes - and many more ## Features Some highlights include: - Safe moving, renaming, copying of files and folders with conflict resolution options. - Fast duplicate file detection. - Exif tags extraction. - Categorization via text extracted from PDF, DOCX and many more. - Powerful template engine. - Inline python and shell commands as filters and actions for maximum flexibility. - Everything can be simulated before touching your files. - Works on macOS, Windows and Linux. - Free and open source software. ## Getting started ### Installation Only python 3.9+ is needed. Install it via your package manager or from [python.org](https://python.org). Installation is done via pip. Note that the package name is `organize-tool`: ```bash pip install -U organize-tool ``` This command can also be used to update to the newest version. Now you can run `organize --help` to check if the installation was successful. ### Create your first rule In your shell, run `organize new` and then `organize edit` to edit the configuration: ```yaml rules: - name: "Find PDFs" locations: - ~/Downloads subfolders: true filters: - extension: pdf actions: - echo: "Found PDF!" ``` > If you have problems editing the configuration you can run `organize show --reveal` to reveal the configuration folder in your file manager. You can then edit the `config.yaml` in your favourite editor. save your config file and run: ```sh organize run ``` You will see a list of all `.pdf` files you have in your downloads folder (+ subfolders). For now we only show the text `Found PDF!` for each file, but this will change soon... (If it shows `Nothing to do` you simply don't have any pdfs in your downloads folder). Run `organize edit` again and add a `move`-action to your rule: ```yml actions: - echo: "Found PDF!" - move: ~/Documents/PDFs/ ``` Now run `organize sim` to see what would happen without touching your files. You will see that your pdf-files would be moved over to your `Documents/PDFs` folder. Congratulations, you just automated your first task. You can now run `organize run` whenever you like and all your pdfs are a bit more organized. It's that easy. > There is so much more. You want to rename / copy files, run custom shell- or python scripts, match names with regular expressions or use placeholder variables? organize has you covered. Have a look at the advanced usage example below! ## Example rules Here are some examples of simple organization and cleanup rules. Modify to your needs! Move all invoices, orders or purchase documents into your documents folder: ```yaml rules: - name: "Sort my invoices and receipts" locations: ~/Downloads subfolders: true filters: - extension: pdf - name: contains: - Invoice - Order - Purchase case_sensitive: false actions: - move: ~/Documents/Shopping/ ``` Recursively delete all empty directories: ```yaml rules: - name: "Recursively delete all empty directories" locations: - path: ~/Downloads targets: dirs subfolders: true filters: - empty actions: - delete ``` You'll find many more examples in the full documentation. ## Command line interface ```txt organize - The file management automation tool. Usage: organize run [options] [] organize sim [options] [] organize new [] organize edit [] organize check [] organize debug [] organize show [--path|--reveal] [] organize list organize docs organize --version organize --help Commands: run Organize your files. sim Simulate organizing your files. new Creates a new config. edit Edit the config file with $EDITOR. check Check whether the config file is valid. debug Shows the raw config parsing steps. show Print the config to stdout. Use --reveal to reveal the file in your file manager Use --path to show the path to the file list Lists config files found in the default locations. docs Open the documentation. Options: A config name or path to a config file -W --working-dir The working directory -F --format (default|jsonl) The output format [Default: default] -T --tags Tags to run (eg. "initial,release") -S --skip-tags Tags to skip -h --help Show this help page. ``` ## Other donation options: ETH: ``` 0x8924a060CD533699E230C5694EC95b26BC4168E7 ``` BTC: ``` 39vpniiZk8qqGB2xEqcDjtWxngFCCdWGjY ``` organize-3.3.0/docs/000077500000000000000000000000001472111340300142575ustar00rootroot00000000000000organize-3.3.0/docs/actions.md000066400000000000000000000222151472111340300162430ustar00rootroot00000000000000# Actions This page shows the specifics of each action. For basic action usage and options have a look at the [Rules](rules.md) section. ## confirm ::: organize.actions.Confirm **Examples** Confirm before deleting a duplicate ```yaml rules: - name: "Delete duplicates with confirmation" locations: - ~/Downloads - ~/Documents filters: - not empty - duplicate - name actions: - confirm: "Delete {name}?" - trash ``` ## copy ::: organize.actions.Copy **Examples:** Copy all pdfs into `~/Desktop/somefolder/` and keep filenames ```yaml rules: - locations: ~/Desktop filters: - extension: pdf actions: - copy: "~/Desktop/somefolder/" ``` Use a placeholder to copy all .pdf files into a "PDF" folder and all .jpg files into a "JPG" folder. Existing files will be overwritten. ```yaml rules: - locations: ~/Desktop filters: - extension: - pdf - jpg actions: - copy: dest: "~/Desktop/{extension.upper()}/" on_conflict: overwrite ``` Use a placeholder to copy all .pdf files into a "PDF" folder and all .jpg files into a "JPG" folder. If two files share the same file name and are duplicates, the duplicate will be skipped. If they aren't duplicates, the second file will be renamed. ```yaml rules: - locations: ~/Desktop filters: - extension: - pdf - jpg actions: - copy: dest: "~/Desktop/{extension.upper()}/" on_conflict: deduplicate ``` Copy into the folder `Invoices`. Keep the filename but do not overwrite existing files. To prevent overwriting files, an index is added to the filename, so `somefile.jpg` becomes `somefile 2.jpg`. The counter separator is `' '` by default, but can be changed using the `counter_separator` property. ```yaml rules: - locations: ~/Desktop/Invoices filters: - extension: - pdf actions: - copy: dest: "~/Documents/Invoices/" on_conflict: "rename_new" rename_template: "{name} {counter}{extension}" ``` ## delete ::: organize.actions.delete.Delete **Examples:** Delete old downloads. ```yaml rules: - locations: "~/Downloads" filters: - lastmodified: days: 365 - extension: - png - jpg actions: - delete ``` Delete all empty subfolders ```yaml rules: - name: Delete all empty subfolders locations: - path: "~/Downloads" max_depth: null targets: dirs filters: - empty actions: - delete ``` ## echo ::: organize.actions.Echo **Examples:** ```yaml rules: - name: "Find files older than a year" locations: ~/Desktop filters: - lastmodified: days: 365 actions: - echo: "Found old file" ``` Prints "Hello World!" and filepath for each file on the desktop: ```yaml rules: - locations: - ~/Desktop actions: - echo: "Hello World! {path}" ``` This will print something like `Found a ZIP: "backup"` for each file on your desktop ```yaml rules: - locations: - ~/Desktop filters: - extension - name actions: - echo: 'Found a {extension.upper()}: "{name}"' ``` Show the `{relative_path}` and `{path}` of all files in '~/Downloads', '~/Desktop' and their subfolders: ```yaml rules: - locations: - path: ~/Desktop max_depth: null - path: ~/Downloads max_depth: null actions: - echo: "Path: {path}" - echo: "Relative: {relative_path}" ``` ## hardlink ::: organize.actions.Hardlink ## macos_tags ::: organize.actions.MacOSTags **Examples:** ```yaml rules: - name: "add a single tag" locations: "~/Documents/Invoices" filters: - name: startswith: "Invoice" - extension: pdf actions: - macos_tags: Invoice ``` Adding multiple tags ("Invoice" and "Important") ```yaml rules: - locations: "~/Documents/Invoices" filters: - name: startswith: "Invoice" - extension: pdf actions: - macos_tags: - Important - Invoice ``` Specify tag colors ```yaml rules: - locations: "~/Documents/Invoices" filters: - name: startswith: "Invoice" - extension: pdf actions: - macos_tags: - Important (green) - Invoice (purple) ``` Add a templated tag with color ```yaml rules: - locations: "~/Documents/Invoices" filters: - created actions: - macos_tags: - Year-{created.year} (red) ``` ## move ::: organize.actions.Move **Examples:** Move all pdfs and jpgs from the desktop into the folder "~/Desktop/media/". Filenames are not changed. ```yaml rules: - locations: ~/Desktop filters: - extension: - pdf - jpg actions: - move: "~/Desktop/media/" ``` Use a placeholder to move all .pdf files into a "PDF" folder and all .jpg files into a "JPG" folder. Existing files will be overwritten. ```yaml rules: - locations: ~/Desktop filters: - extension: - pdf - jpg actions: - move: dest: "~/Desktop/{extension.upper()}/" on_conflict: "overwrite" ``` Move pdfs into the folder `Invoices`. Keep the filename but do not overwrite existing files. To prevent overwriting files, an index is added to the filename, so `somefile.jpg` becomes `somefile 2.jpg`. ```yaml rules: - locations: ~/Desktop/Invoices filters: - extension: - pdf actions: - move: dest: "~/Documents/Invoices/" on_conflict: "rename_new" rename_template: "{name} {counter}{extension}" ``` ## python ::: organize.actions.Python **Examples:** A basic example that shows how to get the current file path and do some printing in a for loop. The `|` is yaml syntax for defining a string literal spanning multiple lines. ```yaml rules: - locations: "~/Desktop" actions: - python: | print('The path of the current file is %s' % path) for _ in range(5): print('Heyho, its me from the loop') ``` ```yaml rules: - name: "You can access filter data" locations: ~/Desktop filters: - regex: '^(?P.*)\.(?P.*)$' actions: - python: | print('Name: %s' % regex["name"]) print('Extension: %s' % regex["extension"]) ``` Running in simulation and [yaml aliases](rules.md#advanced-aliases): ```yaml my_python_script: &script | print("Hello World!") print(path) rules: - name: "Run in simulation and yaml alias" locations: - ~/Desktop/ actions: - python: code: *script run_in_simulation: yes ``` You have access to all the python magic -- do a google search for each filename starting with an underscore: ```yaml rules: - locations: ~/Desktop filters: - name: startswith: "_" actions: - python: | import webbrowser webbrowser.open('https://www.google.com/search?q=%s' % name) ``` ## rename ::: organize.actions.Rename **Examples:** ```yaml rules: - name: "Convert all .PDF file extensions to lowercase (.pdf)" locations: "~/Desktop" filters: - name - extension: PDF actions: - rename: "{name}.pdf" ``` ```yaml rules: - name: "Convert **all** file extensions to lowercase" locations: "~/Desktop" filters: - name - extension actions: - rename: "{name}.{extension.lower()}" ``` ## shell ::: organize.actions.Shell **Examples:** ```yaml rules: - name: "On macOS: Open all pdfs on your desktop" locations: "~/Desktop" filters: - extension: pdf actions: - shell: 'open "{path}"' ``` ## symlink ::: organize.actions.Symlink ## trash ::: organize.actions.Trash **Examples:** ```yaml rules: - name: Move all JPGs and PNGs on the desktop which are older than one year into the trash locations: "~/Desktop" filters: - lastmodified: years: 1 mode: older - extension: - png - jpg actions: - trash ``` ## write ::: organize.actions.Write **Examples** ```yaml rules: - name: "Record file sizes" locations: ~/Downloads filters: - size actions: - write: outfile: "./sizes.txt" text: "{size.traditional} -- {relative_path}" mode: "append" clear_before_first_write: true ``` This will create a file `sizes.txt` in the current working folder which contains the filesizes of everything in the `~/Downloads` folder: ``` 2.9 MB -- SIM7600.pdf 1.0 MB -- Bildschirmfoto 2022-07-05 um 10.43.16.png 5.9 MB -- Albumcover.png 51.2 KB -- Urlaubsantrag 2022-04-19.pdf 1.8 MB -- ETH_USB_HUB_HAT.pdf 2.1 MB -- ArduinoDUE_V02g_sch.pdf ... ``` You can use templates both in the text as well as in the textfile parameter: ```yaml rules: - name: "File sizes by extension" locations: ~/Downloads filters: - size - extension actions: - write: outfile: "./sizes.{extension}.txt" text: "{size.traditional} -- {relative_path}" mode: "prepend" clear_before_first_write: true ``` This will separate the filesizes by extension. organize-3.3.0/docs/changelog.md000066400000000000000000000001121472111340300165220ustar00rootroot00000000000000{% include-markdown "../CHANGELOG.md" rewrite-relative-urls=true %} organize-3.3.0/docs/configuration.md000066400000000000000000000042511472111340300174520ustar00rootroot00000000000000# Configuration ## Editing the configuration organize has a default config file if no other file is given. To edit the default configuration file: ```sh organize edit # opens in $EDITOR organize edit --editor=vim EDITOR=code organize edit ``` To open the folder containing the configuration file: ```sh organize show organize show --path # show the full path to the default config ``` To check your configuration run: ```sh organize check organize check --debug # check with debug output ``` ## Running and simulating To run / simulate the default config file: ```sh organize sim organize run ``` To run / simulate a specific config file: ```shell organize sim [FILE] organize run [FILE] ``` Optionally you can specify the working directory like this: ```shell organize sim [FILE] --working-dir=~/Documents ``` ## Running specific rules of your config You can tag your rules like this: ```yml rules: - name: My first rule actions: - echo: "Hello world" tags: - debug - fast ``` Then use the command line options `--tags` and `--skip-tags` so select the rules you want to run. The options take a comma-separated list of tags: ``` organize sim --tags=debug,foo --skip-tags=slow ``` Special tags: - Rules tagged with the special tag `always` will always run (except if `--skip-tags=always` is specified) - Rules tagged with the special tag `never` will never run (except if ' `--tags=never` is specified) ## Environment variables - `ORGANIZE_CONFIG` - The path to the default config file. - `ORGANIZE_EXIFTOOL_PATH` - Path to the `exiftool` executable (Default: `""`) - `ORGANIZE_NORMALIZE_UNICODE` - Whether to normalize strings to NFC unicode form for comparisons (Default `"1"`) - `NO_COLOR` - if this is set, the output is not colored. - `EDITOR` - The editor used to edit the config file. ## Parallelize jobs To speed up organizing you can run multiple organize processes simultaneously like this (linux / macOS): ```shell organize run config_1.yaml & \ organize run config_2.yaml & \ organize run config_3.yaml & ``` Make sure that the config files are independent from each other, meaning that no rule depends on another rule in another config file. organize-3.3.0/docs/docker.md000066400000000000000000000024731472111340300160560ustar00rootroot00000000000000# Using the organize docker image The organize docker image comes preinstalled with `exiftool` and `pdftotext` as well as all the python dependencies set up and ready to go. !!! danger As organize is mainly used for moving files around you have to be careful about your volume mounts and paths. **If you move a file to a folder which is not persisted it is gone as soon as the container is stopped!** ## Building the image `cd` into the organize folder (containing the `Dockerfile`) and build the image: ```sh docker build -t organize . ``` The image is now tagged as `organize`. Now you can test the image by running ```sh docker run organize ``` This will show the organize usage help text. ## Running Let's create a basic config file `docker-conf.yml`: ```yml rules: - locations: /data actions: - echo: "Found file: {path}" ``` We can now run mount the config file to the container path `/config/config.yml`. The current directory is mounted to `/data` so we have some files present. We can now start the container: ```sh docker run -v ./docker-conf.yml:/config/config.yml -v .:/data organize run ``` ### Passing the config file from stdin Instead of mounting the config file into the container you can also pass it from stdin: ```sh docker run -i organize check --stdin < ./docker-conf.yml ``` organize-3.3.0/docs/filters.md000066400000000000000000000345751472111340300162670ustar00rootroot00000000000000# Filters This page shows the specifics of each filter. ## - How to exclude filters - To exclude a filter, prefix the filter name with **not** (e.g. `"not empty"`, `"not extension": jpg`, etc). !!! note If you want to exclude all filters you can set the rule's `filter_mode` to `none`. Example: ```yaml rules: # using filter_mode - locations: ~/Desktop filter_mode: "none" # <- excludes all filters: - empty - name: endswith: "2022" actions: - echo: "{name}" # Exclude a single filter - locations: ~/Desktop filters: - not extension: jpg # <- matches all non-jpgs - name: startswith: "Invoice" - not empty # <- matches files with content actions: - echo: "{name}" ``` ## created ::: organize.filters.Created **Examples:** Show all files on your desktop created at least 10 days ago ```yaml rules: - name: Show all files on your desktop created at least 10 days ago locations: "~/Desktop" filters: - created: days: 10 actions: - echo: "Was created at least 10 days ago" ``` Show all files on your desktop which were created within the last 5 hours ```yaml rules: - name: Show all files on your desktop which were created within the last 5 hours locations: "~/Desktop" filters: - created: hours: 5 mode: newer actions: - echo: "Was created within the last 5 hours" ``` Sort pdfs by year of creation ```yaml rules: - name: Sort pdfs by year of creation locations: "~/Documents" filters: - extension: pdf - created actions: - move: "~/Documents/PDF/{created.year}/" ``` Formatting the creation date ```yaml rules: - name: Display the creation date locations: "~/Documents" filters: - extension: pdf - created actions: - echo: "ISO Format: {created.strftime('%Y-%m-%d')}" - echo: "As timestamp: {created.timestamp() | int}" ``` ## date_added ::: organize.filters.DateAdded Works the same way as [`created`](#created) and [`lastmodified`](#lastmodified). ** Examples ** ```yaml rules: - name: Show the date the file was added to the folder locations: "~/Desktop" filters: - date_added actions: - echo: "Date added: {date_added.strftime('%Y-%m-%d')}" ``` ## date_lastused ::: organize.filters.DateLastUsed Works the same way as [`created`](#created) and [`lastmodified`](#lastmodified). ** Examples ** ```yaml rules: - name: Show the date the file was added to the folder locations: "~/Desktop" filters: - date_lastused actions: - echo: "Date last used: {date_lastused.strftime('%Y-%m-%d')}" ``` ## duplicate ::: organize.filters.Duplicate **Examples:** Show all duplicate files in your desktop and download folder (and their subfolders) ```yaml rules: - name: Show all duplicate files in your desktop and download folder (and their subfolders) locations: - ~/Desktop - ~/Downloads subfolders: true filters: - duplicate actions: - echo: "{path} is a duplicate of {duplicate.original}" ``` Check for duplicated files between Desktop and a Zip file, select original by creation date ```yaml rules: - name: "Check for duplicated files between Desktop and a Zip file, select original by creation date" locations: - ~/Desktop - zip://~/Desktop/backup.zip filters: - duplicate: detect_original_by: "created" actions: - echo: "Duplicate found!" ``` ## empty ::: organize.filters.Empty **Examples:** Recursively delete empty folders ```yaml rules: - targets: dirs locations: - path: ~/Desktop max_depth: null filters: - empty actions: - delete ``` ## exif ::: organize.filters.Exif Show available EXIF data of your pictures ```yaml rules: - name: "Show available EXIF data of your pictures" locations: - path: ~/Pictures max_depth: null filters: - exif actions: - echo: "{exif}" ``` Copy all images which contain GPS information while keeping subfolder structure: ```yaml rules: - name: "GPS demo" locations: - path: ~/Pictures max_depth: null filters: - exif: gps.gpsdate actions: - copy: ~/Pictures/with_gps/{relative_path}/ ``` Filter by camera manufacturer ```yaml rules: - name: "Filter by camera manufacturer" locations: - path: ~/Pictures max_depth: null filters: - exif: image.model: Nikon D3200 actions: - move: "~/Pictures/My old Nikon/" ``` Sort images by camera manufacturer. This will create folders for each camera model (for example "Nikon D3200", "iPhone 6s", "iPhone 5s", "DMC-GX80") and move the pictures accordingly: ```yaml rules: - name: "camera sort" locations: - path: ~/Pictures max_depth: null filters: - extension: jpg - exif: image.model actions: - move: "~/Pictures/{exif.image.model}/" ``` ## extension ::: organize.filters.Extension **Examples:** Match a single file extension ```yaml rules: - name: "Match a single file extension" locations: "~/Desktop" filters: - extension: png actions: - echo: "Found PNG file: {path}" ``` Match multiple file extensions ```yaml rules: - name: "Match multiple file extensions" locations: "~/Desktop" filters: - extension: - .jpg - jpeg actions: - echo: "Found JPG file: {path}" ``` Make all file extensions lowercase ```yaml rules: - name: "Make all file extensions lowercase" locations: "~/Desktop" filters: - extension actions: - rename: "{path.stem}.{extension.lower()}" ``` Using extension lists ([yaml aliases](rules.md#advanced-aliases) ```yaml img_ext: &img - png - jpg - tiff audio_ext: &audio - mp3 - wav - ogg rules: - name: "Using extension lists" locations: "~/Desktop" filters: - extension: - *img - *audio actions: - echo: "Found media file: {path}" ``` ## filecontent ::: organize.filters.FileContent **Examples:** Show the content of all your PDF files ```yaml rules: - name: "Show the content of all your PDF files" locations: ~/Documents filters: - extension: pdf - filecontent actions: - echo: "{filecontent}" ``` Match an invoice with a regular expression and sort by customer ```yaml rules: - name: "Match an invoice with a regular expression and sort by customer" locations: "~/Desktop" filters: - filecontent: 'Invoice.*Customer (?P\w+)' actions: - move: "~/Documents/Invoices/{filecontent.customer}/" ``` Exampe to filter the filename with respect to a valid date code. The filename should start with `--`. Regex: 1. creates a placeholder variable containing the year 2. allows only years which start with 20 and are followed by 2 numbers 3. months can only have as first digit 0 or 1 and must be followed by a number 4. days can only have 0, 1,2 or 3 and must followed by number Note: Filter is not perfect but still. ```yaml rules: - locations: ~/Desktop filters: - regex: '(?P20\d{2})-[01]\d-[0123]\d.*' actions: - echo: "Year: {regex.year}" ``` !!! note If you have trouble getting the filecontent filter to work, have a look at the [installation hints](textract-hints.md) ## hash ::: organize.filters.Hash **Examples:** Show the hashes of your files: ```yaml rules: - name: "Show the hashes and size of your files" locations: "~/Desktop" filters: - hash - size actions: - echo: "{hash} {size.decimal}" ``` ## lastmodified ::: organize.filters.LastModified **Examples:** ```yaml rules: - name: "Show all files on your desktop last modified at least 10 days ago" locations: "~/Desktop" filters: - lastmodified: days: 10 actions: - echo: "Was modified at least 10 days ago" ``` Show all files on your desktop which were modified within the last 5 hours: ```yaml rules: - locations: "~/Desktop" filters: - lastmodified: hours: 5 mode: newer actions: - echo: "Was modified within the last 5 hours" ``` Sort pdfs by year of last modification ```yaml rules: - name: "Sort pdfs by year of last modification" locations: "~/Documents" filters: - extension: pdf - lastmodified actions: - move: "~/Documents/PDF/{lastmodified.year}/" ``` Formatting the last modified date ```yaml rules: - name: Formatting the lastmodified date locations: "~/Documents" filters: - extension: pdf - lastmodified actions: - echo: "ISO Format: {lastmodified.strftime('%Y-%m-%d')}" - echo: "As timestamp: {lastmodified.timestamp() | int}" ``` ## macos_tags ::: organize.filters.MacOSTags **Examples:** ```yaml rules: - name: "Only files with a red macOS tag" locations: "~/Downloads" filters: - macos_tags: "* (red)" actions: - echo: "File with red tag" ``` ```yaml rules: - name: "All files tagged 'Invoice' (any color)" locations: "~/Downloads" filters: - macos_tags: "Invoice (*)" actions: - echo: "Invoice found" ``` ```yaml rules: - name: "All files with a tag 'Invoice' (any color) or with a green tag" locations: "~/Downloads" filters: - macos_tags: - "Invoice (*)" - "* (green)" actions: - echo: "Match found!" ``` ```yaml rules: - name: "Listing file tags" locations: "~/Downloads" filters: - macos_tags actions: - echo: "{macos_tags}" ``` ## mimetype ::: organize.filters.MimeType **Examples:** Show MIME types ```yaml rules: - name: "Show MIME types" locations: "~/Downloads" filters: - mimetype actions: - echo: "{mimetype}" ``` Filter by 'image' mimetype ```yaml rules: - name: "Filter by 'image' mimetype" locations: "~/Downloads" filters: - mimetype: image actions: - echo: "This file is an image: {mimetype}" ``` Filter by specific MIME type ```yaml rules: - name: Filter by specific MIME type locations: "~/Desktop" filters: - mimetype: application/pdf actions: - echo: "Found a PDF file" ``` Filter by multiple specific MIME types ```yaml rules: - name: Filter by multiple specific MIME types locations: "~/Music" filters: - mimetype: - application/pdf - audio/midi actions: - echo: "Found Midi or PDF." ``` ## name ::: organize.filters.Name **Examples:** Match all files starting with 'Invoice': ```yaml rules: - locations: "~/Desktop" filters: - name: startswith: Invoice actions: - echo: "This is an invoice" ``` Match all files starting with 'A' end containing the string 'hole' (case insensitive): ```yaml rules: - locations: "~/Desktop" filters: - name: startswith: A contains: hole case_sensitive: false actions: - echo: "Found a match." ``` Match all files starting with 'A' or 'B' containing '5' or '6' and ending with '\_end': ```yaml rules: - locations: "~/Desktop" filters: - name: startswith: - "A" - "B" contains: - "5" - "6" endswith: _end case_sensitive: false actions: - echo: "Found a match." ``` ## python ::: organize.filters.Python **Examples:** ```yaml rules: - name: A file name reverser. locations: ~/Documents filters: - extension - python: | return {"reversed_name": path.stem[::-1]} actions: - rename: "{python.reversed_name}.{extension}" ``` A filter for odd student numbers. Assuming the folder `~/Students` contains the files `student-01.jpg`, `student-01.txt`, `student-02.txt` and `student-03.txt` this rule will print `"Odd student numbers: student-01.txt"` and `"Odd student numbers: student-03.txt"` ```yaml rules: - name: "Filter odd student numbers" locations: ~/Students/ filters: - python: | return int(path.stem.split('-')[1]) % 2 == 1 actions: - echo: "Odd student numbers: {path.name}" ``` Advanced usecase. You can access data from previous filters in your python code. This can be used to match files and capturing names with a regular expression and then renaming the files with the output of your python script. ```yaml rules: - name: "Access placeholders in python filter" locations: files filters: - extension: txt - regex: (?P\w+)-(?P\w+)\..* - python: | emails = { "Betts": "dbetts@mail.de", "Cornish": "acornish@google.com", "Bean": "dbean@aol.com", "Frey": "l-frey@frey.org", } if regex.lastname in emails: # get emails from wherever return {"mail": emails[regex.lastname]} actions: - rename: "{python.mail}.txt" ``` Result: - `Devonte-Betts.txt` becomes `dbetts@mail.de.txt` - `Alaina-Cornish.txt` becomes `acornish@google.com.txt` - `Dimitri-Bean.txt` becomes `dbean@aol.com.txt` - `Lowri-Frey.txt` becomes `l-frey@frey.org.txt` - `Someunknown-User.txt` remains unchanged because the email is not found ## regex ::: organize.filters.Regex **Examples:** Match an invoice with a regular expression: ```yaml rules: - locations: "~/Desktop" filters: - regex: '^RG(\d{12})-sig\.pdf$' actions: - move: "~/Documents/Invoices/1und1/" ``` Match and extract data from filenames with regex named groups: This is just like the previous example but we rename the invoice using the invoice number extracted via the regular expression and the named group `the_number`. ```yaml rules: - locations: ~/Desktop filters: - regex: '^RG(?P\d{12})-sig\.pdf$' actions: - move: ~/Documents/Invoices/1und1/{regex.the_number}.pdf ``` ## size ::: organize.filters.Size **Examples:** Trash big downloads: ```yaml rules: - locations: "~/Downloads" targets: files filters: - size: "> 0.5 GB" actions: - trash ``` Move all JPEGS bigger > 1MB and <10 MB. Search all subfolders and keep the original relative path. ```yaml rules: - locations: - path: "~/Pictures" max_depth: null filters: - extension: - jpg - jpeg - size: ">1mb, <10mb" actions: - move: "~/Pictures/sorted/{relative_path}/" ``` organize-3.3.0/docs/img/000077500000000000000000000000001472111340300150335ustar00rootroot00000000000000organize-3.3.0/docs/img/organize-v3.jpg000066400000000000000000007557061472111340300177250ustar00rootroot00000000000000JFIF``dICC_PROFILETlcms0mntrRGB XYZ %acspAPPL-lcms desc>cprtHLwtptchad,rXYZbXYZgXYZrTRC gTRC bTRC chrm0$mluc enUS"sRGB IEC61966-2.1mluc enUS0No copyright, use freelyXYZ -sf32 B%nXYZ o8XYZ $XYZ bparaff Y [chrmT{L&f\C  !"$"$C d !1"AQaq2#BRbr3$%C4Scs5DTVdtu&'6E7FUe㕤 E!1"2AQaq#B34RS$r5Cb% ?n>KG]&+3S 88$χTb~H#u0_~VRD9Ds$2s(>nGqߗI0\ )>y!Rذw3bL"0o0cĬ^oJl|ЂT'd B[wza >0;x{T@ UʶZ)spUKxLBؼԋjJ(jIϦaIm9(Rjwh<1VYs^#ClgLD4>m+Xghx}@zEٺ95)5]Bk@.Ed$Ho2xzod:`i6Y+GӿҲ[t)Kgk#wQ10EW,sH߁6d֤P:Iy$BR6=Etq|訔o6,^$9;71gqs4hX2Z,I;B{ct:)V줫 J3 {ↆ3+ wAu4xi0`$tR6G\g4j> zݳR ܖљRa8=Ƃr(M,{7@Ƃ(NCS5U`Hl%o9]Hʿ#g$ XB2bc4Ю`Ryh.X+B6=DgMĸTX5:jܛJxTJ T[! uYi;.ͼw=qM/{ɖ'S;} մFBG5a4J6૵xh}Wm F>Y°ߜ11d!ukyX)$ڈ>b}4,GMkKe ˴,[_,۰3Tv8G mR%fXo]C[ =d}WhN]^ײÜ8Ksk cҺ -E%!0v^ئ!ϸwp9NZGUR݈JJ&ng?JTӑZϷ?Zt{C#Nյt>Osh,&f.PiLA0O>tURU˲Dsq;=NJt[H">E*[ G{FHBjEl)ݏʜ0K>j8 LQWJ2vo֌(woJ8P[)5hV#n dʜ4-JF L>o;Ie}+=!U.SdУk+ln[>Qd%RA[1Fgdtc'ڴW#X.CWe~L9B?ĸGZ9]g 1^gL qU^$߬_+HEjG,c.ws91%͏ 5lG8}h'1r >zVOlA Xl_8=TNF8` \i6׃|*܌M|-m|{fh^}ȋL⽍|{ȍî=}j yW8I/.R MAZ{y協_JA ';yI;95 !;E[DƄ\\xjJzb3e+ýBˁQÇٶwۚވRQp=C,ߝAڡBRxޣ5ŶvAl"9M 榅:?*sZy8Ydf|"]!lZpe-@/W˞9p^yN= xؽOh`W^LmR6>yq'#K(b8ݗ<brNqWx=OLQ sh/'9is<%$(&6:<}jclfdf k/4XAY-<(Y|L59|-F*`X'2:n@{ )$lؙf * lDamSEheP|Н s\ 'hSDpނ]%u,E#O<7#qډX07OX3v#"{E/u; g+Ao)#pdv1&l{6Sk{?bXI2Ĉ$#Ӂ^ޮpe, 4'Hgtڢc]CDZZJo|C>qo|磊ݑ71Nk#maL#y cWjpoF,{7)Nv3ZY|0M6BPځ#/oZWt-pxlX7cykӳ:9d9c4sRجg$G8A=f{ :)-"rtfTT F&Q #:3~<(9x9@8؋"{vTd. xAvOZf)o:?e淋>Ld՘'זsiju&pUEzl놙N|]Lo -MWdIC:Řr8>:mNXХL~ woNJ}W8|WLFuRmEuB_hS T I+ls_N#\ jݳ_SaJb A#zگeX,Sb7-#%:T61.蹭#^,ol:{/4(1,on#z|m{H=Aii;mV %53f3,w3S^Ҵ3}wܩ_F^b7]-c !'(A:\18TR#Z#C9c *hUTM$mUg_"rh Kr%:y_9知bQ?@`?:'jqCd*iqo+ 6pb'=*u+9/+{!mv>\ѷ"Z|%HHŝsDgQPKd/e>A!'(99E9w(GN*>0+6;Wwҫ~RqlssAUoE?Ѻr8F?Ts2Uu O";+1O)q4ō1Hw4Ev=8* cϵMWFڜ~gf\n_̻N0+DG7 <b;~ݭ$0HneBD`3H|0i; k ?cFyTFjVxߏ7t1S6ilP3CP#kjc|CgGtZ+E T!L:[.}1N*Isڡ1>jblWP[ TKdJ!]ݪ l ~\-#vT`jp$i"’4.̄[Jxg@>Dj_xG_EۄON1O|ZEQVhw\ ظq'#8ߍxVHdliN7_˜8xR YB#JXCf-Do#1czSO ~  TJW]6Z(}I]g] eUP)c 4BZ.^Ю/icvK%aX.s}5.W}#&Kz׷\RD{f$HQbx+ksrS6۲Pch\,luFerYҮrhG}C+6 *>=1p 9$v݌~Tv6K[t9~Ҵcɶb$^ؚQ6&q?{9}{b<#wr}~@189-Z)v#fY?uzޚdMuՠ /<ҮIV{|A6kCerһ)7cG^ޫ <` n5GecT2Vc{V2$ޡ%() I횭Ĵ2S!d]sbz$sWgҦ1ޝqnfyg(gA)Xcu%hSD:}얓#|U #&uLS$k|z Lةϗ)l3vGD c&&=|7{k<#[+Ȅ>ѮZ.kxE ?/uGĎ_VoTIG _G?V`5@GJ6#$EˌqPbBk]r2=8OVEtG2[w\4y217%v2M'(ICC8 T,턆ϵ@LN;cPCcRWV Ȗ _*5 9Nu"cNW&$ŏ7P#p)Ljɜ1 B}"}ɻު 0wgeB#-=dI Zy("ڭ(A#Jr1;zzhWEaeI V_g/p^qi>Fu쟈|{6Z FI|Ql)1<92V։i*q8hÑ Jr[{e nohaS 7R䆭d` Mu.zfmPF\dFܸ>'\<@|5Of{$\>fF~!= R<MZGW~3׼@L{M߼sM2ni^ lnv*}XH'#޺lu)0'ۉfֵ|K. Vs5ɋLCh5F,D^(h^@2dN\3I;bQ:b@;SXDMˊٱ&ͧID@]ɓdr;Xq}/.#yI. m G҆i* 4$q]LXTN]їܲ )|/l5R* cy7ȬTb;5Yy%ȑ<7ec¶ñUIWC nS1Y/,sr@ ɮz~G̛<򦨳|(&~tI)-7dv4h{Fs mchғ:p)HOl8欸C5~Z2wH1=Dh?rd_:Kf\>f79AaZ&+$-=8]ݍ[Z4EI>#29@w* h*_D10ylՉqV۷-vV͆A؜,&]A6 Rp\P&KFLϱdIK%%/.|fz$+qFm9EZ[ADZYH^_j[kN޾Z*S8tI 2PQ3Dքw N˽H8r2qQ-8Muܞs4F\KfxЏi[v幡*0z>vNX~I/ $.h۰ "*P!I&y9rY 5$/ӄ~ޫ^;:\>PHhsg/Qy5qZ*nKW+~(]坈ڭL}Nњ[F2B E&%:nOTk.ea6'3j9hTHKƫh#F#!v?:|Qp\ant7NrR.v ;) P[ Xtj `̲ILd{g)9~X3*<ޤ m[o@yPNl$C\n1ûh-hZ> /Pa!#{$Clj. L}ScrgLlv`r^jhxe|~jiJ!(I#Rʼix#uqg$dәđ~.zK􆡧hCk}sJɮ_"͗?w=][jZKnXMBQ&uV'3<Ű\!?]C?Mi}1%vzxV#!g)t)6h˖ihHd_gsJܑq;D9r .0X,Q3;/z[4H́ c2ϝ2F,s#^=df.{`<֙1rZEm6cQz𙆩%42?T[EЙ=x{-uȩڬ#"L)HufHeQ!O q I'z-#ʡ[NݸrMBlP BlwlYJ bklrUqQ#m)+/fIT\i\#XmV:5S% „xh{my^r17jENW;w$v޹PNF{[\)zJYZ,IxWzuޢhS<â` woX+!@3 IO#v%#G)G#:$&26}2T*VKrvQlTȚ,#`gBOT_3ZߤYe̻{r+vz\%¦Rh.:(Jz?=h'I@<|a S4\˄qsAs1p %^[k6K[hoٛPZa/|F($H#т?l7j(A yo'>CYD,]rc*x\(! 4c-nLUҋ[EUXt>_1ٷا7$>Fv4A9g-,! ." r1?{c;۫$-\ TӑG~qj>LS] %+f3ES$-F0JxSc@sp_ 1-*"'q"_ 4#Z5?W;_-b J}+Dt}ENVq4{qS-]Z\Z$0JAu(ˌ}@/G;3+4DE]mjVB$m+IG+cl&''dF G=!8oa;e${f*#شQQw&U[cRqUQj JO'$/@8>*֍IiFW\^RDllgڭ]t;H^aud|v2$+}'s)QEkc|v#L I#Bk^?1$0b!{i~OmY]%rVm.  ~TDT~&d1zlPIla{-̭1=?R+F<8Ǹ竲n-q\ѱ/Q [܀e,!U_bF_A6=L-Lm4[qZerabLվ/lc;9Jds2T/؋Aq8oz^&䌗Qlj͵ۃxğg$KAeR7pH g˟E\,s?O|;by'"I`d|MpocҼ!goj"iI1\>6|l:?R.#ٞhJ|5zoY /#2Ů2J!UE\̹_\Vc.Cg+݉<\>m9KU{p 9BGsUؙ1 k` s;ӜU |늄 PSj `)3aV&zL)+`zjkxhhNdxfh쳰åi,_9b2C2}tx0_dHN j~U#i>"Z[!C$k|>JԴ)̒H>b8* h^3G.a5KE:'- N>l"63R'"-M"n 3$`޹2+93OPK4d=הZ1K"3tѮ%)B +^&9m"o:w9[PR3޷ N^w=rry؏˲O˃C|rA=CH*r{3j H~hz -oތSҠg@S'm8]Q cF}15c4; 6{{;UW<(&i#-Uyc'Au5-ecq*nџ~pq=qC!5m&m1;!v~Giq&-DhYHgXܢ}3ۑ#\;ޢ7`~qEIW.֗+ N ;wla SD!4^@܃؟j u+-"vkxwxi#w~* hszP NqN6-is\\?$3nd 3vhJ R]9#~,G:Ɍ{DVIC#ڈ7XcxT $yp9#ƭh_=2[h1h"vUA\& cRhoe@1X d SIU|?^1P&G^/8=A.[I#1BRsdzUL;SUP÷K$F[̬A",=M#mGYͱD$dZqpNUXJ;0I= yKɸcbFE=͇]_"O2gw'QyzWO#niF;_%6fcneRq@523{#SقQl7\^/vq\?yY?YmF c -Mkuܸ|ΧP#MpNɯdz&>34Q \crZ)]zw;ڽS=^Э35Ȯ Q5Et3?3=+rU)rHG¤4ϵEydUkL~QZeCL*r@8ϭ#k#u%-(j]ZW }w]xj˻~h} z^_GjSlJ䈔TJ:hnZyɾ)U%sPC9)! P$D ko0SQ7s\E\og;`N3Tߎٻ|9{B<ޑ/Q&]7/;iu]ZAhq*s{2U0}[Gm\T-̱/onɱ*C̑]h]fxmm0Lw++ p=tˇVʧMo\^lrs2yǵB֤)ペtfiԝ@cnPx$~Usp[92%w{88e#vzmsszG|Jܟ-~vQA cT墓١t~ڬ̌śjמf{NW-ƻluk{C Jh6qiG2%֚z[4鵔`KӮc|7K3۸a|Þ}kln6OJ'ؽ>ZKh[;~Imކ6ҙZ_r0iA/mv'imh0<<.mtmBWФyqQ4)Ib#7}BI$4#,>")z∍6K0B{稼lO?<0qCOill<(O>jl]x2WpV㊴hfyF4%lZ#@pjh[@~ibt w} 2UewZ/֤Eid ' 10G*:(˖l޵^rTq>zܩڡ-Pb|)z\ 3SSDqeΣc?:}9eP8*JY}1#=׌?[QG5 4߃'Qz?\<NԧǫWO+v"XVΧ9桦~thM&Cpql#_I01ܭ-+x#=W=3iDfZz2CT05u &sǭ1е[9o,h"qz(oɋ/jJ8gT!>l>PSAelQԂU==*dNWΧoi]>ȡeԕOdJе;U$R.~#MSr{lCm}z簮}dǑ| ,?}!9-vIWY/,=q⻜`32oBwcx+=M_ul@!+潣3a @]NjDA8$<]'}j(˭?Q[$Fw8⹰ͪSMgK8od&si0 Z2x8t퇫ǘ1Z-%A@LyeS&™lr*;؆*-,RGIls\ɝ|X_VJXv?*(ZQ#7ַeuG(ϒ4Ryڬ [3PZFefPJ }ֈ\0 P( ;,4+'>Y>(?M(:gᆕqjJW™=Hh=Gqf"[]+C5?ӱ@ E9Uqr-nGWtbvigIl ?k}>Vre֫oX9ߌ3 2gr8U[႑uKdSjnZF|q$ʛ"gf.OʊX R>/=XnLhqSqBo̝菽m!G6?(p6SJ/n1h%h=([qUDP_[4?}3YʼR^_M-[NkmPCMI:H;.ĬOI'_pxI4n?#ǹ\~j(HQ#pʋ5ԗ.NM6MS+X ß1hW$c) pm`}S( JgE+B,|w7MRMZr{,5[i8v^r TRBe4<͑)V\;ȓj<u ?e(Y>GGV¦q8_w" aڂ't mm{iJ01nrT<[dsc {v*jB^klg chR=91 ,23DэXxX9^>&5eS mqJw:6 ǠǪ&;}ލ1ȩ9A"hWcs }jhBF)6*ESش0I\U:DEѰb0Gku{Ϩt*51~ح/Lgtq7v?ʒԍ0l~#/.G=3"֢y?hZ1O&95>|'doC`{\*q2_B.pc5(r rЅǃsE[)"+/oOCL5)e"-SH^-d1GPgϰN5{ad$v؉8${ě Θ`b 785a"ODJ=֠GCTL _̽-+|MB 3 {d3u'8b {im*GΠ, WB:ԗJ? 1hvFs~Ԥdr)6F sևrUѹIl.8Ȩ1 o@<s(.;Pۢ!`oc\]3ҙG`0}ܳˠyPanjl|Y|N=gN[kg:Lݮ]G}'Wq{WT>P8sy+A<6)OF֠}m$j]S)4!ܔ,+˓~Z;a0#,ݱ\U[W w9Tt5epHw;RĂ~t;Q٫Yzwz,^t7;ƚ,3\Ew'uP~EY a9حŲ-i535N竭.TيvֵsdUrF~ /u숹\ 45Bh^8j\5_hZy#ɀ뵀8>R\Aɖ* XI<9GgɡV+Sޑ|5jX 2٫@Yy>5ׅd4S'֊ xQJTǹ?O&.Ο84wiqK.$&"G'pBߟ5un GZWd8b~&jAmwxeO]j#UQֻMvG[*EIuvQ2HfI/ֆY ooʕR{]{h1f g ;QHӇu¾ٕ]c5 @x!;5jBγ';8*odGۤ'$? 'GC $lc4VR:~@GLV2z_xqy0\+ CF)Uht=bizKkD-ַ:,HOגkd2R<K?m? ~)S,:nZ,jdlpX w[8Y]0| K!|<\ >hPRb_}+Z.Pd+ؓؑiVfcտ5 ;ƼsjRw1>yjGIu.>J.01_5*L9rHkzz 2/ޅcj"\H2bcr917 ۈJۄyۏ§5Ho>2I>lsgV)sԀW-j{GN0h$/W<R2MD#{*.4#3Ntmu1&l'&vF:$MS!5ԱZZV-0`dgrj3N'JHu 4E@һGHv}L=l';.oQGEq]=M @y j.&kvVCp9`.AΐՍVc!;dp'qD=Zܴ-aQ=_[Ih`⁛\'CAsUCm-*lP0Q`F ijXW֘ɱDA\w H5^v!ؒCU(s ooU1`cɏgF8UTq2%«+3,Y3hgJ#`|ljcN6) g5611+08v y|ݳڦߡu; -]aC_je EǕ`{Y g-#R>GGgk:e>\<̽B6[3/!+~\T$8#}q8Rx:kz$gΔ_%cHSs튤jAyj4[./O*7QA ͒ԌPyo Tv(lIdKw+24$P Hqƒ8`?@N?Z6ʶi2@*g` AmIl`⬍3n-aIS ufdCpբngŲo+ܷ b_%JԕGp2Gaf5LH-+"C bDR!JR"0˸ `,%qvz:4Đ$>MԲ U.5kXe0fɮ:6̅{Fuq s8>Av 1Zq9lGsp6WY>{q rc1VӜ٨>jb PkdDZflf6g5/*5*I鎒$PWsA"7J]zgH2\*]?^PO\Gif|8Xb= jwwzfF)=ȠS\u]k0hc1[Xw)<|׌D׬j>d'Y][\oc$ e#_wbOe#t,(픭[4{}~=·lJC%lB R܂ SG&5c+I{?^hՌM6rAqi"6xXC1Ogǫ-i[O=A .:R<74ϗp.k讃}ZnKe0V'-VKvEh)2"\d U(>. }3D?['ZTu^dW Mn͈y:3WcȍRm6!&ΜǔHq blM/ԗl(0ʫST&s֠ QBvTg8=tI Oގ-Iެrlcs`68 %4c +4wm ad3:B0;oz9JGf/DJSqs=M 4Ҭ妏-JcM qJ{C?: X7{xV٭۷zc™6@yr)lS٬txlpGU?PlM$lg޼D7v~q!f.ёqC]4`">㽢huK{ײ7dNӅQOhΎ '԰Td)]N ގ;'UxP gQ eQ!ξTN #Gs|F"lg2c?*N y=֦$g ڙ u _ ։Eb^K9&7yk>^TADUlA#SHŁFAgРpA=4Ap=z4P`X1R="E H] 8֬&ȅoqx-JnzQKbЃ["F76XyVOC^_c\iXr6HeCS)ׁ|ivx6+C:GFy {`.Pz4 a/}7ʂ5nb'pj pS,}=)?^!c-i2%S\^1{LkD^w<#j^'h/-{DMy^\sP\c9S(JÌWP&=r })i5L-Dy&5x}OўI$*wδz,}Qzl۫}B m $Vyϐ\Wy BB'jhSBNOj3GU#05`}hG$,&r ¡45@80=T*r01P1E5ici)op12lq ᘰ\E+}6-Lbc6>j6޽1nt-J/e\ G?rAᏱ57QznڴKg񕶲iIJQZϢQh .3;sЮ\"uiii/V"L0{= #ӏ7>8g{q)''<\̙o{7׽qX]ϷNǐ OFUSQg؀;Ct>E{mp{T2>VR'x GQhh(HZ` 9WF[ Ш%T(')U,3(r< K̶Ut_cӿ+i-e_,1φ^v)w5/t=wH״"nE#ӔseU=GSY :mS+leǺxTXm+"G8p{'*6V{BJFʢկA9j5 بz {9Q'`pO㊡|*c#<қE luh-Z2\.ܒnh-!bri9h])d3rێˎMgjA̖s@?3YnZFC#dI K8ll^ ;yVOPK. Rxg9W:"_JcZ`ԁ~Fh}Ư7UɍIE "j6th:4ǡXvtxNꏬn2,T+N8 ;jRDD 6ftc=W~O' 2k&Ccz>qcam3>>[E:f_\pl}sTғd彉C2J n3oKA.บY2!bBsUj&a$SΠEk+ ́FL9wbeï>[\R ewςpZћټQ$ HWJ3dDɖ#_jXXg*ˎ|~ AP[굹ǐbAVK!vhI'Ʋ7O -FLu4Y)9c~Bچ[;^I4Nhk +*'R#r=)|83.Km>f>{><~(r.;O :L|Dwnu(s _k}3Sљ:Q^ ~ d Ѱ?! |'u)Gȴdc$Je OQ;؜g$-uH 5ғizݥίneFϧI|?QKa{\i0V>cF:–YQSH,}G8⏊1gbj؀'§s찒 VZ(v[b&[nQ96=0(~in6.np1۶)3hxmhd%0+_z{-$lS昮b4ЛEHV<Ї\e.?LVhZE*6~{d~.!F*V&+km |D LF*0B!bKn ;EAqY%A 9fs/;b ibIEAvXн:+,'r2dPES pk VI"F # Ƕ>tD"c[f\X^ZD >x8Ri4`YSfǓǵkk||Y8b{h>9*|:jzfg+ڂs !Xc5&Vh׺TC)`,2sg?<ꦶ5O@G s <'"{E<*pK=kC6C#/nE s410#ѳ)xp@Ug1i jh-;R~bdISj+CǔSK+0 ?Sbk"gҭ3[+oלsF}|qKg}+Cʃkɗ$}>,w9 m`I mq~ˮc2:(a#5VOGW_ޡGo 'zHfw%ȏҦt`[Fb9ǮiV2:jq~HmB9Hw-nuIY\W|]ITt%-\Dl>uط.jwqi+ڭãxL,=3C6Y{[^Pf/ԊxݟGܫ2sӰ[pP$EhufZtIhщߊ:}><q+rxV4y.،yגБѷ_=M9s&jw_pu K*(= 1WwG lY~$^K&tNKvH~#uMƹy$TJĥݾ3];W澻F=-I'\z{)AUU B Jon+S~K;ŷCZb9d}'3끚ōs-Rؚ\R?J<~J#R 8vWVۣT4">)LhBW2A 9ri rq@ 8"ED<(ɍ݋6[Yon^u uMliL5ql0kD\uj.:~@4Ʒ<~ZKr:O.~VzKpۀF >[e6\5HZ0vn/ĢW͵.P^5ݼ@[\B#[1i՜'_iv7mwm-HFP{p}1{s9 x;z D11TJżsqpbHR8r@ } cxrwn[edd,8<1.(PJ{cb T؉4hR.-! ԉ3Db=F@x5(#+I78e|q@2xDCzQ'Etõy^}kٮ9ADg˽RZS Ia"7Mw锎Vkqz>Ǒs~6Po+q^0|t)5IRUf%ql910!$g9ҭw5^ms|Q*RL[kwE9'ѮRm cC7&k`= պbOdZ1Rۓ.sj;/VWsC-Lij;OҴmn|h cҘ 6NsA$0S{ԕC8jrugx+=ly4`yg3<^4uV3aP|M7'Pz𚮵eMj3,J "nisAr:P: }eŊ)C4[ |D9Zڅ# :iipFHH]ֽu>.%#~"o>/fLlK ?XgVFuAl!9SsP[T.>b< `g3 cm,O, $ 栠5JEpJJޖT雍SDNHf3gw&:-@IڬdlBVIiAnf+"_@ҧ7J襣 tlE{7t<3DTevjVxȯRHd㟡Ȟ2cPKw2V$yF{j#LPwf[c3U* }qR!K$ٚ#Iٳ(1XHdmH9d_\MC,} DCPBڵFȲƕ8!GG5jQ?oo?C?XjQcd@FWr2[7S14t)qf񋪌i͈ѼOܹHCJ<[_}/Atȹe{vrP1 ͍1%>^ƺ`x-#]PGqž{/(E'˙sċPZ5khAc>@1UJu a昙@ޭd:T!uBL!p4Qebwc昋a[&t95[+tF^,}itq!h>MoJXOͲG?\;xQ#Q,嶜e%;|Γ:u@ǺEl/DWqu9m1TkFd嵑Y$9R?il+*^vPAqA4!'!ym$PBE!ciB[QTP0jl"jOF)vcR/vR %&>C'=R:uV Uc{'HMTLd1n:<2hA#ċsVsa$?UdU+tbvY79ǭz0 ojЗbBm,NOh pYS$&x1!7ElyNL#($9 FHz[FTT`i-_1^ɯxbn()|/jz_07$UFzc%GÑKzcFथCTSi/Q2GѩbK5;CɃwAY(%El|TEQi7B+SjIwGawcPB11-k'>_rxpzLievxړi1X 8g2d2Xm wTdh#} }qU/MX8#jPl@A*Bt'L/=* d@jh^T4H dw\\hz <]CP7w5S|b{*X*2]E#g+#n>Fl⯉3Ulyzlֿe[,`<7r/ClKr%qv=#w1t/C[i1 ?z׎D/>ƻ$֍n,z_QZ7H};V, ;[A?v&U bgb`P7/@E !}u^!u^uM꽓GT4u\JaDA\Ӣ_qiԉm3[rqQv*٤_ŎMize sߊf4y>w_ :za/-L/FG65ƾGͭoYo*8Rdv̝^\D'VYcG 0r3Z59ɷ~)זqZݐycIlNki-t,wNj !ivCh-: eNB]᱉6A1Lq(Ua1S ^82p)nDǰ%"LT6H,B-!U@NO(4  ب@sQR!m^\iǝyޢQgc2X+y"?q =)-y(EuzRT+w{#G#;eh#8kñgL} 867h;f9@H̛ Sv꘽\4m;}OzB씒4~?Ί3' ŭ#m1NpΗ JNEؑ@P'u!y4k @OIcC[|ֵV[Ch'j;Qu״}c/IBy/xs ;egpro U_52[E\wxcU7kqhP+V/,Y[Leuda j7 ڧ!DG[_۴6(Ϫ$no^  ИbR[m b4F}He sr4F,Dlg-  `4ŴĔ]A'&?xrQ[:8}_xx9?,A!~)2O0AŤ_"}2$1#e7d/9#}k *Hc3F lҠĄY`96`I`SahtQceY~R.[8 7'YGn)Xð_p^4q XV3{q:tM:y\zo׎x?iV!nZ[Xݑ9m}ӢiDR_`B=%27S5/F6n1{b|E@}:"j0<шdjvoŽGM }r#Al^E2b^=)HseFa4d"'67RzH*91 ۆ1T#a Am.&ӡo37c6l8lRJ4$XGU+Tл1Pm'*@9XBtU'H == {h6 zUahwRC*ę7nCvF’]U 9F|yn8b=1n(*P0q]kaFZBr"r #hlR糝416 d}*Cd orAjltu.1Sf8[푏h)a5N,)JXT Si9*iD?έWq6љ\YnNNW?x{ 6sӊz9v}$Ԝ] U ƕL/HQ!wZbsک [@sPPfBPT ;uBΨC i︦#}+m-C26s#o!fv4g~XIЈD5ٙ8߀?\W6mX|Km?TQox +MOFM"sE~#H93lyVz[lQ{eaU&">um1z1im2hs I{6Ғ)-ރ>u4|i T'$TzX4Y4'ML70yI.:K 3V7)+2[*bI ηֶٞu, eFx滸(N)(movs)3δ4ͩ ?*b0ζCi*r`Ρ۹eOS$*ta̋Ή6xU웗tY,i"UNlSQ![*3A%*kn3A9䆧Hm-$s"bRŕvJR4$mr09SA{=3+,Vs|oKFsV_$e5+(sT%F(Dk<c`9w6c jm I(7`ϰFYaw9 i0k $n%ʫ<IJhn2fnpm!''!*pO6sl:pc+67U$6Ն'TĄ]<;zP)ΪBhF14y:2?eU#xԐs?Z+FP}\N}.:fҹMzgы鿽^<>Z|F;W$ǜmй;!y{}hE@٭KC+V*p{ZTtK!&YdmHϜ-`ԓl ϭ@;E0H$JŅ9zb*i>6R;M%-Q4q?*Pj>u`Bw5"[LI 0Aٛ󫓓}9=47\'4 \ Gs(3U&й";2x~Z)$ʓosR)"E0SH8AFHiYzEElg&UsQ{a(x!sr Q=8"f\`Fjw' rjs{$"f>Fb0cYiψ-v= #Z$BR1; jNvzkzEZ[buasַ1a}} ^=_]XG'|^>YkeW\˜`˵kPv*D) =W>5p)K_yZ:M HU- K#BC# M2NM}MLb|wmOfXe i-ܬPI*RCjUبMV@4V(M}W6ql Q+@r4~FW$lyiڄ--ah;#KHLH{v1#s6FzfS_zTҧ`GܓD1̅'J&*7B/M~Fr@ȪM3M!T}"pv=#,-2K C `qn\_WTGfʡzDp+)59D:`qj37Lx">?'&a=p_\)ڋHr+rH-IlTaq$fdO^i"Z,c4fAՍdUDvh3";4& P.nLPEF7`ݩ[`[X  YnН3άM/!?:< n:^67j`r?®G&9d0#5O>i#$b!qFDރ ww断O=m e:1ط le%737ίJ'J\8 Ġ,lTHc 42'Z4s"4Bh8ABX+}}ivǽB-M;69LŰw Csp{!2U B#4A06{⠶rFVTQBVݑ#C)H;(T!ۈPTԮ-n'/gfvP uE!p~'Ρ ؎2 vh6Ρ^B7`XW ]Rk= &M,zFs,mm$ *UϤ֜JwTqWNn%26~g5iW 24I꺅q9 Y3_;;27;8-y̙[2*:m$R}Wʗto4K[ĹnocJ1✤m;Yպ5V [t< @1!2}Sg8FhofC[M#2psHcoEi+;7(`'*vѥwʌ - h@жʡ&?'6FҖ 4PU"D'ڜ4:Q̫?)-K}~u'-]a]oCXrfݪ|L5.m&tyWbV@<׭ sAI\7.&HP;UOĺ-},ʬVH?*쒱hw21L:!biB%Aq{u2O$5AܓQ wIopJ62>Ҥ Rñ3g8 ;>LH䝵Ҧks ½Բs#)k.C[VQkSi[htݏϷҐA HLPd^>j964QI~tY]R3ڴ* mX`MI !.VFcpk<:-Dn$0RG4Թ:)8}du8/./Z"u&@ 15 )Zr [) Ig׋Ê$#򠝜EйHuK H~f`99ëgduFcL ?(@(]'5v.=:S$#2Q?t5t +ִN _f y^!'p=M  h90I&pcsGleziK>rNN?RLr-K"=]纘~y2L2yMyj$l&Ԥ"ǀMvq8\N9u5]3ݮu[4Xk!JWex#F;W#E.--OÏbBC<)[4!13MI[)=Hkia, Кa@gT6roWlP`$TQBnp'ޫa ?=ګB j*lwSeZ;=1P/`[Z *OAMBhQY3ΐ㌊H&XАp{SKaY8$Qm/tKlK;ʐۏ8xc],6F\#! ڏϚ0ϥ![):9Q&Al4Vt@XՐ/?֡cM37PiIb!`=Ϲ\}* ʿS&FE{kFbex7r9Ē.J,i ve%T}9'ȏ0iκSv2+#yL9#`BTRYL+}kG|u;tƑj7̬?{oT#-y푇9Ͽ'FiI[=8|hȥ*O2N)x3tT?ޝ;h7mxuqsғ3>:נ:.,RTqhK6"rUcZX׀j\cBA&f٢(nN{܆ P2qP8=+ъ+m]I"GwO:X#|X>Il~@qEH,ncLo`PbĺdFi7 8_TM<-JgqgU;4DqS}v1 8u[F+jfIu.ʆ r8,cنΕD-x3fp_jIDl9}+G9x<]BQT_y5^5tc!{f|ڣՒ/dc \Mb3:+?l㳵ajr!uutW_gHyGL{'M.:h9>•4luFS,oI_9279'$ih%mtsϿ53I79ǜvcur; ʀ]SqKؽ|B!m{4.&y$A<˱2sP #5F8cCX_(p{T, aX0ȩ|PllզT4әvALL5@ ~j9=]G ◽"G.H!J(>QPt%a=Z$5l9Ud'8PbCwL iL>Er> uf6OQd?tNi-lsg?kK,G.vmZǮ6 ~9 "@8U'M1WqSQKE-_ "ťcKcHeY!OghқD_قNEb /!NOa^HtĹ pFO? L~d.=\N]1 i3.#}4 ?3v?z? =7v"@pABˎ~HTyrSBL$!AV$8#3hdi1 2̅.W4`'ڠxH@R:H +"۲sGzoV"l&'R2]FP9c[qхىWƉfPW9R$"JdT LL+HP&!'޽z!xcfFqBNx=ZdM!&_+>;(N,==1{kpFzR6 Ճmd\F.i bS ;1xGIﱧhՓugREB , qWKzuVZMtk@$OH׹==E>L~nTyh'MjzΥ!4.Vtɭ$x엽!P)mq+mL`$LE8#A}Ev_p 7e۹4-0Db7RBMa3\Jq$LA#*PR%Ddv`BTۜ093WW4f;u "so$RP=H/rL [u+]%LeI?gӇ^Q| C{YM9E9CSg14"9}k<V{?)W}Ԛq ^NW=ǽwzVg7[BE|W*wqn,9AHp^Yo"2OWowd#\)ӆ9U(c;^Џ?c6O , ]¸UPfe*h8'mzc%~Fy~ɵ%gD9l|lwW5ff+ q#~;S;铒= Ԁ~8"aWjT[m+M{-J^'gr[# wM=Ƨf?tkfic&s'M6dF0+[}|YK3mlgTA(bTYu0w?Jx_:[`a>z8>u*my=/MGON>с$s]ms.Mϻ]w.NOδoytZ6Ξլo2x Oʹy_Dnbs'9_((GG".Ygnz 4%A+LLʴMv`ydz$mfIT$K2:<Ѐ*a}* phR+cݾ܀TkkDTd$gA Kvhaae=A+DC[ipMU/-H~ϛ7 .6Z[cawш<{Y7HuvkW[og~1҉d.;mZG*Ea#\bH# Ik|dQ[I57pђ=iS!h^^_QhWPA2qm4db< TDE&wp5Ti .:f ;{U' #VޡpCVT8wcޠ~2C,})'Nt1<7sTJc-n#=iIDu\GhDCPk/ 6`ͽ.&7\T g1P[<,1ǽ@Pry$7-֘Uuݏʪ_PlA0~#*s|:? ɜ?C3LK3OokRQȢO*zV?HF1 C 6(syL $ a9?-F Rӆ9#>n?$'RRcfȭ[yF; Rn ^~-Uk''gkCM6(3]AGY'Ǽv"i\\Yc|*PJ6=FyE$8i؄+Kp`zR䆵GٲҭҲ(S ۏ~,/\9'fvP[L8aK`[$~t|cx,mx,;l֪54o鸲sO|Z^ح#FC;[?yH-=3PƖWꯗʺ#oë2cn \zz+RSv}G=E=-b-u]oKXQYǿ?SϑWɦg}U?5wEpA=2Ns-}QfqqϨ-/4`#e-698oOjۯ)qXWzoYմYbqU;sE>Stkdzk*Ƿt>W#5._R[t,OlRv=M1HmdpDf[qNia n>PΎL_&T6,$%:*69S#R/KBn@uw\M[hR\ٶcTȔP v FFHzF ~N+Ⱥ1 o7qbjȲ63 c9|}3Jc'z3_M>W$z=llXafP ~u͕w0+ٵjhWgZvvnQa޹vE:}FJc˕(5i3=Icgc5}ԣxϹ4FñDFyYȆ~@@k8.`$W ?"(\|$w 4d`®?u~ u10#7NJ˓+;}*)jG(6!{>,J W eMpR"4a+}omq{'˰ǧlFuΚn{F\Byc*{ТĎvLy3h#$VV>Qyi9E$Zq1VgIl#D%[iMTwbx)ȅKr=}MzoڠoVJJg:+NOiвx#Y1.sv%ϑK^]] j;s78ǭ~i'E GS)g<4M!wzQ}~ iuşJDO |2F@Z z5t~Cb弑GTziv،hM}.4+yZS<?ZM[^MAzñ%Ć  >r}ktVHR }M_O v2dRbV[Ac ҵim !ȨT-Ǯje[d[8.#:L4XL//9 nATf27&# #808W$'kP7lT=pٮ-l|<}(FL(xb##JP^h}H)G>GTZdDqw rVHLhH*0G9׊c->Ч-?LZ-ď {^⽮y3l7a^6lC #I2Qb$b4 A.`]nҏcBFH8?P ID4$bVlpm?l{A9P82ex#R7LT$ Ĺ8H!ނȴEM7@բB_fݍw=.4@[5%ҡIYwy ɦI;ЪWFq70y\D~#8H )-! H$g*OY\O#ȯ uKx%fdl~K \^=j&MI{s[<׮0W3/"gd>#'92^v,b'ԓ銄"`jïZ9%+'AMdFxt2B#Ő*4\Y\m>Vv)'~ Kgzp1 Ԕ rmrm=Qh(7 JsM Ir3Gmjhabp(a! Wk}Pul(rggGP"Y斐%wPl3O֓k8JR=++:WKM]D5,eU^se>\QݦF k^YBIm$!U[>oNFjԼ2?:tO!:6p͌~dU(6LǣF'Q?V@y?kF'؂?J>O]~̶^S< )>wGB :(]uG`Q @}wV r#;#zz#>+|<3dЮی'se{GIo,hnKv܌p@ ;OaFblc:$Ң8!twdS lxkhBQ,w+ NAx6pE )@ Q 31 cv\OR^Nȱ(4DqƋeP;WZ3f4w=ObY=SB\Kg3|ގðk>uJ#cz 2H8~ڼuVdKqn7t6WJ\~<:m6p? : W\dI[ H>YCM(i6Nr?au#ffydwnXkt~4+ aShӏkrzI&,x0 }ΏwqiA=';ʧZJFXZvQK*V34_4 ƣr7?،Qt'hu,_,ú۩]?Jt_){77QܦH -Qlk#p߽܏_KX4.1>Jc4rD3;'C&[IxaϻH!G־f݉#gG/0 ӽ1*p]짱:ǣ쯲V_?xf TS{rI >kZoQG^dyn'|=.ќݟ*pP;{R67k ^Οc?Λt+e'(o1Rv=fVF{q$Ȣ"+eV]ӌ^I-=k ί`m#wfq-UɑaܪlցM G/* hw}37֟ }ˀٳosw1Z/ )hC)s^® i962PھUwRG~ b%Isڲ`('?*jܒ}(_aI@:=8f~{S4N9}oV]Q6],ylZC=ſ۵^,FdXʀ?dac7-5MCNm*M7," eM# ҟ?:ܶ[vkEJQ}ժJaK:l`j̤Rro뻷0bд( qcPP|/_VHف ܏ʡ$I5JY[]5Ǐ6{>ܟ҆b^lED28}ZlhIjr *mQ5%j܋/ùTu^O'ŚzO`/ݵgqzuMd㼗c9Oxq%^W#Ǩ߅y{O:%{Wt-F=KKk{ݲU7.V{J4/H-cq5^hZ5hO!6{|P3{F?LK1Ma.=xўh=>2sSDnp}jh{cxed`94-Jጇ;g.;}J)}}Edi({f3o=}ݍyq5P3mǐpSh"o\"EBN1,g< t9!A;:IN|IHwO\OaC"Qh4WUGBpW6M x8 b]z-g2wgz^N@Dj`ڤh=T}_#lr_ xfH@vQ Aw/^Cu(ٜN7:WGKkwQS^WkD#f{$)I$@X2=#~%q%u> $dy/ʕTeƫL#^MuA K|zU?bpC̖%n^K]xl.Uڐ嚪`/:XK4ӺZ6. ػC~Uu^m#?_cZR~od#6p❋%'^(7Ƞ%#W?*]C/ѳm~)Zi I~޼ULNLS%O=7\'Zi8FO]Bܻ++=</ݳG[VEr=g?J,oĨaUW{hg5XB faec~$jJm7bU^Cw>N:i^ő輟ҤpȗulZ<ïtUo+qTO2/jWG^Fӑd?+Tz/S˻A17Wl!oKD%d-kNHtcV֤t3a syr?'V4_պ]' HD!Qs UЗ/ȱķHZ7KxX*Mv-W$ztt4 9Ck6Dڄ\Y;=Q!( sqJFQ@_ub᜿ ҩ1zEؒ_8=銱r}JzةϊSnmm9f}W! \ts.˻= eD{鷚ǁiO2_/y+ GUBw`ph~Cֵž>O=;<$[,H(ɋ&qC{[`1@meON}yQ_ow Hswh׷q@'| kz7%Z@c2kl+Ur}޴>'X:)^\Kn=}k-DOAP]eњ4kc=U9v2ǓX$q嵀8FFdEĪϕtң]x=+c:e~#{o50vUn:^OVm;JMPQFgb~>XjύQb\]=eqF\.Tai=7c!RO_*mۆGN=gqOy4m$6b l~uIhEe9ډv~Iɷ:Hjy2FN~\(I$/"2G ~e!mos,qv .ݽeSSYG_NBd Q"2񜃑U$(vC]?9 q3K"&Fr8! Y$5z5rhsMz [b@a`4ů{[Xa+aF%xhj:}+qqmI¤4izoINM{GFW ms:~CieΣy64dry_jx;AGdRL+ZƸ>Ǧi kkicmu y9hT9?|UnaG=ǽH- /lO灜nc ǥTZ:]802mpL_Fx؞ n}3P-˹IiX q=>].]&rIoScT&6?{;jltP$|woQh#0j$|MW׃',;1[*!ZWڪX4ʼ~UڇKhoydG*P8U(j]pjji!;u'Ώs4oSThz^Wŗy<(:aGlɸ&=rħ;o?z(CXz_BWѴsJf _tYJ9VWUzW®LWm.ц YŤ~{yΨ5fedt:+W#|#2R|;ԄN7=]gESo(1ݏ*_h(HڟM!`H]$;ڴdyv>QɆJ(2 y۵6^MK[ESrb"?/28ޯt ֩wM\2 KRLI"yՉ{gtw7bt/[g-/tmP;r's^#3Y'O+0Mԏ8操,lNp#[]WnEzx|f忺=f88J)=DmV u4sb}U1r,tgL#R7y/#?/6#5VWU|i[䫳aIq"qMڵw[ִg-+c8ڧmXwXO2N\1J?,X[[@[V~a$4.vzT2{,߯jsƩ|Fw=U[E.kbӠ=Y?쌷\u,hl;r;^`c&tɎ?('XgdD*-uh,/if޵c_vޝڗjWZFOuS[ԻB:4]15[G*vNLjtޙF.J3vl 8JhuyW2HqZcTLҵ֢FDc])#5rH'+4Jq+Cg,Xw5-,Z2F]9ع_*jg*S{}s!j>yC^q42kr3z(uݵmK51Bv/5`yeq6 @ep+dTT>9Y"j).1 >ٛX7I+y7\o']u7Ejœ Һ$6>̻U-_&ז$ X}jdyӊ^KVSG_f$)(r{zƝ+Q8.쮬D>/3sLH㛕IN܂{sE?xM{em.@ 1ܯʳ8x:8ؾfIԽ=qͬdr2N S>><ʖĄd\V1 ^M@Л1cދHd M!{9\樦vϷŏz&u kyY&X.q]<GĮ=7qHͩyv8v;d$`+%J}Ξv.-t)W.FMӤi%{_.roR9^?7M8'zF^n.8+ww8 ^|nX{n}q^A~ƭ`<}im(>].Ͷws^6[=~Rlz;($0O%m:ݯ[}kAhwnOcff9&gKmٷ~Ufw9l1Ϩ Z/H 9 XIPpH!27|fJD^/_6E0U,m{/Lب_jFZ]@jRW9" dl, _E&`|䚛bvu ;1eCF{Cq [ pT1։=%`{Ѕ=GP899ϋ6/cQk^"̖*h9.wqMy,?'*n[;)2(Px F+)/i{̭~C烏ObIމw{m gzhþ-Bdy"GeG##Ur$6s֋MuU-Οj9p!rOAOįn%s_~'ϣ$f_-&##>36c/.zb먺71'ф9 <54ob_|7x[_k+ nJ?>eb!o?vhKsLV]]7T?GkGM/RC~Bx\i-lݒ\Tb)[G>PhάvE 3 ķ`I*x9ƿn!/ud r[ڷO'uMjTWo 3,mA4~+Tj"V>=gMl>-Z n/)pvʠ R$_.ΕOsv"xky9^-wCZ#(hem@ҬKl9A>M9r-ȹB R\5W:g;cS[qzFK _L.:Ww˹7D 5rʶqUQܖեC\gSs,1+_Jl~9ؕuU<lRf'o$ ddVe;~u.s:e='>\okF%b'ށ9(/3+˃EH4RۤD2T(o#:[Ԁ]ҤL87J+Ys{vvD9Gg bBg$fz$V0% )=f8 Mn䅔{*rG~VtD]8 }*i.>EHԭMFH/ y[&ÆjIM0h d'0f6c#5Ue-N,id8? qk*1'M[_!<S@G5Ih颐`rѦM͒.K[j:|b$2\ 7> f,rjB!x0NDg'Qh_5+ln8#o=mpY@}Hy78nkhkcw?^WWa v\w~jA5(z23v4cϸWaqa` g3.,c+lv"Dr=sB Jw$ɤ- pz-t.8V=!>(5Nt 0GQbsVŹ8#9kպiPa9qveT9˼OxM\>@ scԈv*(rK[P1y>!B<y=p ln=p1gq}x=Zd׳oݴsWs&xoZ>9gozIR 'N ʒ/xX9K?RR3>m|I78:j+i$En]G9o$e5#20v}_쾡/VC꾫:3Cyn$k2sר#~(j}=CeX}uq"qy}]Vˋ+V,#a,6ahK_q*rwz)Lĕ(r7X5+/V> c[X>~ udd[nѮls+?}/.GK? r|İ&WC2G\KV=@xyӗXeբ̯ԯcuNTYA?zdz"\F@ߏ֊i3WKX:|Ru#sn~`VUk9CQC=3Wy[i?κU)WD'?d%uh#Ajn{Sj!Uɋ-&')܅S N "iae#28 T5RܺX‘/Kd Q}x\$jNHy#ZcX6 *} 掺+`0m.SdW? q]ɕ'6?B d3>ycO<,#e7 ŎMaVJKEJӓf?<.ߏjc*_ IS~֎V "n9rȟAYs7{}z3O+kC?K${ӯ&,v?A\k#?<^~UP8DN}`E}ȓ|jҏ{;#p=q:5b"07<_RR6p ť%.p+N<}S&:HO6 zB*'ɽ*2o={ ի;ؿFGuWꚥ欄F U{{U:];/ֺQK=#Noׁ-VI:cG,~iwA_ėNmi8'٭S{E{[tN碴F(<}[G%eCEnsٷtށ۱(v5;E9ړM OH7:hSb$i, u 7 @]O vK*‘#=Sw#[ - $>?:^ø*zb&Ǽl%pr-BOtMvզY>86TxP5]*  ?V>3)7*3JMc~@exҁ;⡢s'I INB$lA+PҒxgr?:KXǚ[չvܟkz5`( 4>)2˩{ y/pUy 6W)# ;jwJ-4scfO}GQjڔ2Jyڽ>-JG%[5aM9aqڠ #ڈ .1} A *䒫}U;"a}le8UQ_+ƉH]~FcT @T,B5E'rsF(GhDed8bM譆ñlWw;6X/)4"Y`\^PsU`-w>^q@'~2co:s I#P?Ag ѭ|~frC#yҴz#ʪ޹cI<(Q||>8jǟzZeV>AݾuHu+rWqKmU;?3>T#7!x>>Kkk8o;?A]jz||ޱlWveE.۟ҺP]÷*SҘcܥKM2mmph=_.RzFh#6oNT!LFx8{ċMK75pK 翗'˔>-0yKegDj:ub[n 1vv#6Jddq$l|Q)EƲLA*+ƶ> ] S[gG' Rߍ$sG~sU͆c]Ḇir&-:ۨ-j݇nԉU? ך oŊd!\|{~Eֻ\Nr0@| <0WiSyʁ= N}jbr{M HאX0 S¹=4%3)sr~Mm48ٟNmwF R*vfLQ.93ٝKPh9N湸!ƞuyNjUi*>i/Ghe8vgwz)721l->G6XW7JieYg˟>/(ow}]4NTZAHg)Eu.fL18Xm$S6[YߧҹӓOC_4ܐ!I\tIt`~ߞŽۗe{9Nu+bk:3OMR(-28"QjZ?w.fXjV=7P燉cXlZg/Ӳ/SCx*)ԯ mwSbׯ:^kM?N-690~ծ!_i4.cҧFq bY!ުQ$۰#|SmYtGGz+kvw܊cd8.8Qmc.86T:m2mzM ٠:rąy^9})0K>^Wi>HA4:&JKUP#SD2ȎU!l*Qqf{#yf>P:S{7ܾV#5["PYF@ w6SzvKew-w0,vHUUrRu_4{.{ SL{Bv^vsU>̪q >~ڠQ}&]Н|F,ߵ[BNV8h\Q uFpi- ^ZæZd\Nb0ܓn϶(*87&70?w=Rހ R-EKu4ڬo3+Xػv E)3h:/_Kur4jb-D̦e]FCCBDZOr:uEB7YX!Go֬tCXvMlإ=ܽ!#!2( gTWb#V֍!=hN8=^3. j駻e WFUYsD1oz>l<R UзZ@9-[I`Tg2w9Y|8K#Ёu(@jdkI9#>=7T#KxAvfgTqP$+=3}p,VKj0܇|ylt[/Sޑ;K1;φ  JU.<˳MwfB{"YCȤD6<خ72<_WuUWj ?LlO)u<}OYE<[XD l`}[ɆM"O}|Zֻ,1>|DZ<&؞yLU)D+vi;;~z0];wVr z8Z|AaA%_e[.S(_J&V'kDj'P2ȨY#E 1p?JΏz})r{/d4ĽmM-FH8 .*.k#sاGLF6A>,zŲ)JȮ.GS˖h:cZڥI_Kcd%JK2X<MS{pL߮4ut,6[yBHN`ž)(3F.K"uւNMɵø%6)+9{zSLMP.R@ G ە ׽iS$ܭ+l#c{18|8?qg8 vq!F~ʡ[*{:R7)>&>Tl $@[T؍L%nI>DuQ@CM#zvcnn"wuR#Dj[q+,%ē2gۚljҊv>;I,!$M$zP;yDFr=29=cU4O#o֕ꫧ #?j\>`RץL `msF?U¡ I =qws*X3!UROkQ/# }F8*sZ\]p{<٣5Op W29壟ɳG;H@"o+g,TRpkhC{7`=[ ]EhVs{Ql$8iv6.KI c 8$ .Cj%T屎~Llķp~?*Xr?/J#<H4Sߊ p Δz"#*Z[F8|OFkUyk_ d܌]Y{z;?O]gưdr$p6ˏZq pyIfXEǻ!$p1LcPAH*g`8Mrs*x*hZnحBlN^Zd]AheowBR0mVB=CJUuI}?rIkICK=wa%:[J~Wv{U={:sX`EcD҅r}- loSآQ,sM)$_%]-Hmˉd#a\L-J 8N+Upw&y귢wӴ+WTy;db;4ֺW"Uҕg+l|'ĩ w[~[};2@!NPp 8XSgV4d?R{]P-B$]BA#5*ƓZxgkq]tzSH4}>7͔;I eE v5 U| j3#TL#(T}IxqZa[qOb^4h*{M6 ge^mOQ21bs{m!:NOeO!wտ8-nǿ:8ݝ+՝Sϫ='sn/$YHk1ےNGΟg4xeM?:<躞zz!!D#+=i  uhʟJybdQr<גSku-$̞In;N@[6A7PkY6mt_+nv'sQCn]K\bQJF)Q NȱX[I1Yݮ!Hxq:ly}8c8\N9}4ܸ;h<KM6V {ZXű+#V9Z(ɯ .@2yӌqPUV}l⦀sx854XR}*hYOV@ʼg>ҡcΡLA`Π/o2FF}j7y}} 햐mgL{20>>T!٨CB~UY~QXH:G6_WX'WkYG#Kj.9νtS"x`OY*}Dwϭ0C%ʓP[ ㊅ߴg>-<[V('UtŲlgMqeVh )lnTM?UJk\_@=|c &q.0j* ,qW>1WOAӮΟ%&.o;);rOr cCGBqIB&W{,)Yb< ?O󬹳ฝG'Cgiݛ+,8>F62SNEt%ĐgmvU}TsJ?MȗPjqj6q,4"^PVf9 Rp>M_>#/!w֝6 -= HV,Ko}&oሥo;qUQrc o\k tMfF.gc7c c#kN hu^fhx-͏8+/`q.5.]o zzC[%S)C:NOKxXM.dc `7_5YZF9_:˧$= cS4Qih8tiG&P·_q܏/L)@H8oEKKP +qIOps$J(je7VM{r:d*t@7R\znpܳQy?hI3c˗j? 4.[흊nd 3|:]3k :"ƙɦ9ҡӢ)]7W,oOc#?[K#&5 B/nM,{+,xUZTI H9?W+-IjUm$} S%M1?_Oi$SƎ%M:o-AM ɭQQ;?ڊe'Ox_X:;;o6th( l<ΧСzt2L3p?ʂ۴z*OI("gldw9B\kD3c;|YjLfIO=4fT5|W@u1LAȭUZ LC>䃌CJF#L{urjZ|kO6~>OF'ЯRU/ϷGfu:?uȌӐ@n=`8YI5܄Z尓̣Ô }1ֈfgtN;uk@l_O$!|~4̍-m>Ki'yIcB^H#4DuW 6-w1ۈF=}ɦE k p2~}4`hOM7GR}~\y*NHr|rV>߀JDK;6rwTe܋f.FWڣ|#g|=f4TPbC#o@%p~ x#*2B b-0'!N2EF3#qǧҀxY[\4ΛU'˰A#{hH+&ܩ(T=8S[D76WGpegbX|;XQ-{dg8ǥFAJXcG$!k,842Lw8%1< Km$i I|r<8?T' Ec=RmA2hǷ:â-> x}o)A6CE >__jja=2!>* ҅Y|X3(on^(Xcҹ@]wpnv*@.95t20C!Ĭ}3n\ B,LO҉:Uey#E5"l/ġsѡGt^L.Rq7'dl%E>Mw7SJRI2M\20 +GPM AFyAqq TЉ/8v8wB>G܇%p<ݍB8$ؤD*0* #4F X4*lcv~ֿO?ze|+H8U-ybkH9lsj#\ɻ#8G ;ZdbYt*--I&Hşɦ\KbՠWU#Ve?Py\TYzD^i%nC@{wĭV#e;NGD)8Ʒ'`햑htFԚ" (Si7˒ƹ2v_G=\1Q̷\@0AozD?t|=/LMqeH,e.y22WQcQ͢*𴓱8zBg?*m>mv(1[*|aaܶMtՄwgަ*n}g˔u(Qʕ9* 8Q;%SPmc e횯Ĝ%f#?^!'}oM4x-?ޭ.#\"Mk0)s]. uB>Vм)gU88e!X#Km޿*ݴ;a@|dpy8TF~Zivf+8,|!4/J]єZI([=MkG1QANygiû3JxBt/_tu5m56wr >`[k}/uE._mo{Z#q(X#|>MK&kL?aӠo4ΤJ6n~DQPswAꔯ×jMI/ kz]aY (rȮ](?Z)g$G2p|AV?k>reHv׷Y-Wf?gV<:דu^1C?g$>ksjg%Y#4EH9u5VO 4Pϯu;!sRdW_vZoA[~u r?,.ߚ+=]5 gq-z~WRs)ޟ`{7M_=&O}}\RJ͖i rݒ~M^-TU~q[<"oM*(2^wL认`!nUʾűZ:O/[4:ۆ, y8w{kTVZR_z[ мGNE>8կ'~eIZ}[ٟ迻 ێE;TCH¾$Y:+Q!ݼG>XrOtezdS(pDL~,yw)N>"1)KWUGܽ﹝2MD|7esp{I5Vi4EhsSH)wv*jGU.H*6{pHZ'̐29 8ti׼##6FjZxW{%O9:eKzH=>}ZH"OαO2y>%Lv#;ǽs8EܡL&McNE: @P!H>kDf];)k<ݍ/;,2a],,#-l33RM)!Z[i:oQ]`91UP]K~tHDv!akD}J'3~MI{֑'[͎Zv9y`ӱ#T@3 H1-f8#T)WI;olЫf&Y5Wv2sTޅ91K{dH:@9ǿqVZC"Ogj HO &Kpyz BhˑHd$ޟʦ]E|@-x`w޵WN;PCaWrG֔.n"/kq㚃"F䑻#@͜;|`cPc*ܸNL50< dlbF95פu[ɕbrbsY!&FIEM&9BMCRkA2*zmLEBm^iV}eYorem s^[2uT+~LkYk;ɘI'$,DgzTrf:x˲0|Ykb6=V|:5ɝhCv*lG%OX"۪=1_/ʲvKhJMy) ak8syDZq6uJ2.L$|(B7W٢mr?Sq|M;让+SqGr sq>G5Q>=֮ñ(K_ ,I=궉i`Igw ҙh+ ݿLՠh:Nʤat'='C2$pn+@aR*$iŽ&cv@~Tm6:G' *6UGZzZg4F=j6&۝#9̀nwk&+i4vUj?5(B_ כ@i`8!<½&uk',4%2ػ7zz#7'd3t߸:H7>cXC=9'><[i dtpOĚQj}=ql]NЇ8RFHڋ>cmwOeѴ(/Ddr'׀GC~AOL8> |Cu6,R2Mj9ȗ#t]mdid9R<U#DŚW-e]{5 )g>љ,]6/RzSMi7[C/=F0y=eO#/q>_u Vі-l"YCGLϦO/fM}Oak^얛NJ\ΏTKo&U5_#Qޅmg;}ҳG~ȯ7E_-֭w-#c~R-Og:n#qO)}wyo\4_pݞ>Dg>K\yċ˰Q<켒0?*C2\9 Ú\n+h@>I 6?3ZHQَW$DH1#8N>N;ؕ+c~.P:gT! ɣ"h(รuDMެ&">JŐ|'aK5o]wIl2r0)XR_dbiayg,5tLF Zv1/őiZ}y7"=\NrlO5Hҙm3xW$r[hjhҦ:ʹh} ycrBADhӢ3DfGQʠ>62#4I,ۼ *3W5+cYJ!PP⋋)!;!#`>IV;|h&zmV ϧևb ȫU+vbr=l8.OAM 1`yW 9ݞ1؂r埥Y\@'Eىksw܁P^w8]fv,QF+QK{QzLqH#'"$KySR,r2?{֕lë}xn yFX/|S4&x>id򩡓L@@އIŒ:ε}\C5$İ#T/mP%F**Hk4Ln獾 }@2p[!Pp3Pb'&LHdhבA'kp$H?:b^YI &V'2F3qԈb.cS{7ȨH] \e-efa-o_>6(!~im P0%@8'vI8Ŝ$۲e85i:Ь+ܑ9Ͻl&w=* A_pq8ހp;Uv 98)+|*9NT eYX sUdhg(_(PQ>Zb`*ꀁT 5xAD8#Pܱ/B$'5`N*PX}¡{"|BB3PsT2-%JUu2h% ANX8DD\՞I|hJX[ckMR~3iw2p$R>StC.L|X3tY;G4O8XT⸝J"{JB=}|ON [Χᒻb${ޫ>A%HQ{`7$<6('UrͶk?Fu+ |B>Ӏ>5T[>[Ѭ}Aghu^j+H3"t AʶFDLóˑ6qz2d֓$#N)k[؃!ICeC6#RI-)VIe]4N<^EggS IM8Ncin?e{ޙ {wpiQr#2fAl[fU/We٫ FsYpuBPT!uBPT! LLY!5Fx r}dST4<{\)9#Tg;z4O}_麄O@'ut!mZTެO_>M?l!Qwn׾m~ľz~r4)֚jv`.UNӑcc ξ1 2,xp\f܇EQ:2-t;VOZ{ (̣xOi2&ME"ey;ݾ{iiLVLgy%9T*tϐ}-ʀ2jd֧"S1)$L*B 4Ҭ@!F3>r]>X.I՜3tc֠  ,8ߝ HZՕ`n-H7rN)d jvMmed# a241#fi 2{ "6t'eUc>5B(I7˗hB)<# Pc&81l_qb ɨԷ$P9MB09_qLD1In>\b',@BxL`'v/!y761ś3cNJ>NfJ}C(YX)Bl> GsPղB3p~1۽B ?:5jZТ℮~1 ZC5u@AXWqޡmh"O1-_Z؉/sQw BtBQP*  hPPqa wڮզRRSzqa!]j.@?eu1=շyu˻Am`K7Wc'YGzYöt` ׭ij<EZ|]ӱb͚4uj\u[ӍXm9'ؖynoliKz` x ׉ȥ߹G>'bmVm H өًܱLZuv?PY;%4fm*X_b->Vmu-G΋Miti1*aHa8>=|9 {Zh?Qj.Kr;1iy:S诋C'33kb 6w/Ζ񭝸lt[Ivz̓YHyA@)n-8 ;;SFϥu-l%xn#?u+'c2Lꞇ>Y_QYL8:U^ތH#ZSޏQS.*;χ,v(_) ^wOkĿw)Y:ށ$mo 2}b|V[I,;VεV.FXCSJ:8 1هsoϞ2K:5}R2v/wGE~n3\U=Ük1u'CJK{*)?!r~ZQȌ337K.Q=AI0ԺKWMFЮv%7MS}"9p/wz,t˛hKshm5'$DTH9ִ';'.0h>`Kk7{/ck6~E*I~eݝ|Ia{W d%?2;{~l";e Iq, I7 9Z:r)Z/r1kRpGѬҔވ%Lվuh@&e/c_zQpmJLjd̔ |:~qֺvȹ0Ynb} *Gڼ6@jO":7T]o՛|O4O#}8=C)o*IoDQ]gQէoyI죅_ʅGj2О*Im!t4q9q717K[]ȃ6 ;uJe>#X.UF1kُӭ՚Z/LޱRGց{%=?Z+ ؟jó:2٧؏mұ1]*{NR5Hj^;R5EΨYبQبY :C$:228wUG3|,Qf wɋjZ$Zfg!GM_*+#v"[+x$E7OW<3*s TѰm?ZӰ;Wn dsR2;X^bD?c? 9$8t4%.&U{T#x+v;"{+HbpdÄmh\Y|i6fޟ,ӬQH"3Rp{Ui초C2Lrv-A$*go yq)jh LFF M\b"BA*KMyij[iF y3޵G.ٳxyKג U{846ǿYnyV{cn8Tq.K3sɋ ej{$fMI grGZkBYG cg=2fq$ۑABmlJgߑPoȆj>@[:gU ՞( `G`rI٦i?Ype.qT]g$\>$K&§ c!A(:-1?ȑ1h[pd2*89ڭ=I )b2'41Z` 1Wc۟D)F"(d(1ir%}(%]̹p5qJ;xNR:N}GuX#[k[k lEJ0V _"Av- >XT\X"X2qB*u@N(.j ኅwbB*"9>Hz4@*Æ?*jMPBҡ =#Lu4@Qh=T{;9oaNtվfVA%>~5'9 ̳7vϦ0+t8=n^cOkm2<1`N﯒ 'ճXgmp5-ma*>ۚ}fb/gHk:-SgN<~{<iו2ν/LW8*zR[!?]hLf*7G&{0Oƙ/%Hn,Io)x{8qsz]6z?VlM>}3 'Oٺ[䖟Բ6ٗQ[;QU f> x]KvRoO vnoxl]k)zyUwBu=Μmup}ѿTq:NuKq5[Ud} CDe+}w6%ʫǾ3S+Kɻ_Z}6WB] aG )w˺>c}i|0QwmQ^W>ȾYXjF vJ13ڸ[*ߓG]Kc6{N1"']իbecuӮ`pp@'=~t-'o1C.}ybmxql rVzc}~a.>*n=ȧ3kEB}Il@؁Qߑ6{/c>>j+_k0OkX p{Q?#>=!.pS2.j4WB"2Ƹɲ5/!'[C>z+Şch%-?\l/dn'JŸY\Yܓ|%6AMDF,l:#L{5 :ʠUhVۛDTc𬯴4;E2 S@+=4m]VndZ$ֻӣS.9@34Cr=\a*TXx=,KCpˤxDfJ֪壝*Ȫ\4G$ z*(%J0~uE- T@BC:(겙q@RH\4*OlbN#Õ,z`єW/jhھKqsbuKZ>yhU=l޹:8 rZ*=iwKl5H#=??jlx wQÏ(G!DA6Ŏr~dzEC%IČYaE/cq9@y Y\pC8bO*@d{Urr X H<7ǵAR &vq`r~THC?M('X$I9֬yڹR"$mkRьN|Os~SdG^I59gbܶ1(8! }8& c3Kqf(X  mn;P쨱;?\Il"ǁ~UzqI1xGœ^AܼҠ ++"y cg`3UFL{{%4NI'T⟐&uޣQ?65O}*0;%/%Ez{Sd & ġUՓFF9ڂK(I*Z{q"4Q9w- , o$d$7* 3rH:! #jE#C^Fm r~pD_9U8q1Qǥyz 9S%S V``Vj5  "'U#-1шa;T`U1VPdҭgś]\GyޣݘGtc%5r&ZGG7߆} ⺃=NZDW:g]kr:ˌL/4WӣuI6=~-?#ޙCz4Lq\f;%-g1e84G>y-hRD<_O^Qw:U@v4#a}Q4I/,;7&`dXȤw4:Z"ɧuz3&?\4OZDR 5Dx^F߂J Y>T4N9Rvx.^k9gmTY|jMl e%{Zm$YeGt9RG")"] 뉾Λ#t.{ɍuu8◃1'lD*ڭLJMQGˠ!3Q\DFl?^bˡNG5[E(=959"zL$4 (f>UJ=}=\83MU1iqS)S\zm8,ȭ;UGj& J@yU[Z\ .U vvZ:ve\3V#S&Ű1TS4h:2^ش{F̛[ 5`T(!P8Ph\wFy lPT xXU2w?UFQAnYgsnWY,,fK 4`Ú!`bV  D ZS 2ǡܪLkct16ZJ3\-ѳVSb;tuk-ҶZf#܍ۤlWΖ{j$g[]mk6F⁛ 75g7@04>,v4FG/5 E4֠ @@"E. DvjBHF ;Q3SË&th=VWHjEz0]"W_5n]f:ڱA͞K/Ś-ݨȬKN,t9!h!Ű+4͌זMCaa {hG WaD&L9' CY jh5 x0[/ElDL YBOP4<`"DV&@s5FjH E4%4dHnwPH oz-Ay `(H; DnjQ 0cWEQMAv8J'B VaVeBB2WT08j*2{)xfƆaMBBj lT[ 萉5b :AH>eLlSZ]LQgX˱|Afq]*|юDsжՔBVP9@AݍIAꥦiUR̺fTwhHĶCǥ2^ ZOߞ=kد$AI/"n}7!ǥk3%޺,mL~ ]9{|_s@o.XR$lWo*\T!aZk- H!E/C آDg 4lvhO/l:ִGw$4׍Ӷc75f\L؋L/D+, zVi#e˘c})Վ54O]ݫOo4[֮j" er{#s&w!z{:KjhLP254ڐDgiֆV.SCzd]NMc4ģb B,vՑ By4%%e$zcR|6.HjR<,#-B E )x&TC o. Xj6*+`ዱ%[IpZQ,5cBuBl#ɫ(JCVA0`3uBPH*hӾȫqOckf/"N7Ya捘)է|Av3朌sf @" PT d89eM躋"S֋͖m┡մn֛)j""V[r+K|ۤ׽Y~g.R=r\be=muqtoٙs^:GɖDyw i F`E'Ν G sFĴ8r>u|&f1^[346d:vLZ*G~mb -#jF>FV;ZS(Ψ@ B *e#4! `UlnҫaݲxGڊ2G3=9o}ia_OyL"S}D-OҎO2`fcV%:93vZB/9Ox {KG:1m[3ֱO #8y&fɇtT4$Wh0⩆9@L(MjŋBة&ҖgN{4ZF\QVXjlYGS X 91REå|ˆѶhgTfåދPAGL3N14:Yk XœwJ"*ٛC)Š%ӢqEY~6ϔUVˍOgo f2xOҌ&;*rYcG?sn6.Yt*fU4= 1"f0葄b^kLtA SF|iF6;ZK(U56MU&eMQahmy|XNssױHь\`y9/AC-2ᢍc] 2Tȡ>ugh Pw8}N]Ʒp##>Њy݆/֞צI4%wSuJ{a?V[9;m!0cF,WƩfHYAک:7ݚÞa bm\Sҹ=Kׄ֕h>t5A&/wyIh/g\ϛڒ࿮)🤯baIy\cL4Y~M(6D'~}U.Ǻǧ.$Xu9IPe>qkVeJڟoޟъ2E:G*821 /fFȰXaLCQSBtd*Hg"Z#WdS)qr:xIw<|EݼKgOzf-[r.'ƳhYkpRD,DyZfKq#"u$ɏ9zt_ȲiY".:5#oL_B˦̟9߸fO֩>1oՑ0 xLs)baU/ 58AG [ZJGB"kU;HԋCR݌U fq6c4ԅVցgծȺ E22<:dV7nHZ#n.Gȩ_Dcb1@3#]4 &aB0FL Ú`$VH@_6$V‘VXR**)"bHBE#7¡hBvj [;5`6jgV@h7UwU2*`f3PPBY 5 5  5 ٨M!٫ :ZMWtKZC٧UN+;|ǥkv|ޙm|`dO6雟ZL{%?*zrS.v۰ϥn]Ưr2|VHep7MLp>«[a,2VYbAYЈiEdudBGN( #O=PV-kڠwnRI*0N`:vOPC S''gY+2g)}LNOl)GxuQ~;it/]G,&Vv  79G*P^v23^Bs!d$gh-4h50:GQDSF e* 6F_O7;ϧi;TxM62u~'f۞*@EYu~Z)#a]y4 d (وY$M[YT F)s1!h.c=*ǐ ݍYXc5L)5Î\ƇfbE{#sUupQ7GFX3V|%=JX%_1ўk}xV:'Ū[ Se-QJƥIKJmŎ{R'{gӼi궭 DhjYڙ/ȢF v/-2F26'ɶIz2G.Hg6i[ˠT٢9CIt)= chrTLD1SA'/OLjhtWo=!O-7 Uz-uzrh;TYzn{Y TQ0$JWYuL\Q&vjsc!zb6c(kcCfTT$,VY3?J$29dg Tm" @t$*S9:6+Tx!5Z#{BѨz$؅TP h(@jtDfY7]k9܈}aq~'\ mXFb+R$ΕhZlgqSD"uʚ_de鎬MCwɖ R9z|Ǻfǔ>Omۏ88?sܩKLCZlR6`A1GLN_g=|2hdC\j7&{? N,I(AxFZ;~JƆ>L7َ;TbaZTXlM#Ҭ;KҶ;-]t4Sɣ$Bq1*Ƴ_F*<]N1BIVLMXL'ZXe#?S |BT<J]V1Nkt noһ+=m?JdULͺZlcҒ=k:}yXQ58JK=@G;2mW 59v6=C&DPتD}lT@Ҙgݽv9CEx'qcTjf{:TZ9O#_!27b&Bw,^L,Dr#}LٞxH-A!/zE(-P!!W3cu=lm !ցQϲ:􌓈;NY-$­E bI%דLi=A#]UZdb;rTfh4ؚ!ndX4:BдMU[PWC"Fw=}#T(MZ3X!H@"-B)#A@3BT"B Pj@ @N,$‡!VGF?kIs4,~@rhg8nOHa]tyw*AX+o]Geq1 f\4gٯ9g. >u(Z+?E=S[/փ)G=IMw(Nv)맏"j dVZg24g*gL1՚%l-JZGw|Kۅ q^s:gNJڛ#>ČTvg=Ds1#l5-]cdm &HacWV΅xя:]'zz~Vi@6ᜀGҹ[ؖCLe<bԸ;&OR$f-47<21oҟW^Y5R'(2so=AsL UWZ2(VҚp S LU8P6Ž^Fݗ$5Bǂ&i(NX?F|>Giؿ^I>|@AN'KXWZw' -KROZ/]sC[q%?U.~Ҿ /whzṘ5KUqQm?#VʟjTѺ{AкrOR0΋XiX&vKr2R$VZS`te}XM u)ͩJ1bo4eRRhPB4R㑷β˨cG..qA x4CzS}^Y<'zh?ȿZ}saXlz4[_j0$rb1Z8XM Md9PÑfn> wrO[ޯP^s~S~FV|:t+NZ%rH+f7W|kH+Vb.!G"X\+l+ε8[d] L֩کӊ8XJ)BCv+<+"T%)P*9ő0jTi:|9M<+W6e{YGt25ܦ*VbF][VOJMn#YZGĄ[jG6zCisvQLDq:8֙D(4D$H8֔MbCl6:8&rюf+RWIcYlV̌ j+֎xRE JЮBR -EFԩ#]m|^K]S5jͭn2̤pTعUd֬3L"@K j9wcqbPӋ^K~̲Eݪ7pyje-edI5qwr;0dW'ԑkdUzP{dUQ+gi},|CMH.QI z:~[_)(^[Ä5?=IۺSdޒ鏇 tn+Եeq8Oj}yj}G]`2Y{~ ܯ,.䟭qRgrlOa(2oCr4i_/;x{b}?VK^m3u;pRT`洄@aWIard#ƿ~}׊>V?R@JMtj4}.//{ˀ$}y~^+/?'1]2O"R'dyd9yMcQ!ɱ=ވ;eMF͵M[j4+Vؠ޷!]Wӭ,%p9S*jF3`ugdSwn?uõR./tQZk%Fʢ S>ln<{Lt.\Kzz}}ݿ6>==Ͷwsgx'B=1-571к _Dԭ!n"9GjuhebY9kkc,oRpe {r8EOE ՔvPu9V#i2lw .a*UE O%0w5@2bu1:% ?Mr#"׹]t)řQ5՚LVܩ TVy=*'g#pnѺV XbY46oWs3/͟;忻vc-D`rIHՐV fqhejMůvw}QtB%ݜKGw@;0@:/YsXq~}LAɧ/E^~l8͛R͹Z= _:eD`TNlMON$D`TNlMON%eG$-l68Q+"?Q7#ڬ9Bq^v}[QZbUŰ xފ:G+^_}.5=}uGq7s_*q3s`o(}8cSӉ97?=8;cSӉ9>7?=8cSӉ9QĜ_\#L=vEFZ/G#+R9TuJW$b3iGd $h` p9~K$]>}UK1m?T[ual7%fWDN[[;$J~씭;穄c\xi} Iؽ *NCUٓaCgC}l?1WTD'ERZٚ)5..ʣtw:xv~zW7K~b8U_C3[iPmyl{|v &y/iV[ҝh<(,8څ+ A ڏF[3Ƥ(d`,lIY,lLEYvxLϱjLWu/ g S BERqKƭpr άj|U#][L6"H;@[H0hY 7z\k҅P|@}k ͜^{ ZM-PVV-DV&ެ4(8@EB!T\QRi GAB@f*2jmꎶ-?6k:=vL}㋉GU^~cʏ_4:{VaOc q=PWڗ繛EW(PIQi&㤴]19Iץj7N[5c~]/OH)waDZn叝+ ͽ#Q>g;eB@8+oDJcEK'lB@$l2NA%mSNZh x*YF7 v֋]n 0w?c[tI^S&xhb$o†=alx_gBʖ@B.Y+M%vGmk HaTǭ2s&DVT;JЛHȋktczl_y|D4=]Gldh$wn}iC؞[jI} v ^8|?Q S[Eys~@>_J#?:;Q* Ⱥlt^0lY/ÝTGrļKIzKY=,8O%eqF-W6}M(]G|{,_}mGtLx*wBv1;[@_ywkݝeZ05d9Q-@}! I?늑M#7{;_ aBi,k6x_f&M4Eةk"hZhT54 Ml& QdVY?$x*;T!Q"EpN=qׯN/m/oW[+-mβ;+Wdn9 j4gMVq2Of^.Ʃ=wL鱫^-eI9?zfrv)A/]EdVUM/1Oz/ 5[n޹̴OĽ3\[ wd {|bCDe-v|0Ѹd$TkLبD[kmv%I2> ??eWkk_s8G5JGgu?z?3_3_lq'''= lQ4p 'EEyd<[zf1JELRpjWܟ tcQ9$Y囑7MlکxeVR<ۋ>Mxg OUiwjDrS}gJ )[Gv-aϒi闯'ȨCz] XNJT[[%'_ƞ֌=WiFc7deÅ646B*۩!RAp`NGr,Q7Z\e67tZ0gepJK]j=׍kk 3ʹq+ulGOxnIy>(MGZ=L,gH=˶Վb} kݓ]kėLdLs}JRtⴑGKQmڄ3sv`g5=+W`[+î:0o\ ,tXd*L{#_E> {|c|j$ZRs|ٷITBUOcnD7{s0H@_J{.QM=53Hn8UJe$Qe:bݭ+(V+Xu "C \j=2JOO}W:Vw|)CxjOurQ2>͐׾+ Ewq|O__ltmF-:dw-9\A-#z^1y涚~~Ԣȭ;uGQ8F߀V˽~Ď2fO|:P<>kgJ7?hlq_"Azg3t,d}G1NoFTDj:6VЭm|S6c{Ahխ=O|>Ժ#.ѵ=W*o@q}iyֺ|%O7z~ ?ï;èBFnzSVhUfCT@>~*}8&cDTgU?FHGcEv|lOlCECe*/Z>ӇLsV0,V g&G$gGSȵ]rr6KyH҇Lrm|(|FZ&Fo9hcu 'О9(xuy@ze?>XW /fe6 v_.2<*}i˻SBSRYPe&qk_orz_ő7\O x?{4ي+5ú)ik6}=6J׽5_U?`$ g^? "=t 67f$WFE<մyL`1B0jq4U>,Ӯ<)cwF ? ez6oM?j8M_x5}r_zzOgjZ}_ zW8٢٫Dn #s5?qc-,|3R<~Y=?]a:\kZEzkï ;èB;[- JP$qՏ:zuG"4R0)zMFiHrŞfqf:£'g+Y"vY=I?v{{IԛHs=T/p>gYxx~'~tG {pa_/=BOv]|Fwz g81zdmyyۗZsUJ2yy|9dK#]eܿ"V x4F @KXCqs}Gʵbʉ}bKş(]]I\w0\ xx?|ڎ (KޏBjqR^i΄$C94R)GS؃ޭJPjqzk#ddjYI ;WְrV^<._5Qt:_|0^2Etb󮣋(M,(!V c"J!c ˜؛jaW 3iw *''o 8rٖu1lqKْTbA~Dfj),}RqeWPӎGchFNyZ[y}SJe FE*z䅥'g&To$AzoRD\AFa /br;waDXPT)-^*DPy,7Ya "BhV8CHpPbZh:i :xj߳n/޺,-Aеp#K_~|靟*+GlT&[讀aEa]g)V*. .13 -m$0}?Fwqƺ]^l׷KuHޔ9=MZ/.ғk)/@>k(6||GlT&MUI̷!풂GF}O+\{yWԮ;ǹwbI'}蹹qxF/_s??fVGl6âj,kS9mոλ~VԪk^%VޏW:z=BTa} 'TEi3/%gʸ`*ڞ,܂K*l׿z,ѯvf?)!xwɥ,$ XyI.ay%hqrnAO򬹶x?޽{$'=P+_Fs,Fa txZ__]ln2`v/zg{$q3jjBEjkǔiq4G11!65HM@#u#{7Koy 1d6g/-?˷џ2;`vrϕBh]hK*K<5]<"CnAç|HߌjZ.?*: P@Bh](NՔs5X)_'1۵\V)e9,ݧ[+ѤgʸAh*/Hca_@||&Ory1f1> ^b#44>m em慝l{E?eE@+W&Gzgʾxf>U qo%$x{~#ѳdyU#Bѭ:(-$jDNKvW:1hj#T36; /Þպ㩡t|[;8;6:'lGtƕ=;m췄ecO_8ʳ.mqO%|m][i2JAvcfM2hƯ[W)i{aaW7KOW!z&'ֳE]F,-@@xl|!VP+Q?QO^'ګܭKˣGt͟*_GlT&?~m#·2q_f#tK_QћZjc]T][ y%x5^>VX?ܤB2JG+V 4M_;) [gʼAGlT&" ]r|h"+>]U}JTw5ϜudMRu?<;M=MSlqfF#BUxe.R x7ߥcdKiYdo[nL=jPaD\ZcŽWWv<@{2[4 (#2QvT"J  RcP46As3~.'Z^>CIOE nm$ZR#w-ema>vATȨ8T@բh(z;U#(%B^5CP*QBŒ4*hT&UQ]0.Cz-ruۯ ?q=UZLJ^)F= cZ:M37_1* V6tO:}Ix/sreì` M,eyY[Hf"3AHA_[" ׹n.@]c19$U'$Jj;/Wu9 ߤkTr[~q 3P*a$\t.* l`qrqbދj'#ק 1&OB'LdNCnGi}=H`1EiG7'.&[ɞ7qվ"Q,V'^psKhNEZ$3)Šf;\韼U(x^(5JG왕QT)=Al&*`T4uYaY-VPF5 *6+{$a) #fibT3P- P(Svc}rOO1uWMW1?ix_;O삻=e1G.{vGFI=g83g`21͵bR ߇Ffx*;T Tޙu!v?Ǖ_z%$;{=JZ] Vdn+-v\C􄛾#ht?fe,Q^|'x*k ݺp Z1w?$< }344;"Yvbyf>dXo$ʡ t㵌?Ÿ/C }Ix<_9xU/=k.?*:hWPP~!qպ._b=Ȣ^4G.όAJ/ I?_>|d|Gx*FǽFwp<3JAt͙?#~ʾtf;T!CS/?:?3^1gm &-۴Ȇ)^[b͜Rfurdu7w5;N0=%pq=/}þp#_@on"1CQTRͳKW0 `x[!q$?N )<~T|(tP^cˊmfyeyld];Kq!. ⳳ[E1ܩϭNEtLh SPO|2qqіr d״0'?="fU4Fk{rG{n`)e[ m g#ߧmpT~͖SwDo8*}?ȇ}O;"?-# ui4=xo;b]vk]!;< kM~LdNܱc,k\&x<֘X-Wp 9pZke^)]{ ( dyӣ%N)-sh gBMkOҞ7da*'rM=NrFM"ulJ-k=ZeZ]FHN 9M"iTdZ@f5N&[&up00 ӜPq0].#e=*lb_d%by(UЛ 0`VYeMY@P  VQ+aE"sX"h; #QvpͫY8]r=ϧF_25ǣvW*h( hIuc]g cKl5ΥHDy!&É~:{\|RYRN=IUu’?;1=mT6W$_.К` ZH?#5XPˏwȝ-mE8}1!_DV>W)r/|cU3֥$Xl34M5/1Mh'ھY|$tz?aNLV4cLA;˔K/Akvѷ[*dÓ'ue(tÚKe{=b8L \soJy> J 'zZMaUaO8IyE B?:,W6ݜ uxZ ܣuwŎ \.|(#S=9~csbeSJ=H_mi? s{Q.8|\ʒG9Wz?.+;eMfMRmktnplfnGl_8+^ӋqQȏ)uUǤ7tɦ&6d}*#}GmP}/_gGb(4M6x'_k5*XK"y K?Co|O#ꍩ4{;eMg?jQ>??/08Kȓ*+݇#\F~*K?Juf vvʚ&vz :\57| ,VJ[6qL"Ι昘[«ɒ۳4gv *'R֎5tfG[+w 2>ϧo|>/J/g>Mf-A|Ofд玄O*`+= / IǑ+ٯv%^+*yVs_D=EA׳;Vf~MDd&TbLVP~uhǹt5ƒu6&+g>]E}oy(o~^3\s!fJ1ˏ7?vWѣgllywھwl̶0ȹ qN0ԡEpR{Er~s5?!1Vuu)nX܇+R1Z9.GэO?K-XېE~'/d TQnC՚20|?aqXGS.oUӐIyIeAC}%0uy95c]ݓV1O2m{$fG5,=*F4hd-&m;$cלQl*XS"px19))uoaRD:o !2vNI(eˍ ҝho76$z5Z\ʃD1^ɢ#hB) nuw}ACa`̇ڼkAjЋl!Z4 JaI(P5 gf Pe58jC6T6,YfPbT22V\Tßr^M},!i| O_~Y*Y0^\vu}=29_)kOGu.-cE.8Kw>ԯidl_'՜.tJMsk+C2H#" {S׆F<|ј,٢P}u#?O'u9]c?{L˂'52oo .ˏII}3UgvBzRrzGzר``SP{Lk>J?H2By#u+3{qA }>.{)5䮧[g5sJO cgf&Y]X2zNBӖvaX #9psV@4(tKAheמǻ+9[;eBliajy:X@p3^v5PE5g_ NDm W>Z$\w:ɵ -6moq^*6xc;ZƯU?F)HhMdosCɷ}'~Fh[+VP32 J*51O oq3ЩEMMW˿?"~XR9Vc$P  z3ZZfFL5 SiQNM `vjƆ2sѲdnBBսν> nȅ81˾AgƄ]j :ъh(^jf"A)7zP}*{TPPVPi,$+UIc/!A ތُn9 {ll*_7)%Υb= xj?rݵ/~4-+`lh?7}OzW1FuOqdst~x?'9R;||vKkm d?.9Y fKjK.~ UDgn4}/9#b>܊1K迟~uUOo_tgPVvRrOb}I$}x[;'yNSnR?J[KY.n$XK;1w4uU+fˎ|Ws={=E"rO_J]/ #tl~o0qJyRkL=XRkRϢWF4+" U>~wPǗ &ZUѵ_5 + y$I p?fm0J2MGc[Po'6ל־ DsњhOl?O/)s_Oc{so!X` tbԒk&\"`[c-A/5 X`x.u6Q$)v5l((Wʥ76ڭ?2h]Ek}si$+ 7#t:fwد}u/+yGYGL໅|ߟ 2_cCo6to?QlymI_r!f ^2 k`/qז*s" @èbők&K+2@pU>ձ?(!?fb>̓بLU\Zfh/-a5z=y>αˏGQ*>[ὯG̵&jhkKڍ2g+nJO5a$?T."),o0˃]Kgz+aj͋{g+6M5T*i^m4tȈWgq}ʸy9RD&F\%Km34M^ŨXXO&?ΎSVG{)KgzG:w]uH Y 8a0` }ndk{L܊?Lč6W3TqM:+gmM z]П5}rqR%9[׾.pP|t/h&h{kvvښ&+ t{?GOs]':;4),CgxcڳՋ+^[c?>(c+nm"_2UOCWпcW𷬺O~a&(Y千:yX¹]kQ~7Lg&z?my-6vښ&ōuaOyqc"ĬyʮO;ޝacfvx˺?l7p?u_8?Wh1֟ zۣ4tY#nr  iΣ%T|kt(:WVu-αI(hFVi[%Kqq2Y}ֿv8Q=x'5tsW`6{;mMfCmb nm+#ZJ}Ot,<A _%jXֿM7A?dC%)\V1zN:1WW](FkZ^gɶU;f8~O3 ק>G:zu{JLWWv̩cؼxԓZd LM-5y/@bi.00C_JUe|[Pat/DպsUau0GW.]їE~,4]Jm]?, T>)%G^Yo+%lۖR e|rrz+ݗ'_e\|=>|^A>nM!x),>Xbo\}3-gK=',{KkX^L0x1CDyjq-CY Цb6HEp֋fyVP޺NBBc55axd֦+ Ly5Z/?'tmQDsT*Xi+Ra@rIժr1m+WZHYoӮRdgh*tcOPp$q)ya" JPBr+3Xͅ\TB/ ^Pƒl[Z'^1^|6"V!$tH[5lTP'2U4u@}*@XV HV6^ЮyXMؤm=LU3n֙iwڇFu$:4Dd_c] F/;Oe|?-tfPİ1ޠ|ǬL{LYe\q[) &Ée!V,,jmB's,6<ȑƀ3(\V.;+dF˒-|*In&gmMP:jxZdaXm1}3anXeڣ(αg ~$*>QtWg1~@VM?({F5}fjcR%2(?|w |g#OWϽ/?m6yK]l_{ރ?G_n5ou8?=p= 0gG}hSD8{ &Pަ;ަyz??45Y/o{gtS~ătHz{O_Nvk=%ajgmM6vڄjgmM6yXWC{U{f+WYj[r9Wc|LC&RH𡇓4q.5 >. 8QrѤީP }hѦUS V Tfb3[c#)jn'T<84|#L+ΟQЌF %L!BcU-ݪh ^j&EB!V^*Th-B Bj^3P.Bx CU68m7)"Wu_RHE]s龯G~536F#x+oԖYF,)>+%9?r,IKLt AYmqu*k{!i=9J+&`>-uVH;AfI?zX]/Юv} K3K>$5ɲ;,{gZF_>;[:S|iw\Z&wjzū.pgVǑF9~}t'&<]Pd?-g)Gox˫_8ޕ_~)A.oU~Dj?i "Lr˛_=?E'"Cr7"z5BxƏ.q\=x%?|W"zU}O5uSꈻVIg8űx_~)/$v$LH4 #P:/5W⅝VSEﴋԼ Gju["</2~~|?IՔii ڏ>VpkG39ij9PkDxezHɳ/cA @L7cQYD4 b"@+1#mk㉮rFB[uE$dxO-#9e] ד(M8J`U^ M֏,~=mkPuVaHV$X;={{״M|~:>8~_:3ƱY?Дj 3k})zvQK2N=?__ PƿihzT5>"uϰ=xc:;a}GU(z&bPrO+.c..G|QkHdu{)lizMoƟ̡u'zkK"$gq(&&'33,QρNJz}UDꎴT뫗'돺>]STqckKILJI4OOImG7w9FιI?kVG-RdMt a>Ф+Vot}W8ZfhI eRvJI5-;U5 Oٚ~Ov.Ws;A9Nq^F%?g\e7{ χ">4CՠcBBO/usm6KC2geC.U~EzU}C?EDI/W_W/]߮Ԛnd.sN?ZC^]/hl\U_5Q<{#2;6OL^+5H.%#ƑUFœ4GK&&ZO#c?2疱˫_=CE?(>27"z5}LsGY[uTíZۘ#[Ui`NUz..Tq-bj% bލ#_;.u+S:Dp'G?ghq3f(JL!m'_l/s1z5}@??$?Ȟ_P?H觑HFqj˫_=*GD//5?Ȟ_S$$߭Os'WH脌,?Ȟ_PGD-_U KCLW&WWi={6FެL60Z_tjd˖5bF7УnѸ~? T^Y4{JR Z-r}k* n~#tMl;O]Rkj_٬+]Gã4u[vp5ߧbQ_ ckfGߴ&豮~}zOdzoM_OQP¶aCz5̎_bIkԛȪjcOBsE*HZMp ((hIl+j))qI''֩v9l,s<%#,#ey"ɦ)2˥j2ƘrLJɁr,=L\^D_rS}:&픺LS9i"x}hd6R3_3 i lӒ2MQu89kiq^xo%@ZeB%$4qV!T 4$PyEBYdU>%j)ښP$*MBT 5 !ŠX B* A`Վ*hUaE η=3C&Ԃ:َ+_Ŷ-)3<}hS8$sޞNVlrMĸA.4`k7f_oƪ02#"3-Xer5 nJm 5 GZvpk3kaoGˋ} CWnד En.qȩ'(UNQ;"oW(ENO wF75g.oaJdՒJ1pQЄ|0qP;`=~Tl{FM~n~QcJo2?ԗk*xIbWܟ%*ʿ#5*6]oj\B~ǽG|ΣI-ACr=n$2d__ڸ*fcO3*M}Qȑs跚?wNHpw݋KAA$2.]Kj[b~.P~kǸEñwaӗad7ںegL~vt{qt F Zr{UԦq=iz.izmgq/˻!nO{45hK@&(mغBH>pط!Ƭ2+9AA̫ђ^FA+2-);2T>c{g2Լ $j ~e"P̖ʗ`t5Զbp;{S{,F^J7H#D9GH#mFL yE/˄eou+ՂA$Te-i aY,x:5e&jmMtD(cޯeGN *7E>Y?zN"~"ZyWI TW''%-fLcl.ܒh&1ɌrrtujzV@_IvD4\8QXg4/aF诙:{ҟ#LnR ?GJs&E^.!0 bۆM)u#$8^" .Z}(0"HՇ Ԅ[^bŔAF8\z\?[GZddD{nid//ב}=y/Ɨ5{_>frm) h;EF^X`- 1&j9smsKd)BѮpK.؎ܚb1JGgjaNjJq@ȄDT, j2NC^șg{k#`j2Z?ќ4.F*NjlrFa rg'|Aeep_OAvCFͳbU9OTЭs5ׁx u^I ^jg+V0FȨG( 3]H9+T4;sU4V/e HT! "Cȇ hR1V(1Ê"0"\9A MMSJjhtr>)¼ lO$esremΧ͞f}+r=nzD&qar>):95찫<fcs*c%My4՚C zC;yO0Czz)"{ @E"5 tGlоZZ|WMpIk,d J ҲkF>x~ƟLGM;OYUwc՟ww}̯V tfhU#Nat-Ÿ@;P9 2NWdt8I<;WGU5@@OJ~+yjWwo2-`Ga 3j&D_ꗮկ?3ћ>UN>M_{YNǾJ?#^l6nS6ud7.L򬝫#>1W*meoHs-X2!cwɢ \={Adsb^S-/B1W|#eSk}m#@/^f8!^c [51 }VZ -(K9rNgMc 9YScq Y!G⭢b|RٺpHr9ȐF;hmsTGcb,j!b* V',C" H dny4inL-WU= =-޲C&ZC{ЍLC{/,;{+ i @91H*{զ_+a|M62wHZN%Q>,j\: ҵ䶋ML֚ɇcdD4*\Y1ڇBۨ]PJ'[>KTmƔw)2[hƲ\PfI4H;&lٖ]㘞橉q# $QP%6 ElPdlURb6޸HT;LQe!HF2&[-H4rH3o\FFIhvKjHHc&~#5iuuc5qC0jrSϳsO?y}8E=X"!$&HAЛ &`0j3V#YBG Z3)zC;Tp*+hYEXh6*XF`&j hP*l zAm)ygc_rp >U.z#=JS$>gW+ں i#eB m5Ի Th;j@bZ P`* T \yi6sks ջY#a09lj2};3SH뎏"h)  4f\/)K/ WF]849;"m=oRs=V7zL?$`>]zMkM:L-7ī0t']Y?mk>pFl*هn՟ƿ>PE>gQi鿀iyl.Y~hT"]ZO_Υ|o_ 0ơ#5 \y7'mUh z۫4nQ.%q#{]Ҳ:ʪVD}"zY5Ƴ~| ~Ǟz]ӅL0/#cV dF ~B,gluŅ>!i3duƥ녕Q֪\h4 s\AJ$AD5h'Xo7ҝWvx0̟zLybZu<ϸI LךFi qSTv)Dh@}DmQIv2P33տ9=F:B*DC8b$ -k[?RuX@bMPqQxrM5 lʞ27b}iRJ9j@sT5d:D4MSD٨AX*EMɭ.kM63Y^=<`fň QQBd,4lAc"Y]X1t(j`I뼢^kRZrMCZq!T45Bj:B8CS@-1-vnMi.s4KN؏Q]Ẓq?98(=7GNjPDm_4dך>WLIs³7=}մZOgWq0~yxYb&ҡ0n/ /`T q+Z8N,/5QYQrzl(9xD]#E]OeY1tq&vKu1ö_-']~]A62\N4hEk{ 5L|5W翹{zS7䓾N'ʣA^`||֫KB>Cg us\KsffulzJO$Zƛ/1*y{gʕo]Ȓ?˷"۞Kk`Re-%L5g"܀ɫ1Ii6hݍ=3M=p")zeTz:7>/@ǓDݎ*THGjuZ!HU:SV:4 ڦPLF.pWpz4am[L,3TM 4\0FR[CbE=(`L&ikaHe({x#KIq4#=_S^g&yB"9轞r[DL69Z="[C l8Z Du9eoQg Y ..&YA &ӥO5M V6kyB$˜՗H$HH}o41aqF-*Σz9H?EI–ZY ~D5ТC*bFٚ$4ʀv2P8вTiKBU惌jAj"j;5z@-T"6MC[ #4j l m@tvʅh\ hmr$AW͎k^ż>kbHKן3'XAҰcDuiw3ZpCɃ^ iAzRN)SGK% Z:. H;];5z 1L&LpRrjn du"QX-r#ih|%@X5gQ"VCCYhT@j `A& ݠJvR["9$fQDNE~kB)r6j"y08$ \C !@nH x[D~Ju"$j]%M#1{EŸbҩ쌺*Oو#$6(HnD(E"N` D&HZfkڄpHTȩn1!>q96UWHUE@Т/5 Cțc6j&*AQz`VK)/hw&*02>x6b70үdؙ`vBWϙƯ;M=[GǎQ&[]7ׯ3.½~44\*zZC[ǎQB樂sP 8X~UQPLT$\T-DBh[Bh &raj@[8l U@+@MT&UVt V1H[]/]E/\L355_6xE qX˹zG!8תÏcY64⺜{)]tt Cs:#: TUd;jZBF/)4!hmv lywthZ__s l ҫjG/-v07/SYLlyl "0NFDl@{D)sQ3%Q>kCoJBgḾshHM 7j3bmu"P ~D>vo5^!4̓qȪPM13uYCꈇQ"QZ8U88Dbi`lF5Hm[-f ig&$G2bs'6ܩQ* "^W5 9-"UZ2S"eх*-[Y-A諡C|$yWgJ[HnmA)F$#!^Ci4A .A*9 PSRޕ[ ZlST\[_^[|)l{҅M]َ>qj"56>[Mǥe8 J!m * hE( FD^hXJ"a sVD_* ,*q Eh:2jHsxAhU*1c5lbb,0LuIVz]R"{6>мqP9"gF OHuhGӷy|\dQ]ӕϭg;*qXP>W֭6C1SW'$նz8פ4/@ g5`q TZC !ei XItSCyRZ #TXEqU'JEeYu>WzxD1gt\{bZcl䵟nୂpGңM=007Pƚ~ 1TjuZP4ʲ#ZXsSbǥ@D^f3{a?:5ervMh";5+=uFL:M s^vhRzY[;Q)L= u!4 ]ʵc=>Ѥ7V_h?V̶;KܲU\a$]w)?k+P"*'7 j@jmɨ "$Հ;eFY3BqB pAKu@CuBTC"DuFCYhޡ6Uh[ʞhH*,o"l_/QjS. uwpv9'q~pgYTE˗n^R.˾|!ivcw!Ai;?CMJ|W~=oEo=uMbr3<[_]oTe[Mv޼tEQ T^)V/î=%Opi0*9ϊesyj=Ǎj|e_/Į=]٭)eNI'⥖s{ϳؔJ)Io*{(OGͽ^iQݒ2@ ArI&kvڀ *$)$,+3C 9S4hTg.)vՎ ]I #..2OE'^1Y٭9\jZ6d ғ| 3++'{krOj$yHEi+I681xeg3Χa.lLvS@Ѧ2lZ8XG g5ˎV*Vc=F]zA#V<K[:[Xz6JDiolP#`K dp@EGfn'߿w&[}fxU%ty`HwUEiݣWGui7da}8FoZ>K=xh[KIKmntmm wsw &w}vBro|ڇ$s߱; /P,.w1B(9%+YacѼn;~#btzk17ZH"iRv@Hq|ۙeV-n>8u,6v\KlxI+-=VBL,z?TiKSM G Ws@UO#u4⼞UK~ ;V= ,[g2MEpF8ն#7'@X]gӂ[LXE\(2RZ=vNtɷyߎ,l/ܥ eԷ9% = 2C*IAq'D:SE3UZ$ aX ^ bɽ;Ux220[eup1%9W*ٖPh+j&0EI:$A#Mw8/ ReNIFKnW&ޞUWeV[J :ĭX~x&XB_5CMFiЃ Pe2[uOnO8 r ?GیvWu͋6D=(%IQOl>xaSc4껯Dҕ4CU0Lc?FIRߥ ίL Ӌ'/g55:OZd*g۹c[v w[&u떳M=>q*adWߞOҥoZ:VRKJ)FLZث(R[MFuG&4QlZ6+\-Rzm /mvy&õA#^բrq.\NR{kf:M=;tK( bQؤFs\+%*-^;|@J=ƎVVQ,pFG#xQCG/!K_^h6xb<p 1Z-|cl|1ऽڟ|!Jֽ]"{H-9޼zvЎy,ZjK3cwԺBk=:7][=sJogK^lT_$E>#Y3vd\s}ǵ\Qzp+xoI{?zcJˬ-"+.xȸV 9=jZ=eQ+:eX ?ik i--ot^´VQa91Fas^>FD[Ї_})6-ȚV6 c UyD3nQώ>S\d ɖ?Pf(xoJd"+sRI}GR7#da`*zdZkyõTܮ {zZ?ۗZ%Jߊ:% * f&N2|cWjܕɴxb{;m RWެ rzٻ'uOidmō"5 C<AUy]}Mebڟ\ѓHg>@X={Ol4~?МՑXu~R.xʂA JSIJ__6UH+&?@+~J w\V]5+;:/ƺ'Bd,2;Qohm7/e>oE72?"5@0M$ު'j#g?v=zUh. }\|lJqޏ bx~ c\_)/yonW?;[ tNL7=?t}At^W-.RWD ( Ȭ;cΤ-hYnnR- ֥wb֥w4~JR?i@Fͮtɦ7@q#%(YWzz}F 򒂏]6:ꯌ-?:nbgOmE\# WͷU#vм?f2OY|c-[w+WtV|?XMWJ' $; ۆ{cj yoT68?d_7q3{s/eYZ<`?_gN^eN̖J$@Us4Z=G_ꏦaJ^!i+Dy.8(p76,ğВ}Y5VQnѮ.ȵ|[t_0$he$e_jWC16O+ٮ{K_ne>]iJ{i@9QXln6GI7pP5eM )( (Z 4H}}k6Gȷ]ts!KVGWsuV_]sNtm1hƇQP j6+@h#G+BlTB* #'PZcRê4MФqgҩ(xh8#/ K) F3G $EzTi*"h$ 6I־,!|v`H?:]O&aJLk_O֐:<9ޢyIAJV]-w.i=.2k㭥G٭jͻB=tpE4#Ԛ|g/mN(Т*|珖ғwM|-.=ֻ^<&[;siKm&d`?B).;S%%i2k·^ޢD9FT{㳅WV5)7\5gd#DBvz?NGĎ'+?5/=!m䕢w<c>:$̟RO꿘? n:S_5i뵎3Rݢƚ vuE~ԷNYO(^H|r|#?+r. m?:d~j+p--9`<+D Quҳ$Nޓ+EŮV2MsYR}7/*ߢ+MPo>ͬ"*?ڍYn)!iKiV+o$gzȹW1)B`&kig̏ /~+CKEs| M"U@f$}J<m%bA&1ΩjOOKjB" {dݐ8wOwGi-Ƶ#A$'Զ˘; ?^g"23,l8W@v/{;tNuv~|N:R5v\+z,!wȻ›gF]FûRޒ16>0_q^xxp}q붳VI=\,+)ny|YgNР9 1ǂ0S'?}:O%$0_a%1vOM{E [53}g|NO[ۧZY\!RR2WϜz]P-3_V7NVbz?m>NN]5 w(߽6AF` 9vG{bOR̻Zk崛{/=+sjn1mٳ\@cp11hn=&d1͔hfe_euY]uL2FU3{tʩ6d:?5zxSx,ce(1``[bݍGF5'_oz? u0{?VϞ{3L={*Gu]6dO܇Y1}ۈ6R_̻~:j];wi7 ѻ[@lb@;A}38c]жj-R(Nt隼2Y]XKhNdܒm cOjJ_('kf񧭺-lN唛՟t 1!Ay1Zm#e*?[u6wGĞUE\^~x'RZޏC/dlm//ޗgQjOrܴjr v}#D)O|R_e0  0El0aD 5`6,B܃u4RaM)v,E*mqaFN*DƖ1"ĢMnS!;6dy&E'2ZdXe67Zg/ey9cMUeף}IP2tgҘ@Fz haD6Z"m@Z SeqMX#zT {xc۔1+'uWI}ּ?n(,洊(F'qs?&og}wNMRZ :GfK9[g9{yFPqT&}I;֦Iy>~?E|_S~Ľ"_+*]Cl)oƒs?1I~Ogн詺Ss~d ^#h6*ХgP(AVA֍Z-ND'֣_U_B/[ҺV5RaQ{1hr$g>cm7\,*.Qz}p"Q=4ߴ?% =Wuԑ YvրbϘۉ Trz~G WZY4 Z"hsݶ OsbYcM5ıu[].00r1$cQ%y΃=%]Mt]t3w o\-qZ@uci_h/Ii%!-uF˨,\[M+9RTPsi6F p\i?>-\q6ɏF~Oy-/3S2_|S(?zVh7%!C-ܞXV>L.*)2~u_$ί gvqK+Xi(y>-yvdr{֑^mRo_]6wj$;cTmpmJ*}5i/_nnmmb8ذy?*l{gchU˓g!n! 9OG ?5Z` FjLFZ۞C4f:x<^c) }>cwxtda#?WZPr]9J\% ҳwL6kGbHңrtq˩緟U7vV5w2lF? mޫҫxƱ=whG_w>ncDv$)?ӥ)h7\l,dz\^uI\N22ua .q{G:%z_|n[ˋZ}zEh:PPb "+ }sJTtr™8I|_DGHӺƯ (n$%s1pmNUZN;ii}"Euj˜o"fgm㕑#6=҆wJŦG>ak[]:a1A$n=ce^U2ոi6]3@/ٍI;J~udN{/ _7ďqZimd$d`q13KܹgqӾd4˴ڄorgfZw5]O,5 -Zǁ6xO΁_$¶2pkݢ _#]FmV戮IYv_TRT"|jпjn\.V1\LhI*9^{S3@d8앸dʁ3/!/:F'cL[>\h ˦h~U 6dDFP BS Fgw 6Gmۊ-v*v*DSo4[ Q4 U U8V@V$b,h( 3ǑGzfh* QFCGå۫K dKeC'jalbbiUA W'}*2*E?*_?*p |pa_ 1ǁCt.*^eM&/X$M-=H:}ľEF8XyL3fv*QĪȽZU"&v 0* jLU^)Eh6VUlSA?Z!6Qlv\@+Se8Mh6VSGmA*+G2beq%F_UJ%*q R@SdqZ+BdT4p܏0g~M{aVE|+c?:3t.N"m -= q $+rqgN/- EgVhLQ4~Ɣ}dnUBV2J?")L?LjZxd$X'g!.mZxDkD}Ą2Ĉ (mp85H=A{R"`wP 5 : @h(겙ý@C YA z,:jй1ўcZFvU6D?*CWqb. VA G &\D,MS!^b-/h5 -K^QG&Z$%Zrs5 zSxr-F1ڣgćaqRԊ 2 TA429!RAU Rd*6Vl $Yl vR#HLR$W{\T-`FqZUaV @⬭ZVnj"hZ$(H6*l+TSAHBl aR\iM`Tn(n\J|4Ď4!]5R[雇>ƌ޵ɬfѾhq][9ű4ͺ*.2/8{QFxFLL Pep 5 q /J4q"nc̏H E,*"DAVG\ C"읲3A$h"lQX'T@Ye D`lꀝDΨXCVboVQL@g" {f?jhUE@a"FRh4N;hIwT-!1 ,lC9S-pim# )mQAȾ x5i+dho"b bEhvl&e0"i)/&2A4Wu. QoVcuBHz`\iID,^=kZJF'\LR5>.!x\=D=ja5 sS5.Q+WMk$DzƓF l3XFι4ehi$u)DBDecvp"W5GML"v慰 5 Z!HTd:Sֈ(8VP5( >)ɨ*dyўR rH✑ b^E(̋o4rY Jyܫm%n_J'[cJ(S4䌓C5P&eE4mjV بC⨂/jV-tOE 56FŰ( 6({ B@j5` Ƭ$!!PU<8^yJԅb;brЄPQ!tA *(D1 橰@C⁍CZ 1Qd E hDʯb W[+GT8[&WYe/M\b-Pcnۀ1-dE³0](T9vqerMb<:+I.e\cNoDK"]R2A z?Rz 蹖2aq 7)8T(]=ٳœ eںQ2cq8l1[oM0[K,ΈZ8v#sS`K (,>T䆸m$Dt^;;^<0~@zAЄT&֯`T?^jl[C-[3wᏳ181Щ 1Sےi0,NGh(p >0;@q;$R v]5e![l$Qr,qp1S4Zt h!cµ+%AwE鶆K["`k672w?Sm \âٴpr\NCwt)1[g=$VVvXPn-*[Wp JccxP(]T塏afmdR 0N??z[bkzc8=h$8J3>pF*!ʌX2( <QHIvf\S 6"F7ROr}(."J _)9Wqm JŴ& rMrjEh0I%A ;PpHz-HTc,OrHΕdػ_M^ZǡUx> u/9Jܵd$~ړK!mF}MzNWz3]vƹxf3pBxyH0⦑R{T3*vKhɕeu&#ZiQr}5 ٕ*uj ww r{Gb(o bm$vX&֏y סVL0 U#[B()=6FV) @BxlT, jSZ,clmwܱ ?&wȖ-@ei^l:sQ׆oۨ2/q@k~W-+eIӕD:tr2&?RkfeY}ILEi>(ȥ,/i0A*J翘@gOduҟ^MY>^;f#*n2,e|2yK5cΙ/cXjڂw1$2F~.kt鳂];%h>ei $o?Z751l5Ο!F=\iryB8nO~ޟ*Ws}ٓQ_&eu]. *I)- 졿x7֠2a(v{UI ln@ I4?Z)-%m ljB&`FdMC'9ǭM xdG]K[ڡ69SL\ ny±۟n1.PpWLr@\Q RBd g9(vO-I>hbuq@GqGTz4_+qX<۵5$qk'nqb1(`r3Y5 WksVC(ԳDhk|G\qiaL,TMP:C1P Ve[Qˀh,|lSbyz gHo;W d~54 (xhī39Ǩ1`h}gi3X@'w'}Zbvډh4nY|LF}9y3Jz6O︉Ke {e?#Y/@W99>pVX0 B 67bW-NT30}Ob\=vxPDDE(8᱓be#IlnSlPGp}M]GM4y>3l(`qN3ޝ\|i0俆D݃cZq~&EhWc '*@j.QVRˌʎ^J]wkY?3NX0-ww zRQ|9B{8 %:s5[/BNvp>oR*V¦,)n2жe|g晰BA3. n۷j'D0wr*ǖ3A&5 6*ayf99sPbF]9OT*""{T CxNU6IilwUR!5eX_H˨pEɠo9px~!echʕ%ǧFG7$w -"?@w jS>䦕}%l̥Iu݀}-lm.X#ԷznГnOa4"F qm :1?SSb&itT$E+*;Ț܋?½SVE-Ԥ`2̿J6ĝ")'+ϥʄs}^dx}ɟIB|SVs΄L](6Ɓr6GM<ZhY:G #}_F !Zϒ>?qwSrE蠝'XW?S{3|=kYI G~k=EK[: {P hNT5 6q,99'LQǻKqo ēcAl Z7Moy0Lh>6x/u-4i ?{I^YE c8\9}24i.q7LӤlr~3Z)m\4?_STr~6DaL?pKLxRBNș^]:} U~)|cG鋘4=nxRK:RHNmRٓ|;tyYAŴ" `,B 2;^rd-|XX2c[XuCj}BR@^ꗣ}yr_)54Rby'Ӛa>vThj2jB_h1A h.POV8qֆ<G8QT wPrѪ x4"ǔΦɡQP9`V;veTAvޥa@pt; 3+gx@`Z$@$m+<sEbi`SbBG`y ZneRvho*/8e_D@eb1ȥl:E0#Ъ)NTOgB]w1dD;TjL A΍h{8k:gYQOR'ZYuE[O,CQ(0T.oלP}K fJfIrBdijvW5ӢݣՐI2t0Y^\V #i&srLe /mWp՛eCgq;X7'4TЛ#dֶ4sf b=7RL~mlj2ΥΪ,:Q} 5;׊X``ZcgmT-vVSZPBd c&inVE dyiS"X;v;h9K&EርPp {dǜUh;{ٰ|Ӥ닙LjUŇszsY,R|yMhp ~qPrȭU-PwASFHZlvVe$mWPxH40-mE+\:0қ2+F$̏}=nŴkC42D Q|jScjC}ʶҫ#z*=!yQNsQ=]P6{5z`8v A-#= $ՃX[ 5p`Dz0Z;t*l~/kbG PEWǖvf>˥{Ӈxn0+`]e7\893Q&ѧI0R0G 55Gx_kd-#rv$-'MO8Ը<8k)HIi[$cJy9xdmv03Y&tHP?AɆj"lwb8Pq 3lcs60|"8ϯ>u{b*]d-]c}KԾstu`@ISM/Z;xQ;P* O|dziXh\:lg`kKNBz.%=9L}i7Ϙcv5g+ydJVew;5V)x:x8ô ?FTc2ѦoOށ)>#1¨+W7L|2x|"6[ڔ~ Q2 g3f4,JG+el^,oV䳝jl()"c4- X#Ȥ}5`ڠ_U!chb_r9EܩVDI3⬾,p$`0q5V2@nOlhbFc-M9et2{ GxMI܃JM_%uK$EdHlmJ9gcuK|2aնڛZ#>_5era}m==X#-߂[zWFl:Z!RZc3[$I 8C֨,:gCE1bKm v}H*p+J^"68ʮ6%?. SƝ5p4 Kl:gT![+GT4u8@6pM ;8jhvyw(D,:(#(-N <7%$V2Fos#E_afG`G5GB^-0NU݌3~b١s XJU6bs?_9G#+SsG.މ!0P~튎{yO(K7'&#y"J}D@T?|rkj:ad/OlӒ 댓\ؤgƞ,f&A=O昘/GHۏmT;s '8r| pvSr1TԼBS N*ȥu$QivD 1<4R٫.u9 K)awHs%aVKeƬrW'#?ƧWH =@a [G61M5{) ;Йq*9$y+Z-X d`͓hhOkwK f9+RЫPl ЉPKb3vܪЍ́!ly[Ң*KCr.3GؓRe-cS84-CwqێSz-=lD OHZ7x>a1.pFIBҡrbwv$pVcCbqP4=,=ڠiltL6!`I€5Llav=Bۤw6J")31ƭ̉/&/M̛FШ hKJ,=ǰKIrWO8p<÷RZ hEx66Ѳ%D2[;voҖ-f9MOIDKlij,}*ACp9 hMmF3sPSCYc>=лP q4šqCYcOUR_zEOIXaKnc/&g̱ݗ%%419gb;4wuV2oV7e(s z3%R@9bl`֬NbOkG,OL׵rBe^V*ze(bzfaz{Ym\a@˘Z$F {3F ڣMBQm9lʟhW{Cb ROp=yO'VZ+{^Xwmc;$M A>!|!L7*jvXHrsx֡P Llڔ#Cc!w)95V`T EBFpSrfԄfEg|#Q,qN?Mhes f5PP Rq6hjʈXVܟ#RxVn[0Y%jyA-ԣk+2C q=N7O%ct5Gi `pr-B fP1K l9"4.l޺%m¶Hƥ( l` Wzw0r >)ACN߯\7OμP{ժR8o&N̫b[ar+]V]حQ"UPێZU:}"T,^ߝkȶ2l`ziD=/+9QIHl^=Uh?!zrb["$FF*ÑKf` U:C*Uv9csPc,).wDSOenj b+m G*Fg1#9w)[v JϦ=~&t7Z=ms B9\@~웑4Zut~ mm[@!Gޙeu,.ū Ya&=x0ȐG2[=299pGr{|.سV g"lϠBbD1 m9h8 1/ jq 珦j'NotoB|$fbmT *wN>J$Fi+".<#rZ FTEUP@\l:bVOL { 6+G7=Ղ{{ TBXEr>-Gsޗ0cH8 Ira6*w4C2*e. H4đ4LE 4*)/xl]粹or(8$c-!עuȵX[4# 霰;X1ڵIq N7folaHƐ )#)?dA洙c/!ca[\F c[ 3$ynl7sC yExé'ߕG%$ qy#z)b*A*Op; hrP1n3],sFT)qj.>M$EP@Y0<IrѢ֎ BI }έObPؗ (ɤF6_zM;Фٱc$LCh \q~&͐Zc|$P6qeedsKl)il4@𢜌Kbm$C:5OcP 5dH˂1ЙϊޅllzAI5>!t;)NBhgq)3%~4, (|ѿKt""@{2E?p֥6y:K#5-gG/nY2 hYn%ۚV߬E;^ӥ=*q՗dgĖfyvAjg>YZj_LdMoU |.UYbuNlY}Rs?f1jq'[ 9}5HD 8`H-\1 `7i@$ʒpr3}j)A[優w)@vzˠ Z @m_"9MAP2{@HE1- 8Dc9m#Ȧm/ 8BmhRś$VT֠к柡6b0I*pbsIԗm_YiBJb4Ƈc0>">H:f|Eqa6H3Q_U7r#KAhr!X#l'n@෶{fq+A> ASMpplmo8+N犨H<`pFF=hb.䆙 X#E,9)3U-W%d_˾o*. >J'Ȣ9?evWSRxR1Tw8&<24rޘRhLL$#8{1(GOZ3.'daG|%F{ofPN/>d?:faJ {~!~&womul.Yu G5zgMo\,–'[cE; z/j~hg9fN3ޝâF؃iꀶ3[F::{8l,óv#{eMVɽ4\Z\#lx`drly2]!/9_vϻg9$ xP?8L䗑X_hYp0sG|=C Jc|FA >9x\q3R bLyavؑj#Oi $O \J6i,6&2xoJ[$oag_WAOgMhQ.4}L6ܷC$mp4ʒO#uu\>Z%+ی şܱȳ0*b S$RN@HYVٴv5Wm9R_@jmb*~J2G VI U>¸oB{Ҫ{9؞MH3Ojp79th>]ߺw_zIa8IKga[m!ew.̎ :4{H9ȭyd;15*HY]3Ӽ7} źVtۂQ 3f FHfN9ަu(Ž PtTjLs.[#@ mPpxBhaGT "Sa|sLBP*r֠9B2]Wmu2FJrG|9Ŵ:%9(yAἧUOS֍?ᾋsv/ /&Me ' =j\O!/Pg5;!ED</I73Pݓ3D K")s߂ws[2=؝LlIcVLe2S[W&v!Wh>0Xd dP-Q## B1~qX9H_ҢEɹ ⱴEm@&7lx8$&#P:8) rjǶ_ZVa8$w"I#9!l!b0T\wېGÆV5@1YHs@ʐrw s򥱕W+^D}ep'ZR^6I2GhJu1!zl#'7ȹ)'!GOJ,HԢ`I!x!\U1œJ-RdEw`dSzѪ}ރͻ4 uCXc)Ʉ;>}EQ*K}3෧Χ#R2$Mb0 fޓv 7sWŰb[E4a&I=!6 ~F28{QMoIc* CGqI-cF-w9VnmD lER;:~ |wϽ)tek)oD!3)\f %gPH콷cƷ !1or;@ÒU*bMZxϝHW.x .K%spy#F1B*\wCh}+P_Rj ##9,%Ϙ*>>tsuWG^LgDzs^cFP|U!A8?.z -+'`Der!/[-{f}?K7s 8!@=#Sgj-m! {FOl~4]9-/VPS2؏cF oqWȣnypq>gm 4`/Jzr+Ҧ*yj0@`n1EW;D llg,@oWV\[Qp`NM6 r<¡nZ 9PŎv)Np/aqGD< ަ RG-Cq锨E[H9>3wf "0\gV`qeg ,~mi \)RAe$Qv);rTs{*ppB-F Bc wcqa`|nem{vh[V[Ҳ? e)li!n@$`8(p X«t Gش7eq2;Š ! wV&n) UD-)F#f}_fBgеmj,`̠pGbB >m_@>4zV1h0k1k~FDö[R޵H(p|ҟ J]>o,M Ij6ԊwFzQ7! r8\7Gf|rL$Q'6OMs>vOhO}n6Kr70xc.og704kIx#h}UWܚ<餴ioRד@ @l҉ XDtaU.{ބRhR{oDoGQ{;i{}*ġHy%10eGaRk~PuB= ZG r2OVl}Eh1P[Z$^Nm#yU}%I`3:jYFE[|2I4`%$~  }BЗZݣv s qaBZʛSɩ\Z. UA$7H@yL0e 6R4 <|zؙ=RZ"C`z 6B(GDiJ VhM|\dU.I I"P1Ͻ KȢEvf;Xn#}(F]R:{Tt~fn#ۏ$rݣ: u)0=Q>v v\:\2j,սyvL`6 x88Ejs qcyex(`38LTZ7HlC򥺙)D[ymz[!\L$!~"#*GUۡR|83Fp>J̤"~vR,{HHm d`iv09Ir}ahtؼH~P7,$aF;xpqlNHFލ0}aɱ9?_=22"`Zrrq11li*R_ʉk4j*s4A[8w2HcZ}7(T6P5˫:{wib 3(m?]#>ui\jcn:a*F=q]+`7Z];߾ʷ=yuJpm%NIG|{qX퓟cN>7ڮ.'MY6xϔ}K_RæxQvCKG;]o+z'Iͩtޭnm Pgq `:Y#,{U3fZE=$*OH<~u3([{S˲<$y>4ơܾP(DA[$ӓTI.6Yw&)85!x̉ 2 1EwWϜ9>ӚmZjR+}79tN{d-=Q1d7ZΛ$Sa=W&]~ l(D5pC2' :J ;G9SK#T*m#$bBh94IZ=qWAWgc zg]_{4NzG|rSO=wֱsb0-`o@V|<2ECĢk+ZÏbpߍjg3'PѶIVR~VFG(rTpH.̜>r58Hm˞֊Д+ nWCYrOӊɡǘLL r~TP{LRNуMlTtnH()QbL01ɦ$9$&ҢG{tj$Fo9=v79t7~70<x u/SrQ{i)n3s*O"VTNK{1mrf5HbX7Oj^GKMjBe zX۽[)׭'=jSr[UMxm"jlT~8cMȤ6PÈm?c"9*z1^G~mdm OAzY[mQ㏡mG9#֙)#RED!FJZDr-iPAZ ?*43$y rKtb1 50%/w})U.>/Lk,jһ[d~KP&TcN[?K%icԵV[̠IMNz\X~Bߥ:Jۦ,HmFG6F~,ER|{7?\:d'B|-T$PٙŃ3|ku_3o):)#qw9&B%V)P?'{pOP$#{*ڙ̾_iOrg ~?rXaF{w]N1i?AE$&R[Uu>y櫖i&rr+D}ݳW7*,HʮTՂrۏ QE5lq ȶ1[dC41>J-LYTa,T]K:ɏ6,;Hɪ٢2@U;UҼ~0uĕpG_&{%> R갓Z$s$1im Ն d+ahT1c}˗OXk:O/PΨಃPKTޅ1RV5OzFKAc.g k9'3sHS(.muJaʣ, k2l[ogMSsL|90\dv<]F( f7_ኒfx5|qx~Š):U2y`<+z ޤsHM{@=?Ji{'h@e=G6߆^]A`3Gq4kr[L " _$XMT]J~5==5' __oɟȘ5E/AXTօ[aqWM8ȡIT|4ؠ Lۚ.8=0毰6o[Aų4sH1 m_KP&xOʤc%)[8^˧ P)/H3Za9P=9x3dlH!9?®vꛏrѥh +R̚mօd_fs +o]~ʞ_Xhy>쬒_ g"D8ۼsϦj백{Wv,.#Pr)4ؽyT!BүYhdxo\?{vFؙI;% r S 2H`zF&k@K\w#sVޖnʑjnI&86(6,JyՋlS}UDQ۰=?΋/ő!KP&I#h<.ac{ވ ?xϗ+)=!j_m o.W!nk0S9TeV 6M cPYV~MaJ|(>/7Zu>{E%(.+=߁R*1Oʅ29 qjWkd)>GT}N ҨzZ K$wCungO*RfSjw}f_kyhŢ ΁`OT$v QC`0FME-lBQ@3 `w+b2=2~U`ؚɀrԒLVG p;QB$Ԇ-2%6#&-t$q¤l8Ǘ5N ^b3[.tR$Wޣ˅=g>.(8CyV w;FIf$WU;4P',c$N=)ltő䓌`*6R1(p-,f9C'ˎ}lF2P2,d˨Ǧ~,## [hP2K{}kV?jodZ_E37SȦxoo=XzzRY>1Ytλsbey`G<~|v]ӱ\^y1Xd!lBc]}('ЍYܭvæ/:46:_GNǾ9_(C'67}y3-./DʂpOYg,eaOrxcFKucҠ򦘁 A1\٭+mf Hȏ޲Uc/ډzbR/]kk,{61O?*ֶ<1ozi09#Z3aEG}SȒSd "kem:lN)ҩY6=FAP",G{qIj![T8PAA4Q"Jwk7 ?E5mnMyn#3 \C{OeG?-5~1w >g98b3ڠv$+.1iRo @A*OGwj#Sΰc{R&muwl"ȯga*@ZmŁd.;f$-oJem_|&!KXn'E^c&d <}02)+#MeֲF?\w*< I 8Iy ΠG5vbYdic[\"QpN;sJ"/&..}# Νc Rh+cd-Ys3!Dum+Tx/Z0 Ȍh]Ŵ1~><+d.,kEea>SvwCbCBz!lnlmljߚ f`ɦ@j[%PkLjˌ!˨BHe#iSɄK@a@rwg1e%iV'5_Xy50Ɗ͝FOjɒW 5{+bRrA:d$fK$O$jsL.md@?E6-&vpf#>.K7SFeq #mq]>{D.LLM"L>I rjla?zoA"vroSC)VFˢ&K OacV9H^U 7}qc՜[!wn?DQr~1ieƈTXʜō#E=t7|k*to]L'"a-m6RG,i?h柄YYc-z9P 8 6Lr}k<g`+3jR@P³0d-#UN Fng\?gAk/Ik`gM*V,2]6DqA xy ܆T;wkkqbEE`ș od`WpڊVFa]Ksk=++"G 8#)oR&+Sg d%$$hf2p`fJ-y~ Ѭ[*(Eّ ڪTnO]%-"Viֻu1Ϳa,9B <Jt3vT_]0g w$8*tjI&:et;{innd#Ǹ8tdºR^ Ҝ7=i 6M6ݻ#1B0^٦!lU1Fx;i,"xq l*{{Q(lvti$KhKos)zKLթ>hI6z@̮[c'JHuL0}͋eT|HRmmwEǑ}jylků~YX`V & xe9@u0>6J(88HXH_FqLH{S\KH\gF㟠ObkyDԵhZm7&x}=!GnBؤr܏QJ$Lun)}5ùFuRG:3ً;A+5(viˢ@s ad#I2x_fyc=w5 -}A n`}iZ7"BmVΓ q,Yd.kZ}63x>8SJR: Dw`R'Ӫ& W'C:$QQ[w·H\M[jFhV&(BrG;I-;œ(ھg{Kig==Bc!0)W/8`"_MBL[SmY(CxإFr! "VPGlVm|?*%;P1C$6uM [ 1*켶Ѵn*7lPSĉ$ɴ:,QCڶ6!q>6ix=5™ϦBBʫKxqn7ʗ/~~_4bk2bIەE ? (cZ`)"nSPZoۉqW4?*t;)x)ُ\Z?›X;!{H V_MI0Ob1&όvU_F4ƹ=O,OshjԼCZk?OiOO)C̅X(:~hծ)܍+ԭ!iWQZeLWs*7mϥs8ƙ uk3<22v2l}zRs:SQ;iZ;KKlR8&X,BFVG;#F\\[C4KwS=I1mn7=Qm7W1{p07Ti?q?C9FZ2Xe"vW,=cq*WQ`GA?&.~PFdU>ۗ"Œj1d VQkyj휀܃WB^fFy3!IX&_A/ PA)2GnT2qm V;t"R0\Xbo24ʻ˛ *?*HcOf[`5\+tBI]ʺpr9Wq{|mi?7%k(QP^Fy7YVuv!H܊=ưcya8xlѺgAj[$eI=HnKQyԺ;QRēD<˃&7v};j`k|(0">^ꬋR|YRo ,ŌRc'\ĎG+[8A'qWT_d]}tO+2L_f{*` >~`Q,u9eH9'׃ޤs^-š.&Z^dƨb=h/ofJ9"#GuրsιveNգz}2gz:U;iS k}WD'Kyiy+: w~+\Z[hgh1)+ޖIųՍśq$>ׁ1l'Ϯ/=#>j:K,3Ovxd ~݊h)ԡ/&[!IpF/!gm;d^sws4An0J{=n9z+{I--RL0Ĝ`{U/wsird?f-sOl]"6,"-@X݃IE_P+9m 5ۗ*9~U;0+@tՌ-(#*x%Wc/8R`v`\V_1ݯFt햟&cCgdx_+0#򯛃{Q.y9nM/zkN7vzRE3gd@*_-P1!N"ӢH0[aYss7g'I:WC+X$D.[=*ԥnPq+mLSQsHl+*w==hةVԸEߍ {sjNԵ .VRܖc!_.ьڙ\%/?pVA7te{e$**v ~8SyG7'/CG)ڝ՝՚ڛl&X?¤w4^>[VG>Pt!klO6&(6098kA(6y#)76/tѷ3Ioʹ[7.rXݙP Ε;%88gSe:Po6r@`9z(1n:{3 20F{g4XZ0RXSH]=E2$iʑe7R_ki:2 cwphw ӛQ[;8I4F@$g#'P\#Y֚ cvrⷁRtxn*翔'#̯R48o:K #/슁@dT>ɿt::5gt'㽇[OG[$=pJ*PȆ3\KbXvqRi"4vSc9AUQErXխettM?pJe[DrI6K\~qRR,rKm#r@i_q-*VVL"6PU¨L9 ݑ54 G:P" 8PBKXggЩ{1;v)9$sB 6HnvLuŅ8ڤ6nX $<,o0jm#ydd6] lӞh{Y86ug=ؐ!NOpOh[^DK,^x8A iHF=6cI8QDx׽EHVm~HM e*pOx[lޅyqjʪ<9*D5dd/Qc~"[NR"m>G,+e"-ۓϭd{%v6:NF}ɣ}i1h`-$|Mcl" p@[ː-2)lR ;FYr7r5hDP 5,Gnr 2Z SǾ%N>2-я|Q"\V锎Q>^OU?r8JDZ??zj;}]Q/=/%}6 HL7QǙ~Un]xe/Vn~_"ٻf\jqL\eJxO,:WMjjM(W˷~?Gf׃o[ZOj^rF%:R>=K=Jw+c"2P>٦?<?Etr9v7,[!&Mwަv iṗ?ڮF>EsKt=ճ:ލs\nd"r (<߀/Hҥ%'繞mve~](m3A<^'/ΤuíXz~Yy䪙]`@l(iS.`r!F@ ȬF× S6 yHx:D<6r)M2*`8%NyhΩo"5=q]KKBD˃OL]`X;@p'Ϊȣ""m`o8Z[$aP=ԋ'MU.MnTݰ*iOt)Ɏ뮧`ݧH2܏P4&ߍi0kxVxF5R7}7ם&kEc! 9x;VJs{(AFǩt#@w=g@1-7OMH`dʫ$lZcn:VC{ ŏN EPGEJ{-deGP,$?ev>ʮ0@ڴ|ihb,X琿$"=%ucew OWZ_-zLBcCc&qآazW_݅޹yCޑ_뻝vZHgWkxu/$d{Y 9TGQ]OODSGy;Qxm60;SZUInaL]c N{8✢NBxqE11w+v1u>SdkBI-wp=+*r@ l[RۚYΖ;rꭏEH 8qR4*[S.2B!P9-8Ty3q0L^Y"6VipMN $a56.qIȷt0mZ߬pN*A͕.66y ,EcHpKQ> Ѩ"V'fqǽfuϘ% Լit6`nbQ]Qgn }ڟu2p{a9yx<{nb%n CKq hQI=_SW1ww^>~՝SkciG@,̛%GԑڵW$yi:x8~~#¦8#Z"s2NSOud"eoɡtGLXtƜV/%P Oγ]t_+o͵2Z+b)χZD+ed".,Sn䵸e # <(0l99a5ӵ(S{rn zxe UO^fI?<~>LJPnE2OBEN+އ wYf(_A'kJ2lzDN)aԌpJl v2ol$ԗTzj[suZV~ImOs.)rm2R%沚[QpX 9ll}.ŏ&F}-fx; ;}TY#Dmi&g.3n999Y)ٓ:}9dI3dɻ<1>Ϩ5qlǓU-G(_vꚃX=2mT>o+ljGnHl6viq[CʠqRsNr{v3ˮ;#+c5ۈHu}lM]{eFp}hu i[{Ev(mǔ!C=2!Y +gUPa֓[%奎ULeQT "Q-QW1ͷ߹@ratp 3G''vش7{cyc)Q1DfSw1-4v4H*{f7!2c3'@uq?*HtŴZ̲J ["V[$8Ci_GheY7r1>\"iWtrL8zVU=X~hC@yn<{PI - ;?/tY$G!MG$` .1{|Q$MT=gQ.e۲˳GRuսD NhR5GA]9qy,*M?}FGk %̇)x|J1KAivvy*mݿg9xvgYsx&ӺE{&Ϩ2LiQǰyĊWO١^~ev Ӡƫ&vI2g?Yre/tڲt ,bǰѧ4i5_/RwL&Kl+`dg*Z^=FS?e®h8kv VMFIg)ؾFZM/Ğx-ۖ˒)<ժeokw$ro5 r ,IUmzTRUMᤖ1,;8ʟS=4>kͅ9OHBwFX'[e+Hu=>{'-k$^Q6'ғ_ yLdb)Rkye Go5cdG$wc99ʘ/qLۭԦW 0gNUEXY"dV<$ ^DxsrBLPMpW!0 Hovpl29EdYwx%{Qd /v7㓚41ſ/H $_@Ui.gc'##vh)ɰ>rȪ㱍Ͱl> lUq"4kZF1D !nzRg63퓏F>ՋZ孪K`YrQN4A/ỤqWJOd%w~\S֟:j~vFݛx؉ G_F;( ZYM9مW˽K>m[Oc5+÷,NB3?5x$6dZ.:KRֈ[ 3d^VO²<mBK:JdOV#[_ 3\|i?wSN^q}%,]Ox;CYHQzdUcIωpoVY%gjeRzVo .*.R{;XOE3GQ5I37#sNSSr9Gѽ#rrF}8qO'>{qQI/%B֭t7z&pzJC pXl` &em$ue,Z|n9)f{r%ܓ7,Kq-өf ~ +|v|[ݣN rN{UV=5 DevN@翿WpF_FRdVHT:pĊۊc/^M4e幦;c/-%SNp lqKK3=El˦<X'crOKˋzGc]zZF9}=IZ6L7>-׊F&l{GqrCim"JĠj6==;K"SX!dw $qzfDS-#]khT |hGD^9o7[V4Ⲽ}ciYyvF|ݿZ|#ƗcV O 'GnGMXY`$*6P}sZb,!㖒"Gj]4tpVrOQ0BE8y@$}3E5gнǯ>^-BUX7O$܃)RFX#%S3敾^ Ӕ^訥DԵ\y0C]ϐ8F-rA.+]ă*wHNҊ3$)/Q;*|Hqcqdq6}]K,ɸu#W"12Fɳ@qU;NFx\'5m&D6 w+֯ƻ}K sT}V[e\cM: mNʡU\n{fN>YY/z㞍wkq܀Y} ~PGbg.kYmcI=ԬVQAйPy9Z~t?̪@jOm&V0T)Ql] ytͦ 12vL3H3&qj-dZxZi4ǃ(bYO{ԓq˶rqmduӵ&] s*lHpq|rrlɡHуJN9P)ml~S0qYmfoǔ)>՝1Ic)P RI QRTަV9T Rc6q~u6H!Q.0)qj@1B M[}68$YM(BpHOʏ!Xн`hRbB69'2OrӢM]CT->U|g>9 k8*v>ž=|Dxm&Vk˞/$Ve>ޙCp:pʆCΣ]FQ#%'Gʺj[O2k5 ^k#pz;+ӂlz>W:W&'ڮ?M|bxٓ))uQuE@x˕p:/ug-N_e`?#>@n}3&)< /Y_\9l1ǧ5h!8˜VW1d{zGIy淂gSyE#~lZM>&nD,@zŌW’iMRC"2cRP=_QPG\!*g~O g+?7qNs#m$gn3ZP19+LjHOd\,l~~|}P3u|xߌ%m!2DRd{7mQQgFp/p8Ō72^ѦP0PܢWFe]#.nzǧlqx\~U2+#5> 4EJZ1f ؟_1凮t3j}<<I8qUVf} +kȠ+_qpOT տ^NՕ$O\P8#t'c>^^#T$O[DrvpA"Cr6`T fyV#.MXU+li wVBNlw"d?JӍW6r:Q|:2TN'ciP;ֻpZvYRR9AŚFaqigo(weQ8J!ypJ1G«) Fq6n5>bJp''P2]Nr[?9O\gMЙ6rN#46!:添5i4B&AF?ZW/x`d[uC_lpl,ÚfaOw5QRl׶C ݼc e~vj*:vڕ㾼$V4t F;WJO#ꮛqމ6F;d,ͳĉ='=|f*^4+~uk+ZYb/8AaEP8?f̩Q6@4`ɖF'OHҤdY lYGewPwD1􍋌bn};zm pa]1U9do(B;2%&&KqJ*>R cC&W6 26#إf}wXcPs)m_1'#'Ba-VgUiG~~ ZG '^;{+{S꫹?ѩWʙ7e?NxUGGƬrɡj"˷cQS[Ɛ@ wDmR.6QzQ~Z%OJ#TcԷ#1iFxPQ)c6mCTgPpqS҈8-ETwRql9l4HwLI)@|q1_u CP[o$m|:RW3/)>=Dr˝BDv< g>lzj)OM*ꟻZihg SAЈw:[Q0r~mJv1b 61l>r(Z==Bwp񃷹nOR_ D( ܞG|~Rl=Liy5żVW!01SߑKs4ʕq-'þr9=Oixh2csQUCtr =Sl/8RF u1Mңr| ՕkԈi( C?*d,)"˜?iI ) .r]X{RBjqCJ:#r|IÏ\)J)qJR]b? @n&3R=} PR<;nϘ}@:qvls☀kAdm'ԏD(@Kd)A?Jb{Ihgð9!k7[7".%K#Jl|񚨽y = Ȇ73.Tհb;@ܯGcj[:GHWw4ŤJD'1Uw؟iQeKʅCW+ޣHQX08?Z^ݘ+>֫Oc9$ g{cҭ^v( /t-}XqRvG-_234m!r)rUO5)qъ?T<*(+~A(ޤULW|,|\tmImGõVlcy~UŔy|ޣe/f7ANjyP8E=OW)G<.NDm[#T_Dm@"ӢjӔi~_gq#}A>_k6Bvݸ;c'/GEqTj:g_1ٓ^G8ظL;":]iBdi Ƣ?lUP~Ͷ?`FaqY"80}3?)sZ-֡8Gi:h1Gx|ޣRQ"~ GRMt/Gęֵť3k^+Y<[ܥẋi#_(=Ԝz/r[=O}>6Fw#MSϟuO %%!;,aGF|ct{/(e2>;o8>͊~'j̝ւ*ld\~]v] $H.T}Ӹ՞Oc=K#53xF]ۀ9Wdm)弇wc`%Ige\*q8EبT(3LB_ RT?Z_"{y1 +R69.xqLNJ|zmtE"lZ6`@3 9K${὏Ϋ*3W5Nn]23ʊE/ʅ+r"tovmRK>G7Cu}ԛc)&4w-M}#6%vEwo._Xr8Sc8x O/G0z#-ggK~fuY:~,NAlcGYѮI/:|YFmuUoqt׏(lqub1qԚ'Ͷ2uhK.Ծ4hZV8-@;F7n?Q\$I\N[_quŽgZwe6nj}XcInU8}棬^_6뉤9}:wHw3-j &n g_EȇGCAJ*̞h*rB7Z.y,i3p71'8 ح;_S%6vzW֖OUϾ8P0;obؼfפ;g _?$ڀVq)屸Ϛ2}>_^?!1ʿP^ 3jI* 7sv-`9[MRtEKQH=ӓMqk"JN3[eGR(:DQ<|ҽ94tRZ i3YƂY#W! b Y Uwꗽ[;$"6Y.] H1υhA+)-$﵌p_ ? K?w) ?ϭVE\uT*S/^\\,:Q[(KUSZ̩qlkW9qt`\xJUU|^o/AbOجzeU'-n'~|e5Oe=XoRS8. rS[G>-lJSRh~K#Lx1,?,?S[%u|XO4kw˥IJL08Z*]\SՊ+%G!r;{և5]OZ$w1xҫlUK԰ Al g۽IM:HQyxԌ+рxrNUAq櫓4T+/`m,g_/sS{c;ܲ2 ?J'Au\%d7p?*6uC5ދԬ[='Ƒ2 {²,xgVVCJqZ-mA)JЩF\~tJZHG=M o<]ϲbk}34=FdlF$H=%g^=Qk3]>V 8 x9B6(0볼5R)n x<f|0` W?\L,=˩tLЮbQi-gf`wT$ƟӝSq}KXp^Y¢#/"wE50yEn׷.՞ sے Quf%xE%(m8 ? `&)E&lyGLށ[CV:\ê=4 Y*08PxϥoŅUG[CK{ -фvvїwW Q{z1CلP]JxPKyŸqWšV!zyC?ܺPZNG٠uC?A|߬211h6 ڰģ>iN>L]6/+%^6˕! bUfλLi~>Q> +o*?T>[}>[8Y=Bߥbmcn\uܗxiOo2=C迠k+G|qǟWCI(F|CӔu&AKxOO҅I\?qql)Q26cK<&RCt+R[́FGδ.g㓉l$@uu7 uO]0qF3=KxO6b[:zRHlJH'εYeq^]պvQ[qs$9%#&ʎ<6g%̺tVR%ـ1۶3Ȧ j,8;Km1\wOf޸hpc>O%|bw_Frjz)zDَьͽ8-9Kg&BlV1$y1^qs,?|<ս8¸l`d҆39U,x zkj#vBp5|Xj&'umbm Uw"EhUi#?#SLbB>>JM[ "O}8jp&̬60s}hV˒A9?MbٞM++g%*eɹ]k}FfqGaQjO;1UzzVD[#mf(}::(fbs3k =?/OUr(/} . U|B@FCjb+q $9z kb> nxOFBm̄O|g* m,D[9Ms[P;4ػ*LunG`U?!38E[8Lr#"*Gg# q$( c,TS)f-K:oX,xi71grbU}M2|(1 Uن˸Nw>|Sr3J!oc<$QBEB^2H#}*;'lD3Qye @!\8bEK_.,5K!dm@24G]gmBYc]UW8V{`op9.f!gq SRt6Q$ `3۵)u8B\tKWv"4{J/4h#NAW/uG'9n-O=nY: >9^ cܱ#sUAf'ŎϪblkxn=>P?76w[܄%y9Wŋ|שsy-!d |$8zsޟZ\1A%Gۜ8\ -O+V! ݉mQZaCJɟW6r> KqvX?~YePH=kSXxil&9|;V$B6kג0Y<@dg81iGY*Eiu OSy$n-IknṖh!!x"+y0LWP|̩ E Dc2H9IʼnuK{x&-GXÑ9:)$hSII6VvTF62$+3zU4KGm OÎS,JCqa'55sbFHx#3?Lԭ(חjjE|G|x-}pح)Hl^@}r1MFĄyT V,7^tNڥ6PF|L |r2ze=VSKpB;09"#no/kTtai25%a!2vs'繧MR-܋wTZ33%LD4šVVؠx7̙f$&eeb>u;\k:P ?SA6<jn4>MJ-AC9hǨJ- ];E FA˱߂oNeR~j]R(df `>y**$kB=f]ˁTіK}1H%X8tX+1ᜰ8=Ku&kŕ:VM{v NEUd_aLv:}İ # 2`ijj\Wqy0>I:ս:) MR .G⮅xs<|گmkzt7I_VmKS SǰuիҩklK2ZMSX?i+;w$\U3nyut *YM310P79hPqe[me1f𖋯XFo{b19WATppTn4v7FZG`Q2H?sM/E|1ELl<3)?AW8[1{/jNʗ@uoԺ=ıjx7* fR;0=G%w2ߌ۾k1=%c-Q$yx*Wx>'OjZV73=: y_qKFgr$<ϖ$ӳ<$0\y{óRӖz|m۷lu ]M.T1'Uu^4]v]: ̋ۏr9x'+]ҥ|}6fHsv;Pyl#rݬ#_?C9lV,#a]Bw;qcW{Y5 [_$DXpYccw¨Ov=SSO^ 5Oug@["My lI3ʂn|a4,&! en@9I3Tl !##za4"svU.Gո+cr3t\rIM(TGs۟}WfW/  F>P~?Ť"--F\?JfcO:`H}B> q+>pYRA>qVseLK{qIpf9߱ 4F'e|e}sޯZ3Gߕ/58QK|qӁث=P Uּ/M{A}Y o n`^ kSѺcE^WҮm'@[DS4%`zއ4a؃Ggރ+aFc#ښpb=⯋,R82O9ڜYZB}@Y{yB?ws6hT}eێ EnBG G}֘ l^|}r#DhW;`p"ryVgцepī90|;Yn_7{5$A(>u׉]bW<2}z0=}_VdbMcQ2%2?X:D֛=/XwiVKн?t7Og7چc9`1\kU@_fXrb ߑ}&,[-ՁUP~5K[X1jT>6˦[ Au^Y9'8ڂV4uYĪ]Aw%巶hu^,~A%-$Sep6*Q륋^[MdΊ  $Rʘ<<q²S{>糏XN4;;mb7g޶W܈fВultGn4N\~v9lD{6)u?*ltqQ$tvs{I4j1m)Bj==Ik5dW8ɋ{7lmUT{AHw#EM vSS '?"vWgĞM *Gjv/}$ԼTYK`H dsٙ((ꤶ;G[JnE6b{"3z¨ܱpͷ?O1l7D_3`HlD rIjdP_4:f'U;(rx *m1`Yeyr hԌc[M >Z&&KC)`rҔNY.yi6,#U_+{RIF`@nƉ+Q Sh7Җrcn.[k]6C3im#vW5_0ݟ*>>(jvG7+.ݓ}Ɨ4c[ӍHv(*qMe.ETt%T- 'M3$75r>Er!_5ĕ+e$M:41FȉH:/zu|{fnF~mzK< 埝Ggi9bL}X9zkҒHEtASa] x?ϖMb^^rH u& ?kE*EFmPߝ?-)x1azTgY+ot#>DZECR{'N ~l>*+Fiwr<Š={E쇆P8PPR/(rv՘ωcnE2;`/p]9m2һ5nЧFy} OE.\lO~|ex#0=MW=Wc h֎zÏ;$yn) ;kOir GrǠZ)Ş;EOB `'mFyN.RbHIcT&jv1fͷؖҮ$DO #.TFR38~,:WӢwjvt0Gu=DZ>dٞYf1pIh?6*G|de?kVd;vy v%I /\ lq{DPCp yLqr@{² ݞϧٚ<ykm ƻ%{g٣]byZW!" 060;[ڛˈul#l]Lf$,˧im9GZ8 e[/5vY {sC(] bFInS٫ u? I亐FI?W,xJ mSsx2R7pw(Ć 85ȨY {!Rddz$IKqB&1OxQ\`:~tX[`~T SA<6>80XRzUi9h ǵWF;==MN `y}ϭ Pm5XB=S4Lha{(o#4&?ep?~u^4 vh̑Nu=%6L) )MCmI-q䚄' .ިl4*'pJ&3EYmZޛk8FB\dFGͽifT{>=[I^;|s|EW Dj3˻l4aʠWrn%z$15Y}wv})§K5wF tms f /ptg1-ˆgVQ^|f9pg+{z^<|6ۭzQПbҧ^4uQrUCSY6G>IlC T]t?c=ʝY\˟ oϽi*͜hgb#X5ay\8W#s5vFYL2ɪK4oCꚬYD&,c6GfR=]tۻ=B5bpFr[ojF󦣷DrxtD+*Ān}hDbWm@E_Ȳ8M{82}yC%B+.lỾPxѹEYQ8x>{UAND@i=85I-"vъD#S˽IvD#ːnc^VvCy[i;ƮoDҽrAYnqv6݀ *ݱRRtGF3K$2FWH {n21N3rDL܌qރ*&H^s >|G*n.U_]2<|?ζ Ds81˭a?ToPHtn}q"[s"Z~%FA77,Pޭ[pɆD;By ^&Wc2 XC`'PFqȍF-}b7щlx啼G8#n2r|{z%dEY֝}$J|'D:g,aE9,w9+9Y|C36[ Y乍fŔ@fl9jmVGcD薓_%msFot8T;67o͎&_)D`lt<9M*FUН=1A VHtt&KT8H1Bp0yqږ+՝~EKdm>v%3sn}`2iev,Q_#T0“N⫝̸i?ф4E"1l ބJMsuTVĈK8p P<׏U cbψMf饅m*h@X]QqmϘTꔃ+DHu)>3  6[14I:#M.],!u)~Ʀ͊Fr:]@9zP..$dxH;e3ScIn.Ve$c< c7 vAUSޖO1λu="̠.̂GQIXԥX2Vػ*Hg1,&3ޯ*#6S9ojs Np`Fr?=)JLb|m9I&_h$lkp1_cq$0[?NLC3Dq&ݹܫ^uZhSk8# X/Sړ*٣dvUc(FܓQšj~u,I yA*r'İ;Ec2)3Y$x6ַFNNz*sܸEx>8.Py3דiيWOP#sZoL+36SQ|kySbqg˹k}?D%d}c׺ƲRW+syd_޶Z]-zΝ]&^0mE ~>;>b~f'!VTc½몺ZBppe*1s])!"׽SZD)FKaʥ2yT28_"_mv*&ql~: Z.R0{K èm*?Jџ%]?gxY͆6>5N5i=%yCEᎧ2g8 Wk⺝¯Ҭ'b>[V afއaʚ0l[|5h*&O (jjDT?y? H>j|oUP~ԃIJ%5߈0p,u()=M.qOWY)'yk'xc8/Q{9  tWI+ S=~:oQ|^>s~#.Q+$RlPR*jGϩYFܜk:ěd##9lTsyunIi!*y rpzg~W/ݔA{0mmyJj\vەQ3qB;Ƿ4VΡQg6,"4) ya*1  +'kBM) Ғ6|T6[ϟItQ.Z3'˩>z%u<8X/~ Ozlq KllGw5+$Y񞖞d;7Հ-F+TY8 w?5Zlohi~?J֩h7ב9 qnp'w CH'ђLb^t_ڬpPO3M=l>%mJ@fIf$-EN_Yj.V2ďHse˽  !]{E*Su[}:ItӦłl10$|>GSnP|rX_1_HwM?׎=eXĒ-xʃѨv,f2,P1waTFmۚ&_JZ%^/U4)E@,,{F͡e n[dB{е?4[ ޳Km?U#>Ӎ_aOȱ/m;@~x_ \~CIz}݌w'jCRU[br*u-?Δ{ \ BB^;aK|d[:}D7F`3h6֯KV3OLk+2j)%HL*1$ sg c4,N;5~^gF.-v?NC9Z4qu#W9{ǧΗl~8P!I 9eU=]or}N:{Q[XcxGԪ"2q!r뚄s,QDgr"E'R?aSN3+<7#^<gүȒfqw |lqr[L76x/FK-@m-Q>)liz7oHO g/*y/=G5r *ܞe}|kaUm:_4m#.Bg/f[ּvGk=@/r2.t"o O9A-Ïƪy+^ElzoPZm!y|ϥ.$uJw=uti$!A86̞a|2ِu7UjŧlGzzmuw<&w\\b ҵ8DI*b7e0J~Y7Y2֢'u&jGQ+D A8Ǜ%,>OZtRƋzT˘ca{QFux -C}m8fe)k^KϝqbVYF%ݪ3r{rJw?dq.S[ZG4ZNo:Q k&ii*AlaSkbEc(|C0KG7YaΏDӝ9xlk'RN#iRO*2@^$Hi=_?^MBoZ2OK$ܜF^|3c_S岍 n\:\ndx1tj+Jt^uν'-Vx^v/ Go)9ș)^IUnSL5tp&NA-B0F,:cgK2oPwEI88AUE J@D#H$`BJF14&I緅KxoMc}i-1\rP;l6Sc}xx%㷼Ҧ.‚U}MW5ŝ]ĶI KmbdL;>?Z\P6t]VT)ldc>y"V^em} ΈC DL/h3>UE y1NV4֮5kfض@*@!I$*$y:W]jLQ2k森EqodDP˱v#e^S !$v6l2gz[fڛqP&2&g$}>T 5Sŵ#P{ 2?ZИ) τ6pJLMuIg*%d`>_OM.Px^t@qjm r<uDM#mDI,sAA_WJ6_}oA,R.7#R8I(yQT/hB[ݼhu2&Q ?,T"ی* 2Nh_s*G8jL ,m>eWG堰]2n0ro$"l24$+H#|T.ޑ$u\J,mBV0F?ʤRdϯ%:ӥu,b6pGp~U39oHYDr DMYps>ug-JN.¶K,)W>H>j  k:N=e݌+<^E4]-6eay7<s ߃]gzi\ұws럕rss٢3Ԓ-{cnH|?qx]_ҭ(u'IC2ͧ]ac\:,Y+vCշ_XX0V%MY.᳐>8o˭,CE: 0Β{[=U.OuBOn[dyӛjg/tmځ.i e<969[,e<+;FD{7YhD;A"-U}DU!I:Qo-[vh%^41 vJiu !݁*iOčSGMC2󵌲bZ̻OγOF vO'oIs[@[nAxjVL>]J|m'nPmroj{D}OHg@ҬѧӵJPđHsM]BEu.-əcBMv YkNoS? [-5be/H9hG:n$g>' $i#S-|VeیpsWus(D "> HVԍ8i|E~;be0Uq*N?ghK3܌_VN@stq2}OQ\NG?4ċ!$ɪv@E.J~-"a`$gߟJ 'ķ9#,''8Җ7ay$ h8 ܏mSDaL/SBLC_c<`9ږb@ҵqIݑ橡ϖilW$QJYDkn$F~|.C=2[vo=x9*BlO^XYj]_gDVo@'`ImfRHܫ#ʮl(X\!4cP@$n`}}J. :g/c'֬TYm#$ҭ.]/ܪi*6fԸ$0 <qau7PCwE'bPqGLQ9=[ܬ}T)'\JlPFԆ8縦OL>f.7JBHR"v'8XdY8e{P $/ l#ػʹ0>%qlh(FKԘpn^19Kс >> Փ*"4UO|c8%M s]>gu,Qg+KpKP ;O5*&^*2W~u'D:Nol\;4qlt#Ԍ>ccdFG+.y_uu-{5n&"x.\4MS7ozG!.1uUwGҊzү݂Z3O}i H"T{|$:zkO:{|-1$s>ӖD\͹?7_^»H4}*8vwFx_E{93 1-*f1ԲyF["kȗwڼM{e6 R#?|ںxx\f s!OZڬApR'f.}*4|5 FgK+}mqCޖ*Ԙ=p-jGU [̢n:{>j)Oأok\5.}V"3U}Y3tmaRC?Z㳋CT#2tJmocMbȏ e*|9>GB ~n)䑀?:W#mk?t>HD:d/*moR<&FGٺ_4=imzs=V8c?GO@GK̃m~ў|Ϩt ?I䦿[kfyE m. 6sKzt޳oh)%/u- I{,`|~bU%VїMЫwu=cIqxt2}bhR[LKkT?_Zk uA[^KLJqu%NRoh+ԔF^O)1o@<*WZs75Dg`{4q[k:Qʭ|3 i9O<|VQ(|,VJf,m4Gql 2Jʇh{^SR+46as gjsk5a3#U~Im,{FlySM+Ӳs|:c[?Bʬ[lt+.KXciKF89jŲm^F-n1keRh9v1%rI8{VQ53ls_FN ?՟ђ)dVI h&+r2= 4|d#R&sI8=z?Χ$ɍ9?*9/s698RόsOQA`Zƿ'[q+kc[:o|5(}VjͽC"$9$O)?LM % Xn G~*4OQzh|/{-v,2űl`~-}I%$e }l*O>}UlUQ$4#n`~$Lnִ8ͬ;wars&qйE#$ʩN8>+[=`Ől<*菸T #.p9'n/iԚM[~][c}}U?nio"nwLVi^J%SN<}~TQSt}K.F ) #8#JsWgĄ⸻ ݓC5q۴ 1h=KkԚLvךtkxAwH~;wRExvȹ5y %I!tlx1.&S+c# tWɋXkIK]:3mDPwڇ.f˄DRKŽ2s˸ 52zu|Hq/QG4$c"\'sJvk]>1MDZOr]9AFmpG\g+3U\ gҔ\+fR{N7GOiQFs;@qܓEE3Ý487 f*Xv,I9zI8|M:P!@RAxdqA$&ΕY5٬Yƍ4&AC@84q.]etX @{2a4ö @_k/%4X*=ԅOrWr~Q&i,20y^htD8 ==* dśs-(pcėz3BxP2I8;pT 궻kӝKAAg9%"#~r~Du P:^2-2@S4/StOe:h yM?:r7W%;,dTqSa2Bxt[3}>EVxm6cvw Sh(Q $CB$ge%[8ǧ5(@#NNN[͌(ܸ^/29]/\^Q^P"6F9I= T!Qu!39 ~&j Ƭ8έ[!W .}sm>>4⸊BF3S-&𪄓َ#g}0?nx>ݽk "}zWrRn@a#<ԝUǻfL"W`ߐ}r2=.7C%t?WdJd`OniVXR6/R^֧k`wPOm 1"|Y6V~m[FpHlS东LnH4yF5ΤUX^m #0?gcekO|Ne cV~jg_wM~/Z-6fG'i'͜XKԮa,3Lw%ǭs99O>ܫ~ SZ͐H%#чO֢e6,8&8κ5pPR(a$7\qe 4d9dsr({R+{ N\MskKֵKΚ~kqn. u"8^O79p@M?5vzO}SYiȋ8L('$QJrfzx8nS%MjַΧ"~E |1yc]Һ쵘q;c$({GcKK4!̖.GڵSGWfוN23A:iR{`C| _5{.BOO'.zT;fݱ+ t1'(@-ׂ˸`GF<}5iy[)'`w,`=ʰ#4jZ0AZwSB+lH-1iO ɯmQJv=U{58!<b)_+zi͟]1Zj*~|ú*}p@Ϧr+tekYޅ[uMlax0@VCx򦦎6'Q+RCͼxMYRAeeIʂoԥkz.OmRh}޶ur6qӽe{gk F9$.c&wGv-˭։.  )F9V89^([IocQXռ9PESӺ4Ҏcў_Oz;xZބ,2nE)-\C-9y#iIh2ˣԿcQsjڜTp$`׵U 7k։k-ߏm(?jHewBuF&{)kc{+A3^iP{9ʐdzXd [L#6_ ,1RE!e3n֒j_0V|Dѳ9?|.׮b}F *AQ4C;{CJ :wx:z& odhfvFFkՎtq^茗6y*-6wy$#4;F!%[=ZLΟyw%,]Dےd$ClPIR Lb1;d{PԻ =B(1ʣd .3R&hB/`shmr$ 1A}b,[$p$>\  Cz9f@q }*l̄c|X9.CssLdd`PXiŇѭԞxR`K+QULBIΩ(:@1PA0k4ډzCLF0ʀ{SrEO.^X{7oohƥKXX&Xχ!|{Z8(|&%kJLjHFw '{Tz]Ä*[wӚ<| amN{*HNWة6@aYS'KUAIbU(b~HK?x|f<gF`yQ-xwMqd6̮;ɥӴҦ%l3 {l֕kQ4`;c6|Z[K,t=/SТqp줱9o]۱m+-{LLs2K4N?s}軣\$/269_\Aݘn\1'/l1k";.2q^K7GOMv5xT ./#tτNJZ $6Js9'r*?x\xIYCR1la3(R2+͹'5[05̷ QYj(ϰD]Z\Z;AsMY<pF4}u񇩢УL^?M,nYkjgLfun+Z~8c[媒Gz7N\ 1H?i[L|biCu(ըv|ЎwяTa䈗*;Zfܰuĩ{gA=qB2t(JTNrgq[NWldDŽ#Jyr>E]./*vD|$jz)GG^w+%n3; weRP}Bq20{L4K`OZ6#{6u|9E8SH Ejϰ36tHr9Rp#$~>2dqr_k??{9ݼJ2cՏ 2\38e܅cڰJ;XQ>}5Ɵp% n' {VtK6{d: 1Sԍx^U/%5xFU ֗1%t\@GNԏeKRa{ڎ:պBJ!I-q֊?f{|̷ ^&aق'UgR{|{:֕|d7r Cu˪S`ce4]+Q*J۸oֻJ;|\TR_b#[J1ͩAi$k0vvz:Bnx;{H8Yѳ/ qt9ȒIexԹC;JpzCˆͨz$$2ѱۓ'*NyΟ:z_yYSպ6d#7KΣp#E:ccؒ\^-:1;Ͻ:b ;IworU2DĠ$zSmi=i&+y~ǝ1?WXh+u'zK\lY?٪fugH<"V+)?"8?eŜk:vGK\{uYC5\e؁ҕG4gٿf§'Y/&2=5WEwGXZ(-uh1(@GVbtqq/} U-ݝΚ[@I~&0MKA.wDo9Kw;\0㶿xL3sv]=24>*I]&02?FBg&[ J+RyHA?R ҽHMz*[j KuĔliKz;UP#?ACEv>1lQ:$THOYR 6x?4nuDeέҹ4䴏ETx-KNm I~@<,l|8KU-X,fsXT?dORBɯ##TrL^)f $e,[_59GeaQ'Χ4_sM}|*Or=1P:֤iK(È qٰ* *>=XI;Jпa8jpe#Eܷ/ K21-FOqS,U 3D!bỲ| Se^8 #ML8*3SeiC$b[y&O9Iq_Í3U֤Ե?V-,C ,Cx4ߵKBFXYlՏPUvTJktk}_PԴ  \ Ǯ*8ϲ%9q[k*wF$zToZtxB$'rݰU~r\\\F9)}$8Jh]sҍjWWWY ʕ$֏kF =38k[ߴ;#KϿ4GfʢЪ 5E6)0@+0O^wrI K.r~3>t[{[4}Es64A-ʜvlU<>aY=Z->}J0I BW, q=P\ T1N5ZENn~ Яcm,ڬw{Zs }>׵wSYܶ =jd(}}RZ4>!SZDܙ9Rfsߒ!FNQ?#;34q5ƙ+[JFYczVN$W6szO{6fscsMZ9Ӈa^ 5uc&g7͝=}NqmvP\O;?[? >u>pHdp;@H[i%=L|mQ8 1EB q}G([h.xx;+%\OAӔ)VGDwZF֫|Q~XW_(?i#TsP]"K(R s5Ѯ2>ԧ<ٔ+wwz5dctn>YOU\nP u0۸sE=#K~oSo<Ծ]EFOwo,&r§6޹ϹoC}9b{Vob=ْ3FMgף-h$#Tg ף)Eif6JV8^/xF ǡ#XVed=wMCn$8}+>#fJQA,D$IضA ?zAd=N(/,5[9 AۑIz]r]M<E!{t ͖ĒHiČY-G~C)ۆK6x{cv;Aҟ%)dAU%1CXeʠG;Spڥ~'=x3nM)g ||8H g79u.KR*vs^9 p'#@';rx;9xgG}3ux-D&;O|9:O4$\!:p h9El:}i|KWK}nroUM]ԗbZݯS5-u[pRX8<7ϥ_ .yaUv,Q90%}֢}=t>KM;Y.Y'st۰RM;Zп#R^@qCe:dfEzuODhL0[ZqAgUpUJSiGLJۣŮ캽˲'͸⯓n0C pw)*s>X͎]/odikg `ahM*#P6f4ntjQ6 _՝M:"a=c?+JLJ)>/41s qg;}+T$|VY(.U:M  )WBWߑ_\݆gg 3+sUSȊjʑޥpi3HܛALoH3R V0AU'ifAKRS|1g**=᠙?DS`>XGyA96o1n( 71P"O*mc=u4SH~J ]\[x_hǟC* b/qqBPHTA@< *GqUF\d&ȣޤےޓ^Ꚕ:u#\p#ϗwbBATŮnH˹vȮFzdk!_j _ jɠO&M2hV9 \LLyG'P13\-V;fHtYlv7<|ѓIp\w&q p3 RxU|㝼[ 2?*h }*hSDhfuU\qSDسC(QC7* 6fQ:nĥ/ʡ6]y""USǦIf}WW R HTh^# ˿`~5@v O"c߰!fVsMC<|*pW?tzXi&$$&B"/hmQΗt}}=(]+ CjVa")xYLbn3Y>6Cr(/LaSYZA.ml,Tod*&N~}6^ԥx`x伡9ߟq s:Li4P¦PWʘ+dw `[ KpA'OQlQ$ˋ-\ȩwGscFV2jwZ^H%J@R(★g]]cTiK鶅ż1@ݜ0y]PP{V4K6CG†݂vN99s,}#պ΍0-ӭ.$.mDdȏy\r">Xō r@:PFy<qAffv99=J1  $@Fn@$p 5J_VH9$GrKs=譯fVh,̄(UrA8'Q)FDIn[.i*qo6PLsgt-bOA0X_rP&b{DE`<~>6_;Zۛı3e}MZ']]U4cgD| |jr7;k@kkF+y[/'#-.21U5=2bF莸:eiϤiVK3J83J£ =95#Rp4J7b]}Ez̗HJdڶÍM3 K&߽ު-$n3I;s1\WC}?!@K7Su\[sߴm,#DVaK{{Q{Y,8hQx>wv /+bB~d8/fҶ2i=+lkN"fo$Ϡu9¸7hUy?IfvCyRzlY}ecGC*lQǠdBy!xmԣُ 9Ż{VȤbQu_>"F䡀?Vnjt%1[g67R߻dݸ?&[g^d7&bUkw;p u霫)P(@w8KrQ/">vGZja@GzR냋6jz{ < mDf+"m'F0}bڝqIaUV& +F?L7?YbM7Gdp݀s1Qp+O1 ?Y4,.fESOI*}O,<8~x)zPTf}7HZye*S|&:Moq]yҔO2~|ilԻs ۔a56o_RW]WB0rZ= PAG?hKQ+ATd>Qs榙9 τhSL"M0eP@rcr)13r>9n\V(Ij *~9Thn[MH_E$,-qSc$YYuU/ D|e=γZ%rFvpF=)Q@N/jz*K\^:3_.OjrDq3#KO,=4$E`jh!6>Q{͜ g"_ g(t)o;[@QT>*\NIU. $5<͟ 8ezz]!Xu&FF"5' X :1y\Lܫrm}L;hh/mfzt/nBCy?xP8#g%GH f!q~U 2f%2ڡZIҵQTYASBu-Խ/6u"ݔܴdo}s502*z[D}ŭCeRFWT ۻ>HF]{9s-c 5Il计L)c P0\cHT!䘘{x1vFZ.KS(;mE =Sk߳o@=Vh=[g+=H9#5f\i`zM{0Tk.H=kr8R\S>/|;{+5(vQUFSvɈ唀~,N3AdՏ)>ǐ< #,3ڵ[#0Mn+,L8_7C. Zl׿Vv8jl'L~D+H!ݜhug-E= PFq&IjgwJ}vH>ξt#޸ {P{,n;߭kI.ޢƻ60Ÿ ";_WQ똵Ѣ<4+қMmmdsʸ.G)ѥ<}33Sn}=siԖO#Z-*qՊ* =L5l/5&;I[썰]`<iq4m k#X[C{Hʴg̀O΂`jXHkaq5ȎM$RNp3>TuMOɋ*EI 3MXcz)O7xֵ%Έozc[mAFZF!//єbg\xev+ gXIJ³GOL`QS$tq`*I ψfPlFf=Xmt?x| W x̣23JY]Z]z>Z-ȗNɟۗ1[';'R$vCZT#]ӭ3N54E~AQ9O|ʩ ?9éebDtphh- V}1*k][%{ݎ^Wopz!ݏ 2}:VN 5nҵ+=#o4sǢϑƳ&=.DŌH3g,;{ > ]63rm5u 8HgWjwc,2xHJ=Ex+ O󥴎F^k7.?ʔ$ "=˟lRQԮNHA-S )VApܝj-[(m+sy>Ơ؏n4$N91!EP5G~|QGn2[e>'24F" I]tP=#2 VW)DؔJىzRԄLydX D21\QH^_*759x ;RFzzע`!׼B?n df:N7@CaJ$g>URhsd,MJ}Z l &qe>lMwu[ȤIe6qw.5_peFyʹAu4vm@ns[,8D|CὍ楫 v`Kت9 ~icԸOJjk|:͞%W ֒ʡH9^6ve?j]Z&M>- yƭ 18[)B|.Kf_ [ k $h*S99ZXst8›KY.Vݢi ,s`3`^& ~CSM<,q{vZx9BPz"e5 YD>mAIS\Fu$ d`"'0:FFil 8 ~N5!u;đ&y89J) wMGyu~/⪩yjƲVع3(tTUUqсk+$ިVx#ɤ]z%gBvd;ZY>B}Hb,kEi}fPC|4unVW8uPiw?;'E7z4@kY2N>UU-ɣD.~h7!gm4&<-]E[곟r>"rUQk8?™[n72j+XH 1|r}Qsw{~_F?nE.>NRN8 ,k B-9|A~|vLֲl3o b[G2/!Շqw=w#p>eᗊc+a u\(sLYN8Wc ,ښ #R\O;''N9~1BZ|8kU.R$b~5TcyW_q}H 9Nũ/ZFP&Ow}EULUz}gkE =w9\>N2Z096yYKؚDዐ9=j[J!+*}3YMo]d3?g"R_YՖu"ǍNsLқ_(liz]%ƅ^rpGNZUe==1$9U!ۃa3z⣖ WbŢE0t"IZ78$r2h)D7eMiŦOi%6y`+* *f,y~+n玚ذHP_@Q27s8-$eYuJ5'ȁ5ΑL|0I;u;@{ U*yKC,OƧ!% @n}7Zvdz6+&RrR{(n99G6KNxVtizSZH;{7Ilx }+U|.[80JN~۩ns#^c9$(ޚ:PW| ot-b:qV7fH8>u%RkgZ׼ۇl"GqnBs]{cCT\<з0Z+roIۖᓘ1YlG2%]sD[ fi91ζ/'36E4~87>Lg /)MWIHw*`g5E$zVLIّm]OeHe9 %YI7dcuIc:K^'n\4!{w ":tz{pnzGRhw=ݷ$) /{fLDZY#ܿuOȎ7wߔX=on%>C?b> :KST[S_XZ>q~TOibvǪ4 nmZ6_~Y)rVl3$L} $<☲'?GRDES9}Rܿ}'ԍ'GxJ$+˩BHF?Z.f8/EjHBF1"{<@6*鲶#)>(\Y {n\{dThzt9PA) ]f)ly -ho ⦙80 M [Eb p[ 2}EQ ,|Ì`c}Emo#^1H1F34; P";-$X E YH\%vJ/bE Fp8SnL'3)ʉ0mlRmQ8D^PsyH : Ȳ\eg]Ź9ۜO@C~*&ƒ>T{kLH2@QmӔ9'}7=3@ѭuNi](Ңe\c 5ʟ-\j]<vVZg)O*`򠺐Ax(<n*:&W@tWSu[\-,Op(f)_#ZG7+W&UtӚV?E2HsMHf&d2H|9ا9n0|(j4Veޡ.CzZu㐫:N6Hc FG'8)m;­ޠa# nA qOW\㽂95i$wn'ʨELQต^M] `gj|Cl+NU<مv"%-帴FXVGF Q)⫰On]\LZG=YߏZhZJ#P6V"o=6߁+URXydc:Zב*Q }˻=}:eՙvG"$+a%~К%- jǽ 47R =r'?!qǭI.$> V&9n#;qe/^| 5ٶU}wl(9vԗN>ĿIhjvNwxsQɓ?!PYQ:UO yP׉ۑ`yt>$a Ibrv>hǖc}lҴ.=VU;s"*  gR߻U0i-M%ʏ sھwQ>b/c8Thi i&1#j8 F9⅄K: *.sZq>;PBb]#L~P},/ \ܟB;6,>piOrqUWs@1r2[ȿcnYyÏMODqytZXmF|C|VOsre{Xt]O'˵B=}{S pEf7dБf2920sQ5MŚTiΪ]F*@$5ˆtOKTdP hIFX<~u#+# {Y=qHDoүH-T7)եo$յGE"Bs38\LyRfkxvơqq$M+j}T,yYòqd)PW>LimaѵH(>\a};J|QPי!IH%Css]Ry[ h]p:KV0;0ǂ5rOznmdGĮ讫雹Ɉn:ȣ$ x>Z*7(N,Gnse7\0k^?#D"L7HZ09S~K3Ȫ9.20~EqHJײ. ߇/Q6FLvcMyxCUKQbi5 2M:v6$*{~U$ddMě/PKMeZcC3!!1b, ~3GeWR5IK%Ui|_#I#\g cpjt5zT[;+ {O`>| o6@+ wN-h[izrMbŏ#탻dޞ-JnHHRW88Ε빧߳=MN9[y͓(7dvTy#}VKYee*x9W9@^GO˺5=ZѺw6=AUeWtg#Kvnm_K^ݷ}0S+1Mtj+jhut2yґK\˄{ C\5M絻0^Atlu rFȸoz{|u9^N5@E~coW%BkM^§ލ?<#"LdY[VͰո4HXg|GHt)lf.-J>^%]|yLӕjGfWo5v;dS9${KuWCBcքHtG,7ad_hًޖPoMڬ=^̮$܎kGU+W3![{(?iu8!Ю+WvhJ8Q|r0?{vGbjR+>^Lr1!oҦB"v23*NY/9bVڌkkqÐ?Zʄ (YQ61MAU]Th 1.}"䯇}l)`l8*!/qJG}RBOn(]Rr 3ݪ96P{cJqimd~Fc96@8n}QaG!F~(|5?_FUzyHV[! YV%8~u+ xe&*P J;38O\T JIg/GR s~㷵Bh̪*{?ƀOaҡњ͜,N Qu yb{.?x~,]=u^wzF։{sXzN%ɝ.esn&9$;`Wrg/d0- x93wWz 0ɢ{ǩzVy.:wXZM{gvf]u/ OV%b+++U46UGFO4=I$3w9sDiRyiF`ҡ8]'nI@82N38|!nG3'!mAȌc^8l{g_Zp1. ]Qůt5M.n[A̱\636&dSm5ŰwI{"<(˼0dyS+k&v?)؉*ʈṠHb  {*u569V| c۽@Z]>8(yp!iN0q_* s 5[, 8#1퓊'8 f4 ,Y^OˎimRBGDpAXʠ=#90Y*J>TR.v,9{sˮ*(FI%rG"ځ=+։xd~֗2ZZǷ#e. #kQo #/!OrRk\*M*ɎܼѮX[,F#hl)lc+ǀEq#^Loalny }P]>Yzm};Z]C\DžW*}#D;-B6~ȻF{9P/JNi=wXLb ` wsS.XOHXGwljᙎ!Nx:s#uA#]:M+>ȃR2IkMPꮩBjrJpYF S]2ח(ۮa&0GFUB=GzZgQ_HBSnx 1{"͏UX[W }ŷ\g'ҁ$xۜ O1K#+ SN,.w}8lJʹ-E]&M˪'ֱ:AQeo|O1轾Y[۳~ jt: 8qKμA׵4½PDZl֟zVXͧB}$wlc+P]O G/Z"LDmclgpV{05cEEoG '$?*G>M/Pv2`ރ}G )oI.Myqj~fTQ?ZZxӮ4Wtbs}E&24y"f{]^WqҤr2`ᵤvf}6zO`.bRO2Ƚ]3̚兤=hpۣX9zI& 鮘4-&V2-'޺s]7]ǩFղС7QIQz*h1hS-Џf=@qm'?Rj՗1Or. V $<xMKp糫3#(} %`A? <=:̿I R);Iy v>q?˻Wr0a6̬_mQqݍ͒Gxw?%T_U;;V!=7Hish:hx@3*JpK@[$-5U#F֥''MKrQv7+g D֠y&/k$P* 2&v_zjR~EN2.m!r= &xAe+]kRԵV{{so%{cҍB4Lu kH.mG>2N[G69Gqi@G"vQNBpև6ѥլ8P7fsПKޱ:z%Ȅ+ r3OPȮ˗o.ҟWLk{7&yQv Þr9=u˾u l'ɥj'[ӭuk0]Fv]m'bp?#] 8GHbYɱP妥}ÀA97y# y rvUs5} 1'Okm>8f* x棌P{L8xt fT( +)Of#XOƛoR'B֟GM2"HKx8I(fpgkہo:UUHv>䯳C,_Y,y-n3/Z)iWxkF}2 2֩A{qn$׭5=ٲܑ廵_?RBWheCd$\Pr?[/],daib}~gN/ȟUq$^t[&V#..wyE$ y!}5(kEg@ W>Xݞol)w)JU@cYޙklmuFǃnFo`3lBM>P?w.!62!/ҡ`>~,@ʯ8QI2vsD.2y@'5T ٶ۵1nUj׶S˦i u?ݪ a=p<ߵ!l# "rA*QiU@Ÿ8*3=@|M 2,RJw}(c%#5WlI4_*#捭i-PeG8Ji2ɩ]_nD*T@XctQr55]_YO&$ p\8hy=Oo2mR(-uK[ggA+"T>4hX9P}2˒0 7eEÎ=>bs5 i鮟-ӫ$$po![lCdͶm" ®Ѧ6ӼÓ꬜㠳vl !yK\Nr+*[ILh 6x;IT2W2p5#I.y9P^eop%ِ! g%ŰmOtb<d8`8#8 8T[C[ I/ ۶p,|JMZK~gpAv`sڦŻ8KgO,` Nqڦ͜5HA #<>FH4.:z zoFuH:$i fF RO.*juήVCoxKHy sƂL};3> Ȓhc/[5_]/Q[ CņqsJoͻ.1^m+sZk,Rs }ppjտp3=WDH- d@CsßoAA+G*I ZB[\K#@>u6cxJrhKA}VKMv+E$?=I0-$]$+ nVHEu; *2T7qG[9v_l_F;g.{$p9ÙK FpN_)66:{OBwRu&im+fP*d ,ͮ!?/uc,Lγʒf8 `Olgo-wZdr@Ìs@ooė wHD0 cqiSLL]oz[γO3xRD+qRbDm\&HDkc4{agy֍bbuP꛷Xx*\>SQU2e#6`~?iknlđ0ܡG$<;8U&68~YZC9Dg%Yc*mLhi>j7ŝ.7'e-I,(q’Aew O K7PZgt;lў8p{T^mZFMt*[J`nIozr!F `?\WG2M{02~A!\o7v'gdu|?&eos W-W-+^o=vhJ YPBzu=7r'[D5L^Ck$g-ܚiq= ujwV\ܗ%㍎B y?:uƷdFo;^>EGsIq楪(Ǒ25Ġ%ŤW~4pyè<=+:Cײ.=͋u^tMn5k)Rge\@7Ѵe>3Hnnu-q,TYBwnqH6pR"E)8K4$Bq D [blvTDa9TBc Ρmk:[&`L튲X.ӵn{p̥Q݅F ##[br+ZֈmW)7IlAq$c&9W8昘qV#)$m%mTg FF :ЃG_&xASXtx4VP$3895ZBSd~fWib p2_lhĭ[DGL鷺#-U㪞7Jۂ9'4 y{y͝'z@ދ)a(34(;;@h5#9;%~j 70wB<|PF ZbV* }Ρ+`+_AlsKࢫZLKFDN*! U ? ṵ a`@cPqx(2\2ǁfy99𦤙^mjmܸ1(@F9(SG\2#lGpwmn>~(*UTݜgM F!ܨY~sOzbۏ!Хm}7Җpz;]Bi#bzz{Ⳬv1?~&CYuM:M 9_i$g:|1ӗrLYЬ-Z"f0?{'NOηaK1f5:g1+ǵ3:9OZd=|+oS忸H^O2&/ŏ\QBQ 8D}mkHCO)z}iE4 m&KURyZFSa"g=:# ٙ}KЗ/,RY[p_$hT+#vN ,l.=G J]&=FrsC@ڠn'02iW3iֶʅbS,, mp$j6&ON'+߈kƌi&{͈Ɍlr8䃊h^v?ʖmMwxAW1<ۘ,rOcb&m 6$8;S9#AG.)a0HétBK6ىb?JC1v#4僚[_]S0c r"\^,/=GvR1٠éGc @Yn AqM*2c袥/ NIIQ`߮}}ꟺz*{ Ç]~2US/ ksg4m"&?^r?:)-l#^by~=q4f_ qwIU]rc4nrXV]Ԗ~-5̊+a_ yKK&>,@\ʑ9RCj̰B/ZX%刈 ?;Q&A\MvIzhu84~:{[c);6{Ta(i3ұ,kEW\OgЗǦi~OoSȟSŅ=kƊ?ɮ;ԑe[kG8EtzNmpKC+"I"SN l/tDhGyi#,C$zd0>Mcπtz>%K[ЉvɟNڶStu>5xlgj|;oʼnx*Ss<GRE.Ku #yq r)r{L"״g?~N6\115qnʮVnjK.fK >5pW!<6y~T/"KrR{SzP֮u\Adq,/bUq"Y<@}FWd{GZ5t;4. -$vYT'8gm]&f@w3E~AJeRMWm"+VF $z?37 ۘ Q4?Ksjv;j5E (mĵE8=k+M6 VVy &<΀ʊߓ*#\}.EKzfSisI ﷑%,$V$bHvp)Ò󣱉k(AMԜ{m"J{coeQI t7&7e.J`@cvs,&FU.W,x՞&*$QMOA'3W4)ou]ͳ.p> :Dyjq"Bm;9i ſK8uj "@2\ -8~NsoB-#O9\~ [ș|;q8(=)׫jE[M]'9T;wSam)i~ڭvm.B-S?+alpa]tfIn6yMhain=uN1E㴷vZX=$n{zx5](AӜ8\*vUlrݜB|>_؏Q&_gvz욮2)튋x[&*qh-MID/d]։lCnvwQr@ Vḩ4c& NGByPEInQ..-dw):pz.+`UIm\n qS-1\d-c}=F*hw&?k9azqP%.Ysؗ9Bm!TOǃ. $ 9ǥ2휥B&X pxp}(Ȍw> 6|`sqoLl9"O-ݍQ""*ap=2}MkJOGͼǴ-}{CLjV\-夐4 0#Si=G|y6}~Bcj=Ba@1 TϽ^-8On8;rJBO2ݲGPKh̻$`qPi"OYKa*:u)OH0ݜXWr@HV ;#c2I•PpI݊. eX8IN!X/zjMunD.xM!';X5˰[ [[i5QuAJ(&JHޞm-RYȡ;D@pN(^MQyCsR]&8f^݈ESKѸ³\oѝMgtغf#P\9Χ_]Rk_ !Nh}qtX[۽ܛcv` @&/!t̂z[ۋ&%Wd+hWm;,}Ӷ&pn~g:jy(u|OE#M+HC[@i8~X#Mjzx{~NNkn6Gp#*?pn:UY2MRƒ_s)/=FqEL>4ɜ'h]bLf1{HqƚMե.a$pq %0IF3B^MyP[m?С #A`GC3RI}1L[G:l1}Gtƍ=rI^Q =Z#*A3dN 5rEѶH؝ ?:UK?Xۄ|?PW$)p2t; v3U8U2\_o]uVbi]Fq `)j)d}&4kѤIUޘKH7}n14+_F(X*/t*B{Im"q?qV$g[#l>LLDNFnV1,w@xyh n,]ʅNrg>U VW\G>1N!_dR"zuou\ Hݙt2a>v!,,Wbӧ{.HY"ԑ9;2jqґMm^, i*Aҧ D<; 垤j1HCar?Cq_^]/+siWcuP#BrO xvosXUҬ]IYE2!o/ch32\>]b:Ǣn4|&3`db-,O 5K$ |q򮤲9x8q2ftdUk{ff{pv<=+u^ p"}[3X%tvKkeOӏUIT.r9(k1yPL3]t "Kiw$Z[P <}ϗnlci"$m٣fbH|Q2@ݼc NX:j+Mf&2 V\9N2l_"s-!wX,f[ >xޛ3`pv)4d˭ZV [r!>Q̤Fv6>B޸# mJ ΄Ҭ%rw9SKz__uvt;J[v;ʝ5U:C4^y&{D,|~f 8 v89#Mm.qP|_RJ'iak_76ՠ1l͞F8XԅNIr{sSE!3zc3;}* BDgp b`ø߅?\6bK9Jæۋ-< ,LpyQla"zA-W]Wr;犃z;O5->K[N HܧQҠc7"Gnk"w,ȣaZAy>"5=VmghVř n7^soZc`Tf%wzl:lo#okxN!'s*Ȥ;!؜鎣x/!L"; ?$9 |EəĆG2զtηI&wsimnޘ9 $ N.OP|*\fN/g K"42d@f$1E:H!hHnX홃1!q/j1ǟdT'|+ɧipz}?= Lڦ\mklHH,KHFsyA UV-$=R]guGfݽ`8NCg,ZsGw.s}ㅄlj y|um${ r)y ٜW޴ŜYǺ]VKY%JŒsX‹\ƀwG6g rV9'V H;iNj&qs_T, f[}^6B.r0ʸ-I;*X 1ZM,7$$( Wz6'T6㱲Y8',rO܊jQt4KL`ʩFC.weK,1G.UzA_e駳̉oxj8>&z~TTmʝݭ0$iEf~l4VŌk7(\⁶5qg}xnLѸlVf8,!;UtSqSY?toBᱵ}3hcy9]Mee?&zE+"?hHȹ˪>Q8BN RU`pTqX-We#- Ey&UPB(Q71G" zލ)H̦U^<7H$}wDgIA= >jQ֖xEKG S<KlO勛 )@cR;C?Dw1CR 6 T7cTv?{W@wOl~T" Rp#.Uw36zcFtu:ޱouu}ۗ%#wp$:̥N-]r;(QF$ʺnPu'4ܞ"\0\O#\-Kq*֙&=@\ I*ߓ{%p"a}xkyJxGreEzŔBGV&f>R>-E[١UA +J|kY|>prpyvxgRlz_Ď29r{هg(]J䙸Mfh0ff<ӷq#ubL>:ۤa<݉b[Aop7V'W#1z9inm@e WЗku1N|CtCyj;XHy@#@f~$ToM{ĮY$׭Z)-cy6XJF2G֙Mo'ӶŊDG2#S9KF#F?ʎx$Қ"(b.*{~^~͡kM }*HmZORS`8QG*~Z4xNbEa$8ESs~_yw,~+Ln&~L)>r˟nin s #[q6ЁVEBM@?_IE%I8A#\x] _OAupc{QTZi=E ޳vo N iZx&DyV[wKk7cćW$ޒގ#Ҹ5I W5?27Y! IEN3ǟ9_qusoݵݴ4k+ xGqM20q*qѷ: HNC[yem'ޮPrMr,LxKWq?]jkD2Jsh'# ҕ'#(bKy^X @Wʁav)|ȈI>=#K`=VB99j"L) QnVJH.%\$L"}28?jT? hȒAccszU["8ܛ^ Q5 )hTPXmkA=͹2[$mgQi-Ǧ(tDLCrGά tazin#~|1Jp?٨aEkEpYT /~(Z[OA1B|bX`8+mz-.^6NbVbw<_QW|sI*^RGn O$jj>QKmllp/|v.~w"<}(qh({łpVl(mM0bܾA1uB?*v 8nG?/ {/anp7 wsV>fマݽ~]U\$.-0a #F3< A8GA匤Ѷ_!8^OSzz)=Gq85;2VHe*T2`lΖ7q'XG[uUWhaqG}zv vlU4.@]Oswz$vbHrIOLٵˆ)V-ךcCi~їhcB>IcW'SV)-ښiOM[6+x{I'Lu-2e .$Z%/Lb{ mPxʏwH!9qB_B:D^ɭK:vy)9̙G#D-Ai8qDkA_onflh<Ȥmbfd)!ܣonqϵM'"Аéizt6vd߾PT;T~rM3>=ISil]fq*2,e  UFF_Ѻt6:iFyc*b Z"9wfYlwSipQFO]1MS Ps|,(mmk'`i-RY7,۹-{{bF,Lje;Hi$~֭ "&&Bq~`o_CgdEAtX>\[([cډKA] ׺=wo~: s#Fg?w \GO^5=U],n.Jh+ ĔP[N4EIfeMqߌB_'Σ;LiZe>rBc&8ⴸU/ךݖsg} cɜ9ʏ_ȴa<#0k׶:t®|9鸂 7znWT]E{x. weĥrɎ* *&? t)~2"Z{ۋbl7p2q&={J|yq)I6W*Q3g'?ZTǹS.1H~ ut-cbǐ {Sbhߑc߼մROA/̣#8O*^N]-M=cgmt涒 G$"PGMw\5 0͑3ۜUh]s-N BVB\'Z>br*ch<=j];\a~vFJʣ'ս@xlJ&K - ڑNU Ctę$8ǔ*~_ߣxn.spS/Ћ_>NrH{I<*qBcI% y GCtsreagfZ?>ݎ:uΆzkGu: 0r6# v]KFq^8Q$IW;[q8?{%,ܮuVw%F~ʴU5&[ncoQM-ͻ1tSh:={b{&hZ~]K}G4\-( n jr*jLoqky4LySCJ\\ IXaC=fmx[(Yzy<&o6UTa@dö∶unHLe—j-B$_v wO8(mPUF6sg<Ԙ ߽P2F|>K\6 zp>x21frnQAillns@uKX eqO}=?q]=3VSPŕE AC=ʮJDd`G">1ֽs>ka=K(9=LL`xc .u$ "trݔ 3I^\V['/s_6qDa#Lj)~B(ȲZ3ֶ:C{W'>Id,*B#vH,W 9 H=䯵1#\SuW#|>*4(Ŵ~^m+(.NBRw b]%˰S|'q}X$6cDRt 6AbR ў Ϡ?(mPOl!U6ԃ'U(ؽ;>6MHe,}Rl c#_bFxxS+lCa|n70=}ɡz,pQ(wB {TVIh"0$R;-0Wn*'n`2*#8Ǹ "MB'hA7l^92Ĝ`\9tH` JF V#UD@ݕ6,F1ORH瀼2<+Ꮶ35iOgl CT6ddd{spl~?j 4|^}ۓ0s1BeC&=KD'tN}GhnI <Lن9׾| 4غXML;pxFn= qEcoֺՒOIoqʏ dThIr2ۻ[y].aV,TLig%PHC0S,cM~v|w4mWv71X/`}(,LvDNY[t>i{˕K%X7 H BܧG !'UeZm)3:c|}A.NkaA4aSIW%"Cg;[ [߰4r\犚+&(g(dhQDkum+m>^3'# (DUGz kX߽ɑnXd3J^ ľPG;#pOk::R\uQ^ψ*1j"&A[I"Ȳ6o nvW*Ȣuͬ0.As5_c^ Km.9&P" "YC'TdRvI5tx3P}sSh,_f9YZ#r 32ID]Y7i7Wt~i^?,[ЪgI"RނIT, kg$$`ZcF"5θP.%$IU$qn9!<”6އLL`>f) 9[vT9jRѪhz>=wY̻mH7#N+KhѱM t 㴹V cv B wR宛E7UFʛM 2*+*ۏʪd{|>IgUVK{HPJ\MdqԿu-PO,$6rCL%Tbr?_Z[_&AcW30Opr|ybp8r=p4Z5nmZ;"t7N%!3g%v' $RS&k+CYMAV*1Wb0HGԌZhZ6JL|z׭$)fߞOJVȈSbm- na{t$$ ȁP7%N?C ɵ9 x?Ln_1HE]MmWVLdz3珕Tdq )Z,'n"-q$`>—Gؔip29(OdA[c=F, 0*ܐH[ ؎37e$;&c̈́YC.CQBvN>9u4 銰_a_ c#?CxFo9(1MQmā^I33ިd-XFrR$N#khT\)'RObxtM&w`7*TWwwq L\G"QfWS_^HMlA_wQWq[ <xg9{ѨI:PŽ@6#>E zCqqh`d@|T!-v*X?z:Uw.͸"Q@d# 6<ߎ>ē[Gqi#983diבԺ,e xq,I[(_zP_4*G4W14"m{ ZZD3L!8ϋ$U#, sk &n'Mw88 krÓu<&ƽU =J2ԃZb iorq cRrC}6U}WGmF<ڙAr$sJ7/ȭ#&ő1}^ܳ IbS{9#{^{|h[e2H||ޮȴZ |2Q\Pxeq~g?M@FOoҦɠ<cikcn^8-V0]Ĭ{'g%hv8#G&$X Yx0>J$TǦr1>b^8f;Ȓ=T q s7d<(M|(?CZvoPv{L%⁢V!qz>ha2x+Qw&rceNxLTW$ gޚ]}xY)P S޴=RF߼xcP|?8lm9P=x-Mk^Kc#ӟ,Ρ=[u e!0cѯ zZ%*hk6@iOq2[u- ݏ&r]Jԕo_[^ sXi\#,ya1~sOUƿ,u[lھ i}uhtiuSL 9+%˟yr}}Y.NB0PX8^6Hq\\<;Pj8{f{KՊ0b  u[(KzoR֙ $9Y#tRb=>ʻh-̻RJ6{ccڍWx$%> |%4u-CTF =')<Ǵ~+T5g:K. 潊hև!v:f}ҧUm$졂,d~T:|tUnwXZu[#H]1VIcSoV֦E7H~Ml\Lv68{|Glx_y|4?k V:6\I2v0;gTӗz<}L3%Ql5˰%p> _xFJ Ք2[^Y Xb#oA9oOjlцNR./OP[kH3pJL$UIö+Vd|?|#i2Bmxw4QhUd~e5t="mPO>5 foR^Y]ִ;;4;J 5] 7yYvl g٦PЬ0+L:Q46ȃ`.[\{zqZРկmw9`%l3|ؿЖ-?-\ydd$0ؘa lmWAyqH{wsg3~JjMz8(dth* p}2 ܼ[ ' 1 ӵ Z, ~>J1c۳U]ĺԼ⿒ th..wr)؂*xXu,rbGl 5%:⠴:K^}RMx-p1"RuaKr{l]/NF&6V⭎GP4S%jM?c22ЇRQEt%U3p-m֭g2Xթm=WWBb3\|Oj(Xz=SVeT=ۼI[vI 89VL? :4{[{I*B)^T##iW.^ 3B!?"j,Y"<#YU(OWśL.?/n2c\崘wBYt/F\ȚJs,#\La}}o :~E[-4tA軨mb19LK)<(9`]gɢc+᪷?/J]ze{'Z-Iac=dC]}7 }9trkPFczJ(Hb6!w˷<$nAIl#챝\o&w-+r͏P»̤A^ڂkCE+WlUe%<\zT6=l8v{&z=YW'>&;6>"\5MUUcwToj$tq60 ΄.:#m=ΠZuq@!P8 D2OU nNapB qF$ '9K=B@YXFxS? q]ۤvtʎnAM$;t2NohN ފ~ZKpW f7|zT 3y13)K 4l& ӎXQJw}PLbH"yGfqNL +$&5Itڕvi4$*)4f',@Zk< `v=͘Sq7ڱ(Brlh _쓴ga"o8f2Ȑ>mWu)1O'ps-h-̋m K 8/>@cЬ6Ѝi+pEK.}d|J"َx䷯~!CjkO|vZ.w,p@LUv}yԍKEC}SFtY>pgI O`HXlezkEK\O$ h?ۿ1Y5:F _ 螗M&)?UxV|vv@#ErAP[doơ4&?Y4 )Pǔ~DWwy߻Bmw^Cv}){c-LMvV6VVL/6)m_r|ƦiS\_ 6)9BA3TOo 3wp*#lp Y6B 8AW{U)F$8RE"o_¡}vxϭga&)™yW|޵S!ԁ#o~|{y'v 14IL P~/["[u4o![9=d%yܾt%/m|]Z×%gP3չ_4izVq xDDmRp@?RO9G/yH"8%,j qvΨ4"3.d>B[$dIvmÛvb60Gqʎorw|B"˹ {rUfd.[1ܴ0$xE&lTc8O2%ʟ˽nk._,{;vޫ"_#tLMi䲻&Y1ߵ`^OCdT;gקդѬ5ak:InpI]C Z9y2PRKf:-(mMWJHam,v$m=QhY*W0Yec\VVhiO~Z4To* Y<_67)4HF:ANcgW͎TJ>cM\|Jꞩ{L[[hU .¹ p[IgRj C[i%aUMhqECY$v^pҰMjFWmN2o8y8ꞦtFOxBۨ1Q`{rsF[Lg]=ֳm 5X/&mEjN:7BŃrOZ8–ٕ6zQ9VMi#I&<0}܎pSb6ʣm˷=l-IaR$,$yą|?mC7_ l]X3GMBySaACnlyƦ/?*'&06N;'{T'J\.w?+녻RYKv3Hl]C|+ /M[s$ f9KwnG3ݮo^] Uk`*ꆾgǔJ&myZ^Rk{]Nb6!w$9®|u^d_d+ [fv-X:˭ݯZ,9 w6pr:Ԇ=]uV)n;.d ҡtvRM}OAQ5=S~!zYŸA#}+?"*5;u+]organize-3.3.0/docs/img/organize.pdf000066400000000000000000011320251472111340300173500ustar00rootroot00000000000000%PDF-1.5 % 1 0 obj <>/OCGs[7 0 R]>>/Pages 3 0 R/Type/Catalog>> endobj 2 0 obj <>stream application/pdf organize 2017-10-06T16:23:53+02:00 2017-10-06T16:23:53+02:00 2017-10-06T16:23:53+02:00 Adobe Illustrator CS6 (Macintosh) 256 56 JPEG /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgAOAEAAwER AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FUk87ebNP8 peVtR8wX5/cWMRdY60Mkp+GKJfd3IXFXln/ONXnfz/5xj8w6n5ivBc6ZHOiWSmNF4TOC8qIygHgi FNjXr88Ve3Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXyV/zkB55/NrS/zNksrW/1 DS9OQx/oOGyeSKK4UqhLUjIE7GQ0IatPs0xV9NeSLrXrvyho915gi9HWprSF9QjKhCJWQFuSDZWP Ur2O2Kp3irsVdirsVdirsVeK/k9+T/n/AMpefNZ1/wAw61Ff21/FIjGOSV3uZXkVlmmR1VVKqp7m laDbFXo/lLT/ADxaXmtP5m1S21G1nuy+ix28PpNDbU2Rz3Pam52ryNdlWR4qo3N5Z2qq11PHArEK rSuqAsegHIjfFXzH/wA5deaNdlvtO8upa3EGgwAXUt40brDc3LghVSQjiwhQ9j1Y16DFXtf5L+Tz 5T/LjR9LlTheyRfW78EUb17j94yt03QEJ/scVZviqF1TVNO0nTrjUtSuEtbG1Qy3FxKaKijuf898 VeCa9/zmJ5etbxotE0C41O2VmX6zcTrZ8gKUZEEdw1Dv9rifbwVZX+W3/OSHk3znqEWkXEMmi6xP RbeC4ZZIZnP+64phxq9egZFr232xV6B5z82ad5R8s3vmLUo5prKwCNNHbKrSkSSLEOIdo1+046ti rz3yp/zk35B8zeYrDQbKy1OC71CT0YJbmK3WIOQSORSeRt6U2U4ql35h/wDOUnlryvrc+jaVpr67 dWjmK8mWcW8COtQyI/CZnZW2Pwge+KvQ/wAtvPtt548oW/mSK0fT45mkR4JWDhWiPFyrgLyWo60H yxV5r5x/5yz8naPfS2WhafNrzwMUe5Eq21sxBofTkKys49+FD2NN8VYpc/8AOW/mzV2Sw8seUkXV J/ghRpZb92YkfYhhjt2JpXv/AEKr0r8gtf8AzM1bTtabz5Z3lvdfWllspLy2NrWN0o0UcZVKKhQE fD+11O+KoH8wv+cmfLfk/Xb3QRpF7e6pYnjMGMdvDyKh46OTI5VgwNeHTxxVimnf85l6ZJdKupeV pra1/altrtLiQbjpG8VuDtX9vFXu/lTzZoXmvQ4Nb0O5FzYXFQGoVZXXZkdTurKeo/hirDfzG/Pr yf5B12LRdYs9QuLqW3S6V7OOB4+Du6AEyTRNyrGe2Ksb83/85W+SdIgtv0NaTa3eXMEVw0QdYI4f WVX9KWWkv7xVb4lVWodia4qnH5S/85A6B+YGoSaQ1jJpGsrG00ds8gnjlRD8XpyhYyWUUJUoPatD irJ/zJ/M7Qfy+0q11PWbe6uILuf6tGtmkbuH4M9WEkkQpRPHFXnf/Q3/AOWn/Vs1n/kRa/8AZTir 17yt5isvMnl6w16xSWOz1GITwRzhVkCt2cKzrX5McVYz+Zv5xeWfy6fTk1u1vbg6mJTB9SSJ6ehw Dc/Vlhp/eClK4qwf/ob/APLT/q2az/yItf8AspxVkPkP/nIjyV518yQeX9KstShvbhJHSS6igSIC JC7VMc8jdBt8OKsj/MX80vKnkHTo7rW5ma4uKizsIAHnmK9eKkgBR3ZiB9OKvHV/5zMsPrnBvKso suRHrC9Uy8ex9L0Qtfb1PpxV7V5B/MXyt560g6loFwziIhLq1lASeB2FQsqAsN+xUlT2OxxViP8A zkrr3mbRfyyludAllt5JbuGC/uoCVkitZFfkwcbrykCJUfzYq8Z/5xY80eb5fzBbShd3F3o1xbTS 38MrvJHGyCqSjkTwYuQte9cVfV2savpujaXdarqc621hZxtNcTv0VFFfpJ6ADcnYYq+bvOn5dP8A nSZvO/krzE18FpA2i6kjQfVmRFJhidQUUnY0pQk1L4q82h8y/nD+V040nVIZ4dPbb9EapH9a0+VV 7Rhi0ZHvE4+eKvr38sfPEXnfyXYeYlg+qyXIdLi3rUJLE5Rwp7qStV9j44qynFXzv/zmH5ju7bRN B0CCQrBqMs1zdqNuQtggjU+I5SlqeIGKqX/OLv5aeUdS8mXmv63pVpqt3dXb28IvIUuEjhhVPspK GUMzs1SBWlMVeR/nx5V0/wAofmjfWmiL9Ts3WG9tIoyV9FpFDMEI+yBIpK06Dbtir650KLTfzB/K /Sx5gt/rdrrVhbS6hByeIPIAjtRo2RwPVSuxxV8UecYU8sfmZq8WghrJdG1WYaYEZnaL6vOfSozl mJXiOpOBXq2of84ta9D+Xltf2ccupedruWKSew9WGGG3gdWZ1rMY+cgPHkeXXoO5KvTH8s695U/5 xjvdFeI2+r2ul3LXcSsrsvrSvLOOcZdTSORuhxV4L/zjho/lHVvzKhtPM0UFxAbWVrC1uuLQy3fJ AiMj/C/7suQpruMVT7/nKbQvJmieaNIi8uWtrp941u7aja2KpEqUZfRZoo6KjEcuwJxV9DfkZq+t 6t+VWgX2tM8l/JFIhmlrzkjjmeOJ2J3JaNVPI9evfFWMfmV5p/5x80LzJc3vmiytdX8zFFS4thb/ AF2UCNPgV0k/cI3GgHIg9O2+KvDfze89/kz5n0S3j8o+WptE1qCdW9dbW1tIXgKsHRxbyvyPIqRV e3XFXpv/ADhvcyt5f8yWpY+lFdwSqtdg0kbKxA9xGMVYJ/zlz/5M6z/7ZMH/ACfnxV6V+VX5T+QZ vyVTUbzR7e91LVbGe4uL26jWWVHpIq+i7CsXEDbhQ13xV4Z/zj1JJH+cflsoxUmWdTTuGtpQR92K vszzj5E8qecrGGx8yWP1+1t5fXhj9WaHjJxK8qwvGx+Fj1OKvkD/AJyL8leWfJ/nu20vy7Z/UbGT Tobh4fUlmrK8sys3KZ5G6INq0xV9T/kl/wCSn8r/APMDH+s4qmHnP8tPJPnRrRvM2m/X2sRILU+t PDwEvHn/AHEkda8F64q+V/z8svyi8v348s+TNHRNWt3rqupC6u5lhI/490WSZ0L/AM5I+Hp1rxVe l/8AOM35M3OjJH5315Hh1C5iK6TZGqmOCVaGaUfzSKfhXsu53PwqvFfzv8xz+YvzZ1prqfha2V22 m27MCyxQ2rmIkBakgsGfbrXFXr2oa7/zibJ5Pm0G2ktFmFq0NvqI027F56oU8JTcfV/ULc992p26 bYq85/5xg8w3Ol/mtZWSOwtdYimtbmMfZJWNpo2I8Q8dK+5xV9ZfmD508ueUPLcuqeYUeXTZHW2e GOITGQyg/AUNFoQDXkaYq8SX/nJ3yBo8T2nkTyXIk9w20KxW9ijv2YpbCZn+7FUCfKX55fnLfQv5 q5eWfKiMHFqY2hBp3S2c+rI/g8p4jqvhir6I8p+VNF8qaBaaFo0Po2NotF5ULux3aSRgBydjuT/D FWD/AJ8fmZo3krQrCHUNIg146tO0baXclRG1vEtZXPJJBUFlUfD39sVZp5L0bTNI8s2Nrpumfoa2 kT6wdMDF/QkuP3skdST9l3I228NsVTvFXz5/zmB5Yu7zy/ovmG3jZ4tLmlt7wqK8Y7oJwdvYPFx+ bYqlP/OM/wCbnk7QvKd55e8w6jHps8F09zayz1EckUqqCoYAjkrqdj2O3Q4q8q/OrzZaeefzPvL3 Q1a5tHMNjpxVSHm9MBOSr9r45CePelMVfYGjyaX+Xn5Z6aNfuRbWeh2NtDf3IR5QslEjYhYhIxBl bsDir4m85a1pmo/mXq+tWc3q6ZdatNdQXHF15QvOXVuDAOPh3oRXFX3B5M/MvyT50a7XyzqX19rE Rm6Hozw8BLy4f38cda8G6YqyScxCCQyryi4t6ikcqrTcU77dsVfn7GPJfmTzvfS3l0nk3y9dSSy2 5itpr1YB1jj9JG5/F3oeIPQBaABXoXkvyH/zjqdUj/S/n5tVVW5i2a0uNLgcAgcZJZg23+q6nCr6 x5W8PlwnQRE0EVof0YttxaKiR/uRGFqpXYUpir4E8mS6BdeedOm84yu+jzXfqavKxcswYlmLlPjo z/bI3pXAr138/Nf/ACRbylaaT5JtdNbV2uI5GutOtY0ZLeNXUiS4CKxLGm3Ik9T2wqnX/OG2p2Kp 5l0xplF9Iba5jgP2miQOjuPZWdQfmMVYn/zlz/5M6z/7ZMH/ACfnxV7x+U//AJIfSf8Atlzf8zMV fLn/ADj9/wCTi8tf8Zpf+oeTFX2b5x8/eU/JtnBeeZL76hbXUhhgf0ppuThS1KQpIRsO+KvkH/nI vzr5Z84ee7bVPLt59esY9Oht3m9OWGkqSzMy8Zkjbo43pTFXuH5P/nV+Wdl5L8s+WrnWfT1tYYbN rT6tdn9+7cVT1FiMe5Yb8qYqiP8AnIn85pvJenR6BorFfMeqQ+oLmm1tbMzR+qp7yMyME8KE+FVX z5+TP/KsofMTa3+YGqrFDZMJLTTngubj6xNXl6kpiikXgp/ZJ+I9duqr6y8s/nX+WXmfWYNF0PWf rmpXAcwwfVbuOojQu3xyxIgoqnqcVfJX59eVrvy7+aetevCfqupXDalaOQQkiXLGR6Gv7MhZDTwx V6zoWn/84g6npEF9NBDp80iAz2VzeaiksT0+JCPW+Kh6Fag4qyn8o9F/5x41bzFPqPkawZNX0N+U c0s16CUlQp60cVxK3JPiK/ElQew2OKvWPMfl3R/Mei3Wi6xbi50+8ThNEdj4hlI3VlO4I6HFXzFd 6B+ZX5Ba5darosS635Ru6h5ZE5Ko/Y+scPjhdf5x8DfgFXof5Xfn55W1PR7vVfN/mS3s9Zdi0mll JILe2gQkRpbhufrO1SzMGZu1KAYqyvyF+Zc3nvWb280W2a38maYrQnUbleMl5dGhpEp/u4ok+Ji3 xHkvTcYq8ft5B+cn/OQSXMX77yn5YCsr9Ukit3qvsfrFwfpjHtir6hxV2KqF9Y2WoWU9jfQJc2dy jRXFvKoZHRhRlZTsQRirxTW/+cRvIF7eNcabf32lxOSTaKyTxLWlBGZF9QD/AFmbFWT/AJef84/e QfJN6mp28c2pavH/AHV9esrekSACYY0VEX2YgsP5sVZj5z8p6d5u8s3vl3UpJobK/CLNJbMqygRy LKOJdZF+0g6riryf/oUD8tP+rnrP/I+1/wCybFWcfll+Tvln8un1F9Eur24OpiIT/XXienocyvD0 ooaf3hrWuKpj+ZPn228i+WX8wXVlLfW8c0cMkUJVWX1SQGq21OVB9OKvnzSNG/5xv/Ma6vdTubqb yTeLKWlsnvbW2im5gH1IxcJIg+KvwoRTwpTFWMfm9+Xf5P8AlvQre78n+bP0vqck4R7H6za3tYiC S/K1RPT4n+br2xV6h/zh5qesT+W9esLhmfS7K5hNhyJIWSZXadFr0Hwo1PFj44qyLzt/zjB5D8y6 vNq1vPc6PdXLmS6jtfTMDud2cRuvwsx3NGp7YqjPKH/ONf5Z+XoZxcWsmt3NzE8Mk9+wbiki8XES RhFSo/a3cdmxVN/y3/Jjyt+X+qapf6LLcS/pJY4xHcsrmFEZmKRsqoSrFl+1U/CN8VQn5jfkL5P8 /a7FrWsXmoW91FbpaqlnJAkfBHdwSJIZW5VkPfFWXeXvKGm6F5St/K9pLNJp9tbtaxyyshmKPWpL KqrX4v5cVYD5O/5xq8i+VPMtj5h06+1SW9sGZ4Y7iW3aIlkZDyCQRt0bs2Ksp/Mr8rtA/MLTrTT9 auLu3hs5jPE1m8aMWKlKMZI5RSh8MVee/wDQoH5af9XPWf8Akfa/9k2KovSf+cUvy70vVbLU7fUd XaexniuYlkmtiheFw6hgLZTSq70OKsg/Mj8iPKH5ga5BrOs3moW91b2qWaJZyQpGY0kkkBIkhlPK sp74qxT/AKFA/LT/AKues/8AI+1/7JsVZH5A/wCce/JXkfzFHr+lXmo3F5HHJEiXckDxgSijGkcM TVp/lYqy3zt+X/lTzrpq6f5hsluY4yWt5lJSaFj1Mci/Ete46HuDiryOT/nDvyabgtHrmorb1FI2 EDPx7jmEUV9+OKvSvy+/KDyP5EV5NEtGa/kX05dSumEtyyGhK8gFVVJUEhFAOKs0xVp0SRGjkUOj gq6MKgg7EEHFXlHnL/nGf8tPMUklza28mh3r7mTTyqwlv8qBg0Y/2HHFXn9x/wA4y/mlpdpNpvlr zsP0VMrpJaSy3dlEySfbVoofrCHlXfxxV7B+UP5X2H5e+Vxpkci3OpXL+tqd6oKiSToqqDuEjXZf pPfFWcYq7FXYq7FXYq7FXYq7FUDruh6Vr2kXWkatbrdadep6dxA9aMtajcUIIIBBG4O+KvBtZ/5w 40Ge4L6P5iubCAsT6NzbpdkA0oFZXtum/Wv9VWtI/wCcONChn5av5kub2EEER2tulqaCtQWd7nrt 0GKvcvK/lXQfK2jQaNodotpYQVKxrUszH7TuxqzM3cnFWL3flL8wYGEmia96EinUvhu55rqNhczq 9nyWdJj+5iSnwkU3AqCcVT240rzRdeTJtNm1ER+YJoXjOowN6QDsxIKssYK/BtUJXw33xVL/APDf myK7dor9pbaG/tLizE17Pza2igWGeKULEF+Irz4nkGY1NDviqJ0/TvPyeU7i1vtUt5PMr1+r38SK sKbL+yYqdm6o2KpGmifnWQI5NfsVURSL6yKhkMvqK0b0NnwHwclYfSPZVD6xH+cUGsWKidJ9NuNQ jLmxEbNDbLIvITepFDVWTrv4/FsoZVUXQPzxDW7HzJYkLazJcqY46Ncsrei60tFNFYiu/b7LdCqj 30H80HR+evxMQ1k8SIIogfRMBuVLi1JAm4zdmG67AVAVTjzZaed7lrIeWry2skUv9ee4+IkHiECK YZa0+I9V7eOyqUpo35n7zvq9v9YkhkjeJHX0o2+uvLEyVtDVvqjLFyK7MKlXxVAw6L+d0cJ569p8 87FPtKqIoWpanG0qeWw3+Yp0xVHTaD+YyW0K22rRSXcFzqTrcTzEK0NykosucSW1GMDOhKE0+Hqa gKqj9B03z7BqyzaxqsNzpvpyq1qipy5F6xNyWCEsyp8LGqqevEYqoXej/mGuqXMlhq8QsJtQiuIk mKlks/RRZYOP1Z/92KxoHqwI+NCDyVd5f0Tz3BrNjf65qcV5HHbXNteQxSOiFpRaPDKsKxRxMySQ 3FCVDKkgXk1DVVl+KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2 KuxV2KuxV2KuxV2KuxV//9k= uuid:1e5d5c86-71a7-ba4b-a79c-97169be62009 xmp.did:0380117407206811822AF33CB0CB0D8F uuid:5D20892493BFDB11914A8590D31508C8 proof:pdf xmp.iid:0280117407206811822AF33CB0CB0D8F xmp.did:0280117407206811822AF33CB0CB0D8F uuid:5D20892493BFDB11914A8590D31508C8 proof:pdf saved xmp.iid:0180117407206811822AF33CB0CB0D8F 2017-10-06T16:19:14+02:00 Adobe Illustrator CS6 (Macintosh) / saved xmp.iid:0380117407206811822AF33CB0CB0D8F 2017-10-06T16:23:51+02:00 Adobe Illustrator CS6 (Macintosh) / Print False False 1 219.780556 59.266667 Millimeters FiraCode-Medium Fira Code Medium Open Type Version 1.204 False FiraCode-Medium.ttf FiraCode-Bold Fira Code Bold Open Type Version 1.204 False FiraCode-Bold.ttf Black Standard-Farbfeldgruppe 0 Weiß CMYK PROCESS 0.000000 0.000000 0.000000 0.000000 Schwarz CMYK PROCESS 0.000000 0.000000 0.000000 100.000000 CMYK Rot CMYK PROCESS 0.000000 100.000000 100.000000 0.000000 CMYK Gelb CMYK PROCESS 0.000000 0.000000 100.000000 0.000000 CMYK Grün CMYK PROCESS 100.000000 0.000000 100.000000 0.000000 CMYK Cyan CMYK PROCESS 100.000000 0.000000 0.000000 0.000000 CMYK Blau CMYK PROCESS 100.000000 100.000000 0.000000 0.000000 CMYK Magenta CMYK PROCESS 0.000000 100.000000 0.000000 0.000000 C=15 M=100 Y=90 K=10 CMYK PROCESS 14.999998 100.000000 90.000000 10.000002 C=0 M=90 Y=85 K=0 CMYK PROCESS 0.000000 90.000000 85.000000 0.000000 C=0 M=80 Y=95 K=0 CMYK PROCESS 0.000000 80.000000 95.000000 0.000000 C=0 M=50 Y=100 K=0 CMYK PROCESS 0.000000 50.000000 100.000000 0.000000 C=0 M=35 Y=85 K=0 CMYK PROCESS 0.000000 35.000004 85.000000 0.000000 C=5 M=0 Y=90 K=0 CMYK PROCESS 5.000001 0.000000 90.000000 0.000000 C=20 M=0 Y=100 K=0 CMYK PROCESS 19.999998 0.000000 100.000000 0.000000 C=50 M=0 Y=100 K=0 CMYK PROCESS 50.000000 0.000000 100.000000 0.000000 C=75 M=0 Y=100 K=0 CMYK PROCESS 75.000000 0.000000 100.000000 0.000000 C=85 M=10 Y=100 K=10 CMYK PROCESS 85.000000 10.000002 100.000000 10.000002 C=90 M=30 Y=95 K=30 CMYK PROCESS 90.000000 30.000002 95.000000 30.000002 C=75 M=0 Y=75 K=0 CMYK PROCESS 75.000000 0.000000 75.000000 0.000000 C=80 M=10 Y=45 K=0 CMYK PROCESS 80.000000 10.000002 45.000000 0.000000 C=70 M=15 Y=0 K=0 CMYK PROCESS 70.000000 14.999998 0.000000 0.000000 C=85 M=50 Y=0 K=0 CMYK PROCESS 85.000000 50.000000 0.000000 0.000000 C=100 M=95 Y=5 K=0 CMYK PROCESS 100.000000 95.000000 5.000001 0.000000 C=100 M=100 Y=25 K=25 CMYK PROCESS 100.000000 100.000000 25.000000 25.000000 C=75 M=100 Y=0 K=0 CMYK PROCESS 75.000000 100.000000 0.000000 0.000000 C=50 M=100 Y=0 K=0 CMYK PROCESS 50.000000 100.000000 0.000000 0.000000 C=35 M=100 Y=35 K=10 CMYK PROCESS 35.000004 100.000000 35.000004 10.000002 C=10 M=100 Y=50 K=0 CMYK PROCESS 10.000002 100.000000 50.000000 0.000000 C=0 M=95 Y=20 K=0 CMYK PROCESS 0.000000 95.000000 19.999998 0.000000 C=25 M=25 Y=40 K=0 CMYK PROCESS 25.000000 25.000000 39.999996 0.000000 C=40 M=45 Y=50 K=5 CMYK PROCESS 39.999996 45.000000 50.000000 5.000001 C=50 M=50 Y=60 K=25 CMYK PROCESS 50.000000 50.000000 60.000004 25.000000 C=55 M=60 Y=65 K=40 CMYK PROCESS 55.000000 60.000004 65.000000 39.999996 C=25 M=40 Y=65 K=0 CMYK PROCESS 25.000000 39.999996 65.000000 0.000000 C=30 M=50 Y=75 K=10 CMYK PROCESS 30.000002 50.000000 75.000000 10.000002 C=35 M=60 Y=80 K=25 CMYK PROCESS 35.000004 60.000004 80.000000 25.000000 C=40 M=65 Y=90 K=35 CMYK PROCESS 39.999996 65.000000 90.000000 35.000004 C=40 M=70 Y=100 K=50 CMYK PROCESS 39.999996 70.000000 100.000000 50.000000 C=50 M=70 Y=80 K=70 CMYK PROCESS 50.000000 70.000000 80.000000 70.000000 Graustufen 1 C=0 M=0 Y=0 K=100 CMYK PROCESS 0.000000 0.000000 0.000000 100.000000 C=0 M=0 Y=0 K=90 CMYK PROCESS 0.000000 0.000000 0.000000 89.999405 C=0 M=0 Y=0 K=80 CMYK PROCESS 0.000000 0.000000 0.000000 79.998795 C=0 M=0 Y=0 K=70 CMYK PROCESS 0.000000 0.000000 0.000000 69.999702 C=0 M=0 Y=0 K=60 CMYK PROCESS 0.000000 0.000000 0.000000 59.999104 C=0 M=0 Y=0 K=50 CMYK PROCESS 0.000000 0.000000 0.000000 50.000000 C=0 M=0 Y=0 K=40 CMYK PROCESS 0.000000 0.000000 0.000000 39.999401 C=0 M=0 Y=0 K=30 CMYK PROCESS 0.000000 0.000000 0.000000 29.998802 C=0 M=0 Y=0 K=20 CMYK PROCESS 0.000000 0.000000 0.000000 19.999701 C=0 M=0 Y=0 K=10 CMYK PROCESS 0.000000 0.000000 0.000000 9.999103 C=0 M=0 Y=0 K=5 CMYK PROCESS 0.000000 0.000000 0.000000 4.998803 Strahlende Farben 1 C=0 M=100 Y=100 K=0 CMYK PROCESS 0.000000 100.000000 100.000000 0.000000 C=0 M=75 Y=100 K=0 CMYK PROCESS 0.000000 75.000000 100.000000 0.000000 C=0 M=10 Y=95 K=0 CMYK PROCESS 0.000000 10.000002 95.000000 0.000000 C=85 M=10 Y=100 K=0 CMYK PROCESS 85.000000 10.000002 100.000000 0.000000 C=100 M=90 Y=0 K=0 CMYK PROCESS 100.000000 90.000000 0.000000 0.000000 C=60 M=90 Y=0 K=0 CMYK PROCESS 60.000004 90.000000 0.003099 0.003099 Adobe PDF library 10.01 endstream endobj 3 0 obj <> endobj 9 0 obj <>/Resources<>/Font<>/ProcSet[/PDF/Text]/Properties<>>>/Thumb 13 0 R/TrimBox[0.0 0.0 623.0 168.0]/Type/Page>> endobj 10 0 obj <>stream HlK\E +2 uzo"V!Zb= 03" n h{\/߽Njo^/>ˇO#Ogǯ\~gKqy*nxLץ9:JQ/i~XNwjEu6yגcfǵӪCknK>]yi.vBN9 kx͗pywy4u\xr7ΫMލfW \8;d ~"2i5ԫ.6 "2~#PN>S ^@ ӆLtZhwV$גS )X{+pOzl,t DONHB\–@!%rA4 {(%\/ו80P+0g7"ȵWH`!II5"ܤЯPunW*Qe;ks5ҊP" թTsXOׂOK־>-ED0ށWue|"j Uʑ:Ȭ=\mxjI@:DRJ.IyM Psjp+U=mWP ʾk.UQ`V6m! ,-v z?Z|t`&l61u;8Ὕ$u1UPh}'6 lP!% FhrLXM!JSpu`˰`SNAfraœ6BL2_S] ,,~Vߑ[ l.}q;sF͏1wszmA1tIRnLr}/2T^)'9ڼZ)@\uG-yobm7X?.W[4D+ (KHR&΢fID@ \m$su% pBAj {vv'ZI,(1TcGl DjRddXqQmW!NexiJfsXƗ %U6v|i*k!o4}v"N KT.hBBFw5/K4KHOAΤ, UT IFtVSUPDiStӠ{d5 u^VxrJM( جaUthjXk}ۥ/g>M:ϗgw~L%is}5W۾ZFΝߋj%EW'l]}{wypxqǧOOw[yͤ endstream endobj 13 0 obj <>stream 8;Z\r>n4ap%"iU=Sp^REpc9WaL)&4;e[DqVU'3lBo*OuEmfN9XQA?[$Z`fD "CkuFC4bfNCq;Li2JOSARUP9%F=Lq3\`2uTXe3BSB4TDZc9ONb"#jhHa_WXN3>%fF #JH7&eXT`G/b_r^E@VIP%F]I-7Bob8h&#k)TAqd/FM!iA~> endstream endobj 14 0 obj [/Indexed/DeviceRGB 255 15 0 R] endobj 15 0 obj <>stream 8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0 b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup` E1r!/,*0[*9.aFIR2&b-C#soRZ7Dl%MLY\.?d>Mn 6%Q2oYfNRF$$+ON<+]RUJmC0InDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j$XKrcYp0n+Xl_nU*O( l[$6Nn+Z_Nq0]s7hs]`XX1nZ8&94a\~> endstream endobj 7 0 obj <> endobj 16 0 obj [/View/Design] endobj 17 0 obj <>>> endobj 5 0 obj <> endobj 6 0 obj <> endobj 19 0 obj <> endobj 20 0 obj <>stream HԖ{|NGgfy߈H"97l ¶. AHh4B5"%ںVj,ooYԥZGw<3<3sΜy #WK?zMd)iY7&m\0'aY3 9qX=jGTC@T>̃͠22'%Q 4v䨴TB~ Yɾ')4^?53]i:y[5jlv 0(ݟ5&=%KoKn܅Jc X2Ez`pּܝf3vk:&vꀽŏX%u1?4}inݜ@ҏZ>4^Oi/ Brn5?.FӂsO|&g|>bdbX ^ϊ\1S$1]޼\> P)k S^$ 'LbL:A :]-4~v-Hki FZc-P µE UrEJ- gX  n/QO :W:D-EFǦ#MW&2JFV Y$9@-!r"gr\+Oe˛|uo=HoU1zIgS&}^X-V+D\rE+jV_kCk5a}՚jMd~Ȧ"izci>cq8j9;qӸcܷv[mmDԺf*^qǵ|Eޏ,Eꕒzg8z[z"&ӗ1 z.p#bE[Qq[B l*cdk^v K2OBLɍr,iY)myO7z I{kDl=ϣޮkg)zO>S/:ԣi1ژf,1O_P8`1yWc%Q6Rbri.?gLqR9Lrws&RGs㑳nss;:Rgk̙\ծBWk#nLGXH$Ku$9;^WxiObϤr}=>>=# $t{_{=mo^܋ҧ)nbwPS_ I."EqF8uE+_I_[\{vRqovuSL :`)fğ6rMy/yY.y֋2OwG<˫7g ͋/O D1H]ԂUJQ$WpuFlL`/f+xȿP1(6CA+PRk!!^$>7MFb2ph9xYH4[F,s1MUr)"i m77;CC X,E(V# kb4:X'hMg> ~h-h=*V'JGE5 c?@)Ŀw8.8c荳x '3n c x. '"Rq)7&qCqcFOSYxqEb /LW1M_}q F:hj\>RИXKf7V>PMbhUKtc 7Y?vueXwփYO֟%k:\2;ľevc VNR=vUs<*ׁ\1= `83 iΙ\7/⽢@MF \r^* ,wbu".||;}^z;N'c>7gtStRQ ]qtz]t&%_?R! I4Y0i MihAC6@d"=43i Z5,xi耎Ȁ ?:6d 'nGowaCEa0^*[5XuPx [6c'{VMqq =!C' ß1A#F8p1}SN˔_S&7p7g1'1_`"$|0NQuA1F)2&LLYJ0 0bc.`6.b2 *"]\b&$q1'K(KQC<@x5G()Rdƨxr;nq lq[nɜĉܘS97܈[snnũܞ}ܒ's9t3yQ< ͅVP*ּrGT/`8}b}~Cr2tYTL"NDǖWM(\qve3;*\܎JWʖvtǎƸmv+;پ+3YwWo>3;&Č3ėN00sw`!WHK)6]`.HVZmYVZJ>JǾR_Zuj~gl@ժR99e*տk1x^LZ,ް hM}a>#Gh>c8죱ј}4RzIi:kz]+PRNZiT `JAAYp'<` ?Vb <3/gSz^Ov ߷!ؐ?H=q9Eeu,i5mZvN3';_ל4cXNvT'g΃IP"=|~h(?7z49e~:V[Wk‡mnw[>it5pO)Ζ|mm>wv~@33H_$hu{}f.?gjAYߊE|1j,ds E2¬P)vk G4o{l8B al,pp"SΦ ȩpv2RQU1p"5k2j^ሜJO!<~_ t>78roRwگGG&өKڜX4v~l֓ѥ)E7sSR1X0l)9TE$E*O**k`EH@.;T6tcET-:Y#Wg.r>W.JGǃ=n3[aߟOS\ɸ|M7RԪb͐¹b:Wձ : 1*EkU #^ՙd5ӟcD9'Mfvn$>?p"_9]^+!r4ff@M|vkT-24͜:xMࢉo˥2M_PV ;^bUP7wƪ)& o,4BB[~_1V<(iv q;S-"=;x0bC-KCVdaLk Y0__|lM'Ŧa7#Pֳjpvwv;z|Y,Ƽ %f3Y> (*U}]6O3ע c4x]o\6З?ر˚]3-"s^Y SF2UxOG{+-8mrPޜ?Qs}}z$ѧv'&!>籅~eA&O6hBU~V>fC%-CVMiy.eQäTdt>*:Y_WO,@6R_d3%+(q =(MP^2;iUŪPbD7Ɓ 1.WSk)ÿ(^j G/ƙI߾bTjf'_ZD-Ʈ 31݂;MncQ;M"Vi,TzvkUWyH2YףH=>H.Tz3S{2 !̂,e)89 j9ndiK%i>AN [x#GJ+Dûw?:~qy20bs8Sl[i/ਯ*I6 $ RXyAC yZH @Yl V8 cǨZJh;Sꌭ-3v֪Ŷ:Tcf?w/nf>s=sν?*DTUlٙc3N"pN eR+x= 6q:;EfZ_Ұ's yo<w<| x*]1 EnIڈ;JgPjm@i&H̹/ș^59urSQMMy~ρ{3j<[2Q*ZRHK> "2ˣ{%=i [w PXyYC$KWsfĀQ#%B@.d0QƎE0H >8La!}f9q(&rbFA(*ra S|i9}Q< *mF2E -?sb6.˝wn',__W:n4*/:X{}c+ikl/ ly"/X,OWHM鱬|=Mt/927W ]kׂ)Ut$Zl?ae)zN8ll;Muhk 0Bt),P<3\}^ʞ,CwS #.x.wlL̓wk{J]bKOȜv}=<SqR93}A?_Ol<+csvs{E $kx-;ujɬ}: y{A/gld> #{-^:M{(8bM{b`π̛bqY1O}sĸ`& I>^~9"1!K]qWjdV BcVx;_K_J9w;e3UfF0zSzpgȺ^o9>{S|q)G?>pߓ}9Zה:uv]ogU'_.v o]՟'Yc]Oݏmuٺ\'zmpc5œO~/37\*jE,=.ߣX`;wmWL&>O)U+A 7٣=;쒔OBmJǛ 2^mqS8cd ~$|ʹ/ ?IRЗ)0gNNDׅq>|g]!zq-~Gt $-Ɲ徃̈́?zw{ Ui䁙IK< >Ww-5iIq'[>=m~"uQWe-[~' Aw=J37d}:a?h!=FoKᝏc7`@t;NWRs 18 A $H A $H A n"o4}>M,JVJ#VʿlI'#_&y/+$J%ʣ$@i1}U*7%hg䓨3Q)y/;%JmoT:uuOo_lv5:CО5 ;B-j[[Ў?lnim7ip۶fU# MUGF]hCnZQ|e=TK*rظ ڛ[ފ\vvj@~+r 97Jn6D;5뺬6Ҹu!ڂدV=Yk[tZ;=£ΆAX?Y.[v^lx> endobj 21 0 obj <>stream HԖ{|NGgfy_"7ytiڊK-Yu)%ْ RwvRڮƢAHě[=.u'}7X{>gfgfΙ9@Lp"?A~e_=*mtꤑ3tcz-75^ 3SGMh|t 6JJj̤!c%%"r:SM~)i,_Z'S֕?`i&|_C+i^`n46 h"lCkS\(wM /|rJz>r?}_[;'MG~ܥH9^&u%j~)x9Y>g|y._OJ1E/&j>u[ :3z` a0! )tL4q~]-N!r ˱[%`/ *Qq7p̯I7c1bb OJnF|؅PR):g7qQ]/j~B]ZuNׂ$%ib&ybJzc)x?#2D$f,QWxRыq ^#|EhxQOxW?%"@0A" -tDO!}A*kAZc-T5iZ@x/u'tLhGOD,UjXkŊng\p3}y o£yGޅO,υIRkH"I\uIn-=ꥑzX%|#ƫiE-~SBl(uTF2FvK)2S"N S7`u@\])إ*^@bt2yqFPЇ8h%Jb-2!q#q-&qMx\&Z'^.!bb"fYJ>BV"Zb"P WM93 _Uac+Z[Dx_ID"w'{`:ſ?w88C膣3x'n c ށq#ppɸ xqwq Tcj0CQ1Jd)^ԅ/(q Qi>耧#Zj5)C':̪{}6`7);=X,eXγn;zY/֛ d ʮ1!vaG1veORvaeeg«<" ^p$=NAL4L.;^JCKsAȍ 7偺. b8Nd||;}^z;E􉾁?%/MtTDt 4A&%LF J**h`aTF>@l܉AD'̤~![8X.^qIwpG;lD pCG ß0+F=}QS0tF%^ 2a>x| 8&KDwDq+J(9Lטo0 ut\ |٨\\U,5:Pź %ED='&{H^bG&RQ,%Aj(SVP2xRR5ŏ~Li 5OxZIg?6݂_R[,&⼸f)xFϊxIV9r8#aqDc8!N;b/Xɘ\+ QVz6r +rKəl9Z^&s;:?w[V'8G!q|:_ až2}+ikĸ(`#Ǟ:ܧHX  ZV :ȑH'Z}ױT}_ₙ!e/ӈQEEe ~g[ (TvqM"96+`(7 [څ;b*|Q7/xSey7EsHt^2< 퇓ٵU& ldx*юTI4ǯlmhYQp;]7|G%K=J4}UR0bUD,AvpL8Ubo_y#y֧1#9X%'Y}F#Q?t&Dz~8悧 Tfx}zڍnܝ_WHrZ[?U7I`0R}2WUy3FU/`T>|>}VDo)%,*"&fZiajcKfv L8laǤ+[1ve+;fc\ڎ5pe;Еm ܕ. bJJpwͲز6"Y-Nxg˖c+ǎ!;I0d,,c&eTQ2N8p9#3";N(\"HT?T#tUW aF 4:A//؇ᨂ}8v}8>`!HěiUobᡄҋ`"z4G /6bG|%ÃC$*@#CzEq≊%UhQylIc/îFr)p7 wLa=DhV"|@LG x 9`)]oI@5 QDiqT33g"(VLk8grs&BBt E/smr}|<\ M? ^F7 ޮ5Zi@9*,z_3 f@_}* 7 MW8Dm" ow߷ۀH~VV5  #;an]{8JDrcZۈ@ GU:,Hx8Lm>T*T K'.CVr,-O7ʿ57mj[#t"O;m϶gZ_AZ"5Dgt6Do $D~9ܔ .JJ,0HlN Z'(Qq0& r2$R!=9= v0Qm]ͿidIdneE; r0r+`H6]H%U7N@J;ZK™<ĥG8jWK>(Ξ,_n˳/bC i'h%ŪA}QXGmY ˁa6C{\d҆Ih}prD(wnw{`vh?f뗕DP}rerr,9rklˣB@R5+,o.Z46~cMdLk } !3 $D(r$m)bQJDQp"$S+NlZK~D^ 2<<<9 =5X:TVK5Lm+%g$@ŵQz21zQgq;{%=ϦBŅx$9<\s8;Z<1E֓V<~1Lxf#>??>3r8 PuϢ29B qsYp)x) C&.'dҘpb< .[sk#(\]gq٩H /)]L&{{SVyOMCD0WfslBgv=;EwnH 53g.s X6нES$0K C~aWjUgOy9m [vC\|h>Z̕oB!#1KnN(=m `pmjZ[I)Crn/'- "DdK&jMp8ll[QS2ܲf򈳆sIN6'kJ~/Rµ ~:~jdvb!oiupm"1e!}qIg&#]2ftɔ  f![MŗP^N9Z7?_L#.?@гgGҟ@|jc?}Zn /f~Z~ ? C>['ϲ9'x <<_Q_U?{,!$BI !$ic)`^D%4#K5!IB-8*80c;>ƎHjRlPԱ>XR6=~MB)ꌿ|r_=3<{I?GI’NR6cN^l;֓B^技5, _r"DRzqpl*}@`Cm4eUlwOAEF5sp+0Eֽ_`hKk&K6,y\lV궪H;I0!3zeŭ.T=]rCs{kט= ۼ\d[|܋RF"X }V̺t~2L:iw|@R7ySgn>T'깸FdK]u;WpֱLeozr9R#ɚbV.R8ُ?ԺYƸZΐg90K=n׍FXة#AtvDJ,)~MP\_#a/۰["s͖rP^u]a~׽Mhg/Du9#g/2ԝlfZg^$ʽ+/Q}'jxV%3Ѷ17ؖ2vD2>}bQDt6WKr/}.Kk|wוSVtRRw1S<wa3^jnVԙ3)g=ϬI25&wڸا=~6Y#>i `P@o3 K vٿS~Ԍi~%h:ܣR~Ҭ_??BgrFyy 8 ΂3w=9)da(sd۽ClZ3EMnZrŶK*=4z !wyɵ"˱6r'WM_!qoɉNahH%2[EhD/#Cd6ɶTyl=gƱ[=rM:= <*{r9c'C쿃_'!|=.oWC ]/'cB9}wNɸ2A1p/4hwRQ$)]n8uNiwމ:=|CܭIt.ΒxK%~_>N'%><S@%1gbXu~$8YL8ǸQs$۶fG~/ܾRo ҿ,e~~bsf;)6oB=޺rN;%uގλmmL^B3ۭަ;it7ʌux֠ߎ3=3Kԑ{˗B>NrdR:O_Z=e7> }9{?wL%OgM s)\f\GJ.θ9tɵW,S|e[9v -ߥZeuor;{YNq@o1wn{ԼuxYְPnH'x^>>4bqǾ,m%yA2uO$%.J\9c$CϚ|͂O,ȅH97k&8.f-2d@YeǭTDGtʿ `y4%G^!$_@N%JTE/I^Qq < H>zɇw)ޡخޑȱH]u#;Fb#֡x_|0~۷ԴƆMq!9>Ć#P{wt/tvi_?i Ƈ#Jj8 ^2G(B@uhqoʹ6Q#C*=nZAM# ]($͖sZ"A0s݈n*P~>&Zxԕ8z6)e}{[X pxh+a ]cTmABCQOx[x}:< UA]Ut% O(H3P75t]- jm892Vɥ}o8tWG'7n//(UlLQ`WZZ- endstream endobj 12 0 obj <> endobj 11 0 obj <> endobj 22 0 obj <> endobj 23 0 obj <>stream %!PS-Adobe-3.0 %%Creator: Adobe Illustrator(R) 16.0 %%AI8_CreatorVersion: 16.0.0 %%For: (Thomas Feldmann) () %%Title: (organize.svg) %%CreationDate: 06.10.17 16:23 %%Canvassize: 16383 %%BoundingBox: -11 -158 519 -40 %%HiResBoundingBox: -10.1353 -157.6074 518.6563 -40.9722 %%DocumentProcessColors: Black %AI5_FileFormat 12.0 %AI12_BuildNumber: 682 %AI3_ColorUsage: Color %AI7_ImageSettings: 0 %%CMYKProcessColor: 1 1 1 1 ([Passermarken]) %AI3_Cropmarks: -42 -174 581 -6 %AI3_TemplateBox: 298.5 -421.5 298.5 -421.5 %AI3_TileBox: -133.5 -369.5 649.5 189.5 %AI3_DocumentPreview: None %AI5_ArtSize: 14400 14400 %AI5_RulerUnits: 1 %AI9_ColorModel: 2 %AI5_ArtFlags: 0 0 0 1 0 0 1 0 0 %AI5_TargetResolution: 800 %AI5_NumLayers: 1 %AI9_OpenToView: -207.6665 164 1.5 1520 938 18 1 0 85 134 0 0 0 0 1 0 1 1 0 1 %AI5_OpenViewLayers: 7 %%PageOrigin:-8 -817 %AI7_GridSettings: 72 8 72 8 1 0 0.8 0.8 0.8 0.9 0.9 0.9 %AI9_Flatten: 1 %AI12_CMSettings: 00.MS %%EndComments endstream endobj 24 0 obj <>stream %%BoundingBox: -11 -158 519 -40 %%HiResBoundingBox: -10.1353 -157.6074 518.6563 -40.9722 %AI7_Thumbnail: 128 28 8 %%BeginData: 5056 Hex Bytes %0000330000660000990000CC0033000033330033660033990033CC0033FF %0066000066330066660066990066CC0066FF009900009933009966009999 %0099CC0099FF00CC0000CC3300CC6600CC9900CCCC00CCFF00FF3300FF66 %00FF9900FFCC3300003300333300663300993300CC3300FF333300333333 %3333663333993333CC3333FF3366003366333366663366993366CC3366FF %3399003399333399663399993399CC3399FF33CC0033CC3333CC6633CC99 %33CCCC33CCFF33FF0033FF3333FF6633FF9933FFCC33FFFF660000660033 %6600666600996600CC6600FF6633006633336633666633996633CC6633FF %6666006666336666666666996666CC6666FF669900669933669966669999 %6699CC6699FF66CC0066CC3366CC6666CC9966CCCC66CCFF66FF0066FF33 %66FF6666FF9966FFCC66FFFF9900009900339900669900999900CC9900FF %9933009933339933669933999933CC9933FF996600996633996666996699 %9966CC9966FF9999009999339999669999999999CC9999FF99CC0099CC33 %99CC6699CC9999CCCC99CCFF99FF0099FF3399FF6699FF9999FFCC99FFFF %CC0000CC0033CC0066CC0099CC00CCCC00FFCC3300CC3333CC3366CC3399 %CC33CCCC33FFCC6600CC6633CC6666CC6699CC66CCCC66FFCC9900CC9933 %CC9966CC9999CC99CCCC99FFCCCC00CCCC33CCCC66CCCC99CCCCCCCCCCFF %CCFF00CCFF33CCFF66CCFF99CCFFCCCCFFFFFF0033FF0066FF0099FF00CC %FF3300FF3333FF3366FF3399FF33CCFF33FFFF6600FF6633FF6666FF6699 %FF66CCFF66FFFF9900FF9933FF9966FF9999FF99CCFF99FFFFCC00FFCC33 %FFCC66FFCC99FFCCCCFFCCFFFFFF33FFFF66FFFF99FFFFCC110000001100 %000011111111220000002200000022222222440000004400000044444444 %550000005500000055555555770000007700000077777777880000008800 %000088888888AA000000AA000000AAAAAAAABB000000BB000000BBBBBBBB %DD000000DD000000DDDDDDDDEE000000EE000000EEEEEEEE0000000000FF %00FF0000FFFFFF0000FF00FFFFFF00FFFFFF %524C45FD15FFA8FD4BFFA87D7DFD30FF2727A8FD49FFA827F82752FD2EFF %7D27F8FD4AFFA8F827F852FD2DFFA827F852FD4BFF7DF827A8FD1BFF7DF8 %A8FD0FFF7DF827A8FD2BFFA8FD3EFFF82752FF5227FD0CFFF82727FD0DFF %A8FD1BFFA87DF852FD06FFA8FD0FFFCFFD21FFA8FD05FFA852FFFFA87DFD %0BFF5227F87DFD0BFFA85227F82752FD05FFFD047DA8FF7D27F852A8FFFF %FF7D5226272727F82727FFFFFF7D52FD042752FD05FFA87D52FFA8522627 %52FD04FFA8527D7D7D52A8FD05FFFD087DA7FD04FF7D52272727A8FD11FF %FD04A82727A8FD0AFF7DF827F827F827F8A8FFFFCF27F827F8A87D27F827 %F8A8FFFF2727F827F827F8527DA8FFFFA827F827F827F827F8A8FFFFFF7D %F8272727F827F82752FFFFFF5227F827F82727FD05FFF827F827F827F827 %27FFFFFF2727F827F827F852FD05FF7D5252FD07FF5227F8517DFFA8FD0A %FFA8F827F87D7D52F82727FFFFFF2727F8275252F827F827FFFF7D27F827 %52A8272727FD05FF52527D7D5227F82752FFFFFF7D27F827277D5227F827 %A8FFFF7DF8272727F852FD05FFFD0527F827F852FFFF5227F8527D7D2727 %F8A8FFFFFF7D27F82752FD05FFA852F827F827F87DA8FD09FF2727F852FF %FFA827F8277DFFFFFF7D27F827F852522727FFFF52F82752FFFFA8F82727 %FD09FF7D27F852FFFFFF7DF82727FFFFA8F827F8A8FD05FF7DF82752FD07 %FFA8FF7D27F827A8FFA827F827A8FFFF7DF82727FFFFFF7DF827F827FD05 %FFA8FFA85227F827F852FD08FFA827F8277DFFFFFF2727F87DFFFFFF7DF8 %27F852FFA8F852FFFF2727F87DFFFFA827F827A8FD05FFFD047DF82727FF %FFFF5227F852FFFFA827F827A8FD05FF5227F852FD08FF7D27F8277DFFFF %7DF82727FFFFFFA827F827FFFFFFA827F82752FD04FF7DF87DFFFFA87DF8 %27F8A8FD07FFA8F827F8A8FFFFFF52F82752FFFFFF7D27F827A8FFA8FD04 %FF7DF82727CFFF7DF82727FFFFFFA852F827F827F827F827A8FFFF7DF827 %27FFFFA8F827F8A8FD05FF7DF82752FD07FFA827F82752FFFFFF5227F827 %F827F827F827F8A8FFFFFFA87D7DFD04FF7DF827A8FD05FF7D52A8FD07FF %A827F827A8FFFFFF5227F852FFFFFF7DF82727FD08FF52F827F827F827F8 %A8FFFFFF52F827277D7D52F82727FFFFFF5227F852FFFFA827F827A8FD05 %FF5227F852FD06FFA827F82752FD04FF7DF8272727F8FD0527A8FD08FF7D %27F827A8FD05FFA8FD0BFF2727F87DFFFFFF52F82752FFFFFF7D27F852FD %08FF7D27F827F82752A8FFFFFFA8F827F8A8FFFF7D27F827FFFFFF7DF827 %27FFFFA8F827F8FD06FF7DF82752FD05FFA827F82727FD05FF7D27F827A8 %FFA8FFFFFFA8FD04FF7D7D5252F827F852CFFD06FF7DF8A8FD09FF52F827 %27FFFFA8F827F8A8FFFFFF7DF82727FD08FF52F852A8FD07FFA827F827A8 %FFFF7DF82727FFFFFF7D27F852FFFFA827F827A8FD05FF5227F852FD05FF %52F82727FD07FF2627F87DFD04FFA8FD04FF7D27F827F82752A8FD08FFF8 %27A8FD09FFA827F8272752F827F852FFFFFF27F827F827F827A8FD04FFA8 %F827F827F82727527DFFFFFFF827F8275252F827F8277DFFFF7DF82727FF %FFA8F827F8A8FFFF5227F827F827F827F87DFF7DF827F8272727F82727FF %FF7DF827F8525252F827A8FD04FF272727A8FFFFA8FD07FF7D2752FD0BFF %7D27F827F827F852A8FFFFFFF827F827F827F8FD06FF7DF827F827F827F8 %27A8FFFFA8F827F827F8525227F8A8FFFF5227F852FFFFA827F827A8FFFF %52F827F827F827F82752FF7D27F827F827F827F852FFFFFF7DF827F827F8 %2727A8FD05FF272727A8527DFD04FF7DFFFF52F8A8FD0CFFA87D525252A8 %FD05FF7D527D7D7D527DCFFD06FFA87D7D767D2727F827FFFFFFA8525252 %A8FFFF7D52A8FFFFA8527D7DFFFFFF527D7DFFFFFF7D7D527D7D7D527D52 %A8FFA8527D527D7D7D527D7DFD04FFA8FD04527DFD08FF5227F827A8FFFF %A827A8FFA8F852FD23FFA852527DFD04FFA8F827F8FD44FF7D27F8275252 %F87DFFFF27277DFD23FF7D27F827527D7D7D2727F852FD46FF7D2727F827 %52A8522752FD25FF5227F827F827F827F827A8FFFFA8A8FD05FFA8A8A8FF %A8FD0BFFA8FD07FFA8FD05FFA8FD0FFFA8FFFFFFA8A8FD0BFF7D522727F8 %27F8FD27FF7D522627F827277DA8FFFFFF7D7D525252FFFFFD057D52A8FF %7D4B52527D7D52527D5252277D5252767DFFFF527D527D7D7D5252527D52 %A8767D52527DFFA8527D7D527D52FD0EFFA87D76FD35FFA8A87DA8FFFFA8 %FFA8FFA8A8A8FFA8A87DA8A8A87D527DA8A8A8A7A8A8FFA8FFFFA87DFD07 %A87DA8A8A87DA8A8A8FFFFFD06A8FD5BFFA8FDFCFFFDFCFFFD2CFFFF %%EndData endstream endobj 25 0 obj <>stream HWnH|H^~Y狑YĹ `$1H/EoЎe#Æ5ꮮ2&/2Y|E ][֟_vo|G߼>?FGe֕M0py oqض٬'Uݿ;aiE_uxnoLɡ`X8 =iXi=Y;߿ H(ںqrBLh.q[RPM-%M~*(?6}mz1+}^яPV]88*ыpԄ+$We͂z{͏pD4 ow7?Y7i:fţ!Q[gqc6x$m_f3Xge?.8ԪtE>0fiJ0;#Vii]-g:WJWuedK&!ϔ!:Xu]pc0-y_ApdWlMfau1Kç{ʳ-KsQiׄw;]1֝RY|]?vUW3ਘF/F_z8cOB9rU,&77u͊, |}㘚EMߤ8B o.\#w&F%-}oo1S~\ˏ7i?~~iA\4b? d`xAp? 5>9|ٿ@HKVNwxXzɅ?C _^ϛjtjEK?Rp"'Vr.NV;R6\1'6A(jƐ| jR!/.<9! 9 j iNRp .8HJG)mI>,N^*OZ A[ AF3B%L)aVxIFѓ3(JwO.aiؘLkRadi,(&hi7{ut巿Fon{ӦًR?X&`Ǣ1b$nLHF'4C|@Wf<$PIpd @}_~2 dQ9F5t f,J tUL PHҡBʁ_98` R K0,.An*9$nrABA |(("@Ep6bi *JhA)l)V / ) "LֳIFB2GHyH⡰`Ŵ؏^Mc!jaZ@}?2$|CI_! YX 2ePH"v zhWS04d&Y$+qSEVAUP~ _XT+"2 DGELA?쑎e#ԺuN_J 5TȞ  Ѵ=S\d JccIU@>@0R{HZHPX8N"@@!=(J* Z9d0w$*f(+G?@4gLa5V[O455$iWC%>dj)h* ռߴ'CFPrj{TvJz$>f$ڑN$Ah=wIKAX|C*qE<3R*ةF^ZRb%ESSS~@;"$lBo JC _<}ws}!<^pA'_аf^v\Dd-).%,d;Ruc>pQ3:2)9i`CMtzl:B-= >F|$ozQ$!^鍡']v;zW}ysdK1NlE$FP!c5Svsat]1$-mT@1҈τ5,Fyآ~e+ YqN84{<Sg,)|OIs]MBj_Ou~ LpEV):& 8;C:EVA*N)M^7Hz[MF*FEO6z7)Sk1[ ~nHR36C083YYXl:ZֶC]9+~UUJ-*pںw+0',em\(?#mb6D1u*VU:G[A"uCu"Q)<E61z1&'A4uۓ}KQo\F91fng9Du{ pcOmU9Fpe8'./!RTÏ}GuHT\l,2[ _rv6-jh'Uo11rMFŽwmXoH? nDUg)F|>9 =L@1B0FGX]Vkܲ#JCi)FXj%ƿ!V9Qк,leD MAF+Ѡ07A yȷڹ;h9ÍscKY>4pr^VQZmI^ ~'1Ex=m6ǻ*|ƆR-aF'ԟOkfLiBC}A BAV10PQ Dl["oK(؜FTG8Ha1Ю9MgRa7@9(\3illCݺH^X*n?Syd)g>BÉK_b(9N,tblRe;K=츔rFc}p;C}׷xv˧ԥ{̌:Oo_xqѣϞxɫۧz7>||}Oo}٫^>ܷwoqӟW瑎śt U??xt~?}sգo_Wׯ?]?}Ƨ忺Q.ݣe)˻D ç!QWsv9U@,&`s,>~O2$ 'v!,P}[_jU}4\ӽ3&WG݂`Sӄ}fP5o$f2 +!*[dVi@4+14!{uWzYe[izF1$[_ sl#>f 'x2z_lp]5H Kbcf*+?鸾)m!?KgR׎bMl#Vz|$<.$N PzU\:%Tn.֣r=ͯ/s]=nr$Ej5`acAtV94/%?pWg pB-qϐ|8:AH]p9Z'&BB$>#$fjңN,6Yr$""7*|2w2IOT$nncqjW}@_ήs:O:F-4'1SUǮzjE[|i}}4DznNdՇ=HѢEW`;Ϝx`-OҭSf)݆z( 2pW0iBY7!PnupX1+D%vY%0&R|<8#^lbWl*&,f{XG=U3ަ_[ٰKnJ^~ҺJ!)}ZӮ7)^+sM=#Af}}Kj H*B{#X9lgo-H#\qNtNMd$)DBPdLWdj/dqLjPV] kDiBOy(h.ߨ Gp:ӻ"}UiN]#w.E|@ɯ='rE{?\) PT'Jp%W* :`-G_ `Ny]cPUN;ɢ"){a,Mճ8t~Aǩ۽nT9K3:ԡ"0z3( ՟ٮz`QKY7 `r]4Msn!z-ipYuX&nķ5MX" ;L1%ԤaoqʦUKKA5Hvu[Z<ҺJ#Oܰ#fp].&Ke3P6`ߩVr<9XK41)+A 0q2Y,Un@+K SO/Y&%@Ns@4Ч)s1(of (=q-6CE#ljKwr*Q{pQP^ͧk2R1=өC=8"6't񱭏GTm( Z+8b0P̽4uh{ D%֜T"PH-@RJqjGv)nڡbKb_KØܷ_i#s/7a\TX ki D,9 iAM$k+NaSɊl$hO넞.q< \KDZ)>YHЪpR1/Fz {'F,H2~ɧ(VE1 y4 S2`VpI^Zq`/WY>5OztUGfqgO߼h@RUK w,Zz,EnDI6& iԾVXbTqzyXFO B:f0p[tEìu;5ss# o"*XDܞȦĤ#̝q$Q%~C<T!  gBE|vCh-*Qm~r4hw"D(O(F]T"md5Vy%V"h 5%f - 16H3#Rqs0(u96p4P;VwW>؞k F:T[NLLйVDb0Sޥ`ߛA8JT#vVK{6VCXdkٺ>>Uo,\Ro8ܐ=nne7V6xz3>EJFcLqvf{M81֝b+XL&,Z#sx(5u &)1SYgaM"g87v JkIzi*v95D,>ढ$+N ;Zj5{!yœ3O ??x%T+{'G$֢2|4Ǿ|˃ʜw36Ad=ϾcY!Zty~Ӭ7Jl{%[Iyh7ksH*< P5 !WYqHrR[.Jj.s8$y@}14dߣ,_kN آ9(h5Zh&nY0 k5oܱDKh.&ʲ #qt˰e? xzj[c0<}!hJ}K?`iQ?ɬ[eD%c4!aC0fDMDA_O32Vg}`*kJX1C stޓ߽+pN(m` *:oGURw04f=D% l^V;LağWmi5@4Ip}آgWn}WRpmYb-سxU 'jH,ۢrvƕ*McVRy BAE{qYO2BZ%+Sey[ BGQ2>@v*e'Bj$J/Е 4 7^(Z˰*b_rhᣌٜuk`ҿ4@Ix5~˚5TK锏%Q"U5o`eL$:IPC8:w\B}Vσ gRkn@CP ]Dų\^NjMk8]Ge>y*$Jix`C[37t༡y-*؉uP> VA@466x"%e#M՛>=֭&`ȱ} yys蜲c0P a ۂW5Jt\7Ma#S䚄UT0ދyrK4t-5}#τ-}D+Lŕ>#X\gĚC8c"[袼XL怢G(p;G 6A@l\)m<H^^agpiֵ~Ǹ:}*1{jD@ *iah%}Cj~~j3WWU܇F] eĮ.I3h̨U⇂:l1 xslz6 s1E'Gpe2Mt#{T7# ('G,sH%B2RHkO J)^!Bgb-_~%k푑PjO$-EZXff׹hY6i7;OWe܇[ @y}^e N05"TLk3,~s! >>9'YzeStʵ}3~JeU )?cqC`nx&IK QHKJ-B"8mAWV@y{շRYSuUW&kNH"mKS)}7I[/7߾5ϩuHBl^Cpc9 U d 4RoXo qug?ˋaVbg ؽ m^Uꚇ)Qך&IjմR87!mwlb'SA[2yjEϦЕP m( U}\ )yP:xdsuo.߽bSǍ{Ic&X ř)A|VYHbUVZkE 4o?I ,2eB) =\缚5 {b dk!R]MfkȲj}`gOL#^#E}KZˌc_WKgՆڔ)"'{;/__D'.-^u.jQ}:+1ƍXW\w}%k*dDnþoZ5G Z,ö@χM?=՘!ܾvƺx)ңe<fD8_7VKG^: [P=먇!,hdnGݵj# Qn OY]Պ3cbu54s 1/nKLW(- #r&r\K`}KoUΩj(ʩP3"4 &q,rq@bM2K;&[Gɔy2^t읚s!x!PA+0<≙5k]O⃵~ -w 0Bqp54j '/Eg2ΘӨR1j $wBQ`<1qٚg,o5&r9 5a==i@bCpPcP֑ erЗ&i_SBstoOyJ $_: i6MɃj-U-绉ݰO G6]*-z1RcţVe*z$szL`ǹn2kb'2a_KM9`/S(BRQ~vj`OiS>y -mH7G]m&,?^0F8X xe.&~'{ƧGF3J^ЮeW.lbS{<2]?c7ٯݸ#"@22t=y)'pr V &#P )(.3Pr $3+وp$:㨠db5|z1ѕBgOD] ; BH36΂>,UbFK:3=r?<ķJ%= !HqPJ+KHu,'AyESQ 9XT(C/|gZ#$(rs5ɤX@ MIC~+2\M{E]$ KB).1%Oe7vqRĮhH0uS9g3ƶ&nSA99os)kv=0t ɤc'v)-uiNB$ cYA%7PΚV3RD÷$Xcʋ!&0JZO"&R@Oq pL U>:'z z@LcezKs,8d&,Yb\.I=2īM@\#1}dch $V8F!M= ,$޳mgS*-f۳{D" MZAsᔉyvELbib,Mf"QxE^b e֒V$b,)-\̲lY^K.o;0I+W`β(レ'N%ֈ#Cj2'RӲr*Z{$:0PMEʖPDސ9P'4D_S& h"P@-%sQqQD%{)4/?Y>EiF%8yk̈́@ 6MC*YH_gR BGRKt&(XN f'VQENtŘǒӪ{W޿#% 0C(~PÎ_Evjش~Ϟ%^.~Σ_kuOWmOׯ/E_?s^oޜm_px-*G|Ca?[hUy9ʐjdO' r*stJVsծ*âPJ9@8طOf}|}svq-mDEv;Ɂe,TہK2&ڌ-C, r3|d<6=ub#6͏PIt|"&,F FDx|;3~SyRHf~'8~YsҭeݪjOcxxKl=IU}Vn>j),}؎Bb|HGj_A#$h4V`;\ofi _i@IRh=+ ZƜ,I?EUSW'zoAV;gdW$Ũ6jSQq=TjŤ[v9Nnr2/9Y X#V@@ 8eDVL#[~[w^=:wAzN??:<:ݲ?^o˯1=gwwWyw}sϏOZ~y|}}5ٞtKp׷7kR3f>:rRnnžGi"8/:sb"vei"u"x%ݲm^' J䚤IIĭu۱;cI5u}g( cQ*Ցki#ЄEt:fH1twTXeZ\ G&>9y"~<_ݯ0qU9L$L̼M?S7[izR3'?d\:'sONx1V{j+u-`Y-eymT| ǘQ82l>R;ЮfL}{=]yE-;g~bgK14Kgk^{>c;Ś]&l[r|/+UBd2KV8M >g19i.kr˗s=PD$"foJ6UGvxhl URowwi|Ó )恴F+tS場ޮo!d'|uT`"鼼*K Lw{űqzN;W7Q-;6ؚMhyرF7nc'xfx7Î}ΊWo7r&^6<[sƋ#v\͍aP8xd|2GFdUY"h ^w_np+`/вANљ;G M'2YwL*&hݎvJwᇘ3' :Q݊I1|11ҡ͸Jeh@&-״m3o}f!Wk?2w6H曼I&gf`nLѨGiGUv$[VGW0zwq 2T`]~ײYE:z)L6O"ikejP>h6%QF6%{5aDmO>`% :۠G 1U |uV1dq+WUhȕkCzuU>v m=GQ%3Tv lt I[t~%{QfV?dɛ.+wRO٧5O|-ugw%דi` =PI:G|G{71Ƅ#B?wD\o8dô34\tw %: ]z n+ShO ZWaYLFW9d4C:܂r{LR;,} 䓾$T~`}[zU0bo$PpѺd mV$/,i g HRm1MчV-P97'+f}}/'dsX oaMXU/H ဆJd?UA@8]?ߋ_?;fzL1&MhTkTbXtX%bѠZX~pY}9*WڨKGB@uPF + V,MX%]s_T9I\Xڛoʹ?QIli؟/6&Z`Tm'w^`K>0 ~ Э  +FB8逫cP9(4*Fz Lh/p5S)7a }2~V(mQwjFKK:|+(1b}HE? I*ryL*E)z!#JbWM S ?N(8U:k\Э7'99dD&7 ^SPn?4m|/l^OgX!֞nwp ~NĶ byF]j|4 8&{-:yVsɣۦ~{oiŢd;¿ J3$- Ʋ_4T"lVڊ,j{+ J1ScAV.Xv}4ɳB2, UXa٧$'D3de<:1d#[\ȩ73=3;\uYv"# ok5N_B'trrH676+>j[M>̙_"tNs-ɸ=F˪GI^? aP""i8K^gGj@.^y(]51+^-)bjN`v4h  P- \mK &?@]!aT1`U=?> endstream endobj 26 0 obj <>stream H˪`.l]l(Bș$ ?% |/xKya' S+552TAmJX d_Zl ;b,#ɱWQM@Q˾qc]az8?\h=}Y'diN;!AP6/*!bhk"[k'S{bS8F IL v JsDd73Xb#v!5|+UWZ uVy+^1'WxE)RU}6|P2H@l[7O X?Pg. nyԄ !3 ӏgTԊ[̡R i Bpb[!mͱ}15'YߘeN-Q1 ܘ|I}x_!{ۓJ iǡ!_> YjyƂkunmʼ{>C LZL_gT|O]OI_}?^O׷;~;kX}W,̯f@ZB k~С+Nt?PU2m1׹hzDy+o\E$pmF4LCMFNO_hg N/fBllw`%L& i*j=h:,/+.fgNfpf(`;WޜdϮ[ge#u*uO;gBfy:Ekt].c0u2P吞-%?͋/XEĄ~MQ~M!\Wi!ZFl?⹻jMT$կ)w)WyhtLY9&|k{u+֯)\/递_ 㭾ХnFYj03X[VhQn<xulB/:M`8_?9\ߊrr.Z#0Y *~ jkyEX\!d?;6El6+`Zo։ rFW"SXQ䲡ױ=hNt^\]/Z(d^SW,P;BN25,PQ>%GWՊWѳWhɵYG%>wz0P@7W&%—l~.fϯ>ӎn{sEkI8 <[Ka(2{N65ajKSWSk_9Nnfg,,t]1chEY"F'4;tJ2x˖aNu` W0sLG4/Vg mgL׽J@9OZ1cm8GeǨγZa6I J+C7C:#`WSK)PV+iǁQG/G+rF=zuvNb6UH*ee2E|W+5 'td(('u\kP۽bk)`3z-\/,n7uKH1 |²p/ھam砧f\Y!|lM ?hd3aeM"D%jn^#=Ϻ~ΣdWڔ}O1?xo?zo'??}J_4+vg#2RbGT}*Ⱦk2&`Ⱊ=hNƌEFy;IZ?؛&FέlTI;6p NĤidGmhS 2a#*Ơ1G>*cZKhMv+\˜1^Z-;*VU]ŕcHm@VюƠkrsޝ& (u +ħB"wh4G5Hsde;ڐ Ʌր#Ahv4TI[$Jݖ c5ީK\Iؖ)!;dG=zJn*9b`;(j4 wC,e"h.u-7W8 `o䅦֩!]ѵraݝ RݏL!cQ4Q2R [qa1㢖 t@LsjrgLNPBVw/UZ*>e}+9b $v!2"\ ߩBL U|RWVDڋ@0iNckb!cG\ȧ ZNS-~tAY1PKJ뱉;lZa2-c#R>ݺ.TZ/Q[v4afiʴ UJm 9-k9c6tC+{e6.7 0u*QkoŲTӂT)<97+ȝ/ rhȺRRJtxf7S )l :gŝ:ʓ\({i=R!gJ(ڶ,U鴷8y盵6 >w`upi=Akeᑪgc-ܢCi uoRJ'hc3l&xG%6TYzExY#%_C]\]EX#%I7M3%̚t.=|k<pk`tifcHxT +VSOfhyq0]HaGm3IJ% dB N6GIWF9u6:}GӒ6 z|#\pˁJ:I49uV |80G]W'ok[3Xz~ŹfÜ | J W27!/ Ɩs jSWECO*㺗`uVPYY>.9PrQ#9%z6l8o`2= U|¬ׁR(ˈ`l*2A ۣ)C aǽqg3"ud@`!LP7+٣Yプ^i' _M=\:9 ^|(/o?/ۧF-ߤM^nîdf#2&PP40lL?B:@eH wPVy>NT?Vj;PUMLl%%l7~n5,<c^b81:jrW<`rv:?;Ncrqm, ? 6#c}b/ظA⯢X XK)~"΢q`n+!X#j>}5ݱ06ư_-P6s[uWBeZr4Pv3{Ӈv# ZDphh`nl1-ٳ\cq+B 6/;]C}xU{co)6(ڮ4٣WVJRg>+CBw ,( gW|7T㗵DPgEqgq'_E(Q%~ 刞_h}ޢܻsޖ뿚',O_gjۑ㸡_0/ Ӫ >?2}ZB]l`'7oשW*ѡ"ڝPъLhB?P-|TK̴b>31Ӑ+}ng* ن~-3& *ِ>3>  >3hYb@^Rf e\ph)T( h %$B}-pk @TTuCk6@Dp 2PjAH.\UrћV3|ԶqxLQcKJb K~qV "Ъ ǁ(! eb)Pӈg03d=JHKmH?{H| ]nHG;(T`O6j h `$^`BA J!69r.!LCXxpšWRh#Cո(㺒4+gw"X%5jf;V]"`.E0AG/hb_'gG B(NmJ?qy.@RE9=E7*6% Dڷ$$M*sH ";{- ʀNG) (ڦrA @$p|4GD^ )UQ Z:S.|Z ␰ O* Q"iR2)^MkOu ç^&8:HӐyAƶdrf\ph邠͍qj ٵĩCC+ڨՙ`Bɣu9L$" ǁvHԭ*T1\˶QM/-KýjԄh3\3L' :-&6dN%Mg<6iro9(U n\.A,"l!.6@%V8FG~4|N1Qk4, T!mKb_@#Nt7/9XD[u(}7Do%erIb͞"p'o]s\K4FiaKRMM* V44lUAᚭ<`V+':i rk u#.;`w7^H#(x1~BBph;q@,FJ.|;HF鸙Wn5dMߜȞ `pю>۸("dBmiuD 3ij砪x6 f' `-qy927~KYSDNS D/:t"qV:3MS%,rV A E8J_US 5i*HdwYGtJRڳ2- ujT;P)JCL^9gYJPn-b3'e2+hquKVE ÂUN @WXUZL+Jhz3 K?RINyR6'\9:׌(;kБwGBVoTWCPVFlq"/ JT+G!?DVlR<~`9]i{|Ī*%QG-vaP:sgiuj6;ځPɀqD"HߖL(d[yZz|DKh(gc'?tel\KDH_9%*:UvfYC_;;Kx rǮS/%=U8HRޫmӒeJ7UZwcזR&m*vTu#̍fXrquO;Vf$/3ێ #w>Ҥ\Sg /J8}F@[jz/)jzn +o<ϱˊV j+ۍG/vkA,XY?9N?9rteZѡycgJzAu6#c:K5OA<4tU*2tP[jM32}XU\B! #yR-N(NFKYPN%@y@Y E_fa+I97 L`Y&cKÎ^'5jap,/:N'"H1 #Ph#F^AYaJSF1Ow`{DE]Ci zI<A f++EĈv\8ի- {aa$[(-R?Tj~vl*}>TJSכ| O??1`}+&gN͗c磻T.zIr+wiO!GXbY{r5C]V{,Q7j.(D(h4"HXnTNƎ RO;h<3Еk8Z(UP6]+:Fo57K_VlyĐ׎'7P2hQMV*Zl. H=SqqX=,nNUP3RE%;2UC T%rqOvh2N|ߔT ~(<__ 7aubDzEG% \Ia0a4Ș{] ,M!0B#ATk-VєQrϭ^][.+ߟb\r\CK`<Å(.`,l "E4Yhg(*:-iOn>kNI[!)':**ӷ.Q݇KWCZ HM/+dqFf3s?J]r~i<ٕ~ꅤ:Ht˕ULeŕ#i8[jvH }Mc>,en7.9 >APd`,2qyXG;J0=Jm0!MmVt%/p auAUF*kt2"S< f~:Fp[ni)tus킛ou3/4|:OW #2f@$OVb''Q-vfE80UC|c AZ0SnB,ACK+'l矰#32-T<¿g;k#Ǎ4|Y|$asS;ԽANg̙z]lʨ3vXZQ5*#bLAo0qr`oG imKoxu8WuH!0#{gouS?RDO⳯LD!m:vԋwE rحFc,0Yd&U s3'X)'ևG02މg2}lPh H_]Z!5f n W/n{]T4neZq~QH!ȈVCPo~Gׇ"&wLՉ@f}eJ( 95R: {Џ|-`)K>[ik˸ (uR u|PUi=џ~ WX{*owIM4F _A rۉPwNqE*VPKӳVvlSRh3ݲZӯf]y?u9q\ ??[621_!lL&[[[x12%$siI62$1dE.XnEk;rt 4*(USrJ 9ǘ `6iT*,V#q*ke|zM (>P4+G&d-`%&4$Db@[!ilaB:dԍ`BRjr]-a{9Pj,,PȎL% m9tn-nՔL;Ԙ QIU 5E,bI:# TW50IIfM@ Xº ԏd"V wwCB?VKԥ.=Y1$KA#^CL)6A\NȜY >oivJQK:71#CNd/պDYPp"^cCHPPPi& j}=9csK2*0u,I&pp\|scm_>囦~# X*6 )GR^y/{cNω%/!*L!uQ]RyaRqC|RnNW*qg0MRȟ 3Z/^rԎGJE,j^LD0.RB K (=K&9WІCJɜ"Hڃt1u a:02/uQyI'e|fIlplpR2M!`@c K32d|0uٌ+ \z&Øx>%NǐЫd,;pzU }A4I9<[FИfz z O ;/ҭLuD\MG*rJd2>PKXa=Cv?zR,;''+Ҟ@. @r= |3L9ᘆ-ٰ܅]RmAS7:e4\rQgeqa§Q&+h| D@z" l)T fܣa&r3>993j #:Toɫ&zӁ@X6DZy# wYpC? "Rتif<]c#Pz_+6eH^QTGqߡ N*s&&a-BI-Ch%jJٲ_߮],Q}bCE\N{q5P^oE,GǢ:w+֨1i~e6[vdc'B*gL<:tw<ѩH'f'ŵק,\un̜ƨ|sc1hWb znijZRݏ:0g%>f[?_!ܲV0U\:xu1ٯ/ 4,j~W&)$U$ӞV%Vi^],\u_ݼ).FܣcwE~c'ru%=ֿڽ~I5q G5t,zS1(DK8Š*hv} 3;`'GӣD:8cNiE ӫ q {xLS<%,̼7Ӡ+(f.SPm/b#::^kUb?P ٫7I4|P]@VLL%37ߦضg lz mOp>!W`A z&.n- ?vwNQ%tJenؽ}~5;Cg"'V{FW=ԁ ):' n+ʹ 137jc M_:2bG# 16+C㑒L]C%\ hZ0'NeUx L3?o~$:!W>XSLV\§EFx͛bW9Dc:3&h t awū &!d1(b9q= ac0-.X 4kVn91j_)ַ]pxwҧp7 uw@Pnr/ag4 @]"&yHWNF U 6 슮lD-Eh De_Nj=8RfiAxs3;:1Qi8cB\( "\)x >/-?\ìLn; yX؊FEIN*Hz3Wys{!9 $X3 N`5oz@p,`m.՝X ?E$Ho(P7,v,]I^cZOťT$GT ,M4  Y"(nwᏻ}ԃeUk'UQ>)t8IBP>WH Ul( _:/AОm^R4GjAqC9-27kҺ=,qw%CժP,mbgU(8DDSpbوI8WJf GlqTp*mQ!>stream HWYvH^AP9cm0L:aq!̅T@RJ^C?oKB$'O?ֽߝzvm|9D[&vP\r:K`y/*e&w05L8a!88xAJSC]DSNh4WƼ6_f%es4/c/"*J+Y\y9 hDaDH9\#M昺ο hx_Cu,Ek:Ӝ)1Er_ +m@ܑN/r(\$7ԦjPX[m1t-W.bҽ%=b`UvMj%W>z kjʾ @ya S'mmkV*e3oS9b_{J%MG1f93PL=b篙dKĽ,c{M`7:J 0!#;gq-W3ɪPlrl swq4pW#05ZHcS l?g_SzbRM],N-C+Cj\7p50GF:pO8 6jb\wLc\+'ŝ`/ Ho,Yd@+EǑNeTUWuQҟáM|q >\8Z4l\ke,7wԨd "S>h◾DI'qOC:,z M D͌ҷ&MHoXăiZVPN uzb6 k?jJEd; 5o?U94~ #5F0f/BZ V 5B@bE! s]%Cbϙ,2jpn>)ΪKAj@}" u9r+f9vJd'8Lv_le5N% 2y1|S##I;W;R&7|暫X\1G,^6Lly5d:>w6[Aɫ:wjx^PJBVzsюe?6<:nD-+M7G `7OS>ӘxUei7cن:, *[7o>|l\7F>Տ&O;?Vw6։>|?VFxw0lEk\}̅ USՏ^ozz<ӋԎyվ75γo[ڱV~56^l7/vn[o-lm|ix"?awYV Lo7s|?չmLdfۀ9Q@g=qUWp>>UXEjy0t)?zT$Mę\ihςDyr-A|R ,]t/YWG 0|rqtl{OF!0G]0$}C'N=5"A]%9ͺU'޻@-$G>޿E3إ21EU*x1Z {an'-W݋t^cPO?J00eޅ.4Ɂ cdݾq]ZEDH6V5sUWݥT9ދ\vt`_*r`N̵H:ߋC(4W_@NJX  "WFɺ'Gk+ *ܣڰUe 6<:B 6St]_z%D6œȬTdB7xPC4$ ,dXL̐A? ۰!(F~")@˸w?ʆP%:,S%bQ _OpD!>fj2 =e(xL20I]͕-4b~3CFH7 5G)ȞS%cɁTU ©"чrkFnn"Z17~ <ZRЗpBj܋v_lgfm̍ fӦ/}$ hI?X7r:#- bm0\C9 q(OLaϰqV|Ff@V|B%+M[?kM򥡅(gr {x`k *-7sZAiXXlCڔ(BZ @-7Jlk,#90EuR&U?"B$o֣1̶g1!m[JZdf$ 2FY|:xiudM;vB ]ړZpt$n'ѢnIa DsKyWrY1, \~ԣNςp>]Ap{vy⁛<}:75Ov?hk>]蒻ǠmkWAD3NwQ ˹spЗqDb6-6 N=Gi!9_`/r ۻbS4:_MݭbQ,`m.*rv-@yf[=}LOb}XR*4v<.`Jo9@&4/Xl\}^*ۺ&ǏCݐ&nK8nz 3 Lw"9T d~nF^>}głX`?wCo]LmU&zOi(BOeq;^#!UrYX,YdIM(:#m+'1'n"AZ?ΧG҂&;*Qȩ~_ @ꀳ)0?TWձϹFyv::N?f֝+E]ӌJ\P.\|1jT8Ӭ'a?%zD~N^] .̣.֔u%$tS"]hв]Z^+"^֜`r?7 6mXdYV}UкѓZy.L~YdYʹ :xr1q/8aq@l`r+1T# tgq5';uO Y*poàݧzu}^ J5IQe*1MR>e!ejv.pWulu_BB_] &fPƪb tX~ll]0^e۩I y3Ԩ1*j4Q"(ÿVE?{so j{Tɷp$|7{Q+yэu#@bQd7W%~]܁ ψޡDyI\?X=Z_¯fn)~$]i3(| +_WoAEd_V~P+4 c>qУ!ng# Y§t!{}\:g|elԨ_ Oh2vrCg"R/Pɱ,o gA^SBJU;m\G^;jKyΤ=r=e},u74KL18Y%ab2;)$Yک!x1ŕm/z7*Y} \T}E)/l\ֶ‰iX tMk{1;l̴XBh}^<xTmn~DV`>w1S[]1TRM4C6( Չ>L.7]E e%nxiwk]OxC!Y&$%\jG;h 42J,NOġWl?#h: ( Pc&0*{-mmL-tr/)Dꭊoj3/^}+Ģ;1dAbdp3j=i(nq61+ T0b#żN@5v澤,Όk{R=ܻxAtHiR(l}p/R@dwJio\JZ{9PzVi8hJt~ !Tcj;Ħ^͋}\4:bP H<⍳] k__`X|Ά;XS*؄Rf+ C啠]"s8,.8B Bn ^pX_'83*0VfJ{lׁUESiU2*&וMxFP~.wWssG"xdYMH}/eG&h"R)>0AF0鈎*n$"V?Ff "4xK`3*1- DnM/.ΐZ97ɇḪo߻xS5h]SK8'~}q %'w}tMY\fnl 4O*Eƛgthr= %rJؤ &wE{_r 32|oϛ+omjGF$oQA:&2 aw%H (" 'k8EfSXvhh_MyZѺT-:ζDFSdW:أuEv_K8ˮ$ 6 N2-5:Hɰ۩mVcWQc{[0^u vPN:&eEӲgӌᮄ{qAGDUu]2׋h y^_l;(;-dn,!ajy<15̕=o|ɲbE te6~(Ё֩y=wx"F˦ރ;TLDZ1AGBAw:p߈u6ر9R ο^$(It+7fskry/4SW\}h{h멁t)* ӡq,'g~]| $$M)ڥU7Ƣt>M,% TȸLW͐8-:0D%49P+VSMgJX34PY`~ 00SDY F[s]T {ے6 apr9I&Nen)hl5`&Y~uH~fz'W\0V&j,D,­sKpSLAe\;IF]j>sZWFW%ghxYsKbd8Ieƹw1TW:0f^!@뢾*h޿zn"cyNJˤkHeSFSF=uaBIck.q,ևbf>9`Zojhm۠UD֣L'ptwD-zL:\3o$"nL4Hz)c*"IYQ$Cf`\̡A2]ORhLW4\7:N6fLokqkl~4kG8S1SsJ$b{[T6f_m9tuU./y?rf*FLHqn C x#w9[3[BPƈ42cCygUWt+|牑0&vݿi6s "mLeCqڑPi'S"uPH^<di+|]p ;\M.z7N D7Q +o8Q{_b6v1 &ZUN:_r.˼f瑯}N-D7_>ֻZ+^7RQ&P7ͅ YdXY:n 1LhX;~- oi'4p7\鵽nE<<Խ')-y`mDܛ ]f|ˋjz5w4A\Fmz>|RK\ R} ql9ˀbWD>7>[Z]ijte$+okO}xE--W@~x ш˶@wmrna9u l|.&.Y\ǸbR=OZa*cZn8B3_1[6:ƣX/}ԢA5(SA=~m\>d'/"iӠxUϽ_4([cKq8;@lmP~ZzTS mAb`>jӡE[ 9OF\RNT Ǘ{z [ (TӸ Od-|i Ell?CϽABt[2w0MςeT߂J|eT'hӠl\d/[SO th⤅fTh!5('otfBߵ ӫFg;ϊTBs>ЧϑRnkz*5<9B Z5r̎}ϰW 0:|:B=~S[WUCkIb|Yq2qJP4ܑ^JHްށv.?@/Zen3UUvqJYj$_,;ow߼?q;m_Ķ'c/q坦էi:{8W4B֏8~֛Z!Th9;LqXOAbGO_g{%)t+P<-ū9b> 1}!j;76R-cg5\z0Ax!iv A-N1˭0TNS)dS<ȩLj\nN 5Őr t޺@&jGu|~>CCb*23Bo[_訄bXX]3eZEAd6uNj4qmE [pÃj&v$++ 0-^MګtZߠl>F=L1C;mq˃ `ZC/m t ݾc;PE 0BBPyߒXAY/Am)EeZAqᐯ+AG3pkId+k:v&?c1w1K%|>)@D=+ˮ+3TY<;UxD^e mQO?:'XsL1m`̄2A= J6vʭx]E 2+=t%mX]zcԌ)V_[`n>1>YYS̮۟Jrz=!A3"ɮJ=Hiu.,֬̐?U'aVVƆ4d4uͷEw~4{}õ^3jRS.ԹI}3Ԍ ? cB +V8I^Y*{%BR߰++,Vz@V=|~ѽhF~+4c ܮOqE(ӴV%$^OxҊGDMnHաOv遣ܟ7I'ܟ:v(Sll'c@-Ip/؛!nw2Cb4oIר4UZ\Bi:D_>2%h FB,}fjC]YCGs sI6Dڇa+%0.,psa/P@;  S &.mOooKP#j[Rm3|I1/L6L. JZq%5 >x 2t j؇6l ^1\R@jO"(cK+8U-[\B+0)DQ imIKA@=|{oa ,-Y{ T@{kq, YC n>sMrЏX<-Ї 5ϚÞT2ՃDuXAz8HMS԰޷%d{tbD}mmIT(oU$V<5;n[BLڐ pgښdt`!X@.`T0*t^"lA%vʹVN;&uRJOhc<"J\ yK CX:5cW:3m{. A˨tP;} nO;K] 5蘰AXj9\жŔ|+"-)w]*waEN;,:0cB @doƼ7Ui4!NvUxk@90`'h%ʵ⓽؉ DÚ{S;x </֝/:gVt%یZ0em(0n`\Bd):(?7fN1R+`u9 S~vd48_;?hhUр1AF.47AYN{>S>>g+p{44ylҫs.,Ԑ$>),t⣅5fQbPsH zE`Yz]А2uI'{.W;.(cZ'|1*yo م $Vь*׊OI'IN>2oC1}‘A౑񽷾EzG6>cq=k"/iy,S?)67d;"=Z4ŀS4cBUrrGN arMq&~>;SGiu ̱,gww\S5y9WkqzKC_MNFN/d|WR0h+}k[buop/6Q钻XG-)|_@ߪmcF)X\tlC5|\p^٫m,v (ipBZG.Ϋӓ4/ˎ6T}OAE8hRێ.prsC! ègwc">pĭmpcHsU\kR3oXs׋a^X_6Z̋(GkM2gԫBW1nj袵XiHmKwú:P2^U@YT\-jJJS'24? -0?CDz@Sʔ`ǹ%"bN6Ay!D&k\c?r's/9/q>Ɖ*M,q `ʹCzv51C`jmBL@u,'^fsc~Jx-P?m'М$ԭ8Xb(͕Ҡ&AM*Hi=u.Zо'ǹ w"UNyM mcuV7MrʝeXX~?l:DmpSҲ uۣ0 |c+>1eMwV:o cU ?et;ڙghU4&Ǥ_ݙ_\ITJo(W ] qXsp;\8D&TPHHUtd9Z]R:2z[NPE^t*}(/c+={P1q 0To*F'OV՟$$F7XLpUH U\ϧkKᶃ;^Jo:WuƯ7 N·Ѱ(ĎV [3#5n45rf2K(+N5'oNyV L)G`!9-I`\`8a'siX7Ig]|*bIea36LF-C#_q$Q-u hibiKGzjJ'd5Jm 5(m`XHL7It^SӋ%1VE##xI":һjؗh)Ҭcǝ#D'mJN1wv;(F)+-ķ>e'qLTF1.u1)1Glb~ч=93 >A̰p#V G.""&Š?[q&-}ΙlGVd$)kK v$3;o{ʀ޽'&\շM NN2OSV`vI7(X)C: ~%Nŕ<,xcXcXƐB 6cy,Ѷ} 1єcދ۩q ou1_cإbr;1'푱 7ʩ_ Z,`19෗Ȩ&W3le tM:} W,s4]A`v96m<]|{w]|;ml0,w+M`T # -&ߎYQKqU|Yv }5&zc p~==vCY/QO@5JRP+I)+qclH͕5xy+iLF{Ecr8OtWу'+`^2svmV yR6`V;fV.uj/[&ݛ~t6Cn?_H&},?T $E Ov:<\v$ t 83^}{Ý/Bl9=F]/\5Q(/ YN5?qs]o|:Nfw% #pƜߜu0/&Ja9tpo|J:-| iⱁ_y `}yNGL .Wo!G ttv#`2mؚ߫*$ e慪>ЄޗӀf8193Q_0BU}'`'Ъ8@3ᥪmcN aX='UB:[6(%짨]ÅUM'on+Kᆤlڑv`ҁGn =xL`[dq~*6kd㜔嗒͋2tETPymJA¶3CzN͠ީ}[36#SMp^xwcih:Ww>P2Zk$2zF*t㦖-N&6O úJxTS1e^,oiKZ:z&uS^`!g;O=(\2%2a!a7 928 PDAUNFaP^*tܸ edYfO"cFr$)%bĥj5Sd(MR!05~P^zE7Pq3=^[R j?mFӷYuZEKH Fju JHg)%'G{8t U?\ÀɃ5C7E^ qi1f~لj7I#4[pΨ&jvm|zԷ'a`or{f;12oM)} flJi]Na]6d׫ϕ`%f!VL>&'vRh1AjK5Qc0q/dz߰3sΙ5P2ۙ 5h?U'1"xGQ-d.) %)B$>A5/Q `ْ:U-ip$.=_ا=/J <>]jԘ.&VF8NU\ep\U*FeH0m%YaGYA۝5~鞴E ]q܂>stream HW^<}>K[ 8 XQaq`I'3?ss﹅پN @9zU&|+vFT2d6=go j?fCZ6:-߶Jb[o˩\??2ߧGj=VGO: MJUX ˝|WThi+*ji!gW#|Nme'^L]*_b͙rasfN}o kӉo#Blނ m065cboŕV@BXːoq<"+^h _QP*lw|icfMeW^H>Xξ P87 _ʉG?(Eٷix2^2Μ8!fNo(GMB393d!|lk]Aq7n*ɺ^-1Hx ceOZً>Vыx0 œc& v0] 'Ϛq'hNJjA+` 2]s1[N 4V_2fSgk{GEJ '^ tLamP/n ٮqB^x4Hho1> OiH6{Z*v\J1. ;4U0{KBK)å!>8ѐBQiq(R(~K{ sP%7:f 2'ƜaGJ#x r˜O/(4(eBLKede'tXveXpwR#EQ-p:A3Ϳ"'?NfA9OC}6elWno``5eKBa  gmӐ a bqSمԸ[ 7( {z[ȿTU_?FU6+T!7ͯ+xoKޕ-|+I6vac^WϠ0|ba.u_>yF](܍\N;!ScAR`.fyl o\q0ı* EֲG<,ch%Pɲփ+ G=W2Rs@mrsE KJ D*vQ/p#Zޖ+C] etgDruRMF͓=<~.A {=u``A h42& 2-j=f{\M]%z [H3xF_p0fg3i!_l*E݉GFAzfXsB{7h2DWYQ3>pi /t]pΟܚX'?;Q}~b_Govkb~ANȻ:-m ?E=ȓ1 Zxj ΂Tj7Wu{8J =n; ye\qSKPlawa=P AZtgjCHB)ٷ+vUhsPB.uOQJH'8On/[Mb C]|Py ".YT9%}` qǾPZqwVrZʏ*tQ OkOg,cէN0=-+*3ѸyC; I:U:Uy;ܨ4+*6U@)9'*lhTW"ЊUJr+%+- c^-Utr̄s803J/\n /qj+r2Ajp |׺$04g,x^K:;x MɈ撙{,Nn !$s@~F8L}1]mŮ|-5ӊ*1\8Y2bi.uJB.(0=vOBQ.mݭ;wMd(ؽ,(ƑB}y04 M߭MƌaX}MIgȉgϟـqafևj_r\u/\%508B4 Rnq籛!;co]TŃYB=X8My90Aկfg&ZhEi-Ro(>Hhά<~L1KW'K I]dR=#%WUSPTDeEb\JT\;2 ɥ`NsF`=yf<7O%$OsL6Oamɸ*@e+'.a:՛o!TE.ˮyKh R.l=m=[WnѨc'.$\oIMrc64k07C& xdj ȡ=j?N1 o\BST*8Qhو04>|Z#.+*] jN7by:z JkZbV;&K +v9f,`R'} 1'd߹7;eL 5#c]\IEu N(Dj6 nߡS[/ j>^Mb V8v!$!Wb';ģ_vx"u%:g> 躷uĮܞwS)P+?>[9m9uIN`{ يXQ8󛖒8q o C L`kKFJh{ѷK @1Rx/afm.dZ|InWB`}ןxukXA sji:IBԿΕ4^|%j< <2C=>tF ZNB0N>;R o电v EF=1>R-{T . ח$wll\+`7`0VXuAP2 PަEVV[n/D2$o[Cm;/_{16> 4z']P}8 ^DS d93HˠWu*ݸMǛӾ/"$oVe?Ƽn5LTBqP,[Iڀ٣ ӏOD9? La{6,d(͡OU1< vt>TZݦK O eJ|̃ /_X/^)y/w&e\ x9bADʜR&?HsU$(a2 kGJ| g Ш]VDv&LUi>k]OmE-X~}a0}fi';ȑz`DߞtD,0{byfͬq`%}Jc'AD/p3 #nYf{Ŝ rGw2jYCcW,soqJt!h,Цn롋2eԾ037}5n 녌7ia ZiƋ"-S7y¥PqenfN矟 UWr c'^zVf/EW^zah9yXKD,=Tٴ`5Ϩ RQLjPqtNcgk. gfy}y9"$ЈH|{/ { 1sccCA WK|xumif=Єcw, Q^*Ҩ(Y,3K"pd b QV9%b^E+~j /FФ"wP*@2/ ?`Qk4DŸ8#AJB|MR(2VR`S  :( M yNw7O@DMN;TAtyDVIs,,noL"Wx$5 cWmRCq% #Hy2e[X rp,M11bGo-؀l" R_%^ >!f t`tKeH40B 6 b_!h#2[/Rkm̱T bWJ&Ғ}ZVuS242esboqR~:ÿ{Smf )'<9KAྺil:Dvtԑe2ݞ; Y إmA08@5 g;83C"&UvM$ghgq/yBFe0#f3jWc&60(b,rK1#: aZ$&>]cC(0wl\BkDb@lf{ muNS`Qg?BR?]Fi7 m׎ ʄ;9 x0*[)`ۍ- 7?IB:'DL Ղ:._[3%I"A?V۟*fح,ͺvujE |SJO/EDLi+x&02YAxo=F'y7]b7 ͮv8U#8kaQxl9፝I}s$/nĹ <55;lpFvƐٳ;(JvK4bOqR,7uѯǼ̏LcѸh󜇹dkso؎_ x|=E)QnDxy5!bUnii?y!5298>t8bo/f,% o1ы .>-Dk%hy)XT2.W1,e逻Eڛ gt;wq?s#&85REB#^AZ3A M*8# l|l`49RjTb3To$To"IP= N,~0Z.v"aTkK xUSgOgSJ9|Նzq9X هXX <pT" rqV 1:n_(2(ᇚ6p|R~'@jȿ<N }8>TT ]~`HA`">}jDvC9GMO1/)C_gWvĢ$ɡGWWc^ tNvv@+ u\v^=gEfd:]g˞c[q#6!Z#za_sqzd=nDJC'S0+h.a誒9 6h %9@c ~X3#~a7j1F"4YBhRN/흳MER)%zz &>ݵ 9Iv?$%#š1\mz@HlL2*YE=+ҢH.y꼪3FC֣61Ө)a%m wl%|w+ R'>Po3HJ4y_HUnԝyRu @V[Br~vm&j4`1DGeQXP[&ZU9=4##]TӥO w FtѮRN^~V)A-nEHQ %z >cݒ¶볇L<]F{!GPD[;CUʹ=buYWmd qٵ͂yA&yUIM:yODdgFMT (9>J`9Sm YhG+D'Jpv&k9\G$2=޹y9UVxhy-;!=\'͓"lR'W t1'Q UJOZI >-MMrYۓYXFlmmYLan4,u9nYP7RA$<-%nh`^ A;,U .{; O <1Q!rl>X|K9"XO$Q(zk?ܡd+̷bYw(lg'G^+hm ?X:Mt@Y$ 8F\>fʮvrRW)|yć焙sh>`"ZP510pܱ(Ԫ ݨw ſEeB)go.a\BmGlKI.xpD]${ q`܅‰LWG4'eCsN\յC]>J NP 4hd^xsiAF2h}e$͇)b2ѧEz}Ep/ t!% +ToƊj%TψJ%VP(lm/Y!x#߱;ya`lb6*2*z)v%;f=P*?Z5j H\ ˮF4iRAH]:@] &RUtlBih>, M7ޔ`z_D[QxBeL6 bucE՟(`5m @UڴPO{* ?< 4ڢ`*V ]M'繎]Ы]%"84I<`3,糓}K?(Vz2S/3 P -ョf>m)3&|.ȵ䬕5h4H-ă͑djLBj@?pWG lj"(oMDet?Si".zp ’}BuXs(~,HF(Y>t?^[ -\fزd#H6EHӱGy;M G.L&%WLxDɊqYXȈu9?^e7~[Y枨FjQK$)Jȓal{ JT!M_`x628 >@o޷% 41{/|E/. w-Mu Y/gav7.%"0?.!0$bu_3əsbau vSZgh^ *{@:|T(nƹ|)r'c X 9;̺م!$ΟHpC1u&wЁd+*C 70ZJ`d&68x/=XۉPpG mꤛ5Ui,(˩R΢d8Ox[ݾR (+ C6eJ n$a}1 1ILI@ѬRK_MW3 m$+ tSJNHA>Avi|';f d +Xw8Pb\Jv"?М©1@|"w,%3ƕn㻪co0y^xfLhbW~:$QkqK^'R ;;rtpr.|i^ғf˜d%@~.ط.>~1uc' aC0"64qT>s;ՅQu\;\t}'s1 Sbp+`%@T1RjC gU _F9.hLG.`\Lzt)fG6obxѾջhbHBLJv*.1|dYfF]Y<,JXaEs7 ia+'F5ws) d '#ёpx4ȡ_4ach5uBѐ#q O:^yЛak>i<Hyt 'ES vJJEiYDX+ZFf~;L,2HaYX./ehw].> 2ǀTaQp#6Al[\hb}C̶D >NG#1] JyFhb$[v ? [t~IG0 әsy(9sky6geijvG?m@c_.%N?[դ1>4 &^ݏśySElLqhLxut|pO^HG,j*;,>H:2!(H\upMߞF\\Gծ]`~ m,toH.r?`懊+cKr|¶2t~bO7޽-XnD^lE||duPي}?A#8p.M?|[3{SrM-߱yg/2{wgL:uti֭wgNK^}RA7kcoOiY'}NjsEr@~/^n8}~oo~\{kZsp1W?=/]7/?)niP6߃I77o_u\Z<ݝ=k6^ ՞}k\oȃ]C%'ԛ]L< anR/(PoCqCOf3V|氺u/Ý׿bˇau''_>rA9s'0t QX Z k;G'g'cΆÍۧg>o|{㍱=ڛXW:[{m! ~r}<1qwJI>٪qhQKhT ^J_n ,}lUѕLX. w[ro㬆) ͵ ilpw!8˳4"aDIg`U[kq,4Kyb"ED'ো*2"QZ%EaAH@\CVRqZx[ .gOK!YK+,pطo\6 T0+byWHʴ':.$̆KrN6WCx`lr w 5;! 7GZMޜDn _2\n)K-4H_QLҳ[KNGRT9v2vԆ<3:C%,Bl0\Lߊ#Z2>vC%c \LbHI` V8"¡0E-@1]HDIYN⒄`Xjc0CHP X*<}mX)f,S7)A ҜO.IEbUW \I&⦁)cU |(Ie JKF( X'h(5 G)h7 tl4{HxͶPP!$JlxlI3'H NjKBBp7KPxm{$(3%Ĥ2܆5^,U" ؙ}=:)U{ +ԦUrȢI:YMpZkJE<&ZI쒃ݩM1!P vlxV?TuՁԏ-B@&PJ09`=Wňd'RdTUWNܼ2* ٤R= Dlk0ySt$ЈZ:YN`X7fXG6Nn b\Aئ!Ԫ?= CMuKh~"Ӿ{f-P;AbL& j96I|e -y*8LGъc #mjhS&LY55qXٳ|ŀƟL`=ul*gDC3JѨ8h8d8ô\8ʚӀz(D^U@ϑ{bV¼9hC0 dQ~ώ9sL,3AaQ)q:bC4SAT M6-?%1pC2-Q!e[*csG)1@k?/`P)Q a% *e5 %0 6 !+~3rQ9@UέtFtdU&%j`XQ-#04~mZ"0+.1LY:8_38EWMO1lF'L !!j#7d$,{-+Q.ʅevvN,'.n#29EPI'I`LbP`h)14*:)P^lT$eQ tL9*pF'ڝJ#H]θkEUž;ƀ>OWiGWX;ꈦ& O*c&2?IR~M .QfT&L&D׻*v@ɅxgQNW_D8zvJǵsUAk7J S> fiWK]V ho ~̙'g$- @Y (OE+/Z3TpkI"/& >stream HWnG494<b;nZcr$H,V$rs;Gor~1 칡ko-g}W@|8Mon)<-GveSI$fBYNxUY]]}Am>V;PdÛȲw Tf^E#}fTw@9b?x Jm3|t-doYL|X?ow]TMQ 9kv{@-NxQǸ)/~]de5u~Mk eUkxh{!pձ'_}ai/?O wo9"af#09+|J O/zS!V@3.86N-h9פo~W/GiSs唰/T8a_E0TKmSxFbbdHztg^y7peQqfH9Iiv@>M;誸xdղ;$,Jނґ9RIY>jNvl%baRay";J؞G3<)RR#\(EKT<儧yj^#mo3mFFmePbFނV%69o):#CUlX+Gnɩn4yc>޵Ͱ9vѽx] ܕOY~ojb##b\WDH}/q܋+Ol=? ԴU:ڊOnob5kujՏE7c}yL} 9[_w;'<Ä}C* n=jbE ^bg5^niQ &;^=nR@,?qK=gbЋ53@XYyK_YJǒB5h$0i,Zǵ=7E/oޗ-*M`^P| /x}}T6&^W騘^!;1p:ZP{hr[fA(((،؎؍؏iaIYb$fb%v$n%~B ($I,5R3R[KMO׃}&o5C37_Aȿpԡ.O) hH#ӄ4 AQIYhfhvn~H (0 0ȌȎȍȏhDa)?@{9'[,\!SIRcrMC- SSC9F)Z(Hei(4T36B"Q4uP VٔȾ@tMt/qE_PAF#|Y cig^ȯϹ%Q-ic@x-A*bխkzrә8(mfH9-*̢,JŢ,b[p pUnl(^P7JʷU]Ⱥ֬8%Yg%'%('BOAy*PJM*Mݪ~ՠID㙦]9Gm]8c1pkxx A}aor%=MA1l|Lo.v]+CQF#c< WQZo7cl>jpjsɷɵ9nׯm>foKՏkZ~B/%f~/??z}A Ddz{ gXRra3|p GC*ǔ >c%[Y__?}t0X1~H&O968EBBL%CBDE*PQ\d+QWmzz*7%*RR1OEzi!}E!J=2BN@yw<#NaDÆ'Ĕ! R]2L@qI~=! 7?9ȃ6p Ai,wN*"HgOYC_9 x DLiCnˠVҎ`0JmwdweNǎ "n^wžV|ٳt=gr}k۝N{E#QRG KL݉e(A!A%\U`Yf1\]=-U *Ƭb5i(S LU 6Vp 9S NxW'#ȽJgb(\`'U,[MV K=Fi\L0MeaHd$H,s[qV S+ݥ4ZijXj.9΢ &[3^Z"^nB9 uB0zLmT d&(YQ3J*3_`ԓFn˓S+bmcR,wLP J3ޛ,mP`s3X݁=rϬB;]B(M^BcKPQ!&$dIb@+`UZ Xq%&ނP1 )#G5]FdUI!!Zb6܁[7"P.;[cu(NIe%#꨾ihUZG5#h wfkj^mɖF6ph--![qc1٥3fUկ[*U}8ޖÝɟ)T\JyWeOj}i i/_m D%&P7j3\JrVwW^ODR8 A9>_$խ$ ~'A 蓆<aaB@ԣQ(4汌؏88ðd'7y/)y*S70T(W$ DŽq#qY)guPy㣶OO:׽G:m9:wPM獯ůd'aCm9b©}͞->_~M0?OSdMċK1K$&OGyz/>~~ywu诇חO߫OP",- 2;(Pr͢"Gͬ.ꥢPORMA*Iqo^RAR?R=u;+ɕȕ O EυXͪk&5d#DՅ LbR JJ-,I\b`RLjnԎI$(PHQQ5qJgX%J*3aY=52sU"/Ve\B( MqUjUFIn{ܥZ3oޗ{*y/W_mkH l[ՒX7S2 \D9S"v@tsq ZmP}Up8Zm-UZbY dk-m/Ǒ#~A67`Ok+/tc0 7"I*Q``EWWQ$sbʹ̝neU%"}n^7n_6F>i3$>(P4QXTeNb-h[,J{w AbSbKz%Wb^;wAjH4Wf祙]ȝSd?+߉I,/]|1/Y׮6_Pzl_37ӌ2̼fx17L=c6F2lcx#3[0#s]U€tgkY@߄`J' k;3A? H1Df $(T@ Hrg}8rJDd[:txDӉU#FPCt0euR:Gq (1V׫z^3dAbqG ٔ*v-RtwIbʏól>i-'?w/Dj3 1OӘ'6;|1<uAz* 1?X*?{X>PVoa`#|}[hw~6LTn ןy^6W[</S !M2R 30V2YSX&46qc'_l3{lxY^kc{ߩ)_w#g㪷2_pw>31N} Z@gJf;X2ijUfCN5A5h&?B>Fͤ5fjF8MdOG31ifM^U2AveogCʞ|*:E\;1ۏv)MGoqڛ].k ڣ:ֆPebd9coG[}.BLȨI%[)d1sJfRJ%̏NE؆DX]#@tAq/va-S͐ԮJmDj#쩧[DW" D&A#Zz/ IBci ~=A#Ų\~ As"x ` 7 ^DojEJm"mO5Ry4T8 D6`q#Еp۶y#vý3 JNFsM-fK_e5naͮ8VڣVN*6%V,V^B/E+RxclseK㢢z13 R" _Ef1x" 7D5|&nQ~bbbrKRA~d2 +:XA#tCx*4yÎ/PLJ\EYJ撩JM<O>iO[G=q 3Ys`)g,Bl{NP=j= A(١=rye^M!TdjJLJ#8@h2;EeS1E E{U82aV" 0I@%em6:k=f.CxN ,1 FlO/&3l#f 6cI[6[q !C q\[E8H>!I'KHP"$&jJU4lM6ޥe/?{[ǚ,O[~Ȯn=SfaԊWدخWT`YZR6RHUSqMŅkjQhPGn2T\ Zy.Pxkb%X&l6t_C= 1NرlBh*zyC"cVG!3.+7z)ГHZ'[`t۪U-;NӠŤ``!E 5!Mip=lg<$kSԁYRI|v쩳qݎƟ]VS\8tVF^ +r p6."""!U:vLZxm;2|r /(J+N <8G=6,06m!{>Z&2BOg*d-pzrB҃>w{uuU(!E29hdڝ])lld,>{vD?fWr Opޡ7b hH")l$L6,0& g{2yXDϽFt[:j ?-_A؀1 (SK)8l6hС2fղGٔAG1*!MAcۤ=H5q7qWcsΣt[8/ @dT4qa7Ό{F VTydj塌8l5 /LCS/;krL|1FErtF뮍hD;-*U8(_"~-Kv&8c{{*LԳֱT5'ku.ZH s =*H#\HdF:Ij%fIl'ĆB݂J+KK-ߢDd>M4 glܲ\u\֔gCv\nu+*1ȍiݙJk*4ҝ\tm)RUZҪfEj W]~haNPH4wN:cm 2bpUܖGd=ϨRO;CuE9'͛bi @BTh@ $-@Z)Ĉ c!h[d*d8:ܷuQ:87:&]/4uJ"`t93 { PBhi^d9>%0qK_7iq*X:rzӘ4Ǒ#kQ,ة=dk4z r:>{-#v[ ي,lC8eMY`I!  ,,h?&|Y.kY!i2|vY"Kf>`*\{1m.k*9$9FDimB*R8^@<xzkfj\Yf2#jo QU-EnrKK 3dpt)P֥NnJK+t*͑S\W-׎8A懰<Ϸ͝G-|Sq|=h]UQVtqrW}]>FC5 -Wj.I-_Q7շg?RsŽ>O{4UsQU˶O1| L=c'mTOE{1-x| \jǯ&n7`/A8WշӇ~˧7Ɨ>~*o?}[8t.~~hu2EWv4YG |/lQXj.Onjh{`^f0'tEO6>|$oS<!A[ Km2.wSl`I1sbv6. Bf]I؇uyFhCC$m&2PI;DUy~g|#p2!SpQo.ɲYC8͚)W aŦ7XIHߋuc[n6 Otp(:xC`/~ ]eNc ;.鸜27-\:C l* %rx{9kh4Lƛn5GvwKbW` q{0Bn ?!0 pERl"qe@8*n c`OTZ,_̱es0,K8>+Y!i sCYbIRJz#Epܱh7?%E_t'g/bkE?R4:0)%Ԩ=q ނK+.6XL}׶!Q\wx.]so:f}j8kG8 x_;40C``a*X1:<Fn A !Z<`q#ǀzЦuv`dy⟍۬mQF$|kE|g_>?>ɇoZ.ctO`]%#rAn|]xtg~_^>?|GoX~; !0!F  e -$xq[o=]%tv2L!G8$h4)bAzĊCb\i!B{dCx?_|) 0V@0DGZL}Ln\E@pyHؤ(C1A@PX&5\ A!QQ15chsҩ(ObyD/ 2>9E&ڌh H'G_d葞AgူHΊ-沔s9!1yv~稔e^9`K=06^oÙ \Ǔ]'f|5[},6$$C(@Iyկf\VTllo~sjo?..Y-y3nZ^GA/|uh5OOoί]\_~Y~ߨųWw7w?ᭋ>}xݓ6j'p'Y+hnK^I(o #AXfmZ``痳/}*'E"'Rc_YsR|;r_:Xǥ$XQKnnNr1VxA5 ~^qI`<#aak25Ӯl"ŝʮ@:FR7k6ı3X5cnYO2ø3.;ow-U'zi&n>4_hri=4e1ᬩ-]d⎄Ea~΄G{Idm48ޣb-H; `#''T ٴV xTLsE y3( 4l8)"TJh#=swcdžB%TTTM #aaL4vJug#lMgH%| Ց ha"|cMv뱂/>>Jd::N'6"BE r3ѐ,Pg:eĮ$I\k +\i r60Fr *%HڪYəJV/6[ #P@TBY 2m,'S Nhf&y EhU2g#g8OQ Ta`IlUGsfN,)- dAPG كrm\O_&]xpI* *Ռ?6N9TOn ᛭M>}ӷwWxy|vb?ܧo~8}Q/0w_돈P a!-,oʷ}n.BJ)D"X_ qjGjVl7х^FE"4UVTTֽQMO&w4\ØQQ2NIpt&DaQ2_jq$jSFA {6NLtSfGS`zҩ] \[h?G}c\Gm@7+eepvFGMOD6/Hlfcbmf#&^֪i}? fvn}+e߆#nAnآJ3^maXWc&f"xy*:OUHBi(Bm$-q|}Y^9Zg2A\#~D B%a{; D̫8O$SƗ՚0Z 9N{U/$ckzWTiJTהDch!(0AOچ(:ɓF@b=ed!9|_LnTKNTkR }_(W;z E~tن)H6$2ʆ&rs h?l~86f>icy `0u%|dGP\ =HXD-i]9q&PRt5F^ޏWpW+lIg@3W->1 $:1'ڨU A%~Zc׷?jcdGNi%?[yxLgVni%2Wst9͚0+M[f(gSg@l|`H~H^9Fe:c`:kFR\хȦ+@ gM>"PoI.;T@蔩X` ݇R/D̅3Je^C+wPtgRfQPUqk6i̘+չiv]']w)lLg 0þ?nuD3EPbPM̕qߍ)W]KT!5"xZb_g=9@RCIEQ[S@"tNMf .ȶm4:zi] m[|^~K|B~_Zql.@S׮ubvs56=e+ikPA(leO<{UуS]]EUlB$߀RATz1u'xn2މ1e` yߕsnG}+U XWuTS;1Xz(B:(TIMh̽W2yksD+SGUF(hfd*gPKG)- /(\^OZj Jbe5O)/f{$6O 2,QNF^}ؼ5ՖGS> q~Apsi{+ɱtaWZAч1TDt( F'HB͐Iޘ;|W) P]%x!9뀈B06^6QG N U~Nx&C{;AN#EǗ R,R4s& ͙FMFVMf银0ղq?*Xʰd Si-t 9_1Gt$e}X[LE#*wqmZ/A{:B<.!=&iŬ:}9 ̞FͧO2xoỻ>$g 0fBi_j+Qx(y]mI}9"&duw*T蒑fPL=WD?yf7Iith*I_¹EB4 <ɁQL2AV`} gSf"A-bL~W+!M4HB}kA-)yנx@_F*$7:D물bpuu/)`n!JО 8f@) Zu 3o*$cHp L = Kr3`}rO`pR^Hp8`ӌhJ$$#ո_yS^f 5SHUܫ/:'( u2}l@Ǭ`CNCDIyTHMucՌZXYYi$2P'Vd]ŴWBcgEyL}SuP/֍s#̈́$N[o!o$&O(&7jJbLWd]H 0U S%K"vԀJhOspZ-w6i<ɤ?Z[=oJѶnRmW#\xl1$xQVrs .V@,UˬڲH \#gݺg0Q C6h`~e />8X撲rMƚ]I&\LJ^Ӷ BSngc]*k'RD 0UXғs1>UJE2r690ί>MI ɗ}JчEԻUlS! GhrCK>a"<=yP!oLik]L,'(Iy ب"m@k/ Ҧ* 2$x#lW:dȀD#xpDS-eVLSKSq(H)Ȁiʮ57%)%~ltڜR6&-@qZRI$nBAջbtV^^f,HޠprfR!GC0JRBDl7 2"/̎Ь3SI{RsC 38UI @ֆKT 8Iv>V D!|J;qJHPa,+,)$ _ 5OB endstream endobj 30 0 obj <>stream Hl96 DW=(qCsܫA[ qP(k\BWwhX?ٮ4B~cl1kWek؝,他Pnss)]MqU~Rr; t bx6Xaiۋ xsvk:zZZG!q;6~:XbVm=qg_1ӵrxD4w=:KBz o]0O8BWwUxFJ6SNxAsVBשF~|eP0ʘUde"p!XWR=&#(y٫>aL8#_=l8|Ssu' To˔5\Ƥ9e e 06i|D~jT{N_sR 9|6{~Nz9o"q+I6.l[pU:oK 'd:+4z, ?0ߘE5$h!)HFk:ۙGz%UO% |=IChGBhd"SER}}aT:+Rz96Dbebӑn'HmNmM؋E͋LfE=Ǔ֖%"J}#!@QǣQ朡ᬐRadDȹ-ц]q]ZJ%JQ[ZI c$SUu?@tig}g:Ԥx{;q5궒Q7C9?}*8KFY=5n[Fj!x[s Hcdf)2Z>, o)ә@0.xM/N?6{kO2 t^Os*%VASRĨD#t9F3lK%t@M\:ase~J(r k=:[[u&Hs{t)VTx>:s'׀k?W$İLo'JJ{o'1H0geY݆^VhlcsiA;2-YRg3Akͳzlk9=5p4B}G%,u P=sd mMR'8Ni2>…2rጊa>sވG+A*n*l)>$2CSlj\Ԥ(E]lYݺ6U 1b>}Sc΅]twg\>4Q*A\vҩ&^߬+{,iY>*nD9(R۝b=s7 T/X}ߟW?I ܆ӷ^~bH==S0w}z+gM~Op.>|/Φ* $Y,68;gȤaD?\9}>"f#3-ÜIͨZ^\\gSlH5&wٱ[FOVenngzk;:fpGz u攂o9oF 3 95=B02,I:عsWH{qA2Bjw'9 D> 4E+BD2`( ycdٹĆ$j4;]]UP#05FG#Q i옰:krts1h@'ZAGǾ}a| [YS^LFM#ƼNUJtkI:J)ӝ54Z&(aϪ )..B(E4H]]fHr¢ QnaL0O酢k*чR~'%yq1o%*5[MPjD%^I,9k4|Q*r} EySofݓuK;^%L|g}digUǛ ) x"Bم?k]^;͵\ڛ׮uL9j/уuX0>ʫ}AXS`iymkO4~د+@#oGRrO{syFzx{H7=.w0al#wϙ+^Ɣm!ΈNl'i^:KeCߏzge MmϡŴ9inh)X9è+oI)M#ym38}u9/; ¡H\kqR}W~LA1m9ܲ >1vws\otIuI1 <$Ys.jzbp} qTwr`ŕʾ`]ٮyWj<>Z(hDI>G6ȣNX5FZzZ;vHE{8a-.@BV9܇QoQNH;tv :bLc+C!K;X% U4P?qrlUƓͦI=5 $(f' 9दYI_ 3L:1ЪPzL:h9݇ǥg} ۟HFe }%-XAK!rCyF؇II"۩v׏~^~ߗ|}s}8ga5o>ؼ}mgvyU}K =~}/z[޼b&osg˛/T2Ӫԗ?F bK 6RR\0XrmP Ѡwu]ڨ.mEbM`UEGMp&ˠ)G@yMc^O) _}S1oP͏Σ/O]F`Es#r {B=-!g"L\^o vyr/{J`.Гg^>[ {]^tJ{[W2ȗLLft̆D /7amT$wJc07@0_@:j]Fqʭ.n%uxBrL3 aNѿZP%v9@Y#"TqD5gHz ])**yy5ˁ{ NTok  2';(Eʔ\AEـ=qS;>}z7W)lZ̽/WM" 5T&"ydMi"sFsxo0=>u&/Ռٗ UDAUZUKU ,]Mg5pZ 񠁚r $6Q.UC$}Rzre<^< 5AK%dfBRؕ[-*A8R{[1!G $w!+ꡣjbXhg{Z7u P:"i*- 'g( XV0q$nR.µE8 hql҄־v\)]Uv c&׾Ury, i0X\} 7m'*ZOurS0I􇍅Wjwu9b&h+R矊w qǣhHaIPgShejU=Mlɯm s]U9g e^ߋitYˤDHQD>jƶu5)e>g^{!%bb*x]_gxꐐN:`#iӕ⌙HtmJRZ)ShV]uWwmR]D(t#fkۅ/W"w P${O\¶ZAFh} S؍B>[XPou9-<[H7K4egbmDb]4wWdms?P7 YQޮ sǻ,g1'.*,xcZ]B=cJ+X9Wd̈́19NL+0Ė-ՙHV^?MlhBҡiC@+ !iϞio#^U MźDC&'䶗D?"C WCI~uV;ꎍ:Ƽ(,wJɯIw +U!i|yՂ߶a'n2W-hzejUĘrZ6Dn&c8L5{Ȳ\34'=yF4PF>[ 0$"k$1dlC|ǐ F]6z1=hÑ)\,ڔkݹpf\ߤ9ɍ/ל!Y\($?ϩ6AsE&[g}rc~^,)mL|:b8 ^`9al3vۍMuYR]8|\9#3D*Q{.HN˞/!PocGIC*RHڭu_VqYZ޵=Еn:a`7rdXZ C* ˆ*҇ukΚqv6Dk`8=qڙ}gv8fuGMfv9S_8kıwv<-fVF<<Won.߽xt][=IY W0$?_<%)iTV5]i՗߽{oo__yG?WYmzqZ[Ƀc|!pC OkЁnQ􆂨`$gI}(za5xpvPN -:ehBzAx8=p5l[@yէ*S* JA?~><8=u[#hLJ63"qJFҨ~o+4tq}aF6e %j'U@H:_wͼŠw~7a+bɺДv,bAd D7΃*PH֝9buB,5]-gYHcJ{hIn!Y7̳J;@ Fh'\ხ0?<C3g:ab˞2ԏ׺=DaKU@vE*dVKT`nv,l6~lgFܣ {ʭllelCfY8Z{crOH*<y۫o^^]^M{YBuG_y(smF>}ފƿ6B׹,IʧxQ;j2 \X"W"r|&{kDgqgO>q;\N *\( jd_0suBZ' ރ NA_םQ+eCPVOܜ}mCKխ{8KYCGʗJ Nc]a؛RHwӖI /QJxVd&`/"?Byl/5+E oI - Iؓc>_~3kPv=!#vVE~.%?F^<9PTO_>~kp16T$4zÀNO#h9vT#G6@;;7 ;0Z;WI+MQNrtQ: AФuWgCϮe<{Ref_\ i\W3U.|3I |j$ 2'=WvʌU (З1aPBTt%1i$nbF[҄f6W.(G$T$1tZr;}^ UF& ҳ B4aNwXG bY*2?ҫBOTUrh {6DxTH%I3+]MYJr>u$줓3 6?}d$? 5M E@D8B)*p 4o{e_GAJ;#й-B+GDҸAҙRMr&5B;%N; J 0 v6& 8KG5rh>@ȉ<5 w^ +[| n=N}2ǿ4fp:"eКQB@Th'zJ@gATqҽ*<7 IVPJ@ЙꋝyF/dRw ĠD6T,+ɰ=ŝa.B:0H&b7 0'E;Æ,g (Z H#5G uѤXZsbCiNO=RlMQQ"(VcNW4&b5#ņL Ib Wi}Qw8#32L[UDx2MP~?dЖ_%Lǒ)K|ݢu+̭\$^ Bd6,_T`+e-6oR %<4.*Yc_[2`[HF0G`Kc>[ B[c=^ Pm-oXh[`eZ~ \) u<b.&O-&Yr-ْ>SduzG Ra1շZ5ţ*GW^ŷƪ )Ƣ_t5Vj`ziTPmp- r-)mgXӜƊ[rGXfɯof#(8[9gx[5Jk^ؼ]UxN-uH\1Aj:,)0benIi@^H9+AmZՋmws}àN4<<&gTNmfR_iHfWm+ PtHnqWlVɰ=a y؇-pa{THF;s3b26a>WU+Zˤ`4·$X]-z'X_MU4}bHXK,'2Y9<ُk(n cd\~}ĪҟĒs{PC͡xXKSHDU94At|J) cw Eel72_Rh,iW Ge'2'F3oQTb1 ]X~k;LBzИ$D5cچRИ)ZCamX^94%iÝCP ;31}<ΡQz#ʡQ%!G'Fbʧ6F ?dDa>,ECM$RUNh"M2Jv}> 4IDjG7n H+R}eZTp$3qve KTCg-/%2C %Ɏ@cҲ)$U(fQ2^"l "yN܂Aq_R9Q~Km]lpu/-1YCh @)Y Wo6a$IA`9¸e]-$m{<분JY5雙0P,uk\sY1T=徺|9g :gLu4 "Lta4\V]V-B*>I\O{r5coIX6OطTՁ4f>ҧ?xC4<} ńRRDԏSZK1/rzb˂lk { sAKlBPP0Y$LBW@`0Yʦ|L{,1öhC49KR)YxRހz8hL%ҁrċ_sy Q!hl MHd֓fi !OL"ع?XJPl\a"KQfYƯndFڲ b1#r!Bѭ.dnöQ]+,ΤGt+?TܻtO:-%`_A B-)PkHV^Wƙ  "ӾlF]4EAfTetg C!ItSͱ+w=ⱳJmq]ZZNŃwrѰA 2'cmYk#IyPՅvyC?n] +L섂SsA0ty<=b7qrݰ9:O% <^-/AVUԚ^mk-M5 ۷i,o4\#n0EC+,֫6׹(d#_Fk$9!%">H@0,bϡsX}x1߬wxo"HS4/V sJBrmqOk3rJV`yx,1D0v~]b@ Le/%^)zמO{ż\wfÝeCas[#\ *mP.j0r!O,uu")Gbb,Aҩo|n2ԑgW ~<5+d&zSRknr u6Ù {LYkTeۜcԳ¹W1՗'@P'>$Y#XtoIh"lO,^m7V:i7$-Tx)eRd21Oh=fG<}837=:;l7zhQuRQVշi'HiF,UFY\`diQйxru^9Vh F[mpkkZ̷<&})K|J>P-YnvSқ}YClg= M뺞g'BJSdl9$g-(nφf;Tf|MD{]7oI=dx{ŜL!'Gkq;[f|֥ #k(i$6# CI@rMCO1Lwk{[%{%#S&TAa)y&myۼ4&|X&f.:CNhBR9OefeͰәtֽĠ(42BlA/㒶sLc.ӡB: e'z$G8VQmz WO]b|tgu3z*M\I^ϦmxjR\ų\hy|vOLyOX4vcn>qWؼ4]۔|x)2HixB+0ۓYc~c) ˪z2󵋆)l[Q0xj?,yjadwA䛽}1Qν6Oz[*b^&}f*]66R"6s YqF6 E{^n3$FqH۳@Ol1T KFN2۰~AYxrZɹL5iJ4/<:'[RqKF _Z7tK%Gxrmk(q0: l]oM]bL;_`P;kUo(a{n{AGgMu]=WE[$E5Y47T r/ۊ _ :DlWΜd/_bD1Lܾ$,4}+Z 2w'[D)+D zh,Ir (Hc;c鶱8LNχr+J C+UmwNiÙ}+<TF})M5Iʫj_}ԩ*91f# 5N! !'Ĵ,8 ޱŸ6ʦ`mk?ј&"fp ID!x>)2zpf!@'4ر0Rhh/θG(-TD+0Q,]Yfl"V/2$aB0-2]N8h͞8ӟkdD)lcJAQgMw(1e={h~N9 A֜IZ0qE\1%c>9v٣6NC)p+I@q%*-1i6cumП/{Y."rCE[O2K]SLP~-qЇ xn""}J u pT́mܓ˾F'9,x($b{m>o5)Ew VvMIMu J,J '(y){Z2ޮ$fKf]P;shWtQHd`p&ꏌS-bhإvmh#`hʨr^OLu,\M9No#(#Hsm#Rzge&^I]̭/f% g bS4Coȟ._ٟWvڒ$Ϝ6 xZҤL+l&~-e*'DMsD!ˣ2}{,fl;JKFܶ^ϜQ\|4L޳oZTʁMvwɋh(1ô㦋. σJٶJ8l:8^#yݨR2ߛwZQOb9){9whׁf+)g^r[ď粈H԰J,lq.}eY֝[o{'?Q/" D;fIm.P3qî^Ace\xsl>Z"{Q<d3lFomE4z((?XbqKE5DS5MBp!PY[Ck*aШQn `1UJkr8iC(Ne"8yY6}m}6 3Tf&j9Aj|SL\};bz_^|?|p˾K"=: ,QС:_g^]>yUHk/bï&^]7ot?_/oۛ/x]o}ӟ?//~~r~Ҏ_~<?tⷽ_}x6Wriy9ȗ//%M_eih-4JOs^/Bug̽u/Dؾ)hjgU6__;|wү@a@2g)'ml[5<໡q w!wNL</AHK.:%Ȍ¿;Xվp_r,L{Ed]$S|+-Rȏ{/>Ul} NrsQ0G.&PR&~,&f"{v+"]P5 u 5#KIuШ  +"[&7&N'$l}CإfIkyB^Jd6Yb'0g.RTT@ ˷Vڙ;Z$LUh9Y9urs[[q@mN:y _RhI(uύSd%0F&A2mgPM;Z]"drێ*Җq޾r]"yVk:%ŸxctC˽J EҾqDѸ@ڸM;^ 0j-?Pqf]p=9+Аu &o( o9TC⎮<(:i+ (-IGCP/FTԟ)v/,Jݟ<iWIV_Q=~T)" e3nt[n'ěP_ ՙ/Dq/ůa4n* sEixT1I+pF 7IF+R:Q vO8 65G8 (f̂#^r \8‘^Q7ʎG{uG<Җ#07!i{Xx$}ZIB<җJGH!lG2-/͎"J/ R7u, ,,@̪:G[UȈ"q(A4]|9G>ڐ5k -zyNg/>j^uiZMn$BHHV^Gw -n6jnHF% l(ܤ?BwIb̔.*`# 9zsAzi"䐱Gtt zK.M]g7T S4Z*۪gjnڀv%$%\/,} , =gX)hźD,ۿ$nV6(=(i2 w%^?y-tVwOZiX=]J7qYxzd콙ڍ"啎Vzp7#2II(}'mAa4}k >1*@<0%ЮSYV"Jˋ[iOM{}Y`m_zpm[jЫ*[41fLZO1$I[/n޶>-H=n4.6:/k);ӐD]1+"Gu#LP=bXD.ѵ ,qnܤOc[)M|{r $Kald`g}#vf&QWfU<Dܪ=bRhY?*Q=`Q`l]MrKnv>4'D'3\,С$m )ADz۵8Ң佼,Cb6jlvC+lؗ$&@ѥvEó"וa[}9`w2R.}+"b(-)f&yuTٿhE$ d" /Rɢ&+7B.E\RHC+d'dU5E}^_.Y .Q#mh.q >c b6 C{â8>W~^QqۗF僚=q>l}_u}+p+?a^?~?ɗd~Н.LjC@*6h-z /H㷄2̧DIŋE)0HZ pCC;$E1)V97C:3E6u\0+:홃ʘH%`d"jơ=(N C .'/]5J!N2DP4QB̚՘`"ŌdΊ+̚ wǡFدi1Sq̕,0ZJM@gjph]`fQ$E6ޞJ$aF[J䣪X҂C-6뱠k&laY} {jJ,{C~CVΟ@3RmUsSK+mw!9MߜttIV>.G7xql鶂rFf645es CĦf6 Oc)ͣ!3kA qw*-vWw'l]sϳHPl+T\yt(T88WGz`^(ҟb6Fi(1-F3Czהz}z<52N{s4=IolP0#I~mn2/Iq=3 eZ0Ωj6jɌv&0C54`zM ˣ\dg [3_54[kU^M=b@?tt :M(3;y,ewFYyLzy4%xG<}i._9Qz0ߥPЕzH$Un1&*G~]1pWi PzxՎ⣠2T7Őn ʄF/*eqo] !cFcWr3Jb Pr8g!d0 3ve:ݓFHMlFצp+u6t;K…m9ktz&~Qp,~/\$Z5iFnwg$tYthr%Z~4.WpaTUj9R}Q\MY6?0@-*Np"yӵkMG2QR-dL4SΦEdB);VG)lH,YѐD}1'"aXWT?jF Ӛ"*Ƅ*iEC0hNUDڴ6Juha?f%mpT vӬ$Z܀FGPU`iP-_hULdRzi0dx(Lh9h 08ŶqO+}=fT$i<+Ae, d ONJnHzb+g˙T}/W9lVFrmqFp|=J:WaS#GD(N 4. B*kW ,A}چUY'u6|ۅ2jdq9RW&ޫ$ZG2H_E̱n0OZm%/l)I0ﻛBv6J =pR)pg+w^  JE400*W*%n:VO`TW@j(֙- cm`Hoy}O'ç(pIKdsÓ{c#\ٮ7?+nɣWxp܂߯+ABa7gO-Ubgȟxqws~=zw>}}7ݧ{{滇_r?|o<_>==_֣xz{wo=!=ûu܅?u<~b*Jy9ǫǏY7}-ǿTCX=/x7 cɞSh-3M'|zDhXmФ8i@h;:Z'Aa@ ?t|"17&,݇O`< ~@[>t/m[<AXqV tj*3bb6f+Bfg# HV҈O ,>ԓ''!_Fmm[@ A1*-wmάu~H[1Xsw |s)ݴ>sW)" Ia WpBU׀WV_ endstream endobj 31 0 obj <>stream HWko[%\}$M:8 hvX@Q{{)۪°$gg\Cte_僯޻! dC>% eRT|2OF`K&Ju\ `MiU < xmhřҐb"9 EFD9P TcB&PV@@K 3h[k|PQ9QC " R`|T/2HtMNԠvDŽ:'P&Q0C' mûϰ yxPْ q;0`K h񂇋 3DxS<4.I9!@B4Bd0$GfQ6gÙzez{CrX㒃; +/H v%"C1M [ 62 YrCX{ v$gK%!͊="#0Z8 CE9 =VX#ѶjPMM] WdvYTi@{u6Eզpv1sCq#Mt݈ДUxdfvHl ޛetk'׶$ &Q,c'1)fIc}ؤؔS1rSOدSMkm'59?}9l{vsby8zyXz:5{a뎅>[mar}G nmyASy:@yn)fd~f~헨%WhpX߮7o9N_n>Rað/^^`{tɋhɽkjË˙,|x]ufۑɋroXvr{qg'uμSyq_V? q|g/kV%42!K1:iX;Ȼϖb4Mܭvf+@VfNo*6Hr g1." c K\sOM&buCOžG_=bn~qbZY L H .h闙J, *`6mHvIrØH:Pql'TyFXEt}6A^毎t,hkm_(Ip#ŷAtvyp`K)tRӔhXיU0IbPDp*)>7$ӉxECͶD| =YW?T>|b-Np?~V.-#F^A#)|/Ht)3bEx@T;^<)Pe±Xcm%Fq7PuN-SNK3z)\ ?jm"W)҃r}z܉0C_J=7L`Z%raH+Lѱ嚁%pQ;PLVle*⧀-q"U鼕Dt"TA ,J/L|LVM]!Y /#4Z,~}۹VhVjkjj4u)м;6悺P ĠZy! (\ǂV(2$CUE rƏW$șǁ^DY< 3Ṟ8pEdMeIVm[B+Β{C'3bкLvbCӇ>AmlThR3'B #̯RS!X8͘u=gi'+?G^,dp5g_u3, X)_FWX E*Z=@Ҩ D\-\0yV Lo?Xb܁2G@0;7Sa(VKf5rI;R;ȇT"Ag)&@E 6p 쪮Q(P38dlr uH8j } (/= qľ*SH&tܷ DvX@oPEni25"@h[ˑ߹:9v0!g'!4W&9nH `^333 '*@WrY2\*!Vp~$AM& @R]6~o0ۡSvlن_^FTKYU|X5Z+?]lJ/^ sSi@)$ >eWOEѻ'X)y2"Q=)kSwag0^\ bI 8q"ԳUY;>!4KUm ~@) [O%j@44h>Mod[)Z_j]+ Vr&8ˊNȣKGK]f\uɾjN̘ujn j3ۑbr#Jpia7V .Mu0ڠdS էa\m,VXa4zzڐfXSLfU=nZ?A Ӆ4B2 aS"* dqe iHmM1\āy}%Lu|*ǒ}VlcpU `UwYG@Yn&??ҨIl lI NCo1q~_OJnW @PŜS@ؒfdV3 -kSD35L *up\}?@!E |W05@.(ʀ^䤯s8HewU1i|S=ӽLTo9yY8k>\L5HmKZYTY-'yƼGjZ*mO}&["` 5.Bv󆭾]c($oanbLߏ5YA;C*ɉ,_2wt犁]yQtvf*faTGDXhӫ79Y~ C;PGMH[GOا& V )Q) ._]\{EJzʻWaN0΃<0CҪD&x7)F]s OQZǗc1 /wAM*I9bXw/ Z̙&>uϓzl1(Ȯ8~B#nS&וeir`%vx)O0$( 1u䎮5Fp4}b'M@Ƹ0dpT?t5[O~K]mk'̸nVe(/`fEjQ~=v:sYFCߺi.I THIVFvF£/W)CiqY_2Tjm,~\.SzpKf^'UO㺣M3GTc?6$ވͺSg>e/eNS `H5(qg_V>r8v !5w YY#bWg"&a4GܶRvct ԝ?SxM6*)-S`ZxA!2 "1^ &iSebe{5H:.SAN9 ӈE"3tjNC]ED87R^e4gwsUQeT/9E^%F ϻrЬNGl.T7&!v^(IM ' Z`s-8DkŴ1 EeK(!tńJPgPHto!N9ㅬHLH{'!kJ7;2mQ>ȼMuQ4zP%!YaMnLxV-,C&;ɩuQE܃ fE F4n1?Xxy*ixSil6W$: 5kDs 8g 4ꎙb [Ssre)E62H|>Jiqh/ԣs;E'sH}aVeoF+*1N56Y[.ۨfN"GV9e!Vev'GCZɨ؋Vn/_D(>μ z,Zodۮ%8f%Ȯ!E/|uuOܒ.̗F4]LP 6_eW 󊳢+cFRY=y!糞SVi*Dd",qYꨕ:$O)q 'aAq mm6 n`kDo/ 7T+s)zl}{Y]KL>2 pQ*fz8[ Pr-2B5{Q,y2džS{d :$ e2NBG@ .m7{M_$v]9il]E} N#lt$9}54@Sv!9f90OcO[F^_; қ1$nA󄧄w%NJ253<T6co\pVgFe _{FOd5 U% iz7L'zŪ89YIF&]FDQOeWك#vHiI" GR]T$"QG:ѥ.e$hx*aT㉗Z ,5GDeȽ.CH[j4!%qRuS.OO?8ԭEezꮋ@$[9߻XoJoز{("@Xyg*b Yq8EѪQ{&RzoI^f K`p"5^UY65ĴI MѾW|Zק_~_|?O;ˏ0%h{Z4&!ޓlfD}ŵbnb`aa>`0F&-t[ ыF8w ȹ:@63Zki:2t@y)B]I3Й|x; *H0- ]r`5Nnƾ mW;oXWLOtP N43UhRohJA40T.֚wf/Ϡ]kCSU #߽wSIPV*~jnܘ)'-lzܑGOx^!ZBuE" ܭH>#A$-$Dν+~?'09|rhMn0T6㼈[:1?h8ϸcpsGrH%YeqH}$7 %EݵRx4pФ@ܳ h=' +G(xZ:g¿XɁ0PJgr9y݊6\+~"F,͐ݟC{)&sn\ a}c{!Έ x49t-n R("ѧ5}Zi^G@bGc3r` S6bɡ'˯nF4"|L\u95bO7Oron v+N,p ޫ9,8_=/ox`uhE5՞4ˢD`^4 ed?Se ՛삂QcOEnTJӹtZ,ՕVglkD{Pg |-:]_}~v$9gCZ]+$iqJ R i' `4щR'm\zV|AS3)wG5k#ͫ=bnÓ&Md5x,,ZMpÑmH<ՒQssXc d-U`kiE' Sq4Eru7M VuF% )p^]71PSuV6E.ǘXx@qV>4O9_&fg5r# r5]VLkKQzK)F%{T1}-- `TĮk|zc:mԲ[*;sz^zY1 }N$knˈ+Sԑ(朗_ R~n蚶@cOg}܅C{{ECP+ `ĥI!b 5WK+ RU#Li gYu$j"̆zCJy9Uӵx2,\F9S {йf J˙W4҇5Giغii(eڞl%d9zchR 1NmũЖ|-ǣeld[qWZ`e1S_}ƴY5QO-&yƚpe1X+ ߣ "F!o&=6!Nxe0DS(cVY I|ۭ_i(t1} Ƕ\Ngq^\9*U"4Б >|c [ 1_3 w{e5aҧQ˴}2/ ;.ezBjQÆ޷6 =,[Ι*I{DGcz}t]\fՏj{Uº3B.;3vjC'2/ #a,=}Z{Ϝ>q+T]dDM*^rR=B'@OP.mZV $ ;ZyG' F \2ݦ4S , 8!:BgGTrFQ \ mLtJԱ|E~Q] cM/~7dQ83P3\8?R!i^n(q*J#!Bhs?F0}sǯ^/";r<D6%tr+-kH/ {Nqԙ㬁i,8m^> ~ͨs{j >+ijhĞ[rSziqK$L{:ۛVy4% ,b »SF)3@ea.m>Q3 -@̼:Ȫ{҂D~P*YEJ=V*K)P Wkj}Ҹ 2b!a|6k$XgLZ8(W6j-/J$K#H69[Ɂ%UuUW{di:VQrC!J׸.-+v;nBǒV0Ӵ[L`4>|A' utvOyu*ϒ}-bm3WSP *+\@T-f@N euVwTG#Y+vƳ^eNJOg@B[l ptܶJJK >A.ͣ !Y('0 v*:!E1ļbИ[7IV dL""|cpq NWlms\U._:b$GHfj&:WtdZɍi  IqX7(_0 Gc+<3R`9 P6GD*-%yJĠ."M ƜML!kuuQ D~|!Db$ قml@!RC.܀VDZCx%;SNmc5TtvyvGd0-]BF[޾yRHT]@_!5 2g"a<.;C"%9Sl(՝P5/} z2u?|~x/?'A}IBwo??1Ǘ/^/ynxtz2F.Z*cB3H%Ŝ\t&lAiQu)uGՖ d+GE H2D6!%ɟfaי}/mFo^|>B;Ur.$Q7{ZS(FC}-!)/}?@f ɴ<H9%l&9;k[C魄6j1# Y#^T\YcwU˛%8dzOSdbf9 8q[*<ճҙpƠ%=fむ\Z \&4i?97ES7u*JǹԶ[jQ"FȞk@dt QXf-ի3=U{MYEжɉBBO=٦vȳQcyP{ ĬRẃ.];QgkTH_QJTi^@1IDK[+LƓ;a7t4"LE&/hr tn('Aޞ9#duR$*2L;`~"}FVϊ0#ʗ1,FbR=sVvFF&^@6ܱ=[3JT>BrĄKvbnLfu%o$,`U>,L A 5 {؞a,%>kK'I>r'\)R< mH.r|:=lOalل"2Z<;x3/TwTnCfFhP=A%cfBӕ>\1HŶƘoWBaldކx>O)"0!43qIH.V|Tš6PAn"`#Bl7=U* fEiWu$`420"mm؏.ht$xq=&fKnjk;")2D *L'R^x1PA]j{> %.]E.e{DfVДP [=-+\6JRQ7z" x1zu2Kٱ>/LP%9?m0\6Fڠm-*-oDǽ˼֍&GYl jAp!ׁN=.{9^!<u > T[cC iF֞|Mԧ*͵NJP55s paʴ0*6Г몷5Z*gF_Ã[J>f%̩~4.9Dz^ОDW^xv˩r9HZB(=N a7xgTdOWODqqJth>״) +J!Qco8&pqkN9fe r.b@3 uiyq^:bqCuM,f[4>Z .cblQtĤ:j]1[iQjUԹؔ|#,q :vޙ(B v&G sgTপq='{t">)|\jfwmr#Rk*2 EHcn-G@l^s۪ R5g}) e7Jٛʘ-sL$ucے [rZ4~/DHhaKeb:Ýs|ӍcqQH!|eͣRCU&C{$x_v:n]+5QL!ACvos!v*d`tUSRs[ڕ z.PeNAյ K lᗎu$@ a]9|UƘ8@̖D ]ݕb١BA;ێGA5ל(`Нgd2ڦ$ @6CCNH mP|Q]H~DC?6 u5KQ$w' y* * ؅D7˔ТOi+ZQYHC$ɭy>OP od]^Bs#mMi,ءD_5^53$gY?wTxV_55 <뙹.j1.Yǖ^'m;v56WZ'4$E"yŮ V ugvjDwǔ\>N,Vǔ g=R~ 7\jYn0T*.9]n25=e*Lu%mT.& ecyF (Vlݍ07)\Yo7&g`mG;2ˑO=7_d,kpiyA9i`zPF+@]P/T5*2ͼ2Y;Vht y-N ] !MSR6Ximb.}g<}o}|@~kpUߞxǿ=͛/~of~?,QQw^~ -Go/?oOy}W>{}+6ħ?~ՋO__oݗOn'{}Yx3Lh>l{QCk9`ԯX$`\3 ; &+F-8& 9stJ;0 tsHWia[xA(wrtDH#hIFJ4|\t%]N-2u-^Cd c1ԒEV(npE 2}YZJ]Ѥ#H-pz_P\r @aBzi2:B#qܝ8]:xKx]vKҙŹ칽Dygܨctͺ w#LWn؇1mPqf;x&Trɇ|o^J;APF Eu)H;Zs `ŒL{#'&i+`s]{ 7{!dDih]Qv.z@7qh83IݘNkv LɈUyH9n3BNJn)`G R9QL읒y%Sű5%^W=o5ROpjPϙ0L"V|)"7{ ^3C˪I@_eN7 3"VJ\YUg̎1MJZ^8fQ{N,TN,MH;gBl=nR݆ 7'MJFPxҬћr4J}R \JBY <%n^ mb7փv;BnTp3X3)CwL씛ݳ3-}L\ր?l%^T `_LI!X{fJFD֡nRMH|:E^Єi 2*,Ug@N\ 14F5H)JJn%0/2vWyT'd-{[+CjhADJvmEpC-65ދOCmE9BX6m=QҬܦ%W `%62hݮar5d0zzȸ`i AhU0"$aMnIXU P?6~єzvUsu3.U":ir* W6D54r3'C G(P\ѦR=2MuZzAUn@vԀpP9A95$.ݵ|*= ȬŽHzT=,܅cmW5*nd(ᖯ+RɊ;iSTʁ Tv [-F$".F*k1)1 [ B^7P|ӖSkɣPw5M=Mh+OP6Eh)Z/TwSej/aH́W[*7׫d_XM~G 6@jlڍ Ĵm,4.=s6S=' T=<'lIq[*Ͳ |>ke|TKgLv\Ɩ̀hrID$4sOPqM1SRWh>.v j\5tkvK^&Up -82pLIOY6 6[P1c>it2wF!aZ`}]iHY\F}ϣ9$ot3r\|hxZZpR040O7.Lqz똯N9V{jp6QsQ8 aNk9X%]j>I$=vy5jG|SzS==S\$2'qk̈T1x}mه7("B*,?e/]B[6`&Rb|}jZgbS샑#-0v!y]1;an5bLqTIM0q)Y*i|z>VFԳ?@IˆpT:9UY!+'MQ= HWnrY7YzքJ 'pW> !I0 O!as^ w T1^98y0T뇛t1۔#yqV.&K͟6/ԪW^_FroI%Id=V/K[_pHy%[<%__41S "n(7ueYjG\d@]r~eWKM0$(*~XMZcL@~&vfiua1yT:]q d3-k!~ i@U:wMR=nZ9wwYpA[~iCE*ŽtW1Mg `и?G"4nZ(Rk"%LVۉG-\̚a,@7 ]c$pX\)=0Q[6DsHRuvM3΁?dQh*:Y:ETӛgaҿp]~Aާ%}+<_F*Fw@6V;f2'^>컅OW' }[)שm;cte_Y_)IГve%ji"^ z]iSev.SAeI h82FfC]qX@`'EJk05rnƝJ6(,3.5f8_94|f3 mu6Qz  Q"AQmHq5vE-^)Oy7[.+G"#@ԫv 益Hq6v X缹V!PD~ *Byq~C!ҽ3i9^Z si$dS[܈Rzt+tpPB&4EgCfBd7Eˍ/êeaɔa-6;bC٬Hш&6r{Cjc_bAR^~s+SSq:Q2k BVρts#%1Xl!~kJ{|snR.`|XTi# /H}̋%9ŎBNq>tYxEJ,_c5 C6qEE/c~'CLNJ#>JOFžq#ԧX#[u$)_۠ǢOJYqldW͔b"t=[!_4K- ZbSvu9"Kt"?@.vjTW|ՒY/c EY:$ӸN)!_a8Aїy&Nֈ_P\QPc=Pv$vr:@>T"yoKS!A"\El).i p 8d[*)5{Q,q0,S{Zk uK!Ic%VP }aoJ:5;hSN9[&냈6%)o:|Җ#?R )Z,=?#?n_; қg'ySBѻ k!U|l䆁J2կ Evz<̏=*3 5,eV 4|3T+)y1 do'Ig",)#^Ud%M: ((0蛢 ك-V4Q֐}lHLTĞRz=7T庺*;Tè≇Z ,5ܽi7o&*(Eqa?BR )y)IG4~p+ YU&]OUK--mp*$OsSMA yW(%ks|+e6Z6z'R"_UlC઼Dj<æbLL8I7gDFis̤[RqBEcɏ[r'З ?eCqDѭPl4^`` h޽ϩIA;\QRGPyj9qt )^.Ũgs$E6`mm I! *}onxG8qBcmƓW[|gļvNZG a*br'3AD"|2F$}qa,zM@S4#s:!GhTBuB#$N{ΠĦ76][T&A*,~$w?ϟ;7hR}}ϟ>7??~ _??]ux${R 74TPj}>D;aa>` Aa҂: 3~/q>$jsedb_:CIUsn2t@+ jHfEVCnKKޑ`O '_ Fni&lk =0hUȣf;oXWLzOQ1 9d>@"mZz4pȈ W\!̰_kaFZ*sYz콛JNTBtL9ɦ2z#=! vf@¿5~ @Z;$Rsh~_!0sH[)^%q}aϑ{}7"{hI1f}S:1в޽znMr9~$:$z5)!^kxu0ذ~ˣDz)+pechw|=ՏIȤ' b%م3Y(A:Vِ7Ɯcj>Y-C. >#W NnxLj߀ /31gWx(nDSáCzg(Hi\{,hW - o'?1Rn uhFԕ_wMLD%ScM7Oro.Ӓe(S1vFdH"';X,ָ.g&:qjwz"D`*\1 9?)Pr]P0[q[lUjCmhbZT-fT`_7)ΒsVk2 X}+{4ˡ-M&NhxףQ1a;ݯㄛS[R?(D= 'v}4m*Za-•LN;3V ȸ@+0iJ+hx΃ gEG{BJGNE8 X5xYe`| od mQ Atz@ tH9qz'2j3Q0b X1T0@%0+WZC0ǎCu@ɭ~D&n)f-heTg8*ʜ54]炵cf[Te]K*u\9vڶe"D>uFR^߅e$~ ?ønj)N.b6Gݪ,X  Z ٧R?UXҝIHx+#Yz6+U\^֒aU Z`cfJSLK^X;JµH;BOW#]Ʈ2'n WދF$vK.=Q-pdh&| ]8D I)w$.^!{#!Bh=Yݏ\A0. ->V{<]!kسMJҷ*%9DpDa*Ʒ]iClMBjEzc$^, L ?o 9ʈa6>z'o~ w$TJ?/o~O?5k?}VA˯yg|~غ@L ể\\5!,]N 9Xsr)][Y8U3R aWՖ d+WEN$eJt" [6!ՖL]̎ȷ5z^>a!$3+ra%b!,D~XInrV. CqBKϻл4dZ~ @@.V:/P9fբ%1I& sY>rrPA)-g/{9()-, ;ޥvY!k3NwN{>t{ y)@7 XTFa=do _v`avl6(uUۻ)Yl: AT>QBM}jPd41sP˩ ^$Fro}d+ ?mItZրqUo4vU0^mAAt8h*_forKwsL -9# Y%i)`o8${-xMm N$H g,@Ŏ]sY[n" 4Mj|z*Bhh0%.iMTDۍѤ h Ķi -VAp2wU٥˞6q?e@}~pǍ}BZqb& [ hM U#ى d5[:*xqQ-sRatQbF4'3kd)X7]KԧBKW]E=OHFáJ1PN._L'1FÙzoNduRYfߤqP ۆ#>V%a{,FSs;C$ܜmS{fo(o9oJ#D*"=h%(xe'8$$b|DZ֙wK4Ar-l$=ckw&W `ƙjɩ\Svs>%y.;eF5٥fFjt/SkaFzEg 1cйȂ&iA2K i2x'vި}+ڸzM7 ٩#|g#VԿ;1W>HEO & p+-: +1ҍLPj|P:q!MH3BѾ;zsy8̿z5S鶤!biAbJ”~@^,-qq^+Dr@iO#3#k؏/nH4UXccF MN_IuxՔ%" )x1P`P `=߶ΠUv{4 ~Vq3:EM(TǦGB(\6J*s/= VDׅdri뵪HXNods+a1 Q 6qʨAdD'e^75u5qqAp[xq'ebz;P4P2XX"Ҍ=Xwd 4o 羅=j4V.,QYC F6LQq`{8_JYT"\mAz!ِ~^5Y jzxAYST7.r[\:%)5C|g(OM&HGi`=!U#%]itz4;'I0 kD.nYc] 4RHsքT)y@I5_gVUĔϥ)8Eij_zDsv> .ޓ7;-hN?fe,7q!\{mbF B&/x7Jl zi@r^Fue@v\T( ϝG]P Gclv 0uFpƤ$ns9CȳR <=7eMD\&U"T jUVQ¦R=3cEpl⳶3sẼ߳IγߐGdpPjy"jIfCBE OtHkEpU E VB2ҩAQqUz)/c/Kz5i!ERӢԬc (P)ICTy#}#<2" { B[wvVfun׸'Ou6MFs}5tJlZpڶժ2OF>Қ'u+kgIRmW`9]>k $Uy`YiIAy;0DZ֢3Ly/nQACモT>$wCsU߹$ls'W^H4˲C#Q36+m2D eAưIaJ61r:wO(ЍXmEFJ ~1ڇߜeRn`F*8]L lͶjd` ceiW P֬w^i,粮>E 6ڠDk~g[ ]*^YJ8;h+z3K 5mψd2K5kt%l%Z 4`AIAvI$kUi=}X'is$4ol}$iKVǔW0CӸh"3lI+؃ DD%oO5tʒTl(ɭX$ aRxBVe5(e #-p1KYϥf4q?xkԟ\Z6\[]n坳wG&lPqͲRp^ƥvV9dxT-@ s>k١TUUSZwJ'F[IExq?ÜY>zt>U]V-0jgpҪ/YT>ksIGnQL@ ]aNEڝUf2M/$T啀u$_V5x@;K_9Ȇk<*wb<6oH8ƸMBqVE&"S}Nz#|YSə0D'eymR&ޔH@?9WRbք,iI8,3ŵlF`>ըd S,+z ]v7YlgY+U}1Ʀ[(#"]A>PdיP[QSERǀ:f'Dyũ1.oA("Pk/HM5i=gQMV̄uQ:9*NMI#n沀y}TF rduIuم>$1Dor(Iڗ3KsЍL0N\JU$QgC 6U9T9Z7-^se'ɮCh1JjuK*bf6xk%"Q̗okf\yw^?ӗ@O~#u׳>ɋ_W53OY `` hד˿ً^OWo?}_w?7w,Oyzݷ|7GuvsƟ.~}Nזb&xw,6Ƙ"O#5@li&2h(v "JIIknGOR:#)} b;Ue3H7SukZ">zmI)) p x5TFS%&]#Z$wO-d8 EA*ΉM}4ڷUEʉf!&WJ3u.titgJ]Y"uM XIn@6xS7Dp Vds}a_oTuL\B?آRI@zp?@;{lYQR\R>+7 c5fDF kȣX$ZilƘ ^ȵ9V%-N`V RtfhsKڢ H[‡x$ȍxn]=HTUSjyx7)K1(ښqS1isBpj[`hZ$(iP,ayPiސx<2- GtzEH61\q&Dò`U,nI*_kPE,]GS[Aux9ۉ ɉLtaR vHM)=LG[fma^~K'8J^,1{UZI.B%u獒m/;RfMz$uoŖ+υ0Vcv䵯{f( Jg2Fa 9J9˘-Z`ؒ, -<1*VE.k T"9wzx .0WW: kPоV?Ŏem+f`ZW>6P!Nbo-6xY6uX== *&>m-1={KH]rʡwaE:m|û+G!E8r:$|1txp`GXgiHM2⚥B&i(Ȉ/QT孟I/L9/ bU.R (ზx8 rgI`_& 7K^-AeMu?|blSۼIm7B>_JPBQR]ǚ8?BdQIB.*.%AVEt -l!oeklg@ؤz?#?&UFFZ{L'*mR t4Ҍ ш )n>Z0XU8!'E!%w]׮ҋhؚŽuސhgb~T/zUQ<~V><>N&ޘ M*{׏v D3?j:A-հsdR+/!U*nwTϦ+{t ]YVR˦ Hx (Ou3J$H:`-9 ݲ?WC2C\<~X}ʈӛJOt_ {yU!i4~Gj[mO2OY5D{0e \oG~g_]Q~NoBڙ*$wN $z|w_Kx!c@t\bI]QÇo ';WBAr_Y(3t#bSmBzܟ S3.%,N%22~{Y8@#F>KM#b𰈭3ctN^nӾVK"'tUzo(\##N9PytY@Z#*UQO."G,_UBqM`I' tay ?@ƈ8ńPҴM2}tڲFw%B G. Vc瘝;~-[Q"2. l1 endstream endobj 32 0 obj <>stream HWe|D(9v'+`Y%V) @ xTuo [O]]]q{̱/1%![\?]&vɰK5#knqO+3kWƽo}^潰")ȾSڼ b=&# "S ٱsΈBsY^[9_j;V#  =.yM vek|CjNvV={$z'H"3.sC:/W d{>\nǹ.u>42GBrv)ɚZG1\5+Wgx-kc/o/=8l7*Fȸ]o$XbUl>+aؼV6:yޅEZuOmĊ52F{.)pM?#D6?p+'@q DU/- H 8Y7? EZ. :V%ݨoS" V$ Xְe`9ʠ-x H<q}ˁׯ7 \d#PKaMݽތ |g.tT"68H _? ި\9ITt^$xCZhP5A5|sdX[<+< Q;_(xJ@/udIuNf -ٲ9Msb1$vɞ섕>@ SDJ\Q#ds#"[R=*ـtaS?YԓdXfHAV[T!N\rB*]0 %vO+8Ɵ? ;\uTE#XuRVH݈y ٷɔBGB789Uӳw拿p_5hi^9]VrI{ #Be6>"{BS@j^lYfAAi }f8φ$7K[)C?pKAĘuڐBK?@f b1e6Bnkߢ>d%0 D|qBIID:KL ]l_SRAţLud&KT#b⃰T~R: GZ5E: ?Pa27ڵN DwƱDߔ(k?d5"VMB, mV5ew<#\)O{ ? ]B^͡Wn%5ǭ|:Yխ;Mz;H~-1QhFB-7|eQoYP:kv2cB[E-V72󣃨bQp % crC 4ĠKLC`R(G B+':QZlZ:)XR ~/?||÷~ ?SB|~}?g_}?}?~˵_ʿO)U1z;SmGd{{"qvr Y50IglоNBuAO:B vm/j(ӡqBce`cUś}ͪ?ZX,4MWW8 Au>J(@MDmCk,`հ}5SæU!5a)wA;˳VcQC"xr_wt }6koÓSh@uk[5V:>zo/w)O>HMo=닭h Uǃ5baw3`x\xCш#:g9zKbtG|U$=9cx 1#7B><[OD^ )3O'#ۻ4u%aDXk7cmkRw!AFq[1!PzG |m=L~}iu2lBY+$mMvk a1⤏DePiPS،^58jPלO ^gS ^^VhSjN,v*BP:qB b_H\9@MR錞DgO'6Ԭpo>(=syC\E-ӿV߲zvG3QQ1 Yn[!;n,?ۻ5)2\qeI/?ҩ *z%vέ3H"P{ 2NYBmhg#ßx02xYu7m9a Vtڿ#^{?{O5{Ta00UFQBsZ jp@t3a9t7-8ke|BB=f,DH|>QnID]{ /2"aj1>WT=~|ؑe;9rL > c\V((*gEM/:2Б"PJBoq :]յ^U(2qHq\LX2.=Ul֋55-8,Gcv%J-1 4V8`DiaڵEmgc_4k!j1NҔ15&>UKf0>jO H&8Z3BjH!Ol~R3y y$g%Xu]R=e>:k I!:Us)RPھD߮*.zx9 +LUU4"Ǘ`k;[y OqIL)Zdլ6.BV٨zSUoW{:)R&ƚ.kGdDV5\ EsE&QCyP{A[V08U D|Lh_d*UǫI-ي=';eFBB16"w "5bQ3ZiDRĵr0eÜkftqtˣw7B2ڵlxI}g,5 $U7 pӈ=?^k< cQæA10Y"al78 ViNEPnSt G8TQTUD"*n^Mhh`\X*]*o)U3R<Ҷ(UU1Ѕb3T4n&nlk D!Twђ N'bJ@XYh@][Y8kYgYȔA2 r=D+ 6MRe@/}RuBVi4X:"(ި;ס&9<$'͉5N ~EoDP>[=FGeࡐ_/y{ WFUfBNȲjqgd#XN>mplso2ju/@n_OW#X=/q-|w;[T6RLݬ4Ĵl[' @y1 Omk0m%DXI_l ]G2v7oܽp x=]p3ab^9^K| , VP )M[p9eM2k0ӇL@B~_xښ;z;AhQYjvz^hѵ^1RG`yHئ^ur*JC4BX %#ߌCh^UkռxhMm;Ghl$I `71kXβYݫ7߽޾Rг34!yw޾g/^|}ǻtW?zП<+ὙFRWV=+FFéɹ*PgWG>ī>D|Z"J$ ] ,g4M@8ߡ!C~Mr`* Nw+I46PS$ayo53+v!zC?'tp;|Ը?:U 8NJ̍%pẘϏя"T_,d@C҇*Ӌ7. "l^3{U]麖gNH 3,U,,+~8|DL,Ap%b֬*N[>öFզϗ~b l&]@Ml2il@$-(ׁI}zRU)fr´QLМ qL!\>́N*Ro)$ U*T16|u#WCcDm;YOcy1VNF+$kh#&:!>}ǽz͜]bŷar0$m?G9K7:B+6aĖD+=uk|S `Bh%ȁ;eE7v'gۂu5HHtACid,GLW7*);EHJ !J='cW6㐈6~,ˀxCdsW@z;YGۡJW{ H47fUHmM[3/ٞaڔdR5p;h*$߲ܲP˦5[i;ED+=eYrh wݭRF'ב!իT9I\BZ֡NH 2p-cy3eeQDp^ hM[\% X5PQ>X&gC%O)9M@{kTt=j(@s*{W"8 -uH-M]YuIf(7یNL)m9u)A셉+qpH(JsS laI }AG Bd;vTfp4X%u;G0ad!ĵ!vEtDl@߲F=kDk'} bfO#GEtcJ'DdNT5ݿY{Qmw@Xf$TjJ^ESK>#b;2N?hw{ dd,ު-~ 8_P'_0I8EO=7 Ѧ| _23io™t>n ngu4L1[`) Lr)VbS.W7HFDibcaouTrM!O~$LU''8AIYqjFL@ OlЈk ZgQ%`Ma X. hes~ iZ ~0c&-FRť8yA\Fk_bHҮM]U\GxBpg] xP5ֶ8ǎ_DtDX*UD_Mw `C7^{h y,;j N蛬Ta@aoAz@cc7 YPiN.MGfTwT볏=IװAdmRh q<T%R}B{iו]T45,^n۴e* F{GP^BtHP-2CIoi rV$6Yf=.tnvU8zAɴU`pU"8~cd:zپ?7SLGtl}` "N5+"YS 2Kj>pI5D7T`~_kܴS@eXZ]@tD1MllXhqw4sy2W}Sr ZF}E)dn 5u+XQ  ZG93K3$)ikw,zEM$[T43Hߢ3\Ԝ]4.P!צŏp>ydnX[Qjz6GU!+d]7:`abZf,괪S^z˜v#`D`J!fP GɅKguǷW;"? Fޚ0CX;@ F+C@p7JқLDWzdqW"鴵m` NPʪBKRaXsnR6Φ ա0۪$,Fdjw\.rQ D|iK<5ĉJW6[Z6S.VwZ37qBV[y4#t1^ēldP,Q$* !HkMNndrpq~]kKyH% ~ LR J]YI*җTR#Iaw{2w:BI(-e{(RF kpd!P42GO$S0?k/Rd:(F_1dGOiT kAv撸k6ԒTEO܋W/4[懕HO_h:nη R)d4gm.@9tP*(.̓18\ $`:n/@XUM4n]! F+j/yRɟu̺<2w}un/ʥƻd00K[vD%n_~`٤<H>#z{7Qc<Ҍr6BP+ibDS*ppRKނ^~'=4sm\vVn”N)Ù}ĺ8o0BEmuG ~Fw?G]"9 `@Y<L=MmlcѢ؟ ٫ĻQ7rDօ)G*l*Z():#wirFSҪѠ0Y=> [im5)/A^a#'ɒu67ByG*D!SdAᒖOfɒ_J^(۬hS$A4(:Yhf!xndos"h+֥.khQ5ǘ0 \mWiǡ#M/@X,: o#+y& P(ittFE' 鐢-y1ZQ Ft͇-kL[eU*FP沏:Dsy#\Nh\/d N8sQId]124k$YaYY-fe}(8Ḧ*|(A!ȎR_D2[>~q|c͸>4QNɫ. DRɖfL[<.ı MYR&DqOG61>IWlR`$1} GZi٥i$LoCnވSp9E:e ž=R<DQՃ'Xa|a3Q dF ̎"Lh) ׻gh(uA!$*\H%4|Puw$鍈aM ҫŌ#!6h.mRNuHYqlr# z]*kɕ8[5 YN۷~|22T\FJ0[ހpI#m!7r?;ĆI=tx蹘 A괞w Kk0z#;VbؓtGv#=w]Ҝ!9*j@F*Ə2Ͳ>CN7((CHs'gɰODciV֓@!AӑJ\9u!6/d?ĩU1c},ZnEeoڂ{BIi*RL:_؟u }Ӱ г <)4 b\. fFJoԹt ^*=:`Ϳjt")YwwL'||-¬C֋8}]'+ 4u3ӽVͬ/liҎ,$Пu[*hMVSp4pDgyN9_$NwUnȿ:bOG4ct!m.$ ZFk$|o;&uc1~xKV)VR( ^/ ~xYxFUI<ʢ̃¨ 8Ua}r" ɧlJFMp D52w5,3Hj؁&bf(H*2Jt^{QnU~LfjNgT5E h ufgNtv+:_ĕ.#WJ--^K/Z%d ٢v]"=4IˈchЬզEAjiMk0%5>$uz,/饻B1hTѦju>dj 8H;Ⱥ$syTɐB($O` ۉ\xm!#>FPRdDz3 Z9mQ"[~=ت*3"dҩi2!SF]mF[IȞ%Q pk`փW(õj?7>Ʀh2R*AM3u[41 D1f/`)|Hl)f@ڲojúҹj4W64[q`I=|^aJ~D2CazJ͐dpb@6r?" 1Zc<2Y{}Chw9a!~"mn+Ѽ^6bNur^Gothg_Ӓ KtE€1Kvx4GE )37KnBp/鑥PFI[8W|fMQk}ml),ynFAZ="3&$72RuICb Go }BWqezM)"Qyu!q||Q|).pZEbȭ0:&.dVS2lv=uHS" Q1PpVBWQEK ^4ԊХ3*} :=BcLv%- *ˆ{f-Kdj,/NfU!u7nf9CVC!Z3 :Y,Llߢ[K/=* *hҋMQi@߰s0>-S2&e;[l≈.m0aO n.QVy669r,͝D=MJW#@@59S~2Q:cA6 $ߜ&ǞE 2[)wD/}̋*ȥW2$z"q >NRRM@6ы>rdNU>Fb>~'X&N#}|4 aU]y*UiKZT^Z#gE&?s;ɶ])8wS * !ը/[IX.Ķ̧K{:.WuP/P r,D~c-m:Vi$d? Br~9;%zW ȳoL#FڈT Du;$v=+O{o 7:z!AB .du3=u|sAjk胬s-!*$f8ϡ$&=ǭIċ"{e)zqZWW8uc /$n#T]l*/$BhF#S휭TSq 783($飅2 {˻Rm<F6UJiEcɇt|_?N<#rIGRI:sedt 8DT!]oǂ~m%*LvE)d9p>-(9©F)HZcɆ]\3"Byx[##Gc5az M,KC[zҒbAE:* LOD:SsK:'6)LM d[X􈀹{3-;Ф2Z8@D" mo %툻GOZ g(oo e"#멻w9el\r*SdU^jRB1\C0jbl9Q nPYپ4K.XT(6- y~=qC {}xFI mΘ5qgyܾj̍ձ%Vߺ=*3,cI]>5"{D)'eƣ$Bf꼚)RL贰,5~u:"T%]mT*"w[$(z85.ڌS_%̋>q^^hKZ.IdDHx f o㶼Bq32F7>X2:LlB4EIR H'+ܨ:Sb4R4%6[4)`STC?M—>~ς_7[ʗo1wH2BHmLٜꘂ[$$!:MG<P{@,1NiF䒌$s&-Q $"Q;':܋n;ӎծjҤC .>RP-I ZQs }g&HY#%5#"S-YўiW})=1S_72s;77g&Cݤ>ҊQ}˿L[2J;4NWHB_7p)gfL`##)H R߿BZe䃖5lLP Rf1($mLo#M5ю@(rJw @Q! vrьW2H#]Z}N9Nth$;:ƹHBri$ܸS;Hx^w4q`GmQqL5NN ϺnUƛc{dd|ep:Zz\^XO!v "n(̄Y pz% =Z1o`X 5׆5\D-Cv|'ޜ;Aȥ&QWèɏbvy<"=䖆fv6Gw="޶ۙXp`AM7Egvl~piY)3`B`]QF1-TOHUbuXS* 9ܩ̯k#*5v0fuE\w`L:8 Hhl-DOޟ3"rYդȢCm{Q7@)D|^ڑRUZ'"32BiYu@*&L걤\aN t 2a.y;x|{9d!TрutȪIW 9xz'(޷?cIqAtW;96UNwzrQücs Q:8YY5\XGU;(*ueƄ"'~t-)CRr9v6<> lN+}َBWd~P?qV@0"?rotd=)mijZ9 enBVNu,FgW5Q0 XMQ T %ȇ@Yjk%XH錁9}fhfvV2u! ^vdOޑHj@D3I! $KGͩgVg}FSXڧ1RXKj& zȡq3ڂarXuAEՄx"s 4nWHU9@4 6Z='R`Dui`_Ltd9 /]d@\H^Ew:y¡S>{Q;x]0He+j@p3SBC(,$Y{tc0ȃa&UGm&WUZ8!Z0-}Ezy>gE1(IkedqG˙&ƞ5.DI#<°];>;-Z C2\v j;eSʀ64]^$ۏNg0no`1 5gdvC*FWXD1CMSu9MYpƱ]MqS= ob1Uw8Yk9~DMRZ;IO @gLa[X0m ع  $' w: ISט`KD>=;.ILh!Yp:$'AF">D'Hdt+NsW=r/(*jY*Ƭn^!(EքITTFo(V!ݶV.|\U2uY:]!6u| ޕUMiN-rWB6n2s'^'$`zo˾MhSIYBFC76gG~X)iq XX^ -M%l1|YW2,G0 C@0z+cvAеWtzkf.,;SdIJܡd \NT%9fzR&4Ým4W}TD&ŇC:~b(auuA6WSG'Ӑ5 #q"攸В&Lr&e[LtRZ8-OR{GFC +d;;U.4X*c {.#a=|soCUsG(UP{{w|_~ S|R>~|/͛_/cǏO??_yǗ;?o~Ԉn頞ECCu30Ƨu)3z3x9/T 7'T^W[ $[f#_pPS d9v}"42.FeNΖyH!fp83"e'ڹCl8i̖^+]Aɏ+IR~ lDWf€cd9BǵQ76+A*f%yivZƿ39fAZtx@sċRm5z ;S7nX9 E# /"^7}`2V1(2}2 .KJڌ23{ƻ"uymg R)FYw%)ajՕ tE@abߍP>*$KkKq1-j&ɍ;zu)YPDvK%-zʕnC`1'-肁=bD#YdUŢ6B B,I̢N` 1gz+mq:9p K<1=jʜޏbZfcMoQk80 g^}OV5*OCzl )= 9>(-%jG=6;=RZ$]dP;HwAY9t Z?[eҟ8k%D+ւRK@"%)bas'H&hb$W/b gq8v埋zf+&mR'y -cba'` y]Fc!/ɷx9ERqpTMmdj8ԮKHa x)}V_(&>$HK!cvFQs 1W_'b *=s2ȜJmYt//xzw Z`GlK]ӽ=ƢCh4.7+O!3r{&Mۧle6tj /%ka]+] x܍eE뷥x4^Tmm{T=3G[KhLsQKs4%+$z2y&GƺV5Ge_2S,}Yɲ,3A$5ahr?BE׉ww -p.X[PغL"ڑZIhѦAej.Vqtci2CL@O)mWͽ.S-C< ЗZ ܉B/0j1އ4 ;?3Ts7Vs"վv F e<)%UZ+m.6ָ9 "R5:G`9ݞ Y:kى!&Aq`^Y`sΌEXuLER"C ,vr"0x>9F˔ N4vPL B]̶R*BK88C|Q/_YF$h}ElxP CTL oʞcrBm:$Dܢ$mΦQSBg=Kf J >6eQ9ıڠQgbPv&:wf1![ ѻ!|9Uݢ5vT+l}r,b^fG%RF~PIZi,$wx'.4J9 cÃ7rJ.8xhm/yy\:P]䇩6Ԓ 5B_?-F3w=ڸ֍%9j!u1]mL/am<@qE*CDRJQW5mI9#40"D dJ̚tkB)d:΃orOKigE5 K;T)OUfҽ+ ϐ|YQҡp5Š5ŲwP !4O 3٭U7oYw`bV}Dd(xwmB`f b& SkB|fZ}Cw5cyob_vB UZL4iJi˰l.?*rIzD\c=d:%Y #k3Q!粐yGs6ՂU46I6į/q-*A=TbcJ(x&R{ܟ~曇_<?>ϟln2IFWcؗ3箇(#6v4’ߜo?пQΐ\eA*THrg TМ}"!2BV%H!;I'a 2Y~  dy&eT7:F/б; 2j w`ưZx2n3Y1}`E}Ht[ z`_R7,cE<} Aޙ1*:Oޠy{3MA H3h9f\Ll'bI|-xjA?[AP%.Zf-1Tt/i#DWvhu@+LU(yTӳʯ =?K1 O[۸=mlTOuA)}T[[Ü}ZUXTH?`fqb{کCUExJƢ"hXv• '֓$jLB \OZF@(옙╩I6Spǫs ˰Þm))`42%"?*XF{_1.uHCz8qϬ'auO+UH#ބG6H3'z`I)GhSZм4:: B0*(HOgBJsRK5/(&;܂c͎(R[d&M rFkmQ2@5 5T_fiEQΓgƼIBD~B^:;S?Yq@b/s<3@VGo-eM-TA)s>[Wck{\+x:)_:=oUC,ADRO|Se %-T4S]Z=..h($jqNϜ:C浓JĐǍv .j86iKdxvEv;gr91N2e 5{ g>׈  k|:L>gWK.1H^ 2.`&Yk3iq[.B`oqJwW[70Mz&W,@!XCʩMP-)S۰[q(>o0S7^vsܾUztow2R-7Bt"ҶBBr#Ґ3R{̡doi?s`-ډCj=G̶t]PIQ&x\ k_" SUGӮ 7GDYqD_QZP>=pU~͵VKs!rr -_$,F3g8 dj3/J4wܶ3u{~vJS%c%}NA RXܶQ4"]̺f 4@LW X+x(똩,TƩW nMK/Tҿ`V_DTf|8v.VBkAbhWۑ @?83=Ǽ +g[8¶.r V4hUKϯq{*[~82[Oo׃Nu8-ûò%|=Z2 qf!e3W$^Bve ͘17 򄩍CW, ":A[.w [%^6c% ,j8aY ->)gQpB&Q@!1Zp3ܰ'N1cۚb-q_;letI7T ?Y̛9.P }F+*aZ٨ƣ9'OAi'RAPcⷼdMEU{xPpr)t(X-WTtE-T7JFfu<,Z l/B V'K^tIBSm.YЖCYȨii wf]\yTq nH&OQ$lgF|voT_m0G ZTK4?P!32o3Y ]mLȞ%Q}[.&ۑZn_)(õ"ץoF82TV9{xbn']tbđQ![jU,MjUat(.8~ q6;xtH5lxf>Y/{J!EhA׫CC a)7 (l)Tɨ8[ ^26>y"}"3g.FD,n Ѽ^Ďup7^DU$ԋ{YO(ФOeP/L_VJT+!2vpzE% vY㴩t8_#VSx/֨mc5" r(gVQj |c 3(Yo/Q4|z<8@Z[bC2ˎ&_`&{f-K5 @ x{Df"43ʪȈsN)"qs y,s!(a?_\{<$ ;#ʐT 2"/ѐ* F,F PHhR*! Y Ej9yw DߕBGduZǀY)qo`Nl*f,8u}Ph3". >-sw ;/Ap^3R nd2/i;ms7S3ugw4WdB=@Vρtd:9ߜ&Ǟ"\.쥚VpGt#Wh #T׵Sد i$մx)T^4.)ʏw x1lIZ*GF#8l>! M.j*OؤEWΝ<}Jo4vpNDZ>))Bv3(DW.G.23 XXf3⿡/Ac~vV;I*Qc ~Q֫Ha!(1A~^'ƍ͛Sw*8<ԨBo*n܃\xpу\tQu&9S+8XCm QΫXHX׎=f6K$3,pS?LJ|_HFT$62ΡSq>b y+ R٫޽+U` ,7aQ5{"ɇt+}-r.IK'臃湊Q^Y1{BFnBpQ}ia`Py%UDY jW[2HPRe>++j )9.hG@O6,Y˄dvFGvᓃ/i4x.9fmxsv7? X9"c<|]Zs<2ޏwU,f@Y-h P7YV1a [ F^z#xA {:JbS '[^FOndH Y;{>VmmȊD'ڠپ4Ku083'@A!&D/䱌u'v4}oGfhUz9SτHy*3ڌdPrUXrX2&Gq).p 6iE!os^ܑЖx 2N os{ V om>4,fdn$}8pCz MX 2 `ǹST5+cAf8f h{j*5UX6&_}?|ӏ}/A-˷7|?oԓs$#5+^4&&ӈIkpj (.Rö(ߐ2]Xb$ uJ[7cÀcX#wD{Qˠkt=Z@;XK}U&WP#eՒ k5T4w5kjbѝ5RBDZ[[.gdoJg׍Neaz#Uo2D 9˿L[2N7F%9 psE 0nGeZA3'22RZA@E+16~t`M*HFķ'i.%\d [>Ӭ/pq!%J[1eLF@Y DDɡsҞffDd?{CJs74bߥDuH$6"rzR45:8ㅳ͒*)s`GhQJ )P7mvӮj^N<}]D{QaҌ*_!]̸6zrwVzr_GF ճEm[j z4XDX_3!aRI'[M.Prv:mO5RvSXEH;4V7oMK62Og+*O9HDɄs7=-Pz.<~^:4 WJB1)h/E\ъVc̱/=QE!9^ګOhK KwIa46i=#eRoGgu{0pܥ8Χk?Rhv6t63P&Y]Tl {'F6C.p >f4mZlm^9? 하,l&ڌ2?+)&g/uʞg$Rm?gzj;(25׍ɯMԫeX|DPFn;g"HWlA` #NdjP$-nS]&0 1}'Bdu{T H!0xӦX)nT 6ϴ^X^c&X 9c[I,foHv뵬6{uqoఉpOت@\ma]] ,^YMU,4 *EiS6΄bGj( *'ZAKz]!zNٴ6)~+3na  :\05Gش&I s韒xS_T5LqDŚu3=nPy>yu\K}>r{7(/fm q^Kok tc|P͈;Ǔ ᔁOupCYpx5\XGU(a}jNK!XUgK}vp@髺Q%rY̧4jY)"5|?C!$7]M.rƗl&̈dsbq>stream HWK^&`x1/geˬ*A0ATUw%NGuu"xD,j.qnxS!tXs[2}u!1 =mXN>Ed>.y{s˼f Ǜ]m. }\_0@Ⲇe9k-|վFzI~ud_oXu޲,_Íkuyp\&>sk;;~y_1:(,|N:ZU!ˮ{%nt:`Vxa7,gMsӹ5?x H4fPtk;r{iu쩤@^G.<~ F^}D1ȷfǂݮ=&8ܛAf!knxo`}ۺ,}<ޜ6G 8b ΢Tw,Q/hB o gM1V p?qg;E;`$N2W2o:Hv[gk3}[}u66)6 Q%ZvH|dDv`K`h%3t=k% !:}e߰6 ܝrZq!sC!l4&+.(0_MPګ*8ɮ&ɞhC K~! ‰'t~28E+`:UnZ#N[ ]tJxǑDt ѝ+8 =,k/ʊDlCxPF+Y3V9Zu4g>E9GqZ%J_7~e=ߏο#7c>%o?zF9C{QA>%$]d}Vm/ I7sj eImlfL1xA5\TF`!™WaD,G8:pMc$}G`2C8 tRYb0 ( >991?eDe=A`f!5"4\#< $ؐYbI%l{߆BU/@)@߲N߽-Dggst_!yȺ/(.İqf0/Gӿ"LgS?Oy]ףA0m.ڥ )DHbߖ!zv$H &ng8 c7p 4y Q}( :8*nBOLWͱfQ%6ix|Ï@Y@0QPTT xsar`%s@'yt3@l[lmA6cDD$ #䁊f;h'FE)WRB8_Pi IcӹŏҘ0ndÚ\yAfa9Rpq$0 *; P{׮9uir3j8hss RmM!}-QLV(6Gv,[_Elj[.?(UHȘb/v;uYKTPS@&B"bl̯DZO!6[V!F ]6-<ߋDPkW3*|"RL6o>ctyH)U'&Yc`\BH[HƷrZ?$c68.}Qhgi.Yq#%A,%Ρ2ЅT`fTL%Ω興`[!kXr@U*asՋH_u4$,CPzUS 6*" =hڜȑ7I/ (>`ABJҪDޞuwj{z9Z-@wJO1{xw>RaNA">k :GDBwq[.B{YJ ]N4JDٰ͞{&b%~SOSih*2ɮUmsK T>wVE&mk;Sޡ!7RV@7~n"䫠6]肖Z78;dAK1x,9gEq36PZrBl!CEIr.x\HK6M_aFK&2ҙƆͺ,D;uP_"DBWgZݞgrwBYmaa>99G1'Zˮظ,TYZp05䚩G,6A:RL.K_Dӟ7=SpN&87!$LcZJcOI#apLwK&޶28}Kb$JA2y pOTDQ24=Y;DPke _$义݈WX4Ť¦FG\Hx־:2yf7VX-w#F)aՇ p. -J,_=Ro9t@  Cߗw lN'4f1~V/kMEῲS^-S/U@)R"hNN*31w@!HzΈ|s\ O7hepXEHDXX x3N<C(}Pq>C9h@LKN400* Ã#a啴a9aQ`oF5.ɬ/u6xS Q3p } z AFZ7vA1=u5S,+>tva/3,sUt[}yGVH0`L,pKy]u! HLo)"|6bXĪMWis{"N Rf Շ8$ѶNyj#+Qj 5 jGAb*@Dӈc d8e&ݷ7˷\g80Xhv!v_W=}{;G#b-0]G=tӂ@bm}^߬)^u&W%%n}Oq ;MD&n On7Lxe O>w,ea3\mԅIب*ԂlLC%q%*z_ i$Rq? Bw|k79&22A89XTǵe:++pp,4H$;FlJy?d JbgYqPzg*Ʈ`GJ zp?C,.vK~?o>{W^m>~{_y?==fgkGٷҍHuKˏ^~gߋ_>=oo߽_~W_=~߾'8I?glMyM=< w8f٧&@LR,q6?ۿ!BR S5ZX$_~CGqeK-wyE&ƧGjd 6hm "P6I*x t-[GmtugUʖz)Q> MK`4p]R÷ ()fu+Ф4Lͱز}%~}Ie'c 'K<' Mc3 zR8ncnkޭ0WڑW]e*L`ΜAtv%'@z?H+ e[.F8۲ԃ/0DIϵc@CnP.q*DŽ@tz\҃o`gg''/Ty nRń+Y"/t2&rA=GEh}J1د6fΈ8@SȏnꨍJd`ZC`( $1R4[)L&솃O_,(osfn7.{1MgΘa]s#2vbL]iju&g4 v'P1)¡@M FPtd3:=y" Lɂ=#D`S(f, ƖUQ+ˊr#)KWJ+~vo ;]溲F~;/:5``8S s-Dǎe%\Y]!x^)O]wtib)uЖ솟@f#eгeh+eBx4]U}%'FPKњ1=(]=CǢp3irfJYfpp{ AMw=.& e4UR5|r՜[QrӖ^Z, `re=ۦW d@1gm Tb8x(S,ԖUhS2T&ӓLDJVՙ6ئ*rR4oY@QW`G)^pT*i2P1]{O=Jhq:-s$Vi9TK'zQT Ă5$v6~Y 7[oňg#ydũ}R|#jB+L IG3iL0YN/SfH7/yP{'ƙ,PK#!|5mho *+Z *kkfe!e9 w|9Y.hfwވZʵg"+h*_Fr;"u咹U^$zbmcQY@lGP ~"vS.d{bGjcfFe$@0Fo4 ~n~"Hg:Uyp7qʼQK=,_=‘qƮAEExZwƵ5‘-q?ė?h٤$|ޣA ׾,(y ,~hu?Dr'2>ߔX?㤬 nK2y$MzlE )yN"b 'mcw_KޠKp""D1ۂR-%{h?RS|[AMD6њ 3NZPCĖͰeaFKoB|* S5OTJN ƍ%uFu 4vv>~A%,m5Q;k *qd cpw;Ý".Fl=&Z~Ry]Y|Fu ˋW&f  L-4uࠢzc (&h$P[r$dj?dzc{87u fP0nǑFroj_m.hAum0"dLC/@X=\{7~w*Bc틸XIcؔ8EdQQ5=OЪ^Nv98VZ`n6Y8$ua<2\$NySia-5aB|?>M+YE˜$ DYMĞÅUCIIFqԈD|RډTdjjSNyDy.h*1n;/ qߙ)%YIxIڸ ޭ \cWcN_H ƹևJGUZl{u*]]I?CPm5yw/B[+VXFbʭ>fmx&8G@jdH Av!Xn8z=3S](-߈Wh Bڡk42.<0qTs-E7^ުPX; xVrG؊ ׂvk* dY2i-UclhQ>UHbH>RZ%m#i1MM|c^USǤl_5&#'{QЗi vLy"xP2*u ce1ǯHYY:T̓؍7ˑhX`Tċ_,KW)t5 uR"rhmqNHd;R_ꝔUM7EQZ8%.Oi=C͡5w4 9-YbM(mJ2KQKZuss(_,UtWB`@1;r%%U=6.EseDf f{E^jq7r/o),X ];O&j%TG䮰q٦Vu2[ (oOuҩKe廴7Bars4@UO-C ZYQT.sH}̽"Yd)_@a@qZY!]=7"DQEU*ʶ<7 ZVzC -W4tgzzn7ԀO~sŸ-qoO_>OI_}#Ѷ۷/y{O:8r[Q9>mdsX-U![Rp?J_N0S~Q0H ~fHe7y!Cb`t 1KPg!Τr 2Nj5đ[P/{Z(D=B5[G E2t:1lq힀5{sV XvIa/VnEȑ` XS5SmgwefY3( ZV Uf44/i>L[.(9LuiiQjP2"ӕ|𔦾!7ɭb@+@CѤUw7mQczO稛W}^=Vqųqn"E,tE#2Qopb׉1>44#\(1<@I7QC.Oܢ\ao4\c1ki>Nqikf+q0hKwk@j3CJazF(sDX\`i7#̡u/d7yZWl1\mp W3ߘxSE]tpB-Y^!i="dC;q˰-B@8}Џ"$@{1XѠ;}=T(o9`I)˫׶_C `%Ic#O E >p3^,]UpT+x 8?ĝlCqz|䔴>]ڐ)8U4x,p IϮ"TWΰj+Ori쨢_EG )C$ z֮:A$>uNE3þ p]k?Rb|XUiXH m%c"afwE9ueAC\ʼnG;?m@q,q?߳ЉϲP,jA2n8Ϙb5<2M__ac˦UV>J|o4~=J<݆[ ST57BŽx=kekt_̥xw}G3;-f[HR6 J׷ecnF#X٢Ԡ2bA%RIRsHcR0ѳ ^gJQVqNI a>V"WίSKщ"mcw 9MC9R yۦ2n&2&"<ڧ@F3GC28<x([ȹs ΢/T(xu^h C2Q챽fHC[F=ФksvFyzY@c])zA;%٦n`uFOi95He%FOQIыaF.IM0 x|>, 5-'704zjEB"ڳ'YҠÖh|soSѲ{Q؄=}/xgؤĸ"Dok{wJI ޔFn( =t4X[=)MG+> !iAZղ4.rW,L) 7Im=2BW.zClPh|9rT̶_Q*TR/w>~};{J;>/}W_~_9W>.UHTc:c&H.>@ӮEYqn4bg%$**jp]?.IqlVB(Rv=uPƟtօ}|d|r-MoX`Ay}kPM$A>)ǃG;L (<\_2ʎG;cGx9M ٖ()‹nn9N( ޞ8un%5nl Sw]ڋy|× 8mݍ`8S\Q=KTSr*>*MzΎ2gB f3K(ZKR4״jpb A,^> +()b D pPm,(s]۸?`P289cP0ur*bk ͊)]W; kZQ;Ԫ;ެTk&uB-A]ۏJ){'$ J7ٳ݂ũXI/5*Y3zL 3SUZXiooi!LR%iqMbЃ(M{]zDewNۨ/="mIPmnwR?O?pUSE(i{Vєxpʐy:1%;0_DT,݌vd;he:dQ*β+ ƌWl-6UG'-ΛL&D ufʁwS1 )a֧Lk궮jmа 4`'CBUWpBJ7Z5+i5" A?Z$bxG3T- (O]r 6*8QFH:d!,LՑ Hv˛ɶH#W0-Oz)0mێd Vҝ-7_&Dں@hϠ{2)o]6оsT?Ytc>O~о0&hzH%l Q{[ځ\,KӕMVzھG:g6:F;|_qD6h.k =6bR{].4+-RusM%%j%" Qm#% З V8JS=uFENV"o+  xzmj\"kY ૔` rJl3t/@^Vt $-;aFoR/g_e\_}  b Na7`u]VpoF&\bbgpŻY#Ndڐ3mZ> 5}@bSPkPyj0HP?m[`< s{M&6b:eS2m[:E2P%8;QnIW; "YvAgfXE5ʎqbŔ5]~/ZRmY@+cUf  ohvÌpb^}+(fr:{ybCmG ܛGwL JcQrI{7u%kR$ ^"o\JQU-<$ WJtr"wZ[MTRKZC(,ۖMNd u,8m)/ؾaԩ1堨Ч$GRxF봫'q"D:fKx [9IndSjnO1lAjN =O< P)n"?펻kuj=ڃ_[u S$=eqJV-'mЦdVT8>в%?GS4h&f]< \fp!EGEÒo)%&FRf vwjU.IO򞠤(㒄V9m8݋kH;_Cjj)u !@" nկtIMUԤat DGK ǸdA\B]3C!0`֛RkhX$5NtgQ˥So޽z@/~o0Tٟ߿}ۋ/?~Jc?_K7[~|ܾ,>Wo޿{?_~7| Wo߽駟_>߿}?}tuʿ߼1qx&j&50sQ*BFN!q":K-Q|ɸDvAPRn(hF0F(3Ɛ/BXSpFQMB&+ւ< p!+ 7{q"4Zj0eKizѓ,}BZlQ=RsNL c/1@TD^B#Px18g#k&3-vatZRElH0y77]iuo°DJ1ڗJ< g3̉;:S!o0^7HRdbwQМ@ cGycf4g #/@9К< rM-fV }?GG:ÐV[55O=܍*(jRi d˺#W5RJqzG>z %uEK"ɒL]RÝqcJ;v59LJ6FdAsoFBQP!Hٍ#cOL٩UEICߕ!5^߳fSbEnI[agcio 0DY<-;iTwzBl"!$9sDԅZF-y=0LiPYNZ:*Jm:T8Ʊ52͒YMUwXfU]%hSH47<9BF.}PD(H[P13ASfb= qQMUd4RԷ@PfȐg}g"1.uFU$̺ #K˩%|W QiRe#&a=rFA3K(IV`"wF0BLiq1Zu;W#k@jE$ӭjQɿ:[#BǪr9 5_RN)G+A/IG|. k6W1 @BbO2*׸̻H(/1o:YQWo$ː":K(?#Mct$*/] dOllN6 R|v0FLP}8|&LFG0%fZkAf.5DI"=-;ϦGDx:v(/}$*܊SwVfΈ;6rةzvV-7qwEs)-7 n{ۄ!yH))-C"l%b+"i;XaI[o:&$-ыt]7u j:(Vz<.2ْw׺܎)hFPDE_?Y1Fi}ֵzr6L_QOaSw\NI4sIS#3Lc}EZ-wE oZİi=_wR`4ɦ*| $@F]6P4RR ?k+tDDgļoֱS)uUCbGN*ƝIEDgDZ9XC[Ƃ6QghLȄtyQ+k ۞7[hG^t`}AG ]}΃JN߉BDn=N w_'Uep[wȿ>BĞis 7; /ݹ!9U:\m3OXdj;kynB1+g>/^ċ,=zhnGYr12OQ5 AT#sWZ~([aob2[Ǹ=T3]Y_D2ރV=@}󛣩9a!]%VA}ަEQ]QќLSڶvEh\+y|"t|T[0LuKߋP'DP5Ne 8 #]) idhZJ<ԏZ l/BV'1K2YTNmRT%Li}ڹgiN;U׹F\ELHNxKn5-őX`Oyo[@ # 32o;@Wӣg dHti7ۯ4dv^oFl VYM:0NHhۦ`at@lu8tmf8l-6j,4:.>8cEW>3\9l%bCqt{ /1| Ee/pA׫[e< ӒQٲSQ?hu +o2-އ0@+‹w9avrIwp~E0SƋMU,pWh'EHL_VJPK;/*i$M@mQFz@$kRx kT6q<*SD87nM*N 5;+˻D za$ ٶiW z3C7YD~52$QA\c__`l3H܊p*LJ1PǖslXj=c2 :\A""BQd!,lǭ.(D}z-A2s6v2GduͺY,Lٗ[%خb8*fl,ʸuPbh3". =[^er0NC<*SQ6NqNrFv:g9@٣JP,Yh\9S~2h̻&db2i*oKb˱g"{p|^犝]B=G~]$JuI5"BJ$բTJ-<0Sj܍%ѣ##z8Vէl>"w:UMZT=^D ʒ4mvpN)10 )E6b= K>ըխ$|%Ķ̷\KgU3EKg:y"YTagTѩD~˿JώOԧHat (1A?fnY< 0?5*nRˆt D8T"W3]'JMS;Gy$XMv5Vz- q >+81LFD['{0#1&qEN=Xmg 7EQ?a s-tMJ z!mWkSLsT *q|PHI}$ KT$‡ L@TE t1.7Cǿ/~~ӟG'y$"Nt?'t%TW"tV4a-v7c$pGM[Q~muK-Ө] Ca8 _TH1-sTP1 dOF%|}x=Bfl`=t.UZv.xā "c<]OU yexYl,fD=.m;Dw׽n4H:Rr؎üzzZ8C0#ͻn橧:<Pc%"ekG'2dHR*;{>VQǐ#ȍ*ڠپ4K.XT2βIl38MF^R@ڜ3jB8&t:cK*Yg{{ILI?9t[g69G3^&9v]GJ %nX#5+W p`p` (޽DEBS gFf.)tO:oJuqJmFPa^'$),mw=dK)n[= \S_jkҷ?Wj*Zl-HgmH6᭵ g1#kM#޼ƎbG;ޓM8ell7oH.5FY]Bѫrw8E{, "PaOҠ }f$w?|?//AS->m۷_͏?oo/?sIBjcVDŽZiöHUEԲ-U bAn$#E ;zG-mw(ÉN)w՛nmc7/}bWCd]#UՒ$Π@E@;ΌX"fcd2Z[O:0 #]GެCʵe$Sc%[ [~<ڄ FJ;#z1sӖ"`Ԁ㋤o|Akb<6ZREY_A1@p ym,W3.n/|]fvq36;uY1ݾRٚTl4wT g7 v;stC|2`'~QFAL NY6"yTB(s P}N&-BdGdZOGoYo*I2;?.BK&K;[381 iOH}@ Kcv{ oINjѲU9xgN>ݣPRߩE5J.\WOh4jh td %y~G q5bXD: 9f"мNT+L\HC2]RO.Qx0,f(u拤ݳCfl\LJ9mnu6ZkeAaIS*9@ ]y7NB.t1ot{fOFG~IߠKnyfxF# #g$SkTueDz+g>%{0 rT˅玑"SƓfm~j٪j[wmS!N7d>D؂Ìbcd¨'zqݱXen\VA<-˔FDefA-$*Z iO ɠ|κ՟I4Kjfi4 F'Wi-H~^kd9KQl:xqIșI{61~Kj^c܏wh(,”{7.~B P E)S.d9A^NJYQb d*62 ~!:K|pZZ'mޘ[hbo)J]d1IshKamN=Oպ!nɂ;=.ꮬfD'd\Q\ԵP &u_c7ɕuB~g!7նKCbi,/Xr88e`Hm"43R#-Z Dj<Ղw_6b KSS}t$``1 g;YW>.UHD[f9_J;?ediXQ+7LVriWC I=fpRXvlwI<rLauZqB8GW%( g0 @괹Bqt3{U%H(*Z~Yf5a1_ځIc$퐷@}s(+="4_ӅƋ5ktd{A-Pd=@#SxBB l40Lx ΨB.CgVqK"5o !GN#rD5'bEZ9^c",m8DLؼzLƠ"t !nQ5cv6BQ*GFmD F²a"s%}HS@ ₐnu2>s91tjo{Ҋ@Uy6QE/ц/~ŇC5X/U::)-cO*-*ڢ28YY3pä:?QVY}tY1.1}f^GiW,RC E;>sCAp :1\Y*ĄRmsUOz(# gh (JKYן矾۟}Ǧo_~|ן?~/~~a1s}?i_5 cDƏ^:(:݄#0G3]:|/dS+T%V@$knjlj1XhI i#kG,idUD-'XF[Ӝ,$<m8.5 NVH82zbi[6I U9\wc#e[;ЧݨnQd5T ߋʢ؋ܦP5tWL5-AJ3|djpblE )byk'zEB\AD$Dpu?mɘ>'qCaAo_gU׍^TuG4hf=# #fdA-9Rh!#-mhYpF82I_R9$$]/~P[O˹#ZaDcAvjq.:3m dg=Uซ{@ X] z$,Y!qZ]Նr ~H/G `,M}12C%9?^ZsO;yρ%XA؅i$JByÔ QEf#qIb9s 4f=Ho>ąA4p[p+&<7]AYJesP8E ϩHr9 h|3M|3orIk %ojonqt`إ7=p].6}< u'ԁ I|VFv| okbi Ft+Jgi&g@-(;U_dTUqnYH!BT9F74O+}^/Tq?UNxZJ}IX> V"mG/(*#d[FP:kyGX/z$_~I vs;eA'95ϸ.VPOF; Rg]vUMa֐bR]m +]t>7"$O328c c!_ V4+$v$ #5uKf7]Ї!׈&:gQ*MW%As%i|LJe>IPFz+'nܢuRv*Gmo~9$$<lP39QtwWI_qtx,2" ST\g?!sV Mi@G=AUÆx"Y|s/Q7z3 6;;' gg:Cȩh`u4KxMdΚe0x34s '-;ZV ZA'oῷRZ$,.,*MYɏK&>stream Hێ^Gw7H3\B@AAjd Ȍ|#C`gwuVZf~6k;i+uJO7C euT[j3j\ ca&Εy?Zk\1F|2dmezT'Ȭgˎٮmh/}/Qp<Zz)~n\1S@ZÑܬK g2\+:K.|qXƭZO]Z!+7DDhK 7JKT콒ވq_W`۹ϕ/K%]Oҳ]=GН+N@В735%O[ZA*IPCei4 ŴO}RJ[i1 T&[ =5;. ! ÑӸZF7JwI8j&lqWV\HiBWG,#/(%߲h(}F#g!lC72.K+=K^]`}fk톸ACߝ[sĵIU*_,A4gWK!滃R|,9i8>~eNa62gP#:f@ {k~UM4^]4t'ղn"!f;G3,#WыuuyK$swcFdsPn6)| +,s"ЎT,$tLٶdzk3(!*$>)므Bt;Ԑh!\+s*L7*Lysa )#mZ+2 3yJ<3]eC25t2a+}vKO;Dg:ֆIY a 4G֤EPKtTN!r i\*He"YN"ke-$AlZP#-mgA,P>|:X5.D>BR$Q79["v&F<|QYMӪ\@\RSAe}#@7Qp9\.;D`R?# 5 *GlM_o:ZBύϱރcjPC|s~(bXۆ/׆O*3gsṢLE_U&+tL;$Jv;''mYC 5(Iʥj^nѴT)kgT p.D5d,E+gw%伺{RNREIZ<[5&{■tUIHΈ/ѳJxHFEs$6E >WkDŅʐHX-ܟuq|J̱ҰZnE5NUWLSb)M`?c`ݤ5(z)4D!.,S3N#z"IN 4ހ2ZBjKegYҲzjHvkX~'J$Zy| G|EI`Wb+8Dk؋a1AM"0N5Y=o8փB~k E}"4.ZCJ"qRݾi-û}Ğe-ΗGP>HȐ7whh\}GD{40Ӯ;y'%.9Ijϥõ> x+l󲲆K%"Ama>9K.FSrVj S fc !1-ܰgZFl>V gmhpFjϲ2ރB:nU/?퐒,jd0-8aPLXZ𨭝N4PqEI roy1L%i<hK{skPqHW{$cj15>^-A(a0e=>Y^:ﶗ]5и >a74rT b8H;HDtf%CFDY45Oh.MȬQ|l{@h~=_첽$T4q״E( @=\[ C>UsLȔovVBWVg dTmo)7ۯ(õഹ7 6zPyi&O0Kۙ.dDLb8 /%/R,[|' X&UshgkOfJUh:aC Ȟg>e/6Q9bN< jj/ h=d%(l~D2Xg/d-DJ>*w9s1'v9;8N5"MotP/ZiD*oSY-&AE+v4>kNr(fSTGpĨ @s:Q5 ǥP7F1Cs k2kpʏ[*٨V!Pj lΘ\H <0YÃhq<:}DoZ7Y5c6v^/'&~SCٍss9w]ɹVF<^2ʻԢQ]$M@%ie8FϬ7y!\ ?nE ?Bu~K!O#MB$?"s}Jv+Y9Np}G'%Ō>g|_ǖ p0C8ċ$;>R)vTU( "Oq:M;\fNF!@R%?f°Blz|sn_R6a{"'Z-\j/xi|]"a(35CZ"Sﻎ6CU-hj*E}Xۉ 'RK"Ϻ">4D86^RKs^g+Ou lhQu{E zϛ(ݤ@CmR4P*f^9ŲR#()]:iD{@l|ˮ"Kl3f$-i4-pr{<6'NvřOƋ4DN#OSJI3zLzп`|z1>HGF*zCˆtTSZDv|PIgKF܋< SEX&aK+6D;hb }:3%B x< 1 cOcJaŒ@-H\wK!vX EIẕ ~!q[BEMEb|רi9[fv//*RxƋYI*WY{T^Pdâ&DPZctO׏##<HOȓпPdY22dTѲ;!Tڄ~tx40l,ԧCINPdf8 _9|"9U i^cɂ''PTŧǬ.O| ldJ٣))f.*[v"& hOi>JɢS}n䩊_:~g+xgЭJf/[yK=0wovi@u P@J'RJEDz/0 멻*ʡڞd֙E!#|NZQQq@In \ڊa)K1Z7gTپDӬȈb AҫKc"$"pC)4w}xF$6gD&eMHDŽN5'ƒ-%o`Tm$\;oP0>N_Xz9SûF"/bj-`IyC*zoQ:Sq$éqfˍ!kk}0/3R^w$Z,h'##!|Qb?1M#B<I{4!!)UKm:0'5 ϛ!XnTBuRC(5N{NSbE=]#(Ҡ G緟_zVAt+mPC `a {D} @wkΌï?>TϭVlϿ}/o/IfU[$zZmq?ERIԚ]U75-X )˺pA,1)'H?k$쀊mQ~6 H8M1|wNtJ#vmV4omc+kBt]^+"TKRDkP*zj"sgFM,zst31FZ #"[-RBG@]SzrV7jܛQ{?&Sb7c`le=ӑ̹R@4&<Q Gb]暴>Ys ! uF[b.ܥݥY}eD1WҬDNe%Pѓ :2?M/m>K@'jG4b)Iv;u-) NmĿK1OfQˍl#"-ѥmt8M~7w7'8Ɏ8SqHS$" sD h'^Duҫ mng_m uCDϞvdgC)9-wALqOl06č"FN rcDzmɗoWr}ۀXKNżl~u!Aiϒt&O&,$w6"zQ7D2koϲAkb<j7TiA3RW9Q‡XF$ZLW3.oO1LbƵetVI[M漂4o3 W%aPᠦηzJ$uuQAlu c2 Q8tZyQ]Y!?FN!fۗ0K=oknZZЈ+<&32aM̘abAZ/5l2d6Ojj ձKռ=sǛ̓MRH|ӫ 7dF딅3YxB9r6T ҄?N6)>#_Jtf@e5bhͤR kh蔺SorŊw*ZcU׾lvQwe;Un}serME]H\L4u_1tpu*]i3YA#jw;ZFhS g,@\Pm.^;(BY|*HXk:!XpGNAuJ$O{<1T攺 瓫 esmP7yh@^ͳB937"V͹J5rBl .#ѽ*l䠲/bag8<(R,Lj=CW5bK|W^Ѐq''2u! >v;I"Ue/{9ꍽuls=UivJ{e9o(ufu=* ]&ruYu1>C*~JwP[A{glBaE`3˲}wttĝc%bˑR/*K:zp9Lw=I2-ժ:; p"L؁Ş;w¨ Rw ܟqܙF\$)hv_愼}G)w˘fg.JytM1/_]LpnU֠X7iH'`Y.{wKj-ˌ5TBB^Wܢ+7FdY02M}-0EՄzk:|U/MAa-Niչ <;.EIƹ4E맴B'-4/'HeYB 43R#-Z S,ʵ7f)";VS: M>nN#W\佰8 >#4H(\a-L*u⣀r9Ea^ ?0'oPܗ)Y4NgTmyP_`>.4)d|wmsdvl=<# rw(#`=*ݚm$qDe 4 GN 7v#Xվ\ٲhdGQхvW!l=(^.-W+=Ԑ`/0@2!A gf2VWSg벪XDvѵgB۠mB&֪6ՇhC,LWZ>St&} ZEx('0 q<º'<ٟyٷ }xR%3Z?y8m3 aGWwX9z$m~v7>QT:"&l&@Qʰ!Qn~yl Ʌ~#TxZwyYϓ_01Q@ E>}8A+Fb}"2p( T vmtIMX&~[|8<>Uz}vAcLi}u,du>)$60#DԻܺ%Y(ƙ5n1}vN T49V{0z2ZpCEp *3(Za96#u+}u̲z*꓅M3XRpxR49?շ|J)~}/>曟f~FWxjw4f^e.JNVKћOfPX5my^3/c<%=T#c@s`Xdih!]`7o$NΖL@13 N8R2 (r]&ÓeTk9W`41YDy܍2W@YfI턣㧨1Uu斉7M6bG_)D|wu_+,ֲȨbr&FQP *LvMU֒Rje>tˠ~e N[wP! gm@i;x"Ywa6N*ਫwы}4GFpf: vsӁ@$9_;@]{]@g q蓓ܲd<휖Qd"$*4>jj&C$g^6A]=7p!PE9qw:=1С<.m}>'e76t*!w,FTsh%v݇HXD)Q{͖sRoJAuZ/(2Qr+A>ۺF')kfyB;~$R:gxI᫆7])肝^Wi"(U1%C;fgaཱི?(SѽlxNe|l9,m x8L'Mt&ȭ&0ȴ/Hgڄ {vA}/zMnEN9+]Π;k-u[}4fvr}A%\_a@䌳`ZFW"`#lg\7JH!AW<$.P 1UMI pz)dD39"bPRʖ ? ̖8sŰXR>"=L25Fl'7}킪{1~w@gc9wAD{!S N#~.~cQW MyAyl:'Qpbk"*x9̹Pۺ2#rp}klBR' =vt8 eCޠAbAT]u].*:HV.n988.r:u^"VOO`ݹı5MBǛEXwc͵j (&H\;is *]KH/I|!Qz6*bT/؞"SPm.A<Сlz!^Fy}K/_k$ep(>׳N "Us.;XFɓj21&b&v]^Y_(3Wۇ[?;[P $5"XYB{ͱb@ ΚP%ޝ j,u>Ssݰ8P+M)=.-SZ)/ZfxQZCK2tv>8 .aqqL|㑞dT]unYHo!BĉٹFʌ yJ V^aOKL~X 撬 rX ^-#ָt].QA;j<$7]qoN_1)ɷaΐ@'Z "~=ͻV8B~Eٔa]QCy5."̆+Nk=gĈNaQ!%V 2icW)jquI<|iLӳLs(&qKfJ N]CF1Fp X "*uٙPOQA昁(=;+@2ɱj (NQ Q=)fyD?)IW!QxOokA5\h2zuTAUKA҇'r"@"•eȌ|U !3SݽCT'H՜0c}l+<\ofe; j#rmشݡ̌  /#א\P8hі*PZ1YR9Ϩ*v19z:O21th:=ý8!G2!G8BZC/hMfN$flP~@Ρ@F[;*H:Y۵Zpt* Ei$l©޽LjU*`!+@}k}w0;p7e~_`C(s' Xv&T_tjd]#}8-"xSp0MOѪWUcJhTkzȴA:xYt-e"(U>|lJQ>^tq!tfB#ISxݑxfkhac؜hGIVV~ny JI kBwo+ۙkO(e.&&%P}HB_~BjZp;ԌG?Q_j1k{ 6-z:ל&1_|j7#zƮS/9!1svn]Ĕ;JeJB^\!Nk`<$ŃxGWD! DMݦUr۵ _Yx?W.Q9#5}t`Ƶ~Uh12ҎXҖ=SW;ATSk34g/l\*PJҲtLe5Xh9,kIӦ lԦLYcdeW݊ @k$l$gj+*%;Gk3(^V,wd#R<^kdO:4h[JmxOb;Ow׺գ'uiWJ>v! TBDk +v쎚Y*lŪ-Wܕ[="r68wTZt]-H4&rS$.ۂ#b:5nVuP8Beq6wfϾ=H%7,Q:Wr) =1]J.Q}&y! iH8Q*(?(dʅvnWv9"ɥlWeT=ono}SD [}@(X@-05epGP8)lk:zl*Sw#4c2٦4ᙩ1w!4}uBECAFCVہ'%5)H:t)er8<-p?~~oӹ?}۳/|w>w&=LMzL@c"D-){pʑ0|I_$lؿ/X׋dtS9qn2:- VᗩL5V]Z}#(3h%dus1Wݗhk35ṞnXj'+\-Ltk^dNZ-K bqY) {fj`WiƈhܵbWk)nkФAEEy ξ(G˩9lXha鯜!*h б3挍AҜ EJ%s*NmYq"5N W*SsLwY"wo-bcH&@.۪8Z]!UEA|e€95MJy~hJTlk;ֺ_2)%s8AfQITȂuFYslpIR걋0r>-.4kܖF<%ΨsnMQ\|g~-<;MF6.#]©Jui2b%v: $M/[E&%kZ(R$֡@TBtq4_5Mju;+/ ijaÎ Oiԃ wFD.Ѕ _#3/hz mT[,,j`wi?!\tiAes,ɢ`sΌZ ZmtTh v.8jcY}ݹn=:UҴРN.n+ wH(a0xȿz֖yC16'lS`%@|{4!u Vxc[Cdv77R=9' lVƦJ"M/"ebq!Ъ>,ZXzuG6eJ iK|R7QQR}Pn#ZQ)2cWkMq2Y6B@,eoTXq*m7`˞w@E>_e9-3uB\9;8T"a~(4NbDsnF̶ oߣUkVyr2S8h.2*{aT;Bu'oTUGVP-|M4爃fgXg/4zA2c^1x*1>00#T~ꖵw釃|2 H@l$2uhvgn0T)&L6 183sU#6=d>%TQ-RE w7ռa}?lӧ7dM%T Kaַ Z0PLL@6# 9P 4ؼs[@n7E;\#t3mt;,hnx!b6O[UPfë5.) 밁=7UQXtaOsQWBVJ,kJæg 77s6-PM?T)C_U0FU3\̲4Au6]x"9r[[f[W Z:JvU$UUd!lXE௑V\lH.o%=zsURy0+ꖼ?GI,s!D5En9Y@u/* 24|VZTFv{&C*ԇ:Z,)DIƧ qͿ8:hz2G~C|?(>`_=:'W4Ry0f-3nHQJRÅ釄[7qF|Ii;J>O2&]('}*ڠֶu%A;mEds93,FO$an@c2 h葍FWȫaTOJ FF=9! ȬF֊YVi"ˆArӳSFnpF޵*UH @:\ tLOr cyX_eWA'Ǝ/ 5P^ST>8vitbꭊ%H<+KiBCVTs;iT 'G0K̩|bjcCK4 "ܠbLiHiơؤ:X({riK?6¯`ЅB:5}I`*]9,IA&D,[ ʦznL( .9~E%0Y:T Ff9 -F-/TT&WyE<@W۰WiPm_a.DTbir8cqVG4"A W.[7_*Mr2%سә_:ú(L5RT.Xfo\"׿=n~*VAC8!S *=d"*c0O]JModYg(/R1muj$`|5*ZIcv!/?^9KJ#f7"Nҁ~hB%h%aȰkR!b#g5H' :Iv/ZMi5#Zp=j8 t^\DRG!m0з3){ .޲2zY{6?]`j=4 z]"gGRKҳͭd2RWD_%sgQQTeQ7y%N0ԸTaG0][i@˘Xä WX(Gdž~vKr}ԡ;FX}DnC=M-F;UϲCXoHS/zȼMi.Qz^8bkǁ"V,=*@l=slxSPil<D2f&H_.6ǥ +K"'P953`/K(O+f;˾:D7Utw'ɑye< ǹ[W[8|DT]?35ifwSAu47C]FT$EU~8B+FڅNZ#ӷzdՃb=x52HxNj 8(fQ2\FI:6S U%C"*Z:lxh` _h|o/{YW҃K-|}}w~G?nh)skf3ѩOꆒ[S#﫪\6`^fF$h#g9ћa\^Ffm`cH22O DKB&Iڞ`W?AH2p^k6Jɱe9DU 8:EP˴m">K39 D"$vUàc$YLod0+?]̣&[_ M˥'s(F ze*]:ƦN?q.) 6~44' 6~&i%cJ ad}а$cgpHi ~S!.X# _21[fIg7) E'1:ovY%MܝLH5Ki p-ŗHB[:{Yq˒_^ 28SeKGL3v I֓Q^#Y<- e-#:MU5߾KpZ"XNO'JpG^CFk`Szx-s!p=ږoY4?뱙΢7Wi4Hk(能<)?}&^r0պ +쌀FFPwM)]SQe{O O~Hwn:9pȧ>Rkn\},X}dt6_q^iXtBdUn)FuX ~~6d,rh\| yPF؏G?MFO_oR0grTDm.}(,Њ@OߙfLP!'f Zmrl͓݄z5i; NsjaÇK}43o+L!AVvΝgܜ+N4Mr{R.`e79#xa}Tw5Ӌy2]25Յu2`]ٺ: 0pš }#MjcTDi! wպ'jsQ'g=,f~jhCv?zi;QmER.U(Ǝv``t4Y]&{%}qvM~!@OEϙ*p1zB|i–2I.u:H"N)uZ.о1`si$hC;~RrUd h:Bʏ?X/oFC3=Ěqp_]=L !{S#9R+Eg4M=<ڔF iOw6,"`צ|OX²;խMº1K4duE "jd,`ueۀuikPL;<(CCRN*tZ5ƺj_:D1ŀܙJ8W]YPumZO}mIpwe/21w4:l}?;!g4H;VftH i\ZZJ7xS:UZ0(X>OzP_ڸI23`mk|Q1GKE=|Jomkiz r3+( ^ڃ=TaM4uV zqo"HMZee(d,ҟ*]2o`"g|nBkkV/EqI,k]>`$RY8m[Ų?8! 2{ѨL*fQf9#kUD0)_R ;‹9vd|v|JD3|q?YX¿?Ȁ%v5KNQ› ƒ1gP'>e% ;";r##`'-&PTD";PIr)!8A GU-uv]hȌ#WdD t)++hYCleXH.B-I+#8q U ^O |zՂ#%玺0LI/݁(JPz$2gR݋.1O8(>2K^8ub'a8ӧW:aGAP #GiniA`{%dQ{gfGsqL@Vϓ XVX9ͦZրjF YFM ꢐq6Xza]~Rc)*?P$tGj8_蟉ked n #` -C&j"0MIuB&4ZQo'x,OAnP6z^ns zN!4Wj(ҵk_.8a]0@g4Z@* >`K$ >ಬ5eHa#wRE9 J/4Sr_DQ,D>gЉ :@ݾxuWNY|[2f)xIC1j=(Iʘ4k3k<}ZB@Vvu F\H'5p8~DMmA{O)z !itW2f#'K^@mL 媸aW2*BWIГ7>_4rxNV}.V)xPZ%1?|>?/$>~x_|woo_>/oG_y;ODւdht E: ˴ x귮[^mIב ./nFYQo R׃A@l$b͘F,>Nk;k~\ }e*eHgs;C]<,QH96|PaKBj>h`+3%ӋLK_ ?Ԋ z%Q!ei,bH421RCȳsQ8w9R5W#m;*S\]'=_U*41̋ ވgNeU\ G:¼RAF#_FA }h/J<3mqqpN%v", @(Y ycǡ .:)*r,(z'*e%N@xA^F죪1dEԅ(Ch9;(;c= 7:q syMPts]7 CN(N( vR׹ou< cEv h#b>_1M"mݍ`8SʱaY?*>*M)r 3n#:g.Zh-aWMU581Lp Io 꽱b`Hbc's$H T=6*Qa9tQM9D۠`0>ON1<=F(_]WkP=:iHM휖v!*"(>nr G}F^aw_낔o&*;Eܿ kYDžXӌ`h< 7`6FEI_HJǜ#.$D]N+b6U`ZATz8;PMb: D2?!\;@mK]69Ew8r"C7Rvͧzl:8hbDg8$QwDTM#1/~1;0 dKs]AQ77enATq in>Ԩdͨ] _R@uFF!LRh(p#?J [O3xewN +&K/@l\\&dNnr3@F{x 聞e5o\sGv6M["mx=,zLCMD?$7w%VC|RG =+NY7je78RdcߘYE#"(ʛilEpR.6e*,+%QwFfc -Tr7($@9@ bհ\\gV_.* &{=;",ײ`zmj\Ba3Y@XR6H+6go ˊκA}A(rѰ{QҚj~:^\y *HBɭuŇPfi%zA|'Of,ʱu|i|jz;yV5A  G, 0)^Q~/N"df8癹xCK qP&%PEXu7HX37T~ݠHa0zRDqik(hS+@ 5 kĿtv]aj81oayGK B }\pZ'Oܘ 0עa5. *Hy$gۉLYE4oaE VJ{$.F^G} N95Js']vg#Sn*n|)iT4ڱ\YL?=<1Y%/arԎxE?XǙ@ƅұ6.*؊(kϱlᚓ#IJ D(M~:|Oj!:5O/_ GdL밞.vv-O7xagu.XFѸzJܱh/M}"cb}MΊS[`3pt7웖A1CmkA5G>!0QBLqEM)T'$:p/im@Ꚇx&Z'fRqAz=/Xё'S&@i(SNiXQPGgwpbB@3/f_?~ ;*hjy:L;|Ў&ٜ*NߤF#Q']^XNԌIG1w@Xxv/HBpL61iq' x/T|hzxʻ:$L17a=[N99WH1D\&s 0#Pgbx]E>z<Mgu\qCjۍ*;_8/`}$OƐIA$h 8ݖiva"0W]Uc0OA>!la(H (bYyfuBa(F=Gxz؀G) R-gB`*:aII4F%(Ovb4icn72"9>ljZ)XHxQFE^O |BmF`s#Y: ܟȋ\ӱQ:C*F0 )ːFik<<>"H$shx8G Dfƕ tAz1@f%xO! 1P)ImߓQ@g ]e@!#Ę ɔvHMexe$i>:Ƹ" JS>ru)}j0~vօbv`Dz-ҐP|e69uJa ps$ G#]+c-*U(l |~l]Eꍌa`79aY$&`H=!L䦎B,+. TnJ%4P+[ !ʚ4:'?)UxžhVq4*g)M3 ?~:!PeSCQh55/A͚ =#=X爴YK#lBz:7喅bv"^DmЖ̡<%pqI仗 G<ƭ;!`Ӏ/=9N8 cF}2!#"aoݸSAh}FE-c" aY):v A0fu^vd HԼf}~eGO:Tx<~? sb}dxwexxxfixtv]/_~^Yݬη%.~>Z/67_W?nnvߴs(oV; HvO/Wۏ"'_G]ju>}~j= Z}^y[%aݯOyo6x X3xG=0 `p u, n v\@lp"p^=pE,5 ,WhQѰ̐EȒW0U~sRea[uя/|qAmٍ5)RE}$4h1J)h=Vry *輔(Gʅ"GI;7V;g] "r5JEJDR+^b1S98̵xkW)gi .`Yu$X5'CS>N\{ B (5[9B4 th# miZ#|YJӎ]fLzfDDvxOɖ 3蚄+@K0 4]GF'%bIŊn<@V/5,8`?| YS0"!'([9քMzF]g zuq\2!&x.q;JdҚ,=wYɺbkl-Ү;=#E:ꤔ&sL:d ^T@ gȫ R#Xp{!WvmpEw4Fz,Tzi T:Gq^`L `H b(lѩcр+*e4/!^]WW!3*ܚ>".NЙ pYDogs3Ʒ(7f5 ,vv_kZ&æƦ.{Ȯ]rxm!~D9CLV:Uz6ƨiL!9c,5s:l48;\4!RaߛLX**U6S$@N=- z4١ER֨ʹ\\"Cg gdq9̘1ť`Ye0PsjE]DG-8VSOυ*8a\ &`c[=9'ݻ`" a }mVZ[ h[!Xa CU֐BYDLރ4OaGB]"H$#kaWŏb^S":Bnb&h/}֋ݷU/){loM7QKjojF6 &$^#+?7a`&ajs!amN/&쀧 V(]MZd'U.C)A_ ͛M`AbKw^Rbt2Qkil`V6 endstream endobj 35 0 obj <>stream HW;r,;\C$>a.nx2(&P:N($ަ?>?v!j>P}- /?-;LR0K]6 T.pn.0 {翭ylH=tЦ[.{O(Q&'*on`p]CE #{i-+~L7 ӹOmҽ86e+j T}BK#e_olQ83lHr2{PP#Q Nǝ;+|Ice>^+vqCJ9`HWqq䍫+2o˗%m|)5Ra?pU ݢH/Ϳ[Խ V_32ׂ_ rlam٘2*Y27R*B> 1ٸ3.p|R0-gI+(AEoF攘͎l@r#: l:B+xNVtR"6x['xa4\$at$mB0kPpZ>clHk>^!uQWdKwd8֡UX+iR0ӂ6jPشbq^cw5Y-aұo>OI{~[-$t@SIF?QL |Y,ۺG <ת9(H* b`t*3~WaJ]z[ha}{Υ`BixVx~=`[lwU$/}td嶒KO0+vkJZƒ+MSсhVix7s:fTZawC:ާ9xG8RG%o[XLT5VWq*zkU`|Vdi#Y^z1&$/QFۻasr7#'ztMƌG1\|`=GPq˴cP|nU2,K"0r }&H}T8Qq >uu^Z/6ű;,'`{CxK\c~:bDαFE,6.^hJ'A!:Sqx\P,n޵M}3WΑ)mO,&X5 V:8c2Z1@B<(g`N$HYXh^Tp%=xi)+ qA*ӻyUʎ꧃ ~a}`qf5 >%warmp ]z-3[a;^+o$occ$% y*ֵ3a*;Bz/3V֛V B£33X!qX̘jWRX?ߨ{#Xo ' c3!pJjCMaH_ Q\[W)t+4`R"~ `Rlc`X R1+\3oԌKD+I vJ^J Zf[;em5]߫>*Ni\_cM?e,Etם5( m@Fr(ڏM҈7B} ӏν^\_Z;Wͳ\k _Ԩ"%2IU.epFi=my\y\\0Y 6|}nhK;\ɢrjuƵ 8uPE{ulV#$x\93pMx`/Щ>~ôtn&>]AcmYˑ)LNЇշ42D~+tݪ}dN#%H_]g6 !0׮e 1xM's6!tZ5{Q/RLa8xʷɍu2+ 5*"O#25|C^F1K٩YIzmJ~|%s?A)gӊsZI\;|UJV rG8I V8P\LF 8.q/3(\_Œl%Scy+aI1Gb nk9 }XaG1١Q`PKVU\5@^قߒ1r auKXdA[QiqGkK8(f O{'|ֿ;rHWQ&W=-iYCڗ[cֳósW4 J1U3?)j{W{ +9Zz@c4iVs8ϥC7qkM1]f, xDk':{)ӀN:o? J(H{T5RWkR;#Vԗ-aըS}M쵾$>vI79s֨$ń-. dEAd<wplOS[ɊQ*{0F8 ٥rтkM..'ka6\l%'AOު-l?~zX`d*'x'̕SQsn}?m1)Jĵ&;b]5O5dݎ|Lpu]HA})X=-K/mم_z ]%Qugb!hfڔ[^V. IkY_$i1u`q } 2mu:\I >G$fa>F[?JFSH5B^sX Is,QqLFﵞ@N5Fm6y!_C N#V9 RO SH:(1!'x8K^-+A[0s,b]Ԍ+$RGu2`K96 7I2ۭ:?X~._ҋ}Y'h9SY˸wTgfCRV+|/SkcL9 t]unU.Ծ==oX+eZfp]kK2Q:p Yxx3Pb`*K f bb6ʖko/^ٸR'ؔAf=/Vs:^ -^Z:nK3fr*Yn*b`<`/w2;#>S-)~&)|Dx~U]i 9#Ցz4%$u IN5ȩH54LB9(Fs CG|T4 Z!Wtr 9КRȭq!fkvJG𸦕EC #{wt71H•s+5Va;x/!/ν=g5` k*m@YO?a1JȬ$i\FdV 9\:jI9{'ֵkUU.gwˮt΂q[@! Yt~epܞkz1tHldL߲uFP6k;HkgO}DE8)7٫k[_荣VK_]\RHIB`;OM'nȦ8<\ZW[=|#f¬|f8{@ f3~} }VOi߁sM*pvAc8O.pp~{H`!(K}fREq,"@Sc`awzA)k}tIkE7%P`B% Vf`y' {:"Elmu\{kt5wߡʦN~Kc%wytkxs3 P׎'6G]jbp1,0(i}g 2bWu )w%NH+6}Q lCYQU W2=6 c^kF,xڎ p&1X2i(P@·$Ѐڦׅn h3U뢷 ׸p[v5P֭ R0(= ny/ZTyMI{JZֱ,/U F  `7ɭRjZCǝ#]^37_dA;K NW+yˡ,ZQn'1k9cAʫÆhZ Mň5AdM.35 ltb4w>U8lJ"h^C5wCizAwD0?<84r Nl-^uf@j՞/Wˬ^R:,g °-]ŵ>+]40 0Du1h+S8G>K+6"XY k\մμWiW/ZNf%^-`η|K{5[y xyZ;h?sԼzl3b@ȵ8ie ~j7%p{gU =. rtʰ/p@ Wi2AX{aea pRr}&Y x~ q}*E {6l錀ɒIpĜnŅ'Zi;z iq7 ʲZvF{|~Mzu7KX&8  0мLLpqj^Z Ҙ0O%IW ];\[m Ae&b*A`dP˹1^w?I’x,‡w2OA*!Rxm_xkg$c(񗰱7 nGmƲ ^#TB*d@? Ɉ~O3qs >Fr">^ cKAymcFM9##Ou]Z1/G m|.p/ko|AViucZL#]Z !?6S< O|k^ a΢o4>iR3+/@"(>9XPbج9X^8 ]eLY$ `MQʷ +/&Bd*dꁚ #ƨ=9ゅ`ɳX1TaudNY}pb*l K ][%M4_}(=ˑ{v~_L"4@ug}>jhBe!P>6r!;9IQ #*p:k}`Pq븡OwE=r6ifYuQJZC+<}HWZ@S$rHyxCb[^R%FnSj&xk= 09" Z㐁a08cnB'hhJkHi|vꀵ{|C}C'ew ;3.&W)Yo^i**-Z;T;3,|ԨȠ߆ț =M]xV;y,GBM3@4smVڋ81&L1Y'&H{mYM@]/_hliƪQ~mțXRYkmْL%QV"jɋ"3tPW]-Ӡd0Z*5K 5HrcU 06SL}tŋNe~%p6>5o:)bl%K騬;i}yc3WHKo#,nSgmLwFt<9" S9WEwwO;'ߕan 2m~n蚒$ćR`#2@`0}\Rp^N4 +E:v>t~]I޴F.tjFQH J▖AKԙAW~W耆 :P}D"W7*-RQ,@My}BV,*S30ꌽEFWz\*2*2\\t4UU!-hbY~mh+PwП4 iӞPGz[mE:@ $3W (:xL9s6|R!X+S1%v{cvH,+}pN-M&.=+J!H0 dcԳ]A2(YY| qDX`<_@Iy Ơ;j͝my)F-^ZrkP"0:+A%K!#^A,Ns*BdALm\QwHR"n׌,kN-J|RPh8k1m25$LfGRL͐|fej1kXԼ֦N}`z@$L82WeثNCYJk%;"khSrQcKeo2Ͼ=ԁ}_H+ugtst›^(A[yN:!uY}|-rQ`(OM %&$4Q)'mA#麪h R5@ y:4CNZb&g8഍RF+8c۶mG/+eX,6@ayw%yOԜ}-a qW <] i)$v6% &'B:)Ոz>^% n` VuOnb{=L=_?ugIir $ΚJ]- ϼ alĝ w4Q-hꉗb?_} ٲxcgZ"{ .߇& Il E݄ăЭ]ޣew?GK0+ YCkrU4~7PQՙ)@cog(1 jJ,ól pvgu?af.֔pD@[Ӻڻtڹ] {v!ݼمGs`u߇;C$jG_LbvDKjodX5Y =?Lu { P(Z$k9{}PN]XET\[--ZMt;hd+P|0hߴgMW0l6C3g^%eߞ_'>{<ȩŧ8eie" :VM}`ꢲ ?RkP|wwtetQHVxRtCYI $$* !8eT&}j0cG'p ׫׋fzyrp?LE:7Z,afbٓ/ǯz5]߾?v\-Wn?o7Gbyw=vFܾo?{=zZoϾ^\l.} +rO9ftHztbft8o>HnGGr{<__,j3؜.4V(8#TXz/_]NGa5{xXr<o)94:Gc-1wpw<1fYLW_w%>'AOB:]xq3߼3vOC4i͘]/_\lv?NGj>.߼^WÏyzL w~ |6|7bZa(d,Q36!'B/C-yȏ׋x9_q\&?ٯݶq(t1tv'MZmp.Z2YӢGgcFc$'P|7"<=Mؿt4QN'p& 5iEq/1-?؊C1p.-śVڅk=+´Zq$rVYfVАɂa4*F5ˤRQү%7 {z𢅿Ihaoojj_8uY-Q%\4`|YcQhf}KTDJ,U}s(m]'D9SB͍e9~_<455& kvfCޥI1B8yNNg,v;ܴTx؆}31c4ٽX8џ dCu4Dp\+4Zb#>Ti뉮҅|S@$oʡE{<o޵u}>stream Hn:ƟZ$A+;FbS#-EPgۻ}e;ӜjSkmxO3曉\Zc!Ug˿$U-qh˷iv"Z :a_8IY7>L,z)xnߵ=p%A/7 `OZ}YDe5þH"\K*JPQ MpNz*THS+J XD\8Q)7E7o'#.ا[ky>BkNc1Zu&Lr_ @X1$kMu6\BSÝymj`ĭi鷰dbsl1T Li}h;9 u.pSHF%[e7A.ySM ;!#[="4Q }Tz<&}:럗^=&we)qqcQ8pRs.v7@n1 5䲐ͻQgdC7yKGPǦ$.swaq*v5yJUh^ Wv;,*A FW]$U,pV<,U yG!)OZDŚ翻ȞUG⿇o&3P})ƫ,|c^CY^7SsjӖ+٧5ۻNhѩo"nzn(ͽfy?QV3|\qFwq%Ǝp|E%(r pMpܫ&sN)Rna_J#ٯg+Ěz5/i<]s{. }^'G7yGvAܪhs{A'"$m3Wdf #pըYg<c pA&Y@o$pyJje8΂fv ~6΋peKʈEDd@+CxSH 5#K? InGic42I\pj5 pwƓr0E纇q*,dbU6KET4&m'9an>?,JFgY/xwY+>OS&)&k?HbYD-קB.>99y3twY&dI莆NgH]֘qD&j840FiƐ4o&Ő}1JK*u7>_/ߥ褏LVB1A>| \U!CsXw<.tj?KDAjG gCb:LܢF͠ -g~,LЕLd<9 }(m6?=YsR%BI9 g!miL0 +T)9TŘSRfgq6M9ȁ+??ɂ},{#[̦ytBwa"sC ̀U<%U2nXDeJ{.Đvro8:mm )hâdt~地ۡIem^r~U. \j!9,D;I):0@XS T1.`}z*?ŀS-xzAη^'G7yG}i]W [`!ùPӯ@YdG3 5UA9Q|(҄B\/ 1Vu)RcQCq 0Yg4&fHw |[¬\G; 2S`ЕU^B4qm6?:Ys4\u`cEwdb `d4nx`(V)9Tc6ʆC^DZݫ^ս?W߫:Ef'1%^ޫf"a̮S9j-ALU5e-lhLkJ|j;%n^.{d[d!C1Cܢ<,e:5sX:QƮOmZ ejϮ;…c~<[K94Wk^Fη1+*rqw&#d{0ԓJ/Rt!l(.{zTVI fʜ^_`U@?V &e7ܧnIa4m*k| 9!1*)4LIuXgFWLbmv4'xr㈱V$5]^Sٝ`&:0GedzغNqrqkU&kND]2NPfg# ʢjܟ j(oi|LEٓށdFJhLUmzXopc .~<#8Ap f /)N)+2<5tnw5Kp**ɸI(<P|`HJkCFchl=?Ka(: O\g6p@Ūn~SjNU#3KpТ?=jp>nO~\O!k7ϯ۔֔C|=u^>洠 endstream endobj 8 0 obj [7 0 R] endobj 37 0 obj <> endobj xref 0 38 0000000000 65535 f 0000000016 00000 n 0000000144 00000 n 0000048349 00000 n 0000000000 00000 f 0000051783 00000 n 0000052009 00000 n 0000051597 00000 n 0000307125 00000 n 0000048400 00000 n 0000048784 00000 n 0000071190 00000 n 0000071077 00000 n 0000050650 00000 n 0000051036 00000 n 0000051084 00000 n 0000051667 00000 n 0000051698 00000 n 0000061875 00000 n 0000052369 00000 n 0000052637 00000 n 0000062134 00000 n 0000071264 00000 n 0000071702 00000 n 0000072709 00000 n 0000077968 00000 n 0000095118 00000 n 0000110451 00000 n 0000126749 00000 n 0000140437 00000 n 0000159313 00000 n 0000183229 00000 n 0000210715 00000 n 0000237925 00000 n 0000264762 00000 n 0000291193 00000 n 0000303297 00000 n 0000307148 00000 n trailer <<4336D061D10542699B0CDE57451E0D75>]>> startxref 307335 %%EOF organize-3.3.0/docs/img/organize.svg000066400000000000000000001743461472111340300174110ustar00rootroot00000000000000 organize-3.3.0/docs/index.md000066400000000000000000000001541472111340300157100ustar00rootroot00000000000000# Welcome to organize's documentation {% include-markdown "../README.md" rewrite-relative-urls=true %} organize-3.3.0/docs/locations.md000066400000000000000000000073471472111340300166070ustar00rootroot00000000000000# Locations **Locations** are the folders in which organize searches for resources. You can set multiple locations for each rule if you want. A minimum location definition is just a path where to look for files / folders: ```yml rules: - locations: ~/Desktop actions: ... ``` If you want to handle multiple locations in a rule, create a list: ```yml rules: - locations: - ~/Desktop - /usr/bin/ - "%PROGRAMDATA%/test" actions: ... ``` Using options: ```yml rules: - name: "Location list" locations: - path: "~/Desktop" max_depth: 3 actions: ... ``` ## Location options ```yml rules: - locations: - path: ... min_depth: ... max_depth: ... search: ... exclude_files: ... exclude_dirs: ... system_exclude_files: ... system_exclude_dirs: ... ignore_errors: ... filter: ... filter_dirs: ... ``` **path** (`str`)
Path to a local folder **min_depth** (`int` or `null`)
Minimum directory depth to search. This can be useful if you want to only handle files in subdirectories of `location`. **max_depth** (`int` or `null`)
Maximum directory depth to search. **search** (`"breadth"` or `"depth"`)
Whether to use breadth or depth search to recurse into subfolders. Note that if you want to move or delete files from this location, this has to be set to `"depth"`. _(Default: `"depth"`)_ **exclude_files** (`List[str]`)
A list of filename patterns that should be excluded in this location, e.g. `["~*"]`. **exclude_dirs** (`List[str]`)
A list of folder name patterns that will be used to filter out directory names in this location. e.g. `["do-not-move", "*-Important", "Backup*"]` **system_exclude_files** (`List[str]`)
The list of filename patterns that are excluded by default. Defaults to: `["thumbs.db", "desktop.ini", "~$*", ".DS_Store", ".localized"]`. Override with `[]` to include system files. **system_exclude_dirs** (`List[str]`)
The list of folder name patterns that are excluded by default (`['.git', '.svn']`). Override with `[]` to include system dirs. **ignore_errors** (`bool`)
If `true`, any errors reading the location will be ignored. **filter** (`List[str]`)
A list of filename patterns that should be used in this location, e.g. `["*.py"]`. All other files are skipped. **filter_dirs** (`List[str]`)
A list of patterns to match directory names that are included in this location. All other directories are skipped. ### `max_depth` and `subfolders` - If `subfolders: true` is specified on the rule, all locations are set to `max_depth: null` by default. - A `max_depth` setting in a location is given precedence over the rule's `subfolders` setting. ## Environment variables in locations You can use environment variables in your locations. You can access them via the `{env}` placeholder or prefix them with a dollar sign. Examples: ```yaml rules: - locations: # via {env} - the "" are important here! - "{env.MY_FOLDER}" # via $ - equal to the one above. - "$MY_FOLDER" # with location options - path: "{env.OTHER_FOLDER}/Inbox/Invoices" max_depth: null actions: - echo: "{path}" ``` ## Relative locations Locations can be relative. This allows you to create simple one-off rules that can be copied between projects. There is a command line option to change the working directory should you need it. **huge-pic-warner.yaml:** ```yaml rules: - locations: "docs" # here "docs" is relative to the current working dir filters: - extension: jpg - size: ">3 MB" actions: - echo: "Warning - huge pic found!" ``` Then run it with: ```sh organize sim huge-pic-warner.yaml --working-dir=some/other/dir/ ``` organize-3.3.0/docs/migrating.md000066400000000000000000000102771472111340300165710ustar00rootroot00000000000000# Migrating from older versions First of all, thank you for being a long time user of `organize`! I tried to keep the amount of breaking changes small but could not avoid them completely. Feel free to pin organize whatever version you need, but then you're missing the party. Please open a issue on Github if you need help migrating your config file!
## Migrating from v2 to v3 ### Locations In organize v3 remote filesystems are no longer supported. You have to remove all `filesystem` parameters from your config and cannot longer use pyfilesystem URLs your `location`. ### Placeholders - Use `{now()}` instead of `{now}`. - Use `{utcnow()}` instead of `{utcnow}`. - The placeholders `{fs}` and `{fs_path}` are no longer available. ### Command line interface The command line interface changed quite a bit! Update any scripts using the CLI to the new options: - `organize check --debug` becomes `organize debug` - `organize reveal` becomes `organize show --reveal` - `organize reveal --path` becomes `organize show --path` - `organize schema` is not longer supported. - The already deprecated `--config-file` option is now removed. That's it. If you encounter any other bugs or problems during the migration, please reach out!
## Migrating from v1 to v2 ### Folders Folders have become [Locations](locations.md) in organize v2. - `folders` must be renamed to `locations` in your config. - REMOVED: The glob syntax (`/Docs/**/*.png`). See [Location options](locations.md#location-options). - REMOVED: The exclamation mark exclude syntax (`! ~/Desktop/exclude`). See [Location options](locations.md#location-options). - All keys (filter names, action names, option names) now must be lowercase. ### Placeholders organize v2 uses the Jinja template engine. You may need to change some of your placeholders. - `{basedir}` is no longer available. - You have to replace undocumented placeholders like this: ```yaml "{created.year}-{created.month:02}-{created.day:02}" ``` With this: ```yaml "{created.strftime('%Y-%m-%d')}" ``` If you need to left pad other numbers you can now use the following syntax: ```yaml "{'%02d' % your_variable}" # or "{ '{:02}'.format(your_variable) }" ``` ### Filters - [`filename`](filters.md#name) is renamed to `name`. - [`filesize`](filters.md#size) is renamed to `size`. - [`created`](filters.md#created) no longer accepts a timezone and uses the local timezone by default. - [`lastmodified`](filters.md#lastmodified) no longer accepts a timezone and uses the local timezone by default. - [`extension`](filters.md#extension) `lower` and `upper` are now functions and must be called like this: `"{extension.upper()}"` and `"{extension.lower()}"`. ### Actions The copy, move and rename actions got a whole lot more powerful. You now have several conflict options and can specify exactly how a file should be renamed in case of a conflict. This means you might need to change your config to use the new parameters. - [`copy`](actions.md#copy) arguments changed to support conflict resolution options. - [`move`](actions.md#move) arguments changed to support conflict resolution options. - [`rename`](actions.md#rename) arguments changed to support conflict resolution options. Example: ```yml rules: - folders: ~/Desktop filters: - extension: pdf actions: - move: dest: ~/Documents/PDFs/ overwrite: false counter_seperator: "-" ``` becomes (organize v2): ```yaml rules: - locations: ~/Desktop filters: - extension: pdf actions: - move: dest: ~/Documents/PDFs/ on_conflict: rename_new rename_template: "{name}-{counter}{extension}" ``` If you used `move`, `copy` or `rename` without arguments, nothing changes for you. ### Settings The `system_files` setting has been removed. In order to include system files in your search, overwrite the default [`system_exclude_files`](locations.md#location-options) with an empty list: ```yaml rules: - locations: - path: ~/Desktop/ system_exclude_files: [] system_exclude_dirs: [] filters: - name: .DS_Store actions: - trash ``` That's it. Again, feel free to open a issue if you have trouble migrating your config. organize-3.3.0/docs/rules.md000066400000000000000000000102021472111340300157260ustar00rootroot00000000000000# Rules A organize config file can be written in [YAML](https://learnxinyminutes.com/docs/yaml/) or [JSON](https://learnxinyminutes.com/docs/json/). See [configuration](configuration.md) on how to locate your config file. The top level element must be a dict with a key "rules". "rules" contains a list of objects with the required keys "locations" and "actions". A minimum config: ```yaml rules: - locations: "~/Desktop" actions: - echo: "Hello World!" ``` Organize checks your rules from top to bottom. For every resource in each location (top to bottom) it will check whether the filters apply (top to bottom) and then execute the given actions (top to bottom). So with this minimal configuration it will print "Hello World!" for each file it finds in your Desktop. ## Rule options ```yml rules: # First rule - name: ... enabled: ... targets: ... locations: ... subfolders: ... filter_mode: ... filters: ... actions: ... tags: ... # Another rule - name: ... enabled: ... # ... and so on ``` The rule options in detail: - **name** (`str`): The rule name - **enabled** (`bool`): Whether the rule is enabled / disabled _(Default: `true`)_ - **targets** (`str`): `"dirs"` or `"files"` _(Default: `"files"`)_ - **locations** (`str`|`list`) - A single location string or list of [locations](locations.md) - **subfolders** (`bool`): Whether to recurse into subfolders of all locations _(Default: `false`)_ - **filter_mode** (`str`): `"all"`, `"any"` or `"none"` of the filters must apply _(Default: `"all"`)_ - **filters** (`list`): A list of [filters](filters.md) _(Default: `[]`)_ - **actions** (`list`): A list of [actions](actions.md) - **tags** (`list`): A list of [tags](configuration.md#running-specific-rules-of-your-config) ## Targeting directories When `targets` is set to `dirs`, organize will work on the folders, not on files. The filters adjust their meaning automatically. For example the `size` filter sums up the size of all files contained in the given folder instead of returning the size of a single file. Of course other filters like `exif` or `filecontent` do not work on folders and will return an error. ## Templates and placeholders Placeholder variables are used with curly braces `{var}`. These variables are **always available**: `{env}` (`dict`)
All your environment variables. You can access individual env vars like this: `{env.MY_VARIABLE}`. `{path}` ([`pathlib.Path`](https://docs.python.org/3/library/pathlib.html#methods-and-properties))
The full path to the current file / folder on the local harddrive. `{relative_path}` (`str`)
the relative path of the current file or dir. `{now()}` (`datetime`)
The current datetime in the local timezone. `{utcnow()}` (`datetime`)
The current UTC datetime. `{today()}` (`date`)
Today's date. In addition to that nearly all filters add new placeholders with information about the currently handled file / folder. Example on how to access the size and hash of a file: ```yaml rules: - locations: ~/Desktop filters: - size - hash actions: - echo: "{size} {hash}" ``` !!! note In order to use a value returned by a filter it must be listed in the filters! ## Advanced: Aliases Instead of repeating the same locations / actions / filters in each and every rule you can use an alias for multiple locations which you can then reference in each rule. Aliases are a standard feature of the YAML syntax. ```yml all_my_messy_folders: &all - ~/Desktop - ~/Downloads - ~/Documents - ~/Dropbox rules: - locations: *all filters: ... actions: ... - locations: *all filters: ... actions: ... ``` You can even use multiple folder lists: ```yml private_folders: &private - "/path/private" - "~/path/private" work_folders: &work - "/path/work" - "~/My work folder" all_folders: &all - *private - *work rules: - locations: *private filters: ... actions: ... - locations: *work filters: ... actions: ... - locations: *all filters: ... actions: ... # same as *all - locations: - *work - *private filters: ... actions: ... ``` organize-3.3.0/docs/textract-hints.md000066400000000000000000000017031472111340300175630ustar00rootroot00000000000000# Textract installation hints Textract needs [Poppler](https://poppler.freedesktop.org/) to extract text from PDFs. ## Windows 1. Download the latest binary of your choice from [github.com/oschwartz10612](https://github.com/oschwartz10612/poppler-windows/releases). In this example we will download and use [Release-22.01.0-0.zip](https://github.com/oschwartz10612/poppler-windows/releases/download/v22.01.0-0/Release-22.01.0-0.zip). 2. Extract the archive file _Release-22.01.0-0.zip_ 3. Copy the folders from _poppler-22.01.0\Library_ into `C:\Program Files\Poppler`. 4. Thus, the directory structure should look something like this: ``` C:\Program Files\Poppler \bin \include \lib \share ``` 5. Add `C:\Program Files\Poppler\bin` to your system PATH! 6. Try it with a [filecontent example rule](https://organize.readthedocs.io/en/latest/filters/#filecontent) organize-3.3.0/main.py000066400000000000000000000001031472111340300146170ustar00rootroot00000000000000from organize.cli import cli if __name__ == "__main__": cli() organize-3.3.0/manage.py000066400000000000000000000126501472111340300151350ustar00rootroot00000000000000import argparse import getpass import os import re import subprocess from datetime import datetime from pathlib import Path import requests SRC_FOLDER = "organize" CURRENT_FOLDER = Path(__file__).resolve().parent GITHUB_API_ENDPOINT = "https://api.github.com/repos/tfeldmann/organize" def ask_confirm(text): while True: answer = input(f"{text} [y/n]: ").lower() if answer in ("j", "y", "ja", "yes"): return True if answer in ("n", "no", "nein"): return False def set_version(args): """ - reads and validates version number - updates __version__.py - updates pyproject.toml - Searches for '[Unreleased]' in changelog and replaces it with current version and date """ from organize.__version__ import __version__ as current_version print(f"Current version is {current_version}.") # read version from input if not given version = args.version if not version: version = input("Version number: ") # validate and remove 'v' if present version = version.lower() if not re.match(r"v?\d+\.\d+.*", version): return if version.startswith("v"): version = version[1:] # safety check if not ask_confirm(f"Creating version v{version}. Continue?"): return # update library version versionfile = CURRENT_FOLDER / SRC_FOLDER / "__version__.py" with open(versionfile, "w") as f: print(f"Updating {versionfile}") f.write(f'__version__ = "{version}"\n') # update poetry version print("Updating pyproject.toml") subprocess.run(["poetry", "version", version], check=True) # read changelog print("Updating CHANGELOG.md") with open(CURRENT_FOLDER / "CHANGELOG.md", "r") as f: changelog = f.read() # check if WIP section is in changelog wip_regex = re.compile( r"## \[Unreleased\]\n(.*?)(?=\n##)", re.MULTILINE | re.DOTALL ) match = wip_regex.search(changelog) if not match: print('No "## [Unreleased]" section found in changelog') return # change WIP to version number and date changes = match.group(1) today = datetime.now().strftime("%Y-%m-%d") changelog = wip_regex.sub( f"## [Unreleased]\n\n## v{version} ({today})\n{changes}", changelog, count=1, ) # write changelog with open(CURRENT_FOLDER / "CHANGELOG.md", "w") as f: f.write(changelog) if ask_confirm("Commit changes?"): subprocess.run( ["git", "add", "pyproject.toml", "*/__version__.py", "CHANGELOG.md"] ) subprocess.run(["git", "commit", "-m", f"bump version to v{version}"]) print("Please push to github and wait for CI to pass.") print("Success.") def publish(args): """ - reads version - reads changes from changelog - creates git tag - pushes to github - publishes on pypi - creates github release """ from organize.__version__ import __version__ as version if not ask_confirm(f"Publishing version {version}. Is this correct?"): return if ask_confirm("Run the tests?"): os.system("poetry run pytest") os.system("poetry run mypy organize main.py") # extract changes from changelog with open(CURRENT_FOLDER / "CHANGELOG.md", "r") as f: changelog = f.read() wip_regex = re.compile( "## v{}".format(version.replace(".", r"\.")) + r".*?\n(.*?)(?=\n##)", re.MULTILINE | re.DOTALL, ) match = wip_regex.search(changelog) if not match: print("Failed to extract changes from changelog. Do the versions match?") return changes = match.group(1).strip() print(f"Changes:\n{changes}") # create git tag ('vXXX') if ask_confirm("Create tag?"): subprocess.run(["git", "tag", "-a", f"v{version}", "-m", f"v{version}"]) # push to github if ask_confirm("Push to github?"): print("Pushing to github") subprocess.run(["git", "push", "--follow-tags"], check=True) # upload to pypi if ask_confirm("Publish on Pypi?"): subprocess.run(["rm", "-rf", "dist"], check=True) subprocess.run(["poetry", "build"], check=True) subprocess.run(["poetry", "publish"], check=True) # create github release if ask_confirm("Create github release?"): response = requests.post( f"{GITHUB_API_ENDPOINT}/releases", auth=(input("Benutzer: "), getpass.getpass(prompt="API token: ")), json={ "tag_name": f"v{version}", "target_commitish": "main", "name": f"v{version}", "body": changes, "draft": False, "prerelease": False, }, ) response.raise_for_status() print("Success.") def main(): assert CURRENT_FOLDER == Path.cwd().resolve() parser = argparse.ArgumentParser() subparsers = parser.add_subparsers() parser_version = subparsers.add_parser("version", help="Set the version number") parser_version.add_argument( "version", type=str, help="The version number", nargs="?", default=None ) parser_version.set_defaults(func=set_version) parser_publish = subparsers.add_parser("publish", help="Publish the project") parser_publish.set_defaults(func=publish) args = parser.parse_args() if not vars(args): parser.print_help() else: args.func(args) if __name__ == "__main__": main() organize-3.3.0/mkdocs.yml000066400000000000000000000014311472111340300153310ustar00rootroot00000000000000site_name: organize repo_url: https://github.com/tfeldmann/organize/ edit_uri: edit/main/docs/ site_author: "Thomas Feldmann" nav: - Home: index.md - Configuration: configuration.md - Rules: rules.md - Locations: locations.md - Filters: filters.md - Actions: actions.md - Docker: docker.md - Changelog: changelog.md - Migrating from older versions: migrating.md plugins: - search - include-markdown - autorefs - mkdocstrings: default_handler: python handlers: python: options: show_bases: false show_root_toc_entry: false show_root_heading: false show_source: true watch: - organize - docs markdown_extensions: - admonition - toc: permalink: "#" theme: name: readthedocs organize-3.3.0/organize/000077500000000000000000000000001472111340300151455ustar00rootroot00000000000000organize-3.3.0/organize/__init__.py000066400000000000000000000002641472111340300172600ustar00rootroot00000000000000from .config import Config from .errors import ConfigError, ConfigNotFound from .rule import Rule __all__ = ( "Config", "ConfigError", "ConfigNotFound", "Rule", ) organize-3.3.0/organize/__main__.py000066400000000000000000000000771472111340300172430ustar00rootroot00000000000000if __name__ == "__main__": from .cli import cli cli() organize-3.3.0/organize/__version__.py000066400000000000000000000000261472111340300177760ustar00rootroot00000000000000__version__ = "3.3.0" organize-3.3.0/organize/action.py000066400000000000000000000013321472111340300167730ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, ClassVar, NamedTuple, Protocol, runtime_checkable if TYPE_CHECKING: from .output import Output from .resource import Resource class ActionConfig(NamedTuple): name: str standalone: bool files: bool dirs: bool @runtime_checkable class HasActionConfig(Protocol): action_config: ClassVar[ActionConfig] class HasActionPipeline(Protocol): def pipeline(self, res: Resource, output: Output, simulate: bool): ... @runtime_checkable class Action(HasActionConfig, HasActionPipeline, Protocol): def __init__(self, *args, **kwargs) -> None: # allow any amount of args / kwargs for BaseModel and dataclasses. ... organize-3.3.0/organize/actions/000077500000000000000000000000001472111340300166055ustar00rootroot00000000000000organize-3.3.0/organize/actions/__init__.py000066400000000000000000000011371472111340300207200ustar00rootroot00000000000000from typing import Tuple, Type from organize.action import Action from .confirm import Confirm from .copy import Copy from .delete import Delete from .echo import Echo from .hardlink import Hardlink from .macos_tags import MacOSTags from .move import Move from .python import Python from .rename import Rename from .shell import Shell from .symlink import Symlink from .trash import Trash from .write import Write ALL: Tuple[Type[Action], ...] = ( Confirm, Copy, Delete, Echo, Hardlink, MacOSTags, Move, Python, Rename, Shell, Symlink, Trash, Write, ) organize-3.3.0/organize/actions/common/000077500000000000000000000000001472111340300200755ustar00rootroot00000000000000organize-3.3.0/organize/actions/common/__init__.py000066400000000000000000000000001472111340300221740ustar00rootroot00000000000000organize-3.3.0/organize/actions/common/conflict.py000066400000000000000000000100541472111340300222500ustar00rootroot00000000000000from __future__ import annotations import filecmp from pathlib import Path from typing import TYPE_CHECKING, Literal, NamedTuple from organize.output import Output from organize.resource import Resource from organize.template import render if TYPE_CHECKING: from jinja2 import Template # TODO: keep_newer, keep_older, keep_bigger, keep_smaller ConflictMode = Literal[ "skip", "overwrite", "deduplicate", "trash", "rename_new", "rename_existing" ] class ConflictResult(NamedTuple): skip_action: bool # Whether to skip the current action use_dst: Path # The Path to continue with def next_free_name(dst: Path, template: Template) -> Path: """ Increments {counter} in the template until the given resource does not exist. Attributes: dst (Path): The destination path. template (jinja2.Template): A jinja2 template with placeholders for {name}, {extension} and {counter} Raises: ValueError if no free name can be found with the given template. Returns: (Path) A path according to the given template that does not exist. """ if not dst.exists(): return dst counter = 2 prev_candidate = None while True: args = dict( name=dst.stem, extension=dst.suffix, counter=counter, ) new_name = render(template, args) candidate = dst.with_name(new_name) if not candidate.exists(): return candidate if prev_candidate == candidate: raise ValueError( "Could not find a free filename for the given template. " 'Maybe you forgot the "{counter}" placeholder?' ) prev_candidate = candidate counter += 1 def resolve_conflict( dst: Path, res: Resource, conflict_mode: ConflictMode, rename_template: Template, simulate: bool, output: Output, ) -> ConflictResult: """ Handle a conflict if `dst` already exists. """ assert res.path is not None # no conflict, just continue with the action. if not dst.exists(): return ConflictResult(skip_action=False, use_dst=dst) def _print(msg: str): output.msg(res=res, sender="conflict", msg=msg) _print(f'"{dst}" already exists! (Conflict mode is "{conflict_mode}")') if res.path.resolve() == dst.resolve(): _print("Same resource: Skipped.") return ConflictResult(skip_action=True, use_dst=res.path) if conflict_mode == "trash": _print(f'Trash "{dst}"') if not simulate: from organize.actions.trash import trash trash(path=dst) return ConflictResult(skip_action=False, use_dst=dst) elif conflict_mode == "skip": _print("Skipped.") return ConflictResult(skip_action=True, use_dst=res.path) elif conflict_mode == "overwrite": _print(f"Overwriting {dst}.") if not simulate: from organize.actions.delete import delete delete(path=dst) return ConflictResult(skip_action=False, use_dst=dst) elif conflict_mode == "deduplicate": if filecmp.cmp(res.path, dst, shallow=True): _print("Duplicate skipped.") return ConflictResult(skip_action=True, use_dst=res.path) else: new_path = next_free_name( dst=dst, template=rename_template, ) return ConflictResult(skip_action=False, use_dst=new_path) elif conflict_mode == "rename_new": new_path = next_free_name( dst=dst, template=rename_template, ) return ConflictResult(skip_action=False, use_dst=new_path) elif conflict_mode == "rename_existing": new_path = next_free_name( dst=dst, template=rename_template, ) _print('Renaming existing to: "{new_path.name}"') if not simulate: dst.rename(new_path) return ConflictResult(skip_action=False, use_dst=dst) raise ValueError("Unknown conflict_mode %s" % conflict_mode) organize-3.3.0/organize/actions/common/target_path.py000066400000000000000000000020041472111340300227450ustar00rootroot00000000000000from pathlib import Path def user_wants_a_folder(path: str, autodetect: bool) -> bool: """ Try to detect whether the user means to target a folder """ if path.endswith(("/", "\\")): return True if autodetect: return "." not in Path(path).name return False def prepare_target_path( src_name: str, dst: str, autodetect_folder: bool, simulate: bool, ) -> Path: result = Path(dst).resolve() wants_folder = user_wants_a_folder(path=dst, autodetect=autodetect_folder) # if dst is an existing folder, we use it if result.exists(): if result.is_dir(): return result / src_name elif wants_folder: raise ValueError(f'Expected "{dst}" to be a folder, but it\'s not!') if wants_folder: if not simulate: result.mkdir(parents=True, exist_ok=True) return result / src_name else: if not simulate: result.parent.mkdir(parents=True, exist_ok=True) return result organize-3.3.0/organize/actions/confirm.py000066400000000000000000000017111472111340300206140ustar00rootroot00000000000000from typing import ClassVar from pydantic.config import ConfigDict from pydantic.dataclasses import dataclass from organize.action import ActionConfig from organize.output import Output from organize.resource import Resource from organize.template import Template, render @dataclass(config=ConfigDict(coerce_numbers_to_str=True, extra="forbid")) class Confirm: """Ask for confirmation before continuing.""" msg: str = "Continue?" default: bool = True action_config: ClassVar[ActionConfig] = ActionConfig( name="confirm", standalone=True, files=True, dirs=True, ) def __post_init__(self): self._msg = Template.from_string(self.msg) def pipeline(self, res: Resource, output: Output, simulate: bool): msg = render(self._msg, res.dict()) result = output.confirm(res=res, msg=msg, sender=self, default=self.default) if not result: raise StopIteration("Aborted") organize-3.3.0/organize/actions/copy.py000066400000000000000000000070621472111340300201360ustar00rootroot00000000000000import shutil from typing import ClassVar, Literal from pydantic.config import ConfigDict from pydantic.dataclasses import dataclass from organize.action import ActionConfig from organize.output import Output from organize.resource import Resource from organize.template import Template, render from .common.conflict import ConflictMode, resolve_conflict from .common.target_path import prepare_target_path @dataclass(config=ConfigDict(coerce_numbers_to_str=True, extra="forbid")) class Copy: """Copy a file or dir to a new location. If the specified path does not exist it will be created. Attributes: dest (str): The destination where the file / dir should be copied to. If `dest` ends with a slash, it is assumed to be a target directory and the file / dir will be copied into `dest` and keep its name. on_conflict (str): What should happen in case **dest** already exists. One of `skip`, `overwrite`, `trash`, `rename_new` and `rename_existing`. Defaults to `rename_new`. rename_template (str): A template for renaming the file / dir in case of a conflict. Defaults to `{name} {counter}{extension}`. autodetect_folder (bool): In case you forget the ending slash "/" to indicate copying into a folder this settings will handle targets without a file extension as folders. If you really mean to copy to a file without file extension, set this to false. Defaults to True. continue_with (str) = "copy" | "original": Continue the next action either with the path to the copy or the path the original. Defaults to "copy". The next action will work with the created copy. """ dest: str on_conflict: ConflictMode = "rename_new" rename_template: str = "{name} {counter}{extension}" autodetect_folder: bool = True continue_with: Literal["copy", "original"] = "copy" action_config: ClassVar[ActionConfig] = ActionConfig( name="copy", standalone=False, files=True, dirs=True, ) def __post_init__(self): self._dest = Template.from_string(self.dest) self._rename_template = Template.from_string(self.rename_template) def pipeline(self, res: Resource, output: Output, simulate: bool): assert res.path is not None, "Does not support standalone mode" rendered = render(self._dest, res.dict()) # fully resolve the destination for folder targets and prepare the folder # structure dst = prepare_target_path( src_name=res.path.name, dst=rendered, autodetect_folder=self.autodetect_folder, simulate=simulate, ) # Resolve conflicts before copying the file to the destination skip_action, dst = resolve_conflict( dst=dst, res=res, conflict_mode=self.on_conflict, rename_template=self._rename_template, simulate=simulate, output=output, ) if skip_action: return output.msg(res=res, msg=f"Copy to {dst}", sender=self) res.walker_skip_pathes.add(dst) if not simulate: if res.is_dir(): shutil.copytree(src=res.path, dst=dst) else: shutil.copy2(src=res.path, dst=dst) # continue with either the original path or the path to the copy if self.continue_with == "copy": res.path = dst organize-3.3.0/organize/actions/delete.py000066400000000000000000000021411472111340300204170ustar00rootroot00000000000000from __future__ import annotations import shutil from typing import TYPE_CHECKING, ClassVar from pydantic.config import ConfigDict from pydantic.dataclasses import dataclass from organize.action import ActionConfig if TYPE_CHECKING: from pathlib import Path from organize.output import Output from organize.resource import Resource def delete(path: Path): if path.is_dir(): shutil.rmtree(path) else: path.unlink() @dataclass(config=ConfigDict(extra="forbid")) class Delete: """ Delete a file from disk. Deleted files have no recovery option! Using the `Trash` action is strongly advised for most use-cases! """ action_config: ClassVar[ActionConfig] = ActionConfig( name="delete", standalone=False, files=True, dirs=True, ) def pipeline(self, res: Resource, output: Output, simulate: bool): assert res.path is not None, "Does not support standalone mode" output.msg(res=res, msg=f"Deleting {res.path}", sender=self) if not simulate: delete(res.path) res.path = None organize-3.3.0/organize/actions/echo.py000066400000000000000000000017441472111340300201030ustar00rootroot00000000000000from typing import ClassVar from pydantic.config import ConfigDict from pydantic.dataclasses import dataclass from organize.action import ActionConfig from organize.output import Output from organize.resource import Resource from organize.template import Template, render @dataclass(config=ConfigDict(extra="forbid")) class Echo: """Prints the given message. This can be useful to test your rules, especially in combination with placeholder variables. Attributes: msg (str): The message to print. Accepts placeholder variables. """ msg: str = "" action_config: ClassVar[ActionConfig] = ActionConfig( name="echo", standalone=True, files=True, dirs=True, ) def __post_init__(self): self._msg_templ = Template.from_string(self.msg) def pipeline(self, res: Resource, output: Output, simulate: bool): full_msg = render(self._msg_templ, res.dict()) output.msg(res, full_msg, sender=self) organize-3.3.0/organize/actions/hardlink.py000066400000000000000000000056371472111340300207660ustar00rootroot00000000000000import os from pathlib import Path from typing import ClassVar from pydantic.config import ConfigDict from pydantic.dataclasses import dataclass from organize.action import ActionConfig from organize.output import Output from organize.resource import Resource from organize.template import Template, render from .common.conflict import ConflictMode, resolve_conflict from .common.target_path import prepare_target_path def create_hardlink(target: Path, link: Path) -> None: # Path.hardlink_to needs Python >= 3.10 os.link(src=target, dst=link) # create a hardlink pointing to src named dst @dataclass(config=ConfigDict(coerce_numbers_to_str=True, extra="forbid")) class Hardlink: """Create a hardlink. Attributes: dest (str): The hardlink destination. If **dest** ends with a slash `/``, create the hardlink in the given directory. Can contain placeholders. on_conflict (str): What should happen in case **dest** already exists. One of `skip`, `overwrite`, `trash`, `rename_new` and `rename_existing`. Defaults to `rename_new`. rename_template (str): A template for renaming the file / dir in case of a conflict. Defaults to `{name} {counter}{extension}`. autodetect_folder (bool): In case you forget the ending slash "/" to indicate copying into a folder this settings will handle targets without a file extension as folders. If you really mean to copy to a file without file extension, set this to false. Default: true """ dest: str on_conflict: ConflictMode = "rename_new" rename_template: str = "{name} {counter}{extension}" autodetect_folder: bool = True action_config: ClassVar[ActionConfig] = ActionConfig( name="hardlink", standalone=False, files=True, dirs=True, ) def __post_init__(self): self._dest = Template.from_string(self.dest) self._rename_template = Template.from_string(self.rename_template) def pipeline(self, res: Resource, output: Output, simulate: bool): assert res.path is not None, "Does not support standalone mode" rendered = render(self._dest, res.dict()) dst = prepare_target_path( src_name=res.path.name, dst=rendered, autodetect_folder=self.autodetect_folder, simulate=simulate, ) skip_action, dst = resolve_conflict( dst=dst, res=res, conflict_mode=self.on_conflict, rename_template=self._rename_template, simulate=simulate, output=output, ) if skip_action: return output.msg(res=res, msg=f"Creating hardlink at {dst}", sender=self) if not simulate: create_hardlink(target=res.path, link=dst) res.walker_skip_pathes.add(dst) organize-3.3.0/organize/actions/macos_tags.py000066400000000000000000000044111472111340300212770ustar00rootroot00000000000000import sys from typing import ClassVar import simplematch as sm from pydantic.config import ConfigDict from pydantic.dataclasses import dataclass from organize.action import ActionConfig from organize.output import Output from organize.resource import Resource from organize.template import Template, render from organize.validators import FlatList @dataclass(config=ConfigDict(coerce_numbers_to_str=True, extra="forbid")) class MacOSTags: """Add macOS tags. Attributes: *tags (str): A list of tags or a single tag. The color can be specified in brackets after the tag name, for example: ```yaml macos_tags: "Invoices (red)" ``` Available colors are `none`, `gray`, `green`, `purple`, `blue`, `yellow`, `red` and `orange`. """ tags: FlatList[str] action_config: ClassVar[ActionConfig] = ActionConfig( name="macos_tags", standalone=False, files=True, dirs=True, ) def __post_init__(self): self._tags = [Template.from_string(tag) for tag in self.tags] if sys.platform != "darwin": raise EnvironmentError("The macos_tags action is only available on macOS") def pipeline(self, res: Resource, output: Output, simulate: bool): import macos_tags COLORS = [c.name.lower() for c in macos_tags.Color] for template in self._tags: tag = render(template, res.dict()) name, color = self._parse_tag(tag) if color not in COLORS: raise ValueError( "color %s is unknown. (Available: %s)" % (color, " / ".join(COLORS)) ) output.msg( res=res, sender=self, msg=f'Adding tag: "{name}" (color: {color})', ) if not simulate: _tag = macos_tags.Tag( name=name, color=macos_tags.Color[color.upper()], ) # type: ignore macos_tags.add(_tag, file=str(res.path)) def _parse_tag(self, s): """parse a tag definition and return a tuple (name, color)""" result = sm.match("{name} ({color})", s) if not result: return s, "none" return result["name"], result["color"].lower() organize-3.3.0/organize/actions/move.py000066400000000000000000000063671472111340300201410ustar00rootroot00000000000000import shutil from typing import ClassVar from pydantic.config import ConfigDict from pydantic.dataclasses import dataclass from organize.action import ActionConfig from organize.output import Output from organize.resource import Resource from organize.template import Template, render from .common.conflict import ConflictMode, resolve_conflict from .common.target_path import prepare_target_path @dataclass(config=ConfigDict(coerce_numbers_to_str=True, extra="forbid")) class Move: """Move a file to a new location. The file can also be renamed. If the specified path does not exist it will be created. If you only want to rename the file and keep the folder, it is easier to use the `rename` action. Attributes: dest (str): The destination where the file / dir should be moved to. If `dest` ends with a slash, it is assumed to be a target directory and the file / dir will be moved into `dest` and keep its name. on_conflict (str): What should happen in case **dest** already exists. One of `skip`, `overwrite`, `trash`, `rename_new` and `rename_existing`. Defaults to `rename_new`. rename_template (str): A template for renaming the file / dir in case of a conflict. Defaults to `{name} {counter}{extension}`. autodetect_folder (bool): In case you forget the ending slash "/" to indicate moving into a folder this settings will handle targets without a file extension as folders. If you really mean to move to a file without file extension, set this to false. Default: True The next action will work with the moved file / dir. """ dest: str on_conflict: ConflictMode = "rename_new" rename_template: str = "{name} {counter}{extension}" autodetect_folder: bool = True action_config: ClassVar[ActionConfig] = ActionConfig( name="move", standalone=False, files=True, dirs=True, ) def __post_init__(self): self._dest = Template.from_string(self.dest) self._rename_template = Template.from_string(self.rename_template) def pipeline(self, res: Resource, output: Output, simulate: bool): assert res.path is not None, "Does not support standalone mode" rendered = render(self._dest, res.dict()) # fully resolve the destination for folder targets and prepare the folder # structure dst = prepare_target_path( src_name=res.path.name, dst=rendered, autodetect_folder=self.autodetect_folder, simulate=simulate, ) # Resolve conflicts before moving the file to the destination skip_action, dst = resolve_conflict( dst=dst, res=res, conflict_mode=self.on_conflict, rename_template=self._rename_template, simulate=simulate, output=output, ) if skip_action: return output.msg(res=res, msg=f"Move to {dst}", sender=self) res.walker_skip_pathes.add(dst) if not simulate: shutil.move(src=res.path, dst=dst) # continue with the new path res.path = dst organize-3.3.0/organize/actions/python.py000066400000000000000000000045231472111340300205040ustar00rootroot00000000000000import textwrap from typing import ClassVar, Dict, Optional from pydantic.config import ConfigDict from pydantic.dataclasses import dataclass from organize.action import ActionConfig from organize.output import Output from organize.resource import Resource @dataclass(config=ConfigDict(coerce_numbers_to_str=True, extra="forbid")) class Python: """Execute python code. Attributes: code (str): The python code to execute. run_in_simulation (bool): Whether to execute this code in simulation mode (Default false). Variables of previous filters are available, but you have to use the normal python dictionary syntax `x = regex["my_group"]`. """ code: str run_in_simulation: bool = False action_config: ClassVar[ActionConfig] = ActionConfig( name="python", standalone=True, files=True, dirs=True, ) def __post_init__(self): self.code = textwrap.dedent(self.code) def __usercode__(self, print, **kwargs) -> Optional[Dict]: raise NotImplementedError() def pipeline(self, res: Resource, output: Output, simulate: bool): if simulate and not self.run_in_simulation: output.msg( res=res, msg="** Code not run in simulation. **", level="warn", sender=self, ) return def _output_msg(*values, sep: str = " ", end: str = ""): """ the print function for the use code needs to print via the current output """ msg = f"{sep.join(str(x) for x in values)}{end}" output.msg(res=res, msg=msg, sender=self) # codegen the user function with arguments as available in the resource kwargs = ", ".join(res.dict().keys()) func = f"def __userfunc(print, {kwargs}):\n" func += textwrap.indent(self.code, " ") func += "\n\nself.__usercode__ = __userfunc" exec(func, globals().copy(), locals().copy()) result = self.__usercode__(print=_output_msg, **res.dict()) # deep merge the resulting dict if not (result is None or isinstance(result, dict)): raise ValueError("The python code must return None or a dict") if isinstance(result, dict): res.deep_merge(key=self.action_config.name, data=result) organize-3.3.0/organize/actions/rename.py000066400000000000000000000045321472111340300204320ustar00rootroot00000000000000from __future__ import annotations from typing import ClassVar from pydantic.config import ConfigDict from pydantic.dataclasses import dataclass from organize.action import ActionConfig from organize.output import Output from organize.resource import Resource from organize.template import Template, render from .common.conflict import ConflictMode, resolve_conflict @dataclass(config=ConfigDict(coerce_numbers_to_str=True, extra="forbid")) class Rename: """Renames a file. Attributes: new_name (str): The new name for the file / dir. on_conflict (str): What should happen in case **dest** already exists. One of `skip`, `overwrite`, `trash`, `rename_new` and `rename_existing`. Defaults to `rename_new`. rename_template (str): A template for renaming the file / dir in case of a conflict. Defaults to `{name} {counter}{extension}`. The next action will work with the renamed file / dir. """ new_name: str on_conflict: ConflictMode = "rename_new" rename_template: str = "{name} {counter}{extension}" # TODO: keep_extension? action_config: ClassVar[ActionConfig] = ActionConfig( name="rename", standalone=False, files=True, dirs=True, ) def __post_init__(self): self._new_name = Template.from_string(self.new_name) self._rename_template = Template.from_string(self.rename_template) def pipeline(self, res: Resource, output: Output, simulate: bool): assert res.path is not None, "Does not support standalone mode" new_name = render(self._new_name, res.dict()) if "/" in new_name: raise ValueError( "The new name cannot contain slashes. " "To move files or folders use `move`." ) dst = res.path.with_name(new_name) skip_action, dst = resolve_conflict( dst=dst, res=res, conflict_mode=self.on_conflict, rename_template=self._rename_template, simulate=simulate, output=output, ) if skip_action: return output.msg(res=res, msg=f"Renaming to {new_name}", sender=self) if not simulate: res.path.rename(dst) res.path = dst res.walker_skip_pathes.add(dst) organize-3.3.0/organize/actions/shell.py000066400000000000000000000060621472111340300202720ustar00rootroot00000000000000import subprocess from typing import ClassVar from pydantic.config import ConfigDict from pydantic.dataclasses import dataclass from organize.action import ActionConfig from organize.output import Output from organize.resource import Resource from organize.template import Template, render # TODO: Terminal waterfall: https://github.com/Textualize/rich/discussions/2985 @dataclass(config=ConfigDict(coerce_numbers_to_str=True, extra="forbid")) class Shell: """ Executes a shell command Attributes: cmd (str): The command to execute. run_in_simulation (bool): Whether to execute in simulation mode (default = false) ignore_errors (bool): Whether to continue on returncodes != 0. simulation_output (str): The value of `{shell.output}` if run in simulation simulation_returncode (int): The value of `{shell.returncode}` if run in simulation Returns - `{shell.output}` (`str`): The stdout of the executed process. - `{shell.returncode}` (`int`): The returncode of the executed process. """ cmd: str run_in_simulation: bool = False ignore_errors: bool = False simulation_output: str = "** simulation **" simulation_returncode: int = 0 action_config: ClassVar[ActionConfig] = ActionConfig( name="shell", standalone=True, files=True, dirs=True, ) def __post_init__(self): self._cmd = Template.from_string(self.cmd) self._simulation_output = Template.from_string(self.simulation_output) def pipeline(self, res: Resource, output: Output, simulate: bool): full_cmd = render(self._cmd, res.dict()) if not simulate or self.run_in_simulation: output.msg(res=res, msg=f"$ {full_cmd}", sender=self) try: call = subprocess.run( full_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, ) res.vars[self.action_config.name] = { "output": call.stdout.decode("utf-8"), "returncode": call.returncode, } except subprocess.CalledProcessError as e: if not self.ignore_errors: raise e output.msg( res=res, msg=f"Ignoring error: {e}", sender=self, level="warn", ) res.vars[self.action_config.name] = { "output": e.output.decode("utf-8"), "returncode": e.returncode, } else: output.msg( res=res, msg=f"** not run in simulation ** $ {full_cmd}", sender=self, ) res.vars[self.action_config.name] = { "output": render(self._simulation_output, res.dict()), "returncode": self.simulation_returncode, } organize-3.3.0/organize/actions/symlink.py000066400000000000000000000053701472111340300206520ustar00rootroot00000000000000from typing import ClassVar from pydantic.config import ConfigDict from pydantic.dataclasses import dataclass from organize.action import ActionConfig from organize.output import Output from organize.resource import Resource from organize.template import Template, render from .common.conflict import ConflictMode, resolve_conflict from .common.target_path import prepare_target_path @dataclass(config=ConfigDict(coerce_numbers_to_str=True, extra="forbid")) class Symlink: """Create a symbolic link. Attributes: dest (str): The symlink destination. If **dest** ends with a slash `/``, create the symlink in the given directory. Can contain placeholders. on_conflict (str): What should happen in case **dest** already exists. One of `skip`, `overwrite`, `trash`, `rename_new` and `rename_existing`. Defaults to `rename_new`. rename_template (str): A template for renaming the file / dir in case of a conflict. Defaults to `{name} {counter}{extension}`. autodetect_folder (bool): In case you forget the ending slash "/" to indicate creating the link inside the destination folder this settings will handle targets without a file extension as folders. If you really mean to copy to a file without file extension, set this to false. Default: true """ dest: str on_conflict: ConflictMode = "rename_new" rename_template: str = "{name} {counter}{extension}" autodetect_folder: bool = True action_config: ClassVar[ActionConfig] = ActionConfig( name="symlink", standalone=False, files=True, dirs=True, ) def __post_init__(self): self._dest = Template.from_string(self.dest) self._rename_template = Template.from_string(self.rename_template) def pipeline(self, res: Resource, output: Output, simulate: bool): assert res.path is not None, "Does not support standalone mode" rendered = render(self._dest, res.dict()) dst = prepare_target_path( src_name=res.path.name, dst=rendered, autodetect_folder=self.autodetect_folder, simulate=simulate, ) skip_action, dst = resolve_conflict( dst=dst, res=res, conflict_mode=self.on_conflict, rename_template=self._rename_template, simulate=simulate, output=output, ) if skip_action: return output.msg(res=res, msg=f"Creating symlink at {dst}", sender=self) res.walker_skip_pathes.add(dst) if not simulate: dst.symlink_to(target=res.path, target_is_directory=res.is_dir()) organize-3.3.0/organize/actions/trash.py000066400000000000000000000015701472111340300203030ustar00rootroot00000000000000from pathlib import Path from typing import ClassVar from pydantic.config import ConfigDict from pydantic.dataclasses import dataclass from organize.action import ActionConfig from organize.output import Output from organize.resource import Resource def trash(path: Path): from send2trash import send2trash send2trash(path) @dataclass(config=ConfigDict(coerce_numbers_to_str=True, extra="forbid")) class Trash: """Move a file or dir into the trash.""" action_config: ClassVar[ActionConfig] = ActionConfig( name="trash", standalone=False, files=True, dirs=True, ) def pipeline(self, res: Resource, output: Output, simulate: bool): assert res.path is not None, "Does not support standalone mode" output.msg(res=res, msg=f'Trash "{res.path}"', sender=self) if not simulate: trash(res.path) organize-3.3.0/organize/actions/write.py000066400000000000000000000064051472111340300203160ustar00rootroot00000000000000from __future__ import annotations from pathlib import Path from typing import TYPE_CHECKING, ClassVar, Literal from pydantic.config import ConfigDict from pydantic.dataclasses import dataclass from organize.action import ActionConfig from organize.template import Template, render if TYPE_CHECKING: from organize.output import Output from organize.resource import Resource @dataclass(config=ConfigDict(coerce_numbers_to_str=True, extra="forbid")) class Write: """ Write text to a file. If the specified path does not exist it will be created. Attributes: text (str): The text that should be written. Supports templates. outfile (str): The file `text` should be written into. Supports templates. mode (str): Can be either `append` (append text to the file), `prepend` (insert text as first line) or `overwrite` (overwrite content with text). Defaults to `append`. encoding (str): The text encoding to use. Default: "utf-8". newline (str): (Optional) Whether to append a newline to the given `text`. Defaults to `true`. clear_before_first_write (bool): (Optional) Clears the file before first appending / prepending text to it. This happens only the first time the file is written to. If the rule filters don't match anything the file is left as it is. Defaults to `false`. """ text: str outfile: str mode: Literal["append", "prepend", "overwrite"] = "append" encoding: str = "utf-8" newline: bool = True clear_before_first_write: bool = False action_config: ClassVar[ActionConfig] = ActionConfig( name="write", standalone=True, files=True, dirs=True, ) def __post_init__(self): self._text = Template.from_string(self.text) self._path = Template.from_string(self.outfile) self._known_files = set() def pipeline(self, res: Resource, output: Output, simulate: bool): text = render(self._text, res.dict()) path = Path(render(self._path, res.dict())) resolved = path.resolve() if resolved not in self._known_files: self._known_files.add(resolved) if not simulate: resolved.parent.mkdir(parents=True, exist_ok=True) # clear on first write if resolved.exists() and self.clear_before_first_write: output.msg(res=res, msg=f"Clearing file {path}", sender=self) if not simulate: resolved.open("w") # clear the file output.msg(res=res, msg=f'{path}: {self.mode} "{text}"', sender=self) if self.newline: text += "\n" if not simulate: if self.mode == "append": with open(path, "a", encoding=self.encoding) as f: f.write(text) elif self.mode == "prepend": content = "" if path.exists(): content = path.read_text(encoding=self.encoding) path.write_text(text + content, encoding=self.encoding) elif self.mode == "overwrite": path.write_text(text, encoding=self.encoding) organize-3.3.0/organize/cli.py000066400000000000000000000211561472111340300162730ustar00rootroot00000000000000__doc__ = """ organize - The file management automation tool. Usage: organize run [options] [ | --stdin] organize sim [options] [ | --stdin] organize new [] organize edit [] organize check [ | --stdin] organize debug [ | --stdin] organize show [--path|--reveal] [] organize list organize docs organize --version organize --help Commands: run Organize your files. sim Simulate organizing your files. new Creates a default config. edit Edit the config file with $EDITOR check Check config file validity debug Shows the raw config parsing steps. show Print the config to stdout. Use --reveal to reveal the file in your file manager Use --path to show the path to the file list Lists config files found in the default locations. docs Open the documentation. Options: A config name or path to a config file. Some commands also support piping in a config file via the `--stdin` flag. -W --working-dir The working directory -F --format (default|errorsonly|JSONL) The output format [Default: default] -T --tags Tags to run (eg. "initial,release") -S --skip-tags Tags to skip -h --help Show this help page. """ import os import sys from functools import partial from pathlib import Path from typing import Annotated, Literal, Optional, Set, Union from docopt import docopt from pydantic import ( BaseModel, ConfigDict, Field, ValidationError, model_validator, ) from pydantic.functional_validators import BeforeValidator from rich.console import Console from rich.pretty import pprint from rich.syntax import Syntax from rich.table import Table from yaml.scanner import ScannerError from organize import Config, ConfigError from organize.find_config import ( DOCS_RTD, ConfigNotFound, create_example_config, find_config, list_configs, ) from organize.logger import enable_logfile from organize.output import JSONL, Default, Output from organize.utils import escape from .__version__ import __version__ Tags = Set[str] OutputFormat = Annotated[ Literal["default", "jsonl", "errorsonly"], BeforeValidator(lambda v: v.lower()) ] console = Console() class ConfigWithPath(BaseModel): """ Allows reading the config from a path, finding it by name or supplying it directly via stdin. """ config: str config_path: Optional[Path] @classmethod def from_stdin(cls) -> "ConfigWithPath": return cls(config=sys.stdin.read(), config_path=None) @classmethod def by_name_or_path(cls, name_or_path: Optional[str]) -> "ConfigWithPath": config_path = find_config(name_or_path=name_or_path) return cls( config=config_path.read_text(encoding="utf-8"), config_path=config_path, ) def path(self): if self.config_path is not None: return str(self.config_path) return "[config given by string / stdin]" def _open_uri(uri: str) -> None: import webbrowser webbrowser.open(uri) def _output_for_format(format: OutputFormat) -> Output: if format == "default": return Default() elif format == "errorsonly": return Default(errors_only=True) elif format == "jsonl": return JSONL() raise ValueError(f"{format} is not a valid output format.") def execute( config: ConfigWithPath, working_dir: Optional[Path], format: OutputFormat, tags: Tags, skip_tags: Tags, simulate: bool, ) -> None: Config.from_string( config=config.config, config_path=config.config_path, ).execute( simulate=simulate, output=_output_for_format(format), tags=tags, skip_tags=skip_tags, working_dir=working_dir or Path("."), ) def new(config: Optional[str]) -> None: try: new_path = create_example_config(name_or_path=config) console.print( f'Config "{escape(new_path.name)}" created at "{escape(new_path.absolute())}"' ) except FileExistsError as e: console.print( f"{e}\n" r'Use "organize new \[name]" to create a config in the default location.' ) sys.exit(1) def edit(config: Optional[str]) -> None: config_path = find_config(config) editor = os.getenv("EDITOR") if editor: os.system(f'{editor} "{config_path}"') else: _open_uri(config_path.as_uri()) def check(config: ConfigWithPath) -> None: Config.from_string(config=config.config, config_path=config.config_path) console.print(f'No problems found in "{escape(config.path())}".') def debug(config: ConfigWithPath) -> None: conf = Config.from_string(config=config.config, config_path=config.config_path) pprint(conf, expand_all=True, indent_guides=False) def show(config: Optional[str], path: bool, reveal: bool) -> None: config_path = find_config(name_or_path=config) if path: print(config_path) elif reveal: _open_uri(config_path.parent.as_uri()) else: syntax = Syntax(config_path.read_text(encoding="utf-8"), "yaml") console.print(syntax) def list_() -> None: table = Table() table.add_column("Config") table.add_column("Path", no_wrap=True, style="dim") for path in list_configs(): table.add_row(path.stem, str(path)) console.print(table) def docs() -> None: uri = DOCS_RTD print(f'Opening "{escape(uri)}"') _open_uri(uri=uri) class CliArgs(BaseModel): model_config = ConfigDict(extra="forbid") # commands run: bool sim: bool new: bool edit: bool check: bool debug: bool show: bool list: bool docs: bool # run / sim options config: Optional[str] = Field(..., alias="") working_dir: Optional[Path] = Field(..., alias="--working-dir") format: OutputFormat = Field("default", alias="--format") tags: Optional[str] = Field(..., alias="--tags") skip_tags: Optional[str] = Field(..., alias="--skip-tags") stdin: bool = Field(..., alias="--stdin") # show options path: bool = Field(False, alias="--path") reveal: bool = Field(False, alias="--reveal") # docopt options version: bool = Field(..., alias="--version") help: bool = Field(..., alias="--help") @model_validator(mode="after") def either_stdin_or_config(self): if self.stdin and self.config is not None: raise ValueError("Either set a config file or --stdin.") return self def _split_tags(val: Optional[str]) -> Tags: if val is None: return set() return set(val.split(",")) def cli(argv: Union[list[str], str, None] = None) -> None: enable_logfile() assert __doc__ is not None parsed_args = docopt( __doc__, argv=argv, default_help=True, version=f"organize v{__version__}", ) try: args = CliArgs.model_validate(parsed_args) def _config_with_path(): if args.stdin: return ConfigWithPath.from_stdin() else: return ConfigWithPath.by_name_or_path(args.config) if args.sim or args.run: _execute = partial( execute, config=_config_with_path(), working_dir=args.working_dir, format=args.format, tags=_split_tags(args.tags), skip_tags=_split_tags(args.skip_tags), ) if args.run: _execute(simulate=False) elif args.sim: _execute(simulate=True) elif args.new: new(config=args.config) elif args.edit: edit(config=args.config) elif args.check: check(config=_config_with_path()) elif args.debug: debug(config=_config_with_path()) elif args.show: show(config=args.config, path=args.path, reveal=args.reveal) elif args.list: list_() elif args.docs: docs() except (ConfigError, ConfigNotFound) as e: console.print(f"[red]Error: Config problem[/]\n{escape(e)}") sys.exit(1) except ValidationError as e: console.print(f"[red]Error: Invalid CLI arguments[/]\n{escape(e)}") sys.exit(2) except ScannerError as e: console.print(f"[red]Error: YAML syntax error[/]\n{escape(e)}") sys.exit(3) if __name__ == "__main__": cli() organize-3.3.0/organize/config.py000066400000000000000000000066231472111340300167730ustar00rootroot00000000000000from __future__ import annotations import os import textwrap from pathlib import Path from typing import Iterable, List, Optional, Union import yaml from pydantic import ConfigDict, ValidationError from pydantic.dataclasses import dataclass from .errors import ConfigError from .output import Default, Output from .rule import Rule from .template import render from .utils import ReportSummary, normalize_unicode Tags = Iterable[str] def default_yaml_cnst(loader, tag_suffix, node): # disable yaml constructors for strings starting with exclamation marks # https://stackoverflow.com/a/13281292/300783 return str(node.tag) yaml.add_multi_constructor("", default_yaml_cnst, Loader=yaml.SafeLoader) def should_execute(rule_tags: Tags, tags: Tags, skip_tags: Tags) -> bool: """ returns whether the rule with `rule_tags` should be executed, given `tags` and `skip_tags` """ if not rule_tags: rule_tags = set() if not tags: tags = set() if not skip_tags: skip_tags = set() if "always" in rule_tags and "always" not in skip_tags: return True if "never" in rule_tags and "never" not in tags: return False if not tags and not skip_tags: return True if not rule_tags and tags: return False should_run = any(tag in tags for tag in rule_tags) or not tags or not rule_tags should_skip = any(tag in skip_tags for tag in rule_tags) return should_run and not should_skip @dataclass(config=ConfigDict(extra="ignore")) class Config: rules: List[Rule] _config_path: Optional[Path] = None @classmethod def from_string(cls, config: str, config_path: Optional[Path] = None) -> Config: normalized = normalize_unicode(config) dedented = textwrap.dedent(normalized) as_dict = yaml.load(dedented, Loader=yaml.SafeLoader) try: if not as_dict: raise ValueError("Config is empty") inst = cls(**as_dict) inst._config_path = config_path return inst except ValidationError as e: # add a config_path property to the ValidationError raise ConfigError(e=e, config_path=config_path) from e @classmethod def from_path(cls, config_path: Path) -> Config: text = config_path.read_text(encoding="utf-8") inst = cls.from_string(text, config_path=config_path) return inst def execute( self, simulate: bool = True, output: Output = Default(), tags: Tags = set(), skip_tags: Tags = set(), working_dir: Union[str, Path] = ".", ) -> None: working_path = Path(render(str(working_dir))) os.chdir(working_path) output.start( simulate=simulate, config_path=self._config_path, working_dir=working_path, ) summary = ReportSummary() try: for rule_nr, rule in enumerate(self.rules): if should_execute( rule_tags=rule.tags, tags=tags, skip_tags=skip_tags, ): rule_summary = rule.execute( simulate=simulate, output=output, rule_nr=rule_nr, ) summary += rule_summary finally: output.end(summary.success, summary.errors) organize-3.3.0/organize/errors.py000066400000000000000000000027231472111340300170370ustar00rootroot00000000000000from pathlib import Path from typing import Iterable, Optional from pydantic import ValidationError class ConfigError(ValueError): def __init__(self, e: ValidationError, config_path: Optional[Path] = None): self.e = e self.config_path = config_path.resolve() if config_path else None def __str__(self): count = self.e.error_count() title = self.config_path or self.e.title lines = [ f'{count} validation error{"s" if count != 1 else ""} in "{title}"', "", ] for err in self.e.errors(include_url=False): loc = ".".join(str(x) for x in err["loc"]) msg = err["msg"] inp_name = err["input"] inp_type = type(err["input"]).__name__ inp = f'[input_value="{inp_name}", input_type={inp_type}]' lines.append(loc) lines.append(f" {msg} {inp}") return "\n".join(lines) def json(self): return self.e.json() class ConfigNotFound(FileNotFoundError): def __init__( self, config: str, search_pathes: Iterable[Path] = tuple(), ): self.config = config self.search_pathes = search_pathes def __str__(self): msg = f'Cannot find config "{self.config}".' if self.search_pathes: path_listing = "\n".join(f' - "{path}"' for path in self.search_pathes) return f"{msg}\nSearch locations:\n{path_listing}" return msg organize-3.3.0/organize/filter.py000066400000000000000000000041151472111340300170050ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, NamedTuple, Protocol, runtime_checkable from organize.logger import logger if TYPE_CHECKING: from .output import Output from .resource import Resource class FilterConfig(NamedTuple): name: str files: bool dirs: bool @runtime_checkable class HasFilterConfig(Protocol): filter_config: FilterConfig class HasFilterPipeline(Protocol): def pipeline(self, res: Resource, output: Output) -> bool: ... # pragma: no cover @runtime_checkable class Filter(HasFilterPipeline, HasFilterConfig, Protocol): def __init__(self, *args, **kwargs) -> None: # allow any amount of args / kwargs for BaseModel and dataclasses. ... # pragma: no cover class Not: def __init__(self, filter: Filter): self.filter = filter self.filter_config = self.filter.filter_config def pipeline(self, res: Resource, output: Output) -> bool: return not self.filter.pipeline(res=res, output=output) def __repr__(self): return f"Not({self.filter})" class All: def __init__(self, *filters: Filter): self.filters = filters def pipeline(self, res: Resource, output: Output) -> bool: for filter in self.filters: try: match = filter.pipeline(res, output=output) if not match: return False except Exception as e: output.msg(res=res, level="error", msg=str(e), sender=filter) logger.exception(e) return False return True class Any: def __init__(self, *filters: Filter): self.filters = filters def pipeline(self, res: Resource, output: Output) -> bool: result = False for filter in self.filters: try: match = filter.pipeline(res, output=output) if match: result = True except Exception as e: output.msg(res=res, level="error", msg=str(e), sender=filter) logger.exception(e) return result organize-3.3.0/organize/filters/000077500000000000000000000000001472111340300166155ustar00rootroot00000000000000organize-3.3.0/organize/filters/__init__.py000066400000000000000000000013001472111340300207200ustar00rootroot00000000000000from .created import Created from .date_added import DateAdded from .date_lastused import DateLastUsed from .duplicate import Duplicate from .empty import Empty from .exif import Exif from .extension import Extension from .filecontent import FileContent from .hash import Hash from .lastmodified import LastModified from .macos_tags import MacOSTags from .mimetype import MimeType from .name import Name from .python import Python from .regex import Regex from .size import Size ALL = ( Created, DateAdded, DateLastUsed, Duplicate, Empty, Exif, Extension, FileContent, Hash, LastModified, MacOSTags, MimeType, Name, Python, Regex, Size, ) organize-3.3.0/organize/filters/common/000077500000000000000000000000001472111340300201055ustar00rootroot00000000000000organize-3.3.0/organize/filters/common/__init__.py000066400000000000000000000000001472111340300222040ustar00rootroot00000000000000organize-3.3.0/organize/filters/common/timefilter.py000066400000000000000000000043071472111340300226270ustar00rootroot00000000000000from datetime import datetime, tzinfo from pathlib import Path from typing import ClassVar, Literal, Union import arrow from pydantic.config import ConfigDict from pydantic.dataclasses import dataclass from organize.filter import FilterConfig from organize.output import Output from organize.resource import Resource @dataclass(config=ConfigDict(extra="forbid", arbitrary_types_allowed=True)) class TimeFilter: years: int = 0 months: int = 0 weeks: int = 0 days: int = 0 hours: int = 0 minutes: int = 0 seconds: int = 0 mode: Literal["older", "newer"] = "older" timezone: Union[tzinfo, str] = "local" filter_config: ClassVar[FilterConfig] = FilterConfig( "timefilter", files=True, dirs=False, ) def __post_init__(self): self._has_comparison = ( self.years or self.months or self.weeks or self.days or self.hours or self.minutes or self.seconds ) self._comparison_dt = ( arrow.now() .shift( years=-self.years, months=-self.months, weeks=-self.weeks, days=-self.days, hours=-self.hours, minutes=-self.minutes, seconds=-self.seconds, ) .datetime ) def matches_datetime(self, dt: datetime) -> bool: if not self._has_comparison: return True if self.mode == "older": return dt < self._comparison_dt elif self.mode == "newer": return dt > self._comparison_dt else: raise ValueError(f"Unknown mode {self.mode}") def pipeline(self, res: Resource, output: Output) -> bool: assert res.path is not None, "Does not support standalone mode" try: dt = self.get_datetime(res.path) except Exception: return False # apply timezone dt = arrow.get(dt).to(self.timezone).datetime res.vars[self.filter_config.name] = dt return self.matches_datetime(dt) def get_datetime(self, path: Path) -> datetime: raise NotImplementedError() organize-3.3.0/organize/filters/created.py000066400000000000000000000050171472111340300206010ustar00rootroot00000000000000import subprocess import sys from datetime import datetime, timezone from pathlib import Path from typing import ClassVar, Optional from organize.filter import FilterConfig from .common.timefilter import TimeFilter def read_stat_created(path: Path) -> Optional[int]: commands = ( ["stat", "--format=%W", str(path)], # GNU coreutils ["stat", "-f %B", str(path)], # BSD ) for cmd in commands: try: created_str = subprocess.check_output(cmd, encoding="utf-8").strip() timestamp = int(created_str) return timestamp except subprocess.CalledProcessError: pass return None def read_created(path: Path) -> datetime: timestamp = None stat_result = path.stat() # ctime is the creation time only in Windows. # On unix it's the datetime of the last metadata change. if sys.platform == "win32": timestamp = stat_result.st_ctime else: # On other Unix systems (such as FreeBSD), the following # attributes may be available (but may be only filled out if # root tries to use them): try: timestamp = stat_result.st_birthtime # type: ignore except AttributeError: pass # If we still haven't gotten a timestamp, we try the (slower) fallback # method using the `stat` tool. if timestamp is None: timestamp = read_stat_created(path) # give up. if timestamp is None: raise EnvironmentError("The creation time is not available.") return datetime.fromtimestamp(timestamp, timezone.utc) class Created(TimeFilter): """Matches files / folders by created date Attributes: years (int): specify number of years months (int): specify number of months weeks (float): specify number of weeks days (float): specify number of days hours (float): specify number of hours minutes (float): specify number of minutes seconds (float): specify number of seconds mode (str): either 'older' or 'newer'. 'older' matches files / folders created before the given time, 'newer' matches files / folders created within the given time. (default = 'older') Returns: `{created}` (datetime): The datetime the file / folder was created. """ filter_config: ClassVar[FilterConfig] = FilterConfig( name="created", files=True, dirs=True, ) def get_datetime(self, path: Path) -> datetime: return read_created(path) organize-3.3.0/organize/filters/date_added.py000066400000000000000000000032151472111340300212260ustar00rootroot00000000000000import subprocess import sys from datetime import datetime from pathlib import Path from typing import ClassVar from organize.filter import FilterConfig from .common.timefilter import TimeFilter def read_date_added(path: Path) -> datetime: cmd = ["mdls", "-name", "kMDItemDateAdded", "-raw", str(path)] out = subprocess.check_output(cmd, encoding="utf-8").strip() dt = datetime.strptime(out, "%Y-%m-%d %H:%M:%S %z") return dt class DateAdded(TimeFilter): """Matches files by the time the file was added to a folder. **`date_added` is only available on macOS!** Attributes: years (int): specify number of years months (int): specify number of months weeks (float): specify number of weeks days (float): specify number of days hours (float): specify number of hours minutes (float): specify number of minutes seconds (float): specify number of seconds mode (str): either 'older' or 'newer'. 'older' matches files / folders last modified before the given time, 'newer' matches files / folders last modified within the given time. (default = 'older') Returns: `{date_added}`: The datetime the files / folders were added. """ filter_config: ClassVar[FilterConfig] = FilterConfig( name="date_added", files=True, dirs=True, ) def __post_init__(self): if sys.platform != "darwin": raise EnvironmentError("date_added is only available on macOS") return super().__post_init__() def get_datetime(self, path: Path) -> datetime: return read_date_added(path) organize-3.3.0/organize/filters/date_lastused.py000066400000000000000000000033241472111340300220120ustar00rootroot00000000000000import subprocess import sys from datetime import datetime from pathlib import Path from typing import ClassVar from organize.filter import FilterConfig from .common.timefilter import TimeFilter def read_date_lastused(path: Path) -> datetime: cmd = ["mdls", "-name", "kMDItemLastUsedDate", "-raw", str(path)] out = subprocess.check_output(cmd, encoding="utf-8").strip() if out == "(null)": raise ValueError("date_lastused not available") return datetime.strptime(out, "%Y-%m-%d %H:%M:%S %z") class DateLastUsed(TimeFilter): """Matches files by the time the file was last used. **`date_lastused` is only available on macOS!** Attributes: years (int): specify number of years months (int): specify number of months weeks (float): specify number of weeks days (float): specify number of days hours (float): specify number of hours minutes (float): specify number of minutes seconds (float): specify number of seconds mode (str): either 'older' or 'newer'. 'older' matches files / folders last used before the given time, 'newer' matches files / folders last used within the given time. (default = 'older') Returns: {date_lastused}: The datetime the files / folders were added. """ filter_config: ClassVar[FilterConfig] = FilterConfig( name="date_lastused", files=True, dirs=True, ) def __post_init__(self): if sys.platform != "darwin": raise EnvironmentError("date_added is only available on macOS") return super().__post_init__() def get_datetime(self, path: Path) -> datetime: return read_date_lastused(path) organize-3.3.0/organize/filters/duplicate.py000066400000000000000000000161601472111340300211450ustar00rootroot00000000000000""" Duplicate detection filter. Based on this stackoverflow answer: https://stackoverflow.com/a/36113168/300783 Which was updated for python3 in: https://gist.github.com/tfeldmann/fc875e6630d11f2256e746f67a09c1ae """ from collections import defaultdict from pathlib import Path from typing import Any, Callable, ClassVar, Literal, NamedTuple, Tuple from pydantic.config import ConfigDict from pydantic.dataclasses import dataclass from organize.filter import FilterConfig from organize.filters.created import read_created from organize.filters.hash import hash, hash_first_chunk from organize.filters.lastmodified import read_lastmodified from organize.filters.size import read_file_size from organize.output import Output from organize.resource import Resource DetectionMethod = Literal[ "first_seen", "-first_seen", "last_seen", "-last_seen", "name", "-name", "created", "-created", "lastmodified", "-lastmodified", ] class OriginalDetectionResult(NamedTuple): original: Path duplicate: Path @classmethod def by_sorting( cls, known: Path, new: Path, key: Callable[[Path], Any], ) -> "OriginalDetectionResult": if key(known) <= key(new): return cls(original=known, duplicate=new) return cls(original=new, duplicate=known) def reversed(self) -> "OriginalDetectionResult": return OriginalDetectionResult( original=self.duplicate, duplicate=self.original, ) def detect_original( known: Path, new: Path, method: DetectionMethod, reverse: bool ) -> Tuple[Path, Path]: """Returns a tuple (original file, duplicate)""" if method == "first_seen": result = OriginalDetectionResult(original=known, duplicate=new) elif method == "last_seen": result = OriginalDetectionResult(original=new, duplicate=known) elif method == "name": result = OriginalDetectionResult.by_sorting( known=known, new=new, key=lambda x: x.name ) elif method == "created": result = OriginalDetectionResult.by_sorting( known=known, new=new, key=lambda x: read_created(x) ) elif method == "lastmodified": result = OriginalDetectionResult.by_sorting( known=known, new=new, key=lambda x: read_lastmodified(x) ) else: raise ValueError(f"Unknown original detection method: {method}") return result.reversed() if reverse else result @dataclass(config=ConfigDict(extra="forbid")) class Duplicate: """A fast duplicate file finder. This filter compares files byte by byte and finds identical files with potentially different filenames. Attributes: detect_original_by (str): Detection method to distinguish between original and duplicate. Possible values are: - `"first_seen"`: Whatever file is visited first is the original. This depends on the order of your location entries. - `"name"`: The first entry sorted by name is the original. - `"created"`: The first entry sorted by creation date is the original. - `"lastmodified"`: The first file sorted by date of last modification is the original. You can reverse the sorting method by prefixing a `-`. So with `detect_original_by: "-created"` the file with the older creation date is the original and the younger file is the duplicate. This works on all methods, for example `"-first_seen"`, `"-name"`, `"-created"`, `"-lastmodified"`. **Returns:** `{duplicate.original}` - The path to the original """ detect_original_by: DetectionMethod = "first_seen" hash_algorithm: str = "sha1" filter_config: ClassVar[FilterConfig] = FilterConfig( name="duplicate", files=True, dirs=False ) def __post_init__(self): # reverse original detection order if starting with "-" self._detect_original_by = self.detect_original_by self._detect_original_reverse = False if self.detect_original_by.startswith("-"): self._detect_original_by = self.detect_original_by[1:] self._detect_original_reverse = True self._files_for_size = defaultdict(list) self._files_for_chunk = defaultdict(list) self._file_for_hash = dict() # we keep track of the files we already computed the hashes for so we only do # that once. self._seen_files = set() self._first_chunk_known = set() self._hash_known = set() def pipeline(self, res: Resource, output: Output) -> bool: assert res.path is not None, "Does not support standalone mode" # skip symlinks if res.path.is_symlink(): return False # the exact same path has already been handled. This happens if multiple # locations emit this file in a single rule or if we follow symlinks. # We skip these. if res.path in self._seen_files: return False self._seen_files.add(res.path) # check for files with equal size file_size = read_file_size(path=res.path) same_size = self._files_for_size[file_size] same_size.append(res.path) if len(same_size) == 1: # the file is unique in size and cannot be a duplicate return False # for all other files with the same file size: # make sure we know their hash of their first 1024 byte chunk for f in same_size[:-1]: if f not in self._first_chunk_known: chunk_hash = hash_first_chunk(f, algo=self.hash_algorithm) self._first_chunk_known.add(f) self._files_for_chunk[chunk_hash].append(f) # check first chunk hash collisions with the current file chunk_hash = hash_first_chunk(res.path, algo=self.hash_algorithm) same_first_chunk = self._files_for_chunk[chunk_hash] same_first_chunk.append(res.path) self._first_chunk_known.add(res.path) if len(same_first_chunk) == 1: # the file has a unique small hash and cannot be a duplicate return False # Ensure we know the full hashes of all files with the same first chunk as # the investigated file for f in same_first_chunk[:-1]: if f not in self._hash_known: hash_ = hash(f, algo=self.hash_algorithm) self._hash_known.add(f) self._file_for_hash[hash_] = f # check full hash collisions with the current file hash_ = hash(res.path, algo=self.hash_algorithm) self._hash_known.add(res.path) known = self._file_for_hash.get(hash_) if known: original, duplicate = detect_original( known=known, new=res.path, method=self._detect_original_by, reverse=self._detect_original_reverse, ) if known != original: self._file_for_hash[hash_] = original res.path = duplicate res.vars[self.filter_config.name] = {"original": original} return True return False organize-3.3.0/organize/filters/empty.py000066400000000000000000000010411472111340300203210ustar00rootroot00000000000000from typing import ClassVar from pydantic.config import ConfigDict from pydantic.dataclasses import dataclass from organize.filter import FilterConfig from organize.output import Output from organize.resource import Resource @dataclass(config=ConfigDict(extra="forbid")) class Empty: """Finds empty dirs and files""" filter_config: ClassVar[FilterConfig] = FilterConfig( name="empty", files=True, dirs=True, ) def pipeline(self, res: Resource, output: Output) -> bool: return res.is_empty() organize-3.3.0/organize/filters/exif.py000066400000000000000000000211411472111340300201210ustar00rootroot00000000000000import collections import fnmatch import json import os import subprocess from datetime import date, datetime, timedelta from functools import lru_cache from pathlib import Path from typing import Any, ClassVar, DefaultDict, Dict, Optional, Union import exifread from pydantic import BaseModel from rich import print from organize.filter import FilterConfig from organize.logger import logger from organize.output import Output from organize.resource import Resource ExifStrDict = Dict[str, Dict[str, str]] ExifValue = Union[str, datetime, date, timedelta] ExifDict = Dict[str, Dict[str, ExifValue]] ExifDefaultDict = DefaultDict[str, DefaultDict[str, ExifValue]] ORGANIZE_EXIFTOOL_PATH = os.environ.get("ORGANIZE_EXIFTOOL_PATH", "") @lru_cache(maxsize=1) def exiftool_available() -> bool: # don't use exiftool if user blanked the env path if not ORGANIZE_EXIFTOOL_PATH: return False # check whether the given path is executable try: subprocess.check_call( [ORGANIZE_EXIFTOOL_PATH, "-ver"], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, ) return True except subprocess.CalledProcessError: logger.warning("exiftool not available. Falling back to exifread library.") return False def group_keys_by_splitting( data: Dict[str, Any], delimiter: str = " ", ) -> Dict[str, Dict[str, Any]]: """ >>> group_keys_by_splitting({"cat a": 1, "cat b": 2, "x c": 1, "x d": 2}) {"cat": {"a": 1, "b": 2}, "x": {"c": 1, "d": 2}} """ result: DefaultDict[str, Any] = collections.defaultdict(dict) for k, v in data.items(): if delimiter in k: category, field = k.split(delimiter, maxsplit=1) result[category][field] = v else: result[k] = v return dict(result) def lowercase_keys_recursive(data) -> Dict: if isinstance(data, Dict): return {k.lower(): lowercase_keys_recursive(v) for k, v in data.items()} return data def parse_date_value(value: str) -> Union[datetime, date, str]: """ Parse datetime or date values (e.g. image.datetime, exif.datetimeoriginal, ...) Exiftool often gives date and time values for keys with only "date" in their name so we have to check both in order to preserve this information. """ try: return datetime.strptime(value[:19], "%Y:%m:%d %H:%M:%S") except ValueError: pass try: return datetime.strptime(value[:10], "%Y:%m:%d").date() except ValueError: pass return value def parse_offset_value(value: str) -> Union[timedelta, str]: """ Parse offset values (e.g. exif.offsettimeoriginal, exif.offsettimedigitized) Supports formats "+HHMM" or "+HH:MM[:SS]" or "UTC+HH:MM[:SS]" """ try: if value[:3].upper() == "UTC": value = value[3:] # remove UTC return datetime.strptime(value.replace(":", ""), "%z").utcoffset() or timedelta( seconds=0 ) except (ValueError, TypeError): return value def convert_value(key: str, value: str) -> ExifValue: _key = key.lower() if "date" in _key: return parse_date_value(value) if "offset" in _key: return parse_offset_value(value) return value def convert_recursive(data): result = dict() for k, v in data.items(): if isinstance(v, Dict): result[k] = convert_recursive(v) else: result[k] = convert_value(k, v) return result def exifread_read(path: Path) -> ExifStrDict: """ Uses the `exifread` library to read the EXIF data """ with path.open("rb") as f: data = exifread.process_file(fh=f, details=False, debug=False) # at this point data still contains exifread specific types like # Short / Ratio / ASCII which we now convert to a printable representation printable = {key: val.printable for (key, val) in data.items()} grouped = group_keys_by_splitting(printable) return grouped def exiftool_read(path: Path) -> ExifStrDict: """ Uses the `exiftool` tool by Phil Harvey to read the EXIF data """ try: data_json = subprocess.check_output( ( ORGANIZE_EXIFTOOL_PATH, "-j", "-g", "--fast", str(path), ), text=True, ) except subprocess.CalledProcessError: return dict() # we pass a single filepath, so we are interested in the first element data: Dict = json.loads(data_json)[0] # if the result only contains "File", "SourceFile" and "ExifTool" it means exiftool # couldn't find any additional data about this file. if set(data.keys()) == set(["SourceFile", "ExifTool", "File"]): return dict() return data def matches_tags( filter_tags: Dict[str, Optional[str]], data: Dict[str, Dict[str, str]], ) -> bool: if not data: return False for k, v in filter_tags.items(): try: # step into the data dict by dotted notation data_value: Any = data for part in k.split("."): data_value = data_value[part] # if v is None it's enough for the key to exist in the data. # Otherwise we use a glob matcher to check for matches if v is not None and not fnmatch.fnmatch(data_value.lower(), v.lower()): return False except (KeyError, AttributeError): return False return True class Exif(BaseModel): """Filter by image EXIF data The `exif` filter can be used as a filter as well as a way to get exif information into your actions. By default this library uses the `exifread` library. If your image format is not supported you can install `exiftool` (exiftool.org) and set the environment variable: ``` ORGANIZE_EXIFTOOL_PATH="exiftool" ``` organize will then use `exiftool` to extract the EXIF data. Exif fields which contain "datetime", "date" or "offsettime" in their fieldname will have their value converted to 'datetime.datetime', 'datetime.date' and 'datetime.timedelta' respectivly. - `datetime.datetime` : exif.image.datetime, exif.exif.datetimeoriginal, ... - `datetime.date` : exif.gps.date, ... - `datetime.timedelta` : exif.exif.offsettimeoriginal, exif.exif.offsettimedigitized, ... Attributes: lowercase_keys (bool): Whether to lowercase all EXIF keys (Default: true) :returns: ``{exif}`` -- a dict of all the collected exif inforamtion available in the file. Typically it consists of the following tags (if present in the file): - ``{exif.image}`` -- information related to the main image - ``{exif.exif}`` -- Exif information - ``{exif.gps}`` -- GPS information - ``{exif.interoperability}`` -- Interoperability information """ filter_tags: Dict lowercase_keys: bool = True filter_config: ClassVar[FilterConfig] = FilterConfig( name="exif", files=True, dirs=False, ) def __init__( self, *args, filter_tags: Optional[Dict] = None, lowercase_keys: bool = True, **kwargs, ): # exif filter is used differently from other filters. The **kwargs are not # filter parameters but all belong into the filter_tags dictionary to filter # for specific exif tags. params = filter_tags or dict() params.update(kwargs) # *args are tags filtered without a value, like ["gps", "image.model"]. for arg in args: params[arg] = None super().__init__(filter_tags=params, lowercase_keys=lowercase_keys) def pipeline(self, res: Resource, output: Output) -> bool: assert res.path is not None, "Does not support standalone mode" # gather the exif data in a dict if exiftool_available(): data = exiftool_read(path=res.path) else: data = exifread_read(path=res.path) # lowercase keys if wanted if self.lowercase_keys: data = lowercase_keys_recursive(data) # convert strings to datetime objects where possible parsed = convert_recursive(data) res.vars[self.filter_config.name] = parsed return matches_tags(self.filter_tags, data) if __name__ == "__main__": import sys # Usage: # python organize/filters/exif.py tests/resources/images-with-exif/3.jpg data = exifread_read(Path(sys.argv[1])) print("Exifread", data) if exiftool_available(): data = exiftool_read(Path(sys.argv[1])) print("Exiftool", data) else: print("Exiftool not available") organize-3.3.0/organize/filters/extension.py000066400000000000000000000037611472111340300212120ustar00rootroot00000000000000from pathlib import Path from typing import ClassVar, Set, Tuple from pydantic import Field, field_validator from pydantic.config import ConfigDict from pydantic.dataclasses import dataclass from organize.filter import FilterConfig from organize.output import Output from organize.resource import Resource from organize.validators import flatten def convert_to_list(v): if not v: return [] if isinstance(v, str): return v.split() return v def normalize_extension(ext: str) -> str: """strip colon and convert to lowercase""" if ext.startswith("."): return ext[1:].lower() else: return ext.lower() @dataclass(config=ConfigDict(coerce_numbers_to_str=True, extra="forbid")) class Extension: """Filter by file extension Attributes: *extensions (list(str) or str): The file extensions to match (does not need to start with a colon). **Returns:** - `{extension}`: the original file extension (without colon) """ extensions: Set[str] = Field(default_factory=set) filter_config: ClassVar[FilterConfig] = FilterConfig( name="extension", files=True, dirs=False, ) @field_validator("extensions", mode="before") def normalize_extensions(cls, v): as_list = convert_to_list(v) return set(map(normalize_extension, flatten(list(as_list)))) def suffix_match(self, path: Path) -> Tuple[str, bool]: suffix = path.suffix.lstrip(".") if not self.extensions: return (suffix, True) if not suffix: return (suffix, False) return (suffix, normalize_extension(suffix) in self.extensions) def pipeline(self, res: Resource, output: Output) -> bool: assert res.path is not None, "Does not support standalone mode" if res.is_dir(): raise ValueError("Dirs not supported") suffix, match = self.suffix_match(path=res.path) res.vars[self.filter_config.name] = suffix return match organize-3.3.0/organize/filters/filecontent.py000066400000000000000000000102651472111340300215050ustar00rootroot00000000000000import re import subprocess from functools import lru_cache from pathlib import Path from typing import Callable, ClassVar, Dict, Union from pydantic.config import ConfigDict from pydantic.dataclasses import dataclass from organize.filter import FilterConfig from organize.logger import logger from organize.output import Output from organize.resource import Resource def _compress_chars(inp: str) -> str: # Compress lines consisting only of separated chars ("H e l l o W o r l d") result = [] for line in inp.splitlines(): if re.match(r"^(\S +)+\S$", line): result.append(re.sub(r"(\S) ", repl=r"\g<1>", string=line)) else: result.append(line) return "\n".join(result) def _remove_nls(inp: str) -> str: # remove superfluous newlines return re.sub(pattern=r"\n{3,}", repl="\n\n", string=inp, flags=re.MULTILINE) def clean(inp: str) -> str: return _remove_nls(_compress_chars(inp)) def extract_txt(path: Path) -> str: return path.read_text(encoding="utf-8") @lru_cache(maxsize=1) def _pdftotext_available() -> bool: # check whether the given path is executable try: subprocess.check_call( ["pdftotext", "-v"], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, ) return True except subprocess.CalledProcessError: logger.warning("pdftotext not available. Falling back to pdfminer library.") return False def _extract_with_pdftotext(path: Path, keep_layout: bool) -> str: if keep_layout: args = ["-layout", str(path), "-"] else: args = [str(path), "-"] result = subprocess.check_output( ("pdftotext", *args), text=True, stderr=subprocess.DEVNULL, ) return clean(result) def _extract_with_pdfminer(path: Path) -> str: from pdfminer import high_level return clean(high_level.extract_text(path)) def extract_pdf(path: Path, keep_layout: bool = True) -> str: if _pdftotext_available(): return _extract_with_pdftotext(path=path, keep_layout=keep_layout) return _extract_with_pdfminer(path=path) def extract_docx(path: Path) -> str: import docx2txt # type: ignore result = docx2txt.process(path) return clean(result) EXTRACTORS: Dict[str, Callable[[Path], str]] = { ".md": extract_txt, ".txt": extract_txt, ".log": extract_txt, ".pdf": extract_pdf, ".docx": extract_docx, } def textract(path: Path) -> str: extractor = EXTRACTORS[path.suffix.lower()] return extractor(path) @dataclass(config=ConfigDict(coerce_numbers_to_str=True, extra="forbid")) class FileContent: """Matches file content with the given regular expression. Supports .md, .txt, .log, .pdf and .docx files. For PDF content extraction poppler should be installed for the `pdftotext` command. If this is not available `filecontent` will fall back to the `pdfminer` library. Attributes: expr (str): The regular expression to be matched. Any named groups (`(?P.*)`) in your regular expression will be returned like this: **Returns:** - `{filecontent.groupname}`: The text matched with the named group `(?P)` You can test the filter on any file by running: ```sh python -m organize.filters.filecontent "/path/to/file.pdf" ``` """ expr: str = r"(?P.*)" filter_config: ClassVar[FilterConfig] = FilterConfig( name="filecontent", files=True, dirs=False, ) def __post_init__(self): self._expr = re.compile(self.expr, re.MULTILINE | re.DOTALL) def matches(self, path: Path) -> Union[re.Match, None]: try: content = textract(path) match = self._expr.search(content) return match except Exception: return None def pipeline(self, res: Resource, output: Output) -> bool: assert res.path is not None, "Does not support standalone mode" match = self.matches(path=res.path) if match: res.deep_merge(self.filter_config.name, match.groupdict()) return bool(match) if __name__ == "__main__": import sys print(textract(Path(sys.argv[1]))) organize-3.3.0/organize/filters/hash.py000066400000000000000000000051541472111340300201170ustar00rootroot00000000000000import hashlib from pathlib import Path from typing import ClassVar from pydantic.config import ConfigDict from pydantic.dataclasses import dataclass from organize.filter import FilterConfig from organize.output import Output from organize.resource import Resource from organize.template import Template, render def hash(path: Path, algo: str, *, _bufsize=2**18) -> str: # for python >= 3.11 we can use hashlib.file_digest if hasattr(hashlib, "file_digest"): with path.open("rb") as f: return hashlib.file_digest(f, algo, _bufsize=_bufsize).hexdigest() # otherwise we have to use our own backported implementation: h = hashlib.new(algo) buf = bytearray(_bufsize) view = memoryview(buf) with path.open("rb", buffering=0) as f: while size := f.readinto(buf): h.update(view[:size]) return h.hexdigest() def hash_first_chunk(path: Path, algo: str, *, chunksize=1024) -> str: h = hashlib.new(algo) with path.open("rb") as f: chunk = f.read(chunksize) h.update(chunk) return h.hexdigest() @dataclass(config=ConfigDict(extra="forbid")) class Hash: """Calculates the hash of a file. Attributes: algorithm (str): Any hashing algorithm available to python's `hashlib`. `md5` by default. Algorithms guaranteed to be available are `shake_256`, `sha3_256`, `sha1`, `sha3_224`, `sha384`, `sha512`, `blake2b`, `blake2s`, `sha256`, `sha224`, `shake_128`, `sha3_512`, `sha3_384` and `md5`. Depending on your python installation and installed libs there may be additional hash algorithms to chose from. To list the available algorithms on your installation run this in a python interpreter: ```py >>> import hashlib >>> hashlib.algorithms_available {'shake_256', 'whirlpool', 'mdc2', 'blake2s', 'sha224', 'shake_128', 'sha3_512', 'sha3_224', 'sha384', 'md5', 'sha1', 'sha512_256', 'blake2b', 'sha256', 'sha512_224', 'ripemd160', 'sha3_384', 'md4', 'sm3', 'sha3_256', 'md5-sha1', 'sha512'} ``` **Returns:** - `{hash}`: The hash of the file. """ algorithm: str = "md5" filter_config: ClassVar[FilterConfig] = FilterConfig( name="hash", files=True, dirs=False, ) def __post_init__(self): self._algorithm = Template.from_string(self.algorithm) def pipeline(self, res: Resource, output: Output) -> bool: assert res.path is not None algo = render(self._algorithm, res.dict()).lower() result = hash(path=res.path, algo=algo) res.vars[self.filter_config.name] = result return True organize-3.3.0/organize/filters/lastmodified.py000066400000000000000000000024121472111340300216320ustar00rootroot00000000000000from datetime import datetime, timezone from pathlib import Path from typing import ClassVar from organize.filter import FilterConfig from .common.timefilter import TimeFilter def read_lastmodified(path: Path) -> datetime: return datetime.fromtimestamp(path.stat().st_mtime, tz=timezone.utc) class LastModified(TimeFilter): """Matches files by last modified date Attributes: years (int): specify number of years months (int): specify number of months weeks (float): specify number of weeks days (float): specify number of days hours (float): specify number of hours minutes (float): specify number of minutes seconds (float): specify number of seconds mode (str): either 'older' or 'newer'. 'older' matches files / folders last modified before the given time, 'newer' matches files / folders last modified within the given time. (default = 'older') Returns: {lastmodified}: The datetime the files / folders was lastmodified. """ filter_config: ClassVar[FilterConfig] = FilterConfig( name="lastmodified", files=True, dirs=True, ) def get_datetime(self, path: Path) -> datetime: return read_lastmodified(path) organize-3.3.0/organize/filters/macos_tags.py000066400000000000000000000032521472111340300213110ustar00rootroot00000000000000import sys from typing import ClassVar, List from pydantic import Field, field_validator from pydantic.config import ConfigDict from pydantic.dataclasses import dataclass from organize.filter import FilterConfig from organize.output import Output from organize.resource import Resource from organize.utils import glob_match def list_tags(path) -> List[str]: import macos_tags tags = macos_tags.get_all(path) return ["{} ({})".format(tag.name, tag.color.name.lower()) for tag in tags] def matches_tags(filter_tags, file_tags) -> bool: if not filter_tags: return True if not file_tags: return False for tag in file_tags: if any(glob_match(filter_tag, tag) for filter_tag in filter_tags): return True return False @dataclass(config=ConfigDict(coerce_numbers_to_str=True, extra="forbid")) class MacOSTags: """Filter by macOS tags Attributes: tags (list(str) or str): The tags to filter by """ tags: List[str] = Field(default_factory=list) filter_config: ClassVar[FilterConfig] = FilterConfig( name="macos_tags", files=True, dirs=True, ) def __post_init__(self): if sys.platform != "darwin": raise EnvironmentError("The macos_tags filter is only available on macOS") @field_validator("tags", mode="before") def ensure_list(cls, v): if isinstance(v, str): return [v] return v def pipeline(self, res: Resource, output: Output) -> bool: file_tags = list_tags(res.path) res.vars[self.filter_config.name] = file_tags return matches_tags(filter_tags=self.tags, file_tags=file_tags) organize-3.3.0/organize/filters/mimetype.py000066400000000000000000000034721472111340300210260ustar00rootroot00000000000000import mimetypes from typing import ClassVar from pydantic import Field from pydantic.config import ConfigDict from pydantic.dataclasses import dataclass from organize.filter import FilterConfig from organize.output import Output from organize.resource import Resource from organize.validators import FlatList def guess_mimetype(path): type_, _ = mimetypes.guess_type(path, strict=False) return type_ @dataclass(config=ConfigDict(coerce_numbers_to_str=True, extra="forbid")) class MimeType: """Filter by MIME type associated with the file extension. Supports a single string or list of MIME type strings as argument. The types don't need to be fully specified, for example "audio" matches everything from "audio/midi" to "audio/quicktime". You can see a list of known MIME types on your system by running this oneliner: ```sh python3 -m organize.filters.mimetype ``` Attributes: *mimetypes (list(str) or str): The MIME types to filter for. **Returns:** - `{mimetype}`: The MIME type of the file. """ mimetypes: FlatList[str] = Field(default_factory=list) filter_config: ClassVar[FilterConfig] = FilterConfig( name="mimetype", files=True, dirs=False, ) def matches(self, mimetype) -> bool: if mimetype is None: return False if not self.mimetypes: return True return any(mimetype.startswith(x) for x in self.mimetypes) def pipeline(self, res: Resource, output: Output) -> bool: mimetype = guess_mimetype(res.path) res.vars[self.filter_config.name] = mimetype return self.matches(mimetype) if __name__ == "__main__": all_types = set(mimetypes.common_types.values()) | set(mimetypes.types_map.values()) for t in sorted(all_types): print(t) organize-3.3.0/organize/filters/name.py000066400000000000000000000056321472111340300201150ustar00rootroot00000000000000from typing import Any, ClassVar, List, Union import simplematch from pydantic.config import ConfigDict from pydantic.dataclasses import dataclass from organize.filter import FilterConfig from organize.output import Output from organize.resource import Resource from organize.utils import normalize_unicode @dataclass(config=ConfigDict(coerce_numbers_to_str=True, extra="forbid")) class Name: """Match files and folders by name Attributes: match (str): A matching string in [simplematch-syntax](https://github.com/tfeldmann/simplematch) startswith (str): The filename must begin with the given string contains (str): The filename must contain the given string endswith (str): The filename (without extension) must end with the given string case_sensitive (bool): By default, the matching is case sensitive. Change this to False to use case insensitive matching. """ match: str = "*" startswith: Union[str, List[str]] = "" contains: Union[str, List[str]] = "" endswith: Union[str, List[str]] = "" case_sensitive: bool = True filter_config: ClassVar[FilterConfig] = FilterConfig( name="name", files=True, dirs=True, ) def __post_init__(self, *args, **kwargs): self._matcher = simplematch.Matcher( self.match, case_sensitive=self.case_sensitive, ) self.startswith = self.create_list(self.startswith, self.case_sensitive) self.contains = self.create_list(self.contains, self.case_sensitive) self.endswith = self.create_list(self.endswith, self.case_sensitive) def matches(self, name: str) -> bool: if not self.case_sensitive: name = name.lower() is_match = ( self._matcher.test(name) and any(x in name for x in self.contains) and any(name.startswith(x) for x in self.startswith) and any(name.endswith(x) for x in self.endswith) ) return is_match def pipeline(self, res: Resource, output: Output) -> bool: assert res.path is not None, "Does not support standalone mode" if res.is_dir(): name = res.path.stem else: name, ext = res.path.stem, res.path.suffix if not name: name = ext result = self.matches(normalize_unicode(name)) m = self._matcher.match(normalize_unicode(name)) if not m: m = name res.vars[self.filter_config.name] = m return result @staticmethod def create_list(x: Union[int, str, List[Any]], case_sensitive: bool) -> List[str]: if isinstance(x, (int, float)): x = str(x) if isinstance(x, str): x = [x] x = [str(x) for x in x] if not case_sensitive: x = [x.lower() for x in x] return x organize-3.3.0/organize/filters/python.py000066400000000000000000000051601472111340300205120ustar00rootroot00000000000000import textwrap from typing import ClassVar, Dict, Optional from pydantic import field_validator from pydantic.config import ConfigDict from pydantic.dataclasses import dataclass from organize.filter import FilterConfig from organize.output import Output from organize.resource import Resource @dataclass(config=ConfigDict(coerce_numbers_to_str=True, extra="forbid")) class Python: """Use python code to filter files. Attributes: code (str): The python code to execute. The code must contain a `return` statement. **Returns:** - If your code returns `False` or `None` the file is filtered out, otherwise the file is passed on to the next filters. - `{python}` contains the returned value. If you return a dictionary (for example `return {"some_key": some_value, "nested": {"k": 2}}`) it will be accessible via dot syntax actions: `{python.some_key}`, `{python.nested.k}`. - Variables of previous filters are available, but you have to use the normal python dictionary syntax `x = regex["my_group"]`. """ code: str filter_config: ClassVar[FilterConfig] = FilterConfig( name="python", files=True, dirs=True, ) @field_validator("code", mode="after") @classmethod def must_have_return_statement(cls, value): if "return" not in value: raise ValueError("No return statement found in your code!") return value def __post_init__(self): self.code = textwrap.dedent(self.code) def __usercode__(self, print, **kwargs) -> Optional[Dict]: raise NotImplementedError() def pipeline(self, res: Resource, output: Output) -> bool: def _output_msg(*values, sep: str = " ", end: str = ""): """ the print function for the use code needs to print via the current output """ output.msg( res=res, msg=f"{sep.join(str(x) for x in values)}{end}", sender="python", ) # codegen the user function with arguments as available in the resource kwargs = ", ".join(res.dict().keys()) func = f"def __userfunc(print, {kwargs}):\n" func += textwrap.indent(self.code, " ") func += "\n\nself.__usercode__ = __userfunc" exec(func, globals().copy(), locals().copy()) result = self.__usercode__(print=_output_msg, **res.dict()) if isinstance(result, dict): res.deep_merge(key=self.filter_config.name, data=result) else: res.vars[self.filter_config.name] = result return result not in (False, None) organize-3.3.0/organize/filters/regex.py000066400000000000000000000025051472111340300203030ustar00rootroot00000000000000import re from typing import ClassVar from pydantic.config import ConfigDict from pydantic.dataclasses import dataclass from organize.filter import FilterConfig from organize.output import Output from organize.resource import Resource from organize.utils import normalize_unicode @dataclass(config=ConfigDict(coerce_numbers_to_str=True, extra="forbid")) class Regex: """Matches filenames with the given regular expression Attributes: expr (str): The regular expression to be matched. **Returns:** Any named groups in your regular expression will be returned like this: - `{regex.groupname}`: The text matched with the named group `(?P.*)` """ expr: str filter_config: ClassVar[FilterConfig] = FilterConfig( name="regex", files=True, dirs=True, ) def __post_init__(self): self._expr = re.compile(self.expr, flags=re.UNICODE) def matches(self, path: str): return self._expr.search(path) def pipeline(self, res: Resource, output: Output) -> bool: assert res.path is not None, "Does not support standalone mode" match = self.matches(normalize_unicode(res.path.name)) if match: res.deep_merge(key=self.filter_config.name, data=match.groupdict()) return True return False organize-3.3.0/organize/filters/size.py000066400000000000000000000120671472111340300201470ustar00rootroot00000000000000import operator import re from pathlib import Path from typing import ClassVar, Iterable from pydantic import Field from pydantic.config import ConfigDict from pydantic.dataclasses import dataclass from organize.filter import FilterConfig from organize.output import Output from organize.resource import Resource from organize.validators import FlatList OPERATORS = { "<": operator.lt, "<=": operator.le, "==": operator.eq, "=": operator.eq, "": operator.eq, ">=": operator.ge, ">": operator.gt, } SIZE_REGEX = re.compile( r"^(?P[<>=]*)(?P(\d*\.)?\d+)(?P[kmgtpezy]?i?)b?$" ) def read_file_size(path: Path) -> int: return path.stat().st_size def read_dir_size(path: Path) -> int: return sum(f.stat().st_size for f in path.glob("**/*") if f.is_file()) def read_resource_size(res: Resource) -> int: assert res.path is not None if res.is_file(): return read_file_size(res.path) if res.is_dir(): return read_dir_size(res.path) raise ValueError("Unknown file type") def create_constraints(inp: str): """ Given an input string it returns a list of tuples (comparison operator, number of bytes). Accepted formats are: "30k", ">= 5 TiB, <10tb", "< 60 tb", ... Calculation is in bytes, even if the "b" is lowercase. If an "i" is present we calculate base 1024. """ parts = str(inp).replace(" ", "").lower().split(",") for part in parts: try: reg_match = SIZE_REGEX.match(part) if reg_match: match = reg_match.groupdict() op = OPERATORS[match["op"]] num = float(match["num"]) if "." in match["num"] else int(match["num"]) unit = match["unit"] base = 1024 if unit.endswith("i") else 1000 exp = "kmgtpezy".index(unit[0]) + 1 if unit else 0 numbytes = num * base**exp yield (op, numbytes) except (AttributeError, KeyError, IndexError, ValueError, TypeError) as e: raise ValueError("Invalid size format: %s" % part) from e def satisfies_constraints(size, constraints) -> bool: return all(op(size, p_size) for op, p_size in constraints) def number_with_unit(size: int, suffixes: Iterable[str], base: int) -> str: size = int(size) if size == 1: return "1 byte" elif size < base: return "{:,} bytes".format(size) for i, suffix in enumerate(suffixes, 2): unit = base**i if size < unit: break return "{:,.1f} {}".format((base * size / unit), suffix) def traditional(size: int) -> str: """Convert a filesize in to a string (powers of 1024, JDEC prefixes).""" return number_with_unit( size, ("KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"), 1024 ) def binary(size: int) -> str: """Convert a filesize in to a string (powers of 1024, IEC prefixes).""" return number_with_unit( size, ("KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"), 1024 ) def decimal(size: int) -> str: """Convert a filesize in to a string (powers of 1000, SI prefixes).""" return number_with_unit( size, ("kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"), 1000 ) @dataclass(config=ConfigDict(coerce_numbers_to_str=True, extra="forbid")) class Size: """Matches files and folders by size Attributes: *conditions (list(str) or str): The size constraints. Accepts file size conditions, e.g: `">= 500 MB"`, `"< 20k"`, `">0"`, `"= 10 KiB"`. It is possible to define both lower and upper conditions like this: `">20k, < 1 TB"`, `">= 20 Mb, <25 Mb"`. The filter will match if all given conditions are satisfied. - Accepts all units from KB to YB. - If no unit is given, kilobytes are assumend. - If binary prefix is given (KiB, GiB) the size is calculated using base 1024. **Returns:** - `{size.bytes}`: (int) Size in bytes - `{size.traditional}`: (str) Size with unit (powers of 1024, JDEC prefixes) - `{size.binary}`: (str) Size with unit (powers of 1024, IEC prefixes) - `{size.decimal}`: (str) Size with unit (powers of 1000, SI prefixes) """ conditions: FlatList[str] = Field(default_factory=list) filter_config: ClassVar[FilterConfig] = FilterConfig( name="size", files=True, dirs=True ) def __post_init__(self): self._constraints = set() for x in self.conditions: for constraint in create_constraints(x): self._constraints.add(constraint) def matches(self, filesize: int) -> bool: if not self._constraints: return True return all(op(filesize, c_size) for op, c_size in self._constraints) def pipeline(self, res: Resource, output: Output) -> bool: assert res.path is not None bytes = read_resource_size(res=res) res.vars[self.filter_config.name] = { "bytes": bytes, "traditional": traditional(bytes), "binary": binary(bytes), "decimal": decimal(bytes), } return self.matches(bytes) organize-3.3.0/organize/find_config.py000066400000000000000000000065731472111340300177770ustar00rootroot00000000000000import os from collections.abc import Iterable from itertools import chain, product from pathlib import Path from typing import Iterator, Optional import platformdirs from organize.utils import expandvars from .errors import ConfigNotFound DOCS_RTD = "https://organize.readthedocs.io" DOCS_GHPAGES = "https://tfeldmann.github.io/organize/" ENV_ORGANIZE_CONFIG = os.environ.get("ORGANIZE_CONFIG") XDG_CONFIG_DIR = expandvars(os.environ.get("XDG_CONFIG_HOME", "~/.config")) / "organize" USER_CONFIG_DIR = platformdirs.user_config_path(appname="organize") def _search_dirs(include_cwd: bool) -> Iterable[Path]: if include_cwd: yield Path(".") yield XDG_CONFIG_DIR yield USER_CONFIG_DIR def find_config_by_name(name: str) -> Path: stem = Path(name).stem filenames = ( name, f"{stem}.yaml", f"{stem}.yml", f"{name}.yaml", f"{name}.yml", ) search_pathes = [ d / f for d, f in product(_search_dirs(include_cwd=True), filenames) ] for path in search_pathes: if path.is_file(): return path raise ConfigNotFound(config=stem, search_pathes=search_pathes) def find_default_config() -> Path: # if the `ORGANIZE_CONFIG` env variable is set we only check this specific location if ENV_ORGANIZE_CONFIG is not None: result = expandvars(ENV_ORGANIZE_CONFIG) if result.is_file(): return result raise ConfigNotFound(str(result)) # otherwise we check all default locations for "config.y[a]ml" return find_config_by_name("config") def find_config(name_or_path: Optional[str] = None) -> Path: # No config given? Find the default one. if name_or_path is None: return find_default_config() # Maybe we are given the path to a config file? as_path = expandvars(name_or_path) if as_path.is_file(): return as_path # search the default locations for the given name return find_config_by_name(name=name_or_path) def list_configs() -> Iterator[Path]: for loc in _search_dirs(include_cwd=False): yield from chain(loc.glob("*.yml"), loc.glob("*.yaml")) EXAMPLE_CONFIG = f"""\ # organize configuration file # {DOCS_RTD} rules: - locations: filters: actions: - echo: "Hello, World!" """ def example_config_path(name_or_path: Optional[str]) -> Path: # prefer "~/.config/organize" if it is already present on the system preferred_dir = XDG_CONFIG_DIR if XDG_CONFIG_DIR.is_dir() else USER_CONFIG_DIR if name_or_path is None: if ENV_ORGANIZE_CONFIG is not None: return expandvars(ENV_ORGANIZE_CONFIG) return preferred_dir / "config.yaml" # maybe we are given a path to create the config there? if "/" in name_or_path or "\\" in name_or_path: return expandvars(name_or_path) # create at preferred dir - # - keeping the extension if name_or_path.lower().endswith((".yml", ".yaml")): return preferred_dir / name_or_path # - with .yaml extension return preferred_dir / f"{name_or_path}.yaml" def create_example_config(name_or_path: Optional[str] = None) -> Path: path = example_config_path(name_or_path=name_or_path) if path.is_file(): raise FileExistsError(f'Config "{path.absolute()}" already exists.') path.parent.mkdir(parents=True, exist_ok=True) path.write_text(EXAMPLE_CONFIG, encoding="utf-8") return path organize-3.3.0/organize/location.py000066400000000000000000000020201472111340300173210ustar00rootroot00000000000000from typing import List, Literal, Union from pydantic import ConfigDict, Field from pydantic.dataclasses import dataclass from .validators import FlatList, FlatSet DEFAULT_SYSTEM_EXCLUDE_FILES = { "thumbs.db", "desktop.ini", "~$*", ".DS_Store", ".localized", } DEFAULT_SYSTEM_EXCLUDE_DIRS = { ".git", ".svn", } @dataclass(config=ConfigDict(extra="forbid")) class Location: path: FlatList[str] min_depth: int = 0 max_depth: Union[Literal["inherit"], int, None] = "inherit" search: Literal["depth", "breadth"] = "breadth" exclude_files: FlatSet[str] = Field(default_factory=set) exclude_dirs: FlatSet[str] = Field(default_factory=set) system_exclude_files: FlatSet[str] = Field( default_factory=lambda: DEFAULT_SYSTEM_EXCLUDE_FILES ) system_exclude_dirs: FlatSet[str] = Field( default_factory=lambda: DEFAULT_SYSTEM_EXCLUDE_DIRS ) filter: Union[List[str], None] = None filter_dirs: Union[List[str], None] = None ignore_errors: bool = False organize-3.3.0/organize/logger.py000066400000000000000000000011331472111340300167740ustar00rootroot00000000000000import logging from logging.handlers import RotatingFileHandler from pathlib import Path from platformdirs import user_log_dir logger = logging.getLogger(name="organize") def enable_logfile(): logdir = Path(user_log_dir(appname="organize", ensure_exists=True)) logging.basicConfig( level=logging.WARNING, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", handlers=[ RotatingFileHandler( filename=logdir / "organize-errors.log", backupCount=5, maxBytes=5_000_000, ) ], ) organize-3.3.0/organize/output/000077500000000000000000000000001472111340300165055ustar00rootroot00000000000000organize-3.3.0/organize/output/__init__.py000066400000000000000000000002771472111340300206240ustar00rootroot00000000000000from .default import Default from .jsonl import JSONL from .output import Output from .saving import SavingOutput __all__ = ( "JSONL", "Output", "SavingOutput", "Default", ) organize-3.3.0/organize/output/_sender.py000066400000000000000000000006361472111340300205030ustar00rootroot00000000000000from typing import Union from organize.action import HasActionConfig from organize.filter import HasFilterConfig SenderType = Union[HasActionConfig, HasFilterConfig, str] def sender_name(sender: SenderType) -> str: if isinstance(sender, HasFilterConfig): return sender.filter_config.name elif isinstance(sender, HasActionConfig): return sender.action_config.name return str(sender) organize-3.3.0/organize/output/default.py000066400000000000000000000151511472111340300205060ustar00rootroot00000000000000from __future__ import annotations from pathlib import Path from typing import TYPE_CHECKING, Dict, Optional from rich.console import Console from rich.panel import Panel from rich.prompt import Confirm as RichConfirm from rich.status import Status from rich.theme import Theme from organize.utils import ChangeDetector, escape from ._sender import sender_name from .output import Level if TYPE_CHECKING: from typing import List from organize.resource import Resource from ._sender import SenderType def format_path(path: Path, base_style: str, main_style: str) -> str: base = escape(f"{path.parent}/") main = escape(path.name) return f"[{base_style}]{base}[/][{main_style}]{main}[/]" MSG_STYLE: Dict[Level, str] = { "info": "[pipeline.msg]", "warn": "[pipeline.warn]", "error": "[pipeline.error]ERROR! ", } def format_msg( msg: str, level: Level, sender: SenderType, standalone: bool, escape_msg: bool = True, ) -> str: _msg = escape(msg) if escape_msg else msg indent = " - " if not standalone else "" fmt_sender = f"([pipeline.source]{escape(sender_name(sender))}[/])" fmt_msg = f"{MSG_STYLE[level]}{_msg}[/]" return f"{indent}{fmt_sender} {fmt_msg}" class Confirm(RichConfirm): @classmethod def set_error_msg(cls, msg: str) -> None: cls.validate_error_message = msg class Default: def __init__(self, theme: Optional[Theme] = None, errors_only: bool = False): if theme is None: theme = Theme( { "info": "dim cyan", "warning": "yellow", "error": "bold red", "simulation": "bold green", "status": "bold green", "rule": "bold cyan", "location.base": "green", "location.main": "bold green", "path.base": "dim green", "path.main": "green", "pipeline.source": "cyan", "pipeline.msg": "", "pipeline.warn": "yellow", "pipeline.error": "bold red", "pipeline.prompt": "bold yellow", "summary.done": "bold green", "summary.fail": "red", } ) self.errors_only = errors_only self.msg_queue: List[str] = [] self.det_resource = ChangeDetector() self.console = Console(theme=theme, highlight=False) self.status = Status("", console=self.console) self.det_rule = ChangeDetector() self.det_location = ChangeDetector() self.det_path = ChangeDetector() self.simulate = False def show_resource(self, res: Resource): # rule changed if self.det_rule.changed(res.rule): self.det_location.reset() self.det_path.reset() self.console.print() rule_name = f"Rule #{res.rule_nr}" if res.rule is not None and res.rule.name is not None: rule_name += f": {res.rule.name}" self.console.rule( f"[rule]:gear: {escape(rule_name)}", align="left", style="rule", ) # location changed if self.det_location.changed(res.basedir): self.det_path.reset() if res.basedir: path_str = format_path( Path(res.basedir), "location.base", "location.main", ) self.console.print(path_str) # path changed if self.det_path.changed(res.path): relative_path = res.relative_path() if relative_path is not None: path_str = format_path(relative_path, "path.base", "path.main") self.console.print(f" {path_str}") def start( self, simulate: bool, config_path: Optional[Path], working_dir: Path, ) -> None: self.det_rule.reset() self.det_location.reset() self.det_path.reset() self.simulate = simulate if self.simulate: self.console.print(Panel("SIMULATION", style="simulation")) if working_dir.resolve() != Path(".").resolve(): self.console.print(f'Working dir: "{escape(working_dir)}"') if config_path: self.console.print(f'Config: "{escape(config_path)}"') status_verb = "simulating" if simulate else "organizing" self.status.update(f"[status]{status_verb}[/]") self.status.start() def msg( self, res: Resource, msg: str, sender: SenderType, level: Level = "info", ) -> None: msg = format_msg( msg=msg, level=level, sender=sender, standalone=res.path is None, ) if self.errors_only: if self.det_resource.changed(res): self.msg_queue.clear() self.msg_queue.append(msg) if level == "error": self.show_resource(res) for msg in self.msg_queue: self.console.print(msg) self.msg_queue.clear() else: self.show_resource(res) self.console.print(msg) def confirm( self, res: Resource, msg: str, default: bool, sender: SenderType, ) -> bool: self.status.stop() self.show_resource(res) msg_prompt = format_msg( msg=f"[pipeline.prompt]{escape(msg)}[/]", level="info", sender=sender, standalone=res.path is None, escape_msg=False, ) msg_invalid = format_msg( msg="[prompt.invalid]Please enter Y or N[/]", level="info", sender=sender, standalone=res.path is None, escape_msg=False, ) Confirm.set_error_msg(msg=msg_invalid) result = Confirm.ask(prompt=msg_prompt, console=self.console, default=default) self.status.start() return result def end(self, success_count: int, error_count: int): self.status.stop() self.console.print() if success_count == 0 and error_count == 0: self.console.print("[summary.done]Nothing to do[/]") else: msg = ( f"[summary.done]success {success_count}[/] / " f"[summary.fail]fail {error_count}[/]" ) self.console.print(msg) if self.simulate: self.console.print(Panel("SIMULATION", style="simulation")) organize-3.3.0/organize/output/jsonl.py000066400000000000000000000057111472111340300202100ustar00rootroot00000000000000from __future__ import annotations from pathlib import Path from typing import TYPE_CHECKING, Literal, Optional, Union from pydantic import BaseModel from ._sender import sender_name from .output import Level if TYPE_CHECKING: from organize.resource import Resource from ._sender import SenderType class Start(BaseModel): type: Literal["START"] = "START" simulate: bool config_path: Optional[Path] working_dir: Path class Msg(BaseModel): type: Literal["MSG"] = "MSG" level: Level = "info" path: Optional[Path] basedir: Optional[Path] sender: str msg: str rule_nr: int rule_name: str class Confirm(BaseModel): type: Literal["CONFIRM"] = "CONFIRM" path: Optional[Path] basedir: Optional[Path] sender: str msg: str default: bool rule_nr: int rule_name: str class Report(BaseModel): type: Literal["REPORT"] = "REPORT" success_count: int error_count: int EventType = Union[Start, Msg, Confirm, Report] class JSONL: def __init__(self, auto_confirm: bool = False) -> None: self.auto_confirm = auto_confirm def start( self, simulate: bool, config_path: Optional[Path], working_dir: Path, ) -> None: self.emit_event( Start( simulate=simulate, config_path=config_path.resolve() if config_path else None, working_dir=working_dir.resolve(), ) ) def msg( self, res: Resource, msg: str, sender: SenderType, level: Level = "info", ) -> None: self.emit_event( Msg( level=level, path=res.path, basedir=res.basedir, sender=sender_name(sender), msg=msg, rule_nr=res.rule_nr, rule_name=res.rule.name if res.rule and res.rule.name else "", ) ) def confirm( self, res: Resource, msg: str, default: bool, sender: SenderType, ) -> bool: if self.auto_confirm: return True self.emit_event( Confirm( path=res.path, basedir=res.basedir, sender=sender_name(sender), msg=msg, default=default, rule_nr=res.rule_nr, rule_name=res.rule.name if res.rule and res.rule.name else "", ) ) answer = input().lower() if answer == "": return default elif answer in ("j", "y", "ja", "yes", "1"): return True return False def end(self, success_count: int, error_count: int) -> None: report = Report( success_count=success_count, error_count=error_count, ) self.emit_event(report) def emit_event(self, event: EventType) -> None: print(event.model_dump_json()) organize-3.3.0/organize/output/output.py000066400000000000000000000016121472111340300204170ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Literal, Optional, Protocol, runtime_checkable if TYPE_CHECKING: from pathlib import Path from organize.resource import Resource from ._sender import SenderType Level = Literal["info", "warn", "error"] @runtime_checkable class Output(Protocol): """ The protocol all of organize's outputs must adhere to. """ def start( self, simulate: bool, config_path: Optional[Path], working_dir: Path, ) -> None: ... def msg( self, res: Resource, msg: str, sender: SenderType, level: Level = "info", ) -> None: ... def confirm( self, res: Resource, msg: str, default: bool, sender: SenderType, ) -> bool: ... def end(self, success_count: int, error_count: int) -> None: ... organize-3.3.0/organize/output/saving.py000066400000000000000000000021611472111340300203460ustar00rootroot00000000000000from typing import List from .jsonl import JSONL, EventType class SavingOutput(JSONL): """ Saves all the incoming event messages of the latest run. """ def __init__(self) -> None: self.queue: List[EventType] = [] def start(self, *args, **kwargs) -> None: self.queue.clear() super().start(*args, **kwargs) def emit_event(self, event: EventType) -> None: self.queue.append(event) def _messages_of_kind(self, kind: str) -> List: return [x for x in self.queue if x.type == kind] @property def msg_start(self): result = self._messages_of_kind("START") if len(result) != 1: raise ValueError("Multiple start events found") return result[0] @property def msg_msg(self): return self._messages_of_kind("MSG") @property def msg_report(self): result = self._messages_of_kind("REPORT") if len(result) != 1: raise ValueError("Multiple reports found") return result[0] @property def messages(self): return [x.msg for x in self.queue if x.type == "MSG"] organize-3.3.0/organize/py.typed000066400000000000000000000000001472111340300166320ustar00rootroot00000000000000organize-3.3.0/organize/registry.py000066400000000000000000000024121472111340300173660ustar00rootroot00000000000000from __future__ import annotations from typing import Dict, Type from . import actions, filters from .action import Action from .filter import Filter FILTERS: Dict[str, Type[Filter]] = dict() ACTIONS: Dict[str, Type[Action]] = dict() def register_filter(filter: Type[Filter], force: bool = False): name = filter.filter_config.name if not force and name in FILTERS: raise ValueError(f'"{name}" is already registered for filter {FILTERS[name]}') FILTERS[name.lower()] = filter def filter_by_name(name: str) -> Type[Filter]: try: return FILTERS[name.lower()] except KeyError as e: raise ValueError(f'Unknown filter: "{name}"') from e def register_action(action: Type[Action], force: bool = False): name = action.action_config.name if not force and name in ACTIONS: raise ValueError(f'"{name}" is already registered for action {ACTIONS[name]}') ACTIONS[name.lower()] = action def action_by_name(name: str) -> Type[Action]: try: return ACTIONS[name.lower()] except KeyError as e: raise ValueError(f'Unknown action: "{name}"') from e # Register filters and actions for _filter in filters.ALL: register_filter(_filter) # type: ignore for _action in actions.ALL: register_action(_action) organize-3.3.0/organize/resource.py000066400000000000000000000054311472111340300173510ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass, field from pathlib import Path from typing import TYPE_CHECKING, Any, Dict, Optional, Set from organize.utils import deep_merge if TYPE_CHECKING: from .rule import Rule @dataclass class Resource: """ A resource is created for each handled file (or folder) and then passed into the filters and actions pipeline. :param path: The path to the current file or folder :param basedir: The search location as given in rule->location->path :param rule: The rule which is currently executed :param rule_nr: The index of the rule in the config file :param vars: Filters and actions may add values to this dict which are then available for other filters and actions in the pipeline. :param walker_skip_files: Filters and actions may add pathes to this set which are then ignored for the rest of the rule. """ path: Optional[Path] basedir: Optional[Path] = None rule: Optional[Rule] = None # TODO: not optional? rule_nr: int = 0 vars: Dict[str, Any] = field(default_factory=dict) walker_skip_pathes: Set[Path] = field(default_factory=set) def relative_path(self) -> Optional[Path]: if self.basedir is None: return self.path if self.path is None: return None try: return self.path.relative_to(self.basedir) except ValueError: # path is not relative to basedir return None def dict(self): return dict( path=self.path, basedir=self.basedir, location=self.basedir, relative_path=self.relative_path(), rule=self.rule.name if self.rule else None, rule_nr=self.rule_nr, **self.vars, ) def deep_merge(self, key: str, data: Dict) -> None: """ Convenience method for filters / actions to merge data into the `vars` dict. """ prev = self.vars.get(key, dict()) self.vars[key] = deep_merge(prev, data) # TODO: Caching for `is_file` and `is_dir` # TODO: provide a `from_direntry` constructor to speed things up def is_file(self) -> bool: if self.path is None: raise ValueError("No path given") return self.path.is_file() def is_dir(self) -> bool: if self.path is None: raise ValueError("No path given") return self.path.is_dir() def is_empty(self) -> bool: if self.path is None: raise ValueError("No path given") if self.is_file(): return self.path.stat().st_size == 0 elif self.is_dir(): return not any(self.path.iterdir()) raise ValueError("Unknown file type") organize-3.3.0/organize/rule.py000066400000000000000000000236241472111340300164750ustar00rootroot00000000000000from pathlib import Path from typing import Dict, Iterable, List, Literal, Optional, Set from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator from organize.logger import logger from .action import Action from .filter import All, Any, Filter, HasFilterPipeline, Not from .location import Location from .output import Output from .registry import action_by_name, filter_by_name from .resource import Resource from .template import render from .utils import ReportSummary from .validators import FlatList, flatten from .walker import Walker FilterMode = Literal["all", "any", "none"] def action_from_dict(d: Dict) -> Action: """ :param d: A dict in the forms of { "action_name": None } { "action_name": "value" } { "action_name": {"param": "value"} } :returns: An instantiated action. """ if not len(d.keys()) == 1: raise ValueError("Action definition must have only one key") name, value = next(iter(d.items())) ActionCls = action_by_name(name) if value is None: return ActionCls() elif isinstance(value, dict): return ActionCls(**value) else: return ActionCls(value) def filter_from_dict(d: Dict) -> Filter: """ :param d: A dict in the forms of ("not" prefix is optional) { "[not] filter_name": None } { "[not] filter_name": "value" } { "[not] filter_name": {"param": "value"} } :returns: An instantiated filter. """ if not len(d.keys()) == 1: raise ValueError("Filter definition must have a single key") name, value = next(iter(d.items())) # check for "not" in filter key invert_filter = False if name.startswith("not "): name = name[4:] invert_filter = True FilterCls = filter_by_name(name) # instantiate if value is None: inst = FilterCls() elif isinstance(value, dict): inst = FilterCls(**value) else: inst = FilterCls(value) return Not(inst) if invert_filter else inst def filter_pipeline( filters: Iterable[Filter], filter_mode: FilterMode, res: Resource, output: Output, ) -> bool: collection: HasFilterPipeline if filter_mode == "all": collection = All(*filters) elif filter_mode == "any": collection = Any(*filters) elif filter_mode == "none": collection = All(*[Not(x) for x in filters]) else: raise ValueError(f"Unknown filter mode {filter_mode}") return collection.pipeline(res, output=output) def action_pipeline( actions: Iterable[Action], res: Resource, simulate: bool, output: Output, ) -> Iterable[Action]: for action in actions: try: yield action action.pipeline(res=res, simulate=simulate, output=output) except StopIteration: break class Rule(BaseModel): name: Optional[str] = None enabled: bool = True targets: Literal["files", "dirs"] = "files" locations: FlatList[Location] = Field(default_factory=list) subfolders: bool = False tags: Set[str] = Field(default_factory=set) filters: List[Filter] = Field(default_factory=list) filter_mode: FilterMode = "all" actions: List[Action] = Field(..., min_length=1) model_config = ConfigDict( extra="forbid", arbitrary_types_allowed=True, ) @field_validator("locations", mode="before") def validate_locations(cls, locations): if locations is None: return [] locations = flatten(locations) result = [] for x in locations: if isinstance(x, str): x = {"path": x} result.append(x) return result @field_validator("filters", mode="before") def validate_filters(cls, filters): result = [] filters = flatten(filters) for x in filters: # make sure "- extension" becomes "- extension:" if isinstance(x, str): x = {x: None} # create instance from dict if isinstance(x, dict): result.append(filter_from_dict(x)) # other instances else: result.append(x) return result @field_validator("actions", mode="before") def validate_actions(cls, actions): result = [] actions = flatten(actions) for x in actions: # make sure "- extension" becomes "- extension:" if isinstance(x, str): x = {x: None} # create instance from dict if isinstance(x, dict): result.append(action_from_dict(x)) # other instances else: result.append(x) return result @model_validator(mode="after") def validate_target_support(self) -> "Rule": # standalone mode if not self.locations: if self.filters: raise ValueError("Filters are present but no locations are given!") for action in self.actions: if not action.action_config.standalone: raise ValueError( f'Action "{action.action_config.name}" does not support ' "standalone mode (no rule.locations specified)." ) # targets dirs if self.targets == "dirs": for filter in self.filters: if not filter.filter_config.dirs: raise ValueError( f'Filter "{filter.filter_config.name}" does not support ' "folders (targets: dirs)" ) for action in self.actions: if not action.action_config.dirs: raise ValueError( f'Action "{action.action_config.name}" does not support ' "folders (targets: dirs)" ) # targets files elif self.targets == "files": for filter in self.filters: if not filter.filter_config.files: raise ValueError( f'Filter "{filter.filter_config.name}" does not support ' "files (targets: files)" ) for action in self.actions: if not action.action_config.files: raise ValueError( f'Action "{action.action_config.name}" does not support ' "files (targets: files)" ) else: raise ValueError(f"Unknown target: {self.targets}") return self def walk(self, rule_nr: int = 0): for location in self.locations: # instantiate the filesystem walker exclude_files = location.system_exclude_files | location.exclude_files exclude_dirs = location.system_exclude_dirs | location.exclude_dirs if location.max_depth == "inherit": max_depth = None if self.subfolders else 0 else: max_depth = location.max_depth walker = Walker( min_depth=location.min_depth, max_depth=max_depth, filter_dirs=location.filter_dirs, filter_files=location.filter, method="breadth", exclude_dirs=exclude_dirs, exclude_files=exclude_files, ) # whether to walk dirs or files _walk_funcs = { "files": walker.files, "dirs": walker.dirs, } for loc_path in location.path: expanded_path = render(loc_path) for path in _walk_funcs[self.targets](expanded_path): yield Resource( path=Path(path), basedir=Path(expanded_path), rule=self, rule_nr=rule_nr, ) def execute( self, *, simulate: bool, output: Output, rule_nr: int = 0 ) -> ReportSummary: if not self.enabled: return ReportSummary() # standalone mode if not self.locations: res = Resource(path=None, rule_nr=rule_nr) try: for action in action_pipeline( actions=self.actions, res=res, simulate=simulate, output=output, ): pass return ReportSummary(success=1) except Exception as e: output.msg( res=res, msg=str(e), level="error", sender=action, ) logger.exception(e) return ReportSummary(errors=1) # normal mode summary = ReportSummary() skip_pathes: Set[Path] = set() for res in self.walk(rule_nr=rule_nr): if res.path in skip_pathes: continue result = filter_pipeline( filters=self.filters, filter_mode=self.filter_mode, res=res, output=output, ) if result: try: for action in action_pipeline( actions=self.actions, res=res, simulate=simulate, output=output, ): pass skip_pathes = skip_pathes.union(res.walker_skip_pathes) summary.success += 1 except Exception as e: output.msg( res=res, msg=str(e), level="error", sender=action, ) logger.exception(e) summary.errors += 1 return summary organize-3.3.0/organize/template.py000066400000000000000000000025001472111340300173270ustar00rootroot00000000000000import os from datetime import date, datetime from typing import Union import jinja2 # variables that should be always available in a template BASIC_VARS = dict( env=os.environ, now=datetime.now, utcnow=datetime.utcnow, today=date.today, ) def finalize_placeholder(x): # This is used to make the `path` arg available in the filters and actions. # If a template uses `path` where no syspath is available this makes it possible # to raise an exception. if isinstance(x, Exception): raise x return x Template = jinja2.Environment( variable_start_string="{", variable_end_string="}", autoescape=False, finalize=finalize_placeholder, undefined=jinja2.StrictUndefined, ) def render(template: Union[str, jinja2.Template], args=None) -> str: if args is None: args = dict() try: if isinstance(template, jinja2.Template): text = template.render(**args, **BASIC_VARS) else: text = Template.from_string(template).render(**args, **BASIC_VARS) except jinja2.UndefinedError as e: msg = f"Missing value for template: {e}. Maybe you forgot a filter?" raise ValueError(msg) from e # expand user and fill environment vars text = os.path.expanduser(text) text = os.path.expandvars(text) return text organize-3.3.0/organize/utils.py000066400000000000000000000043011472111340300166550ustar00rootroot00000000000000import fnmatch import os import unicodedata from copy import deepcopy from dataclasses import dataclass from pathlib import Path from typing import Any, Literal, Union from rich.markup import escape as rich_escape ENV_ORGANIZE_NORMALIZE_UNICODE = os.environ.get("ORGANIZE_NORMALIZE_UNICODE", "1") def escape(msg: Any) -> str: return rich_escape(str(msg)) def normalize_unicode( text: str, form: Literal["NFC", "NFD", "NFKC", "NFKD"] = "NFC", ) -> str: if ENV_ORGANIZE_NORMALIZE_UNICODE == "1": return unicodedata.normalize(form, text) return text @dataclass class ReportSummary: success: int = 0 errors: int = 0 def __add__(self, other: "ReportSummary") -> "ReportSummary": return ReportSummary( success=self.success + other.success, errors=self.errors + other.errors, ) class ChangeDetector: def __init__(self): self._prev = None self._ready = False def changed(self, value: Any) -> bool: if not self._ready: self._prev = value self._ready = True return True else: changed = value != self._prev self._prev = value return changed def reset(self) -> None: self._ready = False def expandvars(path: Union[str, Path]) -> Path: return Path(os.path.expandvars(path)).expanduser() def glob_match(pattern: str, string: str, *, case_sensitive: bool = False) -> bool: if case_sensitive: return fnmatch.fnmatchcase(string, pattern) return fnmatch.fnmatch(string.lower(), pattern.lower()) def deep_merge(a: dict, b: dict, *, add_keys=True) -> dict: result = deepcopy(a) for bk, bv in b.items(): av = result.get(bk) if isinstance(av, dict) and isinstance(bv, dict): result[bk] = deep_merge(av, bv, add_keys=add_keys) elif (av is not None) or add_keys: result[bk] = deepcopy(bv) return result def deep_merge_inplace(base: dict, updates: dict) -> None: for bk, bv in updates.items(): av = base.get(bk) if isinstance(av, dict) and isinstance(bv, dict): deep_merge_inplace(av, bv) else: base[bk] = bv organize-3.3.0/organize/validators.py000066400000000000000000000012531472111340300176700ustar00rootroot00000000000000from typing import Annotated, Any, Iterable, List, Mapping, Set, TypeVar from pydantic.functional_validators import BeforeValidator def islist(x): return isinstance(x, Iterable) and not isinstance(x, (str, bytes, Mapping)) def _flatten(items): """Yield items from any nested iterable; see Reference.""" for x in items: if islist(x): yield from _flatten(x) else: yield x def flatten(x: Any): if x is None: return [] if not islist(x): x = (x,) return list(_flatten(x)) T = TypeVar("T") FlatList = Annotated[List[T], BeforeValidator(flatten)] FlatSet = Annotated[Set[T], BeforeValidator(flatten)] organize-3.3.0/organize/walker.py000066400000000000000000000121531472111340300170060ustar00rootroot00000000000000import os from fnmatch import fnmatch from pathlib import Path from typing import Iterable, Iterator, List, Literal, NamedTuple, Optional, Set from natsort import os_sorted from pydantic import Field from pydantic.dataclasses import dataclass def pattern_match(name: str, patterns: Iterable[str]) -> bool: return any(fnmatch(name, pat) for pat in patterns) class ScandirResult(NamedTuple): dirs: List[os.DirEntry] nondirs: List[os.DirEntry] def scandir(top: str, collectfiles: bool = True) -> ScandirResult: result = ScandirResult(dirs=[], nondirs=[]) try: # build iterator if we have the permissions to this folder scandir_it = os.scandir(top) except OSError: return result with scandir_it: while True: try: try: entry = next(scandir_it) except StopIteration: break except OSError: return result try: is_symlink = entry.is_symlink() except OSError: # If is_symlink() raises an OSError, consider that the # entry is not a symbolic link, same behaviour than # os.path.islink(). is_symlink = False # As of now, we skip all symlinks. if is_symlink: continue try: is_dir = entry.is_dir() except OSError: # If is_dir() raises an OSError, consider that the entry is not # a directory, same behaviour than os.path.isdir(). is_dir = False if is_dir: result.dirs.append(entry) elif collectfiles: result.nondirs.append(entry) return ScandirResult( dirs=os_sorted(result.dirs, key=lambda x: x.name), nondirs=os_sorted(result.nondirs, key=lambda x: x.name), ) class DirActions(NamedTuple): to_yield: List[os.DirEntry] to_walk: List[os.DirEntry] @dataclass(frozen=True) class Walker: min_depth: int = 0 max_depth: Optional[int] = None method: Literal["breadth", "depth"] = "breadth" filter_dirs: Optional[List[str]] = None filter_files: Optional[List[str]] = None exclude_dirs: Set[str] = Field(default_factory=set) exclude_files: Set[str] = Field(default_factory=set) def _should_yield_file(self, entry: os.DirEntry, lvl: int) -> bool: return ( lvl >= self.min_depth and not pattern_match(entry.name, self.exclude_files) and ( self.filter_files is None or pattern_match(entry.name, self.filter_files) ) ) def _dir_actions(self, entries: Iterable[os.DirEntry], lvl: int) -> DirActions: result = DirActions(to_yield=[], to_walk=[]) for entry in entries: if not pattern_match(entry.name, self.exclude_dirs) and ( self.filter_dirs is None or pattern_match(entry.name, self.filter_dirs) ): if self.max_depth is None or lvl < self.max_depth: result.to_walk.append(entry) if lvl >= self.min_depth: result.to_yield.append(entry) return result def walk( self, top: str, files: bool = True, dirs: bool = True, lvl: int = 0, ) -> Iterator[os.DirEntry]: if not files and not dirs: return # list all dirs and nondirs of the folder result = scandir(top, collectfiles=files) if self.method == "breadth": # Return entries for entry in result.nondirs: if files and self._should_yield_file(entry=entry, lvl=lvl): yield entry dir_actions = self._dir_actions(result.dirs, lvl=lvl) if dirs: yield from dir_actions.to_yield # Recurse into sub-directories for entry in dir_actions.to_walk: yield from self.walk(entry.path, files=files, dirs=dirs, lvl=lvl + 1) elif self.method == "depth": dir_actions = self._dir_actions(result.dirs, lvl=lvl) # Recurse into sub-directories for entry in dir_actions.to_walk: yield from self.walk(entry.path, files=files, dirs=dirs, lvl=lvl + 1) # Return entries for entry in result.nondirs: if files and self._should_yield_file(entry=entry, lvl=lvl): yield entry if dirs: yield from dir_actions.to_yield else: raise ValueError(f'Unknown method "{self.method}"') def files(self, path: str) -> Iterator[Path]: # if path is a single file we emit just the path itself if os.path.isfile(path): yield Path(path) return # otherwise we walk the given folder for entry in self.walk(path, files=True, dirs=False): yield Path(entry.path) def dirs(self, path: str) -> Iterator[Path]: for entry in self.walk(path, files=False, dirs=True): yield Path(entry.path) organize-3.3.0/poetry.lock000066400000000000000000004156431472111340300155400ustar00rootroot00000000000000# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. [[package]] name = "annotated-types" version = "0.7.0" description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, ] [[package]] name = "arrow" version = "1.3.0" description = "Better dates & times for Python" optional = false python-versions = ">=3.8" files = [ {file = "arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80"}, {file = "arrow-1.3.0.tar.gz", hash = "sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85"}, ] [package.dependencies] python-dateutil = ">=2.7.0" types-python-dateutil = ">=2.8.10" [package.extras] doc = ["doc8", "sphinx (>=7.0.0)", "sphinx-autobuild", "sphinx-autodoc-typehints", "sphinx_rtd_theme (>=1.3.0)"] test = ["dateparser (==1.*)", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "pytz (==2021.1)", "simplejson (==3.*)"] [[package]] name = "bracex" version = "2.5.post1" description = "Bash style brace expander." optional = true python-versions = ">=3.8" files = [ {file = "bracex-2.5.post1-py3-none-any.whl", hash = "sha256:13e5732fec27828d6af308628285ad358047cec36801598368cb28bc631dbaf6"}, {file = "bracex-2.5.post1.tar.gz", hash = "sha256:12c50952415bfa773d2d9ccb8e79651b8cdb1f31a42f6091b804f6ba2b4a66b6"}, ] [[package]] name = "certifi" version = "2024.8.30" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, ] [[package]] name = "cffi" version = "1.17.1" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" files = [ {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, ] [package.dependencies] pycparser = "*" [[package]] name = "charset-normalizer" version = "3.4.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7.0" files = [ {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, ] [[package]] name = "click" version = "8.1.7" description = "Composable command line interface toolkit" optional = true python-versions = ">=3.7" files = [ {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, ] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] [[package]] name = "coverage" version = "7.6.8" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" files = [ {file = "coverage-7.6.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b39e6011cd06822eb964d038d5dff5da5d98652b81f5ecd439277b32361a3a50"}, {file = "coverage-7.6.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:63c19702db10ad79151a059d2d6336fe0c470f2e18d0d4d1a57f7f9713875dcf"}, {file = "coverage-7.6.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3985b9be361d8fb6b2d1adc9924d01dec575a1d7453a14cccd73225cb79243ee"}, {file = "coverage-7.6.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:644ec81edec0f4ad17d51c838a7d01e42811054543b76d4ba2c5d6af741ce2a6"}, {file = "coverage-7.6.8-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f188a2402f8359cf0c4b1fe89eea40dc13b52e7b4fd4812450da9fcd210181d"}, {file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e19122296822deafce89a0c5e8685704c067ae65d45e79718c92df7b3ec3d331"}, {file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:13618bed0c38acc418896005732e565b317aa9e98d855a0e9f211a7ffc2d6638"}, {file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:193e3bffca48ad74b8c764fb4492dd875038a2f9925530cb094db92bb5e47bed"}, {file = "coverage-7.6.8-cp310-cp310-win32.whl", hash = "sha256:3988665ee376abce49613701336544041f2117de7b7fbfe91b93d8ff8b151c8e"}, {file = "coverage-7.6.8-cp310-cp310-win_amd64.whl", hash = "sha256:f56f49b2553d7dd85fd86e029515a221e5c1f8cb3d9c38b470bc38bde7b8445a"}, {file = "coverage-7.6.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:86cffe9c6dfcfe22e28027069725c7f57f4b868a3f86e81d1c62462764dc46d4"}, {file = "coverage-7.6.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d82ab6816c3277dc962cfcdc85b1efa0e5f50fb2c449432deaf2398a2928ab94"}, {file = "coverage-7.6.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13690e923a3932e4fad4c0ebfb9cb5988e03d9dcb4c5150b5fcbf58fd8bddfc4"}, {file = "coverage-7.6.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4be32da0c3827ac9132bb488d331cb32e8d9638dd41a0557c5569d57cf22c9c1"}, {file = "coverage-7.6.8-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44e6c85bbdc809383b509d732b06419fb4544dca29ebe18480379633623baafb"}, {file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:768939f7c4353c0fac2f7c37897e10b1414b571fd85dd9fc49e6a87e37a2e0d8"}, {file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e44961e36cb13c495806d4cac67640ac2866cb99044e210895b506c26ee63d3a"}, {file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ea8bb1ab9558374c0ab591783808511d135a833c3ca64a18ec927f20c4030f0"}, {file = "coverage-7.6.8-cp311-cp311-win32.whl", hash = "sha256:629a1ba2115dce8bf75a5cce9f2486ae483cb89c0145795603d6554bdc83e801"}, {file = "coverage-7.6.8-cp311-cp311-win_amd64.whl", hash = "sha256:fb9fc32399dca861584d96eccd6c980b69bbcd7c228d06fb74fe53e007aa8ef9"}, {file = "coverage-7.6.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e683e6ecc587643f8cde8f5da6768e9d165cd31edf39ee90ed7034f9ca0eefee"}, {file = "coverage-7.6.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1defe91d41ce1bd44b40fabf071e6a01a5aa14de4a31b986aa9dfd1b3e3e414a"}, {file = "coverage-7.6.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7ad66e8e50225ebf4236368cc43c37f59d5e6728f15f6e258c8639fa0dd8e6d"}, {file = "coverage-7.6.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fe47da3e4fda5f1abb5709c156eca207eacf8007304ce3019eb001e7a7204cb"}, {file = "coverage-7.6.8-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:202a2d645c5a46b84992f55b0a3affe4f0ba6b4c611abec32ee88358db4bb649"}, {file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4674f0daa1823c295845b6a740d98a840d7a1c11df00d1fd62614545c1583787"}, {file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:74610105ebd6f33d7c10f8907afed696e79c59e3043c5f20eaa3a46fddf33b4c"}, {file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37cda8712145917105e07aab96388ae76e787270ec04bcb9d5cc786d7cbb8443"}, {file = "coverage-7.6.8-cp312-cp312-win32.whl", hash = "sha256:9e89d5c8509fbd6c03d0dd1972925b22f50db0792ce06324ba069f10787429ad"}, {file = "coverage-7.6.8-cp312-cp312-win_amd64.whl", hash = "sha256:379c111d3558272a2cae3d8e57e6b6e6f4fe652905692d54bad5ea0ca37c5ad4"}, {file = "coverage-7.6.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b0c69f4f724c64dfbfe79f5dfb503b42fe6127b8d479b2677f2b227478db2eb"}, {file = "coverage-7.6.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c15b32a7aca8038ed7644f854bf17b663bc38e1671b5d6f43f9a2b2bd0c46f63"}, {file = "coverage-7.6.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63068a11171e4276f6ece913bde059e77c713b48c3a848814a6537f35afb8365"}, {file = "coverage-7.6.8-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f4548c5ead23ad13fb7a2c8ea541357474ec13c2b736feb02e19a3085fac002"}, {file = "coverage-7.6.8-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b4b4299dd0d2c67caaaf286d58aef5e75b125b95615dda4542561a5a566a1e3"}, {file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9ebfb2507751f7196995142f057d1324afdab56db1d9743aab7f50289abd022"}, {file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c1b4474beee02ede1eef86c25ad4600a424fe36cff01a6103cb4533c6bf0169e"}, {file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d9fd2547e6decdbf985d579cf3fc78e4c1d662b9b0ff7cc7862baaab71c9cc5b"}, {file = "coverage-7.6.8-cp313-cp313-win32.whl", hash = "sha256:8aae5aea53cbfe024919715eca696b1a3201886ce83790537d1c3668459c7146"}, {file = "coverage-7.6.8-cp313-cp313-win_amd64.whl", hash = "sha256:ae270e79f7e169ccfe23284ff5ea2d52a6f401dc01b337efb54b3783e2ce3f28"}, {file = "coverage-7.6.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de38add67a0af869b0d79c525d3e4588ac1ffa92f39116dbe0ed9753f26eba7d"}, {file = "coverage-7.6.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b07c25d52b1c16ce5de088046cd2432b30f9ad5e224ff17c8f496d9cb7d1d451"}, {file = "coverage-7.6.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62a66ff235e4c2e37ed3b6104d8b478d767ff73838d1222132a7a026aa548764"}, {file = "coverage-7.6.8-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09b9f848b28081e7b975a3626e9081574a7b9196cde26604540582da60235fdf"}, {file = "coverage-7.6.8-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:093896e530c38c8e9c996901858ac63f3d4171268db2c9c8b373a228f459bbc5"}, {file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a7b8ac36fd688c8361cbc7bf1cb5866977ece6e0b17c34aa0df58bda4fa18a4"}, {file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:38c51297b35b3ed91670e1e4efb702b790002e3245a28c76e627478aa3c10d83"}, {file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2e4e0f60cb4bd7396108823548e82fdab72d4d8a65e58e2c19bbbc2f1e2bfa4b"}, {file = "coverage-7.6.8-cp313-cp313t-win32.whl", hash = "sha256:6535d996f6537ecb298b4e287a855f37deaf64ff007162ec0afb9ab8ba3b8b71"}, {file = "coverage-7.6.8-cp313-cp313t-win_amd64.whl", hash = "sha256:c79c0685f142ca53256722a384540832420dff4ab15fec1863d7e5bc8691bdcc"}, {file = "coverage-7.6.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ac47fa29d8d41059ea3df65bd3ade92f97ee4910ed638e87075b8e8ce69599e"}, {file = "coverage-7.6.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:24eda3a24a38157eee639ca9afe45eefa8d2420d49468819ac5f88b10de84f4c"}, {file = "coverage-7.6.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4c81ed2820b9023a9a90717020315e63b17b18c274a332e3b6437d7ff70abe0"}, {file = "coverage-7.6.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd55f8fc8fa494958772a2a7302b0354ab16e0b9272b3c3d83cdb5bec5bd1779"}, {file = "coverage-7.6.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f39e2f3530ed1626c66e7493be7a8423b023ca852aacdc91fb30162c350d2a92"}, {file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:716a78a342679cd1177bc8c2fe957e0ab91405bd43a17094324845200b2fddf4"}, {file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:177f01eeaa3aee4a5ffb0d1439c5952b53d5010f86e9d2667963e632e30082cc"}, {file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:912e95017ff51dc3d7b6e2be158dedc889d9a5cc3382445589ce554f1a34c0ea"}, {file = "coverage-7.6.8-cp39-cp39-win32.whl", hash = "sha256:4db3ed6a907b555e57cc2e6f14dc3a4c2458cdad8919e40b5357ab9b6db6c43e"}, {file = "coverage-7.6.8-cp39-cp39-win_amd64.whl", hash = "sha256:428ac484592f780e8cd7b6b14eb568f7c85460c92e2a37cb0c0e5186e1a0d076"}, {file = "coverage-7.6.8-pp39.pp310-none-any.whl", hash = "sha256:5c52a036535d12590c32c49209e79cabaad9f9ad8aa4cbd875b68c4d67a9cbce"}, {file = "coverage-7.6.8.tar.gz", hash = "sha256:8b2b8503edb06822c86d82fa64a4a5cb0760bb8f31f26e138ec743f422f37cfc"}, ] [package.dependencies] tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} [package.extras] toml = ["tomli"] [[package]] name = "cryptography" version = "43.0.3" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" files = [ {file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"}, {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"}, {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f"}, {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6"}, {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18"}, {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd"}, {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73"}, {file = "cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2"}, {file = "cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd"}, {file = "cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984"}, {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5"}, {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4"}, {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7"}, {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405"}, {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16"}, {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73"}, {file = "cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995"}, {file = "cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362"}, {file = "cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c"}, {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3"}, {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83"}, {file = "cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7"}, {file = "cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664"}, {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08"}, {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa"}, {file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"}, {file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"}, ] [package.dependencies] cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} [package.extras] docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] nox = ["nox"] pep8test = ["check-sdist", "click", "mypy", "ruff"] sdist = ["build"] ssh = ["bcrypt (>=3.1.5)"] test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] test-randomorder = ["pytest-randomly"] [[package]] name = "docopt-ng" version = "0.9.0" description = "Jazzband-maintained fork of docopt, the humane command line arguments parser." optional = false python-versions = ">=3.7" files = [ {file = "docopt_ng-0.9.0-py3-none-any.whl", hash = "sha256:bfe4c8b03f9fca424c24ee0b4ffa84bf7391cb18c29ce0f6a8227a3b01b81ff9"}, {file = "docopt_ng-0.9.0.tar.gz", hash = "sha256:91c6da10b5bb6f2e9e25345829fb8278c78af019f6fc40887ad49b060483b1d7"}, ] [[package]] name = "docx2txt" version = "0.8" description = "A pure python-based utility to extract text and images from docx files." optional = false python-versions = "*" files = [ {file = "docx2txt-0.8.tar.gz", hash = "sha256:2c06d98d7cfe2d3947e5760a57d924e3ff07745b379c8737723922e7009236e5"}, ] [[package]] name = "exceptiongroup" version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, ] [package.extras] test = ["pytest (>=6)"] [[package]] name = "exifread" version = "2.3.2" description = "Read Exif metadata from tiff and jpeg files." optional = false python-versions = "*" files = [ {file = "ExifRead-2.3.2-py3-none-any.whl", hash = "sha256:3ef8725efdb66530b4b3cd1c4ba5d3f3b35a7872137d2c707f711971f8ebf809"}, {file = "ExifRead-2.3.2.tar.gz", hash = "sha256:a0f74af5040168d3883bbc980efe26d06c89f026dc86ba28eb34107662d51766"}, ] [[package]] name = "ghp-import" version = "2.1.0" description = "Copy your docs directly to the gh-pages branch." optional = true python-versions = "*" files = [ {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, ] [package.dependencies] python-dateutil = ">=2.8.1" [package.extras] dev = ["flake8", "markdown", "twine", "wheel"] [[package]] name = "griffe" version = "1.5.1" description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." optional = true python-versions = ">=3.9" files = [ {file = "griffe-1.5.1-py3-none-any.whl", hash = "sha256:ad6a7980f8c424c9102160aafa3bcdf799df0e75f7829d75af9ee5aef656f860"}, {file = "griffe-1.5.1.tar.gz", hash = "sha256:72964f93e08c553257706d6cd2c42d1c172213feb48b2be386f243380b405d4b"}, ] [package.dependencies] colorama = ">=0.4" [[package]] name = "idna" version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, ] [package.extras] all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] [[package]] name = "importlib-metadata" version = "8.5.0" description = "Read metadata from Python packages" optional = true python-versions = ">=3.8" files = [ {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, ] [package.dependencies] zipp = ">=3.20" [package.extras] check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] perf = ["ipython"] test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] type = ["pytest-mypy"] [[package]] name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] [[package]] name = "jinja2" version = "3.1.4" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" files = [ {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, ] [package.dependencies] MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] [[package]] name = "macos-tags" version = "1.5.1" description = "Use tags to organize files on Mac from Python" optional = false python-versions = ">=3.6,<4.0" files = [ {file = "macos-tags-1.5.1.tar.gz", hash = "sha256:f144c5bc05d01573966d8aca2483cb345b20b76a5b32e9967786e086a38712e7"}, {file = "macos_tags-1.5.1-py3-none-any.whl", hash = "sha256:56419233af32242b703dd35bcf38c9f198abd969faddbe986eb8aaa6d95349cf"}, ] [package.dependencies] mdfind-wrapper = ">=0.1.3,<0.2.0" xattr = ">=0.9.7,<0.10.0" [[package]] name = "markdown" version = "3.7" description = "Python implementation of John Gruber's Markdown." optional = true python-versions = ">=3.8" files = [ {file = "Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803"}, {file = "markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2"}, ] [package.dependencies] importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} [package.extras] docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] testing = ["coverage", "pyyaml"] [[package]] name = "markdown-it-py" version = "3.0.0" description = "Python port of markdown-it. Markdown parsing, done right!" optional = false python-versions = ">=3.8" files = [ {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, ] [package.dependencies] mdurl = ">=0.1,<1.0" [package.extras] benchmarking = ["psutil", "pytest", "pytest-benchmark"] code-style = ["pre-commit (>=3.0,<4.0)"] compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] linkify = ["linkify-it-py (>=1,<3)"] plugins = ["mdit-py-plugins"] profiling = ["gprof2dot"] rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [[package]] name = "markupsafe" version = "2.0.1" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.6" files = [ {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b"}, {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a"}, {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a"}, {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f"}, {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194"}, {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047"}, {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e"}, {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, ] [[package]] name = "mdfind-wrapper" version = "0.1.5" description = "A python library that wraps the mdfind." optional = false python-versions = ">=3.6" files = [ {file = "mdfind-wrapper-0.1.5.tar.gz", hash = "sha256:c0dbd5bc99c6d1fb4678bfa1841a3380ccac61e9b43a26a8d658aa9cafe27441"}, {file = "mdfind_wrapper-0.1.5-py3-none-any.whl", hash = "sha256:fd00e65684b47f2d286eb7394eb172f4766f2926d95eddff6eb948352f620cbc"}, ] [[package]] name = "mdurl" version = "0.1.2" description = "Markdown URL utilities" optional = false python-versions = ">=3.7" files = [ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] [[package]] name = "mergedeep" version = "1.3.4" description = "A deep merge function for 🐍." optional = true python-versions = ">=3.6" files = [ {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, ] [[package]] name = "mkdocs" version = "1.6.1" description = "Project documentation with Markdown." optional = true python-versions = ">=3.8" files = [ {file = "mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e"}, {file = "mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2"}, ] [package.dependencies] click = ">=7.0" colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} ghp-import = ">=1.0" importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} jinja2 = ">=2.11.1" markdown = ">=3.3.6" markupsafe = ">=2.0.1" mergedeep = ">=1.3.4" mkdocs-get-deps = ">=0.2.0" packaging = ">=20.5" pathspec = ">=0.11.1" pyyaml = ">=5.1" pyyaml-env-tag = ">=0.1" watchdog = ">=2.0" [package.extras] i18n = ["babel (>=2.9.0)"] min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.4)", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"] [[package]] name = "mkdocs-autorefs" version = "0.5.0" description = "Automatically link across pages in MkDocs." optional = true python-versions = ">=3.8" files = [ {file = "mkdocs_autorefs-0.5.0-py3-none-any.whl", hash = "sha256:7930fcb8ac1249f10e683967aeaddc0af49d90702af111a5e390e8b20b3d97ff"}, {file = "mkdocs_autorefs-0.5.0.tar.gz", hash = "sha256:9a5054a94c08d28855cfab967ada10ed5be76e2bfad642302a610b252c3274c0"}, ] [package.dependencies] Markdown = ">=3.3" mkdocs = ">=1.1" [[package]] name = "mkdocs-get-deps" version = "0.2.0" description = "MkDocs extension that lists all dependencies according to a mkdocs.yml file" optional = true python-versions = ">=3.8" files = [ {file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"}, {file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"}, ] [package.dependencies] importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""} mergedeep = ">=1.3.4" platformdirs = ">=2.2.0" pyyaml = ">=5.1" [[package]] name = "mkdocs-include-markdown-plugin" version = "6.2.2" description = "Mkdocs Markdown includer plugin." optional = true python-versions = ">=3.8" files = [ {file = "mkdocs_include_markdown_plugin-6.2.2-py3-none-any.whl", hash = "sha256:d293950f6499d2944291ca7b9bc4a60e652bbfd3e3a42b564f6cceee268694e7"}, {file = "mkdocs_include_markdown_plugin-6.2.2.tar.gz", hash = "sha256:f2bd5026650492a581d2fd44be6c22f90391910d76582b96a34c264f2d17875d"}, ] [package.dependencies] mkdocs = ">=1.4" wcmatch = "*" [package.extras] cache = ["platformdirs"] [[package]] name = "mkdocstrings" version = "0.24.3" description = "Automatic documentation from sources, for MkDocs." optional = true python-versions = ">=3.8" files = [ {file = "mkdocstrings-0.24.3-py3-none-any.whl", hash = "sha256:5c9cf2a32958cd161d5428699b79c8b0988856b0d4a8c5baf8395fc1bf4087c3"}, {file = "mkdocstrings-0.24.3.tar.gz", hash = "sha256:f327b234eb8d2551a306735436e157d0a22d45f79963c60a8b585d5f7a94c1d2"}, ] [package.dependencies] click = ">=7.0" importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""} Jinja2 = ">=2.11.1" Markdown = ">=3.3" MarkupSafe = ">=1.1" mkdocs = ">=1.4" mkdocs-autorefs = ">=0.3.1" mkdocstrings-python = {version = ">=0.5.2", optional = true, markers = "extra == \"python\""} platformdirs = ">=2.2.0" pymdown-extensions = ">=6.3" typing-extensions = {version = ">=4.1", markers = "python_version < \"3.10\""} [package.extras] crystal = ["mkdocstrings-crystal (>=0.3.4)"] python = ["mkdocstrings-python (>=0.5.2)"] python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] [[package]] name = "mkdocstrings-python" version = "1.10.0" description = "A Python handler for mkdocstrings." optional = true python-versions = ">=3.8" files = [ {file = "mkdocstrings_python-1.10.0-py3-none-any.whl", hash = "sha256:ba833fbd9d178a4b9d5cb2553a4df06e51dc1f51e41559a4d2398c16a6f69ecc"}, {file = "mkdocstrings_python-1.10.0.tar.gz", hash = "sha256:71678fac657d4d2bb301eed4e4d2d91499c095fd1f8a90fa76422a87a5693828"}, ] [package.dependencies] griffe = ">=0.44" mkdocstrings = ">=0.24.2" [[package]] name = "mypy" version = "1.13.0" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"}, {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"}, {file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"}, {file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"}, {file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"}, {file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"}, {file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"}, {file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"}, {file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"}, {file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"}, {file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"}, {file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"}, {file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"}, {file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"}, {file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"}, {file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"}, {file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"}, {file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"}, {file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"}, {file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"}, {file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"}, {file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"}, {file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"}, {file = "mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"}, {file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"}, {file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"}, {file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"}, {file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"}, {file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"}, {file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"}, {file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"}, {file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"}, ] [package.dependencies] mypy-extensions = ">=1.0.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} typing-extensions = ">=4.6.0" [package.extras] dmypy = ["psutil (>=4.0)"] faster-cache = ["orjson"] install-types = ["pip"] mypyc = ["setuptools (>=50)"] reports = ["lxml"] [[package]] name = "mypy-extensions" version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.5" files = [ {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] [[package]] name = "natsort" version = "8.4.0" description = "Simple yet flexible natural sorting in Python." optional = false python-versions = ">=3.7" files = [ {file = "natsort-8.4.0-py3-none-any.whl", hash = "sha256:4732914fb471f56b5cce04d7bae6f164a592c7712e1c85f9ef585e197299521c"}, {file = "natsort-8.4.0.tar.gz", hash = "sha256:45312c4a0e5507593da193dedd04abb1469253b601ecaf63445ad80f0a1ea581"}, ] [package.extras] fast = ["fastnumbers (>=2.0.0)"] icu = ["PyICU (>=1.0.0)"] [[package]] name = "packaging" version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" files = [ {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] [[package]] name = "pathspec" version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = true python-versions = ">=3.8" files = [ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, ] [[package]] name = "pdfminer-six" version = "20240706" description = "PDF parser and analyzer" optional = false python-versions = ">=3.8" files = [ {file = "pdfminer.six-20240706-py3-none-any.whl", hash = "sha256:f4f70e74174b4b3542fcb8406a210b6e2e27cd0f0b5fd04534a8cc0d8951e38c"}, {file = "pdfminer.six-20240706.tar.gz", hash = "sha256:c631a46d5da957a9ffe4460c5dce21e8431dabb615fee5f9f4400603a58d95a6"}, ] [package.dependencies] charset-normalizer = ">=2.0.0" cryptography = ">=36.0.0" [package.extras] dev = ["atheris", "black", "mypy (==0.931)", "nox", "pytest"] docs = ["sphinx", "sphinx-argparse"] image = ["Pillow"] [[package]] name = "platformdirs" version = "4.3.6" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" files = [ {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, ] [package.extras] docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] type = ["mypy (>=1.11.2)"] [[package]] name = "pluggy" version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, ] [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] [[package]] name = "pycparser" version = "2.22" description = "C parser in Python" optional = false python-versions = ">=3.8" files = [ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] [[package]] name = "pydantic" version = "2.10.1" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" files = [ {file = "pydantic-2.10.1-py3-none-any.whl", hash = "sha256:a8d20db84de64cf4a7d59e899c2caf0fe9d660c7cfc482528e7020d7dd189a7e"}, {file = "pydantic-2.10.1.tar.gz", hash = "sha256:a4daca2dc0aa429555e0656d6bf94873a7dc5f54ee42b1f5873d666fb3f35560"}, ] [package.dependencies] annotated-types = ">=0.6.0" pydantic-core = "2.27.1" typing-extensions = ">=4.12.2" [package.extras] email = ["email-validator (>=2.0.0)"] timezone = ["tzdata"] [[package]] name = "pydantic-core" version = "2.27.1" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" files = [ {file = "pydantic_core-2.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a"}, {file = "pydantic_core-2.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b"}, {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:121ceb0e822f79163dd4699e4c54f5ad38b157084d97b34de8b232bcaad70278"}, {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4603137322c18eaf2e06a4495f426aa8d8388940f3c457e7548145011bb68e05"}, {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a33cd6ad9017bbeaa9ed78a2e0752c5e250eafb9534f308e7a5f7849b0b1bfb4"}, {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15cc53a3179ba0fcefe1e3ae50beb2784dede4003ad2dfd24f81bba4b23a454f"}, {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45d9c5eb9273aa50999ad6adc6be5e0ecea7e09dbd0d31bd0c65a55a2592ca08"}, {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8bf7b66ce12a2ac52d16f776b31d16d91033150266eb796967a7e4621707e4f6"}, {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:655d7dd86f26cb15ce8a431036f66ce0318648f8853d709b4167786ec2fa4807"}, {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:5556470f1a2157031e676f776c2bc20acd34c1990ca5f7e56f1ebf938b9ab57c"}, {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f69ed81ab24d5a3bd93861c8c4436f54afdf8e8cc421562b0c7504cf3be58206"}, {file = "pydantic_core-2.27.1-cp310-none-win32.whl", hash = "sha256:f5a823165e6d04ccea61a9f0576f345f8ce40ed533013580e087bd4d7442b52c"}, {file = "pydantic_core-2.27.1-cp310-none-win_amd64.whl", hash = "sha256:57866a76e0b3823e0b56692d1a0bf722bffb324839bb5b7226a7dbd6c9a40b17"}, {file = "pydantic_core-2.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8"}, {file = "pydantic_core-2.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330"}, {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52"}, {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4"}, {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c"}, {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de"}, {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025"}, {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e"}, {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919"}, {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c"}, {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc"}, {file = "pydantic_core-2.27.1-cp311-none-win32.whl", hash = "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9"}, {file = "pydantic_core-2.27.1-cp311-none-win_amd64.whl", hash = "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5"}, {file = "pydantic_core-2.27.1-cp311-none-win_arm64.whl", hash = "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89"}, {file = "pydantic_core-2.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f"}, {file = "pydantic_core-2.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02"}, {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c"}, {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac"}, {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb"}, {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529"}, {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35"}, {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089"}, {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381"}, {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb"}, {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae"}, {file = "pydantic_core-2.27.1-cp312-none-win32.whl", hash = "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c"}, {file = "pydantic_core-2.27.1-cp312-none-win_amd64.whl", hash = "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16"}, {file = "pydantic_core-2.27.1-cp312-none-win_arm64.whl", hash = "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e"}, {file = "pydantic_core-2.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073"}, {file = "pydantic_core-2.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08"}, {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf"}, {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737"}, {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2"}, {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107"}, {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51"}, {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a"}, {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc"}, {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960"}, {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23"}, {file = "pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05"}, {file = "pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337"}, {file = "pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5"}, {file = "pydantic_core-2.27.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:5897bec80a09b4084aee23f9b73a9477a46c3304ad1d2d07acca19723fb1de62"}, {file = "pydantic_core-2.27.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d0165ab2914379bd56908c02294ed8405c252250668ebcb438a55494c69f44ab"}, {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b9af86e1d8e4cfc82c2022bfaa6f459381a50b94a29e95dcdda8442d6d83864"}, {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f6c8a66741c5f5447e047ab0ba7a1c61d1e95580d64bce852e3df1f895c4067"}, {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a42d6a8156ff78981f8aa56eb6394114e0dedb217cf8b729f438f643608cbcd"}, {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64c65f40b4cd8b0e049a8edde07e38b476da7e3aaebe63287c899d2cff253fa5"}, {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdcf339322a3fae5cbd504edcefddd5a50d9ee00d968696846f089b4432cf78"}, {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bf99c8404f008750c846cb4ac4667b798a9f7de673ff719d705d9b2d6de49c5f"}, {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8f1edcea27918d748c7e5e4d917297b2a0ab80cad10f86631e488b7cddf76a36"}, {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:159cac0a3d096f79ab6a44d77a961917219707e2a130739c64d4dd46281f5c2a"}, {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:029d9757eb621cc6e1848fa0b0310310de7301057f623985698ed7ebb014391b"}, {file = "pydantic_core-2.27.1-cp38-none-win32.whl", hash = "sha256:a28af0695a45f7060e6f9b7092558a928a28553366519f64083c63a44f70e618"}, {file = "pydantic_core-2.27.1-cp38-none-win_amd64.whl", hash = "sha256:2d4567c850905d5eaaed2f7a404e61012a51caf288292e016360aa2b96ff38d4"}, {file = "pydantic_core-2.27.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:e9386266798d64eeb19dd3677051f5705bf873e98e15897ddb7d76f477131967"}, {file = "pydantic_core-2.27.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4228b5b646caa73f119b1ae756216b59cc6e2267201c27d3912b592c5e323b60"}, {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3dfe500de26c52abe0477dde16192ac39c98f05bf2d80e76102d394bd13854"}, {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aee66be87825cdf72ac64cb03ad4c15ffef4143dbf5c113f64a5ff4f81477bf9"}, {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b748c44bb9f53031c8cbc99a8a061bc181c1000c60a30f55393b6e9c45cc5bd"}, {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ca038c7f6a0afd0b2448941b6ef9d5e1949e999f9e5517692eb6da58e9d44be"}, {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e0bd57539da59a3e4671b90a502da9a28c72322a4f17866ba3ac63a82c4498e"}, {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac6c2c45c847bbf8f91930d88716a0fb924b51e0c6dad329b793d670ec5db792"}, {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b94d4ba43739bbe8b0ce4262bcc3b7b9f31459ad120fb595627eaeb7f9b9ca01"}, {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:00e6424f4b26fe82d44577b4c842d7df97c20be6439e8e685d0d715feceb9fb9"}, {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:38de0a70160dd97540335b7ad3a74571b24f1dc3ed33f815f0880682e6880131"}, {file = "pydantic_core-2.27.1-cp39-none-win32.whl", hash = "sha256:7ccebf51efc61634f6c2344da73e366c75e735960b5654b63d7e6f69a5885fa3"}, {file = "pydantic_core-2.27.1-cp39-none-win_amd64.whl", hash = "sha256:a57847b090d7892f123726202b7daa20df6694cbd583b67a592e856bff603d6c"}, {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3fa80ac2bd5856580e242dbc202db873c60a01b20309c8319b5c5986fbe53ce6"}, {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d950caa237bb1954f1b8c9227b5065ba6875ac9771bb8ec790d956a699b78676"}, {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e4216e64d203e39c62df627aa882f02a2438d18a5f21d7f721621f7a5d3611d"}, {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02a3d637bd387c41d46b002f0e49c52642281edacd2740e5a42f7017feea3f2c"}, {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:161c27ccce13b6b0c8689418da3885d3220ed2eae2ea5e9b2f7f3d48f1d52c27"}, {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:19910754e4cc9c63bc1c7f6d73aa1cfee82f42007e407c0f413695c2f7ed777f"}, {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e173486019cc283dc9778315fa29a363579372fe67045e971e89b6365cc035ed"}, {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:af52d26579b308921b73b956153066481f064875140ccd1dfd4e77db89dbb12f"}, {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:981fb88516bd1ae8b0cbbd2034678a39dedc98752f264ac9bc5839d3923fa04c"}, {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5fde892e6c697ce3e30c61b239330fc5d569a71fefd4eb6512fc6caec9dd9e2f"}, {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:816f5aa087094099fff7edabb5e01cc370eb21aa1a1d44fe2d2aefdfb5599b31"}, {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c10c309e18e443ddb108f0ef64e8729363adbfd92d6d57beec680f6261556f3"}, {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98476c98b02c8e9b2eec76ac4156fd006628b1b2d0ef27e548ffa978393fd154"}, {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3027001c28434e7ca5a6e1e527487051136aa81803ac812be51802150d880dd"}, {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:7699b1df36a48169cdebda7ab5a2bac265204003f153b4bd17276153d997670a"}, {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1c39b07d90be6b48968ddc8c19e7585052088fd7ec8d568bb31ff64c70ae3c97"}, {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:46ccfe3032b3915586e469d4972973f893c0a2bb65669194a5bdea9bacc088c2"}, {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:62ba45e21cf6571d7f716d903b5b7b6d2617e2d5d67c0923dc47b9d41369f840"}, {file = "pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235"}, ] [package.dependencies] typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pyfakefs" version = "5.7.1" description = "pyfakefs implements a fake file system that mocks the Python file system modules." optional = false python-versions = ">=3.7" files = [ {file = "pyfakefs-5.7.1-py3-none-any.whl", hash = "sha256:6503ffe7f401701cf974b502311f926da2b0657a72244a6ba36e985ceb3dd783"}, {file = "pyfakefs-5.7.1.tar.gz", hash = "sha256:24774c632f3b67ea26fd56b08115ba7c339d5cd65655410bca8572d73a1ae9a4"}, ] [[package]] name = "pygments" version = "2.18.0" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" files = [ {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, ] [package.extras] windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pymdown-extensions" version = "10.12" description = "Extension pack for Python Markdown." optional = true python-versions = ">=3.8" files = [ {file = "pymdown_extensions-10.12-py3-none-any.whl", hash = "sha256:49f81412242d3527b8b4967b990df395c89563043bc51a3d2d7d500e52123b77"}, {file = "pymdown_extensions-10.12.tar.gz", hash = "sha256:b0ee1e0b2bef1071a47891ab17003bfe5bf824a398e13f49f8ed653b699369a7"}, ] [package.dependencies] markdown = ">=3.6" pyyaml = "*" [package.extras] extra = ["pygments (>=2.12)"] [[package]] name = "pyobjc-core" version = "10.3.1" description = "Python<->ObjC Interoperability Module" optional = false python-versions = ">=3.8" files = [ {file = "pyobjc_core-10.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ea46d2cda17921e417085ac6286d43ae448113158afcf39e0abe484c58fb3d78"}, {file = "pyobjc_core-10.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:899d3c84d2933d292c808f385dc881a140cf08632907845043a333a9d7c899f9"}, {file = "pyobjc_core-10.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:6ff5823d13d0a534cdc17fa4ad47cf5bee4846ce0fd27fc40012e12b46db571b"}, {file = "pyobjc_core-10.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2581e8e68885bcb0e11ec619e81ef28e08ee3fac4de20d8cc83bc5af5bcf4a90"}, {file = "pyobjc_core-10.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ea98d4c2ec39ca29e62e0327db21418696161fb138ee6278daf2acbedf7ce504"}, {file = "pyobjc_core-10.3.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:4c179c26ee2123d0aabffb9dbc60324b62b6f8614fb2c2328b09386ef59ef6d8"}, {file = "pyobjc_core-10.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cb901fce65c9be420c40d8a6ee6fff5ff27c6945f44fd7191989b982baa66dea"}, {file = "pyobjc_core-10.3.1.tar.gz", hash = "sha256:b204a80ccc070f9ab3f8af423a3a25a6fd787e228508d00c4c30f8ac538ba720"}, ] [[package]] name = "pyobjc-framework-cocoa" version = "10.3.1" description = "Wrappers for the Cocoa frameworks on macOS" optional = false python-versions = ">=3.8" files = [ {file = "pyobjc_framework_Cocoa-10.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4cb4f8491ab4d9b59f5187e42383f819f7a46306a4fa25b84f126776305291d1"}, {file = "pyobjc_framework_Cocoa-10.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5f31021f4f8fdf873b57a97ee1f3c1620dbe285e0b4eaed73dd0005eb72fd773"}, {file = "pyobjc_framework_Cocoa-10.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:11b4e0bad4bbb44a4edda128612f03cdeab38644bbf174de0c13129715497296"}, {file = "pyobjc_framework_Cocoa-10.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:de5e62e5ccf2871a94acf3bf79646b20ea893cc9db78afa8d1fe1b0d0f7cbdb0"}, {file = "pyobjc_framework_Cocoa-10.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c5af24610ab639bd1f521ce4500484b40787f898f691b7a23da3339e6bc8b90"}, {file = "pyobjc_framework_Cocoa-10.3.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:a7151186bb7805deea434fae9a4423335e6371d105f29e73cc2036c6779a9dbc"}, {file = "pyobjc_framework_Cocoa-10.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:743d2a1ac08027fd09eab65814c79002a1d0421d7c0074ffd1217b6560889744"}, {file = "pyobjc_framework_cocoa-10.3.1.tar.gz", hash = "sha256:1cf20714daaa986b488fb62d69713049f635c9d41a60c8da97d835710445281a"}, ] [package.dependencies] pyobjc-core = ">=10.3.1" [[package]] name = "pytest" version = "8.3.3" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, ] [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" pluggy = ">=1.5,<2" tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-cov" version = "4.1.0" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.7" files = [ {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, ] [package.dependencies] coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] [[package]] name = "python-dateutil" version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, ] [package.dependencies] six = ">=1.5" [[package]] name = "pywin32" version = "308" description = "Python for Window Extensions" optional = false python-versions = "*" files = [ {file = "pywin32-308-cp310-cp310-win32.whl", hash = "sha256:796ff4426437896550d2981b9c2ac0ffd75238ad9ea2d3bfa67a1abd546d262e"}, {file = "pywin32-308-cp310-cp310-win_amd64.whl", hash = "sha256:4fc888c59b3c0bef905ce7eb7e2106a07712015ea1c8234b703a088d46110e8e"}, {file = "pywin32-308-cp310-cp310-win_arm64.whl", hash = "sha256:a5ab5381813b40f264fa3495b98af850098f814a25a63589a8e9eb12560f450c"}, {file = "pywin32-308-cp311-cp311-win32.whl", hash = "sha256:5d8c8015b24a7d6855b1550d8e660d8daa09983c80e5daf89a273e5c6fb5095a"}, {file = "pywin32-308-cp311-cp311-win_amd64.whl", hash = "sha256:575621b90f0dc2695fec346b2d6302faebd4f0f45c05ea29404cefe35d89442b"}, {file = "pywin32-308-cp311-cp311-win_arm64.whl", hash = "sha256:100a5442b7332070983c4cd03f2e906a5648a5104b8a7f50175f7906efd16bb6"}, {file = "pywin32-308-cp312-cp312-win32.whl", hash = "sha256:587f3e19696f4bf96fde9d8a57cec74a57021ad5f204c9e627e15c33ff568897"}, {file = "pywin32-308-cp312-cp312-win_amd64.whl", hash = "sha256:00b3e11ef09ede56c6a43c71f2d31857cf7c54b0ab6e78ac659497abd2834f47"}, {file = "pywin32-308-cp312-cp312-win_arm64.whl", hash = "sha256:9b4de86c8d909aed15b7011182c8cab38c8850de36e6afb1f0db22b8959e3091"}, {file = "pywin32-308-cp313-cp313-win32.whl", hash = "sha256:1c44539a37a5b7b21d02ab34e6a4d314e0788f1690d65b48e9b0b89f31abbbed"}, {file = "pywin32-308-cp313-cp313-win_amd64.whl", hash = "sha256:fd380990e792eaf6827fcb7e187b2b4b1cede0585e3d0c9e84201ec27b9905e4"}, {file = "pywin32-308-cp313-cp313-win_arm64.whl", hash = "sha256:ef313c46d4c18dfb82a2431e3051ac8f112ccee1a34f29c263c583c568db63cd"}, {file = "pywin32-308-cp37-cp37m-win32.whl", hash = "sha256:1f696ab352a2ddd63bd07430080dd598e6369152ea13a25ebcdd2f503a38f1ff"}, {file = "pywin32-308-cp37-cp37m-win_amd64.whl", hash = "sha256:13dcb914ed4347019fbec6697a01a0aec61019c1046c2b905410d197856326a6"}, {file = "pywin32-308-cp38-cp38-win32.whl", hash = "sha256:5794e764ebcabf4ff08c555b31bd348c9025929371763b2183172ff4708152f0"}, {file = "pywin32-308-cp38-cp38-win_amd64.whl", hash = "sha256:3b92622e29d651c6b783e368ba7d6722b1634b8e70bd376fd7610fe1992e19de"}, {file = "pywin32-308-cp39-cp39-win32.whl", hash = "sha256:7873ca4dc60ab3287919881a7d4f88baee4a6e639aa6962de25a98ba6b193341"}, {file = "pywin32-308-cp39-cp39-win_amd64.whl", hash = "sha256:71b3322d949b4cc20776436a9c9ba0eeedcbc9c650daa536df63f0ff111bb920"}, ] [[package]] name = "pyyaml" version = "6.0.2" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" files = [ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] [[package]] name = "pyyaml-env-tag" version = "0.1" description = "A custom YAML tag for referencing environment variables in YAML files. " optional = true python-versions = ">=3.6" files = [ {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, ] [package.dependencies] pyyaml = "*" [[package]] name = "requests" version = "2.32.3" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" files = [ {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, ] [package.dependencies] certifi = ">=2017.4.17" charset-normalizer = ">=2,<4" idna = ">=2.5,<4" urllib3 = ">=1.21.1,<3" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "rich" version = "13.9.4" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.8.0" files = [ {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, ] [package.dependencies] markdown-it-py = ">=2.2.0" pygments = ">=2.13.0,<3.0.0" typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" version = "0.8.0" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ {file = "ruff-0.8.0-py3-none-linux_armv6l.whl", hash = "sha256:fcb1bf2cc6706adae9d79c8d86478677e3bbd4ced796ccad106fd4776d395fea"}, {file = "ruff-0.8.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:295bb4c02d58ff2ef4378a1870c20af30723013f441c9d1637a008baaf928c8b"}, {file = "ruff-0.8.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7b1f1c76b47c18fa92ee78b60d2d20d7e866c55ee603e7d19c1e991fad933a9a"}, {file = "ruff-0.8.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb0d4f250a7711b67ad513fde67e8870109e5ce590a801c3722580fe98c33a99"}, {file = "ruff-0.8.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e55cce9aa93c5d0d4e3937e47b169035c7e91c8655b0974e61bb79cf398d49c"}, {file = "ruff-0.8.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f4cd64916d8e732ce6b87f3f5296a8942d285bbbc161acee7fe561134af64f9"}, {file = "ruff-0.8.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c5c1466be2a2ebdf7c5450dd5d980cc87c8ba6976fb82582fea18823da6fa362"}, {file = "ruff-0.8.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2dabfd05b96b7b8f2da00d53c514eea842bff83e41e1cceb08ae1966254a51df"}, {file = "ruff-0.8.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:facebdfe5a5af6b1588a1d26d170635ead6892d0e314477e80256ef4a8470cf3"}, {file = "ruff-0.8.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87a8e86bae0dbd749c815211ca11e3a7bd559b9710746c559ed63106d382bd9c"}, {file = "ruff-0.8.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:85e654f0ded7befe2d61eeaf3d3b1e4ef3894469cd664ffa85006c7720f1e4a2"}, {file = "ruff-0.8.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:83a55679c4cb449fa527b8497cadf54f076603cc36779b2170b24f704171ce70"}, {file = "ruff-0.8.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:812e2052121634cf13cd6fddf0c1871d0ead1aad40a1a258753c04c18bb71bbd"}, {file = "ruff-0.8.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:780d5d8523c04202184405e60c98d7595bdb498c3c6abba3b6d4cdf2ca2af426"}, {file = "ruff-0.8.0-py3-none-win32.whl", hash = "sha256:5fdb6efecc3eb60bba5819679466471fd7d13c53487df7248d6e27146e985468"}, {file = "ruff-0.8.0-py3-none-win_amd64.whl", hash = "sha256:582891c57b96228d146725975fbb942e1f30a0c4ba19722e692ca3eb25cc9b4f"}, {file = "ruff-0.8.0-py3-none-win_arm64.whl", hash = "sha256:ba93e6294e9a737cd726b74b09a6972e36bb511f9a102f1d9a7e1ce94dd206a6"}, {file = "ruff-0.8.0.tar.gz", hash = "sha256:a7ccfe6331bf8c8dad715753e157457faf7351c2b69f62f32c165c2dbcbacd44"}, ] [[package]] name = "send2trash" version = "1.8.3" description = "Send file to trash natively under Mac OS X, Windows and Linux" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ {file = "Send2Trash-1.8.3-py3-none-any.whl", hash = "sha256:0c31227e0bd08961c7665474a3d1ef7193929fedda4233843689baa056be46c9"}, {file = "Send2Trash-1.8.3.tar.gz", hash = "sha256:b18e7a3966d99871aefeb00cfbcfdced55ce4871194810fc71f4aa484b953abf"}, ] [package.dependencies] pyobjc-framework-Cocoa = {version = "*", optional = true, markers = "sys_platform == \"darwin\" and extra == \"nativelib\""} pywin32 = {version = "*", optional = true, markers = "sys_platform == \"win32\" and extra == \"nativelib\""} [package.extras] nativelib = ["pyobjc-framework-Cocoa", "pywin32"] objc = ["pyobjc-framework-Cocoa"] win32 = ["pywin32"] [[package]] name = "simplematch" version = "1.4" description = "Minimal, super readable string pattern matching." optional = false python-versions = ">=3.8,<4.0" files = [ {file = "simplematch-1.4-py3-none-any.whl", hash = "sha256:e7b898e174bc11c3bddc1b1ee36a9d70dd96037295837a879195052c92107237"}, {file = "simplematch-1.4.tar.gz", hash = "sha256:55a77278b3d0686cb38e3ffe5a326a5f59c2995f1ba1fa1a4f68872c17caf4cb"}, ] [[package]] name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] [[package]] name = "tomli" version = "2.1.0" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" files = [ {file = "tomli-2.1.0-py3-none-any.whl", hash = "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391"}, {file = "tomli-2.1.0.tar.gz", hash = "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8"}, ] [[package]] name = "types-python-dateutil" version = "2.9.0.20241003" description = "Typing stubs for python-dateutil" optional = false python-versions = ">=3.8" files = [ {file = "types-python-dateutil-2.9.0.20241003.tar.gz", hash = "sha256:58cb85449b2a56d6684e41aeefb4c4280631246a0da1a719bdbe6f3fb0317446"}, {file = "types_python_dateutil-2.9.0.20241003-py3-none-any.whl", hash = "sha256:250e1d8e80e7bbc3a6c99b907762711d1a1cdd00e978ad39cb5940f6f0a87f3d"}, ] [[package]] name = "types-pyyaml" version = "6.0.12.20240917" description = "Typing stubs for PyYAML" optional = false python-versions = ">=3.8" files = [ {file = "types-PyYAML-6.0.12.20240917.tar.gz", hash = "sha256:d1405a86f9576682234ef83bcb4e6fff7c9305c8b1fbad5e0bcd4f7dbdc9c587"}, {file = "types_PyYAML-6.0.12.20240917-py3-none-any.whl", hash = "sha256:392b267f1c0fe6022952462bf5d6523f31e37f6cea49b14cee7ad634b6301570"}, ] [[package]] name = "typing-extensions" version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] [[package]] name = "urllib3" version = "2.2.3" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" files = [ {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, ] [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] [[package]] name = "watchdog" version = "6.0.0" description = "Filesystem events monitoring" optional = true python-versions = ">=3.9" files = [ {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"}, {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"}, {file = "watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3"}, {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c"}, {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2"}, {file = "watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c"}, {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948"}, {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860"}, {file = "watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0"}, {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c"}, {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134"}, {file = "watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b"}, {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8"}, {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a"}, {file = "watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c"}, {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881"}, {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11"}, {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa"}, {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e"}, {file = "watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13"}, {file = "watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379"}, {file = "watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e"}, {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f"}, {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26"}, {file = "watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c"}, {file = "watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2"}, {file = "watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a"}, {file = "watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680"}, {file = "watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f"}, {file = "watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282"}, ] [package.extras] watchmedo = ["PyYAML (>=3.10)"] [[package]] name = "wcmatch" version = "10.0" description = "Wildcard/glob file name matcher." optional = true python-versions = ">=3.8" files = [ {file = "wcmatch-10.0-py3-none-any.whl", hash = "sha256:0dd927072d03c0a6527a20d2e6ad5ba8d0380e60870c383bc533b71744df7b7a"}, {file = "wcmatch-10.0.tar.gz", hash = "sha256:e72f0de09bba6a04e0de70937b0cf06e55f36f37b3deb422dfaf854b867b840a"}, ] [package.dependencies] bracex = ">=2.1.1" [[package]] name = "xattr" version = "0.9.9" description = "Python wrapper for extended filesystem attributes" optional = false python-versions = "*" files = [ {file = "xattr-0.9.9-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:58a9fb4fd19b467e88f4b75b5243706caa57e312d3aee757b53b57c7fd0f4ba9"}, {file = "xattr-0.9.9-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e71efca59705c7abde5b7f76323ebe00ed2977f10cba4204b9421dada036b5ca"}, {file = "xattr-0.9.9-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:1aad96b6603961c3d1ca1aaa8369b1a8d684a7b37357b2428087c286bf0e561c"}, {file = "xattr-0.9.9-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:46cb74f98d31d9d70f975ec3e6554360a9bdcbb4b9fb50a69fabe54f9f928c97"}, {file = "xattr-0.9.9-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:80c2db56058a687d7439be041f916cbeb2943fbe2623e53d5da721a4552d8991"}, {file = "xattr-0.9.9-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:c360d1cc42e885b64d84f64de3c501dd7bce576248327ef583b4625ee63aa023"}, {file = "xattr-0.9.9-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:debd87afe6bdf88c3689bde52eecf2b166388b13ef7388259d23223374db417d"}, {file = "xattr-0.9.9-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:4280c9f33a8678828f1bbc3d3dc8b823b5e4a113ee5ecb0fb98bff60cc2b9ad1"}, {file = "xattr-0.9.9-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:e0916ec1656d2071cd3139d1f52426825985d8ed076f981ef7f0bc13dfa8e96c"}, {file = "xattr-0.9.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a517916fbf2f58a3222bb2048fe1eeff4e23e07a4ce6228a27de004c80bf53ab"}, {file = "xattr-0.9.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e886c882b3b28c7a684c3e3daf46347da5428a46b88bc6d62c4867d574b90c54"}, {file = "xattr-0.9.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:373e3d1fd9258438fc38d1438142d3659f36743f374a20457346ef26741ed441"}, {file = "xattr-0.9.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a7beeb54ca140273b2f6320bb98b701ec30628af2ebe4eb30f7051419eb4ef3"}, {file = "xattr-0.9.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef3ca29cdaae9c47c625d84bb6c9046f7275cccde0ea805caa23ca58d3671f3f"}, {file = "xattr-0.9.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c381d890931cd18b137ce3fb5c5f08b672c3c61e2e47b1a7442ee46e827abfe"}, {file = "xattr-0.9.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:59c5783ccf57cf2700ce57d51a92134900ed26f6ab20d209f383fb898903fea6"}, {file = "xattr-0.9.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:966b885b69d95362e2a12d39f84889cf857090e57263b5ac33409498aa00c160"}, {file = "xattr-0.9.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efaaf0cb1ea8e9febb7baad301ae8cc9ad7a96fdfc5c6399d165e7a19e3e61ce"}, {file = "xattr-0.9.9-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f19fa75ed1e9db86354efab29869cb2be6976d456bd7c89e67b118d5384a1d98"}, {file = "xattr-0.9.9-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ca28ad06828244b315214ee35388f57e81e90aac2ceac3f32e42ae394e31b9c"}, {file = "xattr-0.9.9-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:532c7f1656dd2fe937116b9e210229f716d7fc7ac142f9cdace7da92266d32e8"}, {file = "xattr-0.9.9-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11c28033c17e98c67e0def9d6ebd415ad3c006a7bc3fee6bad79c5e52d0dff49"}, {file = "xattr-0.9.9-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:473cabb30e544ea08c8c01c1ef18053147cdc8552d443ac97815e46fbb13c7d4"}, {file = "xattr-0.9.9-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:c4a308522b444d090fbd66a385c9519b6b977818226921b0d2fc403667c93564"}, {file = "xattr-0.9.9-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:82493434488aca72d88b5129dac8f212e7b8bdca7ceffe7bb977c850f2452e4e"}, {file = "xattr-0.9.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e41d289706c7e8940f4d08e865da6a8ae988123e40a44f9a97ddc09e67795d7d"}, {file = "xattr-0.9.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef08698e360cf43688dca3db3421b156b29948a714d5d089348073f463c11646"}, {file = "xattr-0.9.9-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4eb10ac16ca8d534c0395425d52121e0c1981f808e1b3f577f6a5ec33d3853e4"}, {file = "xattr-0.9.9-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5605fec07b0e964bd980cc70ec335b9eb1b7ac7c6f314c7c2d8f54b09104fe4c"}, {file = "xattr-0.9.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:974e7d577ddb15e4552fb0ec10a4cfe09bdf6267365aa2b8394bb04637785aad"}, {file = "xattr-0.9.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ad6777de922c638bfa87a0d7faebc5722ddef04a1210b2a8909289b58b769af0"}, {file = "xattr-0.9.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3887e70873ebf0efbde32f9929ec1c7e45ec0013561743e2cc0406a91e51113b"}, {file = "xattr-0.9.9-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:83caa8e93a45a0f25f91b92d9b45f490c87bff74f02555df6312efeba0dacc31"}, {file = "xattr-0.9.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e33ec0a1d913d946d1ab7509f37ee37306c45af735347f13b963df34ffe6e029"}, {file = "xattr-0.9.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:263c58dca83372260c5c195e0b59959e38e1f107f0b7350de82e3db38479036c"}, {file = "xattr-0.9.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:125dfb9905428162349d3b8b825d9a18280893f0cb0db2a2467d5ef253fa6ce2"}, {file = "xattr-0.9.9-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e243524e0dde16d7a2e1b52512ad2c6964df2143dd1c79b820dcb4c6c0822c20"}, {file = "xattr-0.9.9-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01ec07d24a14406bdc6a123041c63a88e1c4a3f820e4a7d30f7609d57311b499"}, {file = "xattr-0.9.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:85c1df5f1d209345ea96de137419e886a27bb55076b3ae01faacf35aafcf3a61"}, {file = "xattr-0.9.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ca74d3eff92d6dc16e271fbad9cbab547fb9a0c983189c4031c3ff3d150dd871"}, {file = "xattr-0.9.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7d17505e49ac70c0e71939c5aac96417a863583fb30a2d6304d5ac881230548f"}, {file = "xattr-0.9.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1ae47a6398d3c04623fa386a4aa2f66e5cd3cdb1a7e69d1bfaeb8c73983bf271"}, {file = "xattr-0.9.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:809e2537d0aff9fca97dacf3245cbbaf711bbced5d1b0235a8d1906b04e26114"}, {file = "xattr-0.9.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de3af84364f06d67b3662ccf7c1a73e1d389d8d274394e952651e7bf1bbd2718"}, {file = "xattr-0.9.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b62cdad232d2d2dedd39b543701db8e3883444ec0d57ce3fab8f75e5f8b0301"}, {file = "xattr-0.9.9-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6b11d2eda397d47f7075743409683c233519ca52aa1dac109b413a4d8c15b740"}, {file = "xattr-0.9.9-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:661c0a939aefdf071887121f534bb10588d69c7b2dfca5c486af2fc81a0786e8"}, {file = "xattr-0.9.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5db7c2db320a8d5264d437d71f1eb7270a7e4a6545296e7766161d17752590b7"}, {file = "xattr-0.9.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:83203e60cbaca9536d297e5039b285a600ff84e6e9e8536fe2d521825eeeb437"}, {file = "xattr-0.9.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:42bfb4e4da06477e739770ac6942edbdc71e9fc3b497b67db5fba712fa8109c2"}, {file = "xattr-0.9.9-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:67047d04d1c56ad4f0f5886085e91b0077238ab3faaec6492c3c21920c6566eb"}, {file = "xattr-0.9.9-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:885782bc82ded1a3f684d54a1af259ae9fcc347fa54b5a05b8aad82b8a42044c"}, {file = "xattr-0.9.9-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bc84ccec618b5aa089e7cee8b07fcc92d4069aac4053da604c8143a0d6b1381"}, {file = "xattr-0.9.9-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baeff3e5dda8ea7e9424cfaee51829f46afe3836c30d02f343f9049c685681ca"}, {file = "xattr-0.9.9.tar.gz", hash = "sha256:09cb7e1efb3aa1b4991d6be4eb25b73dc518b4fe894f0915f5b0dcede972f346"}, ] [package.dependencies] cffi = ">=1.0" [[package]] name = "zipp" version = "3.21.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = true python-versions = ">=3.9" files = [ {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, ] [package.extras] check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] type = ["pytest-mypy"] [extras] docs = ["markupsafe", "mkdocs", "mkdocs-autorefs", "mkdocs-include-markdown-plugin", "mkdocstrings"] [metadata] lock-version = "2.0" python-versions = "^3.9" content-hash = "d8596b916c332bfa0e54c0ebe17db40df5da93ef78845ece07eff971fe1953ca" organize-3.3.0/pyproject.toml000066400000000000000000000054541472111340300162530ustar00rootroot00000000000000[tool.poetry] name = "organize-tool" version = "3.3.0" description = "The file management automation tool" packages = [{ include = "organize" }] authors = ["Thomas Feldmann "] license = "MIT" readme = "README.md" repository = "https://github.com/tfeldmann/organize" documentation = "https://organize.readthedocs.io" keywords = [ "file", "management", "automation", "tool", "organization", "rules", "yaml", ] classifiers = [ # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers "Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: End Users/Desktop", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Topic :: Utilities", ] [tool.poetry.scripts] organize = "organize.cli:cli" [tool.poetry.dependencies] python = "^3.9" arrow = "^1.3.0" docopt-ng = "^0.9.0" docx2txt = "^0.8" ExifRead = "2.3.2" # Pinned: https://github.com/tfeldmann/organize/issues/267 Jinja2 = "^3.1.2" macos-tags = { version = "^1.5.1", markers = "sys_platform == 'darwin'" } natsort = "^8.4.0" pdfminer-six = ">=20231228" platformdirs = "^4.0.0" pydantic = "^2.3.0" PyYAML = "^6.0" rich = "^13.4.2" Send2Trash = { version = "^1.8.2", extras = ["nativeLib"] } simplematch = "^1.4" # must be in main dependencies for readthedocs. mkdocs = { version = "^1.5.3", optional = true } mkdocs-autorefs = { version = "^0.5.0", optional = true } mkdocs-include-markdown-plugin = { version = "^6.0.4", optional = true } mkdocstrings = { version = "^0.24.0", extras = ["python"], optional = true } markupsafe = { version = "2.0.1", optional = true } # Pinned: https://stackoverflow.com/q/72191560/300783 [tool.poetry.extras] docs = [ "mkdocs", "mkdocs-autorefs", "mkdocs-include-markdown-plugin", "mkdocstrings", "markupsafe", ] [tool.poetry.group.dev.dependencies] coverage = "^7.2.0" mypy = "^1.4.0" pyfakefs = "^5.3.1" pytest = "^8.0.0" pytest-cov = "^4.1.0" requests = "^2.31.0" ruff = "^0.8.0" types-PyYAML = "^6.0.12.10" [tool.coverage.run] source = ['organize'] [tool.coverage.report] exclude_also = ["pragma: no cover", "if TYPE_CHECKING:"] [tool.mypy] python_version = "3.9" plugins = ["pydantic.mypy"] [[tool.mypy.overrides]] module = [ "schema", "simplematch", "appdirs", "send2trash", "exifread", "textract", "requests", "macos_tags", ] ignore_missing_imports = true [tool.pytest.ini_options] addopts = "--doctest-modules" testpaths = ["tests", "organize"] norecursedirs = ["tests/todo", "organize/filters", ".configs"] filterwarnings = ["ignore::DeprecationWarning"] [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" organize-3.3.0/pyrightconfig.json000066400000000000000000000002071472111340300170750ustar00rootroot00000000000000{ "venvPath": ".", "venv": ".venv", "reportMissingImports": true, "reportMissingTypeStubs": false, "pythonVersion": "3.11" } organize-3.3.0/tests/000077500000000000000000000000001472111340300144715ustar00rootroot00000000000000organize-3.3.0/tests/actions/000077500000000000000000000000001472111340300161315ustar00rootroot00000000000000organize-3.3.0/tests/actions/__init__.py000066400000000000000000000000001472111340300202300ustar00rootroot00000000000000organize-3.3.0/tests/actions/test_common.py000066400000000000000000000054741472111340300210440ustar00rootroot00000000000000from pathlib import Path from conftest import make_files, read_files from organize.actions.common.target_path import prepare_target_path, user_wants_a_folder def test_user_wants_a_folder(): assert user_wants_a_folder("/test/", autodetect=False) assert not user_wants_a_folder("/test", autodetect=False) assert not user_wants_a_folder("/test.asd", autodetect=False) assert user_wants_a_folder("/test.asd/", autodetect=False) assert not user_wants_a_folder("/some/original/folder/name.txt", autodetect=False) assert not user_wants_a_folder("/some/thing.app/subfolder", autodetect=False) def test_user_wants_a_folder_autodetect(): assert user_wants_a_folder("/test/", autodetect=True) assert user_wants_a_folder("/test", autodetect=True) assert not user_wants_a_folder("/test.asd", autodetect=True) assert user_wants_a_folder("/test.asd/", autodetect=True) assert not user_wants_a_folder("/some/original/folder/name.txt", autodetect=True) assert user_wants_a_folder("/some/thing.app/subfolder", autodetect=True) def test_prepare_target_path(fs): # simulate assert ( prepare_target_path( src_name="dst.txt", dst="/test/", autodetect_folder=True, simulate=True, ) == Path("/test/dst.txt").resolve() ) assert not Path("/test").exists() # for real assert ( prepare_target_path( src_name="dst.txt", dst="/test/", autodetect_folder=True, simulate=False, ) == Path("/test/dst.txt").resolve() ) assert read_files("test") == {} def test_prepare_folder_target_advanced(fs): assert ( prepare_target_path( src_name="dst", dst="/some/test/folder", autodetect_folder=True, simulate=False, ) == Path("/some/test/folder/dst").resolve() ) assert read_files("some") == {"test": {"folder": {}}} def test_prepare_folder_target_already_exists(fs): make_files({"some": {"Application.app": {}}}) assert ( prepare_target_path( src_name="info.plist", dst="/some/Application.app", autodetect_folder=True, simulate=False, ) == Path("/some/Application.app/info.plist").resolve() ) assert read_files("some") == {"Application.app": {}} def test_prepare_folder_no_folder(fs): assert ( prepare_target_path( src_name="filename.txt", dst="/some/original/folder/name.txt", autodetect_folder=True, simulate=False, ) == Path("/some/original/folder/name.txt").resolve() ) assert read_files("some") == {"original": {"folder": {}}} # TODO: Hier ist das Ordnerhandling noch unklar, also wenn eine Resource # ein Ordner ist. organize-3.3.0/tests/actions/test_conflict_resolution.py000066400000000000000000000063131472111340300236310ustar00rootroot00000000000000from pathlib import Path import pytest from conftest import make_files, read_files from organize.actions.common.conflict import next_free_name, resolve_conflict from organize.output import JSONL from organize.resource import Resource from organize.template import Template @pytest.mark.parametrize( "template,wanted,result", ( ("{name}-{counter}{extension}", "file.txt", "file-2.txt"), ("{name}-{counter}{extension}", "file.txt", "file-2.txt"), (r"{name}-{'%02d' % counter}{extension}", "file.txt", "file-03.txt"), ("{name}{counter}{extension}", "file.txt", "file4.txt"), ("{name}{counter}{extension}", "other.txt", "other.txt"), ("{name} {counter}{extension}", "folder/test.txt", "folder/test 2.txt"), ("{name} {counter}{extension}", "folder/other.txt", "folder/other.txt"), ), ) def test_next_free_name(fs, template, wanted, result): fs.create_file("file.txt") fs.create_file("file1.txt") fs.create_file("file-01.txt") fs.create_file("file-02.txt") fs.create_file("file2.txt") fs.create_file("file3.txt") fs.create_file("folder/test.txt") tmp = Template.from_string(template) assert next_free_name(dst=Path(wanted), template=tmp) == Path(result) def test_next_free_name_exception(fs): fs.create_file("file.txt") fs.create_file("file1.txt") tmp = Template.from_string("{name}{extension}") with pytest.raises(ValueError): assert next_free_name(dst=Path("file.txt"), template=tmp) @pytest.mark.parametrize( "mode,result,files", ( ( "skip", (True, "test/file.txt"), {"file.txt": "file", "other.txt": "other"}, ), ( "overwrite", (False, "test/other.txt"), {"file.txt": "file"}, ), # ("trash", None, {"file.txt": "file", "other.txt": "other"}), ( "rename_new", (False, "test/other2.txt"), {"file.txt": "file", "other.txt": "other"}, ), ( "rename_existing", (False, "test/other.txt"), {"file.txt": "file", "other2.txt": "other"}, ), ), ) def test_resolve_overwrite_conflict(fs, mode, result, files): make_files( { "file.txt": "file", "other.txt": "other", }, "test", ) output = JSONL() skip_action, use_dst = resolve_conflict( dst=Path("test/other.txt"), res=Resource(path=Path("test/file.txt")), conflict_mode=mode, rename_template=Template.from_string("{name}{counter}{extension}"), simulate=False, output=output, ) assert (skip_action, use_dst) == (result[0], Path(result[1])) assert files == read_files("test") def test_conflicting_folders(fs): make_files({"src": {}, "dir1": {"sub1": {}, "sub2": {}}}, "test") result = resolve_conflict( dst=Path("/test/dir1/sub1"), res=Resource(path=Path("/test/src")), rename_template=Template.from_string("{name} {counter}{extension}"), conflict_mode="rename_new", simulate=False, output=JSONL(), ) assert not result.skip_action assert result.use_dst == Path("/test/dir1/sub1 2") organize-3.3.0/tests/actions/test_copy.py000066400000000000000000000106161472111340300205200ustar00rootroot00000000000000import pytest from conftest import make_files, read_files from organize import Config FILES = { "test.txt": "", "file.txt": "Hello world\nAnother line", "another.txt": "", "folder": { "x.txt": "", }, } def test_copy_on_itself(fs): make_files(FILES, "test") config = """ rules: - locations: "test" actions: - copy: "test" """ Config.from_string(config).execute(simulate=False) result = read_files("test") assert result == FILES def test_copy_basic(fs): config = """ rules: - locations: "test" filters: - name: test actions: - copy: test/newname.pdf """ make_files(["asd.txt", "newname 2.pdf", "newname.pdf", "test.pdf"], "test") Config.from_string(config).execute(simulate=False) assert read_files("test") == { "newname.pdf": "", "newname 2.pdf": "", "newname 3.pdf": "", "test.pdf": "", "asd.txt": "", } def test_copy_into_dir(fs): make_files(FILES, "test") config = """ rules: - locations: "test" actions: - copy: "test/a brand/new/folder/" """ Config.from_string(config).execute(simulate=False) result = read_files("test") assert result == { "test.txt": "", "file.txt": "Hello world\nAnother line", "another.txt": "", "folder": { "x.txt": "", }, "a brand": { "new": { "folder": { "test.txt": "", "file.txt": "Hello world\nAnother line", "another.txt": "", } } }, } def test_copy_into_dir_subfolders(fs): make_files(FILES, "test") config = """ rules: - locations: "/test" subfolders: true actions: - copy: "/test/a brand/new/folder/" """ Config.from_string(config).execute(simulate=False) result = read_files("test") assert result == { "test.txt": "", "file.txt": "Hello world\nAnother line", "another.txt": "", "folder": { "x.txt": "", }, "a brand": { "new": { "folder": { "test.txt": "", "file.txt": "Hello world\nAnother line", "another.txt": "", "x.txt": "", } } }, } @pytest.mark.parametrize( "mode,result", [ ("skip", {"src.txt": "src", "dst.txt": "dst"}), ("overwrite", {"src.txt": "src", "dst.txt": "src"}), ("rename_new", {"src.txt": "src", "dst.txt": "dst", "dst 2.txt": "src"}), ("rename_existing", {"src.txt": "src", "dst.txt": "src", "dst 2.txt": "dst"}), ], ) def test_copy_conflict(fs, mode, result): make_files( { "src.txt": "src", "dst.txt": "dst", }, path="test", ) config = f""" rules: - locations: "/test" filters: - name: src actions: - copy: dest: "/test/dst.txt" on_conflict: {mode} """ Config.from_string(config).execute(simulate=False) assert read_files("test") == result def test_copy_deduplicate_conflict(fs): files = { "src.txt": "src", "duplicate": { "src.txt": "src", }, "nonduplicate": { "src.txt": "src2", }, } config = """ rules: - locations: "/test" subfolders: true filters: - name: src actions: - copy: dest: "/test/dst.txt" on_conflict: deduplicate """ make_files(files, "test") Config.from_string(config).execute(simulate=False) result = read_files("test") assert result == { "src.txt": "src", "duplicate": { "src.txt": "src", }, "nonduplicate": { "src.txt": "src2", }, "dst.txt": "src", "dst 2.txt": "src2", } def test_does_not_create_folder_in_simulation(fs): config = """ rules: - locations: "/test" actions: - copy: "/test/new-subfolder/" - copy: "/test/copyhere/" """ make_files(FILES, "test") Config.from_string(config).execute(simulate=True) result = read_files("test") assert result == FILES organize-3.3.0/tests/actions/test_delete.py000066400000000000000000000041201472111340300210010ustar00rootroot00000000000000from conftest import make_files, read_files from organize import Config FILES = { "test.txt": "", "file.txt": "Hello world\nAnother line", "another.txt": "", "folder": { "x.txt": "", "empty_sub": {}, }, "empty_folder": {}, } def test_delete_empty_files(fs): config = """ rules: - name: "Recursively delete all empty files" locations: - path: "test" subfolders: true filters: - empty actions: - delete """ make_files(FILES, "test") Config.from_string(config).execute(simulate=False) result = read_files("test") assert result == { "file.txt": "Hello world\nAnother line", "folder": { "empty_sub": {}, }, "empty_folder": {}, } def test_delete_empty_dirs(fs): config = """ rules: - name: "Recursively delete all empty directories" locations: "test" subfolders: true targets: dirs filters: - empty actions: - delete """ make_files(FILES, "test") Config.from_string(config).execute(simulate=False) result = read_files("test") assert result == { "test.txt": "", "file.txt": "Hello world\nAnother line", "another.txt": "", "folder": { "x.txt": "", }, } def test_delete_deep(fs): files = { "files": { "folder": { "subfolder": { "test.txt": "", "other.pdf": "binary", }, "file.txt": "Hello world\nAnother line", }, } } make_files(files, "test") config = """ rules: - locations: "test" actions: - delete - locations: "test" targets: dirs actions: - delete """ # sim Config.from_string(config).execute(simulate=True) result = read_files("test") assert result == files # run Config.from_string(config).execute(simulate=False) result = read_files("test") assert result == {} organize-3.3.0/tests/actions/test_echo.py000066400000000000000000000020251472111340300204570ustar00rootroot00000000000000from collections import Counter from datetime import datetime from conftest import make_files from organize import Config def test_echo_basic(testoutput): Config.from_string( """ rules: - actions: - echo: "Hello World" """ ).execute(simulate=False, output=testoutput) assert testoutput.messages == ["Hello World"] def test_echo_args(testoutput): Config.from_string( """ rules: - actions: - echo: "Date formatting: {now().strftime('%Y')}" """ ).execute(simulate=False, output=testoutput) assert testoutput.messages == [f"Date formatting: {datetime.now().year}"] def test_echo_path(fs, testoutput): make_files(["fileA.txt", "fileB.txt"], "test") Config.from_string( """ rules: - locations: /test actions: - echo: "{path.stem}" """ ).execute(simulate=False, output=testoutput) assert Counter(testoutput.messages) == Counter(["fileA", "fileB"]) organize-3.3.0/tests/actions/test_macos_tags.py000066400000000000000000000013671472111340300216710ustar00rootroot00000000000000import sys from pathlib import Path import pytest from organize import Config from organize.filters.macos_tags import list_tags @pytest.mark.skipif(sys.platform != "darwin", reason="runs only on macOS") def test_macos_action(tmp_path: Path): (tmp_path / "file.txt").touch() (tmp_path / "test.txt").touch() tag_param = "{name} ({% if name == 'file' %}GREEN{% else %}RED{% endif %})" Config.from_string( f""" rules: - locations: {tmp_path} filters: - name actions: - macos_tags: "{tag_param}" """ ).execute(simulate=False) assert list_tags(tmp_path / "file.txt") == ["file (green)"] assert list_tags(tmp_path / "test.txt") == ["test (red)"] organize-3.3.0/tests/actions/test_move.py000066400000000000000000000054171472111340300205170ustar00rootroot00000000000000import pytest from conftest import make_files, read_files from organize.config import Config def test_move_onto_itself(fs): FILES = { "test.txt": "", "file.txt": "Hello world\nAnother line", "another.txt": "", "folder": { "x.txt": "", }, } make_files(FILES, "test") config = """ rules: - locations: "test" actions: - move: "test" """ Config.from_string(config).execute(simulate=False) assert read_files("test") == FILES @pytest.mark.parametrize( "mode,result", [ ("skip", {"src.txt": "src", "dst.txt": "dst"}), ("overwrite", {"dst.txt": "src"}), ("rename_new", {"dst 2.txt": "src", "dst.txt": "dst"}), ("rename_existing", {"dst.txt": "src", "dst 2.txt": "dst"}), ], ) def test_move_conflict(fs, mode, result): make_files( { "src.txt": "src", "dst.txt": "dst", }, path="test", ) # src is moved onto dst. config = f""" rules: - locations: "test" filters: - name: src actions: - move: dest: "{{location}}/dst.txt" on_conflict: {mode} """ Config.from_string(config).execute(simulate=False) assert read_files("test") == result def test_move_deduplicate_conflict(fs): files = { "src.txt": "src", "duplicate": { "src.txt": "src", }, "nonduplicate": { "src.txt": "src2", }, } config = """ rules: - locations: "/test" subfolders: true filters: - name: src actions: - move: dest: "/test/dst.txt" on_conflict: deduplicate """ make_files(files, "test") Config.from_string(config).execute(simulate=False) result = read_files("test") assert result == { "duplicate": { "src.txt": "src", }, "nonduplicate": {}, "dst.txt": "src", "dst 2.txt": "src2", } def test_move_folder_conflict(fs): make_files( { "src": {"dir": {"src.txt": ""}}, "dst": {"dir": {"dst.txt": ""}}, }, "test", ) # src is moved onto dst. Config.from_string( """ rules: - locations: "/test/src" targets: dirs filters: - name: dir actions: - move: dest: "{location}/../dst" on_conflict: "rename_new" """ ).execute(simulate=False) assert read_files("test") == { "src": {}, "dst": { "dir": {"dst.txt": ""}, "dir 2": {"src.txt": ""}, }, } organize-3.3.0/tests/actions/test_python.py000066400000000000000000000013751472111340300210710ustar00rootroot00000000000000from pathlib import Path from conftest import make_files from organize import Config def test_python(fs, testoutput): make_files({"file.txt": "File content"}, "test") Config.from_string( """ rules: - locations: /test actions: - python: | from pathlib import Path Path("/test/result.txt").touch() print(f"Handling: {path}") return {"content": path.read_text()} - echo: "{python.content}" """ ).execute(simulate=False, output=testoutput) assert Path("/test/result.txt").exists() assert testoutput.messages == [ f"Handling: {Path('/test/file.txt')}", "File content", ] organize-3.3.0/tests/actions/test_rename.py000066400000000000000000000062761472111340300210240ustar00rootroot00000000000000from conftest import make_files, read_files from organize import Config def test_rename_issue51(fs): # test for issue https://github.com/tfeldmann/organize/issues/51 files = { "19asd_WF_test2.PDF": "", "other.pdf": "", "18asd_WFX_test2.pdf": "", } make_files(files, "test") Config.from_string( """ rules: - locations: "/test" filters: - extension - name: startswith: "19" contains: - "_WF_" actions: - rename: "{name}_unread.{extension.lower()}" - copy: dest: "/test/copy/" """ ).execute(simulate=False) assert read_files("test") == { "copy": { "19asd_WF_test2_unread.pdf": "", }, "19asd_WF_test2_unread.pdf": "", "other.pdf": "", "18asd_WFX_test2.pdf": "", } def test_rename_folders(fs): files = { "[DVD] Best Of Video 1080 [1080p]": { "[DVD] Best Of Video 1080 [1080p]": "", "Metadata": "", }, "[DVD] This Is A Title [1080p]": { "[DVD] This Is A Title [1080p]": "", "Metadata": "", }, } make_files(files, "test") Config.from_string( """ rules: - name: "Renaming DVD folders" locations: "/test" targets: dirs filters: - name: contains: DVD actions: - rename: new_name: "{name.replace('[DVD] ','').replace(' [1080p]','').replace(' ', '_')}" """ ).execute(simulate=False) assert read_files("test") == { "Best_Of_Video_1080": { "[DVD] Best Of Video 1080 [1080p]": "", "Metadata": "", }, "This_Is_A_Title": { "[DVD] This Is A Title [1080p]": "", "Metadata": "", }, } def test_rename_in_subfolders(fs): files = { "folder": { "FIRST-RENAME.pdf": "", "Other": "", }, "Another folder": { "Subfolder": { "RENAME-ME_TOO.txt": "", }, "Metadata": "", }, } make_files(files, "test") Config.from_string( """ rules: - locations: "/test" subfolders: true filters: - name: contains: RENAME - extension actions: - rename: "DONE.{extension}" """ ).execute(simulate=False) assert read_files("test") == { "folder": { "DONE.pdf": "", "Other": "", }, "Another folder": { "Subfolder": { "DONE.txt": "", }, "Metadata": "", }, } def test_filename_move(fs): make_files({"test.PY": ""}, "test") Config.from_string( """ rules: - locations: "/test" filters: - extension actions: - rename: '{path.stem}{path.stem}.{extension.lower()}' """ ).execute(simulate=False) assert read_files("test") == {"testtest.py": ""} organize-3.3.0/tests/actions/test_shell.py000066400000000000000000000022731472111340300206550ustar00rootroot00000000000000from conftest import read_files from organize import Config def test_shell(tmp_path, testoutput): (tmp_path / "test.txt").touch() Config.from_string( f""" rules: - locations: '{tmp_path}' actions: - shell: 'touch {{path}}.bak' """ ).execute(simulate=False, output=testoutput) assert read_files(tmp_path) == { "test.txt": "", "test.txt.bak": "", } # TODO # def test_shell_basic(): # shell = Shell( # "echo 'Hello World'", # simulation_output="-sim-", # simulation_returncode=127, # ) # result = shell.run(simulate=True) # assert "-sim-" in result["shell"]["output"] # assert 127 == result["shell"]["returncode"] # result = shell.run(simulate=False) # result = result["shell"] # assert "Hello World" in result["output"] # windows escapes the string # assert result["returncode"] == 0 # def test_shell_template_simulation(): # shell = Shell("echo '{msg}'", run_in_simulation=True) # result = shell.run(msg="Hello", simulate=True) # result = result["shell"] # assert "Hello" in result["output"] # assert result["returncode"] == 0 organize-3.3.0/tests/actions/test_symlink.py000066400000000000000000000015261472111340300212340ustar00rootroot00000000000000from pathlib import Path from conftest import make_files from organize import Config def test_symlink(fs): make_files({"file.txt": "Content"}, "test") Config.from_string( """ rules: - locations: /test actions: - symlink: /other/ """ ).execute(simulate=False) target = Path("/other/file.txt") assert target.is_symlink() assert target.read_text() == "Content" def test_symlink_dir(fs): make_files({"dir": {"file.txt": "Content"}}, "test") Config.from_string( """ rules: - locations: /test targets: dirs actions: - symlink: /other/ """ ).execute(simulate=False) target = Path("/other/dir") assert target.is_symlink() assert (target / "file.txt").read_text() == "Content" organize-3.3.0/tests/actions/test_trash.py000066400000000000000000000016411472111340300206650ustar00rootroot00000000000000from unittest.mock import patch from organize import Config def test_trash_mocked(tmp_path): testfile = tmp_path / "test.txt" testfile.touch() with patch("organize.actions.trash.trash") as mck: Config.from_string( f""" rules: - locations: {tmp_path} actions: - trash """ ).execute(simulate=False) mck.assert_called_once_with(testfile) def test_trash_folder(tmp_path): testfolder = tmp_path / "test" testfolder.mkdir() (testfolder / "testfile.txt").touch() with patch("organize.actions.trash.trash") as mck: Config.from_string( f""" rules: - locations: {tmp_path} targets: dirs actions: - trash """ ).execute(simulate=False) mck.assert_called_once_with(testfolder) organize-3.3.0/tests/actions/test_write.py000066400000000000000000000065741472111340300207100ustar00rootroot00000000000000from pathlib import Path import pytest from conftest import make_files, read_files from organize.config import Config @pytest.mark.parametrize( ("mode", "newline", "result"), [ ("append", "true", "a\nb\nc\n"), ("append", "false", "abc"), ("prepend", "true", "c\nb\na\n"), ("prepend", "false", "cba"), ("overwrite", "true", "c\n"), ("overwrite", "false", "c"), ], ) def test_write(fs, mode, newline, result): fs.create_file("test/a.txt") fs.create_file("test/b.txt") fs.create_file("test/c.txt") config = """ rules: - locations: "test" filters: - name: "a" actions: - write: text: "{text}" outfile: "new/folder/out.txt" mode: {mode} newline: {newline} - locations: "test" filters: - name: "b" actions: - write: text: "{text}" outfile: "new/folder/out.txt" mode: {mode} newline: {newline} - locations: "test" filters: - name: "c" actions: - write: text: "{text}" outfile: "new/folder/out.txt" mode: {mode} newline: {newline} """.format(text="{name}", mode=mode, newline=newline) Config.from_string(config).execute(simulate=False) with open("new/folder/out.txt") as f: assert result == f.read() def test_write_manyfiles(fs): make_files({"test1.txt": "content\n", "test2.txt": "Other"}, "test") Config.from_string( """ rules: - locations: "/test" actions: - write: text: "WRITE {path.name}" outfile: "/out/for-{path.stem}.txt" mode: overwrite clear_before_first_write: true newline: false """ ).execute(simulate=False) assert read_files("/out") == { "for-test1.txt": "WRITE test1.txt", "for-test2.txt": "WRITE test2.txt", } def test_write_clear_then_append(fs): make_files({"test1.txt": "", "test2.txt": ""}, "loc1") make_files({"test1.txt": "", "test2.txt": ""}, "loc2") make_files( { "test1": { "test1.log": "Previous output", }, "test2": { "test2.log": "Other previous output", }, }, "out", ) Config.from_string( """ rules: - locations: - "loc1" - "loc2" actions: - write: text: "FOUND {path}" outfile: "/out/{path.stem}/{path.stem}.log" mode: append clear_before_first_write: true newline: true """ ).execute(simulate=False) assert read_files("/out") == { "test1": { "test1.log": f"FOUND {Path('loc1/test1.txt')}\nFOUND {Path('loc2/test1.txt')}\n" }, "test2": { "test2.log": f"FOUND {Path('loc1/test2.txt')}\nFOUND {Path('loc2/test2.txt')}\n" }, } organize-3.3.0/tests/combined/000077500000000000000000000000001472111340300162515ustar00rootroot00000000000000organize-3.3.0/tests/combined/__init__.py000066400000000000000000000000001472111340300203500ustar00rootroot00000000000000organize-3.3.0/tests/combined/test_cli.py000066400000000000000000000066421472111340300204410ustar00rootroot00000000000000# from conftest import assertdir, create_filesystem # from organize.cli import main # def test_filename_move(tmp_path): # create_filesystem( # tmp_path, # files=["test.PY"], # config=""" # rules: # - folders: files # filters: # - Extension # actions: # - rename: '{path.stem}{path.stem}.{extension.lower}' # """, # ) # main(["run", "--config-file=%s" % (tmp_path / "config.yaml")]) # assertdir(tmp_path, "testtest.py") # def test_basic(tmp_path): # create_filesystem( # tmp_path, # files=["asd.txt", "newname 2.pdf", "newname.pdf", "test.pdf"], # config=""" # rules: # - folders: files # filters: # - filename: test # actions: # - copy: files/newname.pdf # """, # ) # main(["run", "--config-file=%s" % (tmp_path / "config.yaml")]) # assertdir( # tmp_path, "newname.pdf", "newname 2.pdf", "newname 3.pdf", "test.pdf", "asd.txt" # ) # @pytest.mark.skip # def test_config_file(tempfs: FS): # files = ["my-test-file-name.txt", "my-test-file-name.jpg", "other-file.txt"] # make_files(tempfs, files) # config = """ # rules: # - locations: %s # filters: # - name: my-test-file-name # actions: # - delete # """ # with fs.open_fs("temp://") as configfs: # configfs.writetext("config.yaml", config % tempfs.getsyspath("/")) # args = [configfs.getsyspath("config.yaml")] # result = runner.invoke(cli.sim, args) # print(result.output) # assert set(tempfs.listdir("/")) == set(files) # result = runner.invoke(cli.run, args) # print(result.output) # assert set(tempfs.listdir("/")) == set(["other-file.txt"]) # @pytest.mark.skip # def test_working_dir(tempfs: FS): # files = ["my-test-file-name.txt", "my-test-file-name.jpg", "other-file.txt"] # make_files(tempfs, files) # config = """ # rules: # - locations: "." # filters: # - name: my-test-file-name # actions: # - delete # """ # with fs.open_fs("temp://") as configfs: # configfs.writetext("config.yaml", config) # args = [ # configfs.getsyspath("config.yaml"), # "--working-dir=%s" % tempfs.getsyspath("/"), # ] # print(args) # runner.invoke(cli.sim, args) # assert set(tempfs.listdir("/")) == set(files) # runner.invoke(cli.run, args) # assert set(tempfs.listdir("/")) == set(["other-file.txt"]) # @pytest.mark.skip # def test_with_os_chdir(tempfs: FS): # files = ["my-test-file-name.txt", "my-test-file-name.jpg", "other-file.txt"] # make_files(tempfs, files) # config = """ # rules: # - locations: "." # filters: # - name: my-test-file-name # actions: # - delete # """ # with fs.open_fs("temp://") as configfs: # configfs.writetext("config.yaml", config) # args = [ # configfs.getsyspath("config.yaml"), # ] # print(args) # os.chdir(tempfs.getsyspath("/")) # runner.invoke(cli.sim, args) # assert set(tempfs.listdir("/")) == set(files) # runner.invoke(cli.run, args) # assert set(tempfs.listdir("/")) == set(["other-file.txt"]) organize-3.3.0/tests/combined/test_codepost_usecase.py000066400000000000000000000023071472111340300232140ustar00rootroot00000000000000from conftest import make_files, read_files from organize import Config def test_codepost_usecase(fs): files = { "Devonte-Betts.txt": "", "Alaina-Cornish.txt": "", "Dimitri-Bean.txt": "", "Lowri-Frey.txt": "", "Someunknown-User.txt": "", } make_files(files, "test") Config.from_string( """ rules: - locations: /test filters: - extension: txt - regex: (?P\w+)-(?P\w+)..* - python: | emails = { "Betts": "dbetts@mail.de", "Cornish": "acornish@google.com", "Bean": "dbean@aol.com", "Frey": "l-frey@frey.org", } if regex["lastname"] in emails: return {"mail": emails[regex["lastname"]]} actions: - move: "/files/{python.mail}.txt" """ ).execute(simulate=False) result = read_files("/files") assert result == { "dbetts@mail.de.txt": "", "acornish@google.com.txt": "", "dbean@aol.com.txt": "", "l-frey@frey.org.txt": "", } organize-3.3.0/tests/combined/test_dependent_rules.py000066400000000000000000000013641472111340300230460ustar00rootroot00000000000000from conftest import make_files, read_files from organize import Config def test_dependent_rules(fs): files = { "asd.txt": "", "newname 2.pdf": "", "newname.pdf": "", "test.pdf": "", } make_files(files, "test") Config.from_string( """ rules: - locations: /test filters: - name: test actions: - copy: /test/newfolder/ - locations: /test/newfolder filters: - name: test actions: - rename: test-found.pdf """ ).execute(simulate=False) assert read_files("test") == { "newname.pdf": "", "newname 2.pdf": "", "test.pdf": "", "asd.txt": "", "newfolder": {"test-found.pdf": ""}, } organize-3.3.0/tests/combined/test_example_config.py000066400000000000000000000006401472111340300226420ustar00rootroot00000000000000from organize import Config from organize.find_config import EXAMPLE_CONFIG from organize.output import JSONL, Default def test_example_config(testoutput): config = Config.from_string(EXAMPLE_CONFIG) config.execute(simulate=False, output=testoutput) assert testoutput.messages == ["Hello, World!"] config.execute(simulate=False, output=Default()) config.execute(simulate=False, output=JSONL()) organize-3.3.0/tests/combined/test_keep_folder_structure.py000066400000000000000000000034541472111340300242670ustar00rootroot00000000000000from conftest import make_files, read_files from organize import Config structure = { "file1.txt": "", "file2.txt": "", "folder1": { "folder2": { "file1.2.1.txt": "", "file1.2.2.txt": "", }, "file1.1.txt": "", "file1.2.txt": "", }, } def test_parent_folder_only(fs): make_files(structure, "/test") Config.from_string( """ rules: - locations: "/test" subfolders: true filters: - name: startswith: file actions: - copy: "/dest/{path.parent.name}/" """ ).execute(simulate=False) assert read_files("/dest") == { "test": { "file1.txt": "", "file2.txt": "", }, "folder1": { "file1.1.txt": "", "file1.2.txt": "", }, "folder2": { "file1.2.1.txt": "", "file1.2.2.txt": "", }, } def test_from_monitoring_folder(fs): make_files(structure, "test") Config.from_string( """ rules: - locations: "/test" subfolders: true filters: - name: startswith: file actions: - copy: "/dest/{relative_path}" """ ).execute(simulate=False) assert read_files("dest") == structure def test_from_root(fs): make_files(structure, "/test/sub1/sub2") Config.from_string( """ rules: - locations: "/test/sub1" subfolders: true filters: - name: startswith: file actions: - copy: "/dest/{path}" """ ).execute(simulate=False) assert read_files("/dest") == {"test": {"sub1": {"sub2": structure}}} organize-3.3.0/tests/combined/test_rename_regex.py000066400000000000000000000014471472111340300223310ustar00rootroot00000000000000from conftest import make_files, read_files from organize import Config def test_rename_files_date(fs): # inspired by https://github.com/tfeldmann/organize/issues/43 files = { "File_abc_dat20190812_xyz.pdf": "", "File_xyz_bar19990101_a.pdf": "", "File_123456_foo20000101_xyz20190101.pdf": "", } make_files(files, "test") Config.from_string( r""" rules: - locations: "/test" filters: - regex: 'File_.*?(?P\d{4})(?P\d{2})(?P\d{2}).*?.pdf' actions: - rename: "File_{regex.d}{regex.m}{regex.y}.pdf" """ ).execute(simulate=False) assert read_files("test") == { "File_12082019.pdf": "", "File_01011999.pdf": "", "File_01012000.pdf": "", } organize-3.3.0/tests/combined/test_simple_replace.py000066400000000000000000000010171472111340300226450ustar00rootroot00000000000000from conftest import make_files, read_files from organize import Config structure = { "file1.txt": "", "file2.txt": "", } def test_simple_replace(fs): make_files(structure, "/test") Config.from_string( """ rules: - locations: "/test" subfolders: true actions: - rename: "{path.name.replace('file', 'Datei')}" """ ).execute(simulate=False) assert read_files("/test") == { "Datei1.txt": "", "Datei2.txt": "", } organize-3.3.0/tests/conftest.py000066400000000000000000000034351472111340300166750ustar00rootroot00000000000000from collections import Counter from pathlib import Path from typing import Dict, Iterable, List, Union import pytest from organize.output import SavingOutput ORGANIZE_DIR = Path(__file__).parent.parent @pytest.fixture() def testoutput() -> SavingOutput: return SavingOutput() def equal_items(a: Iterable, b: Iterable) -> bool: return Counter(a) == Counter(b) def make_files(structure: Union[Dict, List], path: Union[Path, str] = "."): """Example structure: { "folder": { "subfolder": { "test.txt": "", "other.pdf": b"binary", }, }, "file.txt": "Hello world\nAnother line", } """ if isinstance(path, str): path = Path(path) path.mkdir(parents=True, exist_ok=True) # structure is a list of filenames if isinstance(structure, list): for name in structure: (path / name).touch() return # structure is a dict for name, content in structure.items(): resource: Path = path / name # folders are dicts if isinstance(content, dict): make_files(structure=content, path=resource) # everything else is a file elif content is None: resource.touch() elif isinstance(content, bytes): resource.write_bytes(content) elif isinstance(content, str): resource.write_text(content) else: raise ValueError(f"Unknown file data {content}") def read_files(path: Union[Path, str] = "."): if isinstance(path, str): path = Path(path) result = dict() for x in path.glob("*"): if x.is_file(): result[x.name] = x.read_text() if x.is_dir(): result[x.name] = read_files(x) return result organize-3.3.0/tests/core/000077500000000000000000000000001472111340300154215ustar00rootroot00000000000000organize-3.3.0/tests/core/__init__.py000066400000000000000000000000001472111340300175200ustar00rootroot00000000000000organize-3.3.0/tests/core/test_config.py000066400000000000000000000167241472111340300203110ustar00rootroot00000000000000import pytest from organize.config import Config, ConfigError def test_basic(): x = Config.from_string( """ rules: - locations: '~/' filters: - extension: - jpg - png - extension: txt actions: - move: dest: '~/New Folder' - echo: 'Moved {path}/{extension.upper()}' - locations: - path: '~/test1' ignore_errors: true actions: - shell: cmd: 'say {path.stem}' """ ) print(x) def test_yaml_ref(): x = Config.from_string( """ media: &media - wav - png all_folders: &all / "~" - "/" rules: - locations: *all filters: - extension: *media - extension: - *media - jpg - lastmodified: days: 10 actions: - echo: 'Hello World' - locations: - *all - path: /more/more ignore_errors: true actions: - trash """ ) print(x) def test_error_filter_dict(): STR = """ rules: - locations: '/' filters: extension: 'jpg' name: test actions: - trash """ with pytest.raises(ConfigError): print(Config.from_string(STR)) def test_error_action_dict(): config = """ rules: - locations: '/' filters: - extension: 'jpg' actions: Trash Echo """ with pytest.raises(ConfigError): Config.from_string(config) def test_empty_filters(): conf = """ rules: - locations: '/' filters: actions: - trash - locations: '~/' actions: - trash """ assert Config.from_string(conf) def test_issue352(): conf = """ all_my_messy__download_folders: &download_folders - D:/Windows/Downloads/Chrome - D:/Windows/Downloads/Chrome/Other rules: - name: "Delete Duplicate" locations: *download_folders filters: - duplicate: detect_original_by: "created" actions: - delete """ assert Config.from_string(conf) def test_flatten_filters_and_actions(): config = """ folder_aliases: Downloads: &downloads ~/Downloads/ Payables_due: &payables_due ~/PayablesDue/ Payables_paid: &payables_paid ~/Accounting/Expenses/ Receivables_due: &receivables_due ~/Receivables/ Receivables_paid: &receivables_paid ~/Accounting/Income/ defaults: filters: &default_filters - extension: pdf - filecontent: '(?P...)' actions: &default_actions - echo: 'Dated: {filecontent.date}' - echo: 'Stem of filename: {filecontent.stem}' post_actions: &default_sorting - rename: '{python.timestamp}-{filecontent.stem}.{extension.lower}' - move: '{path.parent}/{python.quarter}/' rules: - locations: *downloads filters: - *default_filters - filecontent: 'Due Date' # regex to id as payable - filecontent: '(?P...)' # regex to extract supplier actions: - *default_actions - move: *payables_due - *default_sorting - locations: *downloads filters: - *default_filters - filecontent: 'Account: 000000000' # regex to id as receivables due - filecontent: '(?P...)' # regex to extract customer actions: - *default_actions - move: *receivables_due - *default_sorting - locations: *downloads filters: - *default_filters - filecontent: 'PAID' # regex to id as receivables paid - filecontent: '(?P...)' # regex to extract customer - filecontent: '(?P...)' # regex to extract date paid - name: startswith: 2020 actions: - *default_actions - move: *receivables_paid - *default_sorting - rename: '{filecontent.paid}_{filecontent.stem}.{extension}' """ Config.from_string(config) # assert conf.rules == [ # Rule( # folders=["~/Downloads/"], # filters=[ # # default_filters # Extension("pdf"), # FileContent(expr="(?P...)"), # # added filters # FileContent(expr="Due Date"), # FileContent(expr="(?P...)"), # ], # actions=[ # # default_actions # Echo(msg="Dated: {filecontent.date}"), # Echo(msg="Stem of filename: {filecontent.stem}"), # # added actions # Move(dest="~/PayablesDue/", overwrite=False), # # default_sorting # Rename( # name="{python.timestamp}-{filecontent.stem}.{extension.lower}", # overwrite=False, # ), # Move(dest="{path.parent}/{python.quarter}/", overwrite=False), # ], # subfolders=False, # system_files=False, # ), # Rule( # folders=["~/Downloads/"], # filters=[ # # default_filters # Extension("pdf"), # FileContent(expr="(?P...)"), # # added filters # FileContent(expr="Account: 000000000"), # FileContent(expr="(?P...)"), # ], # actions=[ # # default_actions # Echo(msg="Dated: {filecontent.date}"), # Echo(msg="Stem of filename: {filecontent.stem}"), # # added actions # Move(dest="~/Receivables/", overwrite=False), # # default_sorting # Rename( # name="{python.timestamp}-{filecontent.stem}.{extension.lower}", # overwrite=False, # ), # Move(dest="{path.parent}/{python.quarter}/", overwrite=False), # ], # subfolders=False, # system_files=False, # ), # Rule( # folders=["~/Downloads/"], # filters=[ # # default_filters # Extension("pdf"), # FileContent(expr="(?P...)"), # # added filters # FileContent(expr="PAID"), # FileContent(expr="(?P...)"), # FileContent(expr="(?P...)"), # Filename(startswith="2020"), # ], # actions=[ # # default_actions # Echo(msg="Dated: {filecontent.date}"), # Echo(msg="Stem of filename: {filecontent.stem}"), # # added actions # Move(dest="~/Accounting/Income/", overwrite=False), # # default_sorting # Rename( # name="{python.timestamp}-{filecontent.stem}.{extension.lower}", # overwrite=False, # ), # Move(dest="{path.parent}/{python.quarter}/", overwrite=False), # # added actions # Rename( # name="{filecontent.paid}_{filecontent.stem}.{extension}", # overwrite=False, # ), # ], # subfolders=False, # system_files=False, # ), # ] organize-3.3.0/tests/core/test_filter_mode.py000066400000000000000000000024371472111340300213310ustar00rootroot00000000000000import pytest from conftest import make_files from organize import Config @pytest.mark.parametrize( "filter_mode, expected_msgs", ( ("any", ["foo", "x"]), ("all", ["foo"]), ("none", ["baz"]), ), ) def test_filter_mode(fs, testoutput, filter_mode, expected_msgs): make_files(["foo.txt", "baz.bar", "x.txt"], "test") config = f""" rules: - locations: /test filters: - name: foo - extension: txt filter_mode: {filter_mode} actions: - echo: "{{name}}" """ Config.from_string(config).execute(simulate=False, output=testoutput) assert testoutput.messages == expected_msgs @pytest.mark.parametrize( "filter_mode, expected_msgs", ( ("any", ["baz", "foo", "x"]), ("all", ["x"]), ("none", []), ), ) def test_filter_mode_not(fs, testoutput, filter_mode, expected_msgs): make_files(["foo.txt", "baz.bar", "x.txt"], "test") config = f""" rules: - locations: /test filters: - not name: foo - extension: txt filter_mode: {filter_mode} actions: - echo: "{{name}}" """ Config.from_string(config).execute(simulate=False, output=testoutput) assert testoutput.messages == expected_msgs organize-3.3.0/tests/core/test_ignore_seen_files.py000066400000000000000000000026301472111340300225120ustar00rootroot00000000000000import sys import pytest from conftest import make_files, read_files from organize import Config @pytest.mark.skipif(sys.platform == "win32", reason="Wrong path names in windows") def test_ignore_seen_files(fs): make_files( { "sub": {}, "foo.txt": "", "bar.txt": "", }, "test", ) config = """ rules: - locations: /test subfolders: true actions: - move: "{path.parent}/sub/{path.name}" """ Config.from_string(config).execute(simulate=False) result = read_files("test") assert result == { "sub": { "foo.txt": "", "bar.txt": "", } } def test_issue_200(fs): # https://github.com/tfeldmann/organize/issues/200 config = """ # try to extract the first date from the file and rename it accordingly rules: - name: date_rename locations: scan filters: - name - extension: txt - filecontent: '(?P[0123]\d)\.(?P[01]\d)\.(?P[12][09]\d\d)' actions: - rename: "{filecontent.y}-{filecontent.m}-{filecontent.d}_{name}.txt" """ make_files({"20220401_173738.txt": "Datum: 23.03.2022"}, "scan") Config.from_string(config).execute(simulate=False) result = read_files("/scan") assert result == { "2022-23-03_20220401_173738.txt": "Datum: 23.03.2022", } organize-3.3.0/tests/core/test_location.py000066400000000000000000000054111472111340300206430ustar00rootroot00000000000000from pathlib import Path from conftest import make_files from organize import Config def test_standalone(testoutput): Config.from_string( """ rules: - actions: - echo: "Do this" - echo: "And this" - actions: - echo: "And that" """ ).execute(simulate=False, output=testoutput) assert testoutput.messages == ["Do this", "And this", "And that"] assert testoutput.msg_report.success_count == 2 assert testoutput.msg_report.error_count == 0 def test_single_file(fs, testoutput): make_files(["foo.txt", "bar.txt"], "/test") Config.from_string( """ rules: - locations: - /test/foo.txt - /test/bar.txt actions: - echo: '{path.name}' """ ).execute(simulate=False, output=testoutput) assert testoutput.messages == ["foo.txt", "bar.txt"] def test_multiple_pathes(fs, testoutput): make_files(["foo.txt", "bar.txt"], "/test") make_files(["foo.txt", "bar.txt"], "/test2") Config.from_string( """ rules: - locations: - /test - /test2 actions: - echo: '{path.name}' """ ).execute(simulate=False, output=testoutput) assert testoutput.messages == ["bar.txt", "foo.txt"] * 2 def test_multiple_pathes_single_location(fs, testoutput): make_files(["foo.txt", "bar.txt"], "/test") make_files(["foo.txt", "bar.txt"], "/test2") Config.from_string( """ rules: - locations: - path: - /test - /test2 actions: - echo: '{path.name}' """ ).execute(simulate=False, output=testoutput) assert testoutput.messages == ["bar.txt", "foo.txt", "bar.txt", "foo.txt"] def test_multiple_dirs(fs, testoutput): make_files(["foo.txt", "bar.txt"], "/test") make_files(["foo.txt", "bar.txt"], "/test2") Config.from_string( """ rules: - locations: - path: - /test - /test2 targets: dirs actions: - echo: '{path.name}' """ ).execute(simulate=False, output=testoutput) assert testoutput.messages == [] def test_at_sign_in_path(fs): # https://github.com/tfeldmann/organize/issues/332 test_path = "/CloudStorage/ProtonDrive-xxxx@xxxx.xx/Documents.copy/" make_files(["foo.txt", "bar.txt"], "test") Config.from_string( f""" rules: - locations: test actions: - copy: '{test_path}' """ ).execute(simulate=False) assert (Path(test_path) / "foo.txt").exists() assert (Path(test_path) / "bar.txt").exists() organize-3.3.0/tests/core/test_relativepath.py000066400000000000000000000005061472111340300215230ustar00rootroot00000000000000from conftest import make_files from organize import Config def test_relative_path(fs): make_files(["test.txt"], "test") conf = """ rules: - locations: /test actions: - move: "/other/path/" - echo: "{relative_path}" """ Config.from_string(conf).execute(simulate=False) organize-3.3.0/tests/core/test_tags.py000066400000000000000000000025451472111340300177760ustar00rootroot00000000000000import pytest from organize.config import should_execute @pytest.mark.parametrize( "result, rule_tags, tags, skip_tags", ( # no tags given (True, None, None, None), (True, ["tag"], None, None), (True, ["tag", "tag2"], None, None), # run tagged (False, None, ["tag"], None), (True, ["tag"], ["tag"], None), (True, ["tag", "tag2"], ["tag"], None), (False, ["foo", "bar"], ["tag"], None), (False, ["taggity"], ["tag"], None), # skip (True, None, None, ["tag"]), (True, ["tag"], None, ["asd"]), (False, ["tag"], None, ["tag"]), (False, ["tag", "tag2"], None, ["tag"]), # combination (False, None, ["tag"], ["foo"]), (False, ["foo", "tag"], ["tag"], ["foo"]), # always (True, ["always"], ["debug", "test"], None), (True, ["always", "tag"], None, ["tag"]), # skip only if specifically requested (False, ["always", "tag"], None, ["always"]), # never (False, ["never"], None, None), (False, ["never", "tag"], ["tag"], None), # run only if specifically requested (True, ["never", "tag"], ["never"], None), ), ) def test_tags(result, rule_tags, tags, skip_tags): assert should_execute(rule_tags=rule_tags, tags=tags, skip_tags=skip_tags) == result organize-3.3.0/tests/core/test_unicode.py000066400000000000000000000051431472111340300204630ustar00rootroot00000000000000from pathlib import Path import pytest from conftest import make_files, read_files from organize import Config from organize.utils import normalize_unicode def test_startswith_issue74(fs): # test for issue https://github.com/tfeldmann/organize/issues/74 make_files( { "Cálculo_1.pdf": "", "Cálculo_2.pdf": "", "Calculo.pdf": "", }, "test", ) config = r""" # Cálculo PDF rules: - locations: /test filters: - extension: - pdf - name: startswith: Cálculo actions: - move: "/test/Cálculo Integral/Periodo #6/PDF's/" """ Config.from_string(config).execute(simulate=False) assert read_files("test") == { "Cálculo Integral": { "Periodo #6": { "PDF's": { "Cálculo_1.pdf": "", "Cálculo_2.pdf": "", } } }, "Calculo.pdf": "", } def test_folder_umlauts(fs): make_files(["file1", "file2"], "Erträge") conf = Path("config.yaml") conf.write_text( """ rules: - locations: "Erträge" actions: - delete """, encoding="utf-8", ) Config.from_path(conf).execute(simulate=False) assert read_files("Erträge") == {} CONFUSABLES = ( ( b"Ertr\xc3\xa4gnisaufstellung".decode("utf-8"), b"Ertra\xcc\x88gnisaufstellung".decode("utf-8"), ), ( b"Ertra\xcc\x88gnisaufstellung".decode("utf-8"), b"Ertr\xc3\xa4gnisaufstellung".decode("utf-8"), ), ) @pytest.mark.parametrize("a, b", CONFUSABLES) def test_normalize(a, b): assert a != b assert normalize_unicode(a) == normalize_unicode(b) @pytest.mark.parametrize("a, b", CONFUSABLES) def test_normalization_regex(fs, a, b): make_files({f"{a}.txt": ""}, "test") config = f""" rules: - locations: /test filters: - regex: {b} actions: - rename: "found-regex.txt" """ Config.from_string(config).execute(simulate=False) assert read_files("test") == {"found-regex.txt": ""} @pytest.mark.parametrize("a, b", CONFUSABLES) def test_normalization_filename(fs, a, b): make_files({f"{a}.txt": ""}, "test") config = f""" rules: - locations: /test filters: - name: {a} actions: - rename: "found-regex.txt" """ Config.from_string(config).execute(simulate=False) assert read_files("test") == {"found-regex.txt": ""} organize-3.3.0/tests/core/test_walker.py000066400000000000000000000111101472111340300203110ustar00rootroot00000000000000from collections import Counter from pathlib import Path import pytest from conftest import equal_items, make_files from pyfakefs.fake_filesystem import FakeFilesystem from organize.walker import Walker def counter(items): return Counter(str(x) for x in items) def test_location(fs): fs.create_file("test/folder/file.txt") fs.create_file("test/folder/subfolder/another.pdf") fs.create_file("test/hi/there") fs.create_file("test/hi/.other") fs.create_file("test/.hidden/some.pdf") assert equal_items( Walker().files("test"), [ Path("test/folder/file.txt"), Path("test/folder/subfolder/another.pdf"), Path("test/hi/there"), Path("test/hi/.other"), Path("test/.hidden/some.pdf"), ], ) assert equal_items( Walker(method="depth").files("test"), [ Path("test/folder/subfolder/another.pdf"), Path("test/folder/file.txt"), Path("test/hi/there"), Path("test/hi/.other"), Path("test/.hidden/some.pdf"), ], ) @pytest.mark.parametrize("method", ("depth", "breadth")) def test_walk(fs: FakeFilesystem, method): fs.create_file("/test/d1/f1.txt") fs.create_file("/test/d1/d1/f1.txt") fs.create_file("/test/d1/d1/f2.txt") fs.create_file("/test/d1/d1/d1/f1.txt") fs.create_file("/test/f1.txt") fs.create_dir("/test/d1/d2") fs.create_dir("/test/d1/d3") fs.create_dir("/test/d1/d1/d2") assert equal_items( Walker(method=method).files("/test"), [ Path("/test/d1/f1.txt"), Path("/test/d1/d1/f1.txt"), Path("/test/d1/d1/f2.txt"), Path("/test/d1/d1/d1/f1.txt"), Path("/test/f1.txt"), ], ) assert equal_items( Walker(method=method, min_depth=1).files("/test/"), [ Path("/test/d1/f1.txt"), Path("/test/d1/d1/f1.txt"), Path("/test/d1/d1/f2.txt"), Path("/test/d1/d1/d1/f1.txt"), ], ) assert equal_items( Walker(method=method, min_depth=1, max_depth=2).files("/test/"), [ Path("/test/d1/f1.txt"), Path("/test/d1/d1/f1.txt"), Path("/test/d1/d1/f2.txt"), ], ) assert equal_items( Walker(method=method, min_depth=2, max_depth=2).files("/test/"), [ Path("/test/d1/d1/f1.txt"), Path("/test/d1/d1/f2.txt"), ], ) # dirs assert equal_items( Walker(method=method).dirs("/test/"), [ Path("/test/d1"), Path("/test/d1/d1"), Path("/test/d1/d1/d1"), Path("/test/d1/d2"), Path("/test/d1/d3"), Path("/test/d1/d1/d2"), ], ) assert equal_items( Walker(method=method, min_depth=1).dirs("/test/"), [ Path("/test/d1/d1"), Path("/test/d1/d1/d1"), Path("/test/d1/d2"), Path("/test/d1/d3"), Path("/test/d1/d1/d2"), ], ) assert equal_items( Walker(method=method, min_depth=1, max_depth=2).dirs("/test/"), [ Path("/test/d1/d1"), Path("/test/d1/d1/d1"), Path("/test/d1/d2"), Path("/test/d1/d3"), Path("/test/d1/d1/d2"), ], ) assert equal_items( Walker(method=method, min_depth=2, max_depth=2).dirs("/test/"), [ Path("/test/d1/d1/d1"), Path("/test/d1/d1/d2"), ], ) def test_exclude_dirs(fs): make_files( { "subA": {"file.a": "", "file.b": ""}, "subB": { "subC": {"file.ca": "", "file.cb": ""}, "file.ba": "", "file.bb": "", }, }, "test", ) assert len(list(Walker(exclude_dirs=["subC"]).files("/test"))) == 4 def test_order(fs): make_files( { "2024": {"004": "", "001": "", "003": "", "002": ""}, "1989": {"D": "", "C": "", "A": "", "B": ""}, "2000": {"B": {"2": "", "1": ""}, "A": {"1": "", "2": ""}}, }, "test", ) assert list(Walker().files("/test")) == [ Path("/test/1989/A"), Path("/test/1989/B"), Path("/test/1989/C"), Path("/test/1989/D"), Path("/test/2000/A/1"), Path("/test/2000/A/2"), Path("/test/2000/B/1"), Path("/test/2000/B/2"), Path("/test/2024/001"), Path("/test/2024/002"), Path("/test/2024/003"), Path("/test/2024/004"), ] organize-3.3.0/tests/filters/000077500000000000000000000000001472111340300161415ustar00rootroot00000000000000organize-3.3.0/tests/filters/__init__.py000066400000000000000000000000001472111340300202400ustar00rootroot00000000000000organize-3.3.0/tests/filters/test_created.py000066400000000000000000000013701472111340300211620ustar00rootroot00000000000000from datetime import datetime, timedelta from arrow import now as arrow_now from organize.filters import Created from organize.filters.created import read_created def test_min(): now = arrow_now() ct = Created(days=10, hours=12, mode="older") assert not ct.matches_datetime(now - timedelta(days=10, hours=0)) assert ct.matches_datetime(now - timedelta(days=10, hours=13)) def test_max(): now = arrow_now() ct = Created(days=10, hours=12, mode="newer") assert ct.matches_datetime(now - timedelta(days=10, hours=0)) assert not ct.matches_datetime(now - timedelta(days=10, hours=13)) def test_read_created(tmp_path): f = tmp_path / "file.txt" f.touch() assert read_created(f).date() == datetime.utcnow().date() organize-3.3.0/tests/filters/test_duplicate.py000066400000000000000000000034631472111340300215320ustar00rootroot00000000000000from conftest import make_files, read_files from organize import Config CONTENT_SMALL = "COPY CONTENT" CONTENT_LARGE = "XYZ" * 300000 CONFIG_DEEP_DUP_DELETE = """ rules: - locations: "." subfolders: true filters: - duplicate: detect_original_by: name actions: - delete """ def test_duplicate_smallfiles(fs): files = { "unique.txt": "I'm unique.", "unique_too.txt": "I'm unique: too.", "a.txt": CONTENT_SMALL, "copy2.txt": CONTENT_SMALL, "other": { "copy.txt": CONTENT_SMALL, "copy.jpg": CONTENT_SMALL, "large.txt": CONTENT_LARGE, }, "large_unique.txt": CONTENT_LARGE, } make_files(files, "test") Config.from_string(CONFIG_DEEP_DUP_DELETE).execute(simulate=False) result = read_files("test") assert result == { "unique.txt": "I'm unique.", "unique_too.txt": "I'm unique: too.", "a.txt": CONTENT_SMALL, "other": { "large.txt": CONTENT_LARGE, }, } def test_duplicate_largefiles(fs): files = { "unique.txt": CONTENT_LARGE + "1", "unique_too.txt": CONTENT_LARGE + "2", "a.txt": CONTENT_LARGE, "copy2.txt": CONTENT_LARGE, "other": { "copy.txt": CONTENT_LARGE, "copy.jpg": CONTENT_LARGE, "large.txt": CONTENT_LARGE, }, } make_files(files, "test") Config.from_string(CONFIG_DEEP_DUP_DELETE).execute(simulate=False) result = read_files("test") assert result == { "unique.txt": CONTENT_LARGE + "1", "unique_too.txt": CONTENT_LARGE + "2", "a.txt": CONTENT_LARGE, "other": {}, } # TODO detect_original_by: first_seen # TODO detect_original_by: created # TODO detect_original_by: lastmodified organize-3.3.0/tests/filters/test_exif.py000066400000000000000000000066171472111340300205170ustar00rootroot00000000000000from pathlib import Path import pytest from conftest import ORGANIZE_DIR from pyfakefs.fake_filesystem import FakeFilesystem from organize import Config from organize.filters.exif import matches_tags @pytest.fixture def images_folder(fs: FakeFilesystem): RESOURCE_DIR = str(ORGANIZE_DIR / "tests" / "resources" / "images-with-exif") fs.add_real_directory( source_path=RESOURCE_DIR, target_path="/resources", read_only=True, ) return fs def test_matches_tags(): data = { "image": { "make": "NIKON CORPORATION", "model": "NIKON D3200", }, "exif": { "flash": "Flash did not fire", "saturation": "Normal", }, "gps": { "gpsspeed": "0", }, } assert matches_tags({"image.make": "NIKON CORPORATION"}, data) assert matches_tags({"image.make": "NIKON *"}, data) assert not matches_tags({"image.make": "Apple"}, data) assert matches_tags({"gps": None}, data) assert not matches_tags({"other": None}, data) assert matches_tags({"exif.flash": "Flash did not fire"}, data) def test_exif_read_camera(images_folder): config = """ rules: - locations: "/resources" filters: - name - exif: image.make: Apple actions: - write: outfile: "/test/out.txt" text: '{name}: {exif.image.model}' mode: append """ Config.from_string(config).execute(simulate=False) out = Path("/test/out.txt").read_text() assert "1: DMC-GX80" not in out assert "2: NIKON D3200" not in out assert "3: iPhone 6s" in out assert "4: iPhone 6s" in out assert "5: iPhone 5s" in out def test_exif_filter_by_cam(images_folder): config = """ rules: - locations: "/resources" filters: - name - exif: image.model: Nikon D3200 actions: - write: outfile: "/test/out.txt" text: '{name}: {exif.image.model}' mode: append """ Config.from_string(config).execute(simulate=False) out = Path("/test/out.txt").read_text() assert out.strip() == "2: NIKON D3200" def test_exif_filter_tag_exists_and_date_format(images_folder): config = """ rules: - locations: "/resources" filters: - name - exif: gps.gpsdate actions: - write: outfile: "/test/out.txt" text: '{name}: {exif.gps.gpsdate.strftime("%d.%m.%Y")}' mode: append """ Config.from_string(config).execute(simulate=False) out = Path("/test/out.txt").read_text() assert set(out.splitlines()) == set( [ "3: 12.08.2017", "4: 22.02.2018", "5: 08.07.2015", ] ) def test_exif_filter_by_multiple_keys(images_folder): config = """ rules: - locations: "/resources" filters: - name - exif: image.make: Apple exif.lensmodel: "iPhone 6s back camera 4.15mm f/2.2" actions: - move: "/chosen/" """ Config.from_string(config).execute(simulate=False) chosen = set(str(x.name) for x in Path("/chosen").glob("*")) assert chosen == set(["3.jpg", "4.jpg"]) organize-3.3.0/tests/filters/test_extension.py000066400000000000000000000026261472111340300215740ustar00rootroot00000000000000from pathlib import Path import pytest from conftest import make_files from organize import Config from organize.filters.extension import Extension @pytest.mark.parametrize( "path,match,suffix", ( ("/somefile.pdf", True, "pdf"), ("/home/test/somefile.pdf.jpeg", False, "jpeg"), ("/home/test/gif.TXT", False, "txt"), ("/home/test/txt.GIF", True, "gif"), ("/somefile.pdf", True, "pdf"), ), ) def test_extension(path, match, suffix): extension = Extension(["JPG", ".gif", "pdf"]) suffix, match = extension.suffix_match(Path(path)) assert match == match assert suffix == suffix def test_extension_empty(): suffix, match = Extension().suffix_match(Path("any.txt")) assert suffix == "txt" assert match def test_filename_move(fs, testoutput): files = { "test.jpg": "", "asd.JPG": "", "nomatch.jpg.zip": "", "camel.jPeG": "", } make_files(files, "test") config = """ rules: - locations: /test filters: - name - extension: - .jpg - jpeg actions: - echo: 'Found JPG file: {name}' """ Config.from_string(config).execute(simulate=False, output=testoutput) assert testoutput.messages == [ "Found JPG file: asd", "Found JPG file: camel", "Found JPG file: test", ] organize-3.3.0/tests/filters/test_filecontent.py000066400000000000000000000020271472111340300220650ustar00rootroot00000000000000from conftest import make_files, read_files from organize import Config def test_filecontent(fs): # inspired by https://github.com/tfeldmann/organize/issues/43 files = { "Test1.txt": "Lorem MegaCorp Ltd. ipsum\nInvoice 12345\nMore text\nID: 98765", "Test2.txt": "Tests", "Test3.txt": "My Homework ...", } make_files(files, "test") Config.from_string( r""" rules: - locations: "/test" filters: - filecontent: 'MegaCorp Ltd.+^Invoice (?P\w+)$.+^ID: 98765$' actions: - rename: "MegaCorp_Invoice_{filecontent.number}.txt" - locations: "/test" filters: - filecontent: '.*Homework.*' actions: - rename: "Homework.txt" """ ).execute(simulate=False) assert read_files("test") == { "Homework.txt": "My Homework ...", "MegaCorp_Invoice_12345.txt": "Lorem MegaCorp Ltd. ipsum\nInvoice 12345\nMore text\nID: 98765", "Test2.txt": "Tests", } organize-3.3.0/tests/filters/test_hash.py000066400000000000000000000035531472111340300205030ustar00rootroot00000000000000from pathlib import Path from conftest import make_files from organize import Config from organize.filters.hash import hash, hash_first_chunk def test_full_hash(fs): r""" Reference hashsums: ```sh python3 -c 'from pathlib import Path; Path("hello.txt").write_text("Hello world")' md5 hello.txt && shasum -a1 hello.txt && shasum -a256 hello.txt ``` Do not use newlines in the textfiles. In windows a newline is \r\n, unix: \n. """ hello = Path("hello.txt") hello.write_text("Hello world") assert hash(hello, algo="md5") == "3e25960a79dbc69b674cd4ec67a72c62" assert hash(hello, algo="sha1") == "7b502c3a1f48c8609ae212cdfb639dee39673f5e" assert ( hash(hello, algo="sha256") == "64ec88ca00b268e5ba1a35678a1b5316d212f4f366b2477232534a8aeca37f3c" ) def test_first_chunk(fs): hello = Path("hello.txt") hello.write_text("Hello world") hash_hello = hash_first_chunk(hello, algo="md5") assert hash_hello == "3e25960a79dbc69b674cd4ec67a72c62" long_asd = Path("long_asd.txt") long_asd.write_text("asd" * 10000) hash_asd = hash_first_chunk(long_asd, algo="sha1") long_foo = Path("long_foo.txt") long_foo.write_text("foo" * 10000) hash_foo = hash_first_chunk(long_foo, algo="sha1") assert hash_asd != hash_foo != hash_hello # make sure only the first chunk is used long_foo.write_text("foo" * 10001) assert hash_foo == hash_first_chunk(long_foo, algo="sha1") def test_hash(fs, testoutput): make_files({"hello.txt": "Hello world"}, "test") Config.from_string( """ rules: - locations: /test filters: - hash actions: - echo: "File hash: {hash}" """ ).execute(simulate=False, output=testoutput) assert testoutput.messages == ["File hash: 3e25960a79dbc69b674cd4ec67a72c62"] organize-3.3.0/tests/filters/test_lastmodified.py000066400000000000000000000037721472111340300222270ustar00rootroot00000000000000import os from datetime import date, datetime, timedelta from arrow import now as arrow_now from conftest import make_files, read_files from organize import Config from organize.filters import LastModified def test_min(): now = arrow_now() lm = LastModified(days=10, hours=12, mode="older") assert not lm.matches_datetime(now - timedelta(days=10, hours=0)) assert lm.matches_datetime(now - timedelta(days=10, hours=13)) def test_max(): now = arrow_now() lm = LastModified(days=10, hours=12, mode="newer") assert lm.matches_datetime(now - timedelta(days=10, hours=0)) assert not lm.matches_datetime(now - timedelta(days=10, hours=13)) def test_photo_sorting(fs): make_files(["photo1", "photo2", "photo3"], "test") conf = """ rules: - locations: /test subfolders: true filters: - lastmodified actions: - move: dest: "/pics/{lastmodified.strftime('%Y/%m/%d')}/" on_conflict: skip """ d1 = datetime(2000, 1, 12).timestamp() d2 = datetime(2020, 1, 1).timestamp() os.utime("/test/photo1", times=(d1, d1)) os.utime("/test/photo2", times=(d2, d2)) os.utime("/test/photo3", times=(d2, d2)) Config.from_string(conf).execute(simulate=False) assert read_files("/pics") == { "2000": {"01": {"12": {"photo1": ""}}}, "2020": {"01": {"01": {"photo2": "", "photo3": ""}}}, } def test_date_formatting(fs, testoutput): make_files(["test.txt"], "/test") config = """ rules: - locations: /test filters: - lastmodified actions: - echo: "moving to {lastmodified.strftime('%Y%m%d - test.txt')}" - move: "/test/moved/{lastmodified.strftime('%Y%m%d - test.txt')}" """ Config.from_string(config).execute(simulate=False, output=testoutput) new_name = f"{date.today():%Y%m%d} - test.txt" testoutput.messages == [f"Created at {new_name}"] assert read_files("/test/moved") == {new_name: ""} organize-3.3.0/tests/filters/test_macos_tags.py000066400000000000000000000043731472111340300217010ustar00rootroot00000000000000import sys import pytest from organize import Config from organize.filters.macos_tags import list_tags, matches_tags def test_macos_tags_matching(): filter_tags = ("Invoice (*)", "* (red)", "Test (green)") assert matches_tags(filter_tags=filter_tags, file_tags=["Invoice (none)"]) assert matches_tags(filter_tags=filter_tags, file_tags=["Invoice (green)"]) assert not matches_tags(filter_tags=filter_tags, file_tags=["Voice (green)"]) assert matches_tags(filter_tags=filter_tags, file_tags=["Voice (red)"]) assert not matches_tags(filter_tags=filter_tags, file_tags=["Test (blue)"]) assert matches_tags(filter_tags=filter_tags, file_tags=["Test (green)"]) assert matches_tags(filter_tags=["Invoice (red)"], file_tags=["Invoice (red)"]) assert matches_tags( filter_tags=("Invoice (red)", "* (green)"), file_tags=["Invoice (red)"], ) assert matches_tags(["Invoice (red)", "* (green)"], ["Urgent (green)"]) assert matches_tags(["Invoice (red)", "* (green)"], ["Invoice (red)"]) assert not matches_tags(["Invoice (red)", "* (green)"], ["Pictures (blue)"]) @pytest.mark.skipif(sys.platform != "darwin", reason="runs only on macOS") def test_macos_filter(tmp_path, testoutput): import macos_tags testdir = tmp_path / "test" testdir.mkdir() invoice = testdir / "My-Invoice.pdf" invoice.touch() macos_tags.add(macos_tags.Tag("Invoice", macos_tags.Color.RED), file=invoice) assert list_tags(invoice) == ["Invoice (red)"] another = testdir / "Another-File.pdf" another.touch() macos_tags.add(macos_tags.Tag("Urgent", macos_tags.Color.GREEN), file=another) assert list_tags(another) == ["Urgent (green)"] pic = testdir / "Pic.jpg" pic.touch() macos_tags.add(macos_tags.Tag("Pictures", macos_tags.Color.BLUE), file=pic) assert list_tags(pic) == ["Pictures (blue)"] Config.from_string( f""" rules: - locations: {testdir} filters: - macos_tags: - "Invoice (red)" - "* (green)" actions: - echo: "{{','.join(macos_tags)}}" """ ).execute(simulate=False, output=testoutput) assert set(testoutput.messages) == set(["Invoice (red)", "Urgent (green)"]) organize-3.3.0/tests/filters/test_mimetype.py000066400000000000000000000023151472111340300214040ustar00rootroot00000000000000from conftest import make_files, read_files from organize import Config def test_mimetype(fs): make_files({"test.pdf": "", "other.jpg": ""}, "test") Config.from_string( """ rules: - locations: /test filters: - mimetype actions: - move: /test/{mimetype}/ """ ).execute(simulate=False) assert read_files("test") == { "application": { "pdf": { "test.pdf": "", }, }, "image": { "jpeg": { "other.jpg": "", }, }, } def test_mimetype_filter(fs): make_files({"test.pdf": "", "other.jpg": "", "other2.png": ""}, "test") Config.from_string( """ rules: - locations: /test filters: - mimetype: - "image" actions: - move: /test/{mimetype}/ """ ).execute(simulate=False) assert read_files("test") == { "test.pdf": "", "image": { "jpeg": { "other.jpg": "", }, "png": { "other2.png": "", }, }, } organize-3.3.0/tests/filters/test_name.py000066400000000000000000000066241472111340300205020ustar00rootroot00000000000000from conftest import make_files, read_files from organize import Config from organize.filters import Name def test_name_startswith(): name = Name(startswith="begin") assert name.matches("beginhere") assert not name.matches(".beginhere") assert not name.matches("herebegin") def test_name_contains(): name = Name(contains="begin") assert name.matches("beginhere") assert name.matches(".beginhere") assert name.matches("herebegin") assert not name.matches("other") def test_name_endswith(): name = Name(endswith="end") assert name.matches("hereend") assert name.matches("end") assert not name.matches("theendishere") def test_name_multiple(): name = Name(startswith="begin", contains="con", endswith="end") assert name.matches("begin_somethgin_con_end") assert not name.matches("beginend") assert not name.matches("begincon") assert not name.matches("conend") assert name.matches("beginconend") def test_name_case(): name = Name(startswith="star", contains="con", endswith="end", case_sensitive=False) assert name.matches("STAR_conEnD") assert not name.matches("STAREND") assert not name.matches("STARCON") assert not name.matches("CONEND") assert name.matches("STARCONEND") def test_name_list(): name = Name( startswith="_", contains=["1", "A", "3", "6"], endswith=["5", "6"], case_sensitive=False, ) assert name.matches("_15") assert name.matches("_A5") assert name.matches("_A6") assert name.matches("_a6") assert name.matches("_35") assert name.matches("_36") assert name.matches("_somethinga56") assert name.matches("_6") assert not name.matches("") assert not name.matches("a_5") def test_name_list_case_sensitive(): name = Name( startswith="_", contains=["1", "A", "3", "7"], endswith=["5", "6"], case_sensitive=True, ) assert name.matches("_15") assert name.matches("_A5") assert name.matches("_A6") assert not name.matches("_a6") assert name.matches("_35") assert name.matches("_36") assert name.matches("_somethingA56") assert not name.matches("_6") assert not name.matches("_a5") assert not name.matches("-A5") assert not name.matches("") assert not name.matches("_a5") def test_name_match(fs): filename = "Invoice_RE1001_2021_01_31.txt" make_files([filename], "test") Config.from_string( """ rules: - locations: /test filters: - name: "Invoice_*_{year:int}_{month}_{day}" actions: - move: "/test/{name.year}/{name.month}/{name.day}/" """ ).execute(simulate=False) assert read_files("test") == {"2021": {"01": {"31": {filename: ""}}}} def test_name_match_case_insensitive(fs): filename = "UPPER_MiXed_lower.txt" make_files([filename], "test") Config.from_string( """ rules: - locations: /test filters: - name: match: "upper_{m1}_{m2}" case_sensitive: true - name: match: "upper_{m1}_{m2}" case_sensitive: false filter_mode: any actions: - move: "/test/{name.m1}/{name.m2}/" """ ).execute(simulate=False) assert read_files("test") == {"MiXed": {"lower": {filename: ""}}} organize-3.3.0/tests/filters/test_python.py000066400000000000000000000140141472111340300210730ustar00rootroot00000000000000from conftest import make_files, read_files from organize import Config def test_python(fs, testoutput): make_files( ["student-01.jpg", "student-01.txt", "student-02.txt", "student-03.txt"], "test", ) config = """ rules: - locations: /test filters: - name - extension: txt - python: | return int(name.split('.')[0][-2:]) * 100 actions: - echo: '{python}' """ Config.from_string(config).execute(simulate=False, output=testoutput) assert testoutput.messages == [ "100", "200", "300", ] def test_odd_detector(fs, testoutput): make_files( ["student-01.txt", "student-02.txt", "student-03.txt", "student-04.txt"], "test", ) config = """ rules: - locations: /test filters: - name - python: | return int(name.split('-')[1]) % 2 == 1 actions: - echo: 'Odd student numbers: {name}' """ Config.from_string(config).execute(simulate=False, output=testoutput) assert testoutput.messages == [ "Odd student numbers: student-01", "Odd student numbers: student-03", ] def test_python_dict(fs, testoutput): make_files( ["foo-01.jpg", "foo-01.txt", "bar-02.txt", "baz-03.txt"], "test", ) config = """ rules: - locations: /test filters: - extension: txt - name - python: | return { "name": name[:3], "code": int(name.split('.')[0][-2:]) * 100, } actions: - echo: '{python.code} {python.name}' """ Config.from_string(config).execute(simulate=False, output=testoutput) assert testoutput.messages == [ "200 bar", "300 baz", "100 foo", ] def test_name_reverser(fs): make_files(["desrever.jpg", "emanelif.txt"], "test") config = """ rules: - locations: /test filters: - extension - name - python: | return { "reversed_name": name[::-1], } actions: - rename: '{python.reversed_name}.{extension}' """ Config.from_string(config).execute(simulate=False) assert read_files("test") == { "reversed.jpg": "", "filename.txt": "", } def test_folder_instructions(fs): """ I would like to include path/folder-instructions into the filename because I have a lot of different files (and there are always new categories added) I don't want create rules for. For example my filename is '2019_Jobs_CategoryA_TagB_A-Media_content-name_V01_draft_eng.docx' which means: Move the file to the folder '2019/Jobs/CategoryA/TagB/Media/drafts/eng' whereby 'A-' is an additional instruction and should be removed from the filename afterwards ('2019_Jobs_CategoryA_TagB_content-name_V01_draft_eng.docx'). I have a rough idea to figure it out with python but I'm new to it (see below a sketch). Is there a possibility to use such variables, conditions etc. with organizer natively? If no, is it possible to do it with Python in Organizer at all? - Transform file-string into array - Search for 'A-...', 'V...' and 'content-name' and get index of values - remove value 'A-... and 'content-name' of array - build new filename string - remove value 'V...' and 'A-' of array - build folder-path string (convert _ to /) etc. """ # inspired by: https://github.com/tfeldmann/organize/issues/52 make_files( [ "2019_Jobs_CategoryA_TagB_A-Media_content-name_V01_draft_eng.docx", "2019_Work_CategoryC_V-Test_A-Audio_V14_final.pdf", "other.pdf", ], "test", ) config = r""" rules: - locations: /test filters: - extension: - pdf - docx - name: contains: "_" - python: | import os parts = ["/test"] instructions = dict() for part in name.split("_"): if part.startswith("A-"): instructions["A"] = part[2:] elif part.startswith("V-"): instructions["V"] = part[2:] elif part.startswith("content-name"): instructions["content"] = part[12:] else: parts.append(part) return { "new_path": os.path.join(*parts), "instructions": instructions, } actions: - echo: "New path: {python.new_path}" - echo: "Instructions: {python.instructions}" - echo: "Value of A: {python.instructions.A}" - move: "{python.new_path}/{name}.{extension}" """ Config.from_string(config).execute(simulate=False) assert read_files("test") == { "other.pdf": "", "2019": { "Jobs": { "CategoryA": { "TagB": { "V01": { "draft": { "eng": { "2019_Jobs_CategoryA_TagB_A-Media_content-name_V01_draft_eng.docx": "", } } } } } }, "Work": { "CategoryC": { "V14": { "final": { "2019_Work_CategoryC_V-Test_A-Audio_V14_final.pdf": "", } } } }, }, } organize-3.3.0/tests/filters/test_regex.py000066400000000000000000000051001472111340300206600ustar00rootroot00000000000000from pathlib import Path import pytest from conftest import make_files, read_files from pyfakefs.fake_filesystem import FakeFilesystem from organize import Config from organize.filters import Regex from organize.output import Default from organize.resource import Resource def test_regex_backslash(): regex = Regex(r"^\.pdf$") assert regex.matches(".pdf") assert not regex.matches("+pdf") assert not regex.matches("/pdf") assert not regex.matches("\\pdf") @pytest.mark.parametrize( "path,valid,test_result", ( ("RG123456123456-sig.pdf", True, "123456123456"), ("RG002312321542-sig.pdf", True, "002312321542"), ("RG002312321542.pdf", False, None), ), ) def test_regex_return(path, valid, test_result): regex = Regex(r"^RG(?P\d{12})-sig\.pdf$") assert bool(regex.matches(path)) == valid res = Resource(path=Path(path)) if valid: regex.pipeline(res=res, output=Default) assert res.vars == {"regex": {"the_number": test_result}} def test_regex_umlaut(): regex = Regex(r"^Erträgnisaufstellung-(?P\d*)\.pdf") doc = "Erträgnisaufstellung-1998.pdf" assert regex.matches(doc) def test_multiple_regex_placeholders(fs: FakeFilesystem): make_files(["test-123.jpg", "other-456.pdf"], "test") config = """ rules: - locations: /test/ filters: - regex: (?P\w+)-(?P\d+).* - regex: (?P.+?)\.\w{3} - extension actions: - write: text: '{regex.word} {regex.number} {regex.all} {extension}' outfile: /test/out.txt """ Config.from_string(config).execute(simulate=False) out = Path("/test/out.txt").read_text() assert "test 123 test-123 jpg" in out assert "other 456 other-456 pdf" in out def test_podcast_sorting(fs: FakeFilesystem): make_files( [ "My Podcast Ep 1.mp3", "My Podcast Ep 2.mp3", "My Podcast Ep 3.mp3", "Your Podcast Ep 1.mp3", "Your Podcast Ep 2.mp3", "Your Podcast Ep 3.mp3", ], "test", ) config = r""" rules: - locations: /test/ filters: - regex: '^(?P.+?) (?PEp \d+\.mp3)' actions: - move: '/test/{regex.podcast}/{regex.episode}' """ Config.from_string(config).execute(simulate=False) assert read_files("test") == { "My Podcast": {"Ep 1.mp3": "", "Ep 2.mp3": "", "Ep 3.mp3": ""}, "Your Podcast": {"Ep 1.mp3": "", "Ep 2.mp3": "", "Ep 3.mp3": ""}, } organize-3.3.0/tests/filters/test_size.py000066400000000000000000000052031472111340300205240ustar00rootroot00000000000000import pytest from conftest import make_files, read_files from organize import Config from organize.filters import Size def test_constrains_mope1(): assert not Size("<1b,>2b").matches(1) assert Size(">=1b,<2b").matches(1) assert not Size(">1.000001b").matches(1) assert Size("<1.000001B").matches(1) assert Size("<1.000001").matches(1) assert Size("<=1,>=0.001kb").matches(1) assert Size("<1").matches(0) assert not Size(">1").matches(0) assert not Size("<1,>1b").matches(0) assert Size(">99.99999GB").matches(100000000000) assert Size("0").matches(0) def test_constrains_base(): assert Size(">1kb,<1kib").matches(1010) assert Size(">1k,<1ki").matches(1010) assert Size("1k").matches(1000) assert Size("1000").matches(1000) def test_other(): assert Size("<100 Mb").matches(20) assert Size("<100 Mb, <10 mb, <1 mb, > 0").matches(20) assert Size(["<100 Mb", ">= 0 Tb"]).matches(20) def test_size_zero(fs): make_files(["1", "2", "3"], "test") config = """ rules: - locations: "test" filters: - size: 0 actions: - echo: '{path.name}' - delete """ Config.from_string(config).execute(simulate=False) assert read_files("test") == {} def test_basic(fs): files = { "empty": "", "full": "0" * 2000, "halffull": "0" * 1010, "two_thirds.txt": "0" * 666, } make_files(files, "test") config = """ rules: - locations: "test" filters: - size: '> 1kb, <= 1.0 KiB' actions: - echo: '{path.name} {size.bytes}' - locations: "test" filters: - not size: - '> 0.5 kb' - '<1.0 KiB' actions: - delete """ Config.from_string(config).execute(simulate=False) assert read_files("test") == { "halffull": "0" * 1010, "two_thirds.txt": "0" * 666, } @pytest.mark.skip(reason="TODO - template vars in filters not supported") def test_python_args(testfs): make_files( testfs, { "empty": "", "full": "0" * 2000, "halffull": "0" * 1010, "two_thirds.txt": "0" * 666, }, ) config = """ rules: - folders: files filters: - python: | return 2000 - filesize: '= {python}b' actions: - delete """ Config.from_string(config).execute(simulate=False) assert read_files(testfs) == { "empty": "", "halffull": "0" * 1010, "two_thirds.txt": "0" * 666, } organize-3.3.0/tests/resources/000077500000000000000000000000001472111340300165035ustar00rootroot00000000000000organize-3.3.0/tests/resources/images-with-exif/000077500000000000000000000000001472111340300216525ustar00rootroot00000000000000organize-3.3.0/tests/resources/images-with-exif/1.jpg000066400000000000000000000113351472111340300225170ustar00rootroot00000000000000JFIFExifMM*  z (1 2iPanasonicDMC-GX80Ver.1.2 2018:09:29 18:52:46%~"'00230 ƒΒ  ֒0520520520100ޤ2  P 2018:09:29 18:52:462018:09:29 18:52:46+01:001   http://ns.adobe.com/xap/1.0/ xPhotoshop 3.08BIM?Z%G?185246>20180929720180929<1852468BIM%F0e^֔:0u0@ICC_PROFILE0ADBEmntrRGB XYZ  3;acspAPPLnone-ADBE cprt2desc0kwtptbkptrTRCgTRCbTRCrXYZgXYZbXYZtextCopyright 2000 Adobe Systems IncorporateddescAdobe RGB (1998)XYZ QXYZ curv3curv3curv3XYZ OXYZ 4,XYZ &1/! }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyzCC ?(organize-3.3.0/tests/resources/images-with-exif/2.jpg000077500000000000000000000104361472111340300225240ustar00rootroot00000000000000JFIFHH(ExifMM* z (1 2iNIKON CORPORATIONNIKON D3200HHVer.1.00 2018:04:29 16:24:21&"'d00230̒ Ԓܒ  䒆,쒐2020200100K    2018:04:29 16:24:212018:04:29 16:24:212ASCII  http://ns.adobe.com/xap/1.0/ xPhotoshop 3.08BIM?Z%G?162421>20180429720180429<1624218BIM%c60p9-nǬ5! }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyzCC ?(organize-3.3.0/tests/resources/images-with-exif/3.jpg000066400000000000000000000122051472111340300225160ustar00rootroot00000000000000JFIFHHrExifMM*  (12iˆ%4AppleiPhone 6sHH10.3.32017:08:12 12:23:36 HP"'0221Xl     |:6566560100  23 4# 2017:08:12 12:23:362017:08:12 12:23:36By e 0IS2Apple iOSMM  .h     "  bplist00Ok)E;V*}Yz&=>I{iD-^O,I]Luyr:JA{}~p=BQuQyzz|~{JG{9{pzz{|eER@~|{{}~T@L~}}~SPUlMgYXLtV[rF1 $f=,]O*+B0+qlWt  + C#-  -+ !D./  #3  3 bplist00UflagsUvalueUepochYtimescale4]pz;#-/8: ?Z?Xd}!2!2  AppleiPhone 6s back camera 4.15mm f/2.2NE& K >TFTN Vb-4d 5d_ $D9D92017:08:12 http://ns.adobe.com/xap/1.0/ xPhotoshop 3.08BIM?Z%G?122336>20170812720170812<1223368BIM%/ZVsD " }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyzC C   ? (D>|organize-3.3.0/tests/resources/images-with-exif/4.jpg000066400000000000000000000123451472111340300225240ustar00rootroot00000000000000JFIFHHExifMM*  (12iˆ%dAppleiPhone 6sHH11.2.62018:02:22 09:34:41 HP"' 0221Xl     |j843843010023:4#@! 2018:02:22 09:34:412018:02:22 09:34:41/ LcS2Apple iOSMM  .h     R      bplist00O      +!  [O>1! rm^QEPNBA4 &, 3:>)LRKOG3kXy||MQ[`?jN7Pahkpt bplist00UflagsUvalueYtimescaleUepoch$ج;'-/8= ?`25(=!2!2  AppleiPhone 6s back camera 4.15mm f/2.2NE6NV K nTvT~ 30d!Sd"(w2fUfU2018:02:22  http://ns.adobe.com/xap/1.0/ xPhotoshop 3.08BIM?Z%G?093441>20180222720180222<0934418BIM%R*M ]M Z" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyzC C   ?IҬvw{xJI>>y=[ c^̻= #organize-3.3.0/tests/resources/images-with-exif/5.jpg000066400000000000000000000137361472111340300225320ustar00rootroot00000000000000JFIFHHbExifMM*  (18.42i%8AppleiPhone 5sHH2015:07:08 14:43:52!LT"' 0221\p     |:1171170100234#x 2015:07:08 14:43:522015:07:08 14:43:52%]/ AS7:Apple iOSMM  .h     "  bplist00OP _GE ~+Z+Fl#$QG<sF8P0mA-JwhiK-$x`OB2%9vHE8Y}'"(p@<=PeEdQh$!$gH?m6-j|6JV5Uc"G pG!?l>%! 0vYi5&('. ! \!P0%A!!$mf6}*2/$\$!!. nmU&--(@K2"hjUV*",'-~V6-)<-( bplist00UflagsUvalueUepochYtimescaleb;#-/8: ?_,wi!2!2  AppleiPhone 5s back camera 4.15mm f/2.2NE K 6T>TF N&(od5mde +4+?;%j2015:07:08 9http://ns.adobe.com/xap/1.0/ xPhotoshop 3.08BIM?Z%G?144352>20150708720150708<1443528BIM%z%9?-\.m" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyzC C   ?kHoO iVRO-KˠD_U KJukk}MG_*?Ⱦ}3organize-3.3.0/tests/test_api.py000066400000000000000000000012051472111340300166510ustar00rootroot00000000000000from conftest import make_files from organize import Config, Rule from organize.actions import Echo from organize.filters import Name def test_api(fs, testoutput): make_files(["foo.txt", "bar.txt", "baz.txt"], "test") echo = Echo("{name.upper()}{% if name.upper() == 'FOO' %}FOO{% endif %}") config = Config( rules=[ Rule( name="Say something", locations=["/test"], filters=[Name()], actions=[echo], ), ] ) config.execute(simulate=False, output=testoutput) assert testoutput.messages == ["BAR", "BAZ", "FOOFOO"] organize-3.3.0/tests/test_docs.py000066400000000000000000000027341472111340300170400ustar00rootroot00000000000000import re import pytest from conftest import ORGANIZE_DIR from organize import Config from organize.registry import ACTIONS, FILTERS DOCS_DIR = ORGANIZE_DIR / "docs" RE_CONFIG = re.compile(r"```yaml\n(?Prules:(?:.*?\n)+?)```", re.MULTILINE) def _list_examples(): for f in DOCS_DIR.rglob("*.md"): text = f.read_text(encoding="utf-8") for n, match in enumerate(RE_CONFIG.findall(text)): yield (f"{f} #{n}", match) @pytest.mark.parametrize( "location, config", _list_examples(), ids=[x[0] for x in _list_examples()], ) def test_examples_are_valid(location, config): """ Tests all snippets in the docs and readme: (To exclude, use shorthand `yml`) ```yaml rules: ``` """ try: Config.from_string(config) except EnvironmentError: # in case filters are not supported on the test OS pass except Exception as e: print(f"{location}:\n({config})") raise e def test_all_filters_documented(): filter_docs = (DOCS_DIR / "filters.md").read_text(encoding="utf-8") for name in FILTERS.keys(): assert ( "## {}".format(name) in filter_docs ), f"The {name} filter is not documented!" def test_all_actions_documented(): action_docs = (DOCS_DIR / "actions.md").read_text(encoding="utf-8") for name in ACTIONS.keys(): assert ( "## {}".format(name) in action_docs ), f"The {name} action is not documented!" organize-3.3.0/tests/test_make_files.py000066400000000000000000000005401472111340300202000ustar00rootroot00000000000000from conftest import make_files, read_files def test_make_files(fs): files = { "folder": { "subfolder": { "test.txt": "", "other.pdf": "text", }, }, "file.txt": "Hello world\nAnother line", } make_files(files, "test") assert read_files("test") == files organize-3.3.0/tests/test_utils.py000066400000000000000000000037451472111340300172530ustar00rootroot00000000000000from organize.utils import ChangeDetector, deep_merge, deep_merge_inplace def test_changedetector(): d = ChangeDetector() assert d.changed(1) assert not d.changed(1) assert not d.changed(1) d.reset() assert d.changed(1) assert d.changed(2) def test_merges_dicts(): a = {"a": 1, "b": {"b1": 2, "b2": 3}} b = {"a": 1, "b": {"b1": 4}} print(deep_merge(a, b)) assert deep_merge(a, b)["a"] == 1 assert deep_merge(a, b)["b"]["b2"] == 3 assert deep_merge(a, b)["b"]["b1"] == 4 def test_returns_copy(): a = {"regex": {"first": "A", "second": "B"}} b = {"regex": {"third": "C"}} x = deep_merge(a, b) a["regex"]["first"] = "X" assert x["regex"]["first"] == "A" assert x["regex"]["second"] == "B" assert x["regex"]["third"] == "C" def test_inserts_new_keys(): """Will it insert new keys by default?""" a = {"a": 1, "b": {"b1": 2, "b2": 3}} b = {"a": 1, "b": {"b1": 4, "b3": 5}, "c": 6} assert deep_merge(a, b)["a"] == 1 assert deep_merge(a, b)["b"]["b2"] == 3 assert deep_merge(a, b)["b"]["b1"] == 4 assert deep_merge(a, b)["b"]["b3"] == 5 assert deep_merge(a, b)["c"] == 6 def test_does_not_insert_new_keys(): """Will it avoid inserting new keys when required?""" a = {"a": 1, "b": {"b1": 2, "b2": 3}} b = {"a": 1, "b": {"b1": 4, "b3": 5}, "c": 6} assert deep_merge(a, b, add_keys=True) == { "a": 1, "b": {"b1": 4, "b2": 3, "b3": 5}, "c": 6, } assert deep_merge(a, b, add_keys=False) == { "a": 1, "b": {"b1": 4, "b2": 3}, } def test_inplace_merge(): a = {} b = {1: {2: 2, 3: 3, 4: {5: "fin."}}} a = deep_merge(a, b) assert a == b b[1][2] = 5 assert a != b deep_merge_inplace(a, {1: {4: {5: "new.", 6: "fin."}, 2: "x"}}) assert a == {1: {2: "x", 3: 3, 4: {5: "new.", 6: "fin."}}} def test_inplace_keeptype(): a = {} deep_merge_inplace(a, {"nr": {"upper": 1}}) assert a["nr"]["upper"] == 1 organize-3.3.0/tests/test_validators.py000066400000000000000000000004651472111340300202570ustar00rootroot00000000000000from pydantic.type_adapter import TypeAdapter from organize.validators import FlatList def test_flatlist(): ta = TypeAdapter(FlatList[int]) v = ta.validate_python([1, 2, [10, 11, [12, 23]], 3, [4, 5, 6]]) assert v == [1, 2, 10, 11, 12, 23, 3, 4, 5, 6] assert ta.validate_python(None) == []