pax_global_header00006660000000000000000000000064127452222160014515gustar00rootroot0000000000000052 comment=281c7bca7335ee0d40826d6eb889e01e89089287 PyPump-0.7/000077500000000000000000000000001274522221600126155ustar00rootroot00000000000000PyPump-0.7/.coveragerc000066400000000000000000000000261274522221600147340ustar00rootroot00000000000000[run] source = pypump PyPump-0.7/.gitignore000066400000000000000000000004661274522221600146130ustar00rootroot00000000000000*.py[cod] *__pycache__* .*.swp # Packages *.egg *.egg-info dist build eggs parts bin lib lib64 include share var sdist develop-eggs .installed.cfg # Installer logs pip-log.txt # Unit test / coverage reports .coverage .tox #Translations *.mo #Mr Developer .mr.developer.cfg # docs build stuff docs/_build PyPump-0.7/.travis.yml000066400000000000000000000007041274522221600147270ustar00rootroot00000000000000--- language: python python: - "2.7" - "3.3" - "3.4" - "3.5" - "pypy" matrix: include: - python: "2.7" env: TOX_ENV=docs install: pip install tox script: tox -e $TOX_ENV install: - python setup.py develop - pip install -q coverage codecov script: - coverage run --branch setup.py test after_success: codecov notifications: irc: channels: - "ircs://irc.megworld.co.uk:9000#pypump" sudo: false PyPump-0.7/AUTHORS.rst000066400000000000000000000010321274522221600144700ustar00rootroot00000000000000Thank you ========== I want to thank anyone who has contributed, I have limited time myself so this project needs other people. I decided to start this list just to identify contributors and thank them. Programming Contributors ========================= - Christopher Webber / cwebber (https://github.com/cwebber) - Sibi / psibi (https://github.com/psibi) - Jonas Haraldsson / kabniel (https://github.com/kabniel) - Benjamin Ooghe-Tabanou / RouxRC (https://github.com/RouxRC) - Matt Molyneaux / moggers87 (https://github.com/moggers87) PyPump-0.7/CHANGELOG.rst000066400000000000000000000040001274522221600146300ustar00rootroot000000000000000.7 === - Fixed bug where Image.original never got any info `#145 `_ - Added Audio and Video objects - Work around bug in pump.io favorites feed which only let us get 20 latest items `#65 `_ - Person.update() now updates Person.location - Fixed bug where PyPump with Python3 failed to save credentials to theJSONStore - Dropped Python 2.6 support, PyPump now supports Python 2.7 or 3.3+ - PyPump now tries HTTPS first, and then only falls back to HTTP if ``verify_requests`` is ``False`` - Fixed bug where ``PyPump.request()`` didnt sign oauth request on redirect `#120 `_ - Implement list methods on ItemList and Feed (``__getitem__`` and ``__len__``) - Moved exceptions into single module and removed some unused exceptions. Make sure to update your import statements! 0.6 === - Person no longer accept a webfinger without a hostname - PumpObject.add_link and .add_links renamed to ._add_link and ._add_links - Recipients can now be set for Comment, Person objects - Recipient properties (.to, .cc, .bto, .bcc) has been moved from Activity to Activity.obj - Feeds (inbox, followers, etc) can now be sliced by object or object_id. - ``Feed.items(offset=int|since=id|before=id, limit=20)`` method. - Unicode improvements when printing Pump objects. - Instead of skipping an Object attribute which has no response data (f.ex Note.deleted when note has not been deleted) we now set the attribute to None. - Fixed WebPump OAuth token issue (`#89 `_) - Allow you to use ``.comment()`` by passing in just a string apposed to a Comment object. `44f3426 `_ - Introduce "Store" object for saving persistant data. Earlier Releases ================ Sorry we didn't keep a change log prior to 0.6, you'll have to fish through the git commits to see what's changed. PyPump-0.7/COPYING000066400000000000000000001045131274522221600136540ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . PyPump-0.7/MANIFEST.in000066400000000000000000000002121274522221600143460ustar00rootroot00000000000000include *.py include README.rst include COPYING include AUTHORS.md include VERSION recursive-include docs * recursive-include pypump *.py PyPump-0.7/README.rst000066400000000000000000000037441274522221600143140ustar00rootroot00000000000000.. image:: https://travis-ci.org/xray7224/PyPump.svg?branch=master :target: https://travis-ci.org/xray7224/PyPump .. image:: https://codecov.io/github/xray7224/PyPump/coverage.svg?branch=master :target: https://codecov.io/github/xray7224/PyPump ================================ PyPump - Python Pump.io Library ================================ What is Pump.io? ================ Pump.io is a socially oriented federated network. It provides a way to post notes and images as well and subscribe to other people to get updates on what they post. What's PyPump? =============== PyPump provides an interface to the pump.io API's. The aim is to provide very natural pythonic representations of Notes, Images, People, etc... allowing you to painlessly interact with them. What does PyPump require? ========================== PyPump works with: - Python (2.7, 3.3+) - requests-oauthlib 0.3.0+ - requests 2.3.0+ *We provide a unified version which works with both the python 2 and python 3 versions.* Documentation ============= PyPump is documented. They are currently hosted on Read The Docs: `latest docs `_ Installation ============ You can install it via pip:: $ pip install pypump Chat with us? ============= If you've found an issue please report it on the GitHub issue's https://github.com/xray7224/PyPump/issues If you just want to chat and let us know the cool things you're doing with PyPump (or want to do) then we have an IRC channel, we currently have our channel on MegNet: _`MegNet`: https://megworld.co.uk/irc _`Webchat`: https://webchat.megworld.co.uk/?channels=#pypump Licence? ======== We're licensed under the GPL version 3 or (at your option) a later version. You can find more information about this licence at https://www.gnu.org/licenses/gpl.html Want to help out? ================== Fantastic! Please do, we always need developers, translators (for documentation), testers, etc... Head over to the github and IRC! PyPump-0.7/docs/000077500000000000000000000000001274522221600135455ustar00rootroot00000000000000PyPump-0.7/docs/Makefile000066400000000000000000000127511274522221600152130ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. PYTHON = `which python` SPHINXOPTS = SPHINXBUILD = $(PYTHON) `which sphinx-build` PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .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/PyPump.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PyPump.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/PyPump" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PyPump" @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." PyPump-0.7/docs/changelog.rst000066400000000000000000000000631274522221600162250ustar00rootroot00000000000000Changelog --------- .. include:: ../CHANGELOG.rst PyPump-0.7/docs/classref.rst000066400000000000000000000047241274522221600161100ustar00rootroot00000000000000Class reference ==================== Essentials ---------- Classes doing most of the work. .. autoclass:: pypump.PyPump .. autoclass:: pypump.Client Pump objects ------------ Classes representing pump.io objects: * :class:`Note ` * :class:`Image ` * :class:`Audio ` * :class:`Video ` * :class:`Comment ` * :class:`Person ` * :class:`Inbox ` * :class:`Outbox ` * :class:`Lists ` .. autoclass:: pypump.models.note.Note :exclude-members: serialize :inherited-members: .. autoclass:: pypump.models.media.Image :inherited-members: .. attribute:: thumbnail :class:`ImageContainer ` holding information about the thumbnail image. .. attribute:: original :class:`ImageContainer ` holding information about the original image. .. autoclass:: pypump.models.media.Audio :inherited-members: .. attribute:: stream :class:`StreamContainer ` holding information about the stream. .. autoclass:: pypump.models.media.Video :inherited-members: .. attribute:: stream :class:`StreamContainer ` holding information about the stream. .. autoclass:: pypump.models.comment.Comment :inherited-members: .. autoclass:: pypump.models.person.Person :inherited-members: .. autoclass:: pypump.models.feed.Inbox :inherited-members: .. autoclass:: pypump.models.feed.Outbox :inherited-members: .. autoclass:: pypump.models.feed.Lists :inherited-members: Low level objects ----------------- Classes you probably don't need to know about. .. autoclass:: pypump.models.media.ImageContainer .. autoclass:: pypump.models.media.StreamContainer .. .. autoclass:: pypump.models.PumpObject .. .. autoclass:: pypump.models.Mapper .. autoclass:: pypump.models.feed.Feed :inherited-members: .. autoclass:: pypump.models.collection.Collection :inherited-members: .. .. autoclass:: pypump.models.feed.ItemList PyPump-0.7/docs/common/000077500000000000000000000000001274522221600150355ustar00rootroot00000000000000PyPump-0.7/docs/common/media.rst000066400000000000000000000020231274522221600166430ustar00rootroot00000000000000Upload Media ============ Uploading images (and in the future other types of media) via PyPump is relatively easy. Firstly you will need to setup a Client and PyPump model, information for how to do that can be found in the quick and dirty guide. Once you've uploaded an image you will be able to do:: >>> my_image = pump.Image() >>> my_image.from_file("/home/jessica/my_image.jpg") That is enough to post an image to pump (or MediaGoblin). You can now look at the URL on the image model:: >>> my_image.url 'https://microca.st/Tsyesika/image/yZJCA42GTfCuaeEBqyc26Q' ---------------------- Interacting with Media ---------------------- Commenting ~~~~~~~~~~ You can then interact with this image:: >> my_image.comment("Hai, this si my comment") Liking ~~~~~~ If you want to like some media you can use:: >> my_image.like() .. note:: MediaGoblin currently doesn't support liking. Deleting ~~~~~~~~ Deleting media is also easy:: >> my_image.delete() .. note:: MediaGoblin current doesn't support deletion of media. PyPump-0.7/docs/conf.py000066400000000000000000000167711274522221600150600ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # PyPump documentation build configuration file, created by # sphinx-quickstart on Mon May 27 12:57:47 2013. # # 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 time import sys import 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('..')) # -- 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', 'sphinx.ext.doctest', 'sphinx.ext.todo', 'sphinx.ext.viewcode'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # Versions/Release version = "0.7" release = "0.7" # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'PyPump' copyright = u'{year}, Jessica Tallon'.format(year=time.strftime("%Y")) # 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 = ['_build'] # 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 --------------------------------------------------- sys.path.append(os.path.abspath("themes")) # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. #html_theme = 'kr' # 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 = ["themes"] # 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 = 'PyPumpdoc' # -- 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', 'PyPump.tex', u'PyPump Documentation', u'Jessica Tallon', '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', 'pypump', u'PyPump Documentation', [u'Jessica Tallon'], 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', 'PyPump', u'PyPump Documentation', u'Jessica Tallon', 'PyPump', 'One line description of project.', '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' PyPump-0.7/docs/examples/000077500000000000000000000000001274522221600153635ustar00rootroot00000000000000PyPump-0.7/docs/examples/pypump-post-note.py000077500000000000000000000105001274522221600212140ustar00rootroot00000000000000#!/usr/bin/env python ## # Copyright (C) 2013 Jessica T. (Tsyesika) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . ## import argparse import json import os from pypump import PyPump, Client class App(object): client_name = 'pypump-post-note' pump = None config_file = os.path.join(os.environ['HOME'],'.config', client_name, 'config.json') config = dict() def __init__(self): """ Init app and log in to our pump.io account """ # Set up argparse self.parser = argparse.ArgumentParser(description='Post a note to the pump.io network') self.parser.add_argument('-u', '--user', dest='webfinger', required=True, help='user@example.com') self.parser.add_argument('-t', '--title', dest='note_title', default=None, help='Note title') self.parser.add_argument('-n', '--note', dest='note_content', required=True, help='Note content') self.args = self.parser.parse_args() self.read_config() # Try to login to our pump.io account using account credentials # from config, if our webfinger is not found in config we will # have to authorize the app with our account. webfinger = self.args.webfinger client = Client( webfinger=webfinger, type='native', name=self.client_name, key=self.config.get(webfinger, {}).get('key'), secret=self.config.get(webfinger, {}).get('secret'), ) self.pump = PyPump( client=client, token=self.config.get(webfinger, {}).get('token'), secret=self.config.get(webfinger, {}).get('token_secret'), verifier_callback=self.verifier, ) # Add account credentials to config in case we didnt have it already self.config[webfinger] = { 'key': self.pump.get_registration()[0], 'secret': self.pump.get_registration()[1], 'token': self.pump.get_token()[0], 'token_secret': self.pump.get_token()[1], } self.write_config() def verifier(self, url): """ Will ask user to click link to accept app and write code """ webbrowser.open(url) print('A browser should have opened up with a link to allow us to access') print('your account, follow the instructions on the link and paste the verifier') print('Code into here to give us access, if the browser didn\'t open, the link is:') print(url) print() return input('Verifier: ').lstrip(" ").rstrip(" ") def write_config(self): """ Write config to file """ if not os.path.exists(os.path.dirname(self.config_file)): os.makedirs(os.path.dirname(self.config_file)) with open(self.config_file, 'w') as f: f.write(json.dumps(self.config)) f.close() def read_config(self): """ Read config from file """ try: with open(self.config_file, 'r') as f: self.config = json.loads(f.read()) f.close() except IOError: return False return True def post_note(self): """ Post note and return the URL of the posted note """ if self.args.note_title: note_title = self.args.note_title else: note_title = None note_content = self.args.note_content mynote = self.pump.Note(display_name=note_title, content=note_content) mynote.to = self.pump.me.followers mynote.cc = self.pump.Public mynote.send() return mynote.id or None if __name__ == '__main__': app = App() url = app.post_note() if url: print('Note posted') else: print('Note could not be posted') PyPump-0.7/docs/gettingstarted/000077500000000000000000000000001274522221600165755ustar00rootroot00000000000000PyPump-0.7/docs/gettingstarted/authentication.rst000066400000000000000000000035741274522221600223570ustar00rootroot00000000000000Authorization ============= What you need to know --------------------- Pump.io uses OAuth 1.0 with dynamic client registration, this is available through a lot of libraries, PyPump uses `oauthlib `_ and a wrapper around it to provide an provide an interface with the `requests `_ library - `requests-oauthlib `. All of that is handled by PyPump however there are some things to know. OAuth works by exchanging pre-established client credentials and tokens, you however have to provide those each time you instantiate the PyPump object. You will have to provide a mechanism to store these so that you can you can provide them the next time. .. note:: As of version 0.6 PyPump is storing credentials using an internal :doc:`../store` object. Example ------- The following will create (for the first time) a connection to a pump.io server for the user Tsyesika@io.theperplexingpariah.co.uk for my client named "Test.io":: >>> from pypump import PyPump, Client >>> client = Client( ... webfinger="Tsyesika@io.theperplexingpariah.co.uk", ... name="Test.io", ... type="native" ...) >>> def simple_verifier(url): ... print('Go to: ' + url) ... return raw_input('Verifier: ') # they will get a code back >>> pump = PyPump(client=client, verifier_callback=simple_verifier) An example of then connecting again (using the same variable names as above). This will produce a PyPump object which will use the same credentials as established above:: >>> client = Client( ... webfinger="Tsyesika@io.theperplexingpariah.co.uk", ... name="Test.io", ... type="native", ... ) >>> pump = PyPump( ... client=client, ... verifier_callback=simple_verifier ... ) PyPump-0.7/docs/gettingstarted/installing.rst000066400000000000000000000017501274522221600214760ustar00rootroot00000000000000Installation ============= Using pip --------- The best way to install PyPump is via pip, if you haven't, setup:: $ virtualenv path/to/virtualenv $ source path/to/virtualenv/bin/activate $ pip install pypump If you get an error which looks like:: Could not find a version that satisfies the requirement pypump (from versions: 0.1.6a, 0.1.7a, 0.1.8a, 0.1.9a, 0.2, 0.1a) Cleaning up... No distributions matching the version for pypump You need to specify the latest version, for example:: $ pip install pypump==0.6 Using git --------- .. Warning:: The code on git may break, the code on pip is likely to be much more stable You can if you want the latest and greatest use the copy on git, to do this execute:: $ git clone https://github.com/xray7224/PyPump.git $ cd PyPump $ virtualenv .vt_env && . .vt_env/bin/activate $ python setup.py develop To keep this up to date use the following command inside the PyPump folder:: $ git pull PyPump-0.7/docs/gettingstarted/qnd.rst000066400000000000000000000112641274522221600201150ustar00rootroot00000000000000Quick 'n Dirty! =============== Introduction ------------ .. warning:: This is not complete, this is used as a fast intro for those fairly familiar or a reference for those who are a little rusty with PyPump This guide is designed to get you up to speed and using this library in a very short amount of time, to do that I avoid long winded explanations, if you're completely new to PyPump and/or pump.io please use :doc:`tutorial`. Getting connected ----------------- So we need to get started:: >>> from pypump import PyPump, Client As Part of our application we will need to ask the user to input a verification code from the website to give us access as part of the OAuth mechanism, the function needs to take a URL and have the user allow our application, for example:: >>> def simple_verifier(url): ... print('Please follow the instructions at the following URL:') ... print(url) ... return raw_input("Verifier: ") # the verifier is a string First we must tell the server about ourselves:: >>> client = Client( webfinger="me@my.server.tld", name="Quick 'n dirty", type="native" # can be "native" or "web" ) Now make PyPump (the class for talking to pump):: >>> pump = PyPump(client=client, verifier_callback=simple_verifier) Super, next, I wanna see my inbox:: >>> my_inbox = pump.me.inbox >>> for activity in my_inbox[:20]: ... print(activity) .. note:: iterating over the inbox without any slice will iterate until the very first note in your inbox/feed/outbox Oh, my friend Evan isn't there, I probably need to follow him:: >>> evan = pump.Person("evan@e14n.org") >>> evan.follow() Awesome. Lets check again:: >>> for activity in my_inbox[:20]: ... print(activity) Evan likes PyPump, super!:: >>> activity = my_inbox[1] # second activity in my inbox >>> awesome_note = activity.obj >>> awesome_note.content 'Oh wow, PyPump is awesome!' >>> awesome_note.like() I wonder if someone else has liked that:: >>> awesome_note.likes [me@my.server.org, joar@some.other.server] Cool! Lets tell them about these docs:: >>> my_comment = pump.Comment("Guys, if you like PyPump check out the docs!") >>> awesome_note.comment(my_comment) I wonder what was posted last:: >>> latest_activity = my_inbox[0] >>> print(latest_activity) Oh it's an image, lets see the thumbnail:: >>> url = latest_activity.obj.thumbnail.url >>> fout = open("some_image.{0}".format(url.split(".")[-1]), "wb") >>> import urllib2 # this will be different with python3 >>> fout.write(urllib2.urlopen(url).read()) >>> fout.close() Hmm, I want to see a bigger version:: >>> large_url = latest_activity.obj.original.url >>> print(large_url) >>> # you will find Images often hold other pump.Image objects, we just need to extra the url >>> large_url = large_url.url >>> fout = open("some_image_larger.{0}".format(large_url.split(".")[-1]), "wb") >>> fout.write(urllib2.urlopen(url).read()) >>> fout.close() That looks awesome, lets post a comment:: >>> my_comment = pump.Comment("Great, super imaeg") >>> latest_activity.obj.comment(my_comment) Oh no, I made a typo:: >>> my_comment.delete() >>> my_comment.content = "Great, super image") >>> latest_activity.obj.comment(my_comment) Much better! Lets make a note to tell people how easy this all is:: >>> my_note = pump.Note("My gawd... PyPump is super easy to get started with") >>> my_note.send() But hold on though, that only sent it to followers? What gives:: >>> awesome_pump = pump.Note("PyPump is really awesome!") >>> awesome_pump.to = pump.Public >>> awesome_pump.cc = (pump.me.followers, pump.Person("MyFriend@server.com")) >>> awesome_pump.send() Oh cool that's sent to all my friends, So can i make my own lists:: >>> for my_list in pump.me.lists: ... print(my_list) Coworkers Family Friends Oh are all those my lists that are defined. How do I send a note to them?:: >>> new_note = pump.Note("Work sucks!") >>> new_note.to = pump.me.lists["Coworkers"] >>> new_note.cc = pump.me.lists["Friends"] So, can i send something to all of of the groups I made? Yep:: >>> another_note = pump.Note("This really goes to everyone in my groups?") >>> another_note.to = list(pump.me.lists) >>> another_note.cc = (pump.Person("moggers87@microca.st"), pump.Person("cwebber@identi.ca")) >>> another_note.send() Don't forget is there are any issues please issue them on our `GitHub `_! PyPump-0.7/docs/gettingstarted/tutorial.rst000066400000000000000000000156551274522221600212060ustar00rootroot00000000000000Tutorial ======== PyPump and the Pump API ----------------------- PyPump is aiming to implement and interface with the `Pump API `_, which is a federation protocol for the web. You can read the actual Pump API docs to get a sense of all that, but here's a high level overview. The Pump API is all about ActivityStreams and sending JSON-encoded descriptions of activities back and forth across different users on different sites. At the highest conceptual level, it's not too different from the idea of email servers sending emails back and forth, but the messages (activities here) are much more specific and carry more specific meaning about what "type" of message is being sent back and forth. An activity can be a user "favoriting" something or "posting an image" or what have you. In the world of email, each user has an email address; in the world of Pump, each user has a `webfinger `_ address. It looks pretty similar, but it's meant for the web. For the sake of this tutorial, you don't need to know how webfinger works; the PyPump will handle that for you. Each user has two main feeds that are used for communication. In the Pump API docs' own wording: - An **activity outbox** (probably at /api/user//feed). This is where the user posts new activities, and where others can read the user's activities. - An **activity inbox** (probably at /api/user//inbox). This is where the user can read posts that were sent to him/her. Remote servers can post activities here to be delivered to the user. (We use the inbox/outbox convention fairly strongly in PyPump.) You can read the Pump spec, but sometimes coding examples are the best way to learn. So, that said, let's get into an example of using PyPump! A quick example --------------- Let's assume you already have a user with the webfinger id of mizbunny@example.org. We want to check what our latest messages are! But before we can do that, we need to authenticate. If this is your first time, you need to authenticate this client:: >>> from pypump import PyPump, Client >>> client = Client( ... webfinger="mizbunny@example.org", ... type="native", # Can be "native" or "web" ... name="Test.io" ... ) >>> def simple_verifier(url): ... print('Go to: ' + url) ... return raw_input('Verifier: ') # they will get a code back >>> pump = PyPump(client=client, verifier_callback=simple_verifier) The PyPump call will try to verify with OAuth. You may wish to change how it asks for authentication. The ``simple_verifier`` function in the example above writes to standard out a URL for the user to click and reads in from standard in for a verification code presented by the webserver. .. note:: By default PyPump will use the JSONStore which comes with PyPump. This will store the client and OAuth credentials created when you connect to pump at ``~/$XDG_CONFIG_HOME/PyPump/credentials.json.`` If you wish to change the path or store the data somewhere else (postgres, mongo, redis, etc.) we suggest you read the :doc:`../store` documentation. .. warning:: You should store the client credentials and tokens somewhere safe, with this information anyone can access the user's pump.io account! You can now reconnect like so:: >>> client = Client( ... webfinger="mizbunny@example.org", ... type="native", ... name="Test.io", ...) >>> pump = PyPump( ... client=client, ... verifier_callback=simple_verifier ...) Okay, we're connected! Next up, we want to check out what our last 30 items in our inbox are, but first we need to find ourselves:: >>> me = pump.Person("mizbunny@example.org") >>> me.summary >>> 'Hello and welcome to my summary' That looks like us, now to find our inbox items. The inbox comes in three versions - me.inbox.major is where major activities such as posted notes and images end up. - me.inbox.minor is where minor activities such as likes and comments end up. - me.inbox is a combination of both of the above. We only want to see notes, so we use the major inbox. The inbox supports python-style index slicing:: >>> recent_activities = me.inbox.major[:30] # get last 30 activities We could print out each of the most recent activities like so:: >>> for activity in recent_activities: >>> print(activity) ... Maybe we're just looking at our most recent message, and see it's from our friend Evan. It seems that he wants to invite us over for a dinner party:: >>> activity = recent_activities[0] >>> activity >>> message = activity.obj >>> message.author >>> message.content "Yo, want to come over to dinner? We're making asparagus!" We can comment on the message saying we'd love to:: >>> our_reply = pump.Comment("I'd love to!") >>> message.comment(our_reply) # this is Evan's message we got above! (Since this Note activity is being instantiated, it needs a reference to our PyPump class instance. Objects that you get back and forth from the API themselves will try to keep track of their own parent PyPump object for you.) We could even like/favourite the previous message:: >>> message.like() We can also check to see what our buddy's public feed is. Maybe he's said some interesting things?:: >>> evan = message.author >>> for activity in evan.outbox: >>> message = activity.obj >>> print(message.content) Perhaps we want to know a bit about Evan:: >>> print(evan.summary) Maybe we took a picture, and we want to post that picture to our public feed so everyone can see it. We can do this by posting it to our outbox:: >>> img = pump.Image( ... display_name="Sunset", ... content="I took this the other day, came out really well!") >>> img.from_file("sunset.jpg") When posting an image or a note you may wish to post it to more people than just your followers (which is the default on most pump servers). You can easily do this by doing:: >>> my_note = pump.Note("This will go to everyone!") >>> my_note.to = pump.Public >>> my_note.send() .. TODO: add explanation of how to list all collections and how to use them You can also send notes to specific people so if I wanted to send a note only to evan to invite him over, I could do something like this:: >>> my_note = pump.Note("Hey evan, would you like to come over later to check out PyPump") >>> my_note.to = pump.Person("e14n@e14n.org") >>> my_note.send() # Only evan will see this. .. Things missing: - Show different types of activities - Explain how to implement an activity subclass? PyPump-0.7/docs/gettingstarted/verifier_callback.rst000066400000000000000000000061731274522221600227650ustar00rootroot00000000000000Getting Verifier ================ For OAuth to allow OOB (Out of band) applications to have access to an account, first we must provide a link and instructions for the user. Then we must provide a means of copying the verifier into an application and relaying it to the server with other tokens. The server will then provide us with the credentials that we can use. You must write a method which takes a URL that the user needs to visit, provide some way for that user to input a string value (the verification), and then give that value to PyPump. This could be simply a case of printing the link and using raw_input/input to get the verifier or it could be a more complex function which redraws a GUI and opens a browser. Simple verifier ---------------- The following is an example of a simple verifier which could be used for a CLI (command line interface) application. This method is actually the same function which is used to prompt the user to provide a verifier in the PyPump Shell:: def verifier(url): """ Asks for verification code for OAuth OOB """ print("Please open and follow the instructions:") print(url) return raw_input("Verifier: ") Callback -------- Having a function which is called and then returns the verification might be more difficult in GUI programs or other interfaces. We provide a callback mechanism that you can use. If the verifier function returns None then PyPump assumes you will be calling PyPump.verifier which takes the verifier as the argument. Complex GUI example ------------------- As an attempt to avoid writing an example which is tied to one GUI library, I have made one up in order to demonstrate exactly what might be involved:: import webbrowser # it's a python module - really, check it out. from pypump import Client, PyPump class MyWindow(guilib.Window): def __init__(self, *args, **kwargs): # Write out to the screen telling them what we're doing self.draw("Please wait, loading...") # setup pypump object client = Client( webfinger='someone@server.com', type='native', name='An awesome GUI client' ) self.pump = PyPump( client=client, verifier_callback=self.ask_for_verifier ) def ask_for_verifier(self, url): """ Takes a URL, opens it and asks for a verifier """ # Open the URL in a browser webbrowser.open(url) # Clear other stuff from window self.clear_window() # draw a text input box and a button to submit self.draw(guilib.InputBox('Verifier:', name='verifier')) self.draw(guilib.Button('Verify!', onclick=self.verifier)) def verifier(self, verifier): """ When the button is clicked it sends the verifier here which we give to PyPump """ self.pump.verifier(verifier) If you return anything from your verifier callback, pypump will expect that to be the verifier code so unless you're actually using a simple method, ensure you return None. PyPump-0.7/docs/gettingstarted/webpump.rst000066400000000000000000000073351274522221600210160ustar00rootroot00000000000000Web Development using PyPump ============================ .. warning:: This section needs to be updated. One of the problem with PyPump and Web development is that you often have a view which is called and then must return a function. While it is possible it may be difficult to use the regular PyPump callback routines. WebPump is a subclassed version of PyPump which handles that for you. The only real difference is you don't specify a `verifier_callback` (if you do it will be ignored). Once the instanciation has completed you can guarantee that the URL for the callback has been created. Django ------ This is an example of a very basic django view which uses WebPump:: from pypump import WebPump from app.models import PumpModel from django.shortcuts import redirect from django.exceptions import ObjectDoesNotExist def pump_view(request, webfinger): try: webfinger = PumpModel.objects.get(webfinger=webfinger) except ObjectDoesNotExist: webfinger = PumpModel.objects.create(webfinger=webfinger) webfinger.save() # make the WebPump object if webfinger.oauth_credentials: pump = WebPump( webfinger.webfinger, client_type="web", client_name="DjangoApp", key=webfinger.key, secret=webfinger.secret token=webfinger.token, token_secret=token_secret, callback_uri="http://my_app.com/oauth/authorize" ) else: pump = WebPump( webfinger.webfinger, client_type="web", client_name="DjangoApp", callback_uri="http://my_app.com/oauth/authorize" ) # save the client credentials as they won't change webfinger.key, webfinger.secret, webfinger.expirey = pump.get_registeration() # save the request tokens so we can identify the authorize callback webfinger.token, webfinger.secret = pump.get_registrat() # save the model back to db webfinger.save() if pump.url is not None: # The user must go to this url and will get bounced back to our # callback_uri we specified above and add the webfinger as a # session cookie. request.session["webfinger"] = webfinger return redirect(pump.url) # okay oauth completed successfully, we can just save the oauth # credentials and redirect. webfinger.token, webfinger.token_secret = pump.get_registration() webfinger.save() # redirect to profile! return redirect("/profile/{webfinger}".format(webfinger)) def authorize_view(request): """ This is the redirect when authorization is complete """ webfinger = request.session.get("webfinger", None) token, verifier = request.GET["token"], request.GET["verifier"] try: webfinger = PumpModel.objects.get( webfiger=webfinger, token=token ) except ObjectDoesNotExist: return redirect("/error") # tell them this is a invalid request pump = WebPump( webfinger.webfinger, client_name="DjangoApp", client_type="web", key=pump.key, secret=pump.secret, ) pump.verifier(verifier) # Save the access tokens back now. webfinger.token, webfinger.token_secret = pump.get_registration() webfinger.save() # and redirect to their profile return redirect("/profile") PyPump-0.7/docs/index.rst000066400000000000000000000030101274522221600154000ustar00rootroot00000000000000.. PyPump documentation master file, created by sphinx-quickstart on Mon May 27 12:57:47 2013. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. PyPump - Python Pump.io Library =============================== Release v\ |release|. PyPump is a simple but powerful and pythonic way of interfacing with the `pump.io `_ API. PyPump is under the `GPLv3 `_. You should probably look to see if/how this may impact you and your programs if you wish to use PyPump. The community for PyPump lives on IRC in the #pypump channel on `MegNet `_. Getting started --------------- If you're new to PyPump and feeling a bit overwhelmed the :doc:`gettingstarted/tutorial` is the best place to go, however if you're familiar with python and understand pump, check out the :doc:`gettingstarted/qnd` guide! .. toctree:: :caption: User documentation :maxdepth: 1 gettingstarted/installing gettingstarted/qnd gettingstarted/tutorial gettingstarted/authentication gettingstarted/verifier_callback gettingstarted/webpump store common/media .. toctree:: :caption: API documentation :maxdepth: 1 classref About PyPump ============ .. toctree:: :maxdepth: 2 :caption: Version history changelog Links ----- * `PyPump on Github `_ * `PyPump on PyPI `_ This was build on |today| PyPump-0.7/docs/store.rst000066400000000000000000000047761274522221600154510ustar00rootroot00000000000000Store ====== Using store objects is the way PyPump handles the storing of certain data it gets that needs to be saved to disk. There is several pieces of data that might be stored such as: - Client ID and secret - OAuth request token and secret - OAuth access token and secret There might be others in the future too. The store object has an interface like a dictionary. PyPump provides a disk JSON store which allows you to easily just save the data to disk. You should note that this data should be considered sensative as with it someone has access to the users pump.io account. Implementation -------------- You probably want to provide your own storage object. There are two extra methods other than dictionary methods you need to implement are:: @classmethod def load(cls, webfinger, pump): """ This should return an instance of the store object full of any data that has been saved. It's your responsibility to set the `prefix` attribute on the store object. webfinger: String containing webfinger of user. pump: PyPump object loading the store object. """ store = cls() store.prefix = webfinger return store def save(self): """ This should save all the data to the storage. """ pass There is a `prefix` attribute will contain the webfinger of the user the data belongs to. All the data stored and loaded should relate to this webfinger. The save is called frequently and multiple times. The AbstractStore class will call the save method everytime something is set/changed on the object. PyPump ------ There are several ways to provide PyPump with a store object. You can pass it in when you create the PyPump object e.g:: >>> my_store = MyStore.load() >>> pump = PyPump(store=my_store, ...) If no storage object is passed, PyPump will call the .create_store method on itself. This will by default call .load(webfinger, pypump) on whatever class is in store_class on PyPump. You can provide your own class there:: >>> class MyPump(PyPump): ... store_class = MyStore ... >>> pump = MyPump(...) This will use the MyStore class. If you want to do something else you can always override the .create_store method:: class MyPump(PyPump): def create_store(self): """ This should create and return the store object """ return MyStore.load( webfinger=self.client.webfinger, pump=self ) For convenience, PyPump comes with a simple JSON store class, `pypump.store.Store`. PyPump-0.7/pypump-shell000077500000000000000000000146331274522221600152110ustar00rootroot00000000000000#!/usr/bin/env python ## # Copyright (C) 2013 Jessica T. (Tsyesika) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . ## import logging import argparse import sys import requests import six from pypump import PyPump, Client try: from IPython import embed from IPython.config.loader import Config except ImportError: six.print_("You need to have ipython installed to run this") sys.exit(1) welcome_banner = """Welcome to the PyPump Shell, We have setup some useful objects for you: {table} If you need help please visit our docs: https://pypump.readthedocs.org/en/latest """ def ascii_table(headings, data): """ Draws an ascii table """ # first we need to work out how long each column should be columns = {} for heading in headings: columns[heading] = len(heading)+1 # plus one for the extra padding # data could also overspill for record in data: for heading, d in record.items(): dlen = len(d)+1 if dlen > columns[heading]: columns[heading] = dlen table = {} # okay now we need to pad the headers readying for drawing for column, width in columns.items(): table[column] = "{name}{padding}".format( name=column, padding=" "*(width-len(column)) ) heading = "| " for column in table.values(): heading += column heading += "| " # now for the data table = {} for i, record in enumerate(data): table[i] = "| " for column, cdata in record.items(): table[i] += "{data}{padding} | ".format( data=cdata, padding=" "*(columns[column]-len(cdata)-1) ) # make the helper function which will draw the seporators def draw_sep(columns): """ Draws +----+--- etc... """ sep = "+" for width in columns: sep += "-"*(width+1) sep += "+" return sep + "\r\n" sepper = lambda: draw_sep(columns.values()) stable = sepper() stable += heading + "\r\n" stable += sepper() for value in table.values(): stable += value + "\r\n" stable += sepper() return stable # few, glad that's over def verifier(url): """ Asks for verification code for OAuth OOB """ six.print_("Please open and follow the instructions:") six.print_(url) return six.moves.input("Verifier: ").lstrip(" ").rstrip(" ") if __name__ == "__main__": # Taken from https://docs.python.org/2/library/logging.html#logging-levels log_levels = "{'CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG', 'NOTSET'}" parser = argparse.ArgumentParser(description='Command line interface to the pump.io APIs') parser.add_argument("--log", "--loglevel", dest="loglevel", default="warning", help="Set level of logging. Can be one of {}".format(log_levels)) parser.add_argument("--logfile", dest="logfile", action="store_true", default=False, help="Write logging to pypump-shell.log") parser.add_argument("--no-check-certificate", dest="verify", action="store_false", default=True, help="Accepts invalid SSL certificates") parser.add_argument("webfinger", help="Webfinger to use when connecting") options = parser.parse_args() log_level = getattr(logging, options.loglevel.upper(), None) if not isinstance(log_level, int): raise ValueError('Invalid log level: {}. Can be one of {}'.format(options.loglevel, log_levels)) logfile = "pypump-shell.log" if options.logfile else None logging.basicConfig(level=log_level, filename=logfile) webfinger = options.webfinger client = Client( webfinger=webfinger, name="PyPump Shell", type="native" ) sys.stdout.write("-> Setting up PyPump ") sys.stdout.flush() # actually get it on the screen - most terms wait for \n try: pump = PyPump(client=client, verifier_callback=verifier, verify_requests=options.verify) except requests.exceptions.SSLError: # This is caused when there has been an invalid certificate. # We should ask the user if they want to continue (common to use # self-signed/invalid certificates in dev enviroment). six.print_("Couldn't validate SSL/TLS certificate for {0}".format(webfinger.split("@", 1)[1])) answer = None while answer not in ["y", "n"]: if answer is not None: six.print_("Sorry, please type 'y' or 'n' (Ctrl-C to exit).") answer = raw_input("Would you like to accept the invalid/untrusted certificate (y/n): ").lower() answer = answer.replace(" ", "") if answer == "n": sys.exit(0) # Create the same PyPump object as above but without ssl verification pump = PyPump(client=client, verifier_callback=verifier, verify_requests=False) # bring curser back so banner walks over the setup message sys.stdout.write("\r") # drop them into a shell cfg = Config() cfg.InteractiveShell.confirm_exit = False # prep the welcome banner welcome_banner = welcome_banner.format( table=ascii_table( ["Variable", "Representation", "Type"], [ { "Variable": "pump", "Representation": str(repr(pump)), "Type": type(pump).__name__, }, ] ) ) embed( config=cfg, banner1=welcome_banner, exit_msg="Remeber! Report any bugs to https://github.com/xray7224/PyPump/issues" ) PyPump-0.7/pypump/000077500000000000000000000000001274522221600141475ustar00rootroot00000000000000PyPump-0.7/pypump/__init__.py000066400000000000000000000017111274522221600162600ustar00rootroot00000000000000## # Copyright (C) 2013 Jessica T. (Tsyesika) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . ## from __future__ import absolute_import from pypump.pypump import PyPump, WebPump from pypump.client import Client from pypump.store import JSONStore, AbstractStore __all__ = ["PyPump", "WebPump", "Client", "JSONStore", "AbstractStore"] PyPump-0.7/pypump/client.py000066400000000000000000000140151274522221600160000ustar00rootroot00000000000000## # Copyright (C) 2013 Jessica T. (Tsyesika) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . ## from __future__ import absolute_import import json import logging import requests from pypump.exceptions import ClientException _log = logging.getLogger(__name__) class Client(object): """This represents a client/application which is using the Pump API. :param webfinger: webfinger id of this user, ie "mary@example.org" This is probably your username @ the domain of the pump instance you're using. :param type: whether this is a "web" or "native" client. Unless you're using pypump as part of a web service, you should choose "native". :param name: The name of this client, ie your application's name. For example, if you were using PyPump to write CoolCommunicator, you would put "CoolCommunicator" here. :param contacts: Who wrote this application? List of email addresses of those who authored this client. :param redirect: a list of URIs as callbacks for the authorization server :param logo: URI of your application's logo. Additionally, the following init args should only be supplied if you've already registered this client with the server. If not, these will be filled in during the `self.register()` step. :param key: If This is the token (the `client_id`) we got back that identifies our client, assuming we've already registered our client. :param secret: This is your secret token that authorizes you to connect to the pump instance (the `client_secret`). :param expirey: When our token expires (`espires_at`) Note that the above three are not the same as the oauth permissions verifying the user has access to post, this is related to permissions and identification of the client software to post. For the premission related to the access of the account, see PyPump.key and PyPump.secret. """ ENDPOINT = "api/client/register" def __init__(self, webfinger, type, name=None, contacts=None, redirect=None, logo=None, key=None, secret=None, expirey=None): self.webfinger = webfinger self.name = name self.type = type self.logo = logo self.contacts = contacts or [] self.redirect = redirect or [] self.key = key self.secret = secret self.expirey = expirey self._pump = None @property def server(self): return self.webfinger.split("@", 1)[1] @property def nickname(self): return self.webfinger.split("@", 1)[0] def set_pump(self, pump): self._pump = pump @property def context(self): """ Provides request context """ type = "client_associate" if self.key is None else "client_update" data = { "type": type, "application_type": self.type, } # is this an update? if self.key: data["client_id"] = self.key data["client_secret"] = self.secret # Add optional params if self.name: data["application_name"] = self.name if self.logo: data["logo_url"] = self.logo if self.contacts: # space seporated list data["contacts"] = " ".join(self.contacts) if self.redirect: data["redirect_uri"] = " ".join(self.redirect) # Convert to JSON and send return json.dumps(data) def request(self, server=None): """ Sends the request """ request = { "headers": {"Content-Type": "application/json"}, "timeout": self._pump.timeout, "data": self.context, } url = "{proto}://{server}/{endpoint}".format( proto=self._pump.protocol, server=server or self.server, endpoint=self.ENDPOINT, ) response = self._pump._requester(requests.post, url, **request) try: server_data = response.json() except ValueError: raise ClientException(response.content) if "error" in server_data: raise ClientException(server_data["error"], self.context) _log.debug("Client registration recieved: %(id)s %(secret)s %(expire)s", { "id": server_data["client_id"], "secret": server_data["client_secret"], "expire": server_data["expires_at"], }) return server_data def register(self, server=None): """ Registers the client with the Pump API retrieving the id and secret """ if (self.key or self.secret): return self.update() server_data = self.request(server) self.key = server_data["client_id"] self.secret = server_data["client_secret"] self.expirey = server_data["expires_at"] def update(self): """ Updates the information the Pump server has about the client """ error = "" if self.key is None: error = "To update a client you need to provide a key" if self.secret is None: error = "To update a client you need to provide the secret" if error: raise ClientException(error) self.request() return True def __repr__(self): if self.key: return "".format(self.server, self.key) return "".format(self.server) def __str__(self): return repr(self) PyPump-0.7/pypump/exceptions.py000066400000000000000000000025041274522221600167030ustar00rootroot00000000000000## # Copyright (C) 2013 Jessica T. (Tsyesika) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . ## class PyPumpException(Exception): pass class PumpException(Exception): """ This is used when the remote server gives an error """ pass class ClientException(Exception): def __init__(self, message, context=None, *args, **kwargs): if context is not None: message = "{0} (context: {1})".format(message, context) super(ClientException, self).__init__(message, *args, **kwargs) class StoreException(Exception): """ Raised when error occurs in store """ pass class ValidationError(StoreException): """ Raised when validation on a field fails """ pass PyPump-0.7/pypump/models/000077500000000000000000000000001274522221600154325ustar00rootroot00000000000000PyPump-0.7/pypump/models/__init__.py000066400000000000000000000460371274522221600175550ustar00rootroot00000000000000## # Copyright (C) 2013 Jessica T. (Tsyesika) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . ## import logging import re import six import os import mimetypes from dateutil.parser import parse from pypump.exceptions import PumpException _log = logging.getLogger(__name__) class PumpObject(object): _ignore_attr = list() _mapping = { "attachments": "attachments", "author": "author", "content": "content", "display_name": "displayName", "downstream_duplicates": "downstreamDuplicates", "id": "id", "image": "image", "in_reply_to": "inReplyTo", "liked": "liked", "links": "links", "published": "published", "summary": "summary", "updated": "updated", "upstream_duplicates": "upstreamDuplicates", "url": "url", "deleted": "deleted", "object_type": "objectType", "_to": "to", "_cc": "cc", "_bto": "bto", "_bcc": "bcc", "_comments": "replies", "_followers": "followers", "_following": "following", "_likes": "likes", "_shares": "shares", } def __init__(self, pypump=None, *args, **kwargs): """ Sets up pump instance """ self.links = {} if pypump: self._pump = pypump # combine mapping of self and PumpObject mapping = PumpObject._mapping.copy() mapping.update(self._mapping) self._mapping = mapping # remove unwanted attributes from mapping for i in self._ignore_attr: if self._mapping.get(i): del self._mapping[i] # add any missing attributes for key in self._mapping.keys(): if not hasattr(self, key): setattr(self, key, None) def _verb(self, verb): """ Posts minimal activity with verb and bare self object. :param verb: verb to be used. """ activity = { "verb": verb, "object": { "id": self.id, "objectType": self.object_type, } } self._post_activity(activity) def _post_activity(self, activity, unserialize=True): """ Posts a activity to feed """ # I think we always want to post to feed feed_url = "{proto}://{server}/api/user/{username}/feed".format( proto=self._pump.protocol, server=self._pump.client.server, username=self._pump.client.nickname ) data = self._pump.request(feed_url, method="POST", data=activity) if not data: return False if "error" in data: raise PumpException(data["error"]) if unserialize: if "target" in data: # we probably want to unserialize target if it's there # true for collection.{add,remove} self.unserialize(data["target"]) else: # copy activity attributes into object if "author" not in data["object"]: data["object"]["author"] = data["actor"] for key in ["to", "cc", "bto", "bcc"]: if key not in data["object"] and key in data: data["object"][key] = data[key] self.unserialize(data["object"]) return True def __unicode__(self): if self.display_name is not None: return u'{name}'.format(name=self.display_name) else: return u'{type}'.format(type=self.object_type) if six.PY3: def __str__(self): return self.__unicode__() else: def __str__(self): return self.__unicode__().encode('utf-8') def _striptags(self, html): return re.sub(r'<[^>]+>', '', html) def _add_link(self, name, link): """ Adds a link to the model """ self.links[name] = link return True def _add_links(self, links, key="href", proxy_key="proxyURL", endpoints=None): """ Parses and adds block of links """ if endpoints is None: endpoints = ["likes", "replies", "shares", "self", "followers", "following", "lists", "favorites", "members"] if links.get("links"): for endpoint in links['links']: # It would seem occasionally the links["links"][endpoint] is # just a string (what would be the href value). I don't know # why, it's likely a bug in pump.io but for now we'll support # this too. if isinstance(links['links'][endpoint], dict): self._add_link(endpoint, links['links'][endpoint]["href"]) else: self._add_link(endpoint, links["links"][endpoint]) for endpoint in endpoints: if links.get(endpoint, None) is None: continue if "pump_io" in links[endpoint]: self._add_link(endpoint, links[endpoint]["pump_io"][proxy_key]) elif "url" in links[endpoint]: self._add_link(endpoint, links[endpoint]["url"]) else: self._add_link(endpoint, links[endpoint][key]) return self.links def unserialize(self, data): Mapper(pypump=self._pump).parse_map(self, mapping=self._mapping, data=data) self._add_links(data) return self class Mapper(object): """ Handles mapping of json attributes to models """ # TODO probably better to move this into the models, # {"json_attr":("model_attr", "datatype"), .. } or similar literals = ["content", "display_name", "id", "object_type", "summary", "url", "preferred_username", "verb", "username", "total_items", "liked", "license", "embed_code"] dates = ["updated", "published", "deleted", "received"] objects = ["generator", "actor", "obj", "author", "in_reply_to", "location"] lists = ["_to", "_cc", "_bto", "_bcc", "object_types", "_items"] feeds = ["_comments", "_followers", "_following", "_likes", "_shares"] def __init__(self, pypump=None, *args, **kwargs): self._pump = pypump def parse_map(self, obj, mapping=None, *args, **kwargs): """ Parses a dictionary of (model_attr, json_attr) items """ mapping = mapping or obj._mapping if "data" in kwargs: for k, v in mapping.items(): if kwargs["data"].get(v, None) is not None: val = kwargs["data"][v] else: val = None self.add_attr(obj, k, val, from_json=True) else: for k, v in mapping.items(): if k in kwargs: self.add_attr(obj, k, kwargs[k]) def add_attr(self, obj, key, data, from_json=False): if key in self.objects: self.set_object(obj, key, data, from_json) elif key in self.dates: self.set_date(obj, key, data, from_json) elif key in self.lists: self.set_list(obj, key, data, from_json) elif key in self.literals: self.set_literal(obj, key, data, from_json) elif key in self.feeds: self.set_feed(obj, key, data, from_json) else: _log.debug("Ignoring unknown attribute %r", key) def set_literal(self, obj, key, data, from_json): if data is not None: setattr(obj, key, data) else: setattr(obj, key, None) def get_object(self, data): try: # Look for suitable PyPump model based on objectType obj_type = data.get("objectType").capitalize() obj = getattr(self._pump, obj_type) obj = obj().unserialize(data) _log.debug("Created PyPump model %r" % obj.__class__) return obj except AttributeError as e: _log.debug("Exception: %s" % e) try: import pypump.models.activity # Look for suitable pumpobject model based on objectType obj_type = data.get("objectType").capitalize() obj = getattr(pypump.models.activity, obj_type) obj = obj(pypump=self._pump).unserialize(data) _log.debug("Created activity.* model: %r" % obj.__class__) return obj except AttributeError as e: # Fall back to PumpObject _log.debug("Exception: %s" % e) obj = PumpObject(pypump=self._pump).unserialize(data) _log.debug("Created PumpObject: %r" % obj.object_type) return obj def set_object(self, obj, key, data, from_json): if from_json: if data is not None: setattr(obj, key, self.get_object(data)) else: setattr(obj, key, None) def set_date(self, obj, key, data, from_json): if from_json: if data is not None: setattr(obj, key, parse(data)) else: setattr(obj, key, None) def set_list(self, obj, key, data, from_json): if from_json: tmplist = [] if data is not None: for i in data: if isinstance(i, six.string_types): tmplist.append(i) else: tmplist.append(self.get_object(i)) setattr(obj, key, tmplist) def set_feed(self, obj, key, data, from_json): from pypump.models.feed import Feed if from_json: if data is not None: try: setattr(obj, key, Feed(pypump=self._pump).unserialize(data)) except Exception as e: _log.debug("Exception %s" % e) else: setattr(obj, key, []) from pypump.models.feed import Feed class Likeable(object): """ Provides the model with the like and unlike methods as well as the property likes which will look up who's liked the model instance and return you back a list of user objects must have links["likes"] """ _likes = None @property def likes(self): """ A :class:`Feed ` of the people who've liked the object. Example: >>> for person in mynote.likes: ... print(person.webfinger) ... pypumptest1@pumpity.net pypumptest2@pumpyourself.com """ endpoint = self.links["likes"] self._likes = self._likes or Feed(endpoint, pypump=self._pump) return self._likes favorites = likes def like(self): """ Like the object. Example: >>> anote.liked False >>> anote.like() >>> anote.liked True """ self._verb('like') def unlike(self): """ Unlike a previously liked object. Example: >>> anote.liked True >>> anote.unlike() >>> anote.liked False """ self._verb('unlike') def favorite(self): """ Favorite the object. """ self._verb('favorite') def unfavorite(self): """ Unfavorite a previously favorited object. """ self._verb('unfavorite') class Commentable(object): """ Provides the model with the comment method allowing you to post a comment to on the model. It also provides an ability to read comments. must have _links["replies"] """ _comments = None @property def comments(self): """ A :class:`Feed ` of the comments for the object. Example: >>> for comment in mynote.comments: ... print(comment) ... comment by pypumptest2@pumpyourself.com """ endpoint = self.links["replies"] self._comments = self._comments or Feed(endpoint, pypump=self._pump) return self._comments def comment(self, comment): """ Add a :class:`Comment ` to the object. :param comment: A :class:`Comment ` instance, text content is also accepted. Example: >>> anote.comment(pump.Comment('I agree!')) """ if isinstance(comment, six.string_types): comment = self._pump.Comment(comment) comment.in_reply_to = self comment.send() class Shareable(object): """ Provides the model with the share and unshare methods and shares property allowing you to see who's shared the model. must have _links["shares"] """ _shares = None @property def shares(self): """ A :class:`Feed ` of the people who've shared the object. Example: >>> for person in mynote.shares: ... print(person.webfinger) ... pypumptest1@pumpity.net pypumptest2@pumpyourself.com """ endpoint = self.links["shares"] self._shares = self._shares or Feed(endpoint, pypump=self._pump) return self._shares def share(self): """ Share the object. Example: >>> anote.share() """ self._verb('share') def unshare(self): """ Unshare a previously shared object. Example: >>> anote.unshare() """ self._verb('unshare') class Deleteable(object): """ Provides the model with the ability to be deleted """ def delete(self): """ Delete the object content on the server. Example: >>> mynote.deleted >>> mynote.delete() >>> mynote.deleted datetime.datetime(2014, 10, 19, 9, 26, 39, tzinfo=tzutc()) """ self._verb('delete') class Addressable(object): """ Adds methods to set to, cc, bto, bcc""" _to = list() _cc = list() _bto = list() _bcc = list() def _set_people(self, people): """ Sets who the object is sent to """ if hasattr(people, "object_type"): people = [people] elif hasattr(people, "__iter__"): people = list(people) return people def _serialize_people(self, people): tmp = [] for person in people: if isinstance(person, six.class_types): tmp.append(person()) if isinstance(person, type(self._pump.Person())): tmp.append({ "id": person.id, "objectType": person.object_type, }) else: # probably a collection tmp.append({ "id": person.id, "objectType": "collection", }) return tmp # to def _get_to(self): """List of primary recipients. If entry is a :class:`Person` the object will show up in their direct inbox when sent. Example: >>> mynote = pump.Note('hello pypumptest1') >>> mynote.to = pump.Person('pypumptest1@pumpity.net') >>> mynote.to [] """ return self._to def _set_to(self, *args, **kwargs): self._to = self._set_people(*args, **kwargs) to = property(fget=_get_to, fset=_set_to) # cc def _get_cc(self): """List of secondary recipients. The object will show up in the recipients inbox when sent. Example: >>> mynote = pump.Note('hello world') >>> mynote.cc = pump.Public """ return self._cc def _set_cc(self, *args, **kwargs): self._cc = self._set_people(*args, **kwargs) cc = property(fget=_get_cc, fset=_set_cc) # bto def _get_bto(self): return self._bto def _set_bto(self, *args, **kwargs): self._bto = self._set_people(*args, **kwargs) bto = property(fget=_get_bto, fset=_set_bto) # bcc def _get_bcc(self): return self._bcc def _set_bcc(self, *args, **kwargs): self._bcc = self._set_people(*args, **kwargs) bcc = property(fget=_get_bcc, fset=_set_bcc) def serialize(self, *args, **kwargs): # now add the to, cc, bto, bcc data = { "to": self._serialize_people(self._to), "cc": self._serialize_people(self._cc), "bto": self._serialize_people(self._bto), "bcc": self._serialize_people(self._bcc), } return data class Postable(Addressable): """ Adds .send() """ def send(self): """ Send the object to the server. Example: >>> mynote = pump.Note('Hello world!) >>> mynote.send() """ data = self.serialize() self._post_activity(data) class Uploadable(Addressable): """ Adds .from_file() """ def from_file(self, filename): """ Uploads a file from a filename on your system. :param filename: Path to file on your system. Example: >>> myimage.from_file('/path/to/dinner.png') """ mimetype = mimetypes.guess_type(filename)[0] or "application/octal-stream" headers = { "Content-Type": mimetype, "Content-Length": os.path.getsize(filename), } # upload file file_data = self._pump.request( "/api/user/{0}/uploads".format(self._pump.client.nickname), method="POST", data=open(filename, "rb").read(), headers=headers, ) # now post it to the feed data = { "verb": "post", "object": file_data, } data.update(self.serialize()) if not self.content and not self.display_name and not self.license: self._post_activity(data) else: self._post_activity(data, unserialize=False) # update post with display_name and content if self.content: file_data['content'] = self.content if self.display_name: file_data['displayName'] = self.display_name if self.license: file_data['license'] = self.license data = { "verb": "update", "object": file_data, } self._post_activity(data) return self PyPump-0.7/pypump/models/activity.py000066400000000000000000000044631274522221600176470ustar00rootroot00000000000000## # Copyright (C) 2013 Jessica T. (Tsyesika) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . ## from pypump.models import PumpObject, Mapper, Addressable import logging _log = logging.getLogger(__name__) class Application(PumpObject): _ignore_attr = ["likes", "replies", "shares", "author", "content", "in_reply_to", "liked", "summary"] _mapping = {} def __init__(self, *args, **kwargs): super(Application, self).__init__(*args, **kwargs) class Activity(PumpObject, Addressable): _ignore_attr = ["author", "deleted", "display_name", "in_reply_to", "liked", "summary"] _mapping = { "verb": "verb", "generator": "generator", "received": "received", "actor": "actor", "obj": "object", } def __init__(self, *args, **kwargs): super(Activity, self).__init__(*args, **kwargs) def __repr__(self): return ''.format( webfinger=self.actor.id.replace("acct:", ""), verb=self.verb.rstrip("e"), # english: e + ed = ed model=self.obj.object_type ) def __unicode__(self): return u'{0}'.format(self._striptags(self.content)) def unserialize(self, data): """ From JSON -> Activity object """ # copy activity attributes into object if "author" not in data["object"]: data["object"]["author"] = data["actor"] for key in ["to", "cc", "bto", "bcc"]: if key not in data["object"] and key in data: data["object"][key] = data[key] Mapper(pypump=self._pump).parse_map(self, data=data) self._add_links(data) return self PyPump-0.7/pypump/models/collection.py000066400000000000000000000066521274522221600201500ustar00rootroot00000000000000## # Copyright (C) 2013 Jessica T. (Tsyesika) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . ## import logging from pypump.models import (PumpObject, Deleteable) from pypump.models.feed import Feed _log = logging.getLogger(__name__) class Collection(PumpObject, Deleteable): """ This object represents a pump.io **collection**, collections have a members :class:`Feed ` and methods for adding and removing objects to that feed. :param id: (optional) Collection id. Example: >>> friendlist = pump.me.lists['Friends'] >>> list(friendlist.members) [] >>> friendlist.add(pump.Person('bob@example.org')) """ object_type = 'collection' _ignore_attr = ["in_reply_to"] _mapping = { "_members": "members" } def __init__(self, id=None, **kwargs): super(Collection, self).__init__(**kwargs) self.id = id @property def members(self): """ :class:`Feed ` of collection members. """ if self._members is None: self._members = Feed(self.links["members"], pypump=self._pump) return self._members def add(self, obj): """ Adds a member to the collection. :param obj: Object to add. Example: >>> mycollection.add(pump.Person('bob@example.org')) """ activity = { "verb": "add", "object": { "objectType": obj.object_type, "id": obj.id }, "target": { "objectType": self.object_type, "id": self.id } } self._post_activity(activity) # Remove the cash so it's re-generated next time it's needed self._members = None def remove(self, obj): """ Removes a member from the collection. :param obj: Object to remove. Example: >>> mycollection.remove(pump.Person('bob@example.org')) """ activity = { "verb": "remove", "object": { "objectType": obj.object_type, "id": obj.id }, "target": { "objectType": self.object_type, "id": self.id } } self._post_activity(activity) # Remove the cash so it's re-generated next time it's needed self._members = None def __unicode__(self): return u'{0}'.format(self.display_name or self.id) def __repr__(self): return "<{type}: {id}>".format(type=self.object_type.capitalize(), id=self.id) class Public(PumpObject): def __init__(self): self.id = "http://activityschema.org/collection/public" self.object_type = 'collection' PyPump-0.7/pypump/models/comment.py000066400000000000000000000046471274522221600174610ustar00rootroot00000000000000## # Copyright (C) 2013 Jessica T. (Tsyesika) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . ## from pypump.models import (PumpObject, Commentable, Likeable, Shareable, Deleteable, Postable) class Comment(PumpObject, Likeable, Shareable, Deleteable, Commentable, Postable): """ This object represents a pump.io **comment**, comments are used to post text (or html) messages in reply to other objects on the pump.io network. :param content: (optional) Comment content. :param in_reply_to: (optional) Object to reply to. Example: >>> catpic >>> mycomment = pump.Comment(content='Best cat pic ever!', in_reply_to=catpic) >>> mycomment.send() """ object_type = 'comment' _ignore_attr = ["summary"] _mapping = {} def __init__(self, content=None, in_reply_to=None, **kwargs): super(Comment, self).__init__(**kwargs) self.content = content self.in_reply_to = in_reply_to def __repr__(self): return "<{type} by {webfinger}>".format( type=self.object_type.capitalize(), webfinger=getattr(self.author, 'webfinger', 'unknown') ) def __unicode__(self): return u"{type} by {webfinger}".format( type=self.object_type, webfinger=getattr(self.author, 'webfinger', 'unknown') ) def serialize(self): data = super(Comment, self).serialize() data.update({ "verb": "post", "object": { "objectType": self.object_type, "content": self.content, "inReplyTo": { "id": self.in_reply_to.id, "objectType": self.in_reply_to.object_type, }, }, }) return data PyPump-0.7/pypump/models/feed.py000066400000000000000000000453021274522221600167130ustar00rootroot00000000000000## # Copyright (C) 2013 Jessica T. (Tsyesika) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . ## import logging import six from pypump.exceptions import PyPumpException from pypump.models import PumpObject, Mapper _log = logging.getLogger(__name__) class ItemList(object): """ This object is returned when iterating over a :class:`Feed `. :param feed: Feed object: Feed object to return items from :param offset: int or PumpObject: beginning of slice :param stop: int or PumpObject: end of slice :param limit: int or None: Number of objects to return :param since: PumpObject: Return objects newer than this :param before: PumpObject: Return objects older than this :param cached: bool: Return objects from feed._items instead of API :raises PyPumpException: if offset is given as well as before/since :raises PyPumpException: if both before since are given """ _done = False def __init__(self, feed, offset=None, stop=None, limit=None, since=None, before=None, cached=False): self.cache = [] self.feed = feed self.url = self.feed.url self.itemcount = 0 self._offset = offset # set limit based on offset and stop if isinstance(stop, int): if isinstance(offset, int): self._limit = stop - offset else: self._limit = stop else: self._limit = limit # set since to stop if stop is a PumpObject if self.get_obj_id(stop): self._since = self.get_obj_id(stop) else: self._since = self.get_obj_id(since) # set before to offset if offset is a PumpObject if self.get_obj_id(offset): self._before = self.get_obj_id(offset) self._offset = None else: self._before = self.get_obj_id(before) self._cached = cached if self._offset and (self._since or self._before): raise PyPumpException("can not have both offset and since/before parameters") elif self._since and self._before: raise PyPumpException("can not have both since and before parameters") def get_obj_id(self, item): """ Get the id of a PumpObject. :param item: id string or PumpObject """ if item is not None: if isinstance(item, six.string_types): return item elif hasattr(item, 'id'): return item.id def get_page(self, url): """ Get a page of items from API """ if url: data = self.feed._request(url, offset=self._offset, since=self._since, before=self._before) # set values to False to avoid using them for next request self._before = False if self._before is not None else None self._since = False if self._since is not None else None if getattr(self.feed, 'issue65', False): self._offset = False if self._since is not None: # we want oldest items first when using 'since' return reversed(data['items']) else: return data['items'] else: return [] def get_cached(self): """ Get items from feed cache while trying to emulate how API handles offset/since/before parameters """ def id_in_list(list, id): if id: if [i for i in list if i.id == id]: return True else: raise PyPumpException("id %r not in feed." % self._since) tmp = [] if self._before is not None: # return list based on before param if not id_in_list(self.feed._items, self._before): return tmp if isinstance(self._before, six.string_types): found = False for i in self.feed._items: if not found: if i.id == self._before: found = True continue else: tmp.append(i) self._before = False return tmp if self._since is not None: # return list based on since param if not id_in_list(self.feed._items, self._since): return tmp if isinstance(self._since, six.string_types): found = False for i in self.feed._items: if i.id == self._since: found = True break else: tmp.append(i) self._since = False return reversed(tmp) if not hasattr(self, 'usedcache'): self.usedcache = True # invalidate cache if isinstance(self._offset, int): # return list based on offset return self.feed._items[self._offset:] return self.feed._items else: return tmp @property def done(self): """ Check if we should stop returning objects """ if self._done: return self._done if self._limit is None: self._done = False elif self.itemcount >= self._limit: self._done = True return self._done def _build_cache(self): """ Build a list of objects from feed's cached items or API page""" self.cache = [] if self.done: return for i in (self.get_cached() if self._cached else self.get_page(self.url)): if not self._cached: # some objects don't have objectType set (inbox activities) if not i.get("objectType"): i["objectType"] = self.feed.object_types[0] obj = Mapper(pypump=self.feed._pump).get_object(i) else: obj = i self.cache.append(obj) # ran out of items if len(self.cache) <= 0: self._done = True # check what to do next time if getattr(self.feed, 'issue65', False): # work around API bug for favorites feed, see https://github.com/xray7224/PyPump/issues/65 if self._offset is None: self._offset = 0 self._offset += 20 elif self._since is not None: if self.feed.links.get('prev'): self.url = self.feed.links['prev'] del self.feed.links['prev'] # avoid using it again else: if self.feed.links.get('next'): self.url = self.feed.links['next'] del self.feed.links['next'] # avoid using it again else: self.url = None def __getitem__(self, key): """ This method has the same limitations as the method on :class:`Feed ` Additionally raises `PyPumpException` if an offset is specified as well as before/since. """ if isinstance(key, slice): return self._getslice(key) if type(key) is not int: raise TypeError('index must be integer') total = self._limit or self.feed.total_items if key > total or key < -total: raise IndexError("ItemList index out of range") if self._since or self._before: # we can't combine since/before and offset, so grab all results up to the one we want if key < 0: key = key + total items = ItemList(self.feed, before=self._before, since=self._since, limit=key + 1, cached=self.feed.is_cached) items = list(items) # last item fetched will be the one for us return items[-1] else: if self._offset: # shift key by current offset if key >= 0: key = self._offset + key else: key = key + total + self._offset elif key < 0: key = key + total item = ItemList(self.feed, limit=1, offset=key, stop=key + 1, cached=self.feed.is_cached) try: return item.next() except StopIteration: raise IndexError("ItemList index out of range") def _getslice(self, s): if not isinstance(s.start, (type(None), int)) or not isinstance(s.stop, (type(None), int)): raise TypeError('slice indices must be integers or None') if self._before is not None or self._since is not None: if s.start is not None: raise PyPumpException("can not have both offset and since/before parameters") elif s.stop is not None and s.stop < 0: raise PyPumpException("can not count backwards with since/before parameters") return ItemList(self.feed, before=self._before, since=self._since, limit=s.stop, cached=self.feed.is_cached) offset = self._offset or 0 stop = s.stop if stop is None: stop = len(self) elif isinstance(stop, int) and stop < 0: stop = len(self) + stop stop = stop + offset if s.start is not None: if s.start > 0: offset = offset + s.start elif s.start < 0: offset = len(self) + offset + s.start return ItemList(self.feed, offset=offset, stop=stop, cached=self.feed.is_cached) def __len__(self): return len([item for item in self]) def __next__(self): """ Return next object or raise StopIteration """ if len(self.cache) <= 0: self._build_cache() if self.done: raise StopIteration else: obj = self.cache.pop(0) self.itemcount += 1 return obj def next(self): return self.__next__() def clone(self): return ItemList(self.feed, limit=self._limit, offset=self._offset, before=self._before, since=self._since, cached=self.feed.is_cached ) def __iter__(self): return self.clone() class Feed(PumpObject): """ This object represents a basic pump.io **feed**, which is used for navigating a list of objects (inbox, followers, shares, likes and so on). """ _ignore_attr = [] _mapping = { "id": "url", "object_types": "objectTypes", "_items": "items", "total_items": "totalItems", } def __init__(self, url=None, *args, **kwargs): super(Feed, self).__init__(*args, **kwargs) self.url = url or None @property def is_cached(self): return self._items is not None and self.total_items is not None and len(self._items) >= self.total_items def items(self, offset=None, limit=20, since=None, before=None, *args, **kwargs): """ Get a feed's items. :param offset: Amount of items to skip before returning data :param since: Return items added after this id (ordered old -> new) :param before: Return items added before this id (ordered new -> old) :param limit: Amount of items to return """ return ItemList(self, offset=offset, limit=limit, since=since, before=before, cached=self.is_cached) def _request(self, url, offset=None, since=None, before=None): params = dict() for i in ["offset", "since", "before"]: if eval(i): params[i] = eval(i) _log.debug("Feed._request: url: %s, params: %s", url, params) data = self._pump.request(url, params=params) self.unserialize(data) return data def unserialize(self, data): Mapper(pypump=self._pump).parse_map(self, data=data) self._add_links(data) self.url = data.get('pump_io', {}).get('proxyURL') or self.url return self def _subfeed(self, feedname): """ Used for Inbox/Outbox major/minor/direct subfeeds """ url = self.url if not url.endswith("/"): url += "/" return url + feedname def __getitem__(self, key): """ ``key`` should either be an integer or a ``slice`` object. If a ``slice`` object is passed in with a step parameter, the stepping will be silently ignored. For example:: >>> inbox = pump.me.inbox[slice(0, 10, 2)] >>> print(len(inbox)) 10 # step been ignored """ if isinstance(key, slice): stop = key.stop if stop is None: stop = len(self) elif isinstance(stop, int) and stop < 0: stop = len(self) + stop return ItemList(self, offset=key.start, stop=stop, cached=self.is_cached) if type(key) is not int: raise TypeError('index must be integer') item = ItemList(self, limit=1, offset=key, stop=key + 1, cached=self.is_cached) return item[key] def __len__(self): if self.total_items is None: # a hacky way to populate the cache list(ItemList(self, limit=1, cached=False)) return self.total_items def __iter__(self): return self.items(limit=None) def __repr__(self): return ''.format(url=self.url) def __unicode__(self): return u'{name}'.format(name=self.display_name or '') class Followers(Feed): """ Person's followers """ class Following(Feed): """ People followed by Person """ class Favorites(Feed): """ Person's favorites """ # API bug, can only get 20 items, see https://github.com/xray7224/PyPump/issues/65 # mark feed so we can enable bug work around in ItemList._build_cache() issue65 = True class Inbox(Feed): """ This object represents a pump.io **inbox feed**, it contains all activities posted to the owner of the inbox. Example: >>> for activity in pump.me.inbox.items(limit=3): ... print(activity) Alice posted a note Bob posted a comment in reply to a note Alice liked a comment """ _direct = None _minor = None _major = None def __init__(self, *args, **kwargs): super(Inbox, self).__init__(*args, **kwargs) @property def direct(self): """ Direct inbox feed, contains activities addressed directly to the owner of the inbox. """ url = self._subfeed("direct") if "direct" in self.url or "major" in self.url or "minor" in self.url: return self self._direct = self._direct or self.__class__(url, pypump=self._pump) return self._direct @property def major(self): """ Major inbox feed, contains major activities such as notes and images. """ url = self._subfeed("major") if "major" in self.url or "minor" in self.url: return self self._major = self._major or self.__class__(url, pypump=self._pump) return self._major @property def minor(self): """ Minor inbox feed, contains minor activities such as likes, shares and follows. """ url = self._subfeed("minor") if "minor" in self.url or "major" in self.url: return self self._minor = self._minor or self.__class__(url, pypump=self._pump) return self._minor class Outbox(Feed): """ This object represents a pump.io **outbox feed**, it contains all activities posted by the owner of the outbox. Example: >>> for activity in pump.me.outbox.items(limit=3): ... print(activity) Bob posted a note Bob liked an image Bob followed Alice """ _major = None _minor = None def __init__(self, *args, **kwargs): super(Outbox, self).__init__(*args, **kwargs) @property def major(self): """ Major outbox feed, contains major activities such as notes and images. """ url = self._subfeed("major") if "major" in self.url or "minor" in self.url: return self self._major = self._major or self.__class__(url, pypump=self._pump) return self._major @property def minor(self): """ Minor outbox feed, contains minor activities such as likes, shares and follows. """ url = self._subfeed("minor") if "major" in self.url or "minor" in self.url: return self self._minor = self._minor or self.__class__(url, pypump=self._pump) return self._minor class Lists(Feed): """ This object represents a pump.io **lists feed**, it contains the :class:`collections ` (or lists) created by the owner. Example: >>> for i in pump.me.lists.items(): ... print(i) Coworkers Acquaintances Family Friends """ # API bug, offset and count doesnt work right, # see https://github.com/e14n/pump.io/issues/794 # TODO can not see lists for persons on remote server (need more auth than 2-leg) _membertype = "person" @property def membertype(self): return self._membertype def create(self, display_name, content=None): """ Create a new user list :class:`collection `. :param display_name: List title. :param content: (optional) List description. Example: >>> pump.me.lists.create(display_name='Friends', content='List of friends') >>> myfriends = pump.me.lists['Friends'] >>> print(myfriends) Friends """ activity = { "verb": "create", "object": { "objectType": "collection", "objectTypes": [self.membertype], "displayName": display_name, "content": content } } if self._post_activity(activity, unserialize=False): return self[display_name] def __getitem__(self, key): if isinstance(key, six.string_types): lists = list(self) for i in lists: if i.display_name == key: return i else: return super(Lists, self).__getitem__(key) PyPump-0.7/pypump/models/media.py000066400000000000000000000123561274522221600170720ustar00rootroot00000000000000## # Copyright (C) 2015 Jessica T. (Tsyesika) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . ## import logging from pypump.models import (PumpObject, Likeable, Shareable, Commentable, Deleteable, Uploadable, Mapper) _log = logging.getLogger(__name__) class MediaObject(PumpObject, Likeable, Shareable, Commentable, Deleteable, Uploadable): object_type = 'dummy' _ignore_attr = ["summary"] _mapping = { "stream": "stream", "license": "license", "embed_code": "embedCode", } def __init__(self, display_name=None, content=None, license=None, **kwargs): super(MediaObject, self).__init__(**kwargs) self.display_name = display_name self.content = content self.license = license def __repr__(self): return "<{type} by {webfinger}>".format( type=self.object_type.capitalize(), webfinger=getattr(self.author, 'webfinger', 'unknown') ) def __unicode__(self): return u"{type} by {webfinger}".format( type=self.object_type, webfinger=getattr(self.author, 'webfinger', 'unknown') ) def _get_fileurl(self, data): if data.get("pump_io", {}).get("proxyURL"): return data["pump_io"]["proxyURL"] else: return data["url"] def unserialize(self, data): if "stream" in data: stream = data["stream"] self.stream = StreamContainer( url=self._get_fileurl(stream) ) Mapper(pypump=self._pump).parse_map(self, data=data) self._add_links(data) return self class StreamContainer(object): """ Container that holds information about a stream. :param url: URL to the file on the pump.io server. """ def __init__(self, url): self.url = url def __repr__(self): return "".format(url=self.url) class Video(MediaObject): """ This object represents a pump.io **video** object, video objects are used to post video content with optional text (or html) messages to the pump.io network. :param content: (optional) Video text content. :param display_name: (optional) Video title. Example: >>> myogv = pump.Video(display_name='Happy Caturday!') >>> myogv.from_file('/path/to/kitteh.ogv') """ object_type = 'video' class Audio(MediaObject): """ This object represents a pump.io **audio** object, audio objects are used to post audio content with optional text (or html) messages to the pump.io network. :param content: (optional) Audio text content. :param display_name: (optional) Audio title. Example: >>> myogg = pump.Audio(display_name='Happy Caturday!') >>> myogg.from_file('/path/to/kitteh.ogg') """ object_type = 'audio' class ImageContainer(object): """ Container that holds information about an image. :param url: URL to image file on the pump.io server. :param width: Width of the image. :param height: Height of the image. """ def __init__(self, url, width, height): self.url = url self.width = width self.height = height def __repr__(self): return "".format( width=self.width, height=self.height ) class Image(MediaObject): """ This object represents a pump.io **image**, images are used to post image content with optional text (or html) messages to the pump.io network. :param content: (optional) Image text content. :param display_name: (optional) Image title. Example: >>> myimage = pump.Image(display_name='Happy Caturday!') >>> myimage.from_file('/path/to/kitteh.png') """ object_type = 'image' _ignore_attr = ["summary", "image"] _mapping = { "thumbnail": "image", "original": "fullImage", "license": "license", } def unserialize(self, data): if "image" in data: thumbnail = data["image"] self.thumbnail = ImageContainer( url=self._get_fileurl(thumbnail), height=thumbnail.get("height"), width=thumbnail.get("width") ) if "fullImage" in data: full_image = data["fullImage"] self.original = ImageContainer( url=self._get_fileurl(full_image), height=full_image.get("height"), width=full_image.get("width") ) else: self.original = self.thumbnail Mapper(pypump=self._pump).parse_map(self, data=data) self._add_links(data) return self PyPump-0.7/pypump/models/note.py000066400000000000000000000045311274522221600167540ustar00rootroot00000000000000## # Copyright (C) 2013 Jessica T. (Tsyesika) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . ## from pypump.models import (PumpObject, Postable, Likeable, Shareable, Commentable, Deleteable) class Note(PumpObject, Postable, Likeable, Shareable, Commentable, Deleteable): """ This object represents a pump.io **note**, notes are used to post text (or html) messages to the pump.io network. :param content: (optional) Note content. :param display_name: (optional) Note title. Usage:: >>> mynote = pump.Note(content='Hello world!') >>> mynote.send() """ object_type = 'note' _ignore_attr = ["summary"] _mapping = {} def __init__(self, content=None, display_name=None, **kwargs): super(Note, self).__init__(**kwargs) self.content = content self.display_name = display_name def serialize(self): """ Converts the post to something compatible with `json.dumps` """ data = super(Note, self).serialize() data.update({ "verb": "post", "object": { "objectType": self.object_type, "content": self.content, } }) if self.display_name: data["object"]["displayName"] = self.display_name return data def __repr__(self): return "<{type} by {webfinger}>".format( type=self.object_type.capitalize(), webfinger=getattr(self.author, 'webfinger', 'unknown') ) def __unicode__(self): return u"{type} by {webfinger}".format( type=self.object_type, webfinger=getattr(self.author, 'webfinger', 'unknown') ) PyPump-0.7/pypump/models/person.py000066400000000000000000000157441274522221600173250ustar00rootroot00000000000000## # Copyright (C) 2013 Jessica T. (Tsyesika) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . ## import six from pypump.models import PumpObject, Addressable from pypump.exceptions import PyPumpException from pypump.models.feed import (Followers, Following, Lists, Favorites, Inbox, Outbox) class Person(PumpObject, Addressable): """ This object represents a pump.io **person**, a person is a user on the pump.io network. :param webfinger: User ID in ``nickname@hostname`` format. Example: >>> alice = pump.Person('alice@example.org') >>> print(alice.summary) Hi, I'm Alice >>> mynote = pump.Note('Hey Alice, it's Bob!') >>> mynote.to = alice >>> mynote.send() """ object_type = 'person' _ignore_attr = ['liked', 'in_reply_to'] _mapping = { "username": "preferredUsername", "location": "location", } _inbox = None _outbox = None _followers = None _following = None _favorites = None _lists = None @property def outbox(self): """ :class:`Outbox feed ` with all :class:`activities ` sent by the person. Example: >>> for activity in pump.me.outbox[:2]: ... print(activity) ... pypumptest2 unliked a comment in reply to a note pypumptest2 deleted a note """ self._outbox = self._outbox or Outbox(self.links['activity-outbox'], pypump=self._pump) return self._outbox @property def followers(self): """ :class:`Feed ` with all :class:`Person ` objects following the person. Example: >>> alice = pump.Person('alice@example.org') >>> for follower in alice.followers[:2]: ... print(follower.id) ... acct:bob@example.org acct:carol@example.org """ self._followers = self._followers or Followers(self.links['followers'], pypump=self._pump) return self._followers @property def following(self): """ :class:`Feed ` with all :class:`Person ` objects followed by the person. Example: >>> bob = pump.Person('bob@example.org') >>> for followee in bob.following[:3]: ... print(followee.id) ... acct:alice@example.org acct:duncan@example.org """ self._following = self._following or Following(self.links['following'], pypump=self._pump) return self._following @property def favorites(self): """ :class:`Feed ` with all objects liked/favorited by the person. Example: >>> for like in pump.me.favorites[:3]: ... print(like) ... note by alice@example.org image by bob@example.org comment by evan@e14n.com """ self._favorites = self._favorites or Favorites(self.links['favorites'], pypump=self._pump) return self._favorites @property def lists(self): """ :class:`Lists feed ` with all lists owned by the person. Example: >>> for list in pump.me.lists: ... print(list) ... Acquaintances Family Coworkers Friends """ self._lists = self._lists or Lists(self.links['lists'], pypump=self._pump) return self._lists @property def inbox(self): """ :class:`Inbox feed ` with all :class:`activities ` received by the person, can only be read if logged in as the owner. Example: >>> for activity in pump.me.inbox[:2]: ... print(activity.id) ... https://microca.st/api/activity/BvqXQOwXShSey1HxYuJQBQ https://pumpyourself.com/api/activity/iQGdnz5-T-auXnbUUdXh-A """ if not self.isme: raise PyPumpException("You can't read other people's inboxes") self._inbox = self._inbox or Inbox(self.links['activity-inbox'], pypump=self._pump) return self._inbox @property def webfinger(self): return self.id.replace("acct:", "") @property def server(self): return self.id.split("@")[-1] @property def isme(self): return (self.username == self._pump.client.nickname and self.server == self._pump.client.server) def __init__(self, webfinger=None, **kwargs): super(Person, self).__init__(**kwargs) if isinstance(webfinger, six.string_types): if "@" not in webfinger: # TODO do better validation raise PyPumpException("Not a valid webfinger: %s" % webfinger) self.id = "acct:{0}".format(webfinger) self.username = webfinger.split("@")[0] self._add_link('self', "{0}://{1}/api/user/{2}/profile".format( self._pump.protocol, self.server, self.username) ) try: data = self._pump.request(self.links['self']) self.unserialize(data) except: pass def serialize(self, verb): data = super(Person, self).serialize() data.update({ "verb": verb, "object": { "id": self.id, "objectType": self.object_type, "displayName": self.display_name, "summary": self.summary, "location": self.location.serialize() } }) return data def follow(self): """ Follow person """ self._verb('follow') def unfollow(self): """ Unfollow person """ self._verb('stop-following') def update(self): """ Updates person object""" data = self.serialize(verb="update") self._post_activity(data) def __repr__(self): return "<{type}: {webfinger}>".format( type=self.object_type.capitalize(), webfinger=getattr(self, 'webfinger', 'unknown') ) def __unicode__(self): return u"{0}".format(self.display_name or self.username or self.webfinger) PyPump-0.7/pypump/models/place.py000066400000000000000000000047701274522221600171000ustar00rootroot00000000000000## # Copyright (C) 2013 Jessica T. (Tsyesika) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . ## from pypump.models import PumpObject, Mapper class Place(PumpObject): object_type = 'place' _ignore_attr = list() _mapping = {} display_name = None longitude = None latitude = None def __init__(self, display_name=None, longitude=None, latitude=None, *args, **kwargs): super(Place, self).__init__(*args, **kwargs) self.display_name = display_name self.longitude = longitude self.latitude = latitude def __unicode__(self): return u"{name}".format(name=self.display_name or 'unknown') def serialize(self): data = { "displayName": self.display_name, "objectType": self.object_type, } try: data.update({ "lon": float(self.longitude), "lat": float(self.latitude), }) except (TypeError, ValueError): # ignore lat/lon data if it has non-floatable content pass return data def unserialize(self, data): if ("lon" in data and "lat" in data): self.longitude = float(data["lon"]) self.latitude = float(data["lat"]) elif "position" in data: position = data["position"][:-1] if position[1:].find("+") != -1: latitude = position.lstrip("+").split("+", 1)[0] self.latitude = float(latitude) self.longitude = float(position[1:].split("+", 1)[1]) else: latitude = position.lstrip("+").split("-", 1)[0] self.latitude = float(latitude) self.longitude = float(position[1:].split("-", 1)[1]) else: self.longitude = None self.latitude = None Mapper(pypump=self._pump).parse_map(self, data=data) return self PyPump-0.7/pypump/pypump.py000066400000000000000000000434321274522221600160610ustar00rootroot00000000000000# -*- coding: utf-8 -*- ## # Copyright (C) 2013 Jessica T. (Tsyesika) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . ## from __future__ import absolute_import import json import logging import requests from six.moves.urllib import parse from requests_oauthlib import OAuth1, OAuth1Session from pypump.store import JSONStore from pypump.client import Client from pypump.exceptions import PyPumpException # load models from pypump.models.note import Note from pypump.models.comment import Comment from pypump.models.person import Person from pypump.models.place import Place from pypump.models.media import (Video, Audio, Image) from pypump.models.collection import Collection, Public _log = logging.getLogger(__name__) class PyPump(object): """Main class to interface with PyPump. This class keeps everything together and is responsible for making requests to the server on it's own behalf and on the behalf of the other clients as well as handling the OAuth requests. :param client: an instance of :class:`Client `. :param verifier_callback: If this is our first time registering the client, this function will be called with a single argument, the url one can post to for completing verification. :param store: this is the :class:`pypump.Store` instance to save any data persistantly. :param callback: the URI that is used for redirecting a user after they authenticate this client... assuming this is happening over the web. If not, the callback is "oob", or "out of band". :param verify_requests: If this is set to False PyPump won't check SSL/TLS certificates. :param retries: number of times to retry if a request fails. :param timeout: how long to give on a timeout for an http request, in seconds. """ PARAM_VERIFER = "oauth_verifier" PARAM_TOKEN = "oauth_token" PARAM_TOKEN_SECRET = "oauth_token_secret" URL_CLIENT_REGISTRATION = "/api/client/register" store_class = JSONStore def __init__(self, client, verifier_callback, store=None, callback="oob", verify_requests=True, retries=0, timeout=30): self._me = None self.protocol = "https" self.retries = retries self.timeout = timeout self._server_cache = {} self._server_tokens = {} self.verify_requests = verify_requests self.callback = callback self.client = client self.verifier_callback = verifier_callback self._server_cache[self.client.server] = self.client # Setup store object if store is None: self.store = self.create_store() else: self.store = store # Setup variables for client self.client.set_pump(self) if "client-key" in self.store: self.client.key = self.store["client-key"] if "client-secret" in self.store: self.client.secret = self.store["client-secret"] if "client-expirey" in self.store: self.client.expirey = self.store["client-expirey"] if not self.client.key: self.client.register() # Save the info back to the store self.store["client-key"] = self.client.key self.store["client-secret"] = self.client.secret self.store["client-expirey"] = self.client.expirey self._populate_models() if "oauth-request-token" not in self.store and "oauth-access-token" not in self.store: # we Need to make a new oauth request self.oauth_request() @property def me(self): """ Returns :class:`Person ` instance of the logged in user. Example: >>> pump.me """ if self._me is not None: return self._me self._me = self.Person("{username}@{server}".format( username=self.client.nickname, server=self.client.server, )) return self._me def create_store(self): """ Creates store object """ if self.store_class is not None: return self.store_class.load(self.client.webfinger, self) raise NotImplementedError("You need to specify PyPump.store_class or override PyPump.create_store method.") def _populate_models(self): def factory(pypump, model): return lambda *args, **kwargs: model( pypump=kwargs.pop("pypump", pypump), *args, **kwargs) self.Note = factory(self, Note) self.Collection = factory(self, Collection) self.Comment = factory(self, Comment) self.Image = factory(self, Image) self.Video = factory(self, Video) self.Audio = factory(self, Audio) self.Person = factory(self, Person) self.Place = factory(self, Place) self.Public = Public() def _build_url(self, endpoint): """ Returns a fully qualified URL """ server = None if "://" in endpoint: # looks like an url, let's break it down server, endpoint = self._deconstruct_url(endpoint) endpoint = endpoint.lstrip("/") url = "{proto}://{server}/{endpoint}".format( proto=self.protocol, server=self.client.server if server is None else server, endpoint=endpoint, ) return url def _deconstruct_url(self, url): """ Breaks down URL and returns server and endpoint """ url = url.split("://", 1)[-1] server, endpoint = url.split("/", 1) return (server, endpoint) def _add_client(self, url, key=None, secret=None): """ Creates Client object with key and secret for server and adds it to _server_cache if it doesnt already exist """ if "://" in url: server, endpoint = self._deconstruct_url(url) else: server = url if server not in self._server_cache: if not (key and secret): client = Client( webfinger=self.client.webfinger, name=self.client.name, type=self.client.type, ) client.set_pump(self) client.register(server) else: client = Client( webfinger=self.client.webfinger, key=key, secret=secret, type=self.client.type, name=self.client.name, ) client.set_pump(self) self._server_cache[server] = client def request(self, endpoint, method="GET", data="", raw=False, params=None, retries=None, client=None, headers=None, timeout=None, **kwargs): """ Make request to endpoint with OAuth. Returns dictionary with response data. :param endpoint: endpoint path, or a fully qualified URL if raw=True. :param method: GET (default), POST or DELETE. :param data: data to send in the request body. :param raw: use endpoint as entered without trying to modify it. :param params: dictionary of parameters to send in the query string. :param retries: number of times to retry if a request fails. :param client: OAuth client data, if False do request without OAuth. :param headers: dictionary of HTTP headers. :param timeout: the timeout for a request, in seconds. Example: >>> pump.request('https://e14n.com/api/user/evan/profile', raw=True) {u'displayName': u'Evan Prodromou', u'favorites': {u'totalItems': 7227, u'url': u'https://e14n.com/api/user/evan/favorites'}, u'id': u'acct:evan@e14n.com', u'image': {u'height': 96, u'url': u'https://e14n.com/uploads/evan/2014/9/24/knyf1g_thumb.jpg', u'width': 96}, u'liked': False, u'location': {u'displayName': u'Montreal, Quebec, Canada', u'objectType': u'place'}, u'objectType': u'person', u'preferredUsername': u'evan', u'published': u'2013-02-20T15:34:52Z', u'summary': u'I wanna make it with you. http://payb.tc/evanp', u'updated': u'2014-09-24T02:38:32Z', u'url': u'https://e14n.com/evan'} """ retries = self.retries if retries is None else retries timeout = self.timeout if timeout is None else timeout # check client has been setup if client is None: client = self.setup_oauth_client(endpoint) c = client.client fnc = OAuth1Session(c.client_key, client_secret=c.client_secret, resource_owner_key=c.resource_owner_key, resource_owner_secret=c.resource_owner_secret ) elif client is False: fnc = requests params = {} if params is None else params if data and isinstance(data, dict): data = json.dumps(data) if not raw: url = self._build_url(endpoint) else: url = endpoint headers = headers or {"Content-Type": "application/json"} request = { "headers": headers, "params": params, "timeout": timeout, } request.update(kwargs) if method == "POST": fnc = fnc.post request.update({"data": data}) elif method == "PUT": fnc = fnc.put request.update({"data": data}) elif method == "GET": fnc = fnc.get elif method == "DELETE": fnc = fnc.delete for attempt in range(1 + retries): response = self._requester( fnc=fnc, endpoint=endpoint, raw=raw, **request ) if response.status_code == 200: # huray! return response.json() if response.status_code == 400: # can't do much try: try: data = response.json() error = data["error"] except ValueError: error = response.content if not error: raise IndexError # yesss i know. except IndexError: error = "400 - Bad request." raise PyPumpException(error) if response.ok: return response error = "Request Failed to {url} (response: {data} | status: {status})" error = error.format( url=url, data=response.content, status=response.status_code ) raise PyPumpException(error) def _requester(self, fnc, endpoint, raw=False, **kwargs): if not raw: url = self._build_url(endpoint) else: url = endpoint kwargs["verify"] = self.verify_requests try: response = fnc(url, **kwargs) return response except requests.exceptions.ConnectionError: if (self.verify_requests and self.protocol == "https") or raw: raise else: self.set_http() url = self._build_url(endpoint) self.set_https() raw = True return self._requester(fnc, url, raw, **kwargs) def set_https(self): """ Enforces protocol to be https """ self.protocol = "https" def set_http(self): """ Sets protocol to be http """ self.protocol = "http" ## # OAuth specific stuff ## def oauth_request(self): """ Makes a oauth connection """ # get tokens from server and make a dict of them. self._server_tokens = self.request_token() self.store["oauth-request-token"] = self._server_tokens["token"] self.store["oauth-request-secret"] = self._server_tokens["token_secret"] # now we need the user to authorize me to use their pump.io account result = self.verifier_callback(self.construct_oauth_url()) if result is not None: self.verifier(result) def construct_oauth_url(self): """ Constructs verifier OAuth URL """ response = self._requester(requests.head, "{0}://{1}/".format(self.protocol, self.client.server), allow_redirects=False ) if response.is_redirect: server = response.headers['location'] else: server = response.url path = "oauth/authorize?oauth_token={token}".format( token=self.store["oauth-request-token"] ) return "{server}{path}".format( server=server, path=path ) def verifier(self, verifier): """ Called once verifier has been retrieved. """ self.request_access(verifier) def setup_oauth_client(self, url=None): """ Sets up client for requests to pump """ if url and "://" in url: server, endpoint = self._deconstruct_url(url) else: server = self.client.server if server not in self._server_cache: self._add_client(server) if server == self.client.server: self.oauth = OAuth1( client_key=self.store["client-key"], client_secret=self.store["client-secret"], resource_owner_key=self.store["oauth-access-token"], resource_owner_secret=self.store["oauth-access-secret"], ) return self.oauth else: return OAuth1( client_key=self._server_cache[server].key, client_secret=self._server_cache[server].secret, ) def request_token(self): """ Gets OAuth request token """ client = OAuth1( client_key=self._server_cache[self.client.server].key, client_secret=self._server_cache[self.client.server].secret, callback_uri=self.callback, ) request = {"auth": client} response = self._requester( requests.post, "oauth/request_token", **request ) data = parse.parse_qs(response.text) data = { 'token': data[self.PARAM_TOKEN][0], 'token_secret': data[self.PARAM_TOKEN_SECRET][0] } return data def request_access(self, verifier): """ Get OAuth access token so we can make requests """ client = OAuth1( client_key=self._server_cache[self.client.server].key, client_secret=self._server_cache[self.client.server].secret, resource_owner_key=self.store["oauth-request-token"], resource_owner_secret=self.store["oauth-request-secret"], verifier=verifier, ) request = {"auth": client} response = self._requester( requests.post, "oauth/access_token", **request ) data = parse.parse_qs(response.text) self.store["oauth-access-token"] = data[self.PARAM_TOKEN][0] self.store["oauth-access-secret"] = data[self.PARAM_TOKEN_SECRET][0] self._server_tokens = {} # clean up code. class WebPump(PyPump): """ This is a PyPump class which is aimed at mainly web developers. Allowing you to avoid the callbacks making the oauth portion of PyPump instanciation blocking. After initialisation you will be able to do `PyPump.verifier_url` allowing you to get the url to direct your user to. That method will return None if the oauth handshake was successful and no verifier callback needs to be done. Once you have the verifier instanciate this class again and call the verifier method alike what you do using the PyPump class """ url = None def __init__(self, *args, **kwargs): """ This is exactly the same as PyPump.__init__ apart from verifier_callback is no longer an option for kwargs and if specified will be ignored. """ kwargs["verifier_callback"] = self._callback_verifier super(WebPump, self).__init__(*args, **kwargs) self.url = self.construct_oauth_url() def _callback_verifier(self, url): """ This is used to catch the url and store it at `self.url` """ self.url = url @property def logged_in(self): """ Return boolean if is logged in """ if "oauth-access-token" not in self.store: return False response = self.request("/api/whoami", allow_redirects=False) # It should response with a redirect to our profile if it's logged in if response.status_code != 302: return False # the location should be the profile we have if response.headers["location"] != self.me.links["self"]: return False return True PyPump-0.7/pypump/store.py000066400000000000000000000144341274522221600156630ustar00rootroot00000000000000 # This has been taken from "Waterworks" # Commit ID: dc05a36ed34ab94b657bcadeb70ccc3187227b2d # URL: https://github.com/Aeva/waterworks # # PyPump is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # PyPump is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyPump. If not, see . from __future__ import absolute_import import json import os import re import stat import datetime from pypump.exceptions import ValidationError, StoreException # Regex taken from WTForms EMAIL_REGEX = re.compile(r"^.+@[^.].*\.[a-z]{2,10}$", re.IGNORECASE) def webfinger_validator(webfinger): """ Validates webfinger is correct - should look like user@host.tld """ error = "Invalid webfinger. Should be in format username@host.tld" if not EMAIL_REGEX.match(webfinger): raise ValidationError(error) class AbstractStore(dict): """ This should act like a dictionary. This should be persistant and save upon setting a value. The interface to this object is:: >>> store = AbstractStore.load() >>> store["my-key"] = "my-value" >>> store["my-key"] 'my-value' This must save when "my-value" was set (in __setitem__). There should also be a .save method which should take the entire object and write them out. """ prefix = None def __init__(self, *args, **kwargs): self.__validators = {} return super(AbstractStore, self).__init__(*args, **kwargs) def __prefix_key(self, key): """ This will add the prefix to the key if one exists on the store """ # If there isn't a prefix don't bother if self.prefix is None: return key # Don't prefix key if it already has it if key.startswith(self.prefix + "-"): return key return "{0}-{1}".format(self.prefix, key) def __setitem__(self, key, *args, **kwargs): if key in self.__validators.keys(): self.__validators[key](*args, **kwargs) key = self.__prefix_key(key) super(AbstractStore, self).__setitem__(key, *args, **kwargs) self.save() def __getitem__(self, key, *args, **kwargs): key = self.__prefix_key(key) return super(AbstractStore, self).__getitem__(key, *args, **kwargs) def __contains__(self, key, *args, **kwargs): key = self.__prefix_key(key) return super(AbstractStore, self).__contains__(key, *args, **kwargs) def set_validator(self, key, validator): self.__validators[key] = validator def save(self): """ Save all attributes in store """ raise NotImplementedError("This is a dummy class, abstract") def export(self): """ Exports as dictionary """ data = {} for key, value in self.items(): data[key] = value return data @classmethod def load(cls, webfinger, pypump): """ This create and populate a store object """ raise NotImplementedError("This is a dummy class, abstract") def __str__(self): return str(self.export()) class DummyStore(AbstractStore): """ This doesn't persistantly store any data it just acts like a regular dictionary. This shouldn't be used for anything but testing as nothing will be stored on disk. """ def save(self): pass @classmethod def load(cls, webfinger, pypump): return cls() class JSONStore(AbstractStore): """ Persistant dictionary-like storage Will write out all changes to disk as they're made NB: Will overwrite any changes made to disk not on class. """ def __init__(self, data=None, filename=None, *args, **kwargs): if filename is None: filename = self.get_filename() self.filename = filename if data is None: data = {} super(JSONStore, self).__init__(data, *args, **kwargs) def update(self, *args, **kwargs): return_value = super(JSONStore, self).update(*args, **kwargs) self.save() return return_value def save(self): """ Saves dictionary to disk in JSON format. """ if self.filename is None: raise StoreException("Filename must be set to write store to disk") # We need an atomic way of re-writing the settings, we also need to # prevent only overwriting part of the settings file (see bug #116). # Create a temp file and only then re-name it to the config filename = "{filename}.{date}.tmp".format( filename=self.filename, date=datetime.datetime.utcnow().strftime('%Y-%m-%dT%H_%M_%S.%f') ) # The `open` built-in doesn't allow us to set the mode mode = stat.S_IRUSR | stat.S_IWUSR # 0600 fd = os.open(filename, os.O_WRONLY | os.O_CREAT, mode) fout = os.fdopen(fd, "w") fout.write(json.dumps(self.export())) fout.close() # Now we should remove the old config if os.path.isfile(self.filename): os.remove(self.filename) # Now rename the temp file to the real config file os.rename(filename, self.filename) @classmethod def get_filename(cls): """ Gets filename of store on disk """ config_home = os.environ.get("XDG_CONFIG_HOME", "~/.config") config_home = os.path.expanduser(config_home) base_path = os.path.join(config_home, "PyPump") if not os.path.isdir(base_path): os.makedirs(base_path) return os.path.join(base_path, "credentials.json") @classmethod def load(cls, webfinger, pypump): """ Load JSON from disk into store object """ filename = cls.get_filename() if os.path.isfile(filename): data = open(filename).read() data = json.loads(data) store = cls(data, filename=filename) else: store = cls(filename=filename) store.prefix = webfinger return store PyPump-0.7/setup.py000066400000000000000000000046331274522221600143350ustar00rootroot00000000000000## # Copyright (C) 2013 Jessica T. (Tsyesika) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . ## from sys import version_info try: from setuptools import setup except ImportError: from distutils.core import setup install_requires = [ "requests-oauthlib>=0.3.0", "requests>=2.3.0", "python-dateutil>=2.1", ] tests_require = None if version_info[0] == 2: tests_require = [ "mock", ] setup( name="PyPump", version="0.7", description="Python Pump.io library", long_description=open("README.rst").read(), author="Jessica Tallon", author_email="tfmyz@inboxen.org", scripts=['pypump-shell'], url="https://github.com/xray7224/PyPump", packages=["pypump", "pypump.models"], license="GPLv3+", install_requires=install_requires, tests_require=tests_require, classifiers=[ "Development Status :: 3 - Alpha", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: Microsoft :: Windows", "Operating System :: MacOS :: MacOS X", "Intended Audience :: Developers", "License :: OSI Approved", "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", ], test_suite="tests", ) PyPump-0.7/tests/000077500000000000000000000000001274522221600137575ustar00rootroot00000000000000PyPump-0.7/tests/__init__.py000066400000000000000000000122271274522221600160740ustar00rootroot00000000000000from __future__ import absolute_import import json import os import unittest import six from pypump import WebPump, PyPump, Client, AbstractStore class Response(object): def __init__(self, url, data, params=None, status_code=200): self.url = url self.data = data self.status_code = status_code self.params = params or {} def __getitem__(self, key): return self.json()[key] def json(self): if isinstance(self.data, six.string_types): return json.loads(self.data) return self.data @property def content(self): return self.data class Bucket(object): """ Container for useful test data """ def __init__(self, **data): for key, value in data.items(): setattr(self, key, value) class TestStore(AbstractStore): def save(self): pass @classmethod def load(cls, webfinger, pump): store = cls() store.prefix = webfinger # Set testing data store["client-key"] = "ClientToken" store["client-secret"] = "ClientSecret" store["client-expirey"] = 0 store["oauth-request-token"] = "RequestToken" store["oauth-request-secret"] = "RequestSecret" store["oauth-access-token"] = "AccessToken" store["oauth-access-secret"] = "AccessSecret" store["verifier"] = "AVerifier" # not strictly needed. return store class TestMixin(object): _response = None _unit_testing = True store_class = TestStore def __init__(self, *args, **kwargs): self.__oauth_testing = { 'request': { 'token': 'RequestToken', 'token_secret': 'RequestTokenSecret', }, 'access': { 'token': 'AccessToken', 'token_secret': 'AccessTokenSecret', }, 'verifier': 'AVerifier', } # most of the time we don't want to go through oauth self.client = Client( webfinger="Test@example.com", key="AKey", secret="ASecret", name="PumpTest", type="native" ) new_kwargs = dict( client=self.client, ) self._response = kwargs.pop("response") self._testcase = kwargs.pop("testcase") new_kwargs.update(kwargs) super(TestMixin, self).__init__(*args, **new_kwargs) def get_access(self, *args, **kwargs): """ Get verifier """ return self.store["verifier"] def request_token(self, *args, **kwargs): """ Gets request token and token secret """ return { "token": self.store["oauth-request-token"], "token_secret": self.store["oauth-request-secret"] } def request_access(self, *args, **kwargs): """ Gets access token and token secret """ return { "token": self.store["oauth-access-token"], "token_secret": self.store["oauth-access-secret"] } def set_status_code(self, status_code): if self._response is None: raise Exception("Response must be given before status code is specified") self._response.status_code = int(status_code) def get_status_code(self): return self._response.status_code def _requester(self, *args, **kwargs): """ Instead of requesting to a pump server we'll return the data we've been given """ self._testcase.requests.append(Response( url=kwargs.get("endpoint", None), data=kwargs.get("data", None), params=kwargs.get("params", None) )) return self._response def construct_oauth_url(self): return "https://{server}/oauth/authorize?oauth_token=Atoken".format(server=self.client.server) class TestWebPump(TestMixin, WebPump): pass class TestPump(TestMixin, PyPump): def __init__(self, *args, **kwargs): kwargs.setdefault('verifier_callback', self._callback) return super(TestPump, self).__init__(*args, **kwargs) def _callback(self, url): return 'a verifier' class PyPumpTest(unittest.TestCase): """ This is the base test class for PyPump. This will provide a testing PyPump class which allows you to easily test code in PyPump. It easily allows you to specify pump server return values. """ def setUp(self): """ This will setup everything needed to test PyPump """ # response from server, any string will be treated as a json string self.response = Response(url=None, data={}) # These will be set when a request is made self.requests = [] # Setup the bucket test_directory = os.path.abspath(os.path.dirname(__file__)) self.bucket = Bucket( path_to_png=os.path.join(test_directory, "bucket", "test_image.png") ) # make the pump object for testing. self.pump = TestPump(response=self.response, testcase=self) self.webpump = TestWebPump(response=self.response, testcase=self) @property def request(self): """ Returns the last (cronologically) request made """ return self.requests[-1] PyPump-0.7/tests/activity_test.py000066400000000000000000000135071274522221600172320ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import absolute_import from dateutil.parser import parse import six from pypump.models import PumpObject from pypump.models.activity import Activity, Application from tests import PyPumpTest class ActivityTest(PyPumpTest): def setUp(self): super(ActivityTest, self).setUp() self.response.data = { "objectType": "activity", "to": [{ "objectType": "person", "id": "acct:testuser@example.com", }], "cc": [{ "objectType": "person", "id": "acct:testuser3@example.com", }], "verb": "post", "generator": { "objectType": "application", "id": "testapp", }, "object": { "objectType": "test", "content": "testing testing", "id": "testid1", }, "actor": { "objectType": "person", "id": "acct:testuser2@example.com", }, "updated": "2013-12-24T16:58:42Z", "links": { "self": { "href": "https://example.com/api/activity/abc", }, }, "url": "https://example.com/testuser2/activity/xyz", "published": "2013-12-24T16:58:42Z", "received": "2013-12-24T16:58:42Z", "content": "testuser2@example.com posted a test", "id": "https://example.com/api/activity/abc", } self.activity = Activity(pypump=self.pump).unserialize(self.response.data) def test_activity(self): # instance is Activity self.assertTrue(isinstance(self.activity, Activity)) # object to string self.assertEqual(self.activity.__str__(), self.activity._striptags(self.activity.content)) self.activity.content = u'Test användarson posted test' if six.PY3: self.assertEqual(self.activity.__str__(), self.activity.content) else: self.assertEqual(self.activity.__str__(), self.activity.content.encode('utf-8')) def test_activity_attr_verb(self): self.assertTrue(hasattr(self.activity, 'verb')) self.assertEqual(self.activity.verb, self.response["verb"]) def test_activity_attr_generator(self): self.assertTrue(hasattr(self.activity, 'generator')) self.assertTrue(isinstance(self.activity.generator, Application)) def test_activity_attr_obj(self): self.assertTrue(hasattr(self.activity, 'obj')) self.assertTrue(isinstance(self.activity.obj, PumpObject)) def test_activity_attr_actor(self): self.assertTrue(hasattr(self.activity, 'actor')) self.assertTrue(isinstance(self.activity.actor, type(self.pump.Person()))) def test_activity_attr_updated(self): self.assertTrue(hasattr(self.activity, 'updated')) self.assertTrue(self.activity.updated, parse(self.response["updated"])) def test_activity_attr_links(self): self.assertTrue(hasattr(self.activity, 'links')) self.assertTrue(self.activity.links["self"], self.response["links"]["self"]["href"]) def test_activity_attr_url(self): self.assertTrue(hasattr(self.activity, 'url')) self.assertEqual(self.activity.url, self.response["url"]) def test_activity_attr_published(self): self.assertTrue(hasattr(self.activity, 'published')) self.assertTrue(self.activity.published, parse(self.response["published"])) def test_activity_attr_received(self): self.assertTrue(hasattr(self.activity, 'received')) self.assertTrue(self.activity.received, parse(self.response["received"])) def test_activity_attr_content(self): self.assertTrue(hasattr(self.activity, 'content')) self.assertEqual(self.activity.content, self.response["content"]) def test_activity_attr_id(self): self.assertTrue(hasattr(self.activity, 'id')) self.assertEqual(self.activity.id, self.response["id"]) def test_deleted_image(self): """ Activity with deleted image should have image obj with 'deleted' attribute set""" # copy default response and replace object with a deleted image for this test data = self.response.data.copy() data['object'] = { "objectType": "image", "deleted": "2013-12-24T16:58:22", "id": "https://example.com/api/image/uuid", "published": "2013-12-24T16:55:22", "updated": "2013-12-24T16:58:22", "author": { "objectType": "person", "id": "acct:testuser@example.com", }, } activity = Activity(pypump=self.pump).unserialize(data) self.assertTrue(isinstance(activity.obj, type(self.pump.Image()))) self.assertEqual(activity.obj.deleted, parse(data['object']['deleted'])) def test_deleted_custom_object(self): """ Activity with deleted test object should have test obj with 'deleted' attribute set """ # copy default response and replace object with a deleted object for this test data = self.response.data.copy() data['object'] = { "objectType": "test", "deleted": "2013-12-24T16:58:22", "id": "https://example.com/api/test/uuid", "published": "2013-12-24T16:55:22", "updated": "2013-12-24T16:58:22", "author": { "objectType": "person", "id": "acct:testuser@example.com", }, } activity = Activity(pypump=self.pump).unserialize(data) self.assertTrue(isinstance(activity.obj, PumpObject)) self.assertEqual(activity.obj.deleted, parse(data['object']['deleted'])) PyPump-0.7/tests/bucket/000077500000000000000000000000001274522221600152345ustar00rootroot00000000000000PyPump-0.7/tests/bucket/licences.txt000066400000000000000000000003301274522221600175560ustar00rootroot00000000000000The licences for this file are: test_image.png: Licence: CC0 Source: https://gitorious.org/mediagoblin/mediagoblin/source/eebd8fe3edb3cce4675f58eb02fe1d96229af0d5:mediagoblin/tests/test_submission/good.png PyPump-0.7/tests/bucket/test_image.png000066400000000000000000001426461274522221600201000ustar00rootroot00000000000000PNG  IHDR}$sRGBbKGD pHYs  tIME`\)tEXtCommentChristopher Allan Webber was heres. IDATxy%Wu3ޞݒ54I@A@fB̆-!E֒B<l~쩽ZH&{~#&HIAOhA5*k"T ;xHx0BM6Xk]Fakvu˝كl8' 3g"KDJ( VhvAt6W?g 8 Z<$/4l 2Z<&&=` E~ɮ㩕3>Wdl"C1qDĆ A Z6%5qz!`YO.??߮e{~vソӊn&l `)@c@sTPRCF m;~0;9o_iY!J"kɘti=<-E{v9dg""D"y褫 v"JFi`Va.kzR A5 04S/+QT~8HWe/vL3$5L\+l֙豨'QY>1NUzoC^HE6sƶӉkt7"휪^=@FEL(ȭfPdwmPՂN薎Av~9syc7-dx-:Ȥ)/uVgEjkWxג8iV3q"rjU$z!s A03ַ?s&jlQڽTs[VO5k0`{$hԌXZ xGnYmذ;3mޏ6Cg:8y6 jS]WܝAcLv`K.{+n=9Z[&*BEET 7܊j'8rƁq{?|[K}'涿 _{pkKҒ2YE9LQ0h3 !zd[v aּncy6i$ǦZJ9( ڌ]Q1h^ UB H2}HۈYGMiV}\WS"cL湪_8 =J%Rh' 4 P#2۸V3$Z=ѤV#f(J]E^o (dʼnf>g7eYou|E+_ c4o)Fec3l60A\73㢭{KDC@a(uau }Vg"˙Y3_DA(d~ar&;w9c-5Za"a"b0J U"!)r #rޫ P(`Sլ[o#~g J>N]T,U2@֞<G^⻂rи_dExۦ. D[VGg]O f"#Cd(bc 7M8g)rF$B\$p"(`wt]s?czǿ'"و&]pW-(rg0sNdBJw m{BgX쬢 /!H`QehDI6QkFZlDl 9gac EqXZ+0Y88K P$!)B*D $ÔN, }n"!Fhv"/T{cw/NOl6z_|wEs3Woqs/ފWS5{M[. _8Rs;ts0DDDXUf($p`BP6MԞj)޿i|SkŽg6<;ٶjtheU h9jf&fƲ HEls{ M]l (6N 4zМ]hOAqd0 D"fab&cZfl&c uւ lX 99I9r9 c,*Dv=W \}OO&E"{-(]z?i5\9磟?r$5g&_E /*{Ԛ{v<펩{au5'ܖ)CD3D4xb"$P-(~{׼9ϼnOѺ|+ֻ2\yvŧ}2+{*pksO횽ֵ_f$DHE0BPBxC|4(/ NCc B(]_>: _vm.+;rQ?TE"ՙh$/*J*PhUU(Z6'k@XD@" `&U%-N*JyS C: 4sE{ͳGZ!LDQQQaQyNEQ=y1칡l+ژ_ֈxzW\~?Z{_m'՝OM.ŮvRR}q+_HfW7 ~؈SkbVU "d!"Tɫ !U%Q@BNNJj>..ämJ{"j9Q&hJW$"ভZ[Z4bQDCQ"Y@DrCligEP Ek^(yi^gYysTߣtI'/Z*ȲY{Sg]5ќiEweOSokv S{n7hj-vHm񞷆k\ъ}D+ 'Y_~DS Y Ф4ij(L (EVPRO)iP%fxiecƣYـC|$ % 232331 B ,3H y(h4(xf0U"QEQPQ1h)39a%"c"c|P &5!Ib$ic'a[-FTkwۗVOܙX]3΢a#C5~HjU9s'C=wG.94?qno;ֶ5ٻQZt'V޾=DS)sk;:.rڨ%$P#niw5Qd}tK55+CՑiDd3\i{3B DeB$A8 EN"ZF|@-XQՑ&KձB`'_x CB,Gz<)rSm K@l??̋_K?<Ʒr7|Hk6.~GI+GVzB<5l<%vz:Gxu0;5ZCa1 *C}8p dV."\ rdWlEhCD_-(F[:xo I ~Df 6ADUK2B今@T *5f *A{O󌙙CT}$!.O&ͩC/yϴuO!;yfS^o$Q/L{: Ϸ/O[iG4QUT(CȀ89kIH&+YB\n"LD$c$5T3 b3@BP\1D c R~V6Z[횘zw췒nn~Y< IWYb z*#U~vXvZpͨmHW]jsPAɋf}m\>ՉΩZf.\9vx2џz>iZO90T4PiQ B᪪E9w98ePu1æ HT\g]2 SeQHW@ZF*SUWfT*`aJl0զkL'ǝ& ,ڶ՚Ě#v\13M56LU7:}%lDQ ǍD4ކkcɺ D}܄mdnUv[9⽪UgITGN}Tق,aİ~i 3ijI5a{1>%j=9+MU+RjJ "VJ (4THr1etjpy>t{.r5>h0#7|Lڿvemk'<μx"~Nw*ӊfAbBe8M1kzIk@c0(!DvOUqSrmMe5H"x(Me%W@ɑ3Ld+a*݆@8t5 Uu<jP sdٴÊ(T$.+jF ՠL;j".?l4l֧&k'V֗'Ǟ }2vpʲUV]މӰkum1E %-tP H4#cl/ β.<SDM;mf*̧<Cɩ֥rlUМ Ǹ{ E#1G&̖SͣHnd}bjykin xBP!R"d&iТռJ$ͩ |dG  W|qHP֚Gn8|?)KY {+ 5~⚫<}k1w:.lotA^C( ld%/RY6[lj ($hnSĉf3[ְ(,eAVgtydC_ہEjҬ BݴN4ӐZhV}N'64H YHĖ2&㩵C}/bɭo=4EVot8WHz U<#6}K;f?xϡ#&.-ԇT;'ѹp5~ox53/[?A5l.pbA`:s>| .vb;55 8XRB DUe k8,+ Ձ. JU ҏ'$nH6)\?fۍY8pٞZ?jY <9oXK&r/rE9NwMnܷoa3ajCUX(%LCZ>H(57 dV/bt둛!e}SN5vls""96 kGD %bAL R6X <_|LRv%ШOvjVD0D*Q\kjCkM<L߻풯q~OlԋȍUƤ (+y_۴?ya 6NOzkA#d٣`lMdP:ed~ˬ)H5l jU,5f)С# QL}!psc \씍a[U"  I^($^ Ul2L  -pDdpf=w1)`Ӵmﳨ({O*eb1l1̆Pgb3'- IDAT;v?Ix?uyRvy2@QI*{?>ws:1"mʺ2#F)2LN"=-$ k2T.E29 <$hUT  0TCĄ"{ xW ABCмEDl2Dd.(ra:bIh(&qdmX P=ÀZb@U=()5`uN17&[~CK_zqr d'm'i*Qv>]ZAs(_iU5w[- zK`7 :(,lH֨s^!!OR*ldBElly]l .!Ĩ=^O*xD-D) u:~ЮvN 2A6& a3\/4(D&4)oMj˱a'QK8Vn6p$!)S.@5L0 GDDp≝rbQ}(f 7)^25s0&7a}eqo pq*zm[&W6gz3@@L3ͣ"IJjP5*j ɂȠba0Os|wlPzH` 1V#i1,YȧF D2>'?z>t@N6խS>me>EG{GBDHI `\4IZgYycƢ&&e#*0ŨxCQA:G;g<_ `?6&RuzxlWc)~ /މ _fKO냮ngE" QE_B(""* >>2?Õ7\;0ߨ!4niԙZgHEHk#T5`M溧:h9&x]V9Vb8[ck.6iB65Rm:Tc- fQU-lHC4 n0[1ƪ*qY#iϹc׿?*C*KDx},4xwNwn(>9(r, 0jTET`5Ł@in7u˫󞧭tlL욽_V{W_seb|ӛGM~Gd:_e_8ǵN:v|B'/?sw3w˜ml+F|AbE,9Ö\PBS*A`rY6dED}a,g*1JD>A΃xWmAets׶ѓOF~(;üQ\\%(@/?kݷ+!%5"I3$N ##ߞlZH0 ~w{ݵvyJ7]^8+o$>-+/K$j2Jo3"3sQ`{^Exf=?z(I"ksSҌ , 211%Vj,8Mh0f~JxIjY<0Ӊlb5 ̄(ԣfl{,Zzj"u&yGZtxLoʍu'Q ՜ tiol$S?{اAQvGϻ@R'5\֎B ŰC7iZtOn_v/хx(Uɤ@0[@>+{ۑ=cP}%ǣLFZbC;#j1$I q^CK~z$Mӓ{]0pNϟ|ïw[kvM}W y\~b+wFpLo!qS'E"^H)U"a6ƚPLdÜ?tڦ, #>ҟ>"(Ckꎟz/zu-J;.V5.v @p}&뫢iwUi?>mo~'ϰ. `^WoJ}M==E7Uʪ,O B3g^n|Al,WG{صk_xny_gG/RUnO4.|`G_E4MV'ժX#4ZN(!c.8"klKɓxzh"ʧWMğ+BWj݂[o=i5"6bC 뽒J  ə?9rJFDq70&M2ld7AU}OGY%X8r̅;CX1l~1hw/(vmZuKC]Úd({m"^ݻ8p`}򕯜ɲlƤfXg^uEPc abkqQ"֒\b}A*0Q483n@ZɻMV1E浯}'>__1i5hx-1@N1iJUc4]ʊ1&FT,--9|UNh\.Æmpq|yT=j0 ;t7g}#<߱c;O>} n@9.5" *@#JeKfQ hH9be#*ABB %8/oye]qE]y饗>-oy6nޞTʝwv1i]@KHyj&q P6>.;r|E ^TI5}v4j`GeYvsɬ}|ףxk_۲eK!"<;;(ү|+1Q8fQٽfI$E1`hQ&"̓$~kkQݻ__?xž*"@M7ݴlNx13_=&7Wmtv[\gC $!()&diNyU x2qթ%9iIӴVvs/q7ks}cge"C̜TMF F֐ƒ@Ơ`"62b k zQVHk&CƆ޽{ }Cskx4.l2I_r%]vr?<&7\}D YS4@$pYYI Į!F<2;vyͲ,\r⧧?~闺R1k "ݶm[(;XfW5IQ{#y!@QXuP%k*PTi87Eg>U:}mMV+---xIŦW#ef}^wTr阴߼?; )6E{`H6F\SB+DGͳgZVm. |;/wEA#~Ru(S%GV SSSo~Smo{[mTU9A(tr}ZHPD|k0tT%!}~jn&^םgt_6\kQE˪wΘM}98DJl1]6 AaHA.@ Z<_<χy#_Ƿxwͅz׼f2s7MJ 81$2=3Y8gHE AY!Hӕ *!O @! G?z蝷vl}̄!"DQwO_Ub+4?mE tPteyN{/S`0!3u"f<1g>7-/`-2x1"X釛 /|ax~g?6 ox^j|dt*&t,3{tr)YB,Gy4נE "sK/=7pOOk;v,c 67>{UQ]ZZ?ٖ*\sE3oUuyyUoueg4# Yl!9`acpaRr+&8*86q')Va    Y0X3=M/w[ 3ު~{y}{=yOӶ-x?O~1tV7Gt|sŗ^G,d%Dҁ4uY*1h=SW<㧫2ə3geW6|T h\:඄hd1T7' oX)ϋ-D)/2k& c ~q?ݼon89DЩC=sy1É#gQ2tiUUiL]5ʥ[٬:UG@QIU9lUuXV _4˝䅃uTEl`Mgmz{<2r}e;/ ޼?Yt2ٛG,Z"#Gr*XZjR,fNa(X,=ﬞά:~~ ?Nce4n(D=ʾGD0`(Mf|:ںM <+{+MDt{{AԐIP+oӎD5tm#zO"lViV\N`IELK&ǁ;UM1FSU-y4q@cN?eAcnZRO$- ҫ YiAYOsg~|{qjMFpŕ_]oh4M9WVVQ0ƀo;W>諪r܍㼮k Mjtdr󥃎*ۍ-+RfƘ$FdLS&&v1m)IMӰHLBa7{cG ?/T-e !UUd2(PsRئi84 !mo=y#|p{K/ŽbiY>UXka,$蜋жi>Q~vGWOxplm d=-D1Z.AIT2&&Ţeg-:v|OkO0|ucQy>Gp۶4ϑBG1FsB) :R􂶖Kڞ,""ϋ^CaD+;)G]Ŕ2SgGDD{=b)[h>˝ άZ9jLC=ŏ``SԼGߐ7䥭DtF] 10E,eQB`,!E%6 g] ƐᛈaknބUf|i0)Μ{"Iͮ$W,+_t/}ߩ?e0xj}'G8!@1/NV3YjRwQ93' ѶMaQtKlԵiXScJ'?ܯN>C[cR18m@fyf)jL4,7ݏn:5JpX 嘒|6gII1jL 1$IyLL]eZkb妅5>uc\Z usoܘSLΐe.&kqCc>k+6׾` ^N]?iRMI 0YµΛ'S'4n$,[rcz5DZ5ry!1FHB0!F`)~ti$m\RQ"Jaui-چ@],lz~/r.5hm!u.F CD'Wwt#^ګRg,/YdKQXޞ2KLɅǔ.OI"ٿ?xl뫱dݍWQjѝM b^6CsqdKsz_EAwj^{l)뫝':Wp9yNYHXt:^<ϨSv()9GywG~|1oB(bJc`Q͒$VUU5H"B"0p[ѓ!H"Y)]v}ãIބdE-v{Z/ֹ۾Ĩ67YfEU,DR8dap:,n|/ԧ#r,YT />N˧u|ɃnEV;Ѭ(( >p.l3g`SS;kN+11Q΋$1JcJ$)%)@U$(EMRPrRA`(%$0m'E2pl26ƓcBj9SkY<PBT=MU 6*A*N2k, d2 4QB?/M W* zQVuC. g` @Glq鐷Ҥ8i@I-wJd8}<318, RғKHJŴwpr@=V USQ,X$QQN8IbjbE)ZP]6iQ"43hg4muZdj/jь&,j$ 982b[S&$!UIDC )I|Z.2S*O!jۈy'@;Xj|sE7Gr,'[XC &!ff-|3ݚکeiʇe,,#k 1Qt2&w~xts̉bӟ}+b+֪3A)6QsIg8Va4M!))D ibH ""!&>ij!ɆM㛲 mKmp:x_RLiVզn|l\Q 1/i<ߺ6Fw\x-~,{0q$`J16D 101Fk/7ӔeasVYuĜwl-YdYFbbU:}ǹ5S+vY=arXyK4HZnS"3chzoQrE˳GgBhnrޚ}L7 c!w]6v0 EfJ9L$ׇgO=6t}󍋱5ϼW5' >6,;GX6`W "Έ 9u4 GD +_o6c>wfu3]LP@YXjXVWVri'N_tB Ek8=8WfdSvչ,Ę֊q]gTi#zҐA=$4| 51eY/Vɰ5mM4sɋo^6m[SXãӝtwS=мai$ŭ^gl_xo`,݂~V;>w_D|! |>r_M}OѧX9w[['OjW|nlzѰi>W3h@vTÎ_!?:5"ipˎUBTDiVz<ͨ`L,v-sưa&&CYjٵd l"ej2'6OOOtl5wg,_ϼ;Ư;?{>θv QvƝ_wme?߿KbN7SR,(+sZЛW1K ;Eo~GzXfY4/}ĥ{m{׾7/[ }gWy|!A{eՀ{/;zcQSX5d@$Iɲ3d "*0tZ:`PC؀ ȹy5I 0YK LɐMNg>vǏyc~SO_曾,_];(KvKv`3} G|}?}o~WswwC1Y"bno{Wɥ*OO ˞=7/h_}U/qܝgS*;oO~~ ?]o o73Iju'/ncU|ٷP/]ŒvImS(T/ fʹuͩQh^'9sF_^_^T,,7Pz/3W1|Lg/~'Ա;'oz!N=kf*%.r.7ȝ,wYt]df|~|t Ƴoҩͳʍ( >JSTʹjS;/O}}эW_7Fw\ _=aӿrߓW>%P)3hQtr 5~&~|Kָؼ)OВyڃtԓ&phHBjD2Qn ldKACP&Db'5lwFLgcVF+ȲL2(RSJXCQj3SdDȬmADMݒg97u|#M6ZELjMYR;qYU#cL)% 1jQImS-=V-ewֳ)kSwxpm0Dm_g~ʏ7Du&{x4^EN6Lc 4ݯ]h+siJ/+}3@Vҙ UZh#X ,)g=R+ބB:Kq88Ƽ"[t Zkh4ԕݒCf3 *E )rG$)`:qWaw{Ţ) +2c8t{aLGoL*!o4U|U!yBJ*B*nfitDct;NkM.zmzLJ+|Ie+iZGbm!ǔ2$V $NF RʒjCT9C̅b!&wJDM6~GcTĜ2R"6Fb>nڴWTɜ%WXtbva&C2szd-c:Pt<$ RĞaUjNZd]t.BѦ9DN谿dLX(3{kMֈ1,l֟U".yj</Zu6۽ɕ?-6~c~r;3cp~`˃:5d8 ̰G=nGj U"ojTR&<7eNCrz 1"2Elz债ݒ W1@R9!) B" Tk@-T00km3Ǧm(4ؑ mb-mj%DO*"*%IcbQa&"g-k Ij̦slNٌf9d:0fR %"Ӥ0oM6梱(ḥ"v'c_ujC"L߼>ucqs놿q;8UQD1QHŪ|⓽{bܕ2o]_~c< m zG\G1؁Ê9^oj:HM2^q%p6Ԧ[`cֻ^IwNSᰜdK f\\)Mu`1eoRT!ٺ]EԀD̪RqI W&sN;6/,%7TX8Ž,\6_ub]Rhא[a6 @ IVbs]dg3)5::#'\8'ILIn *p"Ž wj8zFftzM{e7m P7֔(HPq)tϠ\˼8EyqMB&4V.%PQ5 ˚wIt}ztƩ m85Ea:ІUIa)vȩ(1JT/t e":Xd(yEt+2i"7d2!#!&FvP=ud'4mY~w|8;˖ϙWx3ϞÆ$sӚ2:#I\+ zXYh.=-NJ3KNh+ +$^# J)#=rUCAA`"fҎq_oe>Lm7ڒ$5qDYJkj}iEu7Y 5v"B3Sv˼HD ŽהqO@!!㕢;X<3N-+-LF|Gfo3o>S,ך^-}ʂ0^el^ MfB?Ƥ(bQ9qD#c68wǝ8Q؄[?Fs8㈚h9 jm;P,hY `* F@"1 Tx^{9xĘԌ4Sdˆ %LUO["SCh1"3LN2VH{L"^DѦQ[lwgzvY慯k52Gsg_1!po?ZW)#2uPd2$$IsDڴ̸MR67bNy3'C]_Yi5氤HW3H +gPIFlr PcX!# 2Ȓ%)w+\!AlƩkuW5:@"}WNΠZ,۸ku8ǃ:69Y1J^JѹN3**^?N ifpVҦB>,wu֛fJyB((ASBn٨_hsN&3Q/#|S=z//&cln,DIynrjòYx~E*Fn+z'X=ʊlݼM"JS)q͉X)!X(D cuMqv-籒S,FrYu34[AW̃^ǖَձI!Q?&[@ݩz'fsv5ڞ<7}(@4LQNe7FEOM6B1PmF[H;Ksg3zO"6,.*o<ҷ9 9Ub 1vDamF$}WlU!hr%|Gg۬? yǍ͛E3xi-5E C@ B+]LbtL3EMiC%j_[O+ky\:s<īeW掺}׶w.vKխͤ8ncR}{` T &۵BIx Fda?5zz5w({G#.? 5g}+wܛ1}Lyg08%aPVhA6^F+HȖ ziFPگ1j`M([fm jxMmZ4-=rnwbX&al(l2؄֭/y2i[/hC&ƠjڠL߽rj{ 7Kg3[ (*-/ M4xۓU@#@D=i͏??9J4;]9tr ۓtT 8g4FsI1bؘ([rQE>Z,D27mLpKkѫqOaZq3q0} !QJcCLxk ̾@)w USfR[JތQѱԺ)&j D@Xą͘TIb 6 מX?:+XeEM)A(ˑDjck e qQ5LGrtQC90|FL+>*#BMC6{ś+D@;V IT-+i"b (Ȳe@L̬2x RwOUmy>suΆRLEXU M15ȵdlQQN%tI̪,UJ%J%2Xe A8!qРA@@ p2-d$U׭:9osoݔRd{c7c٤sry't ~vOj>ęHN+ P*Pi80ty?{?ˆM(AA%4Aα9<Ę5Gż8P&biwWU K^޺t{}\~݈d$-7k￰+ |oY  ,鞄B'чb S[~߯Z&ecME7$&W+=a} Wa lW,86}H֬Ill-wӃG_||ev{\#Ċ$[U9psR{j+\B>xmd@^jG#iu'Fm64!>A{z׷pXnɱ>$$TLÌ7D)&F/6\\,~nncn* О[1~ҏ^*0%6HjH1PDrw 2@6jKA?VcE4ƙʞDrܧdZ,*R*O1s¦%8ƢʝD( % W| Dke\ e~8tfTAQ!ItzX}omSٙ޻=XHyG?AWߦ?]~j",ND347@i7z[-xapK=s<{^Ίr7⵳3/Z֙0,v#`@e7r@IN &Kv|kl *D |P8`GCxr]p>?;hO>XBRtȄ0R4PqVYq+i_ƝM#E #!JrR)EwXpwónkeM6aƟOGi5aon_xx(r(SHAlAQeyqtt3Qw{VFgk}CgBLsEcBqxs eDRۿs0gX5h:'Ȝ䅀jlaQ"]4 @zm G=qh :텃IXUk8Arb2  z_)3fJrצ׾_Nϧ!@7 ğEMQ.,._zuv{ڍ A"IJqYkZkhJ 3z}>.9%awam4>kulJLsyΡ!0b0 {8&+*"Alj)B Gڳk\6yHUDVH[}U2#po7@6y.ӵ*TjYeG˗d vT 1L|xX[Wǧy8 6m|j2hg t`~z0?cotM4I#dPḲ.+C"e o]Ͻ~_Àof~{YG/h]d Ua@JngI=J2( L>yA+t30AU֠)ٔCv8Y+4m)5^k-T3-hn#F8&:F תLڗ2nC4'LH'e@)o_g7?{9K}vw85i[zǁkŞN-AZY%+R ["P#YnRv3}G OKpX+\țٲu'Y MM d_/"#{(&sU"F E;7խS<m\a9.+¾~c|֯^=_WN9I]<YRՃFw7H() }sXq $wg/x14opK }68U7UVq96bK]W+ iÆw [V0/h5 R!-]kkb*f-PgTYқb? r\sqlJ͵z6I\$S񽌋9_Wtm4:|^/;6hpʮ{| ?ц,v{-jvƲ0nvr2 prC{!BtWW=B1+K+͵>QA{J@{w{A 5vZ'MCaEo $%sK)]ے!P܍ƂJbl 73Jeuu@Z) E)^3_(mlUf('i'9Dm.I34|l9m89LlTXs(M0N1mlOcɈZQ{'H2Ӹ-&vUR|A@K:~OO~ɳݓÑuT^ff.r[8yk *`P =Ͻ{GL-p?2=G L/JGnG')]+ f Mem}M~ms * +cd`igKU_: zR ԝH3ArD2 :vd7J{{99,VřRP;8;;j [xQԶz!XJKnML *8ȸg3`-5__}pձp@ln:쳏 ~Knî e+iXWO׌v$nVa7p @6Œ En^@kޮ em-:DZ-gi0Xb@6ThM lc|ǡ]rwodnu:,드E{t ٤!}CBmzhzɩ=2hbk}clOwomҨ\H!3ZSfy{b7G{ln; )2k񹇏;}wMg(eCF| I6+<؊aG!f"$*kV@ m/w7PRAUYv2EP4J2뀠CAgOUݔZVr'@*ȃs k#Kܷ!o.mUn(Ǜ1AKBݾsް}CV!RMMbF:HUʐj5 Y;aqlW&He]V@-5(i}3mRV n$<fmM"6aMb/։ v>s9+^ޱ‰B<7 g/o>7z ]Pp s@‡Ãsy/yY 0/m{#;SoHZ zH"$E7$yX7kS?HΧ|&uXЭڋMJ1M\Hp~A4rt !·U Fz2JCqʈYQ2Pצ+ )G07X0 a dfzjiJ^r:~ìPUĜVՖH3~ m$Wel .F<7l ֙b*b(  %.HŌ%2ؕUTŇnLnև=e@@}Ydoz:o h[a7Ft~8 'fTiS@up)YL sEQ>n&d!TifwHXě>ceNb! qf  ՌYAԣhGaܓmoB $;YyJnGl̀9wdo!J&R1]MhBL<@$ rlJIBFA߯j芻0ru81ˊ(-W(Srv仟Gx@܂K(U{V 68b,! #mW)݅b4pњ!q6 9/ |^_;3U-+@ eL'h@DxN-AD3\MiP15 ȊFߧ0FJjQNL@7]?;-7T;X s|k6yv@fri.Ob41:dKF ƾ_B)2"v QVbΰqpCRL 3T)o8NA ÈqA7n!߲ꄗ+b?1»@03In)F032[`;ɌAعiP!X=,dU,Dc3q,8ì8|` )PR ҌBq"D WܵEzK 07Xj=F+0LAFVl. Z-.Q1q:m4|gy>C7nFe$&薍i >uuզıupb?> OK)aG;[0] zk&85Y9 Qե{)[U`X" 6HP-tZk90 FYf8d@(j7oZʰ;P#5͊^AȡC9mOWlBxn1L҄V4UR'1 [ʽHZ2yq 8x+ae洞.PAFf4`~ 3AjEc<|FD]-ffr:rJ"k&LهI%^̃v s &aFXw$A=fV sqbapUp0'L*ӞDmh:=l EĬ+`U[k@ <3V@N nH@6~ߔ [&7'KZt/կ[3Y*u&uj+ l#]pNF)}AԃMn+N-J=""L9]#郈BxfN8j'&?G/CؑIlEѩ1 /\Lܶ;ZAL1"m3a.M=ƃi_Jye]#!0M1!ּ^_~ʴLMI|ƿ}Orn+wh?ö1fb}!;b=4,!e6(R$x5 dSFwL^0LdǥRX֊Et̀:./KRhsB%Q ? p IdN;W[%2`"J!+Ě~#aQmk/>_;|gF !I:*/zhȮ > Ԗa@ ̻JyU)nO[J2Gous, iD"$#D7k9LBo_YD$dT@rrNj\@[Ef$FTՁX!;(*xV ^ v ݒ*0\]7n0"OxqM 5 8M%8.CntlEX1z֕ dtͰ+cga a% e87Nkf޷=ٺcٰvY锳 H VzpPa$+0.R 4TuxN 7qY=[%FfFSR_|/vNjrΘMSkf ݇;2/ϻ8x1b!)}kU7Đȧ@)eJKuغu{yrk6WaئH8k]oRjdE`F" M+Id&qe)7RaP'N-d!qh2гH(ȩ$]D*&䥀zt<]l;}CW 8NpWvߵo_ߎټ2`l DX5|H"D^!>^6%ZVw$3b .%IPxmᙽ 3 v N34bbrC*99N#݊܌E !DF+(,](J:AaV5tN H;DlICS ոd. 7bnmmXy veXթanyRD#m;?C^RExc7僩koS‹[~p8xS+ZhX j=ε$tlD?v{8g(ݲ6{jF7ﷀcT a]A4 BَN~oJ@uXTТ|@fE3>V#敵HGDSԚ̊ݞ1W") !HJ"C8 :nGXNVx#9sW&;]i#BxQMClB胚. $F?[~9aݱı㑥_nve[оuܻ^ 7tgaafJ$e m䀪&`8dp l#C;+h1PP ..KeNNVu*IK"!fI3g=L=^EFwϜw\7ݤD'4]e~ݼ9^.qʴΠu&bٙL痃H"a^\ם6E`T{T I7K3%CF8 L]IZ4D ([f&q]+c4nz8!wӓRj e$H @KfkI.w!.\\F7MF N 6D%3WD]Bm] !3`؏|Y"Ը^dϲ-0ku 2 .3FHPfAL%$ZТ4 @#j zYsB~C_3`~Ό++/B:Su/]}]M6¨-ܠ[@ë0(yyɊuaQb5#|BjWZSǹYkhI))VtL )Zϰc7vjyɺiM+ڨL=j6gԜOy!f}{SRjR'l} [QߕCƏ9jY^ɮYO7ƿ~_0޾3 W""tu1J L H'@:1,HEw2i_QgHX4d4c6Xf261vPW+ BیhFUhjwZWjSp eH7gpć~5 /Fg? ."هTMImr7dV{Ƽ-#n֍ XFjGnm#uG3ύk5%ۣ%TCveZܪ^,D&LGBg9ծu*f]'0ljެmBS%u{kEOg3כ6O%-Xia!ٺ6 kEB横ـӤ&S 2wsKo Q?ï?i0v7=}K|͖n9Wv~&8CMdWfzag[? mn& 12Qiok<AZSύjֺ=Ḭ<к gmxpSWQ[dKPDyr|}NBt W`TMldSjƸg^ߋuU*!Xhɥ\֪VfdnØ:EދfGgӣK9x}oOZg/wۑǡ_kܖU̳`6wG5D$؉]̒r V^%eLJ^ĩe)z9;:D;-kҵύrQڎe hD$@EUUhMțTJJ,cOj[i^ 4P0Ar7K帍/53lvtv~N%E.>ibh?~xone?'??5^I̴_^pzQ_`5`]"!uk?󒺓]󚺼vlHxjRKV(fy&FZc&HC τFfjIE"봣EpX7GF@U*&sqӺ[Sawv3:_.``&֋ݗ/a ]KT>3 Ǒh0yT[Ra(Z;W{w__\|~ QxyH~ ?tU?sVF#ah8g8#vASy#6J6l"@"$ f&Vn@viBĚa8(lE!ȼh^β;xB F}\hS{@vud:71(#&R`֨Xx]^\l7m  e^ZMj 3yiDz+#Zreն<0_ŗ7'=Ɵ?0a>'Zۭ\Y™N:Qgrs^_|s#&t{H,ң'goJs%, @#+cSE8LT@RJ%M3Y!zRF=2:\@N`(Q0@Z\ۑ%YrOj&RkM,{ofэ n ]TP)-d!j eqhƌ "Ah4 kfB!&-ɐ(sAh ]]{53̏]h X:/^_{Y/Q0FUS.lѪ;VOJ)]je)X! #:bYe;龭{lv">v[>o.o ~g[DvAƴ[bxٍz@JIРH,d2Mm3?B$ ({,Q! @ZrpRHR|YT)@"@Sԋ `$AT.gĽV5*K% Y8GC 1BD UcFtښ0gCaUdIIaR"C5ؑ&uVZ֙Vf$3BŖEnF_Wֱ9jQxTE*Rv8lgp{z1_4޻~|'W?w@YwG{O;;2[XK1w֏˩lgm)@d$6Kp{zw4٬\ Z+U9WM̙!Szd&Q\PRx gಮ-UO(>EE"c*H2ReRYQIBCpʽzK40vCFThBc9ZN)jFD)ĖQH,!h{J56 Cզ$+|Gr g f/ndBc\q>H`s` \ljm:Ud#W/fP$ ]MB@H R(Jd[uTiYhN>" Ħny:K&TIUHPI@HQ*Pa&1PV2P-.:$A9H1XACK!DQUK;S]׍89Aq2)[9EC$AK]Pj-,slimZbWL|;HPUDĪzO[ A7u6]ؿ+>lM&{:2X$ F֩YeScEì*ua'-BJ%*R X2qZVSHuT5)WʌPSJ0DN>cI (jTR:Y2CZr!c"&g$QG|wPOO˽{/{CǾ?{7@/t,B60Ut԰bn>D![TӚ,N~X)qs^fSP-¨Pd*< DAZTޝ"ĐFȤ#EU&`UU)D,AF\OJk`^ UqTUrq/*⭇:vS+ IGf( )-BQ( EJ(d@p\ 16*%̮@ի&"J=hZJeE2!cMQNTMk桰pTAׇE$*؈S aRiY͘(]Lkh]tggqB!s"\wל1q7ǽ/hK;ZHDCV8L闡߉ZN 6׊xɬ3F4ttDm* XD-dv ⮡B{&;vMBNEK 7e n  /k>ٰfsbst9FNGBBHQ2QV&qEE11k2Pzȋ&uH@:C\K*"fP ^)%ҔH;%$:+ޛ~$1_y0Wi[{Bn&lO=Tkan8qm7" 2t;WkGC2 [go|$$ oжT"dj22* @m&yj0W :XbY h3}xn2R5LUfFI_c@zuiYz0oK 74'M3BPHd )QtDV R03a].Cxruw~_p2kNw:>ǩ}qaKL}88;~kؖE7o87V:R4tWZ2 A/ u*HDTD%0&çıJ0 57j'lD%De;bرDRTgҲ*CS>P)httbQ:Z.f37M_ \*XjI«(VE4/ڥ^¨o 'CPm6ě_1w>1qxd ^,EVnѸdd|ΦVގ"[tx+|fce1a'$*D"TqAIa5mtJRJ$k5jځˡ:&U#y upYi0(:&] .Oսή.(U]B'Rц -Ѐac9 IDATNGլCP08)s: cN5րSfydʦ3ùftxn _b.rF:A|l~;b}?,JNFcP4{ ˗X|]?or16.Dej ("=(h&\yX{t34ty^V*L%6#* F/J{Ƕꖤ֫$Sn9A/WcNGTTjXI A2r8Mf*4>!:wupGQ4vPj`xf?|-w=xxm'k@-L绍v\sѬA1q  Tpʘj4![yU{"!xV*S-Q@h (WFrq/@ppFf)8 Z“3 d@H!0Z˪*c >ڵ|OPWcUx3_l֘njPZY+"BR?sEnk/>:p!&6۠}zKzU%/f:jE~3-Ӳ>8 Wlhh$;[%֠l@$J+aKQ}q-ȫjֽδzSPU.(ie2sK~Z֖mUP4->k 3ծ6A,R6ʠ ",QrDO8 H)5yrgzFf5ڂ6T"Ĩꃄtc'Z_65[!uK}+D}~msFSO,g^&@B)CdaaPΑtom BfqWKJ.닃h*aXEZԝaMf :MnvHs G EES+WČ2eRkQ )>3#:UuQG,Q€X)4bT3u"`愙ID5h†:Gb(/fQ-Na.}{ؑ'?)ҔqG* "odT&N2vB×QD$*[fZ\n!&"fcs7 UO0{.Ē\uPͷSm?%*qUL;&QE1m8Oiqk,CW'9<km(ќ:'VU1M,Ԫլ1 :e˜(oKֱ}ܒq 4-#2(cls!㌥6F"Xx3b6̛sZF>h&"rJ*y$9î`)zF<ʨL_.;7yՂHV˟]-/, BIi%dq%<1 *D701f:?XTjp*0w}Zx5Zh T'GFB͌4s`2#:c!T%啘5-`BB82,FyyS ' R3W4rdΆ[f*3t!LO8T1&X :0F lb$cQ+ nQGAMCuxBR*b$Q&V6o)%uXըw+A"hOנ%UM,@UV%Ue*X\M[Q|T?AOs-e܌Ԡp4{In(XVQ1 Q As #sQ bL6 f2TM]dUT^ Q? [` };P<4oX j0lalh!w.X.3aIĽAUW:`$; o^vsrX0mL=B4AL$Մ%!0 FHc>.L/#-3\KcrR%|-F=U3e"Gb6Ġ;B%@\? E:o"ꅇjrqAF(0` @F:k6A8c$QӜD g9[ygJM)I@OO@lGBk;B4Ď)Tʬ%B zX1<Gwߖ/Ɗ%EaN1J;@*i$&g cYf!vLda"k䆟nnp.Fs5h'TLErMZLU[$F[CeaUDP JjWdMkwgO<->`ؓ\zϖM:f⊉X+":_h2$1HMG U%}xxڗbxP൨fVe(sai!qeaq1 a(#!xA/Aw Miln|OT٢gcRu0R(X*J/JaM Ҩ&ġgG#e :j SIuԎk[v6hwv^+hJ KV@@E)KMz[z5>WQͨ !@RHHF1VhP2ԨDX$R,NQnŁn3]oWI}򞤃_xRwAEF "R4H(c)PրI@>D@˅^V_Ӿ).x?*⑰>gހIh WdQPRZ3SQiUDT!b^RTTrgzazm"hY3yTռ.pg6h{svj^/ZRRܲOb)%BU!WW>(O[Tc("QQl} *1*ΎlDT][SF*<>l1yQ #gD T/"E`kMEdwn蓥Js,WEb145pBL~Akj@Nz1y}sXTѩ:!o'J]K\G3k\5EXBV5Ѩgƾy|(KAUˉMo˾2to~ư{ e:T 2~ Gx5h'xM6b1chИ+&֊K?a0mYΦ!F>B1e<>i풕i}++>jP77v!Шw_yT]m/xbk[94Д5/[xa[G^u%>䭇-e>\zG5%2)R0#PjHKA17vi@:ӽ"D ډ[aŻPRQ?tEyؙ2 Ynr͙90h&='2VjJMycsڏS+DxӉwc2Ov}YwOϠ:!;l O;iu5k'ak o}W)&wLTzL4b\{5QǶ ڦJD̘ؗn>vbu]3?<14R@m4V{Hl-j{cSg7]*WG%#UxPy<( s&y«7I1xJq`.s24Vks6Im7/ߩ+Ρ=7w*fڻ[NZeD#!I,HAd +Hֈj.)8"@T\$E0zk# Q:dzQHyӵwBWL5K%ʑ3s.݊{eQ7+'Ltyv&|4fU*ؔay޿?k\_35^tͷ42Gh QB@Bg;t>pO;h&&F,O5v/A1\n^>uC~GQh}ƙׄs9>kwN_9>x&i詯7\;=#8+ݟXi#NB#oːVĨaVgqCB#E?%nKzLE4Tq=36̎$A9'dmv x|,^pQ%<7GKX靤|~ǚW{=ٍf X>kgV-#(W8KFUL+ joItR>X y[&UH$"ve!@!)/r;Gv8 #Es:z44 '0"S2%ˉZxj5jm*L`Ӕu-iIAZ:0o?4 66{lcTlfʐ$j(.Pc fBV|[hs_^6{W{2:ד ˷so@u_LR:1#VC?sCÑ՚T0>vl`d6fd\n0a5( Ž 9XYB0**+ա,BLS|-!Ĝz?ٻ'&YT@T% cP,1. *,@ (eҐ$3Q<mf$/J;nuL~2qza;{_xM;bgԡ/O-y7@MqbZ~*Y/ͣg`upwML^}ɡ<ݼL>Pb6:93=!/r;Y=Ι(&Rۅ 3aU 451-ɎhLw#i[V:wkx[ XL=|޷ ]Rzo-}-NeoVm p~tt"R;lX_[|_-ӤqqT'x˞X9Iy%ٞٔWדp둖_?εhm7F@ 1ҝTѸw^zl Nw4ܴ.t;ٮ"*M"ʌ1SQr u_&g)9<,Vgj|d"yw-tV67~kܱmf ]\}'6IT'Ep38K+Oj~ԝ;rmJ1.Ъ* +.Q%; կۏ|.]{J+F@E?)/l)p>nGIpFh"&<,OiT [\T܂X.jyTH\su/ѵsgV6|.c>[[vt]07#b.À{6FhsB}62Cxm7n/8NrÛe%|;|pnz{V6ՃK(:a~ O,Hjccp8toغLkq~6/<5]Y:OThɕrHw<(ƅVf?%\PʥKvagSFK-,p6gˑI(p9,$̺F:3y^1N>|g~κ;|[g}Q~`[j/ Շq?}T]gG0f}rOe%qy |@u,ά7өL{5Mlr".t{g؜yoy3uMM׾Yh|!umky~?p'N'^fIDAT/g?~g_W7N\cnOW%54OUh@0w\y$P 9+t֭=vm7M#T4WŬc~ώ T[r"3IDr7#-T< m,L6[N:`\_f4 Tg8Qlؙw#Os1Z"I,~^ 3l['4;Ls7_wwx1xݡ굛1KSBI think i killed santa! :O.

