pax_global_header00006660000000000000000000000064145346326370014527gustar00rootroot0000000000000052 comment=3bce29b908239f5fb40ca0794582aaaf21e07079 sidekiq-cron-sidekiq-cron-31b9d88/000077500000000000000000000000001453463263700170515ustar00rootroot00000000000000sidekiq-cron-sidekiq-cron-31b9d88/.github/000077500000000000000000000000001453463263700204115ustar00rootroot00000000000000sidekiq-cron-sidekiq-cron-31b9d88/.github/stale.yml000066400000000000000000000012541453463263700222460ustar00rootroot00000000000000# Number of days of inactivity before an issue becomes stale daysUntilStale: 60 # Number of days of inactivity before a stale issue is closed daysUntilClose: 7 # Issues with these labels will never be considered stale exemptLabels: - pinned - security # Label to use when marking an issue as stale staleLabel: wontfix # Comment to post when marking an issue as stale. Set to `false` to disable markComment: > This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. # Comment to post when closing a stale issue. Set to `false` to disable closeComment: false sidekiq-cron-sidekiq-cron-31b9d88/.github/workflows/000077500000000000000000000000001453463263700224465ustar00rootroot00000000000000sidekiq-cron-sidekiq-cron-31b9d88/.github/workflows/ci.yml000066400000000000000000000015371453463263700235720ustar00rootroot00000000000000name: CI on: [push, pull_request] jobs: test: runs-on: ubuntu-latest strategy: fail-fast: false matrix: ruby: ["2.7", "3.0", "3.1", "3.2", "ruby-head"] sidekiq: ["~> 6", "~> 7"] services: redis: image: redis options: >- --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 ports: - 6379:6379 env: SIDEKIQ_VERSION: "${{ matrix.sidekiq }}" steps: - uses: actions/checkout@v3 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: bundler-cache: true # 'bundle install' and cache gems ruby-version: ${{ matrix.ruby }} - name: Run tests run: bundle exec rake test - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v3 sidekiq-cron-sidekiq-cron-31b9d88/.gitignore000066400000000000000000000001731453463263700210420ustar00rootroot00000000000000.rvmrc .ruby-version tags Gemfile.lock *.swp dump.rdb .rbx coverage/ vendor/ .bundle/ .sass-cache/ Guardfile pkg .DS_Store sidekiq-cron-sidekiq-cron-31b9d88/CHANGELOG.md000066400000000000000000000163721453463263700206730ustar00rootroot00000000000000# Changelog All notable changes to this project will be documented in this file. ## 1.12.0 - Remove Sidekiq.server? check from schedule loader (https://github.com/sidekiq-cron/sidekiq-cron/pull/436) - Parse arguments on `args=` method (https://github.com/sidekiq-cron/sidekiq-cron/pull/442) - Only check out a Redis connection if necessary (https://github.com/sidekiq-cron/sidekiq-cron/pull/438) ## 1.11.0 - Differentiates b/w "schedule" vs "dynamic" jobs (https://github.com/sidekiq-cron/sidekiq-cron/pull/431) - Clears scheduled jobs upon schedule load (https://github.com/sidekiq-cron/sidekiq-cron/pull/431) - Reduce gem size by excluding test files (https://github.com/sidekiq-cron/sidekiq-cron/pull/414) ## 1.10.1 - Use `hset` instead of deprecated `hmset` (https://github.com/sidekiq-cron/sidekiq-cron/pull/410) ## 1.10.0 - Remove EOL Ruby 2.6 support (https://github.com/sidekiq-cron/sidekiq-cron/pull/399) - Add a logo for the project! (https://github.com/sidekiq-cron/sidekiq-cron/pull/402) - Added support for ActiveRecord serialize/deserialize using GlobalID (https://github.com/sidekiq-cron/sidekiq-cron/pull/395) - Allow for keyword args (`embedded: true`) in Poller (https://github.com/sidekiq-cron/sidekiq-cron/pull/398) - Make last_enqueue_time be always an instance of Time (https://github.com/sidekiq-cron/sidekiq-cron/pull/354) - Fix argument error problem update from 1.6.0 to newer (https://github.com/sidekiq-cron/sidekiq-cron/pull/392) - Clear old jobs while loading the jobs from schedule via the schedule loader (https://github.com/sidekiq-cron/sidekiq-cron/pull/405) ## 1.9.1 - Always enqueue via Active Job interface when defined in cron job config (https://github.com/sidekiq-cron/sidekiq-cron/pull/381) - Fix schedule.yml YAML load errors on Ruby 3.1 (https://github.com/sidekiq-cron/sidekiq-cron/pull/386) - Require Fugit v1.8 to refactor internals (https://github.com/sidekiq-cron/sidekiq-cron/pull/385) ## 1.9.0 - Sidekiq v7 support (https://github.com/sidekiq-cron/sidekiq-cron/pull/369) - Add support for ERB templates in the auto schedule loader (https://github.com/sidekiq-cron/sidekiq-cron/pull/373) ## 1.8.0 - Fix deprecation warnings with redis-rb v4.8.0 (https://github.com/sidekiq-cron/sidekiq-cron/pull/356) - Fix poller affecting Sidekiq scheduled set poller (https://github.com/sidekiq-cron/sidekiq-cron/pull/359) - Fix default polling interval (https://github.com/sidekiq-cron/sidekiq-cron/pull/362) - Add italian locale (https://github.com/sidekiq-cron/sidekiq-cron/pull/367) - Allow disabling of cron polling (https://github.com/sidekiq-cron/sidekiq-cron/pull/368) ## 1.7.0 - Enable to use cron notation in natural language (ie `every 30 minutes`) (https://github.com/sidekiq-cron/sidekiq-cron/pull/312) - Fix `date_as_argument` feature to add timestamp argument at every cron job execution (https://github.com/sidekiq-cron/sidekiq-cron/pull/329) - Introduce `Sidekiq::Options` to centralize reading/writing options from different Sidekiq versions (https://github.com/sidekiq-cron/sidekiq-cron/pull/341) - Make auto schedule loading compatible with Array format (https://github.com/sidekiq-cron/sidekiq-cron/pull/345) ## 1.6.0 - Adds support for auto-loading the `config/schedule.yml` file (https://github.com/sidekiq-cron/sidekiq-cron/pull/337) - Fix `Sidekiq.options` deprecation warning (https://github.com/sidekiq-cron/sidekiq-cron/pull/338) ## 1.5.1 - Fixes an issue that prevented the gem to work in previous Sidekiq versions (https://github.com/sidekiq-cron/sidekiq-cron/pull/335) ## 1.5.0 - Integrate Sidekiq v6.5 breaking changes (https://github.com/sidekiq-cron/sidekiq-cron/pull/331) - Add portuguese translations (https://github.com/sidekiq-cron/sidekiq-cron/pull/332) ## 1.4.0 - Fix buttons order in job show view (https://github.com/sidekiq-cron/sidekiq-cron/pull/302) - Dark Mode support in UI (https://github.com/sidekiq-cron/sidekiq-cron/pull/282) - Remove invocation of deprecated Redis functionality (https://github.com/sidekiq-cron/sidekiq-cron/pull/318) - Internal code cleanup (https://github.com/sidekiq-cron/sidekiq-cron/pull/317) - Optimize gem size (https://github.com/sidekiq-cron/sidekiq-cron/pull/322) - Fix "Show All" button on cron jobs view with Sidekiq 6.3.0+ (https://github.com/sidekiq-cron/sidekiq-cron/pull/321) - Documentation updates ## 1.3.0 - Add confirmation dialog when enquing jobs from UI - Start to support Sidekiq `average_scheduled_poll_interval` option (replaced `poll_interval`) - Fix deprecation warning for Redis 4.6.x - Fix different response from Redis#exists in different Redis versions - All PRs: - https://github.com/sidekiq-cron/sidekiq-cron/pull/275 - https://github.com/sidekiq-cron/sidekiq-cron/pull/287 - https://github.com/sidekiq-cron/sidekiq-cron/pull/309 - https://github.com/sidekiq-cron/sidekiq-cron/pull/299 - https://github.com/sidekiq-cron/sidekiq-cron/pull/314 - https://github.com/sidekiq-cron/sidekiq-cron/pull/288 ## 1.2.0 - Updated readme - Fix problem with Sidekiq::Launcher and requiring it when not needed - Better patching of Sidekiq::Launcher - Fixed Dockerfile ## 1.1.0 - Updated readme - Fix unit tests - changed argument error when getting invalid cron format - When fallbacking old job enqueued time use `Time.parse` without format (so Ruby can decide best method to parse it) - Add option `date_as_argument` which will add to your job arguments on last place `Time.now.to_f` when it was eneuqued - Add option `description` which will allow you to add notes to your jobs so in web view you can see it - Fixed translations ## 1.0.4 - Fix problem with upgrading to 1.0.x - parsing last enqued time didn't count with old time format stored in Redis ## 1.0.0 - Use [fugit](https://github.com/floraison/fugit) instead of [rufus-scheduler](https://github.com/jmettraux/rufus-scheduler) - API of cron didn't change (rufus scheduler is using fugit) - Better working with Timezones - Translations for JA, zh-CN - Cron without timezone are considered as UTC, to add Timezone to cron use format `* * * * * Europe/Berlin` - Be aware that this release can change when your jobs are enqueued (for me it didn't change but it is in one project, in other it can shift by different timezone setup) ## 0.6.0 - Set poller to check jobs every 30s by default (possible to override by `Sidekiq.options[:poll_interval] = 10`) - Add group actions (enqueue, enable, disable, delete) all in web view - Fix poller to enqueu all jobs in poll start time - Add performance test for enqueue of jobs (10 000 jobs in less than 19s) - Fix problem with default queue - Remove `redis-namespace` from dependencies - Update Ruby versions in Travis ## 0.5.0 - Add Docker support - All crons are now evaluated in UTC - Fix rufus scheduler & timezones problems - Add support for Sidekiq 4.2.1 - Fix readme - Add Russian locale - User Rack.env in tests - Faster enqueue of jobs - Permit to use `ActiveJob::Base.queue_name_delimiter` - Fix problem with multiple times enqueue #84 - Fix problem with enqueue of unknown class ## 0.4.0 - Enable to work with Sidekiq >= 4.0.0 - Fix readme ## 0.3.1 - Add CSRF tags to forms so it will work with Sidekiq >= 3.4.2 - Remove Tilt dependency ## 0.3.0 - Suport for Active Job - Sidekiq cron web ui needs to be loaded by: require 'sidekiq/cron/web' - Add load_from_hash! and load_from_array! which cleanup jobs before adding new ones sidekiq-cron-sidekiq-cron-31b9d88/Gemfile000066400000000000000000000002011453463263700203350ustar00rootroot00000000000000source 'https://rubygems.org' gemspec # To test different Sidekiq versions gem "sidekiq", ENV.fetch("SIDEKIQ_VERSION", ">= 6") sidekiq-cron-sidekiq-cron-31b9d88/LICENSE.txt000066400000000000000000000020411453463263700206710ustar00rootroot00000000000000Copyright (c) 2013 Ondrej Bartas Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. sidekiq-cron-sidekiq-cron-31b9d88/README.md000066400000000000000000000257751453463263700203500ustar00rootroot00000000000000![Sidekiq-Cron](logos/cover.png) [![Gem Version](https://badge.fury.io/rb/sidekiq-cron.svg)](https://badge.fury.io/rb/sidekiq-cron) [![Build Status](https://github.com/sidekiq-cron/sidekiq-cron/workflows/CI/badge.svg?branch=master)](https://github.com/sidekiq-cron/sidekiq-cron/actions) [![codecov](https://codecov.io/gh/sidekiq-cron/sidekiq-cron/branch/master/graph/badge.svg?token=VK9IVLIaY8)](https://codecov.io/gh/sidekiq-cron/sidekiq-cron) > A scheduling add-on for [Sidekiq](https://sidekiq.org/) 🎬 [Introduction video about Sidekiq-Cron by Drifting Ruby](https://www.driftingruby.com/episodes/periodic-tasks-with-sidekiq-cron) Sidekiq-Cron runs a thread alongside Sidekiq workers to schedule jobs at specified times (using cron notation `* * * * *` parsed by [Fugit](https://github.com/floraison/fugit)). Checks for new jobs to schedule every 30 seconds and doesn't schedule the same job multiple times when more than one Sidekiq worker is running. Scheduling jobs are added only when at least one Sidekiq process is running, but it is safe to use Sidekiq-Cron in environments where multiple Sidekiq processes or nodes are running. If you want to know how scheduling work, check out [under the hood](#under-the-hood). Works with ActiveJob (Rails 4.2+). You don't need Sidekiq PRO, you can use this gem with plain Sidekiq. ## Changelog Before upgrading to a new version, please read our [Changelog](CHANGELOG.md). ## Installation Install the gem: ``` $ gem install sidekiq-cron ``` Or add to your `Gemfile` and run `bundle install`: ```ruby gem "sidekiq-cron" ``` **NOTE** If you are not using Rails, you need to add `require 'sidekiq-cron'` somewhere after `require 'sidekiq'`. ## Getting Started ### Job properties ```ruby { # MANDATORY 'name' => 'name_of_job', # must be uniq! 'cron' => '1 * * * *', # execute at 1 minute of every hour, ex: 12:01, 13:01, 14:01, ... 'class' => 'MyClass', # OPTIONAL 'source' => 'dynamic', # source of the job, `schedule`/`dynamic` (default: `dynamic`) 'queue' => 'name of queue', 'args' => '[Array or Hash] of arguments which will be passed to perform method', 'date_as_argument' => true, # add the time of execution as last argument of the perform method 'active_job' => true, # enqueue job through Rails 4.2+ Active Job interface 'queue_name_prefix' => 'prefix', # Rails 4.2+ Active Job queue with prefix 'queue_name_delimiter' => '.', # Rails 4.2+ Active Job queue with custom delimiter (default: '_') 'description' => 'A sentence describing what work this job performs' 'status' => 'disabled' # default: enabled } ``` ### Time, cron and Sidekiq-Cron For testing your cron notation you can use [crontab.guru](https://crontab.guru). Sidekiq-Cron uses [Fugit](https://github.com/floraison/fugit) to parse the cronline. So please, check Fugit documentation for further information about allowed formats. If using Rails, this is evaluated against the timezone configured in Rails, otherwise the default is UTC. If you want to have your jobs enqueued based on a different time zone you can specify a timezone in the cronline, like this `'0 22 * * 1-5 America/Chicago'`. #### Natural-language formats Since sidekiq-cron `v1.7.0`, you can use the natural-language formats supported by Fugit, such as: ```rb "every day at five" # => '0 5 * * *' "every 3 hours" # => '0 */3 * * *' ``` See [the relevant part of Fugit documentation](https://github.com/floraison/fugit#fugitnat) for details. #### Second-precision (sub-minute) cronlines In addition to the standard 5-parameter cronline format, sidekiq-cron supports scheduling jobs with second-precision using a modified 6-parameter cronline format: `Seconds Minutes Hours Days Months DayOfWeek` For example: `"*/30 * * * * *"` would schedule a job to run every 30 seconds. Note that if you plan to schedule jobs with second precision you may need to override the default schedule poll interval so it is lower than the interval of your jobs: ```ruby Sidekiq::Options[:cron_poll_interval] = 10 ``` The default value at time of writing is 30 seconds. See [under the hood](#under-the-hood) for more details. ### What objects/classes can be scheduled #### Sidekiq Worker In this example, we are using `HardWorker` which looks like: ```ruby class HardWorker include Sidekiq::Worker def perform(*args) # do something end end ``` #### Active Job Worker You can schedule `ExampleJob` which looks like: ```ruby class ExampleJob < ActiveJob::Base queue_as :default def perform(*args) # Do something end end ``` For Active jobs you can use `symbolize_args: true` in `Sidekiq::Cron::Job.create` or in Hash configuration, which will ensure that arguments you are passing to it will be symbolized when passed back to `perform` method in worker. #### Adding Cron job Refer to [Schedule vs Dynamic jobs](#schedule-vs-dynamic-jobs) to understand the difference. ```ruby class HardWorker include Sidekiq::Worker def perform(name, count) # do something end end Sidekiq::Cron::Job.create(name: 'Hard worker - every 5min', cron: '*/5 * * * *', class: 'HardWorker') # execute at every 5 minutes # => true ``` `create` method will return only true/false if job was saved or not. ```ruby job = Sidekiq::Cron::Job.new(name: 'Hard worker - every 5min', cron: '*/5 * * * *', class: 'HardWorker') if job.valid? job.save else puts job.errors end # or simple unless job.save puts job.errors # will return array of errors end ``` Use ActiveRecord models as arguments ```rb class Person < ApplicationRecord end class HardWorker < ActiveJob::Base queue_as :default def perform(person) puts "person: #{person}" end end person = Person.create(id: 1) Sidekiq::Cron::Job.create(name: 'Hard worker - every 5min', cron: '*/5 * * * *', class: 'HardWorker', args: person) # => true ``` Load more jobs from hash: ```ruby hash = { 'name_of_job' => { 'class' => 'MyClass', 'cron' => '1 * * * *', 'args' => '(OPTIONAL) [Array or Hash]' }, 'My super iber cool job' => { 'class' => 'SecondClass', 'cron' => '*/5 * * * *' } } Sidekiq::Cron::Job.load_from_hash hash ``` Load more jobs from array: ```ruby array = [ { 'name' => 'name_of_job', 'class' => 'MyClass', 'cron' => '1 * * * *', 'args' => '(OPTIONAL) [Array or Hash]' }, { 'name' => 'Cool Job for Second Class', 'class' => 'SecondClass', 'cron' => '*/5 * * * *' } ] Sidekiq::Cron::Job.load_from_array array ``` Bang-suffixed methods will remove jobs where source is `schedule` and are not present in the given hash/array, update jobs that have the same names, and create new ones when the names are previously unknown. ```ruby Sidekiq::Cron::Job.load_from_hash! hash Sidekiq::Cron::Job.load_from_array! array ``` Or from YAML (same notation as Resque-scheduler): ```yaml # config/schedule.yml my_first_job: cron: "*/5 * * * *" class: "HardWorker" queue: hard_worker second_job: cron: "*/30 * * * *" # execute at every 30 minutes class: "HardWorker" queue: hard_worker_long args: hard: "stuff" ``` There are multiple ways to load the jobs from a YAML file 1. The gem will automatically load the jobs mentioned in `config/schedule.yml` file (it supports ERB) 2. When you want to load jobs from a different filename, mention the filename in sidekiq configuration, i.e. `cron_schedule_file: "config/users_schedule.yml"` 3. Load the file manually as follows: ```ruby # config/initializers/sidekiq.rb Sidekiq.configure_server do |config| config.on(:startup) do schedule_file = "config/users_schedule.yml" if File.exist?(schedule_file) schedule = YAML.load_file(schedule_file) Sidekiq::Cron::Job.load_from_hash!(schedule, source: "schedule") end end end ``` ### Finding jobs ```ruby # return array of all jobs Sidekiq::Cron::Job.all # return one job by its unique name - case sensitive Sidekiq::Cron::Job.find "Job Name" # return one job by its unique name - you can use hash with 'name' key Sidekiq::Cron::Job.find name: "Job Name" # if job can't be found nil is returned ``` ### Destroy jobs ```ruby # destroy all jobs Sidekiq::Cron::Job.destroy_all! # destroy job by its name Sidekiq::Cron::Job.destroy "Job Name" # destroy found job Sidekiq::Cron::Job.find('Job name').destroy ``` ### Work with job ```ruby job = Sidekiq::Cron::Job.find('Job name') # disable cron scheduling job.disable! # enable cron scheduling job.enable! # get status of job: job.status # => enabled/disabled # enqueue job right now! job.enque! ``` ### Schedule vs Dynamic jobs There are two potential job sources: `schedule` and `dynamic`. Jobs associated with schedule files are labeled as `schedule` as their source, whereas jobs created at runtime without the `source=schedule` argument are classified as `dynamic`. The key distinction lies in how these jobs are managed. When a schedule is loaded, any stale `schedule` jobs are automatically removed to ensure synchronization within the schedule. The `dynamic` jobs remain unaffected by this process. ### How to start scheduling? Just start Sidekiq workers by running: ``` $ bundle exec sidekiq ``` ### Web UI for Cron Jobs If you are using Sidekiq's web UI and you would like to add cron jobs too to this web UI, add `require 'sidekiq/cron/web'` after `require 'sidekiq/web'`. With this, you will get: ![Web UI](docs/images/web-cron-ui.jpeg) ## Under the hood When you start the Sidekiq process, it starts one thread with `Sidekiq::Poller` instance, which perform the adding of scheduled jobs to queues, retries etc. Sidekiq-Cron adds itself into this start procedure and starts another thread with `Sidekiq::Cron::Poller` which checks all enabled Sidekiq cron jobs every 30 seconds, if they should be added to queue (their cronline matches time of check). Sidekiq-Cron is checking jobs to be enqueued every 30s by default, you can change it by setting: ```ruby Sidekiq::Options[:cron_poll_interval] = 10 ``` Sidekiq-Cron is safe to use with multiple Sidekiq processes or nodes. It uses a Redis sorted set to determine that only the first process who asks can enqueue scheduled jobs into the queue. When running with many Sidekiq processes, the polling can add significant load to Redis. You can disable polling on some processes by setting `Sidekiq::Options[:cron_poll_interval] = 0` on these processes. ## Contributing **Thanks to all [contributors](https://github.com/sidekiq-cron/sidekiq-cron/graphs/contributors), you’re awesome and this wouldn’t be possible without you!** * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet. * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it. * Fork the project. * Start a feature/bugfix branch. * Commit and push until you are happy with your contribution. * Make sure to add tests for it. This is important so we don't break it in a future version unintentionally. * Open a pull request! ### Testing You can execute the test suite by running: ``` $ bundle exec rake test ``` ## License Copyright (c) 2013 Ondrej Bartas. See [LICENSE](LICENSE.txt) for further details. sidekiq-cron-sidekiq-cron-31b9d88/Rakefile000066400000000000000000000010541453463263700205160ustar00rootroot00000000000000require 'bundler/gem_tasks' require 'rake/testtask' task :default => :test Rake::TestTask.new(:test) do |t| t.test_files = FileList['test/unit/**/*_test.rb', 'test/integration/**/*_test.rb'] t.warning = false t.verbose = false end namespace :test do Rake::TestTask.new(:unit) do |t| t.test_files = FileList['test/unit/**/*_test.rb'] t.warning = false t.verbose = false end Rake::TestTask.new(:integration) do |t| t.test_files = FileList['test/integration/**/*_test.rb'] t.warning = false t.verbose = false end end sidekiq-cron-sidekiq-cron-31b9d88/docs/000077500000000000000000000000001453463263700200015ustar00rootroot00000000000000sidekiq-cron-sidekiq-cron-31b9d88/docs/images/000077500000000000000000000000001453463263700212465ustar00rootroot00000000000000sidekiq-cron-sidekiq-cron-31b9d88/docs/images/web-cron-ui.jpeg000066400000000000000000005274301453463263700242570ustar00rootroot00000000000000PNG  IHDR0:|iCCPICC Profile(c``*I,(aa``+) rwRR` ` \\À|/JyӦ|6rV%:wJjq2#R d:E%@ [>dd! vV dKIa[)@.ERבI90;@œ r0x00(030X228V:Teg(8C6U9?$HG3/YOGg?Mg;_`܃K})<~km gB_fla810$@$wikiTXtXML:com.adobe.xmp 1214 484 \m@IDATx sŹ. r"r%A5H 1^DM$F߈ADP9aCe9v?OuL찬!<ŇoW񛧞x($! B@teNzNd5nt"'B@! @3唅B@! B@! "|YNQ! B@! g"ī.,B@! B@3_gESB@! B@! H@39 ! B@! B pB@! B@!p&'-,B@! B@&F GUѼ-zE%ڬ6XmVq)"|;3C! B@!  XDP"|% BnG'p9]JDɮB@! B@! `E}a^AUz-تIqx\ =hܸ1nwZ~%S! B@! ?[{^Gf/3 Dm NCP6PB_nӲ_!B@! B@اo<–^ʺeQ3gv0;'6WWAfu+RB@! B@!p`^^`j-a]h]s:9Uhꈮ#B@! B%>9ʲv uyec X,߾={PVY RB@! B@! c%"jAXIݵYžhu_-QB@! B@! H38mB:jXpuN𵥬 mAϋ[dc"B@! B@ėӮf2qecP}c՗xbGüOb%6x@`B@! B@3,ȶvkʡIߗ~Džiɺڏ}^fnr>,OB@! B@!p`eY;o#,|9AP`נ!`N_a5gc=|\檒: .<5ԙ}|iGTL(:4KYJ?LSܩ#^l\_NhUׅװ< Aհk#{5(S8{goTga2T=.g #q)vދ1W!cl)gFR CzQ{Mª_hy Fg{ДL/Oe"䇟sP}7.%9¼Qu=۬"kUvTغw T ]ilbr nz鄯X6 #*ag{*!CouQ©T{ S|{i1unL}k*:#;dM!QR] 9aXF.Z~}(yVb¬p(j_5$oY+2)6kvJ\h1 Q̲EgokV^8# 9 3? 6 iȌ5tLasR +&@R޼ H.^ t@0u]rt~҆錳nDsEx~"=tb_XhvGczMX#{z,i`q0:^p5.;@Sl_5fޗ>:^4#omd79fjx'+.i1.`b= \>4,>>X?76 Է$| _heFb˻}>} ⶙v=/Ǩ *.bO9E$_0cפּ5dB*Ln~B|0Ŝ?'ïHONɤ`sе>{C> _w7[161jl#P-fvv荛F@[pccbL -F:RΥzc#o40Z',E{1&LR㞘uR&O|F{oBS5~Y:&>5E 7C_ӶoJ?$IဇshmM[E4dTj+RI/, #6f]4Z~kjp/i0Ztj^69v.n ζ8d֌/û7_3MP?=9z_q=Fvى@?+g.#}&ӂixw2]]ޕ6g>9fa>R=z #{kYΧ\>w8CLc3測R繏OQHuHrƸf6a)5-0Qp[hspTΟl<\[qe&ȊB@!YGb5V KԴApvz}Cc6 }GP~lQ&݄($o v?:ۗаcW``w~{ouLX^H9,m&<ڂ>ݠKai4/3Iw|%ݚ(`Ywa>^~+Fx?ǐQ(:>ābcXf?YI{5D䋥8p׃(~bQ,[YMGc D/#ɇs̝%1/mΚ%CwʇȸBJWhG!SO3fApB>N6_L~wp_*J9!)h;i Qr8~IudX>wDWpfl'mX6UG-kʹ&c˂Ywa}  M1Ig'aц8p W53fV)mB+Sh TB^mhߡ=ۡx2X= cX ǐ sglV6Hxנ}OkRtZÍcҍbJر}fLVDGȒؗ#c`#4LXڜŁ)GYYKпj,/E>đl]&c=[EH(ƜwwN!zQuDq{ρ ]Vd}EI(E9uo#%Gg?9DRtvүKƽFe/^#J6 6$z^Cv6}ҽ[r${W"XrTw^{+*iYZҦ 1rlٷ3'O"ѫ-ڐ[qKǸg;O<:uX'O¬ECna,(NƤ$z5{PwNĴP`wbѻoc:tL &Ynsxgg#,[qt/T~C]e<?!B#g}rN̈́gL>>{uD7Єi3ENTօok.M2[OQ\}F BX:8sVD|Drh܊^0.B'OFϟ,r%/IE0>ʼ}+IrwvQ&o7vV玼,(v+9cQ-^,[Ӷh,>ǏZ㷂mE~g7$ыrݙȸtx8'[wk0՗hKyM|)k`hww>:ĭJ\'fMse$|3t\?V?p DWl,s=kw׻O1Z9Ϳ7z0;qo\7Ogl9z|qŝ8^KfnrZ?~z|2CՇ{)9'Ч bˏ¯ߗc=/$jG޽FڽcqdRّ xc7, Wwi߯Q jr՟ȃ&x9ѧpQCy^pY0͚NqY4)tUCCJ6h{?FV[IasaS%K5#|o{C\h<#1u9Yr4CkO?1dk_kvn,ax˰׺ Ё}kO6| mQW| Ed&9#ȭ|):7&LV.H6*aorE5Ms`5@A W ֆ?/^!iӴOւObAapԷҜײd 7)p":xwN|=E>¸Pci5շ=U{pK[08*¡n$Y:ttwv߫#>"l ݡb0+F6_`趾X|_C$%V]p3V5'5{ &zqD1cql7/.VעÚ_o:lge}r< ❺#Ur~1B]-GØ9*cXキQzHVlWݿgEϋs^X;Nv7nE/l ܑ}L'Oqn=~ˋhH(r#p/iO^^I畎qQYm/EC KvbMXx>6"ҵexju~s?h1/Qrm'%7@֖}#$9"epVƯJ }O7j{u[9ЃduR[?S _7OUKDFCs=%>KeZ?0ly!ױTz<ъWͨcTB@!pFljx׼b`@h'?=K^c~̿^aW<51.viiN#vB|{|< a%sU7>,7w #hgx32򞸥;FGu4inoEm؂}=/x_r"g/h 9gKUDuʔo0 _?w*{C,Д+GPFDO '\M\F;M̓;(˃tFv({;^a7Exh4) g0fQJ͖{]2ݱ5_+u<4UK `˒*z+T*©OV e|A$b\p8҇fnøvQrV\E6,eHB_״}[ 䛚s\4LiFJE`xI:;>Z9ǪBvOn+nYqZG#mHIQÇS /5X+Vb?;? %{B,fowp rH +W'qqzzr_ /U>TEM$0ZSn6])ՎhxlD ECZ40D6Qhک}kVU ?a/fqxT{m.oڇQ⡋BkQqڧk{T)=nQ὘1]xݥ=s+7Es%qڐxw)#֫|i"i1ĩJYf&? R#mئ! <,:zK_,V*?X]s)\FSq+԰4\,+~Uc2~r˧ %;T>aq @Z14RD1SRqTfx>ji{g`'{qɗA/cu`6 )9`U|`YY4_.Sp-#KMӡ)-=O}ktj Y-pCo~SyeX7qX4!V 5V 89$|%'GY%Ȍz<ԤnU Y>|Wuv,S#0|B֤(pF A߻}!嚉 _֑>-Y5eQ& nZ;~ 粯^-HjPAYh|F[Cm|%Pm<|]+oo2Jԏ=9|GH3ZUM:^o&JiTxy> O?< (.WX9f| zSyEd_Ӷ_{d #l]隺Q]= ]5dّZCji/s(}(/cƛ  zoR4{fk->=%,ymf-M2vQۛqNp ptV!zrrUmhf՞cedS-Z m6i ݳU:J-6\V=ȶHM@ ׋ڽOr߳1ٮ*2u$4INuyUk7ЃLp :+sh;mR:-XJѹON=;ږB@! jF@y$͍=y[ Ndܲ(K mk"RB+ h|{q3%hR&gQ\,=,hxh;){c>~2Sga| >^ú^ckcZc  t!LRkJ?tuBu'Mvr3xo>.|qu> ߔwIsиOwd%b"TmI]`r't׍X2߂9c9,z}Xf#Rtf%Mi8O43G^r8S-# vQLcǍ=zxcXw׽4{;Ьs7ځ>WٱPtEZ7='\ޤP_+mC&{" Ӭsr/\@3RtN/}w ё՟`9:/ÛhknBVl] IƇڹkTЎ?c-SSR:;ӭGNgO&/_\ Wb* yؽDsAz fUDu_nv?ʣ'<c~ tqt7TIݔO w.ڑCڙxrYː;k2t6jrY6}D#9x)f/k BX@iގ10g6ō1 X9k*MBVۨ ^t{YA?,?/bXhB\tvg|JӯEf eCɆ(K}4eΪx?~^l=էzLv%ߚL\0fG8#2BK.M<&y0\sBZ37Q^4kfx ?YS7BOџ1KGi>O3l5\Z8 B?9 >Xzٗ݋,؝t%-.-o?i#=ÉXDpڃzqBHNH ! g,CI^Pr@g~'(/k(J:6zc!+WƷ/OK@_L@8E./zsƐyЬڢ_c7X{i|UDNK%Ε+_!wwϡ+MRY"U'>[VA7݌~d Xf|_Գ2Kl &qz;R)]hv&\]h9ΡY"'Ӯ#7kE8͑"Eg$9>R |2$YK =+^>Un=QH3^<;6|s*s2eQ /:=3JC'OVwE>Wl1\s~~ ^ L4CژߌTuP<|WMTG /aUO굉/<F`-k+TB~Nر5q!E9͸ڱ#[n6y1$ΟM)t/J|zPLY+ &ovRpG̚:7 |IX4>'ca#J|z hz%Y nj-1K{\;$ʔ*7HH9pL:w-!\dkx2M#A@>>3`}cq7h6?u^f4[e=  +v 1[I.ޟIs^g>,` tG"Q5_EN?`2LY;^V~?j6?R1s?}/~eiw@gQc{W<|LDŽs`阨v4Eh?CQtCBE}*Nqjhiz&QSߖNj3(}<}Y37?~G܋]I&J{ַ3qCO;%|DSiPmwo'n5! g0WM!{|O+rBxIRe>K&N{U|4_lOQ!~|/\7}7刉^Nm ؾO&k;ZT,-c/TOkq:م{<.RyA99R"͒~᫦]qG 6] 1սҸi ZFSA>4ycߺ<}euqUF0#dNkIW5>!B@! N~v?IYpI^N.w:4>O~ m͐ӢÑm*Uhٲ%\.QK maHZ /7 x1?ٟ~׃!b߁9.dO}W‰wwG{!kp}c=B@!p:,tziHa/=oӀ6KNGu顒_-yode>';ŪA2Ҿݛd cy6;'磙#b–qlH꒦%B@! B@v;`GcУ,p4b :UDxx#[zr\UNC"B@! B@!pz`K% Élu"|,|qNױul$B@! B@! R`fI85S㜤B@! B@! B"|I#B@! B@! 8- uZ^V9)! B@! B@ ! B@! B@D:-/B@! B@! _B@! B@! NK;vFO3B@! B@! 8 سsDQ>2< U6(,iFcpQD_VE aqتcDD QVku`sٍ, ˈˍH4EC$ziǍ<DXP8 MK8%Lv{ 9(3?gKoбXt0 G [urlE\n)?R?+C+S!y~H8ݞj'`+3HTQmnæM*!zFX׼$:qHˌDCJx)_,zjD/NX]* \ASA0t!*Jq$'. Zr5LiA!ы#[ +O;ΉkQT!CGBܯ-b?pc +?HH ?HH ?Na[nt).'>AO-V7ȸJӉ` `#GFJ hbEreǥ LVf%QOB9X!nn|AZ,pX_шi(;I̢AJcQ+W^xÎ]C7 y2c1]tL+w,)I<`ĆH(~n]S1DmYBX&8n#aJ&3a}#mP ̰Rߗ!ɁE(ee g*=Ñ^ҠHx j"!)"RI"R()uׯYUYvYHURZ2 ь FH-BLVzHi;!3 ;5Wc*֡+xA?0!QA%BZ-Mpe& _2x?Y>&4HKi#nhkr=0 q?[&#NDxkB6!Ϗ8?A['"G?BV0&Tа=rlowҐFl%zqlbs/-/RJ|`P^_(aXi*$LF4'iD#?-cXŎ![2^|E`4y&}θS}6 'r:E/:$ǧQ n^S@IDATaV4@ )H1(?H`+<|h$}.l83PBU,` - :|GʭM2㥮Y [}Qj*#vGmhW0wHt!d!2ZF 8ISͩ`*E",ʘo/_?c) ~Z6N.\$|%OD>^3B\w]#,!p/Jޯ)x~$Y|ѕ !z-"$XM>b ]!+a|!&6Y&ɪ )G94ҨRDPB͖GI?hlӰNُ@h}16bc_^TEXBz*gg'/JB@~ΪLE^Aaz ?gH0!!CGb0oI!#ץ(GL ,VM~ YgY_혥T⥏mg΋J0 Un'Fe5(6H2Ւ},DY'2'm`*81' ҰIG2CdeT/PU7 $!`I!&8Ti!ȃ͊i=zN\?~F!Ϗ*m,Lt#N \CGMH<ϵ{_[!C?{{|nLCω™XM:jc=Y3KY`%_^nmh8"oW!D!x,zvEX3Q@lBf^ +\Z𳉡,i;D$`HYm!zi,>%uӔQ>O !dŢ4D~|{(!9YTmUITXŢ"9-Ʀr=zEur; l ]Wӆ+9Cz6bщ.iCDqAole#$rE " 42,J,&X(qcM1>gw.Ԟ.Zr,Z|~?#*M7rW,郂gfIDY#G,Q\4~ k|ȹ; Y)PIVgnt-:* ţ8{ _`|zf_dfiԇ4KH"T_^Tqfi!##\~#C{?y|ouy]:BPeE?`Tt8IYYBMEc'Ghf7`Y dNHO0T6mŦe#Ñ zq^qv8G٢IP ztLzY,H,zai4ܓ4,xi('|K%z̖eČE/8$A? zS%K8%jr= _<QH0Gp!y ~%W+!߃}}.zI~EZ44fTqD҉LTWfJE3ڜҥp-ڝE-z4*pREM<B_X"&!.ؒ Hh}vF>߫,\v;H5C76ݢY\$pOS< Y a!9flqa-ifv.[&C?r=Pj?^_J!y_SAwC?G=H߱}N\DrСh%. Y99H d"=Ex,C}4cP2e)B@! B@!pfp8pH' pGFT S,PJ _RdaeqkVHQpfXydhXx*+*Q~}x2<4O+WB@! B@! g&[pvDWt(19` D5կH:Փ!_F`{0 _$z(NF>YIB@! B@! 5!NEH.Fx]Agc~eDg <1ȒBfewq0Е8uM2%B@! B@! B 'Pdj5γ1R~eR6 W/UYrYH!^N&_@޾!g~rfoIaaD"(K! B@! B@!&C hTmcY$aL!Y9Y}-MA)A! B@! B@Ԕ]l"]A3:~V}lpPJ+}$lь\rE `%+/h"I# ~\ WrˊB@! B@! H$`D/Naq!?"^]~)TvrfOCät%͢c+AL:D\p/[B@! B@! B* MhpdleH0 { kS~Ih$rKLVx%gT6ӎٚ9ܔ>hnv2 As8ӲZʩa0ӪTLR*Eȓ[#3)pgN+$FG˪wӅ@2j'\ꘛ'(/ͥ]hl4Nф#ԾTݨj;;j&sM?W8c7;U[:qh BdN{i+1)Њ`@Ia=\ \ N(@Y)vһaBSz7G#$J@ O(y0MS,hNV&R ŰZWU||B]D/BaRl&k.#J.]dU5L8&u)f.)~?UhlJ>y F wq6e6K1ګxj9E/{G:t`<+ZqFwU_DfˑӇ@p  %@ϠsxIH_0 :!`+@_`{WDѾ)JV@ TwT\sG8Ǎ@whTW*3ɪ8~z=WH|O\=~إ[/nB\y+/ dC y'J)р[W*NZ:GTeI-dVr'^||mdOaDW+PZwck:Q(YVOx;6&zQY- G{Po(A9Ew" 䢿b#KuO?D/JM{uѫ>jmau e_Zv*҇WW2-.zq94c3˶HG5l4W9gNW J`]=~hѲ^mԯP4|MU[yʒLB6D/ǽBΰ]_Ė_kS#j@@ kǕG>ħXnQU[n1ɗ=YpxG BcAe'_8V]#{'nXmlg7š/xjk,k1"3PqӘs9K__&7¿M3|CCQ81^:b.D쓩XV*LYE#r ˮMrC>(#K+@saQݮ/ꊀVR.MbEf7ˊ8x>V 4"DU[J{fR9'ñd%z^2#/v~{g!&`}G4 & wUykTDlL>,\Qv'/z-6? +ؚ]x tY?% X5UUhD㢷)SyEe}y0 Ns~x<9eOwrΫUh3uWZYaֻqK1K7Xh8c]>,xc[ B{dxO?ikXd%1w<{!pb + 1%K!p| iRH 8N Ŏ5'ܱW1߰PvOoOm+^l|ӌdUb-ςp;m&VٜQhwfz|<1z&9E36u pg3z#ё%=X998wp~"ß+RC6r͍P6n6d:˅Cl.@^Q Vf-_sGE!DY.3 5bKBLfl&m_{^zmC1dmKр{}QeDK9`,wNUч$CNg׹4 s2MKw>KY >y|ۡ=3pΗiשzUy"ypZ<}6GS4S4]?gF7>4a㚳Ҕ %ʮ Gp|lVsm9| S҆LNlyЦppziichʨAXϕ#;Ϸ|f$ƝbQx%tQ:HW}fUyﭏ>=TAAӇ>A40!_~ H 2&1@^i_N:(H?s ź_U}2HANϒ^{WV'I/I+昄իFbUF_ݞ^4oIQciЃG߼z@WI/+óigƖٷQrnGΘ6|]) VA d>/?lIu:?u6d3hp 57)ך%C6@/:n=w%^r E8i~ؒ^NeWn>XWa,ܪ^|, )$Όe13g     arW+7eH,V)D6wLyJalȩ[,49c2z1)0rRA@A@A@A@H `&7ic%qEvfE MKuvX_JZQ!3 ~ח     l&eKՖ=n."jӀcMڜ1gvxQM:༼ll     lr T(c^U:Ą͈L9MpJXNҋ@tiLƽ.r<6 ;O뢹4wOU/呇w nG|Kib!nzzɯV^%zHiO4ޢ[&:?e~ K^Rcw ޠKdPO{Y7ٯ}?# Vм@ip;pf9׻%/!:`٫Nj~wSnKhh#<@#௣ϾB+B?8a&?˹\G4o<4S8`qaw|p4iwݰ}zRC٤zU6v5 ynP.#QW+C[ǤWP UT528W`pv\@[d`iXH* Knkv>s] 3{wWqI/n:$І`mĉVÜ}&?iymSGګk>LCQr2] R3YIҥzv=ߗ H/+'@ySr-w}ñ[Ph6>[yGKoLg_6N[{7tVW[h, [y J„iQWѩi|s }yt1H;ٕsGږg^lzy4lnd%_A#2#߱C/ßCu3 䟿_t&҃7>zh@T`2&drVr8>[{ƪmfitu`.KG0A t >r[D_6_4ygN z2?$:+zWtv [»m1mvUv]6[;OW$Y*s6{ ? eHvˉ 05Ϲ|i4NS^r{B&[6TĭO+LIzt A.}>Ҡ},\Ds~4yHq2} [c+WLzU`E/Kz%߰+"(|1 9 a_H&! \z`Rs:O|2MO-^'~Ng~:Mz ~>3NG_z} ۵si P75A3˾OOG&{7/_BSg.kTy89͛~5ZOzvIsӯ-HGv-^ӭ꯭تz:ZOO ]OOZy+Ad,tE~5wq=T$A#OI\8p3A =?DF3^x Ug8rzㆇim:Huպ)z⩴01.,S7b pjEPb|79ngF_C3Mi1vhA_k_u_GY L6~ח|vXx<>Q.Ȥ=Hd{~@EڗQW1uAvd~8;;'[0O1d}qPP]k倰2/2P#+%L)$uSP\.襠X !'ǔetw_5dG,ezzbҕtmP#_j'/BC>>a4ook㮧xUYJ]JSf.3p)MQҟ ߧϫf&\4~hJG./?#ή-_&a3ͦ@[ SJ]Z5/Si'M^{̤nЃh[>\u.7eϓ-JE8PCs2䃩'>Ey1Su~2M˙~?/on}o8ogŒ <@>c:i%-cvM?^,mٷv怇>͏_J߹J:r;tޓ ͢\Bܕ57V1|J3 ef6 цqܽBsϾ X5鯩ڞ,D di4H1_ufOd;ŴiwFDj": Y6ŨHT Z]9eKc.x2N!L ~Y#l=-~dxt&jo wtA=iߧY5 O㮺~7}}6?gn)he㳴d~Գ|tE'҄?w5~Ʉߴꍎ-}^zONd<ўF^O~vvL|,esG._$M/ǏHv4#/==DžkF=>ô/>/Gki4Q>oIK*6D`>r"ͫ6(Jë?G|χ鐋ОÎߋzߌ?}/j Eӻgz *'/0V/7cqi̽cT~0C7A30F*H?Г;c=,R0?Z8Ƀ_ӂn7P1rdv77'~z&4>;eѼ߾Ay,HخX|HRáhڬsoOMSG]u{уCW9Fx1zn2-ԓ/u ]oGZ WNWϧ5U9vod~FvKn_(th7O~ F]6pqg 4d)) KpfŴ'_z#kQ#_ZQ'|sͦ1N(Om`^ uJo4} :H "S#qԺZl3rP _rJ*.U0j!e RiD6<6eqaJ\I/6tٶ 蛥zO,;~ 4t!Fp>ҋCE=>bH/>wVk>?l?kб8]K Lm fsG++Bү'h0[;TC!p:?>V_o~;?f AGԍ9Xc"K4}7=qi#_Gq T4^4A >SCI Ǻ^w|C7j2ڇT*Fn,7d8}ıhlGK;p'ˈ°70XkT1^K^gw-o*qn&ÿ.r@ᝯM+Ku*m?~Z [u}Z6#ڃnN&OhK0`<9n$|C 4 EoТg_u/D>u0(4}jH;=ޭj+?d8)D4v(F <_u3QK݋8_ܿwD檴5pAeq:%cmG''}4UFZ]!{[ p>J"e9tU*݄Kǁ #hIRXNA"P^8}3& КCrD:n髷J֖MMˀQO B \z`܏za`Q##AP8tu@zhPjyo4?у]t:6ĴאC&:2. 񕀤[z{@>4 q' zxMirS@t~̼/z>aP|Km@vrլ>4{2&?b*|6-g]Lg|x Z;h'o8 Ұ~C/sz Z-C[.=wvy1eQ_z=~noǤER2h-fAoމ!i?΃P2&.ˌs˿AGcEFгпo^,Z U"ḩzu6 1! CAa,r\ 23R[tL|g)soqىϾx1'C'[g\p5u5NT6i֍ 0rPH/.00 .%x@{+Byc3A͌hx5p]uBfoѷv{&F -:K#GV?|W{Z:}*w׸koZq)tmr;' 3ЯBW}Lvҙt)ȋxq*:?w|mu9th!o~˷GJ_ Fxs`ɤko=Oqa=HI-yD'^9짫_V;~y`I]@2`bS}g4yG_H#/<E8hȕz1@㯠K3Ig<JA~5 ѕzM ^csYʅ~uцݥ ?OhΣֲz+K=qu5JϬ|EF?t:ژ}֟~`ֱUs!; PYH5sJ3p] r?ۻ>=Naq D}h:f(ei/t̵Èw={ "ėpŸ!4L}{Ruynއ&L4]3 ;9VMӆ =o%L*Zw\cʧ/dDE/c)p|y~jdJ{Xy1+gN?nG}\KG0 3e^nB]]C*XD]jِa+B1K%t|9j2JzEH,0q@IDATLVZSFP H} `uBfM>E ?}.{OH^pSֿp } HkH7MEKOqOy~"Mz`h)JZc@ҵ_ZowDz 1HE$4e]@OOnuC Y&\dS̹]>eMO+iL稏~K_Azupr/ĂV= r fkrS;~x~Ƙ-4`_áI`񩩴t7GВ ǟW;wqԇN?gB`ӨUPJ=G-r bqG=(h@܎_^A OFQOi1u~gx#i͏+l0sh F^R}}h$\y4Yd }ݴhyc;3Efq(tǜUޠe ޠg&LMDnTx3 ''ӿ>P:Ftu\xӐq"=xdjy Zs,N{O^3K񟷠YUz'u/GyUhGk1 ݟs@}p˭м@v8I=͚pJY43,7K{-liXm+cni:a{ 5\pHx]݉a.^ݒ^(hE+&8d=楺_aHQd/>c 0u+-$snNۄڙB)YJ7UoD_sytMU4LiF?"U?kW}}h{S_Wu4a_"ƪ|&,.GG|.4-ҷ'L0Z}G7:^Q4WLD74I 'SGSoc'ɗt*M8)}$0 AS̤I^FzKӄIwf7Qoۿt"uI ?HdmW|-vlou r7؟뮱4h|<^s Nʤtu_v-͓n#`t /]Jߜ͎k+_GsUHP8m9N)ǰokWB3_qʽncΘCnHBs7CiX<\F GK NYCkcky^BOѧ* |BqF$t+u]a_8iC蘇&ӓ㾅UƩ: m~W^67uڅLܖإ'$uQQzv8ҺlHt^zEˑ۱+[Ǫv7JV]Ko?6o{#O.f+XAfz >NsZ{l0ڰKڰgU6\u +iSGҾ _BQ w7d̈m!7wjv_>7 5>Ya!~6lN<]in[>J'nde 07 .܈*r&X%vr֮]W @v{6uQ-b9=@l #Uө,k֬}ݷz^v#P.L>4yWyz1knt>ZKp MSدrcNS{ԥ*;"h 3" xsVw:~PoK!'7_AǤVJ`BjH@v"1m  5 "۱Z4O*͚\Q;X3KB#63X&<5VM忍k`ޙoa`Cw6"쵍=67 qd@ډۖxNgvK)qlC~4jb:H H.}l2a:o,.i;ngYQu-џחΞYKm%8*2k,;|^񑃏Ab5 `Y(F$A@m@ Zu\uL:|B %IuǜB}f/ YA@A ډCdxfQ±CW+g;,|zz 8gVrtԾ>DprvZ1uFA@A@A@A@j`S>}Ԝ?( f%^mm`,_kh,Dd *Y)}KIA@A@A@A@6XAYkUqaW+Ajj{R+ L"Nֿ> S ~V @e!8RTo/5ʁ    @\`I䈝ALqnH,sV_N6 3Gcsil/CeA@A@A@A@FXT#(/|(eur=2+9@;OfRu)ݾ6+XФZ59A@A@A@A@;bɆ ([JY]\+b7T،1ė ?Ų7)8 %0pWV^MmmȰh~UA@A@A@A@vrJ@ZwR.Cʍl0vu[~ڧmٖAe2maLE:;ʍD%A@A@A@A@vl<,kʃ{j@>1,+=2rjT*Zh|B3r٧&:N痃Dt )MCd gܬGDСfWSK2'ئ ෌j`zrmV Yc͋mC9]@ݭ@f;G 67r@M:pc5Եf\\lƘrȇ ~PV+_:Вc1^rڕGѬ˃۰vY@FceHJyp]Pȇm^[??UxWW2ޕGA$xEvDEéxiǗl"Y6YI.`bLH(+&bDb@D>?kkG=""uBC>jLٟh:r2'/SHhHbcq^P&풛ZU4RFS+ycih?<\Xch볍#hN-+5\d6TXy Eo$Qf٬ 8L-=˔A +»!4kRR""\H{%q)ڠD>bDD>D> QȇG}#FdG7Jbҋrq(W@nҫT*ɘY7A eJ[a Z WMԓ^vH&,)UdFK F뛕(-&Z囇_s29 ˀ[-D`m10KzqR)a!a~YGz=Lz!<)% ^e`ȇȇdm?d@惌B !bgg:b$LRZA+p0K ~&} ҈ɰFךE vz(ٓUjPRgYCͱa,t2J#g 0 2VUVoYVւxCfD>D>jD栻ɇ[aU8H #~TXȞezא^|كt&Hk+Vz'u8TX8VD>jD@CCF$jD>D>D>jD@CCF$jD>D>D>jD;ʇq\N;gV5 y$!+4#y^);<[lIWl:Xd-bSCʟ/g y7Z#rXklXvXpR`⒏A#X7Dʹh%8PŠMZ-2V7},Yv } D `L5%P{CŦȳSaG^k)EH.+_xWz$xNc:NcgXI`WöyD֖gc 1UkzfǣV?X6^޲i kIP$s..68 azC^ﲴ163 AԹ\!;xHS3lXASx]Iۈ H>"NԞiO})) "eψ||p=`"eψ||p=`"eψ|thdz$K3t!k% a rȚ^*4XQ,dLz$ ڤ5lʵ$WڋU|bHǤKz>`b#해W\D>D>? +#u:H|||$nW{ʇ<#fq++M#.dim9 Т2r e;k3KU/s^T2xY/lztK'nWxz:&xIVicA+-r}8oOnD>raE>D>d|Z0[t uW>\o4rt#Յ]J!BIj( JRZ.֬+PΫ5:~NkI/.vXd2 Fv_EϘPQ5&-2L!hu|s_v6~M~$U kmʡ&jߝj-ܣ@V,*?aE* >^R4PD>4߈|CΈ||i2?G%Κ5k#pF.((/ b AB̆13R "s~tІ I{fuG{b+yc*A\*`%U6 fc"IQD9h˥LafrwIw1fPo\D8rRr|ؕ,q$)RUXxGClȇW=CwҟJW2@ 詩 +AW+υ/1U;L8wWľ1ƒxMCPOzew(\|lx0{ImDbaE< M >L=Y˭[45EACGD^xy`-1Vstx /8O&ޝMY?րs@a-IB?+Y%1u?H>ǐ|EꘃI#5JԧW oF z1GCC՜D>XXZ"U{Y\3"""VoE>D>D>˅=#!ѝńYv^Y}p ;4Ua`e kVcرIn(]@.|e%hyec 1{/oRU01dW B3 3`iR+L-&Wx @J9`\N汆yDYG>"R>~܀i+=C6%vbfBoGzUJc)=T‚{|﷭W[KzA`, `>H3^R`R^|-I8O ŚVv>bR R9MVIҫabU$yNr0lOzaHY|¤GCCF\D>J||peD͎ȇȇGH|||ԈD́ǎ%p-'b?]AQ;ᄝCTaFܽLN ժkoN5Yy"kL >0RC4w6W1fՀ ,C+IȐW젞;,Lk2?]lڼE.޻XC,J(P)9#K"Dāy )aq))""^$ EBmB|!!QS!D>j"5p|=3ӄQ) "m`kD>0 qGEڰ SPsR6%I%HJyg %Už!:럌VYSJfV^Հwľ.fa"Y)ɦL4 β}xWD{>b*(H{Cϕ0JW25De~nK`y_2^#J +T_`1, kW0T$NjwY=jqt%1O0eP*gk +X1VP!4\hq0ᤗTǝ'U?(j33Q~N5l ?U4_ȴWr=m5[)8A 8#D>H+-+"  Eu+!a+x+!akȇE"v6&ߖ_JTI/HO:oz 1Qg_yUe+`XìuTbLr60Ł "+m\)PX@^!ǡ 6&l'*ń֗=b.BRݍwlH^|a"ɣn<4R""JI w^q |PF$d~s%5_0J Gr0k z'ĪWnOjv;"x`rF,R0c_\s%ϯwxͮ(Ri!)6ILJҷa!мJc+l!d?d)㛌6˵O?aihżP& !Ani+ KҟZd|%] ?\d!AD|+6j-)6W_WWz k%`)*E@DUv5=S$%rr~JzIa ȴO5$9nquT| Iҋ;Ͳy/?/aIJA-ܬG>)/0N>CK^|JzIyȇȇW?4H.+_xW2C\d~.s—WΪU#YUTb(jE I ^ YX,#" Llr_*9%b}‰JB*Dane;S a|kHZf.!ȵ5M1qvkdu+;#cyNx=):Rz!!!'#"!z,D>D>1"""qm{"1""L>*.I#\OO^| rKKk@1$X[J^/}Ezsrf`|\D>h ȇȇȇ# ]2e~}NΗSDU\6$}eyr@pɣP o gN5ԓ^Kp>(vHmRŁFіbsÈb$T&MfA-lPbm5&R4\eA|զBd0̨p &psbIHy||$||p]Cs_~AWzxxW X%5OG8׬LP6Up(DT j1,P5.=X`נ~bMC&8TM$+&z8PZ_a-*&8eH6,}/g4{%)Ac-kJm){kHl]> -,VtAqP柆K~Y4))gD>c"!^6 !}`LD>D>`|\6JCk YR> Pm χmcդI"4@qV2c>n0?Lf1t<:((҂K@Bc<3c.M IOxgf:=0YX5ؒg4-4vHH?WV?cYI#q=h|Hmȇȇ3[_i4D>D>raE>D>j&wbd|UABj$}uq>«ƾp<"2 .j>s|Nj 0r6KHJAp;!WYe/`01Vbc_^"4ֈV[K 4ȾDVDԍw<` +呬" 4D>D>gE>D>||$C""2<HJDG**Hb,'&RE_=DyQ?1O6ˬsP 6j>*|jdu%DU^(}Mz@+tLPu $2ʱ&CB' B3ZD>Ď8k`$tC8|wZ#2|qeb"/2@ڡq{+8v>b@ۊW[o+uR/VK%gZV3r K,6?cE& "ķ;jdEis|Qi5D4"sͼFGH)G\'D>b,HCC\ 2UwJY]I7IL GEYؗh?"Su_eam<(`\Yn1PffE{OR ހB]ׇu0~A ʣ\ݔ!kT n~NW*c 2JZpƜMG3h]췽6ՖK_ r?*Y%T%f͓Ƀ_DMԜoƇ,5nRPT*Q[[FRB:(?"SՇY(`k.Bk (S-+,<,2onV`"JBʣkD>D>D>Rxk֬C*<|m{@#w5/r9۩Gka^/JlOE o|-kB=来%VU`#Uh8d>SUҋ/8 MM6|^|dY6<~! 0^؃Lz OQsʄWLz} |V`4xN 8ԓ^>960EkDz fk0d&ġ &uCNZ lV)aa鬔ȇȇWI2C??񕌯x+As?&˿ .0|A.Œ?G2\"/ZRlYp I۵uKb΋r2_.$ g"kb0Cz‘WH@|ρ;N)[z@*6DURyMZUcVlTm3 1 d&0{ `DOY-O˦5)ǎ=~O] &YđK<, l``7#hYm9g6x)v<,4R@BCC+?n[s DT(+=U*E=#`{܎`O))/U@[|3b.0e)yܓ؊ea0g mܹCK;d͊=hJWZ_hGpXf8Q&,$I(ǒZ0'A{6%Ulf7Z0mt UYC^o,P.GZa2*'ΎʙmdP>V` 0A}ޫwi JEk۹FMCAGC"!U#ds"""^>xryn88B9|ܼ`_icCU gWyyO Y&XFbʂjoֵ|ْ|b>%EQb,48plU& ,YPW:E YRPfkxrdxs&1%C ;CKW3L"^{J-;k:?^g>?qH0BYDW URDq7 ?`)AC`E>D>#6p3]_U_mݚj)^Ův6Qm^ft[%o%Ɛ<IN.v*ϚV!-|ش[rKod*X$o4JxqrȥBg Bhsa*/5 ׍#1X?in^[XLfnX{٫ L ل9+4CU!Qxqx -ģX)2|XNJ8RzI`Y))UԏG u~0 !@sȂ|eb{dRO p|KYmA_!WҗfI >1>[ ƜöK\&خC|aQ{%+-H RE1 ǶuPg6M˞:\Be{X.r`ʨ z@khu̪Vit+`iWeNUR/١?Ⱌğ-d-lA|\qC Jcx߆{ ^,ϐv"K[Cn^D>4 >"HV *"2ƻG7Dkr㐘?RΜ^Ca^0nebYBsL\N9? 6U`+yL?tK. *[~3f VSaJ!N} $Y=m_7XNq%;vtrUpOP**Ț8^߬9R$\hf\FJ̷ȇA+&Hq_?@KW2!}ʇqlH9mrry<6- SƦ[?x,~,eڨ2ҳLpv7KMaO_ rw9L/ke٬DV&y{*>L*fD>3B!L62R=.2;itDd;{sA{'!JB_K/gf/M ZőLQ:޼ˣ:|o(!(w+h/4!"j 4MȟvYߧJ0FW*zEjZg;ƪqZAkϤ 6i~ȯCvqMPvWYT[]/s_݀ܘW&[kp B`ˇ֒2Q!mσWI/"LZ2ګphv;b1-XP@ W`TV6Q ΓI/)7W^gP-i"]&brtLjJ[ՃA9Hג^~ј 1~C~3K<2}QB4ԮȇȇN}j{gsrZ9]w}WzRO;h7Ui&͍?FQ/{]1gluސLz{*8+Rg˝Q$DN>o[u1=瓟y7ms[I\QGL{?cg/7ſҤi,~{' sˮ.ś_1x,;6 {ʇzU9zy?z^nk҂}7?C_}d:-@ylJO?cgl\[eko:Dyu]^>E/I0i9#}LX6daτ,|ۭy>&# Zq\qv_􏫟Զ>^{UߢAڟ^tz5 _P]=>YrR-;mrW̪2Y@7(/k͒^5["#;7!V}l]_KX3VQi@S/U\B<7dI/{ &BV1 W2 M:IҋK^|̾ 4X,Kzx&lLZiy6y,O0_,ܜUcAYLWxQ`'n V|)`"!aI5JWACuBWI`Wf)ܛ/nArlu0]HڵOg1{!0>6-R;`oy rܕxki3f ބ';{:zҋ'wOPF]Ytc;|<6Ņ_ɲzK>n8вLkE]KweE1|ލCL؇:{8uiz%Y@ ,N VD>G}{xڧ:`J#IOў9w!AQ=X-}"sK^>&n8ax{ u抩\.4VqPkC3ZcI]`Q>;gtqkuvzR ]Mhi!Z_XAf)qrZuTw,~HStxe54ش2*Yf?4^yL{G/y R++t#0_sC/.!:SJrɘ<#A]u@IDATV\v$--'RRC{%}RR2CQWK޳gT׹Tu-=]sFӰ>EYS'F'BÅcAM?rX2y֏mx[2|*ӯ~s#w|_*M-\WW]I<Ъ[ eDgMvL6_:jZ܁8:QSLwzje> j߿A:xڅ $xc&>7ټLF׼L3}v7-)T*@,(H Ϡ ⧂"H HS =3-y&!pN޼w}s-9ܟDjy[WʠNYBɌYgTNBf$d)#tƕ򲅲dd=QUvjw+Z?WN' [Dv w@ޙ#o]p)֥(?P-N?Ȥ lgL<w4gzS 6T2[hW:e#Zq6R`yNiӊRkҲ42`Vx+!v2ĩƚ.՟;:;qȬoj)-~z eߜ)oCY08J~;2.=`V_y>˰=Yt=wxfjoQ9הo8UqsސeKkRO ^L"O<Jgd7l`@/W@O(<3p @/|7*9>z#`Yr]`uw|7X/JT*~nJc?T*W2B 6JDŽscHj(!ӏΖ;_sl(Y҃[$1z :n.'$m9k_/Y;ǔۥ,SߑdSv56* [~Ƴe˰ ѕוZ2lʼn7b_e䚒쾇WJd&H1n|h#nNF=n/hԥd,||d7ט6czЗW%󼛱y=ѭ=ҴS%'- TmrGI-xVK8Ã6B(;ɶHDZ ղ]]u,1hh`BY9e`4|O)J|y#<d_W̾$7P5 !~Ov;n';NG ikS<Ͻyl_Y  EZӟ0V|(1H WI!o ;']o8"cɢ;'e| rW3̛agBPHj/'#|^ Iarczə6 s"=dQHasp$yk/rr! E\`y,UWKp$K)RrF+`[x%JH%gڳne/ٟ-7Aqd@)|~#K!x}+u#ʸ]%=eח,$s6n8W[ZYm@+*4?C@ P,+c#Pf:Oʭ+SR^<'C Fٯ/y~Ͽn:2Xp|uő, Ie+q,SqJ5zpK øF"ZlYLL;-Weɔ'A*oN8K^?K~'no>ȿ<<[\rٗ%AF:wy;2-!_-tl᛾-(/ע.kNyuOYTd`Xio!aFW`BTϣ(w` 1MϹy"]| 0uU7C)'aeɐ$OX7{[ ;V,Z}z_q'|߮.K50d@/V@H `oiv( V@0Kd4@nm36WCdw!4J\q !k{|.2lC/ʁ[ڼIԱ cs+e;?HZ`5,ʥxkHw`:<dbRXTLLl_0I2xJ>fp!OSM1;]d"vztT㒂o;Nzk9% tHG^\X::[~!y$ȣSڗ.1^TIDU򐌅W._ hn,}J<eëI^TF/et V3g5vG53cg0ڻG9/gV/<&tr釗GgeV0gMCQ_~D?8s"*Ky"2_ĆZ~Qx,PpSqƍ`itK% q|a}uFc~L`0xW,&_sWI9G*nn}1pGnkۏɻImK8 ]ǥ3K<(X+vݕEly+3k &Ֆdm~qyēi1ᚈB,*LXxr*A|| 0X.xW3X PA=[?*[8kdedךkfw׍~xNZ("ȿa߻a1.iI|$){ ?fin:dG޿8:0(](e)iVe98qiKtKyRR3ז'~8x~e]y[Kr{>!IT%nfylCbMGo@׬oC q4tWB&h>p9Y~,52(fS!%8-3w|X}:* kaRoCTQq- P{@)Ana"Z@yw{2[pIA/b8 guIO<4~gi{zgIl7AntwytUrMG@~`7'MGڂ'i_=IqyA-OP_%_O񺜣K+wkdSTIVF$tљ_Ŭ'*OmOɔOgڜjxu)كwѲPSf>y܍w~3cO9åyTo=r-2b %'@&G U?y 1 hȃ2Y[WPzQur˥)/NuY $~ bWNҺW3eEnEX2;]:~#"w5Sd{wVZ*ՏGJ7= 9/?>ZDAJ+f"0 3PS&K׿ȞGH_48`[<BI23ߛ /R|^n$ywM2Ǒ8@mi}#换?;aVH@$ԙ.v=~Z9>8ubE+kJ!CM dbxP*>w^9tמּXRIy h=uW^}ΆF ɴ~f9xٲ᭲?uÑ|p򡁵KɁ ,+޿U2l7|Xj_lw!:6]1u>p~yii9x_qMF_ƚ9蟠"Un0n 2: Ӥ# N`'K3EG :@J2jH" 71KB#.JCAo<~~~~~xn'us#(AK\/"'i,]x :gxS!XLȘoQŖB>" ^>YFo^w@'0 1<GKA8vo 6uK&sTKt x`?#w|k}N9c )aEʸ.-4.=}~9SMoK^tyV7Ɉ9¦ 0A1xߑ%_GfrtǺ=b46~sv7s ^.9#4 NKW*p&cv hH*)@M'E2(h / >L!PF .$\ ~(`7ݐ5dUyJAxC jGJt<ԉpj(kM&{׏-sJCyWȋ o D vBKfۥ+ C](>s͗@ų&Z!ៃ1Ag,ɷ"iX!_*satY-{p;b8ӣ+)y9Zb&7Qw=jIueV+1ȀCȧ$͖{5dTy'Ӭ|Aʿf}viݟQ__GHi>j?nB>]d5͵F$Y@sZç+/O7!Sҗ]2dF\krr6cab_A$v"Ր1q p(!^S /{V.pq=+ңFYSS"`%2~,KWˡUP5R9Ly P'69tE:=mqd:[~W]>wY 7O8OrG~Y&_y$2*ǝ! ѱ̎J xy6^&}f{)sVjD&TpJ:Xݧ;2Ǿp%W4/-[Jc,yuA]~v]SuMÇB 6INtt,Ŀ7=8luM^;0Yx-bJ,p ))Kε_ߞ2НTSQr3;5 zeѣշR.u/|v>_7VG6M i벖}utUi7=? Al<䐧yWc&'۰k!@vn (%x*4hɓ@6ZYyJ 'g0&`CЫ|B *-x!r22M ˰N=VSe2>q$[;ğ>J+WΚ_[U*Rѡ>L٪ċm2-psVd)6qjqr1\οɳ@)Z'goA/~Z']2RI}=#?O2gRcIy L'(ioC㢢,QH_|&扺Մ =2'J[W _O?Y~&2gfTю {JGõ$yx!H7"9|B .R}Z*yjETj]"=adU Y S{TQJQKɖ./Lc3×exyX (1\nerr'˅7=*+Ȼ%Z%\* W{G"(%6_Tj(lYSo6~džn]mQ<~}06ImyRƊ0s*X|Us%xW{.-TYZC04 d}<ȲY<3+$;IaI:j F`HȄ QR-հok*'H#EZ$1P]ޭBV2V`]*\3 n0`*FeI_skıj׃RͽM v񻹿&~\ueIu3K7qg 0n$s7!p@HJ/!@Y;ȍ|LɊȃgyxBV/uК^MJ6 ~g˦/Z<,MtcuVDNFpt oˤ%f"CpÓ<<ΘR!b`Qi8ayz1xMG^<2SFھ<$2gWyTÖB}>ɣp[.VI+/EQyTAEo@+yww#V4ڻ7VV*)*RPP0=A+F̽i-0wH?ػyߕO[(Ӆm2K[[6]#^R!зȔ~r@d fMk6O>ʃEK1䫓/'MuɃ>f7 ^"-K=ZE) CO) ؍7 H?TQyؾWv~~pvI Տ7ѐn?MZ~m(p.egPϫ?n^'K o^ߵm փqޕ>^4xQoQ""Aˉ-3$yLB;i_ !~#3^{KVv`|Aޚ5wy,>R֊AD+eiǎ1x9nϼ!KKiBy囓 = *TLGIw3_x uIºkVĒ6jee]A(}w WRB1C+g|_&f,\ю852gƃrd}v'eX/yf9pj?S~yyNg䃣PHz<# M Aۗ.G<[ֺ@%oW^#yXUZs 7# X @s-WRGhԫ½2",iQ& e_)]L{E?P"\3m RKyKG(AGmfQPCs!lUɹi)J*v͜]D^gE)_*z0PY,j^^'%:D7d'HEeX,ܰqy.8/rx r)~]<)C0|9kQUN}~߯ krK&LBw҅ 2{ڕrVEqnyˤ ;{fH|*]\ĘK%XuYﱉ}\vֽ]kWdvI܁28D~t,M7Wf}'tP۴+f#+.~cĂg/3~wlڝWɴ;#_<|F yI=hE%|l6Ũ<NARd<3IC{kr^X,muK2J|Xeu'd)8l5[bˆh(ȃa89CEݓD8-"y5m-AU*x:~qZs.6U'9aTћ֣aFL"L ~ O`.A]~WymvJ>S?ʰ*IbG/hn"X..lz{cU҈EFvz o\ 2g1".=\|DH#Ɵ"?Ř` -D b2dHpz{̸/)ߓ#?t =8gC^c&D4>A/.G^4 T<":\{ɢG 1hVIV%AB?δ<fI+>D FPPʎA:~"u<7Mu~XM|{G9cփF^MM2hkCS\b\gב4zACN g }ylkC$p$[#nkxI _nxr)/4!Sb,,pEW%,UpC)d :D5, DDa@Elݑȕ>} w"ín]?N.O, k!)Z9%|&Iim)-1JĺbP * "|Y|2WkA5GyW1oF (-uGn;(}nB|#Kƕ_O&`/HحTᆔGq%!z$5G}>;yDçUQn*lm+ՏC+ߒ0ޮxϴ y=* {UW+]u}nɒ%?ŏ_yH8Osխ?J1gّ.(^^\NJ7XOA=$y1KfkR))XѱB:; \OX\a?W+lclF[Ȃs:q^\;yGZ=^ "7Z/5@TgE4Q$-te3O .l06O΂u~p1g u 'z!' k +@pW51i@Z?>:24"'#Ufu\Ys;awzep!a_l]inUiFLpY5LeXVaLǘ3.0K0c*-i &jT*H{G [=S9Տz^~~Է ?B~~~QxGlUƩhܰxXig\ KcRCN^4-a{JZnK*;3X{Iܳq (e?auY5)eOIXYP%tI/cuVY+ ^߉%PFp+bBC)Y¥";e2I`YgT),CC guCs_ʏ G{CQۂ׈A?g-uG!Op$18}p0WnxL,ǀ3-9*#XD@^$^$7}VPy~0Vm٠a`?t<NWeG=\W!GT?T?`T?Bl/Ӓ d,ܻСk[`J ~@nܹ0_$@旖,vLL RW KnKwEZ:+&63 ]A"^o&"eZ &%D lhHҙ-qIG ԗVXR¾>vW5CHp+`H{MZ . ֈT*ǪAUWaG8~~ fC/18PhĝC"̑Ao+J$ 'NxʖGG%`'IL]1iI$b^`C^1kN"r4~k{@*d>C`*wTSR@NzѼxR({ gnX#tk4R 1b/Ub`&~W1~8U;):DU)h%=0WR*@#H)5s%5ȓr'hĨVrθ/2!y8)):VUh.#%.$-`â- 7Cl8<0F뵆DjI,k=] *U$1b {. *h0zOU*(T?<'£o QC÷ω[DxTPAs"<~#`٢jS7, bnDZ{EyFQd`O h+"HRV^1M GҰ0j>UPI&6gT%UXUt?J lけpL9<* kv4fYۭW8ʩ̭\B 46_̂q4KКTn\"U*9a;N9O:U+7Uu}x ФWF K@ .U@j!Xb :ЫꂺӼ.ChEl./3ΗcDЋ;;rƪH+3<Ƀ^<zLzqGJ ȃ^,)e<M֛Ã^tUEӽKCiXx݃^NZ*ցª?J?tÚ<0WPw:~ ^# \ίT?ru1.C `~e!b#>䁳*Z33] _2{UpTzЯ'^H:Ћx^y#HKǘrɂ.%<1Xn@/Ve%3&x^z*'Qy~ԁª_]@a_ wd[>AF=`cht}s*fF 2v4t7X`E*&qd˰P ceˆ]3y*!*"`?㭿";'E_'EbR /FiuqV?Jv> <_/`\L-y'8vdL|c^ƝrrVkApafH`CPaBslcP#h ίt~CGVU?T?T?B}?\ø:"%0 p*hESTa_6GX.4KY"~O!pb\/ЉgkYT.H*X-@\فIZMB,2/Nq<7O2.e dveU |c$bbE,#P-w%籕e>++%7JǂmYӄ,1}Wzn.S{~}9$`kQ<\ADЫT4T*l㇎~rBW:bKo:?ds]+^bu_Hz)q!"nʩFةR*ЈnIJ!DaxاЈ#]q'Ŧ,ʇ;%$\b@$fU ȗ:h(JFvucQͯ` c ;!Q4[ bK`[ ]u(p̂/ew0XDݓO+<XXyGf]z3qep:e)#$d]l2~4ATBlr 1ǰCf@&GT?T?tU/ ]]_yp0&zUߪv_!E*n D+nB,^X*FbvE@/ZT[UR@tQ^H,"2bzlz"mX_x*6IX"QЫ/σ^9R3Ћ< E H?U@Pal:BG^]LD0y%fWGm `vjrVL{un/I*0O#8|^ sw;>$heXVWX%c4'c2c aɘ}2@`i1f Ify1Ht!NR8_sn*rSTov`>T* k㇎1~xNGT?<'£o QC÷~)vyJJ01 TߚP9bUPD*0s"0TL 1,UE⎏Q""sH@`(e(&lzЋUu*&ZkIܮ!3,I"1pL"Z`f̣w[Sy,+cӐG ?ՏMB?TJPy6~~&X9:x̗z<6;V?^8X2t9sm5[k[Xx|/}U?xpuůĠ"@Q" %B ?z1mHRs6k@h(U [R1;isr ctuP ݁^>ྷc*57n$E=Ӂ^eVћ}``sA _FN=eY )Q@qTyuVT?L8A}\IW5]?^?"Jǻ^~^KT?zl0/XXiΆ-Eձ'jĭćXi6/S&qIBY b2RKHfV,Y,_ 4k6|hu1.s@ 1+ŪK wÅNXmcN @9^x[wWyp|P*E׆GՏzᯨ~~~ cXWy0fCAܜpcZyl2,"\"uP (J,..K0J%fբĐ/;,y8flp,"2neZ1=hhe 8:n%:,I2B%') E:ҔvViLq/I2ʣh&WT hF">XEit yT6.\@R屉#6cT*?x^;xѻ簾h.-7T}꽽>_jsK,\d\j}%O.P=Ыkb-ع0 z"nII1}-ʴp*ŷ"2sO"- Hx]4ͭXeA&Ẇִ*GzKZ˃^ a$hUmŒT@r}@x*`'K;AKRTh*@U?砿RJ3D`l:~~ڰ]Տ~zPC+]Tq~e fwMQ*\2P~qFT_,@HDƺ fpdb\,V'LU47GRKK [ v**:_ɗ[['tգ`O=JīK怆T \lqZ(sA1 Wչl<,Hns_k ^bp#N*FCA/ኂ^) QV Qj?@^J14]Ѐ^NIe\pdsŜ+&SWf{_ֱL/\?հ>)𩞌F^GžrB~SBVӪfPl:~l6iv=臮ϱ$M/Ĺ޹0)^Qkn"ivotV_Q)2 !2Ҏz v6W!"ek3eQȐQȖ2*l]򨧆@[!<_HCۄ?^;Ƈ8~x9C9ϯad.}A^_ꑖ(cw#D^Q^_ִ_[neݏ5"ym"cvV2]ݯ|w")Σz 7Y 6ƕ^ʶa`4ZW^yA;J] ^^a5{/!^1lPk%&}aW`UL +]D4<_:-MLW ^ 0uӷ]"zKMO?q¯~[rZˮ.ͅNϼ-G|rƀT3`WJ6(4WEl1Z~qgKnC wlDj*kF ;E6$X`t@Ow*t_1/+N'$Z&c3f/ 0Fs}nQ(@m;d;9sم5hUaU#ﯢ2QyE©?k*̣U)vҽGɅ_h"#|}Gvhߕ<An}'$7>oS$9uusM~l-g~TX)x܇⎺d|aGjlXwqs]ȣ6W>+fuMM zK98T݀^ qU: 2D3LgRfD/̓yD8Q_Ι G;ʤXr̕&Hߢ矒7ߜ#,g"* 3%ԓyڐõ. z1ofa߫%Tց*C҈EW|x=W~}^jYt=gU*ϣmgW^#mAW~M?Ifa</dž]$6;{؅hk24L)+IS]~+X(,PX'] 0J#O)a_&y[ϛ'uqMɬ3t{_~[84;J?D> ytO9En=|_0\h_]?]0n,aŃ`L2z=G4K;Z/T DϤ5"D)K:t ߈%fqzGE39x2&Y1n }/~#ȡ4/ 5@"2 rQK{g ;Z9T9lgZ/Ky2;.ǟ|8жtOyyzG(l ]K9"򔊸Ky "ꮹ(: EP&?b3e߭cѿ"_wK9_ώkL塷22ne_*/9,=uEn"#G8pkyGeDW_{"[˩/t)ΟvџsN4A?_;o_Rv_2&3xe|LyŬӆc>?^Z۟뮺S^7yMXv8Wႊo_y)5 LKpn׾8m1ifCG~ :Td ~VYW$Btٖ,q1xҩRŲׁ7 [X|s&8 r8%0NR$(OtM<2fCd^n_|?zlyn"+^-3dpIme ~YoKKdc]|yVIS":,)_X\1q n - s{Dyn.L/7@X'@LrYf}}>9h;VF˝H 2'iT!w/|!\[7[l,3Z.="/?0}qFN/>|\p}n~WF'SΓe3fɥS'cvG@^G:Q?Jp=NzЋNPM^!Ȁ^)ltGˣE'mA/Gj63ij@ߏ<gĚ8zA2±`v<먥air7 *MyzNQn1u).z)^}kozTus9?vW;hnrk՝细3eï28훲Wɯfϓ~KHs}}rΥt嵴mm<2tVZM{}V6Nam% 'Cv*,_.[H'⋏!^EW]l߾z/#ny@Tg  z |}Q.ؚH `jH](1*#gń;DPSQ3aT3h|"nbcwƁQo's i=)<gobQV egFлFk-&9l:'%H z iq?ޝѾM6r6WH/!"|DrcxR^!D2VTr}fl,nvG6XF2H#:hذ3I{[Ơ5vu*7ig yX: O}N9cpzKSu)sOmW!vfPf&"c-/+ݸlj&."Ŵ9_R 8jnacg?yeG[>pAvG6yUP>իMkm'Y苛a/Czu|;pt)[G &lbT#{M*Nzhq謎cb09οVa8ḣ}zģC/ qv.Nkg4%FҫuUyoƽ :Y I?7*Kd%60З/?7pޣΕ*>PQ J :xLy_.϶-GF`>/eX9mCz5Ǡ>qUcǻ ϋVFspqGq͞4+)hҫFT]Mɲ0?Ϗb8c>Es0<: mUkQ>(mi7GS>r#z7Nݺ;/[kXpg"/rCGBBhEsf23O@uRK_ KHwQ݁luέwJz:4L 9/ƧW,d? _Κ;2U㕴8lw$B]kX )=z@zR `;%I DԹlH.xH` 2l%YS\H^@"eׇNZwň XlFǼìcϱ$Q0pvchі!3E;"f4ehPZ/Ha͚U`̝z`X*L6<%{p}kCߢ{TQDE$' NgEMZZN熘/۬qkIj1q.Oh[Z0.[:v߸>(6+˓ly-_Ib#PO1־8,χ8mi_3лxc%q8#:Cw(5eԿ}+pWe-[<{\_z|հ;Fk)e&uMD*͏bѣbykz)]?̖|30c _m /pmbP2?Z4XT%@₟$ߊѩO9 ('x(:9[)EХ<\?!{ŝ7 OHݕ_D%?>!^2h_h RתrօıOƿfYVYR;UlK?_O v lx_/? =\w{q9; {VWl<ΰH&}E"V(]~ݚ+fp&mo˦41_v:M>y7kN7"}/H.Z"[[)yeW{>TaU%ބ< '%镠B,O8%nҒ}5sH/I`CIx8e0!լOwcv&cG?',VT(4aY/csAlfc%E'_P+5ѾKgt-=LS=>* !oMGM82rߧ_UUbLJS|2RmK}2XUeH?ԙ[$8s_^vO!d׾?]ϫ}n703![c-7åB=ZtA$DIN=HՌe%X;oE7%Cy^Ma](RxZ1.: (`7#∄Ȉ.<DC-1σryW'YK 9Ǻv+kיUzDN8W?T VkANhR}iNUh%ƚ)>TEbIIuwH@kIq?ZIvX<4RO[$#|ql]ͺO~>gz9~K*JDұ ZE"*/cڡIq#@&E *,+u׻̯_{+9l:Nx2t2@9y!AFvuVz델دvCٕ1LCТVIA22`bqih]CBӌP?j 9K|Hr34~R9 LA|G̢(5d8Tvx^\yY%CϿX0U7s׺kiSx%\>hn۝~E88ж8 L68?4S#9<ا5Gs> #S]L?fESMAj>[ah}u^<-]Y}ҸEc_+pM@Η1ݼz8k;z,P*.[ƸrRiϫOSG﬈REЭoӻ>Oue%;\+ײad$Nodyiߛgگl#Syryɇ*Hxx;(iGny .d!4K%ђU#<51#MOï-%s_sJKHyϟEf3ft} `F3XxUNQ4 |h |&;'qd # Usi7w$T3H"fX*61?c1@:}iF˓` =-$/IxWę /-,N_<^8pZՇ+Y4-μAVݹ)^0-z9CM_a]wLt_Ե%?M%Lf> 8Wltɨ|lI鶹Zg-&KD01K/LH(aևֆR$VD 2kDe<#gjtLedoܐ l2`wfl qBAM[9m"Dd_gۍ>l}^s?t}p8A駏uˆ\c間(ة֏≹ ?tiWf s g8vK}+oYD M,k-=MGıyfVG q[|3wnV}f\!;+;#\:APB1X!z?_=gQt/}7~3' b޴ {|:01Au\L_(z@s;ؗa٨I^'/lwqfF&9_N86`;@R~^?|G'l}:ZWǺ p|ΟV2P w,'_8r?rQB51]G?<&XP|9ۚ>gDj2T0mXg:mKq41 $=8 7"iME VY(eR#beH(Ms"bAC vk8ܳ K7A QΡycŻKke~5h{n>9';on#WsnXJ㞸9} zu`0NmZVlq}5ID0$#8QXZEŅ:W4N 3.>dD.1,cƃ[U^J K#V/ & M=.z1塮|2rǍG9JMC5]_Z,!jKG}i5||4 >77t$$䶱4-*\^j"bZ숸:͖'i)OFhIF{Z~qgD -l}>qڕlۖ#?lpp*ki>]GY畜>.}.a,\ Z#q.Ckg;Z+-sP *Ԇu&ǯxP7*.]7f=.A'~aywSzS0y3'984GKϾG6& 5"r,[hy6Cv'aY4UM_SGer8Dct&[N2ia>fP^G1:IeeIZpoA!\fN)ҫa+45z*WΫjNnDmqQu95G;uUz >SKgiQ:loߛ g|~g`!j莛yen9R N;U_vy}eHm; 'D@F^0\#4nb ?2pƍ ŸtH72s4R!+DkuoAHXk~Y4%X^Yy,c!Gb4 jWĩ3D'"/ق AUVqsCRUz_$#ZAl;z(*ËD1 0j@!&Z#LOI0mvXˇJK2V^b9R\ yQ!ZJG-.b VQG\_5U #'#y5g\M̗;kClݙ׊X!U`é >*#[Gև3^5U+e9v,"Uv9|xשRieS9ޱLJ%#.Y3u2~$[@~+v5tS>8I?iz0 ;FDʧ>T[A*gߢ$SDO`̨f-@IDATSŒDط6!v]+ny!|%D6,+&ITAR>Ⱥ/,1iaH2Ts_ }ݞw6_⍓ϸ\)O\:QA'^_.nQw$$.-K"8nSDmCQ ¥b$R:>%H^Ԩ]>ѽpeA>M4"sd "X3B {a~K% $&3+@{v^%$1M|X<43}\),V$gы8aDAH)ևEi~OO۸™8W&L}y8;̫< #ù zYs k!{.{dA`և<~bǯ3]:5V)kf\!$_W;lUeD}HPv0_xF#JvQ /ux][6oPw 2Vԙ_>}k}0.@~B_e^R"$eրR?g)ܷEz:*G`^2s\Y>YE?G:oTydV*`=6lLP3L|eI:YB<~@%}4#Wt))^aDf[&dp(qUs$<_fIz.Ynҍ3JҜ"В%IN[(!I$TSq]#qaWo0rX\l$Mf Ud!ϫ DdE1tL[̔OHiL_!Ӛ5_o[I}Hc>v畭[}^Gս?>hƻ}827#lf-/FhR8(f2U?*RDϫ]e+9\|/`cVqz7E %F d;%;NV\n~% $:H"&Xvr-N!qZŕ+)J^b^i3[yI %<^nO:HL*c8>/Β+bfurbnHr4$ 5%;>S)aMK0+?ʾ?+B~Km?tocTYhk{CvP[t%Cz,}yes˗x +u2䌍J8>'1I }VQq9ls+cfi]wD$h0b,$ ˙DRQlRƌEie)8l2%IJ= R/4CXH~CXnK.jY O/s<.˶UERժ#2j c&9ewf}H>q7?t;aW$v}^Fj~xwVyԻ?Dc}ijiWK-{WX2( '8KJ$DND,D$_ǷlRKo8Kl% &$d3Ef]j̗n'eDRq!>'' أʪLXI1I$yLb&9]q$*~YsH-7LO g I2gSi>׷ׇN=[۾?sc}^9a:ޭiV V$ޤVX%{:_ Cnb\)I,qqijE .2^a3{ GEuz*} Z41SyfFD*!XD[!䘟,XnEc=f,0~ H2i2s ˘6ak s`.ڲ5--Q Oq&H$<!X -N.`և?l+Ӿ?\`p,swÍv8%#Ĭ`Z}/T/I!bahaD9% o,)0a.͹r~q5--BB`SJ=EJ)%aH"%J8~{[sVNI_&><>l]>a}:_qN?[&ꃢ9]yfAme:sDi-criR&)1JxVbe%OF̹*n (Z-0FظH4-Μt[Pa^`v|%C;]N?r^ܰ>w9=Q?a<&J{D-*3谌w\n2)6;wfs\: 2Gbas1e@隭E$&YdY-j_ST үW Ql'"^7[7m[[,m:TjޣK2l*Y~ZfR}$kCcaCda}}1_ev|eǻ*d[YFaA` Ğ_tJD.RD )9ENe7kb$ėeH#cYSy#?ez?#0+KR$2]S/sGnʖʛvscY]qJ(fz޺ %1t4w:ς. )ʘe^'gn6_Y359xKZqvj}ԤYbдϫL0_+Ģ>l3}d LcpVlp(V4bG?#*^qًDI u9c'{XIZIrY0:E:E.+SleYΗAz=$+}6(.i/!>aYdF3@&ƾJ}9H戙rKǸgx>Z D4Aպ@CjiNXE53bCCa֜Hc#tȮay5?lcf7WsAp*na?T_]ᰶ yW$!٣4mD^f$1VW)٠~R:)o %ѕ}*X;>f~=9R$*Ql5e=%.k4E fR 8 s- r%|.+⨐|O,El>]ng[Eps_C :ay r\hs>JwSU=Ƨo^^ևجaC]?tحƻ|^9MُVcV?|e ӆҁt1T,cA'ɱP=B pYnB*ZjKE"`X,E"`X,@t᯼ACz%2v cJ]_HzhKl5sŸd[E"`X,E"`X,m l5)K[z%n}z;AO78K#=^tHfO#a E0#M4qVr7@`ɒ%uT-E"`X,-vVM{k3"l"`m ҞE$b8uV;Oe+j)(;#H/9DǺ^2aj c|8]TA2v"`X,E"`X,E"P>HFWYc)+_y&<*5KcB,ef,qך!\{E"`X,E"`X,ELBaZ̃Yi0ՠH 0 +-iyzd[ið}E"`X,E"`X,Ċ3R$6,-;_yeF@ (-syL_2dY9)#$!&Y`"`X,E"`X,E"B (`6G3޼?|STb寶r4J'ArM1lHy f0)W Vҭ1 2.q\wI}Z,E"`X,E"`X'Q.bLi>eN^JF+/˹C/;N-S1)ikfǜtv"`X,E"`X,]+J%U Ezo8G$owܣs;k&Z. #MGP ʟ`+9rl4;<4/KDLg-IF_8ڳX,E"`X,E" _HD_;* (VM ow9x%u~ҫ++L\eS$ٓӿ!Xd2F.7u xX,E"`X,E" FBz,AOtI}_ W Qhا>ho¾{V<>cs/)A /O]$FHl*_%WhEF|[ v+GRL[D1x´ TT&T)TE"`X,E"`X,qoK?Mw][j ȡ}ʵto{zm!;1D ʋ`++>^iE=IDI E\a^̈f5S$~n.viX,E"`X,]M&%D]VhM'X >/}*5.2b$E~(x'URW+?yТ+WUYO\SGK$R,UEUoX,E"`X,v# 1JRc-{eܣ~b]hڀ_QV9W"Sf~rC+ErKH+7FҫĜF3El^9XI(Ƣb}eYXٝ`=^S`M+nr[.Bbd)wģX.j)zgKhnSvv6?E"`T!1^ |•[_'VXqXļ|%[gWr֢%xd$F7vʅ3}@xj.ue{svQGY-sN! O4wW8xKm9{}UEnYqؘ"eSer_ Xp1=WGK9Ǭ3XAnac%gy..%h%V۔-nUVL;^^C,l|)c(0yoYݿk;+^g7K v 6C=\+9b#Vv[魇0ƖTŘ }Sb'c[զv]+Y,oDž7Kco;aA?NF`Ȭ/z ~̶\1jZ^16w7MM׮VW b s<<0ɬbO}1#'>qO((< Wq̱];7*1~ު{ڎbLd]<宋/#~6 L7%:ʰY'cˮ ϶-;>3?[va7ָۗ!qN>?RvսoW؛2!G|" Opgvb,;JXMk=Y]/s1ӽ(;^JBs^?Px$2ת,bՕ L$|^Ck0^!dsVyy7ę$Tn퉹u1˽,[hOם_Q޻󱀙^1kݝ˹ebӐz2n->|ZvUic`FF`l(J6*^"`X~b(KYEH/C-'߇KLRm<.9\\rr'Vc%?ެWmfwl)Ez⋸ _3 扎R:;w;! gJ(IZ௩ @ ѢC7^Uz_*[hhrŁόAT+#t"?oqQ\=oۗW6XbN@Ŕ愳J,姡]s^]H}HLiQ!|0׍AXnm,;Os0_g]+?_8xp(t9ArubR8sZ;o=?+QgOW6u? _,cWઁ}d~$}9|YȁAx1=ӝ ФAB#.SÜd:`{xV3Caޓo- Խ Z kD1o. bS腏yx=1GQ1[:nQ;#?#~yh5_‚gCgdj1%k`ҕOz9 . pr%#Lɯiphh,_Xg_OX-4L^P?2+4qX;}AG!B٢$|F +F@<^x *§CW`~<<4CyH_R<|&bͧ2 ~䌮 JMcQ\i_W;[Gco?[_6H^Qz}զkX,Y\|G~paѳo`q% ohD\Nވ^So:>-Zu]7=WH9x8iqW߅?  '0uuwtKk-zw sr7wC!nXj_܆aD9r]cбnnֲ6/9+]ahN0A̢O&BwQ{IZ{,5<ۘ+:FmB=.Vh!Emnl$zs }ryԑڗMmCg*~sN3apшͫp:5SfI;KS_3ʻ)]8*eo\|y(5fw]h5y1wlH8"SPi>M _CBw\̻`SzdZ'vx "GF8̹#9uѕ9]h?c'rtv}@4nm[OɵrXV e :+u/a@ Y̾.ٷeYW_+@ۯ=+ j֚ AFWae/8p8">VǜkOG]Z$4wGɈa)Ntx''%%)yi%e+,c8攛TdJҥGƭ%]E<0g}t"!?+48o:QSLx6V.>W,Ĕw^{-6{ NMڽS8kq @{ ovO\ I.]-x9G05$W]{'nD/@xjxf<((+<ėJ\xm1f)Si5!oGDcR?4l?bip3 aޟOŢ1p8 Q7c^u'#>l) olڴ%OCyWqR}A?#{=~Xl~G_[ ">8`dNtrJobѕϒ*kU\,ȾxX蔙|{8}wcS:v5ǣP!YiX+Xf$o}pȈވ΢RrD?BToD`?ˁu,'ͮP-ݶX,@lŏSeZ}}Ə+<:ֲ]7`P$'ZR X4}nfn>:l)"Zm:oɄE"e8U_ͻztS-}+ ~c4f gNsKrCJ' | \x:|8qy>LC ~| ? gO_xv}J]PӶG~Q8k:RKl!:Pg4(]up`vu zO=暈?l#a駪SV1n@*ҥc%Z2GzNxoTL~>g ,46׸?T&|˓-*-}'I5湾GN8_45Wu@[: "QuRn?/ܤd~ ߰W7tu!:Ol.q\s>>Kxd^ԕ| uܵc0y@'1%yCTTsYўU4}Cz53>™R}_k}{$vҫ櫯2txJMdPH-ХNw:zQ&yĕKoAw9Ax!׺]²N{gnzz+7|kHƩe:#7qM֌_uʵΠ GDCu:~OvT}ҔPX醐뉃:T.| >Dȟ~iX:o)8}нزT^WiMb0[u:xruꉶhГX^`O?fXr{䊁X ZiHhX?Ҧ:kX,=<Vb ꡉ){(x7ː2;R >w'`H6hXס` C5ndZJ:r\:rmwى{r/8.=ZjecQsMeX6e 8*8FUw EhIeICoeg5iWu:uEzؔsMNկs̽tW z[uRGB˴W U?30oar]G)yqk>Sx۴UrmPuR[}fFԔ r/1H qZx*`-)IT,Eyr5YKh~_n`kWBKq=i.܁vm#4NMPRl-!GXc,"KWd5S? m$.xXe۾^lKbהinA͹mhYļgbմQmS! W}YR-o9hn6_֎xvG5l}qfA_t-dzo}I8o( Zi=c{YAטy+:W ЋѪ@gbӣu"kX,yE֫ 3&1u\62@iX U75Ї%y8,7>P p rs\QITm[f!bSQP(hu`'gNE|?xj:Fg cbayx?:IRc:1|e V_|,\hoL7K?u']O'_z(ggB)ems+~ǜ3ap]cl-{`}Ifڵ TZ2{"cs~M2"%(Zf-V4SIT36:/PIn9"`3WBM)G|HGheej=Ob@eghj.hyf=w~ms%Wj}_?k k\.Bp}s\H~HZJ,dv~r]d\uLt$sK7f Hz'uAnPZ_NG⋏r^m3Wm~=O'asT 7D}^II.NUTލ>M}yB|Ɍkp9ܼh]@;t9 ;g\h*;\V9Q;iD:Fɾ|(9ٯU}V.T2 >M{ !'aTd!UP!ՠw"!M`Z>IQ:fizqLݐ͊_7Cs;X4t]ÿ/݂_f탵?yrܖ9*^G08G$ g;s; h@K`|3cݒqO =6'tQLJV Bۦ'RK]ݟm'%cWӹMHbX,=yBay0LAmV8{9~:. z(M!пM ŰhOMR,^,=b]R M²:m:(S-;+c؄budt`fw'&}2F,1ZY1-aN4:/7Tc\ڋ5 Lb zk<)d9=:S G+3g:nFfF]g@ivk},BvcwE K eKǸP_CkM bln k ofҵi|Q jeto'/@w1몏ip;p?Ѩ؎5H1<݊j.> H,!X?#3`x?o?"&&Nګ첹K*xyw/_L{58?\ͱp5QYmwAGggLLsz =4;̯wߺgFOރ^CwKQI]=-o:a&͗5ohs9y8nٷKlJ43',6S XH9V}b s[e\Ի:~Ea5oг~O\<MK/^C7Y?2.D1>(* `[,75yv#qҸ8hwnkj7@~m@\IaqK 8إE"`Xvc Z*4FĔ9ڒyT3Fj4~5+uxNDV[&AmS%=L2Vцbln&g ¤؟E3}|02m_G|5'- f%R9pk1Qdt|ru R=.lLVD~ő W.匌`+Kϱ_ynPLf *İ?״[ï_.B`L16 xkXPx ׉^(8ÐY1_1AkdվM鐦j_!áݣNŏ7=ooA`z_?<ީ gY61d6ۣѹ:CN=!C.R)ZS՞A| I`w#0m{ԗ_SњkJFQݲ֌eCExn|gyrq7![e 8t3g^}o0~.xPwO}!xhLy֎,.^kp90_՜|ঽ/e@+E"w!oL>5n#MvBFѯ^^]}AeoNq[K;=v-P8nW|Ym㤳\izJ L}x(,\/ߋooW籾g=_W$}V0J)`)tPE(}1CSg e,^Gz|9m6Q7s4u+ ֙eĥ[L˧f989̡6g\qnjSr65)e)fBJVwgdss\_/팅կ#JҤLY+|A6 kMK꧿{ sCFAa Ԛ9fK?O28_+2H?>!Iԓ0>t#&QXvO*+?1/ iu3XS6oR]ԏs̽IbOnƼTu(_7Rv3`8C,z3bIg_;otZ70TRBE`ueƣ8dEuS]!qΔ:NuIlWTZZ}[YFВCFrA}QٟeT %=n2ځ`ak1F"7=<@ӌZ,W˕&d7]?gPɇp+r ՁLC,Ę1w pEmg_ 8<F F݌ *uMwhtKC1D|;cԮEHfX>Zll[ 5EnA5onW㈉(G}ܫq̃!X̾37ZȽ]"5R=Lfl5]/g@y.0 OxX5f&J;ɭhdb*^GFGfeϻb$`ҍ8pwm #_iNu%.)F Gi#1IOjc[{N`p tӰnRzTE"`93'1P\t8N#qP|cEeAoC;}XvdQ~׵ƙ 3};epe1I <.҈ hַc_hR96 ,cS4z9hy0 e8LT l' 1Dd!Tf{.߽)<}Ƣ |Jbcl8 lr_ Þ4u"Lпҙ^ ྑc1bt!{k" (3 gbm9P'Zqή}V^MK^ ?%R'̸DY9ybaȯWOLFŏޅI҅01[ese*:e]?;w%bzOl\A-߭Vd;-΃7zewdmk*cK!!YNjfŤBobsbo4WRaE١og.Z~dҕ@Q. -e6FJe+ƍItp r_ K"ΩBLIPf}\z54iRQ9b%Bmv +kZNN9 m;o)o*5M ɚhf.,Yd넳2MbRh5yՀ4-ǟ|J|ٌ{*Z|SEk]{\hrԾ/pw`;a$hUsb])ËteۋXqHQ1p{|^o]YtZmX,]81frƞʲOL/8T&x8{B\KPB&*޷I_~!Y1R֋ii+'k¥x'5jDE݇ mumgG9:Ӻ0mU-Kǣ_=֗3ޟ-c_i FQXvʢ̻ݤpzvr{H~ݿ8B!2w16ݿ}!sFV=i/hPXmHy7%,6W 'EGF֗(-SPT;pSTA㮙s}T9ѤzoJ1q K({FPm-uB QTŢGhaUW !$ݪ|+&dÁ$}H~)'Xt#j˜dVœAS[>RŠW=3LҮ{3EfbX,=;+"$Wqd2V~{v@PVzmfbK+ʊn7ɤȹ֡љB%s1qEC xj"']).q;ڍ -jVL)ܤ K—g-dS9 ;YYr3 Jg@IDATw16}\ 6̎^/'QjKblaif"W@H4¯AŨixtRKr]Hyk+MY@尿G pNw )vfΕ)x4)Տ8N'Y,E"``+oۃzCR$vqM7?Ľ#_&>Z켋؜~3`d8rc-{v{+J"qF9*'D(|%FOQ Jg0{(&v>R4)$Mn!7 ^2K 2*%T؟:չnޱE"`X,Eyg95p2?+{>D èӵ-~zouszZ0/}UUqNJVcֵ6"e%0@H20l$"ຏKpQ.X,E"`X,E"{ ->}7bIcxٿ`_/ kLjho¾{Vin\ sӪ-\VC_ Jh /ofYF_ M",d]p21rs[E"`X,E"`T$Nx|"=2sr|ŝQ`eނ}EJ" $!).rq0)*c+1uZYs҉KӓgLxE"`X,E"`X~$ rº$Cg_Cr[Xx9N{=wνe1^rMd-?ҫOJxHh eŭ1Ik8+/f4}qBE2׽Þ#"`X,E"`X,]>D-g|S2IʇP^(Lb8JWTE+E7!]P09/4A%SHR85!$6 fvمE"`X,E"`X,EBµhlE+,\TZ[+zi VY kW2K-4Ơw)D*S7ZH~!lƘT^?tbX,E"`X,E"`!hdϤHeLdaEzE%4#FK\M})@y$1+E"`X,E"`X,A#Mw0[h4'/ yGW Tt} ~oR|,MIX,E"`X,E"`XS؂h4Fc4B$4Js wG"1=INXwb:C8*|SLZX,E"`X,E"`:&X# FoC9/oHlUAfKzȱE"b/AWI gf޲g5E"`X,E"`X,!O,pF%QmUʸ:JƜ_8,{dGqz-+yy:7"e/+E"`X,E"`X,ALJ9>yѱtkDY\3 ɯK>,Wg:5PT, ʳ7( /C䡀 *]HSA& %!MΝɦYwuw0XüM\$Qx,eBL*+1KrOGƘJ`ތxܼ_F9lTƓͰꊏk%1fys(daye)JX؃p'ץ\\. 0ǿPuf?yIh4F@#h4%p0ʊҫo( pEGIdQ4f6rs1; d\L5~E kX"M"+7&olލ K6d#oظ} EXx#v`w\3(*]٣},ܲ{OD c܈A8z$ Mvywj% 'a3֥4@%м:<|X<=i2pzWM.Y+fal[qg=~3 iDڹX2 ~*@&q&z^l$钒fm@qхnx [^HA X0l!-:WΚ~jJ^͇]*J6ҲrUJ,K帬v'QC?^0aNR,1׬&3T ɬBĤ. u9O5Z ?&|O6?0GA$/GVGEYPX#đnm#ǏܭEW礛ѳu3v!K}t ~TC1u@7x|y702w kvzi<Կ?D΅5fo5F1a0%,Bu0I/+c5[c~B`=0 +p왇ixH.yv?Q;>+K2ƽ1nX8~0V[7JjE.^rlMiC.<)<ތ'%k6# c)8ŧޒl3an ec1xZm1a0KɒÛijb7{`GOszެ?/ cnQ/C!Mz?8U񧁼etWX{FX0ssp*,| {v&0qU_wzDnK"]Nm>7!\u1>u؈7nx幛@M- DttoW= ;fa7J*I-0֊@a4&Ya$ Yer,ZdIP =S^m|`aqO ?2کk'^ߙ ̳SKyK}x"۳.&7TH$0n"{]pZ/l~ۊʉ]y;<5\3:Q|ir0˿y НHl4F@#h4@$P ϥD_+ЈDM %ⶑ0}v-,B_NrJ4驓C p1kft4z=x" $VC`3-MƼNFr! {4KEk?=%+M%{X[T ks.49:$tO^ʿq;`>E'AZA7TXX'% MЗTՋq_p$?#`~Y4.g=Uo3 |[c쓟pؙor0a3׶tiSh1Y\RfRdm;tǤ>< 5A|="^W}p0p M\*8w||,:e8N03rUh|˦}r;v mTyiI%tggU| ^j9|E|":yt2i Qym 9vCcla[X#7˞u$>1IxQjqŗP@-γOӎIwHr89n= {>FhY;Ick࡛|vg }˙oČp\Vc)FV_c׬G'GΝrq{bbIV}hO=CCOEGZ(N c46CU5q=Ӗi09mY^碟9)sYR׿5RTvoLlzùy)caYbe&;Ԩk?\9A0"n_6+? :je$T3ef=,[VxS7nx~azv%RKMT!7OF@#h4FJ[POË$1:?Pʑ/[%$Ȅ=f± n!rFS" 4F!8>Y Zʵ%Tceƈ+a|Jُ2WN@<]ϐЏأʉxZC}ms}Ne*B -¸<},Gb\ lN%k)R FӋ] h-&!ĉVBz\Ҟ1}|h#P Si{;ΊM"KNэ>MsYn_Ym+~y;/*f?ߕMʽw`/< $>z{M />ӴVyHK ʋZceƈUfI[-~tw wew&Rf9i/3 sEDG%]<¥Ae7[;Դ<W>1De+q4o"zZc lײx>+KJj+3[#h4F@#ph\x&:*f #sЁWEN~q_F&Y)bc16|ܶm[ Fu3}>%!t ennWH}]#ph"64qhTXfhT4cK\dEOe[Щi՗孍;;:F@##?㥿<: N迈.E)ϖW(܈/eX|KH?7'b$"nV0K<4"%* K-3iG5A~<@M#8Hk|6>sT>ޚъ>Ac,O_TS#c?mF@#h4F@#PIA?&:! QEIh(JC'9'uz?+Z9Y_NxiF綑t$Sv$pq1B\m Wǥ]/#F@#h4F@#h4@H1&nHKV *WqX^@+8ǗC(IWq3(^Vn/]q;fe0-F@#h4F@#h4 Llѿ-Bp8rr -t8P%ȒF;hu1#dd'$fCF@#h4F@#h4E䞼jE+_CZځWTqVeZ~ &d8Gt(&ㄖ:/,)B}NӢh4F@#h4F@# j5!W$Z"TRVp:@ϥn%](Yzɍ^D,`,]&a }KNs2IRF@#h4F@#h4F\}wq?j`W˸nJ+ʛM13{,Ir}3vd ->h4F@#h4F@#)Z%9aGx㯜b)F^F槃;6V@V$Nt8 RL¹4F@#h4F@#h4v"\qq9L+@\B@+Z9L 0wnM.,K *.I+/%d==}h4F@#h4F@#åFp8p{INEW_98+9G/+i-u$¬[#h4F@#h4F@#PW<0/II8pp#??? 4Rus"?<)Y4% g_a -59۶mKk4F@#h4F@#hJ!pdtx\ND䙒pMٲ@+HzzeYztϝtjo0SIE#W Kh$DK5uHa!ha'AKZ 项[E"2Tx uqJHO 0\R$.nnӱ~!4O~~~(}C6#P~aZ!e[v6uaTwUC)%Z $itH(뇮z|U~Eٟ!/oeTnǽ++m;Pv}=|烕jo)Bz%I+z +,ܫٞSYR-V$Zh^~: 󇍸(l8 HVĐˠLFb=^".wa"@$w 4+/͔ ksBEt~&dO eEH1%HYAĘ^c$$XBn|/ JGQqٵ>J~3Zlc+]?P[]?t(]/3~qCʐ?Tz>(R`ܼhc"DfU%\c#D!q%fqI)@ٖKZs5I n ub^OvKsm0+-0bYy rqde㹶B#Q!co+$HdrՉ1r1G1hef>-e崍cU#B Z|խ@чeX׏C~h}ZևևխJs]?~XcXYeǡ7==/ U|P׏^RhWW$;g ܒ>BGI"COk'D-Y6W>rsi >7sI8Ū $^"!a%&Xy%1řblI+/qlhOA2y*)ʵ.lr"Z%ddai&&X;9-ׄKn}$T]T>Rh}h}(VUC+]?t{J՟ZP~ՏQCWz~nO͗h_).kw9M&;xqLK@!$~"ıp%>Hp9Irj ,w9ϖVs+&~[jKK_AZs (؊iUZ ɊDK䡣ɭ3%UjH,3bC1I[ -nO W))\d:9DFH^"b %GD0H>o$1}U>" ySg> ~J \?+5>]]?>#`z~7_~8K03~:kOʶt@RܚI΅h}6ɚ".wLDX0evFI]I1vCK..yZ~񤛖^A;!k>"ٷ2!҇X%'+HLSܵe(aihZ[xN#j%19*M2D&>FV[6 HegK>p(ĝ#]^YX,}CA5v)8$!eSD׏C~h}MС^i}h}p+u}׎VTxWêx@T?]?҃98P}`nUʴW.pbI4<9o|< s(ɜA˜˓pbX1Km+߽n(+4jdMS"_4 Ŀ%ot/B yCUII!n_c-aV>|峮*E|EVRI)q ;%N4J | ?';6h/(Bt a5&Z!7YAtT0Y(B>TNGN{"HsV#F?eUY 2CEi3p)H#]MPbg?~с#U> }XZPmnܽ7 i,C]?Jt5^ڡƻZU}ds#Ԧ*Xij}W!N4ٹyHDI|ǞRUo!lϵύQ.5V}3WnEk#L2?/w00Oσlj?5#\br"p)eX(T4?L$+RRRۢZ5j"9/N{|ܼ/"b.nІ&SlCUNZ Yψ(jQLU%Da䏊a[V{JdScHbA%qI|o9#%;s1@ c?dY0=Yfv px ؃'CD0WPb\VTv+敥;v _~!jbb6>>TQTڪ'~_򍫮, U(s璫2)u.>?ҊVy{PC?{0RG .vb&I_"QniB6DS Sf:* ?c.Zv4>tCW6Aq,ԑ?ԋ)ݟB]?]Kz/?dZTTߜ$JpѢO+\<(O8,5q7F(JvqB*5/DĹ%Ų"%,fWشbɩ$,=i&MBX:c00,1-efrE\)Ndkph*$ȒIܒXnC 3UieqZ/"D3)-v)"񬵌R"k&i9_8-I#hv@+E (a [Yè=~[^VźLCA@_Xz>h}>R?Gh#iWc87܊S[a/HrՏ'kbM!rG*IKMRE't#N\(F]%1I8d'Cfvf ,R,dpyKz ė]crf#spddR>]siuQUr6ǯkb6@ue÷y&n*.1-sL|vc oS 2O=Yif8h|eO#6M_崌{wY!"w?TCarETo[Y6+=oe ,.4U봅~/>]]/ ЊR|Ib%n_>j*"SM)fDs=HEioGҷZ LPѹc~/wUP.[:ief!CI,f:x騬8FNo|PL aԈ2}NL*¸s:'>.)m(fb#0qn U=U~~.^d2K1Laڒsmy'/$\8~VӟG&geU~*Fqb=¢4fUܺ9OÖ{t0g4jucz2u~̹](}jpӹnɗI;S%qWjAzR &OȲ A-NIE-L֤>KrP̓ S"ŠZXuY (%&%0L"~: {O#d^ Q@8m\{gdK bX!ޛd_Azy=Jw"*\e\i>s>xsKO#7pW;=_`\Ai ^v}%"(??>>UHsTx7y,+].i;s7^0Ї<;[ 1]o݄GD%s^"GresbWVХ]c#0wcwa>pk ZPr+лg.cU$cB~Cc}pS>Ve{±:l$9_H |瞇cHzGiR9*;@^']R>ה}% w(lb#GhJ9%)0N"cIMJF>E$rarm<ŒIݹu^s&BT<]V{a uoi)kƕNQ3pyѾu kLl.B^H EIlڴݵo74d-o\Lz],WK-I\g)b&Ddv's 0UȘ0_.1G8@3\}wac0|vV[-~?řsmhX/6"̝~Qo'!wL5EsᬛŃih,,5vOYH=ٴ&N9ٓKk%__ wFԭoV8v hz]sowhmhh~Af}~{wYy񻜋y }dk_mnǪ!KGe$uoǛW~>n>վ{h(b'eP'EM T$Lief N!E qP@;K[]>$T/EqhWZZ*{|_nR@IDATܷ_2:#K#'.kRa'y=|_zÃCGYvwbt6X387?ģͫ\2ڄw)it-FyKGwI-% 6kcӓ:asɟ}|%IZeᘿf̈GQ`ː>X/x*OhXNyfxk]bk(+5Z&'cx52=Z#[?I5넋zBfip!O AsI@~0h,wyV֡[S =X[Eܨ 2ʘbIlpF {s?hP7Eecop|h?”ofʻ$ A^ua?R{kcx[@jFQFñh.g%ߩk]܇f  ͫⱊQ-tb%bNY#\8?`v>hݪ*g4N)0 6E]2$s z?* GYjţd㣾Izm%=o5]b0p*5#HzEpw[Zofd`@LoˀuobM$\ss?[(N"ˏ?qFKEkņbCA-n{{Fwձ/:~!Dp aZG8 Hz%^"1?ZUd3c|o-e12S57]6Ŭ7yV_S>߃ ~Es8yP4_XSjb2{I/fD&F3Ji<%%{ŌFDUCkĉ! fWVv6pf#l4v-v-2x_L]~XΞƒ b׫3IzՇID@Ud%Ǿ&@n*LDe9E_\ؤ ,i1hFH/++}\`%+>\G\mB{!*^HW(_^m0ǀF^0f` S_֢,iF|`g"c tn!qн{ǽϫoEEmsv4x:;]5fG<]%M 2y=0_p'7w՛5Vd藷dBVcDz7o}X 'lQj^\抳k)\dKL =pWiM7o6zVee"_ޝixa^j)ӛ*IӮs2C^ÖcD;8]=I4e6s+ͫ'^o&r!$vwGbʫ/a2] Ī_G/m33M<.dPWqs530s[/VǼ_b' >r\um5.sC7W̾h sB,B]h|kztӱ看?'+r\3psdT7,x|:4**څ_?o y^Aϱؽֈ&tdN laJO6YD`H̠afRXoQDF$M,"hIKK!Mb qfjZ"%ԏ(0 P%ɦw>̃"L͊sCcJ{+EukGmZO7kt>j6@f'P0#4qí$-ἣ7? IFdz8g1ZVgBF_f'mi??͵:՟?ߎ}2 VWu(՟Z\ ɫqg?7]WKd$䌳H5gcôOl vMGH3zж/Zm _)յЉ_q.~9IڂAkZwi21 NZ׀kDa\CF@[`#\R\\zyKGбE=|+OͩJ?ؾ*S[|X9Y˷qX\H3P4I$VsWAKNScͮ[>C;nH EW ~5'܊I:<),_b$"ygt;X8bbp^3󙏽pVނNxض+X(Fkޡ33b_⋬^ 7ć& onqo 2ԸeIƹ-ܴj QpllUPLP̏PQ3`XL}vіȧؾԶ~ ;WzKJ a8+-Tf uuG$LmB!"WI T; .X}*4) [>^6=˱}2 X:sک#aaM3P㺮h~9XY6Z;3u3j]%t7IJvfx U!WP"䍀PuwI+2 (Υ3X#\۪jQIP+ʐ /=,)UJF:\ǡ^i}h}^볍t׏4Imrh%]}$2%}+z_D?簆Y_WײT""jLܷu.:\yHB9r1Ȓ<Ȗ$$Е]\ɸ| <) ~V,/3EC8NxxLxg~wFm)p|I8| LXۏX*28wqay -3mMZvL5 ݙY9Vu{׷WndUo׾)]l(3B[U|.iZc*XrdX8v'<%mp6Ʊ|Jr)Ķ*~'gnwCmu6; g#)Ώ(+CK<Gߗ/{΋vhz.4H_bf¥$'_WɋVU bx2l4>'5'Coux8Gҗu/ $_BAqgTKnZ X*IIlcQԁF`T_BnIzI'"Œ` *=Rpj,G^HEy1 npI8"DMǜ4>^6J@J*#?ǁuܸ+˽Nض%ds} <7.L#Lj=HūhHɲ _4Lo&V1;mΌߔ얲R=HXD *v/QXυ|v'-t>`Q|TŇ; !D_i]ƀ3vC1v( _cǨi_οƟenqyûhVKadz>}q)g_x]ojʆ0|Kr0apq!MK6K~#NZůO8#e锷܈"^?xJ8L=nN`dbpfXXi#x2Q^i}h}=U?Jꕒrs4ggaoGqkE%wՇ{fP Zdw?7Z8IlqƅFbq|b?6ƻ%ݟ=(OjW1)mX3jU<9cRA2#@V^!qEteFdQs_ !0׶?7&*vGYo Rڇ=ǏR*M1ye uYƻ)w$^Vn>X>xB2 echDW2漱<_ݿ!~ټޥuS,<)~_I&i\[b:jc:[k|? ,;Pﲓ9Q?YlOe@lshHWSD^InRM,^{?ˀ unOHmgv{{Fb‡(}P⸩>v8Kk[6(WKG!ǶG8} Gvڨ׳P;Xw~zADٺor}9 8dp3FEl3W:)`1ѷ+k~~惒3W/q[sl1,99>-0D |,pӢ'Q"tzRu)Zĥ)NTK 撅QL6m0u\ie[ڐ.-]6|O \[h&84'?"CHB%U꼞& g>CJ{c$dyԯ%!)'H""o$/D\B Fr40v>ʨA+9%H郘XR>ݓ!,O,]r OEcb_&NG Ь%[{)d᎘?⏏7^M*3ޭHkYVdIr`:6-*!,LcZ8s`ǒ.*+3h?gWm?`"'w9Dw!x<:Iƙ?r.=|ޖ|^痾F1 pll,!G9p'&>~_qC!d'2KDmM~#dX|{P;_U?eJDAHԂR>ܐ(˲[KutE!^i}h})해 +N*+uA[/x}1pBXhi8&$}|eZ4-++hsK*+o'Z/a\ZF^\a+W,]MʩY X*ƹVYenL{ê\vN}3X6yfaXhpm?` rZo?}ps\(Fn,6.A)ᗰyW!bI$b;H<|8?.'R2Lj\L9˾,=7, ̘Vd]xUeQ_j{ԓR}fHQ)h#pس]H`Uq)tB#F,%DE+c>OYNc8BSHo<[$ES+qVrIIdRu#pF"]Hn({QrïW #H ecoD3mx=Q̔fhrmO~_ČFC4zM/40IPaJ! .+I<Z j~~惒oUg*lmt"Q)b?&UTNm7I/?")kCX HmF>9sP%.ie[|lٖ_'>c->6hexj,.:8uQC^Kblzdm\/_xSB@hZ\Q#?a'xU\Q[!,;H/$Ee}aN"%!8ZTRwCǨliq;f|vdz_rIHJּ5o'FG]թo}(1b~#bՔFQVyKr\=u|_i!(SbUtz)ݯMа}40.uyt~+hph2y={yiǥ3PDg?5+ޞs6 TQi}G4MGZb) D,KeUaIU!ɾ`}7̲^\kIH59Z~ʕ\ScGY:IP?DgwS2ҶR]ݣ᪃W1Ja^sQ VbjlnVaǪI\v=n.'>Y?ā5aҗ_bm|wՠ,~u> [) ɕ"s~eXlY->cZKtS"e?|a3#墟Ѳ%Ƌz5^/10e?m^_>dl4h߃dLBnz{.>J|6{0>Mͫͼ_U|OCw꣢CT^ydɡ1[lu"ifNˇ:!#]O>_/+;覹ض :y3C|~%Dw""9+3 8olQ->?K aXqػ&~$ulcߵ8G]j޼[uޑ@\4_OȮl4ܣWljX 9j0.4DC98|:??G;uÉtI#V -7.4aыsޗ<\+ Xy;>rGCEJR tD[LQYh=bO>'߾BDR3g]X9h6hN,,Gn&{쵘:uKƛȋ)׃n#y\)"-Э{*wMY<:mĭ)2,CBB`=5mi^ޓG (t .?C} sxiX>v=kgcB'8Dn XdufD*)aBw@ ^b+ڋ4. }H۶mS:H@mӹi8X9bF!VvQ5Y[A&u]iUث}G8܈Mc藸Gp頾+NȏIuB(5,;~/{V2n@qYGptgt:M(?| qUyw X3ń - S`Xp?Rdq.7-Q!b}pg9uNJD@?в޶ZZ* tPx ?dBqUZNi%u+ZF{qhZZK5^r_~ﳬ= s,\?]}F`OkKbdj_,q,e(a1c sywt@jgw",AĂFb~Ѩh,Ĉ!*H ( !vcl"DERT$. ˲mfޔ;;fXu݁]Gss^xk~m%qCy#<tc|^[DGU],nɖ, e(} <=Mll݊fDaXUQN+(<2 }j,j 5ܵ!'6{TE"t%@Xc'  |C HC*`C}L]TW2U}';ɃOqyan.uP'/N:7Mps~.64Inݸqt5 | wf^ t؉2p^i}փxNQ I7?$ap{{+ BL<@R1 3ȸFDb|)CP. ʏ<BGC@+yW6(]7Xz˃Ɣ6-je^G&+ { &Pw8">Ԓ8#_<|brDlW`S Ο.TئLbk?A5bl+5X?6UM—o  A4ꒇ$!ژB\sÕcsى臑5aF?xn_yÙ=n"Y]("0Qnf<眲3?4bwy=a#Qͥ+|nv{f8]?P?aH0ɳ>gW0w{gR~R'sփf\׃` 0@a;<0`hƀ{{qw垸AL:_5kK۞6mOP?<۷Ͳ `o_8A%PXdipb+\A'$S'2Nc,&t T$wj*O1j +j7*PsϩxS$xbӪM"(qPG2~hP=R ג;^}l>h& iF8pKpc_9 X hE  ‹SRrkq" - jtɤG( Hw4<$_%ly( y?#4[v~?l|e'M/:`ܽTii^b((d^& FU1DIEKH@cE^S$9ݔrN(NSM4^+.NzI}+] O/uQ{!R@/)NF6'7+3խr̯>1f?~>ɓ~0:ccEl+)1 ;A$^wV^~WJ0.})XUɭO͆0] /D A]'4#:6ax\R(Aff4LNe1 yYIgr7dScR⎑| /1|ps,~' Ud^VmJ"I)~3MF6+<~8яt91aJuw6gtH'fa8m׃F?ܭ9oa獏E?VoR8l[I\hD|wE"ܙR1 pa/,e@2?! `J^u#pNMztd_`zc)+CjȪyb| BzeK0y;[N Kef|%,dфaC|d;f؟3f0G0ۄdt]C}?lV a獏E?'x7e) T9J~[ے #ۈF (qyIR܏"\T0"%WY厘t'G-k8A( gIɢePfkRU | flˬcNUXa0S/5O,bH x;'//tïTۮ`u1)da݄Cu3~fo0S4ӒKm0g:RXK\PX?bMnZbER۠"7cѿQ/xh5Je҇BҢKXY0KdB~]r̊5C^ 2M(ÓNwvY66v"M$;mʲHVndʙɓ|CC,(<~i!0ҍG2_:;WKfk3\mF]3~hpbo_)i*Wi~47$]p46^)//w98ALx9.r x@B$YzK`%nQWIi(Xik11Q 92V_KȧR.,?u}۝N- 8e^4QZkhuڏ2a %J.S[x \&!ًL=GӐȤ6jC8T5~ȗ1{'A~7JsTyy.Xbe"cGy*Ɋ~7fKqj܏Wvűh< ҄t!0v uYm:HCg?W_}ikz]Z#R[_ĩCKvRuZxbm?;櫗U֚hHu㉳mOEIf} >[emd 3>6_%J _>K[a~%o'[uE-1f#xan![+E~8(Ǐؕ<L^u~`|+' 6P౯IKܳNp>LRb$&;%w]pzB@Nz6(* UMһ);3^Qc,*Z+TbمDܵ2S9nP."ۅSvJtntWPbTF_|Rɛ<ϚN;@IDAT:c k\f[5 y}l*ݎUUiejGlr_e[N!l~T3G?R̝e۷`{ªS?X4~İ~|߶lkJeҏXGͪh= \}5οpi}>J?Jgu5kQ][>_?wsΕd_QE)<5LC_IDtRevv~l[A2U kmb&  Lܚ۰n+=퓕c{Sql-A܉o!PCFazg'3~4q+ `OxnWCm_530^ϯ?Sk!c̯::MW?t;L){R I&ATDm))ɉS8 iu.XO8ޗ1ˆWi@  Ngѕ1 7BAgH4rqp~k({\ER.2@WV)Т+b^SŃ咝WJǸ: 9zMzrd`{QKh!^VH%aMV@ Sɣ|է`)-cC;rU0?YEa#AV*,;҆*SAׯ-Oo~bcʅ,JTZcE3J0fydu)ZwEi"5P A7rfR~lq*Cէ~$*l] ]!w@eї|{;~¼UX>|vub/7`*aɻK`C1Ͼg ;u8~Z㑸[UʋqAY9X tIWV cleܧ*U9V.Z_Cn1d!( YG?ƪ-UhY&qUWolw0/>U[Ēu_ @>aQ="?AdhKi[+,8SaQ_UZw=CD%6.cǂXY0U%97[*q1QaG`-(jD*j)y'3aؑiDы60""xk:*V# #~8P-^ vD:nWd_IɌFכxnaa滻X|C40ύ0$c/^L#$0sOn&伾rпsZsW^q)`'>o%Dc ezaj\5 >5{^XM5`'=B\$qzkvn^|ny$z:',˃Ç ];yoXphY' `L8ʃHCyx$(5y8ɸd`kߣ.C'A@Ta9ymƃ OƅZQw~F7pbOOxxʣc)uӰ 3}A&a|r:[Y7{)/.~WNMú Uл8}iC׵z+&]x(-%[iթx=$v#t韹Dӕ%㍠&t,ٞ1 >Y4f+YTGgW밼X)o8 #F Gш1%E? pĉCZ=$NGQ+uqJ?:[ZZș3̱g(|s1>tZS?~pԳuQ9}nb̨Ӂobb`>SFǮ ֔&& LXr16 E@Q~aqD7D}2bo嗅ʍ'CԍƧ(j LãZ^KxAG ꯔ* y8RLXR?N\wϓ{w:a(b` P;zi]NX[ DZ yvDQ?? F CFj+{5tEkƚRwpȉC('`OGDMA%ಇ~T #z9 F֨di& IG fBYc0x)ٶ[+ӝP\0h.rLC BW-xL-PU/8ނ-joW?՞Y2a;yr(zzXAY3.;-Nmc(Wӫ݇}ӑUu>V:./2' ̾n-9dǪ6C6сSE+C%!4+)3-gJa ?!]y]U>}qZ`ꍷ#wO #fh*_yyc w_q& YMl=`~0!?7^X>n&F bqU<ڋ8 EZBW|T"%=JIR.:$0~N0Bjd!DH" %~r!%.zGVʐbL^Jݒ?e_>.iFziB*Xn&EtrB2IRs>bA/AC|K(p.yt9" }nڡ#Obʣ#rlb\9I1:|Tڍ+֏Opd0EUl(7ew#-\ɶuySqJ 4DleyXHe]ՊnJ3nQ oQGm,Iq,ۈxJ]iyLbR&Pej=P %Va(&rfX誺/.c/?[n܁  O&Yefc[k:Nݏ[ )eM+[ _u$baUdd[~(ڈe|8CggɶU} =i |~ Vh#&ĸ'?LDXU7H-F W|YJØ׳6D5Xh^M?:+'*7#\zC5&#4?HH)Triڂׇ^yj?!oٌf<Me<76>]_q0 5яo Ee--۶;_+ARwuxiSAOCL1_+k&cx`#YƇ'ʠl#c!w4my0 1m8iJ_@~@ڕ~LyQߨ̟֏<2MEӺ)[}*=!`[c’מ']Y|F/`}5CiISSW3T-~|?U?x|PJ6ԭN]vs`*mD0a,Y$ɷoDE}z/,FM,Bp_P&, gu݀7v|&&e[9꛱l[AǑ1Y On=zgn4;-K  D/æ5VX@M ScԿ`*^x=2rkhZgi+wjxOEB7#Ч[1bv`Gf{ 欩cc+zcnѣqS!ܶ>jjk믤M<DTS9nvĬ$\AH}o{?.h2~0n&Z9:*vm[_CPaa8P?My~%5|Wy7鯔iu{=h\;~KČZs*!TV a (+;υ\BQZ2P'{:Z렌+Ĥ$E,$( ,/0('Ȼt)t:~͍вS1-Ŗ]e2b=3*9uKveˀY|p1j\G{4p(:ާܷhE;<T ,'s򈳞)&a}jG.gb0_yԦSt*:&|cm`=N},rYc{N@wz8Cd!7f,Vi5RL8@h-Vagcic eBy~zƦn[䝄a yG <5=d`oQ]Hta1 h>W,f4VU5ʝĩCVr[ ӏJ|+GqVpZ?փ5#m9M_ v#X [?,n2 .:[E{Y~.˫~0W˟DfYG= /r7t՝랸CGgf?ȕzFYAw=?g(G-~dؓq_?2󥯟Ȱ[Κ~l)ْ'4gWӬ?4pjb%hߎqX7tJ/W5S\4s/W '|\$bA/!W9p'!œ⌛SΎ#4UDd%:r: z̪Z^qPK+&M~ZH02 IP$7י1Z %$WyjhR R.`= P44zutMA>@>C,?B#T: z.yڠW<$BM~* (k$yQX :4H?JOg1量I?XB>^9T:RD|T9kЫIu%W5r*s+v|*XoPYU!e:_tD+ͫ=ӕ5a]U!UWH+='\~ozF\^tcwEpq^p^WgN&%֌mcGޅ!3~ㇴ,p:ZChWQ~<̝3F+ᤙ]k!_`sZJ5w[iKhUNc+9`|LH[I%**jՒ]8Yv ?zG0_t/ۺ Ŵ!"(][BJnb"'02qZ] A#(xZ>˝.}.]~C9FiwJ*w80\HEn ,%,=!_ h"1rCae~KX<[dH[ȣl:wc *Awq٬xEcl/ߖEq/eQNײ7y@R,h=+ f@/^9#%$exbnҗY#T+g*ΩiB Z!KX9>jk4.LQ I |C!zBF lg{NF?lM@?<<^8aWx.m)禿8>3U n]Z9x̋~p_5-yh<"ɄJORP.X{LWB yc 3&Q˼"wy$2іhb5EJp5EbEG!Kv vwyL$Q kIEK,1Hlfʤ7!kzQ7?bUt-$Tly<~[}naeb/gWf<4DW f.i˭%f~eWfhs3{*<_7:v@ם;Ku7s䮏UѴE>go85D%qw侗 $40.VB)m 0n R滌ՑykO)~ڔI<4IZy\0E!:E趙WUyc5];i3ur<ȍ=$MK?<75UDӲ) !$C ] Ș](䧋_Ҋ!9KG[1gO`|B:lgqP% I\DƀK)l)k.I .,Mu𹀬5lqbslKe]Wz'|^i"y'O^U5tҁ}rM("ZG i0N?2>W$eb{hGY9. $ܩ WIP܉/yoy %G#=/MK?<<Ԍb<7U~6nCCnGaփ/hA]F9y[7[iK- .!ELX(Njrt D] f (d^[TH?5ZJ8ΙDn2&\hbc9@7B0a&E I\x2\-u1Tdvp|n>mFO&VkřNXeXeP1 ։fU *FMDF?~yy#=\񼉏禿o8ko@PQ0f4U^7[՜xʥ5r﹞(HO_]&iUZZiEWJB<v$X$Q~>&TӛOYט$|a V<;+ jc:#X9 i (g+ wy)7L cyUR( ~:1:LJ3t'垀]bf;~Ǣ[CG]VqVQ#roڴ :u2h"0яFM200㹞d2G~CK;w<ٔ =O6iGC{>#a>7=~8/x#9 *PF"IUywTPF.ȝ ,4h%6d8`8`8`8`8`8`8`8`8`8`8`8`8`8`8`8`8uP:'-ele! _5o3mO%ŰUђB %T zyCr,r%ԠKSϞ=96^F&M@MfdEׂ畽&;ֻ xd45ʘg90|E ᠟WƯ]  #α$!Áp !$:5%{Gljg%Ь,V2,sc~ոv?;                 Á z]_JH-?W}+>B)!ukyZR}PXu-<?Lgg+099`l+qPAJLPʛL0Z~Ad,x2TM ]y_BOHTvNp1AJ<ӻ@w_Á9n'6E܉VXd\%NlKkW~_$3zLԸg#})iN&$=@bIe@$~Ds!ÁY' "a\PHK!}ι^ש:ۨiR )`̙ZB ůheSm~kQ*I D/K!>Dܗ0mLPA&9&Hhh4*D"Xq_{G0Z!qk^ƕz s$SǾ)~D!DX֠S%nn#H4pAvi̹@~9 FjWXQ4b"uэN<= ~ X7~WuWӽKB+g0 YbJZt} d@o_#A/pGŔCEd3              ĽQ,T =W˩y;K $>Ah=7~Wu̗,%tMO1G<4lЫ?҅~ED,;!qm A 0GÁ%~LK@/!s*hM'Vo<?OX)&(P*(EH$/ z^v|Mz Rxr,sÁs@bz%hb( nx_KmsI^/1'eCo_lH5A=2֠ů2@EcѬ;M ,Qeo^UNuŠzTwr`'K(A]ر+yS^;8`E2]>o0d8`8`8`8`8udr,]kKvNe%_?ጕ`ϱbTq:Ъ˱hYIYÒe`Zo~Ķam;1 F2q/ۅ)Ý>`nԓgJ*7o>x%ub\/(-@,Wʌ'hԕ&`zweBOC"ߗ]0'=vmxw xyT^ҥ@om,>r k~=^v6zB᥿ĉu3vz' _e[6 7vXD@oqٸwE_:x`#N0}-'g4-κ'1h&c9ylw,șTawg≧XAmhb-]'s#S `ѓx|[݂9 w]k~9NMߑė́5kͶB;VKjgUx}x?/ǣ zx>GWSʐCd9G URgZSI&D웢)_}=rRzpWqwi{LAW4u έ1Ǘ᪟K901qDK|;E*ǧ+u?pF.&cKo_^~6~vy.7y*tLʫ 1j􏠳*[.WY[ "}WV4f.޾98=:mLD/ Za6PucNA zu+Yv ?Ɲ@_2U,&jhU8ǽ &T^=Xt8p$?.Œq~ymvF3/z]?97ӞƲE@D Պx'PxTyQ}?0t x'$B˧=[n;bbЩz^߰5h!-y[yU8VIm\3$N?y#x88Zgۼ1/+zаSqNBտG|2DU䶕`;_z{Wѧ9|Z|k-s3c%MJmV~9׶ɷc軰ae'΅s(KV6^kAىxuQ6f4f8 f:ʅwF ?yK@sj!Qۧ2D֬sGv?F8ґ:/?> /Nkr*.|&[.N;'!)V_mF= 1x`mXSMs[\w>Lڀ9Tg2vʥ'C{o|9:ɋk3)Oe30w2 H-_ܾsOP?GZC1f!Uڎ? _ 5ɶX23f-bI~:CghX7ith{B]w߈oOp֙vByW Ȏ Jl]މu5#pCûm>>fY#Zkǣ_ž קcYz<:W~MLYuDDǃL/N@IDAT[Gpᢏ edޫXr3]']N|!BY{K= ~]og[_>^VEgW2vQ@/q Q@/!/cpYHGC4҅QdHsHZ %5u5:K nKxϺJmz3NJ%rr̚yfa.Zl_qz%Yx7pRg]q-bqc?JsAa_ZW'ǪIH:O=sr/mJ茿_TbcU2`,{ '4T{$+"J+Mn+ûm/( k-%ؗrRB睊?@t]|19gz,]>*āPڂ37P9N3giUXrlKn+&J'_{}ha2Zx#k"u_:Oç׾~b]?}#q'@?Eme3z^H0}Wbf84k6 H$4숝Ay/`q7a y8pX )KBhƱ%*s 3FMobH GoZ'CNQx%PG"t ~efθtEynp˓[=0 V,=⑵|A͕SQSNaxX*f8:5k}(lDƍt-{cjoMP񦜼y pֵ26g͇ɓQ }ſ={}Q4.CE3>/r+#~ho j57Eo8TN͓/Ձ=kzBe} xNdkxۮ#v#ːC&Ы{Zs>.O3%7Uu_cn_Go9[㜛~܉'o98?q}8[)݀nG2 Jgpq?oFq+ʖb Bd3V1͌r$SӶ=pN8~E\o?QKE]o_u ןǡGLA5[Fek6 e\]ewpI~9}v@Ris{~3 . }ۦ-#`eO~'+[]?rHіar әlBo0Ycp،|}G}1۶Oo{oDQ)BPADR`#؃5Q1~QPPFD(j4FQ @i Fvws{3ofgFؙWn{߽w9Ϝ+w:[[] /$6uxL$Um "z?!:t$̷+%n%CyU/a!΃]d][؂[ϴ[)NJ[D.%);A.eG3b?1Vl= ٛuWr*4Ŝ2M[Tԇ I_)|BǎǼ+aI>⊩Y@Q>/m->/J\!_y0c8F Ǻ*u#Tڋ5b'5{89Pb]G;OK{Ɖw[g:j`}93;65/'W ƠcEYb v.9:?Q<Oϓ!3.*ÜO wMSn0c98`\p>6'_Į@h_b{H_tCٹ'@$&Z> ŀW/6#{;'TgZ_tgl!vm`=7鳟#-NJ{[x. 9?q9%%>I/wX::fV{l{n#pi&#"غpMzIm0㴎Ni׆N`Mz{#=G/J5w2Vo E/$uBgIa  78fd(&Im[QuVĻvwb2yx2:]e_uX鯼XpmW|k}5-]9]98md9s.j_`OP\WE4 8etd=5tShfhYbIl_q׎e$L'9c=8#Nb : g\)cNt&q#MKע)7o0vHsVʮXI/10)szs7캱z_)7J_kX]@'47a^Ojմ07,{y|u7y|S'-+GN=p'|r%ǟhK}mRG(_(:"t>:jҵ>m"Y nQm2|\g)Z-ieBֲ ۖUS89vR6|e{kg7' wSմL3C|{4xYT~aQ$g}z޹MSG/-OruX9O%h WS+מdnzAJ2#bb&k뒅 fg V`\0ҐH2L5U2 |TKs)mnU -^j}ʺ];{ "=Чd;3Qh1f>>^>}MJ=&} 8aa5(Jɘ[ِ*b·0H ];xz9IB=FWFbux%"Dٱsk`VE-/^2˦u7גVd&7\mCirkhמ`ꊮb5ۿ`j;5Ǘhs/G$D~Dzۥ} `06|xO:gWz }J&>v e'`6!vbuzBV!_orՖx!2_k.sq9㷣pݰuVĞqLQh1,}ֵQkW1z0Dft'=<*7^`WNj,.b[-U/ډFԛ!6 ?w(s"Zz֣0V"(`~}~orN>xɟqYVcڑ KƍAp\x]q̻[GaqIkh]|'/A`'ң B LF/hF!Gָ{z-S6EӻNf0kt$]9)=[yocuNs4O{uF{p9!;y-z,ƫOGkYU/Vp{ 霙?v=5mEzi&1«و{[kS aIx$aTf%w?1=.u?,UitMhI<#{G{GheE6|OsJvZiRHΛ @Zc3L븛(f^'ib&޾5]:c ePz0ZqIk)|£'F_Y6:{6>mwgv/npk8Tk@c/9/!gĸllD\nuI6Nw6?AGVMLʄ]J0/o.CzI;lr)PTL*Hv9;;H)"$E6_I Щtx *'Jq9Higt؍?}ʅJ cu{HznKYD˟#P 8wLǨSIcNrwkg𷯍"b 酀sd6[14)=mZ p1-7V>:V1V =w&ZUOJ+]Pe_4%qC琘;k_a:;eW!X9m:m})&n k9yp1j{'!VET<8&u8z汅qi*"p0&. ~}_/g\n|SnZnp(V˛휿܆WaؕbP؛e_iY7;%J˽ɦ~8^F=3[g;AU[9) $cȏ+K*;+A<..Ƣs1gs6e'`_αœ\.` N :gjvS~|'8$1ƅ&}ڦvmQƺJ\8k_ ^SNb_N"'v;uǜS)E򓗰Hj"ykme ᯮ{{k{8g(jo-öxP3Q":W)xy;gsګfx:ԧ_k='XSGC /aEwOv!He=aɄvOo |4 %"W,J&q dQ&ɒe/.iF*#P*/_cȩg&Ÿo(ueD׽gHszGَ62wX:NٯKGnI*;h9)Jcgp޺bX^'Evgsn&y4G{ym^>z\l~Q,}q-vtoᕃ1ϫS;n/2ԗC[t/aBDt˦KهيgjNzunibfp.BI]P6 %k rso=  &ڗ{̣|{xp|_̿^+33!(ѕ rx>qvhZwuc++F|GuKzyeA),p229d%H{ɋ%+̯a"g3OpA._aɿ˰~ݸjbTDI|Id1O$vEVz"_8nV6R1-=P7jǙ{ʪ|XOW6{@زPS3O [W'?ݗ~eTguGCQ [ tY~_gc9בn!VNI|o^_>KÃ53g'ehmȢ6gbә؍)Ψ'^A$Tg&]Z=WsE}}c4,)\UQgP&ڌ7@՗_ČR[i!ZkzؤW9}n=,2k_4!ǾO2ߛ8YBz%Sq\qrWRR ,bTmA)ID@fk 2&bqh:N5\mo2O>s+9e> eG瑱ף.FV1kR$Է_tgM1ضn )N@#{+0{yF/e:bX9+w!8IӮay"dS}r՘PL~>xOe˞6vLSZYDI*&בq9/5.A'bIݽl|26CYہ=샋x/}"e$\ Oe۱n y/;~  _^mN{}ٕ _O^?Cѵ@} wcuuX xS4oڿ;ZcJz){*v1YԛG/@e#=P ~WSE@A"C0*10]KlUEz~;~-= 1V*t%CFG&rU9S1r#teaz;/wKc.EGe.Af܄&ރNEb%o=f0VώDq|nֿؾ gb! 广,~\λN`(1^/;,e/ⶉMEc᧢iND70vsD*SeL#.E2t><P~ЭXX'3񻜢Rkbb/0p*:P ?;z0/M䦥XvHUhZ>oiU=qOh=n:eV6v ' p{ +" oªKdìUQ$Bz] 5=!x^0"'H'Oc}oMz%H5H*+bXHAY_\s=`K@v3Q=-0W?-izS♧c++1m{_|=y.y;0r1k1änӯ$9f;I:H|k(k)Ŝ|aH> >{3:&eo/bqfUT`'5jtxn:_sj]Cخ^]\O+g{ډHrRM=1xP^~XKŜAO{wrUj>CѼ-(gp$-51)-͗{Sfcƭw=;l.ʖר=^cHswZ2K~swL6+=ytD+GQ?b=~zD/z Wgf${ڍuök&c{e0Nn\el`cU&4tc4_qX&{ۓb ٟ..GˍyVg2BQm;^8ausOE@PvB߅ mg$lҫ px#&݊k" ~#&ݎmI~݂gLUO/'͌;ӗyZ_R̲9O+qM0mv=j0q{C;!J{`֬tNq1+O]^{w~Ws.)sh'` 8:~6={:vp Y2k#O9=Unmc-26nO)r>g|ҹXֺAuh(R}vXٺ JvOH՟o?hA%.Uը^ojL4/COsv ˏ= G׼@}l{exLq|\f1[s\w-^gc7q"1 ؚ}+YOXɼE4/./X'YOI'rKE"~nMWE@hwD+3{7ny^BcWcgYl4z oܣ޲yp.rQp-ו2~bzDlAa‰ 1lb*u#P׸D"/F3\y'W~ ob3\NR<=P36G|5۔{*/y>ʕWb@zleBz%" nrM ӋVtLjh-e8"?YKDb5VMx9J7EG-bij2YNȳ%8 QTRľȤ d˼wk5f9ٯ(92lvƯ̠@kX;~93Q{OuDzޓu\$vrV]uJ}S#42۪TBӿ_[gqpQjl-__}E@P>)$H«{en&}Edoܣ!YC=kG4~ ]t[S`)kE Ā?o Z.+~'(Gk|?ػkn]wU^d ~K宫)!R$bJFHxYx6!dH["$M$ھ/ ,*(AR1J2f"ñ׷+V?w,7k"("(@];0-;AWԡZtZ01b{֕7 굆"$4|P n eX+Z@"pHG|vPd"4+%Qsr@toE@PE@PE@PE@9WccC$1Oѿ`}Uk\DŽ}k4|arM\|LWw$Tؐ^ReS+BC<)' YQ!c tzH>$S z9^֕E"("("("|H<& ]C+1rso+bU'`-D~,uyC>ʸtӵczI"1?QK"!y"Ej<,2K\=[i/%) q~~ ;+I^E@PE@PE@PE@nre:!f?1_;s9r ;TkݑqV%K"0"ctT_•#$#Ҹ!.+"e8X`^*hIx ?E@PE@PE@PE@_ />Z^ZT~1Z%rFIXtTH2HA>i!R43KE-`(y;W\Qgt'UE@PE@PE@PE@P I/Ia$eyFbS+)r5GLCq:=43"^ǕIp}Y"|d߬ y Bf/E@PE@PE@PE@PEG^ZFhhX^RM㯼t|YE3mpǺ+7Ob~("("("("(@ ICn .)ģ)Ʀ642ǶJpFG䰊x8<WCmE@PE@PE@PE@PE KvdxIN%3WM᯼XVnrWmI˲l"Iߊ"("("("(@>B B7~$A5TfDj"9Eˡ&YQ"("("("( B bȧl^)I7c;d*¨^@n oY,鈏~%EE䗃~+"("("("ԍ@gXyB䥛i*E.UWo3%ee-IM{ȕE" @IXye&amE@PE@PE@PE@PE!$hcѾ̉p]R|+q,YE:J_҇`I@iz[7E@PE@PE@PE@PHJœ3^Dj2EVd"_Y ##Jѽ1Iࡻ#I~$l^'__ ѵgWp+@Ŝ)wcŘ>ӲMĜLê< v]7[elۋkuQ_[_^9#ПqW_~;wwymT5,<`Z^nm\kWjWNy*Ud^շXch7}{k"3辢x9(qJ ֕7 wLUy'ݍuy:$ݨat}ht5.2>ɷ]3+"("( 6'F!a|>;V+`m"绸BӋW*ɗ TQyS:_^'' bX]\/+&jX&>/߂>f+kKb~^ڧ# $E]_cK2_3͊nk*NIЭ3n?_]ǘї G5OO¨v4< c-YU$̙G=^r}XrnyP}9 ' uߙȜpmqߴi=1+p; # LՊxfE%>x,ʟfHs,VKvfƴ!OnVA}_N5M7Ҽ~R~ۍSpSm1;U3ƺ-0h>E@PE@P(`& T,nPh e(/_dm PjMv 0r^!hz9$[bZdzS+سLo"[$i(<|zYz`>[wVпW{+JZ.#Z7v-L46n3_oIXZ,}kr98c[WeO˗?7m8ѩMU.ƺrlx|Olf[Uѥr]C;|*z BZYכ&hËMIi6EwrDmнtmk)`i*qig3rC? mp\9oF=Q&yp?=:8J{ťFՅ=!5Bn^y/f8#/?oa5}2Et>]qH$aEz@y(+.P/i! ڻx=^9J2KfcٽY.?e .^蒤w|3۲;5ǢW桊!+85o `_t6GО֣xq?A'Ma71o 1w` ooL}2jaq'bOcp;S̻~2Wf۱_~G= wm$g[Oon0k1Ǩx~8uC֘=V;; xt=A\Jm*)N!k]vPtzo6}H qO7\e ,Ղm*%3j` ;~.ϠG7r˯uO)k? 75zs:\~sڗDV]"("("CD@H8lI!GܓPV>jeT"AbBXx]U*)*F. G~ؠ"^N0|S~\кb9'VNֱo=U) m#Λ`Y䄬*}ϙ6m>\.8 APosQ.= *9U6ܙ`8joZUGhdK_#Տ%O= JOBKypx`z< Pv;L܆b)p]cO3&E㚇[oWv̸w*pux>7>{bߊaaT<oy_5@| օkqc1%ْ9=Z5++c[J j./=p:(L\.SɆn>5+>hQko.t.ͬEBmTLtjFMk65qw `Bo^c[pT;fLeW7m6Xǵ=w9xH @ݾ='썫O سI_{!W0{վ lXm ļs_ÜO!\߈!뛽f[m-nO캧t|Vev$)ǞyiUyuXj־ddZkQt-C Ê"("( ?9%#YNēVVMLBz%+rfBzhb/X%HCtp"$ϲB>ŝfS7.\188CzD Y;V^2 q,#r3g)mi^X1>f)GtdC IksY:$7$}ɨFX iSNAr/ғ >VVF:IC.ŀWnس>y9aneh7f^UӨH0/ J5N1no߰uOѻ1bӷ) *ЎER׿&rUZ&I>FiS :l7v>q]HbWJB-C11 hN8W>[b+ IK1uMbC q%zѭ"_ fg;ڇYFag$Dڍ"S6ͧ9*Vbϲ ^G4q~jVY6 ?Ր^ǯO~'v4GX. Iv2OQ; fUû3O> "vJt{k8$Ŗ ?.k$2c"̂9sOڅt(=3׾. uT$Ȇ$z&2[PE@PE@!"ė9 %C"xezNѤce <񉅘G3W|T94b q-f/^S+ٺj=;v#R;'t$!0zm·mgTJrfyԪl+3翂XW`<#&]%Ѽ=1?!#v?w)Hף0塃H`ygo'ޣ~0|pYv}DQd6ts8Nܯimo}GvhSv;.&TY9ٹcmfܷ0sutNp3|CCS{˿~wrҍ &DM< 'gFhk{Hu`Q;*`W9YG %}O\Dd5s z:)U5Rkb_W#䊀"("(;^M+=h&W^d$hg06aH/`P{ -;cͦ[m,E4ҧ+.3 q[eZ̼k:-ZxArdڣ -؊u#w _AF)y-NjSaoVqYFچc_~e NtoxjbL9X׳#ڔ?3xOq-ƍi0Qfp &߱5ZcKh=CwXE2ZMy90Ծ2qj[D`n*; w}+L2N˜EB+[c*Wۚq_XxN|HB6X *џI<0DJgM\z˳XIrlV5EO=Dwֳ1A{$饼/X^M۟QߡnMKX]IҐnX)<&jۦo˗/Tbthw`9o/1 I9P50>+ku[lH}[? )>w? $r̯8R7Ђÿy攥ߊ"("(@᳒v4j*p+`*Hz1vD, _fTT>#8'ѦcMb͕3jεt tA4>) #s漆GC+_Kqh-?19ŀ'-۱kEϑ2JQElϜxm:)m⊁ưnQw]ɘh,Eρ^Vi7nŀ곭vl nw=/q0tXݷ>y wQ@rkX! >{e?筹 㹸ifs/NgL_nzP> Bš|%,G\88Ws^}6I'J;W&.S6gLv= wَ8%Y'.=w]/g1&Mtkax+X|Ё)z(hV#2Rx. vICO-]ǹl^Qmq]u{t=}4n\V8ݕ}ңdOGK+w767O'.+#fӦṘK.xͩ#rg6EY5BX+0N'd+uo]QƑf8.xsi2-qz>.@f,u 淉ME@PE@PE@"/䅏^TSYYig'id$fBv5  BWduޒ6n܈:4,^XfMdc0 }bB?OCEb~"w ʐ%:.uV 8pU,T\Hl EcT# S>W,D9[]*T_0J&b55M˯d> iDi);Oc._:b)EW!krj`ʹ#뜻e;+wӺGݙMƤ;YvSϋ Ҫh(&ƹMֆoVr\;-f>F dʠg^]Džt8~ 8.xů/?{ O;U+n)@K(RhMZ聝{Gac\Q01rKC+5%N7G \x.٤WRb~ҔgwB!F>lDF^rK4NeEM/{-n703{1 nM裨X8O~(tqsyVtnCpb=䯷.s{Ui.圪L'.T_V(X~6%, `Ɍ[0Уs,ESn"Lf$c ԟUwYsҹOnSM9cLyKQZz!oy7Prccѷճŀaɫobks;Eqid`o,}s!k۸Iغd2"("("C@ Fc{U(Z^Z~_ylL9YC:-WHl%bq5.p b*@*dQ`ˁݘ_6 [Yź֞1~P[hi꓏ u }w"(c|a eʤ߲rx^x:hڄn ߲^ͮhҏizs:n<}Wx/'V>LHEqFLc+ʚ!?J&=ͮ E$ I77_gbU*BntmNhuY;'W#2iRE`gEF(Y/U۽~=c:w]!]tCPE@PE@Ip3&)FPbL$-7%Am- l/,ҋۑH 2DIzu(3iu_PE@PE@PE@PE@p# ^"M导pvN2gٍU|r$HŸ2$h^woVE@PE@PE@PE@Py$+MI^K8Eb).eKR"׻DȭPIK|r:`/Ol0WTE@PE@PE@PE@P !!P"qzIDbaě_Y1]߉D)-+${I2DI`z2aVTE@PE@PE@PE@P# $ey Њ⢣_y^;$~LpbלS^ F1YM0adY̪1 3sB?E@PE@PE@PE@PzJUwDاW.eOGY&R$ b"L/[b\#IuutpoE@PE@PE@PE@PHyhOA j0%ī+&F\V"f4zeyl[BȎqjP ijcjWE@PE@PE@PE@P:5~ABJ2jS55~S+oq%3.W3$je$ӤT Y/?Ka5&##f3E@PE@PE@PE@PEaD L$ˆ(-nN+W~?$tWvdfy4DqZ3V/!bF DƑ +DE@PE@PE@PE@PE!vˡ' _y*++$ڈT[REnI/kjpo#qEVƆ\QE@PE@PE@PE@#P\ 1|<U$?%%cл!j(UJXM! ^_0<™YTX?!#B#[/=ՏS?,*"\DcH8Wi 4>FrEĪc%Xa4HRe&iY&̗^f%^'y cf@(-y @m dޠRnr^OF€#.K~Ϗ,H~~`PHDֆGJd~~~dD֎ǏK?h3F+'I$Iby+TL-[?V@Џ'bgiJ[ݟQؖVZf^eUCR`|anJ$nngKgLLDRIWIf1mViBjlJKفIՆU5VNRnuzyiWp#؟bWnjGfa¢=..!cBCCW A|CYB# ՏGրPȂCǭVd0X$V"dEE$bh&l[dqDYᷝWFH抐P*/yؾ'ίY$C)$[ĥ$Rȗ%?yW~:|z/t\ʗd,/:%2.PDZ"V<'{IrlZݖ] N~2\/:p.U?T?T?\ +6ZVR6^x])8XyX=%k%!/.VU)QifGF!v|d07rBEb4%.)ФXLK|yI/'WqbL0Kݤ<4u9ec3.< - c=a% f[ !xBҹI/9&?+sϔ}~XPC~Wk: 8e(_7l\U"p=qDjZ[4 RdU řȜ%#,ȂdX$c,'X-Vڏ'BȘY51a &Ybt7 [4lmfb$v<0Wb;KkI\KIN/!l]sʎfH=-ΑvbVeX[2t?B#Gf4X[DT?T?2A# Տ1Ϗ "`m~dQ#e)1rI,I=H¦y^DM|EK˲Mz!.KţaQ2 I=ёIt#\-ёxԊ;BzI`5$m-DKBzRQs\7E$JB^}b%!'?MnҋUJ=a,9^& aȶՄɡ  yDCCy>!?deyX\ ?qL Q%#XYZnyHM#&.+U!,"'6gaj9S"q2C!=(0UWB$]_D̩lUcYDDu:7b及"%ۚKՄh "]kF\nyʲDV\<},δ3}ixnpU?T?d,~C~<z<}:pܗ?~ 5ΠL;R3bْqW~~8L_Yh~~VPȚ9+}Ϗ#=+3.ާ$x[M _r X]Iɋec_ yN;FN̴H8B˲Ps3- AwYm&I1k\f&+{ Οbl2QY&h!b 1OT2paPȌ Տ PPynJ߯\O?t>(ZmQ9Q)%^eFCzO3cњI>;lH*f AT?+}~C>ʾ7ʗ_Y`~C:4w[7ESʗxUm7p2 K_T3ĎXB )&]qrH*Z;vZhd#7}Э!< ~7ʶ_ҵD"VZ?>Y:"E$|^,IĐ^ 4&' n(UeNg\$M ᧜pX62 JH/9&LDBEAs$ 9!/U ˜|YZB$DrI(zݓj)"oH۲ %doġ#v5f7VSC8-|jh !1ȸxfP~2(T?s}A߯Jwu\u~|I.E.$ }P%cbDiD,sGG'Ue9R h~QtY7\ a3p{0mEZEWC~W|'|G5{!ۗ]$$R-G@Jbث3ʵ%#5,_zD x!\7>ۢ#$1ND6AEW]5IW9ų$A'iK'N)ʬ>K2iS XTZ-@`?V[2ƞg}&!)|š!| _8NV,D[L& U]q eET erҋIzQ3ҋ( >b9/B~>^|BG. JÝdqIBwE\BmbVp[vΤAސTPXSy4n>X!^`‡u@+֎ $]CALO5?&RS|Imq%RaTs?oul_7߱Vقj)8Һ @Dtc`A: sWks\]7|(ƒy "ư/ˣCq7_=>S{VZY!|h|Dy$|G}Z>=&#Gߖw˹(-C/ΊXXޏ٬HXǤ}Hn% Ӗ߄s3 FZ1I) JXo=kfs$.3ݘ?, 8KvnWlVMV`&ǘf*{W*Vm/|^Ķ> ‡kb ‡5텏_!r,EӢ/LR[V Hz:{XT`,ڑ^!6=Vt+8b\9-itC ,oCҋ> co1>I/|r[ Nz$I/Jz˱(3 BsDa}nGzGғݴI/@>Zz ^#4xWo5Ț|P\4?֡ 11ju]h753Y^e+n~%2@!\k%GǠSlHi69h؆6WAW_Q]A *h>皟o=7rݦIENDB`sidekiq-cron-sidekiq-cron-31b9d88/lib/000077500000000000000000000000001453463263700176175ustar00rootroot00000000000000sidekiq-cron-sidekiq-cron-31b9d88/lib/sidekiq-cron.rb000066400000000000000000000000511453463263700225300ustar00rootroot00000000000000require "sidekiq" require "sidekiq/cron" sidekiq-cron-sidekiq-cron-31b9d88/lib/sidekiq/000077500000000000000000000000001453463263700212505ustar00rootroot00000000000000sidekiq-cron-sidekiq-cron-31b9d88/lib/sidekiq/cron.rb000066400000000000000000000002501453463263700225330ustar00rootroot00000000000000require "sidekiq/cron/job" require "sidekiq/cron/poller" require "sidekiq/cron/launcher" require "sidekiq/cron/schedule_loader" module Sidekiq module Cron end end sidekiq-cron-sidekiq-cron-31b9d88/lib/sidekiq/cron/000077500000000000000000000000001453463263700222115ustar00rootroot00000000000000sidekiq-cron-sidekiq-cron-31b9d88/lib/sidekiq/cron/job.rb000066400000000000000000000515701453463263700233200ustar00rootroot00000000000000require 'fugit' require 'globalid' require 'sidekiq' require 'sidekiq/cron/support' require 'sidekiq/options' module Sidekiq module Cron class Job # How long we would like to store informations about previous enqueues. REMEMBER_THRESHOLD = 24 * 60 * 60 # Time format for enqueued jobs. LAST_ENQUEUE_TIME_FORMAT = '%Y-%m-%d %H:%M:%S %z' # Use the exists? method if we're on a newer version of Redis. REDIS_EXISTS_METHOD = Gem::Version.new(Sidekiq::VERSION) >= Gem::Version.new("7.0.0") || Gem.loaded_specs['redis'].version < Gem::Version.new('4.2') ? :exists : :exists? # Use serialize/deserialize key of GlobalID. GLOBALID_KEY = "_sc_globalid" # Crucial part of whole enqueuing job. def should_enque? time return false unless status == "enabled" return false unless not_past_scheduled_time?(time) return false unless not_enqueued_after?(time) enqueue = Sidekiq.redis do |conn| conn.zadd(job_enqueued_key, formatted_enqueue_time(time), formatted_last_time(time)) end enqueue == true || enqueue == 1 end # Remove previous information about run times, # this will clear Redis and make sure that Redis will not overflow with memory. def remove_previous_enques time Sidekiq.redis do |conn| conn.zremrangebyscore(job_enqueued_key, 0, "(#{(time.to_f - REMEMBER_THRESHOLD).to_s}") end end # Test if job should be enqueued. def test_and_enque_for_time! time if should_enque?(time) enque! remove_previous_enques(time) end end # Enqueue cron job to queue. def enque! time = Time.now.utc @last_enqueue_time = time klass_const = begin Sidekiq::Cron::Support.constantize(@klass.to_s) rescue NameError nil end jid = if klass_const if is_active_job?(klass_const) enqueue_active_job(klass_const).try :provider_job_id else enqueue_sidekiq_worker(klass_const) end else if @active_job Sidekiq::Client.push(active_job_message) else Sidekiq::Client.push(sidekiq_worker_message) end end save_last_enqueue_time add_jid_history jid Sidekiq.logger.debug { "enqueued #{@name}: #{@message}" } end def is_active_job?(klass = nil) @active_job || defined?(ActiveJob::Base) && (klass || Sidekiq::Cron::Support.constantize(@klass.to_s)) < ActiveJob::Base rescue NameError false end def date_as_argument? !!@date_as_argument end def enqueue_args args = date_as_argument? ? @args + [Time.now.to_f] : @args deserialize_argument(args) end def enqueue_active_job(klass_const) klass_const.set(queue: @queue).perform_later(*enqueue_args) end def enqueue_sidekiq_worker(klass_const) klass_const.set(queue: queue_name_with_prefix).perform_async(*enqueue_args) end # Sidekiq worker message. def sidekiq_worker_message message = @message.is_a?(String) ? Sidekiq.load_json(@message) : @message message["args"] = enqueue_args message end def queue_name_with_prefix return @queue unless is_active_job? if !"#{@active_job_queue_name_delimiter}".empty? queue_name_delimiter = @active_job_queue_name_delimiter elsif defined?(ActiveJob::Base) && defined?(ActiveJob::Base.queue_name_delimiter) && !ActiveJob::Base.queue_name_delimiter.empty? queue_name_delimiter = ActiveJob::Base.queue_name_delimiter else queue_name_delimiter = '_' end if !"#{@active_job_queue_name_prefix}".empty? queue_name = "#{@active_job_queue_name_prefix}#{queue_name_delimiter}#{@queue}" elsif defined?(ActiveJob::Base) && defined?(ActiveJob::Base.queue_name_prefix) && !"#{ActiveJob::Base.queue_name_prefix}".empty? queue_name = "#{ActiveJob::Base.queue_name_prefix}#{queue_name_delimiter}#{@queue}" else queue_name = @queue end queue_name end # Active Job has different structure how it is loading data from Sidekiq # queue, it creates a wrapper around job. def active_job_message { 'class' => 'ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper', 'wrapped' => @klass, 'queue' => @queue_name_with_prefix, 'description' => @description, 'args' => [{ 'job_class' => @klass, 'job_id' => SecureRandom.uuid, 'queue_name' => @queue_name_with_prefix, 'arguments' => enqueue_args }] } end # Load cron jobs from Hash. # Input structure should look like: # { # 'name_of_job' => { # 'class' => 'MyClass', # 'cron' => '1 * * * *', # 'args' => '(OPTIONAL) [Array or Hash]', # 'description' => '(OPTIONAL) Description of job' # }, # 'My super iber cool job' => { # 'class' => 'SecondClass', # 'cron' => '*/5 * * * *' # } # } # def self.load_from_hash(hash, options = {}) array = hash.map do |key, job| job['name'] = key job end load_from_array(array, options) end # Like #load_from_hash. # If exists old jobs in Redis but removed from args, destroy old jobs. def self.load_from_hash!(hash, options = {}) destroy_removed_jobs(hash.keys) load_from_hash(hash, options) end # Load cron jobs from Array. # Input structure should look like: # [ # { # 'name' => 'name_of_job', # 'class' => 'MyClass', # 'cron' => '1 * * * *', # 'args' => '(OPTIONAL) [Array or Hash]', # 'description' => '(OPTIONAL) Description of job' # }, # { # 'name' => 'Cool Job for Second Class', # 'class' => 'SecondClass', # 'cron' => '*/5 * * * *' # } # ] # def self.load_from_array(array, options = {}) errors = {} array.each do |job_data| job = new(job_data.merge(options)) errors[job.name] = job.errors unless job.save end errors end # Like #load_from_array. # If exists old jobs in Redis but removed from args, destroy old jobs. def self.load_from_array!(array, options = {}) job_names = array.map { |job| job["name"] } destroy_removed_jobs(job_names) load_from_array(array, options) end # Get all cron jobs. def self.all job_hashes = nil Sidekiq.redis do |conn| set_members = conn.smembers(jobs_key) job_hashes = conn.pipelined do |pipeline| set_members.each do |key| pipeline.hgetall(key) end end end job_hashes.compact.reject(&:empty?).collect do |h| # No need to fetch missing args from Redis since we just got this hash from there Sidekiq::Cron::Job.new(h.merge(fetch_missing_args: false)) end end def self.count out = 0 Sidekiq.redis do |conn| out = conn.scard(jobs_key) end out end def self.find name # If name is hash try to get name from it. name = name[:name] || name['name'] if name.is_a?(Hash) return unless exists? name output = nil Sidekiq.redis do |conn| output = Job.new conn.hgetall( redis_key(name) ) end output if output && output.valid? end # Create new instance of cron job. def self.create hash new(hash).save end # Destroy job by name. def self.destroy name # If name is hash try to get name from it. name = name[:name] || name['name'] if name.is_a?(Hash) if job = find(name) job.destroy else false end end attr_accessor :name, :cron, :description, :klass, :args, :message attr_reader :last_enqueue_time, :fetch_missing_args, :source def initialize input_args = {} args = Hash[input_args.map{ |k, v| [k.to_s, v] }] @fetch_missing_args = args.delete('fetch_missing_args') @fetch_missing_args = true if @fetch_missing_args.nil? @name = args["name"] @cron = args["cron"] @description = args["description"] if args["description"] @source = args["source"] == "schedule" ? "schedule" : "dynamic" # Get class from klass or class. @klass = args["klass"] || args["class"] # Set status of job. @status = args['status'] || status_from_redis # Set last enqueue time - from args or from existing job. if args['last_enqueue_time'] && !args['last_enqueue_time'].empty? @last_enqueue_time = parse_enqueue_time(args['last_enqueue_time']) else @last_enqueue_time = last_enqueue_time_from_redis end # Get right arguments for job. @symbolize_args = args["symbolize_args"] == true || ("#{args["symbolize_args"]}" =~ (/^(true|t|yes|y|1)$/i)) == 0 || false @args = parse_args(args["args"]) @date_as_argument = args["date_as_argument"] == true || ("#{args["date_as_argument"]}" =~ (/^(true|t|yes|y|1)$/i)) == 0 || false @active_job = args["active_job"] == true || ("#{args["active_job"]}" =~ (/^(true|t|yes|y|1)$/i)) == 0 || false @active_job_queue_name_prefix = args["queue_name_prefix"] @active_job_queue_name_delimiter = args["queue_name_delimiter"] if args["message"] @message = args["message"] message_data = Sidekiq.load_json(@message) || {} @queue = message_data['queue'] || "default" elsif @klass message_data = { "class" => @klass.to_s, "args" => @args, } # Get right data for message, # only if message wasn't specified before. klass_data = case @klass when Class @klass.get_sidekiq_options when String begin Sidekiq::Cron::Support.constantize(@klass).get_sidekiq_options rescue Exception => e # Unknown class {"queue"=>"default"} end end message_data = klass_data.merge(message_data) # Override queue if setted in config, # only if message is hash - can be string (dumped JSON). if args['queue'] @queue = message_data['queue'] = args['queue'] else @queue = message_data['queue'] || "default" end @message = message_data end @queue_name_with_prefix = queue_name_with_prefix end def status @status end def disable! @status = "disabled" save end def enable! @status = "enabled" save end def enabled? @status == "enabled" end def disabled? !enabled? end def pretty_message JSON.pretty_generate Sidekiq.load_json(message) rescue JSON::ParserError message end def status_from_redis out = "enabled" if fetch_missing_args Sidekiq.redis do |conn| status = conn.hget redis_key, "status" out = status if status end end out end def last_enqueue_time_from_redis out = nil if fetch_missing_args Sidekiq.redis do |conn| out = parse_enqueue_time(conn.hget(redis_key, "last_enqueue_time")) rescue nil end end out end def jid_history_from_redis out = Sidekiq.redis do |conn| conn.lrange(jid_history_key, 0, -1) rescue nil end out && out.map do |jid_history_raw| Sidekiq.load_json jid_history_raw end end # Export job data to hash. def to_hash hash = { name: @name, klass: @klass.to_s, cron: @cron, description: @description, source: @source, args: @args.is_a?(String) ? @args : Sidekiq.dump_json(@args || []), message: @message.is_a?(String) ? @message : Sidekiq.dump_json(@message || {}), status: @status, active_job: @active_job ? "1" : "0", queue_name_prefix: @active_job_queue_name_prefix, queue_name_delimiter: @active_job_queue_name_delimiter, last_enqueue_time: serialized_last_enqueue_time, symbolize_args: symbolize_args? ? "1" : "0", } if date_as_argument? hash.merge!(date_as_argument: "1") end hash end def errors @errors ||= [] end def valid? # Clear previous errors. @errors = [] errors << "'name' must be set" if @name.nil? || @name.size == 0 if @cron.nil? || @cron.size == 0 errors << "'cron' must be set" else begin @parsed_cron = Fugit.do_parse_cronish(@cron) rescue => e errors << "'cron' -> #{@cron.inspect} -> #{e.class}: #{e.message}" end end errors << "'klass' (or class) must be set" unless klass_valid errors.empty? end def klass_valid case @klass when Class true when String @klass.size > 0 else end end def save # If job is invalid, return false. return false unless valid? Sidekiq.redis do |conn| # Add to set of all jobs conn.sadd self.class.jobs_key, [redis_key] # Add informations for this job! conn.hset redis_key, to_hash.transform_values! { |v| v || "" } # Add information about last time! - don't enque right after scheduler poller starts! time = Time.now.utc exists = conn.public_send(REDIS_EXISTS_METHOD, job_enqueued_key) conn.zadd(job_enqueued_key, time.to_f.to_s, formatted_last_time(time).to_s) unless exists == true || exists == 1 end Sidekiq.logger.info { "Cron Jobs - added job with name: #{@name}" } end def save_last_enqueue_time Sidekiq.redis do |conn| # Update last enqueue time. conn.hset redis_key, 'last_enqueue_time', serialized_last_enqueue_time end end def add_jid_history(jid) jid_history = { jid: jid, enqueued: @last_enqueue_time } @history_size ||= (Sidekiq::Options[:cron_history_size] || 10).to_i - 1 Sidekiq.redis do |conn| conn.lpush jid_history_key, Sidekiq.dump_json(jid_history) # Keep only last 10 entries in a fifo manner. conn.ltrim jid_history_key, 0, @history_size end end def destroy Sidekiq.redis do |conn| # Delete from set. conn.srem self.class.jobs_key, [redis_key] # Delete runned timestamps. conn.del job_enqueued_key # Delete jid_history. conn.del jid_history_key # Delete main job. conn.del redis_key end Sidekiq.logger.info { "Cron Jobs - deleted job with name: #{@name}" } end # Remove all job from cron. def self.destroy_all! all.each do |job| job.destroy end Sidekiq.logger.info { "Cron Jobs - deleted all jobs" } end # Remove "removed jobs" between current jobs and new jobs def self.destroy_removed_jobs new_job_names current_job_names = Sidekiq::Cron::Job.all.filter_map { |j| j.name if j.source == "schedule" } removed_job_names = current_job_names - new_job_names removed_job_names.each { |j| Sidekiq::Cron::Job.destroy(j) } removed_job_names end # Parse cron specification '* * * * *' and returns # time when last run should be performed def last_time now = Time.now.utc parsed_cron.previous_time(now.utc).utc end def formatted_enqueue_time now = Time.now.utc last_time(now).getutc.to_f.to_s end def formatted_last_time now = Time.now.utc last_time(now).getutc.iso8601 end def self.exists? name out = Sidekiq.redis do |conn| conn.public_send(REDIS_EXISTS_METHOD, redis_key(name)) end out == true || out == 1 end def exists? self.class.exists? @name end def sort_name "#{status == "enabled" ? 0 : 1}_#{name}".downcase end def args=(args) @args = parse_args(args) end private def parsed_cron @parsed_cron ||= Fugit.parse_cronish(@cron) end def not_enqueued_after?(time) @last_enqueue_time.nil? || @last_enqueue_time.to_i < last_time(time).to_i end # Try parsing inbound args into an array. # Args from Redis will be encoded JSON, # try to load JSON, then failover to string array. def parse_args(args) case args when GlobalID::Identification [convert_to_global_id_hash(args)] when String begin parsed_args = Sidekiq.load_json(args) symbolize_args? ? symbolize_args(parsed_args) : parsed_args rescue JSON::ParserError [*args] end when Hash args = serialize_argument(args) symbolize_args? ? [symbolize_args(args)] : [args] when Array args = serialize_argument(args) symbolize_args? ? symbolize_args(args) : args else [*args] end end def symbolize_args? @symbolize_args end def symbolize_args(input) if input.is_a?(Array) input.map do |arg| if arg.respond_to?(:symbolize_keys) arg.symbolize_keys else arg end end elsif input.is_a?(Hash) && input.respond_to?(:symbolize_keys) input.symbolize_keys else input end end def parse_enqueue_time(timestamp) DateTime.strptime(timestamp, LAST_ENQUEUE_TIME_FORMAT).to_time.utc rescue ArgumentError DateTime.parse(timestamp).to_time.utc end def not_past_scheduled_time?(current_time) last_cron_time = parsed_cron.previous_time(current_time).utc return false if (current_time.to_i - last_cron_time.to_i) > 60 true end # Redis key for set of all cron jobs. def self.jobs_key "cron_jobs" end # Redis key for storing one cron job. def self.redis_key name "cron_job:#{name}" end # Redis key for storing one cron job. def redis_key self.class.redis_key @name end # Redis key for storing one cron job run times (when poller added job to queue) def self.job_enqueued_key name "cron_job:#{name}:enqueued" end def self.jid_history_key name "cron_job:#{name}:jid_history" end def job_enqueued_key self.class.job_enqueued_key @name end def jid_history_key self.class.jid_history_key @name end def serialized_last_enqueue_time @last_enqueue_time&.strftime(LAST_ENQUEUE_TIME_FORMAT) end def convert_to_global_id_hash(argument) { GLOBALID_KEY => argument.to_global_id.to_s } rescue URI::GID::MissingModelIdError raise "Unable to serialize #{argument.class} " \ "without an id. (Maybe you forgot to call save?)" end def deserialize_argument(argument) case argument when String argument when Array argument.map { |arg| deserialize_argument(arg) } when Hash if serialized_global_id?(argument) deserialize_global_id argument else argument.transform_values { |v| deserialize_argument(v) } end else argument end end def serialized_global_id?(hash) hash.size == 1 && hash.include?(GLOBALID_KEY) end def deserialize_global_id(hash) GlobalID::Locator.locate hash[GLOBALID_KEY] end def serialize_argument(argument) case argument when GlobalID::Identification convert_to_global_id_hash(argument) when Array argument.map { |arg| serialize_argument(arg) } when Hash argument.each_with_object({}) do |(key, value), hash| hash[key] = serialize_argument(value) end else argument end end end end end sidekiq-cron-sidekiq-cron-31b9d88/lib/sidekiq/cron/launcher.rb000066400000000000000000000023561453463263700243450ustar00rootroot00000000000000require 'sidekiq/cron/poller' # For Cron we need to add some methods to Launcher # so look at the code bellow. # # We are creating new cron poller instance and # adding start and stop commands to launcher. module Sidekiq module Cron module Launcher DEFAULT_POLL_INTERVAL = 30 # Add cron poller to launcher. attr_reader :cron_poller # Add cron poller and execute normal initialize of Sidekiq launcher. def initialize(config, **kwargs) config[:cron_poll_interval] = DEFAULT_POLL_INTERVAL if config[:cron_poll_interval].nil? @cron_poller = Sidekiq::Cron::Poller.new(config) if config[:cron_poll_interval] > 0 super end # Execute normal run of launcher and run cron poller. def run super cron_poller.start if @cron_poller end # Execute normal quiet of launcher and quiet cron poller. def quiet cron_poller.terminate if @cron_poller super end # Execute normal stop of launcher and stop cron poller. def stop cron_poller.terminate if @cron_poller super end end end end Sidekiq.configure_server do require 'sidekiq/launcher' ::Sidekiq::Launcher.prepend(Sidekiq::Cron::Launcher) end sidekiq-cron-sidekiq-cron-31b9d88/lib/sidekiq/cron/locales/000077500000000000000000000000001453463263700236335ustar00rootroot00000000000000sidekiq-cron-sidekiq-cron-31b9d88/lib/sidekiq/cron/locales/de.yml000066400000000000000000000005461453463263700247530ustar00rootroot00000000000000de: Job: Job Cron: Cron CronJobs: Cronjobs EnqueueNow: In Warteschlange 'Cron string': Cron AreYouSureDeleteCronJob: Sind Sie sicher, dass sie den Cronjob %{job} löschen wollen? NoCronJobsWereFound: Keine Cronjobs gefunden Enable: Aktivieren Disable: Deaktivieren 'Last enqueued': Eingereiht disabled: deaktiviert enabled: aktiviert sidekiq-cron-sidekiq-cron-31b9d88/lib/sidekiq/cron/locales/en.yml000066400000000000000000000013431453463263700247610ustar00rootroot00000000000000en: Job: Job Cron: Cron CronJobs: Cron Jobs EnqueueNow: Enqueue Now EnableAll: Enable All DisableAll: Disable All EnqueueAll: Enqueue All DeleteAll: Delete All 'Cron string': Cron AreYouSureEnqueueCronJobs: Are you sure you want to enqueue ALL cron jobs? AreYouSureEnqueueCronJob: Are you sure you want to enqueue the %{job} cron job? AreYouSureDeleteCronJobs: Are you sure you want to delete ALL cron jobs? AreYouSureDeleteCronJob: Are you sure you want to delete the %{job} cron job? NoCronJobsWereFound: No cron jobs were found Enable: Enable Disable: Disable 'Last enqueued': Last enqueued disabled: disabled enabled: enabled NoHistoryWereFound: No history were found Description: Description sidekiq-cron-sidekiq-cron-31b9d88/lib/sidekiq/cron/locales/it.yml000066400000000000000000000013221453463263700247700ustar00rootroot00000000000000it: Job: Job Cron: Cron CronJobs: Cron job EnqueueNow: Accoda EnableAll: Attiva tutto DisableAll: Disattiva tutto EnqueueAll: Accoda tutto DeleteAll: Cancella tutto "Cron string": Cron AreYouSureEnqueueCronJobs: Vuoi accodare TUTTI i cron job? AreYouSureEnqueueCronJob: "Vuoi accodare il cron job '%{job}'?" AreYouSureDeleteCronJobs: Vuoi cancellare TUTTI i cron job? AreYouSureDeleteCronJob: "Vuoi cancellare il cron job '%{job}'?" NoCronJobsWereFound: Nessun cron job trovato Enable: Attiva Disable: Disattiva "Last enqueued": Ultimo accodamento disabled: disattivato enabled: attivato NoHistoryWereFound: Nessun evento in cronologia Description: Descrizione Message: Payload sidekiq-cron-sidekiq-cron-31b9d88/lib/sidekiq/cron/locales/ja.yml000066400000000000000000000012061453463263700247470ustar00rootroot00000000000000ja: Job: ジョブ Cron: Cron CronJobs: Cronジョブ EnqueueNow: すぐにキューに入れる EnableAll: すべて有効にする DisableAll: すべて無効にする EnqueueAll: すべてキューに入れる DeleteAll: すべて削除 'Cron string': Cron AreYouSureDeleteCronJobs: 本当にすべてのcronジョブを削除しますか? AreYouSureDeleteCronJob: 本当に%{job}のcronジョブを削除しますか? NoCronJobsWereFound: Cronジョブが見つかりませんでした Enable: 有効にする Disable: 無効にする 'Last enqueued': 最後のキュー disabled: 無効 enabled: 有効 sidekiq-cron-sidekiq-cron-31b9d88/lib/sidekiq/cron/locales/pt.yml000066400000000000000000000014631453463263700250050ustar00rootroot00000000000000pt: Job: Tarefa Cron: Cron CronJobs: Tarefas do Cron EnqueueNow: Enfileirar agora EnableAll: Habilitar todos DisableAll: Desabilitar todos EnqueueAll: Enfileirar todos DeleteAll: Excluir todos 'Cron string': Cron AreYouSureEnqueueCronJobs: Tem certeza de que deseja enfileirar TODOS as tarefas? AreYouSureEnqueueCronJob: Tem certeza de que deseja enfileirar a tarefa %{job}? AreYouSureDeleteCronJobs: Tem certeza de que deseja excluir TODOS as tarefas? AreYouSureDeleteCronJob: Tem certeza de que deseja excluir a tarefa %{job}? NoCronJobsWereFound: Nenhuma tarefa foi encontrada Enable: Habilitar Disable: Desabilitar 'Last enqueued': Último enfileirado disabled: desabilitado enabled: habilitado NoHistoryWereFound: Nenhum histórico foi encontrado Description: Descrição sidekiq-cron-sidekiq-cron-31b9d88/lib/sidekiq/cron/locales/ru.yml000066400000000000000000000011011453463263700247750ustar00rootroot00000000000000ru: Job: Задача Cron: Cron CronJobs: Периодические задачи Name: Название 'Cron string': Периодичность (синтаксис Cron) EnqueueNow: Запустить AreYouSureDeleteCronJob: Вы действительно хотите удалить задачу «%{job}»? NoCronJobsWereFound: Не найдено периодических задач Enable: Включить Disable: Отключить 'Last enqueued': Последний запуск disabled: отключено enabled: включено sidekiq-cron-sidekiq-cron-31b9d88/lib/sidekiq/cron/locales/zh-CN.yml000066400000000000000000000010331453463263700252720ustar00rootroot00000000000000zh-CN: Job: 任务 Cron: 定时任务 CronJobs: 定时任务列表 EnqueueNow: 立刻执行 EnableAll: 启用所有 DisableAll: 禁用所有 EnqueueAll: 执行所有 DeleteAll: 删除所有 'Cron string': 定时策略 AreYouSureDeleteCronJobs: 你确定删除所有的定时任务吗? AreYouSureDeleteCronJob: 你确定删除定时任务(%{job})吗? NoCronJobsWereFound: 没有定时任务 Enable: 启用 Disable: 禁用 'Last enqueued': 放入队列时间 disabled: 已禁用 enabled: 已启用 sidekiq-cron-sidekiq-cron-31b9d88/lib/sidekiq/cron/poller.rb000066400000000000000000000026321453463263700240360ustar00rootroot00000000000000require 'sidekiq' require 'sidekiq/cron' require 'sidekiq/scheduled' require 'sidekiq/options' module Sidekiq module Cron # The Poller checks Redis every N seconds for sheduled cron jobs. class Poller < Sidekiq::Scheduled::Poller def initialize(config = nil) if Gem::Version.new(Sidekiq::VERSION) >= Gem::Version.new('6.5.0') super else # Old version of Sidekiq does not accept a config argument. @config = config super() end end def enqueue time = Time.now.utc Sidekiq::Cron::Job.all.each do |job| enqueue_job(job, time) end rescue => ex # Most likely a problem with redis networking. # Punt and try again at the next interval. Sidekiq.logger.error ex.message Sidekiq.logger.error ex.backtrace.first handle_exception(ex) if respond_to?(:handle_exception) end private def enqueue_job(job, time = Time.now.utc) job.test_and_enque_for_time! time if job && job.valid? rescue => ex # Problem somewhere in one job. Sidekiq.logger.error "CRON JOB: #{ex.message}" Sidekiq.logger.error "CRON JOB: #{ex.backtrace.first}" handle_exception(ex) if respond_to?(:handle_exception) end def poll_interval_average(process_count = 1) @config[:cron_poll_interval] end end end end sidekiq-cron-sidekiq-cron-31b9d88/lib/sidekiq/cron/schedule_loader.rb000066400000000000000000000012411453463263700256560ustar00rootroot00000000000000require 'sidekiq' require 'sidekiq/cron/job' require 'sidekiq/options' Sidekiq.configure_server do |config| schedule_file = Sidekiq::Options[:cron_schedule_file] || 'config/schedule.yml' if File.exist?(schedule_file) config.on(:startup) do schedule = Sidekiq::Cron::Support.load_yaml(ERB.new(IO.read(schedule_file)).result) if schedule.kind_of?(Hash) Sidekiq::Cron::Job.load_from_hash!(schedule, source: "schedule") elsif schedule.kind_of?(Array) Sidekiq::Cron::Job.load_from_array!(schedule, source: "schedule") else raise "Not supported schedule format. Confirm your #{schedule_file}" end end end end sidekiq-cron-sidekiq-cron-31b9d88/lib/sidekiq/cron/support.rb000066400000000000000000000030521453463263700242520ustar00rootroot00000000000000# https://github.com/rails/rails/blob/352865d0f835c24daa9a2e9863dcc9dde9e5371a/activesupport/lib/active_support/inflector/methods.rb#L270 module Sidekiq module Cron module Support def self.constantize(camel_cased_word) names = camel_cased_word.split("::".freeze) # Trigger a built-in NameError exception including the ill-formed constant in the message. Object.const_get(camel_cased_word) if names.empty? # Remove the first blank element in case of '::ClassName' notation. names.shift if names.size > 1 && names.first.empty? names.inject(Object) do |constant, name| if constant == Object constant.const_get(name) else candidate = constant.const_get(name) next candidate if constant.const_defined?(name, false) next candidate unless Object.const_defined?(name) # Go down the ancestors to check if it is owned directly. The check # stops when we reach Object or the end of ancestors tree. constant = constant.ancestors.inject(constant) do |const, ancestor| break const if ancestor == Object break ancestor if ancestor.const_defined?(name, false) const end constant.const_get(name, false) end end end def self.load_yaml(src) if Psych::VERSION > "4.0" YAML.safe_load(src, permitted_classes: [Symbol], aliases: true) else YAML.load(src) end end end end end sidekiq-cron-sidekiq-cron-31b9d88/lib/sidekiq/cron/version.rb000066400000000000000000000001351453463263700242220ustar00rootroot00000000000000# frozen_string_literal: true module Sidekiq module Cron VERSION = "1.12.0" end end sidekiq-cron-sidekiq-cron-31b9d88/lib/sidekiq/cron/views/000077500000000000000000000000001453463263700233465ustar00rootroot00000000000000sidekiq-cron-sidekiq-cron-31b9d88/lib/sidekiq/cron/views/cron.erb000066400000000000000000000125341453463263700250060ustar00rootroot00000000000000

<%= t('CronJobs') %>

<% if @cron_jobs.size > 0 %>
<%= csrf_tag if respond_to?(:csrf_tag) %>
<%= csrf_tag if respond_to?(:csrf_tag) %>
<%= csrf_tag if respond_to?(:csrf_tag) %>
<%= csrf_tag if respond_to?(:csrf_tag) %>
<% end %>
<% if @cron_jobs.size > 0 %> <% @cron_jobs.sort{|a,b| a.sort_name <=> b.sort_name }.each_with_index do |job, index| %> <% style = "#{job.status == 'disabled' ? "background: #ecc; color: #585454;": ""}" %> <% end %>
<%= t('Status') %> <%= t('Name') %> <%= t('Cron string') %> <%= t('Last enqueued') %> <%= t('Actions')%>
<%= t job.status %> <%= job.name %>
<% if job.message and job.message.to_s.size > 100 %> <% if Gem::Version.new(Sidekiq::VERSION) >= Gem::Version.new("6.3.0") %>
<%= job.message[0..100] + "... " %>
<% else %>
<%= job.message[0..100] + "... " %>
<% end %> <% else %> <%= job.message %> <% end %>
<%= job.cron.gsub(" ", " ") %> <%= job.last_enqueue_time ? relative_time(job.last_enqueue_time) : "-" %> <% if job.status == 'enabled' %>
<%= csrf_tag if respond_to?(:csrf_tag) %>
<%= csrf_tag if respond_to?(:csrf_tag) %>
<% else %>
<%= csrf_tag if respond_to?(:csrf_tag) %>
<%= csrf_tag if respond_to?(:csrf_tag) %>
<%= csrf_tag if respond_to?(:csrf_tag) %>
<% end %>
<% else %>
<%= t('NoCronJobsWereFound') %>
<% end %> sidekiq-cron-sidekiq-cron-31b9d88/lib/sidekiq/cron/views/cron_show.erb000066400000000000000000000055001453463263700260410ustar00rootroot00000000000000

<%= "#{t('Cron')} #{t('Job')}" %> <%= @job.name %>

<% cron_job_path = "#{root_path}cron/#{CGI.escape(@job.name).gsub('+', '%20')}" %>
<%= csrf_tag if respond_to?(:csrf_tag) %>
<% if @job.status == 'enabled' %>
<%= csrf_tag if respond_to?(:csrf_tag) %>
<% else %>
<%= csrf_tag if respond_to?(:csrf_tag) %>
<%= csrf_tag if respond_to?(:csrf_tag) %>
<% end %>
<%= t 'Status' %> <%= @job.status %>
<%= t 'Name' %> <%= @job.name %>
<%= t 'Description' %> <%= @job.description %>
<%= t 'Message' %>
<%= @job.pretty_message %>
<%= t 'Cron' %> <%= @job.cron.gsub(" ", " ") %>
<%= t 'Last enqueued' %> <%= @job.last_enqueue_time ? relative_time(@job.last_enqueue_time) : "-" %>

<%= t 'History' %>

<% if @job.jid_history_from_redis.size > 0 %> <% @job.jid_history_from_redis.each do |jid_history| %> <% end %>
<%= t 'Enqueued' %> <%= t 'JID' %>
<%= jid_history['enqueued'] %> <%= jid_history['jid'] %>
<% else %>
<%= t 'NoHistoryWereFound' %>
<% end %> sidekiq-cron-sidekiq-cron-31b9d88/lib/sidekiq/cron/web.rb000066400000000000000000000002701453463263700233120ustar00rootroot00000000000000require "sidekiq/cron/web_extension" require "sidekiq/cron/job" if defined?(Sidekiq::Web) Sidekiq::Web.register Sidekiq::Cron::WebExtension Sidekiq::Web.tabs["Cron"] = "cron" end sidekiq-cron-sidekiq-cron-31b9d88/lib/sidekiq/cron/web_extension.rb000066400000000000000000000042341453463263700254120ustar00rootroot00000000000000module Sidekiq module Cron module WebExtension def self.registered(app) app.settings.locales << File.join(File.expand_path("..", __FILE__), "locales") # Index page of cron jobs. app.get '/cron' do view_path = File.join(File.expand_path("..", __FILE__), "views") @cron_jobs = Sidekiq::Cron::Job.all render(:erb, File.read(File.join(view_path, "cron.erb"))) end # Display job detail + jid history. app.get '/cron/:name' do view_path = File.join(File.expand_path("..", __FILE__), "views") @job = Sidekiq::Cron::Job.find(route_params[:name]) if @job render(:erb, File.read(File.join(view_path, "cron_show.erb"))) else redirect "#{root_path}cron" end end # Enqueue cron job. app.post '/cron/:name/enque' do if route_params[:name] === '__all__' Sidekiq::Cron::Job.all.each(&:enque!) elsif job = Sidekiq::Cron::Job.find(route_params[:name]) job.enque! end redirect params['redirect'] || "#{root_path}cron" end # Delete schedule. app.post '/cron/:name/delete' do if route_params[:name] === '__all__' Sidekiq::Cron::Job.all.each(&:destroy) elsif job = Sidekiq::Cron::Job.find(route_params[:name]) job.destroy end redirect "#{root_path}cron" end # Enable job. app.post '/cron/:name/enable' do if route_params[:name] === '__all__' Sidekiq::Cron::Job.all.each(&:enable!) elsif job = Sidekiq::Cron::Job.find(route_params[:name]) job.enable! end redirect params['redirect'] || "#{root_path}cron" end # Disable job. app.post '/cron/:name/disable' do if route_params[:name] === '__all__' Sidekiq::Cron::Job.all.each(&:disable!) elsif job = Sidekiq::Cron::Job.find(route_params[:name]) job.disable! end redirect params['redirect'] || "#{root_path}cron" end end end end end sidekiq-cron-sidekiq-cron-31b9d88/lib/sidekiq/options.rb000066400000000000000000000011731453463263700232720ustar00rootroot00000000000000require 'sidekiq' module Sidekiq module Options def self.[](key) self.config[key] end def self.[]=(key, value) self.config[key] = value end def self.config options_field ? Sidekiq.public_send(options_field) : Sidekiq end def self.options_field return @options_field unless @options_field.nil? sidekiq_version = Gem::Version.new(Sidekiq::VERSION) @options_field = if sidekiq_version >= Gem::Version.new('7.0') :default_configuration elsif sidekiq_version >= Gem::Version.new('6.5') false else :options end end end end sidekiq-cron-sidekiq-cron-31b9d88/logos/000077500000000000000000000000001453463263700201745ustar00rootroot00000000000000sidekiq-cron-sidekiq-cron-31b9d88/logos/cover.png000066400000000000000000001705671453463263700220400ustar00rootroot00000000000000PNG  IHDRe8 pHYs  sRGBgAMA a IDATx{\yy>ofU( !\x^$[S1cM+5!JdWnWt4;=1${-20au!OMHa&<=,[HxD @QsNfPys*Uu9'S)5wI.1uZ BYjdz=.kWza0]`rkr]}/ edg`V?C#µ, [ڕ~u4MIUzT)SYnԯ6|;ߺt(|ѧ7Pr\{c[79 +v:=˪[̉?}DŽrxOݲ7 ;Vl{=o̷/K}-}PMe|~P3~^! Q) h|˭rtSO:ww]8{+\c:L<|x?\Y_2(2:P!^ SShk0N0-!b'Ra~{5;?QmG+"vK{-}e.n~e>?3]uu{*~8|:e7-ntӂxV]V\=+SAFmkjpWZAk tss%s0Dʇ&fרaVvSUZ͛v) AM mSF:?R Uj&o_"׋IK|%l5,%+Tj]*Lp_vUI TL jW:+'z^d=&en6ᒇ2.1mJx,$㍂ַ֪ȕ\ 6. $úvðx2 aZY㲋ň'VzR%*=̺^0C}ք{nUxķPZ_=>Gtݾ#P!/VښP/!z9ȸbhPnqZ19BH:Y!ֶUBl#IU M ;k!%/چZ etiչS%g~/ޕ*>' >-BU/˘? fŧn5D~ҋ>/ {h+cۑYmGw8tV)Tr Ri*ňtЀ}+3]an'dHk~y'4y}jYw?{~h !5mNOt)L#ЇúG0ឣ*u"*&@޿k [-V+SCYm:T`tz7`m[buwCuͪR[Jh_y(=V)0uJ{hX6F&Զᵟ6坐-?U{)MQy ,|XdzgQ2U5֗Z=L'>Ypv[ie2uY6;5}k2aI3WW}-z9*ٚHV=:Z)WθVV~v}~:'y!e~TJ޴>P*L3SJ4*d{Md ]3.MCxз-ބ_.}B^{[cݚu+%JY+CkqU=uR=|i9'eF;>j4/]5 ėvƍƒȶMn֞q36ܣQi ] qQ$ e'УO0ȄɄYY̚X)~bLZ{忚,RkޢbBUP`PfbeZ5v FFiCrw% s/ES|xRK:/8|W]lµVjcScm]> o޿?]eq ,q#:p~\k;|}}eSC5PT7MQ"ztվXW^v/-[*͛|QwŞ<#a˄J\v@KZ!K >]O7Yzۆ|BD( 0JmåUd+@[C>^Yn#ve>Àa]v +wJ2! "q;>*rq߲xKLgشb)͹g ^0 0eFkny܈eah+lOm+f2˜ڴ?:9.U`S'W߽U~y [sWԀ-n:68,Z])=VXp;''WNEYS17Qw׺K)?<:vuXZm%և}A( 0t`Մtg[VzHͪTrFЏ"/J/F5&}vXb*^#B->mZ<蓾M}sȎ#+wL-񅌡@r865^$/]P`ȥۥ7P҃5?m4j 5 kTlgr_\ӕin~Gb-"&γ[t_ j-mI_;/\0m=o_O!>w?rtj~|aL0tpu坷KU\:%','"8?yJλZ&jr[d5R& !4>ޢT;AP)T2?Zϋ<- 4ÅW6'=1ҿ?N֠G5;jdȚ|lsKGM=5 5}| ]> ɏ/swˍo2 V? F( 0BĺRP}or cd=6VP5ZtԈ#)v߻hr6ܵ7QOow} pNŐdFzLS{t\Sc_WrmG?_yVnTV}.duQjXڔZBۤ-Nd`imS{.}ֲ.W>ȴ^DO01*PÓ(,CLuh㾵eK| ;Qb V}B~>Nغn-w}rQ!v=)e#8xRWqo:Kg=4b mʟ?'[v?ޗ-o*C( 0|ʴd5ժ%_ŤZR9jmmxc9w}PB.GĘk5Ւrc Srcxٶf ivr=_D?&v?SlsiMwzZ~oc0[3mQvjMGYpVγP<x E}UPb% Z r\U \ٹ,c5J?Ye"[hYmT~l9T^cݭ4 ݝ3C)J eS<(,02yu7PV<%7x^F=)YQ|@ͨ1iٚ6uY|ѐvҡK, X^u(vƍˮsJϦͶ_H1sJl^>̮ۇNuUHiuXW'LW|aa0d\*'Sml1|m~ɪGC,c'kKqAfUC٪_a]}@[|0O*jywmS_Jܺ8R_q[ N;5jJ Z㼱~ڽ?c8 gv_V'_q9ÌP`<0lC|xw#ogem6Wh 5ڎc.SH)d* #՝/ 2{̒J9 fqB(k$*VQ]_}2W=ݵ0iM6nIG a'|NΨe'P6U-떪h7Wõ뙇Ymư.u+ d|AS} ;滺0>/{zѕj]OX=6"چax7NǽOsBnJOԾgY>,%[n|`(?8VOqZ?`W~WfT7^*>zAa,GN4=X΄4_Yi:^U1_YUlW`fwLUT)J†pvh%U?د_]^[He{ K]$kT[{ g )-5MiM[&bؼuY tbHKe|-ۖy/?Q1 ou{]r0lC6#dkY\v[VVY^b}s/JsJ'tqpT"\Imx-OCi^C(r˭ƣkݿ8BYaUǕK[};?yخǥ;!*0cCN|MΙi?GE[^MKGRJ`zVEX-fh![%f\C4y*bugs+{~g6 OMm?-pʩ/es';bmiYZIVkg- ck2j j\'ypj6}+9CBSLv ɶ/sMP_m9ՖM@vod}Eglj#=+4meV4T7`kdz)ˏ;|Wk XRlxrkgK'UVˊ?06|>(zIo2,tbOYc̑cRvyw@nd#$XUijjw諨$E'1>D]֪zmLlsBEhWyf*51_ĊL7B5&i+̶1Xd>:B~F2ij@>L1鵈O5lN;ʴ1}mK_Y~?KVv"e Y"^ZtsD$A *Wt>[-ˊ- s C8gzQg&QoOO/렪mAU0gX3 ?Wկj2;Mb7lʶP q}+EtjFJ\0c^),)]+ZZgl*in7Y6畵(ȤY[~S1CvC=69dWo*UqpSυcJ6VPB?}1Mo¼zGYC=c{U' U&6>V$n,kCp#v`v%eX^;P`HXޙըb7U1 83.Vg:kc Vckhglj1PmjZWښ eZ[,&`UC຤vsLvU=׏*и }c'dUIsj^^sy [KeL%ت)Ko/VϞ=}/䴻TZ k ׳U7:C𠩊Ҵ[P/} ӶCF1S! "eXڟl`glQ,50s7JxCJCũKe){܅q7q N9&/c(+(ZAi?Fyˑwoe}X\9kչdf~+(`bg1eoK7(ey{A׷&fd/P5W,Xye:*ә*\aU^Wj%xj xZ϶ޖA/C)r٪V􄜩6TF8osY)7`_F(tnJG۟@UBb᝭Ez 㮯yGe 'O@ܑ+:ʜ c@ m^T_Dr1\ mM6뼭wB0ke)sʇEׯ;*P`H~>PvY.JO?CZ lKcog9Rr]AhCGI]>M~'R(sBF!-ep5?&Ut.5eyd\@[K+T4.I@J_M,:aZSǷd+u5=?uFθxI 'LM'J8o` #caRq{H J >)ݮ򥾭t#t 0\jY_'oz챿Q캯#ȺG&/-M(^/c9t]gf@U%3XtJlR]ˀ-ks܍;Ȫ>e|([W8MR)[ob.Z;ՠJCzCo"&'j8\w_>|TʰX?e˜CP`X}:ojY؇_pl,hl*;d\.;Y^([[$m?ŰM7f0ZI[ lYQ˘ R5:GSmeeqC`[.5F5{cW=\tEIc8*̆%}V/_r#Z&ZQmW'ȋي;o=Ez2p()l^d1hD2֗iؠUmY6n+8imUmRMu'/'ۧ^n(/ Z)[Q;c<,brB*V*YkV/@L[BYhլ),͗qec> ߟ \6˷_*[T]zkF–{̿6bKyg^`Ć^l3v]jB_[vP\yUֶZ_J fm*f%˟?ެʪAl^'vƍ`1R%sXMT]-+&][erjTϢZUCYć^ N- ,Hղu_-[wl_b0*%@kLam_>H 0+s/US.nqcx=L?rO\U[aB՝4n= '떑-TfTS͟x`9`?0uYdkFν~/?`vWCm\t Uʖ9SxN؎g%l r*Mn%6P:V;6[b'ؔ~\tIX/X,0qnj2}(;ak~s+rtG^iϲŷ}mz2Ch@}ywN,Fi+s2WZY㐳E[Ym7.sf#=$NdjDyja!J>b fV#@X+fc0mg\7o_O,lZ/oU]4\Pk--Cbf9Lh1-9'?Qv;Me"҃4xBД`ˠmi0P| Z ۔gqq/耞 ㏻JB*[M`;=O1:i)S~P\5tM7\q ,|P ճ*چ\t/iӐi78S_}9ydrjYs-nˇd[\PU^|ܸ^jWN:YZZV̺4oPv:V|k-[*+6(mɅȰrB~݌i`!r?`!`+ gFLV',uVdzDZ-7+0>p^< xwlZ/+o[rla(Lɥc) %/^w]([!rNQg-˛G( C[k;DDVc.U^aXl)m<c#/XIb ĺU>ŪrZSdSx)3'$ M~B2g(0u*;m%l%_5lk{6mnKغz]zrv'ej㥭G4}mK'诽{)U7ɻmc]oh;iMXA( \ j:C|0$AjaOC$kmll<->Ɗ(kjԾ v.bVdtv]d0l^鿬ԖȪ2FH ^&OACcַ-&VB&kﯕef6m6_4-͘;ƍIaYM5voAtYx?h^I5>~WnRiղi{)u4M>^kC/'[m݃;c` e0S]^WTB+d+b56bt c d}P+>({syDžL?v^4`V+jq[vx>ƷznkPgzND{Tȩ[%d0?}?)˻^'I:}Mf94p^L < 9cfp-T"_Lzf .S_zx}R+6o+rr˫G^ڻ.Ld^0R[g l q۾ʠ'LOU&`Rn+ ,.-<5,b5@VbCF~k`lVPM.?kmw|ӝ[#U2|2*ΗD>kJ>yI>yNBViC>۰/N|PVvy= +g2\EVKE}ꂐoÄP`Xť7;Qu֤P9åhlUCHƩqsCUJ kq>F,I!>4ۺYƗUc>ڲISgCնC]ppam( *5X ֟sI36Rfτ eZv8Cl3ˌ ZShm,y6$̢㛯DE&VJߘCnw@iNt}| YR&wSv]h`6]E/U[>(\([p=+s>,ey{K<^ BYa׮\7Bs>l-TZ*g !ml ;>!mFM]ճ-)[IgWm&\)Zr))Vɦƚe6pnce[K4YX[͂ٞKr>Vf.;ұp__f$ms*,,EC][󯛟P^y9ΏJVo*z}vTimCѽs)6/gH[-ղ…eyW~/'k4(UM0\dK6|q0ZwI=\lMغ,q1V-1uXouYk1*>&w=!~ t-KhTJ1 -AqT ;Dz[eѺUR M ^2MSuĶummoSܵq}em~/g8lkc1+زx,~~m׶v'd=&?xq^cxL_g5;'J M-*k*h}?+u;?UFmR]|5η-qNkYaC( 0L :H}.ڴ>_͊AʸЯe:XBZw{]Vs{1y+_2 JDzm)/U`i{[Z])IZ9NcIU\jM?"HLKl-ukiZ/1?3I) Ҷb_eSԑܶ'a;/%y"d7ln ?x|lT9hϾZ6o5Z~yVyo\a)xC& 0teJ=𴻜zWU2۽aG耣b3-v†jŶZU_J[S2\b&lR*qBVqM/@4_J~iϿ2iG R]x})B(9rle/ġO?(o;ONӉ3Ȇcu eO)2jY}ϯڽWfآ9Y \2v弯SS=ot?U4v=w$@s'翵U[o%dzjjUֲ9'cXXe55!uZs͖"1|V1粇[{'9V̄ d߻>]O+.&t}\ [_qoZЪx\be9u!m f5yݲzd|y9-۷O?/61F[wUj5lzRgśooRӇ7ɀ[P`vZvEWܶ].:zj~@UpeuPU+uPKV%tO6k`|T0p<:Xoz2DФŴ SڙqmYwq6iT-gYh{-f/_ߔmҫewo;`ݲMuǿĹekU{~OF[Bq-vCvoj` :7i?gʒ^tT/ZR.Y]+0ۥy~𴖆O;-o'Tc1~<:X*\`]UT)^~hؼuw;E G:OHhU sb0R0ek2m̳SgvۥlhntqD"];\QgQjIa68?FoZ `d Nߛ{^*Uö^N4_v<īO?'< 1֛/6~G6gUn?z)Ll7ȎKt4@[0ߔc.+[heSYݿڍ*Yqǽ/>`p;!L-?F0eP c ; 'O za|;Y; $SSk<@r/tbl-l5zE/6V :A;v-@-;~<[P1;nCK sڎԗ״?cȅgȆ~`V;|ܯL:щ#50#J6k_w1`j[G~Ⱥ 'BOû`} t~ǰ:Th;6qdUqkDzsַ P-?re}]6/E@4mha =Hۨ</?|2TK=G( 0thO۝j-Vhw@N?*LdGMS%E=/C (~F^[>s|cL/V_ėD+0ڏZv2+eqJ{jmmC 的J eoJ\櫶oFy(OOBm:_{oiy xw=ZX%K 0Re&U0 /"䭋}h5!8$8/ "?xx|usYPzje sܑr' y]BqSkeM^,5k:}{!_Cɹq>U( 0eXQ?|h7sv'.d p U05k}\s:rT{ OiZ~en@+,6!ӊٟ=yYo?pL}Mx8=(a8?V{w=!?azSv1uL*"rӁtw[>>IA3:vۖ,*d5+2>DP'% Tv:b =f˼O^oݶo)W5;?)KB>M]k.}+WԆ:ͮ{-ʉ/ #w?0_x-xC 0ReI}{B$pZhAnIHw7ߘ_RQGjKNzmt͢8_!Rجʼx2w7SDl*UP_>)kL8Ś^L:Qcf]ܲNvǭ Gmd}at> ZsĥU֩>DLsI}qqU.!ɲ)G"}rQ \~>o;p4T$>Vv;o7H4E)m*콤6]6 #E7tc1{9zƆnI7C( 0⼲qa\?M0{Ica8KU)XF3,1eL ÅnÏ=y&V f~03m).Vdom6).v[ߦ`v,vjxQb#Y>NFRցF'mȏ8o\zG ~ Ζ| @)eFx\RFh7[L<{2j4$PzP=wPe0DC6T:WytPPG‰@D`Ed{Yn6Ȫ[fٚ+mjx`ǐp3GXk|}Пq!4Q?Cp! g# gԂ~"׋L q0q5r.x6:HE7S+O?'77Y㮇y`>DR_ CىXWcƶvY5-4fmQ_/8J /J0D3&=Nܾiu'sQ'dVՊ Έo.ȴ_} $Ekv|R~y_@^;N>?eCn=oǵpi9…gW!9?q߶m1~EjF\ RN`ָKCn ,L]-n?a]{LXmd,A[=@_v'kt.jMI*!~'eߓ*Xc/v>J{-mv=;F+}xR-ji].CCɺ ^9b#/Y5:` Q~Xeb@t0Tկ6}ZNZ)sήv՛o6(+6U#0uW0r鿑 !Bzl)_kexeטjSZe_t$v3- n{w鷝SG]wA4!9CX_+ai6i?718P_hoJ>Uc H؍d[ NNۿoǥ ?rjIƄ\X1섮z<EBti!꾲4Uj=k hsW[r 返=Yd0|O#ηuӱ%UjjwqѺ/‡q ltL,_:8bԖg\Բ6WֲLC^6Tjgt}i9-^Oδ$ ĖThӼ&,UHUT1@{Kܳ"_nmx.g̴Q l2ږI5F5lKƘ|>?WAg3k=g N7Y &;6c>kYN!krmkgE9gY%|xpW.m6tmAlN)͏۰M}_˗xY_\FgC6?UZGVRVo>]| /s۪_]κbxaOe/r{ݪy4߹8Nsзo>h!펰ČyIi߿N8~=9v_'0}ʘ⢶ͯ͌%[^t.b[I]nw=QƓFgۿnz?wHvO&]Wm,1lTiߗs^ώlej8.-l\pqچ!F}@-VԁUv܅񢕶j|lQ:<~~]_Ռ9%9*]{)(T8ņj|XUƆ q#dFą8pHg'B3搢%Lkut}1Lt ./|ιb܆G3CVcF0jǓ$k),RIal jkߦ3\Ć6:ؤ@$鄋s\ä#yX:ۺpLU4NR ?&۬[۳-xW[\ք1gqR(NlXOlHkENbH$y'^sf.v:8} !tOLOРzDee' 甞L !έr>bClO~vBX?7{١C6)O63ag2qmd'ʢO}IzPD( f1 /6\16/Yly5AyC Z-M*m'y⚍e%U5CT-+\(mx"X論{[X}R0Ihp 4B}^cː 6-Wg_vd}%pEVXTbd~qжoǨDʸ) KTU"MMۚ--OW ۥ gvP<+6ߤ?H&Bo4/{Bhoél[^ @Od3J>[a͇& }Eͷۛnw| z;<=C{C 0C1{㸾>Or8oe{0q0i?K˚𗚊 - 6@(g?Ap1~~⫊S~xBho|W\|t+E "Lq<MYdmEC M᭴4ll [-NLAZjE拭giCUsOo7Laa'<ʒj):ADϕr&,9_k pb#MG{+-KltAˋh94M\J1Yl9t5Yװ|{,U)U6Q\vZ^g Tglwf[ߑ*Z9lmAf3q,_dlßpI;2z잠E$kG-/Ǜ^|K˕7}殶ԲjP8*V_ԍ#T;oR[Py[E Qu fnw*fqP 4/*%>`#V{mVqMӀͫ[#e0C4P]K:VyߜWSO+5tjZ)xʿobcn,9%A}kX/5l_|rho+-y-<~lIf Fj[l ۚĠ֤-+Y=ڬf["n_~Mqk}m,q*lv1#%fO[l ]N;zzד j6o^ʑ:I;㤌Aw{X!ଙ9^9T:1e O|kKR<5ЦuH'x!G3;gf&:G;)[|M"BYt)dJ%k9UaHjkaX*,We|*CHE9fK\wiݤhΡQ0Iؾ3^ަJ4'j02%gU&,e/x"rFi/C;Zinߚ0/tf`6p)& INh"lK!/섩un7NykA1Xz}0^ިW77.cNZ+˛ˁj6pU4ŐVI :~Wk {#孌_[88BYt'UJ!T #Rʒl^o2 mn !6Ħ%l*[\bu„<dKئye2 ~aG)p-+<ȌtVf޿5>i4)&Ŭ4YV|PoZ\+vJIOeS=N\^m g-Y\>9~"ߵQ,}[ԅ(͋4?֚D1[N\b[,.oo{a[Ax;R8QA\>RYm~!߰_vVhE( ( <g~n7f |fNu3<3F{_,ehʶQP)s]%m_Ycb4HK,_OKPݚp츻mN˾yDN>*e`f̎#eXxjqLغfϽ~LJ.#P `V+e}0.crq{,Ni0{3,"sΆ`6/Yw_{U*Uj/F,64ŋL݅Z#oRY0v`t?-7XԅՆbTt/knEV_>)˷*S{_i#P0&5ɃYMjm|telRY+,e#-+0piu5͋!LCλʴ.vˇdYd}hp䘼IZ,ulÅ$\({Ά\P;TAξn Nu'n~ocK܍o?s|}7CZoY4&`fZlk]]մVb|"TGZ)ၲ}j͡ƻymhuua!VJe5օjjCYAkb+`%|?~Bךfm~sm Ykq5Pkc;-ayϬ BY P#lɵfC0\)+Y[b4&%/5ݰVv/}uIdeCP+Z٪c kXbU?ll]xկRs~p2iS,0sFk _[;x=nrF,*'WlBMeq bTڞ=L<-(g^˪m|C>yy(˷*]A^/TjX;9BZi{Gu>V7t՟17~E{` O?/oFms_}{ ;m__`P) R,mbzJF,zZCS_g_AK깁fj`_xͳGYEZ0阵frq괼YYjˆk_0߭Ĺo0e0opAUwnE\.+C fG65|l0ނ'MOX[vO vxgCRU6Į \]jf2H @X %G%}rYZuxgGP}|0k}1M( v[oͪSkZ wҪņc _7?ߪ~vdNg9m],kjRsP- 04ewK]>Yy`&mEì]+enlهAaxuOmNX xch{Aq骄H &\b"]}vڈWv"ZWC|?!q6K׊7wmU^X4k4PP[߼w!HuMˈUʺN" 0Te7p[_6)>EV޽-SکseKY_{ g]ub-p#;-?5UZz9} %:,U׽#uVI[d`X;>]حչXz͇Z_k沼Ч9-5z=v=/K6տ.Z*gհ&4eDܖ׸m{wCS5%*vߣ=~msuk/e-ÂPsoAˠ*c|{6^k*`Mδ,?xz_Y50c嬶5~?rՅ=&a i]3%¿ 暩mW,.+om {웻>Ah|뭾⸗PK_Tihmye.o)VVz}6ƣפ{l8 `Hhk˗{~@Vb_AQ d[i>%y#C)Zpbi@n;[ېwKJrjPI./^:[;<\?~"BQ_mLx&+]{]Wr0Vmݫ9)UjZWD**.-nښZq'̢h-Oھn5}f~DsY$ÅP9c cӏpv͎;dڲQ.+>%=_p;?:g2[*æ~5\ڽ\u篊`[G@!6H=ee]{kkVں0)}]]ZJM__3DѠ'ktsV޽jYH[tҡAcveƈՋgKbB( 0@[:duX}=*0 4)-[RyB6j$m1ɢӾm|z178Nhd,AΫU??\'!ձK5K'R5RLG6tY ~:VYL |`V+fu r}TzH f۬]%,(2aNԮcˑ'>jedc;9zr_ ]N` $!8h WzVq(ΪFWg=o_%(CP`9d׮C'UJM_ZqEȸ9|kFJRݎr,z+}Q3ruzP's@ ^3F?e~v{t9 `mӖZ}֮0hhi@. e5h޶wCeyvʽڞr#Y6Ik3T8*=SϷ}f?[OCdV: BYQ8ֻmO `V֮pǑWhŷWQfd4Mtl=FOc?L(`X!o@O_)iмJx6o_LVo=~ x0F1޻ujZ3!Flxt s BYaWGV}k'G [})mw|ɦqB#r:_u'\>SSFMnq}m?^F?0" 7kze2i0-`u|z ,}œ"6oIwi=Ÿ}y qfG506 (ÃP`ydھEh3Ę毴j\ڼqf6f=1zS|Z1jt٩g_}t[{G'FR6\R` 9m[u_uNY#ߤ}1Zٶڤj?$g%ZlG9tvVz6.Ӽ{ `) 0<ek؆ X_d8Tks;,[2Vmh7ehǟvO,XqJ R{k;d|W݈N@ `?` )[imn|sڴڹp]^ُ[a!f\ʉok~䫶|P̲|Mk&}iws}y/ׯ}cG走׿/R`x 8iM_֯o᠁/׆z[,_Z C5NFF(.eIڂ۰-a,FhgvYqCj&;录Yf[Z?yէ~ō<,SϾ 8`&Vd_jٶ施m4ܖe1(&EY{w=M>g:Y(:J˴/$Vj@qO+v?K!n5?&ƅ":oz捽W )thjMd|Mx9{]nU ##h 'ÄP`h;?Vf2].K۳^~82E,KmhmO3m|ɘ`8ڪ=E__AV.uΙ/ n?3`^q`3.O+nse-ei<+umܥtv Z6B%?onmNO &m` .nUCeWmy`@Ɓ2Ќ蜕εj{/~2X=s:u_履M!#^Uv"˫%et`.}ro 6^u4^` ٱu+e坷yw˰A_X.VGk_wXyx9Úe7Y6)P:pB)0Ne}9zbk]mwz17=Fcj8Avm_?!?-;6Uz:gO0!5=mjXd5t]M 3(\Uygej Cin]0 xXumR`|-Aw\:W(;y[tF*B'p̺0u*챣X%+)#>[zy]>J[߫sL}3lrCP`_^Fb*[oUwo7ݥhwp[=.+ܺj˭rg}<`m φ3H'i\&7 n۪+c6:mvS`v3G_){Qn}86?fQ =9䍇ȺGizũrl7fwmxߐ0, XbEiضaKP"w^-0/v* YDɳ.sm)cPqQ/mW0|qm55ܾ5n{ۻɄ ni!{uqizRUr{n5~OmSUviyFHPX`8t;HQluV@B!F\{i%m.W/ڬj6e@47%ƼГ nr|'~n#T^_3:R5u'OI?iEk(00ShGzG9ۏ痕t3}=?vV 0de8-sp˻~^}3_Uq7*yHíSml6W`AX84]l;⵫vbӴ?o({ֽ#q/I[J[o_24#i)BlÀP` }~IT2CQ 5^0˲͛ZenXEzB\jz\02VlV~6c`]ٶ/NH0j˖PzHSF 0Le*7XuZѸyw=oVj "uX'jq -muw杞PjVYک[Yޮ*46viX莛Ml*肞Ttۦa?oV,aJ``X,`s}04LJ ,޴~AVܜ /І]dG͘_-s/_P-#'',rVCYw]}7(±{ϫjW5>-ot=ܲ]ɘ`с:mkl\/֭Wh(k S#G@织WȄ q[#+6^*ZV_yx|趠s^ΒM7om/#\qW2 dB;?2wӱؾXke%BJ铧=uD6) vx=>lmj'~K0`sevB( @i9]x~.kvGPwg#|]&zXuWlQxܲ|P.\~wmW3nYWy,uU_U:c*ώlq~ꔼcYlشv/e+R~W._mmz2B=Ir¼y]]s ?ԯal}ctLj_[]NObn5~g7P!Xb↱b缻 -^qmr`7]( H9&T=|E~./?7r]Q8{|y 6ɍ7m !R %|C_yoZĴJ!i⯫~Fn}vˇr|u[[,-;>Iqt–br_t~ڰF=R=] *5MoG.kW^=6C9e owZypXɊ\kR+o_j0uӞdr[I'}={$>UvvfaT4'e{0m7|-k&[x$ Q^zYF[}wGτ06Wje `[w~:iZ{50ڶ[j/zG{@ۛNn1 SX)#I-Ze' i: Ƀʨlw Y =ֽkvvq-}p']W ^oxJ7Pz.?B;[-k22>gm6 XhiwsJmzVɪu𕭩zPC%>p aƆ1u?t_OjH?qA܇du_8yF{ 'O͹ Zim/[Jp'͉ڼjCЅXMoMKX!OPc*_I0&o@֌P>o PK=Vk׳l-) 'm]qՓ 9B @&羓-^W۟Ls݊4U<f4݅ OMx=#(]V`N qr~`V+fB42F( A'70\xUzlE؆8oEamӼ69VWJ 2Vͽߓc̹< )mg.?W64߀pi /'l/3kc-gǘ\~GoٟVlXq~՝v[vm~ӞߕW}rDS6ҹiMavH=vܱ wK7QE6GF5(:6Vs2,(:&0nGnZ?.Jw>}NSB|5Ln*YHmLS_ʚͺW/t>^狗]& [442v}6H7ho=W;zIߛ޺{K%g_wKL>e*Vh=s V _yU_+X]h-\nن?Z+{:~E'],eYlq O(ZSWy-f~.V޹ߦMBi^ #wtf#WQ M8@( P!j10_fnh)Ӿ]>Ֆ¾eM !Kt:Yt?(w5-8:yUTptb}o0!c(Bߥ3e~00T̥t\SCώq6[w͟\ G[O TiB( P5&CK;/WDd,kEnj֞,RȦz'KmT$- YngUu7wضXJGzX=Oo]V?Q){,je]v[b>^'ulhۘ8j-Ͻ&TdYpZ,@vZI4~b˄GM5_/#~lƪ:)7wiI7fo}N~+jXMVHM| IuNBXVme?wBϫ'm/TUK/}pH?Cb = ۅ/|/BY0lU)1'W[jŦ[u#&>'KTTdlDp3ۚΫxq|^_Uz.c# Q.f;V I;|or2'rO;˿yYHjq6Ws<բ]{ePn6nslwu2$[+^V2nxMPV[ kV=_,`HVɫ6~QάvԒp޵:u2:hzd˲t9V9-Z~ǭ2BQ3NXc=k w~u-gr1pܼY,Wl]s)Z<6^2P:(jePB| -e;9SPL{3.ljƆ礧z?>LVķRj-z#N`Lw{5/Z`W߸0mglJ{{cM*(تõ`oiev;`о"BV˖tUʕ eS47K{:wЄ%mq,#.@+˘/GO^pa+¼-JZsNJ+9f-*-p!D( P!bsxYEfA QlUvִn'i 14u>WZ3CO/QqO8$8ú=g<9{=wKNϭ"kK@NM-zx]˙uBb0JV˴Lʴ/>f[,gvyM7B'P\6=Eʼa,Fa{]XuHNMN[/)bgƍ_57s2_ʘmo67)Z3/o*WM_5V{f P'Bʲ2Lm2QUhgyM0;uVQJYEf iS zٷGrM, gv.ЮnȰ l!_o6Wԯ(qHP2by-7{3L`mJb|+MٌBiRJ] JVuJw[wz\M0ZӒi ]@Oɧ\8jM2Ȋa>_C93νi,aפAa~6YrU2s*Iݲu{ n Pa -xyh|JZmCZY"Vɪn~FlR.0 C]=65֘8Ktq;O]-/zP^)_5?'̱ 9q`ʿ^jf\+eGLC|eb7ps9dE~sp9Xe7fj>(acbbeX_Urր.{۝M#!fqͶJ]/郇}; _)f(,j8.cҔKLSF~F}Ȏי}S> Su=#~vl##;枯}e(Vz'c}GO@{s#<,RlLT/6?#5YޤPEjm{1l#-N:[5qYUM͌v9CyQ,%E图Xmzc)> eqxgG!Ӝտy1θ5-i_9+r gΜ]uM;ovŸυoS,cX+롅q[#UA/'pOMVEj[3e >BY1Ikc%3Kib*mP-욐SZvJUϘsnŠֶJ.|-|O2q^ϙnwtVe ,ӝʣ9 cWP=.YC8SOXH۾[q>j g{z8+2gZ9<&Wk?ڪG&{tG[ v$Đ0OC{Lox[&}\Wץq,/,P Ѳ(8Of2zZX6f|==F[@v&&|L,]*iY3jbDf-ϓ=,)>8!I?tQ|*74-۶!lX*x[V85|YlN%'ϸ  T8;k=QtL쐄Y:7v(C&U'dgΛdc}3/gx}P b4ww*}P`}rA+gm-$ƛ(TP+iFnj !&er,'lm_!Hjf_Os^͋\v\ݍ=[X|l#5}|\=>ݯbԺp6JzvǕi f?#Sɪ7I?ڹ[^=9XiS{FlUplO<$W9 -}tp/_0[pVuSG( Pqajd};_+ Ř4@A&Vxr$/*YcxjSP RbB<3+cKk!dac R\mͣ_%7^UPNnIFceMag_oJ$K 5m}aWjh5{JXY'p]V)_={ i>mQ cCu}8y%CI#nnƳRijmŨ/3oPU` Du;~VBWDžFl1IhbQ&紱"Vմ3K#m`)M37gz4';,r0=!?{rұ" `C[bbZL[}p'¤:TQ>jjP*bڙc';^Y@0ﵙFڶ¦j:aM*oO+Z ;M[cȚt%d-Ky?f@ě3) v t8z(\_{lyڷ-u_d,Y{|X]O0lpϜlK%ɻ~W~k+em윐룁oaZcBKiRIyonxDa#7uyTQ-9b3|H(K>u,ULVR.Of=mc_=%iN~R{TSXm*i?ye9s]n =![epe}#撆Y dJ4&.ׇ+x WE[tɚUۏ99yHw?f}e-|ڊkO[uA*n PU`}rSkc}6xw7ʾ T.0(;K43?+ ceacQۅ/|6*<Z~2Y.^lmM gQڦxm7+d<2O~rĺ@vҔFV%[pR''k_ѡR>e*b1;yL٥ e,- Xi۪JaWW aӥ :POb-[w:_W"I_!Aja?Xn\>alR]{͡q?z͓1 fb WqSR>e*"Kl_\&]2 gYP PɊ]Cu?u|ݛ'[Ȱ b\ gj k sd+JVοB*_HV_>:WyeƇbֶdݟ住 M3 uB( P>^gJ̲v?_t͂/Ǫ2NM-<%"[\: SS Z٨USz|rJ^19k406X!;Ti{\AήE3NögDw*gXx$tA( P!mŹGO/?5 j3]^JKozQ:Wx'-S}-;nT5V9[(vDz> Y#;?@4}lJaDֿzoDl/n)цo*c'cUlSFml[.@VW=23ğ˩B b8 WŐl6*6?t\NM! c fMWVȈ56V'nfWыm-ήev'J"mFRvYWw#3C<J;TV PU8N+Tp!komaZ2KovW!-'vpֆum6r_3cmÇ>3d5myѶZUq7q=|(6ov?y'.޾ hObطeuqYۊaZoݾ-sͶfZj>P"__A۳TqU[^eZ1/2.߆efuls՗vc1Ֆōt| 2Z6-cͭʉdL-oʯ%K\c|*ZLlyJ~oمr`hbXPVVm$BU[;@= *Xh'K*־Xf^z~=z2 bSɡe;K3G?tY߆VJ-3 /|LxDy|;^dφGm#TNJCx|n|_mAzIc_[wQԥ%Ndt鸟)}aywˍ,7m[F,}I2vUrd[Y#9w ^ cbl%؞:~p+e>Zhr:m0lTpy1'gRoNYi>U%B~UZ(SkY|FLV 9kʑ=G@VXu)wN۷ j5>j KP)nB_wM_;bෟyWNu=󉃇횼4bK5z:qTPٙ*É:~;e[k;6ƽ^\y:YQJV[TJp7qy5sL.XٍG\ZvهDhZ`HC[bi=!q#bmԀT+dSH e.Y6,*}6Zd>v~[  gI}L:n, s,uB( P!u uPfVlݚ*c7\ٳZUXJ|ےmOj(+v2QeՁ{/u5|O5 B؆PS+?'$.tjng}pN8>BVCT:t\kȽo&ܯΗb64?=[۷V y?W#1ny)ӵ#`lcTI݋9).eĄjXg9ڹ% ~;sojYoY~ǭ.@yu-:keg^*(S){lX6̒*W˖݇5YyE7ZD3կotSZܱGSXV}4⾪XGWP`6Y_YٴYt{];T#H*˖B|7{&@}TLQvMt%VZV+J kk]7zgKl&X;lqaU-=[օfڄC60֏zN7O>U4M6je BKf8RŬ ñRo*/:Gf6 a'>#ŝ~uu{D͟Yz!l\w h|REsUs^?5!ȚK]u>\VJLwq>v"ngŕaWrxph?ղz{^zim|ȇ.@?Cy ߔ&dl޲xR YRA]j?<8 f}P3mW兯<(.@<$olzm4%7lOdpN$9+Oo XC6 .`ٹiA$U3y;`N3봕^B{kq_oS/:_uxwTXyG[(k!=euV2uY ^5UVqUZ-'9tk.)w|bCѦxuϧ]hy gM_j?wYG欆6#&Dni UKʺK=TM?sڈI Ӓ3[.zw=䦍_ջ{<J5M> dP?@2t\nR߳ʅ J*em^Dc ~37*곊GWo5~].Fv]wsu68&&FR]Vꏿ{SjMղoN_e;UI+t?H~n{Ϻ˛.jd<Z-2}dlkU fG|:XP}2DrA.qSl?_*)nt6=[|ܶ?p?*<xE_e;j ۰$ }cԥW'O߫ON-&\<*P#/*q:~_U[+);rzwsR%c%vZz3FϖՊ2Vy.TԁOwz|Yoc=!quhb[lU<$BBFNua\Im! VcI?Hl$>T󩃇OBPXC1W9yHNKhK*pפwP;2O?h彝Q-˴w:Yu:^}BfVb{. lH>& wyy(qB76?]\-Bl$KZZJX ^S5)=nu1-T:NwnCK؅.\֟MZ3Jhq쫂j_g-%T?vV̖`nLJ;jPL\3erGխK^jBz?>.onx%^լ5f:> f*͔-oGv㓇:~Vʖ ܯd2ղiduuncrDB@9®K̐8Ufly6mXicۖΆz(ΚN.j%pWN_adcV#mVwwMȫ>Vvq\',lORve** 5}3Q;I]hլJWV}h|䙗:2K:m.V4bդSCˮZu9UnWf+*eUӌR6~gٶVwo>oEH4Gw$M?sE3\+pvAn3/*w^ mV-Kc *W^ٲ yg;~kRΤUZ}ߵW.G,<.l*Q:v"kjlYs)5ɆEw?= ~n f'yTV^V.]ǪBz~75fl.}K7gʎ^27b!es9r(mTV*G}X_]Zk8Н1$,';X]y6}\7w>"ui1@6^饱tYNrAQt1L7,4jJny زa +g%gZ-{rӮ"Ǵ]CN>vXe*R^6_he;_8yޔRm &TK˄WRÿ?+/k?U6F)~ #c5>6T%ٔn=}eHSl]*@G~Y [K]O`7c,8bnniUd#VEjB&K/Z-U$hvBDٲ'v៯1g)dX/򦲬ptudвK -) 1U:{cdZȦ_Fwj]8irwv^6u*<}d~Uz6}{0rV` Y :9E9=y(k+4]-Jn`6YiC.w6VU:}Ww5HYyB `:g 1+v÷626}n?8UfHYݮpag,@TU\mfcY Ӵ=;~{fV '](-A%F1 ! Խ5M='L;6Ҵn.3o9cR I3`+kl?J8f# IM!a.sfuFo|K|R:Z@YΟ'uB( Pa\Jm}*7ZVɞ8xH9g6cߋ`~ EjY dZot ,|ތ]Pvlx`DɅڱunlmbu fGeDY iWM엿yxkד®\[aF( Pq~T40ڌUwZ!['(.ls!nO]iL!pSt_Uho럕:zε.S-՚Eo])919%ǻp9.Yн֦`vжjZ f%T $82`g^ݛ*}+Z'oٴ(.ej mP\Yi{]vMr Ft ḀJ^~_LJ { =+/Upso}q gM_ͤj٩Hev_뭺rhsb _z7͆}wȄ`|Pf˖՟q]A#lLax~N#.l|(\͕,߼}"й8?~ 颕VHYX"[LPt҇»-cٺKUT:"Cu%2)=&[Ya)na>!̖=תe*=VM eq )h6 ZZ7b;l:^ Γ0X}Pۙ^m>&/e1LO"moqz V.U˾kBUZ‚h=븪䜾 Nļm@WdaZ*C A}gfQW˦ٲThսff E[ژx;:_A@i:nm-$mgT}w{e5q[R+tbG:  ] ) u uR է:K-apuӛVSZiN6\kֈU˺e;C-3Zg{+l<ǓrtPl۫5lD G _/^pTI4)x~-vK JjnaY/}R(}*3_|3Pu3¼U˞<${kcnWm,Wat/-חO G+> UQ=@ hhf 8$þZvf~4yXo}g-IEPnJ6cJ+`yE쐿܅V>贈_<L62sp#V<ۆ@Cw?"e!>>dPNL0Y]u|YybXq:zvsſzlJn3^&Jb?SxPAe$VEua4-[[jm媋?޻m$n7=RU1-=m1w DeeUL e45Ӵy߈Qmʆඑ7߳:,Ա_Stğ>6?!׿bٽ))KYQLZGs @}ԍ[DmKJD6Jmť5+-GboOjz\Z>3tbE 42eٲeeSfy0SvN(?ӱ}q=1@lcoegO/HYWa`Rv3ykl=սJߞR.P|#)i9"c9~PѪk]G+4@V_~0Ԇ^i[W?H68Oabr T'[%t]V˞8ۙ>}nsV>1 &2CIyiÿ/VVy^\e8 ,@v)i>郇'w?2nW<詋%7^5@vbSrlbV9Cz f}E, k8υ{B+аkw!yDnU=ղnYZjYP(&k]|sM P d|Vt 6(IM9 ,@h5VGkOnX@vp>qM>ndC^֚R.X}\_+a 7Ζz:p㥮w_V -|hy5Mݱk}6em8É7o-}s,WܿAV'/3zPL ޴*rķl*I]nͅ.|~6年p̏b X߿9=g,}1nEXMdA_tNv/P)k}_}|csٲo>Zvo%tUX ǫA:hug}WnQ ;ݞBũ4jP ei_MȞ-O9U0,U59s<[W#Z%-qY ~9U^M}Xe'JUjTj>ZSfnɶۦbeoۅ =T=dԄC bV9T☖h9/VI+=F? ?_6;m6*natEjvͣ_'" Үvߜ$/}NބTeik}7J.4фJ3me.T-kD. !,f`zMgΗ?9,R6,ǵ-92b]g܇@( 0@tqV -G%u\ m%foK :;Vًpy DEokEa_t[`~s*VeZvYtCs|FZK` #=)_=*'Jݖ{\O\ J, Ifjp,~Eѷ\YzQHH|rpvntQzcݛ'[܆ţhk!c&Vebf/ UNxiY$Jļ'RH9(lBw71O+1Ll!_1{RwhlCqN+BYA7b0e}Ŭt ȑ=43X{1?xʽߑ i3 ݇cHXsd BN6`m@gc_8ڷPֽZΉRsP-a9}n3-7[HHҴݞiZ} cع=o_`,=ٺg;)ۥ:?_V|٬>xXyQsfƎ?3gnJ">' cS.@PcIxfٴr22/-}u?WֆJ!Ru2'f RAlmdiXdz<3ygN8-oo] NuE[3}e.<Ʒ0p̺Pv׽߹vƉ:s7]8AMU:/V/gwsM_+dc;[7eHKݘcKse .qP^ӦfuTj [ZVjYFΞkX>P!=׫{ Z~9'8jŸ!3|+cY4H? mɁ/aܸKr*ղZ=+GM>ܵWNM-uzfY. ;.S;iB{+Mg? mq,F埇4ڷ0f-rZ-Gu;u<{a ǭAaq{W _ mmkV'i mu_TYz- ,3futdM}(?w_?85yH^1_M_իd{iUCS{" ׆K.W'LeZa4꫌s@OF"[2l^,shWY ߭{-'NX ~{Xj2J?vņWmbشJ`AC9.TaJ#UV 7^S궮Fem~aT>COx6=] YՀWy2w*vN*W-;htbWhL.Jj_Y> mCi5e+T?38&Cq8붃H(;V_.'v%ESUo# i}ƿ`_yP~g:Of,eiad~efjl\u_F֬w}^Ti;hP"3z}ğӒV؍׸)5G;? B0pA(%`uKnPB]vg\Xճq^NdJo /U$kn[S׬:;sL`}U[DX.lN[ Hk 2<?k0΀}q(_7~_Cn'9vRry>bhq?ZmC[0KvwvG[g08TR% Pb~TBտ86v -wz"ZA_뎿v'nFt!~^VC)Nz]ޛؗ&V&ԒXI ZY޸H[i?гRaW}S_a3GOjYmc\s%u25*a:JUޟ4i<-}74&]҆ fsWzCM,@]?;cbklhŊFlf[c+6|-uVik]@{x_hՏ䔼g|GwԮr}MBf1n:6i1zF+1~ܖpڽ'ܖq}Q m5Cf*IɃ5ݎ-|ɤjBo7oMZ۔ !WjAGLSܫ]Fe6bɏU3n/R#{h%7QwǪ? *epqfZh[&5nY:wvچpVʚ0ob!^W*[Z^zՠv|JߞRyo*dǀU׏S^x'"?X oVyօֆ_XMm0ևjF<-֊ )<56gMG-3j7zeD6mN_g;[3%(nÇvSضB=t+פ"=gFڳ?[̸k+JVOk WgDi#COaF#;A$ܖd ?Ļh{8?c"f\pn( oQM[ +Fp7CT|oǰRv Y-me,mLϴlSmONq8n{ň@!'H:Ri۶i~2F8B ,&- .^$̗UG>0fcpۊOy#.{_v}W ą.M4_t\VIW C{^MhYf!-<6zFj.٪eˮ?b&6(yX> jz$M Ă#beW]pV{tf]=z[Fګ2*|=Or<2܊ Ḕf7N"1K{P+ }?9xvxGX-< 9Br51HǤM] 'I~M7ӐS)z!@BM 6p5U6ք6ok*{jCPֶ͗suʅz)3aBjj]+bjLh%l |~XR Vdղ<6}H+Y̮k$8f4- -\emd{jmƵ%WCJKZ7)yNc 8e.FUHo L.@' N K^sGB`塬' Y~ 3fPZYXj"k-Vw<_Kf$-&3 l,)Vs65?v2Mǡ,H8[8[!l nvwů}xr4 iƹm;9,t|r+@:f٧s?~vfy/>wyZ[x}Պc|$<{!BY5±<qhs5!ԖSpۈ4i)ӽ^#c5g& /䜪ZlF6[4#oxS;c>,_7?jiKrvC:Aߥ [XR/ZLםyʞ[̟9]':T}}x\|?6}R|܅>ۍ?AƓ%LW&b ' N({;d _k7gKom&U|ƾ, 6|8.w85oq䕤5*泱z"Sj9WqCdcY=6q 'm^Ǫ>z0o/U~y-ccKцosN6d%Uq5-Wu*-k٢k 75SfŚ+bCgT:;h,Z=fX FgYq_ 5(}2niK|JU3ʼ!¼F[*[f WaxdkdTIk`x[Ԏ5TDNr(]|ǨӊOmf= I "~3Y&{qqßՌOy&Hlfc]Su=X3#87!0<,@-`adUios ?fV9R31C3f7?4;Nhjk+Rlf%-0H `JԪ<j!8 [8un'IACP8Ԩ]g+UM5aaQjyd[s3l=Ϊ#9-})ly{Uݦ-Sbf~B,46{$=&+vB<6m-îZ)@ 4*3M3 3}>uطee+ $B @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2@$C @2CЂQ5R_>^.. 7c^/1L SeE@0ߕd >hXXft oXVC7 r#V&GOɻ$EAb\pOmX&WhT N O@.~ÅCo뎊d\ډ }L ޖ5dtB &.a@z-3.م}+'!Уƶc6lT6 #,m@\NJmw{@r](` t#c| <)wiy}J.+Qg]y@!3{wE5;1a t7]um޴kϺp| uhMJ;]%߻/loz,8xVv ^41>aMOmmZgad1B]W7s0r94<75ޏmݏ#v ӳn";!cѪ.xc%H$<v!``/]kD$'}Y+)Q }bL&wn4TĿ=loۀ=7l/6)yg_D7 @7;zM5`KXUWvFnmV;j:T1\ȷ_UQA>[rbS @2qLF4>tZjLJ/@ / |X6 bS.mʃA@?a{ }:-T-sON"Ŧ'kwnOf .pا+“VR?'az H JѝIu^RIxl}&`]PGVsWO"֮di+ǭYV-,[56xtƅ? A? #;sbw݈Cn`Г: #eq'cC)#Ej=ߥ t5;sL>VF*m؉E]ן| keW?qʆ=ZADxn)Z}ޯ0ECA x,zǐvU=hBġulWj[b8!1z1D4@D"w1dxjy "zl{VC& !z@W)zUDgc~`#kvns TA@*w} |#j:<>P?R x`sX/ʛ>r8zGCWz,ct9ݷzvo<DX^:Vd>@AcdjwVZ 9Qw>,D\NWICwb3 ^D|&`@CԷA۸ɨ[^wŶ;e!t)c}}<մAd!th3sLC@НGm&`@ aUO2 *WrEzB!~/3Jn" x@|fԖ k44v!nM>'akW|/WhBwulaV /xE<+@;PAwp' kz-רzOW!%{L[/d,dރB!ft,>fs<P CZXC}"~jv$6s(B@ڭ, +}iEtV>=r?^?ۏ?+ٻm %v5c˅^V@8' zc RO?UN'C7~>Y}Ge> !@㏮vݖdhQBx6?;BZG+/l9Lg@Mj˗ɭ{>'+y\-W,AZSi6B*[&w+ xO~NOWzR*m:CN}{ j72xǟ1Xh `!˭H7q ygy>dUaqU~y97LGe5:-\Yrc>=iߗ (l`i‡y>o!^\h߹hBe6,t9Lt fVj^뺗3ݪL1]pu8ߊv=6T0ΤwZrH#ntZ vTB+䪴C+qVh8aoꞶZaԥ}`: кB%eo-ŗg+qO7X".ĺn0u1}觥UG {n!!dVQ'OCȘ}g.O^ڰFϵWw>/g>!DNWP:smhFA[ :6|.pkWКrxU˹ݣ~OZmWT>Os=rE ]>5^| -JՁsf>O/̃ `$%)+l@{都s7\ߒUwe+34D^HrLUBvB„|ftKbFld=w{[\t֛dksU]YF(@0d&ָt^qs\@x[[MO<@fƌ(áڛ9yڊ3gBBecSro Λ:wQ^JgDu0Wk@0?Z޹s "4 eq3e;Iiآ'AewR !ݭݹ!օsmzF `atOC>lչ{+C۰Zm{Im9yѓ HO_c}uQ/lK+t|^tsjVU>Fd`@КЛb{C&^AV[}s a+C+yq[ ->tG מ%-{<\X.yώ6tCPz+ ׯQ 8p'b:9qu_Z^ΛsV+I=RB ]*qà ⦯n&k vK6ts,S^H%ٞp@PM溯ܮT?[> =x0_+υ/3t1ݏdĵRV?Xv:Zf?~?.nlf>Z )˷_`(} @P_+aIK 6|LZvz]+@:c0@0''6, t/=+bI khzo zu@ohK/DLhdUG4|,|,߲It;?&F,H6W@oje+,Xmh/-9@puq+{[€(t_b.~vsږVc+7d߮VΊX?=L_Nݮvp]\ѯPjŽtm:Br6ڰ zLJ|Lt[7ؖ.^O_G+;9ޥ[Uҵ ۊeyї ̷_T?^q ]ַz+0xZbәB5*H?"`v̊ej ޠ))0:~ի^*fQ!f5FEw& 珝 r9s͘ʄG[dV1Yno]qnX+oU۰N>K u[>(_?gU :ew9? jw?%uD 3?vE~6گcϼǙCɅ6l=~3x"eRvḟV1Z>mrOoC۲I.x%/v9e2V?p>wgbDgU߄ˤwXE5d6J~| 5i8NI6.-Lf_9p'xf*3NPMX&6wi)|H'Snunr뎏ڧŴr ĸ]ϧV:i@j KuçRKuuկƉ=&'$Wβ 22?Х'zq?ZƌbB9Ą_kB)/Qʊ"{{Ѡ﩯t9yYYqʃa7馡V/ߏֺ*tw2xV }ƶ*HH_!`i˷P #Z] w c2L٥, mWQj*T1ސ;2eRC_v?!wڷ)}ltL# O Coף7}to?] 􂩞nQV0b_v-LCBސ=ʇ[j mV|[VTR/RlKn -آZB /ibO>_1{]ߑ^rՐa~:jlR&|+,z[ߒ=O z(.|\j !d^툡D^M?oo_#̃rxY>VW8n-GjaM_Zb֔! mD&]W9}N>KF`\2uh(]$UG诰EX5q$TUVlK4{cͯ ֐ A Xf_wb؞ӷa[QPZh*Pͬh\U7ҒK+3Sq)\e3ˆVx }H%*jh|;VV]^^E =w*{a]JCbf^g5@A, RI/q!#̇}}ծ/[H,zI:s`U jn'UB/ȯ/B:d\PneIl_ts~t= :4|,*r WϰAWiuI7şІjnT@Rdc\jkKmˈ@3t1 Jah^Ո,M_CO#M7,bX!OG:GZftT^4+?۳O~K6-U@$V+Pt {^}_VҪl|><_+n뒼F@Pg6~ѽϹ #|WK2W OG܌KM^kEC{ NW긺rDžRV>k7F+Z-X]?h[ַA&:Cѵ >t[bq!@Jk \JAꇫ ֫evSp'bՐPeHvnB گb,[q J6Whb&.Z>2l_WCwЖ+zt…c'ӓr yr׋-]JmZ7v'ht:Oo؆r[=6YE}}_cSpUAn]K6ugL'iikq:|~ib9um8q%9s5\po#5{y2tNmIx5ftl!Y厶ll7qM -mê@Vl䫺FAΟfR_Ne AZ ]q'&@+ASc>g^ jOJnm7&u۪q]CCq'/SdŒί'o !$f4"/+mtj=nܺ #AIk\p:{Lx]b7@,pb+ doƠ] V !u'H6]b{9V4;}LlnK6[\ .{Ls6UL؀4U^JE)bq`f "'W?2cU\`:S)[uрr=!wgV+]n8_ծGͅQSn)$~A3?^zɲZ`*%:6SϿyƇ r7Z>&++xl͕އկLswzT؊sF·CG& "As`e~5=?o N:2}_ljźy8R `vEMp~i7) ЃP\9K]:gtw*܎Ǵ_%W!CpP#aõ6ckrاnErHNBk˵Y__&ZnTΪZMVkzѴɁAt/#B!zR_€IMhhōhJN[ZrI'\Ea$/5E%a8j J"*0SgF$gE_}--ӫX:GچN9-vj:(~-ۯ[ TUZ]ӋE- Kh5B弇Ђ,.]*?CްF?.΁ۘ|e7`*{[-$]ʊcoxVϑ>WV]vמǪ\O\ŭh4Lۿ-a !lMsol-r8yL tVsݙ?}^=bZ2a9?7aT;1MY}Gxߺ^msm蒦) ~VBw@S͡9#Ĝbf&)Ur!(̬6|M45^ܥH\Q0ofR,+>~]J'xp8H+V8\m LjV`|yP}sSUP*~0zU`T@1nFS>?UP-VE[~ SFO ė֛ fA%/'FaǰYۼ?_c1J_gfl}[ |34ה-fmش6e6\}Ӧ̪OT(RBkòO*lUh=hɊDv2g@Pt-z-Ί(צyFz%[Y¬3~,f|9?!aef3 #yT&3ڡfښ?_f5ω_Ҹ+`];S-WuCWpGUL'VGuE̷biy!y~-s~$t@cئ|27@=;| H۴įKj JRKEy~>|?OvSf{g:'+Šn+pU?߳-G;fjn1!Xql J 3~?Vc%^Bi?bc-.'F S&:Y#r♿uKRmqi._{,ȫ{'|w*}<}Vlb/W pn5!#lFط Z!b̽Bb={ qfR3>|L&􁃲0uNi? !UT\&yuGcߑ@h'@A\þ|¢WcuYlJG+nW;>HW3` _> *HXWW;P>nY{x{4~u`$liDI%ѧ`:YiTnUg11 V?\+ "Zs^g2|4x6D+ dc'PqDvcV\>~>@jcn kUI:jƄ9@õ;^>^Б?wlWt9JCԹ\ rVJ2xɯ0 ]p沊39RsrVp q0>=ׇٜ7\qQTW}ӵrO bwqOu'>W %6$a6x ;ݲ&? $CVbC/EV nЍ1B17@x' mX c\('D"A /ġ7X+ K*^흠*ʫ_8=t,=I~޶[>]m",|#B]JnGADG{鐦Vl/e:ÈVLX-˅:UؠM1FիZv'&.+J ڢvJ:vZA]0zzo 1},:$u+VI$ /wѰ.bֻ4VC ^!f׊ѥ4Lr!%9]?tGtVt L(7҄=ꆄU *j? >OT<6";OU8u_]Ch5Ц&&2nӡ:˯so JxPJ޸X,`{ p.uɾ$ հA+#2tjia=~0֫bJO3{]LؐH]" մt^?Cyri|0JCU[ϣVb t4.&?LXZXף }@t}?@46w#m:멾/t6!.~'N 3 )-?: ~`V5|%zs"VKUHً}ZT!o {ڽ2,Bt ؟>'3iEdWsaHhkГ֡-X̀t0ߧ8[9tpRb77@0;y~ "ʮ'~ ">Fk=W4^ ;%1v;f]rt r[?P2#-NOw>ÖgAB:n\_HWA0>_ 3"oqE1% wRf+s1Քvu=n3  &c[׷` IZ3"ږaHx\!uֱR KxR~,Ëk2a]+zJVƾ?o~H}J*:3 ,W&_oKZit'H ZUpT7=dӃ ȍr "'v?%ꖘ,8دq);p:œ:Nge;k7@-"2e3O'Х!&[M'k~_s/HO؛>wsp?cK6P  DJW 4?AknNOM_sVX;%ɢء i1TWĒbDwG'f yYu]bGՐn mEՔ -'/RM`f@NٕA_75O_:%M}nQs4|-yc14~=RwfoSCȽos5Fábg-!T9b-UaWžjʾoǴbXta:?iYqmwf;e^Ǭt9z_FLͅ+ZAs+@ !^Fq>">G&!%~ߜ]HEf/fiz5MU.ҸlyC[`FB-k̀ W<ȻNO;ِ-BZY!OJ'Ɯ)Jc83⛄&TO7wQJWfWQ MJ2 `QE67Ffט{Y\uYUn,?(>F֔L!JKG'7Cf5W[m (s` g=eo?=YaΏ ِ_E7~bw.dBУSQF\|gT Rs'>/PGȊW5[n„^uX"kj*\haV"{BcFX>Wm3?F6Zoig*i: hM~5_`w%C(b)7Vpn@к̯Mѡl;bCP2~u~E.{? wn_Je^$ `@0?ey}c$ XYUCN?(// "jЫ` Dt ,U>{?{ݟ[*T,!N vOGb̾g](!h]W~!BHy.=*z -XXW2x/ͻ$ ձc0h !nIq;EDA[ĝH Km$>pЭuXoG4 @zw hPq@7+!/K#mVhCHmh{K5+FʮGAhq]J)ce%kȾO 6X&YݾjѮ R 3t,,, ,::JCvK7>,5%KgD2#o}εd]8\PkwnΰQ4ĵ?z,":CHff.X>\:~R=|+V;*!tkwξtdwٸFmƃ IɋnT8BeްT/}˵dQ OAn!s1p벼idKf:ͅ{s;Y0?+)[okO+cIJ Hʵ ]%$4Ά?7}VCN5UwY>>`Va=A@ 9]%>B !Y=NpRwՐg]5=&Z-p[́gX}jRυ`@^{dFH\!K+Z !#aB|GȾ~9s5Pڝeማ,CN?9V  Xa 2o2B-/O=eHN[|ck眦 d +auW ϰ+:D'6udJH+!KFZ-!}_DJ}z[YP+VOZiho|+Qkk冷=u'#d6-5?';?.Q۰N̤UݠĮZnљ-}e\T! T@goX[yVKƶ&.w@=bK5[Ұaߠ{hnrپO(w}Gܲrǃ$XcݰsUoBSg[Vidtm_)V]4ƫcҰ :C diE1ti{a2,w  s! C:HV72t+fitӿ9k{/[<}rsY> ]W[Cď5eχhBw›o^ِ+Ǥ}`b;WD$=IOO8$'v?!.}zjMb`0>!- pةJAIj ҟGL K*BԴ>H ya"VDjA@CHV?. =FABCȔϥc'Q{ ke-r'>*6*Y.9g\ ۳GbއFKDL/g-+ӡ DSuGO 7䛡&]wZ=hPÅhE$\]iֆ'g{"+h2q;ķ0z~<ӿjVJjYqoD&s2Zה[$C+7%>hfNC[ !v,Z+KFt^ȟ>+cY۳n] WچvFTy?be: wSE HUW% ?,_q7>2s]#k~,@\ îWG- Тırسn%c.l!WȲ/kv|XN*PkՓX\XVPBa50-}!HÊ%uJsU̵ai K!$1x>{A|3/c s# ꃴbDʹ  |aB&+c~~u$dqsIT* ^ ?X%t7GDt}UZKJ(! Z~EETXMKV-WzG0یnٷ#fa`sy+g/VAIA'UYj]%Jk՝\`a6~JKKW ݍe&75s"zxa,VD P )όKNǞמ|av+4,r\Jiuk _@k6KtUYulyd/[tKB YK߲6|cCjqH=P0HeE+VFb cJ4xLJZJ-ui[9VGR_>"m7BWCKl]Y +dVƏB{JKL?']+q,3  67򕑸є[7+:t=o+j%(#:v &Z9s{^ihzɆ2wQiI~fz7u=S $&0;ձ0;#J\i$)s;Fן|޾^7vЪu#!`1n)oGB @G6#LJX˭%VecUD2 ]A+sWyc+iF$N#"yV #n\G|(dO8V $6\V7оjӭl!)ݘ|TM*a?wxb̆t^97cХҪi>d H&n/Rn *L\Q+,+`6i?,"^avɡF0l* 6_*Ilv50ԎN[69̏]-hۿsHô`%qpp&̓Ka&Szņ|d2@kA BoDqjuĄG[j~$+VԊ8>w s%Zm:{х#n*-~d,ZjxZ>RŖ=y`/λ^ چ5}m* -;mՂe~z3Gʲ|%aݷlo!$HJ3#RP^{DRc:}stp.ƈX7WīT4L/{tnA[6;tiX+ WYcjViЪ܈W[L3$'wqx}: mE$m KX;Kjyin$@5{"5ʚ>.i@n@pe@~ y+51U8'u.-<BzX|ٿЦkbUD];#TIr.4UV8]W"+{}-: D$ឧu .Lk/7#q**;{6aCުs=P1\_)G*mCN fRtNY3Z2]*糆b5$'eUvO~}hZ[5E] rQ?܃;{t3 W+2ax=F_w-g%xBUR*XݎtZyu-)Nje+7Xt+$y%ɤ^σնlj)vuKQtߓVImkxUA8K\0n^*o\yGU rf_6NjjT@ڢVdM{ @jZˁč )f?bP.weO5UE IAb+%$JrJ$p匿l [:N  us~j[p [ +.ū=' 29j;Fb[mTH*πKBAVnqb+̯qCLA$,['f֭P)q&m̭v(q-u ZV@'JAuzt٧_@o~چ^j5; K*,I`:Ѝڶ׸KUn9iŭb!|Ht>kޑ%{WóbV/96@REG~ڂK0OxV~VnT]t]i3d;t5 f#q-3F7_+yH>Zh 2%EW[i[PsLm uoPqbY*UGUDוyÉo !́!.jۯUWmS5\ɗ{?#tЫƍLJ5WK]!ca]ڧm0)GR^kO~ߵlܾVG>$leIXVʅrD#RE 9[Ys@ KjsAWTǾgU\+1#L  kLf,jlULn0H)̈שq-ۧ9ěoW< $CB-;>ߜ9Z3 : ө@3[:NpL-WeKUn>U+ V-.A VO `eՎwE5?};g.{C}M?ı*#_e9AYUz6͹H:g>_~zrwq6:q[9_M+Տ؁̯r pw+Jl PzwF2y"k6dahΣAat@+nHxV@R9}OXpTGUU [j߄f)-@P~7mr-[,_ygݕpw.(\u$ a$x[V#:2**@񣇿oƒv3UzV } :L/ۯ}VMu?#;b_ }hlDx U˾Inڲ7 #&n,o[ʒU˷ #,&SXPָ|jE:oz}oxx=SM v[1-X㇏, (젭7G]8,Է-µjn>ڴ 2*EBOgO}A.{hfl5/.Ȋmw aWԚOox +Gbp3 (^+6R,;6-۴a}$̂֬Zin*#:[2dj59 b܄04JlH֩fA R -B/?Gg?X`](Vr㤦g@U8oxxa mZמc_W_ gFb[ִWvE`\ۂZ +ájPIKO'+kZ;e?pw ubWJX2Š7zԖppW#nE-Ӱ!Wi%,82ףwhK癊=c5ZYc~ ]_*3CV0ەߍG-X-mexI=2̩ɸRnKo\[ù[7Ca-GdI ЮRU `BKnVu@ғVVfdhk VzM+I'1VȢw@QwV medbE1mXK¦=6ҸtTemm&+UYIVrx[ۃ!V,m}RV~߲БF@z dW9oMG[}ߪ!fDlZK":+"+ftX~ [oۍՙ tqB AzCk~tF,]d~I|dȀ sv\iQ{53.̮RuUCt$ov5 W|boT*Hr땯JC-x^PYXV*mG3,tM+ex{dn QmM+1Z}X~PșWӱpWIa`1?(+a] zrq^Nzb'}BiV>}?:*~m@%?]ErK!-'a U]wDWմ6oH9{UrCiK鉹aYsҾRەY-y4aB4جDŽݺ[)cn+liê&Nf"b1{qn3Tƶʴ ׃m_uxbM84'~2l={[Gݫ~Y9mvl;K!5p>A{q5zo$$_,r[ff8\rEdqh+֑wiX{w/nޱUgUټ}o Bo! Ru&}z (.h?_M&0ԬߥZ[+`ղW\6v,cٹ4'ѱ@(:C[wtZ7ԙ!$3V}BTᪧ7"C{`_k3ZvϮ %0vɉgRqR}^v,mЍ1`ad[ ;8u5)}:5~Y_>W1osNծ8O)towC%UzڡOI}-ߺI+#Rw3m\ʰrb! fD+ m5m1 mx;sయ\O„v,}zU~_.z_!~U _:H4|T]Y-0|ho9xD$=~@[6^):3G795m3 ӎLw$vmX†;rX8Ր~""[jM??ܲʉ\EA\pHYB\lND[ݲ 8g/:ĥB{̷a*}͊U TASeh5xKol.*_?/ud<*UKNYB~ye D.x-@~[\g|p=X~Gb {PKgC=MyVD~a+ZCvVگ+Wi5ߴ_&Ƞ =&ZWk )=r "ÃnḋV>?|4 | %.ǫ'zM0+{b^BCVo4p谴nw~+ubsexYW~ᐬgdt~Q / Pa2zӫ?<~/"b i4u{\J~TDnyu/[lvElrt7SqV[B2®q]'z]5zCƫk>MXLa͆ZV*#;0-+Խc*TҰ#yY{w2nmtC/0L;4V},89WU',3aZp_{xܛCc^ODDdtùn9:dx6=~gaWtc+Ƈmê@:[97RuwoXNU?}:=~Q^t#޶jWqC5.n-220ZD%X6h+^@'LP*EuSzo#?9mݔpZU'$'vӧO4(eNP'([%q7ɂqk6It^BiPPW'CA|] (B[+ p&T8[E&,>Q΄)mJXs귾'|w*} @ke5ȟwAۯ$.NWG 0]BlR^)_)%Sw?]m b{D}y.Q ]O?#d}(NQۏ7m/l\\K??KR>|XZ8-1kVݵa^{AN:t%}^эkVJ0]EFk.Q^2/Om6 [z 7|nB$m۱t |]7$*^ M1|,tkpmaO;{W}c{9nnCSiqbhyA` {ПN<|ջZX#䠴V@u,TA}hzqIv?hAmXz@o89oB@wuxaIq߯Aoh\g݃wW+/f | kB&ZJ M_*^lNh}aϾﯻn{KGO_WW}qXX֮Xo@8~U65|Fa_H&_ bo}ߵTY*+n]SlUK{G#w^\ ˔v/6~X@or>j !@NM~b zߪ1\X@O?|17s< X$L*=̰)n }b rSF9pX~ ? X@+Wvs}BKH= [[E&ukaYrxj !/GM*=ju´:/ gy6d!Z H!ZŦFzW)2Wqzj _.CԈ΃BOoCkRYvwP?%7w\W$i{;obտO@z'ɈXxV"K&n_3wx\ ȯVXCkWȆ5r@g}_>zyc[FYvw@PYE9 3S?5[o@:ƠWrnU ĔZz=%oBNg-_X&/?hMUK3].,,wQ]2`!d / JE+!8ܰC2bLt ?NP\RzH^S6?kWUxa.xY[|NjK+^0уS~DÇ;f?fIeAKjat楣'X=)y?yt\j1=n(]%d$<_ZJW/&pu:cu7hy|yמ+>lC+ J}ADP1dhߒ5qhol%@yuϿ+PsӋW1~(=3!: /MKKF*8m,L\eեKߒwbIlɗ :"%.YO~͵Jzh? yV}zU8:|&KBɟLnQi!=f?e&Z~ ٧>cp@вUʃ/B^1|0ĹDBъAnx>SB'?~Bmqe,!ձęVǑO_0nsVhO:7+TDt(>@g"j#n#3}C5a9Z8XV3z2uݖ\ޝW/Z^;s5B AKR^"sBvW_-k>Pu,^p-;za~=ZNB0 dpãiKK* ?V.?qKޭYr+T+dOCǡO*Hxw>2pϻOf !zɳr@}떯nU +p̊}."ڢזo[ttCmsɗ%SEC#yu )aWj<%z[q++ Yxo9Y?Vkxw =W܇zXZ XO[+"&-=??%zcϼVat`v;$Ug|08t>HI-V\l%d>JWt !da\>[u)q7Q`2͋:G3&|/<4rBn]]% js~A~=p^wKjWu zf8S /pY왞\mϵҕ짾 *^ +fGAO70Ot'_g{L~eҞe+1jP8NmC--5fAi{?JK{`o+WMtr>".|djN+-˖dOp]\vZ0&]ؿqSA\g>֫|xfCE0 gBKFUڒꬒ~w68s5̇Y"$DA5HQ1a+ZPWO? j }G_Z8,[??.Uę3)_ow7cy4ٳJnXx'?G;G_9PȏYp5~E4mƴ1eD6nWۧ;mX'@s{؛ ]Vw %vn0XCW z_.eSrǙ%GmߔN Ћ4pl|>Wݛ/mG/z5g>b(,́ :*g04U&_RRʞ}} AҪ? kW ;kP> c=b=;?={O +>QCO1!E7|酓>o|hXj|]8sTA"ty)wW>|5^۴R2jVn! .ʷɡq'niJcS} yoWB_][ tOBhw˥Cl^֎Ϊxk?{GPC@u$۴ZIdxJOmڶr-|z׉/ uxDr5^i-r!cbջ+ƷiݰC߹,uhB%z7}~tAUtZǩ|s hi\=@iv^ qW*M{"a_'>*w!-ߒ7y H׳0<®k=q=wvϞer9V[J?o3{'+|̣[N[FCHj PZDe·a@' Wp0ۭtXżĔ+1l>諭gڰƤ 'z=-YJG "b6ܽv_*KbdT>0?,$:&ڲ*TSmaXEW#nX÷Zza-6\>a0;Jȴ !0{6iUVnUCqq 1ٯGBhvWG>0,_kgCR.OÉB!z=ޭzT0,~߅N渄ns.Z^ }Sõdi+V$|VB~G>7"4 lc=wI;b/Lm\#:wUZ)* A ;q] 5KO\UDэ~UD:T >xbþ~(_/K9,Һ1xU:~.D xNvǕiog "t/3"ht耹zr_Wz慼j(ƨbG>XGŷge{0H܄LОΉ3zʹU=B0~a\ p8N|tiarNwՐn6;?.C73d,W;g<6t:hǰZ skw"x  NWCzewW)"L,_I˵e"TE zP7ˌ3&oĝ>xiߊ p-WU;fO5t6*Eз& KjU/z%TB]Րa~DWĭׁi,|cxFKxU=꾂o @A3n" fyp=PL뾊ٗW\'Adv{۶Eɫ׫>UEt+gUA?!< CH-. ]5++*#BJ!d*È\a,Q#+mb"`0X(D\f*\t!* SuR􈗇اÇvHΥϏn\NFDP2bAz׊']l+:BDž'dئマq ``oC+~1ḓE[Xlfq>ԒHPHBFy=݇;~-p-4`bSnSA '(bUCQ_̰eŸ〛g+_,qO0\>Tz=݆U+O..jH8#R_dRϧHyYMBFͅtx\q{y>S /6{- ZЗ/?^㼭fizJPTso->kRb\SBB-χTt4, Z]vעb8ѧJ/{%hP.bЀQe-!c6Zսʕ]ׄE3խFCh-EVBw!jWHLDom˽-\qմ\VV8GRD*\8#!%>T?VRݸ6S{Vٵ{;tȔ/ZР+7Z1iOLu=AJ2?xKߖ4[V !*eV&tUq>D*f!{MZ@u- $^ʷSbaDÊ{ .o|E[" FBP/K{N)MQkaU\ohF=TF݈̦\Dih"kjɚjڟ$fRe~́#)s53(gLK9& 9˯">N];o_S>of>fhM**+!m+X__f4O9bO"T~y̏r:jG-Tt4חobz+@*≪)R VFbd:p4ڶb%$_xd*\xRPEPf> wXٌI:mik6djrȊَEqI8 ̃+ '/B)Ր|)&nWѽ6nBS1th۟(x$ #V4{ G@fH,T\a=+%߾UT*$d5 atƠRfR$|eB_9+ާa04<)Y9|Hhc¦1<H7H"+r!>DtYa ":jCt u*gq6Zl+ n("q=Cs+)Nc|RWQBU|MPDfRQiҰ{gHLS4}\1LCeĔy(U"f "yh|q6olj#jkv_i 4.73廗JÀ)l1ܔ="1`ĥ|,İH\ŤDb\J.V zp D"ݦ|=9vDFvJ ’4"NƓ{i* ؘ)UTJ 5]YJB<ТoXq-Z|1^TN\t!>n@jmdt+vV79|ĵSm>?V4:MQeݠyh؆9x@zQsM\iߡ)?-'6s o<. GJ3!y|uX%b͋Xp?nq{ (#JB0f]tV7^6d?v҆ڰqxkW`F=k\FwȔ7COvؾZ8g @?* .5.JHO(6ԦU qD|ućRUD|^iT78u-[+lE&{6sh&ﺍQ/_8z"?"+ȗ܆:XV^T:L+5eB@#-JžB!`*gA|CP1U`zĶam Nuэk󶊢:nŽlr\>7C@O/ڊN7Kh&yv_T)ګyUC&߭V+UHRFA۔*Y\V[y1jYҸ>bR8q$k Df7,Xwp2|=y5d+-KV\>>*[[|6LJ3m6נu Iiзh:޵/Zmˤaw4.[Nӆ˭X_LݐR:!9y:05 w߹! Ց{++p>PKwUF&M$'&rR7{՝Rg/l8Q/Be"x:I{7SFƔSYIB@?(2*C–LX)>?ꮯ3y#\J/RV<6W:a#BHVBy90Kgp؂^WZ*V.@!ّH]KBɤ|<=MKJܮeJJ)֦ׄX c&[TɈDJ.*R<@$3Iik<Y(KS.7Z̲"#1脴P^HcAXyVLP?̔(]k&,+OH:B{R3ݽPg̔LyQ[NjkCh4d -eYڼK ”q#+->JIn*~@,̬_#XlYYʐ5UM6Ǯ!H d !H d !H d !H d !H d !H d !H d !H d !H d !H d !H d !H d !H d !H d !H d !H d !H d !H d !H d !H d !H d !H d !H d !H d !H d !H d !H d !H d !H d !H d !H d !H d !H d !H d !H d !H d !H d !H d !H d !H d !H d !H d !H d ~ Ů6l`# F@6l`# F@6l`# F@6l`# F@6l`# F@6l`# F@6l`# F@6l`# F@6l 95(IENDB`sidekiq-cron-sidekiq-cron-31b9d88/sidekiq-cron.gemspec000066400000000000000000000023131453463263700230050ustar00rootroot00000000000000# frozen_string_literal: true require './lib/sidekiq/cron/version' Gem::Specification.new do |s| s.name = "sidekiq-cron" s.version = Sidekiq::Cron::VERSION s.summary = "Scheduler/Cron for Sidekiq jobs" s.description = "Enables to set jobs to be run in specified time (using CRON notation or natural language)" s.homepage = "https://github.com/sidekiq-cron/sidekiq-cron" s.authors = ["Ondrej Bartas"] s.email = "ondrej@bartas.cz" s.licenses = ["MIT"] s.extra_rdoc_files = [ "LICENSE.txt", "README.md" ] s.files = Dir.glob('lib/**/*') + [ "CHANGELOG.md", "Gemfile", "LICENSE.txt", "Rakefile", "README.md", "sidekiq-cron.gemspec", ] s.required_ruby_version = ">= 2.7" s.add_dependency("fugit", "~> 1.8") s.add_dependency("sidekiq", ">= 6") s.add_dependency("globalid", ">= 1.0.1") s.add_development_dependency("minitest", "~> 5.15") s.add_development_dependency("mocha", "~> 2.1") s.add_development_dependency("rack", "~> 2.2") s.add_development_dependency("rack-test", "~> 1.1") s.add_development_dependency("rake", "~> 13.0") s.add_development_dependency("simplecov", "~> 0.21") s.add_development_dependency("simplecov-cobertura", "~> 2.1") end sidekiq-cron-sidekiq-cron-31b9d88/test/000077500000000000000000000000001453463263700200305ustar00rootroot00000000000000sidekiq-cron-sidekiq-cron-31b9d88/test/integration/000077500000000000000000000000001453463263700223535ustar00rootroot00000000000000sidekiq-cron-sidekiq-cron-31b9d88/test/integration/performance_test.rb000066400000000000000000000022311453463263700262360ustar00rootroot00000000000000require './test/test_helper' require 'benchmark' describe 'Performance Poller' do JOBS_NUMBER = 10_000 MAX_SECONDS = 60 before do # Clear all previous saved data from Redis. Sidekiq.redis do |conn| conn.flushdb end args = { queue: "default", cron: "*/2 * * * *", klass: "CronTestClass" } JOBS_NUMBER.times do |i| Sidekiq::Cron::Job.create(args.merge(name: "Test#{i}")) end @poller = Sidekiq::Cron::Poller.new(Sidekiq.const_defined?(:Config) ? Sidekiq::Config.new : {}) now = Time.now.utc + 3600 enqueued_time = Time.new(now.year, now.month, now.day, now.hour, 10, 5) Time.stubs(:now).returns(enqueued_time) end it "should enqueue #{JOBS_NUMBER} jobs in less than #{MAX_SECONDS}s" do Sidekiq.redis do |conn| assert_equal 0, conn.llen("queue:default"), 'Queue should be empty' end bench = Benchmark.measure { @poller.enqueue } Sidekiq.redis do |conn| assert_equal JOBS_NUMBER, conn.llen("queue:default"), 'Queue should be full' end puts "Performance test finished in #{bench.real}" assert_operator bench.real, :<, MAX_SECONDS end end sidekiq-cron-sidekiq-cron-31b9d88/test/models/000077500000000000000000000000001453463263700213135ustar00rootroot00000000000000sidekiq-cron-sidekiq-cron-31b9d88/test/models/person.rb000066400000000000000000000004641453463263700231520ustar00rootroot00000000000000class Person include GlobalID::Identification attr_reader :id def self.find(id) new(id) end def initialize(id) @id = id end def to_global_id(options = {}) super app: "app" end def ==(other_person) other_person.is_a?(Person) && id.to_s == other_person.id.to_s end end sidekiq-cron-sidekiq-cron-31b9d88/test/test_helper.rb000066400000000000000000000035171453463263700227010ustar00rootroot00000000000000$TESTING = true ENV['RACK_ENV'] = 'test' require 'simplecov' if ENV['CI'] require 'simplecov-cobertura' SimpleCov.formatter = SimpleCov::Formatter::CoberturaFormatter end SimpleCov.start do add_filter "test/" add_group 'Sidekiq-Cron', 'lib/' end require "minitest/autorun" require "rack/test" require 'mocha/minitest' require 'sidekiq' require 'sidekiq/web' require "sidekiq/cli" require 'sidekiq-cron' require 'sidekiq/cron/web' Sidekiq.logger.level = Logger::ERROR Sidekiq.configure_client do |config| config.redis = { url: ENV['REDIS_URL'] || 'redis://0.0.0.0:6379' } end # For testing symbolize args class Hash def symbolize_keys transform_keys { |key| key.to_sym rescue key } end end class CronTestClass include Sidekiq::Worker sidekiq_options retry: true def perform args = {} puts "super croned job #{args}" end end class CronTestClassWithQueue include Sidekiq::Worker sidekiq_options queue: :super, retry: false, backtrace: true def perform args = {} puts "super croned job #{args}" end end module ActiveJob class Base attr_accessor *%i[job_class provider_job_id queue_name arguments] def initialize yield self if block_given? self.provider_job_id ||= SecureRandom.hex(12) end def self.queue_name_prefix @queue_name_prefix end def self.queue_name_prefix=(queue_name_prefix) @queue_name_prefix = queue_name_prefix end def self.set(options) @queue = options['queue'] self end def try(method, *args, &block) send method, *args, &block if respond_to? method end def self.perform_later(*args) new do |instance| instance.job_class = self.class.name instance.queue_name = @queue instance.arguments = [*args] end end end end class ActiveJobCronTestClass < ActiveJob::Base end sidekiq-cron-sidekiq-cron-31b9d88/test/unit/000077500000000000000000000000001453463263700210075ustar00rootroot00000000000000sidekiq-cron-sidekiq-cron-31b9d88/test/unit/fixtures/000077500000000000000000000000001453463263700226605ustar00rootroot00000000000000sidekiq-cron-sidekiq-cron-31b9d88/test/unit/fixtures/schedule_array.yml000066400000000000000000000003321453463263700263730ustar00rootroot00000000000000--- - name: "my_first_job" cron: "*/5 * * * *" class: "HardWorker" queue: "hard_worker" - name: "second_job" cron: "*/30 * * * *" class: "HardWorker" queue: "hard_worker_long" args: hard: "stuff" sidekiq-cron-sidekiq-cron-31b9d88/test/unit/fixtures/schedule_erb.yml000066400000000000000000000001511453463263700260240ustar00rootroot00000000000000--- default: &default cron: <%= "every day at 5 pm" %> class: <%= "DailyJob" %> daily_job: *default sidekiq-cron-sidekiq-cron-31b9d88/test/unit/fixtures/schedule_hash.yml000066400000000000000000000003011453463263700261740ustar00rootroot00000000000000--- my_first_job: cron: "*/5 * * * *" class: "HardWorker" queue: hard_worker second_job: cron: "*/30 * * * *" class: "HardWorker" queue: hard_worker_long args: hard: "stuff" sidekiq-cron-sidekiq-cron-31b9d88/test/unit/fixtures/schedule_string.yml000066400000000000000000000000131453463263700265570ustar00rootroot00000000000000--- string sidekiq-cron-sidekiq-cron-31b9d88/test/unit/job_test.rb000066400000000000000000001312621453463263700231520ustar00rootroot00000000000000require './test/test_helper' require "./test/models/person" describe "Cron Job" do before do # Clear all previous saved data from Redis. Sidekiq.redis do |conn| conn.keys("cron_job*").each do |key| conn.del(key) end end # Clear all queues. Sidekiq::Queue.all.each do |queue| queue.clear end end it "be initialized" do job = Sidekiq::Cron::Job.new() assert_nil job.last_enqueue_time assert job.is_a?(Sidekiq::Cron::Job) end describe "class methods" do it "have create method" do assert Sidekiq::Cron::Job.respond_to?(:create) end it "have destroy method" do assert Sidekiq::Cron::Job.respond_to?(:destroy) end it "have count" do assert Sidekiq::Cron::Job.respond_to?(:count) end it "have all" do assert Sidekiq::Cron::Job.respond_to?(:all) end it "have find" do assert Sidekiq::Cron::Job.respond_to?(:find) end end describe "instance methods" do before do @job = Sidekiq::Cron::Job.new() end it "have save method" do assert @job.respond_to?(:save) end it "have valid? method" do assert @job.respond_to?("valid?".to_sym) end it "have destroy method" do assert @job.respond_to?(:destroy) end it "have enabled? method" do assert @job.respond_to?(:enabled?) end it "have disabled? method" do assert @job.respond_to?(:disabled?) end it 'have sort_name - used for sorting enabled disbaled jobs on frontend' do job = Sidekiq::Cron::Job.new(name: "TestName") assert_equal job.sort_name, "0_testname" end end describe "invalid job" do before do @job = Sidekiq::Cron::Job.new() end it "allow a class instance for the klass" do @job.klass = CronTestClass refute @job.valid? refute @job.errors.any?{|e| e.include?("klass")}, "Should not have error for klass" end it "return false on valid? and errors" do refute @job.valid? assert @job.errors.is_a?(Array) assert @job.errors.any?{|e| e.include?("name")}, "Should have error for name" assert @job.errors.any?{|e| e.include?("cron")}, "Should have error for cron" assert @job.errors.any?{|e| e.include?("klass")}, "Should have error for klass" end it "return false on valid? with invalid cron" do @job.cron = "* s *" refute @job.valid? assert @job.errors.is_a?(Array) assert @job.errors.any?{|e| e.include?("cron")}, "Should have error for cron" end it "return false on save" do refute @job.save end end describe "new" do before do @args = { name: "Test", cron: "* * * * *" } @job = Sidekiq::Cron::Job.new(@args) end it "have all setted attributes" do @args.each do |key, value| assert_equal @job.send(key), value, "New job should have #{key} with value #{value} but it has: #{@job.send(key)}" end end it "have to_hash method" do [:name,:klass,:cron,:description,:source,:args,:message,:status].each do |key| assert @job.to_hash.has_key?(key), "to_hash must have key: #{key}" end end end describe 'cron formats' do before do @args = { name: "Test", klass: "CronTestClass" } end it 'should support natural language format' do @args[:cron] = "every 3 hours" @job = Sidekiq::Cron::Job.new(@args) assert @job.valid? assert_equal Fugit::Cron.new("0 */3 * * *"), @job.send(:parsed_cron) end end describe 'parse_enqueue_time' do before do @args = { name: "Test", cron: "* * * * *" } @job = Sidekiq::Cron::Job.new(@args) end it 'should correctly parse new format' do assert_equal @job.send(:parse_enqueue_time, '2017-01-02 15:23:43 UTC'), Time.new(2017, 1, 2, 15, 23, 43, '+00:00') end it 'should correctly parse new format with different timezone' do assert_equal @job.send(:parse_enqueue_time, '2017-01-02 15:23:43 +01:00'), Time.new(2017, 1, 2, 15, 23, 43, '+01:00') end it 'should correctly parse old format' do assert_equal @job.send(:parse_enqueue_time, '2017-01-02 15:23:43'), Time.new(2017, 1, 2, 15, 23, 43, '+00:00') end end describe 'formatted time' do before do @args = { name: "Test", cron: "* * * * *" } @job = Sidekiq::Cron::Job.new(@args) @time = Time.new(2015, 1, 2, 3, 4, 5, '+01:00') end it 'returns formatted_last_time' do assert_equal '2015-01-02T02:04:00Z', @job.formatted_last_time(@time) end it 'returns formatted_enqueue_time' do assert_equal '1420164240.0', @job.formatted_enqueue_time(@time) end end describe "new with different class inputs" do it "be initialized by 'klass' and Class" do job = Sidekiq::Cron::Job.new('klass' => CronTestClass) assert_equal job.message['class'], 'CronTestClass' end it "be initialized by 'klass' and string Class" do job = Sidekiq::Cron::Job.new('klass' => 'CronTestClass') assert_equal job.message['class'], 'CronTestClass' end it "be initialized by 'class' and string Class" do job = Sidekiq::Cron::Job.new('class' => 'CronTestClass') assert_equal job.message['class'], 'CronTestClass' end it "be initialized by 'class' and Class" do job = Sidekiq::Cron::Job.new('class' => CronTestClass) assert_equal job.message['class'], 'CronTestClass' end end describe "new should find klass specific settings (queue, retry ...)" do it "nothing raise on unknown klass" do job = Sidekiq::Cron::Job.new('klass' => 'UnknownCronClass') assert_equal job.message, {"class"=>"UnknownCronClass", "args"=>[], "queue"=>"default"} end it "be initialized with default attributes" do job = Sidekiq::Cron::Job.new('klass' => 'CronTestClass') assert_equal job.message, {"retry"=>true, "queue"=>"default", "class"=>"CronTestClass", "args"=>[]} end it "be initialized with class specified attributes" do job = Sidekiq::Cron::Job.new('class' => 'CronTestClassWithQueue') assert_equal job.message, {"retry"=>false, "queue"=>:super, "backtrace"=>true, "class"=>"CronTestClassWithQueue", "args"=>[]} end it "be initialized with 'class' and overwrite queue by settings" do job = Sidekiq::Cron::Job.new('class' => CronTestClassWithQueue, queue: 'my_testing_queue') assert_equal job.message, {"retry"=>false, "queue"=>'my_testing_queue', "backtrace"=>true, "class"=>"CronTestClassWithQueue", "args"=>[]} end it "be initialized with 'class' and date_as_argument" do job = Sidekiq::Cron::Job.new('class' => 'CronTestClassWithQueue', "date_as_argument" => true) job_message = job.message job_args = job_message.delete("args") assert_equal job_message, {"retry"=>false, "queue"=>:super, "backtrace"=>true, "class"=>"CronTestClassWithQueue"} assert job_args.empty? enqueue_args = job.enqueue_args assert enqueue_args[-1].is_a?(Float) assert enqueue_args[-1].between?(Time.now.to_f - 1, Time.now.to_f) end it "be initialized with 'class', 2 arguments and date_as_argument" do job = Sidekiq::Cron::Job.new('class' => 'CronTestClassWithQueue', "date_as_argument" => true, "args"=> ["arg1", :arg2]) job_message = job.message job_args = job_message.delete("args") assert_equal job_message, {"retry"=>false, "queue"=>:super, "backtrace"=>true, "class"=>"CronTestClassWithQueue"} assert_equal job_args, ["arg1", :arg2] enqueue_args = job.enqueue_args assert_equal enqueue_args[0..-2], ["arg1", :arg2] assert enqueue_args[-1].is_a?(Float) assert enqueue_args[-1].between?(Time.now.to_f - 1, Time.now.to_f) end end describe "cron test" do before do @job = Sidekiq::Cron::Job.new() end it "return previous minute" do @job.cron = "* * * * *" time = Time.new(2018, 8, 10, 13, 24, 56).utc assert_equal @job.last_time(time).strftime("%Y-%m-%d-%H-%M-%S"), time.strftime("%Y-%m-%d-%H-%M-00") end it "return previous hour" do @job.cron = "1 * * * *" time = Time.new(2018, 8, 10, 13, 24, 56).utc assert_equal @job.last_time(time).strftime("%Y-%m-%d-%H-%M-%S"), time.strftime("%Y-%m-%d-%H-01-00") end it "return previous day" do @job.cron = "1 2 * * * Etc/GMT" time = Time.new(2018, 8, 10, 13, 24, 56).utc if time.hour >= 2 assert_equal @job.last_time(time).strftime("%Y-%m-%d-%H-%M-%S"), time.strftime("%Y-%m-%d-02-01-00") else yesterday = time - 1.day assert_equal @job.last_time(time).strftime("%Y-%m-%d-%H-%M-%S"), yesterday.strftime("%Y-%m-%d-02-01-00") end end end describe '#sidekiq_worker_message' do before do @args = { name: 'Test', cron: '* * * * *', queue: 'super_queue', klass: 'CronTestClass', args: { foo: 'bar' } } @job = Sidekiq::Cron::Job.new(@args) end it 'should return valid payload for Sidekiq::Client' do payload = { "retry" => true, "queue" => "super_queue", "class" => "CronTestClass", "args" => [{:foo=>"bar"}] } assert_equal @job.sidekiq_worker_message, payload end describe 'with date_as_argument' do before do @args.merge!(date_as_argument: true) @job = Sidekiq::Cron::Job.new(@args) end let(:args) { @job.sidekiq_worker_message['args'] } it 'should add timestamp to args' do assert_equal args[0], {foo: 'bar'} assert args[-1].is_a?(Float) assert args[-1].between?(Time.now.to_f - 1, Time.now.to_f) end end describe 'with GlobalID::Identification args' do before do @args.merge!(args: Person.new(1)) @job = Sidekiq::Cron::Job.new(@args) end let(:args) { @job.sidekiq_worker_message['args'] } it 'should add timestamp to args' do assert_equal args[0], Person.new(1) end end describe 'with GlobalID::Identification args in Array' do before do @args.merge!(args: [Person.new(1)]) @job = Sidekiq::Cron::Job.new(@args) end let(:args) { @job.sidekiq_worker_message['args'] } it 'should add timestamp to args' do assert_equal args[0], Person.new(1) end end describe 'with GlobalID::Identification args in Hash' do before do @args.merge!(args: {person: Person.new(1)}) @job = Sidekiq::Cron::Job.new(@args) end let(:args) { @job.sidekiq_worker_message['args'] } it 'should add timestamp to args' do assert_equal args[0], {person: Person.new(1)} end end end describe '#sidekiq_worker_message settings overwrite queue name' do before do @args = { name: 'Test', cron: '* * * * *', queue: 'super_queue', klass: 'CronTestClassWithQueue', args: { foo: 'bar' } } @job = Sidekiq::Cron::Job.new(@args) end it 'should return valid payload for Sidekiq::Client with overwrite queue name' do payload = { "retry" => false, "backtrace"=>true, "queue" => "super_queue", "class" => "CronTestClassWithQueue", "args" => [{:foo=>"bar"}] } assert_equal @job.sidekiq_worker_message, payload end end describe '#active_job_message' do before do SecureRandom.stubs(:uuid).returns('XYZ') ActiveJob::Base.queue_name_prefix = '' @args = { name: 'Test', cron: '* * * * *', klass: 'ActiveJobCronTestClass', queue: 'super_queue', description: nil, args: { foo: 'bar' } } @job = Sidekiq::Cron::Job.new(@args) end it 'should return valid payload for Sidekiq::Client' do payload = { 'class' => 'ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper', 'wrapped' => 'ActiveJobCronTestClass', 'queue' => 'super_queue', 'description' => nil, 'args' => [{ 'job_class' => 'ActiveJobCronTestClass', 'job_id' => 'XYZ', 'queue_name' => 'super_queue', 'arguments' => [{foo: 'bar'}] }] } assert_equal @job.active_job_message, payload end describe 'with date_as_argument' do before do @args.merge!(date_as_argument: true) @job = Sidekiq::Cron::Job.new(@args) end let(:args) { @job.active_job_message['args'][0]['arguments'] } it 'should add timestamp to args' do args = @job.active_job_message['args'][0]['arguments'] assert_equal args[0], {foo: 'bar'} assert args[-1].is_a?(Float) assert args[-1].between?(Time.now.to_f - 1, Time.now.to_f) end end end describe '#active_job_message - unknown Active Job Worker class' do before do SecureRandom.stubs(:uuid).returns('XYZ') ActiveJob::Base.queue_name_prefix = '' @args = { name: 'Test', cron: '* * * * *', klass: 'UnknownActiveJobCronTestClass', active_job: true, queue: 'super_queue', description: nil, args: { foo: 'bar' } } @job = Sidekiq::Cron::Job.new(@args) end it 'should return valid payload for Sidekiq::Client' do payload = { 'class' => 'ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper', 'wrapped' => 'UnknownActiveJobCronTestClass', 'queue' => 'super_queue', 'description' => nil, 'args' => [{ 'job_class' => 'UnknownActiveJobCronTestClass', 'job_id' => 'XYZ', 'queue_name' => 'super_queue', 'arguments' => [{foo: 'bar'}] }] } assert_equal @job.active_job_message, payload end end describe '#active_job_message with symbolize_args (hash)' do before do SecureRandom.stubs(:uuid).returns('XYZ') ActiveJob::Base.queue_name_prefix = '' @args = { name: 'Test', cron: '* * * * *', klass: 'ActiveJobCronTestClass', queue: 'super_queue', description: nil, symbolize_args: true, args: { 'foo' => 'bar' } } @job = Sidekiq::Cron::Job.new(@args) end it 'should return valid payload for Sidekiq::Client' do payload = { 'class' => 'ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper', 'wrapped' => 'ActiveJobCronTestClass', 'queue' => 'super_queue', 'description' => nil, 'args' => [{ 'job_class' => 'ActiveJobCronTestClass', 'job_id' => 'XYZ', 'queue_name' => 'super_queue', 'arguments' => [{foo: 'bar'}] }] } assert_equal @job.active_job_message, payload end end describe '#active_job_message with symbolize_args (array)' do before do SecureRandom.stubs(:uuid).returns('XYZ') ActiveJob::Base.queue_name_prefix = '' @args = { name: 'Test', cron: '* * * * *', klass: 'ActiveJobCronTestClass', queue: 'super_queue', description: nil, symbolize_args: true, args: [{ 'foo' => 'bar' }] } @job = Sidekiq::Cron::Job.new(@args) end it 'should return valid payload for Sidekiq::Client' do payload = { 'class' => 'ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper', 'wrapped' => 'ActiveJobCronTestClass', 'queue' => 'super_queue', 'description' => nil, 'args' => [{ 'job_class' => 'ActiveJobCronTestClass', 'job_id' => 'XYZ', 'queue_name' => 'super_queue', 'arguments' => [{foo: 'bar'}] }] } assert_equal @job.active_job_message, payload end end describe '#active_job_message with queue_name_prefix' do before do SecureRandom.stubs(:uuid).returns('XYZ') ActiveJob::Base.queue_name_prefix = "prefix" @args = { name: 'Test', cron: '* * * * *', klass: 'ActiveJobCronTestClass', queue: 'super_queue', queue_name_prefix: 'prefix', args: { foo: 'bar' } } @job = Sidekiq::Cron::Job.new(@args) end it 'should return valid payload for Sidekiq::Client' do payload = { 'class' => 'ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper', 'wrapped' => 'ActiveJobCronTestClass', 'queue' => 'prefix_super_queue', 'description' => nil, 'args' => [{ 'job_class' => 'ActiveJobCronTestClass', 'job_id' => 'XYZ', 'queue_name' => 'prefix_super_queue', 'arguments' => [{foo: 'bar'}] }] } assert_equal @job.active_job_message, payload end end describe '#enque!' do describe 'active job' do before do @args = { name: 'Test', cron: '* * * * *', klass: 'ActiveJobCronTestClass' } @job = Sidekiq::Cron::Job.new(@args) end it 'pushes to queue active jobs message' do @job.expects(:enqueue_active_job) .returns(ActiveJobCronTestClass.new) @job.enque! end describe 'with date_as_argument' do before do @args.merge!(date_as_argument: true) @job = Sidekiq::Cron::Job.new(@args) end it 'should add timestamp to args' do ActiveJobCronTestClass.expects(:perform_later) .returns(ActiveJobCronTestClass.new) .with { |*args| assert args[-1].is_a?(Float) assert args[-1].between?(Time.now.to_f - 1, Time.now.to_f) } @job.enque! end end describe 'with active_job == true' do before do @args.merge!(active_job: true) end describe 'with active_job job class' do before do @job = Sidekiq::Cron::Job.new(@args.merge(klass: 'ActiveJobCronTestClass')) end it 'enques via active_job interface' do @job.expects(:enqueue_active_job) .returns(ActiveJobCronTestClass.new) @job.enque! end end describe 'with non sidekiq job class' do before do @job = Sidekiq::Cron::Job.new(@args.merge(klass: 'CronTestClass')) end it 'enques via active_job interface' do @job.expects(:enqueue_active_job) .returns(ActiveJobCronTestClass.new) @job.enque! end end end end describe 'active job with queue_name_prefix' do before do @args = { name: 'Test', cron: '* * * * *', klass: 'ActiveJobCronTestClass', queue: 'cron' } @job = Sidekiq::Cron::Job.new(@args) end it 'pushes to queue active jobs message with queue_name_prefix' do @job.expects(:enqueue_active_job) .returns(ActiveJobCronTestClass.new) @job.enque! end end describe 'active job via configuration (bool: true) [unknown class]' do before do @args = { name: 'Test', cron: '* * * * *', klass: 'UnknownClass', active_job: true } @job = Sidekiq::Cron::Job.new(@args) end it 'pushes to queue active jobs message' do @job.expects(:active_job_message) .returns('class' => 'UnknownClass', 'args' => []) @job.enque! end end describe 'active job via configuration (string: true) [unknown class]' do before do @args = { name: 'Test', cron: '* * * * *', klass: 'UnknownClass', active_job: 'true' } @job = Sidekiq::Cron::Job.new(@args) end it 'pushes to queue active jobs message' do @job.expects(:active_job_message) .returns('class' => 'UnknownClass', 'args' => []) @job.enque! end end describe 'active job via configuration (string: yes) [unknown class]' do before do @args = { name: 'Test', cron: '* * * * *', klass: 'UnknownClass', active_job: 'yes' } @job = Sidekiq::Cron::Job.new(@args) end it 'pushes to queue active jobs message' do @job.expects(:active_job_message) .returns('class' => 'UnknownClass', 'args' => []) @job.enque! end end describe 'active job via configuration (number: 1) [unknown class]' do before do @args = { name: 'Test', cron: '* * * * *', klass: 'UnknownClass', active_job: 1 } @job = Sidekiq::Cron::Job.new(@args) end it 'pushes to queue active jobs message' do @job.expects(:active_job_message) .returns('class' => 'UnknownClass', 'args' => []) @job.enque! end end describe 'active job via configuration with queue_name_prefix option [unknown class]' do before do @args = { name: 'Test', cron: '* * * * *', klass: 'UnknownClass', queue: 'cron', active_job: true, queue_name_prefix: 'prefix' } @job = Sidekiq::Cron::Job.new(@args) end it 'pushes to queue active jobs message with queue_name_prefix' do @job.expects(:active_job_message) .returns('class' => 'UnknownClass', 'args' => [], 'queue' => 'prefix_cron') @job.enque! end end describe 'sidekiq worker' do before do @args = { name: 'Test', cron: '* * * * *', klass: 'CronTestClass' } @job = Sidekiq::Cron::Job.new(@args) end it 'pushes to queue active jobs message' do @job.expects(:enqueue_sidekiq_worker) .returns(true) @job.enque! end describe 'with date_as_argument' do before do @args.merge!(date_as_argument: true) @job = Sidekiq::Cron::Job.new(@args) end it 'should add timestamp to args' do CronTestClass::Setter.any_instance .expects(:perform_async) .returns(true) .with { |*args| assert args[-1].is_a?(Float) assert args[-1].between?(Time.now.to_f - 1, Time.now.to_f) } @job.enque! end end end describe 'sidekiq worker unknown class' do before do @args = { name: 'Test', cron: '* * * * *', klass: 'UnknownClass', queue: 'another' } @job = Sidekiq::Cron::Job.new(@args) end it 'pushes to queue sidekiq worker message' do @job.expects(:sidekiq_worker_message) .returns('class' => 'UnknownClass', 'args' => [], 'queue' => 'another') @job.enque! end end end # @note sidekiq-cron 1.6.0 cannot process options correctly if any date_as_argument evaluates to true. # This has been tested to resolve issues in environments where multiple sidekiq-cron versions are running when updating from 1.6.0 # See https://github.com/sidekiq-cron/sidekiq-cron/issues/350#issuecomment-1409798837 for more information. describe "compat with sidekiq cron 1.6.0" do describe "#to_hash with date_as_argument false" do before do @args = { name: "Test", cron: "* * * * *", klass: "CronTestClass", date_as_argument: false, } @job = Sidekiq::Cron::Job.new(@args) end it "should not have date_as_argument property" do assert !@job.to_hash.key?(:date_as_argument) end end describe "#to_hash with no date_as_argument option" do before do @args = { name: "Test", cron: "* * * * *", klass: "CronTestClass", } @job = Sidekiq::Cron::Job.new(@args) end it "should not have date_as_argument property" do assert !@job.to_hash.key?(:date_as_argument) end end describe "#to_hash with date_as_argument" do before do @args = { name: "Test", cron: "* * * * *", klass: "CronTestClass", date_as_argument: true, } @job = Sidekiq::Cron::Job.new(@args) end it "should have date_as_argument property with value '1'" do assert_equal @job.to_hash[:date_as_argument], '1' end end end describe "save" do before do @args = { name: "Test", cron: "* * * * *", klass: "CronTestClass" } @job = Sidekiq::Cron::Job.new(@args) end it "be saved" do assert @job.save end it "be saved and found by name" do assert @job.save, "not saved" assert Sidekiq::Cron::Job.find("Test").is_a?(Sidekiq::Cron::Job) end end describe "nonexisting job" do it "not be found" do assert Sidekiq::Cron::Job.find("nonexisting").nil?, "should return nil" end end describe "disabled/enabled" do before do @args = { name: "Test", cron: "* * * * *", klass: "CronTestClass" } end it "be created and enabled" do Sidekiq::Cron::Job.create(@args) job = Sidekiq::Cron::Job.find(@args) assert_equal job.status, "enabled" end it "be created and then enabled and disabled" do Sidekiq::Cron::Job.create(@args) job = Sidekiq::Cron::Job.find(@args) assert_equal job.status, "enabled" job.enable! assert_equal job.status, "enabled" job.disable! assert_equal job.status, "disabled" end it "be created with status disabled" do Sidekiq::Cron::Job.create(@args.merge(status: "disabled")) job = Sidekiq::Cron::Job.find(@args) assert_equal job.status, "disabled" assert_equal job.disabled?, true assert_equal job.enabled?, false end it "be created with status enabled and disable it afterwards" do Sidekiq::Cron::Job.create(@args) job = Sidekiq::Cron::Job.find(@args) assert_equal job.status, "enabled" assert_equal job.enabled?, true job.disable! assert_equal job.status, "disabled", "directly after call" assert_equal job.disabled?, true job = Sidekiq::Cron::Job.find(@args) assert_equal job.status, "disabled", "after find" end it "status shouldn't be rewritten after save without status" do Sidekiq::Cron::Job.create(@args) job = Sidekiq::Cron::Job.find(@args) assert_equal job.status, "enabled" job.disable! assert_equal job.status, "disabled", "directly after call" job = Sidekiq::Cron::Job.find(@args) assert_equal job.status, "disabled", "after find" Sidekiq::Cron::Job.create(@args) assert_equal job.status, "disabled", "after second create" job = Sidekiq::Cron::Job.find(@args) assert_equal job.status, "disabled", "after second find" end it "last_enqueue_time shouldn't be rewritten after save" do # Adding last_enqueue_time to initialize is only for testing purposes. last_enqueue_time = '2013-01-01 23:59:59 +0000' expected_enqueue_time = DateTime.parse(last_enqueue_time).to_time.utc Sidekiq::Cron::Job.create(@args.merge('last_enqueue_time' => last_enqueue_time)) job = Sidekiq::Cron::Job.find(@args) assert_equal job.last_enqueue_time, expected_enqueue_time Sidekiq::Cron::Job.create(@args) job = Sidekiq::Cron::Job.find(@args) assert_equal job.last_enqueue_time, expected_enqueue_time, "after second create should have same time" end end describe "initialize args" do it "from JSON" do args = { name: "Test", cron: "* * * * *", klass: "CronTestClass", args: JSON.dump(["123"]) } Sidekiq::Cron::Job.new(args).tap do |job| assert_equal job.args, ["123"] assert_equal job.name, "Test" end end it "from String" do args = { name: "Test", cron: "* * * * *", klass: "CronTestClass", args: "(my funny string)" } Sidekiq::Cron::Job.new(args).tap do |job| assert_equal job.args, ["(my funny string)"] assert_equal job.name, "Test" end end it "from Array" do args = { name: "Test", cron: "* * * * *", klass: "CronTestClass", args: ["This is array"] } Sidekiq::Cron::Job.new(args).tap do |job| assert_equal job.args, ["This is array"] assert_equal job.name, "Test" end end it "from GlobalID::Identification" do args = { name: "Test", cron: "* * * * *", klass: "CronTestClass", args: Person.new(1) } Sidekiq::Cron::Job.new(args).tap do |job| assert_equal job.args, [{"_sc_globalid"=>"gid://app/Person/1"}] end end it "from GlobalID::Identification in Array" do args = { name: "Test", cron: "* * * * *", klass: "CronTestClass", args: [Person.new(1)] } Sidekiq::Cron::Job.new(args).tap do |job| assert_equal job.args, [{"_sc_globalid"=>"gid://app/Person/1"}] end end it "from GlobalID::Identification in Hash" do args = { name: "Test", cron: "* * * * *", klass: "CronTestClass", args: {person: Person.new(1)} } Sidekiq::Cron::Job.new(args).tap do |job| assert_equal job.args, [{person: {"_sc_globalid"=>"gid://app/Person/1"}}] end end end describe "create & find methods" do before do @args = { name: "Test", cron: "* * * * *", klass: "CronTestClass" } end it "create first three jobs" do assert_equal Sidekiq::Cron::Job.count, 0, "Should have 0 jobs" Sidekiq::Cron::Job.create(@args) Sidekiq::Cron::Job.create(@args.merge(name: "Test2")) Sidekiq::Cron::Job.create(@args.merge(name: "Test3")) assert_equal Sidekiq::Cron::Job.count, 3, "Should have 3 jobs" end it "create first three jobs - 1 has same name" do assert_equal Sidekiq::Cron::Job.count, 0, "Should have 0 jobs" Sidekiq::Cron::Job.create(@args) Sidekiq::Cron::Job.create(@args.merge(name: "Test2")) Sidekiq::Cron::Job.create(@args.merge(cron: "1 * * * *")) assert_equal Sidekiq::Cron::Job.count, 2, "Should have 2 jobs" end it "be found by method all" do Sidekiq::Cron::Job.create(@args) Sidekiq::Cron::Job.create(@args.merge(name: "Test2")) Sidekiq::Cron::Job.create(@args.merge(name: "Test3")) assert_equal Sidekiq::Cron::Job.all.size, 3, "Should have 3 jobs" assert Sidekiq::Cron::Job.all.all?{|j| j.is_a?(Sidekiq::Cron::Job)}, "All returned jobs should be Job class" end it "be found by method all - defect in set" do Sidekiq::Cron::Job.create(@args) Sidekiq::Cron::Job.create(@args.merge(name: "Test2")) Sidekiq::Cron::Job.create(@args.merge(name: "Test3")) Sidekiq.redis do |conn| conn.sadd Sidekiq::Cron::Job.jobs_key, ["some_other_key"] end assert_equal Sidekiq::Cron::Job.all.size, 3, "All have to return only valid 3 jobs" end it "be found by string name" do Sidekiq::Cron::Job.create(@args) assert Sidekiq::Cron::Job.find("Test") end it "be found by hash with key name" do Sidekiq::Cron::Job.create(@args) assert Sidekiq::Cron::Job.find(name: "Test"), "symbol keys keys" Sidekiq::Cron::Job.create(@args) assert Sidekiq::Cron::Job.find('name' => "Test"), "String keys" end end describe "destroy" do before do @args = { name: "Test", cron: "* * * * *", klass: "CronTestClass" } end it "create and then destroy by hash" do Sidekiq::Cron::Job.create(@args) assert_equal Sidekiq::Cron::Job.all.size, 1, "Should have 1 job" assert Sidekiq::Cron::Job.destroy(@args) assert_equal Sidekiq::Cron::Job.all.size, 0, "Should have 0 job after destroy" end it "return false on destroying nonexisting" do assert_equal Sidekiq::Cron::Job.all.size, 0, "Should have 0 jobs" refute Sidekiq::Cron::Job.destroy("nonexisting") end it "return destroy by string name" do Sidekiq::Cron::Job.create(@args) assert Sidekiq::Cron::Job.destroy("Test") end it "return destroy by hash with key name" do Sidekiq::Cron::Job.create(@args) assert Sidekiq::Cron::Job.destroy(name: "Test"), "symbol keys keys" Sidekiq::Cron::Job.create(@args) assert Sidekiq::Cron::Job.destroy('name' => "Test"), "String keys" end end describe "destroy_removed_jobs only destroys non dynamic jobs" do before do args1 = { name: "WillBeErasedJob", cron: "* * * * *", klass: "CronTestClass", source: "schedule" } Sidekiq::Cron::Job.create(args1) args2 = { name: "ContinueRemainingScheduleJob", cron: "* * * * *", klass: "CronTestClass", source: "schedule" } Sidekiq::Cron::Job.create(args2) args2 = { name: "ContinueRemainingDynamicJob", cron: "* * * * *", klass: "CronTestClass" } Sidekiq::Cron::Job.create(args2) end it "be destroyed removed job that not exists in args" do assert_equal Sidekiq::Cron::Job.destroy_removed_jobs(["ContinueRemainingScheduleJob"]), ["WillBeErasedJob"], "Should be destroyed WillBeErasedJob" end end describe "test of enque" do before do @args = { name: "Test", cron: "* * * * *", klass: "CronTestClass" } # First time is always after next cron time! @time = Time.now.utc + 120 end it "be always false when status is disabled" do refute Sidekiq::Cron::Job.new(@args.merge(status: 'disabled')).should_enque? @time refute Sidekiq::Cron::Job.new(@args.merge(status: 'disabled')).should_enque? @time - 60 refute Sidekiq::Cron::Job.new(@args.merge(status: 'disabled')).should_enque? @time - 120 assert_equal Sidekiq::Queue.all.size, 0, "Sidekiq 0 queues" end it "be false for same times" do assert Sidekiq::Cron::Job.new(@args).should_enque?(@time), "First time - true" refute Sidekiq::Cron::Job.new(@args).should_enque? @time refute Sidekiq::Cron::Job.new(@args).should_enque? @time end it "be false for same times but true for next time" do assert Sidekiq::Cron::Job.new(@args).should_enque?(@time), "First time - true" refute Sidekiq::Cron::Job.new(@args).should_enque? @time assert Sidekiq::Cron::Job.new(@args).should_enque? @time + 135 refute Sidekiq::Cron::Job.new(@args).should_enque? @time + 135 assert Sidekiq::Cron::Job.new(@args).should_enque? @time + 235 refute Sidekiq::Cron::Job.new(@args).should_enque? @time + 235 refute Sidekiq::Cron::Job.new(@args).should_enque? @time refute Sidekiq::Cron::Job.new(@args).should_enque? @time + 135 refute Sidekiq::Cron::Job.new(@args).should_enque? @time + 235 end it "should not enqueue jobs that are past" do assert Sidekiq::Cron::Job.new(@args.merge(cron: "*/1 * * * *")).should_enque? @time refute Sidekiq::Cron::Job.new(@args.merge(cron: "0 1,13 * * *")).should_enque? @time end it 'doesnt skip enqueuing if job is resaved near next enqueue time' do job = Sidekiq::Cron::Job.new(@args) assert job.test_and_enque_for_time!(@time), "should enqueue" future_now = @time + 1 * 60 * 60 Time.stubs(:now).returns(future_now) # Save uses Time.now.utc job.save assert Sidekiq::Cron::Job.new(@args).test_and_enque_for_time!(future_now + 30), "should enqueue" end it "remove old enque times + should be enqeued" do job = Sidekiq::Cron::Job.new(@args) assert_nil job.last_enqueue_time assert job.test_and_enque_for_time!(@time), "should enqueue" assert job.last_enqueue_time refute Sidekiq::Cron::Job.new(@args).test_and_enque_for_time!(@time), "should not enqueue" Sidekiq.redis do |conn| assert_equal conn.zcard(Sidekiq::Cron::Job.new(@args).send(:job_enqueued_key)), 1, "Should have one enqueued job" end assert_equal Sidekiq::Queue.all.first.size, 1, "Sidekiq queue 1 job in queue" # 20 hours after. assert Sidekiq::Cron::Job.new(@args).test_and_enque_for_time! @time + 1 * 60 * 60 refute Sidekiq::Cron::Job.new(@args).test_and_enque_for_time! @time + 1 * 60 * 60 Sidekiq.redis do |conn| assert_equal conn.zcard(Sidekiq::Cron::Job.new(@args).send(:job_enqueued_key)), 2, "Should have two enqueued job" end assert_equal Sidekiq::Queue.all.first.size, 2, "Sidekiq queue 2 jobs in queue" # 26 hour after. assert Sidekiq::Cron::Job.new(@args).test_and_enque_for_time! @time + 26 * 60 * 60 refute Sidekiq::Cron::Job.new(@args).test_and_enque_for_time! @time + 26 * 60 * 60 Sidekiq.redis do |conn| assert_equal conn.zcard(Sidekiq::Cron::Job.new(@args).send(:job_enqueued_key)), 1, "Should have one enqueued job - old jobs should be deleted" end assert_equal Sidekiq::Queue.all.first.size, 3, "Sidekiq queue 3 jobs in queue" end end describe "load" do describe "from hash" do before do @jobs_hash = { 'name_of_job' => { 'class' => 'MyClass', 'cron' => '1 * * * *', 'args' => '(OPTIONAL) [Array or Hash]' }, 'My super iber cool job' => { 'class' => 'SecondClass', 'cron' => '*/5 * * * *' } } end it "create new jobs and update old one with same settings" do assert_equal Sidekiq::Cron::Job.all.size, 0, "Should have 0 jobs before load" out = Sidekiq::Cron::Job.load_from_hash @jobs_hash assert_equal out.size, 0, "should have no errors" assert_equal Sidekiq::Cron::Job.all.size, 2, "Should have 2 jobs after load" end it "duplicate jobs are not loaded" do out = Sidekiq::Cron::Job.load_from_hash! @jobs_hash assert_equal out.size, 0, "should have no errors" assert_equal Sidekiq::Cron::Job.all.size, 2, "Should have 2 jobs after load" out_2 = Sidekiq::Cron::Job.load_from_hash! @jobs_hash assert_equal out_2.size, 0, "should have no errors" assert_equal Sidekiq::Cron::Job.all.size, 2, "Should have 2 jobs after loading again" end it "dynamic jobs are not cleared" do args = { name: "DynamicJob", cron: "* * * * *", klass: "CronTestClass", source: "dynamic" } Sidekiq::Cron::Job.create(args) out = Sidekiq::Cron::Job.load_from_hash! @jobs_hash assert_equal out.size, 0, "should have no errors" assert_equal Sidekiq::Cron::Job.all.size, 3, "Should have 3 jobs after load" out_2 = Sidekiq::Cron::Job.load_from_hash! @jobs_hash assert_equal out_2.size, 0, "should have no errors" assert_equal Sidekiq::Cron::Job.all.size, 3, "Should have 3 jobs after loading again" end it "return errors on loaded jobs" do assert_equal Sidekiq::Cron::Job.all.size, 0, "Should have 0 jobs before load" # Set something bad to hash. @jobs_hash['name_of_job']['cron'] = "bad cron" out = Sidekiq::Cron::Job.load_from_hash @jobs_hash assert_equal 1, out.size, "should have 1 error" assert_includes out['name_of_job'].first, "bad cron" assert_includes out['name_of_job'].first, "ArgumentError:" assert_equal 1, Sidekiq::Cron::Job.all.size, "Should have only 1 job after load" end it "create new jobs and then destroy them all" do assert_equal Sidekiq::Cron::Job.all.size, 0, "Should have 0 jobs before load" out = Sidekiq::Cron::Job.load_from_hash @jobs_hash assert_equal out.size, 0, "should have no errors" assert_equal Sidekiq::Cron::Job.all.size, 2, "Should have 2 jobs after load" Sidekiq::Cron::Job.destroy_all! assert_equal Sidekiq::Cron::Job.all.size, 0, "Should have 0 jobs after destroy all" end it "create new jobs and update old one with same settings with load_from_hash!" do assert_equal Sidekiq::Cron::Job.all.size, 0, "Should have 0 jobs before load" out = Sidekiq::Cron::Job.load_from_hash! @jobs_hash assert_equal out.size, 0, "should have no errors" assert_equal Sidekiq::Cron::Job.all.size, 2, "Should have 2 jobs after load" end end describe "from array" do before do @jobs_array = [ { 'name' => 'name_of_job', 'class' => 'MyClass', 'cron' => '1 * * * *', 'args' => '(OPTIONAL) [Array or Hash]' }, { 'name' => 'Cool Job for Second Class', 'class' => 'SecondClass', 'cron' => '*/5 * * * *' } ] end it "create new jobs and update old one with same settings" do assert_equal Sidekiq::Cron::Job.all.size, 0, "Should have 0 jobs before load" out = Sidekiq::Cron::Job.load_from_array @jobs_array assert_equal out.size, 0, "should have 0 error" assert_equal Sidekiq::Cron::Job.all.size, 2, "Should have 2 jobs after load" end it "duplicate jobs are not loaded" do out = Sidekiq::Cron::Job.load_from_array @jobs_array assert_equal out.size, 0, "should have no errors" assert_equal Sidekiq::Cron::Job.all.size, 2, "Should have 2 jobs after load" out_2 = Sidekiq::Cron::Job.load_from_array @jobs_array assert_equal out_2.size, 0, "should have no errors" assert_equal Sidekiq::Cron::Job.all.size, 2, "Should have 2 jobs after loading again" end it "create new jobs and update old one with same settings with load_from_array" do assert_equal Sidekiq::Cron::Job.all.size, 0, "Should have 0 jobs before load" out = Sidekiq::Cron::Job.load_from_array! @jobs_array assert_equal out.size, 0, "should have 0 error" assert_equal Sidekiq::Cron::Job.all.size, 2, "Should have 2 jobs after load" end end describe "from array with queue_name" do before do @jobs_array = [ { 'name' => 'name_of_job', 'class' => 'CronTestClassWithQueue', 'cron' => '1 * * * *', 'args' => '(OPTIONAL) [Array or Hash]', 'queue' => 'from_array' } ] end it "create new jobs and update old one with same settings" do assert_equal Sidekiq::Cron::Job.all.size, 0, "Should have 0 jobs before load" out = Sidekiq::Cron::Job.load_from_array @jobs_array assert_equal out.size, 0, "should have 0 error" assert_equal Sidekiq::Cron::Job.all.size, 1, "Should have 2 jobs after load" payload = { "retry" => false, "backtrace"=>true, "queue" => "from_array", "class" => "CronTestClassWithQueue", "args" => ['(OPTIONAL) [Array or Hash]'] } assert_equal Sidekiq::Cron::Job.all.first.sidekiq_worker_message, payload end end end describe "args=" do before do @job = Sidekiq::Cron::Job.new(name: "test") end it "should set args" do @job.args = [1, 2, 3] assert_equal @job.args, [1, 2, 3] end it "should set args from string" do @job.args = "(1, 2, 3)" assert_equal @job.args, ["(1, 2, 3)"] end it "should set args from hash" do @job.args = {a: 1, b: 2} assert_equal @job.args, [{a: 1, b: 2}] end it "should set args from array" do @job.args = [{a: 1, b: 2}] assert_equal @job.args, [{a: 1, b: 2}] end it "should set args from GlobalID::Identification" do @job.args = Person.new(1) assert_equal @job.args, [{"_sc_globalid"=>"gid://app/Person/1"}] end it "should set args from GlobalID::Identification in Array" do @job.args = [Person.new(1)] assert_equal @job.args, [{"_sc_globalid"=>"gid://app/Person/1"}] end it "should set args from GlobalID::Identification in Hash" do @job.args = {person: Person.new(1)} assert_equal @job.args, [{person: {"_sc_globalid"=>"gid://app/Person/1"}}] end end end sidekiq-cron-sidekiq-cron-31b9d88/test/unit/launcher_test.rb000066400000000000000000000017521453463263700242010ustar00rootroot00000000000000require './test/test_helper' describe 'Cron launcher' do describe 'initialization' do before do Sidekiq::Options[:cron_poll_interval] = nil end it 'initializes poller with default poll interval when not configured' do Sidekiq::Cron::Poller.expects(:new).with do |options| assert_equal Sidekiq::Cron::Launcher::DEFAULT_POLL_INTERVAL, options[:cron_poll_interval] end Sidekiq::Launcher.new(Sidekiq::Options.config) end it 'initializes poller with the configured poll interval' do Sidekiq::Cron::Poller.expects(:new).with do |options| assert_equal 99, options[:cron_poll_interval] end Sidekiq::Options[:cron_poll_interval] = 99 Sidekiq::Launcher.new(Sidekiq::Options.config) end it 'does not initialize the poller when interval is 0' do Sidekiq::Cron::Poller.expects(:new).never Sidekiq::Options[:cron_poll_interval] = 0 Sidekiq::Launcher.new(Sidekiq::Options.config) end end end sidekiq-cron-sidekiq-cron-31b9d88/test/unit/poller_test.rb000066400000000000000000000100611453463263700236660ustar00rootroot00000000000000require './test/test_helper' describe 'Cron Poller' do before do # Clear all previous saved data from Redis. Sidekiq.redis do |conn| conn.flushdb end @args = { name: "Test", cron: "*/2 * * * *", klass: "CronTestClass" } @args2 = @args.merge(name: 'with_queue', klass: 'CronTestClassWithQueue', cron: "*/10 * * * *") @poller = Sidekiq::Cron::Poller.new(Sidekiq.const_defined?(:Config) ? Sidekiq::Config.new : {}) end it 'not enqueue any job - new jobs' do now = Time.now.utc + 3600 enqueued_time = Time.new(now.year, now.month, now.day, now.hour, 5, 1) Time.stubs(:now).returns(enqueued_time) Sidekiq::Cron::Job.create(@args) Sidekiq::Cron::Job.create(@args2) @poller.enqueue Sidekiq.redis do |conn| assert_equal 0, conn.llen("queue:default") assert_equal 0, conn.llen("queue:super") end # 30 seconds after! enqueued_time = Time.new(now.year, now.month, now.day, now.hour, 5, 30) Time.stubs(:now).returns(enqueued_time) @poller.enqueue Sidekiq.redis do |conn| assert_equal 0, conn.llen("queue:default") assert_equal 0, conn.llen("queue:super") end end it 'should enqueue only job with cron */2' do now = Time.now.utc + 3600 enqueued_time = Time.new(now.year, now.month, now.day, now.hour, 5, 1) Time.stubs(:now).returns(enqueued_time) Sidekiq::Cron::Job.create(@args) Sidekiq::Cron::Job.create(@args2) @poller.enqueue Sidekiq.redis do |conn| assert_equal 0, conn.llen("queue:default") assert_equal 0, conn.llen("queue:super") end enqueued_time = Time.new(now.year, now.month, now.day, now.hour, 6, 1) Time.stubs(:now).returns(enqueued_time) @poller.enqueue Sidekiq.redis do |conn| assert_equal 1, conn.llen("queue:default") assert_equal 0, conn.llen("queue:super") end end it 'should enqueue both jobs' do now = Time.now.utc + 3600 enqueued_time = Time.new(now.year, now.month, now.day, now.hour, 8, 1) Time.stubs(:now).returns(enqueued_time) Sidekiq::Cron::Job.create(@args) Sidekiq::Cron::Job.create(@args2) @poller.enqueue Sidekiq.redis do |conn| assert_equal 0, conn.llen("queue:default") assert_equal 0, conn.llen("queue:super") end enqueued_time = Time.new(now.year, now.month, now.day, now.hour, 10, 5) Time.stubs(:now).returns(enqueued_time) @poller.enqueue Sidekiq.redis do |conn| assert_equal 1, conn.llen("queue:default") assert_equal 1, conn.llen("queue:super") end end it 'should enqueue both jobs but only one time each' do now = Time.now.utc + 3600 enqueued_time = Time.new(now.year, now.month, now.day, now.hour, 8, 1) Time.stubs(:now).returns(enqueued_time) Sidekiq::Cron::Job.create(@args) Sidekiq::Cron::Job.create(@args2) @poller.enqueue Sidekiq.redis do |conn| assert_equal 0, conn.llen("queue:default") assert_equal 0, conn.llen("queue:super") end enqueued_time = Time.new(now.year, now.month, now.day, now.hour, 20, 1) Time.stubs(:now).returns(enqueued_time) @poller.enqueue Sidekiq.redis do |conn| assert_equal 1, conn.llen("queue:default") assert_equal 1, conn.llen("queue:super") end enqueued_time = Time.new(now.year, now.month, now.day, now.hour, 20, 2) Time.stubs(:now).returns(enqueued_time) @poller.enqueue Sidekiq.redis do |conn| assert_equal 1, conn.llen("queue:default") assert_equal 1, conn.llen("queue:super") end enqueued_time = Time.new(now.year, now.month, now.day, now.hour, 20, 20) Time.stubs(:now).returns(enqueued_time) @poller.enqueue Sidekiq.redis do |conn| assert_equal 1, conn.llen("queue:default") assert_equal 1, conn.llen("queue:super") end enqueued_time = Time.new(now.year, now.month, now.day, now.hour, 20, 50) Time.stubs(:now).returns(enqueued_time) @poller.enqueue Sidekiq.redis do |conn| assert_equal 1, conn.llen("queue:default") assert_equal 1, conn.llen("queue:super") end end end sidekiq-cron-sidekiq-cron-31b9d88/test/unit/schedule_loader_test.rb000066400000000000000000000035141453463263700255200ustar00rootroot00000000000000require './test/test_helper' describe 'ScheduleLoader' do before do Sidekiq::Options[:lifecycle_events][:startup].clear end describe 'Schedule is defined in hash' do before do Sidekiq::Options[:cron_schedule_file] = 'test/unit/fixtures/schedule_hash.yml' load 'sidekiq/cron/schedule_loader.rb' end it 'calls Sidekiq::Cron::Job.load_from_hash!' do Sidekiq::Cron::Job.expects(:load_from_hash!) Sidekiq::Options[:lifecycle_events][:startup].first.call end end describe 'Schedule is defined in array' do before do Sidekiq::Options[:cron_schedule_file] = 'test/unit/fixtures/schedule_array.yml' load 'sidekiq/cron/schedule_loader.rb' end it 'calls Sidekiq::Cron::Job.load_from_array!' do Sidekiq::Cron::Job.expects(:load_from_array!) Sidekiq::Options[:lifecycle_events][:startup].first.call end end describe 'Schedule is not defined in hash nor array' do before do Sidekiq::Options[:cron_schedule_file] = 'test/unit/fixtures/schedule_string.yml' load 'sidekiq/cron/schedule_loader.rb' end it 'raises an error' do e = assert_raises StandardError do Sidekiq::Options[:lifecycle_events][:startup].first.call end assert_equal 'Not supported schedule format. Confirm your test/unit/fixtures/schedule_string.yml', e.message end end describe 'Schedule is defined using ERB' do it 'properly parses the schedule file' do Sidekiq::Options[:cron_schedule_file] = 'test/unit/fixtures/schedule_erb.yml' load 'sidekiq/cron/schedule_loader.rb' Sidekiq::Options[:lifecycle_events][:startup].first.call job = Sidekiq::Cron::Job.find("daily_job") assert_equal job.klass, "DailyJob" assert_equal job.cron, "every day at 5 pm" assert_equal job.source, "schedule" end end end sidekiq-cron-sidekiq-cron-31b9d88/test/unit/web_extension_test.rb000066400000000000000000000100111453463263700252350ustar00rootroot00000000000000require './test/test_helper' describe 'Cron web' do include Rack::Test::Methods TOKEN = SecureRandom.base64(32).freeze def app Sidekiq::Web end before do env 'rack.session', { csrf: TOKEN } env 'HTTP_X_CSRF_TOKEN', TOKEN Sidekiq.redis { |c| c.flushdb } end let(:job_name) { "TestNameOfCronJob" } let(:cron_job_name) { "TesQueueNameOfCronJob" } let(:args) do { name: job_name, cron: "*/2 * * * *", klass: "CronTestClass" } end let(:cron_args) do { name: cron_job_name, cron: "*/2 * * * *", klass: "CronQueueTestClass", queue: "cron" } end it 'display cron web' do get '/cron' assert_equal 200, last_response.status end it 'display cron web with message - no cron jobs' do get '/cron' assert last_response.body.include?('No cron jobs were found') end it 'display cron web with cron jobs table' do Sidekiq::Cron::Job.create(args) get '/cron' assert_equal 200, last_response.status refute last_response.body.include?('No cron jobs were found') assert last_response.body.include?('table') assert last_response.body.include?("TestNameOfCronJob") end describe "work with cron job" do before do @job = Sidekiq::Cron::Job.new(args.merge(status: "enabled")) assert @job.save @cron_job = Sidekiq::Cron::Job.new(cron_args.merge(status: "enabled")) assert @cron_job.save end it 'shows history of a cron job' do @job.enque! get "/cron/#{job_name}" jid = Sidekiq.redis do |conn| history = conn.lrange Sidekiq::Cron::Job.jid_history_key(job_name), 0, -1 Sidekiq.load_json(history.last)['jid'] end assert jid assert last_response.body.include?(jid) end it 'redirects to cron path when name not found' do get '/cron/some-fake-name' assert_match %r{\/cron\z}, last_response['Location'] end it "disable and enable all cron jobs" do post "/cron/__all__/disable" assert_equal Sidekiq::Cron::Job.find(job_name).status, "disabled" post "/cron/__all__/enable" assert_equal Sidekiq::Cron::Job.find(job_name).status, "enabled" end it "disable and enable cron job" do post "/cron/#{job_name}/disable" assert_equal Sidekiq::Cron::Job.find(job_name).status, "disabled" post "/cron/#{job_name}/enable" assert_equal Sidekiq::Cron::Job.find(job_name).status, "enabled" end it "enqueue all jobs" do Sidekiq.redis do |conn| assert_equal 0, conn.llen("queue:default"), "Queue should have no jobs" end post "/cron/__all__/enque" Sidekiq.redis do |conn| assert_equal 1, conn.llen("queue:default"), "Queue should have 1 job in default" assert_equal 1, conn.llen("queue:cron"), "Queue should have 1 job in cron" end end it "enqueue job" do Sidekiq.redis do |conn| assert_equal 0, conn.llen("queue:default"), "Queue should have no jobs" end post "/cron/#{job_name}/enque" Sidekiq.redis do |conn| assert_equal 1, conn.llen("queue:default"), "Queue should have 1 job" end # Should enqueue more times. post "/cron/#{job_name}/enque" Sidekiq.redis do |conn| assert_equal 2, conn.llen("queue:default"), "Queue should have 2 job" end # Should enqueue to cron job queue. post "/cron/#{cron_job_name}/enque" Sidekiq.redis do |conn| assert_equal 1, conn.llen("queue:cron"), "Queue should have 1 cron job" end end it "destroy job" do assert_equal Sidekiq::Cron::Job.all.size, 2, "Should have 2 job" post "/cron/#{job_name}/delete" post "/cron/#{cron_job_name}/delete" assert_equal Sidekiq::Cron::Job.all.size, 0, "Should have zero jobs" end it "destroy all jobs" do assert_equal Sidekiq::Cron::Job.all.size, 2, "Should have 2 job" post "/cron/__all__/delete" assert_equal Sidekiq::Cron::Job.all.size, 0, "Should have zero jobs" end end end