pax_global_header00006660000000000000000000000064137151701650014520gustar00rootroot0000000000000052 comment=4cad31bf0421f9f3c66dde0dbc4080ebedfd0d83 khard-0.17.0/000077500000000000000000000000001371517016500126765ustar00rootroot00000000000000khard-0.17.0/.gitignore000066400000000000000000000002321371517016500146630ustar00rootroot00000000000000build dist/ khard/version.py doc/source/api/ doc/source/examples/template.yaml .coverage .dmypy.json .mypy_cache .pytest_cache __pycache__ khard.egg-info khard-0.17.0/.readthedocs.yml000066400000000000000000000005271371517016500157700ustar00rootroot00000000000000# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details version: 2 python: install: # Run setup.py first to generate the version.py file. For some reason it # is not generated with `pip install .[doc]` - method: setuptools path: . - method: pip path: . extra_requirements: - doc khard-0.17.0/.travis.yml000066400000000000000000000011171371517016500150070ustar00rootroot00000000000000language: python python: - "3.6" - "3.7" - "3.8" - "nightly" - "pypy3" # this is 3.6 env: - JOB=tests jobs: include: - python: "3.8" env: - JOB=check - python: "3.8" env: - JOB=docs allow_failures: - python: "nightly" - python: "pypy3" install: | case $JOB in tests) pip install .;; docs) pip install .[doc];; check) pip install mypy;; esac script: | case $JOB in tests) python setup.py test;; docs) python setup.py build; make -C doc html man;; check) mypy --ignore-missing-imports khard;; esac khard-0.17.0/CHANGES000066400000000000000000000225061371517016500136760ustar00rootroot00000000000000Change Log ========== v0.17.0: 2020-08-13 - Do not modify (clean up) search query to find more matches (4583efd) - Remove special search handling for phone numbers (a570a85) - Remove extra pruning from email, phone and postaddress subcommand (3f315f9, 1b9ce98, c704ce1) - Add query syntax for search terms (#131) - Add newline at the end of "show --format=pretty" (#256) - Add -H to select header from which add-email should read (#258) - Expand environment variables in paths in the config file (#269) - Deprecate --strict-search (the new query syntax can be used instead) v0.16.0: 2020-04-04 - Require python >= 3.6 - Require either default_action or a subcommand - Twinkle sound samples converted to .ogg (#228) - Expand documentation - Build and server documentation on https://khard.readthedocs.io/ - Allow display=formatted_name in config - New --fields option for list subcommand - Internal changes: - Expanded the test suite - Use of python type annotations, checked on travis - Represent queries as custom data structures not regex - Use custom loggers per module v0.15.0: 2019-10-24 - Require either default_action or a subcommand in the future (add deprecation warning for now) - add man page for the config file - make all options in the config file optional except for the address book definitions - allow lists of strings for editor an merge_editor config options - add a CONTRIBUTING file on Github - handle ABLABELs on most fields - add formatted name to the yaml template - show formatted name in contact details - make it possible to sort by and display formatted name in linstings - remove the khard-runner.py helper script - validate the config file upon loading it - internal code refactoring v0.14.0: 2019-06-21 - Display ABLABELs for URLs and Private Objects - Allow vcard selections to be aborted explicitly - Unify edit and source subcommands - Merge export and show subcommands - Turn template export into a seperate command - Require python >= 3.5 - Add html documentation (generated with sphinx) - Add man page (generated with sphinx) v0.13.0: 2018-12-25 - New action postaddress: lists all postal (addresses analog to email and phone actions, #196) - New zsh completion function for email addresses - New config variables for the contact table section in khard.conf: preferred_email_address_type and preferred_phone_number_type - Slight speed improvements - Test suite created - Several bug fixes v0.12.2: 2018-01-21 - Fixed: Found contact twice when the whole uid was used with -u (#161) - Fixed: A minor bug in the contact search function (#160) v0.12.1: 2018-01-14 - Fix for issue #148: Config variable "sort" not longer mandatory in config file v0.12.0: 2018-01-12 - Vcard: Support for anniversary attribute (#138) - Config: New config parameter: localize_dates (#118) - Action list: -p / --parsable prints a tab separated contact list (#141) - Action remove: --force: Remove contact without confirmation (#140) - Mutt: You have to update the query command in mutt's config file: set query_command= "khard email --parsable '%s'" - Minor changes in khard example config, zsh completion function and twinkle scripts - Fix: Results of phone number search improved - Fix: Yaml parser switched from pyyaml to ruamel.yaml to allow special unicode characters in the contact template (#133) - Fix: Accentuated characters sorted wrong in contact list (#127) v0.11.4: 2017-02-16 - Unpinned vobject library version (base64 decoding/encoding bug was fixed upstream) - New option: -c / --config /path/to/config.file - Changed short option of --search-in-source-files from -c to -f to avoid confusion with the new -c / --config option - Minor bug fixes v0.11.3: 2016-09-20 - Pinned version of vcard library vobject to version 0.9.2 due to bug https://github.com/eventable/vobject/issues/39 - Added some new action aliases - Fix for birthday date processing (#95) v0.11.2: 2016-08-31 - Extended the photo parsing workaround from khard version 0.11.1 to all base64 encoded vcard attributes (#86 and #87) - Show additional/middle names in name column of contact table (#89) - Added khard-runner.py helper script to simplify source code launching v0.11.1: 2016-07-31 - Workaround for the photo attribute parsing issue of vobject library 0.9.2 (#80) - setup.py: try to convert readme file into restructured text format (pypi requirement) and specify encoding explicitly (#83) v0.11.0: 2016-07-17 - Made khard python3 compatible (#59) - Enhanced read and write support for vcard versions 3.0 and 4.0 - user input parser: - Improved robustness and error handling - Fixed org attribute parsing (#57) - Support for private vcard extensions (#51) - New action birthdays (#64) - New options: --display to display contacts by first or last name (#66) --search-in-source-files to speed up program initialization (#75) --skip-unparsable to skip unparsable vcard files (#75) --strict-search to narrow the contact search to the name field - Added some aliases for program actions (#65) - Removed davcontroler module due to the python3 incompatibility (script moved into the misc folder) - Updated zsh completion function and khards example config file v0.10.0: 2016-05-02 - New Action birthday: list birthdays, sorted by month and day - option -p, --pretty was renamed to -p, --parsable: So, the pretty formatted email or phone number table is the default now. Please adapt the configuration files of potential email and phone applications (e.g.: mutt email -p %s) v0.9.0: 2016-03-17 - Fully restructured command line interface for better usability: - general help with: khard -h - help for a specific action: khard action -h - Updated zsh completion function - New Action addressbooks - New option -p|--pretty for email and phone actions to get pretty formatted output - Fix: Only delete contact after modify, copy or move action was completed successfully v0.8.1: 2016-01-16 - New option "show_uids" in config file to disable uid column in contact table v0.8.0: 2016-01-15 - Sort contact table by first or last name (take note of changed behaviour of "sort" option) - New option -g, --group-by-addressbook to group contact table by address book - Changes in config file: - New group: contact table - new option: sort to sort contact table by first or last name - New option: group_by_addressbook to group contact table by address book - Moved show_nicknames option from group "general" to group "contact table" v0.7.4: 2016-01-11 - Fixed uid dictionary creation v0.7.3: 2016-01-08 - Cancel without further actions if the opened contacts editor is closed without saving (determined by modification date of template file) v0.7.2: 2016-01-03 - Use of module atomicwrites to securely write vcards to disk v0.7.1: 2016-01-01 - Added support for multiple instances of one vcard attribute v0.7.0: 2015-12-18 - Support for vobject library version >= 0.8.2 from https://github.com/tBaxter/vobject - Contact template syntax switched to yaml - alot and mutt actions summarized to new email action (please have a look into the readme file for configuration changes) - Support for extended name attributes - Create and modify contact from stdin or from template file - New action "export" to export data of existing contact in yaml format - New argument --open-editor to open the preferred text editor after successful creation of new contact from stdin or template file - New argument {-u, --uid} to select contact by uid - Added write support for categories attribute - Added wrapper script for sdiff - Fixed a bug, which prevented the creation of new contacts with the add-email action v0.6.3: 2015-10-24 - Added note attribute v0.6.2: 2015-10-10 - Added completion function for zsh v0.6.1: 2015-10-06 - Added title and role attribute - Removed input restrictions of name and post address fields - Parameter default_country in config file is not longer in use v0.6.0: 2015-09-20 - New options copy contact and move contact - Changed behavior of merge command (have a look into the readme file for details) - Get path to editor and merge editor from the $PATH variable - Code cleanup: new object class AddressBook v0.5.0: 2015-09-05 - New option to merge two contacts - Support for XDG_CONFIG_HOME parameter - Post address: Added support for street and house number field with multiple lines v0.4.1: 2015-07-16 - improved search results for phone, mutt and alot v0.4.0: 2015-06-15 - new option "add-email": Get full email from stdin, filter senders mail address and add that address to an existing contact or create a new one. Feature tested with email client mutt. v0.3.3: 2015-05-07 - twinkle option was renamed to phone: Now it prints all phone numbers like the mutt option does for e-mail addresses (more general approach for external phone clients) - adapted twinkle plugin for use with the new phone option v0.3.2: 2015-03-27 - Read-only support for Categories attribute v0.3.1: 2015-03-26 - Nickname attribute added - New parameter in config file: show_nicknames = yes / no v0.3.0: 2015-03-25 - Added support for jabber, skype, twitter and webpage - Created a filter for malformed vcard attributes v0.2.2: 2015-03-14 - Added support for alot (MUA) v0.2.1: 2015-01-14 - created pypi package - missing attribute "fn" in VCard file is handled correctly now. v0.2.0: 2014-10-01 - new project structure - added twinkle plugin - extended readme file v0.1.0: 2014-09-18: - initial release. khard-0.17.0/CONTRIBUTING.rst000066400000000000000000000054631371517016500153470ustar00rootroot00000000000000Contributing ============ **Thank you for considering contributing to khard!** .. toctree:: :maxdepth: 1 self bench API Reference Khard is developed on `Github`_ where you are welcome to post `bug reports`_, `feature requests`_ or join the discussion in general. Bug reports ----------- If you want to report a bug keep in mind that the following things make it much easier for maintainers to help: - update to the latest version if possible and verify the bug there - report the version(s) that are affected - state the python version you are using - if there are stack tracebacks post them with your bug report - supply a minimal configuration (config file and vcards) to reproduce the error Feature requests ---------------- Please stick to the following standards when you open pull requests: - Khard's development tries to follow `Vincent's branching model`_ so normal pull requests should be made against the `develop`_ branch. Only important bug fixes that affect the current release should be opened against `master`_. - Write "good" commit messages, especially a proper subject line. This is also explained in `the Git book`_. - Format your python code according to `PEP 8`_. Tools like `pylint`_ also help in writing maintainable code. - Khard has a test suite, please provide tests for bugs that you fix and also for new code and new features that are introduced. - Please verify *all* tests pass before sending a pull request, they will be checked again by travis but it might be a lot faster to check locally first: |travis| Development ----------- In order to start coding you need to fetch the develop branch: .. code-block:: shell git clone https://github.com/scheibler/khard cd khard python setup.py build # to generate the version.py file python -m khard --help # or pip3 install --editable . khard --help Alternatively you can use the ``setup.py`` script directly. If you want to isolate khard from your system Python environment you can use a `virtualenv`_ to do so. .. _bug reports: https://github.com/scheibler/khard/issues .. _the Git book: https://www.git-scm.com/book/en/v2/Distributed-Git-Contributing-to-a-Project#_commit_guidelines .. _develop: https://github.com/scheibler/khard/tree/develop .. _feature requests: https://github.com/scheibler/khard/pulls .. _Github: https://github.com/scheibler/khard .. _master: https://github.com/scheibler/khard/tree/master .. _PEP 8: https://www.python.org/dev/peps/pep-0008/ .. _pylint: https://pylint.readthedocs.io/en/latest/ .. |travis| image:: https://travis-ci.org/scheibler/khard.svg?branch=develop :target: https://travis-ci.org/scheibler/khard :alt: build status .. _Vincent's branching model: http://nvie.com/posts/a-successful-git-branching-model/ .. _virtualenv: https://virtualenv.pypa.io/en/stable/ khard-0.17.0/LICENSE000066400000000000000000001045131371517016500137070ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . khard-0.17.0/MANIFEST.in000066400000000000000000000001621371517016500144330ustar00rootroot00000000000000# https://packaging.python.org/guides/using-manifest-in/ include AUTHORS include CHANGES recursive-include misc * khard-0.17.0/README.md000066400000000000000000000063671371517016500141710ustar00rootroot00000000000000khard ===== Khard is an address book for the Unix console. It creates, reads, modifies and removes carddav address book entries at your local machine. Khard is also compatible to the email clients mutt and alot and the SIP client twinkle. You can find more information about khard and the whole synchronization process [here][blog]. Warning: If you want to create or modify contacts with khard, beware that the vcard standard is very inconsistent and lacks interoperability. Different actors in that sector have defined their own extensions and even produce non-standard output. A good example is the type value, which is tied to phone numbers, email and post addresses. Khard tries to avoid such incompatibilities but if you sync your contacts with an Android or iOS device, expect problems. You are on the safe side, if you only use khard to read contacts. For further information about the vcard compatibility issues have a look into [this blog post][sad]. Installation ------------ [![Packaging status][repos-badge]][repos] Khard is already packaged for quite some distributions. Chances are you can install it with your default package manager. Releases are also published on [PyPi](https://pypi.org/project/khard/) and can be installed with `pip`. Further instructions can be found in the [documentation](https://khard.readthedocs.io/en/latest/#installation). Usage ----- [![Documentation Status][docs-badge]][docs] There is an [example config file](doc/source/examples/khard.conf.example) which you can copy to the default config file location: `~/.config/khard/khard.conf`. `khard` has several subcommands which are all documented by their `--help` option. [The docs][docs] also have a chapter on [command line usage](https://khard.readthedocs.io/en/latest/commandline.html) and [configuration](https://khard.readthedocs.io/en/latest/#configuration). In order to build the documentation locally you need [Sphinx](https://www.sphinx-doc.org/). It can be build from the Makefile in the `doc` directory. Development ----------- [![Build Status][travis-badge]][travis] Khard is developed [on GitHub](https://github.com/scheibler/khard) where you are welcome to post [bug reports](https://github.com/scheibler/khard/issues) and [feature requests](https://github.com/scheibler/khard/pulls). Also see the [notes for contributors](CONTRIBUTING.rst). Authors ------- Khard was started by [Eric Scheibler](http://eric-scheibler.de) and is currently maintained by @lucc. [Several people](https://github.com/scheibler/khard/graphs/contributors) have contributed over the years. Related projects ---------------- If you need a console based calendar too, try out [khal](https://github.com/geier/khal). [blog]: http://eric-scheibler.de/en/blog/2014/10/Sync-calendars-and-address-books-between-Linux-and-Android/ [sad]: http://alessandrorossini.org/2012/11/15/the-sad-story-of-the-vcard-format-and-its-lack-of-interoperability/ [repos]: https://repology.org/project/khard/versions [repos-badge]: https://repology.org/badge/tiny-repos/khard.svg [docs]: https://khard.readthedocs.io/en/latest/ [docs-badge]: https://readthedocs.org/projects/khard/badge/?version=latest [travis]: https://travis-ci.org/scheibler/khard [travis-badge]: https://travis-ci.org/scheibler/khard.svg?branch=develop khard-0.17.0/doc/000077500000000000000000000000001371517016500134435ustar00rootroot00000000000000khard-0.17.0/doc/Makefile000066400000000000000000000012071371517016500151030ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build SPHINXPROJ = khard SOURCEDIR = source BUILDDIR = build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) # These files are always up to date. Makefile:; khard-0.17.0/doc/source/000077500000000000000000000000001371517016500147435ustar00rootroot00000000000000khard-0.17.0/doc/source/bench.rst000066400000000000000000000014641371517016500165610ustar00rootroot00000000000000Benchmarking, profiling and performance --------------------------------------- When benchmarking code it is important to reduce other load on the system (music player, web browser for example). One can use the python ``timeit`` module or a command line utility like `hyperfine`_: .. code-block:: shell python -m timeit -s 'from khard.khard import main' 'main(["list"])' hyperfine 'python -m khard list' For profiling the ``cProfile`` python module works well. With the help of `gprof2dot`_ one can generate quite useful graphs: .. code-block:: shell python -m cProfile -o output.file -m khard list gprof2dot -f pstats --show-samples output.file | dot -T png > graph.png xdg-open graph.png .. _hyperfine: https://github.com/sharkdp/hyperfine .. _gprof2dot: https://github.com/jrfonseca/gprof2dot khard-0.17.0/doc/source/commandline.rst000066400000000000000000000145041371517016500177670ustar00rootroot00000000000000Command line usage ================== The following subsections give an overview of khard's main features. You may get general help and all available actions as well as detailed information on all available options for the specific commands with the :option:`--help` options: .. code-block:: shell khard --help khard command --help Beware, that the order of the command line parameters matters. Filtering contacts ------------------ Many subcommands of khard accept search terms to narrow the list of contacts that the command should work on. One can simply give some plain search terms on the command line or use a more sophisticated query language of khard. The query language allows the user to search for contacts where a specific term matches in a specific field of the contact. When searching for ``foo`` there might be two contacts that match, because one is called "Foo" and the other has an email address containing "foo": .. code-block:: shell $ khard list foo Index Name Email 1 Bar bar@foo-company.com 2 Foo boss@example.com But when searching for ``name:foo`` or ``emails:foo`` one would only find one of these each time because "foo" only matches in these specific fields of the contact. The available fields are the same as in the YAML format for contacts (an empty YAML template can be seen with ``khard template``). Case does not matter, all filed names will be converted to lower case. For field names with spaces (like "Formatted name") the spaces have to be replaced with underscores (like in ``formatted_name``). And the five name related fields "Prefix", "First name", "Additional", "Last name" and "Suffix" are not available, but a simple ``name:`` query is possible which will search in *any* name field (including nicknames and formatted names). .. note:: Typos in field names result in the query beeing considered as a general search term. So ``email:foo`` will search for "email:foo" in any field of the contact, because the field is called "emails". .. note:: Nested field names like for the :option:`-F` option of the ``ls`` subcommand are currently not supported in the query syntax. You can only search with the top level field names. Show contacts ------------- After you have created a new address book and you have synced it to your local machine, you can list all available contacts with the following command: .. code-block:: shell khard list or if you have more than one address book and you want to filter the output: .. code-block:: shell khard list -a addressbook1,addressbook2 The resulting contact table only contains the first phone number and email address. If you want to view all contact details you can pick one from the list: .. code-block:: shell khard show or search for it: .. code-block:: shell khard show name of contact The parameter :option:`-a` from the examples above is always optional. It can be given on all subcommands that select one or more contacts. The search parameter searches in all data fields. Therefore you aren't limited to the contact's name but you also could for example search for a part of a phone number, email address or post address. However if you explicitly want to narrow your search down to some fields see the query language described in :ref:`Filtering contacts`. Create contact -------------- Add new contact with the following command: .. code-block:: shell khard new [-a "address book name"] The template for the new contact opens in the text editor, which you can set in the config file. It follows the yaml syntax. Alternatively you can create the contact from stdin: .. code-block:: shell echo " First name : John Last name : Smith Email : work : john.smith@example.org Phone : home : xxx 555 1234 Categories : - cat1 - cat2 - cat3 " | khard new or create from input template file: .. code-block:: shell khard new -i contact.yaml You may get an empty contact template with the following command: .. code-block:: shell khard template Assuming the user had configured the three supported private object "Jabber", "Skype", and "Twitter" in their config, the template would look :download:`like this `. Per default khard creates vcards of version 3.0. If your other contact applications support vcards of the more recent version 4.0, you may change this with the option :option:`--vcard-version`. Example: .. code-block:: shell khard new --vcard-version=4.0 For a more permanent solution you may set the preferred_version parameter in the vcard section of the khard config file (see the :download:`example config file ` for more details). But beware, that khard cannot convert already existing contacts from version 3.0 to 4.0. Therefore this setting is not applicable to the modify action. Edit contacts ------------- Use the following to modify the contact after successful creation: .. code-block:: shell khard edit [-a addr_name] [search terms [search terms ...]] If you want to edit the contact elsewhere, you can export the filled contact template: .. code-block:: shell khard show --format=yaml -o contact.yaml [-a addr_name] [search terms [search terms ...]] Edit the yaml file and re-import either through stdin: .. code-block:: shell cat contact.yaml | khard edit [-a addr_name] [search terms [search terms ...]] or file name: .. code-block:: shell khard edit -i contact.yaml [-a addr_name] [search terms [search terms ...]] If you want to merge contacts use the following to select a first and then a second contact: .. code-block:: shell khard merge [-a source_abook] [search terms [search terms ...]] [-A target_abook] [-t target_search_terms] You will be launched into your ``merge_editor`` (see |khard.conf|_) where you can merge all changes from the first selected contact onto the second. Once you are finished, the first contact is deleted and the second one updated. Copy or move contact: .. code-block:: shell khard copy [-a source_abook] [search terms [search terms ...]] [-A target_abook] khard move [-a source_abook] [search terms [search terms ...]] [-A target_abook] Remove contact: .. code-block:: shell khard remove [-a addr_name] [search terms [search terms ...]] .. |khard.conf| replace:: :manpage:`khard.conf` .. _khard.conf: man/khard.conf.html khard-0.17.0/doc/source/conf.py000077500000000000000000000111641371517016500162500ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # khard documentation build configuration file, created by # sphinx-quickstart on Sun Jan 14 10:35:27 2018. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # import os import pathlib import sys sys.path.insert(0, os.path.abspath('../..')) from khard.version import version as original_version # update the template file for the docs if necessary def update_template_file(): here = pathlib.Path(__file__).parent src = here.parent.parent/'khard'/'data'/'template.yaml' dest = here/'examples'/'template.yaml' if not dest.exists() or src.stat().st_ctime > dest.stat().st_ctime: dest.write_text(src.read_text().format( "\n Jabber : \n Skype : \n Twitter : ")) update_template_file() del update_template_file # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'autoapi.extension', 'sphinx.ext.autodoc', 'sphinx.ext.autosectionlabel', 'sphinx.ext.todo', 'sphinx_autodoc_typehints', ] autoapi_type = 'python' autoapi_dirs = ['../../khard'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General information about the project. project = 'khard' copyright = '2020, Eric Scheibler' author = 'Eric Scheibler' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '.'.join(original_version.split('.')[0:2]) # The full version, including alpha/beta/rc tags. release = original_version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = [] # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = 'alabaster' # Custom sidebar templates, must be a dictionary that maps document names # to template names. # # This is required for the alabaster theme # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars html_sidebars = { '**': [ 'relations.html', # needs 'show_related': True theme option to display 'searchbox.html', ] } # -- Options for HTMLHelp output ------------------------------------------ # Output file base name for HTML help builder. htmlhelp_basename = 'kharddoc' # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('man/khard', 'khard', 'Console carddav client', '', 1), ('man/khard.conf', 'khard.conf', 'configuration file for khard', '', 5), ] # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'khard', 'khard Documentation', author, 'khard', 'One line description of project.', 'Miscellaneous'), ] khard-0.17.0/doc/source/contributing.rst000077700000000000000000000000001371517016500232662../../CONTRIBUTING.rstustar00rootroot00000000000000khard-0.17.0/doc/source/davcontroller.rst000066400000000000000000000032731371517016500203600ustar00rootroot00000000000000Davcontroller ------------- Khard also contains a helper script called davcontroller. It's designed to create and remove address books and calendars at the server. I have created davcontroller cause my previously used CalDAV server (Darwin calendarserver) offered no simple way to create new address books and calendars. But davcontroller should be considered as a hacky solution and it's only tested against the Darwin calendarserver. So if your CalDAV server offers a way to create new address books and calendars I recommend to prefer that method over davcontroller. If you nonetheless want to try davcontroller, you have to install the CalDAVClientLibrary first. Unfortunately that library isn't compatible to python3 so you have to create an extra python2 virtual environment and install in there: .. code-block:: shell # create python2 virtual environment virtualenv -p python2 ~/.virtualenvs/davcontroller # get library from svn repository sudo aptitude install subversion svn checkout http://svn.calendarserver.org/repository/calendarserver/CalDAVClientLibrary/trunk CalDAVClientLibrary cd CalDAVClientLibrary # install library ~/.virtualenvs/davcontroller/bin/python setup.py install # start davcontroller script ~/.virtualenvs/davcontroller/bin/python /path/to/khard-x.x.x/misc/davcontroller/davcontroller.py This small script helps to create and remove new address books and calendars at the carddav and caldav server. List available resources: .. code-block:: shell davcontroller -H example.com -p 11111 -u USERNAME -P PASSWORD list Possible actions are: list, new-addressbook, new-calendar and remove. After creating or removing you must adapt your vdirsyncer config. khard-0.17.0/doc/source/examples/000077500000000000000000000000001371517016500165615ustar00rootroot00000000000000khard-0.17.0/doc/source/examples/khard.conf.example000066400000000000000000000035501371517016500221560ustar00rootroot00000000000000# example configuration file for khard version > 0.14.0 # place it under ~/.config/khard/khard.conf # This file is parsed by the configobj library. The syntax is described at # https://configobj.readthedocs.io/en/latest/configobj.html#the-config-file-format [addressbooks] [[family]] path = ~/.contacts/family/ [[friends]] path = ~/.contacts/friends/ [general] debug = no default_action = list # These are either strings or comma seperated lists editor = vim, -i, NONE merge_editor = vimdiff [contact table] # display names by first or last name: first_name / last_name / formatted_name display = first_name # group by address book: yes / no group_by_addressbook = no # reverse table ordering: yes / no reverse = no # append nicknames to name column: yes / no show_nicknames = no # show uid table column: yes / no show_uids = yes # sort by first or last name: first_name / last_name / formatted_name sort = last_name # localize dates: yes / no localize_dates = yes # set a comma separated list of preferred phone number types in descending priority # or nothing for non-filtered alphabetical order preferred_phone_number_type = pref, cell, home # set a comma separated list of preferred email address types in descending priority # or nothing for non-filtered alphabetical order preferred_email_address_type = pref, work, home [vcard] # extend contacts with your own private objects # these objects are stored with a leading "X-" before the object name in the vcard files # every object label may only contain letters, digits and the - character # example: # private_objects = Jabber, Skype, Twitter # default: , (the empty list) private_objects = Jabber, Skype, Twitter # preferred vcard version: 3.0 / 4.0 preferred_version = 3.0 # Look into source vcf files to speed up search queries: yes / no search_in_source_files = no # skip unparsable vcard files: yes / no skip_unparsable = no khard-0.17.0/doc/source/index.rst000066400000000000000000000030011371517016500165760ustar00rootroot00000000000000################################# Welcome to khard's documentation! ################################# .. toctree:: :maxdepth: 1 self commandline scripting contributing man indices Khard is an address book for the Unix command line. It can read, create, modify and delete carddav address book entries. Khard only works with a local store of VCARD files. It is intended to be used in conjunction with other programs like an email client, text editor, vdir synchronizer or VOIP client. Installation ============ .. image:: https://repology.org/badge/tiny-repos/khard.svg :target: https://repology.org/project/khard/versions :alt: Packaging status Khard is available as a native package for some \*nix distributions so you should check your package manager first. If you want or need to install manually you can use the release from `PyPi`_: .. code-block:: shell pip3 install khard If you want to help the development or need more advanced installation instructions see :doc:`contributing`. .. _PyPi: https://pypi.python.org/pypi/khard/ Configuration ============= The configuration file of khard is stored in the XDG conform config directory. If the environment variable ``$XDG_CONFIG_HOME`` is set, it is ``$XDG_CONFIG_HOME/khard/khard.conf`` and it defaults to ``~/.config/khard/khard.conf`` otherwise. An :download:`example configuration ` is provided in the source tree. It looks like this: .. literalinclude:: examples/khard.conf.example :language: ini khard-0.17.0/doc/source/indices.rst000066400000000000000000000001331371517016500171100ustar00rootroot00000000000000Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` khard-0.17.0/doc/source/man.rst000066400000000000000000000002351371517016500162500ustar00rootroot00000000000000Manpages ======== The following man pages are available for khard: .. toctree:: :maxdepth: 1 khard(1) khard.conf(1) khard-0.17.0/doc/source/man/000077500000000000000000000000001371517016500155165ustar00rootroot00000000000000khard-0.17.0/doc/source/man/khard.conf.rst000066400000000000000000000073251371517016500202740ustar00rootroot00000000000000khard.conf ========== Summary ------- The config file for :program:`khard` is a plain text file with an ini-like syntax. Many options have a corresponding command line option. The only mandatory section in the config file is the definition of the available address books. Location -------- The file is looked up at :file:`$XDG_CONFIG_HOME/khard/khard.conf`. If the environment variable :file:`$XDG_CONFIG_HOME` is unset :file:`~/.config/` is used in its stead. The location can be changed with the environment variable :file:`$KHARD_CONFIG` or the command line option :option:`-c` (which takes precedence). Syntax ------ The syntax of the config file is ini-style dialect. It is parsed with the configobj library. The precise definition of the corresponding ini syntax can be found at https://configobj.readthedocs.io/en/latest/configobj.html#the-config-file-format . It supports sections marked with square brackets and nested sections with more square brackets. Each section contains several keys with values delimited by equal signs. The values are typed and type checked. Options ------- The config file consists of these four sections: addressbooks This section contains several subsections, but at least one. Each subsection can have an arbitrary name which will be the name of an addressbook known to khard. Each of these subsections **must** have a *path* key with the path to the folder containing the vcard files for that addressbook. The *path* value supports environment variables and tilde prefixes. :program:`khard` expects the vcard files to hold only one VCARD record each and end in a :file:`.vcf` extension. general This section allows one to configure some general features about khard. The following keys are available in this section: - *debug*: a boolean indication weather the logging level should be set to *debug* by default (same effect as the :option:`--debug` option on the command line) - *default_action*: the default action/subcommand to use if the first non option argument does not match any of the available subcommands - *editor*: the text editor to use to edit address book entries, if not given :file:`$EDITOR` will be used - *merge_editor*: a command used to merge two cards interactively, if not given, :file:`$MERGE_EDITOR` will be used contact table This section is used to configure the behaviour of different output listings of khard. The following keys are available: - *display*: which part of the name to use in listings; this can be one of ``first_name``, ``last_name`` or ``formatted_name`` - *group_by_addressbook*: weather or not to group contacts by address book in listings - *localize_dates*: weather to localize dates or to use ISO date formats - *preferred_email_address_type*: labels of email addresses to prefer - *preferred_phone_number_type*: labels of telephone numbers to prefer - *reverse*: weather to reverse the order of contact listings or not - *show_nicknames*: weather to show nick names - *show_uids*: weather to show uids - *sort*: field by which to sort contact listings vcard - *private_objects*: a list of strings, these are the names of private vCard fields (starting with ``X-``) that will be loaded and displayed by khard - *search_in_source_files*: weather to search in the vcard files before parsing them in order to speed up searches - *skip_unparsable*: weather to skip unparsable vcards, otherwise khard exits on the first unparsable card it encounters - *preferred_version*: the preferred vcard version to use for new cards Example ------- This is the :download:`example config file <../examples/khard.conf.example>`: .. literalinclude :: ../examples/khard.conf.example :language: ini khard-0.17.0/doc/source/man/khard.rst000066400000000000000000000066541371517016500173540ustar00rootroot00000000000000khard ===== Synopsis -------- :program:`khard` [:option:`-c` CONFIG] [:option:`--debug`] [:option:`--skip-unparsable`] SUBCOMMAND ... :program:`khard` :option:`-h` | :option:`--help` :program:`khard` :option:`-v` | :option:`--version` Description ----------- :program:`khard` is an address book for the Unix command line. It can read, create, modify and delete carddav address book entries. :program:`khard` only works with a local store of VCARD files. It is intended to be used in conjunction with other programs like an email client, text editor, vdir synchronizer or VOIP client. Options ------- .. option:: -c CONFIG, --config CONFIG configuration file (default: :file:`~/.config/khard/khard.conf`) .. option:: --debug output debugging information .. option:: -h, --help show a help message and exit .. option:: --skip-unparsable skip unparsable vcards when reading the address books .. option:: -v, --version show program's version number and exit Subcommands ----------- The functionality of khard is divided into several subcommands. All of these have their own help text which can be seen with ``khard SUBCOMMAND --help``. Many subcommands accept search terms to limit the number of contacts they should work on, display or present for selection. The syntax is described in :ref:`Search query syntax`. Listing subcommands ~~~~~~~~~~~~~~~~~~~ These subcommands list information of several contacts who match a search query. list list all (selected) contacts birthdays list birthdays (sorted by month and day) email list email addresses phone list phone numbers postaddress list postal addresses filename list filenames of all matching contacts Detailed display ~~~~~~~~~~~~~~~~ These subcommands display detailed information about one subcommand. show display detailed information about one contact, supported output formats are "pretty", "yaml" and "vcard" export DEPRECATED, use ``show --format=yaml`` instead Modifying subcommands ~~~~~~~~~~~~~~~~~~~~~ These subcommands are used to modify contacts. edit edit the data of a contact, supported formats for editing are "yaml" and "vcard" new create a new contact add-email Extract email address from the "From:" field of an email header and add to an existing contact or create a new one merge merge two contacts copy copy a contact to a different addressbook move move a contact to a different addressbook remove remove a contact source DEPRECATED, use ``edit --format=vcard`` instead Other subcommands ~~~~~~~~~~~~~~~~~ addressbooks list all address books template print an empty yaml template Search query syntax ------------------- Search queries consist of one or more command line arguments. Each can be a simple search term or a search term for a specific field. The field name is separated from the search term by a colon (``:``) without any spaces. Spaces in the field name have to be replaced with underscores. The available fields are the same fields as in the YAML template with the exception of the five name components (first, last, prefix, suffix, additional). But there is the special pseudo field specifier ``name:`` which will search in *any* name related field (including nichnames and formatted names). If a field name is not known the search term is interpreted as a plain search term and the string (including the colon) is looked up in any field of the contact. Configuration ------------- See :manpage:`khard.conf(5)`. khard-0.17.0/doc/source/scripting.rst000066400000000000000000000146501371517016500175050ustar00rootroot00000000000000Scripting ========= Many of khard's subcommands can be used for scripting purposes. The commands ``list``, ``birthdays``, ``email``, ``phone`` and ``postaddress`` feature a ``--parsable`` option which changes the output to be tab separated (normally the fields are visually aligned with spaces). They list several contacts at once. If the search terms are known to match one single contact the command ``khard show --format=yaml`` can also be used for scripting. It produces the contact in the yaml format that is also used for editing. But if the search terms produce more than one result the ``show`` command first asks the user to select one contact which is unsuitable for scripting. Specifying output fields ------------------------ The ``list`` command additionally features a ``--fields``/``-F`` options which allows to specify the fields of a contact that should be printed. The list of supported field names can be seen with ``khard list -F help``. Some fields can hold complex data structures like mappings and lists. These can be specified by dot-subscripting the field name. Lists are subscribed with numbers starting at zero. Subscripting can be nested. If the contact for somebody would contain several email addresses for example: .. code-block:: $ khard list --fields emails somebody Emails {'work': ['work@example.org'], 'home': ['some@example.org', 'body@example.org']} One could access these with different nested field descriptions like this: .. code-block:: $ khard list --fields emails.work somebody Emails ['work@example.org'] $ khard list --fields emails.home.1 somebody Emails body@example.org Integration =========== Khard can be used together with email or SIP clients or a synchronisation program like `vdirsyncer`_. For synchronisation programs it is important to note that khard expects the contacts in the configured address book directories to be stored in individual files. The files are expected to have a ``.vcf`` extension. .. _vdirsyncer: https://github.com/pimutils/vdirsyncer/ vdirsyncer ---------- Make sure to write the contacts into individual files as ``VCARD`` records and give them a ``.vcf`` file extension: .. code-block:: ini [storage local_storage_for_khard] type = "filesystem" fileext = "vcf" path = "..." mutt ---- Khard may be used as an external address book for the email client mutt. To accomplish that, add the following to your mutt config file (mostly ``~/.mutt/muttrc``): .. code-block:: set query_command = "khard email --parsable %s" bind editor complete-query bind editor ^T complete Then you can complete email addresses by pressing the Tab-key in mutt's new mail dialog. If your address books contain hundreds or even thousands of contacts and the query process is very slow, you may try the ``--search-in-source-files`` option to speed up the search: .. code-block:: set query_command = "khard email --parsable --search-in-source-files %s" If you want to complete multi-word search strings like "john smith" then you may try out the following instead: .. code-block:: set query_command = "echo %s | xargs khard email --parsable --" To add email addresses to khard's address book, you may also add the following lines to your muttrc file: .. code-block:: macro index,pager A \ "khard add-email" \ "add the sender email address to khard" Then navigate to an email message in mutt's index view and press "A" to start the address import dialog. Alot ---- Add the following lines to your alot config file: .. code-block:: ini [accounts] [[youraccount]] [[[abook]]] type = shellcommand command = khard email --parsable regexp = '^(?P[^@]+@[^\t]+)\t+(?P[^\t]+)' ignorecase = True Twinkle ------- For those who also use the SIP client twinkle to take phone calls, khard can be used to query incoming numbers. The plugin tries to find the incoming caller id and speaks it together with the phone's ring tone. But it is more or less a proof of concept - feel free to extend. The plugin needs the following programs: .. code-block:: shell sudo aptitude install ffmpeg espeak sox mpc sox and ffmpeg are used to cut and convert the new ring tone and espeak speaks the caller id. mpc is a client for the music player daemon (mpd). It's required to stop music during an incoming call. Skip the last, if you don't use mpd. Don't forget to set the "stop_music"-parameter in the ``config.py`` file to ``False``, too. After the installation, copy the scripts and sounds folders to your twinkle config folder: .. code-block:: shell cp -R misc/twinkle/* ~/.twinkle/ Next convert the sound samples to wave: .. code-block:: shell ffmpeg -i incoming_call.ogg incoming_call.wav ffmpeg -i outgoing_call.ogg outgoing_call.wav ffmpeg -i ringtone_segment.ogg ringtone_segment.wav Then edit your twinkle config file (mostly ``~/.twinkle/twinkle.cfg``) like this: .. code-block:: ini # RING TONES # We need a default ring tone. Otherwise the phone would not ring at all, if # something with the custom ring tone creation goes wrong. ringtone_file=/home/USERNAME/.twinkle/sounds/incoming_call.wav ringback_file=/home/USERNAME/.twinkle/sounds/outgoing_call.wav # SCRIPTS script_incoming_call=/home/USERNAME/.twinkle/scripts/incoming_call.py script_in_call_answered= script_in_call_failed=/home/USERNAME/.twinkle/scripts/incoming_call_failed.py script_outgoing_call= script_out_call_answered= script_out_call_failed= script_local_release=/home/USERNAME/.twinkle/scripts/incoming_call_ended.py script_remote_release=/home/USERNAME/.twinkle/scripts/incoming_call_ended.py Zsh --- The file ``misc/zsh/_khard`` contains a khard cli completion function for the zsh and ``misc/zsh/_email-khard`` completes email addresses. Install by copying to a directory where zsh searches for completion functions (the ``$fpath`` array). If you, for example, put all completion functions into the folder ``~/.zsh/completions`` you must add the following to your zsh main config file: .. code-block:: zsh fpath=( $HOME/.zsh/completions $fpath ) autoload -U compinit compinit sdiff ----- Use the wrapper script ``misc/sdiff/sdiff_khard_wrapper.sh`` if you want to use sdiff as your contact merging tool. Just make the script executable and set it as your merge editor in khard's config file: .. code-block:: ini merge_editor = /path/to/sdiff_khard_wrapper.sh .. include:: davcontroller.rst khard-0.17.0/khard/000077500000000000000000000000001371517016500137675ustar00rootroot00000000000000khard-0.17.0/khard/__init__.py000066400000000000000000000000001371517016500160660ustar00rootroot00000000000000khard-0.17.0/khard/__main__.py000066400000000000000000000000701371517016500160560ustar00rootroot00000000000000#!/usr/bin/env python3 from .khard import main main() khard-0.17.0/khard/actions.py000066400000000000000000000042651371517016500160100ustar00rootroot00000000000000"""Names and aliases for the subcommands on the command line""" from typing import Dict, Generator, Iterable, List, Optional class Actions: """A class to manage the names and aliases of the command line subcommands.""" action_map: Dict[str, List[str]] = { "add-email": [], "addressbooks": ["abooks"], "birthdays": ["bdays"], "copy": ["cp"], "email": [], "export": [], "filename": ["file"], "list": ["ls"], "merge": [], "edit": ["modify", "ed"], "move": ["mv"], "new": ["add"], "phone": [], "postaddress": ["post", "postaddr"], "remove": ["delete", "del", "rm"], "show": ["details"], "source": ["src"], "template": [], } @classmethod def get_action(cls, alias: str) -> Optional[str]: """Find the name of the action for the supplied alias. If no action is asociated with the given alias, None is returned. :param alias: the alias to look up :rturns: the name of the corresponding action or None """ for action, alias_list in cls.action_map.items(): if alias in alias_list: return action return None @classmethod def get_aliases(cls, action: str) -> Optional[List[str]]: """Find all aliases for the given action. If there is no such action, None is returned. :param action: the action name to look up :returns: the list of aliases corresponding to the action or None """ return cls.action_map.get(action) @classmethod def get_actions(cls) -> Iterable[str]: """Find the names of all defined actions. :returns: all action names """ return cls.action_map.keys() @classmethod def get_all(cls) -> Generator[str, None, None]: """Find the names of all defined actions and their aliases. :returns: the names of all actions and aliases """ for key, value in cls.action_map.items(): yield key yield from value khard-0.17.0/khard/address_book.py000066400000000000000000000261771371517016500170150ustar00rootroot00000000000000"""A simple class to load and manage the vcard files from disk.""" import abc import binascii import glob import logging import os import re from typing import Dict, Generator, Iterator, List, Optional, Union import vobject.base from . import carddav_object from .query import AnyQuery, Query logger = logging.getLogger(__name__) class AddressBookParseError(Exception): """Indicate an error while parsing data from an address book backend.""" def __init__(self, filename: str, abook: str, reason: Exception) -> None: """Store the filename that caused the error.""" super().__init__() self.filename = filename self.abook = abook self.reason = reason def __str__(self) -> str: return "Error when parsing {} in address book {}: {}".format( self.filename, self.abook, self.reason) class AddressBookNameError(Exception): """Indicate an error with an address book name.""" class AddressBook(metaclass=abc.ABCMeta): """The base class of all address book implementations.""" def __init__(self, name: str) -> None: """:param str name: the name to identify the address book""" self._loaded = False self.contacts: Dict[str, "carddav_object.CarddavObject"] = {} self._short_uids: Optional[Dict[str, "carddav_object.CarddavObject"]] = None self.name = name def __str__(self) -> str: return self.name def __eq__(self, other: object) -> bool: return isinstance(other, type(self)) and self.name == other.name def __ne__(self, other: object) -> bool: return not self == other @staticmethod def _compare_uids(uid1: str, uid2: str) -> int: """Calculate the minimum length of initial substrings of uid1 and uid2 for them to be different. :param uid1: first uid to compare :param uid2: second uid to compare :returns: the length of the shortes unequal initial substrings """ return len(os.path.commonprefix((uid1, uid2))) def search(self, query: Query) -> Generator["carddav_object.CarddavObject", None, None]: """Search this address book for contacts matching the query. The backend for this address book migth be load()ed if needed. :param query: the query to search for :yields: all found contacts """ logger.debug('address book %s, searching with %s', self.name, query) if not self._loaded: self.load(query) for contact in self.contacts.values(): if query.match(contact): yield contact def get_short_uid_dict(self, query: Query = AnyQuery()) -> Dict[ str, "carddav_object.CarddavObject"]: """Create a dictionary of shortend UIDs for all contacts. All arguments are only used if the address book is not yet initialized and will just be handed to self.load(). :param query: see self.load() :returns: the contacts mapped by the shortes unique prefix of their UID """ if self._short_uids is None: if not self._loaded: self.load(query) if not self.contacts: self._short_uids = {} elif len(self.contacts) == 1: self._short_uids = {uid[0:1]: contact for uid, contact in self.contacts.items()} else: self._short_uids = {} sorted_uids = sorted(self.contacts) # Prepare for the loop; the first and last items are handled # seperatly. item0, item1 = sorted_uids[:2] same1 = self._compare_uids(item0, item1) self._short_uids[item0[:same1 + 1]] = self.contacts[item0] for item_new in sorted_uids[2:]: # shift the items and the common prefix lenght one further item0, item1 = item1, item_new same0, same1 = same1, self._compare_uids(item0, item1) # compute the final prefix length for item1 same = max(same0, same1) self._short_uids[item0[:same + 1]] = self.contacts[item0] # Save the last item. self._short_uids[item1[:same1 + 1]] = self.contacts[item1] return self._short_uids def get_short_uid(self, uid: str) -> str: """Get the shortend UID for the given UID. :param uid: the full UID to shorten :returns: the shortend uid or the empty string """ if uid: short_uids = self.get_short_uid_dict() for length_of_uid in range(len(uid), 0, -1): if short_uids.get(uid[:length_of_uid]) is not None: return uid[:length_of_uid] return "" @abc.abstractmethod def load(self, query: Query = AnyQuery()) -> None: """Load the vCards from the backing store. If a query is given loading is limited to entries which match the query. If the query is None all entries will be loaded. :param query: the query to limit loading to matching entries :returns: the number of loaded contacts and the number of errors """ class VdirAddressBook(AddressBook): """An AddressBook implementation based on a vdir. This address book can load contacts from vcard files that reside in one direcotry on disk. """ def __init__(self, name: str, path: str, private_objects: Optional[List[str]] = None, localize_dates: bool = True, skip: bool = False) -> None: """ :param name: the name to identify the address book :param path: the path to the backing structure on disk :param private_objects: the names of private vCard extension fields to load :param localize_dates: whether to display dates in the local format :param skip: skip unparsable vCard files """ self.path = os.path.expanduser(os.path.expandvars(path)) if not os.path.isdir(self.path): raise FileNotFoundError("[Errno 2] The path {} to the address book" " {} does not exist.".format(path, name)) self._private_objects = private_objects or [] self._localize_dates = localize_dates self._skip = skip super().__init__(name) def load(self, query: Query = AnyQuery(), search_in_source_files: bool = False) -> None: """Load all vcard files in this address book from disk. If a search string is given only files which contents match that will be loaded. :param query: query to limit the vcards that should be parsed :param search_in_source_files: apply search regexp directly on the .vcf files to speed up parsing (less accurate) :throws: AddressBookParseError """ if self._loaded: return logger.debug('Loading Vdir %s with query %s', self.name, query) errors = 0 for filename in glob.glob(os.path.join(self.path, "*.vcf")): try: card = carddav_object.CarddavObject.from_file( self, filename, query if search_in_source_files else AnyQuery(), self._private_objects, self._localize_dates) if card is None: continue except (IOError, vobject.base.ParseError, binascii.Error) as err: verb = "open" if isinstance(err, IOError) else "parse" logger.error("Error: Could not %s file %s\n%s", verb, filename, err) if self._skip: errors += 1 else: raise AddressBookParseError(filename, self.name, err) else: uid = card.uid if not uid: logger.warning("Card %s from address book %s has no UID " "and will not be available.", card, self.name) elif uid in self.contacts: logger.warning( "Card %s and %s from address book %s have the same " "UID. The former will not be available.", card, self.contacts[uid], self.name) else: self.contacts[uid] = card self._loaded = True if errors: logger.warning( "%d of %d vCard files of address book %s could not be parsed.", errors, len(self.contacts) + errors, self) logger.debug('Loded %s contacts from address book %s.', len(self.contacts), self.name) class AddressBookCollection(AddressBook): """A collection of several address books. This represents a temporary merege of the contact collections provided by the underlying address books. On load, all contacts from all subaddressbooks are copied into a dict in this address book. This allows this class to use all other methods from the parent AddressBook class. """ def __init__(self, name: str, abooks: List[VdirAddressBook]) -> None: """ :param name: the name to identify the address book :param abooks: a list of address books to combine in this collection """ super().__init__(name) self._abooks = {ab.name: ab for ab in abooks} def load(self, query: Query = AnyQuery()) -> None: """Load the wrapped address books with the given parameters All parameters will be handed to VdirAddressBook.load. :param query: a query to limit the vcards that should be parsed :throws: AddressBookParseError """ if self._loaded: return logger.debug('Loading collection %s with query %s', self.name, query) for abook in self._abooks.values(): abook.load(query) for uid in abook.contacts: if uid in self.contacts: logger.warning( "Card %s from address book %s will not be available " "because there is already another card with the same " "UID: %s", abook.contacts[uid], abook, uid) else: self.contacts[uid] = abook.contacts[uid] self._loaded = True logger.debug('Loded %s contacts from address book %s.', len(self.contacts), self.name) def __getitem__(self, key: Union[int, str]) -> VdirAddressBook: """Get one of the backing address books by name or index :param key: the name of the address book to get or its index :returns: the matching address book :throws: KeyError """ if isinstance(key, str): return self._abooks[key] return list(self._abooks.values())[key] def __iter__(self) -> Iterator[VdirAddressBook]: """:return: an iterator over the underlying address books""" return iter(self._abooks.values()) def __len__(self) -> int: return len(self._abooks) khard-0.17.0/khard/carddav_object.py000066400000000000000000002171331371517016500173020ustar00rootroot00000000000000"""Classes and logic to handle vCards in khard. This module explicitly supports the vCard specifications version 3.0 and 4.0 which can be found here: - version 3.0: https://tools.ietf.org/html/rfc2426 - version 4.0: https://tools.ietf.org/html/rfc6350 """ import copy import datetime import locale import logging import os import re import sys import time from typing import Callable, Dict, List, Optional, Tuple, Union from atomicwrites import atomic_write from ruamel import yaml import vobject from . import address_book from . import helpers from .object_type import ObjectType from .query import AnyQuery, Query logger = logging.getLogger(__name__) def convert_to_vcard(name: str, value: Union[str, List[str]], allowed_object_type: ObjectType) -> Union[str, List[str]]: """converts user input into vcard compatible data structures :param name: object name, only required for error messages :param value: user input :param allowed_object_type: set the accepted return type for vcard attribute :returns: cleaned user input, ready for vcard or a ValueError """ if isinstance(value, str): if allowed_object_type == ObjectType.list_with_strings: return [value.strip()] return value.strip() if isinstance(value, list): if allowed_object_type == ObjectType.string: raise ValueError("Error: " + name + " must contain a string.") if not all(isinstance(entry, str) for entry in value): raise ValueError("Error: " + name + " must not contain a nested list") # filter out empty list items and strip leading and trailing space return [x.strip() for x in value if x.strip()] if allowed_object_type == ObjectType.string: raise ValueError("Error: " + name + " must be a string.") if allowed_object_type == ObjectType.list_with_strings: raise ValueError("Error: " + name + " must be a list with strings.") raise ValueError("Error: " + name + " must be a string or a list with strings.") def multi_property_key(item: Union[str, Dict]) -> List: """key function to pass to sorted(), allowing sorting of dicts with lists and strings. Dicts will be sorted by their label, after other types. :param item: member of the list being sorted :type item: a dict with a single entry or any sortable type :returns: a list with two members. The first is int(isinstance(item, dict). The second is either the key from the dict or the unchanged item if it is not a dict. :rtype: list(int, type(item)) or list(int, str) """ if isinstance(item, dict): return [1, list(item)[0]] return [0, item] class VCardWrapper: """Wrapper class around a vobject.vCard object. This class can wrap a single vCard and presents its data in a manner suitable for khard. Additionally some details of the vCard specifications in RFC 2426 (version 3.0) and RFC 6350 (version 4.0) that are not enforced by the vobject library are enforced here. """ _default_version = "3.0" _supported_versions = ("3.0", "4.0") # vcard v3.0 supports the following type values phone_types_v3 = ("bbs", "car", "cell", "fax", "home", "isdn", "msg", "modem", "pager", "pcs", "video", "voice", "work") email_types_v3 = ("home", "internet", "work", "x400") address_types_v3 = ("dom", "intl", "home", "parcel", "postal", "work") # vcard v4.0 supports the following type values phone_types_v4 = ("text", "voice", "fax", "cell", "video", "pager", "textphone", "home", "work") email_types_v4 = ("home", "internet", "work") address_types_v4 = ("home", "work") def __init__(self, vcard: vobject.vCard, version: Optional[str] = None ) -> None: """Initialize the wrapper around the given vcard. :param vobject.vCard vcard: the vCard to wrap :param version: the version of the RFC to use (if the card has none) :type version: str or None """ self.vcard = vcard if not self.version: version = version or self._default_version logger.warning("Wrapping unversioned vCard object, setting " "version to %s.", version) self.version = version elif self.version not in self._supported_versions: logger.warning("Wrapping vCard with unsupported version %s, this " "might change any incompatible attributes.", version) def __str__(self) -> str: return self.formatted_name def _get_string_field(self, field: str) -> str: """Get a string field from the underlying vCard. :param field: the field value to get :returns: the field value or the empty string """ try: return getattr(self.vcard, field).value except AttributeError: return "" def _get_multi_property(self, name: str) -> List: """Get a vCard property that can exist more than once. It does not matter what the individual vcard properties store as their value. This function returnes them untouched inside an agregating list. If the property is part of a group containing exactly two items, with exactly one ABLABEL. the property will be prefixed with that ABLABEL. :param name: the name of the property (should be UPPER case) :returns: the values from all occurences of the named property """ values = [] for child in self.vcard.getChildren(): if child.name == name: ablabel = self._get_ablabel(child) if ablabel: values.append({ablabel: child.value}) else: values.append(child.value) return sorted(values, key=multi_property_key) def _delete_vcard_object(self, name: str) -> None: """Delete all fields with the given name from the underlying vCard. If a field that will be deleted is in a group with an X-ABLABEL field, that X-ABLABEL field will also be deleted. These fields are commonly added by the Apple address book to attach custom labels to some fields. :param name: the name of the fields to delete """ # first collect all vcard items, which should be removed to_be_removed = [] for child in self.vcard.getChildren(): if child.name == name: if child.group: for label in self.vcard.getChildren(): if label.name == "X-ABLABEL" and \ label.group == child.group: to_be_removed.append(label) to_be_removed.append(child) # then delete them one by one for item in to_be_removed: self.vcard.remove(item) @staticmethod def _parse_type_value(types: List[str], supported_types: List[str] ) -> Tuple[List[str], List[str], int]: """Parse type value of phone numbers, email and post addresses. :param types: list of type values :param supported_types: all allowed standard types :returns: tuple of standard and custom types and pref integer """ custom_types = [] standard_types = [] pref = 0 for type in types: type = type.strip() if type: if type.lower() in supported_types: standard_types.append(type) elif type.lower() == "pref": pref += 1 elif re.match(r"^pref=\d{1,2}$", type.lower()): pref += int(type.split("=")[1]) else: if type.lower().startswith("x-"): custom_types.append(type[2:]) standard_types.append(type) else: custom_types.append(type) standard_types.append("X-{}".format(type)) return (standard_types, custom_types, pref) def _get_types_for_vcard_object(self, object: vobject.base.ContentLine, default_type: str) -> List[str]: """get list of types for phone number, email or post address :param object: vcard class object :param default_type: use if the object contains no type :returns: list of type labels """ type_list = [] # try to find label group for custom value type if object.group: for label in self.vcard.getChildren(): if label.name == "X-ABLABEL" and label.group == object.group: custom_type = label.value.strip() if custom_type: type_list.append(custom_type) # then load type from params dict standard_types = object.params.get("TYPE") if standard_types is not None: if not isinstance(standard_types, list): standard_types = [standard_types] for type in standard_types: type = type.strip() if type and type.lower() != "pref": if not type.lower().startswith("x-"): type_list.append(type) elif type[2:].lower() not in [x.lower() for x in type_list]: # add x-custom type in case it's not already added by # custom label for loop above but strip x- before type_list.append(type[2:]) # try to get pref parameter from vcard version 4.0 try: type_list.append("pref={}".format( int(object.params.get("PREF")[0]))) except (IndexError, TypeError, ValueError): # else try to determine, if type params contain pref attribute try: for type in object.params.get("TYPE"): if type.lower() == "pref" and "pref" not in type_list: type_list.append("pref") except TypeError: pass # return type_list or default type if type_list: return type_list return [default_type] @property def version(self) -> str: return self._get_string_field("version") @version.setter def version(self, value: str) -> None: if value not in self._supported_versions: logger.warning("Setting vcard version to unsupported version %s", value) # All vCards should only always have one version, this is a requirement # for version 4 but also makes sense for all other versions. self._delete_vcard_object("VERSION") version = self.vcard.add("version") version.value = convert_to_vcard("version", value, ObjectType.string) @property def uid(self) -> str: return self._get_string_field("uid") @uid.setter def uid(self, value: str) -> None: # All vCards should only always have one UID, this is a requirement # for version 4 but also makes sense for all other versions. self._delete_vcard_object("UID") uid = self.vcard.add('uid') uid.value = convert_to_vcard("uid", value, ObjectType.string) def _update_revision(self) -> None: """Generate a new REV field for the vCard, replace any existing All vCards should only always have one revision, this is a requirement for version 4 but also makes sense for all other versions. :rtype: NoneType """ self._delete_vcard_object("REV") rev = self.vcard.add('rev') rev.value = datetime.datetime.now().strftime("%Y%m%dT%H%M%SZ") @property def birthday(self) -> Union[None, str, datetime.datetime]: """Return the birthday as a datetime object or a string depending on weather it is of type text or not. If no birthday is present in the vcard None is returned. :returns: contacts birthday or None if not available """ # vcard 4.0 could contain a single text value try: if self.vcard.bday.params.get("VALUE")[0] == "text": return self.vcard.bday.value except (AttributeError, IndexError, TypeError): pass # else try to convert to a datetime object try: return helpers.string_to_date(self.vcard.bday.value) except (AttributeError, ValueError): pass return None @birthday.setter def birthday(self, date: Union[str, datetime.datetime]) -> None: """Store the given date as BDAY in the vcard. :param date: the new date to store as birthday """ value, text = self._prepare_birthday_value(date) if value is None: logger.warning('Failed to set anniversary to %s', date) return bday = self.vcard.add('bday') bday.value = value if text: bday.params['VALUE'] = ['text'] @property def anniversary(self) -> Union[None, str, datetime.datetime]: """ :returns: contacts anniversary or None if not available """ # vcard 4.0 could contain a single text value try: if self.vcard.anniversary.params.get("VALUE")[0] == "text": return self.vcard.anniversary.value except (AttributeError, IndexError, TypeError): pass # else try to convert to a datetime object try: return helpers.string_to_date(self.vcard.anniversary.value) except (AttributeError, ValueError): # vcard 3.0: x-anniversary (private object) try: return helpers.string_to_date(self.vcard.x_anniversary.value) except (AttributeError, ValueError): pass return None @anniversary.setter def anniversary(self, date: Union[str, datetime.datetime]) -> None: value, text = self._prepare_birthday_value(date) if value is None: logger.warning('Failed to set anniversary to %s', date) return if text: anniversary = self.vcard.add('anniversary') anniversary.params['VALUE'] = ['text'] anniversary.value = value elif self.version == "4.0": self.vcard.add('anniversary').value = value else: self.vcard.add('x-anniversary').value = value def _get_ablabel(self, item: vobject.base.ContentLine) -> str: """Get an ABLABEL for a specified item in the vCard. Will return the ABLABEL only if the item is part of a group with exactly two items, exactly one of which is an ABLABEL. :param item: the item to be labelled :returns: the ABLABEL in the circumstances above or an empty string """ label = "" if item.group: count = 0 for child in self.vcard.getChildren(): if child.group and child.group == item.group: count += 1 if child.name == "X-ABLABEL": if label == "": label = child.value else: return "" if count != 2: label = "" return label def _get_new_group(self, group_type: str = "") -> str: """Get an unused group name for adding new groups. Uses the form item123 or itemgroup_type123 if a grouptype is specified. :param group_type: (Optional) a string to add between "item" and the number :returns: the name of the first unused group of the specified form """ counter = 1 while True: group_name = "item{}{}".format(group_type, counter) for child in self.vcard.getChildren(): if child.group and child.group == group_name: counter += 1 break else: return group_name def _add_labelled_object( self, obj_type: str, user_input, name_groups: bool = False, allowed_object_type: ObjectType = ObjectType.string) -> None: """Add an object to the VCARD. If user_input is a dict, the object will be added to a group with an ABLABEL created from the key of the dict. :param obj_type: type of object to add to the VCARD. :param user_input: Contents of the object to add. If a dict :type user_input: str or list(str) or dict(str) or dict(list(str)) :param name_groups: (Optional) If True, use the obj_type in the group name for labelled objects. :param allowed_object_type: (Optional) set the accepted return type for vcard attribute """ obj = self.vcard.add(obj_type) if isinstance(user_input, dict): if len(user_input) > 1: raise ValueError( "Error: {} must be a string or a dict containing one " "key/value pair.".format(obj_type)) label = list(user_input)[0] group_name = self._get_new_group(obj_type if name_groups else "") obj.group = group_name obj.value = convert_to_vcard(obj_type, user_input[label], allowed_object_type) ablabel_obj = self.vcard.add('X-ABLABEL') ablabel_obj.group = group_name ablabel_obj.value = label else: obj.value = convert_to_vcard(obj_type, user_input, allowed_object_type) def _prepare_birthday_value(self, date: Union[str, datetime.datetime] ) -> Tuple[Optional[str], bool]: """Prepare a value to be stored in a BDAY or ANNIVERSARY attribute. :param date: the date like value to be stored :type date: datetime.datetime or str :returns: the object to set as the .value for the attribute and weather it should be stored as plain text :rtype: tuple(str,bool) """ if isinstance(date, str): if self.version == "4.0": return date.strip(), True return None, False tz = date.tzname() if date.year == 1900 and date.month != 0 and date.day != 0 \ and date.hour == 0 and date.minute == 0 and date.second == 0 \ and self.version == "4.0": fmt = '--%m%d' elif tz and tz[3:]: if self.version == "4.0": fmt = "%Y%m%dT%H%M%S{}".format(tz[3:]) else: fmt = "%FT%T{}".format(tz[3:]) elif date.hour != 0 or date.minute != 0 or date.second != 0: if self.version == "4.0": fmt = "%Y%m%dT%H%M%SZ" else: fmt = "%FT%TZ" else: if self.version == "4.0": fmt = "%Y%m%d" else: fmt = "%F" return date.strftime(fmt), False @property def formatted_name(self) -> str: return self._get_string_field("fn") @formatted_name.setter def formatted_name(self, value: str) -> None: """Set the FN field to the new value. All previously existing FN fields are deleted. Version 4 of the specs requires the vCard to only habe one FN field. For other versions we enforce this equally. :param str value: the new formatted name :returns: None """ self._delete_vcard_object("FN") if value: final = convert_to_vcard("FN", value, ObjectType.string) elif self._get_first_names() or self._get_last_names(): # autofill the FN field from the N field names = [self._get_name_prefixes(), self._get_first_names(), self._get_last_names(), self._get_name_suffixes()] names = [x for x in names if x] final = helpers.list_to_string(names, " ") else: # add an empty FN final = "" self.vcard.add("FN").value = final def _get_names_part(self, part: str) -> List[str]: """Get some part of the "N" entry in the vCard as a list :param part: the name to get e.g. "prefix" or "given" :returns: a list of entries for this name part """ try: the_list = getattr(self.vcard.n.value, part) except AttributeError: return [] else: # check if list only contains empty strings if not ''.join(the_list): return [] return the_list if isinstance(the_list, list) else [the_list] def _get_name_prefixes(self) -> List[str]: return self._get_names_part("prefix") def _get_first_names(self) -> List[str]: return self._get_names_part("given") def _get_additional_names(self) -> List[str]: return self._get_names_part("additional") def _get_last_names(self) -> List[str]: return self._get_names_part("family") def _get_name_suffixes(self) -> List[str]: return self._get_names_part("suffix") def get_first_name_last_name(self) -> str: """Compute the full name of the contact by joining first, additional and last names together """ names = self._get_first_names() + self._get_additional_names() + \ self._get_last_names() if names: return helpers.list_to_string(names, " ") return self.formatted_name def get_last_name_first_name(self) -> str: """Compute the full name of the contact by joining the last names and then after a comma the first and additional names together """ last_names: List[str] = [] if self._get_last_names(): last_names += self._get_last_names() first_and_additional_names = self._get_first_names() + \ self._get_additional_names() if last_names and first_and_additional_names: return "{}, {}".format( helpers.list_to_string(last_names, " "), helpers.list_to_string(first_and_additional_names, " ")) if last_names: return helpers.list_to_string(last_names, " ") if first_and_additional_names: return helpers.list_to_string(first_and_additional_names, " ") return self.formatted_name def _add_name(self, prefix: Union[str, List[str]], first_name: Union[str, List[str]], additional_name: Union[str, List[str]], last_name: Union[str, List[str]], suffix: Union[str, List[str]]) -> None: """Add an N entry to the vCard. No old entries are affected. :param prefix: :param first_name: :param additional_name: :param last_name: :param suffix: """ name_obj = self.vcard.add('n') stringlist = ObjectType.string_or_list_with_strings name_obj.value = vobject.vcard.Name( prefix=convert_to_vcard("name prefix", prefix, stringlist), given=convert_to_vcard("first name", first_name, stringlist), additional=convert_to_vcard("additional name", additional_name, stringlist), family=convert_to_vcard("last name", last_name, stringlist), suffix=convert_to_vcard("name suffix", suffix, stringlist)) @property def organisations(self) -> List[Union[List[str], Dict[str, List[str]]]]: """ :returns: list of organisations, sorted alphabetically """ return self._get_multi_property("ORG") def _add_organisation(self, organisation: Union[str, List[str]]) -> None: """Add one ORG entry to the underlying vcard :param organisation: the value to add """ self._add_labelled_object("org", organisation, True, ObjectType.list_with_strings) # check if fn attribute is already present if not self.vcard.getChildValue("fn") and self.organisations: # if not, set fn to organisation name first_org = self.organisations[0] if isinstance(first_org, dict): first_org = list(first_org.values())[0] org_value = helpers.list_to_string(first_org, ", ") self.formatted_name = org_value.replace("\n", " ").replace("\\", "") showas_obj = self.vcard.add('x-abshowas') showas_obj.value = "COMPANY" @property def titles(self) -> List[Union[str, Dict[str, str]]]: return self._get_multi_property("TITLE") def _add_title(self, title) -> None: self._add_labelled_object("title", title, True) @property def roles(self) -> List[Union[str, Dict[str, str]]]: return self._get_multi_property("ROLE") def _add_role(self, role) -> None: self._add_labelled_object("role", role, True) @property def nicknames(self) -> List[Union[str, Dict[str, str]]]: return self._get_multi_property("NICKNAME") def _add_nickname(self, nickname) -> None: self._add_labelled_object("nickname", nickname, True) @property def notes(self) -> List[Union[str, Dict[str, str]]]: return self._get_multi_property("NOTE") def _add_note(self, note) -> None: self._add_labelled_object("note", note, True) @property def webpages(self) -> List[Union[str, Dict[str, str]]]: return self._get_multi_property("URL") def _add_webpage(self, webpage) -> None: self._add_labelled_object("url", webpage, True) @property def categories(self) -> Union[List[str], List[List[str]]]: category_list = [] for child in self.vcard.getChildren(): if child.name == "CATEGORIES": value = child.value category_list.append( value if isinstance(value, list) else [value]) if len(category_list) == 1: return category_list[0] return sorted(category_list) def _add_category(self, categories: List[str]) -> None: """Add categories to the vCard :param categories: """ categories_obj = self.vcard.add('categories') categories_obj.value = convert_to_vcard("category", categories, ObjectType.list_with_strings) @property def phone_numbers(self) -> Dict[str, List[str]]: """ :returns: dict of type and phone number list """ phone_dict: Dict[str, List[str]] = {} for child in self.vcard.getChildren(): if child.name == "TEL": # phone types type = helpers.list_to_string( self._get_types_for_vcard_object(child, "voice"), ", ") if type not in phone_dict: phone_dict[type] = [] # phone value # # vcard version 4.0 allows URI scheme "tel" in phone attribute value # Doc: https://tools.ietf.org/html/rfc6350#section-6.4.1 # example: TEL;VALUE=uri;PREF=1;TYPE="voice,home":tel:+1-555-555-5555;ext=5555 if child.value.lower().startswith("tel:"): # cut off the "tel:" uri prefix phone_dict[type].append(child.value[4:]) else: # free text field phone_dict[type].append(child.value) # sort phone number lists for number_list in phone_dict.values(): number_list.sort() return phone_dict def _add_phone_number(self, type, number): standard_types, custom_types, pref = self._parse_type_value( helpers.string_to_list(type, ","), self.phone_types_v4 if self.version == "4.0" else self.phone_types_v3) if not standard_types and not custom_types and pref == 0: raise ValueError("Error: label for phone number " + number + " is missing.") if len(custom_types) > 1: raise ValueError("Error: phone number " + number + " got more " "than one custom label: " + helpers.list_to_string(custom_types, ", ")) phone_obj = self.vcard.add('tel') if self.version == "4.0": phone_obj.value = "tel:{}".format( convert_to_vcard("phone number", number, ObjectType.string)) phone_obj.params['VALUE'] = ["uri"] if pref > 0: phone_obj.params['PREF'] = str(pref) else: phone_obj.value = convert_to_vcard("phone number", number, ObjectType.string) if pref > 0: standard_types.append("pref") if standard_types: phone_obj.params['TYPE'] = standard_types if custom_types: custom_label_count = 0 for label in self.vcard.getChildren(): if label.name == "X-ABLABEL" and label.group.startswith( "itemtel"): custom_label_count += 1 group_name = "itemtel{}".format(custom_label_count + 1) phone_obj.group = group_name label_obj = self.vcard.add('x-ablabel') label_obj.group = group_name label_obj.value = custom_types[0] @property def emails(self) -> Dict[str, List[str]]: """ :returns: dict of type and email address list """ email_dict: Dict[str, List[str]] = {} for child in self.vcard.getChildren(): if child.name == "EMAIL": type = helpers.list_to_string( self._get_types_for_vcard_object(child, "internet"), ", ") if type not in email_dict: email_dict[type] = [] email_dict[type].append(child.value) # sort email address lists for email_list in email_dict.values(): email_list.sort() return email_dict def add_email(self, type, address): standard_types, custom_types, pref = self._parse_type_value( helpers.string_to_list(type, ","), self.email_types_v4 if self.version == "4.0" else self.email_types_v3) if not standard_types and not custom_types and pref == 0: raise ValueError("Error: label for email address " + address + " is missing.") if len(custom_types) > 1: raise ValueError("Error: email address " + address + " got more " "than one custom label: " + helpers.list_to_string(custom_types, ", ")) email_obj = self.vcard.add('email') email_obj.value = convert_to_vcard("email address", address, ObjectType.string) if self.version == "4.0": if pref > 0: email_obj.params['PREF'] = str(pref) else: if pref > 0: standard_types.append("pref") if standard_types: email_obj.params['TYPE'] = standard_types if custom_types: custom_label_count = 0 for label in self.vcard.getChildren(): if label.name == "X-ABLABEL" and label.group.startswith( "itememail"): custom_label_count += 1 group_name = "itememail{}".format(custom_label_count + 1) email_obj.group = group_name label_obj = self.vcard.add('x-ablabel') label_obj.group = group_name label_obj.value = custom_types[0] @property def post_addresses(self) -> Dict[str, List[Dict[str, Union[List, str]]]]: """ :returns: dict of type and post address list """ post_adr_dict: Dict[str, List[Dict[str, Union[List, str]]]] = {} for child in self.vcard.getChildren(): if child.name == "ADR": type = helpers.list_to_string(self._get_types_for_vcard_object( child, "home"), ", ") if type not in post_adr_dict: post_adr_dict[type] = [] post_adr_dict[type].append({"box": child.value.box, "extended": child.value.extended, "street": child.value.street, "code": child.value.code, "city": child.value.city, "region": child.value.region, "country": child.value.country}) # sort post address lists for post_adr_list in post_adr_dict.values(): post_adr_list.sort(key=lambda x: ( helpers.list_to_string(x['city'], " ").lower(), helpers.list_to_string(x['street'], " ").lower())) return post_adr_dict def get_formatted_post_addresses(self) -> Dict[str, List[str]]: list2str = helpers.list_to_string formatted_post_adr_dict: Dict[str, List[str]] = {} for type, post_adr_list in self.post_addresses.items(): formatted_post_adr_dict[type] = [] for post_adr in post_adr_list: get = lambda name: list2str(post_adr.get(name, ""), " ") strings = [] if "street" in post_adr: strings.append(list2str(post_adr.get("street", ""), "\n")) if "box" in post_adr and "extended" in post_adr: strings.append("{} {}".format(get("box"), get("extended"))) elif "box" in post_adr: strings.append(get("box")) elif "extended" in post_adr: strings.append(get("extended")) if "code" in post_adr and "city" in post_adr: strings.append("{} {}".format(get("code"), get("city"))) elif "code" in post_adr: strings.append(get("code")) elif "city" in post_adr: strings.append(get("city")) if "region" in post_adr and "country" in post_adr: strings.append("{}, {}".format(get("region"), get("country"))) elif "region" in post_adr: strings.append(get("region")) elif "country" in post_adr: strings.append(get("country")) formatted_post_adr_dict[type].append('\n'.join(strings)) return formatted_post_adr_dict def _add_post_address(self, type, box, extended, street, code, city, region, country): standard_types, custom_types, pref = self._parse_type_value( helpers.string_to_list(type, ","), self.address_types_v4 if self.version == "4.0" else self.address_types_v3) if not standard_types and not custom_types and pref == 0: raise ValueError("Error: label for post address " + street + " is missing.") if len(custom_types) > 1: raise ValueError("Error: post address " + street + " got more " "than one custom " "label: " + helpers.list_to_string(custom_types, ", ")) adr_obj = self.vcard.add('adr') adr_obj.value = vobject.vcard.Address( box=convert_to_vcard("box address field", box, ObjectType.string_or_list_with_strings), extended=convert_to_vcard("extended address field", extended, ObjectType.string_or_list_with_strings), street=convert_to_vcard("street", street, ObjectType.string_or_list_with_strings), code=convert_to_vcard("post code", code, ObjectType.string_or_list_with_strings), city=convert_to_vcard("city", city, ObjectType.string_or_list_with_strings), region=convert_to_vcard("region", region, ObjectType.string_or_list_with_strings), country=convert_to_vcard("country", country, ObjectType.string_or_list_with_strings)) if self.version == "4.0": if pref > 0: adr_obj.params['PREF'] = str(pref) else: if pref > 0: standard_types.append("pref") if standard_types: adr_obj.params['TYPE'] = standard_types if custom_types: custom_label_count = 0 for label in self.vcard.getChildren(): if label.name == "X-ABLABEL" and label.group.startswith( "itemadr"): custom_label_count += 1 group_name = "itemadr{}".format(custom_label_count + 1) adr_obj.group = group_name label_obj = self.vcard.add('x-ablabel') label_obj.group = group_name label_obj.value = custom_types[0] class YAMLEditable(VCardWrapper): """Conversion of vcards to YAML and updateing the vcard from YAML""" def __init__(self, vcard: vobject.vCard, supported_private_objects: Optional[List[str]] = None, version: Optional[str] = None, localize_dates: bool = False ) -> None: """Initialize atributes needed for yaml conversions :param supported_private_objects: the list of private property names that will be loaded from the actual vcard and represented in this pobject :param version: the version of the RFC to use in this card :param localize_dates: should the formatted output of anniversary and birthday be localized or should the isoformat be used instead """ self.supported_private_objects = supported_private_objects or [] self.localize_dates = localize_dates super().__init__(vcard, version) ##################### # getters and setters ##################### def _get_private_objects(self) -> Dict[str, List[str]]: supported = [x.lower() for x in self.supported_private_objects] private_objects: Dict[str, List[str]] = {} for child in self.vcard.getChildren(): lower = child.name.lower() if lower.startswith("x-") and lower[2:] in supported: key_index = supported.index(lower[2:]) key = self.supported_private_objects[key_index] if key not in private_objects: private_objects[key] = [] ablabel = self._get_ablabel(child) private_objects[key].append( {ablabel: child.value} if ablabel else child.value) # sort private object lists for value in private_objects.values(): value.sort(key=multi_property_key) return private_objects def _add_private_object(self, key: str, value) -> None: self._add_labelled_object('X-' + key.upper(), value) def get_formatted_anniversary(self) -> str: return self._format_date_object(self.anniversary, self.localize_dates) def get_formatted_birthday(self) -> str: return self._format_date_object(self.birthday, self.localize_dates) ####################### # object helper methods ####################### @staticmethod def _format_date_object(date: Union[None, str, datetime.datetime], localize: bool) -> str: if not date: return "" if isinstance(date, str): return date if date.year == 1900 and date.month != 0 and date.day != 0 \ and date.hour == 0 and date.minute == 0 and date.second == 0: return date.strftime("--%m-%d") tz = date.tzname() if (tz and tz[3:]) or (date.hour != 0 or date.minute != 0 or date.second != 0): if localize: return date.strftime(locale.nl_langinfo(locale.D_T_FMT)) utc_offset = -time.timezone / 60 / 60 return date.strftime("%FT%T+{:02}:00".format(int(utc_offset))) if localize: return date.strftime(locale.nl_langinfo(locale.D_FMT)) return date.strftime("%F") @staticmethod def _filter_invalid_tags(contents: str) -> str: for pat, repl in [('aim', 'AIM'), ('gadu', 'GADUGADU'), ('groupwise', 'GROUPWISE'), ('icq', 'ICQ'), ('xmpp', 'JABBER'), ('msn', 'MSN'), ('yahoo', 'YAHOO'), ('skype', 'SKYPE'), ('irc', 'IRC'), ('sip', 'SIP')]: contents = re.sub('X-messaging/'+pat+'-All', 'X-'+repl, contents, flags=re.IGNORECASE) return contents @staticmethod def _parse_yaml(input: str) -> Dict: """Parse a YAML document into a dictinary and validate the data to some degree. :param str input: the YAML document to parse :returns: the parsed datastructure :rtype: dict """ yaml_parser = yaml.YAML(typ='base') # parse user input string try: contact_data = yaml_parser.load(input) except (yaml.parser.ParserError, yaml.scanner.ScannerError) as err: raise ValueError(err) else: if not contact_data: raise ValueError("Error: Found no contact information") # check for available data # at least enter name or organisation if not (contact_data.get("First name") or contact_data.get("Last name") or contact_data.get("Organisation")): raise ValueError( "Error: You must either enter a name or an organisation") return contact_data @staticmethod def _set_string_list(setter: Callable[[Union[str, List]], None], key: str, data: Dict) -> None: """Prepocess a string or list and set each value with the given setter :param setter: the setter method to add a value to a card :param key: :param data: """ value = data.get(key) if value: if isinstance(value, str): setter(value) elif isinstance(value, list): for val in value: if val: setter(val) else: raise ValueError( "{} must be a string or a list of strings".format(key)) def _set_date(self, target: str, key: str, data: Dict) -> None: new = data.get(key) if not new: return if not isinstance(new, str): raise ValueError("Error: {} must be a string object.".format(key)) if re.match(r"^text[\s]*=.*$", new): if self.version == "4.0": v1 = ', '.join(x.strip() for x in re.split(r"text[\s]*=", new) if x.strip()) if v1: setattr(self, target, v1) return raise ValueError("Error: Free text format for {} only usable with " "vcard version 4.0.".format(key.lower())) if re.match(r"^--\d\d-?\d\d$", new) and self.version != "4.0": raise ValueError( "Error: {} format --mm-dd and --mmdd only usable with " "vcard version 4.0. You may use 1900 as placeholder, if " "the year is unknown.".format(key)) try: v2 = helpers.string_to_date(new) if v2: setattr(self, target, v2) return except ValueError: pass raise ValueError("Error: Wrong {} format or invalid date\n" "Use format yyyy-mm-dd or " "yyyy-mm-ddTHH:MM:SS".format(key.lower())) def update(self, input: str) -> None: """Update this vcard with some yaml input :param input: a yaml string to parse and then use to update self """ contact_data = self._parse_yaml(input) # update rev self._update_revision() # name self._delete_vcard_object("N") # although the "n" attribute is not explisitely required by the vcard # specification, # the vobject library throws an exception, if it doesn't exist # so add the name regardless if it's empty or not self._add_name( contact_data.get("Prefix", ""), contact_data.get("First name", ""), contact_data.get("Additional", ""), contact_data.get("Last name", ""), contact_data.get("Suffix", "")) if "Formatted name" in contact_data: self.formatted_name = contact_data.get("Formatted name", "") if not self.formatted_name: # Trigger the auto filling code in the setter. self.formatted_name = "" # nickname self._delete_vcard_object("NICKNAME") self._set_string_list(self._add_nickname, "Nickname", contact_data) # organisation self._delete_vcard_object("ORG") self._delete_vcard_object("X-ABSHOWAS") self._set_string_list(self._add_organisation, "Organisation", contact_data) # role self._delete_vcard_object("ROLE") self._set_string_list(self._add_role, "Role", contact_data) # title self._delete_vcard_object("TITLE") self._set_string_list(self._add_title, "Title", contact_data) # phone self._delete_vcard_object("TEL") phone_data = contact_data.get("Phone") if phone_data: if isinstance(phone_data, dict): for type, number_list in phone_data.items(): if isinstance(number_list, str): number_list = [number_list] if isinstance(number_list, list): for number in number_list: if number: self._add_phone_number(type, number) else: raise ValueError( "Error: got no number or list of numbers for the " "phone number type " + type) else: raise ValueError( "Error: missing type value for phone number field") # email self._delete_vcard_object("EMAIL") email_data = contact_data.get("Email") if email_data: if isinstance(email_data, dict): for type, email_list in email_data.items(): if isinstance(email_list, str): email_list = [email_list] if isinstance(email_list, list): for email in email_list: if email: self.add_email(type, email) else: raise ValueError( "Error: got no email or list of emails for the " "email address type " + type) else: raise ValueError( "Error: missing type value for email address field") # post addresses self._delete_vcard_object("ADR") address_data = contact_data.get("Address") if address_data: if isinstance(address_data, dict): for type, post_adr_list in address_data.items(): if isinstance(post_adr_list, dict): post_adr_list = [post_adr_list] if isinstance(post_adr_list, list): for post_adr in post_adr_list: if isinstance(post_adr, dict): address_not_empty = False for key, value in post_adr.items(): if key in ["Box", "Extended", "Street", "Code", "City", "Region", "Country"] and value: address_not_empty = True break if address_not_empty: self._add_post_address( type, post_adr.get("Box", ""), post_adr.get("Extended", ""), post_adr.get("Street", ""), post_adr.get("Code", ""), post_adr.get("City", ""), post_adr.get("Region", ""), post_adr.get("Country", "")) else: raise ValueError( "Error: one of the " + type + " type " "address list items does not contain an " "address") else: raise ValueError( "Error: got no address or list of addresses for " "the post address type " + type) else: raise ValueError( "Error: missing type value for post address field") # categories self._delete_vcard_object("CATEGORIES") cat_data = contact_data.get("Categories") if cat_data: if isinstance(cat_data, str): self._add_category([cat_data]) elif isinstance(cat_data, list): only_contains_strings = True for sub_category in cat_data: if not isinstance(sub_category, str): only_contains_strings = False break # if the category list only contains strings, pack all of them # in a single CATEGORIES vcard tag if only_contains_strings: self._add_category(cat_data) else: for sub_category in cat_data: if sub_category: if isinstance(sub_category, str): self._add_category([sub_category]) else: self._add_category(sub_category) else: raise ValueError( "Error: category must be a string or a list of strings") # urls self._delete_vcard_object("URL") self._set_string_list(self._add_webpage, "Webpage", contact_data) # anniversary self._delete_vcard_object("ANNIVERSARY") self._delete_vcard_object("X-ANNIVERSARY") self._set_date('anniversary', 'Anniversary', contact_data) # birthday self._delete_vcard_object("BDAY") self._set_date('birthday', 'Birthday', contact_data) # private objects for supported in self.supported_private_objects: self._delete_vcard_object("X-{}".format(supported.upper())) private_data = contact_data.get("Private") if private_data: if isinstance(private_data, dict): for key, value_list in private_data.items(): if key in self.supported_private_objects: if isinstance(value_list, str): value_list = [value_list] if isinstance(value_list, list): for value in value_list: if value: self._add_private_object(key, value) else: raise ValueError( "Error: got no value or list of values for " "the private object " + key) else: raise ValueError( "Error: private object key " + key + " was " "changed.\nSupported private keys: " + ', '.join( self.supported_private_objects)) else: raise ValueError("Error: private objects must consist of a " "key : value pair.") # notes self._delete_vcard_object("NOTE") self._set_string_list(self._add_note, "Note", contact_data) def to_yaml(self) -> str: """Convert this contact to a YAML string The conversion follows the implicit schema that is given by the internal YAML template of khard. :returns: a YAML representation of this contact """ strings = [] for line in helpers.get_new_contact_template().splitlines(): if line.startswith("#"): strings.append(line) elif line == "": strings.append(line) elif line.lower().startswith("formatted name"): strings += helpers.convert_to_yaml( "Formatted name", self.formatted_name, 0, 15, True) elif line.lower().startswith("prefix"): strings += helpers.convert_to_yaml( "Prefix", self._get_name_prefixes(), 0, 11, True) elif line.lower().startswith("first name"): strings += helpers.convert_to_yaml( "First name", self._get_first_names(), 0, 11, True) elif line.lower().startswith("additional"): strings += helpers.convert_to_yaml( "Additional", self._get_additional_names(), 0, 11, True) elif line.lower().startswith("last name"): strings += helpers.convert_to_yaml( "Last name", self._get_last_names(), 0, 11, True) elif line.lower().startswith("suffix"): strings += helpers.convert_to_yaml( "Suffix", self._get_name_suffixes(), 0, 11, True) elif line.lower().startswith("nickname"): strings += helpers.convert_to_yaml( "Nickname", self.nicknames, 0, 9, True) elif line.lower().startswith("organisation"): strings += helpers.convert_to_yaml( "Organisation", self.organisations, 0, 13, True) elif line.lower().startswith("title"): strings += helpers.convert_to_yaml( "Title", self.titles, 0, 6, True) elif line.lower().startswith("role"): strings += helpers.convert_to_yaml( "Role", self.roles, 0, 6, True) elif line.lower().startswith("phone"): strings.append("Phone :") if not self.phone_numbers: strings.append(" cell : ") strings.append(" home : ") else: longest_key = max(self.phone_numbers.keys(), key=len) for type, number_list in sorted( self.phone_numbers.items(), key=lambda k: k[0].lower()): strings += helpers.convert_to_yaml( type, number_list, 4, len(longest_key) + 1, True) elif line.lower().startswith("email"): strings.append("Email :") if not self.emails: strings.append(" home : ") strings.append(" work : ") else: longest_key = max(self.emails.keys(), key=len) for type, email_list in sorted(self.emails.items(), key=lambda k: k[0].lower()): strings += helpers.convert_to_yaml( type, email_list, 4, len(longest_key) + 1, True) elif line.lower().startswith("address"): strings.append("Address :") if not self.post_addresses: strings.append(" home :") strings.append(" Box : ") strings.append(" Extended : ") strings.append(" Street : ") strings.append(" Code : ") strings.append(" City : ") strings.append(" Region : ") strings.append(" Country : ") else: for type, post_adr_list in sorted( self.post_addresses.items(), key=lambda k: k[0].lower()): strings.append(" {}:".format(type)) for post_adr in post_adr_list: indentation = 8 if len(post_adr_list) > 1: indentation += 4 strings.append(" -") strings += helpers.convert_to_yaml( "Box", post_adr.get("box"), indentation, 9, True) strings += helpers.convert_to_yaml( "Extended", post_adr.get("extended"), indentation, 9, True) strings += helpers.convert_to_yaml( "Street", post_adr.get("street"), indentation, 9, True) strings += helpers.convert_to_yaml( "Code", post_adr.get("code"), indentation, 9, True) strings += helpers.convert_to_yaml( "City", post_adr.get("city"), indentation, 9, True) strings += helpers.convert_to_yaml( "Region", post_adr.get("region"), indentation, 9, True) strings += helpers.convert_to_yaml( "Country", post_adr.get("country"), indentation, 9, True) elif line.lower().startswith("private"): strings.append("Private :") if self.supported_private_objects: longest_key = max(self.supported_private_objects, key=len) for object in self.supported_private_objects: strings += helpers.convert_to_yaml( object, self._get_private_objects().get(object, ""), 4, len(longest_key) + 1, True) elif line.lower().startswith("anniversary"): anniversary = self.anniversary if anniversary: if isinstance(anniversary, str): strings.append("Anniversary : text= {}".format( anniversary)) elif (anniversary.year == 1900 and anniversary.month != 0 and anniversary.day != 0 and anniversary.hour == 0 and anniversary.minute == 0 and anniversary.second == 0 and self.version == "4.0"): strings.append( anniversary.strftime("Anniversary : --%m-%d")) else: tz = anniversary.tzname() if ((tz and tz[3:]) or anniversary.hour != 0 or anniversary.minute != 0 or anniversary.second != 0): strings.append("Anniversary : {}".format( anniversary.isoformat())) else: strings.append( anniversary.strftime("Anniversary : %F")) else: strings.append("Anniversary : ") elif line.lower().startswith("birthday"): birthday = self.birthday if birthday: if isinstance(birthday, str): strings.append("Birthday : text= {}".format(birthday)) elif birthday.year == 1900 and birthday.month != 0 and \ birthday.day != 0 and birthday.hour == 0 and \ birthday.minute == 0 and birthday.second == 0 and \ self.version == "4.0": strings.append(birthday.strftime("Birthday : --%m-%d")) else: tz = birthday.tzname() if (tz and tz[3:] or birthday.hour != 0 or birthday.minute != 0 or birthday.second != 0): strings.append( "Birthday : {}".format(birthday.isoformat())) else: strings.append(birthday.strftime("Birthday : %F")) else: strings.append("Birthday : ") elif line.lower().startswith("categories"): strings += helpers.convert_to_yaml( "Categories", self.categories, 0, 11, True) elif line.lower().startswith("note"): strings += helpers.convert_to_yaml( "Note", self.notes, 0, 5, True) elif line.lower().startswith("webpage"): strings += helpers.convert_to_yaml( "Webpage", self.webpages, 0, 8, True) # posix standard: eof char must be \n return '\n'.join(strings) + "\n" class CarddavObject(YAMLEditable): def __init__(self, vcard: vobject.vCard, address_book: "address_book.VdirAddressBook", filename: str, supported_private_objects: Optional[List[str]] = None, vcard_version: Optional[str] = None, localize_dates: bool = False) -> None: """Initialize the vcard object. :param vcard: the vCard to wrap :param address_book: a reference to the address book where this vcard is stored :param filename: the path to the file where this vcard is stored :param supported_private_objects: the list of private property names that will be loaded from the actual vcard and represented in this pobject :param vcard_version: the version of the RFC to use :param localize_dates: should the formatted output of anniversary and birthday be localized or should the isoformat be used instead """ self.address_book = address_book self.filename = filename super().__init__(vcard, supported_private_objects, vcard_version, localize_dates) ####################################### # factory methods to create new contact ####################################### @classmethod def new(cls, address_book: "address_book.VdirAddressBook", supported_private_objects: Optional[List[str]] = None, version: Optional[str] = None, localize_dates: bool = False ) -> "CarddavObject": """Create a new CarddavObject from scratch""" vcard = vobject.vCard() uid = helpers.get_random_uid() filename = os.path.join(address_book.path, uid + ".vcf") card = cls(vcard, address_book, filename, supported_private_objects, version, localize_dates) card.uid = uid return card @classmethod def from_file(cls, address_book: "address_book.VdirAddressBook", filename: str, query: Query = AnyQuery(), supported_private_objects: Optional[List[str]] = None, localize_dates: bool = False) -> Optional["CarddavObject"]: """Load a CarddavObject object from a .vcf file if the plain file matches the query. :param address_book: the address book where this contact is stored :param filename: the file name of the .vcf file :param query: the query to search in the source file or None to load the file unconditionally :param supported_private_objects: the list of private property names that will be loaded from the actual vcard and represented in this pobject :param localize_dates: should the formatted output of anniversary and birthday be localized or should the isoformat be used instead :returns: the loaded CarddavObject or None if the file didn't match """ with open(filename, "r") as file: contents = file.read() if query.match(contents): try: vcard = vobject.readOne(contents) except Exception: logger.warning("Filtering some problematic tags from %s", filename) # if creation fails, try to repair some vcard attributes vcard = vobject.readOne(cls._filter_invalid_tags(contents)) return cls(vcard, address_book, filename, supported_private_objects, None, localize_dates) return None @classmethod def from_yaml(cls, address_book: "address_book.VdirAddressBook", yaml: str, supported_private_objects: Optional[List[str]] = None, version: Optional[str] = None, localize_dates: bool = False ) -> "CarddavObject": """Use this if you want to create a new contact from user input.""" contact = cls.new(address_book, supported_private_objects, version, localize_dates=localize_dates) contact.update(yaml) return contact @classmethod def clone_with_yaml_update(cls, contact: "CarddavObject", yaml: str, localize_dates: bool = False ) -> "CarddavObject": """ Use this if you want to clone an existing contact and replace its data with new user input in one step. """ contact = cls( copy.deepcopy(contact.vcard), address_book=contact.address_book, filename=contact.filename, supported_private_objects=contact.supported_private_objects, localize_dates=localize_dates) contact.update(yaml) return contact ###################################### # overwrite some default class methods ###################################### def __eq__(self, other: object) -> bool: return isinstance(other, CarddavObject) and \ self.pretty(False) == other.pretty(False) def __ne__(self, other: object) -> bool: return not self == other def pretty(self, verbose: bool = True) -> str: strings = [] # Every vcard must have an FN field per the RFC. strings.append("Name: {}".format(self.formatted_name)) # name if self._get_first_names() or self._get_last_names(): names = self._get_name_prefixes() + self._get_first_names() + \ self._get_additional_names() + self._get_last_names() + \ self._get_name_suffixes() strings.append("Full name: {}".format( helpers.list_to_string(names, " "))) # organisation if self.organisations: strings += helpers.convert_to_yaml( "Organisation", self.organisations, 0, -1, False) # address book name if verbose: strings.append("Address book: {}".format(self.address_book)) # person related information if (self.birthday is not None or self.anniversary is not None or self.nicknames or self.roles or self.titles): strings.append("General:") if self.anniversary: strings.append(" Anniversary: {}".format( self.get_formatted_anniversary())) if self.birthday: strings.append( " Birthday: {}".format(self.get_formatted_birthday())) if self.nicknames: strings += helpers.convert_to_yaml( "Nickname", self.nicknames, 4, -1, False) if self.roles: strings += helpers.convert_to_yaml( "Role", self.roles, 4, -1, False) if self.titles: strings += helpers.convert_to_yaml( "Title", self.titles, 4, -1, False) # phone numbers if self.phone_numbers: strings.append("Phone") for type, number_list in sorted(self.phone_numbers.items(), key=lambda k: k[0].lower()): strings += helpers.convert_to_yaml( type, number_list, 4, -1, False) # email addresses if self.emails: strings.append("E-Mail") for type, email_list in sorted(self.emails.items(), key=lambda k: k[0].lower()): strings += helpers.convert_to_yaml( type, email_list, 4, -1, False) # post addresses if self.post_addresses: strings.append("Address") for type, post_adr_list in sorted( self.get_formatted_post_addresses().items(), key=lambda k: k[0].lower()): strings += helpers.convert_to_yaml( type, post_adr_list, 4, -1, False) # private objects if self._get_private_objects().keys(): strings.append("Private:") for object in self.supported_private_objects: if object in self._get_private_objects(): strings += helpers.convert_to_yaml( object, self._get_private_objects().get(object), 4, -1, False) # misc stuff if self.categories or self.webpages or self.notes or (verbose and self.uid): strings.append("Miscellaneous") if verbose and self.uid: strings.append(" UID: {}".format(self.uid)) if self.categories: strings += helpers.convert_to_yaml( "Categories", self.categories, 4, -1, False) if self.webpages: strings += helpers.convert_to_yaml( "Webpage", self.webpages, 4, -1, False) if self.notes: strings += helpers.convert_to_yaml( "Note", self.notes, 4, -1, False) return '\n'.join(strings) + '\n' def write_to_file(self, overwrite: bool = False) -> None: # make sure, that every contact contains a uid if not self.uid: self.uid = helpers.get_random_uid() try: with atomic_write(self.filename, overwrite=overwrite) as f: f.write(self.vcard.serialize()) except vobject.base.ValidateError as err: print("Error: Vcard is not valid.\n{}".format(err)) sys.exit(4) except OSError as err: print("Error: Can't write\n{}".format(err)) sys.exit(4) def delete_vcard_file(self) -> None: try: os.remove(self.filename) except IOError as err: logger.error("Can not remove vCard file: %s", err) @classmethod def get_properties(cls) -> List[str]: """Return the property names that are defined on this class.""" return [name for name in dir(CarddavObject) if isinstance(getattr(CarddavObject, name), property)] khard-0.17.0/khard/cli.py000066400000000000000000000566601371517016500151250ustar00rootroot00000000000000"""Command line parsing and configuration logic for khard""" import argparse import logging import sys from typing import List, Tuple from .actions import Actions from .carddav_object import CarddavObject from .config import Config, ConfigError from .query import AndQuery, AnyQuery, FieldQuery, NameQuery, TermQuery, parse from .version import version as khard_version logger = logging.getLogger(__name__) def field_argument(orignal: str) -> List[str]: """Ensure the fields specified for `ls -F` are proper field names. Nested attribute names are not checked. :param orignal: the value from the command line :returns: the orignal value split at "," if the fields are spelled correctly :throws: argparse.ArgumentTypeError """ special_fields = ['index', 'name', 'phone', 'email'] choices = sorted(special_fields + CarddavObject.get_properties()) ret = [] for candidate in orignal.split(','): candidate = candidate.lower() field = candidate.split('.')[0] if field in choices: ret.append(candidate) else: raise argparse.ArgumentTypeError( '"{}" is not an accepted field. Accepted fields are {}.'.format( field, ', '.join('"{}"'.format(c) for c in choices))) return ret def comma_separated_argument(original: str) -> List[str]: """Return the original string split by commas :param original: the value from the command line :returns: the original value split at "," and lower cased """ return [f.lower() for f in original.split(",")] def create_parsers() -> Tuple[argparse.ArgumentParser, argparse.ArgumentParser]: """Create two argument parsers. The first parser is manly used to find the config file which can than be used to set some default values on the second parser. The second parser can parse the remainder of the command line with the subcommand and all further options and arguments. :returns: the two parsers for the first and the second parsing pass :rtype: (argparse.ArgumentParser, argparse.ArgumentParser) """ # Create the base argument parser. It will be reused for the first and # second round of argument parsing. base = argparse.ArgumentParser( description="Khard is a carddav address book for the console", formatter_class=argparse.RawTextHelpFormatter, add_help=False) base.add_argument("-c", "--config", help="config file to use") base.add_argument("--debug", action="store_true", help="enable debug output") base.add_argument("--skip-unparsable", action="store_true", help="skip unparsable vcard files") base.add_argument("-v", "--version", action="version", version="Khard version {}".format(khard_version)) # Create the first argument parser. Its main job is to set the correct # config file. The config file is needed to get the default command if no # subcommand is given on the command line. This parser will ignore most # arguments, as they will be parsed by the second parser. first_parser = argparse.ArgumentParser(parents=[base]) first_parser.add_argument('remainder', nargs=argparse.REMAINDER) # Create the main argument parser. It will handle the complete command # line only ignoring the config and debug options as these have already # been set. parser = argparse.ArgumentParser(parents=[base]) # create address book subparsers with different help texts default_addressbook_parser = argparse.ArgumentParser(add_help=False) default_addressbook_parser.add_argument( "-a", "--addressbook", default=[], type=lambda x: [y.strip() for y in x.split(",")], help="Specify one or several comma separated address book names to " "narrow the list of contacts") new_addressbook_parser = argparse.ArgumentParser(add_help=False) new_addressbook_parser.add_argument( "-a", "--addressbook", default=[], type=lambda x: [y.strip() for y in x.split(",")], help="Specify address book in which to create the new contact") copy_move_addressbook_parser = argparse.ArgumentParser(add_help=False) copy_move_addressbook_parser.add_argument( "-a", "--addressbook", default=[], type=lambda x: [y.strip() for y in x.split(",")], help="Specify one or several comma separated address book names to " "narrow the list of contacts") copy_move_addressbook_parser.add_argument( "-A", "--target-addressbook", default=[], type=lambda x: [y.strip() for y in x.split(",")], help="Specify target address book in which to copy / move the " "selected contact") merge_addressbook_parser = argparse.ArgumentParser(add_help=False) merge_addressbook_parser.add_argument( "-a", "--addressbook", default=[], type=lambda x: [y.strip() for y in x.split(",")], help="Specify one or several comma separated address book names to " "narrow the list of source contacts") merge_addressbook_parser.add_argument( "-A", "--target-addressbook", default=[], type=lambda x: [y.strip() for y in x.split(",")], help="Specify one or several comma separated address book names to " "narrow the list of target contacts") # create input file subparsers with different help texts email_header_input_file_parser = argparse.ArgumentParser(add_help=False) email_header_input_file_parser.add_argument( "-i", "--input-file", default="-", help="Specify input email header file name or use stdin by default") template_input_file_parser = argparse.ArgumentParser(add_help=False) template_input_file_parser.add_argument( "-i", "--input-file", default="-", help="Specify input template file name or use stdin by default") template_input_file_parser.add_argument( "--open-editor", "--edit", action="store_true", help="Open the " "default text editor after successful creation of new contact") # create sort subparser sort_parser = argparse.ArgumentParser(add_help=False) sort_parser.add_argument( "-d", "--display", choices=("first_name", "last_name", "formatted_name"), help="Display names in contact table by first or last name") sort_parser.add_argument( "-g", "--group-by-addressbook", action="store_true", help="Group contact table by address book") sort_parser.add_argument( "-r", "--reverse", action="store_true", help="Reverse order of contact table") sort_parser.add_argument( "-s", "--sort", choices=("first_name", "last_name", "formatted_name"), help="Sort contact table by first or last name") # create search subparsers default_search_parser = argparse.ArgumentParser(add_help=False) default_search_parser.add_argument( "-f", "--search-in-source-files", action="store_true", help="Look into source vcf files to speed up search queries in " "large address books. Beware that this option could lead " "to incomplete results.") default_search_parser.add_argument( "-e", "--strict-search", action="store_true", help="DEPRECATED use the new query syntax instead") default_search_parser.add_argument( "-u", "--uid", type=lambda x: FieldQuery("uid", x), help="DEPRECATED use the new query syntax instead") default_search_parser.add_argument( "search_terms", nargs="*", metavar="search terms", type=parse, default=[], help="search in specified or all fields to find matching " "contact") merge_search_parser = argparse.ArgumentParser(add_help=False) merge_search_parser.add_argument( "-f", "--search-in-source-files", action="store_true", help="Look into source vcf files to speed up search queries in " "large address books. Beware that this option could lead " "to incomplete results.") merge_search_parser.add_argument( "-e", "--strict-search", action="store_true", help="DEPRECATED use the new query syntax instead") merge_search_parser.add_argument( "-t", "--target-contact", "--target", type=parse, help="search for a matching target contact") merge_search_parser.add_argument( "-u", "--uid", type=lambda x: FieldQuery("uid", x), help="DEPRECATED use the new query syntax instead") merge_search_parser.add_argument( "-U", "--target-uid", type=lambda x: FieldQuery("uid", x), help="DEPRECATED use -t with the new query syntax instead") merge_search_parser.add_argument( "source_search_terms", nargs="*", metavar="source", type=parse, default=[], help="search in specified or all fields to find matching source " "contact") # create subparsers for actions subparsers = parser.add_subparsers(dest="action") list_parser = subparsers.add_parser( "list", aliases=Actions.get_aliases("list"), parents=[default_addressbook_parser, default_search_parser, sort_parser], description="list all (selected) contacts", help="list all (selected) contacts") list_parser.add_argument( "-p", "--parsable", action="store_true", help="Machine readable format: uid\\tcontact_name\\taddress_book_name") list_parser.add_argument( "-F", "--fields", default=[], type=field_argument, help="Comma separated list of fields to show") show_parser = subparsers.add_parser( "show", aliases=Actions.get_aliases("show"), parents=[default_addressbook_parser, default_search_parser, sort_parser], description="display detailed information about one contact", help="display detailed information about one contact") show_parser.add_argument( "--format", choices=("pretty", "yaml", "vcard"), default="pretty", help="select the output format") show_parser.add_argument( "-o", "--output-file", default=sys.stdout, type=argparse.FileType("w"), help="Specify output template file name or use stdout by default") subparsers.add_parser("template", help="print an empty yaml template") birthdays_parser = subparsers.add_parser( "birthdays", aliases=Actions.get_aliases("birthdays"), parents=[default_addressbook_parser, default_search_parser], description="list birthdays (sorted by month and day)", help="list birthdays (sorted by month and day)") birthdays_parser.add_argument( "-d", "--display", choices=("first_name", "last_name", "formatted_name"), help="Display names in birthdays table by first or last name") birthdays_parser.add_argument( "-p", "--parsable", action="store_true", help="Machine readable format: name\\tdate") email_parser = subparsers.add_parser( "email", aliases=Actions.get_aliases("email"), parents=[default_addressbook_parser, default_search_parser, sort_parser], description="list email addresses", help="list email addresses") email_parser.add_argument( "-p", "--parsable", action="store_true", help="Machine readable format: address\\tname\\ttype") email_parser.add_argument( "--remove-first-line", action="store_true", help="remove \"searching for '' ...\" line from parsable output " "(that line is required by mutt)") phone_parser = subparsers.add_parser( "phone", aliases=Actions.get_aliases("phone"), parents=[default_addressbook_parser, default_search_parser, sort_parser], description="list phone numbers", help="list phone numbers") phone_parser.add_argument( "-p", "--parsable", action="store_true", help="Machine readable format: number\\tname\\ttype") post_address_parser = subparsers.add_parser( "postaddress", aliases=Actions.get_aliases("postaddress"), parents=[default_addressbook_parser, default_search_parser, sort_parser], description="list postal addresses", help="list postal addresses") post_address_parser.add_argument( "-p", "--parsable", action="store_true", help="Machine readable format: address\\tname\\ttype") new_parser = subparsers.add_parser( "new", aliases=Actions.get_aliases("new"), parents=[new_addressbook_parser, template_input_file_parser], description="create a new contact", help="create a new contact") new_parser.add_argument( "--vcard-version", choices=("3.0", "4.0"), dest='preferred_version', help="Select preferred vcard version for new contact") add_email_parser = subparsers.add_parser( "add-email", aliases=Actions.get_aliases("add-email"), parents=[default_addressbook_parser, email_header_input_file_parser, default_search_parser, sort_parser], description="Extract email address from the \"From:\" field of an " "email header and add to an existing contact or create a new one", help="Extract email address from the \"From:\" field of an email " "header and add to an existing contact or create a new one") add_email_parser.add_argument( "--vcard-version", choices=("3.0", "4.0"), dest='preferred_version', help="Select preferred vcard version for new contact") add_email_parser.add_argument( "-H", "--headers", dest='fields', default=["from"], type=comma_separated_argument, help="Extract contacts from the given comma separated header fields. \ `all` searches all headers.") subparsers.add_parser( "merge", aliases=Actions.get_aliases("merge"), parents=[merge_addressbook_parser, merge_search_parser, sort_parser], description="merge two contacts", help="merge two contacts") edit_parser = subparsers.add_parser( "edit", aliases=Actions.get_aliases("edit"), parents=[default_addressbook_parser, template_input_file_parser, default_search_parser, sort_parser], description="edit the data of a contact", help="edit the data of a contact") edit_parser.add_argument( "--format", choices=("yaml", "vcard"), default="yaml", help="specify the file format to use when editing") subparsers.add_parser( "copy", aliases=Actions.get_aliases("copy"), parents=[copy_move_addressbook_parser, default_search_parser, sort_parser], description="copy a contact to a different addressbook", help="copy a contact to a different addressbook") subparsers.add_parser( "move", aliases=Actions.get_aliases("move"), parents=[copy_move_addressbook_parser, default_search_parser, sort_parser], description="move a contact to a different addressbook", help="move a contact to a different addressbook") remove_parser = subparsers.add_parser( "remove", aliases=Actions.get_aliases("remove"), parents=[default_addressbook_parser, default_search_parser, sort_parser], description="remove a contact", help="remove a contact") remove_parser.add_argument( "--force", action="store_true", help="Remove contact without confirmation") subparsers.add_parser( "addressbooks", aliases=Actions.get_aliases("addressbooks"), description="list addressbooks", help="list addressbooks") subparsers.add_parser( "filename", aliases=Actions.get_aliases("filename"), parents=[default_addressbook_parser, default_search_parser, sort_parser], description="list filenames of all matching contacts", help="list filenames of all matching contacts") # Deprecated subcommands: They can be removed after the next release # (v0.17) export_parser = subparsers.add_parser( "export", aliases=Actions.get_aliases("export"), parents=[ default_addressbook_parser, default_search_parser, sort_parser], description="DEPRECATED use 'show --format=yaml'", help="DEPRECATED use 'show --format=yaml'") export_parser.add_argument("-o", "--output-file", default=sys.stdout, type=argparse.FileType("w")) subparsers.add_parser( "source", aliases=Actions.get_aliases("source"), parents=[ default_addressbook_parser, default_search_parser, sort_parser], description="DEPRECATED use 'edit --format=vcard'", help="DEPRECATED use 'edit --format=vcard'") # Replace the print_help method of the first parser with the print_help # method of the main parser. This makes it possible to have the first # parser handle the help option so that command line help can be printed # without parsing the config file first (which is a problem if there are # errors in the config file). The config file will still be parsed before # the full command line is parsed so errors in the config file might be # reported before command line syntax errors. first_parser.print_help = parser.print_help # type: ignore return first_parser, parser def parse_args(argv: List[str]) -> Tuple[argparse.Namespace, Config]: """Parse the command line arguments and return the namespace that was creates by argparse.ArgumentParser.parse_args(). :param argv: the command line arguments :returns: the namespace parsed from the command line """ first_parser, parser = create_parsers() # Parese the command line with the first argument parser. It will handle # the config option (its main job) and also the help, version and debug # options as these do not depend on anything else. args = first_parser.parse_args(argv) remainder = args.remainder # Set the loglevel to debug if given on the command line. This is done # before parsing the config file to make it possible to debug the parsing # of the config file. if "debug" in args and args.debug: logging.basicConfig(level=logging.DEBUG) # Create the config instance. try: config = Config(args.config) except ConfigError as err: parser.exit(3, "Error in config file: {}\n".format(err)) logger.debug("Finished parsing config=%s", vars(config)) # Check the log level again and merge the value from the command line with # the config file. if ("debug" in args and args.debug) or config.debug: logging.basicConfig(level=logging.DEBUG) logger.debug("first args=%s", args) # Set the default command from the config file if none was given on the # command line. if not remainder or remainder[0] not in Actions.get_all(): if config.default_action is None: parser.error("Missing subcommand on command line or default action" " parameter in config.") remainder.insert(0, config.default_action) logger.debug("updated remainder=%s", remainder) # Save the last option that needs to be carried from the first parser run # to the second. skip = args.skip_unparsable # Parse the remainder of the command line. All options from the previous # run have already been processed and are not needed any more. args = parser.parse_args(remainder) # Restore settings that are left from the first parser run. args.skip_unparsable = skip logger.debug("second args=%s", args) # An integrity check for some options. if "uid" in args and args.uid and ( ("search_terms" in args and args.search_terms) or ("source_search_terms" in args and args.source_search_terms)): # If an uid was given we require that no search terms where given. parser.error("You can not give arbitrary search terms and --uid at the" " same time.") if "target_uid" in args and args.target_uid and args.target_contact: parser.error("You can not give arbitrary target search terms and " "--target-uid at the same time.") # Deprecation workaround if "strict_search" in args and args.strict_search: logger.error("Deprecated option --strict-search, use the new query " "syntax instead.") if "search_terms" in args: args.search_terms = [NameQuery(t.get_term()) for t in args.search_terms] if "source_search_terms" in args: args.source_search_terms = [NameQuery(t.get_term()) for t in args.source_search_terms] if "taget_search_terms" in args: args.taget_search_terms = [NameQuery(t.get_term()) for t in args.taget_search_terms] # Build conjunctive queries. If uid was given the list of search terms # will be empty. If no uid was given it will be None. if "source_search_terms" in args: args.source_search_terms = AndQuery.reduce(args.source_search_terms, args.uid) if "search_terms" in args: args.search_terms = AndQuery.reduce(args.search_terms, args.uid) if "target_contact" in args: # Only one of target_contact or target_uid can be set. args.target_contact = args.target_contact or args.target_uid \ or AnyQuery() # Remove uid values from the args Namespace. They have been merged into # the search terms above. if "uid" in args: del args.uid if "target_uid" in args: del args.target_uid # Normalize all deprecated subcommands and emit warnings. if args.action == "export": logger.error("Deprecated subcommand: use 'show --format=yaml'.") args.action = "show" args.format = "yaml" elif args.action == "source": logger.error("Deprecated subcommand: use 'edit --format=vcard'.") args.action = "edit" args.format = "vcard" return args, config def merge_args_into_config(args: argparse.Namespace, config: Config) -> Config: """Merge the parsed arguments from argparse into the config object. :param args: the parsed command line arguments :param config: the parsed config file :returns: the merged config object """ config.merge_args(args) # Now we can savely initialize the address books as all command line # options have been incorporated into the config object. config.init_address_books() # If the user could but did not specify address books on the command line # it means they want to use all address books in that place. if "addressbook" in args and not args.addressbook: args.addressbook = [abook.name for abook in config.abooks] if "target_addressbook" in args and not args.target_addressbook: args.target_addressbook = [abook.name for abook in config.abooks] return config def init(argv: List[str]) -> Tuple[argparse.Namespace, Config]: """Initialize khard by parsing the command line and reading the config file :param argv: the command line arguments :returns: the parsed command line and the fully initialized config """ args, conf = parse_args(argv) # if args.action isn't one of the defined actions, it must be an alias if args.action not in Actions.get_actions(): # convert alias to corresponding action # example: "ls" --> "list" args.action = Actions.get_action(args.action) return args, merge_args_into_config(args, conf) khard-0.17.0/khard/config.py000066400000000000000000000215641371517016500156160ustar00rootroot00000000000000"""Loading and validation of the configuration file""" from argparse import Namespace import locale import logging import os import re import shlex from typing import Iterable, Dict, List, Optional, Union import configobj import validate from .actions import Actions from .address_book import AddressBookCollection, AddressBookNameError, \ VdirAddressBook from .query import Query logger = logging.getLogger(__name__) class ConfigError(Exception): """Errors during config file parsing""" def validate_command(value: List[str]) -> List[str]: """Special validator to check shell commands The input must either be a list of strings or a string that shlex.split can parse into such. :param value: the config value to validate :returns: the command after validation :raises: validate.ValidateError """ logger.debug("validating %s", value) try: return validate.is_string_list(value) except validate.VdtTypeError: logger.debug('continue with %s', value) if isinstance(value, str): try: return shlex.split(value) except ValueError as err: raise validate.ValidateError( 'Error when parsing shell command "{}": {}'.format( value, err)) raise def validate_action(value: str) -> str: """Check that the given value is a valid action. :param value: the config value to check :returns: the same value :raises: validate.ValidateError """ return validate.is_option(value, *Actions.get_actions()) def validate_private_objects(value: List[str]) -> List[str]: """Check that the private objects are reasonable :param value: the config value to check :returns: the list of private objects :raises: validate.ValidateError """ value = validate.is_string_list(value) for obj in value: if re.search("[^a-z0-9-]", obj, re.IGNORECASE): raise validate.ValidateError( 'Private objects may only contain letters, digits and the' ' \"-\" character.') if obj.startswith("-") or obj.endswith("-"): raise validate.ValidateError( "A \"-\" in a private object label must be at least " "surrounded by one letter or digit.") return value class Config: """Parse and validate the config file with configobj.""" supported_vcard_versions = ("3.0", "4.0") def __init__(self, config_file: Optional[str] = None) -> None: self.config: configobj.ConfigObj self.abooks: AddressBookCollection locale.setlocale(locale.LC_ALL, '') config = self._load_config_file(config_file) self.config = self._validate(config) self._set_attributes() @classmethod def _load_config_file(cls, config_file: Optional[str] ) -> configobj.ConfigObj: """Find and load the config file. :param config_file: the path to the config file to load :returns: the loaded config file """ if not config_file: xdg_config_home = os.getenv("XDG_CONFIG_HOME", os.path.expanduser("~/.config")) config_file = os.getenv("KHARD_CONFIG", os.path.join( xdg_config_home, "khard", "khard.conf")) configspec = os.path.join(os.path.dirname(__file__), 'data', 'config.spec') try: return configobj.ConfigObj( infile=config_file, configspec=configspec, interpolation=False, file_error=True) except configobj.ConfigObjError as err: raise ConfigError(str(err)) @staticmethod def _validate(config: configobj.ConfigObj) -> configobj.ConfigObj: vdr = validate.Validator() vdr.functions.update({'command': validate_command, 'action': validate_action, 'private_objects': validate_private_objects}) result = config.validate(vdr, preserve_errors=True) result = configobj.flatten_errors(config, result) if not config['addressbooks'].keys(): result.append((['addressbooks'], '__any__', 'No address book entries available')) for path, key, exception in result: logger.error("Error in config file, %s: %s", ".".join([*path, key]), exception) if result: raise ConfigError return config def _set_attributes(self) -> None: """Set the attributes from the internal config instance on self.""" general = self.config["general"] self.debug = general["debug"] self.editor = general["editor"] or os.environ.get("EDITOR", "vim") self.merge_editor = general["merge_editor"] \ or os.environ.get("MERGE_EDITOR", "vimdiff") self.default_action = general["default_action"] table = self.config["contact table"] vcard = self.config["vcard"] self.sort = table["sort"] # if display by name attribute is not present in the config file use # the sort attribute value for backwards compatibility self.display = table.get("display", self.sort) self.localize_dates = table['localize_dates'] self.private_objects = vcard['private_objects'] self.preferred_vcard_version = vcard['preferred_version'] self.search_in_source_files = vcard['search_in_source_files'] self.skip_unparsable = vcard['skip_unparsable'] self.group_by_addressbook = table['group_by_addressbook'] self.reverse = table['reverse'] self.show_nicknames = table['show_nicknames'] self.preferred_email_address_type = table['preferred_email_address_type'] self.preferred_phone_number_type = table['preferred_phone_number_type'] self.show_uids = table['show_uids'] def init_address_books(self) -> None: """Initialize the internal address book collection. This method should only be called *after* merging in the command line options as they can hold some options that are relevant for the loading of the address books. """ section = self.config['addressbooks'] kwargs = {'private_objects': self.private_objects, 'localize_dates': self.localize_dates, 'skip': self.skip_unparsable} try: self.abooks = AddressBookCollection( "tmp", [VdirAddressBook(name, section[name]['path'], **kwargs) for name in section]) except IOError as err: raise ConfigError(str(err)) def get_address_books(self, names: Iterable[str], queries: Dict[str, Query] ) -> AddressBookCollection: """Load all address books with the given names. :param names: the address books to load :param queries: a mapping of address book names to search queries :returns: the loaded address books """ all_names = {str(book) for book in self.abooks} if not names: names = all_names elif not all_names.issuperset(names): raise AddressBookNameError( "The following address books are not defined: {}".format( ', '.join(set(names) - all_names))) # load address books which are defined in the configuration file collection = AddressBookCollection("tmp", [self.abooks[name] for name in names]) # We can not use AddressBookCollection.load here because we want to # select the collection based on the address book. for abook in collection: abook.load(queries[abook.name], self.search_in_source_files) return collection def merge(self, other: Union[configobj.ConfigObj, Dict]) -> None: """Merge the config with some other dict or ConfigObj :param other: the other dict or ConfigObj to merge into self :returns: None """ self.config.merge(other) self._validate(self.config) self._set_attributes() def merge_args(self, args: Namespace) -> None: """Merge options from a flat argparse object. :param argparse.Namespace args: the parsed arguments to incorperate """ skel = {'general': ['debug'], 'contact table': ['reverse', 'group_by_addressbook', 'display', 'sort'], 'vcard': ['search_in_source_files', 'skip_unparsable', 'preferred_version'], } merge = {sec: {key: getattr(args, key) for key in opts if key in args and getattr(args, key) is not None} for sec, opts in skel.items()} logger.debug('Merging in %s', merge) self.merge(merge) logger.debug('Merged: %s', vars(self)) khard-0.17.0/khard/data/000077500000000000000000000000001371517016500147005ustar00rootroot00000000000000khard-0.17.0/khard/data/config.spec000066400000000000000000000016101371517016500170170ustar00rootroot00000000000000[general] debug = boolean(default=False) default_action = action(default=None) editor = command(default=None) merge_editor = command(default=None) [contact table] display = option('first_name', 'last_name', 'formatted_name', default='first_name') group_by_addressbook = boolean(default=False) localize_dates = boolean(default=True) preferred_email_address_type = string_list(default=list('pref')) preferred_phone_number_type = string_list(default=list('pref')) reverse = boolean(default=False) show_nicknames = boolean(default=False) show_uids = boolean(default=True) sort = option('first_name', 'last_name', 'formatted_name', default='first_name') [vcard] preferred_version = option('3.0', '4.0', default='3.0') private_objects = private_objects(default=list())) search_in_source_files = boolean(default=False) skip_unparsable = boolean(default=False) [addressbooks] [[__many__]] path = string khard-0.17.0/khard/data/template.yaml000066400000000000000000000057301371517016500174040ustar00rootroot00000000000000# Every contact must contain a formatted name, it will be autofilled # from the full name below if not given. Formatted name : # name components # every entry may contain a string or a list of strings # format: # First name : name1 # Additional : # - name2 # - name3 # Last name : name4 Prefix : First name : Additional : Last name : Suffix : # nickname # may contain a string or a list of strings Nickname : # important dates # Formats: # vcard 3.0 and 4.0: yyyy-mm-dd or yyyy-mm-ddTHH:MM:SS # vcard 4.0 only: --mm-dd or text= string value # anniversary Anniversary : # birthday Birthday : # organisation # format: # Organisation : company # or # Organisation : # - company1 # - company2 # or # Organisation : # - # - company # - unit Organisation : # organisation title and role # every entry may contain a string or a list of strings # # title at organisation # example usage: research scientist Title : # role at organisation # example usage: project leader Role : # phone numbers # format: # Phone: # type1, type2: number # type3: # - number1 # - number2 # custom: number # allowed types: # vcard 3.0: At least one of bbs, car, cell, fax, home, isdn, msg, modem, # pager, pcs, pref, video, voice, work # vcard 4.0: At least one of home, work, pref, text, voice, fax, cell, video, # pager, textphone # Alternatively you may use a single custom label (only letters). # But beware, that not all address book clients will support custom labels. Phone : cell : home : # email addresses # format like phone numbers above # allowed types: # vcard 3.0: At least one of home, internet, pref, work, x400 # vcard 4.0: At least one of home, internet, pref, work # Alternatively you may use a single custom label (only letters). Email : home : work : # post addresses # allowed types: # vcard 3.0: At least one of dom, intl, home, parcel, postal, pref, work # vcard 4.0: At least one of home, pref, work # Alternatively you may use a single custom label (only letters). Address : home : Box : Extended : Street : Code : City : Region : Country : # categories or tags # format: # Categories : single category # or # Categories : # - category1 # - category2 Categories : # web pages # may contain a string or a list of strings Webpage : # private objects # define your own private objects in the vcard section of your khard config file # example: # [vcard] # private_objects = Jabber, Skype, Twitter # these objects are stored with a leading "X-" before the object name in the # vcard files. # every entry may contain a string or a list of strings Private : {} # notes # may contain a string or a list of strings # for multi-line notes use: # Note : | # line one # line two Note : khard-0.17.0/khard/formatter.py000066400000000000000000000100451371517016500163440ustar00rootroot00000000000000"""Formatting and sorting of contacts""" from typing import cast, Dict, List from .carddav_object import CarddavObject class Formatter: """A formtter for CarddavObject. It recieves some settings on initialisation which influence the formatting of the contact. """ FIRST = "first_name" LAST = "last_name" FORMAT = "formatted_name" def __init__(self, display: str, preferred_email: List[str], preferred_phone: List[str], show_nicknames: bool, parsable: bool) -> None: self._display = display self._preferred_email = preferred_email self._preferred_phone = preferred_phone self._show_nicknames = show_nicknames self._parsable = parsable @staticmethod def format_labeled_field(field: Dict[str, List[str]], preferred: List[str] ) -> str: """Format a labeled field from a vcard for display, the first entry under the preferred label will be returned :param field: the labeled field, this must not be empty! :param preferred: the order of preferred labels :returns: the formatted field entry """ # filter out preferred type if set in config file found = [] for pref in preferred: for key in field: if pref.lower() in key.lower(): found.append(key) if found: break keys = found or [k for k in field if "pref" in k.lower()] \ or field.keys() # get first key in alphabetical order first_key = sorted(keys, key=lambda k: k.lower())[0] return "{}: {}".format(first_key, sorted(field.get(first_key, []))[0]) def get_special_field(self, vcard: CarddavObject, field: str) -> str: """Returns certain fields with specific formatting options (for support of some list command options).""" if field == 'name': if self._display == self.FIRST: name = vcard.get_first_name_last_name() elif self._display == self.FORMAT: name = vcard.formatted_name else: name = vcard.get_last_name_first_name() if vcard.nicknames and self._show_nicknames and not self._parsable: return "{} (Nickname: {})".format(name, vcard.nicknames[0]) return name if field == 'phone': if vcard.phone_numbers: return self.format_labeled_field(vcard.phone_numbers, self._preferred_phone) if field == 'email': if vcard.emails: return self.format_labeled_field(vcard.emails, self._preferred_email) return "" @staticmethod def get_nested_field(vcard: CarddavObject, field: str) -> str: """Returns the value of a nested field from a string get_nested_field(vcard,'emails.home.1') is equivalent to vcard.emails['home'][1]. :param vcard: the contact from which to get the field :param field: a field specification :returns: the nested field, or the empty string if it didn't exist """ attr_name = field.split('.')[0] val = '' if hasattr(vcard, attr_name): val = getattr(vcard, attr_name) # Loop through separate parts, changing val to be the head element. for partial in field.split('.')[1:]: if isinstance(val, dict) and partial in val: val = val[partial] elif partial.isdigit() and isinstance(val, list) \ and len(val) > int(partial): val = val[int(partial)] # TODO: Completely support case insensitive indexing elif isinstance(val, dict) and partial.upper() in val: val = val[partial.upper()] else: val = '' # Convert None and other falsy values to the empty string return val or '' khard-0.17.0/khard/helpers.py000066400000000000000000000201341371517016500160030ustar00rootroot00000000000000"""Some helper functions for khard""" import os import pathlib import random import string from datetime import datetime from typing import List, Optional, Union def pretty_print(table: List[List[str]], justify: str = "L") -> str: """Converts a list of lists into a string formatted like a table with spaces separating fields and newlines separating rows""" # support for multiline columns line_break_table = [] for row in table: # get line break count most_line_breaks_in_row = 0 for col in row: if str(col).count("\n") > most_line_breaks_in_row: most_line_breaks_in_row = col.count("\n") # fill table rows for index in range(0, most_line_breaks_in_row+1): line_break_row = [] for col in row: try: line_break_row.append(str(col).split("\n")[index]) except IndexError: line_break_row.append("") line_break_table.append(line_break_row) # replace table variable table = line_break_table # get width for every column column_widths = [0] * len(table[0]) offset = 3 for row in table: for index, col in enumerate(row): width = len(str(col)) if width > column_widths[index]: column_widths[index] = width table_row_list = [] for row in table: single_row_list = [] for col_index, col in enumerate(row): if justify == "R": # justify right formated_column = str(col).rjust(column_widths[col_index] + offset) elif justify == "L": # justify left formated_column = str(col).ljust(column_widths[col_index] + offset) elif justify == "C": # justify center formated_column = str(col).center(column_widths[col_index] + offset) single_row_list.append(formated_column) table_row_list.append(' '.join(single_row_list)) return '\n'.join(table_row_list) def list_to_string(input: Union[str, List], delimiter: str) -> str: """converts list to string recursively so that nested lists are supported :param input: a list of strings and lists of strings (and so on recursive) :param delimiter: the deimiter to use when joining the items :returns: the recursively joined list """ if isinstance(input, list): return delimiter.join( list_to_string(item, delimiter) for item in input) return input def string_to_list(input: Union[str, List[str]], delimiter: str) -> List[str]: if isinstance(input, list): return input return [x.strip() for x in input.split(delimiter)] def string_to_date(string: str) -> datetime: """Convert a date string into a date object. :param string: the date string to parse :returns: the parsed datetime object """ # try date formats --mmdd, --mm-dd, yyyymmdd, yyyy-mm-dd and datetime # formats yyyymmddThhmmss, yyyy-mm-ddThh:mm:ss, yyyymmddThhmmssZ, # yyyy-mm-ddThh:mm:ssZ. for fmt in ("--%m%d", "--%m-%d", "%Y%m%d", "%Y-%m-%d", "%Y%m%dT%H%M%S", "%Y-%m-%dT%H:%M:%S", "%Y%m%dT%H%M%SZ", "%Y-%m-%dT%H:%M:%SZ"): try: return datetime.strptime(string, fmt) except ValueError: continue # with the next format # try datetime formats yyyymmddThhmmsstz and yyyy-mm-ddThh:mm:sstz where tz # may look like -06:00. for fmt in ("%Y%m%dT%H%M%S%z", "%Y-%m-%dT%H:%M:%S%z"): try: return datetime.strptime(''.join(string.rsplit(":", 1)), fmt) except ValueError: continue # with the next format raise ValueError def get_random_uid() -> str: return ''.join([random.choice(string.ascii_lowercase + string.digits) for _ in range(36)]) def file_modification_date(filename: str) -> datetime: return datetime.fromtimestamp(os.path.getmtime(filename)) def convert_to_yaml(name: str, value: Union[None, str, List], indentation: int, index_of_colon: int, show_multi_line_character: bool ) -> List[str]: """converts a value list into yaml syntax :param name: name of object (example: phone) :param value: object contents :type value: str, list(str), list(list(str)), list(dict) :param indentation: indent all by number of spaces :param index_of_colon: use to position : at the name string (-1 for no space) :param show_multi_line_character: option to hide "|" :returns: yaml formatted string array of name, value pair """ strings = [] if isinstance(value, list): # special case for single item lists: if len(value) == 1 and isinstance(value[0], str): # value = ["string"] should not be converted to # name: # - string # but to "name: string" instead value = value[0] elif len(value) == 1 and isinstance(value[0], list) \ and len(value[0]) == 1 and isinstance(value[0][0], str): # same applies to value = [["string"]] value = value[0][0] if isinstance(value, str): strings.append("{}{}{}: {}".format( ' ' * indentation, name, ' ' * (index_of_colon-len(name)), indent_multiline_string(value, indentation+4, show_multi_line_character))) elif isinstance(value, list): strings.append("{}{}{}: ".format( ' ' * indentation, name, ' ' * (index_of_colon-len(name)))) for outer in value: # special case for single item sublists if isinstance(outer, list) and len(outer) == 1 \ and isinstance(outer[0], str): # outer = ["string"] should not be converted to # - # - string # but to "- string" instead outer = outer[0] if isinstance(outer, str): strings.append("{}- {}".format( ' ' * (indentation+4), indent_multiline_string( outer, indentation+8, show_multi_line_character))) elif isinstance(outer, list): strings.append("{}- ".format(' ' * (indentation+4))) for inner in outer: if isinstance(inner, str): strings.append("{}- {}".format( ' ' * (indentation+8), indent_multiline_string( inner, indentation+12, show_multi_line_character))) elif isinstance(outer, dict): # ABLABEL'd lists for k in outer: strings += convert_to_yaml( "- " + k, outer[k], indentation+4, index_of_colon, show_multi_line_character) return strings def indent_multiline_string(input: Union[str, List], indentation: int, show_multi_line_character: bool) -> str: # if input is a list, convert to string first if isinstance(input, list): input = list_to_string(input, "") # format multiline string if "\n" in input or ": " in input: lines = ["|"] if show_multi_line_character else [""] for line in input.split("\n"): lines.append("{}{}".format(' ' * indentation, line.strip())) return '\n'.join(lines) return input.strip() def get_new_contact_template( supported_private_objects: Optional[List[str]] = None) -> str: formatted_private_objects = [] if supported_private_objects: formatted_private_objects.append("") longest_key = max(supported_private_objects, key=len) for object in supported_private_objects: formatted_private_objects += convert_to_yaml( object, "", 12, len(longest_key)+1, True) template = pathlib.Path(__file__).parent / 'data' / 'template.yaml' with template.open() as temp: return temp.read().format('\n'.join(formatted_private_objects)) khard-0.17.0/khard/khard.py000066400000000000000000001361371371517016500154450ustar00rootroot00000000000000"""Main application logic of khard includeing command line handling""" from argparse import Namespace import datetime from email import message_from_string from email.policy import SMTP as SMTP_POLICY from email.headerregistry import Address, AddressHeader, Group import logging import os import subprocess import sys from tempfile import NamedTemporaryFile from typing import cast, Dict, Iterable, List, Optional, TypeVar, Union from unidecode import unidecode from . import helpers from .address_book import (AddressBookCollection, AddressBookNameError, AddressBookParseError, VdirAddressBook) from .carddav_object import CarddavObject from . import cli from .config import Config from .formatter import Formatter from .query import AndQuery, AnyQuery, NameQuery, OrQuery, Query, TermQuery from .version import version as khard_version logger = logging.getLogger(__name__) config: Config T = TypeVar("T") def confirm(message: str) -> bool: """Ask the user for confirmation on the terminal. :param message: the question to print :returns: the answer of the user """ while True: answer = input(message + ' (y/N) ') answer = answer.lower() if answer == 'y': return True if answer in ['', 'n', 'q']: return False print('Please answer with "y" for yes or "n" for no.') def select(items: List[T], include_none: bool = False) -> Optional[T]: """Ask the user to select an item from a list. The list should be displayed to the user before calling this function and should be indexed starting with 1. This function might exit if the user selects "q". :param items: the list from which to select :param include_none: weather to allow the selection of no item :returns: None or the selected item """ while True: try: answer = input("Enter Index ({}q to quit): ".format( "0 for None, " if include_none else "")) answer = answer.lower() if answer in ["", "q"]: print("Canceled") return None index = int(answer) if include_none and index == 0: return None if index > 0: return items[index - 1] except (EOFError, IndexError, ValueError): pass print("Please enter an index value between 1 and {} or q to exit." .format(len(items))) def write_temp_file(text: str = "") -> str: """Create a new temporary file and write some initial text to it. :param text: the text to write to the temp file :returns: the file name of the newly created temp file """ with NamedTemporaryFile(mode='w+t', suffix='.yml', delete=False) as tmp: tmp.write(text) return tmp.name def edit(*filenames: str, merge: bool = False) -> None: """Edit the given files with the configured editor or merge editor""" editor = config.merge_editor if merge else config.editor editor = [editor] if isinstance(editor, str) else editor editor.extend(filenames) child = subprocess.Popen(editor) child.communicate() def create_new_contact(address_book: VdirAddressBook) -> None: # create temp file template = "# create new contact\n# Address book: {}\n# Vcard version: " \ "{}\n# if you want to cancel, exit without saving\n\n{}".format( address_book, config.preferred_vcard_version, helpers.get_new_contact_template(config.private_objects)) temp_file_name = write_temp_file(template) temp_file_creation = helpers.file_modification_date(temp_file_name) while True: edit(temp_file_name) if temp_file_creation == helpers.file_modification_date( temp_file_name): new_contact = None os.remove(temp_file_name) break # read temp file contents after editing with open(temp_file_name, "r") as tmp: new_contact_yaml = tmp.read() # try to create new contact try: new_contact = CarddavObject.from_yaml( address_book, new_contact_yaml, config.private_objects, config.preferred_vcard_version, config.localize_dates) except ValueError as err: print("\n{}\n".format(err)) if not confirm("Do you want to open the editor again?"): print("Canceled") os.remove(temp_file_name) sys.exit(0) else: os.remove(temp_file_name) break # create carddav object from temp file if new_contact is None or template == new_contact_yaml: print("Canceled") else: new_contact.write_to_file() print("Creation successful\n\n{}".format(new_contact.pretty())) def modify_existing_contact(old_contact: CarddavObject) -> None: # create temp file and open it with the specified text editor temp_file_name = write_temp_file( "# Edit contact: {}\n# Address book: {}\n# Vcard version: {}\n" "# if you want to cancel, exit without saving\n\n{}".format( old_contact, old_contact.address_book, old_contact.version, old_contact.to_yaml())) temp_file_creation = helpers.file_modification_date(temp_file_name) while True: edit(temp_file_name) if temp_file_creation == helpers.file_modification_date( temp_file_name): new_contact = None os.remove(temp_file_name) break # read temp file contents after editing with open(temp_file_name, "r") as tmp: new_contact_template = tmp.read() # try to create contact from user input try: new_contact = CarddavObject.clone_with_yaml_update( old_contact, new_contact_template, config.localize_dates) except ValueError as err: print("\n{}\n".format(err)) if not confirm("Do you want to open the editor again?"): print("Canceled") os.remove(temp_file_name) sys.exit(0) else: os.remove(temp_file_name) break # check if the user changed anything if new_contact is None or old_contact == new_contact: print("Nothing changed\n\n{}".format(old_contact.pretty())) else: new_contact.write_to_file(overwrite=True) print("Modification successful\n\n{}".format(new_contact.pretty())) def merge_existing_contacts(source_contact: CarddavObject, target_contact: CarddavObject, delete_source_contact: bool) -> None: # show warning, if target vcard version is not 3.0 or 4.0 if target_contact.version not in config.supported_vcard_versions: print("Warning:\nThe target contact in which to merge is based on " "vcard version {} but khard only supports the modification of " "vcards with version 3.0 and 4.0.\nIf you proceed, the contact " "will be converted to vcard version {} but beware: This could " "corrupt the contact file or cause data loss.".format( target_contact.version, config.preferred_vcard_version)) if not confirm("Do you want to proceed anyway?"): print("Canceled") sys.exit(0) # create temp files for each vcard # source vcard source_temp_file_name = write_temp_file( "# merge from {}\n# Address book: {}\n# Vcard version: {}\n" "# if you want to cancel, exit without saving\n\n{}".format( source_contact, source_contact.address_book, source_contact.version, source_contact.to_yaml())) # target vcard target_temp_file_name = write_temp_file( "# merge into {}\n# Address book: {}\n# Vcard version: {}\n" "# if you want to cancel, exit without saving\n\n{}".format( target_contact, target_contact.address_book, target_contact.version, target_contact.to_yaml())) target_temp_file_creation = helpers.file_modification_date( target_temp_file_name) while True: edit(source_temp_file_name, target_temp_file_name, merge=True) if target_temp_file_creation == helpers.file_modification_date( target_temp_file_name): merged_contact = None os.remove(source_temp_file_name) os.remove(target_temp_file_name) break # load target template contents with open(target_temp_file_name, "r") as target_tf: merged_contact_template = target_tf.read() # try to create contact from user input try: merged_contact = CarddavObject.clone_with_yaml_update( target_contact, merged_contact_template, config.localize_dates) except ValueError as err: print("\n{}\n".format(err)) if not confirm("Do you want to open the editor again?"): print("Canceled") os.remove(source_temp_file_name) os.remove(target_temp_file_name) return else: os.remove(source_temp_file_name) os.remove(target_temp_file_name) break # compare them if merged_contact is None or target_contact == merged_contact: print("Target contact unmodified\n\n{}".format( target_contact.pretty())) sys.exit(0) print("Merge contact {} from address book {} into contact {} from address " "book {}\n\n".format(source_contact, source_contact.address_book, merged_contact, merged_contact.address_book)) if delete_source_contact: print("To be removed") else: print("Keep unchanged") print("\n\n{}\n\nMerged\n\n{}\n".format(source_contact.pretty(), merged_contact.pretty())) if not confirm("Are you sure?"): print("Canceled") return # save merged_contact to disk and delete source contact merged_contact.write_to_file(overwrite=True) if delete_source_contact: source_contact.delete_vcard_file() print("Merge successful\n\n{}".format(merged_contact.pretty())) def copy_contact(contact: CarddavObject, target_address_book: VdirAddressBook, delete_source_contact: bool) -> None: source_contact_filename = "" if delete_source_contact: # if source file should be moved, get its file location to delete after # successful movement source_contact_filename = contact.filename if not delete_source_contact or not contact.uid: # if copy contact or contact has no uid yet # create a new uid contact.uid = helpers.get_random_uid() # set destination file name contact.filename = os.path.join(target_address_book.path, "{}.vcf".format(contact.uid)) # save contact.write_to_file() # delete old file if os.path.isfile(source_contact_filename): os.remove(source_contact_filename) print("{} contact {} from address book {} to {}".format( "Moved" if delete_source_contact else "Copied", contact, contact.address_book, target_address_book)) def list_address_books(address_books: Union[AddressBookCollection, List[VdirAddressBook]]) -> None: table = [["Index", "Address book"]] for index, address_book in enumerate(address_books, 1): table.append([cast(str, index), address_book.name]) print(helpers.pretty_print(table)) def list_contacts(vcard_list: List[CarddavObject], fields: Iterable[str] = (), parsable: bool = False) -> None: selected_address_books: List[VdirAddressBook] = [] for contact in vcard_list: if contact.address_book not in selected_address_books: selected_address_books.append(contact.address_book) table = [] # table header if len(selected_address_books) == 1: if not parsable: print("Address book: {}".format(selected_address_books[0])) table_header = ["index", "name", "phone", "email"] else: if not parsable: print("Address books: {}".format(', '.join( [str(book) for book in selected_address_books]))) table_header = ["index", "name", "phone", "email", "address_book"] if config.show_uids: table_header.append("uid") if parsable: # Legacy default header fields for parsable. table_header = ["uid", "name", "address_book"] if fields: table_header = [x.lower().replace(' ', '_') for x in fields] abook_collection = AddressBookCollection('short uids collection', selected_address_books) if not parsable: table.append([x.title().replace('_', ' ') for x in table_header]) # table body formatter = Formatter(config.display, config.preferred_email_address_type, config.preferred_phone_number_type, config.show_nicknames, parsable) for index, vcard in enumerate(vcard_list): row = [] for field in table_header: if field == 'index': row.append(str(index + 1)) elif field in ['name', 'phone', 'email']: row.append(formatter.get_special_field(vcard, field)) elif field == 'uid': if parsable: row.append(vcard.uid) elif abook_collection.get_short_uid(vcard.uid): row.append(abook_collection.get_short_uid(vcard.uid)) else: row.append("") else: row.append(formatter.get_nested_field(vcard, field)) if parsable: print("\t".join([str(v) for v in row])) else: table.append(row) if not parsable: print(helpers.pretty_print(table)) def list_with_headers(the_list: List, *headers: str) -> None: table = [list(headers)] for row in the_list: table.append(row.split("\t")) print(helpers.pretty_print(table)) def choose_address_book_from_list(header_string: str, address_books: Union[AddressBookCollection, List[VdirAddressBook]] ) -> Optional[VdirAddressBook]: if not address_books: return None if len(address_books) == 1: return address_books[0] print(header_string) list_address_books(address_books) # For all intents and purposes of select() an AddressBookCollection can # also be considered a List[VdirAddressBook]. return select(cast(List[VdirAddressBook], address_books)) def choose_vcard_from_list(header_string: str, vcard_list: List[CarddavObject], include_none: bool = False ) -> Optional[CarddavObject]: if not vcard_list: return None if len(vcard_list) == 1 and not include_none: return vcard_list[0] print(header_string) list_contacts(vcard_list) return select(vcard_list, True) def get_contact_list_by_user_selection( address_books: Union[VdirAddressBook, AddressBookCollection], query: Query) -> List[CarddavObject]: """Find contacts in the given address book grouped, sorted and reversed acording to the loaded configuration . :param address_books: the address book to search :param query: the query to use when searching :returns: list of found CarddavObject objects """ return get_contacts(address_books, query, config.reverse, config.group_by_addressbook, config.sort) def get_contacts(address_book: Union[VdirAddressBook, AddressBookCollection], query: Query, reverse: bool = False, group: bool = False, sort: str = "first_name") -> List[CarddavObject]: """Get a list of contacts from one or more address books. :param address_book: the address book to search :param query: a search query to select contacts :param reverse: reverse the order of the returned contacts :param group: group results by address book :param sort: the field to use for sorting, one of "first_name", "last_name", "formatted_name" :returns: contacts from the address_book that match the query """ # Search for the contacts in all address books. contacts = address_book.search(query) # Sort the contacts. if group: if sort == "first_name": return sorted(contacts, reverse=reverse, key=lambda x: ( unidecode(x.address_book.name).lower(), unidecode(x.get_first_name_last_name()).lower())) if sort == "last_name": return sorted(contacts, reverse=reverse, key=lambda x: ( unidecode(x.address_book.name).lower(), unidecode(x.get_last_name_first_name()).lower())) if sort == "formatted_name": return sorted(contacts, reverse=reverse, key=lambda x: ( unidecode(x.address_book.name).lower(), unidecode(x.formatted_name.lower()))) else: if sort == "first_name": return sorted(contacts, reverse=reverse, key=lambda x: unidecode(x.get_first_name_last_name()).lower()) if sort == "last_name": return sorted(contacts, reverse=reverse, key=lambda x: unidecode(x.get_last_name_first_name()).lower()) if sort == "formatted_name": return sorted(contacts, reverse=reverse, key=lambda x: unidecode(x.formatted_name.lower())) raise ValueError('sort must be "first_name", "last_name" or ' '"formatted_name" not {}.'.format(sort)) def prepare_search_queries(args: Namespace) -> Dict[str, Query]: """Prepare the search query string from the given command line args. Each address book can get a search query string to filter vcards befor loading them. Depending on the question if the address book is used for source or target searches different queries have to be combined. :param args: the parsed command line :returns: a dict mapping abook names to their loading queries """ # get all possible search queries for address book parsing source_queries: List[Query] = [] target_queries: List[Query] = [] if "source_search_terms" in args: source_queries.append(args.source_search_terms) if "search_terms" in args: source_queries.append(args.search_terms) if "target_contact" in args: target_queries.append(args.target_contact) source_query = AndQuery.reduce(source_queries) target_query = AndQuery.reduce(target_queries) logger.debug('Created source query: %s', source_query) logger.debug('Created target query: %s', target_query) # Get all possible search queries for address book parsing, always # depending on the fact if the address book is used to find source or # target contacts or both. queries: Dict[str, List[Query]] = { abook.name: [] for abook in config.abooks} for name in queries: if "addressbook" in args and name in args.addressbook: queries[name].append(source_query) if "target_addressbook" in args and name in args.target_addressbook: queries[name].append(target_query) queries2: Dict[str, Query] = { n: OrQuery.reduce(q) for n, q in queries.items()} logger.debug('Created query: %s', queries) return queries2 def generate_contact_list(args: Namespace) -> List[CarddavObject]: """Find the contact list with which we will work later on :param args: the command line arguments :returns: the contacts for further processing """ if "source_search_terms" in args: # exception for merge command args.search_terms = args.source_search_terms or AnyQuery() if "search_terms" not in args: # It is simpler to handle subcommand that do not have and need search # terms here than conditionally calling generate_contact_list(). return [] return get_contact_list_by_user_selection(args.addressbook, args.search_terms) def new_subcommand(selected_address_books: AddressBookCollection, input_from_stdin_or_file: str, open_editor: bool) -> None: """Create a new contact. :param selected_address_books: a list of addressbooks that were selected on the command line :param input_from_stdin_or_file: the data for the new contact as a yaml formatted string :param open_editor: whether to open the new contact in the edior after creation """ # ask for address book, in which to create the new contact selected_address_book = choose_address_book_from_list( "Select address book for new contact", selected_address_books) if selected_address_book is None: sys.exit("Error: address book list is empty") # if there is some data in stdin if input_from_stdin_or_file: # create new contact from stdin try: new_contact = CarddavObject.from_yaml( selected_address_book, input_from_stdin_or_file, config.private_objects, config.preferred_vcard_version, config.localize_dates) except ValueError as err: sys.exit(err) else: new_contact.write_to_file() if open_editor: modify_existing_contact(new_contact) else: print("Creation successful\n\n{}".format(new_contact.pretty())) else: create_new_contact(selected_address_book) def add_email_to_contact(name: str, email_address: str, abooks: AddressBookCollection) -> None: """Add a new email address to the given contact, creating the contact if necessary. :param name: name of the contact :param email_address: email address of the contact :param abooks: the addressbooks that were selected on the command line """ print("Email address: {}".format(email_address)) if not name: name = input("Contact's name: ") # search for an existing contact selected_vcard = choose_vcard_from_list( "Select contact for the found e-mail address", get_contact_list_by_user_selection(abooks, TermQuery(name))) if selected_vcard is None: if not name: return # create new contact if not confirm("Contact '{}' does not exist. Do you want to create it?" .format(name)): print("Cancelled") return # ask for address book, in which to create the new contact selected_address_book = choose_address_book_from_list( "Select address book for new contact", config.abooks) if selected_address_book is None: sys.exit("Error: address book list is empty") name_parts = name.split() first = name_parts[0] if len(name_parts) > 0 else "" last = name_parts[-1] if len(name_parts) > 1 else "" # ask for name and organisation of new contact while True: if first: first_name = input("First name [empty for '{}']: ".format(first)) if not first_name: first_name = first else: first_name = input("First name: ") if last: last_name = input("Last name [empty for '{}']: ".format(last)) if not last_name: last_name = last else: last_name = input("Last name: ") organisation = input("Organisation: ") if not first_name and not last_name and not organisation: print("Error: All fields are empty.") else: break selected_vcard = CarddavObject.from_yaml( selected_address_book, "First name : {}\nLast name : {}\nOrganisation : {}".format( first_name, last_name, organisation), config.private_objects, config.preferred_vcard_version, config.localize_dates) # check if the contact already contains the email address for _, email_list in sorted(selected_vcard.emails.items(), key=lambda k: k[0].lower()): for email in email_list: if email == email_address: print("The contact {} already contains the email address {}" .format(selected_vcard, email_address)) return # ask for confirmation again if not confirm("Do you want to add the email address {} to the contact {}?" .format(email_address, selected_vcard)): print("Cancelled") return # ask for the email label print("\nAdding email address {} to contact {}\n" "Enter email label\n" " vcard 3.0: At least one of home, internet, pref, work, x400\n" " vcard 4.0: At least one of home, internet, pref, work\n" " Or a custom label (only letters)".format(email_address, selected_vcard)) while True: label = input("email label [internet]: ") or "internet" try: selected_vcard.add_email(label, email_address) except ValueError as err: print(err) else: break # save to disk selected_vcard.write_to_file(overwrite=True) print("Done.\n\n{}".format(selected_vcard.pretty())) def find_email_addresses(text: str, fields: List[str]) -> List[Address]: """Search the text for email addresses in the given fields. :param text: the text to search for email addresses :param fields: the fields to look in for email addresses. The `all` field searches all headers. """ message = message_from_string(text, policy=SMTP_POLICY) def extract_addresses(header) -> List[Address]: if header and isinstance(header, (AddressHeader, Group)): return list(header.addresses) return [] email_addresses = [] _all = any([f == "all" for f in fields]) if _all: for _, value in message.items(): email_addresses.extend(extract_addresses(value)) else: for field in fields: email_addresses.extend(extract_addresses(message[field])) return email_addresses def add_email_subcommand( text: str, abooks: AddressBookCollection, fields: List[str]) -> None: """Add a new email address to contacts, creating new contacts if necessary. :param text: the input text to search for the new email :param abooks: the addressbooks that were selected on the command line :param field: the header field to extract contacts from """ email_addresses = find_email_addresses(text, fields) if not email_addresses: sys.exit("No email addresses found in fields {}".format(fields)) print("Khard: Add email addresses to contacts") for email_address in email_addresses: name = email_address.display_name address = email_address.addr_spec add_email_to_contact(name, address, abooks) print() print("No more email addresses") def birthdays_subcommand(vcard_list: List[CarddavObject], parsable: bool ) -> None: """Print birthday contact table. :param vcard_list: the vcards to search for matching entries which should be printed :param parsable: machine readable output: columns devided by tabulator (\t) """ # filter out contacts without a birthday date vcard_list = [vcard for vcard in vcard_list if vcard.birthday is not None] # sort by date (month and day) # The sort function should work for strings and datetime objects. All # strings will besorted before any datetime objects. vcard_list.sort(key=lambda x: (x.birthday.month, x.birthday.day) if isinstance(x.birthday, datetime.datetime) else (0, 0, x.birthday)) # add to string list birthday_list = [] formatter = Formatter(config.display, config.preferred_email_address_type, config.preferred_phone_number_type, config.show_nicknames, parsable) for vcard in vcard_list: name = formatter.get_special_field(vcard, "name") if parsable: # We did filter out None above but the typechecker does not know # this. bday = cast(Union[str, datetime.datetime], vcard.birthday) if isinstance(bday, str): date = bday else: date = bday.strftime("%Y.%m.%d") birthday_list.append("{}\t{}".format(date, name)) else: date = vcard.get_formatted_birthday() birthday_list.append("{}\t{}".format(name, date)) if birthday_list: if parsable: print('\n'.join(birthday_list)) else: list_with_headers(birthday_list, "Name", "Birthday") else: if not parsable: print("Found no birthdays") sys.exit(1) def phone_subcommand(vcard_list: List[CarddavObject], parsable: bool) -> None: """Print a phone application friendly contact table. :param vcard_list: the vcards to search for matching entries which should be printed :param parsable: machine readable output: columns devided by tabulator (\t) """ formatter = Formatter(config.display, config.preferred_email_address_type, config.preferred_phone_number_type, config.show_nicknames, parsable) numbers = [] for vcard in vcard_list: for type, number_list in sorted(vcard.phone_numbers.items(), key=lambda k: k[0].lower()): for number in sorted(number_list): name = formatter.get_special_field(vcard, "name") if parsable: # parsable option: start with phone number fields = number, name, type else: # else: start with name fields = name, type, number numbers.append("\t".join(fields)) if numbers: if parsable: print('\n'.join(numbers)) else: list_with_headers(numbers, "Name", "Type", "Phone") else: if not parsable: print("Found no phone numbers") sys.exit(1) def post_address_subcommand(vcard_list: List[CarddavObject], parsable: bool ) -> None: """Print a contact table. with all postal / mailing addresses :param vcard_list: the vcards to search for matching entries which should be printed :param parsable: machine readable output: columns devided by tabulator (\t) """ formatter = Formatter(config.display, config.preferred_email_address_type, config.preferred_phone_number_type, config.show_nicknames, parsable) addresses = [] for vcard in vcard_list: name = formatter.get_special_field(vcard, "name") # create post address line list contact_addresses = [] if parsable: for type, post_addresses in sorted(vcard.post_addresses.items(), key=lambda k: k[0].lower()): for post_address in post_addresses: contact_addresses.append([str(post_address), name, type]) else: for type, formatted_addresses in sorted( vcard.get_formatted_post_addresses().items(), key=lambda k: k[0].lower()): for address in sorted(formatted_addresses): contact_addresses.append([name, type, address]) for addr in contact_addresses: addresses.append("\t".join(addr)) if addresses: if parsable: print('\n'.join(addresses)) else: list_with_headers(addresses, "Name", "Type", "Post address") else: if not parsable: print("Found no post addresses") sys.exit(1) def email_subcommand(search_terms: Query, vcard_list: List[CarddavObject], parsable: bool, remove_first_line: bool) -> None: """Print a mail client friendly contacts table that is compatible with the default format used by mutt. Output format: .. code-block:: text single line of text email_address\tname\ttype email_address\tname\ttype [...] :param search_terms: used as search term to filter the contacts before printing :param vcard_list: the vcards to search for matching entries which should be printed :param parsable: machine readable output: columns devided by tabulator (\t) :param remove_first_line: remove first line (searching for '' ...) """ formatter = Formatter(config.display, config.preferred_email_address_type, config.preferred_phone_number_type, config.show_nicknames, parsable) emails = [] for vcard in vcard_list: for type, email_list in sorted(vcard.emails.items(), key=lambda k: k[0].lower()): for email in sorted(email_list): name = formatter.get_special_field(vcard, "name") if parsable: # parsable option: start with email address fields = email, name, type else: # else: start with name fields = name, type, email emails.append("\t".join(fields)) if emails: if parsable: if not remove_first_line: # at least mutt requires that line print("searching for '{}' ...".format(search_terms)) print('\n'.join(emails)) else: list_with_headers(emails, "Name", "Type", "E-Mail") else: if not parsable: print("Found no email addresses") elif not remove_first_line: print("searching for '{}' ...".format(search_terms)) sys.exit(1) def list_subcommand(vcard_list: List[CarddavObject], parsable: bool, fields: List[str]) -> None: """Print a user friendly contacts table. :param vcard_list: the vcards to print :param parsable: machine readable output: columns devided by tabulator (\t) :param fields: list of strings for field evaluation """ if not vcard_list: if not parsable: print("Found no contacts") sys.exit(1) else: list_contacts(vcard_list, fields, parsable) def modify_subcommand(selected_vcard: CarddavObject, input_from_stdin_or_file: str, open_editor: bool, source: bool = False) -> None: """Modify a contact in an external editor. :param selected_vcard: the contact to modify :param input_from_stdin_or_file: new data from stdin (or a file) that should be incorperated into the contact, this should be a yaml formatted string :param open_editor: whether to open the new contact in the edior after creation :param source: edit the source file or a yaml version? """ if source: edit(selected_vcard.filename) return # show warning, if vcard version of selected contact is not 3.0 or 4.0 if selected_vcard.version not in config.supported_vcard_versions: print("Warning:\nThe selected contact is based on vcard version {} " "but khard only supports the creation and modification of vcards" " with version 3.0 and 4.0.\nIf you proceed, the contact will be" " converted to vcard version {} but beware: This could corrupt " "the contact file or cause data loss.".format( selected_vcard.version, config.preferred_vcard_version)) if not confirm("Do you want to proceed anyway?"): print("Canceled") return # if there is some data in stdin if input_from_stdin_or_file: # create new contact from stdin try: new_contact = CarddavObject.clone_with_yaml_update( selected_vcard, input_from_stdin_or_file, config.localize_dates) except ValueError as err: sys.exit(err) if selected_vcard == new_contact: print("Nothing changed\n\n{}".format(new_contact.pretty())) else: print("Modification\n\n{}\n".format(new_contact.pretty())) if confirm("Do you want to proceed?"): new_contact.write_to_file(overwrite=True) if open_editor: modify_existing_contact(new_contact) else: print("Done") else: print("Canceled") else: modify_existing_contact(selected_vcard) def remove_subcommand(selected_vcard: CarddavObject, force: bool) -> None: """Remove a contact from the addressbook. :param selected_vcard: the contact to delete :param force: delete without confirmation """ if not force and not confirm( "Deleting contact {} from address book {}. Are you sure?".format( selected_vcard, selected_vcard.address_book)): print("Canceled") return selected_vcard.delete_vcard_file() print("Contact {} deleted successfully".format( selected_vcard.formatted_name)) def merge_subcommand(vcard_list: List[CarddavObject], abooks: AddressBookCollection, search_terms: Query ) -> None: """Merge two contacts into one. :param vcard_list: the vcards from which to choose contacts for mergeing :param abooks: the addressbooks to use to find the target contact :param search_terms: the search terms to find the target contact """ # Find possible target contacts. target_vcards = get_contact_list_by_user_selection(abooks, search_terms) # get the source vcard, from which to merge source_vcard = choose_vcard_from_list("Select contact from which to merge", vcard_list) if source_vcard is None: sys.exit("Found no source contact for merging") else: print("Merge from {} from address book {}\n\n".format( source_vcard, source_vcard.address_book)) # get the target vcard, into which to merge target_vcard = choose_vcard_from_list("Select contact into which to merge", target_vcards) if target_vcard is None: sys.exit("Found no target contact for merging") else: print("Merge into {} from address book {}\n\n".format( target_vcard, target_vcard.address_book)) # merging if source_vcard == target_vcard: print("The selected contacts are already identical") else: merge_existing_contacts(source_vcard, target_vcard, True) def copy_or_move_subcommand(action: str, vcard_list: List[CarddavObject], target_address_books: AddressBookCollection ) -> None: """Copy or move a contact to a different address book. :param action: the string "copy" or "move" to indicate what to do :param vcard_list: the contact list from which to select one for the action :param target_address_books: the target address books """ # get the source vcard, which to copy or move source_vcard = choose_vcard_from_list( "Select contact to {}".format(action.title()), vcard_list) if source_vcard is None: sys.exit("Found no contact") else: print("{} contact {} from address book {}".format( action.title(), source_vcard, source_vcard.address_book)) # get target address book if len(target_address_books) == 1 \ and target_address_books[0] == source_vcard.address_book: sys.exit("The address book {} already contains the contact {}".format( target_address_books[0], source_vcard)) else: available_address_books = [abook for abook in target_address_books if abook != source_vcard.address_book] target_abook = choose_address_book_from_list( "Select target address book", available_address_books) if target_abook is None: sys.exit("Error: address book list is empty") # check if a contact already exists in the target address book target_vcard = choose_vcard_from_list( "Select target contact to overwrite (or None to add a new entry)", get_contact_list_by_user_selection( target_abook, TermQuery(source_vcard.formatted_name)), True) # If the target contact doesn't exist, move or copy the source contact into # the target address book without further questions. if target_vcard is None: copy_contact(source_vcard, target_abook, action == "move") elif source_vcard == target_vcard: # source and target contact are identical print("Target contact: {}".format(target_vcard)) if action == "move": copy_contact(source_vcard, target_abook, True) else: print("The selected contacts are already identical") else: # source and target contacts are different # either overwrite the target one or merge into target contact print("The address book {} already contains the contact {}\n\n" "Source\n\n{}\n\nTarget\n\n{}\n\nPossible actions:\n" " a: {} anyway\n" " m: Merge from source into target contact\n" " o: Overwrite target contact\n" " q: Quit".format(target_vcard.address_book, source_vcard, source_vcard.pretty(), target_vcard.pretty(), action.title())) while True: input_string = input("Your choice: ") if input_string.lower() == "a": copy_contact(source_vcard, target_abook, action == "move") break if input_string.lower() == "o": copy_contact(source_vcard, target_abook, action == "move") target_vcard.delete_vcard_file() break if input_string.lower() == "m": merge_existing_contacts(source_vcard, target_vcard, action == "move") break if input_string.lower() in ["", "q"]: print("Canceled") break def main(argv: List[str] = sys.argv[1:]) -> None: args, conf = cli.init(argv) # store the config instance in the module level variable global config config = conf # Check some of the simpler subcommands first. These don't have any # options and can directly be run. if args.action == "addressbooks": print('\n'.join(str(book) for book in config.abooks)) return if args.action == "template": print("# Contact template for khard version {}\n#\n" "# Use this yaml formatted template to create a new contact:\n" "# either with: khard new -a address_book -i template.yaml\n" "# or with: cat template.yaml | khard new -a address_book\n" "\n{}".format(khard_version, helpers.get_new_contact_template( config.private_objects))) return search_queries = prepare_search_queries(args) # load address books try: if "addressbook" in args: args.addressbook = config.get_address_books(args.addressbook, search_queries) if "target_addressbook" in args: args.target_addressbook = config.get_address_books( args.target_addressbook, search_queries) except AddressBookParseError as err: sys.exit("{}\nUse --debug for more information or --skip-unparsable " "to proceed".format(err)) except AddressBookNameError as err: sys.exit(err) vcard_list = generate_contact_list(args) if args.action == "filename": print('\n'.join(contact.filename for contact in vcard_list)) return # read from template file or stdin if available input_from_stdin_or_file = "" if "input_file" in args: if args.input_file != "-": # try to read from specified input file try: with open(args.input_file, "r") as infile: input_from_stdin_or_file = infile.read() except IOError as err: sys.exit("Error: {}\n File: {}".format(err.strerror, err.filename)) elif not sys.stdin.isatty(): # try to read from stdin try: input_from_stdin_or_file = sys.stdin.read() except IOError: sys.exit("Error: Can't read from stdin") # try to reopen console # otherwise further user interaction is not possible (for example # selecting a contact from the contact table) try: sys.stdin = open('/dev/tty') except IOError: pass if args.action == "new": new_subcommand(args.addressbook, input_from_stdin_or_file, args.open_editor) elif args.action == "add-email": add_email_subcommand(input_from_stdin_or_file, args.addressbook, args.fields) elif args.action == "birthdays": birthdays_subcommand(vcard_list, args.parsable) elif args.action == "phone": phone_subcommand(vcard_list, args.parsable) elif args.action == "postaddress": post_address_subcommand(vcard_list, args.parsable) elif args.action == "email": email_subcommand(args.search_terms, vcard_list, args.parsable, args.remove_first_line) elif args.action == "list": list_subcommand(vcard_list, args.parsable, args.fields) elif args.action in ["show", "edit", "remove"]: selected_vcard = choose_vcard_from_list( "Select contact for {} action".format(args.action.title()), vcard_list) if selected_vcard is None: sys.exit("Found no contact") if args.action == "show": if args.format == "pretty": output = selected_vcard.pretty() elif args.format == "vcard": output = open(selected_vcard.filename).read() else: output = "# Contact template for khard version {}\n" \ "# Name: {}\n# Vcard version: {}\n\n{}".format( khard_version, selected_vcard, selected_vcard.version, selected_vcard.to_yaml()) args.output_file.write(output) elif args.action == "edit": modify_subcommand(selected_vcard, input_from_stdin_or_file, args.open_editor, args.format == 'vcard') elif args.action == "remove": remove_subcommand(selected_vcard, args.force) elif args.action == "merge": merge_subcommand(vcard_list, args.target_addressbook, args.target_contact) elif args.action in ["copy", "move"]: copy_or_move_subcommand( args.action, vcard_list, args.target_addressbook) khard-0.17.0/khard/object_type.py000066400000000000000000000002701371517016500166470ustar00rootroot00000000000000"""Helper module for validating typed vcard properties""" from enum import Enum class ObjectType(Enum): string = 1 list_with_strings = 2 string_or_list_with_strings = 3 khard-0.17.0/khard/query.py000066400000000000000000000214111371517016500155050ustar00rootroot00000000000000"""Queries to match against contacts""" import abc from datetime import datetime from functools import reduce from operator import and_, or_ from typing import cast, Any, Dict, List, Optional, Union from . import carddav_object class Query(metaclass=abc.ABCMeta): """A query to match against strings, lists of strings and CarddavObjects""" @abc.abstractmethod def match(self, thing: Union[str, "carddav_object.CarddavObject"]) -> bool: """Match the self query against the given thing""" @abc.abstractmethod def get_term(self) -> Optional[str]: """Extract the search terms from a query.""" def __and__(self, other: "Query") -> "Query": """Combine two queries with AND""" if isinstance(self, NullQuery) or isinstance(other, NullQuery): return NullQuery() if isinstance(self, AnyQuery): return other if isinstance(other, AnyQuery): return self if isinstance(self, AndQuery) and isinstance(other, AndQuery): return AndQuery(*self._queries, *other._queries) if isinstance(self, AndQuery): return AndQuery(*self._queries, other) if isinstance(other, AndQuery): return AndQuery(self, *other._queries) return AndQuery(self, other) def __or__(self, other: "Query") -> "Query": """Combine two queries with OR""" if isinstance(self, AnyQuery) or isinstance(other, AnyQuery): return AnyQuery() if isinstance(self, NullQuery): return other if isinstance(other, NullQuery): return self if isinstance(self, OrQuery) and isinstance(other, OrQuery): return OrQuery(*self._queries, *other._queries) if isinstance(self, OrQuery): return OrQuery(*self._queries, other) if isinstance(other, OrQuery): return OrQuery(self, *other._queries) return OrQuery(self, other) def __eq__(self, other: object) -> bool: """A generic equality for all query types without parameters""" return isinstance(other, type(self)) def __hash__(self) -> int: "A generic hashing implementation for all queries without parameters" return hash(type(self)) class NullQuery(Query): """The null-query, it matches nothing.""" def match(self, thing: Union[str, "carddav_object.CarddavObject"]) -> bool: return False def get_term(self) -> None: return None def __str__(self) -> str: return "NONE" class AnyQuery(Query): """The match-anything-query, it always matches.""" def match(self, thing: Union[str, "carddav_object.CarddavObject"]) -> bool: return True def get_term(self) -> str: return "" def __hash__(self) -> int: return hash(NullQuery) def __str__(self) -> str: return "ALL" class TermQuery(Query): """A query to match an object against a fixed string.""" def __init__(self, term: str) -> None: self._term = term.lower() def match(self, thing: Union[str, "carddav_object.CarddavObject"]) -> bool: if isinstance(thing, str): return self._term in thing.lower() return self._term in thing.pretty().lower() def get_term(self) -> str: return self._term def __eq__(self, other: object) -> bool: return isinstance(other, TermQuery) and self._term == other._term def __hash__(self) -> int: return hash((TermQuery, self._term)) def __str__(self) -> str: return self._term class FieldQuery(TermQuery): """A query to match against a certain field in a carddav object.""" def __init__(self, field: str, value: str) -> None: self._field = field super().__init__(value) def match(self, thing: Union[str, "carddav_object.CarddavObject"]) -> bool: if isinstance(thing, str): return super().match(thing) if hasattr(thing, self._field): return self._match_union(getattr(thing, self._field)) return False def _match_union(self, value: Union[str, datetime, List, Dict[str, Any]] ) -> bool: if isinstance(value, str): return self.match(value) if isinstance(value, list): return any(self._match_union(item) for item in value) if isinstance(value, dict): for key in value: if self.match(key) or self._match_union(value[key]): return True return False if isinstance(value, datetime): # currently we only support ISO dates return value == datetime.strptime(self._term, "%Y-%m-%d") # this should actually be a type error return False def __eq__(self, other: object) -> bool: return isinstance(other, FieldQuery) and self._field == other._field \ and super().__eq__(other) def __hash__(self) -> int: return hash((FieldQuery, self._field, self._term)) def __str__(self) -> str: return '{}:{}'.format(self._field, self._term) class AndQuery(Query): """A query to combine multible queries with "and".""" def __init__(self, first: Query, second: Query, *queries: Query) -> None: self._queries = (first, second, *queries) def match(self, thing: Union[str, "carddav_object.CarddavObject"]) -> bool: return all(q.match(thing) for q in self._queries) def get_term(self) -> Optional[str]: terms = [x.get_term() for x in self._queries] if None in terms: return None return "".join(cast(List[str], terms)) def __eq__(self, other: object) -> bool: return isinstance(other, AndQuery) \ and frozenset(self._queries) == frozenset(other._queries) def __hash__(self) -> int: return hash((AndQuery, frozenset(self._queries))) @staticmethod def reduce(queries: List[Query], start: Optional[Query] = None) -> Query: return reduce(and_, queries, start or AnyQuery()) def __str__(self) -> str: return ' '.join(str(q) for q in self._queries) class OrQuery(Query): """A query to combine multible queries with "or".""" def __init__(self, first: Query, second: Query, *queries: Query) -> None: self._queries = (first, second, *queries) def match(self, thing: Union[str, "carddav_object.CarddavObject"]) -> bool: return any(q.match(thing) for q in self._queries) def get_term(self) -> Optional[str]: terms = [x.get_term() for x in self._queries] if all(t is None for t in terms): return None return "".join(filter(None, terms)) def __eq__(self, other: object) -> bool: return isinstance(other, OrQuery) \ and frozenset(self._queries) == frozenset(other._queries) def __hash__(self) -> int: return hash((OrQuery, frozenset(self._queries))) @staticmethod def reduce(queries: List[Query], start: Optional[Query] = None) -> Query: return reduce(or_, queries, start or NullQuery()) def __str__(self) -> str: return ' | '.join(str(q) for q in self._queries) class NameQuery(TermQuery): """special query to match any kind of name field of a vcard""" def __init__(self, term: str) -> None: super().__init__(term) self._props_query = OrQuery(FieldQuery("formatted_name", term), FieldQuery("nicknames", term)) def match(self, thing: Union[str, "carddav_object.CarddavObject"]) -> bool: m = super().match if isinstance(thing, str): return m(thing) return (m(thing.get_first_name_last_name()) or m(thing.get_last_name_first_name()) or self._props_query.match(thing)) def __eq__(self, other: object) -> bool: return isinstance(other, NameQuery) and self._term == other._term def __hash__(self) -> int: return hash((NameQuery, self._term)) def __str__(self) -> str: return 'name:{}'.format(self._term) def parse(string: str) -> Union[TermQuery, FieldQuery]: """Parse a string into a query object The input string interpreted as a :py:class:`FieldQuery` if it starts with a valid property name of the :py:class:`~khard.carddav_object.CarddavObject` class, followed by a colon and an arbitrary search term. Otherwise it is interpreted as a :py:class:`TermQuery`. :param string: a string to parse into a query :returns: a FieldQuery if the string contains a valid field specifier, a TermQuery otherwise """ if ":" in string: field, term = string.split(":", maxsplit=1) if field == "name": return NameQuery(term) if field in carddav_object.CarddavObject.get_properties(): return FieldQuery(field, term) return TermQuery(string) khard-0.17.0/misc/000077500000000000000000000000001371517016500136315ustar00rootroot00000000000000khard-0.17.0/misc/davcontroller/000077500000000000000000000000001371517016500165075ustar00rootroot00000000000000khard-0.17.0/misc/davcontroller/davcontroller.py000066400000000000000000000123401371517016500217370ustar00rootroot00000000000000#!/usr/bin/env python2 # -*- coding: utf-8 -*- import sys import argparse from caldavclientlibrary.client.account import CalDAVAccount from caldavclientlibrary.protocol.url import URL def main(): # create the args parser parser = argparse.ArgumentParser( description="Davcontroller creates, lists and removes caldav " "calendars and carddav address books from server") parser.add_argument("-v", "--version", action="store_true", help="Get current program version") parser.add_argument("-H", "--hostname", default="") parser.add_argument("-p", "--port", default="") parser.add_argument("-u", "--username", default="") parser.add_argument("-P", "--password", default="") parser.add_argument("action", nargs="?", default="", help="Actions: new-addressbook, new-calendar, list, " "and remove") args = parser.parse_args() # version if args.version is True: print("davcontroller version 0.1") sys.exit(0) # check the server's parameter if args.hostname == "": print("Missing host name") sys.exit(1) if args.port == "": print("Missing host port") sys.exit(1) if args.username == "": print("Missing user name") sys.exit(1) if args.password == "": print("Missing password") sys.exit(1) if args.action == "": print("Please specify an action. Possible values are: " "new-addressbook, new-calendar, list and remove") sys.exit(1) elif args.action not in ["new-addressbook", "new-calendar", "list", "remove"]: print("The specified action \"%s\" is not supported. Possible values " "are: new-addressbook, new-calendar, list and remove" % args.action) sys.exit(1) # try to connect to the caldav server account = CalDAVAccount(args.hostname, args.port, ssl=True, user=args.username, pswd=args.password, root="/", principal="") if account.getPrincipal() is None: print("Error: Connection refused") sys.exit(2) if args.action in ["list", "remove"]: # address books print("Available address books") addressbook_list = account.getPrincipal().listAddressBooks() if addressbook_list.__len__() == 0: print("No address books found") else: for index, addressbook in enumerate(addressbook_list): print("%d. %s" % (index+1, addressbook.getDisplayName())) print() # calendars print("Available calendars") calendar_list = account.getPrincipal().listCalendars() if calendar_list.__len__() == 0: print("No calendars found") else: for index, calendar in enumerate(calendar_list): print("%d. %s" % (addressbook_list.__len__() + index + 1, calendar.getDisplayName())) item_list = addressbook_list + calendar_list if item_list.__len__() == 0: sys.exit(2) if args.action == "remove": print() while True: input_string = input("Enter Id: ") if input_string == "": sys.exit(0) try: id = int(input_string) if id > 0 and id <= item_list.__len__(): break except ValueError: pass print("Please enter an Id between 1 and %d or nothing to exit." % item_list.__len__()) item = item_list[id-1] while True: input_string = input("Deleting %s. Are you sure? (y/n): " % item.getDisplayName()) if input_string.lower() in ["", "n", "q"]: print("Canceled") sys.exit(0) if input_string.lower() == "y": break account.session.deleteResource(URL(url=item.path)) if args.action.startswith("new-"): # get full host url host_url = "https://%s:%s" % (account.session.server, account.session.port) # enter new name if args.action == "new-addressbook": input_string = input("Enter new address book name or nothing to " "cancel: ") else: input_string = input("Enter new calendar name or nothing to " "cancel: ") if input_string == "": sys.exit(0) res_name = input_string res_path = input_string.replace(" ", "_").lower() # create new resource if args.action == "new-addressbook": u = URL(url=host_url + account.principal.adbkhomeset[0].path + res_path + "/") account.session.makeAddressBook(u, res_name) else: u = URL(url=host_url + account.principal.homeset[0].path + res_path + "/") account.session.makeCalendar(u, res_name) print("Creation successful") if __name__ == "__main__": main() khard-0.17.0/misc/sdiff/000077500000000000000000000000001371517016500147245ustar00rootroot00000000000000khard-0.17.0/misc/sdiff/sdiff_khard_wrapper.sh000077500000000000000000000012621371517016500212700ustar00rootroot00000000000000#!/bin/sh # khard requires the second file in the diff-operation to be modified in order # to recognize a successful merge. However the file provided to sdiff's "-o" # switch cannot be any of the source files. # # If you want to use sdiff to merge contacts you must set this wrapper script # as your merge editor in khard's config file # # merge_editor = /path/to/sdiff_khard_wrapper.sh set -e # First and second argument to sdiff. FIRST=$1 SECOND=$(mktemp) # Cleanup for signals and normal exit. trap 'trap "" EXIT; rm -f "$SECOND"; exit' INT HUP TERM trap 'rm -f "$SECOND"' EXIT # Free the filename that khard expects to change. mv -f "$2" "$SECOND" sdiff "$FIRST" "$SECOND" -o "$2" khard-0.17.0/misc/twinkle/000077500000000000000000000000001371517016500153065ustar00rootroot00000000000000khard-0.17.0/misc/twinkle/scripts/000077500000000000000000000000001371517016500167755ustar00rootroot00000000000000khard-0.17.0/misc/twinkle/scripts/config.py000066400000000000000000000014211371517016500206120ustar00rootroot00000000000000#!/usr/bin/python # -*- coding: utf-8 -*- import os # twinkle config folder twinkle_config = os.path.join(os.environ['HOME'], ".twinkle") # khard executable khard_exe = os.path.join(os.environ['HOME'], ".virtualenvs", "bin", "khard") # user language language = "de" # stop mpd stop_music = True mpd_host = "192.168.2.100" mpd_port = 6600 # log file for calls call_log_file = os.path.join(twinkle_config, "calls.log") # audio files constant_ringtone_segment = os.path.join(twinkle_config, "sounds", "ringtone_segment.wav") new_ringtone = os.path.join(twinkle_config, "sounds", "special_ringtone.wav") # temp files tmp_mono_file = "/tmp/caller_id.wav" tmp_file_stereo = "/tmp/caller_id_stereo.wav" mpd_lockfile = "/tmp/mpd_stopped" caller_id_filename = "/tmp/current_caller_id" khard-0.17.0/misc/twinkle/scripts/incoming_call.py000077500000000000000000000071751371517016500221620ustar00rootroot00000000000000#!/usr/bin/env python # This script speaks the incoming caller ID for the SIP client Twinkle # The following programs are needed: espeak, ffmpeg and sox, mpc is optional # aptitude install ffmpeg espeak sox mpc # Further information about Twinkle scripts can be found at # http://mfnboer.home.xs4all.nl/twinkle/manual.html#profile_scripts import os, subprocess, sys, re import config def get_caller_id(from_hdr): caller_id = from_hdr[from_hdr.find(":")+1:from_hdr.find("@")] # remove all non digits from caller id caller_id = re.sub("\D", "", caller_id) # remove two digit country identification if present if not caller_id.startswith("0"): return caller_id[2:] return caller_id def caller_from_addressbook(caller_id): try: callers = subprocess.check_output([config.khard_exe, "phone", "--parsable", caller_id]).strip() except subprocess.CalledProcessError: return caller_id if len(callers.split("\n")) == 1: return callers.split("\t")[1] else: # the contact contains multiple phone numbers and we have to obtain the right phone label regexp = re.compile(caller_id, re.IGNORECASE) for entry in callers.split("\n"): if regexp.search(re.sub("\D", "", entry.split("\t")[0])) != None: return "%s (%s)" % (entry.split("\t")[1], entry.split("\t")[2]) return callers.split("\n")[0].split("\t")[1] def create_ringtone(caller_id): if os.path.exists(config.new_ringtone) == True: os.remove(config.new_ringtone) if config.language == "de": subprocess.call(["espeak", "-v", "de", "-s", "300", "-w", config.tmp_mono_file, caller_id]) else: subprocess.call(["espeak", "-v", "en-us", "-s", "300", "-w", config.tmp_mono_file, caller_id]) subprocess.call(["ffmpeg", "-i", config.tmp_mono_file, "-ar", "48000", "-ac", "2", "-y", config.tmp_file_stereo], stdout=open('/dev/null', 'w'), stderr=open('/dev/null', 'w')) subprocess.call(["sox", config.constant_ringtone_segment, config.tmp_file_stereo, config.new_ringtone]) # main part of the script if os.path.exists(config.constant_ringtone_segment) == False: print("The constant part of the ringtone file is missing. Create the sounds folder in your twinkle config and put a wav file in it") sys.exit(1) # pause the music playback # I use a MPD server for playing music so I pause it with the client MPC # You can disable that in the config.py file if config.stop_music: mpc_output = subprocess.check_output(["mpc", "-h", config.mpd_host, "-p", str(config.mpd_port), "status"]) if "playing" in mpc_output: subprocess.call(["mpc", "-h", config.mpd_host, "-p", str(config.mpd_port), "pause"]) music_tmp_file = open(config.mpd_lockfile, "w") music_tmp_file.close() if "SIP_FROM" in os.environ: from_hdr = os.environ["SIP_FROM"] # parse the caller ID of the string caller_id = get_caller_id(from_hdr) # look into the addressbook if caller_id != "": caller_id = caller_from_addressbook(caller_id) else: caller_id = "anonymous" # create the ringtone if config.language == "de": create_ringtone("Anruf von " + caller_id) else: create_ringtone("Call from " + caller_id) # save the caller id for later use with open(config.caller_id_filename, "w") as caller_id_file: caller_id_file.write(caller_id) # if the file creation was successful and the file exists, tell twinkle to use it as the ringtone # else do nothing and play the standard ringtone if os.path.exists(config.new_ringtone) == True: print("ringtone=" + config.new_ringtone) sys.exit() khard-0.17.0/misc/twinkle/scripts/incoming_call_ended.py000077500000000000000000000021211371517016500233030ustar00rootroot00000000000000#!/usr/bin/env python import os, subprocess, time, datetime import config # current date and time datetime = datetime.datetime.now() current_date = "%.2d.%.2d.%.4d" % (datetime.day, datetime.month, datetime.year) current_time = "%.2d:%.2d:%.2d" % (datetime.hour, datetime.minute, datetime.second) # if music was stopped, resume again if os.path.exists(config.mpd_lockfile) == True: os.remove(config.mpd_lockfile) subprocess.call(["mpc", "-h", config.mpd_host, "-p", str(config.mpd_port), "play"]) # try to get the caller name / id from the previously created temp file try: caller_id_file = open(config.caller_id_filename, "r") caller_id = caller_id_file.read().strip() caller_id_file.close() except: caller_id = "anonymous" if config.language == "de": message = "Anruf von %s am %s um %s\n" % (caller_id, current_date, current_time) else: message = "Call of %s in %s at %s\n" % (caller_id, current_date, current_time) try: os.remove(config.caller_id_filename) except: pass # log into file log = open(config.call_log_file, "a") log.write(message) log.close() khard-0.17.0/misc/twinkle/scripts/incoming_call_failed.py000077500000000000000000000021311371517016500234510ustar00rootroot00000000000000#!/usr/bin/env python import os, subprocess, time, datetime import config # current date and time datetime = datetime.datetime.now() current_date = "%.2d.%.2d.%.4d" % (datetime.day, datetime.month, datetime.year) current_time = "%.2d:%.2d:%.2d" % (datetime.hour, datetime.minute, datetime.second) # if music was stopped, resume again if os.path.exists(config.mpd_lockfile) == True: os.remove(config.mpd_lockfile) subprocess.call(["mpc", "-h", config.mpd_host, "-p", str(config.mpd_port), "play"]) # try to get the caller name / id from the previously created temp file try: with open(config.caller_id_filename, "r") as caller_id_file: caller_id = caller_id_file.read().strip() except: caller_id = "anonymous" if config.language == "de": message = "Anruf in Abwesenheit von %s am %s um %s\n" % (caller_id, current_date, current_time) else: message = "Call in absence of %s in %s at %s\n" % (caller_id, current_date, current_time) try: os.remove(config.caller_id_filename) except: pass # log into file with open(config.call_log_file, "a") as log: log.write(message) khard-0.17.0/misc/twinkle/sounds/000077500000000000000000000000001371517016500166215ustar00rootroot00000000000000khard-0.17.0/misc/twinkle/sounds/incoming_call.ogg000066400000000000000000005162561371517016500221340ustar00rootroot00000000000000OggSH|fcvorbisOggSH|\@vorbis Lavf57.56.101encoder=Lavc57.64.101 libvorbisvorbis%BCV@$s*FsBPBkBL2L[%s!B[(АU@AxA!%=X'=!9xiA!B!B!E9h'A08 8E9X'A B9!$5HP9,(05(0ԃ BI5gAxiA!$AHAFAX9A*9 4d((  @Qqɑɱ  YHHH$Y%Y%Y扪,˲,˲,2 HPQ Eq Yd8Xh爎4CSR,1\wD3$ R1s9R9sBT1ƜsB!1sB!RJƜsB!RsB!J)sB!B)B!J(B!BB!RB(!R!B)%R !RBRJ)BRJ)J %R))J!RJJ)TJ J)%RJ!J)8A'Ua BCVdR)-E"KFsPZr RͩR $1T2B BuL)-BrKsA3stG DfDBpxP S@bB.TX\]\@.!!A,pox N)*u \adhlptx||$%@DD4s !"#$ OggSH|]-A;       4qkh~OZV%45UUUEukCCC^'ZcC6;$('8jhʕc+FC eQ7eqaȔEQ$I,˲D\u7˲,;$-nYe"0u]ն뺮,2薤eng8>p|~o6->c3Z0>0nqW|d2/>6yuu 4JVC!9ۆ-z3x7E`]C0:[$ϯ~u3;}K=)fp7RFuVu n&"XM4 p+B@% Dέxyx>rhg .)Z)F+Sp}{aMG3kX ]iC bTDLj[8u@V*0Zhփjܼ  r|%_(l@{[ f _$p@k`îBwoiw\+<~ٰw8yߔP7bg(,s)" l[ut0oO! Fvo[sʃ+畃`<@`|Š( V`eXW QPI.[寓`Q*hut[j'Ij'Y߭f 0 #ZU`PBqG+.b rATzU>[_ g48h) bvE^UȯzAýX&Fp=B-#_nP a`P #Ы2 9G~Jا(pg2@L@y[?.KYC-#t(moiZ`DfZ M B[WUQ* /֦lgd];\ ^zX$z, y}//[\@_4AAA0 cC`dZ9y4˅̄2{O6И:AP<lHrR! t2"0QUP~/ֱ'= s H&*3fۺwo(y^J}fսPb M@(@n>9ًȰ!vb"]OuU\o\z:qM9'3 Ѐ`BG!P fdT\=QV?B<ىRLZn5h **e@e%"<xFvt%yk3+.SyYWY4mzEMS_ݯ\0(@z0p} =c`FK 3 , 8|_kʠYf(?% +4VS"jҝ~A `L` "AFqzBW3QWxPfd /ѵ*ՋDPX(OYcuv,qDUg+PZ\YR ;`2YŽo_ 4P8G!z493_o# F$= %WHn2nc@>LW nc;Mh蚫K̤7A~q` ._@NqLAmEY<]BHmPȥ?)fLAePT2חf Q9EöHεԆ^"Wb+tJ(sPDwoGnR9 `(`Ðz?w!UdBRXQԵ x(f?ASz.N2) 0x祔٤Hzj薁S;VTvUQѯˆd@Ae`OA\=04Z ]@7,fR/xj S62z2 ^HV?*8/7gCmŔ-s@Aw%0 Ns&&͡ G( 2K{Ð d):Z#EE ?#>Oݸh6>@4YMQPP Ū4MhT0"?ORtqh稪tVo $-}NJyET1!=5VsZ_= H w! CX@AЌpl^I UoO)NE! ( '0n a-`O;:Alnt& !E T{y2@TQM:jM~`㸿#]E~qyW|zԸ>/\xJƨ@=oy|Ss`Y ى{zj.,,ι0Th_fc}g<2>i\h>I@'C)fu! _iW݊<5ʼn55ZVn$P>4ĝs~@ `&B 0& AT_F_A.G=Fy4N[o!@QU@c v PAA_UU@TE"T6 a=]ke(` uAd@ Db[_ )SG*)|@^vUDӯPvɇD]lB5_WT@ `DPEda 6]讚<\݇J1j׬n/ >` SQCdzsv:54pm]>K})s__gl!.%T"aշz_1mh,0   ?Or͕a$ mg6* ՙt2? 6,⛦i~5L'3]/ 0.H`P, m3-` )F=,/v3z 2]ut^'/p,@ynDN$'I?Q{rY{9 wݗXԜSٓ_eRC᫜|Zo{*s&7 Ҿ;q e|Hb0;h?&Ӏa |>V|NFzS%C ٟh=9d>=FmC?nƜ`=~ct>&A(S43OܖxU.׆G]NsdJ7nU7&"{ p)׍׽iIy$Y2S֣d6QK q4]^iIVH+WdPbY~_ Ϛ q& $` sP'ȷfPu` +C4Q3Y^o)J ůTTX_{@2ŶIPxx^$zXHuQ]@_uߥۇu:?*]W^ R=Y4м:4ws# > kCY6 "~ɫ~[!Nul(SM17 {ފA&5ʼnEc[qPd6` f P0qP1B`p|h!>ZA0%uAUUy--4*r*" ,(" % J&g=ϺYVqYEn;<< %@c/?@?cpT@0@^4f".6z(>j|h -17 PW԰S=@<@{ ŗfRDFc!L{*"{}Yr@Ѥ<4Uh&2:JԉNkySwͪ<Ù 3-mjU)=" सʓC]Z]YFX0,Y5xW| 6 `BXF 8PE$0?,p,Wawjzضf([0 W,A1Ä\NUq3LQ]=4OBÂ* :ĜKEdG}i8#} a}fsxiC6q4-\MPA _"sȝ(KQJKٵ6. Ja5, ͯ4.AV;P [? ᡗ`ηigwٞΥ(↉:AN.@0 a0psOjYĻ[xPՏp'*&@AVR\Ph/cQݚϪ2 Cj(5U i145hsYgd4zԊ'UXq3Fۿkٮ3P{̂-!֪8|~i 2za 0~)Pltbl@m 9ƹk-u5>iw/Z6]~@kY^XcA|15g%z + `&B=R|r^p~@ ^4_/Wfz2^;9@7^**j4RI yT% R;zlTATZAy)E[pVWg(n#PerXԛV-@2 Rc@ @^YvٻOKyO i)nSp@~>3jif~ax0 L@_A^IXc@<S[`SC!@&rwd@%E ښyGE^s[ODFcE_LkQ,&<JǷs;D8׺t &PPWw{ec()+p*T 䃃 %@0 CWoF8P#x◜)}8QfD!2!;֘S QƊ##*%i^E @B 7N_c(QMQF=#5E\?k.cy˺}*ۨt&(EsVVRE=EYպ('/ # Γ1[w'Kw~Ē1 8Gp [v^(ԻnjV{K h%r5|iEDTUMuuϺ,G\U޶:O{,νTjT@Ug0P,G{n"23 vm籷Ut#p̀ ` (IF/  ͒]l6@6&~% p^t->SxRv/R;&5`t 4L`#!E``a54-$)pJdкc @ъ SY JXۡ?.X\]-45o8ʙ RE=WdݟsQPQ6J7ã؊nSJI.w?cj+ʍC >L%XCB0^6 O0p.@$ }Tzw*"BdPT@  +[N][X-/ )Dd]H"}yR5q@?VA>vkj$Y/fC(f#4:JUA._ O?hop<EL@~9o2V̌X)O7 @k2S(+bٌ>` 45D0S@0qejA=.L!TƮ;3G7o.Z $դOziPd}iM@UEW蓃I#4rڞÞ#)QZUNAjet/`o>zS#0q~@,;o: x4@ǥA/@'(@w 6, Mo^ķа4e@F/1ܿiF{="21n2n0?iPtArEh5Ϻl4}2ԃBAQg09VQTnx-LWy]j3p.aϭb B -Ֆi8[ړ avb+W@/ߤcl x Pd pՆ^ow5j+ơ =4RPT`B[ A+ z@eJt+O^X79`S{&(d)o_;K~jyvxm즷|[38bⴄ YC"Jn㾜ި>M#uL Ȱ@Ajl9 >W$h| 40-eEbWs#G㓡T֊`0xȎP>[N:l8l (Tj~(m@cN+:A::n0*bReT]OV}/̀asUU]ֱYGoL/v>{//oWU Viv o][m Fk J+ KS 0 Kö `(mbWW7As?Ht 4bHwC_ii)f]. CqQ37NEw60C@4!Ac#VAD>D  kb;J.ptpgR꪿ըEtVELͲյǂ0UyKF_C? }?zW/UtǶUau+v*@ T7$ZnU/?*ew> |]Y@_I0:Z U )PdI (~%? 2C)V]7#4ȄP3^e:237E`a N1!.ُ&YǠ|=As4mp$tUQ) DԾa"9sf'dH6^>|-Yk? ^2^j0ӑV z| 1Ӿ  N;c`j k-AO0I P!^)ƙoֆ;oLwQgC/7|o׆\>0X`l 8  L@( RԜRdV>T (Gfrg;@||4HYifZԛW>VD@JJ>7G{gw#՚Ngrƞ>n,T(Hk7 =Ubaߥ>6aB dKi xX]21"3cf#Jh\zun6 1R!}ۻ5R@o$ "`mUDUӭ z8@•( [}sVG"uZu{s#SF(@R9٠WX~ 5憨0_r.`\ 0 @f@%h0ҞP u{> V!Z1Lvt ڝ D] EYdK," UN`B 35tRF>(,9@n/SIFΛWP4{WJQ疩^ozI1"AL%eZgK"#]tH˒(~Q`ݤtc7;[O,ZXBO>/@ wSh@ڒ (b6;lp鲁If_!j|K1$I_\kʼn3ur9ƍ8 eaWu$ 01j " rKig|R3@Ҥw%s ??T> "rYK<(g%&&Џy;[sy; .`zmvM.4/'^dTIeT 0+/@K@?sx jl pIٗ$>G#jdy(-3#p)NJykkl@<@f AMO""jVEv]i x|o?g' 0]9_<z @j.UA ~|qzںsd۹ZW/ξh>Ѽl ߯=50tWcKͅF x)qyXR"`_߭ i<`J@ebO.o.(Պ_b귎8^( @uؔSR'H8z\`BTE`i>|j @l;G ;矟6G* ߗzФǘW<Uuz[],n T;it;ߟ 5I]$6Q+֚vӭӍVrO?~֥-{v4G7)1z lbM0ل @V`IMWkÿ<2ۘ9@>BVh0D,&7l7DdJx A!-\ךWQ-f <`_sUF/ЗEVfΘsnլw-jy@k:TD3b-dVs+H&RDO~=I|heK(XoW? Jj 0 Y6HX@habM x3ԋ8Dv QA֘O0{\\L0*^@! D^/2!hsQQȿ/T %@ɘ  -Or?-r5hbQ>W6XsJW|ť +ݲA]ʁ`02sI>ĄE; HO߯ _84 XOggSwH| R^          CC%^2Sth+DtzJ$jÉBFeP2N`L gutiWH".vzOkaLsR@u(gR3T@њ7TD@/NdTy+lW3m7#v_2yz.#(*O!##+Գ6Lb\XZ<ΟX |%7 PpG 0@BOɎf}26qA URTk.(LdBm p"Vcna\0@S pAiI1ZR!4CA A(cHՉ+w]'LW D@S ݛ{ŔVĢ5}Zc7*s _?ϕEE''>60Ox1RK,, `1@d>=ƿ]-P~kd{7PA6~r߰]#Ԇ^+ޟݔԍPʭ{`(2/jw@N09*@! xۼ28Xe}^DP|HqA9EN ֛7KܕE͋[qG5+OIIssN-Fh(^!Y2ɩ9I2{ΪsFP7 }+o@HZ)1Gx%C/pـPT`>2~9 ãaRu5Vq)`v)')'q(r<kL s9` @0ydBqZw(5}BiAq\IyN3J>`h/* ޚ 0b%{^h~|i^fjwA}^UgDWZd5;F P^:wq6˚ =] \+o lm0m086YvC|?F]SL5L\fN n q(`AuB (&aT` E^{~$X)$x%gt@yR&`~8IS t ޚ-:*=_N9ʜb͘@s:} [Բؕd( k6$0 v [>THP5?@_i@7l@s^)?h_] ,k0|cF|̭T+L Jf臲ijN3 @L`8ri d<9JPP9A"H4nֱ:=Gqj_x/9_`VЭV:Ê*zN\Iۦ6\~ƬT+ZuGTS:׽]PAnI~mb% ,h!}a`+/:#G@AS6ƃ[O[FZgcgΰLWD(Y<Ƹ1fWP=ALh ҪkCW_%>?.!49s5`쳝JTړ Kc ʓ_x:@<ݓ0fo~]Cx$?M$Bh&x?یVll/~k=s;|т DW0_n RЁ`.O"`lo~7f[@(ʻ'!#@ [_osNqḇQ@5Dw]R}͋wԕ4o͹mg|v `WP9LV%T!Ư(, F93RAca҂'k*-)oR @ @4 l0>ً_Q˿z== eYde~`#-~{zR5HdubǡwfsZ :)@DϺz͟kQ fDBhzPzCS"iT;~5+?FHUR @V;t(0ɨZ:tLd()p ^ [m (,(&? #b_mS2#I]ҧ7xd]QKo82`8)*c8 !N0 =00ܰM_4P$rΟJ:m1#s+rUlޮ+ +@ @ (vvUƯvC1 ʕrd2^ ~h~x9x o,/1 ~9НE؊٨ 5JF[H81E=Zw aA00 B@z'V>i^CU&O$AWCת: $?=WmrFՠS,Ŝcm%W_VSyv{o-JU-) @ԇTP(yT`ԕW:p"0X&@#rgl ZP2_ڀ-82``^)w9 ?՘jd\ȋmȀ3gs%DA0"4= xg<"iHd t oTg /A A7+dWS ғW$,/̖P{ҞTzq|߷9$L5&yJrgy۵r @.bq}^ q(. }H~AV'T (Eٻ?ntf3bbfO7o w% qA SZ?Ku`& j@0F[UaW:Q-[ ~5z"쏓OzJ`@f'"*}]xmꏝE֖V CRX5tX:ڏؤf9?>/bNJ\V_V9  CO]I_R@1,`aO ]?| 4CC@S l)^)٫?D]aۊH:i TET["N]f 0q x*dM;>9f §3 ydG٩֦@'ڐʜhOwytbqLW[qXPci^# X V(<_ is+tN`:Y`x"Ҫ=-rlo ٿAp I&,404>I@,~jy{aLCM1삧I|F]lö"5c/@y0 &L} '<0)BB)^&i(!լc3ql5{J\@@'k/fB@g4Ri׬,}2Wjw= k]{,F>U`ԇu+ z/ Ƽ T+~O,/>аxm.@ӆjc@>If٧,#S.?r)f}. 0j> aPk au'ELM nk,sq47^Fn &{!xAi͐Z-oy^8{uDu**]Q%RaZL[Yθ+F_?۱-ۮIsMvPz@O> /,7 7 `lP0^!b(pɌ_~hC\{NyWm`n:AZ&t ס"rz0C>H(!kϠ  ܼ8R@R=7hǾώKj4PP{&,9o쿮֤F}3v1er6zjJYLm/9*e(Qcj \`}cm. X΀2 Y4 BjJ]6q(>J 6٭UK_nq0nPN;T@; fLhC\@x1VL2=,;Wڷ[<3 @Sw:zz^ @ɦܙT(JkK=Crm]W٬mzwmnpAMܴ6F%CV.t % zd(  ,8 `ɯ-h;ݧ؟e*) C0 _ŗ 2_H0\~5 CKԵ8QŹ(9(|w١N0` T$A e{dĻv_>A*^@%NRaC@w$}WV\dRh于.9F-"Fyqd_/ϮBP2@*F-}׎M V@tNNڀ˲_~{[XrߋXj @xY@  @Ҫ@tN^ISW˟<+31{/+GfHey(]GHO,-ـ^a bL!W+J~l!6 "{Kd ą6gMέG$`Y@r!?ܴ`FZ0`2.5[x3h@k-`~sw0H>;,SgN\ eP^&D:AN.@0K!!$ %#PEUUC5:8<-Z>Co:.HJU]_z=ȊԽe{@8\NPy3.dͽ+[5`*~4RBض~ $u]up`9ƙf~fc^cg^" OF8f 5ČQT}6E<Afs":AY/ ?C~Sۄ,`ט9*%? ,T I0v**b@@م @v䖛$K8gVnɜ"7(K_VhN1h}p㈷D(tS`q1C{o )fMԇ6 YvC@o&7赟gx0KX3q=! h/I.0UDAS+@tWU@TFQ'8.F(9xiML/TT?~!~~1䲶Z83eP;X;QfsqNƾvůf:dT乌ɝd8fX|M[4(g/ _$10 2(^T>~cfÁwG˛ϐZq"RT+EAU 2~ijNaS *FBp|ůz.ݡ>yusm:T΢ srrݥz`=i8ޯ_z3dx_ B60UJ}dh]*(0߻D-`?wrS 8 6~(YXHɏ0`BVC_ ЊHW]S p^.jyKg׵xʴ/b d(W̮7 *%@1Ƶ# !ks.vHa "rвf2(@?8kB8ԯ0d@%EPL/`` q/$T+)^x!AdPp+7%.~/v6\ߺ9^ꕂRv]?[rb9  ` "Z*D, ({#@Q4 74UJ9pz&ݞsusUgq9}E+wҒƬ2R >@饾T9a4 S@҂g2s΁p, J` 0g _Kꐌ 2ohz@AEi~96]D䄮;>(QWKD*>)'FVeq PdOv 5i0QAERolO)6OFd:ccu}@; JyxC[Р򣰗kHkw3- V| cPx:&Bu԰qTDP1UpZc hEC$.qө _l",%BvT# lA\C^9c*al6P&)t؆fb$jVv}Ï|ѫ{>ZCf+ƍʠ` P'8 1r (VxkM-x@=Wn 4tJ@W/ACD-/UJT>ZkݻڎNSNLJÅz i1% &@ %Y5r,8wj AJR 4 9>A,2*Eأ" 8rG&յ䅷L z#@ f͡&p^*@H2I<(aep !`NP*4J$ DOxP4^mG2y~uLJ烥\^H* J20ڬZ?3~g SAF`^)U  XWh϶Gܘf(CʬhRDw?PxLEUcԍ&qDq|93/2I1XG]t** (KE?K!Ks Sk䌊}iY)}blP%oԮ=cvEq_ƨh5WumwTa@v4@7cZ>cI֙ PnWǟB13.o_cZS e݊jYxA@ 7*$WBrUx͢ӣ+dpͧq.mLE_06xBH>4b*tq(*2q!:+o<ݵx{ZmG_wb1t@$_Z])PbcT3_T@Ų.Oo_o0T@`l 9ٗ~2 6c)^i!" x9nDQDDj{0q Vu c2(؊ՎiYkxRE |Ү$;LX PPxPWDеMiQ~RS3 * R;99u"ʄH`Un7:@=~6" ݪ10,^gV lH`lYw _M z` (}>jo O^J6V8F~4yJtڌZ,HY=p]9`Bqѐ+`~=RYz.YkkćQ/@ ."){Wj J.* _3s棾w. cQ#Y9rb<j ;$9YCb{q{@7\l5 耵 4lǡsOāCX[pE( @0L3gu4O?2ڠ+wwG}+lG̨lbkS;('S}@t@NLBA@0F *>it1 1F~[J0ο7Vx]EO* #qSRٷT^E^ͥ\\f)jz*xPzJH+8htw3u {R/fq&~-=@7:A:P<4`B*Lb-RBʄsguЬA"j@B񹴉g5Kp9K# kC(`MUoiUMݜ. yRUѽZmik+r[ Gu`-,T0+aDլP/F?zy <^~06߱ JȒq45H ?lAU~AMfz::u|]>0!ԊC hv1QF7Xyo B 0 ` "k,uQC@{wxA24\jWm}y9YvFܪ`F#S2(^x9@/MBrH~~[3t`~,6 ba @s ~?}вç+x3`B NMߗbh`>%r}D,tNͽGSwaIu/ˋhVPPܠzߪK`$e!<)<2yO75@=62~ż!v;@bF2Pw*hW:< wsM4+׊/bx"ۆka:U$䪊X/ SaD*l3z+){*My ̫),ϭ\ w}yrS~qU_O9x"m08{ݎ>g_~{ujDb ltMT T~Q36 2c 8Bi2v-g6h; 3; T֘j#YyO&wSj&/rğ?>)|Jjdc^_a \ŸX%kVk%{/7YA̺DZ>oFh\tYם d>0(6ZgnyJ9ڲ @Ve z /#e3Ft *Om)R` ,@!`@,|bvϗL#(icA6-$`~WD&[#j&sQ.{sm]1{PAKbA(oNIj _ӏOv{@F ]) KJn<ܓwnOVt^q|L ~4IwRc#ၜV~͔ y@R߳H+,9!=O6zK%Dw+A(2o0pJj̐VX̀ 妇myTlWsMϓ ֔=exlo"vv61^U1vR@f,/n?-d]hFrQr|@Q$yXYf:5;X6{4Rw4^Έ\%W20 L`q)d_W܁AjוJ Aқp&^K6@n64}<?ZO2J+cb\P w ԄB@dguBk(^'٫Nq,B pI]eXˋ<#&/ZPD+ 6G;q1CbTD[˥#ʲy)%Tݹ9r}naكiӛӼ?oUC2.,_hCk*u -B:zخ( Mu;&x?.]k3h1V+ިD#ߓ/A* 9T+egq9޲n9qfbG>u`1tu>0OggS3H|s|; ҇yeIu{&ZeRsjnzĭũ)`v PU85܆%0pʗm jzIf!8*%_3Nwcf~Wy6_צ,`cNm׃ "u>ZFlZM^׌料u7 -5_!I$ȍ.ж'46]IIUU%j0'8:<%2KՂ=QNUme/xpO @)ZAfIQ&WݓY/yÐP'L_N}9xưKoVPh95@I恊%*U S״]ݑVK|@wFx3(c#a=b0gs\LW3v=8Q[m,s}o0}}s1W27[/:D[wrWHX_%\Ddh0}d\>a.{8ޛ^5`?XiAQn޶Qb Jks˄ξ+Toʐdu"{z" p#%TUU=;wvc%~LT<ȷOP^;_rZ7n+^0x8{z4ﮖ^4w3r.w;y n2J2`k;`}É3h4 ]>V--cllЛ&$BAy/UͰE"ä;%C ?SG(P\F 6oѮe:]>XLMޖm 2cpW(6qva t GQ0kPI*흣~!>e >HB!t.^W;![ݡ\satjPqx8?_q1=׫-llk)cF+?tUzg>7-tWg޹k hK<įRJT̪rjqu|>Hz Si&&ahn=b@3ݳY?1@i|fy]) U3LR yXXW#~9ܓͮx:&'LL:ʴ_ieOgjN,8!9+] Pt`;3}tjkF}/4tb/}M^f7RiHs{KOu>/h=]\zq9ecV 42otePBC%TU]v?WN:KװYo׷~Xׇ[̀m.ۚ Go2>0Z|v&\$5U @u{~TAl!dL['TCYMM&|vJ.@dHj.rO!ecPu69 ,$,eo-ɽ@\2ohe9ǝ5rh`)K4)Ռe"L5BMmaN8|i>ڌEYUR*M{+&pÇoﭳ{Q쓹(1Czn Ujjb58K|B@@ 0@Y55>{(9gXr } `D鷞UI"_K"g>K%DÝ:&awr%yHS?SJAs+zh \:96.hȜ֡:_bΒ Ii 2@mx̉K )-{77sc*zCkgZI~OK]>τ㯝_O{N|{ha۳3pΦo5p7rc>B(29~'Ibh1da(PE\94晪]\Uj[W-^q֮ŭهT7*_LvV<\^$n\cWI%c 6N=7%#ZlyoURS>TfdR6jUj%by_\՟wuߣ̘Yx{8uu ,OI%7a-|TY$/3o:+ȁiNxuLRlu҂Ԉ$k$mW?fNW݌ʂҰOY IK򀧇^߯J43`^MꌽcU8 GT%|t(a;1߇ܛ{h*;AZTO0\ft+yVBB~sȫ_ccp/5{)f"̥Kw {##%EONXq{.֭Q(2+vM6D2(\~zV@ԊaE_N= ׊/pF1GcD%PU!pk=+F.WQ}Joۏ:Y9<ç`s_n@JȞ`8fvӐady( Wc|Vi0@6 {ȦaPF3;:Um za^ജzvsI1G|ʀ F#L|8#*{`Z9l/ nA!~K*3w܀ó.o<%aGԊ/$X(UŪ*K_s2n27nF+DB>1?=~ruT2t w`@EES= d%JfL7p*0@0 SPlnUɡݾ4YPQ30dBb$y濩d/-҄y*X4נ"wED3r9U!h㚋\f?*Ȝ3lB~%86`mx폃fRp zoƛ_F1" 38<ˇoTVxVK:Tڽ/~/p̱xEiY2Y4S-}iDB$}Rr:|=!yJ Y{@|w/kEu]_ih$~:q!;Q!bhaf[}8dou} h! Ecvxkű奜۱`jC t\*}X'(tf`)Wזmذ8ypny0c)X}zWr@x!nI`MüʹKO] kLU߀m7Eݵͤ`ݻaDw\\4yTD p[Ua¾*rqy׵&d{u{֒lshA  1s>|E}XV [.Uv I֊Aهǯ~i HUUU~=s7o/J@zSqצmfxm}gaWg!'km4\UN86ԏx:IyhoIwa'&+ v+,ҭ*&8hcv@|Yy=pW_=6WGowj7) ٱ42**@~{1#w jҁnjBqP 'NUEU7rԀLfܫžOULބ~]5:v3?oҢj9~/w[?ݵ 8}OfL k}# _k9D1¹2ܒ4O8YZGo=F,C'lhl|Yg!)Ռ_gY^|SS 6-9.gE * ci&1{;U|/E/PKn>sær^eY+p!)CWFH;k$e2@D2$s35խ7߾sOڏ#:߆NG}Oa$6z$PD\F$KZ,z0\cɭ5<ֵ[n酩gPۘ\Zkl;BSSd=G .Oy4^(V%TUd<4MyNP&~}DiQ\罫3XODŽ WeN%Ɂ9ͷlwqp@au|VQ9恵_B4~R|Sԁ!y4sӿ$x/ߒX]HXm*N-VC8;CX04#~eBQ} zKPSd9c,?NSF Tj:ᮀpdWs}3U j_L6I34[@U?*Wt@oC@\SPTAX|k0ݐ]_E7ٔkV Orn}BI޹oMߍDLp4'Y+,Ѱ2w&q!"1` ޵|Չ< jƮ{y&;^/5Et*e#SUU@Q핿[h(f4ݮ|.uo=nx8fCVX[vi>_=%J(s#s{㧚M1LELn=( i(R_[~H~:ƪ?@6]Jnl[X9lz(C`Zm-l |'Ϯ|-45ůk?xw,x KM"n7L:JUUEjLgu}k# $a=cqOSdT\]'vA%o`B-edL8=aO:MȜGM" ܟcf6Lc,,h"j{lpQss|`:XBGc @c"A|;^-b~Q;(PSd뛿m4TUU!| ׏eҿ=|<`ǧq>;Vk$mf\x~Mlקfk ee:˔~493sX~`Zݦ|͸v] yPpjqK*FH,9m4m 0J6F_ ;1Zu+CE4ࡡ/3B ZS53i4Я0>On~p̫};SYD)}=$3/U_T1fLfU6p]zTyo6O\ _ U:ITT9'y4S+i3E)?;)Xl޹|'ZeMhfL`.d8f#w{&by$oj٣BGB, z00>NygNm,cօ{x_a;^yhhx^ffz0\2w][U7>PM,lg-5A wsxh~e{s+pkggzSCa0{߀gzy}{cm./μ5wۚ" ֏/p믟oyo~}6ugz`zpu8ޞ鷧p ^/p,/lBM ԤTX ^^pᚅY^kx/ / =Yx̺p؋؋ػ0 0 0 (/*J^z^p8p8z^zp8`OggSH|M?۶/OggSH|is/OggSgH|R\/-A;      J IJt*DHRV)aʔ)iS~ԏQd2/>6yuu 4JVC!9ۆ-z3x7E`]C0:[$ϯ~u3;}K=)fp7RFuVu n&"XM4 p+B@% Dέxyx>rhg .)Z)F+Sp}{aMG3kX ]iC bTDLj[8u@V*0Zhփjܼ  r|%_(l@{[ f _$p@k`îBwoiw\+<~ٰw8yߔP7bg(,s)" l[ut0oO! Fvo[sʃ+畃`<@`|Š( V`eXW QPI.[寓`Q*hut[j'Ij'Y߭f 0 #ZU`PBqG+.b rATzU>[_ g48h) bvE^UȯzAýX&Fp=B-#_nP a`P #Ы2 9G~Jا(pg2@L@y[?.KYC-#t(moiZ`DfZ M B[WUQ* /֦lgd];\ ^zX$z, y}//[\@_4AAA0 cC`dZ9y4˅̄2{O6И:AP<lHrR! t2"0QUP~/ֱ'= s H&*3fۺwo(y^J}fսPb M@(@n>9ًȰ!vb"]OuU\o\z:qM9'3 Ѐ`BG!P fdT\=QV?B<ىRLZn5h **e@e%"<xFvt%yk3+.SyYWY4mzEMS_ݯ\0(@z0p} =c`FK 3 , 8|_kʠYf(?% +4VS"jҝ~A `L` "AFqzBW3QWxPfd /ѵ*ՋDPX(OYcuv,qDUg+PZ\YR ;`2YŽo_ 4P8G!z493_o# F$= %WHn2nc@>LW nc;Mh蚫K̤7A~q` ._@NqLAmEY<]BHmPȥ?)fLAePT2חf Q9EöHεԆ^"Wb+tJ(sPDwoGnR9 `(`Ðz?w!UdBRXQԵ x(f?ASz.N2) 0x祔٤Hzj薁S;VTvUQѯˆd@Ae`OA\=04Z ]@7,fR/xj S62z2 ^HV?*8/7gCmŔ-s@Aw%0 Ns&&͡ G( 2K{Ð d):Z#EE ?#>Oݸh6>@4YMQPP Ū4MhT0"?ORtqh稪tVo $-}NJyET1!=5VsZ_= H w! CX@AЌpl^I UoO)NE! ( '0n a-`O;:Alnt& !E T{y2@TQM:jM~`㸿#]E~qyW|zԸ>/\xJƨ@=oy|Ss`Y ى{zj.,,ι0Th_fc}g<2>i\h>I@'C)fu! _iW݊<5ʼn55ZVn$P>4ĝs~@ `&B 0& AT_F_A.G=Fy4N[o!@QU@c v PAA_UU@TE"T6 a=]ke(` uAd@ Db[_ )SG*)|@^vUDӯPvɇD]lB5_WT@ `DPEda 6]讚<\݇J1j׬n/ >` SQCdzsv:54pm]>K})s__gl!.%T"aշz_1mh,0   ?Or͕a$ mg6* ՙt2? 6,⛦i~5L'3]/ 0.H`P, m3-` )F=,/v3z 2]ut^'/p,@ynDN$'I?Q{rY{9 wݗXԜSٓ_eRC᫜|Zo{*s&7 Ҿ;q e|Hb0;h?&Ӏa |>V|NFzS%C ٟh=9d>=FmC?nƜ`=~ct>&A(S43OܖxU.׆G]NsdJ7nU7&"{ p)׍׽iIy$Y2S֣d6QK q4]^iIVH+WdPbY~_ Ϛ q& $` sP'ȷfPu` +C4Q3Y^o)J ůTTX_{@2ŶIPxx^$zXHuQ]@_uߥۇu:?*]W^ R=Y4м:4ws# > kCY6 "~ɫ~[!Nul(SM17 {ފA&5ʼnEc[qPd6` f P0qP1B`p|h!>ZA0%uAUUy--4*r*" ,(" % J&g=ϺYVqYEn;<< %@c/?@?cpT@0@^4f".6z(>j|h -17 PW԰S=@<@{ ŗfRDFc!L{*"{}Yr@Ѥ<4Uh&2:JԉNkySwͪ<Ù 3-mjU)=" सʓC]Z]YFX0,Y5xW| 6 `BXF 8PE$0?,p,Wawjzضf([0 W,A1Ä\NUq3LQ]=4OBÂ* :ĜKEdG}i8#} a}fsxiC6q4-\MPA _"sȝ(KQJKٵ6. Ja5, ͯ4.AV;P [? ᡗ`ηigwٞΥ(↉:AN.@0 a0psOjYĻ[xPՏp'*&@AVR\Ph/cQݚϪ2 Cj(5U i145hsYgd4zԊ'UXq3Fۿkٮ3P{̂-!֪8|~i 2za 0~)Pltbl@m 9ƹk-u5>iw/Z6]~@kY^XcA|15g%z + `&B=R|r^p~@ ^4_/Wfz2^;9@7^**j4RI yT% R;zlTATZAy)E[pVWg(n#PerXԛV-@2 Rc@ @^YvٻOKyO i)nSp@~>3jif~ax0 L@_A^IXc@<S[`SC!@&rwd@%E ښyGE^s[ODFcE_LkQ,&<JǷs;D8׺t &PPWw{ec()+p*T 䃃 %@0 CWoF8P#x◜)}8QfD!2!;֘S QƊ##*%i^E @B 7N_c(QMQF=#5E\?k.cy˺}*ۨt&(EsVVRE=EYպ('/ # Γ1[w'Kw~Ē1 8Gp [v^(ԻnjV{K h%r5|iEDTUMuuϺ,G\U޶:O{,νTjT@Ug0P,G{n"23 vm籷Ut#p̀ ` (IF/  ͒]l6@6&~% p^t->SxRv/R;&5`t 4L`#!E``a54-$)pJdкc @ъ SY JXۡ?.X\]-45o8ʙ RE=WdݟsQPQ6J7ã؊nSJI.w?cj+ʍC >L%XCB0^6 O0p.@$ }Tzw*"BdPT@  +[N][X-/ )Dd]H"}yR5q@?VA>vkj$Y/fC(f#4:JUA._ O?hop<EL@~9o2V̌X)O7 @k2S(+bٌ>` 45D0S@0qejA=.L!TƮ;3G7o.Z $դOziPd}iM@UEW蓃I#4rڞÞ#)QZUNAjet/`o>zS#0q~@,;o: x4@ǥA/@'(@w 6, Mo^ķа4e@F/1ܿiF{="21n2n0?iPtArEh5Ϻl4}2ԃBAQg09VQTnx-LWy]j3p.aϭb B -Ֆi8[ړ avb+W@/ߤcl x Pd pՆ^ow5j+ơ =4RPT`B[ A+ z@eJt+O^X79`S{&(d)o_;K~jyvxm즷|[38bⴄ YC"Jn㾜ި>M#uL Ȱ@Ajl9 >W$h| 40-eEbWs#G㓡T֊`0xȎP>[N:l8l (Tj~(m@cN+:A::n0*bReT]OV}/̀asUU]ֱYGoL/v>{//oWU Viv o][m Fk J+ KS 0 Kö `(mbWW7As?Ht 4bHwC_ii)f]. CqQ37NEw60C@4!Ac#VAD>D  kb;J.ptpgR꪿ըEtVELͲյǂ0UyKF_C? }?zW/UtǶUau+v*@ T7$ZnU/?*ew> |]Y@_I0:Z U )PdI (~%? 2C)V]7#4ȄP3^e:237E`a N1!.ُ&YǠ|=As4mp$tUQ) DԾa"9sf'dH6^>|-Yk? ^2^j0ӑV z| 1Ӿ  N;c`j k-AO0I P!^)ƙoֆ;oLwQgC/7|o׆\>0X`l 8  L@( RԜRdV>T (Gfrg;@||4HYifZԛW>VD@JJ>7G{gw#՚Ngrƞ>n,T(Hk7 =Ubaߥ>6aB dKi xX]21"3cf#Jh\zun6 1R!}ۻ5R@o$ "`mUDUӭ z8@•( [}sVG"uZu{s#SF(@R9٠WX~ 5憨0_r.`\ 0 @f@%h0ҞP u{> V!Z1Lvt ڝ D] EYdK," UN`B 35tRF>(,9@n/SIFΛWP4{WJQ疩^ozI1"AL%eZgK"#]tH˒(~Q`ݤtc7;[O,ZXBO>/@ wSh@ڒ (b6;lp鲁If_!j|K1$I_\kʼn3ur9ƍ8 eaWu$ 01j " rKig|R3@Ҥw%s ??T> "rYK<(g%&&Џy;[sy; .`zmvM.4/'^dTIeT 0+/@K@?sx jl pIٗ$>G#jdy(-3#p)NJykkl@<@f AMO""jVEv]i x|o?g' 0]9_<z @j.UA ~|qzںsd۹ZW/ξh>Ѽl ߯=50tWcKͅF x)qyXR"`_߭ i<`J@ebO.o.(Պ_b귎8^( @uؔSR'H8z\`BTE`i>|j @l;G ;矟6G* ߗzФǘW<Uuz[],n T;it;ߟ 5I]$6Q+֚vӭӍVrO?~֥-{v4G7)1z lbM0ل @V`IMWkÿ<2ۘ9@>BVh0D,&7l7DdJx A!-\ךWQ-f <`_sUF/ЗEVfΘsnլw-jy@k:TD3b-dVs+H&RDO~=I|heK(XoW? Jj 0 Y6HX@habM x3ԋ8Dv QA֘O0{\\L0*^@! D^/2!hsQQȿ/T %@ɘ  -Or?-r5hbQ>W6XsJW|ť +ݲA]ʁ`02sI>ĄE; HO߯ _84 X CC%^2Sth+DtzJ$jÉBFeP2N`L gutiWH".vzOkaLsR@u(gR3T@њ7TD@/NdTy+lW3m7#v_2yz.#(*O!##+Գ6Lb\XZ<ΟX |%7 PpG 0@BOɎf}26qA URTk.(LdBm p"Vcna\0@S pAiI1ZR!4CA A(cHՉ+w]'LW D@S ݛ{ŔVĢ5}Zc7*s _?ϕEE''>60Ox1RK,, `1@d>=ƿ]-P~kd{7PA6~r߰]#Ԇ^+ޟݔԍPʭ{`(2/jw@N09*@! xۼ28Xe}^DP|HqA9EN ֛7KܕE͋[qG5+OIIssN-Fh(^!Y2ɩ9I2{ΪsFP7 }+o@HZ)1Gx%C/pـPT`>2~9 ãaRu5Vq)`v)')'q(r<kL s9` @0ydBqZw(5}BiAq\IyN3J>`h/* ޚ 0b%{^h~|i^fjwA}^UgDWZd5;F P^:wq6˚ =] \+o lm0m086YvC|?F]SL5L\fN n q(`AuB (&aT` E^{~$X)$x%gt@yR&`~8IS t ޚ-:*=_N9ʜb͘@s:} [Բؕd( k6$0 v [>THP5?@_i@7l@s^)?h_] ,k0|cF|̭T+L Jf臲ijN3 @L`8ri d<9JPP9A"H4nֱ:=Gqj_x/9_`VЭV:Ê*zN\Iۦ6\~ƬT+ZuGTS:׽]PAnI~mb% ,h!}a`+/:#G@AS6ƃ[O[FZgcgΰLWD(Y<Ƹ1fWP=ALh ҪkCW_%>?.!49s5`쳝JTړ Kc ʓ_x:@<ݓ0fo~]Cx$?M$Bh&x?یVll/~k=s;|т DW0_n RЁ`.O"`lo~7f[@(ʻ'!#@ [_osNqḇQ@5Dw]R}͋wԕ4o͹mg|v `WP9LV%T!Ư(, F93RAca҂'k*-)oR @ @4 l0>ً_Q˿z== eYde~`#-~{zR5HdubǡwfsZ :)@DϺz͟kQ fDBhzPzCS"iT;~5+?FHUR @V;t(0ɨZ:tLd()p ^ [m (,(&? #b_mS2#I]ҧ7xd]QKo82`8)*c8 !N0 =00ܰM_4P$rΟJ:m1#s+rUlޮ+ +@ @ (vvUƯvC1 ʕrd2^ ~h~x9x o,/1 ~9НE؊٨ 5JF[H81E=Zw aA00 B@z'V>i^CU&O$AWCת: $?=WmrFՠS,Ŝcm%W_VSyv{o-JU-) @ԇTP(yT`ԕW:p"0X&@#rgl ZP2_ڀ-82``^)w9 ?՘jd\ȋmȀ3gs%DA0"4= xg<"iHd t oTg /A A7+dWS ғW$,/̖P{ҞTzq|߷9$L5&yJrgy۵r @.bq}^ q(. }H~AV'T (Eٻ?ntf3bbfO7o w% qA SZ?Ku`& j@0F[UaW:Q-[ ~5z"쏓OzJ`@f'"*}]xmꏝE֖V CRX5tX:ڏؤf9?>/bNJ\V_V9  CO]I_R@1,`aO ]?| 4CC@S l)^)٫?D]aۊH:i TET["N]f 0q x*dM;>9f §3 ydG٩֦@'ڐʜhOwytbqLW[qXPci^# X V(<_ is+tN`:Y`x"Ҫ=-rlo ٿAp I&,404>I@,~jy{aLCM1삧I|F]lö"5c/@y0 &L} '<0)BB)^&i(!լc3ql5{J\@@'k/fB@g4Ri׬,}2Wjw= k]{,F>U`ԇu+ z/ Ƽ T+~O,/>аxm.@ӆjc@>If٧,#S.?r)f}. 0j> aPk au'ELM nk,sq47^Fn &{!xAi͐Z-oy^8{uDu**]Q%RaZL[Yθ+F_?۱-ۮIsMvPz@O> /,7 7 `lP0^!b(pɌ_~hC\{NyWm`n:AZ&t ס"rz0C>H(!kϠ  ܼ8R@R=7hǾώKj4PP{&,9o쿮֤F}3v1er6zjJYLm/9*e(Qcj \`}cm. X΀2 Y4 BjJ]6q(>J 6٭UK_nq0nPN;T@; fLhC\@x1VL2=,;Wڷ[<3 @Sw:zz^ @ɦܙT(JkK=Crm]W٬mzwmnpAMܴ6F%CV.t % zd(  ,8 `ɯ-h;ݧ؟e*) C0 _ŗ 2_H0\~5 CKԵ8QŹ(9(|w١N0` T$A e{dĻv_>A*^@%NRaC@w$}WV\dRh于.9F-"Fyqd_/ϮBP2@*F-}׎M V@tNNڀ˲_~{[XrߋXj @xY@  @Ҫ@tN^ISW˟<+31{/+GfHey(]GHO,-ـ^a bL!W+J~l!6 "{Kd ą6gMέG$`Y@r!?ܴ`FZ0`2.5[x3h@k-`~sw0H>;,SgN\ eP^&D:AN.@0K!!$ %#PEUUC5:8<-Z>Co:.HJU]_z=ȊԽe{@8\NPy3.dͽ+[5`*~4RBض~ $u]up`9ƙf~fc^cg^" OF8f 5ČQT}6E<Afs":AY/ ?C~Sۄ,`ט9*%? ,T I0v**b@@م @v䖛$K8gVnɜ"7(K_VhN1h}p㈷D(tS`q1C{o )fMԇ6 YvC@o&7赟gx0KX3q=! h/I.0UDAS+@tWU@TFQ'8.F(9xiML/TT?~!~~1䲶Z83eP;X;QfsqNƾvůf:dT乌ɝd8fX|M[4(g/ _$10 2(^T>~cfÁwG˛ϐZq"RT+EAU 2~ijNaS *FBp|ůz.ݡ>yusm:T΢ srrݥz`=i8ޯ_z3dx_ B60UJ}dh]*(0߻D-`?wrS 8 6~(YXHɏ0`BVC_ ЊHW]S p^.jyKg׵xʴ/b d(W̮7 *%@1Ƶ# !ks.vHa "rвf2(@?8kB8ԯ0d@%EPL/`` q/$T+)^x!AdPp+7%.~/v6\ߺ9^ꕂRv]?[rb9  ` "Z*D, ({#@Q4 74UJ9pz&ݞsusUgq9}E+wҒƬ2R >@饾T9a4 S@҂g2s΁p, J` 0g _Kꐌ 2ohz@AEi~96]D䄮;>(QWKD*>)'FVeq PdOv 5i0QAERolO)6OFd:ccu}@; JyxC[Р򣰗kHkw3- V| cPx:&Bu԰qTDP1UpZc hEC$.qө _l",%BvT# lA\C^9c*al6P&)t؆fb$jVv}Ï|ѫ{>ZCf+ƍʠ` P'8 1r (VxkM-x@=Wn 4tJ@W/ACD-/UJT>ZkݻڎNSNLJÅz i1% &@ %Y5r,8wj AJR 4 9>A,2*Eأ" 8rG&յ䅷L z#@ f͡&p^*@H2I<(aep !`NP*4J$ DOxP4^mG2y~uLJ烥\^H* J20ڬZ?3~g SAF`^)U  XWh϶Gܘf(CʬhRDw?PxLEUcԍ&qDq|93/2I1XG]t** (KE?K!Ks Sk䌊}iY)}blP%oԮ=cvEq_ƨh5WumwTa@v4@7cZ>cI֙ PnWǟB13.o_cZS e݊jYxA@ 7*$WBrUx͢ӣ+dpͧq.mLE_06xBH>4b*tq(*2q!:+o<ݵx{ZmG_wb1t@$_Z])PbcT3_T@Ų.Oo_o0T@`l 9ٗ~2 6c)^i!" x9nDQDDj{0q Vu c2(؊ՎiYkxRE |Ү$;LX PPxPWDеMiQ~RS3 * R;99u"ʄH`Un7:@=~6" ݪ10,^gV lH`lYw _M z` (}>jo O^J6V8F~4yJtڌZ,HY=p]9`Bqѐ+`~=RYz.YkkćQ/@ ."){Wj J.* _3s棾w. cQ#Y9rb<j ;$9YCb{q{@7\l5 耵 4lǡsOāCX[pE( @0L3gu4O?2ڠ+wwG}+lG̨lbkS;('S}@t@NLBA@0F *>it1 1F~[J0ο7Vx]EO* #qSRٷT^E^ͥ\\f)jz*xPzJH+8htw3u {R/fq&~-=@7:A:P<4`B*Lb-RBʄsguЬA"j@B񹴉g5Kp9K# kC(`MUoiUMݜ. yRUѽZmik+r[ Gu`-,T0+aDլP/F?zy <^~06߱ JȒq45H ?lAU~AMfz::u|]>0!ԊC hv1QF7Xyo B 0 ` "k,uQC@{wxA24\jWm}y9YvFܪ`F#S2(^x9@/MBrH~~[3t`~,6 ba @s ~?}вç+x3`B NMߗbh`>%r}D,tNͽGSwaIu/ˋhVPPܠzߪK`$e!<)<2yO75@=62~ż!v;@bF2Pw*hW:< wsM4+׊/bx"ۆka:U$䪊X/ SaD*l3z+){*My ̫),ϭ\ w}yrS~qU_O9x"m08{ݎ>g_~{ujDb ltMT T~Q36 2c 8Bi2v-g6h; 3; OggSH| }@Z>? ҇yeIT֘j#YyO&wSj&/rğ?>)|Jjdc^_a \ŸX%kVk%{/7YA̺DZ>oFh\tYם d>0(6ZgnyJ9ڲ @Ve z /#e3Ft *Om)R` ,@!`@,|bvϗL#(icA6-$`~WD&[#j&sQ.{sm]1{PAKbA(oNIj _ӏOv{@F ]) KJn<ܓwnOVt^q|L ~4IwRc#ၜV~͔ y@R߳H+,9!=O6zK%Dw+A(2o0pJj̐VX̀ 妇myTlWsMϓ ֔=exlo"vv61^U1vR@f,/n?-d]hFrQr|@Q$yXYf:5;X6{4Rw4^Έ\%W20 L`q)d_W܁AjוJ Aқp&^K6@n64}<?ZO2J+cb\P w ԄB@dguBk(^'٫Nq,B pI]eXˋ<#&/ZPD+ 6G;q1CbTD[˥#ʲy)%Tݹ9r}naكiӛӼ?oUC2.,_hCk*u -B:zخ( Mu;&x?.]k3h1V+ިD#ߓ/A* 9T+egq9޲n9qfbG>u`1tu>0u{&ZeRsjnzĭũ)`v PU85܆%0pʗm jzIf!8*%_3Nwcf~Wy6_צ,`cNm׃ "u>ZFlZM^׌料u7 -5_!I$ȍ.ж'46]IIUU%j0'8:<%2KՂ=QNUme/xpO @)ZAfIQ&WݓY/yÐP'L_N}9xưKoVPh95@I恊%*U S״]ݑVK|@wFx3(c#a=b0gs\LW3v=8Q[m,s}o0}}s1W27[/:D[wrWHX_%\Ddh0}d\>a.{8ޛ^5`?XiAQn޶Qb Jks˄ξ+Toʐdu"{z" p#%TUU=;wvc%~LT<ȷOP^;_rZ7n+^0x8{z4ﮖ^4w3r.w;y n2J2`k;`}É3h4 ]>V--cllЛ&$BAy/UͰE"ä;%C ?SG(P\F 6oѮe:]>XLMޖm 2cpW(6qva t GQ0kPI*흣~!>e >HB!t.^W;![ݡ\satjPqx8?_q1=׫-llk)cF+?tUzg>7-tWg޹k hK<įRJT̪rjqu|>Hz Si&&ahn=b@3ݳY?1@i|fy]) U3LR yXXW#~9ܓͮx:&'LL:ʴ_ieOgjN,8!9+] Pt`;3}tjkF}/4tb/}M^f7RiHs{KOu>/h=]\zq9ecV 42otePBC%TU]v?WN:KװYo׷~Xׇ[̀m.ۚ Go2>0Z|v&\$5U @u{~TAl!dL['TCYMM&|vJ.@dHj.rO!ecPu69 ,$,eo-ɽ@\2ohe9ǝ5rh`)K4)Ռe"L5BMmaN8|i>ڌEYUR*M{+&pÇoﭳ{Q쓹(1Czn Ujjb58K|B@@ 0@Y55>{(9gXr } `D鷞UI"_K"g>K%DÝ:&awr%yHS?SJAs+zh \:96.hȜ֡:_bΒ Ii 2@mx̉K )-{77sc*zCkgZI~OK]>τ㯝_O{N|{ha۳3pΦo5p7rc>B(29~'Ibh1da(PE\94晪]\Uj[W-^q֮ŭهT7*_LvV<\^$n\cWI%c 6N=7%#ZlyoURS>TfdR6jUj%by_\՟wuߣ̘Yx{8uu ,OI%7a-|TY$/3o:+ȁiNxuLRlu҂Ԉ$k$mW?fNW݌ʂҰOY IK򀧇^߯J43`^MꌽcU8 GT%|t(a;1߇ܛ{h*;AZTO0\ft+yVBB~sȫ_ccp/5{)f"̥Kw {##%EONXq{.֭Q(2+vM6D2(\~zV@ԊaE_N= ׊/pF1GcD%PU!pk=+F.WQ}Joۏ:Y9<ç`s_n@JȞ`8fvӐady( Wc|Vi0@6 {ȦaPF3;:Um za^ജzvsI1G|ʀ F#L|8#*{`Z9l/ nA!~K*3w܀ó.o<%aGԊ/$X(UŪ*K_s2n27nF+DB>1?=~ruT2t w`@EES= d%JfL7p*0@0 SPlnUɡݾ4YPQ30dBb$y濩d/-҄y*X4נ"wED3r9U!h㚋\f?*Ȝ3lB~%86`mx폃fRp zoƛ_F1" 38<ˇoTVxVK:Tڽ/~/p̱xEiY2Y4S-}iDB$}Rr:|=!yJ Y{@|w/kEu]_ih$~:q!;Q!bhaf[}8dou} h! Ecvxkű奜۱`jC t\*}X'(tf`)Wזmذ8ypny0c)X}zWr@x!nI`MüʹKO] kLU߀m7Eݵͤ`ݻaDw\\4yTD p[Ua¾*rqy׵&d{u{֒lshA  1s>|E}XV [.Uv I֊Aهǯ~i HUUU~=s7o/J@zSqצmfxm}gaWg!'km4\UN86ԏx:IyhoIwa'&+ v+,ҭ*&8hcv@|Yy=pW_=6WGowj7) ٱ42**@~{1#w jҁnjBqP 'NUEU7rԀLfܫžOULބ~]5:v3?oҢj9~/w[?ݵ 8}OfL k}# _k9D1¹2ܒ4O8YZGo=F,C'lhl|Yg!)Ռ_gY^|SS 6-9.gE * ci&1{;U|/E/PKn>sær^eY+p!)CWFH;k$e2@D2$s35խ7߾sOڏ#:߆NG}Oa$6z$PD\F$KZ,z0\cɭ5<ֵ[n酩gPۘ\Zkl;BSSd=G .Oy4^(V%TUd<4MyNP&~}DiQ\罫3XODŽ WeN%Ɂ9ͷlwqp@au|VQ9恵_B4~R|Sԁ!y4sӿ$x/ߒX]HXm*N-VC8;CX04#~eBQ} zKPSd9c,?NSF Tj:ᮀpdWs}3U j_L6I34[@U?*Wt@oC@\SPTAX|k0ݐ]_E7ٔkV Orn}BI޹oMߍDLp4'Y+,Ѱ2w&q!"1` ޵|Չ< jƮ{y&;^/5Et*e#SUU@Q핿[h(f4ݮ|.uo=nx8fCVX[vi>_=%J(s#s{㧚M1LELn=( i(R_[~H~:ƪ?@6]Jnl[X9lz(C`Zm-l |'Ϯ|-45ůk?xw,x KM"n7L:JUUEjLgu}k# $a=cqOSdT\]'vA%o`B-edL8=aO:MȜGM" ܟcf6Lc,,h"j{lpQss|`:XBGc @c"A|;^-b~Q;(PSd뛿m4TUU!| ׏eҿ=|<`ǧq>;Vk$mf\x~Mlקfk ee:˔~493sX~`Zݦ|͸v] yPpjqK*FH,9m4m 0J6F_ ;1Zu+CE4ࡡ/3B ZS53i4Я0>On~p̫};SYD)}=$3/U_T1fLfU6p]zTyo6O\ _ U:ITT9'y4S+i3E)?;)Xl޹|'ZeMhfL`.d8f#w{&by$oj٣BGB, z00>NygNm,cօ{x_a;^yhhx^ffz0\2w][U7>PM,lg-5A wsxh~e{s+pkggzSCa0{߀gzy}{cm./μ5wۚ" ֏/p믟oyo~}6ugz`zpu8ޞ鷧p ^/p,/lBM ԤTX ^^pᚅY^kx/ / =Yx̺p؋؋ػ0 0 0 (/*J^z^p8p8z^zp8`OggSH| 6yuu 4JVC!9ۆ-z3x7E`]C0:[$ϯ~u3;}K=)fp7RFuVu n&"XM4 p+B@% Dέxyx>rhg .)Z)F+Sp}{aMG3kX ]iC bTDLj[8u@V*0Zhփjܼ  r|%_(l@{[ f _$p@k`îBwoiw\+<~ٰw8yߔP7bg(,s)" l[ut0oO! Fvo[sʃ+畃`<@`|Š( V`eXW QPI.[寓`Q*hut[j'Ij'Y߭f 0 #ZU`PBqG+.b rATzU>[_ g48h) bvE^UȯzAýX&Fp=B-#_nP a`P #Ы2 9G~Jا(pg2@L@y[?.KYC-#t(moiZ`DfZ M B[WUQ* /֦lgd];\ ^zX$z, y}//[\@_4AAA0 cC`dZ9y4˅̄2{O6И:AP<lHrR! t2"0QUP~/ֱ'= s H&*3fۺwo(y^J}fսPb M@(@n>9ًȰ!vb"]OuU\o\z:qM9'3 Ѐ`BG!P fdT\=QV?B<ىRLZn5h **e@e%"<xFvt%yk3+.SyYWY4mzEMS_ݯ\0(@z0p} =c`FK 3 , 8|_kʠYf(?% +4VS"jҝ~A `L` "AFqzBW3QWxPfd /ѵ*ՋDPX(OYcuv,qDUg+PZ\YR ;`2YŽo_ 4P8G!z493_o# F$= %WHn2nc@>LW nc;Mh蚫K̤7A~q` ._@NqLAmEY<]BHmPȥ?)fLAePT2חf Q9EöHεԆ^"Wb+tJ(sPDwoGnR9 `(`Ðz?w!UdBRXQԵ x(f?ASz.N2) 0x祔٤Hzj薁S;VTvUQѯˆd@Ae`OA\=04Z ]@7,fR/xj S62z2 ^HV?*8/7gCmŔ-s@Aw%0 Ns&&͡ G( 2K{Ð d):Z#EE ?#>Oݸh6>@4YMQPP Ū4MhT0"?ORtqh稪tVo $-}NJyET1!=5VsZ_= H w! CX@AЌpl^I UoO)NE! ( '0n a-`O;:Alnt& !E T{y2@TQM:jM~`㸿#]E~qyW|zԸ>/\xJƨ@=oy|Ss`Y ى{zj.,,ι0Th_fc}g<2>i\h>I@'C)fu! _iW݊<5ʼn55ZVn$P>4ĝs~@ `&B 0& AT_F_A.G=Fy4N[o!@QU@c v PAA_UU@TE"T6 a=]ke(` uAd@ Db[_ )SG*)|@^vUDӯPvɇD]lB5_WT@ `DPEda 6]讚<\݇J1j׬n/ >` SQCdzsv:54pm]>K})s__gl!.%T"aշz_1mh,0   ?Or͕a$ mg6* ՙt2? 6,⛦i~5L'3]/ 0.H`P, m3-` )F=,/v3z 2]ut^'/p,@ynDN$'I?Q{rY{9 wݗXԜSٓ_eRC᫜|Zo{*s&7 Ҿ;q e|Hb0;h?&Ӏa |>V|NFzS%C ٟh=9d>=FmC?nƜ`=~ct>&A(S43OܖxU.׆G]NsdJ7nU7&"{ p)׍׽iIy$Y2S֣d6QK q4]^iIVH+WdPbY~_ Ϛ q& $` sP'ȷfPu` +C4Q3Y^o)J ůTTX_{@2ŶIPxx^$zXHuQ]@_uߥۇu:?*]W^ R=Y4м:4ws# > kCY6 "~ɫ~[!Nul(SM17 {ފA&5ʼnEc[qPd6` f P0qP1B`p|h!>ZA0%uAUUy--4*r*" ,(" % J&g=ϺYVqYEn;<< %@c/?@?cpT@0@^4f".6z(>j|h -17 PW԰S=@<@{ ŗfRDFc!L{*"{}Yr@Ѥ<4Uh&2:JԉNkySwͪ<Ù 3-mjU)=" सʓC]Z]YFX0,Y5xW| 6 `BXF 8PE$0?,p,Wawjzضf([0 W,A1Ä\NUq3LQ]=4OBÂ* :ĜKEdG}i8#} a}fsxiC6q4-\MPA _"sȝ(KQJKٵ6. Ja5, ͯ4.AV;P [? ᡗ`ηigwٞΥ(↉:AN.@0 a0psOjYĻ[xPՏp'*&@AVR\Ph/cQݚϪ2 Cj(5U i145hsYgd4zԊ'UXq3Fۿkٮ3P{̂-!֪8|~i 2za 0~)Pltbl@m 9ƹk-u5>iw/Z6]~@kY^XcA|15g%z + `&B=R|r^p~@ ^4_/Wfz2^;9@7^**j4RI yT% R;zlTATZAy)E[pVWg(n#PerXԛV-@2 Rc@ @^YvٻOKyO i)nSp@~>3jif~ax0 L@_A^IXc@<S[`SC!@&rwd@%E ښyGE^s[ODFcE_LkQ,&<JǷs;D8׺t &PPWw{ec()+p*T 䃃 %@0 CWoF8P#x◜)}8QfD!2!;֘S QƊ##*%i^E @B 7N_c(QMQF=#5E\?k.cy˺}*ۨt&(EsVVRE=EYպ('/ # Γ1[w'Kw~Ē1 8Gp [v^(ԻnjV{K h%r5|iEDTUMuuϺ,G\U޶:O{,νTjT@Ug0P,G{n"23 vm籷Ut#p̀ ` (IF/  ͒]l6@6&~% p^t->SxRv/R;&5`t 4L`#!E``a54-$)pJdкc @ъ SY JXۡ?.X\]-45o8ʙ RE=WdݟsQPQ6J7ã؊nSJI.w?cj+ʍC >L%XCB0^6 O0p.@$ }Tzw*"BdPT@  +[N][X-/ )Dd]H"}yR5q@?VA>vkj$Y/fC(f#4:JUA._ O?hop<EL@~9o2V̌X)O7 @k2S(+bٌ>` 45D0S@0qejA=.L!TƮ;3G7o.Z $դOziPd}iM@UEW蓃I#4rڞÞ#)QZUNAjet/`o>zS#0q~@,;o: x4@ǥA/@'(@w 6, Mo^ķа4e@F/1ܿiF{="21n2n0?iPtArEh5Ϻl4}2ԃBAQg09VQTnx-LWy]j3p.aϭb B -Ֆi8[ړ avb+W@/ߤcl x Pd pՆ^ow5j+ơ =4RPT`B[ A+ z@eJt+O^X79`S{&(d)o_;K~jyvxm즷|[38bⴄ YC"Jn㾜ި>M#uL Ȱ@Ajl9 >W$h| 40-eEbWs#G㓡T֊`0xȎP>[N:l8l (Tj~(m@cN+:A::n0*bReT]OV}/̀asUU]ֱYGoL/v>{//oWU Viv o][m Fk J+ KS 0 Kö `(mbWW7As?Ht 4bHwC_ii)f]. CqQ37NEw60C@4!Ac#VAD>D  kb;J.ptpgR꪿ըEtVELͲյǂ0UyKF_C? }?zW/UtǶUau+v*@ T7$ZnU/?*ew> |]Y@_I0:Z U )PdI (~%? 2C)V]7#4ȄP3^e:237E`a N1!.ُ&YǠ|=As4mp$tUQ) DԾa"9sf'dH6^>|-Yk? ^2^j0ӑV z| 1Ӿ  N;c`j k-AO0I P!^)ƙoֆ;oLwQgC/7|o׆\>0X`l 8  L@( RԜRdV>T (Gfrg;@||4HYifZԛW>VD@JJ>7G{gw#՚Ngrƞ>n,T(Hk7 =Ubaߥ>6aB dKi xX]21"3cf#Jh\zun6 1R!}ۻ5R@o$ "`mUDUӭ z8@•( [}sVG"uZu{s#SF(@R9٠WX~ 5憨0_r.`\ 0 @f@%h0ҞP u{OggSH| e^         > V!Z1Lvt ڝ D] EYdK," UN`B 35tRF>(,9@n/SIFΛWP4{WJQ疩^ozI1"AL%eZgK"#]tH˒(~Q`ݤtc7;[O,ZXBO>/@ wSh@ڒ (b6;lp鲁If_!j|K1$I_\kʼn3ur9ƍ8 eaWu$ 01j " rKig|R3@Ҥw%s ??T> "rYK<(g%&&Џy;[sy; .`zmvM.4/'^dTIeT 0+/@K@?sx jl pIٗ$>G#jdy(-3#p)NJykkl@<@f AMO""jVEv]i x|o?g' 0]9_<z @j.UA ~|qzںsd۹ZW/ξh>Ѽl ߯=50tWcKͅF x)qyXR"`_߭ i<`J@ebO.o.(Պ_b귎8^( @uؔSR'H8z\`BTE`i>|j @l;G ;矟6G* ߗzФǘW<Uuz[],n T;it;ߟ 5I]$6Q+֚vӭӍVrO?~֥-{v4G7)1z lbM0ل @V`IMWkÿ<2ۘ9@>BVh0D,&7l7DdJx A!-\ךWQ-f <`_sUF/ЗEVfΘsnլw-jy@k:TD3b-dVs+H&RDO~=I|heK(XoW? Jj 0 Y6HX@habM x3ԋ8Dv QA֘O0{\\L0*^@! D^/2!hsQQȿ/T %@ɘ  -Or?-r5hbQ>W6XsJW|ť +ݲA]ʁ`02sI>ĄE; HO߯ _84 X CC%^2Sth+DtzJ$jÉBFeP2N`L gutiWH".vzOkaLsR@u(gR3T@њ7TD@/NdTy+lW3m7#v_2yz.#(*O!##+Գ6Lb\XZ<ΟX |%7 PpG 0@BOɎf}26qA URTk.(LdBm p"Vcna\0@S pAiI1ZR!4CA A(cHՉ+w]'LW D@S ݛ{ŔVĢ5}Zc7*s _?ϕEE''>60Ox1RK,, `1@d>=ƿ]-P~kd{7PA6~r߰]#Ԇ^+ޟݔԍPʭ{`(2/jw@N09*@! xۼ28Xe}^DP|HqA9EN ֛7KܕE͋[qG5+OIIssN-Fh(^!Y2ɩ9I2{ΪsFP7 }+o@HZ)1Gx%C/pـPT`>2~9 ãaRu5Vq)`v)')'q(r<kL s9` @0ydBqZw(5}BiAq\IyN3J>`h/* ޚ 0b%{^h~|i^fjwA}^UgDWZd5;F P^:wq6˚ =] \+o lm0m086YvC|?F]SL5L\fN n q(`AuB (&aT` E^{~$X)$x%gt@yR&`~8IS t ޚ-:*=_N9ʜb͘@s:} [Բؕd( k6$0 v [>THP5?@_i@7l@s^)?h_] ,k0|cF|̭T+L Jf臲ijN3 @L`8ri d<9JPP9A"H4nֱ:=Gqj_x/9_`VЭV:Ê*zN\Iۦ6\~ƬT+ZuGTS:׽]PAnI~mb% ,h!}a`+/:#G@AS6ƃ[O[FZgcgΰLWD(Y<Ƹ1fWP=ALh ҪkCW_%>?.!49s5`쳝JTړ Kc ʓ_x:@<ݓ0fo~]Cx$?M$Bh&x?یVll/~k=s;|т DW0_n RЁ`.O"`lo~7f[@(ʻ'!#@ [_osNqḇQ@5Dw]R}͋wԕ4o͹mg|v `WP9LV%T!Ư(, F93RAca҂'k*-)oR @ @4 l0>ً_Q˿z== eYde~`#-~{zR5HdubǡwfsZ :)@DϺz͟kQ fDBhzPzCS"iT;~5+?FHUR @V;t(0ɨZ:tLd()p ^ [m (,(&? #b_mS2#I]ҧ7xd]QKo82`8)*c8 !N0 =00ܰM_4P$rΟJ:m1#s+rUlޮ+ +@ @ (vvUƯvC1 ʕrd2^ ~h~x9x o,/1 ~9НE؊٨ 5JF[H81E=Zw aA00 B@z'V>i^CU&O$AWCת: $?=WmrFՠS,Ŝcm%W_VSyv{o-JU-) @ԇTP(yT`ԕW:p"0X&@#rgl ZP2_ڀ-82``^)w9 ?՘jd\ȋmȀ3gs%DA0"4= xg<"iHd t oTg /A A7+dWS ғW$,/̖P{ҞTzq|߷9$L5&yJrgy۵r @.bq}^ q(. }H~AV'T (Eٻ?ntf3bbfO7o w% qA SZ?Ku`& j@0F[UaW:Q-[ ~5z"쏓OzJ`@f'"*}]xmꏝE֖V CRX5tX:ڏؤf9?>/bNJ\V_V9  CO]I_R@1,`aO ]?| 4CC@S l)^)٫?D]aۊH:i TET["N]f 0q x*dM;>9f §3 ydG٩֦@'ڐʜhOwytbqLW[qXPci^# X V(<_ is+tN`:Y`x"Ҫ=-rlo ٿAp I&,404>I@,~jy{aLCM1삧I|F]lö"5c/@y0 &L} '<0)BB)^&i(!լc3ql5{J\@@'k/fB@g4Ri׬,}2Wjw= k]{,F>U`ԇu+ z/ Ƽ T+~O,/>аxm.@ӆjc@>If٧,#S.?r)f}. 0j> aPk au'ELM nk,sq47^Fn &{!xAi͐Z-oy^8{uDu**]Q%RaZL[Yθ+F_?۱-ۮIsMvPz@O> /,7 7 `lP0^!b(pɌ_~hC\{NyWm`n:AZ&t ס"rz0C>H(!kϠ  ܼ8R@R=7hǾώKj4PP{&,9o쿮֤F}3v1er6zjJYLm/9*e(Qcj \`}cm. X΀2 Y4 BjJ]6q(>J 6٭UK_nq0nPN;T@; fLhC\@x1VL2=,;Wڷ[<3 @Sw:zz^ @ɦܙT(JkK=Crm]W٬mzwmnpAMܴ6F%CV.t % zd(  ,8 `ɯ-h;ݧ؟e*) C0 _ŗ 2_H0\~5 CKԵ8QŹ(9(|w١N0` T$A e{dĻv_>A*^@%NRaC@w$}WV\dRh于.9F-"Fyqd_/ϮBP2@*F-}׎M V@tNNڀ˲_~{[XrߋXj @xY@  @Ҫ@tN^ISW˟<+31{/+GfHey(]GHO,-ـ^a bL!W+J~l!6 "{Kd ą6gMέG$`Y@r!?ܴ`FZ0`2.5[x3h@k-`~sw0H>;,SgN\ eP^&D:AN.@0K!!$ %#PEUUC5:8<-Z>Co:.HJU]_z=ȊԽe{@8\NPy3.dͽ+[5`*~4RBض~ $u]up`9ƙf~fc^cg^" OF8f 5ČQT}6E<Afs":AY/ ?C~Sۄ,`ט9*%? ,T I0v**b@@م @v䖛$K8gVnɜ"7(K_VhN1h}p㈷D(tS`q1C{o )fMԇ6 YvC@o&7赟gx0KX3q=! h/I.0UDAS+@tWU@TFQ'8.F(9xiML/TT?~!~~1䲶Z83eP;X;QfsqNƾvůf:dT乌ɝd8fX|M[4(g/ _$10 2(^T>~cfÁwG˛ϐZq"RT+EAU 2~ijNaS *FBp|ůz.ݡ>yusm:T΢ srrݥz`=i8ޯ_z3dx_ B60UJ}dh]*(0߻D-`?wrS 8 6~(YXHɏ0`BVC_ ЊHW]S p^.jyKg׵xʴ/b d(W̮7 *%@1Ƶ# !ks.vHa "rвf2(@?8kB8ԯ0d@%EPL/`` q/$T+)^x!AdPp+7%.~/v6\ߺ9^ꕂRv]?[rb9  ` "Z*D, ({#@Q4 74UJ9pz&ݞsusUgq9}E+wҒƬ2R >@饾T9a4 S@҂g2s΁p, J` 0g _Kꐌ 2ohz@AEi~96]D䄮;>(QWKD*>)'FVeq PdOv 5i0QAERolO)6OFd:ccu}@; JyxC[Р򣰗kHkw3- V| cPx:&Bu԰qTDP1UpZc hEC$.qө _l",%BvT# lA\C^9c*al6P&)t؆fb$jVv}Ï|ѫ{>ZCf+ƍʠ` P'8 1r (VxkM-x@=Wn 4tJ@W/ACD-/UJT>ZkݻڎNSNLJÅz i1% &@ %Y5r,8wj AJR 4 9>A,2*Eأ" 8rG&յ䅷L z#@ f͡&p^*@H2I<(aep !`NP*4J$ DOxP4^mG2y~uLJ烥\^H* J20ڬZ?3~g SAF`^)U  XWh϶Gܘf(CʬhRDw?PxLEUcԍ&qDq|93/2I1XG]t** (KE?K!Ks Sk䌊}iY)}blP%oԮ=cvEq_ƨh5WumwTa@v4@7cZ>cI֙ PnWǟB13.o_cZS e݊jYxA@ 7*$WBrUx͢ӣ+dpͧq.mLE_06xBH>4b*tq(*2q!:+o<ݵx{ZmG_wb1t@$_Z])PbcT3_T@Ų.Oo_o0T@`l 9ٗ~2 6c)^i!" x9nDQDDj{0q Vu c2(؊ՎiYkxRE |Ү$;LX PPxPWDеMiQ~RS3 * R;99u"ʄH`Un7:@=~6" ݪ10,^gV lH`lYw _M z` (}>jo O^J6V8F~4yJtڌZ,HY=p]9`Bqѐ+`~=RYz.YkkćQ/@ ."){Wj J.* _3s棾w. cQ#Y9rb<j ;$9YCb{q{@7\l5 耵 4lǡsOāCX[pE( @0L3gu4O?2ڠ+wwG}+lG̨lbkS;('S}@t@NLBA@0F *>it1 1F~[J0ο7Vx]EO* #qSRٷT^E^ͥ\\f)jz*xPzJH+8htw3u {R/fq&~-=@7:A:P<4`B*Lb-RBʄsguЬA"j@B񹴉g5Kp9K# kC(`MUoiUMݜ. yRUѽZmik+r[ Gu`-,T0+aDլP/F?zy <^~06߱ JȒq45H ?lAU~AMfz::u|]>0!ԊC hv1QF7Xyo B 0 ` "k,uQC@{wxA24\jWm}y9YvFܪ`F#S2(^x9@/MBrH~~[3t`~,6 ba @s ~?}вç+x3`B NMߗbh`>%r}D,tNͽGSwaIu/ˋhVPPܠzߪK`$e!<)<2yO75@=62~ż!v;@bF2Pw*hW:< wsM4+׊/bx"ۆka:U$䪊X/ SaD*l3z+){*My ̫),ϭ\ w}yrS~qU_O9x"m08{ݎ>g_~{ujDb ltMT T~Q36 2c 8Bi2v-g6h; 3; T֘j#YyO&wSj&/rğ?>)|Jjdc^_a \ŸX%kVk%{/7YA̺DZ>oFh\tYם d>0(6ZgnyJ9ڲ @Ve z /#e3Ft *Om)R` ,@!`@,|bvϗL#(icA6-$`~WD&[#j&sQ.{sm]1{PAKbA(oNIj _ӏOv{@F ]) KJn<ܓwnOVt^q|L ~4IwRc#ၜV~͔ y@R߳H+,9!=O6zK%Dw+A(2o0pJj̐VX̀ 妇myTlWsMϓ ֔=exlo"vv61^U1vR@f,/n?-d]hFrQr|@Q$yXYf:5;X6{4Rw4^Έ\%W20 L`q)d_W܁AjוJ Aқp&^K6@n64}<?ZO2J+cb\P w ԄB@dguBk(^'٫Nq,B pI]eXˋ<#&/ZPD+ 6G;q1CbTD[˥#ʲy)%Tݹ9r}naكiӛӼ?oUC2.,_hCk*u -B:zخ( Mu;&x?.]k3h1V+ިD#ߓ/A* 9T+egq9޲n9qfbG>u`1tu>0u{&ZeRsjnzĭũ)`v PU85܆%0pʗm jzIf!8*%_3Nwcf~Wy6_צ,`cNm׃ "u>ZFlZM^׌料u7 -5_!I$ȍ.ж'46]IIUU%j0'8:<%2KՂ=QNUme/xpO @)ZAfIQ&WݓY/yÐP'L_N}9xưKoVPh95@I恊%*U S״]ݑVK|@wFx3(c#a=b0gs\LW3v=8Q[m,s}o0}}s1W27[/:D[wrWHX_%\Ddh0}d\>a.{8ޛ^5`?XiAQn޶Qb Jks˄ξ+Toʐdu"{z" p#%TUU=;wvc%~LT<ȷOP^;_rZ7n+^0x8{z4ﮖ^4w3r.w;y n2J2`k;`}É3h4 ]>V--cllЛ&$BAy/UͰE"ä;%C ?SG(P\F 6oѮe:]>XLMޖm 2cpW(6qva t GQ0kPI*흣~!>e >HB!t.^W;![ݡ\satjPqx8?_q1=׫-llk)cF+?tUzg>7-tWg޹k hK<įRJT̪rjqu|>Hz Si&&ahn=b@3ݳY?1@i|fy]) U3LR yXXW#~9ܓͮx:&'LL:ʴ_ieOgjN,8!9+] Pt`;3}tjkF}/4tb/}M^f7RiHs{KOu>/h=]\zq9ecV 42otePBC%TU]v?WN:KװYo׷~Xׇ[̀m.ۚ Go2>0Z|v&\$5U @u{~TAl!dL['TCYMM&|vJ.@dHj.rO!ecPu69 ,$,eo-ɽ@\2ohe9ǝ5rh`)K4)Ռe"L5BMmaN8|i>ڌEYUR*M{+&pÇoﭳ{Q쓹(1Czn Ujjb58K|B@@ 0@Y55>{(9gXr } `D鷞UI"_K"g>K%DÝ:&awr%yHS?SJAs+zh \:96.hȜ֡:_bΒ Ii 2@mx̉K )-{77sc*zCkgZI~OK]>τ㯝_O{N|{ha۳3pΦo5p7rc>B(29~'Ibh1da(PE\94晪]\Uj[W-^q֮ŭهT7*_LvV<\^$n\cWI%c 6N=7%#ZlyoURS>TfdR6jUj%by_\՟wuߣ̘Yx{8uu ,OI%7a-|TY$/3o:+ȁiNxuLRlu҂Ԉ$k$mW?fNW݌ʂҰOY IK򀧇^߯J43`^MꌽcU8 GT%|t(a;1߇ܛ{h*;AZTO0\ft+yVBB~sȫ_ccp/5{)f"̥Kw {##%EONXq{.֭Q(2+vM6D2(\~zV@ԊaE_N= ׊/pF1GcD%PU!pk=+F.WQ}Joۏ:Y9<ç`s_n@JȞ`8fvӐady( Wc|Vi0@6 {ȦaPF3;:Um za^ജzvsI1G|ʀ F#L|8#*{`Z9l/ nA!~K*3w܀ó.o<%aGԊ/$X(UŪ*K_s2n27nF+DB>1?=~ruT2t w`@EES= d%JfL7p*0@0 SPlnUɡݾ4YPQ30dBb$y濩d/-҄y*X4נ"wED3r9U!h㚋\f?*Ȝ3lB~%86`mx폃fRp zoƛ_F1" 38<ˇoTVxVK:Tڽ/~/p̱xEiY2Y4S-}iDB$}Rr:|=!yJ Y{@|w/kEu]_ih$~:q!;Q!bhaf[}8dou} h! Ecvxkű奜۱`jC t\*}X'(tf`)Wזmذ8ypny0c)X}zWr@x!nI`MüʹKO] kLU߀m7Eݵͤ`ݻaDw\\4yTD p[Ua¾*rqy׵&d{u{֒lshA  1s>|E}XV [.Uv I֊Aهǯ~i HUUU~=s7o/J@zSqצmfxm}gaWg!'km4\UN86ԏx:IyhoIwa'&+ v+,ҭ*&8hcv@|Yy=pW_=6WGowj7) ٱ42**@~{1#w jҁnjBqP 'NUEU7rԀLfܫžOULބ~]5:v3?oҢj9~/w[?ݵ 8}OfL k}# _k9D1¹2ܒ4O8YZGo=F,C'lhl|Yg!)Ռ_gY^|SS 6-9.gE * ci&1{;U|/E/PKn>sær^eY+p!)CWFH;k$e2@D2$s35խ7߾sOڏ#:߆NG}Oa$6z$PD\F$KZ,z0\cɭ5<ֵ[n酩gPۘ\Zkl;BSSd=G .Oy4^(V%TUd<4MyNP&~}DiQ\罫3XODŽ WeN%Ɂ9ͷlwqp@au|VQ9恵_B4~R|Sԁ!y4sӿ$x/ߒX]HXm*N-VC8;CX04#~eBQ} zKPSd9c,?NSF Tj:ᮀpdWs}3U j_L6I34[@U?*Wt@oC@\SPTAX|k0ݐ]_E7ٔkV Orn}BI޹oMߍDLp4'Y+,Ѱ2w&q!"1` ޵|Չ< jƮ{y&;^/5Et*e#SUU@Q핿[h(f4ݮ|.uo=nx8fCVX[vi>_=%J(s#s{㧚M1LELn=( i(R_[~H~:ƪ?@6]Jnl[X9lz(C`Zm-l |'Ϯ|-45ůk?xw,x KM"n7L:JUUEjLgu}k# $a=cqOSdT\]'vA%o`B-edL8=aO:MȜGM" ܟcf6Lc,,h"j{lpQss|`:XBGc @c"A|;^-b~Q;(PSd뛿m4TUU!| ׏eҿ=|<`ǧq>;Vk$mf\x~Mlקfk ee:˔~493sX~`Zݦ|͸v] yPpjqK*FH,9m4m 0J6F_ ;1Zu+CE4ࡡ/3B ZS53i4Я0>On~p̫};SYD)}=$3/U_T1fLfU6p]zTyo6O\ _ U:ITT9'y4S+i3E)?;)Xl޹|'ZeMhfL`.d8f#w{&by$oj٣BGB, z00>NygNm,cօ{x_a;^yhhx^ffz0\2w][U7>PM,lg-5A wsxh~e{s+pkggzSCa0{߀gzy}{cm./μ5wۚ" ֏/p믟oyo~}6ugz`zpu8ޞ鷧p ^/p,/lBM ԤTX ^^pᚅY^kx/ / =Yx̺p؋؋ػ0 0 0 (/*J^z^p8p8z^zp8`OggSG H|P3/OggS H|e/OggS H|_aT/-A;     J IJt*DHRV)aʔ)iS~ԏQd2/>6yuu 4JVC!9ۆ-z3x7E`]C0:[$ϯ~u3;}K=)fp7RFuVu n&"XM4 p+B@% Dέxyx>rhg .)Z)F+Sp}{aMG3kX ]iC bTDLj[8u@V*0Zhփjܼ  r|%_(l@{[ f _$p@k`îBwoiw\+<~ٰw8yߔP7bg(,s)" l[ut0oO! Fvo[sʃ+畃`<@`|Š( V`eXW QPI.[寓`Q*hut[j'Ij'Y߭f 0 #ZU`PBqG+.b rATzU>[_ g48h) bvE^UȯzAýX&Fp=B-#_nP a`P #Ы2 9G~Jا(pg2@L@y[?.KYC-#t(moiZ`DfZ M B[WUQ* /֦lgd];\ ^zX$z, y}//[\@_4AAA0 cC`dZ9y4˅̄2{O6И:AP<lHrR! t2"0QUP~/ֱ'= s H&*3fۺwo(y^J}fսPb M@(@n>9ًȰ!vb"]OuU\o\z:qM9'3 Ѐ`BG!P fdT\=QV?B<ىRLZn5h **e@e%"<xFvt%yk3+.SyYWY4mzEMS_ݯ\0(@z0p} =c`FK 3 , 8|_kʠYf(?% +4VS"jҝ~A `L` "AFqzBW3QWxPfd /ѵ*ՋDPX(OYcuv,qDUg+PZ\YR ;`2YŽo_ 4P8G!z493_o# F$= %WHn2nc@>LW nc;Mh蚫K̤7A~q` ._@NqLAmEY<]BHmPȥ?)fLAePT2חf Q9EöHεԆ^"Wb+tJ(sPDwoGnR9 `(`Ðz?w!UdBRXQԵ x(f?ASz.N2) 0x祔٤Hzj薁S;VTvUQѯˆd@Ae`OA\=04Z ]@7,fR/xj S62z2 ^HV?*8/7gCmŔ-s@Aw%0 Ns&&͡ G( 2K{Ð d):Z#EE ?#>Oݸh6>@4YMQPP Ū4MhT0"?ORtqh稪tVo $-}NJyET1!=5VsZ_= H w! CX@AЌpl^I UoO)NE! ( '0n a-`O;:Alnt& !E T{y2@TQM:jM~`㸿#]E~qyW|zԸ>/\xJƨ@=oy|Ss`Y ى{zj.,,ι0Th_fc}g<2>i\h>I@'C)fu! _iW݊<5ʼn55ZVn$P>4ĝs~@ `&B 0& AT_F_A.G=Fy4N[o!@QU@c v PAA_UU@TE"T6 a=]ke(` uAd@ Db[_ )SG*)|@^vUDӯPvɇD]lB5_WT@ `DPEda 6]讚<\݇J1j׬n/ >` SQCdzsv:54pm]>K})s__gl!.%T"aշz_1mh,0   ?Or͕a$ mg6* ՙt2? 6,⛦i~5L'3]/ 0.H`P, m3-` )F=,/v3z 2]ut^'/p,@ynDN$'I?Q{rY{9 wݗXԜSٓ_eRC᫜|Zo{*s&7 Ҿ;q e|Hb0;h?&Ӏa |>V|NFzS%C ٟh=9d>=FmC?nƜ`=~ct>&A(S43OܖxU.׆G]NsdJ7nU7&"{ p)׍׽iIy$Y2S֣d6QK q4]^iIVH+WdPbY~_ Ϛ q& $` sP'ȷfPu` +C4Q3Y^o)J ůTTX_{@2ŶIPxx^$zXHuQ]@_uߥۇu:?*]W^ R=Y4м:4ws# > kCY6 "~ɫ~[!Nul(SM17 {ފA&5ʼnEc[qPd6` f P0qP1B`p|h!>ZA0%uAUUy--4*r*" ,(" % J&g=ϺYVqYEn;<< %@c/?@?cpT@0@^4f".6z(>j|h -17 PW԰S=@<@{ ŗfRDFc!L{*"{}Yr@Ѥ<4Uh&2:JԉNkySwͪ<Ù 3-mjU)=" सʓC]Z]YFX0,Y5xW| 6 `BXF 8PE$0?,p,Wawjzضf([0 W,A1Ä\NUq3LQ]=4OBÂ* :ĜKEdG}i8#} a}fsxiC6q4-\MPA _"sȝ(KQJKٵ6. Ja5, ͯ4.AV;P [? ᡗ`ηigwٞΥ(↉:AN.@0 a0psOjYĻ[xPՏp'*&@AVR\Ph/cQݚϪ2 Cj(5U i145hsYgd4zԊ'UXq3Fۿkٮ3P{̂-!֪8|~i 2za 0~)Pltbl@m 9ƹk-u5>iw/Z6]~@kY^XcA|15g%z + `&B=R|r^p~@ ^4_/Wfz2^;9@7^**j4RI yT% R;zlTATZAy)E[pVWg(n#PerXԛV-@2 Rc@ @^YvٻOKyO i)nSp@~>3jif~ax0 L@_A^IXc@<S[`SC!@&rwd@%E ښyGE^s[ODFcE_LkQ,&<JǷs;D8׺t &PPWw{ec()+p*T 䃃 %@0 CWoF8P#x◜)}8QfD!2!;֘S QƊ##*%i^E @B 7N_c(QMQF=#5E\?k.cy˺}*ۨt&(EsVVRE=EYպ('/ # Γ1[w'Kw~Ē1 8Gp [v^(ԻnjV{K h%r5|iEDTUMuuϺ,G\U޶:O{,νTjT@Ug0P,G{n"23 vm籷Ut#p̀ ` (IF/  ͒]l6@6&~% p^t->SxRv/R;&5`t 4L`#!E``a54-$)pJdкc @ъ SY JXۡ?.X\]-45o8ʙ RE=WdݟsQPQ6J7ã؊nSJI.w?cj+ʍC >L%XCB0^6 O0p.@$ }Tzw*"BdPT@  +[N][X-/ )Dd]H"}yR5q@?VA>vkj$Y/fC(f#4:JUA._ O?hop<EL@~9o2V̌X)O7 @k2S(+bٌ>` 45D0S@0qejA=.L!TƮ;3G7o.Z $դOziPd}iM@UEW蓃I#4rڞÞ#)QZUNAjet/`o>zS#0q~@,;o: x4@ǥA/@'(@w 6, Mo^ķа4e@F/1ܿiF{="21n2n0?iPtArEh5Ϻl4}2ԃBAQg09VQTnx-LWy]j3p.aϭb B -Ֆi8[ړ avb+W@/ߤcl x Pd pՆ^ow5j+ơ =4RPT`B[ A+ z@eJt+O^X79`S{&(d)o_;K~jyvxm즷|[38bⴄ YC"Jn㾜ި>M#uL Ȱ@Ajl9 >W$h| 40-eEbWs#G㓡T֊`0xȎP>[N:l8l (Tj~(m@cN+:A::n0*bReT]OV}/̀asUU]ֱYGoL/v>{//oWU Viv o][m Fk J+ KS 0 Kö `(mbWW7As?Ht 4bOggS{ H|Rʥ=^        HwC_ii)f]. CqQ37NEw60C@4!Ac#VAD>D  kb;J.ptpgR꪿ըEtVELͲյǂ0UyKF_C? }?zW/UtǶUau+v*@ T7$ZnU/?*ew> |]Y@_I0:Z U )PdI (~%? 2C)V]7#4ȄP3^e:237E`a N1!.ُ&YǠ|=As4mp$tUQ) DԾa"9sf'dH6^>|-Yk? ^2^j0ӑV z| 1Ӿ  N;c`j k-AO0I P!^)ƙoֆ;oLwQgC/7|o׆\>0X`l 8  L@( RԜRdV>T (Gfrg;@||4HYifZԛW>VD@JJ>7G{gw#՚Ngrƞ>n,T(Hk7 =Ubaߥ>6aB dKi xX]21"3cf#Jh\zun6 1R!}ۻ5R@o$ "`mUDUӭ z8@•( [}sVG"uZu{s#SF(@R9٠WX~ 5憨0_r.`\ 0 @f@%h0ҞP u{> V!Z1Lvt ڝ D] EYdK," UN`B 35tRF>(,9@n/SIFΛWP4{WJQ疩^ozI1"AL%eZgK"#]tH˒(~Q`ݤtc7;[O,ZXBO>/@ wSh@ڒ (b6;lp鲁If_!j|K1$I_\kʼn3ur9ƍ8 eaWu$ 01j " rKig|R3@Ҥw%s ??T> "rYK<(g%&&Џy;[sy; .`zmvM.4/'^dTIeT 0+/@K@?sx jl pIٗ$>G#jdy(-3#p)NJykkl@<@f AMO""jVEv]i x|o?g' 0]9_<z @j.UA ~|qzںsd۹ZW/ξh>Ѽl ߯=50tWcKͅF x)qyXR"`_߭ i<`J@ebO.o.(Պ_b귎8^( @uؔSR'H8z\`BTE`i>|j @l;G ;矟6G* ߗzФǘW<Uuz[],n T;it;ߟ 5I]$6Q+֚vӭӍVrO?~֥-{v4G7)1z lbM0ل @V`IMWkÿ<2ۘ9@>BVh0D,&7l7DdJx A!-\ךWQ-f <`_sUF/ЗEVfΘsnլw-jy@k:TD3b-dVs+H&RDO~=I|heK(XoW? Jj 0 Y6HX@habM x3ԋ8Dv QA֘O0{\\L0*^@! D^/2!hsQQȿ/T %@ɘ  -Or?-r5hbQ>W6XsJW|ť +ݲA]ʁ`02sI>ĄE; HO߯ _84 X CC%^2Sth+DtzJ$jÉBFeP2N`L gutiWH".vzOkaLsR@u(gR3T@њ7TD@/NdTy+lW3m7#v_2yz.#(*O!##+Գ6Lb\XZ<ΟX |%7 PpG 0@BOɎf}26qA URTk.(LdBm p"Vcna\0@S pAiI1ZR!4CA A(cHՉ+w]'LW D@S ݛ{ŔVĢ5}Zc7*s _?ϕEE''>60Ox1RK,, `1@d>=ƿ]-P~kd{7PA6~r߰]#Ԇ^+ޟݔԍPʭ{`(2/jw@N09*@! xۼ28Xe}^DP|HqA9EN ֛7KܕE͋[qG5+OIIssN-Fh(^!Y2ɩ9I2{ΪsFP7 }+o@HZ)1Gx%C/pـPT`>2~9 ãaRu5Vq)`v)')'q(r<kL s9` @0ydBqZw(5}BiAq\IyN3J>`h/* ޚ 0b%{^h~|i^fjwA}^UgDWZd5;F P^:wq6˚ =] \+o lm0m086YvC|?F]SL5L\fN n q(`AuB (&aT` E^{~$X)$x%gt@yR&`~8IS t ޚ-:*=_N9ʜb͘@s:} [Բؕd( k6$0 v [>THP5?@_i@7l@s^)?h_] ,k0|cF|̭T+L Jf臲ijN3 @L`8ri d<9JPP9A"H4nֱ:=Gqj_x/9_`VЭV:Ê*zN\Iۦ6\~ƬT+ZuGTS:׽]PAnI~mb% ,h!}a`+/:#G@AS6ƃ[O[FZgcgΰLWD(Y<Ƹ1fWP=ALh ҪkCW_%>?.!49s5`쳝JTړ Kc ʓ_x:@<ݓ0fo~]Cx$?M$Bh&x?یVll/~k=s;|т DW0_n RЁ`.O"`lo~7f[@(ʻ'!#@ [_osNqḇQ@5Dw]R}͋wԕ4o͹mg|v `WP9LV%T!Ư(, F93RAca҂'k*-)oR @ @4 l0>ً_Q˿z== eYde~`#-~{zR5HdubǡwfsZ :)@DϺz͟kQ fDBhzPzCS"iT;~5+?FHUR @V;t(0ɨZ:tLd()p ^ [m (,(&? #b_mS2#I]ҧ7xd]QKo82`8)*c8 !N0 =00ܰM_4P$rΟJ:m1#s+rUlޮ+ +@ @ (vvUƯvC1 ʕrd2^ ~h~x9x o,/1 ~9НE؊٨ 5JF[H81E=Zw aA00 B@z'V>i^CU&O$AWCת: $?=WmrFՠS,Ŝcm%W_VSyv{o-JU-) @ԇTP(yT`ԕW:p"0X&@#rgl ZP2_ڀ-82``^)w9 ?՘jd\ȋmȀ3gs%DA0"4= xg<"iHd t oTg /A A7+dWS ғW$,/̖P{ҞTzq|߷9$L5&yJrgy۵r @.bq}^ q(. }H~AV'T (Eٻ?ntf3bbfO7o w% qA SZ?Ku`& j@0F[UaW:Q-[ ~5z"쏓OzJ`@f'"*}]xmꏝE֖V CRX5tX:ڏؤf9?>/bNJ\V_V9  CO]I_R@1,`aO ]?| 4CC@S l)^)٫?D]aۊH:i TET["N]f 0q x*dM;>9f §3 ydG٩֦@'ڐʜhOwytbqLW[qXPci^# X V(<_ is+tN`:Y`x"Ҫ=-rlo ٿAp I&,404>I@,~jy{aLCM1삧I|F]lö"5c/@y0 &L} '<0)BB)^&i(!լc3ql5{J\@@'k/fB@g4Ri׬,}2Wjw= k]{,F>U`ԇu+ z/ Ƽ T+~O,/>аxm.@ӆjc@>If٧,#S.?r)f}. 0j> aPk au'ELM nk,sq47^Fn &{!xAi͐Z-oy^8{uDu**]Q%RaZL[Yθ+F_?۱-ۮIsMvPz@O> /,7 7 `lP0^!b(pɌ_~hC\{NyWm`n:AZ&t ס"rz0C>H(!kϠ  ܼ8R@R=7hǾώKj4PP{&,9o쿮֤F}3v1er6zjJYLm/9*e(Qcj \`}cm. X΀2 Y4 BjJ]6q(>J 6٭UK_nq0nPN;T@; fLhC\@x1VL2=,;Wڷ[<3 @Sw:zz^ @ɦܙT(JkK=Crm]W٬mzwmnpAMܴ6F%CV.t % zd(  ,8 `ɯ-h;ݧ؟e*) C0 _ŗ 2_H0\~5 CKԵ8QŹ(9(|w١N0` T$A e{dĻv_>A*^@%NRaC@w$}WV\dRh于.9F-"Fyqd_/ϮBP2@*F-}׎M V@tNNڀ˲_~{[XrߋXj @xY@  @Ҫ@tN^ISW˟<+31{/+GfHey(]GHO,-ـ^a bL!W+J~l!6 "{Kd ą6gMέG$`Y@r!?ܴ`FZ0`2.5[x3h@k-`~sw0H>;,SgN\ eP^&D:AN.@0K!!$ %#PEUUC5:8<-Z>Co:.HJU]_z=ȊԽe{@8\NPy3.dͽ+[5`*~4RBض~ $u]up`9ƙf~fc^cg^" OF8f 5ČQT}6E<Afs":AY/ ?C~Sۄ,`ט9*%? ,T I0v**b@@م @v䖛$K8gVnɜ"7(K_VhN1h}p㈷D(tS`q1C{o )fMԇ6 YvC@o&7赟gx0KX3q=! h/I.0UDAS+@tWU@TFQ'8.F(9xiML/TT?~!~~1䲶Z83eP;X;QfsqNƾvůf:dT乌ɝd8fX|M[4(g/ _$10 2(^T>~cfÁwG˛ϐZq"RT+EAU 2~ijNaS *FBp|ůz.ݡ>yusm:T΢ srrݥz`=i8ޯ_z3dx_ B60UJ}dh]*(0߻D-`?wrS 8 6~(YXHɏ0`BVC_ ЊHW]S p^.jyKg׵xʴ/b d(W̮7 *%@1Ƶ# !ks.vHa "rвf2(@?8kB8ԯ0d@%EPL/`` q/$T+)^x!AdPp+7%.~/v6\ߺ9^ꕂRv]?[rb9  ` "Z*D, ({#@Q4 74UJ9pz&ݞsusUgq9}E+wҒƬ2R >@饾T9a4 S@҂g2s΁p, J` 0g _Kꐌ 2ohz@AEi~96]D䄮;>(QWKD*>)'FVeq PdOv 5i0QAERolO)6OFd:ccu}@; JyxC[Р򣰗kHkw3- V| cPx:&Bu԰qTDP1UpZc hEC$.qө _l",%BvT# lA\C^9c*al6P&)t؆fb$jVv}Ï|ѫ{>ZCf+ƍʠ` P'8 1r (VxkM-x@=Wn 4tJ@W/ACD-/UJT>ZkݻڎNSNLJÅz i1% &@ %Y5r,8wj AJR 4 9>A,2*Eأ" 8rG&յ䅷L z#@ f͡&p^*@H2I<(aep !`NP*4J$ DOxP4^mG2y~uLJ烥\^H* J20ڬZ?3~g SAF`^)U  XWh϶Gܘf(CʬhRDw?PxLEUcԍ&qDq|93/2I1XG]t** (KE?K!Ks Sk䌊}iY)}blP%oԮ=cvEq_ƨh5WumwTa@v4@7cZ>cOggS7 H|nÙ`G   ҇yI֙ PnWǟB13.o_cZS e݊jYxA@ 7*$WBrUx͢ӣ+dpͧq.mLE_06xBH>4b*tq(*2q!:+o<ݵx{ZmG_wb1t@$_Z])PbcT3_T@Ų.Oo_o0T@`l 9ٗ~2 6c)^i!" x9nDQDDj{0q Vu c2(؊ՎiYkxRE |Ү$;LX PPxPWDеMiQ~RS3 * R;99u"ʄH`Un7:@=~6" ݪ10,^gV lH`lYw _M z` (}>jo O^J6V8F~4yJtڌZ,HY=p]9`Bqѐ+`~=RYz.YkkćQ/@ ."){Wj J.* _3s棾w. cQ#Y9rb<j ;$9YCb{q{@7\l5 耵 4lǡsOāCX[pE( @0L3gu4O?2ڠ+wwG}+lG̨lbkS;('S}@t@NLBA@0F *>it1 1F~[J0ο7Vx]EO* #qSRٷT^E^ͥ\\f)jz*xPzJH+8htw3u {R/fq&~-=@7:A:P<4`B*Lb-RBʄsguЬA"j@B񹴉g5Kp9K# kC(`MUoiUMݜ. yRUѽZmik+r[ Gu`-,T0+aDլP/F?zy <^~06߱ JȒq45H ?lAU~AMfz::u|]>0!ԊC hv1QF7Xyo B 0 ` "k,uQC@{wxA24\jWm}y9YvFܪ`F#S2(^x9@/MBrH~~[3t`~,6 ba @s ~?}вç+x3`B NMߗbh`>%r}D,tNͽGSwaIu/ˋhVPPܠzߪK`$e!<)<2yO75@=62~ż!v;@bF2Pw*hW:< wsM4+׊/bx"ۆka:U$䪊X/ SaD*l3z+){*My ̫),ϭ\ w}yrS~qU_O9x"m08{ݎ>g_~{ujDb ltMT T~Q36 2c 8Bi2v-g6h; 3; T֘j#YyO&wSj&/rğ?>)|Jjdc^_a \ŸX%kVk%{/7YA̺DZ>oFh\tYם d>0(6ZgnyJ9ڲ @Ve z /#e3Ft *Om)R` ,@!`@,|bvϗL#(icA6-$`~WD&[#j&sQ.{sm]1{PAKbA(oNIj _ӏOv{@F ]) KJn<ܓwnOVt^q|L ~4IwRc#ၜV~͔ y@R߳H+,9!=O6zK%Dw+A(2o0pJj̐VX̀ 妇myTlWsMϓ ֔=exlo"vv61^U1vR@f,/n?-d]hFrQr|@Q$yXYf:5;X6{4Rw4^Έ\%W20 L`q)d_W܁AjוJ Aқp&^K6@n64}<?ZO2J+cb\P w ԄB@dguBk(^'٫Nq,B pI]eXˋ<#&/ZPD+ 6G;q1CbTD[˥#ʲy)%Tݹ9r}naكiӛӼ?oUC2.,_hCk*u -B:zخ( Mu;&x?.]k3h1V+ިD#ߓ/A* 9T+egq9޲n9qfbG>u`1tu>0u{&ZeRsjnzĭũ)`v PU85܆%0pʗm jzIf!8*%_3Nwcf~Wy6_צ,`cNm׃ "u>ZFlZM^׌料u7 -5_!I$ȍ.ж'46]IIUU%j0'8:<%2KՂ=QNUme/xpO @)ZAfIQ&WݓY/yÐP'L_N}9xưKoVPh95@I恊%*U S״]ݑVK|@wFx3(c#a=b0gs\LW3v=8Q[m,s}o0}}s1W27[/:D[wrWHX_%\Ddh0}d\>a.{8ޛ^5`?XiAQn޶Qb Jks˄ξ+Toʐdu"{z" p#%TUU=;wvc%~LT<ȷOP^;_rZ7n+^0x8{z4ﮖ^4w3r.w;y n2J2`k;`}É3h4 ]>V--cllЛ&$BAy/UͰE"ä;%C ?SG(P\F 6oѮe:]>XLMޖm 2cpW(6qva t GQ0kPI*흣~!>e >HB!t.^W;![ݡ\satjPqx8?_q1=׫-llk)cF+?tUzg>7-tWg޹k hK<įRJT̪rjqu|>Hz Si&&ahn=b@3ݳY?1@i|fy]) U3LR yXXW#~9ܓͮx:&'LL:ʴ_ieOgjN,8!9+] Pt`;3}tjkF}/4tb/}M^f7RiHs{KOu>/h=]\zq9ecV 42otePBC%TU]v?WN:KװYo׷~Xׇ[̀m.ۚ Go2>0Z|v&\$5U @u{~TAl!dL['TCYMM&|vJ.@dHj.rO!ecPu69 ,$,eo-ɽ@\2ohe9ǝ5rh`)K4)Ռe"L5BMmaN8|i>ڌEYUR*M{+&pÇoﭳ{Q쓹(1Czn Ujjb58K|B@@ 0@Y55>{(9gXr } `D鷞UI"_K"g>K%DÝ:&awr%yHS?SJAs+zh \:96.hȜ֡:_bΒ Ii 2@mx̉K )-{77sc*zCkgZI~OK]>τ㯝_O{N|{ha۳3pΦo5p7rc>B(29~'Ibh1da(PE\94晪]\Uj[W-^q֮ŭهT7*_LvV<\^$n\cWI%c 6N=7%#ZlyoURS>TfdR6jUj%by_\՟wuߣ̘Yx{8uu ,OI%7a-|TY$/3o:+ȁiNxuLRlu҂Ԉ$k$mW?fNW݌ʂҰOY IK򀧇^߯J43`^MꌽcU8 GT%|t(a;1߇ܛ{h*;AZTO0\ft+yVBB~sȫ_ccp/5{)f"̥Kw {##%EONXq{.֭Q(2+vM6D2(\~zV@ԊaE_N= ׊/pF1GcD%PU!pk=+F.WQ}Joۏ:Y9<ç`s_n@JȞ`8fvӐady( Wc|Vi0@6 {ȦaPF3;:Um za^ജzvsI1G|ʀ F#L|8#*{`Z9l/ nA!~K*3w܀ó.o<%aGԊ/$X(UŪ*K_s2n27nF+DB>1?=~ruT2t w`@EES= d%JfL7p*0@0 SPlnUɡݾ4YPQ30dBb$y濩d/-҄y*X4נ"wED3r9U!h㚋\f?*Ȝ3lB~%86`mx폃fRp zoƛ_F1" 38<ˇoTVxVK:Tڽ/~/p̱xEiY2Y4S-}iDB$}Rr:|=!yJ Y{@|w/kEu]_ih$~:q!;Q!bhaf[}8dou} h! Ecvxkű奜۱`jC t\*}X'(tf`)Wזmذ8ypny0c)X}zWr@x!nI`MüʹKO] kLU߀m7Eݵͤ`ݻaDw\\4yTD p[Ua¾*rqy׵&d{u{֒lshA  1s>|E}XV [.Uv I֊Aهǯ~i HUUU~=s7o/J@zSqצmfxm}gaWg!'km4\UN86ԏx:IyhoIwa'&+ v+,ҭ*&8hcv@|Yy=pW_=6WGowj7) ٱ42**@~{1#w jҁnjBqP 'NUEU7rԀLfܫžOULބ~]5:v3?oҢj9~/w[?ݵ 8}OfL k}# _k9D1¹2ܒ4O8YZGo=F,C'lhl|Yg!)Ռ_gY^|SS 6-9.gE * ci&1{;U|/E/PKn>sær^eY+p!)CWFH;k$e2@D2$s35խ7߾sOڏ#:߆NG}Oa$6z$PD\F$KZ,z0\cɭ5<ֵ[n酩gPۘ\Zkl;BSSd=G .Oy4^(V%TUd<4MyNP&~}DiQ\罫3XODŽ WeN%Ɂ9ͷlwqp@au|VQ9恵_B4~R|Sԁ!y4sӿ$x/ߒX]HXm*N-VC8;CX04#~eBQ} zKPSd9c,?NSF Tj:ᮀpdWs}3U j_L6I34[@U?*Wt@oC@\SPTAX|k0ݐ]_E7ٔkV Orn}BI޹oMߍDLp4'Y+,Ѱ2w&q!"1` ޵|Չ< jƮ{y&;^/5Et*e#SUU@Q핿[h(f4ݮ|.uo=nx8fCVX[vi>_=%J(s#s{㧚M1LELn=( i(R_[~H~:ƪ?@6]Jnl[X9lz(C`Zm-l |'Ϯ|-45ůk?xw,x KM"n7L:JUUEjLgu}k# $a=cqOSdT\]'vA%o`B-edL8=aO:MȜGM" ܟcf6Lc,,h"j{lpQss|`:XBGc @c"A|;^-b~Q;(PSd뛿m4TUU!| ׏eҿ=|<`ǧq>;Vk$mf\x~Mlקfk ee:˔~493sX~`Zݦ|͸v] yPpjqK*FH,9m4m 0J6F_ ;1Zu+CE4ࡡ/3B ZS53i4Я0>On~p̫};SYD)}=$3/U_T1fLfU6p]zTyo6O\ _ U:ITT9'y4S+i3E)?;)Xl޹|'ZeMhfL`.d8f#w{&by$oj٣BGB, z00>NygNm,cօ{x_a;^yhhx^ffz0\2w][U7>PM,lg-5A wsxh~e{s+pkggzSCa0{߀gzy}{cm./μ5wۚ" ֏/p믟oyo~}6ugz`zpu8ޞ鷧p ^/p,/OggS H|b/eIlBM ԤTX ^^pᚅY^kx/ / =Yx̺p؋؋ػ0 0 0 (/*J^z^p8p8z^zp8`OggSH|m/OggSkH|juP/-A;     J IJt*DHRV)aʔ)iS~ԏQd2/>6yuu 4JVC!9ۆ-z3x7E`]C0:[$ϯ~u3;}K=)fp7RFuVu n&"XM4 p+B@% Dέxyx>rhg .)Z)F+Sp}{aMG3kX ]iC bTDLj[8u@V*0Zhփjܼ  r|%_(l@{[ f _$p@k`îBwoiw\+<~ٰw8yߔP7bg(,s)" l[ut0oO! Fvo[sʃ+畃`<@`|Š( V`eXW QPI.[寓`Q*hut[j'Ij'Y߭f 0 #ZU`PBqG+.b rATzU>[_ g48h) bvE^UȯzAýX&Fp=B-#_nP a`P #Ы2 9G~Jا(pg2@L@y[?.KYC-#t(moiZ`DfZ M B[WUQ* /֦lgd];\ ^zX$z, y}//[\@_4AAA0 cC`dZ9y4˅̄2{O6И:AP<lHrR! t2"0QUP~/ֱ'= s H&*3fۺwo(y^J}fսPb M@(@n>9ًȰ!vb"]OuU\o\z:qM9'3 Ѐ`BG!P fdT\=QV?B<ىRLZn5h **e@e%"<xFvt%yk3+.SyYWY4mzEMS_ݯ\0(@z0p} =c`FK 3 , 8|_kʠYf(?% +4VS"jҝ~A `L` "AFqzBW3QWxPfd /ѵ*ՋDPX(OYcuv,qDUg+PZ\YR ;`2YŽo_ 4P8G!z493_o# F$= %WHn2nc@>LW nc;Mh蚫K̤7A~q` ._@NqLAmEY<]BHmPȥ?)fLAePT2חf Q9EöHεԆ^"Wb+tJ(sPDwoGnR9 `(`Ðz?w!UdBRXQԵ x(f?ASz.N2) 0x祔٤Hzj薁S;VTvUQѯˆd@Ae`OA\=04Z ]@7,fR/xj S62z2 ^HV?*8/7gCmŔ-s@Aw%0 Ns&&͡ G( 2K{Ð d):Z#EE ?#>Oݸh6>@4YMQPP Ū4MhT0"?ORtqh稪tVo $-}NJyET1!=5VsZ_= H w! CX@AЌpl^I UoO)NE! ( '0n a-`O;:Alnt& !E T{y2@TQM:jM~`㸿#]E~qyW|zԸ>/\xJƨ@=oy|Ss`Y ى{zj.,,ι0Th_fc}g<2>i\h>I@'C)fu! _iW݊<5ʼn55ZVn$P>4ĝs~@ `&B 0& AT_F_A.G=Fy4N[o!@QU@c v PAA_UU@TE"T6 a=]ke(` uAd@ Db[_ )SG*)|@^vUDӯPvɇD]lB5_WT@ `DPEda 6]讚<\݇J1j׬n/ >` SQCdzsv:54pm]>K})s__gl!.%T"aշz_1mh,0   ?Or͕a$ mg6* ՙt2? 6,⛦i~5L'3]/ 0.H`P, m3-` )F=,/v3z 2]ut^'/p,@ynDN$'I?Q{rY{9 wݗXԜSٓ_eRC᫜|Zo{*s&7 Ҿ;q e|Hb0;h?&Ӏa |>V|NFzS%C ٟh=9d>=FmC?nƜ`=~ct>&A(S43OܖxU.׆G]NsdJ7nU7&"{ p)׍׽iIy$Y2S֣d6QK q4]^iIVH+WdPbY~_ Ϛ q& $` sP'ȷfPu` +C4Q3Y^o)J ůTTX_{@2ŶIPxx^$zXHuQ]@_uߥۇu:?*]W^ R=Y4м:4ws# > kCY6 "~ɫ~[!Nul(SM17 {ފA&5ʼnEc[qPd6` f P0qP1B`p|h!>ZA0%uAUUy--4*r*" ,(" % J&g=ϺYVqYEn;<< %@c/?@?cpT@0@^4f".6z(>j|h -17 PW԰S=@<@{ ŗfRDFc!L{*"{}Yr@Ѥ<4Uh&2:JԉNkySwͪ<Ù 3-mjU)=" सʓC]Z]YFX0,Y5xW| 6 `BXF 8PE$0?,p,Wawjzضf([0 W,A1Ä\NUq3LQ]=4OBÂ* :ĜKEdG}i8#} a}fsxiC6q4-\MPA _"sȝ(KQJKٵ6. Ja5, ͯ4.AV;P [? ᡗ`ηigwٞΥ(↉:AN.@0 a0psOjYĻ[xPՏp'*&@AVR\Ph/cQݚϪ2 Cj(5U i145hsYgd4zԊ'UXq3Fۿkٮ3P{̂-!֪8|~i 2za 0~)Pltbl@m 9ƹk-u5>iw/Z6]~@kY^XcA|15g%z + `&B=R|r^p~@ ^4_/Wfz2^;9@7^**j4RI yT% R;zlTATZAy)E[pVWg(n#PerXԛV-@2 Rc@ @^YvٻOKyO i)nSp@~>3jif~ax0 L@_A^IXc@<S[`SC!@&rwd@%E ښyGE^s[ODFcE_LkQ,&<JǷs;D8׺t &PPWw{ec()+p*T 䃃 %@0 CWoF8P#x◜)}8QfD!2!;֘S QƊ##*%i^E @B 7N_c(QMQF=#5E\?k.cy˺}*ۨt&(EsVVRE=EYպ('/ # Γ1[w'Kw~Ē1 8Gp [v^(ԻnjV{K h%r5|iEDTUMuuϺ,G\U޶:O{,νTjT@Ug0P,G{n"23 vm籷Ut#p̀ ` (IF/  ͒]l6@6&~% p^t->SxRv/R;&5`t 4L`#!E``a54-$)pJdкc @ъ SY JXۡ?.X\]-45o8ʙ RE=WdݟsQPQ6J7ã؊nSJI.w?cj+ʍC >L%XCB0^6 O0p.@$ }Tzw*"BdPT@  +[N][X-/ )Dd]H"}yR5q@?VA>vkj$Y/fC(f#4:JUA._ O?hop<EL@OggS'H|`^        ~9o2V̌X)O7 @k2S(+bٌ>` 45D0S@0qejA=.L!TƮ;3G7o.Z $դOziPd}iM@UEW蓃I#4rڞÞ#)QZUNAjet/`o>zS#0q~@,;o: x4@ǥA/@'(@w 6, Mo^ķа4e@F/1ܿiF{="21n2n0?iPtArEh5Ϻl4}2ԃBAQg09VQTnx-LWy]j3p.aϭb B -Ֆi8[ړ avb+W@/ߤcl x Pd pՆ^ow5j+ơ =4RPT`B[ A+ z@eJt+O^X79`S{&(d)o_;K~jyvxm즷|[38bⴄ YC"Jn㾜ި>M#uL Ȱ@Ajl9 >W$h| 40-eEbWs#G㓡T֊`0xȎP>[N:l8l (Tj~(m@cN+:A::n0*bReT]OV}/̀asUU]ֱYGoL/v>{//oWU Viv o][m Fk J+ KS 0 Kö `(mbWW7As?Ht 4bHwC_ii)f]. CqQ37NEw60C@4!Ac#VAD>D  kb;J.ptpgR꪿ըEtVELͲյǂ0UyKF_C? }?zW/UtǶUau+v*@ T7$ZnU/?*ew> |]Y@_I0:Z U )PdI (~%? 2C)V]7#4ȄP3^e:237E`a N1!.ُ&YǠ|=As4mp$tUQ) DԾa"9sf'dH6^>|-Yk? ^2^j0ӑV z| 1Ӿ  N;c`j k-AO0I P!^)ƙoֆ;oLwQgC/7|o׆\>0X`l 8  L@( RԜRdV>T (Gfrg;@||4HYifZԛW>VD@JJ>7G{gw#՚Ngrƞ>n,T(Hk7 =Ubaߥ>6aB dKi xX]21"3cf#Jh\zun6 1R!}ۻ5R@o$ "`mUDUӭ z8@•( [}sVG"uZu{s#SF(@R9٠WX~ 5憨0_r.`\ 0 @f@%h0ҞP u{> V!Z1Lvt ڝ D] EYdK," UN`B 35tRF>(,9@n/SIFΛWP4{WJQ疩^ozI1"AL%eZgK"#]tH˒(~Q`ݤtc7;[O,ZXBO>/@ wSh@ڒ (b6;lp鲁If_!j|K1$I_\kʼn3ur9ƍ8 eaWu$ 01j " rKig|R3@Ҥw%s ??T> "rYK<(g%&&Џy;[sy; .`zmvM.4/'^dTIeT 0+/@K@?sx jl pIٗ$>G#jdy(-3#p)NJykkl@<@f AMO""jVEv]i x|o?g' 0]9_<z @j.UA ~|qzںsd۹ZW/ξh>Ѽl ߯=50tWcKͅF x)qyXR"`_߭ i<`J@ebO.o.(Պ_b귎8^( @uؔSR'H8z\`BTE`i>|j @l;G ;矟6G* ߗzФǘW<Uuz[],n T;it;ߟ 5I]$6Q+֚vӭӍVrO?~֥-{v4G7)1z lbM0ل @V`IMWkÿ<2ۘ9@>BVh0D,&7l7DdJx A!-\ךWQ-f <`_sUF/ЗEVfΘsnլw-jy@k:TD3b-dVs+H&RDO~=I|heK(XoW? Jj 0 Y6HX@habM x3ԋ8Dv QA֘O0{\\L0*^@! D^/2!hsQQȿ/T %@ɘ  -Or?-r5hbQ>W6XsJW|ť +ݲA]ʁ`02sI>ĄE; HO߯ _84 X CC%^2Sth+DtzJ$jÉBFeP2N`L gutiWH".vzOkaLsR@u(gR3T@њ7TD@/NdTy+lW3m7#v_2yz.#(*O!##+Գ6Lb\XZ<ΟX |%7 PpG 0@BOɎf}26qA URTk.(LdBm p"Vcna\0@S pAiI1ZR!4CA A(cHՉ+w]'LW D@S ݛ{ŔVĢ5}Zc7*s _?ϕEE''>60Ox1RK,, `1@d>=ƿ]-P~kd{7PA6~r߰]#Ԇ^+ޟݔԍPʭ{`(2/jw@N09*@! xۼ28Xe}^DP|HqA9EN ֛7KܕE͋[qG5+OIIssN-Fh(^!Y2ɩ9I2{ΪsFP7 }+o@HZ)1Gx%C/pـPT`>2~9 ãaRu5Vq)`v)')'q(r<kL s9` @0ydBqZw(5}BiAq\IyN3J>`h/* ޚ 0b%{^h~|i^fjwA}^UgDWZd5;F P^:wq6˚ =] \+o lm0m086YvC|?F]SL5L\fN n q(`AuB (&aT` E^{~$X)$x%gt@yR&`~8IS t ޚ-:*=_N9ʜb͘@s:} [Բؕd( k6$0 v [>THP5?@_i@7l@s^)?h_] ,k0|cF|̭T+L Jf臲ijN3 @L`8ri d<9JPP9A"H4nֱ:=Gqj_x/9_`VЭV:Ê*zN\Iۦ6\~ƬT+ZuGTS:׽]PAnI~mb% ,h!}a`+/:#G@AS6ƃ[O[FZgcgΰLWD(Y<Ƹ1fWP=ALh ҪkCW_%>?.!49s5`쳝JTړ Kc ʓ_x:@<ݓ0fo~]Cx$?M$Bh&x?یVll/~k=s;|т DW0_n RЁ`.O"`lo~7f[@(ʻ'!#@ [_osNqḇQ@5Dw]R}͋wԕ4o͹mg|v `WP9LV%T!Ư(, F93RAca҂'k*-)oR @ @4 l0>ً_Q˿z== eYde~`#-~{zR5HdubǡwfsZ :)@DϺz͟kQ fDBhzPzCS"iT;~5+?FHUR @V;t(0ɨZ:tLd()p ^ [m (,(&? #b_mS2#I]ҧ7xd]QKo82`8)*c8 !N0 =00ܰM_4P$rΟJ:m1#s+rUlޮ+ +@ @ (vvUƯvC1 ʕrd2^ ~h~x9x o,/1 ~9НE؊٨ 5JF[H81E=Zw aA00 B@z'V>i^CU&O$AWCת: $?=WmrFՠS,Ŝcm%W_VSyv{o-JU-) @ԇTP(yT`ԕW:p"0X&@#rgl ZP2_ڀ-82``^)w9 ?՘jd\ȋmȀ3gs%DA0"4= xg<"iHd t oTg /A A7+dWS ғW$,/̖P{ҞTzq|߷9$L5&yJrgy۵r @.bq}^ q(. }H~AV'T (Eٻ?ntf3bbfO7o w% qA SZ?Ku`& j@0F[UaW:Q-[ ~5z"쏓OzJ`@f'"*}]xmꏝE֖V CRX5tX:ڏؤf9?>/bNJ\V_V9  CO]I_R@1,`aO ]?| 4CC@S l)^)٫?D]aۊH:i TET["N]f 0q x*dM;>9f §3 ydG٩֦@'ڐʜhOwytbqLW[qXPci^# X V(<_ is+tN`:Y`x"Ҫ=-rlo ٿAp I&,404>I@,~jy{aLCM1삧I|F]lö"5c/@y0 &L} '<0)BB)^&i(!լc3ql5{J\@@'k/fB@g4Ri׬,}2Wjw= k]{,F>U`ԇu+ z/ Ƽ T+~O,/>аxm.@ӆjc@>If٧,#S.?r)f}. 0j> aPk au'ELM nk,sq47^Fn &{!xAi͐Z-oy^8{uDu**]Q%RaZL[Yθ+F_?۱-ۮIsMvPz@O> /,7 7 `lP0^!b(pɌ_~hC\{NyWm`n:AZ&t ס"rz0C>H(!kϠ  ܼ8R@R=7hǾώKj4PP{&,9o쿮֤F}3v1er6zjJYLm/9*e(Qcj \`}cm. X΀2 Y4 BjJ]6q(>J 6٭UK_nq0nPN;T@; fLhC\@x1VL2=,;Wڷ[<3 @Sw:zz^ @ɦܙT(JkK=Crm]W٬mzwmnpAMܴ6F%CV.t % zd(  ,8 `ɯ-h;ݧ؟e*) C0 _ŗ 2_H0\~5 CKԵ8QŹ(9(|w١N0` T$A e{dĻv_>A*^@%NRaC@w$}WV\dRh于.9F-"Fyqd_/ϮBP2@*F-}׎M V@tNNڀ˲_~{[XrߋXj @xY@  @Ҫ@tN^ISW˟<+31{/+GfHey(]GHO,-ـ^a bL!W+J~l!6 "{Kd ą6gMέG$`Y@r!?ܴ`FZ0`2.5[x3h@k-`~sw0H>;,SgN\ eP^&D:AN.@0K!!$ %#PEUUC5:8<-Z>Co:.HJU]_z=ȊԽe{@8\NPy3.dͽ+[5`*~4RBض~ $u]up`9ƙf~fc^cg^" OF8f 5ČQT}6E<Afs":AY/ ?C~Sۄ,`ט9*%? ,T I0v**b@@م @v䖛$K8gVnɜ"7(K_VhN1h}p㈷D(tS`q1C{o )fMԇ6 YvC@o&7赟gx0KX3q=! h/I.0UDAS+@tWU@TFQ'8.F(9xiML/TT?~!~~1䲶Z83eP;X;QfsqNƾvůf:dT乌ɝd8fX|M[4(g/ _$10 2(^T>~cfÁwG˛ϐZq"RT+EAU 2~ijNaS *FBp|ůz.ݡ>yusm:T΢ srrݥz`=i8ޯ_z3dx_ B60UJ}dh]*(0߻D-`?wrS 8 6~(YXHɏ0`BVC_ ЊHW]S p^.jyKg׵xʴ/b d(W̮7 *%@1Ƶ# !ks.vHa "rвf2(@?8kB8ԯ0d@%EPL/`` q/$T+)^x!AdPp+7%.~/v6\ߺ9^ꕂRv]?[rb9  ` "Z*D, ({#@Q4 74UJ9pz&ݞsusUgq9}E+wҒƬ2R >@饾T9a4 S@҂g2s΁p, J` 0g _Kꐌ 2ohz@AEi~96]D䄮;>(QWKD*>)'FVeq PdOv 5i0QAERolO)6OFd:ccu}@; JyxC[Р򣰗kHkw3- V| cPx:&Bu԰qTDP1UpZc hEC$.qө _l",%BvT# lA\C^9c*al6P&)t؆fb$jVv}Ï|ѫ{>ZCf+ƍʠ` P'8 1r (VxkM-x@=Wn 4tJ@W/ACD-/UJT>ZkݻڎNSNLJÅz i1% &@ %Y5r,8wj AJR 4 9>A,2*Eأ" 8rG&յ䅷L z#@ f͡&p^*@H2I<(aep !`NP*4J$ DOxP4^mG2y~uLJ烥\^H* J20ڬZ?3~g SAF`^)U  XWh϶Gܘf(CʬhRDw?PxLEUcԍ&qDq|93/2I1XG]t** (KE?K!Ks Sk䌊}iY)}blP%oԮ=cvEq_ƨh5WumwTa@v4@7cZ>cI֙ PnWǟB13.o_cZS e݊jYxA@ 7*$WBrUx͢ӣ+dpͧq.mLE_06xBH>4b*tq(*2q!:+o<ݵx{ZmG_wb1t@$_Z])PbcT3_T@Ų.Oo_o0T@`l 9ٗ~2 6c)^i!" x9nDQDDj{0q Vu c2(؊ՎiYkxRE |Ү$;LX PPxPWDеMiQ~RS3 * R;99u"ʄH`Un7:@=~6" ݪ10,^gV lH`lYw _M z` (}>jo O^J6V8F~4yJtڌZ,HY=p]9`Bqѐ+`~=RYz.YkkćQ/@ ."){Wj J.* _3s棾w. cQ#Y9rb<j ;$9YCb{q{@7\l5 耵 4lǡsOāCX[pE( @0L3gu4O?2ڠ+wwG}+lG̨lbkS;('S}@t@NLBA@0F *>it1 1F~[J0ο7Vx]EO* #qSRٷT^E^ͥ\\f)jz*xPzJH+8htw3u {R/fq&~-=@7:A:P<4`B*Lb-RBʄsguЬA"j@B񹴉g5Kp9K# kC(`MUoiUMݜ. yRUѽZmik+r[ Gu`-,T0+aDլP/F?zy <^~06߱ JȒq45H ?lAU~AMfz::u|]>0!ԊC hv1QF7Xyo B 0 ` "k,uQC@{wxA24\jWm}y9YvFܪ`F#S2(^x9@/MBrH~~[3t`~,6 ba @s ~?}вç+x3`B NMߗbh`>%r}D,tNͽGSwaIu/ˋhVPPܠzߪK`$e!<)<2yO75@=62~ż!v;@bF2Pw*hW:< wsM4+׊/bx"ۆka:U$䪊X/ SaD*l3z+){*My ̫),ϭ\ w}yrS~qU_O9x"m08{ݎ>g_~{ujDb ltMT T~Q36 2c 8Bi2v-g6h; 3; T֘j#YyO&wSj&/rğ?>)|Jjdc^_a \ŸX%kVk%{/7YA̺DZ>oFh\tYם d>0(6ZgnyJ9ڲ @Ve z /#e3Ft *Om)R` ,@!`@,|bvϗL#(icA6-$`~WD&[#j&sQ.{sm]1{PAKbA(oNIj _ӏOv{@F ]) KJn<ܓwnOVt^q|L ~4IwRc#ၜV~͔ y@R߳H+,9!=O6zK%Dw+A(2o0pJj̐VX̀ 妇myTlWsMϓ ֔=exlo"vv61^U1vR@f,/n?-d]hFrQr|@Q$yXYf:5;X6{4Rw4^Έ\%W20 L`q)d_W܁AjוJ Aқp&^K6@n64}<?ZO2J+cb\P w ԄB@dguBk(^'٫Nq,B pI]eXˋ<#&/ZPD+ 6G;q1CbTD[˥#ʲy)%Tݹ9r}naكiӛӼ?oUC2.,_hCk*u -B:zخ( Mu;&x?.]k3h1V+ިD#ߓ/A* 9T+egq9޲n9qfbG>u`1tu>0u{&ZeRsjnzĭũ)`v PU85܆%0pʗm jzIf!8*%_3Nwcf~Wy6_צ,`cNm׃ "u>ZFlZM^׌料u7 -5_!I$ȍ.ж'46]IIUU%j0'8:<%2KՂ=QNUme/xpO @)ZAfIQ&WݓY/yÐP'L_N}9xưKoVPh95@I恊%*U S״]ݑVK|@wFx3(c#a=b0gs\LW3v=8Q[m,s}o0}}s1W27[/:D[wrWHX_%\Ddh0}d\>a.{8ޛ^5`?XiAQn޶Qb Jks˄ξ+Toʐdu"{z" p#%TUU=;wvc%~LT<ȷOP^;_rZ7n+^0x8{z4ﮖ^4w3r.w;y n2J2`k;`}É3h4 ]>V--cllЛ&$BAy/UͰE"ä;%C ?SG(P\F 6oѮe:]>XLMޖm 2cpW(6qva t GQ0kPI*흣~!>e >HB!t.^W;![ݡ\satjPqx8?_q1=׫-llk)cF+?tUzg>7-tWg޹k hK<įRJT̪rjqu|>Hz Si&&ahn=b@3ݳY?1@i|fy]) U3LR yXXW#~9ܓͮx:&'LL:ʴ_ieOgjN,8!9+] Pt`;3}tjkF}/4tb/}M^f7RiHs{KOu>/h=]\zq9ecV 42otePBC%TU]v?WN:KװYo׷~Xׇ[̀m.ۚ Go2>0Z|v&\$5U @u{~TAl!dL['TCYMM&|vJ.@dHj.rO!ecPu69 ,$,eo-ɽ@\2ohe9ǝ5rh`)K4)Ռe"L5BMmaN8|i>ڌEYUR*M{+&pÇoﭳ{Q쓹(1Czn Ujjb58K|B@@ 0@Y55>{(9gXr } `D鷞UI"_K"g>K%DÝ:&awr%yHS?SJAs+zh \:96.hȜ֡:_bΒ Ii 2@mx̉K )-{77sc*zCkgZI~OK]>τ㯝_O{N|{ha۳3pΦo5p7rc>B(29~'Ibh1da(PE\94晪]\Uj[W-^q֮ŭهT7*_LvV<\^$n\cWI%c 6N=7%#ZlyoURS>TfdR6jUj%by_\՟wuߣ̘Yx{8uu ,OI%7a-|TY$/3o:+ȁiNxuLRlu҂Ԉ$k$mW?fNW݌ʂҰOY IK򀧇^߯J43`^MꌽcU8 GT%|t(a;1߇ܛ{h*;AZTO0\ft+yVBB~sȫ_ccp/5{)f"̥Kw {##%EONXq{.֭Q(2+vM6D2(\~zV@ԊaE_N= ׊/pF1GcD%PU!pk=+F.WQ}Joۏ:Y9<ç`s_n@JȞ`8fvӐady( Wc|Vi0@6 {ȦaPF3;:Um za^ജzvsI1G|ʀ F#L|8#*{`Z9l/ nA!~K*3w܀ó.o<%aGԊ/$X(UŪ*K_s2n27nF+DB>1?=~ruT2t w`@EES= d%JfL7p*0@0 SPlnUɡݾ4YPQ30dBb$y濩d/-҄y*X4נ"wED3r9U!h㚋\f?*Ȝ3lB~%86`mx폃fRp zoƛ_F1" 38<ˇoTVxVK:Tڽ/~/p̱xEiY2Y4S-}iDB$}Rr:|=!yJ Y{@|w/kEu]_ih$~:q!;Q!bhaf[}8dou} h! Ecvxkű奜۱`jC t\*}X'(tf`)Wזmذ8ypny0c)X}zWr@x!nI`MüʹKO] kLU߀m7Eݵͤ`ݻaDw\\4yTD p[Ua¾*rqy׵&d{u{֒lshA  1s>|E}XV [.Uv I֊Aهǯ~i HUUU~=s7o/J@zSqצmfxm}gaWg!'km4\UN86ԏx:IyhoIwa'&+ v+,ҭ*&8hcv@|Yy=pW_=6WGowj7) ٱ42**@~{1#w jҁnjBqP 'NUEU7rԀLfܫžOULބ~]5:v3?oҢj9~/w[?ݵ 8}OfL k}# _k9D1¹2ܒ4O8YZGo=F,C'lhl|Yg!)Ռ_gY^|SS 6-9.gE * ci&1{;U|/E/PKn>sær^eY+p!)CWFH;k$e2@D2$s35խ7߾sOڏ#:߆NG}Oa$6z$PD\F$KZ,z0\cɭ5<ֵ[n酩gPۘ\Zkl;BSSd=G .Oy4^(V%TUd<4MyNP&~}DiQ\罫3XODŽ WeN%Ɂ9ͷlwqp@au|VQ9恵_B4~R|Sԁ!y4sӿ$x/ߒX]HXm*N-VC8;CX04#~eBQ} zKPSd9c,?NSF Tj:ᮀpdWs}3U j_L6I34[@U?*Wt@oC@\SPTAX|k0ݐ]_E7ٔkV Orn}BI޹oMߍDLp4'Y+,Ѱ2w&q!"1` ޵|Չ< jƮ{y&;^/5Et*e#SUU@Q핿[h(f4ݮ|.uo=nx8fCVX[vi>_=%J(s#s{㧚M1LELn=( i(R_[~H~:ƪ?@6]Jnl[X9lz(C`Zm-l |'Ϯ|-45ůk?xw,x KM"n7L:JUUEjLgu}k# $a=cqOSdT\]'vA%o`B-edL8=aO:MȜGM" ܟcf6Lc,,h"j{lpQss|`:XBGc @c"A|;^-b~Q;(PSd뛿m4TUU!| ׏eҿ=|<`ǧq>;Vk$mf\x~Mlקfk ee:˔~493sX~`Zݦ|͸v] yPpjqK*FH,9m4m 0J6F_ ;1Zu+CE4ࡡ/3B ZS53i4Я0>On~p̫};SYD)}=$3/U_T1fLfU6p]zTyo6O\ _ U:ITT9'y4S+i3E)?;)Xl޹|'ZeMhfL`.d8f#w{&by$oj٣BGB, z00>NygNm,cօ{x_a;^yhhx^ffz0\2w][U7>PM,lg-5A wsxh~e{s+pkggzSCa0{߀gzy}{cm./μ5wۚ" ֏/p믟oyo~}6ugz`zpu8ޞ鷧p ^/p,/lBM ԤTX ^^pᚅY^kx/ / =Yx̺p؋؋ػ0 0 0 (/*J^z^p8p8z^zp8`OggS[H|^0/OggSȫH|eBkhard-0.17.0/misc/twinkle/sounds/outgoing_call.ogg000066400000000000000000010770121371517016500221550ustar00rootroot00000000000000OggSsoS/vorbisOggSsoS?}@vorbis Lavf57.56.101encoder=Lavc57.64.101 libvorbisvorbis%BCV@$s*FsBPBkBL2L[%s!B[(АU@AxA!%=X'=!9xiA!B!B!E9h'A08 8E9X'A B9!$5HP9,(05(0ԃ BI5gAxiA!$AHAFAX9A*9 4d((  @Qqɑɱ  YHHH$Y%Y%Y扪,˲,˲,2 HPQ Eq Yd8Xh爎4CSR,1\wD3$ R1s9R9sBT1ƜsB!1sB!RJƜsB!RsB!J)sB!B)B!J(B!BB!RB(!R!B)%R !RBRJ)BRJ)J %R))J!RJJ)TJ J)%RJ!J)8A'Ua BCVdR)-E"KFsPZr RͩR $1T2B BuL)-BrKsA3stG DfDBpxP S@bB.TX\]\@.!!A,pox N)*u \adhlptx||$%@DD4s !"#$ OggSsoS{Vp+5ʜ$׏۪UV7./oE/5< L'aG?3\ .Jf$A2JI ä˶Ǐ;05i0Z%I؆پ2˲ QJ 6,u]xNP%::LR 7$mǰ5ӪK6ti~N . ;4~-MR/ODF֖O_m|f۶q",Oܤn=>jM3^>=\X꨽rjCR*A;=ǡfRI/KMA@%WvKT&~?VpJnS=4.bqΓFd7_(BmY $YZe[|Xuv4e](ʭ5}@a]Q5:Xө}A̰Md<?95D9]C4 q%0g}ʔMɌzDN6P!N#m0k/Pm)dǼ!ї߯8&p.>X2z(x~wrL/1kGfÉ=O{"bNoC\+@Ѯ[m̸|dBl]i.LO,y~:TLJ'OJbL#3u;$XV]ȍʗKП? -+޴~S)#eN+iw.gzd:Yigg;t+2_OK.zۮ 0d0RNˤ*؇Uյuf6CGle%A|8mJӪ$ }#R_(e"^\g]´Ejn=ń^7#e&C*@F3*YXWc%FľEЮ=RBkT`FP5=I$:vGsֆ-z҉5KH6ẋ=$ә3}AI,c O1E{ZWi,]}ӐQ%0`I }YnJ`$HmҮm`#?5[>]`shG:@WT~ƣ֔UYCw];Ԍ/U%+nf3ɧ*YThԺTC4M û-I*ZTB:*+Dd 7~v찥M10=aB%'Iڣ8\~o LZu`Y5I-#<2pzn=y0\.?h{  ,0w{m p9 4 B ! ȺH*t Әifbv48;GeͱZwZcGϏWcFX Ypޓ2J#̕0uV*q5ǺZ"NKU1nȀMKS/sN)2؍x LYϦht9IIIfJ5+s{䌡LAn@vuSDj dlEa:#X|oN>ײrP#0 Jefrv;̈hg G=,ky?HJM0t,'L̸ '2 1z(*RU55D>VU Pv5R +FCI2q"PA $` fp3jTʗe6>vg1R'@gYyu:Ӷbi0\yx'`P.U0w7y/Ol~v1dB o78rtwouE 1okq] f"fpF%3vCu])i*C.nC\S&+nv1%PUU!Q @HFy@$`Rr.E+PJXd 34@]ޥ@N9`=18zΡRWm=s PWm.UE0~I>ǗbW1lzn ШNW H?0BK`;t!\sNf2{&JžCjSIիV~ PýRUUl@aPK0H)@@CGM5xN|J -2MaMMB• xzOK: d}heȿ_ **S&*NhءAil<8N17>r6 P;p8ewO)m qWatl_\ 5Ż*]-ˎ Q'"/$ 0K)K2Z*ދ2<(i Py Uæ4(ZP$VSZسpα*ՀBx,(Km&EЅ=HY{s1{LoU֛ je{QHBdeN-ꪕ`< Ҁt!,y4(OtTPpiznhsGCL.VEC65ƶ*98VVc\̒ٳתUU:#TIP@h,rW4;hZ8{uGJTl<@S1j Y?\ QHK϶ @dU>G֓jaYin%yT=.1CԔ,g$wJ*l\vADSS t0-{my#R" 6f6 :t|,E"E:QQ# ՜牝Al؛.2 @&̚'Y+C Tå>*,^ͺ[:gpVشT!@LEM@p/2΂C=l%8ѱ^!+4t!0#ȜԷ`tkf2FGeu{n@rI8N=vR\% ) h{9UUU-0PڵCWl0?n[V(YD,o\QpD@4Z9Wua`pT#2hn5^7~գ]KkuS~Zz ̐嵪"MNljuπAl>WԙƵ<L(?v}@N3!B39UUU)$8BcU+TtvLB"T@K-!`X#x4 cnF֧f%Y֘ HS~#ra4 cG;[E3˪ISݴN#T> |+Vb6O 7gH]TIhX!t$ydoBKrtL&GAPw5&{TNvj}dv#R䃫T ߰@@keJ1.;~rT G)xA0(W &7rx;n3BU/dgd0mYRxQ׳'1r%jQsVA 5zMn$+TIcw3 i(|9# !Q X fٲGu&&טQ><|,' }dtv+W*Jtxd- Y5)F54ٵG#Fci&O`$OSM` 0Y&}X 2͙R?ot󍡡;UDjf= @fbQA=Ku{oPn%$&!10,ݖl w=wy槀h("8h+1mӌmb Q\lp$#~GeQB-@ʩ$ s,"3 vO sr%t8`8=4WN9`<6@&u>cJyQbVM+awd>꩕T`Q$h0ks3 8%`f: 5W0túlW=Zg[fWEA 5 +Rf`l91UUUBZDP3^1U`JHGu*QDvtBhV7xP^-HbQ+=xn5jXuzb[p52E{\ϮTfl|x߹h~d˶Lk1, obFܛDC`+/ZVCt@mC<-Vj-W.YȮBI_f'Ia-ٺM'pteKVTU)4 Y "Qw=&A.zb!A튥,4U?`nczXy9N]A"Ff WT`\z0kd^uYƶL$B# "* c . TU(-8ޑY JA$Fbj@18K)mwcpg[6鳂`;1[;5Y0 #]SUUӭ$ *ZHuf RJvUY)(l,˪JH8fn!3khm^49BlJkrbjkړԌa1zf8cJ=nU]`,Dڬw:6.Qm aۖ d% @*a #p6|Dk\Yِ}8!4YX ~7%nd;DzCo5"/pJ&ѕ"UU!Z"21X_1?s@HIQ4*.ÅBFZ@9.u .d]('STh߮zdjYA"bueG ī?4^}sWߩ7a/"·BKZ#촩 s?ٲe XJh-/Y}&;B2a"tHkGEƏC7uӖ&yT]v}c]6y1ٳ:JT(@@JT#$nv%'#])U 4vU;fUŠ\)p`%DS1 +~`VIMdc(P"8eR/}&a .w*_)Dn?WJQܒGPKP-(UvQ5U2KR=k2bg ]vvq4ŅY]Y'aEs 2n@G9pտCց^g:a5D&pzDR<98;ՊVBQ$@9(FX,Iqnj9bMdA0^\va1,#PA@dEQ+eg<5_N ԩǶ˟Ǿl.E @Zj֑,Y`dY l>Β +)О i 7&n8bܐaWaU wYjD2h_ѝz3lZcD-Хk UUU{UPt5ݫAFq%D}`_>|WZ!0enhcUVFL n!OzX8=d4ʗ5Of3(dV}T<{qmqW)kuYSx^5RzP[O`$~F9ʬTGCUad >NQޚԄl,G5Y:7IkL:CXwҪ70f4B.WHUEDYXBPTi! dOwglH*Zl TV:@TR+v qAtC0 MW6\^ "`k++ Ϥaj]Zl1n. P6$eBG+,);ԋ=*M/[Uhf6@$=bU,IكfUT@q!I$d0 U#_:zl[F|.`hktiMi2qCCG2|ahWIºd2tݥGe KcYiPG-z5Cdtv9VPU,DE#xUiȠ2^,] D ;zIP}@kU- Qg>vUq,Qc޸_9k%9)EW. @T9Nwe>\nge^FZ){J4ՑuF|煩Tf@>C>BͲl`>>(m/(W*@C`V%E*r͸WřEkMV.J Fs*$C%^N'C$+ɀ<@h*P Gub`X*@yn;N1]qdF18RHUHC&)ghHڸ s E;aQ8`H4ؽL6߅] z"4_Km#}V ^ [Bc,ľ-0oG"ld4%*Χvd 'aף{# ^7[;l( qq١Fg "b@~9uAf]i =5 EQ4*E#9H)y98X?FGV{y( y]'* c:9;jtjTA`-{1=$3V齰EH,Lv/ nPI`|4 / WŹ+ҿF-הUAvU\@nA`d !UU% Eߛ6T20#"`X [+: f\1-j 5&h0F]uRCx Qm`G=k"&`פ3`鏭iBp<ù.F8#̳~}yc7XDk$b T*UOIĀ,g` LEWg1s0~W;!*٫BwǩY0Iod (m(06:ZH;L!Ȍxh I9 4XB-`Y˵A ks}+P=RP65 P!TruOpׯ{lϖ= PK~zdKtғm@go>w|FѠ ZG-f rb)#(p^MͦONsR5dWr]KE5&yUz?Dj&'VUUU^N6+"@TP,9tN\IHwR 6ݡ. bC= u:rgzDw(&)b dioɄ10y`awYhgoJRU55.~X]uMj& G58BpPeJ[,4Ic8;h[bFzV 56*ذ}b.^ MV<~?4q[p\}al ֥WnhT' &)Ð_l mo>钙mvm~7չ5qwDZUrGrbv0db^X{Qp3V*2#/;XQ`8Z ""70RL=FJ!De44T_Ɯep*VS{' R LTԈ˩ ;۽ρ3MYy])!aTU;+#k?6Pըx-4:P*UΌlrI@NTYqeCѽ mQ<Y\7 ,k:UhzŃ~.Ikj3yhco]yX- |8I0,ХU]cax Dt&؄{aAR!uɮsG\f:G!&Ì24L0G&O4pd3ZzcjLV<$`6G0 UD̨ $e,ÂȚ쓅飹aBDj.Z 8ʾt j#4A%`Xe1&b1{]9!VVIREUBMfj *@ f񾸭<4$\֣nD +  %_Uc= ԈÀ1NSUUR Qڐ DKC #/MEfBX j1#h)s29dy,,.bN8ǧZW]{bp'&ˊ=1n?V)RbK_^7EY XGΦq1u0- /|8بo;@rK ehYs XcdoG'f,kLRL Wtwh87}w\MVFd UUU!28 e uWaj@ E4;0F@VHdPL5"J!7!,OWn}6쫨n0[X6%oc<4r:QҲ~ZH՗E%$_~$>E3o| /G+Ag^w§?D]YV n6V%ݭ{ZBEM[/_E]Dd&w0$82+K;ADSQ@ z,ijGal~%=m5"52k(``0ŋ)pȏ鲜WF>Eie2,1NrqլUR3 FXNsG8W`8u81cͯ6ƀ/~ƫjc , 2BF$2܍YR1<"Ge]hGLt`t3Tj a"F#ID*%AW^i oZm/mu[ZqFK*qU&P0k# A 5ry V؂oOiREEAӧEXM ;QG]NI\d7$Yk.3_g]~7gd#.gCcLHUVEX=e sncce^k[p.0%]8ձw*h1fv G5AP[J8_%d&8aIǵ#JM'pGc]HR$Ɔ0ڥӸa6Vq(B`D:6$"CҌ@ u&Ƌ/ M|S'w$LzB$eI2*HJϹtF+:y/&^3;Ӊ䰗(S$m i ]>qyǪ*lM@{{І]A bz_VG[gզyT4[;lj@&p}`!STUeDzɸJ-YגD7z!٭ &bHh}`WǾRLuuqa8F @!pW{aKr䓼bmVoJ=)`dgw`0k0r3<*H}'[ odAfԎ ]6dPMLU0!Ld%lH72P$Geq]wpiװX?DM AɞNU ULC  "rY]b& -ԻQ*J`<sVپɑsgW,Ǘ~@3 !TTkb2Өd:_(9~eH(X g,`ng&:#ladݑk! A~D@ "8 ;@_2XYUvq)JNZuaCCGee Z#k$A!*$W-I@gp*HA(`F4I͎*mKmyG?*ij'%F ,.MܣY^`| dXptrc)l9@Y\WcF$*>)dR lNZml LB WouVn5*nX85g1TUE2LT"H4_ɎU qm$Wc4نH#@z[]5f.؜4$+{qldTr5t(/x?ʨ{b]\S!&c 湽e.mjBC93DT@R^%5e Q1TF0iأ)BJH@܅v8N#yW:y]Waz|.P'j{U_v>vDfD92 {B 15 #  ucQ>@-9?#5"AJC6"nX)H 40kq c:s uiJi`"BI{gE `I?ƞ#{&!{qu0&`E CMQ1 @$#daS g2Av8~.z4 9HxGXWŹߖ[JS[V1UQn0{ht /Hx0Z=%IUUEekڬ-B~:*A(̀<=x۴ JֶI3C[[Pz5OM&4+ۑcρ ["a;c wR ]PR+`۽r ~݇:6$sd$a0  ns58;3Cfb>d * 悐GCs]{x',jJ2~.] jez.0-Ar ;蚡JR7 \ƀ:4縸ꎵJJvhab `M鵎[B+\r^ߨa dLߍQgd~ h>ni E_}Tc x--@I? B|gmNvF՞sc.!Hl|Qʹ |We-%uvfWӷz}Q' lEaEg,'"(%RJ4ªňsa"lJ79q盵=ǒl&x͵úR(J,ܮЍȞ& YŴsYNt` kїtpD@B IŶrdDȂ ,9$2&\K ~(jgJ| _Mf7_ Hv)! ga ]Qcge.]Ld7 (tvcDTUU2(Q dR-.VDJI*tzσqUF ~NOPU>e;(u|ZgP'ٝ:cE>u1!74jH10 @7Z)!c<@TMy8 DG611XOa]YP1 }7{)Ă`0b qdPq˔^u=ro5W`Ҁ Ger՟CJ4;~uiGMWR Mw2 R2@J CFfՊ !T(q826X hV C rr,I%Er_`&C%v+@5iA7Ո;rNs{}m+#Yz@$7Y1`[_1H~*)@R7+y hp&l2,І@P 0#W3toScWŹ㏫ nK03(A1o;u T*dD/U\@QΪ%,n_Q첾' 7hHm %˗‡%TBEK[[ڰe]xjGZv;  G[Wu=te5"}UY;’17K)UU[ArD(cxPhB]HS+l)bq&T#ѨךPx b&)߱%bfb;gj*,;4oz*u^u3%b`y0o?; }y$kF1~dIi-8oc‹L@ <D;^#Ǖ~U1і X 8f f%7,]2y*)Nwu$nr@~dSUU(f]L6zhkpɘPbsT0-R0`(xE1 j9v[y_Ԛyx:89%u'M{j^us}q2s 8c_u;خ֯a&Dg#}*Uf>Lp2Vlg|H\rM,1cA>W'Y/4CaX&kJValHUHU5L̺=AN6+ Bד"!XWʒZk( ~>5f zMz')9ٳAEq``K2S_ p'3}یL#ON Ԁ^, QB9֫- 1 ͦx G dۅc$.N\N ;!vaSik#@6Wei3uyE7*9|>c7kC  C- >UUUebKp2(d牣E*w\5] 2&`K^ 5 tVyK>̍`Roikæ!{z4RQ΄ɦ,~)`4M4na v]~<1auЭgI766nIM07@M"L7(uW 6>@o6U%@CG;k6 <*.}]ChBX! ]TNXjP$ fCHLkqg!YhtVP&$ s\0IBDjQ[Р`,]mΊvF} ʫgݙL5PIQِ}dz<M} _ EH60=0C(+n A& @`fknZHVEba %L&ع+Þϋ9Aɲ9wH'X# NY<zV1a=Gʺ;,{oNNY-oҀOb T߅*RECJe $A(e[ˠ4tֈB`|5@st|84 r!CC228Gaj WyxϦR!iI#v&+nf}ӥ+WJY@TCYQčF(0m8r'ei" 8:/M( gВƵݲx %40xcFL֫C6Rgy?1ߞ޿_ kja՚?'R*yozҫ(Z1@<4wp)Ǟv*]E/Jpvj؂ih W]+t;V٫U&+Dv{OF7PCUUEQQT x#gcLr7MtiɳH)@PbpKzg &fq8 F@fᆨ*6F̿)1W/cH S6+hPjB^`$/idS((p}kk^aeN¹X,0 Qi*HWA-;ԌVcW)3EԘx>@)Fg kTj qgDP܄^"M%3"\2b=ׁ-yqەާ:w8`4ˤfL=$H_ߖ1 fH,q{K>ndDn]؀b[R:fMhMsz;FrbS  n] ]Gi"G%8.˖)S3Q5Ԙɠ媪',BT!4@:tNh=RJbjSCAM!X- 5ʎSaH)'D`q҇Tc*@MmQ…{Ȋp"W8`g_bfT]]Yoك֍}zLnft 6ſ ̧l-0jhǽ5Gs-uɣkYRu-ϰhѓQ|b'@Q&$׹A hZ- _3"zbXt@ө}9& ˱SD:xΤqi2iL40dz~j?h`xsUh3re/(ϋ,Z*5; Ip/gDm {@8gEuiᄫi\ޑX^ۍ@" E8"G5Ak>>gVSWc9x'ԑV2iQ5T**EL"2}^ 2 ʄ xPccq8Wj"OPKz+m۹ e hrۗNFmKC9(ug-eUUCy((5k"^.Qw21I H4I)j`Sq.Y4fM[y3 (ݖDLB&Ae/ΞڝeTc[uD1}_6@& M8z= 0RL*6d|  Pl6Yqt7!\y x;2 07`88  AGe;,$ ӯnח_w,MVaI*TU,F |%DVe:x%H̔УR}jJ S!:!06}{@Z{ (YeNn/mUʓp~R$`kLrUU[(,YmeHi' Vv05 ]#e*@HP,Ss*T4ObuP!aq?iMdq`9ˏemc.ޯB6G824u0Fb+Lcf2*@vZ;_=U4rhؾGe.)[FvM5&{T[1kuF6̤DpEayT?>1ӅYhs_FV qV$TxZ^)YRq[$`9; J ˌE㨻Va Hb&P,Y_rT^z^c%rdl)}MԴ|X-drV*~`XJ@!8 _#@[pwiP `9np W5$зڿ#ԈUut}[=j7.JeEt( A$P Z-UU()y@" QԳe., Lr0-kx<4ϙi c=5_ $cpiXJ0"P'D*xc[7 A *(k<{qpf Q`V%*,Zr[ ;{X,^c&+Nۗ,̩"l@T F;|Al@ ̉66(~%f(4 3? k{YyGRX"D* zIs=b۪wU,aL a"`0$ـCB a׌m InajmlPy--~)vB-ɛa  _.)WLq Ow`YvXj 9L̤sث*QDNMp$:ThJzmL|%U/_)@!c0 {c(tӫ-=NdgWWd%EVj \2aj2C<{0qr[4jG Pzd£g,;2 L8_EZdc5hR pCs~7v:,34䎊ӑuds#3 =.9Ði^(,hg/ϱE5(A q͊o{Mc \{V/JqJ@3hd8OY30,-95{^eםſ޶ }(;7@ojpD15{=9L^ܯdb]jӵK"pyP$CG{GPBU+=*ȭ}7]kDkO&+$.ed { UU20@F&D9N#;}Kf7GUUFlvN?O>y0bpG.{@c* R@l~70Rw؅&wS"ܦ/\3.p=(9cWE*cF! Nյ{^.S+h4[ŪN2 =.U8(,TAZsι9)f0-o2K{an09TNv?&vY>hN(RB Ng困PW)|7& @ nxLmDM{ lm:0lOggS6soSbRT       Wj<#03y]ˀ <N5TUUJ ƥb P9A@v5.KR&"8@.9Kͫ R yVMj8 ۞r;3]S`l0Vɨ!d1F$:W T]B|YUׅ(<Ӫtl_(4G:Ķmj 8mӼvqBH汙(9Sl(8[Q~7凯OwS0הQa[-;T4h`ij\l幞rʒMm{՞N˥3Z'KVDjeQgʬZt'[8 {.}vIJ]4,%gWדavyR4 0ɚpTKVdw?i#݁(KQj @66Ɔ G M=5^$uqXeKڎ$:(ODw2UBFjim;qoƙibai=>:f!b1t$If/1C[$ή=QE_x2]%ӷv=6[ 5$D m3_RYX$5f_δݞbeL}rv& dvukZ/:>ePdhM>(޵4bؔȹqkLIxv,a&ߚ欴NsP?~vӟ/b}E|o9glP7>CjavD ÷^9Ͻ+I 13bh<<0 yJ(Zfdg҆8̡9)-,IAG;~u[U_H rk.TҚy$U-:wz;*ǐx@d[}pCu~E*4z4yL' ưeLc2'`)w(JQ#߹ǧڏ_Բw+mu>i408&U{ٹ1|,RaqxS.MӍ? ;aXiuv!Ul &jItcrY=g`jںb 7Q뒁UsJN;HZ}`pbtt֐%:( 9X~Q3eS2h؝e%TUUF7ün7òv[>m\ź6˾~/dmut$,WL BM3y'qMx$&A1s1$]hމ<Uf(%7zD4*gfLs;"CZmP 0Rq!eg$:ONMF+*F#z;k46 4ؒV^SduJRזɃ4I:#PU2nUīTmթaWm~9X_p>mθm}ROˉ3u _B;гeNͬ(M:Bbz|C*ak Tk]fAK J R}HG%ybus&Cd,'8H`v1N*P!J#UP9q3j>L:aEeLSEBgdF=;,WUUC1wϯUwg_W879c/.# 1br:f&FdO:tF̐&MBwv6~1zd<IUUU!SLpcKE~as\wؽvG|%>v靦'ɡj?@Pܓ' z.Qî87$=$] դ7dʺrאpQ.>{֨2 V)2J*"%xG_=X ̔dpB]eFf/K A g@!if4 4{s޵n N)fBY)NMMgf"Bq+Q#B1v#/xlwkoqBĀA.~! l͔ޟx~l~*,d4ZϘSc2̂ X@ GwzϤ&}[}5h=Kw+҆lH"HauUV,hMs`>줇W䩕nj^zSdkӉ&%VwՈXƔT0O̬nSTJ%H'n:bGƀ cpm EWFFccp]D )8݂mj Xa 0]c-K^,E 4-K\4#d'I'1TUUg71U|{>ks$L;*P_KMlv<ڮg)DqpwtS ]PUh%ȺɬɊa7 DQQ9=Lk5=ued!#(IL㕆C/HD5P*(G}ߒ3q9歇LY{ 4KIh lb(]#EyeSQ0Y^7SD6I~,},  k4ME?jBnm~΁`g%r,$g1gz{{0$e6 q/ssa=VW۫ c)QUOZ<3ҰydhDMMh^ZM(f7)Rq71nTPY'T$h%DBRO*Id!lTVu)F]mȘp,De;ۨm E4ѱ(*{%5ȻzP7sn#xi~L0 Ae-f`<ΰ3({!y6 IY{~+?ONjt?]>XE *6i5(j`u]"oh 1j Ajt@Q4> 60ݞ*9yRfGhUmhf(7L>~e/TDRu8q(y}(YJ 0+d-b(ݳ M j KԺ0s;N 6lV^L"mL[:U@s:]hJ;~y=}cy?.U]movT"Go,ۼwy^F*WLrP%GgmPgu1u1нsqghzM1"R8g/]0Pja:X{&xX & ͒SBTH6)PWR 786bwn.Cht"&ϓ6C/ [C+`F4LlזcA1FgRs%TUUmuU I4eG擾#Oߩ|xu!ޮFk~{ϧ9?w/K@"+%зU3]@Dيb j*'-6hEZ )$}vEL3/**tȋ5:;AUU譪Ƚ29E!)RUBBDz?rcd8l^18vcQbb D j3+ ȸjoT`i^&Z kJtde,И%@{Ɍ"UWLnޣƹ@ir{d3^e͊,Gϒ!9|\[K.为UfZx9Hڙe3&:+įD%X VU:A?؛@sLZ"f1U%bŶmZ+U_R>uHI/ A#Kvb!$!-[.${?@Ua& 8@T  t>qQb0$h,[k&IfSLlgZ߻Z{|/~t9~i?=ybt8,%E8RayR9#F7{O\`TP?rf6(8LNӽFm(W3$  k[=Mu@wӆqeNs֏sj9[J%I'S!y$Ų-HQGZicq.HęYutfa& Q9]36y.w,eL.D\hkez2e͂d@-*2nWUU5 œww~x9J;{> .>z徹R,k:٪hXO%^AԩN+C@F$[B)fi<iɡEO=]ef#KL S{9ޭCEG",xkܯf [lع@p_YNˤ =N^7PudVw{} Q` .oL7#  ~q(&eXgLy>.#3ZRUUH\y 6q"U9B/|:x9_8m,ח[X HXWohUiU`X3f Im($ /&/F dJnV/D%T4b

y2xs1*_TmADn#j^, E?b$+ G:='J"4-&RmEQ$rnJ~,eQbce˘d.4 %Y3dp:꽿~co=ys>{q~IгXKE8B Sjr!ٞW4M{YʃefYPddY΂d\*`xV E1K5o جEJ)F(WM}zLk]s%7ٜ74 |'ZdoH6{9wphv@q/^Li(Ʋe-=V h%:ѻ=#ISl+%6]7?>p1l{r{?XZ( ׯd/C{}M(L Qϗx'WNԕEBRg5׷v2ЛI׫[L~߷NM3L6f_LW*1恗s^3ʑJU)J b5)QҲ%-d{3E x)3r#. &,hh !92DZa0"!fYZ`f ʖM l"cg 8 ̤K *E},Uӗ@-+T=mT1ǹދCu3/$B QTrz.iN)IR jfNCPSȖ,{c-L<]Ys~_phV 0# "a# #b`0%dHI~#* >b <^? t3Kpl'ص HP \cU-qQJʖL5 &w,Ȥ BUUu.FPu~|ep/ʼnCq`v?#:+y16t*oR]b&3~WsN1sfSÐٹ BŊjRCۣ~׺aUlک% uB\M+FGp@HjU~:TIi~U[#92˿u\j])if@[Tb^w\܄k$8jMqX笑w ~*Җ%bxF#{UUUU3R;'$~,A9U_umu~f7}9{ W>skui׷ܢͰbS-G|33smRgAM'-2eΆ+,;;,η=?~sޏ/^?V?Z]m]蔪T9_?b|[.yirtԼd ((9ɩ7?9I&STw<ŝ.^W̺|bƸuMH ilj `!Z֦$N'M tΓI;cC ^8jU.#cD<]wN[lr9>Mdo%ELmNiR{T PلcΊtRGaVUUٹ9jV_};^5dy9?V.^?ǝs~};u^SgpzOƚ4K:$ dTtJgaB 꼶BmOkjr>y~5qv(@ؾ$YUJ*Me,~㏐ WW\C.Mp8^{,M' ,+<]c3#_͐Nvʶnฝ=d)PRd=3&@u[o0cD6 h"!TcmZﺩ[pUS55?Mi i\+ '}1]]]Tߤ1L@R9a.K*TU6z{Sp` ֥1"ivj2ٱ/;I(˦%%elZH1=pJ5A0鏜 {aꞶe `$mL[cb4>;HtKR j$,*Kp{~Q Fmrg?%UITU-U~ ard:Ka(^'%NP'&PPib4 㳁J@ EZRlG(-KB0Ѓ;@̤;UUUŞm봣xr5j߱I-X}W>v~r*zq2dmC7KCy"cSaGh<<ܦwS]9mEtväj"Y"%%iăvOB2P H=V0V^/L Â됦*ф| Bs[UiFx.0p=D,eI*4yalY9VV6F{fi(O )ķV۷t8=|Uk2-Re\?aqVszkuek,ˀ9y~tx2Դנ'}Lxuj Q!';JjMf#X=#}5R}{cQ ﰲyxֈkҳ=}Q, 1i\.&;xV 9~O{Q-qd1@8ۅN_#=> ]fal˔\0 ytܙQYָXNmCx8{O_תWŕazq˫5M}! vFf)* 1 ȠȞ$aPajTP/`(6=[S1W jY2);6D626J]E*J $PEeI#ԝ15q[u<-cQ7[rc6pnDZ317z BSy~΂E~HYrC)&h<;'5G3TU!:ϭ_ϐnEG.\ zo&CB**_+ AWHEӪ.?LJ~ !1Go!"mr^֮;4C{`_8%1{1)m,p2$h\[% bƳUUU]uh$Z\FJ_ 7xo{{N-w,q9"4b^㌽vW)T5-N$`[쎫!'abhw6MI$=̌myUk,a@*JDi_y,񖪪dja4&S$ENsVw1&B s.Fɡ4o \tgP>>k^,e E lYS\QٙUUUM6",󷽿S7i&kz8:.#u>8<뾩}Kjro+)ɞdFey!:A5{K;o^ИJ"wԚ[81'Q0M2[uK QWɠdLu]I mtZ]+bl}SBʱ tL>BG5gЎztt@Z!Č>Q~,}UѠlB@tmLjh ԕ0nĿӓmt0o_Si^$[܂ *Qm:{1$Ԭa#BUwv=}3?Ω3ְv*lv2ɢA'SQWUڙbymŢbI}"b,0ӑs r ", LVOȰ^cJ ;CȬ?5= J Ta_@6Xt~ h 7|-"0Hh|\P%޷=,^磬_~a槃d>]N]]?Fg2}MG1kU i 9T;ڝc Vv.CjWz![L%tDӕ7tx8Na)\袔k Kkۨ޲^.E-*=8V67},A}Ԇdv\;C6i.¦]Q?ac Fwyf$ul Q;hkV[EEDө*RU,&??O1y*\,En%围89M X/gR=sz|k^r2SJr#G-O4w1T N) jgly:t V!(A4霘Y,5娳XK'{( —7y܉ODW=% `Ed6P^2=+4 uI<4Ӧj4~,qPBhfҖ!4'ݳ(jsPDݬFKAr[?zam\z}?Z<|}nMsQęG6 9Edf4:d>)1uAtVTQtvr6Ӑ=L vN91_$ITS1 m`}e `y 2uT8_ߑ, aiGl{Eɣ2*}L:^\g`dvKlBsBQti A@˖MɝAYA㝀IwaXUkԈ +LzrʟN,9{m/n[/gZZo,Z*II $Țɮ>Ц-~Ue">;6z2CY@$I5!gZ`m"c_[BD 8ThZ'>^yK bd>[ڍ[Iffymּ KgT ^(>5`^L AҖ5 ∷v`Ld Uj1DC~g!ח7IbntME0{ՙki-$ոgDpf@ңg2ErK2ZU(,s9j}{ُ)y,JT2EhVٶL!X tZ*X܏fQL>d |@*J;mEXOggSsoS'U       te E@ߌhZP9|3J}z1|U%<9jC!aWP=|}z﫝_՜K<:|o{n6NFȕS0t8U+P۫#N1p֠h:PLT̡!1e}sr&!t]NV|gj9H!(Ea 6I *5^K"7 @54>ǣgDlf8PאNkׄtuJmFI$(akPLeJV$\= 'MG\UUUYBM#>]O\~víz;0}碿ͷm=]4H8ilW /`teVMS2Kѫ dӃx bךLRt߱ yt2K]&JMҸPAB6ÑljYXړ&l893ޗl:=wќԾ3'mgJnX5rqQ ڌɔ $44 3,YC!@zy%T?)ﶒv8j\/:gSYy&un *(b(,xz*`zyzvdT*ŪWySM"jL7YLǝxR9vIroe(`@ W(<+ʡsydjd;+4-&Sڹ=ܧiPh5G:ώmFeׅF?KjI)bGSuUUFݫS' [Ȉ+%lK1 cln jP &-; (%d+~Le lj m'ȃ!ڂA=.wהg?K[r\f0]Yh^V륌X3e1]^,k.J3Z4kLԺhi8s3_?Mf{2ZN’8A!*$c!ۤ]&{+Z"ۼ70Ӑч Ȉ_o.0 憋m͟3ptg"4ym9LDi.(2PL4-S 4IGTԢfШ- FʵO:]ܹ+uG9]m3<~5V=gY8xԽZ9wk20Pۓ8*5SBjI iufki(Rav U%4UmC?W;ZtD&I H}ΟP5͆9`Z5@ 0%~LwnРal4932K%TUUX>{.zq=~:⠺>7jKX>QY4cW CUA(2ky;_RHPS.&<9fazI8z{bs m}P3ѬgZtϊ$[|H OXeYrT.M%"; 4p1B8oz)Alpw-vg[3^'sf4#ƖA4upUUZU} R+#[]2R^n a8\T{+†%4?.{5N-zknW1X5kV 5P.ץ\# zjTeL@ Wrr`U8ʕw6M2VQt)CZW^T z" /kZW ԩ6 + h[|֍H$Ų pVUCs (,Rē9 BV!oJD}^,G fgZƚ;Ӱ&ggRIUUU$6t\9\w/n=?>lFOL|<Ԃh]oU7oX("v륻lIS9o2.UIJLmRsO ],P GqϊpyPqF**5^ˈnE"`0@uJQI0$ NIs]4TM}9-HP*## _ #m\Avt~N `]KN$Bh #3Ie+I˥aXZcIsuU_yx#z=V_L+`Nu ]6qNAڐNPLV'$6h\S829Ɖٙ9Cא}'gJhn=`r3 SjI%ӦRYM0n&};Rۏri 3o+ø #Tk3bׁ nJ-rtqd,lsiL3eTrK^@ڲ9Y #,, UUUmvI&K~lYrN}4w2|t}[~3/C{5,Mp\'ӼHC[Γ \A㡪:tU;;)]U 0kSΝPBC_e!ζk!by{y,6<=tl]LIFYQ~ͻi|`@f#܈fҐ;,ulp2kҖNIv".H Udkdҙ䪪Vbk&7'%nXwU쨅J{PO5uOʮ7ȉ12LNujnA{[};6Zq4(f٪YTM6 d6f+ЕbD*f(ѓ޶lA )Җ%A&JVnQUUu`N}H\\D/}Z[x;ۏy}N2[puk\7:LSY,Q{ >:ZdF}҈^6C)\g,R pUV{v{KC'SL*C2C<i8$q"}k iQYq$l,KƔR) c,S-_Vkq t Ј}ހ/MwviF#F .o β޵t'9Kư=(a4 #YB"TUUҽt>cD_&k7o_{ѯql;+u;MbZvtD!onIf-j뒢BaPr梪Rz3cae1ƾ,c2iUF%lc2uw璃١v2C62h@+6I>W:3%~ ;#. iLRytiNPe/ ahNMj9u'ڭA[zy[/k;{˩umg^rAd+7OP1zzg9ӱTz[3f8< g[_na_#D\e0X8U}֮}7DGCkڌNQ_/xϺ[s]3m KkCαPy.s1gb Eane <{ܘ雗9 MWFTͧvUEAW<з[9J˞u%ww.E\UdcWdlR_6]Ys4lM3&4vɺpPтɍׅT0d{lc @R7-SA4j)q2ʤ-n %:fHWaw)˦lۏ-q rMMa3V JIU i6+^5]@k81KI4r71HQ;Hh@q1sf̅Zg./yTU* `m^è#{ʙ.TM coP1dnLȺA l;"֜ۑ909ᆰm84-[22Ft:2xWUU!Zʴa9s{~TOv[o/#!/0Z[KӋL}5}?{XQ뉧50Y&Hix0$sl7[\IQI?liϋYƾˤvz:9q13@p=( ksXRzw֜73sMAc:\plDf@ iBgUd ~q,d6 h<[vpˇ&h<;{ҹī@fF"j*[_sP$o̓>ouV uv gr!}9BWLh`aRsI;,>kT3~4UT"h/o]rjcr\i U R)T% ٖ$O{1Ѿ}kXKT}iuc@քɘzi$ӂiYNsuqk1)>,~ h+=h["U/pQK(,nlg|m`=x;\nbZ/¨Wǥ_Ki*q6>|B2 wgAKlңz(Ne 쩊_ȸ֞rtduSPTU"EPX|YH E%Ra/PJUΔayEjvm$߰JdYC\(4|h Sj lji>,G ABԤlYk+2D3pLM쫿MN/%*{9{է=;l~xy8|{voAZ}%Uryg|L[9"=)MOj/e$YIM+.'` U~i5n$Um?|}N;$/-KP(0,-ˎa[` m.da {uCd`:beA7oa}gL c[:|p>q ER,4- JL4> N<2=Fv/:Wҽ6LiuQz, Y= qLf~[ ˗i{ί/^wGAu]>oq~]v{jF_Jx)_S6]m˼ЇIvɄi G2b%sӠli}2*AO+@FqU B8lbUhRUJ+!kNxHI]|-y@S;C&C$yϖ(@F? (>&|Y>L~4)["`Mnhdf̪"bJjy{8:l\e䣫NXw pQ$Ya֗ͮ ``CUu ,4-oAPF& bV]i.W/_Tg-nvm|}7Yֳ?V76z~nM~z(ϟFWȄ|a.3t MӎzY2Ɯb*DٕMG?}1*BEaiP*Du9. ]wc5HZ>֡6FRwJ'>R[z68{ _^LP5-K h|2@Y3vf(K "ku+_>6ә` SQ=,jՑ@40+[N&P9{-p&=R PŽ^YtfIjn/M*N0 Df͛5d.v1#2 Z-IX1H_@׽yM]8r)A7͕GHֺ̛d9ù4ѷW$24f[>nZP^,} -rWkiڗlhXAxLPUDcTBt-K~V[ߍf{/ߌ4УX3NU" QjՓ05 )bhzڮL R+н^ m,kN5Z*Q^WkByIXA")Rj}*E2- XzHj]ns%HnP-rnB4gr,q:i"4N,ehАL'( 4>>L*=q'vD ݕ`=_)O:{N;/^hw^u8JRCM7, ~a.$ wPH3DmZP5zqmBwYeh5δr6FBbDRȲm-b { q;Z.|+Βsk"9J .YK}B#.J0*.Lm*q6zИu.{Zpň2K6C%TU[`5働ȸ+m6;^m AxL3eJ=@-n窪j-u,[!^/Lw;ƯwKEtr(#uΓ .A ?-d/mlB<e9z^e 4 qW "3Nhg< dave[1f?/]lx&4sBu'9pw|h2u#nr@*^Li` bDc2`, ΌzEUUJ2d~eɭrRxp|uy\.BKXSG!XpRvN- Hm¨`L<'<ݏf9Lc|j⁝D 1M#TQA'T,4cC:lLL:o H]7^)fsT Ds[_ =iuW9_*zX4~ N )Ɩ1NA [ rG7%t UUUjes~O:R;ǴV}<[ovԞ.Z ϣ^콍!c?,m+uRS(]35cf \ӭLhc \LEM$@WD_c㌣*cO?ƙ5K7n-@a% & /XX2(2k"1-3&;iWw6ѳdddb;ա;Pe{Xs"a(t7,^-޶kec9k [p< qd3cݏSdRlJ{n9˵t/Ҕ;;";Jyqu|ۦ 5sW݉N@VL\S9bMabt#a'޶l*ejS3 @㽁 T3fIkvȷbOT2%#ŋ!19x3&x|o;\j=Չcf tlYTnqN>t&qUU#6]^?&]YigյC7n͸ѿ]OŞebbVy:T R#J ߮蜳H73c"J=3I>f͡uza YfN!I@xxg' |nbd~R!PA~<)3OR[O۾yj:uh2`8`DI:(!dH!VBz;(H[wF\ ~LeUAD2)(H('N}\BO]R[e5-uJ0M@l+0-+h"f&vFm4rdwb<7Sf sΩNR F}E5bO'?{{d J) _HJ-6ҪZRJʚTatCBwŌey0I0&䦹C6>CZXGMGy~lW 2147{tUJ)iJzw.?TdN1}o>?ڇ鷗WyϺfa\;cWv?2ۆtQguF6$ )^ Q*[톀JXN*YN['1l5 A+O9]N)5(+ͳ<^Rk‘,tKTEe!_ƋI+v+ ]M{) Ř0Er GLSC4WO5rj*~AƳecP110F[U=tUUUm*>vp{ᬜ>lgO}no&n'>ƞsYI[^:[I6kAdHlόz<8ԟOd &{}˜iqQEGpNܧy Dd5xj@ml F!w$h*TRO" ֯>$_ԫ)W'f-6)3v} b胓.zuC8c9Ty[5Pڏq 4-] PgϢKBG腫=A#Fɖ0$+[}|vm h蠱Yۥm{Uz]Dti @lTe\ (< V52#: CUUUqLvة,k{qܵjVtH .km7Le'鎉Ldl Jt6kq&]]+CUÌf+*GZ\5Nn&kQZ M@B$Ox7"SU0d(clZI^]3^HEpFTFs6n J+]7yC!R [kK&AW ymY)84> vψ9WUUUgTsʵ7Fe٭|kn9Xcŋ+ngڱ6}nƘfk5S=BSJ:j5»&EQ2we< 4a8%&h!}Ր5lC` d;LN*!Ffx}Y7;og.?/29@pDQppmFhȸMM/P-OggSsoS$FX        ޵tqLD8eVq]2IfUUUo~aΞnztw\$[ik>K}1si79O/!H 84gu>Dqvyz:2P눜yOkxf//̙(G-}N9g4#~핽jX-k^|D=U[mɎ"唴w}U3[0vzwN}!7q2y](MlRmhH덎eic : Y>`%2HUU]bK-MCg+/fuώ0~f=x%o?sr 9oNKmor>q%Q ǡcTRTjQYU?rs[_9qv21 S]0K7ۆ3͝dzR+JʣYúz½ܱQ46ȱl۲(P$T?J%m-UOaоw8Cs6W@J$\5ҽZn B`8!wvNA1mTq11$n|d hѣ3XgP ^SaSy9_sۣsVW_s3F1r&b̸q=Lgm(C @;!`ov"Mѓ$p9D *KQC Y1^, !@mWW*/Fa+@j6|64#4B4K!9*$pe+b%G"5s .ZtD@bZL~,u ʀlTΤ&:'SKzKӏ^?+Ѫ~zDz"[<]UקbNk:UӄӃGcܰsp ,lu;~_PLZY$c-m檬Me;%4ϚO5 8"\zw%xo~~O뱺qKÈ &R%Z҉Tú7of%}gLKvUsfDg h)\ї]DT @ uFcX7 f1X3Zz3)j-n[B@X@*9ل&nOOBtkQ#QJBAP5 ;ޅ~,'}eSg@SqĦ{f&\UUޟ7$~ܶ;ݻrKtUlzQۜq}l;ۧma{rTg sjϮ{*su7M7&SJY/ \B@;nN ut(*QWҜ  Cֺ5UX,BI1*C09F3.f:RYWGp.\QrlHno{%G!-njwZuOס)>q 47-K20F=r7THUTV}&] !zVE  OIeUv cI Gyt7QJpJ3܇ljw_N!S0;'4rvYJ+"6Uu[SuLSP~L0@eKbFbwv,lWUUutt巭,$x"/nCNU>š\>\l]>GN:MB 69Pt}w+]= V95.U2y ! :a )7F t6噀2"ۓoOYI7F*dB)*2d{GWV&goeah*$VCsWZC]#2U8&CŕYж K  ^R E4 ˖LɕM4p8͠3UUUlTR|Л뷡7~1efu=NVw;7C"pם (B\j8C+/J-'xyHl3}gj'*"`hD*N՚Yi^G9vHK`1Ļ D`#Ɋ{*U;ҭ Xr<;ք̹yo]mmm4M&l5F_0+Cs~i J lY FfYUUU''1bz&Y5鯽YVp痭\Kqxo$s1r4D8|)vrdLdNQEL35UTm]ƶowUfې1Y5f.P%w7;/ BY H1x@tTWk\G.dZ0ArẐ,LI)9Qqʺ޵q .lYʸ ,Bx*쒙% bUR)4e=uytqLs,//_~]tmzEXwWrNWt^{;U-`vwĈx\fNmymx1SVy VUܨ8E[XLp/Csch {BVusA 'SR$0VM>W; Ki: [1-qNyjmEv:lV.luW].^,i A Xȉ&%K:TUU)#wwN6'g P;?bR9:mk/Y_Sv~%,ΘΕ3=(V"Yh.VʚNu5lj!EU T@`{fg.Ȏ)pzPf.r%hBs5bpNA {&YmM=n.P^XT%ϚX8y8dϓY-16U`4 83}+ULH\&7hƸ b!SL%@espyeD#W32,9M[6r]2va4>Ltii`Ϫ'aEj~Pcjr?j=s+R@茰ͦ o2+׉f `4 g?5ܔ뮵W~Off/I-X #Kgž/{U2g!i{$&dKc QRJ`辛*tɞ(5nѪNoub<_T3(P2Zƃl#92H*:$TGS$^p>$疺h.;-wRլ2]='$:>JsE't*U ~,}*rAMږm X24UBUUmѮ3!mupuN};w=rgk^2 qNk.S@ikB -,"k xlYFIPI]*hj,l~\ÐFKX9 M2FfU"cQ1QQ,uWjeSkK ـ0"͎3.-Lue{ ނMޙC5 5#!βc[^L%+hX[4eh<45KP}ᴣo"ۏ?m>)ERu^~;lk9t t(䎇 ^QT3xAh̼?mEU\Uӯ:Ww4Ӭ,I{m,KƤڼ^zDsQOURޫJ*/]o,F̱̊cg7@M w\ڳ)2A.S %"޶l8k`@C۲)84 ؝&u`fj7SHK ȇoF~ɫ7;瓃({F^9ɹ$ӳ/.jGë])W1N^F=t7.ϿT tKڕ Lה3%7c%$匾Q5+Z\E~ȏ YT); #.ctX8L(Ai'|u&:"L'̽Ĕ!~ +{^iQh([Ʋ$tAxq=(:1TEBe[a=O:{hzmm ROq_\sr)d^8qd.Ȭܦqv% sk,8ɧLicNAi5UQS0, `IBPhѨ&#KD$Qc@7ZGkc!#&G Zcm;\p,hQ~luJ&XlmXA3`h+=YUUfV5e]}1RQ>,F2kqŸS0粴uͽ_3,/4 a$d  WUM[ٳB݌$ɡzrZ&YZ/ 5p%,P@:2g V BOV9W= ]6sarU{F7ZfP[iMsH2{"N =ni*e-y)F4l- 6eңKHHR8}=v^u?ooo mycUPdY]YYY0w`g(tdU@(XݬMPaJLQ/khj n5UjYȥț3y1YC߶%:hK-TQPxEE[B>R9OT͊y]NTJm6cd= QD eFq2ݔ[I ^޵tG83VЎhZ:גThDhsm325$TUհ1uxt5DVTlGLOu?zZ}6԰a{ʞIWǞ}猻""Ӛ'ߍ}v:*Ny*'dNrY1M泋vp?mE:g=Ux``xDf}cJz_‹Y>r1 ahϦ\FCyŁ8 p4c\XDC_ 2i0Mn a@cٲu!=TE d&MwrpUUU!5ؿ[eo^i[;ǯiiXuZYrkN4 M/W`r(xzz|a$LOW13͝dŴN.ވ$=k99>\YJzRUR!Te~KL=S!ZZ&i$N GMl ,`hb ̩^q43 iV{,Uod4:jvj+>|vHyyAT7o.ܶ5ϗ+ޭ$3k͞ zq/Θq{U՝RE- 26-V+ ,;v,Ws}ss7Jקm>)Wu6Ah!uzZZڱs^i,Zٲ))49;tĎ&ٵz!#{6eOإ]Uk\zo.Wuk=wt_lRVjT 5tN۳RM2yU܏;luޅ+{:z&6sj6MN`$t=5hz>J-]#(8y@C]NjǜsFQ`Es 9V e9b _FØu A°nXA9 Q]CUU^)7G4 f9W_-n,J+Û2Q.]8 ꄪsɎS#0ΙgmdO,vI'ZW5ĐZg!'))tP-9Hk6v 2ae`]5 /LglV$B$*"z@{tl5dh4Zjcz&&U9-;G; @(w{!>Li&V4~lhrgFCf0wֆښJvdP: ]|z:yS:kcP!hAU"q8@VkFEmj*βw I7%T¹EAk='l(֤Lmd 88^CRX8jU `$,ye0u"}csۯ"K.Ͼìq9 Ce6$1aS5eC2c4pg~m@Zi,Ӓąg'DSŪFccq3_/oyg,[^Kr>ٺ12Ӝi;=gNǘl Ӈt>ifF֪&̟pȌTFmސQ lnT%TSj1073M;sh^1F,@j0;D(C6oǜc\9׭^Lq@JgT°3{'jc"e_mԲwRz=JY&5;(_Wݬۗ]`>Lz"H|N[6$"D&=+[6_ίyֆ A՗Y$?>h}LgV]s_OoNu4Z(%JhQE/٢jC' 򆤁u\Y1xcz}p[~s"bh5꟫RW/<Ѷ P$ /),#-P0W÷3>뭭}qf<^i}8MⓍl6!`0ѐ,e8Ѓ'wp̬&$.G *?D_j2t?>Ǘ<)I:MݞsWVa6c:e[4Lj gI|g4u\I׈luOwyOONG[Z`p~顒(/Qsq'JzKv%C(NuR2Q!d.RFXC,9Cu x&*DE=e_B2#6?78 ]ĭeh׾V7|͡c\Ә9a݉g6ƙ-ЪwNp0ܡa.n>i E Dٲ>$LZՎ2UUUώ&; +t8{W͕q^?^zx].Qrsgy]OY+FY)/ ~oSSλVM m 3jbVYd<4{@Z"{Zu;;TLʌn}aDIEJb|BėBpPoK1<p6@$`;,A!8Sx9Ft.8#ߚ̟pؓf[^LUdp.[֋<tIZ4vp1C7SD*uʋQo篶 -zj{=s72ĥs9e?iqhfMT/MWT:YȈ:^ RvJ ^ϕvbaL (n6U rvmTEV/@hBL,DJ?ߊ1Z8:GPIJ'1U-4|4mA'Gx7t7-TM>0t'h([2v q6J/Ǜ$ T;t;;WG l<RjZ7[Q#̢ gH*Cx.4c[ `Wg3i3aj\-k  3L%h[4MIgf6UUU{FL,cߎmR|fYiw-c,\m}I%m"QWT!! 3iHyeP3>{(k{ޥQ3 c%T(I,[Q*JEH'z+FX~ml2 ߡk`R;pss[ɎlL޿':p8 WvhqZče˔``7|4z3fpI ?~V+ϫ΋yuy{/ΥnNwO}p^}s儒ӸC+Nʦc"9UQ+,i"IEͼY4ս#|/=psMR7M ﰯx0螤F%2=NY [@A;@~*qޫ-YTFt(ƹPL6Apd0в.AlZ a\Ls>9`+7/Jig@ƲeISr'`Mn=Un隦8\7ѠC`h>dB.]U ss3 E3{ ]}s쟫Ӎ8c׋/rOMFġ@@%-ҍ ʉp^&^Li @l\@LVUU՘pfռEw}zر8}˭\em.cLJۺmFrb\è7R81bT4]  *V1 }MqE9Pսn=0kL8ZSImIJ}ӤDT'n[B1ߦܩ_gm>g+y⇽(>4o-FNЌ8X;,h,m NӖ2)w?3L:&WUPA'0$Y#wqw~VDrG7YI搧@m}UG-/O\[?ꌡ#:;SRsI 鮬8Y@ΆXRUt]C~bZ#Oֺt8#̝!0 1 @1~!<% +=Fc7vmގF&P p&fJ$l `fzH;| pt~,}Ub&g{VDҰNfR3{Y)߹ňw#7O8/S-R6e1 zi!δ)z,!39=L+I uf-E5ɩ+G]S3j6&915#Ɵ*hlrQ(?,#&Xf2I($KUyX]ƙgy4RxG;jmǘsʎW{#b@&Ik\ ! CCPWrqQF;c"~li@ "И¨xnsԘMހVAJS;Kb)Xq'O 7}VZwVo~]l|'#l܃F=aT9HN PvNHD u'[.]m3/TE:)$ȁd)c,9 JI_QXbKHF0}s4%B o# ؆N `"'0 ϵDOggSjsoSLU      ^eDz=B1mRdA ώ2lȕ"Щ_‹irxy8+(}QfIϻCF,QfKH&X7f&΂aXj̱R5j2ɌIPLUIWϪjx3XpR""/i`Zs b_IWۿoo~5N1=XjHsrtxSDg$!8:VȘi$S@̂~Lm A.,axLuJ xgA'MgUUUt{w92.S/3V?oOhMF,l\ׯcK<h1@^\xTөx8^ v꽴@+ LnHa# BvjqA}`"d GlRmBTH*O}=fvbd,q)(4`al?E65=UUU5hY;o:m.4ʃZ!dۿxY(i8LIC[¯pr<~f©|qO2BOkjjf~$K"~ӯj)R@$,9 X4R{,[ٲ?4:3/.鵣%2 ~ n# h6*50qD 䢡k)\Am{'\UU56xզjS0rwW~jZps^6˦hڸ1-ؔk7,B`gn:.=1L9 S.%unO{2ۼR3B)磙c<#cمeA<.RJ@0V2žn_<\ k4J 0h?((eY  0|uQ^Ӕ1Aڲ1`@ys`g(d:|zwV_ rs?S7*zxl~BSӳ=۳j|(UU^l9+ P:izkIW]0S1t3n5 $@;0t:xHXHI;.R`|۱,mJTKlB^ H "ng>ַ9qNW]<~Nc"Q iMBB6釗 9# v޶Xsf֖2l&333UUk 0}hjU~=M&:k_헁Tv}-)~"CML2Ff԰.](}sޤQw/f#gfnbtQ0bj??=bfa8Y $茌b &5r$ZJSM%msHZ"^DB6f/!x"})1! DWabd[3"l7 m@ecP4G12iF/*uP3[W;kB)c=7g_]w>?笱YZ9=IV9=L% Zj k0C_yvVP5J՝ÚXm4 [SUcM?s8NSN෍my)uTH)EFT$sB*턇K°GO3Po<`,_ cPRRsVrk 4[NwƺA5S@U(S+T0q~[:;gcd&1TUU5W-fΧ#YhUsZ_ϵ؏.?crtͭ\hPE_t8yjbUhLlO]9SdF8[MwlMҼRmcD= 2GYenUJPJQ"j-Ji$Ѕ1ha^'S m4"uư'MUUUmGzNmu /SWwټ2Ǭ)Sӳz}_Jd7vu!WgA'>k\#ك=tPҔ_֖]=`1thϥ 6f5 Vk\ڡj}3.wS*Eyu!YJI ,#xc~klkv~G=Pni6 JJ܆1i񟘬ܮ.A,}J b j<[>gP0D OwfF1!U=G8~"2Z1qvy{\\/ ;hu;'t@\P*b19ez]"Ҵ@7=f觀L#DdjPzFg: 5HERyPHJ^%\E"Cuv\y~CYpqkcҫE4oluT*vv "`ip 49#ʈFUUUua}p;} ۻޜN_T= \˗xYKI=wH_RRtO/kή6b~j--gOLu#f}5-ˤ`ML:]+GNu|$UxTL' eKߦR.t#eC+p !9^yo>y0_ϖ?[OSO]X߂KԐ԰欗bv>"ᆇp2܃`J6Mt g ww}<v *9g#;{ff ˆW eʽuoܩ+99l ]tCU%sPG]9&9a`ShEՏDCM~,RŖe,S ل&d@.s%5$BSE-Gn.Ne?p.gAȴ=@OZ,c8g8 PM31ќf2̣4bTEBF_R~e*_Ygw^;jjCdU yjnʵX8/ca, "ffD[ zš7Lг&B9g\\H75NFy7 6_@^wX,lnqb`aƴe)KN@q ̌ZUUU>Cj߁/^S,Mץ8;ٯmk^5n`grƜ{k?H"DKgʾj*_dww$ҐY3_o9EIUH^,,-4?φж,ҏBREc9LJ*N^q+Lt̞OiۖT,^bG 63fh, 얹c@{sn:D DZ>m aKԌBItjU~|:KWՇ?^}[~#gk[Ss7IzhCFr g_hu꺒5 CU\Reuݥt}z1o]pL92ף| $Px Cji/r,r ,?zRX>^5ktAwr]mn|۔4 D} im#xgfqn3 ޵b@c2ih vGt` *m#}~Vqo]Ƀre|j љ2F9G9R7g[wRw=Beeĩ,@)Fhs;>Mka6BMJ 3bAvgyQ _-g,Ư&U*LTRսǪ'USBԯk; - x>M3 L'&ApsBv0j8J034*fR>iN bXe`P4hS8=339*RUvܑ7ÕWH=;ƌ;Dw8YWM膓XE-b;40XÜeia LAƖ)43h%$TUZ|w忾|T_l#kr"j!IF#zuƂ]'X1ٗ[~8qW$/ك͝EgcAt5[w ^߷Wӓ[R,[ *"T_~5%-%#Fغ\7:9ѿ:x-ʥnrٮ+Hd]09;h&XцǮrcX4fڴksEXAٍ(3\UU5 q!O zKVGsڕGS=q%{7ou[srYSybTvibc@W ٢jo$g4eLP(Ѱ$hjb*]EW%_SĩUlYl]Vh2h"{ws~<1ቋ3\Ln\4!1i.FVΨS/3T/n%F9g 4hZʴT <騐]BUUmG s=R~MVƧQ|vrggw-SJXM4iyjVȚ~Qߚy9!lHt7v>|S.];JRt4:yEfISlPΩ-xTvQÖwu RJJ$ZmoÃ`0<T8Kp-Db~&Bd0g~nt~ȣҲA^Li JaKzM^hI\UU<3Mҵח+`kO<ց?-.{qꕯҬ|yۙk|ϢÚ]VOjS|1YEՠAC1XO'8x(YHEu{H}!C5U>T*EJޢߵkV'jq?VhVU4ɍ=mg I T5EBu] ^LeN - h<[v_+Bl B"ft3kSUUWߒh=n X|"qN&v:w5}ͭ[~ yku-/Ulv4tUL3x {#9zkS]n80Gd]rYC&dXzhFɉT=䯞"ʏ8$n\S&v6_`jm"9۱?1JZ[B tm޾>L\@L4[2&WqM̎PUU5u s$܉+Ɖ w{n%wj[^ƒs\\ͥg̯;{d<;CX`$әdStɘ=*bJ,dN}U5gia^\/bh<^ydsևAEX2` pyOι[Khrcܘhܷ9%;ШA\Hd#=:,%%v 2 @ٙQV=lH$J٣w<[m6cw*83N)^ffT{\IZG9fq4Df2i`yHdjZM>@C~Lq A .k}0H h|&yjI:8$x_]r"1^q,4d6-K;@ٙQY JsRػv|l*+T"C*mz]qFu}վPUv_HE/Af/61DUk6Qc9E<=1vQ&!n/N_PML'3¯ҠFQ$BUsTJ[I_IL;G8arXkF WُE2uc2k'NY`O6ð򆾣*icp^,%Wf@ƲeISaҫ ir ٽI'PRUmN%7FN>NI؎𞓗:u?ȑV]x?UܒRA"2*3!]Q ITKR"erƤM&(#zYaAƴP|">+K*M#M6P|#qY}mЁ+w݋C.&GרsSerJ4n-9@㝀QEUUUwgh2-K>UŇMBJw%Ky S_΢FbD j=EftLZpLW٩jd,s:)B( EtIi?e2fe'm,S $nc(@8%!-!KHb4=|\^a PSn g2.>7.dt\Vww t~ i3[K駠 H8Lj%TUU5) ŻS>FVHZX¯xk(@c1sT_?kesW5e:TP]MsAyNSLۂ\dj5U/3z>CJ'E6BQc/c@8 W[9z뛎inuԲ8 l -e9p_V~nA3L)MƖ>M=saMM]LUcZ,G}ǑS{AҼھ/#:ͲEH3Yi!Q#j 3Ƀfa&)Gfo?=F'IiMOY=qӬ32.N8NP0]*U{Y /w!:m@3؟#E=SWr͆16?u2\,Z^Le ΀MҖ-@Q#>ɶQkEC/ _5 {8zo=ms`Ks|p"ZUVYCH`60[YaACE7f蹲A,J&3)P='i^uRLNY^k)ת(R*JBJʢ*$D N=&+lI#jڡY5X&SNݎ0zv*s I %~,(1iX[:%LCw023]UUU*בP[5]FK^݃?NV?ќEo~V'kLGrFM=x`DqTB vմNiQwe&*$Za/{֗-ݥ=2$Ow.u9ǎ_26,ҴJo!2FZd]1HݎXC^NZzuLnIhD!~1L_ˍv RZS^GilƖ1AƳbUU\arqW/ۥ-Ka8UTU0tqfZ8qejO=P q,K7љ̗ڔȄ%E#;Z^ye%tͲ0HlWWCɓ2"'{ YQ$E5H-+ywA%K"n527 t(4#LFᢳ҉0 *OuqM qHƅ޵tf Nk)J@XxNffILթ9~ӿ_>Mǣ=H]ys㣡KZJӢ%J jNB|: g8JCqGElS0y%UJ1"勛C0Sni.p2`lY@]A-hw:AUUS1<.Twzx]'ҟ#6PCVԮ uo!T1IaD. kD!x^Io<c!rauȶCjJIJtJ $]]1}㒸P(^ U{Zua! ~ PfE}}v~eJR`iR4W #3PUUPk,˼Ŗk]+ UCCt>{_2ΥV䔺N S"J Lcg X97$~deꚦT_8+ny5y p̘Yw^0 Eh$`!0~D[nUd dg@ ۭ ^SَkĔ%t]s ٵȬ^uL2~ h=42:YTUUʪTViWb!ٿX\ys~{n2q>M\VW aczK+Z}lxdLZG;nZiD XoԠfٓ1(5<=Wf l[F2by' c!7 \Bg|\5sM.0oN)l7!83IPa;(tsvLzT"a!4aK 잙thj4LŊ6Q[UMN;v״óW7 mֽZ/U,DI.2W6OS'%kij dՙ|]M-5ȝ9˙J[!}[ f&5G1{}iǻړ@ ?sϥR* ݖnN Tm^ΏKi艡o%1mCvt춏t1481Ph@|.[>]2(E,~z/;hQt:*=䪪\叓5^/\n7Y0zs)ގ罖/9ys3jgF|VVHiP}:}=c'{Lu4O˩Zh\FEdKǠrbY{m6mB"[ zrJ-u  :PŊ[wÄC<Z9iOggS'soS9߆.C/.1.,/6?;: Cfc:;\+'tWy+-5OHL@ʘ%3gc7qlptWDz&D@՜qmsv!3=\kKilӉMyB}4Kmb&\ ̸̲͛]8 Wjo!F˰P1RT#T8d68!tq*1iKC<$Ռ̒GN Fʠ`%3~K 3}n,;BϢpV/PK._z;6i{&ΰzu~)StSz29gZM]Cc󬔊}BW>bͣuλVul˚MA*ꦙX sDu[<ׯmT*V}X-WUѽY΢mbIcoG9/M'+_NM~ē195/}# 2D`Zn *OM.@.cKrm1k@̱EQg}Ɉ.W&!{|I5.{`z~-m}xdKFA.{ lBu]$UՒ*YUm恼GRWYPf) W*Y4w[odfb7{4WIYp3}`d#E^C*(tNPP&@ڭv͔B+# Ĥ'@ j_"THΘ!Ofٱ\vc n]I7ezĤ?zr^: @vX`yU*$eL#l/ AD̯wA?[@vA`6J҉HLWaG kɮj[VWcWphA+ג3z(1UJPČr@QE$}a$ԂfbO\Y)P2@fJ@'%Es 2g9gm<628Y}\8]ѧB[ HgJ""ёφސ :B1jDî[0<3*6]Ӏ8ZPG-Xvu%W%dkJ n#2\1RUe^IRT4@z$Z yژ)\Y~_syrqJzQ@&sI[4]騯TkRmrhz׏P0r> m0n-$ϹcX3=ao`?@lgn_tL _n&8fp_x Ge&5NXqѵjD8;I;,^3|+0CN3֩ʬ j$Yk!J H:HHU5j#W9N!"Q^t {ZGtiƲ'$ )P SW Yh T (4$4⁘UnB2pOX+(6; \YלKSꌌ9Հ=iΘS\b2 Zk6@%X(H1zq{aeuVtb<ŖAVCpfm$)0{S{8Zٲi #\5LZq~7=C=MYv4}5cMM=(`dXUEEe{hWHZKٸ҄L%={adW疷 *#^?c wB*16A H"}! " )0W?8$2#qmg;Zo2&`%PGUBon)e/d󫄾badnI%Fh1MC$\.D F0V C8b`s}8yU2sߏ>Tv^w^,*r^ 0 v8ɬב+PE ciD Nɷ^ޓ=QUE> 7C <0w @h|!jhgΎDM7eaR'ԈQq0rKAIjϪ*2@KisVQPM*** !auB0~{,FVkD%5'`k@L&  ]i=ܜL{`Y1!a,R uh .-A,NqwYT{YrnW I}Rr,#hC8tI!Da1C.܆݇*d;C@PiAq`}W ;Uan8X;5"dʱ3f(*Sڞk0d/04*UPXiH X@&P `y::s `.cd>$ gwMKxڵN3*0dnȸB0IM9oK95aN}y.(ͱŊ!RO,D%yAl!K&Ӻ*[hdh(m\j._YҧP*Q=+<嬥4 Wu_TcWRqPGԀL¸ua.6*^g5JF H)h4ja {Yl:4: bȆYl~j̚^0v}6O]Lk. 8J sϽ~to.O,TH GD$O,!K,cKY8\ QZM W{P%[Ts@*;:#3pʺ WauyݎUYfekG;h`;hC^jk3?JBw|[^ )\S_ea׫PsrUF}&sJpڎap{q0~~b82@qT$1<,^Ee&`$YwmGTR$\d61ixn=Z k t'D *Tlnj .I Ga'טUiHݭ Hd7YMUUU@Ax5ViH͘ D]O 4 CM|j3Ql B0;O<X=@؍p3q5NӐ@6a{E *Xt(0TUU) P$8 $N/*u)*a%@ieZU5 4H\.eX6BJ6Djf:@{/b>`" CMu ֲd=>q*s?fTx)U`n>G߾f}WyRA?%zd#: Zy4-$Ù *ifih{K䀃W5aP6e]j{U.v#- 7,5XUU4 D$@V])@cvy1S 2.5)êćR6fcvqO"b&ZȅRPxʁuBGqffs=R]d$kuJrI..|YTD|ɯ|1,g]$ =0z\|(V!~yf{pF2`WQaս#|R1UAf:,B <]2 FU Q($Q@Ѣ@t}φBUDi,v\~D$R*vzXF^8L1Y\H;Fvy1 ]xyFa:Lf:kAMjk=n6tSB&X#Y2>͟9jh-]' 3Jsn[-pGe#XPԔMQ2B:TCKY1T*Mm(^ԾF R QE= z8   5,+`t6}RϩdΞ,3 #$C4/u59t~Ծ'5w)Qb1`d 0&r2Kw&D)~Xm ma% tp @.q@GG*ћ[kL":RzkG\$7Y0IJߝ1DRE@TrjXbBU@%YjTXf)2geWШv`}mg[qC8g`Ա_4Uƚ 5{[Y7HROx_u X[mc7]`(s i[O'@ Eղ|`e}En;@P>':REYn~t}^׌0$j:1'u2m` RU%k$@D<8.PQP K?XH%!.͒!eI;z L rX>B} gۗ1#V4yT,tG^RX fP^Gg[ 3qRd@X681뜛;̓ .x oCꠟ S/Po6dA,kT&EL#qG\c&%G#ǀ&/p3"UU⊉!T(LӮՑTT7 #c"5ex~13 ;.ר]YZЦPj9됒Jb-p5o\f(,-Ձbj^PmHT6`" {'ڪ#`ǚ Ƕ17C kLWlA!]9;6D6PW3ter)髲Lу;|j Bw9 HUUU{"Z2Dn[4* Gt8wP#2G%s`Y5&+vf(tgANUUUYMNDKS!n%fYȰh[u-ꅢ K $A12짒MiT;:=T&Ck­N9{ޢF( W gcdbdy%M0b6檺,AWaOưk<&cvp SzKjPUHJ$#LR>h ZԎ`xa@!]:V/ ,j, dONb`v0@JH򯆾 IC!\[5X.ߚryMNM mR J-Lc6 lÁɫsc|ѪlXu%CQ`Weazw:ק3~\Vz%j@Vn NLUURd Tk(#N/M71)ЎȞoc,~^bCUzo뫨(Y /+ʤI<Ժ&oI OLk!M5坂U^2f2K-=<%`,gI !`+,?spI 45 आF7m 3 ( GVһq2ɛ{P桷>fE4Yl'0TUU"Q,Q<)S#t4@N%[dR5g[B \0i**dJ` xl`Q"]HǷVPʽg-s};( u-y:Rפ0 rFyI@@*Ʊ lӀx偕5erbHA @L 7.`KGe.n),=*~;J,tHUP61 u@E#>JTL<sύD$P @Kkz}nO^^&v)FkN* p }Snu&Tv)QCӎ4 ߭^ l4J t0"d ׯՏmc[`I$^= Mh sXdg1;UUUxKTI[FHx Q)d@*vAZumXq$Zj<&| Y;Hqy' *f9 05W TX#yOo vͪ1K;QݠS^I$ҳm,<v![![غ&M-ae^`:CnA܆bGř"xvQcGEcf#n,PѓQ$UUU'C C D"WMPUzjE"E#}U:ׄ}K(f}[U%g2bΒM6n#g( h 0YY6dגa2zǜrlF7`)`x7mJ%@lBB/UٿbozP 4N 9n~cnvmÎ0Ѳ93G*Q졟#2H .ʧjZ'5bŊDWܧRQ+ Ɋ'h@`XD$16~5!=3 ^  a6);BjHZ,zL(.pf=~ey[ul{*V{QH5s(:xe:$3@h㬆F9Is&=M (_Dida;M`OggSsoS \Ū:Wevj[}PQcW]qM'raMTAdI݄!wj +Ja0A1[  n{8c>/ȲTpcg*RDU0Aj+BziV6fϺk*e_/|Ȱ?2RjQ[ߠnMu ʎC_fuy+N=rqd,@~=/ W4F#G gjD |q5\eGxS lT!U2 F PdAKrSh&0DWY ev 3,kڸ5 ac{aϱ 0_dYds z,1k"*I # DR(I&e#ρfd /0,_K QAdOc۞Ekր\XMx(hZ"*8ma;Ge=,iѯǩ1QaŞZdz&RUUQNԫN8 Mn􎿯^3g}ֿj5`A$¬/F4t1 Wט]]5R`N^AlZ2 F+Y PI)i 1uEp{8ݪns=vqX͙ǭ~P ]0 1 l~7---;ԋsT uxyHKOF'SUUB5Cb$YhcEvV RF<"ק2h@`B U@Lԥm̳j: M7Ie0y<;2Fao@e"hӢ"@͊BG%T6B`oF;F0Tf_0yg6wMuA KvŽ58W:zFMq r׏d5C=r 民\UU(@F֜Q1m6eH48}eqjp5q5U\b[4FLSFFr٧h/ڞlTy9)4_)hZJ#C>58g~чmJU˶gׯguf hb=55[2&"%JVWAyֲwìI]f7-eqSjL&-daNUUUjGMfKxaׯ_Um1G"QE@P p(!IP.EuZ_~0{jh݇w1 anSLּřܖk}s<N6 U}Ji$+լ8R.! C{2XpksYf@Z frv'N(k1 @DA:P Lt Y9kzs=5Ƨ-a^gǨ$W_XBL8>+RJ/Ve$pfMJ;L&U Hi4;9f mGe&vו}%$J3OLyx,;#5]0-eq*RU̕ Q*#f68j]#&x LD4$vK*16(O]0CȐ B+@]y9gڢPwE* Ccbm塡 Q@Lg=979 0vz[]x"JWHB`@|aΟ X 7D04hD",X&P717#A$ػ(G*íZaIk@87JۭaG4D(ӌ.s*ªJe@F#Eh "&Rl%xhee7侌-odL@&\=a7O@\{wU-MPG53uh3(hs A'\g,;H%VziXl̹Jdi W{6`4[3{Î, ƹPP4@?P cGF>5%uTb`z\U&C=3NB `(=NVUU*dmZLgqږ^B} @*ݣ@Bs1\/CAWa3d`;N6rCS@YT:\:>^[%G' lB[J@ ْz[**!JO&4"R>H`TfoETó64Q?V>Q=M#Ge[gqQEKѻ~aLd@"S%TU$rU!/P yMZ=fw.A) @h (ɨBU֠MQ*Vu:kI*eneP$QPX*~KYBi?g2ĘyzI`yejGf@ oU? hl'7^iԅ/˦i#_gRQ<W.a, 5ƾ*,6G <l4PUUU ' Eni7@PBebQU x+k h,Kt yöEv ؋z5P+ԙBZ ̹YYFfmdkRyܝn$K ama @lT(p 's(;q]qG[tk/6d$ٴ(|GeTWlH)٣L(VXd}"$׈%{!ATB:׸}Whtm?dx U ?mAH'cԹJb3RʗtF^ Jhu{>1@s9Kg>X|r;l@Ѭ_|&Ǒ$m[okZ($!AzfաOk:zuM2}1{PB;Fٛ~7.w(6׈MQ~n6(.{ H@ "sQUl^2oIT(UsNN(B%V0q_1j 2`0{*!O=GFdAwZj"*ԙgPf9SHU1H?J'R}G3U_b#I`UPU_Ļ}: ,1|V#g6/Y1A~7ř-%{wK 䎊2oץZw MVWxJ5]3iD /AZ=dCM)=tUfV vq ";h::^ dhpCUH &˦z&Vi:Pf^28#qahe`yMnWHa5xfZhMVabYQ1 Ylذizf!afBTm`|aAN|qt=Й?|PuG[;:*ZO iL.۰"U"::W\gZy%@4Y3|HwL !IůBew!3d J gSKn'dL\ 홨i0P71S1k>0h]iL2m8̊Z²b 8TTEڜ.$iw}/c[  zTY $(i+1PHNl8"G򡇪PD12g˅1&+&Cfo!PUUڋ@{) XP:V-yov*X^D1@"'XuդԷ3&b PW~Yb@;V2ɼ" ֕10My/V 8csLy߿Gus#'F1` _lK $ ɯ #cpf(.pjF Pe8*iƫY`LShDf]!LW&[br^&vK~V&+ޠTӥ\CTL`f~QxNLPZn0RR*HUEP HRbl`R83]z/I!5 La* HDLcY-Lc @{|j|"`A*z.$S_;h0rF,jZ/[ 옆X;Rvi*W\JUe}8AEeN,l(3[v6XJ\ B)JV)A #@}~W.MS@`'PkR˷i' {fPPz1s̪^sPFV__NVk1`UNn}!J·P#-& lpcc!iwfĔLldWe3t~}F8Q#Wř#vס_d@#*䡪*+ fح $s[#դ%F JE 09'f!Ɂ.᥵h1v}-vg,l@ZZ/4ً_MC2_qXWu3asm-^PCHm `ܶm@ 9d< ,}YB i6Y9>ium5cciA|#X3BY@G[{R֯ 5<*ϟx]z5cLT3d@)cIƖ*2-֚QőB0JU=\U1aԪ-Bw `_WRcJPvU^r"*}Ƀ_ͼ9YSQ4jl`gݼ/ȊY#7F-A)BU}%!D 5H37AC7͆a;kh QСXdvaG"CW:Fx)Uif, 9dB24=l{*eY @inx&vTe&7TUU ĄC5m|~Dk fqn (/zP6~C!>ABR ]uqHh/v]1jfg LWZM^ j`+8PTʈ"(;&EZ-7m\$K%L7Xؘ 甫Hyu=k4,&ٞ9`Jѳ8)j1Rb⦠QUJ)5^F uVV_ZWy BdX7,d7кYQJnlaH+cH #\Sǘeh~7ezמja tM07quBx0dt3 !Q(#$!*&O@H)uJl[HH(MR,@cS%XwBg45MR2vSc8{1x{]F^ЀgPc$d֍,PYҭwsSܐД:Gq SG 1JʀWĚBLjTV%{M?}dj{Uq5*Ԉήufs} Q&BdT%}(x^B)UIUoW!`({ȴ(mKjO\Rg)Cw]Nra7c?rμj?ZFVh|˪#UTElɀ~e s1! x͡ ےI r>nJhGՙ-uy j@,ӿzB\352 :AɒK*R'kR^'ZvWԕ@-0ZBvˮfJ ` cDFa,6P,(>0({e^2kT^_}*'lB ?-Vd%|7[gaI,p-]K(7okǷ03+XJ?FF`G7E4kMj(0 f 乼[j$Yȶ֔&1||(NoАG6Ԝivt Fnߖ4)QՁup=nYpcKfpB*NX Ip\SfZ2${"UUv .8#WTt-Sh7عޟu(AQÍqC$vh0L:p- 9.شbHUUB<&s&Iff2-H4g^ƞ2bVw;ƦPe^2ҝ{zuHR_D*T$ ` B1&S>'& V!LPw&8 pC9">7EֻրMtպnǭo!30HUUUL(B@=ŵi٤DaW6=zB, jMsdh;j?r+9+VFżIdπqErITNs2V FM1oܟ=3l/#Qcf2-YVS) P/RK Ogr2iI+* hfj;#A &bK;S@6wWv$-H)UQv؍kИNdtBJ YȔ @Vj}>nYKU `ڎu0C0ԪUjtyÆ% !E]Xps`;]V %+2was<T~~:bJ.1mUuKn .UR`075'dFUέ*d*dW$i$}=MPM[7+X))ԈMav-3l&+n!tʉ*FdT giUPJTNM&hXD2VJ q0$'M#oZ1`5 G5$O\(R8r j+IZs=Q e~kF-O]=bH /Soˊ J?k5Et5aDea 4n&Phݲ_iQG5: Pg<ϖta1B=)Y`MK bUUQ$(l5$ x̄dZοmsD !n?$ b;U06hW:P;* (Ia}NlREen_]eN=)roryi*A6F/$UJ*-Ɇ#'`WО4B@PZ؅dzi nWe.1fXX*NCc_@Jv((]sUUjhXYUiLz-T aDХ 5gUP,8ת!LX,F0Đ<ȐZ<(̱L̶I*0K^ݓEY#F/~ut.;@7WARdQI2HK%K?"Øø!. o1 {6քF4FrGGLpW(I[X/jͤUxex'd /t2gUUU$OuR HvXHRHuozZv]Vot{,'LHmWjtp֬ƙa/NHHbɡ=ٿOm}z-#s q,Rl=X|`JbaT*_cAh/b;fld@o]&$ p UjqWe.ILaY:#Nƽ*?nS^;IZc\0}F14`"R 0f' 3I}ecH[AQ(W1|Q0p\ĮA}3晁Y dvoMbLTuK]aH-VCS09mjdRWe/u'WWKhؽ}!H8[B:҅?n0Us.f iƩ+Cy@]6~7C7mwS3Gq -8+ i h215 dй0CvԍĖATSi Bh(d UTr,I)q 3u0f2:Jٹxnh C7sEJq`F{m_2S>6[Wsu+2岲+c ЛІ")hۑ9GCFrN} V nY+D\ŠwҀ.3 ~7edY=5&qSbp~t7L5&/v]AKq'Ȭ& $4hVKCꨅ /r^@TVB.r,L=M:G+t @@Vc<*"y t WURsL(I2tp~+I'T8w5RRUUձ߄d!%iA!YK4f;SjH0CGw5qFYc00J0!&Eq^G[֧CjL 7}v^8dEԈL:?)2d $>Pezc3084Z-c|ÒH1F,;#{bshQҰt2czx )x0uxj*qBNBjTUxg]18G>@x#5j}{x*UJ#l q9+cK8!kڽdf=TUUm']mCC<.#DƶQv/sYd:T٩gѯ}Y_z*Q':r䬻g> 1>г8Nb]uڽ[>"^$=k̇{u{JV;=Iţ| `ؗRi$,K-`4[oXWG/sp[Y *ʡ7uh0Ӝ9bp1Gi8~޵r AU\@c#4Ƴ33VUUU㇅ b¡ɏytSE?m?WEmf>?:|ý{;g?\Sd0AA_ +p w  tۤvCAu4}S̠*;>Bե N<؀B'R}Y$׃SߴuGFgbrG-ϝacH=x acPe .mpQ P3^~3Vp4-]Caa!x/(todԙpÛ6t*7%v&<σ/uuf!cyV|1brzҦl6Cj3uũT7 ]Ȱ K]GZy:㩔P€M#UJIɮg,]vtC0HtےMVFn${误tt[펉Ƭ(sLO+sn3LʲJ4,RuҕM3ϱs~y\_NmC IfM(Kwwas>ތ*jBdB6y3"ѭ>@&4#P7C Q,Q24f fcK48NPUUm`Ily:_*vHӉc-VjfN'y԰Fd aJsge @ٌgqz/sLhđWd}3N^&%nYg d0kدߪ7ƙ 6(|;"$"*jǒݖ-kO ڒ߉xwf iѰMSF1uqm9SyF0 M4ii9-c")@tf:mO?#:'غ w7 nf[OO_~|ꙈsRa`"jp*'O:$ԉ̟>b" exxGiNힴUY<#Ql dbP@Hdm jJ}QgtLs979L5`N fWazWH3`, 4iN cϖ-I4"x/=;]JvUUU-XuqNs+X}bqZ̥oj_[wY1Ɂ!.!pi@Ui[69[FfcUUVdwt}l$k:A|qVֿxXZk9Kc:&Y\ȢÛ}Kحj? cҩ$*Y`3b hZ2I^֨:oޥr*zuIUJ(R4!EU"q{g ZTȅ"`#ES-&+ E-KaR4')H–MAH'n=Q;#ΞFm|~NnZ[ q#BzOy([')m..W>:ScҪ;sR3}:Y"S\Sa):i+abە3'LfT&&h{*T ! / ȒIKL mYc$TTT]^T a.'O;ۜi, bh #8 &g0 5ߖ(@>eNVH[ 4-Mi+@IGtvn*\?h\]>3OS+?[A۟a뫳{]O X;cNRe!%f[xqm?mu#Tz&b@psq1߮N_.S W))+"#abՖ$TM{/NBah1XA6ѯ AJB%Ǘh .[vf[ӈkߘ ޭ~m A٪iX O۳񀪪Nk]^ r_ W9޷_òyteF5Ɲ6G-U#FE u,daք2z {a ;v7 d =W%xP87Tr>aRBߔ.Q?Jjxn{=;?G <EbVQ91!Xg]vGhykL9L+ᠪtqЈA]cKݖ !Fr-2 ݓY즡\~cJ}+'}/_?[zmכ<ڳy4a'5*52upu7sd7!{Mwɼr% >B[춍8LɭjKt0|a#$ض#[s*-%x}$+V|[mU구HևMزӕ ra쑯cɶ ѩq,k65}ܵ,5Kw[H $ Ԃ=ӝ3on1X 0Pf(TRέh?x8J5,2Wmzg ύ(p>;>aBa0XlKʳ^eJވI\4-M_MbPWxotIWUU ~=vWF^NŸ޽zsldQH.yhƺ8e#tiJIO!鸡"avVU@ g1tz6N?_g/%c+OaYHyF.vZY&T ""Uz G59˺lVE[WnCN7R pIǼQ@b1g[~e _~eN*bl&cQWnvFf'R%TUGPOs wvsc?v!< @SD%1ɽxԒj^ieh?_<1PͽŏSꨮ>"7/㪿XZuj_דjP4U=]8v?-w8>2q_IiBZb)ѐ ̯6s95%WNTTq.]RU_RjeEHlT|?f=GiU$F1}%ov҉*`>Xm[4~LĄU=Y)4ޒݳSR{UYY=֮L-}]wjJNƙsY\S]idN]ʨW=*霈fP1 հ~5S 䝃&g'Դ'V;]=2J-б9eVueiej*U5i+9%!Z@ pn1 Ҷ2 >ɡX40d09n"g(ŷ dqavԐ~}N4-E;w`ggfI$TUHU K$?|f1{tbG^~92sd޻~isi̸A,̤{mjLP(L,N(Lt'^d3HZO'fm?됅vf3畧/@] M(4)>5TTx' nwad^ •!_Ik'[kX~̖b0:Y@ndrD̮M$qi N /LaK LFvrUUq1>d ==qΓsV*m.lgEZAzzK(f:y5 vO\u @@Yq'drcULC=vUUyjPaue_61Y`4R]{rXt›؃k);VUkvk iJ_6;%bjE6Eq !DG3L"CUUսy0>VUm'#1y?v4/?^K/I {'svI6)j"DQc{vyJ]YEuy+h,[4wH_dԈJDs]Կ:j yJUmoEd!/úeU K ДFL ]14m~iurտ1dwa^FW,qE99mWѫj]]Bs {Q zKjhs̊J cWMkd,!Wgo}u?; Ym \"I?vle[ub3cV .ez LdW!م܀X@U0_/.G{H{!! ^il .ʖ/I l@QN;J%|_\=ښ6z6$p`N]bB{ًBT PV*n['ר@E}ѝ,$@mO%T:4X`NLRNj.~C$kF 2u+T?U!-?TG-\C,_b[qgF*cBpr 2j>/Fp~N`YƲik@㽠hweTUUu%,m*g#'W*%b}={{<|\i25bYPL|C?*ffo5;ߵmQR7PW{r&ٵFqU3;"RvQT=< dD@Iز`(69i8wciۭ\<.WBa[8ij-A[ A ynPr`j޵*o%!~>ti.ʖ̡dH#3*c!vi3a?ે!տ>Y1q}t~_v R" (y`fV'MTU)B`NTc&J ^L }Aptq)L'5K!BUUUQwA^ӯyS\yZ}]sxY\֝ޡ3-CkhMI C3G=z5" dv&fɋ;e&uv 3r'jj 'ULWK2x%.@H@-3b,d`c-,kdd$~ڼ@^hN?bYL9J8Oqac6CZ!U>,*.WeKZFf#āPUU59uX)]ۋ?+&-K'mr2ϥshy^u*Pd"( 0h>t15)tc}P_Ԇ|ON׻M:ltn)gQ O5LZ'} HH 馍]?j+?{lQzњSLW1&W dLp㒼MҖɝ sriR?#OJO}jφMݷ@=y;s6ekdg"X.:Jݱ;K 0dcyowYH\S{043M iqswEm1q?my^⢈+BE!QyAT.깊+z /ӊ6G[?!#>0u dEP &DuBۺ%?)(uqF/bkL m;Zod&UUU}h*ŘTFvN~ߏ(_>9kթg-GJ%=2AHE^1/XYu]0LAnX<_0$Ӛ/tn59,~]N]3JU%ꩪrI[*,UROHH6XQ!X_3w߰eJsu2@fL%&!h„fvp ^qI*b-L4-8 ΞQhJ*vc 6öɼTK| F<u;\nԀdghhv ɼHq8A 5RXI<(>4qD#_98Vy.QdUUZCʸ.-v7!|wWBRjM]w5C1+wDQ ~ QgWNClO^c_kfy\x1=Vf PӜy֨*,{+*RRٯv[My; ?`82!Xs edRlUcԡEi8[r1iU4&@kH[66fB{؃4GbZ"1E^V<ؽRo^{<&|~X}NZ}8NCDzyhǔvNNU;j6?/n7Gi*x/vgn(*;{Ε"tlYHR*JEœ`ID c*6 _:Զ,9[QokP#&tƪ=ߎ?{1q->iubh([2 i=](NsUUUD% ^>> ~6ݾϱcO/m燭V8tb}yٶmȒ3f5 >lӝ5/YK+5(1v8s}^ۚΦ+P}VPUM1B58Ǻ[xEI<_=/6!i֦C<8XۅQ+ N?~LiQ 糥Ki +ƻv(K1:dB/_^?hߕUϩۣv N?{>]Cop)rDL*NsAڶGYSMVtUyFN3Y'fZ5W>|F}:cAÚȼ<e5Ջ`]y0#ɯ$X<\?ىo/7g]%wl&94Lw7b"Ⱓf G` b-wOggS[soS `2N      7lזNel6: 3jdf%,TWJo)X`d'GZۣAp¼6޲Vu; !q9rbGڵ1hj"h4lfv{u2#ʉ ,ə) ~ QK5C6LQ΢tmN " eKߦ$ePAx{ҍNixP7^5ŠW~+OՎ}0ju۴+cyyE-nGz3:YtGUB45xc4u*"&?vWS[7..$41kr#e6Ȋ76P^*OTyH)bŇHloWg,$]C"lL:̢$~JLTԴ` ~lu &}l\3=Fvfg$TUU#I-Z,~{=pa5tTz{oRPyUm2($!oƒ=T68 )r2qL}=ˑViBM)Hfyt )P12iqxy^0.x5JBGFb,Vg]}և~Zʾ'gl|3@nZZdfi̹*y8biDFOQ h<;Z 9{8]Iv_uM[GĮ;}4vѭEݪ.OBzB1qF: DݟQu"=@ ALQʄkl21-D!"!iZ:X~-(R<'I.7;35}>g\°i'pCL--u 5`Pq6.ѭ_?RtH?kC^g*84~ %xort!jU1tq߲?ެ^1؝^=Vǧv\ҦsKӌ閪frq{ (Zt\J3fƞr52wGqVb)*A֒\ g:^3I6=dܽvȳ*M +2X@H2BAn[Of.^{öΑ9^hj1iBw+kPIPxl>,˜ 6kKGܘO323j\%TUMvs9wL߭jG ?Ɨ_=:y>͇@aTmN%d=I~tճ1d@kՕˀ|B=0vQ RyKvwL7*jʿarzuL^ cKd (dF4ݭC>}?Iq6i_pѕ`]6uzVZ@zPAhR Dʉ$ )*wx=@[D4nGhh 3Z\TQwr eyd] f4R_4σ;N׭Lz;zi#vH_PDfBxv"8gAи1id'+f@w}HxF2kQUUՑ0qkͯt+xuPvK蘄 #OxJ) OyvTD y]FU4:â\^v^߶76v96M>[vyYnK<3H, dM%UqRߒX;uD6܂X0o6Jf.B n',Nd~=o~e ¥[xLq(+$8aO :{*Rmӏ{ {{U1rLVѡŁ{`LK 5(eE}W.X+qmgQQgmʮc7@ $wf|wd0i@&8h?ltC lA&rqrݜɂ8GQzk_kTBQĚhٞN$_v1^W8\bX++.ozËJN,g^ȼ<+W8EA!z"(Wj:uR !?C5Y_M6ޙfRy|r2Y9opTRY ;W;0F&#َ,i*yx5|$ KÇL Ma6iGyQ/!װ1={F,T%@ӖEjl!h7ybFIjUٍۛGJ{JNmOS:ֵzqcwwsd!Ԯr4fr&>ퟭFP:] 3{ZUaCCRZ=&xNBB>D@Է7]0Տٞ]D{rOE99"T*6#'iWrz9bS `Sr}'}s:u7-t `34~NVD 7-MxKhIZ 2mGɃ^\q~*nZ*)(&Z^-qG\/Udbft$d>p5j̲eYiFt3g; ~zL`$VMz`Xs!@ +KoiBD6P|:JClBp dЪ ۇ[Lk rZ$Ö-mFG $4PUU5^Jsloʲv;3P_H;;|42L:C)rP$JBI?*:G6 &8R24qbxP]dpEE3T&s՛L>ַ13Q:缽K#7+Oj ƒO{ nےq:\,BX ʩÀBBUN f;FZ 2>~Jr+87-]YC+#o ̞4 VUUSXk:#]eAxУnOӔeJ+O',PWA$f6CuSe-zԵsAe#Heg50i0ItҔCD=MMhJY&;,m.ݫ {EHT1OwiL }ǀz{KB7>YOę%#ELbۂAinaJ 4;,uuh\[>:4 v/IGŪڜ,4J2?~z0:A_ӰdضΒ`i_9/tzG..W9Meo*΍Y4cO ^7{ڛ|ը+NtC.aq6x'C0 |lWe̤3*d(ǯqVmm!k{GXv$dEȶAHD`+*UW JERjigu}U˨@/-dm'5;T@!  NCB#O,(tqL&aKh3ݳgCUUU]s=7Yw])OZTO+=~xqsxrVj/Sc1ՙy}[]SñQU#*<ΞgԾK0qEQ11{2Oǽ=!AFBJM,`wJI2Yﲛ1nj`,T,ߵӓ!h~>HU\l8D7M8kCK|3+ ]p{4~@de]@NGI7&خ{qz'zPG ? 뚷R_ }^.Wp1T:fe= P•/E@V2㨰8`Z;:gbĒ]O3d W珊G?AvގF5ʄ%)so ,ےR_6AvXDs;eΦVg`83 B`6hiQCǑ-~itAAPiMjD\h| 1Ȥ&xGgH[J~]V֋{Voݗ\ʹhRTgn<֮BKCcbiJ!m,[8-z K$TUUհ=T6;׹|ǟH/?xR?oS{[s)AhiQDӶݷ3N iejթqFiH1=@<3ISRgzz IE4xDvR"M*DϘ-,G&zԭ^X#٪f.0ǁUQ1|s* ӶC҄mdVpZMD,u fƵe-=IVV4h EZTl&r yk#frٽ=62W44欳/o{8f\rS̘A225(=^70NdyE>٫/9ss)jlsvjGCQ$o<{.NMgUU`l%b@4{/FC͡Z pRRwli%C&p4~8d9Y:́F黂)I rץ1&w?@31:UU.6;o;pח=}= U-6ZMhl҂ {M>K!>-}bB@=PUUu-^\Cqp$߽MV]GZ뱿xۜf90D)ƪͭ\kNJ5B8D4eji aLy$UԚhoj+Z*iRUN;,ُ6 zDǪwJ**'\g74,, h.͉!0 nj#!ViRgw i _nج PVaՏtelKz h%4G9%TUU IX]*< ~/NDן^R~ϰ2oZrsJ9Y !j@ws;9#湹υd9RKLu(~s ЦaSyZPDUjB01=v߅,Vl_ C("! kN^jp%gK%=sD/]hG741 b1VJJ67pcntAm[F.]5i{Q&3ڏ-tC|ZZw&`qxy&֧ڬhY,^8MgQASf%z:; _9js8 %*2J%*'Г<5}=THkVIM<:nFl^"jHضv\r%BŝN XQ6.́v!^lsyi794'أ4>qOjH!ECKR5dVx ࢡ̤3 BUUU@Ub_(q*VgfUMG^Gݸcʂ맿0Of-Bu[hY6@j8jfj'hfiCRk29%> Ub]F#0e9O#Ƕأ kH>:Ά_ЗN6F=U?@e弚وBa.ç~GtJSm4 .S}^RIRf,0eXA(tvVL͛q̭ /^G;<ُeis(.e:9GTU&^6ECstq]Wجp_,ޝ>m-~)C:`Mә^~Ԫag冊kӯ4/jN5EdeyPJ<m3;]eE!֮1)7+ZcjNtNS(4"h:a`P㔞i ٽV&=㕩s;e{/I*)42M(d(TdYwHV,,+fܽACSiq.좓3! I'wF:s1{ ^o>0vLmlE]ÚKLg/QAbej=; {7jRbuwK:~ߛ9?oggN:Oi˹tzgƌ1Z&eQ5Mr+Nd5ĸ<wY̙eB7IzDު%3xxV]KDuj\_ҽr-)MC Dt,M'1MCBdkb&T9Ŀ4o`{Lqt?4E@UAk [j?*$mp2{ҝ̪Kuҋz_޼'Ǯ۲}P9m'$RڱT垓)HelQf; 䨧"tP`Z8ֽvͳ$UDgnc@Ih{c_yQ$* 54]t`}0vJTe!ڋ  -)/7^P\St~= X-#L8/TsQԕ~sgpBAZΦ{_38^LA *h8[8 a0F9%{fh2F랆򁏦jxv|Ge}Ϧg `+>dZKtz;xfR8{g3atjfVSLӥi7iD^t=]I43⎁b`"&`pW.1BR"DYUHe9GRu:e6AU5@WG`Fv@C;L7a^n b{F49[:gMM`l$ 䪪6>ˡ9}X>V>Ǫ >yzۺuw_oۮŬzc}qi~mYY#8HdSs Y?.z^5tUE 2Y[ӺSQxg_I?ZzaUzGGM-, V חnΛ]?FDn^\ohc@%Cr4~ubb_KzFUhQO:+lm3{i`q>RqXn;ebT5 ڊE"CIU1z]#4sDZ&^ufWLwg鷰klvELVRHMгPTa䱰0mwu!X-\3=@=sM J3c1@ɡc6(.ژWBj+ t3;T?4muDMsR)…فh2l#TUU5J #kaع6oajn?y{WT-e^S/=C p*IbaFKI.ˢW^QTUV6%qIhL k0ë𞷿62n&;= q tb{-K1dr05^euh,[yFT7U$h4CUUȀ5g_߉r/wo>[;}&&e+ ZONU/ !IA#'E2WqD"7-EZM!4>E eTȦqc، #5}p_ةJbE}izE=UfY5xTQ e{P^L&f3Brpf7kɅc .x<5AQz01ӉԵ'$RL5[.N*d'UYFRDoivY"2Ԑ %Ȇ3; -qb&2<z Q~iMR֖Ƀh鹁ѳw*z8x^ύjUEs]Bo^;/[yӝct鬮u2shoMGEHT/L@qZARP eu[\9uKS998!Y$KmG e̫=<$&k5n~B3Y$Rhjq/>;VLG ̪ΘBI&t]j gcj7Ӂhۑh^},+ cKr{ڽ \iGi=l w&H/6\,yt6\ρ}LW9'u.nsUMb%1?bTJʛb\3 vg_{*{(5 Cڽ_k=2AX غJ vs#&bzVթӲ Sl%نQf[z&6@CfQJ?4)5D7qY9Gx" F^~l)H9DŽk|`l="`k#6NP9GO7m W`;p3_:_Ccve'~Lq Ƴe,slceeD={PsV3^Jx>_5||ߎ>ø겺xvq?\9=05W;l{\fWnTި_V)Yȇ!K=MŬ*7c`fݨ\r޷{&UB7~݅0xU)'MKi5gN5L+ʉ9dln/ۉM c|: `.=/~mOj li%y`M pIPUUƎATtRVyY - ➎wO=/m@ɳy;[J]WcؗFoD54@R)!)!\:BNGgu14P3sxss!,=b:^i,FXz*T^:vf,}Bjx ˵mM$b?/k>luNsZPG'".- m*anE3I[s9q/aX) $L߆au9ȣ[4_*TH9nw8J=9{u]"`+fy,KT4eҦI򳒪C!R쇃 <,&*5}#jpl2\v<Lɂ6X*$ E[1>~ "]lZb-VxFϤ3UUUȠڟŵΩaqJ;WFG\./Y[>g h{f'\UUvqo5ХtÍrn#}ɸ<=K6|T*~)criqWaƳxVHTdx[N 2vbRO5ܴwBu?3Pa &ł\ʞFӽ&WU1dDu{,]%ST6\A9\ Dۡ^-Vc ^H.xFv u[z sVF]82j~ AΖ>͕{9=UUUu;F__oN),9Ưcs9H%w?:?_}/WǽRMЯb3H^Xt7C۲U{:AKTy5 i_S5yCvyǷkr֗fW OMRVSU}ݯ RxRRIũyЮL5d]1X %%† l|,@2C Hbl3^i %bql)BɳS+Iwj4fUUU{K+Q{|=2ٞk5߽xRX/9Vo2G}.+ hǥSݳ&#\7FDL@= qb4VU|nRvysk̓sZ} YOep 9R.~@ȁ$۹>(/O[\_;\\kՠа\sbtWQVFbJxp]P/fY. :fq-, 4~-b  vLJ*BƢPcopzŵ_Ko{Sacim?)ޖ$NֹʮqX0 tURsjY({ RÚ!Ն- chsb'$0CfuGf4]$AғCVȸ_G窤g<ҩ+i=1g I=@` L\s2*퐑M8vi~d^uhBCtqLr"!4|-Y9O!EtLUUUOߺ~`kjVXmzX'?HLzG՝@`*BdgE$OWWhk'f\5)ۗ]$bDHhB2CBCkˆs,lLH>ti Nal)q60D[ɞ jdP"w=Te;G΂EhQ FjWd&yQvR[T&ٞs311ѓs  H{cԮYO9fn̡2>V;ttj?v2$ eeI" 8Zc}ڤ ݉E߸V;UPUrnːevi>-h31JכM^׮=^Z˧n͈v_m3gDc4ۺy23Pd(eoc4I j(W56؝"5[]ӝ9̧;& _ws:|fio?\HAH#! +~x鎟iiu-J6Q+70Fx5:-Gv\(+(j JRH ^,ejn1n([D. h|"HQ4zt̪j=נYB=i1rR-1V,ï¢,jb^/.c8O,=x *b[$Iw7Ң\USKT5c̩32i~b 15M b{F5ķ:Nʮ-J1Yho/Ʌ1,ꨎ  z< G:i^,mLeAq2Igf窪 c_  rxg_}~.'>N@ͽ k 3.US40x&ɼ<5arh1YCE6VV|Stm>VeZEc +_Ѻ/X/~ZX̪ʜc.*ǡ!X 2osk>ǿicR} 0lBөqƸ.+fdϻ~q˞㚑4, P40J;Nj6 F^_F*Z\D YyLU)R2N~H4c[5$Z?L[R먓<8jOHf{1jWcKa[3 A`K4 "߷@tq޵4~8ص~ A PtT؝nd14TUU8>˯~_Mzaa?:Ƕ;ZMsg,ڊ,K+Q`[&rل̆sH:1TB$s;i(cT(03}C%[IuQJ!ӧ˯"**IdIjKU%r8b6;9ޮ2Qi+!L6r#<eQ87@ FBHa6~LiUP-]cMd5 Ψ'݃yXUU]/Zc ɏM9H;wۮjǬ%s8^W Ǘ٠Dܭ^\:@Ysp5duJM30Aϐ,:6I jFf1c`UDXز#VUjXp@A HdCP20<iM/4\Z–nLavZ^i blAD ;3iz&3Ԝz'+x=׉֣^j/)vuLvyTi\:'ZYaz ]t% \5Ї:@1ljNk2QӼݯ+HpL{)S.25>FĪImjJo#p RXW?Y[{_3a]Pm# نQ(u@2*@C,uhA/KRg,Z16Ng;WUUI :/_1?re`}\ּ޿48yitQWPt~DkX@p=LXUwt{Vjt]ɣ$y=Z>~o=z޳~1/*g~KagMƍTqvr)h+ U }0M6PTeB%@wJ2 cbWqDZ::'3Pg;c {J(`:Qƾ8;`B~j>e v3i aDcR5f232#:irYU5rlQ9<i]v3 ^.<կeY* fd9xcRsfajeHG^rP'aLzv!Hok^r2RtT]Ju'뀽^$MMU@՗~* \oJ ' e|̀F7ìx/l!յg;]X1Zh*ͩ,Ã3&ǡꖘp|͑X6z bƮ%.{l#ggvޫjU>P*6g7+nm?⑳ f6eޗJ2Ve"Pβ@w4tLэx=z/#v.z5$4LJz,"UO]y%6 :^wf7UIQY>6-U,* , QUՕ#,/aapմL6g.I3`0f s ^#=pݭvpl0;Je~eNĦΖ-D3#KwpUUDws[.G]1v:kJ9Wy}yhT\gj?}Lu՜6iX_Ia4 ;AqCXN50evj FV,M3oK~?f:#b-Hs)%TE .?$3dҩ=RX0~&7`WNsd" ;mfGv,D0gx?0'be~LeU")hgXgʪxv3쬪jj*0xJZa;ILpx82{/qT8i UQXk8;=2j:i_LV|%j;~Z7}~y{R5^յN5-B,Qr=VFcS4ٵNdy0#*cZyETZL3j'26&zii$uʞHzSEVjH$lbCOV3B˄>8xڥӀS :fn5[@ZQ]f=>tu)*DҌs`Y=2lY;sz|~[^v6L..$!?Xk{tZ9b`VSl&ϙsb>ʘUsXInUĭ Zdz]Q5ыY>O S=]lSʰW{WxeZժ߇jHa{xE,@K`YGjU.YؤFKχũյDRFV+vC.Cm;{<:@]7øegg ܶS^}L$*HƖ.%8?҆Q#wBUU{#N/r|z#{ :uO?G_oι}\\VˢxJbU2 MON =@߬ŕ>'~<Ljt4aF<v h4d7H"0 Lc+%8&s{Vau.%B۵wg>F@.AmUs69-=LuA^Ac۲Ա!Ĵ 7 FكC UUժ *toaǴbW='׶syo2;%Yeٙ'$4#ܔóMY'CSk&)P.bܷqb]HK;^F9 "/줳0jc2zxh!X[ؒp,`0DVͫ i a~]n^p!FBXw"mOk8\P:zF3~Wl6fʖ-I! w,j-=NӅ$UUUՆ$⇻W?te lLYrc잙tN"UW$ͺwsֿ[+ w/;~8O>4ܛѬtkWu,vW(:3F!h:jL͉跇J-v0ΈʞMTO~(OH:^˯ZzjHa4+.ujX5 F220X=2/wo-Q]23h56h̰ l0C$7)A;Rv~emD#i<[ƸDi.*4y&q4OE]ːsUUUV ~zY.F|s'0zGöl҃_ y-hZ9eW8xnN_ɊK󢞤`19hvj-okBcFt=1sO{N9RCX3>NŏЁ_тv_ṂawR/*C6C@r;"?y'$t.QA.qn{^e(kH nr}t&i{8ΤK)HUF5 uU1O~_]}u͵уxxUSv}1۫Mú2#@,ƓZP[`kN4P: i;"1@HXսh b4Y(Zfl.[KD2 SU $4# 0Ĥ)IݵUwsŖ8S;C[i´–1- %`ZJVd:_ixĖ6 u sؒ9th#P,I'M'UUUmz:5q>x}O*fV㫨[yCZE[Һy {&l.pFi!YjLB@U x35,T@TJ3U䂬\08H4CRbt*JRUM*p I lmcaIh# kS ۧk뵳^Xy}IoL0\#+fQ@DCҧ1(:#:*:$xIz?$O [Z}#KqN}we}{F:Qg"g#{`cly3kT"G= 1A.ȤHEB]|S_M ӁUPIi^g<(jI iP-._$@*oZζkC&`VWҢZTLg\pv>3}'  |-(tHpA}m.M#YelSJ@]S\=#Qj &!ef#ȡ}/mTO/wnmhrki1U39+]-+V I3H @* ͯp*1!{̤WqWJ^xtۋ?&y'Rc˹eVy{Tl#^Bk!BW M 3j ΍ 0sv *hQ2u4`&qܴ9m6&\ gKWB=DMnm(%5 1$FXupeq+ ֟1xVrZ :9dJudӀ$l%ϕT*WluS1*Wrz録kwU$U)_53 scu9q%YHUSՖVxxM#k I^믌L"_ Q2|)5B7D6 1 6K,CE A䕪vèLmqdLҔ=t䞀ݣ3UUUb~W~7.gJ}?pڍr+yY8,jⒻ3d}ۗ,JץX/e;V8w[Ng'ݍ24f@ԝT20#Su+Z`Y.3LȞ*DUHUV*)P 뵴(V-:]52c}Gൌ绣\R<]3 lzJP_9Q$(۱#!i@uӹmA׳Ӗ̥l[ n[9檪Q2+'WNZWy?ry<ݼkz=mtqgj$f:L19*& e4q'M .%} ⬭o8~v,pًm˩LVbck3pfq ʹدBBv12"US " CuLrf@1V֖I%f#$(UUH5y9B(K䥕óثwT1X\]dP;M}YvfgdbMA,? ^j(),o6PY̺UvD tLv>~bXkQg(t5H*%O@h YY6zDiS\ik 4q J̆ҖΡҡ(D32BR90~KU}͍yZzŜc^>` Z02'@`RWGnScׯ8#Ʈ=FaL ;+ɚ3yx?u>f}c@+0Sa!a? 3z[jHkH.@b@cS1#P-1ܮDa+,h/j |>~Lep11m˒̙41ߜ$#49;x݇VTy^o3:Ǖ~th+drC>V%/,{*ʬeYa&/)֒e 0hF,@{CbPܮ 8'jm )^۰B $ B'PsOM;gj=͔ &{C80#l`sCC5E2L޵fU]Ɩ.IαXUU*괧?n<\ UЀ=_=UJ_Jj "xBkoA]qù؉S]zε]M1&X@]q[Ѕ G9cVnO1i ;tEU>em$/ -uboh'Fs tZMy }gcn տZ\.^|yxaezX7yȭ֩] ۝֞[)udHIBM$V4ڔzKj)q?Ռ *T!sT dH {7Bț}nTw3 *\.H'}MjLR O+vc{6JvPU}͒~C<`g2^^Q>n&jt~3;Giy "kF7Je:nŒjR"TFӃ1ؕLS,!LLm͘2[{_kT)IIöcYOLZ ͽx]wh못jT)0S(vqk1P~LeT jQ2)6=4p%sU6TM䔔[ޡoF-NP}|Y^[7efZԼWI,$QΒ nd5ζέ< RƯ6N$%w_/YP; jUsDZd_]DjఢHεB:yN35lK(ը$+Sh/THddݑI93H1 *8MrMvGɎb^1Q፪64n N +&qR%E@FF!jl wљ[ F'n8-b}g]-/we=$P zX{nqG\r=5,:zYErŻZ.guu4nG4Ot hg`Ŗqه3cwq}n-bЭ h,[$34EvzY{UGDn[j1Urʹ|{k79Պs޲V.cwvf}a2nbiYf5'^iԲ$$XʑXd1ׅٗ`Q(eׁ`Uf=~՜VC6smShՀ!-#av39\,eAZڲ9ӯ쑙Kb UU!A-+ 4u0Г^GU?u"=5fy֗=^cW>vYoruRMM햊+;qUU΢Tj3!sfd}ɬLs?W5P!);]IIS06X]oP#/cG1Ȑu[ ,dqҡG`yH!6L3R VуF3^i "Xq@i7P7QUUUv\?V7N5h#֩K'SoދkZs1tFjdl0M7.7l13M_Ⱥn*GlHIH5'=@i*3Wwގ;N=I5B.k.9 .~zcĔsd1)0!h\O$! nQ&Hqk)لpV hd:.p;yL@hF3{:|{f+H?[ UOK2'.0z;_3I`z0( @ %JT\%+c-8lnPLP6/ @qw[F?skA]*BeK 4ta/szܵytEvhCmW(:Y=';c%4Qji׀jU.qc/nlm_fDietCNHzmM='_ fa/$05^*JRX,b,dŞBT[y-.~1`$6d#ohv4@A]A} h}xby%0Xۚ, _(JsEanUmu*fPTSP\xvf&sJUWW[??WuvtMuhd{q^?].FxLCj02aB*&O4LO6*hu 4gB̑-RTDØuJRS5+, e UypETHH;uv$zbXd'#:|IrA.~}vȁr,xE*q-ye@R93Vg^:]!WUUM0dx?K([!J;=&ۤ'AS{g]Wuʯu\.͎.O )HQ:aYgŝ]L"+gF<.QIi֬jn.s N樌R^] P2$Ho.nj{u.TgʜsVPƜt [C@apqΗtn N^](Fcג94&A@=tfKi%>ZzgRVS' .6Szx؏aoۆ M+>\9ZMI-͞bZFͶkφ*W["dw2z:ꆼ&Z.кPD@` :q(KIJ%r`aŒnŷVoѯg&u֊V 5 ;fr iA! o3a@X^LeNAm8[:g,xvϤ^SUU0*Q~̲GSjpOT*o'X}{U|٭٬\)8'QC`8ĬL;+E.=QWAE Gͳ9˺D$I욏8ȜmEѝwυ Uy*Ɲ,0Y`KK_+'YWE# e&PaPڥPݺK vX$r5GC7x޵dr 5_Kh5,dQUU5Mjj7}_konxS?9:t_9K/㓫e[cœj.ՏIM܈L3/Mw8"#_Ú``@YFhƵi;߉'\E1b8*,I پAyIQQn%ݓzCZ5;PrIJ1P' e6Qq-g8HU漚2^,'SbP54-S 2h|$[>v(;PBUU#;֪4Gj w{x;_?ᗛ(779 csf֗/g~>JQk5 yF􅫜$85Th$Eӯ<'E3P 7]h3ϫI153O~'LUu]SRN%wMR~aeqmS藽\ճd  C=t. m8$Œ8Ⱦ]e!A*ue i-v:-pg޵4n,ª. 4-%Bg O?Uŋv?ńDwTٮQ`:[4M4qUb@n<[&&YހQ(`VUUU 7$^?U=[ۿTu _ڧUY,K!/. Ns#cqqʹs@g0$dqFlriP$Xf$D= un3;3e+јg,:!҆x/S S+hk4K=gZ0)@a& 89Zt"M1L6mE@,Mu4y}ldiL !ƲhGdzBh%Ga(s0UUU6m+[غyni{Gw|&vq^/tw{_b9N$EdlFkp xx$1.eH/Im'ҠI:"ߐ*V9tΩou8#掮)@Ȁ)T YŪ[c`JQ|9./ ZRuDBtPQb2z:\e+nց(.}휘_B6>~ xLuQCD=4Mw$s5#x6 k>n~|~0i5%l]eŞhxmR٩3uձ#Tٶxbw8{G3}@ Ag@;s qdLUM3wo-lU_A;-R&Q)Yml ^TJ5ز$7T$ԇg|)IC媩H$5|ۣ}A pN|!QgQvߓ3 ZhjCҴ!hQNn_}t UUU]p]qr.q~~ݗsskS^|߿qi1ӝW{]` b 1#gfcVd m[N-v&4Ɋ}ՕMWFŕ*c` #; !bImb2s;*Pd;t 0I&r6cףڰFn`آgԩd*5n"߳Tګb3dHu Qw{N9j8uwʼWfV@CJ iߛ t\psgZW̸-2ӎѪ962%VmM#I. upsu[-ӌ0ݫvhVSm!V{ +,[1 mƷc#}mN訖$d3P(]{=/yyplhlF'5£J Nidml:-TV%6x *& FE6))#R?:vu }zŅU*?L{Mciߟ=4wozSARy'z x݄]\\|Uuї:dn%xpZ n}c! A(.]Wv2]VkH\Sܫ`z۰;T4Yp=eh8UUUeb&@FT)s)xbk "TlTKXEP)@'\1@ 0*LBEeXmQU巹P203 s+0!@S0ǬݾufƆn^e(XڡNp1?`dٗ_f 6TQ6Xl&gƭ. 8x}$$hMjl1}6Z#@s(iN8GA([ãkD kzx\;Tr7;,`UUV5 @bΣԜdta* tI*Wm1FrRǓ2^슩3+IzEPB0ݵF!Po 1T.sf:sTMˌmKeG !!<- d(61j'0K  + `;P)cGgwUdo?![u7&HdD*epPfhV\t@ sΡ PIwYbJl {~nj/*2i3ȫ')^J9Y2/s6嬾H퍾:CeȎnmeNkvkqo'?L!7ŤcX0:ب:[f5沀dw5p WevW*,oN~&꽰"$0 0 #4#8TUU+(V):x0o&%@+ Y$vF%ՕPvZ[/Y77,mFhB.jO*U@a@ڈ*WvO\1];F&/Ǣ="h-\ybԹd_zMHg}RŰ+6w}[gΠL{v<`P:=H(!$w4jO6 ϶uFS G2 vkFOc8HQLF`gQBf[GV\3"1٫lqKX\ {։ 9Lt镇SUU("0@9PzWYB@Ld(6hRHrƜ#d@[A<KmypR:o43 *̽:&o(Xg1 )c{]8،}=Usq!LyAg! o'{+3Wvr#0;az B`ӥ37CUUhEdB (.!H f#Gѿ!mӛ=ڄ@"#5YWDBۛ][XrdbR^aOhL-M N s*f~;y_i 9 #FA-d6VlH=/Z#3i @1W3oAM\s>L"m/15p. kuJ>f>b7 7%[jS#9qn [ ڣ&+IoSUUU HgOAPK:"g%H$%ˍ9(m4G#.2 @) d]svTRDcֶ{1P@9Z }ո@2(zdYsia`쑳>ևz鞃 EU%Ia"4Zڶ1 ; r ts#,&شw]!6}%`wם́p5ߗԵS6e7WM3ȌLT$ 7NijlC PEՏ=v*kY@ F PSb43kR{>m65hMԋQ&=1׻1I+lmU-Gգ{F\06Uo} âd/%@pUUU"HHpKMU\eۙLŻ׆RhFU[ULz FF DW "R Qxui, LjLR È5`/4T|||H9}Y*~٪FHm{5{@`@@&|-!nxJ⧪ܭ|Ulc| cمh_l@7]pfK0GE%v׏hF s#vוNp5MVFIwy媪(22u 5&:T!DNU=B8A=ʌ83S^9{W.*u̬lW?*("2:3yP9+\uh:=}U AzR ʳi_t}=@ǖR8L~/tvDV1?̊Nq7CL{ >W+Zm5kJ,7t9zr, ,4]d2`$Et@i9 Y."WJ\HA >h58X`u8c,7 [z*_vM鄼!CX~Z&=GgS$Ll1FRQ}O1!X:eų/K]˘d,ۀ7rP,@*}]ʻWok\M(>t,'L ܘANg'xpB*y "8'Z*]mU7Xܼۅ4nP8*ZE({:aQgziq- 7ğ$ƁIHɉ;k`d3&D.QxsY/~ %^j69ˋg4e@ Ļ: q{W<#// O'GW 5&{U_CyP"xzdcdER) CEC3K"r$:<[+RA!%P,%32 `EYju|(.)T8E5WP.h ׾by]ofL_)kdy HBJAR_R}ū$dw~)6:OuyIidrkm2Ú- ~Gm:{z&yT~^vEԀxI1z2dY K8(}M=R ih;E#G TQ6qؓe|: "aL*(>*|{A]yf`!-aQRPœ dc]lxg`gD4 ;PFCmB|Ev fE[  >#HWP=wY!qM^f8ZOqPHnF2{f!WUUH,{#x5BI5T@/!UavhT˘,P$X0+)\0~ ,ݬQPy4 `H _5@Dj[I4 U!M qC龴ZK *3|p1XCYj6VEMKX=*90~kɋF2%*I FIS ~@ mBH$\Eƒ({ `("u/j9goOU UF-=mex@`zڞsOg].fAZh'B@ "wb$?BHd$U "@,`=&.SU2HmDxEA 5,*`Gc9a%SṢXF'M_fZ3{5TUUUG1UD@d>sid ^J)!e(< `X1 P- xG(:q h@@=wLLBW\Rɤs}n<')ȨTb#  iؖI$R)%kq j < H4hFql!U&WPdGU  W5"Q[jH)lnZ~IT#2320V AQ$*ĩX4Eni5FD|t^1"T.8^322&良6 WSjz6G;{'fv%PB( 3nyHY)GzU*>A=Tz:X2fy6g.oG.wحŠaخ]e;aw]lO'kL63[@kbd3Ő3Jhu=ҍD"f# I-UiX" I )-Ja Z Dڶ "v tF03:05(# g3ѳbFs{jm38Tqwu3U}cq P%B1Je`c"2L*k0;2@X H([9`utpCР[:`@Geß1٫\ <\~A0jZ@YS{{62n F)*2Kj8e%EA)A1+P(]-XUX B>Na"@-{0g.7R4ۮa2rDnWQ6dy PΘj,@%RYX_V/a&4`G98y%ƨ|/DO- \3#`[ƨS.wc~4 < ,޴Z@~7ӻ݁ZߡΎM ڡM_HDt8UUUHS9uŃ=1gk{ * [rE\[P)[MPX[a cONf_)aȾ4]:{jRXY^(H7c<15PP[oO WaYeZB"IX:#KA]-a%Ʈ'k]np%UreiphIf75*7חu71ɣ|is5J+4y!Yz_:=T0j΂@B Ր鴃* kiKU!EdKZ) v nDHr NoW lHvJkFt |< sWݠ.J6_fX~ ~qiƒb[f?ǖwBmVG̖,@Z5X1+0E*>M;PWA%j6k{U;tw]'Ԭ+Dv : 2 UUUŮ0S'םP+@| (n`ehޑqBp%B>GQL#f\bݵn̐@PtR^3C$P1ψieq8zk1`rw[-+UoUJmT+J KXo _kmWLVMfB[OɻuVŽy`X | d_KGevy-wV_SGE|qK{qeȃUUUE5Fw2X!" @u qgW5q#$LJ7@ ʴu PHť<@YAݾ5Y!jL a4G,^4 v㤌K0Gzi $n2GE#NZ.+&/0AjBAdj k.ijuQK"HO 48[jN;85=g9fe9Vm0T]ױ r nGk7(0ed|3YO{c\q?¶+=I$E-.>6?. ">zYB'ʙ2؅6U ;D6AG;ՈUifnA@nf{."@HIV(q*P-hR@pEX0 Ħd1cԇ /`=/`î|dJ4jNfmNoz3waڀ0JQJ5H->ghݳ X[g; )CB3W{۬9jV<Q[O8nMk*Ք]fA!䪪,g P ?b7QUŶX5eTh׽N.E5H):Be4#{b` v:eNTt9)55 e`ouqۀ9丬?(NۼZP.l dLhUHh#bD@:FAnNQ*da&A Ҳ*)8|n >Gx t>U5&qS27zs]GVEMVl@޳K4*c@^BF*,H)-\O0R+$P* 0T2<<%heY0 mcgLaAqx 7Ń--M`h kcuY0 0 7:cȭ*EH,u" 1UD| EHe>qŇń`Ds9`%r55}AaT{u⛪^5kϒ Lr={@Knu둇,U$߶*8RJw/-a@7D`V&8; h Ge-%j5%yT_~ܦads(X B*4U1uFt{\je;-/PFQBv2I\Y4-/PIY؀g)C 07tQul`J`5 7Y;O9_teBiKZθث o.HEHA`51 &0岉 ,`rlGb7@p8jLlaas΄ w`d{W?UYTv;6 `d_*㉂|g!psZ,cp_6,-y} ,=qVmH oQWuu˫{:Ueu\>>i LKTB&MihF*o/`j& qZ٪!8-Cb=昞jDtPnl-7 zoRV%j g.dj>d:/l:,ګz;!(holdu @Ɨ$l \BX04 'Mlh]m)BF Q Y4!gGea{rKxֻt ԈQi0}qKxxN%dt*D(bF"Xz-A=׽01%d\DHq4WNpPFFӴF)CCřSmCoTUk0r@.{驡&!w#r H 4iOd$̗m[F/*s.#6X8hwfXg?Ϥ\lxwŕYD:ŨG+{F<*bw]- MVaZdO6UCUUFPS4GȻ۹͘EKP TՉ'k " ϋ6ZQt1njfi )jDրPhĄAz^![]߿^X MERU_S8ͮVQ9>p t- `;¼gSǭ_ ѸL L62W~jvD^eAOل&2w@1F2DXP HԪ슀5(*E*H` rp`.ƴXF!yO~ݏr{ yRF&*Ǥo3if=#z?\si0/,D%=J&Kr8ж6P"="1,8zJvjmvm\O^&~7e޷HQeY.^j1j@^l73=8(E| n΀B_zERAJ R[@C fM(0䞶}qdV(*6ROHUdRTz @a=AՈQOHwڪjۋQZY؅ KyE n+X/,:d$gNؼI(lŇ`g" CBL\ u`k½HB3 W9tk@1~Z^~=N?5F!PUU$TZ/@2*܀!OsEq1-1ITml6``P|vK1phVG,vN kv1dsd$i{_=!1\{kNLξRc˹ka|2%,Y`?$@RPq&4hz"&"Xv M w|  GX,"ɫ|}4Yp/tI 2THUDJxl؁ڊ=RJVW @'`!F1RE93I1@xZ#E MB>t-\Ʃ&\M>5x*gY%_~6e"~uϖNt#858ϘGC{%טQiffzeP3Y -rQpu*" B,5̡VJ)ՀLc7}caFB+A^$býHۆ)>gtL0 Á,{Ƣl{t7Xoue vI&%,fuT^|: XbBJ EB8cWE!,00B@Z pi,OBkN geak`c}I_O^;4j d6)eUUUU@@eD|<Ƀ`~z^zńAQ9 G)G:L=WUmE h1X6'vBPTFi2ӆ5[7C^-w-%jL~j@V<lX.E\UU(" 8q "dZ; -r*=h\>/OBGA` Ȩw@APIL`u 2hr[zӁh) &HL xg)!=5pw8&m\ˈM=7RP2f;׮m-86-퐈Vqmxg`4%~Gp\z^dgׇ,ۖ&/ 0#3{aD *x(HeeXibt%]TQpekYN Rg3泌!q5[ w. h׿0uޙItja!}g)p9"Je!xD ,)B XnC3G30] 0H4,$uW&ٗIv;;G{Hk 428FO+c.j{T5dV.@evSά"/<$8 ү #'-e~#v*Te }Y` cWY KWE m2|:Yn}1ol'ͮa&Ȟ Y~ N8H9 3|, Wb@p>G% =$Jd {dl(ЌJTE*2+ `ZokQVZdV$vB A"Z1ڐJ~v#Im0I:Aa@ȕ %]]͚lQ{9f8zh D6HG^MR"M4'0 _<5UPUlTg*d(b ZV;ݺY;v~fO|6PMGЃtyuwXԔUqYz'ldBp*9/LJZ\)Ԙ˞&cuw9\Se v1 ѺA>/ئeP"sFv!Bs&O r!a}+nGgaV}}G&}V]~bw1-,;"jDVF陉 fUUUY@[9t￳ЀR+ѴaaՃ*UIp (\ݎPQk CÊ9wb uJM*RdRI 0v%V UbBqI_Ʀ+E4d9NJ8C yg~u H s%ڳ:` w9~G%౼n1QQ] MLT@; (=8$**ƔTrjy +iUl0J%=\Zl2_O5xEUP Q֦%Q!ɢ{d;FqŎr-B;~0x=0]x| 9ɮHV(ӝ##3lS+:Bݶ1`oR@65 n"2+B q/Vu=_lHϛ!:\vb4c1 WV곿ߗkL j>>.6svFvRF"F:Z]' Dh0L|܆14'!2cH└^l Px`/S^oEO'RnԀry5gBoW N %̥X?L=d @BB~l'}g`3AdngHXG#y*lӈ4g.C(` G"+iv:3C5=ߗ[u^*Fj؀ї,SUUU DиFn2('%%z 8b#SD4OuO@-H}5ԇ_9om*`ŠHh -̨+ d $@e2\Fif 災! FK7آ yؒ K 7@F֯!lasH6q& KkrV2>mk ^W*ZX"/.Fdʲ]RNvض i s,acFK@DڡwRw PG X)gZ_ $j_M ǶAF [-gH̄؋ uK7 _AیۆFY1 kCWefq-kFd*2'\eu];, <`'Ќ a$dIP!Y (Sɀ7zkhV_KZR".8 Ji*@@prDʃť mi4+uNin[g˨s$'r+@P05c[J Fq1B# Om1j$>4s3Bӈq8ֽ^GE1'%1tgOâ }FRBLUUUQEL"h@ b;84ư_B`[Zͪ*:eKh<be) aJ3 4%@^@07.F_GZHe&I k`ydbx LKYʐhNUUUgu`wt6^R$$WD" \l Wd!R A ڡ,捂3,)@MZ.<9ԙ~ >猧UH LK}O3BWɇ#+4VRU_+Q"`lMQ2o`gWh,ҮP8 9G0+UXiW&ٻ[[oL"3J'vZ2SFsXRU${Dpw࣪(BЎVW1 2#a%H@ ѡPf!(bg^w9* e4EeTg.ly`*BqFؗF~]® p 61Z~7wXNM ?9kE>s؅Aӭ"UM@.C x(mjYU*u8*Q ,ci ژp缟=ng$PȪA[IzC/ qb t6WSS1:a@YTE bM m05qpl?dZ١" ~7eR] 1M47uTcZ hE="JdaP ~Y 9|9 8BѪ LT#z V͗hR8O8 š(P-`$tQ(]^v@k@OMF#˒%$:]t -a>v . ((;fGeQN|5&{T̒mx ha6;12 ;WUU)(Pը5D![4T*E) t*ᚐ>NU c]OeN[3O)p@}t$ HdPOigq112۱J}קJ%{` %bCµRhmƄL`GyZ\y)=g̘s` 1.[q W&6jCkL83._3ԀLA$`RUUQո2=%ݜK$A xyʐt6B#iX\;U8S5̨&.:<ܳ@D$*1Aw3fU(.aԐ`b6V~7xjw`"ف!l8^ۖ 2ZN&ƢC}E5x 02h{K2ʤ=>~=uÂф/GeGw}t3F zeE Y#b UUUEeP@- q]<(E0aSA+ e\:PPx'Iv5;4f5 9g+:Y tպ$S0Ryh0 ԕ U8KUfW+ʯ\tV@ % 0}afG ֊Ap! &)0+#W"!-^^,QkJ,7F|x,TW:{6BHU!lvԠLiÞ[>BX<2CӱC: E[V6@ ejک48xEo ;VM4\˽W&гj ){H`FN u<{VDH`? J$0;DB(IT͟1%m(陓3ak "i+):>Nz'7ZWæ5VNY|[$7nT$dȞUHUcO"  $?6;,iHZDYHDzVC5H(dȼW`DJ\IWU@[1ǐ8D*\v^n(sqxh![e]* [zER:o3kS 6엧\s9F pSOggS soST       ^TS;P$|tlG8F,=;YCG6vƉYr}AIk75+*Nm;HbPW\jۯQc5 @Y d%RY2U@ـͮn?xG:I}{T ]r.)TSnM,#KWFb)` Z[f\f%uò\mUh-\7[dg^G] Y)RϖmxURV̌mr <#$:`Ͻn{]GkJŸE>gqu& 2Y8Uewx5fU51Kj]sdNǪ (bUڨ\Tq7)ꆩ~HerIs|9uh@TU5 ˂r#ц+6ec8vB?%~>ܪEd[[bu|UʆV^07gz7$׫L祥Dic#csՏC*ݭsRTDة<0X4핥 ZsGGvqhwD?z4'2 Ŧ Ah & Ϧ5 Zא~u98te 8w,|CQI*]궰7~d>_VcNSrbxz׃T] ([95bieZʢO wPw΁D9И-_c*fhy Hd$@=GW@N5;JռP, $|8د{W JJT|zJ%Piʫ-m чHT͎lSpsVm%#zhdc@Y6Ua%~dKҠlYX!,d檪v'f;?rWH>?ErO8]j!Iꮁ_T¥g,O5q3IDĐYKgd'3UTWWs$bW.0~  B,&`oFlYdUC$0T>C 0ېv=jMBb>ob2sZSv %^w ʖ)MINLBgҙeYWнv2#eUڗ[Աyt|Pg7gg0\11 E2D%N&JTNp2oq4cή些*.&^bԌ`fLOR^Rb)վVP;vm"ٺIOOs|:3gd6]Kt FNЊkn鼥{g7x:y[~nNcr1κ("b+8q,$XmY* Zwr&a4 yθBD9*%81h l ;: T1 ^,EB Ζϒ=i:KQUU5vX5ӑ=k-a#Կ?'V?(oszO^P5W@R@`Pfa,2dm E P1$R'V#{U\z0!4^SȤ©XqabL 愈Jy(, U#ݪ*Ѣλ@9jAϲPv8ɄSɬd7ɪ!D ãOd߲k"rv1F!~q  h8[ TBQ ^CUUk_|rexO/_vo q=幭sg/Cq@f6]@I:i5UI&' q2 <6eZ+~=cu\SGy. +R_REX,`cHY{i\mβn5ݎC0E4iPĂgg &1Кs m*S lt~g@eSlJ <e$TUUSYkYdCyJI?L'`-Pi AqɤYUUԷMZѼkW{k_ܟ%/GÇ釔n|+v^jSMKEs>ݵ<"ݙ>\:쨠lZ.DE'J ꪻz@V,{Joy<6l>\Ozpj'.ve$2d X8@PX gN4Ջҝ4*wa[6֭L8j+LF9?BkrθnԡLzT~Nsa (~} Җ:H&oڣgf aR]])ժp7M8 o_cSu_Z+/Ra9#qsF=y}\:nP{ыIuœa+o/uarj$)֪-(&EֹөI+ϲsva1_j_Mh3#d$fwPkin尣*Cqq[|Ęm7vsDv2akZ t'@+slcW?gDg7*]H-ƾ^zO!3/{_]nog]x5^5B*-!#C;LdVyeMk7ZHfM,^p*QxL)>jqxyx"1D(R?ꉫBFYg 2 w[897I pSQ΄z$=ZaMH͐N)~ 1mTF@n, Kً]诫d*/?' B3RBcY͘jVG5gXG}2Jx!NJb28RI3VfIf"Ɍj^H2K0]%'s;@]65QR9D.RDL**ɻ bm_)JQߎܟ04` tn4E  Gi]P<~l~I" @ٲ19lsQbL:@UUUe|]nbJ-7_;.<+ gf3b4sDK{lpjSkbS7&N@ٍTЧf2@xޚ@ *MK*߭10MM~=!iݡ` D"yL[bsWӇYNN\e#uZ^edž8xG;T& bҰ*ufڼrƮ6SPOΠ,twUU!u$dV_}4宾݇mN]N>|Ss1}U>y{Pj0^— -sq wc{Dr-t~@i)@m6(wUUU$)T |X u}ivi+^q='h(tf#3xUuUw$<(C5jXogdr'lڊm?X81mcYE{Y^GV=~aқ6QJ*Ȗ0ĞԨ+]WAPi+DqieSPh0awu v~Lit4`mFǖ)ɕ줓;UUU]ˏ-sx5|2vψ2^?+0--j>{_,呜dH ҼlkFo>xM*%:J_bJzRR}M*ܻH8Q3uUx&EŽCz(▍ a2&Sǰ9cyNCt3Ѐi͵`v&d0.ndk֠L˦Xls-sA|^[K`*(hrod9BfUUU]õ5'/vS>Wz)om}=P־`F<_-|,jHS/K/VSCg}{h9䨧HdOAfH%cH WBk]MW f6=b`*=PnטY~γOo[`+*U~6$=m$فgOyG ˻_i7 !Di*F!v8C``W5AڲԩDFϤ3G!TUUsW u?ov6k:Uϯo?:yU{9okf, ;'rj7H6ŋ;Ad@!tہFFWZa(=ъWpQaS*Q׀_'w"^t, l^ΨSKvUUˆ j[>v+]weaڵ [PPZy^?ָ0&26+CpN 2麠!h喽kƾy+5zֺ}@UC>G NP3Ȗx4_k(L;jQ ?=0^,iJD#I2Ş,F^ 7ނ#vff6:b6H%S^w{lofomG+sk=aWiuUr*#*,7/RL@W;d5=V &2y4n\j.g!'\kڼ۶֫G>dQ ry92~go]CJ :ԈuZ? @#IZ~63t>(C~$ W>,A )Ɩ1AejbRW[GΎ^?/h:t_V;}3noo{|~dq~]k`RZw%Q&%S qn A-4)5OCR+k2=Cj,UE cϛiÞseO@SqL%RU/R$+OG<>g@1@y6G qfn[ԝfJuk!,=(t@ 1iHQ's6idLPu9trtbcadL,Ӷv/Y2qFC2mV|R>gT^sжr*צZۭy_z1"I8T{@{SuRO2]po89I;!ssbƼ=K7Y%O@tt앞A:*aF:AUJ hFjʈ1i J@ψƳeiS͝L43ʞImT`;\";}}{9~s[ۋm^0;_seZǖ¤ +T7T&5fiٝ_L?tD<yI2EdOcr?(DeeT3* ®υIF`0~!enY4J96\T > ! "sȾm0 +LfPbzФk}E9`G%)WUUOgSק}#q+^ic;|ؘ|[O\炉s5 LNd,ۃ~IKU|/SL>Z# ԙ*Ќ*B?`23SL7%U*YA]dَ+$sjU$p ,/ }Qwٴ\f=Yr0]~ 4 80-_ z&6Ձ!5d[Ś$7^mJDf6-]Z"@XhxvISTUUrXU\+or3\D;lnl`3uc[kBzu\Иnk8yqsNLAdWca7ya|]ydu?9<]/rPUd@IzTjQ a*y6[b*_tٽ6-Mu"s}$,-MRB4*VtILGt7"Mwy@@\tq AAn [4%h7hgFIgo2YkLJW[N~6xt|﴾ս?B[S?&z21S; fHAd:0-Hn3bFh|BZVcsUM\UV^MvtTU~Q*$8&e)UjY{baًd ṟXucPkYv"ek,?Mh8C"ZioEJ>e,&cKƠ (xkCILI,H_{E_ՐR>I_?i髽֏\^h*2 jM%b '3!C1ZE rxH(ož>fy@9f><$ ,d[}?׼=9k >!MDXW|USn2y|7uwe^͍pllb4oUTT̓T ^ɍ@>ӂ9>i `qc21 @㽀QRjq*Fouiu}]OKnǿgg*c i밷7M2]س^N\:%3)<9T0urM̮8z|!.jk"WN27  +39e&`$"YTUI\mE$DV%O$U{gv}sTm3fnoLeߙ(>fH# yNfiRb)JthA2|^Lm@XL$L4=2jjq-xίAy?[JOXbho#%٦~Xjcu4FuT{vtN)iؘ掑 Օ4 2ӓ((|6 ([j.Kzɿꪌ!) ^Z:̂]d)R$ rnn>'r~ Q+ȥM!۲MmtSH#;+a0SSE`=5޶} J `$mX[҂DM^Ǯ`t:#z]UUա9`-ݝdixs6٪onke][g3gwji]_8)&- TB|dƪ0=@ɩa`h8U{2BqCP=4z1XosWkwj/~|kBjFk mQRZv|?YLS!imPuy0;>sĈ"0#"\*dBz~m @e*Sp6{ vg$Mg8TU ՒJnx[lyT7=î8oc=m9k.L>nR*ًxpʼngș&q-8[Ʈɦe~3@I;׈^jAԐB|/U* _6Rղe13vX[WBq!a꿷1vtY a&CȸjL !ځʶO2>_}Hv^i EҠlDE0MG#鞙ZUBUBNZI]f}z}H}=uO[=5{>~> ۸rƄIt@9GgQ+I*wXos"fb$4#^!qh~,imDċ2ee@x (.%|FON◵oa|lKJ.K gNb{xͩfWBdllp0 jەDAH9a9mmm{[RKGMHW2I(u=ёSJ"QB$^%`َ )”eӰ -g01Hh}\Q4>G4H$׬qmao< Lu ai؁ 4}({ff窪Uqۯ+ٱ_DN:hֿܝsSK {^܏⸿MӚèUzT$1odTRuhc$#PtM{Ȉz P*+[xȌȢ)Y1(tyiɓeguoOϣ-|j3B>Z0H܇#o;VXȅM1Rѡ8o޶lqI+hH[Kh|NˑMdsH i*i6c&?;=~qra%/W[}vW.)YlÙoO[j^0AqW$W{h@=;/]p@OagﮱA "t. :(照@)BURbzʕgs%=Ʋ;V͙7gQ$햻:<Cl\_Hqg}Y#C>LeN, ǖ`h|$~|G $Mg:k.VҘozZi|79"hRʲqd~/EOQFC̉y%55@F,zG͛^~O@m6(C{"Ɉ<1SsI Ujl_2$,a$ɋd[5e9r  cGBHȕP۸IWVu E Bι鶾nqY޷ C[m 1mYlo rt: "n75}֝Z*oo{5/뺎ߟʪ1s[R5 ]و雏$"Н -aOX=S,$\=,=VR֕e|PK &XmXBT ˃NC&KncqqibዱPoB8ACGӨ8i94 <> ~l% b@eS@Q&MgɪTOq J)peL^L !Z(Q%$ڀg,7<U~0>9~(/^_=o࿼]3Lfyw̦5.+Zu ۞ΘITݔ9jq1r-pәYsض6k+zx_?ʨQc,k]\9C?qfGma!\Y,C58bCݞJH7;H1Ǻ%+GZdu(jNkgQ"_')ߊkH¶od7S^L ip>[փƷg TQ)n!M_/ʮX29zc<y-=oeYO;:vԙ'UO6yTd'uta ̠Qx蔜aUFWW(Vɩ^֖XHE"'A6!HI jc"A~zn֠*>CošXL]|զgYukHhD\'U.i*Ԡ gRXAZw?i2#\TUj̹b,enf|KtE.X/z1K^\}d>-f:l4oBN,l("2qGf-=d5yu c;/lɾjnJnH+CoTrV^JJd_/{.kܭZz8Jˡ7p[C^4GC9@5WOggS soSR*V      ^i @\@cڲ= :Ƴ3UUUj->NAa&#uq^_7uM9{9'fڶ؇'s ݹ ʠ[`7p7OU `ϔGSdd/8geS.`!!tI -9^ (⯴hk 6Zg dgwjplprS99tl!(eUxml6,񞀢: \UU%49Y 9֎}韾c{R__߯|~8w?h{v˷=噼xּT9. L&'+QLGOm;mdq?c=O$teݣ]Ǐ.k~&?nM">( Q`Y5?ț$)^E lh6 < r0>^Le h,[23 h{잙ى@UUU>ッVۓ؉U" kbJ[7^yͣz#<@ߏ[1>r}!xzfz㊺7TF KJ"i|Khv 4ht.$y'EV)s^"]ξ:ٽ=I}R ٱIb][*U"xyv߅i:l<)|G֐Q4dME YwvG5'޵tnNR.Ʈqs2zxFI:ɱ>$c=ȋߞzzoMʑ#.cey_8ݍpSkSzOqQ!֯"Z*WLCQǤarb[n ?|51>d{,ˊ,{cIR,`9Z۵BxNwo!/քع?cr 8 CQo:NǹC<>i NpnRLuQx (Gf&]`CUUVVTC9Yu#oFw =OO|y;дX݆,j:#k!oC<!ئD<T}-ɡ4x`Xs0}\Dw: qqbصtq N ^쒙tMA!+On7~[N9]InYo=0f `Mʖ,3x `gI+7iԫf9%^:d+~C\\.XM=L)j:nj3<@8Ȉ>:؎5)'d1LTX.HȄ)*XHg.q1 BN [N#iz|EUӿ6nL0whކ^ec`>c .> >iH 7-Kj*e>q1#(EUU%FK3ן3|ԴЫU菪q>=^/lًMuq(UR k9IrܤLo6ir!f@UfS9mM.3s-|eP5g'ס(Ɲ3a{)P9%{3/;:Ui=$U/G,`y+N [Y8bIE܊뢋4 8T64lU`ҧB8;Ī>~,@AmL[2(( h<;,̤ Yߒ[78M|]4'dk'6sU3 aji)2SD U dUq{蚙g2{8CnʙMԷfzi6-`hȓ]ʮQW-c>/lw%a˲_qR.hB0O'I2|-6L\_Jv5yiE:A4W-W7-(;84?rX7Uuw䧧~,e lYj/hCatԳIGߤCb\bmgeL֎#s0χEˋ9fy16lx"a6QiO`I2w,q/]KE$4#ÌWY`vW-!Pj>ܯ}sT0WF j ![@Hۺ"+@B#BX%;oʕkX+'H+%C i( I-6i4*^,zCXi 郻(LF 9TUUu PEӟ䩜TZ/x5~͏\ѬK|g dЍ* ά*zqY$GSJ];9dSF0gF10z3+Eqou!͇dA& 2ŴwAXeUDJS;bAV]嗵6d$v;F< Q+|[ ՓhmfBPjNcO _W,zC2)^u HƲe,S@N(V0*󕫋n>ű>u.?=v~8}e.+_o7v9V~|_j)/\_W#^nuk# ( ӜewϺx- fR$L`U3S;yGh?h@PO1wC'U.jrh#Y$YR$dYkUU00`3|]cN=?rVspf2wAu+<uU/(%Uy_UgFϓ wvSaVM&K@$G03)p3D+@̬ =^,~J"d6-KB2D3@@bLF8PDrvuRBM_F~uihrnOγak姩O籾99!xmED@'SLF*-ynj*Jt%-u+1Y09gnZ [BOwϻo5vW طd_lZWcӫn9jp Y6U+D:~rQ- ָ w4 FFlXq b6쨪HlO9=.y> ,hz{.k^\qulnΘ87ib$' u)(jgP<9$qZ{[tgfWB)=6KS}\,٩{vǒIDjjSJz*2úz~ tA(bPӺI$#U"O5k82$8 0O@JC~~Le.DP8-S4ςU,R2{->_:M.azgٯy3d*SJqNy|]MFϬte0Eʤ,SʢL-8kN 4L.nmblJz]LԢI|Ip1R$$ `AU~c~I5 0FFː=ٿ/v(۾aoެ͘бmOC۰ f kQvn3%L"(i\[ދ޳ӥUU՚iH{˅_ڧ|+~R;? eIT}kǔŮ5kvL9:c< 4C>qB3G=lbZ\hDA;b*Wm\ўQ['i@)TU܀jϴnYk)>?bYD1V?dյЎ [F4!dwwkLod۱v, [x;5?,~m fT@<G>ؙTUUը\XLW{WIW#G򿼞Iz9mq󥷞,cfQkDgrbHٻYY:vR4C(J:1Y9FKD\ >MVR!I9ayv $}"*}UISYH ,tߤJuoGdÖbK鐐 gUל9îޭM"`D@2MGB$f IˍgZZ2gGE^[`'I&9MQ?ڽAuޜ$a+YJEw鉆%LR.ːe)եe(PHQWN&*EwenUgO&$UIU㯈jC#kxee+Nlz euZheDAǵ -0A _{0]TRowtq( &-!pvm ,Q5la8لeGf>A )–N1AXYAp58tTUUUG6|;ӯU!pZ߾rv&O>mdzmn`v_%l(:,qѐUel?͎ IͦÞRz {gwOR4d &PIi@^  QRdKp>T. u9w^+#o!b; zd$Xr>l|1#o g 2+G1K3LCزĖ<`lCOey<Ԧqr돖GmtڜxZ8ySXh,mr*u™&Zˣ`1)z)5u=/r"a1QU-;ۗ94sbʂd S]W_G7ʘ.+ y-۔=7n.MaD$6YEt3+C-~ BZ#w te@icҔ1@=}( UUUǵA,*΢JGI Ϫ?8u>.`:(^~g?>:w둧39em{ϢʶϾdgeCzJ .^d)cP6Z Cֱ*"T6E,ǓsgWN&jU*UTe2+U䒢tW5ӝCc[7JKsbO, /gblUQ‚|`oo R,3󼦮g@ƴeSp >E{ TUU*en|]^tPq@lko~3{L2?/{#_ڟY2 rp6 ]+ 9NL AC7;g}3) h'Sj#m8h.NT|[)2!k!慠T -dGҟ"-iQ@@#`rp0F< ;sv dyM@V-@޶ AhX[ .xG0Lv UU5!BN}xP^] ~GVc H&waav"lzc|6^4|ܢ8@ijV V8F\WygGe7ajhf?ԧ.湛r2er𦬦A Xp}w/DI0~bğ2e#IXFhM_ׇ 9}"{nxƍVZej*Iv{>LHCWܶ,lX(UU!jK!1˻J/WL[7s2v^յzؗ Lwګ0L*3=pm^-ǃQû'Q?sJ{W8YE -K}J y~(Z\"B**E)kU$|ͦc>`{B; ?n/}\9GUi@ q!jrc{֋3lmLDИLT HmP|->Gf&IZl'VSw68ٻpj~ryrnW-Rehb0^ճXja:.I3AIrgHIK'dAAC5'faDN ΚII^s\tx GE Tf~{{__.'tt,+ H!n^_@2BnlBWZK{f 3<#!gώt0!>aVJpԑ# ^L~UbV4-]c47YljW3(v9l_x߽y^v<RQ&Szx}!a D ia;ĩʦv$KM/J`?؇9Gt T<$@Qf>}]42`ט?nM(5E4"U6`cɲsCgȡ۱`[h4| K0pRAR;rFhʥ:4rjcR98&gg&5T]#: _;q;ݸg.r K{i{?bq}WiAN4'* AJ8RMʊk2"viV f\ f{ ,T\* 1I#K20m%5mcLE*O*Ҋ#wgW`t`#wQ'ae3|3ۤl !A~tG(Q( aKHeJogw:3`Z=h zɩp70##Nw#ax*Tw{շ~vrYU ]){Ɂq%{lVhP\Rz  :tibPw:'d}k=/oz㐂F%1 - H c dmxFv퐜(3["##Ō74A$l@,a.fLHݐMV<4N!p~,iPf`a1mYбRLj&f0TU{ ҮgXW$>@7Y\;XZ .K[?6.}r;a5YVX ]\3ܞ^N0)*pD2~\shJ@LYk()מ`ZM:"^] t O蘇|,$C2=;BY#59!C|ذ. yx0{M>14~u .Ӗ%M!b}zzc]y=ySfo!mcP%D0ћTfI7R< b9e^;b2W3CS06 ٭f_W5b:塝̫a߶<I *JRDRUI{+*޳AU幀R$+}1 ֲsuGCq JcK ,ByPQFt!S$<`GΖG_Kaۄ#[++]M3k2puUcs=o=Le:榙9Ϫʧ7  ^Dj߮n@0ݫX:}3D%V``,!Ӵ˾r9e9cYҴ!nOPH=h%Cy78kۊArn(@> ^q A ʖƲe,s`6xv$ACJ ûxW;ȷ=q~|o\|tp^+5:5⹨㱟1dTTqa^'4ySI/]#oJ_R+o`F-ַS5nCC&TodDrUvM])Rҩ) Jsy_wv^ψ ١|f:vLWCh |}^΀,F  eǎhrvψڳ &f8^j\HP+c_|HF8԰?Ӽܫ{ʬM\5]p"I%L D^ ڏ~Z]&Ba7Jhy[Xh>8Eٴr $4ވ׵q?nY]CYf޵nph [:7mFFgLebHτsV:فO]ϋoGr;ogu\Oy~2YskY/R^*0DڹL6=E&ef@|eΛtVBoD[0@. # 8.U\'9鼖*RmLӹ:9Ʌƙ=l+qx@IXjI9JJ>ZB@颿XMݰL@ΐ &aN<n0Ĉ9vca%tl b cK{2UhDŽH 4-KMQ&aBj[77e;5Ѯ=P܎ߞ{One:[Ai9uxVP?3b#~i6qX(6n}(UYӃ _*ʄH'咓=t\7>.t[uf朳Wۀ<:Lkec#R*T="s0 \;Z܍nK 1P  F vG(\lU#t~:4-`h2A#/"8wUW>Bw3?x#tqNV@MBh4x|S:YFUUUY]ø 1?mV{^q *T=3R&,5 ;$frrguVG\5l:wfZ,mqϘ xz>ew7'aV6/ݚdz`U҇6Lk|Mn@-*>(3h 4-B0H+4evUUH5AW]%^Qv!\~+xg^6,m-iZzAaE{ؙ`:BUUU #_r%G.~1Z{XYrtfwÞNmkoKfL/nZ^rdž]Ԑ<\Y`u3-ήcl|YΫusnaqrz)XoTQGC2U4CDaX:jY}w{=g;tt=~LBS!mYƥȋ4ѣ2$Lp!xOw÷o'> ^}~<_z2ѯ}m;2RfMZVc zCӣ֙ 1fWfҫ) B`"EVuPIW_T^a}D(j*5`upVnHg*j[ݪ# Ls dkhZhŴ t>Ɣ ޶l%@ekcF'{&GNgUUU*)Aer{q;svk]ٱ{8ngGoq,p) yuX_ɰ]*zPj"DGfֈ,tB=lX'65@Xxtz[ s3$Uq"jꝞQ7Z!16, !Un[!RjT+>$vrzZWz:0\)HX]?AZ:kw`Oo(zut7XoWtQPmmӢ9kpb'~ E ali>(ѓJUUUM&pP f&ox3嶝ksetIwOsE^&^ rf`ƺH3۵Ozz˫-U1Tzsmnκ(RDUմIo?V᪸v!/ T*$TUa1K)PZy}F#3uŀjM0֎AfP Bnakd(44lu iL[#Cl]"2䪪c*PL{fN^Vy\m\aΕˏ}^$r}^m?ϧiS,UZ}}eY> 4'0YsM!fɲ8= 3*'9:z9f(&ZW˩3$75G%*EVW;0Yֵ:81B1؆SmϹ5cBqPI24a h}C>e "Q4h(["Cqٙeɬ#9y{o/=[>|o_. E_]5r s/WzΥy5Fk/)6*p f!c3k :bSUD3419IZb=iUE+nYP"EƑ.iy<^Խ%dq!BP [B6HYTJUd` ͬ2#QԹ\k[ƐJ'6&i. ~ N,pnL*"Pxvn UU!5A5" UxGsr]=%/yN<:٩8=Q^`GG[)λhHKt&~B8..rEuvr'TrAMB]Re랆cvlI\2CPєHmJ`E4mj+_w5ё;CF?DEV P9g܃c[ې tq , –>^h:GirUU!QE~χ\gE۟xk1:95uYxXb.]pdd/Jɜ3G,u\2zRie1,MBtbWAUsՙ]gvk |!"c0T^(y&g K6Giz5NR#%V "MtǑ34rW>",9`-m A t1mYm$p:"UUUsLVR).o~mq}k~wi({tqu2΄jOb%Qj#fREFtAV:Fܺ=`؍és9WU^}g&˝!Y]ieŘ$2Ey0BaJ_"ߵuw^oBsAk^=)7gfY>89x ݹDo ~,m*2 lYXAfX@e̞N*4QUWI/; 84_>-xMjO,XQTԚ4Zh5nLtK M,-InymLx27]&ĻIf>Fja\Y~ocU{q7ԶXμ}Oji 5 ,,p.|]U%U">TR%awruY~WAn$8v@im ޵Na3#p?~e l@=h:k Nܖo.ܪ3G?=vs[۳yļ_Gnnٗ\yv?PLOA+ȴәV橥G!eKyLq̤Pٲ) h|97;;#ɫ*J~W , ЀƳOkуƻx3AL88hX3yH64qkR_Zׇ_\N[j;蹦_%Mޙ7^6u8,݄Bx*;˻s:.Գ%z讜X{bW syT'T9UI ˻ ˹8߅D'/cF 6D󪠧m[4G'DK-Nvsp !s, &]1 t;QV2ܧk~,m @Ζ$U3zF43sUUUQI|Vx~DGWY[0z\ndX;c$96Ju2CSwQ53g1?V͂즗K%0gf8$ jHc v.b| hqw&IrՀ JAi/`ovE>~JRbҀƳesr%QfWfߪnW:ӎ{gLW9~,q X4- \4gnPUR1]SMw~?kr푋~eI4}Ȼǟ{vtb1Ċԃ-N`9nѽTک|CݓrƼ@q"Ug>QgA5 v=ۓT6r߮ar`EcToq Ue &6_!&U ;ܡz8ܽB > 1jnBt'FN4:םL^hL[4Lપjh8lUV1ҝmG!ԏk^8@ں\,qؗ+췒eG*Mɚ <9z,Pq=3UlHŮFq EQ1uddx9햅m^wb';}R*BeUUۡQ\{ii@'d7Sv;?6)Lma>W: N:v>~Spby( gUUU!]8i/F>^=Pm2$ۿjx\Z,3޾g/;Oc1քNpmPTf?tFNB3:nR| 5PkL4\6΍уF/]9J|qqNgq/~=Ńx S_?y:%6m)\$fY"x#zWs\-al'7~lΖcoañ3 CUUUK$eUc⣏HD|zt|,ʹo.g7zܒNneV&.Y=CgmԻkC &9 $ D&%ɢ2ʛ/kI:]t9]wq)i,\.J$)<>HHIHvf)֟S?0(aCV}{/D !ꁌGwo{H+cHĀ~,e(=Ʊec@@@i20惶gٳUUU͛jrdZ QC>a=C+a8>/MٌREt6*g_?ɺ)=e0@k)j3I˰'L8+)2OMۙ5@}O5=P90;/g3' 4da*/9{}jo/-O;ɖrhpvR1)8j6 mcFlG5-l"GiL1liҜ@Y2TIV90z睫m^Ee߿,v;W O8?+DQCE(=0+&'EZ ZqqLqjS*qRGR株yWUcs8G'9;wY-z3Ƀ=^?+/{z~qϜ`9z>x00 ay*06iXT%gHK-RUմe G']Pu57eȀHZã ;AsNlq1jm -kIA[;3#gZF1Mg|ҿ{s|u>[F~zӛӮ\OBx=mUdfxF"UR$] DEchj2~3;Gto Ш{fL{x%ySI,uFlu)j֒BHȖHYꭧuUIBE[䊑v0 Isz@)}v݈銣9p )oEC,e 4-{D}{e2PUUuUct|Mxtiu/Wط>_^gW_k*x={6sޟ]1_4g朻f5'ht5kӴ@$!& Bu $8ݝ[,L2ٙљJȧ~ű ޘ6l]TJE\ 0B| -G遏5gMŠEvP s fHbʆpYw ^mqИmB4{f$5TUony$K›_> 71ys⇳ymEJdA:PUTJ,Wiҙ=$tC192.ZPtBȞ |6|{ªL1>L`/F4-4'kJGR.tfsUU|.s 4W?޽ŝzzm(r+x[?֋E3wa:׻#\d+:3GL*na]u[ N]Y+ջyN5ۓ`ԏԎT4Uj?4DL;oW.oYH}7G]" b =;.`_JS 8 ϟ:khhb6^ΖOɋԋa #zadeX+'o[1 T/?Nh^?-zct4jjcǞI=Ϧ 55]xfa*Qg2LOIaĽi>GD^۩ ft;Mikc^f.Ϸ}uίWUUyxxd( ,G5Q&Yems#;9].p tdY`hA4!*LkP˜>qcL ^Lqt&-}"@m"#3*c$mXWEe_믑 ^޼ok[-qY=7FPLOOW~Ox1=L4cMd$jGtf veJnf,_@w%czF:=$? k"J ec|[nddl@0 # ULVe"U#HHx)ZؙptJC),ChQww.e,!7-LnO3t CUUU'"T_9NUWِL=/[8볊ۏjnիm}wNN;Vi,W䀜dNR^P^05~[1ٻwhҖ;!9U-ѐ"R$6F"pqO:Av$8X_*M+`/i\'sQci`r)X6pvɈ~7giWa}8ߧu|u3S@cL2kUP^,"[ UX@ pD#}TUsEKB\/i]i;xBur l΍ aggMUn 5- BI`dFCUUUI7SOn.>z7o ,VȫӞix8}m>oD^&kJd$ϞgDc2vҷm{b!&!&$`*"jG4]L/0y8Gc{،1gzO6Q dS|Gu>AEQRy#!9f1ea0 sl#as>i A@ƏA wѣ*RUCXk.m+v?"|qh}ceZ98Smvz;6θ=mpR?h<΋NPeS$#WԵ(jbMpx9oȈj:sчanrԘ}e{TcG{׸,%F= 86DJsT i*佒Res>rԣv֑F143lAM&Zw\CC`PuX a+P)7&ؾJ>L 36-]3Z㽠`L:PUUUKRM0\_!c߻ t.?by?Z}]ytӯw9m;'UzJ*z 5AV!0C" j(U}8uq$;FiO>*$9$&/A8 y}%{U9\csMRE)vMsLV+5+Rpu In'wB;Hz7fa%q J6,lYX" I8ΨGdjk-Nۉ }X>=;n<6 zM-\?\n!ˁfzGAA@s)RM@0ԥޥPP+k:fiDjT6j鿿8[=S@`P&μd5iQ@%FֿcS$Aj>F36q~A (p0Z6MJ1G181vi}q >')Ѡ@R5B=E9z!Iq{lד%znU5#O7l m**&%-1ٛђ!0ʸW pT&&k>9hrt'7B)*(FLUE5wŤ3vu-6}۫_mXTREYRi_o'-Ĕ|]ֹUD1Gk]Tv9#]DI|vsp8䐓aCeW4i)"" aKgZa\xF2PUU5aQ}^F޽VV+40s%祐[d-ꬶWgnuD;kV73(Y؂r;`&;DziZn_Mi}'a1 ʂǺ IC1c]aєJPQ*$I^FK ?n{tmȻ8H?8^|pA9U9Hn_eiUoF+}ՌgWYbաjqU ABيKŐtNe!M14t" IgLOJDBO @%.y@TdQ-z_[d/ކ{UH H/QI c2Hk7B*jFsӊ\m$ɵiNc\6 wTpu,5k2ɝAqOdIPUU5qNMw:~XUi~viFi?g_S{[+Ռݚ0roeEWfmASF(13Q М?Ҷj ,KjC,k󑪉`:UcW2(Q6'%ZB%$J*jF 4y>}qGG[PP5fP%QU+햪",}J  i:"Wg<)F$jS51˷mh:1|o?"+/393O.ۜ#;m 2g=KPLdf& TC2c̝>NNf]IFt)Ȯ n 2rZ,Dso=ԍcc@d;Ӧ+bnƾ[T{!ĵpg$6_rgrÐU8 Mv)hA~LcO  g h|' dLpUUUi ~]]kM!v'tuƞ]~)yXvk;n{e~&s7VIF5ϓn=I ь3r y>]$E6._ߦ_}nHF|}z:?klAOR5E33W)54tfRTּQ&l!.F|zݎOU3 >xMeMQ4f+b|-{J;v f2Fؠ d Im R`_2EUVmt>EU5b>IP\rv .aWy`OggS< soS[}L    ~Lm 2`i2MlU\xgә$v`#ۋIKS+{cv'`fvO39\6V<4Be =i$pfccInO]s'CMj i 5^TM?GP@6FZ0lWί9 Zר!S]Rr"t jZl  {W1 euWxl@LܙӡؗSkR%i_ڬ&;;f$OgqRRǾ/Y_$#P3C~dk48cjI̹<9rg#Cq'yG{dNM4@pA Gj-=JRʓRzJ 4mg-9.\(l@h`QCnCcl)zw#"\E1`z9KluK\)eiKpVtf=UUUjE ò7\6叼:?|4v=_<=Nb"*Zz:S'^}BS)& x"\IP9\1tC0٪~!ІZǨZ!Xb0Wơm c?i2v?|Rc4o힓9%]Bx0p86wF`#rRm1Y!2Y~ctZhP5=`fG<&iT1I'gw\1C7~ w*<߲3U. 6 U o{ddYԧ~u'sh$il~c\c,G{ r0*=eG_#jhlD#/Ec kX"qMxt HHUÄɨ캷Mygejɐmo]GgC'o~9O.WWІe$:6 QbV:u=^ E{}1tVCs.HI\EUe30 ֡}<SR:UOŽaWo H~lASQGZg z&*N$k֎ATӂFp}p`z%@O>~*h=ϖMAAI'ðmYcP돈˰ yعo.zPW/.m2[0&%` L>PL`֬YU gtLLj'&ƈz_}]UvXSPEA9]\&x";Ĉ䪕@l#tNE0*ysSbsx,$~u  7-c2@݇33ِ 5,s;_Ο0}ݿo\9B[mkѴ>yjOy]tӴ贙^zU`8AIGӯ`^6p*V11 y iPBC1 %19(k9)C-O*UR߿7x¯;K=Nmmb`8G9qWP.#@ү77] # "4^uuQC2)L[4>)̤n@UUU[굞^Xnw6i4cdeodp<ݼTeg6EsYd]EF.F.]9p[Lvg>Lfˊk܌fq1=WM2 \}c֌u֪TWCH*XqT>>RJb{?N鲣j\Cg;\ۚw6{m<i;SH'}.D#0lkmslqTaMҖ-gya&,K&̕ Q KQ+#:Fǃi⸔'n|YHk1r+Y5e#Q̱-:f9LBe,$Z[MtBdmHM5r|ȫHOf6UYxͤLBZfsAaz m>I.D]CٯWE؈Z\g媱(o`8NͥZq=!?Cռ|3ޥ^~NjFX6-STh4nÓh6}2~wnzXU==XǬi4sU_q.<PQ&0R8A)T?c6g= 0[R=S٢flzJga1qʣX;u.. `QQ)z}*d# .J^{Gs=]4Sm( j/G,rpgxa6Oqti҉p>i @싙esPـkt{dS{~ɫ[Zz8A7p~S?%ޚxdOiM&I2Kz>xu"\GT@T*$[(l.LSk؃*  {is B8e0 K"Ʉօn0lz9ZPԘ_e2n]pbZМ%(V@M#>"޵f@nZj$hrv&toPUU5=>Rx+8$/7m}LJ5ڽ;6|!^++\ ̤:KR]䙨:Ty|v Z3W-EI [(=u&$}M93G췗&$X Eg (sblޛ9^1X d9~a{?,9O')n{Ǘ%TPsndr_ɮYuÆnm .15Tf~G”UlS4>nEÎ23;IpUUUjͮ~?#ַףO,kM{՗mKORg&΁뮐jPD,G.j3O.tXۈ0 I3jXHպeH+Dkqo*KQuu2P  f74»!C4|u`KmzY>i ΀eX. 3#:3Yj#eŝ$+-G_=NK&hm}5ruY30Fyq4JwF)زR(gʴQkL%P000Q{?̾4SoB9Z[imKkƘ01Z%Q&UHWﯯڔn"o4-W#w'v-[c_ u !2<)Ikcte J(4-]C0( h<;3bDYRUUs s9v߮xzM?9o]e-6v΋_i' :Vj x!&Vn8M&{yȶTǤӢkC籶dUQ8EMDX`- *xWɡB+E` @jЊpJ i4ڨv^ H ag֎UbqRl9_cLrZ d܃uM4rJ ª1lӴe.CnrTh۷]̯0Dz~bKԴL]ѸxK_՛gfZi 7 FІ{ݖoԈJ{yW B9a )wOLL:qRSGŐ.riQөGΞ'sLq N@ǖd 4ܳPٽgTUUSCpn.{|ᓏTN~٦cy}ؚ6 YW9}.Ka'gVH%g:5Oct7aFM_̘WRc1!TV#SoQiLOoMlfRSB3bmԻׄsh*F]#V;܆n>C;m #C7sE.̷ 1KNiII9 ޵G 5 ]Kcc43$\UU0kUlEkn=jowcȵW[E>*KCzbzn?&?s [r$D߃f!&ɚ$b҉ĩ Z*ID $E?aGrvu0Sowt&BU4~ E , hZ:=E hONtjUUՈLU}9˟h>i"hq[߿y9|:\}n5KЕf}r:B={u{1PuoMMvP)ؤx`.$T6 ^.[]Nw0:N-kҍ@$O^]*^뾕s0RBpn?xHW[.GCq*X`'@vl(%݃K>,qD8,q̠D!m<)kC#drc>R7?Vᵿj0v`;fe>߿Sz~nU'Z$/S8&G[mS{1ii!*)_@e#SԬF$[(i0t_ s )V|='个E'DrGwo?]I/t^, ꆉn0XǭQuNĀ́q3AYrFһx2ai]~,u HXhL[>PsGIQTUUmdc䶚*K>7-߸-5z|qLf~k=t6;G^*w)Dg:{'{O0tf.3:m=;P4h*8Lq Z$L{h{1>ݼ ,PhJޫTEFƢ҆]1KY ! .!hÏPuT84efQjع߭d-#iPd kzk:zh7qwgfTUU56@!>{f|?_28Tz5v\Lzv.VKemfd(3nO9*N+Ue_5ѽr2Ydͮ!SʉG@2C.3"F$2ALkaKA۟mQ"XIL.R`;8m."A{uc2qT ?+09"_UA0Į~,~ @cڲ%@](UUUUk V:~˜<~׽wK>Τrlՙc[gY jϹEUHj&~6 8T靝9I޾=H-5t:[:4VD'3pg3Ùc֔ʐdw3<W`<.W]}gwMEV(o|&} fz*;KJZ*\n|=F^Lm */ϖɂh@ٙ=3PU>+z,GֿHX=Umn~&;ԏKV>$N1.O[Aq:n^\ ԯf* U_C-{#:. r`a (0 4UgQ/R!ŹbWjμT'R$ KɅ˛R R zÂ(NO:mooF]tqL *hHyTj3C9IW&~VRIx)yE[F=E%zڔ/{:vfnV`jD:~3x ̎4|ӑɵu=5`0cgZ:9A9uZn#@G i _K] xx=33CUU;1Cl׮VJUmv&L))iblZVaR޶˜X@cڲfA 5'gX85=9Mx#*T@7#ι xV!!H [;$bY`AXˏT)Uߝ*]wOƃqnmiUWw_RoiJ ÖNAPNGIajqCni>?zz7Oe;<:yxMᰯϽ}7wZVץv:@)&L@9PKu< \ZL/ t&V7 Im~,(P ر2@؊v!V4TYILtO;Y(D>ײr0gw@؈B!,x8vyP ^L~ N -lYJ . c&$3sPUUnX-"ֲ=]?(콡98j@7n4(NB+ގ >=)~]ٲ0F㽀]tDlUUU##_N7`q?}4r~z _{?wgBLbyF9Gki+a3T yRO 6. Z+cMItLU){|܃+-H^%Qm*.&N-k"yꀏHH)  =@_=#t<:B{n(>qlD!^l٘D\̐TUUUՒ5Yb7UUXyk!ct;W Y_r#97 cH 3KYj*W䞤df @[S=zweNq 6*ǴS̤`QԐJM%1Q\?lmŞso|+z=ꙋ5txttXҤj(sXrOygSeG(gJINiXaBe+Z̫fv{d$+#/A×C8FfDZz-\xfϋqVr|cg9Ȇͺ'L;C dP0ȑvr[{+^u NhPLu, @EY`UUU}`"Btn_>txJȳǗ/Oxe\g+fh toACOC<`4wqlv[7t%h>~ A ,5-SC9VVxT{TBӓ>)͍õOs|]8e{^}ԩ/QZEmڳcq.m#= eY&Wt`lV~li8~294yw 3醤VS1wϺuow7pWnLFFuXnw~/#Ҧܷ9dDMETaw ߚdV-I/us˔@f%Iaw;qEs$f'm&$(}WWu_+}\}J%|HiLre^2cuqn;Is݁ RvLb w4d.oÝ^Lq*`g"shRg}q,GLpU>gWtZk^=^cSO1oQe)(1ɪQfs@OggS soSȰ>  W't%[gYj{U[>]ʺ 5+7]pc.=sCUBU) 2)Ɋ*D$^݅](B iJ*: jCgQvBǕrػ/~-6BMH reufJXa̽ L[L޲zy=Yt$7H(( 2]dŮjV112# h; viBƄWŹ~ *j̫htufzo$,TC% M(P"DA ڰ D0V y)6e:bZ O1(Lo ;[MDGt **Q2UF+1 W}!ٻ4(B- ɗA}Z:l?#`CGyh 3sBNn7Wup<)vdǃ}5c왭$h%Wՙo&{U@?'7 pFU>UUUu`E,0A=!zd#6z ZGJ Ucں`jUۖl()Yv HR85 ZiPOϚm@v辺9lK SdYEUkپMy{,8c/߃5B뻩`Ⱥ ?WYEʬī@kj)p:;\kϰ]GvY;l6@]Qqn e J'pFv*EBIB-=; G(S VJp$2nWOLl lL@9+AHAk &uV+WLxRSԴ(Ɯ+s1-U߯=V&eu^^ݪ) `0K:5%Fq5`w^t.:ly ! ms,cs8sf W'zʍ?L@8?}z!L^Ha`=Fg %CUUՐUB, &LeYS}/v9bTʊX {0F=aCrk5?/>:Vͳ Z2]s!>VQB6|'`]+_äOֈYAfJ,#̊YpcD3,a !UUb Ui%zDaOXuLRu TmK|_gU*]a1ApŒȪ5̧f/}^ٚ Є.\迾=oGvgJ԰*dy75/#0 ;d,PޖR!c@Pp AJ]V֪b9e,{ tǀ@|$R 3&rjV'L<O/w!`++ EIvN_v%`%sNsg;oMJd[4C1~i'fM߫sj`A+ ̀$#/i,q`}ׇof`Uq,q|3؛bE882PW@W:;*̬ uBdhKn%BQA0bb&dHt `9T$-q:ze S)foG{1CD_(REzP"Gg%bOfP6X ơ8AYw۞*߿X$C9 {.bBJ+0 /NF7 lEnA]}(r7 Bz_!^Ge-* 5%yTJovtO6 @f58TUUΊXJĔbw<˜$ ⫊" GAhTT`X5mjű\][T&(al5 7E~*iV27%EeW 9D,#媪bJV?xV'@O{rHH!BYPVUI P jqMO2&Uʔu9i'OrϷmކ29 It<5(Y:Ҩzy&(-^E#Q54Do2'٫)ls,yEЪA^Ð&^&5rs)(.`rm FgjL |6^u@&/v9h}$D'"]dPT{YpjaBhHXTATi%jQ,c X^ ua(h]cl 2kS.Q xU {d,RS ;jjyA o}_jfGb+;mݣQ?>u H-0MED0Ym56MJc~P'v2AC,GG~d /vs5:d,ȑ UQ@$u FXI иH9Z:gy Ah p(Q*e𐙱^6K H+/`ޖӘjuy 8(OV^4lsl?ۏuaB#HYl?fdl$lE{\`B A4tI|?V%Ps֯ވK~D|O`=aWvP[ *@CMq /qy <'0z5TUUeQiʆwO*,G:'\R!B F 'J٬xe.4Dj] Xwս~cd1zT+."ײm³;o*Z+oZ J6+Ѭм~aت;TPUZuXG Un1cvb4 |~7C=ӰޡI -XG1Y0}Iê2@ĪN8JUsLǽ4̫AE}UM"&A]. HM &޵YL-s֮ќvu_Pz%*# aKYa"Ts8HݫʐUR̄rke&l{=TS5e $Xo=065&ݗn&C~70aKY9vh@VBFt@Gw#>;kRMez"5&g>TA-3K ISIX/v +23Xԭ`!H FN`1۞ۧEaG Tд.!^` j jVMx:pݪ4e ,ܦ=RTUT!,K0~I P}Ek`p]Mv/2aH4失+ W;Ht34jLc׀72ѣL:%PUU%J%[CDh8>("v@)ZAÆ"V `f!.`pW\׬3#ҡ CP!UU׈R|PY* (R#6e ^?8W 4S` @-q XFNB 5d\{o{Ei_23No_/'Ɖxxߚ1^Y(zMVlۀ` &PQ_XbXw`\F\rv#dl*ĕ$&qжbnI9>7wu7;*Їk=a70 F/eD @=T\kj(#[w^w [J-w{CP2#sep\a$k]8 R9)1KVL> LAS0Tih<>}K&X_,dr"$)DJJEyNR햍 (lMܙLKc[Afii A%JrvwW ^7唬N؂8qa1Y0 b}JBUUU( #a!Cq!j2p-)ՠRv%0,  &@f LEZE@3@ ,+H|n˿oB3jĮ50h ՞I@h< 㪬7I=-C`Fcu۟@p>?PVl6Z6 Ў7e:F 5&x Xt)UUUQ8K'7(9v3u,z]uWsg֘PA e 4CC1,ZMCڽش 2bE*5"O3&[Mu zFk ਺\~ -G~R3m[J^"|f8P'`3L9p!D6;؎;"5kGvaCG'[W uU1QAvq\e9n .%G/]CUU `Ԩ* `NYP݋ 6j<fu JP"0HHuպǔ/oqV:ɢY!Ec0;"U.GN8^{47<~vll@7VxeCւ#"AXuCf?;C69InřW5ZfWeI"Զ:$jDV=ТJU(Xܢh4[@,ZVTD:r!ɮ"I^1 Ta) @mmp2Ysd6r22Cv G{HuaS:sWevy`;T1\qcP*xإ4:08TUE*C/@0Vl=c(o$U(rS/ߺ'yԠtTD c?)6+5<1YSmH-ci,,lm+Y]izrc}Y19Z0$@T ^8{+22WY`[,?0dNd`H[rJsA[X4zs2WeVCV#|Cze="jD^,2X΅QΦ *1l@d 2XEEh!GH7!% Dss2&]H x -OmL86hh,{cc@==Pk`Aqؗ/-} &첯bd1YB6M6Rjhf(0bq(ШYu7gfr^ڜRvS K5^G=X*ɣk![qMd.CPUU,F0P[F ,"xR{Z! *īj ɊhD}h0ltjU DwA t.:H=,N34 sVڷ ֋Lfl$c(;UUUDF% >74\v{oZ+= WIDrV` Q nJ3yY.16HJQ u̺"$Xg0cqF~Zö[azRs@LiV0mKz1m2;4 @ [AdRc>R &63}-h] ~G~Թ*ԡ^&yT]޺L XfXԈ-dà"D1$a,L@cB==i5/@ jj,Z<4{)A&L!+6I I2rՄ6b8#=x u5C%#i Y=3^I {{IgP4 zlb\m.TavV {T5逪aw{ @F7.Q[W1ueGeIGۧK/2SŪ*D%B\H"ĭNLMp %h@Ԧ}jHM(Q h:{<-@V0y7pV&0;.p4jr6Y:&I㜃~kx "c5P@BnSa Ǝz ,$N3lЛٴ|.:쳷GWa-uy6P3UifP5 MV<aASUU("JgI*@P@Tؙ] @L##CW@}m $f{$rJRܦ$Rq2È6Egg'oKP3ǵ˜Ubh}.WIOUӻFBo`Pc&lB*_MhޮBF}GSr46b8bWeAi,3|ԈULMVeIX؛i ,{N<}9m=VE%E2 2[OX6X_w2|}+rX*> ID}pB95X&WuGe岬_Ot-Ƥ rG!ǎRR  ]()IQQ4(S[, (PmDI # tF$gq4r,3kJJIQ5{GO3 K}Cu=קe3+/EP8VETcQU U|=6vm5MpN?I@6a!&^W=º5ź*qYuҕy9L>6] UUU1* Yf!O$PEP]FTEud.N+!xMYik9`W}1@Md 5$t(!!޷_R o"}:5GE@/3 ac[dՊyh.8DYroou5% u*@{<\pW²7GM"b/'Ԇ05"+T5TUUgU4@n=`E#L M޵ ҉$ !!T{68(0 O.u5 c 5 .f"ў, 0d^6 ٱE IxPU$8m`%cp#A4\Tz`7] i6.VQ GVWe*]Fd ӛe,@=eD4K2 @1 "U1$JIM_m*2!d=p]!ZrIfu2`èKO' EԤF*38nHsA߇,|kV_+sLj[vvn: [ILsA"2rТ:6W$E+\iѽC=*M}7댸VU02tDfDa)ybgD )aR,KV* zRN"HaȲc^fw[%P $1.0m[dWT(EJQNb{NLlrQmT1d`tCY_g@2a< 65ZzW/7*01&=f\G#Xm# 6\D>&C8&yCB`aOggSsoSl< WzWwŨfWe a ]2ڋ !RUEfTj*($TR*%6S@5nJG  س$\`cg, hݫ3ZU%8CBZ\\8 x7UE_2bqTe2KKQ_4wUJز ("Tˇ(tc;' /J"Fv>&Pi30@4g~G6Tc̣b{y  Ӓ.=NUZ@SBQլn5oOلH+Tj jVh"jȂN 5JhNen!c6ضޅg VOWTؾjcT0zPR pRCx^ۇ'1mJ_uz2"ʲ)-m6b D@c~ߠ{A7$QZ5k 4F}DWat}Y=wkjD4z\^ ;nrgUUiGjP(tȈÐ^[D*B+d@+ad `ln1 ȣaPTc ä {[}2d#$ >Uq6Oe{ (rFj3<F58{U^W ߬dZdӷ ]6}#G L^cvۼozב&˛[soEiL6G=eW-dRU)0B%)D)?q+25v]}2|BǴr7zkp U: x&jDW$YpwF5"{U%=А!CtE^ݜ;:6F-v7O W ][QW#v'U: A,ɠlXHI' PFꍴȦױT:L:pHJ "2` Z!- F}ǐQ \M:Th pG Z=X//5Π|rP6IJuDgYfOfHFlpdo ^@HS{ZKLJG vtdY XX}OIj ఢ*@F処KQק52lLhs0!WUU)LAD@%ăaPE_sC@r$^L}1դ$yFI4tݞ1˜/oa 甡ѐ1X=s;(Ă ")\ꍂ-Ä.XvlLd$FmSMfM}a>-c/?n[.țZ9\(@Ye K G&at޺aB ,76K>vج5"+ne'HUYT)oTV-2GтPPW#-y}nr驑 4t|@`$3EmWC1n;`pIw(Ho2㡿~Z{D0.w`ȵ;To2AU,tn˒n@ Z't &9<\5 X҈f #9  W7BWg,^3C@ 9}arUUUf 4jpOW4h>Ă<E0jR7"*+f@ LȠeBi1%/F0̠ t;0YMI(N]-Y;M.*#]uedթX%vQ*QUU"CFB.9 @Ʊ{{ t^&HkG/d0 6%ׇ MҦ MV/)RUU(VW4 @nE4hA"*V eT*F74"IHX-#п?/@dP19~<5(L$) T!VyDPكFK9p,.?/7,, sѱ^lﶰ`XVXw=Y4kn pR8h{yH4Ɵ(b#&Q>vc2^WxrYׯaice\3,fBN,`"ŪBR;KDF pW Y0DA`%RM Pb_6J* K2.w{tp[5  U#bG1{9T>fܯy̅dz|KLV/MNnߊ#7h[H 5 T\t9hW`H;lw3 {sahn-~7a~,)΍_y\:5¢ ^tTHfT(MȨDpYqFb>cX/k-Pb (TbW^R(9{ NőgPXfUT ɘ3,<0^($>UоTzT!*h=y?/Q )^;*6GP@R_o wxH0dl0EN3q$[f H5އ]Upx>Gx }t.DKB_[Eɋ<`6PJTUU#T"e̡ܐBF.ъ0~fO)@Jڵm[1 ,ot?a Szп^0 T"njiO͸wY1Lֱ͹ :l)),-[r$K$c]HHXս>WA 0aG+/a.Q!Z9"yw|AGea{KZ3Ե\#Wa'uLd"4typUEYI `6m-YR KCJF%yZBLD@9;|Z 拣)JLbgk%q@tz̍/,L`./ 5H2Pdfo𬓆6 #m%@&$մd Bcp̆jz"`\sx FcW5$Q4(z L"%=z%Na`=!*2IlPD: X=6 W(I,/jCDFoنD'4IF^ `H-5ZH5Ԅ/[LQc̷fxu9ka@+fV$_ƞhJ"TURUO)֝IOcralkfQ=8Uo KgEۦ5ئIWe.1fxj]a }U%cVfejL&`0g YdqdZ~+ z[{ůvjB+@$IV}" m>G=eQ9*>ܕ]+n,<4TUUeDEPGnrLQU#/տ B#@'HL08+f63+(Fs-4O\^0 S`CΥUaϹ83s:cs:cmAF{A3ep`vh;.+z]+c!!&]}3nY DT k]98W,G0sc>85lc>G Wutõ>NԔUMP>b{ !0NaXTU'GxPFwվ+R\U2ˌԶ"s(5C0,(JON ubeZ&^(g0߾z"g9͊[Gf͜ދ|Pc^hRDLիHQ{'hodicY҅XL QnA88GƺaG*͎_]n)u&+f0JDvU UE:k(6Mړ9Q=4VXn#1``5X(Zb)48gl2Ӭ;SP*u`MJY1j c+\dRnU 22-B,d+Na]vX' R&o$q$%m˗ }7:Y) [F%ۛnyJ GЧ޲l!Fnl.3TU,ָH Pd3Ա:vL򀢋E$_%ê9Ygg=)5O h@1XEe0mB:4W/lkQАxX3 Cc]}[_$E7,S3O*daj{cL&d%gfbBK\8G`=?_aPMVشI#a^7e~JwݠgXQ)yL6PSbx }R|EDVa2l8m (O 5q}Nh0%V:s9`^K6U&v2r@i1F k@A`}" g|eH!N=ioهۊJRUp!g-6O% &SGvnd@KEgQ\1[Gc%:WŹ:]j{U?.,kZ;Q#ːi UUUb'FH03jPۺiWØR:9;_!18W B5e$"=@Ƭr}NxEPA T<@L0YÞ5 W/yAkWMsi1"XB¾uBHl#@G`;ـ*o\kMY"zgAja_k>+ׯ&eϹN; / ENJ˺#yOf~ U:]1M.RhXc(4Zp`2$=Uݾ^ ꩙5aE&3t9#0Y)v@G6ִ a4 tU% $}h= i0`r.im 'Et @9'O >G%Xn)|GPc2GUo\_x&AgUIBТ䠳cVUU]N[cْT`&PEHk^B*l6MEn[u@#RJe1b&Ug$3ۏƞ>i 9"4@uU89q*NAzfMpfHoۏ/gdkG>c!xB?k օB'=M1t07g 8GH~}.v2D1Չ`0&'GekC]5&yU,^#Yp'\UUU**RHQ**hP*H@CLvMQRևT3&#\>r)*R5fMo H =Ww;ø,1xn[môIyL5zWy3$T;*6E:ȝBm3;/EY\nZ$xA fS{1!iɚհr}Q.G-u{6U$ʲ+tw+ iss UUUQH@pmy Q8VNDh@`'/QT#'Bd#{ (,/TA$AC. +VO`/j@J"% 05PP @- 3g$Sljʉr̹iKU6iyN* `9h]LD8,[ [(@@IؔaH4l!dVŃpKyX[*j C3KyMLԘwFv^UUU:B&PjTk軉6 TL/?J++ ,\xzz@{bPI3AVӪfL "afԀk_o?@uY{n;`-$|M9BX$^Q_JN]nA.wP<%XOV5|h0R!#3|Wtwg؁VܫLt] 5k+nvDUIH"P jqK3HU(@ AQV$ AUMjK ,J `g=gefEmLwebQv@4D4PGWuտ#՘UYvq}|3P\cA@KndQ&XlQWa(ATү]~"*"R -2EЀ:dϹE,HuY Nb=ahY7]oOs W̚(Zgya`F`oHFl U:"E1 {ֻ+Ga2JE7fN?GZWeahwa b^d[Ոxhn*j JwI@VC]Cw]_hTl+@}~o5t UJ|_1*>2.V 8|tnnEy;9@.B GA_S<tWcYo 0iW^ef,pҡ:"Ic4׭sHj<'LkEڀj {b=PW'؎@J2xRNvRRC'۳9 hP#u x),29ITejIIw%8m4D0#4 p#\KSV  .̚S:J8&c^[A A }BBG@"_d;ڸ[cEsVbi*.UGa~P'j&{Un7;!xMHmB`0i}$9N@'4q{j(ATڇ T%TYY2vs(n[{g.%P fX)A];1ϷoV3MYeLhޛmOvS RϦJU1`0H3q(Y6=9!Gف,*kQ %ݘ01lUWώ G溴x-'zgMMIft?EQ iP(k"UZ@ ZDb8$ḠVm L{I1"A ji_$HK5`K ^<S MA]=4YkoN+o燿/1?oDv:4E;ʦ9dن棡DֺÈfD&lWPSܫ(?n- 5&+$;)GK@*"$9 IΈ%g=7_Sb*GeM0_u+4!T4Q-HqcɳT&Svg0&$m*\ȧ䈚6ILLfI^Mg2T+a ɗ|IDlY,9hש8ja+FfrۆҔ6* 7A,ZX6:ua/斛Lt]ssVDf\2%ٲ5I*H$MuД*vzZR YHX:):&JN`{8Si)0p 3c{7Ǭ2o?K.++3A0+{TR|? xU ݘE=-s07hBl@]+VE Ka]n(kJުh|R5֘$dIw9*v3Ve @eRJgiU/j#@B~5GT %,eĊɮ/ >k;w E ZJ0K8]t{$k0+a2$auqcDmJdXy.Ƕ!Ojm 6Նٸ3g{LtuH73tOW/;*[]PM&F3t2I=T!U#{H.\IWVYQ%&f(: teM

CCQqb4Ym ݓ1UUH( Kh - [MHUd/Y5Q)* P)@G)b.S[a3*w;VA; Q)NUU*nGsߧ*S{^A{H/Н"%uª&7~D]hxwUN@+'ߌu쿝i0TU4%0C WAt=>j{UO=yPx>mYz&UUUb @@ͬ󥮄 k m @;([4ٍ,S] DU 6YbSRn%< @F1I1b&W KQ6 $$c>PƼItӥ~9;sPTfPF?۫͞콷3-B&;SEd%7B vbF I؟XUϵ`PBiv>gvU dWw\ ueg%/Z*j@沕0Jt6BfCڔCSIIa8Xee;SZwNc{ijZY&qVC#!(WW/&՞V4H1WCAW:(&4WJ%ewr^JLfT>՞|SG<=&#IX{vЖ$J=BƖ  '߀~  HR3 |flRv0n0 ~䆵=uΖyqֵٓ67$Ajd/  Gn<=vMFm@.֠8'#bYaTХXj*qԘY2oPI➺*ϸ*rEW3h.q-|1[XPV~iIܒehC[%?8(XB6N5",mzP3 k,U0ph C OggSpsoS^ZzM  ^`_tWi([ x t Ơo2)zOJӪr|jDJ_Kf/u-zgsm3@.X HYSPtԤ:UBylT휟鈪 OAlCh3N=@ B zQDT&a+d9{&:U$-㰥oOB/ LO"@kfְQ `lhz QVʖO[xF\UU5(BK֖֟yl痷*z~6%K1yݳ?^ $ENV/\34eЈB^Xxq2XK q[J3$VUU!BNGy{χӞ>R6ovP%LG{ю]}\ta=+v{Г{:IhaK3.ggzt eE%.xdE3ԎLD.O!b۪H' +vs$'۹cąUsw9pnrӌˎ"Ug,㞈&81f!Ƒ+^uNVe9mL=qgaGN5$F9Kq*}wZaMTWu:e0wYujfTL]tR.w:J̱>(&1&Q`YFyi̵YU#`*"֥ )YSQiD*QD Wa~g5ហZ .䊆N5=D15.v'* ёl$r 8Gז*NPVo~n,)2@Kl h9IglӃ' _9kRbNN}uϋqI6EtY_5,2oRYsR5@LO#,!Y2$T"@90uLCN*y50d>|:" '*MI+$yPYcX%Pi:hBޒ_T)I%J=5 VO WZ~s]K^M mͺZ9=%i>74 :;үN>4e A i4-_#h4]AlZUUU |KvgoWPۃ*~s3%9R4ziq:ϧ3CnG \wΘE]5M:G2/[]4* 1 S9Ƽ@2@̼?#\xLֻSʻ&=H[QB 2HNI|>iӶDG2ou<|r,; Eϣikڔ/.T޵dq N a-7-[MQK/YUUUV?z ?[Z!zv:UN|4UʦkӾj3+{'-Y{!==*j)S D_;3RE389,॑tqd@R) zldUUUEW )k믖 >}u*S/858dn" pmX qwDBj.6L3#e}maF6Y liF3c3(.Gt͘TPfRd3<0; u"wWeJ&q5#^w~~`:v΃=: Xw.;I4ԬgC63=$a{Δ]Zڶq:wY&bbdtnl ⃆Dc2>T2.yM(佯6akW]_UˡG_oNz{}ѓڋ}d.חi{ֹ K7U1FL{=Tֺ0꺖AnȄ2G=OK}g(Oc< ӀYYGʋ12F8w'N%0o9iFcd QЄB8Pk\^q N cKPD{Q1Ĭ٢t׸x*]ŧ'wU Y4;A*v{u).dqy?gWU:9 聨U@=ͅ5TdEaX1FCt&d((rBgͤZ;˅d-Ar6:rFUT UJ\woIU"=sLgqƩZw<ŦDlϬ'19kv.LOLg@/BA24~l" gKjB(4=]UHBU$_ȕ/>~NĠ_X4}IBMhNG(Zc/^/cqic2doy~?Q)EԾe]Nx-]o=V70^h'7̈́&@P8U=Pyђv@ 0u'邢更g.g_3&-DτZe ے|D))K@#nwUOqvLN9ĎFi$Y+QdDv @k`>n-agѷn5+h?tPUUuym~)E>Em>m"50fQ&Ekc\0q+kN'9o{zwc虚0 q4X:g]:fhR6=kjvq? a@Huk]mxޱ2^~g'K-h#Z pX0K8o0iB Afhم7 ^L}J úMΖ)Ue] n3f4R UU3kXX_J cY!+U T,j~^nUc$sysVJ1BzI ai4L?Փe0:)rJq̛xl3٭r9gm,-ǾѦ*8{lJԒE,+_UaQ"~RWI9< h4f8H3JD!8iࠂ PuclZ2hͤuޒQ,uL.BԬՆezDQF=#TUUu]CUt_1oxG[c=o{GU{a\4YR{*rG覟YTN9|#S#F ~0e9Ir)<]&Tr:y$+l:ES9-e=3lmtT}XE$D,@mKkB*I$O% PX¨F.lh[\Zv$8 a,;l;n^j.c { ] 0~,'  iK?5o^,% 窪Tjwz|8n=w=̟Y:Sxf@Z\?lEqKkW W(l*zOmlR*lVL0LLM;Q T?g{™qNҨ5-S]bp;ȍK&݃E "lgRKtfum#K}YOu2sFWM fmFN .M7MY=28ۊlj\5tO%uaz uuƮ$*8bU'ތ\nC%F4gof ץ !G̨J*˪.PUV,W67 hmvhP 0`͑~r -teG!C#>V޵t~LĠ.1lӒFBnSIN~vo<ӛ{Z:7zƝ˥=sks][Ǖi0'$xakQsrщqbG緝Fov74jLVvW6'f ׷{ QQBD7Eaffc%˜jHWW/_Bv;V34"ap_&RЮS>Ya~n ~l')&gT$![1zY|ʫo0 uxc1cuew -n礲Z%O2u.vAN@@/^M[24*X QC49ߦz+v&dTo+T푛dsI#~;,{T.UVJ 6ZG39i\h+n4 @J {gy8'gl}vm=w]МIsAYttiJ.˖n3 Auj@QUUuO*Pe;:;7#O\vԼ9 k}mQ1IƘ! "YMtX:3rY2 F!obpS4JxQ{ͳ{$S QJ%ϗK !QT~+UĢ'Cڝ=bKl#] 03.6sSS]j3Dn^cz ^i3@2̝ɽPPܽgR;Ij Y_o sg|'R:׋Yq`i%T!ΞWʜF_|(ѩ0R,EWB;]Iy-cZF2AP QD Q2ì/ qC臟Cu~iDB_KפbyNgfrQ^y޿^2߬*|lex:MoUEս9+Պ7Ng Wn2UqB2Yazr!gzɚrn :រlj\T-cUbuWj mpov=TzrmE%,+vaA#P,oi+5։~=VD%>Ρ(L68b^@L'FYaG¸Ö&.Ui(=^䪪1@ 2x,=5Ůʭc_7}XܞE5`%MېNE$Uq4ETۙ048y΀ZQ5XY]b@ _g2ۉsPyIkECl զwV|eQO,o;fgĎTeM<QقD1 1`WAݦ _f #B.;i^,G%iKפ"$\& >媪.6OHn*+t_K3ǩk?}wgeV/( e-}ǎ=NQ+L4SDJX @9ʬ3Y!RtjW#̩Ӿv2#B#zvU!\_9@|nݨP85-g張G8 s>mOt P˜M%x EvFQѨT|֕>f\M9۩NN[e2Bb瞌UH"< C(̂)4KBan9gw3ժ9LOCM'$JUTPOM e,#l [j^_r zY!gctϛ:P&o.ᰋ~,'wb gKd! "DM xBvUUUM[*UGwMe{`/+N/lUswGXe E1εBB0ȫx#<}_HwR<^[=@5MZ< C!:orٵ `8PqBMii.x˱[G,CAeЄnʖm ņPyM]7qij2k8Ei-Bn!d1ۨa@d7([S^tmIeKW32TUUB„wumǷ?hЏvׄV{?q?Ӝ?N}ݧf&NAQQ /d LęY`RŰTvul'6~0ԇ*r/$'Ff %Vc F~ .I»K6Ӵ;мN O/Jՙs`v@aM? L&l\5Vn2F7S=tk~L}Ii jCR%th|$ Ic(Jdj[/WOGV^wyr\14f/qi{1!zq%2,q2Cg(Y5yʣ }ƪ_Kqu$aa Jm'g,,˗^s%+*%~$ f:ɿa e2-S: ]`iPg68jBR} teD#. h<['H h<;t#PUU5aY Cߦw_V3NXsq\PғCXy9gk 8 j W¡L";a ]i0żRn$D.KH4y8@2tִ̻j##26 z #g!,^߂X|3OyK=+Som8ѣY+ &=w&4NWskc Cav~, r ɜgvϞt̪>o6UwGv&6JWf^Ǜfʥ֤dXOtA tzk^GFU&Qw Ҵ5o(q# 13f}[!3L:say¯,U}ՌG w;Wt1Tsh)yO c -cڼ^mJ oh gKWPd€3шArPU5*i?<'Y+{7W+}^Kppxv<Rt"]Ywfk6^W zΰiFgf 3P>[=koj>@4EWq)Ec:5WjɶTotW/M^zFa2ߵMÖa{u3 ^E T.nPrf a`G>U- ǖr,:5ЛPUU{ |*vJy͕FV>=QΙcW,K$RITk&Fjgn I:tfyZi_#uUݯJ(PQ|LSS4=5csY=ʌrN$E, aK֍U¢a) =uh30p㍄*yGY hi50}0*ew0y~u J@Zܘe-)2Vx/52jK .L߲R>n',W'_{Zxb-p\9ooZ*&՘fmh&fD/pb;9ska(8镆J-JUM7i<@ ԓbN#5Bz pLkNF.bgBW{V-eۘRt0B@h:vm3s˛H>tq JK֌Ԇ%skDX@Q TUU UU_NZӵX/^ϵʋFŻ^'6\^V+R a;0i+(+O]#(vJRy蚥I!af@{wsLPRCu\Iibpvk!%:"IX'_*~"1C_4&R7Q{g5cAL6I|iVG Vy2.}YPf cEtgPqL b m<{dtCe 8V?[mzzƑ}Q-/sWo̘ u=SWIMg*9 `GeM<9"KorD5%i"Ϻ3!Z2j(n4{H^}ϪDUU^HIkղ73vWPu^u4KIu3O6b 8pb޺[d2qdY;O~|vr}b^uiDY4-8gt&"HHKr+QfOU-4 sE>_/>3OO8C gӠ\DdS&hNtU5Sɚq+4Ϟ"ׄu@CCΘ,PT1°,H%cns1D޴--?O ;)Ɯyg<{@D|g,*.a,Aa5- .>4eu@>-e3L&/8(,{\70' ǯ#wFn.6ߦ|s\Zqzb\K M"9\*\QE4=j]Ik r4PLQg@NWA)Q*1Q 9KAicmXXtUj_LM)Րlwr-uW}K3ɼ; ?hexGg@h]3&( Gi],e*=@nl[yJch7p{Ng2Ɛj=!FR揶͛M^._=nJg?DK6n%mpy^_N'PbQ5B֩L!Κt{2 ͻeCBOv b<GuYV羬Xu#yKJIt4v^]oo=p;VFkl֪EA02k م0d/Q*x.2I!>ti *gڦb B_@GXvd̎KUUU&,-=\}\gifi^8ΣW/o2.ec!9o d,fe $8Z3mvG}~}P̀dҴ9Y 1Dt RUUpU1M}{H{j`쾍$go͔X&N]RUC[#f.5?Hz`-I:,GVf%fbr憟#?gbE3 OggS,soS/O     ^te N@l̤FVѾz?:T.~N~qpbWUޏuΆ!JYs#gZ,6q'iX[@M 7q&2Pk`eY;ffo~{v:rְqX}g[g\VlJϪURc:yQ@Xd8B5,=WeH@r̈́yN.n膉'WaqV8 P0^V1,v]]mxt_M&.?]z# 1צ(aeo\H[Ɔ#8V;r!FN \ ޵nAܸi % `q~4zinrUUw{Xۉ-j{keU\!YY꣇ygϧ\.F!c\;i;Xe}֛Rj\'`G:r9]+t14sm0>Q})%J[RlJLdENӢ9鰜lZq5ni߆z8)l8hЈ[ _x;з.hu>eI /,4-8&wAESƳ{Ȣj8,1k^7#^|a{g?.{3E|s:_ULG M9a&`E@> 43ϔ1+Lp86PU{QKeJ?/bo^^Ĝʡ5Ј/́kXFٛcu~,qtWxuAZxoNg'bK#֊w|tvdΣhGja4GO^-ZYy+0{ D0(a껙Mԥ. 3ӕEC"R Kx gVŪ`"2,;R}vǷĸ&)<ϱїG$:IگH[sX !c`}:Mk\[Hn?HkXMeIMXiK3u{&M[CUUմ!f+?hc%'0:vko^euߗk\꾋9Iy^ u C];`i5G;Rr+tꕦO5<@_Ag԰6k/'G4`L,1rUEyBbe$NuĖɟ!+"4po- ]ztENtu &`֖~\@6hKI'YBUUdq!~oҰoWMߔ]2jUCRJ9=ͬ( +ጋe-,rCA4MLҌr2Z+(j'/뮡M5 ɜyY4~\+(P–bw0Wiewiu/{ySD#oщb Xu@{GiA'Pc9,})rA i4@^A=<ĪcB("ޯ{[¸;?$dꧥ﬽H95O"to~w2xjeT d[i0{`|*5WQ+Eۆ.D$QzB=r)|Y6-f<Sk `5I!VKJ꽬u$.7-Im~tɭ5> SD `ԮZd{Bo]\<\1<$,u "]Dc1)t4> G8]½3{%TEjB?.be}|;k}.ȭ$ks1:&U8)TbjJt]ddƞsƠl$U|z !;lN]7vf%s3wvdl؀0I-I1AP7x,[uKm,\8ypҲ_$5\^>l8G~ *X'r)epcZȴS^uJ bl\ɜ;5{骪 )FvrrgvFQ-"@4ǔrTvo^bUw%^;U2$2$s`Cvχfﰕy` ۥ1m 9KMӕ ݙʚ=2ohAOW,x R%y",HaI6^e50MiwiQ"tdX նF(}r B>|OYt~܌`1Ö:N &4ݳ4d檪j+:v\?8ێ=)zoY.Gl)_/$"dY <}zύ|tջ덾.H`_҉sQj$p\|;-pֹʯv>tn 5k) XYpesUUUn^wdA1k沽QeÞ{|\X}m$ӖYk!}*E0.džDMMց?.Ikzj>0YTM>dnM2t' {Ԩ׶.ŀd'jQJH@@o,jX%Iykt򸳠Di9SvmCw(3aEKМtC 沩䧯~,eJȠHPuNDj;XzϤ3x(WU!1e5}vgW;2L ֹxQm ua!]knjmdҕ3Z1O-Mdr)Q4jڞ *2]։zq1âl$U/df)ZHr^f^Ȋ締J%IW#A՜ul-ʁƒ;n66- !("|<ֻk\ `?8~Le Ă>e,S)VPK=ѳtg* [W䰖~^>o0Tj_}/޿uUf]U53ImBu㢱s>Xt [h}Rsـ|rs lc~ŦmŰ=ގ qJ ,\6)[{LX+{4#ѣN꜊#12:n9;8tӇӭW|_x|4mzKvoʞf.\z4ԳdfrnܮVlo}[M$ U,r}盓x䜪sc췗3o$&LB-]TS~IlNj?(._Ey:&Oǝ^oXӑ8Oэ ~|UzGm5ҭ >eUQ5ƳOS%fYD6>3$$MsUUUF&j/©g!?ϩ/YtJDo*{U@fZsRE 6t3T݄@evajH!61S4˴ݦK ՗Xuվf12ͺ{ZrCA&ƒ-!G)* %JGۨ<<Ϙٰ5uvpaʳ/@Κlr~캭dަd~m 4Ɋ)@ٙNrUUuDOTB-۟^Çi~σl_Swӓ9יTϋceVwE IQUBq0w(t>"v6U39KSP6 PC,f2vk돱/\^jS/)ّ I!R}-yC6y!"v`phPN>+lƲ(}hl"" m~ޒ~E4-]G!v=z:ϪCGWΘ˹S;!ep=KBrR5ꔅصhKx)MSY24I4hҼg_ ]@6&EDklNtd<>׌?Y߫a聤Yk`1֯m &0ƻ";p7~ Oo;IBOذhff#C  vX/\SB`ntd.š%+Kl0 @#tfQUU56$a_/e_knC^ocy^_kU۵,g-1juV:0ʹTc@e_I5LC6X{!GIpL5 sU֘CScbnylۘB<<Tr YYT()O, oᐫt'XFg6v0J`ӮQ =|olE4mO1l)! oaN7J!\UU5Z䨓xg;Zt?%wu0$4;ܕv,J%}vmw{q9 *Q'x0F?--<'#r[e߫neL_ftϳôO@ PŚO{m=) elGP,c 痕C|!besF6OW1xjv#dnΜ:hbZi{^PKd}^te,6 Ԇ+sR`Dtf`*RsC0L?oOoqU?zчҘ˚\ƕ`{Sҥs e A8ȄDUwRkP ^ ӝ$z$$L@Lz;Hzn$%CdBQ'~kSQRJE_GUW1c!Wk>\ MkÆv"1$eo mat;-~㒤p4m 4ޒghwj(KWUU524[U>Wk'b}iq>j7yQ9iwUUUm d犌T:O-34qȠ.kYL"( ?Y@2=2^PUUpȢ6M2] 28,~EO'`fM[v/l5(gYee:j[)EF1d5@P0uf!ګh_s ډG\R]ĝ W5٫؆d#+I"Oe\[i I&٤ Boh:z&o=PV&nrGɊmJݦen'0jwpw~~8VQ1ӐtmMֈ4%]HVUUUOlk|,9:Z6۱zq.:;Uv|!$+-Az-@6 5DǙHeyp#* r9rE9ݘs+!$ Wz2:y-MJ>RZVp%_.=e1b/i VY&D8WPI$r&7a&~i޵~LȠjF9liʘљu(l:3 26Q|J̐~*w6C];=Q6gi3jIQu:Ft% " gb`0L=L6tЙ1JbR,қl !$]) šƞY]PETE}V$_/i׬Czo*b.Lψ{x\~خ9b.㍄@ Dv# UK/Gxnt$`G6^eqKIl-AAx]AtO2*WaX/>??2],v`sY/evu:chSvjwbw\tzjeULeh|RW{IrO.Ш<s$=x`RZ#֧ԛZPUA{ʶ$TkEoU^ٹNE vwӰ +@ݓ݌Ҥ@ j#K1UNs/Z>4~M w gKSSzxvM'f檪bW9"=v_^݌Br=rm/XXS;Tj$U,bYt!Dg5\Yuf{]{u.r3鵴 IWcQA 14AFáAc~y2W3ilgi`Қ ,YfZ`2 g)C~xjrF#GZ޺F=8>ti\6Ī 6-u\ld-}=6zӌN2sUU(I(oyse\Gɇ9:zz~.YAΐ "ΘK8U 6~챧:$J%B='L^6b&(7.hfY)za[ :dϛ f&UJGA H ,}S]5Ȗjzc1}ݼ..<`e%,ւ]6!MXvj>4~TPXiKR0v(ه*(`,I_WnvT7SO6sˢ:#{EW8V,{s2jh+Y#ӹPgL- 2;Io!Bϡ}>c{.gܾ /~,u*/ –/1 M̴ӝi-E6~Mry'LvZ!mL7 Պ"Y1fx]j8!ޜ >Li \h8[ioEG{8TUU5A-`EWWe`UZz?o\]Ҷ݋~tmu ptmNVX@wQޛxj 99)U᳠^V8a`a,@g]6҅լn]sl$͌MWxJ6x^Ig0XH =EUyN*T,KP*jJ=+ e{t۱rKv@R4vU3 Ra75>T ~K(>~NR gT &^_072dUUUs|Ԭru{Jݼȵ ZWN^q>\uӻh.Ym8n̬iC^8aYa&7fX|<$QAJ U kg y,Є4=Qv j{h& T) j2GFY16+~eؘ`jŋ[W% 8ϻ9wVlTH+8 j >4qT#l\$g`v;\UUUJ YmmA8ieW:YJE)+ǝ[4^,eգZc͗%0K ^7+,dW?౷T5g.5hS(㑨dIAM38IWI՚Pϫu :0 /Y `*D$7=={]ueyjECA9AoN{՘`7{+eN`> {~uM*bSWfϖE6٠MZU5wԅüx|SWoN8Eփqe^mI*fy}Nc7hbNH;cXd2/f^&$;lg98wy5,Ep[1d0*)yX$$u'tN4Evhl4.@cICnH;<ܻ!Cah#KEBnLƢ>w^LiL\Tϖf\@=t#3á*RUiE㲒~5d V Y6pʼn)-iKH.L~8v_XA! >34 Xi NŰ0nhL#ƹ4JffHUUÚ.\NFH,E˖_vꗇsܫ}?O͓+gPYDSJ[acgp s.@bCکŨ =T1TNj_U21 8S@4 TTzutNIz{ Uynx^49T2;npW(s@K''WFîGrE^i *faliS "xowzҙf潲t_w.ڧ9ۏoތ=m9X;eEZO?ozW_U-N39 C^L2t2BB6V JQ!կ 9Z6:1Ng8M{zs_9wtW3C:,]6 ZbbE|hK\}㇎ҭ^q(E';xu*3;Dg̬#TUU UAWs?O=6>5lWU~4է׋[]V/#TcY1'!&K].N̳kJf JA3]p U4Ũ*LrS $t\z4T&+>"CLsIPP U*na4M*UG A~ l8D0 '5S1pvF(u[Z6Rz0~}N ѪgK?ɋ4Ff#UAEoV;9TݭSwzCy1{go9m cy2${ü3fw<宾"c 3mQaNfdt@r (^EV\Ru5W&itHG蒚fGe ϙUh] ^B_4CLCgm8TmPr1G0V(,u.Q0IӖ,EVFffl 6b+󋓹۫ύs n] FW<~;Ԯ{yW+,']+j֥5k< Ȯ V\R1W3qUq zwhvY@]I"I &9t+2^LHVpΌm-4 62Dj+^V$o|\ݳ~5u;-[Ґi@7#x ti"Ѝ1u8Kn^}ЈeK?l\P6/GzڽCUUUK|o않V<"+\Ӂ`5]v^L~9r}}wFNiJ3䤢ga̙u5Zj]!!$tZ&lS]c8iDA  @-g  )I+Iv(*6ݹ~}?0ӳjukl@ABf[g0M\Ⱥe2#ls5tu x h[&/%_,Έ*d44\.麸uұό,v,x;bJ7SIgA*B.`F91yHQ@XQ19 ES.FMt@)i̬QUsbO5+T[7l@w^m/~ z5wW_*VY(DBz;}֌YX^ X07AwOggSsoSO      eAf!lA(UUUBBjn'Cdʯ]UθF~YOe|ճߌexlN?K8Cv 49IKxNs>=Or%@V\3I57IeI lL5#=3dMf$X|vC\GF5ъƋ,&V{_ۊ\5clL4a#Nʳ9tmrdg0 өEe䀋Qr)bT ݔvD{R$n@ŏcO9ƽ*!>vàU}4+Hi"F0f,dՂߩV\j 1˶:׽āC2v3b-ⲽ[`qbg.Ph8޵eiU\Ö". +h^vfPUU֔Pz9{/i:==w?}WlAt]]oS 4 CKY00&!(:j&p$ ~LSjh°Ēs|+ޫd r0G4S66f H 6> tiv_p7;;^LqLD[e !lܙ4v_2WjB`ܣ!ߏ_nؿNv׊~bαay,Y׸GNlPgS5U=EE@;\ .4A75%6 4=D3HͫbJg*)ϝd*nFuΩ{+f6G$I"rF}^_/FT):LQL ҂i;͏$&|D&~x~E Š g˒ƈb|.xvfF9TUUKH~z4?G>o8y.e7GҌuChj4pMj$5WL _^v+85MC]1vyyf\~zdzr YtS[5k7Ԛr'3b>v?3v(#Z1BBd6HJJ*UjեdJ} 4E;#Ջ( JCB0&6dܷ@?UteJ$*&-M &PUU՚KC6 G5ߟ{%U_z`4՗g/fDkL=IiuQ*9w%1had^yΠ#6 Q8CIfnJ9ʙdLgTE?3 qZX/!7JJR\'GF7;tw.H׳v,Xw)`A$ Y7Le@h_g>~,Z1l˚,4 C=3(PUUm _r7jl㟤m^;;Y~ߔ4=T13ٛc7Ya5IL{2ML*L5Z\Stkww*)LhA +[sfۦNu}r dn6-/n2DUW|Bm4S)q07Ym ꃄvn^ZRvB#! wƗ!G)~} NXxӒ@= 쒙QbUUR ^{%_7G&X?f\:EWWI'K1u*k& )ϰffJ1:ygFEA!*T M&J*VRQCM$t1Y4d,Vi,Y@Ӛ!(%h3gkgrdi;X>4uݲN[Acؒ#f1VfoNUUUMBzTw@> ;U=]bܼϳ^$p-dHl v@\UDTJRKTR."vYslH2Qh1h%Z'֯7AUž^#Cu4\޵e NteNV^]@9:3seʇ}Qs?T;>ϯ^.nf-U9L!+]κjLogo3Ņ]^j 6o{i2Ơf-T -њ |cm=ʯwvTWvMQU~wlgϯƺ ЬPqYl^P ]b!e@d]՛'Ov&黲7?XTpLt%Xk@9.Jn{MlOAwM E2>te҃ƲN{M4NnPRUծ*"pd|—s˟J+8|,;׬>T_ .*3(1 @0~9ì=fs:}^٩YM?tO4+*9>s{zGUNػj緹8'aaWK!k##;TI^]Я۹E85zik]u =ǚ3*m6⚽Q#3) 'X ֤?'g^uquL^(xAYQR30BUUg$Om]?yTzeH<;hў9؎tbX]5: C "a"=Ƶ4֨& =k:;ƌ):X[4+oyWv<0Ti^a"8dP㨻)(|5p+b.)jY$*jB%IP^hdI4גnsTx#?7 !{ҏ [qEˤCRw9 D:0zhDX 4-}Q"<r=M:tW@p~fp`eŬ>6܎/9[}q< A$-p/АRnO G;* CmJ-IcRtP6Fldok MR~0$<sqr[VA(ǹXB[P vh] % ASViL biP4e!3KpQUc":[Iz|lgI_'+9Zᣜ:j\5ױI%zNfm&"kr3^`7y(`L5 jg3BxlGVjpRkۘdE2B$¶r(,WT!R}:Y(joeR^wiM1ԗz@@Czd<wQW>>&^uA4- dB13.l'TUUJВ;n>r|ٜ7Un?H<_Ml^׶/'=t΢b=s.g =q. DUɢvi A@0 p]\0ANbm}ND@ο7qHjF+`r̒-6Hۺimx^J1AQp|!ԑde񢠼ӊ A(w 9>e . bk)^xxmgF4uj48R*CKʹ$T.rZK̓ܟׯ+_?y蝳NM-.P^3W*jhI,H2ÞUTƯ4rE{^N!3%H+99|Xd.UIiCxPX86)NwlJ-iM=QqB&BI@5Нn@`H٧E3kGe uiX[Yg#/`4ޛQ UUU5ѰTӗ+]tV;u7<# ]:3.MrXY~3kMv.6ԄZƀNJ X/jgdX I[}]Ԍ5qϷyFaggn0grM^y fO_2c+2Vt'HeK$gC4m3GUUUMX1.IڎؕӾ}~*sl?(SK?P&mg3)Wfd˵uy GP&DN9D7P !gba]+dtUV)61ɲU謙*7) H9_KVȪDSUTj5RU0c&T3 `[ܶLI5)~CKѕk00]ޱz /׹Te[Z+`5șTCDR5qX/veؖs]c3ZФ3}gktS->,EM3@`e c=NW1s)m0(lqb@_KıH9}~y_g` Vf]gX0N̈́CLOZkU[h. ݀+0%ٖ~LmJRb5䆳s(hd&Mwإ*ͮ\c/.T$5('s.g1J2;f z tYǓTD8{%`^[`4 Y⡻Y7Fͩз iZ>$1]ȓXyIE; IbC]cCF)6x= Z{fEj9 9]k = X׊mf JyWQ,2FKbfQ 0$T*|YF^JT+I%Kcu/Y M. Ǔi&M>i-Tto:jP{ @Lp{~A~4u f˖:- .:86vftUUUI㝙BM<~F:xS }Y)yqڸ')јC3gogXֆBM,hgP# !Q˦WLceHԠ.)4oԕ݀~Ⱦ~"njɯ[Nv0H"lҭQUi(ExVZUJnjm!VO(u7ܔ WW Am &ٹUPbU"Tɻ@2>i- 2 h,[rRބ33KQjȔ)p=2r??nn~x{wmkDK-l}T.>ތ{ʾ{_j[[i (ŀ*_ޙJDd3L`JXG8C °42n MyFCZDg ^QEXB B* A-㘪 U:ޫ鐸jE>b̡Cvs Ѷ 1;o 0, @'ē*!mZ bhcdIʬj{]B7>Y ҰRb_l.^q0Rs)ҒZt0adTg4ŘU51WF*hjR`͢S>M5sݨ琟,xC(*E"*J*Io9(33d#Еr8󡽶NpߕNnk#丨Bj޵zhLb/)3Lj. }у/lyIx?XkO/We}cUG GaB@.f);اyxb`T՘dMCMzI*WQD $VeM* Y}-tPʮQIt{P~EaI~vUkf11 Nk,pu]i[#9vM臺 E@Dc7lCh Ǿ,fB m[4*.Q[tҌ>:WUUm`,DWie\N>GMHE&9`,+{j* #JAU5=y䦭Fk~a$P{Ψ3MK~_ڣ%Ӻ2uu$Ȃ"UTU8?hp+|'q!@-^嫍qY#@ZsK Ȋ Q`xC}_LmJ&ܸP8^\]2f n*&Ř|_lSw_#sGI wW͓@蟗'󢵗RE4Qi  lk: j1I2@hik,:}vZ#f1]K dVZSAxP,ԳR9B+ X`BSU*W׊FyKjMGhDa4{N 𱦀a4wmiN ]Qܪ~ 5kiCR9ccJfIxvNӽTU%Դⵗ<GKە#曹V⽲4eyم)M*Ӧ8-u$zY9ϕi3TjAa0dA啙lQF*+I;ͻ3{OH$UTlVxBȐDYIϒGqh,n"ɿoq %2y လG=av)RF\΄ AV"f.*`~,օ@Ҵ9th<;ff᪪DðߺaW~t[ÑGW7良i4]ogtFyTf8D D:j1ꓠ -̻V1=^r!2UTE&5';`rKVlqU$l mۢ+TT}g蕦ka#4'.WյVKp')a+2F&NIP͇YJypP;t~U$ Ls@&s GMUUU7:ߦƇj]ZJڗo .Kk/H7'Y˧Yn1_#⭭Nօ4 F !\&+%:V3Wm䄀l Ӏ 2^ιQMn'WcK%B*(R&"kUTY2^7%Tϻ2cg|+{ݍכ'&/]"W{L ~E*QVh8[2%9:x(QOFr>m6QhӊߋuMtb3i%ٯw݇~= T1Qߕm5ҁxUr`TwM~\` 5F2kUI읧@OS_0~EQw@Aޮ)7$-@Ċ~dJ[u\5ckQZtd!Џ@j߸sگVW09t\&WCl%dÐpLRd3yW;3Ls.n(y:MYzB}>@=THdIY`]T>ôz?&άs9I6ATF( Ox:B]f!U@Cܙ &3*)33Of㩁=$뒟JK0OwG:V@z.[4Y;'G9z1TUQDzco5 {Zn=Ȑ&wRN+5]VKbRj"dgER54CNtg76)dOoz*c$h.tn]lfP]қ&]]#Ȉcxo^m=]n$U HbK&F˒+*3.)X~([`c5BwhڢI)RozG( ~LmN$1mY\t+gc;'5WBUU{Uw\}-.'2ˎI (\q{je_mgw rte30P `隘*xImDJ΄^Xjn4cHwC[6ɞs*M0vVo ji"BX&TWȥP~TUE {Ŵ9f]t]/@]ed!ĩFGC468\8wl @@>: n۶=e AvjR58g5ѳ'\UU+EW~vj,FTޑ7ort^+bѷ3 |ݺ8- 3LQNjM`޺xbul/,dAsr~5?NSȲ[C6Yu&R;C;.OS]SC+0 FZN 6T3d*@s:Rbeܙ-]5iD$Dܡ7 RؖU*$#gmqpIlYκ^kaa0 N:%[3{s;Xѭ݆"}U (D v^Lq E҆ec1@㓁uzԩR4S1c]lk5uyg''ƛj x_쾱_Q`95)"C'[n:$Ӝyj² >P#i&Svvd"sE}D1sʴ;PG:bX1wpTq624q`[ƙRt&bZڰ,} ªœp8veIUUXM+J+ʼtZ;Swrq.,b|u^_dq6Թ#ET;(i`NY/QxTVrйr0P/ݹI6*$Jљ外Ey$0ImiY+-!R,[FFZdl*GHe0ԣNK G-Vozk 3 d#teʰ12%YE>,>n\l^EiRǹŦ^Fgde Z= Hsm烣~||=9<,;?)= QZ;E%Us3d|*hkVC X׮ ^PE30'mJd~nC҂C%409rmuD*KRE"I$RK"J~=dQJp.&ViѶi;46v[|>9!,)6p k09Mgvp }0+Z~tu)ИmMVD!^,fVUU>$9jW7џƕFc>K-ΣG=T+.=2eqP (j:iSVdSp+Bx_L2HQ+KRvO{#4d8` Kɡ xPH @_ ;Ķs7vw hC51­1]c ?r(RE]VyonlD!_46cKWe\Mk!=3 Q T F`Vzo:6r]n]$\,gݹIjZǚM{y7*s9p-eu &Fv;f1IvwB p3 m}fLΤ;V]R) XR* mEF2(UYQ Ye8$.VO1I 47M t@Š?H\V-AI5:"+| ](OggSsoSXaH       󒼘 1m9YgP$ vFWUUUc'kNӄaXwmxp-?cڑM|H)5J]`>w3#ɚ1͆Ilo3JdDMSՊ3R9GĔK4Tٔ<^9 (U|mTѦRseU$HeGUQMB.õ3G]b&)1Hb_$l\ L (żCݸ!!InGe~Q.qaKhk"&0p{*>$ @-a[&?i_Zם,_OiL>:2p[og׋"i1,AN*@@ qhsv-5f0Y7la3z8^6T&d? X;,TTGϩgT= -*TKSɳPMӿτJp2)\81P>h0$#MlB|z88K~ۊ˸,m ~XniF4-6E'ƒәTUU]UKY%mw2]0AY@W^Pkf ٨ <8gs,6[ӛy, Icg-rHryzw?M3^QmXFrHaX!BECu`UƝU~2'~=heʈ_K2L~nI˫Lz0ennJ _+[֜ M7g町_CqH`X~} 6 粥Nk7A Q' rS;qiUOs?Z]<~]Jƭ~eZigF} QUT3J=+1dgO%@,]t{Z+K1̗ɰE:Ϊ:՛1s%>=ٔ)(Ryn%*-" R"xO>y Ӱ:~#w:_&'\u$$|ށ4A ?$=hr(fD$#w:e _KZ b$m<#{tgjt8;oTzӲq_[Oᄡ yymqV&͸2SMpj<Χ-sF3U |ֽ eEh` 7͞ju\$nS{tlfnW. ᔊVL ??c+vw'Vpn ױ"u8MIis?>אK}>K q@z~LwD"jFϖ,=shzѳGS.L=^G[>'S"\i=3:$*k-mV=[_=|_l?瘊;\}SKuYZ6Ir{oZ0 e89>$Uddʞ`3 $";$38d,}Wmt;"{J5)[d,M;dT 'ϥ<|Rv~r4ޅp6[h.|h A͍ F2"ۀځyF u {t~ AsR5Y1>C*VU+WUZ~w^]tO7o#R 짵On'yh]}h؝L#ÊbG.U#1KInKQ"y\pBԬ6owzLyǞ1K5;4a=(uuŘRϵ:vOQm,y5Ȗt0wL{.s#\'-"er0ݷԝZq:{Kpƹ x4ʹunl}EB5(S~eUi2Ʉ`E{#Q{0sUURŢQReδwWGwݬ?./|_W^2iL=.H&Cd%2LZ~UbCc2)3 4> h vϤF UUUlf fMlq4)qb8~b/j[oiEԺF}pY}{m-qv-IF D18hȆM=+{`b-ӵAC6kpHE}{?gJ]QU$vYvN,LB!?}3ߦߐ ڀH26A8bB" =Xԋ!՗Jq2|NmĠ  :敼m&b wv%TV 46Ƃ aKV蚜]zff' FW /?qq'uVp0jϋ<.4  EO撢 {6HzzCJڨFOf4^1Y﨣65ɞɾOACn8]󻋚tF:*E*jWT)yhRѶ>J$T|w;+`jɰ?y[yDƄM0 bBWюM^6q۪ng+29~,u,"ՠlnzƻ v,YUU՘XĢis2n1]z՗{Nы8bW n_ug ;XhF0]US]Y@ULxGt@N.Utz3oQ59N"AW Wc8cc5EaKfGiesp~yFGf+͜sr!]u9l‚DLNV,g3@jwFwGCsFACR%ZL!ݓA3z⍳RS:SN{o>NϗrѾ/<,b1S.ΠjfiiॻL&WGl>-=ǘMHSuS"WJ&tL_kc<ǐ%-%T)}` ;v*GJڒ X鴲p-yS]0goUáD6e[+1A׈W duV''4{<,eN lYۘneC-AaghXUUUU;dDTK]Y9q{׻ڹ^)ܼ_oG嵸$3e+de9÷ S6 ZדTفWWzjneDG|݃joƹZgOgyٽJAS{k"25wQ=icX/̛aCgY-Wahv,E _bМI>~DAҤ%^l2IQK5fؒUv]cB[?W w?g>r_1hܽXM,C+Q-); 5dqyjP3Ӆ_yΞc>0o>׽3\d_øZx=/{=8Nl I-vHEUy(@8 9GVXtbJQIG2yv[`jl|졳|69c셹5^~(䅑l24ѳt'XUU! H-52}OS~ ŪO^|\mg,֭^׵˸4sgEI0j躒%sTxº&o%+Bݣ\T۰Otv"rNQUN$/%EK%RUEHDx/BOcn}hi'!{X Z!ਆ9_e08`Xؙbt5Kc;t&A57-WRfЯ5nɑdUUUE׭nM$dnh$Ŭ'KDTY} Ń1Xx`Z#qo8҇V=%2 yk43 !$a\+U}q HQՅR'U] edGە7n:-%nUT%JyF&"ϲC"J#B& .ٶf8'4uhY HcC#[.fL#(:~ɠ]c21Fb4-=UUUj\_8+^|uZ[wӏĎ<ѣ3Fu);<=gNYSwo}0NYOcTl@ %4=L'b(eD uu k_0$2o$.$ȀO͊,!#ݖLH#Nui*ȥ!U`Sh&Ďb`\4(Q4!78H 4f NU4_K闈FA9fiD%-ZUUHݝu"y nU5bTb4njSm?4ͧSNk*&vȮY##,*/Ѳzw1*,M۴炇xZv=55(4bkU}ܟC$;sefr5fԳ,FkFWB,Fa +$2Lk|z8C!R_*lb$ga むB?PDi7ȺB`0{*^4~lD.aK& tu$rUUՊFj/ׇ{GA _n3" kY }>EnE rUZrnQdsye%%;S^긼Z-*w|iU7' G{[7y5'Ktdge)Lg@cJO UX*3I^2H%[_I1]5E@1ZP7Lu&k/Uw;فe+$4$dV2TWw^9*LD$|Qr t籌@6 lK0lRdE_@= ̈#*D}_{떞Hޔ' aGPxZַ}jg_NSVyH&"K07c\.Ɩn!PhZvUXR; \EZ=QavnRC+A#du9wծDCQ&Z-ɧ; ŨAT>o}`Ddan; ޵fq'Ĺ1l)BȍgIѹ*q굸/V_|v>|e FXk_EJ/qvկ3)j&j5pSh@3kݞ[mO DfQ QC%~םt6zRz Ur?P>.κ|* (@EJj'U*TIR-#)]8:ũ8ElE;lqNeEc|.CC`ua4Y L}Q0DC2Kr'Ty.t#RlfO߷Cma}vʷ_,%/㥒Vj1ޅQζ31{jL̢e^Vw7dmĩT11TDEQ%(pL43robR HWα~dWR['E:fUKl.diwT*VjXdR ՇX:W3@7\ nrڇp8%D蒡x\ +A {RZ^eMjĠlZS'Pod^8J_jjb#A:fi|G_$lV{\/kֽh\3V Zơ0$QRTj&Qed"a"?+Q dC lѾ5Wd'+uL󤲮s|8=1- WK?Ճeé!g 2iJunaQڻM?ma&3#VB4Hl.~L~tAeS`9ȞqUUU[QshV91 k緘>q5 ^AnNݣsd3[{]աYV"قHe`V X'ֽ9a&9ʬ,_t FUғS58'u{AXՕX침XIynB"sqwi3'apȝYF,%Y l7VjNr{O8s7(u1Cfih[ވm c7-M] mld졪bWQU2I^p;חxV~k^|۽,|x]u5NҞVũb)IFKIĦ1-Zk9UmfFŚ ]XTMO`0suȂʉhRHjp/}e_jnjaaȖe#uiT^0Bے,@ݺ]U+u "B3a(H%uF !lЈAޞN h6ڗz ~ 䆮KR#{x hFt9ٕMbo?1uzl.ӷ~IuRYzuv;Z?Ukr1=utّxD)|6yNWK5ͮ֐kVZFSJlMȟhX[r Y2$m$dW˳ e0VmpN^1Ī)8Ȗ 43xrl]R5H0T[!^,*a/ iCҗ9əSƳ{,)UUUDF!vO7VtTT#YTMF2ܛA]˜.uu]R}U\rY$<3NDd2CI ABC$1߶1MU@LHeJ3Hn7R *pKݑBNW\p!a!R]rd2Mi7@Nr*#`έ.pS6vAz4eLȀ篥NS i+gt'XE1埘ξկ< ۔veT]t>&t*_ƥC"xBFQaɴ .$`Y*''虹N6OP'6dM9C Fw9IXhzqRj$T'"%C}0taJ![L1QmQ0Uֵ܆>[B[ Z ~L}I 29l=(D-Tؙ$+VUHxTCtyltgBg>OůEͱ*9e:WWӔjY9vf!1*bU"I4ۧ}lGjKقy{&z~)3 ]eU7f֎P^ (PhT=VHS^/:BQ a2#(Q=+K 9Yݻ<KwngT P,8/b{ j9ӕE,j+R ij JAVԄlK4QT$c0LN U} ` *-(Q0ܝnĊ'9 [XaDluI+&ml܉ kJ,YYUEB40,&E+UY y׋O|ۅH}Y"J&rG˂̋ { ϩ6(l U6=Ug*w^`khard-0.17.0/misc/twinkle/sounds/ringtone_segment.ogg000066400000000000000000000760431371517016500227000ustar00rootroot00000000000000OggS Kz?vorbisOggS 2 y@vorbis Lavf57.56.101encoder=Lavc57.64.101 libvorbisvorbis%BCV@$s*FsBPBkBL2L[%s!B[(АU@AxA!%=X'=!9xiA!B!B!E9h'A08 8E9X'A B9!$5HP9,(05(0ԃ BI5gAxiA!$AHAFAX9A*9 4d((  @Qqɑɱ  YHHH$Y%Y%Y扪,˲,˲,2 HPQ Eq Yd8Xh爎4CSR,1\wD3$ R1s9R9sBT1ƜsB!1sB!RJƜsB!RsB!J)sB!B)B!J(B!BB!RB(!R!B)%R !RBRJ)BRJ)J %R))J!RJJ)TJ J)%RJ!J)8A'Ua BCVdR)-E"KFsPZr RͩR $1T2B BuL)-BrKsA3stG DfDBpxP S@bB.TX\]\@.!!A,pox N)*u \adhlptx||$%@DD4s !"#$ OggS 5X]-A;       4qkh~OZV%45UUUEukCCC^'ZcC6;$('8jhʕc+FC eQ7eqaȔEQ$I,˲D\u7˲,;$-nYe"0u]ն뺮,2薤eng8>p|~o6->c3Z0>0nqW|d2/>6yuu 4JVC!9ۆ-z3x7E`]C0:[$ϯ~u3;}K=)fp7RFuVu n&"XM4 p+B@% Dέxyx>rhg .)Z)F+Sp}{aMG3kX ]iC bTDLj[8u@V*0Zhփjܼ  r|%_(l@{[ f _$p@k`îBwoiw\+<~ٰw8yߔP7bg(,s)" l[ut0oO! Fvo[sʃ+畃`<@`|Š( V`eXW QPI.[寓`Q*hut[j'Ij'Y߭f 0 #ZU`PBqG+.b rATzU>[_ g48h) bvE^UȯzAýX&Fp=B-#_nP a`P #Ы2 9G~Jا(pg2@L@y[?.KYC-#t(moiZ`DfZ M B[WUQ* /֦lgd];\ ^zX$z, y}//[\@_4AAA0 cC`dZ9y4˅̄2{O6И:AP<lHrR! t2"0QUP~/ֱ'= s H&*3fۺwo(y^J}fսPb M@(@n>9ًȰ!vb"]OuU\o\z:qM9'3 Ѐ`BG!P fdT\=QV?B<ىRLZn5h **e@e%"<xFvt%yk3+.SyYWY4mzEMS_ݯ\0(@z0p} =c`FK 3 , 8|_kʠYf(?% +4VS"jҝ~A `L` "AFqzBW3QWxPfd /ѵ*ՋDPX(OYcuv,qDUg+PZ\YR ;`2YŽo_ 4P8G!z493_o# F$= %WHn2nc@>LW nc;Mh蚫K̤7A~q` ._@NqLAmEY<]BHmPȥ?)fLAePT2חf Q9EöHεԆ^"Wb+tJ(sPDwoGnR9 `(`Ðz?w!UdBRXQԵ x(f?ASz.N2) 0x祔٤Hzj薁S;VTvUQѯˆd@Ae`OA\=04Z ]@7,fR/xj S62z2 ^HV?*8/7gCmŔ-s@Aw%0 Ns&&͡ G( 2K{Ð d):Z#EE ?#>Oݸh6>@4YMQPP Ū4MhT0"?ORtqh稪tVo $-}NJyET1!=5VsZ_= H w! CX@AЌpl^I UoO)NE! ( '0n a-`O;:Alnt& !E T{y2@TQM:jM~`㸿#]E~qyW|zԸ>/\xJƨ@=oy|Ss`Y ى{zj.,,ι0Th_fc}g<2>i\h>I@'C)fu! _iW݊<5ʼn55ZVn$P>4ĝs~@ `&B 0& AT_F_A.G=Fy4N[o!@QU@c v PAA_UU@TE"T6 a=]ke(` uAd@ Db[_ )SG*)|@^vUDӯPvɇD]lB5_WT@ `DPEda 6]讚<\݇J1j׬n/ >` SQCdzsv:54pm]>K})s__gl!.%T"aշz_1mh,0   ?Or͕a$ mg6* ՙt2? 6,⛦i~5L'3]/ 0.H`P, m3-` )F=,/v3z 2]ut^'/p,@ynDN$'I?Q{rY{9 wݗXԜSٓ_eRC᫜|Zo{*s&7 Ҿ;q e|Hb0;h?&Ӏa |>V|NFzS%C ٟh=9d>=FmC?nƜ`=~ct>&A(S43OܖxU.׆G]NsdJ7nU7&"{ p)׍׽iIy$Y2S֣d6QK q4]^iIVH+WdPbY~_ Ϛ q& $` sP'ȷfPu` +C4Q3Y^o)J ůTTX_{@2ŶIPxx^$zXHuQ]@_uߥۇu:?*]W^ R=Y4м:4ws# > kCY6 "~ɫ~[!Nul(SM17 {ފA&5ʼnEc[qPd6` f P0qP1B`p|h!>ZA0%uAUUy--4*r*" ,(" % J&g=ϺYVqYEn;<< %@c/?@?cpT@0@^4f".6z(>j|h -17 PW԰S=@<@{ ŗfRDFc!L{*"{}Yr@Ѥ<4Uh&2:JԉNkySwͪ<Ù 3-mjU)=" सʓC]Z]YFX0,Y5xW| 6 `BXF 8PE$0?,p,Wawjzضf([0 W,A1Ä\NUq3LQ]=4OBÂ* :ĜKEdG}i8#} a}fsxiC6q4-\MPA _"sȝ(KQJKٵ6. Ja5, ͯ4.AV;P [? ᡗ`ηigwٞΥ(↉:AN.@0 a0psOjYĻ[xPՏp'*&@AVR\Ph/cQݚϪ2 Cj(5U i145hsYgd4zԊ'UXq3Fۿkٮ3P{̂-!֪8|~i 2za 0~)Pltbl@m 9ƹk-u5>iw/Z6]~@kY^XcA|15g%z + `&B=R|r^p~@ ^4_/Wfz2^;9@7^**j4RI yT% R;zlTATZAy)E[pVWg(n#PerXԛV-@2 Rc@ @^YvٻOKyO i)nSp@~>3jif~ax0 L@_A^IXc@<S[`SC!@&rwd@%E ښyGE^s[ODFcE_LkQ,&<JǷs;D8׺t &PPWw{ec()+p*T 䃃 %@0 CWoF8P#x◜)}8QfD!2!;֘S QƊ##*%i^E @B 7N_c(QMQF=#5E\?k.cy˺}*ۨt&(EsVVRE=EYպ('/ # Γ1[w'Kw~Ē1 8Gp [v^(ԻnjV{K h%r5|iEDTUMuuϺ,G\U޶:O{,νTjT@Ug0P,G{n"23 vm籷Ut#p̀ ` (IF/  ͒]l6@6&~% p^t->SxRv/R;&5`t 4L`#!E``a54-$)pJdкc @ъ SY JXۡ?.X\]-45o8ʙ RE=WdݟsQPQ6J7ã؊nSJI.w?cj+ʍC >L%XCB0^6 O0p.@$ }Tzw*"BdPT@  +[N][X-/ )Dd]H"}yR5q@?VA>vkj$Y/fC(f#4:JUA._ O?hop<EL@~9o2V̌X)O7 @k2S(+bٌ>` 45D0S@0qejA=.L!TƮ;3G7o.Z $դOziPd}iM@UEW蓃I#4rڞÞ#)QZUNAjet/`o>zS#0q~@,;o: x4@ǥA/@'(@w 6, Mo^ķа4e@F/1ܿiF{="21n2n0?iPtArEh5Ϻl4}2ԃBAQg09VQTnx-LWy]j3p.aϭb B -Ֆi8[ړ avb+W@/ߤcl x Pd pՆ^ow5j+ơ =4RPT`B[ A+ z@eJt+O^X79`S{&(d)o_;K~jyvxm즷|[38bⴄ YC"Jn㾜ި>M#uL Ȱ@Ajl9 >W$h| 40-eEbWs#G㓡T֊`0xȎP>[N:l8l (Tj~(m@cN+:A::n0*bReT]OV}/̀asUU]ֱYGoL/v>{//oWU Viv o][m Fk J+ KS 0 Kö `(mbWW7As?Ht 4bHwC_ii)f]. CqQ37NEw60C@4!Ac#VAD>D  kb;J.ptpgR꪿ըEtVELͲյǂ0UyKF_C? }?zW/UtǶUau+v*@ T7$ZnU/?*ew> |]Y@_I0:Z U )PdI (~%? 2C)V]7#4ȄP3^e:237E`a N1!.ُ&YǠ|=As4mp$tUQ) DԾa"9sf'dH6^>|-Yk? ^2^j0ӑV z| 1Ӿ  N;c`j k-AO0I P!^)ƙoֆ;oLwQgC/7|o׆\>0X`l 8  L@( RԜRdV>T (Gfrg;@||4HYifZԛW>VD@JJ>7G{gw#՚Ngrƞ>n,T(Hk7 =Ubaߥ>6aB dKi xX]21"3cf#Jh\zun6 1R!}ۻ5R@o$ "`mUDUӭ z8@•( [}sVG"uZu{s#SF(@R9٠WX~ 5憨0_r.`\ 0 @f@%h0ҞP u{> V!Z1Lvt ڝ D] EYdK," UN`B 35tRF>(,9@n/SIFΛWP4{WJQ疩^ozI1"AL%eZgK"#]tH˒(~Q`ݤtc7;[O,ZXBO>/@ wSh@ڒ (b6;lp鲁If_!j|K1$I_\kʼn3ur9ƍ8 eaWu$ 01j " rKig|R3@Ҥw%s ??T> "rYK<(g%&&Џy;[sy; .`zmvM.4/'^dTIeT 0+/@K@?sx jl pIٗ$>G#jdy(-3#p)NJykkl@<@f AMO""jVEv]i x|o?g' 0]9_<z @j.UA ~|qzںsd۹ZW/ξh>Ѽl ߯=50tWcKͅF x)qyXR"`_߭ i<`J@ebO.o.(Պ_b귎8^( @uؔSR'H8z\`BTE`i>|j @l;G ;矟6G* ߗzФǘW<Uuz[],n T;it;ߟ 5I]$6Q+֚vӭӍVrO?~֥-{v4G7)1z lbM0ل @V`IMWkÿ<2ۘ9@>BVh0D,&7l7DdJx A!-\ךWQ-f <`_sUF/ЗEVfΘsnլw-jy@k:TD3b-dVs+H&RDO~=I|heK(XoW? Jj 0 Y6HX@habM x3ԋ8Dv QA֘O0{\\L0*^@! D^/2!hsQQȿ/T %@ɘ  -Or?-r5hbQ>W6XsJW|ť +ݲA]ʁ`02sI>ĄE; HO߯ _84 XOggSw  ^          CC%^2Sth+DtzJ$jÉBFeP2N`L gutiWH".vzOkaLsR@u(gR3T@њ7TD@/NdTy+lW3m7#v_2yz.#(*O!##+Գ6Lb\XZ<ΟX |%7 PpG 0@BOɎf}26qA URTk.(LdBm p"Vcna\0@S pAiI1ZR!4CA A(cHՉ+w]'LW D@S ݛ{ŔVĢ5}Zc7*s _?ϕEE''>60Ox1RK,, `1@d>=ƿ]-P~kd{7PA6~r߰]#Ԇ^+ޟݔԍPʭ{`(2/jw@N09*@! xۼ28Xe}^DP|HqA9EN ֛7KܕE͋[qG5+OIIssN-Fh(^!Y2ɩ9I2{ΪsFP7 }+o@HZ)1Gx%C/pـPT`>2~9 ãaRu5Vq)`v)')'q(r<kL s9` @0ydBqZw(5}BiAq\IyN3J>`h/* ޚ 0b%{^h~|i^fjwA}^UgDWZd5;F P^:wq6˚ =] \+o lm0m086YvC|?F]SL5L\fN n q(`AuB (&aT` E^{~$X)$x%gt@yR&`~8IS t ޚ-:*=_N9ʜb͘@s:} [Բؕd( k6$0 v [>THP5?@_i@7l@s^)?h_] ,k0|cF|̭T+L Jf臲ijN3 @L`8ri d<9JPP9A"H4nֱ:=Gqj_x/9_`VЭV:Ê*zN\Iۦ6\~ƬT+ZuGTS:׽]PAnI~mb% ,h!}a`+/:#G@AS6ƃ[O[FZgcgΰLWD(Y<Ƹ1fWP=ALh ҪkCW_%>?.!49s5`쳝JTړ Kc ʓ_x:@<ݓ0fo~]Cx$?M$Bh&x?یVll/~k=s;|т DW0_n RЁ`.O"`lo~7f[@(ʻ'!#@ [_osNqḇQ@5Dw]R}͋wԕ4o͹mg|v `WP9LV%T!Ư(, F93RAca҂'k*-)oR @ @4 l0>ً_Q˿z== eYde~`#-~{zR5HdubǡwfsZ :)@DϺz͟kQ fDBhzPzCS"iT;~5+?FHUR @V;t(0ɨZ:tLd()p ^ [m (,(&? #b_mS2#I]ҧ7xd]QKo82`8)*c8 !N0 =00ܰM_4P$rΟJ:m1#s+rUlޮ+ +@ @ (vvUƯvC1 ʕrd2^ ~h~x9x o,/1 ~9НE؊٨ 5JF[H81E=Zw aA00 B@z'V>i^CU&O$AWCת: $?=WmrFՠS,Ŝcm%W_VSyv{o-JU-) @ԇTP(yT`ԕW:p"0X&@#rgl ZP2_ڀ-82``^)w9 ?՘jd\ȋmȀ3gs%DA0"4= xg<"iHd t oTg /A A7+dWS ғW$,/̖P{ҞTzq|߷9$L5&yJrgy۵r @.bq}^ q(. }H~AV'T (Eٻ?ntf3bbfO7o w% qA SZ?Ku`& j@0F[UaW:Q-[ ~5z"쏓OzJ`@f'"*}]xmꏝE֖V CRX5tX:ڏؤf9?>/bNJ\V_V9  CO]I_R@1,`aO ]?| 4CC@S l)^)٫?D]aۊH:i TET["N]f 0q x*dM;>9f §3 ydG٩֦@'ڐʜhOwytbqLW[qXPci^# X V(<_ is+tN`:Y`x"Ҫ=-rlo ٿAp I&,404>I@,~jy{aLCM1삧I|F]lö"5c/@y0 &L} '<0)BB)^&i(!լc3ql5{J\@@'k/fB@g4Ri׬,}2Wjw= k]{,F>U`ԇu+ z/ Ƽ T+~O,/>аxm.@ӆjc@>If٧,#S.?r)f}. 0j> aPk au'ELM nk,sq47^Fn &{!xAi͐Z-oy^8{uDu**]Q%RaZL[Yθ+F_?۱-ۮIsMvPz@O> /,7 7 `lP0^!b(pɌ_~hC\{NyWm`n:AZ&t ס"rz0C>H(!kϠ  ܼ8R@R=7hǾώKj4PP{&,9o쿮֤F}3v1er6zjJYLm/9*e(Qcj \`}cm. X΀2 Y4 BjJ]6q(>J 6٭UK_nq0nPN;T@; fLhC\@x1VL2=,;Wڷ[<3 @Sw:zz^ @ɦܙT(JkK=Crm]W٬mzwmnpAMܴ6F%CV.t % zd(  ,8 `ɯ-h;ݧ؟e*) C0 _ŗ 2_H0\~5 CKԵ8QŹ(9(|w١N0` T$A e{dĻv_>A*^@%NRaC@w$}WV\dRh于.9F-"Fyqd_/ϮBP2@*F-}׎M V@tNNڀ˲_~{[XrߋXj @xY@  @Ҫ@tN^ISW˟<+31{/+GfHey(]GHO,-ـ^a bL!W+J~l!6 "{Kd ą6gMέG$`Y@r!?ܴ`FZ0`2.5[x3h@k-`~sw0H>;,SgN\ eP^&D:AN.@0K!!$ %#PEUUC5:8<-Z>Co:.HJU]_z=ȊԽe{@8\NPy3.dͽ+[5`*~4RBض~ $u]up`9ƙf~fc^cg^" OF8f 5ČQT}6E<Afs":AY/ ?C~Sۄ,`ט9*%? ,T I0v**b@@م @v䖛$K8gVnɜ"7(K_VhN1h}p㈷D(tS`q1C{o )fMԇ6 YvC@o&7赟gx0KX3q=! h/I.0UDAS+@tWU@TFQ'8.F(9xiML/TT?~!~~1䲶Z83eP;X;QfsqNƾvůf:dT乌ɝd8fX|M[4(g/ _$10 2(^T>~cfÁwG˛ϐZq"RT+EAU 2~ijNaS *FBp|ůz.ݡ>yusm:T΢ srrݥz`=i8ޯ_z3dx_ B60UJ}dh]*(0߻D-`?wrS 8 6~(YXHɏ0`BVC_ ЊHW]S p^.jyKg׵xʴ/b d(W̮7 *%@1Ƶ# !ks.vHa "rвf2(@?8kB8ԯ0d@%EPL/`` q/$T+)^x!AdPp+7%.~/v6\ߺ9^ꕂRv]?[rb9  ` "Z*D, ({#@Q4 74UJ9pz&ݞsusUgq9}E+wҒƬ2R >@饾T9a4 S@҂g2s΁p, J` 0g _Kꐌ 2ohz@AEi~96]D䄮;>(QWKD*>)'FVeq PdOv 5i0QAERolO)6OFd:ccu}@; JyxC[Р򣰗kHkw3- V| cPx:&Bu԰qTDP1UpZc hEC$.qө _l",%BvT# lA\C^9c*al6P&)t؆fb$jVv}Ï|ѫ{>ZCf+ƍʠ` P'8 1r (VxkM-x@=Wn 4tJ@W/ACD-/UJT>ZkݻڎNSNLJÅz i1% &@ %Y5r,8wj AJR 4 9>A,2*Eأ" 8rG&յ䅷L z#@ f͡&p^*@H2I<(aep !`NP*4J$ DOxP4^mG2y~uLJ烥\^H* J20ڬZ?3~g SAF`^)U  XWh϶Gܘf(CʬhRDw?PxLEUcԍ&qDq|93/2I1XG]t** (KE?K!Ks Sk䌊}iY)}blP%oԮ=cvEq_ƨh5WumwTa@v4@7cZ>cI֙ PnWǟB13.o_cZS e݊jYxA@ 7*$WBrUx͢ӣ+dpͧq.mLE_06xBH>4b*tq(*2q!:+o<ݵx{ZmG_wb1t@$_Z])PbcT3_T@Ų.Oo_o0T@`l 9ٗ~2 6c)^i!" x9nDQDDj{0q Vu c2(؊ՎiYkxRE |Ү$;LX PPxPWDеMiQ~RS3 * R;99u"ʄH`Un7:@=~6" ݪ10,^gV lH`lYw _M z` (}>jo O^J6V8F~4yJtڌZ,HY=p]9`Bqѐ+`~=RYz.YkkćQ/@ ."){Wj J.* _3s棾w. cQ#Y9rb<j ;$9YCb{q{@7\l5 耵 4lǡsOāCX[pE( @0L3gu4O?2ڠ+wwG}+lG̨lbkS;('S}@t@NLBA@0F *>it1 1F~[J0ο7Vx]EO* #qSRٷT^E^ͥ\\f)jz*xPzJH+8htw3u {R/fq&~-=@7:A:P<4`B*Lb-RBʄsguЬA"j@B񹴉g5Kp9K# kC(`MUoiUMݜ. yRUѽZmik+r[ Gu`-,T0+aDլP/F?zy <^~06߱ JȒq45H ?lAU~AMfz::u|]>0!ԊC hv1QF7Xyo B 0 ` "k,uQC@{wxA24\jWm}y9YvFܪ`F#S2(^x9@/MBrH~~[3t`~,6 ba @s ~?}вç+x3`B NMߗbh`>%r}D,tNͽGSwaIu/ˋhVPPܠzߪK`$e!<)<2yO75@=62~ż!v;@bF2Pw*hW:< wsM4+׊/bx"ۆka:U$䪊X/ SaD*l3z+){*My ̫),ϭ\ w}yrS~qU_O9x"m08{ݎ>g_~{ujDb ltMT T~Q36 2c 8Bi2v-g6h; 3; T֘j#YyO&wSj&/rğ?>)|Jjdc^_a \ŸX%kVk%{/7YA̺DZ>oFh\tYם d>0(6ZgnyJ9ڲ @Ve z /#e3Ft *Om)R` ,@!`@,|bvϗL#(icA6-$`~WD&[#j&sQ.{sm]1{PAKbA(oNIj _ӏOv{@F ]) KJn<ܓwnOVt^q|L ~4IwRc#ၜV~͔ y@R߳H+,9!=O6zK%Dw+A(2o0pJj̐VX̀ 妇myTlWsMϓ ֔=exlo"vv61^U1vR@f,/n?-d]hFrQr|@Q$yXYf:5;X6{4Rw4^Έ\%W20 L`q)d_W܁AjוJ Aқp&^K6@n64}<?ZO2J+cb\P w ԄB@dguBk(^'٫Nq,B pI]eXˋ<#&/ZPD+ 6G;q1CbTD[˥#ʲy)%Tݹ9r}naكiӛӼ?oUC2.,_hCk*u -B:zخ( Mu;&x?.]k3h1V+ިD#ߓ/A* 9T+egq9޲n9qfbG>u`1tu>0OggS  u{&ZeRsjnzĭũ)`v PU85܆%0pʗm jzIf!8*%_3Nwcf~Wy6_צ,`cNm׃ "u>ZFlZM^׌料u7 -5_!I$ȍ.ж'46]IIUU%j0'8:<%2KՂ=QNUme/xpO @)ZAfIQ&WݓY/yÐP'L_N}9xưKoVPh95@I恊%*U S״]ݑVK|@wFx3(c#a=b0gs\LW3v=8Q[m,s}o0}}s1W27[/:D[wrWHX_%\Ddh0}d\>a.{8ޛ^5`?XiAQn޶Qb Jks˄ξ+Toʐdu"{z" p#%TUU=;wvc%~LT<ȷOP^;_rZ7n+^0x8{z4ﮖ^4w3r.w;y n2J2`k;`}É3h4 ]>V--cllЛ&$BAy/UͰE"ä;%C ?SG(P\F 6oѮe:]>XLMޖm 2cpW(6qva t GQ0kPI*흣~!>e >HB!t.^W;![ݡ\satjPqx8?_q1=׫-llk)cF+?tUzg>7-tWg޹k hK<įRJT̪rjqu|>Hz Si&&ahn=b@3ݳY?1@i|fy]) U3LR yXXW#~9ܓͮx:&'LL:ʴ_ieOgjN,8!9+] Pt`;3}tjkF}/4tb/}M^f7RiHs{KOu>/h=]\zq9ecz;^NSTᒡddH_/~y{8dqu)`khard-0.17.0/misc/zsh/000077500000000000000000000000001371517016500144355ustar00rootroot00000000000000khard-0.17.0/misc/zsh/_email-khard000066400000000000000000000004141371517016500166740ustar00rootroot00000000000000#autoload _email-khard(){ OLDIFS=${IFS} IFS=$'\n' local khard_output=($(khard email -p 2>/dev/null)) IFS=$'\t' for i in {1..${#khard_output[@]}}; do local line=($(echo ${khard_output[$i]})) reply+=(${line[1]}) done IFS=${OLDIFS} return 300 } khard-0.17.0/misc/zsh/_khard000066400000000000000000000206131371517016500156120ustar00rootroot00000000000000#compdef khard # Zsh completion definition for khard version >= 0.13.0 # Install by copying to a directory where zsh searches for completion # functions (the $fpath array). # # If you, for example, put all completion functions into the folder ~/.zsh/completions you must add # the following to your zsh main config file ~/.zshrc: # fpath=( $HOME/.zsh/completions $fpath ) # autoload -U compinit # compinit # # More information at http://is.muni.cz/www/xsiska2/2014/08/05/generating-completing-functions.html # Define a helper function to complete addressbook names. function _khard_addressbook_names () { local expl _sequence _wanted addressbooks expl "addressbook" compadd - \ ${(f)"$(_call_program addresses khard addressbooks)"} } local curcontext="$curcontext" local -a state line expl local -A opt_args local ret=1 # Define options for the different subcommands. local -a options options=( '(- *)'{-h,--help}'[show a short help message]' ) # First handle global options. Everything that does not match a global option # as defined here is handled later. The $state is set to "subcommand" or # "options" in order to do that. _arguments -C -s \ $options \ '(- *)'{-v,--version}'[show version information]' \ '(-c)'{-c+,--config=}'[config file to use]:config file:_files' \ '--debug[enable debug output]' \ '--skip-unparsable[skip unparsable vcard files]' \ ':subcommand:->subcommand' \ '*::option:->options' && ret=0 case $state in subcommand) # Define an array with the subcommands and the description. local -a subcommands_array subcommands_array=( add-email:'add email address from email header to a contact' {addressbooks,abooks}:'list available addressbooks' {birthdays,bdays}:'list birthdays' {copy,cp}:'copy a contact to another addressbook' {details,show}:'show details for a contact' email:'list email addresses' export:'export a contact' {filename,file}':list internal file names' {list,ls}:'list all (selected) contacts' merge:'merge two contacts' {modify,edit,ed}:'edit a contact' {move,mv}:'move a contact to another addressbook' {new,add}:'add a new contact' phone:'list phone numbers' {postaddress,postaddr,post}:'list post addresses' {remove,rm,del,delete}:'delete a contact' ) # Use this array to complete the subcommands. _describe -t subcommands 'khard subcommand' subcommands_array && ret=0 ;; options) # Define different option groups. # address book options local -a default_addressbook_options new_addressbook_options copy_move_addressbook_options merge_addressbook_options default_addressbook_options=( '(-a)'{-a+,--addressbook=}'[specify addressbooks to narrow the list of contacts]:addressbook:_khard_addressbook_names' ) new_addressbook_options=( '(-a)'{-a+,--addressbook=}'[specify addressbook in which to create new contact]:addressbook:_khard_addressbook_names' ) copy_move_addressbook_options=( '(-a)'{-a+,--addressbook=}'[specify addressbooks to narrow the list of contacts]:addressbook:_khard_addressbook_names' '(-A)'{-A+,--target-addressbook=}'[specify target addressbook in which to copy / move]:addressbook:_khard_addressbook_names' ) merge_addressbook_options=( '(-a)'{-a+,--addressbook=}'[specify addressbooks to narrow the list of source contacts]:addressbook:_khard_addressbook_names' '(-A)'{-A+,--target-addressbook=}'[specify addressbooks to narrow the list of target contacts]:addressbook:_khard_addressbook_names' ) # input file options local -a email_header_input_options template_file_input_options email_header_input_options=( '(-i)'{-i+,--input-file=}'[specify input email header file name or use stdin]:input file:_files' ) template_file_input_options=( '(-i)'{-i+,--input-file=}'[specify input template file name or use stdin]:input file:_files' {--edit,--open-editor}'[open text editor after successful creation of new contact from stdin or template]' ) # sort options local -a sort_options sort_options=( '(-d)'{-d+,--display=}'[display names in contact table by first or last name]:name:(first_name last_name formatted_name)' '(-g)'{-g,--group-by-addressbook}'[group contacts table by address book]' '(-r)'{-r,--reverse}'[reverse order of contact table]' '(-s)'{-s+,--sort=}'[sort contact table]:sort by:(first_name last_name formatted_name)' ) # search options local -a default_search_options merge_search_options default_search_options=( '(-f)'{-f,--search-in-source-files}'[look into source vcf files to speed up search queries in large address books]' '(-e)'{-e,--strict-search}'[narrow contact search to name field]' '(-u)'{-u+,--uid=}'[select contact by uid]:uid' '*: :_guard "^-*" "search term"' ) merge_search_options=( '(-f)'{-f,--search-in-source-files}'[look into source vcf files to speed up search queries in large address books]' '(-e)'{-e,--strict-search}'[narrow contact search to name fields]' '(-t)'{-t+,--target-contact=}'[search in all fields to find matching target contact]:search string' '(-u)'{-u+,--uid=}'[select source contact by uid]:uid' '(-U)'{-U+,--target-uid=}'[select target contact by uid]:uid' '*: :_guard "^-*" "search term"' ) curcontext="${curcontext%:*}-${words[1]}:" # Add the correct options for the subcommand to $options, depending on the # subcommand found in $word[1]. case $words[1] in addressbooks|abooks|template) options+=();; source|src|remove|delete|del|rm|filename|file) options+=( $default_addressbook_options $default_search_options $sort_options );; list|ls) options+=( $default_addressbook_options $default_search_options $sort_options '(-p)'{-p,--parsable}'[machine readable contact table]' '(-F)'{-F+,--fields=}'[output field specification]:field specification:' );; details|show) options+=( $default_addressbook_options $default_search_options $sort_options '(-o)'{-o+,--output-file=}'[specify output template file name or use stdout]:output file:_files' '--format=[output format]:format:(pretty yaml vcard)' );; birthdays|bdays) options+=( $default_addressbook_options $default_search_options '(-d)'{-d+,--display=}'[display names in contact table by first or last name]:name:(first_name last_name formatted_name)' '(-p)'{-p,--parsable}'[machine readable birthday table]' );; email) options+=( $default_addressbook_options $default_search_options $sort_options '(-p)'{-p,--parsable}'[machine readable email address table]' '--remove-first-line[remove first line from output]' );; phone) options+=( $default_addressbook_options $default_search_options $sort_options '(-p)'{-p,--parsable}'[machine readable phone number table]' );; postaddress|postaddr|post) options+=( $default_addressbook_options $default_search_options $sort_options '(-p)'{-p,--parsable}'[machine readable post address table]' );; new|add) options+=( $new_addressbook_options $template_file_input_options '--vcard-version=[select preferred vcard version for new contact]:version:(3.0 4.0)' );; add-email) options+=( $default_addressbook_options $email_header_input_options $default_search_options $sort_options '--vcard-version=[select preferred vcard version for new contact]:version:(3.0 4.0)' );; copy|cp|move|mv) options+=( $copy_move_addressbook_options $default_search_options $sort_options );; modify|edit|ed) options+=( $default_addressbook_options $template_file_input_options $default_search_options $sort_options '--format=[file format to use when editing]:format:(yaml vcard)' );; merge) options+=( $merge_addressbook_options $merge_search_options $sort_options );; remove|delete|del|rm) options+=( $default_addressbook_options $default_search_options $sort_options '--force[Remove contact without confirmation]' );; esac # Complete the subcommand options. _arguments -S $options && ret=0 ;; esac return ret khard-0.17.0/setup.py000066400000000000000000000032271371517016500144140ustar00rootroot00000000000000# -*- coding: utf-8 -*- # tutorials: # - https://packaging.python.org/en/latest/distributing.html # - https://hynek.me/articles/sharing-your-labor-of-love-pypi-quick-and-dirty/ # - https://gehrcke.de/2014/02/distributing-a-python-command-line-application/ from setuptools import setup with open('README.md', 'rb') as f: readme = f.read().decode("utf-8") setup( name='khard', author='Eric Scheibler', author_email='email@eric-scheibler.de', url='https://github.com/scheibler/khard/', description='A console carddav client', long_description=readme, long_description_content_type='text/markdown', license='GPL', keywords='Carddav console addressbook', classifiers=[ "Development Status :: 4 - Beta", "Environment :: Console", "Topic :: Utilities", "Topic :: Communications :: Email :: Address Book", "License :: OSI Approved :: GNU General Public License (GPL)", "Intended Audience :: End Users/Desktop", "Operating System :: POSIX", "Programming Language :: Python :: 3 :: Only", ], install_requires=[ 'atomicwrites', 'configobj', 'ruamel.yaml', 'unidecode', 'vobject' ], extras_require={'doc': ['sphinx', 'sphinx-autoapi', 'sphinx-autodoc-typehints']}, use_scm_version={'write_to': 'khard/version.py'}, setup_requires=['setuptools_scm'], packages=['khard'], entry_points={'console_scripts': ['khard = khard.khard:main']}, test_suite="test", # we use type annotations of unset variables which needs 3.6 python_requires=">=3.6", include_package_data=True, ) khard-0.17.0/test/000077500000000000000000000000001371517016500136555ustar00rootroot00000000000000khard-0.17.0/test/__init__.py000066400000000000000000000003551371517016500157710ustar00rootroot00000000000000"""Module to make it possible to import the test folder as a python package and hence make it possible to run all unittests from the top level direcotry with python -m unittest [discover] and python setup.py test """ khard-0.17.0/test/fixture/000077500000000000000000000000001371517016500153435ustar00rootroot00000000000000khard-0.17.0/test/fixture/broken.abook/000077500000000000000000000000001371517016500177155ustar00rootroot00000000000000khard-0.17.0/test/fixture/broken.abook/unparsable.vcf000077700000000000000000000000001371517016500270752../vcards/unparsable.vcfustar00rootroot00000000000000khard-0.17.0/test/fixture/minimal.abook/000077500000000000000000000000001371517016500200635ustar00rootroot00000000000000khard-0.17.0/test/fixture/minimal.abook/minimal.vcf000077700000000000000000000000001371517016500260272../vcards/minimal.vcfustar00rootroot00000000000000khard-0.17.0/test/fixture/minimal.conf000066400000000000000000000000661371517016500176420ustar00rootroot00000000000000[addressbooks] [[foo]] path = test/fixture/test.abook khard-0.17.0/test/fixture/multiple_values.yaml000066400000000000000000000100641371517016500214420ustar00rootroot00000000000000# Contact template for khard version 0.11.0 # # Use this yaml formatted template to create a new contact: # either with: khard new -a address_book -i template.yaml # or with: cat template.yaml | khard new -a address_book # name components # every entry may contain a string or a list of strings # format: # First name : name1 # Additional : # - name2 # - name3 # Last name : name4 Prefix : - Prof. - Dr. First name : Mark Additional : - Stephe - Tom Last name : Schröder Suffix : II # person related information # # birthday # Formats: # vcard 3.0 and 4.0: yyyy-mm-dd or yyyy-mm-ddTHH:MM:SS # vcard 4.0 only: --mm-dd or text= string value Birthday : 1992-12-21 # nickname # may contain a string or a list of strings Nickname : - several - nicknames # organisation # format: # Organisation : company # or # Organisation : # - company1 # - company2 # or # Organisation : # - # - company # - unit Organisation : - - Company1 - Sub-Company1 - Company2 # organisation title and role # every entry may contain a string or a list of strings # # title at organisation # example usage: research scientist Title : - research scientist - Manager # role at organisation # example usage: project leader Role : - CEO - CTO # phone numbers # format: # Phone: # type1, type2: number # type3: # - number1 # - number2 # custom: number # allowed types: # vcard 3.0: At least one of bbs, car, cell, fax, home, isdn, msg, modem, # pager, pcs, pref, video, voice, work # vcard 4.0: At least one of home, work, pref, text, voice, fax, cell, video, # pager, textphone # Alternatively you may use a single custom label (only letters). # But beware, that not all address book clients will support custom labels. Phone : cell, pref : 0161 99999999 work : - 090 11122200 - 090 33344400 - 090 55566600 # email addresses # format like phone numbers above # allowed types: # vcard 3.0: At least one of home, internet, pref, work, x400 # vcard 4.0: At least one of home, internet, pref, work # Alternatively you may use a single custom label (only letters). Email : home : work : mark@example.com pref : mark@example.org # post addresses # allowed types: # vcard 3.0: At least one of dom, intl, home, parcel, postal, pref, work # vcard 4.0: At least one of home, pref, work # Alternatively you may use a single custom label (only letters). Address : Custom : Box : Extended : Street : Riverlane 1 Code : 99A99 City : A City Region : Region Country : Country home : - Extended : 1st Floor Street : Main street 1 Code : 12345 City : Another City - Extended : Street : | Market Street 1 33333 City Region, Country Code : City : # categories or tags # format: # Categories : single category # or # Categories : # - category1 # - category2 Categories : - - cat1 - cat2 - cät3 - one more # web pages # may contain a string or a list of strings Webpage : - http://example.com - https://example.org # private objects # define your own private objects in the vcard section of your khard.conf file # these objects are stored with a leading "X-" before the object name in the # vcard files. # every entry may contain a string or a list of strings Private : Jabber : - jabber1 - jabber2 Skype : - skype1 - skype2 Twitter : - twitter1 - twitter2 - twitter3 # notes # may contain a string or a list of strings # for multi-line notes use: # Note : | # line one # line two Note : - single line note with emoji 🍵 - | multi-line note khard-0.17.0/test/fixture/nick.abook/000077500000000000000000000000001371517016500173615ustar00rootroot00000000000000khard-0.17.0/test/fixture/nick.abook/joe.vcf000077700000000000000000000000001371517016500236032../vcards/joe.vcfustar00rootroot00000000000000khard-0.17.0/test/fixture/nick.abook/nickname.vcf000077700000000000000000000000001371517016500256232../vcards/nickname.vcfustar00rootroot00000000000000khard-0.17.0/test/fixture/nick.conf000066400000000000000000000001341371517016500171340ustar00rootroot00000000000000[addressbooks] [[foo]] path = test/fixture/nick.abook [contact table] show_nicknames = True khard-0.17.0/test/fixture/single_values.yaml000066400000000000000000000064231371517016500210740ustar00rootroot00000000000000# Contact template for khard version 0.11.0 # # Use this yaml formatted template to create a new contact: # either with: khard new -a address_book -i template.yaml # or with: cat template.yaml | khard new -a address_book # name components # every entry may contain a string or a list of strings # format: # First name : name1 # Additional : # - name2 # - name3 # Last name : name4 Prefix : Dr. First name : Paula Additional : Last name : Smith Suffix : II # person related information # # birthday # Formats: # vcard 3.0 and 4.0: yyyy-mm-dd or yyyy-mm-ddTHH:MM:SS # vcard 4.0 only: --mm-dd or text= string value Birthday : 1983-02-01 # nickname # may contain a string or a list of strings Nickname : myNick # organisation # format: # Organisation : company # or # Organisation : # - company1 # - company2 # or # Organisation : # - # - company # - unit Organisation : Example Company # organisation title and role # every entry may contain a string or a list of strings # # title at organisation # example usage: research scientist Title : research scientist # role at organisation # example usage: project leader Role : Project leader # phone numbers # format: # Phone: # type1, type2: number # type3: # - number1 # - number2 # custom: number # allowed types: # vcard 3.0: At least one of bbs, car, cell, fax, home, isdn, msg, modem, # pager, pcs, pref, video, voice, work # vcard 4.0: At least one of home, work, pref, text, voice, fax, cell, video, # pager, textphone # Alternatively you may use a single custom label (only letters). # But beware, that not all address book clients will support custom labels. Phone : cell : 0152 12345678 work : 090 87654321 # email addresses # format like phone numbers above # allowed types: # vcard 3.0: At least one of home, internet, pref, work, x400 # vcard 4.0: At least one of home, internet, pref, work # Alternatively you may use a single custom label (only letters). Email : work : paula.smith@example.com # post addresses # allowed types: # vcard 3.0: At least one of dom, intl, home, parcel, postal, pref, work # vcard 4.0: At least one of home, pref, work # Alternatively you may use a single custom label (only letters). Address : home : Box : Extended : Street : Mainstreet 123 Code : 31415 City : A City Region : Region Country : Country # categories or tags # format: # Categories : single category # or # Categories : # - category1 # - category2 Categories : a single one # web pages # may contain a string or a list of strings Webpage : http://example.com # private objects # define your own private objects in the vcard section of your khard.conf file # these objects are stored with a leading "X-" before the object name in the # vcard files. # every entry may contain a string or a list of strings Private : Jabber : paula@jabber.org Skype : sky_paula Twitter : twi_paula # notes # may contain a string or a list of strings # for multi-line notes use: # Note : | # line one # line two Note : | first line second line khard-0.17.0/test/fixture/test.abook/000077500000000000000000000000001371517016500174145ustar00rootroot00000000000000khard-0.17.0/test/fixture/test.abook/contact1.vcf000077700000000000000000000000001371517016500255342../vcards/contact1.vcfustar00rootroot00000000000000khard-0.17.0/test/fixture/test.abook/contact2.vcf000077700000000000000000000000001371517016500255362../vcards/contact2.vcfustar00rootroot00000000000000khard-0.17.0/test/fixture/test.abook/text-bday.vcf000077700000000000000000000000001371517016500261062../vcards/text-bday.vcfustar00rootroot00000000000000khard-0.17.0/test/fixture/vcards/000077500000000000000000000000001371517016500166255ustar00rootroot00000000000000khard-0.17.0/test/fixture/vcards/altid.vcf000066400000000000000000000004211371517016500204170ustar00rootroot00000000000000BEGIN:VCARD VERSION:4.0 UID:altid FN:altid N;ALTID=1:representation;one;;; N;ALTID=1:representation;two;;; NOTE:RFC6350 requires N to have cardinality <=1 but allows alternative repr esentations to be included if they are linked by the ALTID parameter. END:VCARD khard-0.17.0/test/fixture/vcards/category.vcf000066400000000000000000000001601371517016500211370ustar00rootroot00000000000000BEGIN:VCARD VERSION:4.0 FN:contact with category EMAIL:foo@example.org CATEGORIES:bar UID:cat1 END:VCARD khard-0.17.0/test/fixture/vcards/contact1.vcf000066400000000000000000000002071371517016500210400ustar00rootroot00000000000000BEGIN:VCARD VERSION:4.0 FN:second contact UID:testuid1 BDAY:20180120 EMAIL;TYPE=home:user@example.com TEL:0123456789 END:VCARD khard-0.17.0/test/fixture/vcards/contact2.vcf000066400000000000000000000001051371517016500210360ustar00rootroot00000000000000BEGIN:VCARD VERSION:4.0 FN:third contact UID:testuid2 END:VCARD khard-0.17.0/test/fixture/vcards/issue249.vcf000066400000000000000000000000771371517016500207200ustar00rootroot00000000000000BEGIN:VCARD VERSION:3.0 UID:issue249 FN:Foo Bar END:VCARD khard-0.17.0/test/fixture/vcards/joe.vcf000066400000000000000000000003101371517016500200740ustar00rootroot00000000000000BEGIN:VCARD VERSION:4.0 UID:6exhjr32bt783wwlr9u0sr8lfqse5x7zqc8y FN:Joe Citizen N:Citizen;Joe;;; NICKNAME:horrible_human_being EMAIL;TYPE=pref:jcitizen@foo.com REV:20200411T072429Z END:VCARD khard-0.17.0/test/fixture/vcards/labels.vcf000066400000000000000000000002111371517016500205610ustar00rootroot00000000000000BEGIN:VCARD VERSION:4.0 UID:51953f58-237e-429a-a271-cb47c026a895 FN:labeled guy item1.ORG:Test Inc item1.X-ABLabel:Work END:VCARD khard-0.17.0/test/fixture/vcards/minimal.vcf000066400000000000000000000000711371517016500207510ustar00rootroot00000000000000BEGIN:VCARD VERSION:4.0 FN:minimal contact END:VCARD khard-0.17.0/test/fixture/vcards/nickname.vcf000066400000000000000000000001731371517016500211130ustar00rootroot00000000000000BEGIN:VCARD VERSION:3.0 UID:issue251part1 N:Smith;Michael;;; NICKNAME:Mike EMAIL;TYPE=pref:ms@example.org END:VCARD khard-0.17.0/test/fixture/vcards/no-nickname.vcf000066400000000000000000000001511371517016500215210ustar00rootroot00000000000000BEGIN:VCARD VERSION:3.0 UID:issue251part2 N:Jones;Mike;;; EMAIL;TYPE=pref:mj@example.org END:VCARD khard-0.17.0/test/fixture/vcards/photov3.vcf000066400000000000000000000007301371517016500207270ustar00rootroot00000000000000BEGIN:VCARD VERSION:3.0 UID:photov3 FN:with photo v3 NOTE:From the RFC: ENCODING must be "b" (which enforces base64 encoded data)\, TYPE may be present\, but if it is present it must be a valid IANA image type. The value can be the image data or an uri. PHOTO;ENCODING=b;TYPE=PNG:iVBORw0KGgoAAAANSUhEUgAAAAsAAAALCAQAAAADpb+tAA AAQklEQVQI122PQQ4AMAjCKv//Mzs4M0zmRYKkamEwWQVoRJogk4PuRoOoMC/EK8nYb+l08 WGvSxKlNHO5kxnp/WXrAzsSERN1N6q5AAAAAElFTkSuQmCC END:VCARD khard-0.17.0/test/fixture/vcards/photov4.vcf000066400000000000000000000005521371517016500207320ustar00rootroot00000000000000BEGIN:VCARD VERSION:4.0 UID:photov4 FN:with photo v4 NOTE:The RFC6350 for vCard 4.0 specifies that the value for PHOTO should be a single uri. PHOTO:data:image/png;base64\,iVBORw0KGgoAAAANSUhEUgAAAAsAAAALCAQAAAADpb+ tAAAAQklEQVQI122PQQ4AMAjCKv//Mzs4M0zmRYKkamEwWQVoRJogk4PuRoOoMC/EK8nYb+ l08WGvSxKlNHO5kxnp/WXrAzsSERN1N6q5AAAAAElFTkSuQmCC END:VCARD khard-0.17.0/test/fixture/vcards/post.vcf000066400000000000000000000002321371517016500203070ustar00rootroot00000000000000BEGIN:VCARD VERSION:4.0 FN:With post address ADR;TYPE=home:PostBox;Ext;Main Street 1;The City;SomeState;00000;HomeCountry UID:postcontact END:VCARD khard-0.17.0/test/fixture/vcards/tel-value-uri.vcf000066400000000000000000000002231371517016500220150ustar00rootroot00000000000000BEGIN:VCARD VERSION:3.0 UID:bf3lycta2gmsk1ts57owath965h38709zoyu FN:bug 195 REV:20180923T191324Z TEL;TYPE=cell;VALUE=URI:67545678 END:VCARD khard-0.17.0/test/fixture/vcards/text-bday.vcf000066400000000000000000000001411371517016500212220ustar00rootroot00000000000000BEGIN:VCARD VERSION:4.0 FN:text birthday UID:testuid3 BDAY;VALUE=text:circa 1800 END:VCARD khard-0.17.0/test/fixture/vcards/unparsable.vcf000066400000000000000000000001001371517016500214500ustar00rootroot00000000000000BEGIN:VCARD VERSION:4.0 FN:second contact with minimal Vcard khard-0.17.0/test/helpers.py000066400000000000000000000075161371517016500157020ustar00rootroot00000000000000"""Helper functions for the tests.""" # pylint: disable=invalid-name import contextlib import io import os import shutil import tempfile from unittest import mock import vobject from khard import address_book from khard import carddav_object def vCard(**kwargs): """Create a simple vobject.vCard for tests.""" vcard = vobject.vCard() if 'fn' not in kwargs: kwargs['fn'] = 'Test vCard' if 'version' not in kwargs: kwargs['version'] = '3.0' for key, value in kwargs.items(): vcard.add(key.upper()).value = value return vcard def TestVCardWrapper(**kwargs): """Create a simple VCardWrapper for tests.""" return carddav_object.VCardWrapper(vCard(**kwargs)) def TestYAMLEditable(**kwargs): """Create a simple YAMLEditable for tests.""" return carddav_object.YAMLEditable(vCard(**kwargs)) def TestCarddavObject(**kwargs): """Create a siple CarddavObject for tests.""" return carddav_object.CarddavObject(vCard(**kwargs), None, None) def mock_stream(name="stdout"): """A context manager to replace a stdio stream with a string buffer. >>> with mock_stream() as s: >>> print("hello world") >>> assert s.getvalue() == "hello world" >>> with mock_stream("stderr") as e: >>> print("hallo error", file=sys.stderr) >>> assert e.getvalue() == "hello error" """ stream = io.StringIO() context_manager = mock.patch('sys.'+name, stream) context_manager.getvalue = stream.getvalue return context_manager def load_contact(path, abook=None): """Load a contact from the fixture directory. :param str path: the file name (full, relative to cwd or the fixture dir) :param AddressBook abook: :returns CarddavObject: """ if not os.path.exists(path): path = os.path.join("test/fixture/vcards", path) return carddav_object.CarddavObject.from_file(abook, path) class TmpAbook: """Context manager to create a temporary address book folder""" def __init__(self, vcards): self.vcards = vcards def __enter__(self): self.tempdir = tempfile.TemporaryDirectory() for card in self.vcards: shutil.copy(self._card_path(card), self.tempdir.name) return address_book.VdirAddressBook("tmp", self.tempdir.name) def __exit__(self, _a, _b, _c): self.tempdir.cleanup() @staticmethod def _card_path(card): if os.path.exists(card): return card return os.path.join("test/fixture/vcards", card) class TmpConfig(contextlib.ContextDecorator): """Context manager to create a temporary khard configuration. The given vcards will be copied to the only address book in the configuration which will be called "tmp". """ def __init__(self, vcards): self.tempdir = None self.config = None self.vcards = vcards self.mock = None def __enter__(self): self.tempdir = tempfile.TemporaryDirectory() for card in self.vcards: shutil.copy(self._card_path(card), self.tempdir.name) with tempfile.NamedTemporaryFile("w", delete=False) as config: config.write("""[general] editor = editor merge_editor = merge_editor [addressbooks] [[tmp]] path = {} """.format(self.tempdir.name)) self.config = config self.mock = mock.patch.dict('os.environ', KHARD_CONFIG=config.name) self.mock.start() return self def __exit__(self, _a, _b, _c): self.mock.stop() os.unlink(self.config.name) self.tempdir.cleanup() @staticmethod def _card_path(card): if os.path.exists(card): return card return os.path.join("test/fixture/vcards", card) khard-0.17.0/test/test_actions.py000066400000000000000000000025501371517016500167300ustar00rootroot00000000000000"""Tests for the action class""" # pylint: disable=missing-docstring import unittest from khard import actions action = 'list' alias = 'ls' unknown = 'this is not an action or an alias' class Action(unittest.TestCase): def test_get_action_resolves_aliases(self): self.assertEqual(action, actions.Actions.get_action(alias)) def test_get_action_returns_none_for_actions(self): self.assertIsNone(actions.Actions.get_action(action)) def test_get_action_returns_none_for_unknown(self): self.assertIsNone(actions.Actions.get_action(unknown)) def test_get_aliases_reverse_resolves_aliases(self): self.assertEqual([alias], actions.Actions.get_aliases(action)) def test_get_aliases_returns_none_for_aliases(self): self.assertIsNone(actions.Actions.get_aliases(alias)) def test_get_aliases_returns_none_for_unknown(self): self.assertIsNone(actions.Actions.get_aliases(unknown)) def test_get_actions_returns_actions(self): self.assertIn(action, actions.Actions.get_actions()) def test_get_actions_does_not_return_aliases(self): self.assertNotIn(alias, actions.Actions.get_actions()) def test_get_all_returns_actions(self): self.assertIn(action, actions.Actions.get_all()) def test_get_all_returns_aliases(self): self.assertIn(alias, actions.Actions.get_all()) khard-0.17.0/test/test_address_book.py000066400000000000000000000166701371517016500177370ustar00rootroot00000000000000"""Tests for the address book classes.""" # pylint: disable=missing-docstring import os import unittest from unittest import mock from khard import address_book, query from .helpers import TmpAbook class _AddressBook(address_book.AddressBook): """Class for testing the abstract AddressBook base class.""" def load(self, query=None): pass class AbstractAddressBookSearch(unittest.TestCase): """Tests for khard.address_book.AddressBook.search()""" def test_search_will_trigger_load_if_not_loaded(self): abook = _AddressBook('test') load_mock = mock.Mock() abook.load = load_mock list(abook.search(query.AnyQuery())) load_mock.assert_called_once() def test_search_will_not_trigger_load_if_loaded(self): abook = _AddressBook('test') load_mock = mock.Mock() abook.load = load_mock abook._loaded = True list(abook.search(query.AnyQuery())) load_mock.assert_not_called() def test_search_passes_query_to_load(self): abook = _AddressBook('test') self.assertFalse(abook._loaded) load_mock = mock.Mock() abook.load = load_mock list(abook.search(query.AnyQuery())) load_mock.assert_called_once_with(query.AnyQuery()) class AddressBookCompareUids(unittest.TestCase): def test_different_strings(self): uid1 = 'abc' uid2 = 'xyz' expected = 0 actual = address_book.AddressBook._compare_uids(uid1, uid2) self.assertEqual(actual, expected) def test_two_simple_strings(self): uid1 = 'abcdef' uid2 = 'abcxyz' expected = 3 actual = address_book.AddressBook._compare_uids(uid1, uid2) self.assertEqual(actual, expected) def test_no_error_on_equal_strings(self): uid = 'abcdefghij' expected = len(uid) actual = address_book.AddressBook._compare_uids(uid, uid) self.assertEqual(actual, expected) class VcardAddressBookLoad(unittest.TestCase): def test_vcards_without_uid_generate_a_warning(self): abook = address_book.VdirAddressBook('test', 'test/fixture/minimal.abook') with self.assertLogs(level='WARNING') as cm: abook.load() messages = ['WARNING:khard.address_book:Card minimal contact from ' 'address book test has no UID and will not be ' 'available.'] self.assertListEqual(cm.output, messages) def test_loading_vcards_from_disk(self): abook = address_book.VdirAddressBook('test', 'test/fixture/test.abook') # At this point we do not really care about the type of abook.contacts, # it could be a list or dict or set or whatever. self.assertEqual(len(abook.contacts), 0) abook.load() self.assertEqual(len(abook.contacts), 3) def test_search_in_source_files_only_loads_matching_cards(self): abook = address_book.VdirAddressBook('test', 'test/fixture/test.abook') abook.load(query=query.TermQuery('second'), search_in_source_files=True) self.assertEqual(len(abook.contacts), 1) def test_loading_unparsable_vcard_fails(self): abook = address_book.VdirAddressBook('test', 'test/fixture/broken.abook') with self.assertRaises(address_book.AddressBookParseError): with self.assertLogs(level='ERROR'): abook.load() def test_unparsable_files_can_be_skipped(self): abook = address_book.VdirAddressBook( 'test', 'test/fixture/broken.abook', skip=True) with self.assertLogs(level='WARNING') as cm: abook.load() self.assertEqual(cm.output[0], 'WARNING:khard.carddav_object:Filtering some problematic tags ' 'from test/fixture/broken.abook/unparsable.vcf') # FIXME Remove this regex assert when either # https://github.com/eventable/vobject/issues/156 is closed or we drop # support for python 3.6 self.assertRegex(cm.output[1], 'ERROR:khard.address_book:Error: Could not parse file ' 'test/fixture/broken.abook/unparsable.vcf\n' 'At line [35]: Component VCARD was never closed') self.assertEqual(cm.output[2], 'WARNING:khard.address_book:1 of 1 vCard files of address book ' 'test could not be parsed.') @mock.patch.dict("os.environ", clear=True) def test_do_not_expand_env_var_that_is_unset(self): # Unset env vars shouldn't expand. with self.assertRaises(FileNotFoundError): address_book.VdirAddressBook( "test", "test/fixture/test.abook${}".format("KHARD_FOO")) @mock.patch.dict("os.environ", KHARD_FOO="") def test_expand_env_var_that_is_empty(self): # Env vars set to empty string should expand to empty string. abook = address_book.VdirAddressBook( "test", "test/fixture/test.abook${}".format("KHARD_FOO")) self.assertEqual(abook.path, "test/fixture/test.abook") @mock.patch.dict("os.environ", KHARD_FOO="test/fixture") def test_expand_env_var_that_is_nonempty(self): # Env vars set to nonempty string should expand appropriately. abook = address_book.VdirAddressBook( "test", "${}/test.abook".format("KHARD_FOO")) self.assertEqual(abook.path, "test/fixture/test.abook") class VcardAddressBookSearch(unittest.TestCase): @staticmethod def _search(query): with TmpAbook(["contact1.vcf", "contact2.vcf"]) as abook: return list(abook.search(query)) def test_uid_query(self): q = query.FieldQuery("uid", "testuid1") l = self._search(q) self.assertEqual(len(l), 1) self.assertEqual(l[0].uid, 'testuid1') def test_term_query(self): q = query.TermQuery("testuid1") l = self._search(q) self.assertEqual(len(l), 1) self.assertEqual(l[0].uid, 'testuid1') def test_term_query_matching(self): q = query.TermQuery("second contact") l = self._search(q) self.assertEqual(len(l), 1) self.assertEqual(l[0].uid, 'testuid1') def test_term_query_failing(self): q = query.TermQuery("this does not match") l = self._search(q) self.assertEqual(len(l), 0) def test_copied_from_merge_test_1(self): q = query.TermQuery("second") l = self._search(q) self.assertEqual(len(l), 1) self.assertEqual(l[0].uid, 'testuid1') def test_copied_from_merge_test_2(self): q = query.TermQuery("third") l = self._search(q) self.assertEqual(len(l), 1) self.assertEqual(l[0].uid, 'testuid2') class AddressBookGetShortUidDict(unittest.TestCase): def test_uniqe_uid_also_reslts_in_shortend_uid_in_short_uid_dict(self): contacts = {'uid123': None} abook = _AddressBook('test') abook.contacts = contacts abook._loaded = True short_uids = abook.get_short_uid_dict() self.assertEqual(len(short_uids), 1) short_uid, contact = short_uids.popitem() self.assertEqual(short_uid, 'u') class ReportedBugs(unittest.TestCase): def test_issue_159_uid_search_doesnt_return_items_twice(self): # This was the first half of bug report #159. abook = address_book.VdirAddressBook('test', 'test/fixture/test.abook') c = abook.search(query.TermQuery('testuid1')) self.assertEqual(len(list(c)), 1) khard-0.17.0/test/test_carddav_object.py000066400000000000000000000041501371517016500202200ustar00rootroot00000000000000"""Tests for the CarddavObject class from the carddav module.""" # pylint: disable=missing-docstring import base64 import datetime import unittest from unittest import mock from khard.carddav_object import CarddavObject class CarddavObjectFormatDateObject(unittest.TestCase): def test_format_date_object_will_not_touch_strings(self): expected = 'untouched string' actual = CarddavObject._format_date_object(expected, False) self.assertEqual(actual, expected) def test_format_date_object_with_simple_date_object(self): d = datetime.datetime(2018, 2, 13) actual = CarddavObject._format_date_object(d, False) self.assertEqual(actual, '2018-02-13') def test_format_date_object_with_simple_datetime_object(self): d = datetime.datetime(2018, 2, 13, 0, 38, 31) with mock.patch('time.timezone', -7200): actual = CarddavObject._format_date_object(d, False) self.assertEqual(actual, '2018-02-13T00:38:31+02:00') def test_format_date_object_with_date_1900(self): d = datetime.datetime(1900, 2, 13) actual = CarddavObject._format_date_object(d, False) self.assertEqual(actual, '--02-13') class AltIds(unittest.TestCase): def test_altids_are_read(self): card = CarddavObject.from_file(None, 'test/fixture/vcards/altid.vcf') expected = 'one representation' self.assertEqual(expected, card.get_first_name_last_name()) class Photo(unittest.TestCase): """Tests related to the PHOTO property of vCards""" PNG_HEADER = b'\x89PNG\r\n\x1a\n' def test_parsing_base64_ecoded_photo_vcard_v3(self): c = CarddavObject.from_file(None, 'test/fixture/vcards/photov3.vcf') self.assertEqual(c.vcard.photo.value[:8], self.PNG_HEADER) def test_parsing_base64_ecoded_photo_vcard_v4(self): c = CarddavObject.from_file(None, 'test/fixture/vcards/photov4.vcf') uri_stuff, data = c.vcard.photo.value.split(',') self.assertEqual(uri_stuff, 'data:image/png;base64') data = base64.decodebytes(data.encode()) self.assertEqual(data[:8], self.PNG_HEADER) khard-0.17.0/test/test_cli.py000066400000000000000000000101451371517016500160360ustar00rootroot00000000000000"""Tests for the cli module""" import unittest from unittest import mock from khard import cli from khard import query from .helpers import mock_stream @mock.patch.dict('os.environ', KHARD_CONFIG='test/fixture/minimal.conf') class TestParseArgs(unittest.TestCase): foo = query.TermQuery("foo") bar = query.TermQuery("bar") baz = query.TermQuery("baz") uid = query.FieldQuery("uid", "foo") def test_normal_search_terms_create_term_queries(self): expected = self.foo args, _config = cli.parse_args(['list', 'foo']) actual = args.search_terms self.assertEqual(expected, actual) def test_uid_options_create_uid_queries(self): expected = self.uid args, _config = cli.parse_args(['list', '--uid=foo']) actual = args.search_terms self.assertEqual(expected, actual) def test_multible_search_terms_generate_and_queries(self): expected = query.AndQuery(self.foo, self.bar) args, _config = cli.parse_args(['list', 'foo', 'bar']) actual = args.search_terms self.assertEqual(expected, actual) def test_no_search_terms_create_an_any_query(self): expected = query.AnyQuery() args, _config = cli.parse_args(['list']) actual = args.search_terms self.assertEqual(expected, actual) def test_target_search_terms_are_typed(self): args, _config = cli.parse_args(['merge', '--target=foo', 'bar']) self.assertEqual(self.foo, args.target_contact) self.assertEqual(self.bar, args.source_search_terms) def test_second_target_search_term_overrides_first(self): args, _config = cli.parse_args(['merge', '--target=foo', '--target=bar', 'baz']) self.assertEqual(self.bar, args.target_contact) self.assertEqual(self.baz, args.source_search_terms) def test_target_uid_option_creates_uid_queries(self): args, _config = cli.parse_args(['merge', '--target-uid=foo', 'bar']) self.assertEqual(self.uid, args.target_contact) self.assertEqual(self.bar, args.source_search_terms) def test_uid_option_is_combined_with_search_terms_for_merge_command(self): args, _config = cli.parse_args(['merge', '--uid=foo', '--target=bar']) self.assertEqual(self.uid, args.source_search_terms) self.assertEqual(self.bar, args.target_contact) def test_uid_and_free_search_terms_produce_a_conflict(self): with self.assertRaises(SystemExit): with mock_stream("stderr"): # just silence stderr cli.parse_args(['list', '--uid=foo', 'bar']) def test_target_uid_and_free_target_search_terms_produce_a_conflict(self): with self.assertRaises(SystemExit): with mock_stream("stderr"): # just silence stderr cli.parse_args(['merge', '--target-uid=foo', '--target=bar']) def test_no_target_specification_results_in_an_any_query(self): expected = query.AnyQuery() args, _config = cli.parse_args(['merge']) actual = args.target_contact self.assertEqual(expected, actual) def test_add_email_defaults_to_from_lowercase(self): args, _config = cli.parse_args(["add-email"]) actual = args.fields self.assertEqual(["from"], actual) def test_add_email_from_field(self): args, _config = cli.parse_args(["add-email", "-H", "from"]) actual = args.fields self.assertEqual(["from"], actual) def test_add_email_another_field(self): args, _config = cli.parse_args(["add-email", "-H", "OtHer"]) actual = args.fields self.assertEqual(["other"], actual) def test_add_email_multiple_headers_separate_args_takes_last(self): args, _config = cli.parse_args( ["add-email", "-H", "OtHer", "-H", "myfield"]) actual = args.fields self.assertEqual(["myfield"], actual) def test_add_email_multiple_headers_comma_separated(self): args, _config = cli.parse_args( ["add-email", "-H", "OtHer,myfield,from"]) actual = args.fields self.assertEqual(["other", "myfield", "from"], actual) khard-0.17.0/test/test_command_line_interface.py000066400000000000000000000470541371517016500217450ustar00rootroot00000000000000"""Test some features of the command line interface of khard. This also contains some "end to end" tests. That means some very high level calls to the main function and a check against the output. These might later be converted to proper "unit" tests. """ # pylint: disable=missing-docstring # TODO We are still missing high level tests for the add-email and merge # subcommands. They depend heavily on user interaction and are hard to test in # their current form. import io import pathlib import shutil import tempfile import unittest from unittest import mock from ruamel.yaml import YAML from khard import cli from khard import config from khard import khard from .helpers import TmpConfig, mock_stream def run_main(*args): """Run the khard.main() method with mocked stdout""" with mock_stream() as stdout: khard.main(args) return stdout @mock.patch('sys.argv', ['TESTSUITE']) class HelpOption(unittest.TestCase): def _test(self, args, expect): """Test the command line args and compare the prefix of the output.""" with self.assertRaises(SystemExit): with mock_stream() as stdout: cli.parse_args(args) text = stdout.getvalue() self.assertRegex(text, expect) def test_global_help(self): self._test(['-h'], r'^usage: TESTSUITE \[-h\]') @mock.patch.dict('os.environ', KHARD_CONFIG='test/fixture/minimal.conf') def test_subcommand_help(self): self._test(['list', '-h'], r'^usage: TESTSUITE list \[-h\]') def test_global_help_with_subcommand(self): self._test(['-h', 'list'], r'^usage: TESTSUITE \[-h\]') @mock.patch.dict('os.environ', KHARD_CONFIG='test/fixture/minimal.conf') class ListingCommands(unittest.TestCase): """Tests for subcommands that simply list stuff.""" def test_simple_ls_without_options(self): stdout = run_main("list") text = [l.strip() for l in stdout.getvalue().splitlines()] expected = [ "Address book: foo", "Index Name Phone " "Email Uid", "1 second contact voice: 0123456789 " "home: user@example.com testuid1", "2 text birthday " " testuid3", "3 third contact " " testuid2"] self.assertListEqual(text, expected) def test_ls_fields_like_email(self): stdout = run_main('ls', '-p', '-F', 'emails.home.0,name') text = stdout.getvalue().splitlines() expected = [ "user@example.com\tsecond contact", "\ttext birthday", "\tthird contact", ] self.assertListEqual(text, expected) @mock.patch.dict('os.environ', LC_ALL='C') def test_simple_bdays_without_options(self): stdout = run_main('birthdays') text = [line.strip() for line in stdout.getvalue().splitlines()] expect = ["Name Birthday", "text birthday circa 1800", "second contact 01/20/18"] self.assertListEqual(text, expect) def test_parsable_bdays(self): stdout = run_main('birthdays', '--parsable') text = stdout.getvalue().splitlines() expect = ["circa 1800\ttext birthday", "2018.01.20\tsecond contact"] self.assertListEqual(text, expect) def test_simple_email_without_options(self): stdout = run_main('email') text = [line.strip() for line in stdout.getvalue().splitlines()] expect = ["Name Type E-Mail", "second contact home user@example.com"] self.assertListEqual(text, expect) def test_simple_phone_without_options(self): stdout = run_main('phone') text = [line.strip() for line in stdout.getvalue().splitlines()] expect = ["Name Type Phone", "second contact voice 0123456789"] self.assertListEqual(text, expect) def test_simple_file_without_options(self): stdout = run_main('filename') text = [line.strip() for line in stdout.getvalue().splitlines()] expect = ["test/fixture/test.abook/contact1.vcf", "test/fixture/test.abook/text-bday.vcf", "test/fixture/test.abook/contact2.vcf"] self.assertListEqual(text, expect) def test_simple_abooks_without_options(self): stdout = run_main('addressbooks') text = stdout.getvalue().strip() expect = "foo" self.assertEqual(text, expect) def test_simple_details_without_options(self): stdout = run_main('details', 'uid1') text = stdout.getvalue() # Currently the FN field is not shown with "details". self.assertIn('Address book: foo', text) self.assertIn('UID: testuid1', text) def test_order_of_search_term_does_not_matter(self): stdout1 = run_main('list', 'second', 'contact') stdout2 = run_main('list', 'contact', 'second') text1 = [l.strip() for l in stdout1.getvalue().splitlines()] text2 = [l.strip() for l in stdout2.getvalue().splitlines()] expected = [ "Address book: foo", "Index Name Phone " "Email Uid", "1 second contact voice: 0123456789 " "home: user@example.com testuid1"] self.assertListEqual(text1, expected) self.assertListEqual(text2, expected) def test_case_of_search_terms_does_not_matter(self): stdout1 = run_main('list', 'second', 'contact') stdout2 = run_main('list', 'SECOND', 'CONTACT') text1 = [l.strip() for l in stdout1.getvalue().splitlines()] text2 = [l.strip() for l in stdout2.getvalue().splitlines()] expected = [ "Address book: foo", "Index Name Phone " "Email Uid", "1 second contact voice: 0123456789 " "home: user@example.com testuid1"] self.assertListEqual(text1, expected) self.assertListEqual(text2, expected) def test_regex_special_chars_are_not_special(self): with self.assertRaises(SystemExit): with mock_stream() as stdout: khard.main(['list', 'uid.']) self.assertEqual(stdout.getvalue(), "Found no contacts\n") def test_display_post_address(self): with TmpConfig(["post.vcf"]): stdout = run_main('postaddress') text = [line.rstrip() for line in stdout.getvalue().splitlines()] expected = [ 'Name Type Post address', 'With post address home Main Street 1', ' PostBox Ext', ' 00000 The City', ' SomeState, HomeCountry'] self.assertListEqual(expected, text) def test_email_lists_only_contacts_with_emails(self): with TmpConfig(["contact1.vcf", "contact2.vcf"]): stdout = run_main("email") text = [line.strip() for line in stdout.getvalue().splitlines()] expect = ["Name Type E-Mail", "second contact home user@example.com"] self.assertListEqual(expect, text) def test_phone_lists_only_contacts_with_phone_nubers(self): with TmpConfig(["contact1.vcf", "contact2.vcf"]): stdout = run_main("phone") text = [line.strip() for line in stdout.getvalue().splitlines()] expect = ["Name Type Phone", "second contact voice 0123456789"] self.assertListEqual(expect, text) def test_postaddr_lists_only_contacts_with_post_addresses(self): with TmpConfig(["contact1.vcf", "post.vcf"]): stdout = run_main("postaddress") text = [line.rstrip() for line in stdout.getvalue().splitlines()] expect = ['Name Type Post address', 'With post address home Main Street 1', ' PostBox Ext', ' 00000 The City', ' SomeState, HomeCountry'] self.assertListEqual(expect, text) class ListingCommands2(unittest.TestCase): def test_list_bug_195(self): with TmpConfig(['tel-value-uri.vcf']): stdout = run_main('list') text = [line.strip() for line in stdout.getvalue().splitlines()] expect = [ "Address book: tmp", "Index Name Phone Email Uid", "1 bug 195 cell: 67545678 b"] self.assertListEqual(text, expect) def test_list_bug_243_part_1(self): """Search for a category with the ls command""" with TmpConfig(['category.vcf']): stdout = run_main('list', 'bar') text = [line.strip() for line in stdout.getvalue().splitlines()] expect = [ "Address book: tmp", "Index Name Phone " "Email Uid", "1 contact with category " "internet: foo@example.org c", ] self.assertListEqual(text, expect) def test_list_bug_243_part_2(self): """Search for a category with the email command""" with TmpConfig(['category.vcf']): stdout = run_main('email', 'bar') text = [line.strip() for line in stdout.getvalue().splitlines()] expect = [ "Name Type E-Mail", "contact with category internet foo@example.org", ] self.assertListEqual(text, expect) def test_list_bug_251(self): "Find contacts by nickname even if a match by name exists" with TmpConfig(["test/fixture/nick.abook/nickname.vcf", "test/fixture/vcards/no-nickname.vcf"]): stdout = run_main('list', 'mike') text = [line.strip() for line in stdout.getvalue().splitlines()] expect = ['Address book: tmp', 'Index Name Phone Email ' 'Uid', '1 Michael Smith pref: ms@example.org ' 'issue251part1', '2 Mike Jones pref: mj@example.org ' 'issue251part2'] self.assertListEqual(text, expect) @mock.patch.dict('os.environ', KHARD_CONFIG='test/fixture/nick.conf') def test_email_bug_251(self): stdout = run_main('email', '--parsable', 'mike') text = [line.strip() for line in stdout.getvalue().splitlines()] expect = ["searching for 'mike' ...", "ms@example.org\tMichael Smith\tpref"] self.assertListEqual(text, expect) @mock.patch.dict('os.environ', KHARD_CONFIG='test/fixture/nick.conf') def test_email_bug_251_part2(self): stdout = run_main('email', '--parsable', 'joe') text = [line.strip() for line in stdout.getvalue().splitlines()] expect = ["searching for 'joe' ...", "jcitizen@foo.com\tJoe Citizen\tpref"] self.assertListEqual(text, expect) def test_email_bug_251_part_3(self): "Find contacts by nickname even if a match by name exists" with TmpConfig(["test/fixture/nick.abook/nickname.vcf", "test/fixture/vcards/no-nickname.vcf"]): stdout = run_main('email', '--parsable', 'mike') text = [line.strip() for line in stdout.getvalue().splitlines()] expect = ["searching for 'mike' ...", 'ms@example.org\tMichael Smith\tpref', 'mj@example.org\tMike Jones\tpref'] self.assertListEqual(text, expect) class FileSystemCommands(unittest.TestCase): """Tests for subcommands that interact with different address books.""" def setUp(self): "Create a temporary directory with two address books and a configfile." self._tmp = tempfile.TemporaryDirectory() path = pathlib.Path(self._tmp.name) self.abook1 = path / 'abook1' self.abook2 = path / 'abook2' self.abook1.mkdir() self.abook2.mkdir() self.contact = self.abook1 / 'contact.vcf' shutil.copy('test/fixture/vcards/contact1.vcf', str(self.contact)) config = path / 'conf' with config.open('w') as fh: fh.write("""[addressbooks] [[abook1]] path = {} [[abook2]] path = {}""".format(self.abook1, self.abook2)) self._patch = mock.patch.dict('os.environ', KHARD_CONFIG=str(config)) self._patch.start() def tearDown(self): self._patch.stop() self._tmp.cleanup() def test_simple_move(self): # just hide stdout with mock.patch('sys.stdout'): khard.main(['move', '-a', 'abook1', '-A', 'abook2', 'testuid1']) # The contact is moved to a filename based on the uid. target = self.abook2 / 'testuid1.vcf' # We currently only assert that the target file exists, nothing about # its contents. self.assertFalse(self.contact.exists()) self.assertTrue(target.exists()) def test_simple_copy(self): # just hide stdout with mock.patch('sys.stdout'): khard.main(['copy', '-a', 'abook1', '-A', 'abook2', 'testuid1']) # The contact is copied to a filename based on a new uid. results = list(self.abook2.glob('*.vcf')) self.assertTrue(self.contact.exists()) self.assertEqual(len(results), 1) def test_simple_remove_with_force_option(self): # just hide stdout with mock.patch('sys.stdout'): # Without the --force this asks for confirmation. khard.main(['remove', '--force', '-a', 'abook1', 'testuid1']) results = list(self.abook2.glob('*.vcf')) self.assertFalse(self.contact.exists()) self.assertEqual(len(results), 0) def test_new_contact_with_simple_user_input(self): old = len(list(self.abook1.glob('*.vcf'))) # Mock user input on stdin (yaml format). with mock.patch('sys.stdin.isatty', return_value=False): with mock.patch('sys.stdin.read', return_value='First name: foo\nLast name: bar'): # just hide stdout with mock.patch('sys.stdout'): # hide warning about missing version in vcard with self.assertLogs(level='WARNING'): khard.main(['new', '-a', 'abook1']) new = len(list(self.abook1.glob('*.vcf'))) self.assertEqual(new, old + 1) class MiscCommands(unittest.TestCase): """Tests for other subcommands.""" @mock.patch.dict('os.environ', KHARD_CONFIG='test/fixture/minimal.conf') def test_simple_show_with_yaml_format(self): stdout = run_main("show", "--format=yaml", "uid1") # This implicitly tests if the output is valid yaml. yaml = YAML(typ="base").load(stdout.getvalue()) # Just test some keys. self.assertIn('Address', yaml) self.assertIn('Birthday', yaml) self.assertIn('Email', yaml) self.assertIn('First name', yaml) self.assertIn('Last name', yaml) self.assertIn('Nickname', yaml) @mock.patch.dict('os.environ', KHARD_CONFIG='test/fixture/minimal.conf') def test_simple_edit_without_modification(self): with mock.patch('subprocess.Popen') as popen: run_main("edit", "uid1") # The editor is called with a temp file so how to we check this more # precisely? popen.assert_called_once() @mock.patch.dict('os.environ', KHARD_CONFIG='test/fixture/minimal.conf', EDITOR='editor') def test_edit_source_file_without_modifications(self): with mock.patch('subprocess.Popen') as popen: run_main("edit", "--format=vcard", "uid1") popen.assert_called_once_with(['editor', 'test/fixture/test.abook/contact1.vcf']) @mock.patch.dict('os.environ', KHARD_CONFIG='test/fixture/minimal.conf') class CommandLineDefaultsDoNotOverwriteConfigValues(unittest.TestCase): @staticmethod def _with_contact_table(args, **kwargs): args = cli.parse_args(args) options = '\n'.join('{}={}'.format(key, kwargs[key]) for key in kwargs) conf = config.Config(io.StringIO('[addressbooks]\n[[test]]\npath=.\n' '[contact table]\n' + options)) return cli.merge_args_into_config(args, conf) def test_group_by_addressbook(self): conf = self._with_contact_table(['list'], group_by_addressbook=True) self.assertTrue(conf.group_by_addressbook) @mock.patch.dict('os.environ', KHARD_CONFIG='test/fixture/minimal.conf') class CommandLineArgumentsOverwriteConfigValues(unittest.TestCase): @staticmethod def _merge(args): args, _conf = cli.parse_args(args) # This config file just loads all defaults from the config.spec. conf = config.Config(io.StringIO('[addressbooks]\n[[test]]\npath=.')) return cli.merge_args_into_config(args, conf) def test_sort_is_picked_up_from_arguments(self): conf = self._merge(['list', '--sort=last_name']) self.assertEqual(conf.sort, 'last_name') def test_display_is_picked_up_from_arguments(self): conf = self._merge(['list', '--display=last_name']) self.assertEqual(conf.display, 'last_name') def test_reverse_is_picked_up_from_arguments(self): conf = self._merge(['list', '--reverse']) self.assertTrue(conf.reverse) def test_group_by_addressbook_is_picked_up_from_arguments(self): conf = self._merge(['list', '--group-by-addressbook']) self.assertTrue(conf.group_by_addressbook) def test_search_in_source_is_picked_up_from_arguments(self): conf = self._merge(['list', '--search-in-source-files']) self.assertTrue(conf.search_in_source_files) class Merge(unittest.TestCase): def test_merge_with_exact_search_terms(self): with TmpConfig(["contact1.vcf", "contact2.vcf"]): with mock.patch('khard.khard.merge_existing_contacts') as merge: run_main("merge", "second", "--target", "third") merge.assert_called_once() # unpack the call arguments call = merge.mock_calls[0] name, args, kwargs = call first, second, delete = args self.assertTrue(delete) first = pathlib.Path(first.filename).name second = pathlib.Path(second.filename).name self.assertEqual('contact1.vcf', first) self.assertEqual('contact2.vcf', second) def test_merge_with_exact_uid_search_terms(self): with TmpConfig(["contact1.vcf", "contact2.vcf"]): with mock.patch('khard.khard.merge_existing_contacts') as merge: run_main("merge", "--uid", "testuid1", "--target-uid", "testuid2") merge.assert_called_once() # unpack the call arguments call = merge.mock_calls[0] name, args, kwargs = call first, second, delete = args self.assertTrue(delete) first = pathlib.Path(first.filename).name second = pathlib.Path(second.filename).name self.assertEqual('contact1.vcf', first) self.assertEqual('contact2.vcf', second) if __name__ == "__main__": unittest.main() khard-0.17.0/test/test_config.py000066400000000000000000000161761371517016500165460ustar00rootroot00000000000000"""Tests for the config module.""" # pylint: disable=missing-docstring import logging import os.path import tempfile import unittest import unittest.mock as mock import configobj from khard import config class LoadingConfigFile(unittest.TestCase): def test_load_non_existing_file_fails(self): filename = "I hope this file never exists" with self.assertRaises(IOError) as cm: config.Config._load_config_file(filename) self.assertTrue(str(cm.exception).startswith('Config file not found:')) def test_uses_khard_config_environment_variable(self): filename = "this is some very random string" with mock.patch.dict("os.environ", clear=True, KHARD_CONFIG=filename): with mock.patch("configobj.ConfigObj", dict): ret = config.Config._load_config_file("") self.assertEqual(ret['infile'], filename) def test_uses_xdg_config_home_environment_variable(self): prefix = "this is some very random string" with mock.patch.dict("os.environ", clear=True, XDG_CONFIG_HOME=prefix): with mock.patch("configobj.ConfigObj", dict): ret = config.Config._load_config_file("") expected = os.path.join(prefix, 'khard', 'khard.conf') self.assertEqual(ret['infile'], expected) def test_uses_config_dir_if_environment_unset(self): prefix = "this is some very random string" with mock.patch.dict("os.environ", clear=True, HOME=prefix): with mock.patch("configobj.ConfigObj", dict): ret = config.Config._load_config_file("") expected = os.path.join(prefix, '.config', 'khard', 'khard.conf') self.assertEqual(ret['infile'], expected) def test_load_empty_file_fails(self): with tempfile.NamedTemporaryFile() as name: with self.assertLogs(level=logging.ERROR): with self.assertRaises(config.ConfigError): config.Config(name) @mock.patch.dict('os.environ', EDITOR='editor', MERGE_EDITOR='meditor') def test_load_minimal_file_by_name(self): cfg = config.Config("test/fixture/minimal.conf") self.assertEqual(cfg.editor, "editor") self.assertEqual(cfg.merge_editor, "meditor") class ConfigPreferredVcardVersion(unittest.TestCase): def test_default_value_is_3(self): c = config.Config("test/fixture/minimal.conf") self.assertEqual(c.preferred_vcard_version, "3.0") def test_set_preferred_version(self): c = config.Config("test/fixture/minimal.conf") c.preferred_vcard_version = "11" self.assertEqual(c.preferred_vcard_version, "11") class Defaults(unittest.TestCase): def test_debug_defaults_to_false(self): c = config.Config("test/fixture/minimal.conf") self.assertFalse(c.debug) def test_default_action_defaults_to_none(self): c = config.Config("test/fixture/minimal.conf") self.assertIsNone(c.default_action) def test_reverse_defaults_to_false(self): c = config.Config("test/fixture/minimal.conf") self.assertFalse(c.reverse) def test_group_by_addressbook_defaults_to_false(self): c = config.Config("test/fixture/minimal.conf") self.assertFalse(c.group_by_addressbook) def test_show_nicknames_defaults_to_false(self): c = config.Config("test/fixture/minimal.conf") self.assertFalse(c.show_nicknames) def test_show_uids_defaults_to_true(self): c = config.Config("test/fixture/minimal.conf") self.assertTrue(c.show_uids) def test_sort_defaults_to_first_name(self): c = config.Config("test/fixture/minimal.conf") self.assertEqual(c.sort, 'first_name') def test_display_defaults_to_first_name(self): c = config.Config("test/fixture/minimal.conf") self.assertEqual(c.display, 'first_name') def test_localize_dates_defaults_to_true(self): c = config.Config("test/fixture/minimal.conf") self.assertTrue(c.localize_dates) def test_preferred_phone_number_type_defaults_to_pref(self): c = config.Config("test/fixture/minimal.conf") self.assertListEqual(c.preferred_phone_number_type, ['pref']) def test_preferred_email_address_type_defaults_to_pref(self): c = config.Config("test/fixture/minimal.conf") self.assertListEqual(c.preferred_email_address_type, ['pref']) def test_private_objects_defaults_to_empty(self): c = config.Config("test/fixture/minimal.conf") self.assertListEqual(c.private_objects, []) def test_search_in_source_files_defaults_to_false(self): c = config.Config("test/fixture/minimal.conf") self.assertFalse(c.search_in_source_files) def test_skip_unparsable_defaults_to_false(self): c = config.Config("test/fixture/minimal.conf") self.assertFalse(c.skip_unparsable) def test_preferred_version_defaults_to_3(self): c = config.Config("test/fixture/minimal.conf") self.assertEqual(c.preferred_vcard_version, '3.0') @mock.patch.dict('os.environ', clear=True) def test_editor_defaults_to_vim(self): c = config.Config("test/fixture/minimal.conf") self.assertEqual(c.editor, 'vim') @mock.patch.dict('os.environ', clear=True) def test_merge_editor_defaults_to_vimdiff(self): c = config.Config("test/fixture/minimal.conf") self.assertEqual(c.merge_editor, 'vimdiff') class Validation(unittest.TestCase): @staticmethod def _template(section, key, value): configspec = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'khard', 'data', 'config.spec') c = configobj.ConfigObj(configspec=configspec) c['general'] = {} c['vcard'] = {} c['contact table'] = {} c['addressbooks'] = {'test': {'path': '/tmp'}} c[section][key] = value return c def test_rejects_invalid_default_actions(self): action = 'this is not a valid action' conf = self._template('general', 'default_action', action) with self.assertLogs(level=logging.ERROR): with self.assertRaises(config.ConfigError): config.Config._validate(conf) def test_rejects_unparsable_editor_commands(self): editor = 'editor --option "unparsable because quotes are missing' conf = self._template('general', 'editor', editor) with self.assertLogs(level=logging.ERROR): with self.assertRaises(config.ConfigError): config.Config._validate(conf) def test_rejects_private_objects_with_strange_chars(self): obj = 'X-VCÄRD-EXTENSIÖN' conf = self._template('vcard', 'private_objects', obj) with self.assertLogs(level=logging.ERROR): with self.assertRaises(config.ConfigError): config.Config._validate(conf) def test_rejects_private_objects_starting_with_minus(self): obj = '-INVALID-' conf = self._template('vcard', 'private_objects', obj) with self.assertLogs(level=logging.ERROR): with self.assertRaises(config.ConfigError): config.Config._validate(conf) if __name__ == "__main__": unittest.main() khard-0.17.0/test/test_formatter.py000066400000000000000000000074731371517016500173040ustar00rootroot00000000000000"""Tests for the vcard formatting functions""" import unittest from vobject.vcard import Name from khard.carddav_object import CarddavObject from khard.formatter import Formatter from .helpers import vCard class FormatLabeledField(unittest.TestCase): def test_labels_are_selected_alphabetically_if_no_preferred_given(self): labeled_field = {'some': ['thing'], 'other': ['thing']} preferred = [] expected = 'other: thing' self.assertEqual(expected, Formatter.format_labeled_field( labeled_field, preferred)) def test_labels_are_selected_alphabetically_if_no_preferred_matches(self): labeled_field = {'some': ['thing'], 'other': ['thing']} preferred = ['nonexistent'] expected = 'other: thing' self.assertEqual(expected, Formatter.format_labeled_field( labeled_field, preferred)) def test_preferred_labels_are_used(self): labeled_field = {'some': ['thing'], 'other': ['thing']} preferred = ['some'] expected = 'some: thing' self.assertEqual(expected, Formatter.format_labeled_field( labeled_field, preferred)) def test_alphabetically_first_value_is_used(self): labeled_field = {'some': ['thing', 'more']} preferred = [] expected = 'some: more' self.assertEqual(expected, Formatter.format_labeled_field( labeled_field, preferred)) def test_not_only_first_char_of_label_is_used(self): preferred = [] labeled_field = {'x-foo': ['foo'], 'x-bar': ['bar']} expected = 'x-bar: bar' self.assertEqual(expected, Formatter.format_labeled_field( labeled_field, preferred)) expected = 'bar: bar' labeled_field = {'foo': ['foo'], 'bar': ['bar']} self.assertEqual(expected, Formatter.format_labeled_field( labeled_field, preferred)) class GetSpecialField(unittest.TestCase): _name = Name(family='Family', given='Given', additional='Additional', prefix='Prefix', suffix='Suffix') _vcard = CarddavObject(vCard(fn="Formatted Name", n=_name, nickname="Nickname"), None, "") def _test_name(self, fmt, nick, parsable, expected): f = Formatter(fmt, [], [], nick, parsable) actual = f.get_special_field(self._vcard, "name") self.assertEqual(expected, actual) def test_name_formatted_as_first_name_last_name(self): self._test_name(Formatter.FIRST, False, False, "Given Additional Family") def test_name_formatted_as_first_name_last_name_with_nickname(self): self._test_name(Formatter.FIRST, True, False, "Given Additional Family (Nickname: Nickname)") def test_name_formatted_as_last_name_first_name(self): self._test_name(Formatter.LAST, False, False, "Family, Given Additional") def test_name_formatted_as_last_name_first_name_with_nickname(self): self._test_name(Formatter.LAST, True, False, "Family, Given Additional (Nickname: Nickname)") def test_name_formatted_as_formatted_name(self): self._test_name(Formatter.FORMAT, False, False, "Formatted Name") def test_name_formatted_as_formatted_name_with_nickname(self): self._test_name(Formatter.FORMAT, True, False, "Formatted Name (Nickname: Nickname)") def test_parsable_overrides_nickname_with_first_formatting(self): self._test_name(Formatter.FIRST, True, True, "Given Additional Family") def test_parsable_overrides_nickname_with_last_formatting(self): self._test_name(Formatter.LAST, True, True, "Family, Given Additional") def test_parsable_overrides_nickname_with_formatted_name(self): self._test_name(Formatter.FORMAT, True, True, "Formatted Name") khard-0.17.0/test/test_helpers.py000066400000000000000000000077371371517016500167460ustar00rootroot00000000000000"""Tests for the helpers module.""" # pylint: disable=missing-docstring import datetime import unittest from khard import helpers class ListToString(unittest.TestCase): def test_empty_list_returns_empty_string(self): the_list = [] delimiter = ' ' expected = '' actual = helpers.list_to_string(the_list, delimiter) self.assertEqual(actual, expected) def test_simple_list(self): the_list = ['a', 'bc', 'def'] delimiter = ' ' expected = 'a bc def' actual = helpers.list_to_string(the_list, delimiter) self.assertEqual(actual, expected) def test_simple_nested_list(self): the_list = ['a', 'bc', ['x', 'y', 'z'], 'def'] delimiter = ' ' expected = 'a bc x y z def' actual = helpers.list_to_string(the_list, delimiter) self.assertEqual(actual, expected) def test_multi_level_nested_list(self): the_list = ['a', ['b', ['c', [[['x', 'y']]]]], 'z'] delimiter = ' ' expected = 'a b c x y z' actual = helpers.list_to_string(the_list, delimiter) self.assertEqual(actual, expected) def test_list_to_string_passes_through_other_objects(self): self.assertIs(helpers.list_to_string(None, "foo"), None) self.assertIs(helpers.list_to_string(42, "foo"), 42) self.assertIs(helpers.list_to_string("foo bar", "foo"), "foo bar") class StringToDate(unittest.TestCase): date = datetime.datetime(year=1900, month=1, day=2) time = datetime.datetime(year=1900, month=1, day=2, hour=12, minute=42, second=17) zone = datetime.datetime(year=1900, month=1, day=2, hour=12, minute=42, second=17, tzinfo=datetime.timezone.utc) def test_mmdd_format(self): string = '--0102' result = helpers.string_to_date(string) self.assertEqual(result, self.date) def test_mm_dd_format(self): string = '--01-02' result = helpers.string_to_date(string) self.assertEqual(result, self.date) def test_yyyymmdd_format(self): string = '19000102' result = helpers.string_to_date(string) self.assertEqual(result, self.date) def test_yyyy_mm_dd_format(self): string = '1900-01-02' result = helpers.string_to_date(string) self.assertEqual(result, self.date) def test_yyyymmddThhmmss_format(self): string = '19000102T124217' result = helpers.string_to_date(string) self.assertEqual(result, self.time) def test_yyyy_mm_ddThh_mm_ss_format(self): string = '1900-01-02T12:42:17' result = helpers.string_to_date(string) self.assertEqual(result, self.time) def test_yyyymmddThhmmssZ_format(self): string = '19000102T124217Z' result = helpers.string_to_date(string) self.assertEqual(result, self.time) def test_yyyy_mm_ddThh_mm_ssZ_format(self): string = '1900-01-02T12:42:17Z' result = helpers.string_to_date(string) self.assertEqual(result, self.time) def test_yyyymmddThhmmssz_format(self): string = '19000102T064217-06:00' result = helpers.string_to_date(string) self.assertEqual(result, self.zone) def test_yyyy_mm_ddThh_mm_ssz_format(self): string = '1900-01-02T06:42:17-06:00' result = helpers.string_to_date(string) self.assertEqual(result, self.zone) class ConvertToYAML(unittest.TestCase): def test_colon_handling(self): result = helpers.convert_to_yaml("Note", "foo: bar", 0, 5, True) self.assertListEqual(result, ["Note : |\n foo: bar"]) def test_none_values_produce_no_output(self): result = helpers.convert_to_yaml("Note", None, 0, 5, True) self.assertListEqual(result, []) def test_empty_strings_produce_empty_values(self): result = helpers.convert_to_yaml("Note", "", 0, 5, True) self.assertListEqual(result, ["Note : "]) if __name__ == "__main__": unittest.main() khard-0.17.0/test/test_khard.py000066400000000000000000000174351371517016500163710ustar00rootroot00000000000000"""Unittests for the khard module""" from argparse import Namespace from email.headerregistry import Address import unittest from unittest import mock from khard import khard, query, config from khard.khard import find_email_addresses from .helpers import TmpAbook class TestSearchQueryPreparation(unittest.TestCase): foo = query.TermQuery("foo") bar = query.TermQuery("bar") def setUp(self): # Set the uninitialized global variable in the khard module to make it # mockable. See https://stackoverflow.com/questions/61193676 khard.config = mock.Mock() @staticmethod def _make_abook(name): abook = mock.Mock() abook.name = name return abook @classmethod def _run(cls, **kwargs): with mock.patch("khard.khard.config.abooks", [cls._make_abook(name) for name in ["foo", "bar", "baz"]]): return khard.prepare_search_queries(Namespace(**kwargs)) def test_queries_for_the_same_address_book_are_joind_by_disjunction(self): expected = self.foo | self.bar prepared = self._run(addressbook=["foo"], target_addressbook=["foo"], source_search_terms=self.foo, target_contact=self.bar) self.assertEqual(expected, prepared["foo"]) def test_no_search_terms_result_in_any_queries(self): expected = query.AnyQuery() prepared = self._run(addressbook=["foo"], target_addressbook=["foo"], source_search_terms=query.AnyQuery(), target_contact=query.AnyQuery()) self.assertEqual(expected, prepared["foo"]) class TestAddEmail(unittest.TestCase): def test_find_email_addresses_empty_text_finds_none(self): text = "" addrs = find_email_addresses(text, ["from"]) self.assertEqual([], addrs) def test_find_email_addresses_single_header_finds_one_address(self): text = """From: John Doe """ addrs = find_email_addresses(text, ["from"]) expected = [Address(display_name="John Doe", username="jdoe", domain="machine.example")] self.assertEqual(expected, addrs) def test_find_email_addresses_single_header_finds_multiple_addresses(self): text = """From: John Doe , \ Mary Smith """ addrs = find_email_addresses(text, ["from"]) expected = [ Address( display_name="John Doe", username="jdoe", domain="machine.example"), Address( display_name="Mary Smith", username="mary", domain="example.net")] self.assertEqual(expected, addrs) def test_find_email_addresses_non_address_header_finds_none(self): text = "From: John Doe , " \ "Mary Smith \nOther: test" addrs = find_email_addresses(text, ["other"]) expected = [] self.assertEqual(expected, addrs) def test_find_email_addresses_multiple_headers_finds_some(self): text = "From: John Doe , " \ "Mary Smith \nOther: test" addrs = find_email_addresses(text, ["other", "from"]) expected = [ Address( display_name="John Doe", username="jdoe", domain="machine.example"), Address( display_name="Mary Smith", username="mary", domain="example.net")] self.assertEqual(expected, addrs) def test_find_email_addresses_multiple_headers_finds_all(self): text = "From: John Doe , " \ "Mary Smith \n" \ "To: Michael Jones " addrs = find_email_addresses(text, ["to", "FrOm"]) expected = [ Address( display_name="Michael Jones", username="mjones", domain="machine.example"), Address( display_name="John Doe", username="jdoe", domain="machine.example"), Address( display_name="Mary Smith", username="mary", domain="example.net")] self.assertEqual(expected, addrs) def test_find_email_addresses_finds_all_emails(self): text = "From: John Doe , " \ "Mary Smith \n" \ "To: Michael Jones " addrs = find_email_addresses(text, ["all"]) expected = [ Address( display_name="John Doe", username="jdoe", domain="machine.example"), Address( display_name="Mary Smith", username="mary", domain="example.net"), Address( display_name="Michael Jones", username="mjones", domain="machine.example")] self.assertEqual(expected, addrs) def test_find_email_addresses_finds_all_emails_with_other_headers_too( self): text = "From: John Doe , " \ "Mary Smith \n" \ "To: Michael Jones " addrs = find_email_addresses(text, ["other", "all", "from"]) expected = [ Address( display_name="John Doe", username="jdoe", domain="machine.example"), Address( display_name="Mary Smith", username="mary", domain="example.net"), Address( display_name="Michael Jones", username="mjones", domain="machine.example")] self.assertEqual(expected, addrs) class TestGetContactListByUserSelection(unittest.TestCase): def setUp(self): """initialize the global config object with a mock""" khard.config = mock.Mock(spec=config.Config) khard.config.group_by_addressbook = False khard.config.reverse = False khard.config.sort = "last_name" def tearDown(self): del khard.config def test_uid_query_without_strict_search(self): q = query.FieldQuery("uid", "testuid1") with TmpAbook(["contact1.vcf", "contact2.vcf"]) as abook: l = khard.get_contact_list_by_user_selection(abook, q) self.assertEqual(len(l), 1) self.assertEqual(l[0].uid, 'testuid1') def test_name_query_with_uid_text_and_strict_search(self): q = query.NameQuery("testuid1") with TmpAbook(["contact1.vcf", "contact2.vcf"]) as abook: l = khard.get_contact_list_by_user_selection(abook, q) self.assertEqual(len(l), 0) def test_name_query_with_uid_text_and_without_strict_search(self): q = query.NameQuery("testuid1") with TmpAbook(["contact1.vcf", "contact2.vcf"]) as abook: l = khard.get_contact_list_by_user_selection(abook, q) self.assertEqual(len(l), 0) def test_term_query_without_strict_search(self): q = query.TermQuery("testuid1") with TmpAbook(["contact1.vcf", "contact2.vcf"]) as abook: l = khard.get_contact_list_by_user_selection(abook, q) self.assertEqual(len(l), 1) self.assertEqual(l[0].uid, 'testuid1') def test_term_query_with_strict_search_matching(self): q = query.TermQuery("second contact") with TmpAbook(["contact1.vcf", "contact2.vcf"]) as abook: l = khard.get_contact_list_by_user_selection(abook, q) self.assertEqual(len(l), 1) self.assertEqual(l[0].uid, 'testuid1') khard-0.17.0/test/test_query.py000066400000000000000000000153261371517016500164420ustar00rootroot00000000000000import unittest from khard.query import AndQuery, AnyQuery, FieldQuery, NameQuery, NullQuery, \ OrQuery, TermQuery, parse from .helpers import TestCarddavObject, load_contact class TestTermQuery(unittest.TestCase): def test_match_if_query_is_anywhere_in_string(self): q = TermQuery('bar') self.assertTrue(q.match('foo bar baz')) def test_query_terms_are_case_insensitive(self): q = TermQuery('BAR') self.assertTrue(q.match('foo bar baz')) def test_match_arguments_are_case_insensitive(self): q = TermQuery('bar') self.assertTrue(q.match('FOO BAR BAZ')) def test_spaces_in_search_subject_are_not_stripped(self): q = TermQuery('oob') self.assertFalse(q.match('foo bar baz')) def test_spaces_in_query_are_not_stripped(self): q = TermQuery('foo bar') self.assertFalse(q.match('foobar')) class TestAndQuery(unittest.TestCase): def test_matches_if_all_subterms_match(self): q1 = TermQuery("a") q2 = TermQuery("b") q = AndQuery(q1, q2) self.assertTrue(q.match("ab")) def test_failes_if_at_least_one_subterm_fails(self): q1 = TermQuery("a") q2 = TermQuery("b") q = AndQuery(q1, q2) self.assertFalse(q.match("ac")) class TestOrQuery(unittest.TestCase): def test_matches_if_at_least_one_subterm_matchs(self): q1 = TermQuery("a") q2 = TermQuery("b") q = OrQuery(q1, q2) self.assertTrue(q.match("ac")) def test_failes_if_all_subterms_fail(self): q1 = TermQuery("a") q2 = TermQuery("b") q = OrQuery(q1, q2) self.assertFalse(q.match("cd")) class TestEquality(unittest.TestCase): def test_any_queries_are_equal(self): self.assertEqual(AnyQuery(), AnyQuery()) def test_null_queries_are_equal(self): self.assertEqual(NullQuery(), NullQuery()) def test_or_queries_match_after_sorting(self): null = NullQuery() any = AnyQuery() term = TermQuery("foo") field = FieldQuery("x", "y") first = OrQuery(null, any , term, field) second = OrQuery(any, null, field, term) self.assertEqual(first, second) def test_and_queries_match_after_sorting(self): null = NullQuery() any = AnyQuery() term = TermQuery("foo") field = FieldQuery("x", "y") first = AndQuery(null, any , term, field) second = AndQuery(any, null, field, term) self.assertEqual(first, second) class TestFieldQuery(unittest.TestCase): @unittest.expectedFailure def test_empty_field_values_match_if_the_field_is_present(self): # This test currently fails because the CarddavObject class has all # attributes set because they are properties. So the test in the query # class if an attribute is present never fails. uid = 'Some Test Uid' vcard1 = TestCarddavObject(uid=uid) vcard2 = TestCarddavObject() query = FieldQuery('uid', '') self.assertTrue(query.match(vcard1)) self.assertFalse(query.match(vcard2)) def test_empty_field_values_fails_if_the_field_is_absent(self): vcard = TestCarddavObject() query = FieldQuery('emails', '') self.assertFalse(query.match(vcard)) def test_values_can_match_exact(self): uid = 'Some Test Uid' vcard = TestCarddavObject(uid=uid) query = FieldQuery('uid', uid) self.assertTrue(query.match(vcard)) def test_values_can_match_substrings(self): uid = 'Some Test Uid' vcard = TestCarddavObject(uid=uid) query = FieldQuery('uid', 'e Test U') self.assertTrue(query.match(vcard)) def test_valuess_can_match_case_insensitive(self): uid = 'Some Test Uid' vcard = TestCarddavObject(uid=uid) query1 = FieldQuery('uid', uid.upper()) query2 = FieldQuery('uid', uid.lower()) self.assertTrue(query1.match(vcard)) self.assertTrue(query2.match(vcard)) def test_match_formatted_name(self): vcard = TestCarddavObject(fn='foo bar') query = FieldQuery('formatted_name', 'foo') self.assertTrue(query.match(vcard)) def test_match_email(self): vcard = load_contact("contact1.vcf") query = FieldQuery('emails', 'user@example.com') self.assertTrue(query.match(vcard)) def test_match_birthday(self): vcard = load_contact("contact1.vcf") query = FieldQuery('birthday', '2018-01-20') self.assertTrue(query.match(vcard)) def test_fail_match_in_other_field(self): vcard = load_contact("contact1.vcf") query = FieldQuery('formatted_name', 'user@example.com') self.assertFalse(query.match(vcard)) def test_match_email_type(self): vcard = load_contact("contact1.vcf") query = FieldQuery('emails', 'home') self.assertTrue(query.match(vcard)) class TestNameQuery(unittest.TestCase): def test_matches_formatted_name_field(self): vcard = load_contact("minimal.vcf") query = NameQuery("minimal") self.assertTrue(query.match(vcard)) def test_matches_name_field(self): vcard = load_contact("nickname.vcf") query = NameQuery("smith") self.assertTrue(query.match(vcard)) def test_matches_nickname_field(self): vcard = load_contact("nickname.vcf") query = NameQuery("mike") self.assertTrue(query.match(vcard)) def test_does_not_match_uid_field(self): vcard = load_contact("contact1.vcf") query = NameQuery("testuid1") self.assertFalse(query.match(vcard)) class TestParser(unittest.TestCase): def test_parsing_simple_terms(self): string = "foo bar" expected = TermQuery(string) actual = parse(string) self.assertEqual(actual, expected) def test_parsing_simple_field_queries(self): actual = parse("formatted_name:foo bar") expected = FieldQuery("formatted_name", "foo bar") self.assertEqual(actual, expected) def test_bad_field_name_returns_term_query(self): string = "foo:bar" actual = parse(string) expected = TermQuery(string) self.assertEqual(actual, expected) def test_field_value_can_be_empty(self): actual = parse("formatted_name:") expected = FieldQuery("formatted_name", "") self.assertEqual(actual, expected) def test_field_value_can_contain_colons(self): actual = parse("formatted_name:foo:bar") expected = FieldQuery("formatted_name", "foo:bar") self.assertEqual(actual, expected) def test_special_field_name_creates_name_queries(self): actual = parse("name:foo") expected = NameQuery("foo") self.assertEqual(actual, expected) khard-0.17.0/test/test_vcard_wrapper.py000066400000000000000000000427671371517016500201450ustar00rootroot00000000000000"""Tests for the VCardWrapper class from the carddav module.""" # pylint: disable=missing-docstring import datetime import unittest import vobject from khard.carddav_object import VCardWrapper from .helpers import vCard, TestVCardWrapper def _from_file(path): """Read a VCARD from a file""" with open(path) as fp: return vobject.readOne(fp) class VcardWrapperInit(unittest.TestCase): def test_stores_vcard_object_unmodified(self): vcard = vCard() expected = vcard.serialize() wrapper = VCardWrapper(vcard) # assert that it is the same object self.assertIs(wrapper.vcard, vcard) # assert that it (the serialization) was not changed self.assertEqual(wrapper.vcard.serialize(), expected) def test_warns_about_unsupported_version(self): with self.assertLogs(level="WARNING"): TestVCardWrapper(version="something unsupported") def test_warns_about_missing_version_and_sets_it(self): vcard = vCard() vcard.remove(vcard.version) with self.assertLogs(level="WARNING"): wrapper = VCardWrapper(vcard) self.assertEqual(wrapper.version, "3.0") class DeleteVcardObject(unittest.TestCase): def test_deletes_fields_given_in_upper_case(self): vcard = vCard() expected = vcard.serialize() vcard.add('FOO').value = 'bar' wrapper = VCardWrapper(vcard) wrapper._delete_vcard_object('FOO') self.assertEqual(wrapper.vcard.serialize(), expected) def test_deletes_all_field_occurences(self): vcard = vCard() expected = vcard.serialize() vcard.add('FOO').value = 'bar' vcard.add('FOO').value = 'baz' wrapper = VCardWrapper(vcard) wrapper._delete_vcard_object('FOO') self.assertEqual(wrapper.vcard.serialize(), expected) def test_deletes_grouped_ablabel_fields(self): vcard = vCard() expected = vcard.serialize() foo = vcard.add('FOO') foo.value = 'bar' foo.group = 'group1' label = vcard.add('X-ABLABEL') label.value = 'test label' label.group = foo.group wrapper = VCardWrapper(vcard) wrapper._delete_vcard_object('FOO') self.assertEqual(wrapper.vcard.serialize(), expected) def test_keeps_other_fields(self): vcard = vCard(foo='bar') expected = vcard.serialize() vcard.add('BAR').value = 'baz' wrapper = VCardWrapper(vcard) wrapper._delete_vcard_object('BAR') self.assertEqual(wrapper.vcard.serialize(), expected) def test_does_not_fail_on_non_existing_field_name(self): vcard = vCard(foo='bar') expected = vcard.serialize() wrapper = VCardWrapper(vcard) wrapper._delete_vcard_object('BAR') self.assertEqual(wrapper.vcard.serialize(), expected) class BirthdayLikeAttributes(unittest.TestCase): def test_birthday_supports_setting_date_objects(self): wrapper = TestVCardWrapper() date = datetime.datetime(2018, 2, 1) wrapper.birthday = date wrapper.vcard.validate() self.assertEqual(wrapper.birthday, date) def test_birthday_supports_setting_datetime_objects(self): wrapper = TestVCardWrapper() date = datetime.datetime(2018, 2, 1, 19, 29, 31) wrapper.birthday = date wrapper.vcard.validate() self.assertEqual(wrapper.birthday, date) def test_birthday_supports_setting_text_values_for_v4(self): vcard = vCard(version="4.0") wrapper = VCardWrapper(vcard, "4.0") date = 'some time yesterday' wrapper.birthday = date wrapper.vcard.validate() self.assertEqual(wrapper.birthday, date) def test_birthday_does_not_support_setting_text_values_for_v3(self): wrapper = TestVCardWrapper(version="3.0") with self.assertLogs(level='WARNING'): wrapper.birthday = 'some time yesterday' wrapper.vcard.validate() self.assertIsNone(wrapper.birthday) def test_anniversary_supports_setting_date_objects(self): wrapper = TestVCardWrapper() date = datetime.datetime(2018, 2, 1) wrapper.anniversary = date wrapper.vcard.validate() self.assertEqual(wrapper.anniversary, date) def test_anniversary_supports_setting_datetime_objects(self): wrapper = TestVCardWrapper() date = datetime.datetime(2018, 2, 1, 19, 29, 31) wrapper.anniversary = date wrapper.vcard.validate() self.assertEqual(wrapper.anniversary, date) def test_anniversary_supports_setting_text_values_for_v4(self): vcard = vCard(version="4.0") wrapper = VCardWrapper(vcard, "4.0") date = 'some time yesterday' wrapper.anniversary = date wrapper.vcard.validate() self.assertEqual(wrapper.anniversary, date) def test_anniversary_does_not_support_setting_text_values_for_v3(self): wrapper = TestVCardWrapper(version="3.0") with self.assertLogs(level='WARNING'): wrapper.birthday = 'some time yesterday' wrapper.vcard.validate() self.assertIsNone(wrapper.anniversary) class NameAttributes(unittest.TestCase): def test_fn_can_be_set_with_a_string(self): vcard = vCard() wrapper = VCardWrapper(vcard) wrapper.formatted_name = 'foo bar' self.assertEqual(vcard.fn.value, 'foo bar') def test_only_one_fn_will_be_stored(self): vcard = vCard() wrapper = VCardWrapper(vcard) wrapper.formatted_name = 'foo bar' self.assertEqual(len(vcard.contents['fn']), 1) def test_fn_is_returned_as_string(self): wrapper = TestVCardWrapper() self.assertIsInstance(wrapper.formatted_name, str) def test_fn_is_used_as_string_representation(self): wrapper = TestVCardWrapper() self.assertEqual(str(wrapper), wrapper.formatted_name) def test_name_can_be_set_with_empty_strings(self): vcard = vCard() wrapper = VCardWrapper(vcard) wrapper._add_name('', '', '', '', '') self.assertEqual(vcard.serialize(), 'BEGIN:VCARD\r\n' 'VERSION:3.0\r\n' 'FN:Test vCard\r\n' 'N:;;;;\r\n' 'END:VCARD\r\n') def test_name_can_be_set_with_empty_lists(self): vcard = vCard() wrapper = VCardWrapper(vcard) wrapper._add_name([], [], [], [], []) self.assertEqual(vcard.serialize(), 'BEGIN:VCARD\r\n' 'VERSION:3.0\r\n' 'FN:Test vCard\r\n' 'N:;;;;\r\n' 'END:VCARD\r\n') def test_name_can_be_set_with_lists_of_empty_strings(self): vcard = vCard() wrapper = VCardWrapper(vcard) wrapper._add_name(['', ''], ['', ''], ['', ''], ['', ''], ['', '']) self.assertEqual(vcard.serialize(), 'BEGIN:VCARD\r\n' 'VERSION:3.0\r\n' 'FN:Test vCard\r\n' 'N:;;;;\r\n' 'END:VCARD\r\n') def test_get_first_name_last_name_retunrs_fn_if_no_name_present(self): wrapper = TestVCardWrapper() self.assertEqual(wrapper.get_first_name_last_name(), 'Test vCard') def test_get_first_name_last_name_with_simple_name(self): wrapper = TestVCardWrapper() wrapper._add_name('', 'given', '', 'family', '') self.assertEqual(wrapper.get_first_name_last_name(), "given family") def test_get_first_name_last_name_with_all_name_fields(self): wrapper = TestVCardWrapper() wrapper._add_name('prefix', 'given', 'additional', 'family', 'suffix') self.assertEqual(wrapper.get_first_name_last_name(), 'given additional family') def test_get_first_name_last_name_with_complex_name(self): wrapper = TestVCardWrapper() wrapper._add_name(['prefix1', 'prefix2'], ['given1', 'given2'], ['additional1', 'additional2'], ['family1', 'family2'], ['suffix1', 'suffix2']) self.assertEqual(wrapper.get_first_name_last_name(), 'given1 given2 ' 'additional1 additional2 family1 family2') def test_get_last_name_first_name_retunrs_fn_if_no_name_present(self): wrapper = TestVCardWrapper() self.assertEqual(wrapper.get_last_name_first_name(), 'Test vCard') def test_get_last_name_first_name_with_simple_name(self): wrapper = TestVCardWrapper() wrapper._add_name('', 'given', '', 'family', '') self.assertEqual(wrapper.get_last_name_first_name(), "family, given") def test_get_last_name_first_name_with_all_name_fields(self): wrapper = TestVCardWrapper() wrapper._add_name('prefix', 'given', 'additional', 'family', 'suffix') self.assertEqual(wrapper.get_last_name_first_name(), 'family, given additional') def test_get_last_name_first_name_with_complex_name(self): wrapper = TestVCardWrapper() wrapper._add_name(['prefix1', 'prefix2'], ['given1', 'given2'], ['additional1', 'additional2'], ['family1', 'family2'], ['suffix1', 'suffix2']) self.assertEqual(wrapper.get_last_name_first_name(), 'family1 family2,' ' given1 given2 additional1 additional2') class TypedProperties(unittest.TestCase): def test_adding_a_simple_phone_number(self): wrapper = TestVCardWrapper() wrapper._add_phone_number('home', '0123456789') self.assertDictEqual(wrapper.phone_numbers, {'home': ['0123456789']}) def test_adding_a_custom_type_phone_number(self): wrapper = TestVCardWrapper() wrapper._add_phone_number('custom_type', '0123456789') self.assertDictEqual(wrapper.phone_numbers, {'custom_type': ['0123456789']}) def test_adding_multible_phone_number(self): wrapper = TestVCardWrapper() wrapper._add_phone_number('work', '0987654321') wrapper._add_phone_number('home', '0123456789') wrapper._add_phone_number('home', '0112233445') self.assertDictEqual( wrapper.phone_numbers, # The lists are sorted! {'home': ['0112233445', '0123456789'], 'work': ['0987654321']}) def test_adding_preferred_phone_number(self): wrapper = TestVCardWrapper() wrapper._add_phone_number('home', '0123456789') wrapper._add_phone_number('pref,home', '0987654321') self.assertDictEqual( wrapper.phone_numbers, {'home': ['0123456789'], 'home, pref': ['0987654321']}) def test_adding_a_simple_email(self): wrapper = TestVCardWrapper() wrapper.add_email('home', 'foo@bar.net') self.assertDictEqual(wrapper.emails, {'home': ['foo@bar.net']}) def test_adding_a_custom_type_emails(self): wrapper = TestVCardWrapper() wrapper.add_email('custom_type', 'foo@bar.net') self.assertDictEqual(wrapper.emails, {'custom_type': ['foo@bar.net']}) def test_adding_multible_emails(self): wrapper = TestVCardWrapper() wrapper.add_email('work', 'foo@bar.net') wrapper.add_email('home', 'foo@baz.net') wrapper.add_email('home', 'baz@baz.net') self.assertDictEqual( wrapper.emails, # The lists are sorted! {'home': ['baz@baz.net', 'foo@baz.net'], 'work': ['foo@bar.net']}) def test_adding_preferred_emails(self): wrapper = TestVCardWrapper() wrapper.add_email('home', 'foo@bar.net') wrapper.add_email('pref,home', 'foo@baz.net') self.assertDictEqual(wrapper.emails, {'home': ['foo@bar.net'], 'home, pref': ['foo@baz.net']}) def test_adding_a_simple_address(self): wrapper = TestVCardWrapper() components = ('box', 'extended', 'street', 'code', 'city', 'region', 'country') wrapper._add_post_address('home', *components) expected = {item: item for item in components} self.assertDictEqual(wrapper.post_addresses, {'home': [expected]}) def test_adding_a_custom_type_address(self): wrapper = TestVCardWrapper() components = ('box', 'extended', 'street', 'code', 'city', 'region', 'country') wrapper._add_post_address('custom_type', *components) expected = {item: item for item in components} self.assertDictEqual(wrapper.post_addresses, {'custom_type': [expected]}) def test_adding_multible_addresses(self): wrapper = TestVCardWrapper() components = ('box', 'extended', 'street', 'code', 'city', 'region', 'country') wrapper._add_post_address('work', *['work ' + c for c in components]) wrapper._add_post_address('home', *['home1 ' + c for c in components]) wrapper._add_post_address('home', *['home2 ' + c for c in components]) expected_work = {item: 'work ' + item for item in components} expected_home2 = {item: 'home2 ' + item for item in components} expected_home1 = {item: 'home1 ' + item for item in components} self.assertDictEqual(wrapper.post_addresses, # The lists are sorted! {'home': [expected_home1, expected_home2], 'work': [expected_work]}) def test_adding_preferred_address(self): wrapper = TestVCardWrapper() components = ('box', 'extended', 'street', 'code', 'city', 'region', 'country') wrapper._add_post_address('home', *['home1 ' + c for c in components]) wrapper._add_post_address('pref,home', *['home2 ' + c for c in components]) expected_work = {item: 'work ' + item for item in components} expected_home2 = {item: 'home2 ' + item for item in components} expected_home1 = {item: 'home1 ' + item for item in components} self.assertDictEqual( wrapper.post_addresses, {'home': [expected_home1], 'home, pref': [expected_home2]}) class OtherProperties(unittest.TestCase): def test_setting_and_getting_organisations(self): # also test that organisations are returned in sorted order wrapper = TestVCardWrapper() org1 = ["Org", "Sub1", "Sub2"] org2 = ["Org2", "Sub3"] org3 = ["Foo", "Bar", "Baz"] wrapper._add_organisation(org1) wrapper._add_organisation(org2) wrapper._add_organisation(org3) self.assertListEqual(wrapper.organisations, [org3, org1, org2]) def test_setting_org_in_different_ways_for_refactoring(self): wrapper1 = TestVCardWrapper() wrapper2 = TestVCardWrapper() wrapper1._add_organisation('foo') wrapper2._add_organisation(['foo']) self.assertEqual(wrapper1.organisations, wrapper2.organisations) def test_setting_and_getting_titles(self): wrapper = TestVCardWrapper() wrapper._add_title('Foo') wrapper._add_title('Bar') self.assertListEqual(wrapper.titles, ['Bar', 'Foo']) def test_setting_and_getting_roles(self): wrapper = TestVCardWrapper() wrapper._add_role('Foo') wrapper._add_role('Bar') self.assertListEqual(wrapper.roles, ['Bar', 'Foo']) def test_setting_and_getting_nicks(self): wrapper = TestVCardWrapper() wrapper._add_nickname('Foo') wrapper._add_nickname('Bar') self.assertListEqual(wrapper.nicknames, ['Bar', 'Foo']) def test_setting_and_getting_notes(self): wrapper = TestVCardWrapper() wrapper._add_note('First long note') wrapper._add_note('Second long note\nwith newline') self.assertListEqual(wrapper.notes, ['First long note', 'Second long note\nwith newline']) def test_setting_and_getting_webpages(self): wrapper = TestVCardWrapper() wrapper._add_webpage('https://github.com/scheibler/khard') wrapper._add_webpage('http://example.com') self.assertListEqual(wrapper.webpages, ['http://example.com', 'https://github.com/scheibler/khard']) def test_setting_and_getting_categories(self): wrapper = TestVCardWrapper() wrapper._add_category(["rfc", "address book"]) wrapper._add_category(["coding", "open source"]) self.assertListEqual(wrapper.categories, [["coding", "open source"], ["rfc", "address book"]]) class ABLabels(unittest.TestCase): def test_setting_and_getting_webpage_ablabel(self): wrapper = TestVCardWrapper() wrapper._add_webpage({'github': 'https://github.com/scheibler/khard'}) wrapper._add_webpage('http://example.com') self.assertListEqual(wrapper.webpages, [ 'http://example.com', {'github': 'https://github.com/scheibler/khard'}]) def test_labels_on_structured_values(self): vcard = VCardWrapper(_from_file('test/fixture/vcards/labels.vcf')) self.assertListEqual(vcard.organisations, [{'Work': ['Test Inc']}]) def test_setting_fn_from_labelled_org(self): wrapper = TestVCardWrapper() wrapper._delete_vcard_object("FN") wrapper._add_organisation({'Work': ['Test Inc']}) self.assertEqual(wrapper.formatted_name, 'Test Inc') khard-0.17.0/test/test_yaml.py000066400000000000000000000203031371517016500162260ustar00rootroot00000000000000"""Tests for the custom YAML format.""" # pylint: disable=missing-docstring import datetime from io import StringIO import unittest from unittest import mock import copy from ruamel.yaml import YAML from khard.carddav_object import CarddavObject import khard.helpers from .helpers import TestYAMLEditable as create_test_card def to_yaml(data): if 'First name' not in data: data['First name'] = 'Nobody' stream = StringIO() YAML().dump(data, stream) return stream.getvalue() def parse_yaml(yaml=''): """Parse some yaml string into a CarddavObject :param yaml: the yaml input string to parse :type yaml: str :returns: the parsed CarddavObject :rtype: CarddavObject """ return CarddavObject.from_yaml(address_book=mock.Mock(path='foo-path'), yaml=yaml, supported_private_objects=[], version='3.0', localize_dates=False) class EmptyFieldsAndSpaces(unittest.TestCase): def test_empty_birthday_in_yaml_input(self): empty_birthday = "First name: foo\nBirthday:" x = parse_yaml(empty_birthday) self.assertIsNone(x.birthday) def test_only_spaces_in_birthday_in_yaml_input(self): spaces_birthday = "First name: foo\nBirthday: " x = parse_yaml(spaces_birthday) self.assertIsNone(x.birthday) def test_empty_anniversary_in_yaml_input(self): empty_anniversary = "First name: foo\nAnniversary:" x = parse_yaml(empty_anniversary) self.assertIsNone(x.anniversary) def test_empty_organisation_in_yaml_input(self): empty_organisation = "First name: foo\nOrganisation:" x = parse_yaml(empty_organisation) self.assertListEqual(x.organisations, []) def test_empty_nickname_in_yaml_input(self): empty_nickname = "First name: foo\nNickname:" x = parse_yaml(empty_nickname) self.assertListEqual(x.nicknames, []) def test_empty_role_in_yaml_input(self): empty_role = "First name: foo\nRole:" x = parse_yaml(empty_role) self.assertListEqual(x.roles, []) def test_empty_title_in_yaml_input(self): empty_title = "First name: foo\nTitle:" x = parse_yaml(empty_title) self.assertListEqual(x.titles, []) def test_empty_categories_in_yaml_input(self): empty_categories = "First name: foo\nCategories:" x = parse_yaml(empty_categories) self.assertListEqual(x.categories, []) def test_empty_webpage_in_yaml_input(self): empty_webpage = "First name: foo\nWebpage:" x = parse_yaml(empty_webpage) self.assertListEqual(x.webpages, []) def test_empty_note_in_yaml_input(self): empty_note = "First name: foo\nNote:" x = parse_yaml(empty_note) self.assertListEqual(x.notes, []) class yaml_ablabel(unittest.TestCase): def test_ablabelled_url_in_yaml_input(self): ablabel_url = "First name: foo\nWebpage:\n - http://example.com\n" \ " - github: https://github.com/scheibler/khard" x = parse_yaml(ablabel_url) self.assertListEqual(x.webpages, [ 'http://example.com', {'github': 'https://github.com/scheibler/khard'}]) class UpdateVcardWithYamlUserInput(unittest.TestCase): _date = datetime.datetime(2000, 1, 1) _datetime = datetime.datetime(2013, 4, 2, 13, 14, 15) _no_year = datetime.datetime(1900, 1, 1) def test_update_org_simple(self): card = create_test_card() data = {'Organisation': 'Foo'} data = to_yaml(data) card.update(data) self.assertListEqual(card.organisations, [['Foo']]) def test_update_org_multi(self): card = create_test_card() orgs = ['foo', 'bar', 'baz'] data = {'Organisation': orgs} data = to_yaml(data) card.update(data) self.assertListEqual(card.organisations, sorted([[x] for x in orgs])) def test_update_org_complex(self): card = create_test_card() org = ['org.', 'dep.', 'office'] data = {'Organisation': [org]} data = to_yaml(data) card.update(data) self.assertListEqual(card.organisations, [org]) def test_update_categories_simple(self): card = create_test_card() data = {'Categories': 'foo'} data = to_yaml(data) card.update(data) self.assertListEqual(card.categories, ['foo']) def test_update_categories_multi(self): card = create_test_card() cat = ['foo', 'bar', 'baz'] data = {'Categories': cat} data = to_yaml(data) card.update(data) self.assertListEqual(card.categories, cat) def test_update_bday_date(self): card = create_test_card() data = {'Birthday': '2000-01-01'} data = to_yaml(data) card.update(data) self.assertEqual(card.birthday, self._date) def test_update_bday_without_year(self): card = create_test_card(version="4.0") data = {'Birthday': '--01-01'} data = to_yaml(data) card.update(data) self.assertEqual(card.birthday, self._no_year) def test_update_bday_with_text(self): card = create_test_card(version="4.0") data = {'Birthday': 'text= some day maybe'} data = to_yaml(data) card.update(data) self.assertEqual(card.birthday, 'some day maybe') def test_update_bday_with_date_and_time(self): card = create_test_card() data = {'Birthday': '2013-04-02T13:14:15'} data = to_yaml(data) card.update(data) self.assertEqual(card.birthday, self._datetime) def test_update_anniverary(self): card = create_test_card() data = {'Anniversary': '2000-01-01'} data = to_yaml(data) card.update(data) self.assertEqual(card.anniversary, self._date) def test_update_anniversary_without_year(self): card = create_test_card(version="4.0") data = {'Anniversary': '--01-01'} data = to_yaml(data) card.update(data) self.assertEqual(card.anniversary, self._no_year) def test_update_anniversary_with_text(self): card = create_test_card(version="4.0") data = {'Anniversary': 'text= some day maybe'} data = to_yaml(data) card.update(data) self.assertEqual(card.anniversary, 'some day maybe') def test_update_anniversary_with_date_and_time(self): card = create_test_card() data = {'Anniversary': '2013-04-02T13:14:15'} data = to_yaml(data) card.update(data) self.assertEqual(card.anniversary, self._datetime) def test_update_name_simple(self): card = create_test_card() data = {'First name': 'first', 'Last name': 'last'} data = to_yaml(data) card.update(data) self.assertEqual(card.get_first_name_last_name(), 'first last') def test_update_fn(self): card = create_test_card() fn = 'me myself and i' data = {'Formatted name': fn} data = to_yaml(data) card.update(data) self.assertEqual(card.formatted_name, fn) def test_parse_field(self): """Test round-trip of a field to/from YAML""" card = create_test_card() data = "First name: Nobody\n" data += "\n".join(khard.helpers.convert_to_yaml("Note", "foobar", 0, 5, True)) card.update(data) self.assertListEqual(card.notes, ["foobar"]) def test_parse_field_with_colon(self): """Test round-trip of a field containing ': ' to/from YAML""" card = create_test_card() data = "First name: Nobody\n" data += "\n".join(khard.helpers.convert_to_yaml("Note", "foo: bar", 0, 5, True)) card.update(data) self.assertListEqual(card.notes, ["foo: bar"]) def test_vcard_round_trip(self): """Test a VCARD can be converted to YAML and back unchanged""" card = create_test_card() card._add_organisation("ACME, Inc") card._add_note("foo: bar") card2 = copy.copy(card) yaml = card.to_yaml() card.update(yaml) self.assertEqual(card.vcard.serialize(), card2.vcard.serialize()) khard-0.17.0/todo.txt000066400000000000000000000004221371517016500144020ustar00rootroot00000000000000ToDo list for khard 1. Add support for vcard attributes kind and member - kind column in contact table - option to filter contact table (--kind) - member action to list all members of an organisation 2. Implement impp attribute, see #105 for more information