From adc44f45a2e3f2763e78d4a8f7c6c44c490cc3fd Mon Sep 17 00:00:00 2001
From: zming <517046497@qq.com>
Date: Fri, 2 Feb 2018 11:16:36 +0800
Subject: [PATCH 01/39] update tests
---
seafileapi/repos.py | 4 ++--
tests/fixtures.py | 3 +--
tests/test_repos.py | 4 +---
3 files changed, 4 insertions(+), 7 deletions(-)
diff --git a/seafileapi/repos.py b/seafileapi/repos.py
index 1005d7eae6..70a8fa7e47 100644
--- a/seafileapi/repos.py
+++ b/seafileapi/repos.py
@@ -5,8 +5,8 @@ class Repos(object):
def __init__(self, client):
self.client = client
- def create_repo(self, name, desc, password=None):
- data = {'name': name, 'desc': desc}
+ def create_repo(self, name, password=None):
+ data = {'name': name}
if password:
data['passwd'] = password
repo_json = self.client.post('/api2/repos/', data=data).json()
diff --git a/tests/fixtures.py b/tests/fixtures.py
index 950f5a0a26..9247e3243b 100644
--- a/tests/fixtures.py
+++ b/tests/fixtures.py
@@ -19,8 +19,7 @@ def client():
@pytest.yield_fixture(scope='function')
def repo(client):
repo_name = 'tmp-测试资料库-%s' % randstring()
- repo_desc = 'tmp, 一个测试资料库-%s' % randstring()
- repo = client.repos.create_repo(repo_name, repo_desc)
+ repo = client.repos.create_repo(repo_name)
try:
yield repo
finally:
diff --git a/tests/test_repos.py b/tests/test_repos.py
index 907f564ef2..d1ab6421d7 100644
--- a/tests/test_repos.py
+++ b/tests/test_repos.py
@@ -25,11 +25,9 @@ def test_list_repos(client):
def _create_repo(client, password=None):
repo_name = '测试资料库-%s' % randstring()
- repo_desc = '一个测试资料库-%s' % randstring()
- repo = client.repos.create_repo(repo_name, repo_desc, password=password)
+ repo = client.repos.create_repo(repo_name, password=password)
assert repo.name == repo_name
- assert repo.desc == repo_desc
assert len(repo.id) == 36
assert repo.encrypted == (password is not None)
assert repo.owner == 'self'
From b2f5745197749ac7aa209f99c9694d4475bce5e1 Mon Sep 17 00:00:00 2001
From: Dongsu Park
Date: Thu, 28 May 2015 18:40:54 +0200
Subject: [PATCH 02/39] functests.sh: use py.test instead of nosetests
---
functests.sh | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/functests.sh b/functests.sh
index 1dc19989ca..6982768ce6 100755
--- a/functests.sh
+++ b/functests.sh
@@ -18,4 +18,4 @@ SRCDIR=$(dirname "${SCRIPT}")
cd "${SRCDIR}"
-nosetests $@
+py.test $@
From 6d9c73d9b1a6a641dcb4ca0dbd69c987812d34ea Mon Sep 17 00:00:00 2001
From: Dongsu Park
Date: Sat, 30 May 2015 17:43:45 +0200
Subject: [PATCH 03/39] seafileapi: implement rename, moveTo, copyTo methods
implement methods in the class _SeafDirentBase:
rename(), moveTo(), and copyTo().
---
seafileapi/files.py | 30 +++++++++++++++++++++++++-----
1 file changed, 25 insertions(+), 5 deletions(-)
diff --git a/seafileapi/files.py b/seafileapi/files.py
index 6cc273edaf..ff7e412106 100644
--- a/seafileapi/files.py
+++ b/seafileapi/files.py
@@ -39,14 +39,34 @@ def delete(self):
resp = self.client.delete(url)
return resp
- def rename(self):
- pass
+ def rename(self, newname):
+ """Change filename to newname
+ """
+ url = '/api2/repos/%s/file/' % self.repo.id + querystr(p=self.path)
+ postdata = {'operation': 'rename', 'newname': newname}
+ resp = self.client.post(url, data=postdata)
+ self.id = resp.headers['oid']
- def copyTo(self, dst_dir, dst_repo=None):
- pass
+ def copyTo(self, file_names, dst_dir, dst_repo=None):
+ """Copy filename to newname (also to a different directory)
+ """
+ src_dir = os.path.dirname(self.path)
+ url = '/api2/repos/%s/fileops/copy/' % self.repo.id + querystr(p=src_dir)
+ if dst_repo is None:
+ dst_repo = self.repo.id
+ postdata = {'operation': 'copy', 'file_names': file_names, 'dst_repo': dst_repo, 'dst_dir': dst_dir}
+ resp = self.client.post(url, data=postdata)
+ self.id = resp.headers['oid']
def moveTo(self, dst_dir, dst_repo=None):
- pass
+ """Move filename to newname (also to a different directory)
+ """
+ url = '/api2/repos/%s/file/' % self.repo.id + querystr(p=self.path)
+ if dst_repo is None:
+ dst_repo = self.repo.id
+ postdata = {'operation': 'move', 'dst_repo': dst_repo, 'dst_dir': dst_dir}
+ resp = self.client.post(url, data=postdata)
+ self.id = resp.headers['oid']
def get_share_link(self):
pass
From 14faea64fbf1063b5b97753259df85e4c240ba73 Mon Sep 17 00:00:00 2001
From: Dongsu Park
Date: Tue, 2 Jun 2015 17:20:37 +0200
Subject: [PATCH 04/39] seafileapi: make moveTo, rename update self.path
self.path should be also updated after renaming/moving a file.
---
seafileapi/files.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/seafileapi/files.py b/seafileapi/files.py
index ff7e412106..0434de2e2c 100644
--- a/seafileapi/files.py
+++ b/seafileapi/files.py
@@ -46,6 +46,7 @@ def rename(self, newname):
postdata = {'operation': 'rename', 'newname': newname}
resp = self.client.post(url, data=postdata)
self.id = resp.headers['oid']
+ self.path = os.path.join(os.path.dirname(self.path), newname)
def copyTo(self, file_names, dst_dir, dst_repo=None):
"""Copy filename to newname (also to a different directory)
@@ -67,6 +68,7 @@ def moveTo(self, dst_dir, dst_repo=None):
postdata = {'operation': 'move', 'dst_repo': dst_repo, 'dst_dir': dst_dir}
resp = self.client.post(url, data=postdata)
self.id = resp.headers['oid']
+ self.path = os.path.join(dst_dir, os.path.basename(self.path))
def get_share_link(self):
pass
From 6afb351e31129ab4f9b195120ff496519ed36542 Mon Sep 17 00:00:00 2001
From: Dongsu Park
Date: Tue, 2 Jun 2015 17:33:23 +0200
Subject: [PATCH 05/39] tests: add a test for rename, moveTo, copyTo
---
tests/test_files.py | 60 +++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 60 insertions(+)
diff --git a/tests/test_files.py b/tests/test_files.py
index e862757e40..94a87b0341 100644
--- a/tests/test_files.py
+++ b/tests/test_files.py
@@ -88,3 +88,63 @@ def test_upload_string_as_file_content(repo):
f = rootdir.upload(fcontent, fname)
assert f.name == fname
assert f.get_content() == fcontent
+
+'/测试目录一-%s' % randstring()
+@pytest.mark.parametrize('parentpath', [
+ '/',
+ '/seafdir-%s' % randstring()
+])
+def test_rename_move_copy(repo, parentpath):
+ rootdir = repo.get_dir('/')
+ assert len(rootdir.ls()) == 0
+
+ if parentpath == '/':
+ parentdir = rootdir
+ else:
+ parentdir = rootdir.mkdir(parentpath[1:])
+
+ # create a file
+ testfile = parentdir.create_empty_file('测试文件-%s.txt' % randstring())
+ assert testfile.size == 0
+
+ entries = parentdir.ls(force_refresh=True)
+ assert len(entries) == 1
+
+ entry = entries[0]
+ assert entry.path == testfile.path
+ assert entry.id == testfile.id
+ assert entry.size == testfile.size
+
+ # rename a file
+ newfname = 'newfile.txt'
+ testfile.rename(newfname)
+ assert testfile.name == newfname
+
+ # create a folder
+ testdir = parentdir.mkdir('测试目录-%s' % randstring())
+ assert len(parentdir.ls()) == 2
+ assert len(testdir.ls()) == 0
+
+ direntry = [entry for entry in parentdir.ls() if entry.isdir][0]
+ assert direntry.path == testdir.path
+
+ # move a file to the new folder
+ testfile.moveTo(testdir.name, dst_repo=None)
+
+ assert testfile.name == newfname
+ fileentry = [entry for entry in testdir.ls() if not entry.isdir][0]
+ assert fileentry.path == testfile.path
+
+ # copy a file to the parent folder
+ filenames.append(testfile)
+ testfile.copyTo(filenames, parentdir.name, dst_repo=None)
+
+ assert testfile.name == newfname
+ fileentry = [entry for entry in parentdir.ls() if not entry.isdir][0]
+ assert fileentry.path == testfile.path
+
+ # clean up test directories / files
+ testfile.delete()
+ assert len(parentdir.ls(force_refresh=True)) == 1
+ testdir.delete()
+ assert len(parentdir.ls(force_refresh=True)) == 0
From 4f7aac5d1a6ea9bc7e434773b8bcad7112e4d233 Mon Sep 17 00:00:00 2001
From: Dongsu Park
Date: Tue, 2 Jun 2015 17:47:59 +0200
Subject: [PATCH 06/39] functests.sh: add a missing export call for
SEAFILE_TEST_SERVER_ADDRESS
---
functests.sh | 1 +
1 file changed, 1 insertion(+)
diff --git a/functests.sh b/functests.sh
index 6982768ce6..b73af57951 100755
--- a/functests.sh
+++ b/functests.sh
@@ -8,6 +8,7 @@
: ${SEAFILE_TEST_ADMIN_USERNAME="admin@seafiletest.com"}
: ${SEAFILE_TEST_ADMIN_PASSWORD="adminadmin"}
+export SEAFILE_TEST_SERVER_ADDRESS
export SEAFILE_TEST_USERNAME
export SEAFILE_TEST_PASSWORD
export SEAFILE_TEST_ADMIN_USERNAME
From 07792a4796e9d16787db1dc08ece77cc96d373d2 Mon Sep 17 00:00:00 2001
From: zming <517046497@qq.com>
Date: Fri, 2 Feb 2018 16:15:26 +0800
Subject: [PATCH 07/39] update
---
seafileapi/files.py | 79 ++++++++----
tests/test_files.py | 293 ++++++++++++++++++++++++++++++++++++++------
2 files changed, 311 insertions(+), 61 deletions(-)
diff --git a/seafileapi/files.py b/seafileapi/files.py
index 0434de2e2c..d36f2ea5c6 100644
--- a/seafileapi/files.py
+++ b/seafileapi/files.py
@@ -40,35 +40,66 @@ def delete(self):
return resp
def rename(self, newname):
- """Change filename to newname
+ """Change file/folder name to newname
"""
- url = '/api2/repos/%s/file/' % self.repo.id + querystr(p=self.path)
+ suffix = 'dir' if self.isdir else 'file'
+ url = '/api2/repos/%s/%s/' % (self.repo.id, suffix) + querystr(p=self.path, reloaddir='true')
postdata = {'operation': 'rename', 'newname': newname}
resp = self.client.post(url, data=postdata)
- self.id = resp.headers['oid']
- self.path = os.path.join(os.path.dirname(self.path), newname)
-
- def copyTo(self, file_names, dst_dir, dst_repo=None):
- """Copy filename to newname (also to a different directory)
+ succeeded = resp.status_code == 200
+ if succeeded:
+ if self.isdir:
+ new_dirent = self.repo.get_dir(os.path.join(os.path.dirname(self.path), newname))
+ else:
+ new_dirent = self.repo.get_file(os.path.join(os.path.dirname(self.path), newname))
+ for key in self.__dict__.keys():
+ self.__dict__[key] = new_dirent.__dict__[key]
+ return succeeded
+
+ def _copy_move_task(self, operation, dirent_type, dst_dir, dst_repo_id=None):
+ url = '/api/v2.1/copy-move-task/'
+ src_repo_id = self.repo.id
+ src_parent_dir = os.path.dirname(self.path)
+ src_dirent_name = os.path.basename(self.path)
+ dst_repo_id = dst_repo_id
+ dst_parent_dir = dst_dir
+ operation = operation
+ dirent_type = dirent_type
+ postdata = {'src_repo_id': src_repo_id, 'src_parent_dir': src_parent_dir,
+ 'src_dirent_name': src_dirent_name, 'dst_repo_id': dst_repo_id,
+ 'dst_parent_dir': dst_parent_dir, 'operation': operation,
+ 'dirent_type': dirent_type}
+ return self.client.post(url, data=postdata)
+
+ def copyTo(self, dst_dir, dst_repo_id=None):
+ """Copy file/folder to other directory (also to a different repo)
"""
- src_dir = os.path.dirname(self.path)
- url = '/api2/repos/%s/fileops/copy/' % self.repo.id + querystr(p=src_dir)
- if dst_repo is None:
- dst_repo = self.repo.id
- postdata = {'operation': 'copy', 'file_names': file_names, 'dst_repo': dst_repo, 'dst_dir': dst_dir}
- resp = self.client.post(url, data=postdata)
- self.id = resp.headers['oid']
-
- def moveTo(self, dst_dir, dst_repo=None):
- """Move filename to newname (also to a different directory)
+ if dst_repo_id is None:
+ dst_repo_id = self.repo.id
+
+ dirent_type = 'dir' if self.isdir else 'file'
+ resp = self._copy_move_task('copy', dirent_type, dst_dir, dst_repo_id)
+ return resp.status_code == 200
+
+ def moveTo(self, dst_dir, dst_repo_id=None):
+ """Move file/folder to other directory (also to a different repo)
"""
- url = '/api2/repos/%s/file/' % self.repo.id + querystr(p=self.path)
- if dst_repo is None:
- dst_repo = self.repo.id
- postdata = {'operation': 'move', 'dst_repo': dst_repo, 'dst_dir': dst_dir}
- resp = self.client.post(url, data=postdata)
- self.id = resp.headers['oid']
- self.path = os.path.join(dst_dir, os.path.basename(self.path))
+ if dst_repo_id is None:
+ dst_repo_id = self.repo.id
+
+ dirent_type = 'dir' if self.isdir else 'file'
+ resp = self._copy_move_task('move', dirent_type, dst_dir, dst_repo_id)
+ succeeded = resp.status_code == 200
+ if succeeded:
+ new_repo = self.client.repos.get_repo(dst_repo_id)
+ dst_path = os.path.join(dst_dir, os.path.basename(self.path))
+ if self.isdir:
+ new_dirent = new_repo.get_dir(dst_path)
+ else:
+ new_dirent = new_repo.get_file(dst_path)
+ for key in self.__dict__.keys():
+ self.__dict__[key] = new_dirent.__dict__[key]
+ return succeeded
def get_share_link(self):
pass
diff --git a/tests/test_files.py b/tests/test_files.py
index 94a87b0341..5509da09bc 100644
--- a/tests/test_files.py
+++ b/tests/test_files.py
@@ -1,5 +1,6 @@
#coding: UTF-8
+import os
import pytest
from tests.utils import randstring, datafile, filesize
@@ -10,7 +11,7 @@
])
def test_create_delete_file_dir(repo, parentpath):
rootdir = repo.get_dir('/')
- assert len(rootdir.ls()) == 0
+ assert len(rootdir.ls(force_refresh=True)) == 0
if parentpath == '/':
parentdir = rootdir
@@ -31,10 +32,10 @@ def test_create_delete_file_dir(repo, parentpath):
# create a folder
testdir = parentdir.mkdir('测试目录-%s' % randstring())
- assert len(parentdir.ls()) == 2
- assert len(testdir.ls()) == 0
+ assert len(parentdir.ls(force_refresh=True)) == 2
+ assert len(testdir.ls(force_refresh=True)) == 0
- direntry = [entry for entry in parentdir.ls() if entry.isdir][0]
+ direntry = [e for e in parentdir.ls(force_refresh=True) if e.isdir][0]
assert direntry.path == testdir.path
testfile.delete()
@@ -49,7 +50,7 @@ def test_create_delete_file_dir(repo, parentpath):
])
def test_upload_file(repo, parentpath):
rootdir = repo.get_dir('/')
- assert len(rootdir.ls()) == 0
+ assert len(rootdir.ls(force_refresh=True)) == 0
if parentpath == '/':
parentdir = rootdir
@@ -89,14 +90,14 @@ def test_upload_string_as_file_content(repo):
assert f.name == fname
assert f.get_content() == fcontent
-'/测试目录一-%s' % randstring()
@pytest.mark.parametrize('parentpath', [
'/',
- '/seafdir-%s' % randstring()
+ #'/测试目录一-%s' % randstring()
+ '/qweqwe%s' % randstring()
])
-def test_rename_move_copy(repo, parentpath):
+def test_rename_file(repo, parentpath):
rootdir = repo.get_dir('/')
- assert len(rootdir.ls()) == 0
+ assert len(rootdir.ls(force_refresh=True)) == 0
if parentpath == '/':
parentdir = rootdir
@@ -107,44 +108,262 @@ def test_rename_move_copy(repo, parentpath):
testfile = parentdir.create_empty_file('测试文件-%s.txt' % randstring())
assert testfile.size == 0
- entries = parentdir.ls(force_refresh=True)
- assert len(entries) == 1
-
- entry = entries[0]
- assert entry.path == testfile.path
- assert entry.id == testfile.id
- assert entry.size == testfile.size
+ assert len(parentdir.ls(force_refresh=True)) == 1
# rename a file
newfname = 'newfile.txt'
testfile.rename(newfname)
- assert testfile.name == newfname
+ assert newfname == testfile.name
+
+@pytest.mark.parametrize('parentpath', [
+ '/',
+ '/测试目录一-%s' % randstring()
+])
+def test_rename_folder(repo, parentpath):
+ rootdir = repo.get_dir('/')
+ assert len(rootdir.ls(force_refresh=True)) == 0
+
+ if parentpath == '/':
+ parentdir = rootdir
+ else:
+ parentdir = rootdir.mkdir(parentpath[1:])
# create a folder
- testdir = parentdir.mkdir('测试目录-%s' % randstring())
- assert len(parentdir.ls()) == 2
- assert len(testdir.ls()) == 0
+ testfolder = parentdir.mkdir('测试文件夹-%s' % randstring())
+ assert testfolder.size == 0
- direntry = [entry for entry in parentdir.ls() if entry.isdir][0]
- assert direntry.path == testdir.path
+ assert len(parentdir.ls(force_refresh=True)) == 1
- # move a file to the new folder
- testfile.moveTo(testdir.name, dst_repo=None)
+ # rename a file
+ newfname = 'newfolder'
+ testfolder.rename(newfname)
+ assert newfname == testfolder.name
- assert testfile.name == newfname
- fileentry = [entry for entry in testdir.ls() if not entry.isdir][0]
- assert fileentry.path == testfile.path
+@pytest.mark.parametrize('parentpath', [
+ '/',
+ '/测试目录一-%s' % randstring()
+])
+def test_copy_file(repo, parentpath):
+ rootdir = repo.get_dir('/')
+ assert len(rootdir.ls(force_refresh=True)) == 0
- # copy a file to the parent folder
- filenames.append(testfile)
- testfile.copyTo(filenames, parentdir.name, dst_repo=None)
+ if parentpath == '/':
+ parentdir = rootdir
+ else:
+ parentdir = rootdir.mkdir(parentpath[1:])
- assert testfile.name == newfname
- fileentry = [entry for entry in parentdir.ls() if not entry.isdir][0]
- assert fileentry.path == testfile.path
+ # create a file
+ testfile = parentdir.create_empty_file('测试文件-%s.txt' % randstring())
+ assert testfile.size == 0
- # clean up test directories / files
- testfile.delete()
assert len(parentdir.ls(force_refresh=True)) == 1
- testdir.delete()
- assert len(parentdir.ls(force_refresh=True)) == 0
+
+ tempfolder = parentdir.mkdir('tempfolder_%s' % randstring())
+ assert len(tempfolder.ls(force_refresh=True)) == 0
+ testfile.copyTo(tempfolder.path)
+ assert len(tempfolder.ls(force_refresh=True)) == 1
+ assert os.path.basename(tempfolder.ls(force_refresh=True)[-1].path) == os.path.basename(testfile.path)
+
+@pytest.mark.parametrize('parentpath', [
+ '/',
+ '/测试目录一-%s' % randstring()
+])
+def test_copy_file_to_other_repo(client, repo, parentpath):
+ rootdir = repo.get_dir('/')
+ assert len(rootdir.ls(force_refresh=True)) == 0
+
+ if parentpath == '/':
+ parentdir = rootdir
+ else:
+ parentdir = rootdir.mkdir(parentpath[1:])
+
+ # create a file
+ testfile = parentdir.create_empty_file('测试文件-%s.txt' % randstring())
+ assert testfile.size == 0
+
+ assert len(parentdir.ls(force_refresh=True)) == 1
+
+ temp_repo = client.repos.create_repo('temp_repo')
+ try:
+ root_dir = temp_repo.get_dir('/')
+ temp_dir = root_dir.mkdir('temp_dir')
+ assert len(temp_dir.ls(force_refresh=True)) == 0
+ testfile.copyTo(temp_dir.path, temp_repo.id)
+ assert len(temp_dir.ls(force_refresh=True)) == 1
+ assert os.path.basename(temp_dir.ls(force_refresh=True)[0].path) == os.path.basename(testfile.path)
+ finally:
+ temp_repo.delete()
+
+@pytest.mark.parametrize('parentpath', [
+ '/',
+ '/测试目录一-%s' % randstring()
+])
+def test_copy_folder(repo, parentpath):
+ rootdir = repo.get_dir('/')
+ assert len(rootdir.ls(force_refresh=True)) == 0
+
+ if parentpath == '/':
+ parentdir = rootdir
+ else:
+ parentdir = rootdir.mkdir(parentpath[1:])
+
+ # create a folder
+ testfolder = parentdir.mkdir('测试文件夹-%s' % randstring())
+ assert testfolder.size == 0
+ tempfolder = parentdir.mkdir('temp-folder-%s' % randstring())
+ assert tempfolder.size == 0
+
+ assert len(tempfolder.ls(force_refresh=True)) == 0
+ assert len(parentdir.ls(force_refresh=True)) == 2
+
+ # copy a file
+ testfolder.copyTo(tempfolder.path)
+ assert len(tempfolder.ls(force_refresh=True)) == 1
+ assert os.path.basename(tempfolder.ls(force_refresh=True)[0].path) == os.path.basename(testfolder.path)
+
+@pytest.mark.parametrize('parentpath', [
+ '/',
+ '/测试目录一-%s' % randstring()
+])
+def test_copy_folder_to_other_repo(client, repo, parentpath):
+ rootdir = repo.get_dir('/')
+ assert len(rootdir.ls(force_refresh=True)) == 0
+
+ if parentpath == '/':
+ parentdir = rootdir
+ else:
+ parentdir = rootdir.mkdir(parentpath[1:])
+
+ # create a folder
+ testfolder = parentdir.mkdir('测试文件夹-%s' % randstring())
+ assert testfolder.size == 0
+
+ assert len(parentdir.ls(force_refresh=True)) == 1
+
+ temp_repo = client.repos.create_repo('temp_repo')
+ try:
+ root_folder = temp_repo.get_dir('/')
+ tempfolder = root_folder.mkdir('tempfolder')
+
+ assert len(tempfolder.ls(force_refresh=True)) == 0
+ # copy a folder
+ testfolder.copyTo(tempfolder.path, temp_repo.id)
+ assert len(tempfolder.ls(force_refresh=True)) == 1
+ assert os.path.basename(tempfolder.ls(force_refresh=True)[0].path) == os.path.basename(testfolder.path)
+ finally:
+ temp_repo.delete()
+
+@pytest.mark.parametrize('parentpath', [
+ '/',
+ '/测试目录一-%s' % randstring()
+])
+def test_move_file(repo, parentpath):
+ rootdir = repo.get_dir('/')
+ assert len(rootdir.ls(force_refresh=True)) == 0
+
+ if parentpath == '/':
+ parentdir = rootdir
+ else:
+ parentdir = rootdir.mkdir(parentpath[1:])
+
+ # create a file
+ testfile = parentdir.create_empty_file('测试文件-%s.txt' % randstring())
+ assert testfile.size == 0
+
+ assert len(parentdir.ls(force_refresh=True)) == 1
+
+ tempfolder = parentdir.mkdir('tempfolder_%s' % randstring())
+ assert len(tempfolder.ls(force_refresh=True)) == 0
+
+ testfile.moveTo(tempfolder.path)
+ assert testfile.path == os.path.join(tempfolder.path, os.path.basename(testfile.path))
+ assert len(tempfolder.ls(force_refresh=True)) == 1
+
+@pytest.mark.parametrize('parentpath', [
+ '/',
+ '/测试目录一-%s' % randstring()
+])
+def test_move_file_to_other_repo(client, repo, parentpath):
+ rootdir = repo.get_dir('/')
+ assert len(rootdir.ls(force_refresh=True)) == 0
+
+ if parentpath == '/':
+ parentdir = rootdir
+ else:
+ parentdir = rootdir.mkdir(parentpath[1:])
+
+ # create a file
+ testfile = parentdir.create_empty_file('测试文件-%s.txt' % randstring())
+ assert testfile.size == 0
+
+ assert len(parentdir.ls(force_refresh=True)) == 1
+
+ temp_repo = client.repos.create_repo('temp_repo')
+ try:
+ root_dir = temp_repo.get_dir('/')
+ temp_dir = root_dir.mkdir('temp_dir')
+ assert len(temp_dir.ls(force_refresh=True)) == 0
+ testfile.moveTo(temp_dir.path, temp_repo.id)
+ assert testfile.path == os.path.join(temp_dir.path, os.path.basename(testfile.path))
+ assert len(temp_dir.ls(force_refresh=True)) == 1
+ assert testfile.repo.id == temp_repo.id
+ finally:
+ temp_repo.delete()
+
+@pytest.mark.parametrize('parentpath', [
+ '/',
+ '/测试目录一-%s' % randstring()
+])
+def test_move_folder(repo, parentpath):
+ rootdir = repo.get_dir('/')
+ assert len(rootdir.ls(force_refresh=True)) == 0
+
+ if parentpath == '/':
+ parentdir = rootdir
+ else:
+ parentdir = rootdir.mkdir(parentpath[1:])
+
+ # create a folder
+ testfolder = parentdir.mkdir('测试文件夹-%s' % randstring())
+ assert testfolder.size == 0
+ tempfolder = parentdir.mkdir('temp-folder-%s' % randstring())
+ assert tempfolder.size == 0
+
+ assert len(parentdir.ls(force_refresh=True)) == 2
+
+ # move a folder
+ testfolder.moveTo(tempfolder.path)
+ assert testfolder.path == os.path.join(tempfolder.path, os.path.basename(testfolder.path))
+
+@pytest.mark.parametrize('parentpath', [
+ '/',
+ '/测试目录一-%s' % randstring()
+])
+def test_move_folder_to_other_repo(client, repo, parentpath):
+ rootdir = repo.get_dir('/')
+ assert len(rootdir.ls(force_refresh=True)) == 0
+
+ if parentpath == '/':
+ parentdir = rootdir
+ else:
+ parentdir = rootdir.mkdir(parentpath[1:])
+
+ # create a folder
+ testfolder = parentdir.mkdir('测试文件夹-%s' % randstring())
+ assert testfolder.size == 0
+
+ assert len(parentdir.ls(force_refresh=True)) == 1
+
+ temp_repo = client.repos.create_repo('temp_repo')
+ try:
+ root_folder = temp_repo.get_dir('/')
+ tempfolder = root_folder.mkdir('tempfolder')
+
+ assert len(tempfolder.ls(force_refresh=True)) == 0
+ # move a folder
+ testfolder.moveTo(tempfolder.path, temp_repo.id)
+ assert testfolder.path == os.path.join(tempfolder.path, os.path.basename(testfolder.path))
+ assert testfolder.repo.id == temp_repo.id
+ assert len(tempfolder.ls(force_refresh=True)) == 1
+ finally:
+ temp_repo.delete()
From 9744abe88d736e11faf1583fd56131ca2925de82 Mon Sep 17 00:00:00 2001
From: zming <517046497@qq.com>
Date: Wed, 31 Jan 2018 18:09:10 +0800
Subject: [PATCH 08/39] public document
---
doc.md | 468 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 468 insertions(+)
create mode 100644 doc.md
diff --git a/doc.md b/doc.md
new file mode 100644
index 0000000000..abf9479110
--- /dev/null
+++ b/doc.md
@@ -0,0 +1,468 @@
+# Python Seafile
+
+
+
+# Python Seafile
+## Get Client ##
+**Request Parameters**
+
+* server
+* username
+* password
+
+**Sample Case**
+
+```python
+
+ import seafileapi
+
+ client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
+```
+
+**Return Type**
+
+A Client Object
+
+## SeafFile ##
+### Get Content ###
+
+**Request Parameters**
+
+None
+
+**Sample Case**
+
+
+
+```python
+
+ import seafileapi
+
+ client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
+ repo = client.repos.get_repo('09c16e2a-ff1a-4207-99f3-1351c3f1e507')
+ seaffile = repo.get_file('/root/test.md')
+
+ content = seaffile.get_content()
+```
+
+**Return Type**
+
+File Content
+
+### Delete a file ###
+**Request Parameters**
+
+None
+
+**Sample Case**
+
+```python
+
+ import seafileapi
+
+ client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
+ repo = client.repos.get_repo('09c16e2a-ff1a-4207-99f3-1351c3f1e507')
+ seaffile = repo.get_file('/root/test.md')
+
+ seaffile.delete()
+```
+
+**Return Type**
+
+A Response Instance
+
+## SeafDir ##
+### List ###
+**Request Parameters**
+
+* force_refresh (default False)
+
+**Sample Case**
+
+```python
+
+ import seafileapi
+
+ client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
+ repo = client.repos.get_repo('09c16e2a-ff1a-4207-99f3-1351c3f1e507')
+ seafdir = repo.get_dir('/root')
+
+ lst = seafdir.ls(force_refresh=True)
+ print lst
+ Out >>> [SeafDir[repo=01ccc4,path=/Seahub/6.1.x,entries=14],
+ SeafDir[repo=01ccc4,path=/Seahub/6.2.2-pro,entries=1],
+ SeafDir[repo=01ccc4,path=/Seahub/6.2.3,entries=15],
+ SeafDir[repo=01ccc4,path=/Seahub/6.2.x,entries=5],
+ SeafFile[repo=01ccc4,path=/Seahub/.DS_Store,size=6148],
+ SeafFile[repo=01ccc4,path=/Seahub/error.md,size=127],
+ SeafFile[repo=01ccc4,path=/Seahub/preview-research.md,size=1030]]
+
+ print [dirent.name for dirent in lst]
+ Out >>> ['6.1.x',
+ '6.2.2-pro',
+ '6.2.3',
+ '6.2.x',
+ '.DS_Store',
+ 'error.md',
+ 'preview-research.md']
+```
+
+**Return Type**
+
+List of SeafDir and SeafFile
+
+### Create Empty File ###
+**Request Parameters**
+
+* name
+
+**Sample Case**
+
+```python
+
+ import seafileapi
+
+ client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
+ repo = client.repos.get_repo('09c16e2a-ff1a-4207-99f3-1351c3f1e507')
+ seafdir = repo.get_dir('/root')
+
+ new_file = seafdir.create_empty_file('tmp_file.md')
+```
+
+**Return Type**
+
+A SeafFile Object of new empty file
+
+### Create New Folder ###
+**Request Parameters**
+
+* name
+
+**Sample Case**
+
+```python
+
+ import seafileapi
+
+ client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
+ repo = client.repos.get_repo('09c16e2a-ff1a-4207-99f3-1351c3f1e507')
+ seafdir = repo.get_dir('/root')
+
+ new_dir = seafdir.mkdir('tmp_dir')
+```
+
+**Return Type**
+
+A SeafDir Object of new dir
+
+### Upload ###
+**Request Parameters**
+* fileobj
+* filename
+
+**Sample Case**
+
+```python
+
+ import seafileapi
+
+ client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
+ repo = client.repos.get_repo('09c16e2a-ff1a-4207-99f3-1351c3f1e507')
+ seafdir = repo.get_dir('/root')
+
+ file = seafdir.upload('this is file content', 'tmp_file.md')
+```
+
+**Return Type**
+
+A SeafFile Object of upload file
+
+
+### Upload Local File ###
+**Request Parameters**
+
+* filepath
+* name (default None, default use local file name)
+
+**Sample Case**
+
+```python
+
+ import seafileapi
+
+ client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
+ repo = client.repos.get_repo('09c16e2a-ff1a-4207-99f3-1351c3f1e507')
+ seafdir = repo.get_dir('/root')
+
+ file = seafdir.upload_local_file('/home/ubuntu/env.md')
+```
+
+**Return Type**
+
+A SeafFile Object of upload file
+
+**Exception**
+
+* Local file does not exist.
+
+### delete a folder ###
+**Request Parameters**
+
+None
+
+**Sample Case**
+
+```python
+
+ import seafileapi
+
+ client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
+ repo = client.repos.get_repo('09c16e2a-ff1a-4207-99f3-1351c3f1e507')
+ seafdir = repo.get_dir('/root')
+
+ seafdir.delete()
+```
+
+**Return Type**
+
+A Response Instance
+
+## Repo ##
+### From Json ###
+**Request Parameters**
+
+* client
+* repo_json
+
+**Sample Case**
+
+```python
+
+ import seafileapi
+ from seafileapi.repo import Repo
+
+ client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
+ repo_json = {'id': '09c16e2a-ff1a-4207-99f3-1351c3f1e507', 'name': 'test_repo', 'encrypted': False, 'permission': 'rw', 'owner': 'test@admin.com'}
+ repo = Repo.from_json(client, repo_json)
+```
+
+
+**Return Type**
+
+A Repo Object
+
+### Is ReadOnly ###
+
+**Request Parameters**
+
+None
+
+**Sample Case**
+
+```python
+
+ import seafileapi
+
+ client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
+ repo = client.repos.get_repo('09c16e2a-ff1a-4207-99f3-1351c3f1e507')
+ is_readonly = repo.is_readonly()
+```
+
+**Return Type**
+
+True or False
+
+### Get File ###
+
+**Request Parameters**
+
+* path
+
+**Sample Case**
+
+```python
+
+ import seafileapi
+
+ client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
+ repo = client.repos.get_repo('09c16e2a-ff1a-4207-99f3-1351c3f1e507')
+ seaffile = repo.get_file('/root/test.md')
+```
+
+**Return Type**
+
+A SeafFile Object
+
+**Exception**
+
+* File does not exist.
+
+### Get Dir ###
+
+**Request Parameters**
+
+* path
+
+**Sample Case**
+
+```python
+
+ import seafileapi
+
+ client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
+ repo = client.repos.get_repo('09c16e2a-ff1a-4207-99f3-1351c3f1e507')
+ seafdir = repo.get_dir('/root')
+```
+
+**Return Type**
+
+A SeafDir Object
+
+**Exception**
+
+* Dir does not exist.
+
+### Delete Repo ###
+
+**Request Parameters**
+
+None
+
+**Sample Case**
+
+```python
+
+ import seafileapi
+
+ client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
+ repo = client.repos.get_repo('09c16e2a-ff1a-4207-99f3-1351c3f1e507')
+ repo.delete()
+```
+
+**Return Type**
+
+None
+
+
+## Repos ##
+### Get Repo ###
+**Request Parameters**
+
+* repo_id
+
+**Sample Case**
+
+```python
+
+ import seafileapi
+
+ client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
+ repo = client.repos.get_repo('09c16e2a-ff1a-4207-99f3-1351c3f1e507')
+```
+
+**Return Type**
+
+A Repo Object
+
+**Exception**
+
+* Repo does not exist.
+
+### List Repo ###
+
+**Request Parameters**
+
+None
+
+**Sample Case**
+
+```python
+
+ import seafileapi
+
+ client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
+ repo_list = client.repos.list_repos()
+
+ print repo_list
+ Out >>> [,
+ ,
+ ,
+ ,
+ ,
+ ]
+
+ print [repo.name for repo in repo_list]
+ Out >>> ['alphabox',
+ 'hello',
+ 'Doc',
+ 'obj_test',
+ 'fs_test',
+ 'global']
+```
+
+
+**Return Type**
+
+A list of Repo Object
+
+### Create Repo ###
+
+**Request Parameters**
+
+* name
+* password (default None)
+
+**Sample Case**
+
+```python
+
+ import seafileapi
+
+ client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
+ repo = client.repos.create_repo('test_repo')
+```
+
+**Return Type**
+
+A Repo Object
+
From 954c440aa239553e6f8216265b62ff1e634c2238 Mon Sep 17 00:00:00 2001
From: zming <517046497@qq.com>
Date: Fri, 2 Feb 2018 16:48:08 +0800
Subject: [PATCH 09/39] review
---
README.md | 2 +
doc.md | 323 ++++++++++++++++++++++++------------------------------
2 files changed, 145 insertions(+), 180 deletions(-)
diff --git a/README.md b/README.md
index c710859593..c99211e64c 100644
--- a/README.md
+++ b/README.md
@@ -4,3 +4,5 @@ python-seafile
==============
python client for seafile web api
+
+Doc: https://github.com/haiwen/seahub/blob/master/doc.md
diff --git a/doc.md b/doc.md
index abf9479110..c5092bfc6d 100644
--- a/doc.md
+++ b/doc.md
@@ -3,39 +3,32 @@
@@ -43,6 +36,8 @@
# Python Seafile
+
+
## Get Client ##
**Request Parameters**
@@ -63,33 +58,33 @@
A Client Object
-## SeafFile ##
-### Get Content ###
+## Library ##
+### Get Library ###
**Request Parameters**
-None
+* repo_id
**Sample Case**
-
-
```python
import seafileapi
client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
repo = client.repos.get_repo('09c16e2a-ff1a-4207-99f3-1351c3f1e507')
- seaffile = repo.get_file('/root/test.md')
-
- content = seaffile.get_content()
```
**Return Type**
-File Content
+A Library Object
+
+**Exception**
+
+* Library does not exist.
+
+### Check Library Permission ###
-### Delete a file ###
**Request Parameters**
None
@@ -99,23 +94,21 @@ None
```python
import seafileapi
-
+
client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
repo = client.repos.get_repo('09c16e2a-ff1a-4207-99f3-1351c3f1e507')
- seaffile = repo.get_file('/root/test.md')
-
- seaffile.delete()
+ is_readonly = repo.is_readonly()
```
**Return Type**
-A Response Instance
+Boolean
+
+### List all Libraries ###
-## SeafDir ##
-### List ###
**Request Parameters**
-* force_refresh (default False)
+None
**Sample Case**
@@ -124,37 +117,35 @@ A Response Instance
import seafileapi
client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
- repo = client.repos.get_repo('09c16e2a-ff1a-4207-99f3-1351c3f1e507')
- seafdir = repo.get_dir('/root')
-
- lst = seafdir.ls(force_refresh=True)
- print lst
- Out >>> [SeafDir[repo=01ccc4,path=/Seahub/6.1.x,entries=14],
- SeafDir[repo=01ccc4,path=/Seahub/6.2.2-pro,entries=1],
- SeafDir[repo=01ccc4,path=/Seahub/6.2.3,entries=15],
- SeafDir[repo=01ccc4,path=/Seahub/6.2.x,entries=5],
- SeafFile[repo=01ccc4,path=/Seahub/.DS_Store,size=6148],
- SeafFile[repo=01ccc4,path=/Seahub/error.md,size=127],
- SeafFile[repo=01ccc4,path=/Seahub/preview-research.md,size=1030]]
+ repo_list = client.repos.list_repos()
- print [dirent.name for dirent in lst]
- Out >>> ['6.1.x',
- '6.2.2-pro',
- '6.2.3',
- '6.2.x',
- '.DS_Store',
- 'error.md',
- 'preview-research.md']
+ print repo_list
+ Out >>> [,
+ ,
+ ,
+ ,
+ ,
+ ]
+
+ print [repo.name for repo in repo_list]
+ Out >>> ['alphabox',
+ 'hello',
+ 'Doc',
+ 'obj_test',
+ 'fs_test',
+ 'global']
```
**Return Type**
-List of SeafDir and SeafFile
+A list of Libraries Object
+
+### Create Library ###
-### Create Empty File ###
**Request Parameters**
* name
+* password (default None)
**Sample Case**
@@ -163,42 +154,19 @@ List of SeafDir and SeafFile
import seafileapi
client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
- repo = client.repos.get_repo('09c16e2a-ff1a-4207-99f3-1351c3f1e507')
- seafdir = repo.get_dir('/root')
-
- new_file = seafdir.create_empty_file('tmp_file.md')
+ repo = client.repos.create_repo('test_repo')
```
**Return Type**
-A SeafFile Object of new empty file
+A Library Object
-### Create New Folder ###
-**Request Parameters**
-* name
+### Delete Library ###
-**Sample Case**
-
-```python
-
- import seafileapi
-
- client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
- repo = client.repos.get_repo('09c16e2a-ff1a-4207-99f3-1351c3f1e507')
- seafdir = repo.get_dir('/root')
-
- new_dir = seafdir.mkdir('tmp_dir')
-```
-
-**Return Type**
-
-A SeafDir Object of new dir
-
-### Upload ###
**Request Parameters**
-* fileobj
-* filename
+
+None
**Sample Case**
@@ -208,21 +176,19 @@ A SeafDir Object of new dir
client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
repo = client.repos.get_repo('09c16e2a-ff1a-4207-99f3-1351c3f1e507')
- seafdir = repo.get_dir('/root')
-
- file = seafdir.upload('this is file content', 'tmp_file.md')
+ repo.delete()
```
**Return Type**
-A SeafFile Object of upload file
+None
+## Directory ##
+### Get Directory ###
-### Upload Local File ###
**Request Parameters**
-* filepath
-* name (default None, default use local file name)
+* path
**Sample Case**
@@ -233,22 +199,27 @@ A SeafFile Object of upload file
client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
repo = client.repos.get_repo('09c16e2a-ff1a-4207-99f3-1351c3f1e507')
seafdir = repo.get_dir('/root')
-
- file = seafdir.upload_local_file('/home/ubuntu/env.md')
+ print seafdir.__dict__
+ Out >>> {'client': SeafileApiClient[server=http://127.0.0.1:8000, user=admin@admin.com],
+ 'entries': [],
+ 'id': 'c3742dd86004d51c358845fa3178c87e4ab3aa60',
+ 'path': '/root',
+ 'repo': ,
+ 'size': 0}
```
**Return Type**
-A SeafFile Object of upload file
+A Directory Object
**Exception**
-* Local file does not exist.
+* Directory does not exist.
-### delete a folder ###
+### List Directory Entries ###
**Request Parameters**
-None
+* force_refresh (default False)
**Sample Case**
@@ -260,39 +231,54 @@ None
repo = client.repos.get_repo('09c16e2a-ff1a-4207-99f3-1351c3f1e507')
seafdir = repo.get_dir('/root')
- seafdir.delete()
+ lst = seafdir.ls(force_refresh=True)
+ print lst
+ Out >>> [SeafDir[repo=01ccc4,path=/Seahub/6.1.x,entries=14],
+ SeafDir[repo=01ccc4,path=/Seahub/6.2.2-pro,entries=1],
+ SeafDir[repo=01ccc4,path=/Seahub/6.2.3,entries=15],
+ SeafDir[repo=01ccc4,path=/Seahub/6.2.x,entries=5],
+ SeafFile[repo=01ccc4,path=/Seahub/.DS_Store,size=6148],
+ SeafFile[repo=01ccc4,path=/Seahub/error.md,size=127],
+ SeafFile[repo=01ccc4,path=/Seahub/preview-research.md,size=1030]]
+
+ print [dirent.name for dirent in lst]
+ Out >>> ['6.1.x',
+ '6.2.2-pro',
+ '6.2.3',
+ '6.2.x',
+ '.DS_Store',
+ 'error.md',
+ 'preview-research.md']
```
**Return Type**
-A Response Instance
+List of Directory and File
+
-## Repo ##
-### From Json ###
+### Create New Folder ###
**Request Parameters**
-* client
-* repo_json
+* name
**Sample Case**
```python
import seafileapi
- from seafileapi.repo import Repo
client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
- repo_json = {'id': '09c16e2a-ff1a-4207-99f3-1351c3f1e507', 'name': 'test_repo', 'encrypted': False, 'permission': 'rw', 'owner': 'test@admin.com'}
- repo = Repo.from_json(client, repo_json)
-```
+ repo = client.repos.get_repo('09c16e2a-ff1a-4207-99f3-1351c3f1e507')
+ seafdir = repo.get_dir('/root')
+ new_dir = seafdir.mkdir('tmp_dir')
+```
**Return Type**
-A Repo Object
-
-### Is ReadOnly ###
+A Directory Object of new directory
+### Delete Directory ###
**Request Parameters**
None
@@ -305,14 +291,18 @@ None
client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
repo = client.repos.get_repo('09c16e2a-ff1a-4207-99f3-1351c3f1e507')
- is_readonly = repo.is_readonly()
+ seafdir = repo.get_dir('/root')
+
+ seafdir.delete()
```
**Return Type**
-True or False
+A Response Instance
+
-### Get File ###
+## File ##
+### Get File ###
**Request Parameters**
@@ -327,21 +317,28 @@ True or False
client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
repo = client.repos.get_repo('09c16e2a-ff1a-4207-99f3-1351c3f1e507')
seaffile = repo.get_file('/root/test.md')
+
+ print seafile.__dict__
+ Out >>> {'client': SeafileApiClient[server=http://127.0.0.1:8000, user=admin@admin.com],
+ 'id': '0000000000000000000000000000000000000000',
+ 'path': '/root/test.md',
+ 'repo': ,
+ 'size': 0}
```
**Return Type**
-A SeafFile Object
+A File Object
**Exception**
* File does not exist.
-### Get Dir ###
+### Get Content ###
**Request Parameters**
-* path
+None
**Sample Case**
@@ -351,22 +348,19 @@ A SeafFile Object
client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
repo = client.repos.get_repo('09c16e2a-ff1a-4207-99f3-1351c3f1e507')
- seafdir = repo.get_dir('/root')
+ seaffile = repo.get_file('/root/test.md')
+
+ content = seaffile.get_content()
```
**Return Type**
-A SeafDir Object
-
-**Exception**
-
-* Dir does not exist.
-
-### Delete Repo ###
+File Content
+### Create Empty File ###
**Request Parameters**
-None
+* name
**Sample Case**
@@ -376,19 +370,21 @@ None
client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
repo = client.repos.get_repo('09c16e2a-ff1a-4207-99f3-1351c3f1e507')
- repo.delete()
+ seafdir = repo.get_dir('/root')
+
+ new_file = seafdir.create_empty_file('tmp_file.md')
```
**Return Type**
-None
+A File Object of new empty file
-## Repos ##
-### Get Repo ###
+### Upload File ###
**Request Parameters**
-* repo_id
+* filepath
+* name (default None, default use local file name)
**Sample Case**
@@ -398,18 +394,21 @@ None
client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
repo = client.repos.get_repo('09c16e2a-ff1a-4207-99f3-1351c3f1e507')
+ seafdir = repo.get_dir('/root')
+
+ file = seafdir.upload_local_file('/home/ubuntu/env.md')
```
**Return Type**
-A Repo Object
+A File Object of upload file
**Exception**
-* Repo does not exist.
+* Local file does not exist.
-### List Repo ###
+### Delete a file ###
**Request Parameters**
None
@@ -421,48 +420,12 @@ None
import seafileapi
client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
- repo_list = client.repos.list_repos()
-
- print repo_list
- Out >>> [,
- ,
- ,
- ,
- ,
- ]
-
- print [repo.name for repo in repo_list]
- Out >>> ['alphabox',
- 'hello',
- 'Doc',
- 'obj_test',
- 'fs_test',
- 'global']
-```
-
-
-**Return Type**
-
-A list of Repo Object
-
-### Create Repo ###
-
-**Request Parameters**
-
-* name
-* password (default None)
-
-**Sample Case**
-
-```python
-
- import seafileapi
+ repo = client.repos.get_repo('09c16e2a-ff1a-4207-99f3-1351c3f1e507')
+ seaffile = repo.get_file('/root/test.md')
- client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
- repo = client.repos.create_repo('test_repo')
+ seaffile.delete()
```
**Return Type**
-A Repo Object
-
+A Response Instance
From 328266a790af42746c15b10c04606a507059ed1b Mon Sep 17 00:00:00 2001
From: zming <517046497@qq.com>
Date: Sat, 3 Feb 2018 13:37:03 +0800
Subject: [PATCH 10/39] update doc link
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index c99211e64c..8b2cea4888 100644
--- a/README.md
+++ b/README.md
@@ -5,4 +5,4 @@ python-seafile
python client for seafile web api
-Doc: https://github.com/haiwen/seahub/blob/master/doc.md
+Doc: https://github.com/haiwen/python-seafile/blob/master/doc.md
From 3dd5fd1630097919163458a843c7c36e2f7861e8 Mon Sep 17 00:00:00 2001
From: zming <517046497@qq.com>
Date: Fri, 1 Jun 2018 18:20:48 +0800
Subject: [PATCH 11/39] share to user for dir
---
seafileapi/client.py | 3 +++
seafileapi/files.py | 17 ++++++++++++++---
2 files changed, 17 insertions(+), 3 deletions(-)
diff --git a/seafileapi/client.py b/seafileapi/client.py
index 9bf79ee88f..52a6ea6b1b 100644
--- a/seafileapi/client.py
+++ b/seafileapi/client.py
@@ -43,6 +43,9 @@ def get(self, *args, **kwargs):
def post(self, *args, **kwargs):
return self._send_request('POST', *args, **kwargs)
+ def put(self, *args, **kwargs):
+ return self._send_request('PUT', *args, **kwargs)
+
def delete(self, *args, **kwargs):
return self._send_request('delete', *args, **kwargs)
diff --git a/seafileapi/files.py b/seafileapi/files.py
index d36f2ea5c6..2c3144d148 100644
--- a/seafileapi/files.py
+++ b/seafileapi/files.py
@@ -76,7 +76,7 @@ def copyTo(self, dst_dir, dst_repo_id=None):
"""
if dst_repo_id is None:
dst_repo_id = self.repo.id
-
+
dirent_type = 'dir' if self.isdir else 'file'
resp = self._copy_move_task('copy', dirent_type, dst_dir, dst_repo_id)
return resp.status_code == 200
@@ -86,7 +86,7 @@ def moveTo(self, dst_dir, dst_repo_id=None):
"""
if dst_repo_id is None:
dst_repo_id = self.repo.id
-
+
dirent_type = 'dir' if self.isdir else 'file'
resp = self._copy_move_task('move', dirent_type, dst_dir, dst_repo_id)
succeeded = resp.status_code == 200
@@ -122,6 +122,16 @@ def ls(self, force_refresh=False):
return self.entries
+ def share_to_user(self, email, permission):
+ url = '/api2/repos/%s/dir/shared_items/' % self.repo.id + querystr(p=self.path)
+ putdata = {
+ 'share_type': 'user',
+ 'username': email,
+ 'permission': permission
+ }
+ resp = self.client.put(url, data=putdata)
+ return resp.status_code == 200
+
def create_empty_file(self, name):
"""Create a new empty file in this dir.
Return a :class:`SeafFile` object of the newly created file.
@@ -210,7 +220,7 @@ def num_entries(self):
if self.entries is None:
self.load_entries()
return len(self.entries) if self.entries is not None else 0
-
+
def __str__(self):
return 'SeafDir[repo=%s,path=%s,entries=%s]' % \
(self.repo.id[:6], self.path, self.num_entries)
@@ -219,6 +229,7 @@ def __str__(self):
class SeafFile(_SeafDirentBase):
isdir = False
+
def update(self, fileobj):
"""Update the content of this file"""
pass
From d3480ba03f1c1b7138314415c4cb26c25a690991 Mon Sep 17 00:00:00 2001
From: sniper-py
Date: Wed, 25 Sep 2019 17:42:59 +0800
Subject: [PATCH 12/39] update ignore
---
.gitignore | 2 ++
1 file changed, 2 insertions(+)
diff --git a/.gitignore b/.gitignore
index db4561eaa1..66b01cd072 100644
--- a/.gitignore
+++ b/.gitignore
@@ -52,3 +52,5 @@ docs/_build/
# PyBuilder
target/
+
+.idea/
From b4b077e53a48ceccfedd4b96dca419e7f2bbcfbf Mon Sep 17 00:00:00 2001
From: sniper-py
Date: Wed, 25 Sep 2019 17:44:18 +0800
Subject: [PATCH 13/39] 2to3 dict
---
seafileapi/files.py | 4 ++--
seafileapi/utils.py | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/seafileapi/files.py b/seafileapi/files.py
index 2c3144d148..ea34559fa0 100644
--- a/seafileapi/files.py
+++ b/seafileapi/files.py
@@ -52,7 +52,7 @@ def rename(self, newname):
new_dirent = self.repo.get_dir(os.path.join(os.path.dirname(self.path), newname))
else:
new_dirent = self.repo.get_file(os.path.join(os.path.dirname(self.path), newname))
- for key in self.__dict__.keys():
+ for key in list(self.__dict__.keys()):
self.__dict__[key] = new_dirent.__dict__[key]
return succeeded
@@ -97,7 +97,7 @@ def moveTo(self, dst_dir, dst_repo_id=None):
new_dirent = new_repo.get_dir(dst_path)
else:
new_dirent = new_repo.get_file(dst_path)
- for key in self.__dict__.keys():
+ for key in list(self.__dict__.keys()):
self.__dict__[key] = new_dirent.__dict__[key]
return succeeded
diff --git a/seafileapi/utils.py b/seafileapi/utils.py
index b529880f15..5f863edad5 100644
--- a/seafileapi/utils.py
+++ b/seafileapi/utils.py
@@ -46,7 +46,7 @@ def querystr(**kwargs):
def utf8lize(obj):
if isinstance(obj, dict):
- return {k: to_utf8(v) for k, v in obj.iteritems()}
+ return {k: to_utf8(v) for k, v in obj.items()}
if isinstance(obj, list):
return [to_utf8(x) for x in ob]
From bc69e77ce76355bf1f68ca5b3889c5e8a20d1b25 Mon Sep 17 00:00:00 2001
From: sniper-py
Date: Wed, 25 Sep 2019 17:44:18 +0800
Subject: [PATCH 14/39] 2to3 except
---
seafileapi/utils.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/seafileapi/utils.py b/seafileapi/utils.py
index 5f863edad5..ad7812ed98 100644
--- a/seafileapi/utils.py
+++ b/seafileapi/utils.py
@@ -28,7 +28,7 @@ def decorator(func):
def wrapped(*args, **kwargs):
try:
return func(*args, **kwargs)
- except ClientHttpError, e:
+ except ClientHttpError as e:
if e.code == 404:
raise DoesNotExist(msg)
else:
From e29d92be4f4b04749232fd924e884068dfdd16a7 Mon Sep 17 00:00:00 2001
From: sniper-py
Date: Wed, 25 Sep 2019 17:44:30 +0800
Subject: [PATCH 15/39] 2to3 unicode
---
seafileapi/utils.py | 4 ++--
tests/test_files.py | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/seafileapi/utils.py b/seafileapi/utils.py
index ad7812ed98..7d52e60fe3 100644
--- a/seafileapi/utils.py
+++ b/seafileapi/utils.py
@@ -37,7 +37,7 @@ def wrapped(*args, **kwargs):
return decorator
def to_utf8(obj):
- if isinstance(obj, unicode):
+ if isinstance(obj, str):
return obj.encode('utf-8')
return obj
@@ -51,7 +51,7 @@ def utf8lize(obj):
if isinstance(obj, list):
return [to_utf8(x) for x in ob]
- if instance(obj, unicode):
+ if instance(obj, str):
return obj.encode('utf-8')
return obj
diff --git a/tests/test_files.py b/tests/test_files.py
index 5509da09bc..111a521372 100644
--- a/tests/test_files.py
+++ b/tests/test_files.py
@@ -84,7 +84,7 @@ def test_upload_file(repo, parentpath):
def test_upload_string_as_file_content(repo):
# test pass as string as file content when upload file
rootdir = repo.get_dir('/')
- fname = u'testfile-%s' % randstring()
+ fname = 'testfile-%s' % randstring()
fcontent = 'line 1\nline 2\n\r'
f = rootdir.upload(fcontent, fname)
assert f.name == fname
From 032545eff3783bd357037edaf918b007d5af922c Mon Sep 17 00:00:00 2001
From: sniper-py
Date: Wed, 25 Sep 2019 17:44:30 +0800
Subject: [PATCH 16/39] 2to3 urllib
---
seafileapi/repo.py | 2 +-
seafileapi/utils.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/seafileapi/repo.py b/seafileapi/repo.py
index ded895e551..312ba42274 100644
--- a/seafileapi/repo.py
+++ b/seafileapi/repo.py
@@ -1,4 +1,4 @@
-from urllib import urlencode
+from urllib.parse import urlencode
from seafileapi.utils import utf8lize
from seafileapi.files import SeafDir, SeafFile
from seafileapi.utils import raise_does_not_exist
diff --git a/seafileapi/utils.py b/seafileapi/utils.py
index 7d52e60fe3..7903414315 100644
--- a/seafileapi/utils.py
+++ b/seafileapi/utils.py
@@ -1,7 +1,7 @@
import string
import random
from functools import wraps
-from urllib import urlencode
+from urllib.parse import urlencode
from seafileapi.exceptions import ClientHttpError, DoesNotExist
def randstring(length=0):
From 209d7f598f19ca114901137e016726899093b1c9 Mon Sep 17 00:00:00 2001
From: sniper-py
Date: Wed, 25 Sep 2019 18:24:34 +0800
Subject: [PATCH 17/39] remove utf8lize
---
seafileapi/files.py | 3 +--
seafileapi/repo.py | 2 --
2 files changed, 1 insertion(+), 4 deletions(-)
diff --git a/seafileapi/files.py b/seafileapi/files.py
index ea34559fa0..ed01e64543 100644
--- a/seafileapi/files.py
+++ b/seafileapi/files.py
@@ -2,7 +2,7 @@
import os
import posixpath
import re
-from seafileapi.utils import querystr, utf8lize
+from seafileapi.utils import querystr
ZERO_OBJ_ID = '0000000000000000000000000000000000000000'
@@ -208,7 +208,6 @@ def load_entries(self, dirents_json=None):
self.entries = [self._load_dirent(entry_json) for entry_json in dirents_json]
def _load_dirent(self, dirent_json):
- dirent_json = utf8lize(dirent_json)
path = posixpath.join(self.path, dirent_json['name'])
if dirent_json['type'] == 'file':
return SeafFile(self.repo, path, dirent_json['id'], dirent_json['size'])
diff --git a/seafileapi/repo.py b/seafileapi/repo.py
index 312ba42274..01811a2db0 100644
--- a/seafileapi/repo.py
+++ b/seafileapi/repo.py
@@ -1,5 +1,4 @@
from urllib.parse import urlencode
-from seafileapi.utils import utf8lize
from seafileapi.files import SeafDir, SeafFile
from seafileapi.utils import raise_does_not_exist
@@ -18,7 +17,6 @@ def __init__(self, client, repo_id, repo_name,
@classmethod
def from_json(cls, client, repo_json):
- repo_json = utf8lize(repo_json)
repo_id = repo_json['id']
repo_name = repo_json['name']
From 528b528f6cf8e986e42f013005586142dc6fafcc Mon Sep 17 00:00:00 2001
From: Shailesh Appukuttan
Date: Wed, 15 Jul 2020 16:28:47 +0200
Subject: [PATCH 18/39] Make changes to source repo to work with HBP seafile
---
README.md | 18 +++-
hbp_seafile/__init__.py | 14 +++
{seafileapi => hbp_seafile}/admin.py | 0
hbp_seafile/client.py | 118 ++++++++++++++++++++++
{seafileapi => hbp_seafile}/exceptions.py | 0
{seafileapi => hbp_seafile}/files.py | 0
{seafileapi => hbp_seafile}/group.py | 0
{seafileapi => hbp_seafile}/repo.py | 0
{seafileapi => hbp_seafile}/repos.py | 0
{seafileapi => hbp_seafile}/utils.py | 0
requirements.txt | 1 +
seafileapi/__init__.py | 5 -
seafileapi/client.py | 77 --------------
setup.py | 14 +--
14 files changed, 153 insertions(+), 94 deletions(-)
create mode 100644 hbp_seafile/__init__.py
rename {seafileapi => hbp_seafile}/admin.py (100%)
create mode 100644 hbp_seafile/client.py
rename {seafileapi => hbp_seafile}/exceptions.py (100%)
rename {seafileapi => hbp_seafile}/files.py (100%)
rename {seafileapi => hbp_seafile}/group.py (100%)
rename {seafileapi => hbp_seafile}/repo.py (100%)
rename {seafileapi => hbp_seafile}/repos.py (100%)
rename {seafileapi => hbp_seafile}/utils.py (100%)
delete mode 100644 seafileapi/__init__.py
delete mode 100644 seafileapi/client.py
diff --git a/README.md b/README.md
index 8b2cea4888..c687aab1e9 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,16 @@
-[![Build Status](https://secure.travis-ci.org/haiwen/python-seafile.svg?branch=master)](http://travis-ci.org/haiwen/python-seafile)
-
-python-seafile
+hbp_seafile
==============
-python client for seafile web api
+Python client interface for HBP Collaboratory Seafile storage
+
+
+Original implementation source:
+https://github.com/haiwen/python-seafile
+by Shuai Lin (linshuai2012@gmail.com)
+
+
+Updated for integration with HBP v2 Collaboratory's Seafile storage
+by Shailesh Appukuttan (appukuttan.shailesh@gmail.com)
+
-Doc: https://github.com/haiwen/python-seafile/blob/master/doc.md
+Documentation: https://github.com/haiwen/python-seafile/blob/master/doc.md
diff --git a/hbp_seafile/__init__.py b/hbp_seafile/__init__.py
new file mode 100644
index 0000000000..461f418d40
--- /dev/null
+++ b/hbp_seafile/__init__.py
@@ -0,0 +1,14 @@
+"""
+A Python package for working with the Human Brain Project Model Validation Framework.
+
+Andrew Davison and Shailesh Appukuttan, CNRS, 2017-2020
+
+License: BSD 3-clause, see LICENSE.txt
+
+"""
+
+from seafileapi.client import SeafileApiClient
+
+def connect(server, username, password):
+ client = SeafileApiClient(server, username, password)
+ return client
diff --git a/seafileapi/admin.py b/hbp_seafile/admin.py
similarity index 100%
rename from seafileapi/admin.py
rename to hbp_seafile/admin.py
diff --git a/hbp_seafile/client.py b/hbp_seafile/client.py
new file mode 100644
index 0000000000..5dd5c5516f
--- /dev/null
+++ b/hbp_seafile/client.py
@@ -0,0 +1,118 @@
+import requests
+from seafileapi.utils import urljoin
+from seafileapi.exceptions import ClientHttpError
+from seafileapi.repos import Repos
+import re
+
+class SeafileApiClient(object):
+ """Wraps seafile web api"""
+ def __init__(self, server="https://drive.ebrains.eu", username=None, password=None, token=None):
+ """Wraps various basic operations to interact with seahub http api.
+ """
+ self.server = server
+ self.username = username
+ self.password = password
+ self._token = token
+
+ self.repos = Repos(self)
+ self.groups = Groups(self)
+
+ if token is None:
+ self._get_token()
+
+ def _check_token_valid(self):
+ url = "https://drive.ebrains.eu/api2/auth/ping/"
+ data = requests.get(url, auth=HBPAuth(self.token), verify=self.verify)
+ if data.status_code == 200:
+ return True
+ else:
+ return False
+
+ def _get_token(self):
+ """
+ HBP authentication based on _hbp_auth() in
+ https://github.com/HumanBrainProject/hbp-validation-client)
+ """
+ base_url = "https://validation-v2.brainsimulation.eu"
+ redirect_uri = base_url + '/auth'
+ session = requests.Session()
+ # log-in page of model validation service
+ r_login = session.get(base_url + "/login", allow_redirects=False)
+ if r_login.status_code != 302:
+ raise Exception(
+ "Something went wrong. Status code {} from login, expected 302"
+ .format(r_login.status_code))
+ # redirects to EBRAINS IAM log-in page
+ iam_auth_url = r_login.headers.get('location')
+ r_iam1 = session.get(iam_auth_url, allow_redirects=False)
+ if r_iam1.status_code != 200:
+ raise Exception(
+ "Something went wrong loading EBRAINS log-in page. Status code {}"
+ .format(r_iam1.status_code))
+ # fill-in and submit form
+ match = re.search(r'action=\"(?P[^\"]+)\"', r_iam1.text)
+ if not match:
+ raise Exception("Received an unexpected page")
+ iam_authenticate_url = match['url'].replace("&", "&")
+ r_iam2 = session.post(
+ iam_authenticate_url,
+ data={"username": self.username, "password": self.password},
+ headers={"Referer": iam_auth_url, "Host": "iam.ebrains.eu", "Origin": "https://iam.ebrains.eu"},
+ allow_redirects=False
+ )
+ if r_iam2.status_code != 302:
+ raise Exception(
+ "Something went wrong. Status code {} from authenticate, expected 302"
+ .format(r_iam2.status_code))
+ # redirects back to model validation service
+ r_val = session.get(r_iam2.headers['Location'])
+ if r_val.status_code != 200:
+ raise Exception(
+ "Something went wrong. Status code {} from final authentication step"
+ .format(r_val.status_code))
+ config = r_val.json()
+ self._token = config['token']['access_token']
+
+ def __str__(self):
+ return 'SeafileApiClient[server=%s, user=%s]' % (self.server, self.username)
+
+ __repr__ = __str__
+
+ def get(self, *args, **kwargs):
+ return self._send_request('GET', *args, **kwargs)
+
+ def post(self, *args, **kwargs):
+ return self._send_request('POST', *args, **kwargs)
+
+ def put(self, *args, **kwargs):
+ return self._send_request('PUT', *args, **kwargs)
+
+ def delete(self, *args, **kwargs):
+ return self._send_request('delete', *args, **kwargs)
+
+ def _send_request(self, method, url, *args, **kwargs):
+ if not url.startswith('http'):
+ url = urljoin(self.server, url)
+
+ headers = kwargs.get('headers', {})
+ headers.setdefault('Authorization', 'Bearer ' + self._token)
+ kwargs['headers'] = headers
+
+ expected = kwargs.pop('expected', 200)
+ if not hasattr(expected, '__iter__'):
+ expected = (expected, )
+ resp = requests.request(method, url, *args, **kwargs)
+ if resp.status_code not in expected:
+ msg = 'Expected %s, but get %s' % \
+ (' or '.join(map(str, expected)), resp.status_code)
+ raise ClientHttpError(resp.status_code, msg)
+
+ return resp
+
+
+class Groups(object):
+ def __init__(self, client):
+ pass
+
+ def create_group(self, name):
+ pass
diff --git a/seafileapi/exceptions.py b/hbp_seafile/exceptions.py
similarity index 100%
rename from seafileapi/exceptions.py
rename to hbp_seafile/exceptions.py
diff --git a/seafileapi/files.py b/hbp_seafile/files.py
similarity index 100%
rename from seafileapi/files.py
rename to hbp_seafile/files.py
diff --git a/seafileapi/group.py b/hbp_seafile/group.py
similarity index 100%
rename from seafileapi/group.py
rename to hbp_seafile/group.py
diff --git a/seafileapi/repo.py b/hbp_seafile/repo.py
similarity index 100%
rename from seafileapi/repo.py
rename to hbp_seafile/repo.py
diff --git a/seafileapi/repos.py b/hbp_seafile/repos.py
similarity index 100%
rename from seafileapi/repos.py
rename to hbp_seafile/repos.py
diff --git a/seafileapi/utils.py b/hbp_seafile/utils.py
similarity index 100%
rename from seafileapi/utils.py
rename to hbp_seafile/utils.py
diff --git a/requirements.txt b/requirements.txt
index f2293605cf..a6de386366 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1 +1,2 @@
requests
+re
diff --git a/seafileapi/__init__.py b/seafileapi/__init__.py
deleted file mode 100644
index d6c3b8dfcf..0000000000
--- a/seafileapi/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from seafileapi.client import SeafileApiClient
-
-def connect(server, username, password):
- client = SeafileApiClient(server, username, password)
- return client
diff --git a/seafileapi/client.py b/seafileapi/client.py
deleted file mode 100644
index 52a6ea6b1b..0000000000
--- a/seafileapi/client.py
+++ /dev/null
@@ -1,77 +0,0 @@
-import requests
-from seafileapi.utils import urljoin
-from seafileapi.exceptions import ClientHttpError
-from seafileapi.repos import Repos
-
-class SeafileApiClient(object):
- """Wraps seafile web api"""
- def __init__(self, server, username=None, password=None, token=None):
- """Wraps various basic operations to interact with seahub http api.
- """
- self.server = server
- self.username = username
- self.password = password
- self._token = token
-
- self.repos = Repos(self)
- self.groups = Groups(self)
-
- if token is None:
- self._get_token()
-
- def _get_token(self):
- data = {
- 'username': self.username,
- 'password': self.password,
- }
- url = urljoin(self.server, '/api2/auth-token/')
- res = requests.post(url, data=data)
- if res.status_code != 200:
- raise ClientHttpError(res.status_code, res.content)
- token = res.json()['token']
- assert len(token) == 40, 'The length of seahub api auth token should be 40'
- self._token = token
-
- def __str__(self):
- return 'SeafileApiClient[server=%s, user=%s]' % (self.server, self.username)
-
- __repr__ = __str__
-
- def get(self, *args, **kwargs):
- return self._send_request('GET', *args, **kwargs)
-
- def post(self, *args, **kwargs):
- return self._send_request('POST', *args, **kwargs)
-
- def put(self, *args, **kwargs):
- return self._send_request('PUT', *args, **kwargs)
-
- def delete(self, *args, **kwargs):
- return self._send_request('delete', *args, **kwargs)
-
- def _send_request(self, method, url, *args, **kwargs):
- if not url.startswith('http'):
- url = urljoin(self.server, url)
-
- headers = kwargs.get('headers', {})
- headers.setdefault('Authorization', 'Token ' + self._token)
- kwargs['headers'] = headers
-
- expected = kwargs.pop('expected', 200)
- if not hasattr(expected, '__iter__'):
- expected = (expected, )
- resp = requests.request(method, url, *args, **kwargs)
- if resp.status_code not in expected:
- msg = 'Expected %s, but get %s' % \
- (' or '.join(map(str, expected)), resp.status_code)
- raise ClientHttpError(resp.status_code, msg)
-
- return resp
-
-
-class Groups(object):
- def __init__(self, client):
- pass
-
- def create_group(self, name):
- pass
diff --git a/setup.py b/setup.py
index d2165df374..0e7ea84e90 100644
--- a/setup.py
+++ b/setup.py
@@ -1,18 +1,18 @@
from setuptools import setup, find_packages
-__version__ = '0.1.1'
+__version__ = '0.1'
-setup(name='seafileapi',
+setup(name='hbp_seafile',
version=__version__,
- license='BSD',
- description='Client interface for Seafile Web API',
- author='Shuai Lin',
- author_email='linshuai2012@gmail.com',
+ license='Apache-2.0 License',
+ description='Python client interface for HBP Collaboratory Seafile storage',
+ author='Shuai Lin, Shailesh Appukuttan',
+ author_email='linshuai2012@gmail.com, appukuttan.shailesh@gmail.com',
url='http://seafile.com',
platforms=['Any'],
packages=find_packages(),
- install_requires=['requests'],
+ install_requires=['requests', 're'],
classifiers=['Development Status :: 4 - Beta',
'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent',
From a2e30b8d44e3a358f16005104102a88caf642f34 Mon Sep 17 00:00:00 2001
From: Shailesh Appukuttan
Date: Wed, 15 Jul 2020 16:32:46 +0200
Subject: [PATCH 19/39] update requirements
---
requirements.txt | 1 -
setup.py | 2 +-
2 files changed, 1 insertion(+), 2 deletions(-)
diff --git a/requirements.txt b/requirements.txt
index a6de386366..f2293605cf 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1 @@
requests
-re
diff --git a/setup.py b/setup.py
index 0e7ea84e90..4a586a234b 100644
--- a/setup.py
+++ b/setup.py
@@ -12,7 +12,7 @@
url='http://seafile.com',
platforms=['Any'],
packages=find_packages(),
- install_requires=['requests', 're'],
+ install_requires=['requests'],
classifiers=['Development Status :: 4 - Beta',
'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent',
From 3204471915e30b32219042268403ca13a08b1cc2 Mon Sep 17 00:00:00 2001
From: Shailesh Appukuttan
Date: Wed, 15 Jul 2020 17:33:39 +0200
Subject: [PATCH 20/39] changes to conform to new package name
---
README.md | 27 +++++++++++++-
doc.md | 76 ++++++++++++++++++++--------------------
eu_logo.jpg | Bin 0 -> 132699 bytes
hbp_seafile/__init__.py | 8 ++---
hbp_seafile/client.py | 8 ++---
hbp_seafile/files.py | 2 +-
hbp_seafile/repo.py | 4 +--
hbp_seafile/repos.py | 4 +--
hbp_seafile/utils.py | 2 +-
pytest.ini | 2 +-
tests/fixtures.py | 4 +--
tests/test_repos.py | 2 +-
12 files changed, 82 insertions(+), 57 deletions(-)
create mode 100644 eu_logo.jpg
diff --git a/README.md b/README.md
index c687aab1e9..e17f2ceab6 100644
--- a/README.md
+++ b/README.md
@@ -13,4 +13,29 @@ Updated for integration with HBP v2 Collaboratory's Seafile storage
by Shailesh Appukuttan (appukuttan.shailesh@gmail.com)
-Documentation: https://github.com/haiwen/python-seafile/blob/master/doc.md
+Documentation: https://github.com/appukuttan-shailesh/hbp-seafile/blob/master/doc.md
+
+Installation: `pip install hbp_seafile`
+
+
+Example usage (refer to docs for more):
+
+```python
+ # 1. import module
+ import hbp_seafile
+
+ # 2. create client object
+ # 2.1 either via
+ client = hbp_seafile.connect('hbp_username', 'password')
+ # 2.2 or via
+ from hbp_seafile.client import SeafileApiClient
+ client = SeafileApiClient(username="hbp_username", password="password")
+
+
+```
+
+
+
+
+### ACKNOWLEDGEMENTS
+This open source software code was developed in part in the Human Brain Project, funded from the European Union's Horizon 2020 Framework Programme for Research and Innovation under Specific Grant Agreements No. 720270 and No. 785907 (Human Brain Project SGA1 and SGA2).
\ No newline at end of file
diff --git a/doc.md b/doc.md
index c5092bfc6d..dfcb011893 100644
--- a/doc.md
+++ b/doc.md
@@ -49,9 +49,9 @@
```python
- import seafileapi
+ import hbp_seafile
- client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
+ client = hbp_seafile.connect('hbp_username', 'password')
```
**Return Type**
@@ -69,9 +69,9 @@ A Client Object
```python
- import seafileapi
+ import hbp_seafile
- client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
+ client = hbp_seafile.connect('hbp_username', 'password')
repo = client.repos.get_repo('09c16e2a-ff1a-4207-99f3-1351c3f1e507')
```
@@ -93,9 +93,9 @@ None
```python
- import seafileapi
+ import hbp_seafile
- client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
+ client = hbp_seafile.connect('hbp_username', 'password')
repo = client.repos.get_repo('09c16e2a-ff1a-4207-99f3-1351c3f1e507')
is_readonly = repo.is_readonly()
```
@@ -114,18 +114,18 @@ None
```python
- import seafileapi
+ import hbp_seafile
- client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
+ client = hbp_seafile.connect('hbp_username', 'password')
repo_list = client.repos.list_repos()
print repo_list
- Out >>> [,
- ,
- ,
- ,
- ,
- ]
+ Out >>> [,
+ ,
+ ,
+ ,
+ ,
+ ]
print [repo.name for repo in repo_list]
Out >>> ['alphabox',
@@ -151,9 +151,9 @@ A list of Libraries Object
```python
- import seafileapi
+ import hbp_seafile
- client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
+ client = hbp_seafile.connect('hbp_username', 'password')
repo = client.repos.create_repo('test_repo')
```
@@ -172,9 +172,9 @@ None
```python
- import seafileapi
+ import hbp_seafile
- client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
+ client = hbp_seafile.connect('hbp_username', 'password')
repo = client.repos.get_repo('09c16e2a-ff1a-4207-99f3-1351c3f1e507')
repo.delete()
```
@@ -194,9 +194,9 @@ None
```python
- import seafileapi
+ import hbp_seafile
- client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
+ client = hbp_seafile.connect('hbp_username', 'password')
repo = client.repos.get_repo('09c16e2a-ff1a-4207-99f3-1351c3f1e507')
seafdir = repo.get_dir('/root')
print seafdir.__dict__
@@ -204,7 +204,7 @@ None
'entries': [],
'id': 'c3742dd86004d51c358845fa3178c87e4ab3aa60',
'path': '/root',
- 'repo': ,
+ 'repo': ,
'size': 0}
```
@@ -225,9 +225,9 @@ A Directory Object
```python
- import seafileapi
+ import hbp_seafile
- client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
+ client = hbp_seafile.connect('hbp_username', 'password')
repo = client.repos.get_repo('09c16e2a-ff1a-4207-99f3-1351c3f1e507')
seafdir = repo.get_dir('/root')
@@ -265,9 +265,9 @@ List of Directory and File
```python
- import seafileapi
+ import hbp_seafile
- client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
+ client = hbp_seafile.connect('hbp_username', 'password')
repo = client.repos.get_repo('09c16e2a-ff1a-4207-99f3-1351c3f1e507')
seafdir = repo.get_dir('/root')
@@ -287,9 +287,9 @@ None
```python
- import seafileapi
+ import hbp_seafile
- client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
+ client = hbp_seafile.connect('hbp_username', 'password')
repo = client.repos.get_repo('09c16e2a-ff1a-4207-99f3-1351c3f1e507')
seafdir = repo.get_dir('/root')
@@ -312,9 +312,9 @@ A Response Instance
```python
- import seafileapi
+ import hbp_seafile
- client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
+ client = hbp_seafile.connect('hbp_username', 'password')
repo = client.repos.get_repo('09c16e2a-ff1a-4207-99f3-1351c3f1e507')
seaffile = repo.get_file('/root/test.md')
@@ -322,7 +322,7 @@ A Response Instance
Out >>> {'client': SeafileApiClient[server=http://127.0.0.1:8000, user=admin@admin.com],
'id': '0000000000000000000000000000000000000000',
'path': '/root/test.md',
- 'repo': ,
+ 'repo': ,
'size': 0}
```
@@ -344,9 +344,9 @@ None
```python
- import seafileapi
+ import hbp_seafile
- client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
+ client = hbp_seafile.connect('hbp_username', 'password')
repo = client.repos.get_repo('09c16e2a-ff1a-4207-99f3-1351c3f1e507')
seaffile = repo.get_file('/root/test.md')
@@ -366,9 +366,9 @@ File Content
```python
- import seafileapi
+ import hbp_seafile
- client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
+ client = hbp_seafile.connect('hbp_username', 'password')
repo = client.repos.get_repo('09c16e2a-ff1a-4207-99f3-1351c3f1e507')
seafdir = repo.get_dir('/root')
@@ -390,9 +390,9 @@ A File Object of new empty file
```python
- import seafileapi
+ import hbp_seafile
- client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
+ client = hbp_seafile.connect('hbp_username', 'password')
repo = client.repos.get_repo('09c16e2a-ff1a-4207-99f3-1351c3f1e507')
seafdir = repo.get_dir('/root')
@@ -417,9 +417,9 @@ None
```python
- import seafileapi
+ import hbp_seafile
- client = seafileapi.connect('http://127.0.0.1:8000', 'test@admin.com', 'password')
+ client = hbp_seafile.connect('hbp_username', 'password')
repo = client.repos.get_repo('09c16e2a-ff1a-4207-99f3-1351c3f1e507')
seaffile = repo.get_file('/root/test.md')
diff --git a/eu_logo.jpg b/eu_logo.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..ac937356d334139276e1f22a22744f99bbfeacc1
GIT binary patch
literal 132699
zcmeFa2Urx#(l9*BlA{C_BukQ0B_$=@!G)6UTM_Dirvlt-fSMR^2A)G9CjmTa2$ULv8wR^gimQhx
zfm{+m4v+6$hJuQMPe4dSOhQTqs>7c%ApjoqkIW+g1Rny$!^a~aA|xh&l8b|x)KGkO
z8c_meJ!{&N?$^Z#=^~N~&T?=b)qi6ne%j+khYAr_e7O#yxICs$4p1
z>v{SM7vTW|MBEZ@J3%I19Jf1XDCq_J{DX!6nggDnEZ_zJa_C{3)PNE&MWe}8GoHyA
zsd-+LGg3cNQ@iI%Ci+;C&a&r;<#(;*o`ZWagJ#DFoNJn2Y>H?qaVvd7@96d@CI_K|
z(9GREn*kJ5?vBp690ifk}KT3iMF_)S%y7*<9Xr5
z$2uDHiSIOd6&~8&XX)&tG*dm2@~kGARFg9Thaq9ZO8>&Uo!y
zn(&O$XSw8gO(P2SK)WeY`b#@!JGp3sDUa~*Eh4(fxccU^qQ~s6`aL!Ye+z#-Bi15{
z1C%=4oEz8rtMhGfz-JsVmp3Dx>~(%Wub`CtVS()hlY(phGrqBiVXI}OSr_|u-F*r-
zvmSTY1A+}%fy^N-ZAEmwFWeY~K&r&rTgFzj2s`FYE18X&M$F14CC_4?p&dS&j?zxb
zgxmW%+?0P6rattDHkQ(q>VlH%bhu1_N88TIt}BOYO#4GR2c}0fVqf8F`xsh%%*6!@
z_fPY4yA7YlFzRa~y}hG5WY1@lOi#@Y^FDsGTbWy2)yZVm?6f0iX2CRS)JHTpSJc0<
zcXGPn2942no)(crsI)j#Uhgri?wzm|QA?xJjMB`PSL4$>q4sXcC;6LkK-S2DAKlc3
z)>=kMpP}C|BbR53l(d^ZL_t)_*z8ZEHbo!(!NH3~({h{~5VO#9hN?WVHDB>$bm+`#
z*U8^;W;dG~R@;B(xh8s3P1!oHl8L8lm~qio7v0^zh~b`|T5F8j^mA-xaG#)GwJU?Z
zBVM(Y5u&|Av_0B`0}3~?SxlY|`Pq{G
zMY3RBc5hKlLV^VDYL|7g5{dh5@cyJyV9y|a+cX4<)&Tl$f&
zBVWwiA%FWrX}0+;*sXXRpgu3;&Kb2Wq|cNtOisUWhdz&;8mhD~Ilf)kvYUznbP$1^
zZI32n=e#QOh3COr-^ipJ?8{5-fVj=-g7Q>qHfoM{`9pmuN1tk<46l`bFD1n)ZARJa
zNaZkR=i?=5Jv%XmXlJvPPVvzlR%}401)fiN`;9eA;50&<;)F!}zO50eexHF7#pK@X
zaGboX`aMdwSb1p9D%Jk^iN)`Ih`((liF)rlg1!4x3|H0+%T7uM1x$<7NN`9(~B)C?cOXRsw7&p>aoojOcQu2n=j!L1&K)bQvMyUHIuPc;t
zi=`9WgWkr4r8r>BDZQRZ%+DnW$uY_i0)+EVd=1`Iq+}6N^8CtB?&>_uveH!EhhW{&
z+*#=vN=))r(|RB=UeWmUiVI}fQ7*yKYfnlhVKe7kW}j?HkmaX-ghN`arTx>82PS9a
z7D|(bhilq4Dsce5!R(ugU4fil^|U*4JBdnZ?zshfCOVU8iA;IoAKsIrne@Bv^zrVE
z(gnLDP8PP`+(?>IX#Z&0sz%eoe$4_Iv#02X^0}QCsmY&<9JO|`&@at-1OQ{JIb!V
z+%;Y+sYt(&o^HA6{^YXd5~taK*-{S2x}|M(9%_x@tCE7Ak!*;LpC1}#Y?UJHlQBPi
zCZGiET$vH>pS_=u&qx^^J%^|TUJ>tn?QW9_2!&zGXrxS99#687tR
zOLB`f;jUlb??_2hW^X7h1gEU0@v~r=EN$-(oxv|_3VD!oVt>NCqyW>K^s(v1Ew9lHy1G|8Oy|^
zE!AK)zo$&GR9wVQOBUZ!fBqa+=VqgL#Dm6n0iNpNR3qZWcQ5h@O;cw9nS&$C?2%;!Sv~Q|XdC?!oU|noy)Zn>bY<<7
zXSKS~X@b{q=Oio^CGPW4r{$(py$pou&T_ve;IQ=c)uyfR%&QNqSs2*Tm(NZ2h3Cs~
zKv~>mW6L^zeZZrTAq%kyr5EhjJWTKA6&nXRMpl9nOSj|wF*7pnMv{g12mIiD#PZ)^L`Xzt@J
zrGAvu!`%cb(Z_a8yw~4Sb|jy`0Y^bSDy%~S9el1s8|BD%OXbJUK^}DJd)_yb?ZDJMFgqcK<+u;?D1Q_Hl&%5a
zYNR;C9hiVDhm;I|PD9rZ(x2
zHuYV&2g3g|M#s|u_9qOXwTqX5wZorql(sOi9L(2C(^JDhN885L)#ac<;y=)TA||=&
z>fx{K;^^>)q^Q1iUgHlmSdz#NW^avj@d7~-Z;rtBb40FT$n%
zR{vS`9~oc~;DRgSK-MH)u5P+WFHhL7ibLuGD$+k;$!%P{K!y4zJQ+B|SO1ax@UA>{
z&=5clo)D$gL-^ZWcHoTrroxA_5&i)MKye4W9Ej~Xc!Dl2$QS^;2Vv0NMGrcwe!I7R
zySJt{f4jGSySIM3w|={~e!I7RySIM3w|={~e!I7RySIM3xBkC%Z+)}T*g-1{04{+i
zwT{onj6M8JV}25bR0
zejIz%%gaqxSQz0cWPK2bA!O?c7xuMw6BZQ`5eDRyeBG>Vonc;VHZai3pun?ISZsxG0lVaX!NAtv*;dAmM@f-X-dENa?goc>S+n`VT@aqKz6v~t
zmCJ(g0a}=c?U2OFS%Jqy=OUXIXuCT*da;3CJwY*P0XBONYdFlu)x()hR7jEstkA>G
zURGaK?R#}#N`dD)-#$J*LO$X`t{x7;qB1fv!XjeAVq$_Ig`lS&!pquM5aG!Iwsy#d
zqsfnAf9kHI$zgB7hDg~Cn^J{&+Il#;c{#cw*bbVu27N+a3Oq=pqn)g%wY9Z~xQM8r
z#A!PbK~YgVJ3$*8QJ5f1#^$u>X%P`ok<(Hadf8FRo&L7RPvB@7KWeG5uLaQc~*IBERH-
zG7vuK^e;mA-yj&bU&ioHg8^oMVi6YxN8^8nkv{^D{(6|){>c7itbX8RH5@&?Ts{00
z<%Rzl_dmnFm!tiEpXn&F#A{@OG#lG|K59t3H(SftvUyu18)8By#vdM!fSrrc$=s;Bk
zPsM|nG(iz5K~ZUgKVs4ZMI>Yo?!bSb{G+grtDU30-@jA%7s?-n!RgY@+ROUiD*h|&
z9~B(34!S16DOT|?5bZBR?S5hYJMI7280hHy>rsHi59flzfzZ*BRduyRf)k&YrmEt%
zXNsMxEjTUzy#KyW&?5h8f93vbkIuS*j_||hmYuCI%msGPCr|Kx75&~@
zJ6l)cnlxt@&EqgcO!pG#=ph&
zTU>uj0)LD6w|4y&*WZ%B-y;64UBAWkwZ*l!C3H&YM-`e$CTz^Xfe~b9H
zcKsID-;%)JBK}|1F4CWld>8`s-ur-VdfaDn6HOHrtBd-&YMR>T4p$t=HMAWOu6ST%
zBpl)8p|5_H&BWALXM!=1*0!E*I+xTA!uJ5pb1H0}AkhK*^VsbD7TDJ%
zsBw{v?I-@U^_ShjD5sA;MvU%Zg&9JgYaE%lReX~2A3U|wE=7s1sPYY#g%6<4^MHNp=7zV-Q_6hLzzTQ*S0lArdRz!as4YDPRaM7{&~J^b=?E1OU*t
z0f2tsCyw(uxOjC707~0`>CdqPefeeC#t~*KbRf_l$A3%kgYv%yerZqWpuHcmV^jSW
z-N|+!R9i4w6Y0U`d9c*NCiss^{J&iA7q))kLqH#95A%Q_z^05qEptRTfWwWjb39yL
zazy-BBmBQu>=zymz;AjD0<@|70G*W(aHO3MfG)KH__SmI)FKhgf&3UZZ4x7Jl@S1p
zS!cfKJqUyOhu1${@E(FmJWt2Nh+#lQ-+;{)>EZp21pnZN03ZcdFX;dVfCb$CgeUO5)uzdhGaqtAmxxZkS0h6q#rU4nS-oBcJQEhzr
zGw@6C>+!qr$MIM3_X#Knm3^0@HJrv;RN9Z5fKps5kHXvkuH%nkrz=2(G#K^qFSO(qA8*+
zVlrY@VliShVl!d{@eSe_;w<7C;!ffjVl2rK5-t*15?vBI58atX>G%slCXeMZ(v>dc5w6?T2X;W$6(N573((%w~(mB!Hr^}`LK(~67=IH68
zMn`>*CLFCkI(7_tjQg18F_&Wxj};#4J+^b4`MC0N*ztSE^Nx2O$Ivsxi^K3`il-XR^;@Ilh
zR@s@@HQ15t$?PB4u_t*>8lJpyGXLZ-2Pua%hdsw*j<+1EoUEKWoPnI#oC91WT+&=u
zxnj8*xiH*3+{WB@xY69RJoG%8JpMe{JVU1_PAQ#oKb3N-o0pJRn%9~48Sh6vJU$7&
zt9%K3?fiKBlKhVR&-gn82n1vV5CSOzeS+kI%7Wg4*@ELj$Aq+nZU~`;mW4Tm&4eR_
z8-@2pBt%?9Qbh(uX++P9-Vm)2T@&LIvlUAa>lUXFR})8xmx+Hl&3_tpI`Q-;30esq
zi8~T+CH5qxBt0baBR^cS|M3sOp#6TisEy{F(q~-Yo!#W&u2K!*qwQKW=5G;*-1G^
zdHJl!S&y?NXE7?WDnTl*RiUcts-dbM&e5DRJQsg%MD3)SgIc!Qs=9=Ffck3<0u3#V
z2#r2XW=&hoOwHBvQs;xt*K3h$UDArz`mD{T?V(+vgQs&|CrW4N0@nq^1+*@ptEn5M
zJA9GnqWi^4Jwm;UdI@^d`l9-Q`i+-pFIip6y0mF<*5IMRpy4S)q~Tj5DkBS{Oe2i3
zs&S<8n2E4SkV%W_2~$VYGBYAGBeR!g8|LTCqs^x*BrHNKdM$Y@eJz_VpSbLDx!Q`#
z%G#>v3c(fQD_K`|t#z$atT${lZJye!+N#*b+Ai3gv3qPc2UCDW!e;Ch>?7@G9TXg*
z9OkYnU5&ZA=&0ft@A$<@!zsxLIWcFBhm!>_>05i|&AM7=AEtFLQ^8^2qK
z+nBqod$jwihnB}nPl%_bXPFlr_%hXsJcYcC9QRiAPVnCHG4d($rS?Vmw)*k;-SeCA
zSMyH`zz?tucpJzbcs+1DNI57u7!qs~{1(N5x`mp$rhYBsI_Y)i>uongZbaVrdeiu3
z#VzJr*KSSRR==GcLK)%_(sxJhPSRccyN-81+!Md||BN4*hB5gC!xk^YerQ94njkJ%oFKK>eQ72OnbIwmQWG!_{<
z7N;Fo_Js4vqbIxZSL1sU&L-qPWqNw=Ddw5ov(CgbiFrxPN%xa>o;yDOl&qdyn!=qD
z^Mde&_lxONqtwPU>9ow3CtluvxtESeAIs3ssLzzj%*$DtD0Mu
zCzzLR&NLEOzOJp_3A&o(|Y&5
zL9L;#QMs}9y~6vdCfTO)X6a^hi)2e_t3+!_n?zg52gwhm?NaS!A7wsPbjWwqbSibe
z=~C@#=+^9R>ABF;(QDZIsn4Qs?33N6*?yP)F9Y5K*uiT<1Vf?2M}}iYm_|}Yc}Mfd
zB*&`9RmWRDU-~>aVLP!f={dPOb!(bpI%bA-CUaJ7wrWmo?&G}q{M3Tm0(LQEiF)bj
z@~P$Gl`|`?t0t>cUp&6x*22~q*3-Y9{`z*~;>O4(d=rZa-D23v*p}XI+%evn!TRhH
z?Z)l#?p5q-?GNGL;6I!F-1FoN_%85m&y#OEiu_wwyq^d3$MH+agB?Ebeq{W+0PbMV
z6TAfIEEEE667qA;lOS-<6Y#YbAjAXr?f7vJ5fc*NlR!zq-C)Q-G$9mh|3@a61tr2G
zhL8ZHWJdrz2o&7)1bV;`9w7-50`7-GO+Z7)PDFbWAQq)l7UMWdqIdR~HK+J-efR71
zHW6GZ$px$2r#(m+-gHRtj9-$x@g*`v)xh(dnv}ZjDMKUU6NPU(Kd;%{^oqI#dr?%^
zHNh)wzmDXSk=4*V55BB9Ty=Ev_VM-eza4Vt?!C~*(J`@cPvTS4UZ!VcW)+u|qRYxF
z>fbdqzHe&o?&M@7|Nl@}~Z|Z9duaW={U0@mUqk1M@q7-d3d#J8HKxnRFSmT-{fV%}1^T
zFAN8aR$zW3{%@9t74GA2#rS<2{PQeeh3ojO7{3+cuN0&6w_^NOjK5NhuHTCBTQUAh
zF}i;%#&5;=E5+#ftr))*_qSrmT@mq9XnXc{sY+1X$liz0ocU&bM>1r7&Clum
z)l7ekfmu{_fh80RVYoq%ch+N^TnQm?0_g@&&>K^0seFIe-0IBQW_C1TX4
z5+TWGcuP%d;YsAFZg)q{D|c%Xj>t-&WEI6&?G`uef4@Tn{OQYeD$^Z#6tq&aJg8I|Yjh9Z%3AHi$Uo@yGL8#f
z;e!sNu76hJ%uD~#=lxJHhv?pT^-IMTY-~VNfp+K8s3u&VMv8$(Bihb2T1X!WmDbzD
z3tADcxVeL=MJ`vb3{nmcdq_v2-+e__F1{t}m&iBw$raXVY
zS#vS2>8_n5w(}z#e_gCtue+{FtXtx0dheG`&6fjO!a_t&l_Oe&{7x=yIARG|(qQ|&c+_2iUVkU&yqlU~=UC7y`$FP~;
zb4tDamuNS^Epgc6Al(`Zl$%tIRfP<8gthX+f}fVcbc4eD8`F%G`S-ac^J=xB=J!pP
zrkot=UfI4z##oK;yjL=Bsfu3KNj;j_FrI@TW)S&6{z{ycGu&iSd5`~^{7$d!Xt$3=
z!vD>;#jmqM?Qg%;!Z2K6H>|?i8?CxYg`DJ-_q)(L!hij(fyV9oi~F_V{t}7tYB)f$
zB%z2uG8A^1>0^UQI^}A;G4OK
zHKGYAkB!q8gBpxIU7F`gTZ126SRGtbDfjjj8l9eq^7zcFe^ww+yKa3Ymeo@@fNFSf
z<=kR=W#^D@z=gi*dUS5o{z5*~6v2J-J{kYLen{h8x@~04*N%~^2!SjD>w3fKXU@||5Mz3UAuC|?z$x&j3WZ)
zY?lO##_Hf^yO=QYAjQu^^oz?@)OW>-pj}4o
z9nJPKJw&2X^Ypq#)^iU{=ztb>K_RbU-D3!$&s;OY6JyHM!4WPYv|K;NmmyHU3M3|?
z(TU5xg76fF^60lGuj{wg6I_&U;sg}@N4qBSFX?$%okQyCb#RjH)%DX0aGy1EOW_;Z
z(dW7_)l9)Alv6XFk(Fhmm@BG?1LWJQk}CUZ_ORQ_GgZbPW_I4x3|pSfx70IZAT?Ef
z+Bwq3c0K5ri0sb!&Dtoo&Go?#I+NK*J#3|G2oL|8m!v2Bl%^-!IPXk1CchZs__&_S
ztQ3dVMyu`CEeGZ5o-ZzA+MkGhwV6^!ANuHu)CPvh>#^wVGmqo6pTUe|&gD=r01-C9
ztF@K3BR!azW$VRQNejy-em7kQk2}-~45aQ7{Pn5d7`_3P<|{9X*pN8>*^FoP)K?+t^K9Rn*H{PO+}tsl3d<=~6^d!KT8;hwKSH
zx5m`k``_#!wrwa8-d9^Kene6LAb_!+SWMrXSFnC=gF+ihikL2Ya7
zRt<0{^#;ka4VlcFa19Up#V>_Vy&RD{@23$KKY+j3V79AT^i0H#SNn011+QJ4yyn^x
zrWo6U3KSlic#6s%nZb>`Q@X8c=5KUmQ|2W-_kpH$ti=(vWMDmq<63wKX
z%O9}kF#H&_-QHAl;DdCpdyw+NSofjc=}%96p4YB~J>~3}e^5q~$3WMMs?$OV*KV(}
zVvhzC;eb;0Dh(VERf_|B)Aw7Y_CESK?0+5BPFg)4G@!JafErdof<(*JIDkTMw+8&&
z!E4l_;d1+aKg%8t;6d$w(cO!4#Xeta-=KN6KiuUW#atXlEmfu+`?znSL@3F<
zFlavL>}4mt*HSG0=~){(duH*%K#Ph>!ka$jsCT_J;^MVp^jTlDEVUzjv#!J+=XX#$
z&QcdRwKuPXx{R7)ahSa9jG0@OvGN;S7RW!TlN7r&U~Zm~NHRe?2){UV?z8G*BBxm6
z$=;0lZUu(LBrjBDyU>c95e_I0@GU^}N{P0UB@8#5iLC#u@u^X8qG88UxpTJJQB7a8
zV++$BUZ`?lGS6oFG`>ZJDS<(596lHyk|J%qYD1`o`J0WvFmXLhM5$Mnoee%tuQ;
z=h541Re5*Gz&Fy6zYT
zc2-%ddlk<>VjGl+K@iBe=27NE4hjm^QyGTGmzh3&aRVlLVq42gE%wv^ZeB-x1tO$
z=8C2iX+&SWu7QaQI$QlO{gPg8`zHepS_6}VH?`ff@9)ul4PIJgvh8}A+U$CsR$DTr
zH##2+FOxUCtvN>OU0WrLod{a;cCAPWz{keF#R0t|frFd(>f-4o`;q&rwR+Qn66W(v
zC{xGoiN&O%*pX#)*?lKVl&8mD+oHc3XN8WE4kAe2NlRr9+98jLXIB1?3SB|r{sUId
z&I(`LXs{}i)6JJHf+CwY3BL>uUWkMfbUxe~xey=X5;g{PvVhCy>xl;)JM}4%sH|^9
z*JIPwJ}4c-I{aY&&ef^mn02=ZXBD2#*K0X^sL?3d9MN!r<>z)^PfaEoRnG*wO@-?Y
z)(*9O*kOd7&agUm?(EEtXr@qOlR;w5wNDh4A%k+BWW}q+B6>@EwYMALLl;D;btJeM
zX;mmCZ``)!(7Zv)=M_d~u7R%oFcT*ei3v;{%tva%mK;y($BPwnc}6
zbeUydzJpg!Sm~8a$iCm7UM9HjzC@-mI&)c}JmzKiSrQgI_YI=a(b%EJktyuEpuV6O
zjJH;V&`SAY0oRwt-N(6PrpwZ-#8Rc}6;PG&LKb}FdeO=q_9VE%rQ91L6
z+sOE+L5J46oMEE-Y;`RueJeZ_q5l8UQ#uFh+q+XEo9#W08MRmrYxpp{MH-#;&cO}o
zs%d^<;vRcsUr1hPrZCgjE`i4z)Z*%VvZDGV7}~*bRg7~KChrV}KUx5tcVXxxuR?CO
zjmD6l$XG}7oeJI;b}`4vI4^`}y~f0pC&kIg`4>zJ84u4Cj4UM?c&ffL)s0is4ilMu
z6VV-wgmEH^kH2x9{!)N_Y$Mw^>9rM%UXlqOWxYFcyNmUe)RNxCh+KotxtIOs;kmb~
z#FbWep7`DXKd!;2i{h(Ap_bidYVAI(FDDJBj9{Krr(1ejz^^f&;V)&o<9eL1&z{=u
z9h2C3Pb>aTz`ez%&r)$kDJaisrZ#0t*ems>(+pHKd_GxVh4EUqE<9&xS0Ipq5i6x;
zcPG0LQ>R$8@|RBM**m*U`&X)YR@bQ|DE&)((w%$tPCTgX2?#Dt+N$%j=~0guHZ0A!
ze|3!8UcacFvD<+Ag%17>s~T#rxwbNURJbO1odO34e|i=*AkEX^Q)^j|11_8{;S71J
z*f1p2jYqC0R!8P|-1ykeL|=H~PSDgg(WeC-^pL~x-6>B0suT2Wo{oxc#!(;W4jz2e*ez;#l^N{Em7D0y3g8$PqHtIzjT^U
zPpojmt9mK%fOhftaxO+{Q))S=>1K3L+SK#PoY@o{;8oToG*$NgxsQc~qopVwKXVS*
z(Hzwnjav^nYL8V9VbDp{bEA|iALNoH-3xXCG_qn!n*&qAU*@Tua3pKqBHuZi6~pSM
z*RB5F+fluPtW9z+QDsWUJgO6{RJ*rp7Do*GO}HcEJz9!NZ4H*5Rnj)R=OPui71d*;
zeW~FT@z_x|VPXNhuCRy$EQG5=I+hTl{+yq-8SO#|FFIwHEh`J>(;xBbo2&IXJ5Qjm
z!E7>5U|d}5c0qMN7pvsgh88(mJGwG*Mk86Wx8nIV>hn0jc|$1ux+z`QiDzEH2(RQV
z>{>#(e{hrRz3_NLlxU;FBh@Yf@z1(%^BDx43VIjx
zh1EB|!ly8C&SHcFz3?ni4*n(!8NBvN5Ej@<<+0UwM1J-}>N^1!GD32{0L-;X){SD!
z&BXHar@Y`_E*F>9wag)aM1c5B2h)89jxQXfc*F$8BW4?v|1lw%xc`9leq-U?NLg!4
zU(@c4vr!j=NHtH9Ge7Ul)xZd$)loeo_)})N7?#sKu3@e7LxoKjVps<*^Pqxbs|(hs
zhOKNiBO7Lgw%g^p-75$?8xaHXatrBnP2e}M_6BLmc{MMmua*v
z=Ss!w)SUhU4p;N)nH#N*7bYDR)?`Th9prr4yP_V
zNEI|F)i0+arfaX$%Q;CIt;=nFvBv?!6DzT8!JkS;n*iR^;!Jrkt3sDzX6B?3Hp!
z!^r&T9qFhwLTeb|Z^%=8ZJrpZn!eCHtHW3)MnWfTYW+wcLgRUV^ff+TdFh5^JB>}i+|t2
zs&z1UH+cAoYxuA*I;(Vfhu+0VMQ-59yqW2E;{=*ehClOCet}qMywgrQ-M);6;mt-Z
zJ-PyQ9gd7d#Vr)_A;vr)2y<_W;>8NbijU~Kau1T~vXDOOVn6Da@twAGcYMk$sp$M;
zExf%OEmoVzJ1rU+C90mTVh4j3x1$Di<}rhmDuXP2aaF`4@V791YVI=&U-h1yUs6d+
zf9&|6r^Ch`?_4jD%(FH`V1t(1vUZKGQetbb?xU9JB8eUu`Me7?X|2&m%gJxs6&V`#
z=(2N7M)b$+>DFdh-m(6)ZEim(;~C=}U|Acxm}~-H%1zN=yTM7P@i|1oWa1pX;W<(w
zNFN89q5%hZ*HjK+%=-~O43ol~g_W9|;SD(-7PZ7_!EdO2?OPm84f>bX#o3*cqNnQJRBztb;?Hr<$J$>}
zQmnbIpxYOhn6^M!vp`Psync})0qy;Ow&AwoXL(A#Sf-6aLV+bcO96`0Iq#lLMm)L4
znS^>)TM61=EUN*T%fb~2Skd9_JN{KeocMjp70;%rLjgS1JdSe!UkYCaDX=hS9wlSb
z61lA&
zKHC5erml-UcPC
zJpf(0zXabMFY<&Z_?(k$5izyxHWU&}qoH4hr*#JMjyv3&J>wJtJtf1x{1CLapVm6G
z?mqJ@v~uYpm$l;3%OD9i=*r?XdSQV$8k3T8i-sFjC%ij?1D+kXK+V2s!~q*TWtIE7
zNDTO`$QL+33I3UDo8+6&p3V~l>>$v$42h8gRgI>PjAn^Tp76-J}VooLO
zmx6vDXw53?SJwd?uvnrr%aWhIZMUmB%t|dQE`zS55zzO(fEnqhXew8_IZ4Z}t{Ik+
znpOwTF);jcj@*!hLPDU&{&jR@d3TZqbY=qE8N9WdiD|$rSWhd#rz>4uN(Dn1`GqrX
zz2S^%;f$3kD%Ng&Z#o%1%1NkYa7FH(0wXpJ%Ti@*?hM3n#&w^@oAUb*VCZ_q!cDf56cWV%(3
zPxv8O=mYJu`S+kYM{Ldzo{u`?-&?uQurTv*7b}Pchf~-ZwSgV&LhmMeVnNU9Y9<1O
z;To=8ipsl$170g&!f?RL^L_g*IpEh&eGW#;e|rK4D7L`C(P{Z)wHJ(8*_NNdpm4yr
z{tymW)~ZAe@_=;L0#+DsfOd5LUaTqT1v>D`l5eax?gzzy-^}ZXeO0@&w=%ls*A9N@
z7W~*_S~;jwvEHEj_5w%LtK`zb)(acM_A
zx7A_2x~R{07fB)OCLh&aMkl|narPVyVT_}xzJvozUvScO#Txr0AfYWlm?I#l?dh8;
z&)^A>(2>r*D@#2mQsWV`W}~2D|h&oj)OdkE@F`}Z7#Q`
z;^7TV)W8O}OQc&CUu!+zje4kMDxiTcf`9Gp<`t@LC(>S}&k#%}7iR{S2B+KUjBT42
zn>4ZwG^aFj{TGHS`u^$vuGFKp-MrFwytnUkGujtET;81SZ$qU%7|5qee0-&pC2Yv6
zq~lXaN}EGUN~315QVUTP`TjapU~Mnivh|Aa>HZO4`IZWk{i(tCaN!N}T)0;KkZNgf
zF4FgL`&PjMyW)cU&Hb{(tu?hS`4gL!S`YKeGqScMA3x@QI2L0~N3`#>QFXlb7NX{c
z?4O(o7Wp%cT@?9g!OFIoLbp9EH7oggh|;d-3@5P3T)yzC|DC<-%eda%>nn4%`>=gX
ziC4fl204^3_vTKV00+@J+M=u`OIbBICcio#{3PE}tFZX`s-LW|sf&=`^
zi}MHfB}o)RW&7ktJJT&H|IMdIvDFk&7sbG+nD@)EoHVls)m?Fc`Sy0L9U<2Xi=A0$
zuf7g%ejoj6*`oDm2PJ(j^pUc8xIPXDOs~#f;u%U$DEDq&Y-|l&2yiV_PRn>pLQ*kA
ze@$6bBD%OV&zGKbjMW
z?et&_xV`AA;IjF}d1xuOrgR8mB-WoqpY`x_3|AyI4v>)TSQ#Cju~PhxagO5t&kF-p
zW1T=XU!LB|uKqkStX@9VaJv0M;hNu4mE=9
ziNUF;>KqIzaqrnQ{^&Np&(-@!zvPbO!#(bA@q|1VNoKb^-@=k&6G6h0TT*ijB^n&R
zef`19H_E5Yp=EO!t^MIeZN{qWb`r{oEZ7)XyO7GpM%gtnxdkT~nJ{>n~k;rK^P?32Q7ge12c5l}2
z_z2ddbZgQQov6nzcq@-2=w!}j;gJ;I22FtEBHm?@=W-^xH69yLmj9&*&>^(#x1OIG
z%mSZ~+-yeq20+e?vMwJjFniX$1Q;
z=uOZDc4nRaPK0;)%)aJpn^OrLaMFh28YTYYJV^y4!ln|q%X
zYB%Lvglk8FjKx}5vV&@l5q>u7CJH`D>_lPz$Uf|(sihY(^EkMY6hAz=hV?=nT@0LA
z-AfP8jtt7~{Y2vA=lw^R01VtM$M?)LBi{}`
zq-J8bFX!m7ix@M*0d(Z@6`%M=Kcrg<&*$yCqr{gpx6%l;>|9pHtCL5ZnH~vsrRMgb
zOARkI`Z@(bxz3%6N%Xy+bCdSMf|ZzwMt8=X=zQal5;i+%5Idw>>NzSKq|1jTH4z=+
z42?~QCtTJ^VKCOpzL;G5L?;JsMIkgIbF<<2WFo<`tPtR^PW6fls*&;-aT}#(e*-Az4sO3@QQ4384K^U_E73a~@
zB_bO6Om|zu!-zRqQF5`H#|JGpru)nLTj1s9=f7ytWpuRA6%=T4L`p#-I<6b5L87nY
z;}I{Ud1H=Pz9HzznG?*(U}h$UA+L}YzTy;n#dd=P|A=zBDt7z2SBp<=z2e-buU^@P
zNvQ*Ct~wbhqFkCo0mrVQb&fKV@a@Z14M{h-zy30tb43F`;z<%1#OH*CVj4G<5?2td
z7S)pTl7s76_r98^l-w%RiZEF+^IaisM;1OJ0q9j$_}||dva&13#E#ukoQw&XA3f^I
z^6~yHg9)|AY9Ea|_4y$AW?})@o`snge`zGnVs}M`SP2(Ak1Q(1G22Fmlc)JSq!^Mz
z`K<)Pg}Ab;HfPZ_L+|X5J#)XilqPnXn)kZby)opPLAs(M_Gu+%t)M?016_7le)ht`
z%)ECd7O^>;=O~0n-cowm!u+`SBMFIT-CLDxjmPE|jXT4uG3lHJ
zo)S!{Iz#01tOu}{xzF=bhdjCrJG)iiM5@{w5sqQEYesND4H34%&)H|F92Hl-*;^M>
z_+-2cUG_=xy+epelX197YLRic>J=4^Q&-LrFQgl>dW=>|&cU3^ma3{H8b)P_!c8!Y
z>EWu@hIYuO*}M$+G{kf5`wS~%KD$_E&`^6Jj6!YTfPSlk*ZJ2ql7|sFIQdAmh
zl`3dhX)Y36G%s9e-x=%d~>uve^mXEw{b~O-P3LLMLxO
zw^5dOo}7dU-P3yJ=A}tJ5phgo(nwwX;``ax*&Nw;IXN%uS%@)SKKD8`hH^rc@lPC4
zQ^vPD;z2y0lhtf^jqe<>Jn?*17HDiry(P`cicc=Z?48QYh
zBL9F{UbZ+T#)b~!xsvOhGz!Bk78jS4$hWn0#=S7;$nNJX6JRClqNA&
z!RgNw^NutJ6GEI@lX6*@4Snb9_*oqq2_Zea
zkVth34o@;kLs)Vn2MIA-~HXn@~@=JR-7y6pdly7!K1vg_7Agr7kS4vC2uKGZ3B84)C`hjY0#c;+4xyvcd#_1AdQCzR0tE2nIcJY&?DxFo>~Ea?
zefxPC{FQMhkae$nt-0pBuHWq8(c@NO1?89Jl^E&C-=hDtuw((8^Pa;7PRCm+tgPWg
z;t6z`h6&SQMtWRSs*LOOjZe?`FW27_%NYf0E|klL_he5OK{75uD2Dd&vvCN?en&Bhpp+mf^rULDA_(_w+7%
z$zY5h_E*>iy*8%NUyPh?o`3!^I8UjsXoj(X!$CXNqvk$8XY|OLqwq7AnERGw$$5qS
zt4B>OE&i7vbp}&H1Kl|tV?8zAKel~x?miwD+zM90_#VB9_F9B$MtQ)+%+bK6x+NgH
zuS1CM66e=+F-}&!9!BF>Rrq61FXD2->RV)UT1b9gaUy{89#QdDP1v7MbKeyZO`udtmn!2W@x&&-&tmM-(h6Y2lkEW-`l_$q^b*0M_
z6mJPKIe0S}%Co{?r;SkYIk8hCqa#f}k;sJJNOJ@{)IdqcH0|@-Qhe5UJl0?E+MZi_
zW74JZ%qjNPH%f6@WG%d#tY7exX6{~YT)UmoWn9{nU9(TGVJvoab>oJ8Xb-yL7ftSY
ze$Lm7QW5YETu6R5WY*#Nta!noJGped?-vb*_s6JF#_fu%OUaiPpV!eA#;CcJqH5TBTdg{&b^otL_MJb5_
zBJivBo{8-~=is|(=v9u8Dsx8%_M{?O`(#q!*lalfpNe?)=-jmJ(Zb7}iuIC;%Cbb$
zt@yW(-Z*o;&HaWan|ZGP>D(qk0&Ks2t-gVkDNFX=rl4_FH%TP&WwyBYNH_Gwt3*ek
z?wS(TEPXUh@7^#?=IKcXNZ1^O5u!|S%FItxkv2s{u%6rg#PMQxKF)v4UHXT)_e#|`
zmVCE7HP-0j@JLg{{C|w}JpO>}HYy=+fF(3Wo;B$?-T>HF7B+3!)d2BRH4AI9uPr5uE^wtW7rk`!VYkSA
zhFH87RbqID{;7i2Mu`gtqOZ>^c8*iofj${MxJ;M_5e!K&TZ2=jF8~N?VR{@o!l};O
zzUDJV>BF%BW0o&ECf!G8_B(6a3TnZrt_uKU#+xBOp2`whk0Dc38X{aL*N@I1=^&K;
zed+V_xy#i$Naxv@;iX1J6->%-HXz8O*^DdNdv7$)g7fRVfck3bViQzT@hrNyS&iuS
zJIBF$Y^XUnB{tQYPxERBHMexpyX?8y!!26H8>UvytBNv0WO}h({v5xeb(!T~5u(
zmO7Qbd1IfLi~CVY_m9~0PIM|vS9W4cH+@nkOm32>*4g=$1_FVo`||QKs#aJly?CKh
z9G*j9sDt;%?;rRp{YvlvYmyr4l+dR7etQtD+Jd`~V-8+@r;
z@pZBJY3Y7!LQ2_XXVp>ia6_v*SA^Ni^4M>Kqiomr3|JEi{V7S}%G~Ev?c%44go3W*
z5FhvFmML%%Lxy*ER7&89NvFC`2%~kItq#jaZ>Xl6K7BG-#bkmHs70i0yzkdovh26O
zJ&EN7HRLIzFmMibbam=3=ZTkSy1y=Zke*)?K1)UNh78tYUN0+m8N17nltG?a?f>3O
zA4IJ0`+8`NjYy?gtWr;PI4%_NOvR_9dp&n@5nP$|m2ya%XS}w5nK9Tmu2rQi*effj
z<#cb!LN67B=q-e1Icp2AZ*@S72z=UFpu`H-B&@8cIzgi^%EXDM4
zn~Sqe(CD+0#N8#eWPmJf_{{6?86jC$M=TH^F8eI&p4gS!OM^*->%HHpu)-9wB@=
z(G*rnnI2iB=xZOyL%FtwmcyuET2WD>$bl^MQI|aN@aB-~dxKW*HEO+DQ+TpYk0NKv
zJqoGMqWm$0Zxj%Pr+G_SikMU@nk_ce50&TA5GRgQvEiBAk;j_&U#FN;B45(opB53K
zKg__@!vC|O?~lzCW1$tsP+xdHmR$W;e6p_8wcsqnqg}y_A^+Ytg6FY|>CbAV_+7N3tx{yN)wyKEj)%GzBWGm}1!_y=UzHCr
z1b3`t!=c01KQl8B+U@NPvjhG6K2?6vrMc=YqPME%jy~Q82msdI4ik0?
zmx!5b@t6Udr;TkLJ;oK{l}puc@*3K`N*rPTpi>N;4F76CfJa&^tF=C=e~L-*4$@c$
zy{cW5&VGsAKEJ2hN8|8
z63FbZL1RPB7#P+lBv+;*VMTC8NYaLR7({HL+^F~t|q
z-nh}S1aG5BS#cAs*OPj(0##tT!D{`&+B7fSXzKYz@=m5E(BceNqjUOGmiB`3Jkx9R
z=YD4>-Bc`$inf9R;IdKz-P2p+F5_ewu{yQ0HO(9f8~ComOj|&r3=64zQ4lW;HYVmV
zsO4>Wcr|OWafEL8jTdBTE6(=@27J0Gu0FS@IQGb$#yMtCtg*KFUH+YK$^2#rYEP~`
zFzfCU?&@axc$20!9Q|Ia0Wd$j2VNQ#~ZhAs2|A(TJidF!0LH
z?rf25y7#YVw8C$^=OCGnb&$LYXtw?2x)2S6Jt&=ISAc<
z!aBZeDJ1oUSCY&zw%-HY0Z1ZQ{JG%%$^8u!_SN0$rLp?(w7#3nv;Lo4i2)p|#PTd1
z$9?4ie^ZeA3NDbEs=t;-e+jnsqDS>3d509Fg=K_!&fudG`D#zQK*X8~192nPTxO#D
zw+>s%O8e98UU+?MKRkW72yXLX0@1@DJp;3_J{~PE4p$>*r?*UeK)wg%vvYUv5%Fl{
zmpCrtHvBFNc?{_nnVL-+kmGvAE`WDdn!H&}FXwY%Ole~8^kbDZ$Bz>
z3Fsa;@o`bB|Bcry(;TxX3ZSg7G2mrWUL0nB$mk9Lcmfv}Xs1J~Ud?z4PtI@_=b11?
zYwABSrKFZ!&GzhY!lojF_8>|O;@6j_-g|y=p7TYYZnZxTGxAZOrDnbpO>!`6-a9>7#guw;)dZX0ki3ekPI2W<+Cm|0#OtfGDYN}k1%
z+%WG3FIrZzFZLgX41a8c+lsqO^ZTHrrKYT|-4R6U0;G!=lQ*>b)WC_+A!e&@-EmW`
zSm-RWi##$?vbeCIShrAh-VG&=v03B?1|F>kF2mxy{UpZ%)`w=U)FGj)+;|Ej#F0h-
zNchRa$HSzwhk`S-ksjmON#|qf5_f`XH-og7imI0xWonh3bah@z#_OprzJI01t-MB@
z@74J6Xy@8i#8S|vVlQ*6!2xDIR)0DLq8-s9Pbn?314Z-Ff#fI+Gjl#g6u?Bw%s&;W
zIdbJEQ00@{8YNO9?PtJz3ibw~;VtS6?&nW4SP(J^2#magOR63-$Xi@UNo2>F>$GBD
znS;%?^&Qz;h13Vi3Pa+KL3_*ozA{Iq=YXJv#i?wb3TY=ZHv5LA2y+&v`1NlW>(9sg
zyUAx_l?T~Mu^_CNz5AM(t>PW_T<3>0;y?!zXOfzL?BC&xY!Em8=iG)mdoMwP}jv
zlRc7O*Ju6kAe@LyenRW2&Fl!(*LF;Bq&jOPmh?)MMepcQm{`IO#-V0ICIh?l_hS`u
zL10|119lFO)wcr*$M2$I{OB{q0tvk8V1tPg_rl7mg{EC1jOm^mCY>1&yB&zdZ0?*uW@nJ0VrH*NXOGwdAx5Ph(0?x`u|T#%_ryI$iDFWM
z9`1GreG}8PW$DM-Ixhx(=y>`oQ8ta?r1BH3=z3Jx9g<($0gpt8?d{0DOk^*LW?4@D
z@|*g<&awsn8++6}zb;By3U`QG%r5}EGtlC<(eP*HRU!GUq%FfJ;$*R>RGMDKR$CQi
zN-Jtx+kqb?`qED=UTn}iZN=I)ANcv$!$9E5{JtHSnF8ivhbmo-@<`%sAr)l^-$dXr
z&FEA$(#;uORhbJ74URO;w228gYsLR|^6U0)Wm}gQzv(`GHEi%Ui$szPhiEK{lo)+F?CGX@L~L@$wg2L8b4f1b}ofA+~}|6yhJ!MS7!
ze9x65;SO1E4qkC&DXv*bftKb*)P400S-=Keb#OSaQ2@qNAbQuvU^+N|ox)m;ErK=C
z-x3=qN&d_7sFU+r#!4!Va-7Oyeo@36np$`~fut%!3KaV>#wHONCz5@UxxF!P^`n8E
z&p-U=vF=4)=%*5w@Wr5=PG1CO0XT;yt;Z_1Y44wWX|aV-V*(T(!fM|$sp~2M)O12rxk|WleYu>rVk&DDrGPs2yYlym>F6*c{7
zrT_Z&FIxz|r+mFPd`fYTM&E|Du%kB8UGAqCz|+p5C{5+I#>?d$^oB4Ndv$bO2N^+r
ztcI9xRzQyC|hl-EFDddi@SO-4!Pg_FqR
z2O2T7nWF04o_741m8fdnNDVB-P{fuq-bRmp^4J7Z(U67B`xcPQ<6LLm9l{{3<#QI(;l1+BZU!oMSLH(DZcmVE7`|qDkICS0df3|*Bi%Gtg87c(B=LFqXuVhS+=_L%_`8L#M+^o
zwCY1t(5}?q#AttawEpYA@bAnZaJC#6Db7b*ca`IQ{72W
z4kM^bZ0Om}xl~VMaI2KRZh|b^w8Oi~oacG-+pL2gN?g&{$xV_nW%}CR)L-{L8`(U7
zIg=&AX`*;zzQ3-XHl*U|a1aD`LTn{z?#;Fr>CLrNyPE>3*9DA$^Yol`B{ir5==q7ko~~w`g^10#VQy#w=~M
zSoDCQ@-)(kgS)t#Ki4R#n+uoN?|yrSzTwCD%;nmi2Mhm9Fd6ro8j~OPIio%S@k8n<
z{mLq_{-Bn=neUI4slOa`_LM#{Y#1nvyiqgVIYkO*z$Wi|>(^oHFugUAXfJ%bhK;vF
zY;8{?I}Sx$2HY=6QD-Kw3cQw-pCTy(L)vX}%UO^W$iVdgTv{wm%}0;0f9)NB^))rq
z#tEOaqFHxBd~IWgm)A_gNrME}?qDfA252xdlYHVuK5aBJJNO}pvgqnk}IESkk*QiNy^TQk^
zr8qg_mDZ8D>c+xCl#~=TE**>$@v}vgNU*nCh<~@-B4}x8vzBb#AThEGJXu$!rj>Ir
z3#hzUsQ>0W$@?R+&KMj(Ov}K;n^L4HZ&vX_N98-ZvkxQep4w96hXJ5UoE)klvEK`t
z7o->$p-)%Mh7)UmKH3R61l4KvDF9`mv8VDKXokS6wNC`89FSHKY#a|EtMPdvYQ`5|
zU}T2QejA}jwd5^7OWvh<
z^)T0}(U_?g7^xU~*)j0a?hVi`Bl|?quM)=Epnmlp-Uz|S&BMVw;z&)yL1G#3Y2(T;Bv(Dy)9vWq~!s
zX3?MyR9oQFILbwsM72aq__#pe%G{B>>eO_9=jLLa(}r7(LOI`Rm_mLiQ&7=wye7cX
z%;wnZ_U=MakEy9)NlIr0AWs{5QA
zKpw|k>-(PkF>@r1$Oy+VTz_XkFZ)m@{Q<|?vY+Hu-~YBfak`Y6t1S~qFU6oOY8RwX
zniUzDm`R*8V>mMyudm-ljmS^;%Qw`KV10*VyE@65v+-*NiF4jx20t9)iFHiqqfYg)
zc71F>VeN~jtqp*U`AAbVP6)#XSIC~1;EHI)^tOq3^zC58A+)&og7T|>b}=Uf9+uM1
z>QF+>DduMd++7}nEqh8;{cFRA*Unt~N(OVjbG3Q3){gxi_%jfJNjY5S>_n%EKj6O9
zHNAaM&IrzI|KxoTJkrsT&|bR!S*Gc9ElGkIucdMCZeD!V?JqGtbsNhpT-
zQ(kl$CbKHl5A4_s7soD4(m%cy0LJqp#~1+jABx
z(B2u&Z$W&hl;^20i(0rHzpKQMUgpcW$=#AyE~*EAT$XAv-egCRyIh9D0;8h8zO0y=
zXSm3{{pjQ@&9m$N7A)5r-`UCN@T0sgopfwXmR{g-(9fW=z@g^&zEOx1{Hmd)
zNpK$ASlm0h7R72zWaLaUf|mr8$lHmBsk>2&yyp*$!}hpSb^uI(v3omyxg1@ufX4!d
zYd?v4U7v;&hM4pL4PGYH5p3^azjqWZHTYcnw+pf_Bd@E`Jb{8wmU`3QCjXqh*Gea<4aX$)KAM
zd6`gk09b;lv3_uMUaz8WCc9d>41+mH9mO>n)dWX^Qh?5aH*c(t&rrpc$Gi(&RYIEQyXeYk5>2jQqNcppyEfAEyrq6aGB$lBT<-&^V&*S
zQ2>~(s@kjw__Jo7hbKu<`j9Wkr9yF5>p(}5#Fm#eoQ_&MtqRS5y0wJvL@x(hW1As#
zwW7;>kl=bninTytAt1!-bf5fm*SRddJiK?pg&E+R0%&H1rrAUcO7=~oTjqq_84z>x
zz%KYwQ0D_wK%Xxf2zo1um}$={W>&RkC6~5w-8AMTEDtyBE#Ng;7LogG>=Dp2G!#8#
zAlA3bMY!AqyE1)Wk)i>S7D_D`L|w+mrkgKh$0%(3*+eFOiki~z9^oFCS#GApxMQDU
z_!e>zVo`3LkRG?&QSGO@Y!C5<)EHr3%PN;0ds~v}P)~2Cia+G)POv8JWbmtmUEb}bEGO|MLQ)RqzK7Fho>VcqF@>UflpHsP5TW#FOgszqPm
z@wurt+O;vsZcHq`7KlaU<}NQJm86KXhoHs)6cC94tu06TCHun<8~a5|ce2}K5hG)~
zOTO4!qa-6Ax-3_cyo%A%m}3rrlw0WGX&al$b4QdQPfo6OJ;aj(-useu0pnKVlqlTqS-c
z_s*8n+hAu2BuK)4*}vgF47d+9dkQmd$=3D&sguk1sfww<`eRd
zrKbh)cxKHwrtP)ss5@c68ibn$;vOgF-p%R!!&WsDU#;n
zaD0Bx8E1Ls(>n*?FPnOPX~S)b~FMuxPK*y@9N8_bzQ>o-mAS
zwgJE!K{K0Z@hbFnK}b#H-C6atU(okNJMQh77)x9M_SD%*YD*IIL)
zQ|hh4hsEZkJZ^e=3I1<<(+d1#e_XFyB8K*};jQ*jo{PM_SLnyx)O;9N@h6R&ysYm*
zVU?|cdDOcGK22Y!U%@rX$d!76gJh3iVbICPix&Of*J`nvI>WJ{uI
zQONUq`UT~KFEc{qNj)q%kNRYemW~H5cZNrdLQDy@bPEWaX1Y2>yO~w
z3nc7_b`Tg}Y4u_*?Ab?5aMrsiwUvBTb!_yuH@b0>@S%IuWY>(Bi|yPWdl~GG6UJbJ
ze6qbdwr7n2eJmVKAV1%_v1XGh3Sh&0eQQi@9IwmV{V}0CP!OVRJ{pt4^rx
z#}{9}E+$OV4e$stvA+n6N*rjp#j3g@+Xnejg-XQ~w8vr(u)y1Ut~m6D2vxK9fv8bT
zkp6b^rvnzRumO_>5zZj0T~F+)yTI16l|-)kvu-F>D`xP9aFpwk#4Wem6I;w*R4wY8
zFsGQg+x~xJ3;XxOb9er=u@v;b@iuD4e=#s#TbyLgjID9jV#Qt5%;#Zq45kl?bIZO1
z@2-{P=TsKBT9@Svq5Px09>37u_T_pdV723dcYT~4j3I3;f-fWP12>Sf09h}D5pRY|
zy4VDd4C0;3$-+luNyMiH_-juxjoPu>`X
z&EGcg`Z>UgB-Sb-pnF&Rov)MWBBe7IGSndXk?|>iHMA0ZM^RU&fUW;^Ug?_{XbP8$
z!lu}jk|-`DuOF6K&qj12t~!^Ot=bDQaUell@5I>IT?0kKu_xjyUm^(Tko
zRyaz%`4kQ3b6d}dUu=Y(TPw`--v#R6Y_j53g8i}@lDFGOGV%X@;`t1rh&O+_csK}C
zNq_HJ6~qQVynNZ1TW=%aTRi*>E-P)I#+cWu)7#f`%il&}_<1Y2S|i9}-p|X2ZQk1%
zzy$H*MfANVS6M@7EfzLqJf~+piWeO`@y3@I(YXOb47tK%O5pqjU%uZ^q=(uctVZiH
zYrfctA@IzdP*S^*|7&7@EC27qSLxT#`w-`KW8TJN361-@PGydvLCh^h3`UN%o*p(w
z2K+KS32JN<>J0j9GB0>EJifE^?)DGgIt0b-~;)1lj5V_m27sX|NmtdXvq)0`)Jl>LoI9$#ZNE#}+kA
zL{@XD?qn~_ss8=A{dYaVJccf+={lPiWLh>1+tT8ycNB92pJVY4(
z#R8|#Z9PW~;VlQa#O1day;B2Jaz9=qY2G-SEK2*07r5N~RQT#lX5!Q*MWQtwV25pX
zZ*QB!bZYd^VJkO$I6{GpM$?UD4o)tnRrV@3_l8~NrOv8=pE|m*hxJ&Z!B@zZeVA$o
zM%+7r_vfJ^5k$&F-!xr;!(LdE_txDZ%c2?9gcxp4cBIzAJ_xzgY|)L>grzKHbO{Y%
z(gNy>HC&ZPr_V0E=%ih34xgmUvE1e4v<^$hQ+=VM-L`d_u}BJ(iCPcLg9SS^%Z}Ns
z4>1NvLA)f~@%#!ChSc5M*zi+U{aDlW@Dr68z3`4dN_uF^p)p^^Mx}80oAs?0wY%RQ
zPlo5*iWM!HpnX&PcEZ4u4x8vt{HLDckMzS#ZUFbSef!75ACa=9NG{3IpJ(XAf+Qk9
z-H{=u?9YWW|Lye1b}{oni0@{%=%aFmdj@+-Y!$aY0a?<_SO&7{MeDr}AymQUlrztNrMrZQj5~aYp
z)b#YiNSDgx_iXwUG;F+dwyraiYUZ)38;%7^i9(Aew{JmqpidDVk1-ra4QO#>l-)=Y
zaCQs=s!XhChy950ARM4_TsJA?d$(qsh8lN)DhFor6_~Jb4cdN?
zy8ERJZ_r$%#t1*u@wv>?_V5dyzsqv(s4LcC_|%MXmrc~Gb*ZZ*j%KJ&ZF_57~kT|MDK)h>}!
zvD3?fd$)=?tE^-HKTvFc_y7M-yztisVz>-khWlFyw$e079kXZbj01O6uFjfXg{q!Z9Gr_^x;%BE)o7dn`#htzUe0o1_D8JC*8j;Z7tR3Ka
z8C(ETNmYtF-|`#+2H|9^yM4NMYXZya0QeatG3tIkah5}mIZMi1Rb%$VvH(=XT+e6g
z$sa#ecN(-rJ;v1dRn7Cewa3D%qEKxG?L@5xHkwl2`xT6;eXC?B3QTRkyrd@F>g{w(
zt|gjiU9lb#F(iHE4E|5)%96MQXD3F(JK>=vX4JZ!9Y-f)LH|@e@;@<${@>co|KhRD
zPCLAlh!PUaxis575_t?By?XEJ9kgJ|%-qb~NT5(gUms(48kx+oyqP??fCzxYr{-?c4x13W?N4
zNbZi+2K~Uf1WZ-r1s0Gc6=%#7FgX4u9xNlqIa~-X0R82d!POd
zT=RcjKm0sFxFJDWIAIRC8v8dq3D#^cdqch9=Q>?I|n_
z^}{^kG_x-tU5es%srR0wDX=FXtW3(6t|VlCWXqx>`YL+e@D#SFrMQ0jq0bNaI>zGM52WU?j0*ZEMjXZ(nkLx2{Ok6Nf`KC9}Ht
zXFA%|4oz)`br3rwuB8AZZWv_zdR{E5z4vDb{(%c}y(ykkggR1LVJ_-{q|`=c2sXVr
z&T1p3#<>Y49uHIMjwAP@5Umh?Zu0P`%JO-8Xq`Z&N6o2^C9+8t2HuaUdbenz`fP@t
z8*IJfCPKI#R3f{Wpe{a>zJ3&V?cMRyqwySNEOT7c^kqaYY8Zgp!=co~leGP$;4f8j
z_u_YG@p7Kf1#%y4_E-&GcGeut&d|ryf$PH-nRaLe%bhH%m
zKWe{pauo)p$1K+yS_@7Ed->E>Zgx*jWarnP=_UraSzROZ{-C`gd@vB~&rV-C9LlEF
z;^!+wgx@={PcY~Wfc{m`yk<;yVlWZ>jN6y;jPJn#OI0?y?IRNd@M>qP<5eUbjk5(!
zhKp`c*51jQ;R%_uz}#6bSSteEHnw~JuRVAEvSvh2FS*oIjo4r3@1|&EYr|)e`$p@U
zm{zh+qWl<$bvHJ}-)zNo*&)3ur>IXX+i(xB=^~o$`VbAFnb&U4ZijE#1u$+(wO1nn
zez)UJGRlb968ZVb7dh?Q%vMvn$1m9-*qS3o4C&If;`|o$P^=%+gZOqSOOhj;9K1ca
z9iMJr8%wKvWN}U6e?hiWB_7rblIF7n+n2CZbve}d5sR6Hz19$I8ZKb17yXrpqtQ%H
zj&3jiTpID_MSE^kpzSwhVh<}Y6&)y=3_s)5NMBY{>x5t&Be(iIHUULJkx>wOx)6Lf
zv3HX)@`W_D#G|~DbD>01*xVSlwPlE9ithVDyY469c1ufeU|)k#VXho)8X=FCUTy|e
zis8hQ1AD=e#|AIB!eZ?h@dB^I>3bjrjabC~Si=$usUTXvDHAz*nMHLQT8*VD9qlUdaxeaTEki=+SU1B3{uDZ`L{#oB)b|uWLSG}&k*l0UlBj*~
z>Xzhx*E#fmAR=m9ULTm^T(IPG!t}mO8VlLcEJ|6j+Xz=z#pdx)IwSqnz!zNB*PO-)
z?>g)FL>P+c&oEb~S*LQ13?5}!FJS!AGT1C1NMK{+qAi8k39jB6sM|&akx)suNcfgj
zr;TFij~WR&=^3Wf*@ZO{aq`u26|QO@LcC<7rPZD)u8$j6bOc^HpTM#j`mX~5`Bt@3
z&hNw)uu%!oskH1GQuOc6z@WCfT%t*g0jsUzH($Jw?YK_q+_e0S2Z`@N2jePttv8;J
z9}|R-yWfoh!*}%M^JB_OE44~Y4vIHAF;Pb}&g=XhyjI9OB$Pz!PRHilRyra?JNI2*
zg{RCFr`J%&|G=pKFHD%fN@Flj`)h%K%KhcY)^QLp;gUt-UH^ziYpWF~{jN}krI+q=
zlUrF-dH&H_Mz<0=mO2u0i$mLaY(w+IG|aHmR}<|O*u^9HjUdRy(EZnqDpEm6D{lEf
zEAA+Y{i5!oX*|n2^&MP8)FB_}Up&xo
z$J2_)NOLC8LyHKjYW3~ifw1ut(4NzI-V{6s#J*?`_CJS3BnI{z~
zwqOyuj0RPO^7%r9;U)QQl$~+t4rBNlS_Q+usYoh?mde^fjtp<`h{Mz>gCaLuB9)#A
z37afE(i?G-qTcZUiw>*W?bT=XV;7M89a66Y!+K}u(sHA5vu=4*VHlnjO=aS%c2MjR?&j9nPlw;}K+jxad&C3vSC_tJi8@C9kf9)<$P&{c+5
zp7${Pfj(%4sa*R3aXV|4hQn_>JuK#LudfPPU69)zbN-F@4ENDF{%MNPgJ%OVTH^P@
z`=%c`M*n)YD5)0hVo+~y!W1(osH55*^ReFevS*kk{|3xs8O#T4xIMu$soE0i=-Eo9
ztFQQS{nu5;LgS*^>LTz!iI>wL#$akP=cab5*7ZP(`5#ql&+tL7Y`_-xu6uK3#{yMA
zferV}dToaO&G@QP&lqNvlJc^glw5DsJVS!d@uhdhA#_NKZERW%@D!%ihvLIJG$`DS
zeZr4gi4;(?CV#eJpktE4^t^=i@M+G~GUrj_iU;q;Z#-V$F`~IlI_03X1`od^jffiW
zwl>}!O&ogH3WqYLga>a)RLCj2D;_hQ!XDxIY++iKG{E
zGtQTb=b4A3tD2%glier5#@-Iz4Z?)x?;~I|COW`~!cdD8C{E^nhPyHEmC2RT1h;9J
z^r25+2{s?Z9`cNEsm6;&lH(HFxN>=k>y1w
zm&x?NX@=T-Mzw>%_CI=(s;<&s87+Lb@g80Lw#Jg;i1N})Zw
z+D3|zAI+ZJLa@Cb_k57z;#K>)VMk!)a*G-oRM_6$@};yqRoqy=PC9WQpPcfA;9HiE
zCsqbAzUVR9!wziduz0}0@NKyHKxph;y}L}&?0QkYjBC2l4&OAp+Bz`$zSZ0@I;Qbxmr-;zno<^@r8O
z)Z!Cdl6>m!qzHA~1MSfqw4Bz=j<#!GT>n1hAEKI-ET~Dt;hS$M>U!^y-E^o)RObP1
z(u#L(Da|ol2)N-IzNn$-{vHA9iR%>eRtk-eTNkdXu7~um-|a2`-5YUCNk~;w_7%f<
z>h;FM;MvhXaFRYtH?L!`mRBi7|AA2f2aiTo%Kdu1_8ad%Fe+|x7jnn^SA*<-pjZCD
zsQf3cY1zoNaWDz=V>e&nvVnPq`p5UR(Cd>pmSh)tEuMx)004M(C3kW9b|Z&1xi*Qn
zBB#!>PHs8TjeF9MahG*eu?HRnF0XT=ok3{Z)2>u+u`qnEn}ESNj7NRyp&5sZ
zHNEXOv-}5l6t1^}deB(|?v?_uV;2E_QIzq@`awQ92Q*Beai<(#j}Aq9;rrH)dSR@?
z^dzPIQPr2sJH|(VRg9CD7Ik7`mG`=v$a#BogU5H}JYQt`Zom?CmgGfZ{WM9czj%&c
zzHN+kS)fa?Y1r$)uAqaIU~kfP%ld01+9yeI;B+p|x#h{kx?F3OH3X0<$Y!AnVg)qB
z+>}xSr9XyqeFmWKA=|F^uu7w}nN~MsPk6X_3v($i?eCac>mV1i*pu$rGb!{hhCgO8
z={$3&%91dxz<14Ntm+r8_(Sw
zcD;au(B5439lNlt2;OfQyM}sZBC-ebHnqB7X@Se_VkzB=Fw2`Ne2T)^qo;G`Hf|=1
z&y<$llnz$(&~0_(?^$Pv#ZHbN+d{lj{q_U-&)pswTSj<$J)ZmPor%4d8R;Og1wQ?QtZFrZ%Ys4
zJZ8OLmSsRTz-l$AOVe_Xna3uc`|%u=oe{@rQ`7C`xIoT`;RF??m`}Cu3p$u|&8W$_
zo>4!{maXxdQ?K2<{UI{;&Zex&P$fNlxBXx1`2WvIs{UGL9yz4>Tyvw?YBva(kv)V2
zPP{FTV+w^op}zQHVMz&iVrLe~wX4Ql{BBT++Z2a~*PM=5Y=aF-SKySfZAU(mM<-dx
z!Hf^=G#Tnmi8vOG2pe1E^Zp+>0>!1(#PzT53N2tVVX07cUbHp5s1AYrf|JK0X%nh1
zU+ya2RFMDL&q^Jl0^qx(ymrGe;G^Y?11igUfxJ6RVed&{6Fs+u9Z-IV#3xKkBA$mz
zN_DkIHLXUy4MLTW&)1pjEid+<{3M}
zw+70)v2X&qi+6XfAR?}8*(bY`ePP&A@&6GXT>c?ExQi@ee@j%LL%yiE#Zk~|u=@Z{
z)RIS7QJ2{8(NUlD!`>u$X};Pn5usw=jmQ@~i7LzjT;Jm5n=r@cBEkSo-u7-yx^5T4
z5>J
z_gO5ip~vy
zuYUH)v(MJ#dot%kbx*UNt<6FiB!a)hTDpJfG`>_RtAxRVj>qjGrH=MFaKHT4E*|sX
zxZ+sJ_J&x`4RFpRM?yjh>9>%kN5*%
zGCbT{KHQs+scz~w_jgH!r#Fe|wdhv_Xc+>ke5B`|sGQrTdcjg7lL
zyg9qhexWRFHJPcZ%{9$<17Lw3CzB1`l`+~0ikN%`VEe=t*r_>;&vrd#0||CzRYVYU
zzn=Ti6rL%>G{UW{;HBLEQvk-l!f?#L%aHPG8lWODo#}uWz%$GIm|PWi7POQ2M593z
z5tv-;IBG9QEfOcss1YN{o%v9as#uPRA*c$~j!8m6yaGq1xJE|kdxU4iO_*gxL+YGV
zp%BkIJL&n%h_B8PpL6DjCyA^UmS4-B(k7q{#IU@FnniHD_F4C56Ryw8WIn}cKhmKP
zXn6Yk$&RN!XKiP|_dV{Ge|loh^*Nf`{Eer2DGQSeo~MdiqnTq)g9g2ocA{;7sGVh0oPrR+9R=ye}bPJbA>B>NkW>mk_c-tA2=W4CJ<^_;=v
zgzFDM*0;)DPtVTzd%%cJS7ZKeUsW
zOsHnE%lU(y^kgR9VRjD7B8a%@et4zN%gv+}Z7r_#MG>}F&T
zxNK^D@Hrq1)XLo`%*Mr5(5*s3roG~EJ3qyV%;|c`lq!Ek9Mcmx)PXWy8pH)}OBKjI
zK1f|7O`!flZx%g9W~5uJMm`W|VBBQ?NqkP(XoXn?5?36_W^zG*;lK>RVL|>I%Z@49
zO97c)t9lX(4JIm&w7CavHFH0?r(N`>cho>-MJf?yfIa8gtNB~8PC?Nz<+z0K^!MZ6!z3FcQ
zqB5G&{gtuNR5+F&%OU{AEi*WZ2El>MO)wO*<3^+sj$ns)+2+o2qcS!h=feDhQYRyI
z>v(aDWTbwKZWu&Gr-lnL4DGBG%JXmBWenjGp8b|hfKzmYspGj`nI3I6)`@<&K^ye2
z)jH%29L4#F)!^WijYP!V&|&)r5p9K}jDPj*Las!#F2m^7@-jC0bkvGwt`(P{ed6LX
zY35vM>R?jgz+R558(dd$VsqzVx2tDN0PBp%hCax?KrfTdsbD8kH>E@;YCC
zR`l6A+GQoyrr3&`vUnRYD55tVKKuCuZ}1*_Ze+b#;5%KisY(0V+|UxH%Ey>YLND<(
z*+>)*IQR
zn5@LA6(%r;vHIDi7@Mp`KAXmQb7}~~Bk$obef021PKT6dRxw!)uS-ks*;_l}7=1}^
zZ>#NA?obQ25-8nD)Wo|P@wA^*c!n$hhwTH{RE1fV%W@gu$oCjsncJ3)Y_ZZQ1B+8;
zs&<+MH~~<($(mJULxk^C*a#|nX?7NEv#w_Z2z;mZ(WCQ~
zQ0Di(AlRF=h8$|2dylHwyFVmqc?GTnU1n(W5-sVSx;ui;-x{*QRY{#e?spsSH16tl
zt2(bx9wms*9JglA5ucnXyZLUQ~gTT&C1P4bjf
z#~&mpmxez!z&{}x2uL23MYwm247~a{V9H9mn@~WKhTM8yo2kPORQ1_SRYiH)08MxZdK?vA0J`a{lG2a-Dxk
zlgcda5Z>=oFkhRHmewSif3d7Z$+vou5s&CcpDUT
z6(_dXRoE9?ZTjTha}dz;yTGHqQ%j9~S9R6y_35eLj_H$-tN*iW_JyYCArUhR<7c?b9}B4Z=>6XOdnY`qI^Bf7?4cGrPAT^69ZDa=iKO?&96KQ4S`)
zVWre{BJ4@~w+%i!?7}{6(hycO1RT&j)VR}(=zHYsC4<m;xrXqf(hKbcCM+^>LwIt?gSjVfiN9L?H3i!Z|T%X4-{zO}=;aL?hN>
zR>o(H__#>!{7WkOUt-nyORDQ<4?Bli3rH0&pYM!(HC1&Ir*H;W;#vj{Fo%}XLxNAgeOJ@vs}%sZk@J5D=gSt~G^|xF|X_kmi}P
zRC=0g(qcmDM+SaevTRGCy87kzy!Jn`xFmJuc{Vj(48;fW)1yx!p}%sKV{aaX+a9ui
ztif_yc~OiGpWd-Ul`D@d%oBR(ej42@m3}n--tDodv}&!Pt&pgD5JHAm+I@O@@P(T)
zO5uy)=@g9ARc38VGZi-GeE#wnvZeV$^MM(Ry$HPOV{P=zD%wX=dW=>eM@XHSIfWc8
za%{=xuGJSf+&0%HZ&d^1D`8JA&YO7t_&@g`f3tR+bl&1k={=IPmmMccHCnM7nqu}6
zF-4+i(@Gljtw_y}YX>k==Y6@)_o=@2YX2~5TC0R*^65+!&bgZ@(|pQDQU8Fkf(jGwc(w&n&o-n_$2{&Q`*VMDSu26Te&%EOQf3f%8QBA&Ux<3}gf(l3#5s=WE
zbde?^y-DvNT}p`b5+I6#(jiEZD!um-dhb
zdwb@be_sfB7w_{v<-V`$b5(AP0WGblGRfWUw&*gHdg@amSqnc_7i~|erO%4G^w@tj
zEGN05$D|;dcZ7@BEHS1tm)Hh8>
ziZRpU&&5}*=TWusWMX)B_fguPNr;A9jhIkU<1SWO+^vhxH>iA{d=07p%CbX>-Gfy;
z`WwmmzfG5a?4l(3{qdsAKx!{=j~|eDntrUu9M=1-`%)^5^K)rLSO}Cw<(gV*@g&{3
z*`gu74INlt1CkzSx8!dhfh~=Ay7LAK$zKKX)kyr(6?2edooZ|E#(H&B!}(Z+t;|Q`
zmEK3HvRzeV7Ple62cYn|0WAzuK~i%;>Yw(-n636&uPr*KDDc?1?9WgPB#l&g8y;+W
z^anJ_@cMH|-~D?2v+WdT=8cm2(O3^K7v3z{W
zI&9@7*IFn8mKl9W@JOzOkKyXgT;B2Ty_q+?ZDWG|3NW`cGoyZlS)Jp~t``$&z8aTj
z-AaV^_zE6Yz|otB;|=2HNUwSoh6`HsNKS@(!eqitA#W$Hyhg=tBi3+(s$g)m3Yhv)Zlte#*
z!jEz;lvyCa&GPLjH!)!ScuYNJ2|v#P2hs1BHt2)l+6pTgH4EPH>-oAr1Vbq@iKh<2|z{8lF
ze~}~PVt@9We)x-4-TMTgsg4cKDX+k1^vr%~USu2C`GPh0yd=pHQ3A}*j1%JfIl#`Y
zXFSs-!Utp~rs-rM$M%v@$_`nc%lt+*xR_f(wBVz%XJd-Yh1b4ce?9-Ep8ArZvg!&A
z?L0-P3Xxb_^_#>$6MQUwCS}I0^?gWKwFKC1#3~HVYX}f(NZHJnk5M3$xjj!@>M6n>
zuHh!!pvV(Ka)w3)qc>WkVq%X!0ZOR@Vk*3|(`&H0%jM~p1XYSsC4G^T+Hfs3COg-b
ze01=8u&T^=V#@R?q-6^wtBLSOy>ijqeT`f}_(YF0>7wE9jm?#%J(dtJujM{sD9@)v
zbXVlOHn4k+j9stvkg3BD99+Y>RAC;uFpfy_R|4&F%;Axrmc&(Ui}U4yRN1f7-)9dQ
zUQr3|S~Q7iK&4GrO5D7D*h1%xxt*P2VD#dG9VT4w7cm!Yck_a?D9_=!GLJkCXN8`Z
zc3s}YLk2I_4%U(uBnUav?l3?|AmfN1hK*cN=_{0EW~DQx9U__bs(r)K7S&|yY3e5B
zy*@X!f2B7>DZ1GFTOuH^cuHYmB1h|Q&Z_^#it<0^Z{-EbG@}iS(EjpXp*HBgs3`dt0s190JvMg`sN#>
z=4*Urq*b?-gqFz?-moisvcg}rgvL-yPhR7EsZ2~}Yw~?0AqCza@zql-1ujzJ7&Ljr
z6cih1p4>jteZJz(6Qz7vX@kMMLXb4Na&+@RpWaoL2O4mmX~t`!9oxTF1O{ZCKl!V1LvbUTYi^}l+0%*6BOlz{S3jdD2NGBSId3pUoK
zTuj$R%h-rK_fkBr_+?yw9j9ApLc?0iy_mPR
zNQ*R7Wfb!-K&&|Nv0C7w`0nj22lz_e62G-db_o3xmvscOxv2Bvo$iy#XZkN*{?%N(
z(-i6L>kaXA2M2iq<`bl(2KOwG3$tGq!yOy|NyK#PnbaeqJDwDh7K7iu*5jQ#s9MXE
zFca@5v@kYfm%zTjcsc&Lf<~!~Mi^Un(DQJnKJ{gjipv|(Zv5YDdu&(xI~Ju#jm=&!x9$NW;Zh2>JDO2m-*HJ32-Xg?oh$)L|
z7Hb#DzTN>_O5EJ9;@K&?et#7rGydgCOy7