unity-scope-click-0.1+14.04.20140410.1/ 0000755 0000153 0177776 00000000000 12321560627 017450 5 ustar pbuser nogroup 0000000 0000000 unity-scope-click-0.1+14.04.20140410.1/autopilot/ 0000755 0000153 0177776 00000000000 12321560627 021470 5 ustar pbuser nogroup 0000000 0000000 unity-scope-click-0.1+14.04.20140410.1/autopilot/unityclickscope/ 0000755 0000153 0177776 00000000000 12321560627 024700 5 ustar pbuser nogroup 0000000 0000000 unity-scope-click-0.1+14.04.20140410.1/autopilot/unityclickscope/fake_services.py 0000644 0000153 0177776 00000004310 12321560256 030057 0 ustar pbuser nogroup 0000000 0000000 # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Copyright (C) 2014 Canonical Ltd.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3, as published
# by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
import dbus
import dbusmock
class FakeDownloadService(object):
"""Fake Download Service using a dbusmock interface."""
def __init__(self, dbus_connection):
super(FakeDownloadService, self).__init__()
self.dbus_connection = dbus_connection
self.mock = self._get_mock_interface()
def _get_mock_interface(self):
return dbus.Interface(
self.dbus_connection.get_object(
'com.canonical.applications.Downloader', '/'),
dbusmock.MOCK_IFACE)
def add_method(self, method, return_value):
if method == 'getAllDownloadsWithMetadata':
self._add_getAllDownloadsWithMetadata(return_value)
elif method == 'createDownload':
self._add_createDownload(return_value)
else:
raise NotImplementedError('Unknown method: {}'.format(method))
def _add_getAllDownloadsWithMetadata(self, return_value):
self.mock.AddMethod(
'com.canonical.applications.DownloadManager',
'getAllDownloadsWithMetadata',
'ss', 'ao', 'ret = {}'.format(return_value))
def _add_createDownload(self, return_value):
self.mock.AddMethod(
'com.canonical.applications.DownloadManager',
'createDownload', '(sssa{sv}a{ss})', 'o',
'ret = "{}"'.format(return_value))
def add_download_object(self, object_path):
self.mock.AddObject(
object_path,
'com.canonical.applications.Download',
{},
[('start', '', '', '')])
unity-scope-click-0.1+14.04.20140410.1/autopilot/unityclickscope/fixture_setup.py 0000644 0000153 0177776 00000004276 12321560256 030167 0 ustar pbuser nogroup 0000000 0000000 # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Copyright (C) 2013, 2014 Canonical Ltd.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3, as published
# by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
"""Set up and clean up fixtures for the Unity Click Scope acceptance tests."""
import logging
import threading
import fixtures
from unityclickscope import fake_servers
logger = logging.getLogger(__name__)
class FakeServerRunning(fixtures.Fixture):
def __init__(self, server_class):
super(FakeServerRunning, self).__init__()
self.server_class = server_class
def setUp(self):
super(FakeServerRunning, self).setUp()
self._start_fake_server()
def _start_fake_server(self):
logger.info('Starting fake server: {}.'.format(self.server_class))
server_address = ('', 0)
fake_server = self.server_class(server_address)
server_thread = threading.Thread(target=fake_server.serve_forever)
server_thread.start()
logger.info('Serving at port {}.'.format(fake_server.server_port))
self.addCleanup(self._stop_fake_server, server_thread, fake_server)
self.url = 'http://localhost:{}/'.format(fake_server.server_port)
def _stop_fake_server(self, thread, server):
logger.info('Stopping fake server: {}.'.format(self.server_class))
server.shutdown()
thread.join()
class FakeSearchServerRunning(FakeServerRunning):
def __init__(self):
super(FakeSearchServerRunning, self).__init__(
fake_servers.FakeSearchServer)
class FakeDownloadServerRunning(FakeServerRunning):
def __init__(self):
super(FakeDownloadServerRunning, self).__init__(
fake_servers.FakeDownloadServer)
unity-scope-click-0.1+14.04.20140410.1/autopilot/unityclickscope/credentials.py 0000644 0000153 0177776 00000007107 12321560256 027552 0 ustar pbuser nogroup 0000000 0000000 # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Copyright (C) 2014 Canonical Ltd.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3, as published
# by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
import threading
from gi.repository import Accounts, GLib, Signon
class CredentialsException(Exception):
"""Exception for credentials problems."""
class AccountManager(object):
def __init__(self):
self._manager = Accounts.Manager()
def _start_main_loop(self):
self.error = None
self._main_loop = GLib.MainLoop()
self._main_loop_thread = threading.Thread(
target=self._main_loop.run)
self._main_loop_thread.start()
def _join_main_loop(self):
self._main_loop_thread.join()
if self.error is not None:
raise CredentialsException(self.error.message)
def add_u1_credentials(self, user_name, password):
self._start_main_loop()
account = self._create_account()
info = self._get_identity_info(user_name, password)
identity = Signon.Identity.new()
identity.store_credentials_with_info(
info, self._set_credentials_id_to_account, account)
self._join_main_loop()
return account
def _create_account(self):
account = self._manager.create_account('ubuntuone')
account.set_enabled(True)
account.store(self._on_account_created, None)
return account
def _on_account_created(self, account, error, _):
if error:
self.error = error
self._main_loop.quit()
def _get_identity_info(self, user_name, password):
info = Signon.IdentityInfo.new()
info.set_username(user_name)
info.set_caption(user_name)
info.set_secret(password, True)
return info
def _set_credentials_id_to_account(self, identity, id, error, account):
if error:
self.error = error
self._main_loop.quit()
account.set_variant('CredentialsId', GLib.Variant('u', id))
account.store(self._process_session, None)
def _process_session(self, account, error, _):
if error:
self.error = error
self._main_loop.quit()
account_service = Accounts.AccountService.new(account, None)
auth_data = account_service.get_auth_data()
identity = auth_data.get_credentials_id()
method = auth_data.get_method()
mechanism = auth_data.get_method()
session_data = auth_data.get_parameters()
session = Signon.AuthSession.new(identity, method)
session.process(
session_data, mechanism, self._on_login_processed, None)
def _on_login_processed(self, session, reply, error, userdata):
if error:
self.error = error
self._main_loop.quit()
def delete_account(self, account):
self._start_main_loop()
account.delete()
account.store(self._on_account_deleted, None)
self._join_main_loop()
def _on_account_deleted(self, account, error, userdata):
if error:
self.error = error
self._main_loop.quit()
unity-scope-click-0.1+14.04.20140410.1/autopilot/unityclickscope/__init__.py 0000644 0000153 0177776 00000000000 12321560256 026775 0 ustar pbuser nogroup 0000000 0000000 unity-scope-click-0.1+14.04.20140410.1/autopilot/unityclickscope/test_fixture_setup.py 0000644 0000153 0177776 00000003667 12321560256 031231 0 ustar pbuser nogroup 0000000 0000000 # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Copyright (C) 2014 Canonical Ltd.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3, as published
# by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
import httplib
import urlparse
import testscenarios
import testtools
from unityclickscope import fixture_setup
class FakeServerRunningTestCase(
testscenarios.TestWithScenarios, testtools.TestCase):
scenarios = [
('fake search server', dict(
fixture=fixture_setup.FakeSearchServerRunning,
request_method='GET', request_path='/api/v1/search')),
('fake download server', dict(
fixture=fixture_setup.FakeDownloadServerRunning,
request_method='HEAD', request_path='/download/dummy.click'))
]
def test_server_should_start_and_stop(self):
fake_server = self.fixture()
self.addCleanup(self._assert_server_not_running)
self.useFixture(fake_server)
self.netloc = urlparse.urlparse(fake_server.url).netloc
connection = httplib.HTTPConnection(self.netloc)
self.addCleanup(connection.close)
self._do_request(connection)
self.assertEqual(connection.getresponse().status, 200)
def _assert_server_not_running(self):
connection = httplib.HTTPConnection(self.netloc)
self.assertRaises(Exception, self._do_request, connection)
def _do_request(self, connection):
connection.request(self.request_method, self.request_path)
unity-scope-click-0.1+14.04.20140410.1/autopilot/unityclickscope/test_click_scope.py 0000644 0000153 0177776 00000020336 12321560256 030571 0 ustar pbuser nogroup 0000000 0000000 # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Copyright (C) 2013, 2014 Canonical Ltd.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3, as published
# by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
import logging
import subprocess
import os
import dbusmock
import fixtures
from autopilot.introspection import dbus as autopilot_dbus
from autopilot.matchers import Eventually
from testtools.matchers import Equals
from ubuntuuitoolkit import emulators as toolkit_emulators
from unity8 import process_helpers
from unity8.shell import (
emulators as unity_emulators,
tests as unity_tests
)
from unityclickscope import credentials, fake_services, fixture_setup
logger = logging.getLogger(__name__)
class BaseClickScopeTestCase(dbusmock.DBusTestCase, unity_tests.UnityTestCase):
scenarios = [
('Desktop Nexus 4', dict(
app_width=768, app_height=1280, grid_unit_px=18)),
('Desktop Nexus 10',
dict(app_width=2560, app_height=1600, grid_unit_px=20))
]
def setUp(self):
super(BaseClickScopeTestCase, self).setUp()
if os.environ.get('U1_SEARCH_BASE_URL') == 'fake':
self._use_fake_server()
if os.environ.get('DOWNLOAD_BASE_URL') == 'fake':
self._use_fake_download_server()
self._use_fake_download_service()
unity_proxy = self.launch_unity()
process_helpers.unlock_unity(unity_proxy)
self.dash = self.main_window.get_dash()
self.scope = self.dash.get_scope('applications')
def _use_fake_server(self):
fake_search_server = fixture_setup.FakeSearchServerRunning()
self.useFixture(fake_search_server)
self.useFixture(fixtures.EnvironmentVariable(
'U1_SEARCH_BASE_URL', newvalue=fake_search_server.url))
self._restart_scope()
def _use_fake_download_server(self):
fake_download_server = fixture_setup.FakeDownloadServerRunning()
self.useFixture(fake_download_server)
self.useFixture(fixtures.EnvironmentVariable(
'DOWNLOAD_BASE_URL', newvalue=fake_download_server.url))
def _use_fake_download_service(self):
self._spawn_fake_downloader()
dbus_connection = self.get_dbus(system_bus=False)
fake_download_service = fake_services.FakeDownloadService(
dbus_connection)
fake_download_service.add_method(
'getAllDownloadsWithMetadata', return_value=[])
download_object_path = '/com/canonical/applications/download/test'
fake_download_service.add_method(
'createDownload', return_value=download_object_path)
fake_download_service.add_download_object(download_object_path)
def _spawn_fake_downloader(self):
download_manager_mock = self.spawn_server(
'com.canonical.applications.Downloader',
'/',
'com.canonical.applications.DownloadManager',
system_bus=False,
stdout=subprocess.PIPE)
self.addCleanup(self._terminate_process, download_manager_mock)
def _terminate_process(self, dbus_mock):
dbus_mock.terminate()
dbus_mock.wait()
def _restart_scope(self):
logging.info('Restarting click scope.')
os.system('pkill click-scope')
os.system(
"dpkg-architecture -c "
"'/usr/lib/$DEB_HOST_MULTIARCH//unity-scope-click/click-scope' &")
def _unlock_screen(self):
self.main_window.get_greeter().swipe()
def open_scope(self):
self.dash.open_scope('applications')
self.scope.isCurrent.wait_for(True)
def open_app_preview(self, name):
self.search(name)
icon = self.scope.wait_select_single('Tile', text=name)
pointing_device = toolkit_emulators.get_pointing_device()
pointing_device.click_object(icon)
preview = self.dash.wait_select_single(AppPreview)
preview.showProcessingAction.wait_for(False)
return preview
def search(self, query):
# TODO move this to the unity8 main view emulator.
# --elopio - 2013-12-27
search_box = self._proxy.select_single("SearchIndicator")
self.touch.tap_object(search_box)
self.keyboard.type(query)
class TestCaseWithHomeScopeOpen(BaseClickScopeTestCase):
def test_open_scope_scrolling(self):
self.assertFalse(self.scope.isCurrent)
self.dash.open_scope('applications')
self.assertThat(self.scope.isCurrent, Eventually(Equals(True)))
class TestCaseWithClickScopeOpen(BaseClickScopeTestCase):
def setUp(self):
super(TestCaseWithClickScopeOpen, self).setUp()
self.open_scope()
def test_search_available_app(self):
self.search('Shorts')
self.scope.wait_select_single('Tile', text='Shorts')
def test_open_app_preview(self):
expected_details = dict(
title='Shorts', publisher='Ubuntu Click Loader')
preview = self.open_app_preview('Shorts')
details = preview.get_details()
self.assertEqual(details, expected_details)
def test_install_without_credentials(self):
preview = self.open_app_preview('Shorts')
preview.install()
error = self.dash.wait_select_single(DashPreview)
details = error.get_details()
self.assertEqual('Login Error', details.get('title'))
class ClickScopeTestCaseWithCredentials(BaseClickScopeTestCase):
def setUp(self):
self.add_u1_credentials()
super(ClickScopeTestCaseWithCredentials, self).setUp()
self.open_scope()
self.preview = self.open_app_preview('Shorts')
def add_u1_credentials(self):
account_manager = credentials.AccountManager()
account = account_manager.add_u1_credentials(
'dummy@example.com', 'dummy')
self.addCleanup(account_manager.delete_account, account)
def test_install_with_credentials_must_start_download(self):
self.assertFalse(self.preview.is_progress_bar_visible())
self.preview.install()
self.assertThat(
self.preview.is_progress_bar_visible, Eventually(Equals(True)))
class Preview(object):
def get_details(self):
"""Return the details of the open preview."""
title = self.select_single('Label', objectName='titleLabel').text
subtitle = self.select_single(
'Label', objectName='subtitleLabel').text
# The description label doesn't have an object name. Reported as bug
# http://pad.lv/1269114 -- elopio - 2014-1-14
# description = self.select_single(
# 'Label', objectName='descriptionLabel').text
# TODO check screenshots, icon, rating and reviews.
return dict(title=title, subtitle=subtitle)
# TODO move this to unity. --elopio - 2014-1-22
class DashPreview(unity_emulators.UnityEmulatorBase, Preview):
"""Autopilot emulator for the generic preview."""
# TODO move this to unity. --elopio - 2014-1-14
class AppPreview(unity_emulators.UnityEmulatorBase, Preview):
"""Autopilot emulator for the application preview."""
def get_details(self):
"""Return the details of the application whose preview is open."""
details = super(AppPreview, self).get_details()
return dict(
title=details.get('title'), publisher=details.get('subtitle'))
def install(self):
install_button = self.select_single('Button', objectName='button0')
if install_button.text != 'Install':
raise unity_emulators.UnityEmulatorException(
'Install button not found.')
self.pointing_device.click_object(install_button)
self.implicitHeight.wait_for(0)
def is_progress_bar_visible(self):
try:
self.select_single('ProgressBar', objectName='progressBar')
return True
except autopilot_dbus.StateNotFoundError:
return False
unity-scope-click-0.1+14.04.20140410.1/autopilot/unityclickscope/fake_servers.py 0000644 0000153 0177776 00000013523 12321560256 027733 0 ustar pbuser nogroup 0000000 0000000 # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Copyright (C) 2013, 2014 Canonical Ltd.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3, as published
# by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
import BaseHTTPServer
import copy
import json
import os
import tempfile
import urlparse
class BaseFakeHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
def send_json_reply(self, code, reply_json):
self.send_response(code)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(reply_json)
def send_file(self, file_path, send_body=True, extra_headers={}):
with open(file_path) as file_:
data = file_.read()
self.send_response(200)
self.send_header('Content-Length', str(len(data)))
for key, value in extra_headers.iteritems():
self.send_header(key, value)
self.end_headers()
if send_body:
self.wfile.write(data)
def send_file_headers(self, file_path, extra_headers={}):
self.send_file(file_path, send_body=False, extra_headers=extra_headers)
class FakeSearchServer(BaseHTTPServer.HTTPServer, object):
def __init__(self, server_address):
super(FakeSearchServer, self).__init__(
server_address, FakeSearchRequestHandler)
class FakeSearchRequestHandler(BaseFakeHTTPRequestHandler):
_SEARCH_PATH = '/api/v1/search'
_FAKE_SEARCH_RESPONSE_DICT = [
{
'resource_url': 'https://TODO/api/v1/package/com.ubuntu.shorts',
'icon_url': '{U1_SEARCH_BASE_URL}extra/shorts.png',
'price': 0.0,
'name': 'com.ubuntu.shorts',
'title': 'Shorts'
}
]
_FAKE_SHORTS_DETAILS_DICT = {
'website': 'https://launchpad.net/ubuntu-rssreader-app',
'description': (
'Shorts is an rssreader application\n'
'Shorts is an rss reader application that allows you to easily '
'search for new feeds.'),
'price': 0.0,
'framework': ["ubuntu-sdk-13.10"],
'terms_of_service': '',
'prices': {'USD': 0.0},
'screenshot_url': 'https://TODO/shorts0.png',
'date_published': '2013-10-16T15:58:52.469000',
'publisher': 'Ubuntu Click Loader',
'name': 'com.ubuntu.shorts',
'license': 'GNU GPL v3',
'changelog': 'Test fixes',
'support_url': 'mailto:ubuntu-touch-coreapps@lists.launchpad.net',
'icon_url': 'https://TODO/shorts.png',
'title': 'Shorts',
'binary_filesize': 164944,
'download_url': (
'{DOWNLOAD_BASE_URL}download/shorts-dummy.click'),
'click_version': '0.1',
'developer_name': 'Ubuntu Click Loader',
'version': '0.2.152',
'company_name': '',
'keywords': ['shorts', 'rss', 'news'],
'screenshot_urls': [
'https://TODO/shorts0.png',
'https://TODO/shorts1.png'
],
'architecture': ['all']
}
_FAKE_DETAILS = {
'com.ubuntu.shorts': _FAKE_SHORTS_DETAILS_DICT
}
def do_GET(self):
parsed_path = urlparse.urlparse(self.path)
if parsed_path.path.startswith(self._SEARCH_PATH):
self.send_json_reply(200, self._get_fake_search_response())
elif parsed_path.path.startswith('/extra/'):
self.send_file(parsed_path.path[1:])
elif parsed_path.path.startswith('/api/v1/package/'):
package = parsed_path.path[16:]
self.send_package_details(package)
else:
raise NotImplementedError(self.path)
def _get_fake_search_response(self):
fake_search_response = copy.deepcopy(self._FAKE_SEARCH_RESPONSE_DICT)
for result in fake_search_response:
result['icon_url'] = result['icon_url'].format(
U1_SEARCH_BASE_URL=os.environ.get('U1_SEARCH_BASE_URL'))
return json.dumps(fake_search_response)
def send_package_details(self, package):
details = copy.deepcopy(self._FAKE_DETAILS.get(package, None))
if details is not None:
details['download_url'] = details['download_url'].format(
DOWNLOAD_BASE_URL=os.environ.get('DOWNLOAD_BASE_URL'))
self.send_json_reply(
200, json.dumps(details))
else:
raise NotImplementedError(package)
class FakeDownloadServer(BaseHTTPServer.HTTPServer, object):
def __init__(self, server_address):
super(FakeDownloadServer, self).__init__(
server_address, FakeDownloadRequestHandler)
class FakeDownloadRequestHandler(BaseFakeHTTPRequestHandler):
def do_HEAD(self):
parsed_path = urlparse.urlparse(self.path)
if parsed_path.path.startswith('/download/'):
self._send_dummy_file_headers(parsed_path.path[10:])
else:
raise NotImplementedError(self.path)
def _send_dummy_file_headers(self, name):
dummy_file_path = self._make_dummy_file(name)
self.send_file_headers(
dummy_file_path, extra_headers={'X-Click-Token': 'dummy'})
os.remove(dummy_file_path)
def _make_dummy_file(self, name):
dummy_file = tempfile.NamedTemporaryFile(
prefix='dummy', suffix='.click', delete=False)
dummy_file.write('Dummy click file.')
dummy_file.write(name)
dummy_file.close()
return dummy_file.name
unity-scope-click-0.1+14.04.20140410.1/autopilot/CMakeLists.txt 0000644 0000153 0177776 00000001140 12321560256 024222 0 ustar pbuser nogroup 0000000 0000000 set (Python_ADDITIONAL_VERSIONS 2.7)
find_package (PythonInterp)
set (AUTOPILOT_DIR unityclickscope)
execute_process (
COMMAND ${PYTHON_EXECUTABLE} -c "from distutils import sysconfig; print sysconfig.get_python_lib()"
COMMAND sed -r -e "s|/usr/(local/)?||g"
OUTPUT_VARIABLE PYTHON_PACKAGE_DIR OUTPUT_STRIP_TRAILING_WHITESPACE)
install (
DIRECTORY ${AUTOPILOT_DIR}
DESTINATION ${PYTHON_PACKAGE_DIR}
)
add_custom_target(test-click-scope-autopilot-local
COMMAND U1_SEARCH_BASE_URL=fake DOWNLOAD_BASE_URL=fake autopilot run ${AUTOPILOT_DIR}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
unity-scope-click-0.1+14.04.20140410.1/data/ 0000755 0000153 0177776 00000000000 12321560627 020361 5 ustar pbuser nogroup 0000000 0000000 unity-scope-click-0.1+14.04.20140410.1/data/clickscope-screenshot.jpg 0000644 0000153 0177776 00000103507 12321560256 025361 0 ustar pbuser nogroup 0000000 0000000 JFIF H H C
% !###&)&")"#" C """"""""""""""""""""""""""""""""""""""""""""""""""" @@ L TApKu`,d5ͣ pJ6Dh A @ .#V`*tiF $ I 2c2DtG@ R
2 A$ J$ ( "
u
2N/fe$ H P#NRY2k2- *\ B( B X @ ,@ Ҋ $[Alj4%!Fhe 狙bYZe{ VWw,~7WnmՓNnCm?fP0r^mɎ$|vo˟tPP.~jŔJg2#y};ݯͳu>Дaop[ǫ'͞ ( d s{[,}ydyRޔU~egt5E]1x"c9i@yfj 8=_.gc^>>W_~@WK;N~V]_)ߪ/O(1˭ɳg,0Ll<V