\n", "displayName": "O M G", "id": "https://example.com/api/image/Pi9rux49S6C1Yhta0zbxyz", "author": { "objectType": "person", "id": "acct:testuser@example.com", }, "image": { "url": "https://example.com/uploads/testuser/2013/12/24/XMAS13_thumb.jpg", "height": 240, "width": 320, }, "fullImage": { "url": "https://example.com/uploads/testuser/2013/12/24/XMAS13.jpg", "width": 1280, "height": 960, }, "objectType": "image", "published": "2013-12-24T23:23:11Z", "updated": "2013-12-24T23:23:13Z", "links": { "self": { "href": "https://example.com/api/image/Pi9rux49S6C1Yhta0zbxyz" } }, "likes": { "url": "https://example.com/api/image/Pi9rux49S6C1Yhta0zbxyz/likes", "totalItems": 0, }, "replies": { "url": "https://example.com/api/image/Pi9rux49S6C1Yhta0zbxyz/replies", "totalItems": 0, }, "shares": { "url": "https://example.com/api/image/Pi9rux49S6C1Yhta0zbxyz/shares", "totalItems": 0, }, "liked": "false", "pump_io": { "shared": "false", }, } self.mini_data = { "objectType": "image", "id": "foo", "image": { "url": "https://example.com/uploads/testuser/2013/12/24/XMAS13_thumb.jpg", "height": 240, "width": 320, } } self.response.data = { "verb": "post", "object": self.imgdata, } def test_create_empty(self): image = self.pump.Image() # object to string self.assertEqual(image.__str__(), 'image by unknown') def test_mini_unserialize(self): image = self.pump.Image().unserialize(self.mini_data) # object is Image instance self.assertTrue(isinstance(image, type(self.pump.Image()))) # Test unserialization is correct self.assertEqual(image.id, self.mini_data["id"]) # Test that both original and thumbnail gets information from data["image"] # if data["fullImage"] does not exist. self.assertEqual(image.thumbnail.url, self.mini_data["image"]["url"]) self.assertEqual(image.original.url, self.mini_data["image"]["url"]) self.assertEqual(image.thumbnail.height, self.imgdata["image"]["height"]) self.assertEqual(image.thumbnail.width, self.imgdata["image"]["width"]) self.assertEqual(image.original.height, self.imgdata["image"]["height"]) self.assertEqual(image.original.width, self.imgdata["image"]["width"]) def test_unserialize(self): """ Tests image unserialization is successful """ # Make the image object image = self.pump.Image().unserialize(self.imgdata) # object is Image instance self.assertTrue(isinstance(image, type(self.pump.Image()))) # object to string self.assertEqual(image.__str__(), 'image by testuser@example.com') # Test unserialization is correct self.assertEqual(image.id, self.imgdata["id"]) self.assertEqual(image.url, self.imgdata["url"]) self.assertEqual(image.thumbnail.url, self.imgdata["image"]["url"]) self.assertEqual(image.original.url, self.imgdata["fullImage"]["url"]) self.assertEqual(image.display_name, self.imgdata["displayName"]) self.assertEqual(image.content, self.imgdata["content"]) self.assertEqual(image.thumbnail.height, self.imgdata["image"]["height"]) self.assertEqual(image.thumbnail.width, self.imgdata["image"]["width"]) self.assertEqual(image.original.height, self.imgdata["fullImage"]["height"]) self.assertEqual(image.original.width, self.imgdata["fullImage"]["width"]) def test_upload_file(self): """ Test image can be uploaded succesfully """ image = self.pump.Image( display_name="My lovely image", content="This is my sexy description" ) # Check image has my attributs as they were set self.assertEqual(image.display_name, "My lovely image") self.assertEqual(image.content, "This is my sexy description") # Upload an image from the bucket image.from_file(self.bucket.path_to_png) # Test the data sent is correct. upload_request = self.requests[0] # It always happens to be the first request # Test that the data is the same binary_image = open(self.bucket.path_to_png, "rb").read() self.assertEqual(upload_request.data, binary_image) PyPump-0.7/tests/mapper_test.py000066400000000000000000000043411274522221600166560ustar00rootroot00000000000000from __future__ import absolute_import from pypump.models import Mapper, PumpObject from pypump.models.activity import Application from tests import PyPumpTest class MapperTest(PyPumpTest): def setUp(self): super(MapperTest, self).setUp() def test_get_object_unknown(self): """ Test creation of unknown activity model """ test_data = { "objectType": "food", # pypump.models.PumpObject "id": "https://example.com/api/food/pancake-v0.1a", "url": "https://example.com/food/pancake-v0.1a", "content": "flour, sugar, eggs, milk, beans", "displayName": "Pancakes (test version)", "author": { "objectType": "person", "id": "acct:badcook@example.com" } } test_obj = Mapper(pypump=self.pump).get_object(test_data) # Test unserialization is correct self.assertEqual(test_obj.object_type, test_data["objectType"]) self.assertEqual(test_obj.id, test_data["id"]) self.assertEqual(test_obj.url, test_data["url"]) self.assertEqual(test_obj.content, test_data["content"]) self.assertEqual(test_obj.display_name, test_data["displayName"]) # test_obj should be PumpObject self.assertTrue(isinstance(test_obj, PumpObject)) # test_obj.author should be PyPump.Person self.assertTrue(isinstance(test_obj.author, type(self.pump.Person()))) def test_get_pump_model(self): """ Test creation of PyPump model """ test_data = { "objectType": "person", # pypump.models.person.Person "id": "acct:testuser@example.com" } test_obj = Mapper(pypump=self.pump).get_object(test_data) # Test person model was made self.assertTrue(isinstance(test_obj, type(self.pump.Person()))) def test_get_activity_model(self): """ Test creation of known activity model """ test_data = { "objectType": "application", # pypump.models.activity.Application "id": "coolapp1.2", "displayName": "Cool app" } test_obj = Mapper(pypump=self.pump).get_object(test_data) self.assertTrue(isinstance(test_obj, Application)) PyPump-0.7/tests/note_test.py000066400000000000000000000112311274522221600163330ustar00rootroot00000000000000from __future__ import absolute_import from tests import PyPumpTest from dateutil.parser import parse class NoteTest(PyPumpTest): def setUp(self): super(NoteTest, self).setUp() self.maxidata = { "displayName": "note title", "content": "

