cmislib-0.5.1/0000755000076500001200000000000012063077404013572 5ustar jpottsadmin00000000000000cmislib-0.5.1/dist/0000755000076500001200000000000012063077404014535 5ustar jpottsadmin00000000000000cmislib-0.5.1/dist/hash-sign.sh0000755000076500001200000001075111556357713016773 0ustar jpottsadmin00000000000000#!/bin/sh # Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright ownership. # The ASF licenses this file to You under the Apache License, Version 2.0 # (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # hash-sign.sh : hash and sign the specified files # # USAGE: hash-sign.sh file1 file2 ... # user="" case "$1" in -u) shift user="$1" shift ;; esac allfiles=$* split="---------------------------------------------------------------------" echo $split echo "" echo "Generating MD5/SHA1 checksum files ..." echo "" # check for executables gpg="`which gpg 2> /dev/null | head -1`" pgp="`which pgp 2> /dev/null | head -1`" openssl="`which openssl 2> /dev/null | head -1`" md5sum="`which md5sum 2> /dev/null | head -1`" sha1sum="`which sha1sum 2> /dev/null | head -1`" md5="`which md5 2> /dev/null | head -1`" sha1="`which sha1 2> /dev/null | head -1`" # if found we use openssl for generating the checksums # and convert the results into machine-readable format. if test -x "${openssl}"; then for file in ${allfiles}; do if test -f "${file}"; then echo "openssl: creating md5 checksum file for ${file} ..." ${openssl} md5 ${file} |\ sed -e 's#^MD5(\(.*\))= \([0-9a-f]*\)$#\2 *\1#' > ${file}.md5 echo "openssl: creating sha1 checksum file for ${file} ..." ${openssl} sha1 ${file} |\ sed -e 's#^SHA1(\(.*\))= \([0-9a-f]*\)$#\2 *\1#' > ${file}.sha1 fi done # no openssl found - check if we have gpg elif test -x "${gpg}"; then for file in ${allfiles}; do if test -f "${file}"; then echo "gpg: creating md5 checksum file for ${file} ..." ${gpg} --print-md md5 ${file} |\ sed -e '{N;s#\n##;}' |\ sed -e 's#\(.*\): \(.*\)#\2::\1#;s#[\r\n]##g;s# ##g' \ -e 'y#ABCDEF#abcdef#;s#::# *#' > ${file}.md5 echo "gpg: creating sha1 checksum file for ${file} ..." ${gpg} --print-md sha1 ${file} |\ sed -e '{N;s#\n##;}' |\ sed -e 's#\(.*\): \(.*\)#\2::\1#;s#[\r\n]##g;s# ##g' \ -e 'y#ABCDEF#abcdef#;s#::# *#' > ${file}.sha1 fi done else # no openssl or gpg found - check for md5sum if test -x "${md5sum}"; then for file in ${allfiles}; do if test -f "${file}"; then echo "md5sum: creating md5 checksum file for ${file} ..." ${md5sum} -b ${file} > ${file}.md5 fi done # no openssl or gpg found - check for md5 elif test -x "${md5}"; then for file in ${allfiles}; do if test -f "${file}"; then echo "md5: creating md5 checksum file for ${file} ..." ${md5} -r ${file} | sed -e 's# # *#' > ${file}.md5 fi done fi # no openssl or gpg found - check for sha1sum if test -x "${sha1sum}"; then for file in ${allfiles}; do if test -f "${file}"; then echo "sha1sum: creating sha1 checksum file for ${file} ..." ${sha1sum} -b ${file} > ${file}.sha1 fi done # no openssl or gpg found - check for sha1 elif test -x "${sha1}"; then for file in ${allfiles}; do if test -f "${file}"; then echo "sha1: creating sha1 checksum file for ${file} ..." ${sha1} -r ${file} | sed -e 's# # *#' > ${file}.sha1 fi done fi fi echo $split echo "" echo "Signing the files ..." echo "" # if found we use pgp for signing the files if test -x "${pgp}"; then if test -n "${user}"; then args="-u ${user}" fi for file in ${allfiles}; do if test -f "${file}"; then echo "pgp: creating asc signature file for ${file} ..." ${pgp} -sba ${file} ${args} fi done # no pgp found - check for gpg elif test -x "${gpg}"; then if test -z "${user}"; then args="--default-key ${args}" else args="-u ${user} ${args}" fi for file in ${allfiles}; do if test -f "${file}"; then echo "gpg: creating asc signature file for ${file} ..." ${gpg} --armor ${args} --detach-sign ${file} fi done else echo "PGP or GnuPG not found! Not signing release!" fi cmislib-0.5.1/dist/release.sh0000755000076500001200000000273211556357713016532 0ustar jpottsadmin00000000000000#!/bin/sh # Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright ownership. # The ASF licenses this file to You under the Apache License, Version 2.0 # (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. set -e # Did they give a user to sign with? user="" case "$1" in -u) shift user="$1" shift ;; esac if test -z "${user}"; then # Do they have a default user already set? gpg_file=$HOME/.gnupg/gpg.conf if test -f $gpg_file; then if grep -q '^default-key' $gpg_file; then user=`grep '^default-key' $gpg_file | awk '{print $2}'` echo "Selected GPG default user of $user" fi fi fi if test -z "${user}"; then echo "must pass -u with a gpg id" exit 1 fi cd .. python setup.py sdist --formats=gztar,zip python setup.py bdist_egg cd dist ./hash-sign.sh -u ${user} *.tar.gz *.zip ./hash-sign.sh -u ${user} *.egg ./hash-sign.sh -u ${user} *.asc cmislib-0.5.1/KEYS0000644000076500001200000000702411556357714014306 0ustar jpottsadmin00000000000000This file contains the PGP keys of various developers. Users: pgp < KEYS gpg --import KEYS Developers: gpg --list-key and append it to this file. gpg -a --export and append it to this file. (gpg --list-key && gpg --armor --export ) >> this file. pub 4096R/99E3E187 2010-11-23 uid Jeff Potts (CODE SIGNING KEY) sub 4096R/2A6CD560 2010-11-23 -----BEGIN PGP PUBLIC KEY BLOCK----- Version: GnuPG v1.4.11 (Darwin) mQINBEzsQscBEACoDu6kafUBujuic9JtpGXQdVA4HKRvSAANbgAX8o4fU4wr2c97 l/OD9SlZMzIkJNBYkQ+NoSIJKTYTJ2hJd7qFAcx0zEwFMhwRVvH/2sZ8xFKaZWjJ s26hgnNNPG0tKeV19jmIncFzSgRZsIL7UOcWGrcjhczgRiWIGPfkSjtWSvlQ/mnI 1dIsw0/IeiN+7ieIoQ7p+Z7rV3/K+aGtchkqoXHQ8tUB+DsmfJNBRONH/Ht/EzeC Ih2mAfaK+r5jnA6YhbUt4FRl4AlvMPid48o7UuCJQK05PJWewVDBnw3XoVFyVCXf y8jmTJEc1W/EcQiY+lr/gYDM+P08GVgz8ItllvCTCVAe6WihExw6+ESN+ygEtdRQ MpPfzY9XUkqr9clzyxFAEzUpfKmd/RSLbzbRCfVK/d8JMwoSmj1guTgt+eeeNNNE b9sBZOYm/JdPq539A4tLoWHLMBRvALuuc6Wd6iOka/Kh+Y8Pg/rLrKBHXtVP4kqZ jdXasVgsuxdrZW7h9jAAKiX5tAoJlHZuTanwzrQwPCCA32dKS8r0lbIKAZTMUPX9 sgSi0hcjy81wKtoLzgOzouQ9i+i+tlenyKf18nc7p+X4vSpQdFRF8B5sQXCvn6o8 s4mUwe6K7GWrX0DH5pEyIGNOkLqq7YLRQTb4Y9FNpfeFx5ESEzujpEjRxQARAQAB tDFKZWZmIFBvdHRzIChDT0RFIFNJR05JTkcgS0VZKSA8anBvdHRzQGFwYWNoZS5v cmc+iQI3BBMBCgAhBQJM7ELHAhsDBQsJCAcDBRUKCQgLBRYCAwEAAh4BAheAAAoJ EL6eLsWZ4+GHFf0P/i3QdYjD3SsQf4HgwAFuptbyast+dUayutMA4NCx9nVwimZD wFu+vqj4Ru8pZbnf+WdMmEQ6RQcrpaGdRZxAMDSk0MV261qdqL2G/NvuhYzyTcj9 iq09/gkYLgtP/TwziZPQ+eQZAxd1gwWaZmoDUEmLqzdK8zsJUmb7gTnKRsr1NOjV +4OepFp4qYUjHhMNshnmf/o6cB8uAaK9owPzu06anrDNIcFKDidppHeIrDnaW6IR SzjN9966lK9OQCknq3Ni2GrbFHAH22pt4bgb44APBZe3xj6SCn/fQoomzQYRY/1a KppZRTl8R9c0upGSysJhUjtRpsQvDW1h7zY89wH6M3s0E5RGPXFc6OAKwmyx6vrG //UwR0SjlXN6bGaSrvnAzReEjEi3XYZdrM7Y6EJYEbTteDXL/iHUjH/tT8wm1NMI w2uP7KgvHWzI9VuEChWwfXrGUq/XlzOVWa7vir68AWnCR4up5W8yhdLhdEAeyt1R 0macdsLG5ixU9nvoDJ+3QhfmEv2RSDh5eB4npCUENT/snwTF2Bn82wbwIcvaZxOZ Cr6wUrgc+rIMJt2eTYzpl3wiGtAcKUuDhBucaYZo3xV2VHzwz89SLi9HS0tNQEiX zwVeL7td9GPLhjZBYGHkENBCt7gIT9oSI9xGSJla5xqEqrorMkdYBwaJzmrouQIN BEzsQscBEADoK1emxgR16Eebcean9pccsrqWqb9yOKkGZ/tXKX4OKKG40zIy+f4a mU2U9RbkcPBln9JSbpjYg5e9HKJs32hQVbL/XDc6cIyTGdAsokegSZkL7efKgVGC AeTwg/UiQIHbRM05tP3c6LiGk829Tp8k/UZG+kW2Xe50MY4KKr2jKZrOYXjuvzVC 0TMd4ByNHBvwdDIryPVOxljFmTqjL7X5SAyafAQoD13lWZF5j2is8tsi+4reQePa jZrdLDXkJwSs1yf+ddCp+f+7fuCWWQKSHFfFe9LM/1cshVgon5YJL6hhinTosGpR diH/VjIUHU7KSKfpR4C1ML6/OelSWBw8CS51xWu8vli2xyE0gytkgztxv+Ba8AxQ 7MaZLQN5O7YMgj5XhNbD05EDk7VL2BsI91qhT6uxVav7QitUvB8ap3GF7x+l9Zdb yo9zQU8DFzV+OrqQGtJFVHukwGJMZvBaAxYrYON9wuzunVFMaQgG470MRauVl1J/ xZwKrD55GHYKBpQb3oayugYDRwHXPR6X52+3PwDPTY/oyvS0ZbPdekiKc6nBEEGL Gll/vd3Nc3+bZT8epQGoKCmy0vvfc8ubovfRKJS0nDq9imVfijNMriLbR/1G/r+K ulJq/jMnVahwSDP3FlZarYzR3UL6ojd0Ts/+sliJz+E6P/82OkpyGwARAQABiQIf BBgBCgAJBQJM7ELHAhsMAAoJEL6eLsWZ4+GHoWEP/25CVVcbaqf/uTUlvBYUDH4M bEmUZUN1VPeOlfl0oc5Vhq7xgK+3ObJm4X+nXoG15hjeWUqTEhi0Ls35l70TXufp RhvJsxuZKjApc1Z7pNOcuxAlrE+zdQsVSijsef4qiHk/28gNR6SaoKvpCvj0ReKG M1fDZ4I7f+LUkfogtwbA6ZQnT1u8xLuW90NU0u7BXx8FLz2D2a6gCSKMzdT9S/rk LUp7bKVMxGBd6FJS22w2gS1qxjB9RCVX8oPV79MKPQiX11VV0Dv4VEs5MWmcpAAF 5/J0l7BWLiCAj/eu3VmAyxzuKHY2BYAm5jsS4qcySgoojLEEnXsw6WAuvPc4S4tY A3GBtc2/09uhQ6Yk4oSbcZo6CtSbpoeZX5nUKlM4Mj8vecyQhQzhcsxqwx0bMbLM VgFmH0rVk3PQXU22TRtxNoN4g1ecNWdaIlpQignq8YvI4HRURRfqiBBVKdHdC/LS HCP+sQyt7Xv8zPH0vpjDYiG0gqyAN1dyqMDRRxQEq4VVpqy9iu3vyobu+21nCThQ 5qe9Jd5IWKO/NBT2vLXZvfDGUmEX8wq/IyQRV/oDR9fd1oz5rj8dcw6nNnRX3eAm osX3n9cWHgCIkMOS+NbH0XS1bBTHddSVJogisVBuVC+tXwR3XU0YmegELx3TJ7QA KGHdnl8nHylVQIVfHSIW =e5dc -----END PGP PUBLIC KEY BLOCK----- cmislib-0.5.1/LICENSE.txt0000644000076500001200000002613411556357714015436 0ustar jpottsadmin00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.cmislib-0.5.1/MANIFEST.in0000644000076500001200000000176611556357714015355 0ustar jpottsadmin00000000000000# # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # recursive-include src/doc/build * prune src/doc/build/.doctrees exclude src/doc/build/.buildinfo exclude .*project include LICENSE.txt include NOTICE.txt cmislib-0.5.1/NOTICE.txt0000644000076500001200000000025212062734045015313 0ustar jpottsadmin00000000000000Apache Chemistry Copyright 2010-2013 The Apache Software Foundation This product includes software developed at The Apache Software Foundation (http://www.apache.org/). cmislib-0.5.1/PKG-INFO0000644000076500001200000000312212063077404014665 0ustar jpottsadmin00000000000000Metadata-Version: 1.1 Name: cmislib Version: 0.5.1 Summary: Apache Chemistry CMIS client library for Python Home-page: http://chemistry.apache.org/ Author: Apache Chemistry Project Author-email: dev@chemistry.apache.org License: Apache License (2.0) Description: ABOUT Thanks for using cmislib, the CMIS client library for Python. The goal of this library is to provide an interoperable API to CMIS repositories such as Alfresco, Nuxeo, KnowledgeTree, MS SharePoint, EMC Documentum, and any other content repository that is CMIS-compliant. More info on CMIS can be found at: http://www.oasis-open.org/committees/cmis SOURCE The source code for this project lives at http://chemistry.apache.org/ TESTS There are unit tests available in the tests directory. They require access to a CMIS provider. There are many freely-available CMIS repositories available to run locally or that are hosted. DOC Documentation that tells you what this is all about can be found in the doc directory. Please see the doc for dependencies, required CMIS version level, required Python version, etc. Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Topic :: Software Development :: Libraries cmislib-0.5.1/README.txt0000644000076500001200000000147111556357714015306 0ustar jpottsadmin00000000000000ABOUT Thanks for using cmislib, the CMIS client library for Python. The goal of this library is to provide an interoperable API to CMIS repositories such as Alfresco, Nuxeo, KnowledgeTree, MS SharePoint, EMC Documentum, and any other content repository that is CMIS-compliant. More info on CMIS can be found at: http://www.oasis-open.org/committees/cmis SOURCE The source code for this project lives at http://chemistry.apache.org/ TESTS There are unit tests available in the tests directory. They require access to a CMIS provider. There are many freely-available CMIS repositories available to run locally or that are hosted. DOC Documentation that tells you what this is all about can be found in the doc directory. Please see the doc for dependencies, required CMIS version level, required Python version, etc. cmislib-0.5.1/setup.cfg0000644000076500001200000000007312063077404015413 0ustar jpottsadmin00000000000000[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 cmislib-0.5.1/setup.py0000644000076500001200000000354511672531520015312 0ustar jpottsadmin00000000000000# # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # import os from setuptools import setup, find_packages version = '0.5.1' def read(fname): return open(os.path.join(os.path.dirname(__file__), fname)).read() setup( name = "cmislib", description = 'Apache Chemistry CMIS client library for Python', version = version, install_requires = [ 'iso8601' ], author = 'Apache Chemistry Project', author_email = 'dev@chemistry.apache.org', license = 'Apache License (2.0)', url = 'http://chemistry.apache.org/', package_dir = {'':'src'}, packages = find_packages('src', exclude=['tests']), #include_package_data = True, exclude_package_data = {'':['tests']}, long_description = read('README.txt'), classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python", "Topic :: Software Development :: Libraries", ], ) cmislib-0.5.1/src/0000755000076500001200000000000012063077404014361 5ustar jpottsadmin00000000000000cmislib-0.5.1/src/cmislib/0000755000076500001200000000000012063077404016003 5ustar jpottsadmin00000000000000cmislib-0.5.1/src/cmislib/__init__.py0000644000076500001200000000200011556357713020116 0ustar jpottsadmin00000000000000# # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # """ Define package contents so that they are easy to import. """ from model import CmisClient, Repository, Folder __all__ = ["CmisClient", "Repository", "Folder"] cmislib-0.5.1/src/cmislib/exceptions.py0000644000076500001200000000445211556357713020555 0ustar jpottsadmin00000000000000# # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # class CmisException(Exception): """ Common base class for all exceptions. """ def __init__(self, status=None, url=None): Exception.__init__(self, "Error %s at %s" % (status, url)) self.status = status self.url = url class InvalidArgumentException(CmisException): """ InvalidArgumentException """ pass class ObjectNotFoundException(CmisException): """ ObjectNotFoundException """ pass class NotSupportedException(CmisException): """ NotSupportedException """ pass class PermissionDeniedException(CmisException): """ PermissionDeniedException """ pass class RuntimeException(CmisException): """ RuntimeException """ pass class ConstraintException(CmisException): """ ConstraintException """ pass class ContentAlreadyExistsException(CmisException): """ContentAlreadyExistsException """ pass class FilterNotValidException(CmisException): """FilterNotValidException """ pass class NameConstraintViolationException(CmisException): """NameConstraintViolationException """ pass class StorageException(CmisException): """StorageException """ pass class StreamNotSupportedException(CmisException): """ StreamNotSupportedException """ pass class UpdateConflictException(CmisException): """ UpdateConflictException """ pass class VersioningException(CmisException): """ VersioningException """ pass cmislib-0.5.1/src/cmislib/messages.py0000644000076500001200000000173411556357713020203 0ustar jpottsadmin00000000000000# # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # NO_ACL_SUPPORT = 'This repository does not support ACLs' NO_CHANGE_LOG_SUPPORT = 'This repository does not support change logs' cmislib-0.5.1/src/cmislib/model.py0000644000076500001200000044306412061413417017464 0ustar jpottsadmin00000000000000# # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # """ Module containing the domain objects used to work with a CMIS provider. """ from net import RESTService as Rest from exceptions import CmisException, RuntimeException, \ ObjectNotFoundException, InvalidArgumentException, \ PermissionDeniedException, NotSupportedException, \ UpdateConflictException import messages from urllib import quote from urllib2 import HTTPError from urlparse import urlparse, urlunparse import re import mimetypes from xml.parsers.expat import ExpatError import datetime import time import iso8601 import StringIO import logging # would kind of like to not have any parsing logic in this module, # but for now I'm going to put the serial/deserialization in methods # of the CMIS object classes from xml.dom import minidom # Namespaces ATOM_NS = 'http://www.w3.org/2005/Atom' APP_NS = 'http://www.w3.org/2007/app' CMISRA_NS = 'http://docs.oasis-open.org/ns/cmis/restatom/200908/' CMIS_NS = 'http://docs.oasis-open.org/ns/cmis/core/200908/' # Content types # Not all of these patterns have variability, but some do. It seemed cleaner # just to treat them all like patterns to simplify the matching logic ATOM_XML_TYPE = 'application/atom+xml' ATOM_XML_ENTRY_TYPE = 'application/atom+xml;type=entry' ATOM_XML_ENTRY_TYPE_P = re.compile('^application/atom\+xml.*type.*entry') ATOM_XML_FEED_TYPE = 'application/atom+xml;type=feed' ATOM_XML_FEED_TYPE_P = re.compile('^application/atom\+xml.*type.*feed') CMIS_TREE_TYPE = 'application/cmistree+xml' CMIS_TREE_TYPE_P = re.compile('^application/cmistree\+xml') CMIS_QUERY_TYPE = 'application/cmisquery+xml' CMIS_ACL_TYPE = 'application/cmisacl+xml' # Standard rels DOWN_REL = 'down' FIRST_REL = 'first' LAST_REL = 'last' NEXT_REL = 'next' PREV_REL = 'prev' SELF_REL = 'self' UP_REL = 'up' TYPE_DESCENDANTS_REL = 'http://docs.oasis-open.org/ns/cmis/link/200908/typedescendants' VERSION_HISTORY_REL = 'version-history' FOLDER_TREE_REL = 'http://docs.oasis-open.org/ns/cmis/link/200908/foldertree' RELATIONSHIPS_REL = 'http://docs.oasis-open.org/ns/cmis/link/200908/relationships' ACL_REL = 'http://docs.oasis-open.org/ns/cmis/link/200908/acl' CHANGE_LOG_REL = 'http://docs.oasis-open.org/ns/cmis/link/200908/changes' POLICIES_REL = 'http://docs.oasis-open.org/ns/cmis/link/200908/policies' RENDITION_REL = 'alternate' # Collection types QUERY_COLL = 'query' TYPES_COLL = 'types' CHECKED_OUT_COLL = 'checkedout' UNFILED_COLL = 'unfiled' ROOT_COLL = 'root' moduleLogger = logging.getLogger('cmislib.model') class CmisClient(object): """ Handles all communication with the CMIS provider. """ def __init__(self, repositoryUrl, username, password, **kwargs): """ This is the entry point to the API. You need to know the :param repositoryUrl: The service URL of the CMIS provider :param username: Username :param password: Password >>> client = CmisClient('http://localhost:8080/alfresco/s/cmis', 'admin', 'admin') """ self.repositoryUrl = repositoryUrl self.username = username self.password = password self.extArgs = kwargs self.logger = logging.getLogger('cmislib.model.CmisClient') self.logger.info('Creating an instance of CmisClient') def __str__(self): """To string""" return 'CMIS client connection to %s' % self.repositoryUrl def getRepositories(self): """ Returns a dict of high-level info about the repositories available at this service. The dict contains entries for 'repositoryId' and 'repositoryName'. >>> client.getRepositories() [{'repositoryName': u'Main Repository', 'repositoryId': u'83beb297-a6fa-4ac5-844b-98c871c0eea9'}] """ result = self.get(self.repositoryUrl, **self.extArgs) if (type(result) == HTTPError): raise RuntimeException() workspaceElements = result.getElementsByTagNameNS(APP_NS, 'workspace') # instantiate a Repository object using every workspace element # in the service URL then ask the repository object for its ID # and name, and return that back repositories = [] for node in [e for e in workspaceElements if e.nodeType == e.ELEMENT_NODE]: repository = Repository(self, node) repositories.append({'repositoryId': repository.getRepositoryId(), 'repositoryName': repository.getRepositoryInfo()['repositoryName']}) return repositories def getRepository(self, repositoryId): """ Returns the repository identified by the specified repositoryId. >>> repo = client.getRepository('83beb297-a6fa-4ac5-844b-98c871c0eea9') >>> repo.getRepositoryName() u'Main Repository' """ doc = self.get(self.repositoryUrl, **self.extArgs) workspaceElements = doc.getElementsByTagNameNS(APP_NS, 'workspace') for workspaceElement in workspaceElements: idElement = workspaceElement.getElementsByTagNameNS(CMIS_NS, 'repositoryId') if idElement[0].childNodes[0].data == repositoryId: return Repository(self, workspaceElement) raise ObjectNotFoundException(url=self.repositoryUrl) def getDefaultRepository(self): """ There does not appear to be anything in the spec that identifies a repository as being the default, so we'll define it to be the first one in the list. >>> repo = client.getDefaultRepository() >>> repo.getRepositoryId() u'83beb297-a6fa-4ac5-844b-98c871c0eea9' """ doc = self.get(self.repositoryUrl, **self.extArgs) workspaceElements = doc.getElementsByTagNameNS(APP_NS, 'workspace') # instantiate a Repository object with the first workspace # element we find repository = Repository(self, [e for e in workspaceElements if e.nodeType == e.ELEMENT_NODE][0]) return repository def get(self, url, **kwargs): """ Does a get against the CMIS service. More than likely, you will not need to call this method. Instead, let the other objects do it for you. For example, if you need to get a specific object by object id, try :class:`Repository.getObject`. If you have a path instead of an object id, use :class:`Repository.getObjectByPath`. Or, you could start with the root folder (:class:`Repository.getRootFolder`) and drill down from there. """ # merge the cmis client extended args with the ones that got passed in if (len(self.extArgs) > 0): kwargs.update(self.extArgs) result = Rest().get(url, username=self.username, password=self.password, **kwargs) if type(result) == HTTPError: self._processCommonErrors(result) return result else: try: return minidom.parse(result) except ExpatError: raise CmisException('Could not parse server response', url) def delete(self, url, **kwargs): """ Does a delete against the CMIS service. More than likely, you will not need to call this method. Instead, let the other objects do it for you. For example, to delete a folder you'd call :class:`Folder.delete` and to delete a document you'd call :class:`Document.delete`. """ # merge the cmis client extended args with the ones that got passed in if (len(self.extArgs) > 0): kwargs.update(self.extArgs) result = Rest().delete(url, username=self.username, password=self.password, **kwargs) if type(result) == HTTPError: self._processCommonErrors(result) return result else: pass def post(self, url, payload, contentType, **kwargs): """ Does a post against the CMIS service. More than likely, you will not need to call this method. Instead, let the other objects do it for you. For example, to update the properties on an object, you'd call :class:`CmisObject.updateProperties`. Or, to check in a document that's been checked out, you'd call :class:`Document.checkin` on the PWC. """ # merge the cmis client extended args with the ones that got passed in if (len(self.extArgs) > 0): kwargs.update(self.extArgs) result = Rest().post(url, payload, contentType, username=self.username, password=self.password, **kwargs) if type(result) != HTTPError: try: return minidom.parse(result) except ExpatError: raise CmisException('Could not parse server response', url) elif result.code == 201: try: return minidom.parse(result) except ExpatError: raise CmisException('Could not parse server response', url) else: self._processCommonErrors(result) return result def put(self, url, payload, contentType, **kwargs): """ Does a put against the CMIS service. More than likely, you will not need to call this method. Instead, let the other objects do it for you. For example, to update the properties on an object, you'd call :class:`CmisObject.updateProperties`. Or, to check in a document that's been checked out, you'd call :class:`Document.checkin` on the PWC. """ # merge the cmis client extended args with the ones that got passed in if (len(self.extArgs) > 0): kwargs.update(self.extArgs) result = Rest().put(url, payload, contentType, username=self.username, password=self.password, **kwargs) if type(result) == HTTPError: self._processCommonErrors(result) return result else: #if result.headers['content-length'] != '0': try: return minidom.parse(result) except ExpatError: # This may happen and is normal return None def _processCommonErrors(self, error): """ Maps HTTPErrors that are common to all to exceptions. Only errors that are truly global, like 401 not authorized, should be handled here. Callers should handle the rest. """ if error.status == 401: raise PermissionDeniedException(error.status, error.url) elif error.status == 400: raise InvalidArgumentException(error.status, error.url) elif error.status == 404: raise ObjectNotFoundException(error.status, error.url) elif error.status == 403: raise PermissionDeniedException(error.status, error.url) elif error.status == 405: raise NotSupportedException(error.status, error.url) elif error.status == 409: raise UpdateConflictException(error.status, error.url) elif error.status == 500: raise RuntimeException(error.status, error.url) defaultRepository = property(getDefaultRepository) repositories = property(getRepositories) class Repository(object): """ Represents a CMIS repository. Will lazily populate itself by calling the repository CMIS service URL. You must pass in an instance of a CmisClient when creating an instance of this class. """ def __init__(self, cmisClient, xmlDoc=None): """ Constructor """ self._cmisClient = cmisClient self.xmlDoc = xmlDoc self._repositoryId = None self._repositoryName = None self._repositoryInfo = {} self._capabilities = {} self._uriTemplates = {} self._permDefs = {} self._permMap = {} self._permissions = None self._propagation = None self.logger = logging.getLogger('cmislib.model.Repository') self.logger.info('Creating an instance of Repository') def __str__(self): """To string""" return self.getRepositoryName() def reload(self): """ This method will re-fetch the repository's XML data from the CMIS repository. """ self.logger.debug('Reload called on object') self.xmlDoc = self._cmisClient.get(self._cmisClient.repositoryUrl.encode('utf-8')) self._initData() def _initData(self): """ This method clears out any local variables that would be out of sync when data is re-fetched from the server. """ self._repositoryId = None self._repositoryName = None self._repositoryInfo = {} self._capabilities = {} self._uriTemplates = {} self._permDefs = {} self._permMap = {} self._permissions = None self._propagation = None def getSupportedPermissions(self): """ Returns the value of the cmis:supportedPermissions element. Valid values are: - basic: indicates that the CMIS Basic permissions are supported - repository: indicates that repository specific permissions are supported - both: indicates that both CMIS basic permissions and repository specific permissions are supported >>> repo.supportedPermissions u'both' """ if not self.getCapabilities()['ACL']: raise NotSupportedException(messages.NO_ACL_SUPPORT) if not self._permissions: if self.xmlDoc == None: self.reload() suppEls = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'supportedPermissions') assert len(suppEls) == 1, 'Expected the repository service document to have one element named supportedPermissions' self._permissions = suppEls[0].childNodes[0].data return self._permissions def getPermissionDefinitions(self): """ Returns a dictionary of permission definitions for this repository. The key is the permission string or technical name of the permission and the value is the permission description. >>> for permDef in repo.permissionDefinitions: ... print permDef ... cmis:all {http://www.alfresco.org/model/system/1.0}base.LinkChildren {http://www.alfresco.org/model/content/1.0}folder.Consumer {http://www.alfresco.org/model/security/1.0}All.All {http://www.alfresco.org/model/system/1.0}base.CreateAssociations {http://www.alfresco.org/model/system/1.0}base.FullControl {http://www.alfresco.org/model/system/1.0}base.AddChildren {http://www.alfresco.org/model/system/1.0}base.ReadAssociations {http://www.alfresco.org/model/content/1.0}folder.Editor {http://www.alfresco.org/model/content/1.0}cmobject.Editor {http://www.alfresco.org/model/system/1.0}base.DeleteAssociations cmis:read cmis:write """ if not self.getCapabilities()['ACL']: raise NotSupportedException(messages.NO_ACL_SUPPORT) if self._permDefs == {}: if self.xmlDoc == None: self.reload() aclEls = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'aclCapability') assert len(aclEls) == 1, 'Expected the repository service document to have one element named aclCapability' aclEl = aclEls[0] perms = {} for e in aclEl.childNodes: if e.localName == 'permissions': permEls = e.getElementsByTagNameNS(CMIS_NS, 'permission') assert len(permEls) == 1, 'Expected permissions element to have a child named permission' descEls = e.getElementsByTagNameNS(CMIS_NS, 'description') assert len(descEls) == 1, 'Expected permissions element to have a child named description' perm = permEls[0].childNodes[0].data desc = descEls[0].childNodes[0].data perms[perm] = desc self._permDefs = perms return self._permDefs def getPermissionMap(self): """ Returns a dictionary representing the permission mapping table where each key is a permission key string and each value is a list of one or more permissions the principal must have to perform the operation. >>> for (k,v) in repo.permissionMap.items(): ... print 'To do this: %s, you must have these perms:' % k ... for perm in v: ... print perm ... To do this: canCreateFolder.Folder, you must have these perms: cmis:all {http://www.alfresco.org/model/system/1.0}base.CreateChildren To do this: canAddToFolder.Folder, you must have these perms: cmis:all {http://www.alfresco.org/model/system/1.0}base.CreateChildren To do this: canDelete.Object, you must have these perms: cmis:all {http://www.alfresco.org/model/system/1.0}base.DeleteNode To do this: canCheckin.Document, you must have these perms: cmis:all {http://www.alfresco.org/model/content/1.0}lockable.CheckIn """ if not self.getCapabilities()['ACL']: raise NotSupportedException(messages.NO_ACL_SUPPORT) if self._permMap == {}: if self.xmlDoc == None: self.reload() aclEls = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'aclCapability') assert len(aclEls) == 1, 'Expected the repository service document to have one element named aclCapability' aclEl = aclEls[0] permMap = {} for e in aclEl.childNodes: permList = [] if e.localName == 'mapping': keyEls = e.getElementsByTagNameNS(CMIS_NS, 'key') assert len(keyEls) == 1, 'Expected mapping element to have a child named key' permEls = e.getElementsByTagNameNS(CMIS_NS, 'permission') assert len(permEls) >= 1, 'Expected mapping element to have at least one permission element' key = keyEls[0].childNodes[0].data for permEl in permEls: permList.append(permEl.childNodes[0].data) permMap[key] = permList self._permMap = permMap return self._permMap def getPropagation(self): """ Returns the value of the cmis:propagation element. Valid values are: - objectonly: indicates that the repository is able to apply ACEs without changing the ACLs of other objects - propagate: indicates that the repository is able to apply ACEs to a given object and propagate this change to all inheriting objects >>> repo.propagation u'propagate' """ if not self.getCapabilities()['ACL']: raise NotSupportedException(messages.NO_ACL_SUPPORT) if not self._propagation: if self.xmlDoc == None: self.reload() propEls = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'propagation') assert len(propEls) == 1, 'Expected the repository service document to have one element named propagation' self._propagation = propEls[0].childNodes[0].data return self._propagation def getRepositoryId(self): """ Returns this repository's unique identifier >>> repo = client.getDefaultRepository() >>> repo.getRepositoryId() u'83beb297-a6fa-4ac5-844b-98c871c0eea9' """ if self._repositoryId == None: if self.xmlDoc == None: self.reload() self._repositoryId = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'repositoryId')[0].firstChild.data return self._repositoryId def getRepositoryName(self): """ Returns this repository's name >>> repo = client.getDefaultRepository() >>> repo.getRepositoryName() u'Main Repository' """ if self._repositoryName == None: if self.xmlDoc == None: self.reload() self._repositoryName = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'repositoryName')[0].firstChild.data return self._repositoryName def getRepositoryInfo(self): """ Returns a dict of repository information. >>> repo = client.getDefaultRepository()>>> repo.getRepositoryName() u'Main Repository' >>> info = repo.getRepositoryInfo() >>> for k,v in info.items(): ... print "%s:%s" % (k,v) ... cmisSpecificationTitle:Version 1.0 Committee Draft 04 cmisVersionSupported:1.0 repositoryDescription:None productVersion:3.2.0 (r2 2440) rootFolderId:workspace://SpacesStore/aa1ecedf-9551-49c5-831a-0502bb43f348 repositoryId:83beb297-a6fa-4ac5-844b-98c871c0eea9 repositoryName:Main Repository vendorName:Alfresco productName:Alfresco Repository (Community) """ if not self._repositoryInfo: if self.xmlDoc == None: self.reload() repoInfoElement = self.xmlDoc.getElementsByTagNameNS(CMISRA_NS, 'repositoryInfo')[0] for node in repoInfoElement.childNodes: if node.nodeType == node.ELEMENT_NODE and node.localName != 'capabilities': try: data = node.childNodes[0].data except: data = None self._repositoryInfo[node.localName] = data return self._repositoryInfo def getCapabilities(self): """ Returns a dict of repository capabilities. >>> caps = repo.getCapabilities() >>> for k,v in caps.items(): ... print "%s:%s" % (k,v) ... PWCUpdatable:True VersionSpecificFiling:False Join:None ContentStreamUpdatability:anytime AllVersionsSearchable:False Renditions:None Multifiling:True GetFolderTree:True GetDescendants:True ACL:None PWCSearchable:True Query:bothcombined Unfiling:False Changes:None """ if not self._capabilities: if self.xmlDoc == None: self.reload() capabilitiesElement = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'capabilities')[0] for node in [e for e in capabilitiesElement.childNodes if e.nodeType == e.ELEMENT_NODE]: key = node.localName.replace('capability', '') value = parseBoolValue(node.childNodes[0].data) self._capabilities[key] = value return self._capabilities def getRootFolder(self): """ Returns the root folder of the repository >>> root = repo.getRootFolder() >>> root.getObjectId() u'workspace://SpacesStore/aa1ecedf-9551-49c5-831a-0502bb43f348' """ # get the root folder id rootFolderId = self.getRepositoryInfo()['rootFolderId'] # instantiate a Folder object using the ID folder = Folder(self._cmisClient, self, rootFolderId) # return it return folder def getFolder(self, folderId): """ Returns a :class:`Folder` object for a specified folderId >>> someFolder = repo.getFolder('workspace://SpacesStore/aa1ecedf-9551-49c5-831a-0502bb43f348') >>> someFolder.getObjectId() u'workspace://SpacesStore/aa1ecedf-9551-49c5-831a-0502bb43f348' """ retObject = self.getObject(folderId) return Folder(self._cmisClient, self, xmlDoc=retObject.xmlDoc) def getTypeChildren(self, typeId=None): """ Returns a list of :class:`ObjectType` objects corresponding to the child types of the type specified by the typeId. If no typeId is provided, the result will be the same as calling `self.getTypeDefinitions` These optional arguments are current unsupported: - includePropertyDefinitions - maxItems - skipCount >>> baseTypes = repo.getTypeChildren() >>> for baseType in baseTypes: ... print baseType.getTypeId() ... cmis:folder cmis:relationship cmis:document cmis:policy """ # Unfortunately, the spec does not appear to present a way to # know how to get the children of a specific type without first # retrieving the type, then asking it for one of its navigational # links. # if a typeId is specified, get it, then get its "down" link if typeId: targetType = self.getTypeDefinition(typeId) childrenUrl = targetType.getLink(DOWN_REL, ATOM_XML_FEED_TYPE_P) typesXmlDoc = self._cmisClient.get(childrenUrl.encode('utf-8')) entryElements = typesXmlDoc.getElementsByTagNameNS(ATOM_NS, 'entry') types = [] for entryElement in entryElements: objectType = ObjectType(self._cmisClient, self, xmlDoc=entryElement) types.append(objectType) # otherwise, if a typeId is not specified, return # the list of base types else: types = self.getTypeDefinitions() return types def getTypeDescendants(self, typeId=None, **kwargs): """ Returns a list of :class:`ObjectType` objects corresponding to the descendant types of the type specified by the typeId. If no typeId is provided, the repository's "typesdescendants" URL will be called to determine the list of descendant types. >>> allTypes = repo.getTypeDescendants() >>> for aType in allTypes: ... print aType.getTypeId() ... cmis:folder F:cm:systemfolder F:act:savedactionfolder F:app:configurations F:fm:forums F:wcm:avmfolder F:wcm:avmplainfolder F:wca:webfolder F:wcm:avmlayeredfolder F:st:site F:app:glossary F:fm:topic These optional arguments are supported: - depth - includePropertyDefinitions >>> types = alfRepo.getTypeDescendants('cmis:folder') >>> len(types) 17 >>> types = alfRepo.getTypeDescendants('cmis:folder', depth=1) >>> len(types) 12 >>> types = alfRepo.getTypeDescendants('cmis:folder', depth=2) >>> len(types) 17 """ # Unfortunately, the spec does not appear to present a way to # know how to get the children of a specific type without first # retrieving the type, then asking it for one of its navigational # links. if typeId: targetType = self.getTypeDefinition(typeId) descendUrl = targetType.getLink(DOWN_REL, CMIS_TREE_TYPE_P) else: descendUrl = self.getLink(TYPE_DESCENDANTS_REL) if not descendUrl: raise NotSupportedException("Could not determine the type descendants URL") typesXmlDoc = self._cmisClient.get(descendUrl.encode('utf-8'), **kwargs) entryElements = typesXmlDoc.getElementsByTagNameNS(ATOM_NS, 'entry') types = [] for entryElement in entryElements: objectType = ObjectType(self._cmisClient, self, xmlDoc=entryElement) types.append(objectType) return types def getTypeDefinitions(self, **kwargs): """ Returns a list of :class:`ObjectType` objects representing the base types in the repository. >>> baseTypes = repo.getTypeDefinitions() >>> for baseType in baseTypes: ... print baseType.getTypeId() ... cmis:folder cmis:relationship cmis:document cmis:policy """ typesUrl = self.getCollectionLink(TYPES_COLL) typesXmlDoc = self._cmisClient.get(typesUrl, **kwargs) entryElements = typesXmlDoc.getElementsByTagNameNS(ATOM_NS, 'entry') types = [] for entryElement in entryElements: objectType = ObjectType(self._cmisClient, self, xmlDoc=entryElement) types.append(objectType) # return the result return types def getTypeDefinition(self, typeId): """ Returns an :class:`ObjectType` object for the specified object type id. >>> folderType = repo.getTypeDefinition('cmis:folder') """ objectType = ObjectType(self._cmisClient, self, typeId) objectType.reload() return objectType def getLink(self, rel): """ Returns the HREF attribute of an Atom link element for the specified rel. """ if self.xmlDoc == None: self.reload() linkElements = self.xmlDoc.getElementsByTagNameNS(ATOM_NS, 'link') for linkElement in linkElements: if linkElement.attributes.has_key('rel'): relAttr = linkElement.attributes['rel'].value if relAttr == rel: return linkElement.attributes['href'].value def getCheckedOutDocs(self, **kwargs): """ Returns a ResultSet of :class:`CmisObject` objects that are currently checked out. >>> rs = repo.getCheckedOutDocs() >>> len(rs.getResults()) 2 >>> for doc in repo.getCheckedOutDocs().getResults(): ... doc.getTitle() ... u'sample-a (Working Copy).pdf' u'sample-b (Working Copy).pdf' These optional arguments are supported: - folderId - maxItems - skipCount - orderBy - filter - includeRelationships - renditionFilter - includeAllowableActions """ return self.getCollection(CHECKED_OUT_COLL, **kwargs) def getUnfiledDocs(self, **kwargs): """ Returns a ResultSet of :class:`CmisObject` objects that are currently unfiled. >>> rs = repo.getUnfiledDocs() >>> len(rs.getResults()) 2 >>> for doc in repo.getUnfiledDocs().getResults(): ... doc.getTitle() ... u'sample-a.pdf' u'sample-b.pdf' These optional arguments are supported: - folderId - maxItems - skipCount - orderBy - filter - includeRelationships - renditionFilter - includeAllowableActions """ return self.getCollection(UNFILED_COLL, **kwargs) def getObject(self, objectId, **kwargs): """ Returns an object given the specified object ID. >>> doc = repo.getObject('workspace://SpacesStore/f0c8b90f-bec0-4405-8b9c-2ab570589808') >>> doc.getTitle() u'sample-b.pdf' The following optional arguments are supported: - returnVersion - filter - includeRelationships - includePolicyIds - renditionFilter - includeACL - includeAllowableActions """ return getSpecializedObject(CmisObject(self._cmisClient, self, objectId, **kwargs), **kwargs) def getObjectByPath(self, path, **kwargs): """ Returns an object given the path to the object. >>> doc = repo.getObjectByPath('/jeff test/sample-b.pdf') >>> doc.getTitle() u'sample-b.pdf' The following optional arguments are not currently supported: - filter - includeAllowableActions """ # get the uritemplate template = self.getUriTemplates()['objectbypath']['template'] # fill in the template with the path provided params = { '{path}': quote(path, '/'), '{filter}': '', '{includeAllowableActions}': 'false', '{includePolicyIds}': 'false', '{includeRelationships}': '', '{includeACL}': 'false', '{renditionFilter}': ''} options = {} addOptions = {} # args specified, but not in the template for k, v in kwargs.items(): pKey = "{" + k + "}" if template.find(pKey) >= 0: options[pKey] = toCMISValue(v) else: addOptions[k] = toCMISValue(v) # merge the templated args with the default params params.update(options) byObjectPathUrl = multiple_replace(params, template) # do a GET against the URL result = self._cmisClient.get(byObjectPathUrl.encode('utf-8'), **addOptions) if type(result) == HTTPError: raise CmisException(result.code) # instantiate CmisObject objects with the results and return the list entryElements = result.getElementsByTagNameNS(ATOM_NS, 'entry') assert(len(entryElements) == 1), "Expected entry element in result from calling %s" % byObjectPathUrl return getSpecializedObject(CmisObject(self._cmisClient, self, xmlDoc=entryElements[0], **kwargs), **kwargs) def query(self, statement, **kwargs): """ Returns a list of :class:`CmisObject` objects based on the CMIS Query Language passed in as the statement. The actual objects returned will be instances of the appropriate child class based on the object's base type ID. In order for the results to be properly instantiated as objects, make sure you include 'cmis:objectId' as one of the fields in your select statement, or just use "SELECT \*". If you want the search results to automatically be instantiated with the appropriate sub-class of :class:`CmisObject` you must either include cmis:baseTypeId as one of the fields in your select statement or just use "SELECT \*". >>> q = "select * from cmis:document where cmis:name like '%test%'" >>> resultSet = repo.query(q) >>> len(resultSet.getResults()) 1 >>> resultSet.hasNext() False The following optional arguments are supported: - searchAllVersions - includeRelationships - renditionFilter - includeAllowableActions - maxItems - skipCount >>> q = 'select * from cmis:document' >>> rs = repo.query(q) >>> len(rs.getResults()) 148 >>> rs = repo.query(q, maxItems='5') >>> len(rs.getResults()) 5 >>> rs.hasNext() True """ if self.xmlDoc == None: self.reload() # get the URL this repository uses to accept query POSTs queryUrl = self.getCollectionLink(QUERY_COLL) # build the CMIS query XML that we're going to POST xmlDoc = self._getQueryXmlDoc(statement, **kwargs) # do the POST #print 'posting:%s' % xmlDoc.toxml(encoding='utf-8') result = self._cmisClient.post(queryUrl.encode('utf-8'), xmlDoc.toxml(encoding='utf-8'), CMIS_QUERY_TYPE) if type(result) == HTTPError: raise CmisException(result.code) # return the result set return ResultSet(self._cmisClient, self, result) def getContentChanges(self, **kwargs): """ Returns a :class:`ResultSet` containing :class:`ChangeEntry` objects. >>> for changeEntry in rs: ... changeEntry.objectId ... changeEntry.id ... changeEntry.changeType ... changeEntry.changeTime ... 'workspace://SpacesStore/0e2dc775-16b7-4634-9e54-2417a196829b' u'urn:uuid:0e2dc775-16b7-4634-9e54-2417a196829b' u'created' datetime.datetime(2010, 2, 11, 12, 55, 14) 'workspace://SpacesStore/bd768f9f-99a7-4033-828d-5b13f96c6923' u'urn:uuid:bd768f9f-99a7-4033-828d-5b13f96c6923' u'updated' datetime.datetime(2010, 2, 11, 12, 55, 13) 'workspace://SpacesStore/572c2cac-6b26-4cd8-91ad-b2931fe5b3fb' u'urn:uuid:572c2cac-6b26-4cd8-91ad-b2931fe5b3fb' u'updated' The following optional arguments are supported: - changeLogToken - includeProperties - includePolicyIDs - includeACL - maxItems You can get the latest change log token by inspecting the repository info via :meth:`Repository.getRepositoryInfo`. >>> repo.info['latestChangeLogToken'] u'2692' >>> rs = repo.getContentChanges(changeLogToken='2692') >>> len(rs) 1 >>> rs[0].id u'urn:uuid:8e88f694-93ef-44c5-9f70-f12fff824be9' >>> rs[0].changeType u'updated' >>> rs[0].changeTime datetime.datetime(2010, 2, 16, 20, 6, 37) """ if self.getCapabilities()['Changes'] == None: raise NotSupportedException(messages.NO_CHANGE_LOG_SUPPORT) changesUrl = self.getLink(CHANGE_LOG_REL) result = self._cmisClient.get(changesUrl.encode('utf-8'), **kwargs) if type(result) == HTTPError: raise CmisException(result.code) # return the result set return ChangeEntryResultSet(self._cmisClient, self, result) def createDocumentFromString(self, name, properties={}, parentFolder=None, contentString=None, contentType=None, contentEncoding=None): """ Creates a new document setting the content to the string provided. If the repository supports unfiled objects, you do not have to pass in a parent :class:`Folder` otherwise it is required. This method is essentially a convenience method that wraps your string with a StringIO and then calls createDocument. >>> repo.createDocumentFromString('testdoc5', parentFolder=testFolder, contentString='Hello, World!', contentType='text/plain') """ # if you didn't pass in a parent folder if parentFolder == None: # if the repository doesn't require fileable objects to be filed if self.getCapabilities()['Unfiling']: # has not been implemented #postUrl = self.getCollectionLink(UNFILED_COLL) raise NotImplementedError else: # this repo requires fileable objects to be filed raise InvalidArgumentException return parentFolder.createDocument(name, properties, StringIO.StringIO(contentString), contentType, contentEncoding) def createDocument(self, name, properties={}, parentFolder=None, contentFile=None, contentType=None, contentEncoding=None): """ Creates a new :class:`Document` object. If the repository supports unfiled objects, you do not have to pass in a parent :class:`Folder` otherwise it is required. To create a document with an associated contentFile, pass in a File object. The method will attempt to guess the appropriate content type and encoding based on the file. To specify it yourself, pass them in via the contentType and contentEncoding arguments. >>> f = open('sample-a.pdf', 'rb') >>> doc = folder.createDocument('sample-a.pdf', contentFile=f) >>> f.close() >>> doc.getTitle() u'sample-a.pdf' The following optional arguments are not currently supported: - versioningState - policies - addACEs - removeACEs """ postUrl = '' # if you didn't pass in a parent folder if parentFolder == None: # if the repository doesn't require fileable objects to be filed if self.getCapabilities()['Unfiling']: # has not been implemented #postUrl = self.getCollectionLink(UNFILED_COLL) raise NotImplementedError else: # this repo requires fileable objects to be filed raise InvalidArgumentException else: postUrl = parentFolder.getChildrenLink() # make sure a name is set properties['cmis:name'] = name # hardcoding to cmis:document if it wasn't # passed in via props if not properties.has_key('cmis:objectTypeId'): properties['cmis:objectTypeId'] = CmisId('cmis:document') # and if it was passed in, making sure it is a CmisId elif not isinstance(properties['cmis:objectTypeId'], CmisId): properties['cmis:objectTypeId'] = CmisId(properties['cmis:objectTypeId']) # build the Atom entry xmlDoc = getEntryXmlDoc(self, None, properties, contentFile, contentType, contentEncoding) # post the Atom entry result = self._cmisClient.post(postUrl.encode('utf-8'), xmlDoc.toxml(encoding='utf-8'), ATOM_XML_ENTRY_TYPE) if type(result) == HTTPError: raise CmisException(result.code) # what comes back is the XML for the new document, # so use it to instantiate a new document # then return it return Document(self._cmisClient, self, xmlDoc=result) def createDocumentFromSource(self, sourceId, properties={}, parentFolder=None): """ This is not yet implemented. The following optional arguments are not yet supported: - versioningState - policies - addACEs - removeACEs """ # TODO: To be implemented raise NotImplementedError def createFolder(self, parentFolder, name, properties={}): """ Creates a new :class:`Folder` object in the specified parentFolder. >>> root = repo.getRootFolder() >>> folder = repo.createFolder(root, 'someFolder2') >>> folder.getTitle() u'someFolder2' >>> folder.getObjectId() u'workspace://SpacesStore/2224a63c-350b-438c-be72-8f425e79ce1f' The following optional arguments are not yet supported: - policies - addACEs - removeACEs """ return parentFolder.createFolder(name, properties) def createRelationship(self, sourceObj, targetObj, relType): """ Creates a relationship of the specific type between a source object and a target object and returns the new :class:`Relationship` object. The following optional arguments are not currently supported: - policies - addACEs - removeACEs """ return sourceObj.createRelationship(targetObj, relType) def createPolicy(self, properties): """ This has not yet been implemented. The following optional arguments are not currently supported: - folderId - policies - addACEs - removeACEs """ # TODO: To be implemented raise NotImplementedError def getUriTemplates(self): """ Returns a list of the URI templates the repository service knows about. >>> templates = repo.getUriTemplates() >>> templates['typebyid']['mediaType'] u'application/atom+xml;type=entry' >>> templates['typebyid']['template'] u'http://localhost:8080/alfresco/s/cmis/type/{id}' """ if self._uriTemplates == {}: if self.xmlDoc == None: self.reload() uriTemplateElements = self.xmlDoc.getElementsByTagNameNS(CMISRA_NS, 'uritemplate') for uriTemplateElement in uriTemplateElements: template = None templType = None mediatype = None for node in [e for e in uriTemplateElement.childNodes if e.nodeType == e.ELEMENT_NODE]: if node.localName == 'template': template = node.childNodes[0].data elif node.localName == 'type': templType = node.childNodes[0].data elif node.localName == 'mediatype': mediatype = node.childNodes[0].data self._uriTemplates[templType] = UriTemplate(template, templType, mediatype) return self._uriTemplates def getCollection(self, collectionType, **kwargs): """ Returns a list of objects returned for the specified collection. If the query collection is requested, an exception will be raised. That collection isn't meant to be retrieved. If the types collection is specified, the method returns the result of `getTypeDefinitions` and ignores any optional params passed in. >>> from cmislib.model import TYPES_COLL >>> types = repo.getCollection(TYPES_COLL) >>> len(types) 4 >>> types[0].getTypeId() u'cmis:folder' Otherwise, the collection URL is invoked, and a :class:`ResultSet` is returned. >>> from cmislib.model import CHECKED_OUT_COLL >>> resultSet = repo.getCollection(CHECKED_OUT_COLL) >>> len(resultSet.getResults()) 1 """ if collectionType == QUERY_COLL: raise NotSupportedException elif collectionType == TYPES_COLL: return self.getTypeDefinitions() result = self._cmisClient.get(self.getCollectionLink(collectionType).encode('utf-8'), **kwargs) if (type(result) == HTTPError): raise CmisException(result.code) # return the result set return ResultSet(self._cmisClient, self, result) def getCollectionLink(self, collectionType): """ Returns the link HREF from the specified collectionType ('checkedout', for example). >>> from cmislib.model import CHECKED_OUT_COLL >>> repo.getCollectionLink(CHECKED_OUT_COLL) u'http://localhost:8080/alfresco/s/cmis/checkedout' """ collectionElements = self.xmlDoc.getElementsByTagNameNS(APP_NS, 'collection') for collectionElement in collectionElements: link = collectionElement.attributes['href'].value for node in [e for e in collectionElement.childNodes if e.nodeType == e.ELEMENT_NODE]: if node.localName == 'collectionType': if node.childNodes[0].data == collectionType: return link def _getQueryXmlDoc(self, query, **kwargs): """ Utility method that knows how to build CMIS query xml around the specified query statement. """ cmisXmlDoc = minidom.Document() queryElement = cmisXmlDoc.createElementNS(CMIS_NS, "query") queryElement.setAttribute('xmlns', CMIS_NS) cmisXmlDoc.appendChild(queryElement) statementElement = cmisXmlDoc.createElementNS(CMIS_NS, "statement") cdataSection = cmisXmlDoc.createCDATASection(query) statementElement.appendChild(cdataSection) queryElement.appendChild(statementElement) for (k, v) in kwargs.items(): optionElement = cmisXmlDoc.createElementNS(CMIS_NS, k) optionText = cmisXmlDoc.createTextNode(v) optionElement.appendChild(optionText) queryElement.appendChild(optionElement) return cmisXmlDoc capabilities = property(getCapabilities) id = property(getRepositoryId) info = property(getRepositoryInfo) name = property(getRepositoryName) rootFolder = property(getRootFolder) permissionDefinitions = property(getPermissionDefinitions) permissionMap = property(getPermissionMap) propagation = property(getPropagation) supportedPermissions = property(getSupportedPermissions) class ResultSet(object): """ Represents a paged result set. In CMIS, this is most often an Atom feed. """ def __init__(self, cmisClient, repository, xmlDoc): ''' Constructor ''' self._cmisClient = cmisClient self._repository = repository self._xmlDoc = xmlDoc self._results = [] self.logger = logging.getLogger('cmislib.model.ResultSet') self.logger.info('Creating an instance of ResultSet') def __iter__(self): ''' Iterator for the result set ''' return iter(self.getResults()) def __getitem__(self, index): ''' Getter for the result set ''' return self.getResults()[index] def __len__(self): ''' Len method for the result set ''' return len(self.getResults()) def _getLink(self, rel): ''' Returns the link found in the feed's XML for the specified rel. ''' linkElements = self._xmlDoc.getElementsByTagNameNS(ATOM_NS, 'link') for linkElement in linkElements: if linkElement.attributes.has_key('rel'): relAttr = linkElement.attributes['rel'].value if relAttr == rel: return linkElement.attributes['href'].value def _getPageResults(self, rel): ''' Given a specified rel, does a get using that link (if one exists) and then converts the resulting XML into a dictionary of :class:`CmisObject` objects or its appropriate sub-type. The results are kept around to facilitate repeated calls without moving the cursor. ''' link = self._getLink(rel) if link: result = self._cmisClient.get(link.encode('utf-8')) if (type(result) == HTTPError): raise CmisException(result.code) # return the result self._xmlDoc = result self._results = [] return self.getResults() def reload(self): ''' Re-invokes the self link for the current set of results. >>> resultSet = repo.getCollection(CHECKED_OUT_COLL) >>> resultSet.reload() ''' self.logger.debug('Reload called on result set') self._getPageResults(SELF_REL) def getResults(self): ''' Returns the results that were fetched and cached by the get*Page call. >>> resultSet = repo.getCheckedOutDocs() >>> resultSet.hasNext() False >>> for result in resultSet.getResults(): ... result ... ''' if self._results: return self._results if self._xmlDoc: entryElements = self._xmlDoc.getElementsByTagNameNS(ATOM_NS, 'entry') entries = [] for entryElement in entryElements: cmisObject = getSpecializedObject(CmisObject(self._cmisClient, self._repository, xmlDoc=entryElement)) entries.append(cmisObject) self._results = entries return self._results def hasObject(self, objectId): ''' Returns True if the specified objectId is found in the list of results, otherwise returns False. ''' for obj in self.getResults(): if obj.id == objectId: return True return False def getFirst(self): ''' Returns the first page of results as a dictionary of :class:`CmisObject` objects or its appropriate sub-type. This only works when the server returns a "first" link. Not all of them do. >>> resultSet.hasFirst() True >>> results = resultSet.getFirst() >>> for result in results: ... result ... ''' return self._getPageResults(FIRST_REL) def getPrev(self): ''' Returns the prev page of results as a dictionary of :class:`CmisObject` objects or its appropriate sub-type. This only works when the server returns a "prev" link. Not all of them do. >>> resultSet.hasPrev() True >>> results = resultSet.getPrev() >>> for result in results: ... result ... ''' return self._getPageResults(PREV_REL) def getNext(self): ''' Returns the next page of results as a dictionary of :class:`CmisObject` objects or its appropriate sub-type. >>> resultSet.hasNext() True >>> results = resultSet.getNext() >>> for result in results: ... result ... ''' return self._getPageResults(NEXT_REL) def getLast(self): ''' Returns the last page of results as a dictionary of :class:`CmisObject` objects or its appropriate sub-type. This only works when the server is returning a "last" link. Not all of them do. >>> resultSet.hasLast() True >>> results = resultSet.getLast() >>> for result in results: ... result ... ''' return self._getPageResults(LAST_REL) def hasNext(self): ''' Returns True if this page contains a next link. >>> resultSet.hasNext() True ''' if self._getLink(NEXT_REL): return True else: return False def hasPrev(self): ''' Returns True if this page contains a prev link. Not all CMIS providers implement prev links consistently. >>> resultSet.hasPrev() True ''' if self._getLink(PREV_REL): return True else: return False def hasFirst(self): ''' Returns True if this page contains a first link. Not all CMIS providers implement first links consistently. >>> resultSet.hasFirst() True ''' if self._getLink(FIRST_REL): return True else: return False def hasLast(self): ''' Returns True if this page contains a last link. Not all CMIS providers implement last links consistently. >>> resultSet.hasLast() True ''' if self._getLink(LAST_REL): return True else: return False class CmisObject(object): """ Common ancestor class for other CMIS domain objects such as :class:`Document` and :class:`Folder`. """ def __init__(self, cmisClient, repository, objectId=None, xmlDoc=None, **kwargs): """ Constructor """ self._cmisClient = cmisClient self._repository = repository self._objectId = objectId self._name = None self._properties = {} self._allowableActions = {} self.xmlDoc = xmlDoc self._kwargs = kwargs self.logger = logging.getLogger('cmislib.model.CmisObject') self.logger.info('Creating an instance of CmisObject') def __str__(self): """To string""" return self.getObjectId() def reload(self, **kwargs): """ Fetches the latest representation of this object from the CMIS service. Some methods, like :class:`^Document.checkout` do this for you. If you call reload with a properties filter, the filter will be in effect on subsequent calls until the filter argument is changed. To reset to the full list of properties, call reload with filter set to '*'. """ self.logger.debug('Reload called on CmisObject') if kwargs: if self._kwargs: self._kwargs.update(kwargs) else: self._kwargs = kwargs templates = self._repository.getUriTemplates() template = templates['objectbyid']['template'] # Doing some refactoring here. Originally, we snagged the template # and then "filled in" the template based on the args passed in. # However, some servers don't provide a full template which meant # supported optional args wouldn't get passed in using the fill-the- # template approach. What's going on now is that the template gets # filled in where it can, but if additional, non-templated args are # passed in, those will get tacked on to the query string as # "additional" options. params = { '{id}': self.getObjectId(), '{filter}': '', '{includeAllowableActions}': 'false', '{includePolicyIds}': 'false', '{includeRelationships}': '', '{includeACL}': 'false', '{renditionFilter}': ''} options = {} addOptions = {} # args specified, but not in the template for k, v in self._kwargs.items(): pKey = "{" + k + "}" if template.find(pKey) >= 0: options[pKey] = toCMISValue(v) else: addOptions[k] = toCMISValue(v) # merge the templated args with the default params params.update(options) # fill in the template byObjectIdUrl = multiple_replace(params, template) self.xmlDoc = self._cmisClient.get(byObjectIdUrl.encode('utf-8'), **addOptions) self._initData() # if a returnVersion arg was passed in, it is possible we got back # a different object ID than the value we started with, so it needs # to be cleared out as well if options.has_key('returnVersion') or addOptions.has_key('returnVersion'): self._objectId = None def _initData(self): """ An internal method used to clear out any member variables that might be out of sync if we were to fetch new XML from the service. """ self._properties = {} self._name = None self._allowableActions = {} def getObjectId(self): """ Returns the object ID for this object. >>> doc = resultSet.getResults()[0] >>> doc.getObjectId() u'workspace://SpacesStore/dc26102b-e312-471b-b2af-91bfb0225339' """ if self._objectId == None: if self.xmlDoc == None: self.logger.debug('Both objectId and xmlDoc were None, reloading') self.reload() props = self.getProperties() self._objectId = CmisId(props['cmis:objectId']) return self._objectId def getObjectParents(self, **kwargs): """ Gets the parents of this object as a :class:`ResultSet`. The following optional arguments are supported: - filter - includeRelationships - renditionFilter - includeAllowableActions - includeRelativePathSegment """ # get the appropriate 'up' link parentUrl = self._getLink(UP_REL) if parentUrl == None: raise NotSupportedException('Root folder does not support getObjectParents') # invoke the URL result = self._cmisClient.get(parentUrl.encode('utf-8'), **kwargs) if type(result) == HTTPError: raise CmisException(result.code) # return the result set return ResultSet(self._cmisClient, self._repository, result) def getPaths(self): """ Returns the object's paths as a list of strings. """ # see sub-classes for implementation pass def getAllowableActions(self): """ Returns a dictionary of allowable actions, keyed off of the action name. >>> actions = doc.getAllowableActions() >>> for a in actions: ... print "%s:%s" % (a,actions[a]) ... canDeleteContentStream:True canSetContentStream:True canCreateRelationship:True canCheckIn:False canApplyACL:False canDeleteObject:True canGetAllVersions:True canGetObjectParents:True canGetProperties:True """ if self._allowableActions == {}: self.reload(includeAllowableActions=True) allowElements = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'allowableActions') assert len(allowElements) == 1, "Expected response to have exactly one allowableActions element" allowElement = allowElements[0] for node in [e for e in allowElement.childNodes if e.nodeType == e.ELEMENT_NODE]: actionName = node.localName actionValue = parseBoolValue(node.childNodes[0].data) self._allowableActions[actionName] = actionValue return self._allowableActions def getTitle(self): """ Returns the value of the object's cmis:title property. """ if self.xmlDoc == None: self.reload() titleElement = self.xmlDoc.getElementsByTagNameNS(ATOM_NS, 'title')[0] if titleElement and titleElement.childNodes: return titleElement.childNodes[0].data def getProperties(self): """ Returns a dict of the object's properties. If CMIS returns an empty element for a property, the property will be in the dict with a value of None. >>> props = doc.getProperties() >>> for p in props: ... print "%s: %s" % (p, props[p]) ... cmis:contentStreamMimeType: text/html cmis:creationDate: 2009-12-15T09:45:35.369-06:00 cmis:baseTypeId: cmis:document cmis:isLatestMajorVersion: false cmis:isImmutable: false cmis:isMajorVersion: false cmis:objectId: workspace://SpacesStore/dc26102b-e312-471b-b2af-91bfb0225339 The optional filter argument is not yet implemented. """ #TODO implement filter if self._properties == {}: if self.xmlDoc == None: self.reload() propertiesElement = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'properties')[0] #cpattern = re.compile(r'^property([\w]*)') for node in [e for e in propertiesElement.childNodes if e.nodeType == e.ELEMENT_NODE and e.namespaceURI == CMIS_NS]: #propertyId, propertyString, propertyDateTime #propertyType = cpattern.search(node.localName).groups()[0] propertyName = node.attributes['propertyDefinitionId'].value if node.childNodes and \ node.getElementsByTagNameNS(CMIS_NS, 'value')[0] and \ node.getElementsByTagNameNS(CMIS_NS, 'value')[0].childNodes: valNodeList = node.getElementsByTagNameNS(CMIS_NS, 'value') if (len(valNodeList) == 1): propertyValue = parsePropValue(valNodeList[0]. childNodes[0].data, node.localName) else: propertyValue = [] for valNode in valNodeList: propertyValue.append(parsePropValue(valNode. childNodes[0].data, node.localName)) else: propertyValue = None self._properties[propertyName] = propertyValue for node in [e for e in self.xmlDoc.childNodes if e.nodeType == e.ELEMENT_NODE and e.namespaceURI == CMISRA_NS]: propertyName = node.nodeName if node.childNodes: propertyValue = node.firstChild.nodeValue else: propertyValue = None self._properties[propertyName] = propertyValue return self._properties def getName(self): """ Returns the value of cmis:name from the getProperties() dictionary. We don't need a getter for every standard CMIS property, but name is a pretty common one so it seems to make sense. >>> doc.getName() u'system-overview.html' """ if self._name == None: self._name = self.getProperties()['cmis:name'] return self._name def updateProperties(self, properties): """ Updates the properties of an object with the properties provided. Only provide the set of properties that need to be updated. >>> folder = repo.getObjectByPath('/someFolder2') >>> folder.getName() u'someFolder2' >>> props = {'cmis:name': 'someFolderFoo'} >>> folder.updateProperties(props) >>> folder.getName() u'someFolderFoo' """ self.logger.debug('Inside updateProperties') # get the self link selfUrl = self._getSelfLink() # if we have a change token, we must pass it back, per the spec args = {} if (self.properties.has_key('cmis:changeToken') and self.properties['cmis:changeToken'] != None): self.logger.debug('Change token present, adding it to args') args = {"changeToken": self.properties['cmis:changeToken']} # the getEntryXmlDoc function may need the object type objectTypeId = None if (self.properties.has_key('cmis:objectTypeId') and not properties.has_key('cmis:objectTypeId')): objectTypeId = self.properties['cmis:objectTypeId'] self.logger.debug('This object type is:%s' % objectTypeId) # build the entry based on the properties provided xmlEntryDoc = getEntryXmlDoc(self._repository, objectTypeId, properties) self.logger.debug('xmlEntryDoc:' + xmlEntryDoc.toxml()) # do a PUT of the entry updatedXmlDoc = self._cmisClient.put(selfUrl.encode('utf-8'), xmlEntryDoc.toxml(encoding='utf-8'), ATOM_XML_TYPE, **args) # reset the xmlDoc for this object with what we got back from # the PUT, then call initData we dont' want to call # self.reload because we've already got the parsed XML-- # there's no need to fetch it again self.xmlDoc = updatedXmlDoc self._initData() return self def move(self, sourceFolder, targetFolder): """ Moves an object from the source folder to the target folder. >>> sub1 = repo.getObjectByPath('/cmislib/sub1') >>> sub2 = repo.getObjectByPath('/cmislib/sub2') >>> doc = repo.getObjectByPath('/cmislib/sub1/testdoc1') >>> doc.move(sub1, sub2) """ postUrl = targetFolder.getChildrenLink() args = {"sourceFolderId": sourceFolder.id} # post the Atom entry result = self._cmisClient.post(postUrl.encode('utf-8'), self.xmlDoc.toxml(encoding='utf-8'), ATOM_XML_ENTRY_TYPE, **args) if type(result) == HTTPError: raise CmisException(result.code) def delete(self, **kwargs): """ Deletes this :class:`CmisObject` from the repository. Note that in the case of a :class:`Folder` object, some repositories will refuse to delete it if it contains children and some will delete it without complaint. If what you really want to do is delete the folder and all of its descendants, use :meth:`~Folder.deleteTree` instead. >>> folder.delete() The optional allVersions argument is supported. """ url = self._getSelfLink() result = self._cmisClient.delete(url.encode('utf-8'), **kwargs) if type(result) == HTTPError: raise CmisException(result.code) def applyPolicy(self, policyId): """ This is not yet implemented. """ # depends on this object's canApplyPolicy allowable action if self.getAllowableActions()['canApplyPolicy']: raise NotImplementedError else: raise CmisException('This object has canApplyPolicy set to false') def createRelationship(self, targetObj, relTypeId): """ Creates a relationship between this object and a specified target object using the relationship type specified. Returns the new :class:`Relationship` object. >>> rel = tstDoc1.createRelationship(tstDoc2, 'R:cmiscustom:assoc') >>> rel.getProperties() {u'cmis:objectId': u'workspace://SpacesStore/271c48dd-6548-4771-a8f5-0de69b7cdc25', u'cmis:creationDate': None, u'cmis:objectTypeId': u'R:cmiscustom:assoc', u'cmis:lastModificationDate': None, u'cmis:targetId': u'workspace://SpacesStore/0ca1aa08-cb49-42e2-8881-53aa8496a1c1', u'cmis:lastModifiedBy': None, u'cmis:baseTypeId': u'cmis:relationship', u'cmis:sourceId': u'workspace://SpacesStore/271c48dd-6548-4771-a8f5-0de69b7cdc25', u'cmis:changeToken': None, u'cmis:createdBy': None} """ if isinstance(relTypeId, str): relTypeId = CmisId(relTypeId) props = {} props['cmis:sourceId'] = self.getObjectId() props['cmis:targetId'] = targetObj.getObjectId() props['cmis:objectTypeId'] = relTypeId xmlDoc = getEntryXmlDoc(self._repository, properties=props) url = self._getLink(RELATIONSHIPS_REL) assert url != None, 'Could not determine relationships URL' result = self._cmisClient.post(url.encode('utf-8'), xmlDoc.toxml(encoding='utf-8'), ATOM_XML_TYPE) if type(result) == HTTPError: raise CmisException(result.code) # instantiate CmisObject objects with the results and return the list entryElements = result.getElementsByTagNameNS(ATOM_NS, 'entry') assert(len(entryElements) == 1), "Expected entry element in result from relationship URL post" return getSpecializedObject(CmisObject(self._cmisClient, self, xmlDoc=entryElements[0])) def getRelationships(self, **kwargs): """ Returns a :class:`ResultSet` of :class:`Relationship` objects for each relationship where the source is this object. >>> rels = tstDoc1.getRelationships() >>> len(rels.getResults()) 1 >>> rel = rels.getResults().values()[0] >>> rel.getProperties() {u'cmis:objectId': u'workspace://SpacesStore/271c48dd-6548-4771-a8f5-0de69b7cdc25', u'cmis:creationDate': None, u'cmis:objectTypeId': u'R:cmiscustom:assoc', u'cmis:lastModificationDate': None, u'cmis:targetId': u'workspace://SpacesStore/0ca1aa08-cb49-42e2-8881-53aa8496a1c1', u'cmis:lastModifiedBy': None, u'cmis:baseTypeId': u'cmis:relationship', u'cmis:sourceId': u'workspace://SpacesStore/271c48dd-6548-4771-a8f5-0de69b7cdc25', u'cmis:changeToken': None, u'cmis:createdBy': None} The following optional arguments are supported: - includeSubRelationshipTypes - relationshipDirection - typeId - maxItems - skipCount - filter - includeAllowableActions """ url = self._getLink(RELATIONSHIPS_REL) assert url != None, 'Could not determine relationships URL' result = self._cmisClient.get(url.encode('utf-8'), **kwargs) if type(result) == HTTPError: raise CmisException(result.code) # return the result set return ResultSet(self._cmisClient, self._repository, result) def removePolicy(self, policyId): """ This is not yet implemented. """ # depends on this object's canRemovePolicy allowable action if self.getAllowableActions()['canRemovePolicy']: raise NotImplementedError else: raise CmisException('This object has canRemovePolicy set to false') def getAppliedPolicies(self): """ This is not yet implemented. """ # depends on this object's canGetAppliedPolicies allowable action if self.getAllowableActions()['canGetAppliedPolicies']: raise NotImplementedError else: raise CmisException('This object has canGetAppliedPolicies set to false') def getACL(self): """ Repository.getCapabilities['ACL'] must return manage or discover. >>> acl = folder.getACL() >>> acl.getEntries() {u'GROUP_EVERYONE': , 'jdoe': } The optional onlyBasicPermissions argument is currently not supported. """ if self._repository.getCapabilities()['ACL']: # if the ACL capability is discover or manage, this must be # supported aclUrl = self._getLink(ACL_REL) result = self._cmisClient.get(aclUrl.encode('utf-8')) if type(result) == HTTPError: raise CmisException(result.code) return ACL(xmlDoc=result) else: raise NotSupportedException def applyACL(self, acl): """ Updates the object with the provided :class:`ACL`. Repository.getCapabilities['ACL'] must return manage to invoke this call. >>> acl = folder.getACL() >>> acl.addEntry(ACE('jdoe', 'cmis:write', 'true')) >>> acl.getEntries() {u'GROUP_EVERYONE': , 'jdoe': } """ if self._repository.getCapabilities()['ACL'] == 'manage': # if the ACL capability is manage, this must be # supported # but it also depends on the canApplyACL allowable action # for this object if not isinstance(acl, ACL): raise CmisException('The ACL to apply must be an instance of the ACL class.') aclUrl = self._getLink(ACL_REL) assert aclUrl, "Could not determine the object's ACL URL." result = self._cmisClient.put(aclUrl.encode('utf-8'), acl.getXmlDoc().toxml(encoding='utf-8'), CMIS_ACL_TYPE) if type(result) == HTTPError: raise CmisException(result.code) return ACL(xmlDoc=result) else: raise NotSupportedException def _getSelfLink(self): """ Returns the URL used to retrieve this object. """ url = self._getLink(SELF_REL) assert len(url) > 0, "Could not determine the self link." return url def _getLink(self, rel, ltype=None): """ Returns the HREF attribute of an Atom link element for the specified rel. """ if self.xmlDoc == None: self.reload() linkElements = self.xmlDoc.getElementsByTagNameNS(ATOM_NS, 'link') for linkElement in linkElements: if ltype: if linkElement.attributes.has_key('rel'): relAttr = linkElement.attributes['rel'].value if ltype and linkElement.attributes.has_key('type'): typeAttr = linkElement.attributes['type'].value if relAttr == rel and ltype.match(typeAttr): return linkElement.attributes['href'].value else: if linkElement.attributes.has_key('rel'): relAttr = linkElement.attributes['rel'].value if relAttr == rel: return linkElement.attributes['href'].value allowableActions = property(getAllowableActions) name = property(getName) id = property(getObjectId) properties = property(getProperties) title = property(getTitle) ACL = property(getACL) class Document(CmisObject): """ An object typically associated with file content. """ def checkout(self): """ Performs a checkout on the :class:`Document` and returns the Private Working Copy (PWC), which is also an instance of :class:`Document` >>> doc.getObjectId() u'workspace://SpacesStore/f0c8b90f-bec0-4405-8b9c-2ab570589808;1.0' >>> doc.isCheckedOut() False >>> pwc = doc.checkout() >>> doc.isCheckedOut() True """ # get the checkedout collection URL checkoutUrl = self._repository.getCollectionLink(CHECKED_OUT_COLL) assert len(checkoutUrl) > 0, "Could not determine the checkedout collection url." # get this document's object ID # build entry XML with it properties = {'cmis:objectId': self.getObjectId()} entryXmlDoc = getEntryXmlDoc(self._repository, properties=properties) # post it to to the checkedout collection URL result = self._cmisClient.post(checkoutUrl.encode('utf-8'), entryXmlDoc.toxml(encoding='utf-8'), ATOM_XML_ENTRY_TYPE) if type(result) == HTTPError: raise CmisException(result.code) # now that the doc is checked out, we need to refresh the XML # to pick up the prop updates related to a checkout self.reload() return Document(self._cmisClient, self._repository, xmlDoc=result) def cancelCheckout(self): """ Cancels the checkout of this object by retrieving the Private Working Copy (PWC) and then deleting it. After the PWC is deleted, this object will be reloaded to update properties related to a checkout. >>> doc.isCheckedOut() True >>> doc.cancelCheckout() >>> doc.isCheckedOut() False """ pwcDoc = self.getPrivateWorkingCopy() if pwcDoc: pwcDoc.delete() self.reload() def getPrivateWorkingCopy(self): """ Retrieves the object using the object ID in the property: cmis:versionSeriesCheckedOutId then uses getObject to instantiate the object. >>> doc.isCheckedOut() False >>> doc.checkout() >>> pwc = doc.getPrivateWorkingCopy() >>> pwc.getTitle() u'sample-b (Working Copy).pdf' """ # reloading the document just to make sure we've got the latest # and greatest PWC ID self.reload() pwcDocId = self.getProperties()['cmis:versionSeriesCheckedOutId'] if pwcDocId: return self._repository.getObject(pwcDocId) def isCheckedOut(self): """ Returns true if the document is checked out. >>> doc.isCheckedOut() True >>> doc.cancelCheckout() >>> doc.isCheckedOut() False """ # reloading the document just to make sure we've got the latest # and greatest checked out prop self.reload() return parseBoolValue(self.getProperties()['cmis:isVersionSeriesCheckedOut']) def getCheckedOutBy(self): """ Returns the ID who currently has the document checked out. >>> pwc = doc.checkout() >>> pwc.getCheckedOutBy() u'admin' """ # reloading the document just to make sure we've got the latest # and greatest checked out prop self.reload() return self.getProperties()['cmis:versionSeriesCheckedOutBy'] def checkin(self, checkinComment=None, **kwargs): """ Checks in this :class:`Document` which must be a private working copy (PWC). >>> doc.isCheckedOut() False >>> pwc = doc.checkout() >>> doc.isCheckedOut() True >>> pwc.checkin() >>> doc.isCheckedOut() False The following optional arguments are supported: - major - properties - contentStream - policies - addACEs - removeACEs """ # Add checkin to kwargs and checkinComment, if it exists kwargs['checkin'] = 'true' kwargs['checkinComment'] = checkinComment # Build an empty ATOM entry entryXmlDoc = getEmptyXmlDoc() # Get the self link # Do a PUT of the empty ATOM to the self link url = self._getSelfLink() result = self._cmisClient.put(url.encode('utf-8'), entryXmlDoc.toxml(encoding='utf-8'), ATOM_XML_TYPE, **kwargs) if type(result) == HTTPError: raise CmisException(result.code) return Document(self._cmisClient, self._repository, xmlDoc=result) def getLatestVersion(self, **kwargs): """ Returns a :class:`Document` object representing the latest version in the version series. The following optional arguments are supported: - major - filter - includeRelationships - includePolicyIds - renditionFilter - includeACL - includeAllowableActions >>> latestDoc = doc.getLatestVersion() >>> latestDoc.getProperties()['cmis:versionLabel'] u'2.1' >>> latestDoc = doc.getLatestVersion(major='false') >>> latestDoc.getProperties()['cmis:versionLabel'] u'2.1' >>> latestDoc = doc.getLatestVersion(major='true') >>> latestDoc.getProperties()['cmis:versionLabel'] u'2.0' """ doc = None if kwargs.has_key('major') and kwargs['major'] == 'true': doc = self._repository.getObject(self.getObjectId(), returnVersion='latestmajor') else: doc = self._repository.getObject(self.getObjectId(), returnVersion='latest') return doc def getPropertiesOfLatestVersion(self, **kwargs): """ Like :class:`^CmisObject.getProperties`, returns a dict of properties from the latest version of this object in the version series. The optional major and filter arguments are supported. """ latestDoc = self.getLatestVersion(**kwargs) return latestDoc.getProperties() def getAllVersions(self, **kwargs): """ Returns a :class:`ResultSet` of document objects for the entire version history of this object, including any PWC's. The optional filter and includeAllowableActions are supported. """ # get the version history link versionsUrl = self._getLink(VERSION_HISTORY_REL) # invoke the URL result = self._cmisClient.get(versionsUrl.encode('utf-8'), **kwargs) if type(result) == HTTPError: raise CmisException(result.code) # return the result set return ResultSet(self._cmisClient, self._repository, result) def getContentStream(self): """ Returns the CMIS service response from invoking the 'enclosure' link. >>> doc.getName() u'sample-b.pdf' >>> o = open('tmp.pdf', 'wb') >>> result = doc.getContentStream() >>> o.write(result.read()) >>> result.close() >>> o.close() >>> import os.path >>> os.path.getsize('tmp.pdf') 117248 The optional streamId argument is not yet supported. """ # TODO: Need to implement the streamId contentElements = self.xmlDoc.getElementsByTagNameNS(ATOM_NS, 'content') assert(len(contentElements) == 1), 'Expected to find exactly one atom:content element.' # if the src element exists, follow that if contentElements[0].attributes.has_key('src'): srcUrl = contentElements[0].attributes['src'].value # the cmis client class parses non-error responses result = Rest().get(srcUrl.encode('utf-8'), username=self._cmisClient.username, password=self._cmisClient.password, **self._cmisClient.extArgs) if result.code != 200: raise CmisException(result.code) return result else: # otherwise, try to return the value of the content element if contentElements[0].childNodes: return contentElements[0].childNodes[0].data def setContentStream(self, contentFile, contentType=None): """ Sets the content stream on this object. The following optional arguments are not yet supported: - overwriteFlag=None """ # get this object's content stream link contentElements = self.xmlDoc.getElementsByTagNameNS(ATOM_NS, 'content') assert(len(contentElements) == 1), 'Expected to find exactly one atom:content element.' # if the src element exists, follow that if contentElements[0].attributes.has_key('src'): srcUrl = contentElements[0].attributes['src'].value # there may be times when this URL is absent, but I'm not sure how to # set the content stream when that is the case assert(srcUrl), 'Unable to determine content stream URL.' # need to determine the mime type mimetype = contentType if not mimetype and hasattr(contentFile, 'name'): mimetype, encoding = mimetypes.guess_type(contentFile.name) if not mimetype: mimetype = 'application/binary' # if we have a change token, we must pass it back, per the spec args = {} if (self.properties.has_key('cmis:changeToken') and self.properties['cmis:changeToken'] != None): self.logger.debug('Change token present, adding it to args') args = {"changeToken": self.properties['cmis:changeToken']} # put the content file result = self._cmisClient.put(srcUrl.encode('utf-8'), contentFile.read(), mimetype, **args) if type(result) == HTTPError: raise CmisException(result.code) # what comes back is the XML for the updated document, # which is not required by the spec to be the same document # we just updated, so use it to instantiate a new document # then return it return Document(self._cmisClient, self._repository, xmlDoc=result) def deleteContentStream(self): """ Delete's the content stream associated with this object. """ # get this object's content stream link contentElements = self.xmlDoc.getElementsByTagNameNS(ATOM_NS, 'content') assert(len(contentElements) == 1), 'Expected to find exactly one atom:content element.' # if the src element exists, follow that if contentElements[0].attributes.has_key('src'): srcUrl = contentElements[0].attributes['src'].value # there may be times when this URL is absent, but I'm not sure how to # delete the content stream when that is the case assert(srcUrl), 'Unable to determine content stream URL.' # if we have a change token, we must pass it back, per the spec args = {} if (self.properties.has_key('cmis:changeToken') and self.properties['cmis:changeToken'] != None): self.logger.debug('Change token present, adding it to args') args = {"changeToken": self.properties['cmis:changeToken']} # delete the content stream result = self._cmisClient.delete(srcUrl.encode('utf-8'), **args) if type(result) == HTTPError: raise CmisException(result.code) def getRenditions(self): """ Returns an array of :class:`Rendition` objects. The repository must support the Renditions capability. The following optional arguments are not currently supported: - renditionFilter - maxItems - skipCount """ # if Renditions capability is None, return notsupported if self._repository.getCapabilities()['Renditions']: pass else: raise NotSupportedException if self.xmlDoc == None: self.reload() linkElements = self.xmlDoc.getElementsByTagNameNS(ATOM_NS, 'link') renditions = [] for linkElement in linkElements: if linkElement.attributes.has_key('rel'): relAttr = linkElement.attributes['rel'].value if relAttr == RENDITION_REL: renditions.append(Rendition(linkElement)) return renditions checkedOut = property(isCheckedOut) def getPaths(self): """ Returns the Document's paths by asking for the parents with the includeRelativePathSegment flag set to true, then concats the value of cmis:path with the relativePathSegment. """ # get the appropriate 'up' link parentUrl = self._getLink(UP_REL) if parentUrl == None: raise NotSupportedException('Root folder does not support getObjectParents') # invoke the URL result = self._cmisClient.get(parentUrl.encode('utf-8'), filter='cmis:path', includeRelativePathSegment=True) if type(result) == HTTPError: raise CmisException(result.code) paths = [] rs = ResultSet(self._cmisClient, self._repository, result) for res in rs: path = res.properties['cmis:path'] relativePathSegment = res.properties['cmisra:relativePathSegment'] # concat with a slash # add it to the list paths.append(path + '/' + relativePathSegment) return paths class Folder(CmisObject): """ A container object that can hold other :class:`CmisObject` objects """ def createFolder(self, name, properties={}): """ Creates a new :class:`Folder` using the properties provided. Right now I expect a property called 'cmis:name' but I don't complain if it isn't there (although the CMIS provider will). If a cmis:name property isn't provided, the value passed in to the name argument will be used. To specify a custom folder type, pass in a property called cmis:objectTypeId set to the :class:`CmisId` representing the type ID of the instance you want to create. If you do not pass in an object type ID, an instance of 'cmis:folder' will be created. >>> subFolder = folder.createFolder('someSubfolder') >>> subFolder.getName() u'someSubfolder' The following optional arguments are not supported: - policies - addACEs - removeACEs """ # get the folder represented by folderId. # we'll use his 'children' link post the new child postUrl = self.getChildrenLink() # make sure the name property gets set properties['cmis:name'] = name # hardcoding to cmis:folder if it wasn't passed in via props if not properties.has_key('cmis:objectTypeId'): properties['cmis:objectTypeId'] = CmisId('cmis:folder') # and checking to make sure the object type ID is an instance of CmisId elif not isinstance(properties['cmis:objectTypeId'], CmisId): properties['cmis:objectTypeId'] = CmisId(properties['cmis:objectTypeId']) # build the Atom entry entryXml = getEntryXmlDoc(self._repository, properties=properties) # post the Atom entry result = self._cmisClient.post(postUrl.encode('utf-8'), entryXml.toxml(encoding='utf-8'), ATOM_XML_ENTRY_TYPE) if type(result) == HTTPError: raise CmisException(result.code) # what comes back is the XML for the new folder, # so use it to instantiate a new folder then return it return Folder(self._cmisClient, self._repository, xmlDoc=result) def createDocumentFromString(self, name, properties={}, contentString=None, contentType=None, contentEncoding=None): """ Creates a new document setting the content to the string provided. If the repository supports unfiled objects, you do not have to pass in a parent :class:`Folder` otherwise it is required. This method is essentially a convenience method that wraps your string with a StringIO and then calls createDocument. >>> testFolder.createDocumentFromString('testdoc3', contentString='hello, world', contentType='text/plain') """ return self._repository.createDocumentFromString(name, properties, self, contentString, contentType, contentEncoding) def createDocument(self, name, properties={}, contentFile=None, contentType=None, contentEncoding=None): """ Creates a new Document object in the repository using the properties provided. Right now this is basically the same as createFolder, but this deals with contentStreams. The common logic should probably be moved to CmisObject.createObject. The method will attempt to guess the appropriate content type and encoding based on the file. To specify it yourself, pass them in via the contentType and contentEncoding arguments. >>> f = open('250px-Cmis_logo.png', 'rb') >>> subFolder.createDocument('logo.png', contentFile=f) >>> f.close() If you wanted to set one or more properties when creating the doc, pass in a dict, like this: >>> props = {'cmis:someProp':'someVal'} >>> f = open('250px-Cmis_logo.png', 'rb') >>> subFolder.createDocument('logo.png', props, contentFile=f) >>> f.close() To specify a custom object type, pass in a property called cmis:objectTypeId set to the :class:`CmisId` representing the type ID of the instance you want to create. If you do not pass in an object type ID, an instance of 'cmis:document' will be created. The following optional arguments are not yet supported: - versioningState - policies - addACEs - removeACEs """ return self._repository.createDocument(name, properties, self, contentFile, contentType, contentEncoding) def getChildren(self, **kwargs): """ Returns a paged :class:`ResultSet`. The result set contains a list of :class:`CmisObject` objects for each child of the Folder. The actual type of the object returned depends on the object's CMIS base type id. For example, the method might return a list that contains both :class:`Document` objects and :class:`Folder` objects. >>> childrenRS = subFolder.getChildren() >>> children = childrenRS.getResults() The following optional arguments are supported: - maxItems - skipCount - orderBy - filter - includeRelationships - renditionFilter - includeAllowableActions - includePathSegment """ # get the appropriate 'down' link childrenUrl = self.getChildrenLink() # invoke the URL result = self._cmisClient.get(childrenUrl.encode('utf-8'), **kwargs) if type(result) == HTTPError: raise CmisException(result.code) # return the result set return ResultSet(self._cmisClient, self._repository, result) def getChildrenLink(self): """ Gets the Atom link that knows how to return this object's children. """ url = self._getLink(DOWN_REL, ATOM_XML_FEED_TYPE_P) assert len(url) > 0, "Could not find the children url" return url def getDescendantsLink(self): """ Returns the 'down' link of type `CMIS_TREE_TYPE` >>> folder.getDescendantsLink() u'http://localhost:8080/alfresco/s/cmis/s/workspace:SpacesStore/i/86f6bf54-f0e8-4a72-8cb1-213599ba086c/descendants' """ url = self._getLink(DOWN_REL, CMIS_TREE_TYPE_P) assert len(url) > 0, "Could not find the descendants url" # some servers return a depth arg as part of this URL # so strip it off but keep other args if url.find("?") >= 0: u = list(urlparse(url)) u[4] = '&'.join([p for p in u[4].split('&') if not p.startswith('depth=')]) url = urlunparse(u) return url def getDescendants(self, **kwargs): """ Gets the descendants of this folder. The descendants are returned as a paged :class:`ResultSet` object. The result set contains a list of :class:`CmisObject` objects where the actual type of each object returned will vary depending on the object's base type id. For example, the method might return a list that contains both :class:`Document` objects and :class:`Folder` objects. The following optional argument is supported: - depth. Use depth=-1 for all descendants, which is the default if no depth is specified. >>> resultSet = folder.getDescendants() >>> len(resultSet.getResults()) 105 >>> resultSet = folder.getDescendants(depth=1) >>> len(resultSet.getResults()) 103 The following optional arguments *may* also work but haven't been tested: - filter - includeRelationships - renditionFilter - includeAllowableActions - includePathSegment """ if not self._repository.getCapabilities()['GetDescendants']: raise NotSupportedException('This repository does not support getDescendants') # default the depth to -1, which is all descendants if "depth" not in kwargs: kwargs['depth'] = -1 # get the appropriate 'down' link descendantsUrl = self.getDescendantsLink() # invoke the URL result = self._cmisClient.get(descendantsUrl.encode('utf-8'), **kwargs) if type(result) == HTTPError: raise CmisException(result.code) # return the result set return ResultSet(self._cmisClient, self._repository, result) def getTree(self, **kwargs): """ Unlike :class:`Folder.getChildren` or :class:`Folder.getDescendants`, this method returns only the descendant objects that are folders. The results do not include the current folder. The following optional arguments are supported: - depth - filter - includeRelationships - renditionFilter - includeAllowableActions - includePathSegment >>> rs = folder.getTree(depth='2') >>> len(rs.getResults()) 3 >>> for folder in rs.getResults().values(): ... folder.getTitle() ... u'subfolder2' u'parent test folder' u'subfolder' """ # Get the descendants link and do a GET against it url = self._getLink(FOLDER_TREE_REL) assert url != None, 'Unable to determine folder tree link' result = self._cmisClient.get(url.encode('utf-8'), **kwargs) if type(result) == HTTPError: raise CmisException(result.code) # return the result set return ResultSet(self._cmisClient, self, result) def getParent(self): """ This is not yet implemented. The optional filter argument is not yet supported. """ # get the appropriate 'up' link parentUrl = self._getLink(UP_REL) # invoke the URL result = self._cmisClient.get(parentUrl.encode('utf-8')) if type(result) == HTTPError: raise CmisException(result.code) # return the result set return Folder(self._cmisClient, self._repository, xmlDoc=result) def deleteTree(self, **kwargs): """ Deletes the folder and all of its descendant objects. >>> resultSet = subFolder.getDescendants() >>> len(resultSet.getResults()) 2 >>> subFolder.deleteTree() The following optional arguments are supported: - allVersions - unfileObjects - continueOnFailure """ # Per the spec, the repo must have the GetDescendants capability # to support deleteTree if not self._repository.getCapabilities()['GetDescendants']: raise NotSupportedException('This repository does not support deleteTree') # Get the descendants link and do a DELETE against it url = self._getLink(DOWN_REL, CMIS_TREE_TYPE_P) result = self._cmisClient.delete(url.encode('utf-8'), **kwargs) if type(result) == HTTPError: raise CmisException(result.code) def addObject(self, cmisObject, **kwargs): """ Adds the specified object as a child of this object. No new object is created. The repository must support multifiling for this to work. >>> sub1 = repo.getObjectByPath("/cmislib/sub1") >>> sub2 = repo.getObjectByPath("/cmislib/sub2") >>> doc = sub1.createDocument("testdoc1") >>> len(sub1.getChildren()) 1 >>> len(sub2.getChildren()) 0 >>> sub2.addObject(doc) >>> len(sub2.getChildren()) 1 >>> sub2.getChildren()[0].name u'testdoc1' The following optional arguments are supported: - allVersions """ if not self._repository.getCapabilities()['Multifiling']: raise NotSupportedException('This repository does not support multifiling') postUrl = self.getChildrenLink() # post the Atom entry result = self._cmisClient.post(postUrl.encode('utf-8'), cmisObject.xmlDoc.toxml(encoding='utf-8'), ATOM_XML_ENTRY_TYPE, **kwargs) if type(result) == HTTPError: raise CmisException(result.code) def removeObject(self, cmisObject): """ Removes the specified object from this folder. The repository must support unfiling for this to work. """ if not self._repository.getCapabilities()['Unfiling']: raise NotSupportedException('This repository does not support unfiling') postUrl = self._repository.getCollectionLink(UNFILED_COLL) args = {"removeFrom": self.getObjectId()} # post the Atom entry to the unfiled collection result = self._cmisClient.post(postUrl.encode('utf-8'), cmisObject.xmlDoc.toxml(encoding='utf-8'), ATOM_XML_ENTRY_TYPE, **args) if type(result) == HTTPError: raise CmisException(result.code) def getPaths(self): """ Returns the paths as a list of strings. The spec says folders cannot be multi-filed, so this should always be one value. We return a list to be symmetric with the same method in :class:`Document`. """ return [self.properties['cmis:path']] class Relationship(CmisObject): """ Defines a relationship object between two :class:`CmisObjects` objects """ def getSourceId(self): """ Returns the :class:`CmisId` on the source side of the relationship. """ if self.xmlDoc == None: self.reload() props = self.getProperties() return CmisId(props['cmis:sourceId']) def getTargetId(self): """ Returns the :class:`CmisId` on the target side of the relationship. """ if self.xmlDoc == None: self.reload() props = self.getProperties() return CmisId(props['cmis:targetId']) def getSource(self): """ Returns an instance of the appropriate child-type of :class:`CmisObject` for the source side of the relationship. """ sourceId = self.getSourceId() return getSpecializedObject(self._repository.getObject(sourceId)) def getTarget(self): """ Returns an instance of the appropriate child-type of :class:`CmisObject` for the target side of the relationship. """ targetId = self.getTargetId() return getSpecializedObject(self._repository.getObject(targetId)) sourceId = property(getSourceId) targetId = property(getTargetId) source = property(getSource) target = property(getTarget) class Policy(CmisObject): """ An arbirary object that can 'applied' to objects that the repository identifies as being 'controllable'. """ pass class ObjectType(object): """ Represents the CMIS object type such as 'cmis:document' or 'cmis:folder'. Contains metadata about the type. """ def __init__(self, cmisClient, repository, typeId=None, xmlDoc=None): """ Constructor """ self._cmisClient = cmisClient self._repository = repository self._kwargs = None self._typeId = typeId self.xmlDoc = xmlDoc self.logger = logging.getLogger('cmislib.model.ObjectType') self.logger.info('Creating an instance of ObjectType') def __str__(self): """To string""" return self.getTypeId() def getTypeId(self): """ Returns the type ID for this object. >>> docType = repo.getTypeDefinition('cmis:document') >>> docType.getTypeId() 'cmis:document' """ if self._typeId == None: if self.xmlDoc == None: self.reload() self._typeId = CmisId(self._getElementValue(CMIS_NS, 'id')) return self._typeId def _getElementValue(self, namespace, elementName): """ Helper method to retrieve child element values from type XML. """ if self.xmlDoc == None: self.reload() #typeEls = self.xmlDoc.getElementsByTagNameNS(CMISRA_NS, 'type') #assert len(typeEls) == 1, "Expected to find exactly one type element but instead found %d" % len(typeEls) #typeEl = typeEls[0] typeEl = None for e in self.xmlDoc.childNodes: if e.nodeType == e.ELEMENT_NODE and e.localName == "type": typeEl = e break assert typeEl, "Expected to find one child element named type" els = typeEl.getElementsByTagNameNS(namespace, elementName) if len(els) >= 1: el = els[0] if el and len(el.childNodes) >= 1: return el.childNodes[0].data def getLocalName(self): """Getter for cmis:localName""" return self._getElementValue(CMIS_NS, 'localName') def getLocalNamespace(self): """Getter for cmis:localNamespace""" return self._getElementValue(CMIS_NS, 'localNamespace') def getDisplayName(self): """Getter for cmis:displayName""" return self._getElementValue(CMIS_NS, 'displayName') def getQueryName(self): """Getter for cmis:queryName""" return self._getElementValue(CMIS_NS, 'queryName') def getDescription(self): """Getter for cmis:description""" return self._getElementValue(CMIS_NS, 'description') def getBaseId(self): """Getter for cmis:baseId""" return CmisId(self._getElementValue(CMIS_NS, 'baseId')) def isCreatable(self): """Getter for cmis:creatable""" return parseBoolValue(self._getElementValue(CMIS_NS, 'creatable')) def isFileable(self): """Getter for cmis:fileable""" return parseBoolValue(self._getElementValue(CMIS_NS, 'fileable')) def isQueryable(self): """Getter for cmis:queryable""" return parseBoolValue(self._getElementValue(CMIS_NS, 'queryable')) def isFulltextIndexed(self): """Getter for cmis:fulltextIndexed""" return parseBoolValue(self._getElementValue(CMIS_NS, 'fulltextIndexed')) def isIncludedInSupertypeQuery(self): """Getter for cmis:includedInSupertypeQuery""" return parseBoolValue(self._getElementValue(CMIS_NS, 'includedInSupertypeQuery')) def isControllablePolicy(self): """Getter for cmis:controllablePolicy""" return parseBoolValue(self._getElementValue(CMIS_NS, 'controllablePolicy')) def isControllableACL(self): """Getter for cmis:controllableACL""" return parseBoolValue(self._getElementValue(CMIS_NS, 'controllableACL')) def getLink(self, rel, linkType): """ Gets the HREF for the link element with the specified rel and linkType. >>> from cmislib.model import ATOM_XML_FEED_TYPE >>> docType.getLink('down', ATOM_XML_FEED_TYPE) u'http://localhost:8080/alfresco/s/cmis/type/cmis:document/children' """ linkElements = self.xmlDoc.getElementsByTagNameNS(ATOM_NS, 'link') for linkElement in linkElements: if linkElement.attributes.has_key('rel') and linkElement.attributes.has_key('type'): relAttr = linkElement.attributes['rel'].value typeAttr = linkElement.attributes['type'].value if relAttr == rel and linkType.match(typeAttr): return linkElement.attributes['href'].value def getProperties(self): """ Returns a list of :class:`Property` objects representing each property defined for this type. >>> objType = repo.getTypeDefinition('cmis:relationship') >>> for prop in objType.properties: ... print 'Id:%s' % prop.id ... print 'Cardinality:%s' % prop.cardinality ... print 'Description:%s' % prop.description ... print 'Display name:%s' % prop.displayName ... print 'Local name:%s' % prop.localName ... print 'Local namespace:%s' % prop.localNamespace ... print 'Property type:%s' % prop.propertyType ... print 'Query name:%s' % prop.queryName ... print 'Updatability:%s' % prop.updatability ... print 'Inherited:%s' % prop.inherited ... print 'Orderable:%s' % prop.orderable ... print 'Queryable:%s' % prop.queryable ... print 'Required:%s' % prop.required ... print 'Open choice:%s' % prop.openChoice """ if self.xmlDoc == None: self.reload(includePropertyDefinitions='true') # Currently, property defs don't have an enclosing element. And, the # element name varies depending on type. Until that changes, I'm going # to find all elements unique to a prop, then grab its parent node. propTypeElements = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'propertyType') if len(propTypeElements) <= 0: self.reload(includePropertyDefinitions='true') propTypeElements = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'propertyType') assert len(propTypeElements) > 0, 'Could not retrieve object type property definitions' props = {} for typeEl in propTypeElements: prop = Property(typeEl.parentNode) props[prop.id] = prop return props def reload(self, **kwargs): """ This method will reload the object's data from the CMIS service. """ if kwargs: if self._kwargs: self._kwargs.update(kwargs) else: self._kwargs = kwargs templates = self._repository.getUriTemplates() template = templates['typebyid']['template'] params = {'{id}': self._typeId} byTypeIdUrl = multiple_replace(params, template) result = self._cmisClient.get(byTypeIdUrl.encode('utf-8'), **kwargs) if type(result) == HTTPError: raise CmisException(result.code) # instantiate CmisObject objects with the results and return the list entryElements = result.getElementsByTagNameNS(ATOM_NS, 'entry') assert(len(entryElements) == 1), "Expected entry element in result from calling %s" % byTypeIdUrl self.xmlDoc = entryElements[0] id = property(getTypeId) localName = property(getLocalName) localNamespace = property(getLocalNamespace) displayName = property(getDisplayName) queryName = property(getQueryName) description = property(getDescription) baseId = property(getBaseId) creatable = property(isCreatable) fileable = property(isFileable) queryable = property(isQueryable) fulltextIndexed = property(isFulltextIndexed) includedInSupertypeQuery = property(isIncludedInSupertypeQuery) controllablePolicy = property(isControllablePolicy) controllableACL = property(isControllableACL) properties = property(getProperties) class Property(object): """ This class represents an attribute or property definition of an object type. """ def __init__(self, propNode): """Constructor""" self.xmlDoc = propNode self.logger = logging.getLogger('cmislib.model.Property') self.logger.info('Creating an instance of Property') def __str__(self): """To string""" return self.getId() def _getElementValue(self, namespace, elementName): """ Utility method for retrieving element values from the object type XML. """ els = self.xmlDoc.getElementsByTagNameNS(namespace, elementName) if len(els) >= 1: el = els[0] if el and len(el.childNodes) >= 1: return el.childNodes[0].data def getId(self): """Getter for cmis:id""" return self._getElementValue(CMIS_NS, 'id') def getLocalName(self): """Getter for cmis:localName""" return self._getElementValue(CMIS_NS, 'localName') def getLocalNamespace(self): """Getter for cmis:localNamespace""" return self._getElementValue(CMIS_NS, 'localNamespace') def getDisplayName(self): """Getter for cmis:displayName""" return self._getElementValue(CMIS_NS, 'displayName') def getQueryName(self): """Getter for cmis:queryName""" return self._getElementValue(CMIS_NS, 'queryName') def getDescription(self): """Getter for cmis:description""" return self._getElementValue(CMIS_NS, 'description') def getPropertyType(self): """Getter for cmis:propertyType""" return self._getElementValue(CMIS_NS, 'propertyType') def getCardinality(self): """Getter for cmis:cardinality""" return self._getElementValue(CMIS_NS, 'cardinality') def getUpdatability(self): """Getter for cmis:updatability""" return parseBoolValue(self._getElementValue(CMIS_NS, 'updatability')) def isInherited(self): """Getter for cmis:inherited""" return parseBoolValue(self._getElementValue(CMIS_NS, 'inherited')) def isRequired(self): """Getter for cmis:required""" return parseBoolValue(self._getElementValue(CMIS_NS, 'required')) def isQueryable(self): """Getter for cmis:queryable""" return parseBoolValue(self._getElementValue(CMIS_NS, 'queryable')) def isOrderable(self): """Getter for cmis:orderable""" return parseBoolValue(self._getElementValue(CMIS_NS, 'orderable')) def isOpenChoice(self): """Getter for cmis:openChoice""" return parseBoolValue(self._getElementValue(CMIS_NS, 'openChoice')) id = property(getId) localName = property(getLocalName) localNamespace = property(getLocalNamespace) displayName = property(getDisplayName) queryName = property(getQueryName) description = property(getDescription) propertyType = property(getPropertyType) cardinality = property(getCardinality) updatability = property(getUpdatability) inherited = property(isInherited) required = property(isRequired) queryable = property(isQueryable) orderable = property(isOrderable) openChoice = property(isOpenChoice) class ACL(object): """ Represents the Access Control List for an object. """ def __init__(self, aceList=None, xmlDoc=None): """ Constructor. Pass in either a list of :class:`ACE` objects or the XML representation of the ACL. If you have only one ACE, don't worry about the list--the constructor will convert it to a list for you. """ if aceList: self._entries = aceList else: self._entries = {} if xmlDoc: self._xmlDoc = xmlDoc self._entries = self._getEntriesFromXml() else: self._xmlDoc = None self.logger = logging.getLogger('cmislib.model.ACL') self.logger.info('Creating an instance of ACL') def addEntry(self, ace): """ Adds an :class:`ACE` entry to the ACL. >>> acl = folder.getACL() >>> acl.addEntry(ACE('jpotts', 'cmis:read', 'true')) >>> acl.addEntry(ACE('jsmith', 'cmis:write', 'true')) >>> acl.getEntries() {u'GROUP_EVERYONE': , u'jdoe': , 'jpotts': , 'jsmith': } """ self._entries[ace.principalId] = ace def removeEntry(self, principalId): """ Removes the :class:`ACE` entry given a specific principalId. >>> acl.getEntries() {u'GROUP_EVERYONE': , u'jdoe': , 'jpotts': , 'jsmith': } >>> acl.removeEntry('jsmith') >>> acl.getEntries() {u'GROUP_EVERYONE': , u'jdoe': , 'jpotts': } """ if self._entries.has_key(principalId): del(self._entries[principalId]) def clearEntries(self): """ Clears all :class:`ACE` entries from the ACL and removes the internal XML representation of the ACL. >>> acl = ACL() >>> acl.addEntry(ACE('jsmith', 'cmis:write', 'true')) >>> acl.addEntry(ACE('jpotts', 'cmis:write', 'true')) >>> acl.entries {'jpotts': , 'jsmith': } >>> acl.getXmlDoc() >>> acl.clearEntries() >>> acl.entries >>> acl.getXmlDoc() """ self._entries.clear() self._xmlDoc = None def getEntries(self): """ Returns a dictionary of :class:`ACE` objects for each Access Control Entry in the ACL. The key value is the ACE principalid. >>> acl = ACL() >>> acl.addEntry(ACE('jsmith', 'cmis:write', 'true')) >>> acl.addEntry(ACE('jpotts', 'cmis:write', 'true')) >>> for ace in acl.entries.values(): ... print 'principal:%s has the following permissions...' % ace.principalId ... for perm in ace.permissions: ... print perm ... principal:jpotts has the following permissions... cmis:write principal:jsmith has the following permissions... cmis:write """ if self._entries: return self._entries else: if self._xmlDoc: # parse XML doc and build entry list self._entries = self._getEntriesFromXml() # then return it return self._entries def _getEntriesFromXml(self): """ Helper method for getting the :class:`ACE` entries from an XML representation of the ACL. """ if not self._xmlDoc: return result = {} # first child is the root node, cmis:acl for e in self._xmlDoc.childNodes[0].childNodes: if e.localName == 'permission': # grab the principal/principalId element value prinEl = e.getElementsByTagNameNS(CMIS_NS, 'principal')[0] if prinEl and prinEl.childNodes: prinIdEl = prinEl.getElementsByTagNameNS(CMIS_NS, 'principalId')[0] if prinIdEl and prinIdEl.childNodes: principalId = prinIdEl.childNodes[0].data # grab the permission values permEls = e.getElementsByTagNameNS(CMIS_NS, 'permission') perms = [] for permEl in permEls: if permEl and permEl.childNodes: perms.append(permEl.childNodes[0].data) # grab the direct value dirEl = e.getElementsByTagNameNS(CMIS_NS, 'direct')[0] if dirEl and dirEl.childNodes: direct = dirEl.childNodes[0].data # create an ACE if (len(perms) > 0): ace = ACE(principalId, perms, direct) # append it to the dictionary result[principalId] = ace return result def getXmlDoc(self): """ This method rebuilds the local XML representation of the ACL based on the :class:`ACE` objects in the entries list and returns the resulting XML Document. """ if not self.getEntries(): return xmlDoc = minidom.Document() aclEl = xmlDoc.createElementNS(CMIS_NS, 'cmis:acl') aclEl.setAttribute('xmlns:cmis', CMIS_NS) for ace in self.getEntries().values(): permEl = xmlDoc.createElementNS(CMIS_NS, 'cmis:permission') #principalId prinEl = xmlDoc.createElementNS(CMIS_NS, 'cmis:principal') prinIdEl = xmlDoc.createElementNS(CMIS_NS, 'cmis:principalId') prinIdElText = xmlDoc.createTextNode(ace.principalId) prinIdEl.appendChild(prinIdElText) prinEl.appendChild(prinIdEl) permEl.appendChild(prinEl) #direct directEl = xmlDoc.createElementNS(CMIS_NS, 'cmis:direct') directElText = xmlDoc.createTextNode(ace.direct) directEl.appendChild(directElText) permEl.appendChild(directEl) #permissions for perm in ace.permissions: permItemEl = xmlDoc.createElementNS(CMIS_NS, 'cmis:permission') permItemElText = xmlDoc.createTextNode(perm) permItemEl.appendChild(permItemElText) permEl.appendChild(permItemEl) aclEl.appendChild(permEl) xmlDoc.appendChild(aclEl) self._xmlDoc = xmlDoc return self._xmlDoc entries = property(getEntries) class ACE(object): """ Represents an individual Access Control Entry. """ def __init__(self, principalId=None, permissions=None, direct=None): """Constructor""" self._principalId = principalId if permissions: if isinstance(permissions, str): self._permissions = [permissions] else: self._permissions = permissions self._direct = direct self.logger = logging.getLogger('cmislib.model.ACE') self.logger.info('Creating an instance of ACE') @property def principalId(self): """Getter for principalId""" return self._principalId @property def direct(self): """Getter for direct""" return self._direct @property def permissions(self): """Getter for permissions""" return self._permissions class ChangeEntry(object): """ Represents a change log entry. Retrieve a list of change entries via :meth:`Repository.getContentChanges`. >>> for changeEntry in rs: ... changeEntry.objectId ... changeEntry.id ... changeEntry.changeType ... changeEntry.changeTime ... 'workspace://SpacesStore/0e2dc775-16b7-4634-9e54-2417a196829b' u'urn:uuid:0e2dc775-16b7-4634-9e54-2417a196829b' u'created' datetime.datetime(2010, 2, 11, 12, 55, 14) 'workspace://SpacesStore/bd768f9f-99a7-4033-828d-5b13f96c6923' u'urn:uuid:bd768f9f-99a7-4033-828d-5b13f96c6923' u'updated' datetime.datetime(2010, 2, 11, 12, 55, 13) 'workspace://SpacesStore/572c2cac-6b26-4cd8-91ad-b2931fe5b3fb' u'urn:uuid:572c2cac-6b26-4cd8-91ad-b2931fe5b3fb' u'updated' """ def __init__(self, cmisClient, repository, xmlDoc): """Constructor""" self._cmisClient = cmisClient self._repository = repository self._xmlDoc = xmlDoc self._properties = {} self._objectId = None self._changeEntryId = None self._changeType = None self._changeTime = None self.logger = logging.getLogger('cmislib.model.ChangeEntry') self.logger.info('Creating an instance of ChangeEntry') def getId(self): """ Returns the unique ID of the change entry. """ if self._changeEntryId == None: self._changeEntryId = self._xmlDoc.getElementsByTagNameNS(ATOM_NS, 'id')[0].firstChild.data return self._changeEntryId def getObjectId(self): """ Returns the object ID of the object that changed. """ if self._objectId == None: props = self.getProperties() self._objectId = CmisId(props['cmis:objectId']) return self._objectId def getChangeType(self): """ Returns the type of change that occurred. The resulting value must be one of: - created - updated - deleted - security """ if self._changeType == None: self._changeType = self._xmlDoc.getElementsByTagNameNS(CMIS_NS, 'changeType')[0].firstChild.data return self._changeType def getACL(self): """ Gets the :class:`ACL` object that is included with this Change Entry. """ # if you call getContentChanges with includeACL=true, you will get a # cmis:ACL entry. change entries don't appear to have a self URL so # instead of doing a reload with includeACL set to true, we'll either # see if the XML already has an ACL element and instantiate an ACL with # it, or we'll get the ACL_REL link, invoke that, and return the result if not self._repository.getCapabilities()['ACL']: return aclEls = self._xmlDoc.getElementsByTagNameNS(CMIS_NS, 'acl') aclUrl = self._getLink(ACL_REL) if (len(aclEls) == 1): return ACL(self._cmisClient, self._repository, aclEls[0]) elif aclUrl: result = self._cmisClient.get(aclUrl.encode('utf-8')) if type(result) == HTTPError: raise CmisException(result.code) return ACL(xmlDoc=result) def getChangeTime(self): """ Returns a datetime object representing the time the change occurred. """ if self._changeTime == None: self._changeTime = self._xmlDoc.getElementsByTagNameNS(CMIS_NS, 'changeTime')[0].firstChild.data return parseDateTimeValue(self._changeTime) def getProperties(self): """ Returns the properties of the change entry. Note that depending on the capabilities of the repository ("capabilityChanges") the list may not include the actual property values that changed. """ if self._properties == {}: propertiesElement = self._xmlDoc.getElementsByTagNameNS(CMIS_NS, 'properties')[0] for node in [e for e in propertiesElement.childNodes if e.nodeType == e.ELEMENT_NODE]: propertyName = node.attributes['propertyDefinitionId'].value if node.childNodes and \ node.getElementsByTagNameNS(CMIS_NS, 'value')[0] and \ node.getElementsByTagNameNS(CMIS_NS, 'value')[0].childNodes: propertyValue = parsePropValue( node.getElementsByTagNameNS(CMIS_NS, 'value')[0].childNodes[0].data, node.localName) else: propertyValue = None self._properties[propertyName] = propertyValue return self._properties def _getLink(self, rel): """ Returns the HREF attribute of an Atom link element for the specified rel. """ linkElements = self._xmlDoc.getElementsByTagNameNS(ATOM_NS, 'link') for linkElement in linkElements: if linkElement.attributes.has_key('rel'): relAttr = linkElement.attributes['rel'].value if relAttr == rel: return linkElement.attributes['href'].value id = property(getId) objectId = property(getObjectId) changeTime = property(getChangeTime) changeType = property(getChangeType) properties = property(getProperties) class ChangeEntryResultSet(ResultSet): """ A specialized type of :class:`ResultSet` that knows how to instantiate :class:`ChangeEntry` objects. The parent class assumes children of :class:`CmisObject` which doesn't work for ChangeEntries. """ def __iter__(self): """ Overriding to make it work with a list instead of a dict. """ return iter(self.getResults()) def __getitem__(self, index): """ Overriding to make it work with a list instead of a dict. """ return self.getResults()[index] def __len__(self): """ Overriding to make it work with a list instead of a dict. """ return len(self.getResults()) def getResults(self): """ Overriding to make it work with a list instead of a dict. """ if self._results: return self._results if self._xmlDoc: entryElements = self._xmlDoc.getElementsByTagNameNS(ATOM_NS, 'entry') entries = [] for entryElement in entryElements: changeEntry = ChangeEntry(self._cmisClient, self._repository, entryElement) entries.append(changeEntry) self._results = entries return self._results class Rendition(object): """ This class represents a Rendition. """ def __init__(self, propNode): """Constructor""" self.xmlDoc = propNode self.logger = logging.getLogger('cmislib.model.Rendition') self.logger.info('Creating an instance of Rendition') def __str__(self): """To string""" return self.getStreamId() def getStreamId(self): """Getter for the rendition's stream ID""" if self.xmlDoc.attributes.has_key('streamId'): return self.xmlDoc.attributes['streamId'].value def getMimeType(self): """Getter for the rendition's mime type""" if self.xmlDoc.attributes.has_key('type'): return self.xmlDoc.attributes['type'].value def getLength(self): """Getter for the renditions's length""" if self.xmlDoc.attributes.has_key('length'): return self.xmlDoc.attributes['length'].value def getTitle(self): """Getter for the renditions's title""" if self.xmlDoc.attributes.has_key('title'): return self.xmlDoc.attributes['title'].value def getKind(self): """Getter for the renditions's kind""" if self.xmlDoc.hasAttributeNS(CMISRA_NS, 'renditionKind'): return self.xmlDoc.getAttributeNS(CMISRA_NS, 'renditionKind') def getHeight(self): """Getter for the renditions's height""" if self.xmlDoc.attributes.has_key('height'): return self.xmlDoc.attributes['height'].value def getWidth(self): """Getter for the renditions's width""" if self.xmlDoc.attributes.has_key('width'): return self.xmlDoc.attributes['width'].value def getHref(self): """Getter for the renditions's href""" if self.xmlDoc.attributes.has_key('href'): return self.xmlDoc.attributes['href'].value def getRenditionDocumentId(self): """Getter for the renditions's width""" if self.xmlDoc.attributes.has_key('renditionDocumentId'): return self.xmlDoc.attributes['renditionDocumentId'].value streamId = property(getStreamId) mimeType = property(getMimeType) length = property(getLength) title = property(getTitle) kind = property(getKind) height = property(getHeight) width = property(getWidth) href = property(getHref) renditionDocumentId = property(getRenditionDocumentId) class CmisId(str): """ This is a marker class to be used for Strings that are used as CMIS ID's. Making the objects instances of this class makes it easier to create the Atom entry XML with the appropriate type, ie, cmis:propertyId, instead of cmis:propertyString. """ pass class UriTemplate(dict): """ Simple dictionary to represent the data stored in a URI template entry. """ def __init__(self, template, templateType, mediaType): """ Constructor """ dict.__init__(self) self['template'] = template self['type'] = templateType self['mediaType'] = mediaType def parsePropValue(value, nodeName): """ Returns a properly-typed object based on the type as specified in the node's element name. """ moduleLogger.debug('Inside parsePropValue') if nodeName == 'propertyId': return CmisId(value) elif nodeName == 'propertyString': return value elif nodeName == 'propertyBoolean': bDict = {'false': False, 'true': True} return bDict[value.lower()] elif nodeName == 'propertyInteger': return int(value) elif nodeName == 'propertyDecimal': return float(value) elif nodeName == 'propertyDateTime': #%z doesn't seem to work, so I'm going to trunc the offset #not all servers return microseconds, so those go too return parseDateTimeValue(value) else: return value def parseDateTimeValue(value): """ Utility function to return a datetime from a string. """ return iso8601.parse_date(value) def parseBoolValue(value): """ Utility function to parse booleans and none from strings """ if value == 'false': return False elif value == 'true': return True elif value == 'none': return None else: return value def toCMISValue(value): """ Utility function to convert Python values to CMIS string values """ if value == False: return 'false' elif value == True: return 'true' elif value == None: return 'none' else: return value def multiple_replace(aDict, text): """ Replace in 'text' all occurences of any key in the given dictionary by its corresponding value. Returns the new string. See http://code.activestate.com/recipes/81330/ """ # Create a regular expression from the dictionary keys regex = re.compile("(%s)" % "|".join(map(re.escape, aDict.keys()))) # For each match, look-up corresponding value in dictionary return regex.sub(lambda mo: aDict[mo.string[mo.start():mo.end()]], text) def getSpecializedObject(obj, **kwargs): """ Returns an instance of the appropriate :class:`CmisObject` class or one of its child types depending on the specified baseType. """ moduleLogger.debug('Inside getSpecializedObject') if 'cmis:baseTypeId' in obj.getProperties(): baseType = obj.getProperties()['cmis:baseTypeId'] if baseType == 'cmis:folder': return Folder(obj._cmisClient, obj._repository, obj.getObjectId(), obj.xmlDoc, **kwargs) if baseType == 'cmis:document': return Document(obj._cmisClient, obj._repository, obj.getObjectId(), obj.xmlDoc, **kwargs) if baseType == 'cmis:relationship': return Relationship(obj._cmisClient, obj._repository, obj.getObjectId(), obj.xmlDoc, **kwargs) if baseType == 'cmis:policy': return Policy(obj._cmisClient, obj._repository, obj.getObjectId(), obj.xmlDoc, **kwargs) # if the base type ID wasn't found in the props (this can happen when # someone runs a query that doesn't select * or doesn't individually # specify baseTypeId) or if the type isn't one of the known base # types, give the object back return obj def getEmptyXmlDoc(): """ Internal helper method that knows how to build an empty Atom entry. """ moduleLogger.debug('Inside getEmptyXmlDoc') entryXmlDoc = minidom.Document() entryElement = entryXmlDoc.createElementNS(ATOM_NS, "entry") entryElement.setAttribute('xmlns', ATOM_NS) entryXmlDoc.appendChild(entryElement) return entryXmlDoc def getEntryXmlDoc(repo=None, objectTypeId=None, properties=None, contentFile=None, contentType=None, contentEncoding=None): """ Internal helper method that knows how to build an Atom entry based on the properties and, optionally, the contentFile provided. """ moduleLogger.debug('Inside getEntryXmlDoc') entryXmlDoc = minidom.Document() entryElement = entryXmlDoc.createElementNS(ATOM_NS, "entry") entryElement.setAttribute('xmlns', ATOM_NS) entryElement.setAttribute('xmlns:app', APP_NS) entryElement.setAttribute('xmlns:cmisra', CMISRA_NS) entryXmlDoc.appendChild(entryElement) # if there is a File, encode it and add it to the XML if contentFile: mimetype = contentType encoding = contentEncoding # need to determine the mime type if not mimetype and hasattr(contentFile, 'name'): mimetype, encoding = mimetypes.guess_type(contentFile.name) if not mimetype: mimetype = 'application/binary' if not encoding: encoding = 'utf8' # This used to be ATOM_NS content but there is some debate among # vendors whether the ATOM_NS content must always be base64 # encoded. The spec does mandate that CMISRA_NS content be encoded # and that element takes precedence over ATOM_NS content if it is # present, so it seems reasonable to use CMIS_RA content for now # and encode everything. fileData = contentFile.read().encode("base64") mediaElement = entryXmlDoc.createElementNS(CMISRA_NS, 'cmisra:mediatype') mediaElementText = entryXmlDoc.createTextNode(mimetype) mediaElement.appendChild(mediaElementText) base64Element = entryXmlDoc.createElementNS(CMISRA_NS, 'cmisra:base64') base64ElementText = entryXmlDoc.createTextNode(fileData) base64Element.appendChild(base64ElementText) contentElement = entryXmlDoc.createElementNS(CMISRA_NS, 'cmisra:content') contentElement.appendChild(mediaElement) contentElement.appendChild(base64Element) entryElement.appendChild(contentElement) objectElement = entryXmlDoc.createElementNS(CMISRA_NS, 'cmisra:object') objectElement.setAttribute('xmlns:cmis', CMIS_NS) entryElement.appendChild(objectElement) if properties: # a name is required for most things, but not for a checkout if properties.has_key('cmis:name'): titleElement = entryXmlDoc.createElementNS(ATOM_NS, "title") titleText = entryXmlDoc.createTextNode(properties['cmis:name']) titleElement.appendChild(titleText) entryElement.appendChild(titleElement) propsElement = entryXmlDoc.createElementNS(CMIS_NS, 'cmis:properties') objectElement.appendChild(propsElement) typeDef = None for propName, propValue in properties.items(): """ the name of the element here is significant: it includes the data type. I should be able to figure out the right type based on the actual type of the object passed in. I could do a lookup to the type definition, but that doesn't seem worth the performance hit """ if (propValue == None or (type(propValue) == list and propValue[0] == None)): # grab the prop type from the typeDef if (typeDef == None): moduleLogger.debug('Looking up type def for: %s' % objectTypeId) typeDef = repo.getTypeDefinition(objectTypeId) #TODO what to do if type not found propType = typeDef.properties[propName].propertyType elif type(propValue) == list: propType = type(propValue[0]) else: propType = type(propValue) propElementName, propValueStrList = getElementNameAndValues(propType, propName, propValue, type(propValue) == list) propElement = entryXmlDoc.createElementNS(CMIS_NS, propElementName) propElement.setAttribute('propertyDefinitionId', propName) for val in propValueStrList: if val == None: continue valElement = entryXmlDoc.createElementNS(CMIS_NS, 'cmis:value') valText = entryXmlDoc.createTextNode(val) valElement.appendChild(valText) propElement.appendChild(valElement) propsElement.appendChild(propElement) return entryXmlDoc def getElementNameAndValues(propType, propName, propValue, isList=False): """ For a given property type, property name, and property value, this function returns the appropriate CMIS Atom entry element name and value list. """ moduleLogger.debug('Inside getElementNameAndValues') moduleLogger.debug('propType:%s propName:%s isList:%s' % (propType, propName, isList)) if (propType == 'id' or propType == CmisId): propElementName = 'cmis:propertyId' if isList: propValueStrList = [] for val in propValue: propValueStrList.append(val) else: propValueStrList = [propValue] elif (propType == 'string' or propType == str): propElementName = 'cmis:propertyString' if isList: propValueStrList = [] for val in propValue: propValueStrList.append(val) else: propValueStrList = [propValue] elif (propType == 'datetime' or propType == datetime.datetime): propElementName = 'cmis:propertyDateTime' if isList: propValueStrList = [] for val in propValue: if val != None: propValueStrList.append(val.isoformat()) else: propValueStrList.append(val) else: if propValue != None: propValueStrList = [propValue.isoformat()] else: propValueStrList = [propValue] elif (propType == 'boolean' or propType == bool): propElementName = 'cmis:propertyBoolean' if isList: propValueStrList = [] for val in propValue: if val != None: propValueStrList.append(unicode(val).lower()) else: propValueStrList.append(val) else: if propValue != None: propValueStrList = [unicode(propValue).lower()] else: propValueStrList = [propValue] elif (propType == 'integer' or propType == int): propElementName = 'cmis:propertyInteger' if isList: propValueStrList = [] for val in propValue: if val != None: propValueStrList.append(unicode(val)) else: propValueStrList.append(val) else: if propValue != None: propValueStrList = [unicode(propValue)] else: propValueStrList = [propValue] elif (propType == 'decimal' or propType == float): propElementName = 'cmis:propertyDecimal' if isList: propValueStrList = [] for val in propValue: if val != None: propValueStrList.append(unicode(val)) else: propValueStrList.append(val) else: if propValue != None: propValueStrList = [unicode(propValue)] else: propValueStrList = [propValue] else: propElementName = 'cmis:propertyString' if isList: propValueStrList = [] for val in propValue: if val != None: propValueStrList.append(unicode(val)) else: propValueStrList.append(val) else: if propValue != None: propValueStrList = [unicode(propValue)] else: propValueStrList = [propValue] return propElementName, propValueStrList cmislib-0.5.1/src/cmislib/net.py0000644000076500001200000002376412062727067017165 0ustar jpottsadmin00000000000000# # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # ''' Module that knows how to connect to the AtomPub Binding of a CMIS repo ''' from urllib import urlencode from urllib2 import HTTPBasicAuthHandler, \ HTTPPasswordMgrWithDefaultRealm, \ HTTPRedirectHandler, \ HTTPDefaultErrorHandler, \ HTTPError, \ Request, \ build_opener, \ AbstractBasicAuthHandler import logging class SmartRedirectHandler(HTTPRedirectHandler): """ Handles 301 and 302 redirects """ def http_error_301(self, req, fp, code, msg, headers): """ Handle a 301 error """ result = HTTPRedirectHandler.http_error_301( self, req, fp, code, msg, headers) result.status = code return result def http_error_302(self, req, fp, code, msg, headers): """ Handle a 302 error """ result = HTTPRedirectHandler.http_error_302( self, req, fp, code, msg, headers) result.status = code return result class DefaultErrorHandler(HTTPDefaultErrorHandler): """ Default error handler """ def http_error_default(self, req, fp, code, msg, headers): """Provide an implementation for the default handler""" result = HTTPError( req.get_full_url(), code, msg, headers, fp) result.status = code return result class ContextualBasicAuthHandler(HTTPBasicAuthHandler): """ Handles 401 errors without recursing indefinitely. The recursing behaviour has been introduced in Python 2.6.5 to handle 401 redirects used by some architectures of authentication. """ def __init__(self, password_mgr): HTTPBasicAuthHandler.__init__(self, password_mgr) self.authContext = set([]) def http_error_401(self, req, fp, code, msg, headers): """Override the default autoretry behaviour""" url = req.get_full_url() hdrs = req.header_items() hdrs = ', '.join(['%s: %s' % (key, value) for key, value in sorted(hdrs)]) context = (url, hdrs) if context in self.authContext: self.authContext.clear() result = HTTPError( req.get_full_url(), code, msg, headers, fp) result.status = code return result self.authContext.add(context) return self.http_error_auth_reqed('www-authenticate', url, req, headers) class RESTService(object): """ Generic service for interacting with an HTTP end point. Sets headers such as the USER_AGENT and builds the basic auth handler. """ def __init__(self): self.user_agent = 'cmislib/%s +http://chemistry.apache.org/' self.logger = logging.getLogger('cmislib.net.RESTService') def get(self, url, username=None, password=None, **kwargs): """ Makes a get request to the URL specified.""" headers = None if kwargs: if 'headers' in kwargs: headers = kwargs['headers'] del(kwargs['headers']) self.logger.debug('Headers passed in:%s' % headers) if url.find('?') >= 0: url = url + '&' + urlencode(kwargs) else: url = url + '?' + urlencode(kwargs) self.logger.debug('About to do a GET on:' + url) request = RESTRequest(url, method='GET') # add a user-agent request.add_header('User-Agent', self.user_agent) if headers: for k, v in headers.items(): self.logger.debug('Adding header:%s:%s' % (k, v)) request.add_header(k, v) # create a password manager passwordManager = HTTPPasswordMgrWithDefaultRealm() passwordManager.add_password(None, url, username, password) opener = build_opener(SmartRedirectHandler(), DefaultErrorHandler(), ContextualBasicAuthHandler(passwordManager)) return opener.open(request) def delete(self, url, username=None, password=None, **kwargs): """ Makes a delete request to the URL specified. """ headers = None if kwargs: if 'headers' in kwargs: headers = kwargs['headers'] del(kwargs['headers']) self.logger.debug('Headers passed in:%s' % headers) if url.find('?') >= 0: url = url + '&' + urlencode(kwargs) else: url = url + '?' + urlencode(kwargs) self.logger.debug('About to do a DELETE on:' + url) request = RESTRequest(url, method='DELETE') # add a user-agent request.add_header('User-Agent', self.user_agent) if headers: for k, v in headers.items(): self.logger.debug('Adding header:%s:%s' % (k, v)) request.add_header(k, v) # create a password manager passwordManager = HTTPPasswordMgrWithDefaultRealm() passwordManager.add_password(None, url, username, password) opener = build_opener(SmartRedirectHandler(), DefaultErrorHandler(), ContextualBasicAuthHandler(passwordManager)) #try: # opener.open(request) #except urllib2.HTTPError, e: # if e.code is not 204: # raise e #return None return opener.open(request) def put(self, url, payload, contentType, username=None, password=None, **kwargs): """ Makes a PUT request to the URL specified and includes the payload that gets passed in. The content type header gets set to the specified content type. """ headers = None if kwargs: if 'headers' in kwargs: headers = kwargs['headers'] del(kwargs['headers']) self.logger.debug('Headers passed in:%s' % headers) if url.find('?') >= 0: url = url + '&' + urlencode(kwargs) else: url = url + '?' + urlencode(kwargs) self.logger.debug('About to do a PUT on:' + url) request = RESTRequest(url, payload, method='PUT') # set the content type header request.add_header('Content-Type', contentType) # add a user-agent request.add_header('User-Agent', self.user_agent) if headers: for k, v in headers.items(): self.logger.debug('Adding header:%s:%s' % (k, v)) request.add_header(k, v) # create a password manager passwordManager = HTTPPasswordMgrWithDefaultRealm() passwordManager.add_password(None, url, username, password) opener = build_opener(SmartRedirectHandler(), DefaultErrorHandler(), ContextualBasicAuthHandler(passwordManager)) return opener.open(request) def post(self, url, payload, contentType, username=None, password=None, **kwargs): """ Makes a POST request to the URL specified and posts the payload that gets passed in. The content type header gets set to the specified content type. """ headers = None if kwargs: if 'headers' in kwargs: headers = kwargs['headers'] del(kwargs['headers']) self.logger.debug('Headers passed in:%s' % headers) if url.find('?') >= 0: url = url + '&' + urlencode(kwargs) else: url = url + '?' + urlencode(kwargs) self.logger.debug('About to do a POST on:' + url) request = RESTRequest(url, payload, method='POST') # set the content type header request.add_header('Content-Type', contentType) # add a user-agent request.add_header('User-Agent', self.user_agent) if headers: for k, v in headers.items(): self.logger.debug('Adding header:%s:%s' % (k, v)) request.add_header(k, v) # create a password manager passwordManager = HTTPPasswordMgrWithDefaultRealm() passwordManager.add_password(None, url, username, password) opener = build_opener(SmartRedirectHandler(), DefaultErrorHandler(), ContextualBasicAuthHandler(passwordManager)) try: return opener.open(request) except HTTPError, e: if e.code is not 201: return e else: return e.read() class RESTRequest(Request): """ Overrides urllib's request default behavior """ def __init__(self, *args, **kwargs): """ Constructor """ self._method = kwargs.pop('method', 'GET') assert self._method in ['GET', 'POST', 'PUT', 'DELETE'] Request.__init__(self, *args, **kwargs) def get_method(self): """ Override the get method """ return self._method cmislib-0.5.1/src/doc/0000755000076500001200000000000012063077404015126 5ustar jpottsadmin00000000000000cmislib-0.5.1/src/doc/src/0000755000076500001200000000000012063077404015715 5ustar jpottsadmin00000000000000cmislib-0.5.1/src/doc/src/.doctrees/0000755000076500001200000000000012063077404017603 5ustar jpottsadmin00000000000000cmislib-0.5.1/src/doc/src/.doctrees/about.doctree0000644000076500001200000001654511556357714022312 0ustar jpottsadmin00000000000000(cdocutils.nodes document qoq}q(U nametypesq}qXabout the cmis python libraryqNsUsubstitution_defsq}qUparse_messagesq ]q (cdocutils.nodes system_message q oq }q (U rawsourceqUU attributesq}q(Udupnamesq]qUlevelqKUidsq]qUbackrefsq]qUsourceqUG/Users/jpotts/workspaces/workspace-django/cmislib/src/doc/src/about.rstqUclassesq]qUnamesq]qUlineqKUtypeqUINFOq uUparentq!(cdocutils.nodes definition q"oq#}q$(hUh}q%(h]q&h]q'h]q(h]q)h]q*uh!(cdocutils.nodes definition_list_item q+oq,}q-(hXi..note:: I realize the createFolder call currently fails the "hide CMIS details from the user" directive.q.h!(cdocutils.nodes definition_list q/oq0}q1(hUh!(cdocutils.nodes section q2oq3}q4(hUh!hUsourceq5hUtagnameq6Usectionq7h}q8(h]q9h]q:h]q;h]qhauUlineq?KUdocumentq@hUchildrenqA]qB((cdocutils.nodes title qCoqD}qE(hXAbout the CMIS Python LibraryqFh!h3h5hh6UtitleqGh}qH(h]qIh]qJh]qKh]qLh]qMuh?Kh@hhA]qNcdocutils.nodes Text qO)qP}qQ(hhFUdataqRXAbout the CMIS Python LibraryqSh!hDubaub(cdocutils.nodes paragraph qToqU}qV(hX{The goal of this project is to create a CMIS client for Python that can be used to work with any CMIS-compliant repository.qWh!h3h5hh6U paragraphqXh}qY(h]qZh]q[h]q\h]q]h]q^uh?Kh@hhA]q_hO)q`}qa(hhWhRX{The goal of this project is to create a CMIS client for Python that can be used to work with any CMIS-compliant repository.qbh!hUubaub(h/oqc}qd(hUh!h3h5Nh6Udefinition_listqeh}qf(h]qgh]qhh]qih]qjh]qkuh?Nh@hhA]ql(h+oqm}qn(hXThe library is being developed with the following guidelines: * Developers using this API should be able to work with CMIS domain objects without having to worry about the underlying implementation details. * The library will use the Resftul AtomPub Binding. * The library will conform to the CMIS spec (need a link). As the current Apache Chemistry test server fails the Apache Chemistry TCK, Alfresco will be used as the primary test repository. qoh!hch6Udefinition_list_itemqph}qq(h]qrh]qsh]qth]quh]qvuh?KhA]qw((cdocutils.nodes term qxoqy}qz(hUh}q{(h]q|h]q}h]q~h]qh]quh!hmhA]qhO)q}q(hX=The library is being developed with the following guidelines:qhRX=The library is being developed with the following guidelines:qh!hyubah6Utermqub(h"oq}q(hUh}q(h]qh]qh]qh]qh]quh!hmhA]q(cdocutils.nodes bullet_list qoq}q(hUh}q(UbulletqX*h]qh]qh]qh]qh]quh!hhA]q((cdocutils.nodes list_item qoq}q(hXDevelopers using this API should be able to work with CMIS domain objects without having to worry about the underlying implementation details.qh}q(h]qh]qh]qh]qh]quh!hhA]q(hToq}q(hhh!hh6hXh}q(h]qh]qh]qh]qh]quh?KhA]qhO)q}q(hhhRXDevelopers using this API should be able to work with CMIS domain objects without having to worry about the underlying implementation details.qh!hubaubah6U list_itemqub(hoq}q(hX1The library will use the Resftul AtomPub Binding.qh}q(h]qh]qh]qh]qh]quh!hhA]q(hToq}q(hhh!hh6hXh}q(h]qh]qh]qh]qh]quh?KhA]qhO)q}q(hhhRX1The library will use the Resftul AtomPub Binding.qh!hubaubah6hub(hoq}q(hXThe library will conform to the CMIS spec (need a link). As the current Apache Chemistry test server fails the Apache Chemistry TCK, Alfresco will be used as the primary test repository. qh}q(h]qh]qh]qh]qh]quh!hhA]q(hToq}q(hXThe library will conform to the CMIS spec (need a link). As the current Apache Chemistry test server fails the Apache Chemistry TCK, Alfresco will be used as the primary test repository.qh!hh6hXh}q(h]qh]qh]qh]qh]quh?KhA]qhO)q}q(hhhRXThe library will conform to the CMIS spec (need a link). As the current Apache Chemistry test server fails the Apache Chemistry TCK, Alfresco will be used as the primary test repository.qh!hubaubah6hubeh6U bullet_listqubah6U definitionqubeubaub(cdocutils.nodes doctest_block qoq}q(hX`>>> cmisClient = CmisClient('http://localhost:8080/alfresco/s/cmis', 'admin', 'admin') >>> repo = cmisClient.getDefaultRepository() >>> rootFolder = repo.getRootFolder() >>> children = rootFolder.getChildren() >>> newFolder = rootFolder.createFolder({'cmis:name': 'testDeleteFolder folder'}) >>> props = newFolder.getProperties() >>> newFolder.delete()qh!h3h5hh6U doctest_blockqh}q(U xml:spaceqUpreserveqh]qh]qh]qh]qh]quh?Kh@hhA]qhO)q}q(hUhRhh!hubaubh0eubh5hh6heh}q(h]qh]qh]qh]qh]quh?Nh@hhA]qh,aubh6hph}q(h]qh]qh]qh]qh]quh?KhA]q((hxor}r(hUh}r(h]rh]rh]rh]rh]ruh!h,hA]rhO)r }r (hX..note::r hRX..note::r h!jubah6hubh#eubhA]r (hTor}r(hX`I realize the createFolder call currently fails the "hide CMIS details from the user" directive.rh!h#h6hXh}r(h]rh]rh]rh]rh]ruh?KhA]rhO)r}r(hjhRX`I realize the createFolder call currently fails the "hide CMIS details from the user" directive.rh!jubaubah6hubhA]r(hTor}r(hUh}r(h]rh]r h]r!h]r"h]r#uh!h hA]r$hO)r%}r&(hUhRU`Blank line missing before literal block (after the "::")? Interpreted as a definition list item.r'h!jubah6hXubah6Usystem_messager(ubaUcurrent_sourcer)NU decorationr*NUautofootnote_startr+KUnameidsr,}r-hh=shA]r.h3ahUU transformerr/NU footnote_refsr0}r1Urefnamesr2}r3Usymbol_footnotesr4]r5Uautofootnote_refsr6]r7Usymbol_footnote_refsr8]r9U citationsr:]r;h@hU current_liner<NUtransform_messagesr=]r>Ureporterr?NUid_startr@KU autofootnotesrA]rBU citation_refsrC}rDUindirect_targetsrE]rFUsettingsrG(cdocutils.frontend Values rHorI}rJ(Ufootnote_backlinksrKKUrecord_dependenciesrLNU rfc_base_urlrMUhttp://tools.ietf.org/html/rNU tracebackrOKUpep_referencesrPNUstrip_commentsrQNU toc_backlinksrRUentryrSU language_coderTUenrUU datestamprVNU report_levelrWKU _destinationrXNU halt_levelrYKU strip_classesrZNhGNUerror_encoding_error_handlerr[Ubackslashreplacer\Udebugr]NUembed_stylesheetr^Uoutput_encoding_error_handlerr_Ustrictr`U sectnum_xformraKUdump_transformsrbNU docinfo_xformrcKUwarning_streamrdNUpep_file_url_templatereUpep-%04drfUexit_status_levelrgKUconfigrhNUstrict_visitorriNUcloak_email_addressesrjUtrim_footnote_reference_spacerkUenvrlNUdump_pseudo_xmlrmNUexpose_internalsrnNUsectsubtitle_xformroU source_linkrpNUrfc_referencesrqNUoutput_encodingrrUutf-8rsU source_urlrtNUinput_encodingruU utf-8-sigrvU_disable_configrwNU id_prefixrxUU tab_widthryKUerror_encodingrzUasciir{U_sourcer|hU generatorr}NUdump_internalsr~NU pep_base_urlrUhttp://www.python.org/dev/peps/rUinput_encoding_error_handlerrj`Uauto_id_prefixrUidrUdoctitle_xformrUstrip_elements_with_classesrNU _config_filesr]rUfile_insertion_enabledrKU raw_enabledrKU dump_settingsrNubUsymbol_footnote_startrKUidsr}rh=h3sUsubstitution_namesr}rh6h@h}r(h]rh]rh]rUsourcerhh]rh]ruU footnotesr]rUrefidsr}rub.cmislib-0.5.1/src/doc/src/.doctrees/environment.pickle0000644000076500001200000000772311556357714023364 0ustar jpottsadmin00000000000000(csphinx.environment BuildEnvironment qoq}q(U anonlabelsq}q(UmodindexqhUqUgenindexqhUq Usearchq h Uq uUdlfilesq csphinx.util FilenameUniqDict q )qc__builtin__ set q]RqbUappqNUlabelsq}q(hhUX Module IndexqhhUXIndexqh h UX Search PagequU currmoduleqNUimagesqh )qh]RqbUtitlesq}q(Uindexq(cdocutils.nodes title qoq}q (U rawsourceq!UU attributesq"}q#(Udupnamesq$]q%Uclassesq&]q'Ubackrefsq(]q)Uidsq*]q+Unamesq,]q-uUchildrenq.]q/cdocutils.nodes Text q0)q1}q2(h!UUdataq3X(Welcome to CMIS Library's documentation!q4Uparentq5hubaUtagnameq6Utitleq7ubUaboutq8(hoq9}q:(h!Uh"}q;(h$]qh*]q?h,]q@uh.]qAh0)qB}qC(h!Uh3XAbout the CMIS Python LibraryqDh5h9ubah6h7ubuU glob_toctreesqEh]RqFU _warnfuncqGNU doctreedirqHUG/Users/jpotts/workspaces/workspace-django/cmislib/src/doc/src/.doctreesqIUversionqJKUdocnameqKNUsrcdirqLU=/Users/jpotts/workspaces/workspace-django/cmislib/src/doc/srcqMU gloss_entriesqNh]RqOUconfigqPcsphinx.config Config qQ)qR}qS(U master_docqTUindexqUU source_suffixqVU.rstqWU copyrightqXX 2009, OptarosqYUtemplates_pathqZ]q[U _templatesq\aU overridesq]}q^U html_contextq_}q`sUpygments_styleqaUsphinxqbUlatex_documentsqc]qd(UindexUCMISLibrary.texXCMIS Library DocumentationXOptarosUmanualqetqfaU exclude_treesqg]qhU_buildqiaUprojectqjX CMIS LibraryqkUhtmlhelp_basenameqlUCMISLibrarydocqmhJU0.1qnU extensionsqo]qp(Usphinx.ext.autodocqqUsphinx.ext.todoqrUsphinx.ext.coverageqseUhtml_static_pathqt]quU_staticqvaU html_themeqwUdefaultqxUreleaseqyhnh_h`UsetupqzNubUmetadataq{}q|(h}q}h8}q~uUversionchangesq}qUtoc_num_entriesq}q(hKh8KuUfiles_to_rebuildq}qXaboutqh]qhaRqsU found_docsqh]q(hh8eRqU longtitlesq}q(hhh8h9uU dependenciesq}qU index_numqKUtoctree_includesq}qh]qhasUtocsq}q(h(cdocutils.nodes bullet_list qoq}q(h!Uh"}q(h$]qh&]qh(]qh*]qh,]quh.]q((cdocutils.nodes list_item qoq}q(h!Uh"}q(h$]qh&]qh(]qh*]qh,]quh5hh.]q((csphinx.addnodes compact_paragraph qoq}q(h!Uh"}q(h$]qh&]qh(]qh*]qh,]quh5hh.]q(cdocutils.nodes reference qoq}q(h!Uh"}q(U anchornameqUUrefuriqhh*]qh(]qh$]qh&]qh,]quh5hh.]qh0)q}q(h!Uh3h4h5hubah6U referencequbah6Ucompact_paragraphqub(hoq}q(h!Uh"}q(h$]qh&]qh(]qh*]qh,]quh5hh.]q(csphinx.addnodes toctree qoq}q(h!Uh"}q(UnumberedqЉUparentqhUglobq҉h*]qh(]qh$]qh&]qh,]qUentriesq]qNhqaUhiddenqۉU includefilesq]qhaUmaxdepthqKuh5hh.]qh6Utoctreequbah6U bullet_listqubeh6U list_itemqub(hoq}q(h!Uh"}q(h$]qh&]qh(]qh*]qh,]quh5hh.]q(hoq}q(h!Uh"}q(h$]qh&]qh(]qh*]qh,]quh5hh.]q(hoq}q(h!Uh"}q(U anchornameqU#indices-and-tablesqUrefuriqhh*]qh(]qh$]qh&]qh,]quh5hh.]rh0)r}r(h!Uh3XIndices and tablesrh5hubah6hubah6hubah6hubeh6hubh8(hor}r(h!Uh"}r(h$]rh&]rh(]r h*]r h,]r uh.]r (hor }r(h!Uh"}r(h$]rh&]rh(]rh*]rh,]ruh5jh.]r(hor}r(h!Uh"}r(h$]rh&]rh(]rh*]rh,]ruh5j h.]r(hor}r (h!Uh"}r!(U anchornamer"UUrefurir#h8h*]r$h(]r%h$]r&h&]r'h,]r(uh5jh.]r)h0)r*}r+(h!Uh3hDh5jubah6hubah6hubah6hubah6hubuU indexentriesr,}r-(h]r.h8]r/uUall_docsr0}r1(hGAG7Gh8GAG5[uUsettingsr2}r3(U rfc_base_urlr4Uhttp://tools.ietf.org/html/r5Ucloak_email_addressesr6Utrim_footnote_reference_spacer7Uwarning_streamr8csphinx.environment WarningStream r9)r:}r;Uwarnfuncr<NsbUenvr=hUdoctitle_xformr>Usectsubtitle_xformr?Uembed_stylesheetr@U pep_base_urlrAUhttp://www.python.org/dev/peps/rBUinput_encodingrCU utf-8-sigrDuU filemodulesrE}rFUmodulesrG}rHU currclassrINUcurrdescrJNU progoptionsrK}rLUnumbered_toctreesrMh]RrNUtoc_secnumbersrO}rPU reftargetsrQ}rRU currprogramrSNUdescrefsrT}rUub.cmislib-0.5.1/src/doc/src/.doctrees/index.doctree0000644000076500001200000001246211556357714022301 0ustar jpottsadmin00000000000000(cdocutils.nodes document qoq}q(U nametypesq}q(X(welcome to cmis library's documentation!qNXindices and tablesqNuUsubstitution_defsq}q Uparse_messagesq ]q Ucurrent_sourceq NU decorationq NUautofootnote_startqKUnameidsq}q(hU'welcome-to-cmis-library-s-documentationqhUindices-and-tablesquUchildrenq]q((cdocutils.nodes comment qoq}q(U rawsourceqXCMIS Library documentation master file, created by sphinx-quickstart on Thu Dec 10 10:12:43 2009. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive.qUparentqhUsourceqUG/Users/jpotts/workspaces/workspace-django/cmislib/src/doc/src/index.rstqUtagnameqUcommentqU attributesq}q (U xml:spaceq!Upreserveq"Uidsq#]q$Ubackrefsq%]q&Udupnamesq']q(Uclassesq)]q*Unamesq+]q,uUlineq-KUdocumentq.hh]q/cdocutils.nodes Text q0)q1}q2(hUUdataq3hhhubaub(cdocutils.nodes section q4oq5}q6(hUhhhhhUsectionq7h}q8(h']q9h)]q:h%]q;h#]q((cdocutils.nodes title q?oq@}qA(hX(Welcome to CMIS Library's documentation!qBhh5hhhUtitleqCh}qD(h']qEh)]qFh%]qGh#]qHh+]qIuh-Kh.hh]qJh0)qK}qL(hhBh3X(Welcome to CMIS Library's documentation!qMhh@ubaub(cdocutils.nodes paragraph qNoqO}qP(hX Contents:qQhh5hhhU paragraphqRh}qS(h']qTh)]qUh%]qVh#]qWh+]qXuh-K h.hh]qYh0)qZ}q[(hhQh3X Contents:q\hhOubaub(csphinx.addnodes toctree q]oq^}q_(hUhh5hhhUtoctreeq`h}qa(UnumberedqbhUindexqcUglobqdh#]qeh%]qfh']qgh)]qhh+]qiUentriesqj]qkNXaboutqlqmaUhiddenqnU includefilesqo]qphlaUmaxdepthqqKuh-Nh.hh]qrubeub(h4oqs}qt(hUhhhhhh7h}qu(h']qvh)]qwh%]qxh#]qyhah+]qzhauh-Kh.hh]q{((h?oq|}q}(hXIndices and tablesq~hhshhhhCh}q(h']qh)]qh%]qh#]qh+]quh-Kh.hh]qh0)q}q(hh~h3XIndices and tablesqhh|ubaub(cdocutils.nodes bullet_list qoq}q(hUhhshhhU bullet_listqh}q(UbulletqX*h#]qh%]qh']qh)]qh+]quh-Kh.hh]q((cdocutils.nodes list_item qoq}q(hX:ref:`genindex`qhhhhhU list_itemqh}q(h']qh)]qh%]qh#]qh+]quh-Nh.hh]q(hNoq}q(hhhhhhRh}q(h']qh)]qh%]qh#]qh+]quh-Kh]q(csphinx.addnodes pending_xref qoq}q(hhhhhU pending_xrefqh}q(UreftypeqXrefqUmodnameqNU refcaptionqU reftargetqXgenindexqh#]qh%]qU classnameqNh']qh)]qh+]quh-Kh]q(cdocutils.nodes emphasis qoq}q(hhh}q(h']qh)]qUxrefqah%]qh#]qh+]quhhh]qh0)q}q(hUh3hhhubahUemphasisqubaubaubaub(hoq}q(hX:ref:`modindex`qhhhhhhh}q(h']qh)]qh%]qh#]qh+]quh-Nh.hh]q(hNoq}q(hhhhhhRh}q(h']qh)]qh%]qh#]qh+]quh-Kh]q(hoq}q(hhhhhhh}q(UreftypeqXrefqUmodnameqNU refcaptionqhXmodindexqh#]qh%]qU classnameqNh']qh)]qh+]quh-Kh]q(hoq}q(hhh}q(h']qh)]qhah%]qh#]qh+]quhhh]qh0)q}q(hUh3hhhubahhubaubaubaub(hoq}q(hX:ref:`search` qhhhhhhh}q(h']qh)]qh%]qh#]qh+]quh-Nh.hh]r(hNor}r(hX :ref:`search`rhhhhRh}r(h']rh)]rh%]rh#]rh+]r uh-Kh]r (hor }r (hjhjhhh}r (UreftyperXrefrUmodnamerNU refcaptionrhXsearchrh#]rh%]rU classnamerNh']rh)]rh+]ruh-Kh]r(hor}r(hjh}r(h']rh)]rhah%]rh#]r h+]r!uhj h]r"h0)r#}r$(hUh3jhjubahhubaubaubaubeubeubehUU transformerr%NU footnote_refsr&}r'Urefnamesr(}r)Usymbol_footnotesr*]r+Uautofootnote_refsr,]r-Usymbol_footnote_refsr.]r/U citationsr0]r1h.hU current_liner2NUtransform_messagesr3]r4Ureporterr5NUid_startr6KU autofootnotesr7]r8U citation_refsr9}r:Uindirect_targetsr;]r<Usettingsr=(cdocutils.frontend Values r>or?}r@(Ufootnote_backlinksrAKUrecord_dependenciesrBNU rfc_base_urlrCUhttp://tools.ietf.org/html/rDU tracebackrEKUpep_referencesrFNUstrip_commentsrGNU toc_backlinksrHUentryrIU language_coderJUenrKU datestamprLNU report_levelrMKU _destinationrNNU halt_levelrOKU strip_classesrPNhCNUerror_encoding_error_handlerrQUbackslashreplacerRUdebugrSNUembed_stylesheetrTUoutput_encoding_error_handlerrUUstrictrVU sectnum_xformrWKUdump_transformsrXNU docinfo_xformrYKUwarning_streamrZNUpep_file_url_templater[Upep-%04dr\Uexit_status_levelr]KUconfigr^NUstrict_visitorr_NUcloak_email_addressesr`Utrim_footnote_reference_spaceraUenvrbNUdump_pseudo_xmlrcNUexpose_internalsrdNUsectsubtitle_xformreU source_linkrfNUrfc_referencesrgNUoutput_encodingrhUutf-8riU source_urlrjNUinput_encodingrkU utf-8-sigrlU_disable_configrmNU id_prefixrnUU tab_widthroKUerror_encodingrpUasciirqU_sourcerrhU generatorrsNUdump_internalsrtNU pep_base_urlruUhttp://www.python.org/dev/peps/rvUinput_encoding_error_handlerrwjVUauto_id_prefixrxUidryUdoctitle_xformrzUstrip_elements_with_classesr{NU _config_filesr|]r}Ufile_insertion_enabledr~KU raw_enabledrKU dump_settingsrNubUsymbol_footnote_startrKUidsr}r(hhshh5uUsubstitution_namesr}rhh.h}r(h']rh#]rh%]rUsourcerhh)]rh+]ruU footnotesr]rUrefidsr}rub.cmislib-0.5.1/src/doc/src/about.rst0000644000076500001200000000521412062721735017565 0ustar jpottsadmin00000000000000.. Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. About Apache Chemistry cmislib ============================== The goal of this project is to create a CMIS client for Python that can be used to work with any CMIS-compliant repository. The library is being developed with the following guidelines: * Developers using this API should be able to work with CMIS domain objects without having to worry about the underlying implementation details. * The library will use the Resftul AtomPub Binding. * The library will conform to the `CMIS spec `_ as closely as possible. Several public CMIS repositories are being used to test the API. * The library should have no hard-coded URL's. It should be able to get everything it needs regarding how to work with the CMIS service from the CMIS service URL response and subsequent calls. * There shouldn't have to be a vendor-specific version of this library. The goal is for it to be interoperable with CMIS-compliant providers. Quick Example ------------- This should give you an idea of how easy and natural it is to work with the API: >>> cmisClient = cmislib.CmisClient('http://localhost:8080/alfresco/cmisatom', 'admin', 'admin') >>> repo = cmisClient.defaultRepository >>> rootFolder = repo.rootFolder >>> children = rootFolder.getChildren() >>> newFolder = rootFolder.createFolder('testDeleteFolder folder') >>> props = newFolder.properties >>> newFolder.delete() To-Do's ------- Miscellaneous * createDocumentFromSource * getProperties filter * getContentStream stream id Unfiling/multifiling support * createDocument without a parent folder (unfiled) * The spec does not yet support this. Although the spec does say that a folder ID is optional, it does not specify which URL to post the unfiled document to. Policies * Policy object * createPolicy * applyPolicy * removePolicy * getAppliedPolicies cmislib-0.5.1/src/doc/src/code.rst0000644000076500001200000000504012062716663017366 0ustar jpottsadmin00000000000000.. Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Code ==== The :mod:`cmislib.model` Module ------------------------------- The :mod:`cmislib.model` Module contains all the CMIS domain objects. The ones you will work with are listed as top level package elements. When working with the repository, the first thing you need to do is grab an instance of :class:`cmislib.CmisClient`, passing it the repository endpoint URL, username, and password. For example, in Alfresco 4 and higher, the repository endpoint is 'http://localhost:8080/alfresco/cmisatom'. In earlier versions of Alfresco, the endpoint is 'http://localhost:8080/alfresco/s/api/cmis'. In both cases, the default username and password are 'admin' and 'admin'. >>> cmisClient = cmislib.CmisClient('http://localhost:8080/alfresco/s/cmis', 'admin', 'admin') From there you can get the default repository... >>> repo = cmisClient.defaultRepository or a specific repository if you know the repository ID. >>> repo = cmisClient.getRepository('83beb297-a6fa-4ac5-844b-98c871c0eea9') Once you have that, you're off to the races. Use the :class:`cmislib.Repository` class to create new :class:`cmislib.Folder` and :class:`cmislib.Document` objects, perform searches, etc. .. automodule:: cmislib.model :members: The :mod:`cmislib.net` Module ----------------------------- The :mod:`cmislib.net` Module contains the classes used by :mod:`cmislib.model.CmisClient` to communicate with the CMIS repository. The most important of which is :class:`cmislib.net.RESTService`. .. automodule:: cmislib.net :members: RESTService The :mod:`tests` Module ------------------------------- The :mod:`tests` Module contains unit tests for all classes and methods in :mod:`cmislib.model`. See :ref:`tests` for more information on running tests. .. automodule:: tests :members: cmislib-0.5.1/src/doc/src/conf.py0000644000076500001200000002341412062725000017207 0ustar jpottsadmin00000000000000# -*- coding: utf-8 -*- # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # # CMIS Library documentation build configuration file, created by # sphinx-quickstart on Fri Dec 14 16:13:15 2012. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys, os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) sys.path.append(os.path.abspath('../..')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'Apache Chemistry cmislib' copyright = u'2013, Apache Software Foundation' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '0.5' # The full version, including alpha/beta/rc tags. release = '0.5.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'cmislibdoc' # -- Options for LaTeX output -------------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'cmislib.tex', u'Apache Chemistry cmislib Documentation', u'Jeff Potts', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'cmislib', u'Apache Chemistry cmislib Documentation', [u'Jeff Potts'], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'cmislib', u'Apache Chemistry cmislib Documentation', u'Jeff Potts', 'cmislib', 'Python client library for CMIS', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # -- Options for Epub output --------------------------------------------------- # Bibliographic Dublin Core info. epub_title = u'Apache Chemistry cmislib Documentation' epub_author = u'Jeff Potts' epub_publisher = u'Jeff Potts' epub_copyright = u'2013, Apache Software Foundation' # The language of the text. It defaults to the language option # or en if the language is not set. #epub_language = '' # The scheme of the identifier. Typical schemes are ISBN or URL. #epub_scheme = '' # The unique identifier of the text. This can be a ISBN number # or the project homepage. #epub_identifier = '' # A unique identification for the text. #epub_uid = '' # A tuple containing the cover image and cover page html template filenames. #epub_cover = () # HTML files that should be inserted before the pages created by sphinx. # The format is a list of tuples containing the path and title. #epub_pre_files = [] # HTML files shat should be inserted after the pages created by sphinx. # The format is a list of tuples containing the path and title. #epub_post_files = [] # A list of files that should not be packed into the epub file. #epub_exclude_files = [] # The depth of the table of contents in toc.ncx. #epub_tocdepth = 3 # Allow duplicate toc entries. #epub_tocdup = True cmislib-0.5.1/src/doc/src/devguide.rst0000644000076500001200000000606212062730062020242 0ustar jpottsadmin00000000000000.. Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. =============== Developer Guide =============== This page is for people who wish to contribute code to this project. Developer Setup --------------- Check out the source from head, switch to the source code's root directory, then run: python setup.py develop That will set up this project's src directory in the easy-install.pth file in site-packages. Release Process --------------- Checklist: #. All newly-added code has a unit test #. All tests pass cleanly (or have good reasons for not passing) #. Change setup.cfg to have the appropriate tag ('dev', for example, or '' for a stable release) #. Change setup.py to have the appropriate version number #. Inline comments updated with changes #. Sphinx doc updated with changes #. Docs build cleanly .. code-block:: bash cd src/doc/src/ make html #. pep8 runs without much complaint .. code-block:: bash pep8 --ignore=E501,W601 --repeat model.py #. pylint runs without much complaint .. code-block:: bash pylint --disable=C0103,R0904,R0913,C0301,W0511 cmislibtest.py #. All changes checked in #. Tag the release using 'cmislib-[release num]-RC[x]' #. Use the release script to build the release artifacts .. code-block:: bash cd dist ./release.sh -u jpotts@apache.org This will do a 'setup.py bdist sdist' and will then sign all artifacts. Note that the artifacts will be named without 'RC[x]'. These are the same artifacts that will be distributed if the vote passes. #. Copy files to the Apache server under ~/public_html/chemistry/cmislib/[release num] #. Start vote. Send an email to dev@chemistry.apache.org announcing the vote, highlighting the changes, pointing to the tagged source, and referencing the artifacts that have been copied to the Apache server. #. After 72 hours, if the vote passes, continue, otherwise address issues and start over #. Copy the files to the appropriate Apache dist directory, which is /www/www.apache.org/dist/chemistry/cmislib/[release num] #. Rename the RC tag in source code control #. Update the cmislib home page with download links to the new release #. Upload files to Pypi #. Check the `cheesecake `_ score .. code-block:: bash python cheesecake_index --name=cmislib cmislib-0.5.1/src/doc/src/docs.rst0000644000076500001200000000244711556357714017421 0ustar jpottsadmin00000000000000.. Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Documentation ============= This documentation was generated with `Sphinx `_. To install Sphinx on Mac OS X using Macports: MAC OS X:: sudo port install py26-sphinx Once you've got Sphinx installed, if you need to regenerate the documentation:: cd /path/to/cmislib/src/doc/src Run either: sphinx-build -b html -d ../build/.doctrees . ../build make html The generated HTML will be placed in doc/build:: firefox file:///path/to/cmislib/src/doc/build/index.html cmislib-0.5.1/src/doc/src/examples.rst0000644000076500001200000001127511731375752020303 0ustar jpottsadmin00000000000000.. Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. .. _examples: ======== Examples ======== There's nothing in cmislib that is specific to any particular vendor. Once you give it your CMIS provider's service URL and some credentials, it figures out where to go from there. Let's look at some examples using Alfresco's public CMIS repository. ----------------------- Get a Repository object ----------------------- #. From the command-line, start the Python shell by typing `python` then hit enter. #. Import the CmisClient: >>> from cmislib import CmisClient #. Point the CmisClient at the repository's service URL >>> client = CmisClient('http://cmis.alfresco.com/cmisatom', 'admin', 'admin') #. Get the default repository for the service >>> repo = client.defaultRepository >>> repo.id u'83beb297-a6fa-4ac5-844b-98c871c0eea9' #. Get the repository's properties. This for-loop spits out everything cmislib knows about the repo. >>> repo.name u'Main Repository' >>> info = repo.info >>> for k,v in info.items(): ... print "%s:%s" % (k,v) ... cmisSpecificationTitle:Version 1.0 Committee Draft 04 cmisVersionSupported:1.0 repositoryDescription:None productVersion:3.2.0 (r2 2440) rootFolderId:workspace://SpacesStore/aa1ecedf-9551-49c5-831a-0502bb43f348 repositoryId:83beb297-a6fa-4ac5-844b-98c871c0eea9 repositoryName:Main Repository vendorName:Alfresco productName:Alfresco Repository (Community) ------------------- Folders & Documents ------------------- Once you've got the Repository object you can start working with folders. #. Create a new folder in the root. You should name yours something unique. >>> root = repo.rootFolder >>> someFolder = root.createFolder('someFolder') >>> someFolder.id u'workspace://SpacesStore/91f344ef-84e7-43d8-b379-959c0be7e8fc' #. Then, you can create some content: >>> someFile = open('test.txt', 'r') >>> someDoc = someFolder.createDocument('Test Document', contentFile=someFile) #. And, if you want, you can dump the properties of the newly-created document (this is a partial list): >>> props = someDoc.properties >>> for k,v in props.items(): ... print '%s:%s' % (k,v) ... cmis:contentStreamMimeType:text/plain cmis:creationDate:2009-12-18T10:59:26.667-06:00 cmis:baseTypeId:cmis:document cmis:isLatestMajorVersion:false cmis:isImmutable:false cmis:isMajorVersion:false cmis:objectId:workspace://SpacesStore/2cf36ad5-92b0-4731-94a4-9f3fef25b479 ---------------------------------- Searching For & Retrieving Objects ---------------------------------- There are several different ways to grab an object: * You can run a CMIS query * You can ask the repository to give you one for a specific path or object ID * You can traverse the repository using a folder's children and/or descendants #. Let's find the doc we just created with a full-text search. .. note:: Note that I'm currently seeing a problem with Alfresco in which the CMIS service returns one less result than what's really there): >>> results = repo.query("select * from cmis:document where contains('test')") >>> for result in results: ... print result.name ... Test Document2 example test script.js #. Alternatively, you can also get objects by their path, like this: >>> someDoc = repo.getObjectByPath('/someFolder/Test Document') >>> someDoc.id u'workspace://SpacesStore/2cf36ad5-92b0-4731-94a4-9f3fef25b479' #. Or their object ID, like this: >>> someDoc = repo.getObject('workspace://SpacesStore/2cf36ad5-92b0-4731-94a4-9f3fef25b479') >>> someDoc.name u'Test Document' #. Folder objects have getChildren() and getDescendants() methods that will return a list of :class:`CmisObject` objects: >>> children= someFolder.getChildren() >>> for child in children: ... print child.name ... Test Document Test Document2 cmislib-0.5.1/src/doc/src/index.rst0000644000076500001200000000256011667703340017565 0ustar jpottsadmin00000000000000.. Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. .. cmislib documentation master file, created by sphinx-quickstart on Thu Dec 10 10:12:43 2009. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Welcome to the Apache Chemistry cmislib documentation! ================================================================== Contents: .. toctree:: :maxdepth: 2 about.rst install.rst examples.rst code.rst devguide.rst tests.rst docs.rst sample-data.rst Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` cmislib-0.5.1/src/doc/src/install.rst0000644000076500001200000000271211556357714020132 0ustar jpottsadmin00000000000000.. Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Installation ============ Requirements ------------ These requirements must be met: - Python 2.6.x - CMIS provider compliant with CMIS 1.0 Steps ----- #. If you don't have `Python `_ installed already, do so. #. If you don't have `setuptools `_ installed already, do so. #. Once setuptools is installed, type `easy_install cmislib` #. That's it! Once you do that, you should be able to fire up Python on the command-line and import cmislib successfully. >>> from cmislib import CmisClient, Repository, Folder To validate everything is working, run some :ref:`tests` or walk through some :ref:`examples`. cmislib-0.5.1/src/doc/src/make.bat0000644000076500001200000000755511556357714017351 0ustar jpottsadmin00000000000000@ECHO OFF REM REM Licensed to the Apache Software Foundation (ASF) under one REM or more contributor license agreements. See the NOTICE file REM distributed with this work for additional information REM regarding copyright ownership. The ASF licenses this file REM to you under the Apache License, Version 2.0 (the REM "License"); you may not use this file except in compliance REM with the License. You may obtain a copy of the License at REM REM http://www.apache.org/licenses/LICENSE-2.0 REM REM Unless required by applicable law or agreed to in writing, REM software distributed under the License is distributed on an REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY REM KIND, either express or implied. See the License for the REM specific language governing permissions and limitations REM under the License. REM REM Command file for Sphinx documentation set SPHINXBUILD=sphinx-build set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\CMISLibrary.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\CMISLibrary.ghc goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) :end cmislib-0.5.1/src/doc/src/Makefile0000644000076500001200000001454012062725576017372 0ustar jpottsadmin00000000000000# # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # # # Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = ../build SOURCEDIR = . # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) $(SOURCEDIR) # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) $(SOURCEDIR) .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/MyTestProject.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/MyTestProject.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/MyTestProject" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/MyTestProject" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." cmislib-0.5.1/src/doc/src/run-sphinx.py0000755000076500001200000000247611556357714020431 0ustar jpottsadmin00000000000000#!/usr/bin/python # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # import os import sys os.environ['SPHINXBUILD']="sphinx-build" build_dir = sys.path[0] + "/../build" os.environ['BUILDDIR']=build_dir os.environ['SOURCEDIR']=sys.path[0] # force a clean every time print "Removing build dir: %s" % build_dir os.system("rm -rf " + build_dir) os.system("make -e --makefile=" + sys.path[0] + "/Makefile html") zip_file = sys.path[0] + "/../docs.zip" os.system("rm " + zip_file) os.chdir(build_dir) os.system("zip -r " + zip_file + " *") cmislib-0.5.1/src/doc/src/sample-data.rst0000644000076500001200000000227611556357714020661 0ustar jpottsadmin00000000000000.. Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Sample Data =========== The /path/to/cmislib/src/data directory contains some sample XML responses from a CMIS service. These are for sample and development purposes and can safely be ignored if you are an end-user of the library. In some cases there are two files for the same response. For example, 'types.xml' came from Alfresco while 'types.chemistry.xml' came from the simple Apache Chemistry test server. cmislib-0.5.1/src/doc/src/tests.rst0000644000076500001200000000276011556357714017631 0ustar jpottsadmin00000000000000.. Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. .. _tests: ===== Tests ===== This code includes unit tests. To run the tests:: cd /path/to/cmislib/tests Edit settings.py Set REPOSITORY_URL, USERNAME, PASSWORD Optionally, set TEST_ROOT_PATH and other settings to meet your needs python cmislibtest.py .. note:: http://cmis.alfresco.com is a freely-available, hosted CMIS service. If you want to use that for testing, the URL is http://cmis.alfresco.com/s/cmis and the username and password are admin/admin. See the wiki for other known CMIS test servers. If everything goes well, you should see:: Ran X tests in 3.607s OK .. note:: Depending on the implementation of the CMIS provider, you may see errors or failures instead of 'OK'. cmislib-0.5.1/src/tests/0000755000076500001200000000000012063077404015523 5ustar jpottsadmin00000000000000cmislib-0.5.1/src/tests/250px-Cmis_logo.png0000644000076500001200000002021111556357714021027 0ustar jpottsadmin00000000000000PNG  IHDRD,(8bKGDC pHYs."."ݒ vpAgD5 w;IDATxyx?w$dE`@Ev}CpcTV,*ڊ.V+ `QPdId !L2gK:!3$|g L޹s={]p0 dx`&@ 4TBDB< %=A2L%q'@~ !oLeP$]7%f=ڢ< <\^`@º'@6w  9 ` 0)H,(qU> >;'@} >>l ZXl| Ő@g@ztT?ˁYng;']բh_ nR~ Dnѽ~`?(04" DW=vyv%,{  W`0 ÙH*Rҋ_⢖ӛgdW ]X]QIyN1W #}:M.!G aeBY=zn<0p[~aJWsLAR=*;Цv1h $w{[hdΜr .:ʓE4$pȀ.l6&K]dhp+Y+V a (?h+=Ȕ9u92v}S6yUCy_Z[ӧ{6 R)텅[}cV?rOٳtȐGLIxLj0pp[e2BRdؐE> .j$ o̔ax xDkiҏ;f =k'E'eY (d_rD_mng)PPl7`-7\߼g5 ZX}6[霡]F9ѧܤg]"hGbWkG$K\,I,08`e~M[c8_dTE-?+}p b+}tں?Gy/ R.!;-|țj$eyYч-^j] }®)MAe \*%I(_oo]]RdaLQnFn2'Jmx!.m`^۾^޸P30p6PZ1Y>?2f}"y&p?/8%!x֥82Ġ S*ҋ{g61 5x 89F_2K6bzqmPccY^HyFwq5OěZ4_ nw%>?<xAWe%u"Ńp[)eI0 $7T ^s|]5H4PW|0V,홢*\;U2Mlg7Xo,rEYB-aF4lQ4s$6a70ުǤ.BR0آVIAHj&I Cf/7^۞3m~m6j˗" 6j`hIB$(8h,u I A`VA1+l=zrУlKJ4d4DJAP8f)>@JJePbCwE)%IHS(?OeQbWvyeW/ߊwN=I8&G#ߥVba.<+WSdz if!n'!OwesƸO!ߤ ,f2! 9& pSi$D8Cep>R:ǃ16R2i#t +/4;^j1M?]9̗߻E/v`lh]BpE9-M /!]$(u\ck%D}ed !ѕT? f \䬼45Ԋl``<.(eMJ6Y2v+C~whٵ ymC3Sw-@ZL2q/A34NqZS;m|D1T Q"~ԾG;:=!Ce5Gax9]V[*gګi-4MIRI*/HWT7#J_'0T]+1C Nh68hZ?Zh(Ց.hn0Q6w:K0'܏7l$f1.n+`M=q<ÑVCH+! $ .ȉ>?x$+߻+,;A˙% _'q1`^{W#nxv-qomAf0fqJV*t n5-j10"0Uynj]{@BQGhvAgV^;Fk@ZiP:Q88C.9?(X[Gy`ܼS?s_PPwkyۊ71J1n79^b?M^]`|Zm2}FpJ${x9de KZvE8.8KRըTI0%Y>2M_jXDq{_,=wgK)^-QuoO]o<((h,xzE}Cu=rVH`+bW\w:3QlAv~$h>"!>%{$! L6ľ*ZGz@h`hvd^։F^;eBtz \um*ųXO17{6ak7k*(/ϵ`RƣsUڡ[s%b>57Ssѹ;BWCx5Y?~} W>146׽}6<*I'J,2[˳}}6VW>7p!Sp!Vlws"sc4A}Wwl9gY1_(.N_)UWP-1%=b,)_<*񝕍 kM1 |>_Rܶ+paC3_~;,F*Z c*eOk[LJɂy={D&1D_`m}_9vwe L!3 2(e|Qec@:1(mv0_82R-}407)hESYp1W KmjNsXV)k^$^`Y>EAU{!5Y R+QKNU~J׌[n]Wuȑ+a eMOq5n T@b˄DèZ/*,iM t7~6өMІT@l؉N'>c '++hyK. 'u5h[=aC 8QMPm6vlsk6)0."DkRXNZ,}@:Zst0Dv a)&.nqy-LD[Zoktx蔥}~b)Ly*3੤3\à<P."OA z@1=ty+9=w0?/S'RQ=@ yMF/yV#6@;.+9!q‡.~qı_CŪG7E*.žcNL1#Pe?ٌ !S ?c 5^I<4[6D޷@ecK.Rmĺj|ԇHln*Йe/n!aDx`S2uj7s['R7~ۗvv(7[^<mv/.8(K^z_=7h6̔kN[W#Z≴CmWQPcB ".>"؁$WJac ˳F\ bbg 6LaUR_p%,9lo%Ix'긎%q;.j7wȦimo&%~@gC00[~H>Sh29CZ6tF0a=g0!>]_'0˿Z#߻pˁQ9Ekl;1])2q{8g0kk\ojP!<"@XK,g0x!&T2tKe[cjk(=zx6"F:]^gȟ /߆z6xn<AB$`h(x[˴R C^tS$gqxSm};Q Or{<t*3?vy,+6r4X*ׯפO)5#-./O)()I-(>VppZAᴂԂ@1watuTQdyH‡nzyUe|vyT8\ޓ&tQVViA&>ѝ4ll N4Qw N{#xyGUrD?G.@81aeтee#ˤ*q~ I B; me/:KQʁP?,Tߓnsp5y>"MB4]mA3NP!"{^kX.b իGK[h*=aiaqJj@|>͇3_@@5\l_8fJY zEg 'vu(g)~=.!xjDWHVt<ȲۍTNw:KN"QWӿ(=sֿQis> Dco{. (gS[skdX, h,>FmWA@C裠?@oXh&t뚎hvIWuJRtIr-0Y&Vʀ'-8k;r/(b~C`^X{1ߋPҀnp${9* 4|Ӊ"htVc ;8^[mB.}Ȣ$t{F:>(L0-#_s杝fBÊ+%k7bNH|'q %4 QP=Ls-'Q}x,׽!w.+Oa(ekƢt8'RSyDL+qXCSha~_'_Dq>l4)hX(w"GaJF0ѕϡ+*'kXs'j ZaQ{Ǎ >mIaZ:LaX)pRWq Ssqe6ɄNg3'KMS1؉Ut.lE7xBi{*mY׸cݍ/:~1:}JlL]` PkXDv_V8pý/oµ-y}N:(UsL^`_ݼM}ϘiwLlx[ ]Zg; Z5!UDp31utIfɥj9&fzO #$aXԊɒW'%z J%<"tO.tf-N $`MнŁqAth Ǫdۅ\++D ! $`1F7=]ϞfC$r ԿE]ַߎ%V{} $P2n=".$,! $}oN] H#BV }3NovI)ggt+z]9ؑhDnC:rtIgf3B@u5 kUͿoS`mfgx<ad>zYzTXtSoftwarexMLOMLLV03ҳP047270S44SHI/-./H,JD(53ҳOOKI(Zȹ!zTXtThumb::Document::Pagesx322 "zTXtThumb::Image::heightx3466 k!zTXtThumb::Image::Widthx34w6"zTXtThumb::MimetypexMLO/K{x_9G zTXtThumb::MTimex342150660 T^zTXtThumb::Sizex3L60N!·^zTXtThumb::URIxA p3^}HC@#PC1|ߙ,O& [qɖ%'56 < ސ 2򁫋QӢryϞIENDB`cmislib-0.5.1/src/tests/__init__.py0000644000076500001200000000156612062716760017650 0ustar jpottsadmin00000000000000# # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # from cmislibtests import * cmislib-0.5.1/src/tests/cmislibtest.py0000644000076500001200000017751012062727376020443 0ustar jpottsadmin00000000000000# -*- coding: utf-8 -*- # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # ''' Unit tests for cmislib ''' import unittest from unittest import TestSuite, TestLoader from cmislib.model import CmisClient, ACE from cmislib.exceptions import \ ObjectNotFoundException, \ CmisException, \ NotSupportedException from cmislib import messages import os from time import sleep, time import settings ## Fix test file paths in case test is launched using nosetests my_dir = os.path.dirname(os.path.abspath(__file__)) try: os.stat(settings.TEST_BINARY_1) except: settings.TEST_BINARY_1 = os.path.join(my_dir, settings.TEST_BINARY_1) try: os.stat(settings.TEST_BINARY_2) except: settings.TEST_BINARY_2 = os.path.join(my_dir, settings.TEST_BINARY_2) class CmisTestBase(unittest.TestCase): """ Common ancestor class for most cmislib unit test classes. """ def setUp(self): """ Create a root test folder for the test. """ self._cmisClient = CmisClient(settings.REPOSITORY_URL, settings.USERNAME, settings.PASSWORD, **settings.EXT_ARGS) self._repo = self._cmisClient.getDefaultRepository() self._rootFolder = self._repo.getObjectByPath(settings.TEST_ROOT_PATH) self._folderName = " ".join(['cmislib', self.__class__.__name__, str(time())]) self._testFolder = self._rootFolder.createFolder(self._folderName) def tearDown(self): """ Clean up after the test. """ try: self._testFolder.deleteTree() except NotSupportedException: print "Couldn't delete test folder because deleteTree is not supported" class CmisClientTest(unittest.TestCase): """ Tests for the :class:`CmisClient` class. """ def testCmisClient(self): '''Instantiate a CmisClient object''' cmisClient = CmisClient(settings.REPOSITORY_URL, settings.USERNAME, settings.PASSWORD, **settings.EXT_ARGS) self.assert_(cmisClient != None) def testGetRepositories(self): '''Call getRepositories and make sure at least one comes back with an ID and a name ''' cmisClient = CmisClient(settings.REPOSITORY_URL, settings.USERNAME, settings.PASSWORD, **settings.EXT_ARGS) repoInfo = cmisClient.getRepositories() self.assert_(len(repoInfo) >= 1) self.assert_('repositoryId' in repoInfo[0]) self.assert_('repositoryName' in repoInfo[0]) def testDefaultRepository(self): '''Get the default repository by calling the repo's service URL''' cmisClient = CmisClient(settings.REPOSITORY_URL, settings.USERNAME, settings.PASSWORD, **settings.EXT_ARGS) repo = cmisClient.getDefaultRepository() self.assert_(repo != None) self.assert_(repo.getRepositoryId() != None) def testGetRepository(self): '''Get a repository by repository ID''' cmisClient = CmisClient(settings.REPOSITORY_URL, settings.USERNAME, settings.PASSWORD, **settings.EXT_ARGS) repo = cmisClient.getDefaultRepository() defaultRepoId = repo.getRepositoryId() defaultRepoName = repo.getRepositoryName() repo = cmisClient.getRepository(defaultRepoId) self.assertEquals(defaultRepoId, repo.getRepositoryId()) self.assertEquals(defaultRepoName, repo.getRepositoryName()) # Error conditions def testCmisClientBadUrl(self): '''Try to instantiate a CmisClient object with a known bad URL''' cmisClient = CmisClient(settings.REPOSITORY_URL + 'foobar', settings.USERNAME, settings.PASSWORD, **settings.EXT_ARGS) self.assertRaises(CmisException, cmisClient.getRepositories) def testGetRepositoryBadId(self): '''Try to get a repository with a bad repo ID''' cmisClient = CmisClient(settings.REPOSITORY_URL, settings.USERNAME, settings.PASSWORD, **settings.EXT_ARGS) self.assertRaises(ObjectNotFoundException, cmisClient.getRepository, '123FOO') class QueryTest(CmisTestBase): """ Tests related to running CMIS queries. """ # TODO: Test the rest of these queries # queryDateRange = "SELECT cmis:name from cmis:document " \ # "where cmis:creationDate >= TIMESTAMP'2009-11-10T00:00:00.000-06:00' and " \ # "cmis:creationDate < TIMESTAMP'2009-11-18T00:00:00.000-06:00'" # queryFolderFullText = "SELECT cmis:name from cmis:document " \ # "where in_folder('workspace://SpacesStore/3935ce21-9f6f-4d46-9e22-4f97e1d5d9d8') " \ # "and contains('contract')" # queryCombined = "SELECT cmis:name from cmis:document " \ # "where in_tree('workspace://SpacesStore/3935ce21-9f6f-4d46-9e22-4f97e1d5d9d8') and " \ # "contains('contract') and cm:description like \"%sign%\"" def setUp(self): """ Override the base setUp to include creating a couple of test docs. """ CmisTestBase.setUp(self) # I think this may be an Alfresco bug. The CMIS query results contain # 1 less entry element than the number of search results. So this test # will create two documents and search for the second one which should # work in all repositories. testFile = open(settings.TEST_BINARY_2, 'rb') self._testContent = self._testFolder.createDocument(testFile.name, contentFile=testFile) testFile.close() testFile = open(settings.TEST_BINARY_2, 'rb') self._testContent2 = self._testFolder.createDocument(settings.TEST_BINARY_2.replace('.', '2.'), contentFile=testFile) testFile.close() self._maxFullTextTries = settings.MAX_FULL_TEXT_TRIES def testSimpleSelect(self): '''Execute simple select star from cmis:document''' querySimpleSelect = "SELECT * FROM cmis:document" resultSet = self._repo.query(querySimpleSelect) self.assertTrue(isInResultSet(resultSet, self._testContent)) def testWildcardPropertyMatch(self): '''Find content w/wildcard match on cmis:name property''' name = self._testContent.getProperties()['cmis:name'] querySimpleSelect = "SELECT * FROM cmis:document where cmis:name like '" + name[:7] + "%'" resultSet = self._repo.query(querySimpleSelect) self.assertTrue(isInResultSet(resultSet, self._testContent)) def testPropertyMatch(self): '''Find content matching cmis:name property''' name = self._testContent2.getProperties()['cmis:name'] querySimpleSelect = "SELECT * FROM cmis:document where cmis:name = '" + name + "'" resultSet = self._repo.query(querySimpleSelect) self.assertTrue(isInResultSet(resultSet, self._testContent2)) def testFullText(self): '''Find content using a full-text query''' queryFullText = "SELECT cmis:objectId, cmis:name FROM cmis:document " \ "WHERE contains('whitepaper')" # on the first full text search the indexer may need a chance to # do its thing found = False maxTries = self._maxFullTextTries while not found and (maxTries > 0): resultSet = self._repo.query(queryFullText) found = isInResultSet(resultSet, self._testContent2) if not found: maxTries -= 1 print 'Not found...sleeping for 10 secs. Remaining tries:%d' % maxTries sleep(settings.FULL_TEXT_WAIT) self.assertTrue(found) def testScore(self): '''Find content using FT, sorted by relevance score''' queryScore = "SELECT cmis:objectId, cmis:name, Score() as relevance " \ "FROM cmis:document WHERE contains('sample') " \ "order by relevance DESC" # on the first full text search the indexer may need a chance to # do its thing found = False maxTries = self._maxFullTextTries while not found and (maxTries > 0): resultSet = self._repo.query(queryScore) found = isInResultSet(resultSet, self._testContent2) if not found: maxTries -= 1 print 'Not found...sleeping for 10 secs. Remaining tries:%d' % maxTries sleep(10) self.assertTrue(found) class RepositoryTest(CmisTestBase): """ Tests for the :class:`Repository` class. """ def testRepositoryInfo(self): '''Retrieve repository info''' repoInfo = self._repo.getRepositoryInfo() self.assertTrue('repositoryId' in repoInfo) self.assertTrue('repositoryName' in repoInfo) self.assertTrue('repositoryDescription' in repoInfo) self.assertTrue('vendorName' in repoInfo) self.assertTrue('productName' in repoInfo) self.assertTrue('productVersion' in repoInfo) self.assertTrue('rootFolderId' in repoInfo) self.assertTrue('cmisVersionSupported' in repoInfo) def testRepositoryCapabilities(self): '''Retrieve repository capabilities''' caps = self._repo.getCapabilities() self.assertTrue('ACL' in caps) self.assertTrue('AllVersionsSearchable' in caps) self.assertTrue('Changes' in caps) self.assertTrue('ContentStreamUpdatability' in caps) self.assertTrue('GetDescendants' in caps) self.assertTrue('GetFolderTree' in caps) self.assertTrue('Multifiling' in caps) self.assertTrue('PWCSearchable' in caps) self.assertTrue('PWCUpdatable' in caps) self.assertTrue('Query' in caps) self.assertTrue('Renditions' in caps) self.assertTrue('Unfiling' in caps) self.assertTrue('VersionSpecificFiling' in caps) self.assertTrue('Join' in caps) def testGetRootFolder(self): '''Get the root folder of the repository''' rootFolder = self._repo.getRootFolder() self.assert_(rootFolder != None) self.assert_(rootFolder.getObjectId() != None) def testCreateFolder(self): '''Create a new folder in the root folder''' folderName = 'testCreateFolder folder' newFolder = self._repo.createFolder(self._rootFolder, folderName) self.assertEquals(folderName, newFolder.getName()) newFolder.delete() def testCreateDocument(self): '''Create a new 'content-less' document''' documentName = 'testDocument' newDoc = self._repo.createDocument(documentName, parentFolder=self._testFolder) self.assertEquals(documentName, newDoc.getName()) def testCreateDocumentFromString(self): '''Create a new document from a string''' documentName = 'testDocument' contentString = 'Test content string' newDoc = self._repo.createDocumentFromString(documentName, parentFolder=self._testFolder, contentString=contentString, contentType='text/plain') self.assertEquals(documentName, newDoc.getName()) self.assertEquals(newDoc.getContentStream().read(), contentString) # CMIS-279 def testCreateDocumentUnicode(self): '''Create a new doc with unicode characters in the name''' documentName = u'abc cdeöäüß%§-_caféè.txt' newDoc = self._repo.createDocument(documentName, parentFolder=self._testFolder) self.assertEquals(documentName, newDoc.getName()) def testGetObject(self): '''Create a test folder then attempt to retrieve it as a :class:`CmisObject` object using its object ID''' folderName = 'testGetObject folder' newFolder = self._repo.createFolder(self._testFolder, folderName) objectId = newFolder.getObjectId() someObject = self._repo.getObject(objectId) self.assertEquals(folderName, someObject.getName()) newFolder.delete() def testReturnVersion(self): '''Get latest and latestmajor versions of an object''' f = open(settings.TEST_BINARY_1, 'rb') doc10 = self._testFolder.createDocument(settings.TEST_BINARY_1, contentFile=f) doc10Id = doc10.getObjectId() if (not doc10.allowableActions['canCheckOut']): print 'The test doc cannot be checked out...skipping' return pwc = doc10.checkout() doc11 = pwc.checkin(major='false') # checkin a minor version, 1.1 if (not doc11.allowableActions['canCheckOut']): print 'The test doc cannot be checked out...skipping' return pwc = doc11.checkout() doc20 = pwc.checkin() # checkin a major version, 2.0 doc20Id = doc20.getObjectId() if (not doc20.allowableActions['canCheckOut']): print 'The test doc cannot be checked out...skipping' return pwc = doc20.checkout() doc21 = pwc.checkin(major='false') # checkin a minor version, 2.1 doc21Id = doc21.getObjectId() docLatest = self._repo.getObject(doc10Id, returnVersion='latest') self.assertEquals(doc21Id, docLatest.getObjectId()) docLatestMajor = self._repo.getObject(doc10Id, returnVersion='latestmajor') self.assertEquals(doc20Id, docLatestMajor.getObjectId()) def testGetFolder(self): '''Create a test folder then attempt to retrieve the Folder object using its object ID''' folderName = 'testGetFolder folder' newFolder = self._repo.createFolder(self._testFolder, folderName) objectId = newFolder.getObjectId() someFolder = self._repo.getFolder(objectId) self.assertEquals(folderName, someFolder.getName()) newFolder.delete() def testGetObjectByPath(self): '''Create test objects (one folder, one document) then try to get them by path''' # names of folders and test docs parentFolderName = 'testGetObjectByPath folder' subFolderName = 'subfolder' docName = 'testdoc' # create the folder structure parentFolder = self._testFolder.createFolder(parentFolderName) subFolder = parentFolder.createFolder(subFolderName) # use the subfolder path to get the folder by path subFolderPath = subFolder.getProperties().get("cmis:path") searchFolder = self._repo.getObjectByPath(subFolderPath) self.assertEquals(subFolder.getObjectId(), searchFolder.getObjectId()) # create a test doc doc = subFolder.createDocument(docName) # ask the doc for its paths searchDocPaths = doc.getPaths() # for each path in the list, try to get the object by path # this is better than building a path with the doc's name b/c the name # isn't guaranteed to be used as the path segment (see CMIS-232) for path in searchDocPaths: searchDoc = self._repo.getObjectByPath(path) self.assertEquals(doc.getObjectId(), searchDoc.getObjectId()) # get the subfolder by path, then ask for its children subFolder = self._repo.getObjectByPath(subFolderPath) self.assertEquals(len(subFolder.getChildren().getResults()), 1) def testGetUnfiledDocs(self): '''Tests the repository's unfiled collection''' if not self._repo.getCapabilities()['Unfiling']: print 'Repo does not support unfiling, skipping' return # create a test folder and test doc testFolder = self._testFolder.createFolder('unfile test') newDoc = testFolder.createDocument('testdoc') # make sure the new doc isn't in the unfiled collection try: rs = self._repo.getUnfiledDocs() self.assertFalse(isInResultSet(rs, newDoc)) except NotSupportedException: print 'This repository does not support read access to the unfiled collection...skipping' return # delete the test folder and tell it to unfile the testdoc objId = newDoc.getObjectId() testFolder.deleteTree(unfileObjects='unfile') # grab the document by object ID newDoc = self._repo.getObject(objId) # the doc should now be in the unfiled collection self.assertTrue(isInResultSet(self._repo.getUnfiledDocs(), newDoc)) self.assertEquals('testdoc', newDoc.getTitle()) # def testCreateUnfiledDocument(self): # '''Create a new unfiled document''' # if self._repo.getCapabilities()['Unfiling'] != True: # print 'Repo does not support unfiling, skipping' # return # documentName = 'testDocument' # newDoc = self._repo.createDocument(documentName) # self.assertEquals(documentName, newDoc.getName()) def testMoveDocument(self): '''Move a Document from one folder to another folder''' subFolder1 = self._testFolder.createFolder('sub1') doc = subFolder1.createDocument('testdoc1') self.assertEquals(len(subFolder1.getChildren()), 1) subFolder2 = self._testFolder.createFolder('sub2') self.assertEquals(len(subFolder2.getChildren()), 0) doc.move(subFolder1, subFolder2) self.assertEquals(len(subFolder1.getChildren()), 0) self.assertEquals(len(subFolder2.getChildren()), 1) self.assertEquals(doc.name, subFolder2.getChildren()[0].name) #Exceptions def testGetObjectBadId(self): '''Attempt to get an object using a known bad ID''' # this object ID is implementation specific (Alfresco) but is universally # bad so it should work for all repositories self.assertRaises(ObjectNotFoundException, self._repo.getObject, self._testFolder.getObjectId()[:-5] + 'BADID') def testGetObjectBadPath(self): '''Attempt to get an object using a known bad path''' self.assertRaises(ObjectNotFoundException, self._repo.getObjectByPath, '/123foo/BAR.jtp') class FolderTest(CmisTestBase): """ Tests for the :class:`Folder` class """ def testGetChildren(self): '''Get the children of the test folder''' childFolderName1 = 'testchild1' childFolderName2 = 'testchild2' grandChildFolderName = 'testgrandchild' childFolder1 = self._testFolder.createFolder(childFolderName1) childFolder2 = self._testFolder.createFolder(childFolderName2) grandChild = childFolder2.createFolder(grandChildFolderName) resultSet = self._testFolder.getChildren() self.assert_(resultSet != None) self.assertEquals(2, len(resultSet.getResults())) self.assertTrue(isInResultSet(resultSet, childFolder1)) self.assertTrue(isInResultSet(resultSet, childFolder2)) self.assertFalse(isInResultSet(resultSet, grandChild)) def testGetDescendants(self): '''Get the descendants of the root folder''' childFolderName1 = 'testchild1' childFolderName2 = 'testchild2' grandChildFolderName1 = 'testgrandchild' childFolder1 = self._testFolder.createFolder(childFolderName1) childFolder2 = self._testFolder.createFolder(childFolderName2) grandChild = childFolder1.createFolder(grandChildFolderName1) # test getting descendants with depth=1 resultSet = self._testFolder.getDescendants(depth=1) self.assert_(resultSet != None) self.assertEquals(2, len(resultSet.getResults())) self.assertTrue(isInResultSet(resultSet, childFolder1)) self.assertTrue(isInResultSet(resultSet, childFolder2)) self.assertFalse(isInResultSet(resultSet, grandChild)) # test getting descendants with depth=2 resultSet = self._testFolder.getDescendants(depth=2) self.assert_(resultSet != None) self.assertEquals(3, len(resultSet.getResults())) self.assertTrue(isInResultSet(resultSet, childFolder1)) self.assertTrue(isInResultSet(resultSet, childFolder2)) self.assertTrue(isInResultSet(resultSet, grandChild)) # test getting descendants with depth=-1 resultSet = self._testFolder.getDescendants() # -1 is the default depth self.assert_(resultSet != None) self.assertEquals(3, len(resultSet.getResults())) self.assertTrue(isInResultSet(resultSet, childFolder1)) self.assertTrue(isInResultSet(resultSet, childFolder2)) self.assertTrue(isInResultSet(resultSet, grandChild)) def testGetTree(self): '''Get the folder tree of the test folder''' childFolderName1 = 'testchild1' childFolderName2 = 'testchild2' grandChildFolderName1 = 'testgrandchild' childFolder1 = self._testFolder.createFolder(childFolderName1) childFolder1.createDocument('testdoc1') childFolder2 = self._testFolder.createFolder(childFolderName2) childFolder2.createDocument('testdoc2') grandChild = childFolder1.createFolder(grandChildFolderName1) grandChild.createDocument('testdoc3') # test getting tree with depth=1 resultSet = self._testFolder.getTree(depth=1) self.assert_(resultSet != None) self.assertEquals(2, len(resultSet.getResults())) self.assertTrue(isInResultSet(resultSet, childFolder1)) self.assertTrue(isInResultSet(resultSet, childFolder2)) self.assertFalse(isInResultSet(resultSet, grandChild)) # test getting tree with depth=2 resultSet = self._testFolder.getTree(depth=2) self.assert_(resultSet != None) self.assertEquals(3, len(resultSet.getResults())) self.assertTrue(isInResultSet(resultSet, childFolder1)) self.assertTrue(isInResultSet(resultSet, childFolder2)) self.assertTrue(isInResultSet(resultSet, grandChild)) def testDeleteEmptyFolder(self): '''Create a test folder, then delete it''' folderName = 'testDeleteEmptyFolder folder' testFolder = self._testFolder.createFolder(folderName) self.assertEquals(folderName, testFolder.getName()) newFolder = testFolder.createFolder('testFolder') testFolderChildren = testFolder.getChildren() self.assertEquals(1, len(testFolderChildren.getResults())) newFolder.delete() testFolderChildren = testFolder.getChildren() self.assertEquals(0, len(testFolderChildren.getResults())) def testDeleteNonEmptyFolder(self): '''Create a test folder with something in it, then delete it''' folderName = 'testDeleteNonEmptyFolder folder' testFolder = self._testFolder.createFolder(folderName) self.assertEquals(folderName, testFolder.getName()) newFolder = testFolder.createFolder('testFolder') testFolderChildren = testFolder.getChildren() self.assertEquals(1, len(testFolderChildren.getResults())) newFolder.createDocument('testDoc') self.assertEquals(1, len(newFolder.getChildren().getResults())) newFolder.deleteTree() testFolderChildren = testFolder.getChildren() self.assertEquals(0, len(testFolderChildren.getResults())) def testGetProperties(self): '''Get the root folder, then get its properties''' props = self._testFolder.getProperties() self.assert_(props != None) self.assert_('cmis:objectId' in props) self.assert_(props['cmis:objectId'] != None) self.assert_('cmis:objectTypeId' in props) self.assert_(props['cmis:objectTypeId'] != None) self.assert_('cmis:name' in props) self.assert_(props['cmis:name'] != None) def testPropertyFilter(self): '''Test the properties filter''' # names of folders and test docs parentFolderName = 'testGetObjectByPath folder' subFolderName = 'subfolder' # create the folder structure parentFolder = self._testFolder.createFolder(parentFolderName) subFolder = parentFolder.createFolder(subFolderName) subFolderPath = subFolder.getProperties().get("cmis:path") # Per CMIS-170, CMIS providers are not required to filter the # properties returned. So these tests will check only for the presence # of the properties asked for, not the absence of properties that # should be filtered if the server chooses to do so. # test when used with getObjectByPath searchFolder = self._repo.getObjectByPath(subFolderPath, filter='cmis:objectId,cmis:objectTypeId,cmis:baseTypeId') self.assertEquals(subFolder.getObjectId(), searchFolder.getObjectId()) self.assertTrue(searchFolder.getProperties().has_key('cmis:objectId')) self.assertTrue(searchFolder.getProperties().has_key('cmis:objectTypeId')) self.assertTrue(searchFolder.getProperties().has_key('cmis:baseTypeId')) # test when used with getObjectByPath + reload searchFolder = self._repo.getObjectByPath(subFolderPath, filter='cmis:objectId,cmis:objectTypeId,cmis:baseTypeId') searchFolder.reload() self.assertEquals(subFolder.getObjectId(), searchFolder.getObjectId()) self.assertTrue(searchFolder.getProperties().has_key('cmis:objectId')) self.assertTrue(searchFolder.getProperties().has_key('cmis:objectTypeId')) self.assertTrue(searchFolder.getProperties().has_key('cmis:baseTypeId')) # test when used with getObject searchFolder = self._repo.getObject(subFolder.getObjectId(), filter='cmis:objectId,cmis:objectTypeId,cmis:baseTypeId') self.assertEquals(subFolder.getObjectId(), searchFolder.getObjectId()) self.assertTrue(searchFolder.getProperties().has_key('cmis:objectId')) self.assertTrue(searchFolder.getProperties().has_key('cmis:objectTypeId')) self.assertTrue(searchFolder.getProperties().has_key('cmis:baseTypeId')) # test when used with getObject + reload searchFolder = self._repo.getObject(subFolder.getObjectId(), filter='cmis:objectId,cmis:objectTypeId,cmis:baseTypeId') searchFolder.reload() self.assertEquals(subFolder.getObjectId(), searchFolder.getObjectId()) self.assertTrue(searchFolder.getProperties().has_key('cmis:objectId')) self.assertTrue(searchFolder.getProperties().has_key('cmis:objectTypeId')) self.assertTrue(searchFolder.getProperties().has_key('cmis:baseTypeId')) # test that you can do a reload with a reset filter searchFolder.reload(filter='*') self.assertTrue(searchFolder.getProperties().has_key('cmis:objectId')) self.assertTrue(searchFolder.getProperties().has_key('cmis:objectTypeId')) self.assertTrue(searchFolder.getProperties().has_key('cmis:baseTypeId')) self.assertTrue(searchFolder.getProperties().has_key('cmis:name')) def testUpdateProperties(self): '''Create a test folder, then update its properties''' folderName = 'testUpdateProperties folder' newFolder = self._testFolder.createFolder(folderName) self.assertEquals(folderName, newFolder.getName()) folderName2 = 'testUpdateProperties folder2' props = {'cmis:name': folderName2} newFolder.updateProperties(props) self.assertEquals(folderName2, newFolder.getName()) def testSubFolder(self): '''Create a test folder, then create a test folder within that.''' parentFolder = self._testFolder.createFolder('testSubFolder folder') self.assert_('cmis:objectId' in parentFolder.getProperties()) childFolder = parentFolder.createFolder('child folder') self.assert_('cmis:objectId' in childFolder.getProperties()) self.assert_(childFolder.getProperties()['cmis:objectId'] != None) def testAllowableActions(self): '''Create a test folder, then get its allowable actions''' actions = self._testFolder.getAllowableActions() self.assert_(len(actions) > 0) def testGetParent(self): '''Get a folder's parent using the getParent call''' childFolder = self._testFolder.createFolder('parentTest') parentFolder = childFolder.getParent() self.assertEquals(self._testFolder.getObjectId(), parentFolder.getObjectId()) def testAddObject(self): '''Add an existing object to another folder''' if not self._repo.getCapabilities()['Multifiling']: print 'This repository does not allow multifiling, skipping' return subFolder1 = self._testFolder.createFolder('sub1') doc = subFolder1.createDocument('testdoc1') self.assertEquals(len(subFolder1.getChildren()), 1) subFolder2 = self._testFolder.createFolder('sub2') self.assertEquals(len(subFolder2.getChildren()), 0) subFolder2.addObject(doc) self.assertEquals(len(subFolder2.getChildren()), 1) self.assertEquals(subFolder1.getChildren()[0].name, subFolder2.getChildren()[0].name) def testRemoveObject(self): '''Remove an existing object from a secondary folder''' if not self._repo.getCapabilities()['Unfiling']: print 'This repository does not allow unfiling, skipping' return subFolder1 = self._testFolder.createFolder('sub1') doc = subFolder1.createDocument('testdoc1') self.assertEquals(len(subFolder1.getChildren()), 1) subFolder2 = self._testFolder.createFolder('sub2') self.assertEquals(len(subFolder2.getChildren()), 0) subFolder2.addObject(doc) self.assertEquals(len(subFolder2.getChildren()), 1) self.assertEquals(subFolder1.getChildren()[0].name, subFolder2.getChildren()[0].name) subFolder2.removeObject(doc) self.assertEquals(len(subFolder2.getChildren()), 0) self.assertEquals(len(subFolder1.getChildren()), 1) self.assertEquals(doc.name, subFolder1.getChildren()[0].name) def testGetPaths(self): '''Get a folder's paths''' # ask the root for its path root = self._repo.getRootFolder() paths = root.getPaths() self.assertTrue(len(paths) == 1) self.assertTrue(paths[0] == '/') # ask the test folder for its paths paths = self._testFolder.getPaths() self.assertTrue(len(paths) == 1) # Exceptions def testBadParentFolder(self): '''Try to create a folder on a bad/bogus/deleted parent folder object''' firstFolder = self._testFolder.createFolder('testBadParentFolder folder') self.assert_('cmis:objectId' in firstFolder.getProperties()) firstFolder.delete() # folder isn't in the repo anymore, but I still have the object # really, this seems like it ought to be an ObjectNotFoundException but # not all CMIS providers report it as such self.assertRaises(CmisException, firstFolder.createFolder, 'bad parent') # Per CMIS-169, nothing in the spec says that an exception should be thrown # when a duplicate folder is created, so this test is really not necessary. # def testDuplicateFolder(self): # '''Try to create a folder that already exists''' # folderName = 'testDupFolder folder' # firstFolder = self._testFolder.createFolder(folderName) # self.assert_('cmis:objectId' in firstFolder.getProperties()) # # really, this needs to be ContentAlreadyExistsException but # # not all CMIS providers report it as such # self.assertRaises(CmisException, # self._testFolder.createFolder, # folderName) class ChangeEntryTest(CmisTestBase): """ Tests for the :class:`ChangeEntry` class """ def testGetContentChanges(self): """Get the content changes and inspect Change Entry props""" # need to check changes capability if not self._repo.capabilities['Changes']: print messages.NO_CHANGE_LOG_SUPPORT return # at least one change should have been made due to the creation of the # test documents rs = self._repo.getContentChanges() self.assertTrue(len(rs) > 0) changeEntry = rs[0] self.assertTrue(changeEntry.id) self.assertTrue(changeEntry.changeType in ['created', 'updated', 'deleted', 'security']) self.assertTrue(changeEntry.changeTime) def testGetACL(self): """Gets the ACL that is included with a Change Entry.""" # need to check changes capability if not self._repo.capabilities['Changes']: print messages.NO_CHANGE_LOG_SUPPORT return # need to check ACL capability if not self._repo.capabilities['ACL']: print messages.NO_ACL_SUPPORT return # need to test once with includeACL set to true rs = self._repo.getContentChanges(includeACL='true') self.assertTrue(len(rs) > 0) changeEntry = rs[0] acl = changeEntry.getACL() self.assertTrue(acl) for entry in acl.getEntries().values(): self.assertTrue(entry.principalId) self.assertTrue(entry.permissions) # need to test once without includeACL set rs = self._repo.getContentChanges() self.assertTrue(len(rs) > 0) changeEntry = rs[0] acl = changeEntry.getACL() self.assertTrue(acl) for entry in acl.getEntries().values(): self.assertTrue(entry.principalId) self.assertTrue(entry.permissions) def testGetProperties(self): """Gets the properties of an object included with a Change Entry.""" # need to check changes capability changeCap = self._repo.capabilities['Changes'] if not changeCap: print messages.NO_CHANGE_LOG_SUPPORT return # need to test once without includeProperties set. the objectID should be there rs = self._repo.getContentChanges() self.assertTrue(len(rs) > 0) changeEntry = rs[0] self.assertTrue(changeEntry.properties['cmis:objectId']) # need to test once with includeProperties set. the objectID should be there plus object props if changeCap in ['properties', 'all']: rs = self._repo.getContentChanges(includeProperties='true') self.assertTrue(len(rs) > 0) changeEntry = rs[0] self.assertTrue(changeEntry.properties['cmis:objectId']) self.assertTrue(changeEntry.properties['cmis:name']) class DocumentTest(CmisTestBase): """ Tests for the :class:`Document` class """ def testCheckout(self): '''Create a document in a test folder, then check it out''' newDoc = self._testFolder.createDocument('testDocument') if (not newDoc.allowableActions['canCheckOut']): print 'The test doc cannot be checked out...skipping' return pwcDoc = newDoc.checkout() try: self.assertTrue(newDoc.isCheckedOut()) self.assert_('cmis:objectId' in newDoc.getProperties()) self.assert_('cmis:objectId' in pwcDoc.getProperties()) checkedOutDocs = self._repo.getCollection('checkedout') self.assertTrue(isInResultSet(checkedOutDocs, pwcDoc)) finally: pwcDoc.delete() def testCheckin(self): '''Create a document in a test folder, check it out, then in''' testFilename = settings.TEST_BINARY_1 contentFile = open(testFilename, 'rb') testDoc = self._testFolder.createDocument(testFilename, contentFile=contentFile) contentFile.close() self.assertEquals(testFilename, testDoc.getName()) if (not testDoc.allowableActions['canCheckOut']): print 'The test doc cannot be checked out...skipping' return pwcDoc = testDoc.checkout() try: self.assertTrue(testDoc.isCheckedOut()) self.assert_('cmis:objectId' in testDoc.getProperties()) self.assert_('cmis:objectId' in pwcDoc.getProperties()) testDoc = pwcDoc.checkin() self.assertFalse(testDoc.isCheckedOut()) finally: if testDoc.isCheckedOut(): pwcDoc.delete() def testCheckinComment(self): '''Checkin a document with a comment''' testFilename = settings.TEST_BINARY_1 contentFile = open(testFilename, 'rb') testDoc = self._testFolder.createDocument(testFilename, contentFile=contentFile) contentFile.close() self.assertEquals(testFilename, testDoc.getName()) if (not testDoc.allowableActions['canCheckOut']): print 'The test doc cannot be checked out...skipping' return pwcDoc = testDoc.checkout() try: self.assertTrue(testDoc.isCheckedOut()) testDoc = pwcDoc.checkin(checkinComment='Just a few changes') self.assertFalse(testDoc.isCheckedOut()) self.assertEquals('Just a few changes', testDoc.getProperties()['cmis:checkinComment']) finally: if testDoc.isCheckedOut(): pwcDoc.delete() def testCheckinAfterGetPWC(self): '''Create a document in a test folder, check it out, call getPWC, then checkin''' if not self._repo.getCapabilities()['PWCUpdatable'] == True: print 'Repository does not support PWCUpdatable, skipping' return testFilename = settings.TEST_BINARY_1 contentFile = open(testFilename, 'rb') testDoc = self._testFolder.createDocument(testFilename, contentFile=contentFile) contentFile.close() self.assertEquals(testFilename, testDoc.getName()) # Alfresco has a bug where if you get the PWC this way # the checkin will not be successful if (not testDoc.allowableActions['canCheckOut']): print 'The test doc cannot be checked out...skipping' return testDoc.checkout() pwcDoc = testDoc.getPrivateWorkingCopy() try: self.assertTrue(testDoc.isCheckedOut()) self.assert_('cmis:objectId' in testDoc.getProperties()) self.assert_('cmis:objectId' in pwcDoc.getProperties()) testDoc = pwcDoc.checkin() self.assertFalse(testDoc.isCheckedOut()) finally: if testDoc.isCheckedOut(): pwcDoc.delete() def testCancelCheckout(self): '''Create a document in a test folder, check it out, then cancel checkout''' newDoc = self._testFolder.createDocument('testDocument') if (not newDoc.allowableActions['canCheckOut']): print 'The test doc cannot be checked out...skipping' return pwcDoc = newDoc.checkout() try: self.assertTrue(newDoc.isCheckedOut()) self.assert_('cmis:objectId' in newDoc.getProperties()) self.assert_('cmis:objectId' in pwcDoc.getProperties()) checkedOutDocs = self._repo.getCollection('checkedout') self.assertTrue(isInResultSet(checkedOutDocs, pwcDoc)) finally: pwcDoc.delete() self.assertFalse(newDoc.isCheckedOut()) checkedOutDocs = self._repo.getCollection('checkedout') self.assertFalse(isInResultSet(checkedOutDocs, pwcDoc)) def testDeleteDocument(self): '''Create a document in a test folder, then delete it''' newDoc = self._testFolder.createDocument('testDocument') children = self._testFolder.getChildren() self.assertEquals(1, len(children.getResults())) newDoc.delete() children = self._testFolder.getChildren() self.assertEquals(0, len(children.getResults())) def testGetLatestVersion(self): '''Get latest version of an object''' f = open(settings.TEST_BINARY_1, 'rb') doc10 = self._testFolder.createDocument(settings.TEST_BINARY_1, contentFile=f) if (not doc10.allowableActions['canCheckOut']): print 'The test doc cannot be checked out...skipping' return pwc = doc10.checkout() doc11 = pwc.checkin(major='false') # checkin a minor version, 1.1 if (not doc11.allowableActions['canCheckOut']): print 'The test doc cannot be checked out...skipping' return pwc = doc11.checkout() doc20 = pwc.checkin() # checkin a major version, 2.0 doc20Id = doc20.getObjectId() if (not doc20.allowableActions['canCheckOut']): print 'The test doc cannot be checked out...skipping' return pwc = doc20.checkout() doc21 = pwc.checkin(major='false') # checkin a minor version, 2.1 doc21Id = doc21.getObjectId() docLatest = doc10.getLatestVersion() self.assertEquals(doc21Id, docLatest.getObjectId()) docLatestMajor = doc10.getLatestVersion(major='true') self.assertEquals(doc20Id, docLatestMajor.getObjectId()) def testGetPropertiesOfLatestVersion(self): '''Get properties of latest version of an object''' f = open(settings.TEST_BINARY_1, 'rb') doc10 = self._testFolder.createDocument(settings.TEST_BINARY_1, contentFile=f) if (not doc10.allowableActions['canCheckOut']): print 'The test doc cannot be checked out...skipping' return pwc = doc10.checkout() doc11 = pwc.checkin(major='false') # checkin a minor version, 1.1 if (not doc11.allowableActions['canCheckOut']): print 'The test doc cannot be checked out...skipping' return pwc = doc11.checkout() doc20 = pwc.checkin() # checkin a major version, 2.0 # what comes back from a checkin may not include all props, so reload doc20.reload() doc20Label = doc20.getProperties()['cmis:versionLabel'] if (not doc20.allowableActions['canCheckOut']): print 'The test doc cannot be checked out...skipping' return pwc = doc20.checkout() doc21 = pwc.checkin(major='false') # checkin a minor version, 2.1 # what comes back from a checkin may not include all props, so reload doc21.reload() doc21Label = doc21.getProperties()['cmis:versionLabel'] propsLatest = doc10.getPropertiesOfLatestVersion() self.assertEquals(doc21Label, propsLatest['cmis:versionLabel']) propsLatestMajor = doc10.getPropertiesOfLatestVersion(major='true') self.assertEquals(doc20Label, propsLatestMajor['cmis:versionLabel']) def testGetProperties(self): '''Create a document in a test folder, then get its properties''' newDoc = self._testFolder.createDocument('testDocument') self.assertEquals('testDocument', newDoc.getName()) self.assertTrue('cmis:objectTypeId' in newDoc.getProperties()) self.assertTrue('cmis:objectId' in newDoc.getProperties()) def testAllowableActions(self): '''Create document in a test folder, then get its allowable actions''' newDoc = self._testFolder.createDocument('testDocument') actions = newDoc.getAllowableActions() self.assert_(len(actions) > 0) def testUpdateProperties(self): '''Create a document in a test folder, then update its properties''' newDoc = self._testFolder.createDocument('testDocument') self.assertEquals('testDocument', newDoc.getName()) props = {'cmis:name': 'testDocument2'} newDoc.updateProperties(props) self.assertEquals('testDocument2', newDoc.getName()) def testSetContentStreamPWC(self): '''Set the content stream on the PWC''' if self._repo.getCapabilities()['ContentStreamUpdatability'] == 'none': print 'This repository does not allow content stream updates, skipping' return testFile1 = settings.TEST_BINARY_1 testFile1Size = os.path.getsize(testFile1) exportFile1 = testFile1.replace('.', 'export.') testFile2 = settings.TEST_BINARY_2 testFile2Size = os.path.getsize(testFile2) exportFile2 = testFile1.replace('.', 'export.') # create a test document contentFile = open(testFile1, 'rb') newDoc = self._testFolder.createDocument(testFile1, contentFile=contentFile) contentFile.close() # export the test document result = newDoc.getContentStream() outfile = open(exportFile1, 'wb') outfile.write(result.read()) result.close() outfile.close() # the file we exported should be the same size as the file we # originally created self.assertEquals(testFile1Size, os.path.getsize(exportFile1)) # checkout the file if (not newDoc.allowableActions['canCheckOut']): print 'The test doc cannot be checked out...skipping' return pwc = newDoc.checkout() # update the PWC with a new file f = open(testFile2, 'rb') pwc.setContentStream(f) f.close() # checkin the PWC newDoc = pwc.checkin() # export the checked in document result = newDoc.getContentStream() outfile = open(exportFile2, 'wb') outfile.write(result.read()) result.close() outfile.close() # the file we exported should be the same size as the file we # checked in after updating the PWC self.assertEquals(testFile2Size, os.path.getsize(exportFile2)) os.remove(exportFile2) def testSetContentStreamPWCMimeType(self): '''Check the mimetype after the PWC checkin''' if self._repo.getCapabilities()['ContentStreamUpdatability'] == 'none': print 'This repository does not allow content stream updates, skipping' return testFile1 = settings.TEST_BINARY_1 # create a test document contentFile = open(testFile1, 'rb') newDoc = self._testFolder.createDocument(testFile1, contentFile=contentFile) origMimeType = newDoc.properties['cmis:contentStreamMimeType'] contentFile.close() # checkout the file if (not newDoc.allowableActions['canCheckOut']): print 'The test doc cannot be checked out...skipping' return pwc = newDoc.checkout() # update the PWC with a new file f = open(testFile1, 'rb') pwc.setContentStream(f) f.close() # checkin the PWC newDoc = pwc.checkin() # CMIS-231 the checked in doc should have the same mime type as # the original document self.assertEquals(origMimeType, newDoc.properties['cmis:contentStreamMimeType']) def testSetContentStreamDoc(self): '''Set the content stream on a doc that's not checked out''' if self._repo.getCapabilities()['ContentStreamUpdatability'] != 'anytime': print 'This repository does not allow content stream updates on the doc, skipping' return testFile1 = settings.TEST_BINARY_1 testFile1Size = os.path.getsize(testFile1) exportFile1 = testFile1.replace('.', 'export.') testFile2 = settings.TEST_BINARY_2 testFile2Size = os.path.getsize(testFile2) exportFile2 = testFile1.replace('.', 'export.') # create a test document contentFile = open(testFile1, 'rb') newDoc = self._testFolder.createDocument(testFile1, contentFile=contentFile) contentFile.close() # export the test document result = newDoc.getContentStream() outfile = open(exportFile1, 'wb') outfile.write(result.read()) result.close() outfile.close() # the file we exported should be the same size as the file we # originally created self.assertEquals(testFile1Size, os.path.getsize(exportFile1)) # update the PWC with a new file f = open(testFile2, 'rb') newDoc.setContentStream(f) f.close() # export the checked in document result = newDoc.getContentStream() outfile = open(exportFile2, 'wb') outfile.write(result.read()) result.close() outfile.close() # the file we exported should be the same size as the file we # checked in after updating the PWC self.assertEquals(testFile2Size, os.path.getsize(exportFile2)) os.remove(exportFile2) def testDeleteContentStreamPWC(self): '''Delete the content stream of a PWC''' if self._repo.getCapabilities()['ContentStreamUpdatability'] == 'none': print 'This repository does not allow content stream updates, skipping' return if not self._repo.getCapabilities()['PWCUpdatable'] == True: print 'Repository does not support PWCUpdatable, skipping' return # create a test document contentFile = open(settings.TEST_BINARY_1, 'rb') newDoc = self._testFolder.createDocument(settings.TEST_BINARY_1, contentFile=contentFile) contentFile.close() if (not newDoc.allowableActions['canCheckOut']): print 'The test doc cannot be checked out...skipping' return pwc = newDoc.checkout() pwc.deleteContentStream() self.assertRaises(CmisException, pwc.getContentStream) pwc.delete() def testCreateDocumentBinary(self): '''Create a binary document using a file from the file system''' testFilename = settings.TEST_BINARY_1 contentFile = open(testFilename, 'rb') newDoc = self._testFolder.createDocument(testFilename, contentFile=contentFile) contentFile.close() self.assertEquals(testFilename, newDoc.getName()) # test to make sure the file we get back is the same length # as the file we sent result = newDoc.getContentStream() exportFilename = testFilename.replace('.', 'export.') outfile = open(exportFilename, 'wb') outfile.write(result.read()) result.close() outfile.close() self.assertEquals(os.path.getsize(testFilename), os.path.getsize(exportFilename)) # cleanup os.remove(exportFilename) def testCreateDocumentFromString(self): '''Create a new document from a string''' documentName = 'testDocument' contentString = 'Test content string' newDoc = self._testFolder.createDocumentFromString(documentName, contentString=contentString, contentType='text/plain') self.assertEquals(documentName, newDoc.getName()) self.assertEquals(newDoc.getContentStream().read(), contentString) def testCreateDocumentPlain(self): '''Create a plain document using a file from the file system''' testFilename = 'plain.txt' testFile = open(testFilename, 'w') testFile.write('This is a sample text file line 1.\n') testFile.write('This is a sample text file line 2.\n') testFile.write('This is a sample text file line 3.\n') testFile.close() contentFile = open(testFilename, 'r') newDoc = self._testFolder.createDocument(testFilename, contentFile=contentFile) contentFile.close() self.assertEquals(testFilename, newDoc.getName()) # test to make sure the file we get back is the same length as the # file we sent result = newDoc.getContentStream() exportFilename = testFilename.replace('txt', 'export.txt') outfile = open(exportFilename, 'w') outfile.write(result.read()) result.close() outfile.close() self.assertEquals(os.path.getsize(testFilename), os.path.getsize(exportFilename)) # export os.remove(exportFilename) os.remove(testFilename) def testGetAllVersions(self): '''Get all versions of an object''' testDoc = self._testFolder.createDocument('testdoc') if (not testDoc.allowableActions['canCheckOut']): print 'The test doc cannot be checked out...skipping' return pwc = testDoc.checkout() doc = pwc.checkin() # 2.0 if (not doc.allowableActions['canCheckOut']): print 'The test doc cannot be checked out...skipping' return pwc = doc.checkout() doc = pwc.checkin() # 3.0 # what comes back from a checkin may not include all props, so reload doc.reload() self.assertEquals('3.0', doc.getProperties()['cmis:versionLabel']) rs = doc.getAllVersions() self.assertEquals(3, len(rs.getResults())) # for count in range(0, 3): # if count == 0: # self.assertEquals('true', # rs.getResults().values()[count].getProperties()['cmis:isLatestVersion']) # else: # self.assertEquals('false', # rs.getResults().values()[count].getProperties()['cmis:isLatestVersion']) def testGetObjectParents(self): '''Gets all object parents of an CmisObject''' childFolder = self._testFolder.createFolder('parentTest') parentFolder = childFolder.getObjectParents().getResults()[0] self.assertEquals(self._testFolder.getObjectId(), parentFolder.getObjectId()) def testGetObjectParentsWithinRootFolder(self): '''Gets all object parents of a root folder''' rootFolder = self._repo.getRootFolder() self.assertRaises(NotSupportedException, rootFolder.getObjectParents) def testGetObjectParentsMultiple(self): '''Gets all parents of a multi-filed object''' if not self._repo.getCapabilities()['Multifiling']: print 'This repository does not allow multifiling, skipping' return subFolder1 = self._testFolder.createFolder('sub1') doc = subFolder1.createDocument('testdoc1') self.assertEquals(len(subFolder1.getChildren()), 1) subFolder2 = self._testFolder.createFolder('sub2') self.assertEquals(len(subFolder2.getChildren()), 0) subFolder2.addObject(doc) self.assertEquals(len(subFolder2.getChildren()), 1) self.assertEquals(subFolder1.getChildren()[0].name, subFolder2.getChildren()[0].name) parentNames = ['sub1', 'sub2'] for parent in doc.getObjectParents(): parentNames.remove(parent.name) self.assertEquals(len(parentNames), 0) def testGetPaths(self): '''Get the paths of a document''' testDoc = self._testFolder.createDocument('testdoc') # ask the test doc for its paths paths = testDoc.getPaths() self.assertTrue(len(paths) >= 1) def testRenditions(self): '''Get the renditions for a document''' if not self._repo.getCapabilities().has_key('Renditions'): print 'Repo does not support unfiling, skipping' return testDoc = self._testFolder.createDocumentFromString('testdoc.txt', contentString='test', contentType='text/plain') sleep(settings.FULL_TEXT_WAIT) if (testDoc.getAllowableActions().has_key('canGetRenditions') and testDoc.getAllowableActions()['canGetRenditions'] == True): rends = testDoc.getRenditions() self.assertTrue(len(rends) >= 1) else: print 'Test doc does not have rendition, skipping' return class TypeTest(unittest.TestCase): """ Tests for the :class:`ObjectType` class (and related methods in the :class:`Repository` class. """ def testTypeDescendants(self): '''Get the descendant types of the repository.''' cmisClient = CmisClient(settings.REPOSITORY_URL, settings.USERNAME, settings.PASSWORD, **settings.EXT_ARGS) repo = cmisClient.getDefaultRepository() typeDefs = repo.getTypeDescendants() folderDef = None for typeDef in typeDefs: if typeDef.getTypeId() == 'cmis:folder': folderDef = typeDef break self.assertTrue(folderDef) self.assertTrue(folderDef.baseId) def testTypeChildren(self): '''Get the child types for this repository and make sure cmis:folder is in the list.''' #This test would be more interesting if there was a standard way to #deploy a custom model. Then we could look for custom types. cmisClient = CmisClient(settings.REPOSITORY_URL, settings.USERNAME, settings.PASSWORD, **settings.EXT_ARGS) repo = cmisClient.getDefaultRepository() typeDefs = repo.getTypeChildren() folderDef = None for typeDef in typeDefs: if typeDef.getTypeId() == 'cmis:folder': folderDef = typeDef break self.assertTrue(folderDef) self.assertTrue(folderDef.baseId) def testTypeDefinition(self): '''Get the cmis:document type and test a few props of the type.''' cmisClient = CmisClient(settings.REPOSITORY_URL, settings.USERNAME, settings.PASSWORD, **settings.EXT_ARGS) repo = cmisClient.getDefaultRepository() docTypeDef = repo.getTypeDefinition('cmis:document') self.assertEquals('cmis:document', docTypeDef.getTypeId()) self.assertTrue(docTypeDef.baseId) def testTypeProperties(self): '''Get the properties for a type.''' cmisClient = CmisClient(settings.REPOSITORY_URL, settings.USERNAME, settings.PASSWORD, **settings.EXT_ARGS) repo = cmisClient.getDefaultRepository() docTypeDef = repo.getTypeDefinition('cmis:document') self.assertEquals('cmis:document', docTypeDef.getTypeId()) props = docTypeDef.getProperties().values() self.assertTrue(len(props) > 0) for prop in props: if prop.queryable: self.assertTrue(prop.queryName) self.assertTrue(prop.propertyType) class ACLTest(CmisTestBase): """ Tests related to :class:`ACL` and :class:`ACE` """ def testSupportedPermissions(self): '''Test the value of supported permissions enum''' if not self._repo.getCapabilities()['ACL']: print messages.NO_ACL_SUPPORT return self.assertTrue(self._repo.getSupportedPermissions() in ['basic', 'repository', 'both']) def testPermissionDefinitions(self): '''Test the list of permission definitions''' if not self._repo.getCapabilities()['ACL']: print messages.NO_ACL_SUPPORT return supportedPerms = self._repo.getPermissionDefinitions() self.assertTrue(supportedPerms.has_key('cmis:write')) def testPermissionMap(self): '''Test the permission mapping''' if not self._repo.getCapabilities()['ACL']: print messages.NO_ACL_SUPPORT return permMap = self._repo.getPermissionMap() self.assertTrue(permMap.has_key('canGetProperties.Object')) self.assertTrue(len(permMap['canGetProperties.Object']) > 0) def testPropagation(self): '''Test the propagation setting''' if not self._repo.getCapabilities()['ACL']: print messages.NO_ACL_SUPPORT return self.assertTrue(self._repo.getPropagation() in ['objectonly', 'propagate', 'repositorydetermined']) def testGetObjectACL(self): '''Test getting an object's ACL''' if not self._repo.getCapabilities()['ACL']: print messages.NO_ACL_SUPPORT return acl = self._testFolder.getACL() for entry in acl.getEntries().values(): self.assertTrue(entry.principalId) self.assertTrue(entry.permissions) def testApplyACL(self): '''Test updating an object's ACL''' if not self._repo.getCapabilities()['ACL']: print messages.NO_ACL_SUPPORT return if not self._repo.getCapabilities()['ACL'] == 'manage': print 'Repository does not support manage ACL' return if not self._repo.getSupportedPermissions() in ['both', 'basic']: print 'Repository needs to support either both or basic permissions for this test' return acl = self._testFolder.getACL() acl.addEntry(ACE(settings.TEST_PRINCIPAL_ID, 'cmis:write', 'true')) acl = self._testFolder.applyACL(acl) # would be good to check that the permission we get back is what we set # but at least one server (Alf) appears to map the basic perm to a # repository-specific perm self.assertTrue(acl.getEntries().has_key(settings.TEST_PRINCIPAL_ID)) def isInCollection(collection, targetDoc): ''' Util function that searches a list of objects for a matching target object. ''' for doc in collection: # hacking around a bizarre thing in Alfresco which is that when the # PWC comes back it has an object ID of say 123ABC but when you look # in the checked out collection the object ID of the PWC is now # 123ABC;1.0. What is that ;1.0? I don't know, but object IDs are # supposed to be immutable so I'm not sure what's going on there. if doc.getObjectId().startswith(targetDoc.getObjectId()): return True return False def isInResultSet(resultSet, targetDoc): """ Util function that searches a :class:`ResultSet` for a specified target object. Note that this function will do a getNext on every page of the result set until it finds what it is looking for or reaches the end of the result set. For every item in the result set, the properties are retrieved. Long story short: this could be an expensive call. """ done = False while not done: if resultSet.hasObject(targetDoc.getObjectId()): return True if resultSet.hasNext(): resultSet.getNext() else: done = True if __name__ == "__main__": #unittest.main() tts = TestSuite() #tts.addTests(TestLoader().loadTestsFromName('testGetObjectByPath', RepositoryTest)) #unittest.TextTestRunner().run(tts) #import sys; sys.exit(0) tts.addTests(TestLoader().loadTestsFromTestCase(CmisClientTest)) tts.addTests(TestLoader().loadTestsFromTestCase(RepositoryTest)) tts.addTests(TestLoader().loadTestsFromTestCase(FolderTest)) tts.addTests(TestLoader().loadTestsFromTestCase(DocumentTest)) tts.addTests(TestLoader().loadTestsFromTestCase(TypeTest)) tts.addTests(TestLoader().loadTestsFromTestCase(ACLTest)) tts.addTests(TestLoader().loadTestsFromTestCase(ChangeEntryTest)) # tts.addTests(TestLoader().loadTestsFromName('testCreateDocumentFromString', RepositoryTest)) # tts.addTests(TestLoader().loadTestsFromName('testCreateDocumentFromString', DocumentTest)) # tts.addTests(TestLoader().loadTestsFromName('testMoveDocument', RepositoryTest)) # tts.addTests(TestLoader().loadTestsFromName('testCreateDocumentBinary', DocumentTest)) # tts.addTests(TestLoader().loadTestsFromName('testCreateDocumentPlain', DocumentTest)) # tts.addTests(TestLoader().loadTestsFromName('testAddObject', FolderTest)) # tts.addTests(TestLoader().loadTestsFromName('testRemoveObject', FolderTest)) # tts.addTests(TestLoader().loadTestsFromName('testFolderLeadingDot', FolderTest)) # tts.addTests(TestLoader().loadTestsFromName('testGetObjectParents', DocumentTest)) # tts.addTests(TestLoader().loadTestsFromName('testGetObjectParentsMultiple', DocumentTest)) # tts.addTests(TestLoader().loadTestsFromName('testRenditions', DocumentTest)) # WARNING: Potentially long-running tests # Query tests #tts.addTests(TestLoader().loadTestsFromTestCase(QueryTest)) #tts.addTest(QueryTest('testPropertyMatch')) #tts.addTest(QueryTest('testFullText')) #tts.addTest(QueryTest('testScore')) #tts.addTest(QueryTest('testWildcardPropertyMatch')) #tts.addTest(QueryTest('testSimpleSelect')) unittest.TextTestRunner().run(tts) cmislib-0.5.1/src/tests/sample-a.pdf0000755000076500001200000034500111556357714017736 0ustar jpottsadmin00000000000000%PDF-1.4 %äüöß 2 0 obj << /Length 3 0 R /Filter /FlateDecode >> stream x0ÜfL "T{ IFKK(.uu!0]_-voy_W뱮"; ?H+u%׽%%s]&)UQ¤ 8ɜQ1Se3y8G;GSuI%n) MG#jvt[pzj>72067ޢ<5,}uYL81к!)[} iȜ!endstream endobj 3 0 obj 241 endobj 5 0 obj << /Length 6 0 R /Filter /FlateDecode /Length1 1523 /Length2 112132 /Length3 0 >> stream xwste_ul۹mvR1nl۶JRI*IŶm[VWݯ7=^s5CA b`twe`ad(Zٙ:):3-pN8 qS7;Ll tIԭ\mOLb`ĜƮqc?)jncg+μqcs/33ЅBҕ[ ?"q=@[ǿmmL^.c33_ƶ@;ZY욅+@+{s+{+W/@ha 36&ng`gw?HTNW@mW_ 13]BL\JrӋ?4?ET`ƛ92i[9ek_:L-[o)|ƶ.@?+s ;̬L]&@? 4XGD_q׿+u0prXY,ae;?uCk7wZ_~V-gV?9Xυ?t(fkP!?1i @WWlwk?[L2f"i 4Sr5+aot*;X003Ϡ=S߽$ao`feoPs#?¦nZ7@O)P(mW*fZM[ѷRCDf2~` `IMBqa.Ԙu/6hv$=ij]5\Vδ7ݯh,, 8:5K^.]ۿ@/ X ,6^"4J}o(DV#H `jS(  __)fڏȿjIw8,R!3w:誶UaR`A"Ԗ 3xiM̊Csx8pAxV'gs;7 .VHr3 42NJY,HH9@֥!Z MgZ&Ͻ,|ľ+e((y%CܓuOe׃BEȗzsYhT{/hB'f=+_|!ǭn5:3A#..=VxʴSU7u:t%oi(b|u;IooMē3Ӣx?NKN%Z}`h8} {FFUg }Fh*`td>B:jǝU{,<@-;NIS\ouQCxF^\¨ߟc`:vN4+0SIo!Ss6oT ub.ox{COV>ŷ ?ED|_Pf(<2BT+mGD7AÄ*`]7&Ⱥ\mt뻧wi#9Y,%L=L4]d?O5V5,7l@H D`)- tkޤ].HRz';X87~}[V 6]Z\J[ ]!?5c|$NS17y%"ѧ~ 3 R -EZ"79, !ȅhi"1CA{]?P[, 7?hb\++f _tD5mmb;ca)"mB]mlۻ.ߡ>1Qe"D-QX8fzveWk )k# nP esN>#a>jj2Y{oğg5en=IΕp/Y(}%>Zx>5J"kȉS%U\d7 3v Y!f4a3#R大2}*R3#B4ji2L]n/͑naKOG.ԑGG 2`e֢ذ%I:g/oX湠01qTPAsMAke'{[1Θ)əRt k7M".H}4du@=G08ZH|GX6L5{9I0^="ģ1_+!5q\ u5ccIFcAA Kv)j3z)V!cb= FZp`i ĶCa0|`#N v} pm7E5!]5TᢓN.<] XG0ު3ON@iDނg˕~gwӈL{qYZaU7f$ Ueǰ۾!ƹsW8[qM~)gu@uK+-z(;3'2c=\Dӓ#\V, ]ϧ Y!kw&` un$*OVoJ@c̼k.1"Sd
x-jv" !WSh20OWb~2OJs>Z@䤙U}ӳ_>&2%J EP[gXF?iTbؽH oA84W$ 켢zP3blcG.XD⬅!Q5Cbjt*G1bo+6Mk7^O9M1&QYFm:h+G ljG%DʝoD[d\OO:byT-mA!m =u6wDl5*O>a J?: 0o;HTz vf܎/Sj3}[iY wn\ |0չƜc-dybwaR_ų3ԗG"mnSߌ<1#")$"ѣ&gkM!v]QTv`8k hؠ7kٰp[E/~KS ğҷʯ6F([iE'@\ͭŒ~FӎpCc:jC>l^ۧᾤWp+~h"7ߒրpAӢdr_Z ?.=)AMKJ -9e)!őƆJ%9hywp_^hq&Du } 8愻-K ޲k9誳c_HҶ՗O:lS{y8˾jۤ#l`!'hŠ=8,?1Xnf-gNhg{ՇӿI$w^uЮ G}֡ 8 )~bʰ BXwٺRF$Jt~:,ul5܊_ oZzydKntKt.UR7ucÖ3$mw@>阂&SBU]U>!Z<9ȏ^&C[yO;NDQ"4U9mJ S %7,m0M44Bmg)(pיWOkV6i."u2y1-L`GN2f2!iQdt&rcmIQw:=}7{':|?2ձ\.K9[$otG΋$cby1m'X\2t0l:za1܁p;y^BD)Ƒ>4u)/#s+Szn)jkMۑ9#dm)RPmɘcDCMO<= !>f EmøWKU?O/7]& .w& 'Nph=R;W2+ᶷ|V62 QdWX}UCJ mNqiMʻ[zFuOfaƝ]ufi;פ6JDgYT,qx w)HrC{;i6ѬHD"gla]0PHK'\" W(cWh`#r+ŹWA[zmp_/Xc"(zm^h\=i[㧳ʭn+|u-7NpوcGH`ڴYTxcX*_{b~53^G@{%fnPn '/ &ĬK<5ۉ%t;*W CBt8t)vuƍ+ V j( K34}?/ /xnYb-URt~xez40ϜFŹFt mǬ1hT6CV/HD.}x J} 0 ((umVSJp8ڴTj #Uh8f=E dW]Z JICXE[.ʽU mqn uagUdlt<5QM Hӈqck\!zKwyH9lz.8T~opq2 n:QsmēJn{R೵4*nYHW~]'ʇƻ?g9 $i,Xibr5  i;X'ƁT2r' hqp E hI› ; 3t^-Ӗ2vȌA:AoU%g*eoqڰK c?so2 Dr6$ES'_>aH0:T_juR΅ls9Xb6>QKEOnuԐjZ֟.ZX8 EȽLJߤRxP3!RrXesXEu d,{  N! !)G1ׯsZLn,3&/ŠP46,&I6aR|Z4My?ѵCͦϪ6`^T^ʫ`1ɯQSDMF~?''`E3PRG6@PC5-C$Pv[|?8 M3f ~% !%4hNt[,,.=@YCY[W iH Y!F{>.q;#:-G<|BJ/jԙ7o ui+A|}p96jp4wkNddɈ99G<9gw$(I1syAmGe+b4p[jmI/\? "Sx^+S4S)G:;--X**#,{6ng7~,Mnkrٴj ^\Q<1H՗64}yU/) EQǡy 62lvǙ`x5>^XwJ$ pF-8RtlQ= ˊi~yg0!_RXbtH{%ɒx-2Mh#2T~53i=UhܢɔI19n7QuÊBdzRw٪,(ynNAVyݿyiϟ9{ h!zdG^R7:SjI~vTf|f}9KWB&6BZEk4Bɸ=8ln.j~O_?~FP5T=Xlh:3{9Y`po':E'PVDeԧ7~6J=Bo}EAe!GG!"nEʗ˟Y Zk*=cKW'nH>o :e4eVRν~}MyEa9[[M UCWkob{e>CTf\ߨ>Rڹ$Ksuf{vj &: OޕdڜfWL,lDq8W'Žm#I0 .ةfľׄb`;Lɽ2Iq(ۼ`%gBů?BE;35V-^3gQʳcLifx4GXB#&{/ f`V&͉,73]~!2a,x;JK"CZL,8 C$ f+/Œ9f3^*&F<. m2 (?5d6es>t1*N^K40;%zh0.#P鋈ޝ`~ 3/ iwF_)޹3QIv0Xf"ٯF`FR]!TTu_oV%>Zy*Yέrڦ٩W$G||I]1kIsL #"$tu3#,ڮv)GN8ϧ&?G×n\J( ĈV ǁ7b9U0KădݧG A@T-/ӝNӕَwǰӄLiKd/Eឿ( 9@_ڏqYN7 s8!x&Gt0@ ʪ9J5}86Y%BmX@=E"mS?BY6Aqуair=//U(c~=T ֝Ku^K \w@Pf q.Wa2Ic;hJomۗ TtFnŀIJXA1ę%kț̾%9戟,›I7S"U-XjVܤ;hʧ-V||LsfhTH"|VvP6 ?ڵ,l mmJ?'S`J5M"4+ge=~3 ܘn);$>"qې歼*ƞҷ&`eϖ -b܇s 'j'dx4 I6dluv^[E>e@7! թД˳/dqn~1ND8gBOU\ #̲xtxbrm]lGݵM]%\5qu!/.hB֙ Al+[𴿱;9j;~NN+趫IP^$MtAFy-CPGqdZ!1\6`bmJX@)X0X+3*ToΏߧXkBNK4}DVfdU:JHނL5)HxlνI,qen=̮yf8 i(6)U׌:fJz(pJ/ k.5JGRKWߓ`لy2l"̬ ]:F+ 䰩,k -ʩL 6˸ ql2G\0- *mR %;WV m08FYI=CU 8 WYծMGS;Xw?# . N}{N)}x/el"ExP9d "Y:̌8!œ@(^,}Ggn>PȟڎyFm/syūnل;{G~F:v<7u/($7|_ſR W*瞰#Fr|WO9|Uw a㑿ڬbW}E UMh`LR~]#p7GOh+s\\He5Fκy?W~&1fƒ"fȊLnl NApGxf+NPPZ2Sh$}zw@+u>Vn_6>6D)@rƚk 0z.M(1^?Szp=\Ki5`ێIx3sOҶ!sB'9nTS]Oz B5b9dHa=٤$@L_LbǬ+g);5VBXocw@zUi5ԣR([]FgkϊbpZd[X=<Ȃ+˯V3tÇKx¬\IPaG]|^7}f;"` O)w_ }R>ҩ@,qh~7|5DY$ٴ_ :*{'?xv͍oL"HMYF wkÿj'Sf)O$jD}ɦ1h1*4G}y§E8IMZ ||kLW5>ndࣚn;sXS nzxBS<=QDuɮZL*x7&|YڼVy]ai'* j̜A.aRڢLfыn2qp6nw'!o|p3Ӗ[$_THk͒H :(od¤Bz,P SOLe7.s^Rq[m ,s3>ciY!ݮMtAKo,G_:+Twt9X%r;{mU՟煽GU$)H"@VQ, 6|4Tifp+,u)$o객+)/:[< LMU+62-[^*+!bURe B 15a|Aa \J(w,'X 4@(.'r?Q/eJPİg=?Mc . ZќHCaOk'ȣyS33J ш;r ~9~mJ[FFj;edWi3y˱#.V|j),b)[PqeL$`TTKHwpSe{E^o߅*wz7'닒RM1&6YD&>Y8=٫_g 3nReZHv:QpVs,LOƀ*7N$y9N,چI *mɆGT9biɓ^}[."Ⱦt^zB?O&4c4Sf^ wG3%ŋ [5tWs~&8(}g,8k.yA``0q 6ibsZ,(#:KySһO.AQ+1 zmEAt8mV:v"sIOLg~G+/dΌ6JݛxRX$ni3os`e(Dik%:'>*'&Zb 7g;CKLnV1a{핧4 {_<pos;ࢶxxA';6h1st8Љ1 b_n;>B7\<)\.M1rgTܬmvZ=^hZ@nc\ݿuBrϼS+ i>e>~Oσ] UȂ+!U!<:\`k]'9 * TUkB|Ч[A*)7̨7# 2)N&%3/C­ĹJI΂4i9@'R!$"@ uf(o?]{֭%pܓZ ΰl/i`LO/ NRF.z}DS+{'+KkoI8l5BZndLo(9I9\"ae/2l$#uZXX qY/KbҶaKoLDiK3=2aC-.%yN0lZw2쥑}6(l"jRƕ@mk{_/FEz5룟T1ˌFؽ[)1Aʬ?vzi\d.y?_aٹm/KAKb@J-d2ebA@.*>'8x!ꋃ7@WS݊ylD?pDYv'2C"7/劻 T#deAxjz%Vn=ܮ9ˑZwa*z 4ɓ䝿ؚP#q7˟L"`e:$K Dj 7{j0R kA p\C2ow@ eڳ"t; 'M2u$6S<&$ExmQ.~ ?DﺹRIju{SgXCx]F̍A <i]ynVBݒI*&n}]?ܱl. zW&0wBHuILCF!^y25~pts[̫f cQa^й$cɤ1 eDU V51*ȟ.4O3 JڴO0ڒeP*a2{VMYNfp`1qNo,=O=9uAmW]}l>-"=cdI#s8CHl\Ù3Ւ\Aڸ W.aCW&Og0[po.f-y*WH-ֺx!¯6_+ @TND%-BiU#~HdK)%x- ߌS[a.4sUh :\B_'H|0t\= #6uAtYw >Gہ{8Qdђ =f rĠ*ZCx!$~$޼(XJ,hE (M:OzGBSyU_]2SC+c]qk{ '*yY2,0B *W֎-ȩaD{`Ejۋl YijΛf'd֌" 7|B ۾ 4F$y %4бqH?N Űt0!9$..[Å`թSۏߊ4!*W԰/דJ;E1eK{?rgF _0f8Hœ\D"Zuk JZnu 10n6_<#WSJDC? 3~9k(B'--'1Jgәȵg[M!?C!TOm?͞xih}*uKdDƳ.nC Я3EPwA5q\M.61Gˑc}G?j4F@U+Bł )V(j :@I%_).`Ф’MÖs\߁;7l3ȑD1Eb4l*hfTZUQ-cu.~,Qt0ZOt=ZVwv2J-a/⢳%o$/#(XṴdSm%ȅbVq"ŠƬ4%ePz`q`Rmo8?[}bum]7xMn9+VG7KA5VYWZ OӔǧfh!Oes_4z&ZA$ V _['}h4f*)J99dʌ'##c͗ WEN,ű0kc4:q;VjbcL=2aLXbwn|cѲ5GY1է  A^SV6R 4|k'T1#J*`TKdU漺94`}Dx*&! UaT!Oh pT6[2ƥ }WqAgV ty|.{c3iq8< dӆSX+ԓݵL솰>T}hM›*eh0VV:͋%.U F<6?pt΍v `p\ezTMz [|uעJwsڤAH$>xM#ѳw0F~.M5F]!TOfUG5˦\^N)ɿBق Ꞁ' Bzw(,֟h:z1ӂC&zNE6]D5)B,7 ıuN'39^3D1kF|'ѥ7;8tوan'ҰQX,`ORoK} Q_\û)0L;?*TTXvX5׏TY[u ӆdVRe=r ,'r`$MZ,y:#GTp-YM&G)r%oM|P% m Ϛ 8OIJQm_^X* ɘ/F}>Z"c-Yv[(SI.o̻8 ctoo?-4fsFnW<|<x,Y$!(5a8oÙ@5\r q^@.< ϲNujj)e$ුi9Pm^p]n;B3E q{}.JК>Rym"O2(9A* ML3ꞈz>cJԺjYr_YR}]r{ӗGȆVeR~F tۜNB$"y}-x(C[n!d4AK|k&"{(+&cV4;X jG - >JVv 1n͖_4 =N{x7߁p4us_)6;fq,4Rd'F]&u*ΏKU/ڇ2ӫDOg(Sd~q(S<W h)ue#mWBBkۿ;hi-\Q6z Y tۥc'Y*Qݎb<ҍH Z kv%ObzS0p R֗Ôa9%{Y-1K,A\7m;vd. =fGT{hFIFh0@rW.0KhƱxOqY42#o$}oe.2I2^sT b\+w~;LPE3]A)AW1KU0.^bTjwSmyߧu_27EfEqj&W";kĀ U`3@'GA-v!<bS@KZ0*9~l#/qtFXdn1/U׿G/)'^:HlBl,dyurCMP 3ȣv5#P:zOGGB Xc#rn8Alj3A"ke!~|Ak@y1h_XYo0yճs#t}v i.8j .սq2B'.-Etde YUI]fE*}KBK"``u486&SH1,!U#'.4LQ"p7BeJe}0YJ(\#Pk'T^ȒjOA ECI&RΦ86S/YK0h8/,%1̏ayi3R ( GCPقK|^$K+ysI)y}1wܱ HE()Kx&'ĄO]w,}Ff(#Աw_կY/yQXAmIL{B,@r.7lƈqQ~KYta fZM#ux3d#CLM\~ذbC0ٌmݵ;BCM!N4V ^dI(a~jIAP99"!&0#^"FIzSS=D9i(KU{A%=8R*C2(A-uBye!c1M>j3DdQ'{:,}|.}LfO+uЍh!Ε qvJ{˪Ʒg gVEu?P-i,O4aҁV.o묿X"qş=^LE !-4 Fx[[d7&.5D|dS㭽XĶdMֱ$;zպ7uJ}D\!88ZA"iG".9iM3H8ݭ3!xbfO XczƷTa)`D!-wCujRW@J9F6J֚*O,{7$FyEpSəG8]pD}ra<#XWt1atܝڔxV :csRVe;͙݇5&^B6o]qcAXn:'-u ]J((F`eYڼ`_یk!%WxP㈉l .%L^?$/$/8 Rn (Du:,WPxs5_y0%)PbME2mN#\ O⃲1W)F߸|Q>.)[  ɢ!*'C*$άc נK&M;~a!Z@|/T;sxQ3E 5!4;FѽA`ǥ@oW/uBb CJ: "ݐJ7#r!l`n>׼{~6ԑk$ُ*&s TYTZ~EaZu ^$wC{QgzR-Ͽu} ywu սtu]al?~II~TZ"ƥּķYU6]kq"1{K$ RX%ˮyT0j뒪…l8 )DLoMHw1\qqfNiSonVGVLО8e[&|Ϩ,i)}F隅4RX"m6A $nݍ&cZr9օn)^bxHOi@qhnzBZ탱 Sx@$Avh opT?jH]'~*7hmyK)&קRٻ8RxtV+aꮒYafbH͕OƈVX3uy؇a'L92X! JzEH5L5RfX( +5,Ӱl!p}^VA԰mo!.fnD&3Hu/'Zr r= (B9?ϵxQH?XAt.I&ٌ0gt^7uБ,qSOzKT'ux#aZ8*wwŰ=@x³(KNw]ٙ\8%e)ݭ% f溥7IYahlHp7xZ@' 9x1QA; n/fmj֏l$Sғxx7-B'+YiL8{/q:G2kĭ-}Xq'][L0y"<@yXKA)ɟj-?\P-F>=5J>KFׂ4鴩>E/Yqo(}0ݎ9;#gg+GOCB4!6-Z+b`Ɏ'Eq!Pk㵧(XWW8ҳ,uFgTAx^ٯ$v=8,1~M-VXS[I` &.frF<Vi 0Cve(5Bdpg+foypO[J0hyJ}lj]ucs8qE-<!bznط\  8 ZZc2jR4mTA5Y'ˤ.>oFEg c7h55ɷr#  XDB֖I<`dž4:lSQmU^=q("A?)0G!с޺ a'dÉq}@x(M#kq,Տc.ܲdVyJ k5ٮ~4=(#1H8 obD_m²x~(C{bl1,0,Ҳ6BG[Ujln?5.#aN΢[x"n{tʣU%jr+FCnni=$mTC€( >hNuBx5Et$Ӕ2Gp*@VHkS33e폋7sºEn\4 ŒM=oҌs/J708U6ӄym 9P8Yqm84rݥxTbRJ,gK'Gʶ]%37S4 ŵtnwpbSY\XT0.{J<뮄e,k@mAWR&vޝ|iL"is47XR}>?z#^@du% i:?@0*U_8;.7}p]0M.c~0+sVA{ʘ+l=ؠ6GHo_r]g0'uA&},F/aJyhA*lR䷽:jz]`_ \X8E,Ⱦ's5M$_޻ڤ-Ɵܒp|@*LF$~XޝY@Ie:vXظ:KS2az}tN.((R]auκ:={4w^ 'kf>]hzoW/g\ 嘃 9YB!8s9:_8+V-ktDA/B)DcΆ}{R `vIJk<.b29J"қ\U%"#^J (#+Լ64^-t1С6o1_@Rmq2:eE/{9)zNY{҆e) %ۛFA^sC޾-\{]"r=J^/9k2?V>lNĝEѼ9ŦV# -}f>)$P |ה-#nrBix G:F>}ɿgO:fF V%OKl jo*ȟhhY˃JpG7iu Hirs5ԉ5llOXpY/K&jm}Nj ~lk?$pt7#?&P8a7ed Kà{:"{i Pwo- rQ?صK=wh6H(}cKѯŲ811Ϥ/'&4ݹq5 QW,IqF܏xJ&1,v)arT̢f$ngTz$:8l>j#qm[KD!HRdz3YbPR P+"OQ994#CĒy`0 Guɦ\G^hAdZav. ޘg 3T[kE:?{3ѢqOVc)uW\G0 s4CE?0Zrhf۞XOr`Wg ,T ze;4_@GmOD۷Ž?y GNbVW`7ڠ/?v& zٟp#9ݥԱkD Y7Q( Fѹ=O!#p@VBEo҆YӳKTbEG7 2N!khw+Q`TšcOy\zu{>H$Ah1߱Zִ¨ƷHoӅz>2 -[a: 3Vriʪq_NdћځL"& (aVa4@Yz `CBnO$c+G{ /1+)OS 1{:mokl0ng {PB2rw$vv+5nrsk;Rlq+Fv]$6=U0HHʊ.8<'f}m3.S8dd: P%HcG%dUb>cw pP[ G?)|8f/D&ç5.? ̉48fHnPбGUPbcH$G 4~CǜG?0Z_qEGֻBb'\Ǘu^E6Wl[1@# F/?uHWY'tN^W1֟A0s ]ΘxQ1T'I^*ivHI&Wi)"Ą8mn.>b 3eh2wi|R>8>Z~,]A& +UB9ϝ~DS%AFbg drU< BFmu犰̟HZp$ d`2)#$VY`; RrHk<^Iz;P>K9r'`.$f_w':LG$ QlnWrQSpח#!T26.E@si,^$O~' /-a] csD~=/en J?{u+.gbgKSO>ã V]Pa7@uwBz1ӠsިquRٲUVqknWaʪ+)ma<)"5h_잨JX0 Xep7p@@uaYY`4B#wȈo$uZ{{|8qSoƠL]ڋ1 _(z:.Ęח%j⨝ɭ?"\X~}C֚=n+R9$L&X9=H`l\7c]jrzo8?zBX͂vQ/Y鄉${z)E#zⅾ!?PrKf;[?iL,PF-f,rDT"I@2 OLgso.͊W W\kr=~’`h3CDߢ$e~ 0Qsftasl;oR]U!'ݛ"r.z=?Umʹ H*y1_2@j ͍ޢNs$FpC sRks;]YjlsXfca <;~GMj X %Vסpa?#Y̡z]0YnM:}n  VgP^Dh̼5IyJV8YOޑ-}9|qSzDC*Ζ.R4z8E!@W800(i4 FSKrg@&t[em}c EQc,B9`!U`>ǯZ5Si0][^mG{D5hsмY7(U?U.{ӡd<\"ϖos)1ktJ<_BIwb5Za/}0 ^DJƜ#`L~ Ƴ*Y;] YΌھ8 r5h ' ~ Q]٥*SB0-`g9 oڵ\K;z1kV; 1.74jz#2>z@a[W[hŁG1 ÖֿKx Lt~ibkKaJ7#Z9&aDfM^0!ˉ#1c>NN#,<_BJ}Ťh)U#Z<, H1 4B^JG29$BҴ Լ)Wdh Y,)F_)I TzxiY%w@U3t 5fLvظ50⟠ 8AJ)|+5D+Qfe_Frb22KV81E*Lt=AeA1)#J+ReR8`:{ fːF Ɯe}i^FHx6"ﲜL境hdO]x9 9 NX/نXhט/}".(n%W߸;_H;Dȶ..Ce.(_~)֛ APjg(" R%}*qaNJDĖ}Z;@:HlŞ+\i=JIvbR݄RF=Tpo[A4QhYOO"i7$b)ю$ՠކd ;R'@E¡@sD 5 qI:;`t~PpŽ#cVnaM%6/{ײ}Cq ;Ezև7awţٟp] R^E "%Cl6)aHGIꗹv1s J]6(oRINZA|zPof`:pV'i봐ϋT^s!9t `wQ 'xV$?aWGيn)n{2駹tR{&m##p:[#E/Ú[yYF%58k73c+-SP?qGY7rY| c85/jUܼDc)˨0zwdQ 2E<؝IqKkGA]6\ϷiNc/*W|gHO((klWnĔ6Ify y'j**+ W;A[ГLl@9b-Ìi;Io^t9SCmï*p:+=!9Ap;bLC 08i6mU쟭QUW }nقųEy%WzzZ6D=`. ˰Z ׍QЕ&`;lc$_+ԕrtQGyHS? E@dCgի7u(ؿR Cvc-}iG}#L\]HrCᗖ7G pXihԓG (UԒaˢ衝jhϩ^/hĮQyzl6nEտJ@o.#yTx5y{'ׇ ~{!-m=`E70Hn:8wz=j7CEM5m c,¾W_,a"*S;wq}Bd3l,R|t@%Gbxɧ98i;?⪏Y$ϙ?wpxMRH7&-P(ywN t4x"U <9{OuĈ:oS"}ILXPCG=+tsDH~@ )FkG'~ )jYyt''Fiz'=3_^Ā"sid^S4Me .]O`J^Y\2sA*Z&QA\PAL֞qt+M+hRC1n=GUΏ"uY_L`H̕j3r#@Y;znA,qv-zCvpbm!){w3w<`j_<ZvaYSH+T+T Ð $Ko+[p-ǟC2NljVO(eU 9dC:#NF" T I Eq2d (KQFXE苀@;o_壷0RSf'%a c?ۧ خJVL<ޤhNN Rr z1pZ DFS+ٷ@0$n&Xm^zoy3D+ GGh!*E泉b/fskwH E#/H5UeaF9L؛7ZWD-O+uf~/ Ԯ6#貌Y}Qx :z^: !b@fu6Z[և Ȣ&gzrixQHg~Cl51&$ĸ. knЎOMv'#noft+wA̕E>fF]_)f`tTew}3I [U:&P?2ɐ_jɢ80XnIR2pqLZY\-f+ /|&"mסi+A8{?8Q{i1zk5gэh$!-h:Px._RHC$& wa&xKzPOc"19f\ gaJ #4 Fz,}s=>2`=4{ *(*X߂xC!7 JF3.Iȃ4TM))Ød 6<`4>N A`Ds$EkOpNO #xB/6Wպ;@aH*vԏ&Mu \Di)@i3 qNX:'d {7ȥ;\J5nLͯWz {v9ruj/Mw= TXSE˱ ñ"m0Yr/ď *;O9PZ84tw|=E͸Ca :Ůی8! m)?P*nF,pMi|4jv8p/ j;;O RUتV$GqBprSMc]Hޚ27r3 kᾌ;l_L>{tO7v?m 21Ak~BAU:MX ʮG'lw<_gj~P]0% ,J2(d0k7@A~$E.&u24K]ɣMxݍ9%0@o/{ EFQ_7Vo5(S?_Kf9BJY(xS)/aG+bx-J忁 4Ԃ Wm% gXᩌDY_qs1I/M(Pϸ GAUO EC]ii6䝿JNs_{tDgn><ӆګ?ѦB龽n)3dҧK]5P+Vgsq3qu%ˠh|6fq0w$ʭ/SnsS J;S>0OCUH]0Ҵ%J3FG+c]`WR)φX:cZL ѿrNui1thB^<1,WYaJp22h4MZ+ YwZ{7oކ%"]}ַ&#P<ԭX^-}!%u7ʘξ([nށVDenVS +&I<&j4+ d/4NDF6d)ߧ*1u ^ȓeOSLc|sŬ;!#]Z F!@SsԠ.VQO/VpTEeL5e7>P8O_իG1iBKr=5TfΚEqy-C뜊'1%} qXeƳ'MmzSJ|G"7߂VP?=/0X1 Q]LSuKZ[-UozXd^/z.nEXecȣ 5:P.`"lm˛6KGIR0%Gi+ OK6v#w}V.sz8.4jK``w m dѷ>>W?-#:9>џ4)=e"̸L&@ pVioVmpD O]=knVc 4 fWh0N^Y+öP)w_QWFZGiAqE^KV%u|#x'hqp^~'dY{,Pİ8Ⱦ6U>q_)("4J%ʶGIКyr|=ldܡ`65*~i5a%s"{`rH겳0 2nW =KYb"iHX! I&sqQN\98Qt[TRzvTPn@\>%6*UK<lHmh/gqcgU)wRs75Zo<.9qZv^~ G3#Kp20.={wYG]TYt$n`<׵,@emj@(h_ZVKSD"KM!C;X'$ lN B*(k煄.+c(E x8]'1Sz~n$}dk\80YEyK㰖0P}~l#E[lBH bk-}~n(i+qv by[чdQ$#R}&Rsⱃ_.!XZX]r[70^Z& msr},'$ v_s5+"OHbr_xaHLBO+PAqYQ4-/>q^Z/,鉸bv#=L{f^y>/?פ5b$(uU BeF;WŸ )|DǨ&1䲐71( 'DdNmtM9Ĥ(=ʦ#;Ny41ŻpZfll,"6oh`^URD*e+a0ٶU1+'GV|+f@;) 1pL7t[257:Jۉї?E)3g}#Lj>8J[1 mk[)e݀Z#~!夐7g)ڐ%8ڳ˳iu8Q!aW  OqNᏐ bZGIt5uYU[3O yD7 :2nEdTGy/b<#Lij8X>ߘ)Ux5x~TE=|S%l 0H}o<7pǁSX o{*F3ЫX>X"n}&V6 hhOO\A7V`=Cv(mS$ 2+) o.2.d:PpL*7^^i>>+ftU]WTƀ\k*<45s~O F.J^utIUxj)AKXJ$Z9 t+Ig|sXe܎n3D vKI - 8,11T ^k+eMP)R!t.z|m~oU$2L @ȻAWTJyOa %X?K "V)t<陥D">ud0oTF )R4ю\Bn5Arϝ{& \$A׋M ˥nK}ǁ{ݰATlej)Bz$M#慳>1>d@ q ɢ: FǓ6!)Ȓ&Swe!ѹ\@}nhr䘑HʾPʾл!V`J8++ $Z/NX.g,.i*ōv\]s HVo3yua.nndERuMK!fҙ:f8*|XJzfuI.A],"6bH,II.| %`T6 E(#9BۯV,"|Y_KT~Z0:c:i߷F\7̑y>j}^"&?QL`2;!("%,;u? PxqYi9x\":-:g*{P۫.XT!гY6gl,4~B- j%n9bD(1CA JwE^0^diTNS gSXux:hBWE+#Y^v->i>NZt:x$̭2 k ]\  U'dP&^s5*_=9qFؑn _Lg7/=˱gN ~D"^%k3 *Qu=02jZ,oJHd492cF[1:،b!X'`UpJ_ǂM >ӯڳz=~kj h\dxIgJ*UmNAYuoZ(o:^8[p*% Ԅ* wAP#&2ٖH!>9DOTVW3Z?zp?j2(Ezr)1˲67~Lȫ3 &?&+aqQgUen& { :="YFJÎs\r,JKy:8E#IXNh|pN-)[򨽳PmeD>y|LFƒʔ{*x]LUM5ގY%E&4hU4U`eayG%l/OV"37mrec" '_hG SCKX6m\ޑUQeEqe*비;?cL kpA82tA~f'I|(pG߾HUIVr"9o6QH*y` *s=Vyj(z6|A\M暟({s|5bejG$ޗ6fr5!FFW`zWW/triQ^Gk-3κFH۫Dw8Yʍ'd,F΋\rZ\<~RKFC˛$jYoS7|+>|ȞivK,i?[!Qu>Y"[^*d&S_n_Tʛ>|hf1(B&m0G}*HmO ~[;-+}:'tgblv߇8CL]˨Mt46~(m_tvduSr!96ʾzƥ#3RR*kC.AXSCb't\(-yc3{80LDP] tl"` AqOBv %oEACP7)@Xm8nDS~@q&_fXcd:pHpmXݽLh*h2e]@)K`~%Qwަ@NSS [[.ڊ6[@Rݬt zqFb8}*: KSΓ.0}ccSVJHW~fꛘEu)S3oK !,ٱϊ%htW >hV8Fõo0uҞ!3RwE+M 8oͫ-+D~5|ƉgFZ~uɎy]}1Nzz6eT)@Xw=_y^yu;_5 {MTZ/\!(rXhL؋p*_vWzpE2#dCöuAʻe'q ዩw۪ hR :jKk*g$FMnozlN6C'4d8i VqaWnξ"6",ȕyo& nꟹ㩕ѷ1t)C޼ |c{i1KrA`f! 'm*;G)&3ajlYCclңیX[x n{5k|q3LEOXA[wa@b5wG= dd5wC̑SY8ܗ'lxɪ|.my,&no5 Wx9*6FׇS:Xjjf]&}")a"Ii-([ƚbZT,1@rYpV@gȊdtkWĚ3?S q4:Ăv,aCWiK#/oխVVa|'6nF¡N5ӱIg)⸚rQaHԨE?&|K8peUOOqkBJqjޕ ֜D^O'g3٦M7G<Շr'eX?8:U{tc FöUlnU șkY=:;cl ߙ~b^ (~V1?-zgby#'S}XYڪ %ݡ-9=Sfx'oX/W\K 4){A v[w1Fv}#Y%Щ\h5 { i%|oq:fa8@L;`c$f^29 fRؑkMRKRLemgOG, ~9${&o,^ 1Gǥ9U3Z `rŴ.yRFU[ y@Rw$sS&mCIY%boݘ%`2t,g탮iVAk}lA#:{F}|KZ\MN`J,z X8}jz8ilۛ gďԔt*6ECAzPPr/P_ɼ!M1gwYa?i Y!p2Eܾ^go#4l yȋ cmu"BqíѰ v`@taK0`Q5$%E2УjH>]Oh؇H7빙8¦#{IJ=񪿢pjUd#2\S-!*/հe,50K @MMG7gdyoQ9h?-Kaܯ@3l ӷK=#A| 4% S,֬&Gt&֢=yPo,5ӫ=ם1?9Č[jg h z(":f>بa3 ]ݫdwa{ ?g* Jg m?X -&]T\0 _t?U|Dk3Dֱ ~#>X)יe i!$}uB/)TZyCn,JN&ˎ*h4=ALxdknwcQQ Y. /`s"gs-|gF%XJe#Gr&. p4DFF%?Z1\5HF琋*ƺpߪ^S` xHƄ)Ke`WiSU)z#9Pʼod#0 kԍnG,5&a/bzɧ37]囫!~Tω}sZ;ĵϗZ w3ŗЀn훭a0 />x{؁# krC\:j~ib=IXJ."_ |'"|&H%F6D+a} &p ;?к];+PkǼh<\ c6;$G׆^K$ 1H4ԑ?Hl4N `56I#B[O(?(1JתC~@[fy녽Tfm+ۙ/ o bj3FC? Uri{2Z/fwB0W;Uj7+jփ,˼}"gȯ!~IrɥH}.%^rWY|N7ɐlu Ź;hzNs;ꪄ)-A~M $Վu5z7 )|yg%˕^Bh[?N fpg<,Xկ]ZG|H*,ȁў\|.4V~=Wpk b ~Ƶ8Qp^K;5|a,̲ΊbbO?3q&K a t2P0ֈ&_k!Btutshgm"}g-v̍;+2+-&mv_xdhFzj> v^hw視Lz 񅇡U5b"b\?P}kʥM$ld8Z]J؜J^o,0k;ωj5L۹L]oҩiA:l.;jlĶRЎƤݿ,!RΞ5Z>M,Qh|Qqgg{=2 .;+@ݴUA%+FgEM$`1Kzl`;2`^\kX$*˜u#D("iU71ρS\.9xb傖!Y4gGn(rA#/|[ S(|JVY{4YqRC5W=ˋ$69>5<q$SsLN+#^G?S:n 7\NsJh ?]9J=FasdPW&EReN'eS9,- n|b,oA*M=P'o $aӕO? Z[ʐ/twMM(&!9R)q<_vCwuCw 01#G Z9bƱkaZ}@C|o Ze JG\WgXbGv$!uʸ)}.ʳ ³<Ӧ} yrh)ǹLBșw8mGxa&ڐӄܚA!έ6aĸ0FSߑR %K6hN rW-q ,2Yڴ-ǚv~&[q@H޾?~$͖Y1 5m_zPswPC2#@;G41{L]XX)[{)y? &ߤkUӴjI6kQTH]=> WxdI zra,M]W*s$K`\5S<I4[U9FB~<@׌])"#}iܸ,ȑb"dwȿ;XQ[fG̹mX4F$k]qw|ǘH㽗Lz3Ws[^&`ARl"J`gOtƳChf^)蚖G M| >@mk[<.rohϳ0N4|Y[MѡP._ VM=63آPs~s4k0'] *Gl0]|CE5yVV9QГ)xAU&9'8>5K%8E 7 ƴ!gwMȣl|xn.ʒW WX~C\wDzݏ}~|LfX7ϯݖ:W&kDC*=BHm\Jj¯0% r@-xRO^ltX\GdɄf_.};~i~F&8> VC56}z8珸6슭pp!.d'&T|7 4HO[6nVC/dm%-mSHV,G!iumC5xv<38YKΏV/@Z&i H [VOKf`X>^wWԙI@Bd L!XZvi߯X"KjxSy}Kp#S'M8A[ESw,/e- 2ޫo ~c%9zo7a"h wGlrΐ1me'#T'K^wG0Cř?o_兂Sh7`%)\TT9oSoF*TvJ<^Oqf1m^ "gݻN ')oBՕ&?3rCz)4ee7GT)OҦIȥ^!$|֥nRI8[g;e^(I['P%,_Z8gk<KKDi <蟨s( #?6xZ"BK:Id1+^MTz7Q|t(!GNnjuI͚Gl>*ccW,.({yy(|AE)CE rԣDΠz4۷}`Jp%u«g +Kpw6IwYlgO`֎M.gcjgAdߖ\@){)=}63&t}3$N;btչ1ZN6GHzz:_>щx#ơ;%ϖJw᥹z];YZOCC/Km\gn/΢}9i, '^nl5;x,rQ[)MZP؝RXJ!޵]LX=v E:j0My0+0dѤ(m "kjHζ˗H*bh<riӮ53^vpzIJ.3fA,k!V?Yp0lPOqRA*oV6Uuukr >S!"/f.2hPSH\0p\?"$ DzG5t~@[[JփrƔwlG^$GH:4~7i|c̈ʒsJ) /2op'Ů8mx *CoeUâN(a^A_y+31]o4nΓZ D+L~_iCuSD&k6O^7i <rPeA4^ 0:͹`oWgq,jJv[jS#bH$2 DEʖ(EE  H<ߚ?%{'< ܛ{?ׂat PTJZm>?=U/\"ZEU!. U B"ITocp#FU(J L/bmKisJ=׼ Du02cMR4h QO(mzZHf0raE\Wo"D7$7DUY| G;Y9c"72/ufcj% XڡpoQJ]OVx@4He28) %`ڳUG{3G-ns͓.;<:Ӧ*z*Na:N@6LW8ބP\)V"Ds+bG\)rFdSF<)'>+GLLEPYykma%Pr'jZBm.Z/6y׀Z|JބQqFZ׭ c-bZ 0Ic[s1v&$WW@( Hp ľQG}KTW-|JfEN͕$,P!@Z5uS%e[I!&vhbfgؽ`V:Hqah\-"kd/iŕ]7u(VZ ;q*ˀ97_؏s`̵CuDah۟g<,GǡqoI5Y6~9kSKwVAVT(m);q!NfEv]U=h>A*[C{t聆R<vz۸FF .<[֋up-e]PkئLǏJ uɝbH'NOZQ)L˃Ω JRBbF4VkZR`Иr6DZx gv`hޯ;ߡq{kҋ;զf3q/EsH %X>\ ӱmLZ&>pʆa~.P/d_E'M lI:/ xV;=rOo11Қ4Kd+J&^G9<`u|܎{6KӛF) nm ukpu݇#z#-C/yWfRid8yM#$s&x`lظ7+Emв1+[xcČ2.1F"lqAUeBr힟]Nj@G, 0} ZJB!G:B#FSS\s{Ue=nEDk1GO,b!!hYmxj>JGL?@PwIɀe{ў}`3=i!aޓ;+r-|oY֫wVID`B PvӠ5eRmrl%dOBxU9vhe6lg'n}fu!nP0Nό]P.(?࿛ q-ʎJh>{ 7z|7 $;A=Wl3 8?q3wvJ{k>z>f1w۞>q;*׮+ņ c";>+ʩ*B dMo%gb6Y_jwŭe! 01[uG{6yBzJ B{[ùɏF` ҷj[ QcN)!¦rE\"uɏ=)dBMj4<ݠ-2P6'r+;F *A#u;f@X >$KWOr8)H W%_Y[ԄW&$Z :b^s&LʭCG/{e{&Oء/+2B_'K3VQ״~ ߶#xuZaDv:mwyدm3R~}W-vФb{k.|XE7h,u utX?N(Yu@&!Gl_#_Ę`y. t<W!%F"I+}P1 HQndvPk= n(?oRj(mNj n=Iݼ8GB0q9< ʂn5LG4a9AV{/ z#ӠQՅu'-D6Iđˠ9f aD#O L!}iF E5X< _(ݖ~"]O%AQA^OwzpRNr `w1ȉd-N⪆Ir P}D;Cr8 ʜ;3fCo!j(dN`:XO%q> N_QZµrUN uK$J~ĞwGOqCYQ61Xb%HJJ}쮉fUGq:iVru;p7(< +08 Q6f,S}ML -{V",JY%Ë'qr+ګiẙEx H\nYPc27s1pZs}{U4CIlXiÞc" o@D$DzIrO^#:3#8ݘOEA .~V޼)htpVvLȂ$p* ff0U4DJ#{}t*K>/"r:"31V|</^wq_N$j{g!M0ڭ*r{BFavaP}i16&ɛ-@w񔙳]'o:'OtJ[1[%y;-N]3hWbvUϙt"#kvV/+C +]ΩJ>1", cA5֟;BH}M_ٔ 厡FM}`_GF= ?Wb$ o; M`-Kd;t2/,&U{C9@%qxdo q;UBWsED nEv ˨DQkxϵúrĦWDn}{XP Yf̅_̫V] g~#p5&GNAct_mh~U"7~5 sl m֎jEO,cFuJy!h[j)~&CXEAh2EV`|~ds%nY,{xq= yKMwU;H# R\$HkǸXQ2.FN>u2 | yY"G_jCB3ҫCJ=bl*/1LG*4@V|:VKn}8+q2VŸb<Rl.p֜.Z4x[S{~WKVkz]MNrJ)6W3J.JyNt${:glJun3j)J1I6x0AZ\ڵ{hnNˊ?BS6.!H!U2O'^ ɾrK8 ~clz4] ,{e+exOgTA33N`黵ug) [BOQ|kJ0.)e7ciXQa L%ʡ,7|m[@}oELb9@?O_7O봐<ߺ0*rxiZX SG_7,~hpGubc?uAhB<.+=;Z룩n$ 4n{wF6P=s9/(Fm ͛[_ E}e q NHmkdgw-:PE- >yvbRd7I)^|Uxk9Eɳ ~~$]M/iwXYN(O9Ԛ 55*.ȷ[,}㋒%{'RpIEF6a4ȃ渞PL1 @s$@;%HϚʨu?ӳ W1ve44% $TL 2f.=;3'P0ky\3 x,ѴA7 6s6׍ 83F_+YEjzB냲N=/٦3*cş u%<5FɤDցݛȴ5aCuFwe/T'Ym]J_.9Iס8UR{A6wqb 4Q{EtBBTJ80ʴ\֓WeR-aBX_d:Ҳְ3\ /4^Wǻ¿|_ߒ'g&w%0S׫v"/+H޽&geiFZb`Xj#A|6" KLfrfvx%5!Gtg<[|LlZf0\R+@NKs0+ ރ^?L),fP=>WRYuy?cc0eJyJ߿'|z +%*k@=E ;t*7^ZqUO|GOv:<vC#Z2kV ͩA_N,#| :e +j'v%E4D㽤/2HM(XW,@ 2D\40C"kW[$-*Y A=Cuirݬy=9QVқ9-gqK+ tXXڋ<.eVl=n;';MZNx,!k5FD F҇$cLB Nݡˀ8x CuMc/lM[)u7dHh֘_r+V=3`4OhƖ(ˡ[2gEf ^nę7L ̉rޭJ'lވ+7"+6{Ӿȼ}Z{SxA̓K` A`3|uug؉':#.p&x^<PM4SM !V 1C}~GM!щYT&ʱ5z|u`GZ{Cxė^k㚞-ՙ r˸BX'N6SPT550~z`S5~#A H 0;m##qz~0]ӿQ75ng]A+s0YAKz/V~ |D]23t>6*=Xi,! IqmU yL2=t 7ζ_18MgUC&eesNrekhQH1 vKhKJ.ǐ=,p'dᥢp&6 "U]փ7ou}\)[;FeT6R^uH1utv˺Whqɦe(-%uf¹YW]LkeM\l`22"\TK?fݚFk8`ؓp^\B3CբQBS68BS%?;͙v@dzWn`3jl>F6(䂥wmHEe&f*H: "DUAO2%TxJ]Z{9GW)$K\<[ɸB߯ٻ2"nJӋwߪ!b-'?;zGzfǦ{ 뀇$rɬRFw-p,:[D(NJm'!XS_rES+ =5nlB`O٫`l}|۱5gs uC7G,nrg$ p?9 C=`cJeQs W y[󾁫͟jiZ<}'?%o ƐZ5q㔻2Ämu{x!G_eOp{IDn t՞Gsn?\!aRАE8)c#nSqcyvbs#d/&9j : 7z|P~ruL#$5z]mb)/(&@fȮaC߁25՞5rNViYR*rgBLv$u(B;У #_)E60$5`҇ӣCFԁ׭ƾ|.F*y?SU0g'yX[ka_yhL~bE䔭 ESHUi`l j`LlBAeƵx<[b(M?~yeTSSnzϼ~9`[-OV&1.w*`Ʊaw T:|ri 6U 4QD㮅Muz Τ.KӒ' +S8!UX&r:;0LڬM;'{y@|Pτ風͚y,xFn‹/tcLKa'Aa0uŗoZGu1AF;ˉ,CQSS܊|CiUSލfNA*[g</%)Tķ6 2TASPeȇD_ fVGFewb]  /_9]<(E #AGlt"ՌoT #~Ke4hm 'G6t)g@}}5=I.zT'q˗W-XBR},BZH"2 jfIqK?W,$ S p.imMbmrSO8N! 9 LP5?w`fnE[2eBCe6T{-<‘1[KT稲zKZCAY%?ПуFQw﹘+}H_m2ԭ=A^Tʑ4T.0oi&͗\R֋/շZ@d_ż7yvQ JiB[xt rsmi.,zmfkR;q5~ߝ?UبHEL)~ ω;ZpHŒݳ-HѻIW]IsͳV]\>W RϦ[*;9 x?TV'tl++9qI#.2'f[Vfw̅ůq-Ps+?2Nxh0Vh(֞[çӤI6$YѫBQSq>cr{ډLWO $qbLߒPJ_CߎeA\&eBG^:mh?ڡ @kvr+شN t2: K5+f}:x׵K_.z4E iK G#a۸c'33v+OkX)YY4z}*˙gNzu_MVvb<7A \TaY}`\5Gҍc6zCuȩ wS5â:לi+q5X(.@6glu8 ;գ8&x {K1Weڳ"h `,E9bII'nC{9E/ h գD$RhL:h8MQgYm%5a,[V"t'-.N{.Tl/b}66;L]ܥܤ>ѻ2ks=ozъM~ȀQ(| 4ƞ]&v~X2T2afIG.|BbBiOޠ2dU߈oQA&b/Lv CxX>vV"6P?5ʥyqm7is"h-KmBGtdY):V,VvG7RGz%i:=+lh}yBShӯ^0s]@*v\݌ZAi$3Ln G^mvs5i& zK/ Vrt"DEjpj?Hr(/0Y=iY펪U;hT4͢{kl!DOKNByTv};X̎ WD~J<:N׭$Ugg6rky^Ի޴Fm~~ $pqq=`R#!AmE}c8n/縰m ̷FPTQcJs%r?uV%[N,Pa5R Qڤ]U} r wJ=Kڂ'W0ɜģ#?$Az,$+sr˞ʒ3 |ܺ;[+Lxz+ZumWQ{eЄ*\xޏ1?_W) ";gሾIGG,"3/\A"K8/lMO˨^pf [^.@9n*yNlrnhtC"Ԩn.F\P:=I9]/g~~4Qͻ%*U]O~\?ڤ6( :7p&UZ)gF4d9=2!Yy EMg'2{M Z]Sb.ծ eT)Y%gdq0o {^ WWcRNUX8dÒ!%H/! @ͮ; (mH %XovZ%( .bU#/NݞdVTy*@-,&W7NtArA٥ƞ(Z귀>cF~^P|Z 3[Ug 3e4ˤ:*5=Nբ]j5º EٻU.OrlXsR5)|)Jzl]DO]_BS _yÿwr\82+{F$517SejtNwW=z+ɔN9řTJMy\-kXf룑mc-2|t.{<f@mhPo;]4~X1r΍$5 0DXؚ=, S\F}t3QeJ/)sO~Jǭ% A\GA_.UUb%Ɣyl"As0[k@A6-aUrw4ӡJE1aW;,'v!#՟}-뀡93ovD;5\ H(+Ұ]A٭]v$j`2 CܞԈ8!EGp ~hZ UC>${YS-S^E^"16zqHXt.dL> L/RY c3>\Gk:N%24kGy}@B huЇQZlSs]\[<1[qE5,*("ꃱn(Ƭ:W#[-s%]8DFmjb(#pLz$-}B#wc 2GۃQJA'i96@H_@BfK)u}gG좸mk}%.'uM8yp`G՘4f5.ĶgTyΒM֭CL9 ٵYY3_P ;B&3IRD.'l g$;XK'r00Hvp AH'*X_ 0Ɍvg1)1hJߛ?ߋYj=h".*.c?HQ*D=]T۽}j %08{& 4]n ' -ֲtZiDU;(u;TΟ쉩-h PT L#PCP=;S^(T0ٛ|\xl+ *X"J8^~ ilvvr{@[?cOA$1QXTӻ ȗPwUSwf.ȱ֒|%b*X̏ތlT @=M<(2 SEl)cW~LHyM9nWӵJcMnqE.$y]9Nh6 8,B?lM(?y@V4XV ]yLBrf䏽b*Y‹,Ve =+AlE=*Fc앎7"Vk krzx\m͙k8 KJS 4>1W1P 2oCMp<00e|'ԱEGDmH&v<![8+cvPxxev^fh),\$wgf8Jc@UۋDjIzs@gwVP2%mX[|>Nơ2kkS$7x<9 Htz-2֥QvCه01EJmnTE7 .4vJzMjN7ofsA7hV>504_m!X y\6UT(]`̻;ZotuNÂwC 8Q aЇx-LF~;+6moN'腣:)|.w [n؍R3ڻ:qd:B1)D柦Y c 2'N2~ s7H #JK@חCnXx\HU="lW?PPhۛ)MIxDIO NbG3ŻE2v9$C a lߏPT]Nϣ:m=蓋Uُ=lxV: fN}g,bcf1 6irf_%UX7I)YHRxÐx33- ]?wqht;0 uDey{PYCemN xd^D[#v (݌K[Rq'6k;M)G|+pwkdӷKO u8s=i uGkthקaj}}}@4i5-WM=@- .C#&H)#XɮVs: UDBǔfi'sօcy>r6ŋ_oP01ȈOJI"zٺ^^bp|naѻKoebا-ڟ3FnAMD]FS* ޣEF_,n5o W =*oۖ.u{GD,::ɗ Q(V}։z(Brڳ,$qU5ɞzFx2|񵕮k'њa7tR/E<jBsWաk'"$dDF2 aO=蕔m6XAk4p.p=?rzA:#=V[hوf&i1]?N6$.OQ NNFjٌ_x;u1 //63)#CC/ Q- :ӇSnw6QyR/4: 6⢇j[I@+YAdA݄hWc&/lBWkB}]4:ɻͬ)^ŮsczPw\x[6~Ц(atus|B %4f;tO%܀T=;V@@s6q`d/~)2eESK mAg>2hO]~5>DwZܱk tzwp]t-E8JUdDÀy`>N4U^_|>+q"̗,|BrG݇ q*aMkn}R9ɂ@(0Qc<.i' 씿11H@5EYA.-,I@^ǏQ !d ]=bWin2ޱeMف}JMqqMdau+ GµDhœ9dY $H>t LdtIUZRɪd5q+z=Q Pi%Ka(K#q} KTxu#i98揉=a%L} '"zz< 6Ѩgoe:%=RGC4BY ,+$Nן%v)j8W„qkߟ,> _z]o }Oz88}EZ0beAӂ_'> *~7{v(ž yypHnG63RjL1OOEbO?X8#q[[u &PXR*Z_&ʝ4Z˨Q/]~8k1e lrT$a pm>=eF .iW 7 fƟ;}"?>꣚kzdeUh|;8AҟjcnHOv87[LD){\b*Rm$ w` pVRkqMei崯&}4IQxAM@+=Zq֖ed\:fqn8J`3 'aIq#k~iA0'LX `W=[(kk7pI3mUZAE8X^6I+Jp)mJɥﱪw,ڊ २Vģ~50g{(Н(l*08k`ܨ|>5f] о` f-B@c \#$To7#3S>)ќ~KctټJF63%tU4 =ITSŸQCAv;VG~9h-QwٞNIt)ѝ˩U[Ay i(n\Ăv </<2f0*JhLc}^QCpP/{"WVF=N||davI%fcZ &3`N_bIkxN5E2 ryx.:_49 #i6(lMDQ[$V⚴6ъ֕;Rf?g!ӳb[~E ~ (w6 v†yE1FFw6w!8FӏyVKHoޏz2l|Q8-U&~izKej1+żV ؒCpi\ ,iĊOx&a[[w_8TF!X0V%Z¾_wneDg>@_:^Ҩє2C6#5#^pka@z7:ZTT`.ӊa0s!J 8~M )RY;" IW ]8M'L4=0-C@A_|l.aVFD@,Iz|Y"+N︺_~o-xɿ;խFFybQ#LQ0a*$-l* Hv(wAgpW l qU)|NY薒OXtE٩]CT6{海 A| /e_^D@Z#h^jǵF͖":Jܙ}.A"N">r\g?b\u *Do-.L5(|Oƃr1Z5[YD՜+Sr}.(Z҄S OxB-3nV$Q"/5Z]j$TC3;I&Ed Iנ*F` _{y@ԝN#ϒk#7R*f]頩loO:!mr[ x+Av5ˏ@ 5(n- LQo[*f)cK}7Cz0*AVl|"K3rշkaf31#isi&*LEύURO+6ǡV(6 ڐ \ 2z v) J.2ޘQ.b.G!t=HG:XgI\e!]MS-N qjIsEcX)-w;_UO"/|3\4 !;Yo0Yܼb7κKyח;s5p "q'% Ai3PGs"sxaԇ{UtCjLr5P20j;X=g MFi\ JQP! X (M/^ո7Β6.l-H*$]6+n- >!o_eAv̆mۢ/jcH rKEfdhLUQA*F8D J&X<؝tUWb(V+nXG)Zٍ79!f$9If]/w}YOԕzTkÜYyY,ߋύT`'_]T~`˳Bͺ_>1>V7)D)g%nNUѧ-_ma8z&A!&lh + C_:趏S3J%9YQcR,{tMY7S/A A_%ώlsx=6VCZ6v1^Gin9AeG'ҿRy=tP7 h)Z͚X=JHȔ.P Kɘ1(gCg6u,ckSLC&:#(}N,(r>cQAt~|%VB]d0a>X?IGu3ym |f&H >W);U:A0PUV!P4qkhfIt2'-viUG}*`Id[KlFO](U'EE`S 'G3Эya{KI]p 3]y8Y]sWk&ܵ۠Er̘ Kl,;(JY:%%=@ꫤFyn"0&Æz$.P-.0Iz/t=VuWUf*S^~Α$ýe@m1zyE[`EDA=r.mà(Q>e2vO{26[&Y(Tpؽ毰C[Dtt Es KDkqʀW/%W=YTLx]K4fw;1/5S |&G&福E䫢3co@P%F!F Q4NTèwl{⽏ծ̱nE w_hC;Kd1WڏCI+ϰ0 LM{#)Phc^? ׀̮ٷ#.eLYu,t`Pu4ycVpOUS+{+{ WD-g ~E# sܜ>tF2Ch>Sp=wl8IpQVH"0 T92MK=ˌ4Ib]gvJt e}Ϩp?W5sOAe8ҘH(3~XVnUD4Y*byvܲݞA2v]L2\|wy~~j6IiPn!`j|2 q Tʨ,*.XhW]5Q?{)P?Zu@'Ԟl{)`Id`NjZaE03>xcm K/k@x=9j"UuxD);-ɨo3L'%Xn~ztH8]nPU/LO"(^J/nM& ()M8on% `k^bJH{c[LZeTC*y=8j%o7jg#b73ogdǨ0 8fYBB8,mܻM2Q&zR b@q>sj!e VsI3>yw< 3X -Xs 6\fPwgբ<`U l{Jj,9lfQ\Y6p0Prx软"_(c!0IZ8>aH˲؏~jzaWe6}S}Y(J~ϓ8A;+p\"Nvᬂy6C,[e6!ʔy¾xIW\$~?e+ϊC,a %9D+R? L͸eP^f.粼>T糕ہB!"RӀAb~ݖEk[|݁Sf]K>A 9XEEhPB8go{YG #I`êDqMP1fXObgZ. 9\"+i.Nilߗh;toSXEg¬1vC-nIVeӛ/t0^,ы]as4b$DVFV,(Bj5'm750B]EaI1j% M6ebA:2HjݫȤqx cTM*p_Nn}޾ zossuxSČpjXJVɋsD/pO#kx89W/ԙb:h(ˏ $u',Ó˘J7)k @L$n8; p !pCQIAFWma󬎚یJڀ+0$'h0z|}TϠ6~'IZ^xj6u>`q{ Z@DE/p:q;wLp 3 C0s(z{cdX|h %[[h>W,|?B!=~??e?2b#*-q9Mی\}V"ZN'HfwuBW[ct}W>-ƿư MɆn1B &c=)ԩ5+Akzo>CB%X9l5:&HI,R1L88D`Hi\N%Pr3fB62D(wGiO/H^a]]zuaK]+>%B_eUi\6DW|G&An~jYU=ikzjzzsCq@ "$굖6S>R}Mtspq TF8,0C!*jv⯄ha]a^1{B*d¯gh#}2`בD2:bibCk8 moKiGgnjn>@ߦm%fEm̈́2aGC`9\$y\yy^[ܶpZW.YISCs x ړ0`#yڧЪvL9t]'k86~QPazsIͺGEqg[ժUD2x-ȼ^㻜q, Z. ;Ker>m&"%cx#jx>^*4~ZԀk+Mf+FUxC 4X3 tHBGx1.]4*b֫h-90N[͡EQq 1'iFR[i: m/ݔ[mQHtC.qϠW3!!`a yE6]?ɶ ZtgT`3V|%Qބe@DbdaGŻTޔF5'% &Yk/)7*M\eb8qgF+MKP i D*{srsoHFSUJr*Sa㔙BPyYP&PT.wV\`HT}gGE6RTS1ޡk4.ѻ‘-80U Nw 6 Gjw Fm޷je"|Cxa{"Kg rB<+sjb+0YJ;X}UrT#-';В'7B|ε9!᣻*`J#+U zNH\RdhGn_#E2R zBV2Et)<:ןEQ&$Qh[}('+^ZEG ŧ޲diPKL7EmL%`UXbkM_ LJH>y[ bbͺB#6ɷ5F}t|XW(IY../`ڕ9kFjKW׌^Z0HU0R`g<3Mʳ.x(,|P;wmd] n:v}3/ 󺻻g /*IQ0g6cBTQ |ŒGT$0 ՇVnVgQ5hu$y&pt 7Sjh|0#v}7]Fqg'tݘI[6#k;|mƗ$\a2ԅ qp5 bbvξ3 $"':* 3 Vy >J5c U~^pSre{WHKx"{p'˧ƍ @D.,YE廩?ʘ1̺l yj2B'/"=q /%ēD\i;s^Jo*v#fVL8Fա 9芛5PFwHbtFZ3쿉9nێfQx,C:I~^HW3 Yp Jt$pdжGφU:Iwѵ: l=1etAdm ǷGwBTъbU9Tcƃ:PC\FC%Wb{x0{r GLX|(*^ Q'9tAI {`Z{'<(`{ ,F*H^GB4>x@drnhStP4{LH\S9D÷Ȋܴi9Oͩn,5땱4҇=u#FC-Ū bTD %69b'|rU!PΜrƩ5@u@uo {\ c(”d@K{GAYvޱtNՊn"SFPO탪GsH0i4?D2s"4㦲5sްN^JUf(kS+<ݑ W;ϡI;م|;yo^AmL3aKg\0d89&l@Ag\)cMˬdCC(̎Ѫ KM&I5X1Bd|Cs)BE-ټ%]ߧt/#1`J(O ʏ1zbx|ps~ d _>=r d kłT~B9xXH|WCNqOw`Do.^“z06Bu:]ob#+PI~FXZւ5KhСׯ{,s1O {@[#32x@+` sgvw@}3GY)n:SvdLe`,}Ċ))O|X"ԉISia %.k S1?Jq拘Ӷ×fi`A9ur0ټFz* ɉ Ȯs<Ð.)ZXg|ͯo X-x\0S?Y|2tmj!a 4 4v"V,;s)gmC~W ȦqkBU,`y''qU3gH/to- R(JuiQCNdߵ%H4#?܆,W;9Ѥ+' iFUO"S(ItLΖI\c^K9E<6+^3Z%d3\ ]gN}OUei{g"o{bprI '\6¯(+񈃬t uvT1,9;M*yEtpO'5̆)gycnA o0\O%أ~~lߕ#<,edCtɪֽ&y=9tK2ɽHF4(\y)^u#u撣\\>qwLߓy+]m>٠ikDv9>a;0}dfX!d>&2U3./}pm1L ͳoe Ҝv+fSmf:B_^FcR-$#[O_;UG'*do^#w+@!Ka(r!lT$涨;#/o~y@7|q3Sr;H--9:-efz0˘MuʨmcZj'M5;gO\z1dMZd=p,6t-٦Y͠$Gm01>>)>kDXqWk뛥]]'u!?)ĀFhkz7'WC4>3p:QzL%P  Wc#s]w诋:$xH5aWI'Ħ+C.m ]YMc+U}g&᪢}7V0o昼sǚn(}s7J,Dr6@_J"E˞]fMB/ -e >Hs4d*76S"6=.&䈜j;̂aēo@$R5dƔfS6:su _[ިFz癴e^)SĠ{-z {hV$gU+댧/~楏xbzeOLB48V`]wzs/+6Dld`8#Ț !ۭ9wx}"b#QEذ }iz&W g&&I3Rз2[{r2E˼v_EÚ(,4{(/|$//ճ<ؖF5`ͲGu;~GV.WqUkm(e{-ܤ9X8ɤ:uemC*n\I^ PP!U5.9\G+OT-ՙPk‚\H%Y WCg8W38A% pj;,Bdul+1i]䰦u7Kb{}9h픛6<ӏ́J+3XmaZ^l3R$>s2:`1er+cO6}@5ڌgRy(M@[i\K4}$wS=͖l$z%yŬ9X&ҟ5 W{@NYrIBǪ]r3h4o`ڂacxۯ|y]hS}EUZ\s_̅ Hs-) ZDr(R "!i:lE{8zS[q/> ԏ [9qd*5xG5@~an3`DݿEL?9\+ |V0i\$' !HCdխlHOBЕw|j؎IKWNv:Cf?g3BdXKUlpnVb`" 4B=*[4_BH_`׺mk.Ob)0=Ft f$l2Û@'Ev/g9ɬr=2n'<;4) ;{%F3n/?uD1PvÐg4qKeBBγ|>HzlꚈ^RHy^ϊ$ 2iWH$[Unj9яJD/ium<b'1}Lk9 VH_Xi.$t[c _=kPB7#,2.2}X?uVv^9Lɖya{*%"ub:/ +F,Zdj0Vfy ].t/;jeh%K_IL^/%}?5wd bsi%evCqGs @o_y:?m77Hzʘ4!+@S=g 𡄓_]~#UZ4s֔C$0h͝/GI.ܭ Sfd䐤O //^\6X! =fΕOX>~$)Jׯy= }6.j{%9&_X#kCR43TI/ 5\TP`Y+WED%7K( &P]V}Hެ,BA:ևlU\_/ҝWlJ޸87O̐)L {kǙW+r-'`)-Up=MB h]5{lLm]q4^N[ȼY0vY \eioEw{]Ɣq CtamICS#2SU^HR ),yoq|9fdƽH!-9˱:_m-濭8fISL2u 3&'媞/P`dydr8!"#6~XB<Αq}.񐚇as~CFc7ݛф~׃9Pry\Rmهf|\D_g|`CloyɩK y7zC8&WdbbDѕ yJF_bzm>?Ee !3$n;Es/)fӸ\L1ѿl"%jukN?+KOiQݾ.Fdr x6:,iunJ:j2,X8j_|fehB;l 'R^jOsMFYeYLrYyd49p1i@"~dнaaq񖭃|Vv=[C^M)x{]L [7TAS?I+aÏ"[clGן05Uxtj?vjS ŀMC"vA:Zds뎞!0 s!`c`hJv_7qwugǷh! K} gh}⛰yӨB֩ųʦ`_A9=ٔ$ :%3([C0Lf4;B- I\ qK.ӻO#QQلF70"pJP>Lζ> |6jBY/:XAb-OĿ$ ":Y`\ dl , N~C<4^u`( ـS<=ݔk逛!aPB F-yN#`4c}_g/LCd{1.qþ3QTx &Oy-]/@"O 4>p6l" (lPY5]FJ-7d^u X :ܳ8JEɹX='^2s` h/F>uz_NsvQ^U8b^ə[S7Z /-A*:{* 1by);dU{ P20j=']r77WxbOr&RQǸG,RFx.)`Fc\[gϗ/-{hHY8"G3~ T&o{s XBa -ǠoTUFhϘ \ n 2 ",eZ GA,ï:7ܬ (L0U㵦wy ?_dƧ. kSoiDj0Y}8:g8]G]G'' Xx[7H!;Dxs 6w2*֗/:޶td&fq{IYO[2Ψ;]ơbV6r%!֞90 -0HI0 =)3F k<An 0=%z_~^S<3hׅgvBFF,j6;9[4 A- #Rc⯓<* Y܁ɬXtoMstE%SHg G*ļЏ#*Ø߹_f0?&Q Uwឣd vܹNH3sJu+N_T̘B[81-_$kP q7ŰFs|nKl6.㬺 Tsi0_Z C52I2lH`T6ƓN`pa~eRI# H.5a'X^igH,4eKvpXʡ.뽵iſܬNngk&37~uҘմ䏵VS{Bbh7 NmZQL#xKN^Ly=|<i/iOvB.z !ݪdKG'aV9bvYKb$ 2315~)fJw8#BWUKN:ƍߢ,jaeDR4HsUw.5+@i'<|nG!EGTh3!'o`nBmZIH:#<:篂9FlNZUk٭qFzh=@("S.zݒh5+?_U&\=aVWk:8tfݍNd5Gak@&rvPnjYu:*BvHϓLG@Z׼~fg |mb@/4z 8\śxld 27aP8o3U<6iĻxT eM 'M@F0iMRz#)Dg2;9^kPc2 џ"_̂9%C~T8oTf-mq6з^o{]hᷭ4ҳyba{jXy Z.{h-jgrB!\2qiZbդt M7{2nJE6m*۰((P\2<[T[?b> KUg~ /2"!Oc GߺBB^0ycO,@Fi)s7]Pyd%yXۯS 4O=J,G0} ?9SO)SnhFʿPbMC ezB3o/nɾx6iOA E}CےnN0G./$nqVӓeWJ3RHNWܝSITOs^Vx2RL+R^F͘Ԃ\FsC :u{/8cR~sww__$Ozjl#DvBNDn:\:Wz h_` &4-5 ?`ߋVДsawS{Z*`=ZTԐ)D֝6Mcf?BHHѯJ8BWT>&إ  6GHlCz|Vژb36y1ZdmD 1c?7M:-eAHp,8N_jaRiwWs݂95FtyX=@|z8չ Kɘ%==˵d%_ܢk׶ٺv6v~^pzw"euHh6٩^n"HOyuc<FkwI}pa;V3VuoaS4T \;{}(/-lH&#+[-`7+p|>t c T>4A#*{,?\|''\$:%%$I^uVh,0\a^}~0V}]ݤ>[ ^}Y蜿^X-4/eW:TO?գEL`E%7_A~i NĕE-iyE`TZ 9 .u-gq#,qa2hfzw^?$>~/^Xpe ?'G-xgðOߦi 4}V.rn{燗䇦Bż! V=n ^0+OkQ: 5@HBzMPt< VH\d6Rjt~5MSZV"涾J}EZ !H.*0s{G=gn{%wP%'NepaR_DT@@P_z5:T?ն{B<ʛ .l$KLȦnó!NAA-}D=7*>\8^zMf 3L7nu莵vV5sȘ%̈ˁQߖ\}r=({n#~3iť_[0,Hys7q&jw7?单JETn,;I̻G7n!Egc}p$S+.K C\q*)@iӓj,thc$9ÃR5\0w}ޕ p. u>c]mK]/[I(Q) Dys.WctjIm<.  hXKbXGa$)m3H1l@`ht͏r"]QL=8,Y*̫9EEȈe(S xl+g1dTfۡ@$arϙYܘ<}4f/j'~/ }`&jI eY>uxE8=M$d۴L=Upgg<;>N kOo#8&3&_cF6:%b#0rJ"tzSdfhR 2Z:p+ڢ.pug"SO(0i <-7 ; Ȩ bEZu6nŧ.!$O5wGSow# $^*G@WSšԻuem$fL9ňF9$;gGpa CYbj/=~ќC+sF pLX݅k݋ZT}3 zԎ7LcSC/<6r)jO EƉ%궺6M@x_0OJ ".t)_s`~kI~`u?[ KCbnR-s&?m""vNWPWНB(pkGhl m!}*e xݏ1>E[9Uׇʏybh?f)v[8! Uc_c_[3/Cv]G Vrm*|cj܌:_z8,|#&Iy!p;PBɨ4UmBX:r"u}Q"Qi%v%8&U+A8^9 |g?K%s5clk|Ms}փpҀ{K6d`Y KƬ]=B`#]@ j K93 <:wx-\05{FL&z'\4fT,^^ǻnR~;(->d lOD  H`B`i%#,1k4 Nn2Os3j3I2Ug{5)zn>eiAmjokDh`YgÙlT78Sa'`?50 555W&ʉSe]2n s46I:϶\ GOr"6#ѯO!coRʥ9Ġz;v𘵌]ć~K vEɞ7A)r㔝KZkVT:Qc!~We٭ac:aQ [ޠHkc]?o5dp/tV9"Uy̔nWDV+Lz2auKءL?h5m,^"B/&h$Kg]lRXɪa|@6MhFuo\w(.c]w-!5f́R͞П^ iT]B >RL ,X?bNL0\w20$@Ӟ #ֶ"#x<"zen8LK7'GPTƭ0cZ-ƧK58dwS*(5'ʞ,a9a) pjtO6 $FGΕZ$&r2!d)QI|ʫLt} ('MJƐfnUS35?Jϣ*NhOߣ+0ܬfNqs44+hRw7a|as>§''d)#٩f_T9$rש!B" Q("ĀCRףqXW ٠!EK|&b(EvmعK_5[v]MNs(`mc XdW;Q/EoQy=Ƕ5~fċԮ)RðrHt(ТFc˞Jmya\PӏhId,<$ҥnOHZhSͳt&l*+[5 IX]:>+~'y ? Μ;Z{LNBPPEN@P4m+=8 ·kʰIۿ`tQ Z<˚vgM م&I |@Wwj鴷ɾvNm56k c;} Hal>m}oB+Ofh 2t}YqWaj5L`>ػ+WNZ6hGfyMky;J?&I_ 5x.F~8}t(FA~Ӌ%m@|#-v)]mY2<3wLi|Z:Mg3_ӝ4=f;TYq&!_Uݽp>S?@t S1+TlqZGg7Ÿՠ:J1Y ۳D,IG39@vASRջJlfJ cv˄gdpMZ_f#X- B5W,u-P0J`Kk1t+ukXCFY,&5 ^G:}ܱM19 ==([UpQ?k/[-p:Uh~TyΖ^k a#jz]C̱efEf`gyBwi\@t #7NJ73Ǩ?:{`h$ ƄSၪ$0HRaEpa(p t桛|3f+xys!ˑYdɲ'&Xg矰paKo5^<ŝ0&DW+O&l UO<[yJnDMY7,JǓAeaͷhZq +[4F>Yó-2Y4@.h9LuU4K:mHM Yךd1lx1i1]} SfoBm'Mi7_i._Q=]ԬMTΕ&hny_SbC3P>;YP`DM铜׬BNjOvē_iAGDWUjR*ɀYYpa&VhIL /B,DR.w(9 D_d6wsqIͪD#Ay~ om>=Ic|P]Y)s8y%AmTUY`ړfbLS>wP G$2mQi|5'\ō 1;wg$YO8qp,漑߱>SO:R4=#$idKQ쎗!삻}tD||r~S)bn5J KiNŻ\M I]ϻ6/IDliPdKpwlV;|u(js-1V,քB)!8a%7_NFm%QDdPCvIy"TZ:k<%jNJmf49D2 s&x$"[D- Lyr҃ܺ \KLs7U0_99R=QhcזvI6VFXF$ğڂJo8szA ()?B~P.yM*Hnxy{jwyޚW42U9-4 3<,TAm:ues/+!p y|T03A%X6?|ҽvt 1²ʼnVrƨ!zͼdM([#شBvmtк˪$ cޤmcHpdV*ǎCsR xvW(4f Om 6qV!Tq3e{Db q弍y]$4 ́$Ae$_1z܋/v4f|W Z͓;-+ UCzv|gڵe{u &&")7ul#A|Oύ#J)&avҏQl*aQvEdFKT񻢃_JĊ @Cج8g%UV&eunp#L-6s?6zP4%YlDO};w[LYD, ? mpeUߺ^l~{C Rj7eNbmмp&.Pp3.jZ' 3Fv9RŴ=_=O~̩WD#*h9'  I9`)M^ǚY{Qif4 3$AHH7f&eI Q%gS3oM`/Yb4y)}</SWj *ZږN ޘRjp){tO9.g/jĨ#sٟeIYft.':a* =^}7&Zsm򭙁4ȧfF&#0ҙ1ѳRniO[BAakrlfr4>ڑyD"u!yH`"]y7h1XO"d(ȠS+S4 ^,$a||wO #Ï3gKЙS5~sS5$W{ w`--G]!kqdTF$.v ކHf(ēA'C"׈ 9ΛKwN8#8 2Xpו@N AJhfxV.omؤ'J/e_WoW{Ry6 dSH.'Uc0 aꞡ}d);}JqF\WT=pyocl:RƢG[Z,ۓŀGvFƅ}Ft653ٮyl'`A*gjLy{i` +Z>CysĖ+u!2fW7psцsCuN=kȹ4L#9 jewwM2oܚ*I9&^;Fat+2i6cV, Z6A14SX݋'v Ķ%VwڴikLi%Gsk\  u.BH͎+hЪIna)\ɏEfakH_.3]C>s*;]__m*_ۿ?vMa'MK.=z© '"}Z/FK*&*ZJըUqR`8nUք]# 7B?UBT|u-Vٞsl_P8i8ɺrYu[[M>3Qd揙 ;L 04r]PEId٦/yR$/O=eIc u<*OPP"; kG{R$iss<,dYɂ4{q6wrK;]%F_ 4ܲ=2BD/$@KXq,XR@\nx`"p&܂=Ŋ4Uփ!5Q>mw;{QQ*Man-Z!%Z Yݭ>f_iz.TFe絛K2]FxЖ |AYH!׎87YY+{pњ!0}"&Yi k;ݘ$gz`$` h`NKZk*DNT`l 5fW r "̛4T8?e*:-3z8 ǩȨihw4qӂl&.$".y}&!̍뢜H3g8cR 뻚9E*:#ᧇjWF?֧!6P~ d }>ڛ5>P+O͋9H61ޑ8) Xk]5a|lGt55Ɩ_"/RL8=1xPmL?iW&Sr3miMXfc4a^*T!"6Ѐ ZCփ,}$ke3ɟ5uɲ' !!A~$7W(6֚Ʈ߫E~gI܉9iIS+zWc&܁:80\ާ04N9  sLQzTMG7%UQEHK5IN>K\=*۽xI#IOtHtҪy~3c { ᷿k(S g[%@{ R`Nܨzce_8BeO&5/w(0Fapkl.zR(8е?ZpaY\J\džݺܟ6 ;Jg7ixɕ$`;Eä[ _-.|gy{ɖj&dŸW_s=Rq˳dK(J,0l!(oH֥Ԇt03\z;ޒ{{Yi%ы"Cx5|*rF@ʹ9{eߗ\x0fң%Cr::tqKL@H!##*>0tZG~ 9&;_+A֪TpR@ٶz+ϏK@uzV. '+X {I, 36BvCUM+טt\DGKu%ǘ^THy戁" 3KM  ZyxeJ| nyI;ɥۦ4;=A{lq<;.(J7%vc˱NXLtTTE4xvF@g&*Il(r~}Inž2|U7Jb-zYGd،b! ',air%k"$N*Z8VTc[|LP)i>3K,|#u(h83} zNx~Lșr[ϱѧnk fsZ }٤M—Z@N(#3}8 @BMDݒg+K]IzCu|~mw亸*-I2 ȉ,-kLs 7 Mn)ʋVI6gOKo@ *fpmd{ -C/&K||E#'eJp-OفO$WRPށ+B ]lGXǩ0cȈ^ |xS7*|$6#NdꓣabLJpuZG[k%Dѓj-0Pg!du{79EʭtF=]mV%5)p1}um5sq(P--#lc=Zr c"[XYR[CI;n'E1Irj:3za>\_> De/x? T}L.g!j(Q4f`Km %1wpīe-G"1;@YyC"h^D?ϺH<PNzݦ"' _-Z}WuV?QSܼ վ ~LAs8$NXH`U ' r>صГoMdpL/>5_ 8w awj<\@M J{Aśq{-K\]V?,zY;T^xnM(,ko %hǢQP)iX Uw)f!Kcvaer]܌P, י=B3OjJNP+ycԁ#\N  \*LQ!5O)'}$D 0At%m4:]%ezkxԂ<"JKQ͇o& 9MPvrz=_|bT}|Y0Z v-BM|<˭0ð3KW]ah}ᗻk\[!tA SgME뼚Pc' !sx?UqvɕܔN|'\)`ALT46@ t{J8Ҕm.sFP)C|ֲ5 t-Mn$yQ:GGNA_:9rKA!mcBH0/ +S򳤂! 1 t7Q)e䁄7*pU=BF*fg Y+6GQ=[IB<{NKRɶrt 紝c)ϊ/HH8޿#u%36 h(݊Zt&*w\RJwҍTQZ6F:7)zATpmAsyi@JByXKYNGgB}"w;xԾ@qFAPj[O!3?FO( rTY[~E?.lӜ Ja 7d1Gts"x<)٥E6~ʼna{-}tP5>< v +qJOAbI.l׾.o\:Q-f0Et9J3xߎ)-c.X#&L%FX?Dզq"W/Ƹ- <D |&:db"9HR`em傣-9&^JP":zeΈæ@YYBk0#bS?+`nDjq'ˤoy'-^6pվЕ WJE[fA:"` ϡ[b |.?w¸7>KJSnN u\:XE]Hgź| z/&q^lhiiKG4mUHUH qɄzD6,:3:a."4[h =+;c?n zϽ<*DlpQ]7ca҉obȫ1PKƃy<fzgvK,8!~RG/1tE1M`Oh"юuB0zUZ';<1^|/?'̤A5XzT\$ŠDɩĝsP]?Ğoey* N jS՚|,b=4qb1X  A+mpSǠ̺tB0 ۾Lvgv< 5X6܆n{3{ JیmgXORW oGՋ W/`ǂ.Wvq"4 mt-HzKe)8ou$lN6CbfѪ`mZ3I8YWS$ K=6CfC>5/ߨ=|9&K04|o<j12ul['7\ED"J{2ҨhM N|'0ՌSXd˷@%_`!cD-|U6*9ܶ{t%-iq;O*կtZ ?Hx g !IALѐr $v9{(<qr~knlwd y;HfwB=>ys: xJ'hOGkn;P;g f04F}<+L- G["oCGNSQV+4,1=)=yL:L؀{ÿ+1B/߈Cή> lOcBtO+g5ඏ^Ns، Nyi}Yʰ9ƁJV1'1D1V)hLgc:M]&uq6`(<U䘢{sW?xFs#} ?W>}!C ,ʔtQX7._|d+xb\hߗ`嚟"͐C0JwctxVݿVDX[1Y8o6ʎC37H_k I4)5?: $y/EiݢD+{I\*w`D @r γz ܔ /S=?F1'@UT:&#LYwh78 rڢXa͎=M@jԾsoFd/ŀnKX?jMypA}VĂ딑:%Q$Ovtkym%Nfˡb qo|i)T;~4w`E7t,c=!ׇ8bҭָ$l37=E k\\xSln j7ڊvʛNٹf/J:Eveu^2k5+:_S #)6fPKYgr* OEv >z3x {A89ڙL|dN2ՕR3!x32V2wܾnzI&|4{"9s@|{mtwܠ`MyŊLW{r6).AԗjقݩW-x 1Mdwc䎶m`DygdF<6]g~\ۡxNg]سd݄z4Ñc"Q|Jcz8;6ؘ>vd1|Kr M/0 SdUR9#U9 U}_ H{McJ*:3M 8ָ6Х esܲc!PB~<zCe)Bq;ZO/lqCm7|/>~0~B N.Cͷ!|~Re;.͙穆ĚPCd^]U꯭_NJ7u];S)l"oε;=-0Xszzv8a!^nCs iNi֔'>#fdo՚lA+.{؈dmֵNQyVfdF!Öɺ#*vPeazoZқm~ich8)GP Ή8*n:rQԜw l 2jf ϸgQ1{S2ܱK)2E'#SJd9 0_SZ[u77UspFuU·e)6_02 .dVV,.zgqgI:.M?f_Ĺؿ$ o%@9gvx`:? 0DbIwpJE>;_R@B#PhRChwRkz&H_0kau)McI\\l;TS^{F=) 80 #۪Xs"/J0[s]N"#dй |KHW#tz%20gf6KHA}FvFO:h";=Lùkڻ%VzrPу?>"ڭ.c̤0ljo(|=xnfԕ"W|Qc~a.TI%)lmԂ9(=RljQ tY5{({qSn*xqQ~WY4Z9̓Q8%PPF<4 M>vr0>lOsɽV̶xD =(pQ!w$@[%\ވwv*<GI boI F]cC?Q5oˀJjߪ鎠qۚ=O WL]|l{$,'^h{d~0'I749~QWMOwM%rxd6HִVaef-~C}ʊS,.jh zn:mvV18Ǝ TgM\,@ke<}Wf,9<$aWN~ʠ*dx;BX*4nxXl/+U]oZDM] ~׺Lt684H!ރo93kq#0Ϯ } %]Շ ݍo=i_Ib631 k&M%Z1 t>ԡt0µۿVqEe:\<2m,/Y.! 1g%JrX?NJԌkL閾0uTą7 gaT K!>T Dk02I~<1Rn`=kH{!{CFGdzܺ8W:6gm3 rWf']0FoJ*vmỳZ3WYkǐ 67lS:9ЎæhVTU$V.N^d\ \e"-:j&FOP<7;0ҟWpUxk4#e?GG#E2s !컠/S\_AER'r0i`in36wF>1Zn7h.X(9M |HU踽&iKiL{9uIBiG,}^wA&|J~7qR$?" >o/HAtIG- U-[I)`XLY)Fero,O*OZʑkf=`ËxEw$-!WgqMk !5Hsu*Ys8yLn4/iSČ+k(d (TA۵H~5Tv("x<*0]*qzevv[hs @i\2W[UF݇4$](#AE _u|`A uv5mN(Lej}ׂ"#)D=Q ݁t:<,Zf*cA* ^yYX ÏNvP^ߗuUřL7,cTyK?h|I tت> -j=Ykp8u<V>aGLP^?|kϰ_̾D(ʤ[ٻBu{,EN_ ,ԃT}@h.;Nqh2Mߏ$-xmxeiNC5ٳZWIC #l럊-|_939hWua*mECLb@)-FnlqM^yPBY4i0-r2w| J]x{9_ L Ņt%ĝIGi8ABQKП?zCA0FBl&k[w}'Bbg 3 zu}!O:+n endstream endobj 6 0 obj 113085 endobj 7 0 obj << /Type /FontDescriptor /FontName /NimbusRomNo9L-Regu /Flags 4 /FontBBox [ -168 -281 1030 924 ] /ItalicAngle 0 /Ascent 924 /Descent -281 /CapHeight 924 /StemV 80 /FontFile 5 0 R >> endobj 8 0 obj << /Length 881 /Filter /FlateDecode >> stream x]n8߃"$1` e b~0i/@#ݏJ1M#2Va8]_px苧4,U]OguY.Voax>rQn]ǏK:؏ygע\.?O5_ܿJ+N׏\5_Tv8Џ_.6e-6]].hUL{z>؏Lv c 5Vp ^A!B]Vk{FF!)dBVhŽFw -#BG8y'qx<qH x<q8> endobj 10 0 obj << /F1 9 0 R >> endobj 11 0 obj << /Font 10 0 R /ProcSet [ /PDF /Text ] >> endobj 1 0 obj << /Type /Page /Parent 4 0 R /Resources 11 0 R /MediaBox [ 0 0 612 792 ] /Group << /S /Transparency /CS /DeviceRGB /I true >> /Contents 2 0 R >> endobj 4 0 obj << /Type /Pages /Resources 11 0 R /MediaBox [ 0 0 595 842 ] /Kids [ 1 0 R ] /Count 1 >> endobj 12 0 obj << /Type /Catalog /Pages 4 0 R >> endobj 13 0 obj << /Author /Creator /Producer /CreationDate (D:20070709114011-05'00') >> endobj xref 0 14 0000000000 65535 f 0000116141 00000 n 0000000021 00000 n 0000000347 00000 n 0000116327 00000 n 0000000373 00000 n 0000113598 00000 n 0000113625 00000 n 0000113864 00000 n 0000114829 00000 n 0000116026 00000 n 0000116068 00000 n 0000116464 00000 n 0000116524 00000 n trailer << /Size 14 /Root 12 0 R /Info 13 0 R /ID [ ] >> startxref 116779 %%EOF cmislib-0.5.1/src/tests/settings.py0000644000076500001200000000475112062733336017746 0ustar jpottsadmin00000000000000# # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # # # Override these settings with values to match your environment. # # CMIS repository's service URL #REPOSITORY_URL = 'http://cmis.alfresco.com/s/cmis' # Alfresco demo #REPOSITORY_URL = 'http://localhost:8080/chemistry/atom' # Apache Chemistry REPOSITORY_URL = 'http://localhost:8080/alfresco/cmisatom' # Alfresco 4.0 #REPOSITORY_URL = 'http://localhost:8080/alfresco/s/api/cmis' # Alfresco #REPOSITORY_URL = 'http://cmis.demo.nuxeo.org/nuxeo/atom/cmis' # Nuxeo demo #REPOSITORY_URL = 'http://localhost:8080/nuxeo/atom/cmis' # Nuxeo local # CMIS repository credentials USERNAME = 'admin' # Alfresco PASSWORD = 'admin' # Alfresco #USERNAME = '' #PASSWORD = '' #USERNAME = 'Administrator' # Nuxeo #PASSWORD = 'Administrator' # Nuxeo EXT_ARGS = {} #EXT_ARGS = {'alf_ticket': 'TICKET_cef29079d8d5341338bf372b08278bc30ec89380'} # Absolute path to a directory where test folders can be created, including # the trailing slash. #TEST_ROOT_PATH = '/default-domain/workspaces/cmislib' # No trailing slash TEST_ROOT_PATH = '/cmislib' # No trailing slash #TEST_ROOT_PATH = '/' # Binary test files. Assumed to exist in the same dir as this python script TEST_BINARY_1 = '250px-Cmis_logo.png' TEST_BINARY_2 = 'sample-a.pdf' # For repositories that support setting an ACL, the name of an existing # principal ID to add to the ACL of a test object. Some repositories care # if this ID doesn't exist. Some repositories don't. TEST_PRINCIPAL_ID = 'tuser1' # For repositories that may index test content asynchronously, the number of # times a query is retried before giving up. MAX_FULL_TEXT_TRIES = 10 # The number of seconds the test should sleep between tries. FULL_TEXT_WAIT = 10