note text

\n", "objectType": "note", "published": "2013-12-23T05:14:54Z", "updated": "2013-12-23T05:14:54Z", "links": { "self": { "href": "https://example.com/api/note/8f40pLbdTQ-uY-ADbQrhwg" } }, "likes": { "url": "https://example.com/api/note/8f40pLbdTQ-uY-ADbQrhwg/likes", "totalItems": 0 }, "replies": { "url": "https://example.com/api/note/8f40pLbdTQ-uY-ADbQrhwg/replies", "totalItems": 0 }, "shares": { "url": "https://example.com/api/note/8f40pLbdTQ-uY-ADbQrhwg/shares", "totalItems": 0 }, "url": "https://example.com/testuser/note/8f40pLbdTQ-uY-ADbQrhwg", "id": "https://example.com/api/note/8f40pLbdTQ-uY-ADbQrhwg", "liked": False, "pump_io": { "shared": False }, "to": [{ "objectType": "person", "id": "acct:notetestuser@example.com" }], "cc": [{ "objectType": "collection", "id": "http://activityschema.org/collection/public" }], } self.minidata = { "objectType": "note", "id": "https://example.com/api/note/8f40pLbdTQ-uY-ADbQrhwg", } # used in test_note_attr_* self.maxinote = self.pump.Note().unserialize(self.maxidata) def test_note_create(self): self.response.data = self.maxidata note = self.pump.Note('test') # object is Note instance self.assertTrue(isinstance(note, type(self.pump.Note()))) # object to string self.assertEqual(note.__str__(), 'note by unknown') def test_note_minimal_unserialize(self): note = self.pump.Note().unserialize(self.minidata) self.assertTrue(isinstance(note, type(self.pump.Note()))) def test_note_unserialize(self): note = self.pump.Note().unserialize(self.maxidata) self.assertTrue(isinstance(note, type(self.pump.Note()))) def test_note_attr_display_name(self): self.assertTrue(hasattr(self.maxinote, 'display_name')) self.assertEqual(self.maxinote.display_name, self.maxidata["displayName"]) def test_note_attr_content(self): self.assertTrue(hasattr(self.maxinote, 'content')) self.assertEqual(self.maxinote.content, self.maxidata["content"]) def test_note_attr_published(self): self.assertTrue(hasattr(self.maxinote, 'published')) self.assertEqual(self.maxinote.published, parse(self.maxidata["published"])) def test_note_attr_updated(self): self.assertTrue(hasattr(self.maxinote, 'updated')) self.assertEqual(self.maxinote.updated, parse(self.maxidata["updated"])) def test_note_attr_links(self): self.assertTrue(hasattr(self.maxinote, 'links')) self.assertEqual(self.maxinote.links['self'], self.maxidata["links"]["self"]["href"]) def test_note_attr_url(self): self.assertTrue(hasattr(self.maxinote, 'url')) self.assertEqual(self.maxinote.url, self.maxidata["url"]) def test_note_attr_id(self): self.assertTrue(hasattr(self.maxinote, 'id')) self.assertEqual(self.maxinote.id, self.maxidata["id"]) def test_note_attr_liked(self): self.assertTrue(hasattr(self.maxinote, 'liked')) self.assertEqual(self.maxinote.liked, self.maxidata["liked"]) def test_note_attr_to(self): self.assertTrue(hasattr(self.maxinote, 'to')) self.assertTrue(isinstance(self.maxinote.to[0], type(self.pump.Person()))) def test_note_attr_cc(self): self.assertTrue(hasattr(self.maxinote, 'cc')) self.assertTrue(isinstance(self.maxinote.cc[0], type(self.pump.Collection()))) # tests mixin methods from models/__init__.py isnt completely broken def test_note_like(self): note = self.pump.Note('test') note.like() def test_note_unlike(self): note = self.pump.Note('test') note.unlike() def test_note_share(self): note = self.pump.Note('test') note.share() def test_note_unshare(self): note = self.pump.Note('test') note.unshare() def test_note_delete(self): note = self.pump.Note('test') note.delete() PyPump-0.7/tests/person_test.py000066400000000000000000000140271274522221600167020ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import absolute_import import six from pypump.models.place import Place from tests import PyPumpTest class PersonTest(PyPumpTest): def setUp(self): super(PersonTest, self).setUp() self.response.data = { "id": "acct:TestUser@example.com", "preferredUsername": "TestUser", "url": "http://example.com/TestUser", "displayName": "Test Userson", "links": { "self": { "href": "http://example.com/api/user/TestUser/profile", }, "activity-inbox": { "href": "http://example.com/api/user/TestUser/inbox", }, "activity-outbox": { "href": "http://example.com/api/user/TestUser/feed", }, }, "objectType": "person", "followers": { "url": "http://example.com/api/user/TestUser/followers", "totalItems": 72, }, "following": { "url": "http://example.com/api/user/TestUser/following", "totalItems": 27, }, "favorites": { "url": "http://example.com/api/user/TestUser/favorites", "totalItems": 720, }, "location": { "objectType": "place", "displayName": "Home Tree, Pandora", }, "summary": "I am a PyPump Test user, I am used for testing!", "image": { "url": "http://example.com/uploads/TestUser/some_image.jpg", "width": 96, "height": 96, }, "pump_io": { "shared": False, "followed": False, }, "updated": "2013-08-13T10:26:54Z", "liked": False, "shares": { "url": "http://example.com/api/person/BlahBlah/shares", "items": [], } } def test_person(self): person = self.pump.Person("TestUser@example.com") # is a Person object self.assertTrue(isinstance(person, type(self.pump.Person()))) # repr self.assertEqual(person.__repr__(), '') # string self.assertEqual(person.__str__(), self.response['displayName']) # unicode person.display_name = u'Test användarson' if six.PY3: self.assertEqual(person.__str__(), person.display_name) else: self.assertEqual(person.__str__(), person.display_name.encode('utf-8')) def test_follow(self): """ Tests that pypump sends correct data when attempting to follow a person """ person = self.pump.Person("TestUser@example.com") # PyPump now expects the object returned back to it self.response.data = { "actor": {"objectType": "person", "id": "acct:foo@bar"}, "verb": "follow", "object": self.response.data } person.follow() # Test verb is 'follow' self.assertEquals(self.request["verb"], "follow") # Test ID is the correct ID self.assertEquals(person.id, self.request["object"]["id"]) # Ensure object type is correct self.assertEquals(person.object_type, self.request["object"]["objectType"]) def test_update(self): """ Test that a update works """ person = self.pump.Person("TestUser@example.com") person.summary = "New summary!" person.display_name = "New user" self.response.data = { "verb": "update", "actor": {"objectType": "person", "id": person.id}, "object": { "id": person.id, "summary": person.summary, "displayName": person.display_name, "objectType": "person", }, } person.update() self.assertEqual(self.request["verb"], "update") self.assertEqual(self.request["object"]["id"], person.id) self.assertEqual(self.request["object"]["objectType"], person.object_type) self.assertEqual(self.request["object"]["summary"], person.summary) self.assertEqual(self.request["object"]["displayName"], person.display_name) def test_unfollow(self): """ Test that you can unfollow a person """ person = self.pump.Person("TestUser@example.com") self.response.data = { "actor": {"objectType": "person", "id": "acct:foo@bar"}, "verb": "stop-following", "object": self.response.data } person.unfollow() self.assertEquals(self.request["verb"], "stop-following") self.assertEquals(self.request["object"]["id"], person.id) self.assertEquals(self.request["object"]["objectType"], person.object_type) def test_minimal_unserialize(self): """ Test the smallest amount of data can be given to unserialize """ self.response.data = { "id": "acct:TestUser@example.com", "objectType": "person", } person = self.pump.Person("TestUser@example.com") self.assertEquals(self.response["id"], person.id) self.assertEquals(self.response["objectType"], person.object_type) def test_unserialize(self): """ Tests person unserialization is successful """ # Make the person object person = self.pump.Person("TestUser@example.com") # Test unserialization is correct self.assertEqual(person.id, self.response["id"]) self.assertEqual(person.username, self.response["preferredUsername"]) self.assertEqual(person.display_name, self.response["displayName"]) self.assertEqual(person.url, self.response["url"]) self.assertEqual(person.summary, self.response["summary"]) # Test image model was made #self.assertTrue(isinstance(person.image, self.pump.Image)) # Test place model was made self.assertTrue(isinstance(person.location, Place)) PyPump-0.7/tests/place_test.py000066400000000000000000000020271274522221600164550ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import absolute_import import six from tests import PyPumpTest class PlaceTest(PyPumpTest): def setUp(self): super(PlaceTest, self).setUp() self.data = { "objectType": "place", "displayName": "Home Tree, Pandora", } def test_place_create(self): self.response.data = self.data place = self.pump.Place() # object is Place instance self.assertTrue(isinstance(place, type(self.pump.Place()))) # object to string self.assertEqual(place.__str__(), 'unknown') def test_place_unserialize(self): # unserialize place = self.pump.Place().unserialize(self.data) # object to string self.assertEqual(place.__str__(), self.data['displayName']) place.display_name = u'Malmö, Sweden' if six.PY3: self.assertEqual(place.__str__(), place.display_name) else: self.assertEqual(place.__str__(), place.display_name.encode('utf-8')) PyPump-0.7/tests/postable_test.py000066400000000000000000000024131274522221600172010ustar00rootroot00000000000000from __future__ import absolute_import from tests import PyPumpTest from pypump.models import Postable class PostableTest(PyPumpTest): def setUp(self): super(PostableTest, self).setUp() self.userdata = {"objectType": "person", "id": "acct:testuser@example.com"} self.collectiondata = {"objectType": "collection", "id": "http://activityschema.org/collection/public"} self.testuser = self.pump.Person().unserialize(self.userdata) self.testcollection = self.pump.Collection().unserialize(self.collectiondata) self.postable = Postable() self.postable._pump = self.pump def test_set_person(self): self.postable.to = self.testuser # is list item a pump person? self.assertTrue(isinstance(self.postable.to[0], type(self.pump.Person()))) def test_set_collection(self): self.postable.to = self.testcollection # is list item a pump collection? self.assertTrue(isinstance(self.postable.to[0], type(self.pump.Collection()))) def test_serialize(self): self.postable.to = [self.testuser, self.testcollection] data = self.postable.serialize() self.assertEqual(data["to"][0], self.userdata) self.assertEqual(data["to"][1], self.collectiondata) PyPump-0.7/tests/pumpobject_test.py000066400000000000000000000117051274522221600175440ustar00rootroot00000000000000from __future__ import absolute_import from tests import PyPumpTest from pypump.models import PumpObject class PumpObjectTest(PyPumpTest): def setUp(self): super(PumpObjectTest, self).setUp() self.model = PumpObject(pypump=self.pump) self.person_json = { "preferredUsername": "testuser", "url": "https://example.com/testuser", "displayName": "TestUser", "links": { "self": { "href": "https://example.com/api/user/testuser/profile" }, "activity-inbox": { "href": "https://example.com/api/user/testuser/inbox" }, "activity-outbox": { "href": "https://example.com/api/user/testuser/feed" } }, "objectType": "person", "updated": "2013-08-05T20:24:38Z", "published": "2013-03-26T18:00:09Z", "followers": { "url": "https://example.com/api/user/testuser/followers" }, "following": { "url": "https://example.com/api/user/testuser/following" }, "favorites": { "url": "https://example.com/api/user/testuser/favorites" }, "lists": { "url": "https://example.com/api/user/testuser/lists/person" }, "pump_io": {}, "location": { "displayName": "North Pole", "objectType": "place" }, "summary": "test summary", "liked": False, "image": { "url": "https://example.com/uploads/testuser/2013/3/27/n76Spw_thumb.jpg", "width": 96, "height": 96 }, "id": "acct:testuser@example.com" } self.note_json = { "objectType": "note", "content": "Test content", "published": "2013-12-22T06:27:13Z", "updated": "2013-12-22T06:27:13Z", "links": { "self": { "href": "https://example.com/api/note/CkFucl8qSmald3qAHTllTw" } }, "likes": { "url": "https://example.com/api/note/CkFucl8qSmald3qAHTllTw/likes", "totalItems": 0, "pump_io": { "proxyURL": "https://example.com/api/proxy/pjwd3nrLR4O_gBYCvAp4mQ" } }, "replies": { "url": "https://example.com/api/note/CkFucl8qSmald3qAHTllTw/replies", "totalItems": 0, "pump_io": { "proxyURL": "https://example.com/api/proxy/99TGqO0ISoazXh-Q_nTinQ" } }, "shares": { "url": "https://example.com/api/note/CkFucl8qSmald3qAHTllTw/shares", "totalItems": 0, }, "url": "https://example.com/testuser/note/CkFucl8qSmald3qAHTllTw", "liked": False, "pump_io": { "shared": False, "proxyURL": "https://example.com/api/proxy/wEPeXhnqRw2E8p0j68QO_g" }, "id": "https://example.com/api/note/CkFucl8qSmald3qAHTllTw" } def test_add_links_person(self): "add person object : _add_links(person)" test_obj = self.person_json self.model._add_links(test_obj) self.assertTrue(self.model.links.get('self')) self.assertEqual(self.model.links['self'], test_obj['links']['self']['href']) self.assertTrue(self.model.links.get('activity-inbox')) self.assertEqual(self.model.links['activity-inbox'], test_obj['links']['activity-inbox']['href']) def test_add_links_note(self): "add notes object : _add_links(note)" test_obj = self.note_json self.model._add_links(test_obj) self.assertTrue(self.model.links.get('self')) self.assertEqual(self.model.links['self'], test_obj['links']['self']['href']) def test_add_links_note_links(self): "add note's links object : _add_links(note['links'])" test_obj = self.note_json self.model._add_links(test_obj['links']) self.assertTrue(self.model.links.get('self')) self.assertEqual(self.model.links['self'], test_obj['links']['self']['href']) def test_add_links_note_shares_no_proxy(self): "note's shares link without a proxyurl" test_obj = self.note_json self.model._add_links(test_obj) self.assertTrue(self.model.links.get('shares')) self.assertEqual(self.model.links['shares'], test_obj['shares']['url']) def test_add_links_note_likes_proxy(self): "note's likes link with a proxyurl" test_obj = self.note_json self.model._add_links(test_obj) self.assertTrue(self.model.links.get('likes')) self.assertEqual(self.model.links['likes'], test_obj['likes']['pump_io']['proxyURL']) PyPump-0.7/tests/pypump_test.py000066400000000000000000000032741274522221600167300ustar00rootroot00000000000000from __future__ import absolute_import from unittest import TestCase try: from unittest import mock except ImportError: import mock from requests import exceptions as request_excs from pypump import PyPump class PyPumpTest(TestCase): @mock.patch("pypump.pypump.requests") def test_https_failover(self, requests_mock): store = mock.MagicMock() store.__iter__.return_value = [] client = mock.Mock() verifier = mock.Mock() requests_mock.post.return_value.text = "%s=thingy&%s=secretthingy" % (PyPump.PARAM_TOKEN, PyPump.PARAM_TOKEN_SECRET) # re-add exceptions to mocked library requests_mock.exceptions = request_excs pump = PyPump(client, verifier, store=store) self.assertEqual(pump.protocol, "https") # verify == True fnc_mock = mock.Mock() fnc_mock.side_effect = request_excs.ConnectionError with self.assertRaises(request_excs.ConnectionError): pump._requester(fnc_mock, "") self.assertEqual(len(fnc_mock.call_args_list), 1) self.assertTrue(fnc_mock.call_args_list[0][0][0].startswith("https://")) self.assertEqual(pump.protocol, "https") # verify == False fnc_mock.reset_mock() pump.verify_requests = False with self.assertRaises(request_excs.ConnectionError): pump._requester(fnc_mock, "") self.assertEqual(len(fnc_mock.call_args_list), 2) self.assertTrue(fnc_mock.call_args_list[0][0][0].startswith("https://")) self.assertTrue(fnc_mock.call_args_list[1][0][0].startswith("http://")) # make sure that we're reset to https after self.assertEqual(pump.protocol, "https") PyPump-0.7/tests/store_test.py000066400000000000000000000062171274522221600165320ustar00rootroot00000000000000from __future__ import absolute_import import os import shutil import stat from pypump import AbstractStore, JSONStore from tests import PyPumpTest class TestStore(AbstractStore): """ Provide a more testable store """ save_called = False def save(self): """ Should save data in store This will just change a flag to show it's been called """ self.save_called = True class StoreTest(PyPumpTest): """ Test the store class """ def test_store_and_get(self): """ Test that a value can be stored and then retrived """ store = TestStore() # Check that it raises a key error when nothing has been stored. def empty_store_key(): store["coffee"] self.assertRaises(KeyError, empty_store_key) # Store something store["coffee"] = "awesome" # Check we can get the same value back out self.assertEqual(store["coffee"], "awesome") def test_save_on_set(self): """ Test that save is called when a value is set """ store = TestStore() # Check that save hasn't been called yet. self.assertEqual(store.save_called, False) # set some information store["coffee"] = "awesome" # Check save has been called self.assertEqual(store.save_called, True) def test_prefix(self): """ Test that the prefix is applied to the get and set keys """ store = TestStore() store.prefix = "hai" # Test that we can store something and get it back store["key"] = "value" self.assertEqual(store["key"], "value") # Remove the prefix and check that we have to manually prefix # the key to get back the previously stored value. store.prefix = None # Unprefixed shouldn't exist. def empty_store_key(): store["key"] self.assertRaises(KeyError, empty_store_key) self.assertEqual(store["hai-key"], "value") class JSONStoreTest(PyPumpTest): """ Test the JSON implementation of the store class """ def setUp(self): filename = os.path.abspath(".") filename = os.path.join(filename, "pypumpstoretest.json") self.assertFalse(os.path.exists(filename), "{0} already exists".format(filename)) self.filename = filename def tearDown(self): try: os.remove(self.filename) except OSError: pass def test_creating_pypump_dir(self): os.environ["XDG_CONFIG_HOME"] = os.path.join(os.path.abspath("."), "pypump_config") self.assertFalse(os.path.exists(os.environ["XDG_CONFIG_HOME"]), "{0} already exists".format(os.environ["XDG_CONFIG_HOME"])) store = JSONStore() store["unittest"] = "framework" shutil.rmtree(os.environ["XDG_CONFIG_HOME"], ignore_errors=True) def test_permissions(self): store = JSONStore(filename=self.filename) store["unittest"] = "framework" mode = os.stat(self.filename).st_mode # we're only going to test to make sure "others" can't read the file self.assertEqual(mode & stat.S_IRWXO, 0, "File mode is insecure") PyPump-0.7/tests/webpump_test.py000066400000000000000000000011411274522221600170440ustar00rootroot00000000000000from __future__ import absolute_import import six from six.moves.urllib import parse from tests import PyPumpTest class WebPumpTest(PyPumpTest): """ Tests to ensure the WebPump works as it should do """ def test_url_is_set(self): """ Tests that URL is provided with token for OAuth """ self.assertTrue(isinstance(self.webpump.url, six.string_types)) url = parse.urlparse(self.webpump.url) query = parse.parse_qs(url.query) self.assertEqual(url.netloc, self.webpump.client.webfinger.split("@", 1)[1]) self.assertEqual(url.path, "/oauth/authorize") PyPump-0.7/tox.ini000066400000000000000000000003331274522221600141270ustar00rootroot00000000000000[tox] envlist = docs [testenv:docs] basepython=python changedir=docs deps=sphinx commands=sphinx-build -W -b html -d _build/doctrees . _build/html [flake8] max-line-length = 120 exclude = .git,env,build,docs/conf